// audio.jsx — synthesized ambient soundscapes + premium UI sound design (Web Audio API)

class AmbientEngine {
  constructor() {
    this.ctx = null;
    this.master = null;     // ambient bus (affected by volume slider)
    this.ui = null;         // UI/effects bus (fixed, tasteful level)
    this.verb = null;       // shared reverb send
    this.nodes = [];
    this.timers = [];
    this.current = null;
    this.volume = 0.6;
    this._buffers = {};
  }

  _ensure() {
    if (this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = this.volume;
    this.master.connect(this.ctx.destination);
    this.ui = this.ctx.createGain();
    this.ui.gain.value = 0.9;
    this.ui.connect(this.ctx.destination);
    // lightweight algorithmic reverb (synthesized impulse) for an atmospheric tail
    this.verb = this.ctx.createConvolver();
    this.verb.buffer = this._impulse(2.6, 2.4);
    const vg = this.ctx.createGain(); vg.gain.value = 0.5;
    this.verb.connect(vg); vg.connect(this.ctx.destination);
  }

  _impulse(seconds, decay) {
    const ctx = this.ctx, rate = ctx.sampleRate, len = rate * seconds;
    const buf = ctx.createBuffer(2, len, rate);
    for (let ch = 0; ch < 2; ch++) {
      const d = buf.getChannelData(ch);
      for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay);
    }
    return buf;
  }

  _resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }

  _noise(type = 'white') {
    if (this._buffers[type]) return this._buffers[type];
    const ctx = this.ctx;
    const len = ctx.sampleRate * 4;
    const buf = ctx.createBuffer(1, len, ctx.sampleRate);
    const d = buf.getChannelData(0);
    if (type === 'brown') {
      let last = 0;
      for (let i = 0; i < len; i++) {
        const w = Math.random() * 2 - 1;
        last = (last + 0.02 * w) / 1.02;
        d[i] = last * 3.2;
      }
    } else if (type === 'pink') {
      let b0 = 0, b1 = 0, b2 = 0;
      for (let i = 0; i < len; i++) {
        const w = Math.random() * 2 - 1;
        b0 = 0.99765 * b0 + w * 0.0990460;
        b1 = 0.96300 * b1 + w * 0.2965164;
        b2 = 0.57000 * b2 + w * 1.0526913;
        d[i] = (b0 + b1 + b2 + w * 0.1848) * 0.25;
      }
    } else {
      for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
    }
    this._buffers[type] = buf;
    return buf;
  }

  _src(type) {
    const s = this.ctx.createBufferSource();
    s.buffer = this._noise(type);
    s.loop = true;
    s.playbackRate.value = 0.85 + Math.random() * 0.3;
    s.start();
    this.nodes.push(s);
    return s;
  }

  _g(v) { const g = this.ctx.createGain(); g.gain.value = v; this.nodes.push(g); return g; }
  _f(type, freq, q) {
    const f = this.ctx.createBiquadFilter();
    f.type = type; f.frequency.value = freq; if (q != null) f.Q.value = q;
    this.nodes.push(f); return f;
  }
  _lfo(rate, depth, target, base) {
    const o = this.ctx.createOscillator();
    o.frequency.value = rate;
    const g = this.ctx.createGain();
    g.gain.value = depth;
    o.connect(g); g.connect(target);
    if (base != null) target.value = base;
    o.start();
    this.nodes.push(o); this.nodes.push(g);
  }

  setVolume(v) {
    this.volume = v;
    if (this.master) {
      const t = this.ctx.currentTime;
      this.master.gain.cancelScheduledValues(t);
      this.master.gain.setTargetAtTime(v, t, 0.05);
    }
  }

  play(kind) {
    this._ensure();
    this._resume();
    this.stop();
    this.current = kind;
    const ctx = this.ctx;
    const out = this._g(0);
    out.connect(this.master);
    out.gain.setValueAtTime(0, ctx.currentTime);
    out.gain.linearRampToValueAtTime(1, ctx.currentTime + 0.38);   // snappy, near-instant switching
    const build = this[`_${kind}`];
    if (build) build.call(this, out);
    return true;
  }

  // ── ambient soundscapes ─────────────────────────────────
  _rain(out) {
    // broadband shhh
    const s = this._src('white');
    const hp = this._f('highpass', 900, 0.4);
    const lp = this._f('lowpass', 8500, 0.3);
    const g = this._g(0.34);
    s.connect(hp); hp.connect(lp); lp.connect(g); g.connect(out);
    // mid body
    const s2 = this._src('pink');
    const lp2 = this._f('lowpass', 2600, 0.5);
    const g2 = this._g(0.22);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    // low distant rumble
    const s3 = this._src('brown');
    const lp3 = this._f('lowpass', 320, 0.6);
    const g3 = this._g(0.16);
    s3.connect(lp3); lp3.connect(g3); g3.connect(out);
    this._lfo(0.11, 0.10, g.gain, 0.34);   // gentle intensity drift
    this._scheduleDroplets(out);
  }
  _ocean(out) {
    // deep water bed
    const s = this._src('brown');
    const lp = this._f('lowpass', 520, 0.5);
    const g = this._g(0.42);
    s.connect(lp); lp.connect(g); g.connect(out);
    this._lfo(0.07, 0.30, g.gain, 0.4);
    this._scheduleWaves(out);   // realistic breaking-wave washes
  }
  _forest(out) {
    // soft wind-through-leaves bed
    const s = this._src('pink');
    const lp = this._f('lowpass', 1400, 0.5);
    const g = this._g(0.18);
    s.connect(lp); lp.connect(g); g.connect(out);
    this._lfo(0.06, 0.07, g.gain, 0.18);
    this._lfo(0.05, 260, lp.frequency, 1400);
    this._scheduleBirds(out);
    this._scheduleRustle(out);
  }
  _wind(out) {
    const s = this._src('brown');
    const lp = this._f('lowpass', 480, 1.0);
    const g = this._g(0.4);
    s.connect(lp); lp.connect(g); g.connect(out);
    // resonant howl
    const s2 = this._src('white');
    const bp = this._f('bandpass', 620, 6);
    const g2 = this._g(0.05);
    s2.connect(bp); bp.connect(g2); g2.connect(out);
    this._lfo(0.06, 360, lp.frequency, 520);
    this._lfo(0.04, 0.26, g.gain, 0.4);
    this._lfo(0.08, 220, bp.frequency, 640);
    this._scheduleGusts(out, lp, g, bp);
  }
  _night(out) {
    // low nocturnal drone
    const s = this._src('brown');
    const lp = this._f('lowpass', 240, 0.7);
    const g = this._g(0.2);
    s.connect(lp); lp.connect(g); g.connect(out);
    this._lfo(0.04, 0.06, g.gain, 0.2);
    this._scheduleCrickets(out);
    this._scheduleOwl(out);
  }
  _fire(out) {
    const s = this._src('brown');
    const lp = this._f('lowpass', 420, 0.7);
    const g = this._g(0.46);
    s.connect(lp); lp.connect(g); g.connect(out);
    this._lfo(0.3, 0.14, g.gain, 0.46);
    this._scheduleCrackle(out);
  }

  // ── scheduled randomized events ─────────────────────────
  _scheduleDroplets(out, key = 'rain') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const burst = ctx.createBufferSource();
      burst.buffer = this._noise('white');
      const bp = ctx.createBiquadFilter();
      bp.type = 'bandpass'; bp.frequency.value = 1800 + Math.random() * 3200; bp.Q.value = 6;
      const g = ctx.createGain(); g.gain.value = 0;
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(0.04 + Math.random() * 0.05, t + 0.003);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.04 + Math.random() * 0.05);
      burst.connect(bp); bp.connect(g); g.connect(out);
      burst.start(t); burst.stop(t + 0.2);
      this.timers.push(setTimeout(tick, 60 + Math.random() * 220));
    };
    this.timers.push(setTimeout(tick, 150));
  }
  _scheduleWaves(out, key = 'ocean') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const wash = ctx.createBufferSource();
      wash.buffer = this._noise('white');
      wash.loop = true;
      const lp = ctx.createBiquadFilter(); lp.type = 'lowpass';
      const hp = ctx.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 350;
      const g = ctx.createGain(); g.gain.value = 0;
      const rise = 1.1 + Math.random() * 1.0;     // wave breaks
      const fall = 2.4 + Math.random() * 2.0;     // foam recedes
      const peak = 0.20 + Math.random() * 0.12;
      // lowpass sweeps up as the wave crests, then closes as it recedes
      lp.frequency.setValueAtTime(500, t);
      lp.frequency.linearRampToValueAtTime(3200, t + rise);
      lp.frequency.linearRampToValueAtTime(700, t + rise + fall);
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(peak, t + rise);
      g.gain.exponentialRampToValueAtTime(0.0001, t + rise + fall);
      wash.connect(hp); hp.connect(lp); lp.connect(g); g.connect(out);
      wash.start(t); wash.stop(t + rise + fall + 0.3);
      this.timers.push(setTimeout(tick, (rise + fall) * 1000 * (0.55 + Math.random() * 0.4)));
    };
    this.timers.push(setTimeout(tick, 400));
  }
  _scheduleBirds(out, key = 'forest') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const base = 1900 + Math.random() * 2400;
      const o = ctx.createOscillator(); o.type = 'sine';
      const g = ctx.createGain();
      const reps = 2 + Math.floor(Math.random() * 3);
      for (let i = 0; i < reps; i++) {
        const tt = t + i * 0.13;
        o.frequency.setValueAtTime(base, tt);
        o.frequency.linearRampToValueAtTime(base * 1.25, tt + 0.05);
        o.frequency.linearRampToValueAtTime(base, tt + 0.1);
        g.gain.setValueAtTime(0, tt);
        g.gain.linearRampToValueAtTime(0.06, tt + 0.02);
        g.gain.linearRampToValueAtTime(0, tt + 0.11);
      }
      o.connect(g); g.connect(out);
      o.start(t); o.stop(t + reps * 0.13 + 0.2);
      this.timers.push(setTimeout(tick, 2200 + Math.random() * 5000));
    };
    this.timers.push(setTimeout(tick, 1400 + Math.random() * 2200));
  }
  _scheduleRustle(out, key = 'forest') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const s = ctx.createBufferSource(); s.buffer = this._noise('pink');
      const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 3200; bp.Q.value = 0.8;
      const g = ctx.createGain(); g.gain.value = 0;
      const dur = 0.5 + Math.random() * 0.8;
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(0.05, t + dur * 0.4);
      g.gain.linearRampToValueAtTime(0, t + dur);
      s.connect(bp); bp.connect(g); g.connect(out);
      s.start(t); s.stop(t + dur + 0.1);
      this.timers.push(setTimeout(tick, 3000 + Math.random() * 5000));
    };
    this.timers.push(setTimeout(tick, 2000));
  }
  _scheduleGusts(out, lp, g, bp, key = 'wind') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const dur = 2.0 + Math.random() * 2.5;
      lp.frequency.cancelScheduledValues(t);
      lp.frequency.setValueAtTime(lp.frequency.value, t);
      lp.frequency.linearRampToValueAtTime(900 + Math.random() * 500, t + dur * 0.45);
      lp.frequency.linearRampToValueAtTime(420, t + dur);
      bp.frequency.cancelScheduledValues(t);
      bp.frequency.setValueAtTime(bp.frequency.value, t);
      bp.frequency.linearRampToValueAtTime(820 + Math.random() * 260, t + dur * 0.45);
      bp.frequency.linearRampToValueAtTime(560, t + dur);
      this.timers.push(setTimeout(tick, dur * 1000 + Math.random() * 2500));
    };
    this.timers.push(setTimeout(tick, 1500));
  }
  _scheduleCrickets(out, key = 'night') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const o = ctx.createOscillator();
      o.type = 'square';
      o.frequency.value = 4200 + Math.random() * 700;
      const lp = ctx.createBiquadFilter();
      lp.type = 'lowpass'; lp.frequency.value = 6200;
      const g = ctx.createGain(); g.gain.value = 0;
      const n = 8 + Math.floor(Math.random() * 10);
      for (let i = 0; i < n; i++) {
        const tt = t + i * 0.03;
        g.gain.setValueAtTime(0, tt);
        g.gain.linearRampToValueAtTime(0.03, tt + 0.008);
        g.gain.linearRampToValueAtTime(0, tt + 0.02);
      }
      o.connect(lp); lp.connect(g); g.connect(out);
      o.start(t); o.stop(t + n * 0.03 + 0.1);
      this.timers.push(setTimeout(tick, 500 + Math.random() * 1100));
    };
    this.timers.push(setTimeout(tick, 400));
  }
  _scheduleOwl(out, key = 'night') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const hoot = (tt, f) => {
        const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
        const g = ctx.createGain(); g.gain.value = 0;
        g.gain.setValueAtTime(0, tt);
        g.gain.linearRampToValueAtTime(0.07, tt + 0.06);
        g.gain.linearRampToValueAtTime(0.05, tt + 0.18);
        g.gain.exponentialRampToValueAtTime(0.0001, tt + 0.45);
        o.connect(g); g.connect(out); if (this.verb) g.connect(this.verb);
        o.start(tt); o.stop(tt + 0.5);
      };
      const f = 360 + Math.random() * 60;
      hoot(t, f); hoot(t + 0.55, f * 0.96);
      this.timers.push(setTimeout(tick, 9000 + Math.random() * 14000));
    };
    this.timers.push(setTimeout(tick, 5000 + Math.random() * 6000));
  }
  _scheduleCrackle(out, key = 'fire') {
    const tick = () => {
      if (this.current !== key) return;
      const ctx = this.ctx, t = ctx.currentTime;
      const burst = ctx.createBufferSource();
      burst.buffer = this._noise('white');
      const bp = ctx.createBiquadFilter();
      bp.type = 'bandpass'; bp.frequency.value = 1200 + Math.random() * 2400; bp.Q.value = 2;
      const g = ctx.createGain(); g.gain.value = 0;
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(0.12 + Math.random() * 0.12, t + 0.004);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.05 + Math.random() * 0.06);
      burst.connect(bp); bp.connect(g); g.connect(out);
      burst.start(t); burst.stop(t + 0.2);
      this.timers.push(setTimeout(tick, 120 + Math.random() * 700));
    };
    this.timers.push(setTimeout(tick, 200));
  }

  // ── generic event scheduler & one-shot voices ───────────
  _sched(key, make, lo, hi) {
    const tick = () => {
      if (this.current !== key) return;
      make(this.ctx.currentTime);
      this.timers.push(setTimeout(tick, lo + Math.random() * (hi - lo)));
    };
    this.timers.push(setTimeout(tick, lo));
  }
  // filtered-noise transient (droplet, clink, page, clack…)
  _ping(out, t, { f = 2400, peak = 0.05, dur = 0.4, q = 8, type = 'bandpass', send = 0.25 } = {}) {
    const ctx = this.ctx;
    const burst = ctx.createBufferSource(); burst.buffer = this._noise('white');
    const bp = ctx.createBiquadFilter(); bp.type = type; bp.frequency.value = f; if (bp.Q) bp.Q.value = q;
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(peak, t + 0.004);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    burst.connect(bp); bp.connect(g); g.connect(out);
    if (this.verb && send) { const sg = ctx.createGain(); sg.gain.value = send; g.connect(sg); sg.connect(this.verb); }
    burst.start(t); burst.stop(t + dur + 0.1);
  }
  // pure-tone one-shot (tick, beep, shimmer)
  _tone(out, t, { f = 880, peak = 0.08, attack = 0.005, dur = 0.12, type = 'sine', send = 0 } = {}) {
    const ctx = this.ctx;
    const o = ctx.createOscillator(); o.type = type; o.frequency.value = f;
    const g = ctx.createGain(); g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(peak, t + attack);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g); g.connect(out);
    if (this.verb && send) { const sg = ctx.createGain(); sg.gain.value = send; g.connect(sg); sg.connect(this.verb); }
    o.start(t); o.stop(t + dur + 0.05);
  }
  // continuous low room tone
  _murmur(out, { lp = 320, g: gv = 0.18, src = 'brown', q = 0.6 } = {}) {
    const s = this._src(src);
    const f = this._f('lowpass', lp, q);
    const gain = this._g(gv);
    s.connect(f); f.connect(gain); gain.connect(out);
    this._lfo(0.08, gv * 0.4, gain.gain, gv);
    return gain;
  }
  // voice-like babble band
  _babble(out, { freq = 520, g: gv = 0.1, q = 1.2 } = {}) {
    const s = this._src('pink');
    const bp = this._f('bandpass', freq, q);
    const gain = this._g(gv);
    s.connect(bp); bp.connect(gain); gain.connect(out);
    this._lfo(0.6, gv * 0.7, gain.gain, gv * 0.6);
    this._lfo(0.13, 180, bp.frequency, freq);
  }
  _droplets(out, key) {
    this._sched(key, (t) => this._ping(out, t, {
      f: 1700 + Math.random() * 3200, peak: 0.035 + Math.random() * 0.045,
      dur: 0.06 + Math.random() * 0.05, q: 8,
    }), 50, 230);
  }

  // ── additional white-noise environments ─────────────────
  _coffee(out) {
    this._murmur(out, { lp: 360, g: 0.2 });
    this._babble(out, { freq: 480, g: 0.12 });
    this._babble(out, { freq: 760, g: 0.06, q: 1.6 });
    this._sched('coffee', (t) => this._ping(out, t, { f: 2200 + Math.random() * 2800, peak: 0.04, dur: 0.5, q: 11 }), 1400, 5200);
  }
  _classroom(out) {
    this._murmur(out, { lp: 440, g: 0.15 });
    this._babble(out, { freq: 600, g: 0.1 });
    this._sched('classroom', (t) => this._ping(out, t, { f: 800 + Math.random() * 1500, peak: 0.05, dur: 0.32, q: 3 }), 2400, 6500);
  }
  _library(out) {
    this._murmur(out, { lp: 240, g: 0.07, src: 'pink' });
    this._sched('library', (t) => this._ping(out, t, { f: 1500 + Math.random() * 1900, peak: 0.03, dur: 0.4, q: 1.5 }), 4500, 12000);
    this._sched('library', (t) => this._tone(out, t, { f: 150 + Math.random() * 60, peak: 0.03, dur: 0.5 }), 9000, 20000);
  }
  _stream(out) {
    const s = this._src('white');
    const hp = this._f('highpass', 1100, 0.4);
    const lp = this._f('lowpass', 7200, 0.3);
    const g = this._g(0.22);
    s.connect(hp); hp.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('pink');
    const lp2 = this._f('lowpass', 1700, 0.5);
    const g2 = this._g(0.16);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    this._lfo(0.2, 0.06, g.gain, 0.22);
    this._sched('stream', (t) => this._ping(out, t, { f: 1400 + Math.random() * 2800, peak: 0.03, dur: 0.1, q: 14 }), 45, 200);
  }
  _train(out) {
    const s = this._src('brown');
    const lp = this._f('lowpass', 210, 0.6);
    const g = this._g(0.4);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('white');
    const hp = this._f('highpass', 2600, 0.5);
    const g2 = this._g(0.05);
    s2.connect(hp); hp.connect(g2); g2.connect(out);
    this._lfo(0.08, 0.12, g.gain, 0.4);
    // rhythmic bogie clack (pairs)
    this._sched('train', (t) => {
      this._ping(out, t,        { f: 110, peak: 0.13, dur: 0.08, q: 1.2, type: 'lowpass', send: 0 });
      this._ping(out, t + 0.15, { f: 140, peak: 0.1,  dur: 0.08, q: 1.2, type: 'lowpass', send: 0 });
    }, 640, 760);
  }
  _whitenoise(out) {
    const s = this._src('white');
    const lp = this._f('lowpass', 11000, 0.3);
    const g = this._g(0.42);
    s.connect(lp); lp.connect(g); g.connect(out);
  }
  _brownnoise(out) {
    const s = this._src('brown');
    const lp = this._f('lowpass', 1500, 0.4);
    const g = this._g(0.5);
    s.connect(lp); lp.connect(g); g.connect(out);
  }
  _pinknoise(out) {
    const s = this._src('pink');
    const g = this._g(0.44);
    s.connect(g); g.connect(out);
  }
  _thunder(out) {
    // rain bed
    const s = this._src('white');
    const hp = this._f('highpass', 900, 0.4);
    const lp = this._f('lowpass', 8000, 0.3);
    const g = this._g(0.3);
    s.connect(hp); hp.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('brown');
    const lp2 = this._f('lowpass', 300, 0.6);
    const g2 = this._g(0.16);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    this._droplets(out, 'thunder');
    // distant rolling thunder
    this._sched('thunder', (t) => {
      const ctx = this.ctx, dur = 2.6 + Math.random() * 2.6;
      const n = ctx.createBufferSource(); n.buffer = this._noise('brown'); n.loop = true;
      const lpf = ctx.createBiquadFilter(); lpf.type = 'lowpass';
      lpf.frequency.setValueAtTime(130, t); lpf.frequency.linearRampToValueAtTime(55, t + dur);
      const gg = ctx.createGain(); gg.gain.setValueAtTime(0, t);
      gg.gain.linearRampToValueAtTime(0.5, t + 0.3 + Math.random() * 0.5);
      gg.gain.linearRampToValueAtTime(0.3, t + dur * 0.5);
      gg.gain.exponentialRampToValueAtTime(0.0001, t + dur);
      n.connect(lpf); lpf.connect(gg); gg.connect(out);
      if (this.verb) { const sg = ctx.createGain(); sg.gain.value = 0.6; gg.connect(sg); sg.connect(this.verb); }
      n.start(t); n.stop(t + dur + 0.2);
    }, 9000, 21000);
  }
  _mountainwind(out) {
    const s = this._src('brown');
    const lp = this._f('lowpass', 560, 1.0);
    const g = this._g(0.42);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('white');
    const bp = this._f('bandpass', 820, 8);
    const g2 = this._g(0.08);
    s2.connect(bp); bp.connect(g2); g2.connect(out);
    this._lfo(0.05, 420, lp.frequency, 560);
    this._lfo(0.035, 0.28, g.gain, 0.42);
    this._lfo(0.07, 280, bp.frequency, 860);
    this._scheduleGusts(out, lp, g, bp, 'mountainwind');
  }
  _clock(out) {
    this._murmur(out, { lp: 170, g: 0.04, src: 'pink' });
    let tock = false;
    this._sched('clock', (t) => { this._ping(out, t, { f: tock ? 2300 : 2700, peak: 0.13, dur: 0.045, q: 6, send: 0.15 }); tock = !tock; }, 1000, 1000);
  }
  _countdown(out) {
    this._murmur(out, { lp: 200, g: 0.035, src: 'pink' });
    this._sched('countdown', (t) => this._tone(out, t, { f: 880, peak: 0.09, dur: 0.11, send: 0.2 }), 1000, 1000);
  }
  _deepspace(out) {
    const ctx = this.ctx;
    [55, 55.4, 82.5].forEach((f, i) => {
      const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = f;
      const gain = this._g(0.12 - i * 0.025);
      o.connect(gain); gain.connect(out);
      this._lfo(0.03 + 0.01 * i, 0.04, gain.gain, 0.1 - i * 0.025);
      o.start(); this.nodes.push(o);
    });
    const s = this._src('pink');
    const bp = this._f('bandpass', 480, 0.8);
    const g2 = this._g(0.05);
    s.connect(bp); bp.connect(g2); g2.connect(out);
    this._lfo(0.02, 300, bp.frequency, 500);
    const shimmer = [523.25, 659.25, 783.99, 987.77];
    this._sched('deepspace', (t) => this._tone(out, t, {
      f: shimmer[Math.floor(Math.random() * shimmer.length)], peak: 0.05, attack: 0.4, dur: 2.4, send: 0.85,
    }), 4000, 11000);
  }
  _nightforest(out) {
    const s = this._src('pink');
    const lp = this._f('lowpass', 900, 0.5);
    const g = this._g(0.14);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('brown');
    const lp2 = this._f('lowpass', 220, 0.7);
    const g2 = this._g(0.16);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    this._lfo(0.04, 0.06, g.gain, 0.14);
    this._scheduleCrickets(out, 'nightforest');
    this._scheduleOwl(out, 'nightforest');
    this._scheduleRustle(out, 'nightforest');
  }

  _campfire(out) {
    // cosy, slightly deeper than Fireplace, with a warm air bed
    const s = this._src('brown');
    const lp = this._f('lowpass', 360, 0.7);
    const g = this._g(0.5);
    s.connect(lp); lp.connect(g); g.connect(out);
    this._lfo(0.26, 0.16, g.gain, 0.5);
    const tick = () => {
      if (this.current !== 'campfire') return;
      this._ping(out, this.ctx.currentTime, { f: 900 + Math.random() * 2200, peak: 0.13 + Math.random() * 0.13, dur: 0.05 + Math.random() * 0.07, q: 2, type: 'bandpass', send: 0.12 });
      this.timers.push(setTimeout(tick, 90 + Math.random() * 620));
    };
    this.timers.push(setTimeout(tick, 200));
  }
  _desertwind(out) {
    // dry, open expanse: filtered brown bed + sparse high sand-hiss whistle
    const s = this._src('brown');
    const lp = this._f('lowpass', 600, 0.9);
    const g = this._g(0.4);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('white');
    const bp = this._f('bandpass', 1500, 4);
    const g2 = this._g(0.045);
    s2.connect(bp); bp.connect(g2); g2.connect(out);
    this._lfo(0.045, 500, lp.frequency, 600);
    this._lfo(0.03, 0.24, g.gain, 0.4);
    this._lfo(0.06, 520, bp.frequency, 1500);
    this._scheduleGusts(out, lp, g, bp, 'desertwind');
  }
  _jungle(out) {
    // humid, alive: warm leaf bed + dense birds, crickets, rustle
    const s = this._src('pink');
    const lp = this._f('lowpass', 1600, 0.5);
    const g = this._g(0.18);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('brown');
    const lp2 = this._f('lowpass', 260, 0.7);
    const g2 = this._g(0.14);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    this._lfo(0.05, 0.07, g.gain, 0.18);
    this._scheduleBirds(out, 'jungle');
    this._scheduleCrickets(out, 'jungle');
    this._scheduleRustle(out, 'jungle');
    // occasional distant whoop
    this._sched('jungle', (t) => this._tone(out, t, { f: 300 + Math.random() * 120, peak: 0.06, attack: 0.05, dur: 0.4, send: 0.7 }), 7000, 16000);
  }
  _waterfall(out) {
    // powerful falling water: broadband white + low cavern rumble
    const s = this._src('white');
    const hp = this._f('highpass', 700, 0.4);
    const lp = this._f('lowpass', 9000, 0.3);
    const g = this._g(0.4);
    s.connect(hp); hp.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('brown');
    const lp2 = this._f('lowpass', 380, 0.7);
    const g2 = this._g(0.34);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    this._lfo(0.16, 0.07, g.gain, 0.4);
    this._sched('waterfall', (t) => this._ping(out, t, { f: 1200 + Math.random() * 3200, peak: 0.035, dur: 0.09, q: 9 }), 40, 150);
  }
  _snowstorm(out) {
    // muffled blizzard: soft white hiss + low howling wind, everything dampened
    const s = this._src('white');
    const lp = this._f('lowpass', 2600, 0.4);
    const g = this._g(0.3);
    s.connect(lp); lp.connect(g); g.connect(out);
    const s2 = this._src('brown');
    const lp2 = this._f('lowpass', 360, 1.0);
    const g2 = this._g(0.34);
    s2.connect(lp2); lp2.connect(g2); g2.connect(out);
    const s3 = this._src('white');
    const bp = this._f('bandpass', 540, 7);
    const g3 = this._g(0.05);
    s3.connect(bp); bp.connect(g3); g3.connect(out);
    this._lfo(0.05, 0.2, g.gain, 0.3);
    this._lfo(0.04, 280, lp2.frequency, 360);
    this._lfo(0.07, 200, bp.frequency, 560);
    this._scheduleGusts(out, lp2, g2, bp, 'snowstorm');
  }

  stop() {
    this.current = null;
    this.timers.forEach(clearTimeout);
    this.timers = [];
    if (this.ctx) {
      const t = this.ctx.currentTime;
      this.nodes.forEach((n) => {
        try {
          if (n.stop) { try { n.gain && n.gain.cancelScheduledValues(t); } catch (e) {} n.stop(t + 0.18); }
          else if (n.disconnect) setTimeout(() => { try { n.disconnect(); } catch (e) {} }, 260);
        } catch (e) {}
      });
    }
    this.nodes = [];
  }

  // ── premium UI sound design ─────────────────────────────
  // A soft sine voice with slow attack + long release, optional reverb send.
  _voice(f, t0, { type = 'sine', attack = 0.04, hold = 0.1, release = 1.2, peak = 0.18, send = 0.5, glide = 0 } = {}) {
    const ctx = this.ctx;
    const o = ctx.createOscillator(); o.type = type; o.frequency.setValueAtTime(f, t0);
    if (glide) o.frequency.linearRampToValueAtTime(f * glide, t0 + attack + hold + release);
    const g = ctx.createGain(); g.gain.value = 0;
    g.gain.setValueAtTime(0, t0);
    g.gain.linearRampToValueAtTime(peak, t0 + attack);
    g.gain.setValueAtTime(peak, t0 + attack + hold);
    g.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + hold + release);
    o.connect(g); g.connect(this.ui);
    if (this.verb && send) { const sg = ctx.createGain(); sg.gain.value = send; g.connect(sg); sg.connect(this.verb); }
    o.start(t0); o.stop(t0 + attack + hold + release + 0.1);
  }

  // Filtered-noise swell — the "breath" under activation/level events.
  _swell(t0, { from = 200, to = 2400, dur = 1.3, peak = 0.10, q = 0.6 } = {}) {
    const ctx = this.ctx;
    const s = ctx.createBufferSource(); s.buffer = this._noise('pink'); s.loop = true;
    const bp = ctx.createBiquadFilter(); bp.type = 'bandpass'; bp.Q.value = q;
    bp.frequency.setValueAtTime(from, t0);
    bp.frequency.exponentialRampToValueAtTime(to, t0 + dur);
    const g = ctx.createGain(); g.gain.value = 0;
    g.gain.setValueAtTime(0, t0);
    g.gain.linearRampToValueAtTime(peak, t0 + dur * 0.6);
    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur + 0.5);
    s.connect(bp); bp.connect(g); g.connect(this.ui);
    if (this.verb) { const sg = ctx.createGain(); sg.gain.value = 0.6; g.connect(sg); sg.connect(this.verb); }
    s.start(t0); s.stop(t0 + dur + 0.8);
  }

  // A short, tactile UI click — a filtered-noise transient with an optional very brief
  // damped resonant body. Percussive and non-melodic: a glass tap, never a note.
  _click(t, { f = 2200, q = 5, peak = 0.16, dur = 0.03, type = 'bandpass', body = 0, bodyPeak = 0.06, send = 0.03 } = {}) {
    const ctx = this.ctx;
    const n = ctx.createBufferSource(); n.buffer = this._noise('white');
    const bp = ctx.createBiquadFilter(); bp.type = type; bp.frequency.value = f; bp.Q.value = q;
    const g = ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(peak, t + 0.0015);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    n.connect(bp); bp.connect(g); g.connect(this.ui);
    if (this.verb && send) { const sg = ctx.createGain(); sg.gain.value = send; g.connect(sg); sg.connect(this.verb); }
    n.start(t); n.stop(t + dur + 0.02);
    if (body) {
      const o = ctx.createOscillator(); o.type = 'sine'; o.frequency.value = body;
      const og = ctx.createGain();
      og.gain.setValueAtTime(0.0001, t);
      og.gain.exponentialRampToValueAtTime(bodyPeak, t + 0.004);
      og.gain.exponentialRampToValueAtTime(0.0001, t + Math.max(dur * 2, 0.05));
      o.connect(og); og.connect(this.ui);
      if (this.verb && send) { const sg = ctx.createGain(); sg.gain.value = send * 1.4; og.connect(sg); sg.connect(this.verb); }
      o.start(t); o.stop(t + Math.max(dur * 2.4, 0.07));
    }
  }

  // Public: subtle, premium, NON-musical tactile cues — soft clicks / glass taps in one
  // cohesive family. Each is very short micro-feedback; none resembles a tone or melody.
  cue(kind, level) {
    this._ensure();
    this._resume();
    const t = this.ctx.currentTime;
    switch (kind) {
      case 'tap':          // generic button / tab / settings tick — barely there
        this._click(t, { f: 2600, q: 4, peak: 0.075, dur: 0.014, send: 0 });
        break;
      case 'start':        // play — a crisp two-stage engage
        this._click(t,         { f: 1700, q: 3, peak: 0.16, dur: 0.024, body: 280, bodyPeak: 0.07 });
        this._click(t + 0.035, { f: 2700, q: 4, peak: 0.12, dur: 0.02 });
        break;
      case 'pause':        // a single soft, damped click
        this._click(t, { f: 1300, q: 3, peak: 0.13, dur: 0.03, body: 200, bodyPeak: 0.06 });
        break;
      case 'resume':       // a light, slightly brighter single click
        this._click(t, { f: 2100, q: 4, peak: 0.13, dur: 0.022, body: 320, bodyPeak: 0.06 });
        break;
      case 'stop':         // reset / end — a low, grounded soft thunk
        this._click(t, { f: 820, q: 2.2, peak: 0.15, dur: 0.04, type: 'lowpass', body: 150, bodyPeak: 0.07 });
        break;
      case 'complete':     // goal complete — a satisfying refined double glass-tap
        this._click(t,         { f: 2500, q: 6, peak: 0.15, dur: 0.026, body: 560, bodyPeak: 0.08, send: 0.06 });
        this._click(t + 0.07,  { f: 3300, q: 7, peak: 0.12, dur: 0.03,  body: 760, bodyPeak: 0.055, send: 0.06 });
        break;
      case 'level':        // rank unlock — three quick ascending ticks, glassy and brief
        this._click(t,         { f: 1900, q: 4, peak: 0.13, dur: 0.022, body: 420, bodyPeak: 0.07, send: 0.05 });
        this._click(t + 0.06,  { f: 2600, q: 5, peak: 0.13, dur: 0.022, body: 560, bodyPeak: 0.06, send: 0.05 });
        this._click(t + 0.12,  { f: 3400, q: 6, peak: 0.12, dur: 0.026, body: 720, bodyPeak: 0.05, send: 0.06 });
        break;
      case 'achievement':  // subscription / achievement — a warm, satisfying double tap
        this._click(t,         { f: 1600, q: 3, peak: 0.16, dur: 0.03, body: 360, bodyPeak: 0.09, send: 0.06 });
        this._click(t + 0.075, { f: 2400, q: 5, peak: 0.13, dur: 0.03, body: 560, bodyPeak: 0.06, send: 0.06 });
        break;
      default: break;
    }
  }
}

window.ambientEngine = window.ambientEngine || new AmbientEngine();

// ── WhiteNoisePlayer ───────────────────────────────────────
// Plays the bundled ambient mp3 files with truly gapless, click-free looping.
// decodeAudioData gives a sample-accurate buffer; an equal-power crossfade at the
// seam removes any waveform discontinuity (no click/pop), and the overlap removes
// any silent gap — so a sound can play unbroken for an unlimited session.
class WhiteNoisePlayer {
  constructor() {
    this.ctx = null;
    this.master = null;
    this.buffers = {};      // url -> AudioBuffer
    this.loading = {};      // url -> Promise<AudioBuffer>
    this.voices = [];       // live { src, g } scheduled for the crossfade chain
    this.timers = [];
    this.current = null;    // active sound key
    this.volume = 0.6;
    this._token = 0;        // guards async play() races
    this._usedSynth = false; // true when we fell back to the synthesized soundscape
  }
  _ensure() {
    if (this.ctx) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = this.volume;
    this.master.connect(this.ctx.destination);
  }
  _resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }
  _load(url) {
    if (this.buffers[url]) return Promise.resolve(this.buffers[url]);
    if (this.loading[url]) return this.loading[url];
    const p = fetch(url)
      .then((r) => { if (!r.ok) throw new Error('http ' + r.status); return r.arrayBuffer(); })
      .then((arr) => new Promise((res, rej) => this.ctx.decodeAudioData(arr, res, rej)))
      .then((buf) => { this.buffers[url] = buf; delete this.loading[url]; return buf; })
      .catch((e) => { delete this.loading[url]; throw e; });
    this.loading[url] = p;
    return p;
  }
  // Preload + decode every bundled ambient file at app start so a later tap on a
  // sound plays instantly from an in-memory AudioBuffer (zero fetch/decode latency).
  preloadAll(list) {
    try { this._ensure(); } catch (e) { return; }
    (list || []).forEach((s) => { if (s && s.file) this._load(s.file).catch(() => {}); });
  }
  setVolume(v) {
    this.volume = v;
    if (this.master) {
      const t = this.ctx.currentTime;
      this.master.gain.cancelScheduledValues(t);
      this.master.gain.setTargetAtTime(v, t, 0.05);
    }
    if (this._usedSynth) { try { window.ambientEngine.setVolume(v); } catch (e) {} }
  }
  play(key, url) {
    this._ensure();
    this._resume();
    const token = ++this._token;
    this.current = key;
    this._usedSynth = false;
    if (!url) { this._useSynth(key); return true; }
    this._load(url).then((buf) => {
      if (token !== this._token) return;   // a newer play()/stop() superseded us
      this._stopVoices(0.02);              // stop the previous sound immediately
      this._startLoop(buf, token);
    }).catch(() => {
      // file couldn't load (offline / blocked) — use the bundled synthesized soundscape
      if (token !== this._token) return;
      this._useSynth(key);
    });
    return true;
  }
  // Offline-safe fallback: the synthesized version of the same environment.
  _useSynth(key) {
    this._usedSynth = true;
    try { window.ambientEngine.setVolume(this.volume); window.ambientEngine.play(key); } catch (e) {}
  }
  _startLoop(buf, token) {
    const ctx = this.ctx;
    const dur = buf.duration;
    const XF = Math.min(0.6, dur * 0.18);   // crossfade length
    const period = dur - XF;                // each voice hands off XF before its end
    const spawn = (startTime, first) => {
      if (token !== this._token) return;
      const src = ctx.createBufferSource();
      src.buffer = buf;
      const g = ctx.createGain();
      src.connect(g); g.connect(this.master);
      if (first) {
        // the very first voice starts at full level — instant playback, no fade-in
        g.gain.setValueAtTime(1, startTime);
      } else {
        // later voices fade in to crossfade seamlessly with the previous one
        g.gain.setValueAtTime(0.0001, startTime);
        g.gain.linearRampToValueAtTime(1, startTime + XF);
      }
      g.gain.setValueAtTime(1, startTime + period);
      g.gain.linearRampToValueAtTime(0.0001, startTime + dur);
      src.start(startTime);
      src.stop(startTime + dur + 0.05);
      this.voices.push({ src, g });
      if (this.voices.length > 4) this.voices = this.voices.slice(-4);
      // schedule the next overlapping voice slightly before this one fades
      const aheadMs = (period - (ctx.currentTime - startTime)) * 1000 - 90;
      const timer = setTimeout(() => spawn(startTime + period, false), Math.max(0, aheadMs));
      this.timers.push(timer);
    };
    spawn(ctx.currentTime + 0.005, true);
  }
  _stopVoices(fade = 0.25) {
    this.timers.forEach(clearTimeout); this.timers = [];
    if (!this.ctx) { this.voices = []; return; }
    const t = this.ctx.currentTime;
    this.voices.forEach(({ src, g }) => {
      try {
        g.gain.cancelScheduledValues(t);
        g.gain.setValueAtTime(Math.max(0.0001, g.gain.value), t);
        g.gain.linearRampToValueAtTime(0.0001, t + fade);
        src.stop(t + fade + 0.05);
      } catch (e) {}
    });
    this.voices = [];
  }
  stop() {
    this._token++;
    this.current = null;
    this._stopVoices(0.3);
    if (this._usedSynth) { try { window.ambientEngine.stop(); } catch (e) {} this._usedSynth = false; }
  }
}
window.whiteNoisePlayer = window.whiteNoisePlayer || new WhiteNoisePlayer();

// ── MediaSession ───────────────────────────────────────────
// Surfaces the active sound, timer mode and status — plus play / pause / stop
// controls — to the OS media UI (Android notification / lock screen, macOS Now
// Playing, etc.) where supported. Full iOS Live Activities and Dynamic Island require
// a native build; this is the web-standard equivalent and keeps the controls in sync.
function setMediaSession(info) {
  if (!('mediaSession' in navigator)) return;
  const ms = navigator.mediaSession;
  try {
    if (!info || info.clear) {
      ms.metadata = null;
      ['play', 'pause', 'stop'].forEach((a) => { try { ms.setActionHandler(a, null); } catch (e) {} });
      ms.playbackState = 'none';
      return;
    }
    ms.metadata = new MediaMetadata({
      title: info.title || 'Emeo',
      artist: info.artist || '',
      album: 'Emeo',
      artwork: [{ src: window.EME_ICON_SRC, sizes: '512x512', type: 'image/jpeg' }],
    });
    ms.playbackState = info.playing ? 'playing' : 'paused';
    try { ms.setActionHandler('play', info.onPlay || null); } catch (e) {}
    try { ms.setActionHandler('pause', info.onPause || null); } catch (e) {}
    try { ms.setActionHandler('stop', info.onStop || null); } catch (e) {}
  } catch (e) {}
}
window.setMediaSession = setMediaSession;

  