mirror of
https://github.com/scummvm/scummvm.git
synced 2025-04-02 10:52:32 -04:00
If running an OPL2 track on a DOS_BOX OPL3 emulator, The buffer will be divided by 2 like if it is stereo, but it will generate a mono audio stream, as opl3Active flag is disabled, so it will act as an OPL2. The error results in wrong sound produced. This fix the issue when OPL3 is used as an OPL2 and therefore as a mono audio source. The current error should be easily reproducible if using OPL3 with .ADL Westwood's files. This bug it might have never been noticed because those files, in kyra engine, are using always OPL2 (kAdlib) is used. When DosBox OPL3 Emulator is run as OPL2 compatibilty mode, requires to call the GenerateBlock2 method as the internal stereo mode used in GenerateBlock3 is not activated. This fix covers all scenarios: - OPL2: default common base case, non stereo. - DUAL_OPL2: This is kind of OPL3 mode, I don't think is really fully supported, - OPL3 in OPL2 mode: In this scenario the audio buffer is stereo, but the emulator is generating mono audio. - OPL3 in OPL3 mode: this is the full stereo mode of OPL3.
356 lines
7.6 KiB
C++
356 lines
7.6 KiB
C++
/* ScummVM - Graphic Adventure Engine
|
|
*
|
|
* ScummVM is the legal property of its developers, whose names
|
|
* are too numerous to list here. Please refer to the COPYRIGHT
|
|
* file distributed with this source distribution.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Based on AdLib emulation code of DOSBox
|
|
* Copyright (C) 2002-2009 The DOSBox Team
|
|
* Licensed under GPLv2+
|
|
* http://www.dosbox.com
|
|
*/
|
|
|
|
#ifndef DISABLE_DOSBOX_OPL
|
|
|
|
#include "dosbox.h"
|
|
#include "dbopl.h"
|
|
|
|
#include "audio/mixer.h"
|
|
#include "common/system.h"
|
|
#include "common/scummsys.h"
|
|
#include "common/util.h"
|
|
|
|
#include <math.h>
|
|
#include <string.h>
|
|
|
|
namespace OPL {
|
|
namespace DOSBox {
|
|
|
|
Timer::Timer() {
|
|
masked = false;
|
|
overflow = false;
|
|
enabled = false;
|
|
counter = 0;
|
|
delay = 0;
|
|
}
|
|
|
|
void Timer::update(double time) {
|
|
if (!enabled || !delay)
|
|
return;
|
|
double deltaStart = time - startTime;
|
|
// Only set the overflow flag when not masked
|
|
if (deltaStart >= 0 && !masked)
|
|
overflow = 1;
|
|
}
|
|
|
|
void Timer::reset(double time) {
|
|
overflow = false;
|
|
if (!delay || !enabled)
|
|
return;
|
|
double delta = (time - startTime);
|
|
double rem = fmod(delta, delay);
|
|
double next = delay - rem;
|
|
startTime = time + next;
|
|
}
|
|
|
|
void Timer::stop() {
|
|
enabled = false;
|
|
}
|
|
|
|
void Timer::start(double time, int scale) {
|
|
//Don't enable again
|
|
if (enabled)
|
|
return;
|
|
enabled = true;
|
|
delay = 0.001 * (256 - counter) * scale;
|
|
startTime = time + delay;
|
|
}
|
|
|
|
bool Chip::write(uint32 reg, uint8 val) {
|
|
switch (reg) {
|
|
case 0x02:
|
|
timer[0].counter = val;
|
|
return true;
|
|
case 0x03:
|
|
timer[1].counter = val;
|
|
return true;
|
|
case 0x04:
|
|
{
|
|
double time = g_system->getMillis() / 1000.0;
|
|
|
|
if (val & 0x80) {
|
|
timer[0].reset(time);
|
|
timer[1].reset(time);
|
|
} else {
|
|
timer[0].update(time);
|
|
timer[1].update(time);
|
|
|
|
if (val & 0x1)
|
|
timer[0].start(time, 80);
|
|
else
|
|
timer[0].stop();
|
|
|
|
timer[0].masked = (val & 0x40) > 0;
|
|
|
|
if (timer[0].masked)
|
|
timer[0].overflow = false;
|
|
|
|
if (val & 0x2)
|
|
timer[1].start(time, 320);
|
|
else
|
|
timer[1].stop();
|
|
|
|
timer[1].masked = (val & 0x20) > 0;
|
|
|
|
if (timer[1].masked)
|
|
timer[1].overflow = false;
|
|
}
|
|
}
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
uint8 Chip::read() {
|
|
double time = g_system->getMillis() / 1000.0;
|
|
|
|
timer[0].update(time);
|
|
timer[1].update(time);
|
|
|
|
uint8 ret = 0;
|
|
// Overflow won't be set if a channel is masked
|
|
if (timer[0].overflow) {
|
|
ret |= 0x40;
|
|
ret |= 0x80;
|
|
}
|
|
if (timer[1].overflow) {
|
|
ret |= 0x20;
|
|
ret |= 0x80;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
OPL::OPL(Config::OplType type) : _type(type), _rate(0), _emulator(nullptr) {
|
|
}
|
|
|
|
OPL::~OPL() {
|
|
stop();
|
|
free();
|
|
}
|
|
|
|
void OPL::free() {
|
|
delete _emulator;
|
|
_emulator = nullptr;
|
|
}
|
|
|
|
bool OPL::init() {
|
|
free();
|
|
|
|
memset(&_reg, 0, sizeof(_reg));
|
|
ARRAYCLEAR(_chip);
|
|
|
|
_emulator = new DBOPL::Chip();
|
|
if (!_emulator)
|
|
return false;
|
|
|
|
DBOPL::InitTables();
|
|
_rate = g_system->getMixer()->getOutputRate();
|
|
_emulator->Setup(_rate);
|
|
|
|
if (_type == Config::kDualOpl2) {
|
|
// Setup opl3 mode in the hander
|
|
_emulator->WriteReg(0x105, 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void OPL::reset() {
|
|
init();
|
|
}
|
|
|
|
void OPL::write(int port, int val) {
|
|
if (port&1) {
|
|
switch (_type) {
|
|
case Config::kOpl2:
|
|
case Config::kOpl3:
|
|
if (!_chip[0].write(_reg.normal, val))
|
|
_emulator->WriteReg(_reg.normal, val);
|
|
break;
|
|
case Config::kDualOpl2:
|
|
// Not a 0x??8 port, then write to a specific port
|
|
if (!(port & 0x8)) {
|
|
byte index = (port & 2) >> 1;
|
|
dualWrite(index, _reg.dual[index], val);
|
|
} else {
|
|
//Write to both ports
|
|
dualWrite(0, _reg.dual[0], val);
|
|
dualWrite(1, _reg.dual[1], val);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
// Ask the handler to write the address
|
|
// Make sure to clip them in the right range
|
|
switch (_type) {
|
|
case Config::kOpl2:
|
|
_reg.normal = _emulator->WriteAddr(port, val) & 0xff;
|
|
break;
|
|
case Config::kOpl3:
|
|
_reg.normal = _emulator->WriteAddr(port, val) & 0x1ff;
|
|
break;
|
|
case Config::kDualOpl2:
|
|
// Not a 0x?88 port, when write to a specific side
|
|
if (!(port & 0x8)) {
|
|
byte index = (port & 2) >> 1;
|
|
_reg.dual[index] = val & 0xff;
|
|
} else {
|
|
_reg.dual[0] = val & 0xff;
|
|
_reg.dual[1] = val & 0xff;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void OPL::writeReg(int r, int v) {
|
|
int tempReg = 0;
|
|
switch (_type) {
|
|
case Config::kOpl2:
|
|
case Config::kDualOpl2:
|
|
case Config::kOpl3:
|
|
// We can't use _handler->writeReg here directly, since it would miss timer changes.
|
|
|
|
// Backup old setup register
|
|
tempReg = _reg.normal;
|
|
|
|
// We directly allow writing to secondary OPL3 registers by using
|
|
// register values >= 0x100.
|
|
if (_type == Config::kOpl3 && r >= 0x100) {
|
|
// We need to set the register we want to write to via port 0x222,
|
|
// since we want to write to the secondary register set.
|
|
write(0x222, r);
|
|
// Do the real writing to the register
|
|
write(0x223, v);
|
|
} else {
|
|
// We need to set the register we want to write to via port 0x388
|
|
write(0x388, r);
|
|
// Do the real writing to the register
|
|
write(0x389, v);
|
|
}
|
|
|
|
// Restore the old register
|
|
if (_type == Config::kOpl3 && tempReg >= 0x100) {
|
|
write(0x222, tempReg & ~0x100);
|
|
} else {
|
|
write(0x388, tempReg);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
};
|
|
}
|
|
|
|
void OPL::dualWrite(uint8 index, uint8 reg, uint8 val) {
|
|
// Make sure you don't use opl3 features
|
|
// Don't allow write to disable opl3
|
|
if (reg == 5)
|
|
return;
|
|
|
|
// Only allow 4 waveforms
|
|
if (reg >= 0xE0 && reg <= 0xE8)
|
|
val &= 3;
|
|
|
|
// Write to the timer?
|
|
if (_chip[index].write(reg, val))
|
|
return;
|
|
|
|
// Enabling panning
|
|
if (reg >= 0xC0 && reg <= 0xC8) {
|
|
val &= 15;
|
|
val |= index ? 0xA0 : 0x50;
|
|
}
|
|
|
|
uint32 fullReg = reg + (index ? 0x100 : 0);
|
|
_emulator->WriteReg(fullReg, val);
|
|
}
|
|
|
|
void OPL::generateSamples(int16 *buffer, int length) {
|
|
const uint bufferLength = 512;
|
|
int32 tempBuffer[bufferLength * 2];
|
|
|
|
if (isStereo()) {
|
|
// For stereo OPL cards, we divide the sample count by 2,
|
|
// to match stereo AudioStream behavior.
|
|
length >>= 1;
|
|
if (_emulator->opl3Active) {
|
|
// DUAL_OPL2 or OPL3 in OPL3 mode (stereo)
|
|
while (length > 0) {
|
|
const uint readSamples = MIN<uint>(length, bufferLength);
|
|
const uint readSamples2 = (readSamples << 1);
|
|
|
|
_emulator->GenerateBlock3(readSamples, tempBuffer);
|
|
|
|
for (uint i = 0; i < readSamples2; ++i)
|
|
buffer[i] = tempBuffer[i];
|
|
|
|
buffer += readSamples2;
|
|
length -= readSamples;
|
|
}
|
|
} else {
|
|
// OPL3 (stereo) in OPL2 compatibility mode (mono)
|
|
while (length > 0) {
|
|
const uint readSamples = MIN<uint>(length, bufferLength);
|
|
const uint readSamples2 = (readSamples << 1);
|
|
|
|
_emulator->GenerateBlock2(readSamples, tempBuffer);
|
|
|
|
for (uint i = 0, j = 0; i < readSamples; ++i, j += 2)
|
|
buffer[j] = buffer[j + 1] = tempBuffer[i];
|
|
|
|
buffer += readSamples2;
|
|
length -= readSamples;
|
|
}
|
|
}
|
|
} else {
|
|
// OPL2
|
|
while (length > 0) {
|
|
const uint readSamples = MIN<uint>(length, bufferLength << 1);
|
|
|
|
_emulator->GenerateBlock2(readSamples, tempBuffer);
|
|
|
|
for (uint i = 0; i < readSamples; ++i)
|
|
buffer[i] = tempBuffer[i];
|
|
|
|
buffer += readSamples;
|
|
length -= readSamples;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // End of namespace DOSBox
|
|
} // End of namespace OPL
|
|
|
|
#endif // !DISABLE_DOSBOX_ADLIB
|