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:
@@ -1,214 +0,0 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:wolf_3d_dart/src/synth/imf_renderer.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
|
||||
|
||||
class WolfAudio implements EngineAudio {
|
||||
@override
|
||||
Future<void> debugSoundTest() async {
|
||||
// Play the first 50 sounds with a 2-second gap to identify them
|
||||
for (int i = 0; i < 50; i++) {
|
||||
Future.delayed(Duration(seconds: i * 2), () {
|
||||
log("[AUDIO] Testing Sound ID: $i");
|
||||
playSoundEffectId(i);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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<AudioPlayer> _sfxPlayers = [];
|
||||
int _currentSfxIndex = 0;
|
||||
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
/// Initializes the audio engine and pre-allocates the SFX pool.
|
||||
@override
|
||||
Future<void> init() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
try {
|
||||
// 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;
|
||||
log(
|
||||
"[AUDIO] AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.",
|
||||
);
|
||||
} catch (e) {
|
||||
log("[AUDIO] Failed to initialize AudioPlayers - $e");
|
||||
}
|
||||
}
|
||||
|
||||
/// Disposes of the audio engine and frees resources.
|
||||
@override
|
||||
void dispose() {
|
||||
stopAllAudio();
|
||||
_musicPlayer.dispose();
|
||||
|
||||
for (final player in _sfxPlayers) {
|
||||
player.stop();
|
||||
player.dispose();
|
||||
}
|
||||
_sfxPlayers.clear();
|
||||
|
||||
_isInitialized = false;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MUSIC MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SFX MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
// Spear demo/shareware has a much smaller digitized table than retail.
|
||||
// If a sound is not explicitly mapped for that variant, skip it rather
|
||||
// than probing an invalid or unrelated slot.
|
||||
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");
|
||||
// The original engine uses a specific starting chunk for digitized sounds.
|
||||
// In many loaders, the 'sounds' list is already just the digitized ones.
|
||||
// If your list contains EVERYTHING, you need to add the offset (174).
|
||||
// If it's JUST digitized sounds, sfxId should work directly.
|
||||
|
||||
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;
|
||||
|
||||
// 2. Convert 8-bit Unsigned PCM -> 16-bit Signed PCM
|
||||
// Wolf3D raw sounds are biased at 128.
|
||||
final Int16List converted16bit = Int16List(raw8bitBytes.length);
|
||||
for (int i = 0; i < raw8bitBytes.length; i++) {
|
||||
// (sample - 128) shifts it to signed 8-bit range (-128 to 127)
|
||||
// Multiplying by 256 scales it to 16-bit range (-32768 to 32512)
|
||||
converted16bit[i] = (raw8bitBytes[i] - 128) * 256;
|
||||
}
|
||||
|
||||
// 3. Wrap in a WAV header (Wolf3D digitized sounds are 7000Hz Mono)
|
||||
final wavBytes = ImfRenderer.createWavFile(
|
||||
converted16bit,
|
||||
sampleRate: 7000,
|
||||
);
|
||||
|
||||
try {
|
||||
final player = _sfxPlayers[_currentSfxIndex];
|
||||
_currentSfxIndex = (_currentSfxIndex + 1) % _maxSfxChannels;
|
||||
|
||||
// Note: We use BytesSource because createWavFile returns Uint8List (the file bytes)
|
||||
await player.play(BytesSource(wavBytes));
|
||||
} catch (e) {
|
||||
log("[AUDIO] SFX Error - $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/// Entry point for the sound synthesis module of Wolf3D.
|
||||
///
|
||||
/// This library provides access to audio functionalities, primarily by exporting
|
||||
/// the [WolfAudio] class.
|
||||
/// This library provides access to audio synthesis and WAV encoding helpers.
|
||||
library;
|
||||
|
||||
export 'src/synth/wolf_3d_audio.dart' show WolfAudio;
|
||||
export 'src/synth/imf_renderer.dart' show ImfRenderer;
|
||||
|
||||
Reference in New Issue
Block a user