Fix errors when parsing audio
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
91
packages/wolf_3d_synth/lib/src/imf_renderer.dart
Normal file
91
packages/wolf_3d_synth/lib/src/imf_renderer.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
86
packages/wolf_3d_synth/lib/src/opl2_emulator.dart
Normal file
86
packages/wolf_3d_synth/lib/src/opl2_emulator.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user