Audio System Architecture
Howler.js playback and Freesound integration for theatrical sound design.
Overview
On Book Pro's audio system provides:
- Sound Library — Searchable collection of clips
- Soundboard — Grid-based pads for instant SFX
- Cue List — Sequenced playback for performances
- Freesound Integration — Search and import public domain audio
Core Components
┌─────────────────────────────────────────────────────────┐
│ Sound Feature │
├─────────────┬─────────────┬─────────────┬───────────────┤
│ Library │ Soundboard │ Cue List │ Freesound │
│ (clips) │ (pads) │ (sequence) │ (search) │
└──────┬──────┴──────┬──────┴──────┬──────┴───────┬───────┘
│ │ │ │
└─────────────┴─────────────┴──────────────┘
│
┌──────▼──────┐
│ Howler.js │
│ (playback) │
└──────┬──────┘
│
┌──────▼──────┐
│ IndexedDB │
│ (cache) │
└─────────────┘Sound Clip Lifecycle
1. Import from Freesound
typescript
// Search Freesound API
const searchFreesound = async (query: string) => {
const response = await fetch(
`https://freesound.org/apiv2/search/text/?query=${query}&token=${API_KEY}`
);
return response.json();
};
// Preview a sound (streaming)
const previewSound = (previewUrl: string) => {
const howl = new Howl({
src: [previewUrl],
html5: true, // Stream without downloading
});
howl.play();
};2. Download and Cache Locally
typescript
// Download full audio file
const downloadClip = async (freesoundId: number): Promise<Blob> => {
const response = await fetch(
`https://freesound.org/apiv2/sounds/${freesoundId}/download/`,
{ headers: { Authorization: `Bearer ${token}` } }
);
return response.blob();
};
// Store in IndexedDB
const cacheLocally = async (clipId: string, blob: Blob) => {
const db = await openDB('sound-cache', 1);
await db.put('blobs', blob, `sound_blob_${clipId}`);
};3. Playback from Cache
typescript
// Load from IndexedDB
const loadFromCache = async (clipId: string): Promise<string> => {
const db = await openDB('sound-cache', 1);
const blob = await db.get('blobs', `sound_blob_${clipId}`);
return URL.createObjectURL(blob);
};
// Create Howl instance
const createHowl = async (clip: SoundClip) => {
const url = clip.localKey
? await loadFromCache(clip.id)
: clip.previewUrl;
return new Howl({
src: [url],
volume: clip.volume,
loop: clip.loop,
sprite: clip.trimEnd ? {
trimmed: [clip.trimStart * 1000, (clip.trimEnd - clip.trimStart) * 1000]
} : undefined,
});
};Waveform Visualization
The Sound feature includes a waveform visualizer for trim point selection:
typescript
// src/features/sound/waveform-visualizer.ts
export const drawWaveform = (
canvas: HTMLCanvasElement,
audioBuffer: AudioBuffer,
options: WaveformOptions
) => {
const ctx = canvas.getContext('2d');
const data = audioBuffer.getChannelData(0);
const step = Math.ceil(data.length / canvas.width);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = options.color || '#3b82f6';
for (let i = 0; i < canvas.width; i++) {
const min = Math.min(...data.slice(i * step, (i + 1) * step));
const max = Math.max(...data.slice(i * step, (i + 1) * step));
const y = ((1 + min) / 2) * canvas.height;
const height = Math.max(1, ((max - min) / 2) * canvas.height);
ctx.fillRect(i, y, 1, height);
}
};Soundboard Pad System
typescript
interface SoundPad {
id: string;
clipId: string;
position: number; // Grid position (0-15 for 4x4)
color: string;
label?: string;
}
// Trigger pad playback
const triggerPad = (pad: SoundPad) => {
const clip = clips.find(c => c.id === pad.clipId);
if (clip) {
const howl = howlInstances.get(clip.id);
howl?.play();
}
};IndexedDB Storage Pattern
typescript
// Sound-specific IndexedDB schema
import { openDB } from 'idb';
const initSoundDB = async () => {
return openDB('sound-cache', 1, {
upgrade(db) {
// Store audio blobs
if (!db.objectStoreNames.contains('blobs')) {
db.createObjectStore('blobs');
}
// Store waveform data (pre-computed)
if (!db.objectStoreNames.contains('waveforms')) {
db.createObjectStore('waveforms');
}
},
});
};Freesound API Integration
Authentication
Freesound uses OAuth2. Store the API key in Firebase Functions:
typescript
// Firebase Function (server-side)
export const searchFreesound = onCall(async (request) => {
const apiKey = process.env.FREESOUND_API_KEY;
const response = await fetch(
`https://freesound.org/apiv2/search/text/?query=${request.data.query}`,
{ headers: { Authorization: `Token ${apiKey}` } }
);
return response.json();
});Client Usage
typescript
import { httpsCallable } from 'firebase/functions';
const searchFreesound = httpsCallable(functions, 'searchFreesound');
const results = await searchFreesound({ query: 'thunder' });Best Practices
- Preload critical sounds — Load soundboard pads on tab mount
- Use IndexedDB for offline — Cache all imported clips locally
- Dispose Howl instances — Call
unload()when unmounting - Throttle waveform rendering — Don't redraw on every frame
Further Reading
Last updated: January 2026