Refactor audio handling in Wolfenstein 3D

- Updated main.dart to use NativeSubprocessAudio instead of CliSubprocessAudio.
- Introduced DebugMusicPlayer interface for music playback.
- Implemented NativeSubprocessAudio for native audio handling with subprocesses.
- Added SilentAudio class as a no-op fallback for audio.
- Removed deprecated FlutterAudioAdapter and default audio backend implementations.
- Integrated Wolf3dPlatformAudio to manage audio across platforms, selecting between NativeSubprocessAudio and an embedded audio player.
- Updated wolf_3d_engine.dart to use SilentAudio as the default audio backend.
- Cleaned up audio-related files and ensured proper audio initialization and playback functionality.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 17:30:02 +01:00
parent ea6825341e
commit fdf84b3a9d
13 changed files with 151 additions and 545 deletions
@@ -3,13 +3,13 @@ import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:wolf_3d_dart/src/engine/audio/debug_music_player.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 DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
DesktopSubprocessAudio({this.maxConcurrentSfx = 8});
class NativeSubprocessAudio implements EngineAudio, DebugMusicPlayer {
NativeSubprocessAudio({this.maxConcurrentSfx = 8});
static bool get supportsCurrentPlatform =>
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
@@ -41,9 +41,9 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
_initialized = true;
if (_isSupported) {
log('[DESKTOP AUDIO] Subprocess backend enabled: ${_backend.name}');
log('[NATIVE AUDIO] Subprocess backend enabled: ${_backend.name}');
} else {
log('[DESKTOP AUDIO] No supported audio backend found; running silent.');
log('[NATIVE AUDIO] No supported audio backend found; running silent.');
}
}
@@ -144,10 +144,7 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
final path = _musicTempFilePath;
_musicTempFilePath = null;
if (path != null) {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
await _cleanupTempWav(path);
}
}
@@ -239,49 +236,60 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
}) async {
try {
switch (_backend) {
case _AudioBackend.linuxPipeWire:
return _startFilePlaybackProcess('pw-play', const [], wavBytes, role);
case _AudioBackend.linuxPulseAudio:
return _startFilePlaybackProcess('paplay', const [], wavBytes, role);
case _AudioBackend.linuxAplay:
return _startStdInPlaybackProcess('aplay', const [
'-q',
'-',
], wavBytes);
return _startFilePlaybackProcess(
'aplay',
const ['-q'],
wavBytes,
role,
);
case _AudioBackend.macosAfplay:
return _startStdInPlaybackProcess('afplay', const ['-'], wavBytes);
return _startFilePlaybackProcess('afplay', const [], wavBytes, role);
case _AudioBackend.windowsPowerShell:
return _startWindowsPlaybackProcess(wavBytes, role: role);
case _AudioBackend.none:
return null;
}
} catch (error) {
log('[DESKTOP AUDIO] Failed to start playback process: $error');
log('[NATIVE AUDIO] Failed to start playback process: $error');
return null;
}
}
Future<Process> _startStdInPlaybackProcess(
Future<Process> _startFilePlaybackProcess(
String executable,
List<String> arguments,
List<String> baseArguments,
Uint8List wavBytes,
_PlaybackRole role,
) async {
final process = await Process.start(executable, arguments);
final path = await _writeTempWav(wavBytes, prefix: 'wolf3d_native_audio_');
try {
await process.stdin.addStream(
Stream<List<int>>.value(wavBytes),
);
} on SocketException catch (error) {
log('[DESKTOP AUDIO] Playback pipe write failed: $error');
} catch (error) {
log('[DESKTOP AUDIO] Playback pipe write failed: $error');
} finally {
try {
await process.stdin.close();
} on SocketException catch (error) {
log('[DESKTOP AUDIO] Playback pipe close failed: $error');
} catch (error) {
log('[DESKTOP AUDIO] Playback pipe close failed: $error');
if (role == _PlaybackRole.music) {
final existing = _musicTempFilePath;
_musicTempFilePath = path;
if (existing != null && existing != path) {
await _cleanupTempWav(existing);
}
}
final process = await Process.start(executable, <String>[
...baseArguments,
path,
]);
unawaited(
process.exitCode.then((code) async {
if (code != 0) {
log('[NATIVE AUDIO] Player exited with code $code: $executable');
}
await _cleanupTempWav(path);
}),
);
return process;
}
@@ -295,10 +303,7 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
final existing = _musicTempFilePath;
_musicTempFilePath = path;
if (existing != null && existing != path) {
final previous = File(existing);
if (await previous.exists()) {
await previous.delete();
}
await _cleanupTempWav(existing);
}
}
@@ -314,15 +319,7 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
unawaited(
process.exitCode.then((_) async {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
final directory = file.parent;
if (await directory.exists()) {
await directory.delete();
}
await _cleanupTempWav(path);
}),
);
@@ -334,7 +331,7 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
required _PlaybackRole role,
}) async {
final tempDir = await Directory.systemTemp.createTemp(
'wolf3d_flutter_audio_',
'wolf3d_native_audio_',
);
final suffix = role == _PlaybackRole.music
? 'music_${DateTime.now().microsecondsSinceEpoch}.wav'
@@ -345,8 +342,30 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
return path;
}
Future<String> _writeTempWav(
Uint8List wavBytes, {
required String prefix,
}) async {
final tempDir = await Directory.systemTemp.createTemp(prefix);
final path =
'${tempDir.path}${Platform.pathSeparator}audio_${DateTime.now().microsecondsSinceEpoch}.wav';
await File(path).writeAsBytes(wavBytes, flush: true);
return path;
}
Future<_AudioBackend> _detectBackend() async {
if (Platform.isLinux) {
final hasPwPlay = await _commandExists('pw-play');
if (hasPwPlay) {
return _AudioBackend.linuxPipeWire;
}
final hasPaplay = await _commandExists('paplay');
if (hasPaplay) {
return _AudioBackend.linuxPulseAudio;
}
final hasAplay = await _commandExists('aplay');
if (hasAplay) {
return _AudioBackend.linuxAplay;
@@ -383,8 +402,31 @@ class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
: await Process.run('which', <String>[command]);
return probe.exitCode == 0;
}
Future<void> _cleanupTempWav(String path) async {
try {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
final directory = file.parent;
if (await directory.exists()) {
await directory.delete();
}
} catch (error) {
log('[NATIVE AUDIO] Temp WAV cleanup failed: $error');
}
}
}
enum _AudioBackend { none, linuxAplay, macosAfplay, windowsPowerShell }
enum _AudioBackend {
none,
linuxPipeWire,
linuxPulseAudio,
linuxAplay,
macosAfplay,
windowsPowerShell,
}
enum _PlaybackRole { music, sfx }
@@ -1,10 +1,14 @@
import 'package:wolf_3d_dart/src/engine/audio/debug_music_player.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/audio/debug_music_player.dart';
class DesktopSubprocessAudio implements EngineAudio, DebugMusicPlayer {
class NativeSubprocessAudio implements EngineAudio, DebugMusicPlayer {
NativeSubprocessAudio({this.maxConcurrentSfx = 8});
static bool get supportsCurrentPlatform => false;
final int maxConcurrentSfx;
@override
WolfensteinData? activeGame;
@@ -1,13 +1,13 @@
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
class CliSilentAudio implements EngineAudio {
class SilentAudio implements EngineAudio {
@override
WolfensteinData? activeGame;
@override
Future<void> init() async {
// No-op for CLI
// No-op fallback backend.
}
@override
@@ -23,18 +23,10 @@ class CliSilentAudio implements EngineAudio {
Future<void> stopAllAudio() async {}
@override
void playSoundEffect(SoundEffect effect) {
// Optional: You could use the terminal 'bell' character here
// to actually make a system beep when a sound plays!
// stdout.write('\x07');
}
void playSoundEffect(SoundEffect effect) {}
@override
void playSoundEffectId(int sfxId) {
// Optional: You could use the terminal 'bell' character here
// to actually make a system beep when a sound plays!
// stdout.write('\x07');
}
void playSoundEffectId(int sfxId) {}
@override
void dispose() {}
@@ -51,7 +51,7 @@ class WolfEngine {
),
_availableGames = availableGames ?? <WolfensteinData>[data!],
saveGameCodec = saveGameCodec ?? CompatibleSaveGameCodec(),
audio = engineAudio ?? CliSilentAudio(),
audio = engineAudio ?? SilentAudio(),
doorManager = DoorManager(
onPlaySound: (effect) => engineAudio?.playSoundEffect(effect),
),
@@ -0,0 +1,6 @@
library;
export 'src/engine/audio/debug_music_player.dart' show DebugMusicPlayer;
export 'src/engine/audio/native_subprocess_audio_stub.dart'
if (dart.library.io) 'src/engine/audio/native_subprocess_audio_io.dart'
show NativeSubprocessAudio;
@@ -6,7 +6,7 @@
library;
export 'src/engine/audio/engine_audio.dart';
export 'src/engine/audio/silent_renderer.dart';
export 'src/engine/audio/silent_audio.dart';
export 'src/engine/input/engine_input.dart';
export 'src/engine/managers/door_manager.dart';
export 'src/engine/managers/pushwall_manager.dart';
@@ -1,64 +0,0 @@
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/wolf_3d_flutter.dart';
class FlutterAudioAdapter implements EngineAudio {
final Wolf3d wolf3d;
FlutterAudioAdapter(this.wolf3d);
@override
void playLevelMusic(Music music) {
wolf3d.audio.playLevelMusic(music);
}
@override
void stopMusic() {
wolf3d.audio.stopMusic();
}
@override
Future<void> stopAllAudio() async {
await wolf3d.audio.stopAllAudio();
}
@override
void playSoundEffect(SoundEffect effect) {
wolf3d.audio.playSoundEffect(effect);
}
@override
void playSoundEffectId(int sfxId) {
wolf3d.audio.playSoundEffectId(sfxId);
}
@override
void playMenuMusic() {
wolf3d.audio.playMenuMusic();
}
@override
Future<void> init() async {
await wolf3d.audio.init();
}
@override
void dispose() {
wolf3d.audio.dispose();
}
@override
Future<void> debugSoundTest() async {
wolf3d.audio.debugSoundTest();
}
@override
WolfensteinData? get activeGame => wolf3d.maybeActiveGame;
@override
set activeGame(WolfensteinData? value) {
if (value != null) {
wolf3d.setActiveGame(value);
}
}
}
@@ -1,28 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/audio/desktop_subprocess_audio_stub.dart'
if (dart.library.io) 'package:wolf_3d_flutter/audio/desktop_subprocess_audio_io.dart';
import 'package:wolf_3d_flutter/audio/wolf_audio.dart';
EngineAudio createDefaultAudioBackend() {
if (!kIsWeb &&
_isDesktopTarget() &&
DesktopSubprocessAudio.supportsCurrentPlatform) {
return DesktopSubprocessAudio();
}
return WolfAudio();
}
bool _isDesktopTarget() {
switch (defaultTargetPlatform) {
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
return true;
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
return false;
}
}
@@ -2,12 +2,87 @@ import 'dart:developer';
import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:wolf_3d_dart/wolf_3d_audio.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 {
class Wolf3dPlatformAudio implements EngineAudio, DebugMusicPlayer {
Wolf3dPlatformAudio()
: _delegate =
(!kIsWeb &&
_isDesktopTarget() &&
NativeSubprocessAudio.supportsCurrentPlatform)
? NativeSubprocessAudio()
: _EmbeddedWolf3dPlatformAudio();
final EngineAudio _delegate;
@override
WolfensteinData? get activeGame => _delegate.activeGame;
@override
set activeGame(WolfensteinData? value) {
_delegate.activeGame = value;
}
@override
Future<void> debugSoundTest() => _delegate.debugSoundTest();
@override
Future<void> init() => _delegate.init();
@override
void dispose() => _delegate.dispose();
@override
void playMenuMusic() => _delegate.playMenuMusic();
@override
void playLevelMusic(Music music) => _delegate.playLevelMusic(music);
@override
void stopMusic() => _delegate.stopMusic();
@override
Future<void> stopAllAudio() => _delegate.stopAllAudio();
@override
void playSoundEffect(SoundEffect effect) => _delegate.playSoundEffect(effect);
@override
void playSoundEffectId(int sfxId) => _delegate.playSoundEffectId(sfxId);
@override
Future<void> playMusic(ImfMusic track, {bool looping = true}) {
if (_delegate is DebugMusicPlayer) {
return (_delegate as DebugMusicPlayer).playMusic(
track,
looping: looping,
);
}
return Future<void>.value();
}
static bool _isDesktopTarget() {
switch (defaultTargetPlatform) {
case TargetPlatform.macOS:
case TargetPlatform.windows:
case TargetPlatform.linux:
return true;
case TargetPlatform.android:
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:
return false;
}
}
}
class _EmbeddedWolf3dPlatformAudio implements EngineAudio, DebugMusicPlayer {
_EmbeddedWolf3dPlatformAudio();
@override
Future<void> debugSoundTest() async {
for (int i = 0; i < 50; i++) {
@@ -44,10 +119,10 @@ class WolfAudio implements EngineAudio, DebugMusicPlayer {
_isInitialized = true;
log(
'[AUDIO] AudioPlayers initialized successfully with $_maxSfxChannels SFX channels.',
'[AUDIO] Platform audio initialized successfully with $_maxSfxChannels SFX channels.',
);
} catch (e) {
log('[AUDIO] Failed to initialize AudioPlayers - $e');
log('[AUDIO] Failed to initialize platform audio - $e');
}
}
@@ -6,17 +6,18 @@ import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_data.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_flutter/audio/default_audio_backend.dart';
import 'package:wolf_3d_flutter/audio/wolf3d_platform_audio.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';
export 'audio/debug_music_player.dart' show DebugMusicPlayer;
export 'audio/wolf_audio.dart' show WolfAudio;
export 'package:wolf_3d_dart/wolf_3d_audio.dart' show DebugMusicPlayer;
export 'audio/wolf3d_platform_audio.dart' show Wolf3dPlatformAudio;
/// Coordinates asset discovery, audio initialization, and input reuse for apps.
class Wolf3d {
/// Creates an empty facade that must be initialized with [init].
Wolf3d({EngineAudio? audioBackend})
: audio = audioBackend ?? createDefaultAudioBackend();
: audio = audioBackend ?? Wolf3dPlatformAudio();
/// All successfully discovered or bundled game data sets.
final List<WolfensteinData> availableGames = [];