From a3639860761dcdb5ef9c31bb34497f32cadd9ff3 Mon Sep 17 00:00:00 2001 From: ayumi Date: Thu, 30 Jan 2025 10:08:37 +0100 Subject: [PATCH] Add support for APEv2 tags and detecting WavPack files --- lib/libtags/CMakeLists.txt | 2 +- lib/libtags/ape.c | 233 +++++++++++++++++++++++++++++++++++++ lib/libtags/tags.c | 2 + lib/libtags/tags.h | 1 + 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 lib/libtags/ape.c diff --git a/lib/libtags/CMakeLists.txt b/lib/libtags/CMakeLists.txt index d8dce988..db5cd0c2 100644 --- a/lib/libtags/CMakeLists.txt +++ b/lib/libtags/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS 437.c 8859.c flac.c id3genres.c id3v1.c id3v2.c it.c m4a.c mod.c opus.c + SRCS 437.c 8859.c ape.c flac.c id3genres.c id3v1.c id3v2.c it.c m4a.c mod.c opus.c s3m.c tags.c utf16.c vorbis.c wav.c xm.c INCLUDE_DIRS . ) diff --git a/lib/libtags/ape.c b/lib/libtags/ape.c new file mode 100644 index 00000000..7ba30649 --- /dev/null +++ b/lib/libtags/ape.c @@ -0,0 +1,233 @@ +#include +#include "tagspriv.h" + +#define leu16int(d) (u16int)(((uchar*)(d))[1]<<8 | ((uchar*)(d))[0]<<0) + +enum +{ + HeaderSize = 32, + FooterSize = HeaderSize, + + MagicOffset = 0, + VersionOffset = 8, + SizeOffset = 12, + CountOffset = 16, + FlagsOffset = 20, + + WvHeaderSize = 32, + WvMagicOffset = 0, + WvVersionOffset = 8, + WvSamplesHighOffset = 11, + WvSamplesLowOffset = 12, + WvFlagsOffset = 24, + WvSampleRateMask = 0xf << 23, + WvSampleRateShift = 23, + WvCustomSampleRate = 16, + WvMonoMask = 4, +}; + +typedef enum +{ + TagUTF8, + TagBinary, + TagExternal, + TagReserved, + + TagInvalid, +} TagType; + +static int +isWavpack(Tagctx *ctx) +{ + uchar header[WvHeaderSize]; + int size; + u16int version; + u32int flags, samplerate; + uvlong samples; + + if(ctx->seek(ctx, 0, 0) < 0) + return 0; + if(ctx->read(ctx, header, WvHeaderSize) != WvHeaderSize) + return 0; + if(memcmp(header+WvMagicOffset, "wvpk", 4)) + return 0; + version = leu16int(header+WvVersionOffset); + if(version<0x402 || version>0x410) + return 0; + samples = (uvlong)(*(uchar*)(header+WvSamplesHighOffset))<<32 | leuint(header+WvSamplesLowOffset); + flags = leuint(header+WvFlagsOffset); + if((flags&WvSampleRateMask)>>WvSampleRateShift != WvCustomSampleRate){ + const u32int samplerates[] = {6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, 192000}; + samplerate = samplerates[(flags&WvSampleRateMask)>>WvSampleRateShift]; + ctx->samplerate = samplerate; + uvlong duration = round((double)samples/samplerate*1000); + ctx->duration = duration; + } + ctx->channels = flags&WvMonoMask ? 1 : 2; + if(ctx->seek(ctx, 0, 0) < 0) + return 0; + if(size = ctx->seek(ctx, 0, 2), size < 0) + return 0; + ctx->bitrate = (double)size*8.0/(samples/samplerate)/1000; + return 1; +} + +static int +detectFormat(Tagctx *ctx) +{ + if(isWavpack(ctx)) + return Fwavpack; + return Funknown; +} + +static int +tagHasHeader(u32int tags) +{ + return (tags&(1<<31)) >> 31; +} + +static int +tagIsHeader(u32int tags) +{ + return (tags&(1<<29)) >> 29; +} + +static TagType +tagGetType(u32int tags) +{ + switch((tags&(1<<1))>>1 | (tags&(1<<2))>>1 | (tags&(1<<3))>>1 | (tags&(1<<4))>>1){ + case 0: + return TagUTF8; + case 1: + return TagBinary; + case 2: + return TagExternal; + case 3: + return TagReserved; + default: + return TagInvalid; + } +} + +static int +tagTagType(char *name) +{ + if(!strcmp(name, "Album")) + return Talbum; + else if(!strcmp(name, "Album Artist")) + return Talbumartist; + else if(!strcmp(name, "Artist")) + return Tartist; + else if(!strcmp(name, "Comment")) + return Tcomment; + else if(!strcmp(name, "Composer")) + return Tcomposer; + else if(!strncmp(name, "Cover Art (", 11)){ + if(name[strlen(name)-1] == ')') + return Timage; + }else if(!strcmp(name, "Genre")) + return Tgenre; + else if(!strcmp(name, "Replaygain_Album_Gain")) + return Talbumgain; + else if(!strcmp(name, "Replaygain_Album_Peak")) + return Talbumpeak; + else if(!strcmp(name, "Replaygain_Track_Gain")) + return Ttrackgain; + else if(!strcmp(name, "Replaygain_Track_Peak")) + return Ttrackpeak; + else if(!strcmp(name, "Title")) + return Ttitle; + else if(!strcmp(name, "Track")) + return Ttrack; + else if(!strcmp(name, "Year")) + return Tdate; + return Tunknown; +} + +int +tagape(Tagctx *ctx) +{ + uchar footer[FooterSize]; + u32int i, count; + + ctx->format = detectFormat(ctx); + + if(ctx->seek(ctx, -FooterSize, 2) < 0) + return -1; + if(ctx->read(ctx, footer, FooterSize) != FooterSize) + return -1; + if(memcmp(footer+MagicOffset, "APETAGEX", 8)) + return -1; + if(leuint(footer+VersionOffset) != 2000) + return -1; + if(tagIsHeader(leuint(footer+FlagsOffset))) + return -1; + + if(ctx->seek(ctx, -FooterSize-leuint(footer+SizeOffset), 2) < 0) + return -1; + if(tagHasHeader(leuint(footer+FlagsOffset))){ + uchar header[HeaderSize]; + if(ctx->read(ctx, header, HeaderSize) != HeaderSize) + return -1; + if(memcmp(header, footer, 23)) + return -1; + if(!tagHasHeader(leuint(header+FlagsOffset))) + return -1; + if(!tagIsHeader(leuint(header+FlagsOffset))) + return -1; + }else if(ctx->seek(ctx, HeaderSize, 1) < 0) + return -1; + + for(i = 0, count = leuint(footer+CountOffset); i < count; i++){ + int valueOffset = 0; + char c; + u32int d, length, flags; + + if(ctx->read(ctx, &d, 4) != 4) + return -1; + length = leuint(&d); + if(ctx->read(ctx, &d, 4) != 4) + return -1; + flags = leuint(&d); + + do{ + if(valueOffset == ctx->bufsz) + return -1; + if(ctx->read(ctx, &c, 1) != 1) + return -1; + if(c<' ' || c>'~') + if(c != '\0') + return -1; + ctx->buf[valueOffset++] = c; + }while(c != '\0'); + if(valueOffset+1+(int)length>ctx->bufsz && tagTagType(ctx->buf)!=Timage){ + if(ctx->seek(ctx, length, 1) < 0) + return -1; + continue; + } + + switch(tagGetType(flags)){ + u32int keyOffset; + case TagUTF8: + if(ctx->read(ctx, ctx->buf+valueOffset, length) != (int)length) + return -1; + (ctx->buf+valueOffset)[length] = '\0'; + for(keyOffset = 0; keyOffset != length;){ + txtcb(ctx, tagTagType(ctx->buf), ctx->buf, ctx->buf+valueOffset+keyOffset); + if(keyOffset += strlen(ctx->buf+valueOffset+keyOffset), keyOffset != length) + keyOffset++; + } + break; + case TagBinary: + if(tagTagType(ctx->buf) == Timage) + tagscallcb(ctx, Timage, ctx->buf, ctx->buf, ctx->seek(ctx, 0, 1), length, NULL); + if(ctx->seek(ctx, length, 1) < 0) + return -1; + break; + default: + if(ctx->seek(ctx, length, 1) < 0) + return -1; + } + } + return 0; +} diff --git a/lib/libtags/tags.c b/lib/libtags/tags.c index b1d6ac33..d3c577dd 100644 --- a/lib/libtags/tags.c +++ b/lib/libtags/tags.c @@ -8,6 +8,7 @@ struct Getter int format; }; +extern int tagape(Tagctx *ctx); extern int tagflac(Tagctx *ctx); extern int tagid3v1(Tagctx *ctx); extern int tagid3v2(Tagctx *ctx); @@ -22,6 +23,7 @@ extern int tagmod(Tagctx *ctx); static const Getter g[] = { + {tagape, Funknown}, {tagid3v2, Fmp3}, {tagid3v1, Fmp3}, {tagvorbis, Fogg}, diff --git a/lib/libtags/tags.h b/lib/libtags/tags.h index b2aa2dfb..0b54936a 100644 --- a/lib/libtags/tags.h +++ b/lib/libtags/tags.h @@ -37,6 +37,7 @@ enum Fm4a, Fopus, Fwav, + Fwavpack, Fit, Fxm, Fs3m,