[Scummvm-git-logs] scummvm master -> 5973a310669b40dc7cf132ca1a4bf1d148065a1c
mduggan
noreply at scummvm.org
Mon Oct 21 04:46:13 UTC 2024
This automated email contains information about 10 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
4bb1410079 DGDS: Fix ADS move ops again
24c1309c99 DGDS: Add Johnny Castaway detection entry
d51aa872b6 DGDS: Integrate SCI midi player code
3f7f33f1fc DGDS: Make midi code a bit closer to SCI version
40f7744f37 DGDS: Fix SFX mapping in RoTD and HoC
aaa48d45d4 DGDS: Read and use sfx/music loop flag
9a35861c13 DGDS: Don't stop music if new one is the same
eff6383081 DGDS: Limit MT32 volume to 80 to match original
8291d75dca DGDS: Small midi fixes for looping
5973a31066 DGDS: Small sound cleanups
Commit: 4bb1410079d8fbe88b45fe6bca5f801365187917
https://github.com/scummvm/scummvm/commit/4bb1410079d8fbe88b45fe6bca5f801365187917
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Fix ADS move ops again
These move operations appear to actually work off the index of the sequence
within the list, not the sequence number. This finally fixes the drawing of
the clothes in Blade's apartment without breaking the tap.
Still need to test through the games to make sure but I think this will now
give the correct execution order.
Changed paths:
engines/dgds/ads.cpp
engines/dgds/parser.cpp
engines/dgds/ttm.cpp
diff --git a/engines/dgds/ads.cpp b/engines/dgds/ads.cpp
index ad5f4c2dc51..359ac810044 100644
--- a/engines/dgds/ads.cpp
+++ b/engines/dgds/ads.cpp
@@ -638,47 +638,29 @@ bool ADSInterpreter::handleOperation(uint16 code, Common::SeekableReadStream *sc
seqnum = scr->readUint16LE();
debug(10, "ADS 0x%04x: mov seq to front env %d seq %d", code, enviro, seqnum);
/*uint16 unk = */scr->readUint16LE();
- // This is O(N) but the N is small and it's not called often.
- TTMSeq seq;
- bool success = false;
- for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
- if (_adsData->_ttmSeqs[i]._enviro == enviro && _adsData->_ttmSeqs[i]._seqNum == seqnum) {
- seq = _adsData->_ttmSeqs[i];
- _adsData->_ttmSeqs.remove_at(i);
- success = true;
- break;
- }
- }
-
- if (success)
+ if (seqnum < _adsData->_ttmSeqs.size()) {
+ // This is O(N) but the N is small and it's not called often.
+ TTMSeq seq = _adsData->_ttmSeqs.remove_at(seqnum);
_adsData->_ttmSeqs.insert_at(0, seq);
- else
+ } else {
warning("ADS: 0x4000 Request to move env %d seq %d which doesn't exist", enviro, seqnum);
+ }
break;
}
- case 0x4010: { // MOVE SEQ TO FRONT
+ case 0x4010: { // MOVE SEQ TO BACK
enviro = scr->readUint16LE();
seqnum = scr->readUint16LE();
debug(10, "ADS 0x%04x: mov seq to back env %d seq %d", code, enviro, seqnum);
/*uint16 unk = */scr->readUint16LE();
- // This is O(N) but the N is small and it's not called often.
- TTMSeq seq;
- bool success = false;
- for (uint i = 0; i < _adsData->_ttmSeqs.size(); i++) {
- if (_adsData->_ttmSeqs[i]._enviro == enviro && _adsData->_ttmSeqs[i]._seqNum == seqnum) {
- seq = _adsData->_ttmSeqs[i];
- _adsData->_ttmSeqs.remove_at(i);
- success = true;
- break;
- }
- }
-
- if (success)
+ if (seqnum < _adsData->_ttmSeqs.size()) {
+ // This is O(N) but the N is small and it's not called often.
+ TTMSeq seq = _adsData->_ttmSeqs.remove_at(seqnum);
_adsData->_ttmSeqs.push_back(seq);
- else
+ } else {
warning("ADS: 0x4010 Request to move env %d seq %d which doesn't exist", enviro, seqnum);
+ }
break;
}
diff --git a/engines/dgds/parser.cpp b/engines/dgds/parser.cpp
index 44d2dbd5137..e2c0f362cdf 100644
--- a/engines/dgds/parser.cpp
+++ b/engines/dgds/parser.cpp
@@ -81,8 +81,12 @@ Common::HashMap<uint16, Common::String> DgdsParser::readTags(Common::SeekableRea
for (uint16 i = 0; i < count; i++) {
uint16 idx = stream->readUint16LE();
- tags[idx] = stream->readString();
- debug(" %2u: %2u, \"%s\"", i, idx, tags[idx].c_str());
+ const Common::String tagVal = stream->readString();
+ debug(" %2u: %2u, \"%s\"", i, idx, tagVal.c_str());
+ // TODO: How to handle when these IDs overlap? (eg, see
+ // BBEDROOM.TTM in RotD)
+ if (!tags.contains(idx))
+ tags[idx] = tagVal;
}
return tags;
diff --git a/engines/dgds/ttm.cpp b/engines/dgds/ttm.cpp
index 50838d01080..d826f02c123 100644
--- a/engines/dgds/ttm.cpp
+++ b/engines/dgds/ttm.cpp
@@ -929,11 +929,12 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
flipMode = kImageFlipHV;
Common::SharedPtr<Image> img = env._scriptShapes[bmpNo];
- if (img)
+ if (img) {
img->drawBitmap(frameno, env._xOff + ivals[0], env._yOff + ivals[1],
seq._drawWin, _vm->_compositionBuffer, flipMode, dstWidth, dstHeight);
- else
+ } else {
warning("Trying to draw image %d in env %d which is not loaded", bmpNo, env._enviro);
+ }
break;
}
case 0xa600: { // DRAW GETPUT: i:int
Commit: 24c1309c99b8560ecb75605ec0d720fec220a6f4
https://github.com/scummvm/scummvm/commit/24c1309c99b8560ecb75605ec0d720fec220a6f4
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Add Johnny Castaway detection entry
Immediately crashes as it expects 640x480, but data loads correctly.
Changed paths:
engines/dgds/detection.cpp
engines/dgds/detection_tables.h
engines/dgds/dgds.cpp
engines/dgds/dgds.h
engines/dgds/font.cpp
diff --git a/engines/dgds/detection.cpp b/engines/dgds/detection.cpp
index 97eebd4eef6..ee861810d91 100644
--- a/engines/dgds/detection.cpp
+++ b/engines/dgds/detection.cpp
@@ -29,6 +29,7 @@ static const PlainGameDescriptor dgdsGames[] = {
{"quarky", "Quarky And Quaysoo's Turbo Science"},
{"sq5demo", "Space Quest V Demo"},
{"comingattractions", "Sierra / Dynamix 1991 Coming Attractions Demo"},
+ {"castaway", "Johnny Castaway"},
{0, 0}
};
diff --git a/engines/dgds/detection_tables.h b/engines/dgds/detection_tables.h
index 094c6431784..61d2737dcb5 100644
--- a/engines/dgds/detection_tables.h
+++ b/engines/dgds/detection_tables.h
@@ -353,6 +353,22 @@ static const ADGameDescription gameDescriptions[] = {
GUIO1(GUIO_NONE)
},
+ // Johnny Castaway screensaver
+ {
+ "castaway",
+ 0,
+ {
+ {"resource.map", 0, "374e6d05c5e0acd88fb5af748948c899", 1461},
+ {"resource.001", 0, "46acc7ab8f6d7f63838ffea5f87380e2", 1175645},
+ AD_LISTEND
+ },
+ Common::EN_ANY,
+ Common::kPlatformWindows,
+ ADGF_DEMO | ADGF_UNSTABLE,
+ GUIO1(GUIO_NONE)
+ },
+
+
AD_TABLE_END_MARKER
};
diff --git a/engines/dgds/dgds.cpp b/engines/dgds/dgds.cpp
index 22c31efe7c6..3b838fce607 100644
--- a/engines/dgds/dgds.cpp
+++ b/engines/dgds/dgds.cpp
@@ -105,6 +105,9 @@ DgdsEngine::DgdsEngine(OSystem *syst, const ADGameDescription *gameDesc)
} else if (!strcmp(gameDesc->gameId, "comingattractions")) {
_isDemo = true;
_gameId = GID_COMINGATTRACTIONS;
+ } else if (!strcmp(gameDesc->gameId, "castaway")) {
+ _isDemo = true;
+ _gameId = GID_CASTAWAY;
} else {
error("Unknown game ID");
}
@@ -417,6 +420,12 @@ void DgdsEngine::loadGameFiles() {
_adsInterp->load("DEMO.ADS");
_adsInterp->segmentOrState(1, 3);
break;
+ case GID_CASTAWAY:
+ _gameGlobals = new Globals(_clock);
+ _gamePals->loadPalette("JOHNCAST.PAL");
+ _adsInterp->load("JOHNNY.ADS");
+ _adsInterp->segmentOrState(1, 3);
+ break;
default:
error("Unsupported game type in loadGameFiles");
}
diff --git a/engines/dgds/dgds.h b/engines/dgds/dgds.h
index eafab0d6857..fa5885d015b 100644
--- a/engines/dgds/dgds.h
+++ b/engines/dgds/dgds.h
@@ -69,7 +69,8 @@ enum DgdsGameId {
GID_WILLY,
GID_SQ5DEMO,
GID_COMINGATTRACTIONS,
- GID_QUARKY
+ GID_QUARKY,
+ GID_CASTAWAY
};
enum DgdsDetailLevel {
diff --git a/engines/dgds/font.cpp b/engines/dgds/font.cpp
index 69ac6f0c222..b35ae91a1e2 100644
--- a/engines/dgds/font.cpp
+++ b/engines/dgds/font.cpp
@@ -234,6 +234,9 @@ FontManager::FontType FontManager::fontTypeByName(const Common::String &filename
void FontManager::loadFonts(DgdsGameId gameId, ResourceManager *resMgr, Decompressor *decomp) {
+ if (gameId == GID_CASTAWAY)
+ return; // no fonts
+
if (gameId == GID_SQ5DEMO) {
tryLoadFont("SSM1_12.FNT", resMgr, decomp);
tryLoadFont("SSM1_15.FNT", resMgr, decomp);
Commit: d51aa872b65a989fd3e6376b2427256f811e4f50
https://github.com/scummvm/scummvm/commit/d51aa872b65a989fd3e6376b2427256f811e4f50
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Integrate SCI midi player code
DGDS basically uses the same midi and patch code as SCI. This change copies
the midi driver out of SCI. It now mostly works, but there is still an
instrument mismatch to fix out with SFX.
Changed paths:
A engines/dgds/sound/drivers/adlib.cpp
A engines/dgds/sound/drivers/amigamac1.cpp
A engines/dgds/sound/drivers/cms.cpp
A engines/dgds/sound/drivers/fmtowns.cpp
A engines/dgds/sound/drivers/gm_names.h
A engines/dgds/sound/drivers/macmixer.h
A engines/dgds/sound/drivers/map-mt32-to-gm.h
A engines/dgds/sound/drivers/midi.cpp
A engines/dgds/sound/drivers/mididriver.h
A engines/dgds/sound/drivers/midipatch.cpp
A engines/dgds/sound/midiparser_sci.cpp
A engines/dgds/sound/midiparser_sci.h
A engines/dgds/sound/music.cpp
A engines/dgds/sound/music.h
A engines/dgds/sound/resource/resource_audio.cpp
A engines/dgds/sound/resource/sci_resource.cpp
A engines/dgds/sound/resource/sci_resource.h
A engines/dgds/sound/scispan.h
R engines/dgds/music.cpp
R engines/dgds/music.h
engines/dgds/console.cpp
engines/dgds/dgds.cpp
engines/dgds/dgds.h
engines/dgds/menu.cpp
engines/dgds/module.mk
engines/dgds/resource.cpp
engines/dgds/sound.cpp
engines/dgds/sound.h
engines/dgds/ttm.cpp
diff --git a/engines/dgds/console.cpp b/engines/dgds/console.cpp
index ea86e3c5c66..007a707d0bc 100644
--- a/engines/dgds/console.cpp
+++ b/engines/dgds/console.cpp
@@ -125,15 +125,18 @@ bool Console::cmdFileDump(int argc, const char **argv) {
continue;
}
- chunk.readContent(_vm->getDecompressor());
+ bool readSuccess = chunk.readContent(_vm->getDecompressor());
+ if (readSuccess) {
+ memcpy(ptr, chunk.getIdStr(), 4);
+ ptr += 4;
- memcpy(ptr, chunk.getIdStr(), 4);
- ptr += 4;
+ chunk.getContent()->read(ptr, chunk.getContent()->size());
+ ptr += chunk.getContent()->size();
- chunk.getContent()->read(ptr, chunk.getContent()->size());
- ptr += chunk.getContent()->size();
-
- size += 4 + chunk.getContent()->size();
+ size += 4 + chunk.getContent()->size();
+ } else {
+ warning("Failed to read content for chunk with id %s", chunk.getIdStr());
+ }
}
}
diff --git a/engines/dgds/dgds.cpp b/engines/dgds/dgds.cpp
index 3b838fce607..d47d1cae83d 100644
--- a/engines/dgds/dgds.cpp
+++ b/engines/dgds/dgds.cpp
@@ -366,6 +366,7 @@ void DgdsEngine::loadGameFiles() {
reqParser.parse(&vcrRequestData, "DVCR.REQ");
break;
case GID_HOC:
+ _soundPlayer->loadSFX("SOUNDS1.SNG");
_gameGlobals = new HocGlobals(_clock);
_gamePals->loadPalette("HOC.PAL");
_gdsScene->load("HOC.GDS", _resource, _decompressor);
diff --git a/engines/dgds/dgds.h b/engines/dgds/dgds.h
index fa5885d015b..2216c3e4e26 100644
--- a/engines/dgds/dgds.h
+++ b/engines/dgds/dgds.h
@@ -177,7 +177,8 @@ public:
void restartGame();
- DgdsGameId getGameId() { return _gameId; }
+ DgdsGameId getGameId() const { return _gameId; }
+ Common::Platform getPlatform() const { return _platform; }
Graphics::ManagedSurface &getBackgroundBuffer() { return _backgroundBuffer; }
Graphics::ManagedSurface &getStoredAreaBuffer() { return _storedAreaBuffer; }
diff --git a/engines/dgds/menu.cpp b/engines/dgds/menu.cpp
index 76de0ce82f6..565594df0e2 100644
--- a/engines/dgds/menu.cpp
+++ b/engines/dgds/menu.cpp
@@ -33,7 +33,6 @@
#include "dgds/font.h"
#include "dgds/globals.h"
#include "dgds/menu.h"
-#include "dgds/music.h"
#include "dgds/request.h"
#include "dgds/scene.h"
#include "dgds/sound.h"
@@ -546,7 +545,6 @@ void Menu::handleClickOptionsMenu(const Common::Point &mouse) {
Gadget *gadget = getClickedMenuItem(mouse);
int16 clickedMenuItem = gadget->_gadgetNo;
Audio::Mixer::SoundType soundType = Audio::Mixer::kMusicSoundType;
- DgdsMidiPlayer *midiPlayer = engine->_soundPlayer->getMidiPlayer();
switch (clickedMenuItem) {
case kMenuOptionsJoystickOnOff:
@@ -563,12 +561,14 @@ void Menu::handleClickOptionsMenu(const Common::Point &mouse) {
case kMenuOptionsMusicOnOffHoC:
if (!mixer->isSoundTypeMuted(soundType)) {
mixer->muteSoundType(soundType, true);
- midiPlayer->syncVolume();
- midiPlayer->pause();
+ warning("TODO: Sync volume and pause music");
+ //midiPlayer->syncVolume();
+ //midiPlayer->pause();
} else {
mixer->muteSoundType(soundType, false);
- midiPlayer->syncVolume();
- midiPlayer->resume();
+ warning("TODO: Sync volume and resume music");
+ //midiPlayer->syncVolume();
+ //midiPlayer->resume();
}
updateOptionsGadget(gadget);
diff --git a/engines/dgds/module.mk b/engines/dgds/module.mk
index ed239282b19..5e163c2e9b5 100644
--- a/engines/dgds/module.mk
+++ b/engines/dgds/module.mk
@@ -18,7 +18,6 @@ MODULE_OBJS := \
inventory.o \
menu.o \
metaengine.o \
- music.o \
parser.o \
request.o \
resource.o \
@@ -27,7 +26,15 @@ MODULE_OBJS := \
sound_raw.o \
ttm.o \
scene.o \
- sound.o
+ sound.o \
+ sound/midiparser_sci.o \
+ sound/music.o \
+ sound/drivers/adlib.o \
+ sound/drivers/amigamac1.o \
+ sound/drivers/midi.o \
+ sound/drivers/midipatch.o \
+ sound/resource/sci_resource.o \
+ sound/resource/resource_audio.o \
# This module can be built as a plugin
ifeq ($(ENABLE_DGDS), DYNAMIC_PLUGIN)
diff --git a/engines/dgds/music.cpp b/engines/dgds/music.cpp
deleted file mode 100644
index 714c710efcd..00000000000
--- a/engines/dgds/music.cpp
+++ /dev/null
@@ -1,379 +0,0 @@
-/* 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 3 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, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "common/debug.h"
-#include "common/config-manager.h"
-#include "audio/midiparser.h"
-
-#include "dgds/sound.h"
-#include "dgds/music.h"
-
-namespace Dgds {
-
-/**
- * The Standard MIDI File version of MidiParser.
- */
-class MidiParser_DGDS : public MidiParser {
-protected:
- byte *_init;
- const byte *_last;
-
- byte _numParts;
- const byte *_trackPtr[0xFF];
- uint16 _trackSz[0xFF];
-
-protected:
- void parseNextEvent(EventInfo &info) override;
-
-public:
- MidiParser_DGDS();
- ~MidiParser_DGDS();
-
- void sendInitCommands() {};
-
- bool loadMusic(byte *data, uint32 size) override;
-
- bool validateNextRead(uint i, const uint16 *trackPos) const;
- byte midiGetNextChannel(const uint16 *trackPos, const uint32 *trackTimer, long ticker) const;
- void mixChannels();
-};
-
-MidiParser_DGDS::MidiParser_DGDS() : _init(nullptr), _last(nullptr), _numParts(0) {
- memset(_trackSz, 0, sizeof(_trackSz));
- memset(_trackPtr, 0, sizeof(_trackPtr));
-}
-
-MidiParser_DGDS::~MidiParser_DGDS() {
- free(_init);
-}
-
-void MidiParser_DGDS::parseNextEvent(EventInfo &info) {
- if (_position._playPos >= _last) {
- // fake an end-of-track meta event
- info.delta = 0;
- info.event = 0xFF;
- info.ext.type = 0x2F;
- info.length = 0;
- return;
- }
-
- info.start = _position._playPos;
- info.delta = 0;
- while (*_position._playPos == 0xF8) {
- info.delta += 240;
- _position._playPos++;
- }
- info.delta += *_position._playPos++;
-
- // Process the next info.
- if ((_position._playPos[0] & 0xF0) >= 0x80)
- info.event = *_position._playPos++;
- else
- info.event = _position._runningStatus;
- if (info.event < 0x80)
- return;
-
- _position._runningStatus = info.event;
- switch (info.command()) {
- case 0x9:
- info.basic.param1 = *_position._playPos++;
- info.basic.param2 = *_position._playPos++;
- if (info.basic.param2 == 0) {
- // NoteOn with param2==0 is a NoteOff
- info.event = info.channel() | 0x80;
- }
- info.length = 0;
- break;
-
- case 0xC:
- case 0xD:
- info.basic.param1 = *_position._playPos++;
- info.basic.param2 = 0;
- break;
-
- case 0x8:
- case 0xA:
- case 0xB:
- case 0xE:
- info.basic.param1 = *_position._playPos++;
- info.basic.param2 = *_position._playPos++;
- info.length = 0;
- break;
-
- case 0xF: // System Common, Meta or SysEx event
- switch (info.event & 0x0F) {
- case 0x2: // Song Position Pointer
- info.basic.param1 = *_position._playPos++;
- info.basic.param2 = *_position._playPos++;
- break;
-
- case 0x3: // Song Select
- info.basic.param1 = *_position._playPos++;
- info.basic.param2 = 0;
- break;
-
- case 0x6:
- case 0x8:
- case 0xA:
- case 0xB:
- case 0xC:
- case 0xE:
- info.basic.param1 = info.basic.param2 = 0;
- break;
-
- case 0x0: // SysEx
- info.length = readVLQ(_position._playPos);
- info.ext.data = _position._playPos;
- _position._playPos += info.length;
- break;
-
- case 0xF: // META event
- info.ext.type = *_position._playPos++;
- info.length = readVLQ(_position._playPos);
- info.ext.data = _position._playPos;
- _position._playPos += info.length;
- break;
-
- default:
- warning("Unexpected midi event 0x%02X in midi data", info.event);
- }
- }
-}
-
-byte MidiParser_DGDS::midiGetNextChannel(const uint16 *trackPos, const uint32 *trackTimer, long ticker) const {
- byte curr = 0xFF;
- uint32 closest = ticker + 1000000, next = 0;
-
- for (byte i = 0; i < _numParts; i++) {
- if (trackTimer[i] == 0xFFFFFFFF) // channel ended
- continue;
- if (trackPos[i] >= _trackSz[i])
- continue;
- next = _trackPtr[i][trackPos[i]]; // when the next event should occur
- if (next == 0xF8) // 0xF8 means 240 ticks delay
- next = 240;
- next += trackTimer[i];
- if (next < closest) {
- curr = i;
- closest = next;
- }
- }
-
- return curr;
-}
-
-bool MidiParser_DGDS::validateNextRead(uint i, const uint16 *trackPos) const {
- if (_trackSz[i] <= trackPos[i]) {
- warning("Unexpected end. Music may sound wrong due to game resource corruption");
- return false;
- } else {
- return true;
- }
-}
-
-static const byte CMD_LENGTHS[] = { 2, 2, 2, 2, 1, 1, 2, 0 };
-
-void MidiParser_DGDS::mixChannels() {
- int totalSize = 0;
-
- uint16 trackPos[0xFF];
- uint32 trackTimer[0xFF];
- byte _prev[0xFF];
- for (byte i = 0; i < _numParts; i++) {
- trackTimer[i] = 0;
- _prev[i] = 0;
- trackPos[i] = 0;
- totalSize += _trackSz[i];
- }
-
- byte *output = (byte *)malloc(totalSize * 2);
- _tracks[0] = output;
-
- uint32 ticker = 0;
- byte channel, curDelta;
- byte midiCommand = 0, midiParam, globalPrev = 0;
- uint32 newDelta;
-
- while ((channel = midiGetNextChannel(trackPos, trackTimer, ticker)) != 0xFF) { // there is still an active channel
- if (!validateNextRead(channel, trackPos))
- goto end;
- curDelta = _trackPtr[channel][trackPos[channel]++];
- trackTimer[channel] += (curDelta == 0xF8 ? 240 : curDelta); // when the command is supposed to occur
- if (curDelta == 0xF8)
- continue;
- newDelta = trackTimer[channel] - ticker;
- ticker += newDelta;
-
- if (!validateNextRead(channel, trackPos))
- goto end;
- midiCommand = _trackPtr[channel][trackPos[channel]++];
- if (midiCommand != 0xFC) {
- // Write delta
- while (newDelta > 240) {
- *output++ = 0xF8;
- newDelta -= 240;
- }
- *output++ = (byte)newDelta;
- }
- // Write command
- switch (midiCommand) {
- case 0xF0: // sysEx
- *output++ = midiCommand;
- do {
- if (!validateNextRead(channel, trackPos))
- goto end;
- midiParam = _trackPtr[channel][trackPos[channel]++];
- *output++ = midiParam;
- } while (midiParam != 0xF7);
- break;
- case 0xFC: // end of channel
- trackTimer[channel] = 0xFFFFFFFF;
- break;
- default: // MIDI command
- if (midiCommand & 0x80) {
- if (!validateNextRead(channel, trackPos))
- goto end;
- midiParam = _trackPtr[channel][trackPos[channel]++];
- } else {// running status
- midiParam = midiCommand;
- midiCommand = _prev[channel];
- }
-
- // remember which channel got used for channel remapping
- //byte midiChannel = midiCommand & 0xF;
- //_channelUsed[midiChannel] = true;
-
- if (midiCommand != globalPrev)
- *output++ = midiCommand;
- *output++ = midiParam;
- if (CMD_LENGTHS[(midiCommand >> 4) - 8] == 2) {
- if (!validateNextRead(channel, trackPos))
- goto end;
- *output++ = _trackPtr[channel][trackPos[channel]++];
- }
- _prev[channel] = midiCommand;
- globalPrev = midiCommand;
- break;
- }
- }
-
-end:
- _last = output;
-}
-
-bool MidiParser_DGDS::loadMusic(byte *data, uint32 size) {
- unloadMusic();
-
- if (!data)
- return false;
-
- _numParts = loadSndTrack(TRACK_MT32, _trackPtr, _trackSz, data, size);
- if (_numParts == 0)
- return false;
-
- for (byte part = 0; part < _numParts; part++) {
- const byte *ptr = _trackPtr[part];
-
- byte number, voices;
- number = (*ptr++);
- voices = (*ptr++) & 0x0F;
- debug(" - #%u: voices: %u", number, voices);
-
- _trackPtr[part] += 2;
- _trackSz[part] -= 2;
- }
-
- mixChannels();
-
- _init = _tracks[0];
- _numTracks = 1;
-
- _ppqn = 1;
- setTempo(16667);
-
- // Note that we assume the original data passed in
- // will persist beyond this call, i.e. we do NOT
- // copy the data to our own buffer. Take warning....
- resetTracking();
- setTrack(0);
-
- return true;
-}
-
-
-DgdsMidiPlayer::DgdsMidiPlayer(bool isSfx) : _isSfx(isSfx), Audio::MidiPlayer() {
- MidiPlayer::createDriver();
-
- int ret = _driver->open();
- if (ret == 0) {
- if (_nativeMT32)
- _driver->sendMT32Reset();
- else
- _driver->sendGMReset();
- _driver->setTimerCallback(this, &timerCallback);
- }
-}
-
-void DgdsMidiPlayer::syncVolumeForChannel() {
- int volume = ConfMan.getInt(_isSfx ? "sfx_volume": "music_volume");
- if (ConfMan.getBool("mute")) {
- volume = -1;
- }
- setVolume(volume);
-}
-
-void DgdsMidiPlayer::play(byte *data, uint32 size) {
- Common::StackLock lock(_mutex);
-
- stop();
- if (!data) return;
-
- MidiParser_DGDS *parser = new MidiParser_DGDS();
- if (parser->loadMusic(data, size)) {
- parser->setMidiDriver(this);
- parser->sendInitCommands();
- parser->setTimerRate(_driver->getBaseTempo());
- /*
- parser->property(MidiParser::mpCenterPitchWheelOnUnload, 1);
- parser->property(MidiParser::mpSendSustainOffOnNotesOff, 1);
- */
- _parser = parser;
- syncVolumeForChannel();
-
- _isLooping = false;
- _isPlaying = true;
- debug("Playing music track sz %d", size);
- } else {
- debug("Cannot play music track");
- delete parser;
- }
-}
-
-void DgdsMidiPlayer::stop() {
- _driver->stopAllNotes();
- _isPlaying = false;
- _isLooping = false;
- Audio::MidiPlayer::stop();
- debug("Stopping track");
-}
-
-} // End of namespace Dgds
-
diff --git a/engines/dgds/resource.cpp b/engines/dgds/resource.cpp
index c9517e225fd..e3cf35fbf50 100644
--- a/engines/dgds/resource.cpp
+++ b/engines/dgds/resource.cpp
@@ -124,6 +124,9 @@ Common::SeekableReadStream *ResourceManager::getResource(Common::String name, bo
Resource res = _resources[name];
+ if (res.size == 0xffffffff) // In willy beamish??
+ return nullptr;
+
return new Common::SeekableSubReadStream(&_volumes[res.volume], res.pos, res.pos + res.size);
}
@@ -229,6 +232,12 @@ bool DgdsChunkReader::isPacked() const {
packed = true;
else if (strcmp(_idStr, "004:") == 0)
packed = true;
+ else if (strcmp(_idStr, "005:") == 0)
+ packed = true;
+ else if (strcmp(_idStr, "007:") == 0)
+ packed = true;
+ else if (strcmp(_idStr, "009:") == 0)
+ packed = true;
else if (strcmp(_idStr, "101:") == 0)
packed = true;
else if (strcmp(_idStr, "VGA:") == 0)
diff --git a/engines/dgds/sound.cpp b/engines/dgds/sound.cpp
index 41987bb2e5e..f5df4a8d63e 100644
--- a/engines/dgds/sound.cpp
+++ b/engines/dgds/sound.cpp
@@ -31,13 +31,18 @@
#include "dgds/decompress.h"
#include "dgds/includes.h"
-#include "dgds/music.h"
#include "dgds/parser.h"
#include "dgds/resource.h"
#include "dgds/sound.h"
+#include "dgds/sound/music.h"
+#include "dgds/sound/resource/sci_resource.h"
namespace Dgds {
+static const uint16 SIGNAL_OFFSET = 0xffff;
+static const int SND_RESOURCE_OFFSET = 4096;
+static const int MUSIC_RESOURCE_OFFSET = 8192;
+
static void _readHeader(const byte* &pos, uint32 &sci_header) {
sci_header = 0;
if (READ_LE_UINT16(pos) == 0x0084)
@@ -132,11 +137,58 @@ static uint32 _availableSndTracks(const byte *data, uint32 size) {
}
+static byte _loadSndTrack(uint32 track, const byte** trackPtr, uint16* trackSiz, const byte *data, uint32 size) {
+ byte matchDrv;
+ switch (track) {
+ case DIGITAL_PCM:
+ case TRACK_ADLIB: matchDrv = 0; break;
+ case TRACK_GM: matchDrv = 7; break;
+ case TRACK_MT32: matchDrv = 12; break;
+ default: return 0;
+ }
+
+ const byte *pos = data;
+
+ uint32 sci_header;
+ _readHeader(pos, sci_header);
+
+ while (pos[0] != 0xFF) {
+ byte drv = *pos++;
+
+ byte part;
+ const byte *ptr;
+
+ part = 0;
+ for (ptr = pos; *ptr != 0xFF; _skipPartHeader(ptr))
+ part++;
+
+ if (matchDrv == drv) {
+ part = 0;
+ while (pos[0] != 0xFF) {
+ uint16 off, siz;
+ _readPartHeader(pos, off, siz);
+ off += sci_header;
+
+ trackPtr[part] = data + off;
+ trackSiz[part] = siz;
+ part++;
+ }
+ debug("- (%d) Play parts = %d", drv, part);
+ return part;
+ } else {
+ pos = ptr;
+ }
+ pos++;
+ }
+ pos++;
+ return 0;
+}
+
+
Sound::Sound(Audio::Mixer *mixer, ResourceManager *resource, Decompressor *decompressor) :
- _mixer(mixer), _resource(resource), _decompressor(decompressor) {
- _midiMusicPlayer = new DgdsMidiPlayer(false);
- //_midiSoundPlayer = new DgdsMidiPlayer(true); // FIXME: Can't have multiple instances of OPL players
- _midiSoundPlayer = nullptr;
+ _mixer(mixer), _resource(resource), _decompressor(decompressor), _music(nullptr) {
+ _music = new SciMusic(true);
+ _music->init();
}
Sound::~Sound() {
@@ -144,9 +196,6 @@ Sound::~Sound() {
for (auto *data: _sfxData)
delete [] data;
-
- delete _midiMusicPlayer;
- delete _midiSoundPlayer;
}
void Sound::playAmigaSfx(const Common::String &filename, byte channel, byte volume) {
@@ -161,26 +210,25 @@ void Sound::playAmigaSfx(const Common::String &filename, byte channel, byte volu
byte *dest = new byte[sfxStream->size()];
sfxStream->read(dest, sfxStream->size());
- _soundData = new Common::MemoryReadStream(dest, sfxStream->size(), DisposeAfterUse::YES);
+ Common::MemoryReadStream *soundData = new Common::MemoryReadStream(dest, sfxStream->size(), DisposeAfterUse::YES);
delete sfxStream;
- stopSfx(channel);
+ stopSfxForChannel(channel);
- if (_soundData) {
+ if (soundData) {
Channel *ch = &_channels[channel];
- Audio::AudioStream *input = Audio::makeAIFFStream(_soundData, DisposeAfterUse::YES);
+ Audio::AudioStream *input = Audio::makeAIFFStream(soundData, DisposeAfterUse::YES);
_mixer->playStream(Audio::Mixer::kSFXSoundType, &ch->handle, input, -1, volume);
- _soundData = 0;
}
}
void Sound::stopAllSfx() {
+ _music->stopSFX();
for (uint i = 0; i < ARRAYSIZE(_channels); i++)
- stopSfx(i);
- //_midiSoundPlayer->stop();
+ stopSfxForChannel(i);
}
-void Sound::stopSfx(byte channel) {
+void Sound::stopSfxForChannel(byte channel) {
if (_mixer->isSoundHandleActive(_channels[channel].handle)) {
_mixer->stopHandle(_channels[channel].handle);
_channels[channel].stream = 0;
@@ -195,7 +243,7 @@ bool Sound::playPCM(const byte *data, uint32 size) {
const byte *trackPtr[0xFF];
uint16 trackSiz[0xFF];
- byte numParts = loadSndTrack(DIGITAL_PCM, trackPtr, trackSiz, data, size);
+ byte numParts = _loadSndTrack(DIGITAL_PCM, trackPtr, trackSiz, data, size);
if (numParts == 0)
return false;
@@ -213,7 +261,6 @@ bool Sound::playPCM(const byte *data, uint32 size) {
uint16 rate, length, first, last;
rate = READ_LE_UINT16(ptr);
-
length = READ_LE_UINT16(ptr + 2);
first = READ_LE_UINT16(ptr + 4);
last = READ_LE_UINT16(ptr + 6);
@@ -226,7 +273,7 @@ bool Sound::playPCM(const byte *data, uint32 size) {
trackSiz[part] = length;
Channel *ch = &_channels[part];
- byte volume = 255;
+ byte volume = 127;
Audio::AudioStream *input = Audio::makeRawStream(trackPtr[part], trackSiz[part],
rate, Audio::FLAG_UNSIGNED, DisposeAfterUse::NO);
_mixer->playStream(Audio::Mixer::kSFXSoundType, &ch->handle, input, -1, volume, 0, DisposeAfterUse::YES);
@@ -276,7 +323,7 @@ void Sound::loadMacMusic(const Common::String &filename) {
uint16 type = stream->readUint16LE();
uint16 count = stream->readUint16LE();
- debug(" SX INF %u [%u]: (%s)", type, count, filename.c_str());
+ debug(" SX INF %u [%u entries]: (%s)", type, count, filename.c_str());
for (uint16 k = 0; k < count; k++) {
uint16 idx = stream->readUint16LE();
debug(" %2u: %u", k, idx);
@@ -295,12 +342,9 @@ void Sound::loadMacMusic(const Common::String &filename) {
}
delete musicStream;
-
- stopMusic();
}
void Sound::loadMusic(const Common::String &filename) {
- stopMusic();
unloadMusic();
loadPCSound(filename, _musicSizes, _musicData);
}
@@ -330,55 +374,191 @@ void Sound::loadPCSound(const Common::String &filename, Common::Array<uint32> &s
Common::SeekableReadStream *stream = chunk.getContent();
if (chunk.isSection(ID_SNG)) {
- int32 musicSize = stream->size();
- byte *data = new byte[musicSize];
- stream->read(data, musicSize);
- sizeArray.push_back(musicSize);
+ int32 chunkSize = stream->size();
+ byte *data = new byte[chunkSize];
+ stream->read(data, chunkSize);
+ sizeArray.push_back(chunkSize);
dataArray.push_back(data);
} else if (chunk.isSection(ID_INF)) {
uint32 count = stream->size() / 2;
- debug(" SNG INF [%u]", count);
+ debug(" SNG INF [%u entries]", count);
for (uint32 k = 0; k < count; k++) {
uint16 idx = stream->readUint16LE();
- debug(" %2u: %u", k, idx);
+ debug(" %2u: 0x%04x", k, idx);
}
+ } else {
+ warning("loadPCSound: skip unused chunk %s in %s", chunk.getIdStr(), filename.c_str());
}
}
delete musicStream;
-
}
void Sound::playSFX(uint num) {
- playPCSound(num, _sfxSizes, _sfxData, _midiSoundPlayer);
+ playPCSound(num, _sfxSizes, _sfxData, Audio::Mixer::kSFXSoundType);
}
void Sound::stopSfxByNum(uint num) {
- warning("TODO: Implement me! Sound::stopSfxById(%d)", num);
+ MusicEntry *musicSlot = _music->getSlot(num + SND_RESOURCE_OFFSET);
+ if (!musicSlot) {
+ warning("stopSfxByNum: Slot not found (%08x)", num);
+ return;
+ }
+
+ musicSlot->dataInc = 0;
+ musicSlot->signal = SIGNAL_OFFSET;
+ _music->soundStop(musicSlot);
}
void Sound::playMusic(uint num) {
- playPCSound(num, _musicSizes, _musicData, _midiMusicPlayer);
+ playPCSound(num, _musicSizes, _musicData, Audio::Mixer::kMusicSoundType);
+}
+
+void Sound::processInitSound(uint num, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
+ // Check if a track with the same sound object is already playing
+ MusicEntry *oldSound = _music->getSlot(num);
+ if (oldSound) {
+ processDisposeSound(num);
+ }
+
+ MusicEntry *newSound = new MusicEntry();
+ newSound->resourceId = num;
+ newSound->soundObj = num;
+ newSound->loop = 1; // Default to one loop
+ newSound->overridePriority = false;
+ newSound->priority = 255; // TODO: Priority?
+ newSound->volume = MUSIC_VOLUME_DEFAULT;
+ newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below
+
+ debugC(kDebugLevelSound, "kDoSound(init): %08x number %d, loop %d, prio %d, vol %d", num,
+ num, newSound->loop, newSound->priority, newSound->volume);
+
+ initSoundResource(newSound, data, dataSz, soundType);
+
+ _music->pushBackSlot(newSound);
+}
+
+void Sound::initSoundResource(MusicEntry *newSound, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
+ if (newSound->resourceId) {
+ newSound->soundRes = new SoundResource(newSound->resourceId, data, dataSz);
+ if (!newSound->soundRes->exists()) {
+ delete newSound->soundRes;
+ newSound->soundRes = nullptr;
+ }
+ } else {
+ newSound->soundRes = nullptr;
+ }
+
+ if (!newSound->isSample && newSound->soundRes)
+ _music->soundInitSnd(newSound);
+
+ newSound->soundType = soundType;
+}
+
+void Sound::processDisposeSound(uint32 obj) {
+ // Mostly copied from SCI soundcmd.
+ MusicEntry *musicSlot = _music->getSlot(obj);
+ if (!musicSlot) {
+ warning("processDisposeSound: Slot not found (%08x)", obj);
+ return;
+ }
+
+ processStopSound(obj, false);
+
+ _music->soundKill(musicSlot);
+}
+
+void Sound::processStopSound(uint32 obj, bool sampleFinishedPlaying) {
+ // Mostly copied from SCI soundcmd.
+ MusicEntry *musicSlot = _music->getSlot(obj);
+ if (!musicSlot) {
+ warning("processStopSound: Slot not found (%08x)", obj);
+ return;
+ }
+
+ musicSlot->dataInc = 0;
+ musicSlot->signal = SIGNAL_OFFSET;
+ _music->soundStop(musicSlot);
}
-void Sound::playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, DgdsMidiPlayer *midiPlayer) {
+void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const byte *data, int dataSz) {
+ // Mostly copied from SCI soundcmd.
+ MusicEntry *musicSlot = _music->getSlot(obj);
+
+ if (!musicSlot) {
+ error("kDoSound(play): Slot not found (%08x)", obj);
+ }
+
+ int32 resourceId;
+ if (!restoring)
+ resourceId = obj;
+ else
+ // Handle cases where a game was saved while track A was playing, but track B was initialized, waiting to be played later.
+ // In such cases, musicSlot->resourceId contains the actual track that was playing (A), while getSoundResourceId(obj)
+ // contains the track that's waiting to be played later (B) - bug #10907.
+ resourceId = musicSlot->resourceId;
+
+ if (musicSlot->resourceId != resourceId) { // another sound loaded into struct
+ processDisposeSound(obj);
+ processInitSound(obj, data, dataSz, Audio::Mixer::kSFXSoundType);
+ // Find slot again :)
+ musicSlot = _music->getSlot(obj);
+ }
+
+ musicSlot->loop = 1; // Play once by default?
+
+ // Get song priority from either obj or soundRes
+ byte resourcePriority = 0xFF;
+ if (musicSlot->soundRes)
+ resourcePriority = musicSlot->soundRes->getSoundPriority();
+ if (!musicSlot->overridePriority && resourcePriority != 0xFF) {
+ musicSlot->priority = resourcePriority;
+ } else {
+ musicSlot->priority = 0xff; // todo: priority?
+ }
+
+ // Reset hold when starting a new song. kDoSoundSetHold is always called after
+ // kDoSoundPlay to set it properly, if needed. Fixes bug #5851.
+ musicSlot->hold = -1;
+ musicSlot->playBed = playBed;
+ musicSlot->volume = MUSIC_VOLUME_DEFAULT;
+
+ debugC(kDebugLevelSound, "kDoSound(play): %08x number %d, loop %d, prio %d, vol %d, bed %d", obj,
+ resourceId, musicSlot->loop, musicSlot->priority, musicSlot->volume, playBed ? 1 : 0);
+
+ _music->soundPlay(musicSlot, restoring);
+
+ // Reset any left-over signals
+ musicSlot->signal = 0;
+ musicSlot->fadeStep = 0;
+}
+
+void Sound::playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType) {
if (_musicIdMap.size()) {
num = _musicIdMap[num];
}
+
if (num < dataArray.size()) {
uint32 tracks = _availableSndTracks(dataArray[num], sizeArray[num]);
- if (midiPlayer && (tracks & TRACK_MT32))
- midiPlayer->play(dataArray[num], sizeArray[num]);
- else if (tracks & DIGITAL_PCM)
+ if (tracks & DIGITAL_PCM) {
playPCM(dataArray[num], sizeArray[num]);
+ } else {
+ int size = sizeArray[num];
+ const byte *data = dataArray[num];
+ uint32 hdrsize = 0;
+ _readHeader(data, hdrsize);
+ size -= hdrsize;
+ int offset = soundType == Audio::Mixer::kSFXSoundType ? SND_RESOURCE_OFFSET : MUSIC_RESOURCE_OFFSET;
+ processInitSound(num + offset, data, size, soundType);
+ processPlaySound(num + offset, false, false, data, size);
+ }
} else {
warning("Sound: Requested to play %d but only have %d tracks", num, dataArray.size());
}
}
void Sound::stopMusic() {
- _midiMusicPlayer->stop();
- _mixer->stopAll();
+ _music->stopMusic();
}
void Sound::unloadMusic() {
@@ -388,58 +568,9 @@ void Sound::unloadMusic() {
delete [] data;
_musicData.clear();
- // Don't unload the sfx data.
-
- delete _soundData;
- _soundData = nullptr;
+ // Don't unload sfxData.
}
-byte loadSndTrack(uint32 track, const byte** trackPtr, uint16* trackSiz, const byte *data, uint32 size) {
- byte matchDrv;
- switch (track) {
- case DIGITAL_PCM:
- case TRACK_ADLIB: matchDrv = 0; break;
- case TRACK_GM: matchDrv = 7; break;
- case TRACK_MT32: matchDrv = 12; break;
- default: return 0;
- }
-
- const byte *pos = data;
-
- uint32 sci_header;
- _readHeader(pos, sci_header);
-
- while (pos[0] != 0xFF) {
- byte drv = *pos++;
-
- byte part;
- const byte *ptr;
-
- part = 0;
- for (ptr = pos; *ptr != 0xFF; _skipPartHeader(ptr))
- part++;
-
- if (matchDrv == drv) {
- part = 0;
- while (pos[0] != 0xFF) {
- uint16 off, siz;
- _readPartHeader(pos, off, siz);
- off += sci_header;
-
- trackPtr[part] = data + off;
- trackSiz[part] = siz;
- part++;
- }
- debug("- (%d) Play parts = %d", drv, part);
- return part;
- } else {
- pos = ptr;
- }
- pos++;
- }
- pos++;
- return 0;
-}
} // End of namespace Dgds
diff --git a/engines/dgds/sound.h b/engines/dgds/sound.h
index 07993296d33..900a7731413 100644
--- a/engines/dgds/sound.h
+++ b/engines/dgds/sound.h
@@ -31,6 +31,9 @@ namespace Dgds {
class ResourceManager;
class Decompressor;
class DgdsMidiPlayer;
+class SciMusic;
+class MusicEntry;
+class AudioPlayer;
struct Channel {
Audio::AudioStream *stream;
@@ -54,20 +57,23 @@ public:
void playSFX(uint num);
- void stopSfx(byte channel);
+ void stopSfxForChannel(byte channel);
void stopSfxByNum(uint num);
void stopAllSfx();
bool playPCM(const byte *data, uint32 size);
- DgdsMidiPlayer *getMidiPlayer() { return _midiMusicPlayer; }
-
private:
void loadPCSound(const Common::String &filename, Common::Array<uint32> &sizeArray, Common::Array<byte *> &dataArray);
- void playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, DgdsMidiPlayer *midiPlayer);
+ void playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType);
+
+ void processInitSound(uint num, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
+ void processDisposeSound(uint32 obj);
+ void processStopSound(uint32 obj, bool sampleFinishedPlaying);
+ void processPlaySound(uint32 obj, bool playBed, bool restoring, const byte *data, int dataSz);
+ void initSoundResource(MusicEntry *newSound, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
struct Channel _channels[2];
- Common::SeekableReadStream *_soundData = nullptr;
Common::Array<uint32> _musicSizes;
Common::Array<byte *> _musicData;
@@ -76,10 +82,9 @@ private:
Common::Array<uint32> _sfxSizes;
Common::Array<byte *> _sfxData;
+ SciMusic *_music;
Audio::Mixer *_mixer;
- DgdsMidiPlayer *_midiMusicPlayer;
- DgdsMidiPlayer *_midiSoundPlayer;
ResourceManager *_resource;
Decompressor *_decompressor;
};
@@ -91,8 +96,6 @@ enum {
TRACK_MT32 = 1 << 3
};
-byte loadSndTrack(uint32 track, const byte** trackPtr, uint16* trackSiz, const byte *data, uint32 size);
-
} // End of namespace Dgds
#endif // DGDS_SOUND_H
diff --git a/engines/dgds/sound/drivers/adlib.cpp b/engines/dgds/sound/drivers/adlib.cpp
new file mode 100644
index 00000000000..5b8a95cc9e8
--- /dev/null
+++ b/engines/dgds/sound/drivers/adlib.cpp
@@ -0,0 +1,957 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+//#include "dgds/sci.h"
+
+#include "common/file.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "audio/fmopl.h"
+#include "audio/mididrv.h"
+
+#include "dgds/sound/resource/sci_resource.h"
+#include "dgds/sound/drivers/mididriver.h"
+#include "dgds/sound/scispan.h"
+#include "dgds/dgds.h"
+
+namespace Dgds {
+
+#ifdef __DC__
+#define STEREO false
+#else
+#define STEREO true
+#endif
+
+class MidiDriver_AdLib : public MidiDriver {
+public:
+ enum {
+ kVoices = 9,
+ kRhythmKeys = 62
+ };
+
+ MidiDriver_AdLib() : _isSCI0(false), _playSwitch(true), _masterVolume(15),
+ _numVoiceMax(kVoices), _rhythmKeyMap(), _opl(nullptr), _adlibTimerParam(nullptr), _adlibTimerProc(nullptr), _stereo(false), _isOpen(false) { }
+ ~MidiDriver_AdLib() override { }
+
+ // MidiDriver
+ int open() override { return -1; } // Dummy implementation (use openAdLib)
+ int openAdLib();
+ void close() override;
+ void send(uint32 b) override;
+ void initTrack(SciSpan<const byte> &header);
+
+ MidiChannel *allocateChannel() override { return nullptr; }
+ MidiChannel *getPercussionChannel() override { return nullptr; }
+ bool isOpen() const override { return _isOpen; }
+ uint32 getBaseTempo() override { return 1000000 / OPL::OPL::kDefaultCallbackFrequency; }
+
+ // MidiDriver
+ void setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) override;
+
+ void onTimer();
+
+ void setVolume(byte volume);
+ void playSwitch(bool play);
+ bool loadResource(const SciSpan<const byte> &data);
+ uint32 property(int prop, uint32 param) override;
+
+ bool useRhythmChannel() const { return _rhythmKeyMap; }
+
+private:
+ enum ChannelID {
+ kLeftChannel = 1,
+ kRightChannel = 2
+ };
+
+ struct AdLibOperator {
+ bool amplitudeMod;
+ bool vibrato;
+ bool envelopeType;
+ bool kbScaleRate;
+ byte frequencyMult; // (0-15)
+ byte kbScaleLevel; // (0-3)
+ byte totalLevel; // (0-63, 0=max, 63=min)
+ byte attackRate; // (0-15)
+ byte decayRate; // (0-15)
+ byte sustainLevel; // (0-15)
+ byte releaseRate; // (0-15)
+ byte waveForm; // (0-3)
+ };
+
+ struct AdLibModulator {
+ byte feedback; // (0-7)
+ bool algorithm;
+ };
+
+ struct AdLibPatch {
+ AdLibOperator op[2];
+ AdLibModulator mod;
+ };
+
+ struct Channel {
+ uint8 patch; // Patch setting
+ uint8 volume; // Channel volume (0-63)
+ uint8 pan; // Pan setting (0-127, 64 is center)
+ uint8 holdPedal; // Hold pedal setting (0 to 63 is off, 127 to 64 is on)
+ uint8 extraVoices; // The number of additional voices this channel optimally needs
+ uint16 pitchWheel; // Pitch wheel setting (0-16383, 8192 is center)
+ uint8 lastVoice; // Last voice used for this MIDI channel
+ bool enableVelocity; // Enable velocity control (SCI0)
+ uint8 voices; // Number of voices currently used by this channel
+ uint8 mappedVoices; // Number of voices currently mapped to this channel
+
+ Channel() : patch(0), volume(63), pan(64), holdPedal(0), extraVoices(0),
+ pitchWheel(8192), lastVoice(0), enableVelocity(false), voices(0),
+ mappedVoices(0) { }
+ };
+
+ struct AdLibVoice {
+ int8 channel; // MIDI channel that is currently using this voice, or -1
+ int8 mappedChannel; // MIDI channel that this voice is mapped to, or -1
+ int8 note; // Currently playing MIDI note or -1
+ int patch; // Currently playing patch or -1
+ uint8 velocity; // Note velocity
+ bool isSustained; // Flag indicating a note that is being sustained by the hold pedal
+ uint16 age; // Age of the current note
+
+ AdLibVoice() : channel(-1), mappedChannel(-1), note(-1), patch(-1), velocity(0), isSustained(false), age(0) { }
+ };
+
+ bool _stereo;
+ bool _isSCI0;
+ OPL::OPL *_opl;
+ bool _isOpen;
+ bool _playSwitch;
+ int _masterVolume;
+ const uint8 _numVoiceMax;
+ Channel _channels[MIDI_CHANNELS];
+ AdLibVoice _voices[kVoices];
+ Common::SpanOwner<SciSpan<const byte> > _rhythmKeyMap;
+ Common::Array<AdLibPatch> _patches;
+ Common::List<int> _voiceQueue;
+
+ Common::TimerManager::TimerProc _adlibTimerProc;
+ void *_adlibTimerParam;
+
+ void loadInstrument(const SciSpan<const byte> &ins);
+ void voiceOn(int voice, int note, int velocity);
+ void voiceOff(int voice);
+ void setPatch(int voice, int patch);
+ void setNote(int voice, int note, bool key);
+ void setVelocity(int voice);
+ void setOperator(int oper, AdLibOperator &op);
+ void setRegister(int reg, int value, int channels = kLeftChannel | kRightChannel);
+ void renewNotes(int channel, bool key);
+ void noteOn(int channel, int note, int velocity);
+ void noteOff(int channel, int note);
+ int findVoice(int channel);
+ int findVoiceLateSci11(int channel);
+ void voiceMapping(int channel, int voices);
+ void assignVoices(int channel, int voices);
+ void releaseVoices(int channel, int voices);
+ void donateVoices();
+ void queueMoveToBack(int voice);
+ void setVelocityReg(int regOffset, int velocity, int kbScaleLevel, int pan);
+ int calcVelocity(int voice, int op);
+};
+
+class MidiPlayer_AdLib : public MidiPlayer {
+public:
+ MidiPlayer_AdLib() : MidiPlayer() { _driver = new MidiDriver_AdLib(); }
+ ~MidiPlayer_AdLib() override {
+ delete _driver;
+ _driver = nullptr;
+ }
+
+ int open(ResourceManager *resMan) override;
+ void close() override;
+
+ byte getPlayId() const override;
+ int getPolyphony() const override { return MidiDriver_AdLib::kVoices; }
+ bool hasRhythmChannel() const override { return false; }
+ void setVolume(byte volume) override { static_cast<MidiDriver_AdLib *>(_driver)->setVolume(volume); }
+ void playSwitch(bool play) override { static_cast<MidiDriver_AdLib *>(_driver)->playSwitch(play); }
+ void initTrack(SciSpan<const byte> &header) override { static_cast<MidiDriver_AdLib *>(_driver)->initTrack(header); }
+ int getLastChannel() const override { return (static_cast<const MidiDriver_AdLib *>(_driver)->useRhythmChannel() ? 8 : 15); }
+};
+
+static const byte registerOffset[MidiDriver_AdLib::kVoices] = {
+ 0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12
+};
+
+static const byte velocityMap1[64] = {
+ 0x00, 0x0c, 0x0d, 0x0e, 0x0f, 0x11, 0x12, 0x13,
+ 0x14, 0x16, 0x17, 0x18, 0x1a, 0x1b, 0x1c, 0x1d,
+ 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26,
+ 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2d, 0x2d, 0x2e,
+ 0x2f, 0x30, 0x31, 0x32, 0x32, 0x33, 0x34, 0x34,
+ 0x35, 0x36, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3a,
+ 0x3b, 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3d, 0x3d,
+ 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3f, 0x3f, 0x3f
+};
+
+static const byte velocityMap2[64] = {
+ 0x00, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a,
+ 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x21,
+ 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29,
+ 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x2f, 0x30,
+ 0x31, 0x32, 0x32, 0x33, 0x34, 0x34, 0x35, 0x36,
+ 0x36, 0x37, 0x38, 0x38, 0x39, 0x39, 0x3a, 0x3a,
+ 0x3b, 0x3b, 0x3b, 0x3c, 0x3c, 0x3c, 0x3d, 0x3d,
+ 0x3d, 0x3e, 0x3e, 0x3e, 0x3e, 0x3f, 0x3f, 0x3f
+};
+
+// One octave with three pitch wheel positions after each note
+static const int adlibFreq[48] = {
+ 0x157, 0x15c, 0x161, 0x166, 0x16b, 0x171, 0x176, 0x17b,
+ 0x181, 0x186, 0x18c, 0x192, 0x198, 0x19e, 0x1a4, 0x1aa,
+ 0x1b0, 0x1b6, 0x1bd, 0x1c3, 0x1ca, 0x1d0, 0x1d7, 0x1de,
+ 0x1e5, 0x1ec, 0x1f3, 0x1fa, 0x202, 0x209, 0x211, 0x218,
+ 0x220, 0x228, 0x230, 0x238, 0x241, 0x249, 0x252, 0x25a,
+ 0x263, 0x26c, 0x275, 0x27e, 0x287, 0x290, 0x29a, 0x2a4
+};
+
+int MidiDriver_AdLib::openAdLib() {
+ _stereo = STEREO;
+
+ debug(3, "ADLIB: Starting driver in %s mode", (_isSCI0 ? "SCI0" : "SCI1"));
+
+ // Fill in the voice queue
+ for (int i = 0; i < kVoices; ++i)
+ _voiceQueue.push_back(i);
+
+ _opl = OPL::Config::create(_stereo ? OPL::Config::kDualOpl2 : OPL::Config::kOpl2);
+
+ // Try falling back to mono, thus plain OPL2 emulator, when no Dual OPL2 is available.
+ if (!_opl && _stereo) {
+ _stereo = false;
+ _opl = OPL::Config::create(OPL::Config::kOpl2);
+ }
+
+ if (!_opl)
+ return -1;
+
+ if (!_opl->init()) {
+ delete _opl;
+ _opl = nullptr;
+ return -1;
+ }
+
+ setRegister(0xBD, 0);
+ setRegister(0x08, 0);
+ setRegister(0x01, 0x20);
+
+ _isOpen = true;
+
+ _opl->start(new Common::Functor0Mem<void, MidiDriver_AdLib>(this, &MidiDriver_AdLib::onTimer));
+
+ return 0;
+}
+
+void MidiDriver_AdLib::close() {
+ delete _opl;
+ _rhythmKeyMap.clear();
+}
+
+void MidiDriver_AdLib::setVolume(byte volume) {
+ _masterVolume = volume;
+ renewNotes(-1, true);
+}
+
+// MIDI messages can be found at https://web.archive.org/web/20120128110425/http://www.midi.org/techspecs/midimessages.php
+void MidiDriver_AdLib::send(uint32 b) {
+ byte command = b & 0xf0;
+ byte channel = b & 0xf;
+ byte op1 = (b >> 8) & 0xff;
+ byte op2 = (b >> 16) & 0xff;
+
+ //debug("midi send %02x %x %02x %02x", command, channel, op1, op2);
+
+ switch (command) {
+ case 0x80:
+ noteOff(channel, op1);
+ break;
+ case 0x90:
+ noteOn(channel, op1, op2);
+ break;
+ case 0xb0:
+ switch (op1) {
+ case 0x07:
+ _channels[channel].volume = op2 >> 1;
+ renewNotes(channel, true);
+ break;
+ case 0x0a:
+ _channels[channel].pan = op2;
+ renewNotes(channel, true);
+ break;
+ case 0x40:
+ _channels[channel].holdPedal = op2;
+ if (op2 == 0) {
+ for (int i = 0; i < kVoices; i++) {
+ if ((_voices[i].channel == channel) && _voices[i].isSustained)
+ voiceOff(i);
+ }
+ }
+ break;
+ case 0x4b:
+#ifndef ADLIB_DISABLE_VOICE_MAPPING
+ voiceMapping(channel, op2);
+#endif
+ break;
+ case 0x4e:
+ _channels[channel].enableVelocity = op2;
+ break;
+ case SCI_MIDI_CHANNEL_NOTES_OFF:
+ for (int i = 0; i < kVoices; i++)
+ if ((_voices[i].channel == channel) && (_voices[i].note != -1))
+ voiceOff(i);
+ break;
+ default:
+ //warning("ADLIB: ignoring MIDI command %02x %02x %02x", command | channel, op1, op2);
+ break;
+ }
+ break;
+ case 0xc0:
+ _channels[channel].patch = op1;
+ break;
+ // The original AdLib driver from sierra ignores aftertouch completely, so should we
+ case 0xa0: // Polyphonic key pressure (aftertouch)
+ case 0xd0: // Channel pressure (aftertouch)
+ break;
+ case 0xe0:
+ _channels[channel].pitchWheel = (op1 & 0x7f) | ((op2 & 0x7f) << 7);
+ renewNotes(channel, true);
+ break;
+ default:
+ warning("ADLIB: Unknown event %02x", command);
+ }
+}
+
+void MidiDriver_AdLib::initTrack(SciSpan<const byte> &header) {
+ if (!_isOpen || !_isSCI0)
+ return;
+
+ uint8 readPos = 0;
+ uint8 caps = header.getInt8At(readPos++);
+ if (caps != 0 && caps != 2)
+ return;
+
+ for (int i = 0; i < kVoices; ++i) {
+ _voices[i].channel = _voices[i].mappedChannel = _voices[i].note = -1;
+ _voices[i].isSustained = false;
+ _voices[i].patch = 13;
+ _voices[i].velocity = 0;
+ _voices[i].age = 0;
+ }
+
+ int numVoices = 0;
+ for (int i = 0; i < 16; ++i) {
+ _channels[i].patch = 13;
+ _channels[i].extraVoices = 0;
+ _channels[i].mappedVoices = 0;
+
+ uint8 val = header.getInt8At(readPos++);
+ if (val & 0x01) {
+ uint8 num = val >> 4;
+ if (!(val & 0x08) && num && num != 0x0F) {
+ while (num--) {
+ if (numVoices >= _numVoiceMax)
+ continue;
+ _voices[numVoices++].mappedChannel = i;
+ _channels[i].mappedVoices++;
+ }
+ }
+ } else if (val & 0x08) {
+ debugC(9, kDebugLevelSound, "MidiDriver_AdLib::initTrack(): Control channel found: 0x%.02x", i);
+ }
+ }
+}
+
+void MidiDriver_AdLib::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) {
+ _adlibTimerProc = timerProc;
+ _adlibTimerParam = timerParam;
+}
+
+void MidiDriver_AdLib::onTimer() {
+ if (_adlibTimerProc)
+ (*_adlibTimerProc)(_adlibTimerParam);
+
+ // Increase the age of the notes
+ for (int i = 0; i < kVoices; i++) {
+ if (_voices[i].note != -1)
+ _voices[i].age++;
+ }
+}
+
+void MidiDriver_AdLib::loadInstrument(const SciSpan<const byte> &ins) {
+ AdLibPatch patch;
+
+ // Set data for the operators
+ for (int i = 0; i < 2; i++) {
+ const byte *op = ins.getUnsafeDataAt(i * 13, 13);
+ patch.op[i].kbScaleLevel = op[0] & 0x3;
+ patch.op[i].frequencyMult = op[1] & 0xf;
+ patch.op[i].attackRate = op[3] & 0xf;
+ patch.op[i].sustainLevel = op[4] & 0xf;
+ patch.op[i].envelopeType = op[5];
+ patch.op[i].decayRate = op[6] & 0xf;
+ patch.op[i].releaseRate = op[7] & 0xf;
+ patch.op[i].totalLevel = op[8] & 0x3f;
+ patch.op[i].amplitudeMod = op[9];
+ patch.op[i].vibrato = op[10];
+ patch.op[i].kbScaleRate = op[11];
+ }
+ patch.op[0].waveForm = ins[26] & 0x3;
+ patch.op[1].waveForm = ins[27] & 0x3;
+
+ // Set data for the modulator
+ patch.mod.feedback = ins[2] & 0x7;
+ patch.mod.algorithm = !ins[12]; // Flag is inverted
+
+ _patches.push_back(patch);
+}
+
+void MidiDriver_AdLib::voiceMapping(int channel, int voices) {
+ int curVoices = 0;
+
+ for (int i = 0; i < _numVoiceMax; i++)
+ if (_voices[i].mappedChannel == channel)
+ curVoices++;
+
+ curVoices += _channels[channel].extraVoices;
+
+ if (curVoices < voices) {
+ debug(3, "ADLIB: assigning %i additional voices to channel %i", voices - curVoices, channel);
+ assignVoices(channel, voices - curVoices);
+ } else if (curVoices > voices) {
+ debug(3, "ADLIB: releasing %i voices from channel %i", curVoices - voices, channel);
+ releaseVoices(channel, curVoices - voices);
+ donateVoices();
+ }
+}
+
+void MidiDriver_AdLib::assignVoices(int channel, int voices) {
+ assert(voices > 0);
+
+ for (int i = 0; i < _numVoiceMax; i++)
+ if (_voices[i].mappedChannel == -1) {
+ if (_voices[i].note != -1) // Late SCI1.1, stop note on unmapped channel
+ voiceOff(i);
+ _voices[i].mappedChannel = channel;
+ ++_channels[channel].mappedVoices;
+ if (--voices == 0)
+ return;
+ }
+
+ // This is already too advanced for SCI0...
+ if (!_isSCI0)
+ _channels[channel].extraVoices += voices;
+}
+
+void MidiDriver_AdLib::releaseVoices(int channel, int voices) {
+ if (_channels[channel].extraVoices >= voices) {
+ _channels[channel].extraVoices -= voices;
+ return;
+ }
+
+ voices -= _channels[channel].extraVoices;
+ _channels[channel].extraVoices = 0;
+
+ for (int i = 0; i < _numVoiceMax; i++) {
+ if ((_voices[i].mappedChannel == channel) && (_voices[i].note == -1)) {
+ _voices[i].mappedChannel = -1;
+ --_channels[channel].mappedVoices;
+ if (--voices == 0)
+ return;
+ }
+ }
+
+ for (int i = 0; i < _numVoiceMax; i++) {
+ if (_voices[i].mappedChannel == channel) {
+ voiceOff(i);
+ _voices[i].mappedChannel = -1;
+ --_channels[channel].mappedVoices;
+ if (--voices == 0)
+ return;
+ }
+ }
+}
+
+void MidiDriver_AdLib::donateVoices() {
+ if (_isSCI0)
+ return;
+
+ int freeVoices = 0;
+
+ for (int i = 0; i < kVoices; i++)
+ if (_voices[i].mappedChannel == -1)
+ freeVoices++;
+
+ if (freeVoices == 0)
+ return;
+
+ for (int i = 0; i < MIDI_CHANNELS; i++) {
+ if (_channels[i].extraVoices >= freeVoices) {
+ assignVoices(i, freeVoices);
+ _channels[i].extraVoices -= freeVoices;
+ return;
+ } else if (_channels[i].extraVoices > 0) {
+ assignVoices(i, _channels[i].extraVoices);
+ freeVoices -= _channels[i].extraVoices;
+ _channels[i].extraVoices = 0;
+ }
+ }
+}
+
+void MidiDriver_AdLib::renewNotes(int channel, bool key) {
+ for (int i = 0; i < kVoices; i++) {
+ // Update all notes playing this channel
+ if ((channel == -1) || (_voices[i].channel == channel)) {
+ if (_voices[i].note != -1)
+ setNote(i, _voices[i].note, key);
+ }
+ }
+}
+
+void MidiDriver_AdLib::noteOn(int channel, int note, int velocity) {
+ if (velocity == 0)
+ return noteOff(channel, note);
+
+ velocity >>= 1;
+
+ // Check for playable notes
+ if ((note < 12) || (note > 107))
+ return;
+
+ for (int i = 0; i < kVoices; i++) {
+ if ((_voices[i].channel == channel) && (_voices[i].note == note)) {
+ voiceOff(i);
+ voiceOn(i, note, velocity);
+ return;
+ }
+ }
+
+ int voice = _rhythmKeyMap ? findVoiceLateSci11(channel) : findVoice(channel);
+
+ if (voice == -1) {
+ debug(3, "ADLIB: failed to find free voice assigned to channel %i", channel);
+ return;
+ }
+
+ voiceOn(voice, note, velocity);
+}
+
+int MidiDriver_AdLib::findVoice(int channel) {
+ int voice = -1;
+ int oldestVoice = -1;
+ uint32 oldestAge = 0;
+
+ // Try to find a voice assigned to this channel that is free (round-robin)
+ for (int i = 0; i < kVoices; i++) {
+ int v = (_channels[channel].lastVoice + i + 1) % kVoices;
+
+ if (_voices[v].mappedChannel == channel) {
+ if (_voices[v].note == -1) {
+ voice = v;
+ _voices[voice].channel = channel;
+ break;
+ }
+
+ // We also keep track of the oldest note in case the search fails
+ // Notes started in the current time slice will not be selected
+ if (_voices[v].age >= oldestAge) {
+ oldestAge = _voices[v].age;
+ oldestVoice = v;
+ }
+ }
+ }
+
+ if (voice == -1) {
+ if (!oldestAge)
+ return -1;
+ voiceOff(oldestVoice);
+ voice = oldestVoice;
+ _voices[voice].channel = channel;
+ }
+
+ _channels[channel].lastVoice = voice;
+
+ return voice;
+}
+
+int MidiDriver_AdLib::findVoiceLateSci11(int channel) {
+ Common::List<int>::const_iterator it;
+
+ // Search for unused voice
+ for (it = _voiceQueue.begin(); it != _voiceQueue.end(); ++it) {
+ int voice = *it;
+ if (_voices[voice].note == -1 && _voices[voice].patch == _channels[channel].patch) {
+ _voices[voice].channel = channel;
+ return voice;
+ }
+ }
+
+ // Same as before, minus the program check
+ for (it = _voiceQueue.begin(); it != _voiceQueue.end(); ++it) {
+ int voice = *it;
+ if (_voices[voice].note == -1) {
+ _voices[voice].channel = channel;
+ return voice;
+ }
+ }
+
+ // Search for channel with highest excess of voices
+ int maxExceed = 0;
+ int maxExceedChan = 0;
+ for (uint i = 0; i < MIDI_CHANNELS; ++i) {
+ if (_channels[i].voices > _channels[i].mappedVoices) {
+ int exceed = _channels[i].voices - _channels[i].mappedVoices;
+ if (exceed > maxExceed) {
+ maxExceed = exceed;
+ maxExceedChan = i;
+ }
+ }
+ }
+
+ // Stop voice on channel with highest excess if possible, otherwise stop
+ // note on this channel.
+ int stopChan = (maxExceed > 0) ? maxExceedChan : channel;
+
+ for (it = _voiceQueue.begin(); it != _voiceQueue.end(); ++it) {
+ int voice = *it;
+ if (_voices[voice].channel == stopChan) {
+ voiceOff(voice);
+ _voices[voice].channel = channel;
+ return voice;
+ }
+ }
+
+ return -1;
+}
+
+void MidiDriver_AdLib::queueMoveToBack(int voice) {
+ _voiceQueue.remove(voice);
+ _voiceQueue.push_back(voice);
+}
+
+void MidiDriver_AdLib::noteOff(int channel, int note) {
+ for (int i = 0; i < kVoices; i++) {
+ if ((_voices[i].channel == channel) && (_voices[i].note == note)) {
+ if (_channels[channel].holdPedal)
+ _voices[i].isSustained = true;
+ else
+ voiceOff(i);
+ return;
+ }
+ }
+}
+
+void MidiDriver_AdLib::voiceOn(int voice, int note, int velocity) {
+ int channel = _voices[voice].channel;
+ int patch = _channels[channel].patch;
+
+ _voices[voice].age = 0;
+ ++_channels[channel].voices;
+ queueMoveToBack(voice);
+
+ if ((channel == 9) && _rhythmKeyMap) {
+ patch = CLIP(note, 27, 88) + 101;
+ }
+
+ // Set patch if different from current patch
+ if (patch != _voices[voice].patch && _playSwitch)
+ setPatch(voice, patch);
+
+ _voices[voice].velocity = velocity;
+ setNote(voice, note, true);
+}
+
+void MidiDriver_AdLib::voiceOff(int voice) {
+ int channel = _voices[voice].channel;
+
+ _voices[voice].isSustained = false;
+ setNote(voice, _voices[voice].note, 0);
+ _voices[voice].note = -1;
+ _voices[voice].age = 0;
+ queueMoveToBack(voice);
+ --_channels[channel].voices;
+}
+
+void MidiDriver_AdLib::setNote(int voice, int note, bool key) {
+ int channel = _voices[voice].channel;
+
+ if ((channel == 9) && _rhythmKeyMap)
+ note = _rhythmKeyMap[CLIP(note, 27, 88) - 27];
+
+ _voices[voice].note = note;
+
+ int index = note << 2;
+ uint16 pitchWheel = _channels[channel].pitchWheel;
+ int sign;
+
+ if (pitchWheel == 0x2000) {
+ pitchWheel = 0;
+ sign = 0;
+ } else if (pitchWheel > 0x2000) {
+ pitchWheel -= 0x2000;
+ sign = 1;
+ } else {
+ pitchWheel = 0x2000 - pitchWheel;
+ sign = -1;
+ }
+
+ pitchWheel /= 171;
+
+ if (sign == 1)
+ index += pitchWheel;
+ else
+ index -= pitchWheel;
+
+ if (index > 0x1fc) // Limit to max MIDI note (<< 2)
+ index = 0x1fc;
+
+ if (index < 0) // Not in SSCI
+ index = 0;
+
+ int freq = adlibFreq[index % 48];
+
+ setRegister(0xA0 + voice, freq & 0xff);
+
+ int oct = index / 48;
+ if (oct > 0)
+ --oct;
+
+ if (oct > 7) // Not in SSCI
+ oct = 7;
+
+ setRegister(0xB0 + voice, (key << 5) | (oct << 2) | (freq >> 8));
+ setVelocity(voice);
+}
+
+void MidiDriver_AdLib::setVelocity(int voice) {
+ AdLibPatch &patch = _patches[_voices[voice].patch];
+ int pan = _channels[_voices[voice].channel].pan;
+ setVelocityReg(registerOffset[voice] + 3, calcVelocity(voice, 1), patch.op[1].kbScaleLevel, pan);
+
+ // In AM mode we need to set the level for both operators
+ if (_patches[_voices[voice].patch].mod.algorithm == 1)
+ setVelocityReg(registerOffset[voice], calcVelocity(voice, 0), patch.op[0].kbScaleLevel, pan);
+}
+
+int MidiDriver_AdLib::calcVelocity(int voice, int op) {
+ if (_isSCI0) {
+ int velocity = _masterVolume;
+
+ if (velocity > 0)
+ velocity += 3;
+
+ if (velocity > 15)
+ velocity = 15;
+
+ int insVelocity;
+ if (_channels[_voices[voice].channel].enableVelocity)
+ insVelocity = _voices[voice].velocity;
+ else
+ insVelocity = 63 - _patches[_voices[voice].patch].op[op].totalLevel;
+
+ // Note: Later SCI0 has a static table that is close to this formula, but not exactly the same.
+ // Early SCI0 does (velocity * (insVelocity / 15))
+ return velocity * insVelocity / 15;
+ } else {
+ AdLibOperator &oper = _patches[_voices[voice].patch].op[op];
+ int velocity = _channels[_voices[voice].channel].volume + 1;
+ velocity = velocity * (velocityMap1[_voices[voice].velocity] + 1) / 64;
+ velocity = velocity * (_masterVolume + 1) / 16;
+
+ if (--velocity < 0)
+ velocity = 0;
+
+ return velocityMap2[velocity] * (63 - oper.totalLevel) / 63;
+ }
+}
+
+void MidiDriver_AdLib::setVelocityReg(int regOffset, int velocity, int kbScaleLevel, int pan) {
+ if (!_playSwitch)
+ velocity = 0;
+
+ if (_stereo) {
+ int velLeft = velocity;
+ int velRight = velocity;
+
+ if (pan > 0x40)
+ velLeft = velLeft * (0x7f - pan) / 0x3f;
+ else if (pan < 0x40)
+ velRight = velRight * pan / 0x40;
+
+ setRegister(0x40 + regOffset, (kbScaleLevel << 6) | (63 - velLeft), kLeftChannel);
+ setRegister(0x40 + regOffset, (kbScaleLevel << 6) | (63 - velRight), kRightChannel);
+ } else {
+ setRegister(0x40 + regOffset, (kbScaleLevel << 6) | (63 - velocity));
+ }
+}
+
+void MidiDriver_AdLib::setPatch(int voice, int patch) {
+ if ((patch < 0) || ((uint)patch >= _patches.size())) {
+ warning("ADLIB: Invalid patch %i requested", patch);
+ // Substitute instrument 0
+ patch = 0;
+ }
+
+ _voices[voice].patch = patch;
+ AdLibModulator &mod = _patches[patch].mod;
+
+ // Set the common settings for both operators
+ setOperator(registerOffset[voice], _patches[patch].op[0]);
+ setOperator(registerOffset[voice] + 3, _patches[patch].op[1]);
+
+ // Set the additional settings for the modulator
+ byte algorithm = mod.algorithm ? 1 : 0;
+ setRegister(0xC0 + voice, (mod.feedback << 1) | algorithm);
+}
+
+void MidiDriver_AdLib::setOperator(int reg, AdLibOperator &op) {
+ setRegister(0x40 + reg, (op.kbScaleLevel << 6) | op.totalLevel);
+ setRegister(0x60 + reg, (op.attackRate << 4) | op.decayRate);
+ setRegister(0x80 + reg, (op.sustainLevel << 4) | op.releaseRate);
+ setRegister(0x20 + reg, (op.amplitudeMod << 7) | (op.vibrato << 6)
+ | (op.envelopeType << 5) | (op.kbScaleRate << 4) | op.frequencyMult);
+ setRegister(0xE0 + reg, op.waveForm);
+}
+
+void MidiDriver_AdLib::setRegister(int reg, int value, int channels) {
+ if (channels & kLeftChannel) {
+ _opl->write(0x220, reg);
+ _opl->write(0x221, value);
+ }
+
+ if (_stereo) {
+ if (channels & kRightChannel) {
+ _opl->write(0x222, reg);
+ _opl->write(0x223, value);
+ }
+ }
+}
+
+void MidiDriver_AdLib::playSwitch(bool play) {
+ _playSwitch = play;
+ renewNotes(-1, play);
+}
+
+bool MidiDriver_AdLib::loadResource(const SciSpan<const byte> &data) {
+ const uint32 size = data.size();
+ if (size != 1344 && size != 2690 && size != 5382) {
+ error("ADLIB: Unsupported patch format (%u bytes)", size);
+ return false;
+ }
+
+ for (int i = 0; i < 48; i++)
+ loadInstrument(data.subspan(28 * i));
+
+ if (size == 1344) {
+ byte dummy[28] = {0};
+
+ // Only 48 instruments, add dummies
+ for (int i = 0; i < 48; i++)
+ loadInstrument(SciSpan<const byte>(dummy, sizeof(dummy)));
+ } else if (size == 2690) {
+ for (int i = 48; i < 96; i++)
+ loadInstrument(data.subspan(2 + (28 * i)));
+ } else {
+ // SCI1.1 and later
+ for (int i = 48; i < 190; i++) {
+ loadInstrument(data.subspan(28 * i));
+ }
+
+ _rhythmKeyMap->allocateFromSpan(data.subspan(5320, kRhythmKeys));
+ }
+
+ return true;
+}
+
+uint32 MidiDriver_AdLib::property(int prop, uint32 param) {
+ switch(prop) {
+ case MIDI_PROP_MASTER_VOLUME:
+ if (param != 0xffff)
+ _masterVolume = param;
+ return _masterVolume;
+ default:
+ break;
+ }
+ return 0;
+}
+
+
+int MidiPlayer_AdLib::open(ResourceManager *resMan) {
+ assert(resMan != nullptr);
+
+ // Load up the patch.003 file, parse out the instruments
+ SciResource *res = getMidiPatchData(3);
+ bool ok = false;
+
+ if (res) {
+ ok = static_cast<MidiDriver_AdLib *>(_driver)->loadResource(*res);
+ } else {
+ // Early SCI0 games have the sound bank embedded in the AdLib driver
+
+ Common::File f;
+
+ if (f.open("ADL.DRV")) {
+ int size = f.size();
+ const uint patchSize = 1344;
+
+ // Note: Funseeker's Guide also has another version of adl.drv, 8803 bytes.
+ // This isn't supported, but it's not really used anywhere, as that demo
+ // doesn't have sound anyway.
+ if (size == 5684 || size == 5720 || size == 5727) {
+ ok = f.seek(0x45a);
+ if (ok) {
+ Common::SpanOwner<SciSpan<const byte> > patchData;
+ patchData->allocateFromStream(f, patchSize);
+ ok = static_cast<MidiDriver_AdLib *>(_driver)->loadResource(*patchData);
+ }
+ }
+ }
+ }
+
+ if (!ok) {
+ warning("ADLIB: Failed to load patch.003");
+ return -1;
+ }
+
+ return static_cast<MidiDriver_AdLib *>(_driver)->openAdLib();
+}
+
+void MidiPlayer_AdLib::close() {
+ if (_driver) {
+ _driver->close();
+ }
+}
+
+byte MidiPlayer_AdLib::getPlayId() const {
+ return 0x00;
+}
+
+MidiPlayer *MidiPlayer_AdLib_create() {
+ return new MidiPlayer_AdLib();
+}
+
+} // End of namespace Dgds
diff --git a/engines/dgds/sound/drivers/amigamac1.cpp b/engines/dgds/sound/drivers/amigamac1.cpp
new file mode 100644
index 00000000000..eff24e04ece
--- /dev/null
+++ b/engines/dgds/sound/drivers/amigamac1.cpp
@@ -0,0 +1,1317 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "dgds/sound/drivers/mididriver.h"
+#include "dgds/sound/drivers/macmixer.h"
+#include "dgds/sound/resource/sci_resource.h"
+#include "dgds/dgds.h"
+
+#include "audio/mixer.h"
+#include "audio/mods/paula.h"
+#include "common/array.h"
+#include "common/debug-channels.h"
+#include "common/hashmap.h"
+#include "common/memstream.h"
+#include "common/mutex.h"
+#include "common/stream.h"
+#include "common/util.h"
+
+namespace Dgds {
+
+class MidiPlayer_AmigaMac1 : public MidiPlayer {
+public:
+ enum {
+ kVoices = 4,
+ kFreqTableSize = 56,
+ kBaseFreq = 60
+ };
+
+ enum kEnvState {
+ kEnvStateAttack,
+ kEnvStateDecay,
+ kEnvStateSustain,
+ kEnvStateRelease
+ };
+
+ MidiPlayer_AmigaMac1(Audio::Mixer *mixer, uint extraSamples, bool wantSignedSamples, Common::Mutex &mutex);
+ virtual ~MidiPlayer_AmigaMac1();
+
+ // MidiPlayer
+ void close() override;
+ void send(uint32 b) override;
+ void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override;
+ uint32 getBaseTempo() override { return (1000000 + kBaseFreq / 2) / kBaseFreq; }
+ byte getPlayId() const override { return 0x06; }
+ int getPolyphony() const override { return kVoices; }
+ bool hasRhythmChannel() const override { return false; }
+ void setVolume(byte volume) override;
+ int getVolume() override;
+ void playSwitch(bool play) override;
+
+protected:
+ struct Wave {
+ Wave() : name(), phase1Start(0), phase1End(0), phase2Start(0), phase2End(0),
+ nativeNote(0), freqTable(nullptr), samples(nullptr), size(0) {}
+
+ ~Wave() {
+ delete[] samples;
+ }
+
+ char name[9];
+ uint16 phase1Start, phase1End;
+ uint16 phase2Start, phase2End;
+ uint16 nativeNote;
+
+ // This table contains frequency data for about one octave with 3 pitch bend positions between semitones
+ // On Mac, this table contains a fixed-point source-samples-per-output-sample value
+ // On Amiga, this table contains how many clock cycles each source sample should be output
+ const uint32 *freqTable;
+ const byte *samples;
+ uint32 size;
+ };
+
+ struct NoteRange {
+ NoteRange() : startNote(0), endNote(0), wave(nullptr), transpose(0), attackSpeed(0),
+ attackTarget(0), decaySpeed(0), decayTarget(0), releaseSpeed(0), fixedNote(0),
+ loop(false) {}
+
+ int16 startNote;
+ int16 endNote;
+
+ const Wave *wave;
+
+ int16 transpose;
+
+ byte attackSpeed;
+ byte attackTarget;
+ byte decaySpeed;
+ byte decayTarget;
+ byte releaseSpeed;
+
+ int16 fixedNote;
+ bool loop;
+ };
+
+ struct Instrument {
+ Instrument() : name() {}
+
+ char name[9];
+ Common::Array<NoteRange> noteRange;
+ };
+
+ Common::Array<const Instrument *> _instruments;
+ typedef Common::HashMap<uint32, const Wave *> WaveMap;
+ WaveMap _waves;
+ typedef Common::HashMap<uint32, const uint32 *> FreqTableMap;
+ FreqTableMap _freqTables;
+
+ bool _playSwitch;
+ uint _masterVolume;
+
+ Audio::Mixer *_mixer;
+ Audio::SoundHandle _mixerSoundHandle;
+ Common::TimerManager::TimerProc _timerProc;
+ void *_timerParam;
+ bool _isOpen;
+
+ uint32 *loadFreqTable(Common::SeekableReadStream &stream);
+ const Wave *loadWave(Common::SeekableReadStream &stream, bool isEarlyPatch);
+ bool loadInstruments(Common::SeekableReadStream &patch, bool isEarlyPatch);
+ void freeInstruments();
+ void distributeVoices();
+ void onTimer();
+
+ class Channel;
+ class Voice {
+ public:
+ Voice(MidiPlayer_AmigaMac1 &driver, byte id) :
+ _channel(nullptr),
+ _note(-1),
+ _velocity(0),
+ _isReleased(false),
+ _isSustained(false),
+ _ticks(0),
+ _releaseTicks(0),
+ _envState(kEnvStateAttack),
+ _envCurVel(0),
+ _envCntDown(0),
+ _noteRange(nullptr),
+ _wave(nullptr),
+ _freqTable(nullptr),
+ _id(id),
+ _driver(driver) {}
+
+ virtual ~Voice() {}
+
+ void noteOn(int8 note, int8 velocity);
+ void noteOff();
+
+ virtual void play(int8 note, int8 velocity) = 0;
+ virtual void stop() = 0;
+ virtual void setVolume(byte volume) = 0;
+ virtual bool calcVoiceStep() = 0;
+
+ void calcMixVelocity();
+ void processEnvelope();
+
+ Channel *_channel;
+ int8 _note;
+ byte _velocity;
+ bool _isReleased;
+ bool _isSustained;
+ uint16 _ticks;
+ uint16 _releaseTicks;
+
+ kEnvState _envState;
+ int8 _envCurVel;
+ byte _envCntDown;
+
+ const NoteRange *_noteRange;
+ const Wave *_wave;
+ const uint32 *_freqTable;
+ const byte _id;
+
+ private:
+ MidiPlayer_AmigaMac1 &_driver;
+ };
+
+ Common::Array<Voice *> _voices;
+ typedef Common::Array<Voice *>::const_iterator VoiceIt;
+
+ class Channel {
+ public:
+ Channel(MidiPlayer_AmigaMac1 &driver) :
+ _patch(0),
+ _pitch(0x2000),
+ _hold(false),
+ _pan(64),
+ _volume(63),
+ _lastVoiceIt(driver._voices.begin()),
+ _extraVoices(0),
+ _driver(driver) {}
+
+ void noteOn(int8 note, int8 velocity);
+ void noteOff(int8 note);
+
+ Voice *findVoice();
+ void voiceMapping(byte voices);
+ void assignVoices(byte voices);
+ void releaseVoices(byte voices);
+ void changePatch(int8 patch);
+ void holdPedal(int8 pedal);
+ void setPitchWheel(uint16 pitch);
+
+ int8 _patch;
+ uint16 _pitch;
+ bool _hold;
+ int8 _pan;
+ int8 _volume;
+ VoiceIt _lastVoiceIt;
+ byte _extraVoices;
+
+ private:
+ MidiPlayer_AmigaMac1 &_driver;
+ };
+
+ Common::Array<Channel *> _channels;
+ typedef Common::Array<Channel *>::const_iterator ChanIt;
+
+ static const byte _envSpeedToStep[32];
+ static const byte _envSpeedToSkip[32];
+ static const byte _velocityMap[64];
+
+ const uint _extraSamples;
+ const bool _wantSignedSamples;
+
+ Common::Mutex &_mixMutex;
+ Common::Mutex _timerMutex;
+};
+
+MidiPlayer_AmigaMac1::MidiPlayer_AmigaMac1(Audio::Mixer *mixer, uint extraSamples, bool wantSignedSamples, Common::Mutex &mutex) :
+ MidiPlayer(),
+ _playSwitch(true),
+ _masterVolume(15),
+ _mixer(mixer),
+ _mixerSoundHandle(),
+ _timerProc(),
+ _timerParam(nullptr),
+ _isOpen(false),
+ _extraSamples(extraSamples),
+ _wantSignedSamples(wantSignedSamples),
+ _mixMutex(mutex) {
+
+ assert(_extraSamples > 0);
+}
+
+MidiPlayer_AmigaMac1::~MidiPlayer_AmigaMac1() {
+ close();
+}
+
+void MidiPlayer_AmigaMac1::close() {
+ if (!_isOpen)
+ return;
+
+ _mixer->stopHandle(_mixerSoundHandle);
+
+ for (ChanIt c = _channels.begin(); c != _channels.end(); ++c)
+ delete *c;
+ _channels.clear();
+
+ for (VoiceIt v = _voices.begin(); v != _voices.end(); ++v)
+ delete *v;
+ _voices.clear();
+
+ freeInstruments();
+
+ _isOpen = false;
+}
+
+void MidiPlayer_AmigaMac1::setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) {
+ Common::StackLock lock(_timerMutex);
+ _timerProc = timer_proc;
+ _timerParam = timer_param;
+}
+
+void MidiPlayer_AmigaMac1::setVolume(byte volume) {
+ Common::StackLock lock(_mixMutex);
+ _masterVolume = volume;
+}
+
+int MidiPlayer_AmigaMac1::getVolume() {
+ Common::StackLock lock(_mixMutex);
+ return _masterVolume;
+}
+
+void MidiPlayer_AmigaMac1::playSwitch(bool play) {
+ Common::StackLock lock(_mixMutex);
+ _playSwitch = play;
+}
+
+uint32 *MidiPlayer_AmigaMac1::loadFreqTable(Common::SeekableReadStream &stream) {
+ uint32 *freqTable = new ufrac_t[kFreqTableSize];
+
+ for (uint i = 0; i < kFreqTableSize; ++i)
+ freqTable[i] = stream.readUint32BE();
+
+ return freqTable;
+}
+
+const MidiPlayer_AmigaMac1::Wave *MidiPlayer_AmigaMac1::loadWave(Common::SeekableReadStream &stream, bool isEarlyPatch) {
+ Wave *wave = new Wave();
+
+ stream.read(wave->name, 8);
+ wave->name[8] = 0;
+
+ bool isSigned = true;
+ if (!isEarlyPatch)
+ isSigned = stream.readUint16BE();
+
+ wave->phase1Start = stream.readUint16BE();
+ wave->phase1End = stream.readUint16BE();
+ wave->phase2Start = stream.readUint16BE();
+ wave->phase2End = stream.readUint16BE();
+ wave->nativeNote = stream.readUint16BE();
+ const uint32 freqTableOffset = stream.readUint32BE();
+
+ // Sanity checks of segment offsets
+ if ((wave->phase2End & ~1) > wave->phase1End || wave->phase1Start > wave->phase1End || wave->phase2Start > wave->phase2End)
+ error("MidiPlayer_AmigaMac1: Invalid segment offsets found for wave '%s'", wave->name);
+
+ // On Mac, 1480 additional samples are present, rounded up to the next word boundary
+ // This allows for a maximum step of 8 during sample generation without bounds checking
+ // On Amiga, 224 additional samples are present
+ wave->size = ((wave->phase1End + 1) + _extraSamples + 1) & ~1;
+ byte *samples = new byte[wave->size];
+ stream.read(samples, wave->size);
+ wave->samples = samples;
+
+ if (_wantSignedSamples && !isSigned) {
+ // The original code uses a signed 16-bit type here, while some samples
+ // exceed INT_MAX in size. In this case, it will change one "random" byte
+ // in memory and then stop converting. We simulate this behaviour here, minus
+ // the memory corruption.
+ // The "maincrnh" instrument in Castle of Dr. Brain has an incorrect signedness
+ // flag, but is not actually converted because of its size.
+
+ if (wave->phase1End + _extraSamples <= 0x8000) {
+ for (uint32 i = 0; i < wave->size; ++i)
+ samples[i] -= 0x80;
+ } else {
+ debugC(kDebugLevelSound, "MidiPlayer_AmigaMac1: Skipping sign conversion for wave '%s' of size %d bytes", wave->name, wave->size);
+ }
+ }
+
+ if (!_freqTables.contains(freqTableOffset)) {
+ stream.seek(freqTableOffset);
+ _freqTables[freqTableOffset] = loadFreqTable(stream);
+ }
+
+ wave->freqTable = _freqTables[freqTableOffset];
+ return wave;
+}
+
+bool MidiPlayer_AmigaMac1::loadInstruments(Common::SeekableReadStream &patch, bool isEarlyPatch) {
+ _instruments.resize(128);
+
+ for (uint patchIdx = 0; patchIdx < 128; ++patchIdx) {
+ patch.seek(patchIdx * 4);
+ uint32 offset = patch.readUint32BE();
+
+ if (offset == 0)
+ continue;
+
+ Instrument *instrument = new Instrument();
+
+ patch.seek(offset);
+ patch.read(instrument->name, 8);
+ instrument->name[8] = 0;
+ patch.skip(2); // Unknown
+
+ debugC(kDebugLevelSound, "Instrument[%d]: '%s'", patchIdx, instrument->name);
+
+ while (1) {
+ NoteRange noteRange;
+
+ noteRange.startNote = patch.readUint16BE();
+
+ if (patch.err() || patch.eos()) {
+ if (_instruments[patchIdx] != instrument) {
+ delete instrument;
+ }
+ return false;
+ }
+
+ if (noteRange.startNote == -1)
+ break;
+
+ noteRange.endNote = patch.readUint16BE();
+
+ const uint32 waveOffset = patch.readUint32BE();
+
+ noteRange.transpose = patch.readSint16BE();
+
+ noteRange.attackSpeed = patch.readByte();
+ noteRange.attackTarget = patch.readByte();
+ noteRange.decaySpeed = patch.readByte();
+ noteRange.decayTarget = patch.readByte();
+ noteRange.releaseSpeed = patch.readByte();
+
+ patch.skip(1); // Probably releaseTarget, unused
+ noteRange.fixedNote = patch.readSint16BE();
+ noteRange.loop = !patch.readUint16BE();
+
+ int32 nextNoteRangePos = patch.pos();
+
+ if (!_waves.contains(waveOffset)) {
+ patch.seek(waveOffset);
+ _waves[waveOffset] = loadWave(patch, isEarlyPatch);
+ }
+
+ noteRange.wave = _waves[waveOffset];
+
+ debugC(kDebugLevelSound, "\tNotes %d-%d", noteRange.startNote, noteRange.endNote);
+ debugC(kDebugLevelSound, "\t\tWave: '%s'", noteRange.wave->name);
+ debugC(kDebugLevelSound, "\t\t\tSegment 1: %d-%d", noteRange.wave->phase1Start, noteRange.wave->phase1End);
+ debugC(kDebugLevelSound, "\t\t\tSegment 2: %d-%d", noteRange.wave->phase2Start, noteRange.wave->phase2End);
+ debugC(kDebugLevelSound, "\t\tTranspose = %d, Fixed note = %d, Loop = %d", noteRange.transpose, noteRange.fixedNote, noteRange.loop);
+ debugC(kDebugLevelSound, "\t\tAttack: %d delta, %d target", noteRange.attackSpeed, noteRange.attackTarget);
+ debugC(kDebugLevelSound, "\t\tDecay: %d delta, %d target", noteRange.decaySpeed, noteRange.decayTarget);
+ debugC(kDebugLevelSound, "\t\tRelease: %d delta, %d target", noteRange.releaseSpeed, 0);
+ debugC(kDebugLevelSound, "\t\tRelease: %d delta, %d target", noteRange.releaseSpeed, 0);
+
+ instrument->noteRange.push_back(noteRange);
+
+ _instruments[patchIdx] = instrument;
+ patch.seek(nextNoteRangePos);
+ }
+ }
+
+ return true;
+}
+
+void MidiPlayer_AmigaMac1::freeInstruments() {
+ for (WaveMap::iterator it = _waves.begin(); it != _waves.end(); ++it)
+ delete it->_value;
+ _waves.clear();
+
+ for (FreqTableMap::iterator it = _freqTables.begin(); it != _freqTables.end(); ++it)
+ delete[] it->_value;
+ _freqTables.clear();
+
+ for (Common::Array<const Instrument *>::iterator it = _instruments.begin(); it != _instruments.end(); ++it)
+ delete *it;
+ _instruments.clear();
+}
+
+void MidiPlayer_AmigaMac1::onTimer() {
+ _mixMutex.unlock();
+ _timerMutex.lock();
+
+ if (_timerProc)
+ (*_timerProc)(_timerParam);
+
+ _timerMutex.unlock();
+ _mixMutex.lock();
+
+ for (VoiceIt it = _voices.begin(); it != _voices.end(); ++it) {
+ Voice *v = *it;
+ if (v->_note != -1) {
+ ++v->_ticks;
+ if (v->_isReleased)
+ ++v->_releaseTicks;
+ v->processEnvelope();
+ v->calcMixVelocity();
+ }
+ }
+}
+
+MidiPlayer_AmigaMac1::Voice *MidiPlayer_AmigaMac1::Channel::findVoice() {
+ assert(_lastVoiceIt != _driver._voices.end());
+
+ VoiceIt voiceIt = _lastVoiceIt;
+ uint16 maxTicks = 0;
+ VoiceIt maxTicksVoiceIt = _driver._voices.end();
+
+ do {
+ ++voiceIt;
+
+ if (voiceIt == _driver._voices.end())
+ voiceIt = _driver._voices.begin();
+
+ Voice *v = *voiceIt;
+
+ if (v->_channel == this) {
+ if (v->_note == -1) {
+ _lastVoiceIt = voiceIt;
+ return v;
+ }
+
+ uint16 ticks;
+
+ if (v->_releaseTicks != 0)
+ ticks = v->_releaseTicks + 0x8000;
+ else
+ ticks = v->_ticks;
+
+ if (ticks >= maxTicks) {
+ maxTicks = ticks;
+ maxTicksVoiceIt = voiceIt;
+ }
+ }
+ } while (voiceIt != _lastVoiceIt);
+
+ if (maxTicksVoiceIt != _driver._voices.end()) {
+ (*maxTicksVoiceIt)->noteOff();
+ _lastVoiceIt = maxTicksVoiceIt;
+ return *maxTicksVoiceIt;
+ }
+
+ return nullptr;
+}
+
+void MidiPlayer_AmigaMac1::Channel::voiceMapping(byte voices) {
+ int curVoices = 0;
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it)
+ if ((*it)->_channel == this)
+ curVoices++;
+
+ curVoices += _extraVoices;
+
+ if (curVoices < voices)
+ assignVoices(voices - curVoices);
+ else if (curVoices > voices) {
+ releaseVoices(curVoices - voices);
+ _driver.distributeVoices();
+ }
+}
+
+void MidiPlayer_AmigaMac1::Channel::assignVoices(byte voices) {
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (!v->_channel) {
+ v->_channel = this;
+
+ if (v->_note != -1)
+ v->noteOff();
+
+ if (--voices == 0)
+ break;
+ }
+ }
+
+ _extraVoices += voices;
+}
+
+void MidiPlayer_AmigaMac1::Channel::releaseVoices(byte voices) {
+ if (_extraVoices >= voices) {
+ _extraVoices -= voices;
+ return;
+ }
+
+ voices -= _extraVoices;
+ _extraVoices = 0;
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if ((v->_channel == this) && (v->_note == -1)) {
+ v->_channel = nullptr;
+ if (--voices == 0)
+ return;
+ }
+ }
+
+ do {
+ uint16 maxTicks = 0;
+ Voice *maxTicksVoice = _driver._voices[0];
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_channel == this) {
+ // The original code seems to be broken here. It reads a word value from
+ // byte array _voiceSustained.
+ uint16 ticks = v->_releaseTicks;
+ if (ticks > 0)
+ ticks += 0x8000;
+ else
+ ticks = v->_ticks;
+
+ if (ticks >= maxTicks) {
+ maxTicks = ticks;
+ maxTicksVoice = v;
+ }
+ }
+ }
+ maxTicksVoice->_isSustained = false;
+ maxTicksVoice->noteOff();
+ maxTicksVoice->_channel = nullptr;
+ } while (--voices > 0);
+}
+
+void MidiPlayer_AmigaMac1::distributeVoices() {
+ int freeVoices = 0;
+
+ for (VoiceIt it = _voices.begin(); it != _voices.end(); ++it)
+ if (!(*it)->_channel)
+ freeVoices++;
+
+ if (freeVoices == 0)
+ return;
+
+ for (ChanIt it = _channels.begin(); it != _channels.end(); ++it) {
+ Channel *channel = *it;
+
+ if (channel->_extraVoices != 0) {
+ if (channel->_extraVoices >= freeVoices) {
+ channel->_extraVoices -= freeVoices;
+ channel->assignVoices(freeVoices);
+ return;
+ } else {
+ freeVoices -= channel->_extraVoices;
+ const byte extraVoices = channel->_extraVoices;
+ channel->_extraVoices = 0;
+ channel->assignVoices(extraVoices);
+ }
+ }
+ }
+}
+
+void MidiPlayer_AmigaMac1::Voice::noteOn(int8 note, int8 velocity) {
+ _isReleased = false;
+ _envCurVel = 0;
+ _envState = kEnvStateAttack;
+ _envCntDown = 0;
+ _ticks = 0;
+ _releaseTicks = 0;
+
+ const int8 patchId = _channel->_patch;
+
+ // Check for valid patch
+ if (patchId < 0 || (uint)patchId >= _driver._instruments.size() || !_driver._instruments[patchId])
+ return;
+
+ const Instrument *ins = _driver._instruments[patchId];
+
+ // Each patch links to one or more waves, where each wave is assigned to a range of notes.
+ Common::Array<NoteRange>::const_iterator noteRange;
+ for (noteRange = ins->noteRange.begin(); noteRange != ins->noteRange.end(); ++noteRange) {
+ if (noteRange->startNote <= note && note <= noteRange->endNote)
+ break;
+ }
+
+ // Abort if this note has no wave assigned to it
+ if (noteRange == ins->noteRange.end())
+ return;
+
+ const Wave *wave = noteRange->wave;
+ const uint32 *freqTable = wave->freqTable;
+
+ _noteRange = noteRange;
+ _wave = wave;
+ _freqTable = freqTable;
+
+ play(note, velocity);
+}
+
+void MidiPlayer_AmigaMac1::Voice::noteOff() {
+ stop();
+ _velocity = 0;
+ _note = -1;
+ _isSustained = false;
+ _isReleased = false;
+ _envState = kEnvStateAttack;
+ _envCntDown = 0;
+ _ticks = 0;
+ _releaseTicks = 0;
+}
+
+void MidiPlayer_AmigaMac1::Voice::processEnvelope() {
+ if (!_noteRange->loop) {
+ _envCurVel = _noteRange->attackTarget;
+ return;
+ }
+
+ if (_isReleased)
+ _envState = kEnvStateRelease;
+
+ switch(_envState) {
+ case kEnvStateAttack: {
+ if (_envCntDown != 0) {
+ --_envCntDown;
+ return;
+ }
+ _envCntDown = _envSpeedToSkip[_noteRange->attackSpeed];
+ _envCurVel += _envSpeedToStep[_noteRange->attackSpeed];
+ if (_envCurVel >= _noteRange->attackTarget) {
+ _envCurVel = _noteRange->attackTarget;
+ _envState = kEnvStateDecay;
+ }
+ break;
+ }
+ case kEnvStateDecay: {
+ if (_envCntDown != 0) {
+ --_envCntDown;
+ return;
+ }
+ _envCntDown = _envSpeedToSkip[_noteRange->decaySpeed];
+ _envCurVel -= _envSpeedToStep[_noteRange->decaySpeed];
+ if (_envCurVel <= _noteRange->decayTarget) {
+ _envCurVel = _noteRange->decayTarget;
+ _envState = kEnvStateSustain;
+ }
+ break;
+ }
+ case kEnvStateSustain:
+ _envCurVel = _noteRange->decayTarget;
+ break;
+ case kEnvStateRelease: {
+ if (_envCntDown != 0) {
+ --_envCntDown;
+ return;
+ }
+ _envCntDown = _envSpeedToSkip[_noteRange->releaseSpeed];
+ _envCurVel -= _envSpeedToStep[_noteRange->releaseSpeed];
+ if (_envCurVel <= 0)
+ noteOff();
+ }
+ }
+}
+
+void MidiPlayer_AmigaMac1::Voice::calcMixVelocity() {
+ byte voiceVelocity = _velocity;
+
+ if (_channel->_volume != 0) {
+ if (voiceVelocity != 0) {
+ voiceVelocity = voiceVelocity * _channel->_volume / 63;
+ if (_envCurVel != 0) {
+ voiceVelocity = voiceVelocity * _envCurVel / 63;
+ if (_driver._masterVolume != 0) {
+ voiceVelocity = voiceVelocity * (_driver._masterVolume << 2) / 63;
+ if (voiceVelocity == 0)
+ ++voiceVelocity;
+ } else {
+ voiceVelocity = 0;
+ }
+ } else {
+ voiceVelocity = 0;
+ }
+ }
+ } else {
+ voiceVelocity = 0;
+ }
+
+ if (!_driver._playSwitch)
+ voiceVelocity = 0;
+
+ setVolume(voiceVelocity);
+}
+
+void MidiPlayer_AmigaMac1::Channel::noteOn(int8 note, int8 velocity) {
+ if (velocity == 0) {
+ noteOff(note);
+ return;
+ }
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_channel == this && v->_note == note) {
+ v->_isSustained = false;
+ v->noteOff();
+ v->noteOn(note, velocity);
+ return;
+ }
+ }
+
+ Voice *v = findVoice();
+ if (v)
+ v->noteOn(note, velocity);
+}
+
+void MidiPlayer_AmigaMac1::Channel::noteOff(int8 note) {
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_channel == this && v->_note == note) {
+ if (_hold)
+ v->_isSustained = true;
+ else {
+ v->_isReleased = true;
+ v->_envCntDown = 0;
+ }
+ return;
+ }
+ }
+}
+
+void MidiPlayer_AmigaMac1::Channel::changePatch(int8 patch) {
+ _patch = patch;
+}
+
+void MidiPlayer_AmigaMac1::Channel::holdPedal(int8 pedal) {
+ _hold = pedal;
+
+ if (pedal != 0)
+ return;
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_channel == this && v->_isSustained) {
+ v->_isSustained = false;
+ v->_isReleased = true;
+ }
+ }
+}
+
+void MidiPlayer_AmigaMac1::Channel::setPitchWheel(uint16 pitch) {
+ _pitch = pitch;
+
+ for (VoiceIt it = _driver._voices.begin(); it != _driver._voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_note != -1 && v->_channel == this)
+ v->calcVoiceStep();
+ }
+}
+
+void MidiPlayer_AmigaMac1::send(uint32 b) {
+ Common::StackLock lock(_mixMutex);
+
+ const byte command = b & 0xf0;
+ Channel *channel = _channels[b & 0xf];
+ const byte op1 = (b >> 8) & 0xff;
+ byte op2 = (b >> 16) & 0xff;
+
+ switch(command) {
+ case 0x80:
+ channel->noteOff(op1);
+ break;
+ case 0x90:
+ channel->noteOn(op1, op2);
+ break;
+ case 0xb0:
+ switch (op1) {
+ case 0x07:
+ if (op2 != 0) {
+ op2 >>= 1;
+ if (op2 == 0)
+ ++op2;
+ }
+ channel->_volume = op2;
+ break;
+ case 0x0a:
+ channel->_pan = op2;
+ break;
+ case 0x40:
+ channel->holdPedal(op2);
+ break;
+ case 0x4b:
+ channel->voiceMapping(op2);
+ break;
+ case 0x7b:
+ for (VoiceIt it = _voices.begin(); it != _voices.end(); ++it) {
+ Voice *v = *it;
+
+ if (v->_channel == channel && v->_note != -1)
+ v->noteOff();
+ }
+ }
+ break;
+ case 0xc0:
+ channel->changePatch(op1);
+ break;
+ case 0xe0:
+ channel->setPitchWheel((op2 << 7) | op1);
+ break;
+ }
+}
+
+const byte MidiPlayer_AmigaMac1::_envSpeedToStep[32] = {
+ 0x40, 0x32, 0x24, 0x18, 0x14, 0x0f, 0x0d, 0x0b, 0x09, 0x08, 0x07, 0x06, 0x05, 0x0a, 0x04, 0x03,
+ 0x05, 0x02, 0x03, 0x0b, 0x05, 0x09, 0x09, 0x01, 0x02, 0x03, 0x07, 0x05, 0x04, 0x03, 0x03, 0x02
+};
+
+const byte MidiPlayer_AmigaMac1::_envSpeedToSkip[32] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x01, 0x00, 0x01, 0x07, 0x02, 0x05, 0x07, 0x00, 0x01, 0x02, 0x08, 0x08, 0x08, 0x09, 0x0e, 0x0b
+};
+
+const byte MidiPlayer_AmigaMac1::_velocityMap[64] = {
+ 0x01, 0x02, 0x03, 0x03, 0x04, 0x05, 0x05, 0x06, 0x07, 0x08, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
+ 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2a,
+ 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x34, 0x35, 0x37, 0x39, 0x3a, 0x3c, 0x3e, 0x40
+};
+
+class MidiPlayer_Mac1 : public Mixer_Mac<MidiPlayer_Mac1>, public MidiPlayer_AmigaMac1 {
+public:
+ MidiPlayer_Mac1(Audio::Mixer *mixer, Mixer_Mac<MidiPlayer_Mac1>::Mode mode);
+
+ // MidiPlayer
+ int open(ResourceManager *resMan) override;
+
+ // MidiDriver
+ void close() override;
+
+ // Mixer_Mac
+ static int8 applyChannelVolume(byte velocity, byte sample);
+ void interrupt() { onTimer(); }
+ void onChannelFinished(uint channel);
+
+private:
+ static int euclDivide(int x, int y);
+
+ class MacVoice : public MidiPlayer_AmigaMac1::Voice {
+ public:
+ MacVoice(MidiPlayer_Mac1 &driver, byte id) :
+ MidiPlayer_AmigaMac1::Voice(driver, id),
+ _macDriver(driver) {}
+
+ private:
+ void play(int8 note, int8 velocity) override;
+ void stop() override;
+ void setVolume(byte volume) override;
+ bool calcVoiceStep() override;
+
+ ufrac_t calcStep(int8 note);
+
+ MidiPlayer_Mac1 &_macDriver;
+ };
+};
+
+MidiPlayer_Mac1::MidiPlayer_Mac1(Audio::Mixer *mixer, Mixer_Mac<MidiPlayer_Mac1>::Mode mode) :
+ Mixer_Mac<MidiPlayer_Mac1>(mode),
+ MidiPlayer_AmigaMac1(mixer, 1480, false, _mutex) {}
+
+int MidiPlayer_Mac1::open(ResourceManager *resMan) {
+ if (_isOpen)
+ return MidiDriver::MERR_ALREADY_OPEN;
+
+ const SciResource *patch = getMidiPatchData(7);
+ if (!patch) {
+ warning("MidiPlayer_Mac1: Failed to open patch 7");
+ return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ }
+
+ Common::MemoryReadStream stream(patch->toStream());
+ if (!loadInstruments(stream, false)) {
+ freeInstruments();
+ return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ }
+
+ for (byte vi = 0; vi < kVoices; ++vi)
+ _voices.push_back(new MacVoice(*this, vi));
+
+ for (byte ci = 0; ci < MIDI_CHANNELS; ++ci)
+ _channels.push_back(new MidiPlayer_AmigaMac1::Channel(*this));
+
+ startMixer();
+ _mixer->playStream(Audio::Mixer::kPlainSoundType, &_mixerSoundHandle, this, -1, _mixer->kMaxChannelVolume, 0, DisposeAfterUse::NO);
+
+ _isOpen = true;
+
+ return 0;
+}
+
+void MidiPlayer_Mac1::close() {
+ MidiPlayer_AmigaMac1::close();
+ stopMixer();
+}
+
+void MidiPlayer_Mac1::onChannelFinished(uint channel) {
+ _voices[channel]->noteOff();
+}
+
+void MidiPlayer_Mac1::MacVoice::play(int8 note, int8 velocity) {
+ if (velocity != 0)
+ velocity = _velocityMap[velocity >> 1];
+
+ _velocity = velocity;
+ _note = note;
+
+ if (!calcVoiceStep()) {
+ _note = -1;
+ return;
+ }
+
+ _macDriver.setChannelVolume(_id, 0);
+
+ uint16 endOffset = _wave->phase2End;
+
+ if (endOffset == 0)
+ endOffset = _wave->phase1End;
+
+ uint16 loopLength = 0;
+
+ if (_wave->phase2End != 0 && _noteRange->loop)
+ loopLength = endOffset - _wave->phase2Start + 1;
+
+ _macDriver.setChannelData(_id, _wave->samples, _wave->phase1Start, endOffset, loopLength);
+}
+
+void MidiPlayer_Mac1::MacVoice::stop() {
+ _macDriver.resetChannel(_id);
+}
+
+void MidiPlayer_Mac1::MacVoice::setVolume(byte volume) {
+ _macDriver.setChannelVolume(_id, volume);
+ _macDriver.setChannelPan(_id, _channel->_pan);
+}
+
+ufrac_t MidiPlayer_Mac1::MacVoice::calcStep(int8 note) {
+ uint16 noteAdj = note + 127 - _wave->nativeNote;
+ uint16 pitch = _channel->_pitch;
+ pitch /= 170;
+ noteAdj += (pitch >> 2) - 12;
+ uint octaveRsh = 0;
+
+ if (noteAdj < 255)
+ octaveRsh = 21 - (noteAdj + 9) / 12;
+
+ noteAdj = (noteAdj + 9) % 12;
+
+ const uint freqTableIndex = (noteAdj << 2) + (pitch & 3);
+ assert(freqTableIndex + 8 < kFreqTableSize);
+ ufrac_t step = (ufrac_t)_freqTable[freqTableIndex + 4];
+
+ int16 transpose = _noteRange->transpose;
+ if (transpose > 0) {
+ ufrac_t delta = (ufrac_t)_freqTable[freqTableIndex + 8] - step;
+ delta >>= 4;
+ delta >>= octaveRsh;
+ delta *= transpose;
+ step >>= octaveRsh;
+ step += delta;
+ } else if (transpose < 0) {
+ ufrac_t delta = step - (ufrac_t)_freqTable[freqTableIndex];
+ delta >>= 4;
+ delta >>= octaveRsh;
+ delta *= -transpose;
+ step >>= octaveRsh;
+ step -= delta;
+ } else {
+ step >>= octaveRsh;
+ }
+
+ return step;
+}
+
+bool MidiPlayer_Mac1::MacVoice::calcVoiceStep() {
+ int8 note = _note;
+
+ int16 fixedNote = _noteRange->fixedNote;
+ if (fixedNote != -1)
+ note = fixedNote;
+
+ ufrac_t step = calcStep(note);
+ if (step == (ufrac_t)-1)
+ return false;
+
+ _macDriver.setChannelStep(_id, step);
+
+ return true;
+}
+
+int MidiPlayer_Mac1::euclDivide(int x, int y) {
+ // Assumes y > 0
+ if (x % y < 0)
+ return x / y - 1;
+ else
+ return x / y;
+}
+
+int8 MidiPlayer_Mac1::applyChannelVolume(byte volume, byte sample) {
+ return euclDivide((sample - 0x80) * volume, 63);
+}
+
+class MidiPlayer_Amiga1 : public Audio::Paula, public MidiPlayer_AmigaMac1 {
+public:
+ MidiPlayer_Amiga1(Audio::Mixer *mixer);
+
+ // MidiPlayer
+ int open(ResourceManager *resMan) override;
+
+ // MidiDriver
+ void close() override;
+
+ // Paula
+ void interrupt() override;
+
+private:
+ class AmigaVoice : public MidiPlayer_AmigaMac1::Voice {
+ public:
+ AmigaVoice(MidiPlayer_Amiga1 &driver, uint id) :
+ MidiPlayer_AmigaMac1::Voice(driver, id),
+ _amigaDriver(driver) {}
+
+ void play(int8 note, int8 velocity) override;
+ void stop() override;
+ void setVolume(byte volume) override;
+ bool calcVoiceStep() override;
+
+ private:
+ uint16 calcPeriod(int8 note);
+
+ MidiPlayer_Amiga1 &_amigaDriver;
+ };
+
+ bool _isSci1Ega;
+
+ static const byte _velocityMapSci1Ega[64];
+};
+
+MidiPlayer_Amiga1::MidiPlayer_Amiga1(Audio::Mixer *mixer) :
+ Paula(true, mixer->getOutputRate(), (mixer->getOutputRate() + kBaseFreq / 2) / kBaseFreq, kFilterModeA500),
+ MidiPlayer_AmigaMac1(mixer, 224, true, _mutex),
+ _isSci1Ega(false) {}
+
+int MidiPlayer_Amiga1::open(ResourceManager *resMan) {
+ if (_isOpen)
+ return MidiDriver::MERR_ALREADY_OPEN;
+
+ const SciResource *patch = getMidiPatchData(9);
+
+ if (!patch) {
+ patch = getMidiPatchData(5);
+
+ if (!patch) {
+ warning("MidiPlayer_Amiga1: Failed to open patch");
+ return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ }
+
+ _isSci1Ega = true;
+ }
+
+ // SCI1 EGA banks start with a uint32 patch size, skip it
+ Common::MemoryReadStream stream(patch->toStream(_isSci1Ega ? 4 : 0));
+ if (!loadInstruments(stream, _isSci1Ega)) {
+ freeInstruments();
+ return MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ }
+
+ for (byte vi = 0; vi < kVoices; ++vi)
+ _voices.push_back(new AmigaVoice(*this, vi));
+
+ for (byte ci = 0; ci < MIDI_CHANNELS; ++ci)
+ _channels.push_back(new MidiPlayer_AmigaMac1::Channel(*this));
+
+ startPaula();
+ // Enable reverse stereo to counteract Audio::Paula's reverse stereo
+ _mixer->playStream(Audio::Mixer::kPlainSoundType, &_mixerSoundHandle, this, -1, _mixer->kMaxChannelVolume, 0, DisposeAfterUse::NO, false, true);
+
+ _isOpen = true;
+
+ return 0;
+}
+
+void MidiPlayer_Amiga1::close() {
+ MidiPlayer_AmigaMac1::close();
+ stopPaula();
+}
+
+void MidiPlayer_Amiga1::interrupt() {
+ // In the original driver, the interrupt handlers for each voice
+ // call voiceOff when non-looping samples are finished.
+ for (uint vi = 0; vi < kVoices; ++vi) {
+ if (_voices[vi]->_note != -1 && !_voices[vi]->_noteRange->loop && getChannelDmaCount(vi) > 0)
+ _voices[vi]->noteOff();
+ }
+
+ onTimer();
+}
+
+void MidiPlayer_Amiga1::AmigaVoice::play(int8 note, int8 velocity) {
+ if (velocity != 0) {
+ if (_amigaDriver._isSci1Ega)
+ velocity = _velocityMapSci1Ega[velocity >> 1];
+ else
+ velocity = _velocityMap[velocity >> 1];
+ }
+
+ _velocity = velocity;
+ _note = note;
+
+ if (!calcVoiceStep()) {
+ _note = -1;
+ return;
+ }
+
+ _amigaDriver.setChannelVolume(_id, 0);
+
+ // The original driver uses double buffering to play the samples. We will instead
+ // play the data directly. The original driver might be OB1 in end offsets and
+ // loop sizes on occasion. Currently, this behavior isn't taken into account, and
+ // the samples are played according to the meta data in the sound bank.
+ const int8 *samples = (const int8 *)_wave->samples;
+ uint16 phase1Start = _wave->phase1Start;
+ uint16 phase1End = _wave->phase1End;
+ uint16 phase2Start = _wave->phase2Start;
+ uint16 phase2End = _wave->phase2End;
+ bool loop = _noteRange->loop;
+
+ uint16 endOffset = phase2End;
+
+ if (endOffset == 0)
+ endOffset = phase1End;
+
+ // Paula consumes one word at a time
+ phase1Start &= 0xfffe;
+ phase2Start &= 0xfffe;
+
+ // If endOffset is odd, the sample byte at endOffset is played, otherwise it isn't
+ endOffset = (endOffset + 1) & 0xfffe;
+
+ int phase1Len = endOffset - phase1Start;
+ int phase2Len = endOffset - phase2Start;
+
+ // The original driver delays the voice start for two MIDI ticks, possibly
+ // due to DMA requirements
+ if (phase2End == 0 || !loop) {
+ // Non-looping
+ _amigaDriver.setChannelData(_id, samples + phase1Start, nullptr, phase1Len, 0);
+ } else {
+ // Looping
+ _amigaDriver.setChannelData(_id, samples + phase1Start, samples + phase2Start, phase1Len, phase2Len);
+ }
+}
+
+void MidiPlayer_Amiga1::AmigaVoice::stop() {
+ _amigaDriver.clearVoice(_id);
+}
+
+void MidiPlayer_Amiga1::AmigaVoice::setVolume(byte volume) {
+ _amigaDriver.setChannelVolume(_id, volume);
+}
+
+uint16 MidiPlayer_Amiga1::AmigaVoice::calcPeriod(int8 note) {
+ uint16 noteAdj = note + 127 - _wave->nativeNote;
+ uint16 pitch = _channel->_pitch;
+ pitch /= 170;
+ noteAdj += (pitch >> 2) - 12;
+
+ // SCI1 EGA is off by one note
+ if (_amigaDriver._isSci1Ega)
+ ++noteAdj;
+
+ const uint octaveRsh = noteAdj / 12;
+ noteAdj %= 12;
+
+ const uint freqTableIndex = (noteAdj << 2) + (pitch & 3);
+ assert(freqTableIndex + 8 < kFreqTableSize);
+ uint32 period = _freqTable[freqTableIndex + 4];
+
+ int16 transpose = _noteRange->transpose;
+ if (transpose > 0) {
+ uint32 delta = period - _freqTable[freqTableIndex + 8];
+ delta >>= 4;
+ delta *= transpose;
+ period -= delta;
+ } else if (transpose < 0) {
+ uint32 delta = _freqTable[freqTableIndex] - period;
+ delta >>= 4;
+ delta *= -transpose;
+ period += delta;
+ }
+
+ period >>= octaveRsh;
+
+ if (period < 0x7c || period > 0xffff)
+ return (uint16)-1;
+
+ return period;
+}
+
+bool MidiPlayer_Amiga1::AmigaVoice::calcVoiceStep() {
+ int8 note = _note;
+
+ int16 fixedNote = _noteRange->fixedNote;
+ if (fixedNote != -1)
+ note = fixedNote;
+
+ uint16 period = calcPeriod(note);
+ if (period == (uint16)-1)
+ return false;
+
+ // Audio::Paula uses int16 instead of uint16?
+ _amigaDriver.setChannelPeriod(_id, period);
+
+ return true;
+}
+
+const byte MidiPlayer_Amiga1::_velocityMapSci1Ega[64] = {
+ 0x01, 0x04, 0x07, 0x0a, 0x0c, 0x0f, 0x11, 0x15, 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x21, 0x22, 0x23,
+ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x30, 0x31, 0x31,
+ 0x32, 0x32, 0x33, 0x33, 0x34, 0x34, 0x35, 0x35, 0x36, 0x36, 0x37, 0x37, 0x38, 0x38, 0x38, 0x39,
+ 0x39, 0x39, 0x3a, 0x3a, 0x3a, 0x3b, 0x3b, 0x3b, 0x3c, 0x3d, 0x3e, 0x3e, 0x3f, 0x3f, 0x40, 0x40
+};
+
+MidiPlayer *MidiPlayer_AmigaMac1_create(Common::Platform platform) {
+ if (platform == Common::kPlatformMacintosh)
+ return new MidiPlayer_Mac1(g_system->getMixer(), Mixer_Mac<MidiPlayer_Mac1>::kModeHqStereo);
+ else
+ return new MidiPlayer_Amiga1(g_system->getMixer());
+}
+
+} // End of namespace Dgds
diff --git a/engines/dgds/sound/drivers/cms.cpp b/engines/dgds/sound/drivers/cms.cpp
new file mode 100644
index 00000000000..7430d9d720f
--- /dev/null
+++ b/engines/dgds/sound/drivers/cms.cpp
@@ -0,0 +1,1278 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "dgds/sound/drivers/mididriver.h"
+
+#include "audio/mididrv.h"
+#include "audio/cms.h"
+
+#include "dgds/sound/resource/sci_resource.h"
+#include "dgds/util.h"
+
+namespace Dgds {
+
+class MidiDriver_CMS;
+
+class CMSVoice {
+public:
+ CMSVoice(uint8 id, MidiDriver_CMS *driver, CMS::CMS *cms, SciSpan<const uint8>& patchData);
+ virtual ~CMSVoice() {}
+
+ virtual void noteOn(int note, int velocity) = 0;
+ virtual void noteOff() = 0;
+ virtual void stop() = 0;
+ virtual void programChange(int program) = 0;
+ virtual void pitchWheel() {}
+
+ virtual void update() = 0;
+
+ virtual void reset() {}
+ virtual void setPanMask(uint8) {}
+
+ uint8 _assign;
+ uint8 _note;
+ bool _sustained;
+ uint16 _duration;
+ uint16 _releaseDuration;
+ CMSVoice *_secondaryVoice;
+
+protected:
+ void sendFrequency();
+ void cmsWrite(uint8 reg, uint8 val);
+
+ CMS::CMS *_cms;
+ MidiDriver_CMS *_driver;
+ SciSpan<const uint8> _patchData;
+
+ const uint8 _id;
+ const uint8 _regOffset;
+ const uint16 _chipOffset;
+
+ static uint8 _octaveRegs[6];
+ static const int _frequencyTable[48];
+
+private:
+ virtual void recalculateFrequency(uint8 &freq, uint8 &octave) = 0;
+};
+
+class CMSVoice_V0 : public CMSVoice {
+public:
+ CMSVoice_V0(uint8 id, MidiDriver_CMS *driver, CMS::CMS *cms, SciSpan<const uint8>& patchData);
+ ~CMSVoice_V0() override {}
+
+ void noteOn(int note, int) override;
+ void noteOff() override;
+ void stop() override;
+ void programChange(int program) override;
+
+ void update() override;
+
+ void reset() override;
+ void setPanMask(uint8 mask) override;
+
+private:
+ void recalculateFrequency(uint8 &frequency, uint8 &octave) override;
+ void recalculateEnvelopeLevels();
+ void selectEnvelope(int id);
+
+ enum EnvelopeState {
+ kReady = 0,
+ kRestart = 1,
+ kAttack = 2,
+ kDecay = 3,
+ kSustain = 4,
+ kRelease = 5
+ };
+
+ EnvelopeState _envState;
+ uint8 _envAR;
+ uint8 _envTL;
+ uint8 _envDR;
+ uint8 _envSL;
+ uint8 _envRR;
+ uint8 _envSLI;
+ uint8 _envPAC;
+ uint8 _envPA;
+
+ uint8 _envNote;
+ uint8 _envSSL;
+ uint8 _panMask;
+ uint8 _strMask;
+
+ int8 _transFreq;
+ int8 _transOct;
+
+ bool _vbrOn;
+ uint8 _vbrSteps;
+ uint8 _vbrState;
+ int8 _vbrMod;
+ int8 _vbrCur;
+ int16 _vbrPhase;
+
+ int _currentLevel;
+ bool _updateCMS;
+
+ const bool _isSecondary;
+
+ static const uint8 _volumeTable[176];
+ static const uint8 _pitchWheelTable[65];
+};
+
+class CMSVoice_V1 : public CMSVoice {
+public:
+ CMSVoice_V1(uint8 id, MidiDriver_CMS *driver, CMS::CMS *cms, SciSpan<const uint8>& patchData);
+ ~CMSVoice_V1() override {}
+
+ void noteOn(int note, int velocity) override;
+ void noteOff() override;
+ void stop() override;
+ void programChange(int program) override;
+ void pitchWheel() override;
+
+ void update() override;
+
+private:
+ void recalculateFrequency(uint8 &frequency, uint8 &octave) override;
+
+ void updateVoiceAmplitude();
+ void setupVoiceAmplitude();
+
+ SciSpan<const uint8> _patchDataCur;
+ uint8 _velocity;
+ uint8 _patchDataIndex;
+ uint8 _amplitudeTimer;
+ uint8 _amplitudeModifier;
+ bool _release;
+
+ static const int _velocityTable[32];
+};
+
+class MidiDriver_CMS : public MidiDriver {
+public:
+ enum {
+ MIDI_PROP_CHANNEL_VOLUME = 1,
+ MIDI_PROP_CHANNEL_PITCHWHEEL = 2,
+ MIDI_PROP_CHANNEL_PANPOS = 3,
+ MIDI_PROP_PLAYSWITCH = 4
+ };
+
+public:
+ MidiDriver_CMS(ResourceManager *resMan);
+ ~MidiDriver_CMS() override;
+
+ int open() override;
+ void close() override;
+ bool isOpen() const override { return _isOpen; }
+
+ void send(uint32 b) override;
+ uint32 property(int prop, uint32 param) override;
+
+ void initTrack(SciSpan<const byte>& header);
+
+ uint32 getBaseTempo() override { return 1000000 / CMS::CMS::DEFAULT_CALLBACK_FREQUENCY; }
+
+ void setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) override;
+ void onTimer();
+
+ MidiChannel *allocateChannel() override { return nullptr; }
+ MidiChannel *getPercussionChannel() override { return nullptr; }
+
+private:
+ void noteOn(int channelNr, int note, int velocity);
+ void noteOff(int channelNr, int note);
+ void controlChange(int channelNr, int control, int value);
+ void programChange(int channelNr, int value);
+ void pitchWheel(int channelNr, int value);
+
+ void voiceMapping(int channelNr, int value);
+ void bindVoices(int channelNr, int voices, bool bindSecondary, bool doProgramChange);
+ void unbindVoices(int channelNr, int voices, bool bindSecondary);
+ void donateVoices(bool bindSecondary);
+ int findVoice(int channelNr, int note);
+ int findVoiceBasic(int channelNr);
+
+ void writeToChip(int chip, int address, int data);
+
+ struct Channel {
+ Channel() : program(0), volume(0), pan(0x40), hold(0), missingVoices(0), lastVoiceUsed(0), pitchWheel(0x2000), isValid(true) {}
+ uint8 program;
+ uint8 volume;
+ uint8 pan;
+ uint8 hold;
+ uint8 missingVoices;
+ uint8 lastVoiceUsed;
+ uint16 pitchWheel;
+ bool isValid;
+ };
+
+ Channel _channel[16];
+ CMSVoice *_voice[12];
+
+ const int _numVoicesPrimary;
+ const int _numVoicesSecondary;
+
+ CMS::CMS *_cms;
+ bool _isOpen;
+ ResourceManager *_resMan;
+ Common::SpanOwner<SciSpan<const uint8> > _patchData;
+
+ bool _playSwitch;
+ uint16 _masterVolume;
+
+ Common::TimerManager::TimerProc _timerProc;
+ void *_timerParam;
+
+ const int _actualTimerInterval;
+ const int _reqTimerInterval;
+ int _updateTimer;
+};
+
+CMSVoice::CMSVoice(uint8 id, MidiDriver_CMS* driver, CMS::CMS *cms, SciSpan<const uint8>& patchData) : _id(id), _regOffset(id > 5 ? id - 6 : id), _chipOffset(id > 5 ? 0x100 : 0),
+ _driver(driver), _cms(cms), _assign(0xFF), _note(0xFF), _sustained(false), _duration(0), _releaseDuration(0), _secondaryVoice(nullptr), _patchData(patchData) {
+ assert(_id < 12);
+ _octaveRegs[_id >> 1] = 0;
+}
+
+void CMSVoice::sendFrequency() {
+ uint8 frequency = 0;
+ uint8 octave = 0;
+
+ recalculateFrequency(frequency, octave);
+
+ uint8 octaveData = _octaveRegs[_id >> 1];
+ octaveData = (_id & 1) ? (octaveData & 0x0F) | (octave << 4) : (octaveData & 0xF0) | octave;
+
+ cmsWrite(8 + _regOffset, frequency);
+ cmsWrite(0x10 + (_regOffset >> 1), octaveData);
+}
+
+void CMSVoice::cmsWrite(uint8 reg, uint8 val) {
+ _cms->writeReg(reg + _chipOffset, val);
+
+ if (reg >= 16 && reg <= 18)
+ _octaveRegs[_id >> 1] = val;
+}
+
+uint8 CMSVoice::_octaveRegs[6] = {
+ 0, 0, 0, 0, 0, 0
+};
+
+const int CMSVoice::_frequencyTable[48] = {
+ 3, 10, 17, 24,
+ 31, 38, 46, 51,
+ 58, 64, 71, 77,
+ 83, 89, 95, 101,
+ 107, 113, 119, 124,
+ 130, 135, 141, 146,
+ 151, 156, 162, 167,
+ 172, 177, 182, 186,
+ 191, 196, 200, 205,
+ 209, 213, 217, 222,
+ 226, 230, 234, 238,
+ 242, 246, 250, 253
+};
+
+CMSVoice_V0::CMSVoice_V0(uint8 id, MidiDriver_CMS* driver, CMS::CMS *cms, SciSpan<const uint8>& patchData) : CMSVoice(id, driver, cms, patchData), _envState(kReady), _currentLevel(0), _strMask(0),
+ _envAR(0), _envTL(0), _envDR(0), _envSL(0), _envRR(0), _envSLI(0), _vbrOn(false), _vbrSteps(0), _vbrState(0), _vbrMod(0), _vbrCur(0), _isSecondary(id > 7),
+ _vbrPhase(0), _transOct(0), _transFreq(0), _envPAC(0), _envPA(0), _panMask(_id & 1 ? 0xF0 : 0x0F), _envSSL(0), _envNote(0xFF), _updateCMS(false) {
+}
+
+void CMSVoice_V0::noteOn(int note, int) {
+ if (!_driver->property(MidiDriver_CMS::MIDI_PROP_PLAYSWITCH, 0xFFFF) || !_envTL)
+ return;
+
+ _note = note;
+ _envNote = note + 3;
+ _envState = kRestart;
+ _vbrPhase = 0;
+ _vbrCur = _vbrMod;
+ _vbrState = _vbrSteps & 0x0F;
+ _envPAC = _envPA;
+
+ if (_secondaryVoice)
+ _secondaryVoice->noteOn(note, 127);
+}
+
+void CMSVoice_V0::noteOff() {
+ if (!_driver->property(MidiDriver_CMS::MIDI_PROP_PLAYSWITCH, 0xFFFF) || !_envTL)
+ return;
+
+ _note = 0xFF;
+ _envState = kRelease;
+ if (_secondaryVoice)
+ _secondaryVoice->noteOff();
+}
+
+void CMSVoice_V0::stop() {
+ _note = 0xFF;
+ if (_envState != kReady)
+ _envState = kRelease;
+ if (_secondaryVoice)
+ _secondaryVoice->stop();
+}
+
+void CMSVoice_V0::programChange(int program) {
+ if (program > 127) {
+ // I encountered this with PQ2 at the airport (program 204 sent on part 13). The original driver does not really handle that.
+ // In fact, it even interprets this value as signed so it will not point into the instrument data buffer, but into a random
+ // invalid memory location (in the case of 204 it will read a value of 8 from the device init data array). Since there seems
+ // to be no effect on the sound I don't emulate this (mis)behaviour.
+ warning("CMSVoice_V0::programChange:: Invalid program '%d' requested on midi channel '%d'", program, _assign);
+ program = 0;
+ } else if (program == 127) {
+ // This seems to replace the start of track offset with the current position so that 0xFC (kEndOfTrack)
+ // midi events would not reset the track to the start, but to the current position instead. This cannot
+ // be handled here. All versions of the SCI0 driver that I have seen so far do this. Still, I somehow
+ // doubt that it will ever come up, but let's see...
+ warning("CMSVoice_V0::programChange(): Unhandled program change 127");
+ return;
+ }
+
+ SciSpan<const uint8> data = _patchData.subspan(128 + (_patchData.getUint8At(program) << 3));
+ uint8 pos = _isSecondary ? 3 : 0;
+
+ selectEnvelope(data.getUint8At(pos++));
+
+ if (_isSecondary) {
+ _envSSL = data.getUint8At(pos++);
+ // This decides whether the secondary voice has the same or the opposite pan position as the primary voice.
+ _panMask = _strMask ^ -(_envSSL & 1);
+ }
+
+ _transOct = data.getInt8At(pos++);
+ _transFreq = data.getInt8At(pos++);
+
+ if (_isSecondary)
+ _envPA = data.getUint8At(pos++);
+
+ if (_secondaryVoice) {
+ assert(!_isSecondary);
+ if (data.getUint8At(pos) == 0xFF) {
+ _secondaryVoice->stop();
+ _secondaryVoice->_assign = 0xFF;
+ _secondaryVoice = nullptr;
+ } else {
+ _secondaryVoice->setPanMask(_panMask);
+ _secondaryVoice->programChange(program);
+ }
+ }
+}
+
+void CMSVoice_V0::update() {
+ if (_updateCMS) {
+ sendFrequency();
+ cmsWrite(_regOffset, ((_currentLevel & 0xF0) | (_currentLevel >> 4)) & _panMask);
+ _updateCMS = false;
+ }
+
+ recalculateEnvelopeLevels();
+
+ switch (_envState) {
+ case kReady:
+ _envNote = 0xFF;
+ return;
+
+ case kRestart:
+ if (_envPAC) {
+ --_envPAC;
+ break;
+ } else {
+ _currentLevel = ((_currentLevel >> 1) > _envAR) ? ((_currentLevel >> 1) - _envAR) : 0;
+ _envState = kAttack;
+ }
+ // fall through
+
+ case kAttack:
+ _currentLevel = _currentLevel + _envAR;
+ if (_currentLevel > _envTL || _currentLevel > 0xFF) {
+ _currentLevel = _envTL;
+ _envState = kDecay;
+ }
+ break;
+
+ case kDecay:
+ _currentLevel -= _envDR;
+ if (_currentLevel <= _envSL) {
+ if (_currentLevel < 0)
+ _currentLevel = 0;
+ _envState = kSustain;
+ }
+ break;
+
+ case kSustain:
+ _currentLevel = _envSL;
+ break;
+
+ case kRelease:
+ _currentLevel -= _envRR;
+ if (_currentLevel < 0) {
+ _currentLevel = 0;
+ _envState = kReady;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (_vbrOn && _envState != kRestart) {
+ _vbrPhase += _vbrCur;
+ if (!--_vbrState) {
+ _vbrCur = -_vbrCur;
+ _vbrState = (_vbrSteps & 0x0F) << 1;
+ }
+ }
+
+ _updateCMS = true;
+ ++_duration;
+}
+
+void CMSVoice_V0::reset() {
+ _envState = kReady;
+ _secondaryVoice = nullptr;
+ _assign = _note = _envNote = 0xFF;
+ _panMask = _id & 1 ? 0xF0 : 0x0F;
+ _envTL = 0;
+ _currentLevel = 0;
+ _duration = 0;
+ _envPA = 0;
+ _transFreq = _transOct = 0;
+ selectEnvelope(3);
+}
+
+void CMSVoice_V0::setPanMask(uint8 mask) {
+ _strMask = mask;
+}
+
+void CMSVoice_V0::recalculateFrequency(uint8 &freq, uint8 &octave) {
+ if (_assign == 0xFF || _envNote == 0xFF)
+ return;
+
+ uint8 note = _envNote % 12;
+ octave = CLIP<int>(_envNote / 12 - 2, 0, 7);
+
+ int16 pbVal = (_driver->property(MidiDriver_CMS::MIDI_PROP_CHANNEL_PITCHWHEEL, _assign) & 0x7FFF) - 0x2000;
+ int16 pbEff = _pitchWheelTable[ABS(pbVal) >> 7] * ((pbVal < 0) ? -1 : 1);
+ int frequency = note * 4 + pbEff;
+
+ if (frequency < 0) {
+ if (octave) {
+ frequency += 48;
+ --octave;
+ } else {
+ frequency = 0;
+ }
+ } else if (frequency >= 48) {
+ if (octave < 7) {
+ frequency -= 48;
+ ++octave;
+ } else {
+ frequency = 47;
+ }
+ }
+
+ octave = CLIP<int8>(octave + _transOct, 0, 7);
+ frequency = _frequencyTable[frequency & 0xFF] + _transFreq + _vbrPhase;
+
+ if (frequency > 255) {
+ frequency &= 0xFF;
+ octave++;
+ } else if (frequency < 0) {
+ frequency &= 0xFF;
+ octave--;
+ }
+
+ octave = CLIP<int8>(octave, 0, 7);
+ freq = frequency;
+}
+
+void CMSVoice_V0::recalculateEnvelopeLevels() {
+ uint8 chanVol = _driver->property(MidiDriver_CMS::MIDI_PROP_CHANNEL_VOLUME, _assign);
+
+ if (_envTL && _isSecondary) {
+ int volIndexTLS = (chanVol >> 4) | (_envSSL & 0xF0);
+ assert(volIndexTLS < ARRAYSIZE(_volumeTable));
+ _envTL = _volumeTable[volIndexTLS];
+ } else if (_envTL) {
+ _envTL = chanVol;
+ }
+
+ int volIndexSL = (_envSLI << 4) + (_envTL >> 4);
+ assert(volIndexSL < ARRAYSIZE(_volumeTable));
+ _envSL = _volumeTable[volIndexSL];
+}
+
+void CMSVoice_V0::selectEnvelope(int id) {
+ SciSpan<const uint8> in = _patchData.subspan(512 + ((id & 0x1F) << 3));
+ _envAR = *in++;
+ _envTL = *in++;
+ _envDR = *in++;
+ _envSLI = *in++;
+ _envRR = *in++;
+ /*unused*/in++;
+ _vbrMod = *in++;
+ _vbrSteps = *in++;
+ _vbrOn = _vbrMod;
+ _vbrCur = _vbrMod;
+ _vbrState = _vbrSteps & 0x0F;
+ _vbrPhase = 0;
+}
+
+const uint8 CMSVoice_V0::_volumeTable[176] = {
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x20, 0x20, 0x20, 0x20, 0x20, 0x30,
+ 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x20, 0x20, 0x20, 0x30, 0x30, 0x30, 0x30, 0x40, 0x40,
+ 0x00, 0x00, 0x00, 0x10, 0x10, 0x20, 0x20, 0x20, 0x30, 0x30, 0x40, 0x40, 0x40, 0x50, 0x50, 0x60,
+ 0x00, 0x00, 0x10, 0x10, 0x20, 0x20, 0x30, 0x30, 0x40, 0x40, 0x50, 0x50, 0x60, 0x60, 0x70, 0x70,
+ 0x00, 0x00, 0x10, 0x10, 0x20, 0x30, 0x30, 0x40, 0x40, 0x50, 0x60, 0x60, 0x70, 0x70, 0x80, 0x90,
+ 0x00, 0x00, 0x10, 0x20, 0x20, 0x30, 0x40, 0x40, 0x50, 0x60, 0x70, 0x70, 0x80, 0x90, 0x90, 0xa0,
+ 0x00, 0x00, 0x10, 0x20, 0x30, 0x40, 0x40, 0x50, 0x60, 0x70, 0x80, 0x80, 0x90, 0xa0, 0xb0, 0xc0,
+ 0x00, 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0x90, 0xa0, 0xb0, 0xc0, 0xd0,
+ 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0, 0xf0
+};
+
+const uint8 CMSVoice_V0::_pitchWheelTable[65] = {
+ 0x00, 0x01, 0x02, 0x02, 0x03, 0x04, 0x05, 0x05,
+ 0x06, 0x07, 0x08, 0x08, 0x09, 0x0a, 0x0b, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0e, 0x0f, 0x10, 0x11, 0x11,
+ 0x12, 0x13, 0x14, 0x14, 0x15, 0x16, 0x17, 0x17,
+ 0x18, 0x19, 0x1a, 0x1a, 0x1b, 0x1c, 0x1d, 0x1d,
+ 0x1e, 0x1f, 0x20, 0x20, 0x21, 0x22, 0x23, 0x23,
+ 0x24, 0x25, 0x26, 0x26, 0x27, 0x28, 0x29, 0x29,
+ 0x2a, 0x2b, 0x2c, 0x2c, 0x2d, 0x2e, 0x2f, 0x2f,
+ 0x30
+};
+
+CMSVoice_V1::CMSVoice_V1(uint8 id, MidiDriver_CMS* driver, CMS::CMS *cms, SciSpan<const uint8>& patchData) : CMSVoice(id, driver, cms, patchData), _velocity(0), _patchDataIndex(0),
+ _amplitudeTimer(0), _amplitudeModifier(0), _release(false) {
+}
+
+void CMSVoice_V1::noteOn(int note, int velocity) {
+ _note = note;
+ _release = false;
+ _patchDataIndex = 0;
+ _amplitudeTimer = 0;
+ _duration = 0;
+ _releaseDuration = 0;
+ _velocity = velocity ? _velocityTable[velocity >> 3] : 0;
+ sendFrequency();
+}
+
+void CMSVoice_V1::noteOff() {
+ _release = true;
+}
+
+void CMSVoice_V1::stop() {
+ _velocity = 0;
+ _note = 0xFF;
+ _sustained = false;
+ _release = false;
+ _patchDataIndex = 0;
+ _amplitudeTimer = 0;
+ _amplitudeModifier = 0;
+ _duration = 0;
+ _releaseDuration = 0;
+
+ setupVoiceAmplitude();
+}
+
+void CMSVoice_V1::programChange(int program) {
+ _patchDataCur = _patchData.subspan(_patchData.getUint16LEAt(program << 1));
+}
+
+void CMSVoice_V1::pitchWheel() {
+ sendFrequency();
+}
+
+void CMSVoice_V1::update() {
+ if (_note == 0xFF)
+ return;
+
+ if (_release)
+ ++_releaseDuration;
+ ++_duration;
+
+ updateVoiceAmplitude();
+ setupVoiceAmplitude();
+}
+
+void CMSVoice_V1::recalculateFrequency(uint8 &freq, uint8 &octave) {
+ assert(_assign != 0xFF);
+
+ int frequency = (CLIP<int>(_note, 21, 116) - 21) * 4;
+ int16 pw = _driver->property(MidiDriver_CMS::MIDI_PROP_CHANNEL_PITCHWHEEL, _assign);
+ int modifier = (pw < 0x2000) ? (0x2000 - pw) / 170 : ((pw > 0x2000) ? (pw - 0x2000) / 170 : 0);
+
+ if (modifier) {
+ if (pw < 0x2000) {
+ if (frequency > modifier)
+ frequency -= modifier;
+ else
+ frequency = 0;
+ } else {
+ int tempFrequency = 384 - frequency;
+ if (modifier < tempFrequency)
+ frequency += modifier;
+ else
+ frequency = 383;
+ }
+ }
+
+ octave = 0;
+ while (frequency >= 48) {
+ frequency -= 48;
+ ++octave;
+ }
+
+ freq = _frequencyTable[frequency & 0xFF];
+}
+
+void CMSVoice_V1::updateVoiceAmplitude() {
+ if (_amplitudeTimer != 0 && _amplitudeTimer != 254) {
+ --_amplitudeTimer;
+ return;
+ } else if (_amplitudeTimer == 254) {
+ if (!_release)
+ return;
+ _amplitudeTimer = 0;
+ }
+
+ int nextDataIndex = _patchDataIndex;
+ uint8 timerData = 0;
+ uint8 amplitudeData = _patchDataCur[nextDataIndex];
+
+ if (amplitudeData == 255) {
+ timerData = amplitudeData = 0;
+ stop();
+ } else {
+ timerData = _patchDataCur[nextDataIndex + 1];
+ nextDataIndex += 2;
+ }
+
+ _patchDataIndex = nextDataIndex;
+ _amplitudeTimer = timerData;
+ _amplitudeModifier = amplitudeData;
+}
+
+void CMSVoice_V1::setupVoiceAmplitude() {
+ assert(_assign != 0xFF);
+ uint amplitude = 0;
+ uint8 chanVolume = _driver->property(MidiDriver_CMS::MIDI_PROP_CHANNEL_VOLUME, _assign);
+ uint8 masterVolume = _driver->property(MIDI_PROP_MASTER_VOLUME, 0xFFFF);
+
+ if (chanVolume && _velocity && _amplitudeModifier && masterVolume) {
+ amplitude = chanVolume * _velocity;
+ amplitude /= 0x0F;
+ amplitude *= _amplitudeModifier;
+ amplitude /= 0x0F;
+ amplitude *= masterVolume;
+ amplitude /= 0x0F;
+
+ if (!amplitude)
+ ++amplitude;
+ }
+
+ uint8 amplitudeData = 0;
+ int pan = _driver->property(MidiDriver_CMS::MIDI_PROP_CHANNEL_PANPOS, _assign) >> 2;
+ if (pan >= 16) {
+ amplitudeData = (amplitude * (31 - pan) / 0x0F) & 0x0F;
+ amplitudeData |= (amplitude << 4);
+ } else {
+ amplitudeData = (amplitude * pan / 0x0F) & 0x0F;
+ amplitudeData <<= 4;
+ amplitudeData |= amplitude;
+ }
+
+ if (!_driver->property(MidiDriver_CMS::MIDI_PROP_PLAYSWITCH, 0xFFFF))
+ amplitudeData = 0;
+
+ cmsWrite(_regOffset, amplitudeData);
+}
+
+const int CMSVoice_V1::_velocityTable[32] = {
+ 1, 3, 6, 8, 9, 10, 11, 12,
+ 12, 13, 13, 14, 14, 14, 15, 15,
+ 0, 1, 2, 2, 3, 4, 4, 5,
+ 6, 6, 7, 8, 8, 9, 10, 10
+};
+
+MidiDriver_CMS::MidiDriver_CMS(ResourceManager *resMan, SciVersion version) : _resMan(resMan), _isOpen(false),
+ _version(version), _cms(nullptr), _playSwitch(true), _masterVolume(0), _numVoicesPrimary(version > SCI_VERSION_0_LATE ? 12 : 8),
+ _timerProc(nullptr), _timerParam(nullptr), _actualTimerInterval(1000000 / CMS::CMS::DEFAULT_CALLBACK_FREQUENCY), _reqTimerInterval(1000000/60),
+ _numVoicesSecondary(0) {
+ memset(_voice, 0, sizeof(_voice));
+ _updateTimer = _reqTimerInterval;
+}
+
+MidiDriver_CMS::~MidiDriver_CMS() {
+ for (int i = 0; i < 12; ++i)
+ delete _voice[i];
+}
+
+int MidiDriver_CMS::open() {
+ if (_isOpen)
+ return MERR_ALREADY_OPEN;
+
+ assert(_resMan);
+ Resource *res = _resMan->findResource(ResourceId(kResourceTypePatch, 101), false);
+ if (!res)
+ return -1;
+
+ _patchData->allocateFromSpan(*res);
+
+ _cms = CMS::Config::create();
+ if (!_cms || !_cms->init())
+ return MERR_CANNOT_CONNECT;
+
+ for (uint i = 0; i < ARRAYSIZE(_channel); ++i)
+ _channel[i] = Channel();
+
+ for (uint i = 0; i < ARRAYSIZE(_voice); ++i) {
+ _voice[i] = new CMSVoice_V1(i, this, _cms, *_patchData);
+ }
+
+ _playSwitch = true;
+ _masterVolume = 0;
+
+ for (int i = 0; i < 31; ++i) {
+ writeToChip(0, i, 0);
+ writeToChip(1, i, 0);
+ }
+
+ // Enable frequency for all channels
+ writeToChip(0, 0x14, 0xFF);
+ writeToChip(1, 0x14, 0xFF);
+
+ // Sync and reset generators
+ writeToChip(0, 0x1C, 2);
+ writeToChip(1, 0x1C, 2);
+
+ // Enable all channels
+ writeToChip(0, 0x1C, 1);
+ writeToChip(1, 0x1C, 1);
+
+ _isOpen = true;
+
+ _cms->start(new Common::Functor0Mem<void, MidiDriver_CMS>(this, &MidiDriver_CMS::onTimer));
+
+ return 0;
+}
+
+void MidiDriver_CMS::close() {
+ if (_cms) {
+ _cms->stop();
+ delete _cms;
+ _cms = nullptr;
+ }
+
+ _patchData.clear();
+ _isOpen = false;
+}
+
+void MidiDriver_CMS::send(uint32 b) {
+ const uint8 command = b & 0xf0;
+ const uint8 channel = b & 0xf;
+ const uint8 op1 = (b >> 8) & 0xff;
+ const uint8 op2 = (b >> 16) & 0xff;
+
+ // This is a SCI0 only feature. For SCI1 we simply set all channels to valid by default so that this check will always pass.
+ if (!_channel[channel].isValid)
+ return;
+
+ switch (command) {
+ case 0x80:
+ noteOff(channel, op1);
+ break;
+
+ case 0x90:
+ noteOn(channel, op1, op2);
+ break;
+
+ case 0xB0:
+ controlChange(channel, op1, op2);
+ break;
+
+ case 0xC0:
+ programChange(channel, op1);
+ break;
+
+ case 0xE0:
+ pitchWheel(channel, (op1 & 0x7f) | ((op2 & 0x7f) << 7));
+ break;
+
+ default:
+ break;
+ }
+}
+
+uint32 MidiDriver_CMS::property(int prop, uint32 param) {
+ switch (prop) {
+ case MIDI_PROP_MASTER_VOLUME:
+ if (param != 0xffff)
+ _masterVolume = param;
+ return _masterVolume;
+ case MIDI_PROP_PLAYSWITCH:
+ if (param != 0xffff)
+ _playSwitch = param ? true : false;
+ return _playSwitch ? 1 : 0;
+ case MIDI_PROP_CHANNEL_VOLUME:
+ return (param < 16) ? _channel[param].volume : 0;
+ case MIDI_PROP_CHANNEL_PITCHWHEEL:
+ return (param < 16) ? _channel[param].pitchWheel : 0;
+ case MIDI_PROP_CHANNEL_PANPOS:
+ return (param < 16) ? _channel[param].pan : 0;
+ default:
+ return MidiDriver::property(prop, param);
+ }
+}
+
+void MidiDriver_CMS::initTrack(SciSpan<const byte>& header) {
+ return;
+}
+
+void MidiDriver_CMS::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) {
+ _timerProc = timerProc;
+ _timerParam = timerParam;
+}
+
+void MidiDriver_CMS::onTimer() {
+ if (_timerProc)
+ (*_timerProc)(_timerParam);
+
+ for (_updateTimer -= _actualTimerInterval; _updateTimer <= 0; _updateTimer += _reqTimerInterval) {
+ for (uint i = 0; i < ARRAYSIZE(_voice); ++i)
+ _voice[i]->update();
+ }
+}
+
+void MidiDriver_CMS::noteOn(int channelNr, int note, int velocity) {
+ if (note < 21 || note > 116)
+ return;
+
+ if (velocity == 0) {
+ noteOff(channelNr, note);
+ return;
+ }
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note == note) {
+ _voice[i]->stop();
+ _voice[i]->programChange(_channel[channelNr].program);
+ _voice[i]->noteOn(note, velocity);
+ return;
+ }
+ }
+
+#ifdef CMS_DISABLE_VOICE_MAPPING
+ int id = findVoiceBasic(channelNr);
+#else
+ int id = findVoice(channelNr, note);
+#endif
+ if (id != -1) {
+ _voice[id]->programChange(_channel[channelNr].program);
+ _voice[id]->noteOn(note, velocity);
+ }
+}
+
+void MidiDriver_CMS::noteOff(int channelNr, int note) {
+ for (uint i = 0; i < ARRAYSIZE(_voice); ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note == note) {
+ if (_channel[channelNr].hold != 0)
+ _voice[i]->_sustained = true;
+ else
+ _voice[i]->noteOff();
+ }
+ }
+}
+
+void MidiDriver_CMS::controlChange(int channelNr, int control, int value) {
+ switch (control) {
+ case 7:
+ _channel[channelNr].volume = (value ? MAX<uint8>(value >> 3, 1) : 0);
+ break;
+
+ case 10:
+ _channel[channelNr].pan = value;
+ break;
+
+ case 64:
+ _channel[channelNr].hold = value;
+
+ if (!value) {
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_sustained) {
+ _voice[i]->_sustained = false;
+ _voice[i]->noteOff();
+ }
+ }
+ }
+ break;
+
+ case 75:
+#ifndef CMS_DISABLE_VOICE_MAPPING
+ voiceMapping(channelNr, value);
+#endif
+ break;
+
+ case 123:
+ for (uint i = 0; i < ARRAYSIZE(_voice); ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note != 0xFF)
+ _voice[i]->stop();
+ }
+ break;
+
+ default:
+ return;
+ }
+}
+
+void MidiDriver_CMS::programChange(int channelNr, int value) {
+ _channel[channelNr].program = value;
+}
+
+void MidiDriver_CMS::pitchWheel(int channelNr, int value) {
+ _channel[channelNr].pitchWheel = value;
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note != 0xFF)
+ _voice[i]->pitchWheel();
+ }
+}
+
+void MidiDriver_CMS::voiceMapping(int channelNr, int value) {
+ int curVoices = 0;
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr)
+ ++curVoices;
+ }
+
+ curVoices += _channel[channelNr].missingVoices;
+
+ if (curVoices < value) {
+ bindVoices(channelNr, value - curVoices, curVoices == 0 && value == 1, true);
+ } else if (curVoices > value) {
+ unbindVoices(channelNr, curVoices - value, value == 1);
+ donateVoices(value == 1);
+ }/*else if (_version < SCI_VERSION_1_EARLY && value == 1) {
+ // The purpose of these lines would be to fill up missing secondary voices.
+ // I have commented them out, since the original driver doesn't do that either.
+ unbindVoices(channelNr, 1, true);
+ bindVoices(channelNr, 1, true, true);
+ }*/
+}
+
+void MidiDriver_CMS::bindVoices(int channelNr, int voices, bool bindSecondary, bool doProgramChange) {
+ int secondary = bindSecondary ? _numVoicesSecondary : 0;
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign != 0xFF)
+ continue;
+
+ _voice[i]->_assign = channelNr;
+ if (_voice[i]->_note != 0xFF)
+ _voice[i]->stop();
+
+ for (int ii = _numVoicesPrimary; ii < _numVoicesPrimary + secondary; ++ii) {
+ if (_voice[ii]->_assign != 0xFF)
+ continue;
+
+ _voice[ii]->_assign = channelNr;
+ _voice[i]->_secondaryVoice = _voice[ii];
+
+ break;
+ }
+
+ // This will also release the secondary voice binding immediately if the current patch does
+ // not require such an extra channel. This condition will not be checked when called from initTrack().
+ if (doProgramChange)
+ _voice[i]->programChange(_channel[channelNr].program);
+
+ --voices;
+ if (voices == 0)
+ break;
+ }
+
+ _channel[channelNr].missingVoices += voices;
+}
+
+void MidiDriver_CMS::unbindVoices(int channelNr, int voices, bool bindSecondary) {
+ int secondary = bindSecondary ? _numVoicesSecondary : 0;
+ Channel &channel = _channel[channelNr];
+
+ if (channel.missingVoices >= voices) {
+ channel.missingVoices -= voices;
+ } else {
+ voices -= channel.missingVoices;
+ channel.missingVoices = 0;
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note == 0xFF) {
+ _voice[i]->_assign = 0xFF;
+
+ CMSVoice *sec = _voice[i]->_secondaryVoice;
+ if (sec) {
+ sec->stop();
+ sec->_assign = 0xFF;
+ _voice[i]->_secondaryVoice = nullptr;
+ }
+
+ --voices;
+ if (voices == 0)
+ return;
+ }
+ }
+
+ do {
+ uint16 voiceTime = 0;
+ uint voiceNr = 0;
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign != channelNr)
+ continue;
+
+ uint16 curTime = _voice[i]->_releaseDuration;
+ if (curTime)
+ curTime += 0x8000;
+ else
+ curTime = _voice[i]->_duration;
+
+ if (curTime >= voiceTime) {
+ voiceNr = i;
+ voiceTime = curTime;
+ }
+ }
+
+ _voice[voiceNr]->_sustained = false;
+ _voice[voiceNr]->stop();
+ _voice[voiceNr]->_assign = 0xFF;
+
+ CMSVoice *sec = _voice[voiceNr]->_secondaryVoice;
+ if (sec) {
+ sec->stop();
+ sec->_assign = 0xFF;
+ _voice[voiceNr]->_secondaryVoice = nullptr;
+ }
+
+ --voices;
+ } while (voices != 0);
+ }
+
+ for (int i = _numVoicesPrimary; i < _numVoicesPrimary + secondary; ++i) {
+ if (_voice[i]->_assign != 0xFF)
+ continue;
+
+ _voice[i]->_assign = channelNr;
+ if (_voice[i]->_note != 0xFF)
+ _voice[i]->stop();
+
+ for (int ii = 0; ii < _numVoicesPrimary; ++ii) {
+ if (_voice[ii]->_assign != channelNr)
+ continue;
+ _voice[ii]->_secondaryVoice = _voice[i];
+ // This will release the secondary binding immediately if the current patch does not require such an extra channel.
+ _voice[ii]->programChange(_channel[channelNr].program);
+ break;
+ }
+
+ if (_voice[i]->_assign == channelNr && _voice[i]->_note != 0xFF)
+ _voice[i]->stop();
+ break;
+ }
+}
+
+void MidiDriver_CMS::donateVoices(bool bindSecondary) {
+ int freeVoices = 0;
+
+ for (int i = 0; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == 0xFF)
+ ++freeVoices;
+ }
+
+ if (!freeVoices)
+ return;
+
+ for (int i = 0; i < ARRAYSIZE(_channel); ++i) {
+ Channel &channel = _channel[i];
+
+ if (!channel.missingVoices) {
+ continue;
+ } else if (channel.missingVoices < freeVoices) {
+ freeVoices -= channel.missingVoices;
+ int missing = channel.missingVoices;
+ channel.missingVoices = 0;
+ bindVoices(i, missing, bindSecondary, true);
+ } else {
+ channel.missingVoices -= freeVoices;
+ bindVoices(i, freeVoices, bindSecondary, true);
+ return;
+ }
+ }
+}
+
+int MidiDriver_CMS::findVoice(int channelNr, int note) {
+ Channel &channel = _channel[channelNr];
+ int voiceNr = channel.lastVoiceUsed;
+ int newVoice = 0;
+ int newVoiceAltSCI0 = -2;
+ uint16 newVoiceTime = 0;
+
+ bool loopDone = false;
+ do {
+ ++voiceNr;
+
+ if (voiceNr == _numVoicesPrimary)
+ voiceNr = 0;
+
+ if (voiceNr == channel.lastVoiceUsed)
+ loopDone = true;
+
+ if (_voice[voiceNr]->_assign == channelNr) {
+ if (_voice[voiceNr]->_note == 0xFF) {
+ channel.lastVoiceUsed = voiceNr ;
+ return voiceNr;
+ }
+
+ int cnt = 1;
+ for (int i = voiceNr + 1; i < _numVoicesPrimary; ++i) {
+ if (_voice[i]->_assign == channelNr)
+ ++cnt;
+ }
+
+ // The SCI0 driver will (before resorting to the "note age test") simply return the first
+ // assigned voice as long as there are no other (primary) voices assigned to the midi part.
+ if (cnt == 1 && newVoiceAltSCI0 == -1)
+ newVoiceAltSCI0 = voiceNr;
+
+ uint16 curTime = _voice[voiceNr]->_releaseDuration;
+ if (curTime)
+ curTime += 0x8000;
+ else
+ curTime = _voice[voiceNr]->_duration;
+
+ if (curTime >= newVoiceTime) {
+ newVoice = voiceNr;
+ newVoiceTime = curTime;
+ }
+ }
+ } while (!loopDone);
+
+ if (newVoiceAltSCI0 >= 0)
+ return newVoiceAltSCI0;
+
+ if (newVoiceTime > 0) {
+ voiceNr = newVoice;
+ channel.lastVoiceUsed = _numVoicesPrimary - 1;
+
+ _voice[voiceNr]->stop();
+ channel.lastVoiceUsed = voiceNr;
+
+ return voiceNr;
+ }
+
+ return -1;
+}
+
+int MidiDriver_CMS::findVoiceBasic(int channelNr) {
+ int voice = -1;
+ int oldestVoice = -1;
+ int oldestAge = -1;
+
+ // Try to find a voice assigned to this channel that is free (round-robin)
+ for (int i = 0; i < _numVoicesPrimary; i++) {
+ int v = (_channel[channelNr].lastVoiceUsed + i + 1) % _numVoicesPrimary;
+
+ if (_voice[v]->_note == 0xFF) {
+ voice = v;
+ break;
+ }
+
+ // We also keep track of the oldest note in case the search fails
+ if (_voice[v]->_duration > oldestAge) {
+ oldestAge = _voice[v]->_duration;
+ oldestVoice = v;
+ }
+ }
+
+ if (voice == -1) {
+ if (oldestVoice >= 0) {
+ _voice[oldestVoice]->stop();
+ voice = oldestVoice;
+ } else {
+ return -1;
+ }
+ }
+
+ _voice[voice]->_assign = channelNr;
+ _channel[channelNr].lastVoiceUsed = voice;
+ return voice;
+}
+
+void MidiDriver_CMS::writeToChip(int chip, int address, int data) {
+ assert(chip == 0 || chip == 1);
+ _cms->writeReg(address + (chip << 8), data);
+}
+
+class MidiPlayer_CMS : public MidiPlayer {
+public:
+ MidiPlayer_CMS(SciVersion version) : MidiPlayer(version), _filesMissing(false) {}
+
+ int open(ResourceManager *resMan) override;
+ void close() override;
+
+ void initTrack(SciSpan<const byte>& header) override;
+
+ bool hasRhythmChannel() const override { return false; }
+ byte getPlayId() const override { return 9; }
+ int getPolyphony() const override { return 12; }
+
+ void playSwitch(bool play) override { _driver->property(MidiDriver_CMS::MIDI_PROP_PLAYSWITCH, play ? 1 : 0); }
+};
+
+int MidiPlayer_CMS::open(ResourceManager *resMan) {
+ if (_driver)
+ return MidiDriver::MERR_ALREADY_OPEN;
+
+ _driver = new MidiDriver_CMS(resMan, _version);
+ int driverRetVal = _driver->open();
+
+ return driverRetVal;
+}
+
+void MidiPlayer_CMS::close() {
+ _driver->setTimerCallback(nullptr, nullptr);
+ _driver->close();
+ delete _driver;
+ _driver = nullptr;
+}
+
+void MidiPlayer_CMS::initTrack(SciSpan<const byte>& header) {
+ if (_driver)
+ static_cast<MidiDriver_CMS*>(_driver)->initTrack(header);
+}
+
+const char MidiPlayer_CMS::_requiredFiles[] = "'PATCH.101'";
+
+MidiPlayer *MidiPlayer_CMS_create(SciVersion version) {
+ return new MidiPlayer_CMS(version);
+}
+
+} // End of namespace SCI
diff --git a/engines/dgds/sound/drivers/fmtowns.cpp b/engines/dgds/sound/drivers/fmtowns.cpp
new file mode 100644
index 00000000000..2a3d32399e0
--- /dev/null
+++ b/engines/dgds/sound/drivers/fmtowns.cpp
@@ -0,0 +1,667 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "sci/sci.h"
+
+#include "common/file.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "audio/softsynth/fmtowns_pc98/towns_audio.h"
+
+#include "sci/sound/resource/sci_resource.h"
+#include "sci/sound/drivers/mididriver.h"
+
+namespace Dgds {
+
+class MidiDriver_FMTowns;
+
+class TownsChannel {
+public:
+ TownsChannel(MidiDriver_FMTowns *driver, uint8 id);
+ ~TownsChannel() {}
+
+ void noteOff();
+ void noteOn(uint8 note, uint8 velo);
+ void pitchBend(int16 val);
+ void updateVolume();
+ void updateDuration();
+
+ uint8 _assign;
+ uint8 _note;
+ uint8 _sustain;
+ uint16 _duration;
+
+private:
+ uint8 _id;
+ uint8 _velo;
+ uint8 _program;
+
+ MidiDriver_FMTowns *_drv;
+};
+
+class TownsMidiPart {
+friend class MidiDriver_FMTowns;
+public:
+ TownsMidiPart(MidiDriver_FMTowns *driver, uint8 id);
+ ~TownsMidiPart() {}
+
+ void noteOff(uint8 note);
+ void noteOn(uint8 note, uint8 velo);
+ void controlChangeVolume(uint8 vol);
+ void controlChangeSustain(uint8 sus);
+ void controlChangePolyphony(uint8 numChan);
+ void controlChangeAllNotesOff();
+ void programChange(uint8 prg);
+ void pitchBend(int16 val);
+
+ void addChannels(int num);
+ void dropChannels(int num);
+
+ uint8 currentProgram() const;
+
+private:
+ int allocateChannel();
+
+ uint8 _id;
+ uint8 _program;
+ uint8 _volume;
+ uint8 _sustain;
+ uint8 _chanMissing;
+ int16 _pitchBend;
+ uint8 _outChan;
+
+ MidiDriver_FMTowns *_drv;
+};
+
+class MidiDriver_FMTowns : public MidiDriver, public TownsAudioInterfacePluginDriver {
+friend class TownsChannel;
+friend class TownsMidiPart;
+public:
+ MidiDriver_FMTowns(Audio::Mixer *mixer, SciVersion version);
+ ~MidiDriver_FMTowns() override;
+
+ int open() override;
+ void loadInstruments(const SciSpan<const uint8> &data);
+ bool isOpen() const override { return _isOpen; }
+ void close() override;
+
+ void send(uint32 b) override;
+
+ uint32 property(int prop, uint32 param) override;
+ void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override;
+
+ void setSoundOn(bool toggle);
+
+ uint32 getBaseTempo() override;
+ MidiChannel *allocateChannel() override { return nullptr; }
+ MidiChannel *getPercussionChannel() override { return nullptr; }
+
+ void timerCallback(int timerId) override;
+
+private:
+ int getChannelVolume(uint8 midiPart);
+ void addMissingChannels();
+
+ void updateParser();
+ void updateChannels();
+
+ Common::TimerManager::TimerProc _timerProc;
+ void *_timerProcPara;
+
+ TownsMidiPart **_parts;
+ TownsChannel **_out;
+
+ uint8 _masterVolume;
+
+ bool _soundOn;
+
+ bool _isOpen;
+ bool _ready;
+
+ const uint16 _baseTempo;
+ SciVersion _version;
+
+ TownsAudioInterface *_intf;
+};
+
+class MidiPlayer_FMTowns : public MidiPlayer {
+public:
+ MidiPlayer_FMTowns(SciVersion version);
+ ~MidiPlayer_FMTowns() override;
+
+ int open(ResourceManager *resMan) override;
+
+ bool hasRhythmChannel() const override;
+ byte getPlayId() const override;
+ int getPolyphony() const override;
+ void playSwitch(bool play) override;
+
+private:
+ MidiDriver_FMTowns *_townsDriver;
+};
+
+TownsChannel::TownsChannel(MidiDriver_FMTowns *driver, uint8 id) : _drv(driver), _id(id), _assign(0xff), _note(0xff), _velo(0), _sustain(0), _duration(0), _program(0xff) {
+}
+
+void TownsChannel::noteOn(uint8 note, uint8 velo) {
+ _duration = 0;
+
+ if (_drv->_version != SCI_VERSION_1_EARLY) {
+ if (_program != _drv->_parts[_assign]->currentProgram() && _drv->_soundOn) {
+ _program = _drv->_parts[_assign]->currentProgram();
+ _drv->_intf->callback(4, _id, _program);
+ }
+ }
+
+ _note = note;
+ _velo = velo;
+ _drv->_intf->callback(1, _id, _note, _velo);
+}
+
+void TownsChannel::noteOff() {
+ if (_sustain)
+ return;
+
+ _drv->_intf->callback(2, _id);
+ _note = 0xff;
+ _duration = 0;
+}
+
+void TownsChannel::pitchBend(int16 val) {
+ _drv->_intf->callback(7, _id, val);
+}
+
+void TownsChannel::updateVolume() {
+ if (_assign > 15 && _drv->_version != SCI_VERSION_1_EARLY)
+ return;
+ _drv->_intf->callback(8, _id, _drv->getChannelVolume((_drv->_version == SCI_VERSION_1_EARLY) ? 0 : _assign));
+}
+
+void TownsChannel::updateDuration() {
+ if (_note != 0xff)
+ _duration++;
+}
+
+TownsMidiPart::TownsMidiPart(MidiDriver_FMTowns *driver, uint8 id) : _drv(driver), _id(id), _program(0), _volume(0x3f), _sustain(0), _chanMissing(0), _pitchBend(0x2000), _outChan(0) {
+}
+
+void TownsMidiPart::noteOff(uint8 note) {
+ for (int i = 0; i < 6; i++) {
+ if ((_drv->_out[i]->_assign != _id && _drv->_version != SCI_VERSION_1_EARLY) || _drv->_out[i]->_note != note)
+ continue;
+ if (_sustain)
+ _drv->_out[i]->_sustain = 1;
+ else
+ _drv->_out[i]->noteOff();
+ return;
+ }
+}
+
+void TownsMidiPart::noteOn(uint8 note, uint8 velo) {
+ if (note < 12 || note > 107)
+ return;
+
+ if (velo == 0) {
+ noteOff(note);
+ return;
+ }
+
+ if (_drv->_version != SCI_VERSION_1_EARLY)
+ velo >>= 1;
+
+ for (int i = 0; i < 6; i++) {
+ if ((_drv->_out[i]->_assign != _id && _drv->_version != SCI_VERSION_1_EARLY) || _drv->_out[i]->_note != note)
+ continue;
+ _drv->_out[i]->_sustain = 0;
+ _drv->_out[i]->noteOff();
+ _drv->_out[i]->noteOn(note, velo);
+ return;
+ }
+
+ int chan = allocateChannel();
+ if (chan != -1)
+ _drv->_out[chan]->noteOn(note, velo);
+}
+
+void TownsMidiPart::controlChangeVolume(uint8 vol) {
+ if (_drv->_version == SCI_VERSION_1_EARLY)
+ return;
+
+ _volume = vol >> 1;
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign == _id)
+ _drv->_out[i]->updateVolume();
+ }
+}
+
+void TownsMidiPart::controlChangeSustain(uint8 sus) {
+ if (_drv->_version == SCI_VERSION_1_EARLY)
+ return;
+
+ _sustain = sus;
+ if (_sustain)
+ return;
+
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign == _id && _drv->_out[i]->_sustain) {
+ _drv->_out[i]->_sustain = 0;
+ _drv->_out[i]->noteOff();
+ }
+ }
+}
+
+void TownsMidiPart::controlChangePolyphony(uint8 numChan) {
+ if (_drv->_version == SCI_VERSION_1_EARLY)
+ return;
+
+ uint8 numAssigned = 0;
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign == _id)
+ numAssigned++;
+ }
+
+ numAssigned += _chanMissing;
+ if (numAssigned < numChan) {
+ addChannels(numChan - numAssigned);
+ } else if (numAssigned > numChan) {
+ dropChannels(numAssigned - numChan);
+ _drv->addMissingChannels();
+ }
+}
+
+void TownsMidiPart::controlChangeAllNotesOff() {
+ for (int i = 0; i < 6; i++) {
+ if ((_drv->_out[i]->_assign == _id || _drv->_version == SCI_VERSION_1_EARLY) && _drv->_out[i]->_note != 0xff)
+ _drv->_out[i]->noteOff();
+ }
+}
+
+void TownsMidiPart::programChange(uint8 prg) {
+ _program = prg;
+}
+
+void TownsMidiPart::pitchBend(int16 val) {
+ _pitchBend = val;
+ val -= 0x2000;
+ for (int i = 0; i < 6; i++) {
+ // Strangely, the early version driver applies the setting to channel 0 only.
+ if (_drv->_out[i]->_assign == _id || (_drv->_version == SCI_VERSION_1_EARLY && i == 0))
+ _drv->_out[i]->pitchBend(val);
+ }
+}
+
+void TownsMidiPart::addChannels(int num) {
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign != 0xff)
+ continue;
+
+ _drv->_out[i]->_assign = _id;
+ _drv->_out[i]->updateVolume();
+
+ if (_drv->_out[i]->_note != 0xff)
+ _drv->_out[i]->noteOff();
+
+ if (!--num)
+ break;
+ }
+
+ _chanMissing += num;
+ programChange(_program);
+ pitchBend(_pitchBend);
+ controlChangeVolume(_volume << 1);
+}
+
+void TownsMidiPart::dropChannels(int num) {
+ if (_chanMissing == num) {
+ _chanMissing = 0;
+ return;
+ } else if (_chanMissing > num) {
+ _chanMissing -= num;
+ return;
+ }
+
+ num -= _chanMissing;
+ _chanMissing = 0;
+
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign != _id || _drv->_out[i]->_note != 0xff)
+ continue;
+ _drv->_out[i]->_assign = 0xff;
+ if (!--num)
+ return;
+ }
+
+ for (int i = 0; i < 6; i++) {
+ if (_drv->_out[i]->_assign != _id)
+ continue;
+ _drv->_out[i]->_sustain = 0;
+ _drv->_out[i]->noteOff();
+ _drv->_out[i]->_assign = 0xff;
+ if (!--num)
+ return;
+ }
+}
+
+uint8 TownsMidiPart::currentProgram() const {
+ return _program;
+}
+
+int TownsMidiPart::allocateChannel() {
+ int chan = _outChan;
+ int ovrChan = 0;
+ int ld = 0;
+ bool found = false;
+
+ for (bool loop = true; loop; ) {
+ if (++chan == 6)
+ chan = 0;
+
+ if (chan == _outChan)
+ loop = false;
+
+ if (_id == _drv->_out[chan]->_assign || _drv->_version == SCI_VERSION_1_EARLY) {
+ if (_drv->_out[chan]->_note == 0xff) {
+ found = true;
+ break;
+ }
+
+ if (_drv->_out[chan]->_duration >= ld) {
+ ld = _drv->_out[chan]->_duration;
+ ovrChan = chan;
+ }
+ }
+ }
+
+ if (!found) {
+ if (!ld)
+ return -1;
+ chan = ovrChan;
+ _drv->_out[chan]->_sustain = 0;
+ _drv->_out[chan]->noteOff();
+ }
+
+ _outChan = chan;
+ return chan;
+}
+
+MidiDriver_FMTowns::MidiDriver_FMTowns(Audio::Mixer *mixer, SciVersion version) : _version(version), _timerProc(nullptr), _timerProcPara(nullptr), _baseTempo(10080), _ready(false), _isOpen(false), _masterVolume(0x0f), _soundOn(true) {
+ _intf = new TownsAudioInterface(mixer, this, true);
+ _out = new TownsChannel*[6];
+ for (int i = 0; i < 6; i++)
+ _out[i] = new TownsChannel(this, i);
+ _parts = new TownsMidiPart*[16];
+ for (int i = 0; i < 16; i++)
+ _parts[i] = new TownsMidiPart(this, i);
+}
+
+MidiDriver_FMTowns::~MidiDriver_FMTowns() {
+ delete _intf;
+
+ if (_parts) {
+ for (int i = 0; i < 16; i++) {
+ delete _parts[i];
+ _parts[i] = nullptr;
+ }
+ delete[] _parts;
+ _parts = nullptr;
+ }
+
+ if (_out) {
+ for (int i = 0; i < 6; i++) {
+ delete _out[i];
+ _out[i] = nullptr;
+ }
+ delete[] _out;
+ _out = nullptr;
+ }
+}
+
+int MidiDriver_FMTowns::open() {
+ if (_isOpen)
+ return MERR_ALREADY_OPEN;
+
+ if (!_ready) {
+ if (!_intf->init())
+ return MERR_CANNOT_CONNECT;
+
+ _intf->callback(0);
+
+ _intf->callback(21, 255, 1);
+ _intf->callback(21, 0, 1);
+ _intf->callback(22, 255, 221);
+
+ _intf->callback(33, 8);
+ _intf->setSoundEffectChanMask(~0x3f);
+
+ _ready = true;
+ }
+
+ _isOpen = true;
+
+ return 0;
+}
+
+void MidiDriver_FMTowns::loadInstruments(const SciSpan<const uint8> &data) {
+ enum {
+ fmDataSize = 48
+ };
+
+ if (data.size()) {
+ SciSpan<const uint8> instrumentData = data.subspan(6);
+ for (int i = 0; i < 128; i++, instrumentData += fmDataSize) {
+ _intf->callback(5, 0, i, instrumentData.getUnsafeDataAt(0, fmDataSize));
+ }
+ }
+
+ _intf->callback(70, 3);
+ property(MIDI_PROP_MASTER_VOLUME, _masterVolume);
+}
+
+void MidiDriver_FMTowns::close() {
+ _isOpen = false;
+}
+
+void MidiDriver_FMTowns::send(uint32 b) {
+ if (!_isOpen)
+ return;
+
+ byte para2 = (b >> 16) & 0xFF;
+ byte para1 = (b >> 8) & 0xFF;
+ byte cmd = b & 0xF0;
+
+ TownsMidiPart *chan = _parts[b & 0x0F];
+
+ switch (cmd) {
+ case 0x80:
+ chan->noteOff(para1);
+ break;
+ case 0x90:
+ chan->noteOn(para1, para2);
+ break;
+ case 0xb0:
+ switch (para1) {
+ case 7:
+ chan->controlChangeVolume(para2);
+ break;
+ case 64:
+ chan->controlChangeSustain(para2);
+ break;
+ case SCI_MIDI_SET_POLYPHONY:
+ chan->controlChangePolyphony(para2);
+ break;
+ case SCI_MIDI_CHANNEL_NOTES_OFF:
+ chan->controlChangeAllNotesOff();
+ break;
+ default:
+ break;
+ }
+ break;
+ case 0xc0:
+ chan->programChange(para1);
+ break;
+ case 0xe0:
+ chan->pitchBend(para1 | (para2 << 7));
+ break;
+ default:
+ break;
+ }
+}
+
+uint32 MidiDriver_FMTowns::property(int prop, uint32 param) {
+ switch(prop) {
+ case MIDI_PROP_MASTER_VOLUME:
+ if (param != 0xffff) {
+ _masterVolume = param;
+ for (int i = 0; i < 6; i++)
+ _out[i]->updateVolume();
+ }
+ return _masterVolume;
+ default:
+ break;
+ }
+ return 0;
+}
+
+void MidiDriver_FMTowns::setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) {
+ _timerProc = timer_proc;
+ _timerProcPara = timer_param;
+}
+
+void MidiDriver_FMTowns::setSoundOn(bool toggle) {
+ _soundOn = toggle;
+}
+
+uint32 MidiDriver_FMTowns::getBaseTempo() {
+ return _baseTempo;
+}
+
+void MidiDriver_FMTowns::timerCallback(int timerId) {
+ if (!_isOpen)
+ return;
+
+ switch (timerId) {
+ case 1:
+ updateParser();
+ updateChannels();
+ break;
+ default:
+ break;
+ }
+}
+
+int MidiDriver_FMTowns::getChannelVolume(uint8 midiPart) {
+ static const uint8 volumeTable[] = { 0x00, 0x0D, 0x1B, 0x28, 0x36, 0x43, 0x51, 0x5F, 0x63, 0x67, 0x6B, 0x6F, 0x73, 0x77, 0x7B, 0x7F };
+ int tableIndex = (_version == SCI_VERSION_1_EARLY) ? _masterVolume : (_parts[midiPart]->_volume * (_masterVolume + 1)) >> 6;
+ assert(tableIndex < 16);
+ return volumeTable[tableIndex];
+}
+
+void MidiDriver_FMTowns::addMissingChannels() {
+ uint8 avlChan = 0;
+ for (int i = 0; i < 6; i++) {
+ if (_out[i]->_assign == 0xff)
+ avlChan++;
+ }
+
+ if (!avlChan)
+ return;
+
+ for (int i = 0; i < 16; i++) {
+ if (!_parts[i]->_chanMissing)
+ continue;
+
+ if (_parts[i]->_chanMissing < avlChan) {
+ avlChan -= _parts[i]->_chanMissing;
+ uint8 m = _parts[i]->_chanMissing;
+ _parts[i]->_chanMissing = 0;
+ _parts[i]->addChannels(m);
+ } else {
+ _parts[i]->_chanMissing -= avlChan;
+ _parts[i]->addChannels(avlChan);
+ return;
+ }
+ }
+}
+
+void MidiDriver_FMTowns::updateParser() {
+ if (_timerProc)
+ _timerProc(_timerProcPara);
+}
+
+void MidiDriver_FMTowns::updateChannels() {
+ for (int i = 0; i < 6; i++)
+ _out[i]->updateDuration();
+}
+
+MidiPlayer_FMTowns::MidiPlayer_FMTowns(SciVersion version) : MidiPlayer(version) {
+ _driver = _townsDriver = new MidiDriver_FMTowns(g_system->getMixer(), version);
+}
+
+MidiPlayer_FMTowns::~MidiPlayer_FMTowns() {
+ delete _driver;
+}
+
+int MidiPlayer_FMTowns::open(ResourceManager *resMan) {
+ int result = MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ if (_townsDriver) {
+ result = _townsDriver->open();
+ if (!result && _version == SCI_VERSION_1_LATE) {
+ Resource *res = resMan->findResource(ResourceId(kResourceTypePatch, 8), false);
+ if (res != nullptr) {
+ _townsDriver->loadInstruments(*res);
+ } else {
+ warning("MidiPlayer_FMTowns: Failed to open patch 8");
+ result = MidiDriver::MERR_DEVICE_NOT_AVAILABLE;
+ }
+ }
+ }
+ return result;
+}
+
+bool MidiPlayer_FMTowns::hasRhythmChannel() const {
+ return false;
+}
+
+byte MidiPlayer_FMTowns::getPlayId() const {
+ return (_version == SCI_VERSION_1_EARLY) ? 0x00 : 0x16;
+}
+
+int MidiPlayer_FMTowns::getPolyphony() const {
+ // WORKAROUND:
+ // I set the return value to 16 for SCI_VERSION_1_EARLY here, which fixes music playback in Mixed Up Mothergoose.
+ // This has been broken since the introduction of SciMusic::remapChannels() and the corresponding code.
+ // The original code of Mixed Up Mothergoose code doesn't have the remapping and doesn't seem to check the polyphony
+ // setting ever. So the value of 1 was probably incorrect.
+ return (_version == SCI_VERSION_1_EARLY) ? 16 : 6;
+}
+
+void MidiPlayer_FMTowns::playSwitch(bool play) {
+ if (_townsDriver)
+ _townsDriver->setSoundOn(play);
+}
+
+MidiPlayer *MidiPlayer_FMTowns_create(SciVersion _soundVersion) {
+ return new MidiPlayer_FMTowns(_soundVersion);
+}
+
+} // End of namespace Sci
+
diff --git a/engines/dgds/sound/drivers/gm_names.h b/engines/dgds/sound/drivers/gm_names.h
new file mode 100644
index 00000000000..d04975580e0
--- /dev/null
+++ b/engines/dgds/sound/drivers/gm_names.h
@@ -0,0 +1,223 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DGDS_SOUND_DRIVERS_GM_NAMES_H
+#define DGDS_SOUND_DRIVERS_GM_NAMES_H
+
+namespace Dgds {
+
+// These tables are only used for debugging. Don't include them for devices
+// with not enough available memory (e.g. phones), where REDUCE_MEMORY_USAGE
+// is defined
+#ifndef REDUCE_MEMORY_USAGE
+
+static const char *const GmInstrumentNames[] = {
+ /*000*/ "Acoustic Grand Piano",
+ /*001*/ "Bright Acoustic Piano",
+ /*002*/ "Electric Grand Piano",
+ /*003*/ "Honky-tonk Piano",
+ /*004*/ "Electric Piano 1",
+ /*005*/ "Electric Piano 2",
+ /*006*/ "Harpsichord",
+ /*007*/ "Clavinet",
+ /*008*/ "Celesta",
+ /*009*/ "Glockenspiel",
+ /*010*/ "Music Box",
+ /*011*/ "Vibraphone",
+ /*012*/ "Marimba",
+ /*013*/ "Xylophone",
+ /*014*/ "Tubular Bells",
+ /*015*/ "Dulcimer",
+ /*016*/ "Drawbar Organ",
+ /*017*/ "Percussive Organ",
+ /*018*/ "Rock Organ",
+ /*019*/ "Church Organ",
+ /*020*/ "Reed Organ",
+ /*021*/ "Accordion",
+ /*022*/ "Harmonica",
+ /*023*/ "Tango Accordion",
+ /*024*/ "Acoustic Guitar (nylon)",
+ /*025*/ "Acoustic Guitar (steel)",
+ /*026*/ "Electric Guitar (jazz)",
+ /*027*/ "Electric Guitar (clean)",
+ /*028*/ "Electric Guitar (muted)",
+ /*029*/ "Overdriven Guitar",
+ /*030*/ "Distortion Guitar",
+ /*031*/ "Guitar Harmonics",
+ /*032*/ "Acoustic Bass",
+ /*033*/ "Electric Bass (finger)",
+ /*034*/ "Electric Bass (pick)",
+ /*035*/ "Fretless Bass",
+ /*036*/ "Slap Bass 1",
+ /*037*/ "Slap Bass 2",
+ /*038*/ "Synth Bass 1",
+ /*039*/ "Synth Bass 2",
+ /*040*/ "Violin",
+ /*041*/ "Viola",
+ /*042*/ "Cello",
+ /*043*/ "Contrabass",
+ /*044*/ "Tremolo Strings",
+ /*045*/ "Pizzicato Strings",
+ /*046*/ "Orchestral Harp",
+ /*047*/ "Timpani",
+ /*048*/ "String Ensemble 1",
+ /*049*/ "String Ensemble 2",
+ /*050*/ "SynthStrings 1",
+ /*051*/ "SynthStrings 2",
+ /*052*/ "Choir Aahs",
+ /*053*/ "Voice Oohs",
+ /*054*/ "Synth Voice",
+ /*055*/ "Orchestra Hit",
+ /*056*/ "Trumpet",
+ /*057*/ "Trombone",
+ /*058*/ "Tuba",
+ /*059*/ "Muted Trumpet",
+ /*060*/ "French Horn",
+ /*061*/ "Brass Section",
+ /*062*/ "SynthBrass 1",
+ /*063*/ "SynthBrass 2",
+ /*064*/ "Soprano Sax",
+ /*065*/ "Alto Sax",
+ /*066*/ "Tenor Sax",
+ /*067*/ "Baritone Sax",
+ /*068*/ "Oboe",
+ /*069*/ "English Horn",
+ /*070*/ "Bassoon",
+ /*071*/ "Clarinet",
+ /*072*/ "Piccolo",
+ /*073*/ "Flute",
+ /*074*/ "Recorder",
+ /*075*/ "Pan Flute",
+ /*076*/ "Blown Bottle",
+ /*077*/ "Shakuhachi",
+ /*078*/ "Whistle",
+ /*079*/ "Ocarina",
+ /*080*/ "Lead 1 (square)",
+ /*081*/ "Lead 2 (sawtooth)",
+ /*082*/ "Lead 3 (calliope)",
+ /*083*/ "Lead 4 (chiff)",
+ /*084*/ "Lead 5 (charang)",
+ /*085*/ "Lead 6 (voice)",
+ /*086*/ "Lead 7 (fifths)",
+ /*087*/ "Lead 8 (bass+lead)",
+ /*088*/ "Pad 1 (new age)",
+ /*089*/ "Pad 2 (warm)",
+ /*090*/ "Pad 3 (polysynth)",
+ /*091*/ "Pad 4 (choir)",
+ /*092*/ "Pad 5 (bowed)",
+ /*093*/ "Pad 6 (metallic)",
+ /*094*/ "Pad 7 (halo)",
+ /*095*/ "Pad 8 (sweep)",
+ /*096*/ "FX 1 (rain)",
+ /*097*/ "FX 2 (soundtrack)",
+ /*098*/ "FX 3 (crystal)",
+ /*099*/ "FX 4 (atmosphere)",
+ /*100*/ "FX 5 (brightness)",
+ /*101*/ "FX 6 (goblins)",
+ /*102*/ "FX 7 (echoes)",
+ /*103*/ "FX 8 (sci-fi)",
+ /*104*/ "Sitar",
+ /*105*/ "Banjo",
+ /*106*/ "Shamisen",
+ /*107*/ "Koto",
+ /*108*/ "Kalimba",
+ /*109*/ "Bag pipe",
+ /*110*/ "Fiddle",
+ /*111*/ "Shannai",
+ /*112*/ "Tinkle Bell",
+ /*113*/ "Agogo",
+ /*114*/ "Steel Drums",
+ /*115*/ "Woodblock",
+ /*116*/ "Taiko Drum",
+ /*117*/ "Melodic Tom",
+ /*118*/ "Synth Drum",
+ /*119*/ "Reverse Cymbal",
+ /*120*/ "Guitar Fret Noise",
+ /*121*/ "Breath Noise",
+ /*122*/ "Seashore",
+ /*123*/ "Bird Tweet",
+ /*124*/ "Telephone Ring",
+ /*125*/ "Helicopter",
+ /*126*/ "Applause",
+ /*127*/ "Gunshot"
+};
+
+// The GM Percussion map is downwards compatible to the MT32 map, which is used in SCI
+static const char *const GmPercussionNames[] = {
+ /*00*/ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ /*10*/ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ /*20*/ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ /*30*/ 0, 0, 0, 0, 0,
+ // The preceding percussions are not covered by the GM standard
+ /*35*/ "Acoustic Bass Drum",
+ /*36*/ "Bass Drum 1",
+ /*37*/ "Side Stick",
+ /*38*/ "Acoustic Snare",
+ /*39*/ "Hand Clap",
+ /*40*/ "Electric Snare",
+ /*41*/ "Low Floor Tom",
+ /*42*/ "Closed Hi-Hat",
+ /*43*/ "High Floor Tom",
+ /*44*/ "Pedal Hi-Hat",
+ /*45*/ "Low Tom",
+ /*46*/ "Open Hi-Hat",
+ /*47*/ "Low-Mid Tom",
+ /*48*/ "Hi-Mid Tom",
+ /*49*/ "Crash Cymbal 1",
+ /*50*/ "High Tom",
+ /*51*/ "Ride Cymbal 1",
+ /*52*/ "Chinese Cymbal",
+ /*53*/ "Ride Bell",
+ /*54*/ "Tambourine",
+ /*55*/ "Splash Cymbal",
+ /*56*/ "Cowbell",
+ /*57*/ "Crash Cymbal 2",
+ /*58*/ "Vibraslap",
+ /*59*/ "Ride Cymbal 2",
+ /*60*/ "Hi Bongo",
+ /*61*/ "Low Bongo",
+ /*62*/ "Mute Hi Conga",
+ /*63*/ "Open Hi Conga",
+ /*64*/ "Low Conga",
+ /*65*/ "High Timbale",
+ /*66*/ "Low Timbale",
+ /*67*/ "High Agogo",
+ /*68*/ "Low Agogo",
+ /*69*/ "Cabasa",
+ /*70*/ "Maracas",
+ /*71*/ "Short Whistle",
+ /*72*/ "Long Whistle",
+ /*73*/ "Short Guiro",
+ /*74*/ "Long Guiro",
+ /*75*/ "Claves",
+ /*76*/ "Hi Wood Block",
+ /*77*/ "Low Wood Block",
+ /*78*/ "Mute Cuica",
+ /*79*/ "Open Cuica",
+ /*80*/ "Mute Triangle",
+ /*81*/ "Open Triangle"
+};
+
+#endif // REDUCE_MEMORY_USAGE
+
+} // End of namespace Dgds
+
+#endif // DGDS_SOUND_DRIVERS_GM_NAMES_H
diff --git a/engines/dgds/sound/drivers/macmixer.h b/engines/dgds/sound/drivers/macmixer.h
new file mode 100644
index 00000000000..1677afd8317
--- /dev/null
+++ b/engines/dgds/sound/drivers/macmixer.h
@@ -0,0 +1,272 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "common/frac.h"
+#include "common/mutex.h"
+#include "common/system.h"
+
+#ifndef DGDS_SOUND_DRIVERS_MACMIXER_H
+#define DGDS_SOUND_DRIVERS_MACMIXER_H
+
+namespace Dgds {
+
+// Unsigned version of frac_t
+typedef uint32 ufrac_t;
+static inline ufrac_t uintToUfrac(uint16 value) { return value << FRAC_BITS; }
+static inline uint16 ufracToUint(ufrac_t value) { return value >> FRAC_BITS; }
+
+template <typename T>
+class Mixer_Mac : public Audio::AudioStream {
+public:
+ enum {
+ kChannels = 4,
+ kInterruptFreq = 60
+ };
+
+ enum Mode {
+ kModeAuthentic,
+ kModeHq,
+ kModeHqStereo
+ };
+
+ Mixer_Mac(Mode mode);
+ void startMixer();
+ void stopMixer();
+ void setMixerVolume(byte volume) { _mixVolume = volume; }
+ void resetChannel(uint channel);
+ void resetChannels();
+ // NOTE: Last sample accessed is data[endOffset + 1] in kModeHq(Stereo)
+ void setChannelData(uint channel, const byte *data, uint16 startOffset, uint16 endOffset, uint16 loopLength = 0);
+ void setChannelStep(uint channel, ufrac_t step);
+ void setChannelVolume(uint channel, byte volume);
+ void setChannelPan(uint channel, byte pan);
+
+ // AudioStream
+ bool isStereo() const override { return _mode == kModeHqStereo; }
+ int getRate() const override { return (_mode == kModeAuthentic ? 11127 : g_system->getMixer()->getOutputRate()); }
+ int readBuffer(int16 *data, const int numSamples) override;
+ bool endOfData() const override { return false; }
+
+ Common::Mutex _mutex;
+
+private:
+ template <Mode mode>
+ void generateSamples(int16 *buf, int len);
+
+ struct Channel {
+ ufrac_t pos;
+ ufrac_t step;
+ const byte *data;
+ uint16 endOffset;
+ uint16 loopLength;
+ byte volume;
+ int8 pan;
+ };
+
+ ufrac_t _nextTick;
+ ufrac_t _samplesPerTick;
+ bool _isPlaying;
+ const Mode _mode;
+ Channel _mixChannels[kChannels];
+ byte _mixVolume;
+};
+
+template <typename T>
+Mixer_Mac<T>::Mixer_Mac(Mode mode) :
+ _nextTick(0),
+ _samplesPerTick(0),
+ _mode(mode),
+ _isPlaying(false),
+ _mixChannels(),
+ _mixVolume(8) {}
+
+template <typename T>
+void Mixer_Mac<T>::startMixer() {
+ _nextTick = _samplesPerTick = uintToUfrac(getRate() / kInterruptFreq) + uintToUfrac(getRate() % kInterruptFreq) / kInterruptFreq;
+
+ resetChannels();
+ _isPlaying = true;
+}
+
+template <typename T>
+void Mixer_Mac<T>::stopMixer() {
+ resetChannels();
+ _isPlaying = false;
+}
+
+template <typename T>
+void Mixer_Mac<T>::setChannelData(uint channel, const byte *data, uint16 startOffset, uint16 endOffset, uint16 loopLength) {
+ assert(channel < kChannels);
+
+ Channel &ch = _mixChannels[channel];
+
+ ch.data = data;
+ ch.pos = uintToUfrac(startOffset);
+ ch.endOffset = endOffset;
+ ch.loopLength = loopLength;
+}
+
+template <typename T>
+void Mixer_Mac<T>::setChannelStep(uint channel, ufrac_t step) {
+ assert(channel < kChannels);
+
+ if (_mode == kModeAuthentic) {
+ _mixChannels[channel].step = step;
+ } else {
+ // We could take 11127Hz here, but it appears the original steps were
+ // computed for 11000Hz
+ // FIXME: One or two more bits of step precision might be nice here
+ _mixChannels[channel].step = (ufrac_t)(step * 11000ULL / getRate());
+ }
+}
+
+template <typename T>
+void Mixer_Mac<T>::setChannelVolume(uint channel, byte volume) {
+ assert(channel < kChannels);
+ _mixChannels[channel].volume = volume;
+}
+
+template <typename T>
+void Mixer_Mac<T>::setChannelPan(uint channel, byte pan) {
+ assert(channel < kChannels);
+ _mixChannels[channel].pan = pan;
+}
+
+template <typename T>
+template <typename Mixer_Mac<T>::Mode mode>
+void Mixer_Mac<T>::generateSamples(int16 *data, int len) {
+ for (int i = 0; i < len; ++i) {
+ int32 mixL = 0;
+ int32 mixR = 0;
+
+ for (int ci = 0; ci < kChannels; ++ci) {
+ Channel &ch = _mixChannels[ci];
+
+ if (!ch.data)
+ continue;
+
+ const uint16 curOffset = ufracToUint(ch.pos);
+
+ if (mode == kModeHq || mode == kModeHqStereo) {
+ int32 sample = (ch.data[curOffset] - 0x80) << 8;
+ // Since _extraSamples > 0, we can safely access this sample
+ const int32 sample2 = (ch.data[curOffset + 1] - 0x80) << 8;
+ sample += fracToInt((sample2 - sample) * (ch.pos & FRAC_LO_MASK));
+ sample *= ch.volume;
+
+ if (mode == kModeHqStereo) {
+ mixL += sample * (127 - ch.pan) / (63 * 64);
+ mixR += sample * ch.pan / (63 * 64);
+ } else {
+ mixL += sample / 63;
+ }
+ } else {
+ mixL += static_cast<T *>(this)->applyChannelVolume(ch.volume, ch.data[curOffset]) << 8;
+ }
+
+ ch.pos += ch.step;
+
+ if (ufracToUint(ch.pos) > ch.endOffset) {
+ if (ch.loopLength > 0) {
+ do {
+ ch.pos -= uintToUfrac(ch.loopLength);
+ } while (ufracToUint(ch.pos) > ch.endOffset);
+ } else {
+ static_cast<T *>(this)->onChannelFinished(ci);
+ ch.data = nullptr;
+ }
+ }
+ }
+
+ *data++ = (int16)CLIP<int32>(mixL, -32768, 32767) * _mixVolume / 8;
+ if (mode == kModeHqStereo)
+ *data++ = (int16)CLIP<int32>(mixR, -32768, 32767) * _mixVolume / 8;
+ }
+}
+
+template <typename T>
+int Mixer_Mac<T>::readBuffer(int16 *data, const int numSamples) {
+ // Would probably be better inside generateSamples, but let's follow Audio::Paula
+ Common::StackLock lock(_mutex);
+
+ if (!_isPlaying) {
+ memset(data, 0, numSamples * 2);
+ return numSamples;
+ }
+
+ const int stereoFactor = isStereo() ? 2 : 1;
+ int len = numSamples / stereoFactor;
+
+ do {
+ int step = len;
+ if (step > ufracToUint(_nextTick))
+ step = ufracToUint(_nextTick);
+
+ switch (_mode) {
+ case kModeAuthentic:
+ generateSamples<kModeAuthentic>(data, step);
+ break;
+ case kModeHq:
+ generateSamples<kModeHq>(data, step);
+ break;
+ case kModeHqStereo:
+ generateSamples<kModeHqStereo>(data, step);
+ }
+
+ _nextTick -= uintToUfrac(step);
+ if (ufracToUint(_nextTick) == 0) {
+ static_cast<T *>(this)->interrupt();
+ _nextTick += _samplesPerTick;
+ }
+
+ data += step * stereoFactor;
+ len -= step;
+ } while (len);
+
+ return numSamples;
+}
+
+template <typename T>
+void Mixer_Mac<T>::resetChannel(uint channel) {
+ assert(channel < kChannels);
+
+ Channel &ch = _mixChannels[channel];
+
+ ch.pos = 0;
+ ch.step = 0;
+ ch.data = nullptr;
+ ch.endOffset = 0;
+ ch.loopLength = 0;
+ ch.volume = 0;
+ ch.pan = 64;
+}
+
+template <typename T>
+void Mixer_Mac<T>::resetChannels() {
+ for (uint ci = 0; ci < kChannels; ++ci)
+ resetChannel(ci);
+}
+
+} // End of namespace Dgds
+
+#endif // DGDS_SOUND_DRIVERS_MACMIXER_H
diff --git a/engines/dgds/sound/drivers/map-mt32-to-gm.h b/engines/dgds/sound/drivers/map-mt32-to-gm.h
new file mode 100644
index 00000000000..0cff0c6ae59
--- /dev/null
+++ b/engines/dgds/sound/drivers/map-mt32-to-gm.h
@@ -0,0 +1,373 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DGDS_SOUND_DRIVERS_MAP_MT32_TO_GM_H
+#define DGDS_SOUND_DRIVERS_MAP_MT32_TO_GM_H
+
+namespace Dgds {
+
+#include "common/list.h"
+
+// Patch not mapped
+#define MIDI_UNMAPPED 0xff
+// Patch mapped to rhythm key
+#define MIDI_MAPPED_TO_RHYTHM 0xfe
+
+struct Mt32ToGmMap {
+ const char *name;
+ uint8 gmInstr;
+ uint8 gmRhythmKey;
+};
+
+/*******************************************
+ * Fancy instrument mappings begin here... *
+ *******************************************/
+
+
+static const Mt32ToGmMap Mt32PresetTimbreMaps[] = {
+ /*000*/ {"AcouPiano1", 0, MIDI_UNMAPPED},
+ /*001*/ {"AcouPiano2", 1, MIDI_UNMAPPED},
+ /*002*/ {"AcouPiano3", 0, MIDI_UNMAPPED},
+ /*003*/ {"ElecPiano1", 4, MIDI_UNMAPPED},
+ /*004*/ {"ElecPiano2", 5, MIDI_UNMAPPED},
+ /*005*/ {"ElecPiano3", 4, MIDI_UNMAPPED},
+ /*006*/ {"ElecPiano4", 5, MIDI_UNMAPPED},
+ /*007*/ {"Honkytonk ", 3, MIDI_UNMAPPED},
+ /*008*/ {"Elec Org 1", 16, MIDI_UNMAPPED},
+ /*009*/ {"Elec Org 2", 17, MIDI_UNMAPPED},
+ /*010*/ {"Elec Org 3", 18, MIDI_UNMAPPED},
+ /*011*/ {"Elec Org 4", 18, MIDI_UNMAPPED},
+ /*012*/ {"Pipe Org 1", 19, MIDI_UNMAPPED},
+ /*013*/ {"Pipe Org 2", 19, MIDI_UNMAPPED},
+ /*014*/ {"Pipe Org 3", 20, MIDI_UNMAPPED},
+ /*015*/ {"Accordion ", 21, MIDI_UNMAPPED},
+ /*016*/ {"Harpsi 1 ", 6, MIDI_UNMAPPED},
+ /*017*/ {"Harpsi 2 ", 6, MIDI_UNMAPPED},
+ /*018*/ {"Harpsi 3 ", 6, MIDI_UNMAPPED},
+ /*019*/ {"Clavi 1 ", 7, MIDI_UNMAPPED},
+ /*020*/ {"Clavi 2 ", 7, MIDI_UNMAPPED},
+ /*021*/ {"Clavi 3 ", 7, MIDI_UNMAPPED},
+ /*022*/ {"Celesta 1 ", 8, MIDI_UNMAPPED},
+ /*023*/ {"Celesta 2 ", 8, MIDI_UNMAPPED},
+ /*024*/ {"Syn Brass1", 62, MIDI_UNMAPPED},
+ /*025*/ {"Syn Brass2", 63, MIDI_UNMAPPED},
+ /*026*/ {"Syn Brass3", 62, MIDI_UNMAPPED},
+ /*027*/ {"Syn Brass4", 63, MIDI_UNMAPPED},
+ /*028*/ {"Syn Bass 1", 38, MIDI_UNMAPPED},
+ /*029*/ {"Syn Bass 2", 39, MIDI_UNMAPPED},
+ /*030*/ {"Syn Bass 3", 38, MIDI_UNMAPPED},
+ /*031*/ {"Syn Bass 4", 39, MIDI_UNMAPPED},
+ /*032*/ {"Fantasy ", 88, MIDI_UNMAPPED},
+ /*033*/ {"Harmo Pan ", 89, MIDI_UNMAPPED},
+ /*034*/ {"Chorale ", 52, MIDI_UNMAPPED},
+ /*035*/ {"Glasses ", 98, MIDI_UNMAPPED},
+ /*036*/ {"Soundtrack", 97, MIDI_UNMAPPED},
+ /*037*/ {"Atmosphere", 99, MIDI_UNMAPPED},
+ /*038*/ {"Warm Bell ", 89, MIDI_UNMAPPED},
+ /*039*/ {"Funny Vox ", 85, MIDI_UNMAPPED},
+ /*040*/ {"Echo Bell ", 39, MIDI_UNMAPPED},
+ /*041*/ {"Ice Rain ", 101, MIDI_UNMAPPED},
+ /*042*/ {"Oboe 2001 ", 68, MIDI_UNMAPPED},
+ /*043*/ {"Echo Pan ", 87, MIDI_UNMAPPED},
+ /*044*/ {"DoctorSolo", 86, MIDI_UNMAPPED},
+ /*045*/ {"Schooldaze", 103, MIDI_UNMAPPED},
+ /*046*/ {"BellSinger", 88, MIDI_UNMAPPED},
+ /*047*/ {"SquareWave", 80, MIDI_UNMAPPED},
+ /*048*/ {"Str Sect 1", 48, MIDI_UNMAPPED},
+ /*049*/ {"Str Sect 2", 48, MIDI_UNMAPPED},
+ /*050*/ {"Str Sect 3", 49, MIDI_UNMAPPED},
+ /*051*/ {"Pizzicato ", 45, MIDI_UNMAPPED},
+ /*052*/ {"Violin 1 ", 40, MIDI_UNMAPPED},
+ /*053*/ {"Violin 2 ", 40, MIDI_UNMAPPED},
+ /*054*/ {"Cello 1 ", 42, MIDI_UNMAPPED},
+ /*055*/ {"Cello 2 ", 42, MIDI_UNMAPPED},
+ /*056*/ {"Contrabass", 43, MIDI_UNMAPPED},
+ /*057*/ {"Harp 1 ", 46, MIDI_UNMAPPED},
+ /*058*/ {"Harp 2 ", 46, MIDI_UNMAPPED},
+ /*059*/ {"Guitar 1 ", 24, MIDI_UNMAPPED},
+ /*060*/ {"Guitar 2 ", 25, MIDI_UNMAPPED},
+ /*061*/ {"Elec Gtr 1", 26, MIDI_UNMAPPED},
+ /*062*/ {"Elec Gtr 2", 27, MIDI_UNMAPPED},
+ /*063*/ {"Sitar ", 104, MIDI_UNMAPPED},
+ /*064*/ {"Acou Bass1", 32, MIDI_UNMAPPED},
+ /*065*/ {"Acou Bass2", 33, MIDI_UNMAPPED},
+ /*066*/ {"Elec Bass1", 34, MIDI_UNMAPPED},
+ /*067*/ {"Elec Bass2", 39, MIDI_UNMAPPED},
+ /*068*/ {"Slap Bass1", 36, MIDI_UNMAPPED},
+ /*069*/ {"Slap Bass2", 37, MIDI_UNMAPPED},
+ /*070*/ {"Fretless 1", 35, MIDI_UNMAPPED},
+ /*071*/ {"Fretless 2", 35, MIDI_UNMAPPED},
+ /*072*/ {"Flute 1 ", 73, MIDI_UNMAPPED},
+ /*073*/ {"Flute 2 ", 73, MIDI_UNMAPPED},
+ /*074*/ {"Piccolo 1 ", 72, MIDI_UNMAPPED},
+ /*075*/ {"Piccolo 2 ", 72, MIDI_UNMAPPED},
+ /*076*/ {"Recorder ", 74, MIDI_UNMAPPED},
+ /*077*/ {"Panpipes ", 75, MIDI_UNMAPPED},
+ /*078*/ {"Sax 1 ", 64, MIDI_UNMAPPED},
+ /*079*/ {"Sax 2 ", 65, MIDI_UNMAPPED},
+ /*080*/ {"Sax 3 ", 66, MIDI_UNMAPPED},
+ /*081*/ {"Sax 4 ", 67, MIDI_UNMAPPED},
+ /*082*/ {"Clarinet 1", 71, MIDI_UNMAPPED},
+ /*083*/ {"Clarinet 2", 71, MIDI_UNMAPPED},
+ /*084*/ {"Oboe ", 68, MIDI_UNMAPPED},
+ /*085*/ {"Engl Horn ", 69, MIDI_UNMAPPED},
+ /*086*/ {"Bassoon ", 70, MIDI_UNMAPPED},
+ /*087*/ {"Harmonica ", 22, MIDI_UNMAPPED},
+ /*088*/ {"Trumpet 1 ", 56, MIDI_UNMAPPED},
+ /*089*/ {"Trumpet 2 ", 56, MIDI_UNMAPPED},
+ /*090*/ {"Trombone 1", 57, MIDI_UNMAPPED},
+ /*091*/ {"Trombone 2", 57, MIDI_UNMAPPED},
+ /*092*/ {"Fr Horn 1 ", 60, MIDI_UNMAPPED},
+ /*093*/ {"Fr Horn 2 ", 60, MIDI_UNMAPPED},
+ /*094*/ {"Tuba ", 58, MIDI_UNMAPPED},
+ /*095*/ {"Brs Sect 1", 61, MIDI_UNMAPPED},
+ /*096*/ {"Brs Sect 2", 61, MIDI_UNMAPPED},
+ /*097*/ {"Vibe 1 ", 11, MIDI_UNMAPPED},
+ /*098*/ {"Vibe 2 ", 11, MIDI_UNMAPPED},
+ /*099*/ {"Syn Mallet", 15, MIDI_UNMAPPED},
+ /*100*/ {"Wind Bell ", 88, MIDI_UNMAPPED},
+ /*101*/ {"Glock ", 9, MIDI_UNMAPPED},
+ /*102*/ {"Tube Bell ", 14, MIDI_UNMAPPED},
+ /*103*/ {"Xylophone ", 13, MIDI_UNMAPPED},
+ /*104*/ {"Marimba ", 12, MIDI_UNMAPPED},
+ /*105*/ {"Koto ", 107, MIDI_UNMAPPED},
+ /*106*/ {"Sho ", 111, MIDI_UNMAPPED},
+ /*107*/ {"Shakuhachi", 77, MIDI_UNMAPPED},
+ /*108*/ {"Whistle 1 ", 78, MIDI_UNMAPPED},
+ /*109*/ {"Whistle 2 ", 78, MIDI_UNMAPPED},
+ /*110*/ {"BottleBlow", 76, MIDI_UNMAPPED},
+ /*111*/ {"BreathPipe", 121, MIDI_UNMAPPED},
+ /*112*/ {"Timpani ", 47, MIDI_UNMAPPED},
+ /*113*/ {"MelodicTom", 117, MIDI_UNMAPPED},
+ /*114*/ {"Deep Snare", MIDI_MAPPED_TO_RHYTHM, 38},
+ /*115*/ {"Elec Perc1", 115, MIDI_UNMAPPED}, // ?
+ /*116*/ {"Elec Perc2", 118, MIDI_UNMAPPED}, // ?
+ /*117*/ {"Taiko ", 116, MIDI_UNMAPPED},
+ /*118*/ {"Taiko Rim ", 118, MIDI_UNMAPPED},
+ /*119*/ {"Cymbal ", MIDI_MAPPED_TO_RHYTHM, 51},
+ /*120*/ {"Castanets ", MIDI_MAPPED_TO_RHYTHM, 75}, // approximation
+ /*121*/ {"Triangle ", 112, MIDI_UNMAPPED},
+ /*122*/ {"Orche Hit ", 55, MIDI_UNMAPPED},
+ /*123*/ {"Telephone ", 124, MIDI_UNMAPPED},
+ /*124*/ {"Bird Tweet", 123, MIDI_UNMAPPED},
+ /*125*/ {"OneNoteJam", 8, MIDI_UNMAPPED}, // approximation
+ /*126*/ {"WaterBells", 98, MIDI_UNMAPPED},
+ /*127*/ {"JungleTune", 75, MIDI_UNMAPPED} // approximation
+};
+
+static const Mt32ToGmMap Mt32RhythmTimbreMaps[] = {
+ /*00*/ {"Acou BD ", MIDI_MAPPED_TO_RHYTHM, 35},
+ /*01*/ {"Acou SD ", MIDI_MAPPED_TO_RHYTHM, 38},
+ /*02*/ {"Acou HiTom", 117, 50},
+ /*03*/ {"AcouMidTom", 117, 47},
+ /*04*/ {"AcouLowTom", 117, 41},
+ /*05*/ {"Elec SD ", MIDI_MAPPED_TO_RHYTHM, 40},
+ /*06*/ {"Clsd HiHat", MIDI_MAPPED_TO_RHYTHM, 42},
+ /*07*/ {"OpenHiHat1", MIDI_MAPPED_TO_RHYTHM, 46},
+ /*08*/ {"Crash Cym ", MIDI_MAPPED_TO_RHYTHM, 49},
+ /*09*/ {"Ride Cym ", MIDI_MAPPED_TO_RHYTHM, 51},
+ /*10*/ {"Rim Shot ", MIDI_MAPPED_TO_RHYTHM, 37},
+ /*11*/ {"Hand Clap ", MIDI_MAPPED_TO_RHYTHM, 39},
+ /*12*/ {"Cowbell ", MIDI_MAPPED_TO_RHYTHM, 56},
+ /*13*/ {"Mt HiConga", MIDI_MAPPED_TO_RHYTHM, 62},
+ /*14*/ {"High Conga", MIDI_MAPPED_TO_RHYTHM, 63},
+ /*15*/ {"Low Conga ", MIDI_MAPPED_TO_RHYTHM, 64},
+ /*16*/ {"Hi Timbale", MIDI_MAPPED_TO_RHYTHM, 65},
+ /*17*/ {"LowTimbale", MIDI_MAPPED_TO_RHYTHM, 66},
+ /*18*/ {"High Bongo", MIDI_MAPPED_TO_RHYTHM, 60},
+ /*19*/ {"Low Bongo ", MIDI_MAPPED_TO_RHYTHM, 61},
+ /*20*/ {"High Agogo", 113, 67},
+ /*21*/ {"Low Agogo ", 113, 68},
+ /*22*/ {"Tambourine", MIDI_MAPPED_TO_RHYTHM, 54},
+ /*23*/ {"Claves ", MIDI_MAPPED_TO_RHYTHM, 75},
+ /*24*/ {"Maracas ", MIDI_MAPPED_TO_RHYTHM, 70},
+ /*25*/ {"SmbaWhis L", 78, 72},
+ /*26*/ {"SmbaWhis S", 78, 71},
+ /*27*/ {"Cabasa ", MIDI_MAPPED_TO_RHYTHM, 69},
+ /*28*/ {"Quijada ", MIDI_MAPPED_TO_RHYTHM, 73},
+ /*29*/ {"OpenHiHat2", MIDI_MAPPED_TO_RHYTHM, 44}
+};
+
+static const uint8 Mt32PresetRhythmKeymap[] = {
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, 35, 36, 37, 38, 39,
+ 40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
+ 50, 51, MIDI_UNMAPPED, MIDI_UNMAPPED, 54, MIDI_UNMAPPED, 56, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
+ 70, 71, 72, 73, MIDI_UNMAPPED, 75, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED,
+ MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED, MIDI_UNMAPPED
+};
+
+/* +++ - Don't change unless you've got a good reason
+ ++ - Looks good, sounds ok
+ + - Not too bad, but is it right?
+ ? - Where do I map this one?
+ ?? - Any good ideas?
+ ??? - I'm clueless?
+ R - Rhythm...
+*/
+static const Mt32ToGmMap Mt32MemoryTimbreMaps[] = {
+ {"AccPnoKA2 ", 1, MIDI_UNMAPPED}, // ++ (KQ1)
+ {"Acou BD ", MIDI_MAPPED_TO_RHYTHM, 35}, // R (PQ2)
+ {"Acou SD ", MIDI_MAPPED_TO_RHYTHM, 38}, // R (PQ2)
+ {"AcouPnoKA ", 0, MIDI_UNMAPPED}, // ++ (KQ1)
+ {"BASS ", 32, MIDI_UNMAPPED}, // + (LSL3)
+ {"BASSOONPCM", 70, MIDI_UNMAPPED}, // + (LB1)
+ {"BEACH WAVE", 122, MIDI_UNMAPPED}, // + (LSL3)
+ {"BagPipes ", 109, MIDI_UNMAPPED},
+ {"BassPizzMS", 45, MIDI_UNMAPPED}, // ++ (QFG1)
+ {"BassoonKA ", 70, MIDI_UNMAPPED}, // ++ (KQ1)
+ {"Bell MS", 112, MIDI_UNMAPPED}, // ++ (Iceman)
+ {"Bells MS", 112, MIDI_UNMAPPED}, // + (QFG1)
+ {"Big Bell ", 14, MIDI_UNMAPPED}, // + (LB1)
+ {"Bird Tweet", 123, MIDI_UNMAPPED},
+ {"BrsSect MS", 61, MIDI_UNMAPPED}, // +++ (Iceman)
+ {"CLAPPING ", 126, MIDI_UNMAPPED}, // ++ (LSL3)
+ {"Cabasa ", MIDI_MAPPED_TO_RHYTHM, 69}, // R (Hoyle)
+ {"Calliope ", 82, MIDI_UNMAPPED}, // +++ (QFG1)
+ {"CelticHarp", 46, MIDI_UNMAPPED}, // ++ (Camelot)
+ {"Chicago MS", 1, MIDI_UNMAPPED}, // ++ (Iceman)
+ {"Chop ", 117, MIDI_UNMAPPED},
+ {"Chorale MS", 52, MIDI_UNMAPPED}, // + (Camelot)
+ {"ClarinetMS", 71, MIDI_UNMAPPED},
+ {"Claves ", MIDI_MAPPED_TO_RHYTHM, 75}, // R (PQ2)
+ {"Claw MS", 118, MIDI_UNMAPPED}, // + (QFG1)
+ {"ClockBell ", 14, MIDI_UNMAPPED}, // + (LB1)
+ {"ConcertCym", MIDI_MAPPED_TO_RHYTHM, 55}, // R ? (KQ1)
+ {"Conga MS", MIDI_MAPPED_TO_RHYTHM, 64}, // R (QFG1)
+ {"CoolPhone ", 124, MIDI_UNMAPPED}, // ++ (LSL3)
+ {"CracklesMS", 115, MIDI_UNMAPPED}, // ? (Camelot, QFG1)
+ {"CreakyD MS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ??? (KQ1)
+ {"Cricket ", 120, MIDI_UNMAPPED}, // ? (LB1)
+ {"CrshCymbMS", MIDI_MAPPED_TO_RHYTHM, 57}, // R +++ (Iceman)
+ {"CstlGateMS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (QFG1)
+ {"CymSwellMS", MIDI_MAPPED_TO_RHYTHM, 55}, // R ? (Camelot, QFG1)
+ {"CymbRollKA", MIDI_MAPPED_TO_RHYTHM, 57}, // R ? (KQ1)
+ {"Cymbal Lo ", MIDI_UNMAPPED, MIDI_UNMAPPED}, // R ? (LSL3)
+ {"card ", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (Hoyle)
+ {"DirtGtr MS", 30, MIDI_UNMAPPED}, // + (Iceman)
+ {"DirtGtr2MS", 29, MIDI_UNMAPPED}, // + (Iceman)
+ {"E Bass MS", 33, MIDI_UNMAPPED}, // + (SQ3)
+ {"ElecBassMS", 33, MIDI_UNMAPPED},
+ {"ElecGtr MS", 27, MIDI_UNMAPPED}, // ++ (Iceman)
+ {"EnglHornMS", 69, MIDI_UNMAPPED},
+ {"FantasiaKA", 88, MIDI_UNMAPPED},
+ {"Fantasy ", 99, MIDI_UNMAPPED}, // + (PQ2)
+ {"Fantasy2MS", 99, MIDI_UNMAPPED}, // ++ (Camelot, QFG1)
+ {"Filter MS", 95, MIDI_UNMAPPED}, // +++ (Iceman)
+ {"Filter2 MS", 95, MIDI_UNMAPPED}, // ++ (Iceman)
+ {"Flame2 MS", 121, MIDI_UNMAPPED}, // ? (QFG1)
+ {"Flames MS", 121, MIDI_UNMAPPED}, // ? (QFG1)
+ {"Flute MS", 73, MIDI_UNMAPPED}, // +++ (QFG1)
+ {"FogHorn MS", 58, MIDI_UNMAPPED},
+ {"FrHorn1 MS", 60, MIDI_UNMAPPED}, // +++ (QFG1)
+ {"FunnyTrmp ", 56, MIDI_UNMAPPED}, // ++ (LB1)
+ {"GameSnd MS", 80, MIDI_UNMAPPED},
+ {"Glock MS", 9, MIDI_UNMAPPED}, // +++ (QFG1)
+ {"Gunshot ", 127, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Hammer MS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (QFG1)
+ {"Harmonica2", 22, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Harpsi 1 ", 6, MIDI_UNMAPPED}, // + (Hoyle)
+ {"Harpsi 2 ", 6, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Heart MS", 116, MIDI_UNMAPPED}, // ? (Iceman)
+ {"Horse1 MS", 115, MIDI_UNMAPPED}, // ? (Camelot, QFG1)
+ {"Horse2 MS", 115, MIDI_UNMAPPED}, // ? (Camelot, QFG1)
+ {"InHale MS", 121, MIDI_UNMAPPED}, // ++ (Iceman)
+ {"KNIFE ", 120, MIDI_UNMAPPED}, // ? (LSL3)
+ {"KenBanjo ", 105, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Kiss MS", 25, MIDI_UNMAPPED}, // ++ (QFG1)
+ {"KongHit ", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ??? (KQ1)
+ {"Koto ", 107, MIDI_UNMAPPED}, // +++ (PQ2)
+ {"Laser MS", 81, MIDI_UNMAPPED}, // ?? (QFG1)
+ {"Meeps MS", 62, MIDI_UNMAPPED}, // ? (QFG1)
+ {"MTrak MS", 62, MIDI_UNMAPPED}, // ?? (Iceman)
+ {"MachGun MS", 127, MIDI_UNMAPPED}, // ? (Iceman)
+ {"OCEANSOUND", 122, MIDI_UNMAPPED}, // + (LSL3)
+ {"Oboe 2001 ", 68, MIDI_UNMAPPED}, // + (PQ2)
+ {"Ocean MS", 122, MIDI_UNMAPPED}, // + (Iceman)
+ {"PPG 2.3 MS", 75, MIDI_UNMAPPED}, // ? (Iceman)
+ {"PianoCrank", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (LB1)
+ {"PicSnareMS", MIDI_MAPPED_TO_RHYTHM, 40}, // R ? (Iceman)
+ {"PiccoloKA ", 72, MIDI_UNMAPPED}, // +++ (KQ1)
+ {"PinkBassMS", 39, MIDI_UNMAPPED},
+ {"Pizz2 ", 45, MIDI_UNMAPPED}, // ++ (LB1)
+ {"Portcullis", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (KQ1)
+ {"Raspbry MS", 81, MIDI_UNMAPPED}, // ? (QFG1)
+ {"RatSqueek ", 72, MIDI_UNMAPPED}, // ? (LauraBow1, Camelot)
+ {"Record78 ", MIDI_UNMAPPED, MIDI_UNMAPPED}, // +++ (LB1)
+ {"RecorderMS", 74, MIDI_UNMAPPED}, // +++ (Camelot)
+ {"Red Baron ", 125, MIDI_UNMAPPED}, // ? (LB1)
+ {"ReedPipMS ", 20, MIDI_UNMAPPED}, // +++ (Camelot)
+ {"RevCymb MS", 119, MIDI_UNMAPPED},
+ {"RifleShot ", 127, MIDI_UNMAPPED}, // + (LB1)
+ {"RimShot MS", MIDI_MAPPED_TO_RHYTHM, 37}, // R
+ {"SHOWER ", 52, MIDI_UNMAPPED}, // ? (LSL3)
+ {"SQ Bass MS", 32, MIDI_UNMAPPED}, // + (SQ3)
+ {"ShakuVibMS", 79, MIDI_UNMAPPED}, // + (Iceman)
+ {"SlapBassMS", 36, MIDI_UNMAPPED}, // +++ (Iceman)
+ {"Snare MS", MIDI_MAPPED_TO_RHYTHM, 38}, // R (QFG1)
+ {"Some Birds", 123, MIDI_UNMAPPED}, // + (LB1)
+ {"Sonar MS", 78, MIDI_UNMAPPED}, // ? (Iceman)
+ {"Soundtrk2 ", 97, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Soundtrack", 97, MIDI_UNMAPPED}, // ++ (Camelot)
+ {"SqurWaveMS", 80, MIDI_UNMAPPED},
+ {"StabBassMS", 34, MIDI_UNMAPPED}, // + (Iceman)
+ {"SteelDrmMS", 114, MIDI_UNMAPPED}, // +++ (Iceman)
+ {"StrSect1MS", 48, MIDI_UNMAPPED}, // ++ (QFG1)
+ {"String MS", 45, MIDI_UNMAPPED}, // + (Camelot)
+ {"Syn-Choir ", 91, MIDI_UNMAPPED},
+ {"Syn Brass4", 63, MIDI_UNMAPPED}, // ++ (PQ2)
+ {"SynBass MS", 38, MIDI_UNMAPPED},
+ {"SwmpBackgr", 120, MIDI_UNMAPPED}, // ?? (LB1, QFG1)
+ {"T-Bone2 MS", 57, MIDI_UNMAPPED}, // +++ (QFG1)
+ {"Taiko ", 116, 35}, // +++ (Camelot)
+ {"Taiko Rim ", 118, 37}, // +++ (LSL3)
+ {"Timpani1 ", 47, MIDI_UNMAPPED}, // +++ (LB1)
+ {"Tom MS", 117, 48}, // +++ (Iceman)
+ {"Toms MS", 117, 48}, // +++ (Camelot, QFG1)
+ {"Tpt1prtl ", 56, MIDI_UNMAPPED}, // +++ (KQ1)
+ {"TriangleMS", 112, 81}, // R (Camelot)
+ {"Trumpet 1 ", 56, MIDI_UNMAPPED}, // +++ (Camelot)
+ {"Type MS", MIDI_MAPPED_TO_RHYTHM, 39}, // + (Iceman)
+ {"Warm Pad" , 89, MIDI_UNMAPPED}, // ++ (PQ3)
+ {"WaterBells", 98, MIDI_UNMAPPED}, // + (PQ2)
+ {"WaterFallK", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (KQ1)
+ {"Whiporill ", 123, MIDI_UNMAPPED}, // + (LB1)
+ {"Wind ", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (LB1)
+ {"Wind MS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (QFG1, Iceman)
+ {"Wind2 MS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (Camelot)
+ {"Woodpecker", 115, MIDI_UNMAPPED}, // ? (LB1)
+ {"WtrFall MS", MIDI_UNMAPPED, MIDI_UNMAPPED}, // ? (Camelot, QFG1, Iceman)
+ {0, 0, 0}
+};
+
+ typedef Common::List<Mt32ToGmMap> Mt32ToGmMapList;
+ extern Mt32ToGmMapList *Mt32dynamicMappings;
+
+} // End of namespace Dgds
+
+#endif // DGDS_SOUND_DRIVERS_MAP_MT32_TO_GM_H
diff --git a/engines/dgds/sound/drivers/midi.cpp b/engines/dgds/sound/drivers/midi.cpp
new file mode 100644
index 00000000000..5f710c652f0
--- /dev/null
+++ b/engines/dgds/sound/drivers/midi.cpp
@@ -0,0 +1,1098 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/config-manager.h"
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/system.h"
+
+#include "audio/mididrv.h"
+#include "audio/mt32gm.h"
+
+#include "dgds/sound/drivers/gm_names.h"
+#include "dgds/sound/drivers/mididriver.h"
+#include "dgds/sound/drivers/map-mt32-to-gm.h"
+#include "dgds/sound/resource/sci_resource.h"
+
+namespace Dgds {
+
+Mt32ToGmMapList *Mt32dynamicMappings = nullptr;
+
+class MidiPlayer_Midi : public MidiPlayer {
+public:
+ enum {
+ kVoices = 32,
+ kReverbConfigNr = 11,
+ kMaxSysExSize = 264
+ };
+
+ enum Mt32Type {
+ kMt32TypeNone,
+ kMt32TypeReal,
+ kMt32TypeEmulated
+ };
+
+ MidiPlayer_Midi();
+ ~MidiPlayer_Midi() override;
+
+ int open(ResourceManager *resMan) override;
+ void close() override;
+ void send(uint32 b) override;
+ void sysEx(const byte *msg, uint16 length) override;
+ uint16 sysExNoDelay(const byte *msg, uint16 length) override;
+ bool hasRhythmChannel() const override { return true; }
+ byte getPlayId() const override;
+ int getPolyphony() const override {
+ return kVoices;
+ }
+ int getFirstChannel() const override;
+ int getLastChannel() const override;
+ void setVolume(byte volume) override;
+ int getVolume() override;
+ void setReverb(int8 reverb) override;
+ void playSwitch(bool play) override;
+ void initTrack(SciSpan<const byte> &) override;
+
+private:
+ bool isMt32GmPatch(const SciSpan<const byte> &data);
+ void readMt32GmPatch(const SciSpan<const byte> &data);
+ void readMt32Patch(const SciSpan<const byte> &data);
+ void readMt32DrvData();
+
+ void mapMt32ToGm(const SciSpan<const byte> &data);
+ uint8 lookupGmInstrument(const char *iname);
+ uint8 lookupGmRhythmKey(const char *iname);
+ uint8 getGmInstrument(const Mt32ToGmMap &Mt32Ins);
+
+ void sendMt32SysEx(const uint32 addr, Common::SeekableReadStream &data, const int len, bool noDelay, bool mainThread);
+ void sendMt32SysEx(const uint32 addr, const SciSpan<const byte> &data, bool noDelay, bool mainThread);
+ void setMt32Volume(byte volume);
+ void resetMt32();
+
+ void noteOn(int channel, int note, int velocity);
+ void setPatch(int channel, int patch);
+ void controlChange(int channel, int control, int value);
+
+ struct Channel {
+ byte mappedPatch;
+ byte patch;
+ int velocityMapIdx;
+ bool playing;
+ int8 keyShift;
+ int8 volAdjust;
+ uint8 pan;
+ uint8 hold;
+ uint8 volume;
+
+ Channel() : mappedPatch(MIDI_UNMAPPED), patch(MIDI_UNMAPPED), velocityMapIdx(0), playing(false),
+ keyShift(0), volAdjust(0), pan(0x40), hold(0), volume(0x7f) { }
+ };
+
+ Mt32Type _mt32Type;
+ uint _mt32LCDSize;
+ bool _useMT32Track;
+ bool _hasReverb;
+ bool _playSwitch;
+ int _masterVolume;
+
+ byte _reverbConfig[kReverbConfigNr][3];
+ int8 _defaultReverb;
+ Channel _channels[16];
+ uint8 _percussionMap[128];
+ int8 _keyShift[128];
+ int8 _volAdjust[128];
+ uint8 _patchMap[128];
+ uint8 _velocityMapIdx[128];
+ uint8 _velocityMap[4][128];
+
+ // These are extensions used for our own MT-32 to GM mapping
+ uint8 _pitchBendRange[128];
+ uint8 _percussionVelocityScale[128];
+
+ byte _goodbyeMsg[32];
+ byte _sysExBuf[kMaxSysExSize];
+
+};
+
+MidiPlayer_Midi::MidiPlayer_Midi() :
+ MidiPlayer(),
+ _playSwitch(true),
+ _masterVolume(15),
+ _mt32Type(kMt32TypeNone),
+ _mt32LCDSize(20),
+ _hasReverb(false),
+ _defaultReverb(-1),
+ _useMT32Track(true) {
+
+ MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI);
+ _driver = MidiDriver::createMidi(dev);
+
+ if (MidiDriver::getMusicType(dev) == MT_MT32 || ConfMan.getBool("native_mt32")) {
+ if (MidiDriver::getDeviceString(dev, MidiDriver::kDriverId) == "mt32") {
+ _mt32Type = kMt32TypeEmulated;
+ } else {
+ _mt32Type = kMt32TypeReal;
+ }
+ }
+
+ _sysExBuf[0] = 0x41;
+ _sysExBuf[1] = 0x10;
+ _sysExBuf[2] = 0x16;
+ _sysExBuf[3] = 0x12;
+
+ Mt32dynamicMappings = new Mt32ToGmMapList();
+}
+
+MidiPlayer_Midi::~MidiPlayer_Midi() {
+ delete _driver;
+
+ const Mt32ToGmMapList::iterator end = Mt32dynamicMappings->end();
+ for (Mt32ToGmMapList::iterator it = Mt32dynamicMappings->begin(); it != end; ++it) {
+ delete[] (*it).name;
+ (*it).name = nullptr;
+ }
+
+ Mt32dynamicMappings->clear();
+ delete Mt32dynamicMappings;
+}
+
+void MidiPlayer_Midi::noteOn(int channel, int note, int velocity) {
+ uint8 patch = _channels[channel].mappedPatch;
+
+ assert(channel <= 15);
+ assert(note <= 127);
+ assert(velocity <= 127);
+
+ if (channel == MIDI_RHYTHM_CHANNEL) {
+ if (_percussionMap[note] == MIDI_UNMAPPED) {
+ debugC(kDebugLevelSound, "[Midi] Percussion instrument %i is unmapped", note);
+ return;
+ }
+
+ note = _percussionMap[note];
+ // Scale velocity;
+ velocity = velocity * _percussionVelocityScale[note] / 127;
+ } else if (patch >= 128) {
+ if (patch == MIDI_UNMAPPED)
+ return;
+
+ // Map to rhythm
+ channel = MIDI_RHYTHM_CHANNEL;
+ note = patch - 128;
+
+ // Scale velocity;
+ velocity = velocity * _percussionVelocityScale[note] / 127;
+ } else {
+ int8 keyshift = _channels[channel].keyShift;
+
+ int shiftNote = note + keyshift;
+
+ if (keyshift > 0) {
+ while (shiftNote > 127)
+ shiftNote -= 12;
+ } else {
+ while (shiftNote < 0)
+ shiftNote += 12;
+ }
+
+ note = shiftNote;
+
+ // We assume that velocity 0 maps to 0 (for note off)
+ int mapIndex = _channels[channel].velocityMapIdx;
+ assert(velocity <= 127);
+ velocity = _velocityMap[mapIndex][velocity];
+ }
+
+ _channels[channel].playing = true;
+ _driver->send(0x90 | channel, note, velocity);
+}
+
+void MidiPlayer_Midi::controlChange(int channel, int control, int value) {
+ assert(channel <= 15);
+ bool standard_midi_controller = true;
+
+ switch (control) {
+ case 0x07:
+ _channels[channel].volume = value;
+
+ if (!_playSwitch)
+ return;
+
+ value += _channels[channel].volAdjust;
+
+ if (value > 0x7f)
+ value = 0x7f;
+
+ if (value < 0)
+ value = 1;
+
+ value *= _masterVolume;
+
+ if (value != 0) {
+ value /= 15;
+
+ if (value == 0)
+ value = 1;
+ }
+ break;
+ case 0x0a:
+ _channels[channel].pan = value;
+ break;
+ case 0x40:
+ _channels[channel].hold = value;
+ break;
+ case 0x4b: // voice mapping
+ // this is an internal Sierra command, and shouldn't be sent to the real MIDI driver - fixing #11409
+ standard_midi_controller = false;
+ break;
+ case 0x4e: // velocity
+ break;
+ case 0x7b:
+ _channels[channel].playing = false;
+ default:
+ break;
+ }
+
+ if (standard_midi_controller)
+ _driver->send(0xb0 | channel, control, value);
+}
+
+void MidiPlayer_Midi::setPatch(int channel, int patch) {
+ bool resetVol = false;
+
+ assert(channel <= 15);
+
+ // No need to do anything if a patch change is sent on the rhythm channel of an MT-32
+ // or if the requested patch is the same as the current patch.
+ if ((_mt32Type != kMt32TypeNone && channel == MIDI_RHYTHM_CHANNEL) || (_channels[channel].patch == patch))
+ return;
+
+ int patchToSend;
+ if (channel != MIDI_RHYTHM_CHANNEL) {
+ _channels[channel].patch = patch;
+ _channels[channel].velocityMapIdx = _velocityMapIdx[patch];
+
+ if (_channels[channel].mappedPatch == MIDI_UNMAPPED)
+ resetVol = true;
+
+ _channels[channel].mappedPatch = patchToSend = _patchMap[patch];
+
+ if (_patchMap[patch] == MIDI_UNMAPPED) {
+ debugC(kDebugLevelSound, "[Midi] Channel %i set to unmapped patch %i", channel, patch);
+ _driver->send(0xb0 | channel, 0x7b, 0);
+ _driver->send(0xb0 | channel, 0x40, 0);
+ return;
+ }
+
+ if (_patchMap[patch] >= 128) {
+ // Mapped to rhythm, don't send channel commands
+ return;
+ }
+
+ if (_channels[channel].keyShift != _keyShift[patch]) {
+ _channels[channel].keyShift = _keyShift[patch];
+ _driver->send(0xb0 | channel, 0x7b, 0);
+ _driver->send(0xb0 | channel, 0x40, 0);
+ resetVol = true;
+ }
+
+ if (resetVol || (_channels[channel].volAdjust != _volAdjust[patch])) {
+ _channels[channel].volAdjust = _volAdjust[patch];
+ controlChange(channel, 0x07, _channels[channel].volume);
+ }
+
+ uint8 bendRange = _pitchBendRange[patch];
+ if (bendRange != MIDI_UNMAPPED)
+ _driver->setPitchBendRange(channel, bendRange);
+ } else {
+ // A patch change on the rhythm channel of a Roland GS device indicates a drumkit change.
+ // Some GM devices support the GS drumkits as well.
+
+ // Apply drumkit fallback to correct invalid drumkit numbers.
+ patchToSend = patch < 128 ? MidiDriver_MT32GM::GS_DRUMKIT_FALLBACK_MAP[patch] : 0;
+ _channels[channel].patch = patchToSend;
+ debugC(kDebugLevelSound, "[Midi] Selected drumkit %i (requested %i)", patchToSend, patch);
+ }
+
+ _driver->send(0xc0 | channel, patchToSend, 0);
+
+ // Send a pointless command to work around a firmware bug in common
+ // USB-MIDI cables. If the first MIDI command in a USB packet is a
+ // Cx or Dx command, the second command in the packet is dropped
+ // somewhere.
+ // FIXME: consider putting a workaround in the MIDI backend drivers
+ // instead.
+ // Known to be affected: alsa, coremidi
+ // Known *not* to be affected: windows (only seems to send one MIDI
+ // command per USB packet even if the device allows larger packets).
+ _driver->send(0xb0 | channel, 0x0a, _channels[channel].pan);
+}
+
+void MidiPlayer_Midi::send(uint32 b) {
+ byte command = b & 0xf0;
+ byte channel = b & 0xf;
+ byte op1 = (b >> 8) & 0x7f;
+ byte op2 = (b >> 16) & 0x7f;
+
+ switch (command) {
+ case 0x80:
+ noteOn(channel, op1, 0);
+ break;
+ case 0x90:
+ noteOn(channel, op1, op2);
+ break;
+ case 0xb0:
+ controlChange(channel, op1, op2);
+ break;
+ case 0xc0:
+ setPatch(channel, op1);
+ break;
+ // The original MIDI driver from sierra ignores aftertouch completely, so should we
+ case 0xa0: // Polyphonic key pressure (aftertouch)
+ case 0xd0: // Channel pressure (aftertouch)
+ break;
+ case 0xe0:
+ _driver->send(b);
+ break;
+ default:
+ warning("Ignoring MIDI event %02x", command);
+ }
+}
+
+// We return 1 for mt32, because if we remap channels to 0 for mt32, those won't get played at all
+// NOTE: SSCI uses channels 1 through 8 for General MIDI as well, in the drivers I checked
+int MidiPlayer_Midi::getFirstChannel() const {
+ return 1;
+}
+
+int MidiPlayer_Midi::getLastChannel() const {
+ return 8;
+}
+
+void MidiPlayer_Midi::setVolume(byte volume) {
+ _masterVolume = volume;
+
+ if (!_playSwitch)
+ return;
+
+ for (uint i = 1; i < 10; i++) {
+ if (_channels[i].volume != 0xff)
+ controlChange(i, 0x07, _channels[i].volume & 0x7f);
+ }
+}
+
+int MidiPlayer_Midi::getVolume() {
+ return _masterVolume;
+}
+
+void MidiPlayer_Midi::setReverb(int8 reverb) {
+ assert(reverb < kReverbConfigNr);
+
+ if (_hasReverb && _reverb != reverb) {
+ sendMt32SysEx(0x100001, SciSpan<const byte>(_reverbConfig[reverb], 3), true, true);
+ }
+
+ _reverb = reverb;
+}
+
+void MidiPlayer_Midi::playSwitch(bool play) {
+ _playSwitch = play;
+ if (play)
+ setVolume(_masterVolume);
+ else {
+ for (uint i = 1; i < 10; i++)
+ _driver->send(0xb0 | i, 7, 0);
+ }
+}
+
+void MidiPlayer_Midi::initTrack(SciSpan<const byte> &header) {
+ return;
+}
+
+bool MidiPlayer_Midi::isMt32GmPatch(const SciSpan<const byte> &data) {
+ uint32 size = data.size();
+
+ // WORKAROUND: Some Mac games (e.g. LSL5) may have an extra byte at the
+ // end, so compensate for that here - bug #6725.
+ if (size == 16890)
+ size--;
+
+ // Need at least 1153 + 2 bytes for a GM patch. Check readMt32GmPatch()
+ // below for more info.
+ if (size < 1153 + 2)
+ return false;
+ // The maximum number of bytes for an MT-32 patch is 16889. The maximum
+ // number of timbres is 64, which leads us to:
+ // 491 + 1 + 64 * 246 + 653 = 16889
+ if (size > 16889)
+ return true;
+
+ bool isMt32 = false;
+ bool isMt32Gm = false;
+
+ // First, check for a GM patch. The presence of MIDI data after the
+ // initial 1153 + 2 bytes indicates a GM patch
+ if (data.getUint16LEAt(1153) + 1155U == size)
+ isMt32Gm = true;
+
+ // Now check for a regular MT-32 patch. Check readMt32Patch() below for
+ // more info.
+ // 491 = 20 + 20 + 20 + 2 + 1 + 11 + 3 * 11 + 256 + 128
+ byte timbresNr = data[491];
+ uint pos = 492 + 246 * timbresNr;
+
+ // Patches 49-96
+ if (size >= pos + 386 && data.getUint16BEAt(pos) == 0xabcd)
+ pos += 386; // 256 + 128 + 2
+
+ // Rhythm key map + partial reserve
+ if (size >= pos + 267 && data.getUint16BEAt(pos) == 0xdcba)
+ pos += 267; // 256 + 9 + 2
+
+ if (size == pos)
+ isMt32 = true;
+
+ if (isMt32 == isMt32Gm)
+ error("Failed to detect MT-32 patch format");
+
+ return isMt32Gm;
+}
+
+void MidiPlayer_Midi::sendMt32SysEx(const uint32 addr, Common::SeekableReadStream &stream, int len, bool noDelay = false, bool mainThread = true) {
+ if (len + 8 > kMaxSysExSize) {
+ warning("SysEx message exceed maximum size; ignoring");
+ return;
+ }
+
+ uint16 chk = 0;
+
+ _sysExBuf[4] = (addr >> 16) & 0xff;
+ _sysExBuf[5] = (addr >> 8) & 0xff;
+ _sysExBuf[6] = addr & 0xff;
+
+ stream.read(_sysExBuf + 7, len);
+
+ for (int i = 4; i < 7 + len; i++)
+ chk -= _sysExBuf[i];
+
+ _sysExBuf[7 + len] = chk & 0x7f;
+
+ uint16 delay = sysExNoDelay(_sysExBuf, len + 8);
+ if (!noDelay && delay > 0) {
+ // Use the appropriate delay technique based on the current thread.
+ // On the main thread, use SciEngine::sleep() to keep the UI responsive,
+ // which is important because loading patches can take several seconds.
+ // On a timer thread however, SciEngine::sleep() can't be used because
+ // it polls events and updates the screen, which isn't thread safe. (bug #12947)
+ if (mainThread) {
+ g_system->delayMillis(delay);
+ } else {
+ g_system->delayMillis(delay);
+ }
+ }
+}
+
+void MidiPlayer_Midi::sendMt32SysEx(const uint32 addr, const SciSpan<const byte> &buf, bool noDelay = false, bool mainThread = true) {
+ Common::MemoryReadStream stream(buf.toStream());
+ sendMt32SysEx(addr, stream, buf.size(), noDelay, mainThread);
+}
+
+
+void MidiPlayer_Midi::readMt32Patch(const SciSpan<const byte> &data) {
+ // MT-32 patch contents:
+ // - 0-19 after-SysEx message (KQ4/LSL2: before)
+ // - 20-39 before-SysEx message (KQ4/LSL2: after)
+ // - 40-59 goodbye SysEx message
+ // - 60-61 volume
+ // - 62 reverb
+ // - 63-73 reverb Sysex message
+ // - 74-106 [3 * 11] reverb data
+ // - 107-490 [256 + 128] patches 1-48
+ // --> total: 491 bytes
+ // - 491 number of timbres (64 max)
+ // - 492..n [246 * number of timbres] timbre data
+ // - n-n+1 flag (0xabcd)
+ // - n+2-n+385 [256 + 128] patches 49-96
+ // - n+386-n+387 flag (0xdcba)
+ // - n+388-n+643 rhythm key map
+ // - n+644-n+652 partial reserve
+
+ Common::MemoryReadStream stream(data.toStream());
+
+ // before-SysEx and after-SysEx texts swapped positions after KQ4 and LSL2.
+ uint beforeTextPos = _mt32LCDSize;
+ uint afterTextPos = 0;
+
+ // Send before-SysEx text
+ stream.seek(beforeTextPos);
+ sendMt32SysEx(0x200000, stream, _mt32LCDSize);
+
+ // Save goodbye message
+ assert(sizeof(_goodbyeMsg) >= _mt32LCDSize);
+ stream.seek(_mt32LCDSize * 2);
+ stream.read(_goodbyeMsg, _mt32LCDSize);
+
+ const uint8 volume = MIN<uint16>(stream.readUint16LE(), 100);
+ setMt32Volume(volume);
+
+ // Reverb default only used in (roughly) SCI0/SCI01
+ _defaultReverb = stream.readSByte();
+
+ _hasReverb = true;
+
+ // Skip reverb SysEx message
+ stream.seek(11, SEEK_CUR);
+
+ // Read reverb data (stored vertically - trac #9261)
+ for (int j = 0; j < 3; ++j) {
+ for (int i = 0; i < kReverbConfigNr; i++) {
+ _reverbConfig[i][j] = stream.readByte();
+ }
+ }
+
+ // Patches 1-48
+ sendMt32SysEx(0x50000, stream, 256);
+ sendMt32SysEx(0x50200, stream, 128);
+
+ // Timbres
+ const uint8 timbresNr = stream.readByte();
+ for (int i = 0; i < timbresNr; i++)
+ sendMt32SysEx(0x80000 + (i << 9), stream, 246);
+
+ uint16 flag = stream.readUint16BE();
+
+ if (!stream.eos() && flag == 0xabcd) {
+ // Patches 49-96
+ sendMt32SysEx(0x50300, stream, 256);
+ sendMt32SysEx(0x50500, stream, 128);
+ flag = stream.readUint16BE();
+ }
+
+ if (!stream.eos() && flag == 0xdcba) {
+ // Rhythm key map
+ sendMt32SysEx(0x30110, stream, 256);
+ // Partial reserve
+ sendMt32SysEx(0x100004, stream, 9);
+ }
+
+ // Send after-SysEx text
+ stream.seek(afterTextPos);
+ sendMt32SysEx(0x200000, stream, _mt32LCDSize);
+
+ // Send the mystery SysEx
+ Common::MemoryReadStream mystery((const byte *)"\x16\x16\x16\x16\x16\x16", 6);
+ sendMt32SysEx(0x52000a, mystery, 6);
+}
+
+void MidiPlayer_Midi::readMt32GmPatch(const SciSpan<const byte> &data) {
+ // GM patch contents:
+ // - 128 bytes patch map
+ // - 128 bytes key shift
+ // - 128 bytes volume adjustment
+ // - 128 bytes percussion map
+ // - 1 byte volume adjust for the rhythm channel
+ // - 128 bytes velocity map IDs
+ // - 512 bytes velocity map
+ // --> total: 1153 bytes
+
+ data.subspan(0, sizeof(_patchMap)).unsafeCopyDataTo(_patchMap);
+ data.subspan(128, sizeof(_keyShift)).unsafeCopyDataTo(_keyShift);
+ data.subspan(256, sizeof(_volAdjust)).unsafeCopyDataTo(_volAdjust);
+ data.subspan(384, sizeof(_percussionMap)).unsafeCopyDataTo(_percussionMap);
+ _channels[MIDI_RHYTHM_CHANNEL].volAdjust = data[512];
+ data.subspan(513, sizeof(_velocityMapIdx)).unsafeCopyDataTo(_velocityMapIdx);
+ data.subspan(641, sizeof(_velocityMap)).unsafeCopyDataTo(_velocityMap);
+
+ uint16 midiSize = data.getUint16LEAt(1153);
+
+ if (midiSize > 0) {
+ if (data.size() < midiSize + 1155U)
+ error("Failed to read MIDI data");
+
+ const SciSpan<const byte> midi = data.subspan(1155, midiSize);
+ byte command = 0;
+ uint i = 0;
+
+ while (i < midiSize) {
+ byte op1, op2;
+
+ if (midi[i] & 0x80)
+ command = midi[i++];
+
+ switch (command & 0xf0) {
+ case 0xf0: {
+ const byte *sysExStart = midi.getUnsafeDataAt(i, midiSize - i);
+ const byte *sysExEnd = (const byte *)memchr(sysExStart, 0xf7, midiSize - i);
+
+ if (!sysExEnd)
+ error("Failed to find end of sysEx");
+
+ int len = sysExEnd - sysExStart;
+ sysEx(sysExStart, len);
+
+ i += len + 1; // One more for the 0xf7
+ break;
+ }
+ case 0x80:
+ case 0x90:
+ case 0xa0:
+ case 0xb0:
+ case 0xe0:
+ if (i + 1 >= midiSize)
+ error("MIDI command exceeds data size");
+
+ op1 = midi[i++];
+ op2 = midi[i++];
+ _driver->send(command, op1, op2);
+ break;
+ case 0xc0:
+ case 0xd0:
+ if (i >= midiSize)
+ error("MIDI command exceeds data size");
+
+ op1 = midi[i++];
+ _driver->send(command, op1, 0);
+ break;
+ default:
+ error("Failed to find MIDI command byte");
+ }
+ }
+ }
+}
+
+void MidiPlayer_Midi::readMt32DrvData() {
+ Common::File f;
+
+ if (f.open("MT32.DRV")) {
+ int size = f.size();
+
+ // Skip before-SysEx text
+ if (size == 1773 || size == 1759 || size == 1747) // XMAS88 / KQ4 early (0.000.253 / 0.000.274)
+ f.seek(0x59);
+ else if (size == 2771) // LSL2 early
+ f.seek(0x29);
+ else
+ error("Unknown MT32.DRV size (%d)", size);
+
+ // Skip 2 extra 0 bytes in some drivers
+ if (f.readUint16LE() != 0)
+ f.seek(-2, SEEK_CUR);
+
+ // Send before-SysEx text
+ sendMt32SysEx(0x200000, f, 20);
+
+ if (size != 2771) {
+ // Send after-SysEx text (SSCI sends this before every song).
+ // There aren't any SysEx calls in old drivers, so this can
+ // be sent right after the before-SysEx text.
+ sendMt32SysEx(0x200000, f, 20);
+ } else { // LSL2 early
+ // Skip the after-SysEx text in the newer patch version, we'll send
+ // it after the SysEx messages are sent.
+ f.skip(20);
+ }
+
+ // Save goodbye message. This isn't a C string, so it may not be
+ // nul-terminated.
+ f.read(_goodbyeMsg, 20);
+
+ // Set volume
+ byte volume = CLIP<uint16>(f.readUint16LE(), 0, 100);
+ setMt32Volume(volume);
+
+ if (size == 2771) {
+ // MT32.DRV in LSL2 early contains more data, like a normal patch
+ _defaultReverb = f.readByte();
+
+ _hasReverb = true;
+
+ // Skip reverb SysEx message
+ f.skip(11);
+
+ // Read reverb data (stored vertically - trac #9261)
+ for (int j = 0; j < 3; ++j) {
+ for (int i = 0; i < kReverbConfigNr; i++) {
+ _reverbConfig[i][j] = f.readByte();
+ }
+ }
+
+ f.skip(2235); // skip driver code
+
+ // Patches 1-48
+ sendMt32SysEx(0x50000, f, 256);
+ sendMt32SysEx(0x50200, f, 128);
+
+ // Send the after-SysEx text
+ f.seek(0x3d);
+ sendMt32SysEx(0x200000, f, 20);
+ } else {
+ byte reverbSysEx[13];
+ // This old driver should have a full reverb SysEx
+ if ((f.read(reverbSysEx, 13) != 13) || (reverbSysEx[0] != 0xf0) || (reverbSysEx[12] != 0xf7))
+ error("Error reading MT32.DRV");
+
+ // Send reverb SysEx
+ sysEx(reverbSysEx + 1, 11);
+ _hasReverb = false;
+
+ f.seek(0x29);
+
+ // Read AdLib->MT-32 patch map
+ for (int i = 0; i < 48; i++) {
+ _patchMap[i] = f.readByte();
+ }
+ }
+
+ f.close();
+ } else {
+ error("Failed to open MT32.DRV");
+ }
+}
+
+
+byte MidiPlayer_Midi::lookupGmInstrument(const char *iname) {
+ int i = 0;
+
+ if (Mt32dynamicMappings != nullptr) {
+ const Mt32ToGmMapList::iterator end = Mt32dynamicMappings->end();
+ for (Mt32ToGmMapList::iterator it = Mt32dynamicMappings->begin(); it != end; ++it) {
+ if (scumm_strnicmp(iname, (*it).name, 10) == 0)
+ return getGmInstrument((*it));
+ }
+ }
+
+ while (Mt32MemoryTimbreMaps[i].name) {
+ if (scumm_strnicmp(iname, Mt32MemoryTimbreMaps[i].name, 10) == 0)
+ return getGmInstrument(Mt32MemoryTimbreMaps[i]);
+ i++;
+ }
+
+ return MIDI_UNMAPPED;
+}
+
+byte MidiPlayer_Midi::lookupGmRhythmKey(const char *iname) {
+ int i = 0;
+
+ if (Mt32dynamicMappings != nullptr) {
+ const Mt32ToGmMapList::iterator end = Mt32dynamicMappings->end();
+ for (Mt32ToGmMapList::iterator it = Mt32dynamicMappings->begin(); it != end; ++it) {
+ if (scumm_strnicmp(iname, (*it).name, 10) == 0)
+ return (*it).gmRhythmKey;
+ }
+ }
+
+ while (Mt32MemoryTimbreMaps[i].name) {
+ if (scumm_strnicmp(iname, Mt32MemoryTimbreMaps[i].name, 10) == 0)
+ return Mt32MemoryTimbreMaps[i].gmRhythmKey;
+ i++;
+ }
+
+ return MIDI_UNMAPPED;
+}
+
+uint8 MidiPlayer_Midi::getGmInstrument(const Mt32ToGmMap &Mt32Ins) {
+ if (Mt32Ins.gmInstr == MIDI_MAPPED_TO_RHYTHM)
+ return Mt32Ins.gmRhythmKey + 0x80;
+ else
+ return Mt32Ins.gmInstr;
+}
+
+void MidiPlayer_Midi::mapMt32ToGm(const SciSpan<const byte> &data) {
+ // FIXME: Clean this up
+ int memtimbres, patches;
+ uint8 group, number, keyshift, /*finetune,*/ bender_range;
+ SciSpan<const byte> patchpointer;
+ uint32 pos;
+ int i;
+
+ for (i = 0; i < 128; i++) {
+ _patchMap[i] = getGmInstrument(Mt32PresetTimbreMaps[i]);
+ _pitchBendRange[i] = 12;
+ }
+
+ for (i = 0; i < 128; i++)
+ _percussionMap[i] = Mt32PresetRhythmKeymap[i];
+
+ memtimbres = data[0x1eb];
+ pos = 0x1ec + memtimbres * 0xf6;
+
+ if (data.size() > pos && data.getUint16BEAt(pos) == 0xabcd) {
+ patches = 96;
+ pos += 2 + 8 * 48;
+ } else {
+ patches = 48;
+ }
+
+ debugC(kDebugLevelSound, "[MT32-to-GM] %d MT-32 Patches detected", patches);
+ debugC(kDebugLevelSound, "[MT32-to-GM] %d MT-32 Memory Timbres", memtimbres);
+
+ debugC(kDebugLevelSound, "\n[MT32-to-GM] Mapping patches..");
+
+ for (i = 0; i < patches; i++) {
+ Common::String name;
+
+ if (i < 48)
+ patchpointer = data.subspan(0x6b + 8 * i);
+ else
+ patchpointer = data.subspan(0x1ec + 8 * (i - 48) + memtimbres * 0xf6 + 2);
+
+ group = patchpointer[0];
+ number = patchpointer[1];
+ keyshift = patchpointer[2];
+ //finetune = patchpointer[3];
+ bender_range = patchpointer[4];
+
+ debugCN(kDebugLevelSound, " [%03d] ", i);
+
+ switch (group) {
+ case 1:
+ number += 64;
+ // Fall through
+ case 0:
+ _patchMap[i] = getGmInstrument(Mt32PresetTimbreMaps[number]);
+ debugCN(kDebugLevelSound, "%s -> ", Mt32PresetTimbreMaps[number].name);
+ break;
+ case 2:
+ if (number < memtimbres) {
+ name = data.getStringAt(0x1ec + number * 0xf6, 10);
+ _patchMap[i] = lookupGmInstrument(name.c_str());
+ debugCN(kDebugLevelSound, "%s -> ", name.c_str());
+ } else {
+ _patchMap[i] = 0xff;
+ debugCN(kDebugLevelSound, "[Invalid] -> ");
+ }
+ break;
+ case 3:
+ _patchMap[i] = getGmInstrument(Mt32RhythmTimbreMaps[number]);
+ debugCN(kDebugLevelSound, "%s -> ", Mt32RhythmTimbreMaps[number].name);
+ break;
+ default:
+ break;
+ }
+
+ if (_patchMap[i] == MIDI_UNMAPPED) {
+ debugC(kDebugLevelSound, "[Unmapped]");
+ } else {
+#ifndef REDUCE_MEMORY_USAGE
+ if (_patchMap[i] >= 128) {
+ debugC(kDebugLevelSound, "%s [Rhythm]", GmPercussionNames[_patchMap[i] - 128]);
+ } else {
+ debugC(kDebugLevelSound, "%s", GmInstrumentNames[_patchMap[i]]);
+ }
+#endif
+ }
+
+ _keyShift[i] = CLIP<uint8>(keyshift, 0, 48) - 24;
+ _pitchBendRange[i] = CLIP<uint8>(bender_range, 0, 24);
+ }
+
+ if (data.size() > pos && data.getUint16BEAt(pos) == 0xdcba) {
+ debugC(kDebugLevelSound, "\n[MT32-to-GM] Mapping percussion..");
+
+ for (i = 0; i < 64; i++) {
+ number = data[pos + 4 * i + 2];
+ byte ins = i + 24;
+
+ debugCN(kDebugLevelSound, " [%03d] ", ins);
+
+ if (number < 64) {
+ Common::String name = data.getStringAt(0x1ec + number * 0xf6, 10);
+ debugCN(kDebugLevelSound, "%s -> ", name.c_str());
+ _percussionMap[ins] = lookupGmRhythmKey(name.c_str());
+ } else {
+ if (number < 94) {
+ debugCN(kDebugLevelSound, "%s -> ", Mt32RhythmTimbreMaps[number - 64].name);
+ _percussionMap[ins] = Mt32RhythmTimbreMaps[number - 64].gmRhythmKey;
+ } else {
+ debugCN(kDebugLevelSound, "[Key %03i] -> ", number);
+ _percussionMap[ins] = MIDI_UNMAPPED;
+ }
+ }
+
+#ifndef REDUCE_MEMORY_USAGE
+ if (_percussionMap[ins] == MIDI_UNMAPPED)
+ debugC(kDebugLevelSound, "[Unmapped]");
+ else
+ debugC(kDebugLevelSound, "%s", GmPercussionNames[_percussionMap[ins]]);
+#endif
+
+ _percussionVelocityScale[ins] = data[pos + 4 * i + 3] * 127 / 100;
+ }
+ }
+}
+
+void MidiPlayer_Midi::setMt32Volume(byte volume) {
+ Common::MemoryReadStream s(&volume, 1);
+ sendMt32SysEx(0x100016, s, 1);
+}
+
+void MidiPlayer_Midi::resetMt32() {
+ Common::MemoryReadStream s((const byte *)"\x01\x00", 2);
+ sendMt32SysEx(0x7f0000, s, 2, true);
+
+ if (_mt32Type != kMt32TypeEmulated) {
+ // This seems to require a longer delay than usual
+ g_system->delayMillis(150); // note that sleep() can only be called from main thread, see bug #12947
+ }
+}
+
+int MidiPlayer_Midi::open(ResourceManager *resMan) {
+ assert(resMan != nullptr);
+
+ int retval = _driver->open();
+ if (retval != 0) {
+ warning("Failed to open MIDI driver");
+ return retval;
+ }
+
+ // By default use no mapping
+ for (uint i = 0; i < 128; i++) {
+ _percussionMap[i] = i;
+ _patchMap[i] = i;
+ _velocityMap[0][i] = i;
+ _velocityMap[1][i] = i;
+ _velocityMap[2][i] = i;
+ _velocityMap[3][i] = i;
+ _keyShift[i] = 0;
+ _volAdjust[i] = 0;
+ _velocityMapIdx[i] = 0;
+ _pitchBendRange[i] = MIDI_UNMAPPED;
+ _percussionVelocityScale[i] = 127;
+ }
+
+ SciResource *res = nullptr;
+
+ if (_mt32Type != kMt32TypeNone) {
+ // MT-32
+ resetMt32();
+
+ res = getMidiPatchData(1);
+
+ if (res) {
+ if (isMt32GmPatch(*res)) {
+ readMt32GmPatch(*res);
+ // Note that _goodbyeMsg is not zero-terminated
+ memcpy(_goodbyeMsg, " ScummVM ", 20);
+ } else {
+ readMt32Patch(*res);
+ }
+ }
+ } else {
+ // General MIDI
+ res = getMidiPatchData(4);
+
+ if (res && isMt32GmPatch(*res)) {
+ // There is a GM patch
+ readMt32GmPatch(*res);
+
+ // Detect the format of patch 1, so that we know what play mask to use
+ res = getMidiPatchData(4);
+ if (!res)
+ _useMT32Track = false;
+ else
+ _useMT32Track = !isMt32GmPatch(*res);
+
+ // Check if the songs themselves have a GM track
+ //if (!_useMT32Track) {
+ //if (!resMan->isGMTrackIncluded())
+ // _useMT32Track = true;
+ //}
+ } else {
+ // No GM patch found, map instruments using MT-32 patch
+
+ warning("Game has no native support for General MIDI, applying auto-mapping");
+
+ // TODO: The MT-32 <-> GM mapping hasn't been worked on for SCI1 games. Throw
+ // a warning to the user
+ //if (getSciVersion() >= SCI_VERSION_1_EGA_ONLY)
+ warning("The automatic mapping for General MIDI hasn't been worked on for "
+ "SCI1 games. Music might sound wrong or broken. Please choose another "
+ "music driver for this game (e.g. AdLib or MT-32) if you are "
+ "experiencing issues with music");
+
+ // Modify velocity map to make low velocity notes a little louder
+ for (uint i = 1; i < 0x40; i++) {
+ _velocityMap[0][i] = 0x20 + (i - 1) / 2;
+ _velocityMap[1][i] = 0x20 + (i - 1) / 2;
+ _velocityMap[2][i] = 0x20 + (i - 1) / 2;
+ _velocityMap[3][i] = 0x20 + (i - 1) / 2;
+ }
+
+ res = getMidiPatchData(1);
+
+ if (res) {
+ if (!isMt32GmPatch(*res)) {
+ mapMt32ToGm(*res);
+ } else {
+ error("MT-32 patch has wrong type");
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+void MidiPlayer_Midi::close() {
+ if (_mt32Type != kMt32TypeNone) {
+ // Send goodbye message
+ sendMt32SysEx(0x200000, SciSpan<const byte>(_goodbyeMsg, _mt32LCDSize), true);
+ }
+
+ _driver->setTimerCallback(nullptr, nullptr);
+ _driver->close();
+}
+
+void MidiPlayer_Midi::sysEx(const byte *msg, uint16 length) {
+ uint16 delay = sysExNoDelay(msg, length);
+
+ if (delay > 0)
+ g_system->delayMillis(delay);
+}
+
+uint16 MidiPlayer_Midi::sysExNoDelay(const byte *msg, uint16 length) {
+ _driver->sysEx(msg, length);
+
+ uint16 delay = 0;
+ if (_mt32Type != kMt32TypeEmulated) {
+ // Wait the time it takes to send the SysEx data
+ delay = (length + 2) * 1000 / 3125;
+
+ // Plus an additional delay for the MT-32 rev00
+ if (_mt32Type == kMt32TypeReal)
+ delay += 40;
+ }
+
+ return delay;
+}
+
+byte MidiPlayer_Midi::getPlayId() const {
+ if (_mt32Type != kMt32TypeNone)
+ return 0x0c;
+ else
+ return _useMT32Track ? 0x0c : 0x07;
+}
+
+MidiPlayer *MidiPlayer_Midi_create() {
+ return new MidiPlayer_Midi();
+}
+
+} // End of namespace Dgds
diff --git a/engines/dgds/sound/drivers/mididriver.h b/engines/dgds/sound/drivers/mididriver.h
new file mode 100644
index 00000000000..4f21a81d023
--- /dev/null
+++ b/engines/dgds/sound/drivers/mididriver.h
@@ -0,0 +1,143 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DGDS_SOUND_DRIVERS_MIDIDRIVER_H
+#define DGDS_SOUND_DRIVERS_MIDIDRIVER_H
+
+#include "dgds/sound/scispan.h"
+#include "dgds/dgds.h"
+#include "audio/mididrv.h"
+#include "common/error.h"
+#include "common/platform.h"
+
+namespace Dgds {
+
+// Music patches in SCI games:
+// ===========================
+// 1.pat - MT-32 driver music patch
+// 2.pat - Yamaha FB01 driver music patch
+// 3.pat - Adlib driver music patch
+// 4.pat - Casio MT-540 (in earlier SCI0 games)
+// 4.pat - GM driver music patch (in later games that support GM)
+// 7.pat (newer) / patch.200 (older) - Mac driver music patch / Casio CSM-1
+// 9.pat (newer) / patch.005 (older) - Amiga driver music patch
+// 98.pat - Unknown, found in later SCI1.1 games. A MIDI format patch
+// 101.pat - CMS/PCjr driver music patch.
+// Only later PCjr drivers use this patch, earlier ones don't use a patch
+// bank.001 - older SCI0 Amiga instruments
+
+class ResourceManager;
+
+enum {
+ MIDI_CHANNELS = 16,
+ MIDI_PROP_MASTER_VOLUME = 0
+};
+
+#define MIDI_RHYTHM_CHANNEL 9
+
+/* Special SCI sound stuff */
+
+#define SCI_MIDI_TIME_EXPANSION_PREFIX 0xF8
+#define SCI_MIDI_TIME_EXPANSION_LENGTH 240
+
+#define SCI_MIDI_EOT 0xFC
+#define SCI_MIDI_SET_SIGNAL 0xCF
+#define SCI_MIDI_SET_POLYPHONY 0x4B
+#define SCI_MIDI_RESET_ON_SUSPEND 0x4C
+#define SCI_MIDI_CHANNEL_MUTE 0x4E
+#define SCI_MIDI_SET_REVERB 0x50
+#define SCI_MIDI_HOLD 0x52
+#define SCI_MIDI_CUMULATIVE_CUE 0x60
+#define SCI_MIDI_CHANNEL_SOUND_OFF 0x78 /* all-sound-off for Bn */
+#define SCI_MIDI_CHANNEL_NOTES_OFF 0x7B /* all-notes-off for Bn */
+
+#define SCI_MIDI_SET_SIGNAL_LOOP 0x7F
+/* If this is the parameter of 0xCF, the loop point is set here */
+
+#define SCI_MIDI_CONTROLLER(status) ((status & 0xF0) == 0xB0)
+
+class MidiPlayer : public MidiDriver_BASE {
+protected:
+ MidiDriver *_driver;
+ int8 _reverb;
+
+public:
+ MidiPlayer() : _driver(0), _reverb(-1) {}
+
+ int open() {
+ ResourceManager *resMan = DgdsEngine::getInstance()->getResourceManager(); // HACK
+ return open(resMan);
+ }
+ virtual int open(ResourceManager *resMan) { return _driver->open(); }
+ virtual void close() { _driver->close(); }
+ void send(uint32 b) override { _driver->send(b); }
+ virtual uint32 getBaseTempo() { return _driver->getBaseTempo(); }
+ virtual bool hasRhythmChannel() const = 0;
+ virtual void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) { _driver->setTimerCallback(timer_param, timer_proc); }
+
+ virtual byte getPlayId() const = 0;
+ virtual int getPolyphony() const = 0;
+ virtual int getFirstChannel() const { return 0; }
+ virtual int getLastChannel() const { return 15; }
+
+ virtual void setVolume(byte volume) {
+ if(_driver)
+ _driver->property(MIDI_PROP_MASTER_VOLUME, volume);
+ }
+
+ virtual int getVolume() {
+ return _driver ? _driver->property(MIDI_PROP_MASTER_VOLUME, 0xffff) : 0;
+ }
+
+ // Returns the current reverb, or -1 when no reverb is active
+ int8 getReverb() const { return _reverb; }
+ // Sets the current reverb, used mainly in MT-32
+ virtual void setReverb(int8 reverb) { _reverb = reverb; }
+
+ virtual void playSwitch(bool play) {
+ if (!play) {
+ // Send "All Sound Off" on all channels
+ for (int i = 0; i < MIDI_CHANNELS; ++i)
+ _driver->send(0xb0 + i, SCI_MIDI_CHANNEL_NOTES_OFF, 0);
+ }
+ }
+
+ // Prepares the driver for the playback of SCI0 midi tracks.
+ // The main purpose is the assignment of voices ("hardware" sound channels) to the 16 midi parts.
+ // This is basically the predecessor of the 0x4B midi event.
+ // Some drivers also do other things in here.
+ virtual void initTrack(SciSpan<const byte> &) {}
+
+};
+
+class SciResource;
+
+extern SciResource *getMidiPatchData(int num);
+
+extern MidiPlayer *MidiPlayer_AdLib_create();
+extern MidiPlayer *MidiPlayer_AmigaMac1_create(Common::Platform platform);
+extern MidiPlayer *MidiPlayer_PCJr_create();
+extern MidiPlayer *MidiPlayer_PCSpeaker_create();
+extern MidiPlayer *MidiPlayer_Midi_create();
+
+} // End of namespace Dgds
+
+#endif // DGDS_SOUND_DRIVERS_MIDIDRIVER_H
diff --git a/engines/dgds/sound/drivers/midipatch.cpp b/engines/dgds/sound/drivers/midipatch.cpp
new file mode 100644
index 00000000000..d88693db219
--- /dev/null
+++ b/engines/dgds/sound/drivers/midipatch.cpp
@@ -0,0 +1,103 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+
+#include "common/config-manager.h"
+#include "common/file.h"
+#include "common/memstream.h"
+#include "common/system.h"
+
+#include "dgds/sound/resource/sci_resource.h"
+#include "dgds/dgds.h"
+#include "dgds/includes.h"
+
+namespace Dgds {
+
+//
+// Unlike the other files in this directory, this is not part of the SCI
+// engine. This is a single function to load patch data from the DGDS
+// resource system.
+//
+
+static const char *PATCH_RESOURCES[] = {
+ "SXTITLE.OVL", // dragon
+ "SXCODE1.OVL", // china (TODO: when do we load SXCODE2??)
+ "SX.OVL", // newer games (beamish, sq5 demo)
+};
+
+SciResource *getMidiPatchData(int num) {
+ assert(num < 999);
+
+ DgdsEngine *engine = DgdsEngine::getInstance();
+ ResourceManager *resource = engine->getResourceManager();
+ Decompressor *decomp = engine->getDecompressor();
+
+ Common::SeekableReadStream *ovlStream;
+
+ int resNum = 0;
+ for (; resNum < ARRAYSIZE(PATCH_RESOURCES); resNum++) {
+ ovlStream = resource->getResource(PATCH_RESOURCES[resNum]);
+ if (ovlStream)
+ break;
+ }
+
+ if (!ovlStream) {
+ warning("Couldn't load DGDS midi patch data from any known OVL file.");
+ return nullptr;
+ }
+
+ DgdsChunkReader chunk(ovlStream);
+
+ const Common::String targetSection = Common::String::format("%03d:", num);
+
+ while (chunk.readNextHeader(EX_OVL, PATCH_RESOURCES[resNum])) {
+ if (chunk.isContainer()) {
+ continue;
+ }
+
+ if (chunk.isSection(targetSection)) {
+ chunk.readContent(decomp);
+ Common::SeekableReadStream *stream = chunk.getContent();
+
+ byte magic = stream->readSByte(); // always 137?
+ byte strLen = stream->readSByte(); // header string len
+ char *buf = new char[strLen + 1];
+ stream->read(buf, strLen);
+ buf[strLen] = '\0';
+
+ int dataLen = stream->size() - (strLen + 2);
+ byte *data = new byte[dataLen];
+ debug("midi patch %s loading magic %d str '%s'", targetSection.c_str(), magic, buf);
+ delete [] buf;
+ stream->read(data, dataLen);
+ return new SciResource(data, dataLen, num);
+ } else {
+ chunk.skipContent();
+ }
+ }
+
+ warning("Didn't find section %s in midi patch resource %s", targetSection.c_str(), PATCH_RESOURCES[resNum]);
+
+ return nullptr;
+}
+
+
+} // end namespace Dgds
diff --git a/engines/dgds/sound/midiparser_sci.cpp b/engines/dgds/sound/midiparser_sci.cpp
new file mode 100644
index 00000000000..c188d326645
--- /dev/null
+++ b/engines/dgds/sound/midiparser_sci.cpp
@@ -0,0 +1,816 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "dgds/sound/midiparser_sci.h"
+#include "dgds/sound/drivers/mididriver.h"
+
+namespace Dgds {
+
+static const int nMidiParams[] = { 2, 2, 2, 2, 1, 1, 2, 0 };
+
+enum SciMidiCommands {
+ kSetSignalLoop = 0x7F,
+ kEndOfTrack = 0xFC,
+ kSetReverb = 0x50,
+ kMidiHold = 0x52,
+ kUpdateCue = 0x60,
+ kResetOnPause = 0x4C
+};
+
+// MidiParser_SCI
+//
+MidiParser_SCI::MidiParser_SCI(SciMusic *music) :
+ MidiParser() {
+ _music = music;
+ // mididata contains delta in 1/60th second
+ // values of ppqn and tempo are found experimentally and may be wrong
+ _ppqn = 1;
+ setTempo(16667);
+
+ _masterVolume = 15;
+ _volume = 127;
+
+ _resetOnPause = false;
+ _pSnd = nullptr;
+
+ _mainThreadCalled = false;
+
+ resetStateTracking();
+}
+
+MidiParser_SCI::~MidiParser_SCI() {
+ unloadMusic();
+ // we do this, so that MidiParser won't be able to call his own ::allNotesOff()
+ // this one would affect all channels and we can't let that happen
+ _driver = nullptr;
+}
+
+void MidiParser_SCI::mainThreadBegin() {
+ assert(!_mainThreadCalled);
+ _mainThreadCalled = true;
+}
+
+void MidiParser_SCI::mainThreadEnd() {
+ assert(_mainThreadCalled);
+ _mainThreadCalled = false;
+}
+
+bool MidiParser_SCI::loadMusic(SoundResource::Track *track, MusicEntry *psnd, int channelFilterMask) {
+ unloadMusic();
+ _track = track;
+ _pSnd = psnd;
+
+ for (int i = 0; i < 16; i++) {
+ _channelUsed[i] = false;
+ _channelVolume[i] = 127;
+ _channelRemap[i] = -1;
+ }
+
+ midiMixChannels();
+
+ _numTracks = 1;
+ _tracks[0] = const_cast<byte *>(_mixedData->data());
+ if (_pSnd)
+ setTrack(0);
+ _loopTick = 0;
+
+ return true;
+}
+
+byte MidiParser_SCI::midiGetNextChannel(long ticker) {
+ byte curr = 0xFF;
+ long closest = ticker + 1000000, next = 0;
+
+ for (int i = 0; i < _track->channelCount; i++) {
+ if (_track->channels[i].time == -1) // channel ended
+ continue;
+ SoundResource::Channel *curChannel = &_track->channels[i];
+ if (curChannel->curPos >= curChannel->data.size())
+ continue;
+ next = curChannel->data[curChannel->curPos]; // when the next event should occur
+ if (next == 0xF8) // 0xF8 means 240 ticks delay
+ next = 240;
+ next += _track->channels[i].time;
+ if (next < closest) {
+ curr = i;
+ closest = next;
+ }
+ }
+
+ return curr;
+}
+
+static inline bool validateNextRead(const SoundResource::Channel *channel) {
+ if (channel->data.size() <= channel->curPos) {
+ warning("Unexpected end of %s. Music may sound wrong due to game resource corruption", channel->data.name().c_str());
+ return false;
+ }
+ return true;
+}
+
+void MidiParser_SCI::midiMixChannels() {
+ int totalSize = 0;
+
+ for (int i = 0; i < _track->channelCount; i++) {
+ _track->channels[i].time = 0;
+ _track->channels[i].prev = 0;
+ _track->channels[i].curPos = 0;
+ // Ignore the digital channel data, if it exists - it's not MIDI data
+ if (i == _track->digitalChannelNr)
+ continue;
+ totalSize += _track->channels[i].data.size();
+ }
+
+ SciSpan<byte> outData = _mixedData->allocate(totalSize * 2, Common::String::format("mixed sound.%d", _pSnd ? _pSnd->resourceId : -1)); // FIXME: creates overhead and still may be not enough to hold all data
+
+ long ticker = 0;
+ byte channelNr, curDelta;
+ byte midiCommand = 0, midiParam, globalPrev = 0;
+ long newDelta;
+ SoundResource::Channel *channel;
+ bool breakOut = false;
+
+ while ((channelNr = midiGetNextChannel(ticker)) != 0xFF) { // there is still an active channel
+ channel = &_track->channels[channelNr];
+ if (!validateNextRead(channel))
+ break;
+ curDelta = channel->data[channel->curPos++];
+ channel->time += (curDelta == 0xF8 ? 240 : curDelta); // when the command is supposed to occur
+ if (curDelta == 0xF8)
+ continue;
+ newDelta = channel->time - ticker;
+ ticker += newDelta;
+
+ if (channelNr == _track->digitalChannelNr)
+ continue;
+ if (!validateNextRead(channel))
+ break;
+ midiCommand = channel->data[channel->curPos++];
+ if (midiCommand != kEndOfTrack) {
+ // Write delta
+ while (newDelta > 240) {
+ *outData++ = 0xF8;
+ newDelta -= 240;
+ }
+ *outData++ = (byte)newDelta;
+ }
+
+ // Write command
+ switch (midiCommand) {
+ case 0xF0: // sysEx
+ *outData++ = midiCommand;
+ do {
+ if (!validateNextRead(channel)) {
+ breakOut = true;
+ break;
+ }
+ midiParam = channel->data[channel->curPos++];
+ *outData++ = midiParam;
+ } while (midiParam != 0xF7);
+ break;
+ case kEndOfTrack: // end of channel
+ channel->time = -1;
+ break;
+ default: // MIDI command
+ if (midiCommand & 0x80) {
+ if (!validateNextRead(channel)) {
+ breakOut = true;
+ break;
+ }
+ midiParam = channel->data[channel->curPos++];
+ } else {// running status
+ midiParam = midiCommand;
+ midiCommand = channel->prev;
+ }
+
+ // remember which channel got used for channel remapping
+ byte midiChannel = midiCommand & 0xF;
+ _channelUsed[midiChannel] = true;
+
+ if (midiCommand != globalPrev)
+ *outData++ = midiCommand;
+ *outData++ = midiParam;
+ if (nMidiParams[(midiCommand >> 4) - 8] == 2) {
+ if (!validateNextRead(channel)) {
+ breakOut = true;
+ break;
+ }
+ *outData++ = channel->data[channel->curPos++];
+ }
+ channel->prev = midiCommand;
+ globalPrev = midiCommand;
+ }
+
+ if (breakOut)
+ break;
+ }
+
+ // Insert stop event
+ *outData++ = 0; // Delta
+ *outData++ = 0xFF; // Meta event
+ *outData++ = 0x2F; // End of track (EOT)
+ *outData++ = 0x00;
+ *outData++ = 0x00;
+}
+
+static inline bool validateNextRead(const SciSpan<const byte> &channelData, const SciSpan<const byte>::size_type size = 1) {
+ if (channelData.size() < size) {
+ warning("Unexpected end of %s. Music may sound wrong due to game resource corruption", channelData.name().c_str());
+ return false;
+ }
+ return true;
+}
+
+
+void MidiParser_SCI::resetStateTracking() {
+ for (int i = 0; i < 16; ++i) {
+ ChannelState &s = _channelState[i];
+ s._modWheel = 0;
+ s._pan = 64;
+ s._patch = 0; // TODO: Initialize properly (from data in LoadMusic?)
+ s._note = -1;
+ s._sustain = false;
+ s._pitchWheel = 0x2000;
+ s._voices = 0;
+
+ _channelVolume[i] = 127;
+ }
+}
+
+void MidiParser_SCI::initTrack() {
+ return;
+}
+
+void MidiParser_SCI::sendInitCommands() {
+ resetStateTracking();
+
+ // reset our "global" volume
+ _volume = 127;
+
+ // Set initial voice count
+ if (_pSnd) {
+ for (int i = 0; i < _track->channelCount; ++i) {
+ byte voiceCount = _track->channels[i].poly;
+ byte num = _track->channels[i].number;
+ // TODO: Should we skip the control channel?
+ sendToDriver(0xB0 | num, 0x4B, voiceCount);
+ }
+ }
+
+ // Reset all the parameters of the channels used by this song
+ for (int i = 0; i < 16; ++i) {
+ if (_channelUsed[i]) {
+ sendToDriver(0xB0 | i, 0x07, 127); // Reset volume to maximum
+ sendToDriver(0xB0 | i, 0x0A, 64); // Reset panning to center
+ sendToDriver(0xB0 | i, 0x40, 0); // Reset hold pedal to none
+ sendToDriver(0xE0 | i, 0, 64); // Reset pitch wheel to center
+ }
+ }
+}
+
+void MidiParser_SCI::unloadMusic() {
+ if (_pSnd) {
+ resetTracking();
+ allNotesOff();
+ // Pending track init commands have to be removed from the queue,
+ // since the sound thread will otherwise continue to try executing these.
+ _music->removeTrackInitCommandsFromQueue(_pSnd);
+ }
+ _numTracks = 0;
+ _pSnd = nullptr;
+ _track = nullptr;
+ _activeTrack = 255;
+ _resetOnPause = false;
+ _mixedData.clear();
+}
+
+// this is used for scripts sending midi commands to us. we verify in that case that the channel is actually
+// used, so that channel remapping will work as well and then send them on
+void MidiParser_SCI::sendFromScriptToDriver(uint32 midi) {
+ byte midiChannel = midi & 0xf;
+
+ if (!_channelUsed[midiChannel]) {
+ // trying to send to an unused channel
+ // this happens for cmdSendMidi at least in sq1vga right at the start, it's a script issue
+ return;
+ }
+
+ if ((midi & 0xFFF0) == 0x4EB0) {
+ // We have to handle this here instead of inside the trackState() method (which handles the input from
+ // the actual midi data). The mute command when sent from the script is independent from the mute
+ // command sent by the actual midi data. The script mute is stacked on the high nibble, while the midi
+ // data mute is stored on the low nibble. So the script cannot undo a mute set by the midi data and vice
+ // versa.
+ byte channel = midi & 0xf;
+ bool op = (midi >> 16) & 0x7f;
+ uint8 m = _pSnd->_chan[channel]._mute;
+
+ if (op && _pSnd->_chan[channel]._mute < 0xF0)
+ _pSnd->_chan[channel]._mute += 0x10;
+ else if (!op && _pSnd->_chan[channel]._mute >= 0x10)
+ _pSnd->_chan[channel]._mute -= 0x10;
+
+ if (_pSnd->_chan[channel]._mute != m) {
+ // CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
+ _music->needsRemap();
+ debugC(2, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
+ }
+
+ return;
+ }
+
+ sendToDriver(midi);
+}
+
+void MidiParser_SCI::sendToDriver(uint32 midi) {
+ byte midiChannel = midi & 0xf;
+
+ // State tracking
+ if (!_pSnd->_chan[midiChannel]._dontMap)
+ trackState(midi);
+
+ if ((midi & 0xFFF0) == 0x4EB0) {
+ // Mute. Handled in trackState()/sendFromScriptToDriver().
+ return;
+ }
+
+ if ((midi & 0xFFF0) == 0x07B0) {
+ // someone trying to set channel volume?
+ int channelVolume = (midi >> 16) & 0xFF;
+ // Adjust volume accordingly to current local volume
+ channelVolume = channelVolume * _volume / 127;
+ midi = (midi & 0xFFFF) | ((channelVolume & 0xFF) << 16);
+ }
+
+ // Channel remapping
+ uint8 msg = (midi & 0xF0);
+ int16 realChannel = _channelRemap[midiChannel];
+ if (_pSnd->_chan[midiChannel]._dontMap) {
+ // The dontMap channel is supposed to have limited access, if the device channel is already in use.
+ // It probably won't happen, but the original does these checks...
+ if (!_music->isDeviceChannelMapped(midiChannel) || (msg != 0xB0 && msg != 0xC0 && msg != 0xE0))
+ realChannel = midiChannel;
+ }
+
+ if (realChannel == -1)
+ return;
+
+ midi = (midi & 0xFFFFFFF0) | realChannel;
+ sendToDriver_raw(midi);
+}
+
+void MidiParser_SCI::sendToDriver_raw(uint32 midi) {
+ if (_mainThreadCalled)
+ _music->putMidiCommandInQueue(midi);
+ else
+ _driver->send(midi);
+}
+
+void MidiParser_SCI::trackState(uint32 b) {
+ // We keep track of most of the state of a midi channel, so we can
+ // at any time reset the device to the current state, even if the
+ // channel has been temporarily disabled due to remapping.
+
+ byte command = b & 0xf0;
+ byte channel = b & 0xf;
+ byte op1 = (b >> 8) & 0x7f;
+ byte op2 = (b >> 16) & 0x7f;
+
+ ChannelState &s = _channelState[channel];
+
+ switch (command) {
+ case 0x90:
+ if (op2 != 0) {
+ // note on
+ s._note = op1;
+ break;
+ }
+ // else, fall-through
+ case 0x80:
+ // note off
+ if (s._note == op1)
+ s._note = -1;
+ break;
+ case 0xB0:
+ // control change
+ switch (op1) {
+ case 0x01: // mod wheel
+ s._modWheel = op2;
+ break;
+ case 0x07: // channel volume
+ _channelVolume[channel] = op2;
+ break;
+ case 0x0A: // pan
+ s._pan = op2;
+ break;
+ case 0x40: // sustain
+ s._sustain = (op2 != 0);
+ break;
+ case 0x4B: // voices
+ if (s._voices != op2) {
+ // CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
+ debugC(2, "Dynamic voice change (%d to %d)", s._voices, op2);
+ _music->needsRemap();
+ }
+ s._voices = op2;
+ _pSnd->_chan[channel]._voices = op2; // Also sync our MusicEntry
+ break;
+ case 0x4E: {// mute
+ // This is channel mute only for sci1.
+
+ // This is handled slightly differently than what we do in sendFromScriptToDriver(). The script mute is stacked
+ // on the high nibble, while the midi data mute (this one here) is stored on the low nibble. So the script cannot
+ // undo a mute set by the midi data and vice versa.
+ uint8 m = (_pSnd->_chan[channel]._mute & 0xf0) | (op2 & 1);
+ if (_pSnd->_chan[channel]._mute != m) {
+ _pSnd->_chan[channel]._mute = m;
+ // CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
+ _music->needsRemap();
+ debugC(2, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ break;
+ case 0xC0:
+ // program change
+ s._patch = op1;
+ break;
+ case 0xE0:
+ // pitchwheel
+ s._pitchWheel = (op2 << 7) | op1;
+ break;
+ default:
+ break;
+ }
+}
+
+void MidiParser_SCI::parseNextEvent(EventInfo &info) {
+ info.start = _position._playPos;
+ info.delta = 0;
+ while (*_position._playPos == 0xF8) {
+ info.delta += 240;
+ _position._playPos++;
+ }
+ info.delta += *(_position._playPos++);
+
+ // Process the next info.
+ if ((_position._playPos[0] & 0xF0) >= 0x80)
+ info.event = *(_position._playPos++);
+ else
+ info.event = _position._runningStatus;
+ if (info.event < 0x80)
+ return;
+
+ _position._runningStatus = info.event;
+ switch (info.command()) {
+ case 0xC:
+ 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: // System Common, Meta or SysEx event
+ switch (info.event & 0x0F) {
+ case 0x2: // Song Position Pointer
+ info.basic.param1 = *(_position._playPos++);
+ info.basic.param2 = *(_position._playPos++);
+ break;
+
+ case 0x3: // Song Select
+ info.basic.param1 = *(_position._playPos++);
+ info.basic.param2 = 0;
+ break;
+
+ case 0x6:
+ case 0x8:
+ case 0xA:
+ case 0xB:
+ case 0xC:
+ case 0xE:
+ info.basic.param1 = info.basic.param2 = 0;
+ break;
+
+ case 0x0: // SysEx
+ info.length = readVLQ(_position._playPos);
+ info.ext.data = _position._playPos;
+ _position._playPos += info.length;
+ break;
+
+ case 0xF: // META event
+ info.ext.type = *(_position._playPos++);
+ info.length = readVLQ(_position._playPos);
+ info.ext.data = _position._playPos;
+ _position._playPos += info.length;
+ break;
+ default:
+ warning(
+ "MidiParser_SCI::parseNextEvent: Unsupported event code %x",
+ info.event);
+ } // // System Common, Meta or SysEx event
+
+ default:
+ break;
+ }// switch (info.command())
+}
+
+bool MidiParser_SCI::processEvent(const EventInfo &info, bool fireEvents) {
+ if (!fireEvents) {
+ // We don't do any processing that should be done while skipping events
+ return MidiParser::processEvent(info, fireEvents);
+ }
+
+ switch (info.command()) {
+ case 0xC:
+ if (info.channel() == 0xF) {// SCI special case
+ if (info.basic.param1 == kSetSignalLoop) {
+ _loopTick = _position._playTick;
+ return true;
+ }
+
+ // At least in kq5/french&mac the first scene in the intro has
+ // a song that sets signal to 4 immediately on tick 0. Signal
+ // isn't set at that point by sierra sci and it would cause the
+ // castle daventry text to get immediately removed, so we
+ // currently filter it. Sierra SCI ignores them as well at that
+ // time. However, this filtering should only be performed for
+ // SCI1 and newer games. Signaling is done differently in SCI0
+ // though, so ignoring these signals in SCI0 games will result
+ // in glitches (e.g. the intro of LB1 Amiga gets stuck - bug
+ // #5693). Refer to MusicEntry::setSignal() in sound/music.cpp.
+ // FIXME: SSCI doesn't start playing at the very beginning
+ // of the stream, but at a fixed location a few commands later.
+ // That is probably why this signal isn't triggered
+ // immediately there.
+ bool skipSignal = false;
+ if (!_position._playTick) {
+ skipSignal = true;
+ }
+ if (!skipSignal) {
+ if (!_jumpingToTick) {
+ _pSnd->setSignal(info.basic.param1);
+ debugC(4, "signal %04x", info.basic.param1);
+ }
+ }
+
+ // Done with this event.
+ return true;
+ }
+
+ // Break to let parent handle the rest.
+ break;
+ case 0xB:
+ // Reference for some events:
+ // https://wiki.scummvm.org/index.php/SCI/Specifications/Sound/SCI0_Resource_Format#Status_Reference
+ // Handle common special events
+ switch (info.basic.param1) {
+ case kSetReverb:
+ if (info.basic.param2 == 127) // Set global reverb instead
+ _pSnd->reverb = _music->getGlobalReverb();
+ else
+ _pSnd->reverb = info.basic.param2;
+
+ ((MidiPlayer *)_driver)->setReverb(_pSnd->reverb);
+ break;
+ default:
+ break;
+ }
+
+ // Handle events sent to the SCI special channel (15)
+ if (info.channel() == 0xF) {
+ switch (info.basic.param1) {
+ case kSetReverb:
+ // Already handled above
+ return true;
+ case kMidiHold:
+ // Check if the hold ID marker is the same as the hold ID
+ // marker set for that song by cmdSetSoundHold.
+ // If it is, loop back, but don't stop notes when jumping.
+ if (info.basic.param2 == _pSnd->hold) {
+ jumpToTick(_loopTick, false, false);
+ // Done with this event.
+ return true;
+ }
+ return true;
+ case kUpdateCue:
+ if (!_jumpingToTick) {
+ _pSnd->dataInc += 1;
+ debugC(4, "datainc %04x", 1);
+
+ }
+ return true;
+ case kResetOnPause:
+ _resetOnPause = info.basic.param2;
+ return true;
+ // Unhandled SCI commands
+ case 0x46: // LSL3 - binoculars
+ case 0x61: // Iceman (AdLib?)
+ case 0x73: // Hoyle
+ case 0xD1: // KQ4, when riding the unicorn
+ // Obscure SCI commands - ignored
+ return true;
+ // Standard MIDI commands
+ case 0x01: // mod wheel
+ case 0x04: // foot controller
+ case 0x07: // channel volume
+ case 0x0A: // pan
+ case 0x0B: // expression
+ case 0x40: // sustain
+ case 0x79: // reset all
+ case 0x7B: // notes off
+ // These are all handled by the music driver, so ignore them
+ break;
+ case 0x4B: // voice mapping
+ // TODO: is any support for this needed at the MIDI parser level?
+ warning("Unhanded SCI MIDI command 0x%x - voice mapping (parameter %d)", info.basic.param1, info.basic.param2);
+ return true;
+ default:
+ warning("Unhandled SCI MIDI command 0x%x (parameter %d)", info.basic.param1, info.basic.param2);
+ return true;
+ }
+
+ }
+
+ // Break to let parent handle the rest.
+ break;
+ case 0xF: // META event
+ if (info.ext.type == 0x2F) {// end of track reached
+ if (_pSnd->loop)
+ _pSnd->loop--;
+ // QFG3 abuses the hold flag. Its scripts call kDoSoundSetHold,
+ // but sometimes there's no hold marker in the associated songs
+ // (e.g. song 110, during the intro). The original interpreter
+ // treats this case as an infinite loop (bug #5744).
+ if (_pSnd->loop || _pSnd->hold > 0) {
+ jumpToTick(_loopTick);
+
+ // Done with this event.
+ return true;
+
+ } else {
+ _pSnd->setSignal(0xffff); // SIGNAL_OFFSET
+
+ debugC(4, "signal EOT");
+ }
+ }
+
+ // Break to let parent handle the rest.
+ break;
+
+ default:
+ // Break to let parent handle the rest.
+ break;
+ }
+
+
+ // Let parent handle the rest
+ return MidiParser::processEvent(info, fireEvents);
+}
+
+byte MidiParser_SCI::getSongReverb() {
+ assert(_track);
+
+ for (int i = 0; i < _track->channelCount; i++) {
+ SoundResource::Channel &channel = _track->channels[i];
+ // Peek ahead in the control channel to get the default reverb setting
+ if (channel.number == 15 && channel.data.size() >= 7)
+ return channel.data[6];
+ }
+
+ return 127;
+}
+
+void MidiParser_SCI::allNotesOff() {
+ if (!_driver)
+ return;
+
+ int i, j;
+
+ // Turn off all active notes
+ for (i = 0; i < 128; ++i) {
+ for (j = 0; j < 16; ++j) {
+ if ((_activeNotes[i] & (1 << j)) && (_channelRemap[j] != -1)){
+ sendToDriver(0x80 | j, i, 0);
+ }
+ }
+ }
+
+ // Turn off all hanging notes
+ for (i = 0; i < ARRAYSIZE(_hangingNotes); i++) {
+ byte midiChannel = _hangingNotes[i].channel;
+ if ((_hangingNotes[i].timeLeft) && (_channelRemap[midiChannel] != -1)) {
+ sendToDriver(0x80 | midiChannel, _hangingNotes[i].note, 0);
+ _hangingNotes[i].timeLeft = 0;
+ }
+ }
+ _hangingNotesCount = 0;
+
+ // To be sure, send an "All Note Off" event (but not all MIDI devices
+ // support this...).
+
+ for (i = 0; i < 16; ++i) {
+ if (_channelRemap[i] != -1) {
+ sendToDriver(0xB0 | i, 0x7b, 0); // All notes off
+ sendToDriver(0xB0 | i, 0x40, 0); // Also send a sustain off event (bug #5524)
+ }
+ }
+
+ memset(_activeNotes, 0, sizeof(_activeNotes));
+}
+
+void MidiParser_SCI::setMasterVolume(byte masterVolume) {
+ assert(masterVolume <= MUSIC_MASTERVOLUME_MAX);
+ _masterVolume = masterVolume;
+ ((MidiPlayer *)_driver)->setVolume(masterVolume);
+}
+
+void MidiParser_SCI::setVolume(byte volume) {
+ assert(volume <= MUSIC_VOLUME_MAX);
+ _volume = volume;
+
+ for (int i = 0; i < 15; i++)
+ if (_channelRemap[i] != -1)
+ sendToDriver(0xB0 + i, 7, _channelVolume[i]);
+
+}
+
+void MidiParser_SCI::remapChannel(int channel, int devChannel) {
+ if (_channelRemap[channel] == devChannel)
+ return;
+
+ _channelRemap[channel] = devChannel;
+
+ if (devChannel == -1)
+ return;
+
+// debug(" restoring state: channel %d on devChannel %d", channel, devChannel);
+
+ // restore state
+ ChannelState &s = _channelState[channel];
+
+ int channelVolume = _channelVolume[channel];
+ channelVolume = (channelVolume * _volume / 127) & 0xFF;
+ byte pitch1 = s._pitchWheel & 0x7F;
+ byte pitch2 = (s._pitchWheel >> 7) & 0x7F;
+
+ sendToDriver_raw(0x0040B0 | devChannel); // sustain off
+ sendToDriver_raw(0x004BB0 | devChannel | (s._voices << 16));
+ sendToDriver_raw(0x0000C0 | devChannel | (s._patch << 8));
+ sendToDriver_raw(0x0007B0 | devChannel | (channelVolume << 16));
+ sendToDriver_raw(0x000AB0 | devChannel | (s._pan << 16));
+ sendToDriver_raw(0x0001B0 | devChannel | (s._modWheel << 16));
+ sendToDriver_raw(0x0040B0 | devChannel | (s._sustain ? 0x7F0000 : 0));
+ sendToDriver_raw(0x0000E0 | devChannel | (pitch1 << 8) | (pitch2 << 16));
+
+ // CHECKME: Some SSCI version send a control change 0x4E with s._note as
+ // parameter.
+ // We need to investigate how (and if) drivers should act on this.
+ // Related: controller 0x4E is used for 'mute' in the midiparser.
+ // This could be a bug in SSCI that went unnoticed because few (or no?)
+ // drivers implement controller 0x4E
+
+ // NB: The line below is _not_ valid since s._note can be 0xFF.
+ // SSCI handles this out of band in the driver interface.
+ // sendToDriver_raw(0x004EB0 | devChannel | (s._note << 16);
+}
+
+} // End of namespace Dgds
diff --git a/engines/dgds/sound/midiparser_sci.h b/engines/dgds/sound/midiparser_sci.h
new file mode 100644
index 00000000000..d0f6cf8079e
--- /dev/null
+++ b/engines/dgds/sound/midiparser_sci.h
@@ -0,0 +1,132 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DGDS_SOUND_MIDIPARSER_H
+#define DGDS_SOUND_MIDIPARSER_H
+
+#include "dgds/sound/music.h"
+#include "dgds/sound/resource/sci_resource.h"
+#include "audio/midiparser.h"
+
+/*
+ Sound drivers info: (from driver cmd0)
+ AdLib/SB : track 0 , voices 9 , patch 3 ah=1
+ ProAudioSp: track 0 , voices 9 , patch 3 ah=17
+ GenerlMIDI: track 7 , voices 32, patch 4 ah=1 SCI1.1
+ Game Blast: track 9 , voices 12, patch 101 ah=1
+ MT-32 : track 12, voices 32, patch 1 ah=1
+ PC Speaker: track 18, voices 1 , patch 0xFF ah=1
+ Tandy : track 19, voices 3 , patch 101 ah=1
+ IBM PS/1 : track 19, voices 3 , patch 101 ah=1
+ */
+
+namespace Dgds {
+
+/**
+ * An extended standard MIDI (SMF) parser. Sierra used an extra channel
+ * with special commands for extended functionality and animation syncing.
+ * Refer to MidiParser_SMF() in /sound/midiparser_smf.cpp for the standard
+ * MIDI (SMF) parser functionality that the SCI MIDI parser is based on
+ */
+class MidiParser_SCI : public MidiParser {
+public:
+ MidiParser_SCI(SciMusic *music);
+ ~MidiParser_SCI() override;
+
+ void mainThreadBegin();
+ void mainThreadEnd();
+
+ bool loadMusic(SoundResource::Track *track, MusicEntry *psnd, int channelFilterMask);
+ bool loadMusic(byte *, uint32) override {
+ return false;
+ }
+ void initTrack();
+ void sendInitCommands();
+ void unloadMusic() override;
+ void setMasterVolume(byte masterVolume);
+ void setVolume(byte volume);
+ void stop() {
+ _abortParse = true;
+ allNotesOff();
+ }
+ void pause() {
+ allNotesOff();
+ if (_resetOnPause)
+ jumpToTick(0);
+ }
+
+ void allNotesOff() override;
+
+ const SciSpan<const byte> &getMixedData() const { return *_mixedData; }
+ byte getSongReverb();
+
+ void sendFromScriptToDriver(uint32 midi);
+ void sendToDriver(uint32 midi) override;
+ void sendToDriver(byte status, byte firstOp, byte secondOp) {
+ sendToDriver(status | ((uint32)firstOp << 8) | ((uint32)secondOp << 16));
+ }
+
+ void remapChannel(int channel, int devChannel);
+
+protected:
+ void parseNextEvent(EventInfo &info) override;
+ bool processEvent(const EventInfo &info, bool fireEvents = true) override;
+ void midiMixChannels();
+ byte midiGetNextChannel(long ticker);
+ void resetStateTracking();
+ void trackState(uint32 midi);
+ void sendToDriver_raw(uint32 midi);
+
+ SciMusic *_music;
+
+ // this is set, when main thread calls us -> we send commands to queue instead to driver
+ bool _mainThreadCalled;
+
+ Common::SpanOwner<SciSpan<const byte> > _mixedData;
+ SoundResource::Track *_track;
+ MusicEntry *_pSnd;
+ uint32 _loopTick;
+ byte _masterVolume; // the overall master volume (same for all tracks)
+ byte _volume; // the global volume of the current track
+
+ bool _resetOnPause;
+
+ bool _channelUsed[16];
+ int16 _channelRemap[16];
+ byte _channelVolume[16];
+
+ struct ChannelState {
+ int8 _modWheel;
+ int8 _pan;
+ int8 _patch;
+ int8 _note;
+ bool _sustain;
+ int16 _pitchWheel;
+ int8 _voices;
+ };
+
+ ChannelState _channelState[16];
+
+};
+
+} // End of namespace Dgds
+
+#endif // DGDS_SOUND_MIDIPARSER_H
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
new file mode 100644
index 00000000000..60298665f6d
--- /dev/null
+++ b/engines/dgds/sound/music.cpp
@@ -0,0 +1,1407 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "common/config-manager.h"
+#include "common/translation.h"
+#include "gui/error.h"
+
+#include "dgds/sound/resource/sci_resource.h"
+#include "dgds/sound/midiparser_sci.h"
+#include "dgds/sound/music.h"
+
+//#define DEBUG_REMAP
+
+namespace Dgds {
+
+SciMusic::SciMusic(bool useDigitalSFX) :
+ _mutex(g_system->getMixer()->mutex()),
+ _soundOn(true),
+ _masterVolume(15),
+ _globalReverb(0),
+ _useDigitalSFX(useDigitalSFX),
+ _needsResume(true),
+ _globalPause(0),
+ _pMidiDrv(nullptr) {
+
+ // Reserve some space in the playlist, to avoid expensive insertion
+ // operations
+ _playList.reserve(10);
+
+ for (int i = 0; i < 16; i++) {
+ _usedChannel[i] = nullptr;
+ _channelRemap[i] = -1;
+ _channelMap[i]._song = nullptr;
+ _channelMap[i]._channel = -1;
+ }
+
+ _queuedCommands.reserve(1000);
+}
+
+SciMusic::~SciMusic() {
+ if (_pMidiDrv) {
+ _pMidiDrv->close();
+ delete _pMidiDrv;
+ }
+}
+
+void SciMusic::init() {
+ // system init
+ _pMixer = g_system->getMixer();
+ // SCI sound init
+ _dwTempo = 0;
+
+ const Common::Platform platform = DgdsEngine::getInstance()->getPlatform();
+ uint32 deviceFlags = MDT_PCSPK | MDT_ADLIB | MDT_MIDI;
+
+ uint32 dev = MidiDriver::detectDevice(deviceFlags);
+ _musicType = MidiDriver::getMusicType(dev);
+
+ switch (_musicType) {
+ case MT_ADLIB:
+ // FIXME: There's no Amiga sound option, so we hook it up to AdLib
+ if (platform == Common::kPlatformMacintosh || platform == Common::kPlatformAmiga) {
+ _pMidiDrv = MidiPlayer_AmigaMac1_create(platform);
+ } else
+ _pMidiDrv = MidiPlayer_AdLib_create();
+ break;
+ case MT_PCSPK:
+ error("TODO: Implement pc speaker driver?");
+ //_pMidiDrv = MidiPlayer_PCSpeaker_create();
+ break;
+ default:
+ _pMidiDrv = MidiPlayer_Midi_create();
+ }
+
+ if (_pMidiDrv && !_pMidiDrv->open()) {
+ _pMidiDrv->setTimerCallback(this, &miditimerCallback);
+ _dwTempo = _pMidiDrv->getBaseTempo();
+ } else {
+ error("Failed to initialize sound driver");
+ }
+
+ // Find out what the first possible channel is (used, when doing channel
+ // remapping).
+ _driverFirstChannel = _pMidiDrv->getFirstChannel();
+ _driverLastChannel = _pMidiDrv->getLastChannel();
+
+ _currentlyPlayingSample = nullptr;
+ _timeCounter = 0;
+ _needsRemap = false;
+}
+
+void SciMusic::miditimerCallback(void *p) {
+ SciMusic *sciMusic = (SciMusic *)p;
+
+ Common::StackLock lock(sciMusic->_mutex);
+ sciMusic->onTimer();
+}
+
+void SciMusic::onTimer() {
+ const MusicList::iterator end = _playList.end();
+ // sending out queued commands that were "sent" via main thread
+ sendMidiCommandsFromQueue();
+
+ // remap channels, if requested
+ if (_needsRemap)
+ remapChannels(false);
+ _needsRemap = false;
+
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i)
+ (*i)->onTimer();
+}
+
+void SciMusic::putMidiCommandInQueue(byte status, byte firstOp, byte secondOp) {
+ putMidiCommandInQueue(status | ((uint32)firstOp << 8) | ((uint32)secondOp << 16));
+}
+
+void SciMusic::putMidiCommandInQueue(uint32 midi) {
+ _queuedCommands.push_back(MidiCommand(MidiCommand::kTypeMidiMessage, midi));
+}
+
+void SciMusic::putTrackInitCommandInQueue(MusicEntry *psnd) {
+ _queuedCommands.push_back(MidiCommand(MidiCommand::kTypeTrackInit, psnd));
+}
+
+void SciMusic::removeTrackInitCommandsFromQueue(MusicEntry *psnd) {
+ for (MidiCommandQueue::iterator i = _queuedCommands.begin(); i != _queuedCommands.end(); )
+ i = (i->_type == MidiCommand::kTypeTrackInit && i->_dataPtr == (void*)psnd) ? _queuedCommands.erase(i) : i + 1;
+}
+
+// This sends the stored commands from queue to driver (is supposed to get
+// called only during onTimer()). At least mt32 emulation doesn't like getting
+// note-on commands from main thread (if we directly send, we would get a crash
+// during piano scene in lsl5).
+void SciMusic::sendMidiCommandsFromQueue() {
+ uint curCommand = 0;
+ uint commandCount = _queuedCommands.size();
+
+ while (curCommand < commandCount) {
+ if (_queuedCommands[curCommand]._type == MidiCommand::kTypeTrackInit) {
+ if (_queuedCommands[curCommand]._dataPtr) {
+ MusicList::iterator psnd = Common::find(_playList.begin(), _playList.end(), static_cast<MusicEntry*>(_queuedCommands[curCommand]._dataPtr));
+ if (psnd != _playList.end() && (*psnd)->pMidiParser)
+ (*psnd)->pMidiParser->initTrack();
+ }
+ } else {
+ _pMidiDrv->send(_queuedCommands[curCommand]._dataVal);
+ }
+ curCommand++;
+ }
+ _queuedCommands.clear();
+}
+
+void SciMusic::clearPlayList() {
+ // we must NOT lock our mutex here. Playlist is modified inside soundKill() which will lock the mutex
+ // during deletion. If we lock it here, a deadlock may occur within soundStop() because that one
+ // calls the mixer, which will also lock the mixer mutex and if the mixer thread is active during
+ // that time, we will get a deadlock.
+ while (!_playList.empty()) {
+ soundStop(_playList[0]);
+ soundKill(_playList[0]);
+ }
+}
+
+void SciMusic::pauseAll(bool pause) {
+ const MusicList::iterator end = _playList.end();
+ bool alreadyUnpaused = (_globalPause <= 0);
+
+ if (pause)
+ _globalPause++;
+ else
+ _globalPause--;
+
+ bool stillUnpaused = (_globalPause <= 0);
+ // This check is for a specific situation (the ScummVM autosave) which will try to unpause the music,
+ // although it is already unpaused, and after the save it will then pause it again. We allow the
+ // _globalPause counter to go into the negative, so that the final outcome of both calls is a _globalPause
+ // counter of 0 (First: 0, then -1, then 0 again). However, the pause counters of the individual sounds
+ // do not support negatives. And it would be somewhat more likely to cause regressions to add that
+ // support than to just contain it here...
+ // So, for cases where the status of the _globalPause counter only changes in the range below or equal 0
+ // we return here. The individual sounds only need to get targeted if they ACTUALLY get paused or
+ // unpaused (_globalPause counter changes from 0 to 1 or from 1 to 0) or if the pause counter is
+ // increased above 1 (since positive counters are supported and required for the individual sounds).
+ if (alreadyUnpaused && stillUnpaused)
+ return;
+
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ soundToggle(*i, pause);
+ }
+}
+
+void SciMusic::resetGlobalPauseCounter() {
+ // This is an adjustment for our savegame loading process,
+ // ONLY when done from kRestoreGame().
+ // The enginge will call SciMusic::pauseAll() before loading.
+ // So the _globalPause will be increased and the individual
+ // sounds will be paused, too. However, the sounds will
+ // then be restored to the playing status that is stored in
+ // the savegame. The _globalPause stays, however. There may
+ // be no unpausing after the loading, since the playing status
+ // in the savegames is the correct one. So, the essence is:
+ // the _globalPause counter needs to go down without anything
+ // else happening.
+ // The loading from GMM has been implemented differently. It
+ // will remove the paused state before loading (and doesn't
+ // do anything unpleasant afterwards, either). So this is not
+ // needed there.
+ // I have added an assert, since it is such a special case,
+ // people need to know what they're doing if they call this.
+ // The value can be greater than 1, since the scripts may
+ // already have increased it, before the kRestoreGame() call
+ // happens.
+ assert(_globalPause >= 1);
+ _globalPause = 0;
+}
+
+void SciMusic::stopAll() {
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ soundStop(*i);
+ }
+}
+
+void SciMusic::stopAllSamples() {
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ if ((*i)->isSample) {
+ soundStop(*i);
+ }
+ }
+}
+
+void SciMusic::soundSetSoundOn(bool soundOnFlag) {
+ Common::StackLock lock(_mutex);
+
+ _soundOn = soundOnFlag;
+ _pMidiDrv->playSwitch(soundOnFlag);
+}
+
+uint16 SciMusic::soundGetVoices() {
+ Common::StackLock lock(_mutex);
+
+ return _pMidiDrv->getPolyphony();
+}
+
+MusicEntry *SciMusic::getSlot(uint32 obj) {
+ Common::StackLock lock(_mutex);
+
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ if ((*i)->soundObj == obj)
+ return *i;
+ }
+
+ return nullptr;
+}
+
+MusicEntry *SciMusic::getFirstSlotWithStatus(SoundStatus status) {
+ for (MusicList::iterator i = _playList.begin(); i != _playList.end(); ++i) {
+ if ((*i)->status == status)
+ return *i;
+ }
+ return nullptr;
+}
+
+void SciMusic::stopMusic() {
+ for (MusicList::iterator i = _playList.begin(); i != _playList.end(); ++i) {
+ if ((*i)->soundType == Audio::Mixer::kMusicSoundType)
+ soundStop(*i);
+ }
+}
+
+void SciMusic::stopSFX() {
+ for (MusicList::iterator i = _playList.begin(); i != _playList.end(); ++i) {
+ if ((*i)->soundType == Audio::Mixer::kSFXSoundType)
+ soundStop(*i);
+ }
+}
+
+
+void SciMusic::setGlobalReverb(int8 reverb) {
+ Common::StackLock lock(_mutex);
+ if (reverb != 127) {
+ // Set global reverb normally
+ _globalReverb = reverb;
+
+ // Check the reverb of the active song...
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ if ((*i)->status == kSoundPlaying) {
+ if ((*i)->reverb == 127) // Active song has no reverb
+ _pMidiDrv->setReverb(reverb); // Set the global reverb
+ break;
+ }
+ }
+ } else {
+ // Set reverb of the active song
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ if ((*i)->status == kSoundPlaying) {
+ _pMidiDrv->setReverb((*i)->reverb); // Set the song's reverb
+ break;
+ }
+ }
+ }
+}
+
+byte SciMusic::getCurrentReverb() {
+ Common::StackLock lock(_mutex);
+ return _pMidiDrv->getReverb();
+}
+
+// A larger priority value has higher priority. For equal priority values,
+// songs that have been added later have higher priority.
+static bool musicEntryCompare(const MusicEntry *l, const MusicEntry *r) {
+ return (l->priority > r->priority) || (l->priority == r->priority && l->time > r->time);
+}
+
+void SciMusic::sortPlayList() {
+ // Sort the play list in descending priority order
+ Common::sort(_playList.begin(), _playList.end(), musicEntryCompare);
+}
+
+void SciMusic::soundInitSnd(MusicEntry *pSnd) {
+ // Remove all currently mapped channels of this MusicEntry first,
+ // since they will no longer be valid.
+ for (int i = 0; i < 16; ++i) {
+ if (_channelMap[i]._song == pSnd) {
+ _channelMap[i]._song = nullptr;
+ _channelMap[i]._channel = -1;
+ }
+ }
+
+ SoundResource::Track *track = pSnd->soundRes->getTrackByType(_pMidiDrv->getPlayId());
+
+ // If MIDI device is selected but there is no digital track in sound
+ // resource try to use Adlib's digital sample if possible. Also, if the
+ // track couldn't be found, load the digital track, as some games depend on
+ // this (e.g. the Longbow demo).
+ if (!track || (_useDigitalSFX && track->digitalChannelNr == -1)) {
+ SoundResource::Track *digital = pSnd->soundRes->getDigitalTrack();
+ if (digital)
+ track = digital;
+ }
+
+ pSnd->time = ++_timeCounter;
+
+ if (track) {
+ bool playSample = (track->digitalChannelNr != -1 && (_useDigitalSFX || track->channelCount == 1));
+
+ // Play digital sample
+ if (playSample) {
+ const SciSpan<const byte> &channelData = track->channels[track->digitalChannelNr].data;
+ delete pSnd->pStreamAud;
+ byte flags = Audio::FLAG_UNSIGNED;
+ // Amiga SCI1 games had signed sound data
+ if (DgdsEngine::getInstance()->getPlatform() == Common::kPlatformAmiga)
+ flags = 0;
+ int endPart = track->digitalSampleEnd > 0 ? (track->digitalSampleSize - track->digitalSampleEnd) : 0;
+ const uint size = track->digitalSampleSize - track->digitalSampleStart - endPart;
+ pSnd->pStreamAud = Audio::makeRawStream(channelData.getUnsafeDataAt(track->digitalSampleStart),
+ size, track->digitalSampleRate, flags, DisposeAfterUse::NO);
+ assert(pSnd->pStreamAud);
+ delete pSnd->pLoopStream;
+ pSnd->pLoopStream = nullptr;
+ pSnd->soundType = Audio::Mixer::kSFXSoundType;
+ pSnd->hCurrentAud = Audio::SoundHandle();
+ pSnd->playBed = false;
+ pSnd->overridePriority = false;
+ pSnd->isSample = true;
+ } else {
+ // play MIDI track
+ Common::StackLock lock(_mutex);
+ pSnd->soundType = Audio::Mixer::kMusicSoundType;
+ if (pSnd->pMidiParser == nullptr) {
+ pSnd->pMidiParser = new MidiParser_SCI(this);
+ pSnd->pMidiParser->setMidiDriver(_pMidiDrv);
+ pSnd->pMidiParser->setTimerRate(_dwTempo);
+ pSnd->pMidiParser->setMasterVolume(_masterVolume);
+ }
+
+ pSnd->pauseCounter = 0;
+
+ for (int i = 0; i < 16; ++i) {
+ pSnd->_usedChannels[i] = 0xFF;
+ pSnd->_chan[i]._dontMap = false;
+ pSnd->_chan[i]._dontRemap = false;
+ pSnd->_chan[i]._prio = -1;
+ pSnd->_chan[i]._voices = -1;
+ pSnd->_chan[i]._mute = 0;
+ }
+ for (int i = 0; i < track->channelCount; ++i) {
+ // skip digital channel
+ if (i == track->digitalChannelNr) {
+ continue;
+ }
+
+ SoundResource::Channel &chan = track->channels[i];
+
+ assert(chan.number < ARRAYSIZE(pSnd->_chan));
+ pSnd->_usedChannels[i] = chan.number;
+ // Flag 1 is exclusive towards the other flags. When it is
+ // set the others won't even get evaluated. And it wouldn't
+ // matter, since channels flagged with 1 operate completely
+ // independent of the channel mapping.
+ // For more info on the flags see the comment in
+ // SoundResource::SoundResource().
+ pSnd->_chan[chan.number]._dontMap |= (bool)(chan.flags & 1);
+ // Flag 2 prevents the channel number from being remapped
+ // to a different free channel on the MIDI device.
+ // It's possible for a MIDI track to define the same channel
+ // multiple times with different values for dontRemap.
+ // This can be a problem if it is a dedicated percussion
+ // channel, so always err on the side of caution.
+ pSnd->_chan[chan.number]._dontRemap |= (bool)(chan.flags & 2);
+ if (pSnd->_chan[chan.number]._prio == -1)
+ pSnd->_chan[chan.number]._prio = chan.prio;
+ if (pSnd->_chan[chan.number]._voices == -1)
+ pSnd->_chan[chan.number]._voices = chan.poly;
+ pSnd->_chan[chan.number]._mute |= ((chan.flags & 4) ? 1 : 0);
+ // FIXME: Most MIDI tracks use the first 10 bytes for
+ // fixed MIDI commands. SSCI skips those the first iteration,
+ // but _does_ update channel state (including volume) with
+ // them. Specifically, prio/voices, patch, volume, pan.
+ // This should probably be implemented in MidiParser_SCI::loadMusic.
+ //
+ // UPDATE: While we could change how we handle it, we DO
+ // read the commands into the channel data arrays when we call
+ // trackState(). So, I think what we do has the same result...
+ }
+
+ pSnd->pMidiParser->mainThreadBegin();
+ // loadMusic() below calls jumpToTick.
+ // Disable sound looping and hold before jumpToTick is called,
+ // otherwise the song may keep looping forever when it ends in
+ // jumpToTick (e.g. LSL3, when going left from room 210).
+ uint16 prevLoop = pSnd->loop;
+ int16 prevHold = pSnd->hold;
+ pSnd->loop = 0;
+ pSnd->hold = -1;
+ pSnd->playBed = false;
+ pSnd->overridePriority = false;
+
+ pSnd->pMidiParser->loadMusic(track, pSnd, 0);
+ pSnd->reverb = pSnd->pMidiParser->getSongReverb();
+
+ // Restore looping and hold
+ pSnd->loop = prevLoop;
+ pSnd->hold = prevHold;
+ pSnd->pMidiParser->mainThreadEnd();
+ }
+ }
+}
+
+void SciMusic::soundPlay(MusicEntry *pSnd, bool restoring) {
+ _mutex.lock();
+
+ uint playListCount = _playList.size();
+ uint playListNo = playListCount;
+ MusicEntry *alreadyPlaying = nullptr;
+
+ // searching if sound is already in _playList
+ for (uint i = 0; i < playListCount; i++) {
+ if (_playList[i] == pSnd)
+ playListNo = i;
+ if ((_playList[i]->status == kSoundPlaying) && (_playList[i]->pMidiParser))
+ alreadyPlaying = _playList[i];
+ }
+ if (playListNo == playListCount) { // not found
+ _playList.push_back(pSnd);
+ }
+
+ pSnd->time = ++_timeCounter;
+ sortPlayList();
+
+ _mutex.unlock(); // unlock to perform mixer-related calls
+
+ if (pSnd->isSample) {
+ if (isDigitalSamplePlaying()) {
+ // Another sample is already playing, we have to stop that one
+ // SSCI is only able to play 1 sample at a time
+ // In Space Quest 5 room 250 the player is able to open the air-hatch and kill himself.
+ // In that situation the scripts are playing 2 samples at the same time and the first sample
+ // is not supposed to play.
+ // TODO: SSCI actually calls kDoAudio(play) internally, which stops other samples from being played
+ // but such a change isn't trivial, because we also handle Sound resources in here, that contain samples
+ _pMixer->stopHandle(_currentlyPlayingSample->hCurrentAud);
+ warning("kDoSound: sample already playing, old resource %d, new resource %d", _currentlyPlayingSample->resourceId, pSnd->resourceId);
+ }
+ // Sierra SCI ignores volume set when playing samples via kDoSound
+ // At least freddy pharkas/CD has a script bug that sets volume to 0
+ // when playing the "score" sample
+ if (pSnd->loop > 1) {
+ pSnd->pLoopStream = new Audio::LoopingAudioStream(pSnd->pStreamAud, pSnd->loop, DisposeAfterUse::NO);
+ _pMixer->playStream(pSnd->soundType, &pSnd->hCurrentAud,
+ pSnd->pLoopStream, -1, _pMixer->kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ } else {
+ // Rewind in case we play the same sample multiple times
+ // (non-looped) like in pharkas right at the start
+ pSnd->pStreamAud->rewind();
+ _pMixer->playStream(pSnd->soundType, &pSnd->hCurrentAud,
+ pSnd->pStreamAud, -1, _pMixer->kMaxChannelVolume, 0,
+ DisposeAfterUse::NO);
+ }
+ // Remember the sample, that is now playing
+ _currentlyPlayingSample = pSnd;
+ } else {
+ if (pSnd->pMidiParser) {
+ Common::StackLock lock(_mutex);
+ pSnd->pMidiParser->mainThreadBegin();
+
+ // Call this before initTrack(), since several sound drivers have custom channel volumes that get set in
+ // initTrack() and we don't want to overwrite those with the generic values from sendInitCommands().
+ if (pSnd->status != kSoundPaused)
+ pSnd->pMidiParser->sendInitCommands();
+
+ // The track init always needs to be done. Otherwise some sounds will not be properly set up (bug #11476).
+ // It is also safe to do this for paused tracks, since the jumpToTick() command further down will parse through
+ // the song from the beginning up to the resume position and ensure that the actual current voice mapping,
+ // instrument and volume settings etc. are correct.
+ // First glance at disasm might suggest that it has to be called only once per sound. But the truth is that
+ // when calling the sound driver opcode for sound restoring (opcode no. 9, we don't have that) it will
+ // internally also call initTrack(). And it wouldn't make sense otherwise, since without that the channel setup
+ // from the last sound would still be active.
+ pSnd->pMidiParser->initTrack();
+
+ pSnd->pMidiParser->setVolume(pSnd->volume);
+
+ // Disable sound looping and hold before jumpToTick is called,
+ // otherwise the song may keep looping forever when it ends in jumpToTick.
+ // This is needed when loading saved games, or when a game
+ // stops the same sound twice (e.g. LSL3 Amiga, going left from
+ // room 210 to talk with Kalalau). Fixes bugs #5404 and #5503.
+ uint16 prevLoop = pSnd->loop;
+ int16 prevHold = pSnd->hold;
+ pSnd->loop = 0;
+ pSnd->hold = -1;
+
+ bool fastForward = (pSnd->status == kSoundPaused) || (pSnd->status == kSoundPlaying && restoring);
+ if (!fastForward) {
+ pSnd->pMidiParser->jumpToTick(0);
+ } else {
+ // Fast forward to the last position and perform associated events when loading
+ pSnd->pMidiParser->jumpToTick(pSnd->ticker, true, true, true);
+ }
+
+ // Restore looping and hold
+ pSnd->loop = prevLoop;
+ pSnd->hold = prevHold;
+ pSnd->pMidiParser->mainThreadEnd();
+ }
+ }
+
+ pSnd->status = kSoundPlaying;
+
+ _mutex.lock();
+ remapChannels();
+ _mutex.unlock();
+}
+
+void SciMusic::soundStop(MusicEntry *pSnd) {
+ SoundStatus previousStatus = pSnd->status;
+ pSnd->status = kSoundStopped;
+
+ if (pSnd->isSample) {
+ if (_currentlyPlayingSample == pSnd)
+ _currentlyPlayingSample = nullptr;
+ _pMixer->stopHandle(pSnd->hCurrentAud);
+ }
+
+ if (pSnd->pMidiParser) {
+ Common::StackLock lock(_mutex);
+ pSnd->pMidiParser->mainThreadBegin();
+ // We shouldn't call stop in case it's paused, otherwise we would send
+ // allNotesOff() again
+ if (previousStatus == kSoundPlaying)
+ pSnd->pMidiParser->stop();
+ pSnd->pMidiParser->mainThreadEnd();
+ remapChannels();
+ }
+
+ pSnd->fadeStep = 0; // end fading, if fading was in progress
+}
+
+void SciMusic::soundSetVolume(MusicEntry *pSnd, byte volume) {
+ assert(volume <= MUSIC_VOLUME_MAX);
+ if (!pSnd->isSample && pSnd->pMidiParser) {
+ Common::StackLock lock(_mutex);
+ pSnd->pMidiParser->mainThreadBegin();
+ pSnd->pMidiParser->setVolume(volume);
+ pSnd->pMidiParser->mainThreadEnd();
+ }
+}
+
+// this is used to set volume of the sample, used for fading only!
+void SciMusic::soundSetSampleVolume(MusicEntry *pSnd, byte volume) {
+ assert(volume <= MUSIC_VOLUME_MAX);
+ assert(pSnd->pStreamAud);
+ _pMixer->setChannelVolume(pSnd->hCurrentAud, volume * 2); // Mixer is 0-255, SCI is 0-127
+}
+
+void SciMusic::soundSetPriority(MusicEntry *pSnd, byte prio) {
+ Common::StackLock lock(_mutex);
+
+ pSnd->priority = prio;
+ pSnd->time = ++_timeCounter;
+ sortPlayList();
+}
+
+void SciMusic::soundKill(MusicEntry *pSnd) {
+ pSnd->status = kSoundStopped;
+
+ _mutex.lock();
+ remapChannels();
+
+ if (pSnd->pMidiParser) {
+ pSnd->pMidiParser->mainThreadBegin();
+ pSnd->pMidiParser->unloadMusic();
+ pSnd->pMidiParser->mainThreadEnd();
+ delete pSnd->pMidiParser;
+ pSnd->pMidiParser = nullptr;
+ }
+
+ _mutex.unlock();
+
+ if (pSnd->isSample) {
+ if (_currentlyPlayingSample == pSnd) {
+ // Forget about this sound, in case it was currently playing
+ _currentlyPlayingSample = nullptr;
+ }
+ _pMixer->stopHandle(pSnd->hCurrentAud);
+ delete pSnd->pStreamAud;
+ pSnd->pStreamAud = nullptr;
+ delete pSnd->pLoopStream;
+ pSnd->pLoopStream = nullptr;
+ pSnd->isSample = false;
+ }
+
+ _mutex.lock();
+ uint sz = _playList.size(), i;
+ // Remove sound from playlist
+ for (i = 0; i < sz; i++) {
+ if (_playList[i] == pSnd) {
+ delete _playList[i]->soundRes;
+ delete _playList[i];
+ _playList.remove_at(i);
+ break;
+ }
+ }
+ _mutex.unlock();
+}
+
+void SciMusic::soundPause(MusicEntry *pSnd) {
+ // SCI seems not to be pausing samples played back by kDoSound at all
+ // It only stops looping samples (actually doesn't loop them again before they are unpaused)
+ // Examples: Space Quest 1 death by acid drops (pause is called even specifically for the sample, see bug #5097)
+ // Eco Quest 1 during the intro when going to the abort-menu
+ // In both cases sierra sci keeps playing
+ // Leisure Suit Larry 1 doll scene - it seems that pausing here actually just stops
+ // further looping from happening
+ // This is a somewhat bigger change, I'm leaving in the old code in here just in case
+ // I'm currently pausing looped sounds directly, non-looped sounds won't get paused
+ if ((pSnd->pStreamAud) && (!pSnd->pLoopStream))
+ return;
+ pSnd->pauseCounter++;
+ if (pSnd->status != kSoundPlaying)
+ return;
+ _needsResume = true;
+ pSnd->status = kSoundPaused;
+ if (pSnd->pStreamAud) {
+ _pMixer->pauseHandle(pSnd->hCurrentAud, true);
+ } else {
+ if (pSnd->pMidiParser) {
+ Common::StackLock lock(_mutex);
+ pSnd->pMidiParser->mainThreadBegin();
+ pSnd->pMidiParser->pause();
+ pSnd->pMidiParser->mainThreadEnd();
+ remapChannels();
+ }
+ }
+}
+
+void SciMusic::soundResume(MusicEntry *pSnd) {
+ if (pSnd->pauseCounter > 0)
+ pSnd->pauseCounter--;
+ if (pSnd->pauseCounter != 0)
+ return;
+ if (pSnd->status != kSoundPaused || (_globalPause > 0 && !_needsResume))
+ return;
+ _needsResume = true;
+ if (pSnd->pStreamAud) {
+ _pMixer->pauseHandle(pSnd->hCurrentAud, false);
+ pSnd->status = kSoundPlaying;
+ } else {
+ soundPlay(pSnd, true);
+ }
+}
+
+void SciMusic::soundToggle(MusicEntry *pSnd, bool pause) {
+ if (pause)
+ soundPause(pSnd);
+ else
+ soundResume(pSnd);
+}
+
+uint16 SciMusic::soundGetMasterVolume() {
+ if (ConfMan.getBool("mute")) {
+ // When a game is muted, the master volume is set to zero so that
+ // mute applies to external MIDI devices, but this should not be
+ // communicated to the game as it will cause the UI to be drawn with
+ // the wrong (zero) volume for music
+ return (ConfMan.getInt("music_volume") + 1) * MUSIC_MASTERVOLUME_MAX / Audio::Mixer::kMaxMixerVolume;
+ }
+
+ return _masterVolume;
+}
+
+void SciMusic::soundSetMasterVolume(uint16 vol) {
+ _masterVolume = vol;
+
+ Common::StackLock lock(_mutex);
+
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ if ((*i)->pMidiParser)
+ (*i)->pMidiParser->setMasterVolume(vol);
+ }
+}
+
+/* These direct midi command sends are not needed in dgds
+void SciMusic::sendMidiCommand(uint32 cmd) {
+ Common::StackLock lock(_mutex);
+ _pMidiDrv->send(cmd);
+}
+
+void SciMusic::sendMidiCommand(MusicEntry *pSnd, uint32 cmd) {
+ Common::StackLock lock(_mutex);
+ if (!pSnd->pMidiParser) {
+ // FPFP calls kDoSound SendMidi to mute and unmute its gameMusic2 sound
+ // object but some scenes set this to an audio sample. In Act 2, room
+ // 660 sets this to audio of restaurant customers talking. Walking up
+ // the hotel stairs from room 500 to 235 calls gameMusic2:mute and
+ // triggers this if gameMusic2 hasn't changed. Bug #10952
+ warning("tried to cmdSendMidi on non midi slot (%08x)", pSnd->soundObj);
+ return;
+ }
+
+ pSnd->pMidiParser->mainThreadBegin();
+ pSnd->pMidiParser->sendFromScriptToDriver(cmd);
+ pSnd->pMidiParser->mainThreadEnd();
+}
+*/
+
+void SciMusic::printPlayList(Console *con) {
+ Common::StackLock lock(_mutex);
+
+ const char *musicStatus[] = { "Stopped", "Initialized", "Paused", "Playing" };
+
+ for (uint32 i = 0; i < _playList.size(); i++) {
+ MusicEntry *song = _playList[i];
+ debug("%d: %08x (%s), resource id: %d, status: %s, %s type\n",
+ i, song->soundObj,
+ "",//g_sci->getEngineState()->_segMan->getObjectName(song->soundObj),
+ song->resourceId, musicStatus[song->status],
+ song->pMidiParser ? "MIDI" : "digital audio");
+ }
+}
+
+void SciMusic::printSongInfo(uint32 obj, Console *con) {
+ Common::StackLock lock(_mutex);
+
+ const char *musicStatus[] = { "Stopped", "Initialized", "Paused", "Playing" };
+
+ const MusicList::iterator end = _playList.end();
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ MusicEntry *song = *i;
+ if (song->soundObj == obj) {
+ debug("Resource id: %d, status: %s\n", song->resourceId, musicStatus[song->status]);
+ debug("dataInc: %d, hold: %d, loop: %d\n", song->dataInc, song->hold, song->loop);
+ debug("signal: %d, priority: %d\n", song->signal, song->priority);
+ debug("ticker: %d, volume: %d\n", song->ticker, song->volume);
+
+ if (song->pMidiParser) {
+ debug("Type: MIDI\n");
+ if (song->soundRes) {
+ SoundResource::Track *track = song->soundRes->getTrackByType(_pMidiDrv->getPlayId());
+ if (track) {
+ debug("Channels: %d\n", track->channelCount);
+ }
+ }
+ } else if (song->pStreamAud || song->pLoopStream) {
+ debug("Type: digital audio (%s), sound active: %s\n",
+ song->pStreamAud ? "non looping" : "looping",
+ _pMixer->isSoundHandleActive(song->hCurrentAud) ? "yes" : "no");
+ if (song->soundRes) {
+ debug("Sound resource information:\n");
+ SoundResource::Track *track = song->soundRes->getTrackByType(_pMidiDrv->getPlayId());
+ if (track && track->digitalChannelNr != -1) {
+ debug("Sample size: %d, sample rate: %d, channels: %d, digital channel number: %d\n",
+ track->digitalSampleSize, track->digitalSampleRate, track->channelCount, track->digitalChannelNr);
+ }
+ }
+ }
+
+ return;
+ }
+ }
+
+ debug("Song object not found in playlist");
+}
+
+MusicEntry::MusicEntry() {
+ soundObj = 0;
+
+ soundRes = nullptr;
+ resourceId = 0;
+
+ time = 0;
+
+ dataInc = 0;
+ ticker = 0;
+ signal = 0;
+ priority = 0;
+ loop = 0;
+ volume = MUSIC_VOLUME_DEFAULT;
+ hold = -1;
+ reverb = -1;
+
+ pauseCounter = 0;
+ sampleLoopCounter = 0;
+
+ fadeTo = 0;
+ fadeStep = 0;
+ fadeTicker = 0;
+ fadeTickerStep = 0;
+ fadeSetVolume = false;
+ fadeCompleted = false;
+ stopAfterFading = false;
+
+ status = kSoundStopped;
+
+ soundType = Audio::Mixer::kMusicSoundType;
+
+ pStreamAud = nullptr;
+ pLoopStream = nullptr;
+ pMidiParser = nullptr;
+ isSample = false;
+
+ for (int i = 0; i < 16; ++i) {
+ _usedChannels[i] = 0xFF;
+ _chan[i]._prio = 127;
+ _chan[i]._voices = 0;
+ _chan[i]._dontRemap = false;
+ _chan[i]._mute = false;
+ }
+}
+
+MusicEntry::~MusicEntry() {
+}
+
+void MusicEntry::onTimer() {
+ if (!signal) {
+ if (!signalQueue.empty()) {
+ // no signal set, but signal in queue, set that one
+ signal = signalQueue[0];
+ signalQueue.remove_at(0);
+ }
+ }
+
+ if (status != kSoundPlaying || !loop)
+ return;
+
+ // Fade MIDI and digital sound effects
+ if (fadeStep)
+ doFade();
+
+ // Only process MIDI streams in this thread, not digital sound effects
+ if (pMidiParser) {
+ pMidiParser->onTimer();
+ ticker = (uint16)pMidiParser->getTick();
+ }
+}
+
+void MusicEntry::doFade() {
+ if (fadeTicker)
+ fadeTicker--;
+ else {
+ fadeTicker = fadeTickerStep;
+ volume += fadeStep;
+ if (((fadeStep > 0) && (volume >= fadeTo)) || ((fadeStep < 0) && (volume <= fadeTo))) {
+ volume = fadeTo;
+ fadeStep = 0;
+ fadeCompleted = true;
+ }
+
+ // Only process MIDI streams in this thread, not digital sound effects
+ if (pMidiParser) {
+ pMidiParser->setVolume(volume);
+ }
+
+ fadeSetVolume = true; // set flag so that SoundCommandParser::cmdUpdateCues will set the volume of the stream
+ }
+}
+
+void MusicEntry::setSignal(int newSignal) {
+ // Set the signal directly for newer games, otherwise the sound
+ // object might be deleted already later on (refer to bug #5243)
+ signal = newSignal;
+}
+
+
+void ChannelRemapping::swap(int i, int j) {
+ DeviceChannelUsage t1;
+ int t2;
+ bool t3;
+
+ t1 = _map[i]; _map[i] = _map[j]; _map[j] = t1;
+ t2 = _prio[i]; _prio[i] = _prio[j]; _prio[j] = t2;
+ t2 = _voices[i]; _voices[i] = _voices[j]; _voices[j] = t2;
+ t3 = _dontRemap[i]; _dontRemap[i] = _dontRemap[j]; _dontRemap[j] = t3;
+}
+
+void ChannelRemapping::evict(int i) {
+ _freeVoices += _voices[i];
+
+ _map[i]._song = nullptr;
+ _map[i]._channel = -1;
+ _prio[i] = 0;
+ _voices[i] = 0;
+ _dontRemap[i] = false;
+}
+
+void ChannelRemapping::clear() {
+ for (int i = 0; i < 16; ++i) {
+ _map[i]._song = nullptr;
+ _map[i]._channel = -1;
+ _prio[i] = 0;
+ _voices[i] = 0;
+ _dontRemap[i] = false;
+ }
+}
+
+ChannelRemapping& ChannelRemapping::operator=(ChannelRemapping& other) {
+ for (int i = 0; i < 16; ++i) {
+ _map[i] = other._map[i];
+ _prio[i] = other._prio[i];
+ _voices[i] = other._voices[i];
+ _dontRemap[i] = other._dontRemap[i];
+ }
+ _freeVoices = other._freeVoices;
+
+ return *this;
+}
+
+int ChannelRemapping::lowestPrio() const {
+ int max = 0;
+ int channel = -1;
+ for (int i = 0; i < 16; ++i) {
+ if (_prio[i] > max) {
+ max = _prio[i];
+ channel = i;
+ }
+ }
+ return channel;
+}
+
+
+void SciMusic::remapChannels(bool mainThread) {
+ // NB: This function should only be called with _mutex locked
+ // Make sure to set the mainThread argument correctly.
+ ChannelRemapping *map = determineChannelMap();
+
+ DeviceChannelUsage currentMap[16];
+
+#ifdef DEBUG_REMAP
+ debug("Remap results:");
+#endif
+
+ // Save current map, and then start from an empty map
+ for (int i = 0; i < 16; ++i) {
+ currentMap[i] = _channelMap[i];
+ _channelMap[i]._song = nullptr;
+ _channelMap[i]._channel = -1;
+ }
+
+ // Inform MidiParsers of any unmapped channels
+ const MusicList::iterator end = _playList.end();
+ int songIndex = -1;
+ for (MusicList::iterator i = _playList.begin(); i != end; ++i) {
+ MusicEntry *song = *i;
+ songIndex++;
+
+ if (!song || !song->pMidiParser)
+ continue;
+
+ bool channelMapped[16];
+#ifdef DEBUG_REMAP
+ bool channelUsed[16];
+#endif
+ for (int j = 0; j < 16; ++j) {
+ channelMapped[j] = false;
+#ifdef DEBUG_REMAP
+ channelUsed[j] = false;
+#endif
+ }
+
+ for (int j = 0; j < 16; ++j) {
+ if (map->_map[j]._song == song) {
+ int channel = map->_map[j]._channel;
+ assert(channel >= 0 && channel <= 0x0F);
+ channelMapped[channel] = true;
+ }
+#ifdef DEBUG_REMAP
+ if (song->_usedChannels[j] <= 0x0F)
+ channelUsed[song->_usedChannels[j]] = true;
+#endif
+ }
+
+ for (int j = 0; j < 16; ++j) {
+ if (!channelMapped[j]) {
+ if (mainThread) song->pMidiParser->mainThreadBegin();
+ song->pMidiParser->remapChannel(j, -1);
+ if (mainThread) song->pMidiParser->mainThreadEnd();
+#ifdef DEBUG_REMAP
+ if (channelUsed[j])
+ debug(" Unmapping song %d, channel %d", songIndex, j);
+#endif
+ (void)songIndex;
+ }
+ }
+ }
+
+ // Now reshuffle the channels on the device.
+
+ // First, set up any dontRemap channels
+ for (int i = 0; i < 16; ++i) {
+
+ if (!map->_map[i]._song || !map->_map[i]._song->pMidiParser || !map->_dontRemap[i])
+ continue;
+
+ songIndex = -1;
+ for (MusicList::iterator iter = _playList.begin(); iter != end; ++iter) {
+ songIndex++;
+ if (map->_map[i]._song == *iter)
+ break;
+ }
+
+ _channelMap[i] = map->_map[i];
+ map->_map[i]._song = nullptr; // mark as done
+
+ // If this channel was not yet mapped to the device, reset it
+ if (currentMap[i] != _channelMap[i]) {
+#ifdef DEBUG_REMAP
+ debug(" Mapping (dontRemap) song %d, channel %d to device channel %d", songIndex, _channelMap[i]._channel, i);
+#endif
+ resetDeviceChannel(i, mainThread);
+ if (mainThread) _channelMap[i]._song->pMidiParser->mainThreadBegin();
+ _channelMap[i]._song->pMidiParser->remapChannel(_channelMap[i]._channel, i);
+ if (mainThread) _channelMap[i]._song->pMidiParser->mainThreadEnd();
+ }
+
+ }
+
+ // Next, we look for channels which were already playing.
+ // We keep those on the same device channel as before.
+ for (int i = 0; i < 16; ++i) {
+
+ if (!map->_map[i]._song)
+ continue;
+
+ songIndex = -1;
+ for (MusicList::iterator iter = _playList.begin(); iter != end; ++iter) {
+ songIndex++;
+ if (map->_map[i]._song == *iter)
+ break;
+ }
+
+
+ for (int j = 0; j < 16; ++j) {
+ if (map->_map[i] == currentMap[j]) {
+ // found it
+ _channelMap[j] = map->_map[i];
+ map->_map[i]._song = nullptr; // mark as done
+#ifdef DEBUG_REMAP
+ debug(" Keeping song %d, channel %d on device channel %d", songIndex, _channelMap[j]._channel, j);
+#endif
+ break;
+ }
+ }
+ }
+
+ // Then, remap the rest.
+ for (int i = 0; i < 16; ++i) {
+
+ if (!map->_map[i]._song || !map->_map[i]._song->pMidiParser)
+ continue;
+
+ songIndex = -1;
+ for (MusicList::iterator iter = _playList.begin(); iter != end; ++iter) {
+ songIndex++;
+ if (map->_map[i]._song == *iter)
+ break;
+ }
+
+ for (int j = _driverLastChannel; j >= _driverFirstChannel; --j) {
+ if (_channelMap[j]._song == nullptr) {
+ _channelMap[j] = map->_map[i];
+ map->_map[i]._song = nullptr;
+#ifdef DEBUG_REMAP
+ debug(" Mapping song %d, channel %d to device channel %d", songIndex, _channelMap[j]._channel, j);
+#endif
+ if (mainThread) _channelMap[j]._song->pMidiParser->mainThreadBegin();
+ _channelMap[j]._song->pMidiParser->remapChannel(_channelMap[j]._channel, j);
+ if (mainThread) _channelMap[j]._song->pMidiParser->mainThreadEnd();
+ break;
+ }
+ }
+
+ }
+
+ // And finally, stop any empty channels
+ for (int i = _driverLastChannel; i >= _driverFirstChannel; --i) {
+ if (!_channelMap[i]._song && currentMap[i]._song)
+ resetDeviceChannel(i, mainThread);
+ }
+
+ delete map;
+}
+
+
+ChannelRemapping *SciMusic::determineChannelMap() {
+#ifdef DEBUG_REMAP
+ debug("Remap: avail chans: %d-%d", _driverFirstChannel, _driverLastChannel);
+#endif
+
+ ChannelRemapping *map = new ChannelRemapping;
+ ChannelRemapping backupMap;
+ map->clear();
+ map->_freeVoices = _pMidiDrv->getPolyphony();
+
+ if (_playList.empty())
+ return map;
+
+ // Set reverb, either from first song, or from global (verified with KQ5 floppy
+ // and LSL6 interpreters, fixes bug # 11683 ("QFG2 - Heavy reverb from city street sounds...").
+ int8 reverb = _playList.front()->reverb;
+ _pMidiDrv->setReverb(reverb == 127 ? _globalReverb : reverb);
+
+ MusicList::iterator songIter;
+ int songIndex = -1;
+ for (songIter = _playList.begin(); songIter != _playList.end(); ++songIter) {
+ songIndex++;
+ MusicEntry *song = *songIter;
+ if (song->status != kSoundPlaying)
+ continue;
+
+ // If song is digital, skip.
+ // CHECKME: Is this condition correct?
+ if (!song->pMidiParser) {
+#ifdef DEBUG_REMAP
+ debug(" Song %d (%p), digital?", songIndex, (void*)song);
+#endif
+ continue;
+ }
+
+
+#ifdef DEBUG_REMAP
+ const char* name = g_sci->getEngineState()->_segMan->getObjectName(song->soundObj);
+ debug(" Song %d (%p) [%s], prio %d%s", songIndex, (void*)song, name, song->priority, song->playBed ? ", bed" : "");
+#endif
+
+ // Store backup. If we fail to map this song, we will revert to this.
+ backupMap = *map;
+
+ bool songMapped = true;
+
+ for (int i = 0; i < 16; ++i) {
+ int c = song->_usedChannels[i];
+ if (c == 0xFF || c == 0xFE || c == 0x0F)
+ continue;
+ const MusicEntryChannel &channel = song->_chan[c];
+ if (channel._dontMap) {
+#ifdef DEBUG_REMAP
+ debug(" Channel %d dontMap, skipping", c);
+#endif
+ continue;
+ }
+ if (channel._mute) {
+#ifdef DEBUG_REMAP
+ debug(" Channel %d muted, skipping", c);
+#endif
+ continue;
+ }
+
+ bool dontRemap = channel._dontRemap || song->playBed;
+
+#ifdef DEBUG_REMAP
+ debug(" Channel %d: prio %d, %d voice%s%s", c, channel._prio, channel._voices, channel._voices == 1 ? "" : "s", dontRemap ? ", dontRemap" : "" );
+#endif
+
+ DeviceChannelUsage dc = { song, c };
+
+ // our target
+ int devChannel = -1;
+
+ if (dontRemap && map->_map[c]._song == nullptr) {
+ // unremappable channel, with channel still free
+ devChannel = c;
+ }
+
+ // try to find a free channel
+ if (devChannel == -1) {
+ for (int j = 0; j < 16; ++j) {
+ if (map->_map[j] == dc) {
+ // already mapped?! (Can this happen?)
+ devChannel = j;
+ break;
+ }
+ if (map->_map[j]._song)
+ continue;
+
+ if (j >= _driverFirstChannel && j <= _driverLastChannel)
+ devChannel = j;
+ }
+ }
+
+ int prio = channel._prio;
+ if (prio > 0) {
+ // prio > 0 means non-essential
+ prio = (16 - prio) + 16*songIndex;
+ }
+
+ if (devChannel == -1 && prio > 0) {
+ // no empty channel, but this isn't an essential channel,
+ // so we just skip it.
+#ifdef DEBUG_REMAP
+ debug(" skipping non-essential");
+#endif
+ continue;
+ }
+
+ // try to empty a previous channel if this is an essential channel
+ if (devChannel == -1) {
+ devChannel = map->lowestPrio();
+ if (devChannel != -1)
+ map->evict(devChannel);
+ }
+
+ if (devChannel == -1) {
+ // failed to map this song.
+#ifdef DEBUG_REMAP
+ debug(" no free (or lower priority) channel found");
+#endif
+ songMapped = false;
+ break;
+ }
+
+ if (map->_map[devChannel] == dc) {
+ // already mapped?! (Can this happen?)
+ continue;
+ }
+
+ int neededVoices = channel._voices;
+ // do we have enough free voices?
+ if (map->_freeVoices < neededVoices) {
+ // We only care for essential channels.
+ // Note: In early SCI1 interpreters, a song started by 'playBed'
+ // would not be skipped even if some channels couldn't be
+ // mapped due to voice limits. So, we treat all channels as
+ // non-essential here for playBed songs.
+ if (prio > 0) {
+#ifdef DEBUG_REMAP
+ debug(" not enough voices; need %d, have %d. Skipping this channel.", neededVoices, map->_freeVoices);
+#endif
+ continue;
+ }
+ do {
+ int j = map->lowestPrio();
+ if (j == -1) {
+#ifdef DEBUG_REMAP
+ debug(" not enough voices; need %d, have %d", neededVoices, map->_freeVoices);
+#endif
+ // failed to free enough voices.
+ songMapped = false;
+ break;
+ }
+#ifdef DEBUG_REMAP
+ debug(" creating room for voices; evict %d", j);
+#endif
+ map->evict(j);
+ } while (map->_freeVoices < neededVoices);
+
+ if (!songMapped) {
+ // failed to map this song.
+ break;
+ }
+ }
+
+ // We have a channel and enough free voices now.
+#ifdef DEBUG_REMAP
+ debug(" trying to map to %d", devChannel);
+#endif
+
+ map->_map[devChannel] = dc;
+ map->_voices[devChannel] = neededVoices;
+ map->_prio[devChannel] = prio;
+ map->_dontRemap[devChannel] = dontRemap;
+ map->_freeVoices -= neededVoices;
+
+ if (!dontRemap || devChannel == c) {
+ // If this channel fits here, we're done.
+#ifdef DEBUG_REMAP
+ debug(" OK");
+#endif
+ continue;
+ }
+
+ // If this channel can't be remapped, we need to move it or fail.
+
+ if (!map->_dontRemap[c]) {
+ // Target channel can be remapped, so just swap
+ map->swap(devChannel, c);
+ continue;
+ }
+#ifdef DEBUG_REMAP
+ debug(" but %d is already dontRemap", c);
+#endif
+
+ if (prio > 0) {
+ // Channel collision, but this channel is non-essential,
+ // so drop it.
+ // TODO: Maybe we should have checked this before making room?
+ map->evict(devChannel);
+ continue;
+ }
+
+ if (map->_prio[c] > 0) {
+ // Channel collision, but the other channel is non-essential,
+ // so we take its place.
+ map->evict(c);
+ map->swap(devChannel, c);
+ continue;
+ }
+
+ // Otherwise, we have two essential channels claiming the same
+ // device channel.
+ songMapped = false;
+ break;
+ }
+
+ if (!songMapped) {
+ // We failed to map this song, so unmap all its channels.
+#ifdef DEBUG_REMAP
+ debug(" Failed song");
+#endif
+ *map = backupMap;
+ }
+ }
+
+ return map;
+}
+
+bool SciMusic::isDeviceChannelMapped(int devChannel) const {
+ return _channelMap[devChannel]._song;
+}
+
+void SciMusic::resetDeviceChannel(int devChannel, bool mainThread) {
+ assert(devChannel >= 0 && devChannel <= 0x0F);
+
+ if (mainThread) {
+ putMidiCommandInQueue(0x0040B0 | devChannel); // sustain off
+ putMidiCommandInQueue(0x007BB0 | devChannel); // notes off
+ putMidiCommandInQueue(0x004BB0 | devChannel); // release voices
+ } else {
+ _pMidiDrv->send(0x0040B0 | devChannel); // sustain off
+ _pMidiDrv->send(0x007BB0 | devChannel); // notes off
+ _pMidiDrv->send(0x004BB0 | devChannel); // release voices
+ }
+}
+
+bool SciMusic::isDigitalSamplePlaying() const {
+ return _currentlyPlayingSample != nullptr &&
+ _pMixer->isSoundHandleActive(_currentlyPlayingSample->hCurrentAud);
+}
+
+} // End of namespace Dgds
diff --git a/engines/dgds/sound/music.h b/engines/dgds/sound/music.h
new file mode 100644
index 00000000000..0b9902d1561
--- /dev/null
+++ b/engines/dgds/sound/music.h
@@ -0,0 +1,304 @@
+/* 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 3 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DGDS_SOUND_MUSIC_H
+#define DGDS_SOUND_MUSIC_H
+
+#include "common/serializer.h"
+#include "common/mutex.h"
+
+#include "audio/mixer.h"
+
+#include "dgds/sound/drivers/mididriver.h"
+#include "dgds/sound/resource/sci_resource.h"
+
+namespace Audio {
+class LoopingAudioStream;
+class RewindableAudioStream;
+}
+
+namespace Dgds {
+
+enum SoundStatus {
+ kSoundStopped = 0,
+ kSoundInitialized = 1,
+ kSoundPaused = 2,
+ kSoundPlaying = 3
+};
+
+#define MUSIC_VOLUME_DEFAULT 127
+#define MUSIC_VOLUME_MAX 127
+#define MUSIC_MASTERVOLUME_DEFAULT 15
+#define MUSIC_MASTERVOLUME_MAX 15
+
+class MidiParser_SCI;
+
+typedef Common::Array<uint16> SignalQueue;
+
+
+struct MusicEntryChannel {
+ // Channel info
+ int8 _prio; // 0 = essential; lower is higher priority
+ int8 _voices;
+ bool _dontRemap;
+ bool _dontMap;
+ uint8 _mute;
+};
+
+
+class MusicEntry {
+public:
+ // Do not get these directly for the sound objects!
+ // It's a bad idea, as the sound code (i.e. the SciMusic
+ // class) should be as separate as possible from the rest
+ // of the engine
+ uint32 soundObj;
+
+ SoundResource *soundRes;
+ int32 resourceId;
+
+ int time; // "tim"estamp to indicate in which order songs have been added
+
+ uint16 dataInc;
+ uint16 ticker;
+ uint16 signal;
+ int16 priority; // must be int16, at least in Laura Bow 1, main music (object conMusic) uses priority -1
Commit: 3f7f33f1fc187ed466e76231e8f34785b0408b9c
https://github.com/scummvm/scummvm/commit/3f7f33f1fc187ed466e76231e8f34785b0408b9c
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Make midi code a bit closer to SCI version
Changed paths:
engines/dgds/sound/drivers/adlib.cpp
engines/dgds/sound/drivers/midi.cpp
engines/dgds/sound/drivers/mididriver.h
engines/dgds/sound/midiparser_sci.cpp
engines/dgds/sound/music.cpp
diff --git a/engines/dgds/sound/drivers/adlib.cpp b/engines/dgds/sound/drivers/adlib.cpp
index 5b8a95cc9e8..b69705012e3 100644
--- a/engines/dgds/sound/drivers/adlib.cpp
+++ b/engines/dgds/sound/drivers/adlib.cpp
@@ -57,7 +57,6 @@ public:
int openAdLib();
void close() override;
void send(uint32 b) override;
- void initTrack(SciSpan<const byte> &header);
MidiChannel *allocateChannel() override { return nullptr; }
MidiChannel *getPercussionChannel() override { return nullptr; }
@@ -190,7 +189,6 @@ public:
bool hasRhythmChannel() const override { return false; }
void setVolume(byte volume) override { static_cast<MidiDriver_AdLib *>(_driver)->setVolume(volume); }
void playSwitch(bool play) override { static_cast<MidiDriver_AdLib *>(_driver)->playSwitch(play); }
- void initTrack(SciSpan<const byte> &header) override { static_cast<MidiDriver_AdLib *>(_driver)->initTrack(header); }
int getLastChannel() const override { return (static_cast<const MidiDriver_AdLib *>(_driver)->useRhythmChannel() ? 8 : 15); }
};
@@ -346,46 +344,6 @@ void MidiDriver_AdLib::send(uint32 b) {
}
}
-void MidiDriver_AdLib::initTrack(SciSpan<const byte> &header) {
- if (!_isOpen || !_isSCI0)
- return;
-
- uint8 readPos = 0;
- uint8 caps = header.getInt8At(readPos++);
- if (caps != 0 && caps != 2)
- return;
-
- for (int i = 0; i < kVoices; ++i) {
- _voices[i].channel = _voices[i].mappedChannel = _voices[i].note = -1;
- _voices[i].isSustained = false;
- _voices[i].patch = 13;
- _voices[i].velocity = 0;
- _voices[i].age = 0;
- }
-
- int numVoices = 0;
- for (int i = 0; i < 16; ++i) {
- _channels[i].patch = 13;
- _channels[i].extraVoices = 0;
- _channels[i].mappedVoices = 0;
-
- uint8 val = header.getInt8At(readPos++);
- if (val & 0x01) {
- uint8 num = val >> 4;
- if (!(val & 0x08) && num && num != 0x0F) {
- while (num--) {
- if (numVoices >= _numVoiceMax)
- continue;
- _voices[numVoices++].mappedChannel = i;
- _channels[i].mappedVoices++;
- }
- }
- } else if (val & 0x08) {
- debugC(9, kDebugLevelSound, "MidiDriver_AdLib::initTrack(): Control channel found: 0x%.02x", i);
- }
- }
-}
-
void MidiDriver_AdLib::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) {
_adlibTimerProc = timerProc;
_adlibTimerParam = timerParam;
diff --git a/engines/dgds/sound/drivers/midi.cpp b/engines/dgds/sound/drivers/midi.cpp
index 5f710c652f0..372c791fe77 100644
--- a/engines/dgds/sound/drivers/midi.cpp
+++ b/engines/dgds/sound/drivers/midi.cpp
@@ -69,7 +69,6 @@ public:
int getVolume() override;
void setReverb(int8 reverb) override;
void playSwitch(bool play) override;
- void initTrack(SciSpan<const byte> &) override;
private:
bool isMt32GmPatch(const SciSpan<const byte> &data);
@@ -423,10 +422,6 @@ void MidiPlayer_Midi::playSwitch(bool play) {
}
}
-void MidiPlayer_Midi::initTrack(SciSpan<const byte> &header) {
- return;
-}
-
bool MidiPlayer_Midi::isMt32GmPatch(const SciSpan<const byte> &data) {
uint32 size = data.size();
diff --git a/engines/dgds/sound/drivers/mididriver.h b/engines/dgds/sound/drivers/mididriver.h
index 4f21a81d023..68228fed2b8 100644
--- a/engines/dgds/sound/drivers/mididriver.h
+++ b/engines/dgds/sound/drivers/mididriver.h
@@ -119,13 +119,6 @@ public:
_driver->send(0xb0 + i, SCI_MIDI_CHANNEL_NOTES_OFF, 0);
}
}
-
- // Prepares the driver for the playback of SCI0 midi tracks.
- // The main purpose is the assignment of voices ("hardware" sound channels) to the 16 midi parts.
- // This is basically the predecessor of the 0x4B midi event.
- // Some drivers also do other things in here.
- virtual void initTrack(SciSpan<const byte> &) {}
-
};
class SciResource;
diff --git a/engines/dgds/sound/midiparser_sci.cpp b/engines/dgds/sound/midiparser_sci.cpp
index c188d326645..5ca037e7967 100644
--- a/engines/dgds/sound/midiparser_sci.cpp
+++ b/engines/dgds/sound/midiparser_sci.cpp
@@ -45,11 +45,13 @@ MidiParser_SCI::MidiParser_SCI(SciMusic *music) :
_ppqn = 1;
setTempo(16667);
+ _track = nullptr;
+ _pSnd = nullptr;
+ _loopTick = 0;
_masterVolume = 15;
_volume = 127;
_resetOnPause = false;
- _pSnd = nullptr;
_mainThreadCalled = false;
@@ -313,7 +315,7 @@ void MidiParser_SCI::sendFromScriptToDriver(uint32 midi) {
return;
}
- if ((midi & 0xFFF0) == 0x4EB0) {
+ if ((midi & 0xFFF0) == 0x4EB0 && DgdsEngine::getInstance()->getGameId() != GID_DRAGON) {
// We have to handle this here instead of inside the trackState() method (which handles the input from
// the actual midi data). The mute command when sent from the script is independent from the mute
// command sent by the actual midi data. The script mute is stacked on the high nibble, while the midi
@@ -331,7 +333,7 @@ void MidiParser_SCI::sendFromScriptToDriver(uint32 midi) {
if (_pSnd->_chan[channel]._mute != m) {
// CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
_music->needsRemap();
- debugC(2, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
+ debugC(2, kDebugLevelSound, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
}
return;
@@ -347,7 +349,7 @@ void MidiParser_SCI::sendToDriver(uint32 midi) {
if (!_pSnd->_chan[midiChannel]._dontMap)
trackState(midi);
- if ((midi & 0xFFF0) == 0x4EB0) {
+ if ((midi & 0xFFF0) == 0x4EB0 && DgdsEngine::getInstance()->getGameId() != GID_DRAGON) {
// Mute. Handled in trackState()/sendFromScriptToDriver().
return;
}
@@ -427,27 +429,27 @@ void MidiParser_SCI::trackState(uint32 b) {
case 0x4B: // voices
if (s._voices != op2) {
// CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
- debugC(2, "Dynamic voice change (%d to %d)", s._voices, op2);
+ debugC(2, kDebugLevelSound, "Dynamic voice change (%d to %d)", s._voices, op2);
_music->needsRemap();
}
s._voices = op2;
_pSnd->_chan[channel]._voices = op2; // Also sync our MusicEntry
break;
- case 0x4E: {// mute
- // This is channel mute only for sci1.
-
- // This is handled slightly differently than what we do in sendFromScriptToDriver(). The script mute is stacked
- // on the high nibble, while the midi data mute (this one here) is stored on the low nibble. So the script cannot
- // undo a mute set by the midi data and vice versa.
- uint8 m = (_pSnd->_chan[channel]._mute & 0xf0) | (op2 & 1);
- if (_pSnd->_chan[channel]._mute != m) {
- _pSnd->_chan[channel]._mute = m;
- // CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
- _music->needsRemap();
- debugC(2, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
+ case 0x4E: // mute
+ // This is channel mute only for HOC+.
+ if (DgdsEngine::getInstance()->getGameId() != GID_DRAGON) {
+ // This is handled slightly differently than what we do in sendFromScriptToDriver(). The script mute is stacked
+ // on the high nibble, while the midi data mute (this one here) is stored on the low nibble. So the script cannot
+ // undo a mute set by the midi data and vice versa.
+ uint8 m = (_pSnd->_chan[channel]._mute & 0xf0) | (op2 & 1);
+ if (_pSnd->_chan[channel]._mute != m) {
+ _pSnd->_chan[channel]._mute = m;
+ // CHECKME: Should we directly call remapChannels() if _mainThreadCalled?
+ _music->needsRemap();
+ debugC(2, kDebugLevelSound, "Dynamic mute change (arg = %d, mainThread = %d)", m, _mainThreadCalled);
+ }
}
break;
- }
default:
break;
}
@@ -588,7 +590,7 @@ bool MidiParser_SCI::processEvent(const EventInfo &info, bool fireEvents) {
if (!skipSignal) {
if (!_jumpingToTick) {
_pSnd->setSignal(info.basic.param1);
- debugC(4, "signal %04x", info.basic.param1);
+ debugC(4, kDebugLevelSound, "signal %04x", info.basic.param1);
}
}
@@ -689,7 +691,7 @@ bool MidiParser_SCI::processEvent(const EventInfo &info, bool fireEvents) {
} else {
_pSnd->setSignal(0xffff); // SIGNAL_OFFSET
- debugC(4, "signal EOT");
+ debugC(4, kDebugLevelSound, "signal EOT");
}
}
@@ -770,7 +772,6 @@ void MidiParser_SCI::setVolume(byte volume) {
for (int i = 0; i < 15; i++)
if (_channelRemap[i] != -1)
sendToDriver(0xB0 + i, 7, _channelVolume[i]);
-
}
void MidiParser_SCI::remapChannel(int channel, int devChannel) {
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
index 60298665f6d..8af17319429 100644
--- a/engines/dgds/sound/music.cpp
+++ b/engines/dgds/sound/music.cpp
@@ -475,6 +475,23 @@ void SciMusic::soundInitSnd(MusicEntry *pSnd) {
void SciMusic::soundPlay(MusicEntry *pSnd, bool restoring) {
_mutex.lock();
+ if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON && pSnd->playBed) {
+ // If pSnd->playBed, and version <= SCI1_EARLY, then kill
+ // existing sounds with playBed enabled.
+
+ uint playListCount = _playList.size();
+ for (uint i = 0; i < playListCount; i++) {
+ if (_playList[i] != pSnd && _playList[i]->playBed) {
+ debugC(2, kDebugLevelSound, "Automatically stopping old playBed song from soundPlay");
+ MusicEntry *old = _playList[i];
+ _mutex.unlock();
+ soundStop(old);
+ _mutex.lock();
+ break;
+ }
+ }
+ }
+
uint playListCount = _playList.size();
uint playListNo = playListCount;
MusicEntry *alreadyPlaying = nullptr;
@@ -846,6 +863,8 @@ MusicEntry::MusicEntry() {
volume = MUSIC_VOLUME_DEFAULT;
hold = -1;
reverb = -1;
+ playBed = false;
+ overridePriority = false;
pauseCounter = 0;
sampleLoopCounter = 0;
@@ -872,6 +891,7 @@ MusicEntry::MusicEntry() {
_chan[i]._prio = 127;
_chan[i]._voices = 0;
_chan[i]._dontRemap = false;
+ _chan[i]._dontMap = false;
_chan[i]._mute = false;
}
}
@@ -989,6 +1009,8 @@ int ChannelRemapping::lowestPrio() const {
void SciMusic::remapChannels(bool mainThread) {
// NB: This function should only be called with _mutex locked
// Make sure to set the mainThread argument correctly.
+
+
ChannelRemapping *map = determineChannelMap();
DeviceChannelUsage currentMap[16];
@@ -1289,7 +1311,7 @@ ChannelRemapping *SciMusic::determineChannelMap() {
// would not be skipped even if some channels couldn't be
// mapped due to voice limits. So, we treat all channels as
// non-essential here for playBed songs.
- if (prio > 0) {
+ if (prio > 0 || (song->playBed && DgdsEngine::getInstance()->getGameId() == GID_DRAGON)) {
#ifdef DEBUG_REMAP
debug(" not enough voices; need %d, have %d. Skipping this channel.", neededVoices, map->_freeVoices);
#endif
Commit: 40f7744f37b6ff04a7ec9fbd6704fb850a068fbe
https://github.com/scummvm/scummvm/commit/40f7744f37b6ff04a7ec9fbd6704fb850a068fbe
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Fix SFX mapping in RoTD and HoC
Rise of the Dragon uses a fixed offset on SFX ids. HoC seems to do the same
although I haven't fully reverse-engineered that bit.
Thanks to @NMIError for the help debugging this issue.
Changed paths:
engines/dgds/module.mk
engines/dgds/sound.cpp
engines/dgds/sound.h
engines/dgds/sound/drivers/adlib.cpp
engines/dgds/sound/drivers/cms.cpp
engines/dgds/sound/drivers/mididriver.h
engines/dgds/sound/music.cpp
engines/dgds/ttm.cpp
diff --git a/engines/dgds/module.mk b/engines/dgds/module.mk
index 5e163c2e9b5..302680c9159 100644
--- a/engines/dgds/module.mk
+++ b/engines/dgds/module.mk
@@ -31,6 +31,7 @@ MODULE_OBJS := \
sound/music.o \
sound/drivers/adlib.o \
sound/drivers/amigamac1.o \
+ sound/drivers/cms.o \
sound/drivers/midi.o \
sound/drivers/midipatch.o \
sound/resource/sci_resource.o \
diff --git a/engines/dgds/sound.cpp b/engines/dgds/sound.cpp
index f5df4a8d63e..e91c431ff9f 100644
--- a/engines/dgds/sound.cpp
+++ b/engines/dgds/sound.cpp
@@ -112,6 +112,7 @@ static uint32 _availableSndTracks(const byte *data, uint32 size) {
break;
case 9:
//debug("- CMS");
+ tracks |= TRACK_CMS;
break;
case 12:
//debug("- MT-32");
@@ -119,9 +120,11 @@ static uint32 _availableSndTracks(const byte *data, uint32 size) {
break;
case 18:
//debug("- PC Speaker");
+ tracks |= TRACK_PCSPK;
break;
case 19:
//debug("- Tandy 1000");
+ tracks |= TRACK_TANDY;
break;
default:
//debug("- Unknown %d", drv);
@@ -347,6 +350,7 @@ void Sound::loadMacMusic(const Common::String &filename) {
void Sound::loadMusic(const Common::String &filename) {
unloadMusic();
loadPCSound(filename, _musicSizes, _musicData);
+ debug("Sound: Loaded music %s with %d entries", filename.c_str(), _musicData.size());
}
void Sound::loadSFX(const Common::String &filename) {
@@ -394,14 +398,24 @@ void Sound::loadPCSound(const Common::String &filename, Common::Array<uint32> &s
delete musicStream;
}
-void Sound::playSFX(uint num) {
- playPCSound(num, _sfxSizes, _sfxData, Audio::Mixer::kSFXSoundType);
+int Sound::mapSfxNum(int num) const {
+ // Fixed offset in Dragon and HoC?
+ if (DgdsEngine::getInstance()->getGameId() == GID_DRAGON || DgdsEngine::getInstance()->getGameId() == GID_HOC)
+ return num - 24;
+ return num;
}
-void Sound::stopSfxByNum(uint num) {
- MusicEntry *musicSlot = _music->getSlot(num + SND_RESOURCE_OFFSET);
+void Sound::playSFX(int num) {
+ int mappedNum = mapSfxNum(num);
+ playPCSound(mappedNum, _sfxSizes, _sfxData, Audio::Mixer::kSFXSoundType);
+}
+
+void Sound::stopSfxByNum(int num) {
+ int mappedNum = mapSfxNum(num);
+
+ MusicEntry *musicSlot = _music->getSlot(mappedNum + SND_RESOURCE_OFFSET);
if (!musicSlot) {
- warning("stopSfxByNum: Slot not found (%08x)", num);
+ debug("stopSfxByNum: Slot for sfx num %d not found.", mappedNum);
return;
}
@@ -410,28 +424,29 @@ void Sound::stopSfxByNum(uint num) {
_music->soundStop(musicSlot);
}
-void Sound::playMusic(uint num) {
+void Sound::playMusic(int num) {
+ debug("Sound: Play music %d of (%d entries)", num, _musicData.size());
playPCSound(num, _musicSizes, _musicData, Audio::Mixer::kMusicSoundType);
}
-void Sound::processInitSound(uint num, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
+void Sound::processInitSound(uint32 obj, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
// Check if a track with the same sound object is already playing
- MusicEntry *oldSound = _music->getSlot(num);
+ MusicEntry *oldSound = _music->getSlot(obj);
if (oldSound) {
- processDisposeSound(num);
+ processDisposeSound(obj);
}
MusicEntry *newSound = new MusicEntry();
- newSound->resourceId = num;
- newSound->soundObj = num;
+ newSound->resourceId = obj;
+ newSound->soundObj = obj;
newSound->loop = 1; // Default to one loop
newSound->overridePriority = false;
newSound->priority = 255; // TODO: Priority?
newSound->volume = MUSIC_VOLUME_DEFAULT;
newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below
- debugC(kDebugLevelSound, "kDoSound(init): %08x number %d, loop %d, prio %d, vol %d", num,
- num, newSound->loop, newSound->priority, newSound->volume);
+ debugC(kDebugLevelSound, "kDoSound(init): %08x number %d, loop %d, prio %d, vol %d", obj,
+ obj, newSound->loop, newSound->priority, newSound->volume);
initSoundResource(newSound, data, dataSz, soundType);
@@ -533,18 +548,18 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const byt
musicSlot->fadeStep = 0;
}
-void Sound::playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType) {
+void Sound::playPCSound(int num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType) {
if (_musicIdMap.size()) {
num = _musicIdMap[num];
}
- if (num < dataArray.size()) {
- uint32 tracks = _availableSndTracks(dataArray[num], sizeArray[num]);
+ if (num >= 0 && num < (int)dataArray.size()) {
+ int size = sizeArray[num];
+ const byte *data = dataArray[num];
+ uint32 tracks = _availableSndTracks(data, size);
if (tracks & DIGITAL_PCM) {
- playPCM(dataArray[num], sizeArray[num]);
+ playPCM(data, size);
} else {
- int size = sizeArray[num];
- const byte *data = dataArray[num];
uint32 hdrsize = 0;
_readHeader(data, hdrsize);
size -= hdrsize;
diff --git a/engines/dgds/sound.h b/engines/dgds/sound.h
index 900a7731413..4a8a684841d 100644
--- a/engines/dgds/sound.h
+++ b/engines/dgds/sound.h
@@ -51,28 +51,30 @@ public:
void loadMacMusic(const Common::String &filename);
void loadSFX(const Common::String &filename);
- void playMusic(uint num);
+ void playMusic(int num);
void stopMusic();
void unloadMusic();
- void playSFX(uint num);
+ void playSFX(int num);
void stopSfxForChannel(byte channel);
- void stopSfxByNum(uint num);
+ void stopSfxByNum(int num);
void stopAllSfx();
bool playPCM(const byte *data, uint32 size);
private:
void loadPCSound(const Common::String &filename, Common::Array<uint32> &sizeArray, Common::Array<byte *> &dataArray);
- void playPCSound(uint num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType);
+ void playPCSound(int num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType);
- void processInitSound(uint num, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
+ void processInitSound(uint32 obj, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
void processDisposeSound(uint32 obj);
void processStopSound(uint32 obj, bool sampleFinishedPlaying);
void processPlaySound(uint32 obj, bool playBed, bool restoring, const byte *data, int dataSz);
void initSoundResource(MusicEntry *newSound, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
+ int mapSfxNum(int num) const;
+
struct Channel _channels[2];
Common::Array<uint32> _musicSizes;
@@ -93,7 +95,10 @@ enum {
DIGITAL_PCM = 1 << 0,
TRACK_ADLIB = 1 << 1,
TRACK_GM = 1 << 2,
- TRACK_MT32 = 1 << 3
+ TRACK_MT32 = 1 << 3,
+ TRACK_CMS = 1 << 4,
+ TRACK_PCSPK = 1 << 5,
+ TRACK_TANDY = 1 << 6,
};
} // End of namespace Dgds
diff --git a/engines/dgds/sound/drivers/adlib.cpp b/engines/dgds/sound/drivers/adlib.cpp
index b69705012e3..b36f07a7e3a 100644
--- a/engines/dgds/sound/drivers/adlib.cpp
+++ b/engines/dgds/sound/drivers/adlib.cpp
@@ -19,8 +19,6 @@
*
*/
-//#include "dgds/sci.h"
-
#include "common/file.h"
#include "common/system.h"
#include "common/textconsole.h"
diff --git a/engines/dgds/sound/drivers/cms.cpp b/engines/dgds/sound/drivers/cms.cpp
index 7430d9d720f..88bd5665517 100644
--- a/engines/dgds/sound/drivers/cms.cpp
+++ b/engines/dgds/sound/drivers/cms.cpp
@@ -25,7 +25,7 @@
#include "audio/cms.h"
#include "dgds/sound/resource/sci_resource.h"
-#include "dgds/util.h"
+#include "dgds/sound/scispan.h"
namespace Dgds {
@@ -715,8 +715,8 @@ const int CMSVoice_V1::_velocityTable[32] = {
6, 6, 7, 8, 8, 9, 10, 10
};
-MidiDriver_CMS::MidiDriver_CMS(ResourceManager *resMan, SciVersion version) : _resMan(resMan), _isOpen(false),
- _version(version), _cms(nullptr), _playSwitch(true), _masterVolume(0), _numVoicesPrimary(version > SCI_VERSION_0_LATE ? 12 : 8),
+MidiDriver_CMS::MidiDriver_CMS(ResourceManager *resMan) : _resMan(resMan), _isOpen(false),
+ _cms(nullptr), _playSwitch(true), _masterVolume(0), _numVoicesPrimary(12),
_timerProc(nullptr), _timerParam(nullptr), _actualTimerInterval(1000000 / CMS::CMS::DEFAULT_CALLBACK_FREQUENCY), _reqTimerInterval(1000000/60),
_numVoicesSecondary(0) {
memset(_voice, 0, sizeof(_voice));
@@ -733,7 +733,7 @@ int MidiDriver_CMS::open() {
return MERR_ALREADY_OPEN;
assert(_resMan);
- Resource *res = _resMan->findResource(ResourceId(kResourceTypePatch, 101), false);
+ SciResource *res = getMidiPatchData(101);
if (!res)
return -1;
@@ -1233,13 +1233,11 @@ void MidiDriver_CMS::writeToChip(int chip, int address, int data) {
class MidiPlayer_CMS : public MidiPlayer {
public:
- MidiPlayer_CMS(SciVersion version) : MidiPlayer(version), _filesMissing(false) {}
+ MidiPlayer_CMS() : MidiPlayer() {}
int open(ResourceManager *resMan) override;
void close() override;
- void initTrack(SciSpan<const byte>& header) override;
-
bool hasRhythmChannel() const override { return false; }
byte getPlayId() const override { return 9; }
int getPolyphony() const override { return 12; }
@@ -1251,7 +1249,7 @@ int MidiPlayer_CMS::open(ResourceManager *resMan) {
if (_driver)
return MidiDriver::MERR_ALREADY_OPEN;
- _driver = new MidiDriver_CMS(resMan, _version);
+ _driver = new MidiDriver_CMS(resMan);
int driverRetVal = _driver->open();
return driverRetVal;
@@ -1264,15 +1262,8 @@ void MidiPlayer_CMS::close() {
_driver = nullptr;
}
-void MidiPlayer_CMS::initTrack(SciSpan<const byte>& header) {
- if (_driver)
- static_cast<MidiDriver_CMS*>(_driver)->initTrack(header);
-}
-
-const char MidiPlayer_CMS::_requiredFiles[] = "'PATCH.101'";
-
-MidiPlayer *MidiPlayer_CMS_create(SciVersion version) {
- return new MidiPlayer_CMS(version);
+MidiPlayer *MidiPlayer_CMS_create() {
+ return new MidiPlayer_CMS();
}
} // End of namespace SCI
diff --git a/engines/dgds/sound/drivers/mididriver.h b/engines/dgds/sound/drivers/mididriver.h
index 68228fed2b8..3043b82c408 100644
--- a/engines/dgds/sound/drivers/mididriver.h
+++ b/engines/dgds/sound/drivers/mididriver.h
@@ -127,6 +127,7 @@ extern SciResource *getMidiPatchData(int num);
extern MidiPlayer *MidiPlayer_AdLib_create();
extern MidiPlayer *MidiPlayer_AmigaMac1_create(Common::Platform platform);
+extern MidiPlayer *MidiPlayer_CMS_create();
extern MidiPlayer *MidiPlayer_PCJr_create();
extern MidiPlayer *MidiPlayer_PCSpeaker_create();
extern MidiPlayer *MidiPlayer_Midi_create();
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
index 8af17319429..94402337ddd 100644
--- a/engines/dgds/sound/music.cpp
+++ b/engines/dgds/sound/music.cpp
@@ -88,6 +88,9 @@ void SciMusic::init() {
error("TODO: Implement pc speaker driver?");
//_pMidiDrv = MidiPlayer_PCSpeaker_create();
break;
+ case MT_CMS:
+ _pMidiDrv = MidiPlayer_CMS_create();
+ break;
default:
_pMidiDrv = MidiPlayer_Midi_create();
}
@@ -391,7 +394,7 @@ void SciMusic::soundInitSnd(MusicEntry *pSnd) {
} else {
// play MIDI track
Common::StackLock lock(_mutex);
- pSnd->soundType = Audio::Mixer::kMusicSoundType;
+ //pSnd->soundType = Audio::Mixer::kMusicSoundType; // set on init for DGDS
if (pSnd->pMidiParser == nullptr) {
pSnd->pMidiParser = new MidiParser_SCI(this);
pSnd->pMidiParser->setMidiDriver(_pMidiDrv);
diff --git a/engines/dgds/ttm.cpp b/engines/dgds/ttm.cpp
index d1e2d236c50..63618546bf6 100644
--- a/engines/dgds/ttm.cpp
+++ b/engines/dgds/ttm.cpp
@@ -316,8 +316,8 @@ static void _dissolveToScreen(const Graphics::ManagedSurface &src, const Common:
static void _doScroll(Graphics::ManagedSurface &compBuf, int16 dir, int16 steps, int16 offset) {
// Scroll the contents of the composition buffer on to the screen
- // Dir 0/1 means y (scroll toward bottom / top)
- // Dir 2 means x (scroll toward right)
+ // Dir 0/1 means y (scroll camera toward bottom / top)
+ // Dir 2/3 means x (scroll camera toward right / left)
//
// This is not at all how the original does it, but we have a bit
// more memory and cpu to play with so an extra 64k screen buffer
@@ -1088,7 +1088,6 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
if (seq._executed) // this is a one-shot op
break;
- _vm->_soundPlayer->stopMusic();
if (_vm->_platform == Common::kPlatformAmiga) {
// TODO: remove hard-coded stuff..
_vm->_soundPlayer->playAmigaSfx("DYNAMIX.INS", 0, 255);
Commit: aaa48d45d411c08dd644184fd6c94ad04cf6c1e4
https://github.com/scummvm/scummvm/commit/aaa48d45d411c08dd644184fd6c94ad04cf6c1e4
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Read and use sfx/music loop flag
Changed paths:
engines/dgds/sound.cpp
engines/dgds/sound.h
engines/dgds/sound/music.cpp
diff --git a/engines/dgds/sound.cpp b/engines/dgds/sound.cpp
index e91c431ff9f..1e24889f66e 100644
--- a/engines/dgds/sound.cpp
+++ b/engines/dgds/sound.cpp
@@ -40,6 +40,9 @@
namespace Dgds {
static const uint16 SIGNAL_OFFSET = 0xffff;
+
+// Offsets from sound/music num to playing ID.
+// This is to make a fake SCI-style "resource ID"
static const int SND_RESOURCE_OFFSET = 4096;
static const int MUSIC_RESOURCE_OFFSET = 8192;
@@ -197,8 +200,8 @@ Sound::Sound(Audio::Mixer *mixer, ResourceManager *resource, Decompressor *decom
Sound::~Sound() {
unloadMusic();
- for (auto *data: _sfxData)
- delete [] data;
+ for (auto &data: _sfxData)
+ delete [] data._data;
}
void Sound::playAmigaSfx(const Common::String &filename, byte channel, byte volume) {
@@ -338,9 +341,9 @@ void Sound::loadMacMusic(const Common::String &filename) {
// TODO: Should we record the indexes?
/*uint16 idx = */ stream->readUint16LE();
/*uint16 type = */ stream->readUint16LE();
- uint32 sz;
- _musicData.push_back(_decompressor->decompress(stream, stream->size() - stream->pos(), sz));
- _musicSizes.push_back(sz);
+ SoundData soundData;
+ soundData._data = _decompressor->decompress(stream, stream->size() - stream->pos(), soundData._size);
+ _musicData.push_back(soundData);
}
}
@@ -349,17 +352,17 @@ void Sound::loadMacMusic(const Common::String &filename) {
void Sound::loadMusic(const Common::String &filename) {
unloadMusic();
- loadPCSound(filename, _musicSizes, _musicData);
+ loadPCSound(filename, _musicData);
debug("Sound: Loaded music %s with %d entries", filename.c_str(), _musicData.size());
}
void Sound::loadSFX(const Common::String &filename) {
- if (_sfxSizes.size())
+ if (_sfxData.size())
error("Sound: SFX data should only be loaded once");
- loadPCSound(filename, _sfxSizes, _sfxData);
+ loadPCSound(filename, _sfxData);
}
-void Sound::loadPCSound(const Common::String &filename, Common::Array<uint32> &sizeArray, Common::Array<byte *> &dataArray) {
+void Sound::loadPCSound(const Common::String &filename, Common::Array<SoundData> &dataArray) {
if (!filename.hasSuffixIgnoreCase(".sng"))
error("Unhandled music file type: %s", filename.c_str());
@@ -378,17 +381,21 @@ void Sound::loadPCSound(const Common::String &filename, Common::Array<uint32> &s
Common::SeekableReadStream *stream = chunk.getContent();
if (chunk.isSection(ID_SNG)) {
- int32 chunkSize = stream->size();
- byte *data = new byte[chunkSize];
- stream->read(data, chunkSize);
- sizeArray.push_back(chunkSize);
- dataArray.push_back(data);
+ SoundData soundData;
+ soundData._size = stream->size();
+ byte *data = new byte[soundData._size];
+ stream->read(data, soundData._size);
+ soundData._data = data;
+ dataArray.push_back(soundData);
} else if (chunk.isSection(ID_INF)) {
uint32 count = stream->size() / 2;
+ if (count > dataArray.size())
+ error("Sound: %s has more flags in INF than SNG entries.", filename.c_str());
debug(" SNG INF [%u entries]", count);
for (uint32 k = 0; k < count; k++) {
- uint16 idx = stream->readUint16LE();
- debug(" %2u: 0x%04x", k, idx);
+ uint16 flags = stream->readUint16LE();
+ debug(" %2u: 0x%04x", k, flags);
+ dataArray[k]._flags = flags;
}
} else {
warning("loadPCSound: skip unused chunk %s in %s", chunk.getIdStr(), filename.c_str());
@@ -407,11 +414,13 @@ int Sound::mapSfxNum(int num) const {
void Sound::playSFX(int num) {
int mappedNum = mapSfxNum(num);
- playPCSound(mappedNum, _sfxSizes, _sfxData, Audio::Mixer::kSFXSoundType);
+ debug("Play SFX %d (-> %d), have %d entries", num, mappedNum, _sfxData.size());
+ playPCSound(mappedNum, _sfxData, Audio::Mixer::kSFXSoundType);
}
void Sound::stopSfxByNum(int num) {
int mappedNum = mapSfxNum(num);
+ debug("Stop SFX %d (-> %d)", num, mappedNum);
MusicEntry *musicSlot = _music->getSlot(mappedNum + SND_RESOURCE_OFFSET);
if (!musicSlot) {
@@ -426,10 +435,10 @@ void Sound::stopSfxByNum(int num) {
void Sound::playMusic(int num) {
debug("Sound: Play music %d of (%d entries)", num, _musicData.size());
- playPCSound(num, _musicSizes, _musicData, Audio::Mixer::kMusicSoundType);
+ playPCSound(num, _musicData, Audio::Mixer::kMusicSoundType);
}
-void Sound::processInitSound(uint32 obj, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
+void Sound::processInitSound(uint32 obj, const SoundData &data, Audio::Mixer::SoundType soundType) {
// Check if a track with the same sound object is already playing
MusicEntry *oldSound = _music->getSlot(obj);
if (oldSound) {
@@ -439,23 +448,27 @@ void Sound::processInitSound(uint32 obj, const byte *data, int dataSz, Audio::Mi
MusicEntry *newSound = new MusicEntry();
newSound->resourceId = obj;
newSound->soundObj = obj;
- newSound->loop = 1; // Default to one loop
+ newSound->loop = 0; // set in playSound
newSound->overridePriority = false;
newSound->priority = 255; // TODO: Priority?
newSound->volume = MUSIC_VOLUME_DEFAULT;
newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below
- debugC(kDebugLevelSound, "kDoSound(init): %08x number %d, loop %d, prio %d, vol %d", obj,
+ debugC(kDebugLevelSound, "processInitSound: %08x number %d, loop %d, prio %d, vol %d", obj,
obj, newSound->loop, newSound->priority, newSound->volume);
- initSoundResource(newSound, data, dataSz, soundType);
+ initSoundResource(newSound, data, soundType);
_music->pushBackSlot(newSound);
}
-void Sound::initSoundResource(MusicEntry *newSound, const byte *data, int dataSz, Audio::Mixer::SoundType soundType) {
+void Sound::initSoundResource(MusicEntry *newSound, const SoundData &data, Audio::Mixer::SoundType soundType) {
if (newSound->resourceId) {
- newSound->soundRes = new SoundResource(newSound->resourceId, data, dataSz);
+ // Skip the header.
+ const byte *dataPtr = data._data;
+ uint32 hdrSize = 0;
+ _readHeader(dataPtr, hdrSize);
+ newSound->soundRes = new SoundResource(newSound->resourceId, dataPtr, data._size - hdrSize);
if (!newSound->soundRes->exists()) {
delete newSound->soundRes;
newSound->soundRes = nullptr;
@@ -496,7 +509,7 @@ void Sound::processStopSound(uint32 obj, bool sampleFinishedPlaying) {
_music->soundStop(musicSlot);
}
-void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const byte *data, int dataSz) {
+void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const SoundData &data) {
// Mostly copied from SCI soundcmd.
MusicEntry *musicSlot = _music->getSlot(obj);
@@ -515,12 +528,12 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const byt
if (musicSlot->resourceId != resourceId) { // another sound loaded into struct
processDisposeSound(obj);
- processInitSound(obj, data, dataSz, Audio::Mixer::kSFXSoundType);
+ processInitSound(obj, data, Audio::Mixer::kSFXSoundType);
// Find slot again :)
musicSlot = _music->getSlot(obj);
}
- musicSlot->loop = 1; // Play once by default?
+ musicSlot->loop = (data._flags & 1) ? -1 : 1;
// Get song priority from either obj or soundRes
byte resourcePriority = 0xFF;
@@ -548,24 +561,20 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const byt
musicSlot->fadeStep = 0;
}
-void Sound::playPCSound(int num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType) {
+void Sound::playPCSound(int num, const Common::Array<SoundData> &dataArray, Audio::Mixer::SoundType soundType) {
if (_musicIdMap.size()) {
num = _musicIdMap[num];
}
if (num >= 0 && num < (int)dataArray.size()) {
- int size = sizeArray[num];
- const byte *data = dataArray[num];
- uint32 tracks = _availableSndTracks(data, size);
+ const SoundData &data = dataArray[num];
+ uint32 tracks = _availableSndTracks(data._data, data._size);
if (tracks & DIGITAL_PCM) {
- playPCM(data, size);
+ playPCM(data._data, data._size);
} else {
- uint32 hdrsize = 0;
- _readHeader(data, hdrsize);
- size -= hdrsize;
- int offset = soundType == Audio::Mixer::kSFXSoundType ? SND_RESOURCE_OFFSET : MUSIC_RESOURCE_OFFSET;
- processInitSound(num + offset, data, size, soundType);
- processPlaySound(num + offset, false, false, data, size);
+ int idOffset = soundType == Audio::Mixer::kSFXSoundType ? SND_RESOURCE_OFFSET : MUSIC_RESOURCE_OFFSET;
+ processInitSound(num + idOffset, data, soundType);
+ processPlaySound(num + idOffset, false, false, data);
}
} else {
warning("Sound: Requested to play %d but only have %d tracks", num, dataArray.size());
@@ -578,9 +587,8 @@ void Sound::stopMusic() {
void Sound::unloadMusic() {
stopMusic();
- _musicSizes.clear();
- for (auto *data: _musicData)
- delete [] data;
+ for (auto &data: _musicData)
+ delete [] data._data;
_musicData.clear();
// Don't unload sfxData.
diff --git a/engines/dgds/sound.h b/engines/dgds/sound.h
index 4a8a684841d..6898963c32e 100644
--- a/engines/dgds/sound.h
+++ b/engines/dgds/sound.h
@@ -41,6 +41,13 @@ struct Channel {
byte volume;
};
+struct SoundData {
+ SoundData() : _size(0), _data(nullptr), _flags(0) {}
+ uint32 _size;
+ const byte *_data;
+ uint16 _flags;
+};
+
class Sound {
public:
Sound(Audio::Mixer *mixer, ResourceManager *resource, Decompressor *decompressor);
@@ -64,25 +71,23 @@ public:
bool playPCM(const byte *data, uint32 size);
private:
- void loadPCSound(const Common::String &filename, Common::Array<uint32> &sizeArray, Common::Array<byte *> &dataArray);
- void playPCSound(int num, const Common::Array<uint32> &sizeArray, const Common::Array<byte *> &dataArray, Audio::Mixer::SoundType soundType);
+ void loadPCSound(const Common::String &filename, Common::Array<SoundData> &dataArray);
+ void playPCSound(int num, const Common::Array<SoundData> &dataArray, Audio::Mixer::SoundType soundType);
- void processInitSound(uint32 obj, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
+ void processInitSound(uint32 obj, const SoundData &data, Audio::Mixer::SoundType soundType);
void processDisposeSound(uint32 obj);
void processStopSound(uint32 obj, bool sampleFinishedPlaying);
- void processPlaySound(uint32 obj, bool playBed, bool restoring, const byte *data, int dataSz);
- void initSoundResource(MusicEntry *newSound, const byte *data, int dataSz, Audio::Mixer::SoundType soundType);
+ void processPlaySound(uint32 obj, bool playBed, bool restoring, const SoundData &data);
+ void initSoundResource(MusicEntry *newSound, const SoundData &data, Audio::Mixer::SoundType soundType);
int mapSfxNum(int num) const;
struct Channel _channels[2];
- Common::Array<uint32> _musicSizes;
- Common::Array<byte *> _musicData;
+ Common::Array<SoundData> _musicData;
Common::HashMap<uint16, uint16> _musicIdMap;
- Common::Array<uint32> _sfxSizes;
- Common::Array<byte *> _sfxData;
+ Common::Array<SoundData> _sfxData;
SciMusic *_music;
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
index 94402337ddd..a5dbae59aee 100644
--- a/engines/dgds/sound/music.cpp
+++ b/engines/dgds/sound/music.cpp
@@ -436,6 +436,11 @@ void SciMusic::soundInitSnd(MusicEntry *pSnd) {
// This can be a problem if it is a dedicated percussion
// channel, so always err on the side of caution.
pSnd->_chan[chan.number]._dontRemap |= (bool)(chan.flags & 2);
+
+ // HACK for DGDS - don't remap rhythm channel.
+ if (chan.number == 9)
+ pSnd->_chan[chan.number]._dontRemap = true;
+
if (pSnd->_chan[chan.number]._prio == -1)
pSnd->_chan[chan.number]._prio = chan.prio;
if (pSnd->_chan[chan.number]._voices == -1)
Commit: 9a35861c13932e9bc4d21e5cdb00920c84f86a7d
https://github.com/scummvm/scummvm/commit/9a35861c13932e9bc4d21e5cdb00920c84f86a7d
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Don't stop music if new one is the same
Changed paths:
engines/dgds/dgds.cpp
engines/dgds/sound.cpp
engines/dgds/sound.h
engines/dgds/ttm.cpp
diff --git a/engines/dgds/dgds.cpp b/engines/dgds/dgds.cpp
index d47d1cae83d..01519ed0f4d 100644
--- a/engines/dgds/dgds.cpp
+++ b/engines/dgds/dgds.cpp
@@ -196,7 +196,6 @@ bool DgdsEngine::changeScene(int sceneNum) {
_scene->unload();
_backgroundFile.clear();
- _soundPlayer->unloadMusic();
_soundPlayer->stopAllSfx();
_gdsScene->runChangeSceneOps();
@@ -754,7 +753,6 @@ Common::Error DgdsEngine::syncGame(Common::Serializer &s) {
if (!_resource->hasResource(sceneFile))
error("Game references non-existent scene %d", sceneNum);
- _soundPlayer->unloadMusic();
_soundPlayer->stopAllSfx();
_scene->unload();
_adsInterp->unload();
diff --git a/engines/dgds/sound.cpp b/engines/dgds/sound.cpp
index 1e24889f66e..400a9ba46a5 100644
--- a/engines/dgds/sound.cpp
+++ b/engines/dgds/sound.cpp
@@ -299,20 +299,22 @@ static void _readStrings(Common::SeekableReadStream *stream) {
}
}
-void Sound::loadMacMusic(const Common::String &filename) {
+bool Sound::loadMacMusic(const Common::String &filename) {
if (filename.hasSuffixIgnoreCase(".sng")) {
Common::String macFileName = filename.substr(0, filename.find(".")) + ".sx";
- loadMacMusic(macFileName);
- return;
+ return loadMacMusic(macFileName);
}
if (!filename.hasSuffixIgnoreCase(".sx"))
error("Unhandled music file type: %s", filename.c_str());
+ if (filename == _currentMusic)
+ return false;
+
Common::SeekableReadStream *musicStream = _resource->getResource(filename);
if (!musicStream) {
warning("Music file %s not found", filename.c_str());
- return;
+ return false;
}
DgdsChunkReader chunk(musicStream);
@@ -348,12 +350,18 @@ void Sound::loadMacMusic(const Common::String &filename) {
}
delete musicStream;
+ _currentMusic = filename;
+ return true;
}
-void Sound::loadMusic(const Common::String &filename) {
+bool Sound::loadMusic(const Common::String &filename) {
+ if (filename == _currentMusic)
+ return false;
unloadMusic();
loadPCSound(filename, _musicData);
+ _currentMusic = filename;
debug("Sound: Loaded music %s with %d entries", filename.c_str(), _musicData.size());
+ return true;
}
void Sound::loadSFX(const Common::String &filename) {
diff --git a/engines/dgds/sound.h b/engines/dgds/sound.h
index 6898963c32e..82a7bb977df 100644
--- a/engines/dgds/sound.h
+++ b/engines/dgds/sound.h
@@ -54,8 +54,8 @@ public:
~Sound();
void playAmigaSfx(const Common::String &filename, byte channel, byte volume);
- void loadMusic(const Common::String &filename);
- void loadMacMusic(const Common::String &filename);
+ bool loadMusic(const Common::String &filename);
+ bool loadMacMusic(const Common::String &filename);
void loadSFX(const Common::String &filename);
void playMusic(int num);
@@ -89,6 +89,8 @@ private:
Common::Array<SoundData> _sfxData;
+ Common::String _currentMusic;
+
SciMusic *_music;
Audio::Mixer *_mixer;
diff --git a/engines/dgds/ttm.cpp b/engines/dgds/ttm.cpp
index 63618546bf6..176540ea5d4 100644
--- a/engines/dgds/ttm.cpp
+++ b/engines/dgds/ttm.cpp
@@ -1092,11 +1092,11 @@ void TTMInterpreter::handleOperation(TTMEnviro &env, TTMSeq &seq, uint16 op, byt
// TODO: remove hard-coded stuff..
_vm->_soundPlayer->playAmigaSfx("DYNAMIX.INS", 0, 255);
} else if (_vm->_platform == Common::kPlatformMacintosh) {
- _vm->_soundPlayer->loadMacMusic(sval.c_str());
- _vm->_soundPlayer->playMusic(seq._currentSongId);
+ if (_vm->_soundPlayer->loadMacMusic(sval.c_str()))
+ _vm->_soundPlayer->playMusic(seq._currentSongId);
} else {
- _vm->_soundPlayer->loadMusic(sval.c_str());
- _vm->_soundPlayer->playMusic(seq._currentSongId);
+ if (_vm->_soundPlayer->loadMusic(sval.c_str()))
+ _vm->_soundPlayer->playMusic(seq._currentSongId);
}
break;
case 0xf080: { // LOAD SCROLL: filename:str
Commit: eff638308174b6ff0f0ad6fe57807fcabc1d9e96
https://github.com/scummvm/scummvm/commit/eff638308174b6ff0f0ad6fe57807fcabc1d9e96
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:15+11:00
Commit Message:
DGDS: Limit MT32 volume to 80 to match original
This is a small way the original diverges from SCI. Needed to avoid clipping.
Credit to @NMIError for helping find and fix this issue.
Changed paths:
engines/dgds/sound/drivers/midi.cpp
diff --git a/engines/dgds/sound/drivers/midi.cpp b/engines/dgds/sound/drivers/midi.cpp
index 372c791fe77..358e38ef83d 100644
--- a/engines/dgds/sound/drivers/midi.cpp
+++ b/engines/dgds/sound/drivers/midi.cpp
@@ -545,7 +545,8 @@ void MidiPlayer_Midi::readMt32Patch(const SciSpan<const byte> &data) {
stream.seek(_mt32LCDSize * 2);
stream.read(_goodbyeMsg, _mt32LCDSize);
- const uint8 volume = MIN<uint16>(stream.readUint16LE(), 100);
+ // Change from SCI - DGDS uses 80 instead of 100 here.
+ const uint8 volume = MIN<uint16>(stream.readUint16LE(), 80);
setMt32Volume(volume);
// Reverb default only used in (roughly) SCI0/SCI01
Commit: 8291d75dcacf6e5f8189ff4e2e3e8923727e88a4
https://github.com/scummvm/scummvm/commit/8291d75dcacf6e5f8189ff4e2e3e8923727e88a4
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:16+11:00
Commit Message:
DGDS: Small midi fixes for looping
Thanks again to @NMIError for finding the cause of looping issues in dragon.
With these fixes the sounds are now really close to the original.
Changed paths:
engines/dgds/sound.cpp
engines/dgds/sound/midiparser_sci.cpp
engines/dgds/sound/music.cpp
diff --git a/engines/dgds/sound.cpp b/engines/dgds/sound.cpp
index 400a9ba46a5..a8eed4c9c38 100644
--- a/engines/dgds/sound.cpp
+++ b/engines/dgds/sound.cpp
@@ -46,6 +46,8 @@ static const uint16 SIGNAL_OFFSET = 0xffff;
static const int SND_RESOURCE_OFFSET = 4096;
static const int MUSIC_RESOURCE_OFFSET = 8192;
+static const uint16 FLAG_LOOP = 1;
+
static void _readHeader(const byte* &pos, uint32 &sci_header) {
sci_header = 0;
if (READ_LE_UINT16(pos) == 0x0084)
@@ -422,13 +424,13 @@ int Sound::mapSfxNum(int num) const {
void Sound::playSFX(int num) {
int mappedNum = mapSfxNum(num);
- debug("Play SFX %d (-> %d), have %d entries", num, mappedNum, _sfxData.size());
+ debug("Sound: Play SFX %d (-> %d), have %d entries", num, mappedNum, _sfxData.size());
playPCSound(mappedNum, _sfxData, Audio::Mixer::kSFXSoundType);
}
void Sound::stopSfxByNum(int num) {
int mappedNum = mapSfxNum(num);
- debug("Stop SFX %d (-> %d)", num, mappedNum);
+ debug("Sound: Stop SFX %d (-> %d)", num, mappedNum);
MusicEntry *musicSlot = _music->getSlot(mappedNum + SND_RESOURCE_OFFSET);
if (!musicSlot) {
@@ -442,7 +444,7 @@ void Sound::stopSfxByNum(int num) {
}
void Sound::playMusic(int num) {
- debug("Sound: Play music %d of (%d entries)", num, _musicData.size());
+ debug("Sound: Play music %d (%s), have %d entries", num, _currentMusic.c_str(), _musicData.size());
playPCSound(num, _musicData, Audio::Mixer::kMusicSoundType);
}
@@ -456,13 +458,13 @@ void Sound::processInitSound(uint32 obj, const SoundData &data, Audio::Mixer::So
MusicEntry *newSound = new MusicEntry();
newSound->resourceId = obj;
newSound->soundObj = obj;
- newSound->loop = 0; // set in playSound
+ newSound->loop = 0; // set in processPlaySound
newSound->overridePriority = false;
- newSound->priority = 255; // TODO: Priority?
+ newSound->priority = 255;
newSound->volume = MUSIC_VOLUME_DEFAULT;
newSound->reverb = -1; // initialize to SCI invalid, it'll be set correctly in soundInitSnd() below
- debugC(kDebugLevelSound, "processInitSound: %08x number %d, loop %d, prio %d, vol %d", obj,
+ debug("processInitSound: %08x number %d, loop %d, prio %d, vol %d", obj,
obj, newSound->loop, newSound->priority, newSound->volume);
initSoundResource(newSound, data, soundType);
@@ -541,7 +543,7 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const Sou
musicSlot = _music->getSlot(obj);
}
- musicSlot->loop = (data._flags & 1) ? -1 : 1;
+ musicSlot->loop = (data._flags & FLAG_LOOP) ? 1 : 0;
// Get song priority from either obj or soundRes
byte resourcePriority = 0xFF;
@@ -550,7 +552,7 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const Sou
if (!musicSlot->overridePriority && resourcePriority != 0xFF) {
musicSlot->priority = resourcePriority;
} else {
- musicSlot->priority = 0xff; // todo: priority?
+ musicSlot->priority = 255;
}
// Reset hold when starting a new song. kDoSoundSetHold is always called after
@@ -559,8 +561,8 @@ void Sound::processPlaySound(uint32 obj, bool playBed, bool restoring, const Sou
musicSlot->playBed = playBed;
musicSlot->volume = MUSIC_VOLUME_DEFAULT;
- debugC(kDebugLevelSound, "kDoSound(play): %08x number %d, loop %d, prio %d, vol %d, bed %d", obj,
- resourceId, musicSlot->loop, musicSlot->priority, musicSlot->volume, playBed ? 1 : 0);
+ debug("processPlaySound: %08x number %d, sz %d, loop %d, prio %d, vol %d, bed %d", obj,
+ resourceId, data._size, musicSlot->loop, musicSlot->priority, musicSlot->volume, playBed ? 1 : 0);
_music->soundPlay(musicSlot, restoring);
@@ -590,6 +592,7 @@ void Sound::playPCSound(int num, const Common::Array<SoundData> &dataArray, Audi
}
void Sound::stopMusic() {
+ debug("Sound: Stop music.");
_music->stopMusic();
}
diff --git a/engines/dgds/sound/midiparser_sci.cpp b/engines/dgds/sound/midiparser_sci.cpp
index 5ca037e7967..5091357e080 100644
--- a/engines/dgds/sound/midiparser_sci.cpp
+++ b/engines/dgds/sound/midiparser_sci.cpp
@@ -565,7 +565,7 @@ bool MidiParser_SCI::processEvent(const EventInfo &info, bool fireEvents) {
case 0xC:
if (info.channel() == 0xF) {// SCI special case
if (info.basic.param1 == kSetSignalLoop) {
- _loopTick = _position._playTick;
+ _loopTick = _position._lastEventTime + info.delta;
return true;
}
@@ -676,14 +676,13 @@ bool MidiParser_SCI::processEvent(const EventInfo &info, bool fireEvents) {
break;
case 0xF: // META event
if (info.ext.type == 0x2F) {// end of track reached
- if (_pSnd->loop)
- _pSnd->loop--;
// QFG3 abuses the hold flag. Its scripts call kDoSoundSetHold,
// but sometimes there's no hold marker in the associated songs
// (e.g. song 110, during the intro). The original interpreter
// treats this case as an infinite loop (bug #5744).
if (_pSnd->loop || _pSnd->hold > 0) {
- jumpToTick(_loopTick);
+ // Change from SCI: Don't stop current notes on loop.
+ jumpToTick(_loopTick, false, false);
// Done with this event.
return true;
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
index a5dbae59aee..61a828b571f 100644
--- a/engines/dgds/sound/music.cpp
+++ b/engines/dgds/sound/music.cpp
@@ -438,7 +438,7 @@ void SciMusic::soundInitSnd(MusicEntry *pSnd) {
pSnd->_chan[chan.number]._dontRemap |= (bool)(chan.flags & 2);
// HACK for DGDS - don't remap rhythm channel.
- if (chan.number == 9)
+ if (chan.number == MIDI_RHYTHM_CHANNEL)
pSnd->_chan[chan.number]._dontRemap = true;
if (pSnd->_chan[chan.number]._prio == -1)
@@ -916,7 +916,7 @@ void MusicEntry::onTimer() {
}
}
- if (status != kSoundPlaying || !loop)
+ if (status != kSoundPlaying)
return;
// Fade MIDI and digital sound effects
Commit: 5973a310669b40dc7cf132ca1a4bf1d148065a1c
https://github.com/scummvm/scummvm/commit/5973a310669b40dc7cf132ca1a4bf1d148065a1c
Author: Matthew Duggan (mgithub at guarana.org)
Date: 2024-10-21T15:27:16+11:00
Commit Message:
DGDS: Small sound cleanups
Changed paths:
engines/dgds/sound/drivers/adlib.cpp
engines/dgds/sound/drivers/amigamac1.cpp
engines/dgds/sound/drivers/cms.cpp
engines/dgds/sound/drivers/midi.cpp
engines/dgds/sound/drivers/mididriver.h
engines/dgds/sound/midiparser_sci.cpp
engines/dgds/sound/music.cpp
engines/dgds/sound/music.h
diff --git a/engines/dgds/sound/drivers/adlib.cpp b/engines/dgds/sound/drivers/adlib.cpp
index b36f07a7e3a..210b20a67eb 100644
--- a/engines/dgds/sound/drivers/adlib.cpp
+++ b/engines/dgds/sound/drivers/adlib.cpp
@@ -179,7 +179,7 @@ public:
_driver = nullptr;
}
- int open(ResourceManager *resMan) override;
+ int open() override;
void close() override;
byte getPlayId() const override;
@@ -856,9 +856,7 @@ uint32 MidiDriver_AdLib::property(int prop, uint32 param) {
}
-int MidiPlayer_AdLib::open(ResourceManager *resMan) {
- assert(resMan != nullptr);
-
+int MidiPlayer_AdLib::open() {
// Load up the patch.003 file, parse out the instruments
SciResource *res = getMidiPatchData(3);
bool ok = false;
diff --git a/engines/dgds/sound/drivers/amigamac1.cpp b/engines/dgds/sound/drivers/amigamac1.cpp
index eff24e04ece..d6493dcfe63 100644
--- a/engines/dgds/sound/drivers/amigamac1.cpp
+++ b/engines/dgds/sound/drivers/amigamac1.cpp
@@ -910,7 +910,7 @@ public:
MidiPlayer_Mac1(Audio::Mixer *mixer, Mixer_Mac<MidiPlayer_Mac1>::Mode mode);
// MidiPlayer
- int open(ResourceManager *resMan) override;
+ int open() override;
// MidiDriver
void close() override;
@@ -945,7 +945,7 @@ MidiPlayer_Mac1::MidiPlayer_Mac1(Audio::Mixer *mixer, Mixer_Mac<MidiPlayer_Mac1>
Mixer_Mac<MidiPlayer_Mac1>(mode),
MidiPlayer_AmigaMac1(mixer, 1480, false, _mutex) {}
-int MidiPlayer_Mac1::open(ResourceManager *resMan) {
+int MidiPlayer_Mac1::open() {
if (_isOpen)
return MidiDriver::MERR_ALREADY_OPEN;
@@ -1091,7 +1091,7 @@ public:
MidiPlayer_Amiga1(Audio::Mixer *mixer);
// MidiPlayer
- int open(ResourceManager *resMan) override;
+ int open() override;
// MidiDriver
void close() override;
@@ -1127,7 +1127,7 @@ MidiPlayer_Amiga1::MidiPlayer_Amiga1(Audio::Mixer *mixer) :
MidiPlayer_AmigaMac1(mixer, 224, true, _mutex),
_isSci1Ega(false) {}
-int MidiPlayer_Amiga1::open(ResourceManager *resMan) {
+int MidiPlayer_Amiga1::open() {
if (_isOpen)
return MidiDriver::MERR_ALREADY_OPEN;
diff --git a/engines/dgds/sound/drivers/cms.cpp b/engines/dgds/sound/drivers/cms.cpp
index 88bd5665517..a2f02559bea 100644
--- a/engines/dgds/sound/drivers/cms.cpp
+++ b/engines/dgds/sound/drivers/cms.cpp
@@ -175,7 +175,7 @@ public:
};
public:
- MidiDriver_CMS(ResourceManager *resMan);
+ MidiDriver_CMS();
~MidiDriver_CMS() override;
int open() override;
@@ -231,7 +231,6 @@ private:
CMS::CMS *_cms;
bool _isOpen;
- ResourceManager *_resMan;
Common::SpanOwner<SciSpan<const uint8> > _patchData;
bool _playSwitch;
@@ -715,7 +714,7 @@ const int CMSVoice_V1::_velocityTable[32] = {
6, 6, 7, 8, 8, 9, 10, 10
};
-MidiDriver_CMS::MidiDriver_CMS(ResourceManager *resMan) : _resMan(resMan), _isOpen(false),
+MidiDriver_CMS::MidiDriver_CMS() : _isOpen(false),
_cms(nullptr), _playSwitch(true), _masterVolume(0), _numVoicesPrimary(12),
_timerProc(nullptr), _timerParam(nullptr), _actualTimerInterval(1000000 / CMS::CMS::DEFAULT_CALLBACK_FREQUENCY), _reqTimerInterval(1000000/60),
_numVoicesSecondary(0) {
@@ -732,7 +731,6 @@ int MidiDriver_CMS::open() {
if (_isOpen)
return MERR_ALREADY_OPEN;
- assert(_resMan);
SciResource *res = getMidiPatchData(101);
if (!res)
return -1;
@@ -751,7 +749,7 @@ int MidiDriver_CMS::open() {
}
_playSwitch = true;
- _masterVolume = 0;
+ _masterVolume = 15;
for (int i = 0; i < 31; ++i) {
writeToChip(0, i, 0);
@@ -1235,7 +1233,7 @@ class MidiPlayer_CMS : public MidiPlayer {
public:
MidiPlayer_CMS() : MidiPlayer() {}
- int open(ResourceManager *resMan) override;
+ int open() override;
void close() override;
bool hasRhythmChannel() const override { return false; }
@@ -1245,11 +1243,11 @@ public:
void playSwitch(bool play) override { _driver->property(MidiDriver_CMS::MIDI_PROP_PLAYSWITCH, play ? 1 : 0); }
};
-int MidiPlayer_CMS::open(ResourceManager *resMan) {
+int MidiPlayer_CMS::open() {
if (_driver)
return MidiDriver::MERR_ALREADY_OPEN;
- _driver = new MidiDriver_CMS(resMan);
+ _driver = new MidiDriver_CMS();
int driverRetVal = _driver->open();
return driverRetVal;
diff --git a/engines/dgds/sound/drivers/midi.cpp b/engines/dgds/sound/drivers/midi.cpp
index 358e38ef83d..3f41f6e7248 100644
--- a/engines/dgds/sound/drivers/midi.cpp
+++ b/engines/dgds/sound/drivers/midi.cpp
@@ -53,7 +53,7 @@ public:
MidiPlayer_Midi();
~MidiPlayer_Midi() override;
- int open(ResourceManager *resMan) override;
+ int open() override;
void close() override;
void send(uint32 b) override;
void sysEx(const byte *msg, uint16 length) override;
@@ -950,9 +950,7 @@ void MidiPlayer_Midi::resetMt32() {
}
}
-int MidiPlayer_Midi::open(ResourceManager *resMan) {
- assert(resMan != nullptr);
-
+int MidiPlayer_Midi::open() {
int retval = _driver->open();
if (retval != 0) {
warning("Failed to open MIDI driver");
diff --git a/engines/dgds/sound/drivers/mididriver.h b/engines/dgds/sound/drivers/mididriver.h
index 3043b82c408..13f51c83956 100644
--- a/engines/dgds/sound/drivers/mididriver.h
+++ b/engines/dgds/sound/drivers/mididriver.h
@@ -82,11 +82,7 @@ protected:
public:
MidiPlayer() : _driver(0), _reverb(-1) {}
- int open() {
- ResourceManager *resMan = DgdsEngine::getInstance()->getResourceManager(); // HACK
- return open(resMan);
- }
- virtual int open(ResourceManager *resMan) { return _driver->open(); }
+ virtual int open() { return _driver->open(); }
virtual void close() { _driver->close(); }
void send(uint32 b) override { _driver->send(b); }
virtual uint32 getBaseTempo() { return _driver->getBaseTempo(); }
diff --git a/engines/dgds/sound/midiparser_sci.cpp b/engines/dgds/sound/midiparser_sci.cpp
index 5091357e080..523e4791080 100644
--- a/engines/dgds/sound/midiparser_sci.cpp
+++ b/engines/dgds/sound/midiparser_sci.cpp
@@ -19,6 +19,8 @@
*
*/
+// This file was mostly copied from SCI with light modifications.
+
#include "dgds/sound/midiparser_sci.h"
#include "dgds/sound/drivers/mididriver.h"
diff --git a/engines/dgds/sound/music.cpp b/engines/dgds/sound/music.cpp
index 61a828b571f..48caf0b76e0 100644
--- a/engines/dgds/sound/music.cpp
+++ b/engines/dgds/sound/music.cpp
@@ -71,6 +71,14 @@ void SciMusic::init() {
_dwTempo = 0;
const Common::Platform platform = DgdsEngine::getInstance()->getPlatform();
+
+ //
+ // TODO: This list seems right for the basic versions of the DOS games.
+ //
+ // Do we ever want to support MDT_CMS? It looks like Willy Beamish
+ // has *some* audio for that track type, but it's not complete and not
+ // supported in the game's installer.
+ //
uint32 deviceFlags = MDT_PCSPK | MDT_ADLIB | MDT_MIDI;
uint32 dev = MidiDriver::detectDevice(deviceFlags);
@@ -85,7 +93,7 @@ void SciMusic::init() {
_pMidiDrv = MidiPlayer_AdLib_create();
break;
case MT_PCSPK:
- error("TODO: Implement pc speaker driver?");
+ error("TODO: Implement PC speaker driver?");
//_pMidiDrv = MidiPlayer_PCSpeaker_create();
break;
case MT_CMS:
diff --git a/engines/dgds/sound/music.h b/engines/dgds/sound/music.h
index 0b9902d1561..1c0e5ffd2d7 100644
--- a/engines/dgds/sound/music.h
+++ b/engines/dgds/sound/music.h
@@ -19,6 +19,16 @@
*
*/
+/**
+ * This file and all files in this directory were imported from the SCI
+ * engine and lightly modified to match DGDS, removing:
+ * * SCI0, SCI2, and SCI32 code
+ * * game-specific code
+ * * some unsupported devices
+ *
+ * The original games also used lightly modified SCI midi drivers.
+ */
+
#ifndef DGDS_SOUND_MUSIC_H
#define DGDS_SOUND_MUSIC_H
More information about the Scummvm-git-logs
mailing list