Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 39 additions & 14 deletions assets/js/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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();
Expand All @@ -190,17 +194,36 @@
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); },
setVolume: function (n) { vol = n / 10; if (master) master.gain.value = vol; }
};
})();

// 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 () {
Expand All @@ -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 = "";
Expand Down Expand Up @@ -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;
Expand Down
59 changes: 59 additions & 0 deletions assets/js/tunes.js
Original file line number Diff line number Diff line change
@@ -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);
5 changes: 4 additions & 1 deletion layouts/partials/console.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
</div>
</section>

{{ $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 }}
<script src="{{ $js.RelPermalink }}" defer></script>