/// Debug browser for SFX and music assets with playback controls. library; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_synth.dart'; import 'package:wolf_3d_flutter/wolf_3d_flutter.dart'; import 'package:wolf_3d_gui/screens/gallery_game_selector.dart'; class _AudioRow { final int id; final List aliases; const _AudioRow({required this.id, required this.aliases}); String get subtitle { if (aliases.isEmpty) { return 'No known alias'; } return aliases.join(', '); } } /// Displays all decoded SFX and music tracks for the selected game data. class AudioGallery extends StatefulWidget { /// Shared app facade used to access game assets and the audio backend. final Wolf3d wolf3d; const AudioGallery({super.key, required this.wolf3d}); @override State createState() => _AudioGalleryState(); } class _AudioGalleryState extends State { late WolfensteinData _selectedGame; int? _playingMusicTrackIndex; int _gridColumnsForWidth(double width) { if (width >= 1400) return 6; if (width >= 1100) return 5; if (width >= 800) return 4; if (width >= 560) return 3; return 2; } static const List _knownSfxKeys = [ SfxKey.openDoor, SfxKey.closeDoor, SfxKey.pushWall, SfxKey.knifeAttack, SfxKey.pistolFire, SfxKey.machineGunFire, SfxKey.chainGunFire, SfxKey.enemyFire, SfxKey.getMachineGun, SfxKey.getAmmo, SfxKey.getChainGun, SfxKey.healthSmall, SfxKey.healthLarge, SfxKey.treasure1, SfxKey.treasure2, SfxKey.treasure3, SfxKey.treasure4, SfxKey.extraLife, SfxKey.guardHalt, SfxKey.dogBark, SfxKey.dogDeath, SfxKey.dogAttack, SfxKey.deathScream1, SfxKey.deathScream2, SfxKey.deathScream3, SfxKey.ssAlert, SfxKey.ssDeath, SfxKey.bossActive, SfxKey.hansGrosseDeath, SfxKey.schabbs, SfxKey.schabbsDeath, SfxKey.hitlerGreeting, SfxKey.hitlerDeath, SfxKey.mechaSteps, SfxKey.ottoAlert, SfxKey.gretelDeath, SfxKey.levelComplete, SfxKey.endBonus1, SfxKey.endBonus2, SfxKey.noBonus, SfxKey.percent100, ]; static const List _knownMusicKeys = [ MusicKey.menuTheme, MusicKey.level01, MusicKey.level02, MusicKey.level03, MusicKey.level04, MusicKey.level05, MusicKey.level06, MusicKey.level07, MusicKey.level08, MusicKey.level09, MusicKey.level10, MusicKey.level11, MusicKey.level12, MusicKey.level13, MusicKey.level14, MusicKey.level15, MusicKey.level16, MusicKey.level17, MusicKey.level18, MusicKey.level19, MusicKey.level20, ]; @override void initState() { super.initState(); _selectedGame = widget.wolf3d.maybeActiveGame ?? widget.wolf3d.availableGames.first; } @override void dispose() { // Ensure debug playback does not continue after closing the gallery. unawaited(widget.wolf3d.audio.stopAllAudio()); super.dispose(); } Map> _buildSfxAliases() { final Map> aliasesById = {}; for (final key in _knownSfxKeys) { final ref = _selectedGame.registry.sfx.resolve(key); if (ref == null) { continue; } aliasesById .putIfAbsent(ref.slotIndex, () => {}) .add(_readableKeyName(key.toString(), 'SfxKey(')); } return aliasesById.map( (id, aliases) => MapEntry(id, aliases.toList()..sort()), ); } Map> _buildMusicAliases() { final Map> aliasesById = {}; for (final key in _knownMusicKeys) { final route = _selectedGame.registry.music.resolve(key); if (route == null) { continue; } aliasesById .putIfAbsent(route.trackIndex, () => {}) .add(_readableKeyName(key.toString(), 'MusicKey(')); } return aliasesById.map( (id, aliases) => MapEntry(id, aliases.toList()..sort()), ); } String _readableKeyName(String raw, String prefix) { final String trimmed = raw.startsWith(prefix) && raw.endsWith(')') ? raw.substring(prefix.length, raw.length - 1) : raw; if (trimmed.isEmpty) { return raw; } return trimmed.replaceAllMapped( RegExp(r'([a-z0-9])([A-Z])'), (match) => '${match.group(1)} ${match.group(2)}', ); } Future _stopAllAudioPlayback() async { await widget.wolf3d.audio.stopAllAudio(); if (!mounted) { return; } setState(() { _playingMusicTrackIndex = null; }); } Future _selectGame(WolfensteinData game) async { if (identical(_selectedGame, game)) { return; } await _stopAllAudioPlayback(); widget.wolf3d.setActiveGame(game); if (!mounted) { return; } setState(() { _selectedGame = game; _playingMusicTrackIndex = null; }); } void _playSfx(int id) { widget.wolf3d.audio.playSoundEffect(id); } Future _toggleMusic(int trackIndex) async { if (_playingMusicTrackIndex == trackIndex) { await _stopAllAudioPlayback(); return; } final engineAudio = widget.wolf3d.audio; if (engineAudio is! WolfAudio) { return; } if (trackIndex < 0 || trackIndex >= _selectedGame.music.length) { return; } await engineAudio.playMusic(_selectedGame.music[trackIndex]); if (!mounted) { return; } setState(() { _playingMusicTrackIndex = trackIndex; }); } @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( appBar: AppBar( title: const Text('Audio Gallery'), actions: [ if (widget.wolf3d.availableGames.length > 1) GalleryGameSelector( wolf3d: widget.wolf3d, selectedGame: _selectedGame, onSelected: (game) { unawaited(_selectGame(game)); }, ), ], bottom: const TabBar( tabs: [ Tab(icon: Icon(Icons.graphic_eq), text: 'SFX'), Tab(icon: Icon(Icons.music_note), text: 'Music'), ], ), ), body: TabBarView( children: [ _buildSfxTab(), _buildMusicTab(), ], ), ), ); } Widget _buildSfxTab() { final aliasesById = _buildSfxAliases(); final rows = List<_AudioRow>.generate( _selectedGame.sounds.length, (id) => _AudioRow(id: id, aliases: aliasesById[id] ?? const []), ); if (rows.isEmpty) { return const Center(child: Text('No SFX available in this game data.')); } return Column( children: [ const ListTile( leading: Icon(Icons.info_outline), title: Text('Tap any SFX to play it once'), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { final columns = _gridColumnsForWidth(constraints.maxWidth); return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 2.8, ), itemCount: rows.length, itemBuilder: (context, index) { final row = rows[index]; return Card( child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => _playSfx(row.id), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), child: Row( children: [ CircleAvatar(child: Text('${row.id}')), const SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'SFX ${row.id}', style: Theme.of( context, ).textTheme.titleSmall, ), const SizedBox(height: 2), Text( row.subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.bodySmall, ), ], ), ), const Icon(Icons.play_arrow), ], ), ), ), ); }, ); }, ), ), ], ); } Widget _buildMusicTab() { final aliasesById = _buildMusicAliases(); final rows = List<_AudioRow>.generate( _selectedGame.music.length, (id) => _AudioRow(id: id, aliases: aliasesById[id] ?? const []), ); if (rows.isEmpty) { return const Center( child: Text('No music tracks available in this game data.'), ); } return Column( children: [ ListTile( leading: const Icon(Icons.info_outline), title: const Text('Tap a track to play, tap again to stop'), subtitle: Text( _playingMusicTrackIndex == null ? 'No track currently playing' : 'Playing track ${_playingMusicTrackIndex!}', ), trailing: FilledButton.icon( onPressed: _playingMusicTrackIndex == null ? null : () { unawaited(_stopAllAudioPlayback()); }, icon: const Icon(Icons.stop), label: const Text('Stop'), ), ), Expanded( child: LayoutBuilder( builder: (context, constraints) { final columns = _gridColumnsForWidth(constraints.maxWidth); return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 2.8, ), itemCount: rows.length, itemBuilder: (context, index) { final row = rows[index]; final bool isPlaying = _playingMusicTrackIndex == row.id; return Card( color: isPlaying ? Theme.of(context).colorScheme.primaryContainer : null, child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { unawaited(_toggleMusic(row.id)); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), child: Row( children: [ CircleAvatar(child: Text('${row.id}')), const SizedBox(width: 10), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Track ${row.id}', style: Theme.of( context, ).textTheme.titleSmall, ), const SizedBox(height: 2), Text( row.subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of( context, ).textTheme.bodySmall, ), ], ), ), Icon(isPlaying ? Icons.stop : Icons.play_arrow), ], ), ), ), ); }, ); }, ), ), ], ); } }