@@ -2,44 +2,39 @@ import 'dart:math' as math;
|
||||
|
||||
enum EnvelopeState { off, attack, decay, sustain, release }
|
||||
|
||||
class Opl2Channel {
|
||||
int fNum = 0;
|
||||
int block = 0;
|
||||
bool keyOn = false;
|
||||
|
||||
class Opl2Operator {
|
||||
double phase = 0.0;
|
||||
double phaseIncrement = 0.0;
|
||||
|
||||
// ADSR Parameters (0.0 to 1.0 scales or rates per sample)
|
||||
// ADSR
|
||||
double attackRate = 0.01;
|
||||
double decayRate = 0.005;
|
||||
double sustainLevel = 0.5;
|
||||
double releaseRate = 0.005;
|
||||
|
||||
// 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;
|
||||
// FM specific parameters
|
||||
double multiplier = 1.0;
|
||||
double volume =
|
||||
1.0; // Acts as volume for Carrier, but "Modulation Strength" for Modulator
|
||||
|
||||
void updateFrequency(double baseFreq) {
|
||||
phaseIncrement =
|
||||
(baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate;
|
||||
}
|
||||
|
||||
void triggerOn() {
|
||||
if (!keyOn) {
|
||||
keyOn = true;
|
||||
phase = 0.0;
|
||||
envState = EnvelopeState.attack;
|
||||
}
|
||||
phase = 0.0;
|
||||
envState = EnvelopeState.attack;
|
||||
}
|
||||
|
||||
void triggerOff() {
|
||||
keyOn = false;
|
||||
envState = EnvelopeState.release;
|
||||
}
|
||||
|
||||
double getSample() {
|
||||
// Process the ADSR state machine
|
||||
// phaseOffset is where the Modulator feeds into the Carrier
|
||||
double getSample(double phaseOffset) {
|
||||
switch (envState) {
|
||||
case EnvelopeState.attack:
|
||||
envVolume += attackRate;
|
||||
@@ -56,7 +51,6 @@ class Opl2Channel {
|
||||
}
|
||||
break;
|
||||
case EnvelopeState.sustain:
|
||||
// Holds steady at the sustain level
|
||||
envVolume = sustainLevel;
|
||||
break;
|
||||
case EnvelopeState.release:
|
||||
@@ -68,19 +62,48 @@ class Opl2Channel {
|
||||
break;
|
||||
case EnvelopeState.off:
|
||||
envVolume = 0.0;
|
||||
return 0.0; // Early exit for silence
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Generate the sine wave
|
||||
double out = math.sin(phase);
|
||||
// Add the Modulator's output to our current phase!
|
||||
double out = math.sin(phase + phaseOffset);
|
||||
|
||||
phase += phaseIncrement;
|
||||
if (phase >= 2 * math.pi) {
|
||||
phase -= 2 * math.pi;
|
||||
if (phase >= 2 * math.pi) phase -= 2 * math.pi;
|
||||
|
||||
return out * envVolume * volume;
|
||||
}
|
||||
}
|
||||
|
||||
class Opl2Channel {
|
||||
Opl2Operator modulator = Opl2Operator();
|
||||
Opl2Operator carrier = Opl2Operator();
|
||||
|
||||
int fNum = 0;
|
||||
int block = 0;
|
||||
bool keyOn = false;
|
||||
|
||||
void updateFrequency() {
|
||||
double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
||||
modulator.updateFrequency(baseFreq);
|
||||
carrier.updateFrequency(baseFreq);
|
||||
}
|
||||
|
||||
double getSample() {
|
||||
if (!keyOn &&
|
||||
carrier.envState == EnvelopeState.off &&
|
||||
modulator.envState == EnvelopeState.off) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Multiply the raw sine wave by our current envelope volume
|
||||
return (out * envVolume) * 0.1;
|
||||
// 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;
|
||||
|
||||
// 2. Feed the modulator's output directly into the carrier's phase!
|
||||
double carOutput = carrier.getSample(modOutput);
|
||||
|
||||
return carOutput * 0.1; // Master channel volume reduction
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,59 +112,68 @@ class Opl2Emulator {
|
||||
|
||||
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
|
||||
|
||||
// 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,
|
||||
// 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.
|
||||
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)
|
||||
];
|
||||
|
||||
int _getChannelIndexForOperator(int regOffset) {
|
||||
return _carrierOffsets.indexOf(regOffset);
|
||||
Opl2Operator? _getOperator(int offset) {
|
||||
if (offset < 0 ||
|
||||
offset >= _operatorMap.length ||
|
||||
_operatorMap[offset] == -1) {
|
||||
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) {
|
||||
// --- 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;
|
||||
// --- 0x20 - 0x35: Multiplier ---
|
||||
if (reg >= 0x20 && reg <= 0x35) {
|
||||
var op = _getOperator(reg - 0x20);
|
||||
if (op != null) {
|
||||
int mult = data & 0x0F;
|
||||
op.multiplier = mult == 0 ? 0.5 : mult.toDouble();
|
||||
}
|
||||
}
|
||||
// --- 0x80 - 0x95: Sustain (Upper 4 bits) & Release (Lower 4 bits) ---
|
||||
// --- 0x40 - 0x55: Total Level (Volume) ---
|
||||
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);
|
||||
}
|
||||
}
|
||||
// --- 0x60 - 0x75: Attack & Decay ---
|
||||
else if (reg >= 0x60 && reg <= 0x75) {
|
||||
var op = _getOperator(reg - 0x60);
|
||||
if (op != null) {
|
||||
int attack = (data & 0xF0) >> 4;
|
||||
int decay = data & 0x0F;
|
||||
op.attackRate = attack == 0 ? 0.0 : (attack / 15.0) * 0.05;
|
||||
op.decayRate = decay == 0 ? 0.0 : (decay / 15.0) * 0.005;
|
||||
}
|
||||
}
|
||||
// --- 0x80 - 0x95: Sustain & Release ---
|
||||
else if (reg >= 0x80 && reg <= 0x95) {
|
||||
int offset = reg - 0x80;
|
||||
int channelIdx = _getChannelIndexForOperator(offset);
|
||||
|
||||
if (channelIdx != -1) {
|
||||
var op = _getOperator(reg - 0x80);
|
||||
if (op != null) {
|
||||
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;
|
||||
op.sustainLevel = 1.0 - (sustain / 15.0);
|
||||
op.releaseRate = release == 0 ? 0.0 : (release / 15.0) * 0.005;
|
||||
}
|
||||
}
|
||||
// --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
|
||||
@@ -160,11 +192,14 @@ class Opl2Emulator {
|
||||
channel.fNum = (channel.fNum & 0xFF) | ((data & 0x03) << 8);
|
||||
channel.updateFrequency();
|
||||
|
||||
// Trigger the ADSR envelope appropriately
|
||||
if (newKeyOn && !channel.keyOn) {
|
||||
channel.triggerOn();
|
||||
channel.keyOn = true;
|
||||
channel.modulator.triggerOn();
|
||||
channel.carrier.triggerOn();
|
||||
} else if (!newKeyOn && channel.keyOn) {
|
||||
channel.triggerOff();
|
||||
channel.keyOn = false;
|
||||
channel.modulator.triggerOff();
|
||||
channel.carrier.triggerOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user