448 lines
14 KiB
C++
448 lines
14 KiB
C++
/**
|
|
* Furnace Tracker - multi-system chiptune tracker
|
|
* Copyright (C) 2021-2024 tildearrow and contributors
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "fileOpsCommon.h"
|
|
|
|
bool DivEngine::loadMod(unsigned char* file, size_t len) {
|
|
struct InvalidHeaderException {};
|
|
bool success=false;
|
|
int chCount=0;
|
|
int ordCount=0;
|
|
std::vector<int> patPtr;
|
|
char magic[4]={0,0,0,0};
|
|
short defaultVols[31];
|
|
int sampLens[31];
|
|
// 0=arp, 1=pslide, 2=vib, 3=trem, 4=vslide
|
|
bool fxUsage[DIV_MAX_CHANS][5];
|
|
SafeReader reader=SafeReader(file,len);
|
|
warnings="";
|
|
|
|
memset(defaultVols,0,31*sizeof(short));
|
|
memset(sampLens,0,31*sizeof(int));
|
|
memset(fxUsage,0,DIV_MAX_CHANS*5*sizeof(bool));
|
|
|
|
try {
|
|
DivSong ds;
|
|
ds.tuning=436.0;
|
|
ds.version=DIV_VERSION_MOD;
|
|
ds.linearPitch=0;
|
|
ds.noSlidesOnFirstTick=true;
|
|
ds.rowResetsArpPos=true;
|
|
ds.ignoreJumpAtEnd=false;
|
|
ds.delayBehavior=0;
|
|
|
|
int insCount=31;
|
|
bool bypassLimits=false;
|
|
|
|
// check mod magic bytes
|
|
if (!reader.seek(1080,SEEK_SET)) {
|
|
logD("couldn't seek to 1080");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
if (reader.read(magic,4)!=4) {
|
|
logD("the magic isn't complete");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
if (memcmp(magic,"M.K.",4)==0 || memcmp(magic,"M!K!",4)==0 || memcmp(magic,"M&K!",4)==0) {
|
|
logD("detected a ProTracker module");
|
|
ds.systemName="Amiga";
|
|
chCount=4;
|
|
} else if (memcmp(magic,"CD81",4)==0 || memcmp(magic,"OKTA",4)==0 || memcmp(magic,"OCTA",4)==0) {
|
|
logD("detected an Oktalyzer/Octalyzer/OctaMED module");
|
|
ds.systemName="Amiga (8-channel)";
|
|
chCount=8;
|
|
} else if (memcmp(magic+1,"CHN",3)==0 && magic[0]>='1' && magic[0]<='9') {
|
|
logD("detected a FastTracker module");
|
|
ds.systemName="PC";
|
|
chCount=magic[0]-'0';
|
|
} else if (memcmp(magic,"FLT",3)==0 && magic[3]>='1' && magic[3]<='9') {
|
|
logD("detected a Fairlight module");
|
|
ds.systemName="Amiga";
|
|
chCount=magic[3]-'0';
|
|
} else if (memcmp(magic,"TDZ",3)==0 && magic[3]>='1' && magic[3]<='9') {
|
|
logD("detected a TakeTracker module");
|
|
ds.systemName="PC";
|
|
chCount=magic[3]-'0';
|
|
} else if ((memcmp(magic+2,"CH",2)==0 || memcmp(magic+2,"CN",2)==0) &&
|
|
(magic[0]>='1' && magic[0]<='9' && magic[1]>='0' && magic[1]<='9')) {
|
|
logD("detected a Fast/TakeTracker module");
|
|
ds.systemName="PC";
|
|
chCount=((magic[0]-'0')*10)+(magic[1]-'0');
|
|
} else {
|
|
insCount=15;
|
|
logD("possibly a Soundtracker module");
|
|
ds.systemName="Amiga";
|
|
chCount=4;
|
|
}
|
|
|
|
// song name
|
|
if (!reader.seek(0,SEEK_SET)) {
|
|
logD("couldn't seek to 0");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
ds.name=reader.readString(20);
|
|
logI("%s",ds.name);
|
|
|
|
// samples
|
|
logD("reading samples... (%d)",insCount);
|
|
ds.sample.reserve(insCount);
|
|
for (int i=0; i<insCount; i++) {
|
|
DivSample* sample=new DivSample;
|
|
sample->depth=DIV_SAMPLE_DEPTH_8BIT;
|
|
sample->name=reader.readString(22);
|
|
logD("%d: %s",i+1,sample->name);
|
|
int slen=((unsigned short)reader.readS_BE())*2;
|
|
sampLens[i]=slen;
|
|
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;
|
|
defaultVols[i]=reader.readC();
|
|
int loopStart=reader.readS_BE()*2;
|
|
int loopLen=reader.readS_BE()*2;
|
|
int loopEnd=loopStart+loopLen;
|
|
// bunch of checks since ProTracker abuses those for one-shot samples
|
|
if (loopStart>loopEnd || loopEnd<4 || loopLen<4) {
|
|
loopStart=0;
|
|
loopLen=0;
|
|
}
|
|
if (loopLen>=2) {
|
|
sample->loopStart=loopStart;
|
|
sample->loopEnd=loopEnd;
|
|
sample->loop=(sample->loopStart>=0)&&(sample->loopEnd>=0);
|
|
}
|
|
sample->init(slen);
|
|
ds.sample.push_back(sample);
|
|
}
|
|
ds.sampleLen=ds.sample.size();
|
|
|
|
// orders
|
|
ds.subsong[0]->ordersLen=ordCount=(unsigned char)reader.readC();
|
|
if (ds.subsong[0]->ordersLen<1 || ds.subsong[0]->ordersLen>128) {
|
|
logD("invalid order count!");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
unsigned char restartPos=reader.readC(); // restart position, unused
|
|
logD("restart position byte: %.2x",restartPos);
|
|
if (insCount==15) {
|
|
if (restartPos>0x60 && restartPos<0x80) {
|
|
logD("detected a Soundtracker module");
|
|
} else {
|
|
logD("no Soundtracker signature found");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
}
|
|
|
|
int patMax=0;
|
|
for (int i=0; i<128; i++) {
|
|
unsigned char pat=reader.readC();
|
|
if (pat>patMax) patMax=pat;
|
|
for (int j=0; j<chCount; j++) {
|
|
ds.subsong[0]->orders.ord[j][i]=pat;
|
|
}
|
|
}
|
|
|
|
if (insCount==15) {
|
|
if (!reader.seek(600,SEEK_SET)) {
|
|
logD("couldn't seek to 600");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
} else {
|
|
if (!reader.seek(1084,SEEK_SET)) {
|
|
logD("couldn't seek to 1084");
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
}
|
|
|
|
// patterns
|
|
ds.subsong[0]->patLen=64;
|
|
for (int ch=0; ch<chCount; ch++) {
|
|
for (int i=0; i<5; i++) {
|
|
fxUsage[ch][i]=false;
|
|
}
|
|
}
|
|
for (int pat=0; pat<=patMax; pat++) {
|
|
DivPattern* chpats[DIV_MAX_CHANS];
|
|
for (int ch=0; ch<chCount; ch++) {
|
|
chpats[ch]=ds.subsong[0]->pat[ch].getPattern(pat,true);
|
|
}
|
|
for (int row=0; row<64; row++) {
|
|
for (int ch=0; ch<chCount; ch++) {
|
|
short* dstrow=chpats[ch]->data[row];
|
|
unsigned char data[4];
|
|
reader.read(&data,4);
|
|
// instrument
|
|
short ins=(data[0]&0xf0)|(data[2]>>4);
|
|
if (ins>0) {
|
|
dstrow[2]=ins-1;
|
|
dstrow[3]=defaultVols[ins-1];
|
|
}
|
|
// note
|
|
int period=data[1]+((data[0]&0x0f)<<8);
|
|
if (period>0 && period<0x0fff) {
|
|
short note=(short)round(log2(3424.0/period)*12);
|
|
dstrow[0]=((note-1)%12)+1;
|
|
dstrow[1]=(note-1)/12+1;
|
|
if (period<114) {
|
|
bypassLimits=true;
|
|
}
|
|
}
|
|
// effects are done later
|
|
short fxtyp=data[2]&0x0f;
|
|
short fxval=data[3];
|
|
dstrow[4]=fxtyp;
|
|
dstrow[5]=fxval;
|
|
switch (fxtyp) {
|
|
case 0:
|
|
if (fxval!=0) fxUsage[ch][0]=true;
|
|
break;
|
|
case 1: case 2: case 3:
|
|
fxUsage[ch][1]=true;
|
|
break;
|
|
case 4:
|
|
fxUsage[ch][2]=true;
|
|
break;
|
|
case 5:
|
|
fxUsage[ch][1]=true;
|
|
fxUsage[ch][4]=true;
|
|
break;
|
|
case 6:
|
|
fxUsage[ch][2]=true;
|
|
fxUsage[ch][4]=true;
|
|
break;
|
|
case 7:
|
|
fxUsage[ch][3]=true;
|
|
break;
|
|
case 10:
|
|
if (fxval!=0) fxUsage[ch][4]=true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// samples
|
|
size_t pos=reader.tell();
|
|
logD("reading sample data...");
|
|
for (int i=0; i<insCount; i++) {
|
|
logV("- %d: %d %d %d",i,pos,ds.sample[i]->samples,sampLens[i]);
|
|
if (!reader.seek(pos,SEEK_SET)) {
|
|
logD("%d: couldn't seek to %d",i,pos);
|
|
throw EndOfFileException(&reader,reader.tell());
|
|
}
|
|
reader.read(ds.sample[i]->data8,ds.sample[i]->samples);
|
|
pos+=sampLens[i];
|
|
}
|
|
|
|
// convert effects
|
|
logD("converting module...");
|
|
for (int ch=0; ch<=chCount; ch++) {
|
|
unsigned char fxCols=1;
|
|
for (int pat=0; pat<=patMax; pat++) {
|
|
auto* data=ds.subsong[0]->pat[ch].getPattern(pat,true)->data;
|
|
short lastPitchEffect=-1;
|
|
short lastEffectState[5]={-1,-1,-1,-1,-1};
|
|
short setEffectState[5]={-1,-1,-1,-1,-1};
|
|
for (int row=0;row<64;row++) {
|
|
const short fxUsageTyp[5]={0x00,0x01,0x04,0x07,0xFA};
|
|
short effectState[5]={0,0,0,0,0};
|
|
unsigned char curFxCol=0;
|
|
short fxTyp=data[row][4];
|
|
short fxVal=data[row][5];
|
|
auto writeFxCol=[data,row,&curFxCol](short typ, short val) {
|
|
data[row][4+curFxCol*2]=typ;
|
|
data[row][5+curFxCol*2]=val;
|
|
curFxCol++;
|
|
};
|
|
writeFxCol(-1,-1);
|
|
curFxCol=0;
|
|
switch (fxTyp) {
|
|
case 0: // arp
|
|
effectState[0]=fxVal;
|
|
break;
|
|
case 5: // vol slide + porta
|
|
effectState[4]=fxVal;
|
|
fxTyp=3;
|
|
fxVal=0;
|
|
// fall through
|
|
case 1: // note slide up
|
|
case 2: // note slide down
|
|
case 3: // porta
|
|
if (fxTyp==3 && fxVal==0) {
|
|
if (setEffectState[1]<0) break;
|
|
fxVal=setEffectState[1];
|
|
}
|
|
setEffectState[1]=fxVal;
|
|
effectState[1]=fxVal;
|
|
if ((effectState[1]!=lastEffectState[1]) ||
|
|
(fxTyp!=lastPitchEffect) ||
|
|
(effectState[1]!=0 && data[row][0]>0)) {
|
|
writeFxCol(fxTyp,fxVal);
|
|
}
|
|
lastPitchEffect=fxTyp;
|
|
lastEffectState[1]=fxVal;
|
|
break;
|
|
case 6: // vol slide + vibrato
|
|
effectState[4]=fxVal;
|
|
fxTyp=4;
|
|
fxVal=0;
|
|
// fall through
|
|
case 4: // vibrato
|
|
// TODO: handle 0 value?
|
|
if (fxVal==0) {
|
|
if (setEffectState[2]<0) break;
|
|
fxVal=setEffectState[2];
|
|
}
|
|
effectState[2]=fxVal;
|
|
setEffectState[2]=fxVal;
|
|
break;
|
|
case 7: // tremolo
|
|
if (fxVal==0) {
|
|
if (setEffectState[3]<0) break;
|
|
fxVal=setEffectState[3];
|
|
}
|
|
effectState[3]=fxVal;
|
|
setEffectState[3]=fxVal;
|
|
break;
|
|
case 9: // set offset
|
|
writeFxCol(0x91,fxVal);
|
|
break;
|
|
case 10: // vol slide
|
|
effectState[4]=fxVal;
|
|
break;
|
|
case 11: // jump to pos
|
|
writeFxCol(fxTyp,fxVal);
|
|
break;
|
|
case 12: // set vol
|
|
data[row][3]=MIN(0x40,fxVal);
|
|
break;
|
|
case 13: // break to row (BCD)
|
|
writeFxCol(fxTyp,((fxVal>>4)*10)+(fxVal&15));
|
|
break;
|
|
case 15: // set speed
|
|
// TODO: somehow handle VBlank tunes
|
|
// TODO: i am so sorry
|
|
if (fxVal>0x20 && ds.name!="klisje paa klisje") {
|
|
writeFxCol(0xf0,fxVal);
|
|
} else {
|
|
writeFxCol(0x0f,fxVal);
|
|
}
|
|
break;
|
|
case 14: // extended
|
|
fxTyp=fxVal>>4;
|
|
fxVal&=0x0f;
|
|
switch (fxTyp) {
|
|
case 0:
|
|
writeFxCol(0x10,!fxVal);
|
|
break;
|
|
case 1: // single note slide up
|
|
case 2: // single note slide down
|
|
writeFxCol(fxTyp-1+0xf1,fxVal);
|
|
break;
|
|
case 9: // retrigger
|
|
writeFxCol(0x0c,fxVal);
|
|
break;
|
|
case 10: // single vol slide up
|
|
case 11: // single vol slide down
|
|
writeFxCol(fxTyp-10+0xf8,fxVal);
|
|
break;
|
|
case 12: // note cut
|
|
case 13: // note delay
|
|
writeFxCol(fxTyp-12+0xec,fxVal);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
for (int i=0; i<5; i++) {
|
|
// pitch slide and volume slide needs to be kept active on new note
|
|
// even after target/max is reached
|
|
if (fxUsage[ch][i] && (effectState[i]!=lastEffectState[i] || (effectState[i]!=0 && i==4 && data[row][3]>=0))) {
|
|
writeFxCol(fxUsageTyp[i],effectState[i]);
|
|
}
|
|
}
|
|
memcpy(lastEffectState,effectState,sizeof(effectState));
|
|
if (curFxCol>fxCols) {
|
|
fxCols=curFxCol;
|
|
}
|
|
}
|
|
}
|
|
ds.subsong[0]->pat[ch].effectCols=fxCols;
|
|
}
|
|
|
|
ds.subsong[0]->hz=50;
|
|
ds.systemLen=(chCount+3)/4;
|
|
for(int i=0; i<ds.systemLen; i++) {
|
|
ds.system[i]=DIV_SYSTEM_AMIGA;
|
|
ds.systemFlags[i].set("clockSel",1); // PAL
|
|
ds.systemFlags[i].set("stereoSep",80);
|
|
ds.systemFlags[i].set("bypassLimits",bypassLimits);
|
|
ds.systemFlags[i].set("chipType",(bool)(ds.systemLen>1 || bypassLimits));
|
|
}
|
|
for(int i=0; i<chCount; i++) {
|
|
ds.subsong[0]->chanShow[i]=true;
|
|
ds.subsong[0]->chanShowChanOsc[i]=true;
|
|
ds.subsong[0]->chanName[i]=fmt::sprintf("Channel %d",i+1);
|
|
ds.subsong[0]->chanShortName[i]=fmt::sprintf("C%d",i+1);
|
|
}
|
|
for(int i=chCount; i<ds.systemLen*4; i++) {
|
|
ds.subsong[0]->pat[i].effectCols=1;
|
|
ds.subsong[0]->chanShow[i]=false;
|
|
ds.subsong[0]->chanShowChanOsc[i]=false;
|
|
}
|
|
|
|
// instrument creation
|
|
ds.ins.reserve(insCount);
|
|
for(int i=0; i<insCount; i++) {
|
|
DivInstrument* ins=new DivInstrument;
|
|
ins->type=DIV_INS_AMIGA;
|
|
ins->amiga.initSample=i;
|
|
ins->name=ds.sample[i]->name;
|
|
ds.ins.push_back(ins);
|
|
}
|
|
ds.insLen=ds.ins.size();
|
|
|
|
if (active) quitDispatch();
|
|
BUSY_BEGIN_SOFT;
|
|
saveLock.lock();
|
|
song.unload();
|
|
song=ds;
|
|
changeSong(0);
|
|
recalcChans();
|
|
saveLock.unlock();
|
|
BUSY_END;
|
|
if (active) {
|
|
initDispatch();
|
|
BUSY_BEGIN;
|
|
renderSamples();
|
|
reset();
|
|
BUSY_END;
|
|
}
|
|
success=true;
|
|
} catch (EndOfFileException& e) {
|
|
//logE("premature end of file!");
|
|
lastError="incomplete file";
|
|
} catch (InvalidHeaderException& e) {
|
|
//logE("invalid info header!");
|
|
lastError="invalid info header!";
|
|
}
|
|
return success;
|
|
}
|
|
|