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 phase = 0.0;
|
||||||
double phaseIncrement = 0.0;
|
double phaseIncrement = 0.0;
|
||||||
|
|
||||||
// ADSR
|
|
||||||
double attackRate = 0.01;
|
double attackRate = 0.01;
|
||||||
double decayRate = 0.005;
|
double decayRate = 0.005;
|
||||||
double sustainLevel = 0.5;
|
double sustainLevel = 0.5;
|
||||||
@@ -14,10 +13,11 @@ class Opl2Operator {
|
|||||||
EnvelopeState envState = EnvelopeState.off;
|
EnvelopeState envState = EnvelopeState.off;
|
||||||
double envVolume = 0.0;
|
double envVolume = 0.0;
|
||||||
|
|
||||||
// FM specific parameters
|
|
||||||
double multiplier = 1.0;
|
double multiplier = 1.0;
|
||||||
double volume =
|
double volume = 1.0;
|
||||||
1.0; // Acts as volume for Carrier, but "Modulation Strength" for Modulator
|
|
||||||
|
// Waveform Selection (0-3)
|
||||||
|
int waveform = 0;
|
||||||
|
|
||||||
void updateFrequency(double baseFreq) {
|
void updateFrequency(double baseFreq) {
|
||||||
phaseIncrement =
|
phaseIncrement =
|
||||||
@@ -33,7 +33,25 @@ class Opl2Operator {
|
|||||||
envState = EnvelopeState.release;
|
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) {
|
double getSample(double phaseOffset) {
|
||||||
switch (envState) {
|
switch (envState) {
|
||||||
case EnvelopeState.attack:
|
case EnvelopeState.attack:
|
||||||
@@ -65,8 +83,8 @@ class Opl2Operator {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the Modulator's output to our current phase!
|
// Pass the phase + modulation offset into our waveform generator!
|
||||||
double out = math.sin(phase + phaseOffset);
|
double out = _getOscillatorOutput(phase + phaseOffset);
|
||||||
|
|
||||||
phase += phaseIncrement;
|
phase += phaseIncrement;
|
||||||
if (phase >= 2 * math.pi) phase -= 2 * math.pi;
|
if (phase >= 2 * math.pi) phase -= 2 * math.pi;
|
||||||
@@ -83,6 +101,12 @@ class Opl2Channel {
|
|||||||
int block = 0;
|
int block = 0;
|
||||||
bool keyOn = false;
|
bool keyOn = false;
|
||||||
|
|
||||||
|
bool isAdditive = false;
|
||||||
|
int feedbackStrength = 0;
|
||||||
|
|
||||||
|
double _prevModOutput1 = 0.0;
|
||||||
|
double _prevModOutput2 = 0.0;
|
||||||
|
|
||||||
void updateFrequency() {
|
void updateFrequency() {
|
||||||
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
||||||
modulator.updateFrequency(baseFreq);
|
modulator.updateFrequency(baseFreq);
|
||||||
@@ -96,14 +120,30 @@ class Opl2Channel {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Get the modulator's output (it takes no phase offset itself)
|
double feedbackPhase = 0.0;
|
||||||
// We multiply it by an arbitrary index (e.g. 2.0) to give it enough strength to warp the carrier
|
if (feedbackStrength > 0) {
|
||||||
double modOutput = modulator.getSample(0.0) * 2.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 modOutput = modulator.getSample(feedbackPhase);
|
||||||
double carOutput = carrier.getSample(modOutput);
|
|
||||||
|
|
||||||
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());
|
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
|
||||||
|
|
||||||
// The OPL2 maps 18 operators to 9 channels in a very fragmented memory layout.
|
// The master lock for waveforms
|
||||||
// This helper array maps a register offset (0x00 to 0x15) to a specific channel and operator type.
|
bool _waveformSelectionEnabled = false;
|
||||||
|
|
||||||
static const List<int> _operatorMap = [
|
static const List<int> _operatorMap = [
|
||||||
0, 1, 2, 0, 1, 2, // Offsets 0x00-0x05 (Ch 0-2 Modulators, then Carriers)
|
0,
|
||||||
-1, -1, // Offsets 0x06-0x07 (Unused)
|
1,
|
||||||
3, 4, 5, 3, 4, 5, // Offsets 0x08-0x0D (Ch 3-5 Modulators, then Carriers)
|
2,
|
||||||
-1, -1, // Offsets 0x0E-0x0F (Unused)
|
0,
|
||||||
6, 7, 8, 6, 7, 8, // Offsets 0x10-0x15 (Ch 6-8 Modulators, then Carriers)
|
1,
|
||||||
|
2,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
];
|
];
|
||||||
|
|
||||||
Opl2Operator? _getOperator(int offset) {
|
Opl2Operator? _getOperator(int offset) {
|
||||||
@@ -129,19 +187,20 @@ class Opl2Emulator {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
int channelIdx = _operatorMap[offset];
|
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;
|
bool isCarrier = (offset % 8) >= 3;
|
||||||
|
|
||||||
return isCarrier
|
return isCarrier
|
||||||
? channels[channelIdx].carrier
|
? channels[channelIdx].carrier
|
||||||
: channels[channelIdx].modulator;
|
: channels[channelIdx].modulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeRegister(int reg, int data) {
|
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 ---
|
// --- 0x20 - 0x35: Multiplier ---
|
||||||
if (reg >= 0x20 && reg <= 0x35) {
|
else if (reg >= 0x20 && reg <= 0x35) {
|
||||||
var op = _getOperator(reg - 0x20);
|
var op = _getOperator(reg - 0x20);
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
int mult = data & 0x0F;
|
int mult = data & 0x0F;
|
||||||
@@ -152,8 +211,7 @@ class Opl2Emulator {
|
|||||||
else if (reg >= 0x40 && reg <= 0x55) {
|
else if (reg >= 0x40 && reg <= 0x55) {
|
||||||
var op = _getOperator(reg - 0x40);
|
var op = _getOperator(reg - 0x40);
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
int level = data & 0x3F; // 0 is loudest, 63 is quietest
|
op.volume = 1.0 - ((data & 0x3F) / 63.0);
|
||||||
op.volume = 1.0 - (level / 63.0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- 0x60 - 0x75: Attack & Decay ---
|
// --- 0x60 - 0x75: Attack & Decay ---
|
||||||
@@ -202,6 +260,23 @@ class Opl2Emulator {
|
|||||||
channel.carrier.triggerOff();
|
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() {
|
double generateSample() {
|
||||||
|
|||||||
Reference in New Issue
Block a user