diff --git a/CMakeLists.txt b/CMakeLists.txt index 87c684e02..4059c694f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -718,6 +718,7 @@ src/engine/platform/esfm.cpp src/engine/platform/powernoise.cpp src/engine/platform/dave.cpp src/engine/platform/gbadma.cpp +src/engine/platform/gbaminmod.cpp src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 06157e841..079a49022 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -251,6 +251,10 @@ enum DivDispatchCmds { DIV_CMD_POWERNOISE_COUNTER_LOAD, // (which, val) DIV_CMD_POWERNOISE_IO_WRITE, // (port, value) + + DIV_CMD_MINMOD_ECHO, + + DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol DIV_CMD_DAVE_HIGH_PASS, DIV_CMD_DAVE_RING_MOD, diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 420876a8e..2840ce740 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -82,6 +82,7 @@ #include "platform/ted.h" #include "platform/c140.h" #include "platform/gbadma.h" +#include "platform/gbaminmod.h" #include "platform/pcmdac.h" #include "platform/esfm.h" #include "platform/powernoise.h" @@ -648,6 +649,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_GBA_DMA: dispatch=new DivPlatformGBADMA; break; + case DIV_SYSTEM_GBA_MINMOD: + dispatch=new DivPlatformGBAMinMod; + break; case DIV_SYSTEM_PCM_DAC: dispatch=new DivPlatformPCMDAC; break; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index fabb26ac3..fe7b5f41e 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -983,7 +983,8 @@ void DivEngine::delUnusedSamples() { i->type==DIV_INS_K053260 || i->type==DIV_INS_C140 || i->type==DIV_INS_C219 || - i->type==DIV_INS_GBA_DMA) { + i->type==DIV_INS_GBA_DMA || + i->type==DIV_INS_GBA_MINMOD) { if (i->amiga.initSample>=0 && i->amiga.initSampleamiga.initSample]=true; } diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 5e1822e46..90f0e2309 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -90,6 +90,7 @@ enum DivInstrumentType: unsigned short { DIV_INS_POWERNOISE_SLOPE=57, DIV_INS_DAVE=58, DIV_INS_GBA_DMA=59, + DIV_INS_GBA_MINMOD=60, DIV_INS_MAX, DIV_INS_NULL }; diff --git a/src/engine/platform/gbaminmod.cpp b/src/engine/platform/gbaminmod.cpp new file mode 100644 index 000000000..118882687 --- /dev/null +++ b/src/engine/platform/gbaminmod.cpp @@ -0,0 +1,725 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 "gbaminmod.h" +#include "../engine.h" +#include "../../ta-log.h" +#include + +#define CHIP_FREQBASE 16777216 + +#define rWrite(a,v) {regPool[a]=v;} + +const char* regCheatSheetMinMod[]={ + "CHx_Counter", "x0", + "CHx_Address", "x2", + "CHx_LastLeft", "x4", + "CHx_LastRight", "x6", + "CHx_Freq", "x8", + "CHx_LoopEnd", "xA", + "CHx_LoopStart", "xC", + "CHx_VolumeLeft", "xE", + "CHx_VolumeRight", "xF", + NULL +}; + +const char** DivPlatformGBAMinMod::getRegisterSheet() { + return regCheatSheetMinMod; +} + +void DivPlatformGBAMinMod::acquire(short** buf, size_t len) { + short sampL, sampR; + size_t sampPos=mixBufReadPos&3; + bool newSamp=true; + // cache channel registers that might change + struct { + unsigned long long address; + unsigned int freq, loopEnd, loopStart; + short volL, volR; + } chState[16]; + for (int i=0; i=sampCycles) { + // the driver generates 4 samples at a time and can be start-offset + sampPos=mixBufReadPos&3; + if (sampPos==mixBufOffset) { + for (int j=mixBufOffset; j<4; j++) { + mixOut[0][j]=0; + mixOut[1][j]=0; + } + for (int i=0; i>32; + chState[i].address+=((unsigned long long)chState[i].freq)<<8; + unsigned int newAddr=chState[i].address>>32; + if (newAddr!=lastAddr) { + if (newAddr>=chState[i].loopEnd) { + newAddr=newAddr-chState[i].loopEnd+chState[i].loopStart; + chState[i].address=(chState[i].address&0xffffffff)|((unsigned long long)newAddr<<32); + } + int newSamp=0; + switch (newAddr>>24) { + case 2: // wavetable + newAddr&=0x0003ffff; + if (newAddr0x800) { + newSamp=mixBuf[(newAddr-0x800)/1024][newAddr&1023]; + } + break; + case 8: // sample + case 9: + case 10: + case 11: + case 12: + newSamp=sampleMem[newAddr&0x01ffffff]; + break; + } + chanOut[i][0]=newSamp*chState[i].volL; + chanOut[i][1]=newSamp*chState[i].volR; + } + int outL=chanOut[i][0]; + int outR=chanOut[i][1]; + int outA=(chan[i].invertL==chan[i].invertR)?outL+outR:outL-outR; + mixOut[0][j]+=(unsigned char)(outL>>15); + mixOut[1][j]+=(unsigned char)(outR>>15); + oscOut[i][j]=volScale>0?outA*64/volScale:0; + } + } + for (int j=mixBufOffset; j<4; j++) { + mixBuf[mixBufPage][mixBufWritePos]=mixOut[0][j]; + mixBuf[mixBufPage+1][mixBufWritePos]=mixOut[1][j]; + mixBufWritePos++; + } + mixBufOffset=0; + } + newSamp=true; + mixBufReadPos++; + sampTimer-=sampCycles; + } + if (newSamp) { + // assuming max PCM FIFO volume + sampL=((short)mixOut[0][sampPos]<<8)&(0xff80<<(9-dacDepth)); + sampR=((short)mixOut[1][sampPos]<<8)&(0xff80<<(9-dacDepth)); + newSamp=false; + } + buf[0][h]=sampL; + buf[1][h]=sampR; + for (int i=0; idata[oscBuf[i]->needle++]=oscOut[i][sampPos]; + } + for (int i=chanMax; i<16; i++) { + oscBuf[i]->data[oscBuf[i]->needle++]=0; + } + while (updTimer>=updCycles) { + // flip buffer + // logV("ut=%d,pg=%d,w=%d,r=%d,sc=%d,st=%d",updTimer,mixBufPage,mixBufWritePos,mixBufReadPos,sampCycles,sampTimer); + mixBufPage=(mixBufPage+2)%(mixBufs*2); + memset(mixBuf[mixBufPage],0,sizeof(mixBuf[mixBufPage])); + memset(mixBuf[mixBufPage+1],0,sizeof(mixBuf[mixBufPage+1])); + // emulate buffer loss prevention and buffer copying + sampsRendered+=mixBufReadPos; + mixBufOffset=(4-(mixBufReadPos&3))&3; + mixBufReadPos=0; + mixBufWritePos=mixBufOffset; + for (int j=0; jgetCurHz(); + float updCyclesNew=(hz>=1)?(16777216.f/hz):1; + // the maximum buffer size in the default multi-rate config is 1024 samples + // (so 15 left/right buffers + mixer code fit the entire 32k of internal RAM) + // if the driver determines that the current tick rate is too low, it will + // internally double the rate until the resulting buffer size fits + while (true) { + updCycles=floorf(updCyclesNew); + // emulate prescaler rounding + if (updCycles>=65536*256) { + updCycles&=~1024; + } else if (updCycles>=65536*64) { + updCycles&=~256; + } else if (updCycles>=65536) { + updCycles&=~64; + } + unsigned int bufSize=(updCycles/sampCycles+3)&~3; + if (bufSize<1024 || updCyclesNew<1) { + break; + } + updCyclesNew/=2; + } + } + updTimer+=1<>16)&0xffff; + chReg[2]=(chState[i].address>>32)&0xffff; + chReg[3]=(chState[i].address>>48)&0xffff; + chReg[4]=(chanOut[i][0]>>7)&0xff00; + chReg[5]=0; + chReg[6]=(chanOut[i][1]>>7)&0xff00; + chReg[7]=0; + chReg[10]=chState[i].loopEnd&0xffff; + chReg[11]=(chState[i].loopEnd>>16)&0xffff; + chReg[12]=chState[i].loopStart&0xffff; + chReg[13]=(chState[i].loopStart>>16)&0xffff; + } +} + +void DivPlatformGBAMinMod::tick(bool sysTick) { + // collect stats for display in chip config + // logV("rendered=%d,updTot=%d",sampsRendered,updCyclesTotal); + if (sysTick && sampsRendered>0) { + // assuming new sample, L!=R and lowest ROM access wait in all channels + // this gives 39.5 cycles/sample, rounded up to 40 for loops + maxCPU=(float)sampsRendered*chanMax*40/updCyclesTotal; + } + sampsRendered=0; + updCyclesTotal=0; + + for (int i=0; icalcArp(chan[i].note,chan[i].std.arp.val)); + } + chan[i].freqChanged=true; + } + if (chan[i].useWave && chan[i].std.wave.had) { + if (chan[i].wave!=chan[i].std.wave.val || chan[i].ws.activeChanged()) { + chan[i].wave=chan[i].std.wave.val; + chan[i].ws.changeWave1(chan[i].wave); + } + } + if (chan[i].std.pitch.had) { + if (chan[i].std.pitch.mode) { + chan[i].pitch2+=chan[i].std.pitch.val; + CLAMP_VAR(chan[i].pitch2,-32768,32767); + } else { + chan[i].pitch2=chan[i].std.pitch.val; + } + chan[i].freqChanged=true; + } + if (chan[i].std.panL.had) { + chan[i].chPanL=(255*(chan[i].std.panL.val&255))/chan[i].macroPanMul; + chan[i].volChangedL=true; + } + + if (chan[i].std.panR.had) { + chan[i].chPanR=(255*(chan[i].std.panR.val&255))/chan[i].macroPanMul; + chan[i].volChangedR=true; + } + if (chan[i].std.phaseReset.had) { + if ((chan[i].std.phaseReset.val==1) && chan[i].active) { + chan[i].audPos=0; + chan[i].setPos=true; + } + } + if (chan[i].std.ex1.had) { + if (chan[i].invertL!=(bool)(chan[i].std.ex1.val&16)) { + chan[i].invertL=chan[i].std.ex1.val&2; + chan[i].volChangedL=true; + } + if (chan[i].invertR!=(bool)(chan[i].std.ex1.val&8)) { + chan[i].invertR=chan[i].std.ex1.val&1; + chan[i].volChangedR=true; + } + } + if (chan[i].setPos) { + // force keyon + chan[i].keyOn=true; + chan[i].setPos=false; + } else { + chan[i].audPos=0; + } + if (chan[i].useWave && chan[i].active) { + if (chan[i].ws.tick()) { + updateWave(i); + } + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + DivSample* s=parent->getSample(chan[i].sample); + double off=(s->centerRate>=1)?((double)s->centerRate/8363.0):1.0; + chan[i].freq=(int)(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE)); + if (chan[i].keyOn) { + unsigned int start, end, loop; + if (chan[i].echo!=0) { + // make sure echo channels' frequency can't be faster than the sample rate + if (chan[i].echo!=0 && chan[i].freq>CHIP_FREQBASE) { + chan[i].freq=CHIP_FREQBASE; + } + // this is only to match the HLE implementation + // the actual engine will handle mid-frame echo switch differently + start=loop=0x08000000; + end=0x08000001; + } else if (chan[i].useWave) { + start=(i*256)|0x02000000; + end=start+chan[i].wtLen; + loop=start; + } else { + size_t maxPos=getSampleMemCapacity(); + start=sampleOff[chan[i].sample]; + if (s->isLoopable()) { + end=MIN(start+MAX(s->length8,1),maxPos); + loop=start+s->loopStart; + } else { + end=MIN(start+s->length8+16,maxPos); + loop=MIN(start+s->length8,maxPos); + } + if (chan[i].audPos>0) { + start=start+MIN(chan[i].audPos,end); + } + start|=0x08000000; + end|=0x08000000; + loop|=0x08000000; + } + rWrite(2+i*16,start&0xffff); + rWrite(3+i*16,start>>16); + rWrite(10+i*16,end&0xffff); + rWrite(11+i*16,end>>16); + rWrite(12+i*16,loop&0xffff); + rWrite(13+i*16,loop>>16); + if (!chan[i].std.vol.had) { + chan[i].outVol=chan[i].vol; + } + chan[i].volChangedL=true; + chan[i].volChangedR=true; + chan[i].keyOn=false; + } + if (chan[i].keyOff) { + chan[i].volChangedL=true; + chan[i].volChangedR=true; + chan[i].keyOff=false; + } + if (chan[i].freqChanged) { + rWrite(8+i*16,chan[i].freq&0xffff); + rWrite(9+i*16,chan[i].freq>>16); + chan[i].freqChanged=false; + } + } + // don't scale echo channels + if (chan[i].volChangedL) { + int out=chan[i].outVol*chan[i].chPanL; + if ((chan[i].echo&0xf)==0) out=(out*volScale)>>16; + else out=out>>1; + if (chan[i].invertL) out=-out; + rWrite(14+i*16,(isMuted[i] || !chan[i].active)?0:out); + chan[i].volChangedL=false; + } + if (chan[i].volChangedR) { + int out=chan[i].outVol*chan[i].chPanR; + if ((chan[i].echo&0xf)==0) out=(out*volScale)>>16; + else out=out>>1; + if (chan[i].invertR) out=-out; + rWrite(15+i*16,(isMuted[i] || !chan[i].active)?0:out); + chan[i].volChangedR=false; + } + } +} + +int DivPlatformGBAMinMod::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA); + chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:255; + chan[c.chan].macroPanMul=ins->type==DIV_INS_AMIGA?127:255; + if (ins->amiga.useWave) { + chan[c.chan].useWave=true; + chan[c.chan].wtLen=ins->amiga.waveLen+1; + if (chan[c.chan].insChanged) { + if (chan[c.chan].wave<0) { + chan[c.chan].wave=0; + } + chan[c.chan].ws.setWidth(chan[c.chan].wtLen); + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + } + chan[c.chan].ws.init(ins,chan[c.chan].wtLen,255,chan[c.chan].insChanged); + } else { + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].sample=ins->amiga.getSample(c.value); + c.value=ins->amiga.getFreq(c.value); + } + chan[c.chan].useWave=false; + } + if (chan[c.chan].useWave || chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=round(NOTE_FREQUENCY(c.value)); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + } + chan[c.chan].active=true; + chan[c.chan].keyOn=true; + chan[c.chan].macroInit(ins); + if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { + chan[c.chan].outVol=chan[c.chan].vol; + } + break; + } + case DIV_CMD_NOTE_OFF: + chan[c.chan].sample=-1; + chan[c.chan].active=false; + chan[c.chan].keyOff=true; + chan[c.chan].macroInit(NULL); + break; + case DIV_CMD_NOTE_OFF_ENV: + case DIV_CMD_ENV_RELEASE: + chan[c.chan].std.release(); + break; + case DIV_CMD_INSTRUMENT: + if (chan[c.chan].ins!=c.value || c.value2==1) { + chan[c.chan].ins=c.value; + } + break; + case DIV_CMD_VOLUME: + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + } + chan[c.chan].volChangedL=true; + chan[c.chan].volChangedR=true; + break; + case DIV_CMD_GET_VOLUME: + if (chan[c.chan].std.vol.has) { + return chan[c.chan].vol; + } + return chan[c.chan].outVol; + break; + case DIV_CMD_SNES_INVERT: + chan[c.chan].invertL=(c.value>>4); + chan[c.chan].invertR=c.value&15; + chan[c.chan].volChangedL=true; + chan[c.chan].volChangedR=true; + break; + case DIV_CMD_PANNING: + chan[c.chan].chPanL=c.value; + chan[c.chan].chPanR=c.value2; + chan[c.chan].volChangedL=true; + chan[c.chan].volChangedR=true; + break; + case DIV_CMD_PITCH: + chan[c.chan].pitch=c.value; + chan[c.chan].freqChanged=true; + break; + case DIV_CMD_WAVE: + if (!chan[c.chan].useWave) break; + chan[c.chan].wave=c.value; + chan[c.chan].ws.changeWave1(chan[c.chan].wave); + break; + case DIV_CMD_NOTE_PORTA: { + int destFreq=NOTE_FREQUENCY(c.value2); + bool return2=false; + if (destFreq>chan[c.chan].baseFreq) { + chan[c.chan].baseFreq+=c.value; + if (chan[c.chan].baseFreq>=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } else { + chan[c.chan].baseFreq-=c.value; + if (chan[c.chan].baseFreq<=destFreq) { + chan[c.chan].baseFreq=destFreq; + return2=true; + } + } + chan[c.chan].freqChanged=true; + if (return2) { + chan[c.chan].inPorta=false; + return 2; + } + break; + } + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_FREQUENCY(c.value+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val-12):(0))); + chan[c.chan].freqChanged=true; + chan[c.chan].note=c.value; + break; + } + case DIV_CMD_PRE_PORTA: + if (chan[c.chan].active && c.value2) { + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_FREQUENCY(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_SAMPLE_POS: + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + break; + case DIV_CMD_MINMOD_ECHO: + chan[c.chan].echo=c.value; + break; + case DIV_CMD_GET_VOLMAX: + return 255; + break; + case DIV_CMD_MACRO_OFF: + chan[c.chan].std.mask(c.value,true); + break; + case DIV_CMD_MACRO_ON: + chan[c.chan].std.mask(c.value,false); + break; + case DIV_ALWAYS_SET_VOLUME: + return 1; + break; + default: + break; + } + return 1; +} + +void DivPlatformGBAMinMod::updateWave(int ch) { + int addr=ch*256; + for (unsigned int i=0; i& wlist) { + for (DivRegWrite& i: wlist) rWrite(i.addr,i.val); +} + +unsigned char* DivPlatformGBAMinMod::getRegisterPool() { + return (unsigned char*)regPool; +} + +int DivPlatformGBAMinMod::getRegisterPoolSize() { + return 256; +} + +int DivPlatformGBAMinMod::getRegisterPoolDepth() { + return 16; +} + +const void* DivPlatformGBAMinMod::getSampleMem(int index) { + return index == 0 ? sampleMem : NULL; +} + +size_t DivPlatformGBAMinMod::getSampleMemCapacity(int index) { + return index == 0 ? 33554432 : 0; +} + +size_t DivPlatformGBAMinMod::getSampleMemUsage(int index) { + return index == 0 ? sampleMemLen : 0; +} + +bool DivPlatformGBAMinMod::isSampleLoaded(int index, int sample) { + if (index!=0) return false; + if (sample<0 || sample>255) return false; + return sampleLoaded[sample]; +} + +void DivPlatformGBAMinMod::renderSamples(int sysID) { + size_t maxPos=getSampleMemCapacity(); + memset(sampleMem,0,maxPos); + + // dummy zero-length samples are at pos 0 as the engine still outputs them + size_t memPos=1; + for (int i=0; isong.sampleLen; i++) { + DivSample* s=parent->song.sample[i]; + if (!s->renderOn[0][sysID]) { + sampleOff[i]=0; + continue; + } + int length=s->length8; + int actualLength=MIN((int)(maxPos-memPos),length); + if (actualLength>0) { + sampleOff[i]=memPos; + memcpy(&sampleMem[memPos],s->data8,actualLength); + memPos+=actualLength; + // if it's one-shot, add 16 silent samples for looping area + // this should be enough for most cases even though the + // frequency register can make position jump by up to 256 samples + if (!s->isLoopable()) { + int oneShotLen=MIN((int)maxPos-memPos,16); + memset(&sampleMem[memPos],0,oneShotLen); + memPos+=oneShotLen; + } + } + if (actualLength>dacDepth; + for (int i=0; i<16; i++) { + oscBuf[i]->rate=rate; + } + sampCycles=16777216/flags.getInt("sampRate",21845); + chipClock=16777216/sampCycles; + resetMixer(); +} + +int DivPlatformGBAMinMod::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) { + parent=p; + dumpWrites=false; + skipRegisterWrites=false; + for (int i=0; i<16; i++) { + isMuted[i]=false; + oscBuf[i]=new DivDispatchOscBuffer; + } + sampleMem=new signed char[getSampleMemCapacity()]; + sampleMemLen=0; + setFlags(flags); + reset(); + + return 16; +} + +void DivPlatformGBAMinMod::quit() { + delete[] sampleMem; + for (int i=0; i<16; i++) { + delete oscBuf[i]; + } +} diff --git a/src/engine/platform/gbaminmod.h b/src/engine/platform/gbaminmod.h new file mode 100644 index 000000000..0b7190bd5 --- /dev/null +++ b/src/engine/platform/gbaminmod.h @@ -0,0 +1,126 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2023 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 _GBA_MINMOD_H +#define _GBA_MINMOD_H + +#include "../dispatch.h" +#include "../waveSynth.h" + +class DivPlatformGBAMinMod: public DivDispatch { + struct Channel: public SharedChannel { + unsigned char echo; + unsigned int audPos, wtLen; + int sample, wave; + bool useWave, setPos, volChangedL, volChangedR, invertL, invertR; + int chPanL, chPanR; + int macroVolMul; + int macroPanMul; + DivWaveSynth ws; + Channel(): + SharedChannel(255), + echo(0), + audPos(0), + wtLen(1), + sample(-1), + wave(-1), + useWave(false), + setPos(false), + volChangedL(false), + volChangedR(false), + invertL(false), + invertR(false), + chPanL(255), + chPanR(255), + macroVolMul(256), + macroPanMul(127) {} + }; + Channel chan[16]; + DivDispatchOscBuffer* oscBuf[16]; + bool isMuted[16]; + unsigned int sampleOff[256]; + bool sampleLoaded[256]; + int volScale; + unsigned char chanMax; + + // emulator part + unsigned int mixBufs; + unsigned int mixBufSize; + unsigned int dacDepth; + unsigned int sampCycles; + unsigned int sampTimer; + unsigned int updCycles; + unsigned int updTimer; + unsigned int updCyclesTotal; + unsigned int sampsRendered; + signed char mixBuf[15*2][1024]; + unsigned char mixOut[2][4]; + short oscOut[16][4]; + int chanOut[16][2]; + size_t mixBufPage; + size_t mixBufReadPos; + size_t mixBufWritePos; + size_t mixBufOffset; + + signed char* sampleMem; + size_t sampleMemLen; + // maximum wavetable length is currently hardcoded to 256 + signed char wtMem[256*16]; + unsigned short regPool[16*16]; + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + + public: + void acquire(short** buf, size_t len); + int dispatch(DivCommand c); + void* getChanState(int chan); + DivMacroInt* getChanMacroInt(int ch); + unsigned short getPan(int chan); + DivDispatchOscBuffer* getOscBuffer(int chan); + unsigned char* getRegisterPool(); + int getRegisterPoolSize(); + int getRegisterPoolDepth(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + int getOutputCount(); + void notifyInsChange(int ins); + void notifyWaveChange(int wave); + void notifyInsDeletion(void* ins); + void poke(unsigned int addr, unsigned short val); + void poke(std::vector& wlist); + const char** getRegisterSheet(); + const void* getSampleMem(int index = 0); + size_t getSampleMemCapacity(int index = 0); + size_t getSampleMemUsage(int index = 0); + bool isSampleLoaded(int index, int sample); + void renderSamples(int chipID); + void setFlags(const DivConfig& flags); + int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags); + void quit(); + + float maxCPU; + private: + void updateWave(int ch); + // emulator part + void resetMixer(); +}; + +#endif diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 49341ce14..c8249b1b9 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -257,6 +257,9 @@ const char* cmdName[]={ "DAVE_CLOCK_DIV", "MACRO_RESTART", + "MINMOD_ECHO", + + "ALWAYS_SET_VOLUME" }; static_assert((sizeof(cmdName)/sizeof(void*))==DIV_CMD_MAX,"update cmdName!"); diff --git a/src/engine/song.h b/src/engine/song.h index 4756499bf..3088be22f 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -118,7 +118,6 @@ enum DivSystem { DIV_SYSTEM_T6W28, DIV_SYSTEM_K007232, DIV_SYSTEM_GA20, - DIV_SYSTEM_GBA_DMA, DIV_SYSTEM_PCM_DAC, DIV_SYSTEM_PONG, DIV_SYSTEM_DUMMY, @@ -136,6 +135,8 @@ enum DivSystem { DIV_SYSTEM_ESFM, DIV_SYSTEM_POWERNOISE, DIV_SYSTEM_DAVE, + DIV_SYSTEM_GBA_DMA, + DIV_SYSTEM_GBA_MINMOD, }; enum DivEffectType: unsigned short { diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 3de964f8b..1cdcdfb70 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -2005,20 +2005,6 @@ void DivEngine::registerSystems() { }, {} ); - - sysDefs[DIV_SYSTEM_GBA_DMA]=new DivSysDef( - "Game Boy Advance DMA Sound", NULL, 0xfe, 0, 2, false, true, 0, false, 1U<type==DIV_INS_AMIGA || ins->type==DIV_INS_SNES || ins->type==DIV_INS_GBA_DMA) { + if (ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SNES || ins->type==DIV_INS_GBA_DMA || ins->type==DIV_INS_GBA_MINMOD) { const char* useWaveText=ins->type==DIV_INS_AMIGA?"Use wavetable (Amiga/Generic DAC only)":"Use wavetable"; ImGui::BeginDisabled(ins->amiga.useNoteMap); P(ImGui::Checkbox(useWaveText,&ins->amiga.useWave)); @@ -6024,7 +6028,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_K053260 || ins->type==DIV_INS_C140 || ins->type==DIV_INS_C219 || - ins->type==DIV_INS_GBA_DMA) { + ins->type==DIV_INS_GBA_DMA || + ins->type==DIV_INS_GBA_MINMOD) { insTabSample(ins); } if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem("Namco 163")) { @@ -6469,7 +6474,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_SCC || ins->type==DIV_INS_SNES || ins->type==DIV_INS_NAMCO || - ins->type==DIV_INS_SM8521) { + ins->type==DIV_INS_SM8521 || + (ins->type==DIV_INS_GBA_MINMOD && ins->amiga.useWave)) { if (ImGui::BeginTabItem("Wavetable")) { switch (ins->type) { case DIV_INS_GB: @@ -6512,6 +6518,10 @@ void FurnaceGUI::drawInsEdit() { wavePreviewLen=ins->amiga.waveLen+1; wavePreviewHeight=15; break; + case DIV_INS_GBA_MINMOD: + wavePreviewLen=ins->amiga.waveLen+1; + wavePreviewHeight=255; + break; default: wavePreviewLen=32; wavePreviewHeight=31; @@ -6770,7 +6780,7 @@ void FurnaceGUI::drawInsEdit() { volMax=31; } if (ins->type==DIV_INS_ADPCMB || ins->type==DIV_INS_YMZ280B || ins->type==DIV_INS_RF5C68 || - ins->type==DIV_INS_GA20 || ins->type==DIV_INS_C140 || ins->type==DIV_INS_C219) { + ins->type==DIV_INS_GA20 || ins->type==DIV_INS_C140 || ins->type==DIV_INS_C219 || ins->type==DIV_INS_GBA_MINMOD) { volMax=255; } if (ins->type==DIV_INS_QSOUND) { @@ -6835,7 +6845,7 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_PET || ins->type==DIV_INS_SEGAPCM || ins->type==DIV_INS_FM || ins->type==DIV_INS_K007232 || ins->type==DIV_INS_GA20 || ins->type==DIV_INS_SM8521 || ins->type==DIV_INS_PV1000 || ins->type==DIV_INS_K053260 || - ins->type==DIV_INS_C140 || ins->type==DIV_INS_GBA_DMA) { + ins->type==DIV_INS_C140 || ins->type==DIV_INS_GBA_DMA || ins->type==DIV_INS_GBA_MINMOD) { dutyMax=0; } if (ins->type==DIV_INS_VBOY) { @@ -7036,6 +7046,9 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_MIKEY) { ex1Max=12; } + if (ins->type==DIV_INS_GBA_MINMOD) { + ex1Max=2; + } int panMin=0; int panMax=0; @@ -7101,7 +7114,7 @@ void FurnaceGUI::drawInsEdit() { panMin=0; panMax=127; } - if (ins->type==DIV_INS_C140 || ins->type==DIV_INS_C219) { + if (ins->type==DIV_INS_C140 || ins->type==DIV_INS_C219 || ins->type==DIV_INS_GBA_MINMOD) { panMin=0; panMax=255; } @@ -7246,6 +7259,8 @@ void FurnaceGUI::drawInsEdit() { macroList.push_back(FurnaceGUIMacroDesc("Control",&ins->std.ex1Macro,0,ex1Max,64,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,daveControlBits)); } else if (ins->type==DIV_INS_MIKEY) { macroList.push_back(FurnaceGUIMacroDesc("Load LFSR",&ins->std.ex1Macro,0,12,160,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); + } else if (ins->type==DIV_INS_GBA_MINMOD) { + macroList.push_back(FurnaceGUIMacroDesc("Special",&ins->std.ex1Macro,0,ex1Max,96,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true,minModModeBits)); } else { macroList.push_back(FurnaceGUIMacroDesc("Duty",&ins->std.ex1Macro,0,ex1Max,160,uiColors[GUI_COLOR_MACRO_OTHER])); } diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 2cf952ba0..b098a98f0 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -137,7 +137,17 @@ void FurnaceGUI::initSystemPresets() { "extendWave=true\n" "dacDepth=9\n" ), - CH(DIV_SYSTEM_GBA_DMA, 0.5f, 0, "dacDepth=9"), + CH(DIV_SYSTEM_GBA_DMA, 0.5f, 0, ""), + } + ); + ENTRY( + "Game Boy Advance (with MinMod)", { + CH(DIV_SYSTEM_GB, 1.0f, 0, + "chipType=3\n" + "extendWave=true\n" + "dacDepth=9\n" + ), + CH(DIV_SYSTEM_GBA_MINMOD, 0.5f, 0, ""), } ); ENTRY( diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 566f0c513..44f61e5f6 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -3552,6 +3552,7 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_POWERNOISE_SLOPE,"PowerNoise (slope)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_DAVE,"Dave"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_GBA_DMA,"GBA DMA"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_GBA_MINMOD,"GBA MinMod"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,"Other/Unknown"); ImGui::TreePop(); } diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 17d9e40d9..fba0a0f3a 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -18,6 +18,7 @@ */ #include "../engine/chipUtils.h" +#include "../engine/platform/gbaminmod.h" #include "gui.h" #include "misc/cpp/imgui_stdlib.h" #include @@ -363,7 +364,8 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl if (ImGui::IsItemHovered()) { ImGui::SetTooltip("note: not supported by the VGM format!\nallows wave channel to have double length and 75%% volume"); } - if (CWSliderInt("DAC bit depth (reduces output rate)",&dacDepth,6,9)) { + ImGui::Text("DAC bit depth (reduces output rate):"); + if (CWSliderInt("##DACDepth",&dacDepth,6,9)) { if (dacDepth<6) dacDepth=6; if (dacDepth>9) dacDepth=9; altered=true; @@ -431,6 +433,60 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl } break; } + case DIV_SYSTEM_GBA_MINMOD: { + supportsCustomRate=false; + int volScale=flags.getInt("volScale",4096); + int mixBufs=flags.getInt("mixBufs",15); + int dacDepth=flags.getInt("dacDepth",9); + int channels=flags.getInt("channels",16); + int sampRate=flags.getInt("sampRate",21845); + ImGui::Text("Volume scale:"); + if (CWSliderInt("##VolScale",&volScale,0,32768)) { + if (volScale<0) volScale=0; + if (volScale>32768) volScale=32768; + altered=true; + } rightClickable + ImGui::Text("Mix buffers (allows longer echo delay):"); + if (CWSliderInt("##MixBufs",&mixBufs,2,15)) { + if (mixBufs<2) mixBufs=2; + if (mixBufs>16) mixBufs=16; + altered=true; + } rightClickable + ImGui::Text("DAC bit depth (reduces output rate):"); + if (CWSliderInt("##DACDepth",&dacDepth,6,9)) { + if (dacDepth<6) dacDepth=6; + if (dacDepth>9) dacDepth=9; + altered=true; + } rightClickable + ImGui::Text("Channel limit:"); + if (CWSliderInt("##Channels",&channels,1,16)) { + if (channels<1) channels=1; + if (channels>16) channels=16; + altered=true; + } rightClickable + ImGui::Text("Sample rate:"); + if (CWSliderInt("##SampRate",&sampRate,256,65536)) { + if (sampRate<1) sampRate=21845; + if (sampRate>65536) sampRate=65536; + altered=true; + } rightClickable + DivPlatformGBAMinMod* dispatch=(DivPlatformGBAMinMod*)e->getDispatch(chan); + float maxCPU=dispatch->maxCPU*100; + ImGui::Text("Actual sample rate: %d Hz", dispatch->chipClock); + FurnaceGUI::pushWarningColor(maxCPU>90); + ImGui::Text("Max mixer CPU usage: %.0f%%", maxCPU); + FurnaceGUI::popWarningColor(); + if (altered) { + e->lockSave([&]() { + flags.set("volScale",volScale); + flags.set("mixBufs",mixBufs); + flags.set("dacDepth",dacDepth); + flags.set("channels",channels); + flags.set("sampRate",sampRate); + }); + } + break; + } case DIV_SYSTEM_OPLL: case DIV_SYSTEM_OPLL_DRUMS: case DIV_SYSTEM_VRC7: {