Shadertone

Exploring Shadertone

Context

I wondered about making a desktop shadertoy clone in Clojure. A quick search turned up an existing project that already does exactly that - overtone/shadertoy. Instead of rewriting that from scratch, this is my log of making it work on my machine (2024-03-15 Debian 13 - trixie, testing). It was a challenge.

Prerequisites

Meeting the Repo

The starting point was git clone https://github.com/overtone/shadertone.git && cd shadertone and lein repl.

$ lein repl
--> Loading Overtone...
--> Booting internal SuperCollider server...
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fa08411c479, pid=17628, tid=17743
#
# JRE version: OpenJDK Runtime Environment (17.0.10+7) (build 17.0.10+7-Debian-1)
# Java VM: OpenJDK 64-Bit Server VM (17.0.10+7-Debian-1, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# C  [ld-linux-x86-64.so.2+0x2479]
#
# Core dump will be written. Default location: Core dumps may be processed with "/usr/lib/systemd/systemd-coredump %P %u %g %s %t 9223372036854775808 %h" (or dumping to /home/owen/empty/demo/shadertone/core.17628)
#
# An error report file with more information is saved as:
# /home/owen/empty/demo/shadertone/hs_err_pid17628.log
#
# If you would like to submit a bug report, please visit:
#   https://bugs.debian.org/openjdk-17
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
*** ERROR: dlopen '/home/owen/empty/demo/shadertone/target/native/linux/x86_64/liblwjgl64.so' err 'libjawt.so: cannot open shared object file: No such file or directory'
Subprocess failed (exit code: 134)

This isn’t ideal. The JVM is crashing! First objective is to start the Clojure REPL without any errors.

Starting the REPL

Update Dependencies

Now I happen to know that libjawt is installed (dpkg -S libawt.so), so something is going wrong. I updated project.clj to use the current version of Clojure (1.11.2). The error got a lot more descriptive:

