diff --git a/assets/js/console.js b/assets/js/console.js index 19ada56..5cb147e 100644 --- a/assets/js/console.js +++ b/assets/js/console.js @@ -152,14 +152,12 @@ log.appendChild(el); }; - // ---- 8-bit soundtrack: calm C-major pentatonic, WebAudio, no assets ---- - // 32 eighth-notes. midi note, 0 = rest. Sparse melody that rings. + // ---- 8-bit soundtrack: WebAudio, no assets. Tunes live in tunes.js ---- + // (window.TUNES), keyed by theme; the engine just plays the active one. + var TUNES = (typeof window !== "undefined" && window.TUNES) || {}; var chiptune = (function () { - var lead = [72, 0, 76, 0, 79, 0, 76, 0, 74, 0, 77, 0, 81, 0, 79, 0, - 72, 0, 76, 0, 84, 0, 79, 0, 81, 0, 79, 0, 76, 0, 74, 0]; - var bass = [48, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, - 41, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0]; - var spn = 60 / 84 / 2; // seconds per eighth-note at a gentle 84bpm + var tune = TUNES.default || { bpm: 84, wave: "triangle", lead: [0], bass: [0] }; + var spn = 60 / tune.bpm / 2; // seconds per eighth-note var ctx, master, timer, nextTime, step, playing = false, vol = 0.5; var freq = function (m) { return 440 * Math.pow(2, (m - 69) / 12); }; var voice = function (m, t, dur, type, peak) { // peak: per-note gain (distinct from master vol) @@ -173,13 +171,19 @@ }; var tick = function () { while (nextTime < ctx.currentTime + 0.12) { - voice(lead[step], nextTime, spn * 2.2, "triangle", 0.14); // let it ring - voice(bass[step], nextTime, spn * 7, "sine", 0.12); // slow pad - step = (step + 1) % lead.length; + voice(tune.lead[step], nextTime, spn * 2.2, tune.wave || "triangle", 0.14); // let it ring + voice(tune.bass[step], nextTime, spn * 7, "sine", 0.12); // slow pad + step = (step + 1) % tune.lead.length; nextTime += spn; } }; return { + // Swap the soundtrack to match a theme (light/dark/auto → default). + setTune: function (name) { + var t = TUNES[name] || TUNES.default; + if (!t || t === tune) return; + tune = t; spn = 60 / tune.bpm / 2; step = 0; + }, // Resolves once audio is actually running; rejects if the browser blocks it. on: function () { if (playing) return Promise.resolve(); @@ -190,7 +194,7 @@ playing = true; step = 0; return Promise.resolve(ctx.resume()).then(function () { nextTime = ctx.currentTime + 0.05; - tick(); timer = setInterval(tick, 25); + clearInterval(timer); tick(); timer = setInterval(tick, 25); }, function (err) { playing = false; throw err; }); }, off: function () { playing = false; clearInterval(timer); }, @@ -198,9 +202,28 @@ }; })(); + // Restore music across page navigations. Autoplay policy blocks a fresh + // load with no user gesture, so we never call on() here (a gesture-less + // resume() stays pending and would wedge `playing`); instead we resume on + // the first interaction. ponytail: relies on stored pref, not tab session. + (function () { + var v = localStorage.getItem("musicVolume"); + if (v != null) chiptune.setVolume(+v); + if (localStorage.getItem("music") !== "on") return; + chiptune.setTune(document.documentElement.dataset.theme); + var arm = function () { + document.removeEventListener("pointerdown", arm); + document.removeEventListener("keydown", arm); + chiptune.on().then(function () {}, function () {}); + }; + document.addEventListener("pointerdown", arm); + document.addEventListener("keydown", arm); + })(); + var applyTheme = function (mode) { if (mode === "auto") { localStorage.removeItem("theme"); delete document.documentElement.dataset.theme; } else { localStorage.setItem("theme", mode); document.documentElement.dataset.theme = mode; } + chiptune.setTune(mode); // swap soundtrack live if music is playing }; var open = function () { @@ -209,7 +232,7 @@ input.focus(); }; var close = function () { - chiptune.off(); + // Music keeps playing when closed — stop it with `music stop`. root.hidden = true; root.classList.remove("console--docked"); root.style.left = root.style.top = root.style.right = root.style.bottom = ""; @@ -242,13 +265,15 @@ res.lines.forEach(function (l) { write(l); }); if (res.theme) applyTheme(res.theme); if (res.music === "on") { + localStorage.setItem("music", "on"); + chiptune.setTune(document.documentElement.dataset.theme); chiptune.on().then( function () { write({ text: "♪ music on" }); log.scrollTop = log.scrollHeight; }, function () { write({ text: "music blocked by the browser — interact with the page and retry." }); log.scrollTop = log.scrollHeight; } ); } - if (res.music === "off") { chiptune.off(); write({ text: "music off" }); } - if (res.volume != null) chiptune.setVolume(res.volume); + if (res.music === "off") { localStorage.removeItem("music"); chiptune.off(); write({ text: "music off" }); } + if (res.volume != null) { localStorage.setItem("musicVolume", res.volume); chiptune.setVolume(res.volume); } input.value = ""; histIdx = cmdHistory.length; // reset recall to the (empty) current line log.scrollTop = log.scrollHeight; diff --git a/assets/js/tunes.js b/assets/js/tunes.js new file mode 100644 index 0000000..de1c15b --- /dev/null +++ b/assets/js/tunes.js @@ -0,0 +1,59 @@ +// Per-theme 8-bit soundtracks for the console. The player engine lives in +// console.js; this is just the note data. Each tune: { bpm, wave?, lead[], +// bass[] } — MIDI notes, 0 = rest, one bar = 8 eighth-notes. `wave` is the lead +// oscillator (bass is always a soft sine). Keyed by theme name; `default` +// covers light/dark/auto, and any theme without an entry falls back to it. +// The engine loops the arrays forever, so each phrase ends back on its tonic +// (bass on the dominant/sub-dominant) to make the wrap seamless. lead and bass +// must be the same length. Kept chill: slow tempo, sparse notes, lots of rests. +(function (root) { + var TUNES = { + // Calm C-major pentatonic — the original 4-bar loop. Bright and neutral. + default: { + bpm: 84, wave: "triangle", + lead: [72, 0, 76, 0, 79, 0, 76, 0, 74, 0, 77, 0, 81, 0, 79, 0, + 72, 0, 76, 0, 84, 0, 79, 0, 81, 0, 79, 0, 76, 0, 74, 0], + bass: [48, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, + 41, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0] + }, + // A natural minor, hollow square lead, slow — moody 8-bar loop, i–VI–iv–v. + dracula: { + bpm: 72, wave: "square", + lead: [69, 0, 72, 0, 76, 0, 72, 0, 74, 0, 72, 0, 69, 0, 67, 0, + 81, 0, 79, 0, 76, 0, 72, 0, 74, 0, 72, 0, 71, 0, 69, 0, + 76, 0, 79, 0, 81, 0, 84, 0, 83, 0, 81, 0, 79, 0, 76, 0, + 77, 0, 76, 0, 74, 0, 72, 0, 71, 0, 69, 0, 69, 0, 0, 0], + bass: [45, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, + 38, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0, + 45, 0, 0, 0, 0, 0, 0, 0, 41, 0, 0, 0, 0, 0, 0, 0, + 38, 0, 0, 0, 0, 0, 0, 0, 40, 0, 0, 0, 0, 0, 0, 0] + }, + // F major, soft sine lead — a sweet, romantic 8-bar loop, I–vi–IV–V. + valentine: { + bpm: 80, wave: "sine", + lead: [72, 0, 74, 0, 77, 0, 76, 0, 74, 0, 72, 0, 69, 0, 72, 0, + 77, 0, 81, 0, 79, 0, 77, 0, 76, 0, 74, 0, 72, 0, 69, 0, + 70, 0, 69, 0, 67, 0, 65, 0, 67, 0, 69, 0, 72, 0, 74, 0, + 77, 0, 76, 0, 74, 0, 72, 0, 69, 0, 67, 0, 65, 0, 0, 0], + bass: [41, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, + 34, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0, + 41, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 0, 0, 0, 0, + 34, 0, 0, 0, 0, 0, 0, 0, 36, 0, 0, 0, 0, 0, 0, 0] + }, + // D major, warm triangle lead — a cozy folk 8-bar loop, I–V–vi–IV with a + // plagal (IV→I) turnaround. Ink-on-cream, unhurried. + paper: { + bpm: 76, wave: "triangle", + lead: [74, 0, 78, 0, 81, 0, 78, 0, 76, 0, 74, 0, 71, 0, 74, 0, + 81, 0, 83, 0, 86, 0, 83, 0, 81, 0, 78, 0, 76, 0, 74, 0, + 79, 0, 78, 0, 76, 0, 74, 0, 76, 0, 78, 0, 81, 0, 83, 0, + 81, 0, 79, 0, 78, 0, 76, 0, 78, 0, 76, 0, 74, 0, 0, 0], + bass: [38, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, + 47, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0, + 38, 0, 0, 0, 0, 0, 0, 0, 45, 0, 0, 0, 0, 0, 0, 0, + 47, 0, 0, 0, 0, 0, 0, 0, 43, 0, 0, 0, 0, 0, 0, 0] + } + }; + if (typeof module !== "undefined" && module.exports) module.exports = TUNES; + else root.TUNES = TUNES; +})(typeof window !== "undefined" ? window : this); diff --git a/layouts/partials/console.html b/layouts/partials/console.html index 7c8ae76..3abe9e4 100644 --- a/layouts/partials/console.html +++ b/layouts/partials/console.html @@ -21,5 +21,8 @@ -{{ $js := resources.Get "js/console.js" | resources.Minify | resources.Fingerprint }} +{{/* tunes.js sets window.TUNES; console.js reads it — concat keeps that order. */}} +{{ $tunes := resources.Get "js/tunes.js" }} +{{ $console := resources.Get "js/console.js" }} +{{ $js := slice $tunes $console | resources.Concat "js/console.bundle.js" | resources.Minify | resources.Fingerprint }}