Skip to content

Commit a45ab82

Browse files
authored
Merge pull request #69 from sam-mfb/audio-upgrade
Audio upgrade
2 parents 81c6c4e + 38bcd3a commit a45ab82

36 files changed

+924
-214
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes
22

3+
## 1.0.5 - 2025.10.09
4+
5+
Upgrade Audio System to use AudioWorklet
6+
37
## 1.0.4 - 2025.10.06
48

59
Fix bug in ship contain

SOUND_UPDATE.md

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
# Sound Service Upgrade
2+
3+
## Status: Phase 1 Complete ✅
4+
5+
Phase 1 (AudioWorklet migration) has been completed and integrated into the game.
6+
7+
## Remaining Work
8+
9+
1. **No Sound Mixing**: Single-channel architecture with priority system prevents simultaneous sounds
10+
2. **No Background Music**: No support for continuous background music tracks
11+
12+
## Audio Format Recommendation
13+
14+
**Use OGG Vorbis** for background music:
15+
16+
**Pros**:
17+
18+
- Excellent compression (smaller files than MP3)
19+
- Better quality at same bitrate
20+
- No patent/licensing issues (fully open)
21+
- Well-supported in modern browsers (Chrome, Firefox, Edge, Safari 14.1+)
22+
- Native Web Audio API support
23+
24+
**Fallback**: Since we don't support old browsers (dropping ScriptProcessorNode), OGG-only is fine. All browsers that support AudioWorklet also support OGG.
25+
26+
For **generated SFX**: Keep current approach (procedural generation from original Mac code)
27+
28+
## Proposed Architecture
29+
30+
### Current Architecture (After Phase 1)
31+
32+
```
33+
src/core/
34+
├── sound/ ✅ AudioWorklet-based implementation
35+
│ ├── service.ts (current service using AudioWorklet)
36+
│ ├── soundEngine.ts
37+
│ ├── audioOutput.ts (uses AudioWorkletNode)
38+
│ ├── bufferManager.ts
39+
│ ├── worklet/
40+
│ │ ├── basicProcessor.worklet.ts
41+
│ │ └── worklet.d.ts
42+
│ ├── types.ts
43+
│ ├── index.ts (exports createSoundService)
44+
│ └── __tests__/
45+
46+
└── sound-shared/ ✅ Shared code
47+
├── constants.ts (SoundType, priorities, etc.)
48+
├── sampleGenerator.ts
49+
├── formatConverter.ts
50+
├── generators-asm/ (all procedural sound generators)
51+
│ ├── fireGenerator.ts
52+
│ ├── explosionGenerator.ts
53+
│ ├── thrusterGenerator.ts
54+
│ └── ...etc
55+
└── __tests__/
56+
```
57+
58+
### Future Architecture (Phase 2+)
59+
60+
When implementing Phase 2, we would add:
61+
62+
```
63+
src/core/
64+
├── sound/ (current AudioWorklet implementation)
65+
├── sound-modern/ (TODO: modern service with mixing + music)
66+
│ ├── service.ts
67+
│ ├── mixer.ts
68+
│ ├── musicPlayer.ts
69+
│ ├── soundEngine.ts
70+
│ ├── audioOutput.ts
71+
│ ├── worklet/
72+
│ │ └── mixerProcessor.worklet.ts
73+
│ ├── types.ts
74+
│ └── index.ts
75+
└── sound-shared/ (shared by both implementations)
76+
```
77+
78+
### Phase 1: Upgrade Current Service to AudioWorklet ✅ COMPLETE
79+
80+
**Goal**: Migrate current service to use AudioWorklet
81+
82+
**Completed Work**:
83+
84+
- ✅ Created `sound-shared/` directory with all shared code (generators, constants, utilities)
85+
- ✅ Replaced ScriptProcessorNode with AudioWorkletNode in `audioOutput.ts`
86+
- ✅ Created `worklet/basicProcessor.worklet.ts` with all audio processing in audio rendering thread
87+
- ✅ Used Vite's `?worker&url` import pattern to bundle worklet with TypeScript and dependencies
88+
- ✅ Implemented onEnded callback support for discrete sounds
89+
-**Kept exact same behavior**: single sound, priority-based
90+
-**Kept exact same API**: all existing methods unchanged
91+
- ✅ No mixing, no music - just modernized internals
92+
- ✅ Fixed unmute issue where continuous sounds weren't restarting
93+
- ✅ Integrated into game and verified all sounds work correctly
94+
- ✅ All 590 tests passing
95+
96+
**Result**: Drop-in replacement using modern, non-deprecated Web Audio APIs. The old ScriptProcessorNode-based implementation has been removed.
97+
98+
### Phase 2: Create Modern Service (TODO)
99+
100+
**Goal**: Build modern service in `sound-modern/` with mixing + music
101+
102+
**Remaining Work**:
103+
104+
- Create `sound-modern/` directory
105+
- Implement new `service.ts` with `SoundService` interface
106+
- All existing methods work identically
107+
- **Plus** new music methods (ModernSoundService type)
108+
- **Plus** multi-channel mixing for SFX
109+
- Create `mixer.ts` component
110+
- Create `musicPlayer.ts` component
111+
- Create `worklet/mixerProcessor.worklet.ts`
112+
- Update imports to use `sound-shared/`
113+
114+
**Note**: The current `sound/` directory now contains the AudioWorklet-based implementation and serves as the foundation for the modern service.
115+
116+
### Phase 3: Selection Mechanism (TODO)
117+
118+
When Phase 2 is complete, each directory would export its own factory:
119+
120+
```typescript
121+
// src/core/sound/index.ts
122+
export { createSoundService }
123+
export type { SoundService }
124+
125+
// src/core/sound-modern/index.ts
126+
export { createModernSoundService }
127+
export type { ModernSoundService }
128+
129+
// Usage in game code:
130+
import { createSoundService } from '@/core/sound'
131+
// OR (for new features)
132+
import { createModernSoundService } from '@/core/sound-modern'
133+
```
134+
135+
Could add a top-level selector if desired:
136+
137+
```typescript
138+
// Optional convenience wrapper
139+
export type SoundServiceType = 'basic' | 'modern'
140+
141+
export async function createSoundService(
142+
initialSettings: { volume: number; muted: boolean },
143+
type: SoundServiceType = 'modern'
144+
): Promise<SoundService> {
145+
if (type === 'basic') {
146+
return (await import('./sound')).createSoundService(initialSettings)
147+
} else {
148+
return (await import('./sound-modern')).createModernSoundService(
149+
initialSettings
150+
)
151+
}
152+
}
153+
```
154+
155+
## Detailed Architecture
156+
157+
### Multi-Channel Mixer (Modern Service Only)
158+
159+
```
160+
Mixer Architecture:
161+
┌─────────────────────────────────────┐
162+
│ Mixer (up to 8 simultaneous sounds) │
163+
├─────────────────────────────────────┤
164+
│ Channel 1: SFX (game sounds) │
165+
│ Channel 2: SFX (game sounds) │
166+
│ Channel 3: SFX (game sounds) │
167+
│ ... │
168+
│ Channel 7: SFX (game sounds) │
169+
│ Channel 8: Background Music │
170+
└─────────────────────────────────────┘
171+
↓ (mix all channels)
172+
AudioWorklet Output
173+
```
174+
175+
**Channel Management**:
176+
177+
- 7 channels for SFX (game sounds)
178+
- 1 dedicated channel for background music
179+
- Each channel has own buffer manager + generator
180+
- Mixer combines all active channels (simple addition)
181+
- Auto-cleanup: channels release when sound ends
182+
183+
**Priority System Evolution**:
184+
185+
- Keep existing priority for **backward compatibility**
186+
- Change behavior: priority now determines **channel selection** instead of blocking
187+
- High-priority sound can claim channel from lower-priority sound
188+
- Multiple copies of same sound can play if channels available
189+
- Falls back to current blocking behavior if all channels busy
190+
191+
### Background Music Support (Modern Service Only)
192+
193+
**New Service Methods** (backward compatible additions):
194+
195+
```typescript
196+
// Extends SoundService interface
197+
export type ModernSoundService = SoundService & {
198+
// New methods - don't break existing API
199+
playBackgroundMusic(
200+
trackId: string,
201+
options?: {
202+
loop?: boolean
203+
fadeIn?: number // ms
204+
volume?: number // 0-1, independent of SFX volume
205+
}
206+
): void
207+
208+
stopBackgroundMusic(options?: {
209+
fadeOut?: number // ms
210+
}): void
211+
212+
setBackgroundMusicVolume(volume: number): void
213+
}
214+
```
215+
216+
**Music System**:
217+
218+
- Dedicated channel 8 (never used for SFX)
219+
- Support for loading audio files (OGG)
220+
- Independent volume control
221+
- Crossfade support for smooth transitions
222+
- Optional loop mode
223+
- Music files stored in assets folder
224+
225+
### AudioWorklet Implementation
226+
227+
Both worklets will:
228+
229+
- Run in audio rendering thread (better performance)
230+
- Receive messages from main thread (play, stop, volume)
231+
- Generate samples using existing generator code
232+
- Post messages back (sound ended callbacks)
233+
234+
**Basic Worklet** (for original service):
235+
236+
- Single channel
237+
- Priority-based blocking
238+
- Exact current behavior
239+
240+
**Mixer Worklet** (for modern service):
241+
242+
- 8 channels (7 SFX + 1 music)
243+
- Mix all active channels
244+
- Priority determines channel allocation
245+
246+
## Benefits of This Approach
247+
248+
1. **Incremental Risk**: Upgrade existing service first, verify stability
249+
2. **Easy Comparison**: Can A/B test original vs modern
250+
3. **Backward Compatible**: Original behavior always available
251+
4. **Code Reuse**: Both share generators, buffer logic, constants
252+
5. **Clean Separation**: Easy to understand which is which
253+
254+
## Implementation Phases
255+
256+
### Phase 1: AudioWorklet Migration (Foundation) ✅ COMPLETE
257+
258+
**Completed**:
259+
260+
- ✅ Created `worklet/basicProcessor.worklet.ts`
261+
- ✅ Migrated buffer manager logic to worklet
262+
- ✅ Updated `audioOutput.ts` to use AudioWorkletNode
263+
- ✅ All 590 tests passing
264+
- ✅ Full backward compatibility maintained
265+
- ✅ Integrated into game and verified working
266+
267+
### Phase 2: Multi-Channel Mixer
268+
269+
- Build `mixer.ts` component
270+
- Create `worklet/mixerProcessor.worklet.ts`
271+
- Implement channel allocation
272+
- Update priority system to use channels
273+
- Create `serviceModern.ts`
274+
- **Ensure backward compatibility**
275+
276+
### Phase 3: Background Music
277+
278+
- Create `musicPlayer.ts`
279+
- Add music channel to mixer
280+
- Implement file loading (OGG)
281+
- Add fade in/out
282+
- Add new service methods to `serviceModern.ts`
283+
284+
## Testing Strategy
285+
286+
**Critical**: Existing tests must pass after each phase
287+
288+
- Unit tests for mixer
289+
- Integration tests for multi-sound playback
290+
- Backward compatibility tests (run existing game, verify no regressions)
291+
- Performance tests (worklet shouldn't add latency)
292+
293+
## Files to Create/Modify
294+
295+
### Phase 0: Shared Code Extraction ✅ COMPLETE
296+
297+
**Completed**:
298+
299+
- ✅ Created `src/core/sound-shared/constants.ts`
300+
- ✅ Created `src/core/sound-shared/sampleGenerator.ts`
301+
- ✅ Created `src/core/sound-shared/formatConverter.ts`
302+
- ✅ Moved `src/core/sound-shared/generators-asm/` (all 13 generators)
303+
- ✅ Moved shared tests to `src/core/sound-shared/__tests__/`
304+
305+
### Phase 1: AudioWorklet Service Files ✅ COMPLETE
306+
307+
**Completed in `sound/` directory**:
308+
309+
- ✅ `src/core/sound/service.ts` (upgraded for worklet)
310+
- ✅ `src/core/sound/soundEngine.ts` (updated imports)
311+
- ✅ `src/core/sound/audioOutput.ts` (upgraded to AudioWorkletNode)
312+
- ✅ `src/core/sound/bufferManager.ts` (kept for compatibility)
313+
- ✅ `src/core/sound/types.ts` (updated)
314+
- ✅ `src/core/sound/worklet/basicProcessor.worklet.ts` (new - runs in audio thread)
315+
- ✅ `src/core/sound/worklet/worklet.d.ts` (new - TypeScript definitions)
316+
- ✅ `src/core/sound/index.ts` (exports)
317+
- ✅ `src/core/sound/__tests__/` (all tests passing)
318+
319+
### Phase 2: Modern Service Files
320+
321+
New directory `sound-modern/` with:
322+
323+
- `src/core/sound-modern/service.ts` (new - extends SoundService)
324+
- `src/core/sound-modern/mixer.ts` (new)
325+
- `src/core/sound-modern/musicPlayer.ts` (new)
326+
- `src/core/sound-modern/soundEngine.ts` (new - mixer-aware)
327+
- `src/core/sound-modern/audioOutput.ts` (new - mixer output)
328+
- `src/core/sound-modern/types.ts` (new - ModernSoundService type, etc.)
329+
- `src/core/sound-modern/worklet/mixerProcessor.worklet.ts` (new)
330+
- `src/core/sound-modern/index.ts` (new - exports)
331+
- `src/core/sound-modern/__tests__/` (new - tests for mixer/music)
332+
333+
### Phase 3: Integration ✅ COMPLETE (for Phase 1)
334+
335+
**Completed**:
336+
337+
- ✅ Updated `src/game/store.ts` to use `@/core/sound`
338+
- ✅ Updated `src/game/soundListenerMiddleware.ts` to use `@/core/sound`
339+
- ✅ Updated `src/game/main.tsx` to use `@/core/sound`
340+
- ✅ Updated `src/dev/components/SoundTestPanel.tsx` to use `@/core/sound`
341+
- ✅ All game sounds verified working
342+
- ✅ Fixed unmute issue for continuous sounds
343+
344+
### Cleanup ✅ COMPLETE (for Phase 1)
345+
346+
**Completed**:
347+
348+
- ✅ Removed old ScriptProcessorNode-based implementation
349+
- ✅ All imports updated to use AudioWorklet-based system
350+
- ✅ All 590 tests passing
351+
- ✅ 4,943 lines of deprecated code removed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "continuum",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"type": "module",
55
"description": "Recreation of the 68000 Mac game Continuum for the web",
66
"scripts": {
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)