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

/*
  TODO:
  These will be added when I see a real file that uses them.

  Header objects:

  Marker (3.7)
  Bitrate Mutual Exclusion (3.8)
  Content Branding (3.13)

  Header Extension objects:

  Group Mutual Exclusion (4.3)
  Stream Prioritization (4.4)
  Bandwidth Sharing (4.5)
  Media Object Index Parameters (4.10)
  Timecode Index Parameters (4.11)
  Advanced Content Encryption (4.13)

  Index objects:

  Media Object Index (6.3)
  Timecode Index (6.4)
*/

#include "asf.h"

static void
print_guid(GUID guid)
{
  PerlIO_printf(PerlIO_stderr(),
    "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x ",
    guid.Data1, guid.Data2, guid.Data3,
    guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3],
    guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]
  );
}

int
get_asf_metadata(PerlIO *infile, char *file, HV *info, HV *tags)
{
  asfinfo *asf = _asf_parse(infile, file, info, tags, 0);

  Safefree(asf);

  return 0;
}

asfinfo *
_asf_parse(PerlIO *infile, char *file, HV *info, HV *tags, uint8_t seeking)
{
  ASF_Object hdr;
  ASF_Object data;
  ASF_Object tmp;
  asfinfo *asf;

  Newz(0, asf, sizeof(asfinfo), asfinfo);
  Newz(0, asf->buf, sizeof(Buffer), Buffer);
  Newz(0, asf->scratch, sizeof(Buffer), Buffer);

  asf->file_size     = _file_size(infile);
  asf->audio_offset  = 0;
  asf->object_offset = 0;
  asf->infile        = infile;
  asf->file          = file;
  asf->info          = info;
  asf->tags          = tags;
  asf->seeking       = seeking;

  buffer_init(asf->buf, ASF_BLOCK_SIZE);

  if ( !_check_buf(infile, asf->buf, 30, ASF_BLOCK_SIZE) ) {
    goto out;
  }

  buffer_get_guid(asf->buf, &hdr.ID);

  if ( !IsEqualGUID(&hdr.ID, &ASF_Header_Object) ) {
    PerlIO_printf(PerlIO_stderr(), "Invalid ASF header: %s\n", file);
    PerlIO_printf(PerlIO_stderr(), "  Expecting: ");
      print_guid(ASF_Header_Object);
    PerlIO_printf(PerlIO_stderr(), "\n        Got: ");
      print_guid(hdr.ID);
    PerlIO_printf(PerlIO_stderr(), "\n");
    goto out;
  }

  hdr.size        = buffer_get_int64_le(asf->buf);
  hdr.num_objects = buffer_get_int_le(asf->buf);
  hdr.reserved1   = buffer_get_char(asf->buf);
  hdr.reserved2   = buffer_get_char(asf->buf);

  if ( hdr.reserved2 != 0x02 ) {
    PerlIO_printf(PerlIO_stderr(), "Invalid ASF header: %s\n", file);
    goto out;
  }

  asf->object_offset += 30;

  while ( hdr.num_objects-- ) {
    if ( !_check_buf(infile, asf->buf, 24, ASF_BLOCK_SIZE) ) {
      goto out;
    }

    buffer_get_guid(asf->buf, &tmp.ID);
    tmp.size = buffer_get_int64_le(asf->buf);

    if ( !_check_buf(infile, asf->buf, tmp.size - 24, ASF_BLOCK_SIZE) ) {
      goto out;
    }

    asf->object_offset += 24;

    DEBUG_TRACE("object_offset %d\n", asf->object_offset);

    if ( IsEqualGUID(&tmp.ID, &ASF_Content_Description) ) {
      DEBUG_TRACE("Content_Description\n");
      _parse_content_description(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_File_Properties) ) {
      DEBUG_TRACE("File_Properties\n");
      _parse_file_properties(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Stream_Properties) ) {
      DEBUG_TRACE("Stream_Properties\n");
      _parse_stream_properties(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Extended_Content_Description) ) {
      DEBUG_TRACE("Extended_Content_Description\n");
      _parse_extended_content_description(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Codec_List) ) {
      DEBUG_TRACE("Codec_List\n");
      _parse_codec_list(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Stream_Bitrate_Properties) ) {
      DEBUG_TRACE("Stream_Bitrate_Properties\n");
      _parse_stream_bitrate_properties(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Content_Encryption) ) {
      DEBUG_TRACE("Content_Encryption\n");
      _parse_content_encryption(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Extended_Content_Encryption) ) {
      DEBUG_TRACE("Extended_Content_Encryption\n");
      _parse_extended_content_encryption(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Script_Command) ) {
      DEBUG_TRACE("Script_Command\n");
      _parse_script_command(asf);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Digital_Signature) ) {
      DEBUG_TRACE("Skipping Digital_Signature\n");
      buffer_consume(asf->buf, tmp.size - 24);
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Header_Extension) ) {
      DEBUG_TRACE("Header_Extension\n");
      if ( !_parse_header_extension(asf, tmp.size) ) {
        PerlIO_printf(PerlIO_stderr(), "Invalid ASF file: %s (invalid header extension object)\n", file);
        goto out;
      }
    }
    else if ( IsEqualGUID(&tmp.ID, &ASF_Error_Correction) ) {
      DEBUG_TRACE("Skipping Error_Correction\n");
      buffer_consume(asf->buf, tmp.size - 24);
    }
    else {
      // Unhandled GUID
      PerlIO_printf(PerlIO_stderr(), "** Unhandled GUID: ");
      print_guid(tmp.ID);
      PerlIO_printf(PerlIO_stderr(), "size: %llu\n", tmp.size);

      buffer_consume(asf->buf, tmp.size - 24);
    }

    asf->object_offset += tmp.size - 24;
  }

  // We should be at the start of the Data object.
  // Seek past it to find more objects
  if ( !_check_buf(infile, asf->buf, 24, ASF_BLOCK_SIZE) ) {
    goto out;
  }

  buffer_get_guid(asf->buf, &data.ID);

  if ( !IsEqualGUID(&data.ID, &ASF_Data) ) {
    PerlIO_printf(PerlIO_stderr(), "Invalid ASF file: %s (no Data object after Header)\n", file);
    goto out;
  }

  // Store offset to beginning of data (50 goes past the top-level data packet)
  asf->audio_offset = hdr.size + 50;
  my_hv_store( info, "audio_offset", newSVuv(asf->audio_offset) );

  my_hv_store( info, "file_size", newSVuv(asf->file_size) );

  data.size = buffer_get_int64_le(asf->buf);
  asf->audio_size = data.size;

  // Check audio_size is not larger than file
  if (asf->audio_size > asf->file_size - asf->audio_offset) {
    asf->audio_size = asf->file_size - asf->audio_offset;
    DEBUG_TRACE("audio_size too large, fixed to %lld\n", asf->audio_size);
  }
  my_hv_store( info, "audio_size", newSVuv(asf->audio_size) );

  if (seeking) {
    if ( hdr.size + data.size < asf->file_size ) {
      DEBUG_TRACE("Seeking past data: %llu\n", hdr.size + data.size);

      if ( PerlIO_seek(infile, hdr.size + data.size, SEEK_SET) != 0 ) {
        PerlIO_printf(PerlIO_stderr(), "Invalid ASF file: %s (Invalid Data object size)\n", file);
        goto out;
      }

      buffer_clear(asf->buf);

      if ( !_parse_index_objects(asf, asf->file_size - hdr.size - data.size) ) {
        PerlIO_printf(PerlIO_stderr(), "Invalid ASF file: %s (Invalid Index object)\n", file);
        goto out;
      }
    }
  }

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

  if (asf->scratch->alloc)
    buffer_free(asf->scratch);
  Safefree(asf->scratch);

  return asf;
}

