/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * This file is derived from mt-daap project. */ #include "mp3.h" int get_mp3fileinfo(PerlIO *infile, char *file, HV *info) { mp3info *mp3 = _mp3_parse(infile, file, info); buffer_free(mp3->buf); Safefree(mp3->buf); Safefree(mp3->first_frame); Safefree(mp3->xing_frame); Safefree(mp3); return 0; } int get_mp3tags(PerlIO *infile, char *file, HV *info, HV *tags) { int ret; off_t file_size = _file_size(infile); // See if this file has an APE tag as fast as possible // This is still a big performance hit :( if ( _has_ape(infile, file_size, info) ) { get_ape_metadata(infile, file, info, tags); } ret = parse_id3(infile, file, info, tags, 0, file_size); return ret; } int _is_ape_header(char *bptr) { if ( bptr[0] == 'A' && bptr[1] == 'P' && bptr[2] == 'E' && bptr[3] == 'T' && bptr[4] == 'A' && bptr[5] == 'G' && bptr[6] == 'E' && bptr[7] == 'X' ) { return 1; } return 0; } int _has_ape(PerlIO *infile, off_t file_size, HV *info) { Buffer buf; uint8_t ret = 0; char *bptr; if ( (PerlIO_seek(infile, file_size - 160, SEEK_SET)) == -1 ) { return 0; } DEBUG_TRACE("Seeked to %d looking for APE tag\n", (int)PerlIO_tell(infile)); // Bug 9942, read 136 bytes so we can check at -32 bytes in case file // does not have an ID3v1 tag buffer_init(&buf, 136); if ( !_check_buf(infile, &buf, 136, 136) ) { goto out; } bptr = buffer_ptr(&buf); if ( _is_ape_header(bptr) ) { DEBUG_TRACE("APE tag found at -160 (with ID3v1)\n"); ret = 1; } else { // Look for Lyrics tag which may possibly be between APE and ID3v1 bptr += 23; if ( bptr[0] == 'L' && bptr[1] == 'Y' && bptr[2] == 'R' && bptr[3] == 'I' && bptr[4] == 'C' && bptr[5] == 'S' && bptr[6] == '2' && bptr[7] == '0' && bptr[8] == '0' ) { // read Lyrics tag size, stored as a 6-digit number (!?) // http://www.id3.org/Lyrics3v2 uint32_t lyrics_size = 0; off_t file_size = _file_size(infile); bptr -= 6; lyrics_size = atoi(bptr); DEBUG_TRACE("LYRICS200 tag found (size %d), adjusting APE offset (%d)\n", lyrics_size, -(160 + lyrics_size + 15)); if ( (PerlIO_seek(infile, file_size - (160 + lyrics_size + 15), SEEK_SET)) == -1 ) { goto out; } DEBUG_TRACE("Seeked before Lyrics tag to %d\n", (int)PerlIO_tell(infile)); buffer_clear(&buf); if ( !_check_buf(infile, &buf, 136, 136) ) { goto out; } if ( _is_ape_header( buffer_ptr(&buf) ) ) { DEBUG_TRACE("APE tag found at %d (ID3v1 + Lyricsv2)\n", -(160 + lyrics_size + 15)); ret = 1; goto out; } // APE code will remove the lyrics_size from audio_size, but if no APE tag do it here if (my_hv_exists(info, "audio_size")) { int audio_size = SvIV(*(my_hv_fetch(info, "audio_size"))); my_hv_store(info, "audio_size", newSVuv(audio_size - lyrics_size - 15)); DEBUG_TRACE("Reduced audio_size value by Lyrics2 tag size %d\n", lyrics_size + 15); } } // APE tag without ID3v1 tag will be -32 bytes from end buffer_consume(&buf, 128); bptr = buffer_ptr(&buf); if ( _is_ape_header(bptr) ) { DEBUG_TRACE("APE tag found at -32 (no ID3v1)\n"); ret = 1; } } out: buffer_free(&buf); return ret; } // _decode_mp3_frame, based on pcutmp3 FrameHeader.decode() int _decode_mp3_frame(unsigned char *bptr, struct mp3frame *frame) { int i; frame->header32 = GET_INT32BE(bptr); frame->mpegID = (frame->header32 >> 19) & 3; frame->layerID = (frame->header32 >> 17) & 3; frame->crc16_used = (frame->header32 & 0x00010000) == 0; frame->bitrate_index = (frame->header32 >> 12) & 0xF; frame->samplingrate_index = (frame->header32 >> 10) & 3; frame->padding = (frame->header32 & 0x00000200) != 0; frame->private_bit_set = (frame->header32 & 0x00000100) != 0; frame->mode = (frame->header32 >> 6) & 3; frame->mode_extension = (frame->header32 >> 4) & 3; frame->copyrighted = (frame->header32 & 0x00000008) != 0; frame->original = (frame->header32 & 0x00000004) == 0; // bit set -> copy frame->emphasis = frame->header32 & 3; frame->valid = (frame->mpegID != ILLEGAL_MPEG_ID) && (frame->layerID != ILLEGAL_LAYER_ID) && (frame->bitrate_index != 0) && (frame->bitrate_index != 15) && (frame->samplingrate_index != ILLEGAL_SR); if (!frame->valid) { return -1; } frame->samplerate = sample_rate_tbl[ frame->samplingrate_index ]; if (frame->mpegID == MPEG2_ID) frame->samplerate >>= 1; // 16,22,48 kHz if (frame->mpegID == MPEG25_ID) frame->samplerate >>= 2; // 8,11,24 kHz frame->channels = (frame->mode == MODE_MONO) ? 1 : 2; frame->bitrate_kbps = bitrate_map[ frame->mpegID ][ frame->layerID ][ frame->bitrate_index ]; if (frame->layerID == LAYER1_ID) { // layer 1: always 384 samples/frame and 4byte-slots frame->samples_per_frame = 384; frame->bytes_per_slot = 4; } else { // layer 2: always 1152 samples/frame // layer 3: MPEG1: 1152 samples/frame, MPEG2/2.5: 576 samples/frame frame->samples_per_frame = ((frame->mpegID == MPEG1_ID) || (frame->layerID == LAYER2_ID)) ? 1152 : 576; frame->bytes_per_slot = 1; } frame->frame_size = ((frame->bitrate_kbps * 125) * frame->samples_per_frame) / frame->samplerate; if (frame->bytes_per_slot > 1) frame->frame_size -= frame->frame_size % frame->bytes_per_slot; if (frame->padding) frame->frame_size += frame->bytes_per_slot; DEBUG_TRACE("Frame @%p: size=%d, %d samples, %dkbps %d/%d\n", bptr, frame->frame_size, frame->samples_per_frame, frame->bitrate_kbps, frame->samplerate, frame->channels); return 0; } // _mp3_get_average_bitrate // average bitrate by averaging all the frames in the file. This used // to seek to the middle of the file and take a 32K chunk but this was // found to have bugs if it seeked near invalid FF sync bytes that could // be detected as a real frame static short _mp3_get_average_bitrate(mp3info *mp3, uint32_t offset, uint32_t audio_size) { struct mp3frame frame; int frame_count = 0; int bitrate_total = 0; int err = 0; int done = 0; int wrap_skip = 0; int prev_bitrate = 0; bool vbr = FALSE; unsigned char *bptr; buffer_clear(mp3->buf); // Seek to offset PerlIO_seek(mp3->infile, offset, SEEK_SET); while ( done < audio_size - 4 ) { // Buffer size is optimized for a possible common case: 20 frames of 192kbps CBR if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE * 3) ) { err = -1; goto out; } done += buffer_len(mp3->buf); if (wrap_skip) { // Skip rest of frame from last buffer DEBUG_TRACE("Wrapped, consuming %d bytes from previous frame\n", wrap_skip); buffer_consume(mp3->buf, wrap_skip); wrap_skip = 0; } while ( buffer_len(mp3->buf) >= 4 ) { bptr = buffer_ptr(mp3->buf); while ( *bptr != 0xFF ) { buffer_consume(mp3->buf, 1); if ( buffer_len(mp3->buf) < 4 ) { // ran out of data goto out; } bptr = buffer_ptr(mp3->buf); } if ( !_decode_mp3_frame( buffer_ptr(mp3->buf), &frame ) ) { // Found a valid frame frame_count++; bitrate_total += frame.bitrate_kbps; if ( !vbr ) { // If we see the bitrate changing, we have a VBR file, and read // the entire file. Otherwise, if we see 20 frames with the same // bitrate, assume CBR and stop if (prev_bitrate > 0 && prev_bitrate != frame.bitrate_kbps) { DEBUG_TRACE("Bitrate changed, assuming file is VBR\n"); vbr = TRUE; } else { if (frame_count > 20) { DEBUG_TRACE("Found 20 frames with same bitrate, assuming CBR\n"); goto out; } prev_bitrate = frame.bitrate_kbps; } } //DEBUG_TRACE(" Frame %d: %dkbps, %dkHz\n", frame_count, frame.bitrate_kbps, frame.samplerate); if (frame.frame_size > buffer_len(mp3->buf)) { // Partial frame in buffer wrap_skip = frame.frame_size - buffer_len(mp3->buf); buffer_consume(mp3->buf, buffer_len(mp3->buf)); } else { buffer_consume(mp3->buf, frame.frame_size); } } else { // Not a valid frame, stray 0xFF buffer_consume(mp3->buf, 1); } } } out: if (err) return err; if (!frame_count) return -1; DEBUG_TRACE("Average of %d frames: %dkbps\n", frame_count, bitrate_total / frame_count); return bitrate_total / frame_count; } static int _parse_xing(mp3info *mp3) { int i; unsigned char *bptr; int xing_offset = 4; if (mp3->first_frame->mpegID == MPEG1_ID) { xing_offset += mp3->first_frame->channels == 2 ? 32 : 17; } else { xing_offset += mp3->first_frame->channels == 2 ? 17 : 9; } if ( !_check_buf(mp3->infile, mp3->buf, 4 + xing_offset, MP3_BLOCK_SIZE) ) { return 0; } buffer_consume(mp3->buf, xing_offset); bptr = buffer_ptr(mp3->buf); if ( bptr[0] == 'X' || bptr[0] == 'I' ) { if ( ( bptr[1] == 'i' && bptr[2] == 'n' && bptr[3] == 'g' ) || ( bptr[1] == 'n' && bptr[2] == 'f' && bptr[3] == 'o' ) ) { DEBUG_TRACE("Found Xing/Info tag\n"); mp3->xing_frame->xing_tag = bptr[0] == 'X'; mp3->xing_frame->info_tag = bptr[0] == 'I'; mp3->xing_frame->frame_size = mp3->first_frame->frame_size; if ( !_check_buf(mp3->infile, mp3->buf, 160, MP3_BLOCK_SIZE) ) { return 0; } // It's VBR if tag is Xing, and CBR if Info mp3->vbr = bptr[1] == 'i' ? VBR : CBR; buffer_consume(mp3->buf, 4); mp3->xing_frame->flags = buffer_get_int(mp3->buf); if (mp3->xing_frame->flags & XING_FRAMES) { mp3->xing_frame->xing_frames = buffer_get_int(mp3->buf); } if ( mp3->xing_frame->flags & XING_BYTES) { mp3->xing_frame->xing_bytes = buffer_get_int(mp3->buf); } if (mp3->xing_frame->flags & XING_TOC) { uint8_t i; bptr = buffer_ptr(mp3->buf); for (i = 0; i < 100; i++) { mp3->xing_frame->xing_toc[i] = bptr[i]; } mp3->xing_frame->has_toc = 1; buffer_consume(mp3->buf, 100); } if (mp3->xing_frame->flags & XING_QUALITY) { mp3->xing_frame->xing_quality = buffer_get_int(mp3->buf); } // LAME tag bptr = buffer_ptr(mp3->buf); if ( bptr[0] == 'L' && bptr[1] == 'A' && bptr[2] == 'M' && bptr[3] == 'E' ) { mp3->xing_frame->lame_tag = TRUE; strncpy(mp3->xing_frame->lame_encoder_version, (char *)bptr, 9); bptr += 9; // revision/vbr method byte mp3->xing_frame->lame_tag_revision = bptr[0] >> 4; mp3->xing_frame->lame_vbr_method = bptr[0] & 15; buffer_consume(mp3->buf, 10); // Determine vbr status switch (mp3->xing_frame->lame_vbr_method) { case 1: case 8: mp3->vbr = CBR; break; case 2: case 9: mp3->vbr = ABR; break; default: mp3->vbr = VBR; } mp3->xing_frame->lame_lowpass = buffer_get_char(mp3->buf) * 100; // Skip peak buffer_consume(mp3->buf, 4); // Replay Gain, code from mpg123 mp3->xing_frame->lame_replay_gain[0] = 0; mp3->xing_frame->lame_replay_gain[1] = 0; for (i=0; i<2; i++) { // Originator unsigned char origin; bptr = buffer_ptr(mp3->buf); origin = (bptr[0] >> 2) & 0x7; if (origin != 0) { // Gain type unsigned char gt = bptr[0] >> 5; if (gt == 1) gt = 0; /* radio */ else if (gt == 2) gt = 1; /* audiophile */ else continue; mp3->xing_frame->lame_replay_gain[gt] = (( (bptr[0] & 0x4) >> 2 ) ? -0.1 : 0.1) * ( (bptr[0] & 0x3) | bptr[1] ); } buffer_consume(mp3->buf, 2); } // Skip encoding flags buffer_consume(mp3->buf, 1); // ABR rate/VBR minimum mp3->xing_frame->lame_abr_rate = buffer_get_char(mp3->buf); // Encoder delay/padding bptr = buffer_ptr(mp3->buf); mp3->xing_frame->lame_encoder_delay = ((((int)bptr[0]) << 4) | (((int)bptr[1]) >> 4)); mp3->xing_frame->lame_encoder_padding = (((((int)bptr[1]) << 8) | (((int)bptr[2]))) & 0xfff); // sanity check if (mp3->xing_frame->lame_encoder_delay < 0 || mp3->xing_frame->lame_encoder_delay > 3000) { mp3->xing_frame->lame_encoder_delay = -1; } if (mp3->xing_frame->lame_encoder_padding < 0 || mp3->xing_frame->lame_encoder_padding > 3000) { mp3->xing_frame->lame_encoder_padding = -1; } buffer_consume(mp3->buf, 3); // Misc bptr = buffer_ptr(mp3->buf); mp3->xing_frame->lame_noise_shaping = bptr[0] & 0x3; mp3->xing_frame->lame_stereo_mode = (bptr[0] & 0x1C) >> 2; mp3->xing_frame->lame_unwise = (bptr[0] & 0x20) >> 5; mp3->xing_frame->lame_source_freq = (bptr[0] & 0xC0) >> 6; buffer_consume(mp3->buf, 1); // XXX MP3 Gain, can't find a test file, current // mp3gain doesn't write this data /* bptr = buffer_ptr(mp3->buf); unsigned char sign = (bptr[0] & 0x80) >> 7; mp3->xing_frame->lame_mp3gain = bptr[0] & 0x7F; if (sign) { mp3->xing_frame->lame_mp3gain *= -1; } mp3->xing_frame->lame_mp3gain_db = mp3->xing_frame->lame_mp3gain * 1.5; */ buffer_consume(mp3->buf, 1); // Preset/Surround bptr = buffer_ptr(mp3->buf); mp3->xing_frame->lame_surround = (bptr[0] & 0x38) >> 3; mp3->xing_frame->lame_preset = ((bptr[0] << 8) | bptr[1]) & 0x7ff; buffer_consume(mp3->buf, 2); // Music Length mp3->xing_frame->lame_music_length = buffer_get_int(mp3->buf); // Skip CRCs } } } // Check for VBRI header from Fhg encoders else if ( bptr[0] == 'V' && bptr[1] == 'B' && bptr[2] == 'R' && bptr[3] == 'I' ) { DEBUG_TRACE("Found VBRI tag\n"); mp3->xing_frame->vbri_tag = TRUE; mp3->vbr = VBR; if ( !_check_buf(mp3->infile, mp3->buf, 14, MP3_BLOCK_SIZE) ) { return 0; } // Skip tag and version ID buffer_consume(mp3->buf, 6); mp3->xing_frame->vbri_delay = buffer_get_short(mp3->buf); mp3->xing_frame->vbri_quality = buffer_get_short(mp3->buf); mp3->xing_frame->vbri_bytes = buffer_get_int(mp3->buf); mp3->xing_frame->vbri_frames = buffer_get_int(mp3->buf); } return 1; } static int _is_mp3x_profile(mp3info *mp3) { if (mp3->first_frame->layerID != LAYER3_ID) return 0; if (mp3->first_frame->mpegID != MPEG1_ID && mp3->first_frame->mpegID != MPEG2_ID) return 0; if (mp3->first_frame->samplerate != 16000 && mp3->first_frame->samplerate != 22050 && mp3->first_frame->samplerate != 24000) return 0; if (mp3->bitrate >= 8 && mp3->bitrate <= 320) return 1; return 0; } static int _is_mp3_profile(mp3info *mp3) { if (mp3->first_frame->layerID != LAYER3_ID) return 0; if (mp3->first_frame->mpegID != MPEG1_ID) return 0; if (mp3->first_frame->samplerate != 32000 && mp3->first_frame->samplerate != 44100 && mp3->first_frame->samplerate != 48000) return 0; if (mp3->bitrate >= 32 && mp3->bitrate <= 320) return 1; return 0; } mp3info * _mp3_parse(PerlIO *infile, char *file, HV *info) { unsigned char *bptr; char id3v1taghdr[4]; uint32_t song_length_ms = 0; uint64_t total_samples = 0; struct mp3frame frame; bool found_first_frame = FALSE; mp3info *mp3; Newz(0, mp3, sizeof(mp3info), mp3info); Newz(0, mp3->buf, sizeof(Buffer), Buffer); Newz(0, mp3->first_frame, sizeof(mp3frame), mp3frame); Newz(0, mp3->xing_frame, sizeof(xingframe), xingframe); mp3->infile = infile; mp3->file = file; mp3->info = info; mp3->file_size = _file_size(infile); mp3->id3_size = 0; mp3->audio_offset = 0; mp3->audio_size = 0; mp3->bitrate = 0; buffer_init(mp3->buf, MP3_BLOCK_SIZE); my_hv_store( info, "file_size", newSVuv(mp3->file_size) ); if ( !_check_buf(mp3->infile, mp3->buf, 10, MP3_BLOCK_SIZE) ) { goto out; } bptr = buffer_ptr(mp3->buf); if ( (bptr[0] == 'I' && bptr[1] == 'D' && bptr[2] == '3') && bptr[3] < 0xff && bptr[4] < 0xff && bptr[6] < 0x80 && bptr[7] < 0x80 && bptr[8] < 0x80 && bptr[9] < 0x80 ) { /* found an ID3 header... */ mp3->id3_size = 10 + (bptr[6]<<21) + (bptr[7]<<14) + (bptr[8]<<7) + bptr[9]; if (bptr[5] & 0x10) { // footer present mp3->id3_size += 10; } DEBUG_TRACE("Found ID3v2.%d.%d tag, size %d\n", bptr[3], bptr[4], mp3->id3_size); // Always seek past the ID3 tags _mp3_skip(mp3, mp3->id3_size); if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) { goto out; } mp3->audio_offset += mp3->id3_size; } // Find an MP3 frame while ( !found_first_frame && buffer_len(mp3->buf) ) { bptr = buffer_ptr(mp3->buf); while ( *bptr != 0xFF ) { buffer_consume(mp3->buf, 1); mp3->audio_offset++; if ( !buffer_len(mp3->buf) ) { if (mp3->audio_offset >= mp3->file_size - 4) { // No audio frames in file warn("Unable to find any MP3 frames in file: %s\n", file); goto out; } if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) { warn("Unable to find any MP3 frames in file: %s\n", file); goto out; } } bptr = buffer_ptr(mp3->buf); } DEBUG_TRACE("Found FF sync at offset %d\n", (int)mp3->audio_offset); // Make sure we have 4 bytes if ( !_check_buf(mp3->infile, mp3->buf, 4, MP3_BLOCK_SIZE) ) { goto out; } if ( !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf), &frame ) ) { struct mp3frame frame2, frame3; // Need the whole frame to consider it valid if ( _check_buf(mp3->infile, mp3->buf, frame.frame_size, MP3_BLOCK_SIZE) // If we have enough data for the start of the next frame then // it must also look valid and be consistent && ( !_check_buf(mp3->infile, mp3->buf, frame.frame_size + 4, MP3_BLOCK_SIZE) || ( !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf) + frame.frame_size, &frame2 ) && frame.samplerate == frame2.samplerate && frame.channels == frame2.channels ) ) // If we have enough data for the start of the over-next frame then // it must also look valid and be consistent && ( !_check_buf(mp3->infile, mp3->buf, frame.frame_size + frame2.frame_size + 4, MP3_BLOCK_SIZE) || ( !_decode_mp3_frame( (unsigned char *)buffer_ptr(mp3->buf) + frame.frame_size + frame2.frame_size, &frame3 ) && frame.samplerate == frame3.samplerate && frame.channels == frame3.channels ) ) ) { // Found a valid frame DEBUG_TRACE(" valid frame\n"); found_first_frame = 1; } else { DEBUG_TRACE(" false sync\n"); } } if (!found_first_frame) { // Not a valid frame, stray 0xFF DEBUG_TRACE(" invalid frame\n"); buffer_consume(mp3->buf, 1); mp3->audio_offset++; } } if ( !found_first_frame ) { warn("Unable to find any MP3 frames in file (checked 4K): %s\n", file); goto out; } mp3->audio_size = mp3->file_size - mp3->audio_offset; memcpy(mp3->first_frame, &frame, sizeof(mp3frame)); // now check for Xing/Info/VBRI/LAME headers if ( !_parse_xing(mp3) ) { goto out; } // use LAME CBR/ABR value for bitrate if available if ( (mp3->vbr == CBR || mp3->vbr == ABR) && mp3->xing_frame->lame_abr_rate ) { if (mp3->xing_frame->lame_abr_rate >= 255) { // ABR rate field only codes up to 255, use preset value instead if (mp3->xing_frame->lame_preset <= 320) { mp3->bitrate = mp3->xing_frame->lame_preset; DEBUG_TRACE("bitrate from lame_preset: %d\n", mp3->bitrate); } } else { mp3->bitrate = mp3->xing_frame->lame_abr_rate; DEBUG_TRACE("bitrate from lame_abr_rate: %d\n", mp3->bitrate); } } // Or if we have a Xing header, use it to determine bitrate if (!mp3->bitrate && (mp3->xing_frame->xing_frames && mp3->xing_frame->xing_bytes)) { float mfs = (float)frame.samplerate / ( frame.mpegID == MPEG2_ID || frame.mpegID == MPEG25_ID ? 72000. : 144000. ); mp3->bitrate = ( mp3->xing_frame->xing_bytes / mp3->xing_frame->xing_frames * mfs ); DEBUG_TRACE("bitrate from Xing header: %d\n", mp3->bitrate); } // Or use VBRI header else if (mp3->xing_frame->vbri_frames && mp3->xing_frame->vbri_bytes) { float mfs = (float)frame.samplerate / ( frame.mpegID == MPEG2_ID || frame.mpegID == MPEG25_ID ? 72000. : 144000. ); mp3->bitrate = ( mp3->xing_frame->vbri_bytes / mp3->xing_frame->vbri_frames * mfs ); DEBUG_TRACE("bitrate from VBRI header: %d\n", mp3->bitrate); } // check if last 128 bytes is ID3v1.0 or ID3v1.1 tag PerlIO_seek(infile, mp3->file_size - 128, SEEK_SET); if (PerlIO_read(infile, id3v1taghdr, 4) == 4) { if (id3v1taghdr[0]=='T' && id3v1taghdr[1]=='A' && id3v1taghdr[2]=='G') { DEBUG_TRACE("ID3v1 tag found\n"); mp3->audio_size -= 128; } } // If we don't know the bitrate from Xing/LAME/VBRI, calculate average if ( !mp3->bitrate ) { DEBUG_TRACE("Calculating average bitrate starting from %d...\n", (int)mp3->audio_offset); mp3->bitrate = _mp3_get_average_bitrate(mp3, mp3->audio_offset, mp3->audio_size); if (mp3->bitrate <= 0) { // Couldn't determine bitrate, just use // the bitrate from the last frame we parsed DEBUG_TRACE("Unable to determine bitrate, using bitrate of most recent frame (%d)\n", frame.bitrate_kbps); mp3->bitrate = frame.bitrate_kbps; } } if (mp3->xing_frame->xing_frames) { total_samples = mp3->xing_frame->xing_frames * frame.samples_per_frame; if (mp3->xing_frame->lame_tag) { // subtract delay/padding to get accurate sample count total_samples -= (mp3->xing_frame->lame_encoder_delay + mp3->xing_frame->lame_encoder_padding); } song_length_ms = (int) ((double)(total_samples * 1000.) / (double) frame.samplerate); } else if (mp3->xing_frame->vbri_frames) { song_length_ms = (int) ((double)(mp3->xing_frame->vbri_frames * frame.samples_per_frame * 1000.)/ (double) frame.samplerate); total_samples = mp3->xing_frame->vbri_frames * frame.samples_per_frame; } else { song_length_ms = (int) ((double)mp3->audio_size * 8. / (double)mp3->bitrate); } mp3->song_length_ms = song_length_ms; my_hv_store( info, "song_length_ms", newSVuv(song_length_ms) ); my_hv_store( info, "layer", newSVuv(frame.layerID) ); my_hv_store( info, "stereo", newSVuv(frame.channels == 2 ? 1 : 0) ); my_hv_store( info, "samples_per_frame", newSVuv(frame.samples_per_frame) ); my_hv_store( info, "padding", newSVuv(frame.padding) ); my_hv_store( info, "audio_size", newSVuv(mp3->audio_size) ); my_hv_store( info, "audio_offset", newSVuv(mp3->audio_offset) ); my_hv_store( info, "bitrate", newSVuv( mp3->bitrate * 1000 ) ); my_hv_store( info, "samplerate", newSVuv( frame.samplerate ) ); if (mp3->xing_frame->xing_tag || mp3->xing_frame->info_tag) { if (mp3->xing_frame->xing_frames) { my_hv_store( info, "xing_frames", newSVuv(mp3->xing_frame->xing_frames) ); } if (mp3->xing_frame->xing_bytes) { my_hv_store( info, "xing_bytes", newSVuv(mp3->xing_frame->xing_bytes) ); } if (mp3->xing_frame->has_toc) { uint8_t i; AV *xing_toc = newAV(); for (i = 0; i < 100; i++) { av_push( xing_toc, newSVuv(mp3->xing_frame->xing_toc[i]) ); } my_hv_store( info, "xing_toc", newRV_noinc( (SV *)xing_toc ) ); } if (mp3->xing_frame->xing_quality) { my_hv_store( info, "xing_quality", newSVuv(mp3->xing_frame->xing_quality) ); } } if (mp3->xing_frame->vbri_tag) { my_hv_store( info, "vbri_delay", newSVuv(mp3->xing_frame->vbri_delay) ); my_hv_store( info, "vbri_frames", newSVuv(mp3->xing_frame->vbri_frames) ); my_hv_store( info, "vbri_bytes", newSVuv(mp3->xing_frame->vbri_bytes) ); my_hv_store( info, "vbri_quality", newSVuv(mp3->xing_frame->vbri_quality) ); } if (mp3->xing_frame->lame_tag) { my_hv_store( info, "lame_encoder_version", newSVpvn(mp3->xing_frame->lame_encoder_version, 9) ); my_hv_store( info, "lame_tag_revision", newSViv(mp3->xing_frame->lame_tag_revision) ); my_hv_store( info, "lame_vbr_method", newSVpv( vbr_methods[mp3->xing_frame->lame_vbr_method], 0 ) ); my_hv_store( info, "lame_lowpass", newSViv(mp3->xing_frame->lame_lowpass) ); if (mp3->xing_frame->lame_replay_gain[0]) { my_hv_store( info, "lame_replay_gain_radio", newSVpvf( "%.1f dB", mp3->xing_frame->lame_replay_gain[0] ) ); } if (mp3->xing_frame->lame_replay_gain[1]) { my_hv_store( info, "lame_replay_gain_audiophile", newSVpvf( "%.1f dB", mp3->xing_frame->lame_replay_gain[1] ) ); } my_hv_store( info, "lame_encoder_delay", newSViv(mp3->xing_frame->lame_encoder_delay) ); my_hv_store( info, "lame_encoder_padding", newSViv(mp3->xing_frame->lame_encoder_padding) ); my_hv_store( info, "lame_noise_shaping", newSViv(mp3->xing_frame->lame_noise_shaping) ); my_hv_store( info, "lame_stereo_mode", newSVpv( stereo_modes[mp3->xing_frame->lame_stereo_mode], 0 ) ); my_hv_store( info, "lame_unwise_settings", newSViv(mp3->xing_frame->lame_unwise) ); my_hv_store( info, "lame_source_freq", newSVpv( source_freqs[mp3->xing_frame->lame_source_freq], 0 ) ); // my_hv_store( info, "lame_mp3gain", newSViv(mp3->xing_frame->lame_mp3gain) ); // my_hv_store( info, "lame_mp3gain_db", newSVnv(mp3->xing_frame->lame_mp3gain_db) ); my_hv_store( info, "lame_surround", newSVpv( surround[mp3->xing_frame->lame_surround], 0 ) ); if (mp3->xing_frame->lame_preset < 8) { my_hv_store( info, "lame_preset", newSVpvn( "Unknown", 7 ) ); } else if (mp3->xing_frame->lame_preset <= 320) { my_hv_store( info, "lame_preset", newSVpvf( "ABR %d", mp3->xing_frame->lame_preset ) ); } else if (mp3->xing_frame->lame_preset <= 500) { mp3->xing_frame->lame_preset /= 10; mp3->xing_frame->lame_preset -= 41; if ( presets_v[mp3->xing_frame->lame_preset] ) { my_hv_store( info, "lame_preset", newSVpv( presets_v[mp3->xing_frame->lame_preset], 0 ) ); } } else if (mp3->xing_frame->lame_preset >= 1000 && mp3->xing_frame->lame_preset <= 1007) { mp3->xing_frame->lame_preset -= 1000; if ( presets_old[mp3->xing_frame->lame_preset] ) { my_hv_store( info, "lame_preset", newSVpv( presets_old[mp3->xing_frame->lame_preset], 0 ) ); } } } if (mp3->vbr == ABR || mp3->vbr == VBR) { my_hv_store( info, "vbr", newSViv(1) ); } // DLNA profile detection if (_is_mp3x_profile(mp3)) my_hv_store( info, "dlna_profile", newSVpvn( "MP3X", 4 ) ); else if (_is_mp3_profile(mp3)) my_hv_store( info, "dlna_profile", newSVpvn( "MP3", 3 ) ); out: return mp3; } int mp3_find_frame(PerlIO *infile, char *file, int offset) { Buffer mp3_buf; unsigned char *bptr; unsigned int buf_size; struct mp3frame frame; int frame_offset = -1; HV *info = newHV(); mp3info *mp3 = _mp3_parse(infile, file, info); buffer_init(&mp3_buf, MP3_BLOCK_SIZE); if (!mp3->song_length_ms) goto out; // (undocumented) If offset is negative, treat it as an absolute file offset in bytes // This is a bit ugly but avoids the need to write an entirely new method if (offset < 0) { frame_offset = abs(offset); if (frame_offset < mp3->audio_offset) { // Force offset to be at least audio_offset, so we don't end up in an ID3 tag frame_offset = mp3->audio_offset; } DEBUG_TRACE("find_frame: using absolute offset value %d\n", frame_offset); } else { if (offset >= mp3->song_length_ms) { goto out; } // Use Xing TOC if available if ( mp3->xing_frame->has_toc ) { float percent; uint8_t ipercent; uint16_t tva; uint16_t tvb; float tvx; percent = (offset * 1.0 / mp3->song_length_ms) * 100; ipercent = (int)percent; if (ipercent > 99) ipercent = 99; // Interpolate between 2 TOC points tva = mp3->xing_frame->xing_toc[ipercent]; if (ipercent < 99) { tvb = mp3->xing_frame->xing_toc[ipercent + 1]; } else { tvb = 256; } tvx = tva + (tvb - tva) * (percent - ipercent); frame_offset = (int)((1.0/256.0) * tvx * mp3->xing_frame->xing_bytes); frame_offset += mp3->audio_offset; // Don't return offset == audio_offset, because that would be the Xing frame if (frame_offset == mp3->audio_offset) { DEBUG_TRACE("find_frame: frame_offset == audio_offset, skipping to next frame\n"); frame_offset += 1; } DEBUG_TRACE("find_frame: using Xing TOC, song_length_ms: %d, percent: %f, tva: %d, tvb: %d, tvx: %f, frame offset: %d\n", mp3->song_length_ms, percent, tva, tvb, tvx, frame_offset ); } else { // calculate offset using bitrate float bytes_per_ms = mp3->bitrate / 8.0; frame_offset = (int)(bytes_per_ms * offset); frame_offset += mp3->audio_offset; DEBUG_TRACE("find_frame: using bitrate %d, bytes_per_ms: %f, frame offset: %d\n", mp3->bitrate, bytes_per_ms, frame_offset); } } // If frame_offset is too near the end of the file we won't find a valid frame // so require offset to be at least 1000 bytes from the end of the file // XXX this would be more accurate if we determined max_frame_len if ((mp3->file_size - frame_offset) < 1000) { frame_offset -= 1000 - (mp3->file_size - frame_offset); if (frame_offset < 0) frame_offset = 0; DEBUG_TRACE("find_frame: offset too close to end of file, adjusted to %d\n", frame_offset); } PerlIO_seek(infile, frame_offset, SEEK_SET); if ( !_check_buf(infile, &mp3_buf, 4, MP3_BLOCK_SIZE) ) { frame_offset = -1; goto out; } bptr = (unsigned char *)buffer_ptr(&mp3_buf); buf_size = buffer_len(&mp3_buf); // Find 0xFF sync and verify it's a valid mp3 frame header while (1) { if ( buf_size < 4 || ( bptr[0] == 0xFF && !_decode_mp3_frame( bptr, &frame ) ) ) { break; } bptr++; buf_size--; } if (buf_size >= 4) { frame_offset += buffer_len(&mp3_buf) - buf_size; DEBUG_TRACE("find_frame: frame_offset: %d\n", frame_offset); } else { // Didn't find a valid frame, probably too near the end of the file DEBUG_TRACE("find_frame: did not find a valid frame\n"); frame_offset = -1; } out: buffer_free(&mp3_buf); SvREFCNT_dec(info); buffer_free(mp3->buf); Safefree(mp3->buf); Safefree(mp3->first_frame); Safefree(mp3->xing_frame); Safefree(mp3); return frame_offset; } void _mp3_skip(mp3info *mp3, uint32_t size) { if ( buffer_len(mp3->buf) >= size ) { buffer_consume(mp3->buf, size); DEBUG_TRACE(" skipped buffer data size %d\n", size); } else { PerlIO_seek(mp3->infile, size - buffer_len(mp3->buf), SEEK_CUR); buffer_clear(mp3->buf); DEBUG_TRACE(" seeked past %d bytes to %d\n", size, (int)PerlIO_tell(mp3->infile)); } }