Astrid MIDI note handling
Astrid MIDI note handling
Astrid MIDI note handling
Here's a little puzzle:
I'm working on the bit of astrid that listens for MIDI notes and triggers renders (and playback) of astrid instrument scripts.
Example instrument script:
from pippi import dsp, oscs
MIDI = ('MidiSport 2x2:MidiSport 2x2 MIDI 1 20:0', 60, 70)
def play(ctx):
# ctx.p contains parameters passed with
# the triggering play command.
# Play commands initiated by the MIDI relay
# will include the MIDI note as a parameter.
# Parameters are provided as strings.
note = float(ctx.p.note or 60)
# ctx.log will write a string to the system log
ctx.log('note %s' % note)
# Convert the MIDI note to a frequency
freq = tune.mtf(note)
# Set up the osc
osc = oscs.SineOsc(freq=freq, amp=0.15)
# Render a 1 second buffer
out = osc.play(1)
# Add an envelope
out = out.env('pluckout').taper(dsp.MS * 5)
# Off to the mixer for playback
yield out
Each instrument script gets its own renderer process (or pool of renderers, but conceptually they are the same here) whose job is basically to:
- Load (and reload) the instrument script into an embedded python interpreter
- Wait on astrid play messages directed toward this instrument script
- For each play message: execute all the play methods in the instrument script and serialize the resulting buffers to place on to a buffer queue (these buffers are deserialized by the mixer process and scheduled for playback)
The command console process can start and stop MIDI relays. A relay is a thread that opens a MIDI port for a given device and waits for messages to arrive. Each relay listens on a single device. (I could change that so there is just one relay for all devices, but for this problem that's not a concern.)
If the MIDI relay gets a MIDI control change message it just writes that into the redis session so instrument scripts can read the values if they please at render time.
If the MIDI relay gets a MIDI note_on message it should trigger a play message to the appropriate renderer process, if that instrument script has been registered for triggers and the MIDI note falls within the range of notes registered.
For example lets say I have an instrument script called Bob, and I've included a line in the Bob script that says: listen for triggers from the Alice device, but only trigger a play message for Bobs if the note value is greater than or equal to 60 and less than or equal to 64. If I press a key on the Alice device that triggers a MIDI note 63, then I'd like the MIDI relay for the Alice device to send a play message to the Bob renderer. If the note is less than 60 or greater than 64 then the note_on should just be ignored.
OK, here's the (maybe) fun part: what's a nice way to structure and implement this behavior?
Specifically, I'm inside the MIDI relay. I've just got a note_on message. I've got the note value, and I know the MIDI device name. Should I trigger an astrid play message? Which renderer should get that play message?
My first pass implementation is this:
When an instrument script is loaded into the embedded python interpreter, the loader checks for a property called 'MIDI' and reads the corresponding device name, low trigger value and high trigger value. (It can be a list of tuples, or just a single tuple.) For each tuple found, the name of the instrument script is added as a key to a redis hashmap (with a dummy value of 1) whose name is the name of the device. (Well, with the suffix "-trigger" for no really good reason.) Additionally, two values are set directly in redis. The low note value is set with a key using the device name and the instrument name together plus the suffix "-low" and the high note value is set in the same way with the suffix "-high".
When a MIDI relay gets a note_on, it first looks up the hashmap for the device (with the "-trigger" suffix) and checks to see if there are any keys in the hashmap. (Actually that's not quite true: there's a local cache of this list of keys that gets updated every time the MIDI message queue is flushed, but same idea.) If there are instrument names in the hashmap, it loops over each and for each instrument name it first makes a redis query to find the low note value. If the current note being handled is lower than the low note value, then the routine exits and moves on to other instruments or waits for more MIDI messages. If not, it makes a redis query to find the high note value and again checks to see if the current note is below the high threshold. If the current note is above the high threshold then the routine exits and moves on to other instruments or waiting for more notes, otherwise it sends a play message to the given instrument script renderer! (And passes the MIDI note metadata along to the renderer so the script can use it during the render if it wants to -- like, to you know, set the pitch and velocity of the render to the same given in the note_on message or whatever.)
This is OK, but given a MIDI note (integer) and a device name (string) is there a nicer way to structure this data so the lookup doesn't need to loop over all devices and ranges? I can't think of how, but!
Love, Me
Astrid MIDI note handling
Astrid MIDI note handling
@mathr@post.lurk.org oh, that's an interesting idea! I like it. would be nice to make a single lookup on each note on to get a list of mapped instruments. writing / updating the names would only happen when the instrument module is loaded or reloaded so writing the extra keys probably isn't a big deal. (not like this is most optimized system in the world anyway haha) nice idea, thanks!
Astrid MIDI note handling
@erik maybe store a list of devices for each note in an array of length 128 ? then you can look up the note directly. updating the ranges would require modifying all the lists of the affected notes. but I dunno how this would work with redis.