Remote calibration
Technical documentation for remote probe calibration functionality.
Overview
Remote calibration enables the VERO-BAAMBI client to calibrate a remote probe’s input trim without transferring raw audio data. The calibration logic executes on the client whilst the probe remains a lightweight metrics transmitter.
Architecture
┌─────────────────────────────────────────────────────────────────────────────┐│ CLIENT (index.html) ││ ││ ┌─────────────────┐ ┌────────────────────┐ ┌──────────────────────┐ ││ │ CalibrationEngine│◄──│ meterState (remote)│◄───│ handleRemoteMetrics()│ ││ │ │ └────────────────────┘ └──────────────────────┘ ││ │ - reads LUFS-I │ ▲ ││ │ - calculates Δ │ │ ││ │ - sends trim │ │ ││ └────────┬────────┘ │ ││ │ │ ││ ▼ │ ││ ┌─────────────────┐ │ ││ │ MetricsReceiver │────────────────────────────────────────┤ ││ │ │ │ ││ │ .sendTrim() │──┐ │ ││ └─────────────────┘ │ │ ││ │ │ │└───────────────────────│─────────────────────────────────────│──────────────┘ │ │ ▼ │ ┌─────────────────┐ │ │ BROKER │ │ │ (server.js) │ │ │ │ │ │ relays setTrim │ │ │ relays metrics │ │ └────────┬────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ PROBE (probe.html) │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ ProbeSender │ │ appState │ │ │ │ │ │ │ │ │ │ #handleSetTrim()│───►│ browserTrim │ │ │ │ │ │ externalTrim │ │ │ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌─────────────────┐ │ │ │ │SourceController │ │ │ │ │ applies trim │ │ │ │ └────────┬────────┘ │ │ │ │ │ │ │ ▼ │ │ │ ┌─────────────────┐ │ │ │ │ LUFSMeter │ │ │ │ │ TruePeakMeter │ │ │ ▼ └────────┬────────┘ │ │ ┌─────────────────┐ │ │ │ │MetricsCollector │◄────────────┘ │ │ │ sends metrics │─────────────────────────────────────► │ │ └─────────────────┘ to broker │ └─────────────────────────────────────────────────────────────┘Data Flow
Metrics Path (Probe → Client)
- Probe captures audio from browser tab or external device
LUFSMetercomputes momentary, short-term, and integrated loudnessMetricsCollector.collect()packages metrics with timestampProbeSendertransmits via WebSocket to broker- Broker relays to subscribed clients
MetricsReceiver.onMetrics()callback fires with parsed metricshandleRemoteMetrics()in bootstrap.js populatesmeterState:meterState.integratedLufs←metrics.lufs.integratedmeterState.shortTermLufs←metrics.lufs.shortTerm
Trim Path (Client → Probe)
CalibrationEnginereadsmeterState.integratedLufs- Calculates offset:
trimDelta = targetLufs - currentLufs - Calls
MetricsReceiver.sendTrim(probeId, newTrim) - Broker relays
{ type: 'setTrim', trimDb, inputMode }to probe ProbeSender.#handleSetTrim()receives command- Updates
appState.browserTrimorappState.externalTrim(SSOT) SourceController._handleStateChange()applies new trim to gain node- Next metrics packet reflects adjusted level
Implementation
CalibrationEngine Extensions
The engine gains a remote mode configured via setRemoteMode():
calibrationEngine.setRemoteMode({ enabled: true, probeId: 'uuid-here', metricsGetter: () => ({ momentary: meterState.lufsM, // if available shortTerm: meterState.shortTermLufs, integrated: meterState.integratedLufs, truePeak: Math.max(meterState.remoteTpL, meterState.remoteTpR) }), trimSender: (trimDb) => { remoteReceiver.sendTrim(selectedRemoteProbeId, trimDb); }, getTrim: () => { // Remote probe's current trim (if tracked) return remoteReceiver.getMetrics(probeId)?.trim ?? 0; }});Modified Methods
getCurrentReadings()
When remote mode is enabled, reads from metricsGetter instead of local meters:
getCurrentReadings() { if (!this._isCalibrating) return null;
if (this._remoteMode?.enabled) { const remote = this._remoteMode.metricsGetter(); return { momentary: remote.momentary ?? -Infinity, shortTerm: remote.shortTerm ?? -Infinity, integrated: remote.integrated ?? -Infinity, offset: isFinite(remote.integrated) ? remote.integrated - this._config.targetLufs : null, truePeak: remote.truePeak ?? -Infinity, confidence: this._calculateConfidence() }; }
// Local mode (existing code) const readings = this._lufsMeter.getReadings(); // ...}adjustTrim()
When remote mode is enabled, calls trimSender instead of local _setTrim:
adjustTrim(trimDb) { if (!this._isCalibrating || this._mode !== 'manual') return;
if (this._remoteMode?.enabled) { this._remoteMode.trimSender(trimDb); } else { this._setTrim?.(trimDb); }
// Reset integration after trim change this._resetMeters?.(); this._measurements = []; this._startTime = performance.now();}Bootstrap Wiring
In bootstrap.js, when entering remote calibration:
// When activeCapture === 'remote' and calibration startscalibrationEngine.setRemoteMode({ enabled: true, probeId: selectedRemoteProbeId, metricsGetter: () => ({ momentary: parseFloat(lufsM?.dataset?.v) || -Infinity, shortTerm: meterState.shortTermLufs, integrated: meterState.integratedLufs, truePeak: Math.max(meterState.remoteTpL, meterState.remoteTpR) }), trimSender: (trimDb) => { remoteReceiver.sendTrim(selectedRemoteProbeId, trimDb); }});When calibration ends or switching away from remote:
calibrationEngine.setRemoteMode({ enabled: false });Edge Cases
Probe Disconnection During Calibration
If probe goes offline mid-calibration:
meterStatevalues freeze at last received values- Stale detection triggers
probe.isOnline = false - UI should show warning and pause/cancel calibration
CalibrationEnginecan detect stale readings via timestamp check
Network Latency
Remote calibration loop has inherent latency:
- Metrics transmission: ~50-100ms
- Broker relay: ~1-5ms
- Trim command: ~50-100ms
- Audio processing delay: ~20ms
Total round-trip: ~150-250ms
Convergence algorithm should:
- Wait for stable readings before adjusting
- Use larger step sizes for initial correction
- Fine-tune with smaller steps once within ±2 dB
Trim Range Limits
Probe clamps trim to [-48, +24] dB range. If calibration requires trim outside this range, the source signal level is fundamentally wrong and hardware adjustment is required.
Testing Checklist
Automated Tests (test/remote-calibration-test.mjs)
Start system and run tests:
./tsg start & # Start broker + GUI with dashboardsleep 2node test/remote-calibration-test.mjsOr run tests directly (requires broker on port 8765):
- Probe connects and registers with broker
- Client subscribes to probe and receives metrics
- CalibrationEngine reads remote metrics via metricsGetter
- Trim commands sent via trimSender reach probe
- Metrics update correctly after trim adjustment
- Engine handles probe disconnect gracefully
- Rapid trim adjustments all processed correctly
- Extreme trim values (-48 to +24 dB) handled
Manual Browser Tests
- Start system:
./tsg start(shows TUI dashboard) - Open http://localhost:3000/probe.html in browser, start capture
- Open http://localhost:3000/ (main client), connect to remote, select probe
- Start manual calibration in remote mode
- Verify LUFS-I updates in calibration UI
- Adjust trim slider, verify probe receives command
- Verify probe metrics reflect new trim
- Complete calibration, verify profile saved
- Disconnect probe mid-calibration, verify graceful handling
Files Modified
| File | Change |
|---|---|
src/calibration/calibration-engine.js | Add setRemoteMode(), modify getCurrentReadings(), adjustTrim(), _pollManualMeasurement(), _measureLoop(), _completeAutoCalibration(), finaliseManualCalibration() |
src/app/bootstrap.js | Wire remote mode when activeCapture === 'remote', store probe info in appState, configure/disable engine remote mode |
src/ui/calibration-wizard.js | Support remoteProbeId/remoteProbeName from appState for device identification |
test/remote-calibration-test.mjs | Integration test for remote calibration flow |
docs/REMOTE-CALIBRATION.md | This documentation |
appState Keys
For remote calibration, two transient keys are used:
remoteProbeId- Probe UUID (not persisted)remoteProbeName- Human-readable name (not persisted)
Cleared when switching away from remote mode or stopping remote capture.
Last updated: 2024-12-30