#include <stdint.h>
#include <cassert>
#include <math.h>
#include <string.h>
#include "debug.h"
#include "cmf.h"
#define BASE_CHAR_MULT 0x20
#define BASE_SCAL_LEVL 0x40
#define BASE_ATCK_DCAY 0x60
#define BASE_SUST_RLSE 0x80
#define BASE_FNUM_L 0xA0
#define BASE_KEYON_FREQ 0xB0
#define BASE_RHYTHM 0xBD
#define BASE_WAVE 0xE0
#define BASE_FEED_CONN 0xC0
#define OPLBIT_KEYON 0x20
#define OPLOFFSET(channel) (((channel) / 3) * 8 + ((channel) % 3))
uint8_t cDefaultPatches[] =
"\x01\x11\x4F\x00\xF1\xD2\x53\x74\x00\x00\x06"
"\x07\x12\x4F\x00\xF2\xF2\x60\x72\x00\x00\x08"
"\x31\xA1\x1C\x80\x51\x54\x03\x67\x00\x00\x0E"
"\x31\xA1\x1C\x80\x41\x92\x0B\x3B\x00\x00\x0E"
"\x31\x16\x87\x80\xA1\x7D\x11\x43\x00\x00\x08"
"\x30\xB1\xC8\x80\xD5\x61\x19\x1B\x00\x00\x0C"
"\xF1\x21\x01\x00\x97\xF1\x17\x18\x00\x00\x08"
"\x32\x16\x87\x80\xA1\x7D\x10\x33\x00\x00\x08"
"\x01\x12\x4F\x00\x71\x52\x53\x7C\x00\x00\x0A"
"\x02\x03\x8D\x00\xD7\xF5\x37\x18\x00\x00\x04"
"\x21\x21\xD1\x00\xA3\xA4\x46\x25\x00\x00\x0A"
"\x22\x22\x0F\x00\xF6\xF6\x95\x36\x00\x00\x0A"
"\xE1\xE1\x00\x00\x44\x54\x24\x34\x02\x02\x07"
"\xA5\xB1\xD2\x80\x81\xF1\x03\x05\x00\x00\x02"
"\x71\x22\xC5\x00\x6E\x8B\x17\x0E\x00\x00\x02"
"\x32\x21\x16\x80\x73\x75\x24\x57\x00\x00\x0E";
CPlayer *CcmfPlayer::factory(Copl *newopl)
{
return new CcmfPlayer(newopl);
}
CcmfPlayer::CcmfPlayer(Copl *newopl) :
CPlayer(newopl),
data(NULL),
pInstruments(NULL),
bPercussive(false),
iPrevCommand(0)
{
assert(OPLOFFSET(1-1) == 0x00);
assert(OPLOFFSET(5-1) == 0x09);
assert(OPLOFFSET(9-1) == 0x12);
}
CcmfPlayer::~CcmfPlayer()
{
if (this->data) delete[] data;
if (this->pInstruments) delete[] pInstruments;
}
bool CcmfPlayer::load(const std::string &filename, const CFileProvider &fp)
{
binistream *f = fp.open(filename); if(!f) return false;
char cSig[4];
f->readString(cSig, 4);
if (
(cSig[0] != 'C') ||
(cSig[1] != 'T') ||
(cSig[2] != 'M') ||
(cSig[3] != 'F')
) {
fp.close(f);
return false;
}
uint16_t iVer = f->readInt(2);
if ((iVer != 0x0101) && (iVer != 0x0100)) {
AdPlug_LogWrite("CMF file is not v1.0 or v1.1 (reports %d.%d)\n", iVer >> 8 , iVer & 0xFF);
fp.close(f);
return false;
}
this->cmfHeader.iInstrumentBlockOffset = f->readInt(2);
this->cmfHeader.iMusicOffset = f->readInt(2);
this->cmfHeader.iTicksPerQuarterNote = f->readInt(2);
this->cmfHeader.iTicksPerSecond = f->readInt(2);
this->cmfHeader.iTagOffsetTitle = f->readInt(2);
this->cmfHeader.iTagOffsetComposer = f->readInt(2);
this->cmfHeader.iTagOffsetRemarks = f->readInt(2);
if (this->cmfHeader.iTagOffsetTitle >= this->cmfHeader.iInstrumentBlockOffset)
this->cmfHeader.iTagOffsetTitle = 0;
if (this->cmfHeader.iTagOffsetComposer >= this->cmfHeader.iInstrumentBlockOffset)
this->cmfHeader.iTagOffsetComposer = 0;
if (this->cmfHeader.iTagOffsetRemarks >= this->cmfHeader.iInstrumentBlockOffset)
this->cmfHeader.iTagOffsetRemarks = 0;
f->readString((char *)this->cmfHeader.iChannelsInUse, 16);
if (iVer == 0x0100) {
this->cmfHeader.iNumInstruments = f->readInt(1);
this->cmfHeader.iTempo = 0;
} else { this->cmfHeader.iNumInstruments = f->readInt(2);
this->cmfHeader.iTempo = f->readInt(2);
}
f->seek(this->cmfHeader.iInstrumentBlockOffset);
this->pInstruments = new SBI[
(this->cmfHeader.iNumInstruments < 128) ? 128 : this->cmfHeader.iNumInstruments
];
for (int i = 0; i < this->cmfHeader.iNumInstruments; i++) {
this->pInstruments[i].op[0].iCharMult = f->readInt(1);
this->pInstruments[i].op[1].iCharMult = f->readInt(1);
this->pInstruments[i].op[0].iScalingOutput = f->readInt(1);
this->pInstruments[i].op[1].iScalingOutput = f->readInt(1);
this->pInstruments[i].op[0].iAttackDecay = f->readInt(1);
this->pInstruments[i].op[1].iAttackDecay = f->readInt(1);
this->pInstruments[i].op[0].iSustainRelease = f->readInt(1);
this->pInstruments[i].op[1].iSustainRelease = f->readInt(1);
this->pInstruments[i].op[0].iWaveSel = f->readInt(1);
this->pInstruments[i].op[1].iWaveSel = f->readInt(1);
this->pInstruments[i].iConnection = f->readInt(1);
f->seek(5, binio::Add); }
for (int i = this->cmfHeader.iNumInstruments; i < 128; i++) {
this->pInstruments[i].op[0].iCharMult = cDefaultPatches[(i % 16) * 11 + 0];
this->pInstruments[i].op[1].iCharMult = cDefaultPatches[(i % 16) * 11 + 1];
this->pInstruments[i].op[0].iScalingOutput = cDefaultPatches[(i % 16) * 11 + 2];
this->pInstruments[i].op[1].iScalingOutput = cDefaultPatches[(i % 16) * 11 + 3];
this->pInstruments[i].op[0].iAttackDecay = cDefaultPatches[(i % 16) * 11 + 4];
this->pInstruments[i].op[1].iAttackDecay = cDefaultPatches[(i % 16) * 11 + 5];
this->pInstruments[i].op[0].iSustainRelease = cDefaultPatches[(i % 16) * 11 + 6];
this->pInstruments[i].op[1].iSustainRelease = cDefaultPatches[(i % 16) * 11 + 7];
this->pInstruments[i].op[0].iWaveSel = cDefaultPatches[(i % 16) * 11 + 8];
this->pInstruments[i].op[1].iWaveSel = cDefaultPatches[(i % 16) * 11 + 9];
this->pInstruments[i].iConnection = cDefaultPatches[(i % 16) * 11 + 10];
}
if (this->cmfHeader.iTagOffsetTitle) {
f->seek(this->cmfHeader.iTagOffsetTitle);
this->strTitle = f->readString('\0');
}
if (this->cmfHeader.iTagOffsetComposer) {
f->seek(this->cmfHeader.iTagOffsetComposer);
this->strComposer = f->readString('\0');
}
if (this->cmfHeader.iTagOffsetRemarks) {
f->seek(this->cmfHeader.iTagOffsetRemarks);
this->strRemarks = f->readString('\0');
}
f->seek(this->cmfHeader.iMusicOffset);
this->iSongLen = fp.filesize(f) - this->cmfHeader.iMusicOffset;
this->data = new unsigned char[this->iSongLen];
f->readString((char *)data, this->iSongLen);
fp.close(f);
rewind(0);
return true;
}
bool CcmfPlayer::update()
{
this->iDelayRemaining = 0;
while (!this->iDelayRemaining) {
uint8_t iCommand = this->data[this->iPlayPointer++];
if ((iCommand & 0x80) == 0) {
this->iPlayPointer--;
iCommand = this->iPrevCommand;
} else {
this->iPrevCommand = iCommand;
}
uint8_t iChannel = iCommand & 0x0F;
switch (iCommand & 0xF0) {
case 0x80: { uint8_t iNote = this->data[this->iPlayPointer++];
uint8_t iVelocity = this->data[this->iPlayPointer++]; this->cmfNoteOff(iChannel, iNote, iVelocity);
break;
}
case 0x90: { uint8_t iNote = this->data[this->iPlayPointer++];
uint8_t iVelocity = this->data[this->iPlayPointer++]; if (iVelocity) {
if (iNotePlaying[iChannel] == iNote)
{ iVelocity = 0;
bNoteFix[iChannel] = true;
}
}
else {
if (bNoteFix[iChannel])
{ iVelocity = 127;
bNoteFix[iChannel] = false;
}
}
iNotePlaying[iChannel] = (iVelocity ? iNote : 255);
if (iVelocity) {
this->cmfNoteOn(iChannel, iNote, iVelocity);
} else {
this->cmfNoteOff(iChannel, iNote, iVelocity); break;
}
break;
}
case 0xA0: { uint8_t iNote = this->data[this->iPlayPointer++];
uint8_t iPressure = this->data[this->iPlayPointer++];
AdPlug_LogWrite("CMF: Key pressure not yet implemented! (wanted ch%d/note %d set to %d)\n", iChannel, iNote, iPressure);
break;
}
case 0xB0: { uint8_t iController = this->data[this->iPlayPointer++];
uint8_t iValue = this->data[this->iPlayPointer++];
this->MIDIcontroller(iChannel, iController, iValue);
break;
}
case 0xC0: { uint8_t iNewInstrument = this->data[this->iPlayPointer++];
this->chMIDI[iChannel].iPatch = iNewInstrument;
AdPlug_LogWrite("CMF: Remembering MIDI channel %d now uses patch %d\n", iChannel, iNewInstrument);
break;
}
case 0xD0: { uint8_t iPressure = this->data[this->iPlayPointer++];
AdPlug_LogWrite("CMF: Channel pressure not yet implemented! (wanted ch%d set to %d)\n", iChannel, iPressure);
break;
}
case 0xE0: { uint8_t iLSB = this->data[this->iPlayPointer++];
uint8_t iMSB = this->data[this->iPlayPointer++];
uint16_t iValue = (iMSB << 7) | iLSB;
this->chMIDI[iChannel].iPitchbend = iValue;
this->cmfNoteUpdate(iChannel);
AdPlug_LogWrite("CMF: Channel %d pitchbent to %d (%+.2f)\n", iChannel + 1, iValue, (float)(iValue - 8192) / 8192);
break;
}
case 0xF0: switch (iCommand) {
case 0xF0: { uint8_t iNextByte;
AdPlug_LogWrite("Sysex message: ");
do {
iNextByte = this->data[this->iPlayPointer++];
AdPlug_LogWrite("%02X", iNextByte);
} while ((iNextByte & 0x80) == 0);
AdPlug_LogWrite("\n");
break;
}
case 0xF1: this->data[this->iPlayPointer++]; break;
case 0xF2: this->data[this->iPlayPointer++]; this->data[this->iPlayPointer++];
break;
case 0xF3: this->data[this->iPlayPointer++]; AdPlug_LogWrite("CMF: MIDI Song Select is not implemented.\n");
break;
case 0xF6: break;
case 0xF7: break;
case 0xF8: case 0xFA: case 0xFB: case 0xFE: break;
case 0xFC: AdPlug_LogWrite("CMF: Received Real Time Stop message (0xFC)\n");
this->bSongEnd = true;
this->iPlayPointer = 0; break;
case 0xFF: { uint8_t iEvent = this->data[this->iPlayPointer++];
switch (iEvent) {
case 0x2F: AdPlug_LogWrite("CMF: End-of-track, stopping playback\n");
this->bSongEnd = true;
this->iPlayPointer = 0; break;
default:
AdPlug_LogWrite("CMF: Unknown MIDI meta-event 0xFF 0x%02X\n", iEvent);
break;
}
break;
}
default:
AdPlug_LogWrite("CMF: Unknown MIDI system command 0x%02X\n", iCommand);
break;
}
break;
default:
AdPlug_LogWrite("CMF: Unknown MIDI command 0x%02X\n", iCommand);
break;
}
if (this->iPlayPointer >= this->iSongLen) {
this->bSongEnd = true;
this->iPlayPointer = 0; }
this->iDelayRemaining = this->readMIDINumber();
}
return !this->bSongEnd;
}
void CcmfPlayer::rewind(int subsong)
{
this->opl->init();
this->writeOPL(0x01, 0x20);
this->writeOPL(0x05, 0x00);
this->writeOPL(0x08, 0x00);
this->writeOPL(BASE_FNUM_L + 8, 514 & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + 8, (1 << 2) | (514 >> 8));
this->writeOPL(BASE_FNUM_L + 7, 509 & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + 7, (2 << 2) | (509 >> 8));
this->writeOPL(BASE_FNUM_L + 6, 432 & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + 6, (2 << 2) | (432 >> 8));
this->writeOPL(0xBD, 0xC0);
this->bSongEnd = false;
this->iPlayPointer = 0;
this->iPrevCommand = 0; this->iNoteCount = 0;
this->iDelayRemaining = this->readMIDINumber();
for (int i = 0; i < 9; i++) {
this->chOPL[i].iNoteStart = 0; this->chOPL[i].iMIDINote = -1;
this->chOPL[i].iMIDIChannel = -1;
this->chOPL[i].iMIDIPatch = -1;
this->chMIDI[i].iPatch = -2;
this->chMIDI[i].iPitchbend = 8192;
this->chMIDI[i].iTranspose = 0;
}
for (int i = 9; i < 16; i++) {
this->chMIDI[i].iPatch = -2;
this->chMIDI[i].iPitchbend = 8192;
this->chMIDI[i].iTranspose = 0;
}
memset(this->iCurrentRegs, 0, 256);
memset(this->iNotePlaying, 255, sizeof(iNotePlaying));
memset(this->bNoteFix, false, sizeof(bNoteFix));
return;
}
float CcmfPlayer::getrefresh()
{
if (this->iDelayRemaining) {
return (float)this->cmfHeader.iTicksPerSecond / (float)this->iDelayRemaining;
} else {
return this->cmfHeader.iTicksPerSecond; }
}
std::string CcmfPlayer::gettitle()
{
return this->strTitle;
}
std::string CcmfPlayer::getauthor()
{
return this->strComposer;
}
std::string CcmfPlayer::getdesc()
{
return this->strRemarks;
}
uint32_t CcmfPlayer::readMIDINumber()
{
uint32_t iValue = 0;
for (int i = 0; i < 4; i++) {
uint8_t iNext = this->data[this->iPlayPointer++];
iValue <<= 7;
iValue |= (iNext & 0x7F); if ((iNext & 0x80) == 0) break; }
return iValue;
}
void CcmfPlayer::writeInstrumentSettings(uint8_t iChannel, uint8_t iOperatorSource, uint8_t iOperatorDest, uint8_t iInstrument)
{
assert(iChannel <= 8);
uint8_t iOPLOffset = OPLOFFSET(iChannel);
if (iOperatorDest) iOPLOffset += 3;
this->writeOPL(BASE_CHAR_MULT + iOPLOffset, this->pInstruments[iInstrument].op[iOperatorSource].iCharMult);
this->writeOPL(BASE_SCAL_LEVL + iOPLOffset, this->pInstruments[iInstrument].op[iOperatorSource].iScalingOutput);
this->writeOPL(BASE_ATCK_DCAY + iOPLOffset, this->pInstruments[iInstrument].op[iOperatorSource].iAttackDecay);
this->writeOPL(BASE_SUST_RLSE + iOPLOffset, this->pInstruments[iInstrument].op[iOperatorSource].iSustainRelease);
this->writeOPL(BASE_WAVE + iOPLOffset, this->pInstruments[iInstrument].op[iOperatorSource].iWaveSel);
this->writeOPL(BASE_FEED_CONN + iChannel, this->pInstruments[iInstrument].iConnection);
return;
}
void CcmfPlayer::writeOPL(uint8_t iRegister, uint8_t iValue)
{
this->opl->write(iRegister, iValue);
this->iCurrentRegs[iRegister] = iValue;
return;
}
void CcmfPlayer::getFreq(uint8_t iChannel, uint8_t iNote, uint8_t * iBlock, uint16_t * iOPLFNum)
{
*iBlock = iNote / 12;
if (*iBlock > 1) (*iBlock)--;
double d = pow(2, (
(double)iNote + (
(this->chMIDI[iChannel].iPitchbend - 8192) / 8192.0
) + (
this->chMIDI[iChannel].iTranspose / 256.0
) - 9) / 12.0 - (*iBlock - 20))
* 440.0 / 32.0 / 50000.0;
*iOPLFNum = (uint16_t)(d+0.5);
}
void CcmfPlayer::cmfNoteOn(uint8_t iChannel, uint8_t iNote, uint8_t iVelocity)
{
uint8_t iBlock = 0;
uint16_t iOPLFNum = 0;
getFreq(iChannel, iNote, &iBlock, &iOPLFNum);
if (iOPLFNum > 1023) AdPlug_LogWrite("CMF: This note is out of range! (send this song to malvineous@shikadi.net!)\n");
if ((iChannel > 10) && (this->bPercussive)) {
uint8_t iPercChannel = this->getPercChannel(iChannel);
this->MIDIchangeInstrument(iPercChannel, iChannel, this->chMIDI[iChannel].iPatch);
int iLevel = 0x25 - sqrt(iVelocity * 16); if (iVelocity > 0x7b) iLevel = 0; if (iLevel < 0) iLevel = 0;
if (iLevel > 0x3F) iLevel = 0x3F;
int iOPLOffset = BASE_SCAL_LEVL + OPLOFFSET(iPercChannel);
if (iChannel == 11) iOPLOffset += 3; this->writeOPL(iOPLOffset, (this->iCurrentRegs[iOPLOffset] & ~0x3F) | iLevel);
this->writeOPL(BASE_FNUM_L + iPercChannel, iOPLFNum & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + iPercChannel, (iBlock << 2) | ((iOPLFNum >> 8) & 0x03));
uint8_t iBit = 1 << (15 - iChannel);
if (this->iCurrentRegs[BASE_RHYTHM] & iBit) this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] & ~iBit);
this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] | iBit);
this->chOPL[iPercChannel].iNoteStart = ++this->iNoteCount;
this->chOPL[iPercChannel].iMIDIChannel = iChannel;
this->chOPL[iPercChannel].iMIDINote = iNote;
} else {
int iOPLChannel = -1;
int iNumChannels = this->bPercussive ? 6 : 9;
for (int i = iNumChannels - 1; i >= 0; i--) {
if (this->chOPL[i].iNoteStart == 0) {
iOPLChannel = i;
if (this->chOPL[i].iMIDIPatch == this->chMIDI[iChannel].iPatch) {
break;
} }
}
if (iOPLChannel == -1) {
iOPLChannel = 0;
int iEarliest = this->chOPL[0].iNoteStart;
for (int i = 1; i < iNumChannels; i++) {
if (this->chOPL[i].iNoteStart < iEarliest) {
iOPLChannel = i;
iEarliest = this->chOPL[i].iNoteStart;
}
}
AdPlug_LogWrite("CMF: Too many polyphonic notes, cutting note on channel %d\n", iOPLChannel);
}
if (this->chOPL[iOPLChannel].iMIDIPatch != this->chMIDI[iChannel].iPatch) {
this->MIDIchangeInstrument(iOPLChannel, iChannel, this->chMIDI[iChannel].iPatch);
}
this->chOPL[iOPLChannel].iNoteStart = ++this->iNoteCount;
this->chOPL[iOPLChannel].iMIDIChannel = iChannel;
this->chOPL[iOPLChannel].iMIDINote = iNote;
#ifdef USE_VELOCITY
uint8_t iOPLOffset = BASE_SCAL_LEVL + OPLOFFSET(iChannel) + 3; uint16_t iLevel = 0x2F - (iVelocity * 0x2F / 127); this->writeOPL(iOPLOffset, (this->iCurrentRegs[iOPLOffset] & ~0x3F) | iLevel);
#endif
this->writeOPL(BASE_FNUM_L + iOPLChannel, iOPLFNum & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + iOPLChannel, OPLBIT_KEYON | (iBlock << 2) | ((iOPLFNum & 0x300) >> 8));
}
return;
}
void CcmfPlayer::cmfNoteOff(uint8_t iChannel, uint8_t iNote, uint8_t iVelocity)
{
if ((iChannel > 10) && (this->bPercussive)) {
int iOPLChannel = this->getPercChannel(iChannel);
if (this->chOPL[iOPLChannel].iMIDINote != iNote) return; this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] & ~(1 << (15 - iChannel)));
this->chOPL[iOPLChannel].iNoteStart = 0; } else { int iOPLChannel = -1;
int iNumChannels = this->bPercussive ? 6 : 9;
for (int i = 0; i < iNumChannels; i++) {
if (
(this->chOPL[i].iMIDIChannel == iChannel) &&
(this->chOPL[i].iMIDINote == iNote) &&
(this->chOPL[i].iNoteStart != 0)
) {
this->chOPL[i].iNoteStart = 0;
iOPLChannel = i;
break;
}
}
if (iOPLChannel == -1) return;
this->writeOPL(BASE_KEYON_FREQ + iOPLChannel, this->iCurrentRegs[BASE_KEYON_FREQ + iOPLChannel] & ~OPLBIT_KEYON);
}
return;
}
void CcmfPlayer::cmfNoteUpdate(uint8_t iChannel)
{
uint8_t iBlock = 0;
uint16_t iOPLFNum = 0;
if ((iChannel > 10) && (this->bPercussive)) {
uint8_t iPercChannel = this->getPercChannel(iChannel);
getFreq(iChannel, this->chOPL[iPercChannel].iMIDINote, &iBlock, &iOPLFNum);
this->writeOPL(BASE_FNUM_L + iPercChannel, iOPLFNum & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + iPercChannel, (iBlock << 2) | ((iOPLFNum >> 8) & 0x03));
} else {
int iNumChannels = this->bPercussive ? 6 : 9;
for (int i = 0; i < iNumChannels; i++) {
if (this->chOPL[i].iMIDIChannel == iChannel && this->chOPL[i].iNoteStart > 0) {
getFreq(iChannel, this->chOPL[i].iMIDINote, &iBlock, &iOPLFNum);
this->writeOPL(BASE_FNUM_L + i, iOPLFNum & 0xFF);
this->writeOPL(BASE_KEYON_FREQ + i, OPLBIT_KEYON | (iBlock << 2) | ((iOPLFNum & 0x300) >> 8));
}
}
}
return;
}
uint8_t CcmfPlayer::getPercChannel(uint8_t iChannel)
{
switch (iChannel) {
case 11: return 7-1; case 12: return 8-1; case 13: return 9-1; case 14: return 9-1; case 15: return 8-1; }
AdPlug_LogWrite("CMF ERR: Tried to get the percussion channel from MIDI channel %d - this shouldn't happen!\n", iChannel);
return 0;
}
void CcmfPlayer::MIDIchangeInstrument(uint8_t iOPLChannel, uint8_t iMIDIChannel, uint8_t iNewInstrument)
{
if ((iMIDIChannel > 10) && (this->bPercussive)) {
switch (iMIDIChannel) {
case 11: this->writeInstrumentSettings(7-1, 0, 0, iNewInstrument);
this->writeInstrumentSettings(7-1, 1, 1, iNewInstrument);
break;
case 12: this->writeInstrumentSettings(8-1, 0, 1, iNewInstrument);
break;
case 13: this->writeInstrumentSettings(9-1, 0, 0, iNewInstrument);
break;
case 14: this->writeInstrumentSettings(9-1, 0, 1, iNewInstrument);
break;
case 15: this->writeInstrumentSettings(8-1, 0, 0, iNewInstrument);
break;
default:
AdPlug_LogWrite("CMF: Invalid MIDI channel %d (not melodic and not percussive!)\n", iMIDIChannel + 1);
break;
}
this->chOPL[iOPLChannel].iMIDIPatch = iNewInstrument;
} else {
this->writeInstrumentSettings(iOPLChannel, 0, 0, iNewInstrument);
this->writeInstrumentSettings(iOPLChannel, 1, 1, iNewInstrument);
this->chOPL[iOPLChannel].iMIDIPatch = iNewInstrument;
}
return;
}
void CcmfPlayer::MIDIcontroller(uint8_t iChannel, uint8_t iController, uint8_t iValue)
{
switch (iController) {
case 0x63:
if (iValue) {
this->writeOPL(BASE_RHYTHM, (this->iCurrentRegs[BASE_RHYTHM] & ~0xC0) | (iValue << 6)); } else {
this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] & ~0xC0); }
AdPlug_LogWrite("CMF: AM+VIB depth change - AM %s, VIB %s\n",
(this->iCurrentRegs[BASE_RHYTHM] & 0x80) ? "on" : "off",
(this->iCurrentRegs[BASE_RHYTHM] & 0x40) ? "on" : "off");
break;
case 0x66:
AdPlug_LogWrite("CMF: Song set marker to 0x%02X\n", iValue);
break;
case 0x67:
this->bPercussive = (iValue != 0);
if (this->bPercussive) {
this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] | 0x20); } else {
this->writeOPL(BASE_RHYTHM, this->iCurrentRegs[BASE_RHYTHM] & ~0x20); }
AdPlug_LogWrite("CMF: Percussive/rhythm mode %s\n", this->bPercussive ? "enabled" : "disabled");
break;
case 0x68:
this->chMIDI[iChannel].iTranspose = iValue;
this->cmfNoteUpdate(iChannel);
AdPlug_LogWrite("CMF: Transposing all notes up by %d * 1/128ths of a semitone on channel %d.\n", iValue, iChannel + 1);
break;
case 0x69:
this->chMIDI[iChannel].iTranspose = -iValue;
this->cmfNoteUpdate(iChannel);
AdPlug_LogWrite("CMF: Transposing all notes down by %d * 1/128ths of a semitone on channel %d.\n", iValue, iChannel + 1);
break;
default:
AdPlug_LogWrite("CMF: Unsupported MIDI controller 0x%02X, ignoring.\n", iController);
break;
}
return;
}