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.
31 module aplayer
is aliced
;
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
{}
428 // ////////////////////////////////////////////////////////////////////////// //
429 __gshared
bool started
= false;
430 struct TMsgQuitReq
{}
432 public void aplayStart () { if (!started
) { started
= true; playertid
= spawn(&playerThread
, thisTid
); } }
433 public void aplayShutdown () { if (started
) { started
= false; playertid
.send(TMsgQuitReq()); } }
436 // ////////////////////////////////////////////////////////////////////////// //
437 // reply with EventFileLoaded
438 struct TMsgPlayFileReq
{ string filename
; bool forcestart
; }
439 struct TMsgStopFileReq
{}
440 struct TMsgTogglePauseReq
{}
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 aplaySeekMS (int timems
) { if (started
) playertid
.send(TMsgSeekReq(timems
)); }
450 // ////////////////////////////////////////////////////////////////////////// //
451 // reply with EventFileLoaded
452 struct TMsgPlayGainReq
{
456 public void aplayPlayGain (int prc
) { if (started
) playertid
.send(TMsgPlayGainReq(prc
)); else alsaGain
= prc
; }
459 // ////////////////////////////////////////////////////////////////////////// //
460 public bool aplayIsPlaying () { return atomicLoad(aplPlaying
); }
461 public int aplayCurTime () { return atomicLoad(aplCurTime
)/1000; }
462 public int aplayTotalTime () { return atomicLoad(aplTotalTime
)/1000; }
463 public int aplayCurTimeMS () { return atomicLoad(aplCurTime
); }
464 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime
); }
467 // ////////////////////////////////////////////////////////////////////////// //
468 enum BUF_SIZE
= 4096;
469 __gshared
short[BUF_SIZE
] buffer
;
471 __gshared
bool paused
= false;
472 shared bool aplPlaying
= false;
473 shared int aplCurTime
= 0;
474 shared int aplTotalTime
= 0;
477 // ////////////////////////////////////////////////////////////////////////// //
478 void playerThread (Tid ownerTid
) {
480 scope(exit
) sio
.close();
482 string newfilereq
= null;
483 bool forcestart
= false;
486 uint realRate
= alsaGetBestSampleRate(sio
.rate
);
487 conwriteln("real sampling rate: ", realRate
);
489 //bool oldpaused = !paused;
490 //int oldgain = alsaGain;
493 receiveTimeout((sio
.valid
&& !paused ? Duration
.min
: 42.seconds
),
497 (TMsgPlayFileReq req
) {
498 newfilereq
= (req
.filename
.length ? req
.filename
: "");
499 forcestart
= req
.forcestart
;
501 (TMsgPlayGainReq req
) {
504 (TMsgStopFileReq req
) {
507 if (alsaIsOpen
) alsaShutdown();
509 (TMsgTogglePauseReq req
) {
513 newtime
= req
.timems
;
514 if (newtime
< 0) newtime
= 0;
520 if (newfilereq
!is null) {
522 auto reply
= new EventFileLoaded();
523 reply
.filename
= newfilereq
;
524 reply
.success
= false;
526 bool wasplaying
= sio
.valid
;
529 sio
= AudioStream
.open(VFile(reply
.filename
));
530 } catch (Exception e
) {
531 sio
= AudioStream
.init
;
534 reply
.duration
= cast(int)(sio
.timetotal
/1000);
535 reply
.album
= sio
.album
;
536 reply
.artist
= sio
.artist
;
537 reply
.title
= sio
.title
;
538 reply
.success
= true;
539 if (forcestart
) paused
= false; else paused
= !wasplaying
;
541 if (alsaIsOpen
) alsaShutdown();
543 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);
549 atomicStore(aplPlaying
, false);
550 atomicStore(aplCurTime
, 0);
551 atomicStore(aplTotalTime
, 0);
555 if (newtime
!= -666) {
558 if (tm
>= sio
.timetotal
) tm
= (sio
.timetotal ? sio
.timetotal
-1 : 0);
559 sio
.seekToTime(cast(uint)tm
);
563 if (!alsaIsOpen
/*|| alsaRate != sio.rate*/ || alsaChannels
!= sio
.channels
) {
564 if (alsaIsOpen
) alsaShutdown();
565 if (!alsaInit(/*sio.rate*/realRate
, sio
.channels
)) assert(0, "cannot init ALSA playback");
567 auto frmread
= sio
.readFrames(buffer
.ptr
, BUF_SIZE
/sio
.channels
);
569 atomicStore(aplPlaying
, false);
570 atomicStore(aplCurTime
, 0);
571 atomicStore(aplTotalTime
, 0);
572 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(new EventFileComplete());
575 atomicStore(aplPlaying
, true);
576 atomicStore(aplCurTime
, cast(int)sio
.timeread
);
577 atomicStore(aplTotalTime
, cast(int)sio
.timetotal
);
578 alsaWriteShort(buffer
[0..frmread
*sio
.channels
]);
581 if (alsaIsOpen
) alsaShutdown();
587 // ////////////////////////////////////////////////////////////////////////// //
588 shared static this () {
591 conRegVar
!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
592 conRegVar
!alsaDevice("device", "audio output device");
593 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
594 conRegVar
!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
595 conRegVar
!alsaEnableResampling("use_resampling", "allow audio resampling?");
596 conRegVar
!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
598 //conRegVar!paused("paused", "is playback paused?");
600 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
601 conRegFunc
!((int idx
, byte value
) {
602 if (value
< -70) value
= -70;
603 if (value
> 30) value
= 30;
604 if (idx
>= 0 && idx
< alsaEqBands
.length
) {
605 if (alsaEqBands
[idx
] != value
) {
606 alsaEqBands
[idx
] = value
;
609 conwriteln("invalid equalizer band index: ", idx
);
611 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
615 })("eq_reset", "reset equalizer");
617 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
618 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");