Garnix Logo
Sep 18, 2023Alex David

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 Melodiesshare

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 Cacheshare

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.? However, this does result in /nix/store blowing up in size quite rapidly with all of the uncompressed audio generated while iterating on a song.

Codashare

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

Dec 19, 2023Alex David

Microsoft's LSP is great for text-editor diversity, but it is severely lacking in flexibility for project-specific configuration.

Dec 7, 2023Alex David

Release announcement for garn version v0.0.19, which includes the ability to import Nix flakes from the internet and from your repository.

Nov 30, 2023Julian K. Arni

Release announcement for garn version v0.0.17 and v0.0.18. Includes better Haskell support, and less confusing file tracking.

View Archive →
black globe

Say hi, ask questions, give feedback