Broadcast audio metering for production environments
A local-first, dependency-free implementation of EBU R128 loudness metering, True Peak detection, and Nordic PPM — designed for verification and monitoring in broadcast workflows.
VERO – "true" in Italian and Esperanto BAAMBI – Broadcast Audio Alignment & Metering for Broadcast Infrastructure
Part of the TSG Suite – tools for alignment, metering, and signal verification.
This application is built on the principle that local operation is not a fallback – it is the primary mode.
-
Zero external dependencies for core functionality
- No CDN imports, no npm packages in production
- No build step required – works from any static file server
- No analytics, no tracking, no phone-home
-
Privacy by design
- All audio processing happens in-browser
- No audio data leaves the device
- Settings stored locally in browser storage
- Remote features are opt-in, never opt-out
-
Offline-capable by default
- Full functionality without network access
- No service worker required for basic operation
- Works from
file://protocol using Chrome launcher (seelaunchers/), or via any HTTP server
-
Predictable behaviour
- Same code runs in development and production
- No environment-specific branches in core logic
- Feature flags are explicit, never hidden
-
Verifiable output
- Metering accuracy can be verified with reference signals
- Implements EBU R128, ITU-R BS.1770-4, IEC 60268-10 algorithms
- Calibration against known test signals documented
Remote metering is now fully implemented with a probe/broker/client architecture:
- Explicit user opt-in — disabled by default, enabled via UI toggle
- Local network first — broker runs on localhost, configurable for LAN
- Metrics only — no audio content transmitted, only numerical values (LUFS, True Peak, PPM, stereo)
- Graceful degradation — queues messages during disconnection, auto-reconnects with exponential backoff
- Zero impact on local mode — remote features are additive, local operation unaffected
See src/remote/ for implementation and broker/ for the relay server.
Device-specific calibration for reference-level alignment:
- Auto-calibration — internal 1 kHz @ −18 dBFS reference tone, 30-second measurement
- Manual calibration — user-adjustable trim with live offset display
- Profile storage — device-keyed profiles persist across sessions
- Status badge — shows calibration age, warns when stale (>30 days)
Supports EBU R128 (−23 LUFS), ATSC A/85 (−24 LKFS), and streaming (−16 LUFS) targets.
HTTP server (recommended):
python3 -m http.server 8080Then visit http://localhost:8080/ in your web browser.
Alternative — Chrome launcher:
Double-click the launcher script for your platform in launchers/. All local features work; see docs/deployment.md for security considerations and limitations.
This section provides step-by-step instructions for users who have never run a local HTTP server or used a terminal emulator before. Each step is a single action; verify success before proceeding to the next.
Before commencing, ensure you have the following:
-
Python 3 interpreter — Python is a programming language; the interpreter is the programme that executes Python code. You will use it to run a local HTTP server.
- Download from: https://python.org/downloads
- Version 3.8 or later is recommended
-
A web browser with Web Audio API support — The Web Audio API is a browser interface for processing and synthesising audio in real time.
- Recommended: Safari, Chrome, or Edge
- Note: Firefox has known limitations with sample rate matching; use an alternative if you encounter audio issues
-
Approximately 50 MB free disc space — For the source files and browser cache
-
A functioning audio input device — A built-in microphone suffices for initial testing; external audio interfaces work for production use
Before proceeding, verify that Python is installed correctly.
Opening a terminal emulator on macOS
- Press
Cmd+Spaceto open Spotlight Search - Type
Terminal - Press
Return
A window with a command prompt appears. This is the terminal emulator (a programme that provides a text-based interface to your operating system).
Opening a terminal emulator on Windows
Method 1:
- Press
Win+Rto open the Run dialogue - Type
cmd - Press
Enter
Method 2:
- Right-click the Start menu
- Select "Terminal" or "Command Prompt"
A window with a command prompt appears.
Opening a terminal emulator on Linux
Press Ctrl+Alt+T on most distributions. If this does not work, search your application menu for "Terminal".
Verify Python installation:
Type the following command and press Return (macOS/Linux) or Enter (Windows):
python3 --versionExpected output:
Python 3.x.x
where x.x is the minor and patch version (e.g., Python 3.11.4).
If the command fails on Windows
Windows may use python instead of python3. Try:
python --versionIf this also fails, Python is not in your PATH environment variable (the list of directories your operating system searches when you type a command). Reinstall Python and ensure you tick "Add Python to PATH" during installation.
- Open your web browser and navigate to the repository URL
- Click the green "Code" button
- Select "Download ZIP"
- Wait for the download to complete
Extracting the ZIP archive on macOS
Double-click the downloaded .zip file. The Archive Utility extracts it automatically, creating a directory with the same name (minus the .zip extension).
Extracting the ZIP archive on Windows
- Right-click the downloaded
.zipfile - Select "Extract All..."
- Choose a destination (the default is acceptable)
- Click "Extract"
Extracting the ZIP archive on Linux
In your file manager, right-click the .zip file and select "Extract Here". Alternatively, in the terminal emulator:
unzip filename.zipNote the full path to the extracted directory. Examples:
- macOS:
/Users/yourname/Downloads/tsg-vero-baambi-main - Windows:
C:\Users\yourname\Downloads\tsg-vero-baambi-main - Linux:
/home/yourname/Downloads/tsg-vero-baambi-main
HTTP server (recommended):
Serving via HTTP provides full functionality across all browsers, enables remote metering features, and matches production deployment. Any HTTP server works — Python's built-in server is used here for simplicity.
Alternative — Chrome launcher:
The scripts in launchers/ start Chrome with the --allow-file-access-from-files flag, which permits ES module loading and AudioWorklet on the file:// protocol. All local metering features work identically to HTTP mode.
Limitations of Chrome launcher:
- Chrome only (Firefox and Safari require HTTP for full functionality)
- Remote metering features require the broker server regardless of protocol
- The flag reduces browser security — use in trusted environments only
Why these restrictions exist: Browsers apply security restrictions to local files opened via file:// protocol. By default, Chrome blocks ES module imports, and all browsers block AudioWorklet.addModule(). These restrictions prevent arbitrary local files from executing code in security-sensitive contexts.
See docs/deployment.md for detailed compatibility information.
-
In your terminal emulator, navigate to the extracted directory using the
cdcommand (cdmeans "change directory"):cd /path/to/tsg-vero-baambi-mainReplace
/path/to/tsg-vero-baambi-mainwith the actual path you noted earlier.Windows path format
On Windows, use backslashes and include the drive letter:
cd C:\Users\yourname\Downloads\tsg-vero-baambi-main
-
Verify you are in the correct directory by listing its contents:
ls
(On Windows Command Prompt, use
dirinstead ofls)You should see files including
index.html,README.md, and asrcdirectory. -
Start the HTTP server:
python3 -m http.server 8080
(On Windows, use
pythonifpython3does not work) -
Observe the output:
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...This confirms the server is running. The terminal emulator is now blocked (occupied by the server process). This is expected behaviour.
-
Open your web browser
-
Type the following in the address bar and press
Return/Enter:http://localhost:8080Explanation:
localhostis an alias for your own machine (the IP address127.0.0.1). The number8080is the port you specified when starting the server. -
The VERO-BAAMBI interface loads:
- A boot splash displays for 777 milliseconds
- The main metering interface appears
-
Your web browser displays a permission dialogue requesting microphone access
-
Privacy note: The application processes all audio locally. No audio data leaves your machine. The browser requires explicit permission as a security measure.
-
Click "Allow"
-
If the interface appears but meters do not respond:
- Click anywhere on the page
- Web browsers require user interaction before the audio context (the Web Audio API component that processes audio) can start
- This is a security feature to prevent websites from playing unwanted audio, not a bug
-
Speak into your microphone or play audio. The meters should respond:
- The loudness radar displays a rotating sweep
- LUFS values update in real time
- The goniometer shows stereo activity
-
Return to the terminal emulator window (it may be behind other windows)
-
Press
Ctrl+CThis sends SIGINT (an interrupt signal) to the server process, instructing it to terminate.
-
The terminal emulator becomes interactive again, displaying a new command prompt
| Symptom | Probable Cause | Resolution |
|---|---|---|
python3: command not found |
Python not in PATH environment variable | Windows: try python instead. macOS/Linux: install Python from python.org |
Address already in use |
Port 8080 occupied by another process | Use a different port: python3 -m http.server 8081 and navigate to http://localhost:8081 |
| ES module error in console | Chrome blocks ES modules on file:// |
Use Chrome launcher in launchers/, or run HTTP server |
| AudioWorklet error in console | Browser blocks AudioWorklet on file:// |
Use Chrome launcher in launchers/, or run HTTP server |
| No audio input detected | Microphone not permitted or incorrect device selected | Check browser permission settings; select correct device in sidebar |
| Meters not moving | Audio context suspended | Click anywhere on the page to activate (browser autoplay policy) |
| Firefox sample rate mismatch | Firefox Web Audio API limitation | Use Safari, Chrome, or Edge instead |
| Page loads but appears broken | Browser cache contains old version | Hard refresh: Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows/Linux) |
You have successfully installed and run VERO-BAAMBI when:
- The loudness radar shows activity (a rotating sweep line)
- LUFS values (M, S, I) update in real time as audio plays
- Pressing
Spacepauses and resumes measurement
Next steps:
- Explore keyboard shortcuts in
docs/shortcuts.md - Read the Standards Implementation section below for technical details
- Try the signal generators in the sidebar for reference tones
server {
listen 80;
server_name meters.example.com;
root /var/www/vero-baambi;
# Serve index.html for root
location / {
try_files $uri $uri/ /index.html;
}
# ES modules require correct MIME type
location ~ \.js$ {
types { application/javascript js; }
}
# Cache static assets
location ~* \.(css|js|png|svg)$ {
expires 1d;
add_header Cache-Control "public, immutable";
}
}Create /etc/systemd/system/vero-broker.service:
[Unit]
Description=VERO-BAAMBI Metrics Broker
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/vero-baambi/broker
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
Environment=BROKER_PORT=8765
[Install]
WantedBy=multi-user.targetsudo systemctl enable vero-broker
sudo systemctl start vero-brokerversion: '3.8'
services:
broker:
build: ./broker
ports:
- "8765:8765" # WebSocket
- "8766:8766" # REST API
restart: unless-stopped
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./:/usr/share/nginx/html:ro
restart: unless-stoppedFor secure WebSocket (wss://), use a reverse proxy:
server {
listen 443 ssl;
server_name broker.example.com;
ssl_certificate /etc/letsencrypt/live/broker.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/broker.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
}Then use wss://broker.example.com as broker URL.
| Key | Action |
|---|---|
Space / P |
Play/Pause measurement (tap = toggle, hold = momentary) |
R |
Reset integrated loudness |
M |
Toggle sidebar menu |
F |
Toggle fullscreen |
1 2 3 4 5 |
Switch meter mode (TP / RMS / SP / Nordic / BBC) |
Full shortcuts reference: docs/shortcuts.md
| Parameter | Standard | Implementation |
|---|---|---|
| K-weighting pre-filter | ITU-R BS.1770-4 §2.1 | Two-stage biquad: high-pass (fc=38 Hz, Q=0.5) + high-shelf (+4 dB @ 4 kHz) |
| Momentary loudness (M) | EBU Tech 3341 | 400 ms sliding window, gated |
| Short-term loudness (S) | EBU Tech 3341 | 3 s sliding window, gated |
| Integrated loudness (I) | ITU-R BS.1770-4 §4 | Programme-length, dual-gated |
| Absolute gate | ITU-R BS.1770-4 §4.1 | −70 LUFS |
| Relative gate | ITU-R BS.1770-4 §4.2 | −10 LU below ungated integrated |
| Loudness Range (LRA) | EBU Tech 3342 | 95th − 10th percentile of gated short-term |
| Target loudness | EBU R128 | −23 LUFS (±1 LU tolerance) |
| Parameter | Standard | Implementation |
|---|---|---|
| Oversampling | ITU-R BS.1770-4 Annex 2 | 4× using Hermite interpolation |
| Maximum permitted level | EBU R128 | −1 dBTP |
| Streaming headroom | Industry practice | −2 dBTP (lossy codec margin) |
Implementation note: ITU-R BS.1770-4 Annex 2 specifies polyphase FIR reconstruction for laboratory-grade measurement. This implementation uses 4-point Hermite interpolation, which:
- Provides sufficient accuracy (±0.5 dB) for broadcast monitoring
- Requires ~8× less computation than polyphase FIR
- May miss edge-case intersample peaks in near-Nyquist content
Polyphase FIR coefficients and implementation guidance are documented in true-peak.js for future laboratory-grade implementation if required.
| Parameter | Standard | Implementation |
|---|---|---|
| Integration time | IEC 60268-10 Type I | 5 ms (quasi-peak) |
| Rise time to −1 dB | IEC 60268-10 Type I | 5 ms ± 0.5 ms |
| Fall time | IEC 60268-10 Type I | 20 dB in 1.7 s (≈11.76 dB/s linear) |
| Scale range | NTP 177-800 / EBU R68 | −40 to +18 PPM (58 dB) |
| Peak hold | RTW/DK convention | 3 s |
| Detector model | IEC 60268-10 | RC circuit (default) or window-max |
Quasi-peak detector: Two modes are available:
- RC detector (default): Models analogue rectifier + RC network per IEC 60268-10. Attack time constant τ ≈ 1.7 ms (−1 dB in 5 ms), decay τ ≈ 740 ms. Provides smooth, "analogue" response with correct tone-burst behaviour.
- Window mode: Simplified maximum-within-window approach. Faster computation but may over-read fast transients by ~1 dB.
| Context | Analogue Reference | Digital Reference | PPM Reading |
|---|---|---|---|
| EBU alignment tone | 0 dBu | −18 dBFS | 0 PPM |
| Nordic TEST level | +6 dBu | −12 dBFS | +6 PPM (TEST) |
| Permitted Maximum Level (PML) | +9 dBu | −9 dBFS | +9 PPM |
| SMPTE alignment (USA) | +4 dBu | −20 dBFS | — |
Conversion formula: PPM = dBFS + 18 (per EBU R68 alignment where 0 dBu = −18 dBFS)
| Measurement | Standard/Method | Range |
|---|---|---|
| Phase correlation | Pearson correlation coefficient | −1 (antiphase) to +1 (mono) |
| L/R balance | RMS level difference | −1 (full left) to +1 (full right) |
| Stereo width | M/S energy ratio (Side/Mid) | 0 (mono) to >1 (wide) |
| Mono compatibility threshold | Broadcast practice | Correlation < −0.3 = warning |
| Type | Standard | Purpose |
|---|---|---|
| GLITS | EBU Tech 3304 | Line-up and identification |
| Stereo identification | EBU Tech 3304 | L/R channel verification (pulsed) |
| Reference tones | — | 1 kHz @ −18 dBFS (EBU), −20 dBFS (SMPTE) |
| Pink/white/brown noise | — | Acoustic measurement, system noise floor |
| Lissajous patterns | — | Vectorscope/goniometer calibration |
| THÅST vector text | — | Goniometer branding via X/Y audio signals (AudioWorklet) |
The K-weighting implementation uses Web Audio API BiquadFilterNode for real-time efficiency:
- Stage 1: High-pass filter (fc=38 Hz, Q=0.5)
- Stage 2: High-shelf filter (+4 dB @ 4 kHz)
Exact ITU-R BS.1770-4 biquad coefficients for 48 kHz are included in k-weighting.js for reference and offline processing. The Web Audio approximation may deviate by ≤0.1 dB from the specification at extreme frequencies.
Sample rate consideration: Coefficients are calculated for 48 kHz. At 44.1 kHz, expect minor deviation (typically <0.2 dB) in high-frequency response.
PPM timing is implemented using requestAnimationFrame scheduling, not hardware timers. Practical implications:
- Attack time (5 ms) depends on audio buffer size and frame timing
- Decay rate (11.76 dB/s) is calculated per-frame using elapsed time
- Browser throttling (background tabs) will affect ballistics accuracy
For critical monitoring, use dedicated hardware meters.
The 4× oversampling uses Hermite interpolation between sample points:
Interpolation points: t = 0.25, 0.50, 0.75 between each sample pair
This catches most intersample peaks but may miss edge cases that a full polyphase FIR would detect. Typical deviation from "true" True Peak: <0.5 dB for normal programme material.
- Formal certification: No third-party laboratory has certified this implementation
- Guaranteed timing precision: Web browser scheduling is not deterministic
- Multi-channel support: Stereo only; BS.1770 5.1/7.1 channel weights not implemented
- Regulatory compliance: Not suitable as sole evidence for delivery QC
This tool is designed for:
- Quick verification during production
- Confidence monitoring alongside dedicated hardware
- Educational understanding of broadcast metering concepts
- Environments where installing dedicated software is impractical
It is not intended to replace certified measurement equipment for delivery QC or regulatory compliance.
node tests/metering-verification.jsRuns 35 synthetic signal tests against metering modules covering:
- dB/gain conversions
- RMS calculation (sine wave at 0.707× peak)
- Pearson correlation (mono, antiphase, uncorrelated)
- Hermite interpolation accuracy
- PPM ballistics (5 ms attack, 20 dB/1.7 s decay)
- True Peak intersample detection
- LUFS integration windows (400 ms, 3 s)
- Stereo width and balance calculations
Click VERIFY METERS in the sidebar to run 5 automated tests:
- LUFS accuracy — pink noise @ −23 LUFS (±0.3 LU tolerance)
- PPM calibration — 1 kHz @ −18 dBFS → TEST (0 PPM)
- Stereo decorrelation — 997/1003 Hz → correlation ≈ 0
- Mono correlation — L=R → correlation = +1.0
- Intersample peak — clipped sine → True Peak > 0 dBTP
Open tools/verify-audio.html in a browser for interactive verification with test tones.
See docs/verification.md for detailed test procedures using reference signals.
tsg-vero-baambi/
├── index.html # Main application (ES modules)
├── probe.html # Remote probe application
├── wallboard.html # NOC display for multiple probes
├── control.html # Remote control interface for headless probes
├── external-meter-processor.js # AudioWorklet for external sources
├── README.md # This file
├── smoke-checklist.md # Manual testing checklist
│
├── launchers/ # Platform-specific launcher scripts
│ ├── chrome-file-mode.command # macOS (double-click in Finder)
│ ├── chrome-file-mode.bat # Windows (double-click in Explorer)
│ └── chrome-file-mode.sh # Linux (run in terminal)
│
├── broker/ # Remote metering relay server
│ ├── server.js # WebSocket broker with rate limiting
│ ├── rest-api.js # REST API (Prometheus metrics, health)
│ └── package.json # Broker dependencies (ws only)
│
└── src/
├── main.js # Application entry, initialisation
│
├── audio/ # Audio processing utilities
│ ├── stereo-sampler.js # Dual-mode L/R synchronisation
│ └── stereo-sampler-worklet.js # AudioWorklet for atomic stereo capture
│
├── metering/ # Measurement algorithms
│ ├── lufs.js # EBU R128 / ITU-R BS.1770-4 loudness
│ ├── ppm.js # IEC 60268-10 Type I PPM (Nordic)
│ ├── true-peak.js # ITU-R BS.1770-4 Annex 2 intersample peak
│ ├── k-weighting.js # ITU-R BS.1770-4 pre-filter
│ └── correlation.js # Phase correlation & stereo analysis
│
├── generators/ # Signal generators
│ ├── oscillators.js # Sine, sweep, GLITS (EBU Tech 3304)
│ ├── noise.js # Pink, white, brown noise
│ ├── lissajous.js # Stereo test patterns
│ ├── thast-vector-text.js # THÅST X/Y signal generator
│ └── thast-vector-worklet.js # AudioWorklet for THÅST output
│
├── ui/ # Display components
│ ├── goniometer.js # Stereo vectorscope (Lissajous)
│ ├── correlation-meter.js # Phase correlation display
│ ├── bar-meter.js # LED bar renderers (TP, RMS, PPM)
│ └── spectrum.js # 1/3-octave spectrum analyser
│
├── app/ # Application integration
│ ├── bootstrap.js # Main initialisation & DOM wiring
│ ├── render-loop.js # 60 Hz visual rendering (RAF-based)
│ └── measure-loop.js # 20 Hz measurement updates
│
└── remote/ # Remote metering module
├── index.js # Remote module exports
├── types.js # Metrics schema (RemoteMetrics type)
├── transport/ # WebSocket communication
│ ├── index.js # Transport exports
│ └── websocket-client.js # Auto-reconnect WebSocket wrapper
├── probe/ # Metrics sender (source side)
│ ├── index.js # Probe exports
│ ├── probe-sender.js # Streams metrics to broker
│ └── metrics-collector.js # Gathers meter values
├── client/ # Metrics receiver (display side)
│ ├── index.js # Client exports
│ └── metrics-receiver.js # Receives and distributes metrics
└── ui/ # Remote UI components
├── index.js # UI exports
├── remote-panel.js # Toggle and status panel
└── remote-panel.css # Panel styling
| Browser | Minimum Version | Notes |
|---|---|---|
| Chrome | 72+ | AudioWorklet + getDisplayMedia |
| Edge | 79+ | Chromium-based |
| Firefox | 76+ | AudioWorklet since 76 |
| Safari | 14.1+ | AudioWorklet since 14.1 |
The application runs directly from source files. No transpilation, bundling, or build process needed.
GitHub Actions runs on every push:
- ESLint: Code style enforcement
- Syntax check:
node --checkon all source files - Type check:
tsc --noEmit --checkJs --stricton metering modules - Tests: 35 metering algorithm verification tests
For enhanced development experience:
- Live Server: Auto-reload on file changes
- TypeScript: JSDoc type checking without compilation
- ESLint: Code style enforcement
MIT Licence. Copyright 2025–2026 David Thåst.
Part of TSG Suite. Maintained by David Thåst · https://github.com/FiLORUX
Built with the assumption that behaviour should be predictable, output should be verifiable, and silence should mean silence.

