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"

int
get_mp3fileinfo(PerlIO *infile, char *file, HV *info)
{
 mp3info *mp3 = _mp3_parse(infile, file, info);

 buffer_free(mp3->buf);
 Safefree(mp3->buf);
 Safefree(mp3->first_frame);
 Safefree(mp3->xing_frame);
 Safefree(mp3);

 return 0;
}

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

  return ret;
}

int
_is_ape_header(char *bptr)
{
  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'
  ) {
    return 1;
  }
  
  return 0;
}

int
_has_ape(PerlIO *infile, off_t file_size, HV *info)
{
  Buffer buf;
  uint8_t ret = 0;
  char *bptr;
  
  if ( (PerlIO_seek(infile, file_size - 160, SEEK_SET)) == -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 ( _is_ape_header(bptr) ) {
    DEBUG_TRACE("APE tag found at -160 (with ID3v1)\n");
    ret = 1;
  }
  else {
    // Look for Lyrics tag which may possibly be between APE and ID3v1
    bptr += 23;
    if ( bptr[0] == 'L' && bptr[1] == 'Y' && bptr[2] == 'R'
      && bptr[3] == 'I' && bptr[4] == 'C' && bptr[5] == 'S'
      && bptr[6] == '2' && bptr[7] == '0' && bptr[8] == '0'
    ) {
      // read Lyrics tag size, stored as a 6-digit number (!?)
      // http://www.id3.org/Lyrics3v2
      uint32_t lyrics_size = 0;
      off_t file_size = _file_size(infile);
      
      bptr -= 6;
      lyrics_size = atoi(bptr);
      
      DEBUG_TRACE("LYRICS200 tag found (size %d), adjusting APE offset (%d)\n", lyrics_size, -(160 + lyrics_size + 15));
      
      if ( (PerlIO_seek(infile, file_size - (160 + lyrics_size + 15), SEEK_SET)) == -1 ) {
        goto out;
      }
      
      DEBUG_TRACE("Seeked before Lyrics tag to %d\n", (int)PerlIO_tell(infile));
      
      buffer_clear(&buf);
      if ( !_check_buf(infile, &buf, 136, 136) ) {
        goto out;
      }
      
      if ( _is_ape_header( buffer_ptr(&buf) ) ) {
        DEBUG_TRACE("APE tag found at %d (ID3v1 + Lyricsv2)\n", -(160 + lyrics_size + 15));
        ret = 1;
        goto out;
      }
      
      // APE code will remove the lyrics_size from audio_size, but if no APE tag do it here
      if (my_hv_exists(info, "audio_size")) {
        int audio_size = SvIV(*(my_hv_fetch(info, "audio_size")));
        my_hv_store(info, "audio_size", newSVuv(audio_size - lyrics_size - 15));
        DEBUG_TRACE("Reduced audio_size value by Lyrics2 tag size %d\n", lyrics_size + 15);
      }
    }
    
    // APE tag without ID3v1 tag will be -32 bytes from end
    buffer_consume(&buf, 128);
    
    bptr = buffer_ptr(&buf);

    if ( _is_ape_header(bptr) ) {
      DEBUG_TRACE("APE tag found at -32 (no ID3v1)\n");
      ret = 1;
    }
  }
  
out:
  buffer_free(&buf);
  
  return ret;
}

// _decode_mp3_frame, based on pcutmp3 FrameHeader.decode()
int
_decode_mp3_frame(unsigned char *bptr, struct mp3frame *frame)
{
  int i;
  
  frame->header32 = GET_INT32BE(bptr);
  
  frame->mpegID             = (frame->header32 >> 19) & 3;
  frame->layerID            = (frame->header32 >> 17) & 3;
  frame->crc16_used         = (frame->header32 & 0x00010000) == 0;
  frame->bitrate_index      = (frame->header32 >> 12) & 0xF;
  frame->samplingrate_index = (frame->header32 >> 10) & 3;
  frame->padding            = (frame->header32 & 0x00000200) != 0;
  frame->private_bit_set    = (frame->header32 & 0x00000100) != 0;
  frame->mode               = (frame->header32 >> 6) & 3;
  frame->mode_extension     = (frame->header32 >> 4) & 3;
  frame->copyrighted        = (frame->header32 & 0x00000008) != 0;
  frame->original           = (frame->header32 & 0x00000004) == 0; // bit set -> copy
  frame->emphasis           = frame->header32 & 3;
  
  frame->valid = (frame->mpegID != ILLEGAL_MPEG_ID) 
    && (frame->layerID != ILLEGAL_LAYER_ID)
    && (frame->bitrate_index != 0)
    && (frame->bitrate_index != 15)
    && (frame->samplingrate_index != ILLEGAL_SR);
  
  if (!frame->valid) {
    return -1;
  }
  
  frame->samplerate = sample_rate_tbl[ frame->samplingrate_index ];
  
  if (frame->mpegID == MPEG2_ID)
    frame->samplerate >>= 1; // 16,22,48 kHz
  
  if (frame->mpegID == MPEG25_ID)
    frame->samplerate >>= 2; // 8,11,24 kHz
  
  frame->channels = (frame->mode == MODE_MONO) ? 1 : 2;
  
  frame->bitrate_kbps = bitrate_map[ frame->mpegID ][ frame->layerID ][ frame->bitrate_index ];
  
  if (frame->layerID == LAYER1_ID) {
    // layer 1: always 384 samples/frame and 4byte-slots
    frame->samples_per_frame = 384;
    frame->bytes_per_slot = 4;
  }
  else {
    // layer 2: always 1152 samples/frame
    // layer 3: MPEG1: 1152 samples/frame, MPEG2/2.5: 576 samples/frame
    frame->samples_per_frame = ((frame->mpegID == MPEG1_ID) || (frame->layerID == LAYER2_ID)) ? 1152 : 576;
    frame->bytes_per_slot = 1;
  }
  
  frame->frame_size = ((frame->bitrate_kbps * 125) * frame->samples_per_frame) / frame->samplerate;
  
  if (frame->bytes_per_slot > 1)
    frame->frame_size -= frame->frame_size % frame->bytes_per_slot;
  
  if (frame->padding)
    frame->frame_size += frame->bytes_per_slot;

  DEBUG_TRACE("Frame @%p: size=%d, %d samples, %dkbps %d/%d\n",
      bptr, frame->frame_size, frame->samples_per_frame,
      frame->bitrate_kbps, frame->samplerate, frame->channels);

  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 mp3frame frame;
  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, 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) < 4 ) {
          // ran out of data
          goto out;
        }
      
        bptr = buffer_ptr(mp3->buf);
      }

      if ( !_decode_mp3_frame( buffer_ptr(mp3->buf), &frame ) ) {
        // Found a valid frame
        frame_count++;
        bitrate_total += frame.bitrate_kbps;
        
        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 != frame.bitrate_kbps) {
            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 = frame.bitrate_kbps;
          }
        }
        
        //DEBUG_TRACE("  Frame %d: %dkbps, %dkHz\n", frame_count, frame.bitrate_kbps, frame.samplerate);

        if (frame.frame_size > buffer_len(mp3->buf)) {
          // Partial frame in buffer
          wrap_skip = frame.frame_size - buffer_len(mp3->buf);
          buffer_consume(mp3->buf, buffer_len(mp3->buf));
        }
        else {
          buffer_consume(mp3->buf, frame.frame_size);
        }
      }
      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)
{
  int i;
  unsigned char *bptr;
  int xing_offset = 4;
  
  if (mp3->first_frame->mpegID == MPEG1_ID) {
    xing_offset += mp3->first_frame->channels == 2 ? 32 : 17;
  }
  else {
    xing_offset += mp3->first_frame->channels == 2 ? 17 : 9;
  }
  
  if ( !_check_buf(mp3->infile, mp3->buf, 4 + xing_offset, MP3_BLOCK_SIZE) ) {
    return 0;
  }
  
  buffer_consume(mp3->buf, 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");
      
      mp3->xing_frame->xing_tag   = bptr[0] == 'X';
      mp3->xing_frame->info_tag   = bptr[0] == 'I';
      mp3->xing_frame->frame_size = mp3->first_frame->frame_size;
      
      if ( !_check_buf(mp3->infile, mp3->buf, 160, MP3_BLOCK_SIZE) ) {
        return 0;
      }
      
      // It's VBR if tag is Xing, and CBR if Info
      mp3->vbr = bptr[1] == 'i' ? VBR : CBR;

      buffer_consume(mp3->buf, 4);

      mp3->xing_frame->flags = buffer_get_int(mp3->buf);

      if (mp3->xing_frame->flags & XING_FRAMES) {
        mp3->xing_frame->xing_frames = buffer_get_int(mp3->buf);
      }

      if ( mp3->xing_frame->flags & XING_BYTES) {
        mp3->xing_frame->xing_bytes = buffer_get_int(mp3->buf);
      }

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

      if (mp3->xing_frame->flags & XING_QUALITY) {
        mp3->xing_frame->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' ) {
        mp3->xing_frame->lame_tag = TRUE;
        
        strncpy(mp3->xing_frame->lame_encoder_version, (char *)bptr, 9);
        bptr += 9;

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

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

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

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

        // Replay Gain, code from mpg123
        mp3->xing_frame->lame_replay_gain[0] = 0;
        mp3->xing_frame->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;

            mp3->xing_frame->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
        mp3->xing_frame->lame_abr_rate = buffer_get_char(mp3->buf);

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

        // Misc
        bptr = buffer_ptr(mp3->buf);
        mp3->xing_frame->lame_noise_shaping = bptr[0] & 0x3;
        mp3->xing_frame->lame_stereo_mode   = (bptr[0] & 0x1C) >> 2;
        mp3->xing_frame->lame_unwise        = (bptr[0] & 0x20) >> 5;
        mp3->xing_frame->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;
        mp3->xing_frame->lame_mp3gain = bptr[0] & 0x7F;
        if (sign) {
          mp3->xing_frame->lame_mp3gain *= -1;
        }
        mp3->xing_frame->lame_mp3gain_db = mp3->xing_frame->lame_mp3gain * 1.5;
*/
        buffer_consume(mp3->buf, 1);

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

        // Music Length
        mp3->xing_frame->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");
    
    mp3->xing_frame->vbri_tag = TRUE;
    mp3->vbr = VBR;
    
    if ( !_check_buf(mp3->infile, mp3->buf, 14, MP3_BLOCK_SIZE) ) {
      return 0;
    }
    
    // Skip tag and version ID
    buffer_consume(mp3->buf, 6);

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

static int
_is_mp3x_profile(mp3info *mp3)
{
  if (mp3->first_frame->layerID != LAYER3_ID)
    return 0;
    
  if (mp3->first_frame->mpegID != MPEG1_ID && mp3->first_frame->mpegID != MPEG2_ID)
    return 0;
  
  if (mp3->first_frame->samplerate != 16000
    && mp3->first_frame->samplerate != 22050
    && mp3->first_frame->samplerate != 24000)
    return 0;
  
  if (mp3->bitrate >= 8 && mp3->bitrate <= 320)
    return 1;
 
  return 0;
}

static int
_is_mp3_profile(mp3info *mp3)
{
  if (mp3->first_frame->layerID != LAYER3_ID)
    return 0;
  
  if (mp3->first_frame->mpegID != MPEG1_ID)
    return 0;
  
  if (mp3->first_frame->samplerate != 32000
    && mp3->first_frame->samplerate != 44100
    && mp3->first_frame->samplerate != 48000)
    return 0;
  
  if (mp3->bitrate >= 32 && mp3->bitrate <= 320)
    return 1;
  
  return 0;
}

mp3info *
_mp3_parse(PerlIO *infile, char *file, HV *info)
{
  unsigned char *bptr;
  char id3v1taghdr[4];

  uint32_t song_length_ms = 0;
  uint64_t total_samples = 0;
  struct mp3frame frame;
  
  bool found_first_frame = FALSE;
  
  mp3info *mp3;
  Newz(0, mp3, sizeof(mp3info), mp3info);
  Newz(0, mp3->buf, sizeof(Buffer), Buffer);
  Newz(0, mp3->first_frame, sizeof(mp3frame), mp3frame);
  Newz(0, mp3->xing_frame, sizeof(xingframe), xingframe);
  
  mp3->infile       = infile;
  mp3->file         = file;
  mp3->info         = info;
  
  mp3->file_size    = _file_size(infile);
  mp3->id3_size     = 0;
  mp3->audio_offset = 0;
  mp3->audio_size   = 0;
  mp3->bitrate      = 0;
  
  buffer_init(mp3->buf, MP3_BLOCK_SIZE);
  
  my_hv_store( info, "file_size", newSVuv(mp3->file_size) );
  
  if ( !_check_buf(mp3->infile, mp3->buf, 10, MP3_BLOCK_SIZE) ) {
    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... */
    mp3->id3_size = 10 + (bptr[6]<<21) + (bptr[7]<<14) + (bptr[8]<<7) + bptr[9];

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

    // Always seek past the ID3 tags
    _mp3_skip(mp3, mp3->id3_size);
    
    if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) {
      goto out;
    }

    mp3->audio_offset += mp3->id3_size;
  }

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

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

    if ( !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf), &frame ) ) {
      struct mp3frame frame2, frame3;
      
      // Need the whole frame to consider it valid
      if ( _check_buf(mp3->infile, mp3->buf, frame.frame_size, MP3_BLOCK_SIZE)

        // If we have enough data for the start of the next frame then
        // it must also look valid and be consistent
        && (
          !_check_buf(mp3->infile, mp3->buf, frame.frame_size + 4, MP3_BLOCK_SIZE)
          || (
               !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf) + frame.frame_size, &frame2 )
            && frame.samplerate == frame2.samplerate
            && frame.channels == frame2.channels
          )
        )

        // If we have enough data for the start of the over-next frame then
        // it must also look valid and be consistent
        && (
          !_check_buf(mp3->infile, mp3->buf, frame.frame_size + frame2.frame_size + 4, MP3_BLOCK_SIZE)
          || (
               !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf) + frame.frame_size + frame2.frame_size, &frame3 )
            && frame.samplerate == frame3.samplerate
            && frame.channels == frame3.channels
          )
        )
      ) {
        // Found a valid frame
        DEBUG_TRACE("  valid frame\n");

        found_first_frame = 1;
      }
      else {
        DEBUG_TRACE("  false sync\n");
      }
    }

    if (!found_first_frame) {
      // Not a valid frame, stray 0xFF
      DEBUG_TRACE("  invalid frame\n");
      
      buffer_consume(mp3->buf, 1);
      mp3->audio_offset++;
    }
  }

  if ( !found_first_frame ) {
    warn("Unable to find any MP3 frames in file (checked 4K): %s\n", file);
    goto out;
  }

  mp3->audio_size = mp3->file_size - mp3->audio_offset;
  
  memcpy(mp3->first_frame, &frame, sizeof(mp3frame));

  // now check for Xing/Info/VBRI/LAME headers
  if ( !_parse_xing(mp3) ) {
    goto out;
  }

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

  // Or if we have a Xing header, use it to determine bitrate
  if (!mp3->bitrate && (mp3->xing_frame->xing_frames && mp3->xing_frame->xing_bytes)) {
    float mfs = (float)frame.samplerate / ( frame.mpegID == MPEG2_ID || frame.mpegID == MPEG25_ID ? 72000. : 144000. );
    mp3->bitrate = ( mp3->xing_frame->xing_bytes / mp3->xing_frame->xing_frames * mfs );
    DEBUG_TRACE("bitrate from Xing header: %d\n", mp3->bitrate);
  }

  // Or use VBRI header
  else if (mp3->xing_frame->vbri_frames && mp3->xing_frame->vbri_bytes) {
    float mfs = (float)frame.samplerate / ( frame.mpegID == MPEG2_ID || frame.mpegID == MPEG25_ID ? 72000. : 144000. );
    mp3->bitrate = ( mp3->xing_frame->vbri_bytes / mp3->xing_frame->vbri_frames * mfs );
    DEBUG_TRACE("bitrate from VBRI header: %d\n", mp3->bitrate);
  }

  // check if last 128 bytes is ID3v1.0 or ID3v1.1 tag
  PerlIO_seek(infile, mp3->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");
      mp3->audio_size -= 128;
    }
  }

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

    if (mp3->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", frame.bitrate_kbps);
      mp3->bitrate = frame.bitrate_kbps;
    }
  }

  if (mp3->xing_frame->xing_frames) {
    total_samples = mp3->xing_frame->xing_frames * frame.samples_per_frame;
        
    if (mp3->xing_frame->lame_tag) {
      // subtract delay/padding to get accurate sample count
      total_samples -= (mp3->xing_frame->lame_encoder_delay + mp3->xing_frame->lame_encoder_padding);
    }
    
    song_length_ms = (int) ((double)(total_samples * 1000.) / (double) frame.samplerate);
  }
  else if (mp3->xing_frame->vbri_frames) {
    song_length_ms = (int) ((double)(mp3->xing_frame->vbri_frames * frame.samples_per_frame * 1000.)/
			(double) frame.samplerate);
    total_samples = mp3->xing_frame->vbri_frames * frame.samples_per_frame;
	}
  else {
    song_length_ms = (int) ((double)mp3->audio_size * 8. /
			(double)mp3->bitrate);
  }
  
  mp3->song_length_ms = song_length_ms;
  
  my_hv_store( info, "song_length_ms", newSVuv(song_length_ms) );
  my_hv_store( info, "layer", newSVuv(frame.layerID) );
  my_hv_store( info, "stereo", newSVuv(frame.channels == 2 ? 1 : 0) );
  my_hv_store( info, "samples_per_frame", newSVuv(frame.samples_per_frame) );
  my_hv_store( info, "padding", newSVuv(frame.padding) );
  my_hv_store( info, "audio_size", newSVuv(mp3->audio_size) );
  my_hv_store( info, "audio_offset", newSVuv(mp3->audio_offset) );
  my_hv_store( info, "bitrate", newSVuv( mp3->bitrate * 1000 ) );
  my_hv_store( info, "samplerate", newSVuv( frame.samplerate ) );

  if (mp3->xing_frame->xing_tag || mp3->xing_frame->info_tag) {
    if (mp3->xing_frame->xing_frames) {
      my_hv_store( info, "xing_frames", newSVuv(mp3->xing_frame->xing_frames) );
    }

    if (mp3->xing_frame->xing_bytes) {
      my_hv_store( info, "xing_bytes", newSVuv(mp3->xing_frame->xing_bytes) );
    }
    
    if (mp3->xing_frame->has_toc) {
      uint8_t i;
      AV *xing_toc = newAV();

      for (i = 0; i < 100; i++) {
        av_push( xing_toc, newSVuv(mp3->xing_frame->xing_toc[i]) );
      }

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

    if (mp3->xing_frame->xing_quality) {
      my_hv_store( info, "xing_quality", newSVuv(mp3->xing_frame->xing_quality) );
    }
  }

  if (mp3->xing_frame->vbri_tag) {
    my_hv_store( info, "vbri_delay", newSVuv(mp3->xing_frame->vbri_delay) );
    my_hv_store( info, "vbri_frames", newSVuv(mp3->xing_frame->vbri_frames) );
    my_hv_store( info, "vbri_bytes", newSVuv(mp3->xing_frame->vbri_bytes) );
    my_hv_store( info, "vbri_quality", newSVuv(mp3->xing_frame->vbri_quality) );
  }

  if (mp3->xing_frame->lame_tag) {
    my_hv_store( info, "lame_encoder_version", newSVpvn(mp3->xing_frame->lame_encoder_version, 9) );
    my_hv_store( info, "lame_tag_revision", newSViv(mp3->xing_frame->lame_tag_revision) );
    my_hv_store( info, "lame_vbr_method", newSVpv( vbr_methods[mp3->xing_frame->lame_vbr_method], 0 ) );
    my_hv_store( info, "lame_lowpass", newSViv(mp3->xing_frame->lame_lowpass) );

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

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

    my_hv_store( info, "lame_encoder_delay", newSViv(mp3->xing_frame->lame_encoder_delay) );
    my_hv_store( info, "lame_encoder_padding", newSViv(mp3->xing_frame->lame_encoder_padding) );

    my_hv_store( info, "lame_noise_shaping", newSViv(mp3->xing_frame->lame_noise_shaping) );
    my_hv_store( info, "lame_stereo_mode", newSVpv( stereo_modes[mp3->xing_frame->lame_stereo_mode], 0 ) );
    my_hv_store( info, "lame_unwise_settings", newSViv(mp3->xing_frame->lame_unwise) );
    my_hv_store( info, "lame_source_freq", newSVpv( source_freqs[mp3->xing_frame->lame_source_freq], 0 ) );

//    my_hv_store( info, "lame_mp3gain", newSViv(mp3->xing_frame->lame_mp3gain) );
//    my_hv_store( info, "lame_mp3gain_db", newSVnv(mp3->xing_frame->lame_mp3gain_db) );

    my_hv_store( info, "lame_surround", newSVpv( surround[mp3->xing_frame->lame_surround], 0 ) );

    if (mp3->xing_frame->lame_preset < 8) {
      my_hv_store( info, "lame_preset", newSVpvn( "Unknown", 7 ) );
    }
    else if (mp3->xing_frame->lame_preset <= 320) {
      my_hv_store( info, "lame_preset", newSVpvf( "ABR %d", mp3->xing_frame->lame_preset ) );
    }
    else if (mp3->xing_frame->lame_preset <= 500) {
      mp3->xing_frame->lame_preset /= 10;
      mp3->xing_frame->lame_preset -= 41;
      if ( presets_v[mp3->xing_frame->lame_preset] ) {
        my_hv_store( info, "lame_preset", newSVpv( presets_v[mp3->xing_frame->lame_preset], 0 ) );
      }
    }
    else if (mp3->xing_frame->lame_preset >= 1000 && mp3->xing_frame->lame_preset <= 1007) {
      mp3->xing_frame->lame_preset -= 1000;
      if ( presets_old[mp3->xing_frame->lame_preset] ) {
        my_hv_store( info, "lame_preset", newSVpv( presets_old[mp3->xing_frame->lame_preset], 0 ) );
      }
    }
  }
  
  if (mp3->vbr == ABR || mp3->vbr == VBR) {
    my_hv_store( info, "vbr", newSViv(1) );
  }
  
  // DLNA profile detection
  if (_is_mp3x_profile(mp3))
    my_hv_store( info, "dlna_profile", newSVpvn( "MP3X", 4 ) );
  else if (_is_mp3_profile(mp3))
    my_hv_store( info, "dlna_profile", newSVpvn( "MP3", 3 ) );
  
out:

  return mp3;
}

