src/
└── audio/
└── playback/
├── index.ts # Module exports
├── synthPlayback.ts # ~290 lines
├── drumPlayback.ts # ~110 lines
└── samplerPlayback.ts # ~240 lines
src/hooks/useAudioEngine.ts- Reduced from ~715 lines to ~420 lines
Functions:
playSynth()- Main synth playback with multiple modes- Open303 (TB-303 clone) routing
- WAV buffer playback (wav-saw, wav-sqr)
- Standard oscillators (sawtooth, square, triangle, sine)
- ADSR envelope
- Filter and delay effects
noteOnSynth()- Interactive note triggernoteOffSynth()- Note releasestopAllSynthNotes()- Panic stopapply303Params()- Parameter mapping for 303
Types:
interface SynthPlaybackContext {
context: AudioContext;
masterGain: GainNode;
open303Engine?: Open303Oscillator | null;
wavSawBuffer: AudioBuffer | null;
wavSqrBuffer: AudioBuffer | null;
}
interface SynthNoteState {
nextNoteId: number;
activeNotes: Map<number, { stop: () => void }>;
}Functions:
playDrum()- Drum sound synthesis- Kick (oscillator with pitch envelope)
- Snare (tone + filtered noise)
- Hi-hats (high-pass filtered noise)
Types:
interface DrumPlaybackContext {
context: AudioContext;
masterGain: GainNode;
noiseBuffer: AudioBuffer | null;
}Functions:
playSampler()- Sample playback- One-shot and looped modes
- Time-stretching with SingingVoice
- Phoneme slicing mode
noteOnSampler()- Interactive triggernoteOffSampler()- Note release with envelopestopAllSamplerNotes()- Panic stoploadSampleToEngine()- Sample loadingprepareVocal()- Phoneme alignment
Types:
interface SamplerPlaybackContext {
context: AudioContext;
masterGain: GainNode;
singingVoice?: SingingVoice | null;
}
interface SamplerState {
loadedSampleBuffers: Map<string, AudioBuffer>;
vocalAlignments: Map<string, AlignmentResult>;
nextNoteId: number;
activeNotes: Map<number, { source: AudioBufferSourceNode; envGain: GainNode }>;
}-
Separation of Concerns
- Audio engine initialization (useAudioEngine.ts)
- Audio playback logic (playback modules)
- Each module has a single responsibility
-
Testability
- Playback functions can be tested in isolation
- No dependency on React hooks
- Pure functions with clear inputs/outputs
-
Maintainability
- Smaller, focused files
- Clear module boundaries
- Easier to find and modify specific functionality
-
Reusability
- Playback functions can be used in other contexts
- Not tied to the hook lifecycle
| Metric | Before | After | Change |
|---|---|---|---|
| useAudioEngine.ts | ~715 lines | ~420 lines | -41% |
| New playback files | 0 | 3 files | +640 lines |
| Total audio code | ~715 lines | ~1060 lines | +48% (separation) |
The playback functions are used in useAudioEngine.ts by creating context objects:
const synthCtx: SynthPlaybackContext = {
context,
masterGain,
open303Engine: open303EngineRef.current,
wavSawBuffer: wavSawBufferRef.current,
wavSqrBuffer: wavSqrBufferRef.current,
};
const drumCtx: DrumPlaybackContext = {
context,
masterGain,
noiseBuffer: noiseBufferRef.current,
};
const samplerCtx: SamplerPlaybackContext = {
context,
masterGain,
singingVoice: singingVoiceRef.current,
};Then wrapped for the AudioEngine interface:
const wrappedPlaySynth = (params: SynthParams, note: string, time: number, durationSteps?: number, stepTime?: number) => {
playSynth(synthCtx, params, note, time, durationSteps, stepTime);
};All existing tests pass (175/180):
- 5 pre-existing failures unrelated to refactoring
- No new test failures introduced
- TypeScript compiles without errors
Ready for Priority 3: Break up App.tsx