diff --git a/.gitmodules b/.gitmodules index 56f11c09..07576900 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "extern/cam900_vgsound_emu"] path = extern/cam900_vgsound_emu url = https://github.com/cam900/vgsound_emu +[submodule "extern/Nuked-OPL3"] + path = extern/Nuked-OPL3 + url = https://github.com/nukeykt/Nuked-OPL3.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 53aebac0..26853932 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,6 +226,7 @@ extern/adpcm/ymz_codec.c extern/Nuked-OPN2/ym3438.c extern/opm/opm.c extern/Nuked-OPLL/opll.c +extern/Nuked-OPL3/opl3.c extern/cam900_vgsound_emu/x1_010/x1_010.cpp @@ -301,6 +302,7 @@ src/engine/platform/ym2610b.cpp src/engine/platform/ym2610bext.cpp src/engine/platform/ay.cpp src/engine/platform/ay8930.cpp +src/engine/platform/opl.cpp src/engine/platform/tia.cpp src/engine/platform/saa.cpp src/engine/platform/amiga.cpp diff --git a/extern/Nuked-OPL3 b/extern/Nuked-OPL3 new file mode 160000 index 00000000..bb5c8d08 --- /dev/null +++ b/extern/Nuked-OPL3 @@ -0,0 +1 @@ +Subproject commit bb5c8d08a85779c42b75c79d7b84f365a1b93b66 diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 84b8315f..d1128af3 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -34,6 +34,7 @@ #include "platform/ym2610bext.h" #include "platform/ay.h" #include "platform/ay8930.h" +#include "platform/opl.h" #include "platform/tia.h" #include "platform/saa.h" #include "platform/amiga.h" @@ -211,6 +212,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do ((DivPlatformOPLL*)dispatch)->setVRC7(sys==DIV_SYSTEM_VRC7); ((DivPlatformOPLL*)dispatch)->setProperDrums(sys==DIV_SYSTEM_OPLL_DRUMS); break; + case DIV_SYSTEM_OPL3: + dispatch=new DivPlatformOPL; + break; case DIV_SYSTEM_SAA1099: { int saaCore=eng->getConfInt("saaCore",0); if (saaCore<0 || saaCore>2) saaCore=0; diff --git a/src/engine/platform/opl.cpp b/src/engine/platform/opl.cpp new file mode 100644 index 00000000..bb195a47 --- /dev/null +++ b/src/engine/platform/opl.cpp @@ -0,0 +1,823 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "opl.h" +#include "../engine.h" +#include +#include + +#define rWrite(a,v) if (!skipRegisterWrites) {pendingWrites[a]=v;} +#define immWrite(a,v) if (!skipRegisterWrites) {writes.emplace(a,v); if (dumpWrites) {addWrite(a,v);} } + +#define CHIP_FREQBASE 4720272 + +// N = invalid +#define N 255 + +/* +const unsigned char slotsOPL2[4][20]={ + {0, 1, 2, 6, 7, 8, 12, 13, 14}, // OP1 + {3, 4, 5, 9, 10, 11, 15, 16, 17}, // OP2 + {N, N, N, N, N, N, N, N, N}, + {N, N, N, N, N, N, N, N, N} +}; + +const unsigned char slotsOPL2Drums[4][20]={ + {0, 1, 2, 6, 7, 8, 12, 16, 14, 17, 13}, // OP1 + {3, 4, 5, 9, 10, 11, 15, N, N, N, N}, // OP2 + {N, N, N, N, N, N, N, N, N, N, N}, + {N, N, N, N, N, N, N, N, N, N, N} +}; + +const unsigned char slotsOPL3[4][20]={ + {0, 6, 1, 7, 2, 8, 18, 24, 19, 25, 20, 26, 30, 31, 32, 12, 13, 14}, // OP1 + {3, 9, 4, 10, 5, 11, 21, 27, 22, 28, 23, 29, 33, 34, 35, 15, 16, 17}, // OP2 + {6, N, 7, N, 8, N, 24, N, 25, N, 26, N, N, N, N, N, N, N}, // OP3 + {9, N, 10, N, 11, N, 27, N, 28, N, 29, N, N, N, N, N, N, N} // OP4 +}; + +const unsigned char slotsOPL3Drums[4][20]={ + {0, 6, 1, 7, 2, 8, 18, 24, 19, 25, 20, 26, 30, 31, 32, 12, 16, 14, 17, 13}, // OP1 + {3, 9, 4, 10, 5, 11, 21, 27, 22, 28, 23, 29, 33, 34, 35, N, N, N, N, N}, // OP2 + {6, N, 7, N, 8, N, 24, N, 25, N, 26, N, N, N, N, N, N, N, N, N}, // OP3 + {9, N, 10, N, 11, N, 27, N, 28, N, 29, N, N, N, N, N, N, N, N, N} // OP4 +}; +*/ + +#undef N + +const char* DivPlatformOPL::getEffectName(unsigned char effect) { + switch (effect) { + case 0x10: + return "10xy: Setup LFO (x: enable; y: speed)"; + break; + case 0x11: + return "11xx: Set feedback (0 to 7)"; + break; + case 0x12: + return "12xx: Set level of operator 1 (0 highest, 3F lowest)"; + break; + case 0x13: + return "13xx: Set level of operator 2 (0 highest, 3F lowest)"; + break; + case 0x14: + return "14xx: Set level of operator 3 (0 highest, 3F lowest; 4-op only)"; + break; + case 0x15: + return "15xx: Set level of operator 4 (0 highest, 3F lowest; 4-op only)"; + break; + case 0x16: + return "16xy: Set operator multiplier (x: operator from 1 to 4; y: multiplier)"; + break; + case 0x17: + return "17xx: Enable channel 6 DAC"; + break; + case 0x18: + return "18xx: Toggle extended channel 3 mode"; + break; + case 0x19: + return "19xx: Set attack of all operators (0 to F)"; + break; + case 0x1a: + return "1Axx: Set attack of operator 1 (0 to F)"; + break; + case 0x1b: + return "1Bxx: Set attack of operator 2 (0 to F)"; + break; + case 0x1c: + return "1Cxx: Set attack of operator 3 (0 to F; 4-op only)"; + break; + case 0x1d: + return "1Dxx: Set attack of operator 4 (0 to F; 4-op only)"; + break; + case 0x20: + return "20xy: Set PSG noise mode (x: preset freq/ch3 freq; y: thin pulse/noise)"; + break; + } + return NULL; +} + +void DivPlatformOPL::acquire_nuked(short* bufL, short* bufR, size_t start, size_t len) { + static short o[2]; + static int os[2]; + + for (size_t h=start; h>8)<<1),w.val); + //printf("write: %x = %.2x\n",w.addr,w.val); + lastBusy=0; + regPool[w.addr&0x1ff]=w.val; + writes.pop(); + } else { + lastBusy++; + //printf("busycounter: %d\n",lastBusy); + OPL3_WriteReg(&fm,0x0+((w.addr>>8)<<1),w.addr); + w.addrOrVal=true; + } + } + + OPL3_Generate(&fm,o); os[0]+=o[0]; os[1]+=o[1]; + + if (os[0]<-32768) os[0]=-32768; + if (os[0]>32767) os[0]=32767; + + if (os[1]<-32768) os[1]=-32768; + if (os[1]>32767) os[1]=32767; + + bufL[h]=os[0]; + bufR[h]=os[1]; + } +} + +void DivPlatformOPL::acquire(short* bufL, short* bufR, size_t start, size_t len) { + //if (useYMFM) { + // acquire_ymfm(bufL,bufR,start,len); + //} else { + acquire_nuked(bufL,bufR,start,len); + //} +} + +void DivPlatformOPL::tick() { + /* + for (int i=0; i<20; i++) { + if (i==2 && extMode) continue; + chan[i].std.next(); + + if (chan[i].std.hadVol) { + chan[i].outVol=(chan[i].vol*MIN(127,chan[i].std.vol))/127; + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + } + + if (chan[i].std.hadArp) { + if (!chan[i].inPorta) { + if (chan[i].std.arpMode) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].std.arp); + } else { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note+(signed char)chan[i].std.arp); + } + } + chan[i].freqChanged=true; + } else { + if (chan[i].std.arpMode && chan[i].std.finishedArp) { + chan[i].baseFreq=NOTE_FREQUENCY(chan[i].note); + chan[i].freqChanged=true; + } + } + + if (chan[i].std.hadAlg) { + chan[i].state.alg=chan[i].std.alg; + rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + if (!parent->song.algMacroBehavior) for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + } + if (chan[i].std.hadFb) { + chan[i].state.fb=chan[i].std.fb; + rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + } + if (chan[i].std.hadFms) { + chan[i].state.fms=chan[i].std.fms; + rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + } + if (chan[i].std.hadAms) { + chan[i].state.ams=chan[i].std.ams; + rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + } + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + DivMacroInt::IntOp& m=chan[i].std.op[j]; + if (m.hadAm) { + op.am=m.am; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.hadAr) { + op.ar=m.ar; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + if (m.hadDr) { + op.dr=m.dr; + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + } + if (m.hadMult) { + op.mult=m.mult; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.hadRr) { + op.rr=m.rr; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.hadSl) { + op.sl=m.sl; + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + } + if (m.hadTl) { + op.tl=127-m.tl; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + if (m.hadRs) { + op.rs=m.rs; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + if (m.hadDt) { + op.dt=m.dt; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + } + if (m.hadD2r) { + op.d2r=m.d2r; + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + } + if (m.hadSsg) { + op.ssgEnv=m.ssg; + rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); + } + } + + if (chan[i].keyOn || chan[i].keyOff) { + immWrite(0x28,0x00|konOffs[i]); + chan[i].keyOff=false; + } + } + */ + + for (int i=0; i<512; i++) { + if (pendingWrites[i]!=oldWrites[i]) { + immWrite(i,pendingWrites[i]&0xff); + oldWrites[i]=pendingWrites[i]; + } + } + + /* + for (int i=0; i<20; i++) { + if (chan[i].freqChanged) { + chan[i].freq=parent->calcFreq(chan[i].baseFreq,chan[i].pitch,false,octave(chan[i].baseFreq)); + if (chan[i].freq>262143) chan[i].freq=262143; + int freqt=toFreq(chan[i].freq); + immWrite(chanOffs[i]+ADDR_FREQH,freqt>>8); + immWrite(chanOffs[i]+ADDR_FREQ,freqt&0xff); + if (chan[i].furnaceDac && dacMode) { + double off=1.0; + if (dacSample>=0 && dacSamplesong.sampleLen) { + DivSample* s=parent->getSample(dacSample); + if (s->centerRate<1) { + off=1.0; + } else { + off=8363.0/(double)s->centerRate; + } + } + dacRate=(1280000*1.25*off)/MAX(1,chan[i].baseFreq); + if (dacRate<1) dacRate=1; + if (dumpWrites) addWrite(0xffff0001,1280000/dacRate); + } + chan[i].freqChanged=false; + } + if (chan[i].keyOn) { + immWrite(0x28,0xf0|konOffs[i]); + chan[i].keyOn=false; + } + } + */ +} + +int DivPlatformOPL::octave(int freq) { + if (freq>=82432) { + return 128; + } else if (freq>=41216) { + return 64; + } else if (freq>=20608) { + return 32; + } else if (freq>=10304) { + return 16; + } else if (freq>=5152) { + return 8; + } else if (freq>=2576) { + return 4; + } else if (freq>=1288) { + return 2; + } else { + return 1; + } + return 1; +} + +int DivPlatformOPL::toFreq(int freq) { + if (freq>=82432) { + return 0x3800|((freq>>7)&0x7ff); + } else if (freq>=41216) { + return 0x3000|((freq>>6)&0x7ff); + } else if (freq>=20608) { + return 0x2800|((freq>>5)&0x7ff); + } else if (freq>=10304) { + return 0x2000|((freq>>4)&0x7ff); + } else if (freq>=5152) { + return 0x1800|((freq>>3)&0x7ff); + } else if (freq>=2576) { + return 0x1000|((freq>>2)&0x7ff); + } else if (freq>=1288) { + return 0x800|((freq>>1)&0x7ff); + } else { + return freq&0x7ff; + } +} + +void DivPlatformOPL::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + /* + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[ch]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[ch].state.op[j]; + if (isMuted[ch]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[ch].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[ch].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + rWrite(chanOffs[ch]+ADDR_LRAF,(isMuted[ch]?0:(chan[ch].pan<<6))|(chan[ch].state.fms&7)|((chan[ch].state.ams&3)<<4)); + */ +} + +int DivPlatformOPL::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins); + + if (chan[c.chan].insChanged) { + chan[c.chan].state=ins->fm; + } + + chan[c.chan].std.init(ins); + if (!chan[c.chan].std.willVol) { + chan[c.chan].outVol=chan[c.chan].vol; + } + + /* + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + if (isMuted[c.chan]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[c.chan].state.alg][i]) { + if (!chan[c.chan].active || chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } + } else { + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + if (chan[c.chan].insChanged) { + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); + } + } + if (chan[c.chan].insChanged) { + rWrite(chanOffs[c.chan]+ADDR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); + } + */ + chan[c.chan].insChanged=false; + + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + } + chan[c.chan].keyOn=true; + chan[c.chan].active=true; + break; + } + case DIV_CMD_NOTE_OFF: + if (c.chan==5) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + } + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + break; + case DIV_CMD_NOTE_OFF_ENV: + if (c.chan==5) { + dacSample=-1; + if (dumpWrites) addWrite(0xffff0002,0); + } + chan[c.chan].keyOff=true; + chan[c.chan].keyOn=false; + chan[c.chan].active=false; + chan[c.chan].std.release(); + break; + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_VOLUME: { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.hasVol) { + chan[c.chan].outVol=c.value; + } + /* + for (int i=0; i<4; i++) { + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + if (isMuted[c.chan]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[c.chan].state.alg][i]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + } + */ + break; + } + case DIV_CMD_GET_VOLUME: { + return chan[c.chan].vol; + break; + } + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].insChanged=true; + } + chan[c.chan].ins=c.value; + break; + case DIV_CMD_PANNING: { + switch (c.value) { + case 0x01: + chan[c.chan].pan=1; + break; + case 0x10: + chan[c.chan].pan=2; + break; + default: + chan[c.chan].pan=3; + break; + } + //rWrite(chanOffs[c.chan]+ADDR_LRAF,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|(chan[c.chan].state.fms&7)|((chan[c.chan].state.ams&3)<<4)); + break; + } + case DIV_CMD_PITCH: { + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + int newFreq; + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + newFreq=chan[c.chan].baseFreq+c.value*octave(chan[c.chan].baseFreq); + if (newFreq>=destFreq) { + newFreq=destFreq; + return2=true; + } + } else { + newFreq=chan[c.chan].baseFreq-c.value*octave(chan[c.chan].baseFreq); + if (newFreq<=destFreq) { + newFreq=destFreq; + return2=true; + } + } + if (!chan[c.chan].portaPause) { + if (octave(chan[c.chan].baseFreq)!=octave(newFreq)) { + chan[c.chan].portaPause=true; + break; + } + } + chan[c.chan].baseFreq=newFreq; + chan[c.chan].portaPause=false; + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_SAMPLE_MODE: { + dacMode=c.value; + rWrite(0x2b,c.value<<7); + break; + } + case DIV_CMD_SAMPLE_BANK: + sampleBank=c.value; + if (sampleBank>(parent->song.sample.size()/12)) { + sampleBank=parent->song.sample.size()/12; + } + break; + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value); + chan[c.chan].note=c.value; + chan[c.chan].freqChanged=true; + break; + } + case DIV_CMD_FM_LFO: { + lfoValue=(c.value&7)|((c.value>>4)<<3); + rWrite(0x22,lfoValue); + break; + } + case DIV_CMD_FM_FB: { + chan[c.chan].state.fb=c.value&7; + //rWrite(chanOffs[c.chan]+ADDR_FB_ALG,(chan[c.chan].state.alg&7)|(chan[c.chan].state.fb<<3)); + break; + } + case DIV_CMD_FM_MULT: { + /* + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.mult=c.value2&15; + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + */ + break; + } + case DIV_CMD_FM_TL: { + /* + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.tl=c.value2; + if (isMuted[c.chan]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[c.chan].state.alg][c.value]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[c.chan].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + */ + break; + } + case DIV_CMD_FM_AR: { + /* + if (c.value<0) { + for (int i=0; i<4; i++) { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[i]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[i]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + } else { + DivInstrumentFM::Operator& op=chan[c.chan].state.op[orderedOps[c.value]]; + op.ar=c.value2&31; + unsigned short baseAddr=chanOffs[c.chan]|opOffs[orderedOps[c.value]]; + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + } + */ + + break; + } + case DIV_ALWAYS_SET_VOLUME: + return 0; + break; + case DIV_CMD_GET_VOLMAX: + return 127; + break; + case DIV_CMD_PRE_PORTA: + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_PRE_NOTE: + break; + default: + //printf("WARNING: unimplemented command %d\n",c.cmd); + break; + } + return 1; +} + +void DivPlatformOPL::forceIns() { + /* + for (int i=0; i<20; i++) { + for (int j=0; j<4; j++) { + unsigned short baseAddr=chanOffs[i]|opOffs[j]; + DivInstrumentFM::Operator& op=chan[i].state.op[j]; + if (isMuted[i]) { + rWrite(baseAddr+ADDR_TL,127); + } else { + if (isOutput[chan[i].state.alg][j]) { + rWrite(baseAddr+ADDR_TL,127-(((127-op.tl)*(chan[i].outVol&0x7f))/127)); + } else { + rWrite(baseAddr+ADDR_TL,op.tl); + } + } + rWrite(baseAddr+ADDR_MULT_DT,(op.mult&15)|(dtTable[op.dt&7]<<4)); + rWrite(baseAddr+ADDR_RS_AR,(op.ar&31)|(op.rs<<6)); + rWrite(baseAddr+ADDR_AM_DR,(op.dr&31)|(op.am<<7)); + rWrite(baseAddr+ADDR_DT2_D2R,op.d2r&31); + rWrite(baseAddr+ADDR_SL_RR,(op.rr&15)|(op.sl<<4)); + rWrite(baseAddr+ADDR_SSG,op.ssgEnv&15); + } + rWrite(chanOffs[i]+ADDR_FB_ALG,(chan[i].state.alg&7)|(chan[i].state.fb<<3)); + rWrite(chanOffs[i]+ADDR_LRAF,(isMuted[i]?0:(chan[i].pan<<6))|(chan[i].state.fms&7)|((chan[i].state.ams&3)<<4)); + if (chan[i].active) { + chan[i].keyOn=true; + chan[i].freqChanged=true; + } + } + if (dacMode) { + rWrite(0x2b,0x80); + } + immWrite(0x22,lfoValue); + */ +} + +void DivPlatformOPL::toggleRegisterDump(bool enable) { + DivDispatch::toggleRegisterDump(enable); +} + +void* DivPlatformOPL::getChanState(int ch) { + return &chan[ch]; +} + +unsigned char* DivPlatformOPL::getRegisterPool() { + return regPool; +} + +int DivPlatformOPL::getRegisterPoolSize() { + return 512; +} + +void DivPlatformOPL::reset() { + while (!writes.empty()) writes.pop(); + memset(regPool,0,512); + /* + if (useYMFM) { + fm_ymfm->reset(); + } + */ + OPL3_Reset(&fm,rate); + if (dumpWrites) { + addWrite(0xffffffff,0); + } + for (int i=0; i<20; i++) { + chan[i]=DivPlatformOPL::Channel(); + chan[i].vol=0x3f; + chan[i].outVol=0x3f; + } + + for (int i=0; i<512; i++) { + oldWrites[i]=-1; + pendingWrites[i]=-1; + } + + lastBusy=60; + dacMode=0; + dacPeriod=0; + dacPos=0; + dacRate=0; + dacSample=-1; + sampleBank=0; + lfoValue=8; + + extMode=false; + + // LFO + immWrite(0x22,lfoValue); + + delay=0; +} + +bool DivPlatformOPL::isStereo() { + return true; +} + +bool DivPlatformOPL::keyOffAffectsArp(int ch) { + return (ch>5); +} + +bool DivPlatformOPL::keyOffAffectsPorta(int ch) { + return (ch>5); +} + +void DivPlatformOPL::notifyInsChange(int ins) { + for (int i=0; i<20; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformOPL::notifyInsDeletion(void* ins) { +} + +void DivPlatformOPL::poke(unsigned int addr, unsigned short val) { + immWrite(addr,val); +} + +void DivPlatformOPL::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) immWrite(i.addr,i.val); +} + +int DivPlatformOPL::getPortaFloor(int ch) { + return (ch>5)?12:0; +} + +void DivPlatformOPL::setYMFM(bool use) { + useYMFM=use; +} + +void DivPlatformOPL::setFlags(unsigned int flags) { + /* + if (flags==3) { + chipClock=COLOR_NTSC*12.0/7.0; + } else if (flags==2) { + chipClock=8000000.0; + } else if (flags==1) { + chipClock=COLOR_PAL*12.0/7.0; + } else { + chipClock=COLOR_NTSC*15.0/7.0; + } + ladder=flags&0x80000000; + OPN2_SetChipType(ladder?ym3438_mode_ym2612:0); + if (useYMFM) { + if (fm_ymfm!=NULL) delete fm_ymfm; + if (ladder) { + fm_ymfm=new ymfm::ym2612(iface); + } else { + fm_ymfm=new ymfm::ym3438(iface); + } + rate=chipClock/144; + } else { + rate=chipClock/36; + }*/ + + chipClock=COLOR_NTSC*4.0; + rate=chipClock/32; +} + +int DivPlatformOPL::init(DivEngine* p, int channels, int sugRate, unsigned int flags) { + parent=p; + dumpWrites=false; + ladder=false; + skipRegisterWrites=false; + for (int i=0; i<20; i++) { + isMuted[i]=false; + } + setFlags(flags); + + reset(); + return 10; +} + +void DivPlatformOPL::quit() { +} + +DivPlatformOPL::~DivPlatformOPL() { +} diff --git a/src/engine/platform/opl.h b/src/engine/platform/opl.h new file mode 100644 index 00000000..c4248192 --- /dev/null +++ b/src/engine/platform/opl.h @@ -0,0 +1,120 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * 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 2 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef _OPL_H +#define _OPL_H +#include "../dispatch.h" +#include "../macroInt.h" +#include +#include "../../../extern/Nuked-OPL3/opl3.h" + +class DivPlatformOPL: public DivDispatch { + protected: + struct Channel { + DivInstrumentFM state; + DivMacroInt std; + unsigned char freqH, freqL; + int freq, baseFreq, pitch, note; + unsigned char ins; + bool active, insChanged, freqChanged, keyOn, keyOff, portaPause, furnaceDac, inPorta; + int vol, outVol; + unsigned char pan; + Channel(): + freqH(0), + freqL(0), + freq(0), + baseFreq(0), + pitch(0), + note(0), + ins(-1), + active(false), + insChanged(true), + freqChanged(false), + keyOn(false), + keyOff(false), + portaPause(false), + furnaceDac(false), + inPorta(false), + vol(0), + pan(3) {} + }; + Channel chan[20]; + bool isMuted[20]; + struct QueuedWrite { + unsigned short addr; + unsigned char val; + bool addrOrVal; + QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v), addrOrVal(false) {} + }; + std::queue writes; + opl3_chip fm; + int delay; + unsigned char lastBusy; + + unsigned char regPool[512]; + + bool dacMode; + int dacPeriod; + int dacRate; + unsigned int dacPos; + int dacSample; + unsigned char sampleBank; + unsigned char lfoValue; + + bool extMode, useYMFM; + bool ladder; + + short oldWrites[512]; + short pendingWrites[512]; + + int octave(int freq); + int toFreq(int freq); + + friend void putDispatchChan(void*,int,int); + + void acquire_nuked(short* bufL, short* bufR, size_t start, size_t len); + //void acquire_ymfm(short* bufL, short* bufR, size_t start, size_t len); + + public: + void acquire(short* bufL, short* bufR, size_t start, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + void reset(); + void forceIns(); + void tick(); + void muteChannel(int ch, bool mute); + bool isStereo(); + void setYMFM(bool use); + bool keyOffAffectsArp(int ch); + bool keyOffAffectsPorta(int ch); + void toggleRegisterDump(bool enable); + void setFlags(unsigned int flags); + void notifyInsChange(int ins); + void notifyInsDeletion(void* ins); + int getPortaFloor(int ch); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char* getEffectName(unsigned char effect); + int init(DivEngine* parent, int channels, int sugRate, unsigned int flags); + void quit(); + ~DivPlatformOPL(); +}; +#endif diff --git a/src/engine/platform/pcspkr.cpp b/src/engine/platform/pcspkr.cpp index 51353fed..5f57f31a 100644 --- a/src/engine/platform/pcspkr.cpp +++ b/src/engine/platform/pcspkr.cpp @@ -21,6 +21,15 @@ #include "../engine.h" #include +#ifdef __linux__ +#include +#include +#include +#include +#include +#include +#endif + #define PCSPKR_DIVIDER 4 #define CHIP_DIVIDER 1 @@ -106,6 +115,34 @@ void DivPlatformPCSpeaker::acquire_piezo(short* bufL, short* bufR, size_t start, } } +void DivPlatformPCSpeaker::beepFreq(int freq) { +#ifdef __linux__ + static struct input_event ie; + if (beepFD>=0) { + gettimeofday(&ie.time,NULL); + ie.type=EV_SND; + ie.code=SND_TONE; + ie.value=freq; + if (write(beepFD,&ie,sizeof(struct input_event))<0) { + perror("error while writing frequency!"); + } else { + //printf("writing freq: %d\n",freq); + } + } +#endif +} + +void DivPlatformPCSpeaker::acquire_real(short* bufL, short* bufR, size_t start, size_t len) { + if (lastOn!=on || lastFreq!=freq) { + lastOn=on; + lastFreq=freq; + beepFreq((on && !isMuted[0])?freq:0); + } + for (size_t i=start; i=0) close(beepFD); +#endif } DivPlatformPCSpeaker::~DivPlatformPCSpeaker() { diff --git a/src/engine/platform/pcspkr.h b/src/engine/platform/pcspkr.h index caf6b7f3..82e0f461 100644 --- a/src/engine/platform/pcspkr.h +++ b/src/engine/platform/pcspkr.h @@ -52,19 +52,22 @@ class DivPlatformPCSpeaker: public DivDispatch { }; Channel chan[1]; bool isMuted[1]; - bool on, flip; - int pos, speakerType; + bool on, flip, lastOn; + int pos, speakerType, beepFD; float low, band; float low2, high2, band2; float low3, band3; - unsigned short freq; + unsigned short freq, lastFreq; unsigned char regPool[2]; friend void putDispatchChan(void*,int,int); + void beepFreq(int freq); + void acquire_unfilt(short* bufL, short* bufR, size_t start, size_t len); void acquire_cone(short* bufL, short* bufR, size_t start, size_t len); void acquire_piezo(short* bufL, short* bufR, size_t start, size_t len); + void acquire_real(short* bufL, short* bufR, size_t start, size_t len); public: void acquire(short* bufL, short* bufR, size_t start, size_t len); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index cb0c6dc4..871dd97f 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -17,7 +17,6 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include #define _USE_MATH_DEFINES #include "gui.h" #include "util.h" @@ -4911,12 +4910,10 @@ bool FurnaceGUI::loop() { e->setSysFlags(i,(flags&(~3))|2,restart); updateWindowTitle(); } - /* - if (ImGui::RadioButton("Use system beeper",(flags&3)==3)) { + if (ImGui::RadioButton("Use system beeper (Linux only!)",(flags&3)==3)) { e->setSysFlags(i,(flags&(~3))|3,restart); updateWindowTitle(); } - */ break; } case DIV_SYSTEM_QSOUND: { diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index f8306c20..29085c56 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -60,7 +60,7 @@ const char* ssgEnvTypes[8]={ }; const char* fmParamNames[3][27]={ - {"Algorithm", "Feedback", "LFO > Freq", "LFO > Amp", "Attack", "Decay", "Decay 2", "Release", "Sustain", "Level", "EnvScale", "Multiplier", "Detune", "Detune 2", "SSG-EG", "AM", "AM Depth", "Vibrato Depth", "EnvAlternate", "EnvAlternate", "LevelScale/key", "Sustain", "Vibrato", "Waveform", "EnvScale/key", "OP2 HalfSine", "OP1 HalfSine"}, + {"Algorithm", "Feedback", "LFO > Freq", "LFO > Amp", "Attack", "Decay", "Decay 2", "Release", "Sustain", "Level", "EnvScale", "Multiplier", "Detune", "Detune 2", "SSG-EG", "AM", "AM Depth", "Vibrato Depth", "Sustained", "Sustained", "Level Scaling", "Sustain", "Vibrato", "Waveform", "Key Scale Rate", "OP2 Half Sine", "OP1 Half Sine"}, {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "SR", "RR", "SL", "TL", "KS", "MULT", "DT", "DT2", "SSG-EG", "AM", "AMD", "FMD", "EGT", "EGT", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM"}, {"ALG", "FB", "FMS/PMS", "AMS", "AR", "DR", "D2R", "RR", "SL", "TL", "RS", "MULT", "DT", "DT2", "SSG-EG", "AM", "DAM", "DVB", "EGT", "EGS", "KSL", "SUS", "VIB", "WS", "KSR", "DC", "DM"} };