Composing composition

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.

KISS and tell

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.

Tone

boar is my fundamental means of making sound. It was the first and only synthesizer I ever finished because I applied some key constraints.

  1. The program completely disregards MIDI and all other interoperability protocols. It only accepts plaintext syntax from stdin.
  2. The grammar exclusively deals with one-letter commands accompanied by a solitary argument. J was an influence here.
  3. All commands only affect immediate tone; there are no "musical" abstractions beyond recognizing the standard Western octave system.
  4. The synthesis engine offers nothing more than a FM carrier:modulator oscillator pair. More complex patches are expected to be handled via multiple instances of 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

play

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

play

It's actually not that hard to decipher.

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.

Notes

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'.

play

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.

Clock

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

play

Motif

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.

play

Conclusion

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.