/* 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); } }