void
_parse_content_description(asfinfo *asf)
{
  int i;
  uint16_t len[5];
  char fields[5][12] = {
    { "Title" },
    { "Author" },
    { "Copyright" },
    { "Description" },
    { "Rating" }
  };

  for (i = 0; i < 5; i++) {
    len[i] = buffer_get_short_le(asf->buf);
  }

  buffer_init_or_clear(asf->scratch, len[0]);

  for (i = 0; i < 5; i++) {
    SV *value;

    if ( len[i] ) {
      buffer_clear(asf->scratch);
      buffer_get_utf16_as_utf8(asf->buf, asf->scratch, len[i], UTF16_BYTEORDER_LE);
      value = newSVpv( buffer_ptr(asf->scratch), 0 );
      sv_utf8_decode(value);

      DEBUG_TRACE("  %s / %s\n", fields[i], SvPVX(value));

      _store_tag( asf->tags, newSVpv(fields[i], 0), value );
    }
  }
}

void
_parse_extended_content_description(asfinfo *asf)
{
  uint16_t count = buffer_get_short_le(asf->buf);
  uint32_t picture_offset = 0;

  buffer_init_or_clear(asf->scratch, 32);

  while ( count-- ) {
    uint16_t name_len;
    uint16_t data_type;
    uint16_t value_len;
    SV *key = NULL;
    SV *value = NULL;

    name_len = buffer_get_short_le(asf->buf);

    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, name_len, UTF16_BYTEORDER_LE);
    key = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(key);

    data_type = buffer_get_short_le(asf->buf);
    value_len = buffer_get_short_le(asf->buf);

    picture_offset += 2 + name_len + 4;

    if (data_type == TYPE_UNICODE) {
      buffer_clear(asf->scratch);
      buffer_get_utf16_as_utf8(asf->buf, asf->scratch, value_len, UTF16_BYTEORDER_LE);
      value = newSVpv( buffer_ptr(asf->scratch), 0 );
      sv_utf8_decode(value);
    }
    else if (data_type == TYPE_BYTE) {
      // handle picture data, interestingly it is compatible with the ID3v2 APIC frame
      if ( !strcmp( SvPVX(key), "WM/Picture" ) ) {
        value = _parse_picture(asf, picture_offset);
      }
      else {
        value = newSVpvn( buffer_ptr(asf->buf), value_len );
        buffer_consume(asf->buf, value_len);
      }
    }
    else if (data_type == TYPE_BOOL) {
      value = newSViv( buffer_get_int_le(asf->buf) );
    }
    else if (data_type == TYPE_DWORD) {
      value = newSViv( buffer_get_int_le(asf->buf) );
    }
    else if (data_type == TYPE_QWORD) {
      value = newSViv( buffer_get_int64_le(asf->buf) );
    }
    else if (data_type == TYPE_WORD) {
      value = newSViv( buffer_get_short_le(asf->buf) );
    }
    else {
      PerlIO_printf(PerlIO_stderr(), "Unknown extended content description data type %d\n", data_type);
      buffer_consume(asf->buf, value_len);
    }

    picture_offset += value_len;

    if (value != NULL) {
#ifdef AUDIO_SCAN_DEBUG
      if ( data_type == 0 ) {
        DEBUG_TRACE("  %s / type %d / %s\n", SvPVX(key), data_type, SvPVX(value));
      }
      else if ( data_type > 1 ) {
        DEBUG_TRACE("  %s / type %d / %d\n", SvPVX(key), data_type, (int)SvIV(value));
      }
      else {
        DEBUG_TRACE("  %s / type %d / <binary>\n", SvPVX(key), data_type);
      }
#endif

      _store_tag( asf->tags, key, value );
    }
  }
}

void
_parse_file_properties(asfinfo *asf)
{
  GUID file_id;
  uint64_t file_size;
  uint64_t creation_date;
  uint64_t data_packets;
  uint64_t play_duration;
  uint64_t send_duration;
  uint64_t preroll;
  uint32_t flags;
  uint32_t min_packet_size;
  uint32_t max_packet_size;
  uint32_t max_bitrate;
  uint8_t broadcast;
  uint8_t seekable;

  buffer_get_guid(asf->buf, &file_id);
  my_hv_store(
    asf->info, "file_id", newSVpvf( "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
      file_id.Data1, file_id.Data2, file_id.Data3,
      file_id.Data4[0], file_id.Data4[1], file_id.Data4[2], file_id.Data4[3],
      file_id.Data4[4], file_id.Data4[5], file_id.Data4[6], file_id.Data4[7]
    )
  );

  file_size       = buffer_get_int64_le(asf->buf);
  creation_date   = buffer_get_int64_le(asf->buf);
  data_packets    = buffer_get_int64_le(asf->buf);
  play_duration   = buffer_get_int64_le(asf->buf);
  send_duration   = buffer_get_int64_le(asf->buf);
  preroll         = buffer_get_int64_le(asf->buf);
  flags           = buffer_get_int_le(asf->buf);
  min_packet_size = buffer_get_int_le(asf->buf);
  max_packet_size = buffer_get_int_le(asf->buf);
  max_bitrate     = buffer_get_int_le(asf->buf);

  broadcast = flags & 0x01 ? 1 : 0;
  seekable  = flags & 0x02 ? 1 : 0;

  if ( !broadcast ) {
    creation_date = (creation_date - 116444736000000000ULL) / 10000000;
    play_duration /= 10000;
    send_duration /= 10000;

    // Don't overwrite the actual file size we found from stat
    //my_hv_store( info, "file_size", newSViv(file_size) );

    my_hv_store( asf->info, "creation_date", newSViv(creation_date) );
    my_hv_store( asf->info, "data_packets", newSViv(data_packets) );
    my_hv_store( asf->info, "play_duration_ms", newSViv(play_duration) );
    my_hv_store( asf->info, "send_duration_ms", newSViv(send_duration) );

    // Calculate actual song duration
    my_hv_store( asf->info, "song_length_ms", newSViv( play_duration - preroll ) );
  }

  my_hv_store( asf->info, "preroll", newSViv(preroll) );
  my_hv_store( asf->info, "broadcast", newSViv(broadcast) );
  my_hv_store( asf->info, "seekable", newSViv(seekable) );
  my_hv_store( asf->info, "min_packet_size", newSViv(min_packet_size) );
  my_hv_store( asf->info, "max_packet_size", newSViv(max_packet_size) );
  my_hv_store( asf->info, "max_bitrate", newSViv(max_bitrate) );

  // DLNA, need to store max_bitrate for later
  asf->max_bitrate = max_bitrate;
}

