From 9d1f38752a9d35dc2c17e3a2631c1ef211d5e2d5 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 11:36:25 +0100 Subject: [PATCH] Added audio loading and decompressing Signed-off-by: Hans Kokx --- lib/game_select_screen.dart | 99 +++++++++++-------- .../lib/src/classes/game_file.dart | 4 +- .../wolf_3d_data/lib/src/classes/sound.dart | 10 ++ .../lib/src/classes/wolfenstein_data.dart | 4 + .../wolf_3d_data/lib/src/io/discovery_io.dart | 2 + packages/wolf_3d_data/lib/src/wl_parser.dart | 58 +++++++++++ .../lib/src/wolfenstein_loader.dart | 4 + packages/wolf_3d_data/lib/wolf_3d_data.dart | 2 +- 8 files changed, 142 insertions(+), 41 deletions(-) diff --git a/lib/game_select_screen.dart b/lib/game_select_screen.dart index 8e92c82..346341e 100644 --- a/lib/game_select_screen.dart +++ b/lib/game_select_screen.dart @@ -58,47 +58,68 @@ class GameSelectScreen extends StatelessWidget { ); } - Future> loadData([ - bool? isShareware, - ]) async { + Future> loadData([String? directory]) async { final List loadedGames = []; - if (kIsWeb) { - switch (isShareware) { - case false: - loadedGames.add( - WolfensteinLoader.loadFromBytes( - version: GameVersion.retail, - vswap: await rootBundle.load('assets/retail/VSWAP.WL6'), - mapHead: await rootBundle.load('assets/retail/MAPHEAD.WL6'), - gameMaps: await rootBundle.load('assets/retail/GAMEMAPS.WL6'), - vgaDict: await rootBundle.load('assets/retail/VGADICT.WL6'), - vgaHead: await rootBundle.load('assets/retail/VGAHEAD.WL6'), - vgaGraph: await rootBundle.load('assets/retail/VGAGRAPH.WL6'), - ), - ); - break; - default: - loadedGames.add( - WolfensteinLoader.loadFromBytes( - version: GameVersion.shareware, - vswap: await rootBundle.load('assets/shareware/VSWAP.WL1'), - mapHead: await rootBundle.load('assets/shareware/MAPHEAD.WL1'), - gameMaps: await rootBundle.load('assets/shareware/GAMEMAPS.WL1'), - vgaDict: await rootBundle.load('assets/shareware/VGADICT.WL1'), - vgaHead: await rootBundle.load('assets/shareware/VGAHEAD.WL1'), - vgaGraph: await rootBundle.load('assets/shareware/VGAGRAPH.WL1'), - ), - ); - break; - } - } else { - final Map discoveredVersions = - await WolfensteinLoader.discover( - directoryPath: 'assets', - recursive: true, - ); - loadedGames.addAll(discoveredVersions.values); + // 1. Always attempt to load bundled assets first (works on ALL platforms) + final versionsToTry = [ + (version: GameVersion.retail, path: 'retail'), + (version: GameVersion.shareware, path: 'shareware'), + ]; + + for (final config in versionsToTry) { + try { + final data = WolfensteinLoader.loadFromBytes( + version: config.version, + vswap: await rootBundle.load( + 'assets/${config.path}/VSWAP.${config.version.fileExtension}', + ), + mapHead: await rootBundle.load( + 'assets/${config.path}/MAPHEAD.${config.version.fileExtension}', + ), + gameMaps: await rootBundle.load( + 'assets/${config.path}/GAMEMAPS.${config.version.fileExtension}', + ), + vgaDict: await rootBundle.load( + 'assets/${config.path}/VGADICT.${config.version.fileExtension}', + ), + vgaHead: await rootBundle.load( + 'assets/${config.path}/VGAHEAD.${config.version.fileExtension}', + ), + vgaGraph: await rootBundle.load( + 'assets/${config.path}/VGAGRAPH.${config.version.fileExtension}', + ), + audioHed: await rootBundle.load( + 'assets/${config.path}/AUDIOHED.${config.version.fileExtension}', + ), + audioT: await rootBundle.load( + 'assets/${config.path}/AUDIOT.${config.version.fileExtension}', + ), + ); + loadedGames.add(data); + } catch (e) { + debugPrint( + "Bundled ${config.version.name} not found or failed to load.", + ); + } + } + + // 2. On non-web, also check for external files in a specific "games" folder + // if you want to support side-loading. + if (!kIsWeb) { + try { + final externalGames = await WolfensteinLoader.discover( + directoryPath: directory, + recursive: true, + ); + for (var entry in externalGames.entries) { + if (!loadedGames.any((g) => g.version == entry.key)) { + loadedGames.add(entry.value); + } + } + } catch (e) { + debugPrint("External discovery failed: $e"); + } } return loadedGames; diff --git a/packages/wolf_3d_data/lib/src/classes/game_file.dart b/packages/wolf_3d_data/lib/src/classes/game_file.dart index 87c5d35..28be8f0 100644 --- a/packages/wolf_3d_data/lib/src/classes/game_file.dart +++ b/packages/wolf_3d_data/lib/src/classes/game_file.dart @@ -4,7 +4,9 @@ enum GameFile { gameMaps('GAMEMAPS'), vgaDict('VGADICT'), vgaHead('VGAHEAD'), - vgaGraph('VGAGRAPH') + vgaGraph('VGAGRAPH'), + audioHed('AUDIOHED'), + audioT('AUDIOT') ; final String baseName; diff --git a/packages/wolf_3d_data/lib/src/classes/sound.dart b/packages/wolf_3d_data/lib/src/classes/sound.dart index 8aae77d..f126f8c 100644 --- a/packages/wolf_3d_data/lib/src/classes/sound.dart +++ b/packages/wolf_3d_data/lib/src/classes/sound.dart @@ -4,3 +4,13 @@ class PcmSound { final Uint8List bytes; PcmSound(this.bytes); } + +class AdLibSound { + final Uint8List bytes; + AdLibSound(this.bytes); +} + +class ImfMusic { + final Uint8List bytes; + ImfMusic(this.bytes); +} diff --git a/packages/wolf_3d_data/lib/src/classes/wolfenstein_data.dart b/packages/wolf_3d_data/lib/src/classes/wolfenstein_data.dart index b0fbd8d..1ee3ca3 100644 --- a/packages/wolf_3d_data/lib/src/classes/wolfenstein_data.dart +++ b/packages/wolf_3d_data/lib/src/classes/wolfenstein_data.dart @@ -5,6 +5,8 @@ class WolfensteinData { final List walls; final List sprites; final List sounds; + final List adLibSounds; + final List music; final List levels; final List vgaImages; @@ -13,6 +15,8 @@ class WolfensteinData { required this.walls, required this.sprites, required this.sounds, + required this.adLibSounds, + required this.music, required this.levels, required this.vgaImages, }); diff --git a/packages/wolf_3d_data/lib/src/io/discovery_io.dart b/packages/wolf_3d_data/lib/src/io/discovery_io.dart index c56a7e2..05f7ed0 100644 --- a/packages/wolf_3d_data/lib/src/io/discovery_io.dart +++ b/packages/wolf_3d_data/lib/src/io/discovery_io.dart @@ -62,6 +62,8 @@ Future> discoverInDirectory({ vgaDict: await _readFile(foundFiles[GameFile.vgaDict]!), vgaHead: await _readFile(foundFiles[GameFile.vgaHead]!), vgaGraph: await _readFile(foundFiles[GameFile.vgaGraph]!), + audioHed: await _readFile(foundFiles[GameFile.audioHed]!), + audioT: await _readFile(foundFiles[GameFile.audioT]!), ); loadedVersions[version] = data; diff --git a/packages/wolf_3d_data/lib/src/wl_parser.dart b/packages/wolf_3d_data/lib/src/wl_parser.dart index bdb2a07..5908a14 100644 --- a/packages/wolf_3d_data/lib/src/wl_parser.dart +++ b/packages/wolf_3d_data/lib/src/wl_parser.dart @@ -47,6 +47,8 @@ abstract class WLParser { vgaDict: await fileFetcher('VGADICT.$ext'), vgaHead: await fileFetcher('VGAHEAD.$ext'), vgaGraph: await fileFetcher('VGAGRAPH.$ext'), + audioHed: await fileFetcher('AUDIOHED.$ext'), + audioT: await fileFetcher('AUDIOT.$ext'), ); } @@ -61,9 +63,13 @@ abstract class WLParser { required ByteData vgaDict, required ByteData vgaHead, required ByteData vgaGraph, + required ByteData audioHed, + required ByteData audioT, }) { final isShareware = version == GameVersion.shareware; + final audio = parseAudio(audioHed, audioT); + return WolfensteinData( version: version, walls: parseWalls(vswap), @@ -71,6 +77,8 @@ abstract class WLParser { sounds: parseSounds(vswap).map((bytes) => PcmSound(bytes)).toList(), levels: parseMaps(mapHead, gameMaps, isShareware: isShareware), vgaImages: parseVgaImages(vgaDict, vgaHead, vgaGraph), + adLibSounds: audio.adLib, + music: audio.music, ); } @@ -289,6 +297,56 @@ abstract class WLParser { return levels; } + /// Extracts AdLib sounds and IMF music tracks from the audio files. + static ({List adLib, List music}) parseAudio( + ByteData audioHed, + ByteData audioT, + ) { + List offsets = []; + // AUDIOHED is a series of 32-bit unsigned integers + for (int i = 0; i < audioHed.lengthInBytes ~/ 4; i++) { + offsets.add(audioHed.getUint32(i * 4, Endian.little)); + } + + List allAudioChunks = []; + + for (int i = 0; i < offsets.length - 1; i++) { + int start = offsets[i]; + int next = offsets[i + 1]; + + // 0xFFFFFFFF (or 4294967295) marks an empty slot + if (start == 0xFFFFFFFF || start >= audioT.lengthInBytes) { + allAudioChunks.add(Uint8List(0)); + continue; + } + + int length = next - start; + if (length <= 0) { + allAudioChunks.add(Uint8List(0)); + } else { + allAudioChunks.add( + audioT.buffer.asUint8List(audioT.offsetInBytes + start, length), + ); + } + } + + // Wolfenstein 3D split: + // Chunks 0-299: AdLib Sounds + // Chunks 300+: IMF Music + List adLib = allAudioChunks + .take(300) + .map((bytes) => AdLibSound(bytes)) + .toList(); + + List music = allAudioChunks + .skip(300) + .where((chunk) => chunk.isNotEmpty) + .map((bytes) => ImfMusic(bytes)) + .toList(); + + return (adLib: adLib, music: music); + } + // --- ALGORITHM 1: CARMACK EXPANSION --- static Uint16List _expandCarmack(Uint8List compressed) { ByteData data = ByteData.sublistView(compressed); diff --git a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart index 96ac9f3..dcda9d9 100644 --- a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart +++ b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart @@ -33,6 +33,8 @@ class WolfensteinLoader { required ByteData vgaDict, required ByteData vgaHead, required ByteData vgaGraph, + required ByteData audioHed, + required ByteData audioT, }) { // We just act as a clean pass-through to the core parser return WLParser.load( @@ -43,6 +45,8 @@ class WolfensteinLoader { vgaDict: vgaDict, vgaHead: vgaHead, vgaGraph: vgaGraph, + audioHed: audioHed, + audioT: audioT, ); } } diff --git a/packages/wolf_3d_data/lib/wolf_3d_data.dart b/packages/wolf_3d_data/lib/wolf_3d_data.dart index 666bd4b..a9320b0 100644 --- a/packages/wolf_3d_data/lib/wolf_3d_data.dart +++ b/packages/wolf_3d_data/lib/wolf_3d_data.dart @@ -6,7 +6,7 @@ library; export 'src/classes/game_file.dart' show GameFile; export 'src/classes/game_version.dart' show GameVersion; export 'src/classes/image.dart' show VgaImage; -export 'src/classes/sound.dart' show PcmSound; +export 'src/classes/sound.dart' show PcmSound, AdLibSound, ImfMusic; export 'src/classes/sprite.dart' hide Matrix; export 'src/classes/wolf_level.dart' show WolfLevel; export 'src/classes/wolfenstein_data.dart' show WolfensteinData;