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
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
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',
61 foreach (dchar ch
; s
) {
63 if (ch
< ' ') ch
= ' ';
64 if (ch
== 127) ch
= '?';
68 foreach (immutable idx
, wchar wch
; charMapKOI8
[]) {
69 if (wch
== ch
) { res
~= cast(char)(idx
+128); found
= true; break; }
71 if (!found
) res
~= '?';
78 // ////////////////////////////////////////////////////////////////////////// //
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
); }
99 if (type
.length
== 0) return false;
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);
110 @property string
typestr () const pure nothrow @safe @nogc { return type
; }
113 if (type
.length
== 0) return;
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;
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;
128 if (ff
is null) return 0;
132 short* bp
= cast(short*)buf
;
134 int xrd
= (count
<= flcbuf
.length ? count
: cast(int)flcbuf
.length
);
135 auto rd
= drflac_read_s32(ff
, xrd
, flcbuf
.ptr
); // samples
137 samplesread
+= rd
; // number of samples read
138 foreach (int v
; flcbuf
[0..cast(int)rd
]) *bp
++ = cast(short)(v
>>16);
142 return cast(int)(res
/channels
); // number of frames read
144 if (vi
is null) return 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
151 auto dptr
= cast(short*)buf
;
152 if (of
is null) return 0;
155 while (count
> 0 && smpbufpos
< smpbufused
) {
156 *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
157 if (channels
== 2) *dptr
++ = smpbuf
.ptr
[smpbufpos
++];
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
[];
168 smpbufused
= cast(uint)rd
.length
;
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
) {
178 if (!mp3
.decodeNextFrame(&reader
)) return 0;
179 mfm
= mp3
.frameSamples
;
180 if (mp3
.sampleRate
!= rate || mp3
.channels
!= channels
) return 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
++];
191 samplesread
+= mp3smpused
-oldmpu
; // number of samples read
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
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);
210 if (!drflac_seek_to_sample(ff
, snum
)) {
211 drflac_seek_to_sample(ff
, 0);
215 return snum
/channels
;
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
;
225 if (of
is null) return 0;
227 samplesread
= of
.smpcurtime
*channels
;
228 return samplesread
/channels
;
230 if (!mp3
.valid
) return 0;
232 if (mp3info
.index
.length
== 0 || snum
== 0) {
233 // alas, we cannot seek here
236 mp3
.restart(&reader
);
239 // find frame containing our sample
240 // stupid binary search; ignore overflow bug
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
) {
250 fl
.seek(mp3info
.index
[cast(usize
)mid
].fpos
);
251 mp3smpused
= cast(uint)(snum
-smps
);
255 if (snum
< smps
) end
= mid
-1; else start
= mid
+1;
257 // alas, we cannot seek
260 mp3
.restart(&reader
);
270 Mp3Info mp3info
; // scanned info, frame index
276 uint smpbufpos
, smpbufused
;
278 int reader (void[] buf
) {
280 auto rd
= fl
.rawRead(buf
);
281 return cast(int)rd
.length
;
282 } catch (Exception e
) {}
287 static StreamIO
open (VFile fl
) {
288 import std
.string
: fromStringz
;
296 import core
.stdc
.stdio
;
297 import core
.stdc
.stdlib
: malloc
, free
;
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
);
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
;
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
);
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);
342 } catch (Exception
) {}
346 if (ov_fopen(fl
, &sio
.vf
) == 0) {
347 scope(failure
) ov_clear(&sio
.vf
);
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);
368 } catch (Exception
) {}
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");
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
);
386 conwritefln
!"time: %d:%02d"(sio
.timetotal
/1000/60, sio
.timetotal
/1000%60);
388 } catch (Exception
) {}
393 sio
.mp3
= new MP3Decoder(&sio
.reader
);
396 // scan file to determine number of frames
399 sio
.mp3info
= mp3Scan
!true((void[] buf
) => cast(int)fl
.rawRead(buf
).length
); // build index too
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);
413 } catch (Exception
) {}
416 } catch (Exception
) {}
417 return StreamIO
.init
;
422 // ////////////////////////////////////////////////////////////////////////// //
423 enum BUF_SIZE
= 4096;
424 short[BUF_SIZE
] buffer
;
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;
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 {
452 auto left
= cast(uint)cobuf
.length
-cobufpos
;
456 if (s
.length
< left
) left
= cast(uint)s
.length
;
457 cobuf
[cobufpos
..cobufpos
+left
] = s
[0..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
;
467 static if (__traits(isUnsigned
, T
)) {
468 static if (T
.sizeof
> 4) {
469 auto len
= snprintf(buf
.ptr
, buf
.length
, "%llu", n
);
471 auto len
= snprintf(buf
.ptr
, buf
.length
, "%u", cast(uint)n
);
474 static if (T
.sizeof
> 4) {
475 auto len
= snprintf(buf
.ptr
, buf
.length
, "%lld", n
);
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));
489 void conOColorBG (uint c
) nothrow @trusted @nogc {
490 conOPut("\x1b[48;5;");
491 conOInt(ttyRgb2Color((c
>>16)&0xff, (c
>>8)&0xff, c
&0xff));
495 void conOAt (int x
, int y
) nothrow @trusted @nogc {
503 if (eqBandEditor
!= eqBandEditorOld
&& !eqBandEditor
) {
504 // erase editor and exit
505 eqBandEditorOld
= eqBandEditor
;
506 eqCurBandOld
= eqCurBand
;
507 eqOldBands
[] = int.min
;
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");
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
;
530 scope(exit
) { conOPut("\x1b8"); conOFlush(); } // restore
532 conOPut("\x1b7\x1b[0m\x1b[H"); // save, reset color, goto top
534 conOColorBG(0x00_00_c
0);
535 conOColorFG(0x80_80_80);
538 foreach (immutable y
; 0..BandHeight
) {
540 foreach (immutable x
; 0../*MBandEq.Bands*/EQ_MAX_BANDS
) {
541 if (x
== eqCurBand
) conOColorFG(0xff_ff
_00);
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;
553 conOInt(BandHeight
+1);
557 //!!!conOInt(cast(int)MBandEq.bandfrqs[x]);
560 foreach (immutable x
; 0../*MBandEq.Bands*/EQ_MAX_BANDS
) {
561 if (x
%2 == 0) continue;
563 conOInt(BandHeight
+2);
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;
575 int y
= BandHeight
-BandHeight
*bv
/100;
582 if (y
!= BandHeight
-BandHeight
*(0+70)/100) {
584 conOInt(BandHeight
-BandHeight
*(0+70)/100);
590 if (x
== eqCurBand
) conOColorFG(0x80_80_80);
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);
600 conOAt(3+eqCurBandOld
*4, BandHeight
-BandHeight
*(0+70)/100);
602 conOAt(3+eqCurBandOld
*4, BandHeight
-BandHeight
*(eqclamp(alsaEqBands
[eqCurBandOld
])+70)/100);
606 conOColorFG(0xff_ff
_00);
607 foreach (immutable y
; 0..BandHeight
) {
608 conOAt(3+eqCurBand
*4, y
+1);
611 conOAt(3+eqCurBand
*4, BandHeight
-BandHeight
*(0+70)/100);
613 conOAt(3+eqCurBand
*4, BandHeight
-BandHeight
*(eqclamp(alsaEqBands
[eqCurBand
])+70)/100);
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;
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;
635 if (key
== "S" || key
== "s") {
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") {
653 // ////////////////////////////////////////////////////////////////////////// //
654 enum Action
{ None
, Quit
, Prev
, Next
}
655 __gshared Action conaction
;
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
);
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;
680 import core
.stdc
.stdio
: snprintf
;
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");
691 void processKeys (bool dowait
) {
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
;
700 case TtyEvent
.Key
.Left
:
704 case TtyEvent
.Key
.Right
:
707 case TtyEvent
.Key
.Down
:
710 case TtyEvent
.Key
.Up
:
713 case TtyEvent
.Key
.F1
:
714 concmd("eq_editor toggle");
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; }
728 if (tm
>= sio
.timetotal
) tm
= (sio
.timetotal ? sio
.timetotal
-1 : 0);
730 //conwriteln("seek to: ", tm);
731 sio
.seekToTime(cast(uint)tm
);
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
]);
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
) {
763 auto conoldcdump
= conDump
;
764 scope(exit
) conDump
= conoldcdump
;
765 conDump
= ConDump
.none
;
770 if (isQuitRequested
) return Action
.Quit
;
771 if (conaction
!= Action
.None
) { auto res
= conaction
; conaction
= Action
.None
; return res
; }
778 extern(C
) void atExitRestoreTty () {
783 void main (string
[] args
) {
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
;
808 conwriteln("invalid equalizer band index: ", idx
);
810 })("eq_band", "set equalizer band #n to v (band 0 is preamp)");
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..$]) {
825 if (fname
.length
== 0) continue;
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");
838 if (ttyIsRedirected
) {
839 import core
.stdc
.stdio
: printf
;
840 import core
.stdc
.stdlib
: exit
, EXIT_FAILURE
;
841 printf("no redirects, please!\n");
845 if (conGetVar
!bool("shuffle")) {
847 playlist
.randomShuffle
;
852 import core
.stdc
.stdlib
: atexit
;
853 atexit(&atExitRestoreTty
);
858 final switch (playFile()) with (Action
) {
859 case Prev
: if (plidx
> 0) --plidx
; break;
861 case Next
: ++plidx
; break;
862 case Quit
: break mainloop
;