import 'dart:developer'; import 'dart:typed_data'; import 'package:audioplayers/audioplayers.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_synth.dart'; import 'package:wolf_3d_flutter/audio/debug_music_player.dart'; class WolfAudio implements EngineAudio, DebugMusicPlayer { @override Future debugSoundTest() async { for (int i = 0; i < 50; i++) { Future.delayed(Duration(seconds: i * 2), () { log('[AUDIO] Testing Sound ID: $i'); playSoundEffectId(i); }); } } bool _isInitialized = false; final AudioPlayer _musicPlayer = AudioPlayer(); static const int _maxSfxChannels = 8; final List _sfxPlayers = []; int _currentSfxIndex = 0; @override WolfensteinData? activeGame; @override Future init() async { if (_isInitialized) return; try { await _musicPlayer.setPlayerMode(PlayerMode.mediaPlayer); for (int i = 0; i < _maxSfxChannels; i++) { final player = AudioPlayer(); await player.setPlayerMode(PlayerMode.lowLatency); await player.setReleaseMode(ReleaseMode.stop); _sfxPlayers.add(player); } _isInitialized = true; log( '[AUDIO] AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.', ); } catch (e) { log('[AUDIO] Failed to initialize AudioPlayers - $e'); } } @override void dispose() { stopAllAudio(); _musicPlayer.dispose(); for (final player in _sfxPlayers) { player.stop(); player.dispose(); } _sfxPlayers.clear(); _isInitialized = false; } @override Future playMusic(ImfMusic track, {bool looping = true}) async { if (!_isInitialized) return; await stopMusic(); try { final pcmSamples = ImfRenderer.render(track); final wavBytes = ImfRenderer.createWavFile(pcmSamples); await _musicPlayer.setReleaseMode( looping ? ReleaseMode.loop : ReleaseMode.stop, ); await _musicPlayer.play(BytesSource(wavBytes)); } catch (e) { log('[AUDIO] Error playing music track - $e'); } } @override Future stopMusic() async { if (!_isInitialized) return; await _musicPlayer.stop(); } @override Future stopAllAudio() async { if (!_isInitialized) return; await _musicPlayer.stop(); for (final player in _sfxPlayers) { await player.stop(); } } Future pauseMusic() async { if (_isInitialized) await _musicPlayer.pause(); } Future resumeMusic() async { if (_isInitialized) await _musicPlayer.resume(); } @override Future playMenuMusic() async { final data = activeGame; final trackIndex = data == null ? null : Music.menuTheme.trackIndexFor(data.version); if (data == null || trackIndex == null || trackIndex >= data.music.length) { return; } await playMusic(data.music[trackIndex]); } @override Future playLevelMusic(Music music) async { final data = activeGame; if (data == null || data.music.isEmpty) return; final index = music.trackIndexFor(data.version) ?? 0; if (index < data.music.length) { await playMusic(data.music[index]); } else { log('[AUDIO] Warning - Track index $index out of bounds.'); } } @override Future playSoundEffect(SoundEffect effect) async { final data = activeGame; if (data == null) return; final resolved = data.registry.sfx.resolve(effect); if (resolved != null) { await playSoundEffectId(resolved.slotIndex); return; } if (data.version == GameVersion.spearOfDestinyDemo) { return; } await playSoundEffectId(effect.idFor(data.version)); } @override Future playSoundEffectId(int sfxId) async { log('[AUDIO] Playing sfx id $sfxId'); final data = activeGame; if (data == null) return; final soundsList = data.sounds; if (sfxId < 0 || sfxId >= soundsList.length) return; final raw8bitBytes = soundsList[sfxId].bytes; if (raw8bitBytes.isEmpty) return; final Int16List converted16bit = Int16List(raw8bitBytes.length); for (int i = 0; i < raw8bitBytes.length; i++) { converted16bit[i] = (raw8bitBytes[i] - 128) * 256; } final wavBytes = ImfRenderer.createWavFile( converted16bit, sampleRate: 7000, ); try { final player = _sfxPlayers[_currentSfxIndex]; _currentSfxIndex = (_currentSfxIndex + 1) % _maxSfxChannels; await player.play(BytesSource(wavBytes)); } catch (e) { log('[AUDIO] SFX Error - $e'); } } }