switched to GPLv3 ONLY, because i don't trust FSF anymore
[amper.git] / aplayer.d
blob778d45cd59d8547b4a4ba94651664fca7597dcf3
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/>.
16 module aplayer;
17 private:
19 //version = amper_debug_decoder;
21 import core.atomic;
22 import core.time;
24 import std.concurrency;
25 import std.datetime;
27 import arsd.simpledisplay;
29 import iv.audiostream;
30 import iv.cmdcon;
31 import iv.cmdcongl;
32 import iv.simplealsa;
33 import iv.strex;
34 import iv.utfutil;
35 import iv.vfs;
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 // ////////////////////////////////////////////////////////////////////////// //
51 struct TMsgQuitReq {}
53 public void aplayStart () {
54 if (!playerStarted) {
55 playertid = spawn(&playerThread, thisTid);
56 playerStarted = true;
60 public void aplayStartScanner () {
61 if (!scannerStarted) {
62 scannertid = spawn(&scannerThread, thisTid);
63 scannerStarted = true;
67 public void aplayShutdown () {
68 if (playerStarted) {
69 playerStarted = false;
70 playertid.send(TMsgQuitReq());
72 if (scannerStarted) {
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; }
87 //TODO: queue this
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) {
98 int v = 14;
99 if (idx >= 0 && idx < 10) {
100 v = alsaEqBands[idx];
101 if (v < -20) v = -20; else if (v > 20) v = 20;
102 v = 27*(v+20)/40;
104 return v;
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 {
120 int prc; // percents
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 {
161 import core.atomic;
162 while (!cas(&buflocked, false, true)) {}
165 void unlockBuffer () nothrow @nogc {
166 import core.atomic;
167 atomicStore(buflocked, false);
171 void withLockedBuffer (scope void delegate () dg) {
172 lockBuffer();
173 scope(exit) unlockBuffer();
174 dg();
178 bool hasBufferedData () nothrow @nogc {
179 lockBuffer();
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;
194 int newtime = -666;
195 bool doQuit = false;
196 bool waitingForDecoder = true; // waiting for decoder to warm up
197 version(amper_debug_decoder) conwriteln("decoder thread started");
198 while (!doQuit) {
199 bool longWait = true;
200 if (sio is null || !sio.valid) hasmorefrms = false;
201 withLockedBuffer((){
202 if (!hasmorefrms) { longWait = ((buffrmused[0]|buffrmused[1]) == 0); return; }
203 if (buffrmused[curbuffer] == BUF_SIZE/sio.channels) { longWait = (buffrmused[curbuffer^1] != 0); return; }
204 longWait = false;
206 receiveTimeout((longWait ? 2.hours : Duration.min),
207 (TMsgQuitReq req) {
208 doQuit = true;
210 (TMsgSeekReq req) {
211 newtime = req.timems;
212 if (newtime < 0) newtime = 0;
213 version(amper_debug_decoder) conwriteln("decoder: seek request, newtime=", newtime);
214 withLockedBuffer(() {
215 buffrmused[] = 0;
216 curbuffer = 0;
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; }
230 sio = newsio;
231 withLockedBuffer(() {
232 curbuffer = 0;
233 buffrmused[] = 0;
234 hasmorefrms = (sio !is null && sio.valid);
236 waitingForDecoder = true;
239 if (doQuit) break;
240 bool sendPing = false;
241 decodeloop: for (;;) {
242 if (sio is null || !sio.valid) hasmorefrms = false;
243 if (!hasmorefrms) {
244 sendPing = true;
245 newtime = -666;
246 // switch buffers if necessary
247 withLockedBuffer(() {
248 if (buffrmused[curbuffer^1] == 0) curbuffer ^= 1;
250 break decodeloop;
252 // seek?
253 if (newtime != -666) {
254 version(amper_debug_decoder) conwriteln("decoder: seeking to ", newtime);
255 int tm = newtime;
256 newtime = -666;
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);
263 continue decodeloop;
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) {
270 // decode
271 int frmread = sio.readFrames(buffers[curbuffer].ptr+bsmpused, (BUF_SIZE-bsmpused)/sio.channels);
272 version(amper_debug_decoder) conwriteln("decoder: frmread=", frmread);
273 if (frmread <= 0) {
274 hasmorefrms = false; // no more frames, we're done
275 } else {
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);
287 curbuffer ^= 1;
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);
296 // ping owner thread
297 if (sendPing && waitingForDecoder) {
298 waitingForDecoder = false;
299 ownerTid.send(TMsgSomeDataDecoded());
305 // ////////////////////////////////////////////////////////////////////////// //
306 __gshared short[4096] sndplaybuf;
308 void playerThread (Tid ownerTid) {
309 AudioStream sio;
310 scope(exit) if (sio !is null && sio.valid) sio.close();
311 bool doQuit = false;
312 string newfilereq = null;
313 bool forcestart = false;
314 bool waitingForDecoder = true; // waiting for decoder to warm up
315 bool paused = false;
317 uint realRate = alsaGetBestSampleRate(48000);
318 conwriteln("real sampling rate: ", realRate);
319 if (realRate != 44100 && realRate != 48000) {
320 realRate = 48000;
321 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
324 auto decodertid = spawn(&decoderThread, thisTid);
326 while (!doQuit) {
327 //conwriteln("***: hasBufferedData=", hasBufferedData, "; paused=", paused);
328 receiveTimeout((hasBufferedData && !paused ? Duration.min : 42.seconds),
329 (TMsgQuitReq req) {
330 doQuit = true;
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;
339 alsaGain = req.prc;
340 //conwriteln("prc=", alsaGain);
342 (TMsgStopFileReq req) {
343 paused = false;
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;
351 (TMsgPauseReq req) {
352 if (hasBufferedData) paused = req.pause;
354 (TMsgSeekReq req) {
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);
367 if (doQuit) break;
369 if (newfilereq !is null) {
370 // create new sio
371 auto reply = new EventFileLoaded();
372 reply.filename = newfilereq;
373 reply.success = false;
374 newfilereq = null;
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; });
389 // (re)init alsa
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
397 } else {
398 if (alsaIsOpen) alsaShutdown();
400 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
401 //conwriteln("starting...");
402 continue;
405 if (waitingForDecoder) continue; // still waiting for decoder to warm up
407 //conwriteln("hasBufferedData=", hasBufferedData);
408 if (!hasBufferedData) {
409 paused = false;
410 atomicStore(aplPaused, false);
411 atomicStore(aplPlaying, false);
412 //atomicStore(aplCurTime, 0);
413 atomicStore(aplTotalTime, 0);
414 withLockedBuffer(() { aplFramesFed = 0; });
415 //conwriteln("!!! 000");
416 continue;
419 if (!paused) {
420 atomicStore(aplPaused, false);
421 if (hasBufferedData()) {
422 if (!alsaIsOpen) {
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];
431 assert(frmleft > 0);
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
466 } else {
467 atomicStore(aplPlaying, true);
468 atomicStore(aplPaused, true);
469 if (alsaIsOpen) alsaShutdown();
473 // quited
474 decodertid.send(TMsgQuitReq());
478 // ////////////////////////////////////////////////////////////////////////// //
479 shared static this () {
480 alsaEqBands[] = 0;
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;
497 } else {
498 conwriteln("invalid equalizer band index: ", idx);
500 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
502 conRegFunc!(() {
503 alsaEqBands[] = 0;
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))) {
519 aplayQueueScan("");
520 } else {
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));
525 } else {
526 //conwriteln("xqueued: '", filename, "'...");
527 scanQueue[fn] = true;
533 public void aplayCancelScan(T:const(char)[]) (T filename) {
534 static if (is(T == typeof(null))) {
535 aplayQueueScan("");
536 } else {
537 if (scannerStarted) {
538 static if (is(T == string)) alias fn = filename; else auto fn = filename.idup;
539 scannertid.send(TMsgScanCancelReq(fn));
540 } else {
541 //conwriteln("xdequeued: '", filename, "'...");
542 scanQueue.remove(filename);
548 void scannerThread (Tid ownerTid) {
549 bool doQuit = false;
550 //conwriteln("scan tread started...");
551 while (!doQuit) {
552 receiveTimeout((scanQueue.length ? Duration.min : 1.hours),
553 (TMsgQuitReq req) {
554 doQuit = true;
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);
566 if (doQuit) break;
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;
582 if (sio !is null) {
583 try { sio.close(); } catch (Exception e) {}
584 delete sio;
586 scanQueue.remove(reply.filename);
587 //conwriteln(" scanned '", reply.filename, "': ", reply.success);
588 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);