Files
scummvm-cursorfix/engines/scumm/imuse_digi/dimuse_waveout.cpp
2026-02-02 04:50:13 +01:00

196 lines
7.2 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/>.
*
*/
#include "scumm/imuse_digi/dimuse_engine.h"
namespace Scumm {
int IMuseDigital::waveOutInit(waveOutParamsStruct *waveOutSettingsStruct) {
_waveOutBytesPerSample = 2;
_waveOutNumChannels = _mixer->getOutputStereo() ? 2 : 1;
_waveOutZeroLevel = 0;
_waveOutSampleRate = _internalSampleRate;
_waveOutPreferredFeedSize = _internalFeedSize;
_waveOutOutputBuffer = nullptr;
_waveOutMixBuffer = nullptr;
_waveOutLowLatencyOutputBuffer = nullptr;
if (!_lowLatencyMode || _isEarlyDiMUSE) {
// Nine buffers (waveOutPreferredFeedSize * 4 bytes each), two will be used for the mixer
_waveOutOutputBuffer = (uint8 *)malloc(_waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize * 9);
_waveOutMixBuffer = _waveOutOutputBuffer + (_waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize * 7); // 8-th buffer
}
// Replicate another set of buffers for the low latency mode, we will use the previous ones for cutscenes if the mode is active
if (_lowLatencyMode) {
_waveOutLowLatencyOutputBuffer = (uint8 *)malloc(_waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize * 9);
}
// This information will be fed to the internal mixer during its initialization
waveOutSettingsStruct->bytesPerSample = _waveOutBytesPerSample * 8;
waveOutSettingsStruct->numChannels = _waveOutNumChannels;
waveOutSettingsStruct->mixBufSize = (_waveOutBytesPerSample * _waveOutNumChannels) * _waveOutPreferredFeedSize;
waveOutSettingsStruct->sizeSampleKB = 0;
waveOutSettingsStruct->mixBuf = _waveOutMixBuffer; // Note: in low latency mode this initialization is a dummy
// Init the buffers filling them with zero volume samples
if (!_lowLatencyMode || _isEarlyDiMUSE) {
memset(_waveOutOutputBuffer, _waveOutZeroLevel, _waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize * 9);
}
if (_lowLatencyMode) {
memset(_waveOutLowLatencyOutputBuffer, _waveOutZeroLevel, _waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize * 9);
}
_waveOutDisableWrite = 0;
return 0;
}
void IMuseDigital::waveOutWrite(uint8 **audioData, int &feedSize, int &sampleRate) {
uint8 *curBufferBlock;
if (_waveOutDisableWrite)
return;
feedSize = 0;
// A bit of context for what follows:
//
// In COMI, when entering certain rooms (e.g. barber shop in Plunder Island), the game
// sends a State music event followed immediately by a Sequence music event.
// If the iMUSE callback happens to run between these two commands, it can cause a brief
// audio glitch where a few milliseconds of the intermediate state music is heard
// before being replaced by the sequence music, with the fade in between canceled.
//
// To prevent this, we skip the very first audio callback after any music change
// by setting _waveOutXorTrigger to 1 in playComiMusic(). This gives the scripts
// enough time to send any follow-up sequence events before iMUSE processes
// the audio transition, ensuring the absence of glitches and most importantly
// no permanent audio delay on other audio feeds.
//
// This is very strongly inspired by what the disassembly does:
//
// The original DirectSound implementation uses the XOR flag combined with play
// cursor position checks that naturally skip audio processing cycles when the
// play cursor gets too close to the write cursor. In addition to this, the main
// portion of tracksCallback() is executed in loop until feedSide is finally 0.
// This creates the exact timing conditions where rapid music events have sufficient
// time to accumulate before iMUSE processes any audio transitions.
//
// We're not using DirectSound, so recreate this I could either simulate the
// play cursor handling shenanigans, or I could just do exactly what I have done :-)
if (_vm->_game.id == GID_CMI && _waveOutXorTrigger != 0) {
_waveOutXorTrigger = 0;
return;
}
if (!_isEarlyDiMUSE && _vm->_game.id == GID_DIG) {
_waveOutXorTrigger ^= 1;
if (!_waveOutXorTrigger)
return;
}
if (_mixer->isReady()) {
curBufferBlock = &_waveOutOutputBuffer[_waveOutPreferredFeedSize * _waveOutWriteIndex * _waveOutBytesPerSample * _waveOutNumChannels];
*audioData = curBufferBlock;
sampleRate = _waveOutSampleRate;
feedSize = _waveOutPreferredFeedSize;
_waveOutWriteIndex = (_waveOutWriteIndex + 1) % 7;
byte *ptr = (byte *)malloc(_outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels);
memcpy(ptr, curBufferBlock, _outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels);
_internalMixer->getStream(-1)->queueBuffer(ptr,
_outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels,
DisposeAfterUse::YES,
waveOutGetStreamFlags());
}
}
int IMuseDigital::waveOutDeinit() {
_waveOutDisableWrite = 1;
return 0;
}
void IMuseDigital::waveOutCallback() {
Common::StackLock lock(*_mutex);
if (_lowLatencyMode) {
tracksLowLatencyCallback();
} else {
tracksCallback();
}
}
byte IMuseDigital::waveOutGetStreamFlags() {
byte flags = Audio::FLAG_16BITS;
if (_mixer->getOutputStereo()) {
flags |= Audio::FLAG_STEREO;
}
#ifdef SCUMM_LITTLE_ENDIAN
flags |= Audio::FLAG_LITTLE_ENDIAN;
#endif
return flags;
}
void IMuseDigital::waveOutLowLatencyWrite(uint8 **audioData, int &feedSize, int &sampleRate, int idx) {
uint8 *curBufferBlock;
if (_waveOutDisableWrite)
return;
if (!_isEarlyDiMUSE && _vm->_game.id == GID_DIG) {
_waveOutXorTrigger ^= 1;
if (!_waveOutXorTrigger)
return;
}
feedSize = 0;
if (_mixer->isReady()) {
curBufferBlock = &_waveOutLowLatencyOutputBuffer[_waveOutPreferredFeedSize * idx * _waveOutBytesPerSample * _waveOutNumChannels];
*audioData = curBufferBlock;
sampleRate = _waveOutSampleRate;
feedSize = _waveOutPreferredFeedSize;
byte *ptr = (byte *)malloc(_outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels);
memcpy(ptr, curBufferBlock, _outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels);
_internalMixer->getStream(idx)->queueBuffer(ptr,
_outputFeedSize * _waveOutBytesPerSample * _waveOutNumChannels,
DisposeAfterUse::YES,
waveOutGetStreamFlags());
}
}
void IMuseDigital::waveOutEmptyBuffer(int idx) {
// This is necessary in low latency mode to clean-up the buffers of stale/finished sounds
int bufferSize = _waveOutNumChannels * _waveOutBytesPerSample * _waveOutPreferredFeedSize;
memset(&_waveOutLowLatencyOutputBuffer[bufferSize * idx], _waveOutZeroLevel, bufferSize);
}
} // End of namespace Scumm