From c6cdee420da18fc02c44c257505f3c43a331c7a4 Mon Sep 17 00:00:00 2001 From: Sigrid Date: Tue, 13 Apr 2021 13:20:27 +0200 Subject: [PATCH] audio/: zuke, mkplist, readtags --- sys/src/cmd/audio/libtags/8859.c | 29 + sys/src/cmd/audio/libtags/flac.c | 108 ++ sys/src/cmd/audio/libtags/id3genres.c | 42 + sys/src/cmd/audio/libtags/id3v1.c | 48 + sys/src/cmd/audio/libtags/id3v2.c | 471 +++++++++ sys/src/cmd/audio/libtags/it.c | 14 + sys/src/cmd/audio/libtags/m4a.c | 154 +++ sys/src/cmd/audio/libtags/mkfile | 24 + sys/src/cmd/audio/libtags/opus.c | 91 ++ sys/src/cmd/audio/libtags/s3m.c | 16 + sys/src/cmd/audio/libtags/tags.c | 66 ++ sys/src/cmd/audio/libtags/tags.h | 86 ++ sys/src/cmd/audio/libtags/tagspriv.h | 39 + sys/src/cmd/audio/libtags/utf16.c | 59 ++ sys/src/cmd/audio/libtags/vorbis.c | 125 +++ sys/src/cmd/audio/libtags/wav.c | 87 ++ sys/src/cmd/audio/libtags/xm.c | 15 + sys/src/cmd/audio/mkfile | 4 +- sys/src/cmd/audio/readtags/mkfile | 13 + sys/src/cmd/audio/readtags/readtags.c | 112 ++ sys/src/cmd/audio/zuke/icy.c | 49 + sys/src/cmd/audio/zuke/icy.h | 1 + sys/src/cmd/audio/zuke/mkfile | 17 + sys/src/cmd/audio/zuke/mkplist.c | 355 +++++++ sys/src/cmd/audio/zuke/plist.c | 27 + sys/src/cmd/audio/zuke/plist.h | 52 + sys/src/cmd/audio/zuke/zuke.c | 1372 +++++++++++++++++++++++++ 27 files changed, 3474 insertions(+), 2 deletions(-) create mode 100644 sys/src/cmd/audio/libtags/8859.c create mode 100644 sys/src/cmd/audio/libtags/flac.c create mode 100644 sys/src/cmd/audio/libtags/id3genres.c create mode 100644 sys/src/cmd/audio/libtags/id3v1.c create mode 100644 sys/src/cmd/audio/libtags/id3v2.c create mode 100644 sys/src/cmd/audio/libtags/it.c create mode 100644 sys/src/cmd/audio/libtags/m4a.c create mode 100644 sys/src/cmd/audio/libtags/mkfile create mode 100644 sys/src/cmd/audio/libtags/opus.c create mode 100644 sys/src/cmd/audio/libtags/s3m.c create mode 100644 sys/src/cmd/audio/libtags/tags.c create mode 100644 sys/src/cmd/audio/libtags/tags.h create mode 100644 sys/src/cmd/audio/libtags/tagspriv.h create mode 100644 sys/src/cmd/audio/libtags/utf16.c create mode 100644 sys/src/cmd/audio/libtags/vorbis.c create mode 100644 sys/src/cmd/audio/libtags/wav.c create mode 100644 sys/src/cmd/audio/libtags/xm.c create mode 100644 sys/src/cmd/audio/readtags/mkfile create mode 100644 sys/src/cmd/audio/readtags/readtags.c create mode 100644 sys/src/cmd/audio/zuke/icy.c create mode 100644 sys/src/cmd/audio/zuke/icy.h create mode 100644 sys/src/cmd/audio/zuke/mkfile create mode 100644 sys/src/cmd/audio/zuke/mkplist.c create mode 100644 sys/src/cmd/audio/zuke/plist.c create mode 100644 sys/src/cmd/audio/zuke/plist.h create mode 100644 sys/src/cmd/audio/zuke/zuke.c diff --git a/sys/src/cmd/audio/libtags/8859.c b/sys/src/cmd/audio/libtags/8859.c new file mode 100644 index 000000000..9efd9026a --- /dev/null +++ b/sys/src/cmd/audio/libtags/8859.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/flac.c b/sys/src/cmd/audio/libtags/flac.c new file mode 100644 index 000000000..f3a215753 --- /dev/null +++ b/sys/src/cmd/audio/libtags/flac.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/id3genres.c b/sys/src/cmd/audio/libtags/id3genres.c new file mode 100644 index 000000000..f1d6f7dc9 --- /dev/null +++ b/sys/src/cmd/audio/libtags/id3genres.c @@ -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", +}; diff --git a/sys/src/cmd/audio/libtags/id3v1.c b/sys/src/cmd/audio/libtags/id3v1.c new file mode 100644 index 000000000..afcf90e94 --- /dev/null +++ b/sys/src/cmd/audio/libtags/id3v1.c @@ -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< 0) + txtcb(ctx, Ttitle, "", out); + if((ctx->found & 1< 0) + txtcb(ctx, Tartist, "", out); + if((ctx->found & 1< 0) + txtcb(ctx, Talbum, "", out); + + in[93+4] = 0; + if((ctx->found & 1<found & 1< 0){ + snprint((char*)out, Outsz, "%d", in[126]); + txtcb(ctx, Ttrack, "", out); + } + + if((ctx->found & 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; +} diff --git a/sys/src/cmd/audio/libtags/it.c b/sys/src/cmd/audio/libtags/it.c new file mode 100644 index 000000000..c62a83be8 --- /dev/null +++ b/sys/src/cmd/audio/libtags/it.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/m4a.c b/sys/src/cmd/audio/libtags/m4a.c new file mode 100644 index 000000000..b1e0fda30 --- /dev/null +++ b/sys/src/cmd/audio/libtags/m4a.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/mkfile b/sys/src/cmd/audio/libtags/mkfile new file mode 100644 index 000000000..fbf4a6004 --- /dev/null +++ b/sys/src/cmd/audio/libtags/mkfile @@ -0,0 +1,24 @@ +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; +} diff --git a/sys/src/cmd/audio/libtags/s3m.c b/sys/src/cmd/audio/libtags/s3m.c new file mode 100644 index 000000000..126c140e0 --- /dev/null +++ b/sys/src/cmd/audio/libtags/s3m.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/tags.c b/sys/src/cmd/audio/libtags/tags.c new file mode 100644 index 000000000..da8121609 --- /dev/null +++ b/sys/src/cmd/audio/libtags/tags.c @@ -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<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; +} diff --git a/sys/src/cmd/audio/libtags/tags.h b/sys/src/cmd/audio/libtags/tags.h new file mode 100644 index 000000000..b8ed7c5f1 --- /dev/null +++ b/sys/src/cmd/audio/libtags/tags.h @@ -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); diff --git a/sys/src/cmd/audio/libtags/tagspriv.h b/sys/src/cmd/audio/libtags/tagspriv.h new file mode 100644 index 000000000..cbead94fc --- /dev/null +++ b/sys/src/cmd/audio/libtags/tagspriv.h @@ -0,0 +1,39 @@ +#include +#include +#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) diff --git a/sys/src/cmd/audio/libtags/utf16.c b/sys/src/cmd/audio/libtags/utf16.c new file mode 100644 index 000000000..285430363 --- /dev/null +++ b/sys/src/cmd/audio/libtags/utf16.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/vorbis.c b/sys/src/cmd/audio/libtags/vorbis.c new file mode 100644 index 000000000..2c00d085b --- /dev/null +++ b/sys/src/cmd/audio/libtags/vorbis.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/wav.c b/sys/src/cmd/audio/libtags/wav.c new file mode 100644 index 000000000..a016a1224 --- /dev/null +++ b/sys/src/cmd/audio/libtags/wav.c @@ -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; +} diff --git a/sys/src/cmd/audio/libtags/xm.c b/sys/src/cmd/audio/libtags/xm.c new file mode 100644 index 000000000..d181fbc00 --- /dev/null +++ b/sys/src/cmd/audio/libtags/xm.c @@ -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; +} diff --git a/sys/src/cmd/audio/mkfile b/sys/src/cmd/audio/mkfile index 0a9317aac..941a094a8 100644 --- a/sys/src/cmd/audio/mkfile +++ b/sys/src/cmd/audio/mkfile @@ -1,7 +1,7 @@ +#include +#include + +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); +} diff --git a/sys/src/cmd/audio/zuke/icy.c b/sys/src/cmd/audio/zuke/icy.c new file mode 100644 index 000000000..80c51ea0e --- /dev/null +++ b/sys/src/cmd/audio/zuke/icy.c @@ -0,0 +1,49 @@ +#include +#include +#include +#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; +} diff --git a/sys/src/cmd/audio/zuke/icy.h b/sys/src/cmd/audio/zuke/icy.h new file mode 100644 index 000000000..326851cc3 --- /dev/null +++ b/sys/src/cmd/audio/zuke/icy.h @@ -0,0 +1 @@ +int icyfill(Meta *m); diff --git a/sys/src/cmd/audio/zuke/mkfile b/sys/src/cmd/audio/zuke/mkfile new file mode 100644 index 000000000..95a085390 --- /dev/null +++ b/sys/src/cmd/audio/zuke/mkfile @@ -0,0 +1,17 @@ + +#include +#include +#include +#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); +} diff --git a/sys/src/cmd/audio/zuke/plist.c b/sys/src/cmd/audio/zuke/plist.c new file mode 100644 index 000000000..f9c6ffeec --- /dev/null +++ b/sys/src/cmd/audio/zuke/plist.c @@ -0,0 +1,27 @@ +#include +#include +#include +#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"); +} diff --git a/sys/src/cmd/audio/zuke/plist.h b/sys/src/cmd/audio/zuke/plist.h new file mode 100644 index 000000000..563d5a03f --- /dev/null +++ b/sys/src/cmd/audio/zuke/plist.h @@ -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); diff --git a/sys/src/cmd/audio/zuke/zuke.c b/sys/src/cmd/audio/zuke/zuke.c new file mode 100644 index 000000000..665a4d7f1 --- /dev/null +++ b/sys/src/cmd/audio/zuke/zuke.c @@ -0,0 +1,1372 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "plist.h" + +#define MAX(a,b) ((a)>=(b)?(a):(b)) +#define MIN(a,b) ((a)<=(b)?(a):(b)) +#define CLAMP(x,min,max) MAX(min, MIN(max, x)) + +typedef struct Color Color; +typedef struct Player Player; +typedef struct Playlist Playlist; + +enum +{ + Cstart = 1, + Cstop, + Ctoggle, + Cseekrel, + + Everror = 1, + Evready, + + Seek = 10, /* 10 seconds */ + Seekfast = 60, /* a minute */ + + Bps = 44100*2*2, /* 44100KHz, stereo, s16 for a sample */ + Relbufsz = Bps/2, /* 0.5s */ + + Dback = 0, + Dfhigh, + Dfmed, + Dflow, + Dfinv, + Dbmed, + Dblow, + Dbinv, + Numcolors, +}; + +struct Color { + u32int rgb; + Image *im; +}; + +struct Player +{ + Channel *ctl; + Channel *ev; + Channel *img; + double seek; + int pcur; +}; + +struct Playlist +{ + Meta *m; + int n; + char *raw; + int rawsz; +}; + +int mainstacksize = 32768; + +static int debug; +static int audio = -1; +static int volume; +static int pnotifies; +static Playlist *pl; +static Player *playernext; +static Player *playercurr; +static vlong byteswritten; +static int pcur, pcurplaying; +static int scroll, scrollsz; +static Font *f; +static Image *cover; +static Channel *playc; +static Mousectl *mctl; +static Keyboardctl *kctl; +static int colwidth[10]; +static int mincolwidth[10]; +static char *cols = "AatD"; +static int colspath; +static int *shuffle; +static Rectangle seekbar; +static int seekmx, newseekmx = -1; +static double seekoff; /* ms */ +static Lock audiolock; +static int audioerr = 0; +static Biobuf out; +static char *covers[] = +{ + "art", "folder", "cover", "Cover", "scans/CD", "Scans/Front", "Covers/Front" +}; + +static Color colors[Numcolors] = +{ + [Dback] = {0xf0f0f0}, + [Dfhigh] = {0xffffff}, + [Dfmed] = {0x343434}, + [Dflow] = {0xa5a5a5}, + [Dfinv] = {0x323232}, + [Dbmed] = {0x72dec2}, + [Dblow] = {0x404040}, + [Dbinv] = {0xffb545}, +}; + +static int Scrollwidth; +static int Scrollheight; +static int Coversz; + +static void +audioon(void) +{ + lock(&audiolock); + if(audio < 0 && (audio = open("/dev/audio", OWRITE|OCEXEC)) < 0 && audioerr == 0){ + fprint(2, "%r\n"); + audioerr = 1; + } + unlock(&audiolock); +} + +static void +audiooff(void) +{ + lock(&audiolock); + close(audio); + audio = -1; + audioerr = 0; + unlock(&audiolock); +} + +#pragma varargck type "P" uvlong +static int +positionfmt(Fmt *f) +{ + char *s, tmp[16]; + u64int sec; + + s = tmp; + sec = va_arg(f->args, int); + if(sec >= 3600){ + s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/3600); + sec %= 3600; + } + s = seprint(s, tmp+sizeof(tmp), "%02lld:", sec/60); + sec %= 60; + seprint(s, tmp+sizeof(tmp), "%02lld", sec); + + return fmtstrcpy(f, tmp); +} + +static char * +getcol(Meta *m, int c) +{ + static char tmp[32]; + + switch(c){ + case Palbum: return m->album; + case Partist: return m->artist[0]; + case Pdate: return m->date; + case Ptitle: return (!colspath && *m->title == 0) ? m->basename : m->title; + case Ptrack: snprint(tmp, sizeof(tmp), "%4s", m->track); return m->track ? tmp : nil; + case Ppath: return m->path; + case Pduration: + tmp[0] = 0; + if(m->duration > 0) + snprint(tmp, sizeof(tmp), "%8P", m->duration/1000); + return tmp; + default: sysfatal("invalid column '%c'", c); + } + + return nil; +} + +static void +adjustcolumns(void) +{ + int i, n, x, total, width; + + if(mincolwidth[0] == 0){ + for(i = 0; cols[i] != 0; i++) + mincolwidth[i] = 1; + for(n = 0; n < pl->n; n++){ + for(i = 0; cols[i] != 0; i++){ + if((x = stringwidth(f, getcol(pl->m+n, cols[i]))) > mincolwidth[i]) + mincolwidth[i] = x; + } + } + } + + total = 0; + n = 0; + width = Dx(screen->r); + for(i = 0; cols[i] != 0; i++){ + if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack) + width -= mincolwidth[i] + 8; + else{ + total += mincolwidth[i]; + n++; + } + } + colspath = 0; + for(i = 0; cols[i] != 0; i++){ + if(cols[i] == Ppath || cols[i] == Pbasename) + colspath = 1; + if(cols[i] == Pduration || cols[i] == Pdate || cols[i] == Ptrack) + colwidth[i] = mincolwidth[i]; + else + colwidth[i] = (width - Scrollwidth - n*8) * mincolwidth[i] / total; + } +} + +static Meta * +getmeta(int i) +{ + return &pl->m[shuffle != nil ? shuffle[i] : i]; +} + +static void +updatescrollsz(void) +{ + scrollsz = Dy(screen->r)/f->height - 2; +} + +static void +redraw(int full) +{ + Image *col; + Point p, sp; + Rectangle sel, r; + int i, j, left, right, scrollcenter, w; + uvlong dur, msec; + char tmp[32]; + + lockdisplay(display); + updatescrollsz(); + scroll = CLAMP(scroll, 0, pl->n - scrollsz); + left = screen->r.min.x; + if(scrollsz < pl->n) /* adjust for scrollbar */ + left += Scrollwidth + 1; + + if(full){ + draw(screen, screen->r, colors[Dback].im, nil, ZP); + + adjustcolumns(); + if(scrollsz < pl->n){ /* scrollbar */ + p.x = sp.x = screen->r.min.x + Scrollwidth; + p.y = screen->r.min.y; + sp.y = screen->r.max.y; + line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP); + + r = screen->r; + r.max.x = r.min.x + Scrollwidth - 1; + r.min.x += 1; + if(scroll < 1) + scrollcenter = 0; + else + scrollcenter = (Dy(screen->r)-Scrollheight*5/4)*scroll / (pl->n - scrollsz); + r.min.y += scrollcenter + Scrollheight/4; + r.max.y = r.min.y + Scrollheight; + draw(screen, r, colors[Dblow].im, nil, ZP); + } + + p.x = sp.x = left; + p.y = 0; + sp.y = screen->r.max.y; + for(i = 0; cols[i+1] != 0; i++){ + p.x += colwidth[i] + 4; + sp.x = p.x; + line(screen, p, sp, Endsquare, Endsquare, 0, colors[Dflow].im, ZP); + p.x += 4; + } + + sp.x = sp.y = 0; + p.x = left + 2; + p.y = screen->r.min.y + 2; + + for(i = scroll; i < pl->n; i++, p.y += f->height){ + if(i < 0) + continue; + if(p.y > screen->r.max.y) + break; + + if(pcur == i){ + sel.min.x = left; + sel.min.y = p.y; + sel.max.x = screen->r.max.x; + sel.max.y = p.y + f->height; + draw(screen, sel, colors[Dbinv].im, nil, ZP); + col = colors[Dfinv].im; + }else{ + col = colors[Dfmed].im; + } + + sel = screen->r; + + p.x = left + 2 + 3; + for(j = 0; cols[j] != 0; j++){ + sel.max.x = p.x + colwidth[j]; + replclipr(screen, 0, sel); + string(screen, p, col, sp, f, getcol(getmeta(i), cols[j])); + p.x += colwidth[j] + 8; + } + replclipr(screen, 0, screen->r); + + if(pcurplaying == i){ + Point rightp, leftp; + leftp.y = rightp.y = p.y - 1; + leftp.x = left; + rightp.x = screen->r.max.x; + line(screen, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp); + leftp.y = rightp.y = p.y + f->height; + line(screen, leftp, rightp, 0, 0, 0, colors[Dflow].im, sp); + } + } + } + + msec = 0; + dur = getmeta(pcurplaying)->duration; + if(pcurplaying >= 0){ + msec = byteswritten*1000/Bps; + if(dur > 0){ + snprint(tmp, sizeof(tmp), "%s%P/%P 100%%", + shuffle != nil ? "∫ " : "", + dur/1000, dur/1000); + w = stringwidth(f, tmp); + msec = MIN(msec, dur); + snprint(tmp, sizeof(tmp), "%s%P/%P %d%%", + shuffle != nil ? "∫ " : "", + (uvlong)(newseekmx >= 0 ? seekoff : msec)/1000, + dur/1000, volume); + }else{ + snprint(tmp, sizeof(tmp), "%s%P %d%%", + shuffle != nil ? "∫ " : "", + msec/1000, 100); + w = stringwidth(f, tmp); + snprint(tmp, sizeof(tmp), "%s%P %d%%", + shuffle != nil ? "∫ " : "", + msec/1000, volume); + } + }else{ + snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", 100); + w = stringwidth(f, tmp); + snprint(tmp, sizeof(tmp), "%s%d%%", shuffle != nil ? "∫ " : "", volume); + } + r = screen->r; + right = r.max.x - w - 4; + r.min.x = left; + r.min.y = r.max.y - f->height - 4; + if(pcurplaying < 0 || dur == 0) + r.min.x = right; + draw(screen, r, colors[Dblow].im, nil, ZP); + p = addpt(Pt(r.max.x-stringwidth(f, tmp)-4, r.min.y), Pt(2, 2)); + r.max.x = right; + string(screen, p, colors[Dfhigh].im, sp, f, tmp); + sel = r; + + if(cover != nil && full){ + r.max.x = r.min.x; + r.min.x = screen->r.max.x - cover->r.max.x - 8; + draw(screen, r, colors[Dblow].im, nil, ZP); + r = screen->r; + r.min.x = r.max.x - cover->r.max.x - 8; + r.min.y = r.max.y - cover->r.max.y - 8 - f->height - 4; + r.max.y = r.min.y + cover->r.max.y + 8; + draw(screen, r, colors[Dblow].im, nil, ZP); + draw(screen, insetrect(r, 4), cover, nil, ZP); + } + + /* seek bar */ + seekbar = ZR; + if(pcurplaying >= 0 && dur > 0){ + r = insetrect(sel, 3); + draw(screen, r, colors[Dback].im, nil, ZP); + seekbar = r; + r.max.x = r.min.x + Dx(r) * (double)msec / (double)dur; + draw(screen, r, colors[Dbmed].im, nil, ZP); + } + + flushimage(display, 1); + unlockdisplay(display); +} + +static void +coverload(void *player_) +{ + int p[2], pid, fd, i; + char *prog, *path, *s, tmp[32]; + Meta *m; + Channel *ch; + Player *player; + Image *newcover; + + threadsetname("cover"); + player = player_; + m = getmeta(player->pcur); + pid = -1; + ch = player->img; + fd = -1; + prog = nil; + + if(m->imagefmt != nil && m->imagereader == 0){ + if(strcmp(m->imagefmt, "image/png") == 0) + prog = "png"; + else if(strcmp(m->imagefmt, "image/jpeg") == 0) + prog = "jpg"; + } + + if(prog == nil){ + path = strdup(m->path); + if(path != nil && (s = utfrrune(path, '/')) != nil){ + *s = 0; + + for(i = 0; i < nelem(covers) && prog == nil; i++){ + if((s = smprint("%s/%s.jpg", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0) + prog = "jpg"; + free(s); + s = nil; + if(fd < 0 && (s = smprint("%s/%s.png", path, covers[i])) != nil && (fd = open(s, OREAD)) >= 0) + prog = "png"; + free(s); + } + } + free(path); + } + + if(prog == nil) + goto done; + + if(fd < 0){ + fd = open(m->path, OREAD); + seek(fd, m->imageoffset, 0); + } + pipe(p); + if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){ + dup(fd, 0); close(fd); + dup(p[1], 1); close(p[1]); + if(!debug){ + dup(fd = open("/dev/null", OWRITE), 2); + close(fd); + } + snprint(tmp, sizeof(tmp), "%s -9t | resample -x%d", prog, Coversz); + execl("/bin/rc", "rc", "-c", tmp, nil); + sysfatal("execl: %r"); + } + close(fd); + close(p[1]); + + if(pid > 0){ + newcover = readimage(display, p[0], 1); + sendp(ch, newcover); + } + close(p[0]); +done: + if(pid < 0) + sendp(ch, nil); + chanclose(ch); + chanfree(ch); + if(pid >= 0) + postnote(PNGROUP, pid, "interrupt"); + threadexits(nil); +} + +static int +playerret(Player *player) +{ + return recvul(player->ev) == Everror ? -1 : 0; +} + +static void +pnotify(Player *p) +{ + Meta *m; + char *s; + int i; + + if(!pnotifies) + return; + + if(p != nil){ + m = getmeta(p->pcur); + for(i = 0; cols[i] != 0; i++) + Bprint(&out, "%s\t", (s = getcol(m, cols[i])) ? s : ""); + } + Bprint(&out, "\n"); + Bflush(&out); +} + +static void +stop(Player *player) +{ + if(player == nil) + return; + + if(player == playernext) + playernext = nil; + if(!getmeta(player->pcur)->filefmt[0]) + playerret(player); + if(player == playercurr) + pnotify(nil); + sendul(player->ctl, Cstop); +} + +static void +start(Player *player) +{ + if(player == nil) + return; + if(!getmeta(player->pcur)->filefmt[0]) + playerret(player); + pnotify(player); + sendul(player->ctl, Cstart); +} + +static void playerthread(void *player_); + +static Player * +newplayer(int pcur, int loadnext) +{ + Player *player; + + if(playernext != nil && loadnext){ + if(pcur == playernext->pcur){ + player = playernext; + playernext = nil; + goto done; + } + stop(playernext); + playernext = nil; + } + + player = mallocz(sizeof(*player), 1); + player->ctl = chancreate(sizeof(ulong), 0); + player->ev = chancreate(sizeof(ulong), 0); + player->pcur = pcur; + + threadcreate(playerthread, player, 4096); + if(getmeta(pcur)->filefmt[0] && playerret(player) < 0) + return nil; + +done: + if(pcur < pl->n-1 && playernext == nil && loadnext) + playernext = newplayer(pcur+1, 0); + + return player; +} + +static void +playerthread(void *player_) +{ + char *buf, cmd[64], seekpos[12], *fmt; + Player *player; + Ioproc *io; + Image *thiscover; + ulong c; + int p[2], fd, pid, noinit, trycoverload; + long n, r; + vlong boffset, boffsetlast; + Meta *cur; + + threadsetname("player"); + player = player_; + noinit = 0; + boffset = 0; + buf = nil; + trycoverload = 1; + io = nil; + pid = -1; + +restart: + cur = getmeta(player->pcur); + fmt = cur->filefmt; + fd = -1; + if(*fmt){ + if((fd = open(cur->path, OREAD)) < 0){ + fprint(2, "%r\n"); + sendul(player->ev, Everror); + chanclose(player->ev); + goto freeplayer; + } + }else{ + sendul(player->ev, Evready); + chanclose(player->ev); + } + + pipe(p); + if((pid = rfork(RFPROC|RFFDG|RFNOTEG|RFCENVG|RFNOWAIT)) == 0){ + close(p[1]); + if(fd < 0) + fd = open("/dev/null", OREAD); + dup(fd, 0); close(fd); + dup(p[0], 1); close(p[0]); + if(!debug){ + dup(fd = open("/dev/null", OWRITE), 2); + close(fd); + } + if(*fmt){ + snprint(cmd, sizeof(cmd), "/bin/audio/%sdec", fmt); + snprint(seekpos, sizeof(seekpos), "%g", (double)boffset/Bps); + execl(cmd, cmd, boffset ? "-s" : nil, seekpos, nil); + }else{ + execl("/bin/play", "play", "-o", "/fd/1", cur->path, nil); + } + close(0); + close(1); + exits("%r"); + } + if(pid < 0) + sysfatal("rfork: %r"); + if(fd >= 0) + close(fd); + close(p[0]); + + c = 0; + if(!noinit){ + if(*fmt){ + sendul(player->ev, Evready); + chanclose(player->ev); + } + buf = malloc(Relbufsz); + if((io = ioproc()) == nil) + sysfatal("player: %r"); + if((n = ioreadn(io, p[1], buf, Relbufsz)) < 0) + fprint(2, "player: %r\n"); + if(recv(player->ctl, &c) < 0 || c != Cstart) + goto freeplayer; + if(n < 1) + goto next; + audioon(); + boffset = iowrite(io, audio, buf, n); + noinit = 1; + } + + boffsetlast = boffset; + byteswritten = boffset; + pcurplaying = player->pcur; + if(c != Cseekrel) + redraw(1); + + while(1){ + n = ioread(io, p[1], buf, Relbufsz); + if(n <= 0) + break; + + thiscover = nil; + if(player->img != nil && nbrecv(player->img, &thiscover) != 0){ + freeimage(cover); + cover = thiscover; + redraw(1); + player->img = nil; + } + r = nbrecv(player->ctl, &c); + if(r < 0){ + audiooff(); + goto stop; + }else if(r != 0){ + if(c == Ctoggle){ + audiooff(); + if(recv(player->ctl, &c) < 0 || c == Cstop) + goto stop; + }else if(c == Cseekrel){ + boffset = MAX(0, boffset + player->seek*Bps); + n = 0; + break; + }else if(c == Cstop){ + audiooff(); + goto stop; + } + } + + boffset += n; + byteswritten = boffset; + audioon(); + iowrite(io, audio, buf, n); + if(trycoverload){ + trycoverload = 0; + player->img = chancreate(sizeof(Image*), 0); + proccreate(coverload, player, 4096); + } + if(labs(boffset/Relbufsz - boffsetlast/Relbufsz) > 0){ + boffsetlast = boffset; + redraw(0); + } + } + + if(n < 1){ /* seeking backwards or end of the song */ + close(p[1]); + p[1] = -1; + if(c != Cseekrel || (getmeta(pcurplaying)->duration && boffset >= getmeta(pcurplaying)->duration/1000*Bps)){ +next: + playercurr = nil; + playercurr = newplayer((player->pcur+1) % pl->n, 1); + start(playercurr); + goto stop; + } + goto restart; + } + +stop: + if(player->img != nil) + freeimage(recvp(player->img)); +freeplayer: + chanfree(player->ctl); + chanfree(player->ev); + if(pid >= 0) + postnote(PNGROUP, pid, "interrupt"); + closeioproc(io); + if(p[1] >= 0) + close(p[1]); + if(player == playercurr) + playercurr = nil; + if(player == playernext) + playernext = nil; + free(buf); + free(player); + threadexits(nil); +} + +static void +toggle(Player *player) +{ + if(player != nil) + sendul(player->ctl, Ctoggle); +} + +static void +seekrel(Player *player, double off) +{ + if(player != nil && *getmeta(pcurplaying)->filefmt){ + player->seek = off; + sendul(player->ctl, Cseekrel); + } +} + +static void +writeplist(void) +{ + int i; + + for(i = 0; i < pl->n; i++) + printmeta(&out, pl->m+i); +} + +static void +freeplist(Playlist *pl) +{ + if(pl != nil){ + free(pl->m); + free(pl->raw); + } + free(pl); +} + +static char * +readall(int f) +{ + int bufsz, sz, n; + char *s; + + bufsz = 1023; + s = nil; + for(sz = 0;; sz += n){ + if(bufsz-sz < 1024){ + bufsz *= 2; + s = realloc(s, bufsz); + } + if((n = readn(f, s+sz, bufsz-sz-1)) < 1) + break; + } + if(n < 0 || sz < 1){ + free(s); + return nil; + } + s[sz] = 0; + + return s; +} + +static Playlist * +readplist(int fd) +{ + char *raw, *s, *e, *a[5], *b; + Playlist *pl; + int plsz; + Meta *m; + + if((raw = readall(fd)) == nil) + return nil; + + plsz = 0; + for(s = raw; (s = strchr(s, '\n')) != nil; s++){ + if(*(++s) == '\n') + plsz++; + } + + if((pl = calloc(1, sizeof(*pl))) == nil || (pl->m = calloc(plsz+1, sizeof(Meta))) == nil){ + freeplist(pl); + werrstr("no memory"); + return nil; + } + + pl->raw = raw; + for(s = pl->raw, m = pl->m;; s = e){ + if((e = strchr(s, '\n')) == nil) + break; + s += 2; + *e++ = 0; + switch(s[-2]){ + case 0: + if(m->path != nil){ + pl->n++; + m++; + } + break; + case Pimage: + if(tokenize(s, a, nelem(a)) >= 4){ + m->imageoffset = atoi(a[0]); + m->imagesize = atoi(a[1]); + m->imagereader = atoi(a[2]); + m->imagefmt = a[3]; + } + break; + case Pduration: + m->duration = strtoull(s, nil, 0); + break; + case Partist: + if(m->numartist < Maxartist) + m->artist[m->numartist++] = s; + break; + case Pfilefmt: m->filefmt = s; break; + case Palbum: m->album = s; break; + case Pdate: m->date = s; break; + case Ptitle: m->title = s; break; + case Ptrack: m->track = s; break; + case Ppath: + m->path = s; + m->basename = (b = utfrrune(s, '/')) == nil ? s : b+1; + break; + } + } + if(m != nil && m->path != nil) + pl->n++; + + return pl; +} + +static void +recenter(void) +{ + updatescrollsz(); + scroll = pcur - scrollsz/2 + 1; +} + +static void +search(char d) +{ + Meta *m; + static char buf[64]; + static int sz; + int inc, i, a, cycle; + + inc = (d == '/' || d == 'n') ? 1 : -1; + if(d == '/' || d == '?') + sz = enter(inc > 0 ? "forward:" : "backward:", buf, sizeof(buf), mctl, kctl, nil); + if(sz < 1) + return; + + cycle = 1; + for(i = pcur+inc; i >= 0 && i < pl->n;){ + m = getmeta(i); + for(a = 0; a < m->numartist; a++){ + if(cistrstr(m->artist[a], buf) != nil) + break; + } + if(m->album != nil && cistrstr(m->album, buf) != nil) + break; + if(m->title != nil && cistrstr(m->title, buf) != nil) + break; + if(cistrstr(m->path, buf) != nil) + break; +onemore: + i += inc; + } + if(i >= 0 && i < pl->n){ + pcur = i; + recenter(); + redraw(1); + }else if(cycle && i+inc < 0){ + cycle = 0; + i = pl->n; + goto onemore; + }else if(cycle && i+inc >= pl->n){ + cycle = 0; + i = -1; + goto onemore; + } +} + +static void +chvolume(int d) +{ + int f, l, r, ol, or; + Biobuf b; + char *s, *a[4]; + + if((f = open("/dev/volume", ORDWR)) < 0) + return; + Binit(&b, f, OREAD); + + l = r = 0; + for(; (s = Brdline(&b, '\n')) != nil;){ + if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){ + l = ol = atoi(a[1]); + r = or = atoi(a[2]); + for(;;){ + l += d; + r += d; + fprint(f, "master %d %d\n", l, r); + Bseek(&b, 0, 0); + for(; (s = Brdline(&b, '\n')) != nil;){ + if(strncmp(s, "master", 6) == 0 && tokenize(s, a, 3) == 3){ + if(atoi(a[1]) == l && atoi(a[2]) == r) + goto end; + if(atoi(a[1]) != ol && atoi(a[2]) != or) + goto end; + if(l < 0 || r < 0 || l > 100 || r > 100) + goto end; + break; + } + } + } + } + } + +end: + volume = (l+r)/2; + if(volume > 100) + volume = 100; + else if(volume < 0) + volume = 0; + + Bterm(&b); + close(f); +} + +static void +toggleshuffle(void) +{ + int i, m, xi, a, c, pcurnew, pcurplayingnew; + + if(shuffle == nil){ + if(pl->n < 2) + return; + + m = pl->n; + if(pl->n < 4){ + a = 1; + c = 3; + m = 7; + }else{ + m += 1; + m |= m >> 1; + m |= m >> 2; + m |= m >> 4; + m |= m >> 8; + m |= m >> 16; + a = 1 + nrand(m/4)*4; /* 1 ≤ a < m && a mod 4 = 1 */ + c = 3 + nrand((m-2)/2)*2; /* 3 ≤ c < m-1 && c mod 2 = 1 */ + } + + shuffle = malloc(pl->n*sizeof(*shuffle)); + xi = pcurplaying < 0 ? pcur : pcurplaying; + pcurplayingnew = -1; + pcurnew = 0; + for(i = 0; i < pl->n;){ + if(xi < pl->n){ + if(pcur == xi) + pcurnew = i; + if(pcurplaying == xi) + pcurplayingnew = i; + shuffle[i++] = xi; + } + xi = (a*xi + c) & m; + } + pcur = pcurnew; + pcurplaying = pcurplayingnew; + }else{ + pcur = shuffle[pcur]; + if(pcurplaying >= 0) + pcurplaying = shuffle[pcurplaying]; + free(shuffle); + shuffle = nil; + } + + stop(playernext); + if(pcur < pl->n-1) + playernext = newplayer(pcur+1, 0); +} + +static void +plumbaudio(void *kbd) +{ + int i, f, pf; + Playlist *p; + Plumbmsg *m; + char *s, *e; + Rune c; + + threadsetname("audio/plumb"); + if((f = plumbopen("audio", OREAD)) >= 0){ + while((m = plumbrecv(f)) != nil){ + s = m->data; + if(strncmp(s, "key", 3) == 0 && isspace(s[3])){ + for(s = s+4; isspace(*s); s++); + for(; (i = chartorune(&c, s)) > 0 && c != Runeerror; s += i) + sendul(kbd, c); + continue; + } + if(*s != '/' && m->wdir != nil) + s = smprint("%s/%.*s", m->wdir, m->ndata, m->data); + + if((e = strrchr(s, '.')) != nil && strcmp(e, ".plist") == 0 && (pf = open(s, OREAD)) >= 0){ + p = readplist(pf); + close(pf); + if(p == nil) + continue; + + freeplist(pl); + pl = p; + memset(mincolwidth, 0, sizeof(mincolwidth)); /* readjust columns */ + sendul(playc, 0); + }else{ + for(i = 0; i < pl->n; i++){ + if(strcmp(pl->m[i].path, s) == 0){ + sendul(playc, i); + break; + } + } + } + + if(s != m->data) + free(s); + plumbfree(m); + } + } + + threadexits(nil); +} + +static void +usage(void) +{ + fprint(2, "usage: %s [-s] [-G] [-c aAdDtTp]\n", argv0); + sysfatal("usage"); +} + +void +threadmain(int argc, char **argv) +{ + Rune key; + Mouse m; + ulong ind; + enum { + Emouse, + Eresize, + Ekey, + Eplay, + }; + Alt a[] = { + { nil, &m, CHANRCV }, + { nil, nil, CHANRCV }, + { nil, &key, CHANRCV }, + { nil, &ind, CHANRCV }, + { nil, nil, CHANEND }, + }; + int n, scrolling, oldpcur, oldbuttons, pnew, shuffled, nogui; + char buf[64]; + + shuffled = 0; + nogui = 0; + ARGBEGIN{ + case 'd': + debug++; + break; + case 's': + shuffled = 1; + break; + case 'c': + cols = EARGF(usage()); + if(strlen(cols) >= nelem(colwidth)) + sysfatal("max %d columns allowed", nelem(colwidth)); + break; + case 'G': + nogui = 1; + break; + default: + usage(); + break; + }ARGEND; + + if((pl = readplist(0)) == nil){ + fprint(2, "playlist: %r\n"); + sysfatal("playlist error"); + } + close(0); + + Binit(&out, 1, OWRITE); + if(nogui){ + writeplist(); + Bterm(&out); + threadexitsall(nil); + } + pnotifies = fd2path(1, buf, sizeof(buf)) == 0 && strcmp(buf, "/dev/cons") != 0; + + if(initdraw(nil, nil, "zuke") < 0) + sysfatal("initdraw: %r"); + f = display->defaultfont; + Scrollwidth = MAX(14, stringwidth(f, "#")); + Scrollheight = MAX(16, f->height); + Coversz = MAX(64, stringwidth(f, "∫ 00:00:00/00:00:00 100%")); + unlockdisplay(display); + display->locking = 1; + if((mctl = initmouse(nil, screen)) == nil) + sysfatal("initmouse: %r"); + if((kctl = initkeyboard(nil)) == nil) + sysfatal("initkeyboard: %r"); + + a[0].c = mctl->c; + a[1].c = mctl->resizec; + a[2].c = kctl->c; + a[3].c = chancreate(sizeof(ind), 0); + playc = a[3].c; + + for(n = 0; n < Numcolors; n++) + colors[n].im = allocimage(display, Rect(0,0,1,1), RGB24, 1, colors[n].rgb<<8 | 0xff); + + srand(time(0)); + pcurplaying = -1; + chvolume(0); + fmtinstall('P', positionfmt); + threadsetname("zuke"); + + if(shuffled){ + pcur = nrand(pl->n); + toggleshuffle(); + recenter(); + } + + redraw(1); + oldbuttons = 0; + scrolling = 0; + + proccreate(plumbaudio, kctl->c, 4096); + + for(;;){ + oldpcur = pcur; + if(seekmx != newseekmx){ + seekmx = newseekmx; + redraw(0); + } + + switch(alt(a)){ + case Emouse: + if(ptinrect(m.xy, seekbar)){ + seekoff = getmeta(pcurplaying)->duration * (double)(m.xy.x-1-seekbar.min.x) / (double)Dx(seekbar); + if(seekoff < 0) + seekoff = 0; + newseekmx = m.xy.x; + }else{ + newseekmx = -1; + } + + if(m.buttons != 2) + scrolling = 0; + if(m.buttons == 0) + break; + if(m.buttons == 8){ + scroll = MAX(scroll-scrollsz/4-1, 0); + redraw(1); + break; + }else if(m.buttons == 16){ + scroll = MIN(scroll+scrollsz/4+1, pl->n-scrollsz); + redraw(1); + break; + } + + + if(oldbuttons == 0 && !scrolling && ptinrect(m.xy, insetrect(seekbar, -4))){ + if(ptinrect(m.xy, seekbar)) + seekrel(playercurr, seekoff/1000.0 - byteswritten/Bps); + break; + } + + n = (m.xy.y - screen->r.min.y)/f->height; + + if(oldbuttons == 0 && m.xy.x <= screen->r.min.x+Scrollwidth){ + if(m.buttons == 1){ + scroll = MAX(0, scroll-n-1); + redraw(1); + break; + }else if(m.buttons == 4){ + scroll = MIN(scroll+n+1, pl->n-scrollsz); + redraw(1); + break; + }else if(m.buttons == 2){ + scrolling = 1; + } + } + + if(scrolling){ + if(scrollsz >= pl->n) + break; + scroll = (m.xy.y - screen->r.min.y - Scrollheight/4)*(pl->n-scrollsz) / (Dy(screen->r)-Scrollheight/2); + scroll = CLAMP(scroll, 0, pl->n-scrollsz); + redraw(1); + }else if(m.buttons == 1 || m.buttons == 2){ + n += scroll; + if(n < pl->n){ + pcur = n; + if(m.buttons == 2){ + stop(playercurr); + playercurr = newplayer(pcur, 1); + start(playercurr); + } + } + } + break; + case Eresize: /* resize */ + if(getwindow(display, Refnone) < 0) + sysfatal("getwindow: %r"); + redraw(1); + break; + case Ekey: + switch(key){ + case Kleft: + seekrel(playercurr, -(double)Seek); + break; + case Kright: + seekrel(playercurr, Seek); + break; + case ',': + seekrel(playercurr, -(double)Seekfast); + break; + case '.': + seekrel(playercurr, Seekfast); + break; + case Kup: + pcur--; + break; + case Kpgup: + pcur -= scrollsz; + break; + case Kdown: + pcur++; + break; + case Kpgdown: + pcur += scrollsz; + break; + case Kend: + pcur = pl->n-1; + scroll = pl->n-scrollsz; + break; + case Khome: + pcur = 0; + break; + case '\n': +playcur: + stop(playercurr); + playercurr = newplayer(pcur, 1); + start(playercurr); + break; + case 'q': + case Kdel: + stop(playercurr); + goto end; + case 'i': + case 'o': + if(pcur == pcurplaying) + oldpcur = -1; + pcur = pcurplaying; + recenter(); + break; + case 'b': + case '>': + if(playercurr == nil) + break; + pnew = pcurplaying; + if(++pnew >= pl->n) + pnew = 0; + stop(playercurr); + playercurr = newplayer(pnew, 1); + start(playercurr); + redraw(1); + break; + case 'z': + case '<': + if(playercurr == nil) + break; + pnew = pcurplaying; + if(--pnew < 0) + pnew = pl->n-1; + stop(playercurr); + playercurr = newplayer(pnew, 1); + start(playercurr); + redraw(1); + break; + case '-': + chvolume(-1); + redraw(0); + break; + case '+': + case '=': + chvolume(+1); + redraw(0); + break; + case 'v': + stop(playercurr); + playercurr = nil; + pcurplaying = -1; + freeimage(cover); + cover = nil; + redraw(1); + break; + case 's': + toggleshuffle(); + recenter(); + redraw(1); + break; + case 'c': + case 'p': + case ' ': + toggle(playercurr); + break; + case '/': + case '?': + case 'n': + case 'N': + search(key); + break; + } + break; + case Eplay: + pcur = ind; + recenter(); + if(playercurr != nil) + goto playcur; + break; + } + + if(pcur != oldpcur){ + pcur = CLAMP(pcur, 0, pl->n-1); + if(pcur < scroll) + scroll = pcur; + else if(pcur > scroll + scrollsz) + scroll = pcur - scrollsz; + scroll = CLAMP(scroll, 0, pl->n-scrollsz); + + if(pcur != oldpcur) + redraw(1); + } + } + +end: + threadexitsall(nil); +}