[Scummvm-git-logs] scummvm master -> 66842c10ed3fc855d3f8b8740c92c1e0a87df196
sev-
noreply at scummvm.org
Mon Dec 13 19:53:25 UTC 2021
This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
3f4d59800d AUDIO: Add Paula per-channel DMA interrupts
66842c10ed SCUMM: rewrite player_v3a to match original
Commit: 3f4d59800dea4ca05e80956c2a338a0bedb6a881
https://github.com/scummvm/scummvm/commit/3f4d59800dea4ca05e80956c2a338a0bedb6a881
Author: Quietust (quietust at gmail.com)
Date: 2021-12-13T20:53:21+01:00
Commit Message:
AUDIO: Add Paula per-channel DMA interrupts
These will be needed for proper emulation of the Amiga SCUMM V2/V3
sound engines.
Changed paths:
audio/mods/paula.cpp
audio/mods/paula.h
diff --git a/audio/mods/paula.cpp b/audio/mods/paula.cpp
index ed30e22ccf..c67de4a97b 100644
--- a/audio/mods/paula.cpp
+++ b/audio/mods/paula.cpp
@@ -81,6 +81,7 @@ void Paula::clearVoice(byte voice) {
_voice[voice].volume = 0;
_voice[voice].offset = Offset(0);
_voice[voice].dmaCount = 0;
+ _voice[voice].interrupt = false;
}
int Paula::readBuffer(int16 *buffer, const int numSamples) {
@@ -239,6 +240,15 @@ int Paula::readBufferIntern(int16 *buffer, const int numSamples) {
ch.data = ch.dataRepeat;
ch.length = ch.lengthRepeat;
+
+ // The Paula chip can generate an interrupt after it copies a channel's
+ // location and length values to its internal registers, signaling that
+ // it's safe to modify them. Some sound engines use this feature in order
+ // to control sound looping.
+ // NOTE: the real Paula would also do this during enableChannel() and in
+ // the middle of setChannelData(); for simplicity, we only do it here.
+ if (ch.interrupt)
+ interruptChannel(voice);
}
// If we have not yet generated enough samples, and looping is active: loop!
diff --git a/audio/mods/paula.h b/audio/mods/paula.h
index 90a5f790f4..af44132b97 100644
--- a/audio/mods/paula.h
+++ b/audio/mods/paula.h
@@ -112,6 +112,7 @@ protected:
Offset offset;
byte panning; // For stereo mixing: 0 = far left, 255 = far right
int dmaCount;
+ bool interrupt;
};
bool _end;
@@ -119,6 +120,8 @@ protected:
virtual void interrupt() = 0;
+ virtual void interruptChannel(byte channel) { }
+
void startPaula() {
_playing = true;
_end = false;
@@ -149,6 +152,11 @@ protected:
// ch.period = ch.periodRepeat;
}
+ void setChannelInterrupt(byte channel, bool enable) {
+ assert(channel < NUM_VOICES);
+ _voice[channel].interrupt = enable;
+ }
+
void setChannelPeriod(byte channel, int16 period) {
assert(channel < NUM_VOICES);
_voice[channel].period = period;
Commit: 66842c10ed3fc855d3f8b8740c92c1e0a87df196
https://github.com/scummvm/scummvm/commit/66842c10ed3fc855d3f8b8740c92c1e0a87df196
Author: Quietust (quietust at gmail.com)
Date: 2021-12-13T20:53:21+01:00
Commit Message:
SCUMM: rewrite player_v3a to match original
Now plays exactly the same as the original engines.
Notable changes:
- Switch to Audio::Paula from Player_MOD (which predated Paula).
- Music engine now works differently between Indy3 and Loom with regards
to the handling of overlapping notes and limited sound channels.
- Music (and some sound effects) now play in stereo.
- Music now overrides sound effects, as in the original games.
Changed paths:
engines/scumm/players/player_v3a.cpp
engines/scumm/players/player_v3a.h
diff --git a/engines/scumm/players/player_v3a.cpp b/engines/scumm/players/player_v3a.cpp
index f15c7ea78d..37e2df1678 100644
--- a/engines/scumm/players/player_v3a.cpp
+++ b/engines/scumm/players/player_v3a.cpp
@@ -27,330 +27,575 @@
namespace Scumm {
-static const uint16 note_freqs[4][12] = {
- {0x06B0, 0x0650, 0x05F4, 0x05A0, 0x054C, 0x0500, 0x04B8, 0x0474, 0x0434, 0x03F8, 0x03C0, 0x0388},
- {0x0358, 0x0328, 0x02FA, 0x02D0, 0x02A6, 0x0280, 0x025C, 0x023A, 0x021A, 0x01FC, 0x01E0, 0x01C4},
- {0x01AC, 0x0194, 0x017D, 0x0168, 0x0153, 0x0140, 0x012E, 0x011D, 0x010D, 0x00FE, 0x00F0, 0x00E2},
- {0x00D6, 0x00CA, 0x00BE, 0x00B4, 0x00A9, 0x00A0, 0x0097, 0x008E, 0x0086, 0x007F, 0x00F0, 0x00E2}
-};
-
-Player_V3A::Player_V3A(ScummEngine *scumm, Audio::Mixer *mixer) {
- int i;
- _vm = scumm;
- for (i = 0; i < V3A_MAXMUS; i++) {
- _mus[i].id = 0;
- _mus[i].dur = 0;
+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;
}
- for (i = 0; i < V3A_MAXSFX; i++) {
- _sfx[i].id = 0;
- _sfx[i].dur = 0;
+ if (!ptr) {
+ error("player_v3a - unable to load music samples resource");
+ return false;
}
- _curSong = 0;
- _songData = nullptr;
- _songPtr = 0;
- _songDelay = 0;
+ // 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);
- _music_timer = 0;
+ int offset = 4;
- _isinit = false;
+ // Parse the header tables into a more convenient structure
+ _wavetablePtrs = new InstData[numInstruments];
+ for (int i = 0; i < numInstruments; i++) {
- _mod = new Player_MOD(mixer);
- _mod->setUpdateProc(update_proc, this, 60);
-}
+ // 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);
-Player_V3A::~Player_V3A() {
- int i;
- delete _mod;
- if (_isinit) {
- for (i = 0; _wavetable[i] != nullptr; i++) {
- for (int j = 0; j < 6; j++) {
- free(_wavetable[i]->_idat[j]);
- free(_wavetable[i]->_ldat[j]);
+ 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;
}
- free(_wavetable[i]);
+
+ // Octave shift for this octave
+ _wavetablePtrs[i].octave[j] = READ_BE_INT16(ptr + offset + 8);
+ offset += 10;
}
- free(_wavetable);
- }
-}
-void Player_V3A::setMusicVolume (int vol) {
- _mod->setMusicVolume(vol);
-}
+ // Fadeout rate, in 1/256ths of a volume level
+ _wavetablePtrs[i].volumeFade = READ_BE_INT16(ptr + offset);
+ offset += 2;
-int Player_V3A::getMusChan (int id) const {
- int i;
- for (i = 0; i < V3A_MAXMUS; i++) {
- if (_mus[i].id == id)
- break;
- }
- if (i == V3A_MAXMUS) {
- if (id == 0)
- warning("player_v3a - out of music channels");
- return -1;
+ 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 i;
+ return true;
}
-int Player_V3A::getSfxChan (int id) const {
- int i;
- for (i = 0; i < V3A_MAXSFX; i++) {
- if (_sfx[i].id == id)
- break;
- }
- if (i == V3A_MAXSFX) {
- if (id == 0)
- warning("player_v3a - out of sfx channels");
- return -1;
+
+Player_V3A::~Player_V3A() {
+ _mixer->stopHandle(_soundHandle);
+ if (_initState == kInitStateReady) {
+ delete[] _wavetableData;
+ delete[] _wavetablePtrs;
}
- return i;
+}
+
+void Player_V3A::setMusicVolume (int vol) {
+ _mixer->setChannelVolume(_soundHandle, vol);
}
void Player_V3A::stopAllSounds() {
- int i;
- for (i = 0; i < V3A_MAXMUS; i++) {
- if (_mus[i].id)
- _mod->stopChannel(_mus[i].id);
- _mus[i].id = 0;
- _mus[i].dur = 0;
+ for (int i = 0; i < 4; i++) {
+ clearVoice(i);
+ _channels[i].resourceId = -1;
}
- _curSong = 0;
+ _curSong = -1;
_songPtr = 0;
_songDelay = 0;
_songData = nullptr;
- for (i = 0; i < V3A_MAXSFX; i++) {
- if (_sfx[i].id)
- _mod->stopChannel(_sfx[i].id | 0x100);
- _sfx[i].id = 0;
- _sfx[i].dur = 0;
- }
}
void Player_V3A::stopSound(int nr) {
- int i;
- if (nr == 0) { // Amiga Loom does this near the end, when Chaos casts SILENCE on Hetchel
- stopAllSounds();
+ 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) {
- for (i = 0; i < V3A_MAXMUS; i++) {
- if (_mus[i].id)
- _mod->stopChannel(_mus[i].id);
- _mus[i].id = 0;
- _mus[i].dur = 0;
- }
- _curSong = 0;
- _songPtr = 0;
+ _curSong = -1;
_songDelay = 0;
+ _songPtr = 0;
_songData = nullptr;
- } else {
- i = getSfxChan(nr);
- if (i != -1) {
- _mod->stopChannel(nr | 0x100);
- _sfx[i].id = 0;
- _sfx[i].dur = 0;
- }
}
}
void Player_V3A::startSound(int nr) {
assert(_vm);
- byte *data = _vm->getResourceAddress(rtSound, nr);
- assert(data);
+ 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 (!_isinit) {
- int i;
- unsigned char *ptr;
- int offset = 4;
- int numInstruments;
+ if (_initState == kInitStateNotReady)
+ _initState = init() ? kInitStateReady : kInitStateFailed;
- if (_vm->_game.id == GID_INDY3) {
- ptr = _vm->getResourceAddress(rtSound, 83);
- numInstruments = 12;
- } else {
- ptr = _vm->getResourceAddress(rtSound, 79);
- numInstruments = 9;
- }
- assert(ptr);
- _wavetable = (instData **)malloc((numInstruments + 1) * sizeof(void *));
- for (i = 0; i < numInstruments; i++) {
- _wavetable[i] = (instData *)malloc(sizeof(instData));
- for (int j = 0; j < 6; j++) {
- int off, len;
- off = READ_BE_UINT16(ptr + offset + 0);
- _wavetable[i]->_ilen[j] = len = READ_BE_UINT16(ptr + offset + 2);
- if (len) {
- _wavetable[i]->_idat[j] = (char *)malloc(len);
- memcpy(_wavetable[i]->_idat[j],ptr + off,len);
- } else _wavetable[i]->_idat[j] = nullptr;
- off = READ_BE_UINT16(ptr + offset + 4);
- _wavetable[i]->_llen[j] = len = READ_BE_UINT16(ptr + offset + 6);
- if (len) {
- _wavetable[i]->_ldat[j] = (char *)malloc(len);
- memcpy(_wavetable[i]->_ldat[j],ptr + off,len);
- } else _wavetable[i]->_ldat[j] = nullptr;
- _wavetable[i]->_oct[j] = READ_BE_UINT16(ptr + offset + 8);
- offset += 10;
- }
- if (_vm->_game.id == GID_INDY3) {
- _wavetable[i]->_pitadjust = 0;
- offset += 2;
- } else {
- _wavetable[i]->_pitadjust = READ_BE_UINT16(ptr + offset + 2);
- offset += 4;
+ // 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);
}
- }
- _wavetable[i] = nullptr;
- _isinit = true;
- }
- if (getSoundStatus(nr))
- stopSound(nr); // if a sound is playing, restart it
+ // Keep a local copy of the song data
+ _songData = data;
+ _curSong = nr;
+ _songPtr = 0;
+ _songDelay = 1;
- if (data[26]) {
- if (_curSong)
- stopSound(_curSong);
- _curSong = nr;
- _songData = data;
- _songPtr = 0x1C;
- _songDelay = 1;
- _music_timer = 0;
- } else {
- int size = READ_BE_UINT16(data + 12);
- int rate = 3579545 / READ_BE_UINT16(data + 20);
- char *sound = (char *)malloc(size);
- int vol = (data[24] << 1) | (data[24] >> 5); // if I boost this to 0-255, it gets too loud and starts to clip
- memcpy(sound, data + READ_BE_UINT16(data + 8), size);
- int loopStart = 0, loopEnd = 0;
- int loopcount = data[27];
- if (loopcount > 1) {
- loopStart = READ_BE_UINT16(data + 10) - READ_BE_UINT16(data + 8);
- loopEnd = READ_BE_UINT16(data + 14);
+ // Start timer at 0 and increment every 30 frames (see below)
+ _musicTimer = 0;
+ } else {
+ // debug("player_v3a - wavetable unavailable, cannot play music");
}
- int i = getSfxChan();
- if (i == -1) {
- free(sound);
+ } 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;
- }
- _sfx[i].id = nr;
- _sfx[i].dur = 1 + loopcount * 60 * size / rate;
- if (READ_BE_UINT16(data + 16)) {
- _sfx[i].rate = READ_BE_UINT16(data + 20) << 16;
- _sfx[i].delta = (int32)READ_BE_UINT32(data + 32);
- _sfx[i].dur = READ_BE_UINT32(data + 40);
+
+ 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 {
- _sfx[i].delta = 0;
+ _channels[chan1].sweepRate = 0;
+ _channels[chan1].haltTimer = 0;
}
- _mod->startChannel(nr | 0x100, sound, size, rate, vol, loopStart, loopEnd);
+
+ _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::update_proc(void *param) {
- ((Player_V3A *)param)->playMusic();
+void Player_V3A::interrupt() {
+ if (_vm->_game.id == GID_INDY3) {
+ updateMusicIndy();
+ } else if (_vm->_game.id == GID_LOOM) {
+ updateMusicLoom();
+ }
+ updateSounds();
}
-void Player_V3A::playMusic() {
- int i;
- for (i = 0; i < V3A_MAXMUS; i++) {
- if (_mus[i].id) {
- _mus[i].dur--;
- if (_mus[i].dur)
- continue;
- _mod->stopChannel(_mus[i].id);
- _mus[i].id = 0;
+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;
}
}
- for (i = 0; i < V3A_MAXSFX; i++) {
- if (_sfx[i].id) {
- if (_sfx[i].delta) {
- uint16 oldrate = _sfx[i].rate >> 16;
- _sfx[i].rate += _sfx[i].delta;
- if (_sfx[i].rate < (55 << 16))
- _sfx[i].rate = 55 << 16; // at rates below 55, frequency
- uint16 newrate = _sfx[i].rate >> 16; // exceeds 65536, which is bad
- if (oldrate != newrate)
- _mod->setChannelFreq(_sfx[i].id | 0x100, 3579545 / newrate);
+}
+
+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);
}
- _sfx[i].dur--;
- if (_sfx[i].dur)
- continue;
- _mod->stopChannel(_sfx[i].id | 0x100);
- _sfx[i].id = 0;
}
+ _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++;
- _music_timer++;
- if (!_curSong)
+ if (!_songDelay || !_songData)
return;
- if (_songDelay && --_songDelay)
+
+ 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;
- if (_songPtr == 0) {
- // at the end of the song, and it wasn't looped - kill it
- _curSong = 0;
+
+ 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 inst, pit, vol, dur, oct;
- inst = _songData[_songPtr++];
- if ((inst & 0xF0) != 0x80) {
- // tune is at the end - figure out what's still playing
- // and see how long we have to wait until we stop/restart
- for (i = 0; i < V3A_MAXMUS; i++) {
- if (_songDelay < _mus[i].dur)
- _songDelay = _mus[i].dur;
+ 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 (inst == 0xFB) // it's a looped song, restart it afterwards
- _songPtr = 0x1C;
- else _songPtr = 0; // otherwise, terminate it
- break;
}
- inst &= 0xF;
- pit = _songData[_songPtr++];
- vol = _songData[_songPtr++] & 0x7F; // if I boost this to 0-255, it gets too loud and starts to clip
- dur = _songData[_songPtr++];
- if (pit == 0) {
- _songDelay = dur;
+ 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;
}
- pit += _wavetable[inst]->_pitadjust;
- oct = (pit / 12) - 2;
- pit = pit % 12;
- if (oct < 0)
- oct = 0;
- if (oct > 5)
- oct = 5;
- int rate = 3579545 / note_freqs[_wavetable[inst]->_oct[oct]][pit];
- if (!_wavetable[inst]->_llen[oct])
- dur = _wavetable[inst]->_ilen[oct] * 60 / rate;
- char *data = (char *)malloc(_wavetable[inst]->_ilen[oct] + _wavetable[inst]->_llen[oct]);
- if (_wavetable[inst]->_idat[oct])
- memcpy(data, _wavetable[inst]->_idat[oct], _wavetable[inst]->_ilen[oct]);
- if (_wavetable[inst]->_ldat[oct])
- memcpy(data + _wavetable[inst]->_ilen[oct], _wavetable[inst]->_ldat[oct], _wavetable[inst]->_llen[oct]);
-
- i = getMusChan();
- if (i == -1) {
- free(data);
- return;
+ 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]);
}
- _mus[i].id = i + 1;
- _mus[i].dur = dur + 1;
- _mod->startChannel(_mus[i].id, data, _wavetable[inst]->_ilen[oct] + _wavetable[inst]->_llen[oct], rate, vol,
- _wavetable[inst]->_ilen[oct], _wavetable[inst]->_ilen[oct] + _wavetable[inst]->_llen[oct]);
}
}
+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() {
- return _music_timer / 30;
+ // 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;
- if (getSfxChan(nr) != -1)
- return 1;
+ for (int i = 0; i < 4; i++)
+ if (_channels[i].resourceId == nr)
+ return 1;
return 0;
}
diff --git a/engines/scumm/players/player_v3a.h b/engines/scumm/players/player_v3a.h
index aa31e3b71c..3b0c763a2f 100644
--- a/engines/scumm/players/player_v3a.h
+++ b/engines/scumm/players/player_v3a.h
@@ -24,8 +24,10 @@
#define SCUMM_PLAYERS_PLAYER_V3A_H
#include "common/scummsys.h"
+#include "common/util.h"
#include "scumm/music.h"
-#include "scumm/players/player_mod.h"
+#include "audio/mixer.h"
+#include "audio/mods/paula.h"
class Mixer;
@@ -36,64 +38,90 @@ class ScummEngine;
/**
* Scumm V3 Amiga sound/music driver.
*/
-class Player_V3A : public MusicEngine {
+class Player_V3A : public MusicEngine, Audio::Paula {
public:
Player_V3A(ScummEngine *scumm, Audio::Mixer *mixer);
~Player_V3A() override;
+ // MusicEngine API
void setMusicVolume(int vol) override;
void startSound(int sound) override;
void stopSound(int sound) override;
void stopAllSounds() override;
- int getMusicTimer() override;
int getSoundStatus(int sound) const override;
+ int getMusicTimer() override;
-private:
- enum {
- V3A_MAXMUS = 24,
- V3A_MAXSFX = 16
- };
+protected:
+ // Paula API
+ void interrupt() override;
+ void interruptChannel(byte channel) override;
- struct musChan {
- int id;
- int dur;
+private:
+ struct SndChan {
+ int period; /* 16.16 fixed point */
+ int volume; /* 8.8 fixed point */
+ int loopCount; /* decrement once per loop, halt playback upon reaching zero */
+ int sweepRate; /* add to period once per frame */
+ int haltTimer; /* decrement once per frame, halt playback upon reaching zero */
+ int fadeRate; /* if haltTimer is zero, subtract 0x100*this from volume once per frame */
+
+ int resourceId;
+ int priority;
+
+ // Both of these are used exclusively by the Loom music engine
+ int instrument;
+ int canOverride;
};
- struct sfxChan {
- int id;
- int dur;
- uint32 rate;
- int32 delta;
+ struct InstData {
+ int8 *mainData[6];
+ uint16 mainLen[6];
+ int8 *loopData[6];
+ uint16 loopLen[6];
+ int16 octave[6];
+ int16 pitchAdjust;
+ int16 volumeFade;
};
- struct instData {
- char *_idat[6];
- uint16 _ilen[6];
- char *_ldat[6];
- uint16 _llen[6];
- uint16 _oct[6];
- int16 _pitadjust;
- };
+ ScummEngine *const _vm;
+ Audio::Mixer *const _mixer;
+ Audio::SoundHandle _soundHandle;
- ScummEngine *_vm;
- Player_MOD *_mod;
+ SndChan _channels[4];
- musChan _mus[V3A_MAXMUS];
- sfxChan _sfx[V3A_MAXSFX];
+ const int SFX_CHANNEL_MAP[2][2] = {
+ { 0, 1 },
+ { 3, 2 }
+ };
+ const uint16 NOTE_FREQS[4][12] = {
+ {0x06B0, 0x0650, 0x05F4, 0x05A0, 0x054C, 0x0500, 0x04B8, 0x0474, 0x0434, 0x03F8, 0x03C0, 0x0388},
+ {0x0358, 0x0328, 0x02FA, 0x02D0, 0x02A6, 0x0280, 0x025C, 0x023A, 0x021A, 0x01FC, 0x01E0, 0x01C4},
+ {0x01AC, 0x0194, 0x017D, 0x0168, 0x0153, 0x0140, 0x012E, 0x011D, 0x010D, 0x00FE, 0x00F0, 0x00E2},
+ {0x00D6, 0x00CA, 0x00BE, 0x00B4, 0x00A9, 0x00A0, 0x0097, 0x008E, 0x0086, 0x007F, 0x00F0, 0x00E2}
+ };
int _curSong;
- uint8 *_songData;
+ int8 *_songData;
uint16 _songPtr;
uint16 _songDelay;
- int _music_timer;
- bool _isinit;
+ int _musicTimer;
+
+ enum {
+ kInitStateFailed = -1,
+ kInitStateNotReady = 0,
+ kInitStateReady = 1
+ } _initState;
+
+ int8 *_wavetableData;
+ InstData *_wavetablePtrs;
- instData **_wavetable;
+ void updateProc();
+ void updateMusicIndy();
+ void updateMusicLoom();
+ void updateSounds();
+ void startNote(int channel, int instrument, int pitch, int volume, int duration);
- int getMusChan (int id = 0) const;
- int getSfxChan (int id = 0) const;
- static void update_proc(void *param);
- void playMusic();
+ bool init();
};
} // End of namespace Scumm
More information about the Scummvm-git-logs
mailing list