Added FM synth

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

View File

@@ -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();
}
}
}