@@ -2,44 +2,39 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
enum EnvelopeState { off, attack, decay, sustain, release }
|
enum EnvelopeState { off, attack, decay, sustain, release }
|
||||||
|
|
||||||
class Opl2Channel {
|
class Opl2Operator {
|
||||||
int fNum = 0;
|
|
||||||
int block = 0;
|
|
||||||
bool keyOn = false;
|
|
||||||
|
|
||||||
double phase = 0.0;
|
double phase = 0.0;
|
||||||
double phaseIncrement = 0.0;
|
double phaseIncrement = 0.0;
|
||||||
|
|
||||||
// ADSR Parameters (0.0 to 1.0 scales or rates per sample)
|
// 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;
|
||||||
double releaseRate = 0.005;
|
double releaseRate = 0.005;
|
||||||
|
|
||||||
// Current Envelope State
|
|
||||||
EnvelopeState envState = EnvelopeState.off;
|
EnvelopeState envState = EnvelopeState.off;
|
||||||
double envVolume = 0.0;
|
double envVolume = 0.0;
|
||||||
|
|
||||||
void updateFrequency() {
|
// FM specific parameters
|
||||||
double realFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
|
double multiplier = 1.0;
|
||||||
phaseIncrement = (realFreq * 2 * math.pi) / Opl2Emulator.sampleRate;
|
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() {
|
void triggerOn() {
|
||||||
if (!keyOn) {
|
|
||||||
keyOn = true;
|
|
||||||
phase = 0.0;
|
phase = 0.0;
|
||||||
envState = EnvelopeState.attack;
|
envState = EnvelopeState.attack;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void triggerOff() {
|
void triggerOff() {
|
||||||
keyOn = false;
|
|
||||||
envState = EnvelopeState.release;
|
envState = EnvelopeState.release;
|
||||||
}
|
}
|
||||||
|
|
||||||
double getSample() {
|
// phaseOffset is where the Modulator feeds into the Carrier
|
||||||
// Process the ADSR state machine
|
double getSample(double phaseOffset) {
|
||||||
switch (envState) {
|
switch (envState) {
|
||||||
case EnvelopeState.attack:
|
case EnvelopeState.attack:
|
||||||
envVolume += attackRate;
|
envVolume += attackRate;
|
||||||
@@ -56,7 +51,6 @@ class Opl2Channel {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case EnvelopeState.sustain:
|
case EnvelopeState.sustain:
|
||||||
// Holds steady at the sustain level
|
|
||||||
envVolume = sustainLevel;
|
envVolume = sustainLevel;
|
||||||
break;
|
break;
|
||||||
case EnvelopeState.release:
|
case EnvelopeState.release:
|
||||||
@@ -68,19 +62,48 @@ class Opl2Channel {
|
|||||||
break;
|
break;
|
||||||
case EnvelopeState.off:
|
case EnvelopeState.off:
|
||||||
envVolume = 0.0;
|
envVolume = 0.0;
|
||||||
return 0.0; // Early exit for silence
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the sine wave
|
// Add the Modulator's output to our current phase!
|
||||||
double out = math.sin(phase);
|
double out = math.sin(phase + phaseOffset);
|
||||||
|
|
||||||
phase += phaseIncrement;
|
phase += phaseIncrement;
|
||||||
if (phase >= 2 * math.pi) {
|
if (phase >= 2 * math.pi) 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiply the raw sine wave by our current envelope volume
|
double getSample() {
|
||||||
return (out * envVolume) * 0.1;
|
if (!keyOn &&
|
||||||
|
carrier.envState == EnvelopeState.off &&
|
||||||
|
modulator.envState == EnvelopeState.off) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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());
|
final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());
|
||||||
|
|
||||||
// Real OPL2 chips use 2 operators (oscillators) per channel.
|
// The OPL2 maps 18 operators to 9 channels in a very fragmented memory layout.
|
||||||
// This maps the IMF register offsets to find the "Carrier" operator for our 9 channels.
|
// This helper array maps a register offset (0x00 to 0x15) to a specific channel and operator type.
|
||||||
static const List<int> _carrierOffsets = [
|
static const List<int> _operatorMap = [
|
||||||
0x03,
|
0, 1, 2, 0, 1, 2, // Offsets 0x00-0x05 (Ch 0-2 Modulators, then Carriers)
|
||||||
0x04,
|
-1, -1, // Offsets 0x06-0x07 (Unused)
|
||||||
0x05,
|
3, 4, 5, 3, 4, 5, // Offsets 0x08-0x0D (Ch 3-5 Modulators, then Carriers)
|
||||||
0x0B,
|
-1, -1, // Offsets 0x0E-0x0F (Unused)
|
||||||
0x0C,
|
6, 7, 8, 6, 7, 8, // Offsets 0x10-0x15 (Ch 6-8 Modulators, then Carriers)
|
||||||
0x0D,
|
|
||||||
0x13,
|
|
||||||
0x14,
|
|
||||||
0x15,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
int _getChannelIndexForOperator(int regOffset) {
|
Opl2Operator? _getOperator(int offset) {
|
||||||
return _carrierOffsets.indexOf(regOffset);
|
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) {
|
void writeRegister(int reg, int data) {
|
||||||
// --- 0x60 - 0x75: Attack (Upper 4 bits) & Decay (Lower 4 bits) ---
|
// --- 0x20 - 0x35: Multiplier ---
|
||||||
if (reg >= 0x60 && reg <= 0x75) {
|
if (reg >= 0x20 && reg <= 0x35) {
|
||||||
int offset = reg - 0x60;
|
var op = _getOperator(reg - 0x20);
|
||||||
int channelIdx = _getChannelIndexForOperator(offset);
|
if (op != null) {
|
||||||
|
int mult = data & 0x0F;
|
||||||
if (channelIdx != -1) {
|
op.multiplier = mult == 0 ? 0.5 : mult.toDouble();
|
||||||
// If it's a carrier operator
|
}
|
||||||
|
}
|
||||||
|
// --- 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 attack = (data & 0xF0) >> 4;
|
||||||
int decay = data & 0x0F;
|
int decay = data & 0x0F;
|
||||||
|
op.attackRate = attack == 0 ? 0.0 : (attack / 15.0) * 0.05;
|
||||||
// Map the 0-15 hardware values to our simple rate scales
|
op.decayRate = decay == 0 ? 0.0 : (decay / 15.0) * 0.005;
|
||||||
// (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) ---
|
// --- 0x80 - 0x95: Sustain & Release ---
|
||||||
else if (reg >= 0x80 && reg <= 0x95) {
|
else if (reg >= 0x80 && reg <= 0x95) {
|
||||||
int offset = reg - 0x80;
|
var op = _getOperator(reg - 0x80);
|
||||||
int channelIdx = _getChannelIndexForOperator(offset);
|
if (op != null) {
|
||||||
|
|
||||||
if (channelIdx != -1) {
|
|
||||||
int sustain = (data & 0xF0) >> 4;
|
int sustain = (data & 0xF0) >> 4;
|
||||||
int release = data & 0x0F;
|
int release = data & 0x0F;
|
||||||
|
op.sustainLevel = 1.0 - (sustain / 15.0);
|
||||||
// OPL2 sustain is inverted (0 is loudest, 15 is quietest)
|
op.releaseRate = release == 0 ? 0.0 : (release / 15.0) * 0.005;
|
||||||
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) ---
|
// --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
|
||||||
@@ -160,11 +192,14 @@ class Opl2Emulator {
|
|||||||
channel.fNum = (channel.fNum & 0xFF) | ((data & 0x03) << 8);
|
channel.fNum = (channel.fNum & 0xFF) | ((data & 0x03) << 8);
|
||||||
channel.updateFrequency();
|
channel.updateFrequency();
|
||||||
|
|
||||||
// Trigger the ADSR envelope appropriately
|
|
||||||
if (newKeyOn && !channel.keyOn) {
|
if (newKeyOn && !channel.keyOn) {
|
||||||
channel.triggerOn();
|
channel.keyOn = true;
|
||||||
|
channel.modulator.triggerOn();
|
||||||
|
channel.carrier.triggerOn();
|
||||||
} else if (!newKeyOn && channel.keyOn) {
|
} else if (!newKeyOn && channel.keyOn) {
|
||||||
channel.triggerOff();
|
channel.keyOn = false;
|
||||||
|
channel.modulator.triggerOff();
|
||||||
|
channel.carrier.triggerOff();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user