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 iv
.audiostream
/*is aliced*/;
34 // ////////////////////////////////////////////////////////////////////////// //
35 public class AudioStream
{
47 Type mType
= Type
.Unknown
;
48 uint mRate
= 1; // just in case
49 ubyte mChannels
= 1; // just in case
50 ulong mSamplesTotal
; // multiplied by channels
51 ulong mSamplesRead
; // samples read so far, multiplied by channels
52 bool mOnlyMeta
= false;
56 final int reader (void[] buf
) {
58 auto rd
= fl
.rawRead(buf
);
59 return cast(int)rd
.length
;
60 } catch (Exception e
) {}
73 final @property uint rate () const pure nothrow @safe @nogc { pragma(inline
, true); return mRate
; }
74 final @property ubyte channels () const pure nothrow @safe @nogc { pragma(inline
, true); return mChannels
; }
76 final @property ulong framesRead () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesRead
/mChannels
; }
77 final @property ulong framesTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesTotal
/mChannels
; }
79 final @property uint timeRead () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesRead
*1000/mRate
/mChannels
); }
80 final @property uint timeTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesTotal
*1000/mRate
/mChannels
); }
82 final @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (mType
!= Type
.Unknown
); }
84 final @property bool onlyMeta () const pure nothrow @safe @nogc { pragma(inline
, true); return mOnlyMeta
; }
90 mSamplesTotal
= mSamplesRead
= 0;
91 album
= artist
= title
= null;
95 abstract int readFrames (void* buf
, int count
);
97 // return new frame index
98 abstract ulong seekToTime (uint msecs
);
101 static AudioStream
detect (VFile fl
, bool onlymeta
=false) nothrow {
102 bool didOpus
, didVorbis
, didFlac
, didMp3
;
104 AudioStream
tryFormat(T
: AudioStream
) (ref bool didit
) nothrow {
105 if (didit
) return null;
107 //conwriteln("trying ", T.stringof);
110 if (auto ast
= T
.detect(fl
, onlymeta
)) return ast
;
111 } catch (Exception e
) {
112 //conwriteln("DETECT ERROR: ", e.msg);
117 AudioStream
tryOpus () nothrow { return tryFormat
!AudioStreamOpus(didOpus
); }
118 AudioStream
tryVorbis () nothrow { return tryFormat
!AudioStreamVorbis(didVorbis
); }
119 AudioStream
tryFlac () nothrow { return tryFormat
!AudioStreamFlac(didFlac
); }
120 AudioStream
tryMp3 () nothrow { return tryFormat
!AudioStreamMp3(didMp3
); }
123 auto fname
= fl
.name
;
124 auto extpos
= fname
.lastIndexOf('.');
126 auto ext
= fname
[extpos
..$];
127 if (ext
.strEquCI(".opus")) { if (auto ast
= tryOpus()) return ast
; }
128 else if (ext
.strEquCI(".ogg")) { if (auto ast
= tryVorbis()) return ast
; }
129 else if (ext
.strEquCI(".flac")) { if (auto ast
= tryFlac()) return ast
; }
130 else if (ext
.strEquCI(".mp3")) { if (auto ast
= tryMp3()) return ast
; }
132 // this is fastest for my collection
133 if (auto ast
= tryFlac()) return ast
;
134 if (auto ast
= tryOpus()) return ast
;
135 if (auto ast
= tryVorbis()) return ast
;
136 if (auto ast
= tryMp3()) return ast
;
137 } catch (Exception e
) {}
143 // ////////////////////////////////////////////////////////////////////////// //
144 final class AudioStreamOpus
: AudioStream
{
148 uint smpbufpos
, smpbufused
;
154 override void close () {
157 smpbufpos
= smpbufused
= 0;
161 override int readFrames (void* buf
, int count
) {
162 if (count
< 1) return 0;
163 if (count
> int.max
/4) count
= int.max
/4;
164 if (!valid || onlyMeta
) return 0;
166 auto dptr
= cast(short*)buf
;
167 if (of
is null) return 0;
170 while (count
> 0 && smpbufpos
< smpbufused
) {
171 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
172 if (mChannels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
175 mSamplesRead
+= mChannels
;
177 if (count
== 0) break;
178 auto rd
= of
.readFrame();
179 if (rd
.length
== 0) break;
180 if (rd
.length
> smpbuf
.length
) {
181 auto optr
= smpbuf
.ptr
;
182 smpbuf
.length
= rd
.length
;
183 if (smpbuf
.ptr
!is optr
) {
184 import core
.memory
: GC
;
185 if (smpbuf
.ptr
is GC
.addrOf(smpbuf
.ptr
)) GC
.setAttr(smpbuf
.ptr
, GC
.BlkAttr
.NO_INTERIOR
);
188 smpbuf
[0..rd
.length
] = rd
[];
190 smpbufused
= cast(uint)rd
.length
;
195 override ulong seekToTime (uint msecs
) {
196 if (!valid || onlyMeta
) return 0;
197 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
199 if (of
is null) return 0;
201 mSamplesRead
= of
.smpcurtime
*mChannels
;
202 return mSamplesRead
/mChannels
;
206 static AudioStreamOpus
detect (VFile fl
, bool onlymeta
) {
207 OpusFile of
= opusOpen(fl
);
208 scope(failure
) opusClose(of
);
209 if (of
.rate
< 1024 || of
.rate
> 96000) throw new Exception("fucked opus");
210 if (of
.channels
< 1 || of
.channels
> 2) throw new Exception("fucked opus");
211 AudioStreamOpus sio
= new AudioStreamOpus();
213 sio
.mType
= Type
.Opus
;
216 sio
.mChannels
= of
.channels
;
217 sio
.mOnlyMeta
= onlymeta
;
218 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
219 sio
.mSamplesTotal
= of
.smpduration
*sio
.mChannels
;
220 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
221 foreach (immutable cidx
; 0..of
.commentCount
) {
222 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
223 auto cmts
= of
.comment(cidx
);
224 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
225 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
226 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
229 scope(exit
) { of
= null; sio
.of
= null; sio
.fl
.close(); }
232 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
238 // ////////////////////////////////////////////////////////////////////////// //
239 final class AudioStreamVorbis
: AudioStream
{
248 override void close () {
249 if (vi
!is null) { vi
= null; ov_clear(&vf
); }
253 override int readFrames (void* buf
, int count
) {
254 if (count
< 1) return 0;
255 if (count
> int.max
/4) count
= int.max
/4;
256 if (!valid || onlyMeta
) return 0;
258 if (vi
is null) return 0;
260 auto ret = ov_read(&vf
, cast(ubyte*)buf
, count
*2*mChannels
, &currstream
);
261 if (ret <= 0) return 0; // error or eof
262 mSamplesRead
+= ret/2; // number of samples read
263 return ret/2/mChannels
; // number of frames read
266 override ulong seekToTime (uint msecs
) {
267 if (!valid || onlyMeta
) return 0;
268 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
270 if (vi
is null) return 0;
271 if (ov_pcm_seek(&vf
, snum
/mChannels
) == 0) {
272 mSamplesRead
= ov_pcm_tell(&vf
)*mChannels
;
273 return mSamplesRead
/mChannels
;
280 static AudioStreamVorbis
detect (VFile fl
, bool onlymeta
) {
282 if (ov_fopen(fl
, &vf
) == 0) {
283 scope(failure
) ov_clear(&vf
);
284 auto sio
= new AudioStreamVorbis();
285 scope(failure
) delete sio
;
286 sio
.mType
= Type
.Vorbis
;
287 sio
.mOnlyMeta
= onlymeta
;
289 sio
.vi
= ov_info(&vf
, -1);
290 if (sio
.vi
.rate
< 1024 || sio
.vi
.rate
> 96000) throw new Exception("fucked vorbis");
291 if (sio
.vi
.channels
< 1 || sio
.vi
.channels
> 2) throw new Exception("fucked vorbis");
292 sio
.mRate
= sio
.vi
.rate
;
293 sio
.mChannels
= cast(ubyte)sio
.vi
.channels
;
294 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
295 //conwriteln("streams: ", ov_streams(&sio.vf));
296 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
297 sio
.mSamplesTotal
= ov_pcm_total(&vf
)*sio
.mChannels
;
298 if (auto vc
= ov_comment(&vf
, -1)) {
299 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
300 foreach (immutable idx
; 0..vc
.comments
) {
301 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
302 auto cmts
= vc
.user_comments
[idx
][0..vc
.comment_lengths
[idx
]];
303 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
304 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
305 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
308 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
310 try { ov_clear(&vf
); } catch (Exception e
) {}
322 // ////////////////////////////////////////////////////////////////////////// //
323 final class AudioStreamFlac
: AudioStream
{
331 override void close () {
332 if (ff
!is null) { drflac_close(ff
); ff
= null; }
336 override int readFrames (void* buf
, int count
) {
337 if (count
< 1) return 0;
338 if (count
> int.max
/4) count
= int.max
/4;
339 if (!valid || onlyMeta
) return 0;
341 if (ff
is null) return 0;
342 int[512] flcbuf
= void;
345 short* bp
= cast(short*)buf
;
347 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
348 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
350 mSamplesRead
+= rd
; // number of samples read
351 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
355 return cast(int)(res
/mChannels
); // number of frames read
358 override ulong seekToTime (uint msecs
) {
359 if (!valid || onlyMeta
) return 0;
360 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
362 if (ff
is null) return 0;
363 if (ff
.totalSampleCount
< 1) return 0;
364 if (snum
>= ff
.totalSampleCount
) {
365 drflac_seek_to_sample(ff
, 0);
368 if (!drflac_seek_to_sample(ff
, snum
)) {
369 drflac_seek_to_sample(ff
, 0);
373 return snum
/mChannels
;
377 static AudioStreamFlac
detect (VFile fl
, bool onlymeta
) {
378 import core
.stdc
.stdio
;
379 import core
.stdc
.stdlib
: malloc
, free
;
382 scope(exit
) if (fcmts
!is null) free(fcmts
);
383 drflac
* ff
= drflac_open_file(fl
, (void* pUserData
, drflac_metadata
* pMetadata
) {
384 if (pMetadata
.type
== DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT
) {
385 if (fcmts
!is null) free(fcmts
);
386 auto csz
= drflac_vorbis_comment_size(pMetadata
.data
.vorbis_comment
.commentCount
, pMetadata
.data
.vorbis_comment
.comments
);
387 if (csz
> 0 && csz
< 0x100_0000) {
388 fcmts
= cast(char*)malloc(cast(uint)csz
);
395 import core
.stdc
.string
: memcpy
;
396 commentCount
= pMetadata
.data
.vorbis_comment
.commentCount
;
397 memcpy(fcmts
, pMetadata
.data
.vorbis_comment
.comments
, cast(uint)csz
);
402 scope(failure
) drflac_close(ff
);
403 if (ff
.sampleRate
< 1024 || ff
.sampleRate
> 96000) throw new Exception("fucked flac");
404 if (ff
.channels
< 1 || ff
.channels
> 2) throw new Exception("fucked flac");
405 AudioStreamFlac sio
= new AudioStreamFlac();
406 scope(failure
) delete sio
;
407 sio
.mRate
= cast(uint)ff
.sampleRate
;
408 sio
.mChannels
= cast(ubyte)ff
.channels
;
409 sio
.mType
= Type
.Flac
;
410 sio
.mSamplesTotal
= ff
.totalSampleCount
;
411 sio
.mOnlyMeta
= onlymeta
;
414 sio
.mOnlyMeta
= false;
417 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
419 drflac_vorbis_comment_iterator i
;
420 drflac_init_vorbis_comment_iterator(&i
, commentCount
, fcmts
);
422 const(char)* pComment
;
423 while ((pComment
= drflac_next_vorbis_comment(&i
, &commentLength
)) !is null) {
424 if (commentLength
> 1024*1024*2) break; // just in case
425 //conwriteln(" ", pComment[0..commentLength]);
426 auto cmts
= pComment
[0..commentLength
];
427 //conwriteln(" <", cmts, ">");
428 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
429 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
430 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
433 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
441 // ////////////////////////////////////////////////////////////////////////// //
442 final class AudioStreamMp3
: AudioStream
{
445 Mp3Info mp3info
; // scanned info, frame index
452 override void close () {
453 if (mp3
!is null && mp3
.valid
) { mp3
.close(); delete mp3
; }
454 delete mp3info
.index
;
455 mp3info
= Mp3Info
.init
;
459 override int readFrames (void* buf
, int count
) {
460 if (count
< 1) return 0;
461 if (count
> int.max
/4) count
= int.max
/4;
462 if (!valid || onlyMeta
) return 0;
464 // yes, i know that frames are not independent, and i should actually
465 // seek to a frame with a correct sync word. meh.
466 if (!mp3
.valid
) return 0;
467 auto mfm
= mp3
.frameSamples
;
468 if (mp3smpused
+mChannels
> mfm
.length
) {
470 if (!mp3
.decodeNextFrame(&reader
)) return 0;
471 mfm
= mp3
.frameSamples
;
472 if (mp3
.sampleRate
!= mRate || mp3
.channels
!= mChannels
) return 0;
475 ushort* b
= cast(ushort*)buf
;
476 auto oldmpu
= mp3smpused
;
477 while (count
> 0 && mp3smpused
+mChannels
<= mfm
.length
) {
478 *b
++ = mfm
[mp3smpused
++];
479 if (mChannels
== 2) *b
++ = mfm
[mp3smpused
++];
483 mSamplesRead
+= mp3smpused
-oldmpu
; // number of samples read
487 override ulong seekToTime (uint msecs
) {
488 if (!valid || onlyMeta
) return 0;
489 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
491 if (!mp3
.valid
) return 0;
493 if (mp3info
.index
.length
== 0 || snum
== 0) {
494 // alas, we cannot seek here
497 mp3
.restart(&reader
);
500 // find frame containing our sample
501 // stupid binary search; ignore overflow bug
503 ulong end
= mp3info
.index
.length
-1;
504 while (start
<= end
) {
505 ulong mid
= (start
+end
)/2;
506 auto smps
= mp3info
.index
[cast(usize
)mid
].samples
;
507 auto smpe
= (mp3info
.index
.length
-mid
> 0 ? mp3info
.index
[cast(usize
)(mid
+1)].samples
: mSamplesTotal
);
508 if (snum
>= smps
&& snum
< smpe
) {
511 fl
.seek(mp3info
.index
[cast(usize
)mid
].fpos
);
512 mp3smpused
= cast(uint)(snum
-smps
);
516 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
518 // alas, we cannot seek
521 mp3
.restart(&reader
);
526 static AudioStreamMp3
detect (VFile fl
, bool onlymeta
) {
527 auto fpos
= fl
.tell
; // usually 0, but...
528 AudioStreamMp3 sio
= new AudioStreamMp3();
529 scope(failure
) delete sio
;
531 scope(failure
) sio
.fl
.close();
532 sio
.mp3
= new MP3Decoder(&sio
.reader
);
533 scope(failure
) delete sio
.mp3
;
534 sio
.mType
= Type
.Mp3
;
536 // scan file to determine number of frames
537 auto xfp
= fl
.tell
; // mp3 decoder already buffered some data
540 sio
.mOnlyMeta
= true;
541 sio
.mp3info
= mp3Scan
!false((void[] buf
) => cast(int)fl
.rawRead(buf
).length
);
543 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
545 if (sio
.mp3info
.valid
) {
546 if (sio
.mp3
.sampleRate
< 1024 || sio
.mp3
.sampleRate
> 96000) throw new Exception("fucked mp3");
547 if (sio
.mp3
.channels
< 1 || sio
.mp3
.channels
> 2) throw new Exception("fucked mp3");
548 sio
.mRate
= sio
.mp3
.sampleRate
;
549 sio
.mChannels
= sio
.mp3
.channels
;
550 sio
.mSamplesTotal
= sio
.mp3info
.samples
;
551 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
552 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
553 //conwriteln("id3v2: ", sio.mp3info.hasID3v2, "; ofs: ", sio.mp3info.id3v2ofs);
555 if (sio
.mp3info
.hasID3v2
) {
558 fl
.seek(fpos
+sio
.mp3info
.id3v2ofs
);
559 if (idtag
.scanParse
!false(fl
)) {
560 sio
.album
= idtag
.album
;
561 sio
.artist
= idtag
.artist
;
562 sio
.title
= idtag
.title
;
564 } catch (Exception e
) {}