/* 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 "base/version.h" #include "common/util.h" #include "common/system.h" #include "scumm/imuse/imuse.h" #include "scumm/imuse/imuse_internal.h" #include "scumm/imuse/instrument.h" #include "scumm/saveload.h" #include "scumm/scumm.h" namespace Scumm { //////////////////////////////////////// // // IMuseInternal implementation // //////////////////////////////////////// IMuseInternal::IMuseInternal() : _native_mt32(false), _enable_gs(false), _sc55(false), _midi_adlib(NULL), _midi_native(NULL), _base_sounds(NULL), _sysex(NULL), _paused(false), _initialized(false), _tempoFactor(0), _player_limit(ARRAYSIZE(_players)), _recycle_players(false), _queue_end(0), _queue_pos(0), _queue_sound(0), _queue_adding(0), _queue_marker(0), _queue_cleared(0), _master_volume(0), _music_volume(0), _trigger_count(0), _snm_trigger_index(0) { memset(_channel_volume,0,sizeof(_channel_volume)); memset(_channel_volume_eff,0,sizeof(_channel_volume_eff)); memset(_volchan_table,0,sizeof(_volchan_table)); } IMuseInternal::~IMuseInternal() { // Do just enough stuff inside the mutex to // make sure any MIDI timing threads won't // interrupt us, and then do the rest outside // the mutex. { Common::StackLock lock(_mutex, "IMuseInternal::~IMuseInternal()"); _initialized = false; stopAllSounds_internal(); } if (_midi_adlib) { _midi_adlib->close(); delete _midi_adlib; _midi_adlib = 0; } if (_midi_native) { if (_native_mt32) { // Reset the MT-32 _midi_native->sysEx((const byte *) "\x41\x10\x16\x12\x7f\x00\x00\x01\x00", 9); _system->delayMillis(250); } _midi_native->close(); delete _midi_native; _midi_native = 0; } } byte *IMuseInternal::findStartOfSound(int sound) { byte *ptr = NULL; int32 size, pos; if (_base_sounds) ptr = _base_sounds[sound]; if (ptr == NULL) { debug(1, "IMuseInternal::findStartOfSound(): Sound %d doesn't exist", sound); return NULL; } // Check for old-style headers first, like 'RO' if (ptr[4] == 'R' && ptr[5] == 'O'&& ptr[6] != 'L') return ptr + 4; if (ptr[8] == 'S' && ptr[9] == 'O') return ptr + 8; ptr += 8; size = READ_BE_UINT32(ptr); ptr += 4; // Okay, we're looking for one of those things: either // an 'MThd' tag (for SMF), or a 'FORM' tag (for XMIDI). size = 48; // Arbitrary; we should find our tag within the first 48 bytes of the resource pos = 0; while (pos < size) { if (!memcmp(ptr + pos, "MThd", 4) || !memcmp(ptr + pos, "FORM", 4)) return ptr + pos; ++pos; // We could probably iterate more intelligently } debug(3, "IMuseInternal::findStartOfSound(): Failed to align on sound %d", sound); return 0; } bool IMuseInternal::isMT32(int sound) { byte *ptr = NULL; uint32 tag; if (_base_sounds) ptr = _base_sounds[sound]; if (ptr == NULL) return false; tag = READ_BE_UINT32(ptr + 4); switch (tag) { case MKTAG('A','D','L',' '): case MKTAG('A','S','F','X'): // Special AD class for old AdLib sound effects case MKTAG('S','P','K',' '): return false; case MKTAG('A','M','I',' '): case MKTAG('R','O','L',' '): return true; case MKTAG('M','A','C',' '): // Occurs in the Mac version of FOA and MI2 return true; case MKTAG('G','M','D',' '): return false; case MKTAG('M','I','D','I'): // Occurs in Sam & Max // HE games use Roland music if (ptr[12] == 'H' && ptr[13] == 'S') return true; else return false; } // Old style 'RO' has equivalent properties to 'ROL' if (ptr[4] == 'R' && ptr[5] == 'O') return true; // Euphony tracks show as 'SO' and have equivalent properties to 'ADL' if (ptr[8] == 'S' && ptr[9] == 'O') return false; error("Unknown music type: '%c%c%c%c'", (char)tag >> 24, (char)tag >> 16, (char)tag >> 8, (char)tag); return false; } bool IMuseInternal::isMIDI(int sound) { byte *ptr = NULL; uint32 tag; if (_base_sounds) ptr = _base_sounds[sound]; if (ptr == NULL) return false; tag = READ_BE_UINT32(ptr + 4); switch (tag) { case MKTAG('A','D','L',' '): case MKTAG('A','S','F','X'): // Special AD class for old AdLib sound effects case MKTAG('S','P','K',' '): return false; case MKTAG('A','M','I',' '): case MKTAG('R','O','L',' '): return true; case MKTAG('M','A','C',' '): // Occurs in the Mac version of FOA and MI2 return true; case MKTAG('G','M','D',' '): case MKTAG('M','I','D','I'): // Occurs in Sam & Max return true; } // Old style 'RO' has equivalent properties to 'ROL' if (ptr[4] == 'R' && ptr[5] == 'O') return true; // Euphony tracks show as 'SO' and have equivalent properties to 'ADL' // FIXME: Right now we're pretending it's GM. if (ptr[8] == 'S' && ptr[9] == 'O') return true; error("Unknown music type: '%c%c%c%c'", (char)tag >> 24, (char)tag >> 16, (char)tag >> 8, (char)tag); return false; } MidiDriver *IMuseInternal::getBestMidiDriver(int sound) { MidiDriver *driver = NULL; if (isMIDI(sound)) { if (_midi_native) { driver = _midi_native; } else { // Route it through AdLib anyway. driver = _midi_adlib; } } else { driver = _midi_adlib; } return driver; } Player *IMuseInternal::allocate_player(byte priority) { Player *player = _players, *best = NULL; int i; byte bestpri = 255; for (i = _player_limit; i != 0; i--, player++) { if (!player->isActive()) return player; if (player->getPriority() < bestpri) { best = player; bestpri = player->getPriority(); } } if (bestpri < priority || _recycle_players) return best; debug(1, "Denying player request"); return NULL; } void IMuseInternal::init_players() { Player *player = _players; int i; for (i = ARRAYSIZE(_players); i != 0; i--, player++) { player->_se = this; player->clear(); // Used to just set _active to false } } void IMuseInternal::init_parts() { Part *part; int i; for (i = 0, part = _parts; i != ARRAYSIZE(_parts); i++, part++) { part->init(); part->_se = this; part->_slot = i; } } //////////////////////////////////////// // // IMuse mixin interface methods // //////////////////////////////////////// void IMuseInternal::on_timer(MidiDriver *midi) { Common::StackLock lock(_mutex, "IMuseInternal::on_timer()"); if (_paused || !_initialized) return; if (midi == _midi_native || !_midi_native) handleDeferredCommands(midi); sequencer_timers(midi); } void IMuseInternal::pause(bool paused) { Common::StackLock lock(_mutex, "IMuseInternal::pause()"); if (_paused == paused) return; int vol = _music_volume; if (paused) _music_volume = 0; update_volumes(); _music_volume = vol; // Fix for Bug #817871. The MT-32 apparently fails // sometimes to respond to a channel volume message // (or only uses it for subsequent note events). // The result is hanging notes on pause. Reportedly // happens in the original distro, too. To fix that, // just send AllNotesOff to the channels. if (_midi_native && _native_mt32) { for (int i = 0; i < 16; ++i) _midi_native->send(123 << 8 | 0xB0 | i); } _paused = paused; } int IMuseInternal::save_or_load(Serializer *ser, ScummEngine *scumm) { Common::StackLock lock(_mutex, "IMuseInternal::save_or_load()"); const SaveLoadEntry mainEntries[] = { MKLINE(IMuseInternal, _queue_end, sleUint8, VER(8)), MKLINE(IMuseInternal, _queue_pos, sleUint8, VER(8)), MKLINE(IMuseInternal, _queue_sound, sleUint16, VER(8)), MKLINE(IMuseInternal, _queue_adding, sleByte, VER(8)), MKLINE(IMuseInternal, _queue_marker, sleByte, VER(8)), MKLINE(IMuseInternal, _queue_cleared, sleByte, VER(8)), MKLINE(IMuseInternal, _master_volume, sleByte, VER(8)), MKLINE(IMuseInternal, _trigger_count, sleUint16, VER(8)), MKLINE(IMuseInternal, _snm_trigger_index, sleUint16, VER(54)), MKARRAY(IMuseInternal, _channel_volume[0], sleUint16, 8, VER(8)), MKARRAY(IMuseInternal, _volchan_table[0], sleUint16, 8, VER(8)), MKEND() }; const SaveLoadEntry cmdQueueEntries[] = { MKARRAY(CommandQueue, array[0], sleUint16, 8, VER(23)), MKEND() }; // VolumeFader is obsolete. const SaveLoadEntry volumeFaderEntries[] = { MK_OBSOLETE(VolumeFader, player, sleUint16, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, active, sleUint8, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, curvol, sleUint8, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, speed_lo_max, sleUint16, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, num_steps, sleUint16, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, speed_hi, sleInt8, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, direction, sleInt8, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, speed_lo, sleInt8, VER(8), VER(16)), MK_OBSOLETE(VolumeFader, speed_lo_counter, sleUint16, VER(8), VER(16)), MKEND() }; const SaveLoadEntry snmTriggerEntries[] = { MKLINE(ImTrigger, sound, sleInt16, VER(54)), MKLINE(ImTrigger, id, sleByte, VER(54)), MKLINE(ImTrigger, expire, sleUint16, VER(54)), MKARRAY(ImTrigger, command[0], sleUint16, 8, VER(54)), MKEND() }; int i; ser->saveLoadEntries(this, mainEntries); ser->saveLoadArrayOf(_cmd_queue, ARRAYSIZE(_cmd_queue), sizeof(_cmd_queue[0]), cmdQueueEntries); ser->saveLoadArrayOf(_snm_triggers, ARRAYSIZE(_snm_triggers), sizeof(_snm_triggers[0]), snmTriggerEntries); // The players for (i = 0; i < ARRAYSIZE(_players); ++i) _players[i].saveLoadWithSerializer(ser); // The parts for (i = 0; i < ARRAYSIZE(_parts); ++i) _parts[i].saveLoadWithSerializer(ser); { // Load/save the instrument definitions, which were revamped with V11. Part *part = &_parts[0]; if (ser->getVersion() >= VER(11)) { for (i = ARRAYSIZE(_parts); i; --i, ++part) { part->_instrument.saveOrLoad(ser); } } else { for (i = ARRAYSIZE(_parts); i; --i, ++part) part->_instrument.clear(); } } // VolumeFader has been replaced with the more generic ParameterFader. // FIXME: replace this loop by something like // if (loading && version <= 16) ser->skip(XXX bytes); for (i = 0; i < 8; ++i) ser->saveLoadEntries(0, volumeFaderEntries); if (ser->isLoading()) { // Load all sounds that we need fix_players_after_load(scumm); fix_parts_after_load(); setImuseMasterVolume(_master_volume); if (_midi_native) reallocateMidiChannels(_midi_native); if (_midi_adlib) reallocateMidiChannels(_midi_adlib); } return 0; } bool IMuseInternal::get_sound_active(int sound) const { Common::StackLock lock(_mutex, "IMuseInternal::get_sound_active()"); return getSoundStatus_internal(sound, false) != 0; } int32 IMuseInternal::doCommand(int numargs, int a[]) { Common::StackLock lock(_mutex, "IMuseInternal::doCommand()"); return doCommand_internal(numargs, a); } void IMuseInternal::setBase(byte **base) { Common::StackLock lock(_mutex, "IMuseInternal::setBase()"); _base_sounds = base; } uint32 IMuseInternal::property(int prop, uint32 value) { Common::StackLock lock(_mutex, "IMuseInternal::property()"); switch (prop) { case IMuse::PROP_TEMPO_BASE: // This is a specified as a percentage of normal // music speed. The number must be an integer // ranging from 50 to 200(for 50% to 200% normal speed). if (value >= 50 && value <= 200) _tempoFactor = value; break; case IMuse::PROP_NATIVE_MT32: _native_mt32 = (value > 0); Instrument::nativeMT32(_native_mt32); if (_midi_native && _native_mt32) initMT32(_midi_native); break; case IMuse::PROP_GS: _enable_gs = (value > 0); // If True Roland MT-32 is not selected, run in GM or GS mode. // If it is selected, change the Roland GS synth to MT-32 mode. if (_midi_native && !_native_mt32) initGM(_midi_native); else if (_midi_native && _native_mt32 && _enable_gs) { _sc55 = true; initGM(_midi_native); } break; case IMuse::PROP_LIMIT_PLAYERS: if (value > 0 && value <= ARRAYSIZE(_players)) _player_limit = (int)value; break; case IMuse::PROP_RECYCLE_PLAYERS: _recycle_players = (value != 0); break; case IMuse::PROP_GAME_ID: _game_id = value; break; } return 0; } void IMuseInternal::addSysexHandler(byte mfgID, sysexfunc handler) { // TODO: Eventually support multiple sysEx handlers and pay // attention to the client-supplied manufacturer ID. Common::StackLock lock(_mutex, "IMuseInternal::property()"); _sysex = handler; } void IMuseInternal::startSoundWithNoteOffset(int sound, int offset) { Common::StackLock lock(_mutex, "IMuseInternal::startSound()"); startSound_internal(sound, offset); } //////////////////////////////////////// // // MusicEngine interface methods // //////////////////////////////////////// void IMuseInternal::setMusicVolume(int vol) { Common::StackLock lock(_mutex, "IMuseInternal::setMusicVolume()"); if (vol > 255) vol = 255; if (_music_volume == vol) return; _music_volume = vol; vol = _master_volume * _music_volume / 255; for (uint i = 0; i < ARRAYSIZE(_channel_volume); i++) { _channel_volume_eff[i] = _channel_volume[i] * vol / 255; } if (!_paused) update_volumes(); } void IMuseInternal::startSound(int sound) { Common::StackLock lock(_mutex, "IMuseInternal::startSound()"); startSound_internal(sound); } void IMuseInternal::stopSound(int sound) { Common::StackLock lock(_mutex, "IMuseInternal::stopSound()"); stopSound_internal(sound); } void IMuseInternal::stopAllSounds() { Common::StackLock lock(_mutex, "IMuseInternal::stopAllSounds()"); stopAllSounds_internal(); } int IMuseInternal::getSoundStatus(int sound) const { Common::StackLock lock(_mutex, "IMuseInternal::getSoundStatus()"); return getSoundStatus_internal (sound, true); } int IMuseInternal::getMusicTimer() { Common::StackLock lock(_mutex, "IMuseInternal::getMusicTimer()"); int best_time = 0; const Player *player = _players; for (int i = ARRAYSIZE(_players); i; i--, player++) { if (player->isActive()) { int timer = player->getMusicTimer(); if (timer > best_time) best_time = timer; } } return best_time; } //////////////////////////////////////// // // Internal versions of the IMuse and // MusicEngine base class methods. // These methods assume the appropriate // mutex locks have already been set, // and may also have slightly different // semantics than the interface methods. // //////////////////////////////////////// bool IMuseInternal::startSound_internal(int sound, int offset) { // Do not start a sound if it is already set to start on an ImTrigger // event. This fixes carnival music problems where a sound has been set // to trigger at the right time, but then is started up immediately // anyway, only to be restarted later when the trigger occurs. // // However, we have to make sure the sound with the trigger is actually // playing, otherwise the music may stop when Sam and Max are thrown // out of Bumpusville, because entering the mansion sets up a trigger // for a sound that isn't necessarily playing. This is somewhat related // to bug #780918. int i; ImTrigger *trigger = _snm_triggers; for (i = ARRAYSIZE(_snm_triggers); i; --i, ++trigger) { if (trigger->sound && trigger->id && trigger->command[0] == 8 && trigger->command[1] == sound && getSoundStatus_internal (trigger->sound,true)) return false; } void *ptr = findStartOfSound(sound); if (!ptr) { debug(2, "IMuseInternal::startSound(): Couldn't find sound %d", sound); return false; } // Check which MIDI driver this track should use. // If it's NULL, it ain't something we can play. MidiDriver *driver = getBestMidiDriver(sound); if (!driver) return false; // If the requested sound is already playing, start it over // from scratch. This was originally a hack to prevent Sam & Max // iMuse messiness while upgrading the iMuse engine, but it // is apparently necessary to deal with fade-and-restart // race conditions that were observed in MI2. Reference // Bug #590511 and Patch #607175 (which was reversed to fix // an FOA regression: Bug #622606). Player *player = findActivePlayer(sound); if (!player) player = allocate_player(128); if (!player) return false; // WORKAROUND: This is to work around a problem at the Dino Bungie // Memorial. // // There are three pieces of music involved here: // // 80 - Main theme (looping) // 81 - Music when entering Rex's and Wally's room (not looping) // 82 - Music when listening to Rex or Wally // // When entering, tune 81 starts, tune 80 is faded down (not out) and // a trigger is set in tune 81 to fade tune 80 back up. // // When listening to Rex or Wally, tune 82 is started, tune 81 is faded // out and tune 80 is faded down even further. // // However, when tune 81 is faded out its trigger will cause tune 80 to // fade back up, resulting in two tunes being played simultaneously at // full blast. It's no use trying to keep tune 81 playing at volume 0. // It doesn't loop, so eventually it will terminate on its own. // // I don't know how the original interpreter handled this - or even if // it handled it at all - but it looks like sloppy scripting to me. Our // workaround is to clear the trigger if the player listens to Rex or // Wally before tune 81 has finished on its own. if (_game_id == GID_SAMNMAX && sound == 82 && getSoundStatus_internal(81, false)) ImClearTrigger(81, 1); player->clear(); player->setOffsetNote(offset); return player->startSound(sound, driver); } int IMuseInternal::stopSound_internal(int sound) { int r = -1; Player *player = findActivePlayer(sound); if (player) { player->clear(); r = 0; } return r; } int IMuseInternal::stopAllSounds_internal() { Player *player = _players; for (int i = ARRAYSIZE(_players); i; i--, player++) { if (player->isActive()) player->clear(); } return 0; } int IMuseInternal::getSoundStatus_internal(int sound, bool ignoreFadeouts) const { const Player *player = _players; for (int i = ARRAYSIZE(_players); i; i--, player++) { if (player->isActive() && (!ignoreFadeouts || !player->isFadingOut())) { if (sound == -1) return player->getID(); else if (player->getID() == (uint16)sound) return 1; } } return (sound == -1) ? 0 : get_queue_sound_status(sound); } int32 IMuseInternal::doCommand_internal (int a, int b, int c, int d, int e, int f, int g, int h) { int args[8]; args[0] = a; args[1] = b; args[2] = c; args[3] = d; args[4] = e; args[5] = f; args[6] = g; args[7] = h; return doCommand_internal(8, args); } int32 IMuseInternal::doCommand_internal(int numargs, int a[]) { if (numargs < 1) return -1; int i; byte cmd = a[0] & 0xFF; byte param = a[0] >> 8; Player *player = NULL; if (!_initialized && (cmd || param)) return -1; { Common::String string = "doCommand - "; string += Common::String::format("%d (%d/%d)", a[0], (int)param, (int)cmd); for (i = 1; i < numargs; ++i) string += Common::String::format(", %d", a[i]); debugC(DEBUG_IMUSE, "%s", string.c_str()); } if (param == 0) { switch (cmd) { case 6: if (a[1] > 127) return -1; else { debug(0, "IMuse doCommand(6) - setImuseMasterVolume (%d)", a[1]); return setImuseMasterVolume((a[1] << 1) | (a[1] ? 0 : 1)); // Convert from 0-127 to 0-255 } case 7: debug(0, "IMuse doCommand(7) - getMasterVolume (%d)", a[1]); return _master_volume / 2; // Convert from 0-255 to 0-127 case 8: return startSound_internal(a[1]) ? 0 : -1; case 9: return stopSound_internal(a[1]); case 10: // FIXME: Sam and Max - Not sure if this is correct return stopAllSounds_internal(); case 11: return stopAllSounds_internal(); case 12: // Sam & Max: Player-scope commands player = findActivePlayer(a[1]); if (player == NULL) return -1; switch (a[3]) { case 6: // Set player volume. return player->setVolume(a[4]); default: error("IMuseInternal::doCommand(12) unsupported sub-command %d", a[3]); } return -1; case 13: return getSoundStatus_internal (a[1], true); case 14: // Sam and Max: Parameter fade player = findActivePlayer(a[1]); if (player) return player->addParameterFader(a[3], a[4], a[5]); return -1; case 15: // Sam & Max: Set hook for a "maybe" jump player = findActivePlayer(a[1]); if (player) { player->setHook(0, a[3], 0); return 0; } return -1; case 16: debug(0, "IMuse doCommand(16) - set_volchan (%d, %d)", a[1], a[2]); return set_volchan(a[1], a[2]); case 17: if (_game_id != GID_SAMNMAX) { debug(0, "IMuse doCommand(17) - set_channel_volume (%d, %d)", a[1], a[2]); return set_channel_volume(a[1], a[2]); } else { if (a[4]) { int b[16]; memset(b, 0, sizeof(b)); for (i = 0; i < numargs; ++i) b[i] = a[i]; return ImSetTrigger(b[1], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11]); } else { return ImClearTrigger(a[1], a[3]); } } case 18: if (_game_id != GID_SAMNMAX) { return set_volchan_entry(a[1], a[2]); } else { // Sam & Max: ImCheckTrigger. // According to Mike's notes to Ender, // this function returns the number of triggers // associated with a particular player ID and // trigger ID. a[0] = 0; for (i = 0; i < ARRAYSIZE(_snm_triggers); ++i) { if (_snm_triggers[i].sound == a[1] && _snm_triggers[i].id && (a[3] == -1 || _snm_triggers[i].id == a[3])) { ++a[0]; } } return a[0]; } case 19: // Sam & Max: ImClearTrigger // This should clear a trigger that's been set up // with ImSetTrigger(cmd == 17). Seems to work.... return ImClearTrigger(a[1], a[3]); case 20: // Sam & Max: Deferred Command addDeferredCommand(a[1], a[2], a[3], a[4], a[5], a[6], a[7]); return 0; case 2: case 3: return 0; default: error("doCommand(%d [%d/%d], %d, %d, %d, %d, %d, %d, %d) unsupported", a[0], param, cmd, a[1], a[2], a[3], a[4], a[5], a[6], a[7]); } } else if (param == 1) { if ((1 << cmd) & 0x783FFF) { player = findActivePlayer(a[1]); if (!player) return -1; if ((1 << cmd) & (1 << 11 | 1 << 22)) { assert(a[2] >= 0 && a[2] <= 15); player = (Player *)player->getPart(a[2]); if (!player) return -1; } } switch (cmd) { case 0: if (_game_id == GID_SAMNMAX) { if (a[3] == 1) // Measure number return ((player->getBeatIndex() - 1) >> 2) + 1; else if (a[3] == 2) // Beat number return player->getBeatIndex(); return -1; } else { return player->getParam(a[2], a[3]); } case 1: if (_game_id == GID_SAMNMAX) { // FIXME: Could someone verify this? // // This jump instruction is known to be used in // the following cases: // // 1) Going anywhere on the USA map // 2) Winning the Wak-A-Rat game // 3) Losing or quitting the Wak-A-Rat game // 4) Conroy hitting Max with a golf club // // For all these cases the position parameters // are always the same: 2, 1, 0, 0. // // 5) When leaving the bigfoot party. The // position parameters are: 3, 4, 300, 0 // 6) At Frog Rock, when the UFO appears. The // position parameters are: 10, 4, 400, 1 // // The last two cases used to be buggy, so I // have made a change to how the last two // position parameters are handled. I still do // not know if it's correct, but it sounds // good to me at least. debug(0, "doCommand(%d [%d/%d], %d, %d, %d, %d, %d, %d, %d)", a[0], param, cmd, a[1], a[2], a[3], a[4], a[5], a[6], a[7]); player->jump(a[3] - 1, (a[4] - 1) * 4 + a[5], a[6] + ((a[7] * player->getTicksPerBeat()) >> 2)); } else player->setPriority(a[2]); return 0; case 2: return player->setVolume(a[2]); case 3: player->setPan(a[2]); return 0; case 4: return player->setTranspose(a[2], a[3]); case 5: player->setDetune(a[2]); return 0; case 6: // WORKAROUND for bug #1324106. When playing the // "flourishes" as Rapp's body appears from his ashes, // MI2 sets up triggers to pause the music, in case the // animation plays too slowly, and then the music is // manually unpaused for the next part of the music. // // In ScummVM, the animation finishes slightly too // quickly, and the pause command is run *after* the // unpause command. So we work around it by ignoring // all attempts at pausing this particular sound. // // I could have sworn this wasn't needed after the // recent timer change, but now it looks like it's // still needed after all. if (_game_id != GID_MONKEY2 || player->getID() != 183 || a[2] != 0) { player->setSpeed(a[2]); } return 0; case 7: return player->jump(a[2], a[3], a[4]) ? 0 : -1; case 8: return player->scan(a[2], a[3], a[4]); case 9: return player->setLoop(a[2], a[3], a[4], a[5], a[6]) ? 0 : -1; case 10: player->clearLoop(); return 0; case 11: ((Part *)player)->set_onoff(a[3] != 0); return 0; case 12: return player->setHook(a[2], a[3], a[4]); case 13: return player->addParameterFader(ParameterFader::pfVolume, a[2], a[3]); case 14: return enqueue_trigger(a[1], a[2]); case 15: return enqueue_command(a[1], a[2], a[3], a[4], a[5], a[6], a[7]); case 16: return clear_queue(); case 19: return player->getParam(a[2], a[3]); case 20: return player->setHook(a[2], a[3], a[4]); case 21: return -1; case 22: ((Part *)player)->volume(a[3]); return 0; case 23: return query_queue(a[1]); case 24: return 0; default: error("doCommand(%d [%d/%d], %d, %d, %d, %d, %d, %d, %d) unsupported", a[0], param, cmd, a[1], a[2], a[3], a[4], a[5], a[6], a[7]); return -1; } } return -1; } // mixin end void IMuseInternal::sequencer_timers(MidiDriver *midi) { Player *player = _players; int i; for (i = ARRAYSIZE(_players); i != 0; i--, player++) { if (player->isActive() && player->getMidiDriver() == midi) { player->onTimer(); } } } void IMuseInternal::handle_marker(uint id, byte data) { uint16 *p = 0; uint pos; if (_queue_adding && _queue_sound == id && data == _queue_marker) return; // Fix for bug #733401, revised for bug #761637: // It would seem that sometimes a marker is in the queue // but not at the head position. In the case of our bug, // this seems to be the result of commands in the queue // for songs that are no longer playing. So we skip // ahead to the appropriate marker, effectively chomping // anything in the queue before it. This fixes the FOA // end credits music, but needs to be tested for inappopriate // behavior elsewhere. pos = _queue_end; while (pos != _queue_pos) { p = _cmd_queue[pos].array; if (p[0] == TRIGGER_ID && p[1] == id && p[2] == data) break; pos = (pos + 1) % ARRAYSIZE(_cmd_queue); } if (pos == _queue_pos) return; if (pos != _queue_end) debug(0, "Skipping entries in iMuse command queue to reach marker"); _trigger_count--; _queue_cleared = false; do { pos = (pos + 1) % ARRAYSIZE(_cmd_queue); if (_queue_pos == pos) break; p = _cmd_queue[pos].array; if (*p++ != COMMAND_ID) break; _queue_end = pos; doCommand_internal(p[0], p[1], p[2], p[3], p[4], p[5], p[6], 0); if (_queue_cleared) return; pos = _queue_end; } while (1); _queue_end = pos; } int IMuseInternal::get_channel_volume(uint a) { if (a < 8) return _channel_volume_eff[a]; return (_master_volume * _music_volume / 255) / 2; } Part *IMuseInternal::allocate_part(byte pri, MidiDriver *midi) { Part *part, *best = NULL; int i; for (i = ARRAYSIZE(_parts), part = _parts; i != 0; i--, part++) { if (!part->_player) return part; if (pri >= part->_pri_eff) { pri = part->_pri_eff; best = part; } } if (best) { best->uninit(); reallocateMidiChannels(midi); } else { debug(1, "Denying part request"); } return best; } int IMuseInternal::get_queue_sound_status(int sound) const { const uint16 *a; int i, j; j = _queue_pos; i = _queue_end; while (i != j) { a = _cmd_queue[i].array; if (a[0] == COMMAND_ID && a[1] == 8 && a[2] == (uint16)sound) return 2; i = (i + 1) % ARRAYSIZE(_cmd_queue); } for (i = 0; i < ARRAYSIZE (_deferredCommands); ++i) { if (_deferredCommands[i].time_left && _deferredCommands[i].a == 8 && _deferredCommands[i].b == sound) { return 2; } } return 0; } int IMuseInternal::set_volchan(int sound, int volchan) { int r; int i; int num; Player *player, *best, *sameid; r = get_volchan_entry(volchan); if (r == -1) return -1; if (r >= 8) { player = findActivePlayer(sound); if (player && player->_vol_chan != (uint16)volchan) { player->_vol_chan = volchan; player->setVolume(player->getVolume()); return 0; } return -1; } else { best = NULL; num = 0; sameid = NULL; for (i = ARRAYSIZE(_players), player = _players; i != 0; i--, player++) { if (player->isActive()) { if (player->_vol_chan == (uint16)volchan) { num++; if (!best || player->getPriority() <= best->getPriority()) best = player; } else if (player->getID() == (uint16)sound) { sameid = player; } } } if (sameid == NULL) return -1; if (num >= r) best->clear(); player->_vol_chan = volchan; player->setVolume(player->getVolume()); return 0; } } int IMuseInternal::clear_queue() { _queue_adding = false; _queue_cleared = true; _queue_pos = 0; _queue_end = 0; _trigger_count = 0; return 0; } int IMuseInternal::enqueue_command(int a, int b, int c, int d, int e, int f, int g) { uint16 *p; uint i; i = _queue_pos; if (i == _queue_end) return -1; if (a == -1) { _queue_adding = false; _trigger_count++; return 0; } p = _cmd_queue[_queue_pos].array; p[0] = COMMAND_ID; p[1] = a; p[2] = b; p[3] = c; p[4] = d; p[5] = e; p[6] = f; p[7] = g; i = (i + 1) % ARRAYSIZE(_cmd_queue); if (_queue_end != i) { _queue_pos = i; return 0; } else { _queue_pos = (i - 1) % ARRAYSIZE(_cmd_queue); return -1; } } int IMuseInternal::query_queue(int param) { switch (param) { case 0: // Get trigger count return _trigger_count; case 1: // Get trigger type if (_queue_end == _queue_pos) return -1; return _cmd_queue[_queue_end].array[1]; case 2: // Get trigger sound if (_queue_end == _queue_pos) return 0xFF; return _cmd_queue[_queue_end].array[2]; default: return -1; } } int IMuseInternal::setImuseMasterVolume(uint vol) { if (vol > 255) vol = 255; if (_master_volume == vol) return 0; _master_volume = vol; vol = _master_volume * _music_volume / 255; for (uint i = 0; i < ARRAYSIZE(_channel_volume); i++) { _channel_volume_eff[i] = _channel_volume[i] * vol / 255; } if (!_paused) update_volumes(); return 0; } int IMuseInternal::enqueue_trigger(int sound, int marker) { uint16 *p; uint pos; pos = _queue_pos; p = _cmd_queue[pos].array; p[0] = TRIGGER_ID; p[1] = sound; p[2] = marker; pos = (pos + 1) % ARRAYSIZE(_cmd_queue); if (_queue_end == pos) { _queue_pos = (pos - 1) % ARRAYSIZE(_cmd_queue); return -1; } _queue_pos = pos; _queue_adding = true; _queue_sound = sound; _queue_marker = marker; return 0; } int32 IMuseInternal::ImSetTrigger(int sound, int id, int a, int b, int c, int d, int e, int f, int g, int h) { // Sam & Max: ImSetTrigger. // Sets a trigger for a particular player and // marker ID, along with doCommand parameters // to invoke at the marker. The marker is // represented by MIDI SysEx block 00 xx(F7) // where "xx" is the marker ID. uint16 oldest_trigger = 0; ImTrigger *oldest_ptr = NULL; int i; ImTrigger *trig = _snm_triggers; for (i = ARRAYSIZE(_snm_triggers); i; --i, ++trig) { if (!trig->id) break; // We used to only compare 'id' and 'sound' here, but at least // at the Dino Bungie Memorial that causes the music to stop // after getting the T-Rex tooth. See bug #888161. if (trig->id == id && trig->sound == sound && trig->command[0] == a) break; uint16 diff; if (trig->expire <= _snm_trigger_index) diff = _snm_trigger_index - trig->expire; else diff = 0x10000 - trig->expire + _snm_trigger_index; if (!oldest_ptr || oldest_trigger < diff) { oldest_ptr = trig; oldest_trigger = diff; } } // If we didn't find a trigger, see if we can expire one. if (!i) { if (!oldest_ptr) return -1; trig = oldest_ptr; } trig->id = id; trig->sound = sound; trig->expire = (++_snm_trigger_index & 0xFFFF); trig->command[0] = a; trig->command[1] = b; trig->command[2] = c; trig->command[3] = d; trig->command[4] = e; trig->command[5] = f; trig->command[6] = g; trig->command[7] = h; // If the command is to start a sound, stop that sound if it's already playing. // This fixes some carnival music problems. // NOTE: We ONLY do this if the sound that will trigger the command is actually // playing. Otherwise, there's a problem when exiting and re-entering the // Bumpusville mansion. Ref Bug #780918. if (trig->command[0] == 8 && getSoundStatus_internal(trig->command[1],true) && getSoundStatus_internal(sound,true)) stopSound_internal(trig->command[1]); return 0; } int32 IMuseInternal::ImClearTrigger(int sound, int id) { int count = 0; int i; ImTrigger *trig = _snm_triggers; for (i = ARRAYSIZE(_snm_triggers); i; --i, ++trig) { if ((sound == -1 || trig->sound == sound) && trig->id && (id == -1 || trig->id == id)) { trig->sound = trig->id = 0; ++count; } } return (count > 0) ? 0 : -1; } int32 IMuseInternal::ImFireAllTriggers(int sound) { if (!sound) return 0; int count = 0; int i; for (i = 0; i < ARRAYSIZE(_snm_triggers); ++i) { if (_snm_triggers[i].sound == sound) { _snm_triggers[i].sound = _snm_triggers[i].id = 0; doCommand_internal(8, _snm_triggers[i].command); ++count; } } return (count > 0) ? 0 : -1; } int IMuseInternal::set_channel_volume(uint chan, uint vol) { if (chan >= 8 || vol > 127) return -1; _channel_volume[chan] = vol; _channel_volume_eff[chan] = _master_volume * _music_volume * vol / 255 / 255; update_volumes(); return 0; } void IMuseInternal::update_volumes() { Player *player; int i; for (i = ARRAYSIZE(_players), player = _players; i != 0; i--, player++) { if (player->isActive()) player->setVolume(player->getVolume()); } } int IMuseInternal::set_volchan_entry(uint a, uint b) { if (a >= 8) return -1; _volchan_table[a] = b; return 0; } int HookDatas::query_param(int param, byte chan) { switch (param) { case 18: return _jump[0]; case 19: return _transpose; case 20: return _part_onoff[chan]; case 21: return _part_volume[chan]; case 22: return _part_program[chan]; case 23: return _part_transpose[chan]; default: return -1; } } int HookDatas::set(byte cls, byte value, byte chan) { switch (cls) { case 0: if (value != _jump[0]) { _jump[1] = _jump[0]; _jump[0] = value; } break; case 1: _transpose = value; break; case 2: if (chan < 16) _part_onoff[chan] = value; else if (chan == 16) memset(_part_onoff, value, 16); break; case 3: if (chan < 16) _part_volume[chan] = value; else if (chan == 16) memset(_part_volume, value, 16); break; case 4: if (chan < 16) _part_program[chan] = value; else if (chan == 16) memset(_part_program, value, 16); break; case 5: if (chan < 16) _part_transpose[chan] = value; else if (chan == 16) memset(_part_transpose, value, 16); break; default: return -1; } return 0; } Player *IMuseInternal::findActivePlayer(int id) { int i; Player *player = _players; for (i = ARRAYSIZE(_players); i != 0; i--, player++) { if (player->isActive() && player->getID() == (uint16)id) return player; } return NULL; } int IMuseInternal::get_volchan_entry(uint a) { if (a < 8) return _volchan_table[a]; return -1; } IMuseInternal *IMuseInternal::create(OSystem *syst, MidiDriver *nativeMidiDriver, MidiDriver *adlibMidiDriver) { IMuseInternal *i = new IMuseInternal; i->initialize(syst, nativeMidiDriver, adlibMidiDriver); return i; } int IMuseInternal::initialize(OSystem *syst, MidiDriver *native_midi, MidiDriver *adlib_midi) { int i; _system = syst; _midi_native = native_midi; _midi_adlib = adlib_midi; if (native_midi != NULL) { _timer_info_native.imuse = this; _timer_info_native.driver = native_midi; initMidiDriver(&_timer_info_native); } if (adlib_midi != NULL) { _timer_info_adlib.imuse = this; _timer_info_adlib.driver = adlib_midi; initMidiDriver(&_timer_info_adlib); } if (!_tempoFactor) _tempoFactor = 100; _master_volume = 255; for (i = 0; i != 8; i++) _channel_volume[i] = _channel_volume_eff[i] = _volchan_table[i] = 127; init_players(); init_queue(); init_parts(); _initialized = true; return 0; } void IMuseInternal::initMidiDriver(TimerCallbackInfo *info) { // Open MIDI driver int result = info->driver->open(); if (result) error("IMuse initialization - %s", MidiDriver::getErrorName(result)); // Connect to the driver's timer info->driver->setTimerCallback(info, &IMuseInternal::midiTimerCallback); } void IMuseInternal::initMT32(MidiDriver *midi) { byte buffer[52]; char info[256] = "ScummVM "; int len; // Reset the MT-32 midi->sysEx((const byte *) "\x41\x10\x16\x12\x7f\x00\x00\x01\x00", 9); _system->delayMillis(250); // Setup master tune, reverb mode, reverb time, reverb level, // channel mapping, partial reserve and master volume midi->sysEx((const byte *) "\x41\x10\x16\x12\x10\x00\x00\x40\x00\x04\x04\x04\x04\x04\x04\x04\x04\x04\x04\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x64\x77", 31); _system->delayMillis(250); // Map percussion to notes 24 - 34 without reverb midi->sysEx((const byte *) "\x41\x10\x16\x12\x03\x01\x10\x40\x64\x07\x00\x4a\x64\x06\x00\x41\x64\x07\x00\x4b\x64\x08\x00\x45\x64\x06\x00\x44\x64\x0b\x00\x51\x64\x05\x00\x43\x64\x08\x00\x50\x64\x07\x00\x42\x64\x03\x00\x4c\x64\x07\x00\x44", 52); _system->delayMillis(250); // Compute version string (truncated to 20 chars max.) strcat(info, gScummVMVersion); len = strlen(info); if (len > 20) len = 20; // Display a welcome message on MT-32 displays. memcpy(&buffer[0], "\x41\x10\x16\x12\x20\x00\x00", 7); memcpy(&buffer[7], " ", 20); memcpy(buffer + 7 +(20 - len) / 2, info, len); byte checksum = 0; for (int i = 4; i < 27; ++i) checksum -= buffer[i]; buffer[27] = checksum & 0x7F; midi->sysEx(buffer, 28); _system->delayMillis(1000); } void IMuseInternal::initGM(MidiDriver *midi) { byte buffer[11]; int i; // General MIDI System On message // Resets all GM devices to default settings memcpy(&buffer[0], "\x7E\x7F\x09\x01", 4); midi->sysEx(buffer, 4); debug(2, "GM SysEx: GM System On"); _system->delayMillis(200); if (_enable_gs) { // All GS devices recognize the GS Reset command, // even with Roland's ID. It is impractical to // support other manufacturers' devices for // further GS settings, as there are limitless // numbers of them out there that would each // require individual SysEx commands with unique IDs. // Roland GS SysEx ID memcpy(&buffer[0], "\x41\x10\x42\x12", 4); // GS Reset memcpy(&buffer[4], "\x40\x00\x7F\x00\x41", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: GS Reset"); _system->delayMillis(200); if (_sc55) { // This mode is for GS devices that support an MT-32-compatible // Map, such as the Roland Sound Canvas line of modules. It // will allow them to work with True MT-32 mode, but will // obviously still ignore MT-32 SysEx (and thus custom // instruments). // Set Channels 1-16 to SC-55 Map, then CM-64/32L Variation for (i = 0; i < 16; ++i) { midi->send(( 127 << 16) | (0 << 8) | (0xB0 | i)); midi->send(( 1 << 16) | (32 << 8) | (0xB0 | i)); midi->send(( 0 << 16) | (0 << 8) | (0xC0 | i)); } debug(2, "GS Program Change: CM-64/32L Map Selected"); // Set Percussion Channel to SC-55 Map (CC#32, 01H), then // Switch Drum Map to CM-64/32L (MT-32 Compatible Drums) midi->getPercussionChannel()->controlChange(0, 0); midi->getPercussionChannel()->controlChange(32, 1); midi->send(127 << 8 | 0xC0 | 9); debug(2, "GS Program Change: Drum Map is CM-64/32L"); } // Set Master Chorus to 0. The MT-32 has no chorus capability. memcpy(&buffer[4], "\x40\x01\x3A\x00\x05", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: Master Chorus Level is 0"); // Set Channels 1-16 Reverb to 64, which is the // equivalent of MT-32 default Reverb Level 5 for (i = 0; i < 16; ++i) midi->send(( 64 << 16) | (91 << 8) | (0xB0 | i)); debug(2, "GM Controller 91 Change: Channels 1-16 Reverb Level is 64"); // Set Channels 1-16 Pitch Bend Sensitivity to // 12 semitones; then lock the RPN by setting null. for (i = 0; i < 16; ++i) { midi->setPitchBendRange(i, 12); } debug(2, "GM Controller 6 Change: Channels 1-16 Pitch Bend Sensitivity is 12 semitones"); // Set channels 1-16 Mod. LFO1 Pitch Depth to 4 memcpy(&buffer[4], "\x40\x20\x04\x04\x18", 5); for (i = 0; i < 16; ++i) { buffer[5] = 0x20 + i; buffer[8] = 0x18 - i; midi->sysEx(buffer, 9); } debug(2, "GS SysEx: Channels 1-16 Mod. LFO1 Pitch Depth Level is 4"); // Set Percussion Channel Expression to 80 midi->getPercussionChannel()->controlChange(11, 80); debug(2, "GM Controller 11 Change: Percussion Channel Expression Level is 80"); // Turn off Percussion Channel Rx. Expression so that // Expression cannot be modified. I don't know why, but // Roland does it this way. memcpy(&buffer[4], "\x40\x10\x0E\x00\x22", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: Percussion Channel Rx. Expression is OFF"); // Change Reverb Character to 0. I don't think this // sounds most like MT-32, but apparently Roland does. memcpy(&buffer[4], "\x40\x01\x31\x00\x0E", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: Reverb Character is 0"); // Change Reverb Pre-LF to 4, which is similar to // what MT-32 reverb does. memcpy(&buffer[4], "\x40\x01\x32\x04\x09", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: Reverb Pre-LF is 4"); // Change Reverb Time to 106; the decay on Hall 2 // Reverb is too fast compared to the MT-32's memcpy(&buffer[4], "\x40\x01\x34\x6A\x21", 5); midi->sysEx(buffer, 9); debug(2, "GS SysEx: Reverb Time is 106"); } } void IMuseInternal::init_queue() { _queue_adding = false; _queue_pos = 0; _queue_end = 0; _trigger_count = 0; } void IMuseInternal::handleDeferredCommands(MidiDriver *midi) { uint32 advance = midi->getBaseTempo(); DeferredCommand *ptr = &_deferredCommands[0]; int i; for (i = ARRAYSIZE(_deferredCommands); i; --i, ++ptr) { if (!ptr->time_left) continue; if (ptr->time_left <= advance) { doCommand_internal(ptr->a, ptr->b, ptr->c, ptr->d, ptr->e, ptr->f, 0, 0); ptr->time_left = advance; } ptr->time_left -= advance; } } // "time" is interpreted as hundredths of a second. // FIXME: Is that correct? // We convert it to microseconds before prceeding void IMuseInternal::addDeferredCommand(int time, int a, int b, int c, int d, int e, int f) { DeferredCommand *ptr = &_deferredCommands[0]; int i; for (i = ARRAYSIZE(_deferredCommands); i; --i, ++ptr) { if (!ptr->time_left) break; } if (i) { ptr->time_left = time * 10000; ptr->a = a; ptr->b = b; ptr->c = c; ptr->d = d; ptr->e = e; ptr->f = f; } } void IMuseInternal::fix_parts_after_load() { Part *part; int i; for (i = ARRAYSIZE(_parts), part = _parts; i != 0; i--, part++) { if (part->_player) part->fix_after_load(); } } // Only call this routine from the main thread, // since it uses getResourceAddress void IMuseInternal::fix_players_after_load(ScummEngine *scumm) { Player *player = _players; int i; for (i = ARRAYSIZE(_players); i != 0; i--, player++) { if (player->isActive()) { scumm->getResourceAddress(rtSound, player->getID()); player->fixAfterLoad(); } } } //////////////////////////////////////// // // Some more IMuseInternal stuff // //////////////////////////////////////// void IMuseInternal::midiTimerCallback(void *data) { TimerCallbackInfo *info = (TimerCallbackInfo *)data; info->imuse->on_timer(info->driver); } void IMuseInternal::reallocateMidiChannels(MidiDriver *midi) { Part *part, *hipart; int i; byte hipri, lopri; Part *lopart; while (true) { hipri = 0; hipart = NULL; for (i = 32, part = _parts; i; i--, part++) { if (part->_player && part->_player->getMidiDriver() == midi && !part->_percussion && part->_on && !part->_mc && part->_pri_eff >= hipri) { hipri = part->_pri_eff; hipart = part; } } if (!hipart) return; if ((hipart->_mc = midi->allocateChannel()) == NULL) { lopri = 255; lopart = NULL; for (i = 32, part = _parts; i; i--, part++) { if (part->_mc && part->_mc->device() == midi && part->_pri_eff <= lopri) { lopri = part->_pri_eff; lopart = part; } } if (lopart == NULL || lopri >= hipri) return; lopart->off(); if ((hipart->_mc = midi->allocateChannel()) == NULL) return; } hipart->sendAll(); } } void IMuseInternal::setGlobalAdLibInstrument(byte slot, byte *data) { if (slot < 32) { _global_adlib_instruments[slot].adlib(data); } } void IMuseInternal::copyGlobalAdLibInstrument(byte slot, Instrument *dest) { if (slot >= 32) return; _global_adlib_instruments[slot].copy_to(dest); } /** * IMuseInternal factory creation method. * This method provides a means for creating an IMuse * implementation without requiring that the details * of that implementation be exposed to the client * through a header file. This allows the internals * of the implementation to be changed and updated * without requiring a recompile of the client code. */ IMuse *IMuse::create(OSystem *syst, MidiDriver *nativeMidiDriver, MidiDriver *adlibMidiDriver) { IMuseInternal *engine = IMuseInternal::create(syst, nativeMidiDriver, adlibMidiDriver); return engine; } } // End of namespace Scumm