/* 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "scumm/player_ad.h" #include "scumm/imuse/imuse.h" #include "scumm/scumm.h" #include "scumm/resource.h" #include "audio/fmopl.h" #include "common/textconsole.h" #include "common/config-manager.h" namespace Scumm { #define AD_CALLBACK_FREQUENCY 472 Player_AD::Player_AD(ScummEngine *scumm, Audio::Mixer *mixer) : _vm(scumm), _mixer(mixer), _rate(mixer->getOutputRate()) { _opl2 = OPL::Config::create(); if (!_opl2->init(_rate)) { error("Could not initialize OPL2 emulator"); } _samplesPerCallback = _rate / AD_CALLBACK_FREQUENCY; _samplesPerCallbackRemainder = _rate % AD_CALLBACK_FREQUENCY; _samplesTillCallback = 0; _samplesTillCallbackRemainder = 0; memset(_registerBackUpTable, 0, sizeof(_registerBackUpTable)); writeReg(0x01, 0x00); writeReg(0xBD, 0x00); writeReg(0x08, 0x00); writeReg(0x01, 0x20); _mixer->playStream(Audio::Mixer::kPlainSoundType, &_soundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true); _engineMusicTimer = 0; _soundPlaying = -1; _curOffset = 0; _sfxTimer = 4; _rndSeed = 1; memset(_channels, 0, sizeof(_channels)); memset(_sfxResource, 0, sizeof(_sfxResource)); memset(_sfxPriority, 0, sizeof(_sfxPriority)); } Player_AD::~Player_AD() { _mixer->stopHandle(_soundHandle); stopAllSounds(); Common::StackLock lock(_mutex); delete _opl2; _opl2 = 0; } void Player_AD::setMusicVolume(int vol) { // HACK: We ignore the parameter and set up the volume specified in the // config manager. This allows us to differentiate between music and sfx // volume changes. setupVolume(); } void Player_AD::startSound(int sound) { Common::StackLock lock(_mutex); // Query the sound resource const byte *res = _vm->getResourceAddress(rtSound, sound); if (res[2] == 0x80) { // Stop the current sounds stopAllSounds(); // Lock the new music resource _soundPlaying = sound; _vm->_res->lock(rtSound, _soundPlaying); // Start the new music resource _resource = res; startMusic(); } else { // Only try to start a sfx when no music is playing. if (_soundPlaying == -1) { const byte priority = res[0]; const byte channel = res[1]; // Check for out of bounds access if (channel >= 3) { warning("AdLib sfx resource %d uses channel %d", sound, channel); return; } // Check whether the channel is free or the priority of the new // sfx resource is above the old one. if (_channels[channel * 3 + 0].state || _channels[channel * 3 + 1].state || _channels[channel * 3 + 2].state) { if (_sfxPriority[channel] > priority) { return; } } // Lock the new resource _sfxResource[channel] = sound; _sfxPriority[channel] = priority; _vm->_res->lock(rtSound, sound); // Start the actual sfx resource _resource = res; startSfx(); } } // Setup the sound volume setupVolume(); } void Player_AD::stopSound(int sound) { Common::StackLock lock(_mutex); if (sound == _soundPlaying) { stopAllSounds(); } else { for (int i = 0; i < 3; ++i) { if (_sfxResource[i] == sound) { if (_channels[i * 3 + 0].state || _channels[i * 3 + 1].state || _channels[i * 3 + 2].state) { // Unlock the sound resource _vm->_res->unlock(rtSound, sound); // Stop the actual sfx playback _channels[i * 3 + 0].state = 0; _channels[i * 3 + 1].state = 0; _channels[i * 3 + 2].state = 0; clearChannel(i * 3 + 0); clearChannel(i * 3 + 1); clearChannel(i * 3 + 2); } } } } } void Player_AD::stopAllSounds() { Common::StackLock lock(_mutex); // Unlock the music resource if present if (_soundPlaying != -1) { _vm->_res->unlock(rtSound, _soundPlaying); _soundPlaying = -1; } // Stop the music playback _curOffset = 0; // Unloack all used sfx resources for (int i = 0; i < 3; ++i) { if (_channels[i * 3 + 0].state || _channels[i * 3 + 1].state || _channels[i * 3 + 2].state) { _vm->_res->unlock(rtSound, _sfxResource[i]); } } // Reset all the sfx channels for (int i = 0; i < 9; ++i) { _channels[i].state = 0; clearChannel(i); } writeReg(0xBD, 0x00); } int Player_AD::getMusicTimer() { return _engineMusicTimer; } int Player_AD::getSoundStatus(int sound) const { return (sound == _soundPlaying); } void Player_AD::saveLoadWithSerializer(Serializer *ser) { Common::StackLock lock(_mutex); if (ser->getVersion() < VER(95)) { IMuse *dummyImuse = IMuse::create(_vm->_system, NULL, NULL); dummyImuse->save_or_load(ser, _vm, false); delete dummyImuse; return; } // TODO: Be nicer than the original and save the data to continue the // currently played sound resources on load? } int Player_AD::readBuffer(int16 *buffer, const int numSamples) { Common::StackLock lock(_mutex); int len = numSamples; while (len > 0) { if (!_samplesTillCallback) { // Run the update callback for music or sfx depending on which is // active. if (_curOffset) { updateMusic(); } else { updateSfx(); } _samplesTillCallback = _samplesPerCallback; _samplesTillCallbackRemainder += _samplesPerCallbackRemainder; if (_samplesTillCallbackRemainder >= AD_CALLBACK_FREQUENCY) { ++_samplesTillCallback; _samplesTillCallbackRemainder -= AD_CALLBACK_FREQUENCY; } } const int samplesToRead = MIN(len, _samplesTillCallback); _opl2->readBuffer(buffer, samplesToRead); buffer += samplesToRead; len -= samplesToRead; _samplesTillCallback -= samplesToRead; } return numSamples; } void Player_AD::setupVolume() { // Setup the correct volume int soundVolumeMusic = CLIP(ConfMan.getInt("music_volume"), 0, Audio::Mixer::kMaxChannelVolume); int soundVolumeSfx = CLIP(ConfMan.getInt("sfx_volume"), 0, Audio::Mixer::kMaxChannelVolume); if (ConfMan.hasKey("mute")) { if (ConfMan.getBool("mute")) { soundVolumeMusic = 0; soundVolumeSfx = 0; } } // In case a music is being played set the music volume. Set the sfx // volume otherwise. This is safe because in the latter case either // sfx are playing or there is no sound being played at all. if (_soundPlaying != -1) { _mixer->setChannelVolume(_soundHandle, soundVolumeMusic); } else { _mixer->setChannelVolume(_soundHandle, soundVolumeSfx); } } void Player_AD::writeReg(int r, int v) { if (r >= 0 && r < ARRAYSIZE(_registerBackUpTable)) { _registerBackUpTable[r] = v; } _opl2->writeReg(r, v); } uint8 Player_AD::readReg(int r) const { if (r >= 0 && r < ARRAYSIZE(_registerBackUpTable)) { return _registerBackUpTable[r]; } else { return 0; } } void Player_AD::setupChannel(const uint channel, const byte *instrOffset) { instrOffset += 2; writeReg(0xC0 + channel, *instrOffset++); setupOperator(_operatorOffsetTable[channel * 2 + 0], instrOffset); setupOperator(_operatorOffsetTable[channel * 2 + 1], instrOffset); } void Player_AD::setupOperator(const uint opr, const byte *&instrOffset) { writeReg(0x20 + opr, *instrOffset++); writeReg(0x40 + opr, *instrOffset++); writeReg(0x60 + opr, *instrOffset++); writeReg(0x80 + opr, *instrOffset++); writeReg(0xE0 + opr, *instrOffset++); } const int Player_AD::_operatorOffsetTable[18] = { 0, 3, 1, 4, 2, 5, 8, 11, 9, 12, 10, 13, 16, 19, 17, 20, 18, 21 }; // Music void Player_AD::startMusic() { memset(_instrumentOffset, 0, sizeof(_instrumentOffset)); memset(_channelLastEvent, 0, sizeof(_channelLastEvent)); memset(_channelFrequency, 0, sizeof(_channelFrequency)); memset(_channelB0Reg, 0, sizeof(_channelB0Reg)); _voiceChannels = 0; uint instruments = _resource[10]; for (uint i = 0; i < instruments; ++i) { const int instrIndex = _resource[11 + i] - 1; if (0 <= instrIndex && instrIndex < 16) { _instrumentOffset[instrIndex] = i * 16 + 16 + 3; _voiceChannels |= _resource[_instrumentOffset[instrIndex] + 13]; } } if (_voiceChannels) { _mdvdrState = 0x20; _voiceChannels = 6; } else { _mdvdrState = 0; _voiceChannels = 9; } _curOffset = 0x93; // TODO: is this the same for Loom? _nextEventTimer = 40; _engineMusicTimer = 0; _internalMusicTimer = 0; _musicTimer = 0; writeReg(0xBD, _mdvdrState); const bool isLoom = (_vm->_game.id == GID_LOOM); _timerLimit = isLoom ? 473 : 256; _musicTicks = _resource[3] * (isLoom ? 2 : 1); _loopFlag = (_resource[4] == 0); _musicLoopStart = READ_LE_UINT16(_resource + 5); } void Player_AD::updateMusic() { _musicTimer += _musicTicks; if (_musicTimer < _timerLimit) { return; } _musicTimer -= _timerLimit; ++_internalMusicTimer; if (_internalMusicTimer > 120) { _internalMusicTimer = 0; ++_engineMusicTimer; } --_nextEventTimer; if (_nextEventTimer) { return; } while (true) { uint command = _resource[_curOffset++]; if (command == 0xFF) { // META EVENT // Get the command number. command = _resource[_curOffset++]; if (command == 47) { // End of track if (_loopFlag) { // In case the track is looping jump to the start. _curOffset = _musicLoopStart; _nextEventTimer = 0; } else { // Otherwise completely stop playback. stopAllSounds(); } return; } else if (command == 88) { // This is proposedly a debug information insertion. The CMS // player code handles this differently, but is still using // the same resources... _curOffset += 5; } else if (command == 81) { // Change tempo. This is used exclusively in Loom. const uint timing = _resource[_curOffset + 2] | (_resource[_curOffset + 1] << 8); _musicTicks = 0x73000 / timing; command = _resource[_curOffset++]; _curOffset += command; } else { // In case an unknown meta event occurs just skip over the // data by using the length supplied. command = _resource[_curOffset++]; _curOffset += command; } } else { if (command >= 0x90) { // NOTE ON // Extract the channel number and save it in command. command -= 0x90; const uint instrOffset = _instrumentOffset[command]; if (instrOffset) { if (_resource[instrOffset + 13] != 0) { setupRhythm(_resource[instrOffset + 13], instrOffset); } else { int channel = findFreeChannel(); if (channel != -1) { noteOff(channel); setupChannel(channel, instrOffset); _channelLastEvent[channel] = command + 0x90; _channelFrequency[channel] = _resource[_curOffset]; setupFrequency(channel, _resource[_curOffset]); } } } } else { // NOTE OFF const uint note = _resource[_curOffset]; command += 0x10; // Find the output channel which plays the note. uint channel = 0xFF; for (uint i = 0; i < _voiceChannels; ++i) { if (_channelFrequency[i] == note && _channelLastEvent[i] == command) { channel = i; break; } } if (channel != 0xFF) { // In case a output channel playing the note was found, // stop it. noteOff(channel); } else { // In case there is no such note this will disable the // rhythm instrument played on the channel. command -= 0x90; const uint instrOffset = _instrumentOffset[command]; if (instrOffset && _resource[instrOffset + 13] != 0) { const uint rhythmInstr = _resource[instrOffset + 13]; if (rhythmInstr < 6) { _mdvdrState &= _mdvdrTable[rhythmInstr] ^ 0xFF; writeReg(0xBD, _mdvdrState); } } } } _curOffset += 2; } // In case there is a delay till the next event stop handling. if (_resource[_curOffset] != 0) { break; } ++_curOffset; } _nextEventTimer = _resource[_curOffset++]; if (_nextEventTimer & 0x80) { _nextEventTimer -= 0x80; _nextEventTimer <<= 7; _nextEventTimer |= _resource[_curOffset++]; } _nextEventTimer >>= (_vm->_game.id == GID_LOOM) ? 2 : 1; if (!_nextEventTimer) { _nextEventTimer = 1; } } void Player_AD::noteOff(uint channel) { _channelLastEvent[channel] = 0; writeReg(0xB0 + channel, _channelB0Reg[channel] & 0xDF); } int Player_AD::findFreeChannel() { for (uint i = 0; i < _voiceChannels; ++i) { if (!_channelLastEvent[i]) { return i; } } return -1; } void Player_AD::setupFrequency(uint channel, int8 frequency) { frequency -= 31; if (frequency < 0) { frequency = 0; } uint octave = 0; while (frequency >= 12) { frequency -= 12; ++octave; } const uint noteFrequency = _noteFrequencies[frequency]; octave <<= 2; octave |= noteFrequency >> 8; octave |= 0x20; writeReg(0xA0 + channel, noteFrequency & 0xFF); _channelB0Reg[channel] = octave; writeReg(0xB0 + channel, octave); } void Player_AD::setupRhythm(uint rhythmInstr, uint instrOffset) { if (rhythmInstr == 1) { setupChannel(6, instrOffset); writeReg(0xA6, _resource[instrOffset++]); writeReg(0xB6, _resource[instrOffset] & 0xDF); _mdvdrState |= 0x10; writeReg(0xBD, _mdvdrState); } else if (rhythmInstr < 6) { const byte *secondOperatorOffset = _resource + instrOffset + 8; setupOperator(_rhythmOperatorTable[rhythmInstr], secondOperatorOffset); writeReg(0xA0 + _rhythmChannelTable[rhythmInstr], _resource[instrOffset++]); writeReg(0xB0 + _rhythmChannelTable[rhythmInstr], _resource[instrOffset++] & 0xDF); writeReg(0xC0 + _rhythmChannelTable[rhythmInstr], _resource[instrOffset]); _mdvdrState |= _mdvdrTable[rhythmInstr]; writeReg(0xBD, _mdvdrState); } } const uint Player_AD::_noteFrequencies[12] = { 0x200, 0x21E, 0x23F, 0x261, 0x285, 0x2AB, 0x2D4, 0x300, 0x32E, 0x35E, 0x390, 0x3C7 }; const uint Player_AD::_mdvdrTable[6] = { 0x00, 0x10, 0x08, 0x04, 0x02, 0x01 }; const uint Player_AD::_rhythmOperatorTable[6] = { 0x00, 0x00, 0x14, 0x12, 0x15, 0x11 }; const uint Player_AD::_rhythmChannelTable[6] = { 0x00, 0x00, 0x07, 0x08, 0x08, 0x07 }; // SFX void Player_AD::startSfx() { writeReg(0xBD, 0x00); // The second byte of the resource defines the logical channel where // the sound effect should be played. const int startChannel = _resource[1] * 3; // Clear the channel. _channels[startChannel + 0].state = 0; _channels[startChannel + 1].state = 0; _channels[startChannel + 2].state = 0; clearChannel(startChannel + 0); clearChannel(startChannel + 1); clearChannel(startChannel + 2); // Set up the first channel to pick up playback. _channels[startChannel].currentOffset = _channels[startChannel].startOffset = _resource + 2; _channels[startChannel].state = 1; // Scan for the start of the other channels and set them up if required. int curChannel = startChannel + 1; const byte *bufferPosition = _resource + 2; uint8 command = 0; while ((command = *bufferPosition) != 0xFF) { switch (command) { case 1: // INSTRUMENT DEFINITION bufferPosition += 15; break; case 2: // NOTE DEFINITION bufferPosition += 11; break; case 0x80: // LOOP bufferPosition += 1; break; default: // START OF CHANNEL bufferPosition += 1; _channels[curChannel].currentOffset = bufferPosition; _channels[curChannel].startOffset = bufferPosition; _channels[curChannel].state = 1; ++curChannel; break; } } } void Player_AD::updateSfx() { if (--_sfxTimer) { return; } _sfxTimer = 4; for (int i = 0; i <= 9; ++i) { if (!_channels[i].state) { continue; } updateChannel(i); } } void Player_AD::clearChannel(int channel) { writeReg(0xA0 + channel, 0x00); writeReg(0xB0 + channel, 0x00); } void Player_AD::updateChannel(int channel) { if (_channels[channel].state == 1) { parseSlot(channel); } else { updateSlot(channel); } } void Player_AD::parseSlot(int channel) { while (true) { const byte *curOffset = _channels[channel].currentOffset; switch (*curOffset) { case 1: // INSTRUMENT DEFINITION ++curOffset; _channels[channel].instrumentData[0] = *(curOffset + 0); _channels[channel].instrumentData[1] = *(curOffset + 2); _channels[channel].instrumentData[2] = *(curOffset + 9); _channels[channel].instrumentData[3] = *(curOffset + 8); _channels[channel].instrumentData[4] = *(curOffset + 4); _channels[channel].instrumentData[5] = *(curOffset + 3); _channels[channel].instrumentData[6] = 0; setupChannel(channel, curOffset); writeReg(0xA0 + channel, *(curOffset + 0)); writeReg(0xB0 + channel, *(curOffset + 1) & 0xDF); _channels[channel].currentOffset += 15; break; case 2: // NOTE DEFINITION ++curOffset; _channels[channel].state = 2; noteOffOn(channel); parseNote(channel, 0, curOffset); parseNote(channel, 1, curOffset); return; case 0x80: // LOOP _channels[channel].currentOffset = _channels[channel].startOffset; break; default: // START OF CHANNEL // When we encounter a start of another channel while playback // it means that the current channel is finished. Thus, we will // stop it. clearChannel(channel); _channels[channel].state = 0; // If no channel of the sound effect is playing anymore, unlock // the resource. channel /= 3; if (!_channels[channel + 0].state && !_channels[channel + 1].state && !_channels[channel + 2].state) { _vm->_res->unlock(rtSound, _sfxResource[channel]); } return; } } } void Player_AD::updateSlot(int channel) { const byte *curOffset = _channels[channel].currentOffset + 1; for (int num = 0; num <= 1; ++num, curOffset += 5) { if (!(*curOffset & 0x80)) { continue; } const int note = channel * 2 + num; bool updateNote = false; if (_notes[note].state == 2) { if (!--_notes[note].sustainTimer) { updateNote = true; } } else { updateNote = processNoteEnvelope(note, _notes[note].instrumentValue); if (_notes[note].bias) { writeRegisterSpecial(note, _notes[note].bias - _notes[note].instrumentValue, *curOffset & 0x07); } else { writeRegisterSpecial(note, _notes[note].instrumentValue, *curOffset & 0x07); } } if (updateNote) { if (processNote(note, curOffset)) { if (!(*curOffset & 0x08)) { _channels[channel].currentOffset += 11; _channels[channel].state = 1; continue; } else if (*curOffset & 0x10) { noteOffOn(channel); } _notes[note].state = -1; processNote(note, curOffset); } } if ((*curOffset & 0x20) && !--_notes[note].playTime) { _channels[channel].currentOffset += 11; _channels[channel].state = 1; } } } void Player_AD::parseNote(int channel, int num, const byte *offset) { if (num) { offset += 5; } if (*offset & 0x80) { const int note = channel * 2 + num; _notes[note].state = -1; processNote(note, offset); _notes[note].playTime = 0; if (*offset & 0x20) { _notes[note].playTime = (*(offset + 4) >> 4) * 118; _notes[note].playTime += (*(offset + 4) & 0x0F) * 8; } } } bool Player_AD::processNote(int note, const byte *offset) { if (++_notes[note].state == 4) { return true; } const int instrumentDataOffset = *offset & 0x07; _notes[note].bias = _noteBiasTable[instrumentDataOffset]; uint8 instrumentDataValue = 0; if (_notes[note].state == 0) { instrumentDataValue = _channels[note / 2].instrumentData[instrumentDataOffset]; } uint8 noteInstrumentValue = readRegisterSpecial(note, instrumentDataValue, instrumentDataOffset); if (_notes[note].bias) { noteInstrumentValue = _notes[note].bias - noteInstrumentValue; } _notes[note].instrumentValue = noteInstrumentValue; if (_notes[note].state == 2) { _notes[note].sustainTimer = _numStepsTable[*(offset + 3) >> 4]; if (*offset & 0x40) { _notes[note].sustainTimer = (((getRnd() << 8) * _notes[note].sustainTimer) >> 16) + 1; } } else { int timer1, timer2; if (_notes[note].state == 3) { timer1 = *(offset + 3) & 0x0F; timer2 = 0; } else { timer1 = *(offset + _notes[note].state + 1) >> 4; timer2 = *(offset + _notes[note].state + 1) & 0x0F; } int adjustValue = ((_noteAdjustTable[timer2] * _noteAdjustScaleTable[instrumentDataOffset]) >> 16) - noteInstrumentValue; setupNoteEnvelopeState(note, _numStepsTable[timer1], adjustValue); } return false; } void Player_AD::noteOffOn(int channel) { const uint8 regValue = readReg(0xB0 | channel); writeReg(0xB0 | channel, regValue & 0xDF); writeReg(0xB0 | channel, regValue | 0x20); } void Player_AD::writeRegisterSpecial(int note, uint8 value, int offset) { if (offset == 6) { return; } // Division by 2 extracts the channel number out of the note. note /= 2; uint8 regNum; if (_useOperatorTable[offset]) { regNum = _operatorOffsetTable[_channelOperatorOffsetTable[offset] + note * 2]; } else { regNum = _channelOffsetTable[note]; } regNum += _baseRegisterTable[offset]; uint8 regValue = readReg(regNum) & (~_registerMaskTable[offset]); regValue |= value << _registerShiftTable[offset]; writeReg(regNum, regValue); } uint8 Player_AD::readRegisterSpecial(int note, uint8 defaultValue, int offset) { if (offset == 6) { return 0; } // Division by 2 extracts the channel number out of the note. note /= 2; uint8 regNum; if (_useOperatorTable[offset]) { regNum = _operatorOffsetTable[_channelOperatorOffsetTable[offset] + note * 2]; } else { regNum = _channelOffsetTable[note]; } regNum += _baseRegisterTable[offset]; uint8 regValue; if (defaultValue) { regValue = defaultValue; } else { regValue = readReg(regNum); } regValue &= _registerMaskTable[offset]; regValue >>= _registerShiftTable[offset]; return regValue; } void Player_AD::setupNoteEnvelopeState(int note, int steps, int adjust) { _notes[note].preIncrease = 0; if (ABS(adjust) > steps) { _notes[note].preIncrease = 1; _notes[note].adjust = adjust / steps; _notes[note].envelope.stepIncrease = ABS(adjust % steps); } else { _notes[note].adjust = adjust; _notes[note].envelope.stepIncrease = ABS(adjust); } _notes[note].envelope.step = steps; _notes[note].envelope.stepCounter = 0; _notes[note].envelope.timer = steps; } bool Player_AD::processNoteEnvelope(int note, int &instrumentValue) { if (_notes[note].preIncrease) { instrumentValue += _notes[note].adjust; } _notes[note].envelope.stepCounter += _notes[note].envelope.stepIncrease; if (_notes[note].envelope.stepCounter >= _notes[note].envelope.step) { _notes[note].envelope.stepCounter -= _notes[note].envelope.step; if (_notes[note].adjust < 0) { --instrumentValue; } else { ++instrumentValue; } } if (--_notes[note].envelope.timer) { return false; } else { return true; } } uint8 Player_AD::getRnd() { if (_rndSeed & 1) { _rndSeed >>= 1; _rndSeed ^= 0xB8; } else { _rndSeed >>= 1; } return _rndSeed; } const uint Player_AD::_noteBiasTable[7] = { 0x00, 0x00, 0x3F, 0x00, 0x3F, 0x00, 0x00 }; const uint Player_AD::_numStepsTable[16] = { 1, 4, 6, 8, 10, 14, 18, 24, 36, 64, 100, 160, 240, 340, 600, 1200 }; const uint Player_AD::_noteAdjustScaleTable[7] = { 255, 7, 63, 15, 63, 15, 63 }; const uint Player_AD::_noteAdjustTable[16] = { 0, 4369, 8738, 13107, 17476, 21845, 26214, 30583, 34952, 39321, 43690, 48059, 52428, 46797, 61166, 65535 }; const bool Player_AD::_useOperatorTable[7] = { false, false, true, true, true, true, false }; const uint Player_AD::_channelOffsetTable[11] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 7 }; const uint Player_AD::_channelOperatorOffsetTable[7] = { 0, 0, 1, 1, 0, 0, 0 }; const uint Player_AD::_baseRegisterTable[7] = { 0xA0, 0xC0, 0x40, 0x20, 0x40, 0x20, 0x00 }; const uint Player_AD::_registerMaskTable[7] = { 0xFF, 0x0E, 0x3F, 0x0F, 0x3F, 0x0F, 0x00 }; const uint Player_AD::_registerShiftTable[7] = { 0, 1, 0, 0, 0, 0, 0 }; } // End of namespace Scumm