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