2 module zflacvt
/*is aliced*/;
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
;
45 import core
.stdc
.stdlib
: malloc
, free
;
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);
64 import core.stdc.string : memcpy;
65 commentCount = pMetadata.data.vorbis_comment.commentCount;
66 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
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
;
100 args
~= ["--padding", "0"];
102 void addTag (string name
, string value
) {
104 value
= value
.xstrip
;
105 if (name
.length
== 0) return;
106 if (value
.length
== 0) return;
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; }
114 foreach (char ch
; name
) {
115 if (ch
>= 'a' && ch
<= 'z') ch
-= 32;
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
);
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");
169 long samplesDone
= 0, prc
= 0;
170 MonoTime lastPrcTime
= MonoTime
.currTime
;
171 if (progressms
>= 0) write(" 0%");
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
177 // alas -- the thing that should not be
181 if (progressms
>= 0) {
182 auto nprc
= 100*samplesDone
/totalSamples
;
184 auto ctt
= MonoTime
.currTime
;
185 if ((ctt
-lastPrcTime
).total
!"msecs" >= progressms
) {
187 if (prc
>= 0) write("\x08\x08\x08\x08");
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
;
198 foreach (immutable i
; 0..rdx
) {
201 if (n
< short.min
) n
= short.min
; else if (n
> short.max
) n
= short.max
;
205 ppc
.stdin
.rawWrite(xopbuf
[0..rdx
]);
212 if (progressms
>= 0) {
213 if (prc
>= 0) write("\x08\x08\x08\x08");
218 mkdirRecurse("opus");
219 ulong samplesProcessed
= 0;
220 foreach (immutable tidx
, ref trk
; cue
.tracks
) {
221 import std
.format
: format
;
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
) {
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
)) {
252 if (de.isFile
&& de.extension
.strEquCI(".cue")) {
253 if (args
.length
== 2) assert(0, "filename?");
257 if (args
.length
< 2) assert(0, "filename?");
260 void findFlac (string dir
) {
263 foreach (DirEntry
de; dirEntries(dir
, "*.flac", SpanMode
.shallow
)) {
265 if (flacfile
.length
) assert(0, "too many flac files");
266 //writeln("flac: <", de.name, ">");
270 if (flacfile
.length
== 0) assert(0, "no flac file");
273 void findCue (string dir
) {
274 //writeln("dir: <", dir, ">");
277 foreach (DirEntry
de; dirEntries(dir
, "*.cue", SpanMode
.shallow
)) {
279 //writeln("cue: <", de.name, ">");
280 if (cuefile
.length
) assert(0, "too many cue files");
284 if (cuefile
.length
== 0) assert(0, "no cue file");
288 if (args
.length
< 2) assert(0, "input file?");
290 if (args
.length
> 2) {
291 if (args
.length
> 3) assert(0, "too many input files");
292 if (flacfile
.extension
.strEquCI(".flac")) {
294 if (cuefile
.extension
.strEquCI(".cue")) assert(0, "invalid input files");
295 } else if (flacfile
.extension
.strEquCI(".cue")) {
298 if (flacfile
.extension
.strEquCI(".flac")) assert(0, "invalid input files");
300 assert(0, "invalid input files");
303 if (flacfile
.extension
.strEquCI(".cue")) {
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
);
311 if (exists(flacfile
~".flac")) {
313 findCue(flacfile
.dirName
);
314 } else if (exists(flacfile
~".cue")) {
315 cuefile
= flacfile
~".cue";
316 findFlac(cuefile
.dirName
);
323 writeln("FLAC: ", flacfile
);
324 writeln("CUE : ", cuefile
);
329 if (cue
.tracks
.length
== 0) assert(0, "no tracks");
330 if (cue
.tracks
[0].startmsecs
!= 0) assert(0, "found first hidden track");
333 makeOggs(flacfile
, cue
, kbps
);