More flexible MP4 file metadata parser.

git-svn-id: svn://svn.rockbox.org/rockbox/trunk@11189 a1c6a512-1295-4272-9138-f99709370657
This commit is contained in:
Magnus Holmgren 2006-10-11 17:42:33 +00:00
parent d63d8fedaa
commit 406069467d

View file

@ -22,6 +22,7 @@
#include <ctype.h>
#include <inttypes.h>
#include "errno.h"
#include "metadata.h"
#include "mp3_playback.h"
#include "logf.h"
@ -40,6 +41,37 @@ enum tagtype { TAGTYPE_APE = 1, TAGTYPE_VORBIS };
#define TAG_NAME_LENGTH 32
#define TAG_VALUE_LENGTH 128
#define MP4_ID(a, b, c, d) (((a) << 24) | ((b) << 16) | ((c) << 8) | (d))
#define MP4_3gp6 MP4_ID('3', 'g', 'p', '6')
#define MP4_alac MP4_ID('a', 'l', 'a', 'c')
#define MP4_calb MP4_ID(0xa9, 'a', 'l', 'b')
#define MP4_cART MP4_ID(0xa9, 'A', 'R', 'T')
#define MP4_cnam MP4_ID(0xa9, 'n', 'a', 'm')
#define MP4_cwrt MP4_ID(0xa9, 'w', 'r', 't')
#define MP4_ftyp MP4_ID('f', 't', 'y', 'p')
#define MP4_gnre MP4_ID('g', 'n', 'r', 'e')
#define MP4_hdlr MP4_ID('h', 'd', 'l', 'r')
#define MP4_ilst MP4_ID('i', 'l', 's', 't')
#define MP4_M4A MP4_ID('M', '4', 'A', ' ')
#define MP4_mdat MP4_ID('m', 'd', 'a', 't')
#define MP4_mdia MP4_ID('m', 'd', 'i', 'a')
#define MP4_mdir MP4_ID('m', 'd', 'i', 'r')
#define MP4_meta MP4_ID('m', 'e', 't', 'a')
#define MP4_minf MP4_ID('m', 'i', 'n', 'f')
#define MP4_moov MP4_ID('m', 'o', 'o', 'v')
#define MP4_mp4a MP4_ID('m', 'p', '4', 'a')
#define MP4_mp42 MP4_ID('m', 'p', '4', '2')
#define MP4_qt MP4_ID('q', 't', ' ', ' ')
#define MP4_soun MP4_ID('s', 'o', 'u', 'n')
#define MP4_stbl MP4_ID('s', 't', 'b', 'l')
#define MP4_stsd MP4_ID('s', 't', 's', 'd')
#define MP4_stts MP4_ID('s', 't', 't', 's')
#define MP4_trak MP4_ID('t', 'r', 'a', 'k')
#define MP4_trkn MP4_ID('t', 'r', 'k', 'n')
#define MP4_udta MP4_ID('u', 'd', 't', 'a')
#define MP4_extra MP4_ID('-', '-', '-', '-')
struct apetag_header
{
char id[8];
@ -190,28 +222,31 @@ static void convert_endian(void *data, const char *format)
}
}
/* read_uint32be() - read an unsigned integer from a big-endian
(e.g. Quicktime) file. This is used by the .m4a parser
*/
/* Read an unsigned 16-bit integer from a big-endian file. */
#ifdef ROCKBOX_BIG_ENDIAN
#define read_uint32be(fd,buf) read((fd),(buf),4)
#define read_uint16be(fd, buf) read((fd), (buf), 2)
#else
int read_uint32be(int fd, unsigned int* buf) {
char tmp;
char* p=(char*)buf;
int read_uint16be(int fd, unsigned short* buf)
{
size_t n;
n = read(fd, (char*) buf, 2);
*buf = betoh16(*buf);
return n;
}
#endif
/* Read an unsigned 32-bit integer from a big-endian file. */
#ifdef ROCKBOX_BIG_ENDIAN
#define read_uint32be(fd,buf) read((fd), (buf), 4)
#else
int read_uint32be(int fd, unsigned int* buf)
{
size_t n;
n=read(fd,p,4);
if (n==4) {
tmp=p[0];
p[0]=p[3];
p[3]=tmp;
tmp=p[2];
p[2]=p[1];
p[1]=tmp;
}
return(n);
n = read(fd, (char*) buf, 4);
*buf = betoh32(*buf);
return n;
}
#endif
@ -921,292 +956,388 @@ static bool get_wave_metadata(int fd, struct mp3entry* id3)
return true;
}
static bool get_m4a_metadata(int fd, struct mp3entry* id3)
/* Read the tag data from an MP4 file, storing up to buffer_size bytes in
* buffer.
*/
unsigned long read_mp4_tag(int fd, unsigned int size_left, char* buffer,
unsigned int buffer_left)
{
unsigned char* buf;
unsigned long totalsamples;
int i,j,k;
size_t n;
size_t bytes_remaining;
char* id3buf;
unsigned int compressedsize;
unsigned int sample_count;
unsigned int sample_duration;
int numentries;
int entry_size;
int size_remaining;
int chunk_len;
unsigned char chunk_id[4];
int sub_chunk_len;
unsigned char sub_chunk_id[4];
unsigned int bytes_read = 0;
if (buffer_left == 0)
{
lseek(fd, size_left, SEEK_CUR); /* Skip everything */
}
else
{
/* Skip the data tag header - maybe we should parse it properly? */
lseek(fd, 16, SEEK_CUR);
size_left -= 16;
/* A simple parser to read vital metadata from an ALAC file.
This parser also works for AAC files - they are both stored in
a Quicktime M4A container. */
if (size_left > buffer_left)
{
read(fd, buffer, buffer_left);
lseek(fd, size_left - buffer_left, SEEK_CUR);
bytes_read = buffer_left;
}
else
{
read(fd, buffer, size_left);
bytes_read = size_left;
}
}
return bytes_read;
}
/* Use the trackname part of the id3 structure as a temporary buffer */
buf=id3->path;
/* Read a string tag from an MP4 file */
unsigned int read_mp4_tag_string(int fd, int size_left, char** buffer,
unsigned int* buffer_left, char** dest)
{
unsigned int bytes_read = read_mp4_tag(fd, size_left, *buffer,
*buffer_left - 1);
unsigned int length = 0;
lseek(fd, 0, SEEK_SET);
if (bytes_read)
{
(*buffer)[bytes_read] = 0;
*dest = *buffer;
length = strlen(*buffer) + 1;
*buffer_left -= length;
*buffer += length;
}
else
{
*dest = NULL;
}
return length;
}
totalsamples=0;
compressedsize=0;
/* read the chunks - we stop when we find the mdat chunk and set compressedsize */
while (compressedsize==0) {
n=read_uint32be(fd,&chunk_len);
static unsigned int read_mp4_atom(int fd, unsigned int* size,
unsigned int* type, unsigned int size_left)
{
read_uint32be(fd, size);
read_uint32be(fd, type);
// This means it was a 64-bit file, so we have problems.
if (chunk_len == 1) {
logf("need 64bit support\n");
return false;
if (*size == 1)
{
/* FAT32 doesn't support files this big, so something seems to
* be wrong. (64-bit sizes should only be used when required.)
*/
errno = EFBIG;
*type = 0;
return 0;
}
n=read(fd,&chunk_id,4);
if (n < 4)
return false;
if (memcmp(&chunk_id,"ftyp",4)==0) {
/* Check for M4A type */
n=read(fd,&chunk_id,4);
if ((memcmp(&chunk_id,"M4A ",4)!=0) &&
(memcmp(&chunk_id,"mp42",4)!=0)) {
logf("Not an M4A file, aborting\n");
return false;
}
/* Skip rest of chunk */
lseek(fd, chunk_len - 8 - 4, SEEK_CUR); /* FIXME not 8 */
} else if (memcmp(&chunk_id,"moov",4)==0) {
size_remaining=chunk_len - 8; /* FIXME not 8 */
while (size_remaining > 0) {
n=read_uint32be(fd,&sub_chunk_len);
if ((sub_chunk_len < 1) || (sub_chunk_len > size_remaining)) {
logf("Strange sub_chunk_len value inside moov: %d (remaining: %d)\n",sub_chunk_len,size_remaining);
return false;
if (*size > 0)
{
if (*size > size_left)
{
size_left = 0;
}
n=read(fd,&sub_chunk_id,4);
size_remaining-=8;
else
{
size_left -= *size;
}
*size -= 8;
}
else
{
*size = size_left;
size_left = 0;
}
return size_left;
}
if (memcmp(&sub_chunk_id,"mvhd",4)==0) {
/* We don't need anything from here - skip */
lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
size_remaining-=(sub_chunk_len-8);
} else if (memcmp(&sub_chunk_id,"udta",4)==0) {
/* The udta chunk contains the metadata - track, artist, album etc.
The format appears to be:
udta
meta
hdlr
ilst
.nam
[rest of tags]
free
static bool read_mp4_tags(int fd, struct mp3entry* id3,
unsigned int size_left)
{
unsigned int size;
unsigned int type;
unsigned int buffer_left = sizeof(id3->id3v2buf) + sizeof(id3->id3v1buf);
char* buffer = id3->id3v2buf;
bool cwrt = false;
NOTE: This code was written by examination of some .m4a files
produced by iTunes v4.9 - it may not therefore be 100%
compliant with all streams. But it should fail gracefully.
*/
j=(sub_chunk_len-8);
size_remaining-=j;
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
j-=8;
if (memcmp(&sub_chunk_id,"meta",4)==0) {
lseek(fd, 4, SEEK_CUR);
j-=4;
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
j-=8;
if (memcmp(&sub_chunk_id,"hdlr",4)==0) {
lseek(fd, sub_chunk_len - 8, SEEK_CUR);
j-=(sub_chunk_len - 8);
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
j-=8;
if (memcmp(&sub_chunk_id,"ilst",4)==0) {
/* Here are the actual tags. We use the id3v2 300-byte buffer
to store the string data */
bytes_remaining=sizeof(id3->id3v2buf);
id3->genre=255; /* Not every track is the Blues */
id3buf=id3->id3v2buf;
k=sub_chunk_len-8;
j-=k;
while (k > 0) {
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
k-=8;
if (memcmp(sub_chunk_id,"\251nam",4)==0) {
read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->title);
} else if (memcmp(sub_chunk_id,"\251ART",4)==0) {
read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->artist);
} else if (memcmp(sub_chunk_id,"\251alb",4)==0) {
read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->album);
} else if (memcmp(sub_chunk_id,"\251gen",4)==0) {
read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->genre_string);
} else if (memcmp(sub_chunk_id,"\251day",4)==0) {
read_m4a_tag_string(fd,sub_chunk_len-8,&id3buf,&bytes_remaining,&id3->year_string);
} else if (memcmp(sub_chunk_id,"trkn",4)==0) {
if (sub_chunk_len==0x20) {
read(fd,buf,sub_chunk_len-8);
id3->tracknum=buf[19];
} else {
lseek(fd, sub_chunk_len-8,SEEK_CUR);
}
} else {
lseek(fd, sub_chunk_len-8,SEEK_CUR);
}
k-=(sub_chunk_len-8);
}
}
do
{
size_left = read_mp4_atom(fd, &size, &type, size_left);
/* DEBUGF("Tag atom: '%c%c%c%c' (%d bytes left)\n", type >> 24 & 0xff,
type >> 16 & 0xff, type >> 8 & 0xff, type & 0xff, size); */
switch (type)
{
case MP4_cnam:
read_mp4_tag_string(fd, size, &buffer, &buffer_left,
&id3->title);
break;
case MP4_cART:
read_mp4_tag_string(fd, size, &buffer, &buffer_left,
&id3->artist);
break;
case MP4_calb:
read_mp4_tag_string(fd, size, &buffer, &buffer_left,
&id3->album);
break;
case MP4_cwrt:
read_mp4_tag_string(fd, size, &buffer, &buffer_left,
&id3->composer);
cwrt = false;
break;
case MP4_gnre:
{
unsigned short genre;
read_mp4_tag(fd, size, (char*) &genre, sizeof(genre));
id3->genre = betoh16(genre);
}
}
/* Skip any remaining data in udta chunk */
lseek(fd, j, SEEK_CUR);
} else if (memcmp(&sub_chunk_id,"trak",4)==0) {
/* Format of trak chunk:
tkhd
mdia
mdhd
hdlr
minf
smhd
dinf
stbl
stsd - Samplerate, Samplesize, Numchannels
stts - time_to_sample array - RLE'd table containing duration of each block
stsz - sample_byte_size array - ?Size in bytes of each compressed block
stsc - Seek table related?
stco - Seek table related?
*/
break;
/* Skip tkhd - not needed */
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
if (memcmp(&sub_chunk_id,"tkhd",4)!=0) {
logf("Expecting tkhd\n");
return false;
}
lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
size_remaining-=sub_chunk_len;
case MP4_trkn:
{
unsigned short n[2];
read_mp4_tag(fd, size, (char*) &n, sizeof(n));
id3->tracknum = betoh16(n[1]);
}
break;
/* Process mdia - skipping possible edts */
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
if (memcmp(&sub_chunk_id,"edts",4)==0) {
lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
size_remaining-=sub_chunk_len;
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
}
case MP4_extra:
{
char tag_name[TAG_NAME_LENGTH];
unsigned int sub_size;
/* "mean" atom */
read_uint32be(fd, &sub_size);
size -= sub_size;
lseek(fd, sub_size - 4, SEEK_CUR);
/* "name" atom */
read_uint32be(fd, &sub_size);
size -= sub_size;
lseek(fd, 8, SEEK_CUR);
sub_size -= 12;
if (sub_size > sizeof(tag_name) - 1)
{
read(fd, tag_name, sizeof(tag_name) - 1);
lseek(fd, sub_size - sizeof(tag_name) - 1, SEEK_CUR);
tag_name[sizeof(tag_name) - 1] = 0;
}
else
{
read(fd, tag_name, sub_size);
tag_name[sub_size] = 0;
}
if ((strcasecmp(tag_name, "composer") == 0) && !cwrt)
{
read_mp4_tag_string(fd, size, &buffer, &buffer_left,
&id3->composer);
}
else
{
char* any;
unsigned int length = read_mp4_tag_string(fd, size,
&buffer, &buffer_left, &any);
if (length > 0)
{
/* Re-use the read buffer as the dest buffer... */
buffer -= length;
buffer_left += length;
if (memcmp(&sub_chunk_id,"mdia",4)!=0) {
logf("Expecting mdia\n");
return false;
}
size_remaining-=sub_chunk_len;
j=sub_chunk_len-8;
while (j > 0) {
n=read_uint32be(fd,&sub_chunk_len);
n=read(fd,&sub_chunk_id,4);
j-=4;
if (memcmp(&sub_chunk_id,"minf",4)==0) {
j=sub_chunk_len-8;
} else if (memcmp(&sub_chunk_id,"stbl",4)==0) {
j=sub_chunk_len-8;
} else if (memcmp(&sub_chunk_id,"stsd",4)==0) {
n=read(fd,buf,sub_chunk_len-8);
j-=sub_chunk_len;
i=0;
/* Skip version and flags */
i+=4;
numentries=(buf[i]<<24)|(buf[i+1]<<16)|(buf[i+2]<<8)|buf[i+3];
i+=4;
if (numentries!=1) {
logf("ERROR: Expecting only one entry in stsd\n");
}
entry_size=(buf[i]<<24)|(buf[i+1]<<16)|(buf[i+2]<<8)|buf[i+3];
i+=4;
if (memcmp(&buf[i],"alac",4)==0) {
id3->codectype=AFMT_ALAC;
} else if (memcmp(&buf[i],"mp4a",4)==0) {
id3->codectype=AFMT_AAC;
} else {
logf("Not an ALAC or AAC file\n");
return false;
}
//numchannels=(buf[i+20]<<8)|buf[i+21]; /* Not used - assume Stereo */
//samplesize=(buf[i+22]<<8)|buf[i+23]; /* Not used - assume 16-bit */
/* Samplerate is 32-bit fixed point, but this works for < 65536 Hz */
id3->frequency=(buf[i+28]<<8)|buf[i+29];
} else if (memcmp(&sub_chunk_id,"stts",4)==0) {
j-=sub_chunk_len;
i=8;
n=read(fd,buf,8);
i+=8;
numentries=(buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7];
for (k=0;k<numentries;k++) {
n=read_uint32be(fd,&sample_count);
n=read_uint32be(fd,&sample_duration);
totalsamples+=sample_count*sample_duration;
i+=8;
}
if (i > 0) lseek(fd, sub_chunk_len - i, SEEK_CUR);
} else if (memcmp(&sub_chunk_id,"stsz",4)==0) {
j-=sub_chunk_len;
i=8;
n=read(fd,buf,8);
i+=8;
numentries=(buf[4]<<24)|(buf[5]<<16)|(buf[6]<<8)|buf[7];
for (k=0;k<numentries;k++) {
n=read_uint32be(fd,&sample_count);
n=read_uint32be(fd,&sample_duration);
totalsamples+=sample_count*sample_duration;
i+=8;
}
if (i > 0) lseek(fd, sub_chunk_len - i, SEEK_CUR);
} else {
lseek(fd, sub_chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
j-=sub_chunk_len;
}
}
} else {
logf("Unexpected sub_chunk_id inside moov: %c%c%c%c\n",
sub_chunk_id[0],sub_chunk_id[1],sub_chunk_id[2],sub_chunk_id[3]);
return false;
parse_replaygain(tag_name, buffer, id3, buffer,
buffer_left);
}
}
}
break;
default:
lseek(fd, size, SEEK_CUR);
break;
}
}
} else if (memcmp(&chunk_id,"mdat",4)==0) {
/* once we hit mdat we stop reading and return.
* this is on the assumption that there is no furhter interesting
* stuff in the stream. if there is stuff will fail (:()).
* But we need the read pointer to be at the mdat stuff
* for the decoder. And we don't want to rely on fseek/ftell,
* as they may not always be avilable */
lseek(fd, chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
compressedsize=chunk_len-8;
} else if (memcmp(&chunk_id,"free",4)==0) {
/* these following atoms can be skipped !!!! */
lseek(fd, chunk_len - 8, SEEK_CUR); /* FIXME not 8 */
} else {
logf("(top) unknown chunk id: %c%c%c%c\n", chunk_id[0],chunk_id[1],chunk_id[2],chunk_id[3]);
return false;
}
}
while ((size_left > 0) && (errno == 0));
id3->vbr=true; /* All ALAC files are VBR */
id3->filesize=filesize(fd);
id3->samples=totalsamples;
id3->length=(10*totalsamples)/(id3->frequency/100);
id3->bitrate=(compressedsize*8)/id3->length;;
return true;
}
return true;
}
static bool read_mp4_container(int fd, struct mp3entry* id3,
unsigned int size_left)
{
unsigned int size;
unsigned int type;
unsigned int handler = 0;
bool rc = true;
do
{
size_left = read_mp4_atom(fd, &size, &type, size_left);
/* DEBUGF("Atom: '%c%c%c%c' (0x%08x, %d bytes left)\n",
(type >> 24) & 0xff, (type >> 16) & 0xff, (type >> 8) & 0xff,
type & 0xff, type, size); */
switch (type)
{
case MP4_ftyp:
{
unsigned int id;
read_uint32be(fd, &id);
size -= 4;
if ((id != MP4_M4A) && (id != MP4_mp42) && (id != MP4_qt)
&& (id != MP4_3gp6))
{
DEBUGF("Unknown MP4 file type: '%c%c%c%c'\n",
id >> 24 & 0xff, id >> 16 & 0xff, id >> 8 & 0xff,
id & 0xff);
return false;
}
}
break;
case MP4_meta:
lseek(fd, 4, SEEK_CUR); /* Skip version */
size -= 4;
/* Fall through */
case MP4_moov:
case MP4_udta:
case MP4_mdia:
case MP4_stbl:
case MP4_trak:
rc = read_mp4_container(fd, id3, size);
size = 0;
break;
case MP4_ilst:
if (handler == MP4_mdir)
{
rc = read_mp4_tags(fd, id3, size);
size = 0;
}
break;
case MP4_minf:
if (handler == MP4_soun)
{
rc = read_mp4_container(fd, id3, size);
size = 0;
}
break;
case MP4_stsd:
lseek(fd, 8, SEEK_CUR);
size -= 8;
rc = read_mp4_container(fd, id3, size);
size = 0;
break;
case MP4_hdlr:
lseek(fd, 8, SEEK_CUR);
read_uint32be(fd, &handler);
size -= 12;
/* DEBUGF(" Handler '%c%c%c%c'\n", handler >> 24 & 0xff,
handler >> 16 & 0xff, handler >> 8 & 0xff,handler & 0xff); */
break;
case MP4_stts:
{
unsigned int entries;
unsigned int i;
lseek(fd, 4, SEEK_CUR);
read_uint32be(fd, &entries);
id3->samples = 0;
for (i = 0; i < entries; i++)
{
unsigned int n;
unsigned int l;
read_uint32be(fd, &n);
read_uint32be(fd, &l);
id3->samples += n * l;
}
size = 0;
}
break;
case MP4_mp4a:
case MP4_alac:
{
unsigned int frequency;
id3->codectype = (type == MP4_mp4a) ? AFMT_AAC : AFMT_ALAC;
lseek(fd, 22, SEEK_CUR);
read_uint32be(fd, &frequency);
size -= 26;
id3->frequency = frequency;
/* There're some child atoms here, but we don't need them */
}
break;
case MP4_mdat:
id3->filesize = size;
break;
default:
break;
}
lseek(fd, size, SEEK_CUR);
}
while (rc && (size_left > 0) && (errno == 0) && (id3->filesize == 0));
/* Break on non-zero filesize, since Rockbox currently doesn't support
* metadata after the mdat atom (which sets the filesize field).
*/
return rc;
}
static bool get_mp4_metadata(int fd, struct mp3entry* id3)
{
id3->codectype = AFMT_UNKNOWN;
id3->genre = 255;
id3->filesize = 0;
errno = 0;
if (read_mp4_container(fd, id3, filesize(fd)) && (errno == 0)
&& (id3->samples > 0) && (id3->frequency > 0)
&& (id3->filesize > 0))
{
if (id3->codectype == AFMT_UNKNOWN)
{
logf("Not an ALAC or AAC file");
return false;
}
id3->length = (id3->samples / id3->frequency) * 1000;
id3->bitrate = ((int64_t) id3->filesize * 8) / id3->length;
DEBUGF("MP4 bitrate %d, frequency %d Hz, length %d ms\n",
id3->bitrate, id3->frequency, id3->length);
}
else
{
logf("MP4 metadata error");
DEBUGF("MP4 metadata error. errno %d, length %d, frequency %d, filesize %d\n",
errno, id3->length, id3->frequency, id3->filesize);
return false;
}
return true;
}
static bool get_musepack_metadata(int fd, struct mp3entry *id3)
{
@ -1718,7 +1849,7 @@ bool get_metadata(struct track_info* track, int fd, const char* trackname,
case AFMT_ALAC:
case AFMT_AAC:
if (!get_m4a_metadata(fd, &(track->id3)))
if (!get_mp4_metadata(fd, &(track->id3)))
{
return false;
}