← Mike Higgins Home
Custom Code Experiment •

Binaural Protocol

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Binaural Protocol v2.0</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js">
    </script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=VT323&family=Comic+Neue:wght@400;700&display=swap');

        * {
            box-sizing: border-box;
        }

        body {
            font-family: 'Comic Neue', 'Comic Sans MS', cursive, sans-serif;
            color: #000080;
            margin: 0;
            padding: 16px;
            background-color: #008080;
            background-image: repeating-conic-gradient(#00a0a0 0% 25%, #008080 0% 50%);
            background-size: 24px 24px;
            min-height: 100vh;
        }

        .marquee-wrap {
            background: #000080;
            border: 4px ridge #c0c0c0;
            padding: 4px 0;
            margin-bottom: 12px;
        }
        marquee {
            color: #ffff00;
            font-family: 'VT323', monospace;
            font-size: 22px;
            letter-spacing: 1px;
        }

        main {
            max-width: 860px;
            margin: 0 auto;
            background: #c0c0c0;
            border: 4px outset #eeeeee;
            padding: 4px;
            box-shadow: 8px 8px 0 rgba(0, 0, 0, 0.4);
        }

        .titlebar {
            background: linear-gradient(90deg, #000080, #1084d0);
            color: white;
            font-family: 'VT323', monospace;
            font-size: 20px;
            padding: 4px 8px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .titlebar-buttons span {
            display: inline-block;
            width: 18px;
            height: 16px;
            background: #c0c0c0;
            border: 2px outset #eee;
            text-align: center;
            font-family: monospace;
            font-size: 12px;
            font-weight: bold;
            color: black;
            margin-left: 3px;
            line-height: 12px;
        }

        .content {
            background: #d4d0c8;
            padding: 14px;
        }

        h1 {
            font-family: 'VT323', monospace;
            font-size: 40px;
            color: #ff00ff;
            text-align: center;
            margin: 6px 0 0 0;
            text-shadow: 2px 2px 0 #000080, -1px -1px 0 #00ffff;
        }
        .subtitle {
            text-align: center;
            font-family: 'VT323', monospace;
            font-size: 18px;
            color: #008000;
            margin-bottom: 4px;
        }
        .blink {
            animation: blinker 1s step-start infinite;
        }
        @keyframes blinker {
            50% {
                opacity: 0;
            }
        }
        @media (prefers-reduced-motion: reduce) {
            .blink {
                animation: none;
            }
        }

        .hr90s {
            height: 6px;
            background: repeating-linear-gradient(90deg, #ff0000 0 8px, #ffff00 8px 16px, #00ff00 16px 24px, #00ffff 24px 32px, #ff00ff 32px 40px);
            border: none;
            margin: 10px 0;
        }

        .status-box {
            background: black;
            border: 3px inset #888;
            padding: 6px 10px;
            font-family: 'VT323', monospace;
            font-size: 20px;
            display: flex;
            align-items: center;
            gap: 10px;
            justify-content: center;
            margin-bottom: 10px;
            flex-wrap: wrap;
        }
        #status-indicator {
            width: 14px;
            height: 14px;
            background: #444;
            border: 1px solid #000;
        }
        #status-text {
            color: #00ff00;
            letter-spacing: 2px;
        }
        #timer-readout {
            color: #00ffff;
            letter-spacing: 2px;
        }
        .beat-led-wrap {
            display: flex;
            align-items: center;
            gap: 5px;
            color: #888;
            font-size: 14px;
        }
        #beat-led {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #330033;
            border: 1px solid #000;
        }

        .warning-table {
            width: 100%;
            border: 3px dashed #ff0000;
            background: #ffff99;
            padding: 8px;
            margin-bottom: 14px;
            font-size: 14px;
        }
        .warning-table strong {
            color: #cc0000;
        }
        .sci-note {
            font-size: 11px;
            color: #665500;
        }

        .panel {
            background: #ece9d8;
            border: 2px groove #fff;
            padding: 10px;
            margin-bottom: 12px;
        }
        .panel-title {
            font-family: 'VT323', monospace;
            font-size: 20px;
            background: #000080;
            color: #ffff00;
            padding: 2px 6px;
            margin: -10px -10px 8px -10px;
        }

        label.ctrl-label {
            font-weight: bold;
            font-size: 13px;
            color: #000080;
        }
        .val-readout {
            font-family: 'VT323', monospace;
            font-size: 18px;
            background: black;
            color: #ff00ff;
            padding: 0 6px;
            border: 2px inset #888;
            min-width: 70px;
            text-align: right;
        }

        .ctrl-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 2px;
            gap: 8px;
        }
        .ctrl-hint {
            font-size: 11px;
            color: #444;
            margin: 2px 0 8px 0;
            font-style: italic;
        }

        input[type=range] {
            -webkit-appearance: none;
            width: 100%;
            height: 18px;
            background: repeating-linear-gradient(90deg, #999 0 2px, #ccc 2px 4px);
            border: 2px inset #888;
        }
        input[type=range]::-webkit-slider-thumb {
            -webkit-appearance: none;
            height: 22px;
            width: 14px;
            background: #ff00ff;
            border: 2px outset #ffccff;
            cursor: pointer;
        }
        input[type=range]:disabled {
            opacity: 0.45;
        }
        input[type=range]:disabled::-webkit-slider-thumb {
            background: #888;
            border-color: #aaa;
        }

        select.retro-select {
            font-family: 'VT323', monospace;
            font-size: 16px;
            background: white;
            border: 2px inset #888;
            color: #000080;
            padding: 2px 4px;
        }

        .checkbox-row {
            display: flex;
            align-items: center;
            gap: 6px;
            margin-top: 6px;
            font-size: 12px;
        }
        .checkbox-row input {
            width: 16px;
            height: 16px;
        }

        .preset-btn,
        .timer-btn,
        .scope-btn {
            font-family: 'VT323', monospace;
            background: #c0c0c0;
            border: 3px outset #eee;
            cursor: pointer;
            color: #000080;
        }
        .preset-btn {
            flex: 1;
            font-size: 15px;
            padding: 6px 2px;
        }
        .preset-btn:active,
        .timer-btn:active,
        .scope-btn:active {
            border-style: inset;
        }
        .preset-btn.active-preset,
        .timer-btn.active-timer,
        .scope-btn.active-scope {
            border-style: inset;
            background: #ffff00;
            color: #cc0000;
        }
        .timer-btn {
            flex: 1;
            font-size: 16px;
            padding: 5px 2px;
        }
        .scope-btn {
            font-size: 13px;
            padding: 1px 6px;
        }

        #viz-panel {
            background: black;
            border: 4px inset #888;
            padding: 4px;
            position: relative;
            height: 190px;
        }
        .viz-label-row {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-family: 'VT323', monospace;
            font-size: 14px;
            padding: 2px 4px;
        }
        canvas#visualizer {
            width: 100%;
            height: 134px;
            display: block;
        }

        #btn-power {
            width: 100%;
            font-family: 'VT323', monospace;
            font-size: 26px;
            padding: 12px;
            background: #ff0000;
            color: #ffff00;
            border: 5px outset #ff8888;
            cursor: pointer;
            letter-spacing: 2px;
            text-shadow: 2px 2px 0 #800000;
        }
        #btn-power:active {
            border-style: inset;
        }
        #btn-power.playing {
            background: #00cc00;
            border-color: #99ff99;
            color: #000080;
        }
        #btn-power:disabled {
            opacity: 0.7;
            cursor: wait;
        }

        #engine-status {
            text-align: center;
            font-family: 'VT323', monospace;
            font-size: 14px;
            color: #444;
            margin-top: 6px;
        }

        .seq-grid {
            display: grid;
            grid-template-columns: 58px repeat(16, 1fr);
            gap: 3px;
            margin-top: 8px;
        }
        .seq-track-lbl {
            font-family: 'VT323', monospace;
            font-size: 15px;
            color: #000080;
            background: #c0c0c0;
            border: 2px outset #eee;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .seq-cell {
            height: 26px;
            background: #a8a8a8;
            border: 2px outset #ddd;
            cursor: pointer;
            padding: 0;
        }
        .seq-cell.beat-group {
            border-left: 3px solid #000080;
        }
        .seq-cell.on {
            background: #ff00ff;
            border-style: inset;
            border-color: #ffccff;
            box-shadow: inset 0 0 4px #800080;
        }
        .seq-cell.playing-col {
            outline: 2px solid #ffff00;
            outline-offset: -1px;
        }
        .seq-cell.on.playing-col {
            background: #ffff00;
        }
        .drum-controls-row {
            display: flex;
            flex-wrap: wrap;
            gap: 14px;
            align-items: flex-end;
            margin-top: 4px;
        }
        .drum-controls-row>div {
            flex: 1;
            min-width: 150px;
        }
    </style>
