From 27713dbbfb0040d07548c01797e9a69d6c99db39 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 13:40:47 +0100 Subject: [PATCH] Smarter asset loading. Better audio rendering. Signed-off-by: Hans Kokx --- .vscode/launch.json | 9 ++- lib/game_select_screen.dart | 55 ++++++------- .../lib/src/wolfenstein_loader.dart | 61 +++++++++----- .../wolf_3d_synth/lib/src/imf_renderer.dart | 81 +++++++++++-------- packages/wolf_3d_synth/lib/wolf_3d_synth.dart | 2 +- 5 files changed, 122 insertions(+), 86 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c582c66..d2f7b06 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,19 +7,22 @@ { "name": "wolf_dart", "request": "launch", - "type": "dart" + "type": "dart", + "program": "lib/main.dart" }, { "name": "wolf_dart (profile mode)", "request": "launch", "type": "dart", - "flutterMode": "profile" + "flutterMode": "profile", + "program": "lib/main.dart" }, { "name": "wolf_dart (release mode)", "request": "launch", "type": "dart", - "flutterMode": "release" + "flutterMode": "release", + "program": "lib/main.dart" } ] } \ No newline at end of file diff --git a/lib/game_select_screen.dart b/lib/game_select_screen.dart index fc3e2fc..68abc42 100644 --- a/lib/game_select_screen.dart +++ b/lib/game_select_screen.dart @@ -59,6 +59,15 @@ class GameSelectScreen extends StatelessWidget { ); } + Future tryLoad(String path) async { + try { + return await rootBundle.load(path); + } catch (e) { + debugPrint("Asset not found: $path"); + return null; + } + } + Future> loadData({String? directory}) async { final List loadedGames = []; @@ -68,40 +77,28 @@ class GameSelectScreen extends StatelessWidget { (version: GameVersion.shareware, path: 'shareware'), ]; - for (final config in versionsToTry) { + for (final version in versionsToTry) { try { + final ext = version.version.fileExtension; + final folder = 'assets/${version.path}'; + 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}', - ), + version: version.version, + vswap: await tryLoad('$folder/VSWAP.$ext'), + mapHead: await tryLoad('$folder/MAPHEAD.$ext'), + gameMaps: await tryLoad('$folder/GAMEMAPS.$ext'), + vgaDict: await tryLoad('$folder/VGADICT.$ext'), + vgaHead: await tryLoad('$folder/VGAHEAD.$ext'), + vgaGraph: await tryLoad('$folder/VGAGRAPH.$ext'), + audioHed: await tryLoad('$folder/AUDIOHED.$ext'), + audioT: await tryLoad('$folder/AUDIOT.$ext'), ); + loadedGames.add(data); } catch (e) { - debugPrint( - "Bundled ${config.version.name} not found or failed to load.", - ); + // The loader now provides the specific error: + // "ArgumentError: Cannot load retail: Missing files: VSWAP.WL6, ..." + debugPrint(e.toString()); } } diff --git a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart index 893d1b0..6c4ba54 100644 --- a/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart +++ b/packages/wolf_3d_data/lib/src/wolfenstein_loader.dart @@ -23,30 +23,53 @@ class WolfensteinLoader { ); } - /// Parses WolfensteinData directly from raw ByteData. - /// This is 100% pure Dart and is safe to use on all platforms, including Web. + /// Parses WolfensteinData from raw ByteData. + /// Throws an [ArgumentError] if any required file is null. static WolfensteinData loadFromBytes({ required GameVersion version, - required ByteData vswap, - required ByteData mapHead, - required ByteData gameMaps, - required ByteData vgaDict, - required ByteData vgaHead, - required ByteData vgaGraph, - required ByteData audioHed, - required ByteData audioT, + required ByteData? vswap, + required ByteData? mapHead, + required ByteData? gameMaps, + 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 + // 1. Validation Check + final Map files = { + 'VSWAP': vswap, + 'MAPHEAD': mapHead, + 'GAMEMAPS': gameMaps, + 'VGADICT': vgaDict, + 'VGAHEAD': vgaHead, + 'VGAGRAPH': vgaGraph, + 'AUDIOHED': audioHed, + 'AUDIOT': audioT, + }; + + final missing = files.entries + .where((e) => e.value == null) + .map((e) => "${e.key}.${version.fileExtension}") + .toList(); + + if (missing.isNotEmpty) { + throw ArgumentError( + 'Cannot load ${version.name}: Missing files: ${missing.join(", ")}', + ); + } + + // 2. Pass-through to parser now that we are guaranteed non-null return WLParser.load( version: version, - vswap: vswap, - mapHead: mapHead, - gameMaps: gameMaps, - vgaDict: vgaDict, - vgaHead: vgaHead, - vgaGraph: vgaGraph, - audioHed: audioHed, - audioT: audioT, + vswap: vswap!, + mapHead: mapHead!, + gameMaps: gameMaps!, + vgaDict: vgaDict!, + vgaHead: vgaHead!, + vgaGraph: vgaGraph!, + audioHed: audioHed!, + audioT: audioT!, ); } } diff --git a/packages/wolf_3d_synth/lib/src/imf_renderer.dart b/packages/wolf_3d_synth/lib/src/imf_renderer.dart index 547d21a..d42e4a0 100644 --- a/packages/wolf_3d_synth/lib/src/imf_renderer.dart +++ b/packages/wolf_3d_synth/lib/src/imf_renderer.dart @@ -5,75 +5,89 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'opl2_emulator.dart'; class ImfRenderer { - /// Renders an ImfMusic track into raw 16-bit PCM samples + /// Renders music in Stereo with a 10ms Haas widening effect. static Int16List render(ImfMusic music) { final emulator = Opl2Emulator(); - const double samplesPerTick = Opl2Emulator.sampleRate / 700.0; - // 1. Pre-calculate the total number of audio samples we need int totalTicks = 0; for (final inst in music.instructions) { totalTicks += inst.delay; } - int totalSamples = (totalTicks * samplesPerTick).round(); - // 2. Pre-allocate a fixed-size buffer (blazing fast!) - final pcmBuffer = Int16List(totalSamples); + // Allocate buffer for Stereo (2 channels per sample) + int totalMonoSamples = (totalTicks * samplesPerTick).round(); + final pcmBuffer = Int16List(totalMonoSamples * 2); int sampleIndex = 0; - for (final instruction in music.instructions) { - // Write the instruction to the chip - emulator.writeRegister(instruction.register, instruction.data); + // --- Stereo Widening (Haas Effect) --- + // 10ms at 44.1kHz = 441 samples. + // This is tight enough to sound like 'width' rather than 'echo'. + const int delaySamples = 441; + final delayLine = Float32List(delaySamples); + int delayPtr = 0; - // Generate the samples for the delay period + for (final instruction in music.instructions) { + emulator.writeRegister(instruction.register, instruction.data); int samplesToGenerate = (instruction.delay * samplesPerTick).round(); for (int i = 0; i < samplesToGenerate; i++) { - if (sampleIndex >= pcmBuffer.length) break; // Safety bounds + if (sampleIndex + 1 >= pcmBuffer.length) break; - double sampleFloat = emulator.generateSample(); - int sampleInt = (sampleFloat * 32767).toInt().clamp(-32768, 32767); + double monoSample = emulator.generateSample(); - pcmBuffer[sampleIndex++] = sampleInt; + // Left channel: Immediate + double left = monoSample; + // Right channel: Delayed + double right = delayLine[delayPtr]; + + delayLine[delayPtr] = monoSample; + delayPtr = (delayPtr + 1) % delaySamples; + + // Interleave L and R + pcmBuffer[sampleIndex++] = (left * 32767).toInt().clamp(-32768, 32767); + pcmBuffer[sampleIndex++] = (right * 32767).toInt().clamp(-32768, 32767); } } - return pcmBuffer; } - /// Wraps raw 16-bit PCM data in a standard WAV file header - static Uint8List createWavFile(Int16List pcmData) { - int byteRate = Opl2Emulator.sampleRate * 2; // 1 channel * 16-bit (2 bytes) - int dataSize = pcmData.length * 2; - int fileSize = 36 + dataSize; + /// Wraps raw PCM data into a standard Stereo WAV file header. + static Uint8List createWavFile( + Int16List pcmData, { + int sampleRate = Opl2Emulator.sampleRate, + }) { + const int numChannels = 2; + const int bytesPerSample = 2; + const int blockAlign = numChannels * bytesPerSample; + + final int dataSize = pcmData.length * bytesPerSample; + final int byteRate = sampleRate * blockAlign; + final int fileSize = 36 + dataSize; final bytes = BytesBuilder(); - // "RIFF" chunk descriptor + // RIFF header bytes.add('RIFF'.codeUnits); bytes.add(_int32ToBytes(fileSize)); bytes.add('WAVE'.codeUnits); - // "fmt " sub-chunk + // Format chunk bytes.add('fmt '.codeUnits); - bytes.add(_int32ToBytes(16)); // Subchunk1Size (16 for PCM) - bytes.add(_int16ToBytes(1)); // AudioFormat (1 = PCM) - bytes.add(_int16ToBytes(1)); // NumChannels (1 = Mono) - bytes.add(_int32ToBytes(Opl2Emulator.sampleRate)); // SampleRate - bytes.add(_int32ToBytes(byteRate)); // ByteRate - bytes.add(_int16ToBytes(2)); // BlockAlign - bytes.add(_int16ToBytes(16)); // BitsPerSample + bytes.add(_int32ToBytes(16)); + bytes.add(_int16ToBytes(1)); + bytes.add(_int16ToBytes(numChannels)); + bytes.add(_int32ToBytes(sampleRate)); + bytes.add(_int32ToBytes(byteRate)); + bytes.add(_int16ToBytes(blockAlign)); + bytes.add(_int16ToBytes(16)); - // "data" sub-chunk + // Data chunk bytes.add('data'.codeUnits); bytes.add(_int32ToBytes(dataSize)); - // Append the actual raw audio data (Blazing Fast version) final byteData = ByteData(dataSize); for (int i = 0; i < pcmData.length; i++) { - // Multiply by 2 because each Int16 takes up 2 bytes. - // Endian.little is strictly required for the standard WAV format. byteData.setInt16(i * 2, pcmData[i], Endian.little); } bytes.add(byteData.buffer.asUint8List()); @@ -85,7 +99,6 @@ class ImfRenderer { value & 0xff, (value >> 8) & 0xff, ]; - static List _int32ToBytes(int value) => [ value & 0xff, (value >> 8) & 0xff, diff --git a/packages/wolf_3d_synth/lib/wolf_3d_synth.dart b/packages/wolf_3d_synth/lib/wolf_3d_synth.dart index 5c15b0c..33645b0 100644 --- a/packages/wolf_3d_synth/lib/wolf_3d_synth.dart +++ b/packages/wolf_3d_synth/lib/wolf_3d_synth.dart @@ -3,4 +3,4 @@ /// More dartdocs go here. library; -export 'src/imf_renderer.dart' show ImfRenderer; +export 'src/imf_renderer.dart' show ImfRenderer, NumChannels;