diff --git a/apps/wolf_3d_cli/bin/main.dart b/apps/wolf_3d_cli/bin/main.dart index 0daf4c1..3576742 100644 --- a/apps/wolf_3d_cli/bin/main.dart +++ b/apps/wolf_3d_cli/bin/main.dart @@ -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(), diff --git a/packages/wolf_3d_flutter/lib/audio/debug_music_player.dart b/packages/wolf_3d_dart/lib/src/engine/audio/debug_music_player.dart similarity index 100% rename from packages/wolf_3d_flutter/lib/audio/debug_music_player.dart rename to packages/wolf_3d_dart/lib/src/engine/audio/debug_music_player.dart diff --git a/apps/wolf_3d_cli/lib/cli_subprocess_audio.dart b/packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_io.dart similarity index 86% rename from apps/wolf_3d_cli/lib/cli_subprocess_audio.dart rename to packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_io.dart index 3580d59..f12a6dd 100644 --- a/apps/wolf_3d_cli/lib/cli_subprocess_audio.dart +++ b/packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_io.dart @@ -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 _playMusicTrack(ImfMusic track) async { + @override + Future 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); - await _startLoopingMusic(wavBytes); + if (looping) { + await _startLoopingMusic(wavBytes); + return; + } + + await stopMusic(); + final process = await _startPlaybackProcess( + wavBytes: wavBytes, + role: _PlaybackRole.music, + ); + if (process != null) { + _musicProcess = process; + } } Future _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 _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 _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 _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'); } } } diff --git a/packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_stub.dart b/packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_stub.dart similarity index 77% rename from packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_stub.dart rename to packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_stub.dart index c1b7718..e77c4bc 100644 --- a/packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_stub.dart +++ b/packages/wolf_3d_dart/lib/src/engine/audio/native_subprocess_audio_stub.dart @@ -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; diff --git a/packages/wolf_3d_dart/lib/src/engine/audio/silent_renderer.dart b/packages/wolf_3d_dart/lib/src/engine/audio/silent_audio.dart similarity index 52% rename from packages/wolf_3d_dart/lib/src/engine/audio/silent_renderer.dart rename to packages/wolf_3d_dart/lib/src/engine/audio/silent_audio.dart index b48d6a5..78e40af 100644 --- a/packages/wolf_3d_dart/lib/src/engine/audio/silent_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/engine/audio/silent_audio.dart @@ -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 init() async { - // No-op for CLI + // No-op fallback backend. } @override @@ -23,18 +23,10 @@ class CliSilentAudio implements EngineAudio { Future 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() {} diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 88908d1..b0e9ae4 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -51,7 +51,7 @@ class WolfEngine { ), _availableGames = availableGames ?? [data!], saveGameCodec = saveGameCodec ?? CompatibleSaveGameCodec(), - audio = engineAudio ?? CliSilentAudio(), + audio = engineAudio ?? SilentAudio(), doorManager = DoorManager( onPlaySound: (effect) => engineAudio?.playSoundEffect(effect), ), diff --git a/packages/wolf_3d_dart/lib/wolf_3d_audio.dart b/packages/wolf_3d_dart/lib/wolf_3d_audio.dart new file mode 100644 index 0000000..6546fba --- /dev/null +++ b/packages/wolf_3d_dart/lib/wolf_3d_audio.dart @@ -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; diff --git a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart index 652720b..95392c8 100644 --- a/packages/wolf_3d_dart/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_dart/lib/wolf_3d_engine.dart @@ -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'; diff --git a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart b/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart deleted file mode 100644 index 9f3db2b..0000000 --- a/packages/wolf_3d_flutter/lib/audio/audio_adaptor.dart +++ /dev/null @@ -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 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 init() async { - await wolf3d.audio.init(); - } - - @override - void dispose() { - wolf3d.audio.dispose(); - } - - @override - Future debugSoundTest() async { - wolf3d.audio.debugSoundTest(); - } - - @override - WolfensteinData? get activeGame => wolf3d.maybeActiveGame; - - @override - set activeGame(WolfensteinData? value) { - if (value != null) { - wolf3d.setActiveGame(value); - } - } -} diff --git a/packages/wolf_3d_flutter/lib/audio/default_audio_backend.dart b/packages/wolf_3d_flutter/lib/audio/default_audio_backend.dart deleted file mode 100644 index 3040e64..0000000 --- a/packages/wolf_3d_flutter/lib/audio/default_audio_backend.dart +++ /dev/null @@ -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; - } -} diff --git a/packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_io.dart b/packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_io.dart deleted file mode 100644 index d35f780..0000000 --- a/packages/wolf_3d_flutter/lib/audio/desktop_subprocess_audio_io.dart +++ /dev/null @@ -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 _sfxProcesses = []; - - @override - Future 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 debugSoundTest() async { - for (int i = 0; i < 50; i++) { - await Future.delayed(const Duration(milliseconds: 500)); - await playSoundEffectId(i); - } - } - - @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 < 0 || index >= data.music.length) { - return; - } - - await playMusic(data.music[index]); - } - - @override - Future 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 _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 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 stopAllAudio() async { - await stopMusic(); - - for (final process in List.from(_sfxProcesses)) { - process.kill(); - } - _sfxProcesses.clear(); - } - - @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 { - 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 _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 _startStdInPlaybackProcess( - String executable, - List arguments, - Uint8List wavBytes, - ) async { - final process = await Process.start(executable, arguments); - - try { - await process.stdin.addStream( - Stream>.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 _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, [ - '-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 _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 _commandExists(String command) async { - final probe = Platform.isWindows - ? await Process.run('where', [command]) - : await Process.run('which', [command]); - return probe.exitCode == 0; - } -} - -enum _AudioBackend { none, linuxAplay, macosAfplay, windowsPowerShell } - -enum _PlaybackRole { music, sfx } diff --git a/packages/wolf_3d_flutter/lib/audio/wolf_audio.dart b/packages/wolf_3d_flutter/lib/audio/wolf3d_platform_audio.dart similarity index 67% rename from packages/wolf_3d_flutter/lib/audio/wolf_audio.dart rename to packages/wolf_3d_flutter/lib/audio/wolf3d_platform_audio.dart index 96d32fa..305e173 100644 --- a/packages/wolf_3d_flutter/lib/audio/wolf_audio.dart +++ b/packages/wolf_3d_flutter/lib/audio/wolf3d_platform_audio.dart @@ -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 debugSoundTest() => _delegate.debugSoundTest(); + + @override + Future 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 stopAllAudio() => _delegate.stopAllAudio(); + + @override + void playSoundEffect(SoundEffect effect) => _delegate.playSoundEffect(effect); + + @override + void playSoundEffectId(int sfxId) => _delegate.playSoundEffectId(sfxId); + + @override + Future playMusic(ImfMusic track, {bool looping = true}) { + if (_delegate is DebugMusicPlayer) { + return (_delegate as DebugMusicPlayer).playMusic( + track, + looping: looping, + ); + } + + return Future.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 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'); } } diff --git a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart index 44acdee..f92f098 100644 --- a/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart +++ b/packages/wolf_3d_flutter/lib/wolf_3d_flutter.dart @@ -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 availableGames = [];