egeditor: more VaVoom C highlighting
[iv.d.git] / cuefile.d
blob428dc4991cfc86d425f6ffed490d2906c58ddc2e
1 /* Invisible Vector Library
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module iv.cuefile /*is aliced*/;
20 import iv.alice;
21 import iv.encoding;
22 import iv.strex;
23 import iv.vfs;
24 import iv.vfs.io;
27 // ////////////////////////////////////////////////////////////////////////// //
28 struct CueFile {
29 public:
30 static string koi2trlocase (const(char)[] s) {
31 string res;
32 foreach (char ch; s) {
33 if (ch == '\xe1' || ch == '\xc1') res ~= "a";
34 else if (ch == '\xe2' || ch == '\xc2') res ~= "b";
35 else if (ch == '\xf7' || ch == '\xd7') res ~= "v";
36 else if (ch == '\xe7' || ch == '\xc7') res ~= "g";
37 else if (ch == '\xe4' || ch == '\xc4') res ~= "d";
38 else if (ch == '\xe5' || ch == '\xc5') res ~= "e";
39 else if (ch == '\xb3' || ch == '\xa3') res ~= "yo";
40 else if (ch == '\xf6' || ch == '\xd6') res ~= "zh";
41 else if (ch == '\xfa' || ch == '\xda') res ~= "z";
42 else if (ch == '\xe9' || ch == '\xc9') res ~= "i";
43 else if (ch == '\xea' || ch == '\xca') res ~= "j";
44 else if (ch == '\xeb' || ch == '\xcb') res ~= "k";
45 else if (ch == '\xec' || ch == '\xcc') res ~= "l";
46 else if (ch == '\xed' || ch == '\xcd') res ~= "m";
47 else if (ch == '\xee' || ch == '\xce') res ~= "n";
48 else if (ch == '\xef' || ch == '\xcf') res ~= "o";
49 else if (ch == '\xf0' || ch == '\xd0') res ~= "p";
50 else if (ch == '\xf2' || ch == '\xd2') res ~= "r";
51 else if (ch == '\xf3' || ch == '\xd3') res ~= "s";
52 else if (ch == '\xf4' || ch == '\xd4') res ~= "t";
53 else if (ch == '\xf5' || ch == '\xd5') res ~= "u";
54 else if (ch == '\xe6' || ch == '\xc6') res ~= "f";
55 else if (ch == '\xe8' || ch == '\xc8') res ~= "h";
56 else if (ch == '\xe3' || ch == '\xc3') res ~= "c";
57 else if (ch == '\xfe' || ch == '\xde') res ~= "ch";
58 else if (ch == '\xfb' || ch == '\xdb') res ~= "sh";
59 else if (ch == '\xfd' || ch == '\xdd') res ~= "sch";
60 else if (ch == '\xff' || ch == '\xdf') {} //res ~= "x"; // tvyordyj znak
61 else if (ch == '\xf9' || ch == '\xd9') res ~= "y";
62 else if (ch == '\xf8' || ch == '\xd8') {} //res ~= "w"; // myagkij znak
63 else if (ch == '\xfc' || ch == '\xdc') res ~= "e";
64 else if (ch == '\xe0' || ch == '\xc0') res ~= "ju";
65 else if (ch == '\xf1' || ch == '\xd1') res ~= "ja";
66 else if (ch >= 'A' && ch <= 'Z') res ~= cast(char)(ch+32);
67 else if (ch >= 'a' && ch <= 'z') res ~= ch;
68 else if (ch >= '0' && ch <= '9') res ~= ch;
69 else {
70 if (res.length > 0 && res[$-1] != '_') res ~= '_';
73 while (res.length && res[$-1] == '_') res = res[0..$-1];
74 if (res.length == 0) res = "_";
75 return res;
78 public:
79 static struct Track {
80 string artist; // performer
81 string title;
82 string genre;
83 uint year; // 0: unknown
84 string filename;
85 ulong pregapmsecs; // index 00, or startmsecs
86 ulong startmsecs; // index 01
88 @property ulong start () const pure nothrow @safe @nogc { pragma(inline, true); return (pregapmsecs != pregapmsecs.max && pregapmsecs < startmsecs ? pregapmsecs : startmsecs); }
91 private:
92 ulong parseIndex (const(char)[] s) {
93 import std.algorithm : splitter;
94 import std.conv : to;
95 import std.range : enumerate;
96 uint[3] msf;
97 bool lastHit = false;
98 foreach (immutable idx, /*auto*/ sv; s.splitter(':').enumerate) {
99 if (idx >= msf.length) throw new Exception("invalid index");
100 lastHit = (idx == msf.length-1);
101 msf[idx] = sv.to!uint;
103 if (!lastHit) throw new Exception("invalid index");
104 if (msf[1] > 59) throw new Exception("invalid index");
105 if (msf[2] > 74) throw new Exception("invalid index");
106 //return cast(uint)((((msf[1]+msf[0]*60)*75)/75.0)*1000.0);
107 return cast(uint)((((msf[1]+msf[0]*60)*75+msf[2])/75.0)*1000.0);
110 public:
111 string artist;
112 string album;
113 string genre;
114 uint year; // 0: unknown
115 string filename;
116 Track[] tracks;
118 public:
119 void clear () { this = this.init; }
121 void load (const(char)[] fname) { load(VFile(fname)); }
123 void load (VFile fl) {
124 clear();
125 scope(failure) clear();
126 char[4096] linebuf;
127 char lastSavedChar = 0;
128 char[] line;
129 bool firstLine = true;
131 bool readLine () {
132 scope(success) {
133 if (firstLine) {
134 firstLine = false;
135 if (line.length >= 3 && line[0..3] == "\xEF\xBB\xBF") line = line[3..$]; // fuck BOM
138 uint pos = 0;
139 if (lastSavedChar) { linebuf[pos++] = lastSavedChar; lastSavedChar = 0; }
140 while (pos < linebuf.length) {
141 auto rd = fl.rawRead(linebuf[pos..pos+1]);
142 if (rd.length == 0) {
143 if (pos == 0) { line = null; return false; }
144 line = linebuf[0..pos];
145 return true;
147 char ch = linebuf[pos];
148 if (ch == '\n') {
149 line = linebuf[0..pos];
150 return true;
152 if (ch == '\r') {
153 rd = fl.rawRead((&lastSavedChar)[0..1]);
154 if (rd.length == 1 && lastSavedChar == '\n') lastSavedChar = 0;
155 line = linebuf[0..pos];
156 return true;
158 ++pos;
160 throw new Exception("line too long!");
163 // null: EOL
164 const(char)[] nextWord(bool doupper) () {
165 while (line.length && line[0] <= ' ') line = line[1..$];
166 if (line.length == 0) return null;
167 char[] res;
168 uint epos = 1;
169 if (line[0] == '"') {
170 // quoted
171 while (epos < line.length && line[epos] != '"') {
172 // just in case
173 if (line[epos] == '\\' && line.length-epos > 1) epos += 2; else ++epos;
175 res = line[1..epos];
176 if (epos < line.length) {
177 assert(line[epos] == '"');
178 ++epos;
180 line = line[epos..$];
181 // remove spaces (i don't need 'em anyway; and i don't care about idiotic filenames)
182 while (res.length && res[0] <= ' ') res = res[1..$];
183 while (res.length && res[$-1] <= ' ') res = res[0..$-1];
184 } else {
185 // normal
186 while (epos < line.length && line[epos] > ' ') ++epos;
187 res = line[0..epos];
188 line = line[epos..$];
190 // recode
191 if (res !is null && !res.utf8Valid) return res.recode("utf-8", "cp1251");
192 static if (doupper) {
193 if (res !is null) {
194 // upcase
195 bool doconv = false;
196 foreach (char ch; res) {
197 if (ch >= 128) { doconv = false; break; }
198 if (ch >= 'a' && ch <= 'z') doconv = true;
200 if (doconv) foreach (ref char ch; res) if (ch >= 'a' && ch <= 'z') ch -= 32;
203 return res;
206 while (readLine) {
207 //writeln("[", line, "]");
208 auto w = nextWord!true();
209 if (w is null) continue;
210 switch (w) {
211 case "REM": // special
212 w = nextWord!true();
213 switch (w) {
214 case "DATE": case "YEAR":
215 w = nextWord!false();
216 int yr = 0;
217 try { import std.conv : to; yr = w.to!ushort(10); } catch (Exception) {}
218 if (yr >= 1900 && yr <= 3000) {
219 if (tracks.length) tracks[$-1].year = yr; else year = yr;
221 break;
222 case "GENRE":
223 w = nextWord!false();
224 if (w.length) {
225 if (tracks.length) tracks[$-1].genre = w.idup; else genre = w.idup;
227 break;
228 default: break;
230 break;
231 case "TRACK": // new track
232 tracks.length += 1;
233 tracks[$-1].pregapmsecs = tracks[$-1].pregapmsecs.max;
234 w = nextWord!true();
235 try {
236 import std.conv : to;
237 auto tn = w.to!ubyte(10);
238 if (tn != tracks.length) throw new Exception("invalid track number");
239 } catch (Exception) {
240 throw new Exception("fucked track number");
242 w = nextWord!true();
243 if (w != "AUDIO") throw new Exception("non-audio track");
244 break;
245 case "PERFORMER":
246 w = nextWord!false();
247 if (w.length) {
248 if (tracks.length) tracks[$-1].artist = w.idup; else artist = w.idup;
250 break;
251 case "TITLE":
252 w = nextWord!false();
253 if (w.length) {
254 if (tracks.length) tracks[$-1].title = w.idup; else album = w.idup;
256 break;
257 case "FILE":
258 w = nextWord!false();
259 if (w.length) {
260 if (tracks.length) tracks[$-1].filename = w.idup; else filename = w.idup;
262 break;
263 case "INDEX":
264 // mm:ss:ff (minute-second-frame) format. There are 75 such frames per second of audio
265 // 00: pregap, optional
266 // 01: song start
267 if (tracks.length == 0) throw new Exception("index without track");
268 w = nextWord!false();
269 try {
270 import std.conv : to;
271 auto n = w.to!ubyte(10);
272 if (n == 1) {
273 w = nextWord!true();
274 tracks[$-1].startmsecs = parseIndex(w);
275 } else if (n == 0) {
276 w = nextWord!true();
277 tracks[$-1].pregapmsecs = parseIndex(w);
279 } catch (Exception e) {
280 writeln("ERROR: ", e.msg, " (", w, ")");
281 throw new Exception("fucked index");
283 break;
284 case "PREGAP": case "POSTGAP": break; // ignore
285 case "ISRC": case "CATALOG": case "FLAGS": case "CDTEXTFILE": break;
286 // SONGWRITER
287 default:
288 writeln("unknown CUE keyword: '", w, "'");
289 throw new Exception("invalid keyword");
293 // normalize tracks
294 foreach (immutable tidx, ref trk; tracks) {
295 if (trk.pregapmsecs == trk.pregapmsecs.max) trk.pregapmsecs = trk.startmsecs;
296 if (trk.artist == artist) trk.artist = null;
297 if (trk.year == year) trk.year = 0;
298 if (trk.genre == genre) trk.genre = null;
299 if (trk.filename == filename) trk.filename = null;
300 int pidx;
301 string t = simpleParseInt(trk.title, pidx);
302 if (pidx == tidx+1 && t.length && t.ptr[0] == '.') t = t[1..$].xstrip;
303 if (pidx == tidx+1 && t.length) trk.title = t;
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 : <", koi2trlocase(trk.title.recodeToKOI8), ">");
328 void dump () { dump(stdout); }
330 private:
331 // num<0: no number
332 // return string w/o parsed number
333 static inout(char)[] simpleParseInt (inout(char)[] src, out int num) nothrow @trusted @nogc {
334 usize pos = 0;
335 while (pos < src.length && src.ptr[pos] <= ' ') ++pos;
336 if (pos >= src.length || src.ptr[pos] < '0' || src.ptr[pos] > '9') {
337 num = -1;
338 return src;
340 num = 0;
341 while (pos < src.length) {
342 char ch = src.ptr[pos];
343 if (ch < '0' || ch > '9') break;
344 auto onum = num;
345 num = num*10+ch-'0';
346 if (num < onum) { num = -1; return src; }
347 ++pos;
349 while (pos < src.length && src.ptr[pos] <= ' ') ++pos;
350 return src[pos..$];