void
_parse_stream_properties(asfinfo *asf)
{
  GUID stream_type;
  GUID ec_type;
  uint64_t time_offset;
  uint32_t type_data_len;
  uint32_t ec_data_len;
  uint16_t flags;
  uint16_t stream_number;
  Buffer type_data_buf;

  buffer_get_guid(asf->buf, &stream_type);
  buffer_get_guid(asf->buf, &ec_type);
  time_offset = buffer_get_int64_le(asf->buf);
  type_data_len = buffer_get_int_le(asf->buf);
  ec_data_len   = buffer_get_int_le(asf->buf);
  flags         = buffer_get_short_le(asf->buf);
  stream_number = flags & 0x007f;

  // skip reserved bytes
  buffer_consume(asf->buf, 4);

  // type-specific data
  buffer_init(&type_data_buf, type_data_len);
  buffer_append(&type_data_buf, buffer_ptr(asf->buf), type_data_len);
  buffer_consume(asf->buf, type_data_len);

  // skip error-correction data
  buffer_consume(asf->buf, ec_data_len);

  if ( IsEqualGUID(&stream_type, &ASF_Audio_Media) ) {
    uint8_t is_wma = 0;
    uint16_t codec_id, channels;
    uint32_t samplerate;

    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_Audio_Media", 0) );

    // Parse WAVEFORMATEX data
    codec_id = buffer_get_short_le(&type_data_buf);
    switch (codec_id) {
      case 0x000a:
        is_wma = 1;
        break;

      case 0x0161:
        is_wma = 1;
        asf->valid_profiles |= IS_VALID_WMA_BASE | IS_VALID_WMA_FULL;
        break;

      case 0x0162:
        is_wma = 1;
        asf->valid_profiles |= IS_VALID_WMA_PRO;
        break;

      case 0x0163:
        is_wma = 1;
        asf->valid_profiles |= IS_VALID_WMA_LSL;
        break;
    }

    _store_stream_info( stream_number, asf->info, newSVpv("codec_id", 0), newSViv(codec_id) );

    channels = buffer_get_short_le(&type_data_buf);
    _store_stream_info( stream_number, asf->info, newSVpv("channels", 0), newSViv(channels) );

    samplerate = buffer_get_int_le(&type_data_buf);
    _store_stream_info( stream_number, asf->info, newSVpv("samplerate", 0), newSViv(samplerate) );

    // Determine DLNA profile
    if (channels > 2) {
      asf->valid_profiles &= ~IS_VALID_WMA_BASE;
      asf->valid_profiles &= ~IS_VALID_WMA_FULL;

      if (codec_id == 0x0163) {
        asf->valid_profiles &= ~IS_VALID_WMA_LSL;
        asf->valid_profiles |= IS_VALID_WMA_LSL_MULT5;
      }
    }

    if (samplerate > 48000) {
      asf->valid_profiles &= ~IS_VALID_WMA_BASE;
      asf->valid_profiles &= ~IS_VALID_WMA_FULL;

      if (samplerate > 96000) {
        asf->valid_profiles &= ~IS_VALID_WMA_PRO;
        asf->valid_profiles &= ~IS_VALID_WMA_LSL; // XXX check N1/N2 defs
        asf->valid_profiles &= ~IS_VALID_WMA_LSL_MULT5;
      }
    }

    if (asf->max_bitrate > 192999) {
      asf->valid_profiles &= ~IS_VALID_WMA_BASE;

      if (asf->max_bitrate > 384999) {
        asf->valid_profiles &= ~IS_VALID_WMA_FULL;

        if (asf->max_bitrate > 1499999) {
          asf->valid_profiles &= ~IS_VALID_WMA_PRO;
          asf->valid_profiles &= ~IS_VALID_WMA_LSL; // XXX check N1/N2 defs
          asf->valid_profiles &= ~IS_VALID_WMA_LSL_MULT5;
        }
      }
    }

    if (asf->valid_profiles & IS_VALID_WMA_BASE)
      my_hv_store( asf->info, "dlna_profile", newSVpvn("WMABASE", 7) );
    else if (asf->valid_profiles & IS_VALID_WMA_FULL)
      my_hv_store( asf->info, "dlna_profile", newSVpvn("WMAFULL", 7) );
    else if (asf->valid_profiles & IS_VALID_WMA_PRO)
      my_hv_store( asf->info, "dlna_profile", newSVpvn("WMAPRO", 6) );
    else if (asf->valid_profiles & IS_VALID_WMA_LSL)
      my_hv_store( asf->info, "dlna_profile", newSVpvn("WMALSL", 6) );
    else if (asf->valid_profiles & IS_VALID_WMA_LSL_MULT5)
      my_hv_store( asf->info, "dlna_profile", newSVpvn("WMALSL_MULT5", 12) );

    _store_stream_info( stream_number, asf->info, newSVpv("avg_bytes_per_sec", 0), newSViv( buffer_get_int_le(&type_data_buf) ) );
    _store_stream_info( stream_number, asf->info, newSVpv("block_alignment", 0), newSViv( buffer_get_short_le(&type_data_buf) ) );
    _store_stream_info( stream_number, asf->info, newSVpv("bits_per_sample", 0), newSViv( buffer_get_short_le(&type_data_buf) ) );

    // Read WMA-specific data
    if (is_wma) {
      buffer_consume(&type_data_buf, 2);
      _store_stream_info( stream_number, asf->info, newSVpv("samples_per_block", 0), newSViv( buffer_get_int_le(&type_data_buf) ) );
      _store_stream_info( stream_number, asf->info, newSVpv("encode_options", 0), newSViv( buffer_get_short_le(&type_data_buf) ) );
      _store_stream_info( stream_number, asf->info, newSVpv("super_block_align", 0), newSViv( buffer_get_int_le(&type_data_buf) ) );
    }
  }
  else if ( IsEqualGUID(&stream_type, &ASF_Video_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_Video_Media", 0) );

    DEBUG_TRACE("type_data_len: %d\n", type_data_len);

    // Read video-specific data
    _store_stream_info( stream_number, asf->info, newSVpv("width", 0), newSVuv( buffer_get_int_le(&type_data_buf) ) );
    _store_stream_info( stream_number, asf->info, newSVpv("height", 0), newSVuv( buffer_get_int_le(&type_data_buf) ) );

    // Skip format size, width, height, reserved
    buffer_consume(&type_data_buf, 17);

    _store_stream_info( stream_number, asf->info, newSVpv("bpp", 0), newSVuv( buffer_get_short_le(&type_data_buf) ) );

    _store_stream_info( stream_number, asf->info, newSVpv("compression_id", 0), newSVpv( buffer_ptr(&type_data_buf), 4 ) );

    // Rest of the data does not seem to apply to video
  }
  else if ( IsEqualGUID(&stream_type, &ASF_Command_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_Command_Media", 0) );
  }
  else if ( IsEqualGUID(&stream_type, &ASF_JFIF_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_JFIF_Media", 0) );

    // type-specific data
    _store_stream_info( stream_number, asf->info, newSVpv("width", 0), newSVuv( buffer_get_int_le(&type_data_buf) ) );
    _store_stream_info( stream_number, asf->info, newSVpv("height", 0), newSVuv( buffer_get_int_le(&type_data_buf) ) );
  }
  else if ( IsEqualGUID(&stream_type, &ASF_Degradable_JPEG_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_Degradable_JPEG_Media", 0) );

    // XXX: type-specific data (section 9.4.2)
  }
  else if ( IsEqualGUID(&stream_type, &ASF_File_Transfer_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_File_Transfer_Media", 0) );

    // XXX: type-specific data (section 9.5)
  }
  else if ( IsEqualGUID(&stream_type, &ASF_Binary_Media) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("stream_type", 0), newSVpv("ASF_Binary_Media", 0) );

    // XXX: type-specific data (section 9.5)
  }

  if ( IsEqualGUID(&ec_type, &ASF_No_Error_Correction) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("error_correction_type", 0), newSVpv("ASF_No_Error_Correction", 0) );
  }
  else if ( IsEqualGUID(&ec_type, &ASF_Audio_Spread) ) {
    _store_stream_info( stream_number, asf->info, newSVpv("error_correction_type", 0), newSVpv("ASF_Audio_Spread", 0) );
  }

  _store_stream_info( stream_number, asf->info, newSVpv("time_offset", 0), newSViv(time_offset) );
  _store_stream_info( stream_number, asf->info, newSVpv("encrypted", 0), newSVuv( flags & 0x8000 ? 1 : 0 ) );

  buffer_free(&type_data_buf);
}

int
_parse_header_extension(asfinfo *asf, uint64_t len)
{
  int ext_size;
  GUID hdr;
  uint64_t hdr_size;
  uint32_t tmp_offset = asf->object_offset;

  // Skip reserved fields
  buffer_consume(asf->buf, 18);

  ext_size = buffer_get_int_le(asf->buf);

  // Sanity check ext size
  // Must be 0 or 24+, and 46 less than header extension object size
  if (ext_size > 0) {
    if (ext_size < 24) {
      return 0;
    }
    if (ext_size != len - 46) {
      return 0;
    }
  }

  DEBUG_TRACE("  size: %d\n", ext_size);

  // Header Extension is always 46 bytes, and we've already counted 24 of it
  asf->object_offset += 46 - 24;

  while (ext_size > 0) {
    buffer_get_guid(asf->buf, &hdr);
    hdr_size = buffer_get_int64_le(asf->buf);
    ext_size -= hdr_size;

    asf->object_offset += 24;

    DEBUG_TRACE("  object_offset %d\n", asf->object_offset);

    if ( IsEqualGUID(&hdr, &ASF_Metadata) ) {
      DEBUG_TRACE("  Metadata\n");
      _parse_metadata(asf);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Extended_Stream_Properties) ) {
      DEBUG_TRACE("  Extended_Stream_Properties\n");
      _parse_extended_stream_properties(asf, hdr_size);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Language_List) ) {
      DEBUG_TRACE("  Language_List\n");
      _parse_language_list(asf);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Advanced_Mutual_Exclusion) ) {
      DEBUG_TRACE("  Advanced_Mutual_Exclusion\n");
      _parse_advanced_mutual_exclusion(asf);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Metadata_Library) ) {
      DEBUG_TRACE("  Metadata_Library\n");
      _parse_metadata_library(asf);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Index_Parameters) ) {
      DEBUG_TRACE("  Index_Parameters\n");
      _parse_index_parameters(asf);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Compatibility) ) {
      // reserved for future use, just ignore
      DEBUG_TRACE("  Skipping Compatibility\n");
      buffer_consume(asf->buf, 2);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Padding) ) {
      // skip padding
      DEBUG_TRACE("  Skipping Padding\n");
      buffer_consume(asf->buf, hdr_size - 24);
    }
    else if ( IsEqualGUID(&hdr, &ASF_Index_Placeholder) ) {
      // skip undocumented placeholder
      DEBUG_TRACE("  Skipping Index_Placeholder\n");
      buffer_consume(asf->buf, hdr_size - 24);
    }
    else {
      // Unhandled
      PerlIO_printf(PerlIO_stderr(), "  ** Unhandled extended header: ");
      print_guid(hdr);
      PerlIO_printf(PerlIO_stderr(), "size: %llu\n", hdr_size);

      buffer_consume(asf->buf, hdr_size - 24);
    }

    asf->object_offset += hdr_size - 24;
  }

  // Put back the original offset, or calcs will be wrong in _asf_parse
  asf->object_offset = tmp_offset;

  return 1;
}

