diff --git a/engines/scumm/imuse/drivers/gmidi.cpp b/engines/scumm/imuse/drivers/gmidi.cpp index d6e8749ab0e..3a8b769029c 100644 --- a/engines/scumm/imuse/drivers/gmidi.cpp +++ b/engines/scumm/imuse/drivers/gmidi.cpp @@ -23,14 +23,328 @@ #include "common/system.h" #include "scumm/imuse/drivers/gmidi.h" + +// This makes older titles play new system style, with 16 virtual channels and +// dynamic allocation (instead of playing on fixed channels). +//#define FORCE_NEWSTYLE_CHANNEL_ALLOCATION + namespace Scumm { -IMuseDriver_GMidi::IMuseDriver_GMidi(MidiDriver::DeviceHandle dev, bool rolandGSMode, bool newSystem) : MidiDriver(), _drv(nullptr), _gsMode(rolandGSMode), _newSystem(newSystem) { +class IMuseChannel_GMidi : public MidiChannel { +public: + IMuseChannel_GMidi(IMuseDriver_GMidi *drv, int number); + ~IMuseChannel_GMidi() override {} + + MidiDriver *device() override { return _drv; } + byte getNumber() override { return _number; } + + bool allocate(); + void release() override { _allocated = false; } + + void send(uint32 b) override { if (_drv) _drv->send((b & ~0x0F) | _number); } + + // Regular messages + void noteOff(byte note) override; + void noteOn(byte note, byte velocity) override; + void controlChange(byte control, byte value) override; + void programChange(byte program) override; + void pitchBend(int16 bend) override; + + // Control Change and SCUMM specific functions + void pitchBendFactor(byte value) override { _pitchBendSensitivity = value; } + void transpose(int8 value) override { _transpose = value; } + void detune(byte value) override { _detune = value; } + void priority(byte value) override { _prio = value; } + void sustain(bool value) override; + void allNotesOff() override; + void sysEx_customInstrument(uint32 type, const byte *instr, uint32 dataSize) override {} + +private: + void noteOffIntern(byte note); + void noteOnIntern(byte note, byte velocity); + + void setNotePlaying(byte note) { _drv->setNoteFlag(_number, note); } + void clearNotePlaying(byte note) { _drv->clearNoteFlag(_number, note); } + bool isNotePlaying(byte note) const { return _drv->queryNoteFlag(_number, note); } + void setNoteSustained(byte note) { _drv->setSustainFlag(_number, note); } + void clearNoteSustained(byte note) { _drv->clearSustainFlag(_number, note); } + bool isNoteSustained(byte note) const { return _drv->querySustainFlag(_number, note); } + + IMuseDriver_GMidi *_drv; + const byte _number; + const bool _newSystem; + bool _allocated; + + byte _polyphony; + byte _channelUsage; + bool _exhaust; + byte _prio; + int8 _detune; + int8 _transpose; + byte _pitchBendSensitivity; + int16 _pitchBend; + bool _sustain; + + GMidiControlChan *&_idleChain; + GMidiControlChan *&_activeChain; +}; + +class GMidiControlChan { +public: + GMidiControlChan() : _prev(nullptr), _next(nullptr), _in(nullptr), _number(0), _note(0) {} + GMidiControlChan *_prev; + GMidiControlChan *_next; + IMuseChannel_GMidi *_in; + byte _number; + byte _note; +}; + +void connect(GMidiControlChan *&chain, GMidiControlChan *node) { + if (!node || node->_prev || node->_next) + return; + if ((node->_next = chain)) + chain->_prev = node; + chain = node; +} + +void disconnect(GMidiControlChan *&chain, GMidiControlChan *node) { + if (!node || !chain) + return; + + const GMidiControlChan *ch = chain; + while (ch && ch != node) + ch = ch->_next; + if (!ch) + return; + + if (node->_next) + node->_next->_prev = node->_prev; + + if (node->_prev) + node->_prev->_next = node->_next; + else + chain = node->_next; + + node->_next = node->_prev = nullptr; +} + +#define sendMidi(stat, par1, par2) _drv->send(((par2) << 16) | ((par1) << 8) | (stat)) + +IMuseChannel_GMidi::IMuseChannel_GMidi(IMuseDriver_GMidi *drv, int number) :MidiChannel(), _drv(drv), _number(number), _allocated(false), _sustain(false), + _pitchBend(0x2000), _polyphony(1), _channelUsage(0), _exhaust(false), _prio(0), _detune(0), _transpose(0), + _pitchBendSensitivity(2), _idleChain(_drv->_idleChain), _activeChain(_drv->_activeChain), _newSystem(_drv->_newSystem) { + assert(_drv); +} + +bool IMuseChannel_GMidi::allocate() { + if (_allocated) + return false; + + if (!_newSystem) { + _prio = 0x80; + } + + return (_allocated = true); +} + +void IMuseChannel_GMidi::noteOff(byte note) { + if (_newSystem) { + if (!isNotePlaying(note)) + return; + + clearNotePlaying(note); + if (_sustain) { + setNoteSustained(note); + return; + } + } + +#ifdef FORCE_NEWSTYLE_CHANNEL_ALLOCATION + noteOffIntern(note); +#else + if (_newSystem) + noteOffIntern(note); + else + sendMidi(0x80 | _number, note, 0x40); +#endif +} + +void IMuseChannel_GMidi::noteOn(byte note, byte velocity) { + if (_newSystem) { + if (isNotePlaying(note)) { + noteOffIntern(note); + } else if (isNoteSustained(note)) { + setNotePlaying(note); + clearNoteSustained(note); + noteOffIntern(note); + } else { + setNotePlaying(note); + } + } + +#ifdef FORCE_NEWSTYLE_CHANNEL_ALLOCATION + noteOnIntern(note, velocity); +#else + if (_newSystem) + noteOnIntern(note, velocity); + else + sendMidi(0x90 | _number, note, velocity); +#endif +} + +void IMuseChannel_GMidi::controlChange(byte control, byte value) { + switch (control) { + case 1: + case 7: + case 10: + case 91: + case 93: + sendMidi(0xB0 | _number, control, value); + break; + case 17: + if (_newSystem) + _polyphony = value; + else + detune(value); + break; + case 18: + priority(value); + break; + case 123: + allNotesOff(); + break; + default: + // The original driver does not pass through "blindly". The + // only controls that get sent are 1, 7, 10, 91 and 93. + warning("Unhandled Control: %d", control); + break; + } +} + +void IMuseChannel_GMidi::programChange(byte program) { + sendMidi(0xC0 | _number, program, 0); +} + +void IMuseChannel_GMidi::pitchBend(int16 bend) { + bend = bend + 0x2000; + sendMidi(0xE0 | _number, _pitchBend & 0x7F, (_pitchBend >> 7) & 0x7F); +} + +void IMuseChannel_GMidi::sustain(bool value) { + _sustain = value; + + if (_newSystem) { + // For SAMNMAX, this is fully software controlled. No control change message gets sent. + if (_sustain) + return; + + for (int i = 0; i < 128; ++i) { + if (isNoteSustained(i)) + noteOffIntern(i); + } + + } else { + sendMidi(0xB0 | _number, 0x40, value); + } +} + +void IMuseChannel_GMidi::allNotesOff() { + if (_newSystem) { + // For SAMNMAX, this is fully software controlled. No control change message gets sent. + if (_sustain) + return; + + for (int i = 0; i < 128; ++i) { + if (isNotePlaying(i)) { + noteOffIntern(i); + clearNotePlaying(i); + } else if (isNoteSustained(i)) { + noteOffIntern(i); + clearNoteSustained(i); + } + } + + } else { + sendMidi(0xB0 | _number, 0x7B, 0); + } +} + +void IMuseChannel_GMidi::noteOffIntern(byte note) { + if (_activeChain == nullptr) + return; + + GMidiControlChan *node = nullptr; + for (GMidiControlChan *i = _activeChain; i; i = i->_next) { + if (i->_number == _number && i->_note == note) { + node = i; + break; + } + } + + if (!node) + return; + + sendMidi(0x80 | _number, note, 0x40); + + if (_newSystem) + _exhaust = (--_channelUsage > _polyphony); + + disconnect(_activeChain, node); + connect(_idleChain, node); +} + +void IMuseChannel_GMidi::noteOnIntern(byte note, byte velocity) { + GMidiControlChan *node = nullptr; + + if (_idleChain) { + node = _idleChain; + disconnect(_idleChain, node); + } else { + IMuseChannel_GMidi *best = this; + for (GMidiControlChan *i = _activeChain; i; i = i->_next) { + assert (i->_in); + if ((best->_exhaust == i->_in->_exhaust && best->_prio >= i->_in->_prio) || (!best->_exhaust && i->_in->_exhaust)) { + best = i->_in; + node = i; + } + } + + if (!node) + return; + + IMuseChannel_GMidi *prt = _drv->_imsParts[node->_number]; + if (prt) { + sendMidi(0x80 | prt->_number, node->_note, 0x40); + if (_newSystem) + prt->_exhaust = (--prt->_channelUsage > prt->_polyphony); + } + + disconnect(_activeChain, node); + } + + assert(node); + node->_in = this; + node->_number = _number; + node->_note = note; + + connect(_activeChain, node); + + if (_newSystem) + _exhaust = (++_channelUsage > _polyphony); + + sendMidi(0x90 | _number, note, velocity); +} + +#undef sendMidi + +IMuseDriver_GMidi::IMuseDriver_GMidi(MidiDriver::DeviceHandle dev, bool rolandGSMode, bool newSystem) : MidiDriver(), _drv(nullptr), _gsMode(rolandGSMode), _imsParts(nullptr), + _newSystem(newSystem), _numChannels(16), _notesPlaying(nullptr), _notesSustained(nullptr), _controlChan(nullptr), _idleChain(nullptr), _activeChain(nullptr) { _drv = MidiDriver::createMidi(dev); assert(_drv); } IMuseDriver_GMidi::~IMuseDriver_GMidi() { + close(); delete _drv; } @@ -42,28 +356,37 @@ int IMuseDriver_GMidi::open() { if (res) return res; + createChannels(); + if (_gsMode) initDeviceAsRolandGS(); else - initDevice(); + initDevice(); return res; } void IMuseDriver_GMidi::close() { - if (isOpen() && _drv) + if (isOpen() && _drv) { + for (int i = 0; i < 16; ++i) { + send(0x0040B0 | i); + send(0x007BB0 | i); + } _drv->close(); + } - //releaseChannels(); + releaseChannels(); } MidiChannel *IMuseDriver_GMidi::allocateChannel() { if (!isOpen()) return nullptr; - // Pass through everything for now. - //if (!_newSystem) - return _drv->allocateChannel(); + for (int i = 0; i < _numChannels; ++i) { + IMuseChannel_GMidi *ch = _imsParts[i]; + if (ch && i != 9 && ch->allocate()) + return ch; + } return nullptr; } @@ -72,20 +395,13 @@ MidiChannel *IMuseDriver_GMidi::getPercussionChannel() { if (!isOpen()) return nullptr; - // Pass through everything for now. - //if (!_newSystem) - return _drv->getPercussionChannel(); - - return nullptr; + return _imsParts[9]; } void IMuseDriver_GMidi::initDevice() { - // These are the init messages from the DOTT General Midi - // driver. This is the major part of the bug fix for bug - // no. 13460 ("DOTT: Incorrect MIDI pitch bending"). - // SAMNMAX has some less of the default settings (since - // the driver works a bit different), but it uses the same - // values for the pitch bend range. + // These are the init messages from the DOTT General Midi driver. This is the major part of the bug fix for bug + // no. 13460 ("DOTT: Incorrect MIDI pitch bending"). SAMNMAX has some less of the default settings (since + // the driver works a bit different), but it uses the same values for the pitch bend range. for (int i = 0; i < 16; ++i) { send(0x0064B0 | i); send(0x0065B0 | i); @@ -209,4 +525,50 @@ void IMuseDriver_GMidi::initDeviceAsRolandGS() { debug(2, "GS SysEx: Reverb Time is 106"); } +void IMuseDriver_GMidi::createChannels() { + releaseChannels(); + + _imsParts = new IMuseChannel_GMidi*[_numChannels]; + _controlChan = new GMidiControlChan*[12]; + + assert(_imsParts); + assert(_controlChan); + + for (int i = 0; i < _numChannels; ++i) + _imsParts[i] = new IMuseChannel_GMidi(this, i); + + for (int i = 0; i < 12; ++i) { + _controlChan[i] = new GMidiControlChan(); + connect(_idleChain, _controlChan[i]); + } + + if (_newSystem) { + _notesPlaying = new uint16[128](); + _notesSustained = new uint16[128](); + } +} + +void IMuseDriver_GMidi::releaseChannels() { + if (_imsParts) { + for (int i = 0; i < _numChannels; ++i) + delete _imsParts[i]; + delete[] _imsParts; + _imsParts = nullptr; + } + + if (_controlChan) { + for (int i = 0; i < 12; ++i) + delete _controlChan[i]; + delete[] _controlChan; + _controlChan = nullptr; + } + + delete[] _notesPlaying; + _notesPlaying = nullptr; + delete[] _notesSustained; + _notesSustained = nullptr; +} + +#undef FORCE_NEWSTYLE_CHANNEL_ALLOCATION + } // End of namespace Scumm diff --git a/engines/scumm/imuse/drivers/gmidi.h b/engines/scumm/imuse/drivers/gmidi.h index 7f7dca52a5a..d2186b4725f 100644 --- a/engines/scumm/imuse/drivers/gmidi.h +++ b/engines/scumm/imuse/drivers/gmidi.h @@ -26,7 +26,11 @@ namespace Scumm { +class IMuseChannel_GMidi; +class GMidiControlChan; + class IMuseDriver_GMidi : public MidiDriver { + friend class IMuseChannel_GMidi; public: IMuseDriver_GMidi(MidiDriver::DeviceHandle dev, bool rolandGSMode, bool newSystem); ~IMuseDriver_GMidi() override; @@ -41,6 +45,7 @@ public: uint32 getBaseTempo() override { return _drv ? _drv->getBaseTempo() : 0; } void send(uint32 b) override { if (_drv) _drv->send(b); }; void sysEx(const byte *msg, uint16 length) override { if (_drv) _drv->sysEx(msg, length); } + void setPitchBendRange(byte channel, uint range) override { if (_drv) _drv->setPitchBendRange(channel, range); } // Channel allocation functions MidiChannel *allocateChannel() override; @@ -49,10 +54,29 @@ public: private: void initDevice(); void initDeviceAsRolandGS(); + void createChannels(); + void releaseChannels(); + + void setNoteFlag(byte chan, byte note) { if (_notesPlaying && chan < 16 && note < 128) _notesPlaying[note] |= (1 << chan); } + void clearNoteFlag(byte chan, byte note) { if (_notesPlaying && chan < 16 && note < 128) _notesPlaying[note] &= ~(1 << chan); } + bool queryNoteFlag(byte chan, byte note) const { return (_notesPlaying && chan < 16 && note < 128) ? _notesPlaying[note] & (1 << chan) : false; } + void setSustainFlag(byte chan, byte note) { if (_notesSustained && chan < 16 && note < 128) _notesSustained[note] |= (1 << chan); } + void clearSustainFlag(byte chan, byte note) { if (_notesSustained && chan < 16 && note < 128) _notesSustained[note] &= ~(1 << chan); } + bool querySustainFlag(byte chan, byte note) const { return (_notesSustained && chan < 16 && note < 128) ? _notesSustained[note] & (1 << chan) : false; } MidiDriver *_drv; - const bool _gsMode; const bool _newSystem; + const bool _gsMode; + const byte _numChannels; + + IMuseChannel_GMidi **_imsParts; + GMidiControlChan **_controlChan; + + GMidiControlChan *_idleChain; + GMidiControlChan *_activeChain; + + uint16 *_notesPlaying; + uint16 *_notesSustained; }; } // End of namespace Scumm