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
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.
37 import std
.concurrency
;
40 import arsd
.simpledisplay
;
56 // ////////////////////////////////////////////////////////////////////////// //
57 public struct AudioStream
{
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
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
); }
80 if (type
.length
== 0) return false;
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);
91 @property string
typestr () const pure nothrow @safe @nogc { return type
; }
94 if (type
.length
== 0) return;
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;
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;
110 if (ff
is null) return 0;
114 short* bp
= cast(short*)buf
;
116 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
117 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
119 samplesread
+= rd
; // number of samples read
120 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
124 return cast(int)(res
/channels
); // number of frames read
126 if (vi
is null) return 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
133 auto dptr
= cast(short*)buf
;
134 if (of
is null) return 0;
137 while (count
> 0 && smpbufpos
< smpbufused
) {
138 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
139 if (channels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
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
[];
150 smpbufused
= cast(uint)rd
.length
;
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
) {
160 if (!mp3
.decodeNextFrame(&reader
)) return 0;
161 mfm
= mp3
.frameSamples
;
162 if (mp3
.sampleRate
!= rate || mp3
.channels
!= channels
) return 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
++];
173 samplesread
+= mp3smpused
-oldmpu
; // number of samples read
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
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);
192 if (!drflac_seek_to_sample(ff
, snum
)) {
193 drflac_seek_to_sample(ff
, 0);
197 return snum
/channels
;
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
;
207 if (of
is null) return 0;
209 samplesread
= of
.smpcurtime
*channels
;
210 return samplesread
/channels
;
212 if (!mp3
.valid
) return 0;
214 if (mp3info
.index
.length
== 0 || snum
== 0) {
215 // alas, we cannot seek here
218 mp3
.restart(&reader
);
221 // find frame containing our sample
222 // stupid binary search; ignore overflow bug
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
) {
232 fl
.seek(mp3info
.index
[cast(size_t
)mid
].fpos
);
233 mp3smpused
= cast(uint)(snum
-smps
);
237 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
239 // alas, we cannot seek
242 mp3
.restart(&reader
);
252 Mp3Info mp3info
; // scanned info, frame index
258 uint smpbufpos
, smpbufused
;
260 int reader (void[] buf
) {
262 auto rd
= fl
.rawRead(buf
);
263 return cast(int)rd
.length
;
264 } catch (Exception e
) {}
269 static AudioStream
open (VFile fl
) {
270 //import std.string : fromStringz;
278 import core
.stdc
.stdio
;
279 import core
.stdc
.stdlib
: malloc
, free
;
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
);
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
;
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
);
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);
329 } catch (Exception
) {}
333 if (ov_fopen(fl
, &sio
.vf
) == 0) {
334 scope(failure
) ov_clear(&sio
.vf
);
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);
359 } catch (Exception
) {}
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");
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
;
383 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
385 } catch (Exception
) {}
390 sio
.mp3
= new MP3Decoder(&sio
.reader
);
393 // scan file to determine number of frames
396 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
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);
410 } catch (Exception
) {}
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
; }
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
{
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
) {
488 scope(exit
) sio
.close();
490 string newfilereq
= null;
491 bool forcestart
= false;
494 uint realRate
= alsaGetBestSampleRate(sio
.rate
);
495 conwriteln("real sampling rate: ", realRate
);
498 receiveTimeout((sio
.valid
&& !paused ? Duration
.min
: 42.seconds
),
502 (TMsgPlayFileReq req
) {
503 newfilereq
= (req
.filename
.length ? req
.filename
: "");
504 forcestart
= req
.forcestart
;
506 (TMsgPlayGainReq req
) {
509 (TMsgStopFileReq req
) {
512 if (alsaIsOpen
) alsaShutdown();
514 (TMsgTogglePauseReq req
) {
515 if (sio
.valid
) paused
= !paused
; else paused
= false;
518 if (sio
.valid
) paused
= req
.pause
;
521 newtime
= req
.timems
;
522 if (newtime
< 0) newtime
= 0;
528 if (newfilereq
!is null) {
530 auto reply
= new EventFileLoaded();
531 reply
.filename
= newfilereq
;
532 reply
.success
= false;
534 bool wasplaying
= sio
.valid
;
537 sio
= AudioStream
.open(VFile(reply
.filename
));
538 } catch (Exception e
) {
539 sio
= AudioStream
.init
;
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
;
549 if (alsaIsOpen
) alsaShutdown();
551 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);
557 atomicStore(aplPaused
, false);
558 atomicStore(aplPlaying
, false);
559 atomicStore(aplCurTime
, 0);
560 atomicStore(aplTotalTime
, 0);
561 //atomicStore(aplSampleRate, 48000);
562 //atomicStore(aplChannels, 2);
566 if (newtime
!= -666) {
569 if (tm
>= sio
.timetotal
) tm
= (sio
.timetotal ? sio
.timetotal
-1 : 0);
570 sio
.seekToTime(cast(uint)tm
);
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
);
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());
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
]);
596 atomicStore(aplPlaying
, true);
597 atomicStore(aplPaused
, true);
598 if (alsaIsOpen
) alsaShutdown();
604 // ////////////////////////////////////////////////////////////////////////// //
605 shared static this () {
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
;
626 conwriteln("invalid equalizer band index: ", idx
);
628 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
632 })("eq_reset", "reset equalizer");
634 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
635 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");