Music now sounds like SB16

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 12:57:00 +01:00
parent 283f118ac2
commit 6507950a95

View File

@@ -6,7 +6,6 @@ class Opl2Operator {
double phase = 0.0;
double phaseIncrement = 0.0;
// ADSR
double attackRate = 0.01;
double decayRate = 0.005;
double sustainLevel = 0.5;
@@ -14,10 +13,11 @@ class Opl2Operator {
EnvelopeState envState = EnvelopeState.off;
double envVolume = 0.0;
// FM specific parameters
double multiplier = 1.0;
double volume =
1.0; // Acts as volume for Carrier, but "Modulation Strength" for Modulator
double volume = 1.0;
// Waveform Selection (0-3)
int waveform = 0;
void updateFrequency(double baseFreq) {
phaseIncrement =
@@ -33,7 +33,25 @@ class Opl2Operator {
envState = EnvelopeState.release;
}
// phaseOffset is where the Modulator feeds into the Carrier
// Applies the OPL2 hardware waveform math
double _getOscillatorOutput(double currentPhase) {
// Normalize phase between 0 and 2*pi
double p = currentPhase % (2 * math.pi);
if (p < 0) p += 2 * math.pi;
switch (waveform) {
case 1: // Half-Sine (silence for the second half)
return p < math.pi ? math.sin(p) : 0.0;
case 2: // Absolute-Sine (fully rectified)
return math.sin(p).abs();
case 3: // Quarter-Sine / Pulse (raspy pulse)
return (p % math.pi) < (math.pi / 2.0) ? math.sin(p) : 0.0;
case 0:
default: // Standard Sine
return math.sin(p);
}
}
double getSample(double phaseOffset) {
switch (envState) {
case EnvelopeState.attack:
@@ -65,8 +83,8 @@ class Opl2Operator {
return 0.0;
}
// Add the Modulator's output to our current phase!
double out = math.sin(phase + phaseOffset);
// Pass the phase + modulation offset into our waveform generator!
double out = _getOscillatorOutput(phase + phaseOffset);
phase += phaseIncrement;
if (phase >= 2 * math.pi) phase -= 2 * math.pi;
@@ -83,6 +101,12 @@ class Opl2Channel {
int block = 0;
bool keyOn = false;
bool isAdditive = false;
int feedbackStrength = 0;
double _prevModOutput1 = 0.0;
double _prevModOutput2 = 0.0;
void updateFrequency() {
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
modulator.updateFrequency(baseFreq);
@@ -96,14 +120,30 @@ class Opl2Channel {
return 0.0;
}
// 1. Get the modulator's output (it takes no phase offset itself)
// We multiply it by an arbitrary index (e.g. 2.0) to give it enough strength to warp the carrier
double modOutput = modulator.getSample(0.0) * 2.0;
double feedbackPhase = 0.0;
if (feedbackStrength > 0) {
double averageMod = (_prevModOutput1 + _prevModOutput2) / 2.0;
double feedbackFactor =
math.pow(2, feedbackStrength - 1) * (math.pi / 16.0);
feedbackPhase = averageMod * feedbackFactor;
}
// 2. Feed the modulator's output directly into the carrier's phase!
double carOutput = carrier.getSample(modOutput);
double modOutput = modulator.getSample(feedbackPhase);
return carOutput * 0.1; // Master channel volume reduction
_prevModOutput2 = _prevModOutput1;
_prevModOutput1 = modOutput;
double channelOutput = 0.0;
if (isAdditive) {
double carOutput = carrier.getSample(0.0);
channelOutput = modOutput + carOutput;
} else {
double carOutput = carrier.getSample(modOutput * 2.0);
channelOutput = carOutput;
}
return channelOutput * 0.1;
}
}
@@ -112,14 +152,32 @@ class Opl2Emulator {
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
// The OPL2 maps 18 operators to 9 channels in a very fragmented memory layout.
// This helper array maps a register offset (0x00 to 0x15) to a specific channel and operator type.
// The master lock for waveforms
bool _waveformSelectionEnabled = false;
static const List<int> _operatorMap = [
0, 1, 2, 0, 1, 2, // Offsets 0x00-0x05 (Ch 0-2 Modulators, then Carriers)
-1, -1, // Offsets 0x06-0x07 (Unused)
3, 4, 5, 3, 4, 5, // Offsets 0x08-0x0D (Ch 3-5 Modulators, then Carriers)
-1, -1, // Offsets 0x0E-0x0F (Unused)
6, 7, 8, 6, 7, 8, // Offsets 0x10-0x15 (Ch 6-8 Modulators, then Carriers)
0,
1,
2,
0,
1,
2,
-1,
-1,
3,
4,
5,
3,
4,
5,
-1,
-1,
6,
7,
8,
6,
7,
8,
];
Opl2Operator? _getOperator(int offset) {
@@ -129,19 +187,20 @@ class Opl2Emulator {
return null;
}
int channelIdx = _operatorMap[offset];
// In our map, if the offset relative to the block is < 3, it's a Modulator.
// Otherwise, it's a Carrier.
bool isCarrier = (offset % 8) >= 3;
return isCarrier
? channels[channelIdx].carrier
: channels[channelIdx].modulator;
}
void writeRegister(int reg, int data) {
// --- 0x01: Test / Waveform Enable ---
if (reg == 0x01) {
// Bit 5 must be set to 1 to allow waveform changes
_waveformSelectionEnabled = (data & 0x20) != 0;
}
// --- 0x20 - 0x35: Multiplier ---
if (reg >= 0x20 && reg <= 0x35) {
else if (reg >= 0x20 && reg <= 0x35) {
var op = _getOperator(reg - 0x20);
if (op != null) {
int mult = data & 0x0F;
@@ -152,8 +211,7 @@ class Opl2Emulator {
else if (reg >= 0x40 && reg <= 0x55) {
var op = _getOperator(reg - 0x40);
if (op != null) {
int level = data & 0x3F; // 0 is loudest, 63 is quietest
op.volume = 1.0 - (level / 63.0);
op.volume = 1.0 - ((data & 0x3F) / 63.0);
}
}
// --- 0x60 - 0x75: Attack & Decay ---
@@ -202,6 +260,23 @@ class Opl2Emulator {
channel.carrier.triggerOff();
}
}
// --- 0xC0 - 0xC8: Feedback & Connection ---
else if (reg >= 0xC0 && reg <= 0xC8) {
int channelIdx = reg - 0xC0;
channels[channelIdx].isAdditive = (data & 0x01) != 0;
channels[channelIdx].feedbackStrength = (data & 0x0E) >> 1;
}
// --- 0xE0 - 0xF5: Waveform Selection ---
else if (reg >= 0xE0 && reg <= 0xF5) {
// The chip ignores this register if the master enable bit wasn't set in 0x01
if (_waveformSelectionEnabled) {
var op = _getOperator(reg - 0xE0);
if (op != null) {
// Bottom 2 bits determine the waveform (0-3)
op.waveform = data & 0x03;
}
}
}
}
double generateSample() {