more id3v2 fixes
[amper.git] / aplayer.d
blob5cb05bfbbc96c9ab06b029aa292d493d3a0eabec
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module aplayer;
18 private:
20 import core.atomic;
21 import core.time;
23 import std.concurrency;
24 import std.datetime;
26 import arsd.simpledisplay;
28 import iv.cmdcon;
29 import iv.cmdcongl;
30 import iv.mbandeq;
31 import iv.simplealsa;
32 import iv.strex;
33 import iv.utfutil;
34 import iv.vfs;
36 import iv.drflac;
37 import iv.minimp3;
38 import iv.mp3scan;
39 import iv.tremor;
40 import iv.dopus;
43 // ////////////////////////////////////////////////////////////////////////// //
44 //version = id3v2_debug;
47 // ////////////////////////////////////////////////////////////////////////// //
48 struct ID3v2 {
49 string artist;
50 string album;
51 string title;
53 // scan file for ID3v2 tags and parse 'em
54 // returns `false` if no tag found
55 // throws if tag is invalid (i.e. found, but inparseable)
56 // fl position is undefined after return/throw
57 bool scanParse (VFile fl) {
58 ubyte[] rdbuf;
59 int rbpos, rbused;
60 bool rbeof;
61 rdbuf.length = 8192;
62 scope(exit) delete rdbuf;
64 uint availData () nothrow @trusted @nogc { return rbused-rbpos; }
66 // returns 'false' if there is no more data at all (i.e. eof)
67 bool fillBuffer () {
68 if (rbeof) return false;
69 if (rbpos >= rbused) rbpos = rbused = 0;
70 while (rbused < rdbuf.length) {
71 auto rd = fl.rawRead(rdbuf[rbused..$]);
72 if (rd.length == 0) break; // no more data
73 rbused += cast(uint)rd.length;
75 if (rbpos >= rbused) { rbeof = true; return false; }
76 assert(rbpos < rbused);
77 return true;
80 bool shiftFillBuffer (uint bytesToLeft) {
81 if (bytesToLeft > rbused-rbpos) assert(0, "ID3v2 scanner internal error");
82 if (rbeof) return false;
83 if (bytesToLeft > 0) {
84 uint xmpos = rbused-bytesToLeft;
85 assert(xmpos < rbused);
86 if (xmpos > 0) {
87 // shift bytes we want to keep
88 import core.stdc.string : memmove;
89 memmove(rdbuf.ptr, rdbuf.ptr+xmpos, bytesToLeft);
92 rbpos = 0;
93 rbused = bytesToLeft;
94 return fillBuffer();
97 ubyte getByte () {
98 if (!fillBuffer()) throw new Exception("out of ID3v2 data");
99 return rdbuf.ptr[rbpos++];
102 ubyte flags;
103 uint wholesize;
104 ubyte verhi, verlo;
105 // scan
106 fillBuffer(); // initial fill
107 for (;;) {
108 import core.stdc.string : memchr;
109 if (rbeof || availData < 10) return false; // alas
110 if (rdbuf.ptr[0] == 'I' && rdbuf.ptr[1] == 'D' && rdbuf.ptr[2] == '3' && rdbuf.ptr[3] <= 3 && rdbuf.ptr[4] != 0xff) {
111 // check flags
112 flags = rdbuf.ptr[5];
113 if (flags&0b11111) goto skipit;
114 wholesize = 0;
115 foreach (immutable bpos; 6..10) {
116 ubyte b = rdbuf.ptr[bpos];
117 if (b&0x80) goto skipit; // oops
118 wholesize = (wholesize<<7)|b;
120 verhi = rdbuf.ptr[3];
121 verlo = rdbuf.ptr[4];
122 rbpos = 10;
123 break;
125 skipit:
126 auto fptr = memchr(rdbuf.ptr+1, 'I', availData-1);
127 uint pos = (fptr !is null ? cast(uint)(fptr-rdbuf.ptr) : availData);
128 shiftFillBuffer(availData-pos);
131 bool flagUnsync = ((flags*0x80) != 0);
132 bool flagExtHeader = ((flags*0x40) != 0);
133 //bool flagExperimental = ((flags*0x20) != 0);
135 version(id3v2_debug) writeln("ID3v2 found! version is 2.", verhi, ".", verlo, "; size: ", wholesize, "; flags (shifted): ", flags>>5);
137 bool lastByteWasFF = false; // used for unsync
139 T getUInt(T) () if (is(T == ubyte) || is(T == ushort) || is(T == uint)) {
140 if (wholesize < T.sizeof) throw new Exception("out of ID3v2 data");
141 uint res;
142 foreach (immutable n; 0..T.sizeof) {
143 ubyte b = getByte;
144 if (flagUnsync) {
145 if (lastByteWasFF && b == 0) {
146 if (wholesize < 1) throw new Exception("out of ID3v2 data");
147 b = getByte;
148 --wholesize;
150 lastByteWasFF = (b == 0xff);
152 res = (res<<8)|b;
153 --wholesize;
155 return cast(T)res;
158 // skip extended header
159 if (flagExtHeader) {
160 uint ehsize = getUInt!uint;
161 while (ehsize-- > 0) getUInt!ubyte;
164 // read frames
165 mainloop: while (wholesize >= 8) {
166 char[4] tag = void;
167 foreach (ref char ch; tag[]) {
168 ch = cast(char)getByte;
169 --wholesize;
170 if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z'))) break mainloop; // oops
172 lastByteWasFF = false;
173 uint tagsize = getUInt!uint;
174 if (tagsize >= wholesize) break; // ooops
175 if (wholesize-tagsize < 2) break; // ooops
176 ushort tagflags = getUInt!ushort;
177 version(id3v2_debug) writeln("TAG: ", tag[]);
178 if ((tagflags&0b000_11111_000_11111) != 0) goto skiptag;
179 if (tagflags&0x80) goto skiptag; // compressed
180 if (tagflags&0x40) goto skiptag; // encrypted
181 if (tag.ptr[0] == 'T') {
182 // text tag
183 if (tagsize < 1) goto skiptag;
184 string* deststr = null;
185 if (tag == "TALB") deststr = &album;
186 else if (tag == "TIT2") deststr = &title;
187 else if (tag == "TPE1") deststr = &artist;
188 if (deststr is null) goto skiptag; // not interesting
189 --tagsize;
190 ubyte encoding = getUInt!ubyte; // 0: iso-8859-1; 1: unicode
191 if (encoding > 1) throw new Exception("invalid ID3v2 text encoding");
192 if (encoding == 0) {
193 // iso-8859-1
194 char[] str;
195 str.reserve(tagsize);
196 auto osize = wholesize;
197 while (osize-wholesize < tagsize) str ~= cast(char)getUInt!ubyte;
198 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
199 foreach (immutable cidx, char ch; str) if (ch == 0) { str = str[0..cidx]; break; }
200 //FIXME
201 char[] s2;
202 s2.reserve(str.length*4);
203 foreach (char ch; str) {
204 char[4] buf = void;
205 auto len = utf8Encode(buf[], cast(dchar)ch);
206 assert(len > 0);
207 s2 ~= buf[0..len];
209 delete str;
210 *deststr = cast(string)s2; // it is safe to cast here
211 } else {
212 if (tagsize < 2) goto skiptag; // no room for BOM
213 //$FF FE or $FE FF
214 auto osize = wholesize;
215 ubyte b0 = getUInt!ubyte;
216 ubyte b1 = getUInt!ubyte;
217 bool bige;
218 bool utf8 = false;
219 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
220 if (b0 == 0xff) {
221 if (b1 != 0xfe) throw new Exception("invalid ID3v2 text content");
222 bige = false;
223 } else if (b0 == 0xfe) {
224 if (b1 != 0xff) throw new Exception("invalid ID3v2 text content");
225 bige = true;
226 } else if (b0 == 0xef) {
227 if (b1 != 0xbb) throw new Exception("invalid ID3v2 text content");
228 if (tagsize < 3) throw new Exception("invalid ID3v2 text content");
229 b1 = getUInt!ubyte;
230 if (b1 != 0xbf) throw new Exception("invalid ID3v2 text content");
231 // utf-8 (just in case)
232 utf8 = true;
234 char[4] buf = void;
235 char[] str;
236 str.reserve(tagsize);
237 while (osize-wholesize < tagsize) {
238 if (!utf8) {
239 b0 = getUInt!ubyte;
240 b1 = getUInt!ubyte;
241 dchar dch = cast(dchar)(bige ? b0*256+b1 : b1*256+b0);
242 if (dch > dchar.max) dch = '\uFFFD';
243 auto len = utf8Encode(buf[], dch);
244 assert(len > 0);
245 str ~= buf[0..len];
246 } else {
247 str ~= cast(char)getUInt!ubyte;
250 if (osize-wholesize > tagsize) throw new Exception("invalid ID3v2 text content");
251 foreach (immutable cidx, char ch; str) if (ch == 0) { str = str[0..cidx]; break; }
252 *deststr = cast(string)str; // it is safe to cast here
254 continue;
256 skiptag:
257 wholesize -= tagsize;
258 foreach (immutable _; 0..tagsize) getByte();
261 return true;
266 // ////////////////////////////////////////////////////////////////////////// //
267 public struct AudioStream {
268 private:
269 VFile fl;
270 string type;
272 public:
273 //long timetotal; // in milliseconds
274 uint rate = 1; // just in case
275 ubyte channels = 1; // just in case
276 ulong samplestotal; // multiplied by channels
277 ulong samplesread; // samples read so far, multiplied by channels
278 string album;
279 string artist;
280 string title;
282 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline, true); return samplesread/channels; }
283 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline, true); return samplestotal/channels; }
285 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplesread*1000/rate/channels); }
286 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplestotal*1000/rate/channels); }
288 public:
289 bool valid () {
290 if (type.length == 0) return false;
291 switch (type[0]) {
292 case 'f': return (ff !is null);
293 case 'v': return (vi !is null);
294 case 'm': return mp3.valid;
295 case 'o': return (of !is null);
296 default:
298 return false;
301 @property string typestr () const pure nothrow @safe @nogc { return type; }
303 void close () {
304 if (type.length == 0) return;
305 switch (type[0]) {
306 case 'f': if (ff !is null) { drflac_close(ff); ff = null; } break;
307 case 'v': if (vi !is null) { vi = null; ov_clear(&vf); } break;
308 case 'm': if (mp3.valid) mp3.close(); break;
309 default:
311 type = null;
314 int readFrames (void* buf, int count) {
315 if (count < 1) return 0;
316 if (count > int.max/4) count = int.max/4;
317 if (!valid) return 0;
318 switch (type[0]) {
319 case 'f':
320 if (ff is null) return 0;
321 int[512] flcbuf;
322 int res = 0;
323 count *= channels;
324 short* bp = cast(short*)buf;
325 while (count > 0) {
326 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
327 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
328 if (rd <= 0) break;
329 samplesread += rd; // number of samples read
330 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
331 res += rd;
332 count -= rd;
334 return cast(int)(res/channels); // number of frames read
335 case 'v':
336 if (vi is null) return 0;
337 int currstream = 0;
338 auto ret = ov_read(&vf, cast(ubyte*)buf, count*2*channels, &currstream);
339 if (ret <= 0) return 0; // error or eof
340 samplesread += ret/2; // number of samples read
341 return ret/2/channels; // number of frames read
342 case 'o':
343 auto dptr = cast(short*)buf;
344 if (of is null) return 0;
345 int total = 0;
346 while (count > 0) {
347 while (count > 0 && smpbufpos < smpbufused) {
348 *dptr++ = smpbuf.ptr[smpbufpos++];
349 if (channels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
350 --count;
351 ++total;
352 samplesread += channels;
354 if (count == 0) break;
355 auto rd = of.readFrame();
356 if (rd.length == 0) break;
357 if (rd.length > smpbuf.length) smpbuf.length = rd.length;
358 smpbuf[0..rd.length] = rd[];
359 smpbufpos = 0;
360 smpbufused = cast(uint)rd.length;
362 return total;
363 case 'm':
364 // yes, i know that frames are not independent, and i should actually
365 // seek to a frame with a correct sync word. meh.
366 if (!mp3.valid) return 0;
367 auto mfm = mp3.frameSamples;
368 if (mp3smpused+channels > mfm.length) {
369 mp3smpused = 0;
370 if (!mp3.decodeNextFrame(&reader)) return 0;
371 mfm = mp3.frameSamples;
372 if (mp3.sampleRate != rate || mp3.channels != channels) return 0;
374 int res = 0;
375 ushort* b = cast(ushort*)buf;
376 auto oldmpu = mp3smpused;
377 while (count > 0 && mp3smpused+channels <= mfm.length) {
378 *b++ = mfm[mp3smpused++];
379 if (channels == 2) *b++ = mfm[mp3smpused++];
380 --count;
381 ++res;
383 samplesread += mp3smpused-oldmpu; // number of samples read
384 return res;
385 default: break;
387 return 0;
390 // return new frame index
391 ulong seekToTime (uint msecs) {
392 if (!valid) return 0;
393 ulong snum = cast(ulong)msecs*rate/1000*channels; // sample number
394 switch (type[0]) {
395 case 'f':
396 if (ff is null) return 0;
397 if (ff.totalSampleCount < 1) return 0;
398 if (snum >= ff.totalSampleCount) {
399 drflac_seek_to_sample(ff, 0);
400 return 0;
402 if (!drflac_seek_to_sample(ff, snum)) {
403 drflac_seek_to_sample(ff, 0);
404 return 0;
406 samplesread = snum;
407 return snum/channels;
408 case 'v':
409 if (vi is null) return 0;
410 if (ov_pcm_seek(&vf, snum/channels) == 0) {
411 samplesread = ov_pcm_tell(&vf)*channels;
412 return samplesread/channels;
414 ov_pcm_seek(&vf, 0);
415 return 0;
416 case 'o':
417 if (of is null) return 0;
418 of.seek(msecs);
419 samplesread = of.smpcurtime*channels;
420 return samplesread/channels;
421 case 'm':
422 if (!mp3.valid) return 0;
423 mp3smpused = 0;
424 if (mp3info.index.length == 0 || snum == 0) {
425 // alas, we cannot seek here
426 samplesread = 0;
427 fl.seek(0);
428 mp3.restart(&reader);
429 return 0;
431 // find frame containing our sample
432 // stupid binary search; ignore overflow bug
433 ulong start = 0;
434 ulong end = mp3info.index.length-1;
435 while (start <= end) {
436 ulong mid = (start+end)/2;
437 auto smps = mp3info.index[cast(size_t)mid].samples;
438 auto smpe = (mp3info.index.length-mid > 0 ? mp3info.index[cast(size_t)(mid+1)].samples : samplestotal);
439 if (snum >= smps && snum < smpe) {
440 // i found her!
441 samplesread = snum;
442 fl.seek(mp3info.index[cast(size_t)mid].fpos);
443 mp3smpused = cast(uint)(snum-smps);
444 mp3.sync(&reader);
445 return snum;
447 if (snum < smps) end = mid-1; else start = mid+1;
449 // alas, we cannot seek
450 samplesread = 0;
451 fl.seek(0);
452 mp3.restart(&reader);
453 return 0;
454 default: break;
456 return 0;
459 private:
460 drflac* ff;
461 MP3Decoder mp3;
462 Mp3Info mp3info; // scanned info, frame index
463 uint mp3smpused;
464 OggVorbis_File vf;
465 vorbis_info* vi;
466 OpusFile of;
467 short[] smpbuf;
468 uint smpbufpos, smpbufused;
470 int reader (void[] buf) {
471 try {
472 auto rd = fl.rawRead(buf);
473 return cast(int)rd.length;
474 } catch (Exception e) {}
475 return -1;
478 public:
479 static AudioStream open (VFile fl) {
480 //import std.string : fromStringz;
481 AudioStream sio;
482 fl.seek(0);
483 // determine format
484 try {
485 auto fpos = fl.tell;
486 // flac
487 try {
488 import core.stdc.stdio;
489 import core.stdc.stdlib : malloc, free;
490 uint commentCount;
491 char* fcmts;
492 scope(exit) if (fcmts !is null) free(fcmts);
493 sio.ff = drflac_open_file(fl, (void* pUserData, drflac_metadata* pMetadata) {
494 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
495 if (fcmts !is null) free(fcmts);
496 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
497 if (csz > 0 && csz < 0x100_0000) {
498 fcmts = cast(char*)malloc(cast(uint)csz);
499 } else {
500 fcmts = null;
502 if (fcmts is null) {
503 commentCount = 0;
504 } else {
505 import core.stdc.string : memcpy;
506 commentCount = pMetadata.data.vorbis_comment.commentCount;
507 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
511 if (sio.ff !is null) {
512 scope(failure) drflac_close(sio.ff);
513 if (sio.ff.sampleRate < 1024 || sio.ff.sampleRate > 96000) throw new Exception("fucked flac");
514 if (sio.ff.channels < 1 || sio.ff.channels > 2) throw new Exception("fucked flac");
515 sio.rate = cast(uint)sio.ff.sampleRate;
516 sio.channels = cast(ubyte)sio.ff.channels;
517 sio.type = "flac";
518 sio.fl = fl;
519 sio.samplestotal = sio.ff.totalSampleCount;
520 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
522 drflac_vorbis_comment_iterator i;
523 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
524 uint commentLength;
525 const(char)* pComment;
526 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
527 if (commentLength > 1024*1024*2) break; // just in case
528 //conwriteln(" ", pComment[0..commentLength]);
529 auto cmts = pComment[0..commentLength];
530 //conwriteln(" <", cmts, ">");
531 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
532 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
533 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
536 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
537 return sio;
539 } catch (Exception) {}
540 fl.seek(fpos);
541 // vorbis
542 try {
543 if (ov_fopen(fl, &sio.vf) == 0) {
544 scope(failure) ov_clear(&sio.vf);
545 sio.type = "vorbis";
546 sio.fl = fl;
547 sio.vi = ov_info(&sio.vf, -1);
548 if (sio.vi.rate < 1024 || sio.vi.rate > 96000) throw new Exception("fucked vorbis");
549 if (sio.vi.channels < 1 || sio.vi.channels > 2) throw new Exception("fucked vorbis");
550 sio.rate = sio.vi.rate;
551 sio.channels = cast(ubyte)sio.vi.channels;
552 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
553 //conwriteln("streams: ", ov_streams(&sio.vf));
554 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
555 sio.samplestotal = ov_pcm_total(&sio.vf)*sio.channels;
556 if (auto vc = ov_comment(&sio.vf, -1)) {
557 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
558 foreach (immutable idx; 0..vc.comments) {
559 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
560 auto cmts = vc.user_comments[idx][0..vc.comment_lengths[idx]];
561 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
562 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
563 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
566 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
567 return sio;
569 } catch (Exception) {}
570 fl.seek(fpos);
571 // opus
572 try {
573 OpusFile of = opusOpen(fl);
574 scope(failure) opusClose(of);
575 if (of.rate < 1024 || of.rate > 96000) throw new Exception("fucked opus");
576 if (of.channels < 1 || of.channels > 2) throw new Exception("fucked opus");
577 sio.of = of;
578 sio.type = "opus";
579 sio.fl = fl;
580 sio.rate = of.rate;
581 sio.channels = of.channels;
582 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
583 sio.samplestotal = of.smpduration*sio.channels;
584 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
585 foreach (immutable cidx; 0..of.commentCount) {
586 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
587 auto cmts = of.comment(cidx);
588 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
589 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
590 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
592 //TODO: comments
593 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
594 return sio;
595 } catch (Exception) {}
596 fl.seek(fpos);
597 // mp3
598 try {
599 sio.fl = fl;
600 sio.mp3 = new MP3Decoder(&sio.reader);
601 sio.type = "mp3";
602 if (sio.mp3.valid) {
603 // scan file to determine number of frames
604 auto xfp = fl.tell;
605 fl.seek(fpos);
606 sio.mp3info = mp3Scan!true((void[] buf) => cast(int)fl.rawRead(buf).length); // build index too
607 if (sio.mp3info.valid) {
608 if (sio.mp3.sampleRate < 1024 || sio.mp3.sampleRate > 96000) throw new Exception("fucked mp3");
609 if (sio.mp3.channels < 1 || sio.mp3.channels > 2) throw new Exception("fucked mp3");
610 sio.rate = sio.mp3.sampleRate;
611 sio.channels = sio.mp3.channels;
612 sio.samplestotal = sio.mp3info.samples;
613 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
614 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
615 // get metadata
616 try {
617 ID3v2 idtag;
618 fl.seek(fpos);
619 if (idtag.scanParse(fl)) {
620 sio.album = idtag.album;
621 sio.artist = idtag.artist;
622 sio.title = idtag.title;
624 } catch (Exception e) {}
625 fl.seek(xfp);
626 return sio;
629 sio.mp3 = null;
630 } catch (Exception) {}
631 sio.mp3 = null;
632 fl.seek(fpos);
633 } catch (Exception) {}
634 return AudioStream.init;
639 // ////////////////////////////////////////////////////////////////////////// //
640 __gshared Tid playertid;
641 __gshared Tid scannertid;
644 // ////////////////////////////////////////////////////////////////////////// //
645 public class EventFileLoaded { string filename; string album; string artist; string title; int durationms; bool success; }
646 public class EventFileComplete {}
648 // ////////////////////////////////////////////////////////////////////////// //
649 __gshared bool started = false;
650 struct TMsgQuitReq {}
652 public void aplayStart () {
653 if (!started) {
654 started = true;
655 playertid = spawn(&playerThread, thisTid);
656 scannertid = spawn(&scannerThread, thisTid);
660 public void aplayShutdown () {
661 if (started) {
662 started = false;
663 playertid.send(TMsgQuitReq());
664 scannertid.send(TMsgQuitReq());
669 // ////////////////////////////////////////////////////////////////////////// //
670 // reply with EventFileLoaded
671 struct TMsgPlayFileReq { string filename; bool forcestart; }
672 struct TMsgStopFileReq {}
673 struct TMsgTogglePauseReq {}
674 struct TMsgPauseReq { bool pause; }
675 struct TMsgSeekReq { int timems; }
677 //TODO: queue this
678 public void aplayPlayFile (string fname, bool forcestart) { if (started) playertid.send(TMsgPlayFileReq(fname.length ? fname : "", forcestart)); }
679 public void aplayStopFile () { if (started) playertid.send(TMsgStopFileReq()); }
680 public void aplayTogglePause () { if (started) playertid.send(TMsgTogglePauseReq()); }
681 public void aplayPause (bool pause) { if (started) playertid.send(TMsgPauseReq(pause)); }
682 public void aplaySeekMS (int timems) { if (started) playertid.send(TMsgSeekReq(timems)); }
685 // ////////////////////////////////////////////////////////////////////////// //
686 // convert from [-20..20] to [0..27]
687 public int aplayGetEqBand (int idx) {
688 int v = 14;
689 if (idx >= 0 && idx < 10) {
690 v = alsaEqBands[idx];
691 if (v < -20) v = -20; else if (v > 20) v = 20;
692 v = 27*(v+20)/40;
694 return v;
697 // convert from [0..27] to [-20..20]
698 public void aplaySetEqBand (int idx, int v) {
699 if (idx >= 0 && idx < 10) {
700 if (v < 0) v = 0; else if (v > 27) v = 27;
701 v = (v != 14 ? (40*v/27)-20 : 0);
702 alsaEqBands[idx] = v;
707 // ////////////////////////////////////////////////////////////////////////// //
708 // reply with EventFileLoaded
709 struct TMsgPlayGainReq {
710 int prc; // percents
713 public void aplayPlayGain (int prc) { if (started) playertid.send(TMsgPlayGainReq(prc)); else alsaGain = prc; }
714 public int aplayPlayGain () { return alsaGain; }
717 // ////////////////////////////////////////////////////////////////////////// //
718 public bool aplayIsPlaying () { return atomicLoad(aplPlaying); }
719 public bool aplayIsPaused () { return atomicLoad(aplPaused); }
720 public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
721 public int aplayTotalTime () { return atomicLoad(aplTotalTime)/1000; }
722 public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
723 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime); }
725 public int aplaySampleRate () { return atomicLoad(aplSampleRate); }
726 public bool aplayIsStereo () { return (atomicLoad(aplChannels) == 2); }
729 // ////////////////////////////////////////////////////////////////////////// //
730 enum BUF_SIZE = 4096;
731 __gshared short[BUF_SIZE] buffer;
733 __gshared bool paused = false;
734 shared bool aplPlaying = false;
735 shared bool aplPaused = false;
736 shared int aplCurTime = 0;
737 shared int aplTotalTime = 0;
738 shared int aplSampleRate = 48000;
739 shared int aplChannels = 2;
742 // ////////////////////////////////////////////////////////////////////////// //
743 void playerThread (Tid ownerTid) {
744 AudioStream sio;
745 scope(exit) sio.close();
746 bool doQuit = false;
747 string newfilereq = null;
748 bool forcestart = false;
749 int newtime = -666;
751 uint realRate = alsaGetBestSampleRate(48000);
752 conwriteln("real sampling rate: ", realRate);
753 if (realRate != 44100 && realRate != 48000) {
754 realRate = 48000;
755 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
758 while (!doQuit) {
759 receiveTimeout((sio.valid && !paused ? Duration.min : 42.seconds),
760 (TMsgQuitReq req) {
761 doQuit = true;
763 (TMsgPlayFileReq req) {
764 newfilereq = (req.filename.length ? req.filename : "");
765 forcestart = req.forcestart;
767 (TMsgPlayGainReq req) {
768 if (req.prc < 0) req.prc = 0;
769 if (req.prc > 200) req.prc = 200;
770 alsaGain = req.prc;
771 //conwriteln("prc=", alsaGain);
773 (TMsgStopFileReq req) {
774 paused = false;
775 sio.close();
776 if (alsaIsOpen) alsaShutdown();
778 (TMsgTogglePauseReq req) {
779 if (sio.valid) paused = !paused; else paused = false;
781 (TMsgPauseReq req) {
782 if (sio.valid) paused = req.pause;
784 (TMsgSeekReq req) {
785 newtime = req.timems;
786 if (newtime < 0) newtime = 0;
790 if (doQuit) break;
792 if (newfilereq !is null) {
793 newtime = -666;
794 auto reply = new EventFileLoaded();
795 reply.filename = newfilereq;
796 reply.success = false;
797 newfilereq = null;
798 bool wasplaying = sio.valid;
799 sio.close();
800 try {
801 sio = AudioStream.open(VFile(reply.filename));
802 } catch (Exception e) {
803 sio = AudioStream.init;
805 if (sio.valid) {
806 reply.durationms = cast(int)sio.timetotal;
807 reply.album = sio.album;
808 reply.artist = sio.artist;
809 reply.title = sio.title;
810 reply.success = true;
811 if (forcestart) paused = false; else paused = !wasplaying;
812 } else {
813 if (alsaIsOpen) alsaShutdown();
815 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
818 if (!sio.valid) {
819 newtime = -666;
820 paused = false;
821 atomicStore(aplPaused, false);
822 atomicStore(aplPlaying, false);
823 atomicStore(aplCurTime, 0);
824 atomicStore(aplTotalTime, 0);
825 //atomicStore(aplSampleRate, 48000);
826 //atomicStore(aplChannels, 2);
827 continue;
830 if (newtime != -666) {
831 int tm = newtime;
832 newtime = -666;
833 if (tm >= sio.timetotal) tm = (sio.timetotal ? sio.timetotal-1 : 0);
834 sio.seekToTime(cast(uint)tm);
837 if (!paused) {
838 if (!alsaIsOpen || alsaRate != sio.rate || alsaChannels != sio.channels) {
839 if (alsaIsOpen) alsaShutdown();
840 if (!alsaInit(sio.rate, sio.channels)) assert(0, "cannot init ALSA playback");
842 auto frmread = sio.readFrames(buffer.ptr, BUF_SIZE/sio.channels);
843 if (frmread <= 0) {
844 atomicStore(aplPaused, false);
845 atomicStore(aplPlaying, false);
846 atomicStore(aplCurTime, 0);
847 atomicStore(aplTotalTime, 0);
848 if (glconCtlWindow !is null) glconCtlWindow.postEvent(new EventFileComplete());
849 sio.close();
850 } else {
851 atomicStore(aplPaused, false);
852 atomicStore(aplPlaying, true);
853 atomicStore(aplCurTime, cast(int)sio.timeread);
854 atomicStore(aplTotalTime, cast(int)sio.timetotal);
855 atomicStore(aplSampleRate, cast(int)sio.rate);
856 atomicStore(aplChannels, cast(int)sio.channels);
857 alsaWriteShort(buffer[0..frmread*sio.channels]);
859 } else {
860 atomicStore(aplPlaying, true);
861 atomicStore(aplPaused, true);
862 if (alsaIsOpen) alsaShutdown();
868 // ////////////////////////////////////////////////////////////////////////// //
869 shared static this () {
870 alsaEqBands[] = 0;
872 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
873 conRegVar!alsaDevice("device", "audio output device");
874 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
875 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
876 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
877 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
879 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
880 conRegFunc!((int idx, byte value) {
881 if (value < -70) value = -70;
882 if (value > 30) value = 30;
883 if (idx >= 0 && idx < alsaEqBands.length) {
884 if (alsaEqBands[idx] != value) {
885 alsaEqBands[idx] = value;
887 } else {
888 conwriteln("invalid equalizer band index: ", idx);
890 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
892 conRegFunc!(() {
893 alsaEqBands[] = 0;
894 })("eq_reset", "reset equalizer");
898 // ////////////////////////////////////////////////////////////////////////// //
899 public class EventFileScanned { string filename; string album; string artist; string title; int durationms; bool success; }
900 // reply with EventFileScanned
901 struct TMsgScanFileReq { string fname; }
902 struct TMsgScanCancelReq { string fname; }
904 __gshared bool[string] scanQueue;
905 __gshared EventFileScanned[string] scanComplete;
908 public void aplayQueueScan(T:const(char)[]) (T filename) {
909 static if (is(T == typeof(null))) {
910 aplayQueueScan("");
911 } else {
912 static if (is(T == string)) alias fn = filename; else auto fn = filename.idup;
913 if (started) scannertid.send(TMsgScanFileReq(fn)); else scanQueue[fn] = true;
918 public void aplayCancelScan(T:const(char)[]) (T filename) {
919 static if (is(T == typeof(null))) {
920 aplayQueueScan("");
921 } else {
922 if (started) {
923 static if (is(T == string)) alias fn = filename; else auto fn = filename.idup;
924 scannertid.send(TMsgScanCancelReq(fn));
925 } else {
926 scanQueue.remove(filename);
932 void scannerThread (Tid ownerTid) {
933 bool doQuit = false;
934 //conwriteln("scan tread started...");
935 while (!doQuit) {
936 receiveTimeout((scanQueue.length ? Duration.min : scanComplete.length ? 100.msecs : 1.hours),
937 (TMsgQuitReq req) {
938 doQuit = true;
940 (TMsgScanFileReq req) {
941 scanQueue[req.fname] = true;
943 (TMsgScanCancelReq req) {
944 scanQueue.remove(req.fname);
945 scanComplete.remove(req.fname);
949 if (doQuit) break;
950 if (scanQueue.length == 0) {
951 if (scanComplete.length == 0 || glconCtlWindow is null) continue;
952 string fname = scanComplete.byKey.front;
953 auto reply = scanComplete[fname];
954 scanComplete.remove(fname);
955 scanQueue.remove(fname);
956 glconCtlWindow.postEvent(reply);
957 continue;
960 string fname = scanQueue.byKey.front;
961 EventFileScanned reply;
962 if (auto rpp = fname in scanComplete) {
963 reply = *rpp;
964 scanComplete.remove(reply.filename);
965 } else {
966 reply = new EventFileScanned();
967 reply.filename = scanQueue.byKey.front;
968 reply.success = false;
969 //conwriteln("scanning '", reply.filename, "'...");
970 try {
971 auto sio = AudioStream.open(VFile(reply.filename));
972 if (sio.valid) {
973 reply.album = sio.album;
974 reply.artist = sio.artist;
975 reply.title = sio.title;
976 reply.durationms = cast(int)sio.timetotal;
977 reply.success = true;
978 sio.close();
980 } catch (Exception e) {}
982 scanQueue.remove(reply.filename);
983 //conwriteln(" scanned '", reply.filename, "': ", reply.success);
984 if (glconCtlWindow !is null) {
985 glconCtlWindow.postEvent(reply);
986 } else {
987 scanComplete[reply.filename] = reply;