void
_parse_metadata(asfinfo *asf)
{
  uint16_t count = buffer_get_short_le(asf->buf);

  buffer_init_or_clear(asf->scratch, 32);

  while ( count-- ) {
    uint16_t stream_number;
    uint16_t name_len;
    uint16_t data_type;
    uint32_t data_len;
    SV *key = NULL;
    SV *value = NULL;

    // Skip reserved
    buffer_consume(asf->buf, 2);

    stream_number = buffer_get_short_le(asf->buf);
    name_len      = buffer_get_short_le(asf->buf);
    data_type     = buffer_get_short_le(asf->buf);
    data_len      = buffer_get_int_le(asf->buf);

    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, name_len, UTF16_BYTEORDER_LE);
    key = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(key);

    if (data_type == TYPE_UNICODE) {
      buffer_clear(asf->scratch);
      buffer_get_utf16_as_utf8(asf->buf, asf->scratch, data_len, UTF16_BYTEORDER_LE);
      value = newSVpv( buffer_ptr(asf->scratch), 0 );
      sv_utf8_decode(value);
    }
    else if (data_type == TYPE_BYTE) {
      value = newSVpvn( buffer_ptr(asf->buf), data_len );
      buffer_consume(asf->buf, data_len);
    }
    else if (data_type == TYPE_BOOL || data_type == TYPE_WORD) {
      value = newSViv( buffer_get_short_le(asf->buf) );
    }
    else if (data_type == TYPE_DWORD) {
      value = newSViv( buffer_get_int_le(asf->buf) );
    }
    else if (data_type == TYPE_QWORD) {
      value = newSViv( buffer_get_int64_le(asf->buf) );
    }
    else {
      DEBUG_TRACE("Unknown metadata data type %d\n", data_type);
      buffer_consume(asf->buf, data_len);
    }

    if (value != NULL) {
#ifdef AUDIO_SCAN_DEBUG
      if ( data_type == 0 ) {
        DEBUG_TRACE("    %s / type %d / stream_number %d / %s\n", SvPVX(key), data_type, stream_number, SvPVX(value));
      }
      else if ( data_type > 1 ) {
        DEBUG_TRACE("    %s / type %d / stream_number %d / %d\n", SvPVX(key), data_type, stream_number, (int)SvIV(value));
      }
      else {
        DEBUG_TRACE("    %s / type %d / stream_number %d / <binary>\n", SvPVX(key), stream_number, data_type);
      }
#endif

      // If stream_number is available, store the data with the stream info
      if (stream_number > 0) {
        _store_stream_info( stream_number, asf->info, key, value );
      }
      else {
        my_hv_store_ent( asf->info, key, value );
        SvREFCNT_dec(key);
      }
    }
  }
}

