From 649a1419a8465eae5c65c38da3705f00fa4a8123 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Sun, 15 Mar 2026 13:01:54 +0100 Subject: [PATCH] Added drums, snares, etc. OPL2 emulator is now feature complete. Signed-off-by: Hans Kokx --- .../wolf_3d_synth/lib/src/opl2_emulator.dart | 100 +++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart index 19ec22c..63069bb 100644 --- a/packages/wolf_3d_synth/lib/src/opl2_emulator.dart +++ b/packages/wolf_3d_synth/lib/src/opl2_emulator.dart @@ -147,9 +147,31 @@ class Opl2Channel { } } +class Opl2Noise { + int _seed = 0xFFFF; + + // Simple 16-bit LFSR to match OPL2 noise characteristics + double next() { + int bit = ((_seed >> 0) ^ (_seed >> 2) ^ (_seed >> 3) ^ (_seed >> 5)) & 1; + _seed = (_seed >> 1) | (bit << 15); + return ((_seed & 1) == 1) ? 1.0 : -1.0; + } +} + class Opl2Emulator { static const int sampleRate = 44100; + bool rhythmMode = false; + + // Key states for the 5 drums + bool bassDrumKey = false; + bool snareDrumKey = false; + bool tomTomKey = false; + bool topCymbalKey = false; + bool hiHatKey = false; + + final Opl2Noise _noise = Opl2Noise(); + final List channels = List.generate(9, (_) => Opl2Channel()); // The master lock for waveforms @@ -276,14 +298,88 @@ class Opl2Emulator { op.waveform = data & 0x03; } } + } else if (reg == 0xBD) { + rhythmMode = (data & 0x20) != 0; // Bit 5: Rhythm Enable + + // Bits 0-4: Key-On for individual drums + bassDrumKey = (data & 0x10) != 0; + snareDrumKey = (data & 0x08) != 0; + tomTomKey = (data & 0x04) != 0; + topCymbalKey = (data & 0x02) != 0; + hiHatKey = (data & 0x01) != 0; } } double generateSample() { double mixedOutput = 0.0; - for (var channel in channels) { - mixedOutput += channel.getSample(); + + // Channels 0-5 always act as normal melodic FM channels + for (int i = 0; i < 6; i++) { + mixedOutput += channels[i].getSample(); } + + if (!rhythmMode) { + // Standard mode: play channels 6, 7, and 8 normally + for (int i = 6; i < 9; i++) { + mixedOutput += channels[i].getSample(); + } + } else { + // RHYTHM MODE: The last 3 channels are re-routed + mixedOutput += _generateBassDrum(); + mixedOutput += _generateSnareAndHiHat(); + mixedOutput += _generateTomAndCymbal(); + } + return mixedOutput.clamp(-1.0, 1.0); } + + // Example of Bass Drum logic (Channel 6) + double _generateBassDrum() { + if (!bassDrumKey) return 0.0; + // Bass drum uses standard FM (Mod -> Car) but usually with very low frequency + return channels[6].getSample(); + } + + double _generateSnareAndHiHat() { + double snareOut = 0.0; + double hiHatOut = 0.0; + + // Snare uses Channel 7 Modulator + if (snareDrumKey) { + // The Snare is a mix of a periodic tone and white noise + double noise = _noise.next(); + snareOut = channels[7].modulator.getSample(0.0) * noise; + } + + // Hi-Hat uses Channel 7 Carrier + if (hiHatKey) { + // Hi-Hats are almost pure high-frequency noise + hiHatOut = + _noise.next() * + channels[7].carrier.envVolume * + channels[7].carrier.volume; + } + + return (snareOut + hiHatOut) * 0.1; + } + + double _generateTomAndCymbal() { + double tomOut = 0.0; + double cymbalOut = 0.0; + + // Tom-tom uses Channel 8 Modulator + if (tomTomKey) { + // Toms are basically just a melodic sine wave with a fast decay + tomOut = channels[8].modulator.getSample(0.0); + } + + // Top Cymbal uses Channel 8 Carrier + if (topCymbalKey) { + // Cymbals use the carrier but are usually phase-modulated by noise + double noise = _noise.next(); + cymbalOut = channels[8].carrier.getSample(noise * 2.0); + } + + return (tomOut + cymbalOut) * 0.1; + } }