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) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.8 committed 2026-06-19 15:58:46 +02:00
1 parent eee3c63133
commit 67a4a32dea
4 files changed
+40 -13

No files matched your search

+25 -2
View File
@@ -53,6 +53,14 @@ class AudioEngine {
this.NW = 256; this.NW = 256;
this.wave = new Float32Array(this.NW); this.wave = new Float32Array(this.NW);
this.timeData = new Uint8Array(this.analyser.fftSize); 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(); } resume() { if (this.ctx.state === 'suspended') this.ctx.resume(); }
@@ -221,8 +229,23 @@ class AudioEngine {
// Time-domain waveform. // Time-domain waveform.
this.analyser.getByteTimeDomainData(this.timeData); this.analyser.getByteTimeDomainData(this.timeData);
const wstep = this.timeData.length / this.NW; const wstep = this.timeData.length / this.NW;
let peak = 0;
for (let i = 0; i < this.NW; i++) { 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; return this;
@@ -234,7 +257,7 @@ class AudioEngine {
} }
get values() { 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 };
} }
} }
+1 -1
View File
@@ -249,7 +249,7 @@ djv.onControl(async (m) => {
case 'recStart': startRecording(); break; case 'recStart': startRecording(); break;
case 'recStop': stopRecording(m); break; case 'recStop': stopRecording(m); break;
case 'gain': audio.gain = m.value; 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 'bandGain': audio[m.band + 'Gain'] = m.value; break;
case 'outputDevice': case 'outputDevice':
try { await audio.setOutputDevice(m.deviceId || ''); } try { await audio.setOutputDevice(m.deviceId || ''); }
+12 -9
View File
@@ -23,6 +23,7 @@ uniform vec3 uColorA, uColorB;
uniform sampler2D uTex; // custom SVG / image source (uploaded flipped-Y) uniform sampler2D uTex; // custom SVG / image source (uploaded flipped-Y)
uniform float uSpectrum[32]; // live 32-band spectrum for VU meters uniform float uSpectrum[32]; // live 32-band spectrum for VU meters
uniform float uWave[256]; // live time-domain waveform (-1..1) 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 uniform float uBgDark; // 1 = force the empty field (v→0) to black
float uT = 0.0; // time * effect speed (set in main) float uT = 0.0; // time * effect speed (set in main)
@@ -284,19 +285,21 @@ float famVUStereo(vec2 uv) {
// ---- Waveform + band-reactive families ---- // ---- Waveform + band-reactive families ----
float waveAt(int i) { return uWave[i]; } 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 famWave(vec2 uv) {
float xn = uv.x * 0.5 + 0.5; float xn = uv.x * 0.5 + 0.5;
if (xn < -0.02 || xn > 1.02) return 0.0; if (xn < 0.0 || xn > 1.0) return 0.0;
float amp = 0.42; float fx = xn * 255.0;
float fx = clamp(xn, 0.0, 1.0) * 255.0;
int i0 = int(floor(fx)); int i0 = int(floor(fx));
int i1 = min(i0 + 1, 255); int i1 = min(i0 + 1, 255);
vec2 p0 = vec2((float(i0) / 255.0 - 0.5) * 2.0, waveAt(i0) * amp); float amp = mix(uWaveHist[i0], uWaveHist[i1], fract(fx)) * 0.85;
vec2 p1 = vec2((float(i1) / 255.0 - 0.5) * 2.0, waveAt(i1) * amp); float ay = abs(uv.y);
vec2 pa = uv - p0, ba = p1 - p0; float fill = smoothstep(amp, amp - 0.012, ay); // solid body
float d = length(pa - ba * clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0)); float edge = smoothstep(0.014, 0.0, abs(ay - amp)); // bright crest
return smoothstep(0.012, 0.0, d) + 0.35 * smoothstep(0.06, 0.0, d); // 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). // Radial oscilloscope: the waveform wrapped around a circle (interpolated).
+2 -1
View File
@@ -26,7 +26,7 @@ class Visualizer {
uFamily: U('uFamily'), uScale: U('uScale'), uRot: U('uRot'), uRotSpeed: U('uRotSpeed'), uSym: U('uSym'), 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'), uHueBase: U('uHueBase'), uHueCycle: U('uHueCycle'), uSat: U('uSat'), uContrast: U('uContrast'),
uInvert: U('uInvert'), uWarp: U('uWarp'), uAudioMix: U('uAudioMix'), uSpeed: U('uSpeed'), 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. // 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.uSpectrum && audio.spectrum) gl.uniform1fv(u.uSpectrum, audio.spectrum);
if (u.uWave && audio.wave) gl.uniform1fv(u.uWave, audio.wave); 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.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex); gl.bindTexture(gl.TEXTURE_2D, this.tex);