Fix errors when parsing audio

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 12:26:35 +01:00
parent b0f75d9f10
commit 431126f893
19 changed files with 385 additions and 62 deletions

View File

@@ -0,0 +1,91 @@
import 'dart:typed_data';
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
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);
int sampleIndex = 0;
for (final instruction in music.instructions) {
// Write the instruction to the chip
emulator.writeRegister(instruction.register, instruction.data);
// Generate the samples for the delay period
int samplesToGenerate = (instruction.delay * samplesPerTick).round();
for (int i = 0; i < samplesToGenerate; i++) {
if (sampleIndex >= pcmBuffer.length) break; // Safety bounds
double sampleFloat = emulator.generateSample();
int sampleInt = (sampleFloat * 32767).toInt().clamp(-32768, 32767);
pcmBuffer[sampleIndex++] = sampleInt;
}
}
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;
final bytes = BytesBuilder();
// "RIFF" chunk descriptor
bytes.add('RIFF'.codeUnits);
bytes.add(_int32ToBytes(fileSize));
bytes.add('WAVE'.codeUnits);
// "fmt " sub-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
// "data" sub-chunk
bytes.add('data'.codeUnits);
bytes.add(_int32ToBytes(dataSize));
// Append the actual raw audio data
for (int sample in pcmData) {
bytes.add(_int16ToBytes(sample));
}
return bytes.toBytes();
}
static List<int> _int16ToBytes(int value) => [
value & 0xff,
(value >> 8) & 0xff,
];
static List<int> _int32ToBytes(int value) => [
value & 0xff,
(value >> 8) & 0xff,
(value >> 16) & 0xff,
(value >> 24) & 0xff,
];
}

View File

@@ -0,0 +1,86 @@
import 'dart:math' as math;
class Opl2Channel {
int fNum = 0;
int block = 0;
bool keyOn = false;
double phase = 0.0;
double phaseIncrement = 0.0;
void updateFrequency() {
// The magic Yamaha YM3812 frequency formula!
// 49716Hz is the internal sample rate of the original 1980s silicon.
double realFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
// Convert that physical frequency into a phase step for our modern 44100Hz output
phaseIncrement = (realFreq * 2 * math.pi) / Opl2Emulator.sampleRate;
}
double getSample() {
// If the note isn't being pressed, return silence
if (!keyOn) return 0.0;
// Generate the raw sine wave output
double out = math.sin(phase);
// Advance the phase for the next frame
phase += phaseIncrement;
if (phase >= 2 * math.pi) {
phase -= 2 * math.pi;
}
// Scale the volume down significantly. If 9 channels play at 1.0 volume,
// the audio will severely clip and distort.
return out * 0.1;
}
}
class Opl2Emulator {
static const int sampleRate = 44100;
// The 9 independent audio channels
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
void writeRegister(int reg, int data) {
// --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
if (reg >= 0xA0 && reg <= 0xA8) {
int channelIdx = reg - 0xA0;
// Keep the upper 2 bits intact, replace the lower 8 bits
channels[channelIdx].fNum = (channels[channelIdx].fNum & 0x300) | data;
channels[channelIdx].updateFrequency();
}
// --- 0xB0 - 0xB8: Key-On, Block, F-Number (Upper 2 bits) ---
else if (reg >= 0xB0 && reg <= 0xB8) {
int channelIdx = reg - 0xB0;
Opl2Channel channel = channels[channelIdx];
// Extract the bits using bitwise masks
channel.keyOn = (data & 0x20) != 0; // Bit 5
channel.block = (data & 0x1C) >> 2; // Bits 2-4
int fNumHigh = (data & 0x03) << 8; // Bits 0-1
// Keep the lower 8 bits intact, replace the upper 2 bits
channel.fNum = (channel.fNum & 0xFF) | fNumHigh;
channel.updateFrequency();
// When a new note is struck, we reset the phase counter to 0
if (channel.keyOn) {
channel.phase = 0.0;
}
}
}
/// Mixes the 9 channels down into a single PCM audio sample
double generateSample() {
double mixedOutput = 0.0;
for (var channel in channels) {
mixedOutput += channel.getSample();
}
// Hard limiter to prevent audio popping if the mix exceeds 1.0
return mixedOutput.clamp(-1.0, 1.0);
}
}

View File

@@ -1,6 +0,0 @@
// TODO: Put public facing types in this file.
/// Checks if you are awesome. Spoiler: you are.
class Awesome {
bool get isAwesome => true;
}

View File

@@ -3,6 +3,4 @@
/// More dartdocs go here.
library;
export 'src/wolf_3d_synth_base.dart';
// TODO: Export any libraries intended for clients of this package.
export 'src/imf_renderer.dart' show ImfRenderer;