2010-02-25
Vorbis Player implemented in Pure
After writing the preforking echo server example in Pure I wanted to try something a bit more complex. I've written Ogg players in various languages before so I decided to do one in Pure to compare. Here's some of the ones I've written in the past:
- oggplayer
- plogg
- Io Language Addons
- Factor Ogg Player
- An unreleased Haskell Vorbis player
I wrapped the libogg and libvorbis libraries using the Pure FFI. A program 'pure-gen' is included with Pure that will generate FFI wrappers given a C include file. I ran this against libogg and libvorbis. The generated Pure wrappers look like:
extern int ogg_page_version(ogg_page*);
extern int ogg_page_continued(ogg_page*);
extern int ogg_page_bos(ogg_page*);
As you can see Pure FFI definitions look very much like the C definitions.
I decided to model an Ogg file as a list of physical pages. This is esentially a list containing elements of the libogg ogg_page object. I have a Pure function that will take this list of pages and return a list of packets for each logical bitstream in the file. For vorbis playback I take this list of packets for the vorbis bitstream and produce a list of decoded PCM data.
Pure is a strict language by default but provides an '&' special form to perform lazy evaluation. I use this to make sure all the returned lists mentioned above are lazy. Playing the lazy list of decoded PCM data never loads the entire file in memory. The file is read, converted into packets and decoded as each element of the PCM data list is requested.
For audio playback I use OpenAL. I queue the PCM data to an OpenAL source up to a limit of 50 queued items, removing them as they get processed and adding new ones when room becomes available.
Here's what some of the API to process the Ogg files looks like in Pure. First load the player code. This will also load the Ogg, Vorbis and OpenAL wrappers:
$ pure -i player.pure
From the Pure REPL, play a vorbis file:
> play_vorbis (sources!0) "test.ogg";
Load an Ogg file getting a lazy list of all pages in the file:
> using namespace ogg;
> let p = file_pages "test.ogg";
> p;
ogg_page (#<pointer 0x9e61cc8>,[...]):#<thunk 0xb7259b38>
Get a list of all packets and the serial number of the first logical bitstream in the list of pages:
> let (stream1,serialno) = packets p;
> serialno;
1426627898
> stream1;
ogg_packet (#<pointer 0x9dd98f0>,[...]):#<thunk 0xb725bce0>
Get the next logical bitstream:
> let p2 = filter (\x->ogg_page_serialno x ~= serialno) p;
> p2;
#<thunk 0xb725dc30>
> let (stream2,serialno2) = packets p2;
> serialno2;
629367739
> stream2;
ogg_packet (#<pointer 0x9cfcf08>,[...]):#<thunk 0xb725c988>
Note that all these results are lazily evaluated so the entire file is not loaded into memory. If we look at the original list of pages you'll see what has been read so far (two pages):
> p;
ogg_page (...):ogg_page (...):#<thunk 0xb725dd98>
Get all the streams in the file:
> let s = all_streams p;
Get all the vorbis streams:
> let v = vorbis_streams s;
Get the first vorbis stream. I ignore the serial number here (this is what the _ does):
> let (v1, _) = v!0;
Decode the vorbis header packets and intialize a vorbis decoder for the first vorbis stream:
> let decoder = vorbis_header v1;
Get a lazy list of all the decoded PCM data from the vorbis stream:
> let pcm = pcm_data decoder;
The mechanics of getting the decoding floating point data from libvorbis into Pure and then back to the OpenAL C API is made easier due to it being possible to easily convert to and from raw C pointers and Pure matrix objects.
For example, the data returned from the vorbis call to decode the data is an array of pointers pointing to an array of floats. To convert this to a Pure matrix with dimensions (c,n) where 'c' is the number of channels and 'n' is the number of samples:
vorbis_synthesis_read dsp samples $$ matrix m
when
f0 = get_pointer data;
floats = [get_pointer (f0+(x*4))|x=0..(channels-1)];
m = map (float_matrix samples) floats;
end;
This code uses a list comprehension to return a list of 'pointer to array of floats'. One pointer for each channel in the audio data. It then maps over this list producing a new list containing a matrix of float's. This is list of matrices each of which contains the audio data for a channel. The 'matrix m' call converts this to a single matrix of dimensions (c,n).
OpenAL expects the data as a pointer to a contiguous matrix of floats with dimensions (n,c) so when writing to OpenAL the matrix needs to be transposed:
play_pcm source rate pcm@(x:xs) =
....
queue_sample source rate (transpose x) $$
play source $$
play_pcm source rate xs;
This transposed matrix is later converted from floats to 16 bit integers and 'packed' so it is contiguous in memory, and a pointer to the raw memory passed to OpenAL:
buffer16 s = pack (map clamp s);
...
data = buffer16 ...pcmdata...;
alBufferData ... (cooked (short_pointer NULL data)) ((#data) * (sizeof sshort_t)) ...;
The 'cooked' call means the C memory has 'free' called on it when nothing in Pure is holding onto it anymore.
One downside with lazy lists is it can be easy to accidently read the entire file and decode everything before playing. This will cause issues with large Ogg files. I managed to avoid that and files play in constant memory. An iteratee style approach might be interesting to try as an alternative to lazy lists. I'd like to try decoding Theora files and doing a/v sync in Pure at some point as well.
I haven't used Pure much but it seemed to be fairly simple to put this together. I'd be interested in feedback on the approach (using lazy lists) and Pure style issues in the code.
The code for this is at http://github.com/doublec/pure-ogg-player.