Playing An OGG File … With Clojure

Background

Trying to play sound files in Java has been unexpectedly challenging. For starters, sound is complex and media formats are also involved. On top of that Java has a couple of ways to play sound many of which are a bit verbose.

I thought that I could use JavaFX. The API seemed pretty good but it has a key flaw - the design of JavaFX uses a hardcoded version of libav, so every time Debian upgrades libav sound on my projects stops working; even if I use the system JavaFX library. It is actually worse than that because sometimes the system library is too new for JavaFX so there isn’t even a released version compatible with Debian’s libav version. That was for mp3s, today I want to play an OGG file and it is time to try something new and hopefully more reliable.

ChatGPT reccommended tritonus but that seems to be a red herring. I had a great deal of difficulty figuring out the dependencies and it wasn’t useful compared to (JavaZoom’s?) vorbisspi. vorbisspi seemed to be necessary when I was trying to set things up, and has appropriate dependencies on tritonus and it’s jorbis plugin.

Prerequisites

System & Libraries

OpenJDK v. 21 on Debian (GNU/Linux trixie/sid).

I installed the tritonus system libraries as an investigative exercise to help me understand what artefacts I would be looking for. In the end I decided to re-download these from maven because adding system libraries has not been a great experience in the past.

$ sudo apt install libtritonus-java libvorbisspi-java
Installing:
  libtritonus-java

Installing dependencies:
  libtritonus-jni
...
$ dpkg -L libtritonus-java
...
/usr/share/java/tritonus_vorbis.jar
...

Project Dependencies

I continue to recommend leiningen for new Clojure projects. Added in project.clj.

  ;; Added in defproject / :dependencies
  ;; This adds Vorbis support to the JVM transparently.
  [com.googlecode.soundlibs/vorbisspi "1.0.3.3"]

A sound file

Today I am using a file from Freesound. Load Drum 1.ogg by hello_flowers – https://freesound.org/s/33823/ – which I renamed to load-drum-1.ogg. I put it into the resources folder of my project. The project structure looks something like this:

$ tree
.
├── project.clj
├── README.md
├── resources
│   └── load-drum-1.ogg
└── src
    └── project
        └── core.clj

Coding

Added to core.clj.

(require '[clojure.java.io :as io])
(import '[javax.sound.sampled AudioSystem AudioFormat AudioFormat$Encoding])

(defn ->pcm-signed-format
  [vorbis-audio-stream]
  (let [format
        (.getFormat vorbis-audio-stream)]
    (AudioFormat. AudioFormat$Encoding/PCM_SIGNED
                  (.getSampleRate format) 16 (.getChannels format) (* 2 (.getChannels format)) (.getSampleRate format) false)))

(defn play-ogg
  [ogg-resource-name]
  (let [audio-stream (AudioSystem/getAudioInputStream (io/resource ogg-resource-name))
        rs (AudioSystem/getAudioInputStream (->pcm-signed-format audio-stream) audio-stream)]
    (doto (AudioSystem/getClip)
      (.open rs)
      (.start))))

(play-ogg "load-drum-1.ogg")

The first effort was a lot simpler. But there were two major errors that needed to be resolved that ended up making the code heavier.

Error 1

The first indicated that the dependencies were set up wrongly and that was when I switched from a direct dependency on tritonus to vorbisspi.

Execution error (UnsupportedAudioFileException) at javax.sound.sampled.AudioSystem/getAudioInputStream (AudioSystem.java:1038).
URL of unsupported format

Error 2

The second error I’m a bit less certain about what is happening. The libraries involved are opening the OGG file without much trouble but as an encoded stream. Apparently (ChatGPT again) this suggests something is going wrong, possibly linked to tritonus-jorbis. The VorbisSPI should be decoding the audio stream somewhere in the plumbing. But it didn’t, so the format had to be adjusted manually.

Execution error (LineUnavailableException) at com.sun.media.sound.DirectAudioDevice$DirectDL/implOpen (DirectAudioDevice.java:484).
line with format VORBISENC 44100.0 Hz, unknown bits per sample, mono, 1 bytes/frame, 7500.0 frames/second not supported.

I had a poke at this error without making real progress. I tried looking at the project dependencies and noted that there was no tritonus-jorbis artefact (which is probably the problem). But I couldn’t find a tritonus-jorbis to add (I tried tritonus-all from the Google Sound repo, but that didn’t fix anything).

$ lein deps :tree
[com.googlecode.soundlibs/vorbisspi "1.0.3.3"]
  [com.googlecode.soundlibs/jorbis "0.0.17.4"]
  [com.googlecode.soundlibs/tritonus-share "0.3.7.4"]

Since I had the libraries, I tried switching to using the local .jar files. That led to … a libogg segfault!

  ;; project.clj
  :resource-paths ["resources"
                  "/usr/share/java/vorbisspi.jar"

                  "/usr/share/java/tritonus_alsa.jar"
                  "/usr/share/java/tritonus_aos.jar"
                  "/usr/share/java/tritonus_cdda.jar"
                  "/usr/share/java/tritonus_core.jar"
                  "/usr/share/java/tritonus_dsp.jar"
                  "/usr/share/java/tritonus_esd.jar"
                  "/usr/share/java/tritonus_fluidsynth.jar"
                  "/usr/share/java/tritonus_gsm.jar"
                  "/usr/share/java/tritonus_javasequencer.jar"
                  "/usr/share/java/tritonus_jorbis.jar"
                  "/usr/share/java/tritonus_mp3.jar"
                  "/usr/share/java/tritonus_pvorbis.jar"
                  "/usr/share/java/tritonus_remaining.jar"
                  "/usr/share/java/tritonus_share.jar"
                  "/usr/share/java/tritonus_src.jar"
                  "/usr/share/java/tritonus_vorbis.jar"]
$ lein repl
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0x00007fee53a110dd, pid=26336, tid=26341
#
# JRE version: OpenJDK Runtime Environment (21.0.5+11) (build 21.0.5+11-Debian-1)
# Java VM: OpenJDK 64-Bit Server VM (21.0.5+11-Debian-1, mixed mode, emulated-client, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# C  [libogg.so.0+0x30dd]  ogg_sync_init+0xd
#

Looks like libtritonus-java is doing something wrong, a gentle probe of the source code (apt source libogg libtritonus-jni) makes me think that tritonus is passing an invalid handle into libogg. Looks harder to debug than just living with manually converting the audio format.

This isn’t a very satisfying conclusion; we have an awkward workaround to a broken dependency and it looks like there might be a bug in Debian’s libtritonus-java package. Or I might be using it wrong. Still, hopefully this approach is more robust than using JavaFX!