A Pen by mode-mercury on CodePen.
Created
December 12, 2025 23:46
-
-
Save mode-mercury/07aa7c49593cfebac193f47f91b5fd56 to your computer and use it in GitHub Desktop.
Untitled
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>OSC+ Sensor Chaos DAW</title> | |
| <style> | |
| body { | |
| background: #111; | |
| color: #eee; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; | |
| padding: 1rem; | |
| } | |
| h1 { | |
| font-size: 1.2rem; | |
| margin-bottom: 0.5rem; | |
| } | |
| .row { | |
| margin: 0.5rem 0; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 0.25rem; | |
| font-size: 0.85rem; | |
| opacity: 0.9; | |
| } | |
| input, select, button { | |
| background: #222; | |
| color: #eee; | |
| border: 1px solid #444; | |
| padding: 0.3rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| } | |
| button { | |
| cursor: pointer; | |
| } | |
| button:hover { | |
| background: #333; | |
| } | |
| #status { | |
| font-size: 0.8rem; | |
| opacity: 0.85; | |
| margin-top: 0.5rem; | |
| } | |
| #mapping { | |
| font-size: 0.8rem; | |
| margin-top: 0.5rem; | |
| line-height: 1.35; | |
| white-space: pre-line; | |
| } | |
| #cam-container { | |
| position: relative; | |
| width: 320px; | |
| max-width: 100%; | |
| margin-top: 1rem; | |
| border: 1px solid #444; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| background: #000; | |
| } | |
| #cam { | |
| width: 100%; | |
| display: block; | |
| transform: scaleX(-1); /* mirror selfie-style */ | |
| } | |
| #hud { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| color: #0ff; | |
| font-size: 0.7rem; | |
| text-shadow: 0 0 4px #000; | |
| padding: 0.25rem; | |
| box-sizing: border-box; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: flex-start; | |
| background: linear-gradient(to bottom, rgba(0,0,0,0.35), transparent 40%); | |
| } | |
| .hud-block { | |
| margin-bottom: 0.2rem; | |
| } | |
| .hud-title { | |
| font-weight: 600; | |
| font-size: 0.7rem; | |
| color: #8ff; | |
| } | |
| .hud-line { | |
| font-family: monospace; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>OSC+ Sensor Chaos DAW</h1> | |
| <div class="row"> | |
| <button id="start">Start</button> | |
| <button id="stop">Stop</button> | |
| <button id="chaos">Chaos Map</button> | |
| </div> | |
| <div class="row"> | |
| <label> | |
| Key (MIDI root: 60 = C4, 57 = A3, etc.) | |
| <input id="root" type="number" value="60"> | |
| </label> | |
| </div> | |
| <div class="row"> | |
| <label> | |
| Scale | |
| <select id="scale"> | |
| <option value="pentatonic">Pentatonic</option> | |
| <option value="major">Major</option> | |
| <option value="minor">Minor</option> | |
| </select> | |
| </label> | |
| </div> | |
| <div class="row"> | |
| <label> | |
| BPM | |
| <input id="bpm" type="number" value="90"> | |
| </label> | |
| </div> | |
| <div id="status">Status: idle</div> | |
| <div id="mapping"></div> | |
| <div id="cam-container"> | |
| <video id="cam" autoplay muted playsinline></video> | |
| <div id="hud"> | |
| <div class="hud-block"> | |
| <div class="hud-title">GPS</div> | |
| <div id="hud-gps" class="hud-line">lat: --, lon: --</div> | |
| </div> | |
| <div class="hud-block"> | |
| <div class="hud-title">Accel</div> | |
| <div id="hud-accel" class="hud-line">x: -- y: -- z: --</div> | |
| <div id="hud-angles" class="hud-line">pitch: -- roll: --</div> | |
| </div> | |
| <div class="hud-block"> | |
| <div class="hud-title">Mods</div> | |
| <div id="hud-mods" class="hud-line"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // -------- helpers -------- | |
| function midiToFreq(m) { | |
| return 440 * Math.pow(2, (m - 69) / 12); | |
| } | |
| function clamp01(x) { | |
| return x < 0 ? 0 : x > 1 ? 1 : x; | |
| } | |
| const SCALES = { | |
| major: [0, 2, 4, 5, 7, 9, 11], | |
| minor: [0, 2, 3, 5, 7, 8, 10], | |
| pentatonic: [0, 2, 5, 7, 9] | |
| }; | |
| class OSCPlusEngine { | |
| constructor() { | |
| this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| this.destination = this.audioContext.destination; | |
| } | |
| async ensureRunning() { | |
| if (this.audioContext.state === "suspended") { | |
| await this.audioContext.resume(); | |
| } | |
| } | |
| } | |
| class DelayUnit { | |
| constructor(engine, time, feedback, mix, maxTime) { | |
| this.engine = engine; | |
| const ctx = engine.audioContext; | |
| this.input = ctx.createGain(); | |
| this.output = ctx.createGain(); | |
| this.delay = ctx.createDelay(maxTime || 2.0); | |
| this.feedback = ctx.createGain(); | |
| this.wet = ctx.createGain(); | |
| this.dry = ctx.createGain(); | |
| this.input.connect(this.dry).connect(this.output); | |
| this.input.connect(this.delay); | |
| this.delay.connect(this.wet).connect(this.output); | |
| this.delay.connect(this.feedback).connect(this.delay); | |
| this.setTime(time || 0.3); | |
| this.setFeedback(feedback || 0.3); | |
| this.setMix(mix || 0.5); | |
| } | |
| connect(target) { | |
| this.output.connect(target); | |
| } | |
| setTime(t) { | |
| this.delay.delayTime.setValueAtTime(t, this.engine.audioContext.currentTime); | |
| } | |
| setFeedback(v) { | |
| this.feedback.gain.setValueAtTime(v, this.engine.audioContext.currentTime); | |
| } | |
| setMix(m) { | |
| m = clamp01(m); | |
| const now = this.engine.audioContext.currentTime; | |
| this.wet.gain.setValueAtTime(m, now); | |
| this.dry.gain.setValueAtTime(1 - m, now); | |
| } | |
| } | |
| // main chaos DAW | |
| class ChaosMiniDAW { | |
| constructor(engine, opts, videoEl, hudEls) { | |
| this.engine = engine; | |
| this.ctx = engine.audioContext; | |
| this.rootMidi = (opts && opts.rootMidi) || 60; | |
| this.scaleName = (opts && opts.scale) || "pentatonic"; | |
| this.bpm = (opts && opts.bpm) || 90; | |
| // master chain: voices -> masterGain -> masterFilter -> panner -> dest | |
| this.masterGain = this.ctx.createGain(); | |
| this.masterGain.gain.value = (opts && opts.masterGain) || 0.35; | |
| this.masterFilter = this.ctx.createBiquadFilter(); | |
| this.masterFilter.type = "lowpass"; | |
| this.masterFilter.frequency.value = 8000; | |
| this.masterFilter.Q.value = 0.2; | |
| this.panner = this.ctx.createStereoPanner(); | |
| this.panner.pan.value = 0; | |
| this.masterGain.connect(this.masterFilter).connect(this.panner).connect(this.engine.destination); | |
| // FX send | |
| this.delay = new DelayUnit(engine, 0.3, 0.35, 0.3); | |
| this.delay.connect(this.masterGain); | |
| this.isRunning = false; | |
| this._timerId = null; | |
| this._step = 0; | |
| // sensors | |
| this.lastGeo = null; | |
| this.lastMotion = null; | |
| this.lastAngles = { pitch: 0, roll: 0 }; | |
| this.hasCamera = false; | |
| // RNG state | |
| this._randSlow = Math.random(); | |
| this._randSlowTime = this.ctx.currentTime; | |
| // mod routing | |
| this.modSources = [ | |
| "timeLFO", | |
| "screenLFO", | |
| "motionMagLFO", | |
| "angleLFO", | |
| "latLFO", | |
| "lonLFO", | |
| "randFast", | |
| "randSlow" | |
| ]; | |
| this.modTargets = [ | |
| "density", | |
| "melodyColor", | |
| "delayTime", | |
| "delayFeedback", | |
| "masterLevel", | |
| "filterCutoff", | |
| "filterRes", | |
| "panPos", | |
| "padIntensity" | |
| ]; | |
| this.modRouting = {}; | |
| this._randomizeMapping(); | |
| this.videoEl = videoEl; | |
| this.hudEls = hudEls; | |
| this._initSensors(); | |
| this._startHudLoop(); | |
| } | |
| async _initSensors() { | |
| // camera | |
| if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ video: true }); | |
| this.hasCamera = true; | |
| if (this.videoEl) { | |
| this.videoEl.srcObject = stream; | |
| } | |
| } catch (e) { | |
| this.hasCamera = false; | |
| } | |
| } | |
| // GPS | |
| if ("geolocation" in navigator) { | |
| navigator.geolocation.watchPosition( | |
| (pos) => { this.lastGeo = pos.coords; }, | |
| () => { this.lastGeo = null; }, | |
| { enableHighAccuracy: true, maximumAge: 5000, timeout: 10000 } | |
| ); | |
| } | |
| // motion / angles | |
| if (typeof window !== "undefined" && "DeviceMotionEvent" in window) { | |
| try { | |
| const handler = (e) => { | |
| this.lastMotion = e.accelerationIncludingGravity; | |
| const x = (this.lastMotion.x || 0); | |
| const y = (this.lastMotion.y || 0); | |
| const z = (this.lastMotion.z || 0); | |
| // crude pitch/roll estimates in degrees | |
| const pitch = Math.atan2(-x, Math.sqrt(y*y + z*z)) * 180 / Math.PI; | |
| const roll = Math.atan2(y, z) * 180 / Math.PI; | |
| this.lastAngles = { pitch, roll }; | |
| }; | |
| if (typeof DeviceMotionEvent.requestPermission === "function") { | |
| DeviceMotionEvent.requestPermission().then((res) => { | |
| if (res === "granted") { | |
| window.addEventListener("devicemotion", handler); | |
| } | |
| }); | |
| } else { | |
| window.addEventListener("devicemotion", handler); | |
| } | |
| } catch (e) { | |
| // ignore | |
| } | |
| } | |
| } | |
| _describeMapping() { | |
| const lines = []; | |
| for (const target of this.modTargets) { | |
| lines.push(`${target.padEnd(13)} ← ${this.modRouting[target]}`); | |
| } | |
| return lines.join("\n"); | |
| } | |
| _randomizeMapping() { | |
| const shuffled = this.modSources.slice(); | |
| for (let i = shuffled.length - 1; i > 0; i--) { | |
| const j = Math.floor(Math.random() * (i + 1)); | |
| [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; | |
| } | |
| this.modTargets.forEach((t, idx) => { | |
| this.modRouting[t] = shuffled[idx % shuffled.length]; | |
| }); | |
| } | |
| _sampleModSources() { | |
| const t = this.ctx.currentTime; | |
| // time LFO | |
| const timeLFO = (Math.sin(t * 0.25) + 1) / 2; | |
| // screen LFO | |
| let screenFactor = 0; | |
| if (typeof window !== "undefined") { | |
| const w = window.innerWidth || 1; | |
| const h = window.innerHeight || 1; | |
| screenFactor = (((w % 997) / 997) + ((h % 991) / 991)) * 0.5; | |
| } | |
| const screenLFO = clamp01(0.5 * screenFactor + 0.5 * (Math.sin(t * 0.13 + screenFactor * 5) + 1) / 2); | |
| // motion magnitude | |
| let motionMag = 0; | |
| if (this.lastMotion) { | |
| const x = this.lastMotion.x || 0; | |
| const y = this.lastMotion.y || 0; | |
| const z = this.lastMotion.z || 0; | |
| motionMag = Math.sqrt(x*x + y*y + z*z); | |
| } | |
| const motionMagLFO = clamp01(motionMag / 25); | |
| // angle-based LFO | |
| const { pitch, roll } = this.lastAngles; | |
| const angleNorm = ((Math.abs(pitch) + Math.abs(roll)) / 180) / 2; | |
| const angleLFO = clamp01(angleNorm); | |
| // lat/lon as 0–1 | |
| let latLFO = 0.5, lonLFO = 0.5; | |
| if (this.lastGeo) { | |
| const lat = this.lastGeo.latitude; | |
| const lon = this.lastGeo.longitude; | |
| latLFO = ((lat % 90) / 90 + 1) / 2; | |
| lonLFO = ((lon % 180) / 180 + 1) / 2; | |
| } | |
| // RNGs | |
| const randFast = Math.random(); | |
| if (t - this._randSlowTime > 1.5) { | |
| this._randSlow = Math.random(); | |
| this._randSlowTime = t; | |
| } | |
| const randSlow = this._randSlow; | |
| return { | |
| timeLFO, | |
| screenLFO, | |
| motionMagLFO, | |
| angleLFO, | |
| latLFO, | |
| lonLFO, | |
| randFast, | |
| randSlow | |
| }; | |
| } | |
| _getScale() { | |
| return SCALES[this.scaleName] || SCALES.pentatonic; | |
| } | |
| _pickNote(chaos, isBass, octaveOffset = 0) { | |
| const scale = this._getScale(); | |
| const idx = Math.floor(chaos * scale.length) % scale.length; | |
| const baseOct = isBass ? -1 : 0; | |
| const octSpan = isBass ? 1 : 2; | |
| const oct = baseOct + octaveOffset + Math.floor(chaos * octSpan); | |
| const semitone = this.rootMidi + scale[idx] + oct * 12; | |
| return midiToFreq(semitone); | |
| } | |
| _playVoice(freq, time, duration, type, level, panSkew = 0) { | |
| const ctx = this.ctx; | |
| const osc = ctx.createOscillator(); | |
| const gain = ctx.createGain(); | |
| const pan = ctx.createStereoPanner(); | |
| osc.type = type || "sine"; | |
| osc.frequency.setValueAtTime(freq, time); | |
| const attack = 0.01; | |
| const decay = duration * 0.7; | |
| const end = time + duration; | |
| gain.gain.setValueAtTime(0, time); | |
| gain.gain.linearRampToValueAtTime(level, time + attack); | |
| gain.gain.linearRampToValueAtTime(0, time + attack + decay); | |
| pan.pan.value = panSkew; | |
| osc.connect(gain).connect(pan); | |
| pan.connect(this.masterGain); | |
| pan.connect(this.delay.input); | |
| osc.start(time); | |
| osc.stop(end + 0.1); | |
| } | |
| _playPad(freq, time, duration, color = 0.5) { | |
| const ctx = this.ctx; | |
| const osc1 = ctx.createOscillator(); | |
| const osc2 = ctx.createOscillator(); | |
| const gain = ctx.createGain(); | |
| // gentle detune | |
| osc1.type = "sine"; | |
| osc2.type = "triangle"; | |
| osc1.frequency.setValueAtTime(freq, time); | |
| osc2.frequency.setValueAtTime(freq * (1 + (color - 0.5) * 0.02), time); | |
| const attack = 0.4; | |
| const release = 0.8; | |
| const end = time + duration; | |
| gain.gain.setValueAtTime(0, time); | |
| gain.gain.linearRampToValueAtTime(0.18, time + attack); | |
| gain.gain.setValueAtTime(0.18, end); | |
| gain.gain.linearRampToValueAtTime(0, end + release); | |
| osc1.connect(gain); | |
| osc2.connect(gain); | |
| gain.connect(this.masterGain); | |
| gain.connect(this.delay.input); | |
| osc1.start(time); | |
| osc2.start(time); | |
| osc1.stop(end + release + 0.1); | |
| osc2.stop(end + release + 0.1); | |
| } | |
| start() { | |
| if (this.isRunning) return; | |
| this.isRunning = true; | |
| const beatMs = 60000 / this.bpm / 2; | |
| this._timerId = setInterval(() => this._tick(), beatMs); | |
| } | |
| stop() { | |
| this.isRunning = false; | |
| if (this._timerId) { | |
| clearInterval(this._timerId); | |
| this._timerId = null; | |
| } | |
| } | |
| _tick() { | |
| if (!this.isRunning) return; | |
| const mods = this._sampleModSources(); | |
| const now = this.ctx.currentTime + 0.05; | |
| const getMod = (target, fallback) => { | |
| const src = this.modRouting[target]; | |
| return (src && mods[src] !== undefined) ? mods[src] : fallback; | |
| }; | |
| const densityMod = getMod("density", 0.5); | |
| const colorMod = getMod("melodyColor", 0.5); | |
| const dTimeMod = getMod("delayTime", 0.5); | |
| const dFbMod = getMod("delayFeedback", 0.5); | |
| const masterMod = getMod("masterLevel", 0.5); | |
| const cutoffMod = getMod("filterCutoff", 0.5); | |
| const resMod = getMod("filterRes", 0.3); | |
| const panMod = getMod("panPos", 0.5); | |
| const padIntensity = getMod("padIntensity", 0.4); | |
| // keep it musical: density 0.35–0.85 | |
| const density = 0.35 + densityMod * 0.5; | |
| // fixed rhythm grid | |
| const pattern = [1, 0, 1, 1, 0, 1, 0, 1]; | |
| const active = pattern[this._step % pattern.length]; | |
| if (active && Math.random() < density) { | |
| // bass | |
| if (this._step % 4 === 0) { | |
| const bassChaos = (mods.timeLFO + mods.motionMagLFO) / 2; | |
| const bassFreq = this._pickNote(bassChaos, true); | |
| this._playVoice(bassFreq, now, 0.4 + 0.25 * (1 - densityMod), "triangle", 0.22, (panMod - 0.5) * 0.3); | |
| } | |
| // melody | |
| const chaos = (mods.screenLFO + mods.angleLFO + mods.randFast) / 3; | |
| const melFreq = this._pickNote(chaos, false); | |
| const waveTypes = ["sine", "triangle", "sawtooth", "square"]; | |
| const idx = Math.floor(colorMod * waveTypes.length) % waveTypes.length; | |
| const type = waveTypes[idx]; | |
| const dur = 0.18 + 0.35 * (1 - densityMod); | |
| this._playVoice(melFreq, now, dur, type, 0.16, (panMod - 0.5)); | |
| } | |
| // pad layer: slower | |
| if (this._step % 8 === 0 && Math.random() < padIntensity) { | |
| const padChaos = (mods.latLFO + mods.lonLFO + mods.randSlow) / 3; | |
| const padFreq = this._pickNote(padChaos, false, 1); // one octave up | |
| this._playPad(padFreq, now, 2.5 + padIntensity * 2, colorMod); | |
| } | |
| // FX / master controls | |
| const newTime = 0.18 + 0.32 * dTimeMod; | |
| const newFb = 0.2 + 0.45 * dFbMod; | |
| this.delay.setTime(newTime); | |
| this.delay.setFeedback(newFb); | |
| const masterLevel = 0.18 + 0.25 * masterMod; | |
| this.masterGain.gain.setValueAtTime(masterLevel, this.ctx.currentTime); | |
| const cutoffHz = 800 + cutoffMod * 7000; | |
| const qVal = 0.2 + resMod * 4; | |
| this.masterFilter.frequency.setValueAtTime(cutoffHz, this.ctx.currentTime); | |
| this.masterFilter.Q.setValueAtTime(qVal, this.ctx.currentTime); | |
| const pan = (panMod - 0.5) * 0.8; | |
| this.panner.pan.setValueAtTime(pan, this.ctx.currentTime); | |
| // occasional auto-chaos remap | |
| if (Math.random() < 0.02) { | |
| this._randomizeMapping(); | |
| } | |
| this._lastMods = { ...mods }; | |
| this._step++; | |
| } | |
| setKey(rootMidi, scaleName) { | |
| this.rootMidi = rootMidi; | |
| if (scaleName) this.scaleName = scaleName; | |
| } | |
| setBpm(bpm) { | |
| this.bpm = bpm; | |
| if (this.isRunning) { | |
| this.stop(); | |
| this.start(); | |
| } | |
| } | |
| chaosRemap() { | |
| this._randomizeMapping(); | |
| } | |
| _startHudLoop() { | |
| const hudGPS = this.hudEls.gps; | |
| const hudAccel = this.hudEls.accel; | |
| const hudAng = this.hudEls.angles; | |
| const hudMods = this.hudEls.mods; | |
| const loop = () => { | |
| // gps | |
| if (this.lastGeo) { | |
| const lat = this.lastGeo.latitude.toFixed(4); | |
| const lon = this.lastGeo.longitude.toFixed(4); | |
| hudGPS.textContent = `lat: ${lat}, lon: ${lon}`; | |
| } else { | |
| hudGPS.textContent = "lat: --, lon: --"; | |
| } | |
| // accel | |
| if (this.lastMotion) { | |
| const x = (this.lastMotion.x || 0).toFixed(2); | |
| const y = (this.lastMotion.y || 0).toFixed(2); | |
| const z = (this.lastMotion.z || 0).toFixed(2); | |
| hudAccel.textContent = `x:${x} y:${y} z:${z}`; | |
| } else { | |
| hudAccel.textContent = "x: -- y: -- z: --"; | |
| } | |
| const { pitch, roll } = this.lastAngles; | |
| hudAng.textContent = `pitch:${pitch.toFixed(1)}° roll:${roll.toFixed(1)}°`; | |
| // mods | |
| if (this._lastMods) { | |
| const m = this._lastMods; | |
| hudMods.textContent = | |
| `time:${m.timeLFO.toFixed(2)} ` + | |
| `scr:${m.screenLFO.toFixed(2)} ` + | |
| `mot:${m.motionMagLFO.toFixed(2)} ` + | |
| `ang:${m.angleLFO.toFixed(2)} ` + | |
| `lat:${m.latLFO.toFixed(2)} ` + | |
| `lon:${m.lonLFO.toFixed(2)} ` + | |
| `rf:${m.randFast.toFixed(2)} ` + | |
| `rs:${m.randSlow.toFixed(2)}`; | |
| } else { | |
| hudMods.textContent = ""; | |
| } | |
| requestAnimationFrame(loop); | |
| }; | |
| requestAnimationFrame(loop); | |
| } | |
| } | |
| // ---- UI wiring ---- | |
| const engine = new OSCPlusEngine(); | |
| let daw = null; | |
| const statusEl = document.getElementById("status"); | |
| const mappingEl = document.getElementById("mapping"); | |
| const videoEl = document.getElementById("cam"); | |
| const hudEls = { | |
| gps: document.getElementById("hud-gps"), | |
| accel: document.getElementById("hud-accel"), | |
| angles: document.getElementById("hud-angles"), | |
| mods: document.getElementById("hud-mods") | |
| }; | |
| function updateMappingDisplay() { | |
| if (!daw) return; | |
| mappingEl.textContent = "Mod routing:\n" + daw._describeMapping(); | |
| } | |
| document.getElementById("start").addEventListener("click", async () => { | |
| await engine.ensureRunning(); | |
| if (!daw) { | |
| daw = new ChaosMiniDAW( | |
| engine, | |
| { | |
| rootMidi: parseInt(document.getElementById("root").value, 10) || 60, | |
| scale: document.getElementById("scale").value, | |
| bpm: parseInt(document.getElementById("bpm").value, 10) || 90 | |
| }, | |
| videoEl, | |
| hudEls | |
| ); | |
| } | |
| daw.start(); | |
| statusEl.textContent = "Status: running (sensor-driven, in key, on grid)"; | |
| updateMappingDisplay(); | |
| }); | |
| document.getElementById("stop").addEventListener("click", () => { | |
| if (daw) daw.stop(); | |
| statusEl.textContent = "Status: stopped"; | |
| }); | |
| document.getElementById("chaos").addEventListener("click", () => { | |
| if (!daw) return; | |
| daw.chaosRemap(); | |
| updateMappingDisplay(); | |
| statusEl.textContent = "Status: routing scrambled via RNG (still musical)"; | |
| }); | |
| document.getElementById("root").addEventListener("input", (e) => { | |
| if (daw) { | |
| const v = parseInt(e.target.value, 10) || 60; | |
| daw.setKey(v, document.getElementById("scale").value); | |
| } | |
| }); | |
| document.getElementById("scale").addEventListener("change", (e) => { | |
| if (daw) { | |
| const root = parseInt(document.getElementById("root").value, 10) || 60; | |
| daw.setKey(root, e.target.value); | |
| } | |
| }); | |
| document.getElementById("bpm").addEventListener("input", (e) => { | |
| if (daw) { | |
| const bpm = parseInt(e.target.value, 10) || 90; | |
| daw.setBpm(bpm); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment