Smarter asset loading. Better audio rendering.
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -59,6 +59,15 @@ class GameSelectScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<ByteData?> tryLoad(String path) async {
|
||||
try {
|
||||
return await rootBundle.load(path);
|
||||
} catch (e) {
|
||||
debugPrint("Asset not found: $path");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<WolfensteinData>> loadData({String? directory}) async {
|
||||
final List<WolfensteinData> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, ByteData?> 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!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> _int32ToBytes(int value) => [
|
||||
value & 0xff,
|
||||
(value >> 8) & 0xff,
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
/// More dartdocs go here.
|
||||
library;
|
||||
|
||||
export 'src/imf_renderer.dart' show ImfRenderer;
|
||||
export 'src/imf_renderer.dart' show ImfRenderer, NumChannels;
|
||||
|
||||
Reference in New Issue
Block a user