diff --git a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart index 164e26b..30b2054 100644 --- a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart +++ b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart @@ -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 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 _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 _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(); } } }