editor: another undo fix
[iv.d.git] / trestex / trestex.d
blobd1463e009e4bd0364af8c5cd8378ca18a1b91314
1 /*
2 Copyright (c) 2016, Ketmar // Invisible Vector
4 Redistribution and use in source and binary forms, with or without
5 modification, are permitted provided that the following conditions
6 are met:
8 - Redistributions of source code must retain the above copyright
9 notice, this list of conditions and the following disclaimer.
11 - Redistributions in binary form must reproduce the above copyright
12 notice, this list of conditions and the following disclaimer in the
13 documentation and/or other materials provided with the distribution.
15 - Neither the name of the Xiph.org Foundation nor the names of its
16 contributors may be used to endorse or promote products derived from
17 this software without specific prior written permission.
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
23 OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 module trestex /*is aliced*/; // due to Phobos bug
33 import iv.alice;
34 import iv.cmdcon;
35 import iv.cmdcontty;
36 //import iv.mbandeq;
37 import iv.rawtty;
38 import iv.simplealsa;
39 import iv.vfs;
41 import iv.drflac;
42 import iv.minimp3;
43 import iv.mp3scan;
44 import iv.tremor;
45 import iv.dopus;
48 // ////////////////////////////////////////////////////////////////////////// //
49 string recodeToKOI8 (const(char)[] s) {
50 immutable wchar[128] charMapKOI8 = [
51 '\u2500','\u2502','\u250C','\u2510','\u2514','\u2518','\u251C','\u2524','\u252C','\u2534','\u253C','\u2580','\u2584','\u2588','\u258C','\u2590',
52 '\u2591','\u2592','\u2593','\u2320','\u25A0','\u2219','\u221A','\u2248','\u2264','\u2265','\u00A0','\u2321','\u00B0','\u00B2','\u00B7','\u00F7',
53 '\u2550','\u2551','\u2552','\u0451','\u0454','\u2554','\u0456','\u0457','\u2557','\u2558','\u2559','\u255A','\u255B','\u0491','\u255D','\u255E',
54 '\u255F','\u2560','\u2561','\u0401','\u0404','\u2563','\u0406','\u0407','\u2566','\u2567','\u2568','\u2569','\u256A','\u0490','\u256C','\u00A9',
55 '\u044E','\u0430','\u0431','\u0446','\u0434','\u0435','\u0444','\u0433','\u0445','\u0438','\u0439','\u043A','\u043B','\u043C','\u043D','\u043E',
56 '\u043F','\u044F','\u0440','\u0441','\u0442','\u0443','\u0436','\u0432','\u044C','\u044B','\u0437','\u0448','\u044D','\u0449','\u0447','\u044A',
57 '\u042E','\u0410','\u0411','\u0426','\u0414','\u0415','\u0424','\u0413','\u0425','\u0418','\u0419','\u041A','\u041B','\u041C','\u041D','\u041E',
58 '\u041F','\u042F','\u0420','\u0421','\u0422','\u0423','\u0416','\u0412','\u042C','\u042B','\u0417','\u0428','\u042D','\u0429','\u0427','\u042A',
60 string res;
61 foreach (dchar ch; s) {
62 if (ch < 128) {
63 if (ch < ' ') ch = ' ';
64 if (ch == 127) ch = '?';
65 res ~= cast(char)ch;
66 } else {
67 bool found = false;
68 foreach (immutable idx, wchar wch; charMapKOI8[]) {
69 if (wch == ch) { res ~= cast(char)(idx+128); found = true; break; }
71 if (!found) res ~= '?';
74 return res;
78 // ////////////////////////////////////////////////////////////////////////// //
79 struct StreamIO {
80 private:
81 VFile fl;
82 string type;
84 public:
85 //long timetotal; // in milliseconds
86 uint rate = 1; // just in case
87 ubyte channels = 1; // just in case
88 ulong samplestotal; // multiplied by channels
89 ulong samplesread; // samples read so far, multiplied by channels
91 @property ulong framesread () const pure nothrow @safe @nogc { pragma(inline, true); return samplesread/channels; }
92 @property ulong framestotal () const pure nothrow @safe @nogc { pragma(inline, true); return samplestotal/channels; }
94 @property uint timeread () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplesread*1000/rate/channels); }
95 @property uint timetotal () const pure nothrow @safe @nogc { pragma(inline, true); return cast(uint)(samplestotal*1000/rate/channels); }
97 public:
98 bool valid () {
99 if (type.length == 0) return false;
100 switch (type[0]) {
101 case 'f': return (ff !is null);
102 case 'v': return (vi !is null);
103 case 'm': return mp3.valid;
104 case 'o': return (of !is null);
105 default:
107 return false;
110 @property string typestr () const pure nothrow @safe @nogc { return type; }
112 void close () {
113 if (type.length == 0) return;
114 switch (type[0]) {
115 case 'f': if (ff !is null) { drflac_close(ff); ff = null; } break;
116 case 'v': if (vi !is null) { vi = null; ov_clear(&vf); } break;
117 case 'm': if (mp3.valid) mp3.close(); break;
118 default:
122 int readFrames (void* buf, int count) {
123 if (count < 1) return 0;
124 if (count > int.max/4) count = int.max/4;
125 if (!valid) return 0;
126 switch (type[0]) {
127 case 'f':
128 if (ff is null) return 0;
129 int[512] flcbuf;
130 int res = 0;
131 count *= channels;
132 short* bp = cast(short*)buf;
133 while (count > 0) {
134 int xrd = (count <= flcbuf.length ? count : cast(int)flcbuf.length);
135 auto rd = drflac_read_s32(ff, xrd, flcbuf.ptr); // samples
136 if (rd <= 0) break;
137 samplesread += rd; // number of samples read
138 foreach (int v; flcbuf[0..cast(int)rd]) *bp++ = cast(short)(v>>16);
139 res += rd;
140 count -= rd;
142 return cast(int)(res/channels); // number of frames read
143 case 'v':
144 if (vi is null) return 0;
145 int currstream = 0;
146 auto ret = ov_read(&vf, cast(ubyte*)buf, count*2*channels, &currstream);
147 if (ret <= 0) return 0; // error or eof
148 samplesread += ret/2; // number of samples read
149 return ret/2/channels; // number of frames read
150 case 'o':
151 auto dptr = cast(short*)buf;
152 if (of is null) return 0;
153 int total = 0;
154 while (count > 0) {
155 while (count > 0 && smpbufpos < smpbufused) {
156 *dptr++ = smpbuf.ptr[smpbufpos++];
157 if (channels == 2) *dptr++ = smpbuf.ptr[smpbufpos++];
158 --count;
159 ++total;
160 samplesread += channels;
162 if (count == 0) break;
163 auto rd = of.readFrame();
164 if (rd.length == 0) break;
165 if (rd.length > smpbuf.length) smpbuf.length = rd.length;
166 smpbuf[0..rd.length] = rd[];
167 smpbufpos = 0;
168 smpbufused = cast(uint)rd.length;
170 return total;
171 case 'm':
172 // yes, i know that frames are not independend, and i should actually
173 // seek to a frame with a correct sync word. meh.
174 if (!mp3.valid) return 0;
175 auto mfm = mp3.frameSamples;
176 if (mp3smpused+channels > mfm.length) {
177 mp3smpused = 0;
178 if (!mp3.decodeNextFrame(&reader)) return 0;
179 mfm = mp3.frameSamples;
180 if (mp3.sampleRate != rate || mp3.channels != channels) return 0;
182 int res = 0;
183 ushort* b = cast(ushort*)buf;
184 auto oldmpu = mp3smpused;
185 while (count > 0 && mp3smpused+channels <= mfm.length) {
186 *b++ = mfm[mp3smpused++];
187 if (channels == 2) *b++ = mfm[mp3smpused++];
188 --count;
189 ++res;
191 samplesread += mp3smpused-oldmpu; // number of samples read
192 return res;
193 default: break;
195 return 0;
198 // return new frame index
199 ulong seekToTime (uint msecs) {
200 if (!valid) return 0;
201 ulong snum = cast(ulong)msecs*rate/1000*channels; // sample number
202 switch (type[0]) {
203 case 'f':
204 if (ff is null) return 0;
205 if (ff.totalSampleCount < 1) return 0;
206 if (snum >= ff.totalSampleCount) {
207 drflac_seek_to_sample(ff, 0);
208 return 0;
210 if (!drflac_seek_to_sample(ff, snum)) {
211 drflac_seek_to_sample(ff, 0);
212 return 0;
214 samplesread = snum;
215 return snum/channels;
216 case 'v':
217 if (vi is null) return 0;
218 if (ov_pcm_seek(&vf, snum/channels) == 0) {
219 samplesread = ov_pcm_tell(&vf)*channels;
220 return samplesread/channels;
222 ov_pcm_seek(&vf, 0);
223 return 0;
224 case 'o':
225 if (of is null) return 0;
226 of.seek(msecs);
227 samplesread = of.smpcurtime*channels;
228 return samplesread/channels;
229 case 'm':
230 if (!mp3.valid) return 0;
231 mp3smpused = 0;
232 if (mp3info.index.length == 0 || snum == 0) {
233 // alas, we cannot seek here
234 samplesread = 0;
235 fl.seek(0);
236 mp3.restart(&reader);
237 return 0;
239 // find frame containing our sample
240 // stupid binary search; ignore overflow bug
241 ulong start = 0;
242 ulong end = mp3info.index.length-1;
243 while (start <= end) {
244 ulong mid = (start+end)/2;
245 auto smps = mp3info.index[cast(usize)mid].samples;
246 auto smpe = (mp3info.index.length-mid > 0 ? mp3info.index[cast(usize)(mid+1)].samples : samplestotal);
247 if (snum >= smps && snum < smpe) {
248 // i found her!
249 samplesread = snum;
250 fl.seek(mp3info.index[cast(usize)mid].fpos);
251 mp3smpused = cast(uint)(snum-smps);
252 mp3.sync(&reader);
253 return snum;
255 if (snum < smps) end = mid-1; else start = mid+1;
257 // alas, we cannot seek
258 samplesread = 0;
259 fl.seek(0);
260 mp3.restart(&reader);
261 return 0;
262 default: break;
264 return 0;
267 private:
268 drflac* ff;
269 MP3Decoder mp3;
270 Mp3Info mp3info; // scanned info, frame index
271 uint mp3smpused;
272 OggVorbis_File vf;
273 vorbis_info* vi;
274 OpusFile of;
275 short[] smpbuf;
276 uint smpbufpos, smpbufused;
278 int reader (void[] buf) {
279 try {
280 auto rd = fl.rawRead(buf);
281 return cast(int)rd.length;
282 } catch (Exception e) {}
283 return -1;
286 public:
287 static StreamIO open (VFile fl) {
288 import std.string : fromStringz;
289 StreamIO sio;
290 fl.seek(0);
291 // determine format
292 try {
293 auto fpos = fl.tell;
294 // flac
295 try {
296 import core.stdc.stdio;
297 import core.stdc.stdlib : malloc, free;
298 uint commentCount;
299 char* fcmts;
300 scope(exit) if (fcmts !is null) free(fcmts);
301 sio.ff = drflac_open_file(fl, (void* pUserData, drflac_metadata* pMetadata) {
302 if (pMetadata.type == DRFLAC_METADATA_BLOCK_TYPE_VORBIS_COMMENT) {
303 if (fcmts !is null) free(fcmts);
304 auto csz = drflac_vorbis_comment_size(pMetadata.data.vorbis_comment.commentCount, pMetadata.data.vorbis_comment.comments);
305 if (csz > 0 && csz < 0x100_0000) {
306 fcmts = cast(char*)malloc(cast(uint)csz);
307 } else {
308 fcmts = null;
310 if (fcmts is null) {
311 commentCount = 0;
312 } else {
313 import core.stdc.string : memcpy;
314 commentCount = pMetadata.data.vorbis_comment.commentCount;
315 memcpy(fcmts, pMetadata.data.vorbis_comment.comments, cast(uint)csz);
319 if (sio.ff !is null) {
320 scope(failure) drflac_close(sio.ff);
321 if (sio.ff.sampleRate < 1024 || sio.ff.sampleRate > 96000) throw new Exception("fucked flac");
322 if (sio.ff.channels < 1 || sio.ff.channels > 2) throw new Exception("fucked flac");
323 sio.rate = cast(uint)sio.ff.sampleRate;
324 sio.channels = cast(ubyte)sio.ff.channels;
325 sio.type = "flac";
326 sio.fl = fl;
327 sio.samplestotal = sio.ff.totalSampleCount;
328 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (flac)");
330 drflac_vorbis_comment_iterator i;
331 drflac_init_vorbis_comment_iterator(&i, commentCount, fcmts);
332 uint commentLength;
333 const(char)* pComment;
334 while ((pComment = drflac_next_vorbis_comment(&i, &commentLength)) !is null) {
335 if (commentLength > 1024*1024*2) break; // just in case
336 conwriteln(" ", pComment[0..commentLength].recodeToKOI8);
339 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
340 return sio;
342 } catch (Exception) {}
343 fl.seek(fpos);
344 // vorbis
345 try {
346 if (ov_fopen(fl, &sio.vf) == 0) {
347 scope(failure) ov_clear(&sio.vf);
348 sio.type = "vorbis";
349 sio.fl = fl;
350 sio.vi = ov_info(&sio.vf, -1);
351 if (sio.vi.rate < 1024 || sio.vi.rate > 96000) throw new Exception("fucked vorbis");
352 if (sio.vi.channels < 1 || sio.vi.channels > 2) throw new Exception("fucked vorbis");
353 sio.rate = sio.vi.rate;
354 sio.channels = cast(ubyte)sio.vi.channels;
355 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (vorbis)");
356 conwriteln("streams: ", ov_streams(&sio.vf));
357 conwriteln("bitrate: ", ov_bitrate(&sio.vf));
358 sio.samplestotal = ov_pcm_total(&sio.vf)*sio.channels;
359 if (auto vc = ov_comment(&sio.vf, -1)) {
360 conwriteln("Encoded by: ", vc.vendor.fromStringz.recodeToKOI8);
361 foreach (immutable idx; 0..vc.comments) {
362 conwriteln(" ", vc.user_comments[idx][0..vc.comment_lengths[idx]].recodeToKOI8);
365 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
366 return sio;
368 } catch (Exception) {}
369 fl.seek(fpos);
370 // opus
371 try {
372 OpusFile of = opusOpen(fl);
373 scope(failure) opusClose(of);
374 if (of.rate < 1024 || of.rate > 96000) throw new Exception("fucked opus");
375 if (of.channels < 1 || of.channels > 2) throw new Exception("fucked opus");
376 sio.of = of;
377 sio.type = "opus";
378 sio.fl = fl;
379 sio.rate = of.rate;
380 sio.channels = of.channels;
381 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (opus)");
382 sio.samplestotal = of.smpduration*sio.channels;
383 if (of.vendor.length) conwriteln("Encoded by: ", of.vendor.recodeToKOI8);
384 foreach (immutable cidx; 0..of.commentCount) conwriteln(" ", of.comment(cidx).recodeToKOI8);
385 //TODO: comments
386 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
387 return sio;
388 } catch (Exception) {}
389 fl.seek(fpos);
390 // mp3
391 try {
392 sio.fl = fl;
393 sio.mp3 = new MP3Decoder(&sio.reader);
394 sio.type = "mp3";
395 if (sio.mp3.valid) {
396 // scan file to determine number of frames
397 auto xfp = fl.tell;
398 fl.seek(fpos);
399 sio.mp3info = mp3Scan!true((void[] buf) => cast(int)fl.rawRead(buf).length); // build index too
400 fl.seek(xfp);
401 if (sio.mp3info.valid) {
402 if (sio.mp3.sampleRate < 1024 || sio.mp3.sampleRate > 96000) throw new Exception("fucked mp3");
403 if (sio.mp3.channels < 1 || sio.mp3.channels > 2) throw new Exception("fucked mp3");
404 sio.rate = sio.mp3.sampleRate;
405 sio.channels = sio.mp3.channels;
406 sio.samplestotal = sio.mp3info.samples;
407 conwriteln("Bitstream is ", sio.channels, " channel, ", sio.rate, "Hz (mp3)");
408 conwritefln!"time: %d:%02d"(sio.timetotal/1000/60, sio.timetotal/1000%60);
409 return sio;
412 sio.mp3 = null;
413 } catch (Exception) {}
414 sio.mp3 = null;
415 fl.seek(fpos);
416 } catch (Exception) {}
417 return StreamIO.init;
422 // ////////////////////////////////////////////////////////////////////////// //
423 enum BUF_SIZE = 4096;
424 short[BUF_SIZE] buffer;
426 string[] playlist;
427 int plidx = 0;
429 __gshared bool paused = false;
432 // ////////////////////////////////////////////////////////////////////////// //
433 enum BandHeight = 20;
434 __gshared int eqCurBand = 0, eqCurBandOld = 0;
435 __gshared bool eqBandEditor = false;
436 __gshared bool eqBandEditorOld = false;
437 __gshared int[/*MBandEq.Bands*/EQ_MAX_BANDS] eqOldBands = int.min;
440 void drawEqBands () {
441 static int eqclamp (int v) { pragma(inline, true); return (v < -70 ? -70 : v > 30 ? 30 : v); }
443 __gshared char[65536] cobuf = void;
444 uint cobufpos = 0;
446 void conOReset () nothrow @trusted @nogc { cobufpos = 0; }
448 void conOFlush () nothrow @trusted @nogc { if (cobufpos) ttyRawWrite(cobuf[0..cobufpos]); cobufpos = 0; }
450 void conOPut (const(char)[] s...) nothrow @trusted @nogc {
451 while (s.length) {
452 auto left = cast(uint)cobuf.length-cobufpos;
453 if (left == 0) {
454 conOFlush();
455 } else {
456 if (s.length < left) left = cast(uint)s.length;
457 cobuf[cobufpos..cobufpos+left] = s[0..left];
458 s = s[left..$];
459 cobufpos += left;
464 void conOInt(T) (T n) nothrow @trusted @nogc if (__traits(isIntegral, T) && !is(T == char) && !is(T == wchar) && !is(T == dchar) && !is(T == bool) && !is(T == enum)) {
465 import core.stdc.stdio : snprintf;
466 char[64] buf = void;
467 static if (__traits(isUnsigned, T)) {
468 static if (T.sizeof > 4) {
469 auto len = snprintf(buf.ptr, buf.length, "%llu", n);
470 } else {
471 auto len = snprintf(buf.ptr, buf.length, "%u", cast(uint)n);
473 } else {
474 static if (T.sizeof > 4) {
475 auto len = snprintf(buf.ptr, buf.length, "%lld", n);
476 } else {
477 auto len = snprintf(buf.ptr, buf.length, "%d", cast(int)n);
480 if (len > 0) conOPut(buf[0..len]);
483 void conOColorFG (uint c) nothrow @trusted @nogc {
484 conOPut("\x1b[38;5;");
485 conOInt(ttyRgb2Color((c>>16)&0xff, (c>>8)&0xff, c&0xff));
486 conOPut("m");
489 void conOColorBG (uint c) nothrow @trusted @nogc {
490 conOPut("\x1b[48;5;");
491 conOInt(ttyRgb2Color((c>>16)&0xff, (c>>8)&0xff, c&0xff));
492 conOPut("m");
495 void conOAt (int x, int y) nothrow @trusted @nogc {
496 conOPut("\x1b[");
497 conOInt(y);
498 conOPut(";");
499 conOInt(x);
500 conOPut("H");
503 if (eqBandEditor != eqBandEditorOld && !eqBandEditor) {
504 // erase editor and exit
505 eqBandEditorOld = eqBandEditor;
506 eqCurBandOld = eqCurBand;
507 eqOldBands[] = int.min;
508 conOReset();
509 scope(exit) { conOPut("\x1b8"); conOFlush(); } // restore
510 conOPut("\x1b7\x1b[0m\x1b[H"); // save, reset color, goto top
511 foreach (immutable y; 0..BandHeight+2) conOPut("\x1b[K\r\n");
512 return;
515 // did something changed?
516 if (eqBandEditor == eqBandEditorOld && eqCurBand == eqCurBandOld) {
517 bool changed = false;
518 foreach (immutable idx, int v; alsaEqBands[]) if (v != eqOldBands[idx]) { changed = true; break; }
519 if (!changed) return;
520 if (!eqBandEditor) return;
523 bool drawFull = false;
525 if (eqBandEditor != eqBandEditorOld) drawFull = true;
527 eqBandEditorOld = eqBandEditor;
529 conOReset();
530 scope(exit) { conOPut("\x1b8"); conOFlush(); } // restore
532 conOPut("\x1b7\x1b[0m\x1b[H"); // save, reset color, goto top
534 conOColorBG(0x00_00_c0);
535 conOColorFG(0x80_80_80);
537 if (drawFull) {
538 foreach (immutable y; 0..BandHeight) {
539 conOPut(" ");
540 foreach (immutable x; 0../*MBandEq.Bands*/EQ_MAX_BANDS) {
541 if (x == eqCurBand) conOColorFG(0xff_ff_00);
542 conOPut("| ");
543 if (x == eqCurBand) conOColorFG(0x80_80_80);
545 conOPut("\x1b[K\r\n");
547 conOPut("\x1b[K\r\n");
548 conOPut("\x1b[K\r\n");
550 foreach (immutable x; 0../*MBandEq.Bands*/EQ_MAX_BANDS) {
551 if (x%2 != 0) continue;
552 conOPut("\x1b[");
553 conOInt(BandHeight+1);
554 conOPut(";");
555 conOInt(3+x*4);
556 conOPut("H");
557 //!!!conOInt(cast(int)MBandEq.bandfrqs[x]);
560 foreach (immutable x; 0../*MBandEq.Bands*/EQ_MAX_BANDS) {
561 if (x%2 == 0) continue;
562 conOPut("\x1b[");
563 conOInt(BandHeight+2);
564 conOPut(";");
565 conOInt(3+x*4);
566 conOPut("H");
567 //!!!conOInt(cast(int)MBandEq.bandfrqs[x]);
570 foreach (immutable x; 0../*MBandEq.Bands*/EQ_MAX_BANDS) {
571 if (x == eqCurBand) conOColorFG(0xff_ff_00);
572 int bv = alsaEqBands[x];
573 if (bv < -70) bv = -70; else if (bv > 30) bv = 30;
574 bv += 70;
575 int y = BandHeight-BandHeight*bv/100;
576 conOPut("\x1b[");
577 conOInt(y);
578 conOPut(";");
579 conOInt(3+x*4);
580 conOPut("H");
581 conOPut("===");
582 if (y != BandHeight-BandHeight*(0+70)/100) {
583 conOPut("\x1b[");
584 conOInt(BandHeight-BandHeight*(0+70)/100);
585 conOPut(";");
586 conOInt(3+x*4);
587 conOPut("H");
588 conOPut("---");
590 if (x == eqCurBand) conOColorFG(0x80_80_80);
592 } else {
593 // remove highlight from old band
594 if (eqCurBandOld >= 0 && eqCurBand != eqCurBandOld) {
595 //conOColorFG(0x80_80_80);
596 foreach (immutable y; 0..BandHeight) {
597 conOAt(3+eqCurBandOld*4, y+1);
598 conOPut(" | ");
600 conOAt(3+eqCurBandOld*4, BandHeight-BandHeight*(0+70)/100);
601 conOPut("---");
602 conOAt(3+eqCurBandOld*4, BandHeight-BandHeight*(eqclamp(alsaEqBands[eqCurBandOld])+70)/100);
603 conOPut("===");
605 // repaint new band
606 conOColorFG(0xff_ff_00);
607 foreach (immutable y; 0..BandHeight) {
608 conOAt(3+eqCurBand*4, y+1);
609 conOPut(" | ");
611 conOAt(3+eqCurBand*4, BandHeight-BandHeight*(0+70)/100);
612 conOPut("---");
613 conOAt(3+eqCurBand*4, BandHeight-BandHeight*(eqclamp(alsaEqBands[eqCurBand])+70)/100);
614 conOPut("===");
617 eqCurBandOld = eqCurBand;
618 eqOldBands[] = alsaEqBands[];
622 bool eqProcessKey (TtyEvent key) {
623 static int eqclamp (int v) { pragma(inline, true); return (v < -70 ? -70 : v > 30 ? 30 : v); }
625 if (!eqBandEditor) return false;
626 switch (key.key) {
627 case TtyEvent.Key.Left: if (eqCurBand > 0) --eqCurBand; return true;
628 case TtyEvent.Key.Right: if (++eqCurBand >= /*MBandEq.Bands*/EQ_MAX_BANDS) eqCurBand = /*MBandEq.Bands*/EQ_MAX_BANDS-1; return true;
629 case TtyEvent.Key.Up: alsaEqBands[eqCurBand] = eqclamp(alsaEqBands[eqCurBand]+10); return true;
630 case TtyEvent.Key.Down: alsaEqBands[eqCurBand] = eqclamp(alsaEqBands[eqCurBand]-10); return true;
631 case TtyEvent.Key.Home: alsaEqBands[eqCurBand] = 30; return true;
632 case TtyEvent.Key.End: alsaEqBands[eqCurBand] = -70; return true;
633 case TtyEvent.Key.Insert: alsaEqBands[eqCurBand] = 0; return true;
634 default:
635 if (key == "S" || key == "s") {
636 try {
637 import iv.vfs.io;
638 auto fo = VFile("./mbeqa.rc", "w");
639 fo.writeln("eq_reset");
640 foreach (immutable idx, int v; alsaEqBands[]) fo.writeln("eq_band ", idx, " ", v);
641 } catch (Exception) {}
642 } else if (key == "L" || key == "l") {
643 concmd("exec mbeqa.rc");
644 } else if (key == "R" || key == "r") {
645 concmd("eq_reset");
647 break;
649 return false;
653 // ////////////////////////////////////////////////////////////////////////// //
654 enum Action { None, Quit, Prev, Next }
655 __gshared Action conaction;
658 Action playFile () {
659 if (plidx < 0) plidx = 0;
660 if (plidx >= playlist.length) return Action.Quit;
661 auto fname = playlist[plidx];
663 StreamIO sio = StreamIO.open(VFile(fname));
664 if (!sio.valid) return Action.Next;
665 scope(exit) sio.close();
667 uint realRate = alsaGetBestSampleRate(sio.rate);
668 conwriteln("real sampling rate: ", realRate);
670 long prevtime = -1;
672 if (!alsaIsOpen || alsaRate != sio.rate || alsaChannels != sio.channels) {
673 if (!alsaInit(sio.rate, sio.channels)) assert(0, "cannot init ALSA playback");
676 bool oldpaused = !paused;
677 int oldgain = alsaGain+1;
679 void writeTime () {
680 import core.stdc.stdio : snprintf;
681 char[128] xbuf;
682 //auto len = snprintf(xbuf.ptr, xbuf.length, "\r%d:%02d / %d:%02d (%d)%s\x1b[K", 0, 0, sio.timetotal/1000/60, sio.timetotal/1000%60, gain, (paused ? " !".ptr : "".ptr));
683 long tm = sio.timeread;
684 auto len = snprintf(xbuf.ptr, xbuf.length, "\r%d:%02d / %d:%02d (%d)%s\x1b[K", cast(uint)(tm/1000/60), cast(uint)(tm/1000%60), cast(uint)(sio.timetotal/1000/60), cast(uint)(sio.timetotal/1000%60), alsaGain, (paused ? " !".ptr : "".ptr));
685 ttyRawWrite(xbuf[0..len]);
687 scope(exit) ttyRawWrite("\r\x1b[0m\x1b[K\n");
689 writeTime();
691 void processKeys (bool dowait) {
692 for (;;) {
693 if (!dowait && !ttyIsKeyHit) return;
694 dowait = false; // only first iteration should be blocking
695 auto key = ttyReadKey(-1, 20);
696 if (!ttyconEvent(key) && !eqProcessKey(key)) {
697 long oldtm = sio.timeread;
698 long tm = oldtm;
699 switch (key.key) {
700 case TtyEvent.Key.Left:
701 tm -= 10*1000;
702 if (tm < 0) tm = 0;
703 break;
704 case TtyEvent.Key.Right:
705 tm += 10*1000;
706 break;
707 case TtyEvent.Key.Down:
708 tm -= 60*1000;
709 break;
710 case TtyEvent.Key.Up:
711 tm += 60*1000;
712 break;
713 case TtyEvent.Key.F1:
714 concmd("eq_editor toggle");
715 break;
716 case TtyEvent.Key.Char:
717 if (key.ch == '<') { concmd("prev"); return; }
718 if (key.ch == '>') { concmd("next"); return; }
719 if (key.ch == 'q') { concmd("quit"); return; }
720 if (key.ch == ' ') { concmd("paused toggle"); return; }
721 if (key.ch == '0') alsaGain = 0;
722 if (key.ch == '-') { alsaGain -= 10; if (alsaGain < -100) alsaGain = -100; }
723 if (key.ch == '+') { alsaGain += 10; if (alsaGain > 1000) alsaGain = 1000; }
724 break;
725 default: break;
727 if (tm < 0) tm = 0;
728 if (tm >= sio.timetotal) tm = (sio.timetotal ? sio.timetotal-1 : 0);
729 if (oldtm != tm) {
730 //conwriteln("seek to: ", tm);
731 sio.seekToTime(cast(uint)tm);
737 mainloop: for (;;) {
738 if (!paused) {
739 if (!alsaIsOpen) {
740 if (!alsaInit(sio.rate, sio.channels)) assert(0, "cannot init ALSA playback");
743 auto frmread = sio.readFrames(buffer.ptr, BUF_SIZE/sio.channels);
744 if (frmread <= 0) break;
746 alsaWriteShort(buffer[0..frmread*sio.channels]);
747 } else {
748 if (alsaIsOpen) alsaShutdown();
749 //import core.thread, core.time;
750 //Thread.sleep(100.msecs);
753 long tm = sio.timeread;
754 if (tm/1000 != prevtime/1000 || paused != oldpaused || alsaGain != oldgain) {
755 prevtime = tm;
756 oldpaused = paused;
757 oldgain = alsaGain;
758 writeTime();
761 processKeys(paused);
763 auto conoldcdump = conDump;
764 scope(exit) conDump = conoldcdump;
765 conDump = ConDump.none;
766 conProcessQueue();
768 drawEqBands();
769 ttyconDraw();
770 if (isQuitRequested) return Action.Quit;
771 if (conaction != Action.None) { auto res = conaction; conaction = Action.None; return res; }
774 return Action.Next;
778 extern(C) void atExitRestoreTty () {
779 ttySetNormal();
783 void main (string[] args) {
784 alsaEqBands[] = 0;
786 conRegUserVar!bool("shuffle", "shuffle playlist");
788 conRegVar!alsaRQuality(0, 10, "rsquality", "resampling quality; 0=worst, 10=best, default is 8");
789 conRegVar!alsaDevice("device", "audio output device");
790 conRegVar!alsaGain(-100, 1000, "gain", "playback gain (0: normal; -100: silent; 100: 2x)");
791 conRegVar!alsaLatencyms(5, 5000, "latency", "playback latency, in milliseconds");
792 conRegVar!alsaEnableResampling("use_resampling", "allow audio resampling?");
793 conRegVar!alsaEnableEqualizer("use_equalizer", "allow audio equalizer?");
795 conRegVar!paused("paused", "is playback paused?");
797 conRegVar!eqBandEditor("eq_editor", "is eq band editor active?");
799 // lol, `std.trait : ParameterDefaults()` blocks using argument with name `value`
800 conRegFunc!((int idx, byte value) {
801 if (value < -70) value = -70;
802 if (value > 30) value = 30;
803 if (idx >= 0 && idx < alsaEqBands.length) {
804 if (alsaEqBands[idx] != value) {
805 alsaEqBands[idx] = value;
807 } else {
808 conwriteln("invalid equalizer band index: ", idx);
810 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
812 conRegFunc!(() {
813 alsaEqBands[] = 0;
814 })("eq_reset", "reset equalizer");
816 conRegFunc!(() { conaction = Action.Next; })("next", "next song");
817 conRegFunc!(() { conaction = Action.Prev; })("prev", "previous song");
819 concmd("exec .config.rc tan");
820 concmd("exec mbeqa.rc tan");
821 conProcessArgs!true(args);
823 foreach (string fname; args[1..$]) {
824 import std.file;
825 if (fname.length == 0) continue;
826 try {
827 if (fname.exists && fname.isFile) playlist ~= fname;
828 } catch (Exception) {}
831 if (playlist.length == 0) {
832 import core.stdc.stdio : printf;
833 import core.stdc.stdlib : exit, EXIT_FAILURE;
834 printf("no files!\n");
835 exit(EXIT_FAILURE);
838 if (ttyIsRedirected) {
839 import core.stdc.stdio : printf;
840 import core.stdc.stdlib : exit, EXIT_FAILURE;
841 printf("no redirects, please!\n");
842 exit(EXIT_FAILURE);
845 if (conGetVar!bool("shuffle")) {
846 import std.random;
847 playlist.randomShuffle;
850 ttySetRaw();
852 import core.stdc.stdlib : atexit;
853 atexit(&atExitRestoreTty);
855 ttyconInit();
857 mainloop: for (;;) {
858 final switch (playFile()) with (Action) {
859 case Prev: if (plidx > 0) --plidx; break;
860 case None:
861 case Next: ++plidx; break;
862 case Quit: break mainloop;