Wednesday, January 18, 2012

Modifying software for fun and music, part 1

Author's Note: This is the first post in an experiment wherein I document my foray into deciphering and modifying a particular piece of open source software as I do it. My interest lies in whether the resulting posts a) are digestible, and b) provide additional insight into the "how" of the process. As such, these will undergo only cursory editing before being posted. Expect typos!

Update 9/14/2013: The second part of this post is finally available here!

A few months ago, I purchased one of these newfangled Internet-enabled televisions so I could stream movies from Netflix without having to plug my laptop into the TV every time. Since I didn't spring for a model with built-in wireless, I subsequently bought a nifty device from some big-name manufacturer, which lets me plug in an Ethernet device and acts as a wireless client on its behalf. This device happens to also let me stream music from said manufacturer's music application to my stereo via an 1/8" audio plug on the device. Pretty nifty stuff.

My main home computer is a desktop running Linux, and I don't want to boot my laptop every time I want to play some music (the whole point of the TV upgrade, right?). So I want an easy way to stream music from Linux to said device. Well, if you're familiar with audio under Linux, there's something like six different subsystems you can run: OSS, ESD, ALSA, Pulse, et cetera. Someone made a nice module for the Pulse audio subsystem that lets these devices act like virtual sound cards, which is great if you're running Pulse. But after an entire afternoon spent breaking and fixing my sound in an effort to shift from ALSA to Pulse, I decided this wasn't the solution for me.

Fortunately, someone else had the same idea and created a utility called raop_play a while back. This is a command line client that takes the IP address of the device we want to stream to and the filename of the audio file (e.g., MP3) to play. After a quick download and compile (okay, a moderately quick compile after installing a few dependencies and subverting build errors), it worked right out of the box. But it lacked a couple of things I wanted:
  1. The command line only takes a single filename, even though there is an interactive mode with support for playing additional files. I'd like to specify an entire album up front.
  2. Although the documentation claimed support for M4A files (which I happen to have a lot of by virtue of using said manufacturer's music store), I only got errors trying to play them. Playback of MP3 files also seems a bit buggy (playback sometimes stops prematurely). I'm thinking of incorporating a different decoding engine.
For today's post, I will focus just on the first item: playing multiple files. Armed with nothing but a compiler and an innate desire to make this software do what I want, this post is my log of trying to get this to work.


Playing multiple files

The essential syntax for the command is:

raop_play 192.168.1.100 awesome_song.mp3

where 192.168.1.100 is the IP address of the device you wish to stream to, and awesome_song.mp3 is the filename you'd like to play. My first solution to overcome single-file playback was a simple loop in the shell:

for i in *.mp3; do raop_play 192.168.1.100 "$i"; done

This works, but stopping in the middle of an album isn't as easy as a single Control-C. The software also takes a bit to connect to and disconnect from the device, introducing a long (several second) silent gap in between songs. Building the loop into the software itself seems cleaner and more effective.

The file where main() lives is raop_play.c, in the folder raop_play/raop_play if you're following along with the same source archive I used. If you aren't, no worries; I'll try to talk through enough of what I'm doing to get the idea across.

In main() we see a standard-looking loop to process command line options:

for(i=1;i<argc;i++){
  if(!strcmp(argv[i],"-i")){
    iact=1;
    continue;
  }
  ...
  if(!fname) {fname=argv[i]; continue;}
}

I've highlighted in red the line that concerns us. As the loop iterates over all the arguments, it eventually arrives at what it decides is the name of the file to be played. The code uses the variable fname to store the path to the file as a C string. Assuming the filename isn't already set, the code sets it. However, we want to be able to continue passing additional filenames, so we need to do something a little bit different here:

if(!findex) {findex=i; continue;}

We create a new variable, int findex = 0, and set it to that index in the argument array the first time we decide we've seen a filename. We also change a sanity check to look at findex:

if(!iact && !findex) return print_usage(argv);

Now, looking down a few lines, we see a block of initialization code:

raopld->raopcl=raopcl_open();
if(!raopld->raopcl) goto erexit;
if(raopcl_connect(raopld->raopcl,host,port)) goto erexit;
if(raopcl_update_volume(raopld->raopcl,volume)) goto erexit;
printf("%s\n",RAOP_CONNECTED);
fflush(stdout);
if(fname && !(raopld->auds=auds_open(fname,0))) goto erexit;
set_fd_event(0,RAOP_FD_READ,console_read,NULL);
rval=0;
while(!rval){
  ...

The highlighted line is where the audio file is opened. We see above that where the software connects to the device, which is not an instantaneous operation. One of my goals is to stay connected to the device across multiple songs. The while loop we see handles certain events within the program, including interactive console commands which we aren't going to be using here. Lets modify the highlighted line and create a new loop to iterate through a sequence of filenames:

...
fflush(stdout);
if(!findex) goto erexit;
for (findex; findex<argc; findex++) {
  fname = argv[findex];
  if (raopld->auds=auds_open(fname,0)) {
    set_fd_event(0,RAOP_FD_READ,console_read,NULL);
    ...

I've replaced the highlighted line from above with the highlighted section here. Notice that the same error checks are maintained: if no filename was found, the code jumps to erexit. Otherwise, we iterate through all remaining arguments, assuming they are filenames. For each of these, we go ahead and assign the pointer fname to that index in the argument array so we don't break any other code that relies on it. In order to handle the case where an invalid filename sneaks in there, we wrap all subsequent code in an if statement that just falls through to the next filename if anything goes wrong in opening the file.

At the end of the original while loop we see:

  ...
}
rval=raopcl_close(raopld->raopcl);
erexit:
if(raopld->auds) auds_close(raopld->auds);
if(raopld) free(raopld);
return rval;

This code disconnects from the device, closes the file, and exits the program. We need to modify this to end our new loop, making sure to close the open file before proceeding to the next one:

      ...

    }
    auds_close(raopld->auds);
  }  /* if statement */
}  /* for loop */
rval=raopcl_close(raopld->raopcl);
erexit:   
if(raopld->auds) auds_close(raopld->auds);
if(raopld) free(raopld);
return rval;

