diff options
author | Max Horn | 2006-02-20 20:57:26 +0000 |
---|---|---|
committer | Max Horn | 2006-02-20 20:57:26 +0000 |
commit | 81e8a2860ee066d6b863bf80bb99a592dec4cbe0 (patch) | |
tree | 3b6cd7038ccdb1767717ea2fc66f53202089f198 /engines/scumm/imuse/imuse.cpp | |
parent | 9cec516e514ac76e7a3e69fdec16e79a2486c85f (diff) | |
download | scummvm-rg350-81e8a2860ee066d6b863bf80bb99a592dec4cbe0.tar.gz scummvm-rg350-81e8a2860ee066d6b863bf80bb99a592dec4cbe0.tar.bz2 scummvm-rg350-81e8a2860ee066d6b863bf80bb99a592dec4cbe0.zip |
Moved iMUSE code to the new directory engines/scumm/imuse/
svn-id: r20801
Diffstat (limited to 'engines/scumm/imuse/imuse.cpp')
-rw-r--r-- | engines/scumm/imuse/imuse.cpp | 2043 |
1 files changed, 2043 insertions, 0 deletions
diff --git a/engines/scumm/imuse/imuse.cpp b/engines/scumm/imuse/imuse.cpp new file mode 100644 index 0000000000..41e4a1aa96 --- /dev/null +++ b/engines/scumm/imuse/imuse.cpp @@ -0,0 +1,2043 @@ +/* ScummVM - Scumm Interpreter + * Copyright (C) 2001 Ludvig Strigeus + * Copyright (C) 2001-2006 The ScummVM project + * + * 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. + * + * $URL$ + * $Id$ + * + */ + +#include "common/stdafx.h" + +#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" +#include "scumm/util.h" + +#include "sound/mididrv.h" + + +namespace Scumm { + +//////////////////////////////////////// +// +// IMuseInternal implementation +// +//////////////////////////////////////// + +IMuseInternal::IMuseInternal() : +_native_mt32(false), +_enable_gs(false), +_sc55(false), +_midi_adlib(0), +_midi_native(0), +_base_sounds(0), +_paused(false), +_initialized(false), +_tempoFactor(0), +_player_limit(ARRAYSIZE(_players)), +_recycle_players(false), +_direct_passthrough(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)); +} + +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 = *(((uint32 *)ptr) + 1); + switch (tag) { + case MKID('ADL '): + case MKID('ASFX'): // Special AD class for old Adlib sound effects + case MKID('SPK '): + return false; + + case MKID('AMI '): + case MKID('ROL '): + return true; + + case MKID('MAC '): // Occurs in the Mac version of FOA and MI2 + return true; + + case MKID('GMD '): + case MKID('MIDI'): // Occurs in Sam & Max + 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: '%s'", tag2str(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 = *(((uint32 *)ptr) + 1); + switch (tag) { + case MKID('ADL '): + case MKID('ASFX'): // Special AD class for old Adlib sound effects + case MKID('SPK '): + return false; + + case MKID('AMI '): + case MKID('ROL '): + return true; + + case MKID('MAC '): // Occurs in the Mac version of FOA and MI2 + return true; + + case MKID('GMD '): + case MKID('MIDI'): // 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: '%s'", tag2str(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; +} + +bool IMuseInternal::startSound(int sound) { + Player *player; + void *ptr; + + // 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(trigger->sound)) + return false; + } + + 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 = findActivePlayer(sound); + if (!player) + player = allocate_player(128); + if (!player) + return false; + + // HACK: 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 (g_scumm->_game.id == GID_SAMNMAX && sound == 82 && getSoundStatus(81, false)) + ImClearTrigger(81, 1); + + player->clear(); + return player->startSound(sound, driver, _direct_passthrough); +} + + +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; + } +} + +int IMuseInternal::stopSound(int sound) { + int r = -1; + Player *player = findActivePlayer(sound); + if (player) { + player->clear(); + r = 0; + } + return r; +} + +int IMuseInternal::stopAllSounds() { + Player *player = _players; + int i; + + for (i = ARRAYSIZE(_players); i != 0; i--, player++) { + if (player->isActive()) + player->clear(); + } + return 0; +} + +void IMuseInternal::on_timer(MidiDriver *midi) { + if (_paused || !_initialized) + return; + + if (midi == _midi_native || !_midi_native) + handleDeferredCommands(midi); + sequencer_timers(midi); +} + +int IMuseInternal::getMusicTimer() const { + int best_time = 0; + const Player *player = _players; + int i; + + for (i = ARRAYSIZE(_players); i != 0; i--, player++) { + if (player->isActive()) { + int timer = player->getMusicTimer(); + if (timer > best_time) + best_time = timer; + } + } + return best_time; +} + +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(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::getSoundStatus(int sound, bool ignoreFadeouts) const { + int i; + const Player *player = _players; + + for (i = ARRAYSIZE(_players); i != 0; 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); +} + +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::setMusicVolume(uint vol) { + if (vol > 255) + vol = 255; + if (_music_volume == vol) + return 0; + _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(); + return 0; +} + +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::terminate1() { + _initialized = false; + stopAllSounds(); + return 0; +} + +// This is the stuff that has to be done +// outside the monitor's mutex, otherwise +// a deadlock occurs. +int IMuseInternal::terminate2() { + if (_midi_adlib) { + _midi_adlib->close(); + delete _midi_adlib; + _midi_adlib = 0; + } + + if (_midi_native) { + _midi_native->close(); + delete _midi_native; + _midi_native = 0; + } + + 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::doCommand(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(8, args); +} + +int32 IMuseInternal::doCommand(int numargs, int a[]) { + int i; + + if (numargs < 1) + return -1; + byte cmd = a[0] & 0xFF; + byte param = a[0] >> 8; + Player *player = NULL; + + if (!_initialized && (cmd || param)) + return -1; + +#ifdef IMUSE_DEBUG + { + char string[128]; + sprintf(string, "doCommand - %d (%d/%d)", a[0], (int)param, (int)cmd); + for (i = 1; i < numargs; ++i) + sprintf(string + strlen(string), ", %d", a[i]); + debug(0, string); + } +#endif + + 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(a[1]) ? 0 : -1; + case 9: + return stopSound(a[1]); + case 10: // FIXME: Sam and Max - Not sure if this is correct + return stopAllSounds(); + case 11: + return stopAllSounds(); + case 12: + // Sam & Max: Player-scope commands + player = findActivePlayer(a[1]); + if (!player) + 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(a[1]); + 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 (g_scumm->_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 (g_scumm->_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 (g_scumm->_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 (g_scumm->_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: + 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; +} + +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(trig->command[1]) && getSoundStatus(sound)) + stopSound(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(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; +} + +uint32 IMuseInternal::property(int prop, uint32 value) { + 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_DIRECT_PASSTHROUGH: + _direct_passthrough = (value != 0); + break; + } + + return 0; +} + +void IMuseInternal::setBase(byte **base) { + _base_sounds = base; +} + +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; + + _midi_native = native_midi; + _midi_adlib = adlib_midi; + if (native_midi != NULL) + initMidiDriver(native_midi); + if (adlib_midi != NULL) + initMidiDriver(adlib_midi); + + 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(MidiDriver *midi) { + // Open MIDI driver + int result = midi->open(); + if (result) + error("IMuse initialization - %s", MidiDriver::getErrorName(result)); + + // Connect to the driver's timer + midi->setTimerCallback(midi, &IMuseInternal::midiTimerCallback); +} + +void IMuseInternal::initMT32(MidiDriver *midi) { + byte buffer[52]; + char info[256] = "ScummVM "; + int len; + + // Reset the MT-32 + memcpy(&buffer[0], "\x41\x10\x16\x12\x7f\x00\x00\x01\x00", 9); + midi->sysEx(buffer, 9); + g_system->delayMillis(100); + + // 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[4], "\x20\x00\x00", 3); + 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); + g_system->delayMillis(500); + + // Setup master tune, reverb mode, reverb time, reverb level, + // channel mapping, partial reserve and master volume + memcpy(&buffer[4], "\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", 27); + midi->sysEx(buffer, 31); + g_system->delayMillis(250); + + // Map percussion to notes 24 - 34 without reverb + memcpy(&buffer[4], "\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", 48); + midi->sysEx(buffer, 52); + g_system->delayMillis(250); +} + +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], "\xF0\x7E\x7F\x09\x01\xF7", 6); + midi->sysEx(buffer, 6); + debug(2, "GM SysEx: GM System On"); + g_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], "\xF0\x41\x10\x42\x12", 5); + + // GS Reset + memcpy(&buffer[5], "\x40\x00\x7F\x00\x41\xF7", 6); + midi->sysEx(buffer, 11); + debug(2, "GS SysEx: GS Reset"); + g_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[5], "\x40\x01\x3A\x00\x05\xF7", 6); + midi->sysEx(buffer, 11); + 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->send(( 0 << 16) | (100 << 8) | (0xB0 | i)); + midi->send(( 0 << 16) | (101 << 8) | (0xB0 | i)); + midi->send(( 12 << 16) | (6 << 8) | (0xB0 | i)); + midi->send(( 0 << 16) | (38 << 8) | (0xB0 | i)); + midi->send(( 127 << 16) | (100 << 8) | (0xB0 | i)); + midi->send(( 127 << 16) | (101 << 8) | (0xB0 | i)); + } + 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[5], "\x40\x20\x04\x04\x18\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x21\x04\x04\x17\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x22\x04\x04\x16\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x23\x04\x04\x15\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x24\x04\x04\x14\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x25\x04\x04\x13\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x26\x04\x04\x12\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x27\x04\x04\x11\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x28\x04\x04\x10\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x29\x04\x04\x0F\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2A\x04\x04\x0E\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2B\x04\x04\x0D\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2C\x04\x04\x0C\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2D\x04\x04\x0B\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2E\x04\x04\x0A\xF7", 6); + midi->sysEx(buffer, 11); + memcpy(&buffer[5], "\x40\x2F\x04\x04\x09\xF7", 6); + midi->sysEx(buffer, 11); + 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[5], "\x40\x10\x0E\x00\x22\xF7", 6); + midi->sysEx(buffer, 11); + 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[5], "\x40\x01\x31\x00\x0E\xF7", 6); + midi->sysEx(buffer, 11); + 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[5], "\x40\x01\x32\x04\x09\xF7", 6); + midi->sysEx(buffer, 11); + 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[5], "\x40\x01\x34\x6A\x21\xF7", 6); + midi->sysEx(buffer, 11); + 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::pause(bool paused) { + 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; +} + +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(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; + } +} + +//////////////////////////////////////////////////////////// +// +// IMuseInternal load/save implementation +// +//////////////////////////////////////////////////////////// + +int IMuseInternal::save_or_load(Serializer *ser, ScummEngine *scumm) { + 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; +} + +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(); + } + } +} + +Part::Part() { + _slot = 0; + _next = 0; + _prev = 0; + _mc = 0; + _player = 0; + _pitchbend = 0; + _pitchbend_factor = 0; + _transpose = 0; + _transpose_eff = 0; + _vol = 0; + _vol_eff = 0; + _detune = 0; + _detune_eff = 0; + _pan = 0; + _pan_eff = 0; + _on = false; + _modwheel = 0; + _pedal = false; + _pri = 0; + _pri_eff = 0; + _chan = 0; + _effect_level = 0; + _chorus = 0; + _percussion = 0; + _bank = 0; + _unassigned_instrument = false; +} + +void Part::saveLoadWithSerializer(Serializer *ser) { + const SaveLoadEntry partEntries[] = { + MKLINE(Part, _pitchbend, sleInt16, VER(8)), + MKLINE(Part, _pitchbend_factor, sleUint8, VER(8)), + MKLINE(Part, _transpose, sleInt8, VER(8)), + MKLINE(Part, _vol, sleUint8, VER(8)), + MKLINE(Part, _detune, sleInt8, VER(8)), + MKLINE(Part, _pan, sleInt8, VER(8)), + MKLINE(Part, _on, sleUint8, VER(8)), + MKLINE(Part, _modwheel, sleUint8, VER(8)), + MKLINE(Part, _pedal, sleUint8, VER(8)), + MK_OBSOLETE(Part, _program, sleUint8, VER(8), VER(16)), + MKLINE(Part, _pri, sleUint8, VER(8)), + MKLINE(Part, _chan, sleUint8, VER(8)), + MKLINE(Part, _effect_level, sleUint8, VER(8)), + MKLINE(Part, _chorus, sleUint8, VER(8)), + MKLINE(Part, _percussion, sleUint8, VER(8)), + MKLINE(Part, _bank, sleUint8, VER(8)), + MKEND() + }; + + int num; + if (ser->isSaving()) { + num = (_next ? (_next - _se->_parts + 1) : 0); + ser->saveUint16(num); + + num = (_prev ? (_prev - _se->_parts + 1) : 0); + ser->saveUint16(num); + + num = (_player ? (_player - _se->_players + 1) : 0); + ser->saveUint16(num); + } else { + num = ser->loadUint16(); + _next = (num ? &_se->_parts[num - 1] : 0); + + num = ser->loadUint16(); + _prev = (num ? &_se->_parts[num - 1] : 0); + + num = ser->loadUint16(); + _player = (num ? &_se->_players[num - 1] : 0); + } + ser->saveLoadEntries(this, partEntries); +} + +void Part::set_detune(int8 detune) { + _detune_eff = clamp((_detune = detune) + _player->getDetune(), -128, 127); + if (_mc) + sendPitchBend(); +} + +void Part::pitchBend(int16 value) { + _pitchbend = value; + if (_mc) + sendPitchBend(); +} + +void Part::volume(byte value) { + _vol_eff = ((_vol = value) + 1) * _player->getEffectiveVolume() >> 7; + if (_mc) + _mc->volume(_vol_eff); +} + +void Part::set_pri(int8 pri) { + _pri_eff = clamp((_pri = pri) + _player->getPriority(), 0, 255); + if (_mc) + _mc->priority(_pri_eff); +} + +void Part::set_pan(int8 pan) { + _pan_eff = clamp((_pan = pan) + _player->getPan(), -64, 63); + if (_mc) + _mc->panPosition(_pan_eff + 0x40); +} + +void Part::set_transpose(int8 transpose) { + _transpose_eff = transpose_clamp((_transpose = transpose) + _player->getTranspose(), -24, 24); + if (_mc) + sendPitchBend(); +} + +void Part::sustain(bool value) { + _pedal = value; + if (_mc) + _mc->sustain(value); +} + +void Part::modulationWheel(byte value) { + _modwheel = value; + if (_mc) + _mc->modulationWheel(value); +} + +void Part::chorusLevel(byte value) { + _chorus = value; + if (_mc) + _mc->chorusLevel(value); +} + +void Part::effectLevel(byte value) +{ + _effect_level = value; + if (_mc) + _mc->effectLevel(value); +} + +void Part::fix_after_load() { + set_transpose(_transpose); + volume(_vol); + set_detune(_detune); + set_pri(_pri); + set_pan(_pan); + sendAll(); +} + +void Part::pitchBendFactor(byte value) { + if (value > 12) + return; + pitchBend(0); + _pitchbend_factor = value; + if (_mc) + _mc->pitchBendFactor(value); +} + +void Part::set_onoff(bool on) { + if (_on != on) { + _on = on; + if (!on) + off(); + if (!_percussion) + _player->_se->reallocateMidiChannels(_player->getMidiDriver()); + } +} + +void Part::set_instrument(byte * data) { + _instrument.adlib(data); + if (clearToTransmit()) + _instrument.send(_mc); +} + +void Part::load_global_instrument(byte slot) { + _player->_se->copyGlobalAdlibInstrument(slot, &_instrument); + if (clearToTransmit()) + _instrument.send(_mc); +} + +void Part::noteOn(byte note, byte velocity) { + if (!_on) + return; + + MidiChannel *mc = _mc; + + // DEBUG + if (_unassigned_instrument && !_percussion) { + _unassigned_instrument = false; + if (!_instrument.isValid()) { + debug(0, "[%02d] No instrument specified", (int)_chan); + return; + } + } + + if (mc && _instrument.isValid()) { + mc->noteOn(note, velocity); + } else if (_percussion) { + mc = _player->getMidiDriver()->getPercussionChannel(); + if (!mc) + return; + static byte prev_vol_eff = 128; + if (_vol_eff != prev_vol_eff){ + mc->volume(_vol_eff); + prev_vol_eff = _vol_eff; + } + if ((note < 35) && (!_player->_se->isNativeMT32())) + note = Instrument::_gmRhythmMap[note]; + + mc->noteOn(note, velocity); + } +} + +void Part::noteOff(byte note) { + if (!_on) + return; + + MidiChannel *mc = _mc; + if (mc) { + mc->noteOff(note); + } else if (_percussion) { + mc = _player->getMidiDriver()->getPercussionChannel(); + if (mc) + mc->noteOff(note); + } +} + +void Part::init() { + _player = NULL; + _next = NULL; + _prev = NULL; + _mc = NULL; +} + +void Part::setup(Player *player) { + _player = player; + + _percussion = (player->isMIDI() && _chan == 9); // true; + _on = true; + _pri_eff = player->getPriority(); + _pri = 0; + _vol = 127; + _vol_eff = player->getEffectiveVolume(); + _pan = clamp(player->getPan(), -64, 63); + _transpose_eff = player->getTranspose(); + _transpose = 0; + _detune = 0; + _detune_eff = player->getDetune(); + _pitchbend_factor = 2; + _pitchbend = 0; + _effect_level = 64; + _instrument.clear(); + _unassigned_instrument = true; + _chorus = 0; + _modwheel = 0; + _bank = 0; + _pedal = false; + _mc = NULL; +} + +void Part::uninit() { + if (!_player) + return; + off(); + _player->removePart(this); + _player = NULL; +} + +void Part::off() { + if (_mc) { + _mc->allNotesOff(); + _mc->release(); + _mc = NULL; + } +} + +bool Part::clearToTransmit() { + if (_mc) + return true; + if (_instrument.isValid()) + _player->_se->reallocateMidiChannels(_player->getMidiDriver()); + return false; +} + +void Part::sendAll() { + if (!clearToTransmit()) + return; + _mc->pitchBendFactor(_pitchbend_factor); + sendPitchBend(); + _mc->volume(_vol_eff); + _mc->sustain(_pedal); + _mc->modulationWheel(_modwheel); + _mc->panPosition(_pan_eff + 0x40); + _mc->effectLevel(_effect_level); + if (_instrument.isValid()) + _instrument.send(_mc); + _mc->chorusLevel(_chorus); + _mc->priority(_pri_eff); +} + +void Part::sendPitchBend() { + int16 bend = _pitchbend; + // RPN-based pitchbend range doesn't work for the MT32, + // so we'll do the scaling ourselves. + if (_player->_se->isNativeMT32()) + bend = bend * _pitchbend_factor / 12; + _mc->pitchBend(clamp(bend + (_detune_eff * 64 / 12) + (_transpose_eff * 8192 / 12), -8192, 8191)); +} + +void Part::programChange(byte value) { + _bank = 0; + _instrument.program(value, _player->isMT32()); + if (clearToTransmit()) + _instrument.send(_mc); +} + +void Part::set_instrument(uint b) { + _bank = (byte)(b >> 8); + if (_bank) + error("Non-zero instrument bank selection. Please report this"); + _instrument.program((byte)b, _player->isMT32()); + if (clearToTransmit()) + _instrument.send(_mc); +} + +void Part::allNotesOff() { + if (!_mc) + return; + _mc->allNotesOff(); +} + +//////////////////////////////////////// +// +// Some more IMuseInternal stuff +// +//////////////////////////////////////// + +void IMuseInternal::midiTimerCallback(void *data) { + MidiDriver *driver = (MidiDriver *)data; + if (g_scumm->_imuse) + g_scumm->_imuse->on_timer(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); +} + +//////////////////////////////////////////////////////////// +// +// IMuse implementation +// +// IMuse actually serves as a concurency monitor front-end +// to IMuseInternal and ensures that only one thread +// accesses the object at a time. This is necessary to +// prevent scripts and the MIDI parser from yanking objects +// out from underneath each other. +// +//////////////////////////////////////////////////////////// + +IMuse::IMuse(OSystem *system, IMuseInternal *target) + : _system(system), _target(target) { + _mutex = system->createMutex(); +} + +IMuse::~IMuse() { + if (_mutex) + _system->deleteMutex(_mutex); + if (_target) + delete _target; +} + +inline void IMuse::in() const { + _system->lockMutex(_mutex); +} +inline void IMuse::out() const { + _system->unlockMutex(_mutex); +} + +void IMuse::on_timer(MidiDriver *midi) { in(); _target->on_timer(midi); out(); } +void IMuse::pause(bool paused) { in(); _target->pause(paused); out(); } +int IMuse::save_or_load(Serializer *ser, ScummEngine *scumm) { in(); int ret = _target->save_or_load(ser, scumm); out(); return ret; } +void IMuse::setMusicVolume(int vol) { in(); _target->setMusicVolume(vol); out(); } +void IMuse::startSound(int sound) { in(); _target->startSound(sound); out(); } +void IMuse::stopSound(int sound) { in(); _target->stopSound(sound); out(); } +void IMuse::stopAllSounds() { in(); _target->stopAllSounds(); out(); } +int IMuse::getSoundStatus(int sound) const { in(); int ret = _target->getSoundStatus(sound, true); out(); return ret; } +bool IMuse::get_sound_active(int sound) const { in(); bool ret = _target->getSoundStatus(sound, false) ? 1 : 0; out(); return ret; } +int IMuse::getMusicTimer() const { in(); int ret = _target->getMusicTimer(); out(); return ret; } +int32 IMuse::doCommand(int a, int b, int c, int d, int e, int f, int g, int h) { in(); int32 ret = _target->doCommand(a,b,c,d,e,f,g,h); out(); return ret; } +int32 IMuse::doCommand(int numargs, int args[]) { in(); int32 ret = _target->doCommand(numargs, args); out(); return ret; } +int IMuse::clear_queue() { in(); int ret = _target->clear_queue(); out(); return ret; } +void IMuse::setBase(byte **base) { in(); _target->setBase(base); out(); } +uint32 IMuse::property(int prop, uint32 value) { in(); uint32 ret = _target->property(prop, value); out(); return ret; } +void IMuse::terminate() { in(); _target->terminate1(); out(); _target->terminate2(); } + +// The IMuse::create method provides a front-end factory +// for creating IMuseInternal without exposing that class +// to the client. +IMuse *IMuse::create(OSystem *syst, MidiDriver *nativeMidiDriver, MidiDriver *adlibMidiDriver) { + IMuseInternal *engine = IMuseInternal::create(syst, nativeMidiDriver, adlibMidiDriver); + return new IMuse(syst, engine); +} + +} // End of namespace Scumm |