diff --git a/src/engine/engine.h b/src/engine/engine.h index b76a3e5b8..7fc2a110c 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -58,6 +58,7 @@ #define DIV_VERSION_MOD 0xff01 #define DIV_VERSION_FC 0xff02 #define DIV_VERSION_S3M 0xff03 +#define DIV_VERSION_FTM 0xff04 // "Namco C163" #define DIV_C163_DEFAULT_NAME "Namco 163" diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 41e2f6ffc..e1ae97b13 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -4061,12 +4061,58 @@ bool DivEngine::loadFC(unsigned char* file, size_t len) { #define CHECK_BLOCK_VERSION(x) \ if (blockVersion>x) { \ - logE("incompatible block version %d for %s!",blockVersion,blockName); \ - lastError="incompatible block version"; \ - delete[] file; \ - return false; \ + logW("incompatible block version %d for %s!",blockVersion,blockName); \ } +const int ftEffectMap[]={ + -1, // none + 0x0f, + 0x0b, + 0x0d, + 0xff, + -1, // volume? not supported in Furnace yet + 0x03, + 0x03, // unused? + 0x13, + 0x14, + 0x00, + 0x04, + 0x07, + 0xe5, + 0xed, + 0x11, + 0x01, // porta up + 0x02, // porta down + 0x12, + 0x90, // sample offset - not supported yet + 0xe1, + 0xe2, + 0x0a, + 0xec, + 0x0c, + -1, // delayed volume - not supported yet + 0x11, // FDS + 0x12, + 0x13, + 0x20, // DPCM pitch + 0x22, // 5B + 0x24, + 0x23, + 0x21, + -1, // VRC7 "custom patch port" - not supported? + -1, // VRC7 "custom patch write" + -1, // release - not supported yet + 0x09, // select groove + -1, // transpose - not supported + 0x10, // Namco 163 + -1, // FDS vol env - not supported + -1, // FDS auto FM - not supported yet + -1, // phase reset - not supported + -1, // harmonic - not supported +}; + +constexpr int ftEffectMapSize=sizeof(ftEffectMap)/sizeof(int); + bool DivEngine::loadFTM(unsigned char* file, size_t len) { SafeReader reader=SafeReader(file,len); warnings=""; @@ -4078,6 +4124,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { unsigned int n163Chans=0; bool hasSequence[256][8]; unsigned char sequenceIndex[256][8]; + unsigned int hilightA=4; + unsigned int hilightB=16; + double customHz=60; memset(hasSequence,0,256*8*sizeof(bool)); memset(sequenceIndex,0,256*8); @@ -4098,6 +4147,14 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { return false; } + for (DivSubSong* i: ds.subsong) { + i->clearData(); + delete i; + } + ds.subsong.clear(); + + ds.linearPitch=0; + while (true) { blockName=reader.readString(3); if (blockName=="END") { @@ -4115,7 +4172,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { logD("reading block %s (version %d, %d bytes)",blockName,blockVersion,blockSize); if (blockName=="PARAMS") { - CHECK_BLOCK_VERSION(6); + // versions 7-9 don't change anything? + CHECK_BLOCK_VERSION(9); unsigned int oldSpeedTempo=0; if (blockVersion<=1) { oldSpeedTempo=reader.readI(); @@ -4125,15 +4183,32 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { } tchans=reader.readI(); unsigned int pal=reader.readI(); - unsigned int customHz=reader.readI(); + if (blockVersion>=7) { + // advanced Hz control + int controlType=reader.readI(); + switch (controlType) { + case 1: + customHz=1000000.0/(double)reader.readI(); + break; + default: + reader.readI(); + break; + } + } else { + customHz=reader.readI(); + } unsigned int newVibrato=0; + bool sweepReset=false; unsigned int speedSplitPoint=0; if (blockVersion>=3) { newVibrato=reader.readI(); } - if (blockVersion>=4) { - ds.subsong[0]->hilightA=reader.readI(); - ds.subsong[0]->hilightB=reader.readI(); + if (blockVersion>=9) { + sweepReset=reader.readI(); + } + if (blockVersion>=4 && blockVersion<7) { + hilightA=reader.readI(); + hilightB=reader.readI(); } if (expansions&8) if (blockVersion>=5) { // N163 channels n163Chans=reader.readI(); @@ -4142,20 +4217,24 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { speedSplitPoint=reader.readI(); } + if (blockVersion>=8) { + int fineTuneCents=reader.readC()*100; + fineTuneCents+=reader.readC(); + + ds.tuning=440.0*pow(2.0,(double)fineTuneCents/1200.0); + } + logV("old speed/tempo: %d",oldSpeedTempo); logV("expansions: %x",expansions); logV("channels: %d",tchans); logV("PAL: %d",pal); - logV("custom Hz: %d",customHz); + logV("custom Hz: %f",customHz); logV("new vibrato: %d",newVibrato); logV("N163 channels: %d",n163Chans); - logV("highlight 1: %d",ds.subsong[0]->hilightA); - logV("highlight 2: %d",ds.subsong[0]->hilightB); + logV("highlight 1: %d",hilightA); + logV("highlight 2: %d",hilightB); logV("split point: %d",speedSplitPoint); - - if (customHz!=0) { - ds.subsong[0]->hz=customHz; - } + logV("sweep reset: %d",sweepReset); // initialize channels int systemID=0; @@ -4200,28 +4279,46 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { CHECK_BLOCK_VERSION(1); ds.name=reader.readString(32); ds.author=reader.readString(32); - ds.copyright=reader.readString(32); + ds.category=reader.readString(32); + ds.systemName="NES"; } else if (blockName=="HEADER") { - CHECK_BLOCK_VERSION(3); + CHECK_BLOCK_VERSION(4); unsigned char totalSongs=reader.readC(); logV("%d songs:",totalSongs+1); for (int i=0; i<=totalSongs; i++) { String subSongName=reader.readString(); + ds.subsong.push_back(new DivSubSong); + ds.subsong[i]->name=subSongName; + ds.subsong[i]->hilightA=hilightA; + ds.subsong[i]->hilightB=hilightB; + if (customHz!=0) { + ds.subsong[i]->hz=customHz; + } logV("- %s",subSongName); } for (unsigned int i=0; ipat[i].effectCols=effectCols+1; - } + ds.subsong[j]->pat[i].effectCols=effectCols+1; logV("- song %d has %d effect columns",j,effectCols); } } + + if (blockVersion>=4) { + for (int i=0; i<=totalSongs; i++) { + ds.subsong[i]->hilightA=(unsigned char)reader.readC(); + ds.subsong[i]->hilightB=(unsigned char)reader.readC(); + } + } } else if (blockName=="INSTRUMENTS") { CHECK_BLOCK_VERSION(6); + + reader.seek(blockSize,SEEK_CUR); + + /* ds.insLen=reader.readI(); if (ds.insLen<0 || ds.insLen>256) { logE("too many instruments/out of range!"); @@ -4381,21 +4478,131 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { ins->name=reader.readString((unsigned int)reader.readI()); logV("- %d: %s",insIndex,ins->name); } + */ } else if (blockName=="SEQUENCES") { CHECK_BLOCK_VERSION(6); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="FRAMES") { CHECK_BLOCK_VERSION(3); + + for (size_t i=0; iordersLen=reader.readI(); + if (blockVersion>=3) { + s->speeds.val[0]=reader.readI(); + } + if (blockVersion>=2) { + s->virtualTempoN=reader.readI(); + s->patLen=reader.readI(); + } + int why=tchans; + if (blockVersion==1) { + why=reader.readI(); + } + logV("reading %d and %d orders",tchans,s->ordersLen); + + for (int j=0; jordersLen; j++) { + for (int k=0; korders.ord[k][j]=o; + } + } + } } else if (blockName=="PATTERNS") { - CHECK_BLOCK_VERSION(5); + CHECK_BLOCK_VERSION(6); + + size_t blockEnd=reader.tell()+blockSize; + + if (blockVersion==1) { + int patLenOld=reader.readI(); + for (DivSubSong* i: ds.subsong) { + i->patLen=patLenOld; + } + } + + // so it appears .ftm doesn't keep track of how many patterns are stored in the file.... + while (reader.tell()=2) subs=reader.readI(); + int ch=reader.readI(); + int patNum=reader.readI(); + int numRows=reader.readI(); + + DivPattern* pat=ds.subsong[subs]->pat[ch].getPattern(patNum,true); + for (int i=0; i=2 && blockVersion<6) { // row index + row=reader.readI(); + } else { + row=reader.readC(); + } + + unsigned char nextNote=reader.readC(); + unsigned char nextOctave=reader.readC(); + if (nextNote==0x0d) { + pat->data[row][0]=100; + } else if (nextNote==0x0e) { + pat->data[row][0]=101; + } else if (nextNote==0x01) { + pat->data[row][0]=12; + pat->data[row][1]=nextOctave-1; + } else if (nextNote==0) { + pat->data[row][0]=0; + } else if (nextNote<0x0d) { + pat->data[row][0]=nextNote-1; + pat->data[row][1]=nextOctave; + } + + unsigned char nextIns=reader.readC(); + if (nextIns<0x40) { + pat->data[row][2]=nextIns; + } else { + pat->data[row][2]=-1; + } + + unsigned char nextVol=reader.readC(); + if (nextVol<0x10) { + pat->data[row][3]=nextVol; + } else { + pat->data[row][3]=-1; + } + + int effectCols=ds.subsong[subs]->pat[ch].effectCols; + if (blockVersion>=6) effectCols=4; + + for (int j=0; jdata[row][4+(j*2)]=-1; + pat->data[row][5+(j*2)]=-1; + } else { + if (nextEffectdata[row][4+(j*2)]=ftEffectMap[nextEffect]; + } else { + pat->data[row][4+(j*2)]=-1; + } + pat->data[row][5+(j*2)]=nextEffectVal; + } + } + } + } } else if (blockName=="DPCM SAMPLES") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="SEQUENCES_VRC6") { // where are the 5B and FDS sequences? CHECK_BLOCK_VERSION(6); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="SEQUENCES_N163") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else if (blockName=="COMMENTS") { CHECK_BLOCK_VERSION(1); + reader.seek(blockSize,SEEK_CUR); } else { logE("block %s is unknown!",blockName); lastError="unknown block "+blockName; @@ -4410,6 +4617,27 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) { return false; } } + + addWarning("FamiTracker import is experimental!"); + + ds.version=DIV_VERSION_FTM; + + 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; + } } catch (EndOfFileException& e) { logE("premature end of file!"); lastError="incomplete file"; diff --git a/src/engine/safeReader.cpp b/src/engine/safeReader.cpp index cf2effbec..5ac416e27 100644 --- a/src/engine/safeReader.cpp +++ b/src/engine/safeReader.cpp @@ -236,10 +236,14 @@ String SafeReader::readString(size_t stlen) { #endif size_t curPos=0; if (isEOF()) throw EndOfFileException(this, len); + bool zero=false; while (!isEOF() && curPos