NIXBOVIK Part 1: Nix Derived Melodies
An abuse of Nix to declaratively compose music
The Garnix team has been growing. As a consequence, many of us had never met in person. We took this year's NixCon as an occasion to change that. And after the conference, we stayed in a flat in Mainz, Germany for some in-person work and pairing.
In the spirit of having a bit of fun, and of sharing Nix knowledge, our CTO, Sönke Hahn, suggested that we have SIGBOVIK-inspired hackathon.
Overall it was a great evening, and we hope to be able to do this again next year. We will get to some of the other fun projects in future blog posts, but for now I want to talk about the one Sönke Hahn, Greg Pfeil and I paired on: Nix derived melodies.
Nix Derived Melodies
Our idea was to come up with a Nix expression to declaratively create music and output a WAV file of the resulting song. Instead of simply generating the full WAV of a song (too easy), we decided to have every note be its own Nix derivation. These derivations can be composed into new derivations with helper functions.
More specificially, we start from a single "musical primitive", sine. sine takes as arguments frequency, duration, and volume, and outputs a WAV file of the specified sine wave using sox:
sine = frequency: duration: vol: let seconds = duration / 6; in pkgs.runCommand "frequency.wav" { nativeBuildInputs = [ pkgs.sox ]; } '' sox -V -r 48000 -n -b 16 -c 2 $out synth ${builtins.toString seconds} sin ${builtins.toString frequency} vol ${builtins.toString (vol / 100)} '';
Now, for the combinators:
The sequence function takes a list of derivations to concatenate sequentially after each other. This is implemented as a foldr over a binarySeq function which concatenates two audio files and use a 0.1 second sine wave at 0 volume (and, arbitrarily, 14kHZ frequency) as the base case. (We originally didn't have a volume parameter, thinking that instead we could have a frequency outside the human range — a silence hack, if you will — serve as the base case. But because that was above the Nyquist frequency for our sample rate, we could hear "phantom" frequencies.)
binarySeq = first: second: pkgs.runCommand "sequence.wav" { nativeBuildInputs = [ pkgs.sox ]; } '' sox ${first} ${second} $out ''; sequence = lib.fold binarySeq (sine 14000 0.1 0.0);
The overlay function works similarly to the sequence function, but overlays derivations to play simultaneously. Once again using our 0.1 second. This time we used ffmpeg instead of sox, because why not:
binaryOverlay = first: second: pkgs.runCommand "overlay.wav" { nativeBuildInputs = [ pkgs.ffmpeg ]; } '' ffmpeg -i ${first} -i ${second} -filter_complex amix=inputs=2:duration=longest $out ''; overlay = lib.fold binaryOverlay (sine 14000 0.1 0.0);
Finally, the square function overlays multiple sine waves to approximate a square wave:
square = f: d: vol: overlay [ (sine f d vol) (sine (f*3.0) d (vol / 3.0)) (sine (f*5.0) d (vol / 5.0)) (sine (f*7.0) d (vol / 7.0)) (sine (f*9.0) d (vol / 9.0)) (sine (f*11.0) d (vol / 11.0)) (sine (f*13.0) d (vol / 13.0)) ];
You can see how we use these functions to compose our song here.
Nix Cache
While this is obviously a questionable way of building music, the Nix cache actually makes it tolerable. Rebuilding the song after a change to the melody takes 1-5 seconds depending on the complexity of the song./nix/store blowing up in size quite rapidly with all of the uncompressed audio generated while iterating on a song.
However, this does result inCoda
How does it sound? You can hear it for yourself with:
nix run github:garnix-io/sigbovik-music
(It will be faster if you use the Garnix cache)
We call it "An homage to Tom7's SIGBOVIK 2017 PAPER.EXE."
So that was our evening of doing things with Nix that it was never designed to do! Stay tuned for write-ups of other projects from the team.
Continue Reading
We've added incremental compilation to garnix. In this blog, we discuss prior art on incremental compilation in Nix, and describe our own design.
A short note about custom typing for functions in Nix