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:
@@ -7,7 +7,7 @@ library;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:wolf_3d_cli/cli_game_loop.dart';
|
||||
import 'package:wolf_3d_cli/cli_subprocess_audio.dart';
|
||||
import 'package:wolf_3d_dart/wolf_3d_audio.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';
|
||||
@@ -63,7 +63,7 @@ void main() async {
|
||||
stdout.terminalLines,
|
||||
),
|
||||
input: CliInput(),
|
||||
engineAudio: CliSubprocessAudio(),
|
||||
engineAudio: NativeSubprocessAudio(),
|
||||
onGameWon: () => stopAndExit(0),
|
||||
onQuit: () => stopAndExit(0),
|
||||
saveGamePersistence: DefaultSaveGamePersistence(),
|
||||
|
||||
+46
-36
@@ -3,12 +3,16 @@ 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';
|
||||
|
||||
class CliSubprocessAudio implements EngineAudio {
|
||||
CliSubprocessAudio({this.maxConcurrentSfx = 8});
|
||||
class NativeSubprocessAudio implements EngineAudio, DebugMusicPlayer {
|
||||
NativeSubprocessAudio({this.maxConcurrentSfx = 8});
|
||||
|
||||
static bool get supportsCurrentPlatform =>
|
||||
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
|
||||
|
||||
final int maxConcurrentSfx;
|
||||
|
||||
@@ -37,9 +41,9 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
_initialized = true;
|
||||
|
||||
if (_isSupported) {
|
||||
log('[CLI AUDIO] Subprocess backend enabled: ${_backend.name}');
|
||||
log('[NATIVE AUDIO] Subprocess backend enabled: ${_backend.name}');
|
||||
} else {
|
||||
log('[CLI AUDIO] No supported audio backend found; running silent.');
|
||||
log('[NATIVE AUDIO] No supported audio backend found; running silent.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +70,7 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
return;
|
||||
}
|
||||
|
||||
await _playMusicTrack(data.music[trackIndex]);
|
||||
await playMusic(data.music[trackIndex]);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -81,10 +85,11 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
return;
|
||||
}
|
||||
|
||||
await _playMusicTrack(data.music[index]);
|
||||
await playMusic(data.music[index]);
|
||||
}
|
||||
|
||||
Future<void> _playMusicTrack(ImfMusic track) async {
|
||||
@override
|
||||
Future<void> playMusic(ImfMusic track, {bool looping = true}) async {
|
||||
if (!_isSupported) {
|
||||
return;
|
||||
}
|
||||
@@ -92,7 +97,19 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
final pcmSamples = ImfRenderer.render(track);
|
||||
final wavBytes = ImfRenderer.createWavFile(pcmSamples);
|
||||
|
||||
if (looping) {
|
||||
await _startLoopingMusic(wavBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
await stopMusic();
|
||||
final process = await _startPlaybackProcess(
|
||||
wavBytes: wavBytes,
|
||||
role: _PlaybackRole.music,
|
||||
);
|
||||
if (process != null) {
|
||||
_musicProcess = process;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startLoopingMusic(Uint8List wavBytes) async {
|
||||
@@ -127,10 +144,7 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
final path = _musicTempFilePath;
|
||||
_musicTempFilePath = null;
|
||||
if (path != null) {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
await _cleanupTempWav(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +255,7 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
log('[CLI AUDIO] Failed to start playback process: $error');
|
||||
log('[NATIVE AUDIO] Failed to start playback process: $error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -252,16 +266,13 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
Uint8List wavBytes,
|
||||
_PlaybackRole role,
|
||||
) async {
|
||||
final path = await _writeTempWav(wavBytes, prefix: 'wolf3d_cli_audio_');
|
||||
final path = await _writeTempWav(wavBytes, prefix: 'wolf3d_native_audio_');
|
||||
|
||||
if (role == _PlaybackRole.music) {
|
||||
final existing = _musicTempFilePath;
|
||||
_musicTempFilePath = path;
|
||||
if (existing != null && existing != path) {
|
||||
final previous = File(existing);
|
||||
if (await previous.exists()) {
|
||||
await previous.delete();
|
||||
}
|
||||
await _cleanupTempWav(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,9 +284,8 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
unawaited(
|
||||
process.exitCode.then((code) async {
|
||||
if (code != 0) {
|
||||
log('[CLI AUDIO] Player exited with code $code: $executable');
|
||||
log('[NATIVE AUDIO] Player exited with code $code: $executable');
|
||||
}
|
||||
|
||||
await _cleanupTempWav(path);
|
||||
}),
|
||||
);
|
||||
@@ -293,10 +303,7 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
final existing = _musicTempFilePath;
|
||||
_musicTempFilePath = path;
|
||||
if (existing != null && existing != path) {
|
||||
final previous = File(existing);
|
||||
if (await previous.exists()) {
|
||||
await previous.delete();
|
||||
}
|
||||
await _cleanupTempWav(existing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,7 +330,9 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
Uint8List wavBytes, {
|
||||
required _PlaybackRole role,
|
||||
}) async {
|
||||
final tempDir = await Directory.systemTemp.createTemp('wolf3d_cli_audio_');
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'wolf3d_native_audio_',
|
||||
);
|
||||
final suffix = role == _PlaybackRole.music
|
||||
? 'music_${DateTime.now().microsecondsSinceEpoch}.wav'
|
||||
: 'sfx_${DateTime.now().microsecondsSinceEpoch}.wav';
|
||||
@@ -333,6 +342,18 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
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');
|
||||
@@ -382,17 +403,6 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
return probe.exitCode == 0;
|
||||
}
|
||||
|
||||
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<void> _cleanupTempWav(String path) async {
|
||||
try {
|
||||
final file = File(path);
|
||||
@@ -405,7 +415,7 @@ class CliSubprocessAudio implements EngineAudio {
|
||||
await directory.delete();
|
||||
}
|
||||
} catch (error) {
|
||||
log('[CLI AUDIO] Temp WAV cleanup failed: $error');
|
||||
log('[NATIVE AUDIO] Temp WAV cleanup failed: $error');
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -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;
|
||||
|
||||
+4
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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});
|
||||
|
||||
static bool get supportsCurrentPlatform =>
|
||||
Platform.isLinux || Platform.isMacOS || Platform.isWindows;
|
||||
|
||||
final int maxConcurrentSfx;
|
||||
|
||||
@override
|
||||
WolfensteinData? activeGame;
|
||||
|
||||
bool _initialized = false;
|
||||
bool _isSupported = false;
|
||||
_AudioBackend _backend = _AudioBackend.none;
|
||||
String _windowsShellCommand = 'powershell';
|
||||
|
||||
Process? _musicProcess;
|
||||
int _musicLoopToken = 0;
|
||||
String? _musicTempFilePath;
|
||||
|
||||
final List<Process> _sfxProcesses = <Process>[];
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
_backend = await _detectBackend();
|
||||
_isSupported = _backend != _AudioBackend.none;
|
||||
_initialized = true;
|
||||
|
||||
if (_isSupported) {
|
||||
log('[DESKTOP AUDIO] Subprocess backend enabled: ${_backend.name}');
|
||||
} else {
|
||||
log('[DESKTOP AUDIO] No supported audio backend found; running silent.');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(stopAllAudio());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> debugSoundTest() async {
|
||||
for (int i = 0; i < 50; i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||||
await playSoundEffectId(i);
|
||||
}
|
||||
}
|
||||
|
||||
@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 < 0 || index >= data.music.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await playMusic(data.music[index]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> playMusic(ImfMusic track, {bool looping = true}) async {
|
||||
if (!_isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
final pcmSamples = ImfRenderer.render(track);
|
||||
final wavBytes = ImfRenderer.createWavFile(pcmSamples);
|
||||
|
||||
if (looping) {
|
||||
await _startLoopingMusic(wavBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
await stopMusic();
|
||||
final process = await _startPlaybackProcess(
|
||||
wavBytes: wavBytes,
|
||||
role: _PlaybackRole.music,
|
||||
);
|
||||
if (process != null) {
|
||||
_musicProcess = process;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startLoopingMusic(Uint8List wavBytes) async {
|
||||
await stopMusic();
|
||||
|
||||
final int token = ++_musicLoopToken;
|
||||
|
||||
while (_musicLoopToken == token) {
|
||||
final process = await _startPlaybackProcess(
|
||||
wavBytes: wavBytes,
|
||||
role: _PlaybackRole.music,
|
||||
);
|
||||
if (process == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
_musicProcess = process;
|
||||
await process.exitCode;
|
||||
|
||||
if (_musicLoopToken != token) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopMusic() async {
|
||||
_musicLoopToken++;
|
||||
_musicProcess?.kill();
|
||||
_musicProcess = null;
|
||||
|
||||
final path = _musicTempFilePath;
|
||||
_musicTempFilePath = null;
|
||||
if (path != null) {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopAllAudio() async {
|
||||
await stopMusic();
|
||||
|
||||
for (final process in List<Process>.from(_sfxProcesses)) {
|
||||
process.kill();
|
||||
}
|
||||
_sfxProcesses.clear();
|
||||
}
|
||||
|
||||
@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 {
|
||||
if (!_isSupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
if (_sfxProcesses.length >= maxConcurrentSfx) {
|
||||
final oldest = _sfxProcesses.removeAt(0);
|
||||
oldest.kill();
|
||||
}
|
||||
|
||||
final process = await _startPlaybackProcess(
|
||||
wavBytes: wavBytes,
|
||||
role: _PlaybackRole.sfx,
|
||||
);
|
||||
if (process == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_sfxProcesses.add(process);
|
||||
unawaited(
|
||||
process.exitCode.then((_) {
|
||||
_sfxProcesses.remove(process);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Process?> _startPlaybackProcess({
|
||||
required Uint8List wavBytes,
|
||||
required _PlaybackRole role,
|
||||
}) async {
|
||||
try {
|
||||
switch (_backend) {
|
||||
case _AudioBackend.linuxAplay:
|
||||
return _startStdInPlaybackProcess('aplay', const [
|
||||
'-q',
|
||||
'-',
|
||||
], wavBytes);
|
||||
case _AudioBackend.macosAfplay:
|
||||
return _startStdInPlaybackProcess('afplay', const ['-'], wavBytes);
|
||||
case _AudioBackend.windowsPowerShell:
|
||||
return _startWindowsPlaybackProcess(wavBytes, role: role);
|
||||
case _AudioBackend.none:
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
log('[DESKTOP AUDIO] Failed to start playback process: $error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Process> _startStdInPlaybackProcess(
|
||||
String executable,
|
||||
List<String> arguments,
|
||||
Uint8List wavBytes,
|
||||
) async {
|
||||
final process = await Process.start(executable, arguments);
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
Future<Process> _startWindowsPlaybackProcess(
|
||||
Uint8List wavBytes, {
|
||||
required _PlaybackRole role,
|
||||
}) async {
|
||||
final path = await _writeWindowsTempWav(wavBytes, role: role);
|
||||
|
||||
if (role == _PlaybackRole.music) {
|
||||
final existing = _musicTempFilePath;
|
||||
_musicTempFilePath = path;
|
||||
if (existing != null && existing != path) {
|
||||
final previous = File(existing);
|
||||
if (await previous.exists()) {
|
||||
await previous.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final escapedPath = path.replaceAll("'", "''");
|
||||
final script = "(New-Object Media.SoundPlayer '$escapedPath').PlaySync()";
|
||||
|
||||
final process = await Process.start(_windowsShellCommand, <String>[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
script,
|
||||
]);
|
||||
|
||||
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();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
Future<String> _writeWindowsTempWav(
|
||||
Uint8List wavBytes, {
|
||||
required _PlaybackRole role,
|
||||
}) async {
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'wolf3d_flutter_audio_',
|
||||
);
|
||||
final suffix = role == _PlaybackRole.music
|
||||
? 'music_${DateTime.now().microsecondsSinceEpoch}.wav'
|
||||
: 'sfx_${DateTime.now().microsecondsSinceEpoch}.wav';
|
||||
final path = '${tempDir.path}${Platform.pathSeparator}$suffix';
|
||||
await File(path).writeAsBytes(wavBytes, flush: true);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
Future<_AudioBackend> _detectBackend() async {
|
||||
if (Platform.isLinux) {
|
||||
final hasAplay = await _commandExists('aplay');
|
||||
if (hasAplay) {
|
||||
return _AudioBackend.linuxAplay;
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isMacOS) {
|
||||
final hasAfplay = await _commandExists('afplay');
|
||||
if (hasAfplay) {
|
||||
return _AudioBackend.macosAfplay;
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.isWindows) {
|
||||
final hasPowerShell = await _commandExists('powershell');
|
||||
if (hasPowerShell) {
|
||||
_windowsShellCommand = 'powershell';
|
||||
return _AudioBackend.windowsPowerShell;
|
||||
}
|
||||
|
||||
final hasPwsh = await _commandExists('pwsh');
|
||||
if (hasPwsh) {
|
||||
_windowsShellCommand = 'pwsh';
|
||||
return _AudioBackend.windowsPowerShell;
|
||||
}
|
||||
}
|
||||
|
||||
return _AudioBackend.none;
|
||||
}
|
||||
|
||||
Future<bool> _commandExists(String command) async {
|
||||
final probe = Platform.isWindows
|
||||
? await Process.run('where', <String>[command])
|
||||
: await Process.run('which', <String>[command]);
|
||||
return probe.exitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
enum _AudioBackend { none, linuxAplay, macosAfplay, windowsPowerShell }
|
||||
|
||||
enum _PlaybackRole { music, sfx }
|
||||
+79
-4
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user