[Scummvm-git-logs] scummvm master -> 332a428191a3bd4e5bfa675570c2b7768413c133

NMIError noreply at scummvm.org
Wed Jan 18 20:43:44 UTC 2023


This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
abdc5b0fb7 AUDIO: Casio MIDI driver enhancements and fixes
f376d56d19 SCI: Add support for Casio MT-540, CT-460 and CSM-1
332a428191 NEWS: Add SCI Casio MIDI support


Commit: abdc5b0fb73910bda1bdadd9d589636d368101b1
    https://github.com/scummvm/scummvm/commit/abdc5b0fb73910bda1bdadd9d589636d368101b1
Author: Coen Rampen (crampen at gmail.com)
Date: 2023-01-18T21:39:29+01:00

Commit Message:
AUDIO: Casio MIDI driver enhancements and fixes

- Add support for sustain controller
- Correct MT-540 <> CT-460/CSM-1 instrument remapping
- Simplify volume control (Casio devices do not support note velocity)

Changed paths:
    audio/casio.cpp
    audio/casio.h


diff --git a/audio/casio.cpp b/audio/casio.cpp
index b084d2c5195..178eb0cd002 100644
--- a/audio/casio.cpp
+++ b/audio/casio.cpp
@@ -31,29 +31,36 @@ void MidiDriver_Casio::ActiveNote::clear() {
 	source = 0x7F;
 	channel = 0xFF;
 	note = 0xFF;
+	sustained = false;
 }
 