int
mp3_find_frame(PerlIO *infile, char *file, int offset)
{
  Buffer mp3_buf;
  unsigned char *bptr;
  unsigned int buf_size;
  struct mp3frame frame;
  int frame_offset = -1;
  HV *info = newHV();
  
  mp3info *mp3 = _mp3_parse(infile, file, info);
  
  buffer_init(&mp3_buf, MP3_BLOCK_SIZE);
  
  if (!mp3->song_length_ms)
    goto out;
  
  // (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 < mp3->audio_offset) {
      // Force offset to be at least audio_offset, so we don't end up in an ID3 tag
      frame_offset = mp3->audio_offset;
    }
    DEBUG_TRACE("find_frame: using absolute offset value %d\n", frame_offset);
  }
  else {
    if (offset >= mp3->song_length_ms) {
      goto out;
    }
    
    // Use Xing TOC if available
    if ( mp3->xing_frame->has_toc ) {
      float percent;
      uint8_t ipercent;
      uint16_t tva;
      uint16_t tvb;
      float tvx;
  
      percent = (offset * 1.0 / mp3->song_length_ms) * 100;
      ipercent = (int)percent;
  
      if (ipercent > 99)
        ipercent = 99;
      
      // Interpolate between 2 TOC points
      tva = mp3->xing_frame->xing_toc[ipercent];
      if (ipercent < 99) {
        tvb = mp3->xing_frame->xing_toc[ipercent + 1];
      }
      else {
        tvb = 256;
      }
    
      tvx = tva + (tvb - tva) * (percent - ipercent);
  
      frame_offset = (int)((1.0/256.0) * tvx * mp3->xing_frame->xing_bytes);
  
      frame_offset += mp3->audio_offset;
  
      // Don't return offset == audio_offset, because that would be the Xing frame
      if (frame_offset == mp3->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",
        mp3->song_length_ms, percent, tva, tvb, tvx, frame_offset
      );
    }
    else {
      // calculate offset using bitrate
      float bytes_per_ms = mp3->bitrate / 8.0;
    
      frame_offset = (int)(bytes_per_ms * offset);
    
      frame_offset += mp3->audio_offset;
    
      DEBUG_TRACE("find_frame: using bitrate %d, bytes_per_ms: %f, frame offset: %d\n", mp3->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 ((mp3->file_size - frame_offset) < 1000) {
    frame_offset -= 1000 - (mp3->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, &frame ) )
    ) {
      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);
  
  buffer_free(mp3->buf);
  Safefree(mp3->buf);
  Safefree(mp3->first_frame);
  Safefree(mp3->xing_frame);
  Safefree(mp3);

  return frame_offset;
}

void
_mp3_skip(mp3info *mp3, uint32_t size)
{
  if ( buffer_len(mp3->buf) >= size ) {
    buffer_consume(mp3->buf, size);
    
    DEBUG_TRACE("  skipped buffer data size %d\n", size);
  }
  else {
    PerlIO_seek(mp3->infile, size - buffer_len(mp3->buf), SEEK_CUR);
    buffer_clear(mp3->buf);
    
    DEBUG_TRACE("  seeked past %d bytes to %d\n", size, (int)PerlIO_tell(mp3->infile));
  }
}