Skip to main content

Featured

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 ...

Python programming 7 Small interactive music maker by PyGame

After I posted my small Drum Machine, with similar idea, I made an interactive music composition and player "Small interactive music maker".

(See my next blog "Better sound on PC" for sample video.)

Based on my previous programs, basic concept is
- tempo
- melody made by Fourier series
- some sample chord progression
can be changed on screen.

See below screen. Please click buttons or move sliders.
  Start: start playing music
  Stop: stop music
  Repeat/Update: (click to change) melody and chord repeat or change at right end.
  bps: tempo (beat per minutes)
  Change/Back: move forward/back of sample chord progressions.
  Melody: change melody (Wave made by of Fourier series)

Some sample chord progressions are included. You can change if you want. (Need to change both chord name and pitch lists.)

I used library Pygame. But I'm new. So Button and Slider classes are from below.
https://www.dreamincode.net/forums/topic/401541-buttons-and-sliders-in-pygame/

Some versions of pygame may have some bug.
Need "pip install pygame==2.0.0.dev6"

Since I include several functions as above, program is longer than previous examples. I wish you can read well. 

I will add some more features later (maybe save/load factors).

---------------------------------------------------------------------------------------

 
blog 7

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 = (40, 40, 55)
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]]

# function for melody pattern
def fx1(x, a, cycle):
    x1 = 2*math.pi/cycle*x
    y = a[0] * math.sin(x1) + a[1] * math.sin(2*x1) + a[2] * math.sin(3*x1) + a[3] * math.sin(4*x1) + a[4] * math.sin(8*x1) + a[5] * math.sin(16*x1)
    return y

# 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():
    for x in range(0, X, 1):

        # Calculations #
        x0 = x/900*64
        x1 = (x+1)/900*64
        y0 = int(fx1(x0,a,64)*(-250)+250)
        y1 = int(fx1(x1,a,64)*(-250)+250)

        # Drawing #
        pygame.draw.line(screen,BLUE,(x,y0),(x+1,y1),2)

# 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, 480, 850, 60], 0)
    for i in range(8):
        text1 = font.render(chord_list[i], True, WHITE)
        screen.blit(text1, (60+105*i,500))

# 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)
    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))

# change Fourier series coefficients and update melody
def update_melody():
    global a0, a1, a2, a3, a4, a5,a
    a0.val = random()-0.5
    a1.val = random()-0.5
    a2.val = random()-0.5
    a3.val = (random()-0.5) * 0.5
    a4.val = (random()-0.5) * 0.5
    a5.val = (random()-0.5) * 0.5
    a = [a0.val,a1.val/(2**(0.5)),a2.val/(3**(0.5)), a3.val/(4**(0.5)), a4.val/(5**(0.5)), a5.val/(6**(0.5))]

###############################################################################
# initialize
# display setting
font = pygame.font.SysFont(None, 30)
screen = pygame.display.set_mode((X, Y))
clock = pygame.time.Clock()

# sliders
a0 = Slider("a0", random()-0.5, 1, -1, 150)
a1 = Slider("a1", random()-0.5, 1, -1, 275)
a2 = Slider("a2", random()-0.5, 1, -1, 400)
a3 = Slider("a3", (random()-0.5)*0.5, 0.5, -0.5, 525)
a4 = Slider("a4", (random()-0.5)*0.5, 0.5, -0.5, 650)
a5 = Slider("a5", (random()-0.5)*0.5, 0.5, -0.5, 775)
bpm = Slider("bpm", 120, 180, 60, 20, 60)
slides = [a0, a1, a2, a3, a4, a5, bpm]
a = [a0.val,a1.val/(2**(0.5)),a2.val/(3**(0.5)), a3.val/(4**(0.5)), a4.val/(5**(0.5)), a5.val/(6**(0.5))]

# buttons
b0 = Button("Start", (60, 30),bg=(50,70,70))
b1 = Button("Stop", (840, 30),bg=(108,75,75))
b2 = Button("Change", (60, 460),bg=(70,60,70))
b3 = Button("Back", (150, 460),bg=(70,60,70))
b4 = Button("Melody", (60, 570),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 = -1
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

update_screen()
prev_time = 0
###############################################################################
# start loop
while True:
    # music Start
    if music_start == 1:
        # 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])
            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,a,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()
        # clic mouse
        elif event.type == pygame.MOUSEBUTTONDOWN:
            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 = 1
            # end
            if b1.rect.collidepoint(pos):
                music_start = 0
                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:
            for s in slides:
                s.hit = False

    # Move slides
    for s in slides:
        if s.hit:
            s.move()
            a = [a0.val,a1.val/(2**(0.5)),a2.val/(3**(0.5)), a3.val/(4**(0.5)), a4.val/(5**(0.5)), a5.val/(6**(0.5))]
            update_screen()

    # update display
    pygame.display.flip()


Comments