Merge pull request #425 from dastels/synth
Add NeoTrellis M4 MIDI synth code and files
This commit is contained in:
commit
6095703c51
7 changed files with 959 additions and 0 deletions
409
NeoTrellis_M4_MIDI_Synth/events.py
Normal file
409
NeoTrellis_M4_MIDI_Synth/events.py
Normal file
|
|
@ -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)
|
||||
43
NeoTrellis_M4_MIDI_Synth/header.py
Normal file
43
NeoTrellis_M4_MIDI_Synth/header.py
Normal file
|
|
@ -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)
|
||||
65
NeoTrellis_M4_MIDI_Synth/main.py
Normal file
65
NeoTrellis_M4_MIDI_Synth/main.py
Normal file
|
|
@ -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
|
||||
257
NeoTrellis_M4_MIDI_Synth/parser.py
Normal file
257
NeoTrellis_M4_MIDI_Synth/parser.py
Normal file
|
|
@ -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)
|
||||
62
NeoTrellis_M4_MIDI_Synth/sequencer.py
Normal file
62
NeoTrellis_M4_MIDI_Synth/sequencer.py
Normal file
|
|
@ -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')
|
||||
BIN
NeoTrellis_M4_MIDI_Synth/sound_files.zip
Normal file
BIN
NeoTrellis_M4_MIDI_Synth/sound_files.zip
Normal file
Binary file not shown.
123
NeoTrellis_M4_MIDI_Synth/synth.py
Normal file
123
NeoTrellis_M4_MIDI_Synth/synth.py
Normal file
|
|
@ -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
|
||||
Loading…
Reference in a new issue