--> Loading Overtone...
#error {
 :cause Call to clojure.core/ns did not conform to spec.
 :data #:clojure.spec.alpha{:problems ({:path [:ns-clauses :refer-clojure :clause], :pred #{:refer-clojure}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-refer-clojure :clojure.core.specs.alpha/ns-refer-clojure], :in [2 0]} {:path [:ns-clauses :require :clause], :pred #{:require}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-require :clojure.core.specs.alpha/ns-require], :in [2 0]} {:path [:ns-clauses :import :classes :class], :pred clojure.core/simple-symbol?, :val [java.awt.Toolkit], :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/import-list], :in [2 1]} {:path [:ns-clauses :import :classes :package-list :classes], :reason Insufficient input, :pred clojure.core/simple-symbol?, :val (), :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/import-list :clojure.core.specs.alpha/package-list :clojure.core.specs.alpha/package-list], :in [2 1]} {:path [:ns-clauses :use :clause], :pred #{:use}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-use :clojure.core.specs.alpha/ns-use], :in [2 0]} {:path [:ns-clauses :refer :clause], :pred #{:refer}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-refer :clojure.core.specs.alpha/ns-refer], :in [2 0]} {:path [:ns-clauses :load :clause], :pred #{:load}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-load :clojure.core.specs.alpha/ns-load], :in [2 0]} {:path [:ns-clauses :gen-class :clause], :pred #{:gen-class}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-gen-class :clojure.core.specs.alpha/ns-gen-class], :in [2 0]}), :spec #object[clojure.spec.alpha$regex_spec_impl$reify__2503 0x32fdec40 clojure.spec.alpha$regex_spec_impl$reify__2503@32fdec40], :value (overtone.libs.app-icon (:use [clojure.java.io] [overtone.helpers.lib :only [branch]] [overtone.helpers.system :only [get-os]]) (:import [java.awt.Toolkit])), :args (overtone.libs.app-icon (:use [clojure.java.io] [overtone.helpers.lib :only [branch]] [overtone.helpers.system :only [get-os]]) (:import [java.awt.Toolkit]))}
 :via
 [{:type clojure.lang.Compiler$CompilerException
   :message Syntax error macroexpanding clojure.core/ns at (overtone/libs/app_icon.clj:1:1).
   :data #:clojure.error{:phase :macro-syntax-check, :line 1, :column 1, :source overtone/libs/app_icon.clj, :symbol clojure.core/ns}
   :at [clojure.lang.Compiler checkSpecs Compiler.java 6989]}
  {:type clojure.lang.ExceptionInfo
   :message Call to clojure.core/ns did not conform to spec.
   :data #:clojure.spec.alpha{:problems ({:path [:ns-clauses :refer-clojure :clause], :pred #{:refer-clojure}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-refer-clojure :clojure.core.specs.alpha/ns-refer-clojure], :in [2 0]} {:path [:ns-clauses :require :clause], :pred #{:require}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-require :clojure.core.specs.alpha/ns-require], :in [2 0]} {:path [:ns-clauses :import :classes :class], :pred clojure.core/simple-symbol?, :val [java.awt.Toolkit], :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/import-list], :in [2 1]} {:path [:ns-clauses :import :classes :package-list :classes], :reason Insufficient input, :pred clojure.core/simple-symbol?, :val (), :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/ns-import :clojure.core.specs.alpha/import-list :clojure.core.specs.alpha/package-list :clojure.core.specs.alpha/package-list], :in [2 1]} {:path [:ns-clauses :use :clause], :pred #{:use}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-use :clojure.core.specs.alpha/ns-use], :in [2 0]} {:path [:ns-clauses :refer :clause], :pred #{:refer}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-refer :clojure.core.specs.alpha/ns-refer], :in [2 0]} {:path [:ns-clauses :load :clause], :pred #{:load}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-load :clojure.core.specs.alpha/ns-load], :in [2 0]} {:path [:ns-clauses :gen-class :clause], :pred #{:gen-class}, :val :import, :via [:clojure.core.specs.alpha/ns-form :clojure.core.specs.alpha/ns-gen-class :clojure.core.specs.alpha/ns-gen-class], :in [2 0]}), :spec #object[clojure.spec.alpha$regex_spec_impl$reify__2503 0x32fdec40 clojure.spec.alpha$regex_spec_impl$reify__2503@32fdec40], :value (overtone.libs.app-icon (:use [clojure.java.io] [overtone.helpers.lib :only [branch]] [overtone.helpers.system :only [get-os]]) (:import [java.awt.Toolkit])), :args (overtone.libs.app-icon (:use [clojure.java.io] [overtone.helpers.lib :only [branch]] [overtone.helpers.system :only [get-os]]) (:import [java.awt.Toolkit]))}
   :at [clojure.spec.alpha$macroexpand_check invokeStatic alpha.clj 712]}]
 :trace
 [[clojure.spec.alpha$macroexpand_check invokeStatic alpha.clj 712]
  [clojure.spec.alpha$macroexpand_check invoke alpha.clj 704]
  
  ... stacktrace elided ...
  
  [clojure.lang.Var applyTo Var.java 705]
  [clojure.main main main.java 40]]}
nREPL server started on port 39947 on host 127.0.0.1 - nrepl://127.0.0.1:39947
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.11.2
OpenJDK 64-Bit Server VM 17.0.10+7-Debian-1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

This looks like a problem in overtone so I also update that to a recent version (0.13.3177). The complaints start to multiply, I get 2 of them - lein doesn’t like the version ranges and it can’t find the scsynth binary:

$ lein repl
WARNING!!! version ranges found for:
[overtone "0.13.3177"] -> [casa.squid/jack "0.2.12"] -> [org.jaudiolibs/jnajack "1.4.0"] -> [net.java.dev.jna/jna "[5.0.0,6.0)"]
Consider using [overtone "0.13.3177" :exclusions [net.java.dev.jna/jna]].

--> Loading Overtone...
[overtone.live] [WARNING] Only :external connection type is supported, :connection-type :internal ignored. (/home/owen/.overtone/config.clj)
#error {
 :cause Failed to find SuperCollider server executable (scsynth). The file does not exist or is not executable. Places I've looked:
