If you've read any previous notes here, you'd know that music is the only reason why I'm a programmer in the first place. I was hacking away at MIDI and dipping my feet in DSP well before I called coding a career. With Christmas approaching I finally have the time to jot down some words on the idiosyncratic audio environment I've pieced together over the past decade. The raw performance might be spotty and the output underwhelming, but interface is everything; DAWs, csound, et al are simply not how I wish to write music. Neither is the example I'm about to present, but the inherent composability of this approach inches me closer to ideal with each script I pile on top of another.
I must've first downloaded Curtis Roads' Computer Music Tutorial in 2014 or so (I've since bought a hard copy) and learned to pitch a wavetable around the same time. Many abortive attempts at spinning up my own synthesizer/sampler followed. Each was a 500lb gorilla rather than a purpose built instrument; I wasted so much time on bullshit ring buffers full of "events" and infinitely-arrangeable "unit generators" implemented with tagged unions. All cool ideas that ultimately never made a peep. It was only in 2019 when I finally got around to applying the Unix philosophy to my noise making. Just write lots of little programs that do one thing, and stitch them together via standard input/output. It's a universal concept, but OpenBSD and sndio made it easier. This finally broke the silence. While I'm far from penning symphonies on my little laptop, it now at least has a place among the other hardware in my studio.
What follows isn't exactly an example song: more of a "motif." I made it using dependency-free C, standard sh
shell scripting, and Chicken Scheme on the aforementioned OpenBSD laptop with sndio as the audio engine.
boar is my fundamental means of making sound. It was the first and only synthesizer I ever finished because I applied some key constraints.
boar
. It will never be a DX7; certain algorithms cannot be replicated.A stdin shell can be spun up with rlwrap boar
. Notes are turned on with n
and off with o
. MIDI note numbers are provided instead of C5, A6, etc.
rlwrap boar
n72
o72
You might find it remarkably ugly, but another J-ism (uh) is at play here. Multiple commands can be put on a single line, allowing one to view the entire patch configuration at a glance. Compare that to a menu-diving synth. Here is an unstable sort of pad. Maybe a bit BoC?
rlwrap boar
l0.65;a0.1;s1;r1;w4;c4;a.6;W7;L1.5;P0.01
n67;n69;n74
o67;o69;o74
It's actually not that hard to decipher.
l0.65
= master volume (loudness) 65%a0.1
= carrier attack 0.1 seconds1
= carrier sustain 100%r1
= carrier release 1 secondw4
= carrier wave #4 (sawtooth)c4
= carrier wave complexity attenuation level 4. Higher values means less harmonics.a.6
= carrier attack envelope #6 (logarithmic). Uses the same numbers as w
, so a.4
would be a linear ramp envelope on attack. Possible to have ridiculous envelopes like sines.W7
= modulating wave #7 (noise). All modulator commands are capitalized.L1.5
= modulating amplitude 150%.P0.01
= modulating frequency 1% that of the fundamental key frequency. The result is a slight warble one might expect from a LFO rather than a FM harmonic explosion.This "everything on one line approach" facilitates a stateless sort of interactive composition. When modifying the patch, I usually re-enter the entire previous line with the desired parameters changed. The shell history is version control.
I rarely even edit a boar session directly like this. You can have it accept input from a FIFO.
mkfifo /tmp/fifo
boar <> /tmp/fifo &
It's then possible to specify the patch in Vim, highlight the line of commands you want to issue, and run
! xargs echo > /tmp/fifo
to send it to boar. FIFOs also allow one to edit parameters interactively in one shell while timed notes are fed in from an automatic source, as we shall see.
boar's exclusive understanding of stdin means that it's not too difficult to abstract a sequencer on top of it. I wrote pop, which works much like a modular synth; you trigger it (one line of stdin) and it outputs a signal (one line of stdout). Rigorous definitions of time never enter the equation. There are no MIDI clocks nor 3/4 signatures nor anything like that—just lines of output that you take when you want. The contents are arbitrary, but here's a chord progression of boar notes.
o63;o67;o70;n67;n69;n74
o67;o69;o74;n63;n67;n70
o63;o67;o70;n62;n67;n69
o62;o67;o69;n63;n67;n70
# comment placed here for markdown formatting purposes
If this were saved to /tmp/chords.score
, then one could run
pop /tmp/chords.score
and then receive a line from the file back every time he or she entered a newline. This becomes a looping sequencer when the input comes from a steady source and the output is piped into a boar FIFO.
mkfifo /tmp/fifo
boar <> /tmp/fifo &
echo "l0.65;a0.1;s1;r1;w4;c4;a.6;W7;L1.5;P0.01" > /tmp/fifo
pop /tmp/chords.score > /tmp/fifo
This is me hand-strumming the enter key to advance through the chord progression specified in 'chords.score'.
But what a pain it is to write notes in terms of numbers, get the spacing between lines correct, and remember to turn the previous input off. That's why I wrote boar-composer, which allows one to specify a loop as a 2D scheme list, which compiles down into a pop score. The above chord progression can be re-expressed as
((steps 32)
(notes `((,G5 ,A5 ,D6)
(,Eb5 ,G5 ,Bb5)
(,D5 ,G5 ,A5)
(,Eb5 ,G5 ,Bb5))))
boar-composer will write any sublist of notes
as a boar chord to stdout, with steps
lines between it and the next chord. Note the quasi-quoting syntax with G5
et al; these are global variables that compile down to the note numbers used earlier.
Even solitary notes must be wrapped in lists. Rests are empty lists, with a handy ∅
synonym. A builtin arp
function helps with this. Here is a monophonic bassline. Note its quasiquoting syntax as well. This is done to keep the final list two-dimensional.
((steps 1)
(notes `(,@(arp (list D4 ∅ ∅ ∅
Eb4 ∅ ∅ ∅
∅ ∅ ∅ ∅
∅ ∅ ∅ ∅
D4 ∅ ∅ ∅
∅ ∅ Eb4 ∅
∅ ∅ D4 ∅
Eb4 ∅ ∅ ∅)))))
In keeping with the "everything at a glance" theme, it's useful to write every instrument's part in a single Scheme file, select the individual S-expressions, and run
! boar-composer > /path/to/score/file.score
to compile them out to pop files. This can even be done live while pop is running.
One can freely mix whatever Scheme he or she wishes into these lists, vastly expanding composition options.
Since everything operates on stdin triggers, it's necessary to have a steady source for them. I wrote two programs that can help with that. bang listens to the microphone port for a signal over a certain amplitude and prints to stdin when it encounters this. It's useful for interfacing with analog signals, like that of my TR-606. There's also boar-midi, which offers the midi-boar
command. This derives boar n
and o
commands from an incoming MIDI stream.
midi-boar
n32866
o98
n32820
o52
n32844
o76
n32857
o89
One can obviously hook a sequencer like a SQ-1 directly up to a boar FIFO and drive the pitch though its (finicky) knobs, but when multiple instruments are involved, it's better to clock respective pop scores for them. That is: filter for n
commands only, and disregard the actual pitch value; just use them as pulses.
The pop-shield
command can filter lines by a leading character. Since n
and o
come in pairs, failing to isolate one from the other produces an unsteady galloping rhythm. This is smoothed out by only letting n
through.
midi-boar | pop-shield n
32866
32820
32844
32857
The pitch values that do pass through are irrelevant; they are simply used to increment a pop score by a line. A score can be looped through at a steady pace now, dynamically responding to the speed knob of the SQ-1.
mkfifo /tmp/fifo
boar <> /tmp/fifo &
echo "l0.65;a0.1;s1;r1;w4;c4;a.6;W7;L1.5;P0.01" > /tmp/fifo
midi-boar | pop-shield n | pop /tmp/chords.score > /tmp/fifo
A four instrument loop (chords, bass, kick, snare) is expressed in two main files: a Scheme file containing note information, and a sh script that handles the boar timbres and signal routing.
The notes. As mentioned earlier, each one is compiled via boar-composer
to a *.score
file.
; chords
((steps 32)
(notes `((,G5 ,A5 ,D6)
(,Eb5 ,G5 ,Bb5)
(,D5 ,G5 ,A5)
(,Eb5 ,G5 ,Bb5))))
; bass
((steps 1)
(notes `(,@(arp (list D4 ∅ ∅ ∅
Eb4 ∅ ∅ ∅
∅ ∅ ∅ ∅
∅ ∅ ∅ ∅
D4 ∅ ∅ ∅
∅ ∅ Eb4 ∅
∅ ∅ D4 ∅
Eb4 ∅ ∅ ∅)))))
; snare
((steps 1)
(notes `(,@(arp (list ∅ ∅ ∅ ∅
C5 ∅ ∅ ∅
∅ ∅ ∅ ∅
C5 ∅ ∅ ∅)))))
; kick
((steps 1)
(notes `(,@(arp (list C2 ∅ ∅ ∅
∅ ∅ ∅ ∅
C2 ∅ ∅ ∅
∅ ∅ ∅ ∅
C2 ∅ ∅ ∅
∅ ∅ C2 ∅
C2 ∅ ∅ ∅
∅ ∅ ∅ ∅)))))
The script.
#!/bin/sh
sampleRate=18000
bassPatch="l0.9;a0.005;d0.3;s0;r0.4;p1;L4;P0.5"
chordsPatch="l0.65;a0.1;s1;r1;w4;c4;a.6;W7;L1.5;P0.01"
snarePatch="l0.65;s0;d0.1;x20;L1;W7;X9999;w2"
kickPatch="l0.9;s0;d0.1;x70;w1"
mkfifo /tmp/bass
mkfifo /tmp/chords
mkfifo /tmp/snare
mkfifo /tmp/kick
boar -rate $sampleRate <> /tmp/bass &
boar -rate $sampleRate <> /tmp/chords &
boar -rate $sampleRate <> /tmp/snare &
boar -rate $sampleRate <> /tmp/kick &
echo $bassPatch > /tmp/bass
echo $chordsPatch > /tmp/chords
echo $snarePatch > /tmp/snare
echo $kickPatch > /tmp/kick
midi-boar | pop-shield n | pop bass.score > /tmp/bass &
midi-boar | pop-shield n | pop chords.score > /tmp/chords &
midi-boar | pop-shield n | pop snare.score > /tmp/snare &
midi-boar | pop-shield n | pop kick.score > /tmp/kick &
Running this will place all the instruments into background standby. Pressing play on the SQ-1 will run them all at once. Once again, it responds dynamically to speed changes.
That's a lot of abstraction for something that's very much not a song, but it only snowballs from there. pop has the ability to jump between different patterns much like one would with a drum machine. boar-composer can accept more Scheme. It's easy to see how rigidly grid-based music like techno and minimal synth can arise from this environment. And I can always script something else out whenever I encounter another barrier.