Monday, July 11, 2011

Loading a wav File with SndfileHandle

Assuming you have libsndfile installed and properly building with your project (check here), it is time to read some audio from disk into an array. I think it's most convenient to use the C++ wrapper class 'SndfileHandle' defined in the sndfile.hh include as shown below:


#include "testApp.h"
#include <sndfile.hh>
#include <stdio.h>



void testApp::setup(){

.
.
.



const char* fn = "/Users/.../test.wav";
SndfileHandle myf = SndfileHandle(fn);
printf ("Opened file '%s'\n", fn) ;
printf ("    Sample rate : %d\n", myf.samplerate ()) ;
printf ("    Channels    : %d\n", myf.channels ()) ;
printf ("    Error       : %s\n", myf.strError());
printf ("    Frames      : %d\n"int(myf.frames())); 
puts("");



 static float buffer [1024]; 
 myf.read (buffer, 1024);
.
.
.



Aside - The 'frame' terminology initially confused me, as I tend to think of 'frames' as being chunks of multiple samples across the time axis (as in STFT analysis). In the libsndfile world however, a 'frame' is all of the values/samples for each channel at a given time instant. For example, if you have a stereo (2-channel) file, a frame will consist of two values or samples. If you have a mono (1-channel) file, a frame will consist of only one value or sample.

It's fairly non-obvious what is actually stored in our float buffer after the call to the SndfileHandle read() method. if you were dealing with MATLAB, you'd have a 2-dimensional array with each column holding the samples for either the right or left channel after a similar call to wavread(). You might even think it would make more sense for the read() method to return the samples corresponding to each channel separately. However, the read() method implemented in libsndfile fills up a 1-dimensional array of values.

For a mono (1-channel) audio file, the interpretation is simple. The buffer is filled with the audio samples just as you might expect:


buffer = [sample1, sample2, sample3, sample4, etc]


For a stereo (2-channel) audio file, the interpretation is less obvious. The buffer is filled with the audio samples in an interleaved fashion:


buffer = [left_sample1, right_sample1, left_sample2, right_sample2, etc]


This pattern continues for n-channel multichannel files.

If you want to inspect the contents of 'buffer' by hand, place a breakpoint after the read() function (double click in the wide column to the left of the text. A little blue tab/flag should appear). Next, go to Build -> Build and Debug, and wait until your program halts at the breakpoint. Now, go to Run -> Show -> Expressions. Enter the following in the the text field at the bottom of the dialogue:


*buffer @ 1024


Hopefully any newcomers to libsndfile and xcode will find this useful!

5 comments:

  1. hi jack,

    first, many thanks for these posts. they have been extremely helpful to me lately.

    second, have you had any success streaming audio out of openFrameworks from a sound file loaded with libsndfile? I'm able to load everything fine, but I can't seem to come up with a good method for buffering and then streaming the audio data out... do you have any suggestions?

    thanks in advance,
    conner

    ReplyDelete
  2. Hey Conner,

    No problem, glad to have been of help.

    This post should help you out:
    http://ofdsp.blogspot.com/2011/07/using-ofsoundstream-to-play-arbitrary.html

    You basically just need to use the ofSoundStreamSetup() function in OF to create the output stream.

    Let me know if this answers your question!

    ReplyDelete
  3. hey jack, thanks for the reply. one more question...

    i've got a very simple setup in oF right now. i'm reading a portion of the soundfile into the buffer and have the audioOut setup just like your posts show, but i can't seem to devise a way to advance forward in the soundfile and get new samples into my buffer. i'm guessing the myf.seek() might do the trick, but i can't figure out exactly how to use it.

    i'm totally new to DSP at this level, so please excuse any blatant ignorance. i feel like once i have this basic key i'll be able to do all kinds of things.

    i've copied a little portion of my code to give you an idea of what i'm thinking...

    again, many thanks!
    c.


    void testApp::audioOut(float * output, int bufferSize, int nChannels){

    getSamps();
    for (int i = 0; i < bufferSize; i++){
    lAudio[i] = output[i*nChannels ] = buffer[i];
    rAudio[i] = output[i*nChannels + 1] = buffer[i];
    }
    }

    void testApp::getSamps(){
    myf.seek(0, SEEK_SET);
    myf.read(buffer, 1024);
    }

    ReplyDelete
  4. Hey Conner,

    I realize that my post is a little confusing here. I'm going to release all of my source code soon, but in the meantime, this may help you.

    First off, I haven't actually used the seek() function in my code, so I'm not sure how/if it works. Intuitively, your approach seems OK, but this line:

    myf.seek(0, SEEK_SET);

    Seems weird to me. It looks to me like the first argument to the seek() method (defined in sniffle.hh) is supposed to be the number of 'frames' you want to seek forward. Seeking forward 0 frames doesn't actually change anything as far as I can tell (I could be completely wrong). Also, it seems like you would want to use SEEK_CUR as the 'whence' parameter.

    If you want to simplify things a bit and just get the audio properly playing, I would suggest loading the entire audio file into an array/buffer or vector. This way you don't have to worry about how seek() and read() are working in libsndfile.

    Here's some skeleton code to load an entire audio file into a STL vector (Apologies for any typos. As I said, the full code will be available soon. I would advise against copy/paste here :) ):

    loadAudio(const char* path) {

    std::vector samples;

    mySndfileHandle = SndfileHandle(path);

    int channels = mySndfileHandle.channels();
    int frames = mySndfileHandle.frames();

    // We'll load in 1024 samples at a time, and stuff them into a buffer
    // We need to ensure that the buffer is long enough to hold all of the
    // Channel data
    uint bufferSize = 1024 * channels;

    // initialize our read buffer
    readbuffer = new float[bufferSize];

    int readcount;
    int i;
    int j;
    int readpointer = 0;

    // DSPAudioBuffer converts all multichannel files to mono by averaging the channels
    // This is probably not an optimal way to convert to mono
    float monoAverage;


    while (readcount = mySndfileHandle.readf(readbuffer, 1024)) {
    readpointer = 0;
    for (i = 0; i < readcount; i++) {
    // for each frame...
    monoAverage = 0;
    for(j = 0; j < channels; j++) {
    monoAverage += readbuffer[readpointer + j];
    }
    monoAverage /= channels;
    readpointer += channels;
    // add the averaged sample to our vector of samples
    samples.push_back(monoAverage);
    }
    }
    }



    In your audioOut method, you now feed the output buffer samples from your vector. Something like:

    lAudio[i] = output[i*nChannels ] = samples[playback_head]
    rAudio[i] = output[i*nChannels + 1] = samples[playback_head];
    playback_head++;


    Make sense?

    ReplyDelete
  5. Make sure you delete the readbuffer too!

    ReplyDelete