aboutsummaryrefslogtreecommitdiff
path: root/engines/sherlock/music.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engines/sherlock/music.cpp')
-rw-r--r--engines/sherlock/music.cpp598
1 files changed, 598 insertions, 0 deletions
diff --git a/engines/sherlock/music.cpp b/engines/sherlock/music.cpp
new file mode 100644
index 0000000000..49c48126f4
--- /dev/null
+++ b/engines/sherlock/music.cpp
@@ -0,0 +1,598 @@
+/* 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 "common/config-manager.h"
+#include "common/mutex.h"
+#include "sherlock/sherlock.h"
+#include "sherlock/music.h"
+#include "sherlock/scalpel/drivers/mididriver.h"
+// for Miles Audio (Sherlock Holmes 2)
+#include "audio/miles.h"
+// for 3DO digital music
+#include "audio/decoders/aiff.h"
+
+namespace Sherlock {
+
+#define NUM_SONGS 45
+
+/* This tells which song to play in each room, 0 = no song played */
+static const char ROOM_SONG[62] = {
+ 0, 20, 43, 6, 11, 2, 8, 15, 6, 28,
+ 6, 38, 7, 32, 16, 5, 8, 41, 9, 22,
+ 10, 23, 4, 39, 19, 24, 13, 27, 0, 30,
+ 3, 21, 26, 25, 16, 29, 1, 1, 18, 12,
+ 1, 17, 17, 31, 17, 34, 36, 7, 20, 20,
+ 33, 8, 44, 40, 42, 35, 0, 0, 0, 12,
+ 12
+};
+
+static const char *const SONG_NAMES[NUM_SONGS] = {
+ "SINGERF", "CHEMIST", "TOBAC", "EQUEST", "MORTUARY", "DOCKS", "LSTUDY",
+ "LORD", "BOY", "PERFUM1", "BAKER1", "BAKER2", "OPERA1", "HOLMES",
+ "FFLAT", "OP1FLAT", "ZOO", "SROOM", "FLOWERS", "YARD", "TAXID",
+ "PUB1", "VICTIM", "RUGBY", "DORM", "SHERMAN", "LAWYER", "THEATRE",
+ "DETECT", "OPERA4", "POOL", "SOOTH", "ANNA1", "ANNA2", "PROLOG3",
+ "PAWNSHOP", "MUSICBOX", "MOZART1", "ROBHUNT", "PANCRAS1", "PANCRAS2", "LORDKILL",
+ "BLACKWEL", "RESCUE", "MAP"
+};
+
+MidiParser_SH::MidiParser_SH() {
+ _ppqn = 1;
+ setTempo(16667);
+ _data = nullptr;
+ _beats = 0;
+ _lastEvent = 0;
+ _trackEnd = nullptr;
+
+ _musData = nullptr;
+ _musDataSize = 0;
+}
+
+MidiParser_SH::~MidiParser_SH() {
+ Common::StackLock lock(_mutex);
+ unloadMusic();
+ _driver = NULL;
+}
+
+void MidiParser_SH::parseNextEvent(EventInfo &info) {
+ Common::StackLock lock(_mutex);
+
+// warning("parseNextEvent");
+
+ // there is no delta right at the start of the music data
+ // this order is essential, otherwise notes will get delayed or even go missing
+ if (_position._playPos != _tracks[0]) {
+ info.delta = *(_position._playPos++);
+ } else {
+ info.delta = 0;
+ }
+
+ info.start = _position._playPos;
+
+ info.event = *_position._playPos++;
+ //warning("Event %x", info.event);
+ _position._runningStatus = info.event;
+
+ switch (info.command()) {
+ case 0xC: { // program change
+ int idx = *_position._playPos++;
+ info.basic.param1 = idx & 0x7f;
+ info.basic.param2 = 0;
+ }
+ break;
+ case 0xD:
+ info.basic.param1 = *_position._playPos++;
+ info.basic.param2 = 0;
+ break;
+
+ case 0xB:
+ info.basic.param1 = *_position._playPos++;
+ info.basic.param2 = *_position._playPos++;
+ info.length = 0;
+ break;
+
+ case 0x8:
+ case 0x9:
+ case 0xA:
+ case 0xE:
+ info.basic.param1 = *(_position._playPos++);
+ info.basic.param2 = *(_position._playPos++);
+ if (info.command() == 0x9 && info.basic.param2 == 0) {
+ // NoteOn with param2==0 is a NoteOff
+ info.event = info.channel() | 0x80;
+ }
+ info.length = 0;
+ break;
+ case 0xF:
+ if (info.event == 0xFF) {
+ error("SysEx META event 0xFF");
+
+ byte type = *(_position._playPos++);
+ switch(type) {
+ case 0x2F:
+ // End of Track
+ allNotesOff();
+ stopPlaying();
+ unloadMusic();
+ return;
+ case 0x51:
+ warning("TODO: 0xFF / 0x51");
+ return;
+ default:
+ warning("TODO: 0xFF / %x Unknown", type);
+ break;
+ }
+ } else if (info.event == 0xFC) {
+ // Official End-Of-Track signal
+ debugC(kDebugLevelMusic, "Music: System META event 0xFC");
+
+ byte type = *(_position._playPos++);
+ switch (type) {
+ case 0x80: // end of track, triggers looping
+ debugC(kDebugLevelMusic, "Music: META event triggered looping");
+ jumpToTick(0, true, true, false);
+ break;
+ case 0x81: // end of track, stop playing
+ debugC(kDebugLevelMusic, "Music: META event triggered music stop");
+ stopPlaying();
+ unloadMusic();
+ break;
+ default:
+ error("MidiParser_SH::parseNextEvent: Unknown META event 0xFC type %x", type);
+ break;
+ }
+ } else {
+ warning("TODO: %x / Unknown", info.event);
+ break;
+ }
+ break;
+ default:
+ warning("MidiParser_SH::parseNextEvent: Unsupported event code %x", info.event);
+ break;
+ }// switch (info.command())
+}
+
+bool MidiParser_SH::loadMusic(byte *musData, uint32 musDataSize) {
+ Common::StackLock lock(_mutex);
+
+ debugC(kDebugLevelMusic, "Music: loadMusic()");
+ unloadMusic();
+
+ _musData = musData;
+ _musDataSize = musDataSize;
+
+ byte *headerPtr = _musData + 12; // skip over the already checked SPACE header
+ byte *pos = headerPtr;
+
+ uint16 headerSize = READ_LE_UINT16(headerPtr);
+ assert(headerSize == 0x7F); // Security check
+
+ // Skip over header
+ pos += headerSize;
+
+ _lastEvent = 0;
+ _trackEnd = _musData + _musDataSize;
+
+ _numTracks = 1;
+ _tracks[0] = pos;
+
+ _ppqn = 1;
+ setTempo(16667);
+ setTrack(0);
+
+ return true;
+}
+
+void MidiParser_SH::unloadMusic() {
+ Common::StackLock lock(_mutex);
+
+ if (_musData) {
+ delete[] _musData;
+ _musData = NULL;
+ _musDataSize = 0;
+ }
+
+ MidiParser::unloadMusic();
+}
+
+/*----------------------------------------------------------------*/
+
+Music::Music(SherlockEngine *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) {
+ _midiDriver = NULL;
+ _midiParser = NULL;
+ _musicType = MT_NULL;
+ _musicPlaying = false;
+ _musicOn = false;
+ _midiOption = false;
+ _musicVolume = 0;
+
+ if (IS_3DO) {
+ // 3DO - uses digital samples for music
+ _musicOn = true;
+ return;
+ }
+
+ if (_vm->_interactiveFl)
+ _vm->_res->addToCache("MUSIC.LIB");
+
+ MidiDriver::DeviceHandle dev;
+
+ if (IS_SERRATED_SCALPEL) {
+ // Serrated Scalpel: used an internal Electronic Arts .MUS music engine
+ _midiParser = new MidiParser_SH();
+ dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_MT32);
+ _musicType = MidiDriver::getMusicType(dev);
+
+ switch (_musicType) {
+ case MT_ADLIB:
+ _midiDriver = MidiDriver_SH_AdLib_create();
+ break;
+ case MT_MT32:
+ _midiDriver = MidiDriver_MT32_create();
+ break;
+ case MT_GM:
+ if (ConfMan.getBool("native_mt32")) {
+ _midiDriver = MidiDriver_MT32_create();
+ _musicType = MT_MT32;
+ }
+ break;
+ default:
+ // Create default one
+ // I guess we shouldn't do this anymore
+ //_midiDriver = MidiDriver::createMidi(dev);
+ break;
+ }
+ } else {
+ // Rose Tattooo: seems to use Miles Audio 3
+ _midiParser = MidiParser::createParser_XMIDI();
+ dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
+ _musicType = MidiDriver::getMusicType(dev);
+
+ switch (_musicType) {
+ case MT_ADLIB:
+ // SAMPLE.AD -> regular AdLib instrument data
+ // SAMPLE.OPL -> OPL-3 instrument data
+ // although in case of Rose Tattoo both files are exactly the same
+ _midiDriver = Audio::MidiDriver_Miles_AdLib_create("SAMPLE.AD", "SAMPLE.OPL");
+ break;
+ case MT_MT32:
+ // Sherlock Holmes 2 does not have a MT32 timbre file
+ _midiDriver = Audio::MidiDriver_Miles_MT32_create("");
+ break;
+ case MT_GM:
+ if (ConfMan.getBool("native_mt32")) {
+ _midiDriver = Audio::MidiDriver_Miles_MT32_create("");
+ _musicType = MT_MT32;
+ } else {
+ _midiDriver = MidiDriver::createMidi(dev);
+ _musicType = MT_GM;
+ }
+ break;
+ default:
+ // Do not create anything
+ break;
+ }
+ }
+
+ if (_midiDriver) {
+ int ret = _midiDriver->open();
+ if (ret == 0) {
+ // Reset is done inside our MIDI driver
+ _midiDriver->setTimerCallback(_midiParser, &_midiParser->timerCallback);
+ }
+ _midiParser->setMidiDriver(_midiDriver);
+ _midiParser->setTimerRate(_midiDriver->getBaseTempo());
+
+ if (IS_SERRATED_SCALPEL) {
+ if (_musicType == MT_MT32) {
+ // Upload patches
+ Common::SeekableReadStream *MT32driverStream = _vm->_res->load("MTHOM.DRV", "MUSIC.LIB");
+
+ if (!MT32driverStream)
+ error("Music: could not load MTHOM.DRV, critical");
+
+ byte *MT32driverData = new byte[MT32driverStream->size()];
+ int32 MT32driverDataSize = MT32driverStream->size();
+ assert(MT32driverData);
+
+ MT32driverStream->read(MT32driverData, MT32driverDataSize);
+ delete MT32driverStream;
+
+ assert(MT32driverDataSize > 12);
+ byte *MT32driverDataPtr = MT32driverData + 12;
+ MT32driverDataSize -= 12;
+
+ MidiDriver_MT32_uploadPatches(_midiDriver, MT32driverDataPtr, MT32driverDataSize);
+ delete[] MT32driverData;
+ }
+ }
+
+ _musicOn = true;
+ }
+}
+
+Music::~Music() {
+ stopMusic();
+ if (_midiDriver) {
+ _midiDriver->setTimerCallback(this, NULL);
+ }
+ if (_midiParser) {
+ _midiParser->stopPlaying();
+ delete _midiParser;
+ _midiParser = nullptr;
+ }
+ if (_midiDriver) {
+ _midiDriver->close();
+ delete _midiDriver;
+ }
+}
+
+bool Music::loadSong(int songNumber) {
+ debugC(kDebugLevelMusic, "Music: loadSong()");
+
+ if(songNumber == 100)
+ songNumber = 55;
+ else if(songNumber == 70)
+ songNumber = 54;
+
+ if((songNumber > 60) || (songNumber < 1))
+ return false;
+
+ songNumber = ROOM_SONG[songNumber];
+
+ if(songNumber == 0)
+ songNumber = 12;
+
+ if((songNumber > NUM_SONGS) || (songNumber < 1))
+ return false;
+
+ Common::String songName = Common::String(SONG_NAMES[songNumber - 1]);
+
+ freeSong(); // free any song that is currently loaded
+ stopMusic();
+
+ if (!playMusic(songName))
+ return false;
+
+ startSong();
+ return true;
+}
+
+bool Music::loadSong(const Common::String &songName) {
+ freeSong(); // free any song that is currently loaded
+ stopMusic();
+
+ if (!playMusic(songName))
+ return false;
+
+ startSong();
+ return true;
+}
+
+void Music::syncMusicSettings() {
+ _musicOn = !ConfMan.getBool("mute") && !ConfMan.getBool("music_mute");
+}
+
+bool Music::playMusic(const Common::String &name) {
+ if (!_musicOn)
+ return false;
+
+ debugC(kDebugLevelMusic, "Music: playMusic('%s')", name.c_str());
+
+ if (!IS_3DO) {
+ // MIDI based
+ if (!_midiDriver)
+ return false;
+
+ Common::String midiMusicName = (IS_SERRATED_SCALPEL) ? name + ".MUS" : name + ".XMI";
+ Common::SeekableReadStream *stream = _vm->_res->load(midiMusicName, "MUSIC.LIB");
+
+ byte *midiMusicData = new byte[stream->size()];
+ int32 midiMusicDataSize = stream->size();
+
+ stream->read(midiMusicData, midiMusicDataSize);
+ delete stream;
+
+ // for dumping the music tracks
+#if 0
+ Common::DumpFile outFile;
+ outFile.open(name + ".RAW");
+ outFile.write(data, stream->size());
+ outFile.flush();
+ outFile.close();
+#endif
+
+ if (midiMusicDataSize < 14) {
+ warning("Music: not enough data in music file");
+ delete[] midiMusicData;
+ return false;
+ }
+
+ byte *dataPos = midiMusicData;
+ uint32 dataSize = midiMusicDataSize;
+
+ if (IS_SERRATED_SCALPEL) {
+ if (memcmp(" ", dataPos, 12)) {
+ warning("Music: expected header not found in music file");
+ delete[] midiMusicData;
+ return false;
+ }
+ dataPos += 12;
+ dataSize -= 12;
+
+ if (dataSize < 0x7F) {
+ warning("Music: expected music header not found in music file");
+ delete[] midiMusicData;
+ return false;
+ }
+
+ uint16 headerSize = READ_LE_UINT16(dataPos);
+ if (headerSize != 0x7F) {
+ warning("Music: header is not as expected");
+ delete[] midiMusicData;
+ return false;
+ }
+ } else {
+ if (memcmp("FORM", dataPos, 4)) {
+ warning("Music: expected header not found in music file");
+ delete[] midiMusicData;
+ return false;
+ }
+ }
+
+ if (IS_SERRATED_SCALPEL) {
+ // Pass the music data to the driver as well
+ // because channel mapping and a few other things inside the header
+ switch (_musicType) {
+ case MT_ADLIB:
+ MidiDriver_SH_AdLib_newMusicData(_midiDriver, dataPos, dataSize);
+ break;
+
+ case MT_MT32:
+ MidiDriver_MT32_newMusicData(_midiDriver, dataPos, dataSize);
+ break;
+
+ default:
+ // should never happen
+ break;
+ }
+ }
+
+ _midiParser->loadMusic(midiMusicData, midiMusicDataSize);
+
+ } else {
+ // 3DO: sample based
+ Audio::AudioStream *musicStream;
+ Common::String digitalMusicName = "music/" + name + "_MW22.aifc";
+
+ if (isPlaying()) {
+ _mixer->stopHandle(_digitalMusicHandle);
+ }
+
+ Common::File *digitalMusicFile = new Common::File();
+ if (!digitalMusicFile->open(digitalMusicName)) {
+ warning("playMusic: can not open 3DO music '%s'", digitalMusicName.c_str());
+ return false;
+ }
+
+ // Try to load the given file as AIFF/AIFC
+ musicStream = Audio::makeAIFFStream(digitalMusicFile, DisposeAfterUse::YES);
+ if (!musicStream) {
+ warning("playMusic: can not load 3DO music '%s'", digitalMusicName.c_str());
+ return false;
+ }
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &_digitalMusicHandle, musicStream);
+ }
+ return true;
+}
+
+void Music::stopMusic() {
+ freeSong();
+}
+
+void Music::startSong() {
+ if (!_musicOn)
+ return;
+
+ // TODO
+ warning("TODO: Sound::startSong");
+ _musicPlaying = true;
+}
+
+void Music::freeSong() {
+ if (!IS_3DO) {
+ if (_midiParser->isPlaying())
+ _midiParser->stopPlaying();
+
+ // Free the MIDI MUS data buffer
+ _midiParser->unloadMusic();
+ }
+
+ _musicPlaying = false;
+}
+
+void Music::waitTimerRoland(uint time) {
+ // TODO
+ warning("TODO: Sound::waitTimerRoland");
+}
+
+bool Music::isPlaying() {
+ if (!IS_3DO) {
+ // MIDI based
+ return _midiParser->isPlaying();
+ } else {
+ // 3DO: sample based
+ return _mixer->isSoundHandleActive(_digitalMusicHandle);
+ }
+}
+
+// Returns the current music position in milliseconds
+uint32 Music::getCurrentPosition() {
+ if (!IS_3DO) {
+ // MIDI based
+ return (_midiParser->getTick() * 1000) / 60; // translate tick to millisecond
+ } else {
+ // 3DO: sample based
+ return _mixer->getSoundElapsedTime(_digitalMusicHandle);
+ }
+}
+
+// This is used to wait for the music in certain situations like especially the intro
+// Note: the original game didn't do this, instead it just waited for certain amounts of time
+// We do this, so that the intro graphics + music work together even on faster/slower hardware.
+bool Music::waitUntilMSec(uint32 msecTarget, uint32 msecMax, uint32 additionalDelay, uint32 noMusicDelay) {
+ uint32 msecCurrent = 0;
+
+ if (!isPlaying()) {
+ return _vm->_events->delay(noMusicDelay, true);
+ }
+ while (1) {
+ if (!isPlaying()) { // Music is not playing anymore -> we are done
+ if (additionalDelay > 0) {
+ if (!_vm->_events->delay(additionalDelay, true))
+ return false;
+ }
+ return true;
+ }
+
+ msecCurrent = getCurrentPosition();
+ //warning("waitUntilMSec: %lx", msecCurrent);
+
+ if ((!msecMax) || (msecCurrent <= msecMax)) {
+ if (msecCurrent >= msecTarget) {
+ if (additionalDelay > 0) {
+ if (!_vm->_events->delay(additionalDelay, true))
+ return false;
+ }
+ return true;
+ }
+ }
+ if (!_vm->_events->delay(10, true))
+ return false;
+ }
+}
+
+void Music::setMIDIVolume(int volume) {
+ warning("TODO: Music::setMIDIVolume");
+}
+
+} // End of namespace Sherlock