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
 */

#include "id3.h"
#include "id3_genre.dat"
#include "id3_compat.c"
#include "id3_frametype.c"

#define NGENRES (sizeof(genre_table) / sizeof(genre_table[0]))

// Read an int from a variable number of bytes
static int
_varint(unsigned char *buf, int length)
{
  int i, b, number = 0;

  if (buf) {
    for ( i = 0; i < length; i++ ) {
      b = length - 1 - i;
      number = number | (unsigned int)( buf[i] & 0xff ) << ( 8*b );
    }
    return number;
  }
  else {
    return 0;
  }
}

int
parse_id3(PerlIO *infile, char *file, HV *info, HV *tags, uint32_t seek, off_t file_size)
{
  int err = 0;
  unsigned char *bptr;

  id3info *id3;
  Newz(0, id3, sizeof(id3info), id3info);
  Newz(0, id3->buf, sizeof(Buffer), Buffer);
  Newz(0, id3->utf8, sizeof(Buffer), Buffer);

  id3->infile = infile;
  id3->file   = file;
  id3->info   = info;
  id3->tags   = tags;
  id3->offset = seek;

  buffer_init(id3->buf, ID3_BLOCK_SIZE);

  if ( !seek ) {
    // Check for ID3v1 tag first
    PerlIO_seek(infile, file_size - 128, SEEK_SET);
    if ( !_check_buf(infile, id3->buf, 128, 128) ) {
      err = -1;
      goto out;
    }

    bptr = buffer_ptr(id3->buf);
    if (bptr[0] == 'T' && bptr[1] == 'A' && bptr[2] == 'G') {
      _id3_parse_v1(id3);
    }
  }

  // Check for ID3v2 tag
  PerlIO_seek(infile, seek, SEEK_SET);
  buffer_clear(id3->buf);

  // Read enough for header (10) + extended header size (4)
  if ( !_check_buf(infile, id3->buf, 14, ID3_BLOCK_SIZE) ) {
    err = -1;
    goto out;
  }

  bptr = buffer_ptr(id3->buf);
  if (bptr[0] == 'I' && bptr[1] == 'D' && bptr[2] == '3') {
    _id3_parse_v2(id3);
  }

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

  if (id3->utf8->alloc)
    buffer_free(id3->utf8);
  Safefree(id3->utf8);

  Safefree(id3);

  return err;
}

int
_id3_parse_v1(id3info *id3)
{
  SV *tmp = NULL;
  uint8_t read = 0;
  unsigned char *bptr;
  uint8_t comment_len;
  uint8_t genre;

  buffer_consume(id3->buf, 3); // TAG

  read = _id3_get_v1_utf8_string(id3, &tmp, 30);
  if (tmp && SvPOK(tmp) && sv_len(tmp)) {
    DEBUG_TRACE("ID3v1 title: %s\n", SvPVX(tmp));
    my_hv_store( id3->tags, ID3_FRAME_TITLE, tmp );
  }
  else {
    if (tmp) SvREFCNT_dec(tmp);
  }
  if (read < 30) {
    buffer_consume(id3->buf, 30 - read);
  }

  tmp = NULL;
  read = _id3_get_v1_utf8_string(id3, &tmp, 30);
  if (tmp && SvPOK(tmp) && sv_len(tmp)) {
    DEBUG_TRACE("ID3v1 artist: %s\n", SvPVX(tmp));
    my_hv_store( id3->tags, ID3_FRAME_ARTIST, tmp );
    tmp = NULL;
  }
  else {
    if (tmp) SvREFCNT_dec(tmp);
  }
  if (read < 30) {
    buffer_consume(id3->buf, 30 - read);
  }

  tmp = NULL;
  read = _id3_get_v1_utf8_string(id3, &tmp, 30);
  if (tmp && SvPOK(tmp) && sv_len(tmp)) {
    DEBUG_TRACE("ID3v1 album: %s\n", SvPVX(tmp));
    my_hv_store( id3->tags, ID3_FRAME_ALBUM, tmp );
    tmp = NULL;
  }
  else {
    if (tmp) SvREFCNT_dec(tmp);
  }
  if (read < 30) {
    buffer_consume(id3->buf, 30 - read);
  }

  tmp = NULL;
  read = _id3_get_v1_utf8_string(id3, &tmp, 4);
  if (tmp && SvPOK(tmp) && sv_len(tmp)) {
    DEBUG_TRACE("ID3v1 year: %s\n", SvPVX(tmp));
    my_hv_store( id3->tags, ID3_FRAME_YEAR, tmp );
    tmp = NULL;
  }
  else {
    if (tmp) SvREFCNT_dec(tmp);
  }
  if (read < 4) {
    buffer_consume(id3->buf, 4 - read);
  }

  bptr = buffer_ptr(id3->buf);
  if (bptr[28] == 0 && bptr[29] != 0) {
    // ID3v1.1 track number is present
    comment_len = 28;
    my_hv_store( id3->tags, ID3_FRAME_TRACK, newSVuv(bptr[29]) );
    my_hv_store( id3->info, "id3_version", newSVpv( "ID3v1.1", 0 ) );
  }
  else {
    comment_len = 30;
    my_hv_store( id3->info, "id3_version", newSVpv( "ID3v1", 0 ) );
  }

  tmp = NULL;
  read = _id3_get_v1_utf8_string(id3, &tmp, comment_len);
  if (tmp && SvPOK(tmp) && sv_len(tmp)) {
    AV *comment_array = newAV();
    av_push( comment_array, newSVpvn("XXX", 3) );
    av_push( comment_array, newSVpvn("", 0) );
    av_push( comment_array, tmp );
    DEBUG_TRACE("ID3v1 comment: %s\n", SvPVX(tmp));
    my_hv_store( id3->tags, ID3_FRAME_COMMENT, newRV_noinc( (SV *)comment_array ) );
    tmp = NULL;
  }
  else {
    if (tmp) SvREFCNT_dec(tmp);
  }
  if (read < 30) {
    buffer_consume(id3->buf, 30 - read);
  }

  genre = buffer_get_char(id3->buf);
  if (genre < NGENRES) {
    char const *genre_string = _id3_genre_index(genre);
    my_hv_store( id3->tags, ID3_FRAME_GENRE, newSVpv(genre_string, 0) );
  }
  else if (genre < 255) {
    my_hv_store( id3->tags, ID3_FRAME_GENRE, newSVpvf("Unknown/%d", genre) );
  }

  return 1;
}

