diff --git a/CMakeLists.txt b/CMakeLists.txt index 581e1c2de..8425a78d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -609,6 +609,8 @@ src/engine/platform/sound/c140_c219.c src/engine/platform/sound/dave/dave.cpp +src/engine/platform/sound/nds.cpp + src/engine/platform/oplAInterface.cpp src/engine/platform/ym2608Interface.cpp src/engine/platform/ym2610Interface.cpp @@ -717,6 +719,7 @@ src/engine/platform/c140.cpp src/engine/platform/esfm.cpp src/engine/platform/powernoise.cpp src/engine/platform/dave.cpp +src/engine/platform/nds.cpp src/engine/platform/pcmdac.cpp src/engine/platform/dummy.cpp diff --git a/src/engine/dispatchContainer.cpp b/src/engine/dispatchContainer.cpp index 7146e90c6..170c1ae95 100644 --- a/src/engine/dispatchContainer.cpp +++ b/src/engine/dispatchContainer.cpp @@ -85,6 +85,7 @@ #include "platform/esfm.h" #include "platform/powernoise.h" #include "platform/dave.h" +#include "platform/nds.h" #include "platform/dummy.h" #include "../ta-log.h" #include "song.h" @@ -661,6 +662,9 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do case DIV_SYSTEM_DAVE: dispatch=new DivPlatformDave; break; + case DIV_SYSTEM_NDS: + dispatch=new DivPlatformNDS; + break; case DIV_SYSTEM_DUMMY: dispatch=new DivPlatformDummy; break; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 52b0879a5..235fda675 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -988,7 +988,8 @@ void DivEngine::delUnusedSamples() { i->type==DIV_INS_GA20 || i->type==DIV_INS_K053260 || i->type==DIV_INS_C140 || - i->type==DIV_INS_C219) { + i->type==DIV_INS_C219 || + i->type==DIV_INS_NDS) { if (i->amiga.initSample>=0 && i->amiga.initSampleamiga.initSample]=true; } diff --git a/src/engine/instrument.cpp b/src/engine/instrument.cpp index 24126c340..cd30a648c 100644 --- a/src/engine/instrument.cpp +++ b/src/engine/instrument.cpp @@ -1098,6 +1098,10 @@ void DivInstrument::putInsData2(SafeWriter* w, bool fui, const DivSong* song, bo break; case DIV_INS_DAVE: break; + case DIV_INS_NDS: + featureSM=true; + if (amiga.useSample) featureSL=true; + break; case DIV_INS_MAX: break; case DIV_INS_NULL: diff --git a/src/engine/instrument.h b/src/engine/instrument.h index fd75a4d0f..d66ce25db 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -89,6 +89,7 @@ enum DivInstrumentType: unsigned short { DIV_INS_POWERNOISE=56, DIV_INS_POWERNOISE_SLOPE=57, DIV_INS_DAVE=58, + DIV_INS_NDS=59,/*temp*/ DIV_INS_MAX, DIV_INS_NULL }; diff --git a/src/engine/platform/nds.cpp b/src/engine/platform/nds.cpp new file mode 100644 index 000000000..a92075bd1 --- /dev/null +++ b/src/engine/platform/nds.cpp @@ -0,0 +1,540 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 "nds.h" +#include "../engine.h" +#include "../../ta-log.h" +#include + +#define CHIP_DIVIDER 32 +#define SAMPLE_DIVIDER 512 + +#define rRead8(a) (nds.read8(a)) +#define rWrite8(a,v) {if(!skipRegisterWrites) {nds.write8((a),(v)); regPool[(a)]=(v); if(dumpWrites) addWrite((a),(v)); }} +#define rWrite16(a,v) { \ + if(!skipRegisterWrites) { \ + nds.write16((a)>>1,(v)); \ + regPool[(a)+0]=(v)&0xff; \ + regPool[(a)+1]=((v)>>8)&0xff; \ + if(dumpWrites) addWrite((a)+0,(v)&0xff); \ + if(dumpWrites) addWrite((a)+1,((v)>>8)&0xff); \ + } \ +} + +#define rWrite32(a,v) { \ + if(!skipRegisterWrites) { \ + nds.write32((a)>>2,(v)); \ + regPool[(a)+0]=(v)&0xff; \ + regPool[(a)+1]=((v)>>8)&0xff; \ + regPool[(a)+2]=((v)>>16)&0xff; \ + regPool[(a)+3]=((v)>>24)&0xff; \ + if(dumpWrites) addWrite((a)+0,(v)&0xff); \ + if(dumpWrites) addWrite((a)+1,((v)>>8)&0xff); \ + if(dumpWrites) addWrite((a)+2,((v)>>16)&0xff); \ + if(dumpWrites) addWrite((a)+3,((v)>>24)&0xff); \ + } \ +} + +const char* regCheatSheetNDS[]={ + "CHx_Control", "000+x*10", + "CHx_Start", "004+x*10", + "CHx_Freq", "008+x*10", + "CHx_LoopStart", "00A+x*10", + "CHx_Length", "00C+x*10", + "Control", "100", + "Bias", "104", + "CAPx_Control", "108+x*1", + "CAPx_Dest", "110+x*8", + "CAPx_Length", "114+x*8", + NULL +}; + +const char** DivPlatformNDS::getRegisterSheet() { + return regCheatSheetNDS; +} + +void DivPlatformNDS::acquire(short** buf, size_t len) { + for (size_t i=0; i32767) lout=32767; + if (lout<-32768) lout=-32768; + if (rout>32767) rout=32767; + if (rout<-32768) rout=-32768; + buf[0][i]=lout; + buf[1][i]=rout; + + for (int i=0; i<16; i++) { + oscBuf[i]->data[oscBuf[i]->needle++]=(nds.chan_lout(i)+nds.chan_rout(i))>>1; + } + } +} + +inline u8 DivPlatformNDS::read_byte(u32 addr) { + if (addrcalcArp(chan[i].note,chan[i].std.arp.val)); + } + chan[i].freqChanged=true; + } + 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 ((i>=8) && (i<14)) { + if (chan[i].std.duty.had) { + chan[i].duty=chan[i].std.duty.val; + if ((!chan[i].pcm)) { // pulse + rWrite8(0x03+i*16,(rRead8(0x03+i*16)&0xe8)|(chan[i].duty&7)); + } + } + } + if (chan[i].std.panL.had) { // panning + chan[i].panning=0x40+chan[i].std.panL.val; + rWrite8(0x02+i*16,chan[i].panning); + } + 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].setPos) { + // force keyon + chan[i].keyOn=true; + chan[i].setPos=false; + } else { + chan[i].audPos=0; + } + if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { + unsigned char ctrl=0; + if (chan[i].pcm || i<8) { + DivSample* s=parent->getSample(chan[i].sample); + switch (s->depth) { + //case DIV_SAMPLE_DEPTH_YMZ_ADPCM: ctrl=0x40; break; + case DIV_SAMPLE_DEPTH_8BIT: ctrl=0x00; break; + case DIV_SAMPLE_DEPTH_16BIT: ctrl=0x20; break; + default: break; + } + double off=(s->centerRate>=1)?(8363.0/(double)s->centerRate):1.0; + chan[i].freq=0x10000-(off*parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,CHIP_DIVIDER)); + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>65535) chan[i].freq=65535; + ctrl|=(chan[i].active?0x80:0)|((s->isLoopable())?0x08:0x10); + if (chan[i].keyOn) { + unsigned int start=0; + unsigned int loopStart=0; + unsigned int loopEnd=0; + unsigned int end=0; + if (chan[i].sample>=0 && chan[i].samplesong.sampleLen) { + start=sampleOff[chan[i].sample]; + end=s->getCurBufLen()/4; + } + if (chan[i].audPos>0) { + switch (s->depth) { + //case DIV_SAMPLE_DEPTH_YMZ_ADPCM: start+=chan[i].audPos/2; break; + case DIV_SAMPLE_DEPTH_8BIT: start+=chan[i].audPos; break; + case DIV_SAMPLE_DEPTH_16BIT: start+=chan[i].audPos*2; break; + default: break; + } + } + if (s->isLoopable()) { + switch (s->depth) { + //case DIV_SAMPLE_DEPTH_YMZ_ADPCM: loopStart=s->loopStart/8; loopEnd=(s->loopEnd-s->loopStart)/8; break; + case DIV_SAMPLE_DEPTH_8BIT: loopStart=s->loopStart/4; loopEnd=(s->loopEnd-s->loopStart)/4; break; + case DIV_SAMPLE_DEPTH_16BIT: loopStart=s->loopStart/2; loopEnd=(s->loopEnd-s->loopStart)/2; break; + default: break; + } + loopEnd=MIN(loopEnd,0x3fffff); + loopStart=MIN(loopStart,0xffff); + rWrite16(0x0a+i*16,loopStart&0xffff); + rWrite32(0x0c+i*16,loopEnd&0x3fffff); + } else { + end=MIN(end,0x3fffff); + rWrite16(0x0a+i*16,0); + rWrite32(0x0c+i*16,end&0x3fffff); + } + rWrite8(0x03+i*16,ctrl&~0x80); // force keyoff first + rWrite32(0x04+i*16,start&0x7fffffc); + } + } else { + chan[i].freq=0x10000-(parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,true,0,chan[i].pitch2,chipClock,8)); + if (chan[i].freq<0) chan[i].freq=0; + if (chan[i].freq>65535) chan[i].freq=65535; + ctrl=(chan[i].active?0xe8:0)|(chan[i].duty&7); + rWrite8(0x03+i*16,ctrl&~0x80); // force keyoff first + } + if (!chan[i].std.vol.had) { + chan[i].outVol=chan[i].vol; + writeOutVol(i); + } + chan[i].keyOn=false; + if (chan[i].keyOff) { + chan[i].keyOff=false; + } + if (chan[i].freqChanged) { + rWrite16(0x08+i*16,chan[i].freq&0xffff); + chan[i].freqChanged=false; + } + rWrite8(0x03+i*16,ctrl); + } + } +} + +int DivPlatformNDS::dispatch(DivCommand c) { + switch (c.cmd) { + case DIV_CMD_NOTE_ON: { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_NDS); + if (ins->type==DIV_INS_AMIGA || ins->amiga.useSample || (c.chan<8)) { + chan[c.chan].pcm=true; + } + if (chan[c.chan].pcm || (c.chan<8)) { + chan[c.chan].macroVolMul=ins->type==DIV_INS_AMIGA?64:127; + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].sample=ins->amiga.getSample(c.value); + chan[c.chan].sampleNote=c.value; + c.value=ins->amiga.getFreq(c.value); + chan[c.chan].sampleNoteDelta=c.value-chan[c.chan].sampleNote; + } + if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) { + chan[c.chan].sample=-1; + } + } else { + chan[c.chan].macroVolMul=127; + } + if (c.value!=DIV_NOTE_NULL) { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value); + } + if (c.value!=DIV_NOTE_NULL) { + 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: + if (chan[c.chan].vol!=c.value) { + chan[c.chan].vol=c.value; + if (!chan[c.chan].std.vol.has) { + chan[c.chan].outVol=c.value; + writeOutVol(c.chan); + } + } + 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_PANNING: + chan[c.chan].panning=MIN(parent->convertPanSplitToLinearLR(c.value,c.value2,127),127); + rWrite8(0x02+c.chan*16,chan[c.chan].panning); + 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_PERIODIC(c.value2+chan[c.chan].sampleNoteDelta); + 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_STD_NOISE_MODE: + if ((c.chan>=8) && (c.chan<14) && (!chan[c.chan].pcm)) { // pulse + chan[c.chan].duty=c.value; + rWrite8(0x03+c.chan*16,(rRead8(0x03+c.chan*16)&0xe8)|(chan[c.chan].duty&7)); + } + break; + case DIV_CMD_LEGATO: { + chan[c.chan].baseFreq=NOTE_PERIODIC(c.value+chan[c.chan].sampleNoteDelta+((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_NDS)); + } + if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); + chan[c.chan].inPorta=c.value; + break; + case DIV_CMD_SAMPLE_POS: + if (chan[c.chan].pcm || (c.chan<8)) { + chan[c.chan].audPos=c.value; + chan[c.chan].setPos=true; + } + break; + case DIV_CMD_GET_VOLMAX: + return 127; + 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_CMD_MACRO_RESTART: + chan[c.chan].std.restart(c.value); + break; + default: + break; + } + return 1; +} + +void DivPlatformNDS::writeOutVol(int ch) { + unsigned char val=isMuted[ch]?0:chan[ch].outVol; + rWrite8(0x00+ch*16,val); +} + +void DivPlatformNDS::muteChannel(int ch, bool mute) { + isMuted[ch]=mute; + writeOutVol(ch); +} + +void DivPlatformNDS::forceIns() { + for (int i=0; i<16; i++) { + chan[i].insChanged=true; + chan[i].freqChanged=true; + chan[i].sample=-1; + + rWrite8(0x02+i*16,chan[i].panning); + } +} + +void* DivPlatformNDS::getChanState(int ch) { + return &chan[ch]; +} + +DivMacroInt* DivPlatformNDS::getChanMacroInt(int ch) { + return &chan[ch].std; +} + +unsigned short DivPlatformNDS::getPan(int ch) { + return parent->convertPanLinearToSplit(chan[ch].panning,8,127); +} + +DivDispatchOscBuffer* DivPlatformNDS::getOscBuffer(int ch) { + return oscBuf[ch]; +} + +void DivPlatformNDS::reset() { + memset(regPool,0,288); + nds.reset(); + rWrite32(0x100,0x807f); // enable keyon + rWrite32(0x104,0x200); // initialize bias + for (int i=0; i<16; i++) { + chan[i]=DivPlatformNDS::Channel(); + chan[i].std.setEngine(parent); + rWrite32(0x00+i*16,0x40007f); + } +} + +int DivPlatformNDS::getOutputCount() { + return 2; +} + +void DivPlatformNDS::notifyInsChange(int ins) { + for (int i=0; i<16; i++) { + if (chan[i].ins==ins) { + chan[i].insChanged=true; + } + } +} + +void DivPlatformNDS::notifyWaveChange(int wave) { + // TODO when wavetables are added + // TODO they probably won't be added unless the samples reside in RAM +} + +void DivPlatformNDS::notifyInsDeletion(void* ins) { + for (int i=0; i<16; i++) { + chan[i].std.notifyInsDeletion((DivInstrument*)ins); + } +} + +void DivPlatformNDS::poke(unsigned int addr, unsigned short val) { + rWrite8(addr,val); +} + +void DivPlatformNDS::poke(std::vector& wlist) { + for (DivRegWrite& i: wlist) rWrite8(i.addr,i.val); +} + +unsigned char* DivPlatformNDS::getRegisterPool() { + return regPool; +} + +int DivPlatformNDS::getRegisterPoolSize() { + return 288; +} + +float DivPlatformNDS::getPostAmp() { + return 1.0f; +} + +const void* DivPlatformNDS::getSampleMem(int index) { + return index == 0 ? sampleMem : NULL; +} + +size_t DivPlatformNDS::getSampleMemCapacity(int index) { + return index == 0 ? (isDSi?16777216:4194304) : 0; +} + +size_t DivPlatformNDS::getSampleMemUsage(int index) { + return index == 0 ? sampleMemLen : 0; +} + +bool DivPlatformNDS::isSampleLoaded(int index, int sample) { + if (index!=0) return false; + if (sample<0 || sample>255) return false; + return sampleLoaded[sample]; +} + +void DivPlatformNDS::renderSamples(int sysID) { + memset(sampleMem,0,16777216); + memset(sampleOff,0,256*sizeof(unsigned int)); + memset(sampleLoaded,0,256*sizeof(bool)); + + size_t memPos=0; + 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->getCurBufLen(); + unsigned char* src=(unsigned char*)s->getCurBuf(); + int actualLength=MIN((int)(getSampleMemCapacity()-memPos),length); + if (actualLength>0) { + memcpy(&sampleMem[memPos],src,actualLength); + sampleOff[i]=memPos; + memPos+=length; + } + if (actualLengthrate=rate; + } +} + +int DivPlatformNDS::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 unsigned char[16777216]; + sampleMemLen=0; + nds.reset(); + setFlags(flags); + reset(); + + return 16; +} + +void DivPlatformNDS::quit() { + delete[] sampleMem; + for (int i=0; i<16; i++) { + delete oscBuf[i]; + } +} diff --git a/src/engine/platform/nds.h b/src/engine/platform/nds.h new file mode 100644 index 000000000..1861b97b9 --- /dev/null +++ b/src/engine/platform/nds.h @@ -0,0 +1,100 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2024 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 _NDS_H +#define _NDS_H + +#include "../dispatch.h" +#include "sound/nds.hpp" + +using namespace nds_sound_emu; + +class DivPlatformNDS: public DivDispatch, public nds_sound_intf { + struct Channel: public SharedChannel { + unsigned int audPos; + int sample, wave; + int panning, duty; + bool setPos, pcm; + int macroVolMul; + Channel(): + SharedChannel(127), + audPos(0), + sample(-1), + wave(-1), + panning(8), + duty(0), + setPos(false), + pcm(false), + macroVolMul(64) {} + }; + Channel chan[16]; + DivDispatchOscBuffer* oscBuf[16]; + bool isMuted[16]; + bool isDSi; + unsigned int sampleOff[256]; + bool sampleLoaded[256]; + + unsigned char* sampleMem; + size_t sampleMemLen; + nds_sound_t nds; + unsigned char regPool[288]; + friend void putDispatchChip(void*,int); + friend void putDispatchChan(void*,int,int); + + virtual inline u8 read_byte(u32 addr) override; + virtual inline void write_byte(u32 addr, u8 data) override; + + 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(); + void reset(); + void forceIns(); + void tick(bool sysTick=true); + void muteChannel(int ch, bool mute); + float getPostAmp(); + 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(); + DivPlatformNDS(): + DivDispatch(), + nds_sound_intf(), + nds(*this) {} + private: + void writeOutVol(int ch); +}; + +#endif diff --git a/src/engine/platform/sound/nds.cpp b/src/engine/platform/sound/nds.cpp new file mode 100644 index 000000000..67c45a884 --- /dev/null +++ b/src/engine/platform/sound/nds.cpp @@ -0,0 +1,634 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#include "nds.hpp" + +namespace nds_sound_emu +{ + void nds_sound_t::reset() + { + for (channel_t &elem : m_channel) + elem.reset(); + for (capture_t &elem : m_capture) + elem.reset(); + + m_control = 0; + m_bias = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::tick(s32 cycle) + { + m_loutput = m_routput = (m_bias & 0x3ff); + if (!enable()) + return; + + // mix outputs + s32 lmix = 0, rmix = 0; + for (u8 i = 0; i < 16; i++) + { + channel_t &channel = m_channel[i]; + channel.update(cycle); + // bypass mixer + if (((i == 1) && (mix_ch1())) || ((i == 3) && (mix_ch3()))) + continue; + + lmix += channel.loutput(); + rmix += channel.routput(); + } + + // send mixer output to capture + m_capture[0].update(lmix, cycle); + m_capture[1].update(rmix, cycle); + + // select left/right output source + switch (lout_from()) + { + case 0: // left mixer + break; + case 1: // channel 1 + lmix = m_channel[1].loutput(); + break; + case 2: // channel 3 + lmix = m_channel[3].loutput(); + break; + case 3: // channel 1 + 3 + lmix = m_channel[1].loutput() + m_channel[3].loutput(); + break; + } + + switch (rout_from()) + { + case 0: // right mixer + break; + case 1: // channel 1 + rmix = m_channel[1].routput(); + break; + case 2: // channel 3 + rmix = m_channel[3].routput(); + break; + case 3: // channel 1 + 3 + rmix = m_channel[1].routput() + m_channel[3].routput(); + break; + } + + // adjust master volume + lmix = (lmix * mvol()) >> 13; + rmix = (rmix * mvol()) >> 13; + + // add bias and clip output + m_loutput = clamp((lmix + (m_bias & 0x3ff)), 0, 0x3ff); + m_routput = clamp((rmix + (m_bias & 0x3ff)), 0, 0x3ff); + } + + u8 nds_sound_t::read8(u32 addr) + { + return bitfield(read32(addr >> 2), bitfield(addr, 0, 2) << 3, 8); + } + + u16 nds_sound_t::read16(u32 addr) + { + return bitfield(read32(addr >> 1), bitfield(addr, 0) << 4, 16); + } + + u32 nds_sound_t::read32(u32 addr) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + if ((addr & 0xc) == 0) + return m_channel[bitfield(addr, 4, 4)].control(); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + return m_control; + case 0x04: + return m_bias; + case 0x08: + return m_capture[0].control() | (m_capture[1].control() << 8); + case 0x10: + case 0x18: + return m_capture[bitfield(addr, 3)].dstaddr(); + default: + break; + } + break; + } + return 0; + } + + void nds_sound_t::write8(u32 addr, u8 data) + { + const u8 bit = bitfield(addr, 0, 2); + const u32 in = u32(data) << (bit << 3); + const u32 in_mask = 0xff << (bit << 3); + write32(addr >> 2, in, in_mask); + } + + void nds_sound_t::write16(u32 addr, u16 data, u16 mask) + { + const u8 bit = bitfield(addr, 0); + const u32 in = u32(data) << (bit << 4); + const u32 in_mask = u32(mask) << (bit << 4); + write32(addr >> 1, in, in_mask); + } + + void nds_sound_t::write32(u32 addr, u32 data, u32 mask) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + m_channel[bitfield(addr, 4, 4)].write(bitfield(addr, 2, 2), data, mask); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + m_control = (m_control & ~mask) | (data & mask); + break; + case 0x04: + mask &= 0x3ff; + m_bias = (m_bias & ~mask) | (data & mask); + break; + case 0x08: + if (bitfield(mask, 0, 8)) + m_capture[0].control_w(data & 0xff); + if (bitfield(mask, 8, 8)) + m_capture[1].control_w((data >> 8) & 0xff); + break; + case 0x10: + case 0x14: + case 0x18: + case 0x1c: + m_capture[bitfield(addr, 3)].addrlen_w(bitfield(addr, 2), data, mask); + break; + default: + break; + } + break; + } + } + + // channels + void nds_sound_t::channel_t::reset() + { + m_control = 0; + m_sourceaddr = 0; + m_freq = 0; + m_loopstart = 0; + m_length = 0; + + m_playing = false; + m_adpcm_out = 0; + m_adpcm_index = 0; + m_prev_adpcm_out = 0; + m_prev_adpcm_index = 0; + m_cur_addr = 0; + m_cur_state = 0; + m_cur_bitaddr = 0; + m_delay = 0; + m_sample = 0; + m_lfsr = 0x7fff; + m_lfsr_out = 0x7fff; + m_counter = 0x10000; + m_output = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::channel_t::write(u32 offset, u32 data, u32 mask) + { + const u32 old = m_control; + switch (offset & 3) + { + case 0: // Control/Status + m_control = (m_control & ~mask) | (data & mask); + if (bitfield(old ^ m_control, 31)) + { + if (busy()) + keyon(); + else if (!busy()) + keyoff(); + } + // reset hold flag + if (!m_playing && !hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + break; + case 1: // Source address + mask &= 0x7ffffff; + m_sourceaddr = (m_sourceaddr & ~mask) | (data & mask); + break; + case 2: // Frequency, Loopstart + if (bitfield(mask, 0, 16)) + m_freq = (m_freq & bitfield(~mask, 0, 16)) | (bitfield(data & mask, 0, 16)); + if (bitfield(mask, 16, 16)) + m_loopstart = (m_loopstart & bitfield(~mask, 16, 16)) | (bitfield(data & mask, 16, 16)); + break; + case 3: // Length + mask &= 0x3fffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::channel_t::keyon() + { + if (!m_playing) + { + m_playing = true; + m_delay = format() == 2 ? 11 : 3; // 3 (11 for ADPCM) delay for playing sample + m_cur_bitaddr = m_cur_addr = 0; + m_cur_state = (format() == 2) ? STATE_ADPCM_LOAD : ((m_loopstart == 0) ? STATE_POST_LOOP : STATE_PRE_LOOP); + m_counter = 0x10000; + m_sample = 0; + m_lfsr_out = 0x7fff; + m_lfsr = 0x7fff; + } + } + + + void nds_sound_t::channel_t::keyoff() + { + if (m_playing) + { + if (busy()) + m_control &= ~(1 << 31); + if (!hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + + m_playing = false; + } + } + + void nds_sound_t::channel_t::update(s32 cycle) + { + if (m_playing) + { + // get output + fetch(); + m_counter -= cycle; + while (m_counter <= m_freq) + { + // advance + advance(); + m_counter += 0x10000 - m_freq; + } + m_output = (m_sample * volume()) >> (7 + voldiv()); + m_loutput = (m_output * lvol()) >> 7; + m_routput = (m_output * rvol()) >> 7; + } + } + + void nds_sound_t::channel_t::fetch() + { + if (m_playing) + { + // fetch samples + switch (format()) + { + case 0: // PCM8 + m_sample = s16(m_host.m_intf.read_byte(addr()) << 8); + break; + case 1: // PCM16 + m_sample = m_host.m_intf.read_word(addr()); + break; + case 2: // ADPCM + m_sample = m_cur_state == STATE_ADPCM_LOAD ? 0 : m_adpcm_out; + break; + case 3: // PSG or Noise + m_sample = 0; + if (m_psg) // psg + m_sample = (duty() == 7) ? -0x8000 : ((m_cur_bitaddr < s32(u32(7) - duty())) ? -0x8000 : 0x7fff); + else if (m_noise) // noise + m_sample = m_lfsr_out; + break; + } + } + + // apply delay + if (format() != 3 && m_delay > 0) + m_sample = 0; + } + + void nds_sound_t::channel_t::advance() + { + if (m_playing) + { + // advance bit address + switch (format()) + { + case 0: // PCM8 + m_cur_bitaddr += 8; + break; + case 1: // PCM16 + m_cur_bitaddr += 16; + break; + case 2: // ADPCM + if (m_cur_state == STATE_ADPCM_LOAD) // load ADPCM data + { + if (m_cur_bitaddr == 0) + m_prev_adpcm_out = m_adpcm_out = m_host.m_intf.read_word(addr()); + if (m_cur_bitaddr == 16) + m_prev_adpcm_index = m_adpcm_index = clamp(m_host.m_intf.read_byte(addr()) & 0x7f, 0, 88); + } + else // decode ADPCM + { + const u8 input = bitfield(m_host.m_intf.read_byte(addr()), m_cur_bitaddr & 4, 4); + s32 diff = ((bitfield(input, 0, 3) * 2 + 1) * m_host.adpcm_diff_table[m_adpcm_index] / 8); + if (bitfield(input, 3)) diff = -diff; + m_adpcm_out = clamp(m_adpcm_out + diff, -0x8000, 0x7fff); + m_adpcm_index = clamp(m_adpcm_index + m_host.adpcm_index_table[bitfield(input, 0, 3)], 0, 88); + } + m_cur_bitaddr += 4; + break; + case 3: // PSG or Noise + if (m_psg) // psg + m_cur_bitaddr = (m_cur_bitaddr + 1) & 7; + else if (m_noise) // noise + { + if (bitfield(m_lfsr, 1)) + { + m_lfsr = (m_lfsr >> 1) ^ 0x6000; + m_lfsr_out = -0x8000; + } + else + { + m_lfsr >>= 1; + m_lfsr_out = 0x7fff; + } + } + break; + } + + // address update + if (format() != 3) + { + // adjust delay + m_delay--; + + // update address, loop + while (m_cur_bitaddr >= 32) + { + // already loaded? + if (format() == 2 && m_cur_state == STATE_ADPCM_LOAD) + { + m_cur_state = m_loopstart == 0 ? STATE_POST_LOOP : STATE_PRE_LOOP; + } + m_cur_addr++; + if (m_cur_state == STATE_PRE_LOOP && m_cur_addr >= m_loopstart) + { + m_cur_state = STATE_POST_LOOP; + m_cur_addr = 0; + if (format() == 2) + { + m_prev_adpcm_out = m_adpcm_out; + m_prev_adpcm_index = m_adpcm_index; + } + } + else if (m_cur_state == STATE_POST_LOOP && m_cur_addr >= m_length) + { + switch (repeat()) + { + case 0: // manual; not correct? + case 2: // one-shot + case 3: // prohibited + keyoff(); + break; + case 1: // loop infinitely + if (format() == 2) + { + if (m_loopstart == 0) // reload ADPCM + { + m_cur_state = STATE_ADPCM_LOAD; + } + else // restore + { + m_adpcm_out = m_prev_adpcm_out; + m_adpcm_index = m_prev_adpcm_index; + } + } + m_cur_addr = 0; + break; + } + } + m_cur_bitaddr -= 32; + } + } + } + } + + // capture + void nds_sound_t::capture_t::reset() + { + m_control = 0; + m_dstaddr = 0; + m_length = 0; + + m_counter = 0x10000; + m_cur_addr = 0; + m_cur_waddr = 0; + m_cur_bitaddr = 0; + m_enable = false; + } + + void nds_sound_t::capture_t::control_w(u8 data) + { + const u8 old = m_control; + m_control = data; + if (bitfield(old ^ m_control, 7)) + { + if (busy()) + capture_on(); + else if (!busy()) + capture_off(); + } + } + + void nds_sound_t::capture_t::addrlen_w(u32 offset, u32 data, u32 mask) + { + switch (offset & 1) + { + case 0: // Destination Address + mask &= 0x7ffffff; + m_dstaddr = (m_dstaddr & ~mask) | (data & mask); + break; + case 1: // Buffer Length + mask &= 0xffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::capture_t::update(s32 mix, s32 cycle) + { + if (m_enable) + { + s32 inval = 0; + // get inputs + // TODO: hardware bugs aren't emulated, add mode behavior not verified + if (addmode()) + inval = get_source() ? m_input.output() + m_output.output() : mix; + else + inval = get_source() ? m_input.output() : mix; + + // clip output + inval = clamp(inval, -0x8000, 0x7fff); + + // update counter + m_counter -= cycle; + while (m_counter <= m_output.freq()) + { + // write to memory; TODO: verify write behavior + if (format()) // 8 bit output + { + m_fifo[m_fifo_head & 7].write_byte(m_cur_bitaddr & 0x18, (inval >> 8) & 0xff); + m_cur_bitaddr += 8; + } + else + { + m_fifo[m_fifo_head & 7].write_word(m_cur_bitaddr & 0x10, inval & 0xffff); + m_cur_bitaddr += 16; + } + + // update address + while (m_cur_bitaddr >= 32) + { + // clear FIFO empty flag + m_fifo_empty = false; + + // advance FIFO head position + m_fifo_head = (m_fifo_head + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_full = true; + + // update loop + if (++m_cur_addr >= m_length) + { + if (repeat()) + m_cur_addr = 0; + else + capture_off(); + } + + if (m_fifo_full) + { + // execute FIFO + fifo_write(); + + // check repeat + if (m_cur_waddr >= m_length && repeat()) + m_cur_waddr = 0; + } + + m_cur_bitaddr -= 32; + } + m_counter += 0x10000 - m_output.freq(); + } + } + } + + bool nds_sound_t::capture_t::fifo_write() + { + if (m_fifo_empty) + return true; + + // clear FIFO full flag + m_fifo_full = false; + + // write FIFO data to memory + m_host.m_intf.write_dword(waddr(), m_fifo[m_fifo_tail].data()); + m_cur_waddr++; + + // advance FIFO tail position + m_fifo_tail = (m_fifo_tail + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_empty = true; + + return m_fifo_empty; + } + + void nds_sound_t::capture_t::capture_on() + { + if (!m_enable) + { + m_enable = true; + + // reset address + m_cur_bitaddr = 0; + m_cur_addr = m_cur_waddr = 0; + m_counter = 0x10000; + + // reset FIFO + m_fifo_head = m_fifo_tail = 0; + m_fifo_empty = true; + m_fifo_full = false; + } + } + + void nds_sound_t::capture_t::capture_off() + { + if (m_enable) + { + // flush FIFO + while (m_cur_waddr < m_length) + { + if (fifo_write()) + break; + } + + m_enable = false; + if (busy()) + m_control &= ~(1 << 7); + } + } + +}; // namespace nds_sound_emu \ No newline at end of file diff --git a/src/engine/platform/sound/nds.hpp b/src/engine/platform/sound/nds.hpp new file mode 100644 index 000000000..fb1dd6fbc --- /dev/null +++ b/src/engine/platform/sound/nds.hpp @@ -0,0 +1,415 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#ifndef NDS_SOUND_EMU_H +#define NDS_SOUND_EMU_H + +namespace nds_sound_emu +{ + using u8 = unsigned char; + using u16 = unsigned short; + using u32 = unsigned int; + using u64 = unsigned long long; + using s8 = signed char; + using s16 = signed short; + using s32 = signed int; + using s64 = signed long long; + + template + static const inline T bitfield(const T in, const u8 pos) + { + return (in >> pos) & 1; + } // bitfield + + template + static const inline T bitfield(const T in, const u8 pos, const u8 len) + { + return (in >> pos) & ((1 << len) - 1); + } // bitfield + + template + static const inline T clamp(const T in, const T min, const T max) + { + return (in < min) ? min : ((in > max) ? max : in); + } // clamp + + class nds_sound_intf + { + public: + nds_sound_intf() + { + } + + virtual inline u8 read_byte(u32 addr) { return 0; } + inline u16 read_word(u32 addr) { return read_byte(addr) | (u16(read_byte(addr + 1)) << 8); } + inline u32 read_dword(u32 addr) { return read_word(addr) | (u16(read_word(addr + 2)) << 16); } + + virtual inline void write_byte(u32 addr, u8 data) {} + inline void write_word(u32 addr, u16 data) + { + write_byte(addr, data & 0xff); + write_byte(addr + 1, data >> 8); + } + inline void write_dword(u32 addr, u32 data) + { + write_word(addr, data & 0xffff); + write_word(addr + 2, data >> 16); + } + }; + + class nds_sound_t + { + public: + nds_sound_t(nds_sound_intf &intf) + : m_intf(intf) + , m_channel{ + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, false, true), channel_t(*this, false, true) + } + , m_capture{ + capture_t(*this, m_channel[0], m_channel[1]), + capture_t(*this, m_channel[2], m_channel[3]) + } + , m_control(0) + , m_bias(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void tick(s32 cycle); + + // host accesses + u32 read32(u32 addr); + void write32(u32 addr, u32 data, u32 mask = ~0); + + u16 read16(u32 addr); + void write16(u32 addr, u16 data, u16 mask = ~0); + + u8 read8(u32 addr); + void write8(u32 addr, u8 data); + + s32 loutput() { return m_loutput; } + s32 routput() { return m_routput; } + + // for debug + s32 chan_lout(u8 ch) { return m_channel[ch].loutput(); } + s32 chan_rout(u8 ch) { return m_channel[ch].routput(); } + + private: + // ADPCM tables + s8 adpcm_index_table[8] = + { + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + u16 adpcm_diff_table[89] = + { + 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x0010, + 0x0011, 0x0013, 0x0015, 0x0017, 0x0019, 0x001c, 0x001f, 0x0022, 0x0025, + 0x0029, 0x002d, 0x0032, 0x0037, 0x003c, 0x0042, 0x0049, 0x0050, 0x0058, + 0x0061, 0x006b, 0x0076, 0x0082, 0x008f, 0x009d, 0x00ad, 0x00be, 0x00d1, + 0x00e6, 0x00fd, 0x0117, 0x0133, 0x0151, 0x0173, 0x0198, 0x01c1, 0x01ee, + 0x0220, 0x0256, 0x0292, 0x02d4, 0x031c, 0x036c, 0x03c3, 0x0424, 0x048e, + 0x0502, 0x0583, 0x0610, 0x06ab, 0x0756, 0x0812, 0x08e0, 0x09c3, 0x0abd, + 0x0bd0, 0x0cff, 0x0e4c, 0x0fba, 0x114c, 0x1307, 0x14ee, 0x1706, 0x1954, + 0x1bdc, 0x1ea5, 0x21b6, 0x2515, 0x28ca, 0x2cdf, 0x315b, 0x364b, 0x3bb9, + 0x41b2, 0x4844, 0x4f7e, 0x5771, 0x602f, 0x69ce, 0x7462, 0x7fff + }; + + // structs + enum + { + STATE_ADPCM_LOAD = 0, + STATE_PRE_LOOP, + STATE_POST_LOOP + }; + + class channel_t + { + public: + channel_t(nds_sound_t &host, bool psg, bool noise) + : m_host(host) + + , m_psg(psg) + , m_noise(noise) + + , m_control(0) + , m_sourceaddr(0) + , m_freq(0) + , m_loopstart(0) + , m_length(0) + , m_playing(false) + , m_adpcm_out(0) + , m_adpcm_index(0) + , m_prev_adpcm_out(0) + , m_prev_adpcm_index(0) + , m_cur_addr(0) + , m_cur_state(0) + , m_cur_bitaddr(0) + , m_delay(0) + , m_sample(0) + , m_lfsr(0x7fff) + , m_lfsr_out(0x7fff) + , m_counter(0x10000) + , m_output(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void write(u32 offset, u32 data, u32 mask = ~0); + + void update(s32 cycle); + + // getters + // control word + u32 control() const { return m_control; } + u32 freq() const { return m_freq; } + + // outputs + s32 output() const { return m_output; } + s32 loutput() const { return m_loutput; } + s32 routput() const { return m_routput; } + + private: + // inline constants + const u8 m_voldiv_shift[4] = {0, 1, 2, 4}; + + // control bits + s32 volume() const { return bitfield(m_control, 0, 7); } // global volume + u32 voldiv() const { return m_voldiv_shift[bitfield(m_control, 8, 2)]; } // volume shift + bool hold() const { return bitfield(m_control, 15); } // hold bit + u32 pan() const { return bitfield(m_control, 16, 7); } // panning (0...127, 0 = left, 127 = right, 64 = half) + u32 duty() const { return bitfield(m_control, 24, 3); } // PSG duty + u32 repeat() const { return bitfield(m_control, 27, 2); } // Repeat mode (Manual, Loop infinitely, One-shot) + u32 format() const { return bitfield(m_control, 29, 2); } // Sound Format (PCM8, PCM16, ADPCM, PSG/Noise when exists) + bool busy() const { return bitfield(m_control, 31); } // Busy flag + + // calculated values + s32 lvol() const { return (pan() == 0x7f) ? 0 : 128 - pan(); } // calculated left volume + s32 rvol() const { return (pan() == 0x7f) ? 128 : pan(); } // calculated right volume + + // calculated address + u32 addr() const { return (m_sourceaddr & ~3) + (m_cur_bitaddr >> 3) + (m_cur_state == STATE_POST_LOOP ? ((m_loopstart + m_cur_addr) << 2) : (m_cur_addr << 2)); } + + void keyon(); + void keyoff(); + void fetch(); + void advance(); + + // interfaces + nds_sound_t &m_host; // host device + + // configuration + bool m_psg = false; // PSG Enable + bool m_noise = false; // Noise Enable + + // registers + u32 m_control = 0; // Control + u32 m_sourceaddr = 0; // Source Address + u16 m_freq = 0; // Frequency + u16 m_loopstart = 0; // Loop Start + u32 m_length = 0; // Length + + // internal states + bool m_playing = false; // playing flag + s32 m_adpcm_out = 0; // current ADPCM sample value + s32 m_adpcm_index = 0; // current ADPCM step + s32 m_prev_adpcm_out = 0; // previous ADPCM sample value + s32 m_prev_adpcm_index = 0; // previous ADPCM step + u32 m_cur_addr = 0; // current address + s32 m_cur_state = 0; // current state + s32 m_cur_bitaddr = 0; // bit address + s32 m_delay = 0; // delay + s16 m_sample = 0; // current sample + u32 m_lfsr = 0x7fff; // noise LFSR + s16 m_lfsr_out = 0x7fff; // LFSR output + s32 m_counter = 0x10000; // clock counter + s32 m_output = 0; // current output + s32 m_loutput = 0; // current left output + s32 m_routput = 0; // current right output + }; + + class capture_t + { + public: + capture_t(nds_sound_t &host, channel_t &input, channel_t &output) + : m_host(host) + , m_input(input) + , m_output(output) + + , m_control(0) + , m_dstaddr(0) + , m_length(0) + + , m_counter(0x10000) + , m_cur_addr(0) + , m_cur_waddr(0) + , m_cur_bitaddr(0) + , m_enable(0) + + , m_fifo{ + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t(), + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t() + } + , m_fifo_head(0) + , m_fifo_tail(0) + , m_fifo_empty(true) + , m_fifo_full(false) + { + } + + void reset(); + void update(s32 mix, s32 cycle); + + void control_w(u8 data); + void addrlen_w(u32 offset, u32 data, u32 mask = ~0); + + // getters + u32 control() const { return m_control; } + u32 dstaddr() const { return m_dstaddr; } + + private: + // inline constants + // control bits + bool addmode() const { return bitfield(m_control, 0); } // Add mode (add channel 1/3 output with channel 0/2) + bool get_source() const { return bitfield(m_control, 1); } // Select source (left or right mixer, channel 0/2) + bool repeat() const { return bitfield(m_control, 2); } // repeat flag + bool format() const { return bitfield(m_control, 3); } // store format (PCM16, PCM8) + bool busy() const { return bitfield(m_control, 7); } // busy flag + + // FIFO offset mask + u32 fifo_mask() const { return format() ? 7 : 3; } + + // calculated address + u32 waddr() const { return (m_dstaddr & ~3) + (m_cur_waddr << 2); } + + void capture_on(); + void capture_off(); + bool fifo_write(); + + // interfaces + nds_sound_t &m_host; // host device + channel_t &m_input; // Input channel + channel_t &m_output; // Output channel + + // registers + u8 m_control = 0; // Control + u32 m_dstaddr = 0; // Destination Address + u32 m_length = 0; // Buffer Length + + // internal states + u32 m_counter = 0x10000; // clock counter + u32 m_cur_addr = 0; // current address + u32 m_cur_waddr = 0; // current write address + s32 m_cur_bitaddr = 0; // bit address + bool m_enable = false; // capture enable + + // FIFO + class fifo_data_t + { + public: + fifo_data_t() + : m_data(0) + { + } + + void reset() + { + m_data = 0; + } + + // accessors + void write_byte(const u8 bit, const u8 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + void write_word(const u8 bit, const u16 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xffff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + // getters + u32 data() const { return m_data; } + + private: + u32 m_data = 0; + }; + + fifo_data_t m_fifo[8]; // FIFO (8 word, for 16 sample delay) + u32 m_fifo_head = 0; // FIFO head + u32 m_fifo_tail = 0; // FIFO tail + bool m_fifo_empty = true; // FIFO empty flag + bool m_fifo_full = false; // FIFO full flag + }; + + nds_sound_intf &m_intf; // memory interface + + channel_t m_channel[16]; // 16 channels + capture_t m_capture[2]; // 2 capture channels + + inline u8 mvol() const { return bitfield(m_control, 0, 7); } // master volume + inline u8 lout_from() const { return bitfield(m_control, 8, 2); } // left output source (mixer, channel 1, channel 3, channel 1+3) + inline u8 rout_from() const { return bitfield(m_control, 10, 2); } // right output source (mixer, channel 1, channel 3, channel 1+3) + inline bool mix_ch1() const { return bitfield(m_control, 12); } // mix/bypass channel 1 + inline bool mix_ch3() const { return bitfield(m_control, 13); } // mix/bypass channel 3 + inline bool enable() const { return bitfield(m_control, 15); } // global enable + + u32 m_control = 0; // global control + u32 m_bias = 0; // output bias + s32 m_loutput = 0; // left output + s32 m_routput = 0; // right output + }; +}; // namespace nds_sound_emu + +#endif // NDS_SOUND_EMU_H \ No newline at end of file diff --git a/src/engine/song.h b/src/engine/song.h index 86674ee52..749feefc8 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -135,6 +135,7 @@ enum DivSystem { DIV_SYSTEM_ESFM, DIV_SYSTEM_POWERNOISE, DIV_SYSTEM_DAVE, + DIV_SYSTEM_NDS, }; enum DivEffectType: unsigned short { diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index e33b17b1c..258d47eff 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -2021,6 +2021,19 @@ void DivEngine::registerSystems() { } ); + sysDefs[DIV_SYSTEM_NDS]=new DivSysDef( + "NDS", NULL, 0xfe/* placeholder */, 0, 16, false, true, 0, false, (1U<type==DIV_INS_AY || ins->type==DIV_INS_AY8930 || ins->type==DIV_INS_VRC6 || - ins->type==DIV_INS_SU) { + ins->type==DIV_INS_SU || + ins->type==DIV_INS_NDS) { P(ImGui::Checkbox("Use sample",&ins->amiga.useSample)); if (ins->type==DIV_INS_X1_010) { if (ImGui::InputInt("Sample bank slot##BANKSLOT",&ins->x1_010.bankSlot,1,4)) { PARAMETER @@ -6021,7 +6022,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_GA20 || ins->type==DIV_INS_K053260 || ins->type==DIV_INS_C140 || - ins->type==DIV_INS_C219) { + ins->type==DIV_INS_C219 || + ins->type==DIV_INS_NDS) { insTabSample(ins); } if (ins->type==DIV_INS_N163) if (ImGui::BeginTabItem("Namco 163")) { @@ -6736,7 +6738,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_FM || ins->type==DIV_INS_SEGAPCM || ins->type==DIV_INS_MIKEY || ins->type==DIV_INS_MULTIPCM || ins->type==DIV_INS_SU || ins->type==DIV_INS_OPZ || ins->type==DIV_INS_OPM || ins->type==DIV_INS_SNES || ins->type==DIV_INS_MSM5232 || - ins->type==DIV_INS_K053260) { + ins->type==DIV_INS_K053260 || ins->type==DIV_INS_NDS) { volMax=127; } if (ins->type==DIV_INS_GB) { @@ -6921,6 +6923,10 @@ void FurnaceGUI::drawInsEdit() { dutyLabel="Noise Freq"; dutyMax=3; } + if (ins->type==DIV_INS_NDS) { + dutyLabel="Duty"; + dutyMax=ins->amiga.useSample?0:7; + } const char* waveLabel="Waveform"; int waveMax=(ins->type==DIV_INS_VERA)?3:(MAX(1,e->song.waveLen-1)); @@ -6959,6 +6965,7 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_POWERNOISE_SLOPE) waveMax=0; if (ins->type==DIV_INS_SU || ins->type==DIV_INS_POKEY) waveMax=7; if (ins->type==DIV_INS_DAVE) waveMax=4; + if (ins->type==DIV_INS_NDS) waveMax=0; if (ins->type==DIV_INS_PET) { waveMax=8; waveBitMode=true; @@ -7111,6 +7118,11 @@ void FurnaceGUI::drawInsEdit() { if (ins->type==DIV_INS_DAVE) { panMax=63; } + if (ins->type==DIV_INS_NDS) { + panMin=-64; + panMax=63; + panSingleNoBit=true; + } if (volMax>0) { macroList.push_back(FurnaceGUIMacroDesc(volumeLabel,&ins->std.volMacro,volMin,volMax,160,uiColors[GUI_COLOR_MACRO_VOLUME])); @@ -7201,7 +7213,8 @@ void FurnaceGUI::drawInsEdit() { ins->type==DIV_INS_ESFM || ins->type==DIV_INS_POWERNOISE || ins->type==DIV_INS_POWERNOISE_SLOPE || - ins->type==DIV_INS_DAVE) { + ins->type==DIV_INS_DAVE || + ins->type==DIV_INS_NDS) { macroList.push_back(FurnaceGUIMacroDesc("Phase Reset",&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); } if (ex1Max>0) { diff --git a/src/gui/presets.cpp b/src/gui/presets.cpp index 18a0ebdc9..ad688b292 100644 --- a/src/gui/presets.cpp +++ b/src/gui/presets.cpp @@ -266,6 +266,11 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_PV1000, 1.0f, 0, "") } ); + ENTRY( + "NDS", { + CH(DIV_SYSTEM_NDS, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN("Computers","let's get to work on chiptune today."); @@ -2703,6 +2708,11 @@ void FurnaceGUI::initSystemPresets() { CH(DIV_SYSTEM_C219, 1.0f, 0, "") } ); + ENTRY( + "NDS", { + CH(DIV_SYSTEM_NDS, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN("Wavetable","chips which use user-specified waveforms to generate sound."); @@ -2873,6 +2883,11 @@ void FurnaceGUI::initSystemPresets() { }, "tickRate=50" ); + ENTRY( + "NDS", { + CH(DIV_SYSTEM_NDS, 1.0f, 0, "") + } + ); CATEGORY_END; CATEGORY_BEGIN("DefleMask-compatible","these configurations are compatible with DefleMask.\nselect this if you need to save as .dmf or work with that program."); diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 22d4b503b..ed919546e 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -3551,6 +3551,7 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_INSTR_POWERNOISE,"PowerNoise (noise)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_POWERNOISE_SLOPE,"PowerNoise (slope)"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_DAVE,"Dave"); + UI_COLOR_CONFIG(GUI_COLOR_INSTR_NDS,"NDS"); UI_COLOR_CONFIG(GUI_COLOR_INSTR_UNKNOWN,"Other/Unknown"); ImGui::TreePop(); } diff --git a/src/gui/sysConf.cpp b/src/gui/sysConf.cpp index 18bc3d967..39957372b 100644 --- a/src/gui/sysConf.cpp +++ b/src/gui/sysConf.cpp @@ -2316,6 +2316,28 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl } break; } + case DIV_SYSTEM_NDS: { + int chipType=flags.getInt("chipType",0); + + ImGui::Text("Model:"); + ImGui::Indent(); + if (ImGui::RadioButton("DS (4MB RAM)",chipType==0)) { + chipType=0; + altered=true; + } + if (ImGui::RadioButton("DSi (16MB RAM)",chipType==1)) { + chipType=1; + altered=true; + } + ImGui::Unindent(); + + if (altered) { + e->lockSave([&]() { + flags.set("chipType",chipType); + }); + } + break; + } case DIV_SYSTEM_SWAN: case DIV_SYSTEM_BUBSYS_WSG: case DIV_SYSTEM_PET: diff --git a/src/main.cpp b/src/main.cpp index 10e8bec5a..4f36135dd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -248,6 +248,8 @@ TAParamResult pVersion(String) { printf("- D65010G031 emulator (modified version) by cam900 (zlib license)\n"); printf("- C140/C219 emulator (modified version) by cam900 (zlib license)\n"); printf("- PowerNoise emulator by scratchminer (MIT)\n"); + printf("- ep128emu by Istvan Varga (GPLv2)\n"); + printf("- NDS sound emulator by cam900 (zlib license)\n"); return TA_PARAM_QUIT; }