/** * 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 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.readStringLatin1(20); logI("%s",ds.name); // samples logD("reading samples... (%d)",insCount); ds.sample.reserve(insCount); for (int i=0; idepth=DIV_SAMPLE_DEPTH_8BIT; sample->name=reader.readStringLatin1(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; jorders.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; chpat[ch].getPattern(pat,true); } for (int row=0; row<64; row++) { for (int ch=0; chdata[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+11)%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; isamples,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; i1 || bypassLimits)); } for(int i=0; ichanShow[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; ipat[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; itype=DIV_INS_AMIGA; ins->amiga.initSample=i; ins->name=ds.sample[i]->name; ds.ins.push_back(ins); } ds.insLen=ds.ins.size(); // find subsongs ds.findSubSongs(chCount); 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; }