DEV Community

hacker_ea
hacker_ea

Posted on

Real-Time Beat Detection in Web-Based DJ Applications

Real-Time Beat Detection in Web-Based DJ Applications

In modern DJ software, the ability to detect beats and BPM (Beats Per Minute) in real time is critical—for syncing tracks, visualizations, and live effects. In this article, we’ll explore how to implement a beat detection pipeline in a web context using:

  1. Web Audio API for client-side audio analysis
  2. Node.js + WebSockets for real-time event streaming
  3. PostgreSQL for persisting beat data
  4. Angular for visualizing beat events

By the end, you’ll have a blueprint for analyzing live audio and broadcasting beat events to front-end clients.


1. Beat Detection Algorithms

There are multiple approaches to detecting beats in audio. Here’s a quick comparison:

Algorithm Accuracy Time Complexity Best For
Autocorrelation Medium (±2 BPM) O(n²) Simple, offline analysis
Spectral Flux High (±0.5 BPM) O(n log n) Real-time, per-frame analysis
Onset Detection Very High (±0.1 BPM) O(n) Live DJ applications

Spectral Flux—which measures changes in the frequency spectrum over time—is a sweet spot for real-time accuracy and performance.


2. Client-Side: Web Audio API

We’ll use the Web Audio API to capture audio (e.g., from a local file or microphone), compute a spectrum, and run spectral-flux algorithm.

// Initialize AudioContext and AnalyserNode
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
const bufferLength = analyser.frequencyBinCount;
const freqData = new Float32Array(bufferLength);

// Load audio source (e.g., <audio> element)
const audioElement = document.querySelector('audio');
const sourceNode = audioCtx.createMediaElementSource(audioElement);
sourceNode.connect(analyser);
analyser.connect(audioCtx.destination);

// Spectral Flux variables
let lastSpectrum = new Float32Array(bufferLength);

function detectBeats() {
  analyser.getFloatFrequencyData(freqData);

  // Compute spectral flux
  let flux = 0;
  for (let i = 0; i < bufferLength; i++) {
    const value = Math.max(0, freqData[i] - lastSpectrum[i]);
    flux += value;
    lastSpectrum[i] = freqData[i];
  }

  // Threshold to determine beat
  if (flux > FLUX_THRESHOLD) {
    const timestamp = audioCtx.currentTime;
    onBeatDetected(timestamp);
  }

  requestAnimationFrame(detectBeats);
}

audioElement.onplay = () => {
  audioCtx.resume();
  detectBeats();
};
Enter fullscreen mode Exit fullscreen mode

Here, whenever flux exceeds a chosen threshold, we consider it a beat.


3. Streaming Beats with Node.js & WebSockets

To broadcast beats to multiple clients (e.g., visualizers, remote controllers), we’ll pipe beat events over a WebSocket.

// server.js (Node.js + ws)
const http = require('http');
const WebSocket = require('ws');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', ws => {
  console.log('Client connected');
  ws.on('message', msg => {
    // Handle incoming messages if needed
  });
});

// Expose an endpoint for clients to post beats
const express = require('express');
const app = express();
app.use(express.json());

app.post('/beat', (req, res) => {
  const { timestamp } = req.body;
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ event: 'beat', timestamp }));
    }
  });
  res.sendStatus(200);
});

server.on('request', app);
server.listen(3000, () => console.log('Listening on port 3000'));
Enter fullscreen mode Exit fullscreen mode

On the client side, modify onBeatDetected to POST the beat and/or send directly via WebSocket:

function onBeatDetected(timestamp) {
  // Send via WebSocket
  socket.send(JSON.stringify({ event: 'beat', timestamp }));
  // Optionally persist
  fetch('/beat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ timestamp }),
  });
}
Enter fullscreen mode Exit fullscreen mode

4. Persisting Beats in PostgreSQL

To analyze live sets later (e.g., generating BPM curves), store beats in a simple table:

Table Columns Description
beats id (PK) Auto-increment
track_id (FK) Associated track
timestamp (float8) AudioContext time (seconds)
received_at (timestamp) Server insertion time
CREATE TABLE beats (
  id SERIAL PRIMARY KEY,
  track_id INT REFERENCES tracks(id),
  timestamp DOUBLE PRECISION NOT NULL,
  received_at TIMESTAMPTZ DEFAULT NOW()
);
Enter fullscreen mode Exit fullscreen mode

Insert from Node.js:

// Using node-postgres
const { Pool } = require('pg');
const pool = new Pool();

async function saveBeat(trackId, timestamp) {
  await pool.query(
    'INSERT INTO beats(track_id, timestamp) VALUES($1, $2)',
    [trackId, timestamp]
  );
}
Enter fullscreen mode Exit fullscreen mode

5. Visualizing Beats in Angular

Leverage Angular’s change detection and RxJS to render beat pulses:

// beat.service.ts
import { Injectable } from '@angular/core';
import { webSocket } from 'rxjs/webSocket';

@Injectable({ providedIn: 'root' })
export class BeatService {
  private socket$ = webSocket('ws://localhost:3000');
  beat$ = this.socket$.multiplex(
    () => ({ subscribe: 'beat' }),
    () => ({ unsubscribe: 'beat' }),
    msg => msg.event === 'beat'
  );
}
Enter fullscreen mode Exit fullscreen mode
<!-- beat-visualizer.component.html -->
<div class="pulse" [class.active]="active"></div>
Enter fullscreen mode Exit fullscreen mode
// beat-visualizer.component.ts
import { Component, OnInit } from '@angular/core';
import { BeatService } from './beat.service';
import { timer } from 'rxjs';

@Component({
  selector: 'app-beat-visualizer',
  templateUrl: './beat-visualizer.component.html',
  styleUrls: ['./beat-visualizer.component.scss']
})
export class BeatVisualizerComponent implements OnInit {
  active = false;

  constructor(private beatService: BeatService) {}

  ngOnInit() {
    this.beatService.beat$.subscribe(() => this.flash());
  }

  flash() {
    this.active = true;
    timer(100).subscribe(() => (this.active = false));
  }
}
Enter fullscreen mode Exit fullscreen mode

Add some CSS:

.pulse {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: lightgray;
  transition: box-shadow 0.1s ease-in-out;
}
.pulse.active {
  box-shadow: 0 0 20px 5px cyan;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

By combining the Web Audio API for live spectral analysis, Node.js + WebSockets for streaming beat events, and a PostgreSQL backend for storage, you can build robust, real-time DJ applications. Integrating with Angular on the front end—complete with visualizers and controls—yields a fully-featured platform for DJs and audiences alike.

Feel free to adapt this pipeline for syncing tracks across devices, generating BPM-based lighting cues, or building collaborative online DJ experiences. Happy coding!

Top comments (0)