|
| 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 |
0 commit comments