int
_id3_parse_v2(id3info *id3)
{
  int ret = 1;
  unsigned char *bptr;

  // Verify we have a valid tag
  bptr = buffer_ptr(id3->buf);
  if ( !(
    bptr[3] < 0xff && bptr[4] < 0xff &&
    bptr[6] < 0x80 && bptr[7] < 0x80 && bptr[8] < 0x80 && bptr[9] < 0x80
  ) ) {
    PerlIO_printf(PerlIO_stderr(), "Invalid ID3v2 tag in %s\n", id3->file);
    return 0;
  }

  buffer_consume(id3->buf, 3); // ID3

  id3->version_major = buffer_get_char(id3->buf);
  id3->version_minor = buffer_get_char(id3->buf);
  id3->flags         = buffer_get_char(id3->buf);
  id3->size          = 10 + buffer_get_syncsafe(id3->buf, 4);

  id3->size_remain = id3->size - 10;

  if (id3->flags & ID3_TAG_FLAG_FOOTERPRESENT) {
    id3->size += 10;
  }

  DEBUG_TRACE("Parsing ID3v2.%d.%d tag, flags %x, size %d\n", id3->version_major, id3->version_minor, id3->flags, id3->size);

  if (id3->flags & ID3_TAG_FLAG_UNSYNCHRONISATION) {
    if (id3->version_major < 4) {
      // It's unclear but the v2.4.0-changes document seems to say that v2.4 should
      // ignore the tag-level unsync flag and only worry about frame-level unsync

      // For v2.2/v2.3, unsync the entire tag.  This is unfortunate due to
      // increased memory usage but the only way to do it, as frame size values only
      // indicate the post-unsync size, so it's not possible to unsync each frame individually
      // tested with v2.3-unsync.mp3
      if ( !_check_buf(id3->infile, id3->buf, id3->size, id3->size) ) {
        ret = 0;
        goto out;
      }

      id3->size_remain = _id3_deunsync( buffer_ptr(id3->buf), id3->size );

      DEBUG_TRACE("    Un-synchronized tag, new_size %d\n", id3->size_remain);
    }
    else {
      DEBUG_TRACE("  Ignoring v2.4 tag un-synchronize flag\n");
    }
  }

  if (id3->flags & ID3_TAG_FLAG_EXTENDEDHEADER) {
    uint32_t ehsize;

    // If the tag is v2.2, this bit is actually the compression bit and the tag should be ignored
    if (id3->version_major == 2) {
      ret = 0;
      goto out;
    }

    // tested with v2.3-ext-header.mp3

    // We don't care about the value of the extended flags or CRC, so just read the size and skip it
    ehsize = buffer_get_int(id3->buf);

    // ehsize may be invalid, tested with v2.3-ext-header-invalid.mp3
    if (ehsize > id3->size_remain - 4) {
      warn("Error: Invalid ID3 extended header size (%s)\n", id3->file);
      ret = 0;
      goto out;
    }

    DEBUG_TRACE("  Skipping extended header, size %d\n", ehsize);

    if ( !_check_buf(id3->infile, id3->buf, ehsize, ID3_BLOCK_SIZE) ) {
      ret = 0;
      goto out;
    }
    buffer_consume(id3->buf, ehsize);

    id3->size_remain -= ehsize + 4;
  }

  // Parse frames
  while (id3->size_remain > 0) {
    //DEBUG_TRACE("    remain: %d\n", id3->size_remain);
    if ( !_id3_parse_v2_frame(id3) ) {
      break;
    }
  }

  if (id3->version_major < 4) {
    // map old year/date/time (TYER/TDAT/TIME) frames to TDRC
    // tested in v2.3-xsop.mp3
    _id3_convert_tdrc(id3);
  }

  // Set id3_version info element, which contains all tag versions found
  {
    SV *version = newSVpvf( "ID3v2.%d.%d", id3->version_major, id3->version_minor );

    if ( my_hv_exists(id3->info, "id3_version") ) {
      SV **entry = my_hv_fetch(id3->info, "id3_version");
      if (entry != NULL) {
        sv_catpv( version, ", " );
        sv_catsv( version, *entry );
      }
    }

    my_hv_store( id3->info, "id3_version", version );
  }

out:
  return ret;
}

