Patch - MP3 Frame validation

Paul Kelly <[email protected]>
Newsgroups gmane.comp.audio.icecast.devel
Message-ID <[email protected]>
(I'm aware that this patch is not likely to be accepted in the upstream 
source because Icecast doesn't really support MP3, but I thought it 
worthwhile to send it here in case anyone wishes to modify their local 
source with it.)

Background:

Icecast does not make any attempt to demarcate the boundaries between MP3 
frames, and when a listening client connects to the server it generally is 
sent an initial partial frame that can't be decoded. This is not a problem 
for almost all client players.

It becomes a problem however when a "pre-roll" intro clip is used. When 
Icecast connects the listener to the main stream after playing the intro 
clip, it will very likely cut in in the middle of a frame, which causes a 
problem for some players. Flash player in particular exhibits strange 
behaviour with the audio cutting in and out every few seconds. Pausing the 
player and resuming cures the problem.

A likely explanation for this is as follows (a best guess; may contain 
some slight inaccuracies and/or generalisations):

Adobe doesn't provide a facility in Flash Player to play a continuous 
audio stream via HTTP (e.g. from Icecast). Flash-based MP3 players 
typically get around this by downloading the stream in lots of separate 
overlapping chunks and playing these as if they were individual files. 
When one chunk is getting near the end, it starts playing the next chunk 
and then cross-fades into it. In effect there are two mini-MP3 players 
operating with constant cross-fading between them.

It looks like what is happening is that the player that was playing when 
the junk data occured at the crossover has got stuck, but that the other 
one is still functioning normally. Therefore silence is heard when the 
"stuck" player is supposed to be doing its bit, but normal audio when it's 
the other one's turn.

The attached patch address the issue by modifying Icecast so that it 
parses the MP3 data and makes sure it only starts streaming to a client at 
a frame boundary. It is in production use on a number of Icecast servers 
and is working well.

Note that the "channels" member of the mpeg_frame_t struct (and the 4 
lines of code in the validate_header() function that calculate its value) 
are not strictly necessary for the frame validation, but are included for 
completeness.

I hope it is useful for someone.

Best regards

_______________________________________________
Icecast-dev mailing list
[email protected]
http://lists.xiph.org/mailman/listinfo/icecast-dev
icecast-mp3-validation.diff (text/x-diff, 7 KB)
--- icecast-2.3.2/src/format_mp3.c~	2007-10-19 03:02:35.000000000 +0000
+++ icecast-2.3.2/src/format_mp3.c	2011-09-01 15:14:04.076836673 +0000
@@ -483,6 +483,162 @@
     return 1;
 }
 
