Skip to content

Instantly share code, notes, and snippets.

@WizardOfArc
Last active February 2, 2020 19:26
Show Gist options
  • Select an option

  • Save WizardOfArc/f4dff00ba79d6e4121e057e4f6d4b3ed to your computer and use it in GitHub Desktop.

Select an option

Save WizardOfArc/f4dff00ba79d6e4121e057e4f6d4b3ed to your computer and use it in GitHub Desktop.
A little "library" to help with melody manipulation.
from enum import Enum
import pygame.midi
import re
import time
instruments = [
'0: Acoustic Grand Piano',
'1: Bright Acoustic Piano',
'2: Electric Grand Piano',
'3: Honky-tonk Piano',
'4: Rhodes Piano',
'5: Chorused Piano',
'6: Harpsichord',
'7: Clavinet',
'8: Celesta',
'9: Glockenspiel',
'10: Music box',
'11: Vibraphone',
'12: Marimba',
'13: Xylophone',
'14: Tubular Bells',
'15: Dulcimer',
'16: Hammond Organ',
'17: Percussive Organ',
'18: Rock Organ',
'19: Church Organ',
'20: Reed Organ',
'21: Accordion',
'22: Harmonica',
'23: Tango Accordion',
'24: Acoustic Guitar (nylon)',
'25: Acoustic Guitar (steel)',
'26: Electric Guitar (jazz)',
'27: Electric Guitar (clean)',
'28: Electric Guitar (muted)',
'29: Overdriven Guitar',
'30: Distortion Guitar',
'31: Guitar Harmonics',
'32: Acoustic Bass',
'33: Electric Bass (finger)',
'34: Electric Bass (pick)',
'35: Fretless Bass',
'36: Slap Bass 1',
'37: Slap Bass 2',
'38: Synth Bass 1',
'39: Synth Bass 2',
'40: Violin',
'41: Viola',
'42: Cello',
'43: Contrabass',
'44: Tremolo Strings',
'45: Pizzicato Strings',
'46: Orchestral Harp',
'47: Timpani',
'48: String Ensemble 1',
'49: String Ensemble 2',
'50: Synth Strings 1',
'51: Synth Strings 2',
'52: Choir Aahs',
'53: Voice Oohs',
'54: Synth Voice',
'55: Orchestra Hit',
'56: Trumpet',
'57: Trombone',
'58: Tuba',
'59: Muted Trumpet',
'60: French Horn',
'61: Brass Section',
'62: Synth Brass 1',
'63: Synth Brass 2',
'64: Soprano Sax',
'65: Alto Sax',
'66: Tenor Sax',
'67: Baritone Sax',
'68: Oboe',
'69: English Horn',
'70: Bassoon',
'71: Clarinet',
'72: Piccolo',
'73: Flute',
'74: Recorder',
'75: Pan Flute',
'76: Bottle Blow',
'77: Shakuhachi',
'78: Whistle',
'79: Ocarina',
'80: Lead 1 (square)',
'81: Lead 2 (sawtooth)',
'82: Lead 3 (calliope lead)',
'83: Lead 4 (chiffer lead)',
'84: Lead 5 (charang)',
'85: Lead 6 (voice)',
'86: Lead 7 (fifths)',
'87: Lead 8 (brass + lead)',
'88: Pad 1 (new age)',
'89: Pad 2 (warm)',
'90: Pad 3 (polysynth)',
'91: Pad 4 (choir)',
'92: Pad 5 (bowed)',
'93: Pad 6 (metallic)',
'94: Pad 7 (halo)',
'95: Pad 8 (sweep)',
'96: FX 1 (rain)',
'97: FX 2 (soundtrack)',
'98: FX 3 (crystal)',
'99: FX 4 (atmosphere)',
'100: FX 5 (brightness)',
'101: FX 6 (goblins)',
'102: FX 7 (echoes)',
'103: FX 8 (sci-fi)',
'104: Sitar',
'105: Banjo',
'106: Shamisen',
'107: Koto',
'108: Kalimba',
'109: Bagpipe',
'110: Fiddle',
'111: Shana',
'112: Tinkle Bell',
'113: Agogo',
'114: Steel Drums',
'115: Woodblock',
'116: Taiko Drum',
'117: Melodic Tom',
'118: Synth Drum',
'119: Reverse Cymbal',
'120: Guitar Fret Noise',
'121: Breath Noise',
'122: Seashore',
'123: Bird Tweet',
'124: Telephone Ring',
'125: Helicopter',
'126: Applause',
'127: Gunshot',
]
class PitchClass(Enum):
C = 0
Csharp = 1
D = 2
Eflat = 3
E = 4
F = 5
Fsharp = 6
G = 7
Aflat = 8
A = 9
Bflat = 10
B = 11
class Scale:
def __init__(self, pitch_classes):
self.pc_list = pitch_classes
def get(self, index: int) -> 'PitchClass':
real_idx = index % len(self.pc_list)
def contains(self, pc: 'PitchClass') -> bool:
return pc in self.pc_list
class Pitch:
def __init__(self, pc: 'PitchClass', octave: int):
self.pitch_class = pc
self.octave = octave
def get_midi_pitch(self) -> int:
return (self.octave - 4)*12 + self.pitch_class.value + 60
def __sub__(self, other: 'Pitch') -> int:
"""Gets the difference in semitones between two notes"""
return (self.octave - other.octave)*12 + self.pitch_class.value - other.pitch_class.value
def _get_octave_marks(self) -> str:
oct = self.octave
marks = ''
if oct > 3:
while oct > 3:
marks += "'"
oct -= 1
elif oct < 3:
while oct < 3:
marks += ','
oct += 1
return marks
def _get_pc_repr(self) -> str:
if self.pitch_class.value == 1:
return 'cis'
if self.pitch_class.value == 3:
return 'ees'
if self.pitch_class.value == 6:
return 'fis'
if self.pitch_class.value == 8:
return 'aes'
if self.pitch_class.value == 10:
return 'bes'
return self.pitch_class.name.lower()
def __repr__(self):
return f'{self._get_pc_repr()}{self._get_octave_marks()}'
def _find_closest_diatonic_pitch(self, scale) -> 'Pitch':
if self.pitch_class in scale:
return self
pitch_above = PitchClass((self.pitch_class.value + 1) % 12)
if pitch_above in scale:
return pitch_above
pitch_below = PitchClass((self.pitch_class.value - 1) % 12)
if pitch_below in scale:
return pitch_below
whole_tone_up = PitchClass((self.pitch_class.value + 2) % 12)
if whole_tone_up in scale:
return whole_tone_up
whole_tone_down = PitchClass((self.pitch_class.value - 2) % 12)
if whole_tone_down in scale:
return whole_tone_down
def diatonic_distance(self, other: 'Pitch', scale) -> int:
d_step = scale.index(other.pitch_class) - scale.index(self.pitch_class)
d_oct = other.octave - self.octave
return d_oct * len(scale) + d_step
def transposed_copy_diatonic(self, steps: int, scale) -> 'Pitch':
"""Returns a pitch transposed given number of steps in the given scale"""
span = len(scale)
closest_pitch = self._find_closest_diatonic_pitch(scale)
idx = scale.index(closest_pitch.pitch_class)
total_steps = steps + idx
delta_oct = total_steps // len(scale)
new_idx = total_steps % len(scale)
return Pitch(scale[new_idx], self.octave + delta_oct)
def transposed_copy(self, semitones: int) -> 'Pitch':
"""Transposes pitch number of semitones (return new pitch object)"""
new_semitones = self.octave * 12 + self.pitch_class.value + semitones
return Pitch(PitchClass(new_semitones % 12), new_semitones // 12)
@staticmethod
def from_midi_pitch(midi_pitch: int) -> 'Pitch':
pc = PitchClass((midi_pitch - 60) % 12)
oct = ((midi_pitch - 60) // 12 + 4)
return Pitch(pc, oct)
@staticmethod
def from_shorthand(note_sh: str, octave_sh: str) -> 'Pitch':
pc = PitchClass.C
octave = 3
if octave_sh and octave_sh[0] == "'":
octave += len(octave_sh)
elif octave_sh and octave_sh[0] == ",":
octave -= len(octave_sh)
if note_sh.lower() == 'c' or note_sh.lower() == 'bes':
pc = PitchClass.C
elif note_sh.lower() == 'cis' or note_sh.lower() == 'des':
pc = PitchClass.Csharp
elif note_sh.lower() == 'd':
pc = PitchClass.D
elif note_sh.lower() == 'dis' or note_sh.lower() == 'ees':
pc = PitchClass.Eflat
elif note_sh.lower() == 'e' or note_sh.lower() == 'fes':
pc = PitchClass.E
elif note_sh.lower() == 'eis' or note_sh.lower() == 'f':
pc = PitchClass.F
elif note_sh.lower() == 'fis' or note_sh.lower() == 'ges':
pc = PitchClass.Fsharp
elif note_sh.lower() == 'g':
pc = PitchClass.G
elif note_sh.lower() == 'gis' or note_sh.lower() == 'aes':
pc = PitchClass.Aflat
elif note_sh.lower() == 'a':
pc = PitchClass.A
elif note_sh.lower() == 'ais' or note_sh.lower() == 'bes':
pc = PitchClass.Bflat
elif note_sh.lower() == 'b' or note_sh.lower() == 'ces':
pc = PitchClass.B
return Pitch(pc, octave)
class MelodicParticle:
@staticmethod
def parse(shorthand: str):
"""Parses lilypond style shorthand into a Note"""
match = re.compile(r"([A-Za-z]+)([,']*)([\d.]+)").match(shorthand)
note_sh = match.group(1)
octave_sh = match.group(2)
duration_sh = match.group(3)
if note_sh == 'r':
return Rest(4 / float(duration_sh))
pitch = Pitch.from_shorthand(note_sh, octave_sh)
return Note(4 / float(duration_sh), pitch)
class Rest:
def __init__(self, val):
self.val = val
def __mul__(self, scalar: int):
return Rest(self.val * scalar)
def __truediv__(self, scalar: int):
return Rest(self.val / scalar)
def get_midi_note(self, tempo: float) -> (int, float):
return None, self.val * (60/tempo)
def __repr__(self):
return f'r{4 / self.val}'
def transposed_copy(self, semitones):
return self
def transposed_copy_diatonic(self, semitones):
return self
def invert(self, pivot: 'Note'):
return self
def invert_diatonic(self, pivot: 'Note', scale):
return self
class Note:
def __init__(self, val: float, pitch: 'Pitch'):
self.val = val
self.pitch = pitch
def __mul__(self, scalar: float) -> 'Note':
return Note(self.val * scalar, self.pitch)
def __truediv__(self, scalar: float) -> 'Note':
return Note(self.val / scalar, self.pitch)
def __sub__(self, other: 'Note') -> int:
"""Gets the difference in semitones between two notes"""
return self.pitch - other.pitch
def invert(self, pivot: 'Note') -> 'Note':
return self.transposed_copy(2*(pivot - self))
def invert_diatonic(self, pivot: 'Note', scale) -> 'Note':
steps = 2 * self.diatonic_distance(pivot, scale)
return self.transposed_copy_diatonic(steps, scale)
def diatonic_distance(self, other: 'Note', scale) -> int:
"""Gets the number of steps between two notes in a scale"""
return self.pitch.diatonic_distance(other.pitch, scale)
def get_midi_note(self, tempo) -> (int, float):
"""Calculate the midi note and the duration from the tempo"""
return self.pitch.get_midi_pitch(), self.val * (60/tempo)
def transposed_copy(self, semitones: int) -> 'Note':
"""Returns new note transposed a number of semitones from given note"""
return Note(self.val, self.pitch.transposed_copy(semitones))
def transposed_copy_diatonic(self, steps: int, scale) -> 'Note':
"""Returns new note transposed a number of semitones from given note"""
return Note(self.val, self.pitch.transposed_copy_diatonic(steps, scale))
def transpose(self, semitones: int) -> None:
"""Transposes Note a number of semitones"""
self.pitch = self.pitch.transpose(semitones)
def __repr__(self):
return f'{self.pitch}{4 / self.val}'
@staticmethod
def from_midi_note(midi_pitch: int, dur: float, tempo) -> 'Note':
return Note((dur * tempo) / 60, Pitch.from_midi_pitch(midi_pitch))
class Melody:
def __init__(self, notes=[]):
self.notes = notes
def __add__(self, other: 'Melody') -> 'Melody':
return Melody(self.notes + other.notes)
def __getitem__(self, index):
return self.notes[index]
def __invert__(self) -> 'Melody':
return Melody([n.invert(self.notes[0]) for n in self.notes])
def invert_diatonic(self, scale) -> 'Melody':
return Melody([n.invert_diatonic(self.notes[0], scale) for n in self.notes])
def __mul__(self, scalar: int) -> 'Melody':
new_notes = [n*scalar for n in self.notes]
return Melody(new_notes)
def __neg__(self) -> 'Melody':
new_notes = self.notes[:]
new_notes.reverse()
return Melody(new_notes)
def __truediv__(self, scalar) -> 'Melody':
return Melody([n / scalar for n in self.notes])
def __mul__(self, scalar) -> 'Melody':
return Melody([n * scalar for n in self.notes])
def __repr__(self):
return ' '.join([repr(n) for n in self.notes])
def __setitem__(self, index, melodic_particle) -> None:
self.notes[index] = melodic_particle
def get_fragment(self, start: int, end: int) -> 'Melody':
if start >= 0 and start < end and end <= len(self.notes):
return Melody(self.notes[start:end])
if start >= 0 and start < end:
return Melody(self.notes[start:])
if start < end and end <= len(self.notes):
return Melody(self.notes[:end])
if start < end:
return self
if start > end and start < len(self.notes) and end >= -1:
return -(Melody(self.notes[end + 1:start +1]))
if start > end and start >= len(self.notes) and end >= (-1):
return -(Melody(self.notes[end + 1:]))
if start > end and start < len(self.notes) and end < -1:
return -(Melody(self.notes[:start + 1]))
if start > end:
return -self
def repeat(self, scalar: int) ->'Melody':
new_melody = Melody([])
for i in range(scalar):
new_melody += self
return new_melody
def transposed_copy(self, semitones: int) -> 'Melody':
notes = [n.transposed_copy(semitones) for n in self.notes]
return Melody(notes)
def transposed_copy_diatonic(self, steps: int, scale) -> 'Melody':
notes = [n.transposed_copy_diatonic(steps, scale) for n in self.notes]
return Melody(notes)
@staticmethod
def parse(shorthand: str) -> 'Melody':
return Melody([MelodicParticle.parse(n) for n in shorthand.split(' ')])
def play_melody(melody: 'Melody', player, tempo: float, instrument: int) -> None:
player.set_instrument(instrument)
for note in melody.notes:
pitch, duration = note.get_midi_note(tempo)
if isinstance(note, Note):
player.note_on(pitch, 127)
time.sleep(duration)
if isinstance(note, Note):
player.note_off(pitch, 127)
def main():
print('init pygame.midi')
pygame.midi.init()
default_output_id = pygame.midi.get_default_output_id()
print(f'create midi player for id:{default_output_id}')
player = pygame.midi.Output(default_output_id)
print('set instrument to 0')
player.set_instrument(0)
melody = Melody([Note(1, PitchClass.A, 3),
Note(1, PitchClass.A, 3),
Note(0.5, PitchClass.E, 4),
Note(0.5, PitchClass.C, 4),
Note(2, PitchClass.A, 3),
Note(1, PitchClass.B, 3),
Note(1, PitchClass.B, 3),
Note(0.25, PitchClass.Fsharp, 4),
Note(0.25, PitchClass.Eflat, 4),
Note(0.5, PitchClass.B, 3),
Note(0.25, PitchClass.Fsharp, 4),
Note(0.25, PitchClass.Eflat, 4),
Note(0.5, PitchClass.B, 3),
])
for inst in range(128):
print(f'instrument: {instruments[inst]}')
play_melody(melody, player, inst)
print('delete the player object')
del player
print('quit pygame.midi')
pygame.midi.quit()
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment