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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user