From 8f75e16363d6c4c34db6cd5f7196787ab1371efd Mon Sep 17 00:00:00 2001 From: sliterok <12751644+sliterok@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:26:22 +0200 Subject: [PATCH 1/2] feat: music sync --- src/backend/pattern/extra.ts | 9 ++++++--- src/backend/wsAudio.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/backend/pattern/extra.ts b/src/backend/pattern/extra.ts index 3603b41..c1e9ecd 100644 --- a/src/backend/pattern/extra.ts +++ b/src/backend/pattern/extra.ts @@ -7,7 +7,8 @@ import { audioState } from '../wsAudio' export const getHeartbeatColor: IColorGetter = (_, time) => { const beat = settings.syncToMusic && audioState.bpm ? 60000 / audioState.bpm : 1000 const cycle = beat - const t = (time * settings.effectSpeed) % cycle + const offset = settings.syncToMusic ? audioState.beatTime : 0 + const t = ((time - offset) * settings.effectSpeed) % cycle if (t < lastHeartbeat) { prevHeartbeat = nextHeartbeat @@ -36,7 +37,8 @@ function pulseIntensity(t: number, offset: number, cycle: number) { export const getStrobeColor: IColorGetter = (_, time) => { const interval = settings.syncToMusic && audioState.bpm ? 60000 / audioState.bpm : 200 - const t = (time * settings.effectSpeed) % interval + const offset = settings.syncToMusic ? audioState.beatTime : 0 + const t = ((time - offset) * settings.effectSpeed) % interval if (t < lastStrobe) { strobeColor = hslToRgb(Math.random() * 360, 1, 0.5) } @@ -47,7 +49,8 @@ export const getStrobeColor: IColorGetter = (_, time) => { export const getPulseColor: IColorGetter = (_, time) => { const cycle = settings.syncToMusic && audioState.bpm ? 60000 / audioState.bpm : 1000 - const t = (time * settings.effectSpeed) % cycle + const offset = settings.syncToMusic ? audioState.beatTime : 0 + const t = ((time - offset) * settings.effectSpeed) % cycle if (t < lastPulse) { pulseColor = hslToRgb(Math.random() * 360, 1, 0.5) } diff --git a/src/backend/wsAudio.ts b/src/backend/wsAudio.ts index 0690789..23c4f1f 100644 --- a/src/backend/wsAudio.ts +++ b/src/backend/wsAudio.ts @@ -11,10 +11,11 @@ export interface AudioState { level: number freq: number bpm: number + beatTime: number bins: number[] } -export const audioState: AudioState = { hue: 0, level: 0, freq: 0, bpm: 0, bins: [] } +export const audioState: AudioState = { hue: 0, level: 0, freq: 0, bpm: 0, beatTime: 0, bins: [] } export function startAudioServer(port = 8081) { const wss = new WebSocketServer({ port }) @@ -53,12 +54,18 @@ export function processAudio(buffer: Buffer, sampleRate = sampleRateDefault) { audioState.level = max / (mags.length / 2) const now = Date.now() - if (now - lastBpmUpdate > 2000 && sampleBuffer.getBufferLength() >= sampleRate * 4) { + if (now - lastBpmUpdate > 500 && sampleBuffer.getBufferLength() >= sampleRate * 4) { lastBpmUpdate = now try { - const mt = new MusicTempo(Float32Array.from(sampleBuffer.toArray())) + const data = Float32Array.from(sampleBuffer.toArray()) + const mt = new MusicTempo(data) const tempo = parseFloat(String(mt.tempo)) - if (!Number.isNaN(tempo)) audioState.bpm = tempo + if (!Number.isNaN(tempo)) { + audioState.bpm = audioState.bpm ? (audioState.bpm * 3 + tempo) / 4 : tempo + const duration = sampleBuffer.getBufferLength() / sampleRate + const beatSec = mt.beats[mt.beats.length - 1] ?? 0 + audioState.beatTime = now - (duration - beatSec) * 1000 + } } catch { // ignore errors } @@ -70,6 +77,7 @@ export function resetAudioState() { audioState.level = 0 audioState.freq = 0 audioState.bpm = 0 + audioState.beatTime = 0 audioState.bins = [] sampleBuffer.clear() lastBpmUpdate = 0 From e8e3399331dcb455f8dd3735d1c5cff1c63d5eb1 Mon Sep 17 00:00:00 2001 From: sliterok <12751644+sliterok@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:30:59 +0200 Subject: [PATCH 2/2] feat: better bpm --- src/backend/wsAudio.ts | 2 +- tests/wsAudio.test.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backend/wsAudio.ts b/src/backend/wsAudio.ts index 23c4f1f..682095f 100644 --- a/src/backend/wsAudio.ts +++ b/src/backend/wsAudio.ts @@ -27,7 +27,7 @@ export function startAudioServer(port = 8081) { } const sampleRateDefault = 44100 -const bpmWindow = sampleRateDefault * 8 +const bpmWindow = sampleRateDefault * 12 const sampleBuffer = new RingBuffer(bpmWindow) let lastBpmUpdate = 0 diff --git a/tests/wsAudio.test.ts b/tests/wsAudio.test.ts index b52fefa..ecf36c7 100644 --- a/tests/wsAudio.test.ts +++ b/tests/wsAudio.test.ts @@ -38,4 +38,11 @@ describe('processAudio', () => { expect(audioState.bpm).toBeLessThan(130) }) + test('detects slow bpm', () => { + const buf = genBeat(80, 8, 44100) + processAudio(buf) + expect(audioState.bpm).toBeGreaterThan(70) + expect(audioState.bpm).toBeLessThan(90) + }) + })