/showcases · Platform
Zes interactieve demo’s, één platform: de architectuur
Hoe AudioLab.tools zes volledig interactieve demo’s voor audioanalyse (MixLab, VoiceLab, HearLab, SignalLab, CueLab, SkillLab) levert vanuit één codebase, met gedeelde analyse in een Web Worker, samples die tijdens runtime worden gesynthetiseerd en nul audioverwerking aan de serverkant.
Outcome
Zes volledig werkende interactieve demo’s. Zware analyse draait buiten de main thread. De UI blijft op 60 fps, zelfs bij lange bestanden. Er gaat geen audio over het netwerk.
Toen het masterplan vroeg om zes labs, elk met een “echte werkende demo, geen screenshot”, was de technische vraag niet hoe je een demo bouwt. Het was hoe je er zes bouwt zonder dat het platform verandert in een onhoudbare wirwar van overlappende audiopijplijnen, react-componenten en workerscripts.
Dit is de architectuur waar we op uitkwamen. Ze is uitgesproken, ze werkt, en ze loste de afwegingen op zoals we het opnieuw zouden doen.
De vorm van het probleem
Elk lab heeft andere vereisten:
| Lab | Invoer | Zwaar werk | Uitvoer |
|---|---|---|---|
| MixLab Analyzer | geüploade audio | BS.1770-4 LUFS + spectrum | metrieken + grafiek + feedback |
| VoiceLab QA | microfoonopname of upload | VAD + ruimte-echo + sisklanken | metrieken + tijdlijn + feedback |
| HearLab Companion | live microfoon | Web Speech API-ondertitels + meter | ondertitels + log |
| SignalLab Indexer | geüploade audio | classificatie + tags + regio’s | JSON + tijdlijn |
| CueLab Monitor | geen (simulatie) | state machine + animaties | routinggrafiek + checklist |
| SkillLab Challenge | microfoonopname + synth-doel | spectrale vergelijkingsscoring | score + facetuitsplitsing |
De gedeelde eigenschappen: elk lab heeft WebAudio + React-state + een resultaatvisualisatie nodig. De uiteenlopende eigenschappen: invoerbron, analysealgoritme, uitvoervorm.
Laag 1: de static-first shell
Elke pagina op de site wordt statisch gerenderd met Astro. De demo’s leven binnen React-islands die alleen hydrateren wanneer dat nodig is:
---
import MixLabAnalyzer from '@/components/MixLabAnalyzer';
import VoiceLabQA from '@/components/VoiceLabQA';
// ... etc
---
{cluster.slug === 'mixlab' && <MixLabAnalyzer client:only="react" />}
{cluster.slug === 'voicelab' && <VoiceLabQA client:only="react" />}
{cluster.slug === 'hearlab' && <HearLabCompanion client:only="react" />}
client:only="react" vertelt Astro om de serverrendering volledig over te slaan. De HTML laadt met een placeholder en daarna neemt het React-island het over zodra het is gedownload. Voor audiodemo’s die afhankelijk zijn van browser-only API’s (WebAudio, MediaRecorder, Web Speech) is dit de juiste keuze: er is niets zinvols om te SSR’en.
De rest van de pagina (de hero, de use cases, de docs, de roadmap) is gewone Astro-markup. Geen hydratatiekosten.
Laag 2: de uniforme analyse-Web Worker
Drie van de demo’s (MixLab, VoiceLab, SignalLab) doen zware analyse buiten de main thread. De naïeve aanpak zou drie aparte workerbestanden zijn. Wij kozen de tegenovergestelde aanpak: één worker, drie soorten analyse.
// worker.ts
import { analyzeChannels } from '../lib/audio-analysis-core';
import { analyzeVoiceChannels } from '../lib/voice-analysis-core';
import { indexChannels } from '../lib/signal-analysis-core';
self.addEventListener('message', async (e) => {
const { id, kind, channels, sampleRate, fileName, ...extra } = e.data;
const onProgress = (pct: number) =>
self.postMessage({ type: 'progress', id, pct });
try {
let result;
if (kind === 'audio') result = await analyzeChannels(channels, sampleRate, fileName, onProgress);
if (kind === 'voice') result = await analyzeVoiceChannels(channels, sampleRate, fileName, onProgress);
if (kind === 'signal') result = await indexChannels(channels, sampleRate, { fileName, ...extra }, onProgress);
self.postMessage({ type: 'result', id, result });
} catch (err) {
self.postMessage({ type: 'error', id, message: String(err) });
}
});
Waarom één worker voor drie pijplijnen?
- Eén worker-instantie per sessie. Geen opstartkosten wanneer de gebruiker binnen dezelfde sessie wisselt tussen MixLab en VoiceLab.
- Eén bundle. Vite produceert één workerbundle die de drie analysecores bevat. Totale kosten: ~40 KB aan gzip-gecomprimeerde worker-JS.
- Eén client-API. De code aan de componentkant kan
analyzeBufferOffThread,analyzeVoiceOffThreadofindexBufferOffThreadaanroepen en krijgt een nette Promise terug. Het feit dat ze een worker delen, blijft verborgen.
De client-wrapper handelt de request/response-correlatie af met monotone ids:
async function dispatch<T>(kind, buffer, extras, fallback) {
const worker = getWorker();
if (!worker) return fallback();
const channels = copyChannels(buffer);
const id = `req-${nextId++}`;
return new Promise<T>((resolve, reject) => {
const onMessage = (e: MessageEvent) => {
const msg = e.data;
if (msg.id !== id) return;
if (msg.type === 'progress') extras.onProgress?.(msg.pct);
else if (msg.type === 'result') { worker.removeEventListener('message', onMessage); resolve(msg.result); }
else if (msg.type === 'error') { worker.removeEventListener('message', onMessage); reject(new Error(msg.message)); }
};
worker.addEventListener('message', onMessage);
worker.postMessage({ type: 'analyze', kind, id, channels, ...extras }, channels.map(c => c.buffer));
});
}
De channels.map(c => c.buffer) is de transferlijst: de ArrayBuffers onder de Float32Array’s worden getransfereerd naar de worker, niet gekopieerd. Nul serialisatiekosten.
Laag 3: synthese van samples tijdens runtime
Het gebruikersgerichte probleem met audio-analysers is dat ze audio nodig hebben. De meeste gebruikers willen geen track uploaden om te zien wat een analyser doet. Ze willen op “probeer sample” klikken en meteen resultaten zien.
Ons antwoord: synthese tijdens runtime via OfflineAudioContext.
export async function synthesizeSample(id: SampleId): Promise<AudioBuffer> {
const sr = 48000;
const def = SAMPLES.find((s) => s.id === id);
const ctx = new OfflineAudioContext(2, Math.floor(sr * def.durationSec), sr);
const buffer = ctx.createBuffer(2, ctx.length, sr);
// Render kick + bass + lead + air directly into Float32Arrays
switch (id) {
case 'modern-master': renderModernMaster(buffer.getChannelData(0), buffer.getChannelData(1), sr); break;
case 'open-mix': renderOpenMix(...);
case 'boxy-room': renderBoxyRoom(...);
case 'voice-sample': renderVoiceSample(...);
}
return buffer;
}
Elke sample is een kleine DSP-routine die rechtstreeks in de kanaaldata schrijft:
function renderModernMaster(left: Float32Array, right: Float32Array, sr: number) {
// Kick on every 0.5s: 60 Hz body with downward sweep
renderInto(left, sr, (t) => {
const beat = t % 0.5;
const env = envelope(beat, 0.001, 0.18);
return Math.sin(2*Math.PI * (60 - 30*beat) * beat) * env;
}, 0.55);
// Bass + lead + air shimmer + master bus soft-clip…
// …
}
De truc is dat deze samples echte audio zijn die de analyser ook echt verwerkt. Klik op “Modern master” en de analyser rapporteert -7 LUFS met “Luid, waarschijnlijk over-gelimit” omdat het gesynthetiseerde signaal werkelijk luid en over-gelimit is. Er wordt niets nagebootst; we hebben simpelweg de audio gegenereerd die de analyser daadwerkelijk analyseert.
Nul kosten voor binaire assets. Deterministische uitvoer. Dezelfde engine waarnaar de gebruiker zou uploaden.
Laag 4: UI-patronen per cluster
Elke demo heeft zijn eigen React-component. Ze delen visuele primitieven (Metric, FeedbackCard, dropzone), maar hun interactievormen verschillen genoeg dat ze verder abstraheren voorbarig zou zijn.
- MixLabAnalyzer: dubbele dropzones (A/B-referentievergelijking), worker-gestuurde analyse, spectrum + LUFS-tijdlijn + metrieken + feedback in gewone taal.
- VoiceLabQA: live MediaRecorder + dropzone, stem-specifieke metrieken, tijdlijn met overlay voor spraak/stilte.
- HearLabCompanion: integratie met de Web Speech API, live ondertitels, omgevingsmeter, check-inlog met JSON-export.
- SignalLabIndexer: tabs (Overzicht / Tijdlijn / Tags / JSON), worker-gestuurde indexering, downloadbare gestructureerde JSON.
- CueLabMonitor: pure state machine, geen audio in of uit. Routinggrafiek getekend als SVG met geanimeerde route-deeltjes. Pre-show-checklist met localStorage-persistentie.
- SkillLabChallenge: gesynthetiseerde doelaudio (WebAudio-oscillators + filters + envelopes), MediaRecorder-opname, spectrale vergelijkingsscoring.
Elke component is ~300–500 regels TypeScript-React en deelt de design tokens van het platform via Tailwind.
Wat we bewust niet hebben gedaan
- Geen globale state manager. Elke demo beheert zijn eigen React-state. De paar component-overstijgende stukjes (thema, command palette) gebruiken directe DOM-API’s.
- Geen backend. Elke demo draait volledig in de browser. Er gaat geen audio over het netwerk. Dit was een harde randvoorwaarde, geen performance-optimalisatie.
- Geen voorbarige abstractie. We hebben drie analysers die oppervlakkig gezien vergelijkbare dingen doen, maar hun algoritmen lopen genoeg uiteen dat een gedeelde abstractie meer zou verhullen dan verduidelijken.
Performance
| Metriek | Waarde |
|---|---|
| Time to interactive (homepage) | ~0,8 s op kabel |
| Cold start MixLab Analyzer | ~150 ms tot dropzone zichtbaar |
| Workeranalyse op track van 3 min | ~600 ms |
| Main thread geblokkeerd tijdens analyse | ~0 ms (dat is de taak van de worker) |
| Largest contentful paint | hero-afbeelding, ~0,5 s |
De hele site, inclusief de demo’s, wordt gehost als statische bestanden. Er is helemaal geen backend.
Resultaat
Zes labs. Zes demo’s. Eén platform. Alles in de browser, niets op een server. De architectuur is uitgesproken, de keuzes zijn weloverwogen, en het resultaat is een site waar elke demo daadwerkelijk werkt, niet een roadmap van demo’s die het niet doen.
Het platform is live. Nu blijven we itereren.
Gerelateerd
More build logs
-
Pijplijn voor merkbeelden: zeven cinematische beelden, één designsysteem
7 cinematische merkbeelden gegenereerd (hero + 6 clusterspecifiek), geoptimaliseerd van 5–9 MB PNG’s naar WebP-varianten van 50–400 KB via de build-pijplijn van Astro, en vervolgens geïntegreerd in clusterkaarten, herosecties en sfeervolle merkmomenten.
-
Hero-motionloop: van tekstprompt naar productieasset van 460 KB
Een subtiele motionloop van 5 seconden gebouwd voor de homepage-hero met Higgsfield seedance 2.0 → ffmpeg → 252 KB WebM + 209 KB H.264 MP4 met afbeeldingsfallback, zonder audiospoor, autoplay-vriendelijk en met respect voor reduced-motion.