/showcases · Platform
Sechs interaktive Demos, eine Plattform: die Architektur
Wie AudioLab.tools sechs vollständig interaktive Demos zur Audioanalyse (MixLab, VoiceLab, HearLab, SignalLab, CueLab, SkillLab) aus einer einzigen Codebasis ausliefert, mit gemeinsam genutzter Analyse im Web Worker, zur Laufzeit synthetisierten Samples und null serverseitiger Audioverarbeitung.
Outcome
Sechs voll funktionsfähige interaktive Demos. Die schwere Analyse läuft außerhalb des Main Threads. Die UI bleibt selbst bei langen Dateien bei 60 fps. Es gehen keine Audiodaten über das Netzwerk.
Als der Masterplan sechs Labs verlangte, jedes mit einer „echten funktionierenden Demo, kein Screenshot“, lautete die technische Frage nicht, wie man eine Demo baut. Sie lautete, wie man sechs baut, ohne dass die Plattform zu einem unwartbaren Wirrwarr aus überlappenden Audiopipelines, react-Komponenten und Worker-Skripten wird.
Das ist die Architektur, auf die wir uns festgelegt haben. Sie hat klare Standpunkte, sie funktioniert, und sie hat die Abwägungen so gelöst, wie wir es wieder tun würden.
Die Form des Problems
Jedes Lab hat andere Anforderungen:
| Lab | Eingabe | Schwere Arbeit | Ausgabe |
|---|---|---|---|
| MixLab Analyzer | hochgeladenes Audio | BS.1770-4 LUFS + Spektrum | Metriken + Diagramm + Feedback |
| VoiceLab QA | Mikrofonaufnahme oder Upload | VAD + Raum-Echo + Zischlaute | Metriken + Timeline + Feedback |
| HearLab Companion | Live-Mikrofon | Web Speech API-Untertitel + Pegel | Untertitel + Log |
| SignalLab Indexer | hochgeladenes Audio | Klassifikation + Tags + Regionen | JSON + Timeline |
| CueLab Monitor | keine (Simulation) | State Machine + Animationen | Routing-Graph + Checkliste |
| SkillLab Challenge | Mikrofonaufnahme + Synth-Ziel | spektrales Vergleichs-Scoring | Score + Facetten-Aufschlüsselung |
Die gemeinsamen Eigenschaften: Jedes Lab braucht WebAudio + React-State + eine Ergebnisvisualisierung. Die abweichenden Eigenschaften: Eingabequelle, Analysealgorithmus, Ausgabeform.
Schicht 1: die static-first Shell
Jede Seite der Website wird statisch mit Astro gerendert. Die Demos leben in React-Islands, die nur bei Bedarf hydratisiert werden:
---
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" weist Astro an, das Server-Rendering vollständig zu überspringen. Das HTML lädt mit einem Platzhalter, dann übernimmt das React-Island, sobald es heruntergeladen ist. Für Audiodemos, die auf reine Browser-APIs angewiesen sind (WebAudio, MediaRecorder, Web Speech), ist das die richtige Wahl: es gibt nichts Sinnvolles zu SSR-en.
Der Rest der Seite (der Hero, die Use Cases, die Docs, die Roadmap) ist reines Astro-Markup. Keine Hydratationskosten.
Schicht 2: der einheitliche Analyse-Web Worker
Drei der Demos (MixLab, VoiceLab, SignalLab) führen schwere Analysen außerhalb des Main Threads durch. Der naive Ansatz wären drei separate Worker-Dateien. Wir haben den gegenteiligen Ansatz gewählt: ein Worker, drei Analysearten.
// 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) });
}
});
Warum ein Worker für drei Pipelines?
- Eine Worker-Instanz pro Sitzung. Keine Startkosten, wenn die Nutzerin innerhalb derselben Sitzung zwischen MixLab und VoiceLab wechselt.
- Ein Bundle. Vite erzeugt ein Worker-Bundle, das die drei Analysekerne enthält. Gesamtkosten: ~40 KB gzip-komprimiertes Worker-JS.
- Eine Client-API. Der Code auf Komponentenseite kann
analyzeBufferOffThread,analyzeVoiceOffThreadoderindexBufferOffThreadaufrufen und erhält ein sauberes Promise zurück. Dass sie sich einen Worker teilen, bleibt verborgen.
Der Client-Wrapper übernimmt die Request/Response-Korrelation mit monotonen 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));
});
}
Das channels.map(c => c.buffer) ist die Transferliste: die ArrayBuffers unter den Float32Arrays werden in den Worker transferiert, nicht kopiert. Null Serialisierungskosten.
Schicht 3: Sample-Synthese zur Laufzeit
Das nutzerseitige Problem bei Audio-Analysatoren ist, dass sie Audio brauchen. Die meisten Nutzer wollen keinen Track hochladen, um zu sehen, was ein Analysator tut. Sie wollen auf „Sample testen“ klicken und sofort Ergebnisse sehen.
Unsere Antwort: Synthese zur Laufzeit über 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;
}
Jedes Sample ist eine kleine DSP-Routine, die direkt in die Kanaldaten schreibt:
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…
// …
}
Der Trick ist, dass diese Samples echtes Audio sind, das der Analysator tatsächlich verarbeitet. Klick auf „Modern Master“ und der Analysator meldet -7 LUFS mit „Laut, wahrscheinlich überlimitiert”, weil das synthetisierte Signal wirklich laut und überlimitiert ist. Da wird nichts vorgetäuscht; wir haben einfach das Audio erzeugt, das der Analysator wirklich analysiert.
Null Kosten für binäre Assets. Deterministische Ausgabe. Dieselbe Engine, in die die Nutzerin hochladen würde.
Schicht 4: UI-Muster pro Cluster
Jede Demo hat ihre eigene React-Komponente. Sie teilen sich visuelle Primitive (Metric, FeedbackCard, Drop Zone), aber ihre Interaktionsformen unterscheiden sich genug, dass eine weitergehende Abstraktion verfrüht wäre.
- MixLabAnalyzer: doppelte Drop Zones (A/B-Referenzvergleich), worker-gestützte Analyse, Spektrum + LUFS-Timeline + Metriken + Feedback in Klartext.
- VoiceLabQA: Live-MediaRecorder + Drop Zone, stimmspezifische Metriken, Timeline mit Sprach-/Stille-Overlay.
- HearLabCompanion: Integration der Web Speech API, Live-Untertitel, Umgebungspegel, Check-in-Log mit JSON-Export.
- SignalLabIndexer: Tabs (Übersicht / Timeline / Tags / JSON), worker-gestützte Indexierung, herunterladbares strukturiertes JSON.
- CueLabMonitor: reine State Machine, kein Audio ein oder aus. Routing-Graph als SVG mit animierten Routenpartikeln gezeichnet. Pre-Show-Checkliste mit localStorage-Persistenz.
- SkillLabChallenge: synthetisiertes Ziel-Audio (WebAudio-Oszillatoren + Filter + Envelopes), MediaRecorder-Aufnahme, spektrales Vergleichs-Scoring.
Jede Komponente umfasst ~300–500 Zeilen TypeScript-React und teilt die Design Tokens der Plattform über Tailwind.
Was wir bewusst nicht getan haben
- Kein globaler State Manager. Jede Demo verwaltet ihren eigenen React-State. Die wenigen komponentenübergreifenden Teile (Theme, Command Palette) nutzen direkte DOM-APIs.
- Kein Backend. Jede Demo läuft vollständig im Browser. Es gehen keine Audiodaten über das Netzwerk. Das war eine harte Vorgabe, keine Performance-Optimierung.
- Keine verfrühte Abstraktion. Wir haben drei Analysatoren, die oberflächlich Ähnliches tun, aber ihre Algorithmen weichen genug voneinander ab, dass eine gemeinsame Abstraktion mehr verschleiern als klären würde.
Performance
| Metrik | Wert |
|---|---|
| Time to Interactive (Homepage) | ~0,8 s über Kabel |
| Cold Start MixLab Analyzer | ~150 ms bis Drop Zone sichtbar |
| Worker-Analyse bei 3-Minuten-Track | ~600 ms |
| Main Thread während der Analyse blockiert | ~0 ms (das ist Aufgabe des Workers) |
| Largest Contentful Paint | Hero-Bild, ~0,5 s |
Die gesamte Website, einschließlich der Demos, wird als statische Dateien gehostet. Es gibt überhaupt kein Backend.
Ergebnis
Sechs Labs. Sechs Demos. Eine Plattform. Alles im Browser, nichts auf einem Server. Die Architektur hat klare Standpunkte, die Entscheidungen sind bewusst getroffen, und das Ergebnis ist eine Website, auf der jede Demo wirklich funktioniert, keine Roadmap aus Demos, die es nicht tun.
Die Plattform ist live. Jetzt iterieren wir weiter.
Verwandt
More build logs
-
Pipeline für Markenbilder: sieben filmreife Bilder, ein Designsystem
7 filmreife Markenbilder generiert (Hero + 6 clusterspezifische), über die Build-Pipeline von Astro von 5–9 MB großen PNGs auf WebP-Varianten von 50–400 KB optimiert und anschließend in Clusterkarten, Hero-Bereiche und atmosphärische Markenmomente integriert.
-
Hero-Motion-Loop: vom Text-Prompt zum 460 KB großen Production-Asset
Eine dezente 5-Sekunden-Motion-Loop für den Homepage-Hero gebaut mit Higgsfield seedance 2.0 → ffmpeg → 252 KB WebM + 209 KB H.264 MP4 mit Bild-Fallback, ohne Audiospur, autoplay-freundlich und reduced-motion-respektierend.