- `:sc-path` in /home/owen/.overtone/config.clj (nil)
- The current PATH (/opt/conda/bin:/home/owen/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/snap/bin:/home/owen/bin:/opt/conda/bin/)
- Well-known locations ("/usr/bin/scsynth")
 :data {}
 :via
 [{:type clojure.lang.Compiler$CompilerException
   :message Syntax error macroexpanding at (overtone/live.clj:7:1).
   :data #:clojure.error{:phase :execution, :line 7, :column 1, :source overtone/live.clj}
   :at [clojure.lang.Compiler load Compiler.java 7665]}
  {:type clojure.lang.ExceptionInfo
   :message Failed to find SuperCollider server executable (scsynth). The file does not exist or is not executable. Places I've looked:
- `:sc-path` in /home/owen/.overtone/config.clj (nil)
- The current PATH (/opt/conda/bin:/home/owen/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games:/snap/bin:/home/owen/bin:/opt/conda/bin/)
- Well-known locations ("/usr/bin/scsynth")
   :data {}
   :at [overtone.sc.machinery.server.connection$scsynth_path invokeStatic connection.clj 282]}]
 :trace
 [[overtone.sc.machinery.server.connection$scsynth_path invokeStatic connection.clj 282]
  [overtone.sc.machinery.server.connection$scsynth_path invoke connection.clj 276]
  
  ... stacktrace elided ...
  
  [clojure.lang.Var applyTo Var.java 705]
  [clojure.main main main.java 40]]}
nREPL server started on port 35097 on host 127.0.0.1 - nrepl://127.0.0.1:35097

This can be fixed by installing scsynth (sudo apt install supercollider) and adjusting the project.clj file as suggested then manually adding a dependency on jna (I used [net.java.dev.jna/jna "5.14.0"]).

Setting up Jack

Further runtime errors start to appear after the initial complaints are resolved.

$ lein repl
--> Loading Overtone...
[overtone.live] [WARNING] Only :external connection type is supported, :connection-type :internal ignored. (/home/owen/.overtone/config.clj)
[overtone.live] [INFO] Found SuperCollider server: /usr/bin/scsynth (PATH)
--> Booting external SuperCollider server...
[overtone.live] [INFO] Booting SuperCollider server (scsynth) with cmd: /usr/bin/scsynth -u 2238 -b 1024 -z 64 -m 262144 -d 1024 -V 0 -n 1024 -r 64 -l 64 -D 0 -o 8 -a 512 -R 0 -c 4096 -H Overtone -i 8 -w 64
[overtone.live] [INFO] Found Jack-compatible server process:
[overtone.live] [INFO]    2947     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2948     owen /usr/bin/pipewire -c filter-chain.conf
[overtone.live] [INFO]    2952     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2968     owen /usr/bin/jackdbus auto
--> Connecting to external SuperCollider server: 127.0.0.1:2238
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
[scynth] no message buffer overruns
[scynth] no message buffer overruns
[scynth] no message buffer overruns
[scynth] jackdmp 1.9.21
[scynth] Copyright 2001-2005 Paul Davis and others.
[scynth] Copyright 2004-2016 Grame.
[scynth] Copyright 2016-2022 Filipe Coelho.
[scynth] jackdmp comes with ABSOLUTELY NO WARRANTY
[scynth] This is free software, and you are welcome to redistribute it
[scynth] under certain conditions; see the file COPYING for details
[scynth] JACK server starting in realtime mode with priority 10
[scynth] self-connect-mode is "Don't restrict self connect requests"
[scynth] audio_reservation_init
[scynth] Acquire audio card Audio0
[scynth] creating alsa driver ... hw:0|hw:0|1024|2|48000|0|0|nomon|swmeter|-|32bit
[scynth] ALSA: Cannot open PCM device alsa_pcm for playback. Falling back to capture-only mode
[scynth] JackTemporaryException : now quits...
[scynth] Released audio card Audio0
[scynth] aannot initialize driver
Cannot initialize driver
[scynth] JackServer::Open failed with -1
[scynth] Failed to open server
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
#error {
 :cause Error: unable to connect to externally booted server after 50 attempts.
Make sure that you have Server.local.options.maxLogins set to greater than 1 in startup file (startup.scd).
Or if you're on Windows, make sure that the Windows defender isn't blocking the scsynth.exe

 :via
 [{:type clojure.lang.Compiler$CompilerException
   :message Syntax error macroexpanding at (overtone/live.clj:7:1).
   :data #:clojure.error{:phase :execution, :line 7, :column 1, :source overtone/live.clj}
   :at [clojure.lang.Compiler load Compiler.java 7665]}
  {:type java.lang.Exception
   :message Error: unable to connect to externally booted server after 50 attempts.
Make sure that you have Server.local.options.maxLogins set to greater than 1 in startup file (startup.scd).
Or if you're on Windows, make sure that the Windows defender isn't blocking the scsynth.exe

   :at [overtone.sc.machinery.server.connection$external_connection_runner invokeStatic connection.clj 162]}]
 :trace
 [[overtone.sc.machinery.server.connection$external_connection_runner invokeStatic connection.clj 162]
  [overtone.sc.machinery.server.connection$external_connection_runner invoke connection.clj 135]
  
  ... stacktrace elided ...
  
  [clojure.lang.Var applyTo Var.java 705]
  [clojure.main main main.java 40]]}
