audio/: zuke, mkplist, readtags

front
Sigrid 2021-04-13 13:20:27 +02:00
parent b777d3fe7d
commit c6cdee420d
27 changed files with 3474 additions and 2 deletions

View File

@ -0,0 +1,29 @@
/* http://en.wikipedia.org/wiki/ISO/IEC_8859-1 */
#include "tagspriv.h"
int
iso88591toutf8(uchar *o, int osz, const uchar *s, int sz)
{
int i;
for(i = 0; i < sz && osz > 1 && s[i] != 0; i++){
if(s[i] >= 0xa0 && osz < 3)
break;
if(s[i] >= 0xc0){
*o++ = 0xc3;
*o++ = s[i] - 0x40;
osz--;
}else if(s[i] >= 0xa0){
*o++ = 0xc2;
*o++ = s[i];
osz--;
}else{
*o++ = s[i];
}
osz--;
}
*o = 0;
return i;
}

View File

@ -0,0 +1,108 @@
/* https://xiph.org/flac/format.html */
#include "tagspriv.h"
#define beu3(d) ((d)[0]<<16 | (d)[1]<<8 | (d)[2]<<0)
int
tagflac(Tagctx *ctx)
{
uchar *d;
int sz, last;
uvlong g;
d = (uchar*)ctx->buf;
/* 8 bytes for marker, block type, length. 18 bytes for the stream info */
if(ctx->read(ctx, d, 8+18) != 8+18 || memcmp(d, "fLaC\x00", 5) != 0)
return -1;
sz = beu3(&d[5]); /* size of the stream info */
ctx->samplerate = beu3(&d[18]) >> 4;
ctx->channels = ((d[20]>>1) & 7) + 1;
g = (uvlong)(d[21] & 0xf)<<32 | beu3(&d[22])<<8 | d[25];
ctx->duration = g * 1000 / ctx->samplerate;
/* skip the rest of the stream info */
if(ctx->seek(ctx, sz-18, 1) != 8+sz)
return -1;
for(last = 0; !last;){
if(ctx->read(ctx, d, 4) != 4)
return -1;
sz = beu3(&d[1]);
if((d[0] & 0x80) != 0)
last = 1;
if((d[0] & 0x7f) == 6){ /* 6 = picture */
int n, offset;
char *mime;
if(sz < 16 || ctx->read(ctx, d, 8) != 8) /* type, mime length */
return -1;
sz -= 8;
n = beuint(&d[4]);
mime = ctx->buf+20;
if(n >= sz || n >= ctx->bufsz-1 || ctx->read(ctx, mime, n) != n)
return -1;
sz -= n;
mime[n] = 0;
ctx->read(ctx, d, 4); /* description */
sz -= 4;
offset = beuint(d) + ctx->seek(ctx, 0, 1) + 20;
ctx->read(ctx, d, 20);
sz -= 20;
n = beuint(&d[16]);
tagscallcb(ctx, Timage, "", mime, offset, n, nil);
if(ctx->seek(ctx, sz, 1) <= 0)
return -1;
}else if((d[0] & 0x7f) == 4){ /* 4 = vorbis comment */
int i, numtags, tagsz, vensz;
char *k, *v;
if(sz < 12 || ctx->read(ctx, d, 4) != 4)
return -1;
sz -= 4;
vensz = leuint(d);
if(vensz < 0 || vensz > sz-4)
return -1;
/* skip vendor, read the number of tags */
if(ctx->seek(ctx, vensz, 1) < 0 || ctx->read(ctx, d, 4) != 4)
return -1;
sz -= vensz + 4;
numtags = leuint(d);
for(i = 0; i < numtags && sz > 4; i++){
if(ctx->read(ctx, d, 4) != 4)
return -1;
tagsz = leuint(d);
sz -= 4;
if(tagsz > sz)
return -1;
/* if it doesn't fit, ignore it */
if(tagsz+1 > ctx->bufsz){
if(ctx->seek(ctx, tagsz, 1) < 0)
return -1;
continue;
}
k = ctx->buf;
if(ctx->read(ctx, k, tagsz) != tagsz)
return -1;
/* some tags have a stupid '\r'; ignore */
if(k[tagsz-1] == '\r')
k[tagsz-1] = 0;
k[tagsz] = 0;
if((v = strchr(k, '=')) != nil){
*v++ = 0;
cbvorbiscomment(ctx, k, v);
}
}
}else if(ctx->seek(ctx, sz, 1) <= 0)
return -1;
}
return 0;
}

View File

@ -0,0 +1,42 @@
#include "tagspriv.h"
const char *id3genres[Numgenre] =
{
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk",
"Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies",
"Other", "Pop", "Rhythm and Blues", "Rap", "Reggae", "Rock",
"Techno", "Industrial", "Alternative", "Ska", "Death Metal",
"Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
"Vocal", "Jazz & Funk", "Fusion", "Trance", "Classical",
"Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
"Noise", "Alternative Rock", "Bass", "Soul", "Punk rock", "Space",
"Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic",
"Gothic", "Darkwave", "Techno-Industrial", "Electronic",
"Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
"Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk",
"Jungle", "Native American", "Cabaret", "New Wave", "Psychedelic",
"Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk",
"Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock",
"Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion",
"Bebop", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde",
"Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock",
"Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic",
"Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata",
"Symphony", "Booty Bass", "Primus", "Porn groove", "Satire", "Slow Jam",
"Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad",
"Rhythmic Soul", "Freestyle", "Duet", "Punk rock", "Drum Solo", "A capella",
"Euro-House", "Dance Hall", "Goa Trance", "Drum & Bass",
"Club-House", "Hardcore Techno", "Terror", "Indie", "BritPop",
"Afro-punk", "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal",
"Black Metal", "Crossover", "Contemporary Christian", "Christian Rock",
"Merengue", "Salsa", "Thrash Metal", "Anime", "Jpop", "Synthpop",
"Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat",
"Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic",
"Electro", "Electroclash", "Emo", "Experimental", "Garage",
"Global", "IDM", "Illbient", "Industro-Goth", "Jam Band",
"Krautrock", "Leftfield", "Lounge", "Math Rock", "New Romantic",
"Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze",
"Space Rock", "Trop Rock", "World Music", "Neoclassical",
"Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast",
"Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient",
};

View File

@ -0,0 +1,48 @@
/*
* http://en.wikipedia.org/wiki/ID3
* Space-padded strings are mentioned there. This is wrong and is a lie.
*/
#include "tagspriv.h"
enum
{
Insz = 128,
Outsz = 61,
};
int
tagid3v1(Tagctx *ctx)
{
uchar *in, *out;
if(ctx->bufsz < Insz+Outsz)
return -1;
in = (uchar*)ctx->buf;
out = in + Insz;
if(ctx->seek(ctx, -Insz, 2) < 0)
return -1;
if(ctx->read(ctx, in, Insz) != Insz || memcmp(in, "TAG", 3) != 0)
return -1;
if((ctx->found & 1<<Ttitle) == 0 && iso88591toutf8(out, Outsz, &in[3], 30) > 0)
txtcb(ctx, Ttitle, "", out);
if((ctx->found & 1<<Tartist) == 0 && iso88591toutf8(out, Outsz, &in[33], 30) > 0)
txtcb(ctx, Tartist, "", out);
if((ctx->found & 1<<Talbum) == 0 && iso88591toutf8(out, Outsz, &in[63], 30) > 0)
txtcb(ctx, Talbum, "", out);
in[93+4] = 0;
if((ctx->found & 1<<Tdate) == 0 && in[93] != 0)
txtcb(ctx, Tdate, "", &in[93]);
if((ctx->found & 1<<Ttrack) == 0 && in[125] == 0 && in[126] > 0){
snprint((char*)out, Outsz, "%d", in[126]);
txtcb(ctx, Ttrack, "", out);
}
if((ctx->found & 1<<Tgenre) == 0 && in[127] < Numgenre)
txtcb(ctx, Tgenre, "", id3genres[in[127]]);
return 0;
}

View File

