Python programming 8 Music with mouse-drawn lines by PyGame
I revised my previous program "Small interactive music maker" and now melody is made from mouse-drawn lines.
Actually, change is not big. Instead of my Fourier series function, two key functions are included as
- draw lines with mouse by pygame
- from line to fx
Drawing lines from below.
https://www.dreamincode.net/forums/topic/401541-buttons-and-sliders-in-pygame/
This must be some pretty fun for kids!
----------------------------------------------------------------------------------------------
blog 8
Some versions of pygame may have some bug.
Need "pip install pygame==2.0.0.dev6"
import pygame, math, sys, time import pygame.midi # use pygame.midi (not Mido) from random import random, choice pygame.init() # initialize Pygame pygame.midi.init() # initialize Pygame MIDI port = pygame.midi.Output(0) # port open port.set_instrument(53, channel = 0) # Voice port.set_instrument(0, channel = 1) # piano port.set_instrument(33, channel = 2) # bass X = 900 # screen width Y = 600 # screen height WHITE = (255, 255, 255) DARK = (70,70,85) BLACK = (0, 0, 0) RED = (255, 50, 50) BLUE = (50, 50, 255) GREY = (100, 90, 100) TRANS = (1, 1, 1) # sample chord progressions chord_progression = [ ["C","Am","Dm7","G7","C","Am","Dm7","G7"], ["Em","Am","F","G","Em","Am","F","G"], ["F","C","G","Am","F","C","G","Am"], ["F","G","Em","Am","F","G","Em","Am"], ["C","Am7","Dm7","G7","C","Am7","Dm7","G7"], ["Am","F","C","G","Am","F","C","G"], ["Am","F","G","C","Am","F","G","C"], ["Am","Dm7","G7","C","Am","Dm7","G7","C"], ["Em","F","G","Am","Em","F","G","Am"], ["FM7","E7","Am7","C7","FM7","E7","Am7","C7"]] chord_progression_p = [ [[60, 64, 67], [69, 72, 76], [62, 65, 69, 72], [67, 71, 74, 77], [60, 64, 67], [69, 72, 76], [62, 65, 69, 72], [67, 71, 74, 77]], [[64, 67, 71], [69, 72, 76], [65, 69, 72], [67, 71, 74], [64, 67, 71], [69, 72, 76], [65, 69, 72], [67, 71, 74]], [[65, 69, 72], [60, 64, 67], [67, 71, 74], [69, 72, 76], [65, 69, 72], [60, 64, 67], [67, 71, 74], [69, 72, 76]], [[65, 69, 72], [67, 71, 74], [64, 67, 71], [69, 72, 76], [65, 69, 72], [67, 71, 74], [64, 67, 71], [69, 72, 76]], [[60, 64, 67], [69, 72, 76, 79], [62, 65, 69, 72], [67, 71, 74, 77], [60, 64, 67], [69, 72, 76, 79], [62, 65, 69, 72], [67, 71, 74, 77]], [[69, 72, 76], [65, 69, 72], [60, 64, 67], [67, 71, 74], [69, 72, 76], [65, 69, 72], [60, 64, 67], [67, 71, 74]], [[69, 72, 76], [65, 69, 72], [67, 71, 74], [60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74], [60, 64, 67]], [[69, 72, 76], [62, 65, 69, 72], [67, 71, 74, 77], [60, 64, 67], [69, 72, 76], [62, 65, 69, 72], [67, 71, 74, 77], [60, 64, 67]], [[64, 67, 71], [65, 69, 72], [67, 71, 74], [69, 72, 76], [64, 67, 71], [65, 69, 72], [67, 71, 74], [69, 72, 76]], [[65, 69, 72, 76], [64, 68, 71, 74], [69, 72, 76, 79], [60, 64, 67, 70], [65, 69, 72, 76], [64, 68, 71, 74], [69, 72, 76, 79], [60, 64, 67, 70]]] # Melody on scame C major C_major_scale = [p + octave * 12 for octave in [0,1,2,3,4,5,6,7,8,9] for p in [0, 2, 4, 5, 7, 9, 11]] # handwritten line => f(x) (f(x) => pitch in below) def fx1(x, cycle): pos_x = x%cycle * 900/cycle pos_y = 0 if len(play_positions) == 0: pos_y = 0 elif pos_x < play_positions[0][0][0]: pos_y = play_positions[0][0][1] elif play_positions[-1][1][0] < pos_x: pos_y = play_positions[-1][1][1] else: for i in range(len(play_positions)): if pos_x == play_positions[i][0][0]: pos_y = play_positions[i][0][1] break elif play_positions[i][0][0] < pos_x and pos_x <= play_positions[i][1][0]: pos_y = (play_positions[i][0][1] * (pos_x - play_positions[i][0][0]) + play_positions[i][1][1] * (play_positions[i][1][0] - pos_x))\ / (play_positions[i][1][0] - play_positions[i][0][0]) break elif i < len(play_positions)-1 and play_positions[i][1][0] < pos_x and pos_x < play_positions[i+1][0][0]: pos_y = play_positions[i][1][1] break return (240-pos_y)/250 # find nearlest pitch on scale def nearest_on_scale(p,scale): if abs(p-scale[sum([p>x for x in scale])-1]) <= abs(p-scale[sum([p>x for x in scale])]): pOnS = scale[sum([p>x for x in scale])-1] else: pOnS = scale[sum([p>x for x in scale])] return pOnS # Draw Fourier series wave def draw_wave(): play_positions.sort() for tp in play_positions: pygame.draw.line(screen, WHITE, tp[0], tp[1], 1) # button on screen class Button(): def __init__(self, txt, location, bg=GREY, fg=WHITE, size=(80, 40), font_name="None", font_size=30): self.bg = bg # actual background color, can change on mouseover self.fg = fg # text color self.size = size self.font = pygame.font.SysFont(font_name, font_size) self.txt = txt self.txt_surf = self.font.render(self.txt, 1, self.fg) self.txt_rect = self.txt_surf.get_rect(center=[s//2 for s in self.size]) self.surface = pygame.surface.Surface(size) self.rect = self.surface.get_rect(center=location) def draw(self): self.surface.fill(self.bg) self.txt_surf = self.font.render(self.txt, 1, self.fg) self.surface.blit(self.txt_surf, self.txt_rect) screen.blit(self.surface, self.rect) # slider on screen class Slider(): def __init__(self, name, val, maxi, mini, pos, pos1 = 550): self.val = val # start value self.maxi = maxi # maximum at slider position right self.mini = mini # minimum at slider position left self.xpos = pos # x-location on screen self.ypos = pos1 self.surf = pygame.surface.Surface((100, 50)) self.hit = False # the hit attribute indicates slider movement due to mouse interaction self.txt_surf = font.render(name, 1, BLACK) self.txt_rect = self.txt_surf.get_rect(center=(50, 15)) # Static graphics - slider background # self.surf.fill(GREY) pygame.draw.rect(self.surf, WHITE, [10, 28, 80, 10], 0) self.surf.blit(self.txt_surf, self.txt_rect) # this surface never changes # dynamic graphics - button surface # self.button_surf = pygame.surface.Surface((20, 20)) self.button_surf.fill(TRANS) self.button_surf.set_colorkey(TRANS) pygame.draw.circle(self.button_surf, BLACK, (10, 10), 6, 0) pygame.draw.circle(self.button_surf, RED, (10, 10), 4, 0) def draw(self): """ Combination of static and dynamic graphics in a copy of the basic slide surface """ # static surf = self.surf.copy() # dynamic pos = (10+int((self.val-self.mini)/(self.maxi-self.mini)*80), 33) self.button_rect = self.button_surf.get_rect(center=pos) surf.blit(self.button_surf, self.button_rect) self.button_rect.move_ip(self.xpos, self.ypos) # move of button box to correct screen position # screen screen.blit(surf, (self.xpos, self.ypos)) def move(self): """ The dynamic part; reacts to movement of the slider button. """ self.val = (pygame.mouse.get_pos()[0] - self.xpos - 10) / 80 * (self.maxi - self.mini) + self.mini if self.val < self.mini: self.val = self.mini if self.val > self.maxi: self.val = self.maxi # show chord name on display def show_chord_name(font,chord_list): pygame.draw.rect(screen, (88,78,88), [20, 530, 850, 60], 0) for i in range(8): text1 = font.render(chord_list[i], True, WHITE) screen.blit(text1, (60+105*i,550)) # print pitch name def pitch_name(num, sharp = 0): if num > 0: if sharp == 0: tone = ["C","Df","D","Ef","E","F","Gf","G","Af","A","Bf","B"][num % 12] else: tone = ["C","Cs","D","Ds","E","F","Fs","G","Gs","A","As","B"][num % 12] octave = ["_1","0","1","2","3","4","5","6","7","8","9"][int(num/12)] return tone+octave else: return "REST" # update whole screen def update_screen(): screen.fill(DARK) for p0 in [61,63,66,68,70,73,75,78,80,82]: # piano black keys y0 = int((p0-72)*(-250)/12+240) # for chart pygame.draw.line(screen,(50,50,65),(0,y0),(899,y0),20) draw_wave() show_chord_name(font,chord_progression[ch]) for s1 in slides: s1.draw() for b in buttons: b.draw() text1 = font.render(str(bpm.val), True, WHITE) screen.blit(text1, (50,100)) # Update melody def update_melody(): global position_list, play_positions if len(position_list) > 0: play_positions = sorted(position_list) position_list = [] ############################################################################### # initialize # display setting font = pygame.font.SysFont(None, 30) screen = pygame.display.set_mode((X, Y)) clock = pygame.time.Clock() # sliders bpm = Slider("bpm", 120, 180, 60, 20, 60) slides = [bpm] # buttons b0 = Button("Start", (60, 30),bg=(50,70,70)) b1 = Button("Stop", (840, 30),bg=(108,75,75)) b2 = Button("Change", (60, 510),bg=(70,60,70)) b3 = Button("Back", (150, 510),bg=(70,60,70)) b4 = Button("Melody", (60, 170),bg=(70,60,70)) b5 = Button("Repeat", (180, 30),bg=(70,60,70),size=(100, 40)) buttons = [b0, b1, b2, b3, b4, b5] # music setting music_start = False x0 = 0 prev_p0 = 0 pitch_length = 0 ch = 0 # selected chord progression ch_i = 0 # ith chord on progression prev_ch_i = 0 # previous ith chord on progression position_list = [] play_positions = [] update_screen() prev_time = 0 # draw line last_pos = None min_x = 0 drawing = False ############################################################################### # start loop while True: # music Start if music_start == True: # refresh at righthand side if x0 % 64 == 0: if b5.txt == "Update": update_melody() ch = choice([0,1,2,3,4,5,6,7,8,9]) else: position_list = [] update_screen() # i-th chord ch_i = int(x0/8)%8 # melody pitch and display coordinates x_0 = int(x0*900/64)%900 x_1 = x_0 + int(900/64) p = int(fx1(x0,64)*12+72) # pitch # select from chord notes or from scale by chance if random() < 0.7: cp = chord_progression_p[ch][ch_i] chord_pitches = [p + 12*oct for p in cp for oct in [-2,-1,0,1,2]] chord_pitches.sort() p0 = nearest_on_scale(p, chord_pitches) else: p0 = nearest_on_scale(p, C_major_scale) y0 = int((p0-72)*(-250)/12+240) # for chart # melody # same pitch => continue note or stop if prev_p0 == p0: # draw note pitch_length += 1 if pitch_length <= 4: pygame.draw.line(screen,(255,51,153),(x_0,y0),(x_1,y0),10) else: port.note_on(prev_p0, velocity=0, channel=0) time.sleep(max(0,30/bpm.val-(time.time()-prev_time))) prev_time = time.time() # new note else: # draw note pygame.draw.line(screen,(255,51,153,120),(x_0,y0),(x_1,y0),10) text1 = font.render(pitch_name(p0), True, (250,200,200)) screen.blit(text1, (x_0,y0)) # play note port.note_on(prev_p0, velocity=0, channel=0) port.note_on(p0, velocity=55, channel=0) pitch_length = 1 prev_p0 = p0 if prev_time == 0: prev_time = time.time() time.sleep(max(0,30/bpm.val-(time.time()-prev_time))) prev_time = time.time() # chord if x0 % 8 == 0: for cp in chord_progression_p[ch][prev_ch_i]: port.note_on(cp, velocity=0, channel=1) for cp in chord_progression_p[ch][ch_i]: port.note_on(cp, velocity=70, channel=1) prev_ch_i = ch_i # bass if x0 % 4 == 0: port.note_on(min(chord_progression_p[ch][ch_i])-24, velocity=70, channel=2) # bass # drum if x0 % 8 == 0: port.note_on(36, velocity=70, channel=9) # Kick elif x0 % 8 == 4: port.note_on(38, velocity=70, channel=9) # Snare port.note_on(42, velocity=50, channel=9) # Closed HiHat x0 += 1 # 1/2 beat # mouse actions for event in pygame.event.get(): # quit if event.type == pygame.QUIT: port.close() pygame.quit() pygame.midi.quit() sys.exit() # draw line elif event.type == pygame.MOUSEMOTION: if 1 == 1: mouse_position = pygame.mouse.get_pos() if last_pos is not None and last_pos[0]< mouse_position[0]: for i in range(len(position_list)): if position_list[i][0][0] <= mouse_position[0] and position_list[i][1][0] >= mouse_position[0]: drawing = False if drawing == True: pygame.draw.line(screen, WHITE, last_pos, mouse_position, 1) min_x = mouse_position[0] position_list.append((last_pos, mouse_position)) last_pos = mouse_position # clic mouse elif event.type == pygame.MOUSEBUTTONDOWN: drawing = True pos = pygame.mouse.get_pos() # slider for s in slides: if s.button_rect.collidepoint(pos): s.hit = True # start if b0.rect.collidepoint(pos): music_start = True play_positions = sorted(position_list) position_list = [] # end if b1.rect.collidepoint(pos): music_start = False port.write_short(0xb0, 123, 0) # all note off # chord change if b2.rect.collidepoint(pos): ch = (ch + 1 ) % 10 show_chord_name(font,chord_progression[ch]) # chord back if b3.rect.collidepoint(pos): ch = (ch - 1 ) % 10 show_chord_name(font,chord_progression[ch]) # melody if b4.rect.collidepoint(pos): update_melody() update_screen() # repeat/new melody if b5.rect.collidepoint(pos): if b5.txt == "Repeat": b5.txt = "Update" else: b5.txt = "Repeat" b5.draw() # mouse off elif event.type == pygame.MOUSEBUTTONUP: drawing = False for s in slides: s.hit = False # Move slides for s in slides: if s.hit: s.move() update_screen() # update display pygame.display.flip()
Comments
Post a Comment