JavaScript functional reactive MIDI Programming
JavaScript functional reactive MIDI library for WebMIDI applications.
Reactive programming offers a similar feeling to using cables. Things are connected and data flows throw them. Also, a lot of typical MIDI processing seems very similar to what reactive frameworks offer: filtering messages, mapping one type of message to another, adapting values, etc.
It’s difficult with current tools to process MIDI in the time-domain. Using rxjs (or other reactive frameworks) allows using already created and tested functions that work in time-domain in an easier way, allowing things like buffering or maintaining state a lot easier.
Not being able of trusting the language virtual machine for rock-solid precise timing, and after reading “A Tale of Two Clocks - Scheduling Web Audio with Precision”, it became clear that to be able to have rock-solid timing precise MIDI message timestamps were necessary.
Unit Testing is a great tool to check valid timestamps in this case, and functional programming allows easier testing by allowing easier function composition.
Ramda library has become a fundamental tool in the process.
npm install frmidi
For input/output in node it’s necessary to use JZZ library navigator implementation.
<script type="module">
import { initialize, logPorts } from 'https://unpkg.com/frmidi'
initialize ().then (logPorts)
</script>
> import { initialize, logPorts } from 'frmidi'
> initialize ()
> logPorts ()
Load on Efimera%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B13,0%5D%7D,%7B%22lines%22:%5B%22logPorts%20()%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B11,0%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:3%7D)
> import { initialize, input, output } from 'frmidi'
> initialize ()
> var my_input = input ('Port-0')
> var my_output = output ('Port-1')
Load on Efimera%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B13,0%5D%7D,%7B%22lines%22:%5B%22var%20my_input%20=%20input%20(‘Port-0’)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B31,0%5D%7D,%7B%22lines%22:%5B%22var%20my_output%20=%20output%20(‘Port-1’)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B33,0%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:4%7D)
> import { input, output } from 'frmidi'
> var my_input = input ('dummy')
> var my_output = output ('dummy')
Load on Efimera%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B30,0%5D%7D,%7B%22lines%22:%5B%22var%20my_output%20=%20output%20(‘dummy’)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B32,0%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:3%7D)
As an input is an observable and and output is a function that accepts an observable as its parameter, we just need input function as parameter to output function to make a connection between them.
> import { input, output } from 'frmidi'
> var my_input = input ('dummy')
> var my_output = output ('dummy')
> var subscription = my_output (my_input)
Load on Efimera%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B30,0%5D%7D,%7B%22lines%22:%5B%22var%20my_output%20=%20output%20(‘dummy’)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B32,0%5D%7D,%7B%22lines%22:%5B%22var%20subscription%20=%20my_output%20(my_input)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B39,0%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:4%7D)
> import { input, output } from 'frmidi'
> var my_input = input ('dummy')
> var my_output = output ('dummy')
> var subscription = my_output (my_input)
> subscription.unsubscribe ()
Load on Efimera%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B30,0%5D%7D,%7B%22lines%22:%5B%22var%20my_output%20=%20output%20(‘dummy’)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B32,0%5D%7D,%7B%22lines%22:%5B%22var%20subscription%20=%20my_output%20(my_input)%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B39,0%5D%7D,%7B%22lines%22:%5B%22subscription.unsubscribe%20()%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B27,0%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:5%7D)
Is it possible to simulate messages coming from an input by using next (msg) method from the input.
Also, we can inspect what’s going out by subscribing to the output.
> import { input, output, on } from 'frmidi'
> var my_input = input ('dummy')
… var my_output = output ('dummy')
… my_output.subscribe (console.log)
… my_output (my_input)
… my_input.next (on (64))
Load on Efimera%22,%22var%20my_output%20=%20output%20(‘dummy’)%22,%22my_output.subscribe%20(console.log)%22,%22my_output%20(my_input)%22,%22my_input.next%20(on%20(64))%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B23,4%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:2%7D)
At this point is when true functionality of frMIDI is shown. Inputs are just observables and outputs are just observers. Between them we can use ReactiveX to do whatever we want with the streams.
> import { input, output, on, off, cc, isNote } from 'frmidi'
> import { filter } from 'rxjs/operators'
> var my_input = input ('dummy')
… var my_output = output ('dummy')
… my_output.subscribe (console.log)
… my_input.pipe (filter (isNote)).subscribe (my_output)
… my_input.next (on (64))
… my_input.next (cc (37)) // Rejected
… my_input.next (off (64))
Load on Efimera%22,%22var%20my_output%20=%20output%20(‘dummy’)%22,%22my_output.subscribe%20(console.log)%22,%22my_input.pipe%20(filter%20(isNote)).subscribe%20(my_output)%22,%22my_input.next%20(on%20(64))%22,%22my_input.next%20(cc%20(37))%22,%22my_input.next%20(off%20(64))%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B24,6%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:3%7D)A
Here we are going to filter notes on input1 and control messages on input2 and then merge both streams and send to output1.
> import { input, output, isNote, isControlChange, on, off, cc } from 'frmidi'
> import { merge } from 'rxjs'
… import { filter } from 'rxjs/operators'
> var input1 = input ('dummy')
… var input2 = input ('dummy')
… var output1 = output ('dummy')
… output1.subscribe (console.log)
… merge ( input1.pipe (filter (isNote)), input2.pipe (filter (isControlChange)) ).subscribe (output1)
… input1.next (on (64))
… input1.next (cc (37, 127)) // Rejected
… input2.next (off (67))
… input2.next (cc (47, 0)) // Rejected
Load on Efimera%22,%22var%20input2%20=%20input%20(‘dummy’)%22,%22var%20output1%20=%20output%20(‘dummy’)%22,%22output1.subscribe%20(console.log)%22,%22merge%20(%20input1.pipe%20(filter%20(isNote)),%20input2.pipe%20(filter%20(isControlChange))%20).subscribe%20(output1)%22,%22input1.next%20(on%20(64))%22,%22input1.next%20(cc%20(37,%20127))%22,%22input2.next%20(off%20(67))%22,%22input2.next%20(cc%20(47,%200))%22,%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B21,5%5D%7D,%7B%22lines%22:%5B%22%22%5D,%22history%22:%5B%5D,%22completions%22:%5B%5D,%22autocompletion%22:%22%22,%22cursor%22:%5B0,0%5D%7D%5D,%22focused%22:2%7D)
frMIDI integrates colxi/midi-parser-js for working with midi files.
Filtering note on/off that has velocity between 64 and 96.
> in.pipe (rxo.filter (R.allPass ([isNote,
… lensP (velocity, R.gte, 64),
… lensP (velocity, R.lte, 96)])))
… .subscribe (console.log)
frMIDI is open-sourced software licensed under GNU GPL-3.0 license.
(Originally, MIT license was used but it was not compatible with integrating colxi/midi-parser-js library, so it was changed to GNU GPL-3.0)