WebAudioRecorder is a doctor-patient clinical audio recording platform built as a browser-only application. It's a Vite + React + TypeScript application with no backend - all data is stored locally in IndexedDB and Origin Private File System (OPFS).
For a high-performance clinical recorder handling 4-hour sessions on 2GB RAM devices, the architectural choice between these APIs is the difference between a stable application and a guaranteed crash. Below is the detailed breakdown with performance numbers and clinical-grade calculations.
| Feature | MediaRecorder | AudioWorklet (Chosen) |
|---|---|---|
| Primary Job | ""Black Box"" encoding to a file. | Raw PCM capture and processing |
| Latency | Variable/High. Managed by the browser's internal buffers. | Ultra-Low (~2.9ms). Operates on 128-sample quantums. |
| Raw Access | None. Only provides compressed blobs (VP8/Opus). | Full. Access to every individual Float32 sample |
| Thread | Main Thread / Browser Internals. | Dedicated Audio Thread. Isolated from UI ""jank"". |
| Pros | Simple API; low CPU for basic tasks. | High precision; real-time visualizers; no audio gaps. |
| Cons | No visualization; gaps in sequential data. | High complexity; requires separate Worker file. |
| Feature | IndexedDB | OPFS (SyncAccessHandle) |
|---|---|---|
| Primary Use | Metadata, WAL, & small state | High-performance binary storage |
| Performance | Slower; triggers UI ""jank"" with large blobs | Near-native synchronous I/O in workers |
| Pros | Widely supported; easy queryability | Scalable to GBs; atomic writes; isolated sandbox |
| Cons | RAM spikes during serialization | Requires Web Worker context for sync access |
Decision: OPFS was selected for binary storage to meet the 4-hour / 2GB RAM requirement, while IndexedDB handles metadata and the Write-Ahead Log.
Microphone → AudioWorklet (pcm-processor.js) → Main Thread → MP3 Web Worker → OPFS (encrypted) → IndexedDB
Zero-copy PCM processing: Uses transferable ArrayBuffers to avoid copying PCM data between threads.
Instead of encrypting/writing each MP3 chunk (4096 samples ≈ 93ms), the system accumulates MP3 frames in a 1MB RAM slab:
- Performance gain: Reduces encryption/write operations by ~1000x for typical recordings
- Crypto overhead: ~28 bytes per slab (12B IV + 16B GCM tag) = 0.0027% for 1MB
- WAL journaling: Each slab write is journaled for crash recovery
Prevents data corruption on crashes:
- Before each slab write: Record
{recordingId, chunkIndex, offset, status:'pending'}to IndexedDB - After successful flush: Update status to
'committed' - Crash recovery: Detect
status:'in_progress'recordings on app restart, truncate OPFS to last committed offset before decrypt
- Initial save: Recording meta data saved in IndexedDB
- OPFS persistence: OPFS files stay alive indefinitely until explicit deletion
- On-demand decryption:
lazyDecryptAndExport()fetches key, decrypts OPFS, creates blob - History operations: Play/download/transcribe all trigger lazy decryption from persistent OPFS
- Captures PCM from microphone at 44.1kHz
- Batches into 4096-sample chunks (93ms each)
- Transfers chunks via zero-copy (transferable ArrayBuffers)
- Handles pause/resume/flush operations
- Manages
AudioContextlifecycle - Sets up audio graph: Mic → Analyser → Worklet → Muted Gain
- Handles microphone permissions and errors
- Provides pause/resume/stop functionality
- Manages MP3 encoder Web Worker
- Creates sessions/recordings in IndexedDB
- Generates and stores AES-GCM encryption keys
- Handles periodic auto-save (every 30s)
- Manages lazy decryption and download
- Loads lamejs MP3 encoder via CDN bundle
- Converts PCM → MP3 at 128kbps
- Implements 1MB slab accumulation
- Handles OPFS file writes with encryption
- Manages WAL entries (worker-side IDB access)
// Core entities
patients: {id, name, dateOfBirth?, notes?, createdAt}
sessions: {id, patientId, createdAt, notes?}
recordings: {id, sessionId, patientId, blob: Blob|null, durationMs, title, transcript, status, createdAt}
// Security & recovery
recording-keys: {id: recordingId, value: CryptoKey}
recording-wal: {id, recordingId, chunkIndex, offset, status:'pending'|'committed'}- AES-GCM encryption with 256-bit keys
- File format:
[4B blockLen][12B IV][ciphertext+16B tag] - Additional authenticated data (AAD): chunkIndex as 8-byte LE
- Persistent until explicit deletion
- Finite state machine for recorder lifecycle
- States:
IDLE → REQUESTING_MIC → RECORDING → PAUSED → STOPPED → ENCODING → ENCODED → TRANSCRIBING → TRANSCRIBED - Timing management with pause/resume support
- Patient selection and transcript storage
- User selects patient, clicks "Start Recording"
- Auto-create session with timestamp
- Generate title: "Recording N – HH:MM"
- Pre-create recording in IDB with
status:'in_progress' - Generate AES-GCM key, save to
recording-keysstore - Initialize MP3 worker with sampleRate, bitRate, key, recordingId
- Start 30s auto-save interval
- Every 93ms: 4096 PCM samples → MP3 encoder → 1MB slab accumulation
- Every 30s:
partial-flush→ update duration in IDB (no slab flush) - WAL entries: Created before each slab write, committed after flush
- Flush remaining PCM from AudioWorklet
- Encode final MP3 tail
flushSlab(true)→ encrypt/write final partial slab- Close OPFS sync handle
- Update recording:
status:'complete', final duration - Keep OPFS file + key persistent
- Delete WAL entries (all committed)
lazyDecryptAndExport(recordingId)- Fetch key from
recording-keysstore - Decrypt OPFS file → create Blob
- For download: create object URL, trigger download
- OPFS file remains alive for future access
- App restart: detect
status:'in_progress'recordings - Show
CrashRecoveryModalwith affected recordings rollbackPendingWal(): truncate OPFS to last committed offset- Decrypt remaining valid data
- Finalize recording as usual
deleteRecordingFull(id): Delete OPFS file + key + WAL entries + IDB row- Cascade delete: Patient/session deletion cleans up all related recordings
- Zero-copy PCM: Transferable ArrayBuffers between AudioWorklet ↔ Main Thread ↔ Worker
- 1MB slab accumulation: Reduces crypto ops from per-chunk to per-slab
- Lazy blob creation: Blobs only created on demand, not stored in IDB
- OPFS as source of truth: Encrypted files in OPFS, metadata in IDB
- WAL minimalism: ~40 bytes per slab entry
- Cascade cleanup: Full cleanup on delete operations
- WAL journaling: Atomic write semantics
- Pending detection: Identify and recover interrupted recordings
- Partial flush safety: Auto-save updates duration without risking data
- Two tabs: Recorder (default) and Patients
- Crash recovery modal: Auto-shows on app restart with in-progress recordings
PatientSelector: Select/create patientsControls: Start/pause/stop buttons with state transitionsTimer: Real-time recording duration displayWaveform: Visual audio level display via AnalyserNodeExportButton: Trigger download of completed recordingsTranscriptPanel: Display/edit transcript (mock API integration)
PatientsPage: List all patients with creation/editingPatientCard: Compact patient displayPatientDetail: Detailed patient view with sessionsSessionCard: Session details with recordingsRecordingRow: Individual recording with play/download/deletePatientForm: Create/edit patient data
- Frontend: React 19 + TypeScript + Vite
- State: Zustand
- Audio: Web Audio API + lamejs (MP3 encoding)
- Storage: IndexedDB + OPFS
- Crypto: Web Crypto API (AES-GCM)
npm run dev: Vite dev server at http://localhost:5173npm run build: TypeScript compilation + Vite production build- Bundle size: ~233KB gzip main bundle
- Must be plain JavaScript in
public/worklets/(not bundled by Vite) - Loaded via
audioContext.audioWorklet.addModule('/worklets/pcm-processor.js')
- Gather IDs
BEFOREwrite transactions (no nested async in transactions) - Transactions auto-commit when no pending requests
- Worker-side IDB access separate from main thread
- AES-GCM 256-bit: Military-grade encryption
- Unique keys: Per-recording key generation
- IV(Initialisation Vector) per slab: Random 12-byte IV for each 1MB slab
- AAD integrity: chunkIndex authenticated but not encrypted
- No backend API calls (except mock transcription)
- No data leaves browser unless user exports
- Full patient data privacy by design
- Explicit deletion only: OPFS files persist until user deletes
- Cascade deletes: Patient → sessions → recordings → OPFS+keys+WAL
- No auto-cleanup: User controls all data lifecycle
- Short consultations: 5-15 minute recordings
- Long sessions: Up to 4+ hours (240 slabs, manageable WAL)
- Batch operations: Multiple patients/sessions per day
- MP3 bitrate: 128kbps = 16KB/s
- 1MB slab: ~64 seconds of audio
- 4-hour recording: ~225MB raw → ~240 slabs + WAL
- WAL overhead: ~9.6KB for 240 entries
- Required APIs: Web Audio, IndexedDB, OPFS, Web Workers, Crypto
- Modern browsers: Chrome/Edge 122+, Firefox 120+, Safari 17+
- Mobile considerations: AudioWorklet support varies
WebAudioRecorder is a sophisticated browser-based clinical audio recording system that prioritizes:
- Performance: Zero-copy audio, slab accumulation, lazy operations
- Reliability: WAL journaling, crash recovery, atomic writes
- Privacy: Local-only storage, AES-GCM encryption, explicit cleanup
- Usability: Simple UI, automatic session creation, easy export
The architecture represents a modern approach to web audio applications, leveraging cutting-edge browser APIs while maintaining robustness and user privacy.
Due to the time constraints of this assessment, the following features were deferred but are fully planned as part of the architectural vision for a production-ready medical tool.
For a 4-hour file (~230MB at 128kbps), standard uploads are unreliable. My planned approach for synchronization includes:
- Background Fetch API: To ensure uploads continue even if the clinician closes the browser tab or the device enters a Wi-Fi "dead zone".
- Multipart Uploads: Splitting the encrypted OPFS file into 5MB parts for S3-compatible cloud storage. This allows for granular retries—if one part fails, only that 5MB chunk is re-sent, not the entire 4-hour recording.
- Sync Queue: Extending the IndexedDB schema to track which 1MB slabs have been successfully acknowledged by the cloud before purging them from local storage.
To transition from "Local Security" to "Cloud Security," the key management strategy must be upgraded:
- Envelope Encryption: The local AES-256 session key would be wrapped (encrypted) using an RSA-OAEP public key from a Cloud Key Management Service (KMS). This ensures that the platform provider can never decrypt the audio without a hardware-secured private key.
- Identity-Bound Decryption: Expanding the AAD to include DoctorID and PatientID to cryptographically bind the audio data to a specific consultation record.
The architectural challenge is balancing high-fidelity medical accuracy with the strict Zero-Knowledge and 2GB RAM constraints of the device. The implementation would split into three distinct pipelines: Live (Streaming), On-Demand (Cloud-Batch), and On-Device (WASM).
Goal: Provide the clinician with real-time "draft" text during the consultation.
-
The "Tap" Mechanism: I would add a second MessagePort to the existing AudioWorkletNode. While one port sends 1MB slabs to the MP3 Encoder, the second port sends smaller 100ms PCM chunks to a dedicated TranscriptionWorker.
-
The WebSocket Relay: The TranscriptionWorker maintains a persistent WSS (Secure WebSocket) connection to a transcription engine (e.g., Whisper, Google Text To Speech).
-
UI Integration: Results are pushed to the useRecorderStore and displayed in a scrolling LiveTranscript component.
-
Security: Since this is "Live," the audio is streamed unencrypted in transit (via TLS). To maintain Zero-Knowledge, the provider must be configured with a "No-Store" policy, where audio is processed in RAM and immediately discarded.
Goal: High-accuracy, speaker-diarized, and punctuated clinical reports generated after the session.
- The Process: This utilizes Sync architecture. Once the encrypted MP3 is successfully uploaded to the cloud, the server triggers a batch job.
- Decryption at the Edge: If using a Zero-Knowledge model, the server cannot decrypt the file. The "On-Demand" request must be sent from the browser after local decryption.
- Implementation:
- Call lazyDecryptAndExport(recordingId) to get the plaintext Blob.
- Stream the Blob to a secure transcription endpoint.
- Store the resulting JSON transcript in the recordings IndexedDB store.
Goal: Maximum privacy. No audio data ever leaves the browser, satisfying the strictest interpretation of BSI TR-03161 and § 203 StGB.
-
The Tech Stack: Use a Whisper.cpp or Transformers.js build compiled to WebAssembly (WASM).
-
The 2GB RAM Challenge: This is the most difficult task. A standard Whisper "base" model requires ~250MB of RAM, but the "medium" model (required for medical accuracy) requires >1.5GB.
-
The Strategy:
- Quantization: I would use a 4-bit quantized (INT4) model to reduce the memory footprint by 75%.
- WebGPU Acceleration: Instead of CPU-bound WASM, use WebGPU to offload the heavy matrix multiplication to the GPU, which has its own memory pool, keeping the main 2GB RAM free for the OS and Browser.
- Chunked Decoding: Feed the model 30-second windows of audio retrieved from OPFS to prevent the memory heap from spiking.
| Feature | Live (Streaming) | On-Demand (Cloud) | On-Device (WASM) |
|---|---|---|---|
| Accuracy | Moderate (Draft) | Highest (Diarized) | High (Model dependent) |
| Privacy | TLS-Only | Zero-Knowledge (Cloud) | Absolute (Local-Only) |
| RAM Usage | Minimal (<10MB) | Moderate (~50MB) | High (250MB - 1GB) |
| Latency | <500ms | 30s - 2m (Post-session) | Variable (CPU speed) |
- Export Memory Pressure: The current
lazyDecryptAndExportimplementation loads the entire recording into memory for decryption. While safe for small files, a 4-hour recording on a 2GB RAM device would require a Streaming Decryptor pattern via a Service Worker to stay under 10MB during export. - Local-Only Persistence: Currently, if a user clears their browser's site data, all recordings and non-extractable keys are lost, as they are strictly sandboxed within IndexedDB and OPFS.
- Browser Compatibility: The solution relies on modern APIs like OPFS SyncAccessHandle and AudioWorklet, making it officially compatible only with recent versions of Chrome, Edge, and Safari (15.2+).
🇩🇪 Security & Compliance Alignment The architecture is designed to align with German medical standards:
- BSI TR-03161: Guidelines for secure medical applications (DiGA/DiPA).
- § 203 StGB: Protecting professional secrecy through End-to-End Encryption (E2EE) logic.
- GDPR/DSGVO: Local-first processing ensures no ePHI is transmitted unencrypted.