void
_parse_extended_stream_properties(asfinfo *asf, uint64_t len)
{
  uint64_t start_time          = buffer_get_int64_le(asf->buf);
  uint64_t end_time            = buffer_get_int64_le(asf->buf);
  uint32_t bitrate             = buffer_get_int_le(asf->buf);
  uint32_t buffer_size         = buffer_get_int_le(asf->buf);
  uint32_t buffer_fullness     = buffer_get_int_le(asf->buf);
  uint32_t alt_bitrate         = buffer_get_int_le(asf->buf);
  uint32_t alt_buffer_size     = buffer_get_int_le(asf->buf);
  uint32_t alt_buffer_fullness = buffer_get_int_le(asf->buf);
  uint32_t max_object_size     = buffer_get_int_le(asf->buf);
  uint32_t flags               = buffer_get_int_le(asf->buf);
  uint16_t stream_number       = buffer_get_short_le(asf->buf);
  uint16_t lang_id             = buffer_get_short_le(asf->buf);
  uint64_t avg_time_per_frame  = buffer_get_int64_le(asf->buf);
  uint16_t stream_name_count   = buffer_get_short_le(asf->buf);
  uint16_t payload_ext_count   = buffer_get_short_le(asf->buf);

  len -= 88;

  if (start_time > 0) {
    _store_stream_info( stream_number, asf->info, newSVpv("start_time", 0), newSViv(start_time) );
  }

  if (end_time > 0) {
    _store_stream_info( stream_number, asf->info, newSVpv("end_time", 0), newSViv(end_time) );
  }

  _store_stream_info( stream_number, asf->info, newSVpv("bitrate", 0), newSViv(bitrate) );
  _store_stream_info( stream_number, asf->info, newSVpv("buffer_size", 0), newSViv(buffer_size) );
  _store_stream_info( stream_number, asf->info, newSVpv("buffer_fullness", 0), newSViv(buffer_fullness) );
  _store_stream_info( stream_number, asf->info, newSVpv("alt_bitrate", 0), newSViv(alt_bitrate) );
  _store_stream_info( stream_number, asf->info, newSVpv("alt_buffer_size", 0), newSViv(alt_buffer_size) );
  _store_stream_info( stream_number, asf->info, newSVpv("alt_buffer_fullness", 0), newSViv(alt_buffer_fullness) );
  _store_stream_info( stream_number, asf->info, newSVpv("alt_buffer_size", 0), newSViv(alt_buffer_size) );
  _store_stream_info( stream_number, asf->info, newSVpv("max_object_size", 0), newSViv(max_object_size) );

  if ( flags & 0x01 )
    _store_stream_info( stream_number, asf->info, newSVpv("flag_reliable", 0), newSViv(1) );

  if ( flags & 0x02 )
    _store_stream_info( stream_number, asf->info, newSVpv("flag_seekable", 0), newSViv(1) );

  if ( flags & 0x04 )
    _store_stream_info( stream_number, asf->info, newSVpv("flag_no_cleanpoint", 0), newSViv(1) );

  if ( flags & 0x08 )
    _store_stream_info( stream_number, asf->info, newSVpv("flag_resend_cleanpoints", 0), newSViv(1) );

  _store_stream_info( stream_number, asf->info, newSVpv("language_index", 0), newSViv(lang_id) );

  if (avg_time_per_frame > 0) {
    // XXX: can't get this to divide properly (?!)
    //_store_stream_info( stream_number, asf->info, newSVpv("avg_time_per_frame", 0), newSVuv(avg_time_per_frame / 10000) );
  }

  while ( stream_name_count-- ) {
    uint16_t stream_name_len;

    // stream_name_lang_id
    buffer_consume(asf->buf, 2);
    stream_name_len = buffer_get_short_le(asf->buf);

    DEBUG_TRACE("stream_name_len: %d\n", stream_name_len);

    // XXX, store this?
    buffer_consume(asf->buf, stream_name_len);

    len -= 4 + stream_name_len;
  }

  while ( payload_ext_count-- ) {
    // Skip
    uint32_t payload_len;

    buffer_consume(asf->buf, 18);
    payload_len = buffer_get_int_le(asf->buf);
    buffer_consume(asf->buf, payload_len);

    len -= 22 + payload_len;
  }

  if (len) {
    // Anything left over means we have an embedded Stream Properties Object
    DEBUG_TRACE("      embedded Stream_Properties, size %llu\n", len);
    buffer_consume(asf->buf, 24);
    _parse_stream_properties(asf);
  }
}

void
_parse_language_list(asfinfo *asf)
{
  AV *list = newAV();
  uint16_t count = buffer_get_short_le(asf->buf);

  buffer_init_or_clear(asf->scratch, 32);

  while ( count-- ) {
    SV *value;

    uint8_t len = buffer_get_char(asf->buf);
    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, len, UTF16_BYTEORDER_LE);
    value = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(value);

    av_push( list, value );
  }

  my_hv_store( asf->info, "language_list", newRV_noinc( (SV*)list ) );
}

void
_parse_advanced_mutual_exclusion(asfinfo *asf)
{
  GUID mutex_type;
  uint16_t count;
  AV *mutex_list;
  HV *mutex_hv = newHV();
  SV *mutex_type_sv;
  AV *mutex_streams = newAV();

  buffer_get_guid(asf->buf, &mutex_type);
  count = buffer_get_short_le(asf->buf);

  if ( IsEqualGUID(&mutex_type, &ASF_Mutex_Language) ) {
    mutex_type_sv = newSVpv( "ASF_Mutex_Language", 0 );
  }
  else if ( IsEqualGUID(&mutex_type, &ASF_Mutex_Bitrate) ) {
    mutex_type_sv = newSVpv( "ASF_Mutex_Bitrate", 0 );
  }
  else {
    mutex_type_sv = newSVpv( "ASF_Mutex_Unknown", 0 );
  }

  while ( count-- ) {
    av_push( mutex_streams, newSViv( buffer_get_short_le(asf->buf) ) );
  }

  my_hv_store_ent( mutex_hv, mutex_type_sv, newRV_noinc( (SV *)mutex_streams ) );
  SvREFCNT_dec(mutex_type_sv);

  if ( !my_hv_exists( asf->info, "mutex_list" ) ) {
    mutex_list = newAV();
    av_push( mutex_list, newRV_noinc( (SV *)mutex_hv ) );
    my_hv_store( asf->info, "mutex_list", newRV_noinc( (SV *)mutex_list ) );
  }
  else {
    SV **entry = my_hv_fetch( asf->info, "mutex_list" );
    if (entry != NULL) {
      mutex_list = (AV *)SvRV(*entry);
    }
    else {
      return;
    }

    av_push( mutex_list, newRV_noinc( (SV *)mutex_hv ) );
  }
}

void
_parse_codec_list(asfinfo *asf)
{
  uint32_t count;
  AV *list = newAV();

  buffer_init_or_clear(asf->scratch, 32);

  // Skip reserved
  buffer_consume(asf->buf, 16);

  count = buffer_get_int_le(asf->buf);

  while ( count-- ) {
    HV *codec_info = newHV();
    uint16_t name_len;
    uint16_t desc_len;
    SV *name = NULL;
    SV *desc = NULL;

    uint16_t codec_type = buffer_get_short_le(asf->buf);

    switch (codec_type) {
      case 0x0001:
        my_hv_store( codec_info, "type", newSVpv("Video", 0) );
        break;
      case 0x0002:
        my_hv_store( codec_info, "type", newSVpv("Audio", 0) );
        break;
      default:
        my_hv_store( codec_info, "type", newSVpv("Unknown", 0) );
    }

    // Unlike other objects, these lengths are the
    // "number of Unicode chars", not bytes, so we need to double it
    name_len = buffer_get_short_le(asf->buf) * 2;
    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, name_len, UTF16_BYTEORDER_LE);
    name = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(name);
    my_hv_store( codec_info, "name", name );

    // Set a 'lossless' flag in info if Lossless codec is used
    if ( strstr( buffer_ptr(asf->scratch), "Lossless" ) ) {
      my_hv_store( asf->info, "lossless", newSVuv(1) );
    }

    desc_len = buffer_get_short_le(asf->buf) * 2;
    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, desc_len, UTF16_BYTEORDER_LE);
    desc = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(desc);
    my_hv_store( codec_info, "description", desc );

    // Skip info
    buffer_consume(asf->buf, buffer_get_short_le(asf->buf));

    av_push( list, newRV_noinc( (SV *)codec_info ) );
  }

  my_hv_store( asf->info, "codec_list", newRV_noinc( (SV *)list ) );
}

void
_parse_stream_bitrate_properties(asfinfo *asf)
{
  uint16_t count = buffer_get_short_le(asf->buf);

  while ( count-- ) {
    uint16_t stream_number = buffer_get_short_le(asf->buf) & 0x007f;

    _store_stream_info( stream_number, asf->info, newSVpv("avg_bitrate", 0), newSViv( buffer_get_int_le(asf->buf) ) );
  }
}

