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 3 Create melody with Fourier Series

Since music is a movement of sounds with some patterns, I simply use Fourier Series to create melody patterns.

Blue line is a sample Fourier series and red bars are the notes. Notes are the nearest pitches in specified scale (e.g. [C, D, E, F, G, A, B, C] in C major).

 

My Fourier series is pretty simple as below. Coefficients of Fourier series are random. So you can hear different sounds every time.  


In a song, usually some phrases are repeats and some phrases are different. Use same coefficients for repeats and use new coefficients for new phrases.

You can hear a full composition based on this method in my Youtube "Etude 4". https://www.youtube.com/watch?v=DfIxoH5QTnQ

Within the actual composition, I make pitches more aligned to chord progressions. e.g. select pitches from chord notes with higher priority. 

Remark) At the bottom of below program, score is saved as a new file. If you don't want, please comment out.

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

 
# blog 3

import mido
import time
from mido import Message, MidiFile, MidiTrack, MetaMessage
from random import random
import numpy as np
import matplotlib.pyplot as plt

# port open
ports = mido.get_output_names()
port = mido.open_output(ports[0])

# Fourier Series function for melody pattern
def fx1(x, a, cycle):
    x1 = 2*np.pi/cycle*x
    y = a[0] * np.sin(x1) + a[1] * np.sin(2*x1) + a[2] * np.sin(4*x1) + a[3] * np.sin(8*x1) + a[4] * np.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

# add chord notes to track
def addChord(track,channel,pitch_list, duration, velocity):
    for i in range(len(pitch_list)):
        track.append(Message('note_on', channel = channel, note=pitch_list[i], velocity=velocity, time=0))
    track.append(Message('note_off', channel = channel, note=pitch_list[0], velocity=0, time=beat_to_tick(duration)))
    for i in range(1,len(pitch_list)):
        track.append(Message('note_off', channel = channel, note=pitch_list[i], velocity=0, time=0))

# convert beat to MIDI time scale (1 beat to 480 ticks)
def beat_to_tick(dur):
    return int(dur * 480 +0.5)

# Fourier series
# coefficient random creation
a = [1/((i+1))*(random()-0.5) for i in range(5) ]

# song length = 32
x = np.linspace(0, 32, 481)
# Fourier series cycle = 32 beats
cycle = 32
y = [72 + 12 * fx1(xi, a, cycle) for xi in x] # pitch center = C5, pitch range [1 octave x max/min(fx1)]

# C major scale pitches
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]]

# Melody as nearest scale pitches
pitch_list = []
duration_list = []
x1 = 0
while sum(duration_list) < x[-1]:
    # pitch on y at time x1
    p0 = 72 + 12 * fx1(x1,a,cycle)
    # find nearest pitch on scale
    pitch = nearest_on_scale(p0, C_major_scale)
    # initial duration of each note is 1/4
    duration = 0.25
    x1 += duration
    # if pitch is same as before, make duration longer
    if len(pitch_list) > 1 and pitch_list[-1] == pitch:
        duration_list[-1] += duration
    # otherwise add new note
    else:
        pitch_list.append(pitch)
        duration_list.append(duration)
 

# create new MIDI file
mid = MidiFile()
track0 = MidiTrack() # tempo
track1 = MidiTrack() # melody
track2 = MidiTrack() # chord
mid.tracks.append(track0) # tempo
mid.tracks.append(track1) # melody
mid.tracks.append(track2) # chord
track1.append(Message('program_change', channel = 0, program=40,time=0)) # melody program:40 = Violin (General MIDI)
track2.append(Message('program_change', channel = 1, program=0,time=0)) # chord   program:0 = Piano

# set tempo
bpm = 120 # beat per minutes
track0.append(MetaMessage('set_tempo', tempo=int(1000000*60/bpm), time=0))

# add melody
velocity = 70
for i in range(len(pitch_list)):
    track1.append(Message('note_on', channel = 0, note=pitch_list[i], velocity=velocity, time=0))
    track1.append(Message('note_off', channel = 0, note=pitch_list[i], velocity=0, time=beat_to_tick(duration_list[i])))

chord_list = ["C", "Em", "G", "CM7onE","C", "Em", "G", "CM7onE"]
chord_pitch_list = [[60, 64, 67], [64, 67, 71], [67, 71, 74], [72, 64, 67, 71],[60, 64, 67], [64, 67, 71], [67, 71, 74], [72, 64, 67, 71]] # from blog 2 chord progression

# add chords
for i in range(len(chord_pitch_list)):
    channel = 1
    addChord(track2, channel, chord_pitch_list[i], 4, 70)

# play MIDI file
for msg in mid:
    time.sleep(msg.time)
    if not msg.is_meta:
        port.send(msg)

# MIDI score saved
score_name = "blog03"
mid.save(score_name+'.mid')


Comments