/*
* 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"
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;
}
}
static int
parse_id3(PerlIO *infile, char *file, HV *info, HV *tags, uint32_t seek)
{
struct id3_file *pid3file;
struct id3_tag *pid3tag;
struct id3_frame *pid3frame;
enum id3_file_mode mode = ID3_FILE_MODE_READONLY;
int err = 0;
int index;
unsigned int nstrings;
unsigned char trck_found = 0; // whether we found a track tag, used to determine ID3v1 from ID3v1.1
id3_ucs4_t const *key;
id3_ucs4_t const *value;
char *utf8_key;
char *utf8_value;
if (seek) {
mode = ID3_FILE_MODE_READONLY_NOSEEK;
}
pid3file = id3_file_fdopen( PerlIO_fileno(infile), mode, seek );
if (!pid3file) {
PerlIO_printf(PerlIO_stderr(), "libid3tag cannot open %s\n", file);
err = -1;
goto out;
}
pid3tag = id3_file_tag(pid3file);
if (!pid3tag) {
err = errno;
id3_file_close(pid3file);
errno = err;
PerlIO_printf(PerlIO_stderr(), "libid3tag cannot get ID3 tag for %s\n", file);
goto out;
}
DEBUG_TRACE("Found %d ID3 frames\n", pid3tag->nframes);
index = 0;
while ((pid3frame = id3_tag_findframe(pid3tag, "", index))) {
key = NULL;
value = NULL;
utf8_key = NULL;
utf8_value = NULL;
DEBUG_TRACE("%s (%d fields)\n", pid3frame->id, pid3frame->nfields);
// Special handling for TXXX/WXXX frames
if ( !strcmp(pid3frame->id, "TXXX") || !strcmp(pid3frame->id, "WXXX") ) {
DEBUG_TRACE(" type %d / %d\n", pid3frame->fields[1].type, pid3frame->fields[2].type);
key = id3_field_getstring(&pid3frame->fields[1]);
if (key) {
// Get the key
utf8_key = (char *)id3_ucs4_utf8duplicate(key);
if ( strlen(utf8_key) ) {
SV *ktmp = newSVpv( upcase(utf8_key), 0 );
sv_utf8_decode(ktmp);
// Get the value
switch (pid3frame->fields[2].type) {
case ID3_FIELD_TYPE_LATIN1:
my_hv_store_ent( tags, ktmp, newSVpv( (char *)id3_field_getlatin1(&pid3frame->fields[2]), 0 ) );
break;
case ID3_FIELD_TYPE_STRING:
value = id3_field_getstring(&pid3frame->fields[2]);
if (value) {
SV *tmp;
utf8_value = (char *)id3_ucs4_utf8duplicate(value);
tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
my_hv_store_ent( tags, ktmp, tmp );
free(utf8_value);
}
else {
my_hv_store_ent( tags, ktmp, &PL_sv_undef );
}
break;
default:
break;
}
// Don't leak
SvREFCNT_dec(ktmp);
}
free(utf8_key);
}
}
// Special handling for TCON genre frame, lookup the genre string
else if ( !strcmp(pid3frame->id, "TCON") ) {
char *genre_string;
value = id3_field_getstrings(&pid3frame->fields[1], 0);
if (value) {
utf8_value = (char *)id3_ucs4_utf8duplicate(value);
if ( isdigit(utf8_value[0]) ) {
// Convert to genre string
genre_string = (char *)id3_ucs4_utf8duplicate( id3_genre_name(value) );
my_hv_store( tags, pid3frame->id, newSVpv( genre_string, 0 ) );
free(genre_string);
}
else if ( utf8_value[0] == '(' && isdigit(utf8_value[1]) ) {
// handle '(26)Ambient'
int genre_num = (int)strtol( (char *)&utf8_value[1], NULL, 0 );
if (genre_num > 0 && genre_num < 148) {
genre_string = (char *)id3_ucs4_utf8duplicate( id3_genre_index(genre_num) );
my_hv_store( tags, pid3frame->id, newSVpv( genre_string, 0 ) );
free(genre_string);
}
}
else {
SV *tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
my_hv_store( tags, pid3frame->id, tmp );
}
free(utf8_value);
}
}
// All other frames
else {
DEBUG_TRACE(" type %d\n", pid3frame->fields[0].type);
// For some reason libid3tag marks some frames as obsolete, when
// they should at least be passed-through as unknown frames
if ( !strcmp(pid3frame->id, "ZOBS") ) {
char *frameid = pid3frame->fields[0].immediate.value;
DEBUG_TRACE(" ZOBS frame %s\n", frameid);
// Special case, TYE(R), TDA(T), TIM(E) are already converted to TDRC
if (
!strcmp(frameid, "TYER")
|| !strcmp(frameid, "YTYE")
|| !strcmp(frameid, "TDAT")
|| !strcmp(frameid, "YTDA")
|| !strcmp(frameid, "TIME")
|| !strcmp(frameid, "YTIM")
) {
index++;
continue;
}
// Convert this frame into the real frame with 1 field of binary data
pid3frame->id[0] = frameid[0];
pid3frame->id[1] = frameid[1];
pid3frame->id[2] = frameid[2];
pid3frame->id[3] = frameid[3];
pid3frame->nfields = 1;
pid3frame->fields[0] = pid3frame->fields[1];
}
// 1- and 2-field frames where the first field is TEXTENCODING are mapped to plain hash entries
// This covers the following frames:
// MCDI - ID3_FIELD_TYPE_BINARYDATA (untested)
// PCNT - ID3_FIELD_TYPE_INT32PLUS
// SEEK - ID3_FIELD_TYPE_INT32 (untested)
// T* (text) - ID3_FIELD_TYPE_TEXTENCODING, ID3_FIELD_TYPE_STRINGLIST
// W* (url) - ID3_FIELD_TYPE_LATIN1
// unknown - ID3_FIELD_TYPE_BINARYDATA
if (
pid3frame->nfields == 1
|| ( pid3frame->nfields == 2 && (pid3frame->fields[0].type == ID3_FIELD_TYPE_TEXTENCODING) )
) {
int i = pid3frame->nfields - 1;
switch (pid3frame->fields[i].type) {
case ID3_FIELD_TYPE_LATIN1:
my_hv_store( tags, pid3frame->id, newSVpv( (char *)id3_field_getlatin1(&pid3frame->fields[i]), 0 ) );
break;
case ID3_FIELD_TYPE_STRINGLIST:
nstrings = id3_field_getnstrings(&pid3frame->fields[i]);
if (nstrings > 1) {
// Store multiple strings as arrayref
AV *atmp = newAV();
int j;
for ( j = 0; j < nstrings; j++ ) {
value = id3_field_getstrings(&pid3frame->fields[i], j);
if (value) {
SV *tmp;
utf8_value = (char *)id3_ucs4_utf8duplicate(value);
tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
av_push( atmp, tmp );
free(utf8_value);
}
else {
av_push( atmp, &PL_sv_undef );
}
}
my_hv_store( tags, pid3frame->id, newRV_noinc( (SV *)atmp ) );
}
else {
// Remember if TRCK tag is found for ID3v1.1
if ( !strcmp(pid3frame->id, "TRCK") ) {
trck_found = 1;
}
value = id3_field_getstrings(&pid3frame->fields[i], 0);
if (value) {
SV *tmp;
utf8_value = (char *)id3_ucs4_utf8duplicate(value);
tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
my_hv_store( tags, pid3frame->id, tmp );
free(utf8_value);
}
else {
my_hv_store( tags, pid3frame->id, &PL_sv_undef );
}
}
break;
case ID3_FIELD_TYPE_INT32:
my_hv_store( tags, pid3frame->id, newSViv( pid3frame->fields[i].number.value ) );
break;
case ID3_FIELD_TYPE_INT32PLUS:
my_hv_store(
tags,
pid3frame->id,
newSViv( _varint( pid3frame->fields[i].binary.data, pid3frame->fields[i].binary.length ) )
);
break;
case ID3_FIELD_TYPE_BINARYDATA:
if ( !strcmp(pid3frame->id, "XHD3" ) ) {
// Ignore XHD3 frame from stupid new mp3HD format
}
// Special handling for RVA(D)
else if ( !strcmp( pid3frame->id, "RVAD" ) || !strcmp( pid3frame->id, "YRVA" ) ) {
unsigned char *rva = (unsigned char*)pid3frame->fields[0].binary.data;
int8_t sign_r = rva[0] & 0x01 ? 1 : -1;
int8_t sign_l = rva[0] & 0x02 ? 1 : -1;
uint8_t bytes = rva[1] / 8;
float vol[2];
float peak[2];
uint8_t i;
AV *framedata = newAV();
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( tags, pid3frame->id, newRV_noinc( (SV *)framedata ) );
}
else {
char *data = (char*)pid3frame->fields[0].binary.data;
unsigned int len = pid3frame->fields[0].binary.length;
SV *bin;
// Consume leading and trailing padding nulls on binary data, these are left over
// from unknown text frames from i.e. iTunes
while ( len && !data[0] ) {
data++;
len--;
}
while ( len && !data[len - 1] ) {
len--;
}
bin = newSVpvn( data, len );
my_hv_store( tags, pid3frame->id, bin );
}
default:
break;
}
}
// 2+ field frames are mapped to arrayrefs
// This covers the following frames:
// UFID, ETCO, MLLT, SYTC, USLT, SYLT, COMM, RVA2, EQU2, RVRB,
// APIC, GEOB, POPM, AENC, LINK, POSS, USER, OWNE, COMR, ENCR,
// GRID, PRIV, SIGN, ASPI
else {
int i;
SV *tmp;
AV *framedata = newAV();
for ( i = 0; i < pid3frame->nfields; i++ ) {
DEBUG_TRACE(" frame %d, type %d\n", i, pid3frame->fields[i].type);
switch (pid3frame->fields[i].type) {
case ID3_FIELD_TYPE_LATIN1:
av_push( framedata, newSVpv( (char *)id3_field_getlatin1(&pid3frame->fields[i]), 0 ) );
break;
// ID3_FIELD_TYPE_LATIN1FULL - not used
case ID3_FIELD_TYPE_LATIN1LIST: // XXX untested, LINK frame
nstrings = id3_field_getnstrings(&pid3frame->fields[i]);
if (nstrings > 1) {
// XXX, turn into an arrayref
PerlIO_printf(PerlIO_stderr(), "LATIN1LIST, %d strings\n", nstrings );
}
else {
av_push( framedata, newSVpv( (char*)pid3frame->fields[i].latin1list.strings[0], 0 ) );
}
break;
case ID3_FIELD_TYPE_STRING:
utf8_value = (char *)id3_ucs4_utf8duplicate( id3_field_getstring(&pid3frame->fields[i]) );
tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
av_push( framedata, tmp );
free(utf8_value);
break;
case ID3_FIELD_TYPE_STRINGFULL:
utf8_value = (char *)id3_ucs4_utf8duplicate( id3_field_getfullstring(&pid3frame->fields[i]) );
tmp = newSVpv( utf8_value, 0 );
sv_utf8_decode(tmp);
av_push( framedata, tmp );
free(utf8_value);
break;
// ID3_FIELD_TYPE_STRINGLIST - only used for text frames, handled above
case ID3_FIELD_TYPE_LANGUAGE:
case ID3_FIELD_TYPE_FRAMEID: // XXX untested, LINK frame
case ID3_FIELD_TYPE_DATE: // XXX untested, OWNE/COMR
av_push( framedata, newSVpv( pid3frame->fields[i].immediate.value, 0 ) );
break;
case ID3_FIELD_TYPE_INT8:
case ID3_FIELD_TYPE_INT16:
case ID3_FIELD_TYPE_INT24:
case ID3_FIELD_TYPE_INT32:
case ID3_FIELD_TYPE_TEXTENCODING:
av_push( framedata, newSViv( pid3frame->fields[i].number.value ) );
break;
case ID3_FIELD_TYPE_INT32PLUS:
av_push( framedata, newSViv( _varint( pid3frame->fields[i].binary.data, pid3frame->fields[i].binary.length ) ) );
break;
case ID3_FIELD_TYPE_BINARYDATA:
// Special handling for RVA2 tags, expand to correct fields
if ( !strcmp( pid3frame->id, "RVA2" ) ) {
unsigned char *rva = (unsigned char*)pid3frame->fields[i].binary.data;
float adj = 0.0;
int adj_fp;
// Channel
av_push( framedata, newSViv(rva[0]) );
rva++;
// Adjustment
adj_fp = *(signed char *)(rva) << 8;
adj_fp |= *(unsigned char *)(rva+1);
adj = adj_fp / 512.0;
av_push( framedata, newSVpvf( "%f dB", adj ) );
rva += 2;
// Ignore peak, nobody seems to support this
av_push( framedata, newSViv(0) );
}
else {
SV *bin = newSVpvn( (char*)pid3frame->fields[i].binary.data, pid3frame->fields[i].binary.length );
av_push( framedata, bin );
}
default:
break;
}
}
// If tag already exists, move it into an arrayref
if ( my_hv_exists( tags, pid3frame->id ) ) {
SV **entry = my_hv_fetch( tags, pid3frame->id );
if (entry != NULL) {
if ( SvTYPE( SvRV(*entry) ) == SVt_PV ) {
// A normal string entry, convert to array
// XXX untested
AV *ref = newAV();
av_push( ref, *entry );
av_push( ref, newRV_noinc( (SV *)framedata ) );
my_hv_store( tags, pid3frame->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 || ( SvTYPE(*first) == SVt_RV && 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( tags, pid3frame->id, newRV_noinc( (SV *)ref ) );
}
}
}
}
else {
my_hv_store( tags, pid3frame->id, newRV_noinc( (SV *)framedata ) );
}
}
}
index++;
}
// Update id3_version field if we found a v1 tag
if ( pid3tag->options & ID3_TAG_OPTION_ID3V1 && !my_hv_fetch( info, "id3_version" ) ) {
if (trck_found == 1) {
my_hv_store( info, "id3_version", newSVpv( "ID3v1.1", 0 ) );
}
else {
my_hv_store( info, "id3_version", newSVpv( "ID3v1", 0 ) );
}
}
id3_file_close(pid3file);
out:
return err;
}