void
_parse_metadata_library(asfinfo *asf)
{
  uint16_t count = buffer_get_short_le(asf->buf);
  uint32_t picture_offset = 0;

  buffer_init_or_clear(asf->scratch, 32);

  while ( count-- ) {
    SV *key = NULL;
    SV *value = NULL;
    uint16_t stream_number, name_len, data_type;
    uint32_t data_len;

#ifdef AUDIO_SCAN_DEBUG
    uint16_t lang_index    = buffer_get_short_le(asf->buf);
#else
    buffer_consume(asf->buf, 2);
#endif

    stream_number = buffer_get_short_le(asf->buf);
    name_len      = buffer_get_short_le(asf->buf);
    data_type     = buffer_get_short_le(asf->buf);
    data_len      = buffer_get_int_le(asf->buf);

    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, name_len, UTF16_BYTEORDER_LE);
    key = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(key);

    picture_offset += 12 + name_len;

    if (data_type == TYPE_UNICODE) {
      buffer_clear(asf->scratch);
      buffer_get_utf16_as_utf8(asf->buf, asf->scratch, data_len, UTF16_BYTEORDER_LE);
      value = newSVpv( buffer_ptr(asf->scratch), 0 );
      sv_utf8_decode(value);
    }
    else if (data_type == TYPE_BYTE) {
      // handle picture data
      if ( !strcmp( SvPVX(key), "WM/Picture" ) ) {
        value = _parse_picture(asf, picture_offset);
      }
      else {
        value = newSVpvn( buffer_ptr(asf->buf), data_len );
        buffer_consume(asf->buf, data_len);
      }
    }
    else if (data_type == TYPE_BOOL || data_type == TYPE_WORD) {
      value = newSViv( buffer_get_short_le(asf->buf) );
    }
    else if (data_type == TYPE_DWORD) {
      value = newSViv( buffer_get_int_le(asf->buf) );
    }
    else if (data_type == TYPE_QWORD) {
      value = newSViv( buffer_get_int64_le(asf->buf) );
    }
    else if (data_type == TYPE_GUID) {
      GUID g;
      buffer_get_guid(asf->buf, &g);
      value = newSVpvf(
        "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x",
        g.Data1, g.Data2, g.Data3,
        g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
        g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]
      );
    }
    else {
      PerlIO_printf(PerlIO_stderr(), "Unknown metadata library data type %d\n", data_type);
      buffer_consume(asf->buf, data_len);
    }

    picture_offset += data_len;

    if (value != NULL) {
#ifdef AUDIO_SCAN_DEBUG
      if ( data_type == 0 || data_type == 6 ) {
        DEBUG_TRACE("    %s / type %d / lang_index %d / stream_number %d / %s\n", SvPVX(key), data_type, lang_index, stream_number, SvPVX(value));
      }
      else if ( data_type > 1 ) {
        DEBUG_TRACE("    %s / type %d / lang_index %d / stream_number %d / %d\n", SvPVX(key), data_type, lang_index, stream_number, (int)SvIV(value));
      }
      else {
        DEBUG_TRACE("    %s / type %d / lang_index %d / stream_number %d / <binary>\n", SvPVX(key), lang_index, stream_number, data_type);
      }
#endif

      // If stream_number is available, store the data with the stream info
      // XXX: should store lang_index?
      if (stream_number > 0) {
        _store_stream_info( stream_number, asf->info, key, value );
      }
      else {
        _store_tag( asf->tags, key, value );
      }
    }
  }
}

void
_parse_index_parameters(asfinfo *asf)
{
  uint16_t count;

  my_hv_store( asf->info, "index_entry_interval", newSViv( buffer_get_int_le(asf->buf) ) );

  count = buffer_get_short_le(asf->buf);

  while ( count-- ) {
    uint16_t stream_number = buffer_get_short_le(asf->buf);
    uint16_t index_type    = buffer_get_short_le(asf->buf);

    switch (index_type) {
      case 0x0001:
        _store_stream_info( stream_number, asf->info, newSVpv("index_type", 0), newSVpv("Nearest Past Data Packet", 0) );
        break;
      case 0x0002:
        _store_stream_info( stream_number, asf->info, newSVpv("index_type", 0), newSVpv("Nearest Past Media Object", 0) );
        break;
      case 0x0003:
        _store_stream_info( stream_number, asf->info, newSVpv("index_type", 0), newSVpv("Nearest Past Cleanpoint", 0) );
        break;
      default:
        _store_stream_info( stream_number, asf->info, newSVpv("index_type", 0), newSViv(index_type) );
    }
  }
}

void
_store_stream_info(int stream_number, HV *info, SV *key, SV *value )
{
  AV *streams;
  HV *streaminfo;
  uint8_t found = 0;
  int i = 0;

  if ( !my_hv_exists( info, "streams" ) ) {
    // Create
    streams = newAV();
    my_hv_store( info, "streams", newRV_noinc( (SV*)streams ) );
  }
  else {
    SV **entry = my_hv_fetch( info, "streams" );
    if (entry != NULL) {
      streams = (AV *)SvRV(*entry);
    }
    else {
      return;
    }
  }

  if (streams != NULL) {
    // Find entry for this stream number
    for (i = 0; av_len(streams) >= 0 && i <= av_len(streams); i++) {
      SV **stream = av_fetch(streams, i, 0);
      if (stream != NULL) {
        SV **sn;

        streaminfo = (HV *)SvRV(*stream);
        sn = my_hv_fetch( streaminfo, "stream_number" );
        if (sn != NULL) {
          if ( SvIV(*sn) == stream_number ) {
            // XXX: if item exists, create array
            my_hv_store_ent( streaminfo, key, value );
            SvREFCNT_dec(key);

            found = 1;
            break;
          }
        }
      }
    }

    if ( !found ) {
      // New stream number
      streaminfo = newHV();

      my_hv_store( streaminfo, "stream_number", newSViv(stream_number) );
      my_hv_store_ent( streaminfo, key, value );
      SvREFCNT_dec(key);

      av_push( streams, newRV_noinc( (SV *)streaminfo ) );
    }
  }
}

void
_store_tag(HV *tags, SV *key, SV *value)
{
  // if key exists, create array
  if ( my_hv_exists_ent( tags, key ) ) {
    SV **entry = my_hv_fetch( tags, SvPVX(key) );
    if (entry != NULL) {
      if ( SvROK(*entry) && SvTYPE(SvRV(*entry)) == SVt_PVAV ) {
        av_push( (AV *)SvRV(*entry), value );
      }
      else {
      // A non-array entry, convert to array.
        AV *ref = newAV();
        av_push( ref, newSVsv(*entry) );
        av_push( ref, value );
        my_hv_store_ent( tags, key, newRV_noinc( (SV*)ref ) );
      }
    }
  }
  else {
    my_hv_store_ent( tags, key, value );
  }

  SvREFCNT_dec(key);
}

