diff --git a/CMakeLists.txt b/CMakeLists.txt index e7e34aa93..22d99ef2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -639,6 +639,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 14bfd0db2..52d2127ac 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -551,6 +551,7 @@ 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 loadTFM(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..6fed9dc25 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -152,6 +152,8 @@ 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 loadTFM(file,len); } // step 3: try loading as .mod 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..fcc5b2bc3 --- /dev/null +++ b/src/engine/fileOps/tfm.cpp @@ -0,0 +1,230 @@ +/** + * 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--) { + b[i++]=readC(); + } + } + + 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 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_YM2612; + + 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); + } + } + + ds.notes=notes; + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + changeSong(0); + recalcChans(); + saveLock.unlock(); + BUSY_END; + success=true; + } catch(TFMEndOfFileException& e) { + lastError="incomplete file!"; + } catch(InvalidHeaderException& e) { + lastError="invalid info header!"; + } + + return success; +}