diff --git a/.gitignore b/.gitignore index 1e8aaa7e..9b26fb10 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ dist/ *.egg-info/ /venv + +/.idea +!/mingus_examples/saved_blues.json diff --git a/doc/ccm_notes.txt b/doc/ccm_notes.txt new file mode 100644 index 00000000..2c76db0a --- /dev/null +++ b/doc/ccm_notes.txt @@ -0,0 +1,38 @@ +Note Class: + +requires some indication of the sound to play + pitch and instrument + percussion instrument (e.g. no pitch) + +should be playable by itself (e.g. not as part of a bar) + +optional params: + duration + velocity + channel + bank? + other params like pitch bend? + + +Bar Class: +Notes should be able to start in a bar and end in a different bar + +a bar can have a meter, or inherit it from a track + + +Inheritence Precident: + + Some params should be inherited from outer containers, unless they are specified. + + For example, a track, contains a bar contains, a notecontainer contains, a note. If the track has + a channel, it should pass it to each bar. If a bar has a channel it should use it. But if the bar's + channel is None, it should inherit it from the track, etc... + + +****************************************************************** +Listing all the instruments in a sound font file: https://github.com/FluidSynth/fluidsynth/wiki/UserManual#soundfonts + +1. Open terminal +2. type: fluidsynth +3. type: load "path to sound font" +4. type: inst 1 \ No newline at end of file diff --git a/doc/wiki/tutorialExtraLilypond.rst b/doc/wiki/tutorialExtraLilypond.rst index de12d29e..9a7a283a 100644 --- a/doc/wiki/tutorialExtraLilypond.rst +++ b/doc/wiki/tutorialExtraLilypond.rst @@ -1,7 +1,7 @@ Tutorial 1 - Generating Sheet Music with LilyPond ================================================= -The LilyPond module provides some methods to help you generate files in the LilyPond format. This allows you to create sheet music from some of the objects in mingus.containers. +The LilyPond module provides some methods to help you generate files in the LilyPond format. This allows you to create sheet music from some of the objects in mingus.containers. Note: you need to install `LilyPond `_ on your system first. >>> import mingus.extra.lilypond as LilyPond diff --git a/mingus/containers/__init__.py b/mingus/containers/__init__.py index c2ec2c57..4060f392 100644 --- a/mingus/containers/__init__.py +++ b/mingus/containers/__init__.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from mingus.containers.note import Note +from mingus.containers.note import Note, PercussionNote from mingus.containers.note_container import NoteContainer from mingus.containers.bar import Bar from mingus.containers.track import Track diff --git a/mingus/containers/bar.py b/mingus/containers/bar.py index b4be04b2..9d981fe3 100644 --- a/mingus/containers/bar.py +++ b/mingus/containers/bar.py @@ -20,39 +20,54 @@ import six +from mingus.containers import PercussionNote from mingus.containers.mt_exceptions import MeterFormatError from mingus.containers.note_container import NoteContainer from mingus.core import meter as _meter from mingus.core import progressions, keys from typing import Optional +from mingus.containers.get_note_length import get_note_length, get_beat_start, get_bar_length + class Bar(object): """A bar object. - A Bar is basically a container for NoteContainers. + A Bar is basically a container for NoteContainers. This is where NoteContainers + get their duration. + + Each NoteContainer must start in the bar, but it can end outside the bar. Bars can be stored together with Instruments in Tracks. """ - - key = "C" - meter = (4, 4) - current_beat = 0.0 - length = 0.0 - bar = [] - - def __init__(self, key="C", meter=(4, 4)): + def __init__(self, key="C", meter=(4, 4), bpm=120, bars=None): # warning should check types if isinstance(key, six.string_types): key = keys.Key(key) self.key = key + self.bpm = bpm self.set_meter(meter) - self.empty() + + if bars: + self.bar = bars + self.current_beat = 0.0 + else: + self.empty() + + def to_json(self): + d = { + 'class_name': self.__class__.__name__, + 'key': self.key, + 'meter': self.meter, + 'bpm': self.bpm, + 'bars': self.bar + } + return d def empty(self): """Empty the Bar, remove all the NoteContainers.""" - self.bar = [] - self.current_beat = 0.0 + self.bar = [] # list of [current_beat, note duration number, list of notes] + self.current_beat = 0.0 # fraction of way through bar return self.bar def set_meter(self, meter): @@ -72,9 +87,9 @@ def set_meter(self, meter): self.length = 0.0 else: raise MeterFormatError( - "The meter argument '%s' is not an " + f"The meter argument {meter} is not an " "understood representation of a meter. " - "Expecting a tuple." % meter + "Expecting a tuple." ) def place_notes(self, notes, duration): @@ -85,8 +100,7 @@ def place_notes(self, notes, duration): Raise a MeterFormatError if the duration is not valid. - Return True if succesful, False otherwise (ie. the Bar hasn't got - enough room for a note of that duration). + Return True if successful, False otherwise if the note does not start in the bar. """ # note should be able to be one of strings, lists, Notes or # NoteContainers @@ -98,18 +112,13 @@ def place_notes(self, notes, duration): notes = NoteContainer(notes) elif isinstance(notes, list): notes = NoteContainer(notes) - if self.current_beat + 1.0 / duration <= self.length or self.length == 0.0: + + if self.is_full(): + return False + else: self.bar.append([self.current_beat, duration, notes]) self.current_beat += 1.0 / duration return True - else: - return False - - def place_notes_at(self, notes, at): - """Place notes at the given index.""" - for x in self.bar: - if x[0] == at: - x[2] += notes def place_rest(self, duration): """Place a rest of given duration on the current_beat. @@ -118,7 +127,8 @@ def place_rest(self, duration): """ return self.place_notes(None, duration) - def _is_note(self, note: Optional[NoteContainer]) -> bool: + @staticmethod + def _is_note(note: Optional[NoteContainer]) -> bool: """ Return whether the 'note' contained in a bar position is an actual NoteContainer. If False, it is a rest (currently represented by None). @@ -157,14 +167,14 @@ def change_note_duration(self, at, to): def get_range(self): """Return the highest and the lowest note in a tuple.""" - (min, max) = (100000, -1) + (min_note, max_note) = (100000, -1) for cont in self.bar: for note in cont[2]: - if int(note) < int(min): - min = note - elif int(note) > int(max): - max = note - return (min, max) + if int(note) < int(min_note): + min_note = note + elif int(note) > int(max_note): + max_note = note + return min_note, max_note def space_left(self): """Return the space left on the Bar.""" @@ -223,6 +233,58 @@ def get_note_names(self): res.append(x) return res + def play(self, start_time: int, bpm: float, channel: int, score: dict) -> int: + """ + Put bar events into score. + + :param start_time: start time of bar in milliseconds + :param bpm: beats per minute + :param channel: channel number + :param score: dict of events + :return: duration of bar in milliseconds + """ + assert type(start_time) == int + + for bar_fraction, duration_type, notes in self.bar: + duration_ms = get_note_length(duration_type, self.meter[1], bpm) + + current_beat = bar_fraction * self.meter[1] + 1.0 + beat_start = get_beat_start(current_beat, bpm) + start_key = start_time + beat_start + end_key = start_key + duration_ms + + if notes: + for note in notes: + score.setdefault(start_key, []).append( + { + 'func': 'start_note', + 'note': note, + 'channel': channel, + 'velocity': note.velocity + } + ) + + note_duration = getattr(note, 'duration', None) + if note_duration: + score.setdefault(start_key + note_duration, []).append( + { + 'func': 'end_note', + 'note': note, + 'channel': channel, + } + ) + elif not isinstance(note, PercussionNote): + score.setdefault(end_key, []).append( + { + 'func': 'end_note', + 'note': note, + 'channel': channel, + } + ) + else: + pass + return get_bar_length(self.meter, bpm) + def __add__(self, note_container): """Enable the '+' operator on Bars.""" if self.meter[1] != 0: diff --git a/mingus/containers/get_note_length.py b/mingus/containers/get_note_length.py new file mode 100644 index 00000000..0f011f86 --- /dev/null +++ b/mingus/containers/get_note_length.py @@ -0,0 +1,73 @@ +from unittest import TestCase + + +def get_note_length(note_type, beat_length, bpm) -> int: + """ + Since we are working in milliseconds as integers, we want to unify how we calculate + note lengths so that tracks do not get out of sync + + :param note_type: 1=whole note, 4 = quarter note, etc... + :param beat_length: 4 - quarter note, 8 - eighth note + :param bpm: beats per minute + :return: note length in milliseconds + """ + beat_ms = ((1.0 / bpm) * 60.0) * 1000.0 # milliseconds + length = (beat_length / note_type) * beat_ms + return round(length) + + +def get_beat_start(beat_number, bpm): + """ + + :param beat_number: 1, 2, 3, 4 for 4/4, etc.. + :param bpm: beats per minute + :return: note length in milliseconds + """ + beat_ms = ((1.0 / bpm) * 60.0) * 1000.0 # milliseconds + start = (beat_number - 1.0) * beat_ms + return round(start) + + +def get_bar_length(meter, bpm): + return get_beat_start(meter[0] + 1, bpm) + + +class TestLengthCalculations(TestCase): + + def setUp(self) -> None: + super().setUp() + self.whole = 1.0 + self.quarter = 4.0 + self.eighth = 8.0 + + def test_get_note_length(self): + # A quarter note, in 4/4 with 1/2 second per beat + length = get_note_length(self.quarter, 4.0, 120.0) + self.assertEqual(500, length) + + # A whole note, in 4/4 with 1/2 second per beat + length = get_note_length(self.whole, 4.0, 120.0) + self.assertEqual(2000, length) + + # An eighth note, in 4/4 with 1/2 second per beat + length = get_note_length(self.eighth, 4.0, 120.0) + self.assertEqual(250, length) + + # An eighth note, in 6/8 with 1/2 second per beat + length = get_note_length(self.eighth, 8.0, 120.0) + self.assertEqual(500, length) + + # An quarter note, in 6/8 with 1/2 second per beat + length = get_note_length(self.quarter, 8.0, 120.0) + self.assertEqual(1000, length) + + def test_get_beat_start(self): + start = get_beat_start(2.0, 120.0) + self.assertEqual(500, start) + + def test_get_bar_length(self): + length = get_bar_length((4.0, 4.0), 120.0) + self.assertEqual(2000, length) + + length = get_bar_length((6.0, 8.0), 120.0) + self.assertEqual(3000, length) diff --git a/mingus/containers/instrument.py b/mingus/containers/instrument.py index 06645002..69005d6d 100644 --- a/mingus/containers/instrument.py +++ b/mingus/containers/instrument.py @@ -23,10 +23,8 @@ import six -class Instrument(object): - - """An instrument object. - +class Instrument: + """ The Instrument class is pretty self explanatory. Instruments can be used with Tracks to define which instrument plays what, with the added bonus of checking whether the entered notes are in the range of the @@ -35,31 +33,43 @@ class Instrument(object): It's probably easiest to subclass your own Instruments (see Piano and Guitar for examples). """ + def __init__(self, name, note_range=None, clef="bass and treble", tuning=None, bank=0): + self.name = name + if note_range is None: + self.note_range = (Note("C", 0), Note("C", 8)) + else: + self.note_range = note_range + self.clef = clef + self.tuning = tuning + self.bank = bank + + def to_json(self): + d = { + 'class_name': self.__class__.__name__, + 'name': self.name, + 'note_range': self.note_range, + 'clef': self.clef, + 'tuning': self.tuning, + 'bank': self.bank + } + return d - name = "Instrument" - range = (Note("C", 0), Note("C", 8)) - clef = "bass and treble" - tuning = None # optional StringTuning object - - def __init__(self): - pass - - def set_range(self, range): - """Set the range of the instrument. + def set_range(self, note_range): + """Set the note_rNGE of the instrument. - A range is a tuple of two Notes or note strings. + A note_range is a tuple of two Notes or note strings. """ - if isinstance(range[0], six.string_types): - range[0] = Note(range[0]) - range[1] = Note(range[1]) - if not hasattr(range[0], "name"): + if isinstance(note_range[0], six.string_types): + note_range[0] = Note(note_range[0]) + note_range[1] = Note(note_range[1]) + if not hasattr(note_range[0], "name"): raise UnexpectedObjectError( - "Unexpected object '%s'. " "Expecting a mingus.containers.Note object" % range[0] + "Unexpected object '%s'. " "Expecting a mingus.containers.Note object" % note_range[0] ) - self.range = range + self.note_range = note_range def note_in_range(self, note): - """Test whether note is in the range of this Instrument. + """Test whether note is in the note_range of this Instrument. Return True if so, False otherwise. """ @@ -69,7 +79,7 @@ def note_in_range(self, note): raise UnexpectedObjectError( "Unexpected object '%s'. " "Expecting a mingus.containers.Note object" % note ) - if note >= self.range[0] and note <= self.range[1]: + if self.note_range[0] <= note <= self.note_range[1]: return True return False @@ -78,7 +88,7 @@ def notes_in_range(self, notes): return self.can_play_notes(notes) def can_play_notes(self, notes): - """Test if the notes lie within the range of the instrument. + """Test if the notes lie within the note_range of the instrument. Return True if so, False otherwise. """ @@ -93,364 +103,165 @@ def can_play_notes(self, notes): def __repr__(self): """Return a string representing the object.""" - return "%s [%s - %s]" % (self.name, self.range[0], self.range[1]) + return "%s [%s - %s]" % (self.name, self.note_range[0], self.note_range[1]) class Piano(Instrument): - name = "Piano" - range = (Note("F", 0), Note("B", 8)) - - def __init__(self): - Instrument.__init__(self) + note_range = (Note("F", 0), Note("B", 8)) class Guitar(Instrument): - name = "Guitar" - range = (Note("E", 3), Note("E", 7)) + note_range = (Note("E", 3), Note("E", 7)) clef = "Treble" - def __init__(self): - Instrument.__init__(self) - def can_play_notes(self, notes): if len(notes) > 6: return False return Instrument.can_play_notes(self, notes) -class MidiInstrument(Instrument): - - range = (Note("C", 0), Note("B", 8)) - instrument_nr = 1 - name = "" - names = [ - "Acoustic Grand Piano", - "Bright Acoustic Piano", - "Electric Grand Piano", - "Honky-tonk Piano", - "Electric Piano 1", - "Electric Piano 2", - "Harpsichord", - "Clavi", - "Celesta", - "Glockenspiel", - "Music Box", - "Vibraphone", - "Marimba", - "Xylophone", - "Tubular Bells", - "Dulcimer", - "Drawbar Organ", - "Percussive Organ", - "Rock Organ", - "Church Organ", - "Reed Organ", - "Accordion", - "Harmonica", - "Tango Accordion", - "Acoustic Guitar (nylon)", - "Acoustic Guitar (steel)", - "Electric Guitar (jazz)", - "Electric Guitar (clean)", - "Electric Guitar (muted)", - "Overdriven Guitar", - "Distortion Guitar", - "Guitar harmonics", - "Acoustic Bass", - "Electric Bass (finger)", - "Electric Bass (pick)", - "Fretless Bass", - "Slap Bass 1", - "Slap Bass 2", - "Synth Bass 1", - "Synth Bass 2", - "Violin", - "Viola", - "Cello", - "Contrabass", - "Tremolo Strings", - "Pizzicato Strings", - "Orchestral Harp", - "Timpani", - "String Ensemble 1", - "String Ensemble 2", - "SynthStrings 1", - "SynthStrings 2", - "Choir Aahs", - "Voice Oohs", - "Synth Voice", - "Orchestra Hit", - "Trumpet", - "Trombone", - "Tuba", - "Muted Trumpet", - "French Horn", - "Brass Section", - "SynthBrass 1", - "SynthBrass 2", - "Soprano Sax", - "Alto Sax", - "Tenor Sax", - "Baritone Sax", - "Oboe", - "English Horn", - "Bassoon", - "Clarinet", - "Piccolo", - "Flute", - "Recorder", - "Pan Flute", - "Blown Bottle", - "Shakuhachi", - "Whistle", - "Ocarina", - "Lead1 (square)", - "Lead2 (sawtooth)", - "Lead3 (calliope)", - "Lead4 (chiff)", - "Lead5 (charang)", - "Lead6 (voice)", - "Lead7 (fifths)", - "Lead8 (bass + lead)", - "Pad1 (new age)", - "Pad2 (warm)", - "Pad3 (polysynth)", - "Pad4 (choir)", - "Pad5 (bowed)", - "Pad6 (metallic)", - "Pad7 (halo)", - "Pad8 (sweep)", - "FX1 (rain)", - "FX2 (soundtrack)", - "FX 3 (crystal)", - "FX 4 (atmosphere)", - "FX 5 (brightness)", - "FX 6 (goblins)", - "FX 7 (echoes)", - "FX 8 (sci-fi)", - "Sitar", - "Banjo", - "Shamisen", - "Koto", - "Kalimba", - "Bag pipe", - "Fiddle", - "Shanai", - "Tinkle Bell", - "Agogo", - "Steel Drums", - "Woodblock", - "Taiko Drum", - "Melodic Tom", - "Synth Drum", - "Reverse Cymbal", - "Guitar Fret Noise", - "Breath Noise", - "Seashore", - "Bird Tweet", - "Telephone Ring", - "Helicopter", - "Applause", - "Gunshot", - ] - - def __init__(self, name=""): - self.name = name - - -class MidiPercussionInstrument(Instrument): - def __init__(self): - super(MidiPercussionInstrument, self).__init__() - self.name = "Midi Percussion" - self.mapping = { - 35: "Acoustic Bass Drum", - 36: "Bass Drum 1", - 37: "Side Stick", - 38: "Acoustic Snare", - 39: "Hand Clap", - 40: "Electric Snare", - 41: "Low Floor Tom", - 42: "Closed Hi Hat", - 43: "High Floor Tom", - 44: "Pedal Hi-Hat", - 45: "Low Tom", - 46: "Open Hi-Hat", - 47: "Low-Mid Tom", - 48: "Hi Mid Tom", - 49: "Crash Cymbal 1", - 50: "High Tom", - 51: "Ride Cymbal 1", - 52: "Chinese Cymbal", - 53: "Ride Bell", - 54: "Tambourine", - 55: "Splash Cymbal", - 56: "Cowbell", - 57: "Crash Cymbal 2", - 58: "Vibraslap", - 59: "Ride Cymbal 2", - 60: "Hi Bongo", - 61: "Low Bongo", - 62: "Mute Hi Conga", - 63: "Open Hi Conga", - 64: "Low Conga", - 65: "High Timbale", - 66: "Low Timbale", - 67: "High Agogo", - 68: "Low Agogo", - 69: "Cabasa", - 70: "Maracas", - 71: "Short Whistle", - 72: "Long Whistle", - 73: "Short Guiro", - 74: "Long Guiro", - 75: "Claves", - 76: "Hi Wood Block", - 77: "Low Wood Block", - 78: "Mute Cuica", - 79: "Open Cuica", - 80: "Mute Triangle", - 81: "Open Triangle", - } - - def acoustic_bass_drum(self): - return Note(35 - 12) - - def bass_drum_1(self): - return Note(36 - 12) - - def side_stick(self): - return Note(37 - 12) - - def acoustic_snare(self): - return Note(38 - 12) - - def hand_clap(self): - return Note(39 - 12) - - def electric_snare(self): - return Note(40 - 12) - - def low_floor_tom(self): - return Note(41 - 12) - - def closed_hi_hat(self): - return Note(42 - 12) - - def high_floor_tom(self): - return Note(43 - 12) - - def pedal_hi_hat(self): - return Note(44 - 12) - - def low_tom(self): - return Note(45 - 12) - - def open_hi_hat(self): - return Note(46 - 12) - - def low_mid_tom(self): - return Note(47 - 12) - - def hi_mid_tom(self): - return Note(48 - 12) +instruments = [ + "Acoustic Grand Piano", + "Bright Acoustic Piano", + "Electric Grand Piano", + "Honky-tonk Piano", + "Electric Piano 1", + "Electric Piano 2", + "Harpsichord", + "Clavi", + "Celesta", + "Glockenspiel", + "Music Box", + "Vibraphone", + "Marimba", + "Xylophone", + "Tubular Bells", + "Dulcimer", + "Drawbar Organ", + "Percussive Organ", + "Rock Organ", + "Church Organ", + "Reed Organ", + "Accordion", + "Harmonica", + "Tango Accordion", + "Acoustic Guitar (nylon)", + "Acoustic Guitar (steel)", + "Electric Guitar (jazz)", + "Electric Guitar (clean)", + "Electric Guitar (muted)", + "Overdriven Guitar", + "Distortion Guitar", + "Guitar harmonics", + "Acoustic Bass", + "Electric Bass (finger)", + "Electric Bass (pick)", + "Fretless Bass", + "Slap Bass 1", + "Slap Bass 2", + "Synth Bass 1", + "Synth Bass 2", + "Violin", + "Viola", + "Cello", + "Contrabass", + "Tremolo Strings", + "Pizzicato Strings", + "Orchestral Harp", + "Timpani", + "String Ensemble 1", + "String Ensemble 2", + "SynthStrings 1", + "SynthStrings 2", + "Choir Aahs", + "Voice Oohs", + "Synth Voice", + "Orchestra Hit", + "Trumpet", + "Trombone", + "Tuba", + "Muted Trumpet", + "French Horn", + "Brass Section", + "SynthBrass 1", + "SynthBrass 2", + "Soprano Sax", + "Alto Sax", + "Tenor Sax", + "Baritone Sax", + "Oboe", + "English Horn", + "Bassoon", + "Clarinet", + "Piccolo", + "Flute", + "Recorder", + "Pan Flute", + "Blown Bottle", + "Shakuhachi", + "Whistle", + "Ocarina", + "Lead1 (square)", + "Lead2 (sawtooth)", + "Lead3 (calliope)", + "Lead4 (chiff)", + "Lead5 (charang)", + "Lead6 (voice)", + "Lead7 (fifths)", + "Lead8 (bass + lead)", + "Pad1 (new age)", + "Pad2 (warm)", + "Pad3 (polysynth)", + "Pad4 (choir)", + "Pad5 (bowed)", + "Pad6 (metallic)", + "Pad7 (halo)", + "Pad8 (sweep)", + "FX1 (rain)", + "FX2 (soundtrack)", + "FX 3 (crystal)", + "FX 4 (atmosphere)", + "FX 5 (brightness)", + "FX 6 (goblins)", + "FX 7 (echoes)", + "FX 8 (sci-fi)", + "Sitar", + "Banjo", + "Shamisen", + "Koto", + "Kalimba", + "Bag pipe", + "Fiddle", + "Shanai", + "Tinkle Bell", + "Agogo", + "Steel Drums", + "Woodblock", + "Taiko Drum", + "Melodic Tom", + "Synth Drum", + "Reverse Cymbal", + "Guitar Fret Noise", + "Breath Noise", + "Seashore", + "Bird Tweet", + "Telephone Ring", + "Helicopter", + "Applause", + "Gunshot", +] + + +def get_instrument_number(instrument_name): + number = instruments.index(instrument_name) + return number - def crash_cymbal_1(self): - return Note(49 - 12) - def high_tom(self): - return Note(50 - 12) - - def ride_cymbal_1(self): - return Note(51 - 12) - - def chinese_cymbal(self): - return Note(52 - 12) - - def ride_bell(self): - return Note(53 - 12) - - def tambourine(self): - return Note(54 - 12) - - def splash_cymbal(self): - return Note(55 - 12) - - def cowbell(self): - return Note(56 - 12) - - def crash_cymbal_2(self): - return Note(57 - 12) - - def vibraslap(self): - return Note(58 - 12) - - def ride_cymbal_2(self): - return Note(59 - 12) - - def hi_bongo(self): - return Note(60 - 12) - - def low_bongo(self): - return Note(61 - 12) - - def mute_hi_conga(self): - return Note(62 - 12) - - def open_hi_conga(self): - return Note(63 - 12) - - def low_conga(self): - return Note(64 - 12) - - def high_timbale(self): - return Note(65 - 12) - - def low_timbale(self): - return Note(66 - 12) - - def high_agogo(self): - return Note(67 - 12) - - def low_agogo(self): - return Note(68 - 12) - - def cabasa(self): - return Note(69 - 12) - - def maracas(self): - return Note(70 - 12) - - def short_whistle(self): - return Note(71 - 12) - - def long_whistle(self): - return Note(72 - 12) - - def short_guiro(self): - return Note(73 - 12) - - def long_guiro(self): - return Note(74 - 12) - - def claves(self): - return Note(75 - 12) - - def hi_wood_block(self): - return Note(76 - 12) - - def low_wood_block(self): - return Note(77 - 12) - - def mute_cuica(self): - return Note(78 - 12) - - def open_cuica(self): - return Note(79 - 12) - - def mute_triangle(self): - return Note(80 - 12) +class MidiInstrument(Instrument): + names = instruments - def open_triangle(self): - return Note(81 - 12) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.number = get_instrument_number(self.name) diff --git a/mingus/containers/midi_percussion.py b/mingus/containers/midi_percussion.py new file mode 100644 index 00000000..7b1ad520 --- /dev/null +++ b/mingus/containers/midi_percussion.py @@ -0,0 +1,75 @@ +""" +MIDI percussion is treated as one instrument, with each type of percussion instrument being a "key" +(i.e. like a key on a piano. +""" + +percussion_instruments = { + 'Acoustic Bass Drum': 35, + 'Bass Drum 1': 36, + 'Side Stick': 37, + 'Acoustic Snare': 38, + 'Hand Clap': 39, + 'Electric Snare': 40, + 'Low Floor Tom': 41, + 'Closed Hi Hat': 42, + 'High Floor Tom': 43, + 'Pedal Hi-Hat': 44, + 'Low Tom': 45, + 'Open Hi-Hat': 46, + 'Low-Mid Tom': 47, + 'Hi Mid Tom': 48, + 'Crash Cymbal 1': 49, + 'High Tom': 50, + 'Ride Cymbal 1': 51, + 'Chinese Cymbal': 52, + 'Ride Bell': 53, + 'Tambourine': 54, + 'Splash Cymbal': 55, + 'Cowbell': 56, + 'Crash Cymbal 2': 57, + 'Vibraslap': 58, + 'Ride Cymbal 2': 59, + 'Hi Bongo': 60, + 'Low Bongo': 61, + 'Mute Hi Conga': 62, + 'Open Hi Conga': 63, + 'Low Conga': 64, + 'High Timbale': 65, + 'Low Timbale': 66, + 'High Agogo': 67, + 'Low Agogo': 68, + 'Cabasa': 69, + 'Maracas': 70, + 'Short Whistle': 71, + 'Long Whistle': 72, + 'Short Guiro': 73, + 'Long Guiro': 74, + 'Claves': 75, + 'Hi Wood Block': 76, + 'Low Wood Block': 77, + 'Mute Cuica': 78, + 'Open Cuica': 79, + 'Mute Triangle': 80, + 'Open Triangle': 81 +} + + +def percussion_index_to_name(index): + for name, i in percussion_instruments.items(): + if i == index: + return name + return 'Unknown' + + +class MidiPercussion: + def __init__(self, bank=128): + self.bank = bank + self.number = 1 + self.name = 'Percussion' + + def to_json(self): + d = { + 'class_name': self.__class__.__name__, + 'bank': self.bank, + } + return d diff --git a/mingus/containers/midi_snippet.py b/mingus/containers/midi_snippet.py new file mode 100644 index 00000000..b4e33081 --- /dev/null +++ b/mingus/containers/midi_snippet.py @@ -0,0 +1,78 @@ +from typing import Optional + +import mido + +from mingus.containers import PercussionNote +import mingus.tools.mingus_json as mingus_json + + +class MidiPercussionSnippet(mingus_json.JsonMixin): + """ + Sometimes you might want to create a percusion part in another program (e.g. Musescore). If you export + it as a MIDI file, you can import it with this class. The result can be added to a Track. + """ + def __init__(self, midi_file_path, start: float = 0.0, length_in_seconds: Optional[float] = None, + n_replications: int = 1): + """ + + :param midi_file_path: + :param start: in seconds + :param length_in_seconds: Original length. Needed for repeats. + :param n_replications: + """ + self.midi_file_path = midi_file_path + self.start = start # in seconds + self.length_in_seconds = length_in_seconds + self.n_replications = n_replications + assert not (n_replications > 1 and length_in_seconds is None), \ + f'If there are replications, then length_in_seconds cannot be None' + + def to_json(self): + snippet_dict = super().to_json() + for param in ("midi_file_path", "start", "length_in_seconds", "n_replications"): + snippet_dict[param] = getattr(self, param) + return snippet_dict + + def put_into_score(self, score: dict, channel: int, bpm: Optional[float] = None): + """ + See: https://majicdesigns.github.io/MD_MIDIFile/page_timing.html + https://mido.readthedocs.io/en/latest/midi_files.html?highlight=tempo#about-the-time-attribute + + :param channel: + :param score: the score dict + :param bpm: the target bpm of the first tempo in the snippet. + + + :return: + """ + midi_data = mido.MidiFile(self.midi_file_path) + + length_in_sec = 0.0 + elapsed_time = self.start + tempo = None + speed = None + for i, track in enumerate(midi_data.tracks): + # print('Track {}: {}'.format(i, track.name)) + for msg in track: + if msg.type == 'note_on': + elapsed_time += mido.tick2second(msg.time, ticks_per_beat=midi_data.ticks_per_beat, tempo=tempo) + + for j in range(self.n_replications): + # The score dict key in milliseconds + key = round((elapsed_time + j * length_in_sec) * 1000.0) + score.setdefault(key, []).append( + { + 'func': 'start_note', + 'note': PercussionNote(None, number=msg.note, velocity=msg.velocity, channel=channel), + 'channel': channel, + 'velocity': msg.velocity + } + ) + elif msg.type == 'set_tempo': + if tempo is None: + speed = bpm / mido.tempo2bpm(msg.tempo) + if self.length_in_seconds: + length_in_sec = self.length_in_seconds / speed + + assert speed is not None, f'Could not set speed for midi snippet: {self.midi_file_path}' + tempo = msg.tempo / speed # microseconds per beat diff --git a/mingus/containers/note.py b/mingus/containers/note.py index 42b278b9..7687e5c1 100644 --- a/mingus/containers/note.py +++ b/mingus/containers/note.py @@ -18,7 +18,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json + from mingus.core import notes, intervals +import mingus.containers.midi_percussion as mp from mingus.containers.mt_exceptions import NoteFormatError from math import log import six @@ -45,13 +48,7 @@ class Note(object): You can use the class NoteContainer to group Notes together in intervals and chords. """ - - name = _DEFAULT_NAME - octave = _DEFAULT_OCTAVE - channel = _DEFAULT_CHANNEL - velocity = _DEFAULT_VELOCITY - - def __init__(self, name="C", octave=4, dynamics=None, velocity=None, channel=None): + def __init__(self, name="C", octave=4, dynamics=None, velocity=64, channel=None): """ :param name: :param octave: @@ -59,18 +56,23 @@ def __init__(self, name="C", octave=4, dynamics=None, velocity=None, channel=Non :param int velocity: Integer (0-127) :param int channel: Integer (0-15) """ + # Save params for json encode and decode + self.channel = channel + if dynamics is None: dynamics = {} - if velocity is not None: - dynamics["velocity"] = velocity + dynamics["velocity"] = velocity + self.velocity = velocity + if channel is not None: dynamics["channel"] = channel if isinstance(name, six.string_types): self.set_note(name, octave, dynamics) elif hasattr(name, "name"): - # Hardcopy Note object + # Hard copy Note object + # noinspection PyUnresolvedReferences self.set_note(name.name, name.octave, name.dynamics) elif isinstance(name, int): self.from_int(name) @@ -315,7 +317,7 @@ def __int__(self): return res def __lt__(self, other): - """Enable the comparing operators on Notes (>, <, \ ==, !=, >= and <=). + """Enable the comparing operators on Notes (>, <, ==, !=, >= and <=). So we can sort() Intervals, etc. @@ -350,3 +352,52 @@ def __ge__(self, other): def __repr__(self): """Return a helpful representation for printing Note classes.""" return "'%s-%d'" % (self.name, self.octave) + + def to_json(self): + d = { + 'class_name': self.__class__.__name__, + 'name': self.name, + 'octave': self.octave, + 'velocity': self.velocity, + 'channel': self.channel + } + return d + + +class PercussionNote(Note): + """Percusion notes do not have a name of the staff (e.g. C or F#)""" + + # noinspection PyMissingConstructor + def __init__(self, name, number=None, velocity=64, channel=None, duration=None): + """ + Set duration in milliseconds if you want to stop the instrument before it stops itself. + For example, a player might manual stop a triangle after 1 second. + """ + self.name = name + if number is None: + self.key_number = mp.percussion_instruments[name] + else: + self.key_number = number + self.name = str(number) + + assert 0 <= velocity < 128, 'Velocity must be between 0 and 127' + self.velocity = velocity + self.channel = channel + self.duration = duration + + def __int__(self): + return self.key_number + + def __repr__(self): + return self.name + + def to_json(self): + d = { + 'class_name': self.__class__.__name__, + 'name': self.name, + 'number': self.key_number, + 'velocity': self.velocity, + 'channel': self.channel, + 'duration': self.duration + } + return d diff --git a/mingus/containers/note_container.py b/mingus/containers/note_container.py index d51d3840..ddeb8b14 100644 --- a/mingus/containers/note_container.py +++ b/mingus/containers/note_container.py @@ -18,13 +18,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from mingus.containers.note import Note +from mingus.containers.note import Note, PercussionNote from mingus.core import intervals, chords, progressions from mingus.containers.mt_exceptions import UnexpectedObjectError +from tools.mingus_json import JsonMixin import six -class NoteContainer(object): +class NoteContainer(JsonMixin): """A container for notes. @@ -34,14 +35,16 @@ class NoteContainer(object): It can be used to store single and multiple notes and is required for working with Bars. """ - - notes = [] - def __init__(self, notes=None): - if notes is None: - notes = [] self.empty() - self.add_notes(notes) + + if notes: + self.add_notes(notes) + + def to_json(self): + note_container_dict = super().to_json() + note_container_dict['notes'] = self.notes + return note_container_dict def empty(self): """Empty the container.""" @@ -65,10 +68,11 @@ def add_note(self, note, octave=None, dynamics=None): note = Note(note, self.notes[-1].octave + 1, dynamics) else: note = Note(note, self.notes[-1].octave, dynamics) - if not hasattr(note, "name"): + elif not isinstance(note, Note): raise UnexpectedObjectError( - "Object '%s' was not expected. " "Expecting a mingus.containers.Note object." % note + f"Object {note} was not expected. " "Expecting a mingus.containers.Note object." ) + if note not in self.notes: self.notes.append(note) self.notes.sort() diff --git a/mingus/containers/raw_snippet.py b/mingus/containers/raw_snippet.py new file mode 100644 index 00000000..5d4d477f --- /dev/null +++ b/mingus/containers/raw_snippet.py @@ -0,0 +1,41 @@ +from typing import Optional + + +import mingus.tools.mingus_json as mingus_json + + +class RawSnippet(mingus_json.JsonMixin): + """ + A RawSnippet packages a dict of events and an instrument for a Track. + """ + def __init__(self, events: dict, start: float = 0.0, length_in_seconds: Optional[float] = None, + n_replications: int = 1): + """ + :param events: keys are in milliseconds, values are lists of events + :param start: in seconds + :param length_in_seconds: Original length. Needed for repeats. + :param n_replications: + """ + self.events = events + self.start = start # in seconds + self.length_in_seconds = length_in_seconds + self.n_replications = n_replications + assert not (n_replications > 1 and length_in_seconds is None), \ + f'If there are replications, then length_in_seconds cannot be None' + + def to_json(self): + snippet_dict = super().to_json() + for param in ("events", "start", "length_in_seconds", "n_replications"): + snippet_dict[param] = getattr(self, param) + return snippet_dict + + def put_into_score(self, score: dict, *args, **kwargs): + length_in_msec = (self.length_in_seconds or 0.0) * 1000.0 + elapsed_time = self.start * 1000.0 + + for j in range(self.n_replications): + for event_time, event_list in self.events.items(): + key = round((elapsed_time + event_time + j * length_in_msec)) # The score dict key in milliseconds + if key not in score: + score[key] = [] + score[key] += event_list diff --git a/mingus/containers/track.py b/mingus/containers/track.py index 22869bc3..02255931 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -1,9 +1,4 @@ -# -*- coding: utf-8 -*- - -from __future__ import absolute_import - -# mingus - Music theory Python package, track module. -# Copyright (C) 2008-2009, Bart Spaans +# Copyright (C) 2022, Charles Martin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,40 +13,93 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from enum import Enum +from typing import Optional, Union, TYPE_CHECKING from mingus.containers.mt_exceptions import InstrumentRangeError, UnexpectedObjectError from mingus.containers.note_container import NoteContainer from mingus.containers.bar import Bar import mingus.core.value as value -import six -from six.moves import range - - -class Track(object): - - """A track object. - - The Track class can be used to store Bars and to work on them. - - The class is also designed to be used with Instruments, but this is - optional. - - Tracks can be stored together in Compositions. - """ - - bars = [] - instrument = None - name = "Untitled" # Will be looked for when saving a MIDI file. - tuning = None # Used by tablature - - def __init__(self, instrument=None): - self.bars = [] +import mingus.tools.mingus_json as mingus_json +from mingus.containers.midi_snippet import MidiPercussionSnippet +from mingus.containers.raw_snippet import RawSnippet + + +if TYPE_CHECKING: + from mingus.containers.instrument import MidiInstrument + from mingus.containers.midi_percussion import MidiPercussion + + +class MidiControl(Enum): + VIBRATO = 1 + VOLUME = 7 + PAN = 10 # left to right + EXPRESSION = 11 # soft to loud + SUSTAIN = 64 + REVERB = 91 + CHORUS = 93 + + +class ControlChangeEvent(mingus_json.JsonMixin): + def __init__(self, beat: float, control: Union[MidiControl, int], value: int): + self.beat = beat + if isinstance(control, int): + self.control = MidiControl(control) + else: + self.control = control + self.value = value + + def put_into_score(self, score, channel, bpm): + t = round((self.beat / bpm) * 60000.0) # in milliseconds + score.setdefault(t, []).append( + { + 'func': 'control_change', + 'channel': channel, + 'control': self.control, + 'value': self.value + }) + + def to_json(self): + event_dict = super().to_json() + event_dict["beat"] = self.beat + event_dict["control"] = self.control.value + event_dict["value"] = self.value + return event_dict + + +class Track(mingus_json.JsonMixin): + + def __init__(self, instrument: Union["MidiInstrument", "MidiPercussion"], bpm=120.0, name=None, + bars: Optional[list] = None, snippets: Optional[list] = None): + self.bars = bars or [] + self.snippets = snippets or [] + self.events = [] + self.bpm = bpm self.instrument = instrument + self.name = name - def add_bar(self, bar): + def add_bar(self, bar, n_times=1): """Add a Bar to the current track.""" - self.bars.append(bar) + for _ in range(n_times): + self.bars.append(bar) return self + def add_snippet(self, snippet): + assert isinstance(snippet, MidiPercussionSnippet) or isinstance(snippet, RawSnippet), "Invalid snippet" + self.snippets.append(snippet) + + def add_event(self, event): + """For doing stuff like turning on chorus""" + self.events.append(event) + + def repeat(self, n_repetitions): + """The terminology here might be confusing. If a section is played only once, it has 0 repetitions.""" + if n_repetitions > 0: + self.bars = self.bars * (n_repetitions + 1) + for snippet in self.snippets: + assert snippet.length_in_beats is not None, \ + "To repeat a snippet, the snippet must have a length_in_beats" + snippet.n_repetitions = n_repetitions + def add_notes(self, note, duration=None): """Add a Note, note as string or NoteContainer to the last Bar. @@ -64,12 +112,12 @@ def add_notes(self, note, duration=None): attached to the Track, but the note turns out not to be within the range of the Instrument. """ - if self.instrument != None: + if self.instrument is not None: if not self.instrument.can_play_notes(note): raise InstrumentRangeError( "Note '%s' is not in range of the instrument (%s)" % (note, self.instrument) ) - if duration == None: + if duration is None: duration = 4 # Check whether the last bar is full, if so create a new bar and add the @@ -180,7 +228,7 @@ def __add__(self, value): return self.add_bar(value) elif hasattr(value, "notes"): return self.add_notes(value) - elif hasattr(value, "name") or isinstance(value, six.string_types): + elif hasattr(value, "name") or isinstance(value, str): return self.add_notes(value) def test_integrity(self): @@ -218,3 +266,12 @@ def __repr__(self): def __len__(self): """Enable the len() function for Tracks.""" return len(self.bars) + + def to_json(self): + track_dict = super().to_json() + track_dict['instrument'] = self.instrument + track_dict['bpm'] = self.bpm + track_dict['name'] = self.name + track_dict['bars'] = self.bars + track_dict['snippets'] = self.snippets + return track_dict diff --git a/mingus/core/keys.py b/mingus/core/keys.py index 527cfd57..5ba3d002 100644 --- a/mingus/core/keys.py +++ b/mingus/core/keys.py @@ -28,6 +28,7 @@ from mingus.core import notes from mingus.core.mt_exceptions import NoteFormatError, RangeError +from tools.mingus_json import JsonMixin keys = [ ("Cb", "ab"), # 7 b @@ -166,7 +167,7 @@ def relative_minor(key): raise NoteFormatError("'%s' is not a major key" % key) -class Key(object): +class Key(JsonMixin): """A key object.""" @@ -190,6 +191,11 @@ def __init__(self, key="C"): self.signature = get_key_signature(self.key) + def to_json(self): + d = super().to_json() + d['key'] = self.key + return d + def __eq__(self, other): if self.key == other.key: return True diff --git a/mingus/extra/musicxml.py b/mingus/extra/musicxml.py index d3f1860c..ecd8db8d 100644 --- a/mingus/extra/musicxml.py +++ b/mingus/extra/musicxml.py @@ -280,7 +280,7 @@ def _composition2musicxml(comp): # the MIDI # channels? program = doc.createElement("midi-program") - program.appendChild(doc.createTextNode(str(t.instrument.instrument_nr))) + program.appendChild(doc.createTextNode(str(t.instrument.number))) midi.appendChild(channel) midi.appendChild(program) score_part.appendChild(midi) diff --git a/mingus/midi/fluid_synth2.py b/mingus/midi/fluid_synth2.py new file mode 100644 index 00000000..242f1432 --- /dev/null +++ b/mingus/midi/fluid_synth2.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# mingus - Music theory Python package, fluidsynth module. +# Copyright (C) 2008-2009, Bart Spaans +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# noinspection HttpUrlsUsage +"""FluidSynth support for mingus. + +FluidSynth is a software MIDI synthesizer which allows you to play the +containers in mingus.containers real-time. To work with this module, you'll +need fluidsynth and a nice instrument collection (look here: +http://www.hammersound.net, go to Sounds → Soundfont Library → Collections). + +An alternative is the FreePats project. You can download a SoundFont from +https://freepats.zenvoid.org/SoundSets/general-midi.html. Note that you will +need to uncompress the .tar.xz archive to get the actual .sf2 file. +""" +import time +import wave + +from mingus.midi import pyfluidsynth as fs +from mingus.midi.sequencer2 import Sequencer + + +class FluidSynthPlayer: + def __init__(self, sound_font_path, driver=None, file=None, gain=0.2): + super().__init__() + self.fs = fs.Synth(gain=gain) + self.sfid = None + self.sound_font_path = sound_font_path + if file is not None: + self.start_recording(file) + else: + self.start_audio_output(driver) + + def __del__(self): + self.fs.delete() + + def start_audio_output(self, driver=None): + """Start the audio output. + + The optional driver argument can be any of 'alsa', 'oss', 'jack', + 'portaudio', 'sndmgr', 'coreaudio', 'Direct Sound', 'dsound', + 'pulseaudio'. Not all drivers will be available for every platform. + """ + self.fs.start(driver) + + def start_recording(self, file="mingus_dump.wav"): + """Initialize a new wave file for recording.""" + w = wave.open(file, "wb") + w.setnchannels(2) + w.setsampwidth(2) + w.setframerate(44100) + self.wav = w + + # Implement Sequencer's interface + def play_event(self, note, channel, velocity): + self.fs.noteon(channel, note, velocity) + + def stop_event(self, note, channel): + self.fs.noteoff(channel, note) + + def load_sound_font(self): + self.sfid = self.fs.sfload(self.sound_font_path) + assert self.sfid != -1, f'Could not load soundfont: {self.sound_font_path}' + + def set_instrument(self, channel, instr, bank): + # Delay loading sound font because it is slow + if self.sfid is None: + self.sfid = self.fs.sfload(self.sound_font_path) + assert self.sfid != -1, f'Could not load soundfont: {self.sound_font_path}' + self.fs.program_reset() + self.fs.program_select(channel, self.sfid, bank, instr) + + def sleep(self, seconds): + if hasattr(self, "wav"): + samples = fs.raw_audio_string(self.fs.get_samples(int(seconds * 44100))) + self.wav.writeframes(bytes(samples)) + else: + time.sleep(seconds) + + def control_change(self, channel, control, value): + """Send a control change message. + + See the MIDI specification for more information. + """ + if control < 0 or control > 128: + return False + if value < 0 or value > 128: + return False + self.fs.cc(channel, control, value) + return True + + def modulation(self, channel, value): + """Set the modulation.""" + return self.control_change(channel, 1, value) + + def main_volume(self, channel, value): + """Set the main volume.""" + return self.control_change(channel, 7, value) + + def pan(self, channel, value): + """Set the panning.""" + return self.control_change(channel, 10, value) + + def play_note(self, note, channel, velocity): + self.play_event(int(note) + 12, int(channel), int(velocity)) + + def play_percussion_note(self, note, channel, velocity): + self.play_event(int(note), int(channel), int(velocity)) + + def stop_note(self, note, channel): + self.stop_event(int(note) + 12, int(channel)) + + def stop_percussion_note(self, note, channel): + self.stop_event(int(note), int(channel)) + + def play_tracks(self, tracks, channels, bpm=120.0, start_time=1, end_time=50_000_000, stop_func=None): + sequencer = Sequencer() + sequencer.play_Tracks(tracks, channels, bpm=bpm) + sequencer.play_score(self, stop_func=stop_func, start_time=start_time, end_time=end_time) + + def stop_everything(self): + """Stop all the notes on all channels.""" + for x in range(118): + for c in range(16): + self.stop_note(x, c) diff --git a/mingus/midi/get_soundfont_path.py b/mingus/midi/get_soundfont_path.py new file mode 100644 index 00000000..d113fd0d --- /dev/null +++ b/mingus/midi/get_soundfont_path.py @@ -0,0 +1,8 @@ +"""Centralize this in case we want to enhance it in the future.""" +import os + + +def get_soundfont_path(): + soundfont_path = os.getenv('MINGUS_SOUNDFONT') + assert soundfont_path, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + return soundfont_path diff --git a/mingus/midi/midi_file_in.py b/mingus/midi/midi_file_in.py index 67c4cb2d..a33ca3ef 100644 --- a/mingus/midi/midi_file_in.py +++ b/mingus/midi/midi_file_in.py @@ -123,7 +123,7 @@ def MIDI_to_Composition(self, file): elif event["event"] == 12: # program change i = MidiInstrument() - i.instrument_nr = event["param1"] + i.number = event["param1"] t.instrument = i elif event["event"] == 0x0F: # meta event Text diff --git a/mingus/midi/midi_file_out.py b/mingus/midi/midi_file_out.py index 63a9e444..0337748c 100644 --- a/mingus/midi/midi_file_out.py +++ b/mingus/midi/midi_file_out.py @@ -169,7 +169,7 @@ def write_Composition(file, composition, bpm=120, repeat=0, verbose=False): t + b t + b m = MidiInstrument() - m.instrument_nr = 13 + m.number = 13 t.instrument = m t.name = "Track Name Test" write_NoteContainer("test.mid", n) diff --git a/mingus/midi/midi_track.py b/mingus/midi/midi_track.py index 77951367..74e8fdf8 100644 --- a/mingus/midi/midi_track.py +++ b/mingus/midi/midi_track.py @@ -115,7 +115,7 @@ def play_Track(self, track): instr = track.instrument if hasattr(instr, "instrument_nr"): self.change_instrument = True - self.instrument = instr.instrument_nr + self.instrument = instr.number for bar in track: self.play_Bar(bar) diff --git a/mingus/midi/pyfluidsynth.py b/mingus/midi/pyfluidsynth.py index b13dac97..e092a452 100644 --- a/mingus/midi/pyfluidsynth.py +++ b/mingus/midi/pyfluidsynth.py @@ -104,6 +104,8 @@ def cfunc(name, result, *args): delete_fluid_audio_driver = cfunc("delete_fluid_audio_driver", None, ("driver", c_void_p, 1)) delete_fluid_synth = cfunc("delete_fluid_synth", None, ("synth", c_void_p, 1)) delete_fluid_settings = cfunc("delete_fluid_settings", None, ("settings", c_void_p, 1)) + +# https://www.fluidsynth.org/api/group__soundfont__management.html#ga0ba0bc9d4a19c789f9969cd22d22bf66 fluid_synth_sfload = cfunc( "fluid_synth_sfload", c_int, diff --git a/mingus/midi/sequencer.py b/mingus/midi/sequencer.py index 7cb4e829..bb86159b 100644 --- a/mingus/midi/sequencer.py +++ b/mingus/midi/sequencer.py @@ -132,7 +132,7 @@ def control_change(self, channel, control, value): ) return True - def play_Note(self, note, channel=1, velocity=100): + def play_Note(self, note, channel=None, velocity=100): """Play a Note object on a channel with a velocity[0-127]. You can either specify the velocity and channel here as arguments or @@ -141,8 +141,8 @@ def play_Note(self, note, channel=1, velocity=100): """ if hasattr(note, "velocity"): velocity = note.velocity - if hasattr(note, "channel"): - channel = note.channel + if channel is None: + channel = getattr(note, 'channel', 1) self.play_event(int(note) + 12, int(channel), int(velocity)) self.notify_listeners( self.MSG_PLAY_INT, @@ -158,14 +158,10 @@ def play_Note(self, note, channel=1, velocity=100): ) return True - def stop_Note(self, note, channel=1): - """Stop a note on a channel. - - If Note.channel is set, it will take presedence over the channel - argument given here. - """ - if hasattr(note, "channel"): - channel = note.channel + def stop_Note(self, note, channel=None): + """Stop a note on a channel.""" + if channel is None: + channel = getattr(note, 'channel', 1) self.stop_event(int(note) + 12, int(channel)) self.notify_listeners(self.MSG_STOP_INT, {"channel": int(channel), "note": int(note) + 12}) self.notify_listeners(self.MSG_STOP_NOTE, {"channel": int(channel), "note": note}) @@ -234,7 +230,7 @@ def play_Bars(self, bars, channels, bpm=120): by providing one or more of the NoteContainers with a bpm argument. """ self.notify_listeners(self.MSG_PLAY_BARS, {"bars": bars, "channels": channels, "bpm": bpm}) - qn_length = 60.0 / bpm # length of a quarter note + qn_length = 60.0 / bpm # length of a quarter note in seconds tick = 0.0 # place in beat from 0.0 to bar.length cur = [0] * len(bars) # keeps the index of the NoteContainer under # investigation in each of the bars diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py new file mode 100644 index 00000000..680db1d4 --- /dev/null +++ b/mingus/midi/sequencer2.py @@ -0,0 +1,164 @@ +import logging + +import sortedcontainers + +from mingus.containers import PercussionNote +import mingus.tools.mingus_json as mingus_json + + +logging.basicConfig(level=logging.INFO) + + +def calculate_bar_start_time(bpm: float, beats_per_bar: int, bar_number: int) -> int: + """ + Since tracks can have different bpm and beats_per_bar, it's easiest t + + :param bpm: + :param beats_per_bar: + :param bar_number: the first bar is bar_number 1 + :return: time in milliseconds + """ + beat = (bar_number - 1) * beats_per_bar + minutes = beat / bpm + t = round(minutes * 60_000) + return t + + +def calculate_bar_end_time(bpm: float, beats_per_bar: int, bar_number: int) -> int: + """ + :param bpm: + :param beats_per_bar: + :param bar_number: the first bar is bar_number 1 + :return: time in milliseconds + """ + t = calculate_bar_start_time(bpm, beats_per_bar, bar_number + 1) + return t + + +class Sequencer: + """ + This sequencer creates a "score" that is a dict with time in milliseconds as keys and list of + events as the values. + + To build the score, just go through all the tracks, bars, notes, etc... and add keys and events. + Then when playing the score, first sort by keys. + + We use sortedcontainers containers to make that fast for the case where there are thousands of events. + """ + def __init__(self, score=None): + super().__init__() + # Keys will be in milliseconds since the start. Values will be lists of stuff to do. + self.score = score or {} + self.instruments = [] + + # noinspection PyPep8Naming + def play_Track(self, track, channel=1, bpm=120.0): + """Play a Track object.""" + start_time = 0 + for bar in track.bars: + bpm = bar.bpm or bpm + start_time += bar.play(start_time, bpm, channel, self.score) + + for snippet in track.snippets: + snippet.put_into_score(self.score, channel, bpm) + + for event in track.events: + event.put_into_score(self.score, channel, bpm) + + # noinspection PyPep8Naming + def play_Tracks(self, tracks, channels, bpm=None): + """Play a list of Tracks.""" + # Set the instruments. Previously, if an instrument number could not be found, it was set to 1. That can + # be confusing to users, so just crash if it cannot be found. + for track_num, track in enumerate(tracks): + if track.instrument is not None: + self.instruments.append((channels[track_num], track.instrument)) + + # Because of possible changes in bpm, render each track separately + for track, channel in zip(tracks, channels): + bpm = bpm or track.bpm + self.play_Track(track, channel, bpm=bpm) + + # noinspection PyPep8Naming + def play_Composition(self, composition, channels=None, bpm=120): + if channels is None: + channels = [x + 1 for x in range(len(composition.tracks))] + return self.play_Tracks(composition.tracks, channels, bpm) + + def play_score(self, synth, stop_func=None, start_time=0, end_time=50_000_000): + """ + + :param synth: + :param stop_func: a function that returns True if the score should stop playing. We tried + using a global variable for this, that ended up passing around the variable value, not the + reference. + :param start_time: in milliseconds. It might seem easier to pass in the start beat, but tracks can have + different bpm or meter, etc... So time in milliseconds is more universal. There are helper functions + at the top of this module to calculate time from bpm, ... + :param end_time: in milliseconds + :return: None + """ + score = sortedcontainers.SortedDict(self.score) + + for channel, instrument in self.instruments: + synth.set_instrument(channel, instrument.number, instrument.bank) + logging.info(f'Instrument: {instrument.number} Channel: {channel}') + logging.info('--------------\n') + + the_time = 0 + for event_start_time, events in score.items(): + if stop_func and stop_func(): + break + + if event_start_time > end_time: + break + + # Sleep until the next event + dt = event_start_time - the_time + if dt > 0 and event_start_time >= start_time: + synth.sleep(dt / 1000.0) + the_time = event_start_time + + for event in events: + if event['func'] == 'start_note' and event_start_time >= start_time: + if isinstance(event['note'], PercussionNote): + synth.play_percussion_note(event['note'], event['channel'], event['velocity']) + else: + synth.play_note(event['note'], event['channel'], event['velocity']) + + logging.info('Start: {} Note: {note} Velocity: {velocity} Channel: {channel}'. + format(the_time, **event)) + + elif event['func'] == 'end_note': + if isinstance(event['note'], PercussionNote): + synth.stop_percussion_note(event['note'], event['channel']) + else: + synth.stop_note(event['note'], event['channel']) + logging.info('Stop: {} Note: {note} Channel: {channel}'.format(the_time, **event)) + + elif event['func'] == 'control_change': + synth.control_change(event['channel'], event['control'].value, event['value']) + logging.info('Control change: Channel: {channel} Control: {control} Value: {value}'. + format(the_time, **event)) + + logging.info('--------------\n') + synth.sleep(2) # prevent cutoff at the end + + def save_tracks(self, path, tracks, channels, bpm): + self.play_Tracks(tracks, channels, bpm=bpm) + score = sortedcontainers.SortedDict(self.score) + + to_save = { + 'instruments': self.instruments, + 'score': dict(score) + } + + with open(path, 'w') as fp: + mingus_json.dump(to_save, fp, indent=4) + + def load_tracks(self, path): + with open(path, 'r') as fp: + data = mingus_json.load(fp) + + self.instruments = data['instruments'] + self.score = {int(k): v for k, v in data['score'].items()} diff --git a/mingus/tools/__init__.py b/mingus/tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mingus/tools/keyboard_drumset.py b/mingus/tools/keyboard_drumset.py new file mode 100644 index 00000000..72a8401e --- /dev/null +++ b/mingus/tools/keyboard_drumset.py @@ -0,0 +1,515 @@ +import json +import time +import tkinter as tk +from functools import partial +from pathlib import Path +from threading import Thread +from tkinter import ttk +from tkinter.filedialog import askopenfile, asksaveasfile +from typing import Optional + +import midi_percussion as mp + +import mingus.tools.mingus_json as mingus_json +from mingus.containers import PercussionNote, Track +from mingus.containers.raw_snippet import RawSnippet +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path +from mingus.midi.sequencer2 import Sequencer + +# A global variable for communicating with the click track thread +click_track_done = False +player_track_done = False + +# noinspection SpellCheckingInspection +KEYS = ' zxcvbnm,./' + + +class TrackGUI: + def __init__(self, widget=None, track=None, path: Optional[str] = None): + self.widget = widget + self.track = track + self.path = path + + if path: + full_path = Path(path).expanduser() + with open(full_path, 'r') as fp: + self.track = mingus_json.load(fp) + + def destroy_widget(self): + if self.widget: + self.destroy_widget() + self.widget = None + + def load(self): + fp = askopenfile() + if fp: + try: + self.path = fp.name + self.track = mingus_json.load(fp) + except Exception as e: + print(f"An error occurred while writing to the file: {e}") + finally: + # Make sure to close the file after using it + fp.close() + + +class ClickTrack(Thread): + instrument = 85 + + def __init__(self, synth, percussion_channel, bpm, beats_per_bar): + super().__init__() + self.synth = synth + self.percussion_channel = percussion_channel + self.sleep_seconds = (1.0 / bpm) * 60.0 + self.beats_per_bar = beats_per_bar + + def run(self): + global click_track_done + count = 0 + while not click_track_done: + if count == 0: + velocity = 127 + else: + velocity = 50 + self.synth.noteon(self.percussion_channel, self.instrument, velocity) + time.sleep(self.sleep_seconds) + + count += 1 + + if count == self.beats_per_bar: + count = 0 + + +class PlayOld(Thread): + def __init__(self, synth, score): + super().__init__() + self.synth = synth + self.sequencer = Sequencer(score=score) + + def run(self): + global player_track_done + + self.sequencer.play_score(self.synth) + + # while not player_track_done: + # print('beat') + # time.sleep(3) + + print('player_done') + + +class Play(Thread): + def __init__(self, synth, sequencer): + super().__init__() + self.synth = synth + self.sequencer = sequencer + + def run(self): + global player_track_done + + def stop_func(): + global player_track_done + return player_track_done + + self.sequencer.play_score(self.synth, stop_func=stop_func) + + print('player_done') + + +class KeyboardDrumSet: + """ + For creating percussion tracks from the keyboard. This is useful for working out rhythms. It is lacking in + velocity control. + + SECTIONS + + Assign keys to instruments: + + Key: spinner? | Instrument: drop-down | Delete + "Add" Button + + Define Tracks + + DRUM SET FORMAT + Dict saved to a Json file in the format: + + drum_set = { + 'instruments': { + 'z': 'Acoustic Bass Drum', + 'm': 'Acoustic Snare' + }, + 'click_track': { + 'bpm': 120, + 'beats_per_bar': 4, + 'enabled': True + } + } + """ + def __init__(self, setup_synth=True, drum_set=None): + self.recording = {} # keys are times in milliseconds, values are lists of events + self.is_recording = False + self.start_recording_time = None + self.play_click_track = False + + if drum_set is None: + self.drum_set = { + 'instruments': { + 'z': 'Acoustic Bass Drum', + 'm': 'Acoustic Snare' + }, + 'click_track': { + 'bpm': 120, + 'beats_per_bar': 4, + 'enabled': True + }, + 'tracks': [ + '~/python_mingus/tracks/test_percussion.json', + '~/python_mingus/tracks/test_bass.json' + ] + } + else: + self.drum_set = drum_set + + self.instruments = self.drum_set['instruments'] + + if setup_synth: + soundfont_path = get_soundfont_path() + self.synth = FluidSynthPlayer(soundfont_path, gain=1) + self.synth.load_sound_font() + + self.percussion_channel = 1 # can be any channel between 1-128 + bank = 128 # Must be 128 (at least for the Musescore default sound font) + preset = 1 + self.synth.fs.program_select(self.percussion_channel, self.synth.sfid, bank, preset) + else: + self.synth = None + self.sound_font = 'not loaded' + self.percussion_channel = None + + self.velocity = 80 + self.padding = { + 'padx': 2, + 'pady': 2 + } + + self.window = tk.Tk() + self.window.title("Keyboard Drum Set") + + # Instruments --------------------------------------------------------------------------------------------- + self.instrument_frame = ttk.LabelFrame(self.window, text='Instruments', borderwidth=5, relief=tk.GROOVE) + self.instrument_frame.pack(fill=tk.BOTH, expand=tk.YES) + self.instrument_widgets = [] + self.bound_keys = set() + self.render_instrument_section() + + # Click track --------------------------------------------------------------------------------------------- + click_track_frame = ttk.LabelFrame(self.window, text='Click Track', padding=10) + click_track_frame.pack(fill=tk.BOTH, expand=tk.YES) + row = 0 + ttk.Label(click_track_frame, text='BPM').grid(row=row, column=0, sticky=tk.W, **self.padding) + self.bpm = tk.IntVar(value=self.drum_set['click_track']['bpm']) + spin_box = tk.Spinbox(click_track_frame, from_=50, to=200, increment=1, textvariable=self.bpm) + spin_box.grid(row=row, column=1, sticky=tk.W, **self.padding) + + row += 1 + ttk.Label(click_track_frame, text='Beats/Bar').grid(row=row, column=0, sticky=tk.W, **self.padding) + self.beats_per_bar = tk.IntVar(value=self.drum_set['click_track']['beats_per_bar']) + spin_box = tk.Spinbox(click_track_frame, from_=2, to=16, increment=1, textvariable=self.beats_per_bar) + spin_box.grid(row=row, column=1, sticky=tk.W, **self.padding) + + row += 1 + ttk.Label(click_track_frame, text='Enable Clicks').grid(row=row, column=0, sticky=tk.W, **self.padding) + self.play_click_track = tk.BooleanVar(value=self.drum_set['click_track']['enabled']) + tk.Checkbutton(click_track_frame, variable=self.play_click_track).\ + grid(row=row, column=1, sticky=tk.W, **self.padding) + + # Background tracks -------------------------------------------------------------------------------------- + self.tracks_frame = ttk.LabelFrame(self.window, text='Tracks', padding=5) + self.tracks_frame.pack(fill=tk.BOTH, expand=tk.YES) + self.tracks = [TrackGUI(path=path) for path in self.drum_set.get('tracks', [])] + self.render_tracks_section() + + # Recorded Controls -------------------------------------------------------------------------------------- + recorder_frame = ttk.LabelFrame(self.window, text='Recorder', padding=5) + recorder_frame.pack(fill=tk.BOTH, expand=tk.YES) + tk.Button(recorder_frame, text="|<", command=self.rewind).pack(side=tk.LEFT) + tk.Button(recorder_frame, text=">", command=self.play).pack(side=tk.LEFT) + self.start_stop_recording_button = tk.Button(recorder_frame, text="Start", command=self.start_stop_recording) + self.start_stop_recording_button.pack(side=tk.LEFT) + + # Controls ------------------------------------------------------------------------------------------------ + control_button_frame = ttk.LabelFrame(self.window, text='Controls', padding=5) + control_button_frame.pack(fill=tk.BOTH, expand=tk.YES) + tk.Button(control_button_frame, text="Clear", command=self.clear).pack(side=tk.LEFT) + tk.Button(control_button_frame, text="Save", command=self.save).pack(side=tk.LEFT) + tk.Button(control_button_frame, text="Quit", command=self.quit).pack(side=tk.LEFT) + + self.window.mainloop() + + # Instruments ------------------------------------------------------------------------------------------------- + def delete_instrument(self, char): + del self.instruments[char] + self.render_instrument_section() + + def render_instrument_section(self): + # Delete all + for widget in reversed(self.instrument_widgets): + widget.destroy() + self.instrument_widgets = [] + + for key in self.bound_keys: + self.window.bind(key, self.do_nothing) + + self.window.update() + + # Draw all + self.instruments = self.drum_set['instruments'] + row = 0 + for row, (char, instrument) in enumerate(self.instruments.items()): + instrument_label = ttk.Label(self.instrument_frame, text=f'{char} -> {instrument}') + instrument_label.grid(row=row, column=0, sticky=tk.W, **self.padding) + self.instrument_widgets.append(instrument_label) + delete_instrument_button = tk.Button( + self.instrument_frame, + text='Delete', + command=partial(self.delete_instrument, char) + ) + delete_instrument_button.grid(row=row, column=1, sticky=tk.E, **self.padding) + self.instrument_widgets.append(delete_instrument_button) + + self.window.bind(char, self.play_note) + self.bound_keys.add(char) + + row += 1 + buttons = [ + ('Add Instrument', self.make_instrument_popup), + ('Load Drum Set', self.load_drum_set), + ('Save Drum Set', self.save_drum_set), + ] + for column, (button_text, command) in enumerate(buttons): + button = tk.Button(self.instrument_frame, text=button_text, command=command) + button.grid(row=row, column=column, sticky=tk.W, **self.padding) + self.instrument_widgets.append(button) + self.window.update() + + # noinspection PyUnusedLocal + def add_key_and_instrument(self, ev): + if self.new_key.get() and self.new_instrument.get(): + self.save_new_instrument_button['state'] = tk.NORMAL + + def save_instrument(self): + self.top.destroy() + char = self.new_key.get() + self.instruments[char] = self.new_instrument.get() + self.render_instrument_section() + + def make_instrument_popup(self): + padding = {'padx': 2, 'pady': 2} + self.top = tk.Toplevel(self.window) + + row = 0 + ttk.Label(self.top, text='Key:').grid(row=row, column=0, sticky=tk.W, **padding) + self.new_key = tk.StringVar() + ttk.OptionMenu(self.top, self.new_key, *KEYS, command=self.add_key_and_instrument).\ + grid(row=row, column=1, sticky=tk.W, **padding) + + row += 1 + ttk.Label(self.top, text='Instrument:').grid(row=row, column=0, sticky=tk.W, **padding) + self.new_instrument = tk.StringVar() + ttk.OptionMenu(self.top, self.new_instrument, '', *mp.percussion_instruments.keys(), + command=self.add_key_and_instrument).\ + grid(row=row, column=1, sticky=tk.W, **padding) + + row += 1 + self.save_new_instrument_button = tk.Button(self.top, text="Save", command=self.save_instrument, + state=tk.DISABLED) + self.save_new_instrument_button.grid(row=row, column=0, sticky=tk.W, **padding) + tk.Button(self.top, text="Cancel", command=lambda: self.top.destroy()).grid(row=row, column=1, + sticky=tk.W, **padding) + + def load_drum_set(self): + file = askopenfile() + if file: + self.drum_set = json.load(file) + self.render_instrument_section() + + def save_drum_set(self): + files = [ + ('All Files', '*.*'), + ('Drum Sets', '*.json') + ] + file = asksaveasfile(filetypes=files, defaultextension=".json") + if file: + json.dump(self.drum_set, file) + + # Tracks ------------------------------------------------------------------------------------------------------- + def render_tracks_section(self): + # Delete all + for track in reversed(self.tracks): + track.destroy_widget() + self.window.update() + + # Draw all + row = 0 + for_sequencer = {'tracks': [], 'channels': [], 'bpm': self.bpm.get()} + for i, track in enumerate(self.tracks, start=10): + messages = tk.Text(self.tracks_frame, height=1) + messages.grid(row=row, column=0, sticky=tk.W, **self.padding) + messages.insert(tk.END, getattr(track.track, 'name', 'Unknown')) + for_sequencer['tracks'].append(track.track) + for_sequencer['channels'].append(i) + row += 1 + + self.sequencer = Sequencer() + self.sequencer.play_Tracks(**for_sequencer) + + row += 1 + column = 0 + button = tk.Button(self.tracks_frame, text='Add Track', command=self.add_track_popup) + button.grid(row=row, column=column, sticky=tk.W, **self.padding) + self.window.update() + + def add_track_popup(self): + padding = {'padx': 2, 'pady': 2} + self.top = tk.Toplevel(self.window) + + row = 0 + ttk.Label(self.top, text='File:').grid(row=row, column=0, sticky=tk.W, **padding) + tk.Button(self.top, text="Load", command=self.load_track).\ + grid(row=row, column=1, sticky=tk.W, **padding) + + row += 1 + self.add_track_button = tk.Button(self.top, text="Add", command=self.add_track, state=tk.DISABLED) + self.add_track_button.grid(row=row, column=0, sticky=tk.W, **padding) + + tk.Button(self.top, text="Cancel", command=lambda: self.top.destroy()).grid(row=row, column=1, + sticky=tk.W, **padding) + + def load_track(self): + self.new_track = TrackGUI() + self.new_track.load() + self.add_track_button['state'] = tk.NORMAL + + def add_track(self): + self.tracks.append(self.new_track) + self.top.destroy() + self.render_tracks_section() + + # Play --------------------------------------------------------------------------------------------------------- + def play_note(self, event): + instrument_name = self.instruments[event.char] + instrument_number = mp.percussion_instruments[instrument_name] + + if self.synth is not None: + self.synth.fs.noteon(self.percussion_channel, instrument_number, self.velocity) + + if self.is_recording and self.start_recording_time is not None: + start_key = int((time.time() - self.start_recording_time) * 1000.0) # in milliseconds + note = PercussionNote(name=None, number=instrument_number, velocity=64, channel=self.percussion_channel) + self.recording.setdefault(start_key, []).append( + { + 'func': 'start_note', + 'note': note, + 'channel': self.percussion_channel, + 'velocity': note.velocity + } + ) + else: + print('Note did not play because synth is not setup.') + + def do_nothing(self, event): + pass + + def start_click_track(self): + global click_track_done + click_track_done = False + + if self.percussion_channel: + self.click_thread = \ + ClickTrack(self.synth.fs, self.percussion_channel, self.bpm.get(), self.beats_per_bar.get()) + self.click_thread.start() + else: + print('Click track does not work because synth is not setup.') + + def stop_click_track(self): + global click_track_done + click_track_done = True + + try: + del self.click_thread + except: + pass + + def start_stop_recording(self): + global player_track_done + + if self.is_recording: + self.is_recording = False + self.start_stop_recording_button['text'] = 'Start' + + if self.play_click_track.get(): + self.stop_click_track() + self.start_recording_time = None + player_track_done = True + else: + self.is_recording = True + self.start_stop_recording_button['text'] = 'Stop' + + # if self.play_click_track.get(): + # self.start_click_track() + + # self.sequencer.play_score(self.synth) + player = Play(self.synth, self.sequencer) + player_track_done = False + player.start() + + self.start_recording_time = time.time() + print('recording started') + + def rewind(self): + pass + + def play(self): + from mingus.containers.midi_percussion import MidiPercussion + global player_track_done + + snippet = RawSnippet(self.recording) + track = Track(instrument=MidiPercussion(), snippets=[snippet]) + sequencer = Sequencer() # TODO: prob want to add to existing sequencer + sequencer.play_Track(track, channel=self.percussion_channel) + player_track_done = False + player = Play(self.synth, sequencer) + player.start() + + def clear(self): + self.recording = {} + + def quit(self): + global click_track_done + global player_track_done + + click_track_done = True + player_track_done = True + + self.window.withdraw() + self.window.destroy() + + def save(self): + files = [ + ('All Files', '*.*'), + ('Drum Tracks', '*.json') + ] + file = asksaveasfile(filetypes=files, defaultextension=files) + if file: + output = { + 'version': '1', + 'bpm': self.bpm.get(), + 'beats_per_bar': self.beats_per_bar.get(), + 'events': self.recording + } + mingus_json.dump(output, file) + + +if __name__ == '__main__': + KeyboardDrumSet(setup_synth=True) diff --git a/mingus/tools/load_midi_file.py b/mingus/tools/load_midi_file.py new file mode 100644 index 00000000..b82a9b57 --- /dev/null +++ b/mingus/tools/load_midi_file.py @@ -0,0 +1,28 @@ +from collections import defaultdict + +from pathlib import Path + +import mido + + +path = Path.home() / 'drum 2.mid' + +mid = mido.MidiFile(path) + +inst = defaultdict(list) +events = defaultdict(list) + +elapsed_time = 0 +tempo = 500_000 +for i, track in enumerate(mid.tracks): + print('Track {}: {}'.format(i, track.name)) + for msg in track: + if msg.type == 'note_on': + elapsed_time += mido.tick2second(msg.time, ticks_per_beat=mid.ticks_per_beat, tempo=tempo) + print(f'Elapsed: {elapsed_time} instrument: {msg.note} velocity: {msg.velocity}') + elif msg.type == 'set_tempo': + tempo = msg.tempo + print(msg) + + +print('x') diff --git a/mingus/tools/mingus_json.py b/mingus/tools/mingus_json.py new file mode 100644 index 00000000..02b982ad --- /dev/null +++ b/mingus/tools/mingus_json.py @@ -0,0 +1,77 @@ +import json + + +class MingusJSONEncoder(json.JSONEncoder): + + def default(self, obj): + try: + return obj.to_json() + except: + return super().default(obj) + + +def encode(obj, *args, **kwargs): + return MingusJSONEncoder(*args, **kwargs).encode(obj) + + +def dumps(obj, *args, **kwargs): + return encode(obj, *args, **kwargs) + + +def dump(obj, fp, *args, **kwargs): + json_str = dumps(obj, *args, **kwargs) + fp.write(json_str) + + +class MingusJSONDecoder(json.JSONDecoder): + + def __init__(self, *args, **kwargs): + json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) + + # noinspection PyUnresolvedReferences + def object_hook(self, obj): + from mingus.containers import PercussionNote, Note, Bar, MidiInstrument, Track, NoteContainer + from mingus.containers.track import ControlChangeEvent + from mingus.containers.midi_percussion import MidiPercussion + from mingus.core.keys import Key + + # handle your custom classes + if isinstance(obj, dict): + class_name = obj.get('class_name') + if class_name: + params = obj + params.pop('class_name', None) + obj = eval(f'{class_name}(**params)') + return obj + + # handling the resolution of nested objects + if isinstance(obj, dict): + for key in list(obj): + obj[key] = self.object_hook(obj[key]) + return obj + + if isinstance(obj, list): + for i in range(0, len(obj)): + obj[i] = self.object_hook(obj[i]) + return obj + + return obj + + +def decode(json_str): + return MingusJSONDecoder().decode(json_str) + + +def loads(json_str): + return decode(json_str) + + +def load(fp): + json_str = fp.read() + return loads(json_str) + + +class JsonMixin: + def to_json(self): + d = {'class_name': self.__class__.__name__} + return d diff --git a/mingus/tools/percussion_composer.pkl b/mingus/tools/percussion_composer.pkl new file mode 100644 index 00000000..8ecbf443 --- /dev/null +++ b/mingus/tools/percussion_composer.pkl @@ -0,0 +1 @@ +[{"name": "Acoustic Bass Drum", "beats": [1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, {"name": "Acoustic Snare", "beats": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}] \ No newline at end of file diff --git a/mingus/tools/percussion_composer.py b/mingus/tools/percussion_composer.py new file mode 100644 index 00000000..aa3916d6 --- /dev/null +++ b/mingus/tools/percussion_composer.py @@ -0,0 +1,156 @@ +from functools import partial +import os +import json +from pathlib import Path + +import tkinter as tk +from tkinter import ttk + +from mingus.containers import Bar, Track, PercussionNote +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.containers.midi_percussion import MidiPercussion, percussion_instruments + + +class PercussionComposer: + def __init__(self, setup_synth=True, instruments_path='percussion_composer.pkl'): + """ + + :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed + :type setup_synth: bool + """ + self.instruments_path = Path(instruments_path) + if setup_synth: + self.setup_synth() + else: + self.synth = None + self.sound_font = 'not loaded' + + padding = { + 'padx': 10, + 'pady': 10 + } + + self.root = tk.Tk() + self.root.title("Percussion Composer") + + self.composer_frame = tk.Frame(self.root) + self.composer_frame.grid(row=0, column=0) + + control_frame = tk.Frame(self.root, **padding) + control_frame.grid(row=1, column=0) + + # Composer frame + self.note_duration = 32 + + if os.path.exists(self.instruments_path): + self.instruments = self.from_json(path=instruments_path) + else: + self.instruments = [self.make_instrument()] + + self.composer_row = 0 + for instrument in self.instruments: + self.add_instrument(row=self.composer_row, instrument=instrument) + self.composer_row += 1 + + # Controls + tk.Button(control_frame, text="Add Instrument", command=self.add_instrument).pack(side='left') + tk.Button(control_frame, text="Play", command=self.play).pack(side='left') + tk.Button(control_frame, text="Save", command=self.to_json).pack(side='left') + tk.Button(control_frame, text="Quit", command=self.quit).pack(side='left') + + self.root.mainloop() + + def setup_synth(self): + self.soundfont_path = os.getenv('MINGUS_SOUNDFONT') + assert self.soundfont_path, \ + 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + + self.synth = FluidSynthPlayer(self.soundfont_path, gain=1.0) + + def do_something(self, row, col): + pass + + def make_instrument(self, name=''): + instrument = { + 'name': tk.StringVar(value=name), + 'beats': [tk.IntVar(0) for _ in range(self.note_duration)] + } + return instrument + + def add_instrument(self, row=None, instrument=None): + row = row or self.composer_row + + if instrument is None: + instrument = self.make_instrument() + self.instruments.append(instrument) + + column = 0 + ttk.Combobox( + self.composer_frame, + textvariable=instrument['name'], + values=list(percussion_instruments.keys()), + state="READONLY" + ).grid(row=row, column=column, sticky=tk.W) + column += 1 + + for beat in instrument['beats']: + ttk.Checkbutton( + self.composer_frame, + variable=beat, + onvalue=1, + offvalue=0, + command=partial(self.do_something, row, column - 1) + ).grid(row=row, column=column, sticky=tk.W) + column += 1 + self.composer_row = row + + def play(self): + tracks = [] + for instrument in self.instruments: + track = Track(MidiPercussion()) + bar = Bar() + note = PercussionNote(instrument['name'].get(), velocity=62) + for beat in instrument['beats']: + if beat.get(): + bar.place_notes([note], self.note_duration) + else: + bar.place_rest(self.note_duration) + track.add_bar(bar) + tracks.append(track) + + self.synth.play_tracks(tracks, range(1, len(tracks) + 1)) + + def to_json(self, path=None): + path = path or self.instruments_path + d = [] + for instrument in self.instruments: + d.append( + { + 'name': instrument['name'].get(), + 'beats': [beat.get() for beat in instrument['beats']] + } + ) + with open(path, 'w') as fp: + json.dump(d, fp) + + @staticmethod + def from_json(path): + with open(path, 'r') as fp: + data = json.load(fp) + + instruments = [] + for instrument_data in data: + instrument = { + 'name': tk.StringVar(value=instrument_data['name']), + 'beats': [tk.IntVar(value=note) for note in instrument_data['beats']] + } + instruments.append(instrument) + return instruments + + def quit(self): + self.root.withdraw() + self.root.destroy() + + +if __name__ == '__main__': + PercussionComposer() diff --git a/mingus/tools/player.py b/mingus/tools/player.py new file mode 100644 index 00000000..ee6267ec --- /dev/null +++ b/mingus/tools/player.py @@ -0,0 +1,87 @@ +import json +import importlib +import time +import tkinter as tk +from functools import partial +from pathlib import Path +from threading import Thread +from tkinter import ttk +from tkinter.filedialog import askopenfile, asksaveasfile +from typing import Optional + +import midi_percussion as mp + +import mingus.tools.mingus_json as mingus_json +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path +from mingus.midi.sequencer2 import Sequencer + + +# A global variable for communicating with the click track thread +player_track_done = False + + +def load_tracks(module_name): + module = importlib.import_module(module_name) + importlib.reload(module) + # noinspection PyUnresolvedReferences + tracks, channels = module.play_in_player(n_times=1) + return tracks, channels + + +def stop_func(): + global player_track_done + return player_track_done + + +class Play(Thread): + def __init__(self, synth, module_name): + super().__init__() + self.synth = synth + self.tracks, self.channels = load_tracks(module_name) + + def run(self): + self.synth.play_tracks(self.tracks, self.channels, stop_func=stop_func) + print('player_done') + + +class Player: + """ + Reloading the synth is too slow. This keeps the synth running and loads .py file as needed and plays them + """ + def __init__(self): + soundfont_path = get_soundfont_path() + self.synth = FluidSynthPlayer(soundfont_path, gain=1.0) + print("Loading soundfont...") + self.synth.load_sound_font() + print("done") + + self.module_name = 'mingus_examples.blues' + + self.main = tk.Tk() + # Buttons ------------------------------------------------------------------------------------------------- + tk.Button(self.main, text="Play", command=self.play).pack() + tk.Button(self.main, text="Stop", command=self.stop_playback).pack() + tk.Button(self.main, text="Quit", command=self.quit).pack() + + self.main.mainloop() + + def quit(self): + self.main.withdraw() + self.main.destroy() + + def play(self): + global player_track_done + player_track_done = False + + play = Play(self.synth, self.module_name) + play.start() + + @staticmethod + def stop_playback(): + global player_track_done + player_track_done = True + + +if __name__ == "__main__": + Player() diff --git a/mingus_examples/blues.py b/mingus_examples/blues.py new file mode 100644 index 00000000..507b4dc0 --- /dev/null +++ b/mingus_examples/blues.py @@ -0,0 +1,201 @@ +import copy +from pathlib import Path + +from mingus.containers import Bar, Track, PercussionNote, Note +from mingus.containers.track import ControlChangeEvent, MidiControl +from mingus.containers import MidiInstrument +from mingus.containers.midi_snippet import MidiPercussionSnippet +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.containers.midi_percussion import MidiPercussion +from mingus.midi.get_soundfont_path import get_soundfont_path +from mingus.midi.sequencer2 import Sequencer, calculate_bar_start_time, calculate_bar_end_time +import mingus.tools.mingus_json as mingus_json + + +soundfont_path = get_soundfont_path() + + +def melody(n_times): + rest_bar = Bar() + rest_bar.place_rest(1) + + i_bar = Bar() + i_bar.place_notes(Note('C-5', velocity=60), 16.0 / 2.5) + i_bar.place_notes(Note('C-5', velocity=50), 16.0 / 1.5) + i_bar.place_rest(8.0 / 5.0) + + i_bar_2 = Bar() + i_bar_2.place_rest(8.0 / 5.0) + i_bar_2.place_notes(Note('C-5'), 8.0 / 3.0) + + turn_around = Bar() + turn_around.place_notes(Note('C-5', velocity=60), 4) + turn_around.place_notes(Note('C-5', velocity=80), 4) + turn_around.place_rest(4.0 / 2.0) + + iv_bar = copy.deepcopy(i_bar) + iv_bar.transpose("4") + + v_bar = copy.deepcopy(i_bar) + v_bar.transpose("5") + + track = Track(MidiInstrument("Trumpet"), name="Trumpet") + track.add_bar(i_bar, n_times=1) + track.add_bar(rest_bar, 2) + track.add_bar(i_bar_2) + + track.add_bar(iv_bar, n_times=1) + track.add_bar(rest_bar) + track.add_bar(i_bar, n_times=1) + track.add_bar(rest_bar) + + track.add_bar(v_bar) + track.add_bar(iv_bar) + track.add_bar(rest_bar) + track.add_bar(turn_around) + + event = ControlChangeEvent(beat=0, control=MidiControl.CHORUS, value=80) + track.add_event(event) + + return track + + +def bass(n_times): + # Make the bars + i_bar = Bar() + i_bar.place_notes(Note('C-3'), 4) + i_bar.place_notes(Note('C-2'), 4) + i_bar.place_notes(Note('Eb-2'), 4) + i_bar.place_notes(Note('E-2'), 4) + + turn_around = Bar() + turn_around.place_notes(Note('Eb-2'), 4) + turn_around.place_notes(Note('E-2'), 4) + turn_around.place_notes(Note('G-3'), 2) + + iv_bar = copy.deepcopy(i_bar) + iv_bar.transpose("4") + + v_bar = copy.deepcopy(i_bar) + v_bar.transpose("5") + + # Make the track + bass_track = Track(MidiInstrument("Acoustic Bass"), name='Bass') + + # Make section + bass_track.add_bar(i_bar, n_times=4) + + bass_track.add_bar(iv_bar, n_times=2) + bass_track.add_bar(i_bar, n_times=2) + + bass_track.add_bar(v_bar) + bass_track.add_bar(iv_bar) + bass_track.add_bar(i_bar) + bass_track.add_bar(turn_around) + + bass_track.repeat(n_times - 1) + + return bass_track + + +def percussion(n_times): + track = Track(MidiPercussion(), name='Percussion') + drum_bar = Bar() + note = PercussionNote('Ride Cymbal 1', velocity=62) + note2 = PercussionNote('Ride Cymbal 1', velocity=32) + drum_bar.place_notes([note2], 4) + drum_bar.place_notes([note], 4) + drum_bar.place_notes([note2], 4) + drum_bar.place_notes([note], 4) + + for i in range(3): + for j in range(4): + track.add_bar(drum_bar) + + track.repeat(n_times - 1) + return track + + +def snare(n_times): + snare_track = Track(MidiPercussion(), name='Snare') + snare = PercussionNote('Acoustic Snare', velocity=62) + snare2 = PercussionNote('Acoustic Snare', velocity=32) + rest_bar = Bar() + rest_bar.place_rest(1) + + drum_turn_around_bar = Bar() + drum_turn_around_bar.place_rest(16.0 / 11.0) + drum_turn_around_bar.place_notes([snare2], 16) + drum_turn_around_bar.place_notes([snare], 16.0 / 3.0) + drum_turn_around_bar.place_notes([snare2], 16) + + for i in range(3): + for j in range(3): + snare_track.add_bar(rest_bar) + snare_track.add_bar(drum_turn_around_bar) + + snare_track.repeat(n_times - 1) + + return snare_track + + +def play(voices, n_times, **kwargs): + fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1), **kwargs) + + +def save(path, voices, bpm=120): + n_times = 1 + channels = range(1, len(voices) + 1) + sequencer = Sequencer() + sequencer.save_tracks(path, [voice(n_times) for voice in voices], channels, bpm=bpm) + + +def load(path): + sequencer = Sequencer() + sequencer.load_tracks(path) + + fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + sequencer.play_score(fluidsynth) + print('x') + + +def play_in_player(n_times): + # noinspection PyListCreation + voices = [] + voices.append(percussion) # percusion is a track + # voices.append(snare) + voices.append(bass) # a track + voices.append(melody) + tracks = [voice(n_times) for voice in voices] + channels = list(range(1, len(voices) + 1)) + return tracks, channels + + +if __name__ == '__main__': + # noinspection PyListCreation + voices = [] + voices.append(percussion) # percusion is a track + # voices.append(snare) + voices.append(bass) # a track + voices.append(melody) + start_time = calculate_bar_start_time(120.0, 4, 9) + end_time = calculate_bar_start_time(120.0, 4, 13) + + play(voices, n_times=1, start_time=start_time, end_time=end_time) + # score_path = Path.home() / 'python_mingus' / 'scores' / 'blues.json' + # save(score_path, voices) + + # Track manipulations + # track = percussion(1) + # track = bass(1) + # track_path = Path.home() / 'python_mingus' / 'tracks' / 'test_bass.json' + # with open(track_path, 'w') as fp: + # mingus_json.dump(track, fp) + + # with open(track_path, 'r') as fp: + # new_track = mingus_json.load(fp) + # + # fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + # fluidsynth.play_tracks([track], [2]) + print('done') diff --git a/mingus_examples/improviser/improviser.py b/mingus_examples/improviser/improviser.py index c44aa897..9700e52c 100755 --- a/mingus_examples/improviser/improviser.py +++ b/mingus_examples/improviser/improviser.py @@ -10,7 +10,7 @@ Based on play_progression.py """ - +import os from mingus.core import progressions, intervals from mingus.core import chords as ch from mingus.containers import NoteContainer, Note @@ -19,7 +19,9 @@ import sys from random import random, choice, randrange -SF2 = "soundfont.sf2" +SF2 = os.getenv('MINGUS_SOUNDFONT') +assert SF2, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + progression = ["I", "bVdim7"] # progression = ["I", "vi", "ii", "iii7", "I7", "viidom7", "iii7", @@ -130,7 +132,7 @@ if play_drums and loop > 0: if t % (len(beats) / 2) == 0 and t != 0: - fluidsynth.play_Note(Note("E", 2), 9, randrange(50, 100)) # snare + fluidsynth.play_Note(Note("E", 2), 9, randrange(50, 100)) # snare, channel 9 else: if random() > 0.8 or t == 0: fluidsynth.play_Note(Note("C", 2), 9, randrange(20, 100)) # bass diff --git a/mingus_examples/multiple_instruments.py b/mingus_examples/multiple_instruments.py new file mode 100644 index 00000000..aac6ca12 --- /dev/null +++ b/mingus_examples/multiple_instruments.py @@ -0,0 +1,60 @@ +""" +This module demonstrates two tracks, each playing a different instrument along with a percussion track. +""" +from mingus.containers import Bar, Track, PercussionNote +from mingus.containers import MidiInstrument +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.containers.midi_percussion import MidiPercussion +from mingus.midi.get_soundfont_path import get_soundfont_path + + +soundfont_path = get_soundfont_path() + +fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + +# Some half notes +a_bar = Bar() +a_bar.place_notes('A-4', 2) # play two successive notes and an instrument without decay to see if we hear 2 notes +a_bar + 'A-4' + +# Some whole notes +c_bar = Bar() +c_bar.place_notes('C-5', 1) + +f_bar = Bar() +f_bar.place_notes('G-5', 1) + +rest_bar = Bar() +rest_bar.place_rest(1) + +t1 = Track(MidiInstrument("Rock Organ")) # an instrument without decay +t1.add_bar(a_bar) # by itself +t1.add_bar(rest_bar) +t1.add_bar(a_bar) # with track 2 +t1.add_bar(rest_bar) +t1.add_bar(rest_bar) + +t2 = Track(MidiInstrument("Choir Aahs")) +t2.add_bar(rest_bar) +t2.add_bar(rest_bar) +t2.add_bar(c_bar) # with track 1 +t2.add_bar(rest_bar) +t2.add_bar(f_bar) # by itself + +t3 = Track(MidiPercussion()) +drum_bar = Bar() +note = PercussionNote('High Tom', velocity=127) +note2 = PercussionNote('High Tom', velocity=62) +drum_bar.place_notes([note], 4) +drum_bar.place_notes([note2], 4) +drum_bar.place_notes([note2], 4) +drum_bar.place_notes([note2], 4) + +t3.add_bar(drum_bar) +t3.add_bar(drum_bar) +t3.add_bar(drum_bar) +t3.add_bar(drum_bar) +t3.add_bar(drum_bar) + +fluidsynth.play_tracks([t1, t2, t3], [1, 2, 3]) +# fluidsynth.play_tracks([t1], [1]) diff --git a/mingus_examples/note_durations.py b/mingus_examples/note_durations.py new file mode 100644 index 00000000..8fc13568 --- /dev/null +++ b/mingus_examples/note_durations.py @@ -0,0 +1,58 @@ +""" +This module demonstrates note durations and rests +""" +from mingus.containers import Bar, Track, PercussionNote +from mingus.containers import MidiInstrument +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.containers.midi_percussion import MidiPercussion +from mingus.midi.get_soundfont_path import get_soundfont_path + + +soundfont_path = get_soundfont_path() + +fluidsynth = FluidSynthPlayer(soundfont_path, driver='coreaudio', gain=1.0) + +# Some half notes +a_bar = Bar() +a_bar.place_notes('A-4', 2) +a_bar + 'A-4' + +# Eight 8th notes +b_bar = Bar() +for _ in range(8): + r = b_bar.place_notes('A-4', 8) + +# 3 eighth notes tied together, quarter note rest, then 3 eighth notes tied together (off the beat) +c_bar = Bar() +c_bar.place_notes('B-4', 8.0 / 3.0) +c_bar.place_rest(4) +c_bar.place_notes('B-4', 8.0 / 3.0) + +# Two whole notes tied together +d_bar = Bar() +d_bar.place_notes('A-4', 1.0 / 2.0) + +rest_bar = Bar() +rest_bar.place_rest(1) + +t1 = Track(MidiInstrument("Acoustic Grand Piano")) +t1.add_bar(a_bar) +t1.add_bar(b_bar) +t1.add_bar(c_bar) +t1.add_bar(d_bar) + + +# Add beat +t3 = Track(MidiPercussion()) +drum_bar = Bar() +note = PercussionNote('High Tom', velocity=127) +note2 = PercussionNote('High Tom', velocity=62) +drum_bar.place_notes([note], 4) +drum_bar.place_notes([note2], 4) +drum_bar.place_notes([note2], 4) +drum_bar.place_notes([note2], 4) + +for _ in range(5): + t3.add_bar(drum_bar) + +fluidsynth.play_tracks([t1, t3], [1, 3]) diff --git a/mingus_examples/percussion_browser.py b/mingus_examples/percussion_browser.py new file mode 100644 index 00000000..529828a5 --- /dev/null +++ b/mingus_examples/percussion_browser.py @@ -0,0 +1,96 @@ +from functools import partial +from time import sleep + +import tkinter as tk + +import mingus.containers.midi_percussion as mp +import mingus.midi.pyfluidsynth as pyfluidsynth +from mingus.midi.get_soundfont_path import get_soundfont_path + + +class PlayPercussion: + def __init__(self, setup_synth=True): + """ + Presents a grid of buttons to playing each instrument + + :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed + """ + self.window = tk.Tk() + self.window.title("Percusion Browser") + self.padding = { + 'padx': 10, + 'pady': 10 + } + + # Messages ---------------------------------------------------------------------------------------------- + self.messages_frame = tk.Frame(self.window) + messages = tk.Text(self.messages_frame, height=2) + messages.pack(fill=tk.BOTH, expand=tk.YES, padx=2, pady=5) + messages.insert(tk.END, 'Loading sound font. Please wait...') + + # Instruments ------------------------------------------------------------------------------------------- + self.instrument_frame = tk.Frame(self.window) + instrument_number = 1 + for row in range(16): + for column in range(8): + name = mp.percussion_index_to_name(instrument_number) + tk.Button( + self.instrument_frame, + text=f"{instrument_number} - {name}", + command=partial(self.play, instrument_number) + ).grid(row=row, column=column, sticky=tk.NSEW) + + instrument_number += 1 + + # Buttons ------------------------------------------------------------------------------------------------- + self.button_frame = tk.Frame(self.window) + tk.Button(self.button_frame, text="Quit", command=self.quit).pack() + + if setup_synth: + self.messages_frame.pack(fill=tk.BOTH, expand=tk.YES, **self.padding) + self.window.after(500, self.setup_synth) + else: + self.instrument_frame.pack(fill=tk.BOTH, expand=tk.YES, **self.padding) + self.button_frame.pack(fill=tk.BOTH, expand=tk.YES, **self.padding) + self.sound_font = 'not loaded' + self.synth = None + self.sfid = None + + self.window.mainloop() + + def setup_synth(self): + self.sound_font = get_soundfont_path() + self.synth = pyfluidsynth.Synth(gain=1.0) + self.sfid = self.synth.sfload(self.sound_font) + self.synth.start() + + # Update GUI + self.messages_frame.pack_forget() + self.button_frame.pack_forget() + self.instrument_frame.pack(fill=tk.BOTH, expand=tk.YES) + self.button_frame.pack(fill=tk.BOTH, expand=tk.YES) + + def quit(self): + self.window.withdraw() + self.window.destroy() + + def play(self, instrument_number): + if self.synth is None: + self.button_frame.pack_forget() + self.instrument_frame.pack_forget() + self.messages_frame.pack(fill=tk.BOTH, expand=tk.YES, **self.padding) + self.window.update() + sleep(0.5) + self.setup_synth() + + channel = 1 + bank = 128 + preset = 1 # seem like it can be any integer + self.synth.program_select(channel, self.sfid, bank, preset) + + velocity = 100 + self.synth.noteon(channel, instrument_number, velocity) + + +if __name__ == '__main__': + PlayPercussion(setup_synth=True) diff --git a/mingus_examples/piano_roll.py b/mingus_examples/piano_roll.py new file mode 100644 index 00000000..2a2a8469 --- /dev/null +++ b/mingus_examples/piano_roll.py @@ -0,0 +1,117 @@ +from collections import defaultdict +import tkinter as tk +from pathlib import Path + +from mingus.midi.sequencer2 import Sequencer +from mingus.containers.midi_percussion import percussion_index_to_name + + +def process_score(score): + """ + Not sure how we are going to handle this. So for now use this function to find instruments and times. + """ + tracks = defaultdict(list) + names = {} + for start_time, events in score.items(): + for event in events: + name = names.setdefault(event['note'].name, percussion_index_to_name(int(event['note'].name))) + tracks[name].append(start_time) + return tracks + + +class Drawer: + def __init__(self, tracks, canvas, width, height, bpm, time_signature, quantization): + self.tracks = tracks + self.canvas = canvas + self.width = width + self.height = height + self.bpm = bpm + self.beats_per_millisecond = bpm * (1.0 / 60000.0) + self.time_signature = time_signature + self.quantization = quantization + + self.n_bars = 4 + self.top_padding = 20.0 + self.label_width = 120.0 + self.bar_width = (self.width - self.label_width) / self.n_bars + + self.colors = { + "major_grid_line": "red", + "minor_grid_line": "blue", + "text": "black" + } + + self.row_height = min((self.height - self.top_padding) / len(self.tracks), 20.0) + + def draw_grid(self): + + # Draw minor grid ------------------------------------------------------------------------------------------ + cell_width = (self.bar_width / self.time_signature[0]) / (self.quantization / self.time_signature[1]) + bottom_y = self.top_padding + (self.row_height * len(self.tracks)) + + x = self.label_width + for _ in range(self.n_bars * self.time_signature[0] * round(self.quantization / self.time_signature[1])): + self.canvas.create_line(x, self.top_padding, x, bottom_y, fill=self.colors["minor_grid_line"]) + x += cell_width + + # Major grid --------------------------------------------------------------------------------------------- + y = self.top_padding + for _ in self.tracks: + self.canvas.create_line(0, y, self.width - 1, y, fill=self.colors["major_grid_line"]) + y += self.row_height + self.canvas.create_line(0, y, self.width - 1, y, fill=self.colors["major_grid_line"]) + + x = self.label_width + for bar_num in range(self.n_bars): + self.canvas.create_line(x, 0, x, bottom_y, fill=self.colors["major_grid_line"]) + self.canvas.create_text(x, self.top_padding / 2.0, text=str(bar_num + 1), fill=self.colors["text"]) + x += self.bar_width + self.canvas.create_line(x, 0, x, bottom_y, fill=self.colors["major_grid_line"]) + + def time_to_x(self, t_milliseconds): + beat = self.beats_per_millisecond * t_milliseconds + x = self.bar_width * beat / self.time_signature[0] + return x + + def draw_notes(self): + y = self.top_padding + 1 + for name, times in self.tracks.items(): + self.canvas.create_text(5.0, y + self.row_height / 2.0, text=name, fill=self.colors["text"], anchor=tk.W) + for time in times: + x = self.time_to_x(time) + self.label_width + self.canvas.create_rectangle(x + 1, y, x + 10, y + self.row_height - 1, fill="green") + y += self.row_height + + def draw_all(self): + self.draw_grid() + self.draw_notes() + + +class PianoRoll: + def __init__(self): + score_path = Path.home() / 'python_mingus' / 'scores' / 'blues.json' + sequencer = Sequencer() + sequencer.load_tracks(score_path) + self.tracks = process_score(sequencer.score) + + bpm = 120.0 + time_signature = (4, 4) + quantization = 8 + canvas_width = 1000 + canvas_height = 200 + + self.root = tk.Tk() + self.root.title("Piano Roll") + self.root.geometry(f"{canvas_width + 50}x{canvas_height + 20}") + + canvas = tk.Canvas(self.root, width=canvas_width, height=canvas_height, bg="white") + canvas.pack() + + drawer = Drawer(self.tracks, canvas, canvas_width, canvas_height, bpm, time_signature, quantization) + drawer.draw_all() + + self.root.mainloop() + + +if __name__ == '__main__': + PianoRoll() diff --git a/mingus_examples/play_c_4.py b/mingus_examples/play_c_4.py new file mode 100644 index 00000000..e4523e03 --- /dev/null +++ b/mingus_examples/play_c_4.py @@ -0,0 +1,20 @@ +from mingus.containers import Bar, Track +from mingus.containers import MidiInstrument +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path + + +soundfont_path = get_soundfont_path() + +fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + +# Some whole notes +c_bar = Bar() +c_bar.place_notes('C-4', 1) + + +t1 = Track(MidiInstrument("Acoustic Grand Piano",)) +t1.add_bar(c_bar) + + +fluidsynth.play_tracks([t1], [1]) diff --git a/mingus_examples/play_drums.py b/mingus_examples/play_drums.py new file mode 100644 index 00000000..e8d5b3cb --- /dev/null +++ b/mingus_examples/play_drums.py @@ -0,0 +1,42 @@ +""" +A simple demonstration of using percussion with fluidsynth. + +This code was developed using the Musescore default sound font. We are not sure how it will work for other +sound-fonts. +""" +from time import sleep + +import midi_percussion as mp +import pyfluidsynth +from mingus.midi.get_soundfont_path import get_soundfont_path + +soundfont_path = get_soundfont_path() + +synth = pyfluidsynth.Synth() +sfid = synth.sfload(soundfont_path) +synth.start() + +# Percussion -------------------------------------------- +percussion_channel = 1 # can be any channel between 1-128 +bank = 128 # Must be 128 (at least for the Musescore default sound font) +preset = 1 # seem like it can be any integer +synth.program_select(percussion_channel, sfid, bank, preset) # percussion + +# Default non-percussion (i.e. piano) +bank = 0 # not percussion +instrument = 1 +synth.program_select(percussion_channel + 1, sfid, bank, instrument) + +velocity = 100 +# Percussion does not use a noteoff +print('Starting') +for _ in range(3): + synth.noteon(percussion_channel, 81, velocity) + sleep(0.5) + synth.noteon(percussion_channel + 1, 45, velocity) + sleep(0.25) + +# Do a hand clap using the midi percussion dict to make it more readable. +synth.noteon(percussion_channel, mp.percussion_instruments['Hand Clap'], velocity) +sleep(0.5) +print('done') diff --git a/mingus_examples/play_effects.py b/mingus_examples/play_effects.py new file mode 100644 index 00000000..f130bf99 --- /dev/null +++ b/mingus_examples/play_effects.py @@ -0,0 +1,66 @@ +from time import sleep + +from mingus.containers import Bar, MidiInstrument, Track +from mingus.containers.instrument import get_instrument_number +from mingus.containers.track import ControlChangeEvent, MidiControl +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path + + +def play_w_chorus(): + """Get single notes working""" + soundfont_path = get_soundfont_path() + + synth = FluidSynthPlayer(soundfont_path, gain=1.0) + + bank = 0 # not percussion + instrument = get_instrument_number("Trumpet") + channel = 1 + synth.set_instrument(channel=channel, instr=instrument, bank=bank) + + velocity = 100 + note_dur = 2.0 + print('Starting') + + def play_note(chorus_level): + if chorus_level: + chorus = MidiControl.CHORUS + synth.control_change(channel=channel, control=chorus, value=chorus_level) + + synth.play_note(note=60, channel=channel, velocity=velocity) + sleep(note_dur) + synth.stop_note(note=60, channel=channel) + sleep(0.1) + + play_note(0) + play_note(64) + play_note(127) + + +def play_with_chorus_2(): + """Get chorus working with tracks.""" + soundfont_path = get_soundfont_path() + + fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + + # Some whole notes + c_bar = Bar() + c_bar.place_notes('C-4', 2) + + track = Track(MidiInstrument("Trumpet", )) + track.add_bar(c_bar) + track.add_bar(c_bar) + track.add_bar(c_bar) + + event = ControlChangeEvent(beat=4, control=MidiControl.CHORUS, value=63) + track.add_event(event) + + event = ControlChangeEvent(beat=8, control=MidiControl.CHORUS, value=127) + track.add_event(event) + + fluidsynth.play_tracks([track], [1]) + + +play_with_chorus_2() + +print('done') diff --git a/mingus_examples/play_progression/play-progression.py b/mingus_examples/play_progression/play-progression.py index 028d1252..0d9bcdfa 100755 --- a/mingus_examples/play_progression/play-progression.py +++ b/mingus_examples/play_progression/play-progression.py @@ -7,7 +7,7 @@ You should specify the SF2 soundfont file. """ - +import os from mingus.core import progressions, intervals from mingus.core import chords as ch from mingus.containers import NoteContainer, Note @@ -16,7 +16,9 @@ import sys from random import random -SF2 = "soundfont_example.sf2" +SF2 = os.getenv('MINGUS_SOUNDFONT') +assert SF2, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + progression = [ "I", "vi", diff --git a/mingus_examples/saved_blues.json b/mingus_examples/saved_blues.json new file mode 100644 index 00000000..344d775e --- /dev/null +++ b/mingus_examples/saved_blues.json @@ -0,0 +1,1842 @@ +{ + "instruments": [ + [ + 1, + { + "class_name": "MidiPercussion", + "bank": 128 + } + ], + [ + 2, + { + "class_name": "MidiInstrument", + "name": "Acoustic Bass", + "note_range": [ + { + "class_name": "Note", + "name": "C", + "octave": 0, + "velocity": 64, + "channel": null + }, + { + "class_name": "Note", + "name": "C", + "octave": 8, + "velocity": 64, + "channel": null + } + ], + "clef": "bass and treble", + "tuning": null, + "bank": 0 + } + ] + ], + "score": { + "0": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "1000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "1500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "2000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "2500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "3000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "3500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "4000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "4500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "5000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "5500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "6000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "6500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "7000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "7500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "8000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "8500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "9000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "9500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "10000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "10500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "11000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "11500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "12000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "12500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "13000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "13500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "14000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "14500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "15000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "15500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "16000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "16500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "17000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Bb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "17500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Bb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "B", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "18000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "B", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "18500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "19000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "F", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "19500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Ab", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "20000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "A", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "20500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "21000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "C", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "21500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "22000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "22500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "Eb", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "23000": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 32, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 32 + }, + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "E", + "octave": 2, + "velocity": 64, + "channel": null + }, + "channel": 2 + }, + { + "func": "start_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2, + "velocity": 64 + } + ], + "23500": [ + { + "func": "start_note", + "note": { + "class_name": "PercussionNote", + "name": "Ride Cymbal 1", + "number": 51, + "velocity": 62, + "channel": null, + "duration": null + }, + "channel": 1, + "velocity": 62 + } + ], + "24000": [ + { + "func": "end_note", + "note": { + "class_name": "Note", + "name": "G", + "octave": 3, + "velocity": 64, + "channel": null + }, + "channel": 2 + } + ] + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a7c51c75 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +six~=1.16.0 +setuptools==60.7.1 +sortedcontainers~=2.4.0 +numpy~=1.22.2 +mido~=1.2.10 +isort==5.10.1 \ No newline at end of file diff --git a/tests/unit/containers/test_bar.py b/tests/unit/containers/test_bar.py index 035e9787..55628293 100644 --- a/tests/unit/containers/test_bar.py +++ b/tests/unit/containers/test_bar.py @@ -166,3 +166,24 @@ def test_determine_progression(self): b + ["C", "E", "G"] b + ["F", "A", "C"] self.assertEqual([[0.0, ["I"]], [0.25, ["IV"]]], b.determine_progression(True)) + + def test_play(self): + b = Bar() + b + ["C", "E", "G"] + b + None + b + ["F", "A", "C"] + score = {} + start_time = 3 + bpm = 120.0 + channel = 0 + b.play(start_time, bpm, channel, score) + + self.assertEqual([3, 503, 1003, 1503], list(score.keys())) + + for key in [3, 1003]: + self.assertEqual(['start_note']*3, [x['func'] for x in score[key]]) + + for key in [503, 1503]: + self.assertEqual(['end_note']*3, [x['func'] for x in score[key]]) + + print('done') \ No newline at end of file diff --git a/tests/unit/containers/test_json.py b/tests/unit/containers/test_json.py new file mode 100644 index 00000000..130ad6ff --- /dev/null +++ b/tests/unit/containers/test_json.py @@ -0,0 +1,69 @@ +from mingus.containers import Bar, Track, PercussionNote, Note, NoteContainer +from mingus.containers.track import ControlChangeEvent, MidiControl +from mingus.containers import MidiInstrument +from mingus.tools import mingus_json + + +def make_bar(): + bar = Bar() + n1 = Note('C-3') + n2 = Note('C-2') + bar.place_notes(n1, 4) + bar.place_notes(n2, 4) + return bar + + +def test_json_notes(): + c_note = Note("C", 5) + p_note = PercussionNote('Ride Cymbal 1', velocity=62) + initial = [c_note, p_note] + + s = mingus_json.encode(initial) + results = mingus_json.decode(s) + + assert results == initial + + +def test_json_note_container(): + n1 = Note('C-3') + n2 = Note('C-2') + note_container = NoteContainer(notes=[n1, n2]) + + s = mingus_json.encode(note_container) + results = mingus_json.decode(s) + + assert results == note_container + + # print('done') + + +def test_json_bars(): + bar = make_bar() + s = mingus_json.encode(bar) + results = mingus_json.decode(s) + + assert results == bar + + +def test_json_track(): + bar = make_bar() + track = Track(MidiInstrument("Acoustic Bass")) + track.add_bar(bar, n_times=4) + + s = mingus_json.encode(track) + results = mingus_json.decode(s) + + assert results == track + + +def test_control_event_json(): + event = ControlChangeEvent(beat=1.0, control=MidiControl.CHORUS, value=127) + + s = mingus_json.encode(event) + results = mingus_json.decode(s) + + # Not sure why this works in other tests, but not here + # assert results == event + + s2 = mingus_json.encode(results) + assert s == s2 diff --git a/tests/unit/midi/__init__.py b/tests/unit/midi/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/midi/test_sequncer2.py b/tests/unit/midi/test_sequncer2.py new file mode 100644 index 00000000..108ad576 --- /dev/null +++ b/tests/unit/midi/test_sequncer2.py @@ -0,0 +1,32 @@ +from mingus.midi.sequencer2 import Sequencer +from mingus.containers import PercussionNote, Track +from mingus.containers.raw_snippet import RawSnippet + +import midi_percussion as mp + + +def test_snippets(): + recording = {} + channel = 1 + instrument_number = mp.percussion_instruments['Acoustic Snare'] + note = PercussionNote(name=None, number=instrument_number, velocity=64, channel=channel) + for i in range(4): + recording[i * 1000] = [ + { + 'func': 'start_note', + 'note': note, + 'channel': note.channel, + 'velocity': note.velocity + } + ] + + snippet = RawSnippet(recording) + track = Track(instrument=mp.MidiPercussion(), snippets=[snippet]) + sequencer = Sequencer() + sequencer.play_Track(track, channel=channel) + + assert len(sequencer.score) == 4 + assert sequencer.score[0][0]['func'] == 'start_note' + assert sequencer.score[0][0]['channel'] == channel + assert sequencer.score[0][0]['velocity'] == 64 + assert isinstance(sequencer.score[0][0]['note'], PercussionNote)