/* Ambient masking bed -- under-voice texture to smooth TTS artifacts. * * Distinct from the antechamber wait-loop (loop.js): that bed is her SPOKEN voice * during the synth wait, and it FADES OUT at handoff so two voices never overlap. * THIS bed is non-vocal texture (generated brown noise + an optional theta tone) that * deliberately KEEPS PLAYING UNDER the real session voice. A continuous, low, * spectrally-warm bed reduces how critically the ear parses the voice, masking the * slight timbre error and -- crucially -- filling the bare silences at the `...` * long-pause seams (TTS_PROSODY_CONTRACT.md), where TTS artifacts are most exposed. * * It must sit FAR under the voice -- a wash you barely notice, not a second instrument. * * Everything is synthesized in-browser via the Web Audio API: zero audio assets, fully * runtime-tunable, and the binaural/isochronic generator needs no files at all. * * Signal graph -- the noise bed and the tone are INDEPENDENT, each with its own volume * bus, so they can be balanced separately (each has its own slider): * * noiseSrc -> lowpass --------> busBed (bed slider) ----\ * ent oscillators -> entSum --> busEnt (tone slider) ---+--> masterFade -> output * * masterFade is ONLY the start/pause/stop envelope (0..1); it never sets a layer's level. * * Four layers: * 1. brown-noise bed -- looped buffer through a lowpass that slowly "breathes" * (an LFO sweeps the cutoff) so the wash is never static. * 2. binaural beat -- two detuned sines panned hard L/R; the brain hears the * difference frequency (theta ~6 Hz). HEADPHONES ONLY. * 3. isochronic tone -- one sine gated on/off at the beat rate; works on SPEAKERS. * 4. MUSIC -- HypnoMusic (musicbed.js), sharing THIS AudioContext. Added * because layers 1-3 are masking, not music: they smooth the TTS * but they are the same wash in every session forever. The music * layer is what actually varies -- AI-rendered beds when a bank has * been built, a seeded generative synth when it has not -- and it * DUCKS under her voice, which the noise bed deliberately does not * (the noise has to keep filling the `...` pause seams; music * sitting at full level through a pause would draw the ear instead). * It has its own slider and starts at a low default: on for the * variety, quiet enough that nothing about layers 1-3 changes. * The theta tone is ON by default (binaural), and eases in VERY slowly so its arrival is * never noticed -- it plays under both the antechamber loop and the session voice. * * Volume model: each slider is 0..1; perceived loudness is ~quadratic, so a bus gain is * v*v*MAX. That gives fine control down at the low, subtle end where these live. * * Transition cue: HypnoBed.cue() plays a soft synthesized bell-swell -- fired at the * antechamber->session handoff to gently pull attention to the session beginning. * * Lifecycle: * HypnoBed.start({fadeIn}) -- from the "Voice my session" click (the gesture that * unblocks audio). Builds the graph, eases in over a * long fade, reveals the control bar. Idempotent. The * rise ALWAYS begins at 0 (never at the current level), * on a quadratic curve, and only once the context runs. * HypnoBed.toggle() -- pause/resume the whole bed from the bar button. * HypnoBed.setEntrainment(mode) -- "off" | "binaural" | "isochronic" (slow fade either way). * HypnoBed.setVolume(0..1) -- bed (noise) level. Ramped fast; click-free. * HypnoBed.setEntVolume(0..1) -- tone level, INDEPENDENT of the bed. * HypnoBed.setMusicVolume(0..1) -- music level, INDEPENDENT of both. 0 = no music at all. * HypnoBed.setMusicMood(id) -- crossfade the music to another mood. * HypnoBed.duckFor(el) -- duck the music under an