zymosis: cosmetix
[iv.d.git] / audiostream.d
blob69231d0d96a6cefc2392a2e500d5e97573e61a59
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*/;
18 private:
20 import iv.alice;
21 //import iv.cmdcon;
22 import iv.id3v2;
23 import iv.mp3scan;
24 import iv.strex;
25 import iv.utfutil;
26 import iv.vfs;
28 import iv.dopus;
29 import iv.drflac;
30 import iv.minimp3;
31 import iv.tremor;
34 // ////////////////////////////////////////////////////////////////////////// //
35 public class AudioStream {
36 public:
37 enum Type {
38 Unknown,
39 Opus,
40 Vorbis,
41 Flac,
42 Mp3,
45 protected:
46 VFile fl;
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;
54 protected:
56 final int reader (void[] buf) {
57 try {
58 auto rd = fl.rawRead(buf);
59 return cast(int)rd.length;
60 } catch (Exception e) {}
61 return -1;
64 protected:
65 this () {}
67 public:
68 string album;
69 string artist;
70 string title;
72 public:
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; }
86 void close () {
87 mType = Type.Unknown;
88 mRate = 1;
89 mChannels = 1;
90 mSamplesTotal = mSamplesRead = 0;
91 album = artist = title = null;
92 fl.close();
95 abstract int readFrames (void* buf, int count);
97 // return new frame index
98 abstract ulong seekToTime (uint msecs);
100 public:
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;
106 didit = true;
107 //conwriteln("trying ", T.stringof);
108 try {
109 fl.seek(0);
110 if (auto ast = T.detect(fl, onlymeta)) return ast;
111 } catch (Exception e) {
112 //conwriteln("DETECT ERROR: ", e.msg);
114 return null;
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); }
122 try {
123 auto fname = fl.name;
124 auto extpos = fname.lastIndexOf('.');
125 if (extpos >= 0) {
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) {}
138 return null;
143 // ////////////////////////////////////////////////////////////////////////// //
144 final class AudioStreamOpus : AudioStream {
145 private:
146 OpusFile of;
147 short[] smpbuf;
148 uint smpbufpos, smpbufused;
150 protected:
151 this () {}
153 public:
154 override void close () {
155 opusClose(of);
156 delete smpbuf;
157 smpbufpos = smpbufused = 0;
158 super.close();
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;
168 int total = 0;
169 while (count > 0) {
170 while (count > 0 && smpbufpos < smpbufused) {
171 *dptr++ = smpbuf.ptr[smpbufpos++];
172 if (mChannels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
173 --count;
174 ++total;
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[];
189 smpbufpos = 0;
190 smpbufused = cast(uint)rd.length;
192 return total;
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;
200 of.seek(msecs);
201 mSamplesRead = of.smpcurtime*mChannels;
202 return mSamplesRead/mChannels;
205 protected:
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();
212 sio.of = of;
213 sio.mType = Type.Opus;
214 sio.fl = fl;
215 sio.mRate = of.rate;
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;
228 if (onlymeta) {
229 scope(exit) { of = null; sio.of = null; sio.fl.close(); }
230 opusClose(sio.of);
232 //conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
233 return sio;
238 // ////////////////////////////////////////////////////////////////////////// //
239 final class AudioStreamVorbis : AudioStream {
240 private:
241 OggVorbis_File vf;
242 vorbis_info* vi;
244 protected:
245 this () {}
247 public:
248 override void close () {
249 if (vi !is null) { vi = null; ov_clear(&vf); }
250 super.close();
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;
259 int currstream = 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;
275 ov_pcm_seek(&vf, 0);
276 return 0;
279 protected:
280 static AudioStreamVorbis detect (VFile fl, bool onlymeta) {
281 OggVorbis_File vf;
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;
288 sio.fl = fl;
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);
309 if (onlymeta) {
310 try { ov_clear(&vf); } catch (Exception e) {}
311 sio.fl.close();
312 } else {
313 sio.vf = vf;
315 return sio;
317 return null;
322 // ////////////////////////////////////////////////////////////////////////// //
323 final class AudioStreamFlac : AudioStream {
324 private:
325 drflac* ff;
327 protected:
328 this () {}
330 public:
331 override void close () {
332 if (ff !is null) { drflac_close(ff); ff = null; }
333 super.close();
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;
343 int res = 0;
344 count *= mChannels;
345 short* bp = cast(short*)buf;
346 while (count > 0) {
347 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
348 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
349 if (rd <= 0) break;
350 mSamplesRead += rd; // number of samples read
351 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
352 res += rd;
353 count -= rd;
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);
366 return 0;
368 if (!drflac_seek_to_sample(ff, snum)) {
369 drflac_seek_to_sample(ff, 0);
370 return 0;
372 mSamplesRead = snum;
373 return snum/mChannels;
376 protected:
377 static AudioStreamFlac detect (VFile fl, bool onlymeta) {
378 import core.stdc.stdio;
379 import core.stdc.stdlib : malloc, free;
380 uint commentCount;
381 char* fcmts;
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);
389 } else {
390 fcmts = null;
392 if (fcmts is null) {
393 commentCount = 0;
394 } else {
395 import core.stdc.string : memcpy;
396 commentCount = pMetadata.data.vorbis_comment.commentCount;
397 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
401 if (ff !is null) {
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;
412 if (!onlymeta) {
413 sio.ff = ff;
414 sio.mOnlyMeta = false;
415 sio.fl = fl;
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);
421 uint commentLength;
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);
434 return sio;
436 return null;
441 // ////////////////////////////////////////////////////////////////////////// //
442 final class AudioStreamMp3 : AudioStream {
443 private:
444 MP3Decoder mp3;
445 Mp3Info mp3info; // scanned info, frame index
446 uint mp3smpused;
448 protected:
449 this () {}
451 public:
452 override void close () {
453 if (mp3 !is null && mp3.valid) { mp3.close(); delete mp3; }
454 delete mp3info.index;
455 mp3info = Mp3Info.init;
456 super.close();
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) {
469 mp3smpused = 0;
470 if (!mp3.decodeNextFrame(&reader)) return 0;
471 mfm = mp3.frameSamples;
472 if (mp3.sampleRate != mRate || mp3.channels != mChannels) return 0;
474 int res = 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++];
480 --count;
481 ++res;
483 mSamplesRead += mp3smpused-oldmpu; // number of samples read
484 return res;
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;
492 mp3smpused = 0;
493 if (mp3info.index.length == 0 || snum == 0) {
494 // alas, we cannot seek here
495 mSamplesRead = 0;
496 fl.seek(0);
497 mp3.restart(&reader);
498 return 0;
500 // find frame containing our sample
501 // stupid binary search; ignore overflow bug
502 ulong start = 0;
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) {
509 // i found her!
510 mSamplesRead = snum;
511 fl.seek(mp3info.index[cast(usize)mid].fpos);
512 mp3smpused = cast(uint)(snum-smps);
513 mp3.sync(&reader);
514 return snum;
516 if (snum < smps) end = mid-1; else start = mid+1;
518 // alas, we cannot seek
519 mSamplesRead = 0;
520 fl.seek(0);
521 mp3.restart(&reader);
522 return 0;
525 protected:
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;
530 sio.fl = fl;
531 scope(failure) sio.fl.close();
532 sio.mp3 = new MP3Decoder(&sio.reader);
533 scope(failure) delete sio.mp3;
534 sio.mType = Type.Mp3;
535 if (sio.mp3.valid) {
536 // scan file to determine number of frames
537 auto xfp = fl.tell; // mp3 decoder already buffered some data
538 fl.seek(fpos);
539 if (onlymeta) {
540 sio.mOnlyMeta = true;
541 sio.mp3info = mp3Scan!false((void[] buf) => cast(int)fl.rawRead(buf).length);
542 } else {
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);
554 // get metadata
555 if (sio.mp3info.hasID3v2) {
556 try {
557 ID3v2 idtag;
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) {}
566 fl.seek(xfp);
567 return sio;
570 // cleanup
571 sio.fl.close();
572 delete sio.mp3;
573 delete sio;
574 return null;