Smarter asset loading. Better audio rendering.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 13:40:47 +01:00
parent 649a1419a8
commit 27713dbbfb
5 changed files with 122 additions and 86 deletions

View File

@@ -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!,
);
}
}

View File

@@ -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,

View File

@@ -3,4 +3,4 @@
/// More dartdocs go here.
library;
export 'src/imf_renderer.dart' show ImfRenderer;
export 'src/imf_renderer.dart' show ImfRenderer, NumChannels;