A 30 SLOC CLI native code sound file player
Here is a short, approximatively 30 SLOC OCaml code to play a soundfile through Jack or Pipewire. I like tidy, simple, straight to the point code and languages, minimizing the number of SLOC to achieve a given task, but still compiled to native code with a serious, highly bug-screening compilation process. Ocaml ideally fits the bill here, but a garbage collector based language does not makes things easy regarding real-time constraints. We use ocaml-bjack
, which buffers sound I/O and we give half a second of buffering to shield us from the GC runtime.
First lets open the soundfile using ocaml-libsndfile
and query the number of channels and samplerate of the file:
let () = let soundfile = Sys.argv.(1) in let sf = Sndfile.openfile soundfile in let nchan = Sndfile.channels sf in let sfreq = Sndfile.samplerate sf in Printf.printf "Opened sound file %s.\n%!" soundfile ;
Next we start our Jack client and indicate a one second depth buffer (sfreq*nchan)
for I/O of which we'll use half:
Bjack.set_conversion_function Bjack.Best_quality ; let bjk = Bjack.open_t ~rate:sfreq ~bits_per_sample:16 ~input_channels:0 ~output_channels:nchan ~flags:[ Bjack.Output ] ~client_name:"sfplay" ~ringbuffer_size:(sfreq*nchan) () in Printf.printf "Opened JACK audio device.\n%!" ;
Now define a buffer for passing data from libsndfile to jack:
let buflen = 1024 in (* samples per channel *) let inlen = buflen * nchan in let interleaved = Array.make inlen 0. in
More interestening, we receive data from libsndfile
in a normalized to ]-1.0; +1.0] stream of floating point values, while ocaml-bjack
accept samples in a stream of 16bit integers encoded in a string. The function short_from_float
takes a float, amplify it by 32768 and return a pair of values bounded by [0; 255], ready to be encoded as characters in a string. Next the function paircat
takes as input an Array of such pairs and build a string representing a flow of samples using a fold.
let short_from_float input = let inint = int_of_float (32768. *. input) in let lower = inint land 0xff in let upper = (inint land 0xff00) lsr 8 in (upper, lower) in let paircat s cp = s ^ String.make 1 (Char.chr (snd cp)) ^ String.make 1 (Char.chr (fst cp)) in
Note that the caret means string concatenation and Char.chr
return a character out of its integer index in the 8bit ASCII table, while fst and snd return the first and second in a pair, respectively.
We then define a pause time half the time to sink a buflen
buffer:
let pausetime = (float_of_int buflen) /. (2. *. float_of_int sfreq) in
We start the processing loop until an indication there is nothing more to read from the soundfile:
let read = ref 1 in while !read > 0 do if Bjack.get_bytes_stored bjk < sfreq*nchan/2 then (
Here is the main processing of this program, we read the soundfile buflen*nchan
at a time, use Array.map
and fold_left
to convert our floats to a string, and write this string in the Jack ringbuffer:
read := Sndfile.read sf interleaved ; let pairs = Array.map (fun x -> short_from_float x) interleaved in let shorts = Array.fold_left paircat "" pairs in ignore(Bjack.write bjk shorts)
When enough data is in the Jack ringbuffer, we simply idle doing nothing, for a time long enough to empty a bit over one buflen*nchan
in the Jack ringbuffer. Note that using map and fold here imply allocating and collecting arrays of memory inside the tight processing loop, this is not an ideal way of writing a soft real-time program. It can be seen by looking at the CPU usage, something suboptimal is going on (the reccuring, high frequency memory allocation of relatively large arrays). This is a short tutorial only, but shows that OCaml, even used far from its design goal (the Coq theorem prover implementation language) is still able to cope with basics multimedia requirements. It happens, computers history shows, that OCaml has ended as being a broad, expressive general purpose programming language.
) else Unix.sleepf (3.*.pausetime) ; Printf.printf "Bjack.get_bytes_stored=%d\r%!" (Bjack.get_bytes_stored bjk) done ; Bjack.close bjk
That's it, a little starter dish in OCaml with more than a "Hello world" or a fibonacci, for peoples having other simpler language as a background.