scummvm/audio/softsynth/opl/dosbox.cpp
Raffaello Bertini 15425aa66b AUDIO: Fix playback in OPL3 mono mode
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.
2025-01-24 22:40:35 +01:00

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