int
_parse_index_objects(asfinfo *asf, int index_size)
{
  GUID tmp;
  uint64_t size;

  while (index_size > 0) {
    // Make sure we have enough data
    if ( !_check_buf(asf->infile, asf->buf, 24, ASF_BLOCK_SIZE) ) {
      return 0;
    }

    buffer_get_guid(asf->buf, &tmp);
    size = buffer_get_int64_le(asf->buf);

    if ( !_check_buf(asf->infile, asf->buf, size - 24, ASF_BLOCK_SIZE) ) {
      return 0;
    }

    if ( IsEqualGUID(&tmp, &ASF_Index) ) {
      DEBUG_TRACE("Index size %llu\n", size);
      _parse_index(asf, size - 24);
    }
    else if ( IsEqualGUID(&tmp, &ASF_Simple_Index) ) {
      DEBUG_TRACE("Skipping Simple_Index size %llu\n", size);
      // Simple Index is used for video files only
      buffer_consume(asf->buf, size - 24);
    }
    else {
      // Unhandled GUID
      PerlIO_printf(PerlIO_stderr(), "** Unhandled Index GUID: ");
      print_guid(tmp);
      PerlIO_printf(PerlIO_stderr(), "size: %llu\n", size);

      buffer_consume(asf->buf, size - 24);
    }

    index_size -= size;
  }

  return 1;
}

void
_parse_index(asfinfo *asf, uint64_t size)
{
  uint32_t time_interval;
  uint16_t spec_count;
  uint32_t block_count;
  uint32_t entry_count;
  int i, ec;

  time_interval = buffer_get_int_le(asf->buf);
  spec_count    = buffer_get_short_le(asf->buf);
  block_count   = buffer_get_int_le(asf->buf);

  // XXX ignore block_count > 1 for now, for files larger than 2^32
  if (block_count > 1) {
    buffer_consume(asf->buf, size);
    return;
  }

  DEBUG_TRACE("  time_interval %d, spec_count %d\n", time_interval, spec_count);

  asf->spec_count = spec_count;

  New(0, asf->specs, spec_count * sizeof(*asf->specs), struct asf_index_specs);

  DEBUG_TRACE("  Index Specifiers:\n");
  for (i = 0; i < spec_count; i++) {
    asf->specs[i].stream_number = buffer_get_short_le(asf->buf);
    asf->specs[i].index_type    = buffer_get_short_le(asf->buf);
    asf->specs[i].time_interval = time_interval;
    DEBUG_TRACE("    stream_number %d, index_type %d\n", asf->specs[i].stream_number, asf->specs[i].index_type);
  }

  entry_count = buffer_get_int_le(asf->buf);

  DEBUG_TRACE("  entry_count %d\n", entry_count);

  for (i = 0; i < spec_count; i++) {
    asf->specs[i].block_pos   = buffer_get_int64_le(asf->buf);
    asf->specs[i].entry_count = entry_count;
    DEBUG_TRACE("  specs[%d].block_pos %llu\n", i, asf->specs[i].block_pos);

    // allocate space for this spec's offsets
    New(0, asf->specs[i].offsets, entry_count * sizeof(uint32_t), uint32_t);
  }

  for (ec = 0; ec < entry_count; ec++) {
    for (i = 0; i < spec_count; i++) {
      // These are byte offsets relative to start of the first data packet,
      // so we add audio_offset here.  An additional 50 bytes are already added
      // to skip past the top-level Data Object
      asf->specs[i].offsets[ec] = asf->audio_offset + buffer_get_int_le(asf->buf);
      DEBUG_TRACE("  entry %d spec %d offset: %d\n", ec, i, asf->specs[i].offsets[ec]);
    }
  }
}

void
_parse_content_encryption(asfinfo *asf)
{
  uint32_t protection_type_len;
  uint32_t key_len;
  uint32_t license_url_len;

  // Skip secret data
  buffer_consume(asf->buf, buffer_get_int_le(asf->buf));

  protection_type_len = buffer_get_int_le(asf->buf);
  my_hv_store( asf->info, "drm_protection_type", newSVpvn( buffer_ptr(asf->buf), protection_type_len - 1 ) );
  buffer_consume(asf->buf, protection_type_len);

  key_len = buffer_get_int_le(asf->buf);
  my_hv_store( asf->info, "drm_key", newSVpvn( buffer_ptr(asf->buf), key_len - 1 ) );
  buffer_consume(asf->buf, key_len);

  license_url_len = buffer_get_int_le(asf->buf);
  my_hv_store( asf->info, "drm_license_url", newSVpvn( buffer_ptr(asf->buf), license_url_len - 1 ) );
  buffer_consume(asf->buf, license_url_len);
}

void
_parse_extended_content_encryption(asfinfo *asf)
{
  uint32_t len = buffer_get_int_le(asf->buf);
  SV *value;
  unsigned char *tmp_ptr = buffer_ptr(asf->buf);

  if ( tmp_ptr[0] == 0xFF && tmp_ptr[1] == 0xFE ) {
    buffer_consume(asf->buf, 2);
    buffer_init_or_clear(asf->scratch, len - 2);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, len - 2, UTF16_BYTEORDER_LE);
    value = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(value);

    my_hv_store( asf->info, "drm_data", value );
  }
  else {
    buffer_consume(asf->buf, len);
  }
}

void
_parse_script_command(asfinfo *asf)
{
  uint16_t command_count;
  uint16_t type_count;
  AV *types = newAV();
  AV *commands = newAV();

  buffer_init_or_clear(asf->scratch, 32);

  // Skip reserved
  buffer_consume(asf->buf, 16);

  command_count = buffer_get_short_le(asf->buf);
  type_count    = buffer_get_short_le(asf->buf);

  while ( type_count-- ) {
    SV *value;
    uint16_t len = buffer_get_short_le(asf->buf);

    buffer_clear(asf->scratch);
    buffer_get_utf16_as_utf8(asf->buf, asf->scratch, len * 2, UTF16_BYTEORDER_LE);
    value = newSVpv( buffer_ptr(asf->scratch), 0 );
    sv_utf8_decode(value);

    av_push( types, value );
  }

  while ( command_count-- ) {
    HV *command = newHV();
    SV *value;

    uint32_t pres_time  = buffer_get_int_le(asf->buf);
    uint16_t type_index = buffer_get_short_le(asf->buf);
    uint16_t name_len   = buffer_get_short_le(asf->buf);

    if (name_len) {
      buffer_clear(asf->scratch);
      buffer_get_utf16_as_utf8(asf->buf, asf->scratch, name_len * 2, UTF16_BYTEORDER_LE);
      value = newSVpv( buffer_ptr(asf->scratch), 0 );
      sv_utf8_decode(value);
      my_hv_store( command, "command", value );
    }

    my_hv_store( command, "time", newSVuv(pres_time) );
    my_hv_store( command, "type", newSVuv(type_index) );

    av_push( commands, newRV_noinc( (SV *)command ) );
  }

  my_hv_store( asf->info, "script_types", newRV_noinc( (SV *)types ) );
  my_hv_store( asf->info, "script_commands", newRV_noinc( (SV *)commands ) );
}

