diff --git a/NeoTrellis_M4_MIDI_Synth/events.py b/NeoTrellis_M4_MIDI_Synth/events.py new file mode 100644 index 00000000..550cddf1 --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/events.py @@ -0,0 +1,409 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +# Events as defined in http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html +# pylint: disable=unused-argument,no-self-use + + +class Event(object): + + def __init__(self, delta_time): + self._delta_time = delta_time + + @property + def time(self): + return self._delta_time + + def execute(self, sequencer): + return False + + +class F0SysexEvent(Event): + def __init__(self, delta_time, data): + Event.__init__(self, delta_time) + self._data = data + + +class F7SysexEvent(Event): + + def __init__(self, delta_time, data): + Event.__init__(self, delta_time) + self._data = data + +class MetaEvent(Event): + + def __init__(self, delta_time): + Event.__init__(self, delta_time) + + +class SequenceNumberMetaEvent(MetaEvent): + + def __init__(self, delta_time, sequence_number): + MetaEvent.__init__(self, delta_time) + self._sequence_number = sequence_number + + def __str__(self): + return '%d : Sequence Number : %d' % (self._delta_time, self._sequence_number) + + +class TextMetaEvent(MetaEvent): + def __init__(self, delta_time, text): + MetaEvent.__init__(self, delta_time) + self._text = text + + def __str__(self): + return '%d : Text : %s' % (self._delta_time, self._text) + + +class CopyrightMetaEvent(MetaEvent): + + def __init__(self, delta_time, copyright_notice): + MetaEvent.__init__(self, delta_time) + self._copyright_notice = copyright_notice + + def __str__(self): + return '%d : Copyright : %s' % (self._delta_time, self._copyright_notice) + + +class TrackNameMetaEvent(MetaEvent): + def __init__(self, delta_time, track_name): + MetaEvent.__init__(self, delta_time) + self._track_name = track_name + + def __str__(self): + return '%d : Track Name : %s' % (self._delta_time, self._track_name) + + +class InstrumentNameMetaEvent(MetaEvent): + + def __init__(self, delta_time, instrument_name): + MetaEvent.__init__(self, delta_time) + self._instrument_name = instrument_name + + def __str__(self): + return '%d : Instrument Name : %s' % (self._delta_time, self._instrument_name) + + +class LyricMetaEvent(MetaEvent): + + def __init__(self, delta_time, lyric): + MetaEvent.__init__(self, delta_time) + self._lyric = lyric + + def __str__(self): + return '%d : Lyric : %s' % (self._delta_time, self._lyric) + + +class MarkerMetaEvent(MetaEvent): + + def __init__(self, delta_time, marker): + MetaEvent.__init__(self, delta_time) + self._marker = marker + + def __str__(self): + return '%d : Marker : %s' % (self._delta_time, self._marker) + + +class CuePointMetaEvent(MetaEvent): + + def __init__(self, delta_time, cue): + MetaEvent.__init__(self, delta_time) + self._cue = cue + + def __str__(self): + return '%d : Cue : %s' % (self._delta_time, self._cue) + + +class ChannelPrefixMetaEvent(MetaEvent): + + def __init__(self, delta_time, channel): + MetaEvent.__init__(self, delta_time) + self._channel = channel + + def __str__(self): + return '%d: Channel Prefix : %d' % (self._delta_time, self._channel) + + +class EndOfTrackMetaEvent(MetaEvent): + + def __init__(self, delta_time): + MetaEvent.__init__(self, delta_time) + + def __str__(self): + return '%d: End Of Track' % (self._delta_time) + + def execute(self, sequencer): + sequencer.end_track() + return True + + +class SetTempoMetaEvent(MetaEvent): + + def __init__(self, delta_time, tempo): + MetaEvent.__init__(self, delta_time) + self._tempo = tempo + + def __str__(self): + return '%d: Set Tempo : %d' % (self._delta_time, self._tempo) + + def execute(self, sequencer): + sequencer.set_tempo(self._tempo) + return False + + +class SmpteOffsetMetaEvent(MetaEvent): + + def __init__(self, delta_time, hour, minute, second, fr, rr): + MetaEvent.__init__(self, delta_time) + self._hour = hour + self._minute = minute + self._second = second + self._fr = fr + self._rr = rr + + def __str__(self): + return '%d : SMPTE Offset : %02d:%02d:%02d %d %d' % (self._delta_time, + self._hour, + self._minute, + self._second, + self._fr, + self._rr) + + +class TimeSignatureMetaEvent(MetaEvent): + + def __init__(self, delta_time, nn, dd, cc, bb): + MetaEvent.__init__(self, delta_time) + self._numerator = nn + self._denominator = dd + self._cc = cc + self._bb = bb + + def __str__(self): + return '%d : Time Signature : %d %d %d %d' % (self._delta_time, + self._numerator, + self._denominator, + self._cc, + self._bb) + + def execute(self, sequencer): + sequencer.set_time_signature(self._numerator, self._denominator, self._cc) + return False + + +class KeySignatureMetaEvent(MetaEvent): + + def __init__(self, delta_time, sf, mi): + MetaEvent.__init__(self, delta_time) + self._sf = sf + self._mi = mi + + def __str__(self): + return '%d : Key Signature : %d %d' % (self._delta_time, self._sf, self._mi) + + +class SequencerSpecificMetaEvent(MetaEvent): + + def __init__(self, delta_time, data): + MetaEvent.__init__(self, delta_time) + self._data = data + + +class MidiEvent(Event): + + def __init__(self, delta_time, channel): + Event.__init__(self, delta_time) + self._channel = channel + + +class NoteOffEvent(MidiEvent): + + def __init__(self, delta_time, channel, key, velocity): + MidiEvent.__init__(self, delta_time, channel) + self._key = key + self._velocity = velocity + + def __str__(self): + return '%d : Note Off : key %d, velocity %d' % (self._delta_time, + self._key, + self._velocity) + + def execute(self, sequencer): + sequencer.note_off(self._key, self._velocity) + return False + + +class NoteOnEvent(MidiEvent): + + def __init__(self, delta_time, channel, key, velocity): + MidiEvent.__init__(self, delta_time, channel) + self._key = key + self._velocity = velocity + + def __str__(self): + return '%d : Note On : key %d, velocity %d' % (self._delta_time, + self._key, + self._velocity) + + def execute(self, sequencer): + sequencer.note_on(self._key, self._velocity) + return False + + +class PolyphonicKeyPressureEvent(MidiEvent): + + def __init__(self, delta_time, channel, key, pressure): + MidiEvent.__init__(self, delta_time, channel) + self._key = key + self._pressure = pressure + + def __str__(self): + return '%d : Poly Key Pressure : key %d, velocity %d' % (self._delta_time, + self._key, + self._pressure) + + +class ControlChangeEvent(MidiEvent): + + def __init__(self, delta_time, channel, controller, value): + MidiEvent.__init__(self, delta_time, channel) + self._controller = controller + self._value = value + + def __str__(self): + return '%d : Control Change : controller %d, value %d' % (self._delta_time, + self._controller, + self._value) + + + +class ProgramChangeEvent(MidiEvent): + + def __init__(self, delta_time, channel, program_number): + MidiEvent.__init__(self, delta_time, channel) + self._program_number = program_number + + def __str__(self): + return '%d : Program Change : program %d' % (self._delta_time, + self._program_number) + +class ChannelPressureEvent(MidiEvent): + + def __init__(self, delta_time, channel, pressure): + MidiEvent.__init__(self, delta_time, channel) + self._pressure = pressure + + def __str__(self): + return '%d : Channel Pressure : %d' % (self._delta_time, self._channel) + +class PitchWheelChangeEvent(MidiEvent): + + def __init__(self, delta_time, channel, value): + MidiEvent.__init__(self, delta_time, channel) + self._value = value + + def __str__(self): + return '%d : Pitch Wheel Change : %d' % (self._delta_time, self._value) + + +class SystemExclusiveEvent(MidiEvent): + + def __init__(self, delta_time, channel, data): + MidiEvent.__init__(self, delta_time, channel) + self._data = data + + +class SongPositionPointerEvent(MidiEvent): + + def __init__(self, delta_time, beats): + MidiEvent.__init__(self, delta_time, None) + self._beats = beats + + def __str__(self): + return '%d: SongPositionPointerEvent(beats %d)' % (self._delta_time, + self._beats) + + +class SongSelectEvent(MidiEvent): + + def __init__(self, delta_time, song): + MidiEvent.__init__(self, delta_time, None) + self._song = song + + def __str__(self): + return '%d: SongSelectEvent(song %d)' % (self._delta_time, + self._song) + + +class TuneRequestEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Tune Request' % (self._delta_time) + + +class TimingClockEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Timing Clock' % (self._delta_time) + + +class StartEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Start' % (self._delta_time) + + +class ContinueEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Continue' % (self._delta_time) + + +class StopEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Stop' % (self._delta_time) + + +class ActiveSensingEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Active Sensing' % (self._delta_time) + + +class ResetEvent(MidiEvent): + + def __init__(self, delta_time): + MidiEvent.__init__(self, delta_time, None) + + def __str__(self): + return '%d : Reset' % (self._delta_time) diff --git a/NeoTrellis_M4_MIDI_Synth/header.py b/NeoTrellis_M4_MIDI_Synth/header.py new file mode 100644 index 00000000..f667ed5a --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/header.py @@ -0,0 +1,43 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +class MidiHeader(object): + + def __init__(self, + midi_format, + number_of_tracks, + ticks_per_frame, + negative_SMPTE_format, + ticks_per_quarternote): + self._format = midi_format + self._number_of_tracks = number_of_tracks + self._ticks_per_frame = ticks_per_frame + self._negative_SMPTE_format = negative_SMPTE_format + self._ticks_per_quarternote = ticks_per_quarternote + + @property + def number_of_tracks(self): + return self._number_of_tracks + + def __str__(self): + format_string = ('Header - format: {0}, ' + 'track count: {1}, ' + 'ticks per frame: {2}, ' + 'SMPTE: {3}, ' + 'ticks per quarternote: {4}') + return format_string.format(self._format, + self._number_of_tracks, + self._ticks_per_frame, + self._negative_SMPTE_format, + self._ticks_per_quarternote) diff --git a/NeoTrellis_M4_MIDI_Synth/main.py b/NeoTrellis_M4_MIDI_Synth/main.py new file mode 100644 index 00000000..d1cb2876 --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/main.py @@ -0,0 +1,65 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +import os +import parser +import sequencer +import synth +import adafruit_trellism4 + +trellis = adafruit_trellism4.TrellisM4Express(rotation=0) +trellis.pixels.brightness = 0.1 +trellis.pixels.fill(0) + +syn = synth.Synth() +seq = sequencer.Sequencer(syn) +p = parser.MidiParser() + +voices = sorted([f.split('.')[0] for f in os.listdir('/samples') if f.endswith('.txt')]) +tunes = sorted([f for f in os.listdir('/midi') if f.endswith('.mid')]) + +selected_voice = None + +def reset_voice_buttons(): + for i in range(len(voices)): + trellis.pixels[(i, 0)] = 0x0000FF + +def reset_tune_buttons(): + for i in range(len(tunes)): + trellis.pixels[(i % 8, (i // 8) + 1)] = 0x00FF00 + +current_press = set() +reset_voice_buttons() +reset_tune_buttons() + +while True: + pressed = set(trellis.pressed_keys) + just_pressed = pressed - current_press + for down in just_pressed: + if down[1] == 0: + if down[0] < len(voices): # a voice selection + selected_voice = down[0] + reset_voice_buttons() + trellis.pixels[down] = 0xFFFFFF + syn.voice = voices[selected_voice] + else: + tune_index = (down[1] - 1) * 8 + down[0] + if tune_index < len(tunes) and selected_voice is not None: + trellis.pixels[down] = 0xFFFFFF + header, tracks = p.parse('/midi/' + tunes[tune_index]) + for track in tracks: + seq.play(track) + reset_tune_buttons() + + current_press = pressed diff --git a/NeoTrellis_M4_MIDI_Synth/parser.py b/NeoTrellis_M4_MIDI_Synth/parser.py new file mode 100644 index 00000000..26d2298e --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/parser.py @@ -0,0 +1,257 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +# pylint: disable=no-self-use,too-many-return-statements,too-many-branches,inconsistent-return-statements + +import header +import events + +def log(txt): + print(txt) + #pass + +class MidiParser(object): + + def __init__(self): + pass + + def _as_8(self, d): + return d[0] + + def _as_16(self, d): + return (d[0] << 8) | d[1] + + def _as_24(self, d): + return (d[0] << 16) | (d[1] << 8) | d[2] + + def _as_32(self, d): + return (d[0] << 24) | (d[1] << 16) | (d[2] << 8) | d[3] + + def _as_str(self, d): + return str(d, encoding='utf8') + + def _read_bytes(self, f, count): + val = f.read(count) + return val + + def _read_1_byte(self, f): + return self._read_bytes(f, 1) + + def _read_2_bytes(self, f): + return self._read_bytes(f, 2) + + def _read_3_bytes(self, f): + return self._read_bytes(f, 3) + + def _read_4_bytes(self, f): + return self._read_bytes(f, 4) + + def _read_8(self, f): + return self._as_8(self._read_bytes(f, 1)) + + def _read_16(self, f): + return self._as_16(self._read_bytes(f, 2)) + + def _read_24(self, f): + return self._as_24(self._read_bytes(f, 3)) + + def _read_32(self, f): + return self._as_32(self._read_bytes(f, 4)) + + def _parse_header(self, f): + if self._read_4_bytes(f) != b'MThd': + return None + if self._read_32(f) != 6: + return None + midi_format = self._read_16(f) + midi_number_of_tracks = self._read_16(f) + d = self._read_2_bytes(f) + if d[0] & 0x80: + ticks_per_frame = d[1] + negative_SMPTE_format = d[0] & 0x7F + ticks_per_quarternote = None + else: + ticks_per_frame = None + negative_SMPTE_format = None + ticks_per_quarternote = (d[0] << 8) | d[1] + return header.MidiHeader(midi_format, + midi_number_of_tracks, + ticks_per_frame, + negative_SMPTE_format, + ticks_per_quarternote) + + def _parse_variable_length_number(self, f): + value = self._read_8(f) + if not value & 0x80: + return value + value &= 0x7F + b = self._read_8(f) + while b & 0x80: + value = (value << 7) | (b & 0x7F) + b = self._read_8(f) + return (value << 7) | b + + def _parse_F0_sysex_event(self, delta_time, f): + length = self._parse_variable_length_number(f) + data = self._read_bytes(f, length) + return events.F0SysexEvent(delta_time, data) + + def _parse_F7_sysex_event(self, f, delta_time): + length = self._parse_variable_length_number(f) + data = self._read_bytes(f, length) + return events.F7SysexEvent(delta_time, data) + + def _parse_meta_event(self, f, delta_time): + meta_event_type = self._read_8(f) + length = self._parse_variable_length_number(f) + data = self._read_bytes(f, length) + if meta_event_type == 0x00: + return events.SequenceNumberMetaEvent(delta_time, self._as_16(data)) + elif meta_event_type == 0x01: + return events.TextMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x02: + return events.CopyrightMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x03: + return events.TrackNameMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x04: + return events.InstrumentNameMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x05: + return events.LyricMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x06: + return events.MarkerMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x07: + return events.CuePointMetaEvent(delta_time, self._as_str(data)) + elif meta_event_type == 0x20: + if length != 0x01: + return None + track = self._as_8(data) + if track > 15: + return None + return events.ChannelPrefixMetaEvent(delta_time, track) + elif meta_event_type == 0x2F: + if length != 0: + return None + return events.EndOfTrackMetaEvent(delta_time) + elif meta_event_type == 0x51: + if length != 3: + return None + return events.SetTempoMetaEvent(delta_time, self._as_24(data)) + elif meta_event_type == 0x54: + if length != 5: + return None + return events.SmpteOffsetMetaEvent(delta_time, data[0], data[1], + data[2], data[3], data[4]) + elif meta_event_type == 0x58: + if length != 4: + return None + return events.TimeSignatureMetaEvent(delta_time, data[0], data[1], + data[2], data[3]) + elif meta_event_type == 0x59: + if length != 2: + return None + return events.KeySignatureMetaEvent(delta_time, data[0], data[1]) + elif meta_event_type == 0x7F: + return events.SequencerSpecificMetaEvent(delta_time, data) + + def _parse_midi_event(self, f, delta_time, status): + if status & 0xF0 != 0xF0: + command = (status & 0xF0) >> 4 + channel = status & 0x0F + data_1 = self._read_8(f) & 0x7F + data_2 = 0 + if command in [8, 9, 10, 11, 14]: + data_2 = self._read_8(f) & 0x7F + if command == 8: + return events.NoteOffEvent(delta_time, channel, data_1, data_2) + elif command == 9: + if data_2 == 0: + return events.NoteOffEvent(delta_time, channel, data_1, data_2) + return events.NoteOnEvent(delta_time, channel, data_1, data_2) + elif command == 10: + return events.PolyphonicKeyPressureEvent(delta_time, channel, data_1, data_2) + elif command == 11: + return events.ControlChangeEvent(delta_time, channel, data_1, data_2) + elif command == 12: + return events.ProgramChangeEvent(delta_time, channel, data_1) + elif command == 13: + return events.ChannelPressureEvent(delta_time, channel, data_1) + elif command == 14: + return events.PitchWheelChangeEvent(delta_time, channel, (data_2 << 7) | data_1) + return None + message_id = status & 0x0F + if message_id == 0: + manufacturer_id = self._read_8(f) + data = [] + d = self._read_8(f) + while d != 0xF7: + data.append(d) + d = self._read_8(f) + return events.SystemExclusiveEvent(delta_time, manufacturer_id, data) + elif message_id == 2: + lo7 = self._read_8(f) + hi7 = self._read_8(f) + return events.SongPositionPointerEvent(delta_time, (hi7 << 7) | lo7) + elif message_id == 3: + return events.SongSelectEvent(delta_time, self._read_8(f)) + elif message_id == 6: + return events.TuneRequestEvent(delta_time) + elif message_id == 8: + return events.TimingClockEvent(delta_time) + elif message_id == 10: + return events.StartEvent(delta_time) + elif message_id == 11: + return events.ContinueEvent(delta_time) + elif message_id == 12: + return events.StopEvent(delta_time) + elif message_id == 14: + return events.ActiveSensingEvent(delta_time) + elif message_id == 15: + return events.ResetEvent(delta_time) + return None + + def parse_mtrk_event(self, f): + delta_time = self._parse_variable_length_number(f) + event_type = self._read_8(f) + if event_type == 0xF0: #sysex event + event = self._parse_F0_sysex_event(f, delta_time) + elif event_type == 0xF7: #sysex event + event = self._parse_F7_sysex_event(f, delta_time) + elif event_type == 0xFF: #meta event + event = self._parse_meta_event(f, delta_time) + else: #regular midi event + event = self._parse_midi_event(f, delta_time, event_type) + log(event) + return event + + def _parse_track(self, f): + if self._read_4_bytes(f) != b'MTrk': + return None + track_length = self._read_32(f) + track_data = [] + for _ in range(track_length): + event = self.parse_mtrk_event(f) + if event is None: + log('Error') + track_data.append(event) + if isinstance(event, events.EndOfTrackMetaEvent): + return track_data + return track_data + + def parse(self, filename): + with open(filename, 'rb') as f: + tracks = [] + h = self._parse_header(f) + for _ in range(h.number_of_tracks): + tracks.append(self._parse_track(f)) + return (h, tracks) diff --git a/NeoTrellis_M4_MIDI_Synth/sequencer.py b/NeoTrellis_M4_MIDI_Synth/sequencer.py new file mode 100644 index 00000000..5254304d --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/sequencer.py @@ -0,0 +1,62 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +import time + +class Sequencer(object): + + def __init__(self, synth): + self._synth = synth + self.set_tempo(120) + self._numerator = 4 + self._denominator = 2 + self._clocks_per_metronome_click = 24 + self.set_tempo(500000) + self.set_time_signature(4, 2, 24) + + def _tick(self): + time.sleep(self._tick_size) + + def play(self, track): + for event in track: + delta_time = 0 + while event.time > delta_time: + delta_time += 1 + self._tick() + print('Executing %s' % str(event)) + if event.execute(self): + return + + def set_tempo(self, tempo): + print('Setting tempo %d' % tempo) + self._tempo = tempo + self._tick_size = tempo / 250000000.0 + + def set_time_signature(self, numerator, denominator, clocks_per_metronome_click): + print('Setting time signature') + self._numerator = numerator + self._denominator = denominator + self._clocks_per_metronome_click = clocks_per_metronome_click + + def note_on(self, key, velocity): +# print('Note on') + self._synth.note_on(key, velocity) + + def note_off(self, key, velocity): +# print('Note off') + self._synth.note_off(key, velocity) + + def end_track(self): + pass +# print('End track') diff --git a/NeoTrellis_M4_MIDI_Synth/sound_files.zip b/NeoTrellis_M4_MIDI_Synth/sound_files.zip new file mode 100644 index 00000000..2e6f468e Binary files /dev/null and b/NeoTrellis_M4_MIDI_Synth/sound_files.zip differ diff --git a/NeoTrellis_M4_MIDI_Synth/synth.py b/NeoTrellis_M4_MIDI_Synth/synth.py new file mode 100644 index 00000000..adfe6f7d --- /dev/null +++ b/NeoTrellis_M4_MIDI_Synth/synth.py @@ -0,0 +1,123 @@ +""" +NeoTrellis M4 Express MIDI synth + +Adafruit invests time and resources providing this open source code. +Please support Adafruit and open source hardware by purchasing +products from Adafruit! + +Written by Dave Astels for Adafruit Industries +Copyright (c) 2018 Adafruit Industries +Licensed under the MIT license. + +All text above must be included in any redistribution. +""" + +# pylint: disable=unused-argument + +import time +import board +import audioio + +SAMPLE_FOLDER = '/samples/' # the name of the folder containing the samples +VOICE_COUNT = 8 + +def capitalize(s): + if not s: + return '' + return s[0].upper() + ''.join([x.lower() for x in s[1:]]) + + +class Synth(object): + + def __init__(self): + self._voice_name = None + self._voice_file = None + self._samples = [None] * 128 + self._channel_count = None + self._bits_per_sample = None + self._sample_rate = None + self._audio = None + self._mixer = None + self._currently_playing = [{'key': None, 'voice' : x} for x in range(VOICE_COUNT)] + self._voices_used = 0 + + def _initialize_audio(self): + if self._audio is None: + self._audio = audioio.AudioOut(board.A1) + self._mixer = audioio.Mixer(voice_count=VOICE_COUNT, + sample_rate=16000, + channel_count=1, + bits_per_sample=16, + samples_signed=True) + self._audio.play(self._mixer) + + def reset(self): + for i in range(len(self._samples)): + self._samples[i] = None + for p in self._currently_playing: + p['key'] = None + + @property + def voice(self): + return self._voice_name + + @voice.setter + def voice(self, v): + self._initialize_audio() + self._voice_name = capitalize(v) + self._voice_file = '/samples/%s.txt' % v.lower() + first_note = None + with open(self._voice_file, "r") as f: + for line in f: + cleaned = line.strip() + if len(cleaned) > 0 and cleaned[0] != '#': + key, filename = cleaned.split(',', 1) + self._samples[int(key)] = filename.strip() + if first_note is None: + first_note = filename.strip() + sound_file = open(SAMPLE_FOLDER+first_note, 'rb') + wav = audioio.WaveFile(sound_file) + self._mixer.play(wav, voice=0, loop=False) + time.sleep(0.5) + self._mixer.stop_voice(0) + + def _find_usable_voice_for(self, key): + if self._voices_used == VOICE_COUNT: + return None + available = None + for voice in self._currently_playing: + if voice['key'] == key: + return None + if voice['key'] is None: + available = voice + if available is not None: + self._voices_used += 1 + return available + return None + + def _find_voice_for(self, key): + for voice in self._currently_playing: + if voice['key'] == key: + return voice + return None + + def note_on(self, key, velocity): + fname = self._samples[key] + if fname is not None: + f = open(SAMPLE_FOLDER+fname, 'rb') + wav = audioio.WaveFile(f) + voice = self._find_usable_voice_for(key) + if voice is not None: + voice['key'] = key + voice['file'] = f + self._mixer.play(wav, voice=voice['voice'], loop=False) + + def note_off(self, key, velocity): + if self._voices_used > 0: + voice = self._find_voice_for(key) + if voice is not None: + self._voices_used -= 1 + self._mixer.stop_voice(voice['voice']) + voice['file'].close() + voice['file'] = None + voice['key'] = None