int
_id3_parse_v2_frame(id3info *id3)
{
  int ret = 1;
  char id[5];
  uint16_t flags = 0;
  uint32_t size  = 0;
  uint32_t decoded_size = 0;
  uint32_t unsync_extra = 0;
  id3_frametype const *frametype;
  Buffer *tmp_buf = 0;

  // If the frame is compressed, it will be decompressed here
  Buffer *decompressed = 0;

  // tag_data_safe flag is used if skipping artwork and artwork is not raw image data (needs unsync)
  id3->tag_data_safe = 1;

  if ( !_check_buf(id3->infile, id3->buf, 10, ID3_BLOCK_SIZE) ) {
    ret = 0;
    goto out;
  }

  if (id3->version_major == 2) {
    // v2.2
    id3_compat const *compat;

    // Read 3-letter id
    buffer_get(id3->buf, &id, 3);
    id[3] = 0;

    if (id[0] == 0) {
      // padding
      DEBUG_TRACE("  Found start of padding, aborting\n");
      ret = 0;
      goto out;
    }

    size = buffer_get_int24(id3->buf);

    DEBUG_TRACE("  %s, size %d\n", id, size);

    // map 3-char id to 4-char id
    compat = _id3_compat_lookup((char *)&id, 3);
    if (compat && compat->equiv) {
      strncpy(id, compat->equiv, 4);
      id[4] = 0;

      DEBUG_TRACE("    compat -> %s\n", id);
    }
    else {
      // no compat mapping (obsolete), prepend 'Y' to id
      id[4] = 0;
      id[3] = id[2];
      id[2] = id[1];
      id[1] = id[0];
      id[0] = 'Y';

      DEBUG_TRACE("    obsolete/unknown -> %s\n", id);
    }

    id3->size_remain -= 6;

    if (size > id3->size_remain) {
      DEBUG_TRACE("    frame size too big, aborting\n");
      ret = 0;
      goto out;
    }
  }
  else {
    // Read 4-letter id
    buffer_get(id3->buf, &id, 4);
    id[4] = 0;

    if (id[0] == 0) {
      // padding
      DEBUG_TRACE("  Found start of padding, aborting\n");
      ret = 0;
      goto out;
    }

    id3->size_remain -= 4;

    if (id3->version_major == 3) {
      // v2.3
      id3_compat const *compat;

      size  = buffer_get_int(id3->buf);
      flags = buffer_get_short(id3->buf);

      DEBUG_TRACE("  %s, frame flags %x, size %d\n", id, flags, size);

      // map to v2.4 id
      if (id[3] == ' ') {
        // iTunes writes bad frame IDs such as 'TSA ', these should be run through compat
        // as 3-char frames
        compat = _id3_compat_lookup((char *)&id, 3);
      }
      else {
        compat = _id3_compat_lookup((char *)&id, 4);
      }
      if (compat && compat->equiv) {
        strncpy(id, compat->equiv, 4);
        id[4] = 0;

        DEBUG_TRACE("    compat -> %s\n", id);
      }

      id3->size_remain -= 6;

      if (size > id3->size_remain) {
        DEBUG_TRACE("    frame size too big, aborting\n");
        ret = 0;
        goto out;
      }

      if (flags & ID3_FRAME_FLAG_V23_COMPRESSION) {
        // tested with v2.3-compressed-frame.mp3
        decoded_size = buffer_get_int(id3->buf);
        id3->size_remain -= 4;
        size -= 4;
      }

      if (flags & ID3_FRAME_FLAG_V23_ENCRYPTION) {
        // tested with v2.3-encrypted-frame.mp3
#ifdef AUDIO_SCAN_DEBUG
        DEBUG_TRACE("    encrypted, method %d\n", buffer_get_char(id3->buf));
#else
        buffer_consume(id3->buf, 1);
#endif

        id3->size_remain--;
        size--;

        DEBUG_TRACE("    skipping encrypted frame\n");
        _id3_skip(id3, size);
        id3->size_remain -= size;
        goto out;
      }

      if (flags & ID3_FRAME_FLAG_V23_GROUPINGIDENTITY) {
        // tested with v2.3-group-id.mp3
#ifdef AUDIO_SCAN_DEBUG
        DEBUG_TRACE("    group_id %d\n", buffer_get_char(id3->buf));
#else
        buffer_consume(id3->buf, 1);
#endif

        id3->size_remain--;
        size--;
      }

      // Perform decompression if necessary after all optional extra bytes have been read
      // XXX need test for compressed + unsync
      if (flags & ID3_FRAME_FLAG_V23_COMPRESSION && decoded_size) {
        unsigned long tmp_size;

        if ( !_check_buf(id3->infile, id3->buf, size, ID3_BLOCK_SIZE) ) {
          ret = 0;
          goto out;
        }

        DEBUG_TRACE("    decompressing, decoded_size %d\n", decoded_size);

        Newz(0, decompressed, sizeof(Buffer), Buffer);
        buffer_init(decompressed, decoded_size);

        tmp_size = decoded_size;
        if (
          uncompress(buffer_ptr(decompressed), &tmp_size, buffer_ptr(id3->buf), size) != Z_OK
          ||
    	    tmp_size != decoded_size
    	  ) {
          DEBUG_TRACE("    unable to decompress frame\n");
          buffer_free(decompressed);
          Safefree(decompressed);
          decompressed = 0;
        }
        else {
          // Hack buffer so it knows we've added data directly
          decompressed->end = decoded_size;
        }
      }
    }
    else {
      // v2.4

      // iTunes writes non-syncsafe length integers, check for this here
      if ( _varint(buffer_ptr(id3->buf), 4) & 0x80 ) {
        size = buffer_get_int(id3->buf);
        DEBUG_TRACE("    found non-syncsafe iTunes size for %s, size adjusted to %d\n", id, size);
      }
      else {
        size = buffer_get_syncsafe(id3->buf, 4);
      }

      flags = buffer_get_short(id3->buf);

      id3->size_remain -= 6;

      DEBUG_TRACE("  %s, frame flags %x, size %d\n", id, flags, size);

      if (size > id3->size_remain) {
        DEBUG_TRACE("    frame size too big, aborting\n");
        ret = 0;
        goto out;
      }

      // iTunes writes bad frame IDs such as 'TSA ', these should be run through compat
      // as 3-char frames
      if (id[3] == ' ') {
        id3_compat const *compat;
        compat = _id3_compat_lookup((char *)&id, 3);
        if (compat && compat->equiv) {
          strncpy(id, compat->equiv, 4);
          id[4] = 0;

          DEBUG_TRACE("    bad iTunes v2.4 tag, compat -> %s\n", id);
        }
      }

      if (flags & ID3_FRAME_FLAG_V24_GROUPINGIDENTITY) {
        // tested with v2.4-group-id.mp3
#ifdef AUDIO_SCAN_DEBUG
        DEBUG_TRACE("    group_id %d\n", buffer_get_char(id3->buf));
#else
        buffer_consume(id3->buf, 1);
#endif
        id3->size_remain--;
        size--;
      }

      if (flags & ID3_FRAME_FLAG_V24_ENCRYPTION) {
        // tested with v2.4-encrypted-frame.mp3
#ifdef AUDIO_SCAN_DEBUG
        DEBUG_TRACE("    encrypted, method %d\n", buffer_get_char(id3->buf));
#else
        buffer_consume(id3->buf, 1);
#endif

        id3->size_remain--;
        size--;

        DEBUG_TRACE("    skipping encrypted frame\n");
        _id3_skip(id3, size);
        id3->size_remain -= size;
        goto out;
      }

      if (flags & ID3_FRAME_FLAG_V24_DATALENGTHINDICATOR) {
        decoded_size = buffer_get_syncsafe(id3->buf, 4);
        id3->size_remain -= 4;
        size -= 4;

        DEBUG_TRACE("    data length indicator, size %d\n", decoded_size);
      }

      if (flags & ID3_FRAME_FLAG_V24_UNSYNCHRONISATION) {
        // Special case, do not unsync an APIC frame if not reading artwork,
        // FF's are not likely to appear in the part we care about anyway
        if ( !strcmp(id, "APIC") && _env_true("AUDIO_SCAN_NO_ARTWORK") ) {
          DEBUG_TRACE("    Would un-synchronize APIC frame, but ignoring because of AUDIO_SCAN_NO_ARTWORK\n");

          // Reset decoded_size to 0 since we aren't actually decoding.
          // XXX this would break if we have a compressed + unsync APIC frame but not very likely in the real world
          decoded_size = 0;

          id3->tag_data_safe = 0;
        }
        else {
          // tested with v2.4-unsync.mp3
          if ( !_check_buf(id3->infile, id3->buf, size, ID3_BLOCK_SIZE) ) {
            ret = 0;
            goto out;
          }

          decoded_size = _id3_deunsync( buffer_ptr(id3->buf), size );

          unsync_extra = size - decoded_size;

          DEBUG_TRACE("    Un-synchronized frame, new_size %d\n", decoded_size);
        }
      }

      if (flags & ID3_FRAME_FLAG_V24_COMPRESSION) {
        // tested with v2.4-compressed-frame.mp3
        // XXX need test for compressed + unsync
        unsigned long tmp_size;

        if ( !_check_buf(id3->infile, id3->buf, size, ID3_BLOCK_SIZE) ) {
          ret = 0;
          goto out;
        }

        DEBUG_TRACE("    decompressing\n");

        Newz(0, decompressed, sizeof(Buffer), Buffer);
        buffer_init(decompressed, decoded_size);

        tmp_size = decoded_size;
        if (
          uncompress(buffer_ptr(decompressed), &tmp_size, buffer_ptr(id3->buf), size) != Z_OK
          ||
    	    tmp_size != decoded_size
    	  ) {
          DEBUG_TRACE("    unable to decompress frame\n");
          buffer_free(decompressed);
          Safefree(decompressed);
          decompressed = 0;
        }
        else {
          // Hack buffer so it knows we've added data directly
          decompressed->end = decoded_size;
        }
      }
    }
  }

  // Special case, completely skip XHD3 frame (mp3HD) as it will be large
  // Also skip NCON, a large tag written by MusicMatch
  if ( !strcmp(id, "XHD3") || !strcmp(id, "NCON") ) {
    DEBUG_TRACE("    skipping large binary %s frame\n", id);
    _id3_skip(id3, size);
    id3->size_remain -= size;
    goto out;
  }

  frametype = _id3_frametype_lookup(id, 4);
  if (frametype == 0) {
    switch ( id[0] ) {
    case 'T':
      frametype = &id3_frametype_text;
      break;

    case 'W':
      frametype = &id3_frametype_url;
      break;

    case 'X':
    case 'Y':
    case 'Z':
      frametype = &id3_frametype_experimental;
      break;

    default:
      frametype = &id3_frametype_unknown;
      break;
    }
  }

#ifdef AUDIO_SCAN_DEBUG
  {
    int i;
    DEBUG_TRACE("    nfields %d:", frametype->nfields);
    for (i = 0; i < frametype->nfields; ++i) {
      DEBUG_TRACE(" %d", frametype->fields[i]);
    }
    DEBUG_TRACE("\n");
  }
#endif

  // If frame was compressed, temporarily set the id3 buffer to use the decompressed buffer
  if (decompressed) {
    tmp_buf  = id3->buf;
    id3->buf = decompressed;
  }

  if ( !_id3_parse_v2_frame_data(id3, (char *)&id, decoded_size ? decoded_size : size, frametype) ) {
    DEBUG_TRACE("    error parsing frame, aborting\n");
    ret = 0;
    goto out;
  }

  if (id3->size_remain > size) {
    id3->size_remain -= size;
  }
  else {
    id3->size_remain = 0;
  }

  // Consume extra bytes if we had to unsync this frame
  if (unsync_extra) {
    DEBUG_TRACE("    consuming extra bytes after unsync: %d\n", unsync_extra);
    buffer_consume(id3->buf, unsync_extra);
  }

out:
  if (decompressed) {
    // Reset id3 buffer and consume rest of compressed frame
    id3->buf = tmp_buf;
    buffer_consume(id3->buf, size);

    buffer_free(decompressed);
    Safefree(decompressed);
  }

  return ret;
}

