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>□</span><span>X</span>
</div>
</div>
<div class="content">
<h1>Acoustic Binaural Protocol</h1>
<p class="subtitle"><< BINAURAL TRANSLATION ENGINE v2.0 >> <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">∞</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;">⚠</td>
<td>
<strong class="blink">HEADPHONES REQUIRED!!!</strong> <span id="warn-speaker-ok" style="display:none; color:#008000; font-weight:bold;">(SPEAKER MODE ACTIVE — 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 · 4-8Hz Theta · 8-13Hz Alpha · 30-40Hz Gamma.</p>
<div class="checkbox-row">
<input type="checkbox" id="cb-isochronic">
<label for="cb-isochronic"><b>SPEAKER MODE</b> (isochronic pulse — 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">∞</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">▶ 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 <DRUM.EXE> <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 = '⏳ 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 = '◼ 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 = '▶ 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 ? '∞' : 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>