[scynth] Cannot connect to server socket err = No such file or directory
[scynth] Cannot connect to server request channel
nREPL server started on port 45607 on host 127.0.0.1 - nrepl://127.0.0.1:45607
[scynth] jack server is not running or cannot be started
[scynth] JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
[scynth] JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
[scynth] terminate called without an active exception
[scynth] could not initialize audio.
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.11.2
OpenJDK 64-Bit Server VM 17.0.10+7-Debian-1

Jack is an audio server that is often used by serious music types; it isn’t running on my machine so overtone doesn’t know where to send sound. I don’t want to run it. Linux sound is one of those topics that I’m happy to live in ignorance of right now. The plan is to set Pipewire to mimic a Jack server enough to play sound. I install the Pipewire Jack plugin (sudo apt install pipewire-jack) and then set up ldconf as described here. Note it is sudo ldconfig and no touch is required for Debian 12+.

I then tested this with mpv -ao=jack nothern-glade.mp3 and it seemed to be working.

Further Bitrot

$ lein repl
--> Loading Overtone...
[overtone.live] [WARNING] Only :external connection type is supported, :connection-type :internal ignored. (/home/owen/.overtone/config.clj)
[overtone.live] [INFO] Found SuperCollider server: /usr/bin/scsynth (PATH)
--> Booting external SuperCollider server...
[overtone.live] [INFO] Booting SuperCollider server (scsynth) with cmd: /usr/bin/scsynth -u 2744 -b 1024 -z 64 -m 262144 -d 1024 -V 0 -n 1024 -r 64 -l 64 -D 0 -o 8 -a 512 -R 0 -c 4096 -H Overtone -i 8 -w 64
[overtone.live] [INFO] Found Jack-compatible server process:
[overtone.live] [INFO]    2947     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2948     owen /usr/bin/pipewire -c filter-chain.conf
[overtone.live] [INFO]    2952     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2968     owen /usr/bin/jackdbus auto
--> Connecting to external SuperCollider server: 127.0.0.1:2744
[scynth] JackDriver: client name is 'Overtone-57'
[scynth] SC_AudioDriver: sample rate = 48000.000000, driver's block size = 1024
[scynth] SuperCollider 3 server ready.
--> Connection established

    _____                 __
   / __  /_  _____  _____/ /_____  ____  ___
  / / / / | / / _ \/ ___/ __/ __ \/ __ \/ _ \
 / /_/ /| |/ /  __/ /  / /_/ /_/ / / / /  __/
 \____/ |___/\___/_/   \__/\____/_/ /_/\___/

   Collaborative Programmable Music. v0.13.3177


Hello Owen, may algorithmic beauty pour forth from your fingertips today.

#error {
 :cause Unable to resolve symbol: buffer-data in this context
 :via
 [{:type clojure.lang.Compiler$CompilerException
   :message Syntax error compiling at (shadertone/tone.clj:127:19).
   :data #:clojure.error{:phase :compile-syntax-check, :line 127, :column 19, :source shadertone/tone.clj}
   :at [clojure.lang.Compiler analyze Compiler.java 6825]}
  {:type java.lang.RuntimeException
   :message Unable to resolve symbol: buffer-data in this context
   :at [clojure.lang.Util runtimeException Util.java 221]}]
 :trace
 [[clojure.lang.Util runtimeException Util.java 221]
  [clojure.lang.Compiler resolveIn Compiler.java 7431]
  
  ... stacktrace elided ...
  
  [clojure.lang.Var applyTo Var.java 705]
  [clojure.main main main.java 40]]}
nREPL server started on port 36183 on host 127.0.0.1 - nrepl://127.0.0.1:36183
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.11.2
OpenJDK 64-Bit Server VM 17.0.10+7-Debian-1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

shadertone.core=> 

Nearly there! I looked at the code and there is a function called buffer-data thaqt isn’t working. According to the changelog it has been removed. I guessed that it should be replaced by create-buffer-data (this guess proved to be wrong later on). However, for the moment, that was enough to get the REPL to load!

