total extinction of legacy sample mode, part 1
This commit is contained in:
parent
08e21a6298
commit
416148bd62
34 changed files with 44 additions and 437 deletions
|
|
@ -55,8 +55,8 @@ class DivWorkPool;
|
|||
|
||||
#define DIV_UNSTABLE
|
||||
|
||||
#define DIV_VERSION "dev238"
|
||||
#define DIV_ENGINE_VERSION 238
|
||||
#define DIV_VERSION "dev239"
|
||||
#define DIV_ENGINE_VERSION 239
|
||||
// for imports
|
||||
#define DIV_VERSION_MOD 0xff01
|
||||
#define DIV_VERSION_FC 0xff02
|
||||
|
|
|
|||
|
|
@ -934,10 +934,9 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) {
|
|||
sample->name="";
|
||||
}
|
||||
logD("%d name %s (%d)",i,sample->name.c_str(),length);
|
||||
sample->rate=22050;
|
||||
sample->centerRate=22050;
|
||||
if (ds.version>=0x0b) {
|
||||
sample->rate=fileToDivRate(reader.readC());
|
||||
sample->centerRate=sample->rate;
|
||||
sample->centerRate=fileToDivRate(reader.readC());
|
||||
pitch=reader.readC();
|
||||
vol=reader.readC();
|
||||
|
||||
|
|
@ -947,7 +946,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) {
|
|||
}
|
||||
}
|
||||
if (ds.version<=0x08) {
|
||||
sample->rate=ymuSampleRate*400;
|
||||
sample->centerRate=ymuSampleRate*400;
|
||||
}
|
||||
if (ds.version>0x15) {
|
||||
sample->depth=(DivSampleDepth)reader.readC();
|
||||
|
|
@ -1680,7 +1679,7 @@ SafeWriter* DivEngine::saveDMF(unsigned char version) {
|
|||
for (DivSample* i: song.sample) {
|
||||
w->writeI(i->samples);
|
||||
w->writeString(i->name,true);
|
||||
w->writeC(divToFileRate(i->rate));
|
||||
w->writeC(divToFileRate(i->centerRate));
|
||||
w->writeC(5);
|
||||
w->writeC(50);
|
||||
// i'm too lazy to deal with .dmf's weird way of storing 8-bit samples
|
||||
|
|
|
|||
|
|
@ -1986,7 +1986,6 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len, bool dnft, bool dnft_si
|
|||
|
||||
DivSample* sample = ds.sample[index];
|
||||
|
||||
sample->rate = 33144;
|
||||
sample->centerRate = 33144;
|
||||
sample->depth = DIV_SAMPLE_DEPTH_1BIT_DPCM;
|
||||
|
||||
|
|
|
|||
|
|
@ -112,8 +112,7 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) {
|
|||
if (slen==2) slen=0;
|
||||
signed char fineTune=reader.readC()&0x0f;
|
||||
if (fineTune>=8) fineTune-=16;
|
||||
sample->rate=(int)(pow(2.0,(double)fineTune/96.0)*8363.0);
|
||||
sample->centerRate=sample->rate;
|
||||
sample->centerRate=(int)(pow(2.0,(double)fineTune/96.0)*8363.0);
|
||||
defaultVols[i]=reader.readC();
|
||||
int loopStart=reader.readS_BE()*2;
|
||||
int loopLen=reader.readS_BE()*2;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ void DivEngine::loadP(SafeReader& reader, std::vector<DivSample*>& ret, String&
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = P_SAMPLE_RATE;
|
||||
s->centerRate = P_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_VOX;
|
||||
|
||||
|
|
@ -121,4 +120,4 @@ void DivEngine::loadP(SafeReader& reader, std::vector<DivSample*>& ret, String&
|
|||
lastError=_("premature end of file");
|
||||
logE("premature end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,6 @@ void DivEngine::loadP86(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = P86_SAMPLE_RATE;
|
||||
s->centerRate = P86_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_8BIT;
|
||||
s->init(headers[i].sample_length); //byte per sample
|
||||
|
|
@ -139,4 +138,4 @@ void DivEngine::loadP86(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
lastError=_("premature end of file");
|
||||
logE("premature end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ void DivEngine::loadPDX(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = PDX_SAMPLE_RATE;
|
||||
s->centerRate = PDX_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_VOX;
|
||||
s->init(headers[i].sample_length * 2);
|
||||
|
|
@ -98,4 +97,4 @@ void DivEngine::loadPDX(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
lastError=_("premature end of file");
|
||||
logE("premature end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ void DivEngine::loadPPC(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = PPC_SAMPLE_RATE;
|
||||
s->centerRate = PPC_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_ADPCM_B;
|
||||
s->init((headers[i].end_pointer - headers[i].start_pointer) * 32 * 2);
|
||||
|
|
@ -139,4 +138,4 @@ void DivEngine::loadPPC(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
lastError=_("premature end of file");
|
||||
logE("premature end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ void DivEngine::loadPPS(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = PPS_SAMPLE_RATE;
|
||||
s->centerRate = PPS_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_4BIT;
|
||||
s->init(headers[i].sample_length*2); // bytes->samples
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ void DivEngine::loadPVI(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = PVI_SAMPLE_RATE;
|
||||
s->centerRate = PVI_SAMPLE_RATE;
|
||||
s->depth = DIV_SAMPLE_DEPTH_ADPCM_B;
|
||||
s->init((headers[i].end_pointer - headers[i].start_pointer) * 32 * 2);
|
||||
|
|
@ -155,4 +154,4 @@ void DivEngine::loadPVI(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
lastError=_("premature end of file");
|
||||
logE("premature end of file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ void DivEngine::loadPZI(SafeReader& reader, std::vector<DivSample*>& ret, String
|
|||
{
|
||||
DivSample* s = new DivSample;
|
||||
|
||||
s->rate = headers[i].sample_rate;
|
||||
s->centerRate = headers[i].sample_rate;
|
||||
s->depth = DIV_SAMPLE_DEPTH_8BIT;
|
||||
s->init(headers[i].sample_length); //byte per sample
|
||||
|
|
|
|||
|
|
@ -262,7 +262,6 @@ SafeWriter* DivEngine::saveText(bool separatePatterns) {
|
|||
w->writeText(fmt::sprintf("- data length: %d\n",sample->getCurBufLen()));
|
||||
w->writeText(fmt::sprintf("- samples: %d\n",sample->samples));
|
||||
w->writeText(fmt::sprintf("- rate: %d\n",sample->centerRate));
|
||||
w->writeText(fmt::sprintf("- compat rate: %d\n",sample->rate));
|
||||
w->writeText(fmt::sprintf("- loop: %s\n",trueFalse[sample->loop?1:0]));
|
||||
if (sample->loop) {
|
||||
w->writeText(fmt::sprintf(" - start: %d\n",sample->loopStart));
|
||||
|
|
|
|||
|
|
@ -213,12 +213,10 @@ std::vector<DivSample*> DivEngine::sampleFromFile(const char* path) {
|
|||
}
|
||||
|
||||
if (extS==".dmc") {
|
||||
sample->rate=33144;
|
||||
sample->centerRate=33144;
|
||||
sample->depth=DIV_SAMPLE_DEPTH_1BIT_DPCM;
|
||||
sample->init(len*8);
|
||||
} else if (extS==".brr") {
|
||||
sample->rate=32000;
|
||||
sample->centerRate=32000;
|
||||
sample->depth=DIV_SAMPLE_DEPTH_BRR;
|
||||
sample->init(16*(len/9));
|
||||
|
|
@ -400,9 +398,6 @@ std::vector<DivSample*> DivEngine::sampleFromFile(const char* path) {
|
|||
delete[] (float*)buf;
|
||||
}
|
||||
|
||||
sample->rate=si.samplerate;
|
||||
if (sample->rate<4000) sample->rate=4000;
|
||||
if (sample->rate>96000) sample->rate=96000;
|
||||
sample->centerRate=si.samplerate;
|
||||
|
||||
SF_INSTRUMENT inst;
|
||||
|
|
@ -563,7 +558,6 @@ DivSample* DivEngine::sampleFromFileRaw(const char* path, DivSampleDepth depth,
|
|||
return NULL;
|
||||
}
|
||||
|
||||
sample->rate=rate;
|
||||
sample->centerRate=rate;
|
||||
sample->depth=depth;
|
||||
sample->init(samples);
|
||||
|
|
|
|||
|
|
@ -723,29 +723,7 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
|
|||
//chan[c.chan].keyOn=true;
|
||||
chan[c.chan].dac.furnaceDAC=true;
|
||||
} else {
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].dac.sample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].dac.sample>=parent->song.sampleLen) {
|
||||
chan[c.chan].dac.sample=-1;
|
||||
//if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
|
||||
break;
|
||||
} else {
|
||||
//if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
|
||||
}
|
||||
if (chan[c.chan].dac.setPos) {
|
||||
chan[c.chan].dac.setPos=false;
|
||||
} else {
|
||||
chan[c.chan].dac.pos=0;
|
||||
}
|
||||
chan[c.chan].dac.period=0;
|
||||
chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*2048;
|
||||
if (dumpWrites) {
|
||||
rWrite(0x08+c.chan,0);
|
||||
//addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate);
|
||||
}
|
||||
chan[c.chan].dac.furnaceDAC=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
chan[c.chan].curPSGMode.val&=~8;
|
||||
chan[c.chan].curPSGMode.val|=chan[c.chan].nextPSGMode.val&8;
|
||||
|
|
|
|||
|
|
@ -551,29 +551,7 @@ int DivPlatformAY8930::dispatch(DivCommand c) {
|
|||
//chan[c.chan].keyOn=true;
|
||||
chan[c.chan].dac.furnaceDAC=true;
|
||||
} else {
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].dac.sample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].dac.sample>=parent->song.sampleLen) {
|
||||
chan[c.chan].dac.sample=-1;
|
||||
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
|
||||
}
|
||||
if (chan[c.chan].dac.setPos) {
|
||||
chan[c.chan].dac.setPos=false;
|
||||
} else {
|
||||
chan[c.chan].dac.pos=0;
|
||||
}
|
||||
chan[c.chan].dac.period=0;
|
||||
chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*4096;
|
||||
if (dumpWrites) {
|
||||
rWrite(0x08+c.chan,0);
|
||||
addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate);
|
||||
}
|
||||
chan[c.chan].dac.furnaceDAC=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
chan[c.chan].curPSGMode.val&=~8;
|
||||
chan[c.chan].curPSGMode.val|=chan[c.chan].nextPSGMode.val&8;
|
||||
|
|
|
|||
|
|
@ -1108,25 +1108,7 @@ int DivPlatformGenesis::dispatch(DivCommand c) {
|
|||
//chan[c.chan].keyOn=true;
|
||||
chan[c.chan].active=true;
|
||||
} else { // compatible mode
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].sampleNote=DIV_NOTE_NULL;
|
||||
chan[c.chan].sampleNoteDelta=0;
|
||||
chan[c.chan].dacSample=12*chan[c.chan].sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].dacSample>=parent->song.sampleLen) {
|
||||
chan[c.chan].dacSample=-1;
|
||||
if (dumpWrites) addWrite(0xffff0002,0);
|
||||
break;
|
||||
} else {
|
||||
rWrite(0x2b,1<<7);
|
||||
if (dumpWrites) addWrite(0xffff0000,chan[c.chan].dacSample);
|
||||
}
|
||||
chan[c.chan].dacPos=0;
|
||||
chan[c.chan].dacPeriod=0;
|
||||
chan[c.chan].dacRate=MAX(1,parent->getSample(chan[c.chan].dacSample)->rate);
|
||||
if (dumpWrites) addWrite(0xffff0001,parent->getSample(chan[c.chan].dacSample)->rate);
|
||||
chan[c.chan].furnaceDac=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,26 +221,7 @@ int DivPlatformMMC5::dispatch(DivCommand c) {
|
|||
chan[c.chan].keyOn=true;
|
||||
chan[c.chan].furnaceDac=true;
|
||||
} else {
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
dacSample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (dacSample>=parent->song.sampleLen) {
|
||||
dacSample=-1;
|
||||
if (dumpWrites) addWrite(0xffff0002,0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites) addWrite(0xffff0000,dacSample);
|
||||
}
|
||||
if (chan[c.chan].setPos) {
|
||||
chan[c.chan].setPos=false;
|
||||
} else {
|
||||
dacPos=0;
|
||||
}
|
||||
dacPeriod=0;
|
||||
dacRate=parent->getSample(dacSample)->rate;
|
||||
if (dumpWrites) addWrite(0xffff0001,dacRate);
|
||||
chan[c.chan].furnaceDac=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -565,49 +565,7 @@ int DivPlatformNES::dispatch(DivCommand c) {
|
|||
chan[c.chan].keyOn=true;
|
||||
chan[c.chan].furnaceDac=true;
|
||||
} else {
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
dacSample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (dacSample>=parent->song.sampleLen) {
|
||||
dacSample=-1;
|
||||
if (dumpWrites && !dpcmMode) addWrite(0xffff0002,0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites && !dpcmMode) addWrite(0xffff0000,dacSample);
|
||||
}
|
||||
if (chan[c.chan].setPos) {
|
||||
chan[c.chan].setPos=false;
|
||||
} else {
|
||||
dacPos=0;
|
||||
}
|
||||
dacPeriod=0;
|
||||
dacRate=parent->getSample(dacSample)->rate;
|
||||
if (dumpWrites && !dpcmMode) addWrite(0xffff0001,dacRate);
|
||||
chan[c.chan].furnaceDac=false;
|
||||
if (dpcmMode && !skipRegisterWrites) {
|
||||
unsigned int dpcmAddr=sampleOffDPCM[dacSample]+(dacPos>>3);
|
||||
int dpcmLen=(parent->getSample(dacSample)->lengthDPCM-(dacPos>>3))>>4;
|
||||
if (dpcmLen<0) dpcmLen=0;
|
||||
if (dpcmLen>255) dpcmLen=255;
|
||||
goingToLoop=parent->getSample(dacSample)->isLoopable();
|
||||
// write DPCM
|
||||
rWrite(0x4015,15);
|
||||
if (nextDPCMFreq>=0) {
|
||||
rWrite(0x4010,nextDPCMFreq|(goingToLoop?0x40:0));
|
||||
nextDPCMFreq=-1;
|
||||
} else {
|
||||
rWrite(0x4010,calcDPCMRate(dacRate)|(goingToLoop?0x40:0));
|
||||
}
|
||||
rWrite(0x4012,(dpcmAddr>>6)&0xff);
|
||||
rWrite(0x4013,dpcmLen&0xff);
|
||||
rWrite(0x4015,31);
|
||||
if (dpcmBank!=(dpcmAddr>>14)) {
|
||||
dpcmBank=dpcmAddr>>14;
|
||||
logV("switching bank to %d",dpcmBank);
|
||||
if (dumpWrites) addWrite(0xffff0004,dpcmBank);
|
||||
}
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
} else if (c.chan==3) { // noise
|
||||
|
|
|
|||
|
|
@ -1879,34 +1879,7 @@ int DivPlatformOPL::dispatch(DivCommand c) {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
chan[c.chan].sample=-1;
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) {
|
||||
break;
|
||||
}
|
||||
chan[c.chan].sample=12*sampleBank+c.value%12;
|
||||
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
|
||||
DivSample* s=parent->getSample(12*sampleBank+c.value%12);
|
||||
immWrite(8,0);
|
||||
immWrite(9,(sampleOffB[chan[c.chan].sample]>>2)&0xff);
|
||||
immWrite(10,(sampleOffB[chan[c.chan].sample]>>10)&0xff);
|
||||
int end=sampleOffB[chan[c.chan].sample]+s->lengthB-1;
|
||||
immWrite(11,(end>>2)&0xff);
|
||||
immWrite(12,(end>>10)&0xff);
|
||||
int freq=(65536.0*(double)s->rate)/(double)chipRateBase;
|
||||
chan[c.chan].fixedFreq=freq;
|
||||
immWrite(16,freq&0xff);
|
||||
immWrite(17,(freq>>8)&0xff);
|
||||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
immWrite(7,0x01); // reset
|
||||
immWrite(9,0);
|
||||
immWrite(10,0);
|
||||
immWrite(11,0);
|
||||
immWrite(12,0);
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -348,32 +348,7 @@ int DivPlatformPCE::dispatch(DivCommand c) {
|
|||
}
|
||||
//chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
chan[c.chan].furnaceDac=false;
|
||||
chan[c.chan].sampleNote=DIV_NOTE_NULL;
|
||||
chan[c.chan].sampleNoteDelta=0;
|
||||
if (skipRegisterWrites) break;
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].dacSample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].dacSample>=parent->song.sampleLen) {
|
||||
chan[c.chan].dacSample=-1;
|
||||
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample);
|
||||
}
|
||||
if (chan[c.chan].setPos) {
|
||||
chan[c.chan].setPos=false;
|
||||
} else {
|
||||
chan[c.chan].dacPos=0;
|
||||
}
|
||||
chan[c.chan].dacPeriod=0;
|
||||
chan[c.chan].dacRate=parent->getSample(chan[c.chan].dacSample)->rate;
|
||||
if (dumpWrites) {
|
||||
chWrite(c.chan,0x04,parent->song.disableSampleMacro?0xdf:(0xc0|chan[c.chan].vol));
|
||||
addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate);
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,20 +233,7 @@ int DivPlatformSegaPCM::dispatch(DivCommand c) {
|
|||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
chan[c.chan].macroInit(NULL);
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].pcm.sample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].pcm.sample>=parent->song.sampleLen) {
|
||||
chan[c.chan].pcm.sample=-1;
|
||||
rWrite(0x86+(c.chan<<3),3);
|
||||
break;
|
||||
}
|
||||
chan[c.chan].pcm.freq=MIN(255,(parent->getSample(chan[c.chan].pcm.sample)->rate*255)/rate);
|
||||
chan[c.chan].furnacePCM=false;
|
||||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,24 +397,7 @@ int DivPlatformSwan::dispatch(DivCommand c) {
|
|||
chan[1].macroInit(ins);
|
||||
furnaceDac=true;
|
||||
} else {
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[1].note=c.value;
|
||||
}
|
||||
dacSample=12*sampleBank+chan[1].note%12;
|
||||
if (dacSample>=parent->song.sampleLen) {
|
||||
dacSample=-1;
|
||||
if (dumpWrites) postWrite(0xffff0002,0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites) postWrite(0xffff0000,dacSample);
|
||||
}
|
||||
dacRate=parent->getSample(dacSample)->rate;
|
||||
if (dumpWrites) {
|
||||
postWrite(0xffff0001,dacRate);
|
||||
}
|
||||
chan[1].active=true;
|
||||
chan[1].keyOn=true;
|
||||
furnaceDac=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,32 +303,7 @@ int DivPlatformVRC6::dispatch(DivCommand c) {
|
|||
//chan[c.chan].keyOn=true;
|
||||
chan[c.chan].furnaceDac=true;
|
||||
} else {
|
||||
chan[c.chan].sampleNote=DIV_NOTE_NULL;
|
||||
chan[c.chan].sampleNoteDelta=0;
|
||||
if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
}
|
||||
chan[c.chan].dacSample=12*sampleBank+chan[c.chan].note%12;
|
||||
if (chan[c.chan].dacSample>=parent->song.sampleLen) {
|
||||
chan[c.chan].dacSample=-1;
|
||||
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
|
||||
break;
|
||||
} else {
|
||||
if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dacSample);
|
||||
}
|
||||
if (chan[c.chan].setPos) {
|
||||
chan[c.chan].setPos=false;
|
||||
} else {
|
||||
chan[c.chan].dacPos=0;
|
||||
}
|
||||
chan[c.chan].dacPeriod=0;
|
||||
chan[c.chan].dacRate=parent->getSample(chan[c.chan].dacSample)->rate;
|
||||
if (dumpWrites) {
|
||||
chWrite(c.chan,2,0x80);
|
||||
chWrite(c.chan,0,isMuted[c.chan]?0:0x80);
|
||||
addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dacRate);
|
||||
}
|
||||
chan[c.chan].furnaceDac=false;
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,43 +592,15 @@ int DivPlatformX1_010::dispatch(DivCommand c) {
|
|||
} else {
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
// huh?
|
||||
if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) {
|
||||
chWrite(c.chan,0,0); // reset
|
||||
chWrite(c.chan,1,0);
|
||||
chWrite(c.chan,2,0);
|
||||
chWrite(c.chan,4,0);
|
||||
chWrite(c.chan,5,0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
chan[c.chan].sample=12*sampleBank+c.value%12;
|
||||
if (chan[c.chan].sample<0 || chan[c.chan].sample>=parent->song.sampleLen) {
|
||||
// TODO: there was a check for legacy sample bank here. why?
|
||||
chWrite(c.chan,0,0); // reset
|
||||
chWrite(c.chan,1,0);
|
||||
chWrite(c.chan,2,0);
|
||||
chWrite(c.chan,4,0);
|
||||
chWrite(c.chan,5,0);
|
||||
break;
|
||||
}
|
||||
DivSample* s=parent->getSample(chan[c.chan].sample);
|
||||
if (isBanked) {
|
||||
bankSlot[chan[c.chan].bankSlot]=sampleOffX1[chan[c.chan].sample]>>17;
|
||||
unsigned int bankedOffs=(chan[c.chan].bankSlot<<17)|(sampleOffX1[chan[c.chan].sample]&0x1ffff);
|
||||
chWrite(c.chan,4,(bankedOffs>>12)&0xff);
|
||||
int end=(bankedOffs+MIN(s->length8,0x1ffff)+0xfff)&~0xfff; // padded
|
||||
chWrite(c.chan,5,(0x100-(end>>12))&0xff);
|
||||
} else {
|
||||
chWrite(c.chan,4,(sampleOffX1[chan[c.chan].sample]>>12)&0xff);
|
||||
int end=(sampleOffX1[chan[c.chan].sample]+s->length8+0xfff)&~0xfff; // padded
|
||||
chWrite(c.chan,5,(0x100-(end>>12))&0xff);
|
||||
}
|
||||
// ????
|
||||
chan[c.chan].fixedFreq=(((unsigned int)s->rate)<<4)/(chipClock/512);
|
||||
chan[c.chan].freqChanged=true;
|
||||
} else {
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
} else if (c.value!=DIV_NOTE_NULL) {
|
||||
chan[c.chan].note=c.value;
|
||||
|
|
|
|||
|
|
@ -1196,36 +1196,7 @@ int DivPlatformYM2608::dispatch(DivCommand c) {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
chan[c.chan].sample=-1;
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) {
|
||||
break;
|
||||
}
|
||||
chan[c.chan].sample=12*sampleBank+c.value%12;
|
||||
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
|
||||
DivSample* s=parent->getSample(chan[c.chan].sample);
|
||||
immWrite(0x100,0x01); // reset
|
||||
immWrite(0x102,(sampleOffB[chan[c.chan].sample]>>5)&0xff);
|
||||
immWrite(0x103,(sampleOffB[chan[c.chan].sample]>>13)&0xff);
|
||||
int end=sampleOffB[chan[c.chan].sample]+s->lengthB-1;
|
||||
immWrite(0x104,(end>>5)&0xff);
|
||||
immWrite(0x105,(end>>13)&0xff);
|
||||
immWrite(0x101,(isMuted[c.chan]?0:(chan[c.chan].pan<<6))|memConfig);
|
||||
int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0);
|
||||
immWrite(0x109,freq&0xff);
|
||||
immWrite(0x10a,(freq>>8)&0xff);
|
||||
immWrite(0x10b,chan[c.chan].outVol);
|
||||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
immWrite(0x100,0x01); // reset
|
||||
immWrite(0x102,0);
|
||||
immWrite(0x103,0);
|
||||
immWrite(0x104,0);
|
||||
immWrite(0x105,0);
|
||||
break;
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1113,35 +1113,7 @@ int DivPlatformYM2610::dispatch(DivCommand c) {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
chan[c.chan].sample=-1;
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) {
|
||||
break;
|
||||
}
|
||||
chan[c.chan].sample=12*sampleBank+c.value%12;
|
||||
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
|
||||
DivSample* s=parent->getSample(12*sampleBank+c.value%12);
|
||||
immWrite(0x12,(sampleOffB[chan[c.chan].sample]>>8)&0xff);
|
||||
immWrite(0x13,sampleOffB[chan[c.chan].sample]>>16);
|
||||
int end=sampleOffB[chan[c.chan].sample]+s->lengthB-1;
|
||||
immWrite(0x14,(end>>8)&0xff);
|
||||
immWrite(0x15,end>>16);
|
||||
immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6));
|
||||
int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0);
|
||||
immWrite(0x19,freq&0xff);
|
||||
immWrite(0x1a,(freq>>8)&0xff);
|
||||
immWrite(0x1b,chan[c.chan].outVol);
|
||||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
immWrite(0x10,0x01); // reset
|
||||
immWrite(0x12,0);
|
||||
immWrite(0x13,0);
|
||||
immWrite(0x14,0);
|
||||
immWrite(0x15,0);
|
||||
break;
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1182,35 +1182,7 @@ int DivPlatformYM2610B::dispatch(DivCommand c) {
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
chan[c.chan].sample=-1;
|
||||
chan[c.chan].macroInit(NULL);
|
||||
chan[c.chan].outVol=chan[c.chan].vol;
|
||||
if ((12*sampleBank+c.value%12)>=parent->song.sampleLen) {
|
||||
break;
|
||||
}
|
||||
chan[c.chan].sample=12*sampleBank+c.value%12;
|
||||
if (chan[c.chan].sample>=0 && chan[c.chan].sample<parent->song.sampleLen) {
|
||||
DivSample* s=parent->getSample(12*sampleBank+c.value%12);
|
||||
immWrite(0x12,(sampleOffB[chan[c.chan].sample]>>8)&0xff);
|
||||
immWrite(0x13,sampleOffB[chan[c.chan].sample]>>16);
|
||||
int end=sampleOffB[chan[c.chan].sample]+s->lengthB-1;
|
||||
immWrite(0x14,(end>>8)&0xff);
|
||||
immWrite(0x15,end>>16);
|
||||
immWrite(0x11,isMuted[c.chan]?0:(chan[c.chan].pan<<6));
|
||||
int freq=(65536.0*(double)s->rate)/((double)chipClock/144.0);
|
||||
immWrite(0x19,freq&0xff);
|
||||
immWrite(0x1a,(freq>>8)&0xff);
|
||||
immWrite(0x1b,chan[c.chan].outVol);
|
||||
chan[c.chan].active=true;
|
||||
chan[c.chan].keyOn=true;
|
||||
} else {
|
||||
immWrite(0x10,0x01); // reset
|
||||
immWrite(0x12,0);
|
||||
immWrite(0x13,0);
|
||||
immWrite(0x14,0);
|
||||
immWrite(0x15,0);
|
||||
break;
|
||||
}
|
||||
assert(false && "LEGACY SAMPLE MODE!!!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ void DivSample::putSampleData(SafeWriter* w) {
|
|||
|
||||
w->writeString(name,false);
|
||||
w->writeI(samples);
|
||||
w->writeI(rate);
|
||||
w->writeI(centerRate);
|
||||
w->writeI(centerRate);
|
||||
w->writeC(depth);
|
||||
w->writeC(loopMode);
|
||||
|
|
@ -116,7 +116,8 @@ DivDataErrors DivSample::readSampleData(SafeReader& reader, short version) {
|
|||
if (!isNewSample) {
|
||||
loopEnd=samples;
|
||||
}
|
||||
rate=reader.readI();
|
||||
// just in case it's not new sample, it's a very old version and we gotta read a rate.
|
||||
centerRate=reader.readI();
|
||||
|
||||
if (isNewSample) {
|
||||
centerRate=reader.readI();
|
||||
|
|
@ -916,7 +917,6 @@ void DivSample::convert(DivSampleDepth newDepth, unsigned int formatMask) {
|
|||
if (loopStart>=0) loopStart=(double)loopStart*(tRate/sRate); \
|
||||
if (loopEnd>=0) loopEnd=(double)loopEnd*(tRate/sRate); \
|
||||
centerRate=(int)((double)centerRate*(tRate/sRate)); \
|
||||
rate=(int)((double)rate*(tRate/sRate)); \
|
||||
samples=finalCount; \
|
||||
if (depth==DIV_SAMPLE_DEPTH_16BIT) { \
|
||||
delete[] oldData16; \
|
||||
|
|
@ -1665,9 +1665,9 @@ DivSampleHistory* DivSample::prepareUndo(bool data, bool doNotPush) {
|
|||
duplicate=new unsigned char[getCurBufLen()];
|
||||
memcpy(duplicate,getCurBuf(),getCurBufLen());
|
||||
}
|
||||
h=new DivSampleHistory(duplicate,getCurBufLen(),samples,depth,rate,centerRate,loopStart,loopEnd,loop,brrEmphasis,brrNoFilter,dither,loopMode);
|
||||
h=new DivSampleHistory(duplicate,getCurBufLen(),samples,depth,centerRate,loopStart,loopEnd,loop,brrEmphasis,brrNoFilter,dither,loopMode);
|
||||
} else {
|
||||
h=new DivSampleHistory(depth,rate,centerRate,loopStart,loopEnd,loop,brrEmphasis,brrNoFilter,dither,loopMode);
|
||||
h=new DivSampleHistory(depth,centerRate,loopStart,loopEnd,loop,brrEmphasis,brrNoFilter,dither,loopMode);
|
||||
}
|
||||
if (!doNotPush) {
|
||||
while (!redoHist.empty()) {
|
||||
|
|
@ -1695,7 +1695,6 @@ DivSampleHistory* DivSample::prepareUndo(bool data, bool doNotPush) {
|
|||
memcpy(buf,h->data,h->length); \
|
||||
} \
|
||||
} \
|
||||
rate=h->rate; \
|
||||
centerRate=h->centerRate; \
|
||||
loopStart=h->loopStart; \
|
||||
loopEnd=h->loopEnd; \
|
||||
|
|
|
|||
|
|
@ -66,16 +66,15 @@ struct DivSampleHistory {
|
|||
unsigned char* data;
|
||||
unsigned int length, samples;
|
||||
DivSampleDepth depth;
|
||||
int rate, centerRate, loopStart, loopEnd;
|
||||
int centerRate, loopStart, loopEnd;
|
||||
bool loop, brrEmphasis, brrNoFilter, dither;
|
||||
DivSampleLoopMode loopMode;
|
||||
bool hasSample;
|
||||
DivSampleHistory(void* d, unsigned int l, unsigned int s, DivSampleDepth de, int r, int cr, int ls, int le, bool lp, bool be, bool bf, bool di, DivSampleLoopMode lm):
|
||||
DivSampleHistory(void* d, unsigned int l, unsigned int s, DivSampleDepth de, int cr, int ls, int le, bool lp, bool be, bool bf, bool di, DivSampleLoopMode lm):
|
||||
data((unsigned char*)d),
|
||||
length(l),
|
||||
samples(s),
|
||||
depth(de),
|
||||
rate(r),
|
||||
centerRate(cr),
|
||||
loopStart(ls),
|
||||
loopEnd(le),
|
||||
|
|
@ -85,12 +84,11 @@ struct DivSampleHistory {
|
|||
dither(di),
|
||||
loopMode(lm),
|
||||
hasSample(true) {}
|
||||
DivSampleHistory(DivSampleDepth de, int r, int cr, int ls, int le, bool lp, bool be, bool bf, bool di, DivSampleLoopMode lm):
|
||||
DivSampleHistory(DivSampleDepth de, int cr, int ls, int le, bool lp, bool be, bool bf, bool di, DivSampleLoopMode lm):
|
||||
data(NULL),
|
||||
length(0),
|
||||
samples(0),
|
||||
depth(de),
|
||||
rate(r),
|
||||
centerRate(cr),
|
||||
loopStart(ls),
|
||||
loopEnd(le),
|
||||
|
|
@ -105,7 +103,7 @@ struct DivSampleHistory {
|
|||
|
||||
struct DivSample {
|
||||
String name;
|
||||
int rate, centerRate, loopStart, loopEnd;
|
||||
int centerRate, loopStart, loopEnd;
|
||||
// valid values are:
|
||||
// - 0: ZX Spectrum overlay drum (1-bit)
|
||||
// - 1: 1-bit NES DPCM (1-bit)
|
||||
|
|
@ -337,7 +335,6 @@ struct DivSample {
|
|||
int redo();
|
||||
DivSample():
|
||||
name(""),
|
||||
rate(32000),
|
||||
centerRate(8363),
|
||||
loopStart(-1),
|
||||
loopEnd(-1),
|
||||
|
|
|
|||
|
|
@ -243,7 +243,6 @@ void FurnaceGUI::drawDebug() {
|
|||
continue;
|
||||
}
|
||||
if (ImGui::TreeNode(fmt::sprintf("%d: %s",i,sample->name).c_str())) {
|
||||
ImGui::Text("rate: %d",sample->rate);
|
||||
ImGui::Text("centerRate: %d",sample->centerRate);
|
||||
ImGui::Text("loopStart: %d",sample->loopStart);
|
||||
ImGui::Text("loopEnd: %d", sample->loopEnd);
|
||||
|
|
|
|||
|
|
@ -956,7 +956,6 @@ void FurnaceGUI::doAction(int what) {
|
|||
if (sample!=NULL) {
|
||||
DivWavetable* wave=e->song.wave[curWave];
|
||||
unsigned int waveLen=wave->len;
|
||||
sample->rate=(int)round(261.625565301*waveLen); // c3
|
||||
sample->centerRate=(int)round(261.625565301*waveLen); // c3
|
||||
sample->loopStart=0;
|
||||
sample->loopEnd=waveLen;
|
||||
|
|
@ -1045,7 +1044,6 @@ void FurnaceGUI::doAction(int what) {
|
|||
e->lockEngine([this,prevSample]() {
|
||||
DivSample* sample=e->getSample(curSample);
|
||||
if (sample!=NULL) {
|
||||
sample->rate=prevSample->rate;
|
||||
sample->centerRate=prevSample->centerRate;
|
||||
sample->name=prevSample->name;
|
||||
sample->loopStart=prevSample->loopStart;
|
||||
|
|
|
|||
|
|
@ -9134,7 +9134,6 @@ FurnaceGUI::FurnaceGUI():
|
|||
sampleSelStart(-1),
|
||||
sampleSelEnd(-1),
|
||||
sampleInfo(true),
|
||||
sampleCompatRate(false),
|
||||
sampleDragActive(false),
|
||||
sampleDragMode(false),
|
||||
sampleDrag16(false),
|
||||
|
|
|
|||
|
|
@ -2657,7 +2657,7 @@ class FurnaceGUI {
|
|||
int resampleStrat;
|
||||
float amplifyVol, amplifyOff;
|
||||
int sampleSelStart, sampleSelEnd;
|
||||
bool sampleInfo, sampleCompatRate;
|
||||
bool sampleInfo;
|
||||
bool sampleDragActive, sampleDragMode, sampleDrag16, sampleZoomAuto;
|
||||
bool sampleCheckLoopStart, sampleCheckLoopEnd;
|
||||
// 0: start
|
||||
|
|
|
|||
|
|
@ -611,7 +611,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
if (isChipVisible[i]) selColumns++;
|
||||
}
|
||||
|
||||
int targetRate=sampleCompatRate?sample->rate:sample->centerRate;
|
||||
int targetRate=sample->centerRate;
|
||||
|
||||
if (ImGui::BeginTable("SampleProps",(selColumns>1)?4:3,ImGuiTableFlags_SizingStretchSame|ImGuiTableFlags_BordersV|ImGuiTableFlags_BordersOuterH)) {
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
|
||||
|
|
@ -622,20 +622,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
ImGui::SameLine();
|
||||
ImGui::Text(_("Info"));
|
||||
ImGui::TableNextColumn();
|
||||
pushToggleColors(!sampleCompatRate);
|
||||
if (ImGui::Button(_("Rate"))) {
|
||||
sampleCompatRate=false;
|
||||
}
|
||||
popToggleColors();
|
||||
ImGui::SameLine();
|
||||
pushToggleColors(sampleCompatRate);
|
||||
if (ImGui::Button(_("Compat Rate"))) {
|
||||
sampleCompatRate=true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(_("used in DefleMask-compatible sample mode (17xx), in where samples are mapped to an octave."));
|
||||
}
|
||||
popToggleColors();
|
||||
ImGui::Text(_("Rate"));
|
||||
ImGui::TableNextColumn();
|
||||
bool doLoop=(sample->loop);
|
||||
pushWarningColor(!warnLoop.empty());
|
||||
|
|
@ -774,11 +761,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
if (targetRate<100) targetRate=100;
|
||||
if (targetRate>384000) targetRate=384000;
|
||||
|
||||
if (sampleCompatRate) {
|
||||
sample->rate=targetRate;
|
||||
} else {
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
|
|
@ -818,11 +801,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
if (targetRate<100) targetRate=100;
|
||||
if (targetRate>384000) targetRate=384000;
|
||||
|
||||
if (sampleCompatRate) {
|
||||
sample->rate=targetRate;
|
||||
} else {
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
|
|
@ -852,11 +831,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
if (targetRate<100) targetRate=100;
|
||||
if (targetRate>384000) targetRate=384000;
|
||||
|
||||
if (sampleCompatRate) {
|
||||
sample->rate=targetRate;
|
||||
} else {
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
sample->centerRate=targetRate;
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
|
|
@ -1662,7 +1637,7 @@ void FurnaceGUI::drawSampleEdit() {
|
|||
|
||||
ImGui::ItemSize(size,style.FramePadding.y);
|
||||
if (ImGui::ItemAdd(rect,ImGui::GetID("SETime"))) {
|
||||
int targetRate=sampleCompatRate?sample->rate:sample->centerRate;
|
||||
int targetRate=sample->centerRate;
|
||||
int curDivisorSel=0;
|
||||
int curMultiplierSel=0;
|
||||
double divisor=1000.0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue