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/>.
23 import std
.concurrency
;
26 import arsd
.simpledisplay
;
43 // ////////////////////////////////////////////////////////////////////////// //
44 //version = id3v2_debug;
47 // ////////////////////////////////////////////////////////////////////////// //
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
) {
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)
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
);
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
);
87 // shift bytes we want to keep
88 import core
.stdc
.string
: memmove
;
89 memmove(rdbuf
.ptr
, rdbuf
.ptr
+xmpos
, bytesToLeft
);
98 if (!fillBuffer()) throw new Exception("out of ID3v2 data");
99 return rdbuf
.ptr
[rbpos
++];
106 fillBuffer(); // initial fill
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) {
112 flags
= rdbuf
.ptr
[5];
113 if (flags
&0b11111) goto skipit
;
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];
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");
142 foreach (immutable n
; 0..T
.sizeof
) {
145 if (lastByteWasFF
&& b
== 0) {
146 if (wholesize
< 1) throw new Exception("out of ID3v2 data");
150 lastByteWasFF
= (b
== 0xff);
158 // skip extended header
160 uint ehsize
= getUInt
!uint;
161 while (ehsize
-- > 0) getUInt
!ubyte;
165 mainloop
: while (wholesize
>= 8) {
167 foreach (ref char ch
; tag
[]) {
168 ch
= cast(char)getByte
;
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') {
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
190 ubyte encoding
= getUInt
!ubyte; // 0: iso-8859-1; 1: unicode
191 if (encoding
> 1) throw new Exception("invalid ID3v2 text encoding");
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; }
202 s2
.reserve(str.length
*4);
203 foreach (char ch
; str) {
205 auto len
= utf8Encode(buf
[], cast(dchar)ch
);
210 *deststr
= cast(string
)s2
; // it is safe to cast here
212 if (tagsize
< 2) goto skiptag
; // no room for BOM
214 auto osize
= wholesize
;
215 ubyte b0
= getUInt
!ubyte;
216 ubyte b1
= getUInt
!ubyte;
219 if (osize
-wholesize
> tagsize
) throw new Exception("invalid ID3v2 text content");
221 if (b1
!= 0xfe) throw new Exception("invalid ID3v2 text content");
223 } else if (b0
== 0xfe) {
224 if (b1
!= 0xff) throw new Exception("invalid ID3v2 text content");
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");
230 if (b1
!= 0xbf) throw new Exception("invalid ID3v2 text content");
231 // utf-8 (just in case)
236 str.reserve(tagsize
);
237 while (osize
-wholesize
< tagsize
) {
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
);
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
257 wholesize
-= tagsize
;
258 foreach (immutable _
; 0..tagsize
) getByte();
266 // ////////////////////////////////////////////////////////////////////////// //
267 public struct AudioStream
{
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
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
); }
290 if (type
.length
== 0) return false;
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);
301 @property string
typestr () const pure nothrow @safe @nogc { return type
; }
304 if (type
.length
== 0) return;
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;
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;
320 if (ff
is null) return 0;
324 short* bp
= cast(short*)buf
;
326 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
327 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
329 samplesread
+= rd
; // number of samples read
330 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
334 return cast(int)(res
/channels
); // number of frames read
336 if (vi
is null) return 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
343 auto dptr
= cast(short*)buf
;
344 if (of
is null) return 0;
347 while (count
> 0 && smpbufpos
< smpbufused
) {
348 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
349 if (channels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
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
[];
360 smpbufused
= cast(uint)rd
.length
;
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
) {
370 if (!mp3
.decodeNextFrame(&reader
)) return 0;
371 mfm
= mp3
.frameSamples
;
372 if (mp3
.sampleRate
!= rate || mp3
.channels
!= channels
) return 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
++];
383 samplesread
+= mp3smpused
-oldmpu
; // number of samples read
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
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);
402 if (!drflac_seek_to_sample(ff
, snum
)) {
403 drflac_seek_to_sample(ff
, 0);
407 return snum
/channels
;
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
;
417 if (of
is null) return 0;
419 samplesread
= of
.smpcurtime
*channels
;
420 return samplesread
/channels
;
422 if (!mp3
.valid
) return 0;
424 if (mp3info
.index
.length
== 0 || snum
== 0) {
425 // alas, we cannot seek here
428 mp3
.restart(&reader
);
431 // find frame containing our sample
432 // stupid binary search; ignore overflow bug
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
) {
442 fl
.seek(mp3info
.index
[cast(size_t
)mid
].fpos
);
443 mp3smpused
= cast(uint)(snum
-smps
);
447 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
449 // alas, we cannot seek
452 mp3
.restart(&reader
);
462 Mp3Info mp3info
; // scanned info, frame index
468 uint smpbufpos
, smpbufused
;
470 int reader (void[] buf
) {
472 auto rd
= fl
.rawRead(buf
);
473 return cast(int)rd
.length
;
474 } catch (Exception e
) {}
479 static AudioStream
open (VFile fl
) {
480 //import std.string : fromStringz;
488 import core
.stdc
.stdio
;
489 import core
.stdc
.stdlib
: malloc
, free
;
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
);
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
;
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
);
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);
539 } catch (Exception
) {}
543 if (ov_fopen(fl
, &sio
.vf
) == 0) {
544 scope(failure
) ov_clear(&sio
.vf
);
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);
569 } catch (Exception
) {}
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");
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
;
593 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
595 } catch (Exception
) {}
600 sio
.mp3
= new MP3Decoder(&sio
.reader
);
603 // scan file to determine number of frames
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);
619 if (idtag
.scanParse(fl
)) {
620 sio
.album
= idtag
.album
;
621 sio
.artist
= idtag
.artist
;
622 sio
.title
= idtag
.title
;
624 } catch (Exception e
) {}
630 } catch (Exception
) {}
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 () {
655 playertid
= spawn(&playerThread
, thisTid
);
656 scannertid
= spawn(&scannerThread
, thisTid
);
660 public void aplayShutdown () {
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
; }
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
) {
689 if (idx
>= 0 && idx
< 10) {
690 v
= alsaEqBands
[idx
];
691 if (v
< -20) v
= -20; else if (v
> 20) v
= 20;
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
{
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
) {
745 scope(exit
) sio
.close();
747 string newfilereq
= null;
748 bool forcestart
= false;
751 uint realRate
= alsaGetBestSampleRate(48000);
752 conwriteln("real sampling rate: ", realRate
);
753 if (realRate
!= 44100 && realRate
!= 48000) {
755 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
759 receiveTimeout((sio
.valid
&& !paused ? Duration
.min
: 42.seconds
),
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;
771 //conwriteln("prc=", alsaGain);
773 (TMsgStopFileReq req
) {
776 if (alsaIsOpen
) alsaShutdown();
778 (TMsgTogglePauseReq req
) {
779 if (sio
.valid
) paused
= !paused
; else paused
= false;
782 if (sio
.valid
) paused
= req
.pause
;
785 newtime
= req
.timems
;
786 if (newtime
< 0) newtime
= 0;
792 if (newfilereq
!is null) {
794 auto reply
= new EventFileLoaded();
795 reply
.filename
= newfilereq
;
796 reply
.success
= false;
798 bool wasplaying
= sio
.valid
;
801 sio
= AudioStream
.open(VFile(reply
.filename
));
802 } catch (Exception e
) {
803 sio
= AudioStream
.init
;
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
;
813 if (alsaIsOpen
) alsaShutdown();
815 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);
821 atomicStore(aplPaused
, false);
822 atomicStore(aplPlaying
, false);
823 atomicStore(aplCurTime
, 0);
824 atomicStore(aplTotalTime
, 0);
825 //atomicStore(aplSampleRate, 48000);
826 //atomicStore(aplChannels, 2);
830 if (newtime
!= -666) {
833 if (tm
>= sio
.timetotal
) tm
= (sio
.timetotal ? sio
.timetotal
-1 : 0);
834 sio
.seekToTime(cast(uint)tm
);
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
);
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());
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
]);
860 atomicStore(aplPlaying
, true);
861 atomicStore(aplPaused
, true);
862 if (alsaIsOpen
) alsaShutdown();
868 // ////////////////////////////////////////////////////////////////////////// //
869 shared static this () {
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
;
888 conwriteln("invalid equalizer band index: ", idx
);
890 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
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))) {
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))) {
923 static if (is(T
== string
)) alias fn
= filename
; else auto fn
= filename
.idup
;
924 scannertid
.send(TMsgScanCancelReq(fn
));
926 scanQueue
.remove(filename
);
932 void scannerThread (Tid ownerTid
) {
934 //conwriteln("scan tread started...");
936 receiveTimeout((scanQueue
.length ? Duration
.min
: scanComplete
.length ?
100.msecs
: 1.hours
),
940 (TMsgScanFileReq req
) {
941 scanQueue
[req
.fname
] = true;
943 (TMsgScanCancelReq req
) {
944 scanQueue
.remove(req
.fname
);
945 scanComplete
.remove(req
.fname
);
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
);
960 string fname
= scanQueue
.byKey
.front
;
961 EventFileScanned reply
;
962 if (auto rpp
= fname
in scanComplete
) {
964 scanComplete
.remove(reply
.filename
);
966 reply
= new EventFileScanned();
967 reply
.filename
= scanQueue
.byKey
.front
;
968 reply
.success
= false;
969 //conwriteln("scanning '", reply.filename, "'...");
971 auto sio
= AudioStream
.open(VFile(reply
.filename
));
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;
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
);
987 scanComplete
[reply
.filename
] = reply
;