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

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