diff --git a/CMakeLists.txt b/CMakeLists.txt index 8248a71bb..55ba66f91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -653,6 +653,7 @@ src/engine/fileOps/fur.cpp src/engine/fileOps/mod.cpp src/engine/fileOps/s3m.cpp src/engine/fileOps/text.cpp +src/engine/fileOps/tfm.cpp src/engine/blip_buf.c src/engine/brrUtils.c diff --git a/src/engine/engine.h b/src/engine/engine.h index 72ce8d266..fe1673403 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -556,6 +556,8 @@ class DivEngine { bool loadS3M(unsigned char* file, size_t len); bool loadFTM(unsigned char* file, size_t len, bool dnft, bool dnftSig, bool eft); bool loadFC(unsigned char* file, size_t len); + bool loadTFMv1(unsigned char* file, size_t len); + bool loadTFMv2(unsigned char* file, size_t len); void loadDMP(SafeReader& reader, std::vector& ret, String& stripPath); void loadTFI(SafeReader& reader, std::vector& ret, String& stripPath); diff --git a/src/engine/fileOps/fileOpsCommon.cpp b/src/engine/fileOps/fileOpsCommon.cpp index 52e1d31be..bc470bae3 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -152,10 +152,14 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { return loadFur(file,len,DIV_FUR_VARIANT_B); } else if (memcmp(file,DIV_FC13_MAGIC,4)==0 || memcmp(file,DIV_FC14_MAGIC,4)==0) { return loadFC(file,len); + } else if (memcmp(file,DIV_TFM_MAGIC,8)==0) { + return loadTFMv2(file,len); } - // step 3: try loading as .mod - if (loadMod(file,len)) { + // step 3: try loading as .mod or TFEv1 (if the file extension matches) + if (extS==".tfe") { + return loadTFMv1(file,len); + } else if (loadMod(file,len)) { delete[] f; return true; } diff --git a/src/engine/fileOps/fileOpsCommon.h b/src/engine/fileOps/fileOpsCommon.h index 1ba6eff8a..dc705cfdf 100644 --- a/src/engine/fileOps/fileOpsCommon.h +++ b/src/engine/fileOps/fileOpsCommon.h @@ -53,6 +53,7 @@ struct NotZlibException { #define DIV_FC13_MAGIC "SMOD" #define DIV_FC14_MAGIC "FC14" #define DIV_S3M_MAGIC "SCRM" +#define DIV_TFM_MAGIC "TFMfmtV2" #define DIV_FUR_MAGIC_DS0 "Furnace-B module" diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp new file mode 100644 index 000000000..b4535c762 --- /dev/null +++ b/src/engine/fileOps/tfm.cpp @@ -0,0 +1,901 @@ +/** + * 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" + +class TFMRLEReader; + +struct TFMEndOfFileException { + TFMRLEReader* reader; + size_t finalSize; + TFMEndOfFileException(TFMRLEReader* r, size_t fs): + reader(r), + finalSize(fs) {} +}; + + +class TFMRLEReader { + const unsigned char* buf; + size_t len; + size_t curSeek; + bool inTag; + int tagLenLeft; + signed char tagChar; + + void decodeRLE(unsigned char prevChar) { + int lenShift=0; + tagLenLeft=0; + unsigned char rleTag=0; + + do { + rleTag=readCNoRLE(); + tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); + + unsigned char ret=buf[curSeek++]; + + // MISLEADING DOCUMENTATION: while TFM music maker's documentation says if the next byte + // is zero, then it's not a tag but just 0x80 (for example: 0x00 0x80 0x00 = 0x00 0x80) + // this is actually wrong + // through research and experimentation, there are times that TFM music maker + // will use 0x80 0x00 for actual tags (for example: 0x00 0x80 0x00 0x84 = 512 times 0x00 + // in certain parts of the header and footer) + // TFM music maker actually uses double 0x80 to escape the 0x80 + // for example: 0xDA 0x80 0x80 0x00 0x23 = 0xDA 0x80 0x00 0x23) + if (ret==0x80) { + decodeRLE(buf[curSeek-2]); + tagLenLeft--; + return tagChar; + } + return ret; + } + + signed char readCNoRLE() { + if (curSeek>len) throw TFMEndOfFileException(this,len); + return buf[curSeek++]; + } + + void read(unsigned char* b, size_t l) { + int i=0; + while(l--) { + unsigned char nextChar=readC(); + b[i++]=nextChar; + logD("read next char: %x, index: %d",nextChar,i); + } + } + + void readNoRLE(unsigned char *b, size_t l) { + int i=0; + while (l--) { + b[i++]=buf[curSeek++]; + if (curSeek>len) throw TFMEndOfFileException(this,len); + } + } + + short readS() { + return readC()|readC()<<8; + } + + short readSNoRLE() { + if (curSeek+2>len) throw TFMEndOfFileException(this,len); + short ret=buf[curSeek]|buf[curSeek+1]<<8; + curSeek+=2; + return ret; + } + + String readString(size_t l) { + String ret; + ret.reserve(l); + while (l--) { + unsigned char byte=readC(); + if (!byte) { + skip(l); + break; + } + ret += byte; + } + return ret; + } + void skip(size_t l) { + // quick and dirty + while (l--) { + logD("skipping l %d",l); + readC(); + } + } + +}; + +String TFMparseDate(short date) { + return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); +} + +struct TFMSpeed { + unsigned char speedEven; + unsigned char speedOdd; + unsigned char interleaveFactor; + + bool operator==(const TFMSpeed &s) const { + return speedEven==s.speedEven && speedOdd==s.speedOdd && interleaveFactor==s.interleaveFactor; + } +}; + +// to make it work with map +template<> +struct std::hash +{ + size_t operator()(const TFMSpeed& s) const noexcept { + return s.speedEven<<16|s.speedOdd<<8|s.interleaveFactor; + } +}; + +struct TFMParsePatternInfo { + TFMRLEReader* reader; + unsigned char maxPat; + unsigned char* patLens; + unsigned char* orderList; + unsigned char speedEven; + unsigned char speedOdd; + unsigned char interleaveFactor; + bool* patExists; + DivSong* ds; + int* insNumMaps; + bool v2; +}; + +void TFMParsePattern(struct TFMParsePatternInfo info) { + // PATTERN DATA FORMAT (not described properly in the documentation) + // for each channel in a pattern: + // - note data (256 bytes) + // - volume data (256 bytes, values always 0x00-0x1F) + // - instrument number data (256 bytes) + // - effect number (256 bytes, values 0x0-0x23 (to represent 0-F and G-Z)) + // - effect value (256 bytes) + // - extra 3 effects (1536 bytes 256x3x2) (ONLY ON V2) + // notes are stored as an inverted value of note+octave*12 + // key-offs are stored in the note data as 0x01 + unsigned char patDataBuf[256]; + unsigned short lastSlide=0; + unsigned short lastVibrato=0; + + struct TFMSpeed speed; + DivGroovePattern groove; + speed.speedEven=info.speedEven; + speed.speedOdd=info.speedOdd; + speed.interleaveFactor=info.interleaveFactor; + int speedGrooveIndex=1; + + int usedEffectsCol=0; + std::unordered_map speeds({{speed, 0}}); + + // initialize the global groove pattern first + if (speed.interleaveFactor>8) { + logW("speed interleave factor is bigger than 8, speed information may be inaccurate"); + speed.interleaveFactor=8; + } + for (int i=0; igrooves.push_back(groove); + + for (int i=0; i<256; i++) { + if (i>info.maxPat) break; + else if (!info.patExists[i]) { + logD("skipping pattern %d",i); + info.reader->skip((info.v2) ? 16896 : 7680); + continue; + } + + logD("parsing pattern %d",i); + for (int j=0; j<6; j++) { + DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; + + // notes + info.reader->read(patDataBuf,256); + + logD("parsing notes of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + else if (patDataBuf[k]==1) { + // note off + pat->data[k][0]=100; + } else { + unsigned char invertedNote=~patDataBuf[k]; + pat->data[k][0]=invertedNote%12; + pat->data[k][1]=(invertedNote/12)-1; + + if (pat->data[k][0]==0) { + pat->data[k][0]=12; + pat->data[k][1]--; + } + } + } + + // volume + info.reader->read(patDataBuf,256); + + logD("parsing volumes of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + else pat->data[k][3]=0x60+patDataBuf[k]; + } + + // instrument + info.reader->read(patDataBuf,256); + + logD("parsing instruments of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + pat->data[k][2]=info.insNumMaps[patDataBuf[k]-1]; + } + + // effects + + int numEffectsCol=(info.v2) ? 4 : 1; + for (int l=0; lread(effectNum,256); + info.reader->read(effectVal,256); + + for (int k=0; k<256; k++) { + if (effectNum[k] || effectVal[k]) usedEffectsCol=l+1; + switch (effectNum[k]) { + case 0: + // arpeggio or no effect (if effect val is 0) + if (effectVal[k]==0) break; + pat->data[k][4+(l*2)]=effectNum[k]; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 1: + // pitch slide up + case 2: + // pitch slide down + pat->data[k][4+(l*2)]=effectNum[k]; + if (effectVal[k]) { + lastSlide=effectVal[k]; + pat->data[k][5+(l*2)]=effectVal[k]; + } else { + pat->data[k][5+(l*2)]=lastSlide; + } + break; + case 3: + // portamento + case 4: + // vibrato + pat->data[k][5+(l*2)]=0; + if (effectVal[k]&0xF0) { + pat->data[k][5+(l*2)]|=effectVal[k]&0xF0; + } else { + pat->data[k][5+(l*2)]|=lastVibrato&0xF0; + } + if (effectVal[k]&0x0F) { + pat->data[k][5+(l*2)]|=effectVal[k]&0x0F; + } else { + pat->data[k][5+(l*2)]|=lastVibrato&0x0F; + } + pat->data[k][4+(l*2)]=effectNum[k]; + lastVibrato=pat->data[k][5+(l*2)]; + break; + case 5: + // poramento + volume slide + pat->data[k][4+(l*2)]=0x06; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 6: + // vibrato + volume slide + pat->data[k][4+(l*2)]=0x05; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 8: + // modify TL of operator 1 + pat->data[k][4+(l*2)]=0x12; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 9: + // modify TL of operator 2 + pat->data[k][4+(l*2)]=0x13; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 10: + // volume slide + pat->data[k][4+(l*2)]=0xA; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 11: + // multi-frequency mode of CH3 control + // TODO + case 12: + // modify TL of operator 3 + pat->data[k][4+(l*2)]=0x14; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 13: + // modify TL of operator 2 + pat->data[k][4+(l*2)]=0x15; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 14: + switch (effectVal[k]>>4) { + case 0: + case 1: + case 2: + case 3: + // modify multiplier of operators + pat->data[k][4+(l*2)]=0x16; + pat->data[k][5+(l*2)]=((effectVal[k]&0xF0)+0x100)|(effectVal[k]&0xF); + break; + case 8: + // pan + pat->data[k][4+(l*2)]=0x80; + if ((effectVal[k]&0xF)==1) { + pat->data[k][5+(l*2)]=0; + } else if ((effectVal[k]&0xF)==2) { + pat->data[k][5+(l*2)]=0xFF; + } else { + pat->data[k][5+(l*2)]=0x80; + } + break; + } + break; + case 15: + // speed + + if (effectVal[k]==0) { + // if speed is set to zero (reset to global values) + speed.speedEven=info.speedEven; + speed.speedOdd=info.speedOdd; + speed.interleaveFactor=info.interleaveFactor; + } else if (effectVal[k]>>4==0) { + // if the top nibble is set to zero (set interleave factor) + speed.interleaveFactor=effectVal[k]&0xF; + } else if ((effectVal[k]>>4)==(effectVal[k]&0xF)) { + // if both speeds are equal + pat->data[k][4+(l*2)]=0x0F; + unsigned char speedSet=effectVal[k]>>4; + pat->data[k][5+(l*2)]=speedSet; + break; + } else { + speed.speedEven=effectVal[k]>>4; + speed.speedOdd=effectVal[k]&0xF; + } + + auto speedIndex = speeds.find(speed); + if (speedIndex != speeds.end()) { + pat->data[k][4+(l*2)]=0x09; + pat->data[k][5+(l*2)]=speedIndex->second; + break; + } + if (speed.interleaveFactor>8) { + logW("speed interleave factor is bigger than 8, speed information may be inaccurate"); + speed.interleaveFactor=8; + } + for (int i=0; igrooves.push_back(groove); + speeds[speed]=speedGrooveIndex; + + pat->data[k][4+(l*2)]=0x09; + pat->data[k][5+(l*2)]=speedGrooveIndex; + speedGrooveIndex++; + break; + } + } + info.ds->subsong[0]->pat[j].effectCols=(usedEffectsCol*2)+1; + + // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern length + if (info.patLens[i]!=0 && info.patLens[i]subsong[0]->patLen) { + pat->data[info.patLens[i]-1][4+(usedEffectsCol*4)]=0x0D; + pat->data[info.patLens[i]-1][5+(usedEffectsCol*4)]=0x00; + } + } + } + } + + // 2nd pass: fixing pitch slides, arpeggios, etc. so the result doesn't sound weird. + + bool chArpeggio[6]={false}; + bool chVibrato[6]={false}; + bool chPorta[6]={false}; + bool chVolumeSlide[6]={false}; + + for (int i=0; isubsong[0]->ordersLen; i++) { + for (int j=0; j<6; j++) { + for (int l=0; lsubsong[0]->pat[j].data[info.orderList[i]]; + + // default instrument + if (i==0 && pat->data[0][2]==-1) pat->data[0][2]=0; + + unsigned char truePatLen=(info.patLens[info.orderList[i]]subsong[0]->patLen) ? info.patLens[info.orderList[i]] : info.ds->subsong[0]->patLen; + + for (int k=0; kdata[k][4+(l*2)]!=0x00 && pat->data[k][0]!=-1) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x00; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chArpeggio[j]=false; + } else if (chPorta[j] && pat->data[k][4+(l*2)]!=0x03 && pat->data[k][4+(l*2)]!=0x01 && pat->data[k][4+(l*2)]!=0x02) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x03; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chPorta[j]=false; + } else if (chVibrato[j] && pat->data[k][4+(l*2)]!=0x04 && pat->data[k][0]!=-1) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x04; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chVibrato[j]=false; + } else if (chVolumeSlide[j] && pat->data[k][4+(l*2)]!=0x0A) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x0A; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chVolumeSlide[j]=false; + } + + switch (pat->data[k][4+l]) { + case 0: + chArpeggio[j]=true; + break; + case 1: + case 2: + case 3: + chPorta[j]=true; + break; + case 4: + chVibrato[j]=true; + break; + case 0xA: + chVolumeSlide[j]=true; + break; + default: + break; + } + } + } + } + } +} + +bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { + // the documentation for this version is in russian only + struct InvalidHeaderException {}; + bool success=false; + TFMRLEReader reader=TFMRLEReader(file,len); + + try { + DivSong ds; + ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; + ds.subsong[0]->hz=50; + ds.systemLen=1; + + ds.system[0]=DIV_SYSTEM_YM2612; + ds.loopModality=1; + + unsigned char speed=reader.readCNoRLE(); + unsigned char interleaveFactor=reader.readCNoRLE(); + + // TODO: due to limitations with the groove pattern, only interleave factors up to 8 + // are allowed in furnace + if (interleaveFactor>8) { + logW("interleave factor is bigger than 8, speed information may be inaccurate"); + interleaveFactor=8; + } + if ((speed>>4)==(speed&0xF)) { + ds.subsong[0]->speeds.val[0]=speed&0xF; + ds.subsong[0]->speeds.len=1; + } else { + for (int i=0; ispeeds.val[i]=speed>>4; + ds.subsong[0]->speeds.val[i+interleaveFactor]=speed&0xF; + } + ds.subsong[0]->speeds.len=interleaveFactor*2; + } + ds.subsong[0]->ordersLen=reader.readCNoRLE(); + + // order loop position, unused + (void)reader.readCNoRLE(); + + ds.createdDate=TFMparseDate(reader.readSNoRLE()); + ds.revisionDate=TFMparseDate(reader.readSNoRLE()); + + // TODO: use this for something, number of saves + (void)reader.readSNoRLE(); + + // author + logD("parsing author"); + ds.author=reader.readString(64); + + // name + logD("parsing name"); + ds.name=reader.readString(64); + + // notes + logD("parsing notes"); + String notes=reader.readString(384); + + // fix \r\n to \n + for (auto& c : notes) { + if (c=='\r') { + notes.erase(c,1); + } + } + + // order list + logD("parsing order list"); + unsigned char orderList[256]; + reader.read(orderList,256); + + bool patExists[256]; + unsigned char maxPat=0; + for (int i=0; iordersLen; i++) { + patExists[orderList[i]]=true; + if (maxPatorders.ord[j][i]=orderList[i]; + ds.subsong[0]->pat[j].data[orderList[i]]=new DivPattern; + } + } + + DivInstrument* insMaps[256]; + int insNumMaps[256]; + + // instrument names + logD("parsing instruments"); + unsigned char insName[16]; + int insCount=0; + for (int i=0; i<255; i++) { + reader.read(insName,16); + + if (memcmp(insName,"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",16)==0) { + logD("instrument unused"); + insNumMaps[i]=i; + insMaps[i]=NULL; + continue; + } + + DivInstrument* ins=new DivInstrument; + ins->type=DIV_INS_FM; + ins->name=String((const char*)insName,strnlen((const char*)insName,16)); + ds.ins.push_back(ins); + + insNumMaps[i]=insCount; + insCount++; + + insMaps[i]=ins; + } + + ds.insLen=insCount; + + // instrument data + for (int i=0; i<255; i++) { + if (!insMaps[i]) { + reader.skip(42); + continue; + } + + insMaps[i]->fm.alg=reader.readC(); + insMaps[i]->fm.fb=reader.readC(); + + for (int j=0; j<4; j++) { + insMaps[i]->fm.op[j].mult=reader.readC(); + insMaps[i]->fm.op[j].dt=reader.readC(); + insMaps[i]->fm.op[j].tl=reader.readC()^0x7F; + insMaps[i]->fm.op[j].rs=reader.readC(); + insMaps[i]->fm.op[j].ar=reader.readC(); + insMaps[i]->fm.op[j].dr=reader.readC(); + insMaps[i]->fm.op[j].d2r=reader.readC(); + insMaps[i]->fm.op[j].rr=reader.readC(); + insMaps[i]->fm.op[j].sl=reader.readC(); + insMaps[i]->fm.op[j].ssgEnv=reader.readC(); + } + } + + ds.notes=notes; + + unsigned char patLens[256]; + int maxPatLen=0; + reader.read(patLens, 256); + for (int i=0; i<256; i++) { + if (patLens[i]==0) { + maxPatLen=256; + break; + } else if (patLens[i]>maxPatLen) { + maxPatLen=patLens[i]; + } + } + + ds.subsong[0]->patLen=maxPatLen; + + struct TFMParsePatternInfo info; + info.ds=&ds; + info.insNumMaps=insNumMaps; + info.maxPat=maxPat; + info.patExists=patExists; + info.orderList=orderList; + info.speedEven=speed>>4; + info.speedOdd=speed&0xF; + info.interleaveFactor=interleaveFactor; + info.patLens=patLens; + info.reader=&reader; + info.v2=false; + TFMParsePattern(info); + + 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(TFMEndOfFileException& e) { + lastError="incomplete file!"; + } catch(InvalidHeaderException& e) { + lastError="invalid info header!"; + } + + delete[] file; + return success; +} + +bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { + struct InvalidHeaderException {}; + bool success=false; + TFMRLEReader reader=TFMRLEReader(file,len); + + try { + DivSong ds; + ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; + ds.subsong[0]->hz=50; + ds.systemLen=1; + + ds.system[0]=DIV_SYSTEM_YM2612; + ds.loopModality=1; + + unsigned char magic[8]={0}; + + reader.readNoRLE(magic,8); + if (memcmp(magic,DIV_TFM_MAGIC,8)!=0) throw InvalidHeaderException(); + + unsigned char speedEven=reader.readCNoRLE(); + unsigned char speedOdd=reader.readCNoRLE(); + unsigned char interleaveFactor=reader.readCNoRLE(); + + // TODO: due to limitations with the groove pattern, only interleave factors up to 8 + // are allowed in furnace + if (interleaveFactor>8) { + addWarning("interleave factor is bigger than 8, speed information may be inaccurate"); + interleaveFactor=8; + } + + if (speedEven==speedOdd) { + ds.subsong[0]->speeds.val[0]=speedEven; + ds.subsong[0]->speeds.len=1; + } else { + for (int i=0; ispeeds.val[i]=speedEven; + ds.subsong[0]->speeds.val[i+interleaveFactor]=speedOdd; + } + ds.subsong[0]->speeds.len=interleaveFactor*2; + } + ds.subsong[0]->ordersLen=reader.readCNoRLE(); + + // order loop position, unused + (void)reader.readCNoRLE(); + + ds.createdDate=TFMparseDate(reader.readSNoRLE()); + ds.revisionDate=TFMparseDate(reader.readSNoRLE()); + + // TODO: use this for something, number of saves + (void)reader.readSNoRLE(); + + // author + logD("parsing author"); + ds.author=reader.readString(64); + + // name + logD("parsing name"); + ds.name=reader.readString(64); + + // notes + logD("parsing notes"); + String notes=reader.readString(384); + + // fix \r\n to \n + for (auto& c : notes) { + if (c=='\r') { + notes.erase(c,1); + } + } + + // order list + logD("parsing order list"); + unsigned char orderList[256]; + reader.read(orderList,256); + + bool patExists[256]; + unsigned char maxPat=0; + for (int i=0; iordersLen; i++) { + patExists[orderList[i]]=true; + if (maxPatorders.ord[j][i]=orderList[i]; + ds.subsong[0]->pat[j].data[orderList[i]]=new DivPattern; + } + } + + DivInstrument* insMaps[256]; + int insNumMaps[256]; + + // instrument names + logD("parsing instruments"); + unsigned char insName[16]; + int insCount=0; + for (int i=0; i<255; i++) { + reader.read(insName,16); + + if (memcmp(insName,"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",16)==0) { + logD("instrument unused"); + insNumMaps[i]=i; + insMaps[i]=NULL; + continue; + } + + DivInstrument* ins=new DivInstrument; + ins->type=DIV_INS_FM; + ins->name=String((const char*)insName,strnlen((const char*)insName,16)); + ds.ins.push_back(ins); + + insNumMaps[i]=insCount; + insCount++; + + insMaps[i]=ins; + } + + ds.insLen=insCount; + + // instrument data + for (int i=0; i<255; i++) { + if (!insMaps[i]) { + reader.skip(42); + continue; + } + + insMaps[i]->fm.alg=reader.readC(); + insMaps[i]->fm.fb=reader.readC(); + + for (int j=0; j<4; j++) { + insMaps[i]->fm.op[j].mult=reader.readC(); + insMaps[i]->fm.op[j].dt=reader.readC(); + insMaps[i]->fm.op[j].tl=reader.readC()^0x7F; + insMaps[i]->fm.op[j].rs=reader.readC(); + insMaps[i]->fm.op[j].ar=reader.readC()^0x1F; + insMaps[i]->fm.op[j].dr=reader.readC()^0x1F; + insMaps[i]->fm.op[j].d2r=reader.readC()^0x1F; + insMaps[i]->fm.op[j].rr=reader.readC()^0xF; + insMaps[i]->fm.op[j].sl=reader.readC(); + insMaps[i]->fm.op[j].ssgEnv=reader.readC(); + } + } + + ds.notes=notes; + + unsigned char patLens[256]; + int maxPatLen=0; + reader.read(patLens, 256); + for (int i=0; i<256; i++) { + if (patLens[i]==0) { + maxPatLen=256; + break; + } else if (patLens[i]>maxPatLen) { + maxPatLen=patLens[i]; + } + } + + ds.subsong[0]->patLen=maxPatLen; + + struct TFMParsePatternInfo info; + info.ds=&ds; + info.insNumMaps=insNumMaps; + info.maxPat=maxPat; + info.patExists=patExists; + info.orderList=orderList; + info.speedEven=speedEven; + info.speedOdd=speedOdd; + info.interleaveFactor=interleaveFactor; + info.patLens=patLens; + info.reader=&reader; + info.v2=true; + TFMParsePattern(info); + + 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(TFMEndOfFileException& e) { + lastError="incomplete file!"; + } catch(InvalidHeaderException& e) { + lastError="invalid info header!"; + } + + delete[] file; + return success; +} diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 5dc3c221c..14ebf0610 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1647,7 +1647,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (!dirExists(workingDirSong)) workingDirSong=getHomeDir(); hasOpened=fileDialog->openLoad( "Open File", - {"compatible files", "*.fur *.dmf *.mod *.fc13 *.fc14 *.smod *.fc *.ftm *.0cc *.dnm *.eft *.fub", + {"compatible files", "*.fur *.dmf *.mod *.fc13 *.fc14 *.smod *.fc *.ftm *.0cc *.dnm *.eft *.fub *.tfe", "all files", "*"}, workingDirSong, dpiScale