sq3: show SQLite error messages on stderr by default
[iv.d.git] / xyph / samples / zflacvorbis.d
blob5103bae9a34497a2fdd1e670da4340cb76074031
1 #!/usr/bin/env rdmd
2 module zflacvt /*is aliced*/;
4 import iv.alice;
5 import iv.drflac;
6 import iv.cmdcon;
7 import iv.cuefile;
8 import iv.encoding;
9 import iv.strex;
10 import iv.vfs;
11 import iv.vfs.io;
12 import iv.xyph;
15 // ////////////////////////////////////////////////////////////////////////// //
16 __gshared ubyte quality = 6;
17 __gshared int progressms = 500;
18 __gshared uint toffset = 0;
19 __gshared uint tmax = 0;
21 shared static this () {
22 conRegVar!quality(0, 9, "quality", "vorbis encoding quality");
23 conRegVar!progressms("progress_time", "progress update time, in milliseconds (-1: don't show progress)");
24 conRegVar!toffset("toffset", "track offset");
25 conRegVar!tmax("tmax", "maxumum track number (0: default)");
30 // ////////////////////////////////////////////////////////////////////////// //
31 enum READ = 1024; // there is no reason to use bigger buffer
32 int[READ*2] smpbuffer; // out of the data segment, not the stack
35 // ////////////////////////////////////////////////////////////////////////// //
36 void makeOggs (string flacfile, ref CueFile cue, ubyte quality) {
37 import std.file : mkdirRecurse;
38 import std.string : toStringz;
40 if (quality < 0) quality = 0;
41 if (quality > 9) quality = 9;
43 import core.stdc.stdlib : malloc, free;
44 drflac* flc;
45 uint commentCount;
46 char* fcmts;
47 scope(exit) if (fcmts !is null) free(fcmts);
50 flc = drflac_open_file_with_metadata(flacfile.toStringz, (void* pUserData, drflac_metadata* pMetadata) {
51 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
52 if (fcmts !is null) free(fcmts);
53 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
54 if (csz > 0 && csz < 0x100_0000) {
55 fcmts = cast(char*)malloc(cast(uint)csz);
56 } else {
57 fcmts = null;
59 if (fcmts is null) {
60 commentCount = 0;
61 } else {
62 import core.stdc.string : memcpy;
63 commentCount = pMetadata.data.vorbis_comment.commentCount;
64 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
67 });
70 flc = drflac_open_file(flacfile);
71 if (flc is null) throw new Exception("can't open input file");
72 scope(exit) drflac_close(flc);
75 if (flc.sampleRate < 1024 || flc.sampleRate > 96000) throw new Exception("invalid flac sample rate");
76 if (flc.channels < 1 || flc.channels > 2) throw new Exception("invalid flac channel number");
77 if (flc.totalSampleCount%flc.channels != 0) throw new Exception("invalid flac sample count");
79 writeln(flc.sampleRate, "Hz, ", flc.channels, " channels; quality=", quality);
81 writeln("=======================");
82 if (cue.artist.length) writeln("ARTIST: <", cue.artist.recodeToKOI8, ">");
83 if (cue.album.length) writeln("ALBUM : <", cue.album.recodeToKOI8, ">");
84 if (cue.genre.length) writeln("GENRE : <", cue.genre.recodeToKOI8, ">");
85 if (cue.year) writeln("YEAR : <", cue.year, ">");
87 void encodeSamples (VFile fo, uint tidx, ulong totalSamples) {
88 import std.conv : to;
90 ogg_stream_state os; // take physical pages, weld into a logical stream of packets
91 ogg_page og; // one Ogg bitstream page. Vorbis packets are inside
92 ogg_packet op; // one raw packet of data for decode
94 vorbis_info vi; // struct that stores all the static vorbis bitstream settings
95 vorbis_comment vc; // struct that stores all the user comments
97 vorbis_dsp_state vd; // central working state for the packet->PCM decoder
98 vorbis_block vb; // local working space for packet->PCM decode
101 vorbis_info_init(&vi);
102 scope(exit) vorbis_info_clear(&vi);
104 // choose an encoding mode. A few possibilities commented out, one actually used:
105 /*********************************************************************
106 Encoding using a VBR quality mode. The usable range is -.1
107 (lowest quality, smallest file) to 1. (highest quality, largest file).
108 Example quality mode .4: 44kHz stereo coupled, roughly 128kbps VBR
110 ret = vorbis_encode_init_vbr(&vi, 2, 44100, .4);
112 ---------------------------------------------------------------------
114 Encoding using an average bitrate mode (ABR).
115 example: 44kHz stereo coupled, average 128kbps VBR
117 ret = vorbis_encode_init(&vi, 2, 44100, -1, 128000, -1);
119 *********************************************************************/
120 if (vorbis_encode_init_vbr(&vi, flc.channels, flc.sampleRate, quality/9.0f) != 0) throw new Exception("cannot init vorbis encoder");
121 /* do not continue if setup failed; this can happen if we ask for a
122 mode that libVorbis does not support (eg, too low a bitrate, etc,
123 will return 'OV_EIMPL') */
125 // add comments
126 vorbis_comment_init(&vc);
129 drflac_vorbis_comment_iterator i;
130 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
131 uint commentLength;
132 const(char)* pComment;
133 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
134 if (commentLength > 1024*1024*2) break; // just in case
135 //comments ~= pComment[0..commentLength].idup;
136 auto cmt = pComment[0..commentLength];
137 auto eqpos = cmt.indexOf('=');
138 if (eqpos < 1) {
139 writeln("invalid comment: [", cmt, "]");
140 } else {
141 import std.string : toStringz;
142 vorbis_comment_add_tag(&vc, cmt[0..eqpos].toStringz, cmt[eqpos+1..$].toStringz);
143 //writeln(" [", cmt[0..eqpos], "] [", cmt[eqpos+1..$], "]");
149 string val = cue.tracks[tidx].artist;
150 if (val.length == 0) val = cue.artist;
151 if (val.length) vorbis_comment_add_tag(&vc, "ARTIST", val.toStringz);
153 if (cue.tracks[tidx].year) vorbis_comment_add_tag(&vc, "DATE", cue.tracks[tidx].year.to!string.toStringz);
154 else if (cue.year) vorbis_comment_add_tag(&vc, "DATE", cue.year.to!string.toStringz);
156 string val = cue.album;
157 if (val.length) vorbis_comment_add_tag(&vc, "ALBUM", val.toStringz);
160 string val = cue.tracks[tidx].title;
161 if (val.length == 0) val = cue.album;
162 if (val.length == 0) val = "untitled";
163 vorbis_comment_add_tag(&vc, "TITLE", val.toStringz);
166 string val = cue.tracks[tidx].artist;
167 if (val.length == 0) val = cue.artist;
168 if (val.length) vorbis_comment_add_tag(&vc, "PERFORMER", val.toStringz);
170 vorbis_comment_add_tag(&vc, "TRACKNUMBER", (tidx+1+toffset).to!string.toStringz);
171 vorbis_comment_add_tag(&vc, "TRACKTOTAL", (tmax ? tmax : cue.tracks.length+toffset).to!string.toStringz);
173 // set up the analysis state and auxiliary encoding storage
174 vorbis_analysis_init(&vd, &vi);
175 vorbis_block_init(&vd, &vb);
176 scope(exit) vorbis_block_clear(&vb);
177 scope(exit) vorbis_dsp_clear(&vd);
179 // set up our packet->stream encoder
180 // pick a random serial number; that way we can more likely build chained streams just by concatenation
182 import std.random : uniform;
183 ogg_stream_init(&os, uniform!"[]"(1, cast(uint)(uint.max-1)));
185 scope(exit) ogg_stream_clear(&os);
187 bool eos = false;
189 /* Vorbis streams begin with three headers; the initial header (with
190 most of the codec setup parameters) which is mandated by the Ogg
191 bitstream spec. The second header holds any comment fields. The
192 third header holds the bitstream codebook. We merely need to
193 make the headers, then pass them to libvorbis one at a time;
194 libvorbis handles the additional Ogg bitstream constraints */
196 ogg_packet header;
197 ogg_packet header_comm;
198 ogg_packet header_code;
200 vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code);
201 ogg_stream_packetin(&os, &header); // automatically placed in its own page
202 ogg_stream_packetin(&os, &header_comm);
203 ogg_stream_packetin(&os, &header_code);
205 // this ensures the actual audio data will start on a new page, as per spec
206 while (!eos) {
207 int result = ogg_stream_flush(&os, &og);
208 if (result == 0) break;
209 fo.rawWriteExact(og.header[0..og.header_len]);
210 fo.rawWriteExact(og.body[0..og.body_len]);
214 import core.time;
215 long samplesDone = 0, prc = 0;
216 MonoTime lastPrcTime = MonoTime.currTime;
217 if (progressms >= 0) write(" 0%");
218 while (!eos) {
219 uint rdsmp = cast(uint)(totalSamples-samplesDone > smpbuffer.length ? smpbuffer.length : totalSamples-samplesDone);
220 if (rdsmp == 0) {
221 /* end of file. this can be done implicitly in the mainline,
222 but it's easier to see here in non-clever fashion.
223 Tell the library we're at end of stream so that it can handle
224 the last frame and mark end of stream in the output properly */
225 vorbis_analysis_wrote(&vd, 0);
226 //writeln("DONE!");
227 } else {
228 auto rdx = drflac_read_s32(flc, rdsmp, smpbuffer.ptr); // interleaved 32-bit samples
229 if (rdx < 1) {
230 // alas -- the thing that should not be
231 writeln("FUCK!");
232 vorbis_analysis_wrote(&vd, 0);
233 } else {
234 samplesDone += rdx;
235 if (progressms >= 0) {
236 auto nprc = 100*samplesDone/totalSamples;
237 if (nprc != prc) {
238 auto ctt = MonoTime.currTime;
239 if ((ctt-lastPrcTime).total!"msecs" >= progressms) {
240 lastPrcTime = ctt;
241 if (prc >= 0) write("\x08\x08\x08\x08");
242 prc = nprc;
243 writef("%3d%%", prc);
248 // expose the buffer to submit data
249 uint frames = cast(uint)(rdx/flc.channels);
250 float** buffer = vorbis_analysis_buffer(&vd, /*READ*/frames);
252 // uninterleave samples
253 auto wd = smpbuffer.ptr;
254 foreach (immutable i; 0..frames) {
255 foreach (immutable cn; 0..flc.channels) {
256 buffer[cn][i] = ((*wd)>>16)/32768.0f;
257 ++wd;
261 // tell the library how much we actually submitted
262 vorbis_analysis_wrote(&vd, frames);
266 /* vorbis does some data preanalysis, then divvies up blocks for
267 more involved (potentially parallel) processing. Get a single
268 block for encoding now */
269 while (vorbis_analysis_blockout(&vd, &vb) == 1) {
270 // analysis, assume we want to use bitrate management
271 vorbis_analysis(&vb, null);
272 vorbis_bitrate_addblock(&vb);
273 while (vorbis_bitrate_flushpacket(&vd, &op)){
274 // weld the packet into the bitstream
275 ogg_stream_packetin(&os, &op);
276 // write out pages (if any)
277 while (!eos) {
278 int result = ogg_stream_pageout(&os, &og);
279 if (result == 0) break;
280 //fwrite(og.header, 1, og.header_len, stdout);
281 //fwrite(og.body, 1, og.body_len, stdout);
282 fo.rawWriteExact(og.header[0..og.header_len]);
283 fo.rawWriteExact(og.body[0..og.body_len]);
284 // this could be set above, but for illustrative purposes, I do it here (to show that vorbis does know where the stream ends)
285 if (ogg_page_eos(&og)) eos = true;
290 if (progressms >= 0) {
291 if (prc >= 0) write("\x08\x08\x08\x08");
293 writeln("DONE");
295 // clean up and exit. vorbis_info_clear() must be called last
296 // ogg_page and ogg_packet structs always point to storage in libvorbis. They're never freed or manipulated directly
297 //ogg_stream_clear(&os);
298 //vorbis_block_clear(&vb);
299 //vorbis_dsp_clear(&vd);
300 //vorbis_comment_clear(&vc);
301 //vorbis_info_clear(&vi);
304 mkdirRecurse("ogg");
305 ulong samplesProcessed = 0;
306 foreach (immutable tidx, ref trk; cue.tracks) {
307 import std.format : format;
308 string fname;
309 if (trk.title.length) fname = CueFile.koi2trlocase(trk.title.recodeToKOI8); else fname = "untitled";
310 string ofname = "ogg/%02d_%s.ogg".format(tidx+1+toffset, fname);
311 write("[", tidx+1, "/", cue.tracks.length, "] ", (trk.title.length ? trk.title.recodeToKOI8 : "untitled"), " -> ", ofname, " ");
312 ulong smpstart, smpend;
313 if (tidx == 0) smpstart = 0; else smpstart = cast(ulong)((trk.startmsecs/1000.0)*flc.sampleRate)*flc.channels;
314 if (smpstart != samplesProcessed) assert(0, "index fucked");
315 if (tidx == cue.tracks.length-1) smpend = flc.totalSampleCount; else smpend = cast(ulong)((cue.tracks[tidx+1].startmsecs/1000.0)*flc.sampleRate)*flc.channels;
316 if (smpend <= samplesProcessed) assert(0, "index fucked");
317 samplesProcessed = smpend;
318 encodeSamples(VFile(ofname, "w"), tidx, smpend-smpstart);
323 // ////////////////////////////////////////////////////////////////////////// //
324 void main (string[] args) {
325 import std.path;
326 import std.file : exists;
328 concmd("exec .encoder.rc tan");
329 conProcessArgs!true(args);
332 string flacfile, cuefile;
334 if (args.length < 2) {
335 import std.file : dirEntries, DirEntry, SpanMode;
336 foreach (DirEntry de; dirEntries(".", SpanMode.shallow)) {
337 import std.path;
338 if (de.isFile && de.extension.strEquCI(".cue")) {
339 if (args.length == 2) assert(0, "filename?");
340 args ~= de.name;
343 if (args.length < 2) assert(0, "filename?");
346 void findFlac (string dir) {
347 import std.file;
348 flacfile = null;
349 foreach (DirEntry de; dirEntries(dir, "*.flac", SpanMode.shallow)) {
350 if (de.isFile) {
351 if (flacfile.length) assert(0, "too many flac files");
352 //writeln("flac: <", de.name, ">");
353 flacfile = de.name;
356 if (flacfile.length == 0) assert(0, "no flac file");
359 void findCue (string dir) {
360 //writeln("dir: <", dir, ">");
361 import std.file;
362 cuefile = null;
363 foreach (DirEntry de; dirEntries(dir, "*.cue", SpanMode.shallow)) {
364 if (de.isFile) {
365 //writeln("cue: <", de.name, ">");
366 if (cuefile.length) assert(0, "too many cue files");
367 cuefile = de.name;
370 if (cuefile.length == 0) assert(0, "no cue file");
374 if (args.length < 2) assert(0, "input file?");
375 flacfile = args[1];
376 if (args.length > 2) {
377 if (args.length > 3) assert(0, "too many input files");
378 if (flacfile.extension.strEquCI(".flac")) {
379 cuefile = args[2];
380 if (cuefile.extension.strEquCI(".cue")) assert(0, "invalid input files");
381 } else if (flacfile.extension.strEquCI(".cue")) {
382 cuefile = flacfile;
383 flacfile = args[2];
384 if (flacfile.extension.strEquCI(".flac")) assert(0, "invalid input files");
385 } else {
386 assert(0, "invalid input files");
388 } else {
389 if (flacfile.extension.strEquCI(".cue")) {
390 cuefile = flacfile;
391 flacfile = cuefile.setExtension(".flac");
392 if (!flacfile.exists) findFlac(flacfile.dirName);
393 } else if (flacfile.extension.strEquCI(".flac")) {
394 cuefile = flacfile.setExtension(".cue");
395 if (!cuefile.exists) findCue(cuefile.dirName);
396 } else {
397 if (exists(flacfile~".flac")) {
398 flacfile ~= ".flac";
399 findCue(flacfile.dirName);
400 } else if (exists(flacfile~".cue")) {
401 cuefile = flacfile~".cue";
402 findFlac(cuefile.dirName);
403 } else {
404 assert(0, "wtf?!");
409 writeln("FLAC: ", flacfile);
410 writeln("CUE : ", cuefile);
412 CueFile cue;
413 cue.load(cuefile);
415 if (cue.tracks.length == 0) assert(0, "no tracks");
416 if (cue.tracks[0].startmsecs != 0) assert(0, "found first hidden track");
418 //cue.dump();
419 makeOggs(flacfile, cue, quality);