Files
2026-02-02 04:50:13 +01:00

339 lines
12 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 "agos/drivers/simon1/adlib.h"
#include "common/file.h"
namespace AGOS {
// Rhythm map hardcoded in the Simon 1 executable.
const MidiDriver_Simon1_AdLib::RhythmMapEntry MidiDriver_Simon1_AdLib::RHYTHM_MAP[39] = {
{ 11, 123, 40 },
{ 12, 127, 50 },
{ 12, 124, 1 },
{ 12, 124, 90 },
{ 13, 125, 50 },
{ 13, 125, 25 },
{ 15, 127, 80 },
{ 13, 125, 25 },
{ 15, 127, 40 },
{ 13, 125, 35 },
{ 15, 127, 90 },
{ 13, 125, 35 },
{ 13, 125, 45 },
{ 14, 126, 90 },
{ 13, 125, 45 },
{ 15, 127, 90 },
{ 0, 0, 0 },
{ 15, 127, 60 },
{ 0, 0, 0 },
{ 13, 125, 60 },
{ 0, 0, 0 },
{ 0, 0, 0 },
{ 0, 0, 0 },
{ 13, 125, 45 },
{ 13, 125, 40 },
{ 13, 125, 35 },
{ 13, 125, 30 },
{ 13, 125, 25 },
{ 13, 125, 80 },
{ 13, 125, 40 },
{ 13, 125, 80 },
{ 13, 125, 40 },
{ 14, 126, 40 },
{ 15, 127, 60 },
{ 0, 0, 0 },
{ 0, 0, 0 },
{ 14, 126, 80 },
{ 0, 0, 0 },
{ 13, 125, 100 }
};
// Frequency table hardcoded in the Simon 1 executable.
// Note that only the first 12 entries are used.
const uint16 MidiDriver_Simon1_AdLib::FREQUENCY_TABLE[16] = {
0x0157, 0x016B, 0x0181, 0x0198, 0x01B0, 0x01CA, 0x01E5, 0x0202,
0x0220, 0x0241, 0x0263, 0x0287, 0x2100, 0xD121, 0xA307, 0x46A4
};
MidiDriver_Simon1_AdLib::MidiDriver_Simon1_AdLib(OPL::Config::OplType oplType, const byte *instrumentData) : MidiDriver_ADLIB_Multisource(oplType), _musicRhythmNotesDisabled(false) {
// The Simon 1 MIDI data is written for MT-32 and rhythm notes are played
// by quickly turning a note on and off. The note off has no effect on an
// MT-32, but it will cut off the rhythm note on OPL. To prevent this, note
// off events for rhythm notes are ignored and the rhythm note is turned
// off right before this rhythm instrument is played again. The original
// interpreter does this as well.
_rhythmModeIgnoreNoteOffs = true;
parseInstrumentData(instrumentData);
}
MidiDriver_Simon1_AdLib::~MidiDriver_Simon1_AdLib() {
delete[] _instrumentBank;
delete[] _rhythmBank;
}
int MidiDriver_Simon1_AdLib::open() {
int result = MidiDriver_ADLIB_Multisource::open();
if (result >= 0)
// Simon 1 has the OPL rhythm mode permanently enabled.
setRhythmMode(true);
return result;
}
void MidiDriver_Simon1_AdLib::parseInstrumentData(const byte *instrumentData) {
const byte *dataPtr = instrumentData;
// The instrument data consists of 128 16-byte entries.
OplInstrumentDefinition *instrumentBank = new OplInstrumentDefinition[128];
for (int i = 0; i < 128; i++) {
instrumentBank[i].fourOperator = false;
instrumentBank[i].operator0.freqMultMisc = *dataPtr++;
instrumentBank[i].operator1.freqMultMisc = *dataPtr++;
instrumentBank[i].operator0.level = *dataPtr++;
instrumentBank[i].operator1.level = *dataPtr++;
instrumentBank[i].operator0.decayAttack = *dataPtr++;
instrumentBank[i].operator1.decayAttack = *dataPtr++;
instrumentBank[i].operator0.releaseSustain = *dataPtr++;
instrumentBank[i].operator1.releaseSustain = *dataPtr++;
instrumentBank[i].operator0.waveformSelect = *dataPtr++;
instrumentBank[i].operator1.waveformSelect = *dataPtr++;
instrumentBank[i].connectionFeedback0 = *dataPtr++;
instrumentBank[i].connectionFeedback1 = 0;
instrumentBank[i].rhythmNote = 0;
instrumentBank[i].rhythmType = RHYTHM_TYPE_UNDEFINED;
instrumentBank[i].transpose = 0;
// Remaining bytes seem to be unused.
dataPtr += 5;
}
// Construct a rhythm bank from the original rhythm map data.
OplInstrumentDefinition *rhythmBank = new OplInstrumentDefinition[39];
// MIDI note range 36-74.
_rhythmBankFirstNote = 36;
_rhythmBankLastNote = 36 + 39 - 1;
for (int i = 0; i < 39; i++) {
if (RHYTHM_MAP[i].channel == 0) {
// Some notes in the range have no definition.
rhythmBank[i].rhythmType = RHYTHM_TYPE_UNDEFINED;
} else {
// The rhythm bank makes use of instruments defined in the main instrument bank.
rhythmBank[i] = instrumentBank[RHYTHM_MAP[i].program];
// The MIDI channels used in the rhythm map correspond to OPL rhythm instrument types:
// 11 - bass drum
// 12 - snare drum
// 13 - tom tom
// 14 - cymbal
// 15 - hi-hat
rhythmBank[i].rhythmType = static_cast<OplInstrumentRhythmType>(6 - (RHYTHM_MAP[i].channel - 10));
rhythmBank[i].rhythmNote = RHYTHM_MAP[i].note;
rhythmBank[i].transpose = 0;
}
}
// Set the const class variables with our just allocated banks
_instrumentBank = instrumentBank;
_rhythmBank = rhythmBank;
}
void MidiDriver_Simon1_AdLib::noteOn(uint8 channel, uint8 note, uint8 velocity, uint8 source) {
if (_musicRhythmNotesDisabled && _sources[source].type != SOURCE_TYPE_SFX && channel == MIDI_RHYTHM_CHANNEL)
// A music source played a rhythm note while these are disabled.
// Ignore this event.
return;
if (_sources[source].type == SOURCE_TYPE_SFX)
// The original interpreter uses max velocity for all SFX notes.
velocity = 0x7F;
MidiDriver_ADLIB_Multisource::noteOn(channel, note, velocity, source);
}
void MidiDriver_Simon1_AdLib::programChange(uint8 channel, uint8 program, uint8 source) {
MidiDriver_ADLIB_Multisource::programChange(channel, program, source);
_activeNotesMutex.lock();
// Deallocate all inactive OPL channels for this MIDI channel and source.
for (int i = 0; i < _numMelodicChannels; i++) {
uint8 oplChannel = _melodicChannels[i];
if (_activeNotes[oplChannel].channelAllocated && !_activeNotes[oplChannel].noteActive &&
_activeNotes[oplChannel].channel == channel && _activeNotes[oplChannel].source == source) {
_activeNotes[oplChannel].channelAllocated = false;
}
}
_activeNotesMutex.unlock();
// Note: the original also sets up the new instrument on active OPL
// channels, i.e. channels which are currently playing a note. This is
// against the MIDI spec, which states that program changes should not
// affect active notes, and against the behavior of the MT-32, for which
// the music is composed. So instead, the new instrument is set up when a
// new note is played on these OPL channels.
}
void MidiDriver_Simon1_AdLib::deinitSource(uint8 source) {
if (_sources[source].type != SOURCE_TYPE_MUSIC)
// When a sound effect has finished playing, re-enable music rhythm
// notes.
_musicRhythmNotesDisabled = false;
MidiDriver_ADLIB_Multisource::deinitSource(source);
}
void MidiDriver_Simon1_AdLib::disableMusicRhythmNotes() {
_musicRhythmNotesDisabled = true;
}
uint8 MidiDriver_Simon1_AdLib::allocateOplChannel(uint8 channel, uint8 source, InstrumentInfo &instrumentInfo) {
// Use the regular allocation algorithm for rhythm instruments.
if (channel == MIDI_RHYTHM_CHANNEL)
return MidiDriver_ADLIB_Multisource::allocateOplChannel(channel, source, instrumentInfo);
// When allocating an OPL channel for playback of a note, the algorithm
// looks for the following types of channels:
// - An OPL channel already allocated to this source and MIDI channel that
// is not playing a note.
// - An unallocated OPL channel.
// - An OPL channel allocated to a different source and/or MIDI channel
// that is not playing a note.
//
// If no free OPL channel could be found, an active channel is "stolen" and
// the note it is currently playing is cut off. This channel is always
// channel 0.
uint8 allocatedChannel = 0xFF;
uint8 unallocatedChannel = 0xFF;
uint8 inactiveChannel = 0xFF;
for (int i = 0; i < _numMelodicChannels; i++) {
uint8 oplChannel = _melodicChannels[i];
if (_activeNotes[oplChannel].channelAllocated && _activeNotes[oplChannel].channel == channel &&
_activeNotes[oplChannel].source == source && !_activeNotes[oplChannel].noteActive) {
// Found an OPL channel already allocated to this source and MIDI
// channel that is not playing a note.
allocatedChannel = oplChannel;
// Always use the first available channel of this type.
break;
}
if (!_activeNotes[oplChannel].channelAllocated && unallocatedChannel == 0xFF)
// Found an unallocated OPL channel.
unallocatedChannel = oplChannel;
if (!_activeNotes[oplChannel].noteActive && inactiveChannel == 0xFF)
// Found an OPL channel allocated to a different source and/or MIDI
// channel that is not playing a note.
inactiveChannel = oplChannel;
}
if (allocatedChannel == 0xFF) {
// No allocated channel found.
if (unallocatedChannel != 0xFF) {
// Found an unallocated channel - use this.
allocatedChannel = unallocatedChannel;
} else if (inactiveChannel != 0xFF) {
// Found an inactive channel - use this.
allocatedChannel = inactiveChannel;
} else {
// A channel already playing a note must be "stolen".
// The original had some logic for a priority based reuse of
// channels. However, the priority value is always 0, which causes
// the first channel to be picked all the time.
allocatedChannel = 0;
}
}
if (_activeNotes[allocatedChannel].noteActive)
// Turn off the current note if the channel was "stolen".
writeKeyOff(allocatedChannel);
_activeNotes[allocatedChannel].channelAllocated = true;
_activeNotes[allocatedChannel].source = source;
_activeNotes[allocatedChannel].channel = channel;
return allocatedChannel;
}
uint16 MidiDriver_Simon1_AdLib::calculateFrequency(uint8 channel, uint8 source, uint8 note) {
// Determine the octave note. Notes 120-127 are clipped to octave note 12.
uint8 octaveNote = note >= 120 ? 12 : note % 12;
// Determine the octave / block. Notes 12-96 are in octaves 0-7, with lower
// and higher notes clipped to octave 0 and 7, respectively.
uint8 octave = CLIP((note / 12) - 1, 0, 7);
// Look up the OPL frequency / F-num.
uint16 octaveNoteFrequency = FREQUENCY_TABLE[octaveNote];
// Combine block and F-num in the format used by the OPL Ax and Bx
// registers.
return (octave << 10) | octaveNoteFrequency;
}
uint8 MidiDriver_Simon1_AdLib::calculateUnscaledVolume(uint8 channel, uint8 source, uint8 velocity, const OplInstrumentDefinition &instrumentDef, uint8 operatorNum) {
if (channel == MIDI_RHYTHM_CHANNEL && _sources[source].type != SOURCE_TYPE_SFX)
// The original interpreter halves the velocity for music rhythm notes.
// Note that SFX notes always use max velocity.
velocity >>= 1;
// Invert the instrument definition attenuation.
uint8 instDefVolume = 0x3F - (instrumentDef.getOperatorDefinition(operatorNum).level & 0x3F);
// Calculate the note volume using velocity and instrument definition
// volume.
uint8 calculatedVolume = ((velocity | 0x80) * instDefVolume) >> 8;
// Invert the calculated volume to an attenuation.
return 0x3F - calculatedVolume;
}
MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilename, OPL::Config::OplType oplType) {
// Load instrument data.
Common::File ibk;
if (!ibk.open(instrumentFilename)) {
error("MidiDriver_Simon1_AdLib::createMidiDriverSimon1AdLib - Could not find AdLib instrument bank file %s", instrumentFilename);
}
// Check for the expected FourCC (IBK\x1A)
if (ibk.readUint32BE() != 0x49424b1a) {
error("MidiDriver_Simon1_AdLib::createMidiDriverSimon1AdLib - Invalid AdLib instrument bank file %s", instrumentFilename);
}
byte *instrumentData = new byte[128 * 16];
if (ibk.read(instrumentData, 128 * 16) != 128 * 16) {
// Failed to read the expected amount of data.
delete[] instrumentData;
error("MidiDriver_Simon1_AdLib::createMidiDriverSimon1AdLib - Unexpected AdLib instrument bank file %s size", instrumentFilename);
}
MidiDriver_Simon1_AdLib *driver = new MidiDriver_Simon1_AdLib(oplType, instrumentData);
delete[] instrumentData;
return driver;
}
} // End of namespace AGOS