Saturday, September 14, 2013

Modifying software for fun and music, part 2

Author's Note: Whew! I've been on a long hiatus from posting. This post in particular is a long-overdue follow-up to an experiment where I documented my foray into deciphering and modifying a particular piece of open source software as I went along. Unlike that post, the following is compiled from notes made during the process. Enjoy!

Last time we left off with having added the ability to specify multiple songs from the command line. However, the last song in the list was getting truncated several seconds early. Also, m4a (AAC) files were not playing at all. Finally, after further testing it came to light that mono MP3 files were not playing correctly. Read on for my notes on treating each of these issues, and finally the output of diff on the original code and my changes, if you're so inclined to use them!


Eliminating song truncation

First, let's revisit the issue of the last song being truncated. Each song in the list supplied, up until the last, played in full. However, the last song in the list would be cut off several seconds early. It turns out that there are two "close" operations: auds_close() that closes a single file, and raopcl_close() that closes the connection to the device. auds_close() can be called safely once the end of the file is reached since any unplayed audio is in an internal buffer waiting to be sent to the device. However, calling raopcl_close() terminates the connection to the device regardless of any unplayed audio in the buffer. Since this is only called at the end of the program, it has the effect of truncating the last song.

Fortunately, there is another function raopcl_wait_songdone() that sets a flag to prevent raopcl_close() from terminating the connection until the buffer is emptied. By adding the goto in the code below, this call never occurred and the last song was truncated! We can add a check to see whether findex (our index into the list of command line arguments) has reached the last song yet (equal to argc-1). When it does, we won't jump to nextsong but rather let the program follow its original course:

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

And, just for good measure, we can add another call to it outside our for loop:

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

  raopcl_wait_songdone(raopld->raopcl,1);
  rval=raopcl_close(raopld->raopcl);
...

Observe that raopcl_wait_songdone() will now always be called prior to raopcl_close(). This code can be further cleaned up to eliminate redundant calls, but it is sufficient to play through each song and to play the last song fully before terminating the connection to the device!

Getting .m4a files to play

My next challenge is getting m4a files to play. First, I traced where in the software m4a files are handled. audio_stream.c is the main file for opening media. In it, function get_data_type() determines the type of each file based on its extension. Function auds_open() uses this type to determine which piece of code to use to open and play that file. For m4a files, it first tries to treat the file as type ALAC. If it cannot find an ALAC marker in the file, it then tries type AAC:

...
case AUD_TYPE_ALAC:
  rval=m4a_open(auds,fname);
  if(rval) { // retry AAC
    rval=aac_open(auds,fname);
    auds->data_type = AUD_TYPE_AAC;
  }
  break;
...

Using the program ffmpeg, I found that my m4a files are indeed of type AAC:

$ ffmpeg -i myfile.m4a
...
 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'myfile.m4a':
  Duration: 00:03:27.88, start: 0.000000, bitrate: 289 kb/s
    Stream #0.0(eng): Audio: aac, 44100 Hz, stereo, s16
...

Whenever I tried opening one of these files with raop_play, I received the following error:

ERR: find_atom_pos: alac can't be found in the file
sh: faad: command not found
ERR: can't open fifo
Segmentation fault

Ah, I should have paid closer attention to that error message! raop_play requires an external utility, faad, to decode these files. Of course, faad wasn't installed at this point. Fortunately, it is available as a package for my Linux distribution, so the fix was as easy as running apt-get. It's great when a problem is solved that easily!

Getting mono .mp3 files to play right

The final issue I was running into was in the handling of some of my older mp3 files, which were converted from mono mp2 files (mp2 was a precursor to mp3). These files would play, but at double the speed and pitch. It seemed that raop_play was expecting stereo data and would simply take two mono chunks at once in place of a single stereo chunk and play them together!

There is a place in mp3_stream.c that appears to handle this case, but for some reason it wasn't being reached:

if(!memcmp(bufp+3,"mono",4)){
  auds->channels=1;
}

Rather than dig into the file format and understand what triggers this code, I observed that raop_play again uses an external utility (this time mpg123) to decode mp3 files. mpg123 can take an argument --stereo that forces the decoded output to be stereo regardless if the input file is stereo or mono. So we can set this flag in the following code in mp3_stream.c:

...
/* Changed by Mike Clement (4 to 5, added "--stereo") */
  char *darg[5]={MP3_DECODER,"--stereo","-s",NULL, NULL};
  int efd;
  char buf[1024];
  char *bufp;
  int rsize,tsize=0,i;
  int rval=-1;

  /* Changed by Mike Clement (2 to 3) */
  darg[3]=mp3->fname;
...

We also have to increase the size of darg[] and change which index gets the filename. Now mono files play at the right speed and pitch!

... and the code

One further note: I've said 'file' a lot throughout this post, but raop_play can also take URLs to streaming media. For instance, as I've been writing this post, I've been listening to a live stream of KEXP (out of Seattle) using the following command:

raop_play 192.168.1.100 http://live-mp3-128.kexp.org:8000/

Finally, here is the output of diff between the original (v0.5.2) raop_play code and my modified code. It should be possible to apply these changes directly to the two files using the patch command.

raop_play/raop_play/mp3_stream.c: (download mp3_stream.c.diff)
34c34,35
<       char *darg[4]={MP3_DECODER,"-s",NULL, NULL};
---
>       /* Changed by Mike Clement (4 to 5, added "--stereo") */
>       char *darg[5]={MP3_DECODER,"--stereo","-s",NULL, NULL};
41c42,43
<       darg[2]=mp3->fname;
---
>       /* Changed by Mike Clement (2 to 3) */
>       darg[3]=mp3->fname;

raop_play/raop_play/raop_play.c: (download raop_play.c.diff)
186a187
>       int findex=0;
220c221
<               if(!fname) {fname=argv[i]; continue;}
---
>               if(!findex) {findex=i; continue;}
223c224
<       if(!iact && !fname) return print_usage(argv);
---
>       if(!iact && !findex) return print_usage(argv);
236,239c237,246
<       if(fname && !(raopld->auds=auds_open(fname,0))) goto erexit;
<       set_fd_event(0,RAOP_FD_READ,console_read,NULL);
<       rval=0;
<       while(!rval){
---

>       /* Added by Mike Clement */
>       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);
>           rval=0;
>           while(!rval){
262a270,271
>                               if (findex<argc-1)  /* TEST - may need tweaking */
>                                 goto nextsong;  /* Added by Mike Clement */
270c279,280
<                       }while(raopld->auds && raopcl_sample_remsize(raopld->raopcl));
---
>                       }while(raopld->auds && 
>                              raopcl_sample_remsize(raopld->raopcl));
276c286,295
<       }
---
>           }
>           
> /* Added by Mike Clement */
> nextsong:
>           if(raopld->auds) auds_close(raopld->auds);
>           raopld->auds=NULL;
>         }  /* if statement */
>       }  /* for loop */

>       raopcl_wait_songdone(raopld->raopcl,1);

No comments:

Post a Comment