mirror of
git://git.9front.org/plan9front/plan9front
synced 2025-01-12 11:10:06 +00:00
audio/: zuke, mkplist, readtags
This commit is contained in:
parent
b777d3fe7d
commit
c6cdee420d
27 changed files with 3474 additions and 2 deletions
29
sys/src/cmd/audio/libtags/8859.c
Normal file
29
sys/src/cmd/audio/libtags/8859.c
Normal 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;
|
||||
}
|
108
sys/src/cmd/audio/libtags/flac.c
Normal file
108
sys/src/cmd/audio/libtags/flac.c
Normal 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;
|
||||
}
|
42
sys/src/cmd/audio/libtags/id3genres.c
Normal file
42
sys/src/cmd/audio/libtags/id3genres.c
Normal 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",
|
||||
};
|
48
sys/src/cmd/audio/libtags/id3v1.c
Normal file
48
sys/src/cmd/audio/libtags/id3v1.c
Normal 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;
|
||||
}
|
471
sys/src/cmd/audio/libtags/id3v2.c
Normal file
471
sys/src/cmd/audio/libtags/id3v2.c
Normal 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;
|
||||
}
|
14
sys/src/cmd/audio/libtags/it.c
Normal file
14
sys/src/cmd/audio/libtags/it.c
Normal 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;
|
||||
}
|
154
sys/src/cmd/audio/libtags/m4a.c
Normal file
154
sys/src/cmd/audio/libtags/m4a.c
Normal 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;
|
||||
}
|
24
sys/src/cmd/audio/libtags/mkfile
Normal file
24
sys/src/cmd/audio/libtags/mkfile
Normal 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
|
91
sys/src/cmd/audio/libtags/opus.c
Normal file
91
sys/src/cmd/audio/libtags/opus.c
Normal 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;
|
||||
}
|
16
sys/src/cmd/audio/libtags/s3m.c
Normal file
16
sys/src/cmd/audio/libtags/s3m.c
Normal 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;
|
||||
}
|
66
sys/src/cmd/audio/libtags/tags.c
Normal file
66
sys/src/cmd/audio/libtags/tags.c
Normal 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;
|
||||
}
|
86
sys/src/cmd/audio/libtags/tags.h
Normal file
86
sys/src/cmd/audio/libtags/tags.h
Normal 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);
|
39
sys/src/cmd/audio/libtags/tagspriv.h
Normal file
39
sys/src/cmd/audio/libtags/tagspriv.h
Normal 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)
|
59
sys/src/cmd/audio/libtags/utf16.c
Normal file
59
sys/src/cmd/audio/libtags/utf16.c
Normal 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;
|
||||
}
|
125
sys/src/cmd/audio/libtags/vorbis.c
Normal file
125
sys/src/cmd/audio/libtags/vorbis.c
Normal 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;
|
||||
}
|
87
sys/src/cmd/audio/libtags/wav.c
Normal file
87
sys/src/cmd/audio/libtags/wav.c
Normal 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;
|
||||
}
|
15
sys/src/cmd/audio/libtags/xm.c
Normal file
15
sys/src/cmd/audio/libtags/xm.c
Normal 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;
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
13
sys/src/cmd/audio/readtags/mkfile
Normal file
13
sys/src/cmd/audio/readtags/mkfile
Normal 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
|
112
sys/src/cmd/audio/readtags/readtags.c
Normal file
112
sys/src/cmd/audio/readtags/readtags.c
Normal 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);
|
||||
}
|
49
sys/src/cmd/audio/zuke/icy.c
Normal file
49
sys/src/cmd/audio/zuke/icy.c
Normal 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;
|
||||
}
|
1
sys/src/cmd/audio/zuke/icy.h
Normal file
1
sys/src/cmd/audio/zuke/icy.h
Normal file
|
@ -0,0 +1 @@
|
|||
int icyfill(Meta *m);
|
17
sys/src/cmd/audio/zuke/mkfile
Normal file
17
sys/src/cmd/audio/zuke/mkfile
Normal 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
|
355
sys/src/cmd/audio/zuke/mkplist.c
Normal file
355
sys/src/cmd/audio/zuke/mkplist.c
Normal 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);
|
||||
}
|
27
sys/src/cmd/audio/zuke/plist.c
Normal file
27
sys/src/cmd/audio/zuke/plist.c
Normal 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");
|
||||
}
|
52
sys/src/cmd/audio/zuke/plist.h
Normal file
52
sys/src/cmd/audio/zuke/plist.h
Normal 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);
|
1372
sys/src/cmd/audio/zuke/zuke.c
Normal file
1372
sys/src/cmd/audio/zuke/zuke.c
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue