Skip to content

Commit 7e2a92d

Browse files
committed
feat: mic bpm
1 parent 93c6e7d commit 7e2a92d

File tree

5 files changed

+84
-25
lines changed

5 files changed

+84
-25
lines changed

mic-sender.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
1-
import record from 'node-record-lpcm16'
1+
import { Essentia, arrayToVector } from 'essentia.js'
2+
import mic from 'mic'
23
import WebSocket from 'ws'
34

4-
const url = process.argv[2] || 'ws://localhost:8081'
5-
const ws = new WebSocket(url)
5+
async function main() {
6+
const url = process.argv[2] || 'ws://localhost:8081'
7+
const ws = new WebSocket(url)
8+
await new Promise(resolve => ws.once('open', resolve))
9+
10+
const essentia = await Essentia()
11+
const sampleRate = 44100
12+
const bufferSizeSec = 5
13+
const bufferSize = sampleRate * bufferSizeSec
14+
let buffer = new Float32Array(0)
15+
16+
const rhythmAlgo = (signal) =>
17+
essentia.RhythmExtractor2013({ signal, sampleRate })
18+
19+
const micInst = mic({ rate: sampleRate.toString(), channels: '1' })
20+
const micStream = micInst.getAudioStream()
21+
22+
micInst.start()
23+
24+
micStream.on('data', (chunk) => {
25+
ws.send(chunk)
26+
const pcm = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4)
27+
const tmp = new Float32Array(buffer.length + pcm.length)
28+
tmp.set(buffer)
29+
tmp.set(pcm, buffer.length)
30+
buffer = tmp
31+
if (buffer.length >= bufferSize) {
32+
const sigVec = arrayToVector(buffer)
33+
const res = rhythmAlgo(sigVec)
34+
const bpm = res.bpm
35+
ws.send(JSON.stringify({ bpm }))
36+
console.log(`BPM: ${bpm.toFixed(2)}, ticks: ${res.ticks.length}`)
37+
buffer = buffer.subarray(bufferSize)
38+
}
39+
})
40+
41+
micStream.on('error', console.error)
42+
}
43+
44+
main().catch(console.error)
645

7-
ws.on('open', () => {
8-
const mic = record.start({ sampleRate: 44100, channels: 1 })
9-
mic.on('data', chunk => ws.send(chunk))
10-
})

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"express": "^4.18.2",
5555
"fft-js": "^0.0.12",
5656
"music-tempo": "^1.0.3",
57-
"node-record-lpcm16": "^1.0.1",
57+
"mic": "^2.1.1",
58+
"essentia.js": "^0.1.3",
5859
"react": "^19.1.0",
5960
"react-colorful": "^5.6.1",
6061
"react-dom": "^19.1.0",

pnpm-lock.yaml

Lines changed: 24 additions & 12 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: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,17 @@ export const getPulseColor: IColorGetter = (_, time) => {
5555

5656
const dt = (time - lastPulseTime) * settings.effectSpeed
5757
lastPulseTime = time
58-
pulsePhase += dt / pulseCycle
58+
59+
const elapsed = pulsePhase * pulseCycle + dt
60+
pulseCycle = target
61+
pulsePhase = elapsed / pulseCycle
5962

6063
if (pulsePhase >= 1) {
61-
pulsePhase %= 1
64+
const cycles = Math.floor(pulsePhase)
65+
pulsePhase -= cycles
6266
pulseColor = hslToRgb(Math.random() * 360, 1, 0.5)
6367
}
6468

65-
pulseCycle = target
66-
6769
const intensity = Math.sin(pulsePhase * Math.PI)
6870
return pulseColor.map(c => Math.round(c * intensity)) as IArrColor
6971
}

src/backend/wsAudio.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ export function startAudioServer(port = 8081) {
2020
const wss = new WebSocketServer({ port })
2121
wss.on('connection', ws => {
2222
ws.on('message', data => {
23-
if (Buffer.isBuffer(data)) processAudio(data)
23+
if (Buffer.isBuffer(data)) {
24+
processAudio(data)
25+
} else {
26+
try {
27+
const msg = JSON.parse(data.toString())
28+
if (typeof msg.bpm === 'number') audioState.bpm = msg.bpm
29+
} catch {
30+
// ignore non-json messages
31+
}
32+
}
2433
})
2534
})
2635
}

0 commit comments

Comments
 (0)