Added ADSR to sound rendering
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
enum EnvelopeState { off, attack, decay, sustain, release }
|
||||
|
||||
class Opl2Channel {
|
||||
int fNum = 0;
|
||||
int block = 0;
|
||||
@@ -8,46 +10,143 @@ class Opl2Channel {
|
||||
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);
|
||||
// ADSR Parameters (0.0 to 1.0 scales or rates per sample)
|
||||
double attackRate = 0.01;
|
||||
double decayRate = 0.005;
|
||||
double sustainLevel = 0.5;
|
||||
double releaseRate = 0.005;
|
||||
|
||||
// Convert that physical frequency into a phase step for our modern 44100Hz output
|
||||
// Current Envelope State
|
||||
EnvelopeState envState = EnvelopeState.off;
|
||||
double envVolume = 0.0;
|
||||
|
||||
void updateFrequency() {
|
||||
double realFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
||||
phaseIncrement = (realFreq * 2 * math.pi) / Opl2Emulator.sampleRate;
|
||||
}
|
||||
|
||||
double getSample() {
|
||||
// If the note isn't being pressed, return silence
|
||||
if (!keyOn) return 0.0;
|
||||
void triggerOn() {
|
||||
if (!keyOn) {
|
||||
keyOn = true;
|
||||
phase = 0.0;
|
||||
envState = EnvelopeState.attack;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the raw sine wave output
|
||||
void triggerOff() {
|
||||
keyOn = false;
|
||||
envState = EnvelopeState.release;
|
||||
}
|
||||
|
||||
double getSample() {
|
||||
// Process the ADSR state machine
|
||||
switch (envState) {
|
||||
case EnvelopeState.attack:
|
||||
envVolume += attackRate;
|
||||
if (envVolume >= 1.0) {
|
||||
envVolume = 1.0;
|
||||
envState = EnvelopeState.decay;
|
||||
}
|
||||
break;
|
||||
case EnvelopeState.decay:
|
||||
envVolume -= decayRate;
|
||||
if (envVolume <= sustainLevel) {
|
||||
envVolume = sustainLevel;
|
||||
envState = EnvelopeState.sustain;
|
||||
}
|
||||
break;
|
||||
case EnvelopeState.sustain:
|
||||
// Holds steady at the sustain level
|
||||
envVolume = sustainLevel;
|
||||
break;
|
||||
case EnvelopeState.release:
|
||||
envVolume -= releaseRate;
|
||||
if (envVolume <= 0.0) {
|
||||
envVolume = 0.0;
|
||||
envState = EnvelopeState.off;
|
||||
}
|
||||
break;
|
||||
case EnvelopeState.off:
|
||||
envVolume = 0.0;
|
||||
return 0.0; // Early exit for silence
|
||||
}
|
||||
|
||||
// Generate the sine wave
|
||||
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;
|
||||
// Multiply the raw sine wave by our current envelope volume
|
||||
return (out * envVolume) * 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;
|
||||
// Real OPL2 chips use 2 operators (oscillators) per channel.
|
||||
// This maps the IMF register offsets to find the "Carrier" operator for our 9 channels.
|
||||
static const List<int> _carrierOffsets = [
|
||||
0x03,
|
||||
0x04,
|
||||
0x05,
|
||||
0x0B,
|
||||
0x0C,
|
||||
0x0D,
|
||||
0x13,
|
||||
0x14,
|
||||
0x15,
|
||||
];
|
||||
|
||||
// Keep the upper 2 bits intact, replace the lower 8 bits
|
||||
int _getChannelIndexForOperator(int regOffset) {
|
||||
return _carrierOffsets.indexOf(regOffset);
|
||||
}
|
||||
|
||||
void writeRegister(int reg, int data) {
|
||||
// --- 0x60 - 0x75: Attack (Upper 4 bits) & Decay (Lower 4 bits) ---
|
||||
if (reg >= 0x60 && reg <= 0x75) {
|
||||
int offset = reg - 0x60;
|
||||
int channelIdx = _getChannelIndexForOperator(offset);
|
||||
|
||||
if (channelIdx != -1) {
|
||||
// If it's a carrier operator
|
||||
int attack = (data & 0xF0) >> 4;
|
||||
int decay = data & 0x0F;
|
||||
|
||||
// Map the 0-15 hardware values to our simple rate scales
|
||||
// (15 is fastest, 0 is slowest/infinite)
|
||||
channels[channelIdx].attackRate = attack == 0
|
||||
? 0.0
|
||||
: (attack / 15.0) * 0.05;
|
||||
channels[channelIdx].decayRate = decay == 0
|
||||
? 0.0
|
||||
: (decay / 15.0) * 0.005;
|
||||
}
|
||||
}
|
||||
// --- 0x80 - 0x95: Sustain (Upper 4 bits) & Release (Lower 4 bits) ---
|
||||
else if (reg >= 0x80 && reg <= 0x95) {
|
||||
int offset = reg - 0x80;
|
||||
int channelIdx = _getChannelIndexForOperator(offset);
|
||||
|
||||
if (channelIdx != -1) {
|
||||
int sustain = (data & 0xF0) >> 4;
|
||||
int release = data & 0x0F;
|
||||
|
||||
// OPL2 sustain is inverted (0 is loudest, 15 is quietest)
|
||||
channels[channelIdx].sustainLevel = 1.0 - (sustain / 15.0);
|
||||
channels[channelIdx].releaseRate = release == 0
|
||||
? 0.0
|
||||
: (release / 15.0) * 0.005;
|
||||
}
|
||||
}
|
||||
// --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
|
||||
else if (reg >= 0xA0 && reg <= 0xA8) {
|
||||
int channelIdx = reg - 0xA0;
|
||||
channels[channelIdx].fNum = (channels[channelIdx].fNum & 0x300) | data;
|
||||
channels[channelIdx].updateFrequency();
|
||||
}
|
||||
@@ -56,34 +155,25 @@ class Opl2Emulator {
|
||||
int channelIdx = reg - 0xB0;
|
||||
Opl2Channel channel = channels[channelIdx];
|
||||
|
||||
// Track the previous key state to prevent constant phase resetting
|
||||
bool wasKeyOn = channel.keyOn;
|
||||
|
||||
// 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;
|
||||
bool newKeyOn = (data & 0x20) != 0;
|
||||
channel.block = (data & 0x1C) >> 2;
|
||||
channel.fNum = (channel.fNum & 0xFF) | ((data & 0x03) << 8);
|
||||
channel.updateFrequency();
|
||||
|
||||
// ONLY reset the phase if the note was just struck
|
||||
if (!wasKeyOn && channel.keyOn) {
|
||||
channel.phase = 0.0;
|
||||
// Trigger the ADSR envelope appropriately
|
||||
if (newKeyOn && !channel.keyOn) {
|
||||
channel.triggerOn();
|
||||
} else if (!newKeyOn && channel.keyOn) {
|
||||
channel.triggerOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user