/* HypnoMusic -- the shared background-music engine (game + session + antechamber). * * WHY THIS EXISTS: the old beds were two hardcoded patterns. strokegame.js played a fixed * 124 BPM kick/hat/bass loop with no harmony and no variation -- byte-identical every * session -- and bed.js played brown noise plus a 6 Hz theta tone, which is masking, not * music. Neither had any notion of "this session should sound different from the last one". * * This module replaces both music layers with ONE hybrid engine: * * 1. BANK layer -- pre-rendered AI clips (audiogen/build_musicbank.py -> /musicbank.json), * shuffled and equal-power crossfaded. Real instruments, real production; * variety comes from having many clips per mood and never repeating an * order. Crossfading BETWEEN different clips also sidesteps seamless-loop * hunting entirely -- the transition is the feature. * 2. PROCEDURAL -- a seeded generative synth: key, mode, tempo and chord progression are * layer rolled per session, and the arrangement moves through sections * (intro / rise / peak / breakdown) so it never settles into a loop. * * They are not alternatives, they compose. With a bank present the procedural engine drops to * "texture" (pads and swells only, no drums) and sits UNDER the clips, so the rendered audio * is the music and the synth is the glue that makes clip changes feel arranged rather than * shuffled. With no bank rendered it runs "full" and is the whole bed on its own -- so a fresh * checkout with an empty musicbank still sounds better than what it replaced, and nothing here * depends on a GPU render having happened. * * Everything else that made the old beds work is preserved: * - the binaural/isochronic entrainment tone stays in bed.js, phase-locked and untouched; * this module deliberately owns no entrainment (mixing a beat frequency into evolving * harmony would smear it). * - the bed must sit FAR under the voice. That is enforced two ways: a low absolute level, * and DUCKING -- duck()/attachVoice() pull the whole music bus down whenever the Dom is * actually speaking. Ducking is what lets the music be more present than the old flat * MUSIC_VOL = 0.10 without ever competing with the TTS. * * Signal graph: * * bank clips --> bankBus ---\ * drums/bass/pad/arp -> dryBus +--> musicBus -> duckGain -> masterFade -> destination * \-> reverb -/ * * duckGain -- voice ducking ONLY (1 = nobody speaking). * masterFade -- start/pause/stop envelope ONLY (0..1). * musicBus -- the user-facing volume (setVolume). * Keeping those three separate is what stops a pause from being undone by a duck release, or * a volume slider from cancelling a fade -- the old bed collapsed all of this onto one gain * and every automation fought the others. * * API: * HypnoMusic.start(opts) -- opts: {ctx, destination, mood, seed, volume, bank, proc, fadeIn} * fadeIn (seconds) lengthens the start envelope; it always * rises from 0, never from the current level. * Call from a user gesture. Idempotent; a second call with a * different mood just re-moods. * HypnoMusic.stop() -- fade out and tear down. * HypnoMusic.pause() / resume() * HypnoMusic.setVolume(0..1) -- perceived-loudness curve applied internally. * HypnoMusic.getVolume() * HypnoMusic.setMood(name) -- crossfades key/tempo/arrangement AND the bank selection. * HypnoMusic.getMood() * HypnoMusic.duck(depth, ms) -- depth 0..1 (1 = untouched, 0.25 = well out of the way). * HypnoMusic.unduck(ms) * HypnoMusic.attachVoice(el) -- auto-duck for the lifetime of an