Music now sounds like SB16
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user