diff --git a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart index 30b2054..19ec22c 100644 --- a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart +++ b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart @@ -6,7 +6,6 @@ class Opl2Operator { double phase = 0.0; double phaseIncrement = 0.0; - // ADSR double attackRate = 0.01; double decayRate = 0.005; double sustainLevel = 0.5; @@ -14,10 +13,11 @@ class Opl2Operator { EnvelopeState envState = EnvelopeState.off; double envVolume = 0.0; - // FM specific parameters double multiplier = 1.0; - double volume = - 1.0; // Acts as volume for Carrier, but "Modulation Strength" for Modulator + double volume = 1.0; + + // Waveform Selection (0-3) + int waveform = 0; void updateFrequency(double baseFreq) { phaseIncrement = @@ -33,7 +33,25 @@ class Opl2Operator { 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) { switch (envState) { case EnvelopeState.attack: @@ -65,8 +83,8 @@ class Opl2Operator { return 0.0; } - // Add the Modulator's output to our current phase! - double out = math.sin(phase + phaseOffset); + // Pass the phase + modulation offset into our waveform generator! + double out = _getOscillatorOutput(phase + phaseOffset); phase += phaseIncrement; if (phase >= 2 * math.pi) phase -= 2 * math.pi; @@ -83,6 +101,12 @@ class Opl2Channel { int block = 0; bool keyOn = false; + bool isAdditive = false; + int feedbackStrength = 0; + + double _prevModOutput1 = 0.0; + double _prevModOutput2 = 0.0; + void updateFrequency() { double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0); modulator.updateFrequency(baseFreq); @@ -96,14 +120,30 @@ class Opl2Channel { 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; + double feedbackPhase = 0.0; + if (feedbackStrength > 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 carOutput = carrier.getSample(modOutput); + double modOutput = modulator.getSample(feedbackPhase); - 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 channels = List.generate(9, (_) => Opl2Channel()); - // 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. + // The master lock for waveforms + bool _waveformSelectionEnabled = false; + 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) + 0, + 1, + 2, + 0, + 1, + 2, + -1, + -1, + 3, + 4, + 5, + 3, + 4, + 5, + -1, + -1, + 6, + 7, + 8, + 6, + 7, + 8, ]; Opl2Operator? _getOperator(int offset) { @@ -129,19 +187,20 @@ class Opl2Emulator { 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) { + // --- 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 --- - if (reg >= 0x20 && reg <= 0x35) { + else if (reg >= 0x20 && reg <= 0x35) { var op = _getOperator(reg - 0x20); if (op != null) { int mult = data & 0x0F; @@ -152,8 +211,7 @@ class Opl2Emulator { 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); + op.volume = 1.0 - ((data & 0x3F) / 63.0); } } // --- 0x60 - 0x75: Attack & Decay --- @@ -202,6 +260,23 @@ class Opl2Emulator { 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() {