notification icon is embedded now
[amper.git] / aplayer.d
blob5757b25231ce8eb2a06875a5ddbb9d884b1b7d63
1 /*
2 Copyright (c) 2016, Ketmar // Invisible Vector
4 Redistribution and use in source and binary forms, with or without
5 modification, are permitted provided that the following conditions
6 are met:
8 - Redistributions of source code must retain the above copyright
9 notice, this list of conditions and the following disclaimer.
11 - Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
15 - Neither the name of the Xiph.org Foundation nor the names of its
16 contributors may be used to endorse or promote products derived from
17 this software without specific prior written permission.
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
23 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 module aplayer;
32 private:
34 import core.atomic;
35 import core.time;
37 import std.concurrency;
38 import std.datetime;
40 import arsd.simpledisplay;
42 import iv.cmdcon;
43 import iv.cmdcongl;
44 import iv.mbandeq;
45 import iv.simplealsa;
46 import iv.strex;
47 import iv.vfs;
49 import iv.drflac;
50 import iv.minimp3;
51 import iv.mp3scan;
52 import iv.tremor;
53 import iv.dopus;
56 // ////////////////////////////////////////////////////////////////////////// //
57 public struct AudioStream {
58 private:
59 VFile fl;
60 string type;
62 public:
63 //long timetotal; // in milliseconds
64 uint rate = 1; // just in case
65 ubyte channels = 1; // just in case
66 ulong samplestotal; // multiplied by channels
67 ulong samplesread; // samples read so far, multiplied by channels
68 string album;
69 string artist;
70 string title;
72 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline, true); return samplesread/channels; }
73 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline, true); return samplestotal/channels; }
75 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplesread*1000/rate/channels); }
76 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplestotal*1000/rate/channels); }
78 public:
79 bool valid () {
80 if (type.length == 0) return false;
81 switch (type[0]) {
82 case 'f': return (ff !is null);
83 case 'v': return (vi !is null);
84 case 'm': return mp3.valid;
85 case 'o': return (of !is null);
86 default:
88 return false;
91 @property string typestr () const pure nothrow @safe @nogc { return type; }
93 void close () {
94 if (type.length == 0) return;
95 switch (type[0]) {
96 case 'f': if (ff !is null) { drflac_close(ff); ff = null; } break;
97 case 'v': if (vi !is null) { vi = null; ov_clear(&vf); } break;
98 case 'm': if (mp3.valid) mp3.close(); break;
99 default:
101 type = null;
104 int readFrames (void* buf, int count) {
105 if (count < 1) return 0;
106 if (count > int.max/4) count = int.max/4;
107 if (!valid) return 0;
108 switch (type[0]) {
109 case 'f':
110 if (ff is null) return 0;
111 int[512] flcbuf;
112 int res = 0;
113 count *= channels;
114 short* bp = cast(short*)buf;
115 while (count > 0) {
116 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
117 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
118 if (rd <= 0) break;
119 samplesread += rd; // number of samples read
120 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
121 res += rd;
122 count -= rd;
124 return cast(int)(res/channels); // number of frames read
125 case 'v':
126 if (vi is null) return 0;
127 int currstream = 0;
128 auto ret = ov_read(&vf, cast(ubyte*)buf, count*2*channels, &currstream);
129 if (ret <= 0) return 0; // error or eof
130 samplesread += ret/2; // number of samples read
131 return ret/2/channels; // number of frames read
132 case 'o':
133 auto dptr = cast(short*)buf;
134 if (of is null) return 0;
135 int total = 0;
136 while (count > 0) {
137 while (count > 0 && smpbufpos < smpbufused) {
138 *dptr++ = smpbuf.ptr[smpbufpos++];
139 if (channels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
140 --count;
141 ++total;
142 samplesread += channels;
144 if (count == 0) break;
145 auto rd = of.readFrame();
146 if (rd.length == 0) break;
147 if (rd.length > smpbuf.length) smpbuf.length = rd.length;
148 smpbuf[0..rd.length] = rd[];
149 smpbufpos = 0;
150 smpbufused = cast(uint)rd.length;
152 return total;
153 case 'm':
154 // yes, i know that frames are not independend, and i should actually
155 // seek to a frame with a correct sync word. meh.
156 if (!mp3.valid) return 0;
157 auto mfm = mp3.frameSamples;
158 if (mp3smpused+channels > mfm.length) {
159 mp3smpused = 0;
160 if (!mp3.decodeNextFrame(&reader)) return 0;
161 mfm = mp3.frameSamples;
162 if (mp3.sampleRate != rate || mp3.channels != channels) return 0;
164 int res = 0;
165 ushort* b = cast(ushort*)buf;
166 auto oldmpu = mp3smpused;
167 while (count > 0 && mp3smpused+channels <= mfm.length) {
168 *b++ = mfm[mp3smpused++];
169 if (channels == 2) *b++ = mfm[mp3smpused++];
170 --count;
171 ++res;
173 samplesread += mp3smpused-oldmpu; // number of samples read
174 return res;
175 default: break;
177 return 0;
180 // return new frame index
181 ulong seekToTime (uint msecs) {
182 if (!valid) return 0;
183 ulong snum = cast(ulong)msecs*rate/1000*channels; // sample number
184 switch (type[0]) {
185 case 'f':
186 if (ff is null) return 0;
187 if (ff.totalSampleCount < 1) return 0;
188 if (snum >= ff.totalSampleCount) {
189 drflac_seek_to_sample(ff, 0);
190 return 0;
192 if (!drflac_seek_to_sample(ff, snum)) {
193 drflac_seek_to_sample(ff, 0);
194 return 0;
196 samplesread = snum;
197 return snum/channels;
198 case 'v':
199 if (vi is null) return 0;
200 if (ov_pcm_seek(&vf, snum/channels) == 0) {
201 samplesread = ov_pcm_tell(&vf)*channels;
202 return samplesread/channels;
204 ov_pcm_seek(&vf, 0);
205 return 0;
206 case 'o':
207 if (of is null) return 0;
208 of.seek(msecs);
209 samplesread = of.smpcurtime*channels;
210 return samplesread/channels;
211 case 'm':
212 if (!mp3.valid) return 0;
213 mp3smpused = 0;
214 if (mp3info.index.length == 0 || snum == 0) {
215 // alas, we cannot seek here
216 samplesread = 0;
217 fl.seek(0);
218 mp3.restart(&reader);
219 return 0;
221 // find frame containing our sample
222 // stupid binary search; ignore overflow bug
223 ulong start = 0;
224 ulong end = mp3info.index.length-1;
225 while (start <= end) {
226 ulong mid = (start+end)/2;
227 auto smps = mp3info.index[cast(size_t)mid].samples;
228 auto smpe = (mp3info.index.length-mid > 0 ? mp3info.index[cast(size_t)(mid+1)].samples : samplestotal);
229 if (snum >= smps && snum < smpe) {
230 // i found her!
231 samplesread = snum;
232 fl.seek(mp3info.index[cast(size_t)mid].fpos);
233 mp3smpused = cast(uint)(snum-smps);
234 mp3.sync(&reader);
235 return snum;
237 if (snum < smps) end = mid-1; else start = mid+1;
239 // alas, we cannot seek
240 samplesread = 0;
241 fl.seek(0);
242 mp3.restart(&reader);
243 return 0;
244 default: break;
246 return 0;
249 private:
250 drflac* ff;
251 MP3Decoder mp3;
252 Mp3Info mp3info; // scanned info, frame index
253 uint mp3smpused;
254 OggVorbis_File vf;
255 vorbis_info* vi;
256 OpusFile of;
257 short[] smpbuf;
258 uint smpbufpos, smpbufused;
260 int reader (void[] buf) {
261 try {
262 auto rd = fl.rawRead(buf);
263 return cast(int)rd.length;
264 } catch (Exception e) {}
265 return -1;
268 public:
269 static AudioStream open (VFile fl) {
270 //import std.string : fromStringz;
271 AudioStream sio;
272 fl.seek(0);
273 // determine format
274 try {
275 auto fpos = fl.tell;
276 // flac
277 try {
278 import core.stdc.stdio;
279 import core.stdc.stdlib : malloc, free;
280 uint commentCount;
281 char* fcmts;
282 scope(exit) if (fcmts !is null) free(fcmts);
283 sio.ff = drflac_open_file(fl, (void* pUserData, drflac_metadata* pMetadata) {
284 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
285 if (fcmts !is null) free(fcmts);
286 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
287 if (csz > 0 && csz < 0x100_0000) {
288 fcmts = cast(char*)malloc(cast(uint)csz);
289 } else {
290 fcmts = null;
292 if (fcmts is null) {
293 commentCount = 0;
294 } else {
295 import core.stdc.string : memcpy;
296 commentCount = pMetadata.data.vorbis_comment.commentCount;
297 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
301 if (sio.ff !is null) {
302 scope(failure) drflac_close(sio.ff);
303 if (sio.ff.sampleRate < 1024 || sio.ff.sampleRate > 96000) throw new Exception("fucked flac");
304 if (sio.ff.channels < 1 || sio.ff.channels > 2) throw new Exception("fucked flac");
305 sio.rate = cast(uint)sio.ff.sampleRate;
306 sio.channels = cast(ubyte)sio.ff.channels;
307 sio.type = "flac";
308 sio.fl = fl;
309 sio.samplestotal = sio.ff.totalSampleCount;
310 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
312 drflac_vorbis_comment_iterator i;
313 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
314 uint commentLength;
315 const(char)* pComment;
316 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
317 if (commentLength > 1024*1024*2) break; // just in case
318 //conwriteln(" ", pComment[0..commentLength]);
319 auto cmts = pComment[0..commentLength];
320 //conwriteln(" <", cmts, ">");
321 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
322 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
323 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
326 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
327 return sio;
329 } catch (Exception) {}
330 fl.seek(fpos);
331 // vorbis
332 try {
333 if (ov_fopen(fl, &sio.vf) == 0) {
334 scope(failure) ov_clear(&sio.vf);
335 sio.type = "vorbis";
336 sio.fl = fl;
337 sio.vi = ov_info(&sio.vf, -1);
338 if (sio.vi.rate < 1024 || sio.vi.rate > 96000) throw new Exception("fucked vorbis");
339 if (sio.vi.channels < 1 || sio.vi.channels > 2) throw new Exception("fucked vorbis");
340 sio.rate = sio.vi.rate;
341 sio.channels = cast(ubyte)sio.vi.channels;
342 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
343 //conwriteln("streams: ", ov_streams(&sio.vf));
344 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
345 sio.samplestotal = ov_pcm_total(&sio.vf)*sio.channels;
346 if (auto vc = ov_comment(&sio.vf, -1)) {
347 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
348 foreach (immutable idx; 0..vc.comments) {
349 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
350 auto cmts = vc.user_comments[idx][0..vc.comment_lengths[idx]];
351 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
352 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
353 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
356 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
357 return sio;
359 } catch (Exception) {}
360 fl.seek(fpos);
361 // opus
362 try {
363 OpusFile of = opusOpen(fl);
364 scope(failure) opusClose(of);
365 if (of.rate < 1024 || of.rate > 96000) throw new Exception("fucked opus");
366 if (of.channels < 1 || of.channels > 2) throw new Exception("fucked opus");
367 sio.of = of;
368 sio.type = "opus";
369 sio.fl = fl;
370 sio.rate = of.rate;
371 sio.channels = of.channels;
372 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
373 sio.samplestotal = of.smpduration*sio.channels;
374 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
375 foreach (immutable cidx; 0..of.commentCount) {
376 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
377 auto cmts = of.comment(cidx);
378 if (cmts.startsWithCI("ALBUM=")) sio.album = cmts[6..$].xstrip.idup;
379 else if (cmts.startsWithCI("ARTIST=")) sio.artist = cmts[7..$].xstrip.idup;
380 else if (cmts.startsWithCI("TITLE=")) sio.title = cmts[6..$].xstrip.idup;
382 //TODO: comments
383 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
384 return sio;
385 } catch (Exception) {}
386 fl.seek(fpos);
387 // mp3
388 try {
389 sio.fl = fl;
390 sio.mp3 = new MP3Decoder(&sio.reader);
391 sio.type = "mp3";
392 if (sio.mp3.valid) {
393 // scan file to determine number of frames
394 auto xfp = fl.tell;
395 fl.seek(fpos);
396 sio.mp3info = mp3Scan!true((void[] buf) => cast(int)fl.rawRead(buf).length); // build index too
397 fl.seek(xfp);
398 if (sio.mp3info.valid) {
399 if (sio.mp3.sampleRate < 1024 || sio.mp3.sampleRate > 96000) throw new Exception("fucked mp3");
400 if (sio.mp3.channels < 1 || sio.mp3.channels > 2) throw new Exception("fucked mp3");
401 sio.rate = sio.mp3.sampleRate;
402 sio.channels = sio.mp3.channels;
403 sio.samplestotal = sio.mp3info.samples;
404 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
405 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
406 return sio;
409 sio.mp3 = null;
410 } catch (Exception) {}
411 sio.mp3 = null;
412 fl.seek(fpos);
413 } catch (Exception) {}
414 return AudioStream.init;
419 // ////////////////////////////////////////////////////////////////////////// //
420 __gshared Tid playertid;
423 // ////////////////////////////////////////////////////////////////////////// //
424 public class EventFileLoaded { string filename; string album; string artist; string title; int duration; bool success; }
425 public class EventFileComplete {}
427 // ////////////////////////////////////////////////////////////////////////// //
428 __gshared bool started = false;
429 struct TMsgQuitReq {}
431 public void aplayStart () { if (!started) { started = true; playertid = spawn(&playerThread, thisTid); } }
432 public void aplayShutdown () { if (started) { started = false; playertid.send(TMsgQuitReq()); } }
435 // ////////////////////////////////////////////////////////////////////////// //
436 // reply with EventFileLoaded
437 struct TMsgPlayFileReq { string filename; bool forcestart; }
438 struct TMsgStopFileReq {}
439 struct TMsgTogglePauseReq {}
440 struct TMsgPauseReq { bool pause; }
441 struct TMsgSeekReq { int timems; }
443 //TODO: queue this
444 public void aplayPlayFile (string fname, bool forcestart) { if (started) playertid.send(TMsgPlayFileReq(fname.length ? fname : "", forcestart)); }
445 public void aplayStopFile () { if (started) playertid.send(TMsgStopFileReq()); }
446 public void aplayTogglePause () { if (started) playertid.send(TMsgTogglePauseReq()); }
447 public void aplayPause (bool pause) { if (started) playertid.send(TMsgPauseReq(pause)); }
448 public void aplaySeekMS (int timems) { if (started) playertid.send(TMsgSeekReq(timems)); }
451 // ////////////////////////////////////////////////////////////////////////// //
452 // reply with EventFileLoaded
453 struct TMsgPlayGainReq {
454 int prc; // percents
457 public void aplayPlayGain (int prc) { if (started) playertid.send(TMsgPlayGainReq(prc)); else alsaGain = prc; }
460 // ////////////////////////////////////////////////////////////////////////// //
461 public bool aplayIsPlaying () { return atomicLoad(aplPlaying); }
462 public bool aplayIsPaused () { return atomicLoad(aplPaused); }
463 public int aplayCurTime () { return atomicLoad(aplCurTime)/1000; }
464 public int aplayTotalTime () { return atomicLoad(aplTotalTime)/1000; }
465 public int aplayCurTimeMS () { return atomicLoad(aplCurTime); }
466 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime); }
468 public int aplaySampleRate () { return atomicLoad(aplSampleRate); }
469 public bool aplayIsStereo () { return (atomicLoad(aplChannels) == 2); }
472 // ////////////////////////////////////////////////////////////////////////// //
473 enum BUF_SIZE = 4096;
474 __gshared short[BUF_SIZE] buffer;
476 __gshared bool paused = false;
477 shared bool aplPlaying = false;
478 shared bool aplPaused = false;
479 shared int aplCurTime = 0;
480 shared int aplTotalTime = 0;
481 shared int aplSampleRate = 48000;
482 shared int aplChannels = 2;
485 // ////////////////////////////////////////////////////////////////////////// //
486 void playerThread (Tid ownerTid) {
487 AudioStream sio;
488 scope(exit) sio.close();
489 bool doQuit = false;
490 string newfilereq = null;
491 bool forcestart = false;
492 int newtime = -666;
494 uint realRate = alsaGetBestSampleRate(sio.rate);
495 conwriteln("real sampling rate: ", realRate);
497 while (!doQuit) {
498 receiveTimeout((sio.valid && !paused ? Duration.min : 42.seconds),
499 (TMsgQuitReq req) {
500 doQuit = true;
502 (TMsgPlayFileReq req) {
503 newfilereq = (req.filename.length ? req.filename : "");
504 forcestart = req.forcestart;
506 (TMsgPlayGainReq req) {
507 alsaGain = req.prc;
509 (TMsgStopFileReq req) {
510 paused = false;
511 sio.close();
512 if (alsaIsOpen) alsaShutdown();
514 (TMsgTogglePauseReq req) {
515 if (sio.valid) paused = !paused; else paused = false;
517 (TMsgPauseReq req) {
518 if (sio.valid) paused = req.pause;
520 (TMsgSeekReq req) {
521 newtime = req.timems;
522 if (newtime < 0) newtime = 0;
526 if (doQuit) break;
528 if (newfilereq !is null) {
529 newtime = -666;
530 auto reply = new EventFileLoaded();
531 reply.filename = newfilereq;
532 reply.success = false;
533 newfilereq = null;
534 bool wasplaying = sio.valid;
535 sio.close();
536 try {
537 sio = AudioStream.open(VFile(reply.filename));
538 } catch (Exception e) {
539 sio = AudioStream.init;
541 if (sio.valid) {
542 reply.duration = cast(int)(sio.timetotal/1000);
543 reply.album = sio.album;
544 reply.artist = sio.artist;
545 reply.title = sio.title;
546 reply.success = true;
547 if (forcestart) paused = false; else paused = !wasplaying;
548 } else {
549 if (alsaIsOpen) alsaShutdown();
551 if (glconCtlWindow !is null) glconCtlWindow.postEvent(reply);
554 if (!sio.valid) {
555 newtime = -666;
556 paused = false;
557 atomicStore(aplPaused, false);
558 atomicStore(aplPlaying, false);
559 atomicStore(aplCurTime, 0);
560 atomicStore(aplTotalTime, 0);
561 //atomicStore(aplSampleRate, 48000);
562 //atomicStore(aplChannels, 2);
563 continue;
566 if (newtime != -666) {
567 int tm = newtime;
568 newtime = -666;
569 if (tm >= sio.timetotal) tm = (sio.timetotal ? sio.timetotal-1 : 0);
570 sio.seekToTime(cast(uint)tm);
573 if (!paused) {
574 if (!alsaIsOpen /*|| alsaRate != sio.rate*/ || alsaChannels != sio.channels) {
575 if (alsaIsOpen) alsaShutdown();
576 if (!alsaInit(/*sio.rate*/realRate, sio.channels)) assert(0, "cannot init ALSA playback");
578 auto frmread = sio.readFrames(buffer.ptr, BUF_SIZE/sio.channels);
579 if (frmread <= 0) {
580 atomicStore(aplPaused, false);
581 atomicStore(aplPlaying, false);
582 atomicStore(aplCurTime, 0);
583 atomicStore(aplTotalTime, 0);
584 if (glconCtlWindow !is null) glconCtlWindow.postEvent(new EventFileComplete());
585 sio.close();
586 } else {
587 atomicStore(aplPaused, false);
588 atomicStore(aplPlaying, true);
589 atomicStore(aplCurTime, cast(int)sio.timeread);
590 atomicStore(aplTotalTime, cast(int)sio.timetotal);
591 atomicStore(aplSampleRate, cast(int)sio.rate);
592 atomicStore(aplChannels, cast(int)sio.channels);
593 alsaWriteShort(buffer[0..frmread*sio.channels]);
595 } else {
596 atomicStore(aplPlaying, true);
597 atomicStore(aplPaused, true);
598 if (alsaIsOpen) alsaShutdown();
604 // ////////////////////////////////////////////////////////////////////////// //
605 shared static this () {
606 alsaEqBands[] = 0;
608 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
609 conRegVar!alsaDevice("device", "audio output device");
610 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
611 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
612 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
613 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
615 //conRegVar!paused("paused", "is playback paused?");
617 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
618 conRegFunc!((int idx, byte value) {
619 if (value < -70) value = -70;
620 if (value > 30) value = 30;
621 if (idx >= 0 && idx < alsaEqBands.length) {
622 if (alsaEqBands[idx] != value) {
623 alsaEqBands[idx] = value;
625 } else {
626 conwriteln("invalid equalizer band index: ", idx);
628 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
630 conRegFunc!(() {
631 alsaEqBands[] = 0;
632 })("eq_reset", "reset equalizer");
634 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
635 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");