more scanline shader parameters
[zxemut.git] / daylet.d
blob7a50403352357ebf3cbd234be5266300960554ea
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/>.
18 module daylet;
20 import core.time : MonoTime;
22 import iv.alsa;
23 import iv.cmdcon;
24 import iv.cmdcontty;
25 import iv.rawtty;
26 import iv.strex;
27 import iv.vfs;
29 import zxinfo;
30 import emuconfig;
31 import sound;
33 import modplay;
36 // ////////////////////////////////////////////////////////////////////////// //
37 struct TimeTag {
38 int min;
39 int sec;
40 int subsecframes;
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;
63 //doneFade = false;
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;
73 // returns:
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) {
79 if (sndvSkipSynth) {
80 sndvSkipSynth = false;
81 soundForceReinit(false); // don't reset AY
83 soundBlankFrame();
84 } else {
85 // check for fade needed
87 if (!done_fade && stopafter && (tunetime.min*60+tunetime.sec)*50+tunetime.subsecframes >= stopafter) {
88 done_fade = 1;
89 if (fadetime) sound_start_fade(fadetime/50);
92 if (skipIntrs > 0) {
93 --skipIntrs;
94 sndvSkipSynth = (skipIntrs > 0);
95 if (!sndvSkipSynth) soundForceReinit(false); // don't reset AY
97 // incr time
98 if (++tunetime.subsecframes >= 50) {
99 tunetime.subsecframes = 0;
100 if (++tunetime.sec >= 60) {
101 tunetime.sec = 0;
102 ++tunetime.min;
105 // play frame, and stop if it's been silent for a while
106 if (soundFrame!true()) {
107 ++silentFrames;
108 //conwriteln("silent frames: ", silent_for);
109 } else {
110 silentFrames = 0;
112 if ((silentMax > 0 && silentFrames >= silentMax) || (tunetime.min*60+tunetime.sec)*50+tunetime.subsecframes >= stopafter+fadetime) {
113 silentFrames = 0;
114 // do next track, or file, or just stop
115 ++curTrackIdx;
116 if (playOneTrackOnly) return -1;
117 if (curTrackIdx >= aymodules.length) return -1;
118 return 1;
121 return 0;
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 = '.';
131 res[idx] = cc;
133 return res;
136 return s;
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;
149 aymodules = null;
151 ubyte[] moddata;
152 scope(exit) delete moddata;
154 auto fsz = fl.size;
155 if (fsz < 1 || fsz > 4*1024*1024) throw new Exception("invalid file size");
156 moddata.length = cast(uint)fsz;
157 fl.seek(0);
158 fl.rawReadExact(moddata);
159 aymodules = zxModuleDetect(moddata);
160 if (aymodules.length == 0) throw new Exception("not a module");
162 if (ttyGoUp) {
163 conwrite("\r");
164 foreach (immutable _; 0..6) conwrite("\x1b[A\x1b[K");
165 ttyGoUp = false;
168 conwriteln("*** ", fl.name, " ***");
169 conwriteln("tracks: ", aymodules.length);
171 curTrackIdx = (lastTrack ? cast(int)aymodules.length-1 : 0);
173 int oldtime = -1;
174 bool oldpause = !paused;
175 MonoTime pauseTime;
176 bool pauseStars = false;
178 void drawTime(bool force=false) () {
179 import core.stdc.stdio : snprintf;
180 int newtime = tunetime.min*60+tunetime.sec;
181 static if (!force) {
182 if (skipIntrs > 0 && !paused) return;
183 if (newtime == oldtime && oldpause == paused) {
184 if (!paused) return;
185 if (!oldpause) {
186 // just paused
187 pauseTime = MonoTime.currTime;
188 pauseStars = false;
189 oldpause = paused;
190 } else {
191 auto curtm = MonoTime.currTime;
192 if ((curtm-pauseTime).total!"msecs" < 1000) return;
193 pauseTime = curtm;
194 pauseStars = !pauseStars;
198 if (oldpause != paused) pauseStars = false;
199 char[128] xbuf;
200 oldtime = newtime;
201 oldpause = paused;
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;
205 if (!stopafter) {
206 ttyRawWrite("--:--");
207 } else {
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(" ");
212 int tw = ttyWidth;
213 if (!sta || tw-58-2 < 2) return;
214 if (newtime > sta) newtime = sta;
215 xbuf[] = (pauseStars ? '*' : '=');
216 //xbuf[] = '=';
217 xbuf[0] = '[';
218 xbuf[57] = ']';
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)) {
231 switch (key.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; }
236 if (key.ch == '-') {
237 if (Config.volumeAY < 10) Config.volumeAY = 0; else Config.volumeAY -= 10;
238 drawTime!true();
239 soundReinit();
240 break;
242 if (key.ch == '+') {
243 if (Config.volumeAY > 390) Config.volumeAY = 400; else Config.volumeAY += 10;
244 drawTime!true();
245 soundReinit();
246 break;
248 if (key.ch == ' ') paused = !paused;
249 if (key.ch == '?') {
250 ttyRawWrite(
251 "\r\x1b[4A=== HELP ===\x1b[K\n"~
252 " <: previous -: AY volume down\x1b[K\n"~
253 " >: next +: AY volume up\x1b[K\n"~
254 " q: quit\x1b[K\n"~
255 " space: pause\x1b[K\n"
257 drawTime!true();
258 break;
260 break;
261 case TtyEvent.Key.Right:
262 skipIntrs += 50*5;
263 break;
264 case TtyEvent.Key.Left:
265 int curtime = (tunetime.min*60+tunetime.sec)*50;
266 soundReinit();
267 aymodules[curTrackIdx].reset();
268 tunetimeReset();
269 if ((curtime -= 50*5) < 0) curtime = 0;
270 skipIntrs = curtime;
271 break;
272 default: break;
276 return false;
279 ttyGoUp = false;
280 trackloop: while (ares == Action.None) {
281 if (curTrackIdx >= aymodules.length) break;
282 auto mod = aymodules[curTrackIdx];
284 sndvSkipSynth = false;
285 skipIntrs = 0;
286 Config.machine = zxFindMachine(mod.modelName);
287 soundReinit();
289 if (ttyGoUp) {
290 conwrite("\r\x1b[K");
291 foreach (immutable _; 0..3) conwrite("\x1b[A\x1b[K");
292 ttyGoUp = false;
294 ttyGoUp = true;
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; }
302 silentFrames = 0;
303 soundResetAY();
304 tunetimeReset();
305 mod.reset();
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;
314 drawTime!true();
316 // play track
317 for (;;) {
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
323 } else {
324 // paused
325 soundBlankFrame();
328 auto conoldcdump = conDump;
329 scope(exit) conDump = conoldcdump;
330 conDump = ConDump.none;
331 conProcessQueue();
333 drawTime();
334 if (processKeys()) break;
335 ttyconDraw();
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;
343 return ares;
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);
386 auto aname = fname;
387 fname = null;
388 foreach (const ref de; vfsFileList()) {
389 if (de.name.zxModuleGoodExtension) {
390 ayplaylist ~= aname~":"~de.name;
393 vfsRemovePak(drv);
394 } else if (fname.zxModuleGoodExtension) {
395 ayplaylist ~= fname;
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")) {
405 ubyte[] moddata;
406 scope(exit) delete moddata;
407 long total = 0;
409 static struct ModInfo {
410 string name;
411 uint intrs;
413 ModInfo[string] modcache;
415 try {
416 auto fl = VFile(".zxmodules.cache");
417 for (;;) {
418 auto nlen = fl.readNum!uint;
419 if (nlen == 0) break;
420 auto name = new char[](nlen);
421 fl.rawReadExact(name);
422 ModInfo mi;
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) {
431 try {
432 if (auto mip = ayplaylist[idx] in modcache) {
433 total += cast(long)mip.intrs;
434 } else {
435 auto fl = VFile(ayplaylist[idx]);
436 auto fsz = fl.size;
437 if (fsz < 1 || fsz > 4*1024*1024) throw new Exception("invalid file size");
438 moddata.assumeSafeAppend;
439 moddata.length = cast(uint)fsz;
440 fl.seek(0);
441 fl.rawReadExact(moddata);
442 auto aym = zxModuleDetect!true(moddata);
443 if (aym.length == 0) throw new Exception("not a module");
444 ModInfo mi;
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); }
463 --idx;
467 conwrite(ayplaylist.length, " module", (ayplaylist.length != 1 ? "s" : ""), ", ");
468 total /= 50; // seconds
469 auto days = total/(60*60*24);
470 total %= 60*60*24;
471 auto hours = total/(60*60);
472 total %= 60*60;
473 auto mins = total/60;
474 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;
485 ttySetRaw();
486 scope(exit) ttySetNormal();
487 ttyconInit();
489 int aycurfile = 0;
490 bool fromLast = false;
491 bool ttyGoUp = false;
493 mainloop: while (aycurfile < ayplaylist.length) {
494 try {
495 string fname = ayplaylist[aycurfile];
496 VFile fl = VFile(fname);
497 Action act;
499 soundInit();
500 if (!sndvEnabled) assert(0, "cannot init sound");
501 scope(exit) soundDeinit();
502 act = ayPlayFile(fl, fromLast, ttyGoUp);
503 fromLast = false;
504 ttyGoUp = true;
506 final switch (act) {
507 case Action.None:
508 case Action.Next:
509 ++aycurfile;
510 break;
511 case Action.Prev:
512 if (aycurfile > 0) { --aycurfile; fromLast = true; }
513 break;
514 case Action.Quit:
515 break mainloop;
517 } catch (Exception e) {
518 ttyGoUp = false;
519 conwriteln("ERROR(", ayplaylist[aycurfile], "): ", e.msg);
520 { import std.algorithm : remove; ayplaylist = ayplaylist.remove(aycurfile); }