A powerful audio system called JACK allows us to build pipelines consisting of audio interfaces, players, recorders, filters and effects… and route sound streams (both PCM and MIDI) through them. MIDI messages can come from keyboards or other hardware MIDI controllers or from MIDI players and other software.
While relpipe-in-jack
can be used for monitoring MIDI messages (recording),
the relpipe-out-jack
(since v0.17) is designed for sending messages to JACK (playback).
So instead of playing Standard MIDI Files (.smf, .mid)
we can read relational data and send them as MIDI messages to JACK daemon that forwards them to a hardware or software synthesizer
or other program (called client in JACK terminology).
The relpipe-out-jack
reads relational data in the same format as relpipe-in-jack
.
So we can capture some MIDI data and play it later.
But this is bit boring and any music sequencer would do a better job.
We can have more fun with programmatically generating the „music“ (or rather just sounds in beginnings).
Thanks to the nature of Relational pipes, the relation containing such „music“ can come from many various sources.
In this example we will use the SQL language.
Before doing anything audible, we will start with saying hello to our sound module.
We need a relation with two attributes. The first one is event
and says what should be done.
The second one raw
contains the raw MIDI data. It is a SysEx message specific to Roland MT-32,
but it can be any MIDI event:
relpipe-in-cli \
--relation "midi" \
--attribute "event" string \
--attribute "raw" string \
--record "sysex" "f0 41 10 16 12 20 00 00 52 65 6c 70 69 70 65 21 6e f7" \
| relpipe-out-jack --connect-to j2a_bridge:playback
If we omit the --connect-to j2a_bridge:playback
part, the relpipe-out-jack
command
will wait util we connect it somewhere e.g. using QjackCtl.
We can control this behavior by explicitly specifying --required-connections N
(default N
is 1)
and say e.g. „Wait until you have 3 connections and then start sending“.
But because we specified the destination port, the relpipe-out-jack
command immediately
connects to it and sends data there.
The j2a_bridge:playback
port was created by the j2amidi_bridge
that bridges JACK-MIDI and ALSA-MIDI.
In QjackCtl or other tool, we can connect the ALSA end of this bridge to a physical interface (sound module, synthesizer).
If we have no audio hardware, we omit this part and stay in the JACK world – we can connect our MIDI ports to various
applications like Yoshimi, QMidiArp, Qtractor etc.
See more details on MT-32 and MIDI SysEx in: Monitoring MIDI messages using JACK.
Instruments (like particular pianos, guitars, drums, special effects etc.) can be set on our synthesizer for particular channels.
However for reproducible results and more comfort, it is better to configure the instruments through MIDI commands (Patch change).
Currently we misuse the sysex
event for this and send this configuration as raw MIDI commands
(the c0 0e
and c1 58
below).
In later versions of relpipe-out-jack
, there might be better support for instrument configuration.
The list of standard instruments is available at MIDI.org: General MIDI: GM 1 Sound Set.
Specifying the notes as raw MIDI messages is possible but uncomfortable.
So we will put note
in the event
attribute
and specify also time
, channel
, note_on
, note_pitch
and note_velocity
attributes.
The relpipe-out-jack
will compose the raw MIDI event from these values.
We are not going to create any tables or insert any records. Everything in this example is done in-memory by a single SELECT statement executed on an empty database. But of course, we can persist our creations in database tables and later play the music by SELECTing from them.
WITH
-- Configuration parameters:
duration AS (SELECT 16 AS value), -- in seconds
bpm AS (SELECT 120 AS value), -- beats per minute
-- Numbered beats, the outline:
beat AS (
WITH RECURSIVE beat0 AS (
SELECT 0 AS value
UNION ALL
SELECT beat0.value + 1 FROM beat0, duration, bpm
WHERE beat0.value < (duration.value * bpm.value / 60 - 1)
)
SELECT * FROM beat0
),
-- Set instruments for particular channels:
raw AS (
SELECT 0 AS time, 'c0 0e' AS raw UNION ALL -- channel 1 = Tubular Bells
SELECT 0 AS time, 'c1 58' AS raw -- channel 2 = Fantasia
),
-- Parts (quite random sounds):
drums_1 AS (
SELECT
9 AS channel,
beat.value AS beat, NULL AS custom_time,
250 * 1000 AS duration,
CASE WHEN beat.value % 4 IN (0,1,2) THEN 35 ELSE 40 END AS note_pitch,
80 AS note_velocity
FROM beat, bpm
),
bells_1 AS (
SELECT
0 AS channel,
beat.value AS beat, NULL AS custom_time,
500 * 1000 AS duration,
CASE WHEN beat.value / 4 % 2 = 0 THEN 68 ELSE 55 END AS note_pitch,
90 AS note_velocity
FROM beat, bpm
WHERE beat.value % 4 IN (1)
),
fantasia_1 AS (
SELECT
1 AS channel,
beat.value AS beat, NULL AS custom_time,
500 * 1000 AS duration,
60 + beat.value % 4 AS note_pitch,
90 AS note_velocity
FROM beat, bpm
WHERE beat.value % 4 IN (2,3,0)
),
-- Put all parts together:
notes AS (
SELECT
part.*,
CASE
WHEN custom_time IS NULL THEN beat * 1000 * 1000 * 60 / bpm.value
ELSE CAST(custom_time AS integer)
END AS time
FROM (
SELECT * FROM drums_1 UNION ALL
SELECT * FROM bells_1 UNION ALL
SELECT * FROM fantasia_1
) AS part, bpm
)
-- We need to emit two MIDI events: one for key press and one for key release.
-- So we add the release events (note_on = false) derived from the time and duration values.
SELECT 'note' AS event, time, channel, 1 AS note_on, note_pitch, note_velocity, '' AS raw FROM notes UNION ALL
SELECT 'note' AS event, time+duration AS time, channel, 0 AS note_on, note_pitch, note_velocity, '' AS raw FROM notes UNION ALL
SELECT 'sysex' AS event, time, 0 AS channel, 0 AS note_on, 0 AS note_pitch, 0 AS note_velocity, raw FROM raw
ORDER BY time ASC, event DESC;
Download: examples/jack-midi-1.sql
The SQL engine (SQLite by default) called from relpipe-in-sql
converts our script into a relation.
Then the relpipe-out-jack
translates this relation to actual MIDI events and sends them to JACK system.
relpipe-in-sql \
--relation "midi" "$(cat examples/jack-midi-1.sql)" \
--type-cast 'note_on' boolean \
| relpipe-out-jack \
--connect-to j2a_bridge:playback \
--connect-to relpipe-in-jack:midi-in
We connect it to two ports, so we can hear the sounds and at the same time, we can monitor the MIDI events
(through relpipe-in-jack
and usually relpipe-out-csv
).
We can also use relpipe-out-tabular
instead of relpipe-out-jack
and check generated data without sending them to JACK.
For visual check, we can connect to the JACK port of a MIDI program like Qtractor and record there our creation (like recording from a MIDI keyboard).
We can also do some real-time post-processing e.g. using QMidiArp (arpeggiator).
If we have some hardware, it should look and sound like this:
In this video, the Roland MT-32 and SC-88 Pro are daisy-chained and most sounds come from the SC-88.
n.b. the MIDI channel 1 (index 0) is usually silent on MT-32 (default configuration).
The relpipe-out-jack
supports following event types:
note
: press or release (depending on note_on
) of particular key on keyboard
control
: change value of a controller (knob, slider, switch etc.)
sysex
: SystemExclusive command specific for particular device or manufacturer (e.g. show some text on the display)
connect
: link two JACK ports using a virtual cable
disconnect
: put that cable away
Because the events of various types are usually interleaved, we pass them as records of one relation. Each type uses a different set of attributes:
event
|
note
|
control
|
sysex
|
connect
|
disconnect
|
time
|
✔ | ✔ | ✔ | ||
channel
|
✔ | ✔ | ✔ | ||
note_on
|
✔ | ||||
note_pitch
|
✔ | ||||
note_velocity
|
✔ | ||||
controller_id
|
✔ | ||||
controller_value
|
✔ | ||||
raw
|
✔ | ||||
source_port
|
✔ | ✔ | |||
destination_port
|
✔ | ✔ |
Note: this design pattern is one of possible ways how to implement the inheritance (an object-oriented concept) in the relational world. The relation contains one attribute that determines the type (or class) and then union of all attributes of all that types. Each type uses just its relevant attributes and the rest is empty.
The meaning of the attributes is following:
attribute | type | description |
event
|
string
|
type of the record |
time
|
integer
|
time in microseconds since start of the playback |
channel
|
integer
|
number of the MIDI channel; starts from 0 |
note_on
|
boolean
|
whether the key was pressed (true ) or released (false ) |
note_pitch
|
integer
|
pitch or tone; starts from 0; e.g. C4 is 60 |
note_velocity
|
integer
|
force with which a note is played; 0-127 |
controller_id
|
integer
|
number of the controller; starts from 0 |
controller_value
|
integer
|
value of the controller; 0-127 |
raw
|
string
|
raw MIDI bytes written in hexadecimal format; e.g. 91 3c 41
|
source_port
|
string
|
name of the JACK output port; e.g. system:capture_1
|
destination_port
|
string
|
name of the JACK input port; e.g. system:playback_1
|
Besides the event
we need to specify only attributes we use.
So if we e.g. only send SysEx messages, we need only event
and raw
attributes.
If the time
is missing, the event is processed as soon as possible (in the next real-time cycle).
The time
attribute is used for precise timing
– so the process is not driven by the time when the event arrives on STDIN of the relpipe-out-jack
.
Relational pipes, open standard and free software © 2018-2022 GlobalCode