From 5a182045fc65c252448a303b0644ca47f501af50 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sat, 5 Feb 2022 08:07:45 -0500 Subject: [PATCH 01/11] Fixed bug that caused all tracks to play to channel 1. Thus all tracks were played with the instrument in track 1. Fixed bug that caused note outside of track one to never stop. All stop commands were being sent to channel 1. Added an example that was affected by these bugs and now plays correctly. --- doc/wiki/tutorialExtraLilypond.rst | 2 +- mingus/midi/sequencer.py | 20 +++++------- mingus_examples/multiple_instruments.py | 43 +++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 mingus_examples/multiple_instruments.py 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/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_examples/multiple_instruments.py b/mingus_examples/multiple_instruments.py new file mode 100644 index 00000000..1ae398dc --- /dev/null +++ b/mingus_examples/multiple_instruments.py @@ -0,0 +1,43 @@ +""" +This module demonstrates two tracks, each playing a different instrument. +""" + +import os + +from mingus.containers import Bar, Track +from mingus.containers import MidiInstrument +from mingus.midi import fluidsynth + +sound_font = os.getenv('MINGUS_SOUNDFONT') +assert sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + +fluidsynth.init(sound_font) + +# Some whole notes +a_bar = Bar() +a_bar.place_notes('A-4', 1) + +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")) +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 + +fluidsynth.play_Tracks([t1, t2], [1, 2]) From 75a2fa28f2a78ecd617f384a5cc2d60cc6c16ec4 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sat, 19 Feb 2022 12:27:14 -0600 Subject: [PATCH 02/11] Added new sequencer that builds a score as a dict with keys as times in milliseconds and list of events as values. Added percussion class. Updated multiple_instruments.py demo to include percussion. Fixed Note to properly play velocity. --- mingus/containers/__init__.py | 2 +- mingus/containers/bar.py | 85 ++++-- mingus/containers/get_note_length.py | 73 +++++ mingus/containers/instrument.py | 264 ++---------------- mingus/containers/midi_percussion.py | 61 ++++ mingus/containers/note.py | 37 ++- mingus/containers/note_container.py | 7 +- mingus/containers/track.py | 9 +- mingus/extra/musicxml.py | 2 +- mingus/midi/fluid_synth2.py | 129 +++++++++ mingus/midi/midi_file_in.py | 2 +- mingus/midi/midi_file_out.py | 2 +- mingus/midi/midi_track.py | 2 +- mingus/midi/sequencer2.py | 76 +++++ mingus_examples/improviser/improviser.py | 8 +- mingus_examples/multiple_instruments.py | 26 +- mingus_examples/play_drums.py | 44 +++ .../play_progression/play-progression.py | 6 +- requirements.txt | 4 + tests/unit/containers/test_bar.py | 21 ++ 20 files changed, 570 insertions(+), 290 deletions(-) create mode 100644 mingus/containers/get_note_length.py create mode 100644 mingus/containers/midi_percussion.py create mode 100644 mingus/midi/fluid_synth2.py create mode 100644 mingus/midi/sequencer2.py create mode 100644 mingus_examples/play_drums.py create mode 100644 requirements.txt 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..ca0f7523 100644 --- a/mingus/containers/bar.py +++ b/mingus/containers/bar.py @@ -20,39 +20,39 @@ import six +from mingus.containers import PercussionNote, Note 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 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): # 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() 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 +72,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): @@ -98,6 +98,7 @@ 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: self.bar.append([self.current_beat, duration, notes]) self.current_beat += 1.0 / duration @@ -105,12 +106,6 @@ def place_notes(self, notes, duration): 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. @@ -223,6 +218,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..0d429202 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,30 @@ 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)) + self.clef = clef + self.tuning = tuning + self.bank = bank - 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 +66,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 +75,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,27 +90,19 @@ 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 @@ -121,10 +110,6 @@ def 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", @@ -256,201 +241,6 @@ class MidiInstrument(Instrument): "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) - - 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) - - def open_triangle(self): - return Note(81 - 12) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.number = self.names.index(self.name) diff --git a/mingus/containers/midi_percussion.py b/mingus/containers/midi_percussion.py new file mode 100644 index 00000000..cf000229 --- /dev/null +++ b/mingus/containers/midi_percussion.py @@ -0,0 +1,61 @@ +""" +MIDI percussion is treated as one instrument, with each type of percussion instrument being a "key" +(i.e. like a key on a piano. +""" + +midi_percussion = { + '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 +} + + +class MidiPercussion: + def __init__(self, bank=128): + self.bank = bank + self.number = 1 + self.name = 'Percussion' diff --git a/mingus/containers/note.py b/mingus/containers/note.py index 42b278b9..9bc9ccb1 100644 --- a/mingus/containers/note.py +++ b/mingus/containers/note.py @@ -19,6 +19,7 @@ # along with this program. If not, see . from mingus.core import notes, intervals +from mingus.containers.midi_percussion import midi_percussion as mp from mingus.containers.mt_exceptions import NoteFormatError from math import log import six @@ -45,13 +46,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: @@ -62,8 +57,9 @@ def __init__(self, name="C", octave=4, dynamics=None, velocity=None, channel=Non 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 @@ -350,3 +346,26 @@ def __ge__(self, other): def __repr__(self): """Return a helpful representation for printing Note classes.""" return "'%s-%d'" % (self.name, self.octave) + + +class PercussionNote(Note): + """Percusion notes do not have a name of the staff (e.g. C or F#)""" + + # noinspection PyMissingConstructor + def __init__(self, name, 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 + self.key_number = mp[name] + 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 diff --git a/mingus/containers/note_container.py b/mingus/containers/note_container.py index d51d3840..03075b62 100644 --- a/mingus/containers/note_container.py +++ b/mingus/containers/note_container.py @@ -18,7 +18,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.core import intervals, chords, progressions from mingus.containers.mt_exceptions import UnexpectedObjectError import six @@ -65,9 +65,10 @@ 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"): + + if not (hasattr(note, "name") or isinstance(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) diff --git a/mingus/containers/track.py b/mingus/containers/track.py index 22869bc3..cd56ad2b 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -37,14 +37,9 @@ class Track(object): 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): + def __init__(self, instrument, bpm=120.0): self.bars = [] + self.bpm = bpm self.instrument = instrument def add_bar(self, bar): 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..295acbae --- /dev/null +++ b/mingus/midi/fluid_synth2.py @@ -0,0 +1,129 @@ +# -*- 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 . + +"""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 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 stop_note(self, note, channel): + self.stop_event(int(note) + 12, int(channel)) + + def play_tracks(self, tracks, channels, bpm=120.0): + sequencer = Sequencer() + sequencer.play_Tracks(tracks, channels, bpm=bpm) + sequencer.play_score(self) + + 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/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/sequencer2.py b/mingus/midi/sequencer2.py new file mode 100644 index 00000000..8eef9988 --- /dev/null +++ b/mingus/midi/sequencer2.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +import logging + +import sortedcontainers + + +logging.basicConfig(level=logging.INFO) + + +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): + super().__init__() + # Keys will be in milliseconds since the start. Values will be lists of stuff to do. + self.score = {} + 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) + + # noinspection PyPep8Naming + def play_Tracks(self, tracks, channels, bpm=120.0): + """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 = track.bpm or 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): + 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 start_time, events in score.items(): + dt = start_time - the_time + if dt > 0: + synth.sleep(dt / 1000.0) + the_time = start_time + for event in events: + if event['func'] == 'start_note': + 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': + synth.stop_note(event['note'], event['channel']) + logging.info('Stop: {} Note: {note} Channel: {channel}'.format(the_time, **event)) + logging.info('--------------\n') 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 index 1ae398dc..ce922c47 100644 --- a/mingus_examples/multiple_instruments.py +++ b/mingus_examples/multiple_instruments.py @@ -1,17 +1,18 @@ """ -This module demonstrates two tracks, each playing a different instrument. +This module demonstrates two tracks, each playing a different instrument along with a percussion track. """ import os -from mingus.containers import Bar, Track +from mingus.containers import Bar, Track, Note, PercussionNote from mingus.containers import MidiInstrument -from mingus.midi import fluidsynth +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.containers.midi_percussion import MidiPercussion sound_font = os.getenv('MINGUS_SOUNDFONT') assert sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' -fluidsynth.init(sound_font) +fluidsynth = FluidSynthPlayer(sound_font, driver='coreaudio', gain=1.0) # Some whole notes a_bar = Bar() @@ -40,4 +41,19 @@ t2.add_bar(rest_bar) t2.add_bar(f_bar) # by itself -fluidsynth.play_Tracks([t1, t2], [1, 2]) +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]) diff --git a/mingus_examples/play_drums.py b/mingus_examples/play_drums.py new file mode 100644 index 00000000..258e6332 --- /dev/null +++ b/mingus_examples/play_drums.py @@ -0,0 +1,44 @@ +""" +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. +""" + +import os +from time import sleep + +from midi_percussion import midi_percussion as mp +import pyfluidsynth + +sound_font = os.getenv('MINGUS_SOUNDFONT') +assert sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + +synth = pyfluidsynth.Synth() +sfid = synth.sfload(sound_font) +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['Hand Clap'], velocity) +sleep(0.5) +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/requirements.txt b/requirements.txt new file mode 100644 index 00000000..0f2a5260 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +six~=1.16.0 +binarytree~=6.3.0 +setuptools==60.7.1 +sortedcontainers~=2.4.0 \ 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 From addf5bf51b590c37dd28c2dbedc676104243409a Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 27 Feb 2022 12:06:55 -0600 Subject: [PATCH 03/11] Minor enhancements. Added percussion_browser.py. --- .gitignore | 2 + doc/ccm_notes.txt | 38 ++++++++++ mingus/containers/bar.py | 26 +++---- mingus/containers/midi_percussion.py | 9 ++- mingus/containers/note.py | 4 +- mingus/containers/track.py | 11 ++- mingus/midi/fluid_synth2.py | 1 + mingus/midi/get_soundfont_path.py | 8 +++ mingus/midi/pyfluidsynth.py | 2 + mingus_examples/blues.py | 72 +++++++++++++++++++ mingus_examples/multiple_instruments.py | 21 +++--- mingus_examples/note_durations.py | 58 ++++++++++++++++ mingus_examples/percussion_browser.py | 92 +++++++++++++++++++++++++ mingus_examples/play_drums.py | 12 ++-- requirements.txt | 4 +- 15 files changed, 323 insertions(+), 37 deletions(-) create mode 100644 doc/ccm_notes.txt create mode 100644 mingus/midi/get_soundfont_path.py create mode 100644 mingus_examples/blues.py create mode 100644 mingus_examples/note_durations.py create mode 100644 mingus_examples/percussion_browser.py diff --git a/.gitignore b/.gitignore index 1e8aaa7e..ca1f3a02 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist/ *.egg-info/ /venv + +/.idea 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/mingus/containers/bar.py b/mingus/containers/bar.py index ca0f7523..8fb9772e 100644 --- a/mingus/containers/bar.py +++ b/mingus/containers/bar.py @@ -20,7 +20,7 @@ import six -from mingus.containers import PercussionNote, Note +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 @@ -85,8 +85,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 @@ -99,12 +98,12 @@ def place_notes(self, notes, duration): 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_rest(self, duration): """Place a rest of given duration on the current_beat. @@ -113,7 +112,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). @@ -152,14 +152,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.""" diff --git a/mingus/containers/midi_percussion.py b/mingus/containers/midi_percussion.py index cf000229..6751f46a 100644 --- a/mingus/containers/midi_percussion.py +++ b/mingus/containers/midi_percussion.py @@ -3,7 +3,7 @@ (i.e. like a key on a piano. """ -midi_percussion = { +percussion_instruments = { 'Acoustic Bass Drum': 35, 'Bass Drum 1': 36, 'Side Stick': 37, @@ -54,6 +54,13 @@ } +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 diff --git a/mingus/containers/note.py b/mingus/containers/note.py index 9bc9ccb1..a87962c7 100644 --- a/mingus/containers/note.py +++ b/mingus/containers/note.py @@ -19,7 +19,7 @@ # along with this program. If not, see . from mingus.core import notes, intervals -from mingus.containers.midi_percussion import midi_percussion as mp +import mingus.containers.midi_percussion as mp from mingus.containers.mt_exceptions import NoteFormatError from math import log import six @@ -358,7 +358,7 @@ def __init__(self, name, velocity=64, channel=None, duration=None): For example, a player might manual stop a triangle after 1 second. """ self.name = name - self.key_number = mp[name] + self.key_number = mp.percussion_instruments[name] assert 0 <= velocity < 128, 'Velocity must be between 0 and 127' self.velocity = velocity self.channel = channel diff --git a/mingus/containers/track.py b/mingus/containers/track.py index cd56ad2b..417e92d2 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import copy from mingus.containers.mt_exceptions import InstrumentRangeError, UnexpectedObjectError from mingus.containers.note_container import NoteContainer @@ -42,11 +43,17 @@ def __init__(self, instrument, bpm=120.0): self.bpm = bpm self.instrument = instrument - 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 repeat(self, n_repetitions): + """The terminology here might be confusing. If a section is played one 1, it has 0 repetitions.""" + if n_repetitions > 0: + self.bars = self.bars * (n_repetitions + 1) + def add_notes(self, note, duration=None): """Add a Note, note as string or NoteContainer to the last Bar. diff --git a/mingus/midi/fluid_synth2.py b/mingus/midi/fluid_synth2.py index 295acbae..7927749d 100644 --- a/mingus/midi/fluid_synth2.py +++ b/mingus/midi/fluid_synth2.py @@ -16,6 +16,7 @@ # 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 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/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_examples/blues.py b/mingus_examples/blues.py new file mode 100644 index 00000000..a4153a8a --- /dev/null +++ b/mingus_examples/blues.py @@ -0,0 +1,72 @@ +import copy + +from mingus.containers import Bar, Track, PercussionNote, Note +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) + + +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")) + + # 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) + + if n_times > 1: + bass_track.repeat(n_times - 1) + + return bass_track + + +def percussion(n_times): + track = Track(MidiPercussion()) + drum_bar = Bar() + note = PercussionNote('Ride Cymbal 1', velocity=127) + note2 = PercussionNote('Ride Cymbal 1', velocity=62) + 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 _ in range(12): + track.add_bar(drum_bar) + + if n_times > 1: + track.repeat(n_times - 1) + + return track + + +fluidsynth.play_tracks([bass(1), percussion(1)], [1, 2]) diff --git a/mingus_examples/multiple_instruments.py b/mingus_examples/multiple_instruments.py index ce922c47..aac6ca12 100644 --- a/mingus_examples/multiple_instruments.py +++ b/mingus_examples/multiple_instruments.py @@ -1,23 +1,23 @@ """ This module demonstrates two tracks, each playing a different instrument along with a percussion track. """ - -import os - -from mingus.containers import Bar, Track, Note, PercussionNote +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 -sound_font = os.getenv('MINGUS_SOUNDFONT') -assert sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' -fluidsynth = FluidSynthPlayer(sound_font, driver='coreaudio', gain=1.0) +soundfont_path = get_soundfont_path() -# Some whole notes +fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) + +# Some half notes a_bar = Bar() -a_bar.place_notes('A-4', 1) +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) @@ -27,7 +27,7 @@ rest_bar = Bar() rest_bar.place_rest(1) -t1 = Track(MidiInstrument("Rock Organ")) +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 @@ -57,3 +57,4 @@ 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..326b7be4 --- /dev/null +++ b/mingus_examples/percussion_browser.py @@ -0,0 +1,92 @@ +import os +from time import sleep + +import tkinter as tk +from tkinter import ttk + +import midi_percussion as mp +import pyfluidsynth + + +# noinspection PyUnusedLocal +class PlayPercussion: + def __init__(self, instrument_number=50, setup_synth=True): + """ + + :param instrument_number: + :type instrument_number: int + :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed + :type setup_synth: bool + """ + if setup_synth: + self.setup_synth() + else: + self.synth = None + self.sound_font = 'not loaded' + + self.window = tk.Tk() + self.window.title("Instrument Browser") + padding = { + 'padx': 10, + 'pady': 10 + } + + self.message = tk.StringVar(value=f'Font: {self.sound_font}') + + row = 0 + ttk.Label(self.window, textvariable=self.message, anchor='w', justify='left').\ + grid(row=row, column=0, columnspan=2, sticky='EW', **padding) + row += 1 + + self.instrument_number = tk.IntVar(value=instrument_number) + ttk.Label(self.window, text="Instrument").grid(row=row, column=0, sticky=tk.W, **padding) + spin_box = tk.Spinbox(self.window, from_=1, to=128, increment=1, textvariable=self.instrument_number, + command=self.spinner) + spin_box.bind("", self.spinner) + spin_box.grid(row=row, column=1, sticky=tk.W, **padding) + row += 1 + + ttk.Label(self.window, text="Name").grid(row=row, column=0, sticky=tk.W, **padding) + self.instrument_name = tk.StringVar(value=mp.percussion_index_to_name(instrument_number)) + ttk.Label(self.window, text="", textvariable=self.instrument_name).\ + grid(row=row, column=1, sticky=tk.W, **padding) + row += 1 + + tk.Button(self.window, text="Play", command=self.play).grid(row=row, column=0, **padding) + tk.Button(self.window, text="Quit", command=self.quit).grid(row=row, column=1, **padding) + + self.window.mainloop() + + def setup_synth(self): + self.sound_font = os.getenv('MINGUS_SOUNDFONT') + assert self.sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' + + self.synth = pyfluidsynth.Synth(gain=1.0) + self.sfid = self.synth.sfload(self.sound_font) + self.synth.start() + + def quit(self): + self.window.withdraw() + self.window.destroy() + + def play(self): + if self.synth is None: + 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 + for _ in range(3): + self.synth.noteon(channel, self.instrument_number.get(), velocity) + sleep(0.5) + + def spinner(self, *args): + number = self.instrument_number.get() + self.instrument_name.set(mp.percussion_index_to_name(number)) + self.window.after(250, self.play) + + +play = PlayPercussion() diff --git a/mingus_examples/play_drums.py b/mingus_examples/play_drums.py index 258e6332..e8d5b3cb 100644 --- a/mingus_examples/play_drums.py +++ b/mingus_examples/play_drums.py @@ -4,18 +4,16 @@ This code was developed using the Musescore default sound font. We are not sure how it will work for other sound-fonts. """ - -import os from time import sleep -from midi_percussion import midi_percussion as mp +import midi_percussion as mp import pyfluidsynth +from mingus.midi.get_soundfont_path import get_soundfont_path -sound_font = os.getenv('MINGUS_SOUNDFONT') -assert sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' +soundfont_path = get_soundfont_path() synth = pyfluidsynth.Synth() -sfid = synth.sfload(sound_font) +sfid = synth.sfload(soundfont_path) synth.start() # Percussion -------------------------------------------- @@ -39,6 +37,6 @@ sleep(0.25) # Do a hand clap using the midi percussion dict to make it more readable. -synth.noteon(percussion_channel, mp['Hand Clap'], velocity) +synth.noteon(percussion_channel, mp.percussion_instruments['Hand Clap'], velocity) sleep(0.5) print('done') diff --git a/requirements.txt b/requirements.txt index 0f2a5260..144c8b53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ six~=1.16.0 -binarytree~=6.3.0 setuptools==60.7.1 -sortedcontainers~=2.4.0 \ No newline at end of file +sortedcontainers~=2.4.0 +numpy~=1.22.2 \ No newline at end of file From e21d62261d5c5d7f24f16df8b0fcbe058933178b Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 6 Mar 2022 10:15:21 -0600 Subject: [PATCH 04/11] Fixed bug that caused the wrong percussion instrument to play. --- mingus/midi/fluid_synth2.py | 6 ++++++ mingus/midi/sequencer2.py | 14 ++++++++++++-- mingus_examples/play_c_4.py | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 mingus_examples/play_c_4.py diff --git a/mingus/midi/fluid_synth2.py b/mingus/midi/fluid_synth2.py index 7927749d..8ef1b0e1 100644 --- a/mingus/midi/fluid_synth2.py +++ b/mingus/midi/fluid_synth2.py @@ -115,9 +115,15 @@ def pan(self, channel, 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): sequencer = Sequencer() sequencer.play_Tracks(tracks, channels, bpm=bpm) diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py index 8eef9988..58223ade 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -3,6 +3,8 @@ import sortedcontainers +from mingus.containers import PercussionNote + logging.basicConfig(level=logging.INFO) @@ -67,10 +69,18 @@ def play_score(self, synth): the_time = start_time for event in events: if event['func'] == 'start_note': - synth.play_note(event['note'], event['channel'], event['velocity']) + 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': - synth.stop_note(event['note'], event['channel']) + 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)) logging.info('--------------\n') 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]) From f39b5bc47d92df52dab218f75af6efc38c094ac2 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 10 Apr 2022 14:07:00 -0500 Subject: [PATCH 05/11] Created new percussion browser. More updates to keyboard_drumset.py. --- mingus/containers/midi_snippet.py | 67 +++++ mingus/containers/note.py | 14 +- mingus/containers/track.py | 10 +- mingus/midi/fluid_synth2.py | 4 + mingus/midi/sequencer2.py | 48 +++- mingus/tools/__init__.py | 0 mingus/tools/keyboard_drumset.py | 354 +++++++++++++++++++++++++ mingus/tools/load_midi_file.py | 28 ++ mingus/tools/percussion_composer.pkl | 1 + mingus/tools/percussion_composer.py | 156 +++++++++++ mingus_examples/blues.py | 38 ++- mingus_examples/percussion_browser.py | 92 ------- mingus_examples/percussion_browser2.py | 77 ++++++ requirements.txt | 3 +- 14 files changed, 783 insertions(+), 109 deletions(-) create mode 100644 mingus/containers/midi_snippet.py create mode 100644 mingus/tools/__init__.py create mode 100644 mingus/tools/keyboard_drumset.py create mode 100644 mingus/tools/load_midi_file.py create mode 100644 mingus/tools/percussion_composer.pkl create mode 100644 mingus/tools/percussion_composer.py delete mode 100644 mingus_examples/percussion_browser.py create mode 100644 mingus_examples/percussion_browser2.py diff --git a/mingus/containers/midi_snippet.py b/mingus/containers/midi_snippet.py new file mode 100644 index 00000000..52a15df3 --- /dev/null +++ b/mingus/containers/midi_snippet.py @@ -0,0 +1,67 @@ +from typing import Optional + +import mido + +from mingus.containers import PercussionNote + + +class MidiPercussionSnippet: + 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 put_into_score(self, channel: int, score: dict, 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 a87962c7..5702d622 100644 --- a/mingus/containers/note.py +++ b/mingus/containers/note.py @@ -66,7 +66,8 @@ def __init__(self, name="C", octave=4, dynamics=None, velocity=64, channel=None) 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) @@ -311,7 +312,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. @@ -352,13 +353,18 @@ class PercussionNote(Note): """Percusion notes do not have a name of the staff (e.g. C or F#)""" # noinspection PyMissingConstructor - def __init__(self, name, velocity=64, channel=None, duration=None): + 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 - self.key_number = mp.percussion_instruments[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 diff --git a/mingus/containers/track.py b/mingus/containers/track.py index 417e92d2..c26631cf 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -40,6 +40,7 @@ class Track(object): """ def __init__(self, instrument, bpm=120.0): self.bars = [] + self.snippets = [] self.bpm = bpm self.instrument = instrument @@ -49,10 +50,17 @@ def add_bar(self, bar, n_times=1): self.bars.append(bar) return self + def add_midi_snippet(self, snippet): + self.snippets.append(snippet) + def repeat(self, n_repetitions): - """The terminology here might be confusing. If a section is played one 1, it has 0 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. diff --git a/mingus/midi/fluid_synth2.py b/mingus/midi/fluid_synth2.py index 8ef1b0e1..f3230d33 100644 --- a/mingus/midi/fluid_synth2.py +++ b/mingus/midi/fluid_synth2.py @@ -73,6 +73,10 @@ def play_event(self, note, channel, 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: diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py index 58223ade..c2d27c2b 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -4,6 +4,7 @@ import sortedcontainers from mingus.containers import PercussionNote +from mingus.containers.midi_snippet import MidiPercussionSnippet logging.basicConfig(level=logging.INFO) @@ -19,10 +20,10 @@ class Sequencer: We use sortedcontainers containers to make that fast for the case where there are thousands of events. """ - def __init__(self): + 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 = {} + self.score = score or {} self.instruments = [] # noinspection PyPep8Naming @@ -33,8 +34,12 @@ def play_Track(self, track, channel=1, bpm=120.0): bpm = bar.bpm or bpm start_time += bar.play(start_time, bpm, channel, self.score) + for snippet in track.snippets: + if isinstance(snippet, MidiPercussionSnippet): + snippet.put_into_score(channel, self.score, bpm) + # noinspection PyPep8Naming - def play_Tracks(self, tracks, channels, bpm=120.0): + 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. @@ -44,7 +49,7 @@ def play_Tracks(self, tracks, channels, bpm=120.0): # Because of possible changes in bpm, render each track separately for track, channel in zip(tracks, channels): - bpm = track.bpm or bpm + bpm = bpm or track.bpm self.play_Track(track, channel, bpm=bpm) # noinspection PyPep8Naming @@ -84,3 +89,38 @@ def play_score(self, synth): logging.info('Stop: {} Note: {note} Channel: {channel}'.format(the_time, **event)) logging.info('--------------\n') + + def save_tracks(self, path, tracks, channels, bpm): + self.play_Tracks(tracks, channels, bpm=bpm) + score = sortedcontainers.SortedDict(self.score) + + print('x') + + # 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 start_time, events in score.items(): + # dt = start_time - the_time + # if dt > 0: + # synth.sleep(dt / 1000.0) + # the_time = start_time + # for event in events: + # if event['func'] == 'start_note': + # 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)) + # logging.info('--------------\n') 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..2a0bbdba --- /dev/null +++ b/mingus/tools/keyboard_drumset.py @@ -0,0 +1,354 @@ +from collections import defaultdict +import json +import time +from threading import Thread +from functools import partial + +import tkinter as tk +from tkinter import ttk +from tkinter.filedialog import asksaveasfile, askopenfile + +import midi_percussion as mp +from midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path +from mingus.containers import PercussionNote +from mingus.midi.sequencer2 import Sequencer + + +# A global variable for communicating with the click track thread +click_track_done = False + + +KEYS = ' zxcvbnm,./' + + +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 Play(Thread): + def __init__(self, synth, score): + super().__init__() + self.synth = synth + self.sequencer = Sequencer(score=score) + + def run(self): + self.sequencer.play_score(self.synth) + + +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 + + """ + def __init__(self, setup_synth=True, drum_set=None): + self.recording = {} + self.is_recording = False + self.start_recording_time = None + self.play_click_track = False + + if drum_set is None: + drum_set = { + 'instruments': { + 'z': 'Acoustic Bass Drum', + 'm': 'Acoustic Snare' + }, + 'click_track': { + 'bpm': 120, + 'beats_per_bar': 4, + 'enabled': True + } + } + self.instruments = 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=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=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=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) + + # 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() + + 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) + + # Draw all + 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) + + 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.instruments = json.load(file) + self.render_instrument_section() + + def save_drum_set(self): + files = [ + ('All Files', '*.*'), + ('Drum Sets', '*.json') + ] + file = asksaveasfile(filetypes=files, defaultextension=files) + if file: + json.dump(self.instruments, file) + + 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 + print('time ', start_key) + 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): + 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 + else: + self.is_recording = True + self.start_stop_recording_button['text'] = 'Stop' + + if self.play_click_track.get(): + self.start_click_track() + self.start_recording_time = time.time() + + def rewind(self): + pass + + def play(self): + player = Play(self.synth, self.recording) + player.start() + + def clear(self): + self.recording = [] + + def quit(self): + global click_track_done + + click_track_done = True + for r in self.recording: + print(r) + + 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 + } + json.dump(self.recording, 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/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_examples/blues.py b/mingus_examples/blues.py index a4153a8a..67ec026d 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -1,10 +1,14 @@ import copy +from pathlib import Path from mingus.containers import Bar, Track, PercussionNote, Note 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 + soundfont_path = get_soundfont_path() @@ -44,8 +48,7 @@ def bass(n_times): bass_track.add_bar(i_bar) bass_track.add_bar(turn_around) - if n_times > 1: - bass_track.repeat(n_times - 1) + bass_track.repeat(n_times - 1) return bass_track @@ -53,8 +56,8 @@ def bass(n_times): def percussion(n_times): track = Track(MidiPercussion()) drum_bar = Bar() - note = PercussionNote('Ride Cymbal 1', velocity=127) - note2 = PercussionNote('Ride Cymbal 1', velocity=62) + 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) @@ -63,10 +66,31 @@ def percussion(n_times): for _ in range(12): track.add_bar(drum_bar) - if n_times > 1: - track.repeat(n_times - 1) + # path = Path.home() / 'drum 1.mid' + # snippet = MidiPercussionSnippet(path, start=0.0, length_in_seconds=4.0, n_replications=6) + # track.add_midi_snippet(snippet) + + track.repeat(n_times - 1) return track -fluidsynth.play_tracks([bass(1), percussion(1)], [1, 2]) +def play(voices, n_times): + fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1)) + + +def save(voices, bpm=120): + n_times = 1 + channels = range(1, len(voices) + 1) + sequencer = Sequencer() + sequencer.save_tracks('my path', [voice(n_times) for voice in voices], channels, bpm=bpm) + + +if __name__ == '__main__': + # noinspection PyListCreation + voices = [] + voices.append(percussion) + voices.append(bass) + # play(voices, n_times=1) + save(voices) + diff --git a/mingus_examples/percussion_browser.py b/mingus_examples/percussion_browser.py deleted file mode 100644 index 326b7be4..00000000 --- a/mingus_examples/percussion_browser.py +++ /dev/null @@ -1,92 +0,0 @@ -import os -from time import sleep - -import tkinter as tk -from tkinter import ttk - -import midi_percussion as mp -import pyfluidsynth - - -# noinspection PyUnusedLocal -class PlayPercussion: - def __init__(self, instrument_number=50, setup_synth=True): - """ - - :param instrument_number: - :type instrument_number: int - :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed - :type setup_synth: bool - """ - if setup_synth: - self.setup_synth() - else: - self.synth = None - self.sound_font = 'not loaded' - - self.window = tk.Tk() - self.window.title("Instrument Browser") - padding = { - 'padx': 10, - 'pady': 10 - } - - self.message = tk.StringVar(value=f'Font: {self.sound_font}') - - row = 0 - ttk.Label(self.window, textvariable=self.message, anchor='w', justify='left').\ - grid(row=row, column=0, columnspan=2, sticky='EW', **padding) - row += 1 - - self.instrument_number = tk.IntVar(value=instrument_number) - ttk.Label(self.window, text="Instrument").grid(row=row, column=0, sticky=tk.W, **padding) - spin_box = tk.Spinbox(self.window, from_=1, to=128, increment=1, textvariable=self.instrument_number, - command=self.spinner) - spin_box.bind("", self.spinner) - spin_box.grid(row=row, column=1, sticky=tk.W, **padding) - row += 1 - - ttk.Label(self.window, text="Name").grid(row=row, column=0, sticky=tk.W, **padding) - self.instrument_name = tk.StringVar(value=mp.percussion_index_to_name(instrument_number)) - ttk.Label(self.window, text="", textvariable=self.instrument_name).\ - grid(row=row, column=1, sticky=tk.W, **padding) - row += 1 - - tk.Button(self.window, text="Play", command=self.play).grid(row=row, column=0, **padding) - tk.Button(self.window, text="Quit", command=self.quit).grid(row=row, column=1, **padding) - - self.window.mainloop() - - def setup_synth(self): - self.sound_font = os.getenv('MINGUS_SOUNDFONT') - assert self.sound_font, 'Please put the path to a soundfont file in the environment variable: MINGUS_SOUNDFONT' - - self.synth = pyfluidsynth.Synth(gain=1.0) - self.sfid = self.synth.sfload(self.sound_font) - self.synth.start() - - def quit(self): - self.window.withdraw() - self.window.destroy() - - def play(self): - if self.synth is None: - 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 - for _ in range(3): - self.synth.noteon(channel, self.instrument_number.get(), velocity) - sleep(0.5) - - def spinner(self, *args): - number = self.instrument_number.get() - self.instrument_name.set(mp.percussion_index_to_name(number)) - self.window.after(250, self.play) - - -play = PlayPercussion() diff --git a/mingus_examples/percussion_browser2.py b/mingus_examples/percussion_browser2.py new file mode 100644 index 00000000..2560481a --- /dev/null +++ b/mingus_examples/percussion_browser2.py @@ -0,0 +1,77 @@ +from functools import partial +from time import sleep + +import tkinter as tk + +import midi_percussion as mp +import pyfluidsynth +from mingus.midi.get_soundfont_path import get_soundfont_path + + +# noinspection PyUnusedLocal +class PlayPercussion: + def __init__(self, instrument_number=50, setup_synth=True): + """ + + :param instrument_number: + :type instrument_number: int + :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed + :type setup_synth: bool + """ + if setup_synth: + self.setup_synth() + else: + self.synth = None + self.sound_font = 'not loaded' + + self.window = tk.Tk() + self.window.title("Percusion Browser") + padding = { + 'padx': 10, + 'pady': 10 + } + + instrument_number = 1 + row = 0 + for row in range(16): + for column in range(8): + name = mp.percussion_index_to_name(instrument_number) + tk.Button( + self.window, + text=f"{instrument_number} - {name}", + command=partial(self.play, instrument_number) + ).grid(row=row, column=column, sticky=tk.NSEW) + + instrument_number += 1 + + row += 1 + tk.Button(self.window, text="Quit", command=self.quit).grid(row=row, column=0, **padding) + + 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() + + def quit(self): + self.window.withdraw() + self.window.destroy() + + def play(self, instrument_number): + if self.synth is None: + 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 + for _ in range(3): + self.synth.noteon(channel, instrument_number, velocity) + sleep(0.5) + + +play = PlayPercussion(setup_synth=False) diff --git a/requirements.txt b/requirements.txt index 144c8b53..37ba1c7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ six~=1.16.0 setuptools==60.7.1 sortedcontainers~=2.4.0 -numpy~=1.22.2 \ No newline at end of file +numpy~=1.22.2 +mido~=1.2.10 \ No newline at end of file From 1459405c345bf7dd7daaeb0bf9c148b0d1a5ae0f Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 17 Apr 2022 08:15:19 -0500 Subject: [PATCH 06/11] Finished new, grid based, percussion browser. --- mingus/containers/bar.py | 2 +- mingus_examples/percussion_browser.py | 96 ++++++++++++++++++++++++++ mingus_examples/percussion_browser2.py | 77 --------------------- 3 files changed, 97 insertions(+), 78 deletions(-) create mode 100644 mingus_examples/percussion_browser.py delete mode 100644 mingus_examples/percussion_browser2.py diff --git a/mingus/containers/bar.py b/mingus/containers/bar.py index 8fb9772e..bde1f07d 100644 --- a/mingus/containers/bar.py +++ b/mingus/containers/bar.py @@ -27,7 +27,7 @@ from mingus.core import progressions, keys from typing import Optional -from get_note_length import get_note_length, get_beat_start, get_bar_length +from mingus.containers.get_note_length import get_note_length, get_beat_start, get_bar_length class Bar(object): 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/percussion_browser2.py b/mingus_examples/percussion_browser2.py deleted file mode 100644 index 2560481a..00000000 --- a/mingus_examples/percussion_browser2.py +++ /dev/null @@ -1,77 +0,0 @@ -from functools import partial -from time import sleep - -import tkinter as tk - -import midi_percussion as mp -import pyfluidsynth -from mingus.midi.get_soundfont_path import get_soundfont_path - - -# noinspection PyUnusedLocal -class PlayPercussion: - def __init__(self, instrument_number=50, setup_synth=True): - """ - - :param instrument_number: - :type instrument_number: int - :param setup_synth: synth setup can take several seconds. Set this to False to delay setup until it is needed - :type setup_synth: bool - """ - if setup_synth: - self.setup_synth() - else: - self.synth = None - self.sound_font = 'not loaded' - - self.window = tk.Tk() - self.window.title("Percusion Browser") - padding = { - 'padx': 10, - 'pady': 10 - } - - instrument_number = 1 - row = 0 - for row in range(16): - for column in range(8): - name = mp.percussion_index_to_name(instrument_number) - tk.Button( - self.window, - text=f"{instrument_number} - {name}", - command=partial(self.play, instrument_number) - ).grid(row=row, column=column, sticky=tk.NSEW) - - instrument_number += 1 - - row += 1 - tk.Button(self.window, text="Quit", command=self.quit).grid(row=row, column=0, **padding) - - 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() - - def quit(self): - self.window.withdraw() - self.window.destroy() - - def play(self, instrument_number): - if self.synth is None: - 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 - for _ in range(3): - self.synth.noteon(channel, instrument_number, velocity) - sleep(0.5) - - -play = PlayPercussion(setup_synth=False) From 7be9ff0257aaca7acdf4501706752f3d42f7ea63 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sat, 23 Apr 2022 09:56:50 -0500 Subject: [PATCH 07/11] Added code for writing and reading the sequencer data to json. --- .gitignore | 1 + mingus/containers/instrument.py | 13 +++++ mingus/containers/midi_percussion.py | 7 +++ mingus/containers/note.py | 26 ++++++++++ mingus/midi/sequencer2.py | 46 ++++++----------- mingus/tools/keyboard_drumset.py | 41 +++++++++++---- mingus/tools/mingus_json.py | 75 ++++++++++++++++++++++++++++ mingus_examples/blues.py | 16 ++++-- tests/unit/containers/test_json.py | 13 +++++ 9 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 mingus/tools/mingus_json.py create mode 100644 tests/unit/containers/test_json.py diff --git a/.gitignore b/.gitignore index ca1f3a02..9b26fb10 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist/ /venv /.idea +!/mingus_examples/saved_blues.json diff --git a/mingus/containers/instrument.py b/mingus/containers/instrument.py index 0d429202..8955718a 100644 --- a/mingus/containers/instrument.py +++ b/mingus/containers/instrument.py @@ -37,10 +37,23 @@ def __init__(self, name, note_range=None, clef="bass and treble", tuning=None, b 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 + def set_range(self, note_range): """Set the note_rNGE of the instrument. diff --git a/mingus/containers/midi_percussion.py b/mingus/containers/midi_percussion.py index 6751f46a..7b1ad520 100644 --- a/mingus/containers/midi_percussion.py +++ b/mingus/containers/midi_percussion.py @@ -66,3 +66,10 @@ 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/note.py b/mingus/containers/note.py index 5702d622..7687e5c1 100644 --- a/mingus/containers/note.py +++ b/mingus/containers/note.py @@ -18,6 +18,8 @@ # 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 @@ -54,6 +56,9 @@ def __init__(self, name="C", octave=4, dynamics=None, velocity=64, channel=None) :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 = {} @@ -348,6 +353,16 @@ 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#)""" @@ -375,3 +390,14 @@ def __int__(self): 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/midi/sequencer2.py b/mingus/midi/sequencer2.py index c2d27c2b..29c159a2 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- +import json import logging import sortedcontainers from mingus.containers import PercussionNote from mingus.containers.midi_snippet import MidiPercussionSnippet +import mingus.tools.mingus_json as mingus_json logging.basicConfig(level=logging.INFO) @@ -94,33 +96,17 @@ def save_tracks(self, path, tracks, channels, bpm): self.play_Tracks(tracks, channels, bpm=bpm) score = sortedcontainers.SortedDict(self.score) - print('x') - - # 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 start_time, events in score.items(): - # dt = start_time - the_time - # if dt > 0: - # synth.sleep(dt / 1000.0) - # the_time = start_time - # for event in events: - # if event['func'] == 'start_note': - # 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)) - # logging.info('--------------\n') + 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/keyboard_drumset.py b/mingus/tools/keyboard_drumset.py index 2a0bbdba..95596ce5 100644 --- a/mingus/tools/keyboard_drumset.py +++ b/mingus/tools/keyboard_drumset.py @@ -73,6 +73,23 @@ class KeyboardDrumSet: 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 = {} @@ -81,7 +98,7 @@ def __init__(self, setup_synth=True, drum_set=None): self.play_click_track = False if drum_set is None: - drum_set = { + self.drum_set = { 'instruments': { 'z': 'Acoustic Bass Drum', 'm': 'Acoustic Snare' @@ -92,7 +109,10 @@ def __init__(self, setup_synth=True, drum_set=None): 'enabled': True } } - self.instruments = drum_set['instruments'] + else: + self.drum_set = drum_set + + self.instruments = self.drum_set['instruments'] if setup_synth: soundfont_path = get_soundfont_path() @@ -124,25 +144,24 @@ def __init__(self, setup_synth=True, drum_set=None): 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=drum_set['click_track']['bpm']) + 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=drum_set['click_track']['beats_per_bar']) + 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=drum_set['click_track']['enabled']) + 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) @@ -176,7 +195,10 @@ def render_instrument_section(self): 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}') @@ -203,6 +225,7 @@ def render_instrument_section(self): 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() def add_key_and_instrument(self, ev): if self.new_key.get() and self.new_instrument.get(): @@ -241,7 +264,7 @@ def make_instrument_popup(self): def load_drum_set(self): file = askopenfile() if file: - self.instruments = json.load(file) + self.drum_set = json.load(file) self.render_instrument_section() def save_drum_set(self): @@ -249,9 +272,9 @@ def save_drum_set(self): ('All Files', '*.*'), ('Drum Sets', '*.json') ] - file = asksaveasfile(filetypes=files, defaultextension=files) + file = asksaveasfile(filetypes=files, defaultextension=".json") if file: - json.dump(self.instruments, file) + json.dump(self.drum_set, file) def play_note(self, event): instrument_name = self.instruments[event.char] diff --git a/mingus/tools/mingus_json.py b/mingus/tools/mingus_json.py new file mode 100644 index 00000000..88686746 --- /dev/null +++ b/mingus/tools/mingus_json.py @@ -0,0 +1,75 @@ +import json + +# noinspection PyUnresolvedReferences +from mingus.containers import PercussionNote, Note + +# noinspection PyUnresolvedReferences +from mingus.containers import MidiInstrument + +# noinspection PyUnresolvedReferences +from mingus.containers.midi_percussion import MidiPercussion + + +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) + + def object_hook(self, obj): + + # 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) diff --git a/mingus_examples/blues.py b/mingus_examples/blues.py index 67ec026d..9f9894c3 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -79,11 +79,18 @@ def play(voices, n_times): fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1)) -def save(voices, bpm=120): +def save(path, voices, bpm=120): n_times = 1 channels = range(1, len(voices) + 1) sequencer = Sequencer() - sequencer.save_tracks('my path', [voice(n_times) for voice in voices], channels, bpm=bpm) + sequencer.save_tracks('saved_blues.json', [voice(n_times) for voice in voices], channels, bpm=bpm) + + +def load(path): + sequencer = Sequencer() + sequencer.load_tracks(path) + sequencer.play_score(fluidsynth) + print('x') if __name__ == '__main__': @@ -92,5 +99,6 @@ def save(voices, bpm=120): voices.append(percussion) voices.append(bass) # play(voices, n_times=1) - save(voices) - + path = 'saved_blues.json' + # save(path, voices) + load(path) diff --git a/tests/unit/containers/test_json.py b/tests/unit/containers/test_json.py new file mode 100644 index 00000000..410ba745 --- /dev/null +++ b/tests/unit/containers/test_json.py @@ -0,0 +1,13 @@ +from mingus.containers.note import Note, PercussionNote +from mingus.tools import mingus_json + + +def test_json(): + 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 From ce83b9bbf876831f64196f60d58aef07fcbb68ca Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 24 Apr 2022 13:12:09 -0500 Subject: [PATCH 08/11] More code for json. --- mingus/containers/bar.py | 19 +- mingus/containers/midi_snippet.py | 6 + mingus/containers/note_container.py | 21 +- mingus/containers/track.py | 27 +- mingus/core/keys.py | 8 +- mingus/tools/keyboard_drumset.py | 115 +- mingus/tools/mingus_json.py | 19 +- mingus_examples/blues.py | 31 +- mingus_examples/saved_blues.json | 1842 +++++++++++++++++++++++++++ tests/unit/containers/test_json.py | 46 +- 10 files changed, 2085 insertions(+), 49 deletions(-) create mode 100644 mingus_examples/saved_blues.json diff --git a/mingus/containers/bar.py b/mingus/containers/bar.py index bde1f07d..9d981fe3 100644 --- a/mingus/containers/bar.py +++ b/mingus/containers/bar.py @@ -40,14 +40,29 @@ class Bar(object): Bars can be stored together with Instruments in Tracks. """ - def __init__(self, key="C", meter=(4, 4), bpm=120): + 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.""" diff --git a/mingus/containers/midi_snippet.py b/mingus/containers/midi_snippet.py index 52a15df3..03dbda29 100644 --- a/mingus/containers/midi_snippet.py +++ b/mingus/containers/midi_snippet.py @@ -22,6 +22,12 @@ def __init__(self, midi_file_path, start: float = 0.0, length_in_seconds: Option 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): + params = ("midi_file_path", "start", "length_in_seconds", "n_replications") + d = {param: getattr(self, param) for param in params} + d['class_name'] = self.__class__.__name__ + return d + def put_into_score(self, channel: int, score: dict, bpm: Optional[float] = None): """ See: https://majicdesigns.github.io/MD_MIDIFile/page_timing.html diff --git a/mingus/containers/note_container.py b/mingus/containers/note_container.py index 03075b62..ddeb8b14 100644 --- a/mingus/containers/note_container.py +++ b/mingus/containers/note_container.py @@ -21,10 +21,11 @@ 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,11 +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") or isinstance(note, )): + elif not isinstance(note, Note): raise UnexpectedObjectError( 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/track.py b/mingus/containers/track.py index c26631cf..029ac36e 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -17,17 +17,17 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import copy - +from typing import Optional 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 mingus.tools.mingus_json as mingus_json import six from six.moves import range -class Track(object): +class Track(mingus_json.JsonMixin): """A track object. @@ -38,11 +38,12 @@ class Track(object): Tracks can be stored together in Compositions. """ - def __init__(self, instrument, bpm=120.0): - self.bars = [] - self.snippets = [] + def __init__(self, instrument, bpm=120.0, name=None, bars: Optional[list] = None, snippets: Optional[list] = None): + self.bars = bars or [] + self.snippets = snippets or [] self.bpm = bpm self.instrument = instrument + self.name = name def add_bar(self, bar, n_times=1): """Add a Bar to the current track.""" @@ -51,6 +52,7 @@ def add_bar(self, bar, n_times=1): return self def add_midi_snippet(self, snippet): + """Add a MidiPercussionSnippet""" self.snippets.append(snippet) def repeat(self, n_repetitions): @@ -74,12 +76,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 @@ -228,3 +230,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/tools/keyboard_drumset.py b/mingus/tools/keyboard_drumset.py index 95596ce5..1c219534 100644 --- a/mingus/tools/keyboard_drumset.py +++ b/mingus/tools/keyboard_drumset.py @@ -1,8 +1,9 @@ -from collections import defaultdict import json import time from threading import Thread from functools import partial +from pathlib import Path +from typing import Optional import tkinter as tk from tkinter import ttk @@ -13,15 +14,45 @@ from mingus.midi.get_soundfont_path import get_soundfont_path from mingus.containers import PercussionNote from mingus.midi.sequencer2 import Sequencer +import mingus.tools.mingus_json as mingus_json # A global variable for communicating with the click track thread click_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 @@ -87,9 +118,6 @@ class KeyboardDrumSet: 'enabled': True } } - - - """ def __init__(self, setup_synth=True, drum_set=None): self.recording = {} @@ -107,7 +135,8 @@ def __init__(self, setup_synth=True, drum_set=None): 'bpm': 120, 'beats_per_bar': 4, 'enabled': True - } + }, + 'tracks': ['~/python_mingus/tracks/test_percussion.json'] } else: self.drum_set = drum_set @@ -164,7 +193,13 @@ def __init__(self, setup_synth=True, drum_set=None): 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) @@ -182,6 +217,7 @@ def __init__(self, setup_synth=True, drum_set=None): self.window.mainloop() + # Instruments ------------------------------------------------------------------------------------------------- def delete_instrument(self, char): del self.instruments[char] self.render_instrument_section() @@ -227,6 +263,7 @@ def render_instrument_section(self): 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 @@ -275,7 +312,60 @@ def save_drum_set(self): 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, track.track.name) + for_sequencer['tracks'].append(track.track) + for_sequencer['channels'].append(i) + + 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] @@ -333,8 +423,13 @@ def start_stop_recording(self): self.is_recording = True self.start_stop_recording_button['text'] = 'Stop' - if self.play_click_track.get(): - self.start_click_track() + # if self.play_click_track.get(): + # self.start_click_track() + + self.sequencer.play_score(self.synth) + player = Play(self.synth, self.sequencer.score) + player.start() + self.start_recording_time = time.time() def rewind(self): @@ -370,7 +465,7 @@ def save(self): 'beats_per_bar': self.beats_per_bar.get(), 'events': self.recording } - json.dump(self.recording, file) + mingus_json.dump(output, file) if __name__ == '__main__': diff --git a/mingus/tools/mingus_json.py b/mingus/tools/mingus_json.py index 88686746..41416bc2 100644 --- a/mingus/tools/mingus_json.py +++ b/mingus/tools/mingus_json.py @@ -1,14 +1,5 @@ import json -# noinspection PyUnresolvedReferences -from mingus.containers import PercussionNote, Note - -# noinspection PyUnresolvedReferences -from mingus.containers import MidiInstrument - -# noinspection PyUnresolvedReferences -from mingus.containers.midi_percussion import MidiPercussion - class MingusJSONEncoder(json.JSONEncoder): @@ -37,7 +28,11 @@ 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.midi_percussion import MidiPercussion + from mingus.core.keys import Key # handle your custom classes if isinstance(obj, dict): @@ -73,3 +68,9 @@ def loads(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_examples/blues.py b/mingus_examples/blues.py index 9f9894c3..513cdc6e 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -8,12 +8,11 @@ from mingus.containers.midi_percussion import MidiPercussion from mingus.midi.get_soundfont_path import get_soundfont_path from mingus.midi.sequencer2 import Sequencer +import mingus.tools.mingus_json as mingus_json soundfont_path = get_soundfont_path() -fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) - def bass(n_times): # Make the bars @@ -54,7 +53,7 @@ def bass(n_times): def percussion(n_times): - track = Track(MidiPercussion()) + track = Track(MidiPercussion(), name='Percussion') drum_bar = Bar() note = PercussionNote('Ride Cymbal 1', velocity=62) note2 = PercussionNote('Ride Cymbal 1', velocity=32) @@ -76,6 +75,7 @@ def percussion(n_times): def play(voices, n_times): + fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1)) @@ -89,16 +89,31 @@ def save(path, voices, bpm=120): def load(path): sequencer = Sequencer() sequencer.load_tracks(path) + + fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) sequencer.play_score(fluidsynth) print('x') if __name__ == '__main__': # noinspection PyListCreation - voices = [] - voices.append(percussion) - voices.append(bass) + # voices = [] + # voices.append(percussion) # percusion is a track + # voices.append(bass) # a track # play(voices, n_times=1) - path = 'saved_blues.json' + # path = 'saved_blues.json' # save(path, voices) - load(path) + # load(path) + + # Track manipulations + track = percussion(1) + track_path = Path.home() / 'python_mingus' / 'tracks' / 'test_percussion.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('x') 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/tests/unit/containers/test_json.py b/tests/unit/containers/test_json.py index 410ba745..e014887d 100644 --- a/tests/unit/containers/test_json.py +++ b/tests/unit/containers/test_json.py @@ -1,8 +1,18 @@ -from mingus.containers.note import Note, PercussionNote +from mingus.containers import Bar, Track, PercussionNote, Note, NoteContainer +from mingus.containers import MidiInstrument from mingus.tools import mingus_json -def test_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] @@ -11,3 +21,35 @@ def test_json(): 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 From 2b81f9540b68ade8595f0cfa30cdd4ee827c0790 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sat, 30 Apr 2022 11:30:47 -0500 Subject: [PATCH 09/11] More work on keyboard_drumset.py playback. --- mingus/containers/midi_snippet.py | 17 ++++--- mingus/containers/raw_snippet.py | 41 +++++++++++++++ mingus/containers/track.py | 25 +++++----- mingus/midi/sequencer2.py | 11 ++-- mingus/tools/keyboard_drumset.py | 83 +++++++++++++++++++++++-------- mingus_examples/blues.py | 17 ++++--- requirements.txt | 3 +- tests/unit/midi/__init__.py | 0 tests/unit/midi/test_sequncer2.py | 32 ++++++++++++ 9 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 mingus/containers/raw_snippet.py create mode 100644 tests/unit/midi/__init__.py create mode 100644 tests/unit/midi/test_sequncer2.py diff --git a/mingus/containers/midi_snippet.py b/mingus/containers/midi_snippet.py index 03dbda29..b4e33081 100644 --- a/mingus/containers/midi_snippet.py +++ b/mingus/containers/midi_snippet.py @@ -3,9 +3,14 @@ import mido from mingus.containers import PercussionNote +import mingus.tools.mingus_json as mingus_json -class MidiPercussionSnippet: +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): """ @@ -23,12 +28,12 @@ def __init__(self, midi_file_path, start: float = 0.0, length_in_seconds: Option f'If there are replications, then length_in_seconds cannot be None' def to_json(self): - params = ("midi_file_path", "start", "length_in_seconds", "n_replications") - d = {param: getattr(self, param) for param in params} - d['class_name'] = self.__class__.__name__ - return d + 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, channel: int, score: dict, bpm: Optional[float] = None): + 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 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 029ac36e..e67620c9 100644 --- a/mingus/containers/track.py +++ b/mingus/containers/track.py @@ -17,28 +17,27 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Optional +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 mingus.tools.mingus_json as mingus_json +from mingus.containers.midi_snippet import MidiPercussionSnippet +from mingus.containers.raw_snippet import RawSnippet + import six from six.moves import range +if TYPE_CHECKING: + from mingus.containers.instrument import MidiInstrument + from mingus.containers.midi_percussion import MidiPercussion -class Track(mingus_json.JsonMixin): - - """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. +class Track(mingus_json.JsonMixin): - Tracks can be stored together in Compositions. - """ - def __init__(self, instrument, bpm=120.0, name=None, bars: Optional[list] = None, snippets: Optional[list] = None): + 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.bpm = bpm @@ -51,8 +50,8 @@ def add_bar(self, bar, n_times=1): self.bars.append(bar) return self - def add_midi_snippet(self, snippet): - """Add a MidiPercussionSnippet""" + def add_snippet(self, snippet): + assert isinstance(snippet, MidiPercussionSnippet) or isinstance(snippet, RawSnippet), "Invalid snippet" self.snippets.append(snippet) def repeat(self, n_repetitions): diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py index 29c159a2..5622b6e0 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- -import json import logging import sortedcontainers from mingus.containers import PercussionNote -from mingus.containers.midi_snippet import MidiPercussionSnippet import mingus.tools.mingus_json as mingus_json @@ -37,8 +34,7 @@ def play_Track(self, track, channel=1, bpm=120.0): start_time += bar.play(start_time, bpm, channel, self.score) for snippet in track.snippets: - if isinstance(snippet, MidiPercussionSnippet): - snippet.put_into_score(channel, self.score, bpm) + snippet.put_into_score(self.score, channel, bpm) # noinspection PyPep8Naming def play_Tracks(self, tracks, channels, bpm=None): @@ -60,7 +56,7 @@ def play_Composition(self, composition, channels=None, bpm=120): channels = [x + 1 for x in range(len(composition.tracks))] return self.play_Tracks(composition.tracks, channels, bpm) - def play_score(self, synth): + def play_score(self, synth, stop_func=None): score = sortedcontainers.SortedDict(self.score) for channel, instrument in self.instruments: @@ -70,6 +66,9 @@ def play_score(self, synth): the_time = 0 for start_time, events in score.items(): + if stop_func and stop_func(): + break + dt = start_time - the_time if dt > 0: synth.sleep(dt / 1000.0) diff --git a/mingus/tools/keyboard_drumset.py b/mingus/tools/keyboard_drumset.py index 1c219534..72a8401e 100644 --- a/mingus/tools/keyboard_drumset.py +++ b/mingus/tools/keyboard_drumset.py @@ -1,24 +1,25 @@ import json import time -from threading import Thread +import tkinter as tk from functools import partial from pathlib import Path -from typing import Optional - -import tkinter as tk +from threading import Thread from tkinter import ttk -from tkinter.filedialog import asksaveasfile, askopenfile +from tkinter.filedialog import askopenfile, asksaveasfile +from typing import Optional import midi_percussion as mp -from midi.fluid_synth2 import FluidSynthPlayer + +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.containers import PercussionNote from mingus.midi.sequencer2 import Sequencer -import mingus.tools.mingus_json as mingus_json - # A global variable for communicating with the click track thread click_track_done = False +player_track_done = False # noinspection SpellCheckingInspection KEYS = ' zxcvbnm,./' @@ -80,15 +81,41 @@ def run(self): count = 0 -class Play(Thread): +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: """ @@ -120,7 +147,7 @@ class KeyboardDrumSet: } """ def __init__(self, setup_synth=True, drum_set=None): - self.recording = {} + 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 @@ -136,7 +163,10 @@ def __init__(self, setup_synth=True, drum_set=None): 'beats_per_bar': 4, 'enabled': True }, - 'tracks': ['~/python_mingus/tracks/test_percussion.json'] + 'tracks': [ + '~/python_mingus/tracks/test_percussion.json', + '~/python_mingus/tracks/test_bass.json' + ] } else: self.drum_set = drum_set @@ -326,9 +356,10 @@ def render_tracks_section(self): 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, track.track.name) + 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) @@ -375,7 +406,6 @@ def play_note(self, event): 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 - print('time ', start_key) note = PercussionNote(name=None, number=instrument_number, velocity=64, channel=self.percussion_channel) self.recording.setdefault(start_key, []).append( { @@ -412,6 +442,8 @@ def stop_click_track(self): 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' @@ -419,6 +451,7 @@ def start_stop_recording(self): 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' @@ -426,28 +459,38 @@ def start_stop_recording(self): # if self.play_click_track.get(): # self.start_click_track() - self.sequencer.play_score(self.synth) - player = Play(self.synth, self.sequencer.score) + # 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): - player = Play(self.synth, self.recording) + 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 = [] + self.recording = {} def quit(self): global click_track_done + global player_track_done click_track_done = True - for r in self.recording: - print(r) + player_track_done = True self.window.withdraw() self.window.destroy() diff --git a/mingus_examples/blues.py b/mingus_examples/blues.py index 513cdc6e..d859e2af 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -34,7 +34,7 @@ def bass(n_times): v_bar.transpose("5") # Make the track - bass_track = Track(MidiInstrument("Acoustic Bass")) + bass_track = Track(MidiInstrument("Acoustic Bass"), name='Bass') # Make section bass_track.add_bar(i_bar, n_times=4) @@ -106,14 +106,15 @@ def load(path): # load(path) # Track manipulations - track = percussion(1) - track_path = Path.home() / 'python_mingus' / 'tracks' / 'test_percussion.json' + # 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]) + # 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('x') diff --git a/requirements.txt b/requirements.txt index 37ba1c7d..a7c51c75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ six~=1.16.0 setuptools==60.7.1 sortedcontainers~=2.4.0 numpy~=1.22.2 -mido~=1.2.10 \ No newline at end of file +mido~=1.2.10 +isort==5.10.1 \ No newline at end of file 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) From 4ca47aa8d7f13de9cbe6b8e7286fa0068ce7bfab Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sat, 7 May 2022 12:36:18 -0500 Subject: [PATCH 10/11] Added chorusing to tracks. --- mingus/containers/instrument.py | 270 +++++++++++++++-------------- mingus/containers/track.py | 55 +++++- mingus/midi/sequencer2.py | 11 +- mingus/tools/mingus_json.py | 1 + mingus_examples/blues.py | 103 ++++++++--- mingus_examples/piano_roll.py | 117 +++++++++++++ mingus_examples/play_effects.py | 57 ++++++ tests/unit/containers/test_json.py | 14 ++ 8 files changed, 467 insertions(+), 161 deletions(-) create mode 100644 mingus_examples/piano_roll.py create mode 100644 mingus_examples/play_effects.py diff --git a/mingus/containers/instrument.py b/mingus/containers/instrument.py index 8955718a..69005d6d 100644 --- a/mingus/containers/instrument.py +++ b/mingus/containers/instrument.py @@ -122,138 +122,146 @@ def can_play_notes(self, notes): return Instrument.can_play_notes(self, notes) +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 + + class MidiInstrument(Instrument): - 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", - ] + names = instruments def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.number = self.names.index(self.name) + self.number = get_instrument_number(self.name) diff --git a/mingus/containers/track.py b/mingus/containers/track.py index e67620c9..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 @@ -17,6 +12,8 @@ # # 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 @@ -26,20 +23,56 @@ from mingus.containers.midi_snippet import MidiPercussionSnippet from mingus.containers.raw_snippet import RawSnippet -import six -from six.moves import range 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 @@ -53,6 +86,10 @@ def add_bar(self, bar, n_times=1): 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.""" @@ -191,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): diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py index 5622b6e0..18fe0e93 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -36,6 +36,9 @@ def play_Track(self, track, channel=1, bpm=120.0): 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.""" @@ -87,9 +90,15 @@ def play_score(self, synth, stop_func=None): 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) diff --git a/mingus/tools/mingus_json.py b/mingus/tools/mingus_json.py index 41416bc2..02b982ad 100644 --- a/mingus/tools/mingus_json.py +++ b/mingus/tools/mingus_json.py @@ -31,6 +31,7 @@ def __init__(self, *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 diff --git a/mingus_examples/blues.py b/mingus_examples/blues.py index d859e2af..2a0aa981 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -2,6 +2,7 @@ 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 @@ -14,6 +15,48 @@ 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-4'), 8) + i_bar.place_notes(Note('C-4'), 8) + 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-4'), 8.0 / 3.0) + + turn_around = i_bar + + 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(i_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() @@ -62,18 +105,37 @@ def percussion(n_times): drum_bar.place_notes([note2], 4) drum_bar.place_notes([note], 4) - for _ in range(12): - track.add_bar(drum_bar) - - # path = Path.home() / 'drum 1.mid' - # snippet = MidiPercussionSnippet(path, start=0.0, length_in_seconds=4.0, n_replications=6) - # track.add_midi_snippet(snippet) + 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): fluidsynth = FluidSynthPlayer(soundfont_path, gain=1.0) fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1)) @@ -83,7 +145,7 @@ def save(path, voices, bpm=120): n_times = 1 channels = range(1, len(voices) + 1) sequencer = Sequencer() - sequencer.save_tracks('saved_blues.json', [voice(n_times) for voice in voices], channels, bpm=bpm) + sequencer.save_tracks(path, [voice(n_times) for voice in voices], channels, bpm=bpm) def load(path): @@ -97,24 +159,25 @@ def load(path): if __name__ == '__main__': # noinspection PyListCreation - # voices = [] - # voices.append(percussion) # percusion is a track - # voices.append(bass) # a track - # play(voices, n_times=1) - # path = 'saved_blues.json' - # save(path, voices) - # load(path) + voices = [] + voices.append(percussion) # percusion is a track + # voices.append(snare) + voices.append(bass) # a track + voices.append(melody) + play(voices, n_times=1) + # 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) + # 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('x') + print('done') 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_effects.py b/mingus_examples/play_effects.py new file mode 100644 index 00000000..03eba09a --- /dev/null +++ b/mingus_examples/play_effects.py @@ -0,0 +1,57 @@ +from time import sleep + +from mingus.midi.fluid_synth2 import FluidSynthPlayer +from mingus.midi.get_soundfont_path import get_soundfont_path +from mingus.containers.instrument import get_instrument_number +from mingus.containers import Bar, Track +from mingus.containers import MidiInstrument + + +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 = 93 + 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(): + 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]) + + +play_with_chorus_2() + +print('done') diff --git a/tests/unit/containers/test_json.py b/tests/unit/containers/test_json.py index e014887d..130ad6ff 100644 --- a/tests/unit/containers/test_json.py +++ b/tests/unit/containers/test_json.py @@ -1,4 +1,5 @@ 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 @@ -53,3 +54,16 @@ def test_json_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 From 9e74b3f158a39ad69687e9172b3049fa8f91a3a0 Mon Sep 17 00:00:00 2001 From: Chuck Martin Date: Sun, 8 May 2022 11:53:22 -0500 Subject: [PATCH 11/11] Added code for starting and stopping scores at any times. Started creating player to keep the synth running between changes to code that generates tracks. This is helpful because it takes more than 5 seconds to initialize the synth on my Macbook Pro. --- mingus/midi/fluid_synth2.py | 4 +- mingus/midi/sequencer2.py | 56 ++++++++++++++++++--- mingus/tools/player.py | 87 +++++++++++++++++++++++++++++++++ mingus_examples/blues.py | 36 ++++++++++---- mingus_examples/play_effects.py | 25 +++++++--- 5 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 mingus/tools/player.py diff --git a/mingus/midi/fluid_synth2.py b/mingus/midi/fluid_synth2.py index f3230d33..242f1432 100644 --- a/mingus/midi/fluid_synth2.py +++ b/mingus/midi/fluid_synth2.py @@ -128,10 +128,10 @@ def stop_note(self, note, channel): def stop_percussion_note(self, note, channel): self.stop_event(int(note), int(channel)) - def play_tracks(self, tracks, channels, bpm=120.0): + 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) + 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.""" diff --git a/mingus/midi/sequencer2.py b/mingus/midi/sequencer2.py index 18fe0e93..680db1d4 100644 --- a/mingus/midi/sequencer2.py +++ b/mingus/midi/sequencer2.py @@ -9,6 +9,32 @@ 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 @@ -59,7 +85,19 @@ def play_Composition(self, composition, channels=None, bpm=120): 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): + 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: @@ -68,16 +106,21 @@ def play_score(self, synth, stop_func=None): logging.info('--------------\n') the_time = 0 - for start_time, events in score.items(): + for event_start_time, events in score.items(): if stop_func and stop_func(): break - dt = start_time - the_time - if dt > 0: + 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 = start_time + the_time = event_start_time + for event in events: - if event['func'] == 'start_note': + 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: @@ -85,6 +128,7 @@ def play_score(self, synth, stop_func=None): 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']) 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 index 2a0aa981..507b4dc0 100644 --- a/mingus_examples/blues.py +++ b/mingus_examples/blues.py @@ -8,7 +8,7 @@ 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 +from mingus.midi.sequencer2 import Sequencer, calculate_bar_start_time, calculate_bar_end_time import mingus.tools.mingus_json as mingus_json @@ -20,15 +20,18 @@ def melody(n_times): rest_bar.place_rest(1) i_bar = Bar() - i_bar.place_notes(Note('C-4'), 8) - i_bar.place_notes(Note('C-4'), 8) + 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-4'), 8.0 / 3.0) + i_bar_2.place_notes(Note('C-5'), 8.0 / 3.0) - turn_around = i_bar + 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") @@ -48,7 +51,7 @@ def melody(n_times): track.add_bar(v_bar) track.add_bar(iv_bar) - track.add_bar(i_bar) + track.add_bar(rest_bar) track.add_bar(turn_around) event = ControlChangeEvent(beat=0, control=MidiControl.CHORUS, value=80) @@ -136,9 +139,9 @@ def snare(n_times): return snare_track -def play(voices, n_times): +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)) + fluidsynth.play_tracks([voice(n_times) for voice in voices], range(1, len(voices) + 1), **kwargs) def save(path, voices, bpm=120): @@ -157,6 +160,18 @@ def load(path): 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 = [] @@ -164,7 +179,10 @@ def load(path): # voices.append(snare) voices.append(bass) # a track voices.append(melody) - play(voices, n_times=1) + 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) diff --git a/mingus_examples/play_effects.py b/mingus_examples/play_effects.py index 03eba09a..f130bf99 100644 --- a/mingus_examples/play_effects.py +++ b/mingus_examples/play_effects.py @@ -1,10 +1,10 @@ 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 -from mingus.containers.instrument import get_instrument_number -from mingus.containers import Bar, Track -from mingus.containers import MidiInstrument def play_w_chorus(): @@ -24,7 +24,7 @@ def play_w_chorus(): def play_note(chorus_level): if chorus_level: - chorus = 93 + chorus = MidiControl.CHORUS synth.control_change(channel=channel, control=chorus, value=chorus_level) synth.play_note(note=60, channel=channel, velocity=velocity) @@ -38,18 +38,27 @@ def play_note(chorus_level): 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', 1) + 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) - t1 = Track(MidiInstrument("Acoustic Grand Piano", )) - t1.add_bar(c_bar) + event = ControlChangeEvent(beat=8, control=MidiControl.CHORUS, value=127) + track.add_event(event) - fluidsynth.play_tracks([t1], [1]) + fluidsynth.play_tracks([track], [1]) play_with_chorus_2()