egra: checkbox cosmetix
[iv.d.git] / audiostream.d
blob88ba551d70f942c1dc0c83678555dc8a125611b0
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*/;
17 private:
19 import iv.alice;
20 //import iv.cmdcon;
21 import iv.id3v2;
22 import iv.mp3scan;
23 import iv.strex;
24 import iv.utfutil;
25 import iv.vfs;
27 import iv.dopus;
28 import iv.drflac;
29 import iv.minimp3;
30 import iv.tremor;
33 // ////////////////////////////////////////////////////////////////////////// //
34 public class AudioStream {
35 public:
36 enum Type {
37 Unknown,
38 Opus,
39 Vorbis,
40 Flac,
41 Mp3,
44 protected:
45 VFile fl;
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;
53 protected:
55 final int reader (void[] buf) {
56 try {
57 auto rd = fl.rawRead(buf);
58 return cast(int)rd.length;
59 } catch (Exception e) {}
60 return -1;
63 protected:
64 this () {}
66 public:
67 string album;
68 string artist;
69 string title;
71 public:
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; }
85 void close () {
86 mType = Type.Unknown;
87 mRate = 1;
88 mChannels = 1;
89 mSamplesTotal = mSamplesRead = 0;
90 album = artist = title = null;
91 fl.close();
94 abstract int readFrames (void* buf, int count);
96 // return new frame index
97 abstract ulong seekToTime (uint msecs);
99 public:
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;
105 didit = true;
106 //conwriteln("trying ", T.stringof);
107 try {
108 fl.seek(0);
109 if (auto ast = T.detect(fl, onlymeta)) return ast;
110 } catch (Exception e) {
111 //conwriteln("DETECT ERROR: ", e.msg);
113 return null;
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); }
121 try {
122 auto fname = fl.name;
123 auto extpos = fname.lastIndexOf('.');
124 if (extpos >= 0) {
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) {}
137 return null;
142 // ////////////////////////////////////////////////////////////////////////// //
143 final class AudioStreamOpus : AudioStream {
144 private:
145 OpusFile of;
146 short[] smpbuf;
147 uint smpbufpos, smpbufused;
149 protected:
150 this () {}
152 public:
153 override void close () {
154 opusClose(of);
155 delete smpbuf;
156 smpbufpos = smpbufused = 0;
157 super.close();
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;
167 int total = 0;
168 while (count > 0) {
169 while (count > 0 && smpbufpos < smpbufused) {
170 *dptr++ = smpbuf.ptr[smpbufpos++];
171 if (mChannels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
172 --count;
173 ++total;
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[];
188 smpbufpos = 0;
189 smpbufused = cast(uint)rd.length;
191 return total;
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;
199 of.seek(msecs);
200 mSamplesRead = of.smpcurtime*mChannels;
201 return mSamplesRead/mChannels;
204 protected:
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();
211 sio.of = of;
212 sio.mType = Type.Opus;
213 sio.fl = fl;
214 sio.mRate = of.rate;
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;
227 if (onlymeta) {
228 scope(exit) { of = null; sio.of = null; sio.fl.close(); }
229 opusClose(sio.of);
231 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
232 return sio;
237 // ////////////////////////////////////////////////////////////////////////// //
238 final class AudioStreamVorbis : AudioStream {
239 private:
240 OggVorbis_File vf;
241 vorbis_info* vi;
243 protected:
244 this () {}
246 public:
247 override void close () {
248 if (vi !is null) { vi = null; ov_clear(&vf); }
249 super.close();
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;
258 int currstream = 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;
274 ov_pcm_seek(&vf, 0);
275 return 0;
278 protected:
279 static AudioStreamVorbis detect (VFile fl, bool onlymeta) {
280 OggVorbis_File vf;
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;
287 sio.fl = fl;
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);
308 if (onlymeta) {
309 try { ov_clear(&vf); } catch (Exception e) {}
310 sio.fl.close();
311 } else {
312 sio.vf = vf;
314 return sio;
316 return null;
321 // ////////////////////////////////////////////////////////////////////////// //
322 final class AudioStreamFlac : AudioStream {
323 private:
324 drflac* ff;
326 protected:
327 this () {}
329 public:
330 override void close () {
331 if (ff !is null) { drflac_close(ff); ff = null; }
332 super.close();
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;
342 int res = 0;
343 count *= mChannels;
344 short* bp = cast(short*)buf;
345 while (count > 0) {
346 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
347 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
348 if (rd <= 0) break;
349 mSamplesRead += rd; // number of samples read
350 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
351 res += rd;
352 count -= rd;
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);
365 return 0;
367 if (!drflac_seek_to_sample(ff, snum)) {
368 drflac_seek_to_sample(ff, 0);
369 return 0;
371 mSamplesRead = snum;
372 return snum/mChannels;
375 protected:
376 static AudioStreamFlac detect (VFile fl, bool onlymeta) {
377 import core.stdc.stdio;
378 import core.stdc.stdlib : malloc, free;
379 uint commentCount;
380 char* fcmts;
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);
388 } else {
389 fcmts = null;
391 if (fcmts is null) {
392 commentCount = 0;
393 } else {
394 import core.stdc.string : memcpy;
395 commentCount = pMetadata.data.vorbis_comment.commentCount;
396 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
400 if (ff !is null) {
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;
411 if (!onlymeta) {
412 sio.ff = ff;
413 sio.mOnlyMeta = false;
414 sio.fl = fl;
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);
420 uint commentLength;
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);
433 return sio;
435 return null;
440 // ////////////////////////////////////////////////////////////////////////// //
441 final class AudioStreamMp3 : AudioStream {
442 private:
443 MP3Decoder mp3;
444 Mp3Info mp3info; // scanned info, frame index
445 uint mp3smpused;
447 protected:
448 this () {}
450 public:
451 override void close () {
452 if (mp3 !is null && mp3.valid) { mp3.close(); delete mp3; }
453 delete mp3info.index;
454 mp3info = Mp3Info.init;
455 super.close();
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) {
468 mp3smpused = 0;
469 if (!mp3.decodeNextFrame(&reader)) return 0;
470 mfm = mp3.frameSamples;
471 if (mp3.sampleRate != mRate || mp3.channels != mChannels) return 0;
473 int res = 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++];
479 --count;
480 ++res;
482 mSamplesRead += mp3smpused-oldmpu; // number of samples read
483 return res;
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;
491 mp3smpused = 0;
492 if (mp3info.index.length == 0 || snum == 0) {
493 // alas, we cannot seek here
494 mSamplesRead = 0;
495 fl.seek(0);
496 mp3.restart(&reader);
497 return 0;
499 // find frame containing our sample
500 // stupid binary search; ignore overflow bug
501 ulong start = 0;
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) {
508 // i found her!
509 mSamplesRead = snum;
510 fl.seek(mp3info.index[cast(usize)mid].fpos);
511 mp3smpused = cast(uint)(snum-smps);
512 mp3.sync(&reader);
513 return snum;
515 if (snum < smps) end = mid-1; else start = mid+1;
517 // alas, we cannot seek
518 mSamplesRead = 0;
519 fl.seek(0);
520 mp3.restart(&reader);
521 return 0;
524 protected:
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;
529 sio.fl = fl;
530 scope(failure) sio.fl.close();
531 sio.mp3 = new MP3Decoder(&sio.reader);
532 scope(failure) delete sio.mp3;
533 sio.mType = Type.Mp3;
534 if (sio.mp3.valid) {
535 // scan file to determine number of frames
536 auto xfp = fl.tell; // mp3 decoder already buffered some data
537 fl.seek(fpos);
538 if (onlymeta) {
539 sio.mOnlyMeta = true;
540 sio.mp3info = mp3Scan!false((void[] buf) => cast(int)fl.rawRead(buf).length);
541 } else {
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);
553 // get metadata
554 if (sio.mp3info.hasID3v2) {
555 try {
556 ID3v2 idtag;
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) {}
565 fl.seek(xfp);
566 return sio;
569 // cleanup
570 sio.fl.close();
571 delete sio.mp3;
572 delete sio;
573 return null;