feat: Implement audio backend with subprocess support and refactor audio handling
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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<void> 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<AudioPlayer> _sfxPlayers = [];
|
||||
int _currentSfxIndex = 0;
|
||||
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
@override
|
||||
Future<void> 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<void> 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<void> stopMusic() async {
|
||||
if (!_isInitialized) return;
|
||||
await _musicPlayer.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {
|
||||
if (!_isInitialized) return;
|
||||
|
||||
await _musicPlayer.stop();
|
||||
for (final player in _sfxPlayers) {
|
||||
await player.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pauseMusic() async {
|
||||
if (_isInitialized) await _musicPlayer.pause();
|
||||
}
|
||||
|
||||
Future<void> resumeMusic() async {
|
||||
if (_isInitialized) await _musicPlayer.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<void> 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<void> 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<void> 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user