@ -0,0 +1,471 @@
/*
* Have fun reading the following:
*
* http://id3.org/id3v2.4.0-structure
* http://id3.org/id3v2.4.0-frames
* http://id3.org/d3v2.3.0
* http://id3.org/id3v2-00
* http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
* http://wiki.hydrogenaud.io/index.php?title=MP3#VBRI.2C_XING.2C_and_LAME_headers
* http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header#VBRIHeader
*/
#include "tagspriv.h"
#define synchsafe(d) (uint)(((d)[0]&127)<<21 | ((d)[1]&127)<<14 | ((d)[2]&127)<<7 | ((d)[3]&127)<<0)
static int
v2cb(Tagctx *ctx, char *k, char *v)
{
k++;
if(strcmp(k, "AL") == 0 || strcmp(k, "ALB") == 0)
txtcb(ctx, Talbum, k-1, v);
else if(strcmp(k, "PE1") == 0 || strcmp(k, "PE2") == 0 || strcmp(k, "P1") == 0 || strcmp(k, "P2") == 0)
txtcb(ctx, Tartist, k-1, v);
else if(strcmp(k, "IT2") == 0 || strcmp(k, "T2") == 0)
txtcb(ctx, Ttitle, k-1, v);
else if(strcmp(k, "YE") == 0 || strcmp(k, "YER") == 0 || strcmp(k, "DRC") == 0)
txtcb(ctx, Tdate, k-1, v);
else if(strcmp(k, "RK") == 0 || strcmp(k, "RCK") == 0)
txtcb(ctx, Ttrack, k-1, v);
else if(strcmp(k, "CO") == 0 || strcmp(k, "CON") == 0){
for(; v[0]; v++){
if(v[0] == '(' && v[1] <= '9' && v[1] >= '0'){
int i = atoi(&v[1]);
if(i < Numgenre)
txtcb(ctx, Tgenre, k-1, id3genres[i]);
for(v++; v[0] && v[0] != ')'; v++);
v--;
}else if(v[0] != '(' && v[0] != ')'){
txtcb(ctx, Tgenre, k-1, v);
break;
}
}
}else if(strcmp(k, "XXX") == 0 && strncmp(v, "REPLAYGAIN_", 11) == 0){
int type = -1;
v += 11;
if(strncmp(v, "TRACK_", 6) == 0){
v += 6;
if(strcmp(v, "GAIN") == 0)
type = Ttrackgain;
else if(strcmp(v, "PEAK") == 0)
type = Ttrackpeak;
}else if(strncmp(v, "ALBUM_", 6) == 0){
v += 6;
if(strcmp(v, "GAIN") == 0)
type = Talbumgain;
else if(strcmp(v, "PEAK") == 0)
type = Talbumpeak;
}
if(type >= 0)
txtcb(ctx, type, k-1, v+5);
else
return 0;
}else{
txtcb(ctx, Tunknown, k-1, v);
}
return 1;
}
static int
rva2(Tagctx *ctx, char *tag, int sz)
{
uchar *b, *end;
if((b = memchr(tag, 0, sz)) == nil)
return -1;
b++;
for(end = (uchar*)tag+sz; b+4 < end; b += 5){
int type = b[0];
float peak;
float va = (float)(b[1]<<8 | b[2]) / 512.0f;
if(b[3] == 24){
peak = (float)(b[4]<<16 | b[5]<<8 | b[6]) / 32768.0f;
b += 2;
}else if(b[3] == 16){
peak = (float)(b[4]<<8 | b[5]) / 32768.0f;
b += 1;
}else if(b[3] == 8){
peak = (float)b[4] / 32768.0f;
}else
return -1;
if(type == 1){ /* master volume */
char vas[16], peaks[8];
snprint(vas, sizeof(vas), "%+.5f dB", va);
snprint(peaks, sizeof(peaks), "%.5f", peak);
vas[sizeof(vas)-1] = 0;
peaks[sizeof(peaks)-1] = 0;
if(strcmp((char*)tag, "track") == 0){
txtcb(ctx, Ttrackgain, "RVA2", vas);
txtcb(ctx, Ttrackpeak, "RVA2", peaks);
}else if(strcmp((char*)tag, "album") == 0){
txtcb(ctx, Talbumgain, "RVA2", vas);
txtcb(ctx, Talbumpeak, "RVA2", peaks);
}
break;
}
}
return 0;
}
static int
resync(uchar *b, int sz)
{
int i;
if(sz < 4)
return sz;
for(i = 0; i < sz-2; i++){
if(b[i] == 0xff && b[i+1] == 0x00 && (b[i+2] & 0xe0) == 0xe0){
memmove(&b[i+1], &b[i+2], sz-i-2);
sz--;
}
}
return sz;
}
static int
unsyncread(void *buf, int *sz)
{
int i;
uchar *b;
b = buf;
for(i = 0; i < *sz; i++){
if(b[i] == 0xff){
if(i+1 >= *sz || (b[i+1] == 0x00 && i+2 >= *sz))
break;
if(b[i+1] == 0x00 && (b[i+2] & 0xe0) == 0xe0){
memmove(&b[i+1], &b[i+2], *sz-i-2);
(*sz)--;
}
}
}
return i;
}
static int
nontext(Tagctx *ctx, uchar *d, int tsz, int unsync)
{
int n, offset;
char *b, *tag;
Tagread f;
tag = ctx->buf;
n = 0;
f = unsync ? unsyncread : nil;
if(strcmp((char*)d, "APIC") == 0){
offset = ctx->seek(ctx, 0, 1);
if((n = ctx->read(ctx, tag, 256)) == 256){ /* APIC mime and description should fit */
b = tag + 1; /* mime type */
for(n = 1 + strlen(b) + 2; n < 253; n++){
if(tag[0] == 0 || tag[0] == 3){ /* one zero byte */
if(tag[n] == 0){
n++;
break;
}
}else if(tag[n] == 0 && tag[n+1] == 0 && tag[n+2] == 0){
n += 3;
break;
}
}
tagscallcb(ctx, Timage, "APIC", b, offset+n, tsz-n, f);
n = 256;
}
}else if(strcmp((char*)d, "PIC") == 0){
offset = ctx->seek(ctx, 0, 1);
if((n = ctx->read(ctx, tag, 256)) == 256){ /* PIC description should fit */
b = tag + 1; /* mime type */
for(n = 5; n < 253; n++){
if(tag[0] == 0 || tag[0] == 3){ /* one zero byte */
if(tag[n] == 0){
n++;
break;
}
}else if(tag[n] == 0 && tag[n+1] == 0 && tag[n+2] == 0){
n += 3;
break;
}
}
tagscallcb(ctx, Timage, "PIC", strcmp(b, "JPG") == 0 ? "image/jpeg" : "image/png", offset+n, tsz-n, f);
n = 256;
}
}else if(strcmp((char*)d, "RVA2") == 0 && tsz >= 6+5){
/* replay gain. 6 = "track\0", 5 = other */
if(ctx->bufsz >= tsz && (n = ctx->read(ctx, tag, tsz)) == tsz)
rva2(ctx, tag, unsync ? resync((uchar*)tag, n) : n);
}
return ctx->seek(ctx, tsz-n, 1) < 0 ? -1 : 0;
}
static int
text(Tagctx *ctx, uchar *d, int tsz, int unsync)
{
char *b, *tag;
if(ctx->bufsz >= tsz+1){
/* place the data at the end to make best effort at charset conversion */
tag = &ctx->buf[ctx->bufsz - tsz - 1];
if(ctx->read(ctx, tag, tsz) != tsz)
return -1;
}else{
ctx->seek(ctx, tsz, 1);
return 0;
}
if(unsync)
tsz = resync((uchar*)tag, tsz);
tag[tsz] = 0;
b = &tag[1];
switch(tag[0]){
case 0: /* iso-8859-1 */
if(iso88591toutf8((uchar*)ctx->buf, ctx->bufsz, (uchar*)b, tsz) > 0)
v2cb(ctx, (char*)d, ctx->buf);
break;
case 1: /* utf-16 */
case 2:
if(utf16to8((uchar*)ctx->buf, ctx->bufsz, (uchar*)b, tsz) > 0)
v2cb(ctx, (char*)d, ctx->buf);
break;
case 3: /* utf-8 */
if(*b)
v2cb(ctx, (char*)d, b);
break;
}
return 0;
}
static int
isid3(uchar *d)
{
/* "ID3" version[2] flags[1] size[4] */
return (
d[0] == 'I' && d[1] == 'D' && d[2] == '3' &&
d[3] < 0xff && d[4] < 0xff &&
d[6] < 0x80 && d[7] < 0x80 && d[8] < 0x80 && d[9] < 0x80
);
}
static const uchar bitrates[4][4][16] = {
{
{0},
{0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 0}, /* v2.5 III */
{0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 0}, /* v2.5 II */
{0, 16, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96, 112, 128, 0}, /* v2.5 I */
},
{ {0}, {0}, {0}, {0} },
{
{0},
{0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 0}, /* v2 III */
{0, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 64, 72, 80, 0}, /* v2 II */
{0, 16, 24, 28, 32, 40, 48, 56, 64, 72, 80, 88, 96, 112, 128, 0}, /* v2 I */
},
{
{0},
{0, 16, 20, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 0}, /* v1 III */
{0, 16, 24, 28, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 0}, /* v1 II */
{0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 0}, /* v1 I */
}
};
static const uint samplerates[4][4] = {
{11025, 12000, 8000, 0},
{ 0, 0, 0, 0},
{22050, 24000, 16000, 0},
{44100, 48000, 32000, 0},
};
static const int chans[] = {2, 2, 2, 1};
static const int samplesframe[4][4] = {
{0, 0, 0, 0},
{0, 576, 1152, 384},
{0, 576, 1152, 384},
{0, 1152, 1152, 384},
};
static void
getduration(Tagctx *ctx, int offset)
{
uvlong n, framelen, samplespf, toc;
uchar *b;
uint x;
int xversion, xlayer, xbitrate, i;
if(ctx->read(ctx, ctx->buf, 256) != 256)
return;
x = beuint((uchar*)ctx->buf);
xversion = x >> 19 & 3;
xlayer = x >> 17 & 3;
xbitrate = x >> 12 & 0xf;
ctx->bitrate = 2000*(int)bitrates[xversion][xlayer][xbitrate];
samplespf = samplesframe[xversion][xlayer];
ctx->samplerate = samplerates[xversion][x >> 10 & 3];
ctx->channels = chans[x >> 6 & 3];
if(ctx->samplerate > 0){
framelen = (uvlong)144*ctx->bitrate / ctx->samplerate;
if((x & (1<<9)) != 0) /* padding */
framelen += xlayer == 3 ? 4 : 1; /* for I it's 4 bytes */
if(memcmp(&ctx->buf[0x24], "Info", 4) == 0 || memcmp(&ctx->buf[0x24], "Xing", 4) == 0){
b = (uchar*)ctx->buf + 0x28;
x = beuint(b); b += 4;
if((x & 1) != 0){ /* number of frames is set */
n = beuint(b); b += 4;
ctx->duration = n * samplespf * 1000 / ctx->samplerate;
}
if((x & 2) != 0){ /* file size is set */
n = beuint(b); b += 4;
if(ctx->duration == 0 && framelen > 0)
ctx->duration = n * samplespf * 1000 / framelen / ctx->samplerate;
if((x & 4) != 0 && ctx->toc != nil){ /* TOC is set */
toc = offset + 100 + (char*)b - ctx->buf;
if((x & 8) != 0) /* VBR scale */
toc += 4;
for(i = 0; i < 100; i++){
/*
* offset = n * b[i] / 256
* ms = i * duration / 100
*/
ctx->toc(ctx, i * ctx->duration / 100, toc + (n * b[i]) / 256);
}
b += 100;
if((x & 8) != 0) /* VBR scale */
b += 4;
}
}
offset += (char*)b - ctx->buf;
}else if(memcmp(&ctx->buf[0x24], "VBRI", 4) == 0){
n = beuint((uchar*)&ctx->buf[0x32]);
ctx->duration = n * samplespf * 1000 / ctx->samplerate;
if(ctx->duration == 0 && framelen > 0){
n = beuint((uchar*)&ctx->buf[0x28]); /* file size */
ctx->duration = n * samplespf * 1000 / framelen / ctx->samplerate;
}
}
}
if(ctx->bitrate > 0 && ctx->duration == 0) /* worst case -- use real file size instead */
ctx->duration = (ctx->seek(ctx, 0, 2) - offset)/(ctx->bitrate / 1000) * 8;
}
int
tagid3v2(Tagctx *ctx)
{
int sz, exsz, framesz;
int ver, unsync, offset;
uchar d[10], *b;
if(ctx->read(ctx, d, sizeof(d)) != sizeof(d))
return -1;
if(!isid3(d)){ /* no tags, but the stream information is there */
if(d[0] != 0xff || (d[1] & 0xfe) != 0xfa)
return -1;
ctx->seek(ctx, -(int)sizeof(d), 1);
getduration(ctx, 0);
return 0;
}
header:
ver = d[3];
unsync = d[5] & (1<<7);
sz = synchsafe(&d[6]);
if(ver == 2 && (d[5] & (1<<6)) != 0) /* compression */
return -1;
if(ver > 2){
if((d[5] & (1<<4)) != 0) /* footer */
sz -= 10;
if((d[5] & (1<<6)) != 0){ /* skip extended header */
if(ctx->read(ctx, d, 4) != 4)
return -1;
exsz = (ver >= 3) ? beuint(d) : synchsafe(d);
if(ctx->seek(ctx, exsz, 1) < 0)
return -1;
sz -= exsz;
}
}
framesz = (ver >= 3) ? 10 : 6;
for(; sz > framesz;){
int tsz, frameunsync;
if(ctx->read(ctx, d, framesz) != framesz)
return -1;
sz -= framesz;
/* return on padding */
if(memcmp(d, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", framesz) == 0)
break;
if(ver >= 3){
tsz = (ver == 3) ? beuint(&d[4]) : synchsafe(&d[4]);
if(tsz < 0 || tsz > sz)
break;
frameunsync = d[9] & (1<<1);
d[4] = 0;
if((d[9] & 0x0c) != 0){ /* compression & encryption */
ctx->seek(ctx, tsz, 1);
sz -= tsz;
continue;
}
if(ver == 4 && (d[9] & 1<<0) != 0){ /* skip data length indicator */
ctx->seek(ctx, 4, 1);
sz -= 4;
tsz -= 4;
}
}else{
tsz = beuint(&d[3]) >> 8;
if(tsz > sz)
return -1;
frameunsync = 0;
d[3] = 0;
}
sz -= tsz;
if(d[0] == 'T' && text(ctx, d, tsz, unsync || frameunsync) != 0)
return -1;
else if(d[0] != 'T' && nontext(ctx, d, tsz, unsync || frameunsync) != 0)
return -1;
}
offset = ctx->seek(ctx, sz, 1);
sz = ctx->bufsz <= 2048 ? ctx->bufsz : 2048;
b = nil;
for(exsz = 0; exsz < 2048; exsz += sz){
if(ctx->read(ctx, ctx->buf, sz) != sz)
break;
for(b = (uchar*)ctx->buf; (b = memchr(b, 'I', sz - 1 - ((char*)b - ctx->buf))) != nil; b++){
ctx->seek(ctx, (char*)b - ctx->buf + offset + exsz, 0);
if(ctx->read(ctx, d, sizeof(d)) != sizeof(d))
return 0;
if(isid3(d))
goto header;
}
for(b = (uchar*)ctx->buf; (b = memchr(b, 0xff, sz-3)) != nil; b++){
if((b[1] & 0xe0) == 0xe0){
offset = ctx->seek(ctx, (char*)b - ctx->buf + offset + exsz, 0);
exsz = 2048;
break;
}
}
}
if(b != nil)
getduration(ctx, offset);
return 0;
}

View File

@ -0,0 +1,14 @@
#include "tagspriv.h"
int
tagit(Tagctx *ctx)
{
char d[4+26+1];
if(ctx->read(ctx, d, 4+26) != 4+26 || memcmp(d, "IMPM", 4) != 0)
return -1;
d[4+26] = 0;
txtcb(ctx, Ttitle, "", d+4);
return 0;
}

View File

@ -0,0 +1,154 @@
/* http://wiki.multimedia.cx/?title=QuickTime_container */
/* https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html */
#include "tagspriv.h"
#define beuint16(d) (ushort)((d)[0]<<8 | (d)[1]<<0)
int
tagm4a(Tagctx *ctx)
{
uvlong duration;
uchar *d;
int sz, type, dtype, i, skip, n;
d = (uchar*)ctx->buf;
/* 4 bytes for atom size, 4 for type, 4 for data - exect "ftyp" to come first */
if(ctx->read(ctx, d, 4+4+4) != 4+4+4 || memcmp(d+4, "ftypM4A ", 8) != 0)
return -1;
sz = beuint(d) - 4; /* already have 8 bytes */
for(;;){
if(ctx->seek(ctx, sz, 1) < 0)
return -1;
if(ctx->read(ctx, d, 4) != 4) /* size */
break;
sz = beuint(d);
if(sz == 0)
continue;
if(ctx->read(ctx, d, 4) != 4) /* type */
return -1;
if(sz < 8)
continue;
d[4] = 0;
if(memcmp(d, "meta", 4) == 0){
sz = 4;
continue;
}else if(
memcmp(d, "udta", 4) == 0 ||
memcmp(d, "ilst", 4) == 0 ||
memcmp(d, "trak", 4) == 0 ||
memcmp(d, "mdia", 4) == 0 ||
memcmp(d, "minf", 4) == 0 ||
memcmp(d, "moov", 4) == 0 ||
memcmp(d, "trak", 4) == 0 ||
memcmp(d, "stbl", 4) == 0){
sz = 0;
continue;
}else if(memcmp(d, "stsd", 4) == 0){
sz -= 8;
if(ctx->read(ctx, d, 8) != 8)
return -1;
sz -= 8;
for(i = beuint(&d[4]); i > 0 && sz > 0; i--){
if(ctx->read(ctx, d, 8) != 8) /* size + format */
return -1;
sz -= 8;
skip = beuint(d) - 8;
if(memcmp(&d[4], "mp4a", 4) == 0){ /* audio */
n = 6+2 + 2+4+2 + 2+2 + 2+2 + 4; /* read a bunch at once */
/* reserved+id, ver+rev+vendor, channels+bps, ?+?, sample rate */
if(ctx->read(ctx, d, n) != n)
return -1;
skip -= n;
sz -= n;
ctx->channels = beuint16(&d[16]);
ctx->samplerate = beuint(&d[24])>>16;
}
if(ctx->seek(ctx, skip, 1) < 0)
return -1;
sz -= skip;
}
continue;
}
sz -= 8;
type = -1;
if(memcmp(d, "\251nam", 4) == 0)
type = Ttitle;
else if(memcmp(d, "\251alb", 4) == 0)
type = Talbum;
else if(memcmp(d, "\251ART", 4) == 0)
type = Tartist;
else if(memcmp(d, "\251gen", 4) == 0 || memcmp(d, "gnre", 4) == 0)
type = Tgenre;
else if(memcmp(d, "\251day", 4) == 0)
type = Tdate;
else if(memcmp(d, "covr", 4) == 0)
type = Timage;
else if(memcmp(d, "trkn", 4) == 0)
type = Ttrack;
else if(memcmp(d, "mdhd", 4) == 0){
if(ctx->read(ctx, d, 4) != 4)
return -1;
sz -= 4;
duration = 0;
if(d[0] == 0){ /* version 0 */
if(ctx->read(ctx, d, 16) != 16)
return -1;
sz -= 16;
duration = beuint(&d[12]) / beuint(&d[8]);
}else if(d[1] == 1){ /* version 1 */
if(ctx->read(ctx, d, 28) != 28)
return -1;
sz -= 28;
duration = ((uvlong)beuint(&d[20])<<32 | beuint(&d[24])) / (uvlong)beuint(&d[16]);
}
ctx->duration = duration * 1000;
continue;
}
if(type < 0)
continue;
if(ctx->seek(ctx, 8, 1) < 0) /* skip size and "data" */
return -1;
sz -= 8;
if(ctx->read(ctx, d, 8) != 8) /* read data type and 4 bytes of whatever else */
return -1;
sz -= 8;
d[0] = 0;
dtype = beuint(d);
if(type == Ttrack){
if(ctx->read(ctx, d, 4) != 4)
return -1;
sz -= 4;
snprint((char*)d, ctx->bufsz, "%d", beuint(d));
txtcb(ctx, type, "", d);
}else if(type == Tgenre){
if(ctx->read(ctx, d, 2) != 2)
return -1;
sz -= 2;
if((i = d[1]-1) >= 0 && i < Numgenre)
txtcb(ctx, type, "", id3genres[i]);
}else if(dtype == 1){ /* text */
if(sz >= ctx->bufsz) /* skip tags that can't fit into memory. ">=" because of '\0' */
continue;
if(ctx->read(ctx, d, sz) != sz)
return -1;
d[sz] = 0;
txtcb(ctx, type, "", d);
sz = 0;
}else if(type == Timage && dtype == 13) /* jpeg cover image */
tagscallcb(ctx, Timage, "", "image/jpeg", ctx->seek(ctx, 0, 1), sz, nil);
else if(type == Timage && dtype == 14) /* png cover image */
tagscallcb(ctx, Timage, "", "image/png", ctx->seek(ctx, 0, 1), sz, nil);
}
return 0;
}

View File

@ -0,0 +1,24 @@
</$objtype/mkfile
LIB=libtags.a$O
OFILES=\
8859.$O\
flac.$O\
id3genres.$O\
id3v1.$O\
id3v2.$O\
it.$O\
m4a.$O\
opus.$O\
s3m.$O\
tags.$O\
utf16.$O\
vorbis.$O\
wav.$O\
xm.$O\
HFILES=\
tags.h\
tagspriv.h\
</sys/src/cmd/mklib

View File

@ -0,0 +1,91 @@
#include "tagspriv.h"
int
tagopus(Tagctx *ctx)
{
char *v;
uchar *d, h[4];
int sz, numtags, i, npages;
d = (uchar*)ctx->buf;
/* need to find vorbis frame with type=3 */
for(npages = 0; npages < 2; npages++){ /* vorbis comment is the second header */
int nsegs;
if(ctx->read(ctx, d, 27) != 27)
return -1;
if(memcmp(d, "OggS", 4) != 0)
return -1;
/* calculate the size of the packet */
nsegs = d[26];
if(ctx->read(ctx, d, nsegs+8) != nsegs+8)
return -1;
for(sz = i = 0; i < nsegs; sz += d[i++]);
if(memcmp(&d[nsegs], "OpusHead", 8) == 0){
if(ctx->read(ctx, d, 8) != 8 || d[0] != 1)
return -1;
sz -= 8;
ctx->channels = d[1];
ctx->samplerate = leuint(&d[4]);
}else if(memcmp(&d[nsegs], "OpusTags", 8) == 0){
break;
}
ctx->seek(ctx, sz-8, 1);
}
if(npages < 3){
if(ctx->read(ctx, d, 4) != 4)
return -1;
sz = leuint(d);
if(ctx->seek(ctx, sz, 1) < 0 || ctx->read(ctx, h, 4) != 4)
return -1;
numtags = leuint(h);
for(i = 0; i < numtags; i++){
if(ctx->read(ctx, h, 4) != 4)
return -1;
if((sz = leuint(h)) < 0)
return -1;
if(ctx->bufsz < sz+1){
if(ctx->seek(ctx, sz, 1) < 0)
return -1;
continue;
}
if(ctx->read(ctx, ctx->buf, sz) != sz)
return -1;
ctx->buf[sz] = 0;
if((v = strchr(ctx->buf, '=')) == nil)
return -1;
*v++ = 0;
cbvorbiscomment(ctx, ctx->buf, v);
}
}
/* calculate the duration */
if(ctx->samplerate > 0){
sz = ctx->bufsz <= 4096 ? ctx->bufsz : 4096;
for(i = sz; i < 65536+16; i += sz - 16){
if(ctx->seek(ctx, -i, 2) <= 0)
break;
v = ctx->buf;
if(ctx->read(ctx, v, sz) != sz)
break;
for(; v != nil && v < ctx->buf+sz;){
v = memchr(v, 'O', ctx->buf+sz - v - 14);
if(v != nil && v[1] == 'g' && v[2] == 'g' && v[3] == 'S' && (v[5] & 4) == 4){ /* last page */
uvlong g = leuint(v+6) | (uvlong)leuint(v+10)<<32;
ctx->duration = g * 1000 / 48000; /* granule positions are always 48KHz */
return 0;
}
if(v != nil)
v++;
}
}
}
return 0;
}

View File

@ -0,0 +1,16 @@
#include "tagspriv.h"
int
tags3m(Tagctx *ctx)
{
char d[28+1+1], *s;
if(ctx->read(ctx, d, 28+1+1) != 28+1+1 || (d[28] != 0x1a && d[28] != 0) || d[29] != 0x10)
return -1;
d[28] = 0;
for(s = d+27; s != d-1 && (*s == ' ' || *s == 0); s--);
s[1] = 0;
txtcb(ctx, Ttitle, "", d);
return 0;
}

View File

@ -0,0 +1,66 @@
#include "tagspriv.h"
typedef struct Getter Getter;
struct Getter
{
int (*f)(Tagctx *ctx);
int format;
};
extern int tagflac(Tagctx *ctx);
extern int tagid3v1(Tagctx *ctx);
extern int tagid3v2(Tagctx *ctx);
extern int tagit(Tagctx *ctx);
extern int tagm4a(Tagctx *ctx);
extern int tagopus(Tagctx *ctx);
extern int tags3m(Tagctx *ctx);
extern int tagvorbis(Tagctx *ctx);
extern int tagwav(Tagctx *ctx);
extern int tagxm(Tagctx *ctx);
static const Getter g[] =
{
{tagid3v2, Fmp3},
{tagid3v1, Fmp3},
{tagvorbis, Fogg},
{tagflac, Fflac},
{tagm4a, Fm4a},
{tagopus, Fopus},
{tagwav, Fwav},
{tagit, Fit},
{tagxm, Fxm},
{tags3m, Fs3m},
};
void
tagscallcb(Tagctx *ctx, int type, const char *k, const char *s, int offset, int size, Tagread f)
{
if(type != Tunknown){
ctx->found |= 1<<type;
ctx->num++;
}
ctx->tag(ctx, type, k, s, offset, size, f);
}
int
tagsget(Tagctx *ctx)
{
int i, res;
ctx->channels = ctx->samplerate = ctx->bitrate = ctx->duration = 0;
ctx->found = 0;
ctx->format = Funknown;
res = -1;
for(i = 0; i < (int)(sizeof(g)/sizeof(g[0])); i++){
ctx->num = 0;
if(g[i].f(ctx) == 0){
if(ctx->num > 0)
res = 0;
ctx->format = g[i].format;
}
ctx->seek(ctx, 0, 0);
}
return res;
}

View File

@ -0,0 +1,86 @@
#pragma lib "/sys/src/cmd/audio/libtags/libtags.a$O"
typedef struct Tagctx Tagctx;
typedef int (*Tagread)(void *buf, int *cnt);
/* Tag type. */
enum
{
Tunknown = -1,
Tartist,
Talbum,
Ttitle,
Tdate, /* "2014", "2015/02/01", but the year goes first */
Ttrack, /* "1", "01", "1/4", but the track number goes first */
Talbumgain,
Talbumpeak,
Ttrackgain,
Ttrackpeak,
Tgenre,
Timage,
};
/* Format of the audio file. */
enum
{
Funknown = -1,
Fmp3,
Fogg,
Fflac,
Fm4a,
Fopus,
Fwav,
Fit,
Fxm,
Fs3m,
Fmax,
};
/* Tag parser context. You need to set it properly before parsing an audio file using libtags. */
struct Tagctx
{
/* Read function. This is what libtags uses to read the file. */
int (*read)(Tagctx *ctx, void *buf, int cnt);
/* Seek function. This is what libtags uses to seek through the file. */
int (*seek)(Tagctx *ctx, int offset, int whence);
/* Callback that is used by libtags to inform about the tags of a file.
* "type" is the tag's type (Tartist, ...) or Tunknown if libtags doesn't know how to map a tag kind to
* any of these. "k" is the raw key like "TPE1", "TPE2", etc. "s" is the null-terminated string unless "type" is
* Timage. "offset" and "size" define the placement and size of the image cover ("type" = Timage)
* inside the file, and "f" is not NULL in case reading the image cover requires additional
* operations on the data, in which case you need to read the image cover as a stream and call this
* function to apply these operations on the contents read.
*/
void (*tag)(Tagctx *ctx, int type, const char *k, const char *s, int offset, int size, Tagread f);
/* Approximate millisecond-to-byte offsets within the file, if available. This callback is optional. */
void (*toc)(Tagctx *ctx, int ms, int offset);
/* Auxiliary data. Not used by libtags. */
void *aux;
/* Memory buffer to work in. */
char *buf;
/* Size of the buffer. Must be at least 256 bytes. */
int bufsz;
/* Here goes the stuff libtags sets. It should be accessed after tagsget() returns.
* A value of 0 means it's undefined.
*/
int channels; /* Number of channels. */
int samplerate; /* Hz */
int bitrate; /* Bitrate, bits/s. */
int duration; /* ms */
int format; /* Fmp3, Fogg, Fflac, Fm4a */
/* Private, don't touch. */
int found;
int num;
};
/* Parse the file using this function. Returns 0 on success. */
extern int tagsget(Tagctx *ctx);

View File

@ -0,0 +1,39 @@
#include <u.h>
#include <libc.h>
#include "tags.h"
enum
{
Numgenre = 192,
};
#define beuint(d) (uint)(((uchar*)(d))[0]<<24 | ((uchar*)(d))[1]<<16 | ((uchar*)(d))[2]<<8 | ((uchar*)(d))[3]<<0)
#define leuint(d) (uint)(((uchar*)(d))[3]<<24 | ((uchar*)(d))[2]<<16 | ((uchar*)(d))[1]<<8 | ((uchar*)(d))[0]<<0)
extern const char *id3genres[Numgenre];
/*
* Converts (to UTF-8) at most sz bytes of src and writes it to out buffer.
* Returns the number of bytes converted.
* You need sz*2+1 bytes for out buffer to be completely safe.
*/
int iso88591toutf8(uchar *out, int osz, const uchar *src, int sz);
/*
* Converts (to UTF-8) at most sz bytes of src and writes it to out buffer.
* Returns the number of bytes converted or < 0 in case of error.
* You need sz*4+1 bytes for out buffer to be completely safe.
* UTF-16 defaults to big endian if there is no BOM.
*/
int utf16to8(uchar *out, int osz, const uchar *src, int sz);
/*
* This one is common for both vorbis.c and flac.c
* It maps a string k to tag type and executes the callback from ctx.
* Returns 1 if callback was called, 0 otherwise.
*/
void cbvorbiscomment(Tagctx *ctx, char *k, char *v);
void tagscallcb(Tagctx *ctx, int type, const char *k, const char *s, int offset, int size, Tagread f);
#define txtcb(ctx, type, k, s) tagscallcb(ctx, type, k, (const char*)s, 0, 0, nil)

View File

@ -0,0 +1,59 @@
/* Horror stories: http://en.wikipedia.org/wiki/UTF-16 */
#include "tagspriv.h"
#define rchr(s) (be ? ((s)[0]<<8 | (s)[1]) : ((s)[1]<<8 | (s)[0]))
static const uchar mark[] = {0x00, 0x00, 0xc0, 0xe0, 0xf0};
int
utf16to8(uchar *o, int osz, const uchar *s, int sz)
{
int i, be, c, c2, wr, j;
i = 0;
be = 1;
if(s[0] == 0xfe && s[1] == 0xff)
i += 2;
else if(s[0] == 0xff && s[1] == 0xfe){
be = 0;
i += 2;
}
for(; i < sz-1 && osz > 1;){
c = rchr(&s[i]);
i += 2;
if(c >= 0xd800 && c <= 0xdbff && i < sz-1){
c2 = rchr(&s[i]);
if(c2 >= 0xdc00 && c2 <= 0xdfff){
c = 0x10000 | (c - 0xd800)<<10 | (c2 - 0xdc00);
i += 2;
}else
return -1;
}else if(c >= 0xdc00 && c <= 0xdfff)
return -1;
if(c < 0x80)
wr = 1;
else if(c < 0x800)
wr = 2;
else if(c < 0x10000)
wr = 3;
else
wr = 4;
osz -= wr;
if(osz < 1)
break;
o += wr;
for(j = wr; j > 1; j--){
*(--o) = (c & 0xbf) | 0x80;
c >>= 6;
}
*(--o) = c | mark[wr];
o += wr;
}
*o = 0;
return i;
}

View File

@ -0,0 +1,125 @@
/*
* https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005
* https://wiki.xiph.org/VorbisComment
*/
#include "tagspriv.h"
void
cbvorbiscomment(Tagctx *ctx, char *k, char *v){
if(*v == 0)
return;
if(cistrcmp(k, "album") == 0)
txtcb(ctx, Talbum, k, v);
else if(cistrcmp(k, "title") == 0)
txtcb(ctx, Ttitle, k, v);
else if(cistrcmp(k, "artist") == 0)
txtcb(ctx, Tartist, k, v);
else if(cistrcmp(k, "tracknumber") == 0)
txtcb(ctx, Ttrack, k, v);
else if(cistrcmp(k, "date") == 0)
txtcb(ctx, Tdate, k, v);
else if(cistrcmp(k, "replaygain_track_peak") == 0)
txtcb(ctx, Ttrackpeak, k, v);
else if(cistrcmp(k, "replaygain_track_gain") == 0)
txtcb(ctx, Ttrackgain, k, v);
else if(cistrcmp(k, "replaygain_album_peak") == 0)
txtcb(ctx, Talbumpeak, k, v);
else if(cistrcmp(k, "replaygain_album_gain") == 0)
txtcb(ctx, Talbumgain, k, v);
else if(cistrcmp(k, "genre") == 0)
txtcb(ctx, Tgenre, k, v);
else
txtcb(ctx, Tunknown, k, v);
}
int
tagvorbis(Tagctx *ctx)
{
char *v;
uchar *d, h[4];
int sz, numtags, i, npages;
d = (uchar*)ctx->buf;
/* need to find vorbis frame with type=3 */
for(npages = 0; npages < 2; npages++){ /* vorbis comment is the second header */
int nsegs;
if(ctx->read(ctx, d, 27) != 27)
return -1;
if(memcmp(d, "OggS", 4) != 0)
return -1;
/* calculate the size of the packet */
nsegs = d[26];
if(ctx->read(ctx, d, nsegs+1) != nsegs+1)
return -1;
for(sz = i = 0; i < nsegs; sz += d[i++]);
if(d[nsegs] == 3) /* comment */
break;
if(d[nsegs] == 1 && sz >= 28){ /* identification */
if(ctx->read(ctx, d, 28) != 28)
return -1;
sz -= 28;
ctx->channels = d[10];
ctx->samplerate = leuint(&d[11]);
if((ctx->bitrate = leuint(&d[15])) == 0) /* maximum */
ctx->bitrate = leuint(&d[19]); /* nominal */
}
ctx->seek(ctx, sz-1, 1);
}
if(npages < 3) {
if(ctx->read(ctx, &d[1], 10) != 10 || memcmp(&d[1], "vorbis", 6) != 0)
return -1;
sz = leuint(&d[7]);
if(ctx->seek(ctx, sz, 1) < 0 || ctx->read(ctx, h, 4) != 4)
return -1;
numtags = leuint(h);
for(i = 0; i < numtags; i++){
if(ctx->read(ctx, h, 4) != 4)
return -1;
if((sz = leuint(h)) < 0)
return -1;
if(ctx->bufsz < sz+1){
if(ctx->seek(ctx, sz, 1) < 0)
return -1;
continue;
}
if(ctx->read(ctx, ctx->buf, sz) != sz)
return -1;
ctx->buf[sz] = 0;
if((v = strchr(ctx->buf, '=')) == nil)
return -1;
*v++ = 0;
cbvorbiscomment(ctx, ctx->buf, v);
}
}
/* calculate the duration */
if(ctx->samplerate > 0){
sz = ctx->bufsz <= 4096 ? ctx->bufsz : 4096;
for(i = sz; i < 65536+16; i += sz - 16){
if(ctx->seek(ctx, -i, 2) <= 0)
break;
v = ctx->buf;
if(ctx->read(ctx, v, sz) != sz)
break;
for(; v != nil && v < ctx->buf+sz;){
v = memchr(v, 'O', ctx->buf+sz - v - 14);
if(v != nil && v[1] == 'g' && v[2] == 'g' && v[3] == 'S' && (v[5] & 4) == 4){ /* last page */
uvlong g = leuint(v+6) | (uvlong)leuint(v+10)<<32;
ctx->duration = g * 1000 / ctx->samplerate;
return 0;
}
if(v != nil)
v++;
}
}
}
return 0;
}

View File

@ -0,0 +1,87 @@
#include "tagspriv.h"
#define le16u(d) (u16int)((d)[0] | (d)[1]<<8)
static struct {
char *s;
int type;
}t[] = {
{"IART", Tartist},
{"ICRD", Tdate},
{"IGNR", Tgenre},
{"INAM", Ttitle},
{"IPRD", Talbum},
{"ITRK", Ttrack},
};
int
tagwav(Tagctx *ctx)
{
uchar *d;
int i, n, info;
u32int csz;
uvlong sz;
d = (uchar*)ctx->buf;
sz = 1;
info = 0;
for(i = 0; i < 8 && sz > 0; i++){
if(ctx->read(ctx, d, 4+4+(i?0:4)) != 4+4+(i?0:4))
return -1;
if(i == 0){
if(memcmp(d, "RIFF", 4) != 0 || memcmp(d+8, "WAVE", 4) != 0)
return -1;
sz = leuint(d+4);
if(sz < 4)
return -1;
sz -= 4;
continue;
}else if(memcmp(d, "INFO", 4) == 0){
info = 1;
ctx->seek(ctx, -4, 1);
continue;
}
if(sz <= 8)
break;
sz -= 4+4;
csz = leuint(d+4);
if(sz < csz)
break;
sz -= csz;
if(i == 1){
if(memcmp(d, "fmt ", 4) != 0 || csz < 16)
return -1;
if(ctx->read(ctx, d, 16) != 16)
return -1;
csz -= 16;
ctx->channels = le16u(d+2);
ctx->samplerate = leuint(d+4);
ctx->duration = sz*1000 / leuint(d+8);
}else if(memcmp(d, "LIST", 4) == 0){
sz = csz - 4;
continue;
}else if(memcmp(d, "data", 4) == 0){
break;
}else if(info){
csz++;
for(n = 0; n < nelem(t); n++){
if(memcmp(d, t[n].s, 4) == 0){
if(ctx->read(ctx, d, csz) != csz)
return -1;
d[csz-1] = 0;
txtcb(ctx, t[n].type, "", d);
csz = 0;
break;
}
}
}
if(ctx->seek(ctx, csz, 1) < 0)
return -1;
}
return i > 0 ? 0 : -1;
}

View File

@ -0,0 +1,15 @@
#include "tagspriv.h"
int
tagxm(Tagctx *ctx)
{
char d[17+20+1], *s;
if(ctx->read(ctx, d, 17+20) != 17+20 || memcmp(d, "Extended Module: ", 17) != 0)
return -1;
d[17+20] = 0;
for(s = d+17; *s == ' '; s++);
txtcb(ctx, Ttitle, "", s);
return 0;
}

View File

@ -1,7 +1,7 @@
</$objtype/mkfile
LIBS=libogg libvorbis libFLAC
PROGS=pcmconv oggdec oggenc mp3dec mp3enc flacdec flacenc wavdec sundec mixfs
LIBS=libogg libvorbis libFLAC libtags
PROGS=pcmconv oggdec oggenc mp3dec mp3enc flacdec flacenc wavdec sundec mixfs readtags zuke
#libs must be made first
DIRS=$LIBS $PROGS

View File

@ -0,0 +1,13 @@
</$objtype/mkfile
<../config
TARG=readtags
CFLAGS=$CFLAGS -I../libtags
OFILES=\
readtags.$O\
HFILES=\
../libtags/tags.h\
</sys/src/cmd/mkone

View File

@ -0,0 +1,112 @@
#include <u.h>
#include <libc.h>
#include <tags.h>
typedef struct Aux Aux;
struct Aux
{
int fd;
};
static const char *t2s[] =
{
[Tartist] = "artist",
[Talbum] = "album",
[Ttitle] = "title",
[Tdate] = "date",
[Ttrack] = "track",
[Talbumgain] = "albumgain",
[Talbumpeak] = "albumpeak",
[Ttrackgain] = "trackgain",
[Ttrackpeak] = "trackpeak",
[Tgenre] = "genre",
[Timage] = "image",
};
static void
tag(Tagctx *ctx, int t, const char *k, const char *v, int offset, int size, Tagread f)
{
USED(ctx); USED(k); USED(f);
if(t == Timage)
print("%-12s %s %d %d\n", t2s[t], v, offset, size);
else if(t != Tunknown)
print("%-12s %s\n", t2s[t], v);
}
static void
toc(Tagctx *ctx, int ms, int offset)
{
USED(ctx); USED(ms); USED(offset);
}
static int
ctxread(Tagctx *ctx, void *buf, int cnt)
{
Aux *aux = ctx->aux;
return read(aux->fd, buf, cnt);
}
static int
ctxseek(Tagctx *ctx, int offset, int whence)
{
Aux *aux = ctx->aux;
return seek(aux->fd, offset, whence);
}
static void
usage(void)
{
fprint(2, "usage: %s FILE...\n", argv0);
exits("usage");
}
void
main(int argc, char **argv)
{
int i;
char buf[256];
Aux aux;
Tagctx ctx =
{
.read = ctxread,
.seek = ctxseek,
.tag = tag,
.toc = toc,
.buf = buf,
.bufsz = sizeof(buf),
.aux = &aux,
};
ARGBEGIN{
default:
usage();
}ARGEND
if(argc < 1)
usage();
for(i = 0; i < argc; i++){
print("*** %s\n", argv[i]);
if((aux.fd = open(argv[i], OREAD)) < 0)
print("failed to open\n");
else{
if(tagsget(&ctx) != 0)
print("no tags or failed to read tags\n");
else{
if(ctx.duration > 0)
print("%-12s %d ms\n", "duration", ctx.duration);
if(ctx.samplerate > 0)
print("%-12s %d\n", "samplerate", ctx.samplerate);
if(ctx.channels > 0)
print("%-12s %d\n", "channels", ctx.channels);
if(ctx.bitrate > 0)
print("%-12s %d\n", "bitrate", ctx.bitrate);
}
close(aux.fd);
}
print("\n");
}
exits(nil);
}

View File

@ -0,0 +1,49 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
#include "plist.h"
#include "icy.h"
int
icyfill(Meta *m)
{
char *s, *s0, *e, *p, *path, *d;
int f, n;
path = strdup(m->path);
s = strchr(path, ':')+3;
if((e = strchr(s, '/')) != nil)
*e++ = 0;
if((p = strchr(s, ':')) != nil)
*p = '!';
p = smprint("tcp!%s", s);
free(path);
f = -1;
if((d = netmkaddr(p, "tcp", "80")) != nil)
f = dial(d, nil, nil, nil);
free(p);
if(f < 0)
return -1;
fprint(f, "GET /%s HTTP/0.9\r\nIcy-MetaData: 1\r\n\r\n", e ? e : "");
s0 = malloc(4096);
if((n = readn(f, s0, 4095)) > 0){
s0[n] = 0;
for(s = s0; s = strchr(s, '\n');){
s++;
if(strncmp(s, "icy-name:", 9) == 0 && (e = strchr(s, '\r')) != nil){
*e = 0;
m->artist[0] = strdup(s+9);
m->numartist = 1;
s = e+1;
}else if(strncmp(s, "icy-url:", 8) == 0 && (e = strchr(s, '\r')) != nil){
*e = 0;
m->title = strdup(s+8);
s = e+1;
}
}
}
free(s0);
close(f);
return n > 0 ? 0 : -1;
}

View File

@ -0,0 +1 @@
int icyfill(Meta *m);

View File

@ -0,0 +1,17 @@
</$objtype/mkfile
<../config
TARG=mkplist zuke
HFILES=\
plist.h\
icy.h\
../libtags/tags.h\
default:V: all
$O.mkplist: icy.$O plist.$O mkplist.$O
$O.zuke: plist.$O zuke.$O
</sys/src/cmd/mkmany

View File

@ -0,0 +1,355 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <tags.h>
#include "plist.h"
#include "icy.h"
enum
{
Maxname = 256+2, /* seems enough? */
Maxdepth = 16, /* max recursion depth */
};
#define MAX(a, b) (a > b ? a : b)
static Biobuf *bf, out;
static Meta *curr;
static Meta *all;
static int numall;
static int firstiscomposer;
static int keepfirstartist;
static char *fmts[] =
{
[Fmp3] = "mp3",
[Fogg] = "ogg",
[Fflac] = "flac",
[Fm4a] = "m4a",
[Fopus] = "opus",
[Fwav] = "wav",
[Fit] = "mod",
[Fxm] = "mod",
[Fs3m] = "mod",
};
static Meta *
newmeta(void)
{
if(numall == 0){
free(all);
all = nil;
}
if(all == nil)
all = mallocz(sizeof(Meta), 1);
else if((numall & (numall-1)) == 0)
all = realloc(all, numall*2*sizeof(Meta));
if(all == nil)
return nil;
memset(&all[numall++], 0, sizeof(Meta));
return &all[numall-1];
}
static void
cb(Tagctx *ctx, int t, const char *k, const char *v, int offset, int size, Tagread f)
{
int i, iscomposer;
USED(ctx);
switch(t){
case Tartist:
if(curr->numartist < Maxartist){
iscomposer = strcmp(k, "TCM") == 0 || strcmp(k, "TCOM") == 0;
/* prefer lead performer/soloist, helps when TP2/TPE2 is the first one and is set to "VA" */
/* always put composer first, if available */
if(iscomposer || (!keepfirstartist && (strcmp(k, "TP1") == 0 || strcmp(k, "TPE1") == 0))){
if(curr->numartist > 0)
curr->artist[curr->numartist] = curr->artist[curr->numartist-1];
curr->artist[0] = strdup(v);
curr->numartist++;
keepfirstartist = 1;
firstiscomposer = iscomposer;
return;
}
for(i = 0; i < curr->numartist; i++){
if(cistrcmp(curr->artist[i], v) == 0)
return;
}
curr->artist[curr->numartist++] = strdup(v);
}
break;
case Talbum:
if(curr->album == nil)
curr->album = strdup(v);
break;
case Ttitle:
if(curr->title == nil)
curr->title = strdup(v);
break;
case Tdate:
if(curr->date == nil)
curr->date = strdup(v);
break;
case Ttrack:
if(curr->track == nil)
curr->track = strdup(v);
break;
case Timage:
if(curr->imagefmt == nil){
curr->imagefmt = strdup(v);
curr->imageoffset = offset;
curr->imagesize = size;
curr->imagereader = f != nil;
}
break;
}
}
static int
ctxread(Tagctx *ctx, void *buf, int cnt)
{
USED(ctx);
return Bread(bf, buf, cnt);
}
static int
ctxseek(Tagctx *ctx, int offset, int whence)
{
USED(ctx);
return Bseek(bf, offset, whence);
}
static char buf[4096];
static Tagctx ctx =
{
.read = ctxread,
.seek = ctxseek,
.tag = cb,
.buf = buf,
.bufsz = sizeof(buf),
.aux = nil,
};
static uvlong
modduration(char *path)
{
static int moddec = -1;
int f, pid, p[2], n;
char t[1024], *s;
if(moddec < 0)
moddec = close(open("/bin/audio/moddec", OEXEC)) == 0;
if(!moddec)
return 0;
pipe(p);
if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){
dup(f = open(path, OREAD), 0); close(f);
close(1);
dup(p[1], 2); close(p[1]);
close(p[0]);
execl("/bin/audio/moddec", "moddec", "-r", "0", nil);
sysfatal("execl: %r");
}
close(p[1]);
n = pid > 0 ? readn(p[0], t, sizeof(t)-1) : -1;
close(p[0]);
if(n > 0){
t[n] = 0;
for(s = t; s != nil; s = strchr(s+1, '\n')){
if(*s == '\n')
s++;
if(strncmp(s, "duration: ", 10) == 0)
return strtod(s+10, nil)*1000.0;
}
}
return 0;
}
static void
scanfile(char *path)
{
int res;
char *s;
if((bf = Bopen(path, OREAD)) == nil){
fprint(2, "%s: %r\n", path);
return;
}
if((curr = newmeta()) == nil)
sysfatal("no memory");
firstiscomposer = keepfirstartist = 0;
res = tagsget(&ctx);
if(ctx.format != Funknown){
if(res != 0)
fprint(2, "%s: no tags\n", path);
}else{
numall--;
Bterm(bf);
return;
}
if(ctx.duration == 0){
if(ctx.format == Fit || ctx.format == Fxm || ctx.format == Fs3m)
ctx.duration = modduration(path);
if(ctx.duration == 0)
fprint(2, "%s: no duration\n", path);
}
if(curr->title == nil){
if((s = utfrrune(path, '/')) == nil)
s = path;
curr->title = strdup(s+1);
}
curr->path = strdup(path);
curr->duration = ctx.duration;
if(ctx.format >= nelem(fmts))
sysfatal("mkplist needs a rebuild with updated libtags");
curr->filefmt = fmts[ctx.format];
Bterm(bf);
}
static int
scan(char **dir, int depth)
{
char *path;
Dir *buf, *d;
long n;
int dirfd, len;
if((dirfd = open(*dir, OREAD)) < 0)
sysfatal("%s: %r", *dir);
len = strlen(*dir);
if((*dir = realloc(*dir, len+1+Maxname)) == nil)
sysfatal("no memory");
path = *dir;
path[len] = '/';
for(n = 0, buf = nil; n >= 0;){
if((n = dirread(dirfd, &buf)) < 0){
path[len] = 0;
scanfile(path);
break;
}
if(n == 0){
free(buf);
break;
}
for(d = buf; n > 0; n--, d++){
if(strcmp(d->name, ".") == 0 || strcmp(d->name, "..") == 0)
continue;
path[len+1+Maxname-2] = 0;
strncpy(&path[len+1], d->name, Maxname);
if(path[len+1+Maxname-2] != 0)
sysfatal("Maxname=%d was a bad choice", Maxname);
if((d->mode & DMDIR) == 0){
scanfile(path);
}else if(depth < Maxdepth){ /* recurse into the directory */
scan(dir, depth+1);
path = *dir;
}else{
fprint(2, "%s: too deep\n", path);
}
}
free(buf);
}
close(dirfd);
return 0;
}
static int
cmpmeta(void *a_, void *b_)
{
Meta *a, *b;
char *ae, *be;
int i, x;
a = a_;
b = b_;
ae = utfrrune(a->path, '/');
be = utfrrune(b->path, '/');
if(ae != nil && be != nil && (x = cistrncmp(a->path, b->path, MAX(ae-a->path, be-b->path))) != 0) /* different path */
return x;
/* same path, must be the same album/cd, but first check */
for(i = 0; i < a->numartist && i < b->numartist; i++){
if((x = cistrcmp(a->artist[i], b->artist[i])) != 0){
if(a->album != nil && b->album != nil && cistrcmp(a->album, b->album) != 0)
return x;
}
}
if(a->date != nil || b->date != nil){
if(a->date == nil && b->date != nil) return -1;
if(a->date != nil && b->date == nil) return 1;
if((x = atoi(a->date) - atoi(b->date)) != 0) return x;
}else if(a->album != nil || b->album != nil){
if(a->album == nil && b->album != nil) return -1;
if(a->album != nil && b->album == nil) return 1;
if((x = cistrcmp(a->album, b->album)) != 0) return x;
}
if(a->track != nil || b->track != nil){
if(a->track == nil && b->track != nil) return -1;
if(a->track != nil && b->track == nil) return 1;
if((x = atoi(a->track) - atoi(b->track)) != 0) return x;
}
return cistrcmp(a->path, b->path);
}
void
main(int argc, char **argv)
{
char *dir, wd[4096];
int i;
if(argc < 2){
fprint(2, "usage: mkplist DIR [DIR2 ...] > noise.plist\n");
exits("usage");
}
getwd(wd, sizeof(wd));
Binit(&out, 1, OWRITE);
for(i = 1; i < argc; i++){
if(strncmp(argv[i], "http://", 7) == 0 || strncmp(argv[i], "https://", 8) == 0){
if((curr = newmeta()) == nil)
sysfatal("no memory");
curr->title = argv[i];
curr->path = argv[i];
curr->filefmt = "";
if(icyfill(curr) != 0)
fprint(2, "%s: %r\n", argv[i]);
}else{
if(argv[i][0] == '/')
dir = strdup(argv[i]);
else
dir = smprint("%s/%s", wd, argv[i]);
cleanname(dir);
scan(&dir, 0);
}
}
qsort(all, numall, sizeof(Meta), cmpmeta);
for(i = 0; i < numall; i++){
if(all[i].numartist < 1)
fprint(2, "no artists: %s\n", all[i].path);
if(all[i].title == nil)
fprint(2, "no title: %s\n", all[i].path);
printmeta(&out, all+i);
}
Bterm(&out);
fprint(2, "found %d tagged tracks\n", numall);
exits(nil);
}

View File

@ -0,0 +1,27 @@
#include <u.h>
#include <libc.h>
#include <bio.h>
#include "plist.h"
void
printmeta(Biobuf *b, Meta *m)
{
int i;
Bprint(b, "%c %s\n%c %s\n", Ppath, m->path, Pfilefmt, m->filefmt);
for(i = 0; i < m->numartist; i++)
Bprint(b, "%c %s\n", Partist, m->artist[i]);
if(m->album != nil)
Bprint(b, "%c %s\n", Palbum, m->album);
if(m->title != nil)
Bprint(b, "%c %s\n", Ptitle, m->title);
if(m->date != nil)
Bprint(b, "%c %s\n", Pdate, m->date);
if(m->track != nil)
Bprint(b, "%c %s\n", Ptrack, m->track);
if(m->duration > 0)
Bprint(b, "%c %llud\n", Pduration, m->duration);
if(m->imagesize > 0)
Bprint(b, "%c %d %d %d %s\n", Pimage, m->imageoffset, m->imagesize, m->imagereader, m->imagefmt);
Bprint(b, "\n");
}

View File

@ -0,0 +1,52 @@
/* Playlist begins with "# x\n" where x is the total number of records.
* Each record begins with "# x y\n" where x is record index, y is its size in bytes.
* Records are sorted according to mkplist.c:/^cmpmeta function.
* This makes it somewhat easy to just load the whole playlist into memory once,
* map all (Meta*)->... fields to it, saving on memory allocations, and using the same
* data to provide poor's man full text searching.
* Encoding: mkplist.c:/^printmeta/.
* Decoding: zuke.c:/^readplist/.
*/
enum
{
Precord='#',
Palbum= 'a',
Partist= 'A',
Pbasename= 'b',
Pdate= 'd',
Pduration= 'D',
Pimage= 'i',
Ptitle= 't',
Ptrack= 'T',
Ppath= 'p',
Pfilefmt= 'f',
/* unused */
Pchannels= 'c',
Psamplerate= 's',
Maxartist=16, /* max artists for a track */
};
typedef struct Meta Meta;
struct Meta
{
char *artist[Maxartist];
char *album;
char *title;
char *date;
char *track;
char *path;
char *basename;
char *imagefmt;
char *filefmt;
uvlong duration;
int numartist;
int imageoffset;
int imagesize;
int imagereader; /* non-zero if a special reader required */
};
void printmeta(Biobuf *b, Meta *m);

File diff suppressed because it is too large Load Diff