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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module iv
.audiostream
/*is aliced*/;
33 // ////////////////////////////////////////////////////////////////////////// //
34 public class AudioStream
{
46 Type mType
= Type
.Unknown
;
47 uint mRate
= 1; // just in case
48 ubyte mChannels
= 1; // just in case
49 ulong mSamplesTotal
; // multiplied by channels
50 ulong mSamplesRead
; // samples read so far, multiplied by channels
51 bool mOnlyMeta
= false;
55 final int reader (void[] buf
) {
57 auto rd
= fl
.rawRead(buf
);
58 return cast(int)rd
.length
;
59 } catch (Exception e
) {}
72 final @property uint rate () const pure nothrow @safe @nogc { pragma(inline
, true); return mRate
; }
73 final @property ubyte channels () const pure nothrow @safe @nogc { pragma(inline
, true); return mChannels
; }
75 final @property ulong framesRead () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesRead
/mChannels
; }
76 final @property ulong framesTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesTotal
/mChannels
; }
78 final @property uint timeRead () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesRead
*1000/mRate
/mChannels
); }
79 final @property uint timeTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesTotal
*1000/mRate
/mChannels
); }
81 final @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (mType
!= Type
.Unknown
); }
83 final @property bool onlyMeta () const pure nothrow @safe @nogc { pragma(inline
, true); return mOnlyMeta
; }
89 mSamplesTotal
= mSamplesRead
= 0;
90 album
= artist
= title
= null;
94 abstract int readFrames (void* buf
, int count
);
96 // return new frame index
97 abstract ulong seekToTime (uint msecs
);
100 static AudioStream
detect (VFile fl
, bool onlymeta
=false) nothrow {
101 bool didOpus
, didVorbis
, didFlac
, didMp3
;
103 AudioStream
tryFormat(T
: AudioStream
) (ref bool didit
) nothrow {
104 if (didit
) return null;
106 //conwriteln("trying ", T.stringof);
109 if (auto ast
= T
.detect(fl
, onlymeta
)) return ast
;
110 } catch (Exception e
) {
111 //conwriteln("DETECT ERROR: ", e.msg);
116 AudioStream
tryOpus () nothrow { return tryFormat
!AudioStreamOpus(didOpus
); }
117 AudioStream
tryVorbis () nothrow { return tryFormat
!AudioStreamVorbis(didVorbis
); }
118 AudioStream
tryFlac () nothrow { return tryFormat
!AudioStreamFlac(didFlac
); }
119 AudioStream
tryMp3 () nothrow { return tryFormat
!AudioStreamMp3(didMp3
); }
122 auto fname
= fl
.name
;
123 auto extpos
= fname
.lastIndexOf('.');
125 auto ext
= fname
[extpos
..$];
126 if (ext
.strEquCI(".opus")) { if (auto ast
= tryOpus()) return ast
; }
127 else if (ext
.strEquCI(".ogg")) { if (auto ast
= tryVorbis()) return ast
; }
128 else if (ext
.strEquCI(".flac")) { if (auto ast
= tryFlac()) return ast
; }
129 else if (ext
.strEquCI(".mp3")) { if (auto ast
= tryMp3()) return ast
; }
131 // this is fastest for my collection
132 if (auto ast
= tryFlac()) return ast
;
133 if (auto ast
= tryOpus()) return ast
;
134 if (auto ast
= tryVorbis()) return ast
;
135 if (auto ast
= tryMp3()) return ast
;
136 } catch (Exception e
) {}
142 // ////////////////////////////////////////////////////////////////////////// //
143 final class AudioStreamOpus
: AudioStream
{
147 uint smpbufpos
, smpbufused
;
153 override void close () {
156 smpbufpos
= smpbufused
= 0;
160 override int readFrames (void* buf
, int count
) {
161 if (count
< 1) return 0;
162 if (count
> int.max
/4) count
= int.max
/4;
163 if (!valid || onlyMeta
) return 0;
165 auto dptr
= cast(short*)buf
;
166 if (of
is null) return 0;
169 while (count
> 0 && smpbufpos
< smpbufused
) {
170 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
171 if (mChannels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
174 mSamplesRead
+= mChannels
;
176 if (count
== 0) break;
177 auto rd
= of
.readFrame();
178 if (rd
.length
== 0) break;
179 if (rd
.length
> smpbuf
.length
) {
180 auto optr
= smpbuf
.ptr
;
181 smpbuf
.length
= rd
.length
;
182 if (smpbuf
.ptr
!is optr
) {
183 import core
.memory
: GC
;
184 if (smpbuf
.ptr
is GC
.addrOf(smpbuf
.ptr
)) GC
.setAttr(smpbuf
.ptr
, GC
.BlkAttr
.NO_INTERIOR
);
187 smpbuf
[0..rd
.length
] = rd
[];
189 smpbufused
= cast(uint)rd
.length
;
194 override ulong seekToTime (uint msecs
) {
195 if (!valid || onlyMeta
) return 0;
196 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
198 if (of
is null) return 0;
200 mSamplesRead
= of
.smpcurtime
*mChannels
;
201 return mSamplesRead
/mChannels
;
205 static AudioStreamOpus
detect (VFile fl
, bool onlymeta
) {
206 OpusFile of
= opusOpen(fl
);
207 scope(failure
) opusClose(of
);
208 if (of
.rate
< 1024 || of
.rate
> 96000) throw new Exception("fucked opus");
209 if (of
.channels
< 1 || of
.channels
> 2) throw new Exception("fucked opus");
210 AudioStreamOpus sio
= new AudioStreamOpus();
212 sio
.mType
= Type
.Opus
;
215 sio
.mChannels
= of
.channels
;
216 sio
.mOnlyMeta
= onlymeta
;
217 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
218 sio
.mSamplesTotal
= of
.smpduration
*sio
.mChannels
;
219 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
220 foreach (immutable cidx
; 0..of
.commentCount
) {
221 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
222 auto cmts
= of
.comment(cidx
);
223 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
224 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
225 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
228 scope(exit
) { of
= null; sio
.of
= null; sio
.fl
.close(); }
231 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
237 // ////////////////////////////////////////////////////////////////////////// //
238 final class AudioStreamVorbis
: AudioStream
{
247 override void close () {
248 if (vi
!is null) { vi
= null; ov_clear(&vf
); }
252 override int readFrames (void* buf
, int count
) {
253 if (count
< 1) return 0;
254 if (count
> int.max
/4) count
= int.max
/4;
255 if (!valid || onlyMeta
) return 0;
257 if (vi
is null) return 0;
259 auto ret = ov_read(&vf
, cast(ubyte*)buf
, count
*2*mChannels
, &currstream
);
260 if (ret <= 0) return 0; // error or eof
261 mSamplesRead
+= ret/2; // number of samples read
262 return ret/2/mChannels
; // number of frames read
265 override ulong seekToTime (uint msecs
) {
266 if (!valid || onlyMeta
) return 0;
267 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
269 if (vi
is null) return 0;
270 if (ov_pcm_seek(&vf
, snum
/mChannels
) == 0) {
271 mSamplesRead
= ov_pcm_tell(&vf
)*mChannels
;
272 return mSamplesRead
/mChannels
;
279 static AudioStreamVorbis
detect (VFile fl
, bool onlymeta
) {
281 if (ov_fopen(fl
, &vf
) == 0) {
282 scope(failure
) ov_clear(&vf
);
283 auto sio
= new AudioStreamVorbis();
284 scope(failure
) delete sio
;
285 sio
.mType
= Type
.Vorbis
;
286 sio
.mOnlyMeta
= onlymeta
;
288 sio
.vi
= ov_info(&vf
, -1);
289 if (sio
.vi
.rate
< 1024 || sio
.vi
.rate
> 96000) throw new Exception("fucked vorbis");
290 if (sio
.vi
.channels
< 1 || sio
.vi
.channels
> 2) throw new Exception("fucked vorbis");
291 sio
.mRate
= sio
.vi
.rate
;
292 sio
.mChannels
= cast(ubyte)sio
.vi
.channels
;
293 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
294 //conwriteln("streams: ", ov_streams(&sio.vf));
295 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
296 sio
.mSamplesTotal
= ov_pcm_total(&vf
)*sio
.mChannels
;
297 if (auto vc
= ov_comment(&vf
, -1)) {
298 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
299 foreach (immutable idx
; 0..vc
.comments
) {
300 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
301 auto cmts
= vc
.user_comments
[idx
][0..vc
.comment_lengths
[idx
]];
302 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
303 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
304 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
307 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
309 try { ov_clear(&vf
); } catch (Exception e
) {}
321 // ////////////////////////////////////////////////////////////////////////// //
322 final class AudioStreamFlac
: AudioStream
{
330 override void close () {
331 if (ff
!is null) { drflac_close(ff
); ff
= null; }
335 override int readFrames (void* buf
, int count
) {
336 if (count
< 1) return 0;
337 if (count
> int.max
/4) count
= int.max
/4;
338 if (!valid || onlyMeta
) return 0;
340 if (ff
is null) return 0;
341 int[512] flcbuf
= void;
344 short* bp
= cast(short*)buf
;
346 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
347 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
349 mSamplesRead
+= rd
; // number of samples read
350 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
354 return cast(int)(res
/mChannels
); // number of frames read
357 override ulong seekToTime (uint msecs
) {
358 if (!valid || onlyMeta
) return 0;
359 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
361 if (ff
is null) return 0;
362 if (ff
.totalSampleCount
< 1) return 0;
363 if (snum
>= ff
.totalSampleCount
) {
364 drflac_seek_to_sample(ff
, 0);
367 if (!drflac_seek_to_sample(ff
, snum
)) {
368 drflac_seek_to_sample(ff
, 0);
372 return snum
/mChannels
;
376 static AudioStreamFlac
detect (VFile fl
, bool onlymeta
) {
377 import core
.stdc
.stdio
;
378 import core
.stdc
.stdlib
: malloc
, free
;
381 scope(exit
) if (fcmts
!is null) free(fcmts
);
382 drflac
* ff
= drflac_open_file(fl
, (void* pUserData
, drflac_metadata
* pMetadata
) {
383 if (pMetadata
.type
== DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT
) {
384 if (fcmts
!is null) free(fcmts
);
385 auto csz
= drflac_vorbis_comment_size(pMetadata
.data
.vorbis_comment
.commentCount
, pMetadata
.data
.vorbis_comment
.comments
);
386 if (csz
> 0 && csz
< 0x100_0000) {
387 fcmts
= cast(char*)malloc(cast(uint)csz
);
394 import core
.stdc
.string
: memcpy
;
395 commentCount
= pMetadata
.data
.vorbis_comment
.commentCount
;
396 memcpy(fcmts
, pMetadata
.data
.vorbis_comment
.comments
, cast(uint)csz
);
401 scope(failure
) drflac_close(ff
);
402 if (ff
.sampleRate
< 1024 || ff
.sampleRate
> 96000) throw new Exception("fucked flac");
403 if (ff
.channels
< 1 || ff
.channels
> 2) throw new Exception("fucked flac");
404 AudioStreamFlac sio
= new AudioStreamFlac();
405 scope(failure
) delete sio
;
406 sio
.mRate
= cast(uint)ff
.sampleRate
;
407 sio
.mChannels
= cast(ubyte)ff
.channels
;
408 sio
.mType
= Type
.Flac
;
409 sio
.mSamplesTotal
= ff
.totalSampleCount
;
410 sio
.mOnlyMeta
= onlymeta
;
413 sio
.mOnlyMeta
= false;
416 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
418 drflac_vorbis_comment_iterator i
;
419 drflac_init_vorbis_comment_iterator(&i
, commentCount
, fcmts
);
421 const(char)* pComment
;
422 while ((pComment
= drflac_next_vorbis_comment(&i
, &commentLength
)) !is null) {
423 if (commentLength
> 1024*1024*2) break; // just in case
424 //conwriteln(" ", pComment[0..commentLength]);
425 auto cmts
= pComment
[0..commentLength
];
426 //conwriteln(" <", cmts, ">");
427 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
428 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
429 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
432 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
440 // ////////////////////////////////////////////////////////////////////////// //
441 final class AudioStreamMp3
: AudioStream
{
444 Mp3Info mp3info
; // scanned info, frame index
451 override void close () {
452 if (mp3
!is null && mp3
.valid
) { mp3
.close(); delete mp3
; }
453 delete mp3info
.index
;
454 mp3info
= Mp3Info
.init
;
458 override int readFrames (void* buf
, int count
) {
459 if (count
< 1) return 0;
460 if (count
> int.max
/4) count
= int.max
/4;
461 if (!valid || onlyMeta
) return 0;
463 // yes, i know that frames are not independent, and i should actually
464 // seek to a frame with a correct sync word. meh.
465 if (!mp3
.valid
) return 0;
466 auto mfm
= mp3
.frameSamples
;
467 if (mp3smpused
+mChannels
> mfm
.length
) {
469 if (!mp3
.decodeNextFrame(&reader
)) return 0;
470 mfm
= mp3
.frameSamples
;
471 if (mp3
.sampleRate
!= mRate || mp3
.channels
!= mChannels
) return 0;
474 ushort* b
= cast(ushort*)buf
;
475 auto oldmpu
= mp3smpused
;
476 while (count
> 0 && mp3smpused
+mChannels
<= mfm
.length
) {
477 *b
++ = mfm
[mp3smpused
++];
478 if (mChannels
== 2) *b
++ = mfm
[mp3smpused
++];
482 mSamplesRead
+= mp3smpused
-oldmpu
; // number of samples read
486 override ulong seekToTime (uint msecs
) {
487 if (!valid || onlyMeta
) return 0;
488 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
490 if (!mp3
.valid
) return 0;
492 if (mp3info
.index
.length
== 0 || snum
== 0) {
493 // alas, we cannot seek here
496 mp3
.restart(&reader
);
499 // find frame containing our sample
500 // stupid binary search; ignore overflow bug
502 ulong end
= mp3info
.index
.length
-1;
503 while (start
<= end
) {
504 ulong mid
= (start
+end
)/2;
505 auto smps
= mp3info
.index
[cast(usize
)mid
].samples
;
506 auto smpe
= (mp3info
.index
.length
-mid
> 0 ? mp3info
.index
[cast(usize
)(mid
+1)].samples
: mSamplesTotal
);
507 if (snum
>= smps
&& snum
< smpe
) {
510 fl
.seek(mp3info
.index
[cast(usize
)mid
].fpos
);
511 mp3smpused
= cast(uint)(snum
-smps
);
515 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
517 // alas, we cannot seek
520 mp3
.restart(&reader
);
525 static AudioStreamMp3
detect (VFile fl
, bool onlymeta
) {
526 auto fpos
= fl
.tell
; // usually 0, but...
527 AudioStreamMp3 sio
= new AudioStreamMp3();
528 scope(failure
) delete sio
;
530 scope(failure
) sio
.fl
.close();
531 sio
.mp3
= new MP3Decoder(&sio
.reader
);
532 scope(failure
) delete sio
.mp3
;
533 sio
.mType
= Type
.Mp3
;
535 // scan file to determine number of frames
536 auto xfp
= fl
.tell
; // mp3 decoder already buffered some data
539 sio
.mOnlyMeta
= true;
540 sio
.mp3info
= mp3Scan
!false((void[] buf
) => cast(int)fl
.rawRead(buf
).length
);
542 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
544 if (sio
.mp3info
.valid
) {
545 if (sio
.mp3
.sampleRate
< 1024 || sio
.mp3
.sampleRate
> 96000) throw new Exception("fucked mp3");
546 if (sio
.mp3
.channels
< 1 || sio
.mp3
.channels
> 2) throw new Exception("fucked mp3");
547 sio
.mRate
= sio
.mp3
.sampleRate
;
548 sio
.mChannels
= sio
.mp3
.channels
;
549 sio
.mSamplesTotal
= sio
.mp3info
.samples
;
550 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
551 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
552 //conwriteln("id3v2: ", sio.mp3info.hasID3v2, "; ofs: ", sio.mp3info.id3v2ofs);
554 if (sio
.mp3info
.hasID3v2
) {
557 fl
.seek(fpos
+sio
.mp3info
.id3v2ofs
);
558 if (idtag
.scanParse
!false(fl
)) {
559 sio
.album
= idtag
.album
;
560 sio
.artist
= idtag
.artist
;
561 sio
.title
= idtag
.title
;
563 } catch (Exception e
) {}