+static int bitrate_table[2][3][14] =
+{
+ {
+   /* MPEG-2 Layer III */
+   { 8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160},
+   /* MPEG-2 Layer II */
+   { 8, 16, 24,  32,  40,  48,  56,  64,  80,  96, 112, 128, 144, 160},
+   /* MPEG-2 Layer I */
+   {32, 48, 56,  64,  80,  96, 112, 128, 144, 160, 176, 192, 224, 256}
+ },
+ {
+   /* MPEG-1 Layer III */
+   {32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320},
+   /* MPEG-1 Layer II */
+   {32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384},
+   /* MPEG-1 Layer I */
+   {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}
+ }
+};
+
+static int samplerate_table[4][3] =
+{
+   {11025, 12000,  8000}, /* MPEG-2 LSF */
+   {    0,     0,     0}, /* Reserved */
+   {22050, 24000, 16000}, /* MPEG-2 */
+   {44100, 48000, 32000}  /* MPEG-1 */
+};
+
+static int framesize_table[2][3] =
+{
+ /* L.III  L.II   L.I */
+   {  576, 1152,  384}, /* MPEG-2 */
+   { 1152, 1152,  384}  /* MPEG-1 */
+};
+
+static int slotsize_table[3] =
+{
+   1, /* L. III */
+   1, /* L. II  */
+   4  /* L. I   */
+};
+
+static int validate_header(mpeg_frame_t *fr)
+{
+    unsigned char version_code, layer_code, bitrate_code, 
+                  samplerate_code, padding;
+    char message[200];
+
+#define MAX_FRAME_LEN 2880  /* 160kbps Layer II @ 8kHz */
+
+    /* Check sync word is present */
+    if (fr->data[0] != 0xff || (fr->data[1] & 0xe0) != 0xe0)
+        goto invalid_frame;
+
+    /* Validate header by checking no reserved values are present */
+    if ( (version_code    = (fr->data[1] & 0x18) >> 3) ==  1
+      || (layer_code      = (fr->data[1] & 0x06) >> 1) ==  0
+      || (bitrate_code    = (fr->data[2] & 0xf0) >> 4) == 15
+      || (samplerate_code = (fr->data[2] & 0x0c) >> 2) ==  3)
+        goto invalid_frame;
+
+    if (bitrate_code == 0) /* Free-format bitrate */
+        /* We can't calculate the frame length anyway from this so can go no 
+         * further with validation, so return the header as invalid. This is
+         * arguably a bug. */
+        goto invalid_frame;
+
+    /* Calculate data length of frame */
+    fr->kbps = bitrate_table[version_code & 1][layer_code - 1][bitrate_code - 1];
+    fr->sample_rate_Hz = samplerate_table[version_code][samplerate_code];
+    padding = (fr->data[2] & 0x02) >> 1;
+    fr->bytes = (framesize_table[version_code & 1][layer_code - 1] / 8 
+                 / slotsize_table[layer_code - 1] * fr->kbps * 1000 
+                 / fr->sample_rate_Hz + padding)
+                * slotsize_table[layer_code - 1];
+
+    if (fr->bytes <= 0 || fr->bytes > MAX_FRAME_LEN)
+        goto invalid_frame;
+
+    if ((fr->data[3] & 0xc0) >> 6 == 3) /* mono */
+        fr->channels = 1;
+    else
+        fr->channels = 2;
+
+    return fr->bytes;
+
+invalid_frame:
+    fr->bytes = -1;
+    return -1;
+}
+
+/* Parse the MP3 data (also handles MPEG audio layers I/II) to check if there
+ * is a partial frame at the end of the data. If so the partial data is stored
+ * in the MP3 state (where it will be appended to the next time complete_read()
+ * is called) and the modified refbuf containing only complete frames is 
+ * returned.
+ * Note that incomplete frames occuring at the *start* of the data buffer are
+ * ignored. This is so that huge frames (greater than the REFBUF size) can still
+ * be handled correctly.
+ */
+static void remove_partial_frames(refbuf_t *refbuf, mp3_state *source_mp3)
+{
+    int frame_offset = 0, valid_frames = 0;
+
+    /* while there are enough bytes remaining for a valid header */
+    while (refbuf->len - frame_offset >= 4)
+    {
+        mpeg_frame_t fr;
+
+        /* check the header is valid and determine the total frame length */
+        fr.data = (unsigned char *)refbuf->data+frame_offset;
+        validate_header(&fr);
+
+        /* If any frames are bigger than the refbuf size, then we need to leave
+         * them intact and just put up with the fact they will be split up. Such
+         * huge frames should be very rare in practice. */
+        if (fr.bytes > REFBUF_SIZE)
+            return;
+
+        if (fr.bytes > 0) /* if header is valid */
+        {
+            if(frame_offset + fr.bytes > refbuf->len)
+                /* this frame extends beyond the buffer */
+                break;
+
+            valid_frames++;
+            /* Skip to start of next frame */
+            frame_offset += fr.bytes;
+            continue;
+        }
+
+        /* Validation failed: shift forward by one byte and keep searching for header */
+        frame_offset++;
+    }
+
+    if (frame_offset == refbuf->len || valid_frames == 0)
+        /* buffer contained only either wholly complete or wholly partial frames */
+        return;
+
+    /* Allocate a new refbuf to hold the partial last frame. When complete_read()
+     * is called again it will append to this. */
+    source_mp3->read_data = refbuf_new (REFBUF_SIZE);
+    source_mp3->read_count = refbuf->len - frame_offset;
+
+    /* Copy the partial frame into the new refbuf and reduce the byte count for 
+     * the existing refbuf accordingly. */
+    memcpy (source_mp3->read_data->data, refbuf->data+frame_offset,
+            source_mp3->read_count);
+    refbuf->len = frame_offset;
+
+    /* Finally adjust the metadata interval offset to avoid "double-counting" of 
+     * the bytes in the partial frame. */
+    source_mp3->offset -= source_mp3->read_count;
+
+    return;
+}
 
 /* read an mp3 stream which does not have shoutcast style metadata */
 static refbuf_t *mp3_get_no_meta (source_t *source)
@@ -503,7 +659,10 @@
     }
     refbuf->associated = source_mp3->metadata;
     refbuf_addref (source_mp3->metadata);
+
+    remove_partial_frames(refbuf, source_mp3);
     refbuf->sync_point = 1;
+
     return refbuf;
 }
 
@@ -623,6 +782,8 @@
     }
     refbuf->associated = source_mp3->metadata;
     refbuf_addref (source_mp3->metadata);
+
+    remove_partial_frames(refbuf, source_mp3);
     refbuf->sync_point = 1;
 
     return refbuf;
--- icecast-2.3.2/src/format_mp3.h~	2006-09-21 01:49:16.000000000 +0000
+++ icecast-2.3.2/src/format_mp3.h	2011-08-31 16:34:05.888838280 +0000
@@ -37,6 +37,16 @@
     char build_metadata[4081];
 } mp3_state;
 
+typedef struct
+{
+    unsigned char *data; /* Pointer to complete MPEG audio frame (including 
+                          * sync word and header */
+    int bytes;           /* Length of MPEG frame in bytes */
+    int kbps;            /* Bitrate in kilobits per second */
+    int sample_rate_Hz;  /* Sample rate in hertz */
+    int channels;        /* Number of channels (mono/stereo) */
+} mpeg_frame_t;
+
 int format_mp3_get_plugin(struct source_tag *src);
 
 #endif  /* __FORMAT_MP3_H__ */
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.