//----------------------------------------------------------------------------- // // Copyright 2002-2016 Randy Heit // Copyright 2005-2014 Simon Howard // Copyright 2017 Christoph Oelckers // // 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/ // //----------------------------------------------------------------------------- // // This is mostly a reimplementation of the interface provided by // MusLib based on Chocolate-Doom's OPL player, although the // interface has been cleaned up a bit to be more consistent and readable. // // #include #include #include "musicblock.h" #include "c_cvars.h" musicBlock::musicBlock () { memset (this, 0, sizeof(*this)); for(auto &oplchannel : oplchannels) oplchannel.Panning = 64; // default to center panning. for(auto &voice : voices) voice.index = ~0u; // mark all free. } musicBlock::~musicBlock () { } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- int musicBlock::releaseVoice(uint32_t slot, uint32_t killed) { struct OPLVoice *ch = &voices[slot]; io->WriteFrequency(slot, ch->note, ch->pitch, 0); ch->index = ~0u; if (killed) io->MuteChannel(slot); return slot; } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- int musicBlock::findFreeVoice() { for (uint32_t i = 0; i < io->NumChannels; ++i) { if (voices[i].index == ~0u) { releaseVoice(i, 1); return i; } } return -1; } //---------------------------------------------------------------------------- // // When all voices are in use, we must discard an existing voice to // play a new note. Find and free an existing voice. The channel // passed to the function is the channel for the new note to be // played. // //---------------------------------------------------------------------------- int musicBlock::replaceExistingVoice() { // Check the allocated voices, if we find an instrument that is // of a lower priority to the new instrument, discard it. // If a voice is being used to play the second voice of an instrument, // use that, as second voices are non-essential. // Lower numbered MIDI channels implicitly have a higher priority // than higher-numbered channels, eg. MIDI channel 1 is never // discarded for MIDI channel 2. int result = 0; for (uint32_t i = 0; i < io->NumChannels; ++i) { if (voices[i].current_instr_voice == &voices[i].current_instr->voices[1] || voices[i].index >= voices[result].index) { result = i; } } releaseVoice(result, 1); return result; } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::voiceKeyOn(uint32_t slot, uint32_t channo, GenMidiInstrument *instrument, uint32_t instrument_voice, uint32_t key, uint32_t volume) { struct OPLVoice *voice = &voices[slot]; auto &channel = oplchannels[channo]; GenMidiVoice *gmvoice; voice->index = channo; voice->key = key; // Program the voice with the instrument data: voice->current_instr = instrument; gmvoice = voice->current_instr_voice = &instrument->voices[instrument_voice]; io->WriteInstrument(slot,gmvoice, channel.Vibrato); io->WritePan(slot, gmvoice, channel.Panning); // Set the volume level. voice->note_volume = volume; io->WriteVolume(slot, gmvoice, channel.Volume, channel.Expression, volume); // Write the frequency value to turn the note on. // Work out the note to use. This is normally the same as // the key, unless it is a fixed pitch instrument. int note; if (instrument->flags & GENMIDI_FLAG_FIXED) note = instrument->fixed_note; else if (channo == CHAN_PERCUSSION) note = 60; else note = key; // If this is the second voice of a double voice instrument, the // frequency index can be adjusted by the fine tuning field. voice->fine_tuning = (instrument_voice != 0) ? (voice->current_instr->fine_tuning / 2) - 64 : 0; voice->pitch = voice->fine_tuning + channel.Pitch; if (!(instrument->flags & GENMIDI_FLAG_FIXED) && channo != CHAN_PERCUSSION) { note += gmvoice->base_note_offset; } // Avoid possible overflow due to base note offset: while (note < 0) { note += 12; } while (note > HIGHEST_NOTE) { note -= 12; } voice->note = note; io->WriteFrequency(slot, note, voice->pitch, 1); } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- CVAR(Bool, opl_singlevoice, 0, 0) void musicBlock::noteOn(uint32_t channel, uint8_t key, int volume) { if (volume <= 0) { noteOff(channel, key); return; } GenMidiInstrument *instrument; // Percussion channel is treated differently. if (channel == CHAN_PERCUSSION) { if (key < GENMIDI_FIST_PERCUSSION || key >= GENMIDI_FIST_PERCUSSION + GENMIDI_NUM_PERCUSSION) { return; } instrument = &OPLinstruments[key + (GENMIDI_NUM_INSTRS - GENMIDI_FIST_PERCUSSION)]; } else { auto inst = oplchannels[channel].Instrument; if (inst >= GENMIDI_NUM_TOTAL) return; // better safe than sorry. instrument = &OPLinstruments[inst]; } bool double_voice = ((instrument->flags) & GENMIDI_FLAG_2VOICE) && !opl_singlevoice; int i = findFreeVoice(); if (i < 0) i = replaceExistingVoice(); if (i >= 0) { voiceKeyOn(i, channel, instrument, 0, key, volume); if (double_voice) { i = findFreeVoice(); if (i > 0) { voiceKeyOn(i, channel, instrument, 1, key, volume); } } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::noteOff(uint32_t id, uint8_t note) { uint32_t sustain = oplchannels[id].Sustain; for(uint32_t i = 0; i < io->NumChannels; i++) { if (voices[i].index == id && voices[i].key == note) { if (sustain >= MIN_SUSTAIN) voices[i].sustained = true; else releaseVoice(i, 0); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::changePitch(uint32_t id, int val1, int val2) { // Convert pitch from 14-bit to 7-bit, then scale it, since the player // code only understands sensitivities of 2 semitones. int pitch = ((val1 | (val2 << 7)) - 8192) * oplchannels[id].PitchSensitivity / (200 * 128) + 64; oplchannels[id].Pitch = pitch; for(uint32_t i = 0; i < io->NumChannels; i++) { auto &ch = voices[i]; if (ch.index == id) { ch.pitch = ch.fine_tuning + pitch; io->WriteFrequency(i, ch.note, ch.pitch, 1); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::changeModulation(uint32_t id, int value) { bool vibrato = (value >= VIBRATO_THRESHOLD); oplchannels[id].Vibrato = vibrato; for (uint32_t i = 0; i < io->NumChannels; i++) { auto &ch = voices[i]; if (ch.index == id) { io->WriteTremolo(i, ch.current_instr_voice, vibrato); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::changeSustain(uint32_t id, int value) { oplchannels[id].Sustain = value; if (value < MIN_SUSTAIN) { for (uint32_t i = 0; i < io->NumChannels; i++) { if (voices[i].index == id && voices[i].sustained) releaseVoice(i, 0); } } } //---------------------------------------------------------------------------- // // Change volume or expression. // Since both go to the same register, one function can handle both. // //---------------------------------------------------------------------------- void musicBlock::changeVolume(uint32_t id, int value, bool expression) { auto &chan = oplchannels[id]; if (!expression) chan.Volume = value; else chan.Expression = value; for (uint32_t i = 0; i < io->NumChannels; i++) { auto &ch = voices[i]; if (ch.index == id) { io->WriteVolume(i, ch.current_instr_voice, chan.Volume, chan.Expression, ch.note_volume); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::changePanning(uint32_t id, int value) { oplchannels[id].Panning = value; for(uint32_t i = 0; i < io->NumChannels; i++) { auto &ch = voices[i]; if (ch.index == id) { io->WritePan(i, ch.current_instr_voice, value); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::notesOff(uint32_t id, int value) { for (uint32_t i = 0; i < io->NumChannels; ++i) { if (voices[i].index == id) { if (oplchannels[id].Sustain >= MIN_SUSTAIN) voices[i].sustained = true; else releaseVoice(i, 0); } } } //---------------------------------------------------------------------------- // // release all notes for this channel // //---------------------------------------------------------------------------- void musicBlock::allNotesOff(uint32_t id, int value) { for (uint32_t i = 0; i < io->NumChannels; ++i) { if (voices[i].index == id) { releaseVoice(i, 0); } } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::changeExtended(uint32_t id, uint8_t controller, int value) { switch (controller) { case ctrlRPNHi: oplchannels[id].RPN = (oplchannels[id].RPN & 0x007F) | (value << 7); break; case ctrlRPNLo: oplchannels[id].RPN = (oplchannels[id].RPN & 0x3F80) | value; break; case ctrlNRPNLo: case ctrlNRPNHi: oplchannels[id].RPN = 0x3FFF; break; case ctrlDataEntryHi: if (oplchannels[id].RPN == 0) { oplchannels[id].PitchSensitivity = value * 100 + (oplchannels[id].PitchSensitivity % 100); } break; case ctrlDataEntryLo: if (oplchannels[id].RPN == 0) { oplchannels[id].PitchSensitivity = value + (oplchannels[id].PitchSensitivity / 100) * 100; } break; } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::resetControllers(uint32_t chan, int vol) { auto &channel = oplchannels[chan]; channel.Volume = vol; channel.Expression = 127; channel.Sustain = 0; channel.Pitch = 64; channel.RPN = 0x3fff; channel.PitchSensitivity = 200; } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::programChange(uint32_t channel, int value) { oplchannels[channel].Instrument = value; } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::resetAllControllers(int vol) { uint32_t i; for (i = 0; i < NUM_CHANNELS; i++) { resetControllers(i, vol); } } //---------------------------------------------------------------------------- // // // //---------------------------------------------------------------------------- void musicBlock::stopAllVoices() { for (uint32_t i = 0; i < io->NumChannels; i++) { if (voices[i].index != ~0u) releaseVoice(i, 1); } }