The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * This file is derived from mt-daap project.
 */

#include "mp3.h"

static int
get_mp3tags(PerlIO *infile, char *file, HV *info, HV *tags)
{
  int ret;
  
  // See if this file has an APE tag as fast as possible
  // This is still a big performance hit :(
  if ( _has_ape(infile) ) {
    get_ape_metadata(infile, file, info, tags);
  }
  
  ret = parse_id3(infile, file, info, tags, 0);

  return ret;
}

static int
_has_ape(PerlIO *infile)
{
  Buffer buf;
  uint8_t ret = 0;
  char *bptr;
  
  if ( (PerlIO_seek(infile, -160, SEEK_END)) == -1 ) {
    return 0;
  }
  
  DEBUG_TRACE("Seeked to %d looking for APE tag\n", (int)PerlIO_tell(infile));
  
  // Bug 9942, read 136 bytes so we can check at -32 bytes in case file
  // does not have an ID3v1 tag
  buffer_init(&buf, 136);
  if ( !_check_buf(infile, &buf, 136, 136) ) {
    goto out;
  }
  
  bptr = buffer_ptr(&buf);
  
  if ( bptr[0] == 'A' && bptr[1] == 'P' && bptr[2] == 'E'
    && bptr[3] == 'T' && bptr[4] == 'A' && bptr[5] == 'G'
    && bptr[6] == 'E' && bptr[7] == 'X'
  ) {
    DEBUG_TRACE("APE tag found at -160 (with ID3v1)\n");
    ret = 1;
  }
  else {
    // APE tag without ID3v1 tag will be -32 bytes from end
    buffer_consume(&buf, 128);
    
    bptr = buffer_ptr(&buf);

    if ( bptr[0] == 'A' && bptr[1] == 'P' && bptr[2] == 'E'
      && bptr[3] == 'T' && bptr[4] == 'A' && bptr[5] == 'G'
      && bptr[6] == 'E' && bptr[7] == 'X'
    ) {
      DEBUG_TRACE("APE tag found at -32 (no ID3v1)\n");
      ret = 1;
    }
  }
  
out:
  buffer_free(&buf);
  
  return ret;
}

// _decode_mp3_frame
static int
_decode_mp3_frame(unsigned char *frame, struct mp3_frameinfo *pfi)
{
  int ver;
  int layer_index;
  int sample_index;
  int bitrate_index;
  int samplerate_index;

  if ((frame[0] != 0xFF) || (frame[1] < 224)) {
    return -1;
  }

  ver = (frame[1] & 0x18) >> 3;
  if (ver == 1) return -1;
    
  pfi->layer = 4 - ((frame[1] & 0x6) >> 1);
  if (pfi->layer == 4) return -1;
  
  pfi->crc_protected = !(frame[1] & 0x1);

  layer_index = sample_index = -1;

  switch(ver) {
  case 0:
    pfi->mpeg_version = 0x25;			// 2.5
    sample_index = 2;
    if (pfi->layer == 1)
      layer_index = 3;
    if ((pfi->layer == 2) || (pfi->layer == 3))
      layer_index = 4;
	break;
  case 2:
    pfi->mpeg_version = 0x20;			// 2.0
    sample_index = 1;
    if (pfi->layer == 1)
      layer_index = 3;
    if ((pfi->layer == 2) || (pfi->layer == 3))
      layer_index = 4;
    break;
  case 3:
    pfi->mpeg_version = 0x10;			// 1.0
    sample_index = 0;
    if (pfi->layer == 1)
      layer_index = 0;
    if (pfi->layer == 2)
      layer_index = 1;
    if (pfi->layer == 3)
      layer_index = 2;
    break;
  }

  if ((layer_index < 0) || (layer_index > 4)) {
    return -1;
  }

  if ((sample_index < 0) || (sample_index > 2)) {
    return -1;
  }

  if (pfi->layer==1) pfi->samples_per_frame = 384;
  if (pfi->layer==2) pfi->samples_per_frame = 1152;
  if (pfi->layer==3) {
    if (pfi->mpeg_version == 0x10)
      pfi->samples_per_frame = 1152;
    else
      pfi->samples_per_frame = 576;
  }

  bitrate_index = (frame[2] & 0xF0) >> 4;
  samplerate_index = (frame[2] & 0x0C) >> 2;

  if ((bitrate_index == 0xF) || (bitrate_index==0x0)) {
    return -1;
  }

  if (samplerate_index == 3) {
    return -1;
  }

  pfi->bitrate = bitrate_tbl[layer_index][bitrate_index];
  pfi->samplerate = sample_rate_tbl[sample_index][samplerate_index];
  
  // Validate emphasis is not reserved
  if ((frame[3] & 0x3) == 2) return -1;

  if (((frame[3] & 0xC0) >> 6) == 3)
    pfi->stereo = 0;
  else
    pfi->stereo = 1;

  if (frame[2] & 0x02)
    pfi->padding = 1;
  else
    pfi->padding=0;

  if (pfi->mpeg_version == 0x10) {
    if (pfi->stereo)
      pfi->xing_offset = 36;
    else
      pfi->xing_offset = 21;
  }
  else {
    if (pfi->stereo)
      pfi->xing_offset = 21;
    else
      pfi->xing_offset = 13;
  }

  if (pfi->layer == 3) {
    if (pfi->mpeg_version == 0x10)
      pfi->frame_length = (144 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding);
    else
      pfi->frame_length = (72 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding);
  }
  else if (pfi->layer == 2)
    pfi->frame_length = 144 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding;
  else
    pfi->frame_length = (12 * pfi->bitrate * 1000 / pfi->samplerate + pfi->padding) * 4;

  if ((pfi->frame_length > 2880) || (pfi->frame_length <= 0)) {
    return -1;
  }
  
  /*
  DEBUG_TRACE("frame: len %d, ver: 0x%x, layer %d, bitrate, %d, samplerate %d\n",
    pfi->frame_length,
    pfi->mpeg_version,
    pfi->layer,
    pfi->bitrate,
    pfi->samplerate
  );
  */

  return 0;
}

