Skip to content

Commit 3745eb6

Browse files
authored
Merge pull request #49 from sliterok/codex/fix-pulse-mode-flashing-on-bpm-change
Fix pulse bpm sync
2 parents 414fc40 + 2dd7acb commit 3745eb6

File tree

7 files changed

+71
-40
lines changed

7 files changed

+71
-40
lines changed

mic-sender.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import record from 'node-record-lpcm16'
1+
import mic from 'mic'
22
import WebSocket from 'ws'
33

44
const url = process.argv[2] || 'ws://localhost:8081'
55
const ws = new WebSocket(url)
66

77
ws.on('open', () => {
8-
const mic = record.start({ sampleRate: 44100, channels: 1 })
9-
mic.on('data', chunk => ws.send(chunk))
8+
const micInst = mic({ rate: '44100', channels: '1' })
9+
const micStream = micInst.getAudioStream()
10+
micInst.start()
11+
micStream.on('data', chunk => ws.send(chunk))
12+
micStream.on('error', console.error)
1013
})
14+

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@
5252
"d3-interpolate": "^3.0.1",
5353
"dotenv": "^16.4.5",
5454
"express": "^4.18.2",
55-
"fft-js": "^0.0.12",
56-
"music-tempo": "^1.0.3",
57-
"node-record-lpcm16": "^1.0.1",
55+
"fft-js": "^0.0.12",
56+
"mic": "^2.1.1",
57+
"essentia.js": "^0.1.3",
5858
"react": "^19.1.0",
5959
"react-colorful": "^5.6.1",
6060
"react-dom": "^19.1.0",

pnpm-lock.yaml

Lines changed: 24 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/backend/pattern/extra.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,27 @@ export const getStrobeColor: IColorGetter = (_, time) => {
4646
}
4747

4848
export const getPulseColor: IColorGetter = (_, time) => {
49-
const cycle = settings.syncToMusic && audioState.bpm ? 60000 / audioState.bpm : 1000
50-
const t = (time * settings.effectSpeed) % cycle
51-
if (t < lastPulse) {
49+
const target = settings.syncToMusic && audioState.bpm ? 60000 / audioState.bpm : 1000
50+
51+
if (!lastPulseTime) {
52+
lastPulseTime = time
53+
pulseCycle = target
54+
}
55+
56+
const dt = (time - lastPulseTime) * settings.effectSpeed
57+
lastPulseTime = time
58+
59+
const elapsed = pulsePhase * pulseCycle + dt
60+
pulseCycle = target
61+
pulsePhase = elapsed / pulseCycle
62+
63+
if (pulsePhase >= 1) {
64+
const cycles = Math.floor(pulsePhase)
65+
pulsePhase -= cycles
5266
pulseColor = hslToRgb(Math.random() * 360, 1, 0.5)
5367
}
54-
lastPulse = t
55-
const intensity = Math.sin((t / cycle) * Math.PI)
68+
69+
const intensity = Math.sin(pulsePhase * Math.PI)
5670
return pulseColor.map(c => Math.round(c * intensity)) as IArrColor
5771
}
5872

@@ -82,7 +96,9 @@ export const getMultiPulseColor: IColorGetter = (index, time) => {
8296
let strobeColor: IArrColor = [255, 255, 255]
8397
let lastStrobe = 0
8498
let pulseColor: IArrColor = [255, 0, 0]
85-
let lastPulse = 0
99+
let pulseCycle = 1000
100+
let pulsePhase = 0
101+
let lastPulseTime = 0
86102
let multiPulseColors: IArrColor[] = Array(4)
87103
.fill(null)
88104
.map(() => hslToRgb(Math.random() * 360, 1, 0.5))
@@ -95,7 +111,9 @@ export function resetExtraPatterns() {
95111
strobeColor = [255, 255, 255]
96112
lastStrobe = 0
97113
pulseColor = [255, 0, 0]
98-
lastPulse = 0
114+
pulseCycle = 1000
115+
pulsePhase = 0
116+
lastPulseTime = 0
99117
multiPulseColors = Array(4)
100118
.fill(null)
101119
.map(() => hslToRgb(Math.random() * 360, 1, 0.5))

src/backend/wsAudio.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { WebSocketServer } from 'ws'
22
import fftjs from 'fft-js'
3-
import MusicTempo from 'music-tempo'
3+
import { Essentia, EssentiaWASM } from 'essentia.js'
44
// eslint-disable-next-line import/default
55
import RingBufferTs from 'ring-buffer-ts'
66
const RingBuffer = RingBufferTs.RingBuffer
@@ -29,8 +29,9 @@ const sampleRateDefault = 44100
2929
const bpmWindow = sampleRateDefault * 8
3030
const sampleBuffer = new RingBuffer<number>(bpmWindow)
3131
let lastBpmUpdate = 0
32+
const essentiaPromise = Promise.resolve(new Essentia(EssentiaWASM))
3233

33-
export function processAudio(buffer: Buffer, sampleRate = sampleRateDefault) {
34+
export async function processAudio(buffer: Buffer, sampleRate = sampleRateDefault) {
3435
const samples = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2)
3536
const input = Array.from(samples, s => s / 32768)
3637
for (const s of input) sampleBuffer.add(s)
@@ -56,8 +57,10 @@ export function processAudio(buffer: Buffer, sampleRate = sampleRateDefault) {
5657
if (now - lastBpmUpdate > 2000 && sampleBuffer.getBufferLength() >= sampleRate * 4) {
5758
lastBpmUpdate = now
5859
try {
59-
const mt = new MusicTempo(Float32Array.from(sampleBuffer.toArray()))
60-
const tempo = parseFloat(String(mt.tempo))
60+
const essentia = await essentiaPromise
61+
const signal = essentia.arrayToVector(Float32Array.from(sampleBuffer.toArray()))
62+
const res = essentia.RhythmExtractor2013(signal, 208, 'multifeature', 40)
63+
const tempo = res.bpm
6164
if (!Number.isNaN(tempo)) audioState.bpm = tempo
6265
} catch {
6366
// ignore errors

src/types/essentia.js.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module 'essentia.js'

tests/wsAudio.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ describe('processAudio', () => {
2424
beforeEach(() => {
2525
resetAudioState()
2626
})
27-
test('detects frequency', () => {
27+
test('detects frequency', async () => {
2828
const buf = genSine(440, 1234, 44100)
29-
processAudio(buf)
29+
await processAudio(buf)
3030
expect(audioState.freq).toBeGreaterThan(430)
3131
expect(audioState.freq).toBeLessThan(450)
3232
})
3333

34-
test('detects bpm', () => {
34+
test('detects bpm', async () => {
3535
const buf = genBeat(120, 4, 44100)
36-
processAudio(buf)
36+
await processAudio(buf)
3737
expect(audioState.bpm).toBeGreaterThan(110)
3838
expect(audioState.bpm).toBeLessThan(130)
3939
})

0 commit comments

Comments
 (0)