/showcases · Platform
Six démos interactives, une seule plateforme : l’architecture
Comment AudioLab.tools livre six démos d’analyse audio entièrement interactives (MixLab, VoiceLab, HearLab, SignalLab, CueLab, SkillLab) à partir d’une seule base de code, avec une analyse partagée dans un Web Worker, des échantillons synthétisés au runtime et zéro traitement audio côté serveur.
Outcome
Six démos interactives pleinement fonctionnelles. L’analyse lourde s’exécute hors du thread principal. L’interface reste à 60 fps, même sur des fichiers longs. Aucun audio ne transite par le réseau.
Quand le plan directeur a demandé six labs, chacun doté d’une « vraie démo fonctionnelle, pas une capture d’écran », la question d’ingénierie n’était pas comment construire une démo. C’était comment en construire six sans que la plateforme ne devienne un fouillis ingérable de pipelines audio, de composants react et de scripts de worker qui se chevauchent.
Voici l’architecture que nous avons retenue. Elle est tranchée, elle fonctionne, et elle a résolu les compromis de la façon dont nous les résoudrions à nouveau.
La forme du problème
Chaque lab a des exigences différentes :
| Lab | Entrée | Travail lourd | Sortie |
|---|---|---|---|
| MixLab Analyzer | audio téléversé | BS.1770-4 LUFS + spectre | métriques + graphique + retour |
| VoiceLab QA | capture micro ou téléversement | VAD + écho de pièce + sifflantes | métriques + chronologie + retour |
| HearLab Companion | micro en direct | sous-titres Web Speech API + niveau | sous-titres + journal |
| SignalLab Indexer | audio téléversé | classification + tags + régions | JSON + chronologie |
| CueLab Monitor | aucune (simulation) | machine à états + animations | graphe de routage + checklist |
| SkillLab Challenge | capture micro + cible synth | scoring par comparaison spectrale | score + ventilation par facette |
Les propriétés communes : chacun a besoin de WebAudio + état React + une visualisation du résultat. Les propriétés divergentes : la source d’entrée, l’algorithme d’analyse, la forme de la sortie.
Couche 1 : le shell static-first
Chaque page du site est rendue statiquement avec Astro. Les démos vivent à l’intérieur d’îlots React qui ne s’hydratent que lorsque c’est nécessaire :
---
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" indique à Astro d’ignorer entièrement le rendu côté serveur. Le HTML se charge avec un espace réservé, puis l’îlot React prend le relais une fois téléchargé. Pour des démos audio qui dépendent d’API propres au navigateur (WebAudio, MediaRecorder, Web Speech), c’est le bon choix : il n’y a rien de pertinent à faire en SSR.
Le reste de la page (le hero, les cas d’usage, la doc, la roadmap) est du markup Astro ordinaire. Aucun coût d’hydratation.
Couche 2 : le Web Worker d’analyse unifié
Trois des démos (MixLab, VoiceLab, SignalLab) effectuent une analyse lourde hors thread. L’approche naïve serait trois fichiers de worker distincts. Nous avons pris l’approche inverse : un seul worker, trois types d’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) });
}
});
Pourquoi un seul worker pour trois pipelines ?
- Une instance de worker par session. Aucun coût de démarrage lorsque l’utilisateur passe de MixLab à VoiceLab au cours d’une même session.
- Un seul bundle. Vite produit un bundle de worker qui inclut les trois cœurs d’analyse. Coût total : ~40 Ko de JS de worker compressé en gzip.
- Une seule API client. Le code côté composant peut appeler
analyzeBufferOffThread,analyzeVoiceOffThreadouindexBufferOffThreadet récupérer une Promise propre. Le fait qu’ils partagent un worker reste masqué.
Le wrapper client gère la corrélation requête/réponse avec des ids monotones :
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));
});
}
Le channels.map(c => c.buffer) est la liste de transfert : les ArrayBuffers sous-jacents aux Float32Array sont transférés au worker, pas copiés. Zéro coût de sérialisation.
Couche 3 : la synthèse d’échantillons au runtime
Le problème côté utilisateur avec les analyseurs audio, c’est qu’ils ont besoin d’audio. La plupart des utilisateurs ne veulent pas téléverser un morceau pour voir ce que fait un analyseur. Ils veulent cliquer sur « essayer un échantillon » et voir des résultats immédiatement.
Notre réponse : la synthèse au 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;
}
Chaque échantillon est une petite routine DSP qui écrit directement dans les données de canal :
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…
// …
}
L’astuce, c’est que ces échantillons sont du vrai audio que l’analyseur traite réellement. Cliquez sur « Modern master » et l’analyseur indique -7 LUFS avec « Fort, probablement sur-limité » parce que le signal synthétisé est effectivement fort et sur-limité. Rien n’est simulé ; nous avons simplement généré l’audio que l’analyseur analyse vraiment.
Zéro coût d’assets binaires. Sortie déterministe. Le même moteur que celui vers lequel l’utilisateur téléverserait.
Couche 4 : les motifs d’UI par cluster
Chaque démo a son propre composant React. Elles partagent des primitives visuelles (Metric, FeedbackCard, zone de dépôt), mais leurs formes d’interaction diffèrent assez pour qu’une abstraction plus poussée soit prématurée.
- MixLabAnalyzer : double zone de dépôt (comparaison de référence A/B), analyse adossée au worker, spectre + chronologie LUFS + métriques + retour en langage clair.
- VoiceLabQA : MediaRecorder en direct + zone de dépôt, métriques spécifiques à la voix, chronologie avec superposition parole/silence.
- HearLabCompanion : intégration de la Web Speech API, sous-titres en direct, niveau d’environnement, journal de check-in avec export JSON.
- SignalLabIndexer : onglets (Vue d’ensemble / Chronologie / Tags / JSON), indexation adossée au worker, JSON structuré téléchargeable.
- CueLabMonitor : machine à états pure, aucun audio en entrée ni en sortie. Graphe de routage dessiné en SVG avec des particules de route animées. Checklist d’avant-spectacle avec persistance localStorage.
- SkillLabChallenge : audio cible synthétisé (oscillateurs WebAudio + filtres + enveloppes), capture MediaRecorder, scoring par comparaison spectrale.
Chaque composant fait ~300-500 lignes de TypeScript-React et partage les design tokens de la plateforme via Tailwind.
Ce que nous avons délibérément évité de faire
- Aucun gestionnaire d’état global. Chaque démo possède son propre état React. Les rares éléments inter-composants (thème, palette de commandes) utilisent des API DOM directes.
- Aucun backend. Chaque démo s’exécute entièrement dans le navigateur. Aucun audio ne transite par le réseau. C’était une contrainte stricte, pas une optimisation de performance.
- Aucune abstraction prématurée. Nous avons trois analyseurs qui font des choses superficiellement similaires, mais leurs algorithmes divergent suffisamment pour qu’une abstraction partagée obscurcisse plus qu’elle ne clarifie.
Performance
| Métrique | Valeur |
|---|---|
| Time to interactive (page d’accueil) | ~0,8 s sur câble |
| Démarrage à froid de MixLab Analyzer | ~150 ms jusqu’à l’affichage de la zone de dépôt |
| Analyse worker sur un morceau de 3 min | ~600 ms |
| Thread principal bloqué pendant l’analyse | ~0 ms (c’est le travail du worker) |
| Largest contentful paint | image hero, ~0,5 s |
L’ensemble du site, démos comprises, est hébergé sous forme de fichiers statiques. Il n’y a aucun backend.
Résultat
Six labs. Six démos. Une plateforme. Tout dans le navigateur, rien sur un serveur. L’architecture est tranchée, les choix sont intentionnels, et le résultat est un site où chaque démo fonctionne vraiment, pas une roadmap de démos qui ne fonctionnent pas.
La plateforme est en ligne. Maintenant, nous continuons d’itérer.
À lire aussi
More build logs
-
Pipeline d’imagerie de marque : sept images cinématographiques, un seul système de design
7 images de marque cinématographiques générées (hero + 6 spécifiques aux clusters), optimisées de PNG de 5-9 Mo vers des variantes WebP de 50-400 Ko via la pipeline de build d’Astro, puis intégrées aux cartes de cluster, aux sections hero et aux temps forts atmosphériques de la marque.
-
Boucle de mouvement hero : du prompt textuel à l’asset de production de 460 Ko
Une subtile boucle de mouvement de 5 secondes construite pour le hero de la page d’accueil avec Higgsfield seedance 2.0 → ffmpeg → 252 Ko WebM + 209 Ko H.264 MP4 avec fallback image, sans piste audio, compatible autoplay et respectueuse de reduced-motion.