SV *
_parse_picture(asfinfo *asf, uint32_t picture_offset)
{
  char *tmp_ptr;
  uint16_t mime_len = 2; // to handle double-null
  uint16_t desc_len = 2;
  uint32_t image_len;
  SV *mime;
  SV *desc;
  HV *picture = newHV();

  buffer_init_or_clear(asf->scratch, 32);

  my_hv_store( picture, "image_type", newSVuv( buffer_get_char(asf->buf) ) );

  image_len = buffer_get_int_le(asf->buf);

  // MIME type is a double-null-terminated UTF-16 string
  tmp_ptr = buffer_ptr(asf->buf);
  while ( tmp_ptr[0] != '\0' || tmp_ptr[1] != '\0' ) {
    mime_len += 2;
    tmp_ptr += 2;
  }

  buffer_get_utf16_as_utf8(asf->buf, asf->scratch, mime_len, UTF16_BYTEORDER_LE);
  mime = newSVpv( buffer_ptr(asf->scratch), 0 );
  sv_utf8_decode(mime);
  my_hv_store( picture, "mime_type", mime );

  // Description is a double-null-terminated UTF-16 string
  tmp_ptr = buffer_ptr(asf->buf);
  while ( tmp_ptr[0] != '\0' || tmp_ptr[1] != '\0' ) {
    desc_len += 2;
    tmp_ptr += 2;
  }

  buffer_clear(asf->scratch);
  buffer_get_utf16_as_utf8(asf->buf, asf->scratch, desc_len, UTF16_BYTEORDER_LE);
  desc = newSVpv( buffer_ptr(asf->scratch), 0 );
  sv_utf8_decode(desc);
  my_hv_store( picture, "description", desc );

  if ( _env_true("AUDIO_SCAN_NO_ARTWORK") ) {
    my_hv_store( picture, "image", newSVuv(image_len) );
    picture_offset += 5 + mime_len + desc_len + 2;
    my_hv_store( picture, "offset", newSVuv(asf->object_offset + picture_offset) );
  }
  else {
    my_hv_store( picture, "image", newSVpvn( buffer_ptr(asf->buf), image_len ) );
  }

  buffer_consume(asf->buf, image_len);

  return newRV_noinc( (SV *)picture );
}

// offset is in ms
// Based on some code from Rockbox
int
asf_find_frame(PerlIO *infile, char *file, int time_offset)
{
  int frame_offset = -1;
  uint32_t song_length_ms;
  int32_t offset_index = 0;
  uint32_t min_packet_size, max_packet_size;
  uint8_t found = 0;

  // We need to read all info first to get some data we need to calculate
  HV *info = newHV();
  HV *tags = newHV();
  asfinfo *asf = _asf_parse(infile, file, info, tags, 1);

  // We'll need to reuse the scratch buffer
  Newz(0, asf->scratch, sizeof(Buffer), Buffer);

  // No seeking without at least 1 stream
  if ( !my_hv_exists(info, "streams") ) {
    DEBUG_TRACE("No streams found in file, not seeking\n");
    goto out;
  }

  min_packet_size = SvIV( *(my_hv_fetch(info, "min_packet_size")) );
  max_packet_size = SvIV( *(my_hv_fetch(info, "max_packet_size")) );

  // No seeking if min != max, according to the ASF spec these must be the same
  // and without this value we can't find the data packets properly
  if (min_packet_size != max_packet_size) {
    DEBUG_TRACE("min_packet_size != max_packet_size, cannot seek\n");
    goto out;
  }

  song_length_ms = SvIV( *(my_hv_fetch( info, "song_length_ms" )) );

  if (time_offset > song_length_ms)
    time_offset = song_length_ms;

  // Use ASF_Index if available
  if ( asf->spec_count ) {
    // Use the index to find the nearest offset
    offset_index = time_offset / asf->specs[0].time_interval;

    if (offset_index >= asf->specs[0].entry_count)
      offset_index = asf->specs[0].entry_count - 1;

    // An offset may be -1 so look backwards if we find one of those
    while (frame_offset == -1 && offset_index >= 0) {
      frame_offset = asf->specs[0].offsets[offset_index];

      // XXX should add asf->specs[0].block_pos here, but since we
      // aren't supporting 64-bit it should always be 0

      DEBUG_TRACE(
        "offset_index for %d / %d: %d = %d\n",
        time_offset, asf->specs[0].time_interval, offset_index, frame_offset
      );

      offset_index--;
    }
  }

  // Calculate seek position using bitrate
  else if (asf->max_bitrate) {
    float bytes_per_ms = asf->max_bitrate / 8000.0;
    int packet = (int)((bytes_per_ms * time_offset) / max_packet_size);

    frame_offset = asf->audio_offset + (packet * max_packet_size);

    DEBUG_TRACE("seeking to data packet %d @ %d, via max_bitrate (bytes_per_ms %.2f, time_offset %d, packet size %d)\n",
      packet, frame_offset, bytes_per_ms, time_offset, max_packet_size);
  }
  else {
    // No ASF_Index, no max_bitrate, probably an invalid file
    goto out;
  }

  // Double-check above frame, make sure we have the right one
  // with a timestamp within our desired range
  while ( !found && frame_offset >= 0 ) {
    int time, duration;

    DEBUG_TRACE("Checking for frame with timestamp %d at %d\n", time_offset, frame_offset);

    if ( frame_offset > asf->file_size - 64 ) {
      DEBUG_TRACE("  Offset too large: %d\n", frame_offset);
      break;
    }

    time = _timestamp(asf, frame_offset, &duration);

    DEBUG_TRACE("  Timestamp for frame at %d: %d, duration: %d\n", frame_offset, time, duration);

    if (time < 0) {
      DEBUG_TRACE("  Invalid timestamp, giving up\n");
      break;
    }

    if ( time + duration >= time_offset && time <= time_offset ) {
      DEBUG_TRACE("  Found frame at offset %d\n", frame_offset);
      found = 1;
    }
    else {
      int delta = time_offset - time;

      DEBUG_TRACE("  Wrong frame, delta: %d\n", delta);

      if (
        (delta < 0 && (frame_offset - max_packet_size) < asf->audio_offset)
        ||
        (delta > 0 && (frame_offset + max_packet_size) > (asf->audio_offset + asf->audio_size - 64))
      ) {
        // Reached the first/last audio packet, break out
        DEBUG_TRACE("  Giving up, reached the beginning or end of audio\n");
        break;
      }

      // XXX probably could be more efficient using a binary search,
      // but with the use of an index we should already be very close to the right place
      if (delta > 0) {
        frame_offset += max_packet_size;
      }
      else {
        frame_offset -= max_packet_size;
      }
    }
  }

out:
  // Don't leak
  SvREFCNT_dec(info);
  SvREFCNT_dec(tags);

  if (asf->spec_count) {
    int i;
    for (i = 0; i < asf->spec_count; i++) {
      DEBUG_TRACE("Freeing specs[%d] offsets\n", i);
      Safefree(asf->specs[i].offsets);
    }

    DEBUG_TRACE("Freeing specs\n");
    Safefree(asf->specs);
  }

  if (asf->scratch->alloc)
    buffer_free(asf->scratch);
  Safefree(asf->scratch);

  Safefree(asf);

  return frame_offset;
}

// Return the timestamp of the data packet at offset
int
_timestamp(asfinfo *asf, int offset, int *duration)
{
  int timestamp = -1;
  uint8_t tmp;

  if ((PerlIO_seek(asf->infile, offset, SEEK_SET)) != 0) {
    return -1;
  }

  buffer_init_or_clear(asf->scratch, 64);

  if ( !_check_buf(asf->infile, asf->scratch, 64, 64) ) {
    goto out;
  }

  // Read Error Correction Flags
  tmp = buffer_get_char(asf->scratch);

  if (tmp & 0x80) {
    // Skip error correction data
    buffer_consume(asf->scratch, tmp & 0x0f);

    // Read Length Type Flags
    tmp = buffer_get_char(asf->scratch);
  }
  else {
    // The byte we already read is Length Type Flags
  }

  // Skip Property Flags, Packet Length, Sequence, Padding Length
  buffer_consume( asf->scratch,
    1 + GETLEN2b((tmp >> 1) & 0x03) +
        GETLEN2b((tmp >> 3) & 0x03) +
        GETLEN2b((tmp >> 5) & 0x03)
  );

  timestamp = buffer_get_int_le(asf->scratch);
  *duration = buffer_get_short_le(asf->scratch);

out:
  return timestamp;
}