Added drums, snares, etc. OPL2 emulator is now feature complete.

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 13:01:54 +01:00
parent 6507950a95
commit 649a1419a8

View File

@@ -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<Opl2Channel> 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;
}
}