// _mp3_get_average_bitrate
// average bitrate by averaging all the frames in the file.  This used
// to seek to the middle of the file and take a 32K chunk but this was
// found to have bugs if it seeked near invalid FF sync bytes that could
// be detected as a real frame
static short _mp3_get_average_bitrate(mp3info *mp3, uint32_t offset, uint32_t audio_size)
{
  struct mp3_frameinfo fi;
  int frame_count   = 0;
  int bitrate_total = 0;
  int err = 0;
  int done = 0;
  int wrap_skip = 0;
  int prev_bitrate = 0;
  bool vbr = FALSE;

  unsigned char *bptr;
  
  buffer_clear(mp3->buf);

  // Seek to offset
  PerlIO_seek(mp3->infile, 0, SEEK_END);
  PerlIO_seek(mp3->infile, offset, SEEK_SET);
  
  while ( done < audio_size - 4 ) {
    // Buffer size is optimized for a possible common case: 20 frames of 192kbps CBR
    if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE * 3) ) {
      err = -1;
      goto out;
    }
    
    done += buffer_len(mp3->buf);
    
    if (wrap_skip) {
      // Skip rest of frame from last buffer
      DEBUG_TRACE("Wrapped, consuming %d bytes from previous frame\n", wrap_skip);
      buffer_consume(mp3->buf, wrap_skip);
      wrap_skip = 0;
    }
  
    while ( buffer_len(mp3->buf) >= 4 ) {
      bptr = buffer_ptr(mp3->buf);
      while ( *bptr != 0xFF ) {
        buffer_consume(mp3->buf, 1);
      
        if ( !buffer_len(mp3->buf) ) {
          // ran out of data
          goto out;
        }
      
        bptr = buffer_ptr(mp3->buf);
      }

      if ( !_decode_mp3_frame( buffer_ptr(mp3->buf), &fi ) ) {
        // Found a valid frame
        frame_count++;
        bitrate_total += fi.bitrate;
        
        if ( !vbr ) {
          // If we see the bitrate changing, we have a VBR file, and read
          // the entire file.  Otherwise, if we see 20 frames with the same
          // bitrate, assume CBR and stop
          if (prev_bitrate > 0 && prev_bitrate != fi.bitrate) {
            DEBUG_TRACE("Bitrate changed, assuming file is VBR\n");
            vbr = TRUE;
          }
          else {
            if (frame_count > 20) {
              DEBUG_TRACE("Found 20 frames with same bitrate, assuming CBR\n");
              goto out;
            }
            
            prev_bitrate = fi.bitrate;
          }
        }
        
        //DEBUG_TRACE("  Frame %d: %dkbps\n", frame_count, fi.bitrate);

        if (fi.frame_length > buffer_len(mp3->buf)) {
          // Partial frame in buffer
          wrap_skip = fi.frame_length - buffer_len(mp3->buf);
          buffer_consume(mp3->buf, buffer_len(mp3->buf));
        }
        else {
          buffer_consume(mp3->buf, fi.frame_length);
        }
      }
      else {
        // Not a valid frame, stray 0xFF
        buffer_consume(mp3->buf, 1);
      }
    }
  }

