Sunday, 20 November 2011

Making a Really Simple Audio Player

For some reason, the first moment I decided to embark on the journey of audio programming, the ultimate milestone to me was writing a program that produced sound.  Programs I've written up to this point have all processed or produced audio through file I/O.  Even weeks after, it's ridiculous how much satisfaction I get out of a really simple little program, just because I start it up, and it starts making noises.

The audio player is about as simple as it gets.  In no way is it simple, but all the difficult stuff is done by libraries.  The file reading is largely done by portsf - the libary that came with my textbook - and PortAudio.  I would like to now shamelessly advertise PortAudio because it is a reasonably easy to grasp API, and is perfect for anyone wanting to get audio streaming.  It's portable and supports all the major ways of streaming - DirectSound, ASIO, WASAPI, and other long acronyms.

I'm not going to lie, it took me several solid days to get the library working.  I've never used a largeish library, and it took me a while to figure out all the stuff I had to do, including what any of the preprocessor stuff meant.  Also, there were numerous other large libraries I discovered had to install before I could install PortAudio with full capabilities - the ASIO and DirectX SDKs.  What made it worse was I was closely following the Audio Programming Book for guidance, in fact it was basically a walkthrough.  Except, it didn't work smoothly, so in the end I just ended up reading up on the API documentation.  However, seeing how it all works is in reality far more useful, and from all the problems, days of frustration and just wanting sound to come out, I learnt lots.

I also discovered how to add more include and library directories, how DLLs are located by Windows and how to include lots of projects into a single solution and then create dependencies, to make sure the build works properly.  All these little things I've never really come across, because I've never worked with larger, more complex projects before.  I'm probably a better person because of it.

The audio program itself is very simple - read a block of frames (samples for each channel) from the file, and then output this block straight to PortAudio's streaming buffer.  PortAudio supports two mechanisms for streaming - blocking and callback. The callback mechanism is the super fast and more efficient option, and basically involves you giving PortAudio a function (pointer) with certain parameters and return type, that it calls every time it's got stuff to process.  The real downside to this method is it must be a super fast function, so no file I/O and definitely no memory allocation.  This is because the function is called with a really high priority by the system.

The alternate method is called blocking, and works by freezing the program's execution until there's another buffer full of samples to process.  For something like audio streaming from a file, it really doesn't matter if there's a small delay, and this method is preferred for a program involving file I/O.

Here is a little snippet of code, again, I don't see much point posting an entire program of this nature, where so much is dependent on libraries.  Maybe for future projects I'll get some slightly more helpful and complete resources together, but this code should just act as an insight into the internals.

void play(const char *filename, double start, double dur)
{
 int sfd, frames_read;
 unsigned long first_frame, end_frame, total_frames, total_read;
 PSF_PROPS props;
 float buffer[BUFFER_SIZE * 2]; 
 
 sfd = psf_sndOpen(filename, &props, 0);
 if(sfd < 0)
 {
  printf("Note: Soundfile <%s> couldn't be opened.\n", filename);
  return;
 }
 else if(props.chans > 2)
 {
  printf("Note: Only mono and stereo formats supported.\n");
  return;
 }

 /* find position of first and last frame to be read */
 first_frame = (unsigned long) (start * props.srate + 0.5);
 if(dur <= 0.0) /* default is to read entire file */
  end_frame = psf_sndSize(sfd);
 else end_frame = (unsigned long) (first_frame + dur * props.srate + 0.5);
 /* seek position in file */
 psf_sndSeek(sfd, first_frame, PSF_SEEK_SET);
 
 total_frames = end_frame - first_frame;
 frames_read = psf_sndReadFloatFrames(sfd, buffer, BUFFER_SIZE);
 for(total_read = frames_read; total_read < total_frames; total_read += frames_read)
 {
  if(frames_read != BUFFER_SIZE)
  {
   printf("Error: couldn't read input file.\n");
   break;
  }
  /* read frames from file and output to the DAC (digital audio converter) */
  if(props.chans == 2) // stereo
   outBlockInterleaved(buffer, frames_read);
  else
   outBlockMono(buffer, frames_read);
  frames_read = psf_sndReadFloatFrames(sfd, buffer, BUFFER_SIZE);
 }
 psf_sndClose(sfd);
}
In fact for this project, I also used a library from the Audio Programming book, called TinyAudioLib, which did most of the PortAudio stuff for me.  At least in theory.  In reality, I ended up rewriting lots of it to get it to work for my setup.  Anyway, in my next PortAudio project I just used the API directly.

Although it was a pain to get working, and even after compiling took many hours before sound was produced, thank God (or maybe just programmers) for libraries.  After the blood, sweat, and tears (I was crying inside), the satisfaction of running the program and it working was massive.  It can only play WAVE files and other lossless formats, so I won't be rivalling iTunes yet, but I'll be looking to return to the subject of audio players after I've got some more synth stuff under my belt.

The Audio Player in action... pictorially
A little note on the different sound interfaces: ASIO is a very low latency (delay) API made by Steinberg, that bypasses Windows sound mapper.  Most cheapy sound cards e.g. mine, won't have ASIO drivers, but this can be remedied with ASIO4ALL.  DirectSound is DirectX's sound interface, WASAPI (Windows Audio Session API) is Vista's sound interface, which is why PortAudio has chosen this as the default to use for the audio player.  Also note that S/PDIF out appears, if I were to choose this is an output, I wouldn't hear anything from the speakers.  The S/PDIF output is used to connect audio devices e.g. in a home theater system.

No comments:

Post a Comment