-const uint8 MidiDriver_Casio::INSTRUMENT_REMAPPING_CT460_TO_MT540[30] {
-	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0E, 0x0A, 0x07,
-	0x09, 0x1B, 0x0F, 0x10, 0x11, 0x14, 0x08, 0x15, 0x0B, 0x0C,
-	0x0D, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1C, 0x1D, 0x12, 0x13
+const int MidiDriver_Casio::CASIO_CHANNEL_POLYPHONY[] = { 6, 4, 2, 4 };
+
+const uint8 MidiDriver_Casio::INSTRUMENT_REMAPPING_CT460_TO_MT540[] {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x10, 0x0C, 0x07,
+	0x0B, 0x1B, 0x11, 0x08, 0x09, 0x14, 0x0A, 0x15, 0x0D, 0x0E,
+	0x0F, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1C, 0x1D, 0x12, 0x13
 };
 
-const uint8 MidiDriver_Casio::INSTRUMENT_REMAPPING_MT540_TO_CT460[30] {
-	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x09, 0x10, 0x0A,
-	0x08, 0x12, 0x13, 0x14, 0x07, 0x0C, 0x0D, 0x0E, 0x1C, 0x1D,
+const uint8 MidiDriver_Casio::INSTRUMENT_REMAPPING_MT540_TO_CT460[] {
+	0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x09, 0x0D, 0x0E,
+	0x10, 0x0A, 0x08, 0x12, 0x13, 0x14, 0x07, 0x0C, 0x1C, 0x1D,
 	0x0F, 0x11, 0x15, 0x16, 0x17, 0x18, 0x19, 0x0B, 0x1A, 0x1B
 };
 
-const uint8 MidiDriver_Casio::RHYTHM_INSTRUMENT_MT540 = 0x10;
+const uint8 MidiDriver_Casio::RHYTHM_INSTRUMENT_MT540 = 0x08;
 const uint8 MidiDriver_Casio::RHYTHM_INSTRUMENT_CT460 = 0x0D;
+const uint8 MidiDriver_Casio::BASS_INSTRUMENT_MT540 = 0x12;
+const uint8 MidiDriver_Casio::BASS_INSTRUMENT_CT460 = 0x1C;
 
 MidiDriver_Casio::MidiDriver_Casio(MusicType midiType) : _midiType(midiType), _driver(nullptr),
-		_deviceType(MT_CT460), _isOpen(false), _rhythmNoteRemapping(nullptr) {
+		_deviceType(MT_CT460), _isOpen(false), _rhythmNoteRemapping(nullptr), _sendUntrackedNoteOff(true) {
 	if (!(_midiType == MT_MT540 || _midiType == MT_CT460)) {
-		error("MidiDriver_Casio - Unsupported music data type %i", midiType);
+		error("MidiDriver_Casio - Unsupported music data type %i", _midiType);
 	}
 	Common::fill(_instruments, _instruments + ARRAYSIZE(_instruments), 0);
+	Common::fill(_rhythmChannel, _rhythmChannel + ARRAYSIZE(_rhythmChannel), false);
+	Common::fill(_sustain, _sustain + ARRAYSIZE(_sustain), false);
 }
 
 MidiDriver_Casio::~MidiDriver_Casio() {
@@ -128,6 +135,7 @@ void MidiDriver_Casio::close() {
 	if (_driver && _isOpen) {
 		stopAllNotes();
 
+		_driver->setTimerCallback(nullptr, nullptr);
 		_driver->close();
 		_isOpen = false;
 	}
@@ -181,6 +189,9 @@ void MidiDriver_Casio::processEvent(int8 source, uint32 b, uint8 outputChannel)
 	case MIDI_COMMAND_PROGRAM_CHANGE:
 		programChange(outputChannel, op1, source);
 		break;
+	case MIDI_COMMAND_CONTROL_CHANGE:
+		controlChange(outputChannel, op1, op2, source);
+		break;
 	default:
 		warning("MidiDriver_Casio::processEvent - Received unsupported event %02x", command);
 		break;
@@ -197,16 +208,25 @@ void MidiDriver_Casio::noteOff(byte outputChannel, byte command, byte note, byte
 	_mutex.lock();
 
 	// Remove this note from the active note registry.
+	bool foundActiveNote = false;
 	for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) {
 		if (_activeNotes[i].channel == outputChannel && _activeNotes[i].note == mappedNote &&
-				_activeNotes[i].source == source) {
-			_activeNotes[i].clear();
+				_activeNotes[i].source == source && !_activeNotes[i].sustained) {
+			foundActiveNote = true;
+			if (outputChannel < 4 && _sustain[outputChannel]) {
+				_activeNotes[i].sustained = true;
+			} else {
+				_activeNotes[i].clear();
+			}
 			break;
 		}
 	}
 
 	_mutex.unlock();
 
+	if (!foundActiveNote && !_sendUntrackedNoteOff)
+		return;
+
 	_driver->send(command | outputChannel, mappedNote, velocity);
 }
 
@@ -257,9 +277,12 @@ int8 MidiDriver_Casio::mapNote(byte outputChannel, byte note) {
 byte MidiDriver_Casio::calculateVelocity(int8 source, byte velocity) {
 	byte calculatedVelocity = velocity;
 	// Apply volume settings to velocity.
+	// The Casio devices do not apply note velocity, so the only volume control
+	// possible is setting the velocity to 0 if one of the volume settings is 0.
 	if (source >= 0) {
 		// Scale to source volume.
-		calculatedVelocity = (velocity * _sources[source].volume) / _sources[source].neutralVolume;
+		if (_sources[source].volume == 0)
+			calculatedVelocity = 0;
 	}
 	if (_userVolumeScaling) {
 		if (_userMute) {
@@ -267,28 +290,34 @@ byte MidiDriver_Casio::calculateVelocity(int8 source, byte velocity) {
 		} else {
 			// Scale to user volume.
 			uint16 userVolume = _sources[source].type == SOURCE_TYPE_SFX ? _userSfxVolume : _userMusicVolume;
-			calculatedVelocity = (calculatedVelocity * userVolume) >> 8;
+			if (userVolume == 0)
+				calculatedVelocity = 0;
 		}
 	}
-	// Source volume scaling might clip volume, so reduce to maximum,
-	return MIN(calculatedVelocity, static_cast<byte>(0x7F));
+
+	return calculatedVelocity;
 }
 
-void MidiDriver_Casio::programChange(byte outputChannel, byte patchId, int8 source) {
+void MidiDriver_Casio::programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping) {
 	if (outputChannel < 4)
 		// Register the new instrument.
 		_instruments[outputChannel] = patchId;
 
 	// Apply instrument mapping.
-	byte mappedInstrument = mapInstrument(patchId);
+	byte mappedInstrument = mapInstrument(patchId, applyRemapping);
+
+	if (outputChannel < 4) {
+		_rhythmChannel[outputChannel] =
+			mappedInstrument == (_deviceType == MT_MT540 ? RHYTHM_INSTRUMENT_MT540 : RHYTHM_INSTRUMENT_CT460);
+	}
 
 	_driver->send(MIDI_COMMAND_PROGRAM_CHANGE | outputChannel | (mappedInstrument << 8));
 }
 
-byte MidiDriver_Casio::mapInstrument(byte program) {
+byte MidiDriver_Casio::mapInstrument(byte program, bool applyRemapping) {
 	byte mappedInstrument = program;
 
-	if (_instrumentRemapping)
+	if (applyRemapping && _instrumentRemapping)
 		// Apply custom instrument mapping.
 		mappedInstrument = _instrumentRemapping[program];
 
@@ -304,15 +333,39 @@ byte MidiDriver_Casio::mapInstrument(byte program) {
 	return mappedInstrument;
 }
 
+void MidiDriver_Casio::controlChange(byte outputChannel, byte controllerNumber, byte controllerValue, int8 source) {
+	if (outputChannel >= 4)
+		return;
+
+	if (controllerNumber == MIDI_CONTROLLER_SUSTAIN) {
+		_sustain[outputChannel] = controllerValue >= 0x40;
+
+		if (!_sustain[outputChannel]) {
+			// Remove sustained notes if sustain is turned off.
+
+			_mutex.lock();
+
+			for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) {
+				if (_activeNotes[i].channel == outputChannel && _activeNotes[i].sustained) {
+					_activeNotes[i].clear();
+				}
+			}
+
+			_mutex.unlock();
+		}
+
+		_driver->send(MIDI_COMMAND_CONTROL_CHANGE | outputChannel | (controllerNumber << 8) | (controllerValue << 16));
+	} else {
+		// No other controllers are supported.
+		//warning("MidiDriver_Casio::controlChange - Received event for unsupported controller %02x", controllerNumber);
+	}
+}
+
 bool MidiDriver_Casio::isRhythmChannel(uint8 outputChannel) {
 	if (outputChannel >= 4)
 		return false;
 
-	// Check if the current instrument on the channel is the rhythm instrument.
-	byte currentInstrument = mapInstrument(_instruments[outputChannel]);
-	byte rhythmInstrument = _deviceType == MT_MT540 ? RHYTHM_INSTRUMENT_MT540 : RHYTHM_INSTRUMENT_CT460;
-
-	return currentInstrument == rhythmInstrument;
+	return _rhythmChannel[outputChannel];
 }
 
 void MidiDriver_Casio::stopAllNotes(bool stopSustainedNotes) {
@@ -322,12 +375,20 @@ void MidiDriver_Casio::stopAllNotes(bool stopSustainedNotes) {
 void MidiDriver_Casio::stopAllNotes(uint8 source, uint8 channel) {
 	_mutex.lock();
 
+	// Turn off sustain.
+	for (int i = 0; i < 4; i++) {
+		if (channel == 0xFF || i == channel) {
+			controlChange(i, MIDI_CONTROLLER_SUSTAIN, 0, source > 127 ? -1 : source);
+		}
+	}
+
 	// Send note off events for all notes in the active note registry for this
 	// source and channel.
 	for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) {
 		if (_activeNotes[i].note != 0xFF && (source == 0xFF || _activeNotes[i].source == source) &&
 				(channel == 0xFF || _activeNotes[i].channel == channel)) {
-			noteOff(_activeNotes[i].channel, MIDI_COMMAND_NOTE_OFF, _activeNotes[i].note, 0, _activeNotes[i].source);
+			_driver->send(MIDI_COMMAND_NOTE_OFF | _activeNotes[i].channel, _activeNotes[i].note, 0);
+			_activeNotes[i].clear();
 		}
 	}
 
diff --git a/audio/casio.h b/audio/casio.h
index 510ae07e6b4..674016c9956 100644
--- a/audio/casio.h
+++ b/audio/casio.h
@@ -65,6 +65,9 @@ protected:
 		// The MIDI note number of the playing note
 		// (0xFF if no note is tracked).
 		uint8 note;
+		// True if this note is sustained (turned off but held due to the
+		// sustain controller).
+		bool sustained;
 
 		ActiveNote();
 
@@ -73,6 +76,9 @@ protected:
 	};
 
 public:
+	// The maximum polyphony for each output channel.
+	static const int CASIO_CHANNEL_POLYPHONY[4];
+
 	// Array for remapping instrument numbers from CT-460/CSM-1 to MT-540.
 	static const uint8 INSTRUMENT_REMAPPING_CT460_TO_MT540[30];
 	// Array for remapping instrument numbers from MT-540 to CT-460/CSM-1.
@@ -83,6 +89,12 @@ public:
 	// The instrument number used for rhythm sounds on the CT-460 and CSM-1.
 	static const uint8 RHYTHM_INSTRUMENT_CT460;
 
+	// The instrument number used for the bass instruments on the MT-540.
+	static const uint8 BASS_INSTRUMENT_MT540;
+	// The instrument number used for the bass instruments on the CT-460 and
+	// CSM-1.
+	static const uint8 BASS_INSTRUMENT_CT460;
+
 	/**
 	 * Constructs a new Casio MidiDriver instance.
 	 * 
@@ -162,8 +174,19 @@ protected:
 	 * @param outputChannel The MIDI channel on which the event should be sent.
 	 * @param patchId The instrument that should be set.
 	 * @param source The source sending the MIDI event.
+	 * @param applyRemapping True if the instrument remapping
+	 * (_instrumentRemapping) should be applied.
 	 */
-	virtual void programChange(byte outputChannel, byte patchId, int8 source);
+	virtual void programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping = true);
+	/**
+	 * Processes a MIDI control change event.
+	 *
+	 * @param outputChannel The MIDI channel on which the event should be sent.
+	 * @param controllerNumber The controller for which the value should be set.
+	 * @param controllerValue The controller value that should be set.
+	 * @param source The source sending the MIDI event.
+	 */
+	virtual void controlChange(byte outputChannel, byte controllerNumber, byte controllerValue, int8 source);
 
 	/**
 	 * Maps the specified note to a different note according to the rhythm note
@@ -188,14 +211,17 @@ protected:
 	/**
 	 * Maps the specified instrument to the instrument value that should be
 	 * sent to the MIDI device. This applies the current instrument remapping
-	 * (if present) and maps MT-540 instruments to CT-460/CSM-1 instruments
-	 * (or the other way around) if necessary.
+	 * (if present and if applyRemapping is specified) and maps MT-540
+	 * instruments to CT-460/CSM-1 instruments (or the other way around) if
+	 * necessary.
 	 * 
 	 * @param program The instrument that should be mapped.
+	 * @param applyRemapping True if the instrument remapping
+	 * (_instrumentRemapping) should be applied.
 	 * @return The mapped instrument, or the specified instrument if no mapping
 	 * was necessary.
 	 */
-	virtual byte mapInstrument(byte program);
+	virtual byte mapInstrument(byte program, bool applyRemapping = true);
 	/**
 	 * Returns whether the specified MIDI channel is a rhythm channel. On the
 	 * Casio devices, the rhythm channel is not fixed but is created by setting
@@ -205,7 +231,7 @@ protected:
 	 * @return True if the specified channel is a rhythm channel, false
 	 * otherwise.
 	 */
-	bool isRhythmChannel(uint8 outputChannel);
+	virtual bool isRhythmChannel(uint8 outputChannel);
 	// This implementation does nothing, because source volume is applied to
 	// note velocity and cannot be applied immediately.
 	void applySourceVolume(uint8 source) override;
@@ -225,14 +251,27 @@ protected:
 	// instrument number as specified in the program change events (before
 	// remapping is applied).
 	byte _instruments[4];
+	// Indicates if each output channel is currently a rhythm channel (i.e. it
+	// has the rhythm instrument set).
+	bool _rhythmChannel[4];
 	// Tracks the notes currently active on the MIDI device.
 	ActiveNote _activeNotes[32];
+	// Tracks the sustain controller status of the output channels.
+	bool _sustain[4];
 
 	// Optional remapping for rhythm notes. Should point to a 128 byte array
 	// which maps the rhythm note numbers in the input MIDI data to the rhythm
 	// note numbers used by the output device.
 	byte *_rhythmNoteRemapping;
 
+	// If true the driver will send note off events for notes which are not in
+	// the active note registry. Typically this should be true to prevent
+	// hanging notes in case there are more active notes than can be stored in
+	// the active note registry. Can be set to false if these note offs are not
+	// desirable, f.e. because the driver will be receiving note off events
+	// without corresponding note on events.
+	bool _sendUntrackedNoteOff;
+
 	// Mutex for operations on active notes.
 	Common::Mutex _mutex;
 


Commit: f376d56d198d00dd4dffa8f9a839f7eeef91cc7a
    https://github.com/scummvm/scummvm/commit/f376d56d198d00dd4dffa8f9a839f7eeef91cc7a
Author: Coen Rampen (crampen at gmail.com)
Date: 2023-01-18T21:39:29+01:00

Commit Message:
SCI: Add support for Casio MT-540, CT-460 and CSM-1

This adds support for the Casio MIDI devices originally supported by several
SCI0 games:

- Space Quest III
- Hoyle's Book Of Games I
- Quest For Glory I
- Leisure Suit Larry III
- The Colonel's Bequest
- Codename: Iceman
- Conquests Of Camelot

Changed paths:
  A engines/sci/sound/drivers/casio.cpp
    engines/sci/detection.h
    engines/sci/detection_options.h
    engines/sci/module.mk
    engines/sci/sound/drivers/mididriver.h
    engines/sci/sound/music.cpp


diff --git a/engines/sci/detection.h b/engines/sci/detection.h
index 14efa8f6638..e6dc9ead709 100644
--- a/engines/sci/detection.h
+++ b/engines/sci/detection.h
@@ -150,7 +150,9 @@ enum SciVersion {
 enum kMidiMode {
 	kMidiModeStandard,
 	kMidiModeFB01,
-	kMidiModeD110
+	kMidiModeD110,
+	kMidiModeMT540,
+	kMidiModeCT460
 };
 
 } // End of namespace Sci
diff --git a/engines/sci/detection_options.h b/engines/sci/detection_options.h
index 01d4a50fa36..d03dff624b7 100644
--- a/engines/sci/detection_options.h
+++ b/engines/sci/detection_options.h
@@ -251,6 +251,14 @@ const PopUpOptionsMap popUpOptionsList[] = {
 				_s("Yamaha FB-01"),
 				kMidiModeFB01
 			},
+			{
+				_s("Casio MT-540"),
+				kMidiModeMT540
+			},
+			{
+				_s("Casio CT-460 / CSM-1"),
+				kMidiModeCT460
+			},
 			POPUP_OPTIONS_ITEMS_TERMINATOR
 		}
 	},
diff --git a/engines/sci/module.mk b/engines/sci/module.mk
index d72f06bee67..9898f653b64 100644
--- a/engines/sci/module.mk
+++ b/engines/sci/module.mk
@@ -79,6 +79,7 @@ MODULE_OBJS := \
 	sound/drivers/adlib.o \
 	sound/drivers/amigamac0.o \
 	sound/drivers/amigamac1.o \
+	sound/drivers/casio.o \
 	sound/drivers/cms.o \
 	sound/drivers/fb01.o \
 	sound/drivers/fmtowns.o \
diff --git a/engines/sci/sound/drivers/casio.cpp b/engines/sci/sound/drivers/casio.cpp
new file mode 100644
index 00000000000..b350bdd7968
--- /dev/null
+++ b/engines/sci/sound/drivers/casio.cpp
@@ -0,0 +1,492 @@
+/* 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/sound/drivers/mididriver.h"
+
+#include "audio/casio.h"
+
+#include "sci/resource/resource.h"
+
+namespace Sci {
+
+class MidiDriver_Casio : public ::MidiDriver_Casio {
+protected:
+	// The instrument number used for the slap bass instrument on MT-540.
+	static const uint8 SLAP_BASS_INSTRUMENT_MT540;
+	// The instrument number used for the slap bass instrument on CT-460 and
+	// CSM-1.
+	static const uint8 SLAP_BASS_INSTRUMENT_CT460;
+	static const uint8 PATCH_RESOURCE_SIZE;
+
+public:
+	MidiDriver_Casio(MusicType midiType) : ::MidiDriver_Casio(midiType),
+			_highSplitInstOutputChannel(-1), _rhythmChannelMapped(false), _playSwitch(true) {
+		Common::fill(_instrumentRemapping, _instrumentRemapping + ARRAYSIZE(_instrumentRemapping), 0);
+		setInstrumentRemapping(_instrumentRemapping);
+		_rhythmNoteRemapping = new byte[128];
+
+		Common::fill(_instrumentFixedNotes, _instrumentFixedNotes + ARRAYSIZE(_instrumentFixedNotes), 0);
+		Common::fill(_channelMap, _channelMap + ARRAYSIZE(_channelMap), 0);
+		Common::fill(_channelFixedNotes, _channelFixedNotes + ARRAYSIZE(_channelFixedNotes), 0);
+
+		_sendUntrackedNoteOff = false;
+	}
+	~MidiDriver_Casio() {
+		delete[] _rhythmNoteRemapping;
+	}
+
+	bool loadResource(const SciSpan<const byte> &data, MusicType midiType = MT_AUTO);
+	void initTrack(SciSpan<const byte> &header);
+
+	void playSwitch(bool play);
+
+protected:
+	void noteOn(byte outputChannel, byte note, byte velocity, int8 source) override;
+	void programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping = true) override;
+	void programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping, bool applyBassSwap);
+
+	int8 mapSourceChannel(uint8 source, uint8 dataChannel) override;
+	byte mapInstrument(byte program, bool applyRemapping) override;
+	int8 mapNote(byte outputChannel, byte note) override;
+	bool isRhythmChannel(uint8 outputChannel) override;
+	byte calculateVelocity(int8 source, byte velocity) override;
+
+	byte _instrumentRemapping[128];
+	// If > 0, a fixed note value should be played for the corresponding
+	// instrument instead of the MIDI event note value.
+	byte _instrumentFixedNotes[0x60];
+	// Tracks the output channel which is currently being used by the "high"
+	// split bass instrument (if any). Will be either -1 or 2.
+	int8 _highSplitInstOutputChannel;
+
+	int8 _channelMap[16];
+	// True if the rhythm channel has been mapped to output channel 3.
+	bool _rhythmChannelMapped;
+	// The fixed note that needs to be played on each output channel instead of
+	// the MIDI event note value (or 0 if there is no fixed note).
+	byte _channelFixedNotes[4];
+	bool _playSwitch;
+};
+
+class MidiPlayer_Casio : public MidiPlayer {
+public:
+	static const uint8 RESOURCE_HEADER_FLAG;
+
+protected:
+	static const byte PATCH_RESOURCE_MT540;
+	static const byte PATCH_RESOURCE_CT460;
+
+public:
+	MidiPlayer_Casio(SciVersion soundVersion, MusicType midiType);
+	~MidiPlayer_Casio() override;
+
+	int open(ResourceManager *resMan) override;
+	void close() override;
+
+	byte getPlayId() const override;
+	int getPolyphony() const override;
+	bool hasRhythmChannel() const override;
+	void setVolume(byte volume) override;
+	void playSwitch(bool play) override;
+	void initTrack(SciSpan<const byte> &header) override;
+	int getLastChannel() const override;
+
+	void send(uint32 b) override;
+
+protected:
+	MidiDriver_Casio *_casioDriver;
+	MusicType _midiType;
+};
+
+const uint8 MidiDriver_Casio::SLAP_BASS_INSTRUMENT_MT540 = 0x14;
+const uint8 MidiDriver_Casio::SLAP_BASS_INSTRUMENT_CT460 = 0x1E;
+
+const uint8 MidiDriver_Casio::PATCH_RESOURCE_SIZE = 0xE9;
+
+bool MidiDriver_Casio::loadResource(const SciSpan<const byte> &data, MusicType midiType) {
+	if (midiType != MT_AUTO) {
+		if (!(midiType == MT_MT540 || midiType == MT_CT460)) {
+			error("CASIO: Unsupported music data type %i", midiType);
+		}
+		_midiType = midiType;
+	}
+
+	const uint32 size = data.size();
+	if (size != PATCH_RESOURCE_SIZE) {
+		error("CASIO: Unsupported patch format (%u bytes)", size);
+		return false;
+	}
+
+	uint32 dataIndex = 0;
+	for (int i = 0; i < 0x60; i++) {
+		_instrumentRemapping[i] = data.getUint8At(dataIndex++);
+		_instrumentFixedNotes[i] = data.getUint8At(dataIndex++);
+	}
+	for (int i = 0; i < 0x29; i++) {
+		_rhythmNoteRemapping[0x23 + i] = data.getUint8At(dataIndex++);
+	}
+
+	return true;
+}
+
+void MidiDriver_Casio::initTrack(SciSpan<const byte> &header) {
+	if (!_isOpen)
+		return;
+
+	Common::fill(_channelMap, _channelMap + ARRAYSIZE(_channelMap), -1);
+	Common::fill(_rhythmChannel, _rhythmChannel + ARRAYSIZE(_rhythmChannel), false);
+	Common::fill(_channelFixedNotes, _channelFixedNotes + ARRAYSIZE(_channelFixedNotes), 0);
+	_rhythmChannelMapped = false;
+
+	uint8 readPos = 0;
+	uint8 caps = header.getInt8At(readPos++);
+	if (caps != 0 && caps != 2)
+		// Not a supported sound resource type.
+		return;
+
+	uint8 numChannels = 16;
+	if (caps == 2)
+		// Digital sound data on channel 15; don't use this channel.
+		numChannels--;
+
+	byte outputChannel = 0;
+	for (int i = 0; i < numChannels; i++) {
+		bool rhythmChannel = ((header.getInt8At(readPos++) & 0x80) > 0);
+		bool deviceFlag = ((header.getInt8At(readPos++) & MidiPlayer_Casio::RESOURCE_HEADER_FLAG) > 0);
+		if (!deviceFlag)
+			// Data channel is not used for Casio devices.
+			continue;
+
+		if (rhythmChannel) {
+			if (!_rhythmChannelMapped) {
+				if (outputChannel == 4) {
+					// The rhythm channel has already been assigned to a melodic
+					// instrument. This means that more than 4 channels have
+					// been flagged for Casio, which should not happen, but
+					// clear the existing channel mapping just in case.
+					for (int j = 0; j < numChannels; j++) {
+						if (_channelMap[j] == 3)
+							_channelMap[j] = -1;
+					}
+				}
+				_channelMap[i] = 3;
+				programChange(3, _midiType == MusicType::MT_MT540 ? RHYTHM_INSTRUMENT_MT540 : RHYTHM_INSTRUMENT_CT460, 0, false);
+				_rhythmChannelMapped = true;
+			}
+		} else if (outputChannel < (_rhythmChannelMapped ? 3 : 4)) {
+			_channelMap[i] = outputChannel++;
+		}
+	}
+}
+
+void MidiDriver_Casio::playSwitch(bool play) {
+	_playSwitch = play;
+	if (!_playSwitch)
+		stopAllNotes(0xFF, 0xFF);
+};
+
+void MidiDriver_Casio::noteOn(byte outputChannel, byte note, byte velocity, int8 source) {
+	if (velocity == 0) {
+		// Note on with velocity 0 is a note off.
+		noteOff(outputChannel, MIDI_COMMAND_NOTE_ON, note, velocity, source);
+		return;
+	}
+
+	_mutex.lock();
+
+	// Check if there is an available voice for this note.
+	int polyphonyCount = 0;
+	for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) {
+		// Note that this check ignores sustained notes; original driver does
+		// this too.
+		if (_activeNotes[i].channel == outputChannel && !_activeNotes[i].sustained) {
+			polyphonyCount++;
+		}
+	}
+	if (polyphonyCount >= CASIO_CHANNEL_POLYPHONY[outputChannel]) {
+		// Maximum channel polyphony has been reached. Don't play this note.
+		_mutex.unlock();
+		return;
+	}
+
+	::MidiDriver_Casio::noteOn(outputChannel, note, velocity, source);
+
+	_mutex.unlock();
+}
+
+void MidiDriver_Casio::programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping) {
+	programChange(outputChannel, patchId, source, applyRemapping, true);
+}
+
+void MidiDriver_Casio::programChange(byte outputChannel, byte patchId, int8 source, bool applyRemapping, bool applyBassSwap) {
+	if ((_rhythmChannelMapped && outputChannel == 3) || outputChannel >= 4)
+		// Ignore program change on the rhythm channel or on unused channels.
+		return;
+
+	// Apply instrument mapping.
+	byte mappedInstrument = mapInstrument(patchId, applyRemapping);
+
+	// The Casio devices have an instrument (at 0x12 / 0x1C) which combines two
+	// different bass instruments with a split note range. SCI assigns a
+	// separate number to the slap bass instrument, which is on the "high" note
+	// range (0x14 / 0x1E). Check for this number.
+	if (mappedInstrument == (_deviceType == MT_MT540 ? SLAP_BASS_INSTRUMENT_MT540 : SLAP_BASS_INSTRUMENT_CT460)) {
+		// The "high" split instrument (slap bass) has been selected.
+		// Set the channel using this instrument so notes can be remapped to
+		// the correct range.
+		_highSplitInstOutputChannel = 2; // Output channel is set to 2 below.
+		// Set the actual instrument number used by the Casio devices.
+		mappedInstrument = (_deviceType == MT_MT540 ? BASS_INSTRUMENT_MT540 : BASS_INSTRUMENT_CT460);
+	} else if (_highSplitInstOutputChannel == outputChannel) {
+		// The instrument on this channel is changed from the "high" split
+		// instrument to a different instrument. Reset the output channel
+		// variable.
+		_highSplitInstOutputChannel = -1;
+	}
+
+	// If the bass instrument is set on any channel, SCI always moves this
+	// instrument to output channel 2. This is probably because the Casio
+	// devices have a limited fixed polyphony on each channel: 6, 4,
+	// 2 and 4 respectively. Moving the bass to channel 2 overcomes this
+	// limitation somewhat, because this channel has the lowest polyphony and
+	// the bass doesn't tend to play chords.
+	// Check if the bass instrument is set to a channel other than 2, and
+	// move it to channel 2 if necessary.
+	if (applyBassSwap && mappedInstrument == (_deviceType == MT_MT540 ? BASS_INSTRUMENT_MT540 : BASS_INSTRUMENT_CT460) && outputChannel != 2) {
+		_mutex.lock();
+
+		// The original drivers do not stop all notes before swapping channels.
+		// This could potentially cause hanging notes, so this is done here to
+		// be safe. Instead, the original drivers swap out the registered
+		// active notes between the channels. This does not accomplish anything
+		// other than putting the driver state out of sync with the device
+		// state.
+		stopAllNotes(source, outputChannel);
+		stopAllNotes(source, 2);
+
+		int currentDataChannel = -1;
+		int currentTargetDataChannel = -1;
+		for (int i = 0; i < MIDI_CHANNEL_COUNT; i++) {
+			if (_channelMap[i] == outputChannel) {
+				currentDataChannel = i;
+			} else if (_channelMap[i] == 2) {
+				currentTargetDataChannel = i;
+			}
+		}
+		_channelMap[currentDataChannel] = 2;
+		_channelMap[currentTargetDataChannel] = outputChannel;
+
+		programChange(outputChannel, _instruments[2], source, applyRemapping, false);
+
+		_mutex.unlock();
+
+		outputChannel = 2;
+	}
+
+	// Register the new instrument.
+	_instruments[outputChannel] = patchId;
+
+	_channelFixedNotes[outputChannel] = (patchId < ARRAYSIZE(_instrumentFixedNotes) ? _instrumentFixedNotes[patchId] : 0);
+
+	_rhythmChannel[outputChannel] =
+		mappedInstrument == (_deviceType == MT_MT540 ? RHYTHM_INSTRUMENT_MT540 : RHYTHM_INSTRUMENT_CT460);
+
+	_driver->send(MIDI_COMMAND_PROGRAM_CHANGE | outputChannel | (mappedInstrument << 8));
+}
+
+int8 MidiDriver_Casio::mapSourceChannel(uint8 source, uint8 dataChannel) {
+	// Only source 0 is used by this driver.
+	return _channelMap[dataChannel];
+}
+
+byte MidiDriver_Casio::mapInstrument(byte program, bool applyRemapping) {
+	byte mappedInstrument = ::MidiDriver_Casio::mapInstrument(program, applyRemapping);
+
+	if (applyRemapping) {
+		// Correct remapping of the extra SCI slap bass instrument.
+		if (_midiType == MT_MT540 && _deviceType == MT_CT460 &&
+				mappedInstrument == INSTRUMENT_REMAPPING_MT540_TO_CT460[SLAP_BASS_INSTRUMENT_MT540]) {
+			// For the MT-540, SCI uses 0x14 as the slap bass instrument, which
+			// actually is the honky-tonk piano. If the instrument has been mapped
+			// to the CT-460 honky-tonk piano, correct it to the CT-460 SCI slap
+			// bass.
+			mappedInstrument = SLAP_BASS_INSTRUMENT_CT460;
+		} else if (_midiType == MT_CT460 && _deviceType == MT_MT540 && mappedInstrument == SLAP_BASS_INSTRUMENT_CT460) {
+			// For the CT-460, SCI uses 0x1E as the slap bass instrument, which
+			// is unused, so it will not be remapped. Manually remap it here to
+			// the MT-540 SCI slap bass instrument.
+			mappedInstrument = SLAP_BASS_INSTRUMENT_MT540;
+		}
+	}
+
+	return mappedInstrument;
+}
+
+int8 MidiDriver_Casio::mapNote(byte outputChannel, byte note) {
+	if (!isRhythmChannel(outputChannel) && outputChannel < 4) {
+		if (_highSplitInstOutputChannel == outputChannel) {
+			// The slap bass instrument has been set on this output channel.
+			// Transpose the note up to the range used by this instrument.
+			byte transposedNote = note + 0x18;
+			if (transposedNote < 0x3C)
+				transposedNote += 0xC;
+			return transposedNote;
+		}
+
+		// Check if the MIDI event note should be replaced by a fixed note.
+		byte fixedNote = _channelFixedNotes[outputChannel];
+		return fixedNote > 0 ? fixedNote : note;
+	}
+
+	// Apply rhythm note mapping.
+	return ::MidiDriver_Casio::mapNote(outputChannel, note);
+}
+
+bool MidiDriver_Casio::isRhythmChannel(uint8 outputChannel) {
+	// SCI only uses channel 3 as the rhythm channel. If the drum instrument is
+	// set on a different channel, it is for a sound effect and rhythm note
+	// remapping should not be applied.
+	return _rhythmChannelMapped && outputChannel == 3;
+}
+
+byte MidiDriver_Casio::calculateVelocity(int8 source, byte velocity) {
+	if (!_playSwitch)
+		return 0;
+
+	return ::MidiDriver_Casio::calculateVelocity(source, velocity);
+}
+
+const uint8 MidiPlayer_Casio::RESOURCE_HEADER_FLAG = 0x08;
+
+const byte MidiPlayer_Casio::PATCH_RESOURCE_MT540 = 4;
+const byte MidiPlayer_Casio::PATCH_RESOURCE_CT460 = 7;
+
+MidiPlayer_Casio::MidiPlayer_Casio(SciVersion soundVersion, MusicType midiType) : MidiPlayer(soundVersion) {
+	_casioDriver = new MidiDriver_Casio(midiType);
+	_driver = _casioDriver;
+	_midiType = midiType;
+}
+
+MidiPlayer_Casio::~MidiPlayer_Casio() {
+	delete _casioDriver;
+	_casioDriver = nullptr;
+	_driver = nullptr;
+}
+
+int MidiPlayer_Casio::open(ResourceManager* resMan) {
+	if (_version < SCI_VERSION_0_LATE || _version > SCI_VERSION_01) {
+		warning("CASIO: Unsupported SCI version.");
+		return -1;
+	}
+
+	assert(resMan != nullptr);
+
+	// WORKAROUND The Casio devices have a bass instrument which combines two
+	// instruments on different note ranges. To make the second instrument
+	// (slap bass) selectable, SCI assigns a new instrument number to this
+	// instrument. On the MT-540 Sierra used instrument 0x14 (20), probably
+	// because they thought this was the first unused instrument number.
+	// However, besides the 20 instruments selectable on the keyboard, the
+	// device has 10 more instruments which can only be selected via MIDI.
+	// The result of this is that the instrument which actually uses number
+	// 0x14 on the MT-540 (honky-tonk piano) cannot be used by the instrument
+	// mapping. Sierra worked around this by using the normal piano instead of
+	// the honky-tonk piano for the MT-540. This affects at least Hoyle 1.
+	// The CT-460 and CSM-1 are not affected by this issue because Sierra used
+	// instrument 0x1E (30) as the slap bass instrument. To fix this problem,
+	// the CT-460 instrument mapping is also used for the MT-540, with the
+	// output instruments remapped to their MT-540 equivalents.
+
+	// Load the CT-460 patch resource.
+	int patchResource = PATCH_RESOURCE_CT460;
+	_midiType = MusicType::MT_CT460;
+	Resource *res = resMan->findResource(ResourceId(kResourceTypePatch, patchResource), false);
+	bool ok = false;
+
+	if (res) {
+		ok = _casioDriver->loadResource(*res, _midiType);
+	}
+
+	if (!ok) {
+		// CT-460 patch resource not present. Fall back to the MT-540 resource.
+		MusicType altMidiType = MT_MT540;
+		int altPatchResource = PATCH_RESOURCE_CT460;
+		warning("CASIO: Failed to load patch.00%i - falling back to patch.00%i", patchResource, altPatchResource);
+		res = resMan->findResource(ResourceId(kResourceTypePatch, altPatchResource), false);
+
+		if (res) {
+			ok = _casioDriver->loadResource(*res, altMidiType);
+		}
+
+		if (!ok) {
+			warning("CASIO: Failed to load fallback patch.00%i", altPatchResource);
+			return -1;
+		}
+
+		_midiType = altMidiType;
+	}
+
+	return _casioDriver->open();
+}
+
+void MidiPlayer_Casio::close() {
+	_driver->close();
+}
+
+byte MidiPlayer_Casio::getPlayId() const {
+	return RESOURCE_HEADER_FLAG;
+}
+
+int MidiPlayer_Casio::getPolyphony() const {
+	return 16;
+}
+
+bool MidiPlayer_Casio::hasRhythmChannel() const {
+	// Only use the rhythm channel if it has the Casio flag set.
+	return false;
+}
+
+void MidiPlayer_Casio::setVolume(byte volume) {
+	_casioDriver->setSourceVolume(0, volume);
+}
+
+void MidiPlayer_Casio::playSwitch(bool play) {
+	_casioDriver->playSwitch(play);
+}
+
+void MidiPlayer_Casio::initTrack(SciSpan<const byte> &header) {
+	_casioDriver->initTrack(header);
+}
+
+int MidiPlayer_Casio::getLastChannel() const {
+	// Not relevant for SCI0.
+	return 15;
+}
+
+void MidiPlayer_Casio::send(uint32 b) {
+	_driver->send(0, b);
+}
+
+MidiPlayer *MidiPlayer_Casio_create(SciVersion version, MusicType midiType) {
+	return new MidiPlayer_Casio(version, midiType);
+}
+
+} // End of namespace Sci
diff --git a/engines/sci/sound/drivers/mididriver.h b/engines/sci/sound/drivers/mididriver.h
index 0009398f2d6..5f97852b16d 100644
--- a/engines/sci/sound/drivers/mididriver.h
+++ b/engines/sci/sound/drivers/mididriver.h
@@ -147,6 +147,7 @@ extern MidiPlayer *MidiPlayer_CMS_create(SciVersion version);
 extern MidiPlayer *MidiPlayer_MacSci0_create(SciVersion version);
 extern MidiPlayer *MidiPlayer_Midi_create(SciVersion version);
 extern MidiPlayer *MidiPlayer_Fb01_create(SciVersion version);
+extern MidiPlayer *MidiPlayer_Casio_create(SciVersion version, MusicType midiType);
 extern MidiPlayer *MidiPlayer_FMTowns_create(SciVersion version);
 extern MidiPlayer *MidiPlayer_PC9801_create(SciVersion version);
 
diff --git a/engines/sci/sound/music.cpp b/engines/sci/sound/music.cpp
index 9caeb4888d2..1b703d1ef9f 100644
--- a/engines/sci/sound/music.cpp
+++ b/engines/sci/sound/music.cpp
@@ -137,9 +137,15 @@ void SciMusic::init() {
 		_pMidiDrv = MidiPlayer_PC9801_create(_soundVersion);
 		break;
 	default:
-		if (ConfMan.getInt("midi_mode") == kMidiModeFB01
+		int midiMode;
+		midiMode = ConfMan.getInt("midi_mode");
+		if (midiMode == kMidiModeFB01
 		    || (ConfMan.hasKey("native_fb01") && ConfMan.getBool("native_fb01")))
 			_pMidiDrv = MidiPlayer_Fb01_create(_soundVersion);
+		else if (midiMode == kMidiModeMT540)
+			_pMidiDrv = MidiPlayer_Casio_create(_soundVersion, MusicType::MT_MT540);
+		else if (midiMode == kMidiModeCT460)
+			_pMidiDrv = MidiPlayer_Casio_create(_soundVersion, MusicType::MT_CT460);
 		else
 			_pMidiDrv = MidiPlayer_Midi_create(_soundVersion);
 	}


Commit: 332a428191a3bd4e5bfa675570c2b7768413c133
    https://github.com/scummvm/scummvm/commit/332a428191a3bd4e5bfa675570c2b7768413c133
Author: Coen Rampen (crampen at gmail.com)
Date: 2023-01-18T21:39:29+01:00

Commit Message:
NEWS: Add SCI Casio MIDI support

Changed paths:
    NEWS.md


diff --git a/NEWS.md b/NEWS.md
index f036e79f328..b807cf614b4 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -92,6 +92,8 @@ For a more comprehensive changelog of the latest experimental code, see:
 
  SCI:
    - Improved text rendering for Macintosh titles.
+   - Added support for Casio MT-540, CT-460 and CSM-1 MIDI devices for the SCI0
+     games that originally supported it.
 
  SCUMM:
    - Added support for CGA, CGA Composite, CGA black & white and Hercules modes




More information about the Scummvm-git-logs mailing list