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/>.
35 // ////////////////////////////////////////////////////////////////////////// //
36 public class AudioStream
{
48 Type mType
= Type
.Unknown
;
49 uint mRate
= 1; // just in case
50 ubyte mChannels
= 1; // just in case
51 ulong mSamplesTotal
; // multiplied by channels
52 ulong mSamplesRead
; // samples read so far, multiplied by channels
53 bool mOnlyMeta
= false;
57 final int reader (void[] buf
) {
59 auto rd
= fl
.rawRead(buf
);
60 return cast(int)rd
.length
;
61 } catch (Exception e
) {}
74 final @property uint rate () const pure nothrow @safe @nogc { pragma(inline
, true); return mRate
; }
75 final @property ubyte channels () const pure nothrow @safe @nogc { pragma(inline
, true); return mChannels
; }
77 final @property ulong framesRead () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesRead
/mChannels
; }
78 final @property ulong framesTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return mSamplesTotal
/mChannels
; }
80 final @property uint timeRead () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesRead
*1000/mRate
/mChannels
); }
81 final @property uint timeTotal () const pure nothrow @safe @nogc { pragma(inline
, true); return cast(uint)(mSamplesTotal
*1000/mRate
/mChannels
); }
83 final @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (mType
!= Type
.Unknown
); }
85 final @property bool onlyMeta () const pure nothrow @safe @nogc { pragma(inline
, true); return mOnlyMeta
; }
91 mSamplesTotal
= mSamplesRead
= 0;
92 album
= artist
= title
= null;
96 abstract int readFrames (void* buf
, int count
);
98 // return new frame index
99 abstract ulong seekToTime (uint msecs
);
102 static AudioStream
detect (VFile fl
, bool onlymeta
=false) nothrow {
103 bool didOpus
, didVorbis
, didFlac
, didMp3
;
105 AudioStream
tryFormat(T
: AudioStream
) (ref bool didit
) nothrow {
106 if (didit
) return null;
108 //conwriteln("trying ", T.stringof);
111 if (auto ast
= T
.detect(fl
, onlymeta
)) return ast
;
112 } catch (Exception e
) {
113 //conwriteln("DETECT ERROR: ", e.msg);
118 AudioStream
tryOpus () nothrow { return tryFormat
!AudioStreamOpus(didOpus
); }
119 AudioStream
tryVorbis () nothrow { return tryFormat
!AudioStreamVorbis(didVorbis
); }
120 AudioStream
tryFlac () nothrow { return tryFormat
!AudioStreamFlac(didFlac
); }
121 AudioStream
tryMp3 () nothrow { return tryFormat
!AudioStreamMp3(didMp3
); }
124 auto fname
= fl
.name
;
125 auto extpos
= fname
.lastIndexOf('.');
127 auto ext
= fname
[extpos
..$];
128 if (ext
.strEquCI(".opus")) { if (auto ast
= tryOpus()) return ast
; }
129 else if (ext
.strEquCI(".ogg")) { if (auto ast
= tryVorbis()) return ast
; }
130 else if (ext
.strEquCI(".flac")) { if (auto ast
= tryFlac()) return ast
; }
131 else if (ext
.strEquCI(".mp3")) { if (auto ast
= tryMp3()) return ast
; }
133 // this is fastest for my collection
134 if (auto ast
= tryFlac()) return ast
;
135 if (auto ast
= tryOpus()) return ast
;
136 if (auto ast
= tryVorbis()) return ast
;
137 if (auto ast
= tryMp3()) return ast
;
138 } catch (Exception e
) {}
144 // ////////////////////////////////////////////////////////////////////////// //
145 final class AudioStreamOpus
: AudioStream
{
149 uint smpbufpos
, smpbufused
;
155 override void close () {
158 smpbufpos
= smpbufused
= 0;
162 override int readFrames (void* buf
, int count
) {
163 if (count
< 1) return 0;
164 if (count
> int.max
/4) count
= int.max
/4;
165 if (!valid || onlyMeta
) return 0;
167 auto dptr
= cast(short*)buf
;
168 if (of
is null) return 0;
171 while (count
> 0 && smpbufpos
< smpbufused
) {
172 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
173 if (mChannels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
176 mSamplesRead
+= mChannels
;
178 if (count
== 0) break;
179 auto rd
= of
.readFrame();
180 if (rd
.length
== 0) break;
181 if (rd
.length
> smpbuf
.length
) {
182 auto optr
= smpbuf
.ptr
;
183 smpbuf
.length
= rd
.length
;
184 if (smpbuf
.ptr
!is optr
) {
185 import core
.memory
: GC
;
186 if (smpbuf
.ptr
is GC
.addrOf(smpbuf
.ptr
)) GC
.setAttr(smpbuf
.ptr
, GC
.BlkAttr
.NO_INTERIOR
);
189 smpbuf
[0..rd
.length
] = rd
[];
191 smpbufused
= cast(uint)rd
.length
;
196 override ulong seekToTime (uint msecs
) {
197 if (!valid || onlyMeta
) return 0;
198 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
200 if (of
is null) return 0;
202 mSamplesRead
= of
.smpcurtime
*mChannels
;
203 return mSamplesRead
/mChannels
;
207 static AudioStreamOpus
detect (VFile fl
, bool onlymeta
) {
208 OpusFile of
= opusOpen(fl
);
209 scope(failure
) opusClose(of
);
210 if (of
.rate
< 1024 || of
.rate
> 96000) throw new Exception("fucked opus");
211 if (of
.channels
< 1 || of
.channels
> 2) throw new Exception("fucked opus");
212 AudioStreamOpus sio
= new AudioStreamOpus();
214 sio
.mType
= Type
.Opus
;
217 sio
.mChannels
= of
.channels
;
218 sio
.mOnlyMeta
= onlymeta
;
219 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
220 sio
.mSamplesTotal
= of
.smpduration
*sio
.mChannels
;
221 //if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
222 foreach (immutable cidx
; 0..of
.commentCount
) {
223 //conwriteln(" ", of.comment(cidx).recodeToKOI8);
224 auto cmts
= of
.comment(cidx
);
225 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
226 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
227 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
230 scope(exit
) { of
= null; sio
.of
= null; sio
.fl
.close(); }
233 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
239 // ////////////////////////////////////////////////////////////////////////// //
240 final class AudioStreamVorbis
: AudioStream
{
249 override void close () {
250 if (vi
!is null) { vi
= null; ov_clear(&vf
); }
254 override int readFrames (void* buf
, int count
) {
255 if (count
< 1) return 0;
256 if (count
> int.max
/4) count
= int.max
/4;
257 if (!valid || onlyMeta
) return 0;
259 if (vi
is null) return 0;
261 auto ret = ov_read(&vf
, cast(ubyte*)buf
, count
*2*mChannels
, &currstream
);
262 if (ret <= 0) return 0; // error or eof
263 mSamplesRead
+= ret/2; // number of samples read
264 return ret/2/mChannels
; // number of frames read
267 override ulong seekToTime (uint msecs
) {
268 if (!valid || onlyMeta
) return 0;
269 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
271 if (vi
is null) return 0;
272 if (ov_pcm_seek(&vf
, snum
/mChannels
) == 0) {
273 mSamplesRead
= ov_pcm_tell(&vf
)*mChannels
;
274 return mSamplesRead
/mChannels
;
281 static AudioStreamVorbis
detect (VFile fl
, bool onlymeta
) {
283 if (ov_fopen(fl
, &vf
) == 0) {
284 scope(failure
) ov_clear(&vf
);
285 auto sio
= new AudioStreamVorbis();
286 scope(failure
) delete sio
;
287 sio
.mType
= Type
.Vorbis
;
288 sio
.mOnlyMeta
= onlymeta
;
290 sio
.vi
= ov_info(&vf
, -1);
291 if (sio
.vi
.rate
< 1024 || sio
.vi
.rate
> 96000) throw new Exception("fucked vorbis");
292 if (sio
.vi
.channels
< 1 || sio
.vi
.channels
> 2) throw new Exception("fucked vorbis");
293 sio
.mRate
= sio
.vi
.rate
;
294 sio
.mChannels
= cast(ubyte)sio
.vi
.channels
;
295 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
296 //conwriteln("streams: ", ov_streams(&sio.vf));
297 //conwriteln("bitrate: ", ov_bitrate(&sio.vf));
298 sio
.mSamplesTotal
= ov_pcm_total(&vf
)*sio
.mChannels
;
299 if (auto vc
= ov_comment(&vf
, -1)) {
300 //conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
301 foreach (immutable idx
; 0..vc
.comments
) {
302 //conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
303 auto cmts
= vc
.user_comments
[idx
][0..vc
.comment_lengths
[idx
]];
304 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
305 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
306 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
309 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
311 try { ov_clear(&vf
); } catch (Exception e
) {}
323 // ////////////////////////////////////////////////////////////////////////// //
324 final class AudioStreamFlac
: AudioStream
{
332 override void close () {
333 if (ff
!is null) { drflac_close(ff
); ff
= null; }
337 override int readFrames (void* buf
, int count
) {
338 if (count
< 1) return 0;
339 if (count
> int.max
/4) count
= int.max
/4;
340 if (!valid || onlyMeta
) return 0;
342 if (ff
is null) return 0;
343 int[512] flcbuf
= void;
346 short* bp
= cast(short*)buf
;
348 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
349 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
351 mSamplesRead
+= rd
; // number of samples read
352 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
356 return cast(int)(res
/mChannels
); // number of frames read
359 override ulong seekToTime (uint msecs
) {
360 if (!valid || onlyMeta
) return 0;
361 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
363 if (ff
is null) return 0;
364 if (ff
.totalSampleCount
< 1) return 0;
365 if (snum
>= ff
.totalSampleCount
) {
366 drflac_seek_to_sample(ff
, 0);
369 if (!drflac_seek_to_sample(ff
, snum
)) {
370 drflac_seek_to_sample(ff
, 0);
374 return snum
/mChannels
;
378 static AudioStreamFlac
detect (VFile fl
, bool onlymeta
) {
379 import core
.stdc
.stdio
;
380 import core
.stdc
.stdlib
: malloc
, free
;
383 scope(exit
) if (fcmts
!is null) free(fcmts
);
384 drflac
* ff
= drflac_open_file(fl
, (void* pUserData
, drflac_metadata
* pMetadata
) {
385 if (pMetadata
.type
== DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT
) {
386 if (fcmts
!is null) free(fcmts
);
387 auto csz
= drflac_vorbis_comment_size(pMetadata
.data
.vorbis_comment
.commentCount
, pMetadata
.data
.vorbis_comment
.comments
);
388 if (csz
> 0 && csz
< 0x100_0000) {
389 fcmts
= cast(char*)malloc(cast(uint)csz
);
396 import core
.stdc
.string
: memcpy
;
397 commentCount
= pMetadata
.data
.vorbis_comment
.commentCount
;
398 memcpy(fcmts
, pMetadata
.data
.vorbis_comment
.comments
, cast(uint)csz
);
403 scope(failure
) drflac_close(ff
);
404 if (ff
.sampleRate
< 1024 || ff
.sampleRate
> 96000) throw new Exception("fucked flac");
405 if (ff
.channels
< 1 || ff
.channels
> 2) throw new Exception("fucked flac");
406 AudioStreamFlac sio
= new AudioStreamFlac();
407 scope(failure
) delete sio
;
408 sio
.mRate
= cast(uint)ff
.sampleRate
;
409 sio
.mChannels
= cast(ubyte)ff
.channels
;
410 sio
.mType
= Type
.Flac
;
411 sio
.mSamplesTotal
= ff
.totalSampleCount
;
412 sio
.mOnlyMeta
= onlymeta
;
415 sio
.mOnlyMeta
= false;
418 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
420 drflac_vorbis_comment_iterator i
;
421 drflac_init_vorbis_comment_iterator(&i
, commentCount
, fcmts
);
423 const(char)* pComment
;
424 while ((pComment
= drflac_next_vorbis_comment(&i
, &commentLength
)) !is null) {
425 if (commentLength
> 1024*1024*2) break; // just in case
426 //conwriteln(" ", pComment[0..commentLength]);
427 auto cmts
= pComment
[0..commentLength
];
428 //conwriteln(" <", cmts, ">");
429 if (cmts
.startsWithCI("ALBUM=")) sio
.album
= cmts
[6..$].xstrip
.idup
;
430 else if (cmts
.startsWithCI("ARTIST=")) sio
.artist
= cmts
[7..$].xstrip
.idup
;
431 else if (cmts
.startsWithCI("TITLE=")) sio
.title
= cmts
[6..$].xstrip
.idup
;
434 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
442 // ////////////////////////////////////////////////////////////////////////// //
443 final class AudioStreamMp3
: AudioStream
{
446 Mp3Info mp3info
; // scanned info, frame index
453 override void close () {
454 if (mp3
!is null && mp3
.valid
) { mp3
.close(); delete mp3
; }
455 delete mp3info
.index
;
456 mp3info
= Mp3Info
.init
;
460 override int readFrames (void* buf
, int count
) {
461 if (count
< 1) return 0;
462 if (count
> int.max
/4) count
= int.max
/4;
463 if (!valid || onlyMeta
) return 0;
465 // yes, i know that frames are not independent, and i should actually
466 // seek to a frame with a correct sync word. meh.
467 if (!mp3
.valid
) return 0;
468 auto mfm
= mp3
.frameSamples
;
469 if (mp3smpused
+mChannels
> mfm
.length
) {
471 if (!mp3
.decodeNextFrame(&reader
)) return 0;
472 mfm
= mp3
.frameSamples
;
473 if (mp3
.sampleRate
!= mRate || mp3
.channels
!= mChannels
) return 0;
476 ushort* b
= cast(ushort*)buf
;
477 auto oldmpu
= mp3smpused
;
478 while (count
> 0 && mp3smpused
+mChannels
<= mfm
.length
) {
479 *b
++ = mfm
[mp3smpused
++];
480 if (mChannels
== 2) *b
++ = mfm
[mp3smpused
++];
484 mSamplesRead
+= mp3smpused
-oldmpu
; // number of samples read
488 override ulong seekToTime (uint msecs
) {
489 if (!valid || onlyMeta
) return 0;
490 ulong snum
= cast(ulong)msecs
*mRate
/1000*mChannels
; // sample number
492 if (!mp3
.valid
) return 0;
494 if (mp3info
.index
.length
== 0 || snum
== 0) {
495 // alas, we cannot seek here
498 mp3
.restart(&reader
);
501 // find frame containing our sample
502 // stupid binary search; ignore overflow bug
504 ulong end
= mp3info
.index
.length
-1;
505 while (start
<= end
) {
506 ulong mid
= (start
+end
)/2;
507 auto smps
= mp3info
.index
[cast(size_t
)mid
].samples
;
508 auto smpe
= (mp3info
.index
.length
-mid
> 0 ? mp3info
.index
[cast(size_t
)(mid
+1)].samples
: mSamplesTotal
);
509 if (snum
>= smps
&& snum
< smpe
) {
512 fl
.seek(mp3info
.index
[cast(size_t
)mid
].fpos
);
513 mp3smpused
= cast(uint)(snum
-smps
);
517 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
519 // alas, we cannot seek
522 mp3
.restart(&reader
);
527 static AudioStreamMp3
detect (VFile fl
, bool onlymeta
) {
528 auto fpos
= fl
.tell
; // usually 0, but...
529 AudioStreamMp3 sio
= new AudioStreamMp3();
530 scope(failure
) delete sio
;
532 scope(failure
) sio
.fl
.close();
533 sio
.mp3
= new MP3Decoder(&sio
.reader
);
534 scope(failure
) delete sio
.mp3
;
535 sio
.mType
= Type
.Mp3
;
537 // scan file to determine number of frames
538 auto xfp
= fl
.tell
; // mp3 decoder already buffered some data
541 sio
.mOnlyMeta
= true;
542 sio
.mp3info
= mp3Scan
!false((void[] buf
) => cast(int)fl
.rawRead(buf
).length
);
544 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
546 if (sio
.mp3info
.valid
) {
547 if (sio
.mp3
.sampleRate
< 1024 || sio
.mp3
.sampleRate
> 96000) throw new Exception("fucked mp3");
548 if (sio
.mp3
.channels
< 1 || sio
.mp3
.channels
> 2) throw new Exception("fucked mp3");
549 sio
.mRate
= sio
.mp3
.sampleRate
;
550 sio
.mChannels
= sio
.mp3
.channels
;
551 sio
.mSamplesTotal
= sio
.mp3info
.samples
;
552 //conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
553 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
554 //conwriteln("id3v2: ", sio.mp3info.hasID3v2, "; ofs: ", sio.mp3info.id3v2ofs);
556 if (sio
.mp3info
.hasID3v2
) {
559 fl
.seek(fpos
+sio
.mp3info
.id3v2ofs
);
560 if (idtag
.scanParse
!false(fl
)) {
561 sio
.album
= idtag
.album
;
562 sio
.artist
= idtag
.artist
;
563 sio
.title
= idtag
.title
;
565 } catch (Exception e
) {}