out:
  if (err) return err;
  
  if (!frame_count) return -1;
  
  DEBUG_TRACE("Average of %d frames: %dkbps\n", frame_count, bitrate_total / frame_count);

  return bitrate_total / frame_count;
}

static int
_parse_xing(mp3info *mp3, struct mp3_frameinfo *pfi)
{
  int i;
  int xing_flags;
  unsigned char *bptr;
  
  if ( !_check_buf(mp3->infile, mp3->buf, 4 + pfi->xing_offset, MP3_BLOCK_SIZE) ) {
    return 0;
  }
  
  buffer_consume(mp3->buf, pfi->xing_offset);
  
  bptr = buffer_ptr(mp3->buf);

  if ( bptr[0] == 'X' || bptr[0] == 'I' ) {
    if (
      ( bptr[1] == 'i' && bptr[2] == 'n' && bptr[3] == 'g' )
      ||
      ( bptr[1] == 'n' && bptr[2] == 'f' && bptr[3] == 'o' )
    ) {
      DEBUG_TRACE("Found Xing/Info tag\n");
      
      if ( !_check_buf(mp3->infile, mp3->buf, 160, MP3_BLOCK_SIZE) ) {
        return 0;
      }
      
      // It's VBR if tag is Xing, and CBR if Info
      pfi->vbr = bptr[1] == 'i' ? VBR : CBR;

      buffer_consume(mp3->buf, 4);

      xing_flags = buffer_get_int(mp3->buf);

      if (xing_flags & XING_FRAMES) {
        pfi->xing_frames = buffer_get_int(mp3->buf);
      }

      if (xing_flags & XING_BYTES) {
        pfi->xing_bytes = buffer_get_int(mp3->buf);
      }

      if (xing_flags & XING_TOC) {
        uint8_t i;
        bptr = buffer_ptr(mp3->buf);
        for (i = 0; i < 100; i++) {
          pfi->xing_toc[i] = bptr[i];
        }
        
        pfi->xing_has_toc = 1;
        
        buffer_consume(mp3->buf, 100);
      }

      if (xing_flags & XING_QUALITY) {
        pfi->xing_quality = buffer_get_int(mp3->buf);
      }

      // LAME tag
      bptr = buffer_ptr(mp3->buf);
      if ( bptr[0] == 'L' && bptr[1] == 'A' && bptr[2] == 'M' && bptr[3] == 'E' ) {
        strncpy(pfi->lame_encoder_version, (char *)bptr, 9);
        bptr += 9;

        // revision/vbr method byte
        pfi->lame_tag_revision = bptr[0] >> 4;
        pfi->lame_vbr_method   = bptr[0] & 15;
        buffer_consume(mp3->buf, 10);

        // Determine vbr status
        switch (pfi->lame_vbr_method) {
          case 1:
          case 8:
            pfi->vbr = CBR;
            break;
          case 2:
          case 9:
            pfi->vbr = ABR;
            break;
          default:
            pfi->vbr = VBR;
        }

        pfi->lame_lowpass = buffer_get_char(mp3->buf) * 100;

        // Skip peak
        buffer_consume(mp3->buf, 4);

        // Replay Gain, code from mpg123
        pfi->lame_replay_gain[0] = 0;
        pfi->lame_replay_gain[1] = 0;

        for (i=0; i<2; i++) {
          // Originator
          unsigned char origin;
          bptr = buffer_ptr(mp3->buf);
          
          origin = (bptr[0] >> 2) & 0x7;

          if (origin != 0) {
            // Gain type
            unsigned char gt = bptr[0] >> 5;
            if (gt == 1)
              gt = 0; /* radio */
            else if (gt == 2)
              gt = 1; /* audiophile */
            else
              continue;

            pfi->lame_replay_gain[gt]
              = (( (bptr[0] & 0x4) >> 2 ) ? -0.1 : 0.1)
              * ( (bptr[0] & 0x3) | bptr[1] );
          }

          buffer_consume(mp3->buf, 2);
        }

        // Skip encoding flags
        buffer_consume(mp3->buf, 1);

        // ABR rate/VBR minimum
        pfi->lame_abr_rate = buffer_get_char(mp3->buf);

        // Encoder delay/padding
        bptr = buffer_ptr(mp3->buf);
        pfi->lame_encoder_delay = ((((int)bptr[0]) << 4) | (((int)bptr[1]) >> 4));
        pfi->lame_encoder_padding = (((((int)bptr[1]) << 8) | (((int)bptr[2]))) & 0xfff);
        // sanity check
        if (pfi->lame_encoder_delay < 0 || pfi->lame_encoder_delay > 3000) {
          pfi->lame_encoder_delay = -1;
        }
        if (pfi->lame_encoder_padding < 0 || pfi->lame_encoder_padding > 3000) {
          pfi->lame_encoder_padding = -1;
        }
        buffer_consume(mp3->buf, 3);

        // Misc
        bptr = buffer_ptr(mp3->buf);
        pfi->lame_noise_shaping = bptr[0] & 0x3;
        pfi->lame_stereo_mode   = (bptr[0] & 0x1C) >> 2;
        pfi->lame_unwise        = (bptr[0] & 0x20) >> 5;
        pfi->lame_source_freq   = (bptr[0] & 0xC0) >> 6;
        buffer_consume(mp3->buf, 1);

        // XXX MP3 Gain, can't find a test file, current
        // mp3gain doesn't write this data
/*
        bptr = buffer_ptr(mp3->buf);
        unsigned char sign = (bptr[0] & 0x80) >> 7;
        pfi->lame_mp3gain = bptr[0] & 0x7F;
        if (sign) {
          pfi->lame_mp3gain *= -1;
        }
        pfi->lame_mp3gain_db = pfi->lame_mp3gain * 1.5;
*/
        buffer_consume(mp3->buf, 1);

        // Preset/Surround
        bptr = buffer_ptr(mp3->buf);
        pfi->lame_surround = (bptr[0] & 0x38) >> 3;
        pfi->lame_preset   = ((bptr[0] << 8) | bptr[1]) & 0x7ff;
        buffer_consume(mp3->buf, 2);

        // Music Length
        pfi->lame_music_length = buffer_get_int(mp3->buf);

        // Skip CRCs
      }
    }
  }
  // Check for VBRI header from Fhg encoders
  else if ( bptr[0] == 'V' && bptr[1] == 'B' && bptr[2] == 'R' && bptr[3] == 'I' ) {
    DEBUG_TRACE("Found VBRI tag\n");
    
    if ( !_check_buf(mp3->infile, mp3->buf, 14, MP3_BLOCK_SIZE) ) {
      return 0;
    }
    
    // Skip tag and version ID
    buffer_consume(mp3->buf, 6);

    pfi->vbri_delay   = buffer_get_short(mp3->buf);
    pfi->vbri_quality = buffer_get_short(mp3->buf);
    pfi->vbri_bytes   = buffer_get_int(mp3->buf);
    pfi->vbri_frames  = buffer_get_int(mp3->buf);
  }
  
  return 1;
}

