pause is working again
[amper.git] / aplayer.d
blob5da0f38294c4d530e47334568cda28360665095f
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 //version = amper_debug_decoder;
22 import core.atomic;
23 import core.time;
25 import std.concurrency;
26 import std.datetime;
28 import arsd.simpledisplay;
30 import iv.audiostream;
31 import iv.cmdcon;
32 import iv.cmdcongl;
33 import iv.simplealsa;
34 import iv.strex;
35 import iv.utfutil;
36 import iv.vfs;
39 // ////////////////////////////////////////////////////////////////////////// //
40 __gshared bool playerStarted = false;
41 __gshared bool scannerStarted = false;
43 __gshared Tid playertid;
44 __gshared Tid scannertid;
47 // ////////////////////////////////////////////////////////////////////////// //
48 public class EventFileLoaded { string filename; string album; string artist; string title; int durationms; bool success; }
49 public class EventFileComplete {}
51 // ////////////////////////////////////////////////////////////////////////// //
52 struct TMsgQuitReq {}
54 public void aplayStart () {
55 if (!playerStarted) {
56 playertid = spawn(&playerThread, thisTid);
57 playerStarted = true;
61 public void aplayStartScanner () {
62 if (!scannerStarted) {
63 scannertid = spawn(&scannerThread, thisTid);
64 scannerStarted = true;
68 public void aplayShutdown () {
69 if (playerStarted) {
70 playerStarted = false;
71 playertid.send(TMsgQuitReq());
73 if (scannerStarted) {
74 scannerStarted = false;
75 scannertid.send(TMsgQuitReq());
80 // ////////////////////////////////////////////////////////////////////////// //
81 // reply with EventFileLoaded
82 struct TMsgPlayFileReq { string filename; bool forcestart; }
83 struct TMsgStopFileReq {}
84 struct TMsgTogglePauseReq {}
85 struct TMsgPauseReq { bool pause; }
86 struct TMsgSeekReq { int timems; }
88 //TODO: queue this
89 public void aplayPlayFile (string fname, bool forcestart) { if (playerStarted) playertid.send(TMsgPlayFileReq(fname.length ? fname : "", forcestart)); }
90 public void aplayStopFile () { if (playerStarted) playertid.send(TMsgStopFileReq()); }
91 public void aplayTogglePause () { if (playerStarted) playertid.send(TMsgTogglePauseReq()); }
92 public void aplayPause (bool pause) { if (playerStarted) playertid.send(TMsgPauseReq(pause)); }
93 public void aplaySeekMS (int timems) { if (playerStarted) playertid.send(TMsgSeekReq(timems)); }
96 // ////////////////////////////////////////////////////////////////////////// //
97 // convert from [-20..20] to [0..27]
98 public int aplayGetEqBand (int idx) {
99 int v = 14;
100 if (idx >= 0 && idx < 10) {
101 v = alsaEqBands[idx];
102 if (v < -20) v = -20; else if (v > 20) v = 20;
103 v = 27*(v+20)/40;
105 return v;
108 // convert from [0..27] to [-20..20]
109 public void aplaySetEqBand (int idx, int v) {
110 if (idx >= 0 && idx < 10) {
111 if (v < 0) v = 0; else if (v > 27) v = 27;
112 v = (v != 14 ? (40*v/27)-20 : 0);
113 alsaEqBands[idx] = v;
118 // ////////////////////////////////////////////////////////////////////////// //
119 // reply with EventFileLoaded
120 struct TMsgPlayGainReq {
121 int prc; // percents
124 public void aplayPlayGain (int prc) { if (playerStarted) playertid.send(TMsgPlayGainReq(prc)); else alsaGain = prc; }
125 public int aplayPlayGain () { return alsaGain; }
128 // ////////////////////////////////////////////////////////////////////////// //
129 public bool aplayIsPlaying () { return atomicLoad(aplPlaying); }
130 public bool aplayIsPaused () { return atomicLoad(aplPaused); }
131 //public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
132 //public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
133 public int aplayCurTime () { lockBuffer(); scope(exit) unlockBuffer(); return cast(int)(aplFramesFed/atomicLoad(aplSampleRate)); }
134 public int aplayCurTimeMS () { lockBuffer(); scope(exit) unlockBuffer(); return cast(int)(aplFramesFed*1000/atomicLoad(aplSampleRate)); }
135 public int aplayTotalTime () { return atomicLoad(aplTotalTime)/1000; }
136 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime); }
138 public int aplaySampleRate () { return atomicLoad(aplSampleRate); }
139 public bool aplayIsStereo () { return (atomicLoad(aplChannels) == 2); }
142 // ////////////////////////////////////////////////////////////////////////// //
143 shared bool aplPlaying = false;
144 shared bool aplPaused = false;
145 //shared int aplCurTime = 0;
146 shared int aplTotalTime = 0;
147 shared int aplSampleRate = 48000;
148 shared int aplChannels = 2;
149 __gshared ulong aplFramesFed = 0; // for current track
152 // ////////////////////////////////////////////////////////////////////////// //
153 enum BUF_SIZE = 4096*10;
154 // read/write only when buffer is locked!
155 __gshared short[BUF_SIZE][2] buffers;
156 __gshared uint curbuffer = 0; // current buffer decoder is filling; alternate buffer *can* have some data
157 __gshared uint[2] buffrmused = 0; // for both buffers
158 shared bool buflocked = false;
161 void lockBuffer () nothrow @nogc {
162 import core.atomic;
163 while (!cas(&buflocked, false, true)) {}
166 void unlockBuffer () nothrow @nogc {
167 import core.atomic;
168 atomicStore(buflocked, false);
172 void withLockedBuffer (scope void delegate () dg) {
173 lockBuffer();
174 scope(exit) unlockBuffer();
175 dg();
179 bool hasBufferedData () nothrow @nogc {
180 lockBuffer();
181 scope(exit) unlockBuffer();
182 return ((buffrmused[0]|buffrmused[1]) != 0);
186 // ////////////////////////////////////////////////////////////////////////// //
187 struct TMsgPingDecoder {}
188 struct TMsgSomeDataDecoded {}
189 struct TMsgReplaceSIO { shared AudioStream sio; }
191 // audio decoding thread; should keep buffer filled
192 void decoderThread (Tid ownerTid) {
193 AudioStream sio = null;
194 bool hasmorefrms = false;
195 int newtime = -666;
196 bool doQuit = false;
197 bool waitingForDecoder = true; // waiting for decoder to warm up
198 version(amper_debug_decoder) conwriteln("decoder thread started");
199 while (!doQuit) {
200 bool longWait = true;
201 if (sio is null || !sio.valid) hasmorefrms = false;
202 withLockedBuffer((){
203 if (!hasmorefrms) { longWait = ((buffrmused[0]|buffrmused[1]) == 0); return; }
204 if (buffrmused[curbuffer] == BUF_SIZE/sio.channels) { longWait = (buffrmused[curbuffer^1] != 0); return; }
205 longWait = false;
207 receiveTimeout((longWait ? 2.hours : Duration.min),
208 (TMsgQuitReq req) {
209 doQuit = true;
211 (TMsgSeekReq req) {
212 newtime = req.timems;
213 if (newtime < 0) newtime = 0;
214 version(amper_debug_decoder) conwriteln("decoder: seek request, newtime=", newtime);
215 withLockedBuffer(() {
216 buffrmused[] = 0;
217 curbuffer = 0;
218 hasmorefrms = (sio !is null && sio.valid);
220 waitingForDecoder = true;
222 (TMsgPingDecoder req) {
223 version(amper_debug_decoder) conwriteln("decoder: ping");
224 // do nothing here, this is just a ping to go on with decoding
226 (TMsgReplaceSIO req) {
227 AudioStream newsio = cast()req.sio; // remove `shared`
228 if (sio !is newsio) {
229 if (sio !is null) { sio.close(); delete sio; }
231 sio = newsio;
232 withLockedBuffer(() {
233 curbuffer = 0;
234 buffrmused[] = 0;
235 hasmorefrms = (sio !is null && sio.valid);
237 waitingForDecoder = true;
240 if (doQuit) break;
241 bool sendPing = false;
242 decodeloop: for (;;) {
243 if (sio is null || !sio.valid) hasmorefrms = false;
244 if (!hasmorefrms) {
245 sendPing = true;
246 newtime = -666;
247 // switch buffers if necessary
248 withLockedBuffer(() {
249 if (buffrmused[curbuffer^1] == 0) curbuffer ^= 1;
251 break decodeloop;
253 // seek?
254 if (newtime != -666) {
255 version(amper_debug_decoder) conwriteln("decoder: seeking to ", newtime);
256 int tm = newtime;
257 newtime = -666;
258 assert(sio !is null && sio.valid);
259 if (tm >= sio.timeTotal) tm = (sio.timeTotal ? sio.timeTotal-1 : 0);
260 sio.seekToTime(cast(uint)tm);
261 //conwriteln("tm=", tm, "; timeRead=", sio.timeRead, "; framesRead=", sio.framesRead, "; frok=", sio.timeRead*sio.rate/1000);
262 aplFramesFed = sio.framesRead;
263 hasmorefrms = (sio !is null && sio.valid);
264 continue decodeloop;
266 // it is safe to work with current buffer without the lock here
267 uint bsmpused = buffrmused[curbuffer]*sio.channels;
268 version(amper_debug_decoder) conwriteln("decoder: curbuffer=", curbuffer, "; used[0]=", buffrmused[0], "; used[1]=", buffrmused[1]);
269 // fill current buffer, switch to next
270 if (bsmpused < BUF_SIZE) {
271 // decode
272 int frmread = sio.readFrames(buffers[curbuffer].ptr+bsmpused, (BUF_SIZE-bsmpused)/sio.channels);
273 version(amper_debug_decoder) conwriteln("decoder: frmread=", frmread);
274 if (frmread <= 0) {
275 hasmorefrms = false; // no more frames, we're done
276 } else {
277 bsmpused = (buffrmused[curbuffer] += cast(uint)frmread)*sio.channels;
279 assert(bsmpused <= BUF_SIZE);
281 // switch buffers, if alternate buffer was drained (and fill it)
282 version(amper_debug_decoder) conwriteln("decoder: bsmpused=", bsmpused, "; BUF_SIZE=", BUF_SIZE);
283 if (bsmpused == BUF_SIZE) {
284 // but here we should aquire lock
285 withLockedBuffer(() {
286 if (buffrmused[curbuffer^1] == 0) {
287 version(amper_debug_decoder) conwriteln("decoder: curbuffer=", curbuffer, " is full, switching buffers; used[0]=", buffrmused[0], "; used[1]=", buffrmused[1], "; hasmorefrms=", hasmorefrms);
288 curbuffer ^= 1;
290 if (!hasmorefrms || (buffrmused[0]|buffrmused[1]) != 0) sendPing = true;
292 // get out of decoder if current buffer is still full
293 if (buffrmused[curbuffer] == BUF_SIZE/sio.channels) break decodeloop;
296 //version(amper_debug_decoder) conwriteln("decoder: done; curbuffer=", curbuffer, "; used[0]=", buffrmused[0], "; used[1]=", buffrmused[1], "; sendPing=", sendPing, "; waitingForDecoder=", waitingForDecoder);
297 // ping owner thread
298 if (sendPing && waitingForDecoder) {
299 waitingForDecoder = false;
300 ownerTid.send(TMsgSomeDataDecoded());
306 // ////////////////////////////////////////////////////////////////////////// //
307 __gshared short[4096] sndplaybuf;
309 void playerThread (Tid ownerTid) {
310 AudioStream sio;
311 scope(exit) if (sio !is null && sio.valid) sio.close();
312 bool doQuit = false;
313 string newfilereq = null;
314 bool forcestart = false;
315 bool waitingForDecoder = true; // waiting for decoder to warm up
316 bool paused = false;
318 uint realRate = alsaGetBestSampleRate(48000);
319 conwriteln("real sampling rate: ", realRate);
320 if (realRate != 44100 && realRate != 48000) {
321 realRate = 48000;
322 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
325 auto decodertid = spawn(&decoderThread, thisTid);
327 while (!doQuit) {
328 //conwriteln("***: hasBufferedData=", hasBufferedData, "; paused=", paused);
329 receiveTimeout((hasBufferedData && !paused ? Duration.min : 42.seconds),
330 (TMsgQuitReq req) {
331 doQuit = true;
333 (TMsgPlayFileReq req) {
334 newfilereq = (req.filename.length ? req.filename : "");
335 forcestart = req.forcestart;
337 (TMsgPlayGainReq req) {
338 if (req.prc < 0) req.prc = 0;
339 if (req.prc > 200) req.prc = 200;
340 alsaGain = req.prc;
341 //conwriteln("prc=", alsaGain);
343 (TMsgStopFileReq req) {
344 paused = false;
345 if (sio !is null) { sio.close(); delete sio; }
346 decodertid.send(TMsgReplaceSIO(null));
347 if (alsaIsOpen) alsaShutdown();
349 (TMsgTogglePauseReq req) {
350 if (hasBufferedData) paused = !paused; else paused = false;
352 (TMsgPauseReq req) {
353 if (hasBufferedData) paused = req.pause;
355 (TMsgSeekReq req) {
356 int newtime = req.timems;
357 if (newtime < 0) newtime = 0;
358 version(amper_debug_decoder) conwriteln("player: seek request, newtime=", newtime);
359 decodertid.send(TMsgSeekReq(newtime));
360 waitingForDecoder = true; // wait while decoder is warming up
362 (TMsgSomeDataDecoded req) {
363 waitingForDecoder = false;
364 version(amper_debug_decoder) conwriteln("decoder sent ping; hasBufferedData=", hasBufferedData);
368 if (doQuit) break;
370 if (newfilereq !is null) {
371 // create new sio
372 auto reply = new EventFileLoaded();
373 reply.filename = newfilereq;
374 reply.success = false;
375 newfilereq = null;
376 bool wasplaying = hasBufferedData();
377 sio = AudioStream.detect(VFile(reply.filename));
378 if (sio !is null && sio.valid) {
379 reply.durationms = cast(int)sio.timeTotal;
380 reply.album = sio.album;
381 reply.artist = sio.artist;
382 reply.title = sio.title;
383 reply.success = true;
384 if (forcestart) paused = false; else paused = !wasplaying;
385 // setup new parameters
386 atomicStore(aplTotalTime, cast(int)sio.timeTotal);
387 atomicStore(aplSampleRate, cast(int)sio.rate);
388 atomicStore(aplChannels, cast(int)sio.channels);
389 withLockedBuffer(() { aplFramesFed = 0; });
390 // (re)init alsa
391 if (!alsaIsOpen || alsaRate != sio.rate || alsaChannels != sio.channels) {
392 if (alsaIsOpen) alsaShutdown();
393 if (!alsaInit(sio.rate, sio.channels)) assert(0, "cannot init ALSA playback");
395 waitingForDecoder = true;
396 decodertid.send(TMsgReplaceSIO(cast(shared)sio)); // notify decoder that we (possibly) have new sio
397 sio = null; // now sio is owned by decoder
398 } else {
399 if (alsaIsOpen) alsaShutdown();
401 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
402 //conwriteln("starting...");
403 continue;
406 if (waitingForDecoder) continue; // still waiting for decoder to warm up
408 //conwriteln("hasBufferedData=", hasBufferedData);
409 if (!hasBufferedData) {
410 paused = false;
411 atomicStore(aplPaused, false);
412 atomicStore(aplPlaying, false);
413 //atomicStore(aplCurTime, 0);
414 atomicStore(aplTotalTime, 0);
415 withLockedBuffer(() { aplFramesFed = 0; });
416 //conwriteln("!!! 000");
417 continue;
420 if (!paused) {
421 atomicStore(aplPaused, false);
422 if (hasBufferedData()) {
423 if (!alsaIsOpen) {
424 if (!alsaInit(atomicLoad(aplSampleRate), cast(ubyte)atomicLoad(aplChannels))) assert(0, "cannot init ALSA playback");
426 uint samplesToSend = 0;
427 withLockedBuffer(() {
428 import core.stdc.string : memcpy, memmove;
429 // drain current buffer
430 uint playbuf = curbuffer^1;
431 uint frmleft = buffrmused[playbuf];
432 assert(frmleft > 0);
433 uint chans = atomicLoad(aplChannels);
434 assert(chans == 1 || chans == 2);
435 uint loadsamples = frmleft*chans;
436 if (loadsamples > sndplaybuf.length) loadsamples = cast(uint)sndplaybuf.length;
437 //conwriteln("loadsamples=", loadsamples, "; smpleft=", frmleft*chans, "; framesfed=", aplFramesFed);
438 atomicStore(aplPaused, false);
439 atomicStore(aplPlaying, true);
440 //atomicStore(aplFramesFed, atomicLoad(aplFramesFed)+loadsamples/chans);
441 aplFramesFed += loadsamples/chans;
442 // copy bytes to play
443 memcpy(sndplaybuf.ptr, buffers[playbuf].ptr, loadsamples*2);
444 // remove bytes from buffer
445 uint oldsmp = buffrmused[playbuf]*chans;
446 uint delsmp = loadsamples;
447 assert(delsmp <= oldsmp);
448 if (delsmp != oldsmp) memmove(buffers[playbuf].ptr, buffers[playbuf].ptr+delsmp, (oldsmp-delsmp)*2);
449 buffrmused[playbuf] -= delsmp/chans;
450 samplesToSend = loadsamples;
451 // ping decoder (we want more data)
452 if (buffrmused[playbuf] == 0) decodertid.send(TMsgPingDecoder());
454 //conwriteln("004: ", alsaIsOpen, "; ", samplesToSend, " : ", sndplaybuf.length);
455 alsaWriteShort(sndplaybuf[0..samplesToSend]);
457 if (!hasBufferedData) {
458 if (alsaIsOpen) alsaShutdown(); // so it will finish playing
459 atomicStore(aplPaused, false);
460 atomicStore(aplPlaying, false);
461 withLockedBuffer(() { aplFramesFed = 0; });
462 atomicStore(aplTotalTime, 0);
463 //conwriteln("!!! 001");
464 if (glconCtlWindow !is null) glconCtlWindow.postEvent(new EventFileComplete());
465 decodertid.send(TMsgReplaceSIO(null)); // no more
467 } else {
468 atomicStore(aplPlaying, true);
469 atomicStore(aplPaused, true);
470 if (alsaIsOpen) alsaShutdown();
474 // quited
475 decodertid.send(TMsgQuitReq());
479 // ////////////////////////////////////////////////////////////////////////// //
480 shared static this () {
481 alsaEqBands[] = 0;
483 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
484 conRegVar!alsaDevice("device", "audio output device");
485 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
486 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
487 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
488 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
490 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
491 conRegFunc!((int idx, byte value) {
492 if (value < -70) value = -70;
493 if (value > 30) value = 30;
494 if (idx >= 0 && idx < alsaEqBands.length) {
495 if (alsaEqBands[idx] != value) {
496 alsaEqBands[idx] = value;
498 } else {
499 conwriteln("invalid equalizer band index: ", idx);
501 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
503 conRegFunc!(() {
504 alsaEqBands[] = 0;
505 })("eq_reset", "reset equalizer");
509 // ////////////////////////////////////////////////////////////////////////// //
510 public class EventFileScanned { string filename; string album; string artist; string title; int durationms; bool success; }
511 // reply with EventFileScanned
512 struct TMsgScanFileReq { string fname; }
513 struct TMsgScanCancelReq { string fname; }
515 __gshared bool[string] scanQueue;
518 public void aplayQueueScan(T:const(char)[]) (T filename) {
519 static if (is(T == typeof(null))) {
520 aplayQueueScan("");
521 } else {
522 static if (is(T == string)) alias fn = filename; else auto fn = filename.idup;
523 if (scannerStarted) {
524 //conwriteln("zqueued: '", filename, "'...");
525 scannertid.send(TMsgScanFileReq(fn));
526 } else {
527 //conwriteln("xqueued: '", filename, "'...");
528 scanQueue[fn] = true;
534 public void aplayCancelScan(T:const(char)[]) (T filename) {
535 static if (is(T == typeof(null))) {
536 aplayQueueScan("");
537 } else {
538 if (scannerStarted) {
539 static if (is(T == string)) alias fn = filename; else auto fn = filename.idup;
540 scannertid.send(TMsgScanCancelReq(fn));
541 } else {
542 //conwriteln("xdequeued: '", filename, "'...");
543 scanQueue.remove(filename);
549 void scannerThread (Tid ownerTid) {
550 bool doQuit = false;
551 //conwriteln("scan tread started...");
552 while (!doQuit) {
553 receiveTimeout((scanQueue.length ? Duration.min : 1.hours),
554 (TMsgQuitReq req) {
555 doQuit = true;
557 (TMsgScanFileReq req) {
558 scanQueue[req.fname] = true;
559 //conwriteln("queued: '", req.fname, "'...");
561 (TMsgScanCancelReq req) {
562 //conwriteln("dequeued: '", req.fname, "'...");
563 scanQueue.remove(req.fname);
567 if (doQuit) break;
568 if (scanQueue.length == 0) continue;
570 string fname = scanQueue.byKey.front;
571 EventFileScanned reply = new EventFileScanned();
572 reply.filename = scanQueue.byKey.front;
573 reply.success = false;
574 //conwriteln("scanning '", reply.filename, "'...");
575 auto sio = AudioStream.detect(VFile(reply.filename), true); // only metadata
576 if (sio !is null && sio.valid) {
577 reply.album = sio.album;
578 reply.artist = sio.artist;
579 reply.title = sio.title;
580 reply.durationms = cast(int)sio.timeTotal;
581 reply.success = true;
583 if (sio !is null) {
584 try { sio.close(); } catch (Exception e) {}
585 delete sio;
587 scanQueue.remove(reply.filename);
588 //conwriteln(" scanned '", reply.filename, "': ", reply.success);
589 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);