int
_id3_parse_v2_frame_data(id3info *id3, char const *id, uint32_t size, id3_frametype const *frametype)
{
  int ret = 1;
  uint32_t read = 0;
  int8_t encoding = -1;

  uint8_t buffer_art = ( !strcmp(id, "APIC") ) ? 1 : 0;
  uint8_t skip_art   = ( buffer_art && _env_true("AUDIO_SCAN_NO_ARTWORK") ) ? 1 : 0;

  // Bug 16703, a completely empty frame is against the rules, skip it
  if (!size)
    return 1;

  if (skip_art) {
    // Only buffer enough for the APIC header fields, this is only a rough guess
    // because the description could technically be very long
    if ( !_check_buf(id3->infile, id3->buf, 128, ID3_BLOCK_SIZE) ) {
      return 0;
    }
    DEBUG_TRACE("    partial read due to AUDIO_SCAN_NO_ARTWORK\n");
  }
  else {
    // Use a special buffering mode for binary artwork, to avoid
    // using 2x the memory of the APIC frame (once for buffer, once for SV)
    if (buffer_art) {
      // Buffer enough for encoding/MIME/picture type/description
      if ( !_check_buf(id3->infile, id3->buf, 128, ID3_BLOCK_SIZE) ) {
        return 0;
      }
    }
    else {
      // Buffer the entire frame
      if ( !_check_buf(id3->infile, id3->buf, size, ID3_BLOCK_SIZE) ) {
        return 0;
      }
    }
  }

  if ( frametype->fields[0] == ID3_FIELD_TYPE_TEXTENCODING ) {
    // many frames have an encoding byte, read it here
    encoding = buffer_get_char(id3->buf);
    read++;
    DEBUG_TRACE("    encoding: %d\n", encoding);

    if (encoding < 0 || encoding > 3) {
      DEBUG_TRACE("    invalid encoding, skipping frame\n");
      goto out;
    }
  }

  // Special handling for TXXX/WXXX frames
  if ( !strcmp(id, "TXXX") || !strcmp(id, "WXXX") ) {
    // Read key and uppercase it
    SV *key   = NULL;
    SV *value = NULL;

    read += _id3_get_utf8_string(id3, &key, size - read, encoding);

    if (key != NULL && SvPOK(key) && sv_len(key)) {
      upcase(SvPVX(key));

      // Read value
      if (frametype->fields[2] == ID3_FIELD_TYPE_LATIN1) {
        // WXXX frames have a latin1 value field regardless of encoding byte
        encoding = ISO_8859_1;
      }

      read += _id3_get_utf8_string(id3, &value, size - read, encoding);

      // (T|W)XXX frames don't support multiple strings separated by nulls, even in v2.4

      // Only one tag per unique key value is allowed, that's why there is no array support here
      if (value != NULL && SvPOK(value) && sv_len(value)) {
        my_hv_store_ent( id3->tags, key, value );
      }
      else {
        my_hv_store_ent( id3->tags, key, &PL_sv_undef );
        if (value) SvREFCNT_dec(value);
      }
    }
    else {
      DEBUG_TRACE("    invalid/empty (T|W)XXX key, skipping frame\n");
    }

    if (key) SvREFCNT_dec(key);
  }

  // Special handling for TCON genre frame
  else if ( !strcmp(id, "TCON") ) {
    AV *genres = newAV();
    char *sptr, *end, *tmp;

    while (read < size) {
      SV *value  = NULL;

      // v2.4 handles multiple genres using null char separators (or $00 $00 in UTF-16),
      // this is handled by _id3_get_utf8_string
      read += _id3_get_utf8_string(id3, &value, size - read, encoding);
      if (value != NULL && SvPOK(value)) {
        sptr = SvPVX(value);

        // Test if the string contains only a number,
        // strtol will set tmp to end in this case
        end = sptr + sv_len(value);
        strtol(sptr, &tmp, 0);

        if ( tmp == end ) {
          // Convert raw number to genre string
          av_push( genres, newSVpv( _id3_genre_name((char *)sptr), 0 ) );

          // value as an SV won't be used, must drop refcnt
          SvREFCNT_dec(value);
        }
        else if ( *sptr == '(' ) {
          // Handle (26), (26)Ambient, etc, only the number portion will be read

          if (id3->version_major < 4) {
            // v2.2/v2.3 handle multiple genres using parens for some reason, i.e. (51)(39) or (55)(Text)
            char *ptr = sptr;
            char *end = sptr + sv_len(value);

            while (end - ptr > 0) {
              if ( *ptr++ == '(' ) {
                char *paren = strchr(ptr, ')');
                if (paren == NULL)
                  paren = end;

                if ( isdigit(*ptr) || !strncmp((char *)ptr, "RX", 2) || !strncmp((char *)ptr, "CR", 2) ) {
                  av_push( genres, newSVpv( _id3_genre_name((char *)ptr), 0 ) );
                }
                else {
                  // Handle text within parens
                  av_push( genres, newSVpvn(ptr,  paren - ptr) );
                }
                ptr = paren;
              }
            }
          }
          else {
            // v2.4, the (51) method is no longer valid but we will support it anyway
            sptr++;
            if ( isdigit(*sptr) || !strncmp(sptr, "RX", 2) || !strncmp(sptr, "CR", 2) ) {
              av_push( genres, newSVpv( _id3_genre_name((char *)sptr), 0 ) );
            }
            else {
              av_push( genres, newSVpv( (char *)sptr, 0 ) );
            }
          }

          // value as an SV won't be used, must drop refcnt
          SvREFCNT_dec(value);
        }
        else {
          // Support raw RX/CR value
          if ( !strncmp(sptr, "RX", 2) || !strncmp(sptr, "CR", 2) ) {
            av_push( genres, newSVpv( _id3_genre_name((char *)sptr), 0 ) );

            // value as an SV won't be used, must drop refcnt
            SvREFCNT_dec(value);
          }
          else {
            // Store plain text genre
            av_push( genres, value );
          }
        }
      }
    }

    if (av_len(genres) > 0) {
      my_hv_store( id3->tags, id, newRV_noinc( (SV *)genres ) );
    }
    else if (av_len(genres) == 0) {
      my_hv_store( id3->tags, id, av_shift(genres) );
      SvREFCNT_dec(genres);
    }
    else {
      SvREFCNT_dec(genres);
    }
  }

  // 1-field frames: MCDI, PCNT, SEEK (unsupported), T* (text), W* (url), unknown
  // and 2-field frames where the first field is encoding
  // are mapped to plain hash entries
  else if (
    frametype->nfields == 1 ||
    (frametype->nfields == 2 && frametype->fields[0] == ID3_FIELD_TYPE_TEXTENCODING)
  ) {
    int i = frametype->nfields - 1;
    AV *array = NULL;
    SV *value = NULL;
    int count = 0;

    switch ( frametype->fields[i] ) {
      case ID3_FIELD_TYPE_LATIN1: // W* frames
        read += _id3_get_utf8_string(id3, &value, size - read, ISO_8859_1);
        if (value != NULL && SvPOK(value))
          my_hv_store( id3->tags, id, value );
        break;

      case ID3_FIELD_TYPE_STRINGLIST: // T* frames
        // XXX technically in v2.2/v2.3 we should ignore multiple strings separated by nulls, but
        // allowing it is fine I think
        while (read < size) {
          if (count++ == 1 && value != NULL) {
            // we're reading the second string in the list, move first value to new array
            array = newAV();
            av_push(array, value);
          }
          value = NULL;

          read += _id3_get_utf8_string(id3, &value, size - read, encoding);

          if (array != NULL && value != NULL && SvPOK(value)) {
            // second+ string, add to array
            // Bug 16452, do not add a null string
            if (sv_len(value) > 0)
              av_push(array, value);
          }
        }

        if (array != NULL) {
          if (av_len(array) == 0) {
            // Handle the case where we have multiple empty strings leaving an array of 1
            my_hv_store( id3->tags, id, av_shift(array) );
            SvREFCNT_dec(array);
          }
          else {
            my_hv_store( id3->tags, id, newRV_noinc( (SV *)array ) );
          }
        }
        else if (value != NULL && SvPOK(value)) {
          my_hv_store( id3->tags, id, value );
        }
        break;

      case ID3_FIELD_TYPE_INT32: // SEEK (unsupported, XXX need test)
        my_hv_store( id3->tags, id, newSViv( buffer_get_int(id3->buf) ) );
        read += 4;
        break;

      case ID3_FIELD_TYPE_INT32PLUS: // PCNT
        my_hv_store( id3->tags, id, newSViv( _varint( buffer_ptr(id3->buf), size - read ) ) );
        buffer_consume(id3->buf, size - read);
        read = size;
        break;

      case ID3_FIELD_TYPE_BINARYDATA: // unknown/obsolete frames
        // Special handling for RVA(D), tested in v2.2-itunes81.mp3, v2.3-itunes81.mp3
        if ( !strcmp(id, "RVAD") ) {
          read += _id3_parse_rvad(id3, id, size - read);
        }

        // Special handling for RGAD (non-standard replaygain frame), tested in v2.3-rgad.mp3
        // Based on some code found at http://getid3.sourceforge.net/source/module.tag.id3v2.phps
        else if ( !strcmp(id, "RGAD") ) {
          read += _id3_parse_rgad(id3);
        }

        // Other unknown binary data
        else {
          // Y* obsolete frames
          my_hv_store( id3->tags, id, newSVpvn( buffer_ptr(id3->buf), size - read ) );
          buffer_consume(id3->buf, size - read);
          read = size;
        }
        break;

      default:
        // XXX
        warn("   !!! unhandled field type %d\n", frametype->fields[i]);
        buffer_consume(id3->buf, size - read);
        read += size - read;
        break;
    }
  }

  // 2+ field frames are mapped to arrayrefs:
  // The following frames have tests:
  // ETCO, UFID, USLT, SYLT, COMM, RVA2, APIC, GEOB, POPM, LINK, PRIV
  //
  // XXX The following frames need tests:
  // MLLT, SYTC, EQU2, RVRB, AENC, POSS, USER, OWNE,
  // COMR, ENCR, GRID, SIGN, ASPI, LINK (v2.4)
  else {
    int i = 0;
    AV *framedata = newAV();

    // If we read an initial encoding byte, start at field 2
    if (encoding >= 0)
      i = 1;

    for (; i < frametype->nfields; i++) {
      SV *value = NULL;

      switch ( frametype->fields[i] ) {
        case ID3_FIELD_TYPE_LATIN1:
          // Special case, fix v2.2 PIC frame fields as they don't match APIC
          // This is a rather hackish place to put this, but there's not really any other place
          if ( id3->version_major == 2 && !strcmp(id, "APIC") ) {
            av_push( framedata, newSVpvn( buffer_ptr(id3->buf), 3 ) );
            buffer_consume(id3->buf, 3);
            read += 3;
            DEBUG_TRACE("    PIC image format, read %d\n", read);
          }
          else {
            read += _id3_get_utf8_string(id3, &value, size - read, ISO_8859_1);
            if (value != NULL && SvPOK(value))
              av_push( framedata, value );
          }
          break;

        // ID3_FIELD_TYPE_LATIN1FULL - not used

        case ID3_FIELD_TYPE_LATIN1LIST: // LINK
          while (read < size) {
            read += _id3_get_utf8_string(id3, &value, size - read, ISO_8859_1);
            if (value != NULL && SvPOK(value))
              av_push( framedata, value );
            value = NULL;
            DEBUG_TRACE("    latin1list, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_STRING:
          read += _id3_get_utf8_string(id3, &value, size - read, encoding);
          if (value != NULL && SvPOK(value)) {
            av_push( framedata, value );
            DEBUG_TRACE("    string, read %d: %s\n", read, SvPVX(value));
          }
          else {
            av_push( framedata, &PL_sv_undef );
            if (value) SvREFCNT_dec(value);
          }
          break;

        case ID3_FIELD_TYPE_STRINGFULL: // USLT, COMM, read entire string until end of frame
        {
          SV *tmp = newSVpvn( "", 0 );
          while (read < size) {
            read += _id3_get_utf8_string(id3, &value, size - read, encoding);
            if (value != NULL && SvPOK(value)) {
              sv_catsv( tmp, value );
              SvREFCNT_dec(value);
            }
            value = NULL;
          }
          av_push( framedata, tmp );
          DEBUG_TRACE("    stringfull, read %d: %s\n", read, SvPVX(tmp));
          break;
        }

        // ID3_FIELD_TYPE_STRINGLIST - only used for text frames, handled above

        case ID3_FIELD_TYPE_LANGUAGE: // USLT, SYLT, COMM, USER, 3-byte language code
          if (size - read >= 3) {
            av_push( framedata, newSVpvn( buffer_ptr(id3->buf), 3 ) );
            buffer_consume(id3->buf, 3);
            read += 3;
            DEBUG_TRACE("    language, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_FRAMEID: // LINK, 3-byte frame id (v2.3, must be a bug in the spec?),
                                     // 4-byte frame id (v2.4) XXX need test
        {
          uint8_t len = (id3->version_major == 3) ? 3 : 4;
          if (size - read >= len) {
            av_push( framedata, newSVpvn( buffer_ptr(id3->buf), len ) );
            buffer_consume(id3->buf, len);
            read += len;
            DEBUG_TRACE("    frameid, read %d\n", read);
          }
          break;
        }

        case ID3_FIELD_TYPE_DATE: // OWNE, COMR, XXX need test, YYYYMMDD
          if (size - read >= 8) {
            av_push( framedata, newSVpvn( buffer_ptr(id3->buf), 8 ) );
            buffer_consume(id3->buf, 8);
            read += 8;
            DEBUG_TRACE("    date, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_INT8: // ETCO, MLLT, SYTC, SYLT, EQU2, RVRB, APIC,
                                  // POPM, RBUF, POSS, COMR, ENCR, GRID, SIGN, ASPI
          if (size - read >= 1) {
            av_push( framedata, newSViv( buffer_get_char(id3->buf) ) );
            read += 1;
            DEBUG_TRACE("    int8, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_INT16: // MLLT, RVRB, AENC, ASPI
          if (size - read >= 2) {
            av_push( framedata, newSViv( buffer_get_short(id3->buf) ) );
            read += 2;
            DEBUG_TRACE("    int16, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_INT24: // MLLT, RBUF
          if (size - read >= 3) {
            av_push( framedata, newSViv( buffer_get_int24(id3->buf) ) );
            read += 3;
            DEBUG_TRACE("    int24, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_INT32: // RBUF, SEEK, ASPI
          if (size - read >= 4) {
            av_push( framedata, newSViv( buffer_get_int(id3->buf) ) );
            read += 4;
            DEBUG_TRACE("    int32, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_INT32PLUS: // POPM
          if (size - read >= 4) {
            av_push( framedata, newSViv( _varint( buffer_ptr(id3->buf), size - read ) ) );
            buffer_consume(id3->buf, size - read);
            read = size;
            DEBUG_TRACE("    int32plus, read %d\n", read);
          }
          break;

        case ID3_FIELD_TYPE_BINARYDATA: // ETCO, MLLT, SYTC, SYLT, RVA2, EQU2, APIC,
                                        // GEOB, AENC, POSS, COMR, ENCR, GRID, PRIV, SIGN, ASPI
          // Special handling for APIC tags when in skip_art mode
          if (skip_art) {
            av_push( framedata, newSVuv(size - read) );

            // Record offset of APIC image data too, unless the data needs to be unsynchronized or is empty
            if (id3->tag_data_safe && (size - read) > 0)
              av_push( framedata, newSVuv(id3->offset + (id3->size - id3->size_remain) + read) );

            _id3_skip(id3, size - read);
            read = size;
          }

          // Special buffering mode for APIC data, avoids a large buffer allocation
          else if (buffer_art) {
            uint32_t remain = size - read;
            uint32_t chunk_size;
            SV *artwork = newSVpv("", 0);

            while (read < size) {
              if ( !_check_buf(id3->infile, id3->buf, 1, ID3_BLOCK_SIZE) ) {
                return 0;
              }

              chunk_size = remain < buffer_len(id3->buf) ? remain : buffer_len(id3->buf);

              read += chunk_size;
              remain -= chunk_size;

              sv_catpvn( artwork, buffer_ptr(id3->buf), chunk_size );
              buffer_consume(id3->buf, chunk_size);

              DEBUG_TRACE("    buffered %d bytes of APIC data (remaining %d)\n", chunk_size, remain);
            }

            av_push( framedata, artwork );
          }

          // Special handling for RVA2 tags
          else if ( !strcmp(id, "RVA2") ) {
            read += _id3_parse_rva2(id3, size, framedata);
          }

          // Special handling for SYLT tags
          else if ( !strcmp(id, "SYLT") ) {
            read += _id3_parse_sylt(id3, encoding, size - read, framedata);
          }

          // Special handling for ETCO tags
          else if ( !strcmp(id, "ETCO") ) {
            read += _id3_parse_etco(id3, size - read, framedata);
          }

          // All other binary frames, copy as-is
          else {
            if (size - read > 1) {
              av_push( framedata, newSVpvn( buffer_ptr(id3->buf), size - read ) );
              buffer_consume(id3->buf, size - read);
              read = size;
              DEBUG_TRACE("    binarydata, read %d\n", read);
            }
          }
          break;

        default:
          break;
      }
    }

    _id3_set_array_tag(id3, id, framedata);
  }

out:
  if (read < size) {
    buffer_consume(id3->buf, size - read);
    DEBUG_TRACE("    !!! consuming extra bytes in frame: %d\n", size - read);
  }

  return ret;
}

void
_id3_set_array_tag(id3info *id3, char const *id, AV *framedata)
{
  if ( av_len(framedata) != -1 ) {
    if ( my_hv_exists( id3->tags, id ) ) {
      // If tag already exists, move it to an arrayref
      SV **entry = my_hv_fetch( id3->tags, id );
      if (entry != NULL) {
        if ( SvTYPE( SvRV(*entry) ) == SVt_PV ) {
          // A normal string entry, convert to array
	  AV *ref = newAV();

          // XXX need test, this may be illegal because you can't have multiple duplicate frames?
          DEBUG_TRACE("   !!! converting normal string tag to array\n");

          av_push( ref, *entry );
          av_push( ref, newRV_noinc( (SV *)framedata ) );
          my_hv_store( id3->tags, id, newRV_noinc( (SV *)ref ) );
        }
        else if ( SvTYPE( SvRV(*entry) ) == SVt_PVAV ) {
          // If type of first item is array, add new item to entry
          SV **first = av_fetch( (AV *)SvRV(*entry), 0, 0 );
          if ( first == NULL || ( SvROK(*first) && SvTYPE( SvRV(*first) ) == SVt_PVAV ) ) {
            av_push( (AV *)SvRV(*entry), newRV_noinc( (SV *)framedata ) );
          }
          else {
            AV *ref = newAV();
            av_push( ref, SvREFCNT_inc(*entry) );
            av_push( ref, newRV_noinc( (SV *)framedata) );
            my_hv_store( id3->tags, id, newRV_noinc( (SV *)ref ) );
          }
        }
      }
    }
    else {
      my_hv_store( id3->tags, id, newRV_noinc( (SV *)framedata ) );
    }
  }
  else {
    SvREFCNT_dec(framedata);
  }
}

// Read a latin1 or UTF-8 string from an ID3v1 tag
// This function handles trimming spaces off the end
uint32_t
_id3_get_v1_utf8_string(id3info *id3, SV **string, uint32_t len)
{
  uint32_t read = 0;
  char *ptr;
  char *str;

  read = _id3_get_utf8_string(id3, string, len, ISO_8859_1);

  if (read) {
    // Trim spaces from end
    if (*string != NULL) {
      str = SvPVX(*string);
      ptr = str + sv_len(*string);

      while (ptr > str && ptr[-1] == ' ')
        --ptr;

      *ptr = 0;
      SvCUR_set(*string, ptr - str);
    }
  }

  return read;
}

uint32_t
_id3_get_utf8_string(id3info *id3, SV **string, uint32_t len, uint8_t encoding)
{
  uint8_t byteorder = UTF16_BYTEORDER_ANY;
  uint32_t read = 0;
  unsigned char *bptr;

  // Init scratch buffer if necessary
  if ( !id3->utf8->alloc ) {
    // Use a larger initial buffer if reading ISO-8859-1 to avoid
    // always having to allocate a second time
    buffer_init( id3->utf8, encoding == ISO_8859_1 ? len * 2 : len );
  }
  else {
    // Reset scratch buffer
    buffer_clear(id3->utf8);
  }

  if ( *string != NULL ) {
    warn("    !!! string SV is not null: %s\n", SvPVX(*string));
  }

  switch (encoding) {
    case ISO_8859_1:
      read += buffer_get_latin1_as_utf8(id3->buf, id3->utf8, len);
      break;

    case UTF_16BE:
      byteorder = UTF16_BYTEORDER_BE;

    case UTF_16:
      bptr = buffer_ptr(id3->buf);

      switch ( (bptr[0] << 8) | bptr[1] ) {
      case 0xfeff:
        DEBUG_TRACE("    UTF-16 BOM is big-endian\n");
        byteorder = UTF16_BYTEORDER_BE;
        buffer_consume(id3->buf, 2);
        read += 2;
        break;

      case 0xfffe:
        DEBUG_TRACE("    UTF-16 BOM is little-endian\n");
        byteorder = UTF16_BYTEORDER_LE;
        buffer_consume(id3->buf, 2);
        read += 2;
        break;
      }

      /* Bug 14728
        If there is no BOM, assume LE, this is what appears in the wild -andy
      */
      if (byteorder == UTF16_BYTEORDER_ANY) {
        DEBUG_TRACE("    UTF-16 byte order defaulting to little-endian, no BOM\n");
        byteorder = UTF16_BYTEORDER_LE;
      }

      read += buffer_get_utf16_as_utf8(id3->buf, id3->utf8, len - read, byteorder);
      break;

    case UTF_8:
      read += buffer_get_utf8(id3->buf, id3->utf8, len);
      break;

    default:
      break;
  }

  if (read) {
    if ( buffer_len(id3->utf8) ) {
      *string = newSVpv( buffer_ptr(id3->utf8), 0 );
      sv_utf8_decode(*string);
      DEBUG_TRACE("    read utf8 string of %d bytes: %s\n", buffer_len(id3->utf8), SvPVX(*string));
    }
    else {
      DEBUG_TRACE("    empty string\n");
    }
  }

  return read;
}

uint32_t
_id3_parse_rvad(id3info *id3, char const *id, uint32_t size)
{
  unsigned char *rva = buffer_ptr(id3->buf);
  int sign_r = rva[0] & 0x01 ? 1 : -1;
  int sign_l = rva[0] & 0x02 ? 1 : -1;
  int bytes = rva[1] / 8;
  float vol[2];
  float peak[2];
  int i;
  AV *framedata = newAV();

  // Sanity check, first byte must be either 0 or 1, second byte > 0
  if (rva[0] & 0xFE || rva[1] == 0) {
    return 0;
  }

  // Calculated size must match the actual size
  if (size != 2 + (bytes * 4)) {
    return 0;
  }

  rva += 2;

  vol[0] = _varint( rva, bytes ) * sign_r / 256.;
  vol[1] = _varint( rva + bytes, bytes ) * sign_l / 256.;

  peak[0] = _varint( rva + (bytes * 2), bytes );
  peak[1] = _varint( rva + (bytes * 3), bytes );

  // iTunes uses a range of -255 to 255
	// to be -100% (silent) to 100% (+6dB)
  for (i = 0; i < 2; i++) {
    if ( vol[i] == -255 ) {
      vol[i] = -96.0;
    }
    else {
      vol[i] = 20.0 * log( ( vol[i] + 255 ) / 255 ) / log(10);
    }

    av_push( framedata, newSVpvf( "%f dB", vol[i] ) );
    av_push( framedata, newSVpvf( "%f", peak[i] ) );
  }

  my_hv_store( id3->tags, id, newRV_noinc( (SV *)framedata ) );

  buffer_consume(id3->buf, 2 + (bytes * 4));

  return 2 + (bytes * 4);
}

uint32_t
_id3_parse_rgad(id3info *id3)
{
  float radio = 0.0;
  float audiophile = 0.0;
  uint8_t sign = 0;
  HV *framedata = newHV();
  uint32_t read = 0;

  // Peak (32-bit float)
  my_hv_store( framedata, "peak", newSVpvf( "%f", (float)buffer_get_float32(id3->buf) ) );
  read += 4;

  // Radio (16 bits)

  // Radio Name code (3 bits, should always be 1)
  buffer_get_bits(id3->buf, 3);

  my_hv_store( framedata, "track_originator", newSVuv( buffer_get_bits(id3->buf, 3) ) );

  // Sign bit (1 bit)
  sign = buffer_get_bits(id3->buf, 1);

  // Gain value (9 bits)
  radio = (float)buffer_get_bits(id3->buf, 9);
  radio /= 10.0;
  if (sign == 1) radio *= -1.0;
  my_hv_store( framedata, "track_gain", newSVpvf( "%f dB", radio ) );

  read += 2;

  // Audiophile (16 bits)

  // Audiophile Name code (3 bits, should always be 2)
  buffer_get_bits(id3->buf, 3);

  // Audiophile Originator code (3 bits)
  my_hv_store( framedata, "album_originator", newSVuv( buffer_get_bits(id3->buf, 3) ) );

  // Sign bit (1 bit)
  sign = buffer_get_bits(id3->buf, 1);

  // Gain value (9 bits)
  audiophile = (float)buffer_get_bits(id3->buf, 9);
  audiophile /= 10.0;
  if (sign == 1) audiophile *= -1.0;
  my_hv_store( framedata, "album_gain", newSVpvf( "%f dB", audiophile ) );

  read += 2;

  my_hv_store( id3->tags, "RGAD", newRV_noinc( (SV *)framedata ) );

  return read;
}

uint32_t
_id3_parse_rva2(id3info *id3, uint32_t len, AV *framedata)
{
  float adj = 0.0;
  int adj_fp;
  uint8_t peakbits;
  float peak = 0.0;
  uint32_t read = 0;
  unsigned char *bptr;

  // Channel
  av_push( framedata, newSViv( buffer_get_char(id3->buf) ) );

  // Adjustment
  bptr = buffer_ptr(id3->buf);
  adj_fp = *(signed char *)(bptr) << 8;
  adj_fp |= *(unsigned char *)(bptr+1);
  adj = adj_fp / 512.0;
  av_push( framedata, newSVpvf( "%f dB", adj ) );
  buffer_consume(id3->buf, 2);

  // Peak
  // Based on code from mp3gain
  peakbits = buffer_get_char(id3->buf);

  read += 4;

  if (4 + (peakbits + 7) / 8 <= len) {
    DEBUG_TRACE("    peakbits: %d\n", peakbits);
    if (peakbits > 0) {
      peak += (float)buffer_get_char(id3->buf);
      read++;
    }
    if (peakbits > 8) {
      peak += (float)buffer_get_char(id3->buf) / 256.0;
      read++;
    }
    if (peakbits > 16) {
      peak += (float)buffer_get_char(id3->buf) / 65536.0;
      read++;
    }

    if (peakbits > 0)
      peak /= (float)(1 << ((peakbits - 1) & 7));
  }

  av_push( framedata, newSVpvf( "%f dB", peak ) );

  return read;
}

uint32_t
_id3_parse_sylt(id3info *id3, uint8_t encoding, uint32_t len, AV *framedata)
{
  uint32_t read = 0;
  AV *content = newAV();
  unsigned char *bptr;

  while (read < len) {
    SV *value = NULL;
    HV *lyric = newHV();

    read += _id3_get_utf8_string(id3, &value, len - read, encoding);
    if (value != NULL && SvPOK(value) && sv_len(value)) {
      my_hv_store( lyric, "text", value );
    }
    else {
      my_hv_store( lyric, "text", &PL_sv_undef );
      if (value) SvREFCNT(value);
    }

    my_hv_store( lyric, "timestamp", newSVuv( buffer_get_int(id3->buf) ) );
    read += 4;

    // A $0A newline byte may follow, for some odd reason
    bptr = buffer_ptr(id3->buf);
    if ( len - read > 0 && bptr[0] == 0x0a ) {
      buffer_consume(id3->buf, 1);
      read++;
    }

    av_push( content, newRV_noinc( (SV *)lyric ) );
  }

  av_push( framedata, newRV_noinc( (SV *)content ) );

  return read;
}

uint32_t
_id3_parse_etco(id3info *id3, uint32_t len, AV *framedata)
{
  uint32_t read = 0;
  AV *content = newAV();

  while (read < len) {
    HV *event = newHV();

    my_hv_store( event, "type", newSVuv( buffer_get_char(id3->buf) ) );
    my_hv_store( event, "timestamp", newSVuv( buffer_get_int(id3->buf) ) );
    read += 5;

    av_push( content, newRV_noinc( (SV *)event ) );
  }

  av_push( framedata, newRV_noinc( (SV *)content ) );

  return read;
}

void
_id3_convert_tdrc(id3info *id3)
{
  char timestamp[17] = { 0 };

  if ( my_hv_exists(id3->tags, "TYER") ) {
    SV *tyer = my_hv_delete(id3->tags, "TYER");
    if (SvPOK(tyer) && sv_len(tyer) == 4) {
      char *ptr = SvPVX(tyer);
      timestamp[0] = ptr[0];
      timestamp[1] = ptr[1];
      timestamp[2] = ptr[2];
      timestamp[3] = ptr[3];
      DEBUG_TRACE("  Converted TYER (%s) to TDRC (%s)\n", SvPVX(tyer), timestamp);
    }
  }

  if ( my_hv_exists(id3->tags, "TDAT") ) {
    SV *tdat = my_hv_delete(id3->tags, "TDAT");
    if (SvPOK(tdat) && sv_len(tdat) == 4) {
      char *ptr = SvPVX(tdat);
      timestamp[4] = '-';
      timestamp[5] = ptr[2];
      timestamp[6] = ptr[3];
      timestamp[7] = '-';
      timestamp[8] = ptr[0];
      timestamp[9] = ptr[1];
      DEBUG_TRACE("  Converted TDAT (%s) to TDRC (%s)\n", SvPVX(tdat), timestamp);
    }
  }

  if ( my_hv_exists(id3->tags, "TIME") ) {
    SV *time = my_hv_delete(id3->tags, "TIME");
    if (SvPOK(time) && sv_len(time) == 4) {
      char *ptr = SvPVX(time);
      timestamp[10] = 'T';
      timestamp[11] = ptr[0];
      timestamp[12] = ptr[1];
      timestamp[13] = ':';
      timestamp[14] = ptr[2];
      timestamp[15] = ptr[3];
      DEBUG_TRACE("  Converted TIME (%s) to TDRC (%s)\n", SvPVX(time), timestamp);
    }
  }

  if (timestamp[0]) {
    my_hv_store( id3->tags, "TDRC", newSVpv(timestamp, 0) );
  }
}

// deunsync in-place, from libid3tag
uint32_t
_id3_deunsync(unsigned char *data, uint32_t length)
{
  unsigned char *old;
  unsigned char *end = data + length;
  unsigned char *new;

  if (length == 0)
    return 0;

  for (old = new = data; old < end - 1; ++old) {
    *new++ = *old;
    if (old[0] == 0xff && old[1] == 0x00)
      ++old;
  }

  *new++ = *old;

  return new - data;
}

void
_id3_skip(id3info *id3, uint32_t size)
{
  if ( buffer_len(id3->buf) >= size ) {
    buffer_consume(id3->buf, size);

    DEBUG_TRACE("  skipped buffer data size %d\n", size);
  }
  else {
    PerlIO_seek(id3->infile, size - buffer_len(id3->buf), SEEK_CUR);
    buffer_clear(id3->buf);

    DEBUG_TRACE("  seeked past %d bytes to %d\n", size, (int)PerlIO_tell(id3->infile));
  }
}

// return an ID3v1 genre string indexed by number
char const *
_id3_genre_index(unsigned int index)
{
  return (index < NGENRES) ? genre_table[index] : 0;
}

// translate an ID3v2 genre number/keyword to its full name
char const *
_id3_genre_name(char const *string)
{
  static char const genre_remix[] = { 'R', 'e', 'm', 'i', 'x', 0 };
  static char const genre_cover[] = { 'C', 'o', 'v', 'e', 'r', 0 };
  unsigned long number;

  if (string == 0 || *string == 0)
    return 0;

  if (string[0] == 'R' && string[1] == 'X')
    return genre_remix;
  if (string[0] == 'C' && string[1] == 'R')
    return genre_cover;

  number = strtol(string, NULL, 0);

  return (number < NGENRES) ? genre_table[number] : string;
}