/* 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 "agos/drivers/accolade/cms.h"
#include "common/debug.h"
namespace AGOS {
// Hardcoded in RUNVGA.EXE.
const byte MidiDriver_Accolade_Cms::CMS_NOTE_FREQUENCIES[] = {
0x03, 0x1F, 0x3A, 0x53, 0x6B, 0x82,
0x97, 0xAC, 0xBF, 0xD1, 0xE2, 0xF2
};
// Hardcoded in RUNVGA.EXE; probably also used for Tandy.
const byte MidiDriver_Accolade_Cms::CMS_VOLUME_ADJUSTMENTS[] = {
0xF7, 0xFD, 0xFB, 0x00, 0xEC, 0xEE, 0x00, 0x1E, 0x08, 0x07, 0x02, 0x03, 0x08, 0x0A, 0x00, 0x00,
0xF6, 0xF4, 0xEF, 0x05, 0xFB, 0xFC, 0xF5, 0xF5, 0xF6, 0x28, 0x00, 0xEF, 0x00, 0x00, 0x00, 0x00,
0x29, 0x03, 0xE9, 0x00, 0xF5, 0x50, 0xF9, 0xF9, 0xF2, 0x1E, 0xFE, 0x0B, 0xF7, 0xF7, 0xF5, 0xF8,
0xFD, 0xFD, 0xFD, 0xE4, 0xED, 0xFB, 0xFE, 0xFB, 0xD1, 0x1F, 0x00, 0xF1, 0xF1, 0xF6, 0x00, 0x0A,
0xEE, 0xDB, 0x0E, 0xE5, 0x0B, 0x00, 0x0A, 0x0D, 0x03, 0x06, 0xF3, 0xF5, 0x1E, 0x1E, 0x0A, 0xFA,
0xF8, 0xF9, 0x0A, 0x00, 0xFA, 0xFA, 0xF9, 0x00, 0xF2, 0xF9, 0x00, 0xF9, 0x00, 0x04, 0xF8, 0xF7,
0xFC, 0xF1, 0x0A, 0xF1, 0x20, 0x20, 0xF7, 0xF2, 0xF6, 0xF7, 0xFA, 0xFF, 0x20, 0x20, 0x00, 0x00,
0xEA, 0x15, 0x00, 0xFB, 0x0E, 0x00, 0x14, 0x12, 0x00, 0xE2, 0xF7, 0xFB, 0x00, 0x00, 0xF1, 0xFF
};
MidiDriver_Accolade_Cms::MidiDriver_Accolade_Cms() : _cms(nullptr), _isOpen(false), _timer_proc(nullptr), _timer_param(nullptr) {
Common::fill(_instruments, _instruments + ARRAYSIZE(_instruments), 0);
Common::fill(_activeNotes, _activeNotes + ARRAYSIZE(_activeNotes), 0xFF);
Common::fill(_octaveRegisterValues, _octaveRegisterValues + ARRAYSIZE(_octaveRegisterValues), 0);
}
MidiDriver_Accolade_Cms::~MidiDriver_Accolade_Cms() {
if (_isOpen)
close();
}
int MidiDriver_Accolade_Cms::open() {
if (_isOpen)
return MERR_ALREADY_OPEN;
_cms = CMS::Config::create();
if (!_cms || !_cms->init())
return MERR_CANNOT_CONNECT;
_isOpen = true;
cmsInit();
_cms->start(new Common::Functor0Mem(this, &MidiDriver_Accolade_Cms::onTimer));
return 0;
}
void MidiDriver_Accolade_Cms::close() {
if (_cms) {
_cms->stop();
delete _cms;
_cms = nullptr;
}
_isOpen = false;
}
bool MidiDriver_Accolade_Cms::isOpen() const {
return _isOpen;
}
uint32 MidiDriver_Accolade_Cms::getBaseTempo() {
return 1000000 / CMS::CMS::DEFAULT_CALLBACK_FREQUENCY;
}
MidiChannel *MidiDriver_Accolade_Cms::allocateChannel() {
return nullptr;
}
MidiChannel *MidiDriver_Accolade_Cms::getPercussionChannel() {
return nullptr;
}
void MidiDriver_Accolade_Cms::cmsInit() {
for (int i = 0; i < 2; i++) {
int chipOffset = i * 0x100;
// Set registers 0x00 - 0x19 to 0 (note that this includes a few unused
// registers).
for (int j = 0; j <= 0x19; j++) {
writeRegister(j + chipOffset, 0);
}
// Frequency reset.
writeRegister(REGISTER_RESET_SOUND_ENABLE + chipOffset, 0x02);
// Sound enable.
writeRegister(REGISTER_RESET_SOUND_ENABLE + chipOffset, 0x01);
}
}
void MidiDriver_Accolade_Cms::send(uint32 b) {
byte channel = b & 0x0F;
// WORKAROUND This check is bugged in the original interpreter. The channel
// is first resolved through the channel mapping, which is 1 on 1 for
// channels 0-5, and the default FF for the other channels, which means
// they should not be played. Next, the mapped channel is checked against
// the maximum channels for the device, >= 6. However, this check is
// signed, which means FF is interpreted as -1, and it passes the check.
// This comes into play when drums are played on channel 9. The code for
// handling events does not expect channel FF and this results in writes to
// invalid registers and interference with notes on the other channels.
// This check is fixed here to restore the intended behavior.
// Note that this does result in an extended period of silence during the
// intro track where only drums are played.
if (channel >= 6)
return;
byte command = b & 0xF0;
byte op1 = (b >> 8) & 0xFF;
byte op2 = (b >> 16) & 0xFF;
switch (command) {
case MIDI_COMMAND_NOTE_OFF:
noteOff(channel, op1);
break;
case MIDI_COMMAND_NOTE_ON:
noteOn(channel, op1, op2);
break;
case MIDI_COMMAND_CONTROL_CHANGE:
controlChange(channel, op1, op2);
break;
case MIDI_COMMAND_PROGRAM_CHANGE:
programChange(channel, op1);
break;
default:
// Other MIDI events are ignored.
break;
}
}
void MidiDriver_Accolade_Cms::noteOff(uint8 channel, uint8 note) {
if (_activeNotes[channel] != note)
return;
// Remove the note from the active note registry.
_activeNotes[channel] = 0xFF;
// Turn off the frequency enable bit for the channel.
byte freqEnableRegValue = determineFrequencyEnableRegisterValue();
writeRegister(REGISTER_FREQUENCY_ENABLE, freqEnableRegValue);
}
void MidiDriver_Accolade_Cms::noteOn(uint8 channel, uint8 note, uint8 velocity) {
if (velocity == 0) {
// Note on with velocity 0 is a note off.
noteOff(channel, note);
return;
}
// Add the note to the active note registry.
_activeNotes[channel] = note;
// Add octaves to bring the note up into the 0x15 - 0x7F range, then lower
// the note with 0x15.
while (note <= 0x15)
note += 0xC;
note -= 0x15;
// Determine the octave and note within the octave.
byte octave = note / 0xC;
byte octaveNote = note % 0xC;
// Apply the volume adjustment for the current instrument on the channel.
int8 volumeAdjustment = static_cast(CMS_VOLUME_ADJUSTMENTS[_instruments[channel]]);
uint8 volume = CLIP(velocity + volumeAdjustment, 0, 0x7F);
// Calculate and write the amplitude register value.
byte amplitude = volume >> 4;
writeRegister(REGISTER_BASE_AMPLITUDE + channel, amplitude | (amplitude << 4));
// Look up and write the note frequecy.
byte frequency = CMS_NOTE_FREQUENCIES[octaveNote];
writeRegister(REGISTER_BASE_FREQUENCY + channel, frequency);
// An octave register contains the octaves for 2 channels.
// Get the current value of the register containing the octave value for
// this channel.
byte octaveRegisterValue = _octaveRegisterValues[channel / 2];
// Clear the bits containing the octave value for this channel.
if (channel & 1) {
// Octave is in the upper nibble.
octave <<= 4;
octaveRegisterValue &= 0x0F;
} else {
// Octave is in the lower nibble.
octaveRegisterValue &= 0xF0;
}
// Set and write the new octave value.
octaveRegisterValue |= octave;
_octaveRegisterValues[channel / 2] = octaveRegisterValue;
writeRegister(REGISTER_BASE_OCTAVE + (channel / 2), octaveRegisterValue);
// Turn on the frequency enable bit for the channel.
byte freqEnableRegValue = determineFrequencyEnableRegisterValue();
writeRegister(REGISTER_FREQUENCY_ENABLE, freqEnableRegValue);
}
void MidiDriver_Accolade_Cms::programChange(uint8 channel, uint8 instrument) {
// Just keep track of the current instrument.
_instruments[channel] = instrument;
}
void MidiDriver_Accolade_Cms::controlChange(uint8 channel, uint8 controller, uint8 value) {
// Only All Note Off is processed; it will turn off the active note on this
// channel (if there is one).
if (controller != MIDI_CONTROLLER_ALL_NOTES_OFF || _activeNotes[channel] == 0xFF)
return;
noteOff(channel, _activeNotes[channel]);
}
byte MidiDriver_Accolade_Cms::determineFrequencyEnableRegisterValue() {
byte freqEnableRegValue = 0;
for (int i = 0; i < 6; i++) {
// If a note is active on a channel, set the frequency enable bit for
// that channel.
if (_activeNotes[i] != 0xFF)
freqEnableRegValue |= 1 << i;
}
return freqEnableRegValue;
}
void MidiDriver_Accolade_Cms::writeRegister(uint16 reg, uint8 value) {
//debug("Writing register %02X %02X", reg, value);
_cms->writeReg(reg, value);
}
void MidiDriver_Accolade_Cms::setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) {
_timer_param = timer_param;
_timer_proc = timer_proc;
}
void MidiDriver_Accolade_Cms::onTimer() {
if (_timer_proc && _timer_param)
_timer_proc(_timer_param);
}
}