I come across a cool application called Sonic Pi which describes itself as "free code-based music creation and performance tool". I am immediately drawn to it as I realize it allows one to write music in a rich Ruby-based DSL. I learn that there are alternatives such as Tidal Cycles which uses a Haskell-based DSL, and Strudel which runs in the web browser and uses JavaScript. But Ruby was the first programming language I fell in love with. I will jump on any opportunity to use it for something fun.
How do I install this thing?
Unfortunately Sonic Pi does not supply a Linux build. They officially only support Windows, macOS, and Raspberry Pi OS (.deb). I'm currently using OpenSUSE Tumbleweed so those are not helpful. I check the Tumbleweed repositories with zypper search sonic, but no luck. Oh well, that's not too surprising, I'm used to more niche software not being available in the official repositories. 1 I can probably either find it on nixpkgs, or build it from source.
So I search for the package on nixpkgs, and I find what I am looking for: nixpkgs#sonic-pi. I fire up a terminal, and after a brief detour of figuring out I need nixGL, I get it to run!
$ nix profile add nixpkgs#sonic-pi $ sonic-pi # crashes due to "Could not initialize GLX" $ nix profile add --impure github:nix-community/nixGL $ nixGL sonic-pi # it works!
Sonic Pi is fun
I start skimming the built-in tutorial, and soon I'm already making something vaguely resembling primitive music. This is awesome! I would not consider myself a musical person. I have never played an instrument and I know nothing about music theory so I'm relying mostly on trial and error. Within a few hours I manage to create a couple of scripts that I'm unreasonably happy with. I've played around with making digital music a couple times before in software like Ableton, but writing code comes more naturally for me.
Wouldn't it be nice if...
After the initial excitement I start noticing that I'm constantly trying to use familiar keybindings that don't work (like Ctrl-a to select all). Turns out Sonic Pi uses emacs keybindings in which Ctrl-a means "go to start of line". I look around but can't find settings to change the keybindings. Time to dive into the code. Sonic Pi is open source so I'm just going to add the keybindings I need. And while I'm at it I might look into changing the default configuration/state directories to follow the XDG base directory specification. I'm not a fan of ~/.sonic-pi littering my home directory. 2
How hard could it be?
I open up the sonic-pi Github repository and do a couple of searches to make sure I'm not missing anything. I realize it already has three different sets of keybindings: emacs, win, and mac. The keybindings labeled "win" seem good enough for me. How come I couldn't find the setting? Well, it turns out that the version from nixpkgs is 4.5.1, and the keybinding setting was added in 4.6.0. I guess I can just build the new version from source, that shouldn't be too difficult?
I find the build instructions for Linux. It looks like Sonic Pi is written with C++/Qt and built with CMake. It depends on a number of libraries, and additionally requires two different dynamic languages, Ruby and Elixir (which in turn requires Erlang/BEAM) at runtime. A bit of an unholy union of different technologies. Definitely not how I like to build my software but hey if it works, it works.
It's always a bit annoying to build software that depends on dynamic libraries. Projects tend to give a command to install them only for a specific distro or two. In this case there is apt-get command for Debian/Ubuntu. I now need to figure out what the equivalent packages are called on Tumbleweed and install them. I do my best to install the ones I need but it still takes me several attempts to get it built successfully, adding missing libraries one at a time as I get new error messages. When I finally get it built, it crashes whenever I try to play a note:
terminate called after throwing an instance of 'std::runtime_error'
what(): Cannot connect to shared memory
fish: Job 1, './build/gui/sonic-pi' terminated by signal SIGABRT (Abort)
I follow up on a few leads on what could be causing the error but without success.
Maybe building it with nix magically fixes it?
I set out to modify the flake from nixpkgs to update it to 4.6.0. I have not touched .nix files before, but that should be simple, right? Nope. I have a lot of trouble getting this to work:
- If you just copy the default.nix flake from nixpkgs repository it does not work! I think they rely on some implicit external logic that comes from somewhere (I still don't know where). At first I build the flake with commands like
nix build --impure --expr 'let pkgs = import <nixpkgs> {}; in ./sonic-pi.nix'. I eventually figure out (with a lot of help from LLMs) that I can rewrite the .nix file itself in a different format to make it build it with justnix build. I find this design infuriating. - Every attempt to build the flake after making changes takes well over 3 minutes, which makes the feedback loop painfully long. I ask LLMs if there's a way to introduce caching or do incremental builds or something to make the feedback loop tighter, but they don't seem to offer any easy solutions.
- Sonic Pi now uses Qt6 instead of Qt5 so all the Qt libraries had different names. I add the ones I know will be needed, but missing ones keep popping up every time I build. After many attempts I essentially give up and just use qt6-full.
- Nix is not as declarative as I thought. A big part of it is just shell scripts embedded in strings, and there isn't any guarantee you got those right.
- RPATHs cause problems, and I don't know what they are (as I write this blog post I still don't fully understand them). I end up doing
patchelf --remove-rpathwhich I'm pretty sure is a bad way to solve the problem. - Why is
wrapQtAppneeded? The nix documentation is inadequate. After a bit of searching I think I've found its source code in a wrap-qt-apps-hook.sh in nixpkgs Github repository. It just callswrapProgram(which also comes from an unknown source). If you follow the trail all the way down it seems that it creates a shell script that calls the executable, but I'm still not sure what it does exactly. It seems to get rid of some errors about loading shared libraries though. The LLMs suggest numerous ways to hold it wrong. It is really difficult for me to figure out why/what is wrong because its purpose and interface are not clear to me.
After about 6 hours(!) of back and forth with three different LLMs 3 and dozens of build attempts I finally manage to get create a nix flake that builds and creates an executable that actually runs:
flake.nix
{ description = "Sonic Pi flake"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; flake-utils.url = "github:numtide/flake-utils"; }; outputs = { nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; qt6 = pkgs.qt6Packages; ruby = pkgs.ruby_3_2; sonicPi = pkgs.stdenv.mkDerivation rec { pname = "sonic-pi"; version = "4.6.0"; src = pkgs.fetchFromGitHub { owner = "sonic-pi-net"; repo = pname; rev = "v${version}"; hash = "sha256-sF/ksVhUzSV5P3Oe/T8hAiFMFiuOMEPmuBlUZtPSvtk="; }; mixFodDeps = pkgs.beamPackages.fetchMixDeps { inherit version; pname = "mix-deps-${pname}"; mixEnv = "test"; src = "${src}/app/server/beam/tau"; hash = "sha256-UoETv6X/Q/RmKb0uCsu59DH7OF0H+A9e7+4uRM/B1Wk="; }; strictDeps = true; nativeBuildInputs = [ ruby qt6.wrapQtAppsHook pkgs.copyDesktopItems pkgs.cmake pkgs.pkg-config pkgs.erlang pkgs.elixir pkgs.beamPackages.hex pkgs.autoPatchelfHook ]; buildInputs = [ qt6.qtbase qt6.full pkgs.catch2_3 pkgs.crossguid pkgs.reproc pkgs.platform-folders pkgs.alsa-lib pkgs.rtmidi pkgs.aubio pkgs.stdenv.cc.cc.lib pkgs.kissfftFloat pkgs.boost ]; qtWrapperArgs = [ "--prefix" "PATH" ":" (pkgs.lib.makeBinPath [ ruby pkgs.supercollider-with-sc3-plugins pkgs.jack2 pkgs.jack-example-tools pkgs.pipewire.jack ]) "--prefix" "LD_LIBRARY_PATH" ":" (pkgs.lib.makeLibraryPath buildInputs) ]; nativeCheckInputs = [ pkgs.parallel pkgs.supercollider-with-sc3-plugins pkgs.jack2 ]; cmakeFlags = [ "-DWITH_QT_GUI_WEBENGINE=OFF" "-DAPP_INSTALL_ROOT=${placeholder "out"}/app" ]; doCheck = true; postPatch = '' patchShebangs app bin ''; preConfigure = '' export SONIC_PI_HOME="$TMPDIR/spi" export HEX_HOME="$TEMPDIR/hex" export HEX_OFFLINE=1 export MIX_REBAR3='${pkgs.beamPackages.rebar3}/bin/rebar3' export REBAR_GLOBAL_CONFIG_DIR="$TEMPDIR/rebar3" export REBAR_CACHE_DIR="$TEMPDIR/rebar3.cache" export MIX_HOME="$TEMPDIR/mix" export MIX_DEPS_PATH="$TEMPDIR/deps" export MIX_ENV=prod echo 'Copying ${mixFodDeps} to Mix deps' cp --no-preserve=mode -R '${mixFodDeps}' "$MIX_DEPS_PATH" cd app ./linux-prebuild.sh -o ''; postBuild = '' ../linux-post-tau-prod-release.sh -o ''; checkPhase = '' runHook preCheck pushd ../server/ruby rake test popd pushd api-tests SONIC_PI_ENV=test parallel --no-notice -j2 --halt now,done=1 ::: 'jackd -rd dummy' 'ctest --verbose' popd runHook postCheck ''; installPhase = '' runHook preInstall ../linux-release.sh mkdir $out cp -r linux_dist/* $out/ install -Dm644 ../gui/images/logo-smaller.png $out/share/icons/hicolor/256x256/apps/sonic-pi.png ldd $out/app/build/gui/sonic-pi runHook postInstall ''; preFixup = '' patchelf --remove-rpath $out/app/build/gui/sonic-pi for file in $(grep -FrIl '${pkgs.erlang}/lib/erlang' $out/app/server/beam/tau); do substituteInPlace "$file" --replace '${pkgs.erlang}/lib/erlang' $out/app/server/beam/tau/_build/prod/rel/tau done ''; fixupPhase = '' wrapQtApp $out/app/build/gui/sonic-pi ${toString qtWrapperArgs} ''; desktopItems = [ (pkgs.makeDesktopItem { name = "sonic-pi"; exec = "sonic-pi"; icon = "sonic-pi"; desktopName = "Sonic Pi"; comment = meta.description; categories = [ "Audio" "AudioVideo" "Education" ]; }) ]; meta = with pkgs.lib; { homepage = "https://sonic-pi.net/"; description = "Free live coding synth for everyone"; license = licenses.mit; platforms = platforms.linux; }; }; in { packages = { default = sonicPi; }; defaultPackage = sonicPi; } ); }
And...
terminate called after throwing an instance of 'std::runtime_error'
what(): Cannot connect to shared memory
/home/mikko/Packages/my-nix/sonic-pi/result/bin/sonic-pi: line 18: 2900526 Aborted (core dumped) $DIR/../app/build/gui/sonic-pi
Nope, nix did not help. At this point I'm out of both ideas and motivation to dig deeper.
-
Well actually there is an experimental package for Tumbleweed, but I try to avoid adding extra repositories to zypper when I can. ↩
-
You can make it use a different directory by setting the environment variable
SONIC_PI_HOME=~/.local/state/sonic-pi, but annoyingly it will still create a hidden directory~/.local/state/sonic-pi/.sonic-pi/. ↩ -
I used free versions ChatGPT, Perplexity, and Claude. ChatGPT felt like the worst of the three, but I wasn't happy with any of them. Many of their suggestions were unhelpful or straight up wrong. In retrospect they probably wasted more time than they saved. They are very good at suggesting something so you feel less stuck, but if most of the suggestions lead you down the wrong paths... ↩
i love sonic and pie