diff --git a/audio/casio.cpp b/audio/casio.cpp new file mode 100644 index 00000000000..b084d2c5195 --- /dev/null +++ b/audio/casio.cpp @@ -0,0 +1,362 @@ +/* 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 . + * + */ + +#include "audio/casio.h" + +#include "common/config-manager.h" + +MidiDriver_Casio::ActiveNote::ActiveNote() { + clear(); +} + +void MidiDriver_Casio::ActiveNote::clear() { + source = 0x7F; + channel = 0xFF; + note = 0xFF; +} + +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 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, + 0x0F, 0x11, 0x15, 0x16, 0x17, 0x18, 0x19, 0x0B, 0x1A, 0x1B +}; + +const uint8 MidiDriver_Casio::RHYTHM_INSTRUMENT_MT540 = 0x10; +const uint8 MidiDriver_Casio::RHYTHM_INSTRUMENT_CT460 = 0x0D; + +MidiDriver_Casio::MidiDriver_Casio(MusicType midiType) : _midiType(midiType), _driver(nullptr), + _deviceType(MT_CT460), _isOpen(false), _rhythmNoteRemapping(nullptr) { + if (!(_midiType == MT_MT540 || _midiType == MT_CT460)) { + error("MidiDriver_Casio - Unsupported music data type %i", midiType); + } + Common::fill(_instruments, _instruments + ARRAYSIZE(_instruments), 0); +} + +MidiDriver_Casio::~MidiDriver_Casio() { + close(); + + if (_driver) { + _driver->setTimerCallback(nullptr, nullptr); + delete _driver; + _driver = nullptr; + } +} + +int MidiDriver_Casio::open() { + assert(!_driver); + + // Detect the output device. + MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_GM); + MusicType deviceMusicType = MidiDriver::getMusicType(dev); + // System MIDI ports have type GM. This driver supports no other type. + if (deviceMusicType != MT_GM) + error("MidiDriver_Casio::open - Detected device has unsupported type %i", deviceMusicType); + + // Create the driver. + MidiDriver *driver = MidiDriver::createMidi(dev); + + // Check the MIDI mode setting to determine if the device connected to the + // system MIDI port is an MT-540 or a CT-460/CSM-1. + int midiMode = ConfMan.getInt("midi_mode"); + MusicType deviceType; + if (midiMode == 3) { + deviceType = MT_MT540; + } else if (midiMode == 4) { + deviceType = MT_CT460; + } else { + error("MidiDriver_Casio::open - Unsupported MIDI mode %i", midiMode); + } + + return open(driver, deviceType); +} + +int MidiDriver_Casio::open(MidiDriver* driver, MusicType deviceType) { + assert(!_driver); + + _driver = driver; + _deviceType = deviceType; + if (!(_deviceType == MT_MT540 || _deviceType == MT_CT460)) { + error("MidiDriver_Casio::open - Unsupported device type %i", _deviceType); + } + + if (!_driver) + return 255; + + int result = _driver->open(); + if (result != MidiDriver::MERR_ALREADY_OPEN && result != 0) { + return result; + } + + // Initialize the device by setting the instrument to 0 on all channels. + for (int i = 0; i < 4; i++) { + programChange(i, 0, -1); + } + + _timerRate = _driver->getBaseTempo(); + _driver->setTimerCallback(this, timerCallback); + + _isOpen = true; + + return 0; +} + +void MidiDriver_Casio::close() { + if (_driver && _isOpen) { + stopAllNotes(); + + _driver->close(); + _isOpen = false; + } +} + +bool MidiDriver_Casio::isOpen() const { + return _isOpen; +} + +void MidiDriver_Casio::send(int8 source, uint32 b) { + byte dataChannel = b & 0xf; + int8 outputChannel = source < 0 ? dataChannel : mapSourceChannel(source, dataChannel); + if (outputChannel < 0 || outputChannel >= 4) + // Only process events for channels 0-3. + return; + + processEvent(source, b, outputChannel); +} + +void MidiDriver_Casio::metaEvent(int8 source, byte type, byte *data, uint16 length) { + assert(source < MAXIMUM_SOURCES); + + if (type == 0x2F && source >= 0) // End of Track + deinitSource(source); + + _driver->metaEvent(type, data, length); +} + +int8 MidiDriver_Casio::mapSourceChannel(uint8 source, uint8 dataChannel) { + // TODO Multisource functionality has not been fully implemented. Current + // implementation assumes 1 source with access to all channels. + return dataChannel; +} + +void MidiDriver_Casio::processEvent(int8 source, uint32 b, uint8 outputChannel) { + assert(source < MAXIMUM_SOURCES); + + byte command = b & 0xF0; + byte op1 = (b >> 8) & 0xFF; + byte op2 = (b >> 16) & 0xFF; + + // The only commands supported by the Casio devices are note on, note off + // and program change. + switch (command) { + case MIDI_COMMAND_NOTE_OFF: + noteOff(outputChannel, command, op1, op2, source); + break; + case MIDI_COMMAND_NOTE_ON: + noteOn(outputChannel, op1, op2, source); + break; + case MIDI_COMMAND_PROGRAM_CHANGE: + programChange(outputChannel, op1, source); + break; + default: + warning("MidiDriver_Casio::processEvent - Received unsupported event %02x", command); + break; + } +} + +void MidiDriver_Casio::noteOff(byte outputChannel, byte command, byte note, byte velocity, int8 source) { + // Apply rhythm note mapping. + int8 mappedNote = mapNote(outputChannel, note); + if (mappedNote < 0) + // Rhythm note with no Casio equivalent. + return; + + _mutex.lock(); + + // Remove this note from the active note registry. + for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) { + if (_activeNotes[i].channel == outputChannel && _activeNotes[i].note == mappedNote && + _activeNotes[i].source == source) { + _activeNotes[i].clear(); + break; + } + } + + _mutex.unlock(); + + _driver->send(command | outputChannel, mappedNote, velocity); +} + +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; + } + + // Apply rhythm note mapping. + int8 mappedNote = mapNote(outputChannel, note); + if (mappedNote < 0) + // Rhythm note with no Casio equivalent. + return; + + _mutex.lock(); + + // Add this note to the active note registry. + for (int i = 0; i < ARRAYSIZE(_activeNotes); i++) { + if (_activeNotes[i].note == 0xFF) { + _activeNotes[i].channel = outputChannel; + _activeNotes[i].note = mappedNote; + _activeNotes[i].source = source; + break; + } + } + + _mutex.unlock(); + + byte calculatedVelocity = calculateVelocity(source, velocity); + + _driver->send(MIDI_COMMAND_NOTE_ON | outputChannel, mappedNote, calculatedVelocity); +} + +int8 MidiDriver_Casio::mapNote(byte outputChannel, byte note) { + int8 mappedNote = note; + + if (_rhythmNoteRemapping && isRhythmChannel(outputChannel)) { + mappedNote = _rhythmNoteRemapping[note]; + if (mappedNote == 0) + mappedNote = -1; + } + + return mappedNote; +} + +byte MidiDriver_Casio::calculateVelocity(int8 source, byte velocity) { + byte calculatedVelocity = velocity; + // Apply volume settings to velocity. + if (source >= 0) { + // Scale to source volume. + calculatedVelocity = (velocity * _sources[source].volume) / _sources[source].neutralVolume; + } + if (_userVolumeScaling) { + if (_userMute) { + calculatedVelocity = 0; + } else { + // Scale to user volume. + uint16 userVolume = _sources[source].type == SOURCE_TYPE_SFX ? _userSfxVolume : _userMusicVolume; + calculatedVelocity = (calculatedVelocity * userVolume) >> 8; + } + } + // Source volume scaling might clip volume, so reduce to maximum, + return MIN(calculatedVelocity, static_cast(0x7F)); +} + +void MidiDriver_Casio::programChange(byte outputChannel, byte patchId, int8 source) { + if (outputChannel < 4) + // Register the new instrument. + _instruments[outputChannel] = patchId; + + // Apply instrument mapping. + byte mappedInstrument = mapInstrument(patchId); + + _driver->send(MIDI_COMMAND_PROGRAM_CHANGE | outputChannel | (mappedInstrument << 8)); +} + +byte MidiDriver_Casio::mapInstrument(byte program) { + byte mappedInstrument = program; + + if (_instrumentRemapping) + // Apply custom instrument mapping. + mappedInstrument = _instrumentRemapping[program]; + + // Apply MT-540 <> CT-460 instrument mapping if necessary. + if (mappedInstrument < ARRAYSIZE(INSTRUMENT_REMAPPING_MT540_TO_CT460)) { + if (_midiType == MT_MT540 && _deviceType == MT_CT460) { + mappedInstrument = INSTRUMENT_REMAPPING_MT540_TO_CT460[mappedInstrument]; + } else if (_midiType == MT_CT460 && _deviceType == MT_MT540) { + mappedInstrument = INSTRUMENT_REMAPPING_CT460_TO_MT540[mappedInstrument]; + } + } + + return mappedInstrument; +} + +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; +} + +void MidiDriver_Casio::stopAllNotes(bool stopSustainedNotes) { + stopAllNotes(0xFF, 0xFF); +} + +void MidiDriver_Casio::stopAllNotes(uint8 source, uint8 channel) { + _mutex.lock(); + + // 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); + } + } + + _mutex.unlock(); +} + +void MidiDriver_Casio::applySourceVolume(uint8 source) { + // Because the Casio devices do not support the volume controller, source + // volume is applied to note velocity and it cannot be applied directly. +} + +MidiChannel *MidiDriver_Casio::allocateChannel() { + // MidiChannel objects are not supported by this driver. + return nullptr; +} + +MidiChannel *MidiDriver_Casio::getPercussionChannel() { + // MidiChannel objects are not supported by this driver. + return nullptr; +} + +uint32 MidiDriver_Casio::getBaseTempo() { + if (_driver) { + return _driver->getBaseTempo(); + } + return 0; +} + +void MidiDriver_Casio::timerCallback(void *data) { + MidiDriver_Casio *driver = static_cast(data); + driver->onTimer(); +} diff --git a/audio/casio.h b/audio/casio.h new file mode 100644 index 00000000000..81edee81888 --- /dev/null +++ b/audio/casio.h @@ -0,0 +1,243 @@ +/* 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 . + * + */ + +#ifndef AUDIO_CASIO_H +#define AUDIO_CASIO_H + +#include "audio/mididrv.h" +#include "audio/mididrv_ms.h" + +/** + * MIDI driver implementation for the Casio MT-540, CT-640 and CSM-1 devices. + * + * This driver provides source volume and user volume scaling, as well as + * fades (due to device limitations these are applied to note velocity instead + * of the volume controller). It also provides instrument mapping between the + * MT-540 and CT-640/CSM-1 instrument map and can map rhythm notes from the + * input MIDI data by specifying a remapping. + * + * TODO This driver does not provide a full multisource functionality + * implementation because this was not needed for the game for which it was + * added (Elvira). It assumes only 1 source sends MIDI data at the same time + * and this source has access to all MIDI channels. + * + * Some details of these Casio devices: + * - They seem to support only note on, note off and program change MIDI + * events. Because they do not support the volume controller, volume is + * applied to note velocity (and I'm not sure if they support even that...). + * All Notes Off is performed by keeping track of active notes and sending + * note off events for all active notes. + * - They only use MIDI channels 0-3. The MT-32 and GM devices have channel 9 + * as the fixed rhythm channel. The Casio devices can switch any used channel + * to a rhythm channel by setting a specific instrument. + * - They have only 30 instruments, 3 of which are rhythm or SFX banks. + * - All devices seem to have the same capabilities, but the instrument + * numbering is different between the MT-540 on the one hand and the CT-640 + * and CSM-1 on the other. + */ +class MidiDriver_Casio : public MidiDriver_Multisource { +protected: + // Tracks a note currently playing on the device. + struct ActiveNote { + // The source that played the note (0x7F if no note is tracked). + int8 source; + // The output MIDI channel on which the note is playing + // (0xFF if no note is tracked). + uint8 channel; + // The MIDI note number of the playing note + // (0xFF if no note is tracked). + uint8 note; + + ActiveNote(); + + // Sets the struct to values indicating no note is currently tracked. + void clear(); + }; + +public: + // 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. + static const uint8 INSTRUMENT_REMAPPING_MT540_TO_CT460[30]; + + // The instrument number used for rhythm sounds on the MT-540. + static const uint8 RHYTHM_INSTRUMENT_MT540; + // The instrument number used for rhythm sounds on the CT-460 and CSM-1. + static const uint8 RHYTHM_INSTRUMENT_CT460; + + /** + * Constructs a new Casio MidiDriver instance. + * + * @param midiType The type of MIDI data that will be sent to the driver + * (MT-540 or CT-460/CSM-1). + */ + MidiDriver_Casio(MusicType midiType); + ~MidiDriver_Casio(); + + int open() override; + /** + * Opens the driver wrapping the specified MidiDriver instance. + * + * @param driver The driver that will receive MIDI events from this driver. + * @param deviceType The type of MIDI device that will receive MIDI events + * from this driver (MT-540 or CT-460/CSM-1). + * @return 0 if the driver was opened successfully; >0 if an error occured. + */ + virtual int open(MidiDriver *driver, MusicType deviceType); + void close() override; + bool isOpen() const override; + + using MidiDriver_BASE::send; + void send(int8 source, uint32 b) override; + void metaEvent(int8 source, byte type, byte *data, uint16 length) override; + + void stopAllNotes(bool stopSustainedNotes = false) override; + MidiChannel *allocateChannel() override; + MidiChannel *getPercussionChannel() override; + uint32 getBaseTempo() override; + +protected: + /** + * Maps a data MIDI channel to an output MIDI channel for the specified + * source. + * TODO This driver has no default implementation for a channel allocation + * scheme. It assumes only one source is active at a time and has access to + * all output channels. The default implementation for this method just + * returns the data channel. + * + * @param source The source for which the MIDI channel should be mapped. + * @param dataChannel The data channel that should be mapped. + * @return The output MIDI channel. + */ + virtual int8 mapSourceChannel(uint8 source, uint8 dataChannel); + /** + * Processes a MIDI event. + * + * @param source The source sending the MIDI event. + * @param b The MIDI event data. + * @param outputChannel The MIDI channel on which the event should be sent. + */ + virtual void processEvent(int8 source, uint32 b, uint8 outputChannel); + /** + * Processes a MIDI note off event. + * + * @param outputChannel The MIDI channel on which the event should be sent. + * @param command The MIDI command that triggered the note off event (other + * than note off (0x80) this can also be note on (0x90) with velocity 0). + * @param note The MIDI note that should be turned off. + * @param velocity The note off velocity. + * @param source The source sending the MIDI event. + */ + virtual void noteOff(byte outputChannel, byte command, byte note, byte velocity, int8 source); + /** + * Processes a MIDI note on event. + * + * @param outputChannel The MIDI channel on which the event should be sent. + * @param note The MIDI note that should be turned on. + * @param velocity The note velocity, + * @param source The source sending the MIDI event. + */ + virtual void noteOn(byte outputChannel, byte note, byte velocity, int8 source); + /** + * Processes a MIDI program change event. + * + * @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. + */ + virtual void programChange(byte outputChannel, byte patchId, int8 source); + + /** + * Maps the specified note to a different note according to the rhythm note + * mapping. This mapping is only applied if the note is played on a rhythm + * channel. + * + * @param outputChannel The MIDI channel on which the note is/will be + * active. + * @param note The note that should be mapped. + * @return The mapped note, or the specified note if it was not mapped. + */ + virtual int8 mapNote(byte outputChannel, byte note); + /** + * Calculates the velocity for a note on event. This applies source volume + * and user volume settings to the specified velocity value. + * + * @param source The source that sent the note on event. + * @param velocity The velocity specified in the note on event. + * @return The calculated velocity. + */ + virtual byte calculateVelocity(int8 source, byte velocity); + /** + * 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. + * + * @param program The instrument that should be mapped. + * @return The mapped instrument, or the specified instrument if no mapping + * was necessary. + */ + virtual byte mapInstrument(byte program); + /** + * 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 + * a channel to a specific instrument (see the rhythm instrument constants). + * + * @param outputChannel The channel that should be checked. + * @return True if the specified channel is a rhythm channel, false + * otherwise. + */ + 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; + void stopAllNotes(uint8 source, uint8 channel) override; + + // The wrapped MIDI driver. + MidiDriver *_driver; + // The type of Casio device accessed by the wrapped driver: MT-540 or + // CT-460/CSM-1. + MusicType _deviceType; + // The type of MIDI data supplied to the driver: MT-540 or CT-460/CSM-1. + MusicType _midiType; + // True if this MIDI driver has been opened. + bool _isOpen; + + // The current instrument on each MIDI output channel. This is the + // instrument number as specified in the program change events (before + // remapping is applied). + byte _instruments[4]; + // Tracks the notes currently active on the MIDI device. + ActiveNote _activeNotes[32]; + + // 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; + + // Mutex for operations on active notes. + Common::Mutex _mutex; + +public: + static void timerCallback(void *data); +}; + +#endif diff --git a/audio/mididrv.h b/audio/mididrv.h index 275dd7b1235..243483fad84 100644 --- a/audio/mididrv.h +++ b/audio/mididrv.h @@ -57,7 +57,9 @@ enum MusicType { MT_SEGACD, // SegaCD MT_GM, // General MIDI MT_MT32, // MT-32 - MT_GS // Roland GS + MT_GS, // Roland GS + MT_MT540, // Casio MT-540 + MT_CT460 // Casio CT-460 / CSM-1 }; /** diff --git a/audio/module.mk b/audio/module.mk index b2a413f6e44..8519db4438c 100644 --- a/audio/module.mk +++ b/audio/module.mk @@ -4,6 +4,7 @@ MODULE_OBJS := \ adlib.o \ adlib_ms.o \ audiostream.o \ + casio.o \ fmopl.o \ mididrv.o \ mididrv_ms.o \