The highlighted lines make sure that, if the file opened correctly, it gets closed. Then the if statement and while loop close so we can proceed to the next filename.

Okay, great, we're done! Do a make && make install inside the raop_play directory, and run the following from a directory containing our favorite album:

raop_play 192.168.1.100 *

And... no dice. I see the following console output after the first song ends:

[0:18] Decoding of awesome_song.mp3 finished.
done
INFO: fd_event_callback: read, disconnected on the other end
Segmentation fault

Hmmm... looks like the song finished, it waited about 10 seconds after writing "done," and then disconnected and segfaulted. So some shutdown code is still happening when we don't want it to be. Let's take another look inside that original while loop:

if(auds_get_next_sample(raopld->auds, &buf, &size)){
  auds_close(raopld->auds);
  raopld->auds=NULL;
  raopcl_wait_songdone(raopld->raopcl,1);
}

Ah ha! It seems that when the song finishes (i.e., there are no audio samples left to get), function auds_get_next_sample() returns a nonzero value, triggering this block of code. The block closes the file and then does something to wait for whatever is in the buffer finish playing. It would seem that once this happens, we can't play another song without re-initializing some things. So, let's hack it:

if(auds_get_next_sample(raopld->auds, &buf, &size)){
  goto nextsong;
  auds_close(raopld->auds);
  raopld->auds=NULL;
  raopcl_wait_songdone(raopld->raopcl,1);
}

and at the end of our loop:

       ...
    }
nextsong:
    auds_close(raopld->auds);
  }  /* if statement */
}  /* for loop */

Now when a song finishes, we skip what the software wanted to do and jump to the end of our if statement, where we already have code to close the song. Another compile and... it works! As soon as the first song ends, the second begins - no gap!

However, the final song doesn't actually finish, it gets cut off a few seconds too soon. Also, when the program terminates, there appears to be a pointer-freeing issue that causes a crash. This turns out to be me forgetting about this line at the end of the program:

if(raopld->auds) auds_close(raopld->auds);

Since I never set raopld->auds to NULL, the code attempts to close the last file a second time. To fix this, let's borrow a line from the original code and place it at the end of our loop:

      ...
    }
    auds_close(raopld->auds);
    raopld->auds=NULL;
  }  /* if statement */
}  /* for loop */
rval=raopcl_close(raopld->raopcl);
...

This solves the crash after the last song. The truncation issue is still there, as well as the desire to support other audio formats. I will tackle some or all of this in my next post.

No comments:

Post a Comment