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/>.
23 import std
.concurrency
;
26 import arsd
.simpledisplay
;
42 // ////////////////////////////////////////////////////////////////////////// //
43 public struct AudioStream
{
49 //long timetotal; // in milliseconds
50 uint rate
= 1; // just in case
51 ubyte channels
= 1; // just in case
52 ulong samplestotal
; // multiplied by channels
53 ulong samplesread
; // samples read so far, multiplied by channels
58 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline
, true); return samplesread
/channels
; }
59 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline
, true); return samplestotal
/channels
; }
61 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(samplesread
*1000/rate
/channels
); }
62 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(samplestotal
*1000/rate
/channels
); }
66 if (type
.length
== 0) return false;
68 case 'f': return (ff
!is null);
69 case 'v': return (vi
!is null);
70 case 'm': return mp3
.valid
;
71 case 'o': return (of
!is null);
77 @property string
typestr () const pure nothrow @safe @nogc { return type
; }
80 if (type
.length
== 0) return;
82 case 'f': if (ff
!is null) { drflac_close(ff
); ff
= null; } break;
83 case 'v': if (vi
!is null) { vi
= null; ov_clear(&vf
); } break;
84 case 'm': if (mp3
.valid
) mp3
.close(); break;
90 int readFrames (void* buf
, int count
) {
91 if (count
< 1) return 0;
92 if (count
> int.max
/4) count
= int.max
/4;
96 if (ff
is null) return 0;
100 short* bp
= cast(short*)buf
;
102 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
103 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
105 samplesread
+= rd
; // number of samples read
106 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
110 return cast(int)(res
/channels
); // number of frames read
112 if (vi
is null) return 0;
114 auto ret = ov_read(&vf
, cast(ubyte*)buf
, count
*2*channels
, &currstream
);
115 if (ret <= 0) return 0; // error or eof
116 samplesread
+= ret/2; // number of samples read
117 return ret/2/channels
; // number of frames read
119 auto dptr
= cast(short*)buf
;
120 if (of
is null) return 0;
123 while (count
> 0 && smpbufpos
< smpbufused
) {
124 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
125 if (channels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
128 samplesread
+= channels
;
130 if (count
== 0) break;
131 auto rd
= of
.readFrame();
132 if (rd
.length
== 0) break;
133 if (rd
.length
> smpbuf
.length
) smpbuf
.length
= rd
.length
;
134 smpbuf
[0..rd
.length
] = rd
[];
136 smpbufused
= cast(uint)rd
.length
;
140 // yes, i know that frames are not independend, and i should actually
141 // seek to a frame with a correct sync word. meh.
142 if (!mp3
.valid
) return 0;
143 auto mfm
= mp3
.frameSamples
;
144 if (mp3smpused
+channels
> mfm
.length
) {
146 if (!mp3
.decodeNextFrame(&reader
)) return 0;
147 mfm
= mp3
.frameSamples
;
148 if (mp3
.sampleRate
!= rate || mp3
.channels
!= channels
) return 0;
151 ushort* b
= cast(ushort*)buf
;
152 auto oldmpu
= mp3smpused
;
153 while (count
> 0 && mp3smpused
+channels
<= mfm
.length
) {
154 *b
++ = mfm
[mp3smpused
++];
155 if (channels
== 2) *b
++ = mfm
[mp3smpused
++];
159 samplesread
+= mp3smpused
-oldmpu
; // number of samples read
166 // return new frame index
167 ulong seekToTime (uint msecs
) {
168 if (!valid
) return 0;
169 ulong snum
= cast(ulong)msecs
*rate
/1000*channels
; // sample number
172 if (ff
is null) return 0;
173 if (ff
.totalSampleCount
< 1) return 0;
174 if (snum
>= ff
.totalSampleCount
) {
175 drflac_seek_to_sample(ff
, 0);
178 if (!drflac_seek_to_sample(ff
, snum
)) {
179 drflac_seek_to_sample(ff
, 0);
183 return snum
/channels
;
185 if (vi
is null) return 0;
186 if (ov_pcm_seek(&vf
, snum
/channels
) == 0) {
187 samplesread
= ov_pcm_tell(&vf
)*channels
;
188 return samplesread
/channels
;
193 if (of
is null) return 0;
195 samplesread
= of
.smpcurtime
*channels
;
196 return samplesread
/channels
;
198 if (!mp3
.valid
) return 0;
200 if (mp3info
.index
.length
== 0 || snum
== 0) {
201 // alas, we cannot seek here
204 mp3
.restart(&reader
);
207 // find frame containing our sample
208 // stupid binary search; ignore overflow bug
210 ulong end
= mp3info
.index
.length
-1;
211 while (start
<= end
) {
212 ulong mid
= (start
+end
)/2;
213 auto smps
= mp3info
.index
[cast(size_t
)mid
].samples
;
214 auto smpe
= (mp3info
.index
.length
-mid
> 0 ? mp3info
.index
[cast(size_t
)(mid
+1)].samples
: samplestotal
);
215 if (snum
>= smps
&& snum
< smpe
) {
218 fl
.seek(mp3info
.index
[cast(size_t
)mid
].fpos
);
219 mp3smpused
= cast(uint)(snum
-smps
);
223 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
225 // alas, we cannot seek
228 mp3
.restart(&reader
);
238 Mp3Info mp3info
; // scanned info, frame index
244 uint smpbufpos
, smpbufused
;
246 int reader (void[] buf
) {
248 auto rd
= fl
.rawRead(buf
);
249 return cast(int)rd
.length
;
250 } catch (Exception e
) {}
255 static AudioStream
open (VFile fl
) {
256 //import std.string : fromStringz;
264 import core
.stdc
.stdio
;
265 import core
.stdc
.stdlib
: malloc
, free
;
268 scope(exit
) if (fcmts
!is null) free(fcmts
);
269 sio
.ff
= drflac_open_file(fl
, (void* pUserData
, drflac_metadata
* pMetadata
) {
270 if (pMetadata
.type
== DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT
) {
271 if (fcmts
!is null) free(fcmts
);
272 auto csz
= drflac_vorbis_comment_size(pMetadata
.data
.vorbis_comment
.commentCount
, pMetadata
.data
.vorbis_comment
.comments
);
273 if (csz
> 0 && csz
< 0x100_0000) {
274 fcmts
= cast(char*)malloc(cast(uint)csz
);
281 import core
.stdc
.string
: memcpy
;
282 commentCount
= pMetadata
.data
.vorbis_comment
.commentCount
;
283 memcpy(fcmts
, pMetadata
.data
.vorbis_comment
.comments
, cast(uint)csz
);
287 if (sio
.ff
!is null) {
288 scope(failure
) drflac_close(sio
.ff
);
289 if (sio
.ff
.sampleRate
< 1024 || sio
.ff
.sampleRate
> 96000) throw new Exception("fucked flac");
290 if (sio
.ff
.channels
< 1 || sio
.ff
.channels
> 2) throw new Exception("fucked flac");
291 sio
.rate
= cast(uint)sio
.ff
.sampleRate
;
292 sio
.channels
= cast(ubyte)sio
.ff
.channels
;
295 sio
.samplestotal
= sio
.ff
.totalSampleCount
;
296 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
298 drflac_vorbis_comment_iterator i
;
299 drflac_init_vorbis_comment_iterator(&i
, commentCount
, fcmts
);
301 const(char)* pComment
;
302 while ((pComment
= drflac_next_vorbis_comment(&i
, &commentLength
)) !is null) {
303 if (commentLength
> 1024*1024*2) break; // just in case
304 //conwriteln(" ", pComment[0..commentLength]);
305 auto cmts
= pComment
[0..commentLength
];
306 //conwriteln(" <", cmts, ">");
307 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
308 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
309 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
312 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
315 } catch (Exception
) {}
319 if (ov_fopen(fl
, &sio
.vf
) == 0) {
320 scope(failure
) ov_clear(&sio
.vf
);
323 sio
.vi
= ov_info(&sio
.vf
, -1);
324 if (sio
.vi
.rate
< 1024 || sio
.vi
.rate
> 96000) throw new Exception("fucked vorbis");
325 if (sio
.vi
.channels
< 1 || sio
.vi
.channels
> 2) throw new Exception("fucked vorbis");
326 sio
.rate
= sio
.vi
.rate
;
327 sio
.channels
= cast(ubyte)sio
.vi
.channels
;
328 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
329 //conwriteln("streams: ", ov_streams(&sio.vf));
330 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
331 sio
.samplestotal
= ov_pcm_total(&sio
.vf
)*sio
.channels
;
332 if (auto vc
= ov_comment(&sio
.vf
, -1)) {
333 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
334 foreach (immutable idx
; 0..vc
.comments
) {
335 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
336 auto cmts
= vc
.user_comments
[idx
][0..vc
.comment_lengths
[idx
]];
337 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
338 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
339 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
342 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
345 } catch (Exception
) {}
349 OpusFile of
= opusOpen(fl
);
350 scope(failure
) opusClose(of
);
351 if (of
.rate
< 1024 || of
.rate
> 96000) throw new Exception("fucked opus");
352 if (of
.channels
< 1 || of
.channels
> 2) throw new Exception("fucked opus");
357 sio
.channels
= of
.channels
;
358 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
359 sio
.samplestotal
= of
.smpduration
*sio
.channels
;
360 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
361 foreach (immutable cidx
; 0..of
.commentCount
) {
362 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
363 auto cmts
= of
.comment(cidx
);
364 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
365 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
366 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
369 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
371 } catch (Exception
) {}
376 sio
.mp3
= new MP3Decoder(&sio
.reader
);
379 // scan file to determine number of frames
382 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
384 if (sio
.mp3info
.valid
) {
385 if (sio
.mp3
.sampleRate
< 1024 || sio
.mp3
.sampleRate
> 96000) throw new Exception("fucked mp3");
386 if (sio
.mp3
.channels
< 1 || sio
.mp3
.channels
> 2) throw new Exception("fucked mp3");
387 sio
.rate
= sio
.mp3
.sampleRate
;
388 sio
.channels
= sio
.mp3
.channels
;
389 sio
.samplestotal
= sio
.mp3info
.samples
;
390 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
391 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
396 } catch (Exception
) {}
399 } catch (Exception
) {}
400 return AudioStream
.init
;
405 // ////////////////////////////////////////////////////////////////////////// //
406 __gshared Tid playertid
;
409 // ////////////////////////////////////////////////////////////////////////// //
410 public class EventFileLoaded
{ string filename
; string album
; string artist
; string title
; int duration
; bool success
; }
411 public class EventFileComplete
{}
413 // ////////////////////////////////////////////////////////////////////////// //
414 __gshared
bool started
= false;
415 struct TMsgQuitReq
{}
417 public void aplayStart () { if (!started
) { started
= true; playertid
= spawn(&playerThread
, thisTid
); } }
418 public void aplayShutdown () { if (started
) { started
= false; playertid
.send(TMsgQuitReq()); } }
421 // ////////////////////////////////////////////////////////////////////////// //
422 // reply with EventFileLoaded
423 struct TMsgPlayFileReq
{ string filename
; bool forcestart
; }
424 struct TMsgStopFileReq
{}
425 struct TMsgTogglePauseReq
{}
426 struct TMsgPauseReq
{ bool pause
; }
427 struct TMsgSeekReq
{ int timems
; }
430 public void aplayPlayFile (string fname
, bool forcestart
) { if (started
) playertid
.send(TMsgPlayFileReq(fname
.length ? fname
: "", forcestart
)); }
431 public void aplayStopFile () { if (started
) playertid
.send(TMsgStopFileReq()); }
432 public void aplayTogglePause () { if (started
) playertid
.send(TMsgTogglePauseReq()); }
433 public void aplayPause (bool pause
) { if (started
) playertid
.send(TMsgPauseReq(pause
)); }
434 public void aplaySeekMS (int timems
) { if (started
) playertid
.send(TMsgSeekReq(timems
)); }
437 // ////////////////////////////////////////////////////////////////////////// //
438 // reply with EventFileLoaded
439 struct TMsgPlayGainReq
{
443 public void aplayPlayGain (int prc
) { if (started
) playertid
.send(TMsgPlayGainReq(prc
)); else alsaGain
= prc
; }
446 // ////////////////////////////////////////////////////////////////////////// //
447 public bool aplayIsPlaying () { return atomicLoad(aplPlaying
); }
448 public bool aplayIsPaused () { return atomicLoad(aplPaused
); }
449 public int aplayCurTime () { return atomicLoad(aplCurTime
)/1000; }
450 public int aplayTotalTime () { return atomicLoad(aplTotalTime
)/1000; }
451 public int aplayCurTimeMS () { return atomicLoad(aplCurTime
); }
452 public int aplayTotalTimeMS () { return atomicLoad(aplTotalTime
); }
454 public int aplaySampleRate () { return atomicLoad(aplSampleRate
); }
455 public bool aplayIsStereo () { return (atomicLoad(aplChannels
) == 2); }
458 // ////////////////////////////////////////////////////////////////////////// //
459 enum BUF_SIZE
= 4096;
460 __gshared
short[BUF_SIZE
] buffer
;
462 __gshared
bool paused
= false;
463 shared bool aplPlaying
= false;
464 shared bool aplPaused
= false;
465 shared int aplCurTime
= 0;
466 shared int aplTotalTime
= 0;
467 shared int aplSampleRate
= 48000;
468 shared int aplChannels
= 2;
471 // ////////////////////////////////////////////////////////////////////////// //
472 void playerThread (Tid ownerTid
) {
474 scope(exit
) sio
.close();
476 string newfilereq
= null;
477 bool forcestart
= false;
480 uint realRate
= alsaGetBestSampleRate(48000);
481 conwriteln("real sampling rate: ", realRate
);
482 if (realRate
!= 44100 && realRate
!= 48000) {
484 conwriteln("WARNING! something is wrong with ALSA! trying to fix it...");
488 receiveTimeout((sio
.valid
&& !paused ? Duration
.min
: 42.seconds
),
492 (TMsgPlayFileReq req
) {
493 newfilereq
= (req
.filename
.length ? req
.filename
: "");
494 forcestart
= req
.forcestart
;
496 (TMsgPlayGainReq req
) {
499 (TMsgStopFileReq req
) {
502 if (alsaIsOpen
) alsaShutdown();
504 (TMsgTogglePauseReq req
) {
505 if (sio
.valid
) paused
= !paused
; else paused
= false;
508 if (sio
.valid
) paused
= req
.pause
;
511 newtime
= req
.timems
;
512 if (newtime
< 0) newtime
= 0;
518 if (newfilereq
!is null) {
520 auto reply
= new EventFileLoaded();
521 reply
.filename
= newfilereq
;
522 reply
.success
= false;
524 bool wasplaying
= sio
.valid
;
527 sio
= AudioStream
.open(VFile(reply
.filename
));
528 } catch (Exception e
) {
529 sio
= AudioStream
.init
;
532 reply
.duration
= cast(int)(sio
.timetotal
/1000);
533 reply
.album
= sio
.album
;
534 reply
.artist
= sio
.artist
;
535 reply
.title
= sio
.title
;
536 reply
.success
= true;
537 if (forcestart
) paused
= false; else paused
= !wasplaying
;
539 if (alsaIsOpen
) alsaShutdown();
541 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(reply
);
547 atomicStore(aplPaused
, false);
548 atomicStore(aplPlaying
, false);
549 atomicStore(aplCurTime
, 0);
550 atomicStore(aplTotalTime
, 0);
551 //atomicStore(aplSampleRate, 48000);
552 //atomicStore(aplChannels, 2);
556 if (newtime
!= -666) {
559 if (tm
>= sio
.timetotal
) tm
= (sio
.timetotal ? sio
.timetotal
-1 : 0);
560 sio
.seekToTime(cast(uint)tm
);
564 if (!alsaIsOpen || alsaRate
!= sio
.rate || alsaChannels
!= sio
.channels
) {
565 if (alsaIsOpen
) alsaShutdown();
566 if (!alsaInit(sio
.rate
, sio
.channels
)) assert(0, "cannot init ALSA playback");
568 auto frmread
= sio
.readFrames(buffer
.ptr
, BUF_SIZE
/sio
.channels
);
570 atomicStore(aplPaused
, false);
571 atomicStore(aplPlaying
, false);
572 atomicStore(aplCurTime
, 0);
573 atomicStore(aplTotalTime
, 0);
574 if (glconCtlWindow
!is null) glconCtlWindow
.postEvent(new EventFileComplete());
577 atomicStore(aplPaused
, false);
578 atomicStore(aplPlaying
, true);
579 atomicStore(aplCurTime
, cast(int)sio
.timeread
);
580 atomicStore(aplTotalTime
, cast(int)sio
.timetotal
);
581 atomicStore(aplSampleRate
, cast(int)sio
.rate
);
582 atomicStore(aplChannels
, cast(int)sio
.channels
);
583 alsaWriteShort(buffer
[0..frmread
*sio
.channels
]);
586 atomicStore(aplPlaying
, true);
587 atomicStore(aplPaused
, true);
588 if (alsaIsOpen
) alsaShutdown();
594 // ////////////////////////////////////////////////////////////////////////// //
595 shared static this () {
598 conRegVar
!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
599 conRegVar
!alsaDevice("device", "audio output device");
600 //conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
601 conRegVar
!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
602 conRegVar
!alsaEnableResampling("use_resampling", "allow audio resampling?");
603 conRegVar
!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
605 //conRegVar!paused("paused", "is playback paused?");
607 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
608 conRegFunc
!((int idx
, byte value
) {
609 if (value
< -70) value
= -70;
610 if (value
> 30) value
= 30;
611 if (idx
>= 0 && idx
< alsaEqBands
.length
) {
612 if (alsaEqBands
[idx
] != value
) {
613 alsaEqBands
[idx
] = value
;
616 conwriteln("invalid equalizer band index: ", idx
);
618 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
622 })("eq_reset", "reset equalizer");
624 //conRegFunc!(() { conaction = Action.Next; })("next", "next song");
625 //conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");