scummvm/audio/miles.h
NMIError 0af83d3faa
KYRA/MIDI: Fix minor MT-32/GM issues (#2333)
* KYRA: Added GM initialization for Lands of Lore

The original interpreter of Lands of Lore uses two executables, which
both do initialization of MIDI devices. The main executable runs a
sysex file for GM devices; the intro executable does not.

ScummVM only does MIDI initialization once and it performs this like
the intro executable. I've added the GM initialization of the main
executable. Note that the initialization file consists mostly of
sysexes which alter the display of the SC-55, so it's not particulary
useful. Not many games did this though, so I thought it would be a
fun feature to include.

I also noticed that the check which distinguishes between the two
demo versions of LoL did not work properly; the useAltShapeHeader
flag was true for both versions. I've changed it to check for the
existence of a PAK file which was only included with one of the two
demos.

* MIDI: Delay parser after handling SysEx events

This changes the way delays between SysEx events in MIDI data are handled from
delaying the backend to delaying the MidiParser.

SysEx events require a delay between events to ensure correct processing by the
MIDI device. This is usually implemented by calling OSystem::delayMillis.
Some games use some form of MIDI sequence filled with SysEx messages to
initialize the MIDI device. This is handled by the MidiParser. Using the
delayMillis method causes the following MIDI events to be "bunched up" and be
executed in a shorter timespan than intended.

I've altered this by making the MidiParser stop parsing when a SysEx event is
encountered and not enough time has passed since the last SysEx. After enough
time has passed, the next SysEx is sent and parsing resumes. To facilitate
this, I've introduced an alternate sysExNoDelay fuction on the MidiDiver. This
does not execute the delay itself, but instead returns the required delay time,
so the parser can handle this instead.

I've currently only implemented this method for the Miles MT-32/GM driver. For
other driver implementations, this will call the regular sysEx method and
return a 0 delay time, so there will be no change in behavior.

This restores a sound effect at the end of the Legend of Kyrandia MT-32 MIDI
initialization. Before, the Note On and Note Off events would be transmitted
instantly after each other, causing the notes not to play.

* MIDI: Add instrument bank fallback

Some games rely on a feature of the Roland SC-55 v1.xx that corrects invalid
instrument banks. This feature was removed in later versions of the device and
not implemented in most other GM devices, causing some MIDI data (sound effects
mostly) to play incorrectly.

Specifically, this applies to Lands of Lore. For example, in the intro, the
sound effect of the ring sparkling uses an incorrect instrument bank. Depending
on how the MIDI device handles this, the sound will play correctly, with the
wrong instrument, or not at all.

This commit emulates the SC-55 functionality that corrects the invalid bank
numbers. I've implemented this (partially) on the MidiDriver, so that it can be
re-used for other games (Xeen 4 and 5 also have this issue with some sound
effects).

* KYRA: Start MIDI playback after selecting track

The MIDI parser would automatically start playback after loading MIDI data, but
KYRA engine games use MIDI files with multiple tracks. Because of this it was
necessary to immediately stop playback after loading MIDI data, and then select
the track that should be played for correct playback. The parser now has a
feature to disable automatically starting playback, so I've implemented this
for KYRA.

* KYRA: Improve stopping MIDI playback

In two places All Notes Off events were sent on all channels to stop MIDI
playback of the background music. However, this will also cut off any MIDI
sound effects which are playing. Where needed I've replaced this with simply
stopping playback of the background music; the parser will turn off any active
notes.
I've also made sure playback is stopped before freeing the music data
memory to prevent any issues.
I've added sending All Notes Off events when the game quits, just in case any
hanging notes are not ended by the parsers (should not really be a problem
though).

* MIDI/KYRA: Add pausing playback to MIDI parser

* KYRA: Fix invalid track selection

If a game would attempt to play an invalid track, the parser would start
playing the previously selected track. Fixed this so the parser will not start
playing.
2020-06-20 23:27:30 +02:00

426 lines
14 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 2
* 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#ifndef AUDIO_MILES_MIDIDRIVER_H
#define AUDIO_MILES_MIDIDRIVER_H
#include "audio/mididrv.h"
#include "common/error.h"
#include "common/mutex.h"
#include "common/stream.h"
namespace Audio {
#define MILES_MIDI_CHANNEL_COUNT 16
#define MILES_RHYTHM_CHANNEL 9
// Miles Audio supported controllers for control change messages
#define MILES_CONTROLLER_SELECT_PATCH_BANK 114
#define MILES_CONTROLLER_PROTECT_VOICE 112
#define MILES_CONTROLLER_PROTECT_TIMBRE 113
#define MILES_CONTROLLER_LOCK_CHANNEL 110
#define MILES_CONTROLLER_PROTECT_CHANNEL 111
#define MILES_CONTROLLER_BANK_SELECT_MSB 0
#define MILES_CONTROLLER_BANK_SELECT_LSB 32
#define MILES_CONTROLLER_MODULATION 1
#define MILES_CONTROLLER_VOLUME 7
#define MILES_CONTROLLER_EXPRESSION 11
#define MILES_CONTROLLER_PANNING 10
#define MILES_CONTROLLER_SUSTAIN 64
#define MILES_CONTROLLER_PITCH_RANGE 6
#define MILES_CONTROLLER_RESET_ALL 121
#define MILES_CONTROLLER_ALL_NOTES_OFF 123
#define MILES_CONTROLLER_OMNI_ON 124
#define MILES_CONTROLLER_OMNI_OFF 125
#define MILES_CONTROLLER_MONO_ON 126
#define MILES_CONTROLLER_POLY_ON 127
#define MILES_CONTROLLER_PATCH_REVERB 59
#define MILES_CONTROLLER_PATCH_BENDER 60
#define MILES_CONTROLLER_REVERB_MODE 61
#define MILES_CONTROLLER_REVERB_TIME 62
#define MILES_CONTROLLER_REVERB_LEVEL 63
#define MILES_CONTROLLER_RHYTHM_KEY_TIMBRE 58
// 3 SysEx controllers, each range 5
// 32-36 for 1st queue
// 37-41 for 2nd queue
// 42-46 for 3rd queue
#define MILES_CONTROLLER_SYSEX_RANGE_BEGIN 32
#define MILES_CONTROLLER_SYSEX_RANGE_END 46
#define MILES_CONTROLLER_SYSEX_QUEUE_COUNT 3
#define MILES_CONTROLLER_SYSEX_QUEUE_SIZE 32
#define MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS1 0
#define MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS2 1
#define MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS3 2
#define MILES_CONTROLLER_SYSEX_COMMAND_DATA 3
#define MILES_CONTROLLER_SYSEX_COMMAND_SEND 4
#define MILES_CONTROLLER_XMIDI_RANGE_BEGIN 110
#define MILES_CONTROLLER_XMIDI_RANGE_END 120
// Miles Audio actually used 0x4000, because they didn't shift the 2 bytes properly
#define MILES_PITCHBENDER_DEFAULT 0x2000
// The maximum number of sources sending MIDI data to this driver.
// This is based on the requirements of the KYRA engine, but can be increased if
// necessary.
#define MILES_MAXIMUM_SOURCES 4
// Maximum number of tracked active notes for the MT-32
// This is the maximum polyphony of the MT-32 plus some overhead (MIDI data may send
// more notes than the MT-32 can handle simultaneously).
#define MILES_MT32_ACTIVE_NOTES 48
// Maximum number of tracked active notes for GM
// This is the maximum polyphony of the SC-88 and AWE64 plus some overhead (MIDI data
// may send more notes than the GM device can handle simultaneously).
#define MILES_GM_ACTIVE_NOTES 96
#define MILES_MT32_PATCHES_COUNT 128
#define MILES_MT32_CUSTOMTIMBRE_COUNT 64
#define MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE 14
#define MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE 58
#define MILES_MT32_PATCHDATA_PARTIALPARAMETERS_COUNT 4
#define MILES_MT32_PATCHDATA_TOTAL_SIZE (MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE + (MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE * MILES_MT32_PATCHDATA_PARTIALPARAMETERS_COUNT))
struct MilesMT32InstrumentEntry {
byte bankId;
byte patchId;
byte commonParameter[MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE + 1];
byte partialParameters[MILES_MT32_PATCHDATA_PARTIALPARAMETERS_COUNT][MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE + 1];
};
class MidiDriver_Miles_Midi : public MidiDriver {
public:
MidiDriver_Miles_Midi(MusicType midiType, MilesMT32InstrumentEntry *instrumentTablePtr, uint16 instrumentTableCount);
virtual ~MidiDriver_Miles_Midi();
public:
// MidiDriver
int open() override;
// Open the Miles driver using the specified MidiDriver instance.
int open(MidiDriver *driver, bool nativeMT32);
void close() override;
bool isOpen() const override { return _isOpen; }
using MidiDriver_BASE::send;
void send(uint32 b) override;
void send(int8 source, uint32 b) override;
void sysEx(const byte *msg, uint16 length) override;
uint16 sysExNoDelay(const byte *msg, uint16 length) override;
void metaEvent(int8 source, byte type, byte *data, uint16 length) override;
/**
* De-initialize a source. Call this after playing a track or sound effect using this source.
* This will unlock and unprotect channels used by this source and stop any active notes
* from this source.
* Automatically executed when an End Of Track meta event is received.
*/
void deinitSource(uint8 source);
/**
* Set the volume for this source. This will be used to scale the volume values in the MIDI
* data from this source. Expected volume values are 0 - 256.
* Note that source volume remains set for the source number even after deinitializing the
* source. If the same source numbers are consistently used for music and SFX sources, the
* source volume will only need to be set once.
*/
void setSourceVolume(uint8 source, uint16 volume);
/** Stops all notes currently playing on the MIDI device. */
void allNotesOff();
MidiChannel *allocateChannel() override {
if (_driver)
return _driver->allocateChannel();
return NULL;
}
MidiChannel *getPercussionChannel() override {
if (_driver)
return _driver->getPercussionChannel();
return NULL;
}
void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override {
if (_driver)
_driver->setTimerCallback(timer_param, timer_proc);
}
uint32 getBaseTempo() override {
if (_driver) {
return _driver->getBaseTempo();
}
return 1000000 / _baseFreq;
}
protected:
Common::Mutex _mutex;
MidiDriver *_driver;
bool _isOpen;
// The type of MIDI data supplied to the driver: MT-32 or General MIDI.
MusicType _midiType;
// True if the MIDI output is an MT-32 (hardware or 100% emulated),
// false if the MIDI output is a General MIDI device.
bool _nativeMT32;
// True if the General MIDI output supports Roland GS for improved MT-32 mapping.
bool _enableGS;
// Bitmask of the MIDI channels in use by the output device
uint16 _outputChannelMask;
int _baseFreq;
public:
void processXMIDITimbreChunk(const byte *timbreListPtr, uint32 timbreListSize);
private:
void initMidiDevice();
void MT32SysEx(const uint32 targetAddress, const byte *dataPtr);
uint32 calculateSysExTargetAddress(uint32 baseAddress, uint32 index);
void writeRhythmSetup(byte note, byte customTimbreId);
void writePatchTimbre(byte patchId, byte timbreGroup, byte timbreId);
void writePatchByte(byte patchId, byte index, byte patchValue);
void writeToSystemArea(byte index, byte value);
const MilesMT32InstrumentEntry *searchCustomInstrument(byte patchBank, byte patchId);
int16 searchCustomTimbre(byte patchBank, byte patchId);
void setupPatch(byte patchBank, byte patchId);
int16 installCustomTimbre(byte patchBank, byte patchId);
bool isOutputChannelUsed(uint8 outputChannel) { return _outputChannelMask & (1 << outputChannel); }
private:
/**
* This stores the values of the MIDI controllers for
* a MIDI channel. It is used to keep track of controller
* values while a channel is locked, so they can be
* restored when the channel is unlocked.
*/
struct MidiChannelControlData {
// The source that last sent an event to this channel
int8 source;
// True if the source volume has been applied to this channel
bool sourceVolumeApplied;
byte program;
uint16 pitchWheel;
byte modulation;
// The volume specified by the MIDI data
byte volume;
// The volume scaled using the source volume
byte scaledVolume;
byte panPosition;
byte expression;
bool sustain;
// Custom timbre data
byte currentPatchBank;
bool usingCustomTimbre;
byte currentCustomTimbreId;
MidiChannelControlData() : source(-1),
sourceVolumeApplied(false),
program(0),
pitchWheel(MILES_PITCHBENDER_DEFAULT),
modulation(0),
volume(0xFF),
scaledVolume(0x64),
panPosition(0x40),
expression(0x7F),
sustain(false),
currentPatchBank(0),
usingCustomTimbre(false),
currentCustomTimbreId(0) { }
};
struct MidiChannelEntry {
// True if this channel is locked. A locked channel will
// only accept MIDI messages from the source that locked it.
bool locked;
// The channel in the MIDI data of the lock source that
// is assigned to this locked output channel. This is a
// reverse lookup for MidiSource::channelMap.
// -1 if the channel is not locked.
int8 lockDataChannel;
// True if this channel is protected from locking.
// The channel can still be locked, but unprotected
// channels will be prioritized.
bool lockProtected;
// The source that protected this channel from locking.
// -1 if the channel is not protected.
int8 protectedSource;
// The number of notes currently active on the channel.
uint8 activeNotes;
// The MIDI controller values currently used by the channel.
MidiChannelControlData currentData;
// The MIDI controller values set by the sources which are
// not currently using the channel because it is locked.
// These values will be set on the channel when the channel
// is unlocked.
MidiChannelControlData unlockData;
MidiChannelEntry() : locked(false),
lockDataChannel(-1),
lockProtected(false),
protectedSource(-1),
activeNotes(0) { }
};
/**
* Send out a control change MIDI message using the specified data.
* @param controlData The new MIDI controller value will be set on this MidiChannelControlData
* @param sendMessage True if the message should be sent out to the device
*/
void controlChange(byte outputChannel, byte controllerNumber, byte controllerValue, int8 source, MidiChannelControlData &controlData, bool sendMessage);
/**
* Removes active notes from the active notes registration on the specified channel.
* @param sustainedNotes True if only sustained notes should be removed; otherwise only regular active notes will be removed
*/
void removeActiveNotes(uint8 outputChannel, bool sustainedNotes);
/**
* Find and lock an output channel and reserve it for the specified
* source. The output channel will be mapped to the specified data
* channel.
*/
void lockChannel(uint8 source, uint8 dataChannel);
/**
* Find an output channel to lock. This will be based on the number
* of active notes on the channels and whether the channel is
* protected or not.
* @param useProtectedChannels When true, protected channels are considered for locking
* @returns The output channel to lock, or -1 if no channel is available
*/
int8 findLockChannel(bool useProtectedChannels = false);
/**
* Unlock an output channel. This will stop all notes on the channel,
* restore the controller values and make it available to other sources.
*/
void unlockChannel(uint8 outputChannel);
/**
* Send a program change MIDI message using the specified data.
* @param controlData The new program value will be set on this MidiChannelControlData
* @param sendMessage True if the message should be sent out to the device
*/
void programChange(byte outputChannel, byte patchId, uint8 source, MidiChannelControlData &controlData, bool sendMessage);
void stopNotesOnChannel(uint8 outputChannelNumber);
struct MidiCustomTimbreEntry {
bool used;
bool protectionEnabled;
byte currentPatchBank;
byte currentPatchId;
uint32 lastUsedNoteCounter;
MidiCustomTimbreEntry() : used(false),
protectionEnabled(false),
currentPatchBank(0),
currentPatchId(0),
lastUsedNoteCounter(0) {}
};
struct MilesMT32SysExQueueEntry {
uint32 targetAddress;
byte dataPos;
byte data[MILES_CONTROLLER_SYSEX_QUEUE_SIZE + 1]; // 1 extra byte for terminator
MilesMT32SysExQueueEntry() : targetAddress(0),
dataPos(0) {
memset(data, 0, sizeof(data));
}
};
/**
* This stores data about a specific source of MIDI data.
*/
struct MidiSource {
// The source volume as set by ScummVM (music/SFX volume)
uint16 volume;
// The mapping of MIDI data channels to output channels
// for this source.
int8 channelMap[MILES_MIDI_CHANNEL_COUNT];
MidiSource() : volume(256) {
memset(channelMap, 0, sizeof(channelMap));
}
};
// stores information about all MIDI channels
MidiChannelEntry _midiChannels[MILES_MIDI_CHANNEL_COUNT];
// stores information about all custom timbres
MidiCustomTimbreEntry _customTimbres[MILES_MT32_CUSTOMTIMBRE_COUNT];
byte _patchesBank[MILES_MT32_PATCHES_COUNT];
// holds all instruments
MilesMT32InstrumentEntry *_instrumentTablePtr;
uint16 _instrumentTableCount;
uint32 _noteCounter; // used to figure out, which timbres are outdated
// SysEx Queues
MilesMT32SysExQueueEntry _sysExQueues[MILES_CONTROLLER_SYSEX_QUEUE_COUNT];
// MIDI sources sending messages to this driver.
MidiSource _sources[MILES_MAXIMUM_SOURCES];
struct ActiveNote {
int8 source;
uint8 channel;
uint8 note;
bool sustain;
ActiveNote() : source(0x7F),
channel(0xFF),
note(0xFF),
sustain(false) { }
};
// The maximum number of active notes that have to be tracked for this MIDI device.
uint8 _maximumActiveNotes;
// Tracks the notes being played by the MIDI device.
ActiveNote *_activeNotes;
};
extern MidiDriver *MidiDriver_Miles_AdLib_create(const Common::String &filenameAdLib, const Common::String &filenameOPL3, Common::SeekableReadStream *streamAdLib = nullptr, Common::SeekableReadStream *streamOPL3 = nullptr);
extern MidiDriver_Miles_Midi *MidiDriver_Miles_MT32_create(const Common::String &instrumentDataFilename);
extern MidiDriver_Miles_Midi *MidiDriver_Miles_MIDI_create(MusicType midiType, const Common::String &instrumentDataFilename);
extern void MidiDriver_Miles_MT32_processXMIDITimbreChunk(MidiDriver_BASE *driver, const byte *timbreListPtr, uint32 timbreListSize);
} // End of namespace Audio
#endif // AUDIO_MILES_MIDIDRIVER_H