$ lein repl
--> Loading Overtone...
[overtone.live] [WARNING] Only :external connection type is supported, :connection-type :internal ignored. (/home/owen/.overtone/config.clj)
[overtone.live] [INFO] Found SuperCollider server: /usr/bin/scsynth (PATH)
--> Booting external SuperCollider server...
[overtone.live] [INFO] Booting SuperCollider server (scsynth) with cmd: /usr/bin/scsynth -u 20502 -b 1024 -z 64 -m 262144 -d 1024 -V 0 -n 1024 -r 64 -l 64 -D 0 -o 8 -a 512 -R 0 -c 4096 -H Overtone -i 8 -w 64
[overtone.live] [INFO] Found Jack-compatible server process:
[overtone.live] [INFO]    2947     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2948     owen /usr/bin/pipewire -c filter-chain.conf
[overtone.live] [INFO]    2952     owen /usr/bin/pipewire 
[overtone.live] [INFO]    2968     owen /usr/bin/jackdbus auto
--> Connecting to external SuperCollider server: 127.0.0.1:20502
[scynth] JackDriver: client name is 'Overtone-98'
[scynth] SC_AudioDriver: sample rate = 48000.000000, driver's block size = 1024
[scynth] SuperCollider 3 server ready.
--> Connection established

    _____                 __
   / __  /_  _____  _____/ /_____  ____  ___
  / / / / | / / _ \/ ___/ __/ __ \/ __ \/ _ \
 / /_/ /| |/ /  __/ /  / /_/ /_/ / / / /  __/
 \____/ |___/\___/_/   \__/\____/_/ /_/\___/

   Collaborative Programmable Music. v0.13.3177


Hello Owen, may this be the start of a beautiful music hacking session...

nREPL server started on port 43117 on host 127.0.0.1 - nrepl://127.0.0.1:43117
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.11.2
OpenJDK 64-Bit Server VM 17.0.10+7-Debian-1
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

shadertone.core=> 

Running the Demo

Sound

Next I loaded the namespace from examples/00demo_intro_tour.clj in Emacs. It doesn’t seem to want to work. Neither does (demo (sin-osc)) at the REPL (I checked with pavucontrol - no stream created at all, although maybe Jack output doesn’t get sent to Pulseaudio tools? I assume it would be). This is a tricky one.

I refer to ChatGPT on how to test SuperCollider. It recommends scide (the remarkably rookie friendly IDE bundled with SuperCollider) then running

s.boot;

{ SinOsc.ar(440, 0, 0.2) }.play;

which produced a boring but functional tone; so the issue is somewhere in Overtone. I try to connect to the server that Overtone starts from scide and get all sorts of errors (it seems not to be Overtone’s fault though, scide requires <=32 maximum allowable user connections and scsynth defaults to allowing 64. Weird). Eventually I settled on this code in sdide, where the default server (stored in the s variable) works and my server (being run in a terminal with scsynth -u 30000 -l 10) does not:

// Works
s.boot;
{ SinOsc.ar(440, 0, 0.2) }.play(s);
s.freeAll;

// Does not work.
// Start server with `scsynth -u 30000 -l 10`
~port = 30000;
~server = Server.remote('clojureServer', NetAddr("127.0.0.1", ~port));
~server.boot;
{ SinOsc.ar(440, 0, 0.2) }.play(~server); 
~server.freeAll;

Sleuthing eventually included running ps aux | grep scsynth and using exactly the same command line parameters for both servers which did not help at all. Reading the documentation, I discovered that I needed to run export SC_JACK_DEFAULT_OUTPUTS="system" in the terminal because obviously nobody would default to the system default without explicitly being told to. An hour of my life I will not get back, but I learned a bit about SuperCollider so that was nice, I suppose. The variable was then set in /etc/environment. After logging out and back in, the demo ((demo (sin-osc))) worked and - remarkably - I don’t think most of that struggle was related to the overtone project.

GLSL

This was the point where I took a long break and listened to some guitar-strums from the demo to reflect on my success so far. The next problem is the GLSL demo (t/start "examples/sine_dance.glsl") doesn’t work properly. It displays a blank screen. Looking at the shader code, it seems to be supposed to plot sound data from Overtone. I’ve seen some error messages about Internal and External overtone servers on the way through, so I’m expecting it to be related to that.

No error messages are visible so maybe an error message being swallowed somewhere? I load the code and jump down through the most obvious execution path looking for suspicious patterns. I quickly find this function in shadertone’s shader.clj:

