fixes for new arsd.image
[amper.git] / audiostream.d
blob97ae31fda57d3e02ebab1b2bca36a9fc0f82c3b5
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 audiostream;
18 private:
20 import core.time;
22 import iv.cmdcon;
23 import iv.id3v2;
24 import iv.mp3scan;
25 import iv.strex;
26 import iv.utfutil;
27 import iv.vfs;
29 import iv.dopus;
30 import iv.drflac;
31 import iv.minimp3;
32 import iv.tremor;
35 // ////////////////////////////////////////////////////////////////////////// //
36 public class AudioStream {
37 public:
38 enum Type {
39 Unknown,
40 Opus,
41 Vorbis,
42 Flac,
43 Mp3,
46 protected:
47 VFile fl;
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;
55 protected:
57 final int reader (void[] buf) {
58 try {
59 auto rd = fl.rawRead(buf);
60 return cast(int)rd.length;
61 } catch (Exception e) {}
62 return -1;
65 protected:
66 this () {}
68 public:
69 string album;
70 string artist;
71 string title;
73 public:
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; }
87 void close () {
88 mType = Type.Unknown;
89 mRate = 1;
90 mChannels = 1;
91 mSamplesTotal = mSamplesRead = 0;
92 album = artist = title = null;
93 fl.close();
96 abstract int readFrames (void* buf, int count);
98 // return new frame index
99 abstract ulong seekToTime (uint msecs);
101 public:
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;
107 didit = true;
108 //conwriteln("trying ", T.stringof);
109 try {
110 fl.seek(0);
111 if (auto ast = T.detect(fl, onlymeta)) return ast;
112 } catch (Exception e) {
113 //conwriteln("DETECT ERROR: ", e.msg);
115 return null;
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); }
123 try {
124 auto fname = fl.name;
125 auto extpos = fname.lastIndexOf('.');
126 if (extpos >= 0) {
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) {}
139 return null;
144 // ////////////////////////////////////////////////////////////////////////// //
145 final class AudioStreamOpus : AudioStream {
146 private:
147 OpusFile of;
148 short[] smpbuf;
149 uint smpbufpos, smpbufused;
151 protected:
152 this () {}
154 public:
155 override void close () {
156 opusClose(of);
157 delete smpbuf;
158 smpbufpos = smpbufused = 0;
159 super.close();
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;
169 int total = 0;
170 while (count > 0) {
171 while (count > 0 && smpbufpos < smpbufused) {
172 *dptr++ = smpbuf.ptr[smpbufpos++];
173 if (mChannels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
174 --count;
175 ++total;
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[];
190 smpbufpos = 0;
191 smpbufused = cast(uint)rd.length;
193 return total;
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;
201 of.seek(msecs);
202 mSamplesRead = of.smpcurtime*mChannels;
203 return mSamplesRead/mChannels;
206 protected:
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();
213 sio.of = of;
214 sio.mType = Type.Opus;
215 sio.fl = fl;
216 sio.mRate = of.rate;
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;
229 if (onlymeta) {
230 scope(exit) { of = null; sio.of = null; sio.fl.close(); }
231 opusClose(sio.of);
233 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
234 return sio;
239 // ////////////////////////////////////////////////////////////////////////// //
240 final class AudioStreamVorbis : AudioStream {
241 private:
242 OggVorbis_File vf;
243 vorbis_info* vi;
245 protected:
246 this () {}
248 public:
249 override void close () {
250 if (vi !is null) { vi = null; ov_clear(&vf); }
251 super.close();
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;
260 int currstream = 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;
276 ov_pcm_seek(&vf, 0);
277 return 0;
280 protected:
281 static AudioStreamVorbis detect (VFile fl, bool onlymeta) {
282 OggVorbis_File vf;
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;
289 sio.fl = fl;
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);
310 if (onlymeta) {
311 try { ov_clear(&vf); } catch (Exception e) {}
312 sio.fl.close();
313 } else {
314 sio.vf = vf;
316 return sio;
318 return null;
323 // ////////////////////////////////////////////////////////////////////////// //
324 final class AudioStreamFlac : AudioStream {
325 private:
326 drflac* ff;
328 protected:
329 this () {}
331 public:
332 override void close () {
333 if (ff !is null) { drflac_close(ff); ff = null; }
334 super.close();
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;
344 int res = 0;
345 count *= mChannels;
346 short* bp = cast(short*)buf;
347 while (count > 0) {
348 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
349 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
350 if (rd <= 0) break;
351 mSamplesRead += rd; // number of samples read
352 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
353 res += rd;
354 count -= rd;
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);
367 return 0;
369 if (!drflac_seek_to_sample(ff, snum)) {
370 drflac_seek_to_sample(ff, 0);
371 return 0;
373 mSamplesRead = snum;
374 return snum/mChannels;
377 protected:
378 static AudioStreamFlac detect (VFile fl, bool onlymeta) {
379 import core.stdc.stdio;
380 import core.stdc.stdlib : malloc, free;
381 uint commentCount;
382 char* fcmts;
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);
390 } else {
391 fcmts = null;
393 if (fcmts is null) {
394 commentCount = 0;
395 } else {
396 import core.stdc.string : memcpy;
397 commentCount = pMetadata.data.vorbis_comment.commentCount;
398 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
402 if (ff !is null) {
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;
413 if (!onlymeta) {
414 sio.ff = ff;
415 sio.mOnlyMeta = false;
416 sio.fl = fl;
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);
422 uint commentLength;
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);
435 return sio;
437 return null;
442 // ////////////////////////////////////////////////////////////////////////// //
443 final class AudioStreamMp3 : AudioStream {
444 private:
445 MP3Decoder mp3;
446 Mp3Info mp3info; // scanned info, frame index
447 uint mp3smpused;
449 protected:
450 this () {}
452 public:
453 override void close () {
454 if (mp3 !is null && mp3.valid) { mp3.close(); delete mp3; }
455 delete mp3info.index;
456 mp3info = Mp3Info.init;
457 super.close();
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) {
470 mp3smpused = 0;
471 if (!mp3.decodeNextFrame(&reader)) return 0;
472 mfm = mp3.frameSamples;
473 if (mp3.sampleRate != mRate || mp3.channels != mChannels) return 0;
475 int res = 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++];
481 --count;
482 ++res;
484 mSamplesRead += mp3smpused-oldmpu; // number of samples read
485 return res;
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;
493 mp3smpused = 0;
494 if (mp3info.index.length == 0 || snum == 0) {
495 // alas, we cannot seek here
496 mSamplesRead = 0;
497 fl.seek(0);
498 mp3.restart(&reader);
499 return 0;
501 // find frame containing our sample
502 // stupid binary search; ignore overflow bug
503 ulong start = 0;
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) {
510 // i found her!
511 mSamplesRead = snum;
512 fl.seek(mp3info.index[cast(size_t)mid].fpos);
513 mp3smpused = cast(uint)(snum-smps);
514 mp3.sync(&reader);
515 return snum;
517 if (snum < smps) end = mid-1; else start = mid+1;
519 // alas, we cannot seek
520 mSamplesRead = 0;
521 fl.seek(0);
522 mp3.restart(&reader);
523 return 0;
526 protected:
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;
531 sio.fl = fl;
532 scope(failure) sio.fl.close();
533 sio.mp3 = new MP3Decoder(&sio.reader);
534 scope(failure) delete sio.mp3;
535 sio.mType = Type.Mp3;
536 if (sio.mp3.valid) {
537 // scan file to determine number of frames
538 auto xfp = fl.tell; // mp3 decoder already buffered some data
539 fl.seek(fpos);
540 if (onlymeta) {
541 sio.mOnlyMeta = true;
542 sio.mp3info = mp3Scan!false((void[] buf) => cast(int)fl.rawRead(buf).length);
543 } else {
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);
555 // get metadata
556 if (sio.mp3info.hasID3v2) {
557 try {
558 ID3v2 idtag;
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) {}
567 fl.seek(xfp);
568 return sio;
571 // cleanup
572 sio.fl.close();
573 delete sio.mp3;
574 delete sio;
575 return null;