sq3: show SQLite error messages on stderr by default
[iv.d.git] / xyph / samples / flacvt.d
blob247d5d81d47ddb71104205d118ec12f995a7d993
1 /* Written 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 zflacvt /*is aliced*/;
18 import iv.alice;
19 import iv.drflac;
20 import iv.cmdcon;
21 import iv.encoding;
22 import iv.strex;
23 import iv.vfs;
24 import iv.vfs.io;
25 import iv.xyph;
28 // ////////////////////////////////////////////////////////////////////////// //
29 __gshared ubyte quality = 6;
30 __gshared int progressms = 500;
32 shared static this () {
33 conRegVar!quality(0, 9, "quality", "vorbis encoding quality");
34 conRegVar!progressms("progress_time", "progress update time, in milliseconds (-1: don't show progress)");
39 // ////////////////////////////////////////////////////////////////////////// //
40 struct CueFile {
41 private import iv.encoding;
42 private import iv.vfs;
43 private import iv.vfs.io;
45 public:
46 static string koi2tr (const(char)[] s) {
47 string res;
48 foreach (char ch; s) {
49 if (ch == '\xe1' || ch == '\xc1') res ~= "a";
50 else if (ch == '\xe2' || ch == '\xc2') res ~= "b";
51 else if (ch == '\xf7' || ch == '\xd7') res ~= "v";
52 else if (ch == '\xe7' || ch == '\xc7') res ~= "g";
53 else if (ch == '\xe4' || ch == '\xc4') res ~= "d";
54 else if (ch == '\xe5' || ch == '\xc5') res ~= "e";
55 else if (ch == '\xb3' || ch == '\xa3') res ~= "yo";
56 else if (ch == '\xf6' || ch == '\xd6') res ~= "zh";
57 else if (ch == '\xfa' || ch == '\xda') res ~= "z";
58 else if (ch == '\xe9' || ch == '\xc9') res ~= "i";
59 else if (ch == '\xea' || ch == '\xca') res ~= "j";
60 else if (ch == '\xeb' || ch == '\xcb') res ~= "k";
61 else if (ch == '\xec' || ch == '\xcc') res ~= "l";
62 else if (ch == '\xed' || ch == '\xcd') res ~= "m";
63 else if (ch == '\xee' || ch == '\xce') res ~= "n";
64 else if (ch == '\xef' || ch == '\xcf') res ~= "o";
65 else if (ch == '\xf0' || ch == '\xd0') res ~= "p";
66 else if (ch == '\xf2' || ch == '\xd2') res ~= "r";
67 else if (ch == '\xf3' || ch == '\xd3') res ~= "s";
68 else if (ch == '\xf4' || ch == '\xd4') res ~= "t";
69 else if (ch == '\xf5' || ch == '\xd5') res ~= "u";
70 else if (ch == '\xe6' || ch == '\xc6') res ~= "f";
71 else if (ch == '\xe8' || ch == '\xc8') res ~= "h";
72 else if (ch == '\xe3' || ch == '\xc3') res ~= "c";
73 else if (ch == '\xfe' || ch == '\xde') res ~= "ch";
74 else if (ch == '\xfb' || ch == '\xdb') res ~= "sh";
75 else if (ch == '\xfd' || ch == '\xdd') res ~= "sch";
76 else if (ch == '\xff' || ch == '\xdf') {} //res ~= "x"; // tvyordyj znak
77 else if (ch == '\xf9' || ch == '\xd9') res ~= "y";
78 else if (ch == '\xf8' || ch == '\xd8') {} //res ~= "w"; // myagkij znak
79 else if (ch == '\xfc' || ch == '\xdc') res ~= "e";
80 else if (ch == '\xe0' || ch == '\xc0') res ~= "ju";
81 else if (ch == '\xf1' || ch == '\xd1') res ~= "ja";
82 else if (ch >= 'A' && ch <= 'Z') res ~= cast(char)(ch+32);
83 else if (ch >= 'a' && ch <= 'z') res ~= ch;
84 else if (ch >= '0' && ch <= '9') res ~= ch;
85 else {
86 if (res.length > 0 && res[$-1] != '_') res ~= '_';
89 while (res.length && res[$-1] == '_') res = res[0..$-1];
90 if (res.length == 0) res = "_";
91 return res;
94 public:
95 static struct Track {
96 string artist; // performer
97 string title;
98 string genre;
99 uint year; // 0: unknown
100 string filename;
101 ulong startmsecs; // index 01
104 private:
105 ulong parseIndex (const(char)[] s) {
106 import std.algorithm : splitter;
107 import std.conv : to;
108 import std.range : enumerate;
109 uint[3] msf;
110 bool lastHit = false;
111 foreach (immutable idx, /*auto*/ sv; s.splitter(':').enumerate) {
112 if (idx >= msf.length) throw new Exception("invalid index");
113 lastHit = (idx == msf.length-1);
114 msf[idx] = sv.to!uint;
116 if (!lastHit) throw new Exception("invalid index");
117 if (msf[1] > 59) throw new Exception("invalid index");
118 if (msf[2] > 74) throw new Exception("invalid index");
119 return cast(uint)((((msf[1]+msf[0]*60)*75)/75.0)*1000.0);
122 public:
123 string artist;
124 string album;
125 string genre;
126 uint year; // 0: unknown
127 string filename;
128 Track[] tracks;
130 public:
131 void clear () { this = this.init; }
133 void load (const(char)[] fname) { load(VFile(fname)); }
135 void load (VFile fl) {
136 clear();
137 scope(failure) clear();
138 char[4096] linebuf;
139 char lastSavedChar = 0;
140 char[] line;
141 bool firstLine = true;
143 bool readLine () {
144 scope(success) {
145 if (firstLine) {
146 firstLine = false;
147 if (line.length >= 3 && line[0..3] == "\xEF\xBB\xBF") line = line[3..$]; // fuck BOM
150 uint pos = 0;
151 if (lastSavedChar) { linebuf[pos++] = lastSavedChar; lastSavedChar = 0; }
152 while (pos < linebuf.length) {
153 auto rd = fl.rawRead(linebuf[pos..pos+1]);
154 if (rd.length == 0) {
155 if (pos == 0) { line = null; return false; }
156 line = linebuf[0..pos];
157 return true;
159 char ch = linebuf[pos];
160 if (ch == '\n') {
161 line = linebuf[0..pos];
162 return true;
164 if (ch == '\r') {
165 rd = fl.rawRead((&lastSavedChar)[0..1]);
166 if (rd.length == 1 && lastSavedChar == '\n') lastSavedChar = 0;
167 line = linebuf[0..pos];
168 return true;
170 ++pos;
172 throw new Exception("line too long!");
175 // null: EOL
176 const(char)[] nextWord(bool doupper) () {
177 while (line.length && line[0] <= ' ') line = line[1..$];
178 if (line.length == 0) return null;
179 char[] res;
180 uint epos = 1;
181 if (line[0] == '"') {
182 // quoted
183 while (epos < line.length && line[epos] != '"') {
184 // just in case
185 if (line[epos] == '\\' && line.length-epos > 1) epos += 2; else ++epos;
187 res = line[1..epos];
188 if (epos < line.length) {
189 assert(line[epos] == '"');
190 ++epos;
192 line = line[epos..$];
193 // remove spaces (i don't need 'em anyway; and i don't care about idiotic filenames)
194 while (res.length && res[0] <= ' ') res = res[1..$];
195 while (res.length && res[$-1] <= ' ') res = res[0..$-1];
196 } else {
197 // normal
198 while (epos < line.length && line[epos] > ' ') ++epos;
199 res = line[0..epos];
200 line = line[epos..$];
202 // recode
203 if (res !is null && !res.utf8Valid) return res.recode("utf-8", "cp1251");
204 static if (doupper) {
205 if (res !is null) {
206 // upcase
207 bool doconv = false;
208 foreach (char ch; res) {
209 if (ch >= 128) { doconv = false; break; }
210 if (ch >= 'a' && ch <= 'z') doconv = true;
212 if (doconv) foreach (ref char ch; res) if (ch >= 'a' && ch <= 'z') ch -= 32;
215 return res;
218 while (readLine) {
219 //writeln("[", line, "]");
220 auto w = nextWord!true();
221 if (w is null) continue;
222 switch (w) {
223 case "REM": // special
224 w = nextWord!true();
225 switch (w) {
226 case "DATE": case "YEAR":
227 w = nextWord!false();
228 int yr = 0;
229 try { import std.conv : to; yr = w.to!ushort(10); } catch (Exception) {}
230 if (yr >= 1900 && yr <= 3000) {
231 if (tracks.length) tracks[$-1].year = yr; else year = yr;
233 break;
234 case "GENRE":
235 w = nextWord!false();
236 if (w.length) {
237 if (tracks.length) tracks[$-1].genre = w.idup; else genre = w.idup;
239 break;
240 default: break;
242 break;
243 case "TRACK": // new track
244 tracks.length += 1;
245 w = nextWord!true();
246 try {
247 import std.conv : to;
248 auto tn = w.to!ubyte(10);
249 if (tn != tracks.length) throw new Exception("invalid track number");
250 } catch (Exception) {
251 throw new Exception("fucked track number");
253 w = nextWord!true();
254 if (w != "AUDIO") throw new Exception("non-audio track");
255 break;
256 case "PERFORMER":
257 w = nextWord!false();
258 if (w.length) {
259 if (tracks.length) tracks[$-1].artist = w.idup; else artist = w.idup;
261 break;
262 case "TITLE":
263 w = nextWord!false();
264 if (w.length) {
265 if (tracks.length) tracks[$-1].title = w.idup; else album = w.idup;
267 break;
268 case "FILE":
269 w = nextWord!false();
270 if (w.length) {
271 if (tracks.length) tracks[$-1].filename = w.idup; else filename = w.idup;
273 break;
274 case "INDEX":
275 // mm:ss:ff (minute-second-frame) format. There are 75 such frames per second of audio
276 // 00: pregap, optional
277 // 01: song start
278 if (tracks.length == 0) throw new Exception("index without track");
279 w = nextWord!false();
280 try {
281 import std.conv : to;
282 auto n = w.to!ubyte(10);
283 if (n == 1) tracks[$-1].startmsecs = parseIndex(nextWord!true);
284 } catch (Exception e) {
285 writeln("ERROR: ", e.msg);
286 throw new Exception("fucked index");
288 break;
289 case "PREGAP": case "POSTGAP": break; // ignore
290 case "ISRC": case "CATALOG": case "FLAGS": case "CDTEXTFILE": break;
291 // SONGWRITER
292 default:
293 writeln("unknown CUE keyword: '", w, "'");
294 throw new Exception("invalid keyword");
298 // normalize tracks
299 foreach (ref trk; tracks) {
300 if (trk.artist == artist) trk.artist = null;
301 if (trk.year == year) trk.year = 0;
302 if (trk.genre == genre) trk.genre = null;
303 if (trk.filename == filename) trk.filename = null;
307 void dump (VFile fo) {
308 fo.writeln("=======================");
309 if (artist.length) fo.writeln("ARTIST: <", artist.recodeToKOI8, ">");
310 if (album.length) fo.writeln("ALBUM : <", album.recodeToKOI8, ">");
311 if (genre.length) fo.writeln("GENRE : <", genre.recodeToKOI8, ">");
312 if (year) fo.writeln("YEAR : <", year, ">");
313 if (filename.length) fo.writeln("FILE : <", filename.recodeToKOI8, ">");
314 if (tracks.length) {
315 fo.writeln("TRACKS: ", tracks.length);
316 foreach (immutable tidx, const ref trk; tracks) {
317 fo.writefln(" TRACK #%02d: start: %d:%02d.%03d", tidx+1, trk.startmsecs/1000/60, (trk.startmsecs/1000)%60, trk.startmsecs%1000);
318 if (trk.artist.length) fo.writeln(" ARTIST: <", trk.artist.recodeToKOI8, ">");
319 if (trk.title.length) fo.writeln(" TITLE : <", trk.title.recodeToKOI8, ">");
320 if (trk.genre.length) fo.writeln(" GENRE : <", trk.genre.recodeToKOI8, ">");
321 if (trk.year) fo.writeln(" YEAR : <", trk.year, ">");
322 if (trk.filename.length) fo.writeln(" FILE : <", trk.filename.recodeToKOI8, ">");
323 if (trk.title.length) fo.writeln(" XFILE : <", koi2tr(trk.title.recodeToKOI8), ">");
328 void dump () { dump(stdout); }
332 // ////////////////////////////////////////////////////////////////////////// //
333 enum READ = 1024; // there is no reason to use bigger buffer
334 int[READ*2] smpbuffer; // out of the data segment, not the stack
337 // ////////////////////////////////////////////////////////////////////////// //
338 void makeOggs (string flacfile, ref CueFile cue, ubyte quality) {
339 import std.file : mkdirRecurse;
340 import std.string : toStringz;
342 if (quality < 0) quality = 0;
343 if (quality > 9) quality = 9;
345 import core.stdc.stdlib : malloc, free;
346 drflac* flc;
347 uint commentCount;
348 char* fcmts;
349 scope(exit) if (fcmts !is null) free(fcmts);
352 flc = drflac_open_file_with_metadata(flacfile.toStringz, (void* pUserData, drflac_metadata* pMetadata) {
353 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
354 if (fcmts !is null) free(fcmts);
355 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
356 if (csz > 0 && csz < 0x100_0000) {
357 fcmts = cast(char*)malloc(cast(uint)csz);
358 } else {
359 fcmts = null;
361 if (fcmts is null) {
362 commentCount = 0;
363 } else {
364 import core.stdc.string : memcpy;
365 commentCount = pMetadata.data.vorbis_comment.commentCount;
366 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
372 flc = drflac_open_file(flacfile.toStringz);
373 if (flc is null) throw new Exception("can't open input file");
374 scope(exit) drflac_close(flc);
377 if (flc.sampleRate < 1024 || flc.sampleRate > 96000) throw new Exception("invalid flac sample rate");
378 if (flc.channels < 1 || flc.channels > 2) throw new Exception("invalid flac channel number");
379 if (flc.totalSampleCount%flc.channels != 0) throw new Exception("invalid flac sample count");
381 writeln(flc.sampleRate, "Hz, ", flc.channels, " channels; quality=", quality);
383 writeln("=======================");
384 if (cue.artist.length) writeln("ARTIST: <", cue.artist.recodeToKOI8, ">");
385 if (cue.album.length) writeln("ALBUM : <", cue.album.recodeToKOI8, ">");
386 if (cue.genre.length) writeln("GENRE : <", cue.genre.recodeToKOI8, ">");
387 if (cue.year) writeln("YEAR : <", cue.year, ">");
389 void encodeSamples (VFile fo, uint tidx, ulong totalSamples) {
390 import std.conv : to;
392 ogg_stream_state os; // take physical pages, weld into a logical stream of packets
393 ogg_page og; // one Ogg bitstream page. Vorbis packets are inside
394 ogg_packet op; // one raw packet of data for decode
396 vorbis_info vi; // struct that stores all the static vorbis bitstream settings
397 vorbis_comment vc; // struct that stores all the user comments
399 vorbis_dsp_state vd; // central working state for the packet->PCM decoder
400 vorbis_block vb; // local working space for packet->PCM decode
403 vorbis_info_init(&vi);
404 scope(exit) vorbis_info_clear(&vi);
406 // choose an encoding mode. A few possibilities commented out, one actually used:
407 /*********************************************************************
408 Encoding using a VBR quality mode. The usable range is -.1
409 (lowest quality, smallest file) to 1. (highest quality, largest file).
410 Example quality mode .4: 44kHz stereo coupled, roughly 128kbps VBR
412 ret = vorbis_encode_init_vbr(&vi, 2, 44100, .4);
414 ---------------------------------------------------------------------
416 Encoding using an average bitrate mode (ABR).
417 example: 44kHz stereo coupled, average 128kbps VBR
419 ret = vorbis_encode_init(&vi, 2, 44100, -1, 128000, -1);
421 *********************************************************************/
422 if (vorbis_encode_init_vbr(&vi, flc.channels, flc.sampleRate, quality/9.0f) != 0) throw new Exception("cannot init vorbis encoder");
423 /* do not continue if setup failed; this can happen if we ask for a
424 mode that libVorbis does not support (eg, too low a bitrate, etc,
425 will return 'OV_EIMPL') */
427 // add comments
428 vorbis_comment_init(&vc);
431 drflac_vorbis_comment_iterator i;
432 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
433 uint commentLength;
434 const(char)* pComment;
435 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
436 if (commentLength > 1024*1024*2) break; // just in case
437 //comments ~= pComment[0..commentLength].idup;
438 auto cmt = pComment[0..commentLength];
439 auto eqpos = cmt.indexOf('=');
440 if (eqpos < 1) {
441 writeln("invalid comment: [", cmt, "]");
442 } else {
443 import std.string : toStringz;
444 vorbis_comment_add_tag(&vc, cmt[0..eqpos].toStringz, cmt[eqpos+1..$].toStringz);
445 //writeln(" [", cmt[0..eqpos], "] [", cmt[eqpos+1..$], "]");
451 string val = cue.tracks[tidx].artist;
452 if (val.length == 0) val = cue.artist;
453 if (val.length) vorbis_comment_add_tag(&vc, "ARTIST", val.toStringz);
455 if (cue.tracks[tidx].year) vorbis_comment_add_tag(&vc, "DATE", cue.tracks[tidx].year.to!string.toStringz);
456 else if (cue.year) vorbis_comment_add_tag(&vc, "DATE", cue.year.to!string.toStringz);
458 string val = cue.album;
459 if (val.length) vorbis_comment_add_tag(&vc, "ALBUM", val.toStringz);
462 string val = cue.tracks[tidx].title;
463 if (val.length == 0) val = cue.album;
464 if (val.length == 0) val = "untitled";
465 vorbis_comment_add_tag(&vc, "TITLE", val.toStringz);
468 string val = cue.tracks[tidx].artist;
469 if (val.length == 0) val = cue.artist;
470 if (val.length) vorbis_comment_add_tag(&vc, "PERFORMER", val.toStringz);
472 vorbis_comment_add_tag(&vc, "TRACKNUMBER", (tidx+1).to!string.toStringz);
473 vorbis_comment_add_tag(&vc, "TRACKTOTAL", cue.tracks.length.to!string.toStringz);
475 // set up the analysis state and auxiliary encoding storage
476 vorbis_analysis_init(&vd, &vi);
477 vorbis_block_init(&vd, &vb);
478 scope(exit) vorbis_block_clear(&vb);
479 scope(exit) vorbis_dsp_clear(&vd);
481 // set up our packet->stream encoder
482 // pick a random serial number; that way we can more likely build chained streams just by concatenation
484 import std.random : uniform;
485 ogg_stream_init(&os, uniform!"[]"(0, uint.max));
487 scope(exit) ogg_stream_clear(&os);
489 bool eos = false;
491 /* Vorbis streams begin with three headers; the initial header (with
492 most of the codec setup parameters) which is mandated by the Ogg
493 bitstream spec. The second header holds any comment fields. The
494 third header holds the bitstream codebook. We merely need to
495 make the headers, then pass them to libvorbis one at a time;
496 libvorbis handles the additional Ogg bitstream constraints */
498 ogg_packet header;
499 ogg_packet header_comm;
500 ogg_packet header_code;
502 vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code);
503 ogg_stream_packetin(&os, &header); // automatically placed in its own page
504 ogg_stream_packetin(&os, &header_comm);
505 ogg_stream_packetin(&os, &header_code);
507 // this ensures the actual audio data will start on a new page, as per spec
508 while (!eos) {
509 int result = ogg_stream_flush(&os, &og);
510 if (result == 0) break;
511 fo.rawWriteExact(og.header[0..og.header_len]);
512 fo.rawWriteExact(og.body[0..og.body_len]);
516 import core.time;
517 long samplesDone = 0, prc = 0;
518 MonoTime lastPrcTime = MonoTime.currTime;
519 if (progressms >= 0) write(" 0%");
520 while (!eos) {
521 uint rdsmp = cast(uint)(totalSamples-samplesDone > smpbuffer.length ? smpbuffer.length : totalSamples-samplesDone);
522 if (rdsmp == 0) {
523 /* end of file. this can be done implicitly in the mainline,
524 but it's easier to see here in non-clever fashion.
525 Tell the library we're at end of stream so that it can handle
526 the last frame and mark end of stream in the output properly */
527 vorbis_analysis_wrote(&vd, 0);
528 //writeln("DONE!");
529 } else {
530 auto rdx = drflac_read_s32(flc, rdsmp, smpbuffer.ptr); // interleaved 32-bit samples
531 if (rdx < 1) {
532 // alas -- the thing that should not be
533 writeln("FUCK!");
534 vorbis_analysis_wrote(&vd, 0);
535 } else {
536 samplesDone += rdx;
537 if (progressms >= 0) {
538 auto nprc = 100*samplesDone/totalSamples;
539 if (nprc != prc) {
540 auto ctt = MonoTime.currTime;
541 if ((ctt-lastPrcTime).total!"msecs" >= progressms) {
542 lastPrcTime = ctt;
543 if (prc >= 0) write("\x08\x08\x08\x08");
544 prc = nprc;
545 writef("%3d%%", prc);
550 // expose the buffer to submit data
551 uint frames = cast(uint)(rdx/flc.channels);
552 float** buffer = vorbis_analysis_buffer(&vd, /*READ*/frames);
554 // uninterleave samples
555 auto wd = smpbuffer.ptr;
556 foreach (immutable i; 0..frames) {
557 foreach (immutable cn; 0..flc.channels) {
558 buffer[cn][i] = ((*wd)>>16)/32768.0f;
559 ++wd;
563 // tell the library how much we actually submitted
564 vorbis_analysis_wrote(&vd, frames);
568 /* vorbis does some data preanalysis, then divvies up blocks for
569 more involved (potentially parallel) processing. Get a single
570 block for encoding now */
571 while (vorbis_analysis_blockout(&vd, &vb) == 1) {
572 // analysis, assume we want to use bitrate management
573 vorbis_analysis(&vb, null);
574 vorbis_bitrate_addblock(&vb);
575 while (vorbis_bitrate_flushpacket(&vd, &op)){
576 // weld the packet into the bitstream
577 ogg_stream_packetin(&os, &op);
578 // write out pages (if any)
579 while (!eos) {
580 int result = ogg_stream_pageout(&os, &og);
581 if (result == 0) break;
582 //fwrite(og.header, 1, og.header_len, stdout);
583 //fwrite(og.body, 1, og.body_len, stdout);
584 fo.rawWriteExact(og.header[0..og.header_len]);
585 fo.rawWriteExact(og.body[0..og.body_len]);
586 // this could be set above, but for illustrative purposes, I do it here (to show that vorbis does know where the stream ends)
587 if (ogg_page_eos(&og)) eos = true;
592 if (progressms >= 0) {
593 if (prc >= 0) write("\x08\x08\x08\x08");
595 writeln("DONE");
597 // clean up and exit. vorbis_info_clear() must be called last
598 // ogg_page and ogg_packet structs always point to storage in libvorbis. They're never freed or manipulated directly
599 //ogg_stream_clear(&os);
600 //vorbis_block_clear(&vb);
601 //vorbis_dsp_clear(&vd);
602 //vorbis_comment_clear(&vc);
603 //vorbis_info_clear(&vi);
606 mkdirRecurse("ogg");
607 ulong samplesProcessed = 0;
608 foreach (immutable tidx, ref trk; cue.tracks) {
609 import std.format : format;
610 string fname;
611 if (trk.title.length) fname = CueFile.koi2tr(trk.title.recodeToKOI8); else fname = "untitled";
612 string ofname = "ogg/%02d_%s.ogg".format(tidx, fname);
613 write("[", tidx+1, "/", cue.tracks.length, "] ", (trk.title.length ? trk.title.recodeToKOI8 : "untitled"), " -> ", ofname, " ");
614 ulong smpstart, smpend;
615 if (tidx == 0) smpstart = 0; else smpstart = cast(ulong)((trk.startmsecs/1000.0)*flc.sampleRate)*flc.channels;
616 if (smpstart != samplesProcessed) assert(0, "index fucked");
617 if (tidx == cue.tracks.length-1) smpend = flc.totalSampleCount; else smpend = cast(ulong)((cue.tracks[tidx+1].startmsecs/1000.0)*flc.sampleRate)*flc.channels;
618 if (smpend <= samplesProcessed) assert(0, "index fucked");
619 samplesProcessed = smpend;
620 encodeSamples(VFile(ofname, "w"), tidx, smpend-smpstart);
625 // ////////////////////////////////////////////////////////////////////////// //
626 void main (string[] args) {
627 import std.path;
628 import std.file : exists;
630 concmd("exec .encoder.rc tan");
631 conProcessArgs!true(args);
633 //writeln(args);
635 string flacfile, cuefile;
636 //if (args.length == 1) args ~= "linda_karandashi_i_spichki.cue";
637 if (args.length < 2) assert(0, "filename?");
639 void findFlac (string dir) {
640 import std.file;
641 flacfile = null;
642 foreach (DirEntry de; dirEntries(dir, "*.flac", SpanMode.shallow)) {
643 if (de.isFile) {
644 if (flacfile.length) assert(0, "too many flac files");
645 //writeln("flac: <", de.name, ">");
646 flacfile = de.name;
649 if (flacfile.length == 0) assert(0, "no flac file");
652 void findCue (string dir) {
653 //writeln("dir: <", dir, ">");
654 import std.file;
655 cuefile = null;
656 foreach (DirEntry de; dirEntries(dir, "*.cue", SpanMode.shallow)) {
657 if (de.isFile) {
658 //writeln("cue: <", de.name, ">");
659 if (cuefile.length) assert(0, "too many cue files");
660 cuefile = de.name;
663 if (cuefile.length == 0) assert(0, "no cue file");
667 if (args.length < 2) assert(0, "input file?");
668 flacfile = args[1];
669 if (args.length > 2) {
670 if (args.length > 3) assert(0, "too many input files");
671 if (flacfile.extension.strEquCI(".flac")) {
672 cuefile = args[2];
673 if (cuefile.extension.strEquCI(".cue")) assert(0, "invalid input files");
674 } else if (flacfile.extension.strEquCI(".cue")) {
675 cuefile = flacfile;
676 flacfile = args[2];
677 if (flacfile.extension.strEquCI(".flac")) assert(0, "invalid input files");
678 } else {
679 assert(0, "invalid input files");
681 } else {
682 if (flacfile.extension.strEquCI(".cue")) {
683 cuefile = flacfile;
684 flacfile = cuefile.setExtension(".flac");
685 if (!flacfile.exists) findFlac(flacfile.dirName);
686 } else if (flacfile.extension.strEquCI(".flac")) {
687 cuefile = flacfile.setExtension(".cue");
688 if (!cuefile.exists) findCue(cuefile.dirName);
689 } else {
690 if (exists(flacfile~".flac")) {
691 flacfile ~= ".flac";
692 findCue(flacfile.dirName);
693 } else if (exists(flacfile~".cue")) {
694 cuefile = flacfile~".cue";
695 findFlac(cuefile.dirName);
696 } else {
697 assert(0, "wtf?!");
702 writeln("FLAC: ", flacfile);
703 writeln("CUE : ", cuefile);
705 CueFile cue;
706 cue.load(cuefile);
708 if (cue.tracks.length == 0) assert(0, "no tracks");
709 if (cue.tracks[0].startmsecs != 0) assert(0, "found first hidden track");
711 //cue.dump();
712 makeOggs(flacfile, cue, quality);