AUDIO: Enable dual OPL2 for hardware OPL3

This adds emulation for the dual OPL2 FM synthesis chip configuration to the
supported hardware OPL3 options: RetroWave OPL3 and OPL3LPT. This enables
stereo AdLib playback for SCI (and possibly other engines) on these devices.

This was already implemented for the OPL3 emulators (Dosbox and Nuked) and
ALSA. Generic code has been added to the OPL class to add similar emulation
for the other OPL3 options; this can also be easily added to any future OPL3
hardware or emulators which do not already have dual OPL2 support.
This commit is contained in:
Coen Rampen 2023-04-27 19:55:36 +02:00
parent 68bc11b73d
commit ca111f3170
4 changed files with 131 additions and 15 deletions

View File

@ -74,6 +74,10 @@ OPL::OPL() {
if (_hasInstance)
error("There are multiple OPL output instances running");
_hasInstance = true;
_rhythmMode = false;
_connectionFeedbackValues[0] = 0;
_connectionFeedbackValues[1] = 0;
_connectionFeedbackValues[2] = 0;
}
const Config::EmulatorDescription Config::_drivers[] = {
@ -90,10 +94,10 @@ const Config::EmulatorDescription Config::_drivers[] = {
#endif
#ifdef ENABLE_OPL2LPT
{ "opl2lpt", _s("OPL2LPT"), kOPL2LPT, kFlagOpl2},
{ "opl3lpt", _s("OPL3LPT"), kOPL3LPT, kFlagOpl2 | kFlagOpl3 },
{ "opl3lpt", _s("OPL3LPT"), kOPL3LPT, kFlagOpl2 | kFlagDualOpl2 | kFlagOpl3 },
#endif
#ifdef USE_RETROWAVE
{"rwopl3", _s("RetroWave OPL3"), kRWOPL3, kFlagOpl2 | kFlagOpl3},
{"rwopl3", _s("RetroWave OPL3"), kRWOPL3, kFlagOpl2 | kFlagDualOpl2 | kFlagOpl3},
#endif
{ nullptr, nullptr, 0, 0 }
};
@ -229,20 +233,11 @@ OPL *Config::create(DriverId driver, OplType type) {
warning("OPL2LPT only supprts OPL2");
return 0;
case kOPL3LPT:
if (type == kOpl2 || type == kOpl3) {
return OPL2LPT::create(type);
}
warning("OPL3LPT does not support dual OPL2");
return 0;
return OPL2LPT::create(type);
#endif
#ifdef USE_RETROWAVE
case kRWOPL3:
if (type == kDualOpl2) {
warning("RetroWave OPL3 does not support dual OPL2");
return 0;
}
return RetroWaveOPL3::create(type);
#endif
@ -264,6 +259,90 @@ void OPL::stop() {
_callback.reset();
}
void OPL::initDualOpl2OnOpl3(Config::OplType oplType) {
if (oplType != Config::OplType::kDualOpl2)
return;
// Enable OPL3 mode.
writeReg(0x105, 1);
// Set panning for channels 0-8 and 9-17 to right and left, respectively.
for (int i = 0; i <= 0x100; i += 0x100) {
for (int j = 0xC0; j <= 0xC8; j++) {
writeReg(i | j, i == 0 ? 0x20 : 0x10);
}
}
}
bool OPL::emulateDualOpl2OnOpl3(int r, int v, Config::OplType oplType) {
if (oplType != Config::OplType::kDualOpl2)
return true;
// Prevent writes to the following registers of the second set:
// - 01 - Test register. Setting any bit here will disable output.
// - 04 - Connection select. This is used to enable 4 operator instruments,
// which are not used for dual OPL2.
// - 05 - New. Only allow writes which set bit 0 to 1, which enables OPL3
// features.
if (r == 0x101 || r == 0x104 || (r == 0x105 && ((v & 1) == 0)))
return false;
// Clear bit 2 of waveform select register writes. This will prevent
// selection of OPL3-specific waveforms, which are not used for dual OPL2.
if ((r & 0xFF) >= 0xE0 && (r & 0xFF) <= 0xF5 && ((v & 4) > 0)) {
writeReg(r, v & ~4);
return false;
}
// Handle rhythm mode register writes.
if ((r & 0xFF) == 0xBD) {
// Check if rhythm mode is enabled or disabled.
bool newRhythmMode = (v & 0x20) > 0;
if (newRhythmMode != _rhythmMode) {
_rhythmMode = newRhythmMode;
// Set panning for channels 6-8 (used by rhythm mode instruments)
// to center or right if rhythm mode is enabled or disabled,
// respectively.
writeReg(0xC6, (_rhythmMode ? 0x30 : 0x20) | _connectionFeedbackValues[0]);
writeReg(0xC7, (_rhythmMode ? 0x30 : 0x20) | _connectionFeedbackValues[1]);
writeReg(0xC8, (_rhythmMode ? 0x30 : 0x20) | _connectionFeedbackValues[2]);
}
if (r == 0x1BD) {
// Send writes to the rhythm mode register on the 2nd OPL2 to the
// single rhythm mode register on the OPL3.
writeReg(0xBD, v);
return false;
}
}
// Keep track of the connection and feedback values set for channels 6-8.
// This is necessary for handling rhythm mode panning (see above).
if (r >= 0xC6 && r <= 0xC8) {
_connectionFeedbackValues[r - 0xC6] = v & 0xF;
}
// Add panning bits to writes to the connection/feedback registers.
if ((r & 0xFF) >= 0xC0 && (r & 0xFF) <= 0xC8) {
// Add right or left panning for the first or second OPL2, respectively.
int newValue = (r < 0x100 ? 0x20 : 0x10) | (v & 0xF);
if (_rhythmMode && r >= 0xC6 && r <= 0xC8) {
// If rhythm mode is enabled, pan channels 6-8 center.
newValue = 0x30 | (v & 0xF);
}
if (v == newValue) {
// Panning bits are already correct.
return true;
} else {
// Write the new value with the correct panning bits instead.
writeReg(r, newValue);
return false;
}
}
// Any other register writes can be processed normally.
return true;
}
bool OPL::_hasInstance = false;
RealOPL::RealOPL() : _baseFreq(0), _remainingTicks(0) {

View File

@ -191,6 +191,28 @@ public:
};
protected:
/**
* Initializes an OPL3 chip for emulating dual OPL2.
*
* @param oplType The type of OPL configuration that the engine expects.
* If this is not DualOpl2, this function will do nothing.
*/
void initDualOpl2OnOpl3(Config::OplType oplType);
/**
* Processes the specified register write so it will be correctly handled
* for emulating dual OPL2 on an OPL3 chip.
*
* @param r The register that is written to.
* @param v The value written to the register.
* @param oplType The type of OPL configuration that the engine expects.
* If this is not DualOpl2, this function will do nothing.
* @return True if the register write can be processed normally; false if
* it should be discarded. In this case, a new call to writeReg is usually
* performed by this function to replace the discarded register write.
*/
bool emulateDualOpl2OnOpl3(int r, int v, Config::OplType oplType);
/**
* Start the callbacks.
*/
@ -205,6 +227,9 @@ protected:
* The functor for callbacks.
*/
Common::ScopedPtr<TimerCallback> _callback;
bool _rhythmMode;
int _connectionFeedbackValues[3];
};
/**

View File

@ -121,12 +121,14 @@ void OPL::reset() {
for(int i = 0; i < 256; i ++) {
writeReg(i, 0);
}
if (_type == Config::kOpl3) {
if (_type == Config::kOpl3 || _type == Config::kDualOpl2) {
for (int i = 0; i < 256; i++) {
writeReg(i + 256, 0);
}
}
index = 0;
initDualOpl2OnOpl3(_type);
}
void OPL::write(int port, int val) {
@ -138,6 +140,7 @@ void OPL::write(int port, int val) {
index = val & 0xff;
break;
case Config::kOpl3:
case Config::kDualOpl2:
index = (val & 0xff) | ((port << 7) & 0x100);
break;
default:
@ -153,13 +156,16 @@ byte OPL::read(int port) {
}
void OPL::writeReg(int r, int v) {
if (_type == Config::kOpl3) {
if (_type == Config::kOpl3 || _type == Config::kDualOpl2) {
r &= 0x1ff;
} else {
r &= 0xff;
}
v &= 0xff;
if (!emulateDualOpl2OnOpl3(r, v, _type))
return;
ieee1284_write_data(_pport, r & 0xff);
if (r < 0x100) {
ieee1284_write_control(_pport, OPL2LPTRegisterSelect[0]);

View File

@ -133,6 +133,8 @@ bool OPL::init() {
} else {
retrowave_io_init(&_retrowaveGlobalContext);
_initialized = true;
initDualOpl2OnOpl3(_type);
}
_rwMutex->unlock();
@ -160,6 +162,8 @@ void OPL::reset() {
writeReg(offset + reg, 0, true);
}
}
initDualOpl2OnOpl3(_type);
}
_rwMutex->unlock();
@ -183,7 +187,9 @@ byte OPL::read(int portAddress) {
}
void OPL::writeReg(int reg, int value) {
writeReg(reg, value, false);
if (emulateDualOpl2OnOpl3(reg, value, _type)) {
writeReg(reg, value, false);
}
}
void OPL::writeReg(int reg, int value, bool forcePort) {