Last active
February 2, 2020 19:26
-
-
Save WizardOfArc/f4dff00ba79d6e4121e057e4f6d4b3ed to your computer and use it in GitHub Desktop.
A little "library" to help with melody manipulation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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