diff --git a/packages/wolf_3d_data/lib/src/wl_parser.dart b/packages/wolf_3d_data/lib/src/wl_parser.dart index 48b476c..bea9f07 100644 --- a/packages/wolf_3d_data/lib/src/wl_parser.dart +++ b/packages/wolf_3d_data/lib/src/wl_parser.dart @@ -363,7 +363,7 @@ abstract class WLParser { } /// Extracts AdLib sounds and IMF music tracks from the audio files. - static ({List adLib, List music}) parseAudio( + static ({List adLib, List music}) parseAudio( ByteData audioHed, ByteData audioT, GameVersion version, @@ -400,9 +400,9 @@ abstract class WLParser { // Chunks 174-260: Digitized Sounds int musicStartIndex = 261; - List adLib = allAudioChunks + List adLib = allAudioChunks .take(musicStartIndex) - .map((bytes) => AdLibSound(bytes)) + .map((bytes) => PcmSound(bytes)) .toList(); List music = allAudioChunks diff --git a/packages/wolf_3d_data_types/lib/src/sound.dart b/packages/wolf_3d_data_types/lib/src/sound.dart index faa8632..072ad40 100644 --- a/packages/wolf_3d_data_types/lib/src/sound.dart +++ b/packages/wolf_3d_data_types/lib/src/sound.dart @@ -5,11 +5,6 @@ class PcmSound { PcmSound(this.bytes); } -class AdLibSound { - final Uint8List bytes; - AdLibSound(this.bytes); -} - class ImfInstruction { final int register; final int data; diff --git a/packages/wolf_3d_data_types/lib/src/wolfenstein_data.dart b/packages/wolf_3d_data_types/lib/src/wolfenstein_data.dart index 1bf3bb6..5ea80a9 100644 --- a/packages/wolf_3d_data_types/lib/src/wolfenstein_data.dart +++ b/packages/wolf_3d_data_types/lib/src/wolfenstein_data.dart @@ -5,7 +5,7 @@ class WolfensteinData { final List walls; final List sprites; final List sounds; - final List adLibSounds; + final List adLibSounds; final List music; final List vgaImages; final List episodes; diff --git a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart index 08e7c00..a830d82 100644 --- a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart +++ b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart @@ -12,8 +12,7 @@ export 'src/game_file.dart' show GameFile; export 'src/game_version.dart' show GameVersion; export 'src/image.dart' show VgaImage; export 'src/map_objects.dart' show MapObject; -export 'src/sound.dart' - show PcmSound, AdLibSound, ImfMusic, ImfInstruction, WolfMusicMap; +export 'src/sound.dart' show PcmSound, ImfMusic, ImfInstruction, WolfMusicMap; export 'src/sprite.dart' hide Matrix; export 'src/sprite_frame_range.dart' show SpriteFrameRange; export 'src/wolf_level.dart' show WolfLevel; diff --git a/packages/wolf_3d_flutter/lib/wolf_3d.dart b/packages/wolf_3d_flutter/lib/wolf_3d.dart index a0fa8de..bcc69e7 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d.dart @@ -38,7 +38,7 @@ class Wolf3d { List get walls => activeGame.walls; List get sprites => activeGame.sprites; List get sounds => activeGame.sounds; - List get adLibSounds => activeGame.adLibSounds; + List get adLibSounds => activeGame.adLibSounds; List get music => activeGame.music; List get vgaImages => activeGame.vgaImages; diff --git a/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart index e27ee7e..701277b 100644 --- a/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart +++ b/packages/wolf_3d_synth/lib/src/wolf_3d_audio.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:audioplayers/audioplayers.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_synth/src/imf_renderer.dart'; @@ -5,20 +7,38 @@ import 'package:wolf_3d_synth/src/imf_renderer.dart'; class WolfAudio { bool _isInitialized = false; + // --- Music State --- final AudioPlayer _musicPlayer = AudioPlayer(); + // --- SFX State --- + // A pool of players to allow overlapping sound effects. + static const int _maxSfxChannels = 8; + final List _sfxPlayers = []; + int _currentSfxIndex = 0; + WolfensteinData? activeGame; - /// Initializes the audio engine. + /// Initializes the audio engine and pre-allocates the SFX pool. Future init() async { if (_isInitialized) return; try { - // audioplayers doesn't require complex global initialization like SoLoud, - // but setting the audio context can be useful for mobile platforms later. + // Set music player mode await _musicPlayer.setPlayerMode(PlayerMode.mediaPlayer); + + // Initialize the SFX pool + for (int i = 0; i < _maxSfxChannels; i++) { + final player = AudioPlayer(); + // lowLatency mode is highly recommended for short game sounds + await player.setPlayerMode(PlayerMode.lowLatency); + await player.setReleaseMode(ReleaseMode.stop); + _sfxPlayers.add(player); + } + _isInitialized = true; - print("WolfAudio: AudioPlayers initialized successfully."); + print( + "WolfAudio: AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.", + ); } catch (e) { print("WolfAudio: Failed to initialize AudioPlayers - $e"); } @@ -28,6 +48,13 @@ class WolfAudio { void dispose() { stopMusic(); _musicPlayer.dispose(); + + for (final player in _sfxPlayers) { + player.stop(); + player.dispose(); + } + _sfxPlayers.clear(); + _isInitialized = false; } @@ -35,72 +62,73 @@ class WolfAudio { // MUSIC MANAGEMENT // ========================================== - /// Renders and plays a specific IMF music track. Future playMusic(ImfMusic track, {bool looping = true}) async { if (!_isInitialized) return; - - // Stop currently playing music to prevent overlap await stopMusic(); try { - // Render hardware instructions into PCM and wrap in WAV final pcmSamples = ImfRenderer.render(track); final wavBytes = ImfRenderer.createWavFile(pcmSamples); - // Configure looping behavior await _musicPlayer.setReleaseMode( looping ? ReleaseMode.loop : ReleaseMode.stop, ); - - // Play the generated WAV file directly from memory await _musicPlayer.play(BytesSource(wavBytes)); } catch (e) { print("WolfAudio: Error playing music track - $e"); } } - /// Halts playback for the current track. Future stopMusic() async { if (!_isInitialized) return; await _musicPlayer.stop(); } - /// Pauses the current track. Future pauseMusic() async { - if (_isInitialized) { - await _musicPlayer.pause(); - } + if (_isInitialized) await _musicPlayer.pause(); } - /// Resumes a paused track. Future resumeMusic() async { - if (_isInitialized) { - await _musicPlayer.resume(); - } + if (_isInitialized) await _musicPlayer.resume(); } Future playMenuMusic() async { final data = activeGame; - // We can't play if data isn't set or doesn't have enough tracks if (data == null || data.music.length <= 1) return; - - // Track 1 is the menu theme in both Shareware and Retail await playMusic(data.music[1]); } - /// Plays the specific track assigned to a WolfLevel. Future playLevelMusic(WolfLevel level) async { final data = activeGame; if (data == null || data.music.isEmpty) return; final index = level.musicIndex; - if (index < data.music.length) { await playMusic(data.music[index]); } else { - print( - "WolfAudio: Warning - Track index $index out of bounds for level ${level.name}.", - ); + print("WolfAudio: Warning - Track index $index out of bounds."); + } + } + + // ========================================== + // SFX MANAGEMENT + // ========================================== + + /// Plays a sound effect from a WAV byte array using the round-robin pool. + Future playSfx(Uint8List wavBytes) async { + if (!_isInitialized) return; + + try { + // Grab the next available player in the pool + final player = _sfxPlayers[_currentSfxIndex]; + + // Move to the next index, looping back to 0 if we hit the max + _currentSfxIndex = (_currentSfxIndex + 1) % _maxSfxChannels; + + // Play the sound (this interrupts whatever this specific channel was playing) + await player.play(BytesSource(wavBytes)); + } catch (e) { + print("WolfAudio: Error playing SFX - $e"); } } }