Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules*
dist
.env
vscode-profile-*
deeplinkUsers.json
deeplinkUsers.json
coverage
39 changes: 34 additions & 5 deletions src/backend/wsAudio.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { WebSocketServer } from 'ws'
import fftjs from 'fft-js'
import MusicTempo from 'music-tempo'
// eslint-disable-next-line import/default
import RingBufferTs from 'ring-buffer-ts'
const RingBuffer = RingBufferTs.RingBuffer
Expand Down Expand Up @@ -30,6 +29,37 @@ const bpmWindow = sampleRateDefault * 8
const sampleBuffer = new RingBuffer<number>(bpmWindow)
let lastBpmUpdate = 0

function detectBpm(samples: number[], sampleRate: number): number {
const windowSize = 1024
const energies: number[] = []
for (let i = 0; i < samples.length; i += windowSize) {
let sum = 0
for (let j = 0; j < windowSize && i + j < samples.length; j++) {
const v = samples[i + j]
sum += v * v
}
energies.push(sum / windowSize)
}
if (energies.length < 3) return 0
const mean = energies.reduce((a, b) => a + b, 0) / energies.length
const threshold = mean * 1.5
const peaks: number[] = []
for (let i = 1; i < energies.length - 1; i++) {
if (energies[i] > threshold && energies[i] > energies[i - 1] && energies[i] > energies[i + 1]) {
peaks.push(i)
}
}
if (peaks.length < 2) return 0
const intervals = []
for (let i = 1; i < peaks.length; i++) intervals.push(((peaks[i] - peaks[i - 1]) * windowSize) / sampleRate)
intervals.sort((a, b) => a - b)
const median = intervals[Math.floor(intervals.length / 2)]
if (!median) return 0
const bpm = 60 / median
if (bpm < 60 || bpm > 180) return 0
return bpm
}

export function processAudio(buffer: Buffer, sampleRate = sampleRateDefault) {
const samples = new Int16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 2)
const input = Array.from(samples, s => s / 32768)
Expand All @@ -53,12 +83,11 @@ 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 * 2) {
lastBpmUpdate = now
try {
const mt = new MusicTempo(Float32Array.from(sampleBuffer.toArray()))
const tempo = parseFloat(String(mt.tempo))
if (!Number.isNaN(tempo)) audioState.bpm = tempo
const bpm = detectBpm(sampleBuffer.toArray(), sampleRate)
if (bpm) audioState.bpm = bpm
} catch {
// ignore errors
}
Expand Down