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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 //version = amper_debug_decoder;
24 import std
.concurrency
;
27 import arsd
.simpledisplay
;
29 import iv
.audiostream
;
38 // ////////////////////////////////////////////////////////////////////////// //
39 __gshared
bool playerStarted
= false;
40 __gshared
bool scannerStarted
= false;
42 __gshared Tid playertid
;
43 __gshared Tid scannertid
;
46 // ////////////////////////////////////////////////////////////////////////// //
47 public class EventFileLoaded
{ string filename
; string album
; string artist
; string title
; int durationms
; bool success
; }
48 public class EventFileComplete
{}
50 // ////////////////////////////////////////////////////////////////////////// //
53 public void aplayStart () {
55 playertid
= spawn(&playerThread
, thisTid
);
60 public void aplayStartScanner () {
61 if (!scannerStarted
) {
62 scannertid
= spawn(&scannerThread
, thisTid
);
63 scannerStarted
= true;
67 public void aplayShutdown () {
69 playerStarted
= false;
70 playertid
.send(TMsgQuitReq());
73 scannerStarted
= false;
74 scannertid
.send(TMsgQuitReq());
79 // ////////////////////////////////////////////////////////////////////////// //
80 // reply with EventFileLoaded
81 struct TMsgPlayFileReq
{ string filename
; bool forcestart
; }
82 struct TMsgStopFileReq
{}
83 struct TMsgTogglePauseReq
{}
84 struct TMsgPauseReq
{ bool pause
; }
85 struct TMsgSeekReq
{ int timems
; }
88 public void aplayPlayFile (string fname
, bool forcestart
) { if (playerStarted
) playertid
.send(TMsgPlayFileReq(fname
.length ? fname
: "", forcestart
)); }
89 public void aplayStopFile () { if (playerStarted
) playertid
.send(TMsgStopFileReq()); }
90 public void aplayTogglePause () { if (playerStarted
) playertid
.send(TMsgTogglePauseReq()); }
91 public void aplayPause (bool pause
) { if (playerStarted
) playertid
.send(TMsgPauseReq(pause
)); }
92 public void aplaySeekMS (int timems
) { if (playerStarted
) playertid
.send(TMsgSeekReq(timems
)); }
95 // ////////////////////////////////////////////////////////////////////////// //
96 // convert from [-20..20] to [0..27]
97 public int aplayGetEqBand (int idx
) {
99 if (idx
>= 0 && idx
< 10) {
100 v
= alsaEqBands
[idx
];
101 if (v
< -20) v
= -20; else if (v
> 20) v
= 20;
107 // convert from [0..27] to [-20..20]
108 public void aplaySetEqBand (int idx
, int v
) {
109 if (idx
>= 0 && idx
< 10) {
110 if (v
< 0) v
= 0; else if (v
> 27) v
= 27;
111 v
= (v
!= 14 ?
(40*v
/27)-20 : 0);
112 alsaEqBands
[idx
] = v
;
117 // ////////////////////////////////////////////////////////////////////////// //
118 // reply with EventFileLoaded
119 struct TMsgPlayGainReq
{
123 public void aplayPlayGain (int prc
) { if (playerStarted
) playertid
.send(TMsgPlayGainReq(prc
)); else alsaGain
= prc
; }
124 public int aplayPlayGain () { return alsaGain
; }
127 // ////////////////////////////////////////////////////////////////////////// //
128 public bool aplayIsPlaying () { return atomicLoad(aplPlaying
); }
129 public bool aplayIsPaused () { return atomicLoad(aplPaused
); }
130 //public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
131 //public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
132 public int aplayCurTime () { lockBuffer(); scope(exit
) unlockBuffer(); return cast(int)(aplFramesFed
/atomicLoad(aplSampleRate
)); }
133 public int aplayCurTimeMS () { lockBuffer(); scope(exit
) unlockBuffer(); return cast(int)(aplFramesFed
*1000/atomicLoad(aplSampleRate
)); }
134 public int aplayTotalTime () { return atomicLoad(aplTotalTime
)/1000; }
135 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime
); }
137 public int aplaySampleRate () { return atomicLoad(aplSampleRate
); }
138 public bool aplayIsStereo () { return (atomicLoad(aplChannels
) == 2); }
141 // ////////////////////////////////////////////////////////////////////////// //
142 shared bool aplPlaying
= false;
143 shared bool aplPaused
= false;
144 //shared int aplCurTime = 0;
145 shared int aplTotalTime
= 0;
146 shared int aplSampleRate
= 48000;
147 shared int aplChannels
= 2;
148 __gshared
ulong aplFramesFed
= 0; // for current track
151 // ////////////////////////////////////////////////////////////////////////// //
152 enum BUF_SIZE
= 4096*10;
153 // read/write only when buffer is locked!
154 __gshared
short[BUF_SIZE
][2] buffers
;
155 __gshared
uint curbuffer
= 0; // current buffer decoder is filling; alternate buffer *can* have some data
156 __gshared
uint[2] buffrmused
= 0; // for both buffers
157 shared bool buflocked
= false;
160 void lockBuffer () nothrow @nogc {
162 while (!cas(&buflocked
, false, true)) {}
165 void unlockBuffer () nothrow @nogc {
167 atomicStore(buflocked
, false);
171 void withLockedBuffer (scope void delegate () dg
) {
173 scope(exit
) unlockBuffer();
178 bool hasBufferedData () nothrow @nogc {
180 scope(exit
) unlockBuffer();
181 return ((buffrmused
[0]|buffrmused
[1]) != 0);
185 // ////////////////////////////////////////////////////////////////////////// //
186 struct TMsgPingDecoder
{}
187 struct TMsgSomeDataDecoded
{}
188 struct TMsgReplaceSIO
{ shared AudioStream sio
; }
190 // audio decoding thread; should keep buffer filled
191 void decoderThread (Tid ownerTid
) {
192 AudioStream sio
= null;
193 bool hasmorefrms
= false;
196 bool waitingForDecoder
= true; // waiting for decoder to warm up
197 version(amper_debug_decoder
) conwriteln("decoder thread started");
199 bool longWait
= true;
200 if (sio
is null ||
!sio
.valid
) hasmorefrms
= false;
202 if (!hasmorefrms
) { longWait
= ((buffrmused
[0]|buffrmused
[1]) == 0); return; }
203 if (buffrmused
[curbuffer
] == BUF_SIZE
/sio
.channels
) { longWait
= (buffrmused
[curbuffer^
1] != 0); return; }
206 receiveTimeout((longWait ?
2.hours
: Duration
.min
),
211 newtime
= req
.timems
;
212 if (newtime
< 0) newtime
= 0;
213 version(amper_debug_decoder
) conwriteln("decoder: seek request, newtime=", newtime
);
214 withLockedBuffer(() {
217 hasmorefrms
= (sio
!is null && sio
.valid
);
219 waitingForDecoder
= true;
221 (TMsgPingDecoder req
) {
222 version(amper_debug_decoder
) conwriteln("decoder: ping");
223 // do nothing here, this is just a ping to go on with decoding
225 (TMsgReplaceSIO req
) {
226 AudioStream newsio
= cast()req
.sio
; // remove `shared`
227 if (sio
!is newsio
) {
228 if (sio
!is null) { sio
.close(); delete sio
; }
231 withLockedBuffer(() {
234 hasmorefrms
= (sio
!is null && sio
.valid
);
236 waitingForDecoder
= true;
240 bool sendPing
= false;
241 decodeloop
: for (;;) {
242 if (sio
is null ||
!sio
.valid
) hasmorefrms
= false;
246 // switch buffers if necessary
247 withLockedBuffer(() {
248 if (buffrmused
[curbuffer^
1] == 0) curbuffer ^
= 1;
253 if (newtime
!= -666) {
254 version(amper_debug_decoder
) conwriteln("decoder: seeking to ", newtime
);
257 assert(sio
!is null && sio
.valid
);
258 if (tm
>= sio
.timeTotal
) tm
= (sio
.timeTotal ? sio
.timeTotal
-1 : 0);
259 sio
.seekToTime(cast(uint)tm
);
260 //conwriteln("tm=", tm, "; timeRead=", sio.timeRead, "; framesRead=", sio.framesRead, "; frok=", sio.timeRead*sio.rate/1000);
261 aplFramesFed
= sio
.framesRead
;
262 hasmorefrms
= (sio
!is null && sio
.valid
);
265 // it is safe to work with current buffer without the lock here
266 uint bsmpused
= buffrmused
[curbuffer
]*sio
.channels
;
267 version(amper_debug_decoder
) conwriteln("decoder: curbuffer=", curbuffer
, "; used[0]=", buffrmused
[0], "; used[1]=", buffrmused
[1]);
268 // fill current buffer, switch to next
269 if (bsmpused
< BUF_SIZE
) {
271 int frmread
= sio
.readFrames(buffers
[curbuffer
].ptr
+bsmpused
, (BUF_SIZE
-bsmpused
)/sio
.channels
);
272 version(amper_debug_decoder
) conwriteln("decoder: frmread=", frmread
);
274 hasmorefrms
= false; // no more frames, we're done
276 bsmpused
= (buffrmused
[curbuffer
] += cast(uint)frmread
)*sio
.channels
;
278 assert(bsmpused
<= BUF_SIZE
);
280 // switch buffers, if alternate buffer was drained (and fill it)
281 version(amper_debug_decoder
) conwriteln("decoder: bsmpused=", bsmpused
, "; BUF_SIZE=", BUF_SIZE
);
282 if (bsmpused
== BUF_SIZE
) {
283 // but here we should aquire lock
284 withLockedBuffer(() {
285 if (buffrmused
[curbuffer^
1] == 0) {
286 version(amper_debug_decoder
) conwriteln("decoder: curbuffer=", curbuffer
, " is full, switching buffers; used[0]=", buffrmused
[0], "; used[1]=", buffrmused
[1], "; hasmorefrms=", hasmorefrms
);
289 if (!hasmorefrms ||
(buffrmused
[0]|buffrmused
[1]) != 0) sendPing
= true;
291 // get out of decoder if current buffer is still full
292 if (buffrmused
[curbuffer
] == BUF_SIZE
/sio
.channels
) break decodeloop
;
295 //version(amper_debug_decoder) conwriteln("decoder: done; curbuffer=", curbuffer, "; used[0]=", buffrmused[0], "; used[1]=", buffrmused[1], "; sendPing=", sendPing, "; waitingForDecoder=", waitingForDecoder);
297 if (sendPing
&& waitingForDecoder
) {
298 waitingForDecoder
= false;
299 ownerTid
.send(TMsgSomeDataDecoded());
305 // ////////////////////////////////////////////////////////////////////////// //
306 __gshared
short[4096] sndplaybuf
;
308 void playerThread (Tid ownerTid
) {
310 scope(exit
) if (sio
!is null && sio
.valid
) sio
.close();
312 string newfilereq
= null;
313 bool forcestart
= false;
314 bool waitingForDecoder
= true; // waiting for decoder to warm up
317 uint realRate
= alsaGetBestSampleRate(48000);
318 conwriteln("real sampling rate: ", realRate
);
319 if (realRate
!= 44100 && realRate
!= 48000) {
321 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
324 auto decodertid
= spawn(&decoderThread
, thisTid
);
327 //conwriteln("***: hasBufferedData=", hasBufferedData, "; paused=", paused);
328 receiveTimeout((hasBufferedData
&& !paused ? Duration
.min
: 42.seconds
),
332 (TMsgPlayFileReq req
) {
333 newfilereq
= (req
.filename
.length ? req
.filename
: "");
334 forcestart
= req
.forcestart
;
336 (TMsgPlayGainReq req
) {
337 if (req
.prc
< 0) req
.prc
= 0;
338 if (req
.prc
> 200) req
.prc
= 200;
340 //conwriteln("prc=", alsaGain);
342 (TMsgStopFileReq req
) {
344 if (sio
!is null) { sio
.close(); delete sio
; }
345 decodertid
.send(TMsgReplaceSIO(null));
346 if (alsaIsOpen
) alsaShutdown();
348 (TMsgTogglePauseReq req
) {
349 if (hasBufferedData
) paused
= !paused
; else paused
= false;
352 if (hasBufferedData
) paused
= req
.pause
;
355 int newtime
= req
.timems
;
356 if (newtime
< 0) newtime
= 0;
357 version(amper_debug_decoder
) conwriteln("player: seek request, newtime=", newtime
);
358 decodertid
.send(TMsgSeekReq(newtime
));
359 waitingForDecoder
= true; // wait while decoder is warming up
361 (TMsgSomeDataDecoded req
) {
362 waitingForDecoder
= false;
363 version(amper_debug_decoder
) conwriteln("decoder sent ping; hasBufferedData=", hasBufferedData
);
369 if (newfilereq
!is null) {
371 auto reply
= new EventFileLoaded();
372 reply
.filename
= newfilereq
;
373 reply
.success
= false;
375 bool wasplaying
= hasBufferedData();
376 sio
= AudioStream
.detect(VFile(reply
.filename
));
377 if (sio
!is null && sio
.valid
) {
378 reply
.durationms
= cast(int)sio
.timeTotal
;
379 reply
.album
= sio
.album
;
380 reply
.artist
= sio
.artist
;
381 reply
.title
= sio
.title
;
382 reply
.success
= true;
383 if (forcestart
) paused
= false; else paused
= !wasplaying
;
384 // setup new parameters
385 atomicStore(aplTotalTime
, cast(int)sio
.timeTotal
);
386 atomicStore(aplSampleRate
, cast(int)sio
.rate
);
387 atomicStore(aplChannels
, cast(int)sio
.channels
);
388 withLockedBuffer(() { aplFramesFed
= 0; });
390 if (!alsaIsOpen || alsaRate
!= sio
.rate || alsaChannels
!= sio
.channels
) {
391 if (alsaIsOpen
) alsaShutdown();
392 if (!alsaInit(sio
.rate
, sio
.channels
)) assert(0, "cannot init ALSA playback");
394 waitingForDecoder
= true;
395 decodertid
.send(TMsgReplaceSIO(cast(shared)sio
)); // notify decoder that we (possibly) have new sio
396 sio
= null; // now sio is owned by decoder
398 if (alsaIsOpen
) alsaShutdown();
400 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);
401 //conwriteln("starting...");
405 if (waitingForDecoder
) continue; // still waiting for decoder to warm up
407 //conwriteln("hasBufferedData=", hasBufferedData);
408 if (!hasBufferedData
) {
410 atomicStore(aplPaused
, false);
411 atomicStore(aplPlaying
, false);
412 //atomicStore(aplCurTime, 0);
413 atomicStore(aplTotalTime
, 0);
414 withLockedBuffer(() { aplFramesFed
= 0; });
415 //conwriteln("!!! 000");
420 atomicStore(aplPaused
, false);
421 if (hasBufferedData()) {
423 if (!alsaInit(atomicLoad(aplSampleRate
), cast(ubyte)atomicLoad(aplChannels
))) assert(0, "cannot init ALSA playback");
425 uint samplesToSend
= 0;
426 withLockedBuffer(() {
427 import core
.stdc
.string
: memcpy
, memmove
;
428 // drain current buffer
429 uint playbuf
= curbuffer^
1;
430 uint frmleft
= buffrmused
[playbuf
];
432 uint chans
= atomicLoad(aplChannels
);
433 assert(chans
== 1 || chans
== 2);
434 uint loadsamples
= frmleft
*chans
;
435 if (loadsamples
> sndplaybuf
.length
) loadsamples
= cast(uint)sndplaybuf
.length
;
436 //conwriteln("loadsamples=", loadsamples, "; smpleft=", frmleft*chans, "; framesfed=", aplFramesFed);
437 atomicStore(aplPaused
, false);
438 atomicStore(aplPlaying
, true);
439 //atomicStore(aplFramesFed, atomicLoad(aplFramesFed)+loadsamples/chans);
440 aplFramesFed
+= loadsamples
/chans
;
441 // copy bytes to play
442 memcpy(sndplaybuf
.ptr
, buffers
[playbuf
].ptr
, loadsamples
*2);
443 // remove bytes from buffer
444 uint oldsmp
= buffrmused
[playbuf
]*chans
;
445 uint delsmp
= loadsamples
;
446 assert(delsmp
<= oldsmp
);
447 if (delsmp
!= oldsmp
) memmove(buffers
[playbuf
].ptr
, buffers
[playbuf
].ptr
+delsmp
, (oldsmp
-delsmp
)*2);
448 buffrmused
[playbuf
] -= delsmp
/chans
;
449 samplesToSend
= loadsamples
;
450 // ping decoder (we want more data)
451 if (buffrmused
[playbuf
] == 0) decodertid
.send(TMsgPingDecoder());
453 //conwriteln("004: ", alsaIsOpen, "; ", samplesToSend, " : ", sndplaybuf.length);
454 alsaWriteShort(sndplaybuf
[0..samplesToSend
]);
456 if (!hasBufferedData
) {
457 if (alsaIsOpen
) alsaShutdown(); // so it will finish playing
458 atomicStore(aplPaused
, false);
459 atomicStore(aplPlaying
, false);
460 withLockedBuffer(() { aplFramesFed
= 0; });
461 atomicStore(aplTotalTime
, 0);
462 //conwriteln("!!! 001");
463 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(new EventFileComplete());
464 decodertid
.send(TMsgReplaceSIO(null)); // no more
467 atomicStore(aplPlaying
, true);
468 atomicStore(aplPaused
, true);
469 if (alsaIsOpen
) alsaShutdown();
474 decodertid
.send(TMsgQuitReq());
478 // ////////////////////////////////////////////////////////////////////////// //
479 shared static this () {
482 conRegVar
!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
483 conRegVar
!alsaDevice("device", "audio output device");
484 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
485 conRegVar
!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
486 conRegVar
!alsaEnableResampling("use_resampling", "allow audio resampling?");
487 conRegVar
!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
489 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
490 conRegFunc
!((int idx
, byte value
) {
491 if (value
< -70) value
= -70;
492 if (value
> 30) value
= 30;
493 if (idx
>= 0 && idx
< alsaEqBands
.length
) {
494 if (alsaEqBands
[idx
] != value
) {
495 alsaEqBands
[idx
] = value
;
498 conwriteln("invalid equalizer band index: ", idx
);
500 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
504 })("eq_reset", "reset equalizer");
508 // ////////////////////////////////////////////////////////////////////////// //
509 public class EventFileScanned
{ string filename
; string album
; string artist
; string title
; int durationms
; bool success
; }
510 // reply with EventFileScanned
511 struct TMsgScanFileReq
{ string fname
; }
512 struct TMsgScanCancelReq
{ string fname
; }
514 __gshared
bool[string
] scanQueue
;
517 public void aplayQueueScan(T
:const(char)[]) (T filename
) {
518 static if (is(T
== typeof(null))) {
521 static if (is(T
== string
)) alias fn
= filename
; else auto fn
= filename
.idup
;
522 if (scannerStarted
) {
523 //conwriteln("zqueued: '", filename, "'...");
524 scannertid
.send(TMsgScanFileReq(fn
));
526 //conwriteln("xqueued: '", filename, "'...");
527 scanQueue
[fn
] = true;
533 public void aplayCancelScan(T
:const(char)[]) (T filename
) {
534 static if (is(T
== typeof(null))) {
537 if (scannerStarted
) {
538 static if (is(T
== string
)) alias fn
= filename
; else auto fn
= filename
.idup
;
539 scannertid
.send(TMsgScanCancelReq(fn
));
541 //conwriteln("xdequeued: '", filename, "'...");
542 scanQueue
.remove(filename
);
548 void scannerThread (Tid ownerTid
) {
550 //conwriteln("scan tread started...");
552 receiveTimeout((scanQueue
.length ? Duration
.min
: 1.hours
),
556 (TMsgScanFileReq req
) {
557 scanQueue
[req
.fname
] = true;
558 //conwriteln("queued: '", req.fname, "'...");
560 (TMsgScanCancelReq req
) {
561 //conwriteln("dequeued: '", req.fname, "'...");
562 scanQueue
.remove(req
.fname
);
567 if (scanQueue
.length
== 0) continue;
569 string fname
= scanQueue
.byKey
.front
;
570 EventFileScanned reply
= new EventFileScanned();
571 reply
.filename
= scanQueue
.byKey
.front
;
572 reply
.success
= false;
573 //conwriteln("scanning '", reply.filename, "'...");
574 auto sio
= AudioStream
.detect(VFile(reply
.filename
), true); // only metadata
575 if (sio
!is null && sio
.valid
) {
576 reply
.album
= sio
.album
;
577 reply
.artist
= sio
.artist
;
578 reply
.title
= sio
.title
;
579 reply
.durationms
= cast(int)sio
.timeTotal
;
580 reply
.success
= true;
583 try { sio
.close(); } catch (Exception e
) {}
586 scanQueue
.remove(reply
.filename
);
587 //conwriteln(" scanned '", reply.filename, "': ", reply.success);
588 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);