/** * 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; char tagChar; void decodeRLE(char prevChar) { int lenShift=0; tagLenLeft=0; char rleTag=0; do { rleTag=readC(); tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); if (inTag) { if (!tagLenLeft) { inTag=false; return readC(); } tagLenLeft--; logD("one char RLE decompressed, tag left: %d, char: %d",tagLenLeft,tagChar); return tagChar; } 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 && curSeek+1len) 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; } void skip(size_t l) { while (l--) readC(); } }; String TFMparseDate(short date) { return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); } bool DivEngine::loadTFM(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_YM2608; 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) { logW("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(); unsigned char buffer[384]; // author logD("parsing author"); reader.read(buffer,64); ds.author=String((const char*)buffer,strnlen((const char*)buffer,64)); memset(buffer, 0, 64); // name logD("parsing name"); reader.read(buffer,64); ds.name=String((const char*)buffer,strnlen((const char*)buffer,64)); memset(buffer, 0, 64); // notes logD("parsing notes"); reader.read(buffer,384); String notes((const char*)buffer,strnlen((const char*)buffer,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); for (int i=0; iordersLen; i++) { for (int j=0; j<6; j++) { ds.subsong[0]->orders.ord[j][i]=orderList[i]; } } DivInstrument* insMaps[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"); 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); 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; 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!"; } return success; }