1 /* daylet -- .AY music file player
2 * Copyright (C) 2001 Russell Marks and Ian Collier
3 * Copyright (C) 2012-2017 Ketmar // Invisible Vector
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/>.
20 import core
.time
: MonoTime
;
36 // ////////////////////////////////////////////////////////////////////////// //
44 // ////////////////////////////////////////////////////////////////////////// //
45 __gshared ZXModule
[] aymodules
;
46 __gshared TimeTag tunetime
;
47 __gshared
int defaultStopAfter
= 3*60*50; /* in intrs */
48 __gshared
int stopafter
= 3*60*50; /* in intrs */
49 __gshared
int fadetime
= 10*50; /* fadeout time *after* that in intr, 0=none */
50 //__gshared bool doneFade = false;
51 __gshared
bool paused
= false;
52 __gshared
bool playOneTrackOnly
= false;
53 __gshared
int curTrackIdx
= 0;
55 //enum FRAME_STATES_48 = 69888; /*(3500000/50)*/
56 //enum FRAME_STATES_128 = 70908; /*(3546900/50)*/
57 //enum FRAME_STATES_CPC = 4000000/50;
60 // ////////////////////////////////////////////////////////////////////////// //
61 void tunetimeReset () {
62 tunetime
.min
= tunetime
.sec
= tunetime
.subsecframes
= 0;
67 // ////////////////////////////////////////////////////////////////////////// //
68 __gshared
uint silentFrames
= 0;
69 __gshared
uint silentMax
= cast(uint)(3*50); // max frames of silence before skipping
70 __gshared
int skipIntrs
= 0;
74 // <0: go to next file
75 // >0: moved to next track
76 // 0: continue with this track
77 int sendSoundFrame () {
78 if (paused
&& skipIntrs
< 1) {
80 sndvSkipSynth
= false;
81 soundForceReinit(false); // don't reset AY
85 // check for fade needed
87 if (!done_fade && stopafter && (tunetime.min*60+tunetime.sec)*50+tunetime.subsecframes >= stopafter) {
89 if (fadetime) sound_start_fade(fadetime/50);
94 sndvSkipSynth
= (skipIntrs
> 0);
95 if (!sndvSkipSynth
) soundForceReinit(false); // don't reset AY
98 if (++tunetime
.subsecframes
>= 50) {
99 tunetime
.subsecframes
= 0;
100 if (++tunetime
.sec
>= 60) {
105 // play frame, and stop if it's been silent for a while
106 if (soundFrame
!true()) {
108 //conwriteln("silent frames: ", silent_for);
112 if ((silentMax
> 0 && silentFrames
>= silentMax
) ||
(tunetime
.min
*60+tunetime
.sec
)*50+tunetime
.subsecframes
>= stopafter
+fadetime
) {
114 // do next track, or file, or just stop
116 if (playOneTrackOnly
) return -1;
117 if (curTrackIdx
>= aymodules
.length
) return -1;
125 const(char)[] safeChars (const(char)[] s
) {
126 foreach (char ch
; s
) {
127 if (ch
< ' ' || ch
== 127) {
128 auto res
= new char[](s
.length
);
129 foreach (immutable idx
, char cc
; s
) {
130 if (cc
< ' ' || cc
== 127) cc
= '.';
140 // ////////////////////////////////////////////////////////////////////////// //
141 __gshared string
[] ayplaylist
;
144 enum Action
{ None
, Quit
, Prev
, Next
}
146 Action
ayPlayFile (VFile fl
, bool lastTrack
, bool ttyGoUp
) {
147 Action ares
= Action
.None
;
152 scope(exit
) delete moddata
;
155 if (fsz
< 1 || fsz
> 4*1024*1024) throw new Exception("invalid file size");
156 moddata
.length
= cast(uint)fsz
;
158 fl
.rawReadExact(moddata
);
159 aymodules
= zxModuleDetect(moddata
);
160 if (aymodules
.length
== 0) throw new Exception("not a module");
164 foreach (immutable _
; 0..6) conwrite("\x1b[A\x1b[K");
168 conwriteln("*** ", fl
.name
, " ***");
169 conwriteln("tracks: ", aymodules
.length
);
171 curTrackIdx
= (lastTrack ?
cast(int)aymodules
.length
-1 : 0);
174 bool oldpause
= !paused
;
176 bool pauseStars
= false;
178 void drawTime(bool force
=false) () {
179 import core
.stdc
.stdio
: snprintf
;
180 int newtime
= tunetime
.min
*60+tunetime
.sec
;
182 if (skipIntrs
> 0 && !paused
) return;
183 if (newtime
== oldtime
&& oldpause
== paused
) {
187 pauseTime
= MonoTime
.currTime
;
191 auto curtm
= MonoTime
.currTime
;
192 if ((curtm
-pauseTime
).total
!"msecs" < 1000) return;
194 pauseStars
= !pauseStars
;
198 if (oldpause
!= paused
) pauseStars
= false;
202 auto len
= snprintf(xbuf
.ptr
, xbuf
.length
, "\r%u:%02u / ", cast(uint)(newtime
/60), cast(uint)(newtime
%60));
203 ttyRawWrite(xbuf
[0..len
]);
204 auto sta
= stopafter
/50;
206 ttyRawWrite("--:--");
208 len
= snprintf(xbuf
.ptr
, xbuf
.length
, "%d:%02d (%2d)", cast(uint)(sta
/60), cast(uint)(sta
%60), cast(uint)(fadetime
/50/60));
209 ttyRawWrite(xbuf
[0..len
]);
211 if (paused
) ttyRawWrite("!"); else ttyRawWrite(" ");
213 if (!sta || tw
-58-2 < 2) return;
214 if (newtime
> sta
) newtime
= sta
;
215 xbuf
[] = (pauseStars ?
'*' : '=');
219 xbuf
[1+(56*newtime
/sta
)] = '|';
220 ttyRawWrite(xbuf
[0..58]);
222 len
= snprintf(xbuf
.ptr
, xbuf
.length
, "%4d", cast(uint)Config
.volumeAY
);
223 ttyRawWrite(xbuf
[0..len
]);
227 bool processKeys () {
228 while (ttyIsKeyHit
) {
229 auto key
= ttyReadKey(0, 20);
230 if (!ttyconEvent(key
)) {
232 case TtyEvent
.Key
.Char
:
233 if (key
.ch
== '<') { if (curTrackIdx
> 0) --curTrackIdx
; else ares
= Action
.Prev
; return true; }
234 if (key
.ch
== '>') { ++curTrackIdx
; return true; }
235 if (key
.ch
== 'q') { ares
= Action
.Quit
; return true; }
237 if (Config
.volumeAY
< 10) Config
.volumeAY
= 0; else Config
.volumeAY
-= 10;
243 if (Config
.volumeAY
> 390) Config
.volumeAY
= 400; else Config
.volumeAY
+= 10;
248 if (key
.ch
== ' ') paused
= !paused
;
251 "\r\x1b[4A=== HELP ===\x1b[K\n"~
252 " <: previous -: AY volume down\x1b[K\n"~
253 " >: next +: AY volume up\x1b[K\n"~
255 " space: pause\x1b[K\n"
261 case TtyEvent
.Key
.Right
:
264 case TtyEvent
.Key
.Left
:
265 int curtime
= (tunetime
.min
*60+tunetime
.sec
)*50;
267 aymodules
[curTrackIdx
].reset();
269 if ((curtime
-= 50*5) < 0) curtime
= 0;
280 trackloop
: while (ares
== Action
.None
) {
281 if (curTrackIdx
>= aymodules
.length
) break;
282 auto mod
= aymodules
[curTrackIdx
];
284 sndvSkipSynth
= false;
286 Config
.machine
= zxFindMachine(mod
.modelName
);
290 conwrite("\r\x1b[K");
291 foreach (immutable _
; 0..3) conwrite("\x1b[A\x1b[K");
296 conwriteln("track : [", curTrackIdx
+1, "/", aymodules
.length
, "] ", mod
.name
.safeChars
, " (", mod
.typeStr
, ")");
297 conwriteln("author: ", mod
.author
.safeChars
);
298 conwriteln("misc : ", mod
.misc
.safeChars
);
300 //if (oldtime >= 0) { ttyRawWrite("\r\n"); oldtime = -1; }
306 stopafter
= mod
.intrCount
;
307 fadetime
= mod
.fadeCount
;
308 if (stopafter
== 0) {
309 stopafter
= defaultStopAfter
;
310 fadetime
= 50; //10*50;
312 //if (fadetime < 50) fadetime = 50;
318 if (!paused || skipIntrs
> 0) {
319 if (!mod
.doIntr()) continue trackloop
; // moved to next track
320 auto ff
= sendSoundFrame();
321 if (ff
< 0) { ares
= Action
.Next
; break trackloop
; } // go to next file
322 if (ff
> 0) continue trackloop
; // moved to next track
328 auto conoldcdump
= conDump
;
329 scope(exit
) conDump
= conoldcdump
;
330 conDump
= ConDump
.none
;
334 if (processKeys()) break;
336 if (isQuitRequested
) ares
= Action
.Quit
;
337 if (ares
!= Action
.None
) break trackloop
;
341 if (oldtime
>= 0) ttyRawWrite("\r\n");
342 if (ares
== Action
.None
) ares
= Action
.Next
;
347 // ////////////////////////////////////////////////////////////////////////// //
348 void main (string
[] args
) {
349 if (ttyIsRedirected
) assert(0, "no redirects, please!");
351 fuck_alsa_messages();
353 conRegUserVar
!bool("calc_total", "calculate total time");
354 conRegUserVar
!bool("dump_bad", "dump bad modules to stderr when calculating totals");
356 conRegUserVar
!bool("shuffle", "shuffle playlist");
357 conRegUserVar
!string("dbg_default_ay", "debug: default AY file");
359 //conRegVar!zxmodel("zx_model", "zx spectrum model");
360 conRegVar
!silentMax("silent_max_frames", "max frames of silence before skipping track");
362 conRegVar
!(Config
.volumeBeeper
)(0, 400, "volume_beeper", "beeper volume");
363 conRegVar
!(Config
.volumeAY
)(0, 400, "volume_ay", "ay volume");
364 conRegVar
!(Config
.stereoType
)("stereo_mode", "AY stereo mode: ABC, ACB, NONE");
365 conRegVar
!(Config
.speakerType
)("speaker_type", "speaker type: TV, Beeper, Default, Flat, Crisp");
367 concmd("exec daylet.rc tan");
368 conProcessQueue(256*1024); // load config
369 conProcessArgs
!true(args
);
371 // process all console commands
372 foreach (immutable _
; 0..42) if (!conProcessQueue()) break;
374 //Config.stereoType = Config.Stereo.BCA;
376 if (args
.length
== 1) {
377 string day
= conGetVar
!string("dbg_default_ay");
378 if (day
.length
) args
~= day
;
381 foreach (string fname
; args
[1..$]) {
382 if (fname
.length
== 0) continue;
383 if (fname
.length
> 1 && fname
[0..2] == "!/") fname
= exeDir
~fname
[1..$];
384 if (fname
.endsWithCI(".zip")) {
385 auto drv
= vfsAddPak(fname
);
388 foreach (const ref de; vfsFileList()) {
389 if (de.name
.zxModuleGoodExtension
) {
390 ayplaylist
~= aname
~":"~de.name
;
394 } else if (fname
.zxModuleGoodExtension
) {
399 if (ayplaylist
.length
< 1) assert(0, "no files!");
400 if (ayplaylist
.length
> 1024*1024*32) assert(0, "too many files");
402 if (ayplaylist
.length
> 1) conwriteln(ayplaylist
.length
, " modules found");
404 if (conGetVar
!bool("calc_total")) {
406 scope(exit
) delete moddata
;
409 static struct ModInfo
{
413 ModInfo
[string
] modcache
;
416 auto fl
= VFile(".zxmodules.cache");
418 auto nlen
= fl
.readNum
!uint;
419 if (nlen
== 0) break;
420 auto name
= new char[](nlen
);
421 fl
.rawReadExact(name
);
423 mi
.name
= cast(string
)name
; // it is safe to cast here
424 mi
.intrs
= fl
.readNum
!uint;
425 modcache
[mi
.name
] = mi
;
427 } catch (Exception e
) {
430 for (int idx
= 0; idx
< ayplaylist
.length
; ++idx
) {
432 if (auto mip
= ayplaylist
[idx
] in modcache
) {
433 total
+= cast(long)mip
.intrs
;
435 auto fl
= VFile(ayplaylist
[idx
]);
437 if (fsz
< 1 || fsz
> 4*1024*1024) throw new Exception("invalid file size");
438 moddata
.assumeSafeAppend
;
439 moddata
.length
= cast(uint)fsz
;
441 fl
.rawReadExact(moddata
);
442 auto aym
= zxModuleDetect
!true(moddata
);
443 if (aym
.length
== 0) throw new Exception("not a module");
445 mi
.name
= ayplaylist
[idx
];
446 foreach (ZXModule mod
; aym
) {
447 mi
.intrs
+= mod
.intrCount
+mod
.fadeCount
;
448 total
+= cast(long)mod
.intrCount
+mod
.fadeCount
;
450 auto fo
= VFile(".zxmodules.cache", "a");
451 fo
.writeNum
!uint(cast(uint)mi
.name
.length
);
452 fo
.rawWriteExact(mi
.name
[]);
453 fo
.writeNum
!uint(mi
.intrs
);
454 modcache
[mi
.name
] = mi
;
456 } catch (Exception e
) {
457 conwriteln("BAD MODULE: ", ayplaylist
[idx
], " (", e
.msg
, ")");
458 if (conGetVar
!bool("dump_bad")) {
459 import core
.stdc
.stdio
;
460 stderr
.fprintf("%.*s\n", cast(uint)ayplaylist
[idx
].length
, ayplaylist
[idx
].ptr
);
462 { import std
.algorithm
: remove
; ayplaylist
= ayplaylist
.remove(idx
); }
467 conwrite(ayplaylist
.length
, " module", (ayplaylist
.length
!= 1 ?
"s" : ""), ", ");
468 total
/= 50; // seconds
469 auto days
= total
/(60*60*24);
471 auto hours
= total
/(60*60);
473 auto mins
= total
/60;
475 if (days
> 0) conwritefln
!"%s day%s %02s:%02s:%02s"(days
, (days
!= 1 ?
"s" : ""), hours
, mins
, total
);
476 else if (hours
> 0) conwritefln
!"%s:%02s:%02s"(hours
, mins
, total
);
477 else conwritefln
!"%2s:%02s"(mins
, total
);
480 if (conGetVar
!bool("shuffle")) {
481 import std
.random
: randomShuffle
;
482 ayplaylist
.randomShuffle
;
486 scope(exit
) ttySetNormal();
490 bool fromLast
= false;
491 bool ttyGoUp
= false;
493 mainloop
: while (aycurfile
< ayplaylist
.length
) {
495 string fname
= ayplaylist
[aycurfile
];
496 VFile fl
= VFile(fname
);
500 if (!sndvEnabled
) assert(0, "cannot init sound");
501 scope(exit
) soundDeinit();
502 act
= ayPlayFile(fl
, fromLast
, ttyGoUp
);
512 if (aycurfile
> 0) { --aycurfile
; fromLast
= true; }
517 } catch (Exception e
) {
519 conwriteln("ERROR(", ayplaylist
[aycurfile
], "): ", e
.msg
);
520 { import std
.algorithm
: remove
; ayplaylist
= ayplaylist
.remove(aycurfile
); }