Merge pull request #1803 from akumanatt/minmod

Add GBA support
This commit is contained in:
tildearrow 2024-03-18 00:40:09 -05:00 committed by GitHub
commit a8c4cd9a83
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1869 additions and 52 deletions

View file

@ -722,6 +722,8 @@ src/engine/platform/c140.cpp
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/nds.cpp
src/engine/platform/pcmdac.cpp
src/engine/platform/dummy.cpp

View file

@ -361,6 +361,7 @@ size | description
1 | sound length
| - 64 is infinity
1 | flags
| - bit 2: double wave width for GBA (>=196)
| - bit 1: always init envelope
| - bit 0: software envelope (zombie mode)
1 | hardware sequence length

View file

@ -258,6 +258,8 @@ enum DivDispatchCmds {
DIV_CMD_DAVE_LOW_PASS,
DIV_CMD_DAVE_CLOCK_DIV,
DIV_CMD_MINMOD_ECHO,
DIV_CMD_MAX
};
@ -476,6 +478,7 @@ enum DivMemoryWaveView: unsigned char {
DIV_MEMORY_WAVE_NONE=0,
DIV_MEMORY_WAVE_4BIT, // Namco 163
DIV_MEMORY_WAVE_6BIT, // Virtual Boy
DIV_MEMORY_WAVE_8BIT_SIGNED, // SCC
};
struct DivMemoryComposition {

View file

@ -81,6 +81,8 @@
#include "platform/k053260.h"
#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"
@ -645,6 +647,12 @@ void DivDispatchContainer::init(DivSystem sys, DivEngine* eng, int chanCount, do
dispatch=new DivPlatformC140;
((DivPlatformC140*)dispatch)->set219(true);
break;
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;

View file

@ -989,7 +989,9 @@ void DivEngine::delUnusedSamples() {
i->type==DIV_INS_K053260 ||
i->type==DIV_INS_C140 ||
i->type==DIV_INS_C219 ||
i->type==DIV_INS_NDS) {
i->type==DIV_INS_NDS ||
i->type==DIV_INS_GBA_DMA ||
i->type==DIV_INS_GBA_MINMOD) {
if (i->amiga.initSample>=0 && i->amiga.initSample<song.sampleLen) {
isUsed[i->amiga.initSample]=true;
}

View file

@ -54,8 +54,8 @@ class DivWorkPool;
#define DIV_UNSTABLE
#define DIV_VERSION "dev195"
#define DIV_ENGINE_VERSION 195
#define DIV_VERSION "dev196"
#define DIV_ENGINE_VERSION 196
// for imports
#define DIV_VERSION_MOD 0xff01
#define DIV_VERSION_FC 0xff02

View file

@ -83,7 +83,8 @@ bool DivInstrumentGB::operator==(const DivInstrumentGB& other) {
_C(soundLen) &&
_C(hwSeqLen) &&
_C(softEnv) &&
_C(alwaysInit)
_C(alwaysInit) &&
_C(doubleWave)
);
}
@ -484,6 +485,7 @@ void DivInstrument::writeFeatureGB(SafeWriter* w) {
w->writeC(gb.soundLen);
w->writeC(
(gb.doubleWave?4:0)|
(gb.alwaysInit?2:0)|
(gb.softEnv?1:0)
);
@ -1102,6 +1104,14 @@ void DivInstrument::putInsData2(SafeWriter* w, bool fui, const DivSong* song, bo
featureSM=true;
if (amiga.useSample) featureSL=true;
break;
case DIV_INS_GBA_DMA:
featureSM=true;
featureSL=true;
break;
case DIV_INS_GBA_MINMOD:
featureSM=true;
featureSL=true;
break;
case DIV_INS_MAX:
break;
case DIV_INS_NULL:
@ -1625,6 +1635,7 @@ void DivInstrument::readFeatureGB(SafeReader& reader, short version) {
gb.soundLen=reader.readC();
next=reader.readC();
if (version>=196) gb.doubleWave=next&4;
gb.alwaysInit=next&2;
gb.softEnv=next&1;

View file

@ -90,6 +90,8 @@ enum DivInstrumentType: unsigned short {
DIV_INS_POWERNOISE_SLOPE=57,
DIV_INS_DAVE=58,
DIV_INS_NDS=59,
DIV_INS_GBA_DMA=60,
DIV_INS_GBA_MINMOD=61,
DIV_INS_MAX,
DIV_INS_NULL
};
@ -382,7 +384,7 @@ struct DivInstrumentSTD {
struct DivInstrumentGB {
unsigned char envVol, envDir, envLen, soundLen, hwSeqLen;
bool softEnv, alwaysInit;
bool softEnv, alwaysInit, doubleWave;
enum HWSeqCommands: unsigned char {
DIV_GB_HWCMD_ENVELOPE=0,
DIV_GB_HWCMD_SWEEP,
@ -410,7 +412,8 @@ struct DivInstrumentGB {
soundLen(64),
hwSeqLen(0),
softEnv(false),
alwaysInit(false) {
alwaysInit(false),
doubleWave(false) {
memset(hwSeq,0,256*sizeof(HWSeqCommandGB));
}
};

View file

@ -81,17 +81,41 @@ void DivPlatformGB::acquire(short** buf, size_t len) {
}
void DivPlatformGB::updateWave() {
rWrite(0x1a,0);
for (int i=0; i<16; i++) {
int nibble1=ws.output[((i<<1)+antiClickWavePos)&31];
int nibble2=ws.output[((1+(i<<1))+antiClickWavePos)&31];
if (invertWave) {
nibble1^=15;
nibble2^=15;
if (doubleWave) {
rWrite(0x1a,0x40); // select 1 -> write to bank 0
for (int i=0; i<16; i++) {
int nibble1=ws.output[((i<<1)+antiClickWavePos)&63];
int nibble2=ws.output[((1+(i<<1))+antiClickWavePos)&63];
if (invertWave) {
nibble1^=15;
nibble2^=15;
}
rWrite(0x30+i,(nibble1<<4)|nibble2);
}
rWrite(0x30+i,(nibble1<<4)|nibble2);
rWrite(0x1a,0); // select 0 -> write to bank 1
for (int i=0; i<16; i++) {
int nibble1=ws.output[((32+(i<<1))+antiClickWavePos)&63];
int nibble2=ws.output[((33+(i<<1))+antiClickWavePos)&63];
if (invertWave) {
nibble1^=15;
nibble2^=15;
}
rWrite(0x30+i,(nibble1<<4)|nibble2);
}
antiClickWavePos&=63;
} else {
rWrite(0x1a,model==GB_MODEL_AGB_NATIVE?0x40:0);
for (int i=0; i<16; i++) {
int nibble1=ws.output[((i<<1)+antiClickWavePos)&31];
int nibble2=ws.output[((1+(i<<1))+antiClickWavePos)&31];
if (invertWave) {
nibble1^=15;
nibble2^=15;
}
rWrite(0x30+i,(nibble1<<4)|nibble2);
}
antiClickWavePos&=31;
}
antiClickWavePos&=31;
}
static unsigned char chanMuteMask[4]={
@ -112,6 +136,13 @@ static unsigned char gbVolMap[16]={
0x20, 0x20, 0x20, 0x20
};
static unsigned char gbVolMapEx[16]={
0x00, 0x00, 0x00, 0x00,
0x60, 0x60, 0x60, 0x60,
0x40, 0x40, 0x40, 0x40,
0xa0, 0xa0, 0x20, 0x20
};
static unsigned char noiseTable[256]={
0,
0xf7, 0xf6, 0xf5, 0xf4,
@ -156,7 +187,7 @@ void DivPlatformGB::tick(bool sysTick) {
if (chan[i].outVol<0) chan[i].outVol=0;
if (i==2) {
rWrite(16+i*5+2,gbVolMap[chan[i].outVol]);
rWrite(16+i*5+2,(model==GB_MODEL_AGB_NATIVE?gbVolMapEx:gbVolMap)[chan[i].outVol]);
chan[i].soundLen=64;
} else {
chan[i].envLen=0;
@ -188,7 +219,7 @@ void DivPlatformGB::tick(bool sysTick) {
rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(chan[i].soundLen&63)));
} else if (!chan[i].softEnv) {
if (parent->song.waveDutyIsVol) {
rWrite(16+i*5+2,gbVolMap[(chan[i].std.duty.val&3)<<2]);
rWrite(16+i*5+2,(model==GB_MODEL_AGB_NATIVE?gbVolMapEx:gbVolMap)[(chan[i].std.duty.val&3)<<2]);
}
}
}
@ -301,8 +332,8 @@ void DivPlatformGB::tick(bool sysTick) {
if (chan[i].keyOn) {
if (i==2) { // wave
rWrite(16+i*5,0x00);
rWrite(16+i*5,0x80);
rWrite(16+i*5+2,gbVolMap[chan[i].outVol]);
rWrite(16+i*5,doubleWave?0xa0:0x80);
rWrite(16+i*5+2,(model==GB_MODEL_AGB_NATIVE?gbVolMapEx:gbVolMap)[chan[i].outVol]);
} else {
rWrite(16+i*5+1,((chan[i].duty&3)<<6)|(63-(chan[i].soundLen&63)));
rWrite(16+i*5+2,((chan[i].envVol<<4))|(chan[i].envLen&7)|((chan[i].envDir&1)<<3));
@ -379,11 +410,16 @@ int DivPlatformGB::dispatch(DivCommand c) {
chan[c.chan].softEnv=ins->gb.softEnv;
chan[c.chan].macroInit(ins);
if (c.chan==2) {
doubleWave=(model==GB_MODEL_AGB_NATIVE) && ins->gb.doubleWave;
if (chan[c.chan].wave<0) {
chan[c.chan].wave=0;
ws.changeWave1(chan[c.chan].wave);
}
ws.init(ins,32,15,chan[c.chan].insChanged);
ws.init(ins,doubleWave?64:32,15,chan[c.chan].insChanged);
if (doubleWave!=lastDoubleWave) {
ws.changeWave1(chan[c.chan].wave);
lastDoubleWave=doubleWave;
}
}
if ((chan[c.chan].insChanged || ins->gb.alwaysInit) && !chan[c.chan].softEnv) {
if (!chan[c.chan].soManyHacksToMakeItDefleCompatible && c.chan!=2) {
@ -447,7 +483,7 @@ int DivPlatformGB::dispatch(DivCommand c) {
chan[c.chan].vol=c.value;
chan[c.chan].outVol=c.value;
if (c.chan==2) {
rWrite(16+c.chan*5+2,gbVolMap[chan[c.chan].outVol]);
rWrite(16+c.chan*5+2,(model==GB_MODEL_AGB_NATIVE?gbVolMapEx:gbVolMap)[chan[c.chan].outVol]);
}
if (!chan[c.chan].softEnv) {
chan[c.chan].envVol=chan[c.chan].vol;
@ -619,6 +655,8 @@ void DivPlatformGB::reset() {
antiClickPeriodCount=0;
antiClickWavePos=0;
doubleWave=false;
lastDoubleWave=false;
}
int DivPlatformGB::getPortaFloor(int ch) {
@ -630,7 +668,7 @@ int DivPlatformGB::getOutputCount() {
}
bool DivPlatformGB::getDCOffRequired() {
return (model==GB_MODEL_AGB);
return (model==GB_MODEL_AGB_NATIVE);
}
void DivPlatformGB::notifyInsChange(int ins) {
@ -676,7 +714,7 @@ void DivPlatformGB::setFlags(const DivConfig& flags) {
model=GB_MODEL_CGB_E;
break;
case 3:
model=GB_MODEL_AGB;
model=GB_MODEL_AGB_NATIVE;
break;
}
invertWave=flags.getBool("invertWave",true);

View file

@ -59,6 +59,8 @@ class DivPlatformGB: public DivDispatch {
bool antiClickEnabled;
bool invertWave;
bool enoughAlready;
bool doubleWave;
bool lastDoubleWave;
unsigned char lastPan;
DivWaveSynth ws;
struct QueuedWrite {

View file

@ -0,0 +1,528 @@
/**
* 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.
*/
#define _USE_MATH_DEFINES
#include "gbadma.h"
#include "../engine.h"
#include "../filter.h"
#include <math.h>
#define CHIP_DIVIDER 16
void DivPlatformGBADMA::acquire(short** buf, size_t len) {
// HLE for now
int outL[2]={0,0};
int outR[2]={0,0};
for (size_t h=0; h<len; h++) {
// internal mixing is always 10-bit
for (int i=0; i<2; i++) {
bool newSamp=h==0;
chan[i].audDat=0;
if (chan[i].active && (chan[i].useWave || (chan[i].sample>=0 && chan[i].sample<parent->song.sampleLen))) {
chan[i].audSub+=(1<<outDepth);
if (chan[i].useWave) {
if (chan[i].audPos<(int)chan[i].audLen) {
chan[i].audDat=wtMem[i*256+chan[i].audPos];
}
newSamp=true;
if (chan[i].audSub>=chan[i].freq) {
int posInc=chan[i].audSub/chan[i].freq;
chan[i].audSub-=chan[i].freq*posInc;
chan[i].audPos+=posInc;
chan[i].dmaCount+=posInc;
if (chan[i].dmaCount>=16 && chan[i].audPos>=(int)chan[i].audLen) {
chan[i].audPos%=chan[i].audLen;
}
chan[i].dmaCount&=15;
}
} else if (sampleLoaded[chan[i].sample]) {
DivSample* s=parent->getSample(chan[i].sample);
if (s->samples>0) {
if (chan[i].audPos>=0) {
unsigned int pos=(sampleOff[chan[i].sample]+chan[i].audPos)&0x01ffffff;
chan[i].audDat=sampleMem[pos];
}
newSamp=true;
if (chan[i].audSub>=chan[i].freq) {
int posInc=chan[i].audSub/chan[i].freq;
chan[i].audSub-=chan[i].freq*posInc;
chan[i].audPos+=posInc;
chan[i].dmaCount+=posInc;
if (s->isLoopable()) {
if (chan[i].dmaCount>=16 && chan[i].audPos>=s->loopEnd) {
int loopStart=s->loopStart&~3;
int loopPos=chan[i].audPos-loopStart;
chan[i].audPos=(loopPos%(s->loopEnd-s->loopStart))+loopStart;
}
} else if (chan[i].audPos>=(int)s->samples) {
chan[i].sample=-1;
}
chan[i].dmaCount&=15;
}
} else {
chan[i].sample=-1;
chan[i].audSub=0;
chan[i].audPos=0;
}
}
}
if (!isMuted[i] && newSamp) {
int out=chan[i].audDat*(chan[i].vol*chan[i].envVol/2)<<1;
outL[i]=(chan[i].pan&2)?out:0;
outR[i]=(chan[i].pan&1)?out:0;
}
oscBuf[i]->data[oscBuf[i]->needle++]=(short)((outL[i]+outR[i])<<5);
}
int l=outL[0]+outL[1];
int r=outR[0]+outR[1];
l=(l>>(10-outDepth))<<(16-outDepth);
r=(r>>(10-outDepth))<<(16-outDepth);
if (l<-32768) l=-32768;
if (l>32767) l=32767;
if (r<-32768) r=-32768;
if (r>32767) r=32767;
buf[0][h]=(short)l;
buf[1][h]=(short)r;
}
}
void DivPlatformGBADMA::tick(bool sysTick) {
for (int i=0; i<2; i++) {
DivInstrument* ins=parent->getIns(chan[i].ins,DIV_INS_AMIGA);
chan[i].std.next();
if (chan[i].std.vol.had) {
chan[i].envVol=chan[i].std.vol.val;
if (ins->type==DIV_INS_AMIGA) chan[i].envVol/=32;
else if (chan[i].envVol>2) chan[i].envVol=2;
}
if (NEW_ARP_STRAT) {
chan[i].handleArp();
} else if (chan[i].std.arp.had) {
if (!chan[i].inPorta) {
chan[i].baseFreq=NOTE_PERIODIC(parent->calcArp(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].keyOff) chan[i].keyOn=true;
}
}
if (chan[i].useWave && chan[i].active) {
if (chan[i].ws.tick()) {
updateWave(i);
}
}
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 (ins->type==DIV_INS_AMIGA) {
if (chan[0].std.panL.had) {
chan[0].pan=(chan[0].pan&~2)|(chan[0].std.panL.val>0?2:0);
}
if (chan[0].std.panR.had) {
chan[0].pan=(chan[0].pan&~1)|(chan[0].std.panR.val>0?1:0);
}
} else {
if (chan[i].std.panL.had) {
chan[i].pan=chan[i].std.panL.val&3;
}
}
if (chan[i].std.phaseReset.had && chan[i].std.phaseReset.val==1) {
chan[i].audPos=0;
}
if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) {
double off=1.0;
if (!chan[i].useWave && chan[i].sample>=0 && chan[i].sample<parent->song.sampleLen) {
DivSample* s=parent->getSample(chan[i].sample);
off=(s->centerRate>=1)?(8363.0/(double)s->centerRate):1.0;
}
chan[i].freq=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);
// emulate prescaler rounding
if (chan[i].freq<65536) {
if (chan[i].freq<1) chan[i].freq=1;
} else if (chan[i].freq<65536*64) {
chan[i].freq=chan[i].freq&~63;
} else if (chan[i].freq<65536*256) {
chan[i].freq=chan[i].freq&~255;
} else {
chan[i].freq=chan[i].freq&~1024;
if (chan[i].freq>65536*1024) chan[i].freq=65536*1024;
}
if (chan[i].keyOn) {
if (!chan[i].std.vol.had) {
chan[i].envVol=2;
}
chan[i].keyOn=false;
}
if (chan[i].keyOff) {
chan[i].keyOff=false;
}
chan[i].freqChanged=false;
}
}
}
int DivPlatformGBADMA::dispatch(DivCommand c) {
switch (c.cmd) {
case DIV_CMD_NOTE_ON: {
DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA);
if (ins->amiga.useWave) {
chan[c.chan].useWave=true;
chan[c.chan].audLen=ins->amiga.waveLen+1;
wtMemCompo.entries[c.chan].end=wtMemCompo.entries[c.chan].begin+chan[c.chan].audLen;
if (chan[c.chan].insChanged) {
if (chan[c.chan].wave<0) {
chan[c.chan].wave=0;
chan[c.chan].ws.setWidth(chan[c.chan].audLen);
chan[c.chan].ws.changeWave1(chan[c.chan].wave);
}
}
} 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 (c.value!=DIV_NOTE_NULL) {
chan[c.chan].baseFreq=NOTE_PERIODIC(c.value);
}
if (chan[c.chan].useWave || chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) {
chan[c.chan].sample=-1;
}
if (chan[c.chan].setPos) {
chan[c.chan].setPos=false;
} else {
chan[c.chan].audPos=0;
}
chan[c.chan].audSub=0;
chan[c.chan].audDat=0;
chan[c.chan].dmaCount=0;
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].envVol=2;
}
if (chan[c.chan].useWave) {
chan[c.chan].ws.init(ins,chan[c.chan].audLen,255,chan[c.chan].insChanged);
}
chan[c.chan].insChanged=false;
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;
chan[c.chan].insChanged=true;
}
break;
case DIV_CMD_VOLUME:
if (chan[c.chan].vol!=c.value) {
chan[c.chan].vol=MIN(c.value,2);
if (!chan[c.chan].std.vol.has) {
chan[c.chan].envVol=2;
}
}
break;
case DIV_CMD_GET_VOLUME:
return chan[c.chan].vol;
break;
case DIV_CMD_PANNING:
chan[c.chan].pan=0;
chan[c.chan].pan|=(c.value>0)?2:0;
chan[c.chan].pan|=(c.value2>0)?1:0;
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].keyOn=true;
chan[c.chan].ws.changeWave1(chan[c.chan].wave);
break;
case DIV_CMD_NOTE_PORTA: {
DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_AMIGA);
chan[c.chan].sample=ins->amiga.getSample(c.value2);
int destFreq=NOTE_PERIODIC(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_PERIODIC(c.value+((HACKY_LEGATO_MESS)?(chan[c.chan].std.arp.val):(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));
}
chan[c.chan].inPorta=c.value;
break;
case DIV_CMD_SAMPLE_POS:
if (chan[c.chan].useWave) break;
chan[c.chan].audPos=c.value;
chan[c.chan].setPos=true;
break;
case DIV_CMD_GET_VOLMAX:
return 2;
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 DivPlatformGBADMA::updateWave(int ch) {
int addr=ch*256;
for (unsigned int i=0; i<chan[ch].audLen; i++) {
wtMem[addr+i]=(signed char)(chan[ch].ws.output[i]-128);
}
}
void DivPlatformGBADMA::muteChannel(int ch, bool mute) {
isMuted[ch]=mute;
}
void DivPlatformGBADMA::forceIns() {
for (int i=0; i<2; i++) {
chan[i].insChanged=true;
chan[i].freqChanged=true;
chan[i].audPos=0;
chan[i].sample=-1;
}
}
void* DivPlatformGBADMA::getChanState(int ch) {
return &chan;
}
DivDispatchOscBuffer* DivPlatformGBADMA::getOscBuffer(int ch) {
return oscBuf[ch];
}
void DivPlatformGBADMA::reset() {
for (int i=0; i<2; i++) {
chan[i]=DivPlatformGBADMA::Channel();
chan[i].std.setEngine(parent);
chan[i].ws.setEngine(parent);
chan[i].ws.init(NULL,32,255);
chan[i].audDat=0;
}
}
int DivPlatformGBADMA::getOutputCount() {
return 2;
}
DivMacroInt* DivPlatformGBADMA::getChanMacroInt(int ch) {
return &chan[ch].std;
}
unsigned short DivPlatformGBADMA::getPan(int ch) {
return ((chan[ch].pan&2)<<7)|(chan[ch].pan&1);
}
DivSamplePos DivPlatformGBADMA::getSamplePos(int ch) {
if (ch>=2 || !chan[ch].active ||
chan[ch].sample<0 || chan[ch].sample>=parent->song.sampleLen) {
return DivSamplePos();
}
return DivSamplePos(
chan[ch].sample,
chan[ch].audPos,
chipClock/chan[ch].freq
);
}
void DivPlatformGBADMA::notifyInsChange(int ins) {
for (int i=0; i<2; i++) {
if (chan[i].ins==ins) {
chan[i].insChanged=true;
}
}
}
void DivPlatformGBADMA::notifyWaveChange(int wave) {
for (int i=0; i<2; i++) {
if (chan[i].useWave && chan[i].wave==wave) {
chan[i].ws.changeWave1(wave);
updateWave(i);
}
}
}
void DivPlatformGBADMA::notifyInsDeletion(void* ins) {
for (int i=0; i<2; i++) {
chan[i].std.notifyInsDeletion((DivInstrument*)ins);
}
}
const void* DivPlatformGBADMA::getSampleMem(int index) {
return index == 0 ? sampleMem : NULL;
}
size_t DivPlatformGBADMA::getSampleMemCapacity(int index) {
return index == 0 ? 33554432 : 0;
}
size_t DivPlatformGBADMA::getSampleMemUsage(int index) {
return index == 0 ? sampleMemLen : 0;
}
bool DivPlatformGBADMA::isSampleLoaded(int index, int sample) {
if (index!=0) return false;
if (sample<0 || sample>255) return false;
return sampleLoaded[sample];
}
const DivMemoryComposition* DivPlatformGBADMA::getMemCompo(int index) {
switch (index) {
case 0: return &romMemCompo;
case 1: return &wtMemCompo;
}
return NULL;
}
void DivPlatformGBADMA::renderSamples(int sysID) {
size_t maxPos=getSampleMemCapacity();
memset(sampleMem,0,maxPos);
romMemCompo.entries.clear();
romMemCompo.capacity=maxPos;
size_t memPos=0;
for (int i=0; i<parent->song.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 (actualLength<length) {
logW("out of GBA DMA PCM memory for sample %d!",i);
break;
}
sampleLoaded[i]=true;
romMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_SAMPLE,"PCM",i,sampleOff[i],memPos));
// pad to multiple of 16 bytes
memPos=(memPos+15)&~15;
}
sampleMemLen=memPos;
romMemCompo.used=sampleMemLen;
}
void DivPlatformGBADMA::setFlags(const DivConfig& flags) {
outDepth=flags.getInt("dacDepth",9);
chipClock=1<<24;
CHECK_CUSTOM_CLOCK;
rate=chipClock>>outDepth;
for (int i=0; i<2; i++) {
oscBuf[i]->rate=rate;
}
}
int DivPlatformGBADMA::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) {
parent=p;
dumpWrites=false;
skipRegisterWrites=false;
for (int i=0; i<2; i++) {
isMuted[i]=false;
oscBuf[i]=new DivDispatchOscBuffer;
wtMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_WAVE_RAM, fmt::sprintf("Channel %d",i),-1,i*256,i*256));
}
sampleMem=new signed char[getSampleMemCapacity()];
sampleMemLen=0;
romMemCompo=DivMemoryComposition();
romMemCompo.name="Sample ROM";
wtMemCompo=DivMemoryComposition();
wtMemCompo.name="Wavetable RAM";
wtMemCompo.used=256*2;
wtMemCompo.capacity=256*2;
wtMemCompo.memory=(unsigned char*)wtMem;
wtMemCompo.waveformView=DIV_MEMORY_WAVE_8BIT_SIGNED;
setFlags(flags);
reset();
return 2;
}
void DivPlatformGBADMA::quit() {
delete[] sampleMem;
for (int i=0; i<2; i++) {
delete oscBuf[i];
}
}

View file

@ -0,0 +1,101 @@
/**
* 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_DMA_H
#define _GBA_DMA_H
#include "../dispatch.h"
#include "../waveSynth.h"
class DivPlatformGBADMA: public DivDispatch {
struct Channel: public SharedChannel<int> {
unsigned int audLoc;
unsigned short audLen;
int audDat;
int audPos;
int audSub;
int dmaCount;
int sample, wave;
int pan;
bool useWave, setPos;
int envVol;
DivWaveSynth ws;
Channel():
SharedChannel<int>(2),
audLoc(0),
audLen(0),
audDat(0),
audPos(0),
audSub(0),
dmaCount(0),
sample(-1),
wave(-1),
pan(3),
useWave(false),
setPos(false),
envVol(2) {}
};
Channel chan[2];
DivDispatchOscBuffer* oscBuf[2];
bool isMuted[2];
unsigned int sampleOff[256];
bool sampleLoaded[256];
int outDepth;
signed char* sampleMem;
size_t sampleMemLen;
// maximum wavetable length is currently hardcoded to 256
signed char wtMem[256*2];
DivMemoryComposition romMemCompo;
DivMemoryComposition wtMemCompo;
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);
DivDispatchOscBuffer* getOscBuffer(int chan);
void reset();
void forceIns();
void tick(bool sysTick=true);
void muteChannel(int ch, bool mute);
int getOutputCount();
DivMacroInt* getChanMacroInt(int ch);
unsigned short getPan(int chan);
DivSamplePos getSamplePos(int ch);
void setFlags(const DivConfig& flags);
void notifyInsChange(int ins);
void notifyWaveChange(int wave);
void notifyInsDeletion(void* ins);
const void* getSampleMem(int index = 0);
size_t getSampleMemCapacity(int index = 0);
size_t getSampleMemUsage(int index = 0);
bool isSampleLoaded(int index, int sample);
const DivMemoryComposition* getMemCompo(int index);
void renderSamples(int chipID);
int init(DivEngine* parent, int channels, int sugRate, const DivConfig& flags);
void quit();
private:
void updateWave(int ch);
};
#endif

View file

@ -0,0 +1,781 @@
/**
* 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 <math.h>
#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 {
uint64_t address;
unsigned int freq, loopEnd, loopStart;
short volL, volR;
} chState[16];
for (int i=0; i<chanMax; i++) {
unsigned short* chReg=&regPool[i*16];
chState[i].address=chReg[0]|((uint64_t)chReg[1]<<16)|((uint64_t)chReg[2]<<32)|((uint64_t)chReg[3]<<48);
chState[i].freq=chReg[8]|((unsigned int)chReg[9]<<16);
chState[i].loopEnd=chReg[10]|((unsigned int)chReg[11]<<16);
chState[i].loopStart=chReg[12]|((unsigned int)chReg[13]<<16);
chState[i].volL=(short)chReg[14];
chState[i].volR=(short)chReg[15];
}
for (size_t h=0; h<len; h++) {
while (sampTimer>=sampCycles) {
// the driver generates 4 samples at a time and can be start-offset
sampPos=mixBufReadPos&3;
if (sampPos==mixBufOffset) {
for (size_t j=mixBufOffset; j<4; j++) {
mixOut[0][j]=0;
mixOut[1][j]=0;
}
for (int i=0; i<chanMax; i++) {
for (size_t j=mixBufOffset; j<4; j++) {
unsigned int lastAddr=chState[i].address>>32;
chState[i].address+=((uint64_t)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)|((uint64_t)newAddr<<32);
}
int newSamp=0;
switch (newAddr>>24) {
case 2: // wavetable
newAddr&=0x0003ffff;
if (newAddr<sizeof(wtMem)) {
newSamp=wtMem[newAddr];
}
break;
case 3: // echo
newAddr&=0x00007fff;
if (newAddr>0x800) {
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 (size_t 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; i<chanMax; i++) {
oscBuf[i]->data[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);
mixMemCompo.entries[mixBufPage].end=mixMemCompo.entries[mixBufPage].begin+mixBufWritePos;
mixMemCompo.entries[mixBufPage+1].end=mixMemCompo.entries[mixBufPage+1].begin+mixBufWritePos;
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 (size_t j=0; j<mixBufOffset; j++) {
mixOut[0][j]=mixOut[0][4-mixBufOffset+j];
mixOut[1][j]=mixOut[1][4-mixBufOffset+j];
mixBuf[mixBufPage][j]=mixOut[0][j];
mixBuf[mixBufPage+1][j]=mixOut[1][j];
}
// check for echo channels and give them proper addresses
for (int i=0; i<chanMax; i++) {
unsigned char echoDelay=MIN(chan[i].echo&0x0f,mixBufs-1);
if(echoDelay) {
echoDelay=echoDelay*2-((chan[i].echo&0x10)?0:1);
size_t echoPage=(mixBufPage+mixBufs*2-echoDelay)%(mixBufs*2);
chState[i].address=(0x03000800ULL+echoPage*1024)<<32;
chState[i].loopStart=0-echoDelay;
chState[i].loopEnd=0-echoDelay;
}
}
updTimer-=updCycles;
updCyclesTotal+=updCycles;
// recalculate update timer from a new tick rate
float hz=parent->getCurHz();
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<<dacDepth;
sampTimer+=1<<dacDepth;
}
// write back changed cached channel registers
for (int i=0; i<chanMax; i++) {
unsigned short* chReg=&regPool[i*16];
chReg[0]=chState[i].address&0xffff;
chReg[1]=(chState[i].address>>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 && updCyclesTotal>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/(float)updCyclesTotal;
}
sampsRendered=0;
updCyclesTotal=0;
for (int i=0; i<chanMax; i++) {
chan[i].std.next();
if (chan[i].std.vol.had) {
chan[i].outVol=(chan[i].vol*MIN(chan[i].macroVolMul,chan[i].std.vol.val))/chan[i].macroVolMul;
chan[i].volChangedL=true;
chan[i].volChangedR=true;
}
if (NEW_ARP_STRAT) {
chan[i].handleArp();
} else if (chan[i].std.arp.had) {
if (!chan[i].inPorta) {
chan[i].baseFreq=NOTE_FREQUENCY(parent->calcArp(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&0xf)!=0) {
// make sure echo channels' frequency can't be faster than the sample rate
if (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 (c.chan<chanMax) {
wtMemCompo.entries[c.chan].end=wtMemCompo.entries[c.chan].begin+chan[c.chan].wtLen;
}
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_CMD_MACRO_RESTART:
chan[c.chan].std.restart(c.value);
break;
default:
break;
}
return 1;
}
void DivPlatformGBAMinMod::updateWave(int ch) {
int addr=ch*256;
for (unsigned int i=0; i<chan[ch].wtLen; i++) {
wtMem[addr+i]=(signed char)(chan[ch].ws.output[i]-128);
}
}
void DivPlatformGBAMinMod::muteChannel(int ch, bool mute) {
isMuted[ch]=mute;
chan[ch].volChangedL=true;
chan[ch].volChangedR=true;
}
void DivPlatformGBAMinMod::forceIns() {
for (int i=0; i<chanMax; i++) {
chan[i].insChanged=true;
chan[i].volChangedL=true;
chan[i].volChangedR=true;
chan[i].sample=-1;
chan[i].active=false;
}
}
void* DivPlatformGBAMinMod::getChanState(int ch) {
return &chan[ch];
}
DivMacroInt* DivPlatformGBAMinMod::getChanMacroInt(int ch) {
return &chan[ch].std;
}
unsigned short DivPlatformGBAMinMod::getPan(int ch) {
return (chan[ch].chPanL<<8)|(chan[ch].chPanR);
}
DivSamplePos DivPlatformGBAMinMod::getSamplePos(int ch) {
if (ch>=chanMax ||
chan[ch].sample<0 || chan[ch].sample>=parent->song.sampleLen ||
!chan[ch].active || (chan[ch].echo&0xf)!=0
) {
return DivSamplePos();
}
return DivSamplePos(
chan[ch].sample,
(((int)regPool[ch*16+2]|((int)regPool[ch*16+3]<<16))&0x01ffffff)-sampleOff[chan[ch].sample],
(int64_t)chan[ch].freq*chipClock/CHIP_FREQBASE
);
}
DivDispatchOscBuffer* DivPlatformGBAMinMod::getOscBuffer(int ch) {
return oscBuf[ch];
}
void DivPlatformGBAMinMod::reset() {
resetMixer();
memset(regPool,0,sizeof(regPool));
memset(wtMem,0,sizeof(wtMem));
for (int i=0; i<16; i++) {
chan[i]=DivPlatformGBAMinMod::Channel();
chan[i].std.setEngine(parent);
chan[i].ws.setEngine(parent);
chan[i].ws.init(NULL,32,255);
}
}
void DivPlatformGBAMinMod::resetMixer() {
sampTimer=sampCycles;
updTimer=0;
updCycles=0;
mixBufReadPos=0;
mixBufWritePos=0;
mixBufPage=0;
mixBufOffset=0;
sampsRendered=0;
memset(mixBuf,0,sizeof(mixBuf));
memset(chanOut,0,sizeof(chanOut));
}
int DivPlatformGBAMinMod::getOutputCount() {
return 2;
}
void DivPlatformGBAMinMod::notifyInsChange(int ins) {
for (int i=0; i<16; i++) {
if (chan[i].ins==ins) {
chan[i].insChanged=true;
}
}
}
void DivPlatformGBAMinMod::notifyWaveChange(int wave) {
for (int i=0; i<16; i++) {
if (chan[i].useWave && chan[i].wave==wave) {
chan[i].ws.changeWave1(wave);
updateWave(i);
}
}
}
void DivPlatformGBAMinMod::notifyInsDeletion(void* ins) {
for (int i=0; i<16; i++) {
chan[i].std.notifyInsDeletion((DivInstrument*)ins);
}
}
void DivPlatformGBAMinMod::poke(unsigned int addr, unsigned short val) {
rWrite(addr,val);
}
void DivPlatformGBAMinMod::poke(std::vector<DivRegWrite>& 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];
}
const DivMemoryComposition* DivPlatformGBAMinMod::getMemCompo(int index) {
switch (index) {
case 0: return &romMemCompo;
case 1: return &wtMemCompo;
case 2: return &mixMemCompo;
}
return NULL;
}
void DivPlatformGBAMinMod::renderSamples(int sysID) {
size_t maxPos=getSampleMemCapacity();
memset(sampleMem,0,maxPos);
romMemCompo.entries.clear();
romMemCompo.capacity=maxPos;
// dummy zero-length samples are at pos 0 as the engine still outputs them
size_t memPos=1;
for (int i=0; i<parent->song.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;
romMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_SAMPLE,"PCM",i,sampleOff[i],memPos));
// 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<length) {
logW("out of GBA MinMod PCM memory for sample %d!",i);
break;
}
sampleLoaded[i]=true;
}
sampleMemLen=memPos;
romMemCompo.used=sampleMemLen;
}
void DivPlatformGBAMinMod::setFlags(const DivConfig& flags) {
volScale=flags.getInt("volScale",4096);
mixBufs=flags.getInt("mixBufs",15);
dacDepth=flags.getInt("dacDepth",9);
chanMax=flags.getInt("channels",16);
rate=16777216>>dacDepth;
for (int i=0; i<16; i++) {
oscBuf[i]->rate=rate;
}
sampCycles=16777216/flags.getInt("sampRate",21845);
chipClock=16777216/sampCycles;
resetMixer();
wtMemCompo.used=256*chanMax;
mixMemCompo.used=2048*mixBufs;
wtMemCompo.entries.clear();
mixMemCompo.entries.clear();
for (int i=0; i<chanMax; i++) {
wtMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_WAVE_RAM, fmt::sprintf("Channel %d",i),-1,i*256,i*256));
}
for (int i=0; i<(int)mixBufs; i++) {
mixMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_ECHO, fmt::sprintf("Buffer %d Left",i),-1,i*2048,i*2048));
mixMemCompo.entries.push_back(DivMemoryEntry(DIV_MEMORY_ECHO, fmt::sprintf("Buffer %d Right",i),-1,i*2048+1024,i*2048+1024));
}
}
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;
romMemCompo=DivMemoryComposition();
romMemCompo.name="Sample ROM";
wtMemCompo=DivMemoryComposition();
wtMemCompo.name="Wavetable RAM";
wtMemCompo.capacity=256*16;
wtMemCompo.memory=(unsigned char*)wtMem;
wtMemCompo.waveformView=DIV_MEMORY_WAVE_8BIT_SIGNED;
mixMemCompo=DivMemoryComposition();
mixMemCompo.name="Mix/Echo Buffer";
mixMemCompo.capacity=2048*15;
mixMemCompo.memory=(unsigned char*)mixBuf;
mixMemCompo.waveformView=DIV_MEMORY_WAVE_8BIT_SIGNED;
setFlags(flags);
reset();
return 16;
}
void DivPlatformGBAMinMod::quit() {
delete[] sampleMem;
for (int i=0; i<16; i++) {
delete oscBuf[i];
}
}

View file

@ -0,0 +1,131 @@
/**
* 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<int> {
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<int>(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 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
unsigned short regPool[16*16];
signed char wtMem[256*16];
DivMemoryComposition romMemCompo;
DivMemoryComposition mixMemCompo;
DivMemoryComposition wtMemCompo;
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);
DivSamplePos getSamplePos(int ch);
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<DivRegWrite>& 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);
const DivMemoryComposition* getMemCompo(int index);
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

View file

@ -668,15 +668,17 @@ void GB_apu_run(GB_gameboy_t *gb)
if (gb->apu.is_active[GB_WAVE]) {
uint8_t cycles_left = cycles;
while (unlikely(cycles_left > gb->apu.wave_channel.sample_countdown)) {
uint8_t base = (!gb->apu.wave_channel.double_length && gb->apu.wave_channel.bank_select) ? 32 : 0;
cycles_left -= gb->apu.wave_channel.sample_countdown + 1;
gb->apu.wave_channel.sample_countdown = gb->apu.wave_channel.sample_length ^ 0x7FF;
gb->apu.wave_channel.current_sample_index++;
gb->apu.wave_channel.current_sample_index &= 0x1F;
gb->apu.wave_channel.current_sample_index &= gb->apu.wave_channel.double_length ? 0x3F : 0x1F;
gb->apu.wave_channel.current_sample =
gb->apu.wave_channel.wave_form[gb->apu.wave_channel.current_sample_index];
update_sample(gb, GB_WAVE,
gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift,
cycles - cycles_left);
gb->apu.wave_channel.wave_form[base + gb->apu.wave_channel.current_sample_index];
int8_t sample = gb->apu.wave_channel.force_3 ?
(gb->apu.wave_channel.current_sample * 3) >> 2 :
gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift;
update_sample(gb, GB_WAVE, sample, cycles - cycles_left);
gb->apu.wave_channel.wave_form_just_read = true;
}
if (cycles_left) {
@ -738,8 +740,10 @@ void GB_apu_init(GB_gameboy_t *gb)
memset(&gb->apu, 0, sizeof(gb->apu));
/* Restore the wave form */
for (unsigned reg = GB_IO_WAV_START; reg <= GB_IO_WAV_END; reg++) {
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = gb->io_registers[reg] >> 4;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = gb->io_registers[reg] & 0xF;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = gb->io_registers[reg] >> 4;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = gb->io_registers[reg] & 0xF;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 32] = gb->io_registers[reg] >> 4;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 33] = gb->io_registers[reg] & 0xF;
}
gb->apu.lf_div = 1;
/* APU glitch: When turning the APU on while DIV's bit 4 (or 5 in double speed mode) is on,
@ -903,6 +907,7 @@ static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb)
}
break;
case GB_MODEL_AGB:
case GB_MODEL_AGB_NATIVE:
/* TODO: AGBs are not affected, but AGSes are. They don't seem to follow a simple
pattern like the other revisions. */
/* For the most part, AGS seems to do:
@ -1160,14 +1165,24 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value)
gb->apu.is_active[GB_WAVE] = false;
update_sample(gb, GB_WAVE, 0, 0);
}
if (gb->model==GB_MODEL_AGB_NATIVE) {
gb->apu.wave_channel.bank_select = value & 0x40;
gb->apu.wave_channel.double_length = value & 0x20;
}
break;
case GB_IO_NR31:
gb->apu.wave_channel.pulse_length = (0x100 - value);
break;
case GB_IO_NR32:
gb->apu.wave_channel.shift = (uint8_t[]){4, 0, 1, 2}[(value >> 5) & 3];
if (gb->model==GB_MODEL_AGB_NATIVE) {
gb->apu.wave_channel.force_3 = value & 0x80;
}
if (gb->apu.is_active[GB_WAVE]) {
update_sample(gb, GB_WAVE, gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift, 0);
int8_t sample = gb->apu.wave_channel.force_3 ?
(gb->apu.wave_channel.current_sample * 3) >> 2 :
gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift;
update_sample(gb, GB_WAVE, sample, 0);
}
break;
case GB_IO_NR33:
@ -1209,9 +1224,10 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value)
}
if (!gb->apu.is_active[GB_WAVE]) {
gb->apu.is_active[GB_WAVE] = true;
update_sample(gb, GB_WAVE,
gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift,
0);
int8_t sample = gb->apu.wave_channel.force_3 ?
(gb->apu.wave_channel.current_sample * 3) >> 2 :
gb->apu.wave_channel.current_sample >> gb->apu.wave_channel.shift;
update_sample(gb, GB_WAVE, sample, 0);
}
gb->apu.wave_channel.sample_countdown = (gb->apu.wave_channel.sample_length ^ 0x7FF) + 3;
gb->apu.wave_channel.current_sample_index = 0;
@ -1411,8 +1427,13 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value)
default:
if (reg >= GB_IO_WAV_START && reg <= GB_IO_WAV_END) {
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2] = value >> 4;
gb->apu.wave_channel.wave_form[(reg - GB_IO_WAV_START) * 2 + 1] = value & 0xF;
uint8_t base = 0;
if (gb->model == GB_MODEL_AGB_NATIVE &&
(!gb->apu.global_enable || !gb->apu.wave_channel.bank_select)) {
base = 32;
}
gb->apu.wave_channel.wave_form[base + (reg - GB_IO_WAV_START) * 2] = value >> 4;
gb->apu.wave_channel.wave_form[base + (reg - GB_IO_WAV_START) * 2 + 1] = value & 0xF;
}
}
gb->io_registers[reg] = value;

View file

@ -93,12 +93,15 @@ typedef struct
uint8_t shift; // NR32
uint16_t sample_length; // NR33, NR34, in APU ticks
bool length_enabled; // NR34
bool double_length; // NR30
bool bank_select; // NR30
bool force_3; // NR32
uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length, xorred $7FF)
uint8_t current_sample_index;
uint8_t current_sample; // Current sample before shifting.
int8_t wave_form[32];
int8_t wave_form[64];
bool wave_form_just_read;
} wave_channel;

View file

@ -114,6 +114,7 @@ typedef enum {
// GB_MODEL_CGB_D = 0x204,
GB_MODEL_CGB_E = 0x205,
GB_MODEL_AGB = 0x206,
GB_MODEL_AGB_NATIVE = 0x226,
} GB_model_t;
enum {

View file

@ -247,6 +247,8 @@ const char* cmdName[]={
"ESFM_MODIN",
"ESFM_ENV_DELAY",
"MACRO_RESTART",
"POWERNOISE_COUNTER_LOAD",
"POWERNOISE_IO_WRITE",
@ -256,7 +258,7 @@ const char* cmdName[]={
"DAVE_LOW_PASS",
"DAVE_CLOCK_DIV",
"MACRO_RESTART",
"MINMOD_ECHO",
};
static_assert((sizeof(cmdName)/sizeof(void*))==DIV_CMD_MAX,"update cmdName!");

View file

@ -136,6 +136,8 @@ enum DivSystem {
DIV_SYSTEM_POWERNOISE,
DIV_SYSTEM_DAVE,
DIV_SYSTEM_NDS,
DIV_SYSTEM_GBA_DMA,
DIV_SYSTEM_GBA_MINMOD,
};
enum DivEffectType: unsigned short {

View file

@ -1862,7 +1862,11 @@ void DivEngine::registerSystems() {
{"Sample"},
{"PCM"},
{DIV_CH_PCM},
{DIV_INS_AMIGA}
{DIV_INS_AMIGA},
{},
{
{0x10, {DIV_CMD_WAVE, "10xx: Set waveform"}},
}
);
sysDefs[DIV_SYSTEM_K007232]=new DivSysDef(
@ -2021,6 +2025,36 @@ void DivEngine::registerSystems() {
}
);
sysDefs[DIV_SYSTEM_GBA_DMA]=new DivSysDef(
"Game Boy Advance DMA Sound", NULL, 0xd7, 0, 2, false, true, 0, false, 1U<<DIV_SAMPLE_DEPTH_8BIT, 0, 256,
"additional PCM FIFO channels in Game Boy Advance driven directly by its DMA hardware.",
{"PCM 1", "PCM 2"},
{"P1", "P2"},
{DIV_CH_PCM, DIV_CH_PCM},
{DIV_INS_GBA_DMA, DIV_INS_GBA_DMA},
{DIV_INS_AMIGA, DIV_INS_AMIGA},
{},
{
{0x10, {DIV_CMD_WAVE, "10xx: Set waveform"}},
}
);
sysDefs[DIV_SYSTEM_GBA_MINMOD]=new DivSysDef(
"Game Boy Advance MinMod", NULL, 0xd8, 0, 16, false, true, 0, false, 1U<<DIV_SAMPLE_DEPTH_8BIT, 0, 256,
"additional PCM FIFO channels in Game Boy Advance driven by software mixing to provide up to sixteen sample channels",
{"Channel 1", "Channel 2", "Channel 3", "Channel 4", "Channel 5", "Channel 6", "Channel 7", "Channel 8", "Channel 9", "Channel 10", "Channel 11", "Channel 12", "Channel 13", "Channel 14", "Channel 15", "Channel 16"},
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"},
{DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM, DIV_CH_PCM},
{DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD, DIV_INS_GBA_MINMOD},
{DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA, DIV_INS_AMIGA},
{},
{
{0x10, {DIV_CMD_WAVE, "10xx: Set waveform"}},
{0x11, {DIV_CMD_MINMOD_ECHO, "11xy: Set echo channel (x: left/right source; y: delay (0 disables))"}},
{0x12, {DIV_CMD_SNES_INVERT, "12xy: Toggle invert (x: left; y: right)"}},
}
);
sysDefs[DIV_SYSTEM_NDS]=new DivSysDef(
"NDS", NULL, 0xd6, 0, 16, false, true, 0, false, (1U<<DIV_SAMPLE_DEPTH_8BIT)|(1U<<DIV_SAMPLE_DEPTH_IMA_ADPCM)|(1U<<DIV_SAMPLE_DEPTH_16BIT), 32, 32,
"a handheld video game console with two screens. it uses a stylus.",

View file

@ -1542,7 +1542,9 @@ void FurnaceGUI::doAction(int what) {
i==DIV_INS_K053260 ||
i==DIV_INS_C140 ||
i==DIV_INS_C219 ||
i==DIV_INS_NDS) {
i==DIV_INS_NDS ||
i==DIV_INS_GBA_DMA ||
i==DIV_INS_GBA_MINMOD) {
makeInsTypeList.push_back(i);
}
}

View file

@ -296,6 +296,8 @@ enum FurnaceGUIColors {
GUI_COLOR_INSTR_POWERNOISE_SLOPE,
GUI_COLOR_INSTR_DAVE,
GUI_COLOR_INSTR_NDS,
GUI_COLOR_INSTR_GBA_DMA,
GUI_COLOR_INSTR_GBA_MINMOD,
GUI_COLOR_INSTR_UNKNOWN,
GUI_COLOR_CHANNEL_BG,

View file

@ -180,6 +180,8 @@ const char* insTypes[DIV_INS_MAX+1][3]={
{"PowerNoise (slope)",ICON_FUR_SAW,ICON_FUR_INS_POWERNOISE_SAW},
{"Dave",ICON_FA_BAR_CHART,ICON_FUR_INS_DAVE},
{"NDS",ICON_FA_BAR_CHART,ICON_FUR_INS_NDS},
{"GBA DMA",ICON_FA_GAMEPAD,ICON_FUR_INS_GBA_DMA},
{"GBA MinMod",ICON_FA_VOLUME_UP,ICON_FUR_INS_GBA_MINMOD},
{NULL,ICON_FA_QUESTION,ICON_FA_QUESTION}
};
@ -994,6 +996,8 @@ const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={
D(GUI_COLOR_INSTR_POWERNOISE_SLOPE,"",ImVec4(1.0f,0.6f,0.3f,1.0f)),
D(GUI_COLOR_INSTR_DAVE,"",ImVec4(0.7f,0.7f,0.8f,1.0f)),
D(GUI_COLOR_INSTR_NDS,"",ImVec4(0.7f,0.7f,0.8f,1.0f)),
D(GUI_COLOR_INSTR_GBA_DMA,"",ImVec4(0.6f,0.4f,1.0f,1.0f)),
D(GUI_COLOR_INSTR_GBA_MINMOD,"",ImVec4(0.5f,0.45f,0.7f,1.0f)),
D(GUI_COLOR_INSTR_UNKNOWN,"",ImVec4(0.3f,0.3f,0.3f,1.0f)),
D(GUI_COLOR_CHANNEL_BG,"",ImVec4(0.4f,0.6f,0.8f,1.0f)),
@ -1231,6 +1235,8 @@ const int availableSystems[]={
DIV_SYSTEM_TED,
DIV_SYSTEM_C140,
DIV_SYSTEM_C219,
DIV_SYSTEM_GBA_DMA,
DIV_SYSTEM_GBA_MINMOD,
DIV_SYSTEM_PCM_DAC,
DIV_SYSTEM_ESFM,
DIV_SYSTEM_PONG,
@ -1352,6 +1358,8 @@ const int chipsSample[]={
DIV_SYSTEM_C140,
DIV_SYSTEM_C219,
DIV_SYSTEM_NDS,
DIV_SYSTEM_GBA_DMA,
DIV_SYSTEM_GBA_MINMOD,
0 // don't remove this last one!
};

View file

@ -356,6 +356,10 @@ const char* es5506ControlModes[3]={
"pause", "reverse", NULL
};
const char* minModModeBits[3]={
"invert right", "invert left", NULL
};
const int orderedOps[4]={
0, 2, 1, 3
};
@ -2526,14 +2530,15 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) {
ImGui::EndCombo();
}
// Wavetable
if (ins->type==DIV_INS_AMIGA || ins->type==DIV_INS_SNES) {
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("Use wavetable (Amiga/SNES/Generic DAC only)",&ins->amiga.useWave));
P(ImGui::Checkbox(useWaveText,&ins->amiga.useWave));
if (ins->amiga.useWave) {
int len=ins->amiga.waveLen+1;
int origLen=len;
if (ImGui::InputInt("Width",&len,2,16)) {
if (ins->type==DIV_INS_SNES) {
if (ins->type==DIV_INS_SNES || ins->type==DIV_INS_GBA_DMA) {
if (len<16) len=16;
if (len>256) len=256;
if (len>origLen) {
@ -5396,6 +5401,7 @@ void FurnaceGUI::drawInsEdit() {
if (ins->type==DIV_INS_GB) if (ImGui::BeginTabItem("Game Boy")) {
P(ImGui::Checkbox("Use software envelope",&ins->gb.softEnv));
P(ImGui::Checkbox("Initialize envelope on every note",&ins->gb.alwaysInit));
P(ImGui::Checkbox("Double wave length (GBA only)",&ins->gb.doubleWave));
ImGui::BeginDisabled(ins->gb.softEnv);
if (ImGui::BeginTable("GBParams",2)) {
@ -6023,7 +6029,9 @@ void FurnaceGUI::drawInsEdit() {
ins->type==DIV_INS_K053260 ||
ins->type==DIV_INS_C140 ||
ins->type==DIV_INS_C219 ||
ins->type==DIV_INS_NDS) {
ins->type==DIV_INS_NDS ||
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")) {
@ -6458,6 +6466,7 @@ void FurnaceGUI::drawInsEdit() {
}
if (ins->type==DIV_INS_GB ||
(ins->type==DIV_INS_AMIGA && ins->amiga.useWave) ||
(ins->type==DIV_INS_GBA_DMA && ins->amiga.useWave) ||
(ins->type==DIV_INS_X1_010 && !ins->amiga.useSample) ||
ins->type==DIV_INS_N163 ||
ins->type==DIV_INS_FDS ||
@ -6467,7 +6476,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:
@ -6502,6 +6512,7 @@ void FurnaceGUI::drawInsEdit() {
wavePreviewHeight=255;
break;
case DIV_INS_AMIGA:
case DIV_INS_GBA_DMA:
wavePreviewLen=ins->amiga.waveLen+1;
wavePreviewHeight=255;
break;
@ -6509,6 +6520,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;
@ -6767,13 +6782,13 @@ 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) {
volMax=16383;
}
if (ins->type==DIV_INS_POKEMINI) {
if (ins->type==DIV_INS_POKEMINI || ins->type==DIV_INS_GBA_DMA) {
volMax=2;
}
@ -6832,7 +6847,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_C140 || ins->type==DIV_INS_GBA_DMA || ins->type==DIV_INS_GBA_MINMOD) {
dutyMax=0;
}
if (ins->type==DIV_INS_VBOY) {
@ -7038,6 +7053,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;
@ -7052,7 +7070,8 @@ void FurnaceGUI::drawInsEdit() {
ins->type==DIV_INS_VERA ||
ins->type==DIV_INS_ADPCMA ||
ins->type==DIV_INS_ADPCMB ||
ins->type==DIV_INS_ESFM) {
ins->type==DIV_INS_ESFM ||
ins->type==DIV_INS_GBA_DMA) {
panMax=2;
panSingle=true;
}
@ -7102,7 +7121,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;
}
@ -7214,7 +7233,8 @@ void FurnaceGUI::drawInsEdit() {
ins->type==DIV_INS_POWERNOISE ||
ins->type==DIV_INS_POWERNOISE_SLOPE ||
ins->type==DIV_INS_DAVE ||
ins->type==DIV_INS_NDS) {
ins->type==DIV_INS_NDS ||
ins->type==DIV_INS_GBA_DMA) {
macroList.push_back(FurnaceGUIMacroDesc("Phase Reset",&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true));
}
if (ex1Max>0) {
@ -7252,6 +7272,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]));
}

View file

@ -109,6 +109,14 @@ void FurnaceGUI::drawMemory() {
dl->AddRectFilled(pos1,pos2,ImGui::GetColorU32(uiColors[GUI_COLOR_MEMORY_DATA]));
}
break;
case DIV_MEMORY_WAVE_8BIT_SIGNED:
for (int k=0; k<(int)mc->capacity; k++) {
signed char val=(signed char)mc->memory[k];
ImVec2 pos1=ImLerp(dataRect.Min,dataRect.Max,ImVec2((double)k/(double)(mc->capacity),1.0f-((float)(val+129)/256.0f)));
ImVec2 pos2=ImLerp(dataRect.Min,dataRect.Max,ImVec2((double)(k+1)/(double)(mc->capacity),1.0f));
dl->AddRectFilled(pos1,pos2,ImGui::GetColorU32(uiColors[GUI_COLOR_MEMORY_DATA]));
}
break;
default:
break;
}

View file

@ -130,6 +130,18 @@ void FurnaceGUI::initSystemPresets() {
CH(DIV_SYSTEM_GB, 1.0f, 0, "")
}
);
ENTRY(
"Game Boy Advance (no software mixing)", {
CH(DIV_SYSTEM_GB, 1.0f, 0, "chipType=3"),
CH(DIV_SYSTEM_GBA_DMA, 0.5f, 0, ""),
}
);
ENTRY(
"Game Boy Advance (with MinMod)", {
CH(DIV_SYSTEM_GB, 1.0f, 0, "chipType=3"),
CH(DIV_SYSTEM_GBA_MINMOD, 0.5f, 0, ""),
}
);
ENTRY(
"Neo Geo Pocket", {
CH(DIV_SYSTEM_T6W28, 1.0f, 0, ""),

View file

@ -379,6 +379,20 @@ void FurnaceGUI::drawSampleEdit() {
if (sample->samples>129024) {
SAMPLE_WARN(warnLength,"MSM6295: maximum bankswitched sample length is 129024");
}
break;
case DIV_SYSTEM_GBA_DMA:
if (sample->loop) {
if (sample->loopStart&3) {
SAMPLE_WARN(warnLoopStart,"GBA DMA: loop start must be a multiple of 4");
}
if ((sample->loopEnd-sample->loopStart)&15) {
SAMPLE_WARN(warnLoopEnd,"GBA DMA: loop length must be a multiple of 16");
}
}
if (sample->samples&15) {
SAMPLE_WARN(warnLength,"GBA DMA: sample length will be padded to multiple of 16");
}
break;
default:
break;
}

View file

@ -3552,6 +3552,8 @@ 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_NDS,"NDS");
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();
}

View file

@ -18,6 +18,7 @@
*/
#include "../engine/chipUtils.h"
#include "../engine/platform/gbaminmod.h"
#include "gui.h"
#include "misc/cpp/imgui_stdlib.h"
#include <imgui.h>
@ -390,6 +391,78 @@ bool FurnaceGUI::drawSysConf(int chan, int sysPos, DivSystem type, DivConfig& fl
}
break;
}
case DIV_SYSTEM_GBA_DMA: {
int dacDepth=flags.getInt("dacDepth",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;
}
if (altered) {
e->lockSave([&]() {
flags.set("dacDepth",dacDepth);
});
}
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);
if (maxCPU>90) ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_WARNING]);
ImGui::Text("Max mixer CPU usage: %.0f%%", maxCPU);
if (maxCPU>90) ImGui::PopStyleColor();
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: {