Files
scummvm-cursorfix/engines/sci/sound/drivers/casio.cpp
2026-02-02 04:50:13 +01:00

497 lines
17 KiB
C++

/* 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();
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;
}
}
// These data channels should always be mapped, but check it just in case.
if (currentDataChannel >= 0 && currentTargetDataChannel >= 0) {
// 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);
_channelMap[currentDataChannel] = 2;
_channelMap[currentTargetDataChannel] = outputChannel;
programChange(outputChannel, _instruments[2], source, applyRemapping, false);
outputChannel = 2;
}
_mutex.unlock();
}
// 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