From 67a4a32dea06a4736764df25d9e9ee89799cfa74 Mon Sep 17 00:00:00 2001 From: luciano Date: Fri, 19 Jun 2026 15:58:46 +0200 Subject: [PATCH] Waveform: scrolling amplitude envelope (song-style) tied to the speed slider Replaces the fast instantaneous oscilloscope with a mirrored filled waveform that scrolls over time like DJ-software waveforms; scroll rate follows the visual speed control. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/audio.js | 27 +++++++++++++++++++++++++-- src/output.js | 2 +- src/shaders.js | 21 ++++++++++++--------- src/visualizer.js | 3 ++- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/audio.js b/src/audio.js index e4680c6..b6fc058 100644 --- a/src/audio.js +++ b/src/audio.js @@ -53,6 +53,14 @@ class AudioEngine { this.NW = 256; this.wave = new Float32Array(this.NW); this.timeData = new Uint8Array(this.analyser.fftSize); + + // Scrolling amplitude history (song-style waveform envelope). + this.NH = 256; + this.waveHist = new Float32Array(this.NH); + this.histPeak = 0; + this.histAccum = 0; + this.PUSH_EVERY = 2.4; // frames between samples at scrollRate 1 (~40ms) + this.scrollRate = 1; // tied to the visual speed slider } resume() { if (this.ctx.state === 'suspended') this.ctx.resume(); } @@ -221,8 +229,23 @@ class AudioEngine { // Time-domain waveform. this.analyser.getByteTimeDomainData(this.timeData); const wstep = this.timeData.length / this.NW; + let peak = 0; for (let i = 0; i < this.NW; i++) { - this.wave[i] = (this.timeData[Math.floor(i * wstep)] - 128) / 128; + const s = (this.timeData[Math.floor(i * wstep)] - 128) / 128; + this.wave[i] = s; + const a = Math.abs(s); + if (a > peak) peak = a; + } + + // Scrolling waveform: accumulate the peak, push into history at a rate + // controlled by the speed slider (so it reads as a song's waveform). + this.histPeak = Math.max(this.histPeak, peak); + this.histAccum += Math.max(0.04, this.scrollRate); + while (this.histAccum >= this.PUSH_EVERY) { + this.histAccum -= this.PUSH_EVERY; + this.waveHist.copyWithin(0, 1); + this.waveHist[this.NH - 1] = this.histPeak; + this.histPeak = peak; } return this; @@ -234,7 +257,7 @@ class AudioEngine { } get values() { - return { bass: this.bass, mid: this.mid, treble: this.treble, level: this.level, beat: this.beat, spectrum: this.spectrum, wave: this.wave }; + return { bass: this.bass, mid: this.mid, treble: this.treble, level: this.level, beat: this.beat, spectrum: this.spectrum, wave: this.wave, waveHist: this.waveHist }; } } diff --git a/src/output.js b/src/output.js index 1e9f795..fa39740 100644 --- a/src/output.js +++ b/src/output.js @@ -249,7 +249,7 @@ djv.onControl(async (m) => { case 'recStart': startRecording(); break; case 'recStop': stopRecording(m); break; case 'gain': audio.gain = m.value; break; - case 'speed': speed = m.value; break; + case 'speed': speed = m.value; audio.scrollRate = m.value; break; case 'bandGain': audio[m.band + 'Gain'] = m.value; break; case 'outputDevice': try { await audio.setOutputDevice(m.deviceId || ''); } diff --git a/src/shaders.js b/src/shaders.js index a8e0c9e..ac1b11b 100644 --- a/src/shaders.js +++ b/src/shaders.js @@ -23,6 +23,7 @@ uniform vec3 uColorA, uColorB; uniform sampler2D uTex; // custom SVG / image source (uploaded flipped-Y) uniform float uSpectrum[32]; // live 32-band spectrum for VU meters uniform float uWave[256]; // live time-domain waveform (-1..1) +uniform float uWaveHist[256]; // scrolling amplitude history (song waveform) uniform float uBgDark; // 1 = force the empty field (v→0) to black float uT = 0.0; // time * effect speed (set in main) @@ -284,19 +285,21 @@ float famVUStereo(vec2 uv) { // ---- Waveform + band-reactive families ---- float waveAt(int i) { return uWave[i]; } -// Horizontal oscilloscope: smooth line via distance to the interpolated segment. +// Scrolling song waveform: a mirrored filled envelope that moves with time, +// like the waveform display in DJ software (newest sample at the right edge). float famWave(vec2 uv) { float xn = uv.x * 0.5 + 0.5; - if (xn < -0.02 || xn > 1.02) return 0.0; - float amp = 0.42; - float fx = clamp(xn, 0.0, 1.0) * 255.0; + if (xn < 0.0 || xn > 1.0) return 0.0; + float fx = xn * 255.0; int i0 = int(floor(fx)); int i1 = min(i0 + 1, 255); - vec2 p0 = vec2((float(i0) / 255.0 - 0.5) * 2.0, waveAt(i0) * amp); - vec2 p1 = vec2((float(i1) / 255.0 - 0.5) * 2.0, waveAt(i1) * amp); - vec2 pa = uv - p0, ba = p1 - p0; - float d = length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)); - return smoothstep(0.012, 0.0, d) + 0.35 * smoothstep(0.06, 0.0, d); + float amp = mix(uWaveHist[i0], uWaveHist[i1], fract(fx)) * 0.85; + float ay = abs(uv.y); + float fill = smoothstep(amp, amp - 0.012, ay); // solid body + float edge = smoothstep(0.014, 0.0, abs(ay - amp)); // bright crest + // a faint centre line so silence still reads as a waveform + float centre = smoothstep(0.006, 0.0, ay) * 0.4; + return clamp(fill * 0.55 + edge + centre, 0.0, 1.5); } // Radial oscilloscope: the waveform wrapped around a circle (interpolated). diff --git a/src/visualizer.js b/src/visualizer.js index baefbb4..fd829d2 100644 --- a/src/visualizer.js +++ b/src/visualizer.js @@ -26,7 +26,7 @@ class Visualizer { uFamily: U('uFamily'), uScale: U('uScale'), uRot: U('uRot'), uRotSpeed: U('uRotSpeed'), uSym: U('uSym'), uHueBase: U('uHueBase'), uHueCycle: U('uHueCycle'), uSat: U('uSat'), uContrast: U('uContrast'), uInvert: U('uInvert'), uWarp: U('uWarp'), uAudioMix: U('uAudioMix'), uSpeed: U('uSpeed'), - uColorA: U('uColorA'), uColorB: U('uColorB'), uTex: U('uTex'), uSpectrum: U('uSpectrum'), uBgDark: U('uBgDark'), uWave: U('uWave') + uColorA: U('uColorA'), uColorB: U('uColorB'), uTex: U('uTex'), uSpectrum: U('uSpectrum'), uBgDark: U('uBgDark'), uWave: U('uWave'), uWaveHist: U('uWaveHist') }; // Custom-source texture (for the SVG/Image effect family). Starts empty. @@ -126,6 +126,7 @@ class Visualizer { if (u.uSpectrum && audio.spectrum) gl.uniform1fv(u.uSpectrum, audio.spectrum); if (u.uWave && audio.wave) gl.uniform1fv(u.uWave, audio.wave); + if (u.uWaveHist && audio.waveHist) gl.uniform1fv(u.uWaveHist, audio.waveHist); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.tex);