static int
get_mp3fileinfo(PerlIO *infile, char *file, HV *info)
{
  struct mp3_frameinfo fi;
  unsigned char *bptr;
  char id3v1taghdr[4];

  unsigned int id3_size = 0; // size of leading ID3 data

  off_t file_size;           // total file size
  off_t audio_offset = 0;    // offset to first audio frame
  off_t audio_size;          // size of all audio frames

  int song_length_ms = 0;    // duration of song in ms
  int bitrate        = 0;    // actual bitrate of song

  int found;
  int err = 0;
  
  mp3info *mp3;
  Newz(0, mp3, sizeof(mp3info), mp3info);
  Newz(0, mp3->buf, sizeof(Buffer), Buffer);
  
  mp3->infile = infile;
  mp3->file   = file;
  mp3->info   = info;
  
  buffer_init(mp3->buf, MP3_BLOCK_SIZE);
  
  file_size = _file_size(infile);
  
  my_hv_store( info, "file_size", newSVuv(file_size) );

  memset((void*)&fi, 0, sizeof(fi));
  
  if ( !_check_buf(mp3->infile, mp3->buf, 10, MP3_BLOCK_SIZE) ) {
    err = -1;
    goto out;
  }
  
  bptr = buffer_ptr(mp3->buf);

  if (
    (bptr[0] == 'I' && bptr[1] == 'D' && bptr[2] == '3') &&
    bptr[3] < 0xff && bptr[4] < 0xff &&
    bptr[6] < 0x80 && bptr[7] < 0x80 && bptr[8] < 0x80 && bptr[9] < 0x80
  ) {
    /* found an ID3 header... */
    id3_size = 10 + (bptr[6]<<21) + (bptr[7]<<14) + (bptr[8]<<7) + bptr[9];

    if (bptr[5] & 0x10) {
      // footer present
      id3_size += 10;
    }
    
    DEBUG_TRACE("Found ID3v2.%d.%d tag, size %d\n", bptr[3], bptr[4], id3_size);

    // Always seek past the ID3 tags
    buffer_clear(mp3->buf);
    
    PerlIO_seek(infile, id3_size, SEEK_SET);
    
    if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) {
      err = -1;
      goto out;
    }

    audio_offset += id3_size;
  }

  found = 0;

  // Find an MP3 frame
  while ( !found && buffer_len(mp3->buf) ) {
    bptr = buffer_ptr(mp3->buf);
    
    while ( *bptr != 0xFF ) {
      buffer_consume(mp3->buf, 1);
     
      audio_offset++;

      if ( !buffer_len(mp3->buf) ) {
        if (audio_offset >= file_size - 4) {
          // No audio frames in file
          err = -1;
          goto out;
        }
        
        if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) {
          PerlIO_printf(PerlIO_stderr(), "Unable to find any MP3 frames in file: %s\n", file);
          err = -1;
          goto out;
        }
      }
      
      bptr = buffer_ptr(mp3->buf);
    }
    
    DEBUG_TRACE("Found FF sync at offset %d\n", (int)audio_offset);
    
    // Make sure we have 4 bytes
    if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) {
      err = -1;
      goto out;
    }

    if ( !_decode_mp3_frame( buffer_ptr(mp3->buf), &fi ) ) {
      // Found a valid frame
      DEBUG_TRACE("  valid frame\n");
      
      found = 1;
    }
    else {
      // Not a valid frame, stray 0xFF
      DEBUG_TRACE("  invalid frame\n");
      
      buffer_consume(mp3->buf, 1);
      audio_offset++;
    }
  }

  if ( !found ) {
    PerlIO_printf(PerlIO_stderr(), "Unable to find any MP3 frames in file (checked 4K): %s\n", file);
    err = -1;
    goto out;
  }

  audio_size = file_size - audio_offset;

  // now check for Xing/Info/VBRI/LAME headers
  if ( !_parse_xing(mp3, &fi) ) {
    err = -1;
    goto out;
  }

  // use LAME CBR/ABR value for bitrate if available
  if ( (fi.vbr == CBR || fi.vbr == ABR) && fi.lame_abr_rate ) {
    if (fi.lame_abr_rate >= 255) {
      // ABR rate field only codes up to 255, use preset value instead
      if (fi.lame_preset <= 320) {
        bitrate = fi.lame_preset;
        DEBUG_TRACE("bitrate from lame_preset: %d\n", bitrate);
      }
    }
    else {
      bitrate = fi.lame_abr_rate;
      DEBUG_TRACE("bitrate from lame_abr_rate: %d\n", bitrate);
    }
  }

  // Or if we have a Xing header, use it to determine bitrate
  else if (fi.xing_frames && fi.xing_bytes) {
    float mfs = (float)fi.samplerate / ( fi.mpeg_version == 0x20 || fi.mpeg_version == 0x25 ? 72000. : 144000. );
    bitrate = ( fi.xing_bytes / fi.xing_frames * mfs );
    DEBUG_TRACE("bitrate from Xing header: %d\n", bitrate);
  }

  // Or use VBRI header
  else if (fi.vbri_frames && fi.vbri_bytes) {
    float mfs = (float)fi.samplerate / ( fi.mpeg_version == 0x20 || fi.mpeg_version == 0x25 ? 72000. : 144000. );
    bitrate = ( fi.vbri_bytes / fi.vbri_frames * mfs );
    DEBUG_TRACE("bitrate from VBRI header: %d\n", bitrate);
  }

  // check if last 128 bytes is ID3v1.0 or ID3v1.1 tag
  PerlIO_seek(infile, file_size - 128, SEEK_SET);
  if (PerlIO_read(infile, id3v1taghdr, 4) == 4) {
    if (id3v1taghdr[0]=='T' && id3v1taghdr[1]=='A' && id3v1taghdr[2]=='G') {
      DEBUG_TRACE("ID3v1 tag found\n");
      audio_size -= 128;
    }
  }

  // If we don't know the bitrate from Xing/LAME/VBRI, calculate average
  if ( !bitrate ) {    
    DEBUG_TRACE("Calculating average bitrate starting from %d...\n", (int)audio_offset);
    bitrate = _mp3_get_average_bitrate(mp3, audio_offset, audio_size);

    if (bitrate <= 0) {
      // Couldn't determine bitrate, just use
      // the bitrate from the last frame we parsed
      DEBUG_TRACE("Unable to determine bitrate, using bitrate of most recent frame (%d)\n", fi.bitrate);
      bitrate = fi.bitrate;
    }
  }

  if (!song_length_ms) {
    if (fi.xing_frames) {
      song_length_ms = (int) ((double)(fi.xing_frames * fi.samples_per_frame * 1000.)/
				  (double) fi.samplerate);
    }
    else if (fi.vbri_frames) {
      song_length_ms = (int) ((double)(fi.vbri_frames * fi.samples_per_frame * 1000.)/
				  (double) fi.samplerate);
		}
    else {
      song_length_ms = (int) ((double)audio_size * 8. /
				  (double)bitrate);
    }
  }

  my_hv_store( info, "song_length_ms", newSViv(song_length_ms) );

  my_hv_store( info, "layer", newSViv(fi.layer) );
  my_hv_store( info, "stereo", newSViv(fi.stereo) );
  my_hv_store( info, "samples_per_frame", newSViv(fi.samples_per_frame) );
  my_hv_store( info, "padding", newSViv(fi.padding) );
  my_hv_store( info, "audio_size", newSViv(audio_size) );
  my_hv_store( info, "audio_offset", newSViv(audio_offset) );
  my_hv_store( info, "bitrate", newSViv( bitrate * 1000 ) );
  my_hv_store( info, "samplerate", newSViv( fi.samplerate ) );

  if (fi.xing_frames) {
    my_hv_store( info, "xing_frames", newSViv(fi.xing_frames) );
  }

  if (fi.xing_bytes) {
    my_hv_store( info, "xing_bytes", newSViv(fi.xing_bytes) );
    
    if (fi.xing_has_toc) {
      uint8_t i;
      AV *xing_toc = newAV();

      for (i = 0; i < 100; i++) {
        av_push( xing_toc, newSVuv(fi.xing_toc[i]) );
      }

      my_hv_store( info, "xing_toc", newRV_noinc( (SV *)xing_toc ) );
    }
  }

  if (fi.xing_quality) {
    my_hv_store( info, "xing_quality", newSViv(fi.xing_quality) );
  }

  if (fi.vbri_frames) {
    my_hv_store( info, "vbri_delay", newSViv(fi.vbri_delay) );
    my_hv_store( info, "vbri_frames", newSViv(fi.vbri_frames) );
    my_hv_store( info, "vbri_bytes", newSViv(fi.vbri_bytes) );
    my_hv_store( info, "vbri_quality", newSViv(fi.vbri_quality) );
  }

  if (fi.lame_encoder_version[0]) {
    my_hv_store( info, "lame_encoder_version", newSVpvn(fi.lame_encoder_version, 9) );
    my_hv_store( info, "lame_tag_revision", newSViv(fi.lame_tag_revision) );
    my_hv_store( info, "lame_vbr_method", newSVpv( vbr_methods[fi.lame_vbr_method], 0 ) );
    my_hv_store( info, "lame_lowpass", newSViv(fi.lame_lowpass) );

    if (fi.lame_replay_gain[0]) {
      my_hv_store( info, "lame_replay_gain_radio", newSVpvf( "%.1f dB", fi.lame_replay_gain[0] ) );
    }

    if (fi.lame_replay_gain[1]) {
      my_hv_store( info, "lame_replay_gain_audiophile", newSVpvf( "%.1f dB", fi.lame_replay_gain[1] ) );
    }

    my_hv_store( info, "lame_encoder_delay", newSViv(fi.lame_encoder_delay) );
    my_hv_store( info, "lame_encoder_padding", newSViv(fi.lame_encoder_padding) );

    my_hv_store( info, "lame_noise_shaping", newSViv(fi.lame_noise_shaping) );
    my_hv_store( info, "lame_stereo_mode", newSVpv( stereo_modes[fi.lame_stereo_mode], 0 ) );
    my_hv_store( info, "lame_unwise_settings", newSViv(fi.lame_unwise) );
    my_hv_store( info, "lame_source_freq", newSVpv( source_freqs[fi.lame_source_freq], 0 ) );

/*
    my_hv_store( info, "lame_mp3gain", newSViv(fi.lame_mp3gain) );
    my_hv_store( info, "lame_mp3gain_db", newSVnv(fi.lame_mp3gain_db) );
*/

    my_hv_store( info, "lame_surround", newSVpv( surround[fi.lame_surround], 0 ) );

    if (fi.lame_preset < 8) {
      my_hv_store( info, "lame_preset", newSVpvn( "Unknown", 7 ) );
    }
    else if (fi.lame_preset <= 320) {
      my_hv_store( info, "lame_preset", newSVpvf( "ABR %d", fi.lame_preset ) );
    }
    else if (fi.lame_preset <= 500) {
      fi.lame_preset /= 10;
      fi.lame_preset -= 41;
      if ( presets_v[fi.lame_preset] ) {
        my_hv_store( info, "lame_preset", newSVpv( presets_v[fi.lame_preset], 0 ) );
      }
    }
    else if (fi.lame_preset >= 1000 && fi.lame_preset <= 1007) {
      fi.lame_preset -= 1000;
      if ( presets_old[fi.lame_preset] ) {
        my_hv_store( info, "lame_preset", newSVpv( presets_old[fi.lame_preset], 0 ) );
      }
    }

    if (fi.vbr == ABR || fi.vbr == VBR) {
      my_hv_store( info, "vbr", newSViv(1) );
    }
  }

out:
  buffer_free(mp3->buf);
  Safefree(mp3->buf);
  Safefree(mp3);

  if (err) return err;

  return 0;
}

static int
mp3_find_frame(PerlIO *infile, char *file, int offset)
{
  Buffer mp3_buf;
  unsigned char *bptr;
  unsigned int buf_size;
  struct mp3_frameinfo fi;
  int frame_offset = -1;
  off_t file_size;
  off_t audio_offset;
  uint32_t song_length_ms;
  HV *info = newHV();
  
  buffer_init(&mp3_buf, MP3_BLOCK_SIZE);
  
  if ( (get_mp3fileinfo(infile, file, info)) != 0 ) {
    goto out;
  }
  
  file_size      = SvIV( *(my_hv_fetch(info, "file_size")) );
  audio_offset   = SvIV( *(my_hv_fetch(info, "audio_offset")) );
  song_length_ms = SvIV( *(my_hv_fetch(info, "song_length_ms")) );
  
  // (undocumented) If offset is negative, treat it as an absolute file offset in bytes
  // This is a bit ugly but avoids the need to write an entirely new method
  if (offset < 0) {
    frame_offset = abs(offset);
    if (frame_offset < audio_offset) {
      // Force offset to be at least audio_offset, so we don't end up in an ID3 tag
      frame_offset = audio_offset;
    }
    DEBUG_TRACE("find_frame: using absolute offset value %d\n", frame_offset);
  }
  else {
    if (offset >= song_length_ms) {
      goto out;
    }
    
    // Use Xing TOC if available
    if ( my_hv_exists(info, "xing_toc") ) {
      float percent;
      uint8_t ipercent;
      uint16_t tva;
      uint16_t tvb;
      float tvx;
    
      AV *xing_toc        = (AV *)SvRV( *(my_hv_fetch(info, "xing_toc")) );
      uint32_t xing_bytes = SvIV( *(my_hv_fetch(info, "xing_bytes")) );
  
      percent = (offset * 1.0 / song_length_ms) * 100;
      ipercent = (int)percent;
  
      if (ipercent > 99)
        ipercent = 99;
      
      // Interpolate between 2 TOC points
      tva = SvIV( *(av_fetch(xing_toc, ipercent, 0)) );
      if (ipercent < 99) {
        tvb = SvIV( *(av_fetch(xing_toc, ipercent + 1, 0)) );
      }
      else {
        tvb = 256;
      }
    
      tvx = tva + (tvb - tva) * (percent - ipercent);
  
      frame_offset = (int)((1.0/256.0) * tvx * xing_bytes);
  
      frame_offset += audio_offset;
  
      // Don't return offset == audio_offset, because that would be the Xing frame
      if (frame_offset == audio_offset) {
        DEBUG_TRACE("find_frame: frame_offset == audio_offset, skipping to next frame\n");
        frame_offset += 1;
      }
  
      DEBUG_TRACE("find_frame: using Xing TOC, song_length_ms: %d, percent: %f, tva: %d, tvb: %d, tvx: %f, frame offset: %d\n",
        song_length_ms, percent, tva, tvb, tvx, frame_offset
      );
    }
    else {
      // calculate offset using bitrate
      uint32_t bitrate = SvIV( *(my_hv_fetch(info, "bitrate")) );
      float bytes_per_ms = bitrate / 8000.0;
    
      frame_offset = (int)(bytes_per_ms * offset);
    
      frame_offset += audio_offset;
    
      DEBUG_TRACE("find_frame: using bitrate %d, bytes_per_ms: %f, frame offset: %d\n", bitrate, bytes_per_ms, frame_offset);
    }
  }
  
  // If frame_offset is too near the end of the file we won't find a valid frame
  // so require offset to be at least 1000 bytes from the end of the file
  // XXX this would be more accurate if we determined max_frame_len
  if ((file_size - frame_offset) < 1000) {
    frame_offset -= 1000 - (file_size - frame_offset);
    if (frame_offset < 0)
      frame_offset = 0;
    DEBUG_TRACE("find_frame: offset too close to end of file, adjusted to %d\n", frame_offset);
  }
  
  PerlIO_seek(infile, frame_offset, SEEK_SET);

  if ( !_check_buf(infile, &mp3_buf, 4, MP3_BLOCK_SIZE) ) {
    frame_offset = -1;
    goto out;
  }
  
  bptr = (unsigned char *)buffer_ptr(&mp3_buf);
  buf_size = buffer_len(&mp3_buf);
  
  // Find 0xFF sync and verify it's a valid mp3 frame header
  while (1) {
    if (
      buf_size < 4
      ||
      ( bptr[0] == 0xFF && !_decode_mp3_frame( bptr, &fi ) )
    ) {
      break;
    }
    
    bptr++;
    buf_size--;
  }
  
  if (buf_size >= 4) {
    frame_offset += buffer_len(&mp3_buf) - buf_size;
    DEBUG_TRACE("find_frame: frame_offset: %d\n", frame_offset);
  }
  else {
    // Didn't find a valid frame, probably too near the end of the file
    DEBUG_TRACE("find_frame: did not find a valid frame\n");
    frame_offset = -1;
  }

out:
  buffer_free(&mp3_buf);
  SvREFCNT_dec(info);

  return frame_offset;
}