sq3: show SQLite error messages on stderr by default
[iv.d.git] / xyph / samples / zflacopus.d
blobbfccbf4613291c0aa0497255461fa954d5d9d1f4
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;
14 // ////////////////////////////////////////////////////////////////////////// //
15 __gshared ushort kbps = 192;
16 __gshared ubyte comp = 10;
17 __gshared int progressms = 500;
18 __gshared uint toffset = 0;
19 __gshared uint tmax = 0;
20 __gshared bool dbgShowArgs;
22 shared static this () {
23 conRegVar!dbgShowArgs("dbg_show_args", "debug: show opusenc args");
24 conRegVar!kbps(64, 320, "kbps", "opus encoding kbps");
25 conRegVar!comp(0, 10, "comp", "opus compression quality");
26 conRegVar!progressms("progress_time", "progress update time, in milliseconds (-1: don't show progress)");
27 conRegVar!toffset("toffset", "track offset");
28 conRegVar!tmax("tmax", "maxumum track number (0: default)");
33 // ////////////////////////////////////////////////////////////////////////// //
34 enum READ = 1024; // there is no reason to use bigger buffer
35 int[READ*2] smpbuffer; // out of the data segment, not the stack
38 // ////////////////////////////////////////////////////////////////////////// //
39 void makeOggs (string flacfile, ref CueFile cue, ushort kbps) {
40 import std.file : mkdirRecurse;
41 import std.string : toStringz;
43 short[] xopbuf;
45 import core.stdc.stdlib : malloc, free;
46 drflac* flc;
47 uint commentCount;
48 char* fcmts;
49 scope(exit) if (fcmts !is null) free(fcmts);
52 flc = drflac_open_file_with_metadata(flacfile.toStringz, (void* pUserData, drflac_metadata* pMetadata) {
53 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
54 if (fcmts !is null) free(fcmts);
55 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
56 if (csz > 0 && csz < 0x100_0000) {
57 fcmts = cast(char*)malloc(cast(uint)csz);
58 } else {
59 fcmts = null;
61 if (fcmts is null) {
62 commentCount = 0;
63 } else {
64 import core.stdc.string : memcpy;
65 commentCount = pMetadata.data.vorbis_comment.commentCount;
66 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
69 });
72 flc = drflac_open_file(flacfile);
73 if (flc is null) throw new Exception("can't open input file");
74 scope(exit) drflac_close(flc);
77 if (flc.sampleRate < 1024 || flc.sampleRate > 96000) throw new Exception("invalid flac sample rate");
78 if (flc.channels < 1 || flc.channels > 2) throw new Exception("invalid flac channel number");
79 if (flc.totalSampleCount%flc.channels != 0) throw new Exception("invalid flac sample count");
81 writeln(flc.sampleRate, "Hz, ", flc.channels, " channels; kbps=", kbps, "; comp=", comp);
83 writeln("=======================");
84 if (cue.artist.length) writeln("ARTIST: <", cue.artist.recodeToKOI8, ">");
85 if (cue.album.length) writeln("ALBUM : <", cue.album.recodeToKOI8, ">");
86 if (cue.genre.length) writeln("GENRE : <", cue.genre.recodeToKOI8, ">");
87 if (cue.year) writeln("YEAR : <", cue.year, ">");
89 void encodeSamples (const(char)[] outfname, uint tidx, ulong totalSamples) {
90 import std.internal.cstring;
91 import std.conv : to;
92 import std.process;
94 string[] args;
96 args ~= "opusenc";
97 args ~= "--quiet";
99 // metadata args
100 args ~= ["--padding", "0"];
102 void addTag (string name, string value) {
103 name = name.xstrip;
104 value = value.xstrip;
105 if (name.length == 0) return;
106 if (value.length == 0) return;
107 bool doFix = false;
108 foreach (char ch; name) {
109 if (ch >= 127 || ch == '=') assert(0, "tag name is fucked: '"~name~"'");
110 if (ch >= 'a' && ch <= 'z') { doFix = true; break; }
112 if (doFix) {
113 string s;
114 foreach (char ch; name) {
115 if (ch >= 'a' && ch <= 'z') ch -= 32;
116 s ~= ch;
118 name = s;
120 assert(value.length);
121 args ~= ["--comment", name~"="~value];
125 string val = cue.tracks[tidx].artist;
126 if (val.length == 0) val = cue.artist;
127 addTag("ARTIST", val);
129 if (cue.tracks[tidx].year) addTag("DATE", cue.tracks[tidx].year.to!string);
130 else if (cue.year) addTag("DATE", cue.year.to!string);
131 addTag("ALBUM", cue.album);
133 string val = cue.tracks[tidx].title;
134 if (val.length == 0) val = cue.album;
135 if (val.length == 0) val = "untitled";
136 addTag("TITLE", val);
139 string val = cue.tracks[tidx].genre;
140 if (val.length == 0) val = cue.genre;
141 addTag("GENRE", val);
144 string val = cue.tracks[tidx].artist;
145 if (val.length == 0) val = cue.artist;
146 addTag("PERFORMER", val);
148 addTag("TRACKNUMBER", (tidx+1+toffset).to!string);
149 addTag("TRACKTOTAL", (tmax ? tmax : cue.tracks.length+toffset).to!string);
151 // raw data format
152 args ~= "--raw";
153 args ~= ["--raw-bits", "16"];
154 args ~= ["--raw-rate", flc.sampleRate.to!string];
155 args ~= ["--raw-chan", flc.channels.to!string];
157 args ~= ["--comp", comp.to!string];
159 args ~= ["--bitrate", kbps.to!string];
161 args ~= "-"; // input: stdin
162 args ~= outfname.idup; // output
164 if (dbgShowArgs) writeln("args: ", args);
165 auto ppc = pipeProcess(args, Redirect.stdin, null, Config.retainStdout|Config.retainStderr);
166 if (!ppc.stdin.isOpen) assert(0, "fuuuuu");
168 import core.time;
169 long samplesDone = 0, prc = 0;
170 MonoTime lastPrcTime = MonoTime.currTime;
171 if (progressms >= 0) write(" 0%");
172 for (;;) {
173 uint rdsmp = cast(uint)(totalSamples-samplesDone > smpbuffer.length ? smpbuffer.length : totalSamples-samplesDone);
174 if (rdsmp == 0) break;
175 auto rdx = cast(int)drflac_read_s32(flc, rdsmp, smpbuffer.ptr); // interleaved 32-bit samples
176 if (rdx < 1) {
177 // alas -- the thing that should not be
178 writeln("FUCK!");
179 } else {
180 samplesDone += rdx;
181 if (progressms >= 0) {
182 auto nprc = 100*samplesDone/totalSamples;
183 if (nprc != prc) {
184 auto ctt = MonoTime.currTime;
185 if ((ctt-lastPrcTime).total!"msecs" >= progressms) {
186 lastPrcTime = ctt;
187 if (prc >= 0) write("\x08\x08\x08\x08");
188 prc = nprc;
189 writef("%3d%%", prc);
194 // convert samples from 32 bit to 16 bit
195 if (xopbuf.length < rdx) xopbuf.length = rdx;
196 auto s = smpbuffer.ptr;
197 auto d = xopbuf.ptr;
198 foreach (immutable i; 0..rdx) {
199 int n = *s++;
200 n >>= 16;
201 if (n < short.min) n = short.min; else if (n > short.max) n = short.max;
202 *d++ = cast(short)n;
205 ppc.stdin.rawWrite(xopbuf[0..rdx]);
208 ppc.stdin.flush();
209 ppc.stdin.close();
210 wait(ppc.pid);
212 if (progressms >= 0) {
213 if (prc >= 0) write("\x08\x08\x08\x08");
215 writeln("... DONE");
218 mkdirRecurse("opus");
219 ulong samplesProcessed = 0;
220 foreach (immutable tidx, ref trk; cue.tracks) {
221 import std.format : format;
222 string fname;
223 if (trk.title.length) fname = CueFile.koi2trlocase(trk.title.recodeToKOI8); else fname = "untitled";
224 string ofname = "opus/%02d_%s.opus".format(tidx+1+toffset, fname);
225 write("[", tidx+1, "/", cue.tracks.length, "] ", (trk.title.length ? trk.title.recodeToKOI8 : "untitled"), " -> ", ofname, " ");
226 ulong smpstart, smpend;
227 if (tidx == 0) smpstart = 0; else smpstart = cast(ulong)((trk.startmsecs/1000.0)*flc.sampleRate)*flc.channels;
228 if (smpstart != samplesProcessed) assert(0, "index fucked");
229 if (tidx == cue.tracks.length-1) smpend = flc.totalSampleCount; else smpend = cast(ulong)((cue.tracks[tidx+1].startmsecs/1000.0)*flc.sampleRate)*flc.channels;
230 if (smpend <= samplesProcessed) assert(0, "index fucked");
231 samplesProcessed = smpend;
232 try { import std.file : remove; ofname.remove; } catch (Exception) {}
233 encodeSamples(ofname, tidx, smpend-smpstart);
238 // ////////////////////////////////////////////////////////////////////////// //
239 void main (string[] args) {
240 import std.path;
241 import std.file : exists;
243 concmd("exec .encoder.rc tan");
244 conProcessArgs!true(args);
246 string flacfile, cuefile;
248 if (args.length < 2) {
249 import std.file : dirEntries, DirEntry, SpanMode;
250 foreach (DirEntry de; dirEntries(".", SpanMode.shallow)) {
251 import std.path;
252 if (de.isFile && de.extension.strEquCI(".cue")) {
253 if (args.length == 2) assert(0, "filename?");
254 args ~= de.name;
257 if (args.length < 2) assert(0, "filename?");
260 void findFlac (string dir) {
261 import std.file;
262 flacfile = null;
263 foreach (DirEntry de; dirEntries(dir, "*.flac", SpanMode.shallow)) {
264 if (de.isFile) {
265 if (flacfile.length) assert(0, "too many flac files");
266 //writeln("flac: <", de.name, ">");
267 flacfile = de.name;
270 if (flacfile.length == 0) assert(0, "no flac file");
273 void findCue (string dir) {
274 //writeln("dir: <", dir, ">");
275 import std.file;
276 cuefile = null;
277 foreach (DirEntry de; dirEntries(dir, "*.cue", SpanMode.shallow)) {
278 if (de.isFile) {
279 //writeln("cue: <", de.name, ">");
280 if (cuefile.length) assert(0, "too many cue files");
281 cuefile = de.name;
284 if (cuefile.length == 0) assert(0, "no cue file");
288 if (args.length < 2) assert(0, "input file?");
289 flacfile = args[1];
290 if (args.length > 2) {
291 if (args.length > 3) assert(0, "too many input files");
292 if (flacfile.extension.strEquCI(".flac")) {
293 cuefile = args[2];
294 if (cuefile.extension.strEquCI(".cue")) assert(0, "invalid input files");
295 } else if (flacfile.extension.strEquCI(".cue")) {
296 cuefile = flacfile;
297 flacfile = args[2];
298 if (flacfile.extension.strEquCI(".flac")) assert(0, "invalid input files");
299 } else {
300 assert(0, "invalid input files");
302 } else {
303 if (flacfile.extension.strEquCI(".cue")) {
304 cuefile = flacfile;
305 flacfile = cuefile.setExtension(".flac");
306 if (!flacfile.exists) findFlac(flacfile.dirName);
307 } else if (flacfile.extension.strEquCI(".flac")) {
308 cuefile = flacfile.setExtension(".cue");
309 if (!cuefile.exists) findCue(cuefile.dirName);
310 } else {
311 if (exists(flacfile~".flac")) {
312 flacfile ~= ".flac";
313 findCue(flacfile.dirName);
314 } else if (exists(flacfile~".cue")) {
315 cuefile = flacfile~".cue";
316 findFlac(cuefile.dirName);
317 } else {
318 assert(0, "wtf?!");
323 writeln("FLAC: ", flacfile);
324 writeln("CUE : ", cuefile);
326 CueFile cue;
327 cue.load(cuefile);
329 if (cue.tracks.length == 0) assert(0, "no tracks");
330 if (cue.tracks[0].startmsecs != 0) assert(0, "found first hidden track");
332 //cue.dump();
333 makeOggs(flacfile, cue, kbps);