Partially working TFM file format (v2) importer

This commit is contained in:
techmetx11 2024-04-10 15:13:54 +00:00
parent 63e7fa085a
commit c2ad98b998
No known key found for this signature in database
GPG key ID: 20E0C88A0E7E5AF2
5 changed files with 235 additions and 0 deletions

View file

@ -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

View file

@ -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<DivInstrument*>& ret, String& stripPath);
void loadTFI(SafeReader& reader, std::vector<DivInstrument*>& ret, String& stripPath);

View file

@ -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

View file

@ -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"

230
src/engine/fileOps/tfm.cpp Normal file
View file

@ -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)<<lenShift;
lenShift++;
// sync back since we've already read one character
tagLenLeft--;
} while (!(rleTag&0x80));
inTag=true;
tagChar=prevChar;
}
public:
TFMRLEReader(const void* b, size_t l) :
buf((const unsigned char*)b),
len(l),
curSeek(0),
inTag(false),
tagLenLeft(0),
tagChar(0) {}
// these functions may throw TFMEndOfFileException
unsigned char readC() {
if (curSeek>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+1<len) {
if (buf[curSeek+1]!=0x80) {
decodeRLE(buf[curSeek-2]);
tagLenLeft--;
return tagChar;
} else {
// to avoid outputting the extra 0x80
curSeek++;
return ret;
}
}
return ret;
}
char readCNoRLE() {
if (curSeek+1>len) 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; i<interleaveFactor; i++) {
ds.subsong[0]->speeds.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;
}