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*/;
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 // ////////////////////////////////////////////////////////////////////////// //
41 private import iv
.encoding
;
42 private import iv
.vfs
;
43 private import iv
.vfs
.io
;
46 static string
koi2tr (const(char)[] s
) {
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
;
86 if (res
.length
> 0 && res
[$-1] != '_') res
~= '_';
89 while (res
.length
&& res
[$-1] == '_') res
= res
[0..$-1];
90 if (res
.length
== 0) res
= "_";
96 string artist
; // performer
99 uint year
; // 0: unknown
101 ulong startmsecs
; // index 01
105 ulong parseIndex (const(char)[] s
) {
106 import std
.algorithm
: splitter
;
107 import std
.conv
: to
;
108 import std
.range
: enumerate
;
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);
126 uint year
; // 0: unknown
131 void clear () { this = this.init
; }
133 void load (const(char)[] fname
) { load(VFile(fname
)); }
135 void load (VFile fl
) {
137 scope(failure
) clear();
139 char lastSavedChar
= 0;
141 bool firstLine
= true;
147 if (line
.length
>= 3 && line
[0..3] == "\xEF\xBB\xBF") line
= line
[3..$]; // fuck BOM
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
];
159 char ch
= linebuf
[pos
];
161 line
= linebuf
[0..pos
];
165 rd
= fl
.rawRead((&lastSavedChar
)[0..1]);
166 if (rd
.length
== 1 && lastSavedChar
== '\n') lastSavedChar
= 0;
167 line
= linebuf
[0..pos
];
172 throw new Exception("line too long!");
176 const(char)[] nextWord(bool doupper
) () {
177 while (line
.length
&& line
[0] <= ' ') line
= line
[1..$];
178 if (line
.length
== 0) return null;
181 if (line
[0] == '"') {
183 while (epos
< line
.length
&& line
[epos
] != '"') {
185 if (line
[epos
] == '\\' && line
.length
-epos
> 1) epos
+= 2; else ++epos
;
188 if (epos
< line
.length
) {
189 assert(line
[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];
198 while (epos
< line
.length
&& line
[epos
] > ' ') ++epos
;
200 line
= line
[epos
..$];
203 if (res
!is null && !res
.utf8Valid
) return res
.recode("utf-8", "cp1251");
204 static if (doupper
) {
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;
219 //writeln("[", line, "]");
220 auto w
= nextWord
!true();
221 if (w
is null) continue;
223 case "REM": // special
226 case "DATE": case "YEAR":
227 w
= nextWord
!false();
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
;
235 w
= nextWord
!false();
237 if (tracks
.length
) tracks
[$-1].genre
= w
.idup
; else genre
= w
.idup
;
243 case "TRACK": // new track
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");
254 if (w
!= "AUDIO") throw new Exception("non-audio track");
257 w
= nextWord
!false();
259 if (tracks
.length
) tracks
[$-1].artist
= w
.idup
; else artist
= w
.idup
;
263 w
= nextWord
!false();
265 if (tracks
.length
) tracks
[$-1].title
= w
.idup
; else album
= w
.idup
;
269 w
= nextWord
!false();
271 if (tracks
.length
) tracks
[$-1].filename
= w
.idup
; else filename
= w
.idup
;
275 // mm:ss:ff (minute-second-frame) format. There are 75 such frames per second of audio
276 // 00: pregap, optional
278 if (tracks
.length
== 0) throw new Exception("index without track");
279 w
= nextWord
!false();
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");
289 case "PREGAP": case "POSTGAP": break; // ignore
290 case "ISRC": case "CATALOG": case "FLAGS": case "CDTEXTFILE": break;
293 writeln("unknown CUE keyword: '", w
, "'");
294 throw new Exception("invalid keyword");
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
, ">");
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
;
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);
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') */
428 vorbis_comment_init(&vc
);
431 drflac_vorbis_comment_iterator i;
432 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
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('=');
441 writeln("invalid comment: [", cmt, "]");
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
);
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 */
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
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
]);
517 long samplesDone
= 0, prc
= 0;
518 MonoTime lastPrcTime
= MonoTime
.currTime
;
519 if (progressms
>= 0) write(" 0%");
521 uint rdsmp
= cast(uint)(totalSamples
-samplesDone
> smpbuffer
.length ? smpbuffer
.length
: totalSamples
-samplesDone
);
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);
530 auto rdx
= drflac_read_s32(flc
, rdsmp
, smpbuffer
.ptr
); // interleaved 32-bit samples
532 // alas -- the thing that should not be
534 vorbis_analysis_wrote(&vd
, 0);
537 if (progressms
>= 0) {
538 auto nprc
= 100*samplesDone
/totalSamples
;
540 auto ctt
= MonoTime
.currTime
;
541 if ((ctt
-lastPrcTime
).total
!"msecs" >= progressms
) {
543 if (prc
>= 0) write("\x08\x08\x08\x08");
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;
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)
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");
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);
607 ulong samplesProcessed
= 0;
608 foreach (immutable tidx
, ref trk
; cue
.tracks
) {
609 import std
.format
: format
;
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
) {
628 import std
.file
: exists
;
630 concmd("exec .encoder.rc tan");
631 conProcessArgs
!true(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
) {
642 foreach (DirEntry
de; dirEntries(dir
, "*.flac", SpanMode
.shallow
)) {
644 if (flacfile
.length
) assert(0, "too many flac files");
645 //writeln("flac: <", de.name, ">");
649 if (flacfile
.length
== 0) assert(0, "no flac file");
652 void findCue (string dir
) {
653 //writeln("dir: <", dir, ">");
656 foreach (DirEntry
de; dirEntries(dir
, "*.cue", SpanMode
.shallow
)) {
658 //writeln("cue: <", de.name, ">");
659 if (cuefile
.length
) assert(0, "too many cue files");
663 if (cuefile
.length
== 0) assert(0, "no cue file");
667 if (args
.length
< 2) assert(0, "input file?");
669 if (args
.length
> 2) {
670 if (args
.length
> 3) assert(0, "too many input files");
671 if (flacfile
.extension
.strEquCI(".flac")) {
673 if (cuefile
.extension
.strEquCI(".cue")) assert(0, "invalid input files");
674 } else if (flacfile
.extension
.strEquCI(".cue")) {
677 if (flacfile
.extension
.strEquCI(".flac")) assert(0, "invalid input files");
679 assert(0, "invalid input files");
682 if (flacfile
.extension
.strEquCI(".cue")) {
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
);
690 if (exists(flacfile
~".flac")) {
692 findCue(flacfile
.dirName
);
693 } else if (exists(flacfile
~".cue")) {
694 cuefile
= flacfile
~".cue";
695 findFlac(cuefile
.dirName
);
702 writeln("FLAC: ", flacfile
);
703 writeln("CUE : ", cuefile
);
708 if (cue
.tracks
.length
== 0) assert(0, "no tracks");
709 if (cue
.tracks
[0].startmsecs
!= 0) assert(0, "found first hidden track");
712 makeOggs(flacfile
, cue
, quality
);