{
"$type": "site.standard.document",
"content": "---\ntitle: \"Sound file I/O in Extempore with libsndfile\"\ndescription: \"A crash course on reading, processing and writing audio files in Extempore\n using the libsndfile C library bindings.\"\ntags:\n - extempore\n---\n\n[libsndfile](https://github.com/erikd/libsndfile/) is \"a C library for reading\nand writing files containing sampled audio data\", and it's pretty great. Here's\na quick crash-course on using\n[Extempore's libsndfile bindings](https://github.com/digego/extempore/blob/master/libs/external/sndfile.xtm)\nto read, process and write audio data files.\n\n## Loading the xtlang wrappers for the sndfile library functions\n\nOnce you've got Extempore up and running, the first thing to do is to load the\nlibsndfile wrapper functions:\n\n```xtlang\n(sys:load \"libs/external/sndfile.xtm\")\n```\n\n`libs/external/sndlib.xtm` contains `bind-lib` definitions for all[^pm-all] the\nfunctions in the libsndfile library (compare that file to the\n[C header](https://github.com/erikd/libsndfile/blob/master/src/sndfile.h.in) to\nsee for yourself).\n\n[^pm-all]: well, _pretty much_ all\n\nIn this blog post I'm mostly going to write \"thin\" xtlang code---calling the C\nfunctions directly wherever possible, and using the same basic types as the\nunderlying C library. You could easily write xtlang helper functions to make all\nthis stuff nicer, and `libs/external/sndfile.xtm` actually has a bunch of these\nas well (down the bottom of that file). However, I hope this bare-bones approach\nis helpful for understanding what's going on (and how you might use other C\nshared libs if you wanted to).\n\n## Reading the audio file data into memory\n\nIn a fairly common API design pattern, to get info about a sound file (length,\nchannels, sample rate, format, etc.) we need to:\n\n1. use `sf_open` to give us a pointer to the data structure which libsndfile\n uses to represent the audio file (a `SNDFILE*`)\n\n2. pass a pointer to another data structure (a `SF_INFO*`) which `sf_open` will\n populate with info about the file\n\n`sf_open` doesn't read the audio data into memory (where we can do stuff with\nit) though---because first we need to figure out how big a buffer to allocate\nfor the data---and to do that, we need to know how many frames there are in the\naudio file, and how many channels per frame.\n\nLooking at the documentation (i.e. the comment above the function declaration in\nthe C header file) for `SF_INFO` we see that the first (tuple index `0`) and\nthird (tuple index `2`) fields are going to be useful\n\n```c\nstruct SF_INFO\n{ sf_count_t frames ; /* Used to be called samples. Changed to avoid confusion. */\n int samplerate ;\n int channels ;\n int format ;\n int sections ;\n int seekable ;\n} ;\n```\n\nUsing all this info, then, we can make a simple xtlang function to return the\nnumber of frames\n\n```xtlang\n(bind-func get_number_of_frames\n (lambda (filename)\n (let ((info:SF_INFO* (salloc))\n ;; call sf_info to populate info with data about the file\n (sfile (sf_open filename SFM_READ info)))\n (sf_close sfile)\n ;; return the number of frames\n (tref info 0))))\n\n;; test it out using a wave file from the Extempore assets directory\n;; (it should return the number 288366)\n(get_number_of_frames \"assets/samples/piano/60.wav\")\n```\n\n:::tip\n\nMost of the code in this post doesn't check for e.g. bad filenames or other\npotential errors, so if that's a thing which might happen in your situation then\nyou'll need to check that `sf_open` doesn't return `null`.\n\n:::\n\nWe can do the exact same thing to get the number of channels per frame (just\nreturning a different element of the `info` struct):\n\n```xtlang\n(bind-func get_number_of_channels\n (lambda (filename)\n (let ((info:SF_INFO* (salloc))\n (sfile (sf_open filename SFM_READ info)))\n (sf_close sfile)\n ;; return the number of channels\n (tref info 2))))\n\n;; returns 2 (it's a stereo file)\n(get_number_of_channels \"assets/samples/piano/60.wav\")\n```\n\nFinally, we can calculate how many samples (num frames × num channels) we'll\nneed in our \"audio data\" buffer. We can then use `sf_read` to read the audio\ndata from the file into our buffer, converting it to e.g. `float` (or whatever\nthe type of `SAMPLE` is) as we go (libsndfile can read audio files in a bunch of\ndifferent formats, but for working with it in Extempore we just want floating\npoint values).\n\nFirst, set up a DSP callback---just playing white noise so that we can check\nthat it's working.\n\n```xtlang\n(bind-func dsp:DSP\n (lambda (in time chan dat)\n (random .1)))\n\n(dsp:set! dsp)\n```\n\nNow, we add a bunch of sndfile-related stuff to the top-level `dsp` closure\nenvironment (the outer `let`) to\n\n1. get the number of frames/channels from the file\n2. allocate a `SAMPLE`[^sample] buffer big enough to fit all the audio data\n3. read the audio file data into this buffer\n\nFinally, in the inner `lambda` we have a super-naive playback loop (look at the\nway we're incrementing `i`---this will only work if the number of output\nchannels matches the number of channels in the audio file).\n\n[^sample]: `SAMPLE` is an alias for `float` by default\n\n```xtlang\n(bind-func dsp:DSP 10000000 ;; allocate plenty of memory for our DSP closure\n\n (let ((filename \"assets/samples/piano/60.wav\")\n (nframes (get_number_of_frames filename))\n (nchan (convert (get_number_of_channels filename)))\n (nsamp (* nframes nchan))\n (info:SF_INFO* (alloc))\n (sfile (sf_open filename SFM_READ info)) ;; SFM_READ = open the audio file in \"read-only\" mode\n ;; here's the pointer to the audio data\n (data:SAMPLE* (alloc nsamp))\n (i 0))\n\n (println \"read\" (sf_read sfile data nsamp) \"frames\")\n (sf_close sfile)\n\n (lambda (in time chan dat)\n ;; a super-naive \"playback\" loop\n (set! i (% (+ i 1) nsamp))\n (* .2 (pref data i)))))\n```\n\nIf it's all worked, you should hear a piano playing repeated (legato) notes on\nmiddle C.\n\nOf course, we could have simplified this by just calling `sf_open` and\npopulating the `info` with data once at the top of an xtlang function, then\ndoing stuff based on that information and finally `sf_close`ing the file at the\nend. That's left as an exercise for the reader 😉\n\n:::tip\n\nOne thing worth noting with all this is that calling a C lib from Extempore\ndoesn't obviate the need to understand how the C library works, e.g. we still\nneed to match every call to `sf_open` with a call to `sf_close` as stated in the\nlibsndfile docs.\n\n:::\n\n## Writing data in memory to an audio file\n\nThere's one more thing we want to do with our libsndfile library: write a bunch\nof audio data (which we've gloriously munged in Extempore) and write it back to\nan audio file.\n\nHere's a simple munging function which will replace the first `22050` samples\nwith white noise, then leave the next `22050` untouched, then replace the next\n`22050` with more white noise, and so on. I'm sure you can come up with\nsomething more (sonically) interesting; this is just an easy one to test (by\near) if it's working.\n\n```xtlang\n(bind-func munge_audio_data\n (lambda (data:SAMPLE* nsamp)\n (doloop (i nsamp)\n (if (< (modulo i 44100) 22050)\n (pset! data i (random .1))))))\n```\n\nThe final thing to do is to create _another_ `SNDFILE` object (this time opened\nin `SFM_WRITE` mode) where we'll write the audio data. We'll make some small\nadditions to our `dsp` closure:\n\n```xtlang\n(bind-func dsp:DSP 10000000 ;; allocate plenty of memory for our DSP closure\n\n (let ((filename \"assets/samples/piano/60.wav\")\n (nframes (get_number_of_frames filename))\n (nchan (convert (get_number_of_channels filename)))\n (nsamp (* nframes nchan))\n (info:SF_INFO* (alloc))\n (srcfile (sf_open filename SFM_READ info))\n (dstfile (sf_open \"assets/samples/piano/60-munged.wav\" SFM_WRITE info))\n ;; here's the pointer to the audio data\n (data:SAMPLE* (alloc nsamp))\n (i 0))\n\n (println \"read\" (sf_read srcfile data nsamp) \"frames\")\n (sf_close srcfile)\n\n ;; munge the audio data\n (munge_audio_data data nsamp)\n\n (println \"wrote\" (sf_write dstfile data nsamp) \"frames\")\n (sf_close dstfile)\n\n (lambda (in time chan dat)\n ;; a super-naive \"playback\" loop\n (set! i (% (+ i 1) nsamp))\n (* .2 (pref data i)))))\n```\n\nIf you re-evaluate _that_ `dsp` closure, you should (a) hear the munged audio\nand (b) it should have been written to the \"assets/samples/piano/60-munged.wav\"\nfile. Note that we re-used the `info` data structure (which was populated with\nthe info from `srcfile`) in the `dstfile` call---this is deliberate, and makes\nsure that we use the same file format for the output file as for the input file.\nIf you want to write it in some _other_ format, then look at the libsndfile\ndocs---there are lots of options.\n\n## Wrapping up\n\nThere's lots more to explore, but I'll leave it here for now. If you've got any\ncomments, then get in touch on the\n[Extempore mailing list](mailto:extemporelang@googlegroups.com).\n\n[c-xtlang-interop]: There's more detail on how this works in the\n[Extempore docs](https://extemporelang.github.io/docs/reference/c-xtlang-interop/)\nif you're interested.\n",
"createdAt": "2026-05-13T23:14:57.601Z",
"description": "A crash course on reading, processing and writing audio files in Extempore using the libsndfile C library bindings.",
"path": "/blog/2019/10/15/sound-file-io-in-extempore-with-libsndfile",
"publishedAt": "2019-10-15T00:00:00.000Z",
"site": "at://did:plc:tevykrhi4kibtsipzci76d76/site.standard.publication/self",
"tags": [
"extempore"
],
"textContent": "A crash course on reading, processing and writing audio files in Extempore using the libsndfile C library bindings.",
"title": "Sound file I/O in Extempore with libsndfile"
}