scummvm/engines/scumm/players/player_v3a.cpp
2021-12-26 18:48:43 +01:00

602 lines
17 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 "engines/engine.h"
#include "scumm/players/player_v3a.h"
#include "scumm/scumm.h"
namespace Scumm {
Player_V3A::Player_V3A(ScummEngine *scumm, Audio::Mixer *mixer)
: Paula(true, mixer->getOutputRate(), mixer->getOutputRate() / 60),
_vm(scumm),
_mixer(mixer),
_soundHandle(),
_songData(nullptr),
_wavetableData(nullptr),
_wavetablePtrs(nullptr),
_musicTimer(0),
_initState(kInitStateNotReady) {
assert(scumm);
assert(mixer); // this one's a bit pointless, since we had to dereference it to initialize Paula
assert((_vm->_game.id == GID_INDY3) || (_vm->_game.id == GID_LOOM));
stopAllSounds();
// As in the original game, the same Paula is shared between both SFX and music and plays continuously.
// Doing them separately would require subclassing Paula and creating two instances
// (since all of the important methods are protected)
startPaula();
_mixer->playStream(Audio::Mixer::kPlainSoundType, &_soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true);
}
bool Player_V3A::init() {
byte *ptr;
int numInstruments = 0;
// Determine which sound resource contains the wavetable data and how large it is
// This is hardcoded into each game's executable
if (_vm->_game.id == GID_INDY3) {
ptr = _vm->getResourceAddress(rtSound, 83);
numInstruments = 12;
} else if (_vm->_game.id == GID_LOOM) {
ptr = _vm->getResourceAddress(rtSound, 79);
numInstruments = 9;
} else {
error("player_v3a - unknown game");
return false;
}
if (!ptr) {
error("player_v3a - unable to load music samples resource");
return false;
}
// Keep a copy of the resource data, since the original pointer may eventually go bad
int length = READ_LE_UINT16(ptr);
_wavetableData = new int8[length];
if (!_wavetableData) {
error("player_v3a - failed to allocate copy of wavetable data");
return false;
}
memcpy(_wavetableData, ptr, length);
int offset = 4;
// Parse the header tables into a more convenient structure
_wavetablePtrs = new InstData[numInstruments];
for (int i = 0; i < numInstruments; i++) {
// Each instrument defines 6 octaves
for (int j = 0; j < 6; j++) {
// Offset/length for intro/main component
int dataOff = READ_BE_UINT16(_wavetableData + offset + 0);
int dataLen = READ_BE_UINT16(_wavetableData + offset + 2);
if (dataLen) {
_wavetablePtrs[i].mainLen[j] = dataLen;
_wavetablePtrs[i].mainData[j] = &_wavetableData[dataOff];
} else {
_wavetablePtrs[i].mainLen[j] = 0;
_wavetablePtrs[i].mainData[j] = nullptr;
}
// Offset/length for looped component, if any
dataOff = READ_BE_UINT16(ptr + offset + 4);
dataLen = READ_BE_UINT16(ptr + offset + 6);
if (dataLen) {
_wavetablePtrs[i].loopLen[j] = dataLen;
_wavetablePtrs[i].loopData[j] = &_wavetableData[dataOff];
} else {
_wavetablePtrs[i].loopLen[j] = 0;
_wavetablePtrs[i].loopData[j] = nullptr;
}
// Octave shift for this octave
_wavetablePtrs[i].octave[j] = READ_BE_INT16(ptr + offset + 8);
offset += 10;
}
// Fadeout rate, in 1/256ths of a volume level
_wavetablePtrs[i].volumeFade = READ_BE_INT16(ptr + offset);
offset += 2;
if (_vm->_game.id == GID_LOOM) {
// Loom's sound samples aren't all in tune with each other,
// so it stores an extra adjustment here
_wavetablePtrs[i].pitchAdjust = READ_BE_INT16(ptr + offset);
offset += 2;
} else {
_wavetablePtrs[i].pitchAdjust = 0;
}
}
return true;
}
Player_V3A::~Player_V3A() {
_mixer->stopHandle(_soundHandle);
if (_initState == kInitStateReady) {
delete[] _wavetableData;
delete[] _wavetablePtrs;
}
}
void Player_V3A::setMusicVolume (int vol) {
_mixer->setChannelVolume(_soundHandle, vol);
}
void Player_V3A::stopAllSounds() {
for (int i = 0; i < 4; i++) {
clearVoice(i);
_channels[i].resourceId = -1;
}
_curSong = -1;
_songPtr = 0;
_songDelay = 0;
_songData = nullptr;
}
void Player_V3A::stopSound(int nr) {
if (nr <= 0)
return;
for (int i = 0; i < 4; i++) {
if (_channels[i].resourceId == nr) {
clearVoice(i);
_channels[i].resourceId = -1;
}
}
if (nr == _curSong) {
_curSong = -1;
_songDelay = 0;
_songPtr = 0;
_songData = nullptr;
}
}
void Player_V3A::startSound(int nr) {
assert(_vm);
int8 *data = (int8 *)_vm->getResourceAddress(rtSound, nr);
if (!data)
return;
if ((_vm->_game.id != GID_INDY3) && (_vm->_game.id != GID_LOOM))
error("player_v3a - unknown game");
if (_initState == kInitStateNotReady)
_initState = init() ? kInitStateReady : kInitStateFailed;
// is this a Music resource?
if (data[26]) {
if (_initState == kInitStateReady) {
stopAllSounds();
for (int i = 0; i < 4; i++) {
_channels[i].haltTimer = 0;
_channels[i].resourceId = nr;
_channels[i].priority = READ_BE_UINT16(data + 4);
}
// Keep a local copy of the song data
_songData = data;
_curSong = nr;
_songPtr = 0;
_songDelay = 1;
// Start timer at 0 and increment every 30 frames (see below)
_musicTimer = 0;
} else {
// debug("player_v3a - wavetable unavailable, cannot play music");
}
} else {
int priority = READ_BE_UINT16(data + 4);
int channel = READ_BE_UINT16(data + 6);
if (_channels[channel].resourceId != -1 && _channels[channel].priority > priority)
return;
int chan1 = SFX_CHANNEL_MAP[channel][0];
int chan2 = SFX_CHANNEL_MAP[channel][1];
int offsetL = READ_BE_UINT16(data + 8);
int offsetR = READ_BE_UINT16(data + 10);
int lengthL = READ_BE_UINT16(data + 12);
int lengthR = READ_BE_UINT16(data + 14);
// Period and Volume are both stored in fixed-point
_channels[chan1].period = READ_BE_UINT16(data + 20) << 16;
_channels[chan2].period = READ_BE_UINT16(data + 22) << 16;
_channels[chan1].volume = data[24] << 8;
_channels[chan2].volume = data[25] << 8;
_channels[chan1].loopCount = data[27];
_channels[chan2].loopCount = data[27];
int sweepOffset = READ_BE_UINT16(data + 16);
if (sweepOffset) {
// This data contains a list of offset/value pairs, processed in sequence
// The offset points into a data structure in the original sound engine
// Offset 0x18 sets the channel's Sweep Rate (fractional)
// Offset 0x2C with nonzero value delays until reading the next packet
// Offset 0x2C with zero value stops playback immediately
// The other offsets are unknown, but they are never used
// Indy3 always uses 0x18, 0x2C-nonzero, then 0x2C-zero
// Loom doesn't use these at all
for (int i = 0; i < 3; i++)
{
int offset = READ_BE_UINT32(data + sweepOffset + i*8 + 0);
int value = READ_BE_INT32(data + sweepOffset + i*8 + 4);
if (offset == 0x18)
{
_channels[chan1].sweepRate = value;
_channels[chan2].sweepRate = value;
}
if (offset == 0x2c && value != 0)
{
_channels[chan1].haltTimer = value;
_channels[chan2].haltTimer = value;
}
}
} else {
_channels[chan1].sweepRate = 0;
_channels[chan1].haltTimer = 0;
}
_channels[chan1].priority = priority;
_channels[chan2].priority = priority;
_channels[chan1].resourceId = nr;
_channels[chan2].resourceId = nr;
// Start the Paula playing it
setChannelInterrupt(chan1, true);
setChannelInterrupt(chan2, true);
setChannelPeriod(chan1, MAX((_channels[chan1].period >> 16) & 0xFFFF, 124));
setChannelPeriod(chan2, MAX((_channels[chan2].period >> 16) & 0xFFFF, 124));
setChannelVolume(chan1, MIN((_channels[chan1].volume >> 8) & 0x3F, 0x3F));
setChannelVolume(chan2, MIN((_channels[chan2].volume >> 8) & 0x3F, 0x3F));
// Start as looped, then generate interrupts to handle looping properly
setChannelData(chan1, (int8 *)data + offsetL, (int8 *)data + offsetL, lengthL, lengthL);
setChannelData(chan2, (int8 *)data + offsetR, (int8 *)data + offsetR, lengthR, lengthR);
interruptChannel(chan1);
interruptChannel(chan2);
}
}
void Player_V3A::interrupt() {
if (_vm->_game.id == GID_INDY3) {
updateMusicIndy();
} else if (_vm->_game.id == GID_LOOM) {
updateMusicLoom();
}
updateSounds();
}
void Player_V3A::interruptChannel(byte channel) {
// check looping
if (_channels[channel].loopCount == -1)
return;
if (_channels[channel].loopCount) {
_channels[channel].loopCount--;
if (_channels[channel].loopCount <= 0) {
// On the last loop, set it to no longer repeat
setChannelInterrupt(channel, false);
setChannelSampleStart(channel, nullptr);
setChannelSampleLen(channel, 0);
// If there was no music playing, mark the channel as Unused
if (_curSong == -1)
_channels[channel].resourceId = -1;
}
}
}
void Player_V3A::updateSounds() {
for (int i = 0; i < 4; i++) {
if (!_channels[i].loopCount)
continue;
setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
setChannelPeriod(i, MAX((_channels[i].period >> 16) & 0xFFFF, 124));
// Only process ones that are sweeping, since others are handled by interruptChannel above
if (!_channels[i].sweepRate)
continue;
if (_channels[i].haltTimer) {
_channels[i].haltTimer--;
if (!_channels[i].haltTimer) {
// Once the timer reaches zero, immediately it stop looping
_channels[i].loopCount = 1;
interruptChannel(i);
}
}
_channels[i].period += _channels[i].sweepRate;
}
}
void Player_V3A::updateMusicIndy() {
// technically, musicTimer should only be incremented during playback, but that seems to cause problems
_musicTimer++;
if (!_songDelay || !_songData)
return;
for (int i = 0; i < 4; i++) {
if (_channels[i].haltTimer)
_channels[i].haltTimer--;
// When a looped sample runs out, fade the volume to zero
// Non-looped samples will be allowed to continue playing
if (!_channels[i].haltTimer && _channels[i].loopCount) {
_channels[i].volume -= _channels[i].fadeRate;
// Once the volume hits zero, immediately silence it
if (_channels[i].volume < 1) {
_channels[i].volume = 0;
_channels[i].loopCount = 0;
clearVoice(i);
setChannelInterrupt(i, false);
} else
setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
}
}
if (--_songDelay)
return;
int8 *songData = &_songData[0x1C + _songPtr];
while (1) {
int code = songData[0];
if ((code & 0xF0) == 0x80) {
// play a note
int instrument = songData[0] & 0xF;
int pitch = songData[1] & 0xFF;
int volume = (songData[2] / 2) & 0xFF;
int duration = songData[3] & 0xFF;
_songPtr += 4;
songData += 4;
// pitch 0 == global rest
if (pitch == 0) {
_songDelay = duration;
return;
}
// Find an available sound channel
// Indy3 starts at channel (inst & 3) and tries them in sequence
int channel = instrument & 0x3;
for (int i = 0; i < 4; i++) {
if (!_channels[channel].haltTimer)
break;
channel = (channel + 1) & 3;
}
startNote(channel, instrument, pitch, volume, duration);
} else {
// Reached the end
for (int i = 0; i < 4; i++) {
// Subtle bug in the original engine - it only checks the LAST playing channel
// (rather than checking all of them)
if (_channels[i].loopCount)
_songDelay = _channels[i].haltTimer;
}
if (_songDelay == 0) {
if ((code & 0xFF) == 0xFB) {
// repeat
_songPtr = 0;
_songDelay = 1;
} else {
// stop
stopSound(_curSong);
}
}
}
if ((_songDelay) || (_curSong == -1))
break;
}
}
void Player_V3A::updateMusicLoom() {
// technically, musicTimer should only be incremented during playback, but that seems to cause problems
_musicTimer++;
if (!_songDelay || !_songData)
return;
// Update all playing notes
for (int i = 0; i < 4; i++) {
// Mark all notes that were started during a previous update
_channels[i].canOverride = 1;
if (_channels[i].haltTimer)
_channels[i].haltTimer--;
// When a looped sample runs out, fade the volume to zero
// Non-looped samples will be allowed to continue playing
if (!_channels[i].haltTimer && _channels[i].loopCount) {
_channels[i].volume -= _channels[i].fadeRate;
// Once the volume hits zero, immediately silence it
if (_channels[i].volume < 1) {
_channels[i].volume = 0;
_channels[i].loopCount = 0;
clearVoice(i);
setChannelInterrupt(i, false);
} else
setChannelVolume(i, MIN((_channels[i].volume >> 8) & 0x3F, 0x3F));
}
}
if (--_songDelay)
return;
int8 *songData = &_songData[0x1C + _songPtr];
// Loom uses an elaborate queue to deal with overlapping notes and limited sound channels
int queuePos = 0;
int queueInstrument[4];
int queuePitch[4];
int queueVolume[4];
int queueDuration[4];
while (1) {
int code = songData[0];
if ((code & 0xF0) == 0x80) {
// play a note
int instrument = songData[0] & 0xF;
int pitch = songData[1] & 0xFF;
int volume = (((songData[2] < 0) ? (songData[2] + 1) : songData[2]) / 2) & 0xFF;
int duration = songData[3] & 0xFF;
_songPtr += 4;
songData += 4;
// pitch 0 == global rest
if (pitch == 0) {
_songDelay = duration;
break;
}
// Try to find an appropriate channel to use
// Channel must be playing the same instrument, started during a previous loop, and within 6 frames of ending
int channel;
for (channel = 0; channel < 4; channel++)
if ((_channels[channel].instrument == instrument) && (_channels[channel].canOverride) && (_channels[channel].haltTimer < 6))
break;
if (channel != 4) {
// Channel was found, so start playing the note
startNote(channel, instrument, pitch, volume, duration);
} else if (queuePos < 4) {
// No channel found - put it in a queue to process at the end
queueInstrument[queuePos] = instrument;
queuePitch[queuePos] = pitch;
queueVolume[queuePos] = volume;
queueDuration[queuePos] = duration;
++queuePos;
}
} else {
// Reached end of song
for (int i = 0; i < 4; i++) {
// Subtle bug in the original engine - it only checks the LAST playing channel
// rather than checking ALL of them
if (_channels[i].loopCount)
_songDelay = _channels[i].haltTimer;
}
if (_songDelay == 0) {
if ((code & 0xFF) == 0xFB) {
// repeat
_songPtr = 0;
_songDelay = 1;
} else {
// stop
stopSound(_curSong);
}
}
}
if ((_songDelay) || (_curSong == -1))
break;
}
while (queuePos--) {
// Take all of the enqueued note requests and try to fit them somewhere
int channel;
for (channel = 0; channel < 4; channel++) {
// First, find a soon-to-expire channel that wasn't explicitly assigned this loop
if ((_channels[channel].canOverride) && (_channels[channel].haltTimer < 6))
break;
}
if (channel == 4) {
// If no channel found, pick the first channel playing this instrument
for (channel = 0; channel < 4; channel++) {
if (_channels[channel].instrument == queueInstrument[queuePos])
break;
}
}
if (channel != 4) {
// If we found a channel, play the note there - otherwise, it gets lost
startNote(channel, queueInstrument[queuePos], queuePitch[queuePos], queueVolume[queuePos], queueDuration[queuePos]);
}
}
}
void Player_V3A::startNote(int channel, int instrument, int pitch, int volume, int duration) {
const InstData &instData = _wavetablePtrs[instrument];
SndChan &curChan = _channels[channel];
// for Loom, adjust pitch
pitch += instData.pitchAdjust;
// and set channel precedence parameters
curChan.instrument = instrument;
curChan.canOverride = 0;
// Split pitch into octave+offset, truncating as needed
int octave = (pitch / 12) - 2;
pitch = pitch % 12;
if (octave < 0)
octave = 0;
if (octave > 5)
octave = 5;
int actualOctave = instData.octave[octave];
curChan.period = NOTE_FREQS[actualOctave][pitch] << 16;
curChan.volume = (volume & 0xFF) << 8;
curChan.sweepRate = 0;
curChan.fadeRate = instData.volumeFade;
curChan.haltTimer = duration;
// For music, pre-decrement the loop counter and skip the initial interrupt
if (instData.loopLen[octave]) {
curChan.loopCount = -1;
setChannelInterrupt(channel, true);
} else {
curChan.loopCount = 0;
setChannelInterrupt(channel, false);
}
setChannelPeriod(channel, MAX((curChan.period >> 16) & 0xFFFF, 124));
setChannelVolume(channel, MIN((curChan.volume >> 8) & 0x3F, 0x3F));
setChannelData(channel, instData.mainData[octave], instData.loopData[octave], instData.mainLen[octave], instData.loopLen[octave]);
}
int Player_V3A::getMusicTimer() {
// Actual code in Amiga version returns 5+timer/28, which syncs poorly in ScummVM
// Presumably, this was meant to help slower machines sync better
return _musicTimer / 30;
}
int Player_V3A::getSoundStatus(int nr) const {
if (nr == -1)
return 0;
if (nr == _curSong)
return 1;
for (int i = 0; i < 4; i++)
if (_channels[i].resourceId == nr)
return 1;
return 0;
}
} // End of namespace Scumm