(defn start-shader-display
  "Start a new shader display with the specified mode. Prefer start or
   start-fullscreen for simpler usage."
  [mode shader-filename-or-str-atom textures title true-fullscreen?
   user-data user-fn display-sync-hz]
  (let [is-filename     (not (instance? clojure.lang.Atom shader-filename-or-str-atom))
        shader-filename (if is-filename
                          shader-filename-or-str-atom)
        ;; Fix for issue 15.  Normalize the given shader-filename to the
        ;; path separators that the system will use.  If user gives path/to/shader.glsl
        ;; and windows returns this as path\to\shader.glsl from .getPath, this
        ;; change should make comparison to path\to\shader.glsl work.
        shader-filename (if (and is-filename (not (nil? shader-filename)))
                          (.getPath (File. ^String shader-filename)))
        shader-str-atom (if-not is-filename
                          shader-filename-or-str-atom
                          (atom nil))
        shader-str      (if-not is-filename
                          @shader-str-atom)]
    (when (sane-user-inputs mode shader-filename shader-str textures title true-fullscreen? user-fn)
      ;; stop the current shader
      (stop)
      ;; start the watchers
      (if is-filename
        (when-not (nil? shader-filename)
          (swap! watcher-future
                 (fn [x] (start-watcher shader-filename))))
        (add-watch shader-str-atom :shader-str-watch watch-shader-str-atom))
      ;; set a global window-state instead of creating a new one
      (reset! the-window-state default-state-values)
      ;; set user data
      (reset! shader-user-data user-data)
      ;; start the requested shader
      (.start (Thread.
               (fn [] (run-thread the-window-state
                                 mode
                                 shader-filename
                                 shader-str-atom
                                 textures
                                 title
                                 true-fullscreen?
                                 user-fn
                                 display-sync-hz)))))))

We see a new thread being started. Maybe there are exceptions on this thread? .start is a void function, so we aren’t keeping a reference to the thread in the Clojure code. We also see an add-watch - less suspicious, but maybe it is doing something that throws an error. We also have some state being stored in other atoms (reset!) which might be problematic.

I’ve been making Java profilers part of my normal practice, so I tried connecting visualvm (setting _JAVA_AWT_WM_NONREPARENTING=1 to make it work on Wayland; the Wayland protocol has left the ecosystem with a lot of design issues to solve). It didn’t reveal anything useful.

However, adding a (try ... (catch Throwable t ...)) to the run-thread function revealed something was being thrown.

#error {
 :cause Wrong number of args (1) passed to: overtone.sc.buffer/create-buffer-data
 :via
 [{:type clojure.lang.ArityException
   :message Wrong number of args (1) passed to: overtone.sc.buffer/create-buffer-data
   :at [clojure.lang.AFn throwArity AFn.java 429]}]
 :trace
 [[clojure.lang.AFn throwArity AFn.java 429]
  [clojure.lang.AFn invoke AFn.java 32]
  [shadertone.tone$tone_fftwave_fn invokeStatic tone.clj 127]
  [shadertone.tone$tone_fftwave_fn invoke tone.clj 98]
  [shadertone.tone$tone_default_fn invokeStatic tone.clj 205]
  [shadertone.tone$tone_default_fn invoke tone.clj 172]
  [shadertone.shader$draw invokeStatic shader.clj 585]
  [shadertone.shader$draw invoke shader.clj 548]
  [shadertone.shader$update_and_draw invokeStatic shader.clj 697]
  [shadertone.shader$update_and_draw invoke shader.clj 664]
  [shadertone.shader$run_thread invokeStatic shader.clj 729]
  [shadertone.shader$run_thread invoke shader.clj 722]
  [shadertone.shader$start_shader_display$fn__11628 invoke shader.clj 940]
  [clojure.lang.AFn run AFn.java 22]
  [java.lang.Thread run Thread.java 840]]}

Aha, well that is unfortunate. It would appear that create-buffer and create-buffer-data are quite different functions - one probably extracted from a buffer and the other creates a new buffer. I do some research to figure out where the buffer abstraction comes from. It seems to be stemming from scsynth. That creates a Buffer concept for clients, and then Overtone creates a Clojure record - also called Buffer - to represent this.

I replace create-buffer-data with buffer-read. The function documentation notes that buffer-read might be quite slow. Slow it may be, but the improvement to the demo is quite noticeable. Now there is a wiggly line in the shader display window, presumably wobbling sinusoidally. It wobbles with even more enthusiasm when the guitar strum sound is played. The rest of the demo seems to work too.