</head>
<body>

    <div class="marquee-wrap">
        <marquee behavior="scroll" direction="left">*** WELCOME TO THE ACOUSTIC BINAURAL PROTOCOL v2.0 *** NOW WITH SLEEP TIMER + X/Y OSCILLOSCOPE + SPEAKER MODE *** REQUIRES HEADPHONES (UNLESS SPEAKER MODE!!) *** BEST VIEWED AT 800x600 *** PLEASE SIGN THE GUESTBOOK *** </marquee>
    </div>

    <main>
        <div class="titlebar">
            <span>C:\PROGRAMS\AEP\euphoria2.exe</span>
            <div class="titlebar-buttons">
                <span>_</span><span>&#9633;</span><span>X</span>
            </div>
        </div>

        <div class="content">
            <h1>Acoustic Binaural Protocol</h1>
            <p class="subtitle">&lt;&lt; BINAURAL TRANSLATION ENGINE v2.0 &gt;&gt; <span class="blink">UPGRADED!!</span></p>
            <hr class="hr90s">

            <div class="status-box">
                <div id="status-indicator"></div>
                <span id="status-text">STANDBY</span>
                <span id="timer-readout">&#8734;</span>
                <div class="beat-led-wrap">BEAT:
                    <div id="beat-led"></div>
                </div>
            </div>

            <table class="warning-table" cellpadding="0" cellspacing="0">
                <tr>
                    <td style="width:32px; vertical-align:top; font-size:22px;">&#9888;</td>
                    <td>
                        <strong class="blink">HEADPHONES REQUIRED!!!</strong> <span id="warn-speaker-ok" style="display:none; color:#008000; font-weight:bold;">(SPEAKER MODE ACTIVE &mdash; headphones optional!)</span><br>
                        Binaural beats require stereo isolation to function. Ensure L/R channels are properly oriented. Start with low volume. Do not operate heavy machinery. Void where prohibited.<br>
                        <span class="sci-note">Scientific note: evidence that binaural beats alter brain states is mixed at best. Enjoy this as ambient relaxation, not medicine.</span>
                    </td>
                </tr>
            </table>

            <div style="display:flex; flex-wrap:wrap; gap:14px;">

                <div style="flex:1; min-width:280px;">

                    <div class="panel">
                        <div class="panel-title">CARRIER FREQUENCY (BECK BASE)</div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-carrier">Left Ear Base</label>
                            <span id="lbl-carrier" class="val-readout">111.0 Hz</span>
                        </div>
                        <input type="range" id="sl-carrier" min="60" max="250" step="0.5" value="111.0">
                        <p class="ctrl-hint">Base frequency delivered to the left ear.</p>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sel-wave">Oscillator Shape</label>
                            <select id="sel-wave" class="retro-select">
                                <option value="sine" selected>SINE</option>
                                <option value="triangle">TRIANGLE</option>
                            </select>
                        </div>
                        <div class="checkbox-row">
                            <input type="checkbox" id="cb-harmonic" checked>
                            <label for="cb-harmonic">Enable 10x Harmonic Multiplex</label>
                        </div>
                    </div>

                    <div class="panel">
                        <div class="panel-title">BINAURAL OFFSET (TARGET STATE)</div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-offset">Right Ear Delta</label>
                            <span id="lbl-offset" class="val-readout">2.5 Hz</span>
                        </div>
                        <input type="range" id="sl-offset" min="0.5" max="40" step="0.1" value="2.5">
                        <p class="ctrl-hint">0.5-4Hz Delta &middot; 4-8Hz Theta &middot; 8-13Hz Alpha &middot; 30-40Hz Gamma.</p>
                        <div class="checkbox-row">
                            <input type="checkbox" id="cb-isochronic">
                            <label for="cb-isochronic"><b>SPEAKER MODE</b> (isochronic pulse &mdash; no headphones needed)</label>
                        </div>
                    </div>

                    <div class="panel">
                        <div class="panel-title">RHYTHM GATING (LFO)</div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-gating">Pulse Rate</label>
                            <span id="lbl-gating" class="val-readout">2.0 Hz</span>
                        </div>
                        <input type="range" id="sl-gating" min="0.1" max="12" step="0.1" value="2.0">
                        <div class="ctrl-row" style="margin-top:6px;">
                            <label class="ctrl-label" for="sl-depth">Gate Depth</label>
                            <span id="lbl-depth" class="val-readout">80%</span>
                        </div>
                        <input type="range" id="sl-depth" min="0" max="100" step="1" value="80">
                        <p class="ctrl-hint">In SPEAKER MODE the pulse locks to the offset frequency at full depth.</p>
                    </div>

                    <div class="panel">
                        <div class="panel-title">NOISE MASKING</div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sel-noise">Noise Color</label>
                            <select id="sel-noise" class="retro-select">
                                <option value="pink" selected>PINK</option>
                                <option value="brown">BROWN</option>
                                <option value="white">WHITE</option>
                            </select>
                        </div>
                        <div class="ctrl-row" style="margin-top:6px;">
                            <label class="ctrl-label" for="sl-noise">Static Blend</label>
                            <span id="lbl-noise" class="val-readout">30%</span>
                        </div>
                        <input type="range" id="sl-noise" min="0" max="100" step="1" value="30">
                    </div>

                    <div class="panel">
                        <div class="panel-title">MASTER INTENSITY</div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-volume">Output Volume</label>
                            <span id="lbl-volume" class="val-readout">35%</span>
                        </div>
                        <input type="range" id="sl-volume" min="0" max="100" step="1" value="35">
                        <p class="ctrl-hint">Starts low on purpose. Your ears will thank us.</p>
                    </div>

                </div>

                <div style="flex:1; min-width:280px;">

                    <div class="panel">
                        <div class="panel-title">SESSION PRESETS</div>
                        <div style="display:flex; gap:6px;">
                            <button class="preset-btn" data-preset="delta">Deep Delta<br>[2Hz]</button>
                            <button class="preset-btn" data-preset="theta">Theta Trance<br>[5Hz]</button>
                            <button class="preset-btn" data-preset="alpha">Alpha Focus<br>[10Hz]</button>
                            <button class="preset-btn" data-preset="gamma">Gamma Blast<br>[40Hz]</button>
                        </div>
                        <p class="ctrl-hint" style="margin-bottom:0;">Presets configure carrier, offset, gating and noise together.</p>
                    </div>

                    <div class="panel">
                        <div class="panel-title">SLEEP TIMER (AUTO-FADE)</div>
                        <div style="display:flex; gap:6px;">
                            <button class="timer-btn active-timer" data-mins="0">&#8734;</button>
                            <button class="timer-btn" data-mins="15">15 MIN</button>
                            <button class="timer-btn" data-mins="30">30 MIN</button>
                            <button class="timer-btn" data-mins="60">60 MIN</button>
                        </div>
                        <p class="ctrl-hint" style="margin-bottom:0;">Fades to silence over the final 15 seconds, then powers down.</p>
                    </div>

                    <div id="viz-panel">
                        <div class="viz-label-row">
                            <span id="viz-left-lbl" style="color:#ff8800;">L: <b>AMBER</b></span>
                            <span>
                                <button class="scope-btn active-scope" id="btn-dual">DUAL TRACE</button>
                                <button class="scope-btn" id="btn-xy">X/Y MODE</button>
                            </span>
                            <span id="viz-right-lbl" style="color:#00ffff;">R: <b>CYAN</b></span>
                        </div>
                        <canvas id="visualizer"></canvas>
                    </div>

                    <div style="height:10px;"></div>

                    <button id="btn-power">&#9654; INITIALIZE PROTOCOL</button>

                    <div style="text-align:center;">
                        <p id="engine-status">Audio Engine: Disconnected</p>
                    </div>
                </div>

            </div>

            <div class="panel" style="margin-top:2px;">
                <div class="panel-title">RHYTHM SECTION &lt;DRUM.EXE&gt; <span class="blink" style="color:#00ff00;">NEW!!</span></div>

                <div class="checkbox-row" style="margin-top:0;">
                    <input type="checkbox" id="cb-drums">
                    <label for="cb-drums"><b>ENGAGE DRUM MACHINE</b> (runs while protocol is transmitting)</label>
                </div>

                <div class="drum-controls-row">
                    <div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-bpm">Tempo</label>
                            <span id="lbl-bpm" class="val-readout">90 BPM</span>
                        </div>
                        <input type="range" id="sl-bpm" min="50" max="180" step="1" value="90">
                        <div class="checkbox-row">
                            <input type="checkbox" id="cb-sync">
                            <label for="cb-sync">SYNC TO GATE (1 pulse = 1 beat)</label>
                        </div>
                    </div>
                    <div>
                        <div class="ctrl-row">
                            <label class="ctrl-label" for="sl-drumvol">Drum Volume</label>
                            <span id="lbl-drumvol" class="val-readout">50%</span>
                        </div>
                        <input type="range" id="sl-drumvol" min="0" max="100" step="1" value="50">
                        <div style="display:flex; gap:6px; margin-top:8px;">
                            <button class="preset-btn drum-preset" data-pattern="downtempo">DOWNTEMPO</button>
                            <button class="preset-btn drum-preset" data-pattern="boombap">BOOM BAP</button>
                            <button class="preset-btn drum-preset" data-pattern="techno">TECHNO</button>
                            <button class="preset-btn drum-preset" data-pattern="clear">CLEAR</button>
                        </div>
                    </div>
                </div>

                <div class="seq-grid" id="seq-grid"></div>
                <p class="ctrl-hint" style="margin-bottom:0;">Click cells to program the 16-step pattern. Yellow column = current step.</p>
            </div>

        </div>
    </main>

    <script>
        let isInitialized = false;
        let isPlaying = false;
        let sourcesRunning = false;
        let stopTimeout = null;
        let rafActive = false;
        let vizMode = 'dual';
        let speakerMode = false;

        let timerTotalSecs = 0;
        let timerRemaining = 0;
        let timerInterval = null;
        let fadeOutStarted = false;

        const FADE_IN = 1.5;
        const FADE_OUT = 1.2;
        const TIMER_FADE = 15;

        let masterVol, fadeGain, lfo, gateL, gateR;
        let oscL, oscR, oscL_high, oscR_high, panL, panR;
        let noise, noiseVol, analyserL, analyserR;

        const btnPower = document.getElementById('btn-power');
        const statusIndicator = document.getElementById('status-indicator');
        const statusText = document.getElementById('status-text');
        const timerReadout = document.getElementById('timer-readout');
        const engineStatus = document.getElementById('engine-status');
        const canvas = document.getElementById('visualizer');
        const ctx = canvas.getContext('2d');
        const cbHarmonic = document.getElementById('cb-harmonic');
        const cbIsochronic = document.getElementById('cb-isochronic');
        const selWave = document.getElementById('sel-wave');
        const selNoise = document.getElementById('sel-noise');
        const beatLed = document.getElementById('beat-led');
        const warnSpeakerOk = document.getElementById('warn-speaker-ok');

        const controls = {
            carrier: { slider: document.getElementById('sl-carrier'), label: document.getElementById('lbl-carrier') },
            offset: { slider: document.getElementById('sl-offset'), label: document.getElementById('lbl-offset') },
            gating: { slider: document.getElementById('sl-gating'), label: document.getElementById('lbl-gating') },
            depth: { slider: document.getElementById('sl-depth'), label: document.getElementById('lbl-depth') },
            noise: { slider: document.getElementById('sl-noise'), label: document.getElementById('lbl-noise') },
            volume: { slider: document.getElementById('sl-volume'), label: document.getElementById('lbl-volume') }
        };

        function resizeCanvas() {
            canvas.width = canvas.clientWidth;
            canvas.height = canvas.clientHeight;
            if (!isPlaying) drawFlatLine();
        }
        window.addEventListener('resize', resizeCanvas);

        async function initAudioEngine() {
            await Tone.start();

            masterVol = new Tone.Volume(sliderToDecibels(parseFloat(controls.volume.slider.value))).toDestination();
            fadeGain = new Tone.Gain(0).connect(masterVol);

            gateL = new Tone.Gain(1);
            gateR = new Tone.Gain(1);
            analyserL = new Tone.Analyser('waveform', 256);
            analyserR = new Tone.Analyser('waveform', 256);

            const depth = parseFloat(controls.depth.slider.value) / 100;
            lfo = new Tone.LFO({
                frequency: parseFloat(controls.gating.slider.value),
                min: 1 - depth,
                max: 1,
                type: 'sine'
            }).start();
            lfo.connect(gateL.gain);
            lfo.connect(gateR.gain);

            const carrierFreq = parseFloat(controls.carrier.slider.value);
            const offsetFreq = parseFloat(controls.offset.slider.value);
            const rightFreq = speakerMode ? carrierFreq : carrierFreq + offsetFreq;
            const waveType = selWave.value;
            const harmVol = cbHarmonic.checked ? -20 : -Infinity;

            oscL = new Tone.Oscillator({ frequency: carrierFreq, type: waveType });
            oscL_high = new Tone.Oscillator({ frequency: carrierFreq * 10, type: 'sine', volume: harmVol });
            panL = new Tone.Panner(-1).connect(fadeGain);
            gateL.connect(panL);
            gateL.connect(analyserL);
            oscL.connect(gateL);
            oscL_high.connect(gateL);

            oscR = new Tone.Oscillator({ frequency: rightFreq, type: waveType });
            oscR_high = new Tone.Oscillator({ frequency: rightFreq * 10, type: 'sine', volume: harmVol });
            panR = new Tone.Panner(1).connect(fadeGain);
            gateR.connect(panR);
            gateR.connect(analyserR);
            oscR.connect(gateR);
            oscR_high.connect(gateR);

            noise = new Tone.Noise(selNoise.value);
            noiseVol = new Tone.Volume(-20).connect(fadeGain);
            noise.connect(noiseVol);

            drumBus = new Tone.Volume(sliderToDecibels(parseFloat(slDrumVol.value)) - 4).connect(fadeGain);
            kickSynth = new Tone.MembraneSynth({
                pitchDecay: 0.05,
                octaves: 6,
                envelope: { attack: 0.001, decay: 0.35, sustain: 0.01, release: 0.6 }
            }).connect(drumBus);
            snareSynth = new Tone.NoiseSynth({
                noise: { type: 'white' },
                envelope: { attack: 0.001, decay: 0.18, sustain: 0 },
                volume: -6
            }).connect(drumBus);
            hatSynth = new Tone.MetalSynth({
                frequency: 250,
                envelope: { attack: 0.001, decay: 0.06, release: 0.02 },
                harmonicity: 5.1,
                modulationIndex: 32,
                resonance: 4000,
                octaves: 1.5,
                volume: -18
            }).connect(drumBus);

            Tone.Transport.bpm.value = parseFloat(slBpm.value);
            Tone.Transport.scheduleRepeat((time) => {
                const step = currentStep;
                if (pattern.kick[step]) kickSynth.triggerAttackRelease('C1', '8n', time);
                if (pattern.snare[step]) snareSynth.triggerAttackRelease('16n', time);
                if (pattern.hat[step]) hatSynth.triggerAttackRelease('32n', time, 0.6);
                Tone.Draw.schedule(() => highlightStep(step), time);
                currentStep = (currentStep + 1) % NUM_STEPS;
            }, '16n');

            updateVolumes();
            applySpeakerMode();

            isInitialized = true;
            engineStatus.textContent = 'Audio Engine: Tone.js Online';
            engineStatus.style.color = '#008000';
        }

        function sliderToDecibels(v) {
            if (v <= 0) return -Infinity;
            return 20 * Math.log10(v / 100);
        }

        function updateVolumes() {
            if (!isInitialized) return;
            masterVol.volume.rampTo(sliderToDecibels(parseFloat(controls.volume.slider.value)), 0.1);
            const noiseVal = parseFloat(controls.noise.slider.value);
            const noiseDb = noiseVal === 0 ? -Infinity : sliderToDecibels(noiseVal) - 10;
            noiseVol.volume.rampTo(noiseDb, 0.1);
        }

        function updateFrequencies() {
            if (!isInitialized) return;
            const carrierFreq = parseFloat(controls.carrier.slider.value);
            const offsetFreq = parseFloat(controls.offset.slider.value);
            const rightFreq = speakerMode ? carrierFreq : carrierFreq + offsetFreq;

            oscL.frequency.rampTo(carrierFreq, 0.1);
            oscL_high.frequency.rampTo(carrierFreq * 10, 0.1);
            oscR.frequency.rampTo(rightFreq, 0.1);
            oscR_high.frequency.rampTo(rightFreq * 10, 0.1);

            if (speakerMode) {
                lfo.frequency.rampTo(offsetFreq, 0.1);
                lfo.min = 0;
            } else {
                lfo.frequency.rampTo(parseFloat(controls.gating.slider.value), 0.1);
                lfo.min = 1 - parseFloat(controls.depth.slider.value) / 100;
            }

            syncBpmToGate();
        }

        function applySpeakerMode() {
            speakerMode = cbIsochronic.checked;
            controls.gating.slider.disabled = speakerMode;
            controls.depth.slider.disabled = speakerMode;
            warnSpeakerOk.style.display = speakerMode ? 'inline' : 'none';
            if (speakerMode) {
                controls.gating.label.textContent = '= OFFSET';
                controls.depth.label.textContent = '100%';
            } else {
                controls.gating.label.textContent = parseFloat(controls.gating.slider.value).toFixed(1) + ' Hz';
                controls.depth.label.textContent = controls.depth.slider.value + '%';
            }
            updateFrequencies();
        }

        function startSources() {
            if (sourcesRunning) return;
            oscL.start();
            oscL_high.start();
            oscR.start();
            oscR_high.start();
            noise.start();
            sourcesRunning = true;
        }

        function stopSources() {
            if (!sourcesRunning) return;
            oscL.stop();
            oscL_high.stop();
            oscR.stop();
            oscR_high.stop();
            noise.stop();
            sourcesRunning = false;
        }

        async function startPlayback() {
            if (!isInitialized) {
                btnPower.disabled = true;
                btnPower.innerHTML = '&#9203; LOADING.EXE...';
                await initAudioEngine();
                btnPower.disabled = false;
            }
            if (stopTimeout) {
                clearTimeout(stopTimeout);
                stopTimeout = null;
            }

            startSources();
            fadeGain.gain.cancelScheduledValues(Tone.now());
            fadeGain.gain.rampTo(1, FADE_IN);
            isPlaying = true;
            fadeOutStarted = false;

            btnPower.innerHTML = '&#9724; TERMINATE';
            btnPower.classList.add('playing');
            statusIndicator.style.background = '#ff0000';
            statusIndicator.classList.add('blink');
            statusText.textContent = 'TRANSMITTING';
            statusText.style.color = '#ffff00';

            startTimerTick();
            startViz();
            if (cbDrums.checked) startDrums();
        }

        function stopPlayback(immediate = false) {
            isPlaying = false;
            stopTimerTick();
            stopDrums();

            if (isInitialized) {
                fadeGain.gain.cancelScheduledValues(Tone.now());
                if (immediate) {
                    fadeGain.gain.rampTo(0, 0.05);
                    stopTimeout = setTimeout(stopSources, 100);
                } else {
                    fadeGain.gain.rampTo(0, FADE_OUT);
                    stopTimeout = setTimeout(stopSources, FADE_OUT * 1000 + 100);
                }
            }

            btnPower.innerHTML = '&#9654; RESUME PROTOCOL';
            btnPower.classList.remove('playing');
            statusIndicator.style.background = '#444';
            statusIndicator.classList.remove('blink');
            statusText.textContent = 'STANDBY';
            statusText.style.color = '#00ff00';
        }

        btnPower.addEventListener('click', async () => {
            if (isPlaying) stopPlayback();
            else await startPlayback();
        });

        function formatTime(s) {
            const m = Math.floor(s / 60);
            const sec = s % 60;
            return String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
        }

        function updateTimerReadout() {
            timerReadout.innerHTML = timerTotalSecs === 0 ? '&#8734;' : formatTime(timerRemaining);
        }

        document.querySelectorAll('.timer-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const mins = parseInt(e.currentTarget.dataset.mins, 10);
                timerTotalSecs = mins * 60;
                timerRemaining = timerTotalSecs;
                fadeOutStarted = false;
                if (isInitialized && isPlaying) {
                    fadeGain.gain.cancelScheduledValues(Tone.now());
                    fadeGain.gain.rampTo(1, 0.5);
                }
                document.querySelectorAll('.timer-btn').forEach(b => b.classList.remove('active-timer'));
                e.currentTarget.classList.add('active-timer');
                updateTimerReadout();
            });
        });

        function startTimerTick() {
            stopTimerTick();
            timerInterval = setInterval(() => {
                if (timerTotalSecs === 0) return;
                timerRemaining--;
                updateTimerReadout();

                if (timerRemaining === TIMER_FADE && !fadeOutStarted) {
                    fadeOutStarted = true;
                    fadeGain.gain.cancelScheduledValues(Tone.now());
                    fadeGain.gain.rampTo(0, TIMER_FADE);
                    statusText.textContent = 'FADING...';
                    statusText.style.color = '#ff8800';
                }
                if (timerRemaining <= 0) {
                    stopPlayback(true);
                    timerTotalSecs = 0;
                    timerRemaining = 0;
                    document.querySelectorAll('.timer-btn').forEach(b => b.classList.remove('active-timer'));
                    document.querySelector('.timer-btn[data-mins="0"]').classList.add('active-timer');
                    updateTimerReadout();
                    statusText.textContent = 'SESSION COMPLETE';
                }
            }, 1000);
        }

        function stopTimerTick() {
            if (timerInterval) {
                clearInterval(timerInterval);
                timerInterval = null;
            }
        }

        Object.entries(controls).forEach(([key, config]) => {
            config.slider.addEventListener('input', (e) => {
                const val = e.target.value;
                if (key === 'noise' || key === 'volume' || key === 'depth') {
                    config.label.textContent = val + '%';
                } else {
                    config.label.textContent = parseFloat(val).toFixed(1) + ' Hz';
                }
                if (key === 'volume' || key === 'noise') updateVolumes();
                else updateFrequencies();
                if (key === 'offset') clearPresetHighlight();
            });
        });

        cbHarmonic.addEventListener('change', (e) => {
            if (!isInitialized) return;
            const vol = e.target.checked ? -20 : -Infinity;
            oscL_high.volume.rampTo(vol, 0.1);
            oscR_high.volume.rampTo(vol, 0.1);
        });

        cbIsochronic.addEventListener('change', applySpeakerMode);

        selWave.addEventListener('change', (e) => {
            if (!isInitialized) return;
            oscL.type = e.target.value;
            oscR.type = e.target.value;
        });

        selNoise.addEventListener('change', (e) => {
            if (!isInitialized) return;
            noise.type = e.target.value;
        });

        const PRESETS = {
            delta: { carrier: 100, offset: 2, gating: 2, depth: 85, noise: 40 },
            theta: { carrier: 140, offset: 5, gating: 4, depth: 80, noise: 30 },
            alpha: { carrier: 200, offset: 10, gating: 8, depth: 70, noise: 15 },
            gamma: { carrier: 220, offset: 40, gating: 10, depth: 60, noise: 10 }
        };

        function clearPresetHighlight() {
            document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active-preset'));
        }

        document.querySelectorAll('.preset-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const p = PRESETS[e.currentTarget.dataset.preset];
                if (!p) return;

                controls.carrier.slider.value = p.carrier;
                controls.carrier.label.textContent = p.carrier.toFixed(1) + ' Hz';
                controls.offset.slider.value = p.offset;
                controls.offset.label.textContent = p.offset.toFixed(1) + ' Hz';
                controls.gating.slider.value = p.gating;
                controls.depth.slider.value = p.depth;
                if (!speakerMode) {
                    controls.gating.label.textContent = p.gating.toFixed(1) + ' Hz';
                    controls.depth.label.textContent = p.depth + '%';
                }
                controls.noise.slider.value = p.noise;
                controls.noise.label.textContent = p.noise + '%';

                clearPresetHighlight();
                e.currentTarget.classList.add('active-preset');

                updateFrequencies();
                updateVolumes();
            });
        });

        const btnDual = document.getElementById('btn-dual');
        const btnXY = document.getElementById('btn-xy');
        const vizLeftLbl = document.getElementById('viz-left-lbl');
        const vizRightLbl = document.getElementById('viz-right-lbl');

        function setVizMode(mode) {
            vizMode = mode;
            btnDual.classList.toggle('active-scope', mode === 'dual');
            btnXY.classList.toggle('active-scope', mode === 'xy');
            if (mode === 'xy') {
                vizLeftLbl.innerHTML = 'X: <b>LEFT</b>';
                vizRightLbl.innerHTML = 'Y: <b>RIGHT</b>';
                vizLeftLbl.style.color = '#00ff00';
                vizRightLbl.style.color = '#00ff00';
            } else {
                vizLeftLbl.innerHTML = 'L: <b>AMBER</b>';
                vizRightLbl.innerHTML = 'R: <b>CYAN</b>';
                vizLeftLbl.style.color = '#ff8800';
                vizRightLbl.style.color = '#00ffff';
            }
            ctx.fillStyle = 'black';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }
        btnDual.addEventListener('click', () => setVizMode('dual'));
        btnXY.addEventListener('click', () => setVizMode('xy'));

        let beatPhase = 0;
        let lastFrameTime = null;

        function updateBeatLed(now) {
            if (lastFrameTime === null) lastFrameTime = now;
            const dt = (now - lastFrameTime) / 1000;
            lastFrameTime = now;
            const beatFreq = parseFloat(controls.offset.slider.value);
            beatPhase = (beatPhase + dt * beatFreq) % 1;
            const brightness = (Math.sin(beatPhase * Math.PI * 2) + 1) / 2;
            const c = Math.round(60 + brightness * 195);
            beatLed.style.background = 'rgb(' + c + ',0,' + c + ')';
            beatLed.style.boxShadow = brightness > 0.6 ? '0 0 6px #ff00ff' : 'none';
        }

        function startViz() {
            if (rafActive) return;
            rafActive = true;
            requestAnimationFrame(vizLoop);
        }

        function vizLoop(now) {
            if (!isPlaying) {
                rafActive = false;
                lastFrameTime = null;
                beatLed.style.background = '#330033';
                beatLed.style.boxShadow = 'none';
                drawFlatLine();
                return;
            }
            requestAnimationFrame(vizLoop);
            if (!isInitialized) return;

            updateBeatLed(now);

            const valuesL = analyserL.getValue();
            const valuesR = analyserR.getValue();

            if (vizMode === 'dual') {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                drawWave(valuesR, '#00ffff');
                drawWave(valuesL, '#ff8800');
            } else {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.12)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                const cx = canvas.width / 2;
                const cy = canvas.height / 2;
                const scale = Math.min(canvas.width, canvas.height) * 0.42;
                ctx.beginPath();
                ctx.lineWidth = 2;
                ctx.strokeStyle = '#00ff00';
                for (let i = 0; i < valuesL.length; i++) {
                    const x = cx + valuesL[i] * scale;
                    const y = cy - valuesR[i] * scale;
                    if (i === 0) ctx.moveTo(x, y);
                    else ctx.lineTo(x, y);
                }
                ctx.shadowBlur = 8;
                ctx.shadowColor = '#00ff00';
                ctx.stroke();
            }
            ctx.shadowBlur = 0;
        }

        function drawWave(values, color) {
            ctx.beginPath();
            ctx.lineWidth = 2;
            ctx.strokeStyle = color;
            for (let i = 0; i < values.length; i++) {
                const x = (i / values.length) * canvas.width;
                const y = (0.5 - (values[i] / 2.5)) * canvas.height;
                if (i === 0) ctx.moveTo(x, y);
                else ctx.lineTo(x, y);
            }
            ctx.shadowBlur = 8;
            ctx.shadowColor = color;
            ctx.stroke();
        }

        function drawFlatLine() {
            ctx.fillStyle = 'black';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.beginPath();
            ctx.moveTo(0, canvas.height / 2);
            ctx.lineTo(canvas.width, canvas.height / 2);
            ctx.lineWidth = 2;
            ctx.strokeStyle = '#003300';
            ctx.stroke();
        }

        const NUM_STEPS = 16;
        const TRACKS = [
            { id: 'kick', name: 'KICK' },
            { id: 'snare', name: 'SNARE' },
            { id: 'hat', name: 'HAT' }
        ];
        const pattern = {
            kick: Array(NUM_STEPS).fill(false),
            snare: Array(NUM_STEPS).fill(false),
            hat: Array(NUM_STEPS).fill(false)
        };
        let drumBus, kickSynth, snareSynth, hatSynth;
        let currentStep = 0;
        let lastLitStep = -1;

        const cbDrums = document.getElementById('cb-drums');
        const cbSync = document.getElementById('cb-sync');
        const slBpm = document.getElementById('sl-bpm');
        const lblBpm = document.getElementById('lbl-bpm');
        const slDrumVol = document.getElementById('sl-drumvol');
        const lblDrumVol = document.getElementById('lbl-drumvol');
        const seqGrid = document.getElementById('seq-grid');
        const cellRefs = { kick: [], snare: [], hat: [] };

        TRACKS.forEach(track => {
            const lbl = document.createElement('div');
            lbl.className = 'seq-track-lbl';
            lbl.textContent = track.name;
            seqGrid.appendChild(lbl);
            for (let i = 0; i < NUM_STEPS; i++) {
                const cell = document.createElement('button');
                cell.className = 'seq-cell' + (i % 4 === 0 ? ' beat-group' : '');
                cell.setAttribute('aria-label', track.name + ' step ' + (i + 1));
                cell.addEventListener('click', () => {
                    pattern[track.id][i] = !pattern[track.id][i];
                    cell.classList.toggle('on', pattern[track.id][i]);
                    clearDrumPresetHighlight();
                });
                seqGrid.appendChild(cell);
                cellRefs[track.id].push(cell);
            }
        });

        function highlightStep(step) {
            TRACKS.forEach(t => {
                if (lastLitStep >= 0) cellRefs[t.id][lastLitStep].classList.remove('playing-col');
                cellRefs[t.id][step].classList.add('playing-col');
            });
            lastLitStep = step;
        }

        function clearStepHighlight() {
            if (lastLitStep < 0) return;
            TRACKS.forEach(t => cellRefs[t.id][lastLitStep].classList.remove('playing-col'));
            lastLitStep = -1;
        }

        function startDrums() {
            if (!isInitialized) return;
            currentStep = 0;
            Tone.Transport.start();
        }

        function stopDrums() {
            if (Tone.Transport.state === 'started') Tone.Transport.stop();
            clearStepHighlight();
        }

        cbDrums.addEventListener('change', () => {
            if (cbDrums.checked && isPlaying) startDrums();
            else stopDrums();
        });

        function syncBpmToGate() {
            if (!cbSync.checked) return;
            const gateRate = speakerMode ?
                parseFloat(controls.offset.slider.value) :
                parseFloat(controls.gating.slider.value);
            const bpm = Math.min(220, Math.max(40, gateRate * 60));
            slBpm.value = bpm;
            lblBpm.textContent = Math.round(bpm) + ' BPM' + (bpm !== gateRate * 60 ? '*' : '');
            if (isInitialized) Tone.Transport.bpm.rampTo(bpm, 0.2);
        }

        slBpm.addEventListener('input', () => {
            lblBpm.textContent = slBpm.value + ' BPM';
            if (isInitialized) Tone.Transport.bpm.rampTo(parseFloat(slBpm.value), 0.1);
        });

        cbSync.addEventListener('change', () => {
            slBpm.disabled = cbSync.checked;
            if (cbSync.checked) syncBpmToGate();
            else lblBpm.textContent = slBpm.value + ' BPM';
        });

        slDrumVol.addEventListener('input', () => {
            lblDrumVol.textContent = slDrumVol.value + '%';
            if (isInitialized) {
                const v = parseFloat(slDrumVol.value);
                drumBus.volume.rampTo(v === 0 ? -Infinity : sliderToDecibels(v) - 4, 0.1);
            }
        });

        const DRUM_PATTERNS = {
            downtempo: {
                kick: [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0],
                snare: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
                hat: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1]
            },
            boombap: {
                kick: [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0],
                snare: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
                hat: [1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0]
            },
            techno: {
                kick: [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0],
                snare: [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
                hat: [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0]
            },
            clear: {
                kick: Array(16).fill(0),
                snare: Array(16).fill(0),
                hat: Array(16).fill(0)
            }
        };

        function clearDrumPresetHighlight() {
            document.querySelectorAll('.drum-preset').forEach(b => b.classList.remove('active-preset'));
        }

        document.querySelectorAll('.drum-preset').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const p = DRUM_PATTERNS[e.currentTarget.dataset.pattern];
                if (!p) return;
                TRACKS.forEach(t => {
                    for (let i = 0; i < NUM_STEPS; i++) {
                        pattern[t.id][i] = !!p[t.id][i];
                        cellRefs[t.id][i].classList.toggle('on', pattern[t.id][i]);
                    }
                });
                clearDrumPresetHighlight();
                if (e.currentTarget.dataset.pattern !== 'clear') {
                    e.currentTarget.classList.add('active-preset');
                }
            });
        });

        resizeCanvas();
        updateTimerReadout();
    </script>
</body>
</html>