mirror of
https://github.com/libretro/scummvm.git
synced 2024-12-04 16:26:53 +00:00
971 lines
35 KiB
C++
971 lines
35 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 "audio/miles.h"
|
|
|
|
#include "common/config-manager.h"
|
|
#include "common/file.h"
|
|
#include "common/mutex.h"
|
|
#include "common/system.h"
|
|
#include "common/textconsole.h"
|
|
|
|
namespace Audio {
|
|
|
|
// Miles Audio MT-32 / General MIDI driver
|
|
//
|
|
|
|
#define MILES_MT32_TIMBREBANK_STANDARD_ROLAND 0
|
|
#define MILES_MT32_TIMBREBANK_MELODIC_MODULE 127
|
|
|
|
const byte milesMT32SysExChansSetup[] = {
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09
|
|
};
|
|
|
|
const byte milesMT32SysExPartialReserveTable[] = {
|
|
0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x04
|
|
};
|
|
|
|
const byte milesMT32SysExInitReverb[] = {
|
|
0x00, 0x03, 0x02 // Reverb mode 0, reverb time 3, reverb level 2
|
|
};
|
|
|
|
MidiDriver_Miles_Midi::MidiDriver_Miles_Midi(MusicType midiType, MilesMT32InstrumentEntry *instrumentTablePtr, uint16 instrumentTableCount) :
|
|
MidiDriver_MT32GM(midiType), _noteCounter(0), _milesVersion(MILES_VERSION_2) {
|
|
memset(_patchesBank, 0, sizeof(_patchesBank));
|
|
|
|
_instrumentTablePtr = instrumentTablePtr;
|
|
_instrumentTableCount = instrumentTableCount;
|
|
|
|
setSourceNeutralVolume(MILES_DEFAULT_SOURCE_NEUTRAL_VOLUME);
|
|
setSourceVolume(MILES_DEFAULT_SOURCE_NEUTRAL_VOLUME);
|
|
}
|
|
|
|
MidiDriver_Miles_Midi::~MidiDriver_Miles_Midi() {
|
|
if (_instrumentTablePtr)
|
|
delete[] _instrumentTablePtr;
|
|
|
|
for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) {
|
|
if (_midiChannels[i].unlockData)
|
|
delete _midiChannels[i].unlockData;
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::initControlData() {
|
|
for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) {
|
|
_controlData[i] = _midiChannels[i].currentData = new MilesMidiChannelControlData();
|
|
_midiChannels[i].unlockData = new MilesMidiChannelControlData();
|
|
_controlData[i]->volume = _controlData[i]->scaledVolume =
|
|
(_nativeMT32 ? MT32_DEFAULT_CHANNEL_VOLUME : GM_DEFAULT_CHANNEL_VOLUME);
|
|
if (_nativeMT32 && i >= 1 && i <= 8) {
|
|
_midiChannels[i].currentData->program = MT32_DEFAULT_INSTRUMENTS[i - 1];
|
|
_midiChannels[i].currentData->panPosition = MT32_DEFAULT_PANNING[i - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::initMidiDevice() {
|
|
MidiDriver_MT32GM::initMidiDevice();
|
|
|
|
// Additional Miles AIL specific initialization
|
|
|
|
if (_midiType == MT_MT32 && _nativeMT32) {
|
|
// init part/channel assignments
|
|
sysExMT32(milesMT32SysExChansSetup, 9, (0x10 << 14) | (0x00 << 7) | 0x0D, false, true);
|
|
|
|
// partial reserve table
|
|
sysExMT32(milesMT32SysExPartialReserveTable, 9, (0x10 << 14) | (0x00 << 7) | 0x04, false, true);
|
|
|
|
// init reverb
|
|
sysExMT32(milesMT32SysExInitReverb, 3, (0x10 << 14) | (0x00 << 7) | 0x01, false, true);
|
|
}
|
|
|
|
// Set Miles default controller values
|
|
if (_milesVersion == MILES_VERSION_2) {
|
|
// Note that AIL/MSS apparently did not get full support for GM until
|
|
// version 3.00 in 09/1994. Many games used the MT-32 driver to
|
|
// implement GM support. As a result, default parameters were only sent
|
|
// out on the MT-32 channels (2-10). Also, the default MT-32 instrument
|
|
// numbers were set on GM devices, even though they map to different
|
|
// instruments. This is reproduced here to prevent possible issues with
|
|
// games that depend on this behavior.
|
|
|
|
for (int i = 1; i < 10; ++i) {
|
|
// Volume 7F (max)
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_VOLUME, 0x7F);
|
|
if (_midiType == MT_MT32) {
|
|
// Panning center - not the MT-32 default for all channels
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_PANNING, 0x40);
|
|
}
|
|
// Patch
|
|
if (_midiType == MT_GM && i != MIDI_RHYTHM_CHANNEL) {
|
|
// Send the MT-32 default instrument numbers out to GM devices.
|
|
send(-1, MIDI_COMMAND_PROGRAM_CHANGE | i, MT32_DEFAULT_INSTRUMENTS[i - 1], 0);
|
|
}
|
|
// The following settings are also sent out by the AIL driver:
|
|
// - Modulation 0
|
|
// - Expression 7F (max)
|
|
// - Sustain off
|
|
// - Pitch bend neutral
|
|
// These are the default MT-32 and GM settings, so it is not
|
|
// necessary to send these.
|
|
}
|
|
} else {
|
|
// MSS 3 initialization
|
|
for (int i = (_midiType == MT_GM ? 0 : 1); i < (_midiType == MT_GM ? MIDI_CHANNEL_COUNT : 10); ++i) {
|
|
// Patch
|
|
if (_midiType == MT_MT32 && i != MIDI_RHYTHM_CHANNEL) {
|
|
// Set instrument to 0.
|
|
send(-1, MIDI_COMMAND_PROGRAM_CHANGE | i, 0, 0);
|
|
}
|
|
// Volume 7F (max)
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_VOLUME, 0x7F);
|
|
if (_midiType == MT_MT32) {
|
|
// Panning center - not the MT-32 default for all channels
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_PANNING, 0x40);
|
|
}
|
|
if (_midiType == MT_GM) {
|
|
// Reverb 28h
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_REVERB, 0x28);
|
|
}
|
|
// Pitch bend range 2 semitones
|
|
// TODO Some games seem to initialize this to a different value, so
|
|
// this might need to be configurable.
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_RPN_LSB, 0x00);
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_RPN_MSB, 0x00);
|
|
if (_midiType == MT_GM) {
|
|
// MT-32 does not respond to the LSB, so only send it out for GM
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_DATA_ENTRY_LSB, 0x00);
|
|
}
|
|
send(-1, MIDI_COMMAND_CONTROL_CHANGE | i, MIDI_CONTROLLER_DATA_ENTRY_MSB, 0x02);
|
|
|
|
// MSS 3 also sets the following settings:
|
|
// - Program 0 (also for GM)
|
|
// - Pitch bend neutral
|
|
// - Modulation 0
|
|
// - Panning center (also for GM)
|
|
// - Expression 7F
|
|
// - Sustain off
|
|
// - Chorus 0
|
|
// These are the default settings, so it is not necessary to send
|
|
// these.
|
|
}
|
|
}
|
|
}
|
|
|
|
// MIDI messages can be found at https://web.archive.org/web/20120128110425/http://www.midi.org/techspecs/midimessages.php
|
|
void MidiDriver_Miles_Midi::send(int8 source, uint32 b) {
|
|
assert(source < MAXIMUM_SOURCES);
|
|
|
|
byte command = b & 0xf0;
|
|
byte dataChannel = b & 0xf;
|
|
byte op1 = (b >> 8) & 0xff;
|
|
byte op2 = (b >> 16) & 0xff;
|
|
byte outputChannel = source < 0 ? dataChannel : _channelMap[source][dataChannel];
|
|
|
|
MidiChannelEntry &outputChannelEntry = _midiChannels[outputChannel];
|
|
// Only send the message to the MIDI device if the channel is not locked or
|
|
// if the source that locked the channel is sending the message
|
|
bool channelLockedByOtherSource = source >= 0 && outputChannelEntry.locked &&
|
|
outputChannelEntry.currentData->source != source;
|
|
// Track controller changes on the current data if the MIDI message is sent out,
|
|
// or on the unlock data otherwise.
|
|
MilesMidiChannelControlData &controlData = channelLockedByOtherSource ?
|
|
*outputChannelEntry.unlockData : *outputChannelEntry.currentData;
|
|
|
|
if (command == MIDI_COMMAND_CONTROL_CHANGE && op1 == MILES_CONTROLLER_LOCK_CHANNEL) {
|
|
// The lock channel controller will allocate an output channel to use
|
|
// to send the events on this data channel. In this case, the data
|
|
// channel should not be assigned to the source, because it will not
|
|
// actually be used to send MIDI events. processEvent will assign the
|
|
// data channel to the source, so it is bypassed and controlChange is
|
|
// called directly.
|
|
controlChange(outputChannel, op1, op2, source, controlData, channelLockedByOtherSource);
|
|
} else {
|
|
processEvent(source, b, outputChannel, controlData, channelLockedByOtherSource);
|
|
}
|
|
|
|
if (command == MIDI_COMMAND_NOTE_OFF || command == MIDI_COMMAND_NOTE_ON || command == MIDI_COMMAND_PITCH_BEND ||
|
|
command == MIDI_COMMAND_POLYPHONIC_AFTERTOUCH || command == MIDI_COMMAND_CHANNEL_AFTERTOUCH) {
|
|
_noteCounter++;
|
|
if (controlData.usingCustomTimbre) {
|
|
// Remember that this timbre got used now
|
|
_customTimbres[controlData.currentCustomTimbreId].lastUsedNoteCounter = _noteCounter;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::controlChange(byte outputChannel, byte controllerNumber, byte controllerValue,
|
|
int8 source, MidiChannelControlData &controlData, bool channelLockedByOtherSource) {
|
|
assert(source < MAXIMUM_SOURCES);
|
|
|
|
MilesMidiChannelControlData &milesControlData = channelLockedByOtherSource ?
|
|
*_midiChannels[outputChannel].unlockData : *_midiChannels[outputChannel].currentData;
|
|
|
|
// XMIDI controllers
|
|
switch (controllerNumber) {
|
|
case MILES_CONTROLLER_SELECT_PATCH_BANK:
|
|
milesControlData.currentPatchBank = controllerValue;
|
|
return;
|
|
|
|
case MILES_CONTROLLER_PROTECT_TIMBRE:
|
|
if (milesControlData.usingCustomTimbre) {
|
|
// custom timbre set on current channel
|
|
_customTimbres[milesControlData.currentCustomTimbreId].protectionEnabled = controllerValue >= 64;
|
|
}
|
|
return;
|
|
|
|
case MILES_CONTROLLER_LOCK_CHANNEL:
|
|
if (source >= 0) {
|
|
if (controllerValue >= 0x40) {
|
|
lockChannel(source, outputChannel);
|
|
} else {
|
|
unlockChannel(outputChannel);
|
|
}
|
|
}
|
|
return;
|
|
|
|
case MILES_CONTROLLER_PROTECT_CHANNEL:
|
|
if (source >= 0 && !_midiChannels[outputChannel].locked) {
|
|
_midiChannels[outputChannel].lockProtected = controllerValue >= 0x40;
|
|
_midiChannels[outputChannel].protectedSource = controllerValue >= 0x40 ? source : -1;
|
|
}
|
|
return;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// XMIDI MT-32 specific controllers
|
|
if (_midiType == MT_MT32 && _nativeMT32) {
|
|
switch (controllerNumber) {
|
|
case MILES_CONTROLLER_PATCH_REVERB:
|
|
writePatchByte(milesControlData.program, 6, controllerValue);
|
|
if (!channelLockedByOtherSource)
|
|
_driver->send(MIDI_COMMAND_PROGRAM_CHANGE | outputChannel | (milesControlData.program << 8));
|
|
return;
|
|
|
|
case MILES_CONTROLLER_PATCH_BENDER:
|
|
writePatchByte(milesControlData.program, 4, controllerValue);
|
|
if (!channelLockedByOtherSource)
|
|
_driver->send(MIDI_COMMAND_PROGRAM_CHANGE | outputChannel | (milesControlData.program << 8));
|
|
return;
|
|
|
|
case MILES_CONTROLLER_REVERB_MODE:
|
|
writeToSystemArea(1, controllerValue);
|
|
return;
|
|
|
|
case MILES_CONTROLLER_REVERB_TIME:
|
|
writeToSystemArea(2, controllerValue);
|
|
return;
|
|
|
|
case MILES_CONTROLLER_REVERB_LEVEL:
|
|
writeToSystemArea(3, controllerValue);
|
|
return;
|
|
|
|
case MILES_CONTROLLER_RHYTHM_KEY_TIMBRE:
|
|
if (milesControlData.usingCustomTimbre) {
|
|
// custom timbre is set on current channel
|
|
writeRhythmSetup(controllerValue, milesControlData.currentCustomTimbreId);
|
|
}
|
|
return;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// XMIDI MT-32 SysEx controllers
|
|
if (_midiType == MT_MT32 && (controllerNumber >= MILES_CONTROLLER_SYSEX_RANGE_BEGIN) &&
|
|
(controllerNumber <= MILES_CONTROLLER_SYSEX_RANGE_END)) {
|
|
if (!_nativeMT32)
|
|
return;
|
|
|
|
// send SysEx
|
|
byte sysExQueueNr = 0;
|
|
|
|
// figure out which queue is accessed
|
|
controllerNumber -= MILES_CONTROLLER_SYSEX_RANGE_BEGIN;
|
|
while (controllerNumber > MILES_CONTROLLER_SYSEX_COMMAND_FINAL_DATA) {
|
|
sysExQueueNr++;
|
|
controllerNumber -= (MILES_CONTROLLER_SYSEX_COMMAND_FINAL_DATA + 1);
|
|
}
|
|
assert(sysExQueueNr < MILES_CONTROLLER_SYSEX_QUEUE_COUNT);
|
|
|
|
byte sysExPos = _milesSysExQueues[sysExQueueNr].dataPos;
|
|
bool sysExSend = false;
|
|
|
|
switch(controllerNumber) {
|
|
case MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS1:
|
|
_milesSysExQueues[sysExQueueNr].targetAddress &= 0x003FFF;
|
|
_milesSysExQueues[sysExQueueNr].targetAddress |= (controllerValue << 14);
|
|
break;
|
|
case MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS2:
|
|
_milesSysExQueues[sysExQueueNr].targetAddress &= 0x1FC07F;
|
|
_milesSysExQueues[sysExQueueNr].targetAddress |= (controllerValue << 7);
|
|
break;
|
|
case MILES_CONTROLLER_SYSEX_COMMAND_ADDRESS3:
|
|
_milesSysExQueues[sysExQueueNr].targetAddress &= 0x1FFF80;
|
|
_milesSysExQueues[sysExQueueNr].targetAddress |= controllerValue;
|
|
break;
|
|
case MILES_CONTROLLER_SYSEX_COMMAND_DATA:
|
|
if (sysExPos < MILES_CONTROLLER_SYSEX_QUEUE_SIZE) {
|
|
// Space left? put current byte into queue
|
|
_milesSysExQueues[sysExQueueNr].data[sysExPos] = controllerValue;
|
|
sysExPos++;
|
|
_milesSysExQueues[sysExQueueNr].dataPos = sysExPos;
|
|
if (sysExPos >= MILES_CONTROLLER_SYSEX_QUEUE_SIZE) {
|
|
// overflow? -> send it now
|
|
sysExSend = true;
|
|
}
|
|
}
|
|
break;
|
|
case MILES_CONTROLLER_SYSEX_COMMAND_FINAL_DATA:
|
|
if (sysExPos < MILES_CONTROLLER_SYSEX_QUEUE_SIZE) {
|
|
// Space left? put current byte into queue
|
|
_milesSysExQueues[sysExQueueNr].data[sysExPos] = controllerValue;
|
|
sysExPos++;
|
|
// Do not increment dataPos. Subsequent Final Data commands will
|
|
// re-send the last address byte with the new controller value.
|
|
sysExSend = true;
|
|
}
|
|
break;
|
|
default:
|
|
assert(0);
|
|
}
|
|
|
|
if (sysExSend) {
|
|
if (sysExPos > 0) {
|
|
// data actually available? -> send it
|
|
sysExMT32(_milesSysExQueues[sysExQueueNr].data, sysExPos, _milesSysExQueues[sysExQueueNr].targetAddress, false, true);
|
|
|
|
// Adjust target address to point at the final data byte, or at the
|
|
// end of the current data in case of an overflow
|
|
_milesSysExQueues[sysExQueueNr].targetAddress += _milesSysExQueues[sysExQueueNr].dataPos;
|
|
|
|
// reset queue data buffer
|
|
_milesSysExQueues[sysExQueueNr].dataPos = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ((controllerNumber >= MILES_CONTROLLER_XMIDI_RANGE_BEGIN) && (controllerNumber <= MILES_CONTROLLER_XMIDI_RANGE_END)) {
|
|
// XMIDI controllers? Don't send these to the MIDI device
|
|
return;
|
|
}
|
|
|
|
// Handle other controllers and send message (if necessary)
|
|
MidiDriver_MT32GM::controlChange(outputChannel, controllerNumber, controllerValue, source, milesControlData, channelLockedByOtherSource);
|
|
}
|
|
|
|
bool MidiDriver_Miles_Midi::addActiveNote(uint8 outputChannel, uint8 note, int8 source) {
|
|
bool added = MidiDriver_MT32GM::addActiveNote(outputChannel, note, source);
|
|
|
|
if (added)
|
|
_midiChannels[outputChannel].activeNotes++;
|
|
|
|
return added;
|
|
}
|
|
|
|
bool MidiDriver_Miles_Midi::removeActiveNote(uint8 outputChannel, uint8 note, int8 source) {
|
|
bool removed = MidiDriver_MT32GM::removeActiveNote(outputChannel, note, source);
|
|
|
|
if (removed) {
|
|
if (_midiChannels[outputChannel].activeNotes == 0) {
|
|
warning("MILES-MIDI: active notes 0 on channel %d when turning off note %x", outputChannel, note);
|
|
} else {
|
|
_midiChannels[outputChannel].activeNotes--;
|
|
}
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::removeActiveNotes(uint8 outputChannel, bool sustainedNotes) {
|
|
Common::StackLock lock(_activeNotesMutex);
|
|
|
|
// Remove sustained or non-sustained notes from the active notes registration
|
|
for (int i = 0; i < _maximumActiveNotes; ++i) {
|
|
if (_activeNotes[i].channel == outputChannel && _activeNotes[i].sustain == sustainedNotes) {
|
|
_activeNotes[i].clear();
|
|
if (_midiChannels[outputChannel].activeNotes == 0) {
|
|
warning("MILES-MIDI: active notes 0 on channel %d (sustained %i) when removing active notes", outputChannel, sustainedNotes);
|
|
continue;
|
|
}
|
|
--_midiChannels[outputChannel].activeNotes;
|
|
}
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::lockChannel(uint8 source, uint8 dataChannel) {
|
|
assert(source < MAXIMUM_SOURCES);
|
|
|
|
int8 lockChannel = findLockChannel();
|
|
if (lockChannel == -1)
|
|
// Try again, but consider lock protected channels
|
|
lockChannel = findLockChannel(true);
|
|
if (lockChannel == -1)
|
|
// Could not find a channel to lock
|
|
return;
|
|
|
|
// stopNotesOnChannel will turn off sustain, so record the current sustain
|
|
// value so it can be set on the unlock data.
|
|
bool currentSustain = _midiChannels[lockChannel].currentData->sustain;
|
|
|
|
stopNotesOnChannel(lockChannel);
|
|
|
|
_midiChannels[lockChannel].locked = true;
|
|
_midiChannels[lockChannel].lockDataChannel = dataChannel;
|
|
_channelMap[source][dataChannel] = lockChannel;
|
|
// Copy current controller values so they can be restored when unlocking the channel
|
|
*_midiChannels[lockChannel].unlockData = *_midiChannels[lockChannel].currentData;
|
|
_midiChannels[lockChannel].unlockData->sustain = currentSustain;
|
|
_midiChannels[lockChannel].currentData->source = source;
|
|
|
|
// Set any specified default controller values on the channel
|
|
applyControllerDefaults(source, *_midiChannels[lockChannel].currentData, lockChannel, false);
|
|
|
|
// Send volume change to apply the new source volume
|
|
controlChange(lockChannel, MIDI_CONTROLLER_VOLUME, 0x7F, source, *_midiChannels[lockChannel].currentData);
|
|
}
|
|
|
|
int8 MidiDriver_Miles_Midi::findLockChannel(bool useProtectedChannels) {
|
|
// Starting at the highest (non-rhythm) channel, find the channel
|
|
// with the least active notes that isn't already locked.
|
|
// If useProtectedChannels is false, channels that are protected
|
|
// from channel locking will not be considered.
|
|
int8 potentialLockChannel = -1;
|
|
uint8 notes = 255;
|
|
for (int i = MIDI_CHANNEL_COUNT - 1; i >= 0; --i) {
|
|
if (!isOutputChannelUsed(i) || i == MIDI_RHYTHM_CHANNEL || _midiChannels[i].locked ||
|
|
(!useProtectedChannels && _midiChannels[i].lockProtected))
|
|
continue;
|
|
if (_midiChannels[i].activeNotes < notes) {
|
|
potentialLockChannel = i;
|
|
notes = _midiChannels[i].activeNotes;
|
|
}
|
|
}
|
|
return potentialLockChannel;
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::unlockChannel(uint8 outputChannel) {
|
|
MidiChannelEntry &channel = _midiChannels[outputChannel];
|
|
if (!channel.locked)
|
|
return;
|
|
|
|
stopNotesOnChannel(outputChannel);
|
|
|
|
// Unlock the channel
|
|
channel.locked = false;
|
|
_channelMap[channel.currentData->source][channel.lockDataChannel] = channel.lockDataChannel;
|
|
channel.lockDataChannel = -1;
|
|
channel.currentData->source = channel.unlockData->source;
|
|
|
|
// Send the unlock channel data to the MIDI device to reset the channel parameters
|
|
if (channel.unlockData->volume != 0xFF) {
|
|
controlChange(outputChannel, MIDI_CONTROLLER_VOLUME, channel.unlockData->volume, channel.currentData->source, *channel.currentData);
|
|
} else {
|
|
channel.currentData->volume = 0xFF;
|
|
}
|
|
if (channel.currentData->modulation != channel.unlockData->modulation)
|
|
controlChange(outputChannel, MIDI_CONTROLLER_MODULATION, channel.unlockData->modulation, channel.currentData->source, *channel.currentData);
|
|
if (channel.currentData->panPosition != channel.unlockData->panPosition)
|
|
controlChange(outputChannel, MIDI_CONTROLLER_PANNING, channel.unlockData->panPosition, channel.currentData->source, *channel.currentData);
|
|
if (channel.currentData->expression != channel.unlockData->expression)
|
|
controlChange(outputChannel, MIDI_CONTROLLER_EXPRESSION, channel.unlockData->expression, channel.currentData->source, *channel.currentData);
|
|
if (channel.currentData->sustain != channel.unlockData->sustain)
|
|
controlChange(outputChannel, MIDI_CONTROLLER_SUSTAIN, channel.unlockData->sustain ? 0x7F : 0x00, channel.currentData->source, *channel.currentData);
|
|
if (channel.currentData->currentPatchBank != channel.unlockData->currentPatchBank)
|
|
controlChange(outputChannel, MILES_CONTROLLER_SELECT_PATCH_BANK, channel.unlockData->currentPatchBank,
|
|
channel.currentData->source, *channel.currentData);
|
|
if (channel.unlockData->program != 0xFF && (channel.currentData->program != channel.unlockData->program ||
|
|
channel.currentData->currentPatchBank != channel.unlockData->currentPatchBank))
|
|
programChange(outputChannel, channel.unlockData->program, channel.currentData->source, *channel.currentData);
|
|
if (channel.currentData->pitchWheel != channel.unlockData->pitchWheel)
|
|
send(channel.currentData->source, MIDI_COMMAND_PITCH_BEND | outputChannel,
|
|
channel.unlockData->pitchWheel & 0x7F, (channel.unlockData->pitchWheel >> 7) & 0x7F);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::stopNotesOnChannel(uint8 outputChannelNumber) {
|
|
MidiChannelEntry &channel = _midiChannels[outputChannelNumber];
|
|
if (channel.currentData->sustain) {
|
|
controlChange(outputChannelNumber, MIDI_CONTROLLER_SUSTAIN, 0, channel.currentData->source, *channel.currentData);
|
|
}
|
|
if (channel.activeNotes > 0) {
|
|
controlChange(outputChannelNumber, MIDI_CONTROLLER_ALL_NOTES_OFF, 0, channel.currentData->source, *channel.currentData);
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::stopAllNotes(bool stopSustainedNotes) {
|
|
MidiDriver_MT32GM::stopAllNotes(stopSustainedNotes);
|
|
|
|
for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) {
|
|
if (isOutputChannelUsed(i))
|
|
_midiChannels[i].activeNotes = 0;
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::programChange(byte outputChannel, byte patchId, int8 source,
|
|
MidiChannelControlData &controlData, bool channelLockedByOtherSource) {
|
|
MilesMidiChannelControlData &milesControlData = channelLockedByOtherSource ?
|
|
*_midiChannels[outputChannel].unlockData : *_midiChannels[outputChannel].currentData;
|
|
|
|
if (_midiType == MT_MT32) {
|
|
byte channelPatchBank = milesControlData.currentPatchBank;
|
|
byte activePatchBank = _patchesBank[patchId];
|
|
|
|
//warning("patch channel %d, patch %x, bank %x", midiChannel, patchId, channelPatchBank);
|
|
|
|
if (channelPatchBank != activePatchBank) {
|
|
// associate patch with timbre
|
|
setupPatch(channelPatchBank, patchId);
|
|
}
|
|
|
|
// If this is a custom patch, remember customTimbreId
|
|
int16 customTimbre = searchCustomTimbre(channelPatchBank, patchId);
|
|
if (customTimbre >= 0) {
|
|
milesControlData.usingCustomTimbre = true;
|
|
milesControlData.currentCustomTimbreId = customTimbre;
|
|
} else {
|
|
milesControlData.usingCustomTimbre = false;
|
|
}
|
|
}
|
|
|
|
MidiDriver_MT32GM::programChange(outputChannel, patchId, source, milesControlData, channelLockedByOtherSource);
|
|
}
|
|
|
|
int16 MidiDriver_Miles_Midi::searchCustomTimbre(byte patchBank, byte patchId) {
|
|
byte customTimbreId = 0;
|
|
|
|
for (customTimbreId = 0; customTimbreId < MILES_MT32_CUSTOMTIMBRE_COUNT; customTimbreId++) {
|
|
if (_customTimbres[customTimbreId].used) {
|
|
if ((_customTimbres[customTimbreId].currentPatchBank == patchBank) &&
|
|
(_customTimbres[customTimbreId].currentPatchId == patchId)) {
|
|
return customTimbreId;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
const MilesMT32InstrumentEntry *MidiDriver_Miles_Midi::searchCustomInstrument(byte patchBank, byte patchId) {
|
|
const MilesMT32InstrumentEntry *instrumentPtr = _instrumentTablePtr;
|
|
|
|
for (uint16 instrumentNr = 0; instrumentNr < _instrumentTableCount; instrumentNr++) {
|
|
if ((instrumentPtr->bankId == patchBank) && (instrumentPtr->patchId == patchId))
|
|
return instrumentPtr;
|
|
instrumentPtr++;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::setupPatch(byte patchBank, byte patchId, bool useSysExQueue) {
|
|
_patchesBank[patchId] = patchBank;
|
|
|
|
if (patchBank) {
|
|
// non-built-in bank
|
|
int16 customTimbreId = searchCustomTimbre(patchBank, patchId);
|
|
if (customTimbreId >= 0) {
|
|
// now available? -> use this timbre
|
|
writePatchTimbre(patchId, 2, customTimbreId, useSysExQueue); // Group MEMORY
|
|
return;
|
|
}
|
|
}
|
|
|
|
// for built-in bank (or timbres, that are not available) use default MT32 timbres
|
|
byte timbreId = patchId & 0x3F;
|
|
if (!(patchId & 0x40)) {
|
|
writePatchTimbre(patchId, 0, timbreId, useSysExQueue); // Group A
|
|
} else {
|
|
writePatchTimbre(patchId, 1, timbreId, useSysExQueue); // Group B
|
|
}
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::processXMIDITimbreChunk(const byte *timbreListPtr, uint32 timbreListSize) {
|
|
if (_midiType != MT_MT32)
|
|
// Some GM files contain timbre chunks, but custom patches cannot
|
|
// be loaded on a GM device.
|
|
return;
|
|
|
|
uint16 timbreCount = 0;
|
|
uint32 expectedSize = 0;
|
|
const byte *timbreListSeeker = timbreListPtr;
|
|
|
|
if (timbreListSize < 2) {
|
|
warning("MILES-MIDI: XMIDI-TIMB chunk - not enough bytes in chunk");
|
|
return;
|
|
}
|
|
|
|
timbreCount = READ_LE_UINT16(timbreListPtr);
|
|
expectedSize = timbreCount * 2;
|
|
if (expectedSize > timbreListSize) {
|
|
warning("MILES-MIDI: XMIDI-TIMB chunk - size mismatch");
|
|
return;
|
|
}
|
|
|
|
timbreListSeeker += 2;
|
|
|
|
while (timbreCount) {
|
|
const byte patchId = *timbreListSeeker++;
|
|
const byte patchBank = *timbreListSeeker++;
|
|
int16 customTimbreId = 0;
|
|
|
|
switch (patchBank) {
|
|
case MILES_MT32_TIMBREBANK_STANDARD_ROLAND:
|
|
case MILES_MT32_TIMBREBANK_MELODIC_MODULE:
|
|
// ignore those 2 banks
|
|
break;
|
|
|
|
default:
|
|
// Check, if this timbre was already loaded
|
|
customTimbreId = searchCustomTimbre(patchBank, patchId);
|
|
|
|
if (customTimbreId < 0) {
|
|
// currently not loaded, try to install it
|
|
installCustomTimbre(patchBank, patchId);
|
|
}
|
|
}
|
|
timbreCount--;
|
|
}
|
|
}
|
|
|
|
//
|
|
int16 MidiDriver_Miles_Midi::installCustomTimbre(byte patchBank, byte patchId) {
|
|
switch(patchBank) {
|
|
case MILES_MT32_TIMBREBANK_STANDARD_ROLAND: // Standard Roland MT32 bank
|
|
case MILES_MT32_TIMBREBANK_MELODIC_MODULE: // Reserved for melodic mode
|
|
return -1;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Original driver did a search for custom timbre here
|
|
// and in case it was found, it would call setup_patch()
|
|
// we are called from within setup_patch(), so this isn't needed
|
|
|
|
int16 customTimbreId = -1;
|
|
int16 leastUsedTimbreId = -1;
|
|
uint32 leastUsedTimbreNoteCounter = _noteCounter;
|
|
const MilesMT32InstrumentEntry *instrumentPtr = nullptr;
|
|
|
|
// Check, if requested instrument is actually available
|
|
instrumentPtr = searchCustomInstrument(patchBank, patchId);
|
|
if (!instrumentPtr) {
|
|
warning("MILES-MIDI: instrument not found during installCustomTimbre()");
|
|
return -1; // not found -> bail out
|
|
}
|
|
|
|
// Look for an empty timbre slot
|
|
// or get the least used non-protected slot
|
|
for (byte customTimbreNr = 0; customTimbreNr < MILES_MT32_CUSTOMTIMBRE_COUNT; customTimbreNr++) {
|
|
if (!_customTimbres[customTimbreNr].used) {
|
|
// found an empty slot -> use this one
|
|
customTimbreId = customTimbreNr;
|
|
break;
|
|
} else {
|
|
// used slot
|
|
if (!_customTimbres[customTimbreNr].protectionEnabled) {
|
|
// not protected
|
|
uint32 customTimbreNoteCounter = _customTimbres[customTimbreNr].lastUsedNoteCounter;
|
|
if (customTimbreNoteCounter < leastUsedTimbreNoteCounter) {
|
|
leastUsedTimbreId = customTimbreNr;
|
|
leastUsedTimbreNoteCounter = customTimbreNoteCounter;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (customTimbreId < 0) {
|
|
// no empty slot found, check if we got a least used non-protected slot
|
|
if (leastUsedTimbreId < 0) {
|
|
// everything is protected, bail out
|
|
warning("MILES-MIDI: no non-protected timbre slots available during installCustomTimbre()");
|
|
return -1;
|
|
}
|
|
customTimbreId = leastUsedTimbreId;
|
|
}
|
|
|
|
// setup timbre slot
|
|
_customTimbres[customTimbreId].used = true;
|
|
_customTimbres[customTimbreId].currentPatchBank = patchBank;
|
|
_customTimbres[customTimbreId].currentPatchId = patchId;
|
|
_customTimbres[customTimbreId].lastUsedNoteCounter = _noteCounter;
|
|
_customTimbres[customTimbreId].protectionEnabled = false;
|
|
|
|
uint32 targetAddress = ((0x08 << 14) | (0x00 << 7) | 0x00) + (customTimbreId * 0x100);
|
|
uint32 targetAddressCommon = targetAddress;
|
|
uint32 targetAddressPartial1 = targetAddress + 0x0E;
|
|
uint32 targetAddressPartial2 = targetAddress + 0x48;
|
|
uint32 targetAddressPartial3 = targetAddress + 0x82;
|
|
uint32 targetAddressPartial4 = targetAddress + 0xBC;
|
|
|
|
#if 0
|
|
byte parameterData[MILES_MT32_PATCHDATA_TOTAL_SIZE + 1];
|
|
uint16 parameterDataPos = 0;
|
|
|
|
memcpy(parameterData, instrumentPtr->commonParameter, MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE);
|
|
parameterDataPos += MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE;
|
|
memcpy(parameterData + parameterDataPos, instrumentPtr->partialParameters[0], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE);
|
|
parameterDataPos += MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE;
|
|
memcpy(parameterData + parameterDataPos, instrumentPtr->partialParameters[1], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE);
|
|
parameterDataPos += MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE;
|
|
memcpy(parameterData + parameterDataPos, instrumentPtr->partialParameters[2], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE);
|
|
parameterDataPos += MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE;
|
|
memcpy(parameterData + parameterDataPos, instrumentPtr->partialParameters[3], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE);
|
|
parameterDataPos += MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE;
|
|
parameterData[parameterDataPos] = MILES_MT32_SYSEX_TERMINATOR;
|
|
|
|
MT32SysEx(targetAddressCommon, parameterData);
|
|
#endif
|
|
|
|
// upload common parameter data
|
|
sysExMT32(instrumentPtr->commonParameter, MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE, targetAddressCommon, true);
|
|
// upload partial parameter data
|
|
sysExMT32(instrumentPtr->partialParameters[0], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE, targetAddressPartial1, true);
|
|
sysExMT32(instrumentPtr->partialParameters[1], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE, targetAddressPartial2, true);
|
|
sysExMT32(instrumentPtr->partialParameters[2], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE, targetAddressPartial3, true);
|
|
sysExMT32(instrumentPtr->partialParameters[3], MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE, targetAddressPartial4, true);
|
|
|
|
setupPatch(patchBank, patchId, true);
|
|
|
|
return customTimbreId;
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::writeRhythmSetup(byte note, byte customTimbreId) {
|
|
byte sysExData[1];
|
|
uint32 targetAddress = (0x03 << 14) | (0x01 << 7) | 0x10;
|
|
|
|
targetAddress += ((note - 24) << 2);
|
|
|
|
sysExData[0] = customTimbreId;
|
|
|
|
sysExMT32(sysExData, 1, targetAddress);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::writePatchTimbre(byte patchId, byte timbreGroup, byte timbreId, bool useSysExQueue) {
|
|
byte sysExData[2];
|
|
uint32 targetAddress = (0x05 << 14) | (0x00 << 7) | 0x00;
|
|
|
|
// write to patch memory (starts at 0x050000, each entry is 8 bytes)
|
|
targetAddress += (patchId << 3);
|
|
|
|
sysExData[0] = timbreGroup; // 0 - group A, 1 - group B, 2 - memory, 3 - rhythm
|
|
sysExData[1] = timbreId; // timbre number (0-63)
|
|
|
|
sysExMT32(sysExData, 2, targetAddress, useSysExQueue);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::writePatchByte(byte patchId, byte index, byte patchValue) {
|
|
byte sysExData[1];
|
|
uint32 targetAddress = (0x05 << 14) | (0x00 << 7) | 0x00;
|
|
|
|
targetAddress += (patchId << 3) + index;
|
|
|
|
sysExData[0] = patchValue;
|
|
|
|
sysExMT32(sysExData, 1, targetAddress);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::writeToSystemArea(byte index, byte value) {
|
|
byte sysExData[1];
|
|
uint32 targetAddress = (0x10 << 14) | (0x00 << 7) | 0x00;
|
|
|
|
targetAddress += index;
|
|
|
|
sysExData[0] = value;
|
|
|
|
sysExMT32(sysExData, 1, targetAddress);
|
|
}
|
|
|
|
MidiDriver_Miles_Midi *MidiDriver_Miles_MT32_create(const Common::String &instrumentDataFilename) {
|
|
return MidiDriver_Miles_MIDI_create(MT_MT32, instrumentDataFilename);
|
|
}
|
|
|
|
MidiDriver_Miles_Midi *MidiDriver_Miles_MIDI_create(MusicType midiType, const Common::String &instrumentDataFilename) {
|
|
assert(midiType == MT_MT32 || midiType == MT_GM || midiType == MT_GS);
|
|
|
|
MilesMT32InstrumentEntry *instrumentTablePtr = nullptr;
|
|
uint16 instrumentTableCount = 0;
|
|
|
|
if (midiType == MT_MT32 && !instrumentDataFilename.empty()) {
|
|
// Load MT32 instrument data from file SAMPLE.MT
|
|
Common::File *fileStream = new Common::File();
|
|
uint32 fileSize = 0;
|
|
byte *fileDataPtr = nullptr;
|
|
uint32 fileDataOffset = 0;
|
|
uint32 fileDataLeft = 0;
|
|
|
|
byte curBankId;
|
|
byte curPatchId;
|
|
|
|
MilesMT32InstrumentEntry *instrumentPtr = nullptr;
|
|
uint32 instrumentOffset;
|
|
uint16 instrumentDataSize;
|
|
|
|
if (!fileStream->open(instrumentDataFilename))
|
|
error("MILES-MIDI: could not open instrument file '%s'", instrumentDataFilename.c_str());
|
|
|
|
fileSize = fileStream->size();
|
|
|
|
fileDataPtr = new byte[fileSize];
|
|
|
|
if (fileStream->read(fileDataPtr, fileSize) != fileSize)
|
|
error("MILES-MIDI: error while reading instrument file");
|
|
fileStream->close();
|
|
delete fileStream;
|
|
|
|
// File is like this:
|
|
// [patch:BYTE] [bank:BYTE] [patchoffset:UINT32]
|
|
// ...
|
|
// until patch + bank are both 0xFF, which signals end of header
|
|
|
|
// First we check how many entries there are
|
|
fileDataOffset = 0;
|
|
fileDataLeft = fileSize;
|
|
while (1) {
|
|
if (fileDataLeft < 6)
|
|
error("MILES-MIDI: unexpected EOF in instrument file");
|
|
|
|
curPatchId = fileDataPtr[fileDataOffset++];
|
|
curBankId = fileDataPtr[fileDataOffset++];
|
|
|
|
if ((curBankId == 0xFF) && (curPatchId == 0xFF))
|
|
break;
|
|
|
|
fileDataOffset += 4; // skip over offset
|
|
instrumentTableCount++;
|
|
}
|
|
|
|
if (instrumentTableCount == 0)
|
|
error("MILES-MIDI: no instruments in instrument file");
|
|
|
|
// Allocate space for instruments
|
|
instrumentTablePtr = new MilesMT32InstrumentEntry[instrumentTableCount];
|
|
|
|
// Now actually read all entries
|
|
instrumentPtr = instrumentTablePtr;
|
|
|
|
fileDataOffset = 0;
|
|
while (1) {
|
|
curPatchId = fileDataPtr[fileDataOffset++];
|
|
curBankId = fileDataPtr[fileDataOffset++];
|
|
|
|
if ((curBankId == 0xFF) && (curPatchId == 0xFF))
|
|
break;
|
|
|
|
instrumentOffset = READ_LE_UINT32(fileDataPtr + fileDataOffset);
|
|
fileDataOffset += 4;
|
|
|
|
instrumentPtr->bankId = curBankId;
|
|
instrumentPtr->patchId = curPatchId;
|
|
|
|
instrumentDataSize = READ_LE_UINT16(fileDataPtr + instrumentOffset);
|
|
if (instrumentDataSize != (MILES_MT32_PATCHDATA_TOTAL_SIZE + 2))
|
|
error("MILES-MIDI: unsupported instrument size");
|
|
|
|
instrumentOffset += 2;
|
|
// Copy common parameter data
|
|
memcpy(instrumentPtr->commonParameter, fileDataPtr + instrumentOffset, MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE);
|
|
instrumentOffset += MILES_MT32_PATCHDATA_COMMONPARAMETER_SIZE;
|
|
|
|
// Copy partial parameter data
|
|
for (byte partialNr = 0; partialNr < MILES_MT32_PATCHDATA_PARTIALPARAMETERS_COUNT; partialNr++) {
|
|
memcpy(&instrumentPtr->partialParameters[partialNr], fileDataPtr + instrumentOffset, MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE);
|
|
instrumentOffset += MILES_MT32_PATCHDATA_PARTIALPARAMETER_SIZE;
|
|
}
|
|
|
|
// Instrument read, next instrument please
|
|
instrumentPtr++;
|
|
}
|
|
|
|
// Free instrument file data
|
|
delete[] fileDataPtr;
|
|
}
|
|
|
|
return new MidiDriver_Miles_Midi(midiType, instrumentTablePtr, instrumentTableCount);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::deinitSource(uint8 source) {
|
|
// Unlock and unprotect channels which were locked or protected by this source.
|
|
for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) {
|
|
if (!isOutputChannelUsed(i))
|
|
continue;
|
|
|
|
if (_midiChannels[i].currentData->source == source && _midiChannels[i].locked) {
|
|
unlockChannel(i);
|
|
}
|
|
if (_midiChannels[i].lockProtected && _midiChannels[i].protectedSource == source) {
|
|
_midiChannels[i].lockProtected = false;
|
|
_midiChannels[i].protectedSource = -1;
|
|
}
|
|
if (_midiChannels[i].unlockData->source == source)
|
|
_midiChannels[i].unlockData->source = -1;
|
|
}
|
|
|
|
MidiDriver_MT32GM::deinitSource(source);
|
|
}
|
|
|
|
void MidiDriver_Miles_Midi::applySourceVolume(uint8 source) {
|
|
for (int i = 0; i < MIDI_CHANNEL_COUNT; ++i) {
|
|
if (!isOutputChannelUsed(i))
|
|
continue;
|
|
|
|
MidiChannelEntry &channel = _midiChannels[i];
|
|
MilesMidiChannelControlData *channelData = nullptr;
|
|
bool channelLockedByOtherSource = false;
|
|
// Apply the new source volume to this channel if this source is active
|
|
// on this channel, or if it was active on the channel before it was
|
|
// locked.
|
|
if (source == 0xFF || (channel.currentData && channel.currentData->source == source)) {
|
|
channelData = channel.currentData;
|
|
} else if (channel.locked && channel.unlockData && channel.unlockData->source == source) {
|
|
channelData = channel.unlockData;
|
|
channelLockedByOtherSource = true;
|
|
}
|
|
|
|
if (channelData && channelData->volume != 0xFF)
|
|
controlChange(i, MIDI_CONTROLLER_VOLUME, channelData->volume, channelData->source, *channelData, channelLockedByOtherSource);
|
|
}
|
|
}
|
|
|
|
uint32 MidiDriver_Miles_Midi::property(int prop, uint32 param) {
|
|
switch (prop) {
|
|
case PROP_MILES_VERSION:
|
|
if (param == 0xFFFF)
|
|
return _milesVersion;
|
|
|
|
switch (param) {
|
|
case MILES_VERSION_3:
|
|
_milesVersion = MILES_VERSION_3;
|
|
break;
|
|
case MILES_VERSION_2:
|
|
default:
|
|
_milesVersion = MILES_VERSION_2;
|
|
}
|
|
|
|
break;
|
|
default:
|
|
return MidiDriver_Multisource::property(prop, param);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // End of namespace Audio
|