From c2ad98b9983f113e8ff4108028faee60d1ec85a2 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 10 Apr 2024 15:13:54 +0000 Subject: [PATCH 01/29] Partially working TFM file format (v2) importer --- CMakeLists.txt | 1 + src/engine/engine.h | 1 + src/engine/fileOps/fileOpsCommon.cpp | 2 + src/engine/fileOps/fileOpsCommon.h | 1 + src/engine/fileOps/tfm.cpp | 230 +++++++++++++++++++++++++++ 5 files changed, 235 insertions(+) create mode 100644 src/engine/fileOps/tfm.cpp 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; +} From 9f26257364cff6c4c70de76ecc3b38ed106065eb Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 10 Apr 2024 16:39:03 +0000 Subject: [PATCH 02/29] Fixed bug in RLE decompressor, added order list --- src/engine/fileOps/tfm.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index fcc5b2bc3..7c0e949ef 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -45,7 +45,9 @@ class TFMRLEReader { do { rleTag=readC(); tagLenLeft|=(rleTag&0x7F)<ordersLen;i++) { + for (int j=0; j<6; j++) { + ds.subsong[0]->orders.ord[j][i]=orderList[i]; + } + } + ds.notes=notes; BUSY_BEGIN_SOFT; saveLock.lock(); From 9bbd673bfafef16654165c4355c480ecb1c73ded Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 10 Apr 2024 18:37:52 +0000 Subject: [PATCH 03/29] Fix another bug in the RLE decompressor, parsing instruments now --- src/engine/fileOps/tfm.cpp | 82 +++++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 7c0e949ef..4680c2f1d 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -45,12 +45,11 @@ class TFMRLEReader { do { rleTag=readC(); tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this, len); + 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); + logD("one char RLE decompressed, tag left: %d, char: %d",tagLenLeft,tagChar); return tagChar; } @@ -102,16 +101,16 @@ public: } char readCNoRLE() { - if (curSeek+1>len) throw TFMEndOfFileException(this, len); + if (curSeek+1>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(); + unsigned char nextChar=readC(); b[i++]=nextChar; - logD("read next char: %x, index: %d", nextChar, i); + logD("read next char: %x, index: %d",nextChar,i); } } @@ -119,7 +118,7 @@ public: int i=0; while (l--) { b[i++]=buf[curSeek++]; - if (curSeek>len) throw TFMEndOfFileException(this, len); + if (curSeek>len) throw TFMEndOfFileException(this,len); } } @@ -128,21 +127,25 @@ public: } short readSNoRLE() { - if (curSeek+2>len) throw TFMEndOfFileException(this, len); + 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); + 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); + TFMRLEReader reader=TFMRLEReader(file,len); try { DivSong ds; @@ -219,12 +222,61 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { unsigned char orderList[256]; reader.read(orderList,256); - for (int i=0; iordersLen;i++) { + 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; BUSY_BEGIN_SOFT; saveLock.lock(); From 833ed614b6b97f22c027f02c67906aff6ecf3437 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 10 Apr 2024 19:22:57 +0000 Subject: [PATCH 04/29] Forgot to properly initalize sound, also switched from YM2612 to YM2608, due to the sound ceiling not properly rendering sounds as they should be --- src/engine/fileOps/tfm.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 4680c2f1d..6d8ba8371 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -152,8 +152,8 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; ds.systemLen = 1; - ds.system[0]=DIV_SYSTEM_YM2612; + ds.system[0]=DIV_SYSTEM_YM2608; unsigned char magic[8]={0}; reader.readNoRLE(magic, 8); @@ -278,6 +278,8 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { } ds.notes=notes; + + if (active) quitDispatch(); BUSY_BEGIN_SOFT; saveLock.lock(); song.unload(); @@ -286,6 +288,13 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { recalcChans(); saveLock.unlock(); BUSY_END; + if (active) { + initDispatch(); + BUSY_BEGIN; + renderSamples(); + reset(); + BUSY_END; + } success=true; } catch(TFMEndOfFileException& e) { lastError="incomplete file!"; From 1de99ca6c638b4638fef45e83c3e222fedf847b0 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 11 Apr 2024 15:16:05 +0000 Subject: [PATCH 05/29] Somewhat implemented pattern decoding now (effects not implemented) --- src/engine/fileOps/tfm.cpp | 102 ++++++++++++++++++++++++++++++++++++- src/gui/gui.cpp | 2 +- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 6d8ba8371..df53f6511 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -65,7 +65,6 @@ public: // these functions may throw TFMEndOfFileException unsigned char readC() { - if (curSeek>len) throw TFMEndOfFileException(this,len); if (inTag) { if (!tagLenLeft) { inTag=false; @@ -75,6 +74,7 @@ public: logD("one char RLE decompressed, tag left: %d, char: %d",tagLenLeft,tagChar); return tagChar; } + if (curSeek>len) throw TFMEndOfFileException(this,len); unsigned char ret=buf[curSeek++]; @@ -134,8 +134,13 @@ public: } void skip(size_t l) { - while (l--) readC(); + // quick and dirty + while (l--) { + logD("skipping l %d", l); + readC(); + } } + }; String TFMparseDate(short date) { @@ -222,13 +227,20 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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"); @@ -239,6 +251,7 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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; } @@ -247,7 +260,10 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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; } @@ -279,6 +295,88 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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; + + // 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) + // - padding(?) (1536 bytes, always set to 0) + // 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]; + + for (int i=0; i<256; i++) { + if (i>maxPat) break; + else if (!patExists[i]) { + logD("skipping pattern %d", i); + reader.skip(16896); + continue; + } + + logD("parsing pattern %d", i); + for (int j=0; j<6; j++) { + DivPattern* pat = ds.subsong[0]->pat[j].data[i]; + + // notes + 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; + } + } + + // volume + 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]=patDataBuf[k]*4; + } + + // instrument + 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; + else { + pat->data[k][2]=insNumMaps[patDataBuf[k]-1]; + } + } + + logD("ignoring unused data of pattern %d channel %d", i, j); + reader.read(patDataBuf, 256); + reader.read(patDataBuf, 256); + + reader.skip(1536); + } + + } if (active) quitDispatch(); BUSY_BEGIN_SOFT; saveLock.lock(); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 0c4c43b44..0b1db5861 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 From 772a7a414462dddfeb1258bedf546e54a6e0b5b0 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 11 Apr 2024 15:58:54 +0000 Subject: [PATCH 06/29] Fix notes and patterns that don't have the same size as the biggest pattern in the song --- src/engine/fileOps/tfm.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index df53f6511..104b5e5d4 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -346,9 +346,19 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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]--; + } } } + // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern lengths + if (patLens[i]!=0 && patLens[i]patLen) { + pat->data[patLens[i]][4]=0x0D; + pat->data[patLens[i]][5]=0x00; + } // volume reader.read(patDataBuf, 256); From b18c29a1daf1f8250155dd753f790fd25e72f27b Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 11 Apr 2024 16:05:54 +0000 Subject: [PATCH 07/29] Code formatting --- src/engine/fileOps/tfm.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 104b5e5d4..aedd557be 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -136,7 +136,7 @@ public: void skip(size_t l) { // quick and dirty while (l--) { - logD("skipping l %d", l); + logD("skipping l %d",l); readC(); } } @@ -336,7 +336,7 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { // notes reader.read(patDataBuf, 256); - logD("parsing notes of pattern %d channel %d", i, j); + 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) { @@ -360,18 +360,18 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { pat->data[patLens[i]][5]=0x00; } // volume - reader.read(patDataBuf, 256); + reader.read(patDataBuf,256); - logD("parsing volumes of pattern %d channel %d", i, j); + 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]=patDataBuf[k]*4; } // instrument - reader.read(patDataBuf, 256); + reader.read(patDataBuf,256); - logD("parsing instruments of pattern %d channel %d", i, j); + logD("parsing instruments of pattern %d channel %d",i,j); for (int k=0; k<256; k++) { if (patDataBuf[k]==0) continue; else { @@ -379,9 +379,9 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { } } - logD("ignoring unused data of pattern %d channel %d", i, j); - reader.read(patDataBuf, 256); - reader.read(patDataBuf, 256); + logD("ignoring unused data of pattern %d channel %d",i,j); + reader.read(patDataBuf,256); + reader.read(patDataBuf,256); reader.skip(1536); } From 736f812503bf9ee708b28c1ae463123098b77376 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 11 Apr 2024 16:12:18 +0000 Subject: [PATCH 08/29] Revert back to YM2612 (OPN2), We only need 6 FM channels --- src/engine/fileOps/tfm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index aedd557be..7572afee2 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -158,7 +158,7 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { ds.subsong[0]->hz=50; ds.systemLen = 1; - ds.system[0]=DIV_SYSTEM_YM2608; + ds.system[0]=DIV_SYSTEM_YM2612; unsigned char magic[8]={0}; reader.readNoRLE(magic, 8); From f75ab6186d525ffff30b2391d4c8515f3ba714ab Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 11 Apr 2024 22:43:53 +0000 Subject: [PATCH 09/29] Partially implement TFM's effects Fix the mismatched patterns hack to be more seamless, by adding the "jump to next pattern" effects in the last row, in addition to the row after that --- src/engine/fileOps/tfm.cpp | 52 +++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 7572afee2..6fadee58e 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -86,7 +86,7 @@ public: // 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+1patLen) { + if (pat->data[patLens[i]-1][4]==-1 && pat->data[patLens[i]-1][5]==-1) { + pat->data[patLens[i]-1][4]=0x0D; + pat->data[patLens[i]-1][5]=0x00; + } pat->data[patLens[i]][4]=0x0D; pat->data[patLens[i]][5]=0x00; } @@ -374,14 +378,48 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { logD("parsing instruments of pattern %d channel %d",i,j); for (int k=0; k<256; k++) { if (patDataBuf[k]==0) continue; - else { - pat->data[k][2]=insNumMaps[patDataBuf[k]-1]; - } + pat->data[k][2]=insNumMaps[patDataBuf[k]-1]; } - logD("ignoring unused data of pattern %d channel %d",i,j); - reader.read(patDataBuf,256); - reader.read(patDataBuf,256); + // effects + + unsigned char effectNum[256]; + unsigned char effectVal[256]; + reader.read(effectNum,256); + reader.read(effectVal,256); + + for (int k=0; k<256; k++) { + switch (effectNum[k]) { + case 0: + // arpeggio or no effect (if effect val is 0) + if (effectVal[k]==0) break; + pat->data[k][4]=effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; + case 1: + // pitch slide up + case 2: + // pitch slide down + case 3: + // portamento + case 4: + // vibrato + pat->data[k][4]=effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; + case 5: + // poramento + volume slide + pat->data[k][4]=0x06; + pat->data[k][5]=effectVal[k]; + break; + case 6: + // vibrato + volume slide + pat->data[k][4]=0x05; + pat->data[k][5]=effectVal[k]; + default: + break; + } + } reader.skip(1536); } From bcb0b2c1b73c679b17e107c4a5cce6fc9fdeb617 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Fri, 12 Apr 2024 05:13:09 +0000 Subject: [PATCH 10/29] Fix the volume a bit, and the suggestions tildearrow proposed --- src/engine/fileOps/tfm.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 6fadee58e..d53e51925 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -36,12 +36,12 @@ class TFMRLEReader { size_t curSeek; bool inTag; int tagLenLeft; - char tagChar; + signed char tagChar; - void decodeRLE(char prevChar) { + void decodeRLE(signed char prevChar) { int lenShift=0; tagLenLeft=0; - char rleTag=0; + signed char rleTag=0; do { rleTag=readC(); tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); return buf[curSeek++]; } @@ -369,7 +369,7 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { 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]=patDataBuf[k]*4; + else pat->data[k][3]=0x41+(patDataBuf[k]*2); } // instrument @@ -416,6 +416,7 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { // vibrato + volume slide pat->data[k][4]=0x05; pat->data[k][5]=effectVal[k]; + break; default: break; } From 99729a41570579e77823b9b5b324278888e5773f Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Fri, 12 Apr 2024 10:15:20 +0000 Subject: [PATCH 11/29] Converted tab to space --- src/engine/fileOps/fileOpsCommon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/fileOps/fileOpsCommon.cpp b/src/engine/fileOps/fileOpsCommon.cpp index 6fed9dc25..df480d847 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -153,7 +153,7 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { } 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); + return loadTFM(file,len); } // step 3: try loading as .mod From 7a051b44868edab869b755173eaf517c755236ef Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Fri, 12 Apr 2024 13:09:18 +0000 Subject: [PATCH 12/29] RLE bug squashed, now the modules that couldn't load can load now --- src/engine/fileOps/tfm.cpp | 43 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index d53e51925..7d9a8687e 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -38,20 +38,27 @@ class TFMRLEReader { int tagLenLeft; signed char tagChar; - void decodeRLE(signed char prevChar) { + void decodeRLE(unsigned char prevChar) { int lenShift=0; tagLenLeft=0; - signed char rleTag=0; + unsigned char rleTag=0; + do { - rleTag=readC(); + rleTag=readCNoRLE(); tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); + if (curSeek>len) throw TFMEndOfFileException(this,len); return buf[curSeek++]; } From 4bc4bfac32fa8fb26c3b8bff46aa2671170d73d3 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Fri, 12 Apr 2024 21:36:27 +0000 Subject: [PATCH 13/29] Made a dedicated read string function, messed a bit with the TFM effects. Added a compatibility flag that resets the effects after a row change/new note --- src/engine/engine.h | 5 ++++- src/engine/fileOps/tfm.cpp | 33 +++++++++++++++++++-------------- src/engine/playback.cpp | 35 ++++++++++++++++++++++++++++++++++- src/engine/song.h | 4 +++- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/engine/engine.h b/src/engine/engine.h index 52d2127ac..95559e393 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -104,7 +104,7 @@ struct DivChannelState { int delayOrder, delayRow, retrigSpeed, retrigTick; int vibratoDepth, vibratoRate, vibratoPos, vibratoPosGiant, vibratoDir, vibratoFine; int tremoloDepth, tremoloRate, tremoloPos; - unsigned char arp, arpStage, arpTicks, panL, panR, panRL, panRR, lastVibrato, lastPorta, cutType; + unsigned char arp, arpStage, arpTicks, panL, panR, panRL, panRR, lastVibrato, lastPorta, cutType, lastArp, lastVibrato2, lastPorta2; bool doNote, legato, portaStop, keyOn, keyOff, nowYouCanStop, stopOnOff, releasing; bool arpYield, delayLocked, inPorta, scheduledSlideReset, shorthandPorta, wasShorthandPorta, noteOnInhibit, resetArp; bool wentThroughNote, goneThroughNote; @@ -150,6 +150,9 @@ struct DivChannelState { lastVibrato(0), lastPorta(0), cutType(0), + lastArp(0), + lastVibrato2(0), + lastPorta2(0), doNote(false), legato(false), portaStop(false), diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 7d9a8687e..b23fa18a3 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -134,6 +134,14 @@ public: return ret; } + String readString(size_t l) { + String ret; + ret.reserve(l); + while (l--) { + ret += readC(); + } + return ret; + } void skip(size_t l) { // quick and dirty while (l--) { @@ -157,12 +165,13 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { DivSong ds; ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; - ds.systemLen = 1; + ds.systemLen=1; + ds.resetEffectsOnNewNote=true; ds.system[0]=DIV_SYSTEM_YM2612; unsigned char magic[8]={0}; - reader.readNoRLE(magic, 8); + reader.readNoRLE(magic,8); if (memcmp(magic,DIV_TFM_MAGIC,8)!=0) throw InvalidHeaderException(); unsigned char speedEven=reader.readCNoRLE(); @@ -197,24 +206,17 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { // 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); + ds.author=reader.readString(64); // name logD("parsing name"); - reader.read(buffer,64); - ds.name=String((const char*)buffer,strnlen((const char*)buffer,64)); - memset(buffer, 0, 64); + ds.name=reader.readString(64); // notes logD("parsing notes"); - reader.read(buffer,384); - String notes((const char*)buffer,strnlen((const char*)buffer,384)); + String notes=reader.readString(384); // fix \r\n to \n for (auto& c : notes) { @@ -398,9 +400,12 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { pat->data[k][5]=effectVal[k]; break; case 1: - // pitch slide up + // note slide up case 2: - // pitch slide down + // note slide down + pat->data[k][4]=0xF0|effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; case 3: // portamento case 4: diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 1e7d823c2..9364f1a89 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -740,6 +740,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x03: // portamento + chan[i].lastPorta2=effectVal; if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -768,6 +769,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x04: // vibrato + chan[i].lastVibrato2=effectVal; if (effectVal) chan[i].lastVibrato=effectVal; chan[i].vibratoDepth=effectVal&15; chan[i].vibratoRate=effectVal>>4; @@ -775,6 +777,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); break; case 0x05: // vol slide + vibrato + chan[i].lastVibrato2=effectVal; if (effectVal==0) { chan[i].vibratoDepth=0; chan[i].vibratoRate=0; @@ -782,6 +785,7 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].vibratoDepth=chan[i].lastVibrato&15; chan[i].vibratoRate=chan[i].lastVibrato>>4; } + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); // TODO: non-0x-or-x0 value should be treated as 00 @@ -800,6 +804,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x06: // vol slide + porta + chan[i].lastPorta2=effectVal; if (effectVal==0 || chan[i].lastPorta==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -870,6 +875,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x00: // arpeggio + chan[i].lastArp=effectVal; chan[i].arp=effectVal; if (chan[i].arp==0 && song.arp0Reset) { chan[i].resetArp=true; @@ -1110,6 +1116,33 @@ void DivEngine::processRow(int i, bool afterDelay) { } } + if (song.resetEffectsOnNewNote) { + if (chan[i].lastArp) { + chan[i].lastArp=0; + } else { + chan[i].arp=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); + } + + if (chan[i].lastVibrato2) { + chan[i].lastVibrato2=0; + } else { + chan[i].vibratoDepth=0; + chan[i].vibratoRate=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); + } + + if (chan[i].lastPorta2) { + chan[i].lastPorta2=0; + } else { + chan[i].portaSpeed=-1; + chan[i].portaNote=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + chan[i].inPorta=false; + dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); + } + } + if (panChanged) { dispatchCmd(DivCommand(DIV_CMD_PANNING,i,chan[i].panL,chan[i].panR)); } @@ -1146,7 +1179,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,chan[i].note,chan[i].volume>>8)); chan[i].releasing=false; if (song.resetArpPhaseOnNewNote) { - chan[i].arpStage=-1; + chan[i].arpStage=-1; } chan[i].goneThroughNote=true; chan[i].wentThroughNote=true; diff --git a/src/engine/song.h b/src/engine/song.h index f9983ba9e..0d99cc747 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -331,6 +331,7 @@ struct DivSong { bool resetArpPhaseOnNewNote; bool ceilVolumeScaling; bool oldAlwaysSetVolume; + bool resetEffectsOnNewNote; std::vector ins; std::vector wave; @@ -454,7 +455,8 @@ struct DivSong { oldDPCM(false), resetArpPhaseOnNewNote(false), ceilVolumeScaling(false), - oldAlwaysSetVolume(false) { + oldAlwaysSetVolume(false), + resetEffectsOnNewNote(false) { for (int i=0; i Date: Sat, 13 Apr 2024 06:43:37 +0000 Subject: [PATCH 14/29] Support for TFEv1 files --- src/engine/engine.h | 3 +- src/engine/fileOps/fileOpsCommon.cpp | 8 +- src/engine/fileOps/tfm.cpp | 450 ++++++++++++++++++++------- 3 files changed, 337 insertions(+), 124 deletions(-) diff --git a/src/engine/engine.h b/src/engine/engine.h index 95559e393..0910ca13c 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -554,7 +554,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 loadTFM(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 df480d847..bc470bae3 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -153,11 +153,13 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { } 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); + 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/tfm.cpp b/src/engine/fileOps/tfm.cpp index b23fa18a3..0f4559e11 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -156,7 +156,327 @@ 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 TFMparsePatternInfo { + TFMRLEReader* reader; + unsigned char maxPat; + unsigned char* patLens; + 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) + // - padding(?) (1536 bytes, always set to 0) (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]; + + 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(16896); + 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]--; + } + } + } + + // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern lengths + if (info.patLens[i]!=0 && info.patLens[i]subsong[0]->patLen) { + if (pat->data[info.patLens[i]-1][4]==-1 && pat->data[info.patLens[i]-1][5]==-1) { + pat->data[info.patLens[i]-1][4]=0x0D; + pat->data[info.patLens[i]-1][5]=0x00; + } + pat->data[info.patLens[i]][4]=0x0D; + pat->data[info.patLens[i]][5]=0x00; + } + // 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]=0x41+(patDataBuf[k]*2); + } + + // 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 + + unsigned char effectNum[256]; + unsigned char effectVal[256]; + info.reader->read(effectNum,256); + info.reader->read(effectVal,256); + + for (int k=0; k<256; k++) { + switch (effectNum[k]) { + case 0: + // arpeggio or no effect (if effect val is 0) + if (effectVal[k]==0) break; + pat->data[k][4]=effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; + case 1: + // note slide up + case 2: + // note slide down + pat->data[k][4]=0xF0|effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; + case 3: + // portamento + case 4: + // vibrato + pat->data[k][4]=effectNum[k]; + pat->data[k][5]=effectVal[k]; + break; + case 5: + // poramento + volume slide + pat->data[k][4]=0x06; + pat->data[k][5]=effectVal[k]; + break; + case 6: + // vibrato + volume slide + pat->data[k][4]=0x05; + pat->data[k][5]=effectVal[k]; + break; + default: + break; + } + } + + if (info.v2) info.reader->skip(1536); + } + + } +} + +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.resetEffectsOnNewNote=true; + + ds.system[0]=DIV_SYSTEM_YM2612; + + 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.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!"; + } + + return success; +} + +bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { struct InvalidHeaderException {}; bool success=false; TFMRLEReader reader=TFMRLEReader(file,len); @@ -312,126 +632,16 @@ bool DivEngine::loadTFM(unsigned char* file, size_t len) { ds.subsong[0]->patLen=maxPatLen; - // 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) - // - padding(?) (1536 bytes, always set to 0) - // 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]; + struct TFMparsePatternInfo info; + info.ds=&ds; + info.insNumMaps=insNumMaps; + info.maxPat=maxPat; + info.patExists=patExists; + info.patLens=patLens; + info.reader=&reader; + info.v2=true; + TFMparsePattern(info); - for (int i=0; i<256; i++) { - if (i>maxPat) break; - else if (!patExists[i]) { - logD("skipping pattern %d", i); - reader.skip(16896); - continue; - } - - logD("parsing pattern %d", i); - for (int j=0; j<6; j++) { - DivPattern* pat = ds.subsong[0]->pat[j].data[i]; - - // notes - 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]--; - } - } - } - - // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern lengths - if (patLens[i]!=0 && patLens[i]patLen) { - if (pat->data[patLens[i]-1][4]==-1 && pat->data[patLens[i]-1][5]==-1) { - pat->data[patLens[i]-1][4]=0x0D; - pat->data[patLens[i]-1][5]=0x00; - } - pat->data[patLens[i]][4]=0x0D; - pat->data[patLens[i]][5]=0x00; - } - // volume - 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]=0x41+(patDataBuf[k]*2); - } - - // instrument - 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]=insNumMaps[patDataBuf[k]-1]; - } - - // effects - - unsigned char effectNum[256]; - unsigned char effectVal[256]; - reader.read(effectNum,256); - reader.read(effectVal,256); - - for (int k=0; k<256; k++) { - switch (effectNum[k]) { - case 0: - // arpeggio or no effect (if effect val is 0) - if (effectVal[k]==0) break; - pat->data[k][4]=effectNum[k]; - pat->data[k][5]=effectVal[k]; - break; - case 1: - // note slide up - case 2: - // note slide down - pat->data[k][4]=0xF0|effectNum[k]; - pat->data[k][5]=effectVal[k]; - break; - case 3: - // portamento - case 4: - // vibrato - pat->data[k][4]=effectNum[k]; - pat->data[k][5]=effectVal[k]; - break; - case 5: - // poramento + volume slide - pat->data[k][4]=0x06; - pat->data[k][5]=effectVal[k]; - break; - case 6: - // vibrato + volume slide - pat->data[k][4]=0x05; - pat->data[k][5]=effectVal[k]; - break; - default: - break; - } - } - - reader.skip(1536); - } - - } if (active) quitDispatch(); BUSY_BEGIN_SOFT; saveLock.lock(); From d6fd63f813dba8e5278204fc7ce5ddb5a5f7f67f Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 13 Apr 2024 06:52:08 +0000 Subject: [PATCH 15/29] Revert pitch slide back to its original form (01xx/02xx) --- src/engine/fileOps/tfm.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 0f4559e11..9927a0fa6 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -255,12 +255,9 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { pat->data[k][5]=effectVal[k]; break; case 1: - // note slide up + // pitch slide up case 2: - // note slide down - pat->data[k][4]=0xF0|effectNum[k]; - pat->data[k][5]=effectVal[k]; - break; + // pitch slide down case 3: // portamento case 4: From 50f1cf163a6f54fbd9816deff7c742fb31d341aa Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 13 Apr 2024 17:45:12 +0000 Subject: [PATCH 16/29] Replicated TFM's single-row pitch slide --- src/engine/fileOps/tfm.cpp | 13 +++++++-- src/engine/playback.cpp | 56 ++++++++++++++++++++------------------ src/engine/song.h | 4 +-- src/gui/compatFlags.cpp | 4 +++ 4 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 9927a0fa6..9c675a106 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -246,6 +246,7 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { info.reader->read(effectNum,256); info.reader->read(effectVal,256); + unsigned short lastSlide=0; for (int k=0; k<256; k++) { switch (effectNum[k]) { case 0: @@ -258,6 +259,14 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { // pitch slide up case 2: // pitch slide down + pat->data[k][4]=effectNum[k]; + if (effectVal[k]) { + lastSlide=effectVal[k]; + pat->data[k][5]=effectVal[k]; + } else { + pat->data[k][5]=lastSlide; + } + break; case 3: // portamento case 4: @@ -297,7 +306,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; ds.systemLen=1; - ds.resetEffectsOnNewNote=true; + ds.resetEffectsOnRowChange=true; ds.system[0]=DIV_SYSTEM_YM2612; @@ -483,7 +492,7 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; ds.systemLen=1; - ds.resetEffectsOnNewNote=true; + ds.resetEffectsOnRowChange=true; ds.system[0]=DIV_SYSTEM_YM2612; unsigned char magic[8]={0}; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 9364f1a89..f784dd707 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -653,6 +653,14 @@ void DivEngine::processRow(int i, bool afterDelay) { bool surroundPanChanged=false; // effects + if (song.resetEffectsOnRowChange) { + chan[i].portaSpeed=-1; + chan[i].portaNote=-1; + dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); + chan[i].inPorta=false; + if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); + } + for (int j=0; jdata[whatRow][4+(j<<1)]; short effectVal=pat->data[whatRow][5+(j<<1)]; @@ -697,6 +705,7 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x01: // ramp up if (song.ignoreDuplicateSlides && (lastSlide==0x01 || lastSlide==0x1337)) break; + chan[i].lastPorta2=effectVal; lastSlide=0x01; if (effectVal==0) { chan[i].portaNote=-1; @@ -719,6 +728,7 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x02: // ramp down if (song.ignoreDuplicateSlides && (lastSlide==0x02 || lastSlide==0x1337)) break; + chan[i].lastPorta2=effectVal; lastSlide=0x02; if (effectVal==0) { chan[i].portaNote=-1; @@ -1116,33 +1126,6 @@ void DivEngine::processRow(int i, bool afterDelay) { } } - if (song.resetEffectsOnNewNote) { - if (chan[i].lastArp) { - chan[i].lastArp=0; - } else { - chan[i].arp=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); - } - - if (chan[i].lastVibrato2) { - chan[i].lastVibrato2=0; - } else { - chan[i].vibratoDepth=0; - chan[i].vibratoRate=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); - } - - if (chan[i].lastPorta2) { - chan[i].lastPorta2=0; - } else { - chan[i].portaSpeed=-1; - chan[i].portaNote=-1; - dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); - chan[i].inPorta=false; - dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); - } - } - if (panChanged) { dispatchCmd(DivCommand(DIV_CMD_PANNING,i,chan[i].panL,chan[i].panR)); } @@ -1155,10 +1138,28 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); } + if (song.resetEffectsOnRowChange) { + if (chan[i].lastArp) { + chan[i].lastArp=0; + } else { + chan[i].arp=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); + } + + if (chan[i].lastVibrato2) { + chan[i].lastVibrato2=0; + } else { + chan[i].vibratoDepth=0; + chan[i].vibratoRate=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); + } + } + if (chan[i].doNote) { if (!song.continuousVibrato) { chan[i].vibratoPos=0; } + dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); if (chan[i].legato && (!chan[i].inPorta || song.brokenPortaLegato)) { dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); @@ -1187,6 +1188,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } } chan[i].doNote=false; + if (!chan[i].keyOn && chan[i].scheduledSlideReset) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; diff --git a/src/engine/song.h b/src/engine/song.h index 0d99cc747..871973900 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -331,7 +331,7 @@ struct DivSong { bool resetArpPhaseOnNewNote; bool ceilVolumeScaling; bool oldAlwaysSetVolume; - bool resetEffectsOnNewNote; + bool resetEffectsOnRowChange; std::vector ins; std::vector wave; @@ -456,7 +456,7 @@ struct DivSong { resetArpPhaseOnNewNote(false), ceilVolumeScaling(false), oldAlwaysSetVolume(false), - resetEffectsOnNewNote(false) { + resetEffectsOnRowChange(false) { for (int i=0; isong.resetEffectsOnRowChange); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("when enabled, effects in a row will be canceled after the row changes"); + } ImGui::EndTabItem(); } ImGui::EndTabBar(); From a6be0a192524433397f320524e8e5fdf54c9c86d Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 13 Apr 2024 20:04:43 +0000 Subject: [PATCH 17/29] Fix arpeggio --- src/engine/playback.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index f784dd707..66857bbc4 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1138,24 +1138,24 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,DIV_NOTE_NULL)); } - if (song.resetEffectsOnRowChange) { - if (chan[i].lastArp) { - chan[i].lastArp=0; - } else { - chan[i].arp=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); - } - - if (chan[i].lastVibrato2) { - chan[i].lastVibrato2=0; - } else { - chan[i].vibratoDepth=0; - chan[i].vibratoRate=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); - } - } - if (chan[i].doNote) { + if (song.resetEffectsOnRowChange) { + if (chan[i].lastArp) { + chan[i].lastArp=0; + } else { + chan[i].arp=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); + } + + if (chan[i].lastVibrato2) { + chan[i].lastVibrato2=0; + } else { + chan[i].vibratoDepth=0; + chan[i].vibratoRate=0; + dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); + } + } + if (!song.continuousVibrato) { chan[i].vibratoPos=0; } From 607b541307428088d1490008ac4fa1bcb63a1991 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 14 Apr 2024 14:00:21 +0100 Subject: [PATCH 18/29] Add warning, and fix pattern parsing --- src/engine/fileOps/tfm.cpp | 82 ++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 7 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 9c675a106..78b71c3f5 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -182,17 +182,17 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { 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(16896); + logD("skipping pattern %d",i); + info.reader->skip((info.v2) ? 16896 : 7680); continue; } - logD("parsing pattern %d", i); + 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); + info.reader->read(patDataBuf,256); logD("parsing notes of pattern %d channel %d",i,j); for (int k=0; k<256; k++) { @@ -247,6 +247,7 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { info.reader->read(effectVal,256); unsigned short lastSlide=0; + unsigned short lastVibrato=0; for (int k=0; k<256; k++) { switch (effectNum[k]) { case 0: @@ -271,8 +272,19 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { // portamento case 4: // vibrato + pat->data[k][5]=0; + if (effectVal[k]&0xF0) { + pat->data[k][5]|=effectVal[k]&0xF0; + } else { + pat->data[k][5]|=lastVibrato&0xF0; + } + if (effectVal[k]&0x0F) { + pat->data[k][5]|=effectVal[k]&0x0F; + } else { + pat->data[k][5]|=lastVibrato&0x0F; + } pat->data[k][4]=effectNum[k]; - pat->data[k][5]=effectVal[k]; + lastVibrato=pat->data[k][5]; break; case 5: // poramento + volume slide @@ -284,14 +296,66 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { pat->data[k][4]=0x05; pat->data[k][5]=effectVal[k]; break; + case 8: + // modify TL of operator 1 + pat->data[k][4]=0x12; + pat->data[k][5]=effectVal[k]; + break; + case 9: + // modify TL of operator 2 + pat->data[k][4]=0x13; + pat->data[k][5]=effectVal[k]; + break; + case 10: + // volume slide + pat->data[k][4]=0xA; + pat->data[k][5]=effectVal[k]; + break; + case 11: + // multi-frequency mode of CH3 control + // TODO + case 12: + // modify TL of operator 3 + pat->data[k][4]=0x14; + pat->data[k][5]=effectVal[k]; + break; + case 13: + // modify TL of operator 2 + pat->data[k][4]=0x15; + pat->data[k][5]=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]=0x16; + pat->data[k][5]=((effectVal[k]&0xF0)+0x100)|(effectVal[k]&0xF); + break; + case 8: + // pan + pat->data[k][4]=0x80; + if (effectVal[k]==1) { + pat->data[k][5]=0; + } else if (effectVal[k]==2) { + pat->data[k][5]=0xFF; + } else { + pat->data[k][5]=0x80; + } + break; + } + break; default: + pat->data[k][4]=effectNum[k]; + pat->data[k][5]=effectVal[k]; break; } } if (info.v2) info.reader->skip(1536); } - } } @@ -307,6 +371,8 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { ds.subsong[0]->hz=50; ds.systemLen=1; ds.resetEffectsOnRowChange=true; + addWarning("this song relies on a compatibility flag to make the sound more accurate," \ + " it will not be preserved when you save it"); ds.system[0]=DIV_SYSTEM_YM2612; @@ -493,6 +559,8 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { ds.subsong[0]->hz=50; ds.systemLen=1; ds.resetEffectsOnRowChange=true; + addWarning("this song relies on a compatibility flag to make the sound more accurate," \ + " it will not be preserved when you save it"); ds.system[0]=DIV_SYSTEM_YM2612; unsigned char magic[8]={0}; @@ -507,7 +575,7 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { // 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"); + addWarning("interleave factor is bigger than 8, speed information may be inaccurate"); interleaveFactor=8; } From d64614e7fe09bfe45eb7a4fa36aa207e1bc4d78d Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 14 Apr 2024 15:54:18 +0100 Subject: [PATCH 19/29] Remove compatibility flag --- src/engine/engine.h | 5 +---- src/engine/fileOps/tfm.cpp | 10 ++-------- src/engine/playback.cpp | 32 -------------------------------- src/engine/song.h | 4 +--- src/gui/compatFlags.cpp | 4 ---- 5 files changed, 4 insertions(+), 51 deletions(-) diff --git a/src/engine/engine.h b/src/engine/engine.h index 0910ca13c..2a0f467aa 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -104,7 +104,7 @@ struct DivChannelState { int delayOrder, delayRow, retrigSpeed, retrigTick; int vibratoDepth, vibratoRate, vibratoPos, vibratoPosGiant, vibratoDir, vibratoFine; int tremoloDepth, tremoloRate, tremoloPos; - unsigned char arp, arpStage, arpTicks, panL, panR, panRL, panRR, lastVibrato, lastPorta, cutType, lastArp, lastVibrato2, lastPorta2; + unsigned char arp, arpStage, arpTicks, panL, panR, panRL, panRR, lastVibrato, lastPorta, cutType; bool doNote, legato, portaStop, keyOn, keyOff, nowYouCanStop, stopOnOff, releasing; bool arpYield, delayLocked, inPorta, scheduledSlideReset, shorthandPorta, wasShorthandPorta, noteOnInhibit, resetArp; bool wentThroughNote, goneThroughNote; @@ -150,9 +150,6 @@ struct DivChannelState { lastVibrato(0), lastPorta(0), cutType(0), - lastArp(0), - lastVibrato2(0), - lastPorta2(0), doNote(false), legato(false), portaStop(false), diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 78b71c3f5..7c345ee32 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -337,9 +337,9 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { case 8: // pan pat->data[k][4]=0x80; - if (effectVal[k]==1) { + if ((effectVal[k]&0xF)==1) { pat->data[k][5]=0; - } else if (effectVal[k]==2) { + } else if ((effectVal[k]&0xF)==2) { pat->data[k][5]=0xFF; } else { pat->data[k][5]=0x80; @@ -370,9 +370,6 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; ds.systemLen=1; - ds.resetEffectsOnRowChange=true; - addWarning("this song relies on a compatibility flag to make the sound more accurate," \ - " it will not be preserved when you save it"); ds.system[0]=DIV_SYSTEM_YM2612; @@ -558,9 +555,6 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.subsong[0]->hz=50; ds.systemLen=1; - ds.resetEffectsOnRowChange=true; - addWarning("this song relies on a compatibility flag to make the sound more accurate," \ - " it will not be preserved when you save it"); ds.system[0]=DIV_SYSTEM_YM2612; unsigned char magic[8]={0}; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 66857bbc4..971dac593 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -653,14 +653,6 @@ void DivEngine::processRow(int i, bool afterDelay) { bool surroundPanChanged=false; // effects - if (song.resetEffectsOnRowChange) { - chan[i].portaSpeed=-1; - chan[i].portaNote=-1; - dispatchCmd(DivCommand(DIV_CMD_HINT_PORTA,i,CLAMP(chan[i].portaNote,-128,127),MAX(chan[i].portaSpeed,0))); - chan[i].inPorta=false; - if (!song.arpNonPorta) dispatchCmd(DivCommand(DIV_CMD_PRE_PORTA,i,false,0)); - } - for (int j=0; jdata[whatRow][4+(j<<1)]; short effectVal=pat->data[whatRow][5+(j<<1)]; @@ -705,7 +697,6 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x01: // ramp up if (song.ignoreDuplicateSlides && (lastSlide==0x01 || lastSlide==0x1337)) break; - chan[i].lastPorta2=effectVal; lastSlide=0x01; if (effectVal==0) { chan[i].portaNote=-1; @@ -728,7 +719,6 @@ void DivEngine::processRow(int i, bool afterDelay) { break; case 0x02: // ramp down if (song.ignoreDuplicateSlides && (lastSlide==0x02 || lastSlide==0x1337)) break; - chan[i].lastPorta2=effectVal; lastSlide=0x02; if (effectVal==0) { chan[i].portaNote=-1; @@ -750,7 +740,6 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x03: // portamento - chan[i].lastPorta2=effectVal; if (effectVal==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -779,7 +768,6 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x04: // vibrato - chan[i].lastVibrato2=effectVal; if (effectVal) chan[i].lastVibrato=effectVal; chan[i].vibratoDepth=effectVal&15; chan[i].vibratoRate=effectVal>>4; @@ -787,7 +775,6 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); break; case 0x05: // vol slide + vibrato - chan[i].lastVibrato2=effectVal; if (effectVal==0) { chan[i].vibratoDepth=0; chan[i].vibratoRate=0; @@ -814,7 +801,6 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x06: // vol slide + porta - chan[i].lastPorta2=effectVal; if (effectVal==0 || chan[i].lastPorta==0) { chan[i].portaNote=-1; chan[i].portaSpeed=-1; @@ -885,7 +871,6 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); break; case 0x00: // arpeggio - chan[i].lastArp=effectVal; chan[i].arp=effectVal; if (chan[i].arp==0 && song.arp0Reset) { chan[i].resetArp=true; @@ -1139,23 +1124,6 @@ void DivEngine::processRow(int i, bool afterDelay) { } if (chan[i].doNote) { - if (song.resetEffectsOnRowChange) { - if (chan[i].lastArp) { - chan[i].lastArp=0; - } else { - chan[i].arp=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_ARPEGGIO,i,chan[i].arp)); - } - - if (chan[i].lastVibrato2) { - chan[i].lastVibrato2=0; - } else { - chan[i].vibratoDepth=0; - chan[i].vibratoRate=0; - dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); - } - } - if (!song.continuousVibrato) { chan[i].vibratoPos=0; } diff --git a/src/engine/song.h b/src/engine/song.h index 871973900..f9983ba9e 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -331,7 +331,6 @@ struct DivSong { bool resetArpPhaseOnNewNote; bool ceilVolumeScaling; bool oldAlwaysSetVolume; - bool resetEffectsOnRowChange; std::vector ins; std::vector wave; @@ -455,8 +454,7 @@ struct DivSong { oldDPCM(false), resetArpPhaseOnNewNote(false), ceilVolumeScaling(false), - oldAlwaysSetVolume(false), - resetEffectsOnRowChange(false) { + oldAlwaysSetVolume(false) { for (int i=0; isong.resetEffectsOnRowChange); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("when enabled, effects in a row will be canceled after the row changes"); - } ImGui::EndTabItem(); } ImGui::EndTabBar(); From 8a19d6e0cb66df167b8797caec1ce560da3e7d1b Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 14 Apr 2024 20:19:36 +0100 Subject: [PATCH 20/29] Create a 2nd pass, which fixes most of the issues, Speed is partially implemented --- src/engine/fileOps/tfm.cpp | 82 +++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 7c345ee32..696fc5eac 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -160,6 +160,7 @@ struct TFMparsePatternInfo { TFMRLEReader* reader; unsigned char maxPat; unsigned char* patLens; + unsigned char* orderList; bool* patExists; DivSong* ds; int* insNumMaps; @@ -178,6 +179,8 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { // 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; for (int i=0; i<256; i++) { if (i>info.maxPat) break; @@ -190,6 +193,7 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { logD("parsing pattern %d",i); for (int j=0; j<6; j++) { DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; + info.ds->subsong[0]->pat[j].effectCols=3; // notes info.reader->read(patDataBuf,256); @@ -212,14 +216,10 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { } } - // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern lengths + // 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) { - if (pat->data[info.patLens[i]-1][4]==-1 && pat->data[info.patLens[i]-1][5]==-1) { - pat->data[info.patLens[i]-1][4]=0x0D; - pat->data[info.patLens[i]-1][5]=0x00; - } - pat->data[info.patLens[i]][4]=0x0D; - pat->data[info.patLens[i]][5]=0x00; + pat->data[info.patLens[i]-1][8]=0x0D; + pat->data[info.patLens[i]-1][9]=0x00; } // volume info.reader->read(patDataBuf,256); @@ -240,14 +240,11 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { } // effects - unsigned char effectNum[256]; unsigned char effectVal[256]; info.reader->read(effectNum,256); info.reader->read(effectVal,256); - unsigned short lastSlide=0; - unsigned short lastVibrato=0; for (int k=0; k<256; k++) { switch (effectNum[k]) { case 0: @@ -357,6 +354,67 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { if (info.v2) info.reader->skip(1536); } } + + // 2nd pass: fixing pitch slides, arpeggios, etc. so the result doesn't sound weird. + + bool chSlide[6]={false}; + bool chVibrato[6]={false}; + bool chVolumeSlide[6]={false}; + + for (int i=0; isubsong[0]->ordersLen; i++) { + for (int j=0; j<6; j++) { + DivPattern* pat = info.ds->subsong[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]!=0x01 && pat->data[k][4]!=0x02) { + pat->data[k][6]=0x01; + pat->data[k][7]=0; + chSlide[j]=false; + } + + if (chVibrato[j] && pat->data[k][4]!=0x03 && pat->data[k][4]!=0x04 && pat->data[k][0]!=-1) { + pat->data[k][6]=0x04; + pat->data[k][7]=0; + chVibrato[j]=false; + } + if (chVolumeSlide[j] && pat->data[k][4]!=0x0A) { + pat->data[k][6]=0x0A; + pat->data[k][7]=0; + chVolumeSlide[j]=false; + } + + switch (pat->data[k][4]) { + case 1: + case 2: + chSlide[j]=true; + break; + case 3: + case 4: + chVibrato[j]=true; + break; + case 0xA: + chVolumeSlide[j]=true; + break; + case 0xF: + // correct speed + + // if both speeds are equal + if ((pat->data[k][5]>>4)==(pat->data[k][5]&0xF)) { + unsigned char speed=pat->data[k][5]>>4; + pat->data[k][5]=speed; + } + break; + default: + break; + } + } + } + } } bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { @@ -382,7 +440,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { logW("interleave factor is bigger than 8, speed information may be inaccurate"); interleaveFactor=8; } - if (!((speed>>4)^(speed&0xF))) { + if ((speed>>4)==(speed&0xF)) { ds.subsong[0]->speeds.val[0]=speed&0xF; ds.subsong[0]->speeds.len=1; } else { @@ -514,6 +572,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { info.insNumMaps=insNumMaps; info.maxPat=maxPat; info.patExists=patExists; + info.orderList=orderList; info.patLens=patLens; info.reader=&reader; info.v2=false; @@ -705,6 +764,7 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { info.insNumMaps=insNumMaps; info.maxPat=maxPat; info.patExists=patExists; + info.orderList=orderList; info.patLens=patLens; info.reader=&reader; info.v2=true; From 08f2216d50399a2fc6dc0c0bc2d2efa514a0d3f4 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 14 Apr 2024 22:26:38 +0100 Subject: [PATCH 21/29] Fix arpeggio, porta, and vibrato not resetting on loop --- src/engine/fileOps/tfm.cpp | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 696fc5eac..76f72ca6c 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -357,8 +357,9 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { // 2nd pass: fixing pitch slides, arpeggios, etc. so the result doesn't sound weird. - bool chSlide[6]={false}; + bool chArpeggio[6]={false}; bool chVibrato[6]={false}; + bool chPorta[6]={false}; bool chVolumeSlide[6]={false}; for (int i=0; isubsong[0]->ordersLen; i++) { @@ -371,29 +372,33 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { 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]!=0x01 && pat->data[k][4]!=0x02) { - pat->data[k][6]=0x01; + if (chArpeggio[j] && pat->data[k][4]!=0x00 && pat->data[k][0]!=-1) { + pat->data[k][6]=0x00; pat->data[k][7]=0; - chSlide[j]=false; - } - - if (chVibrato[j] && pat->data[k][4]!=0x03 && pat->data[k][4]!=0x04 && pat->data[k][0]!=-1) { + chArpeggio[j]=false; + } else if (chPorta[j] && pat->data[k][4]!=0x03 && pat->data[k][4]!=0x01 && pat->data[k][4]!=0x02) { + pat->data[k][6]=0x03; + pat->data[k][7]=0; + chPorta[j]=false; + } else if (chVibrato[j] && pat->data[k][4]!=0x04 && pat->data[k][0]!=-1) { pat->data[k][6]=0x04; pat->data[k][7]=0; chVibrato[j]=false; - } - if (chVolumeSlide[j] && pat->data[k][4]!=0x0A) { + } else if (chVolumeSlide[j] && pat->data[k][4]!=0x0A) { pat->data[k][6]=0x0A; pat->data[k][7]=0; chVolumeSlide[j]=false; } switch (pat->data[k][4]) { + case 0: + chArpeggio[j]=true; + break; case 1: case 2: - chSlide[j]=true; - break; case 3: + chPorta[j]=true; + break; case 4: chVibrato[j]=true; break; @@ -430,6 +435,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { ds.systemLen=1; ds.system[0]=DIV_SYSTEM_YM2612; + ds.loopModality=1; unsigned char speed=reader.readCNoRLE(); unsigned char interleaveFactor=reader.readCNoRLE(); @@ -616,6 +622,8 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { ds.systemLen=1; ds.system[0]=DIV_SYSTEM_YM2612; + ds.loopModality=1; + unsigned char magic[8]={0}; reader.readNoRLE(magic,8); From 6be1dbdf5ebd5ff9bea127ff703fd5c68fd9c8d8 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 14 Apr 2024 23:21:18 +0100 Subject: [PATCH 22/29] Implemented tildearrow's suggestions --- src/engine/fileOps/tfm.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 76f72ca6c..1fc250bce 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -138,7 +138,12 @@ public: String ret; ret.reserve(l); while (l--) { - ret += readC(); + unsigned char byte=readC(); + if (!byte) { + skip(l); + break; + } + ret += byte; } return ret; } @@ -562,7 +567,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { unsigned char patLens[256]; int maxPatLen=0; reader.read(patLens, 256); - for (int i=0;i<256;i++) { + for (int i=0; i<256; i++) { if (patLens[i]==0) { maxPatLen=256; break; From 77523588a4ad473964a2fcb7891e7903e0ffb956 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Mon, 15 Apr 2024 23:56:49 +0100 Subject: [PATCH 23/29] Fully implement speed --- src/engine/fileOps/tfm.cpp | 120 +++++++++++++++++++++++++++++++------ 1 file changed, 101 insertions(+), 19 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 1fc250bce..c2bb8878f 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -161,18 +161,40 @@ String TFMparseDate(short date) { return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); } -struct TFMparsePatternInfo { +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) { +void TFMParsePattern(struct TFMParsePatternInfo info) { // PATTERN DATA FORMAT (not described properly in the documentation) // for each channel in a pattern: // - note data (256 bytes) @@ -187,6 +209,28 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { 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; + + 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]) { @@ -349,9 +393,50 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { break; } break; - default: - pat->data[k][4]=effectNum[k]; - pat->data[k][5]=effectVal[k]; + 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]=0x0F; + unsigned char speedSet=effectVal[k]>>4; + pat->data[k][5]=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]=0x09; + pat->data[k][5]=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]=0x09; + pat->data[k][5]=speedGrooveIndex; + speedGrooveIndex++; break; } } @@ -410,15 +495,6 @@ void TFMparsePattern(struct TFMparsePatternInfo info) { case 0xA: chVolumeSlide[j]=true; break; - case 0xF: - // correct speed - - // if both speeds are equal - if ((pat->data[k][5]>>4)==(pat->data[k][5]&0xF)) { - unsigned char speed=pat->data[k][5]>>4; - pat->data[k][5]=speed; - } - break; default: break; } @@ -578,16 +654,19 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { ds.subsong[0]->patLen=maxPatLen; - struct TFMparsePatternInfo info; + 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); + TFMParsePattern(info); if (active) quitDispatch(); BUSY_BEGIN_SOFT; @@ -761,7 +840,7 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { unsigned char patLens[256]; int maxPatLen=0; reader.read(patLens, 256); - for (int i=0;i<256;i++) { + for (int i=0; i<256; i++) { if (patLens[i]==0) { maxPatLen=256; break; @@ -772,16 +851,19 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { ds.subsong[0]->patLen=maxPatLen; - struct TFMparsePatternInfo info; + 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); + TFMParsePattern(info); if (active) quitDispatch(); BUSY_BEGIN_SOFT; From 7e57e2db716f08401001cb74325be2521db4e813 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Tue, 16 Apr 2024 13:45:07 +0100 Subject: [PATCH 24/29] Correct volume --- src/engine/fileOps/tfm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index c2bb8878f..046c1b90e 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -276,7 +276,7 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { 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]=0x41+(patDataBuf[k]*2); + else pat->data[k][3]=0x60+patDataBuf[k]; } // instrument From 787bf7f3288d0990bb8849f0add63ac3291330df Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Tue, 16 Apr 2024 22:25:29 +0100 Subject: [PATCH 25/29] Add support for multiple effects --- src/engine/fileOps/tfm.cpp | 370 +++++++++++++++++++------------------ 1 file changed, 188 insertions(+), 182 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 046c1b90e..8f9be72a5 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -202,7 +202,7 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { // - instrument number data (256 bytes) // - effect number (256 bytes, values 0x0-0x23 (to represent 0-F and G-Z)) // - effect value (256 bytes) - // - padding(?) (1536 bytes, always set to 0) (ONLY ON V2) + // - 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]; @@ -216,6 +216,7 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { speed.interleaveFactor=info.interleaveFactor; int speedGrooveIndex=1; + int usedEffectsCol=0; std::unordered_map speeds({{speed, 0}}); // initialize the global groove pattern first @@ -242,7 +243,6 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { logD("parsing pattern %d",i); for (int j=0; j<6; j++) { DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; - info.ds->subsong[0]->pat[j].effectCols=3; // notes info.reader->read(patDataBuf,256); @@ -289,159 +289,163 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { } // effects - unsigned char effectNum[256]; - unsigned char effectVal[256]; - info.reader->read(effectNum,256); - info.reader->read(effectVal,256); - for (int k=0; k<256; k++) { - switch (effectNum[k]) { - case 0: - // arpeggio or no effect (if effect val is 0) - if (effectVal[k]==0) break; - pat->data[k][4]=effectNum[k]; - pat->data[k][5]=effectVal[k]; - break; - case 1: - // pitch slide up - case 2: - // pitch slide down - pat->data[k][4]=effectNum[k]; - if (effectVal[k]) { - lastSlide=effectVal[k]; - pat->data[k][5]=effectVal[k]; - } else { - pat->data[k][5]=lastSlide; - } - break; - case 3: - // portamento - case 4: - // vibrato - pat->data[k][5]=0; - if (effectVal[k]&0xF0) { - pat->data[k][5]|=effectVal[k]&0xF0; - } else { - pat->data[k][5]|=lastVibrato&0xF0; - } - if (effectVal[k]&0x0F) { - pat->data[k][5]|=effectVal[k]&0x0F; - } else { - pat->data[k][5]|=lastVibrato&0x0F; - } - pat->data[k][4]=effectNum[k]; - lastVibrato=pat->data[k][5]; - break; - case 5: - // poramento + volume slide - pat->data[k][4]=0x06; - pat->data[k][5]=effectVal[k]; - break; - case 6: - // vibrato + volume slide - pat->data[k][4]=0x05; - pat->data[k][5]=effectVal[k]; - break; - case 8: - // modify TL of operator 1 - pat->data[k][4]=0x12; - pat->data[k][5]=effectVal[k]; - break; - case 9: - // modify TL of operator 2 - pat->data[k][4]=0x13; - pat->data[k][5]=effectVal[k]; - break; - case 10: - // volume slide - pat->data[k][4]=0xA; - pat->data[k][5]=effectVal[k]; - break; - case 11: - // multi-frequency mode of CH3 control - // TODO - case 12: - // modify TL of operator 3 - pat->data[k][4]=0x14; - pat->data[k][5]=effectVal[k]; - break; - case 13: - // modify TL of operator 2 - pat->data[k][4]=0x15; - pat->data[k][5]=effectVal[k]; - break; - case 14: - switch (effectVal[k]>>4) { + 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: - case 1: - case 2: - case 3: - // modify multiplier of operators - pat->data[k][4]=0x16; - pat->data[k][5]=((effectVal[k]&0xF0)+0x100)|(effectVal[k]&0xF); + // 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 8: - // pan - pat->data[k][4]=0x80; - if ((effectVal[k]&0xF)==1) { - pat->data[k][5]=0; - } else if ((effectVal[k]&0xF)==2) { - pat->data[k][5]=0xFF; + 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]=0x80; + pat->data[k][5+(l*2)]=lastSlide; } 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]=0x0F; - unsigned char speedSet=effectVal[k]>>4; - pat->data[k][5]=speedSet; + 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; - } else { - speed.speedEven=effectVal[k]>>4; - speed.speedOdd=effectVal[k]&0xF; - } + 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 - auto speedIndex = speeds.find(speed); - if (speedIndex != speeds.end()) { - pat->data[k][4]=0x09; - pat->data[k][5]=speedIndex->second; + 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; } - 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]=0x09; - pat->data[k][5]=speedGrooveIndex; - speedGrooveIndex++; - break; } + info.ds->subsong[0]->pat[j].effectCols=(usedEffectsCol*2)+1; } - - if (info.v2) info.reader->skip(1536); } } @@ -454,49 +458,51 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { for (int i=0; isubsong[0]->ordersLen; i++) { for (int j=0; j<6; j++) { - DivPattern* pat = info.ds->subsong[0]->pat[j].data[info.orderList[i]]; + 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; + // 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; + 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]!=0x00 && pat->data[k][0]!=-1) { - pat->data[k][6]=0x00; - pat->data[k][7]=0; - chArpeggio[j]=false; - } else if (chPorta[j] && pat->data[k][4]!=0x03 && pat->data[k][4]!=0x01 && pat->data[k][4]!=0x02) { - pat->data[k][6]=0x03; - pat->data[k][7]=0; - chPorta[j]=false; - } else if (chVibrato[j] && pat->data[k][4]!=0x04 && pat->data[k][0]!=-1) { - pat->data[k][6]=0x04; - pat->data[k][7]=0; - chVibrato[j]=false; - } else if (chVolumeSlide[j] && pat->data[k][4]!=0x0A) { - pat->data[k][6]=0x0A; - pat->data[k][7]=0; - chVolumeSlide[j]=false; - } + 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]) { - 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; + 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; + } } } } From acbd08d5a6e2c08cf75ffb5ebbf962a791b2e98f Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Fri, 19 Apr 2024 18:30:06 +0100 Subject: [PATCH 26/29] Fix "jump to next pattern" not working on songs that have no effects or multiple effects --- src/engine/fileOps/tfm.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 8f9be72a5..8a6880b29 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -265,11 +265,6 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { } } - // 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][8]=0x0D; - pat->data[info.patLens[i]-1][9]=0x00; - } // volume info.reader->read(patDataBuf,256); @@ -445,6 +440,12 @@ void TFMParsePattern(struct TFMParsePatternInfo info) { } } 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; + } } } } From d93b1ba186554b295f032af0d4e1ef1250f5ed8a Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 21 Apr 2024 13:13:25 +0100 Subject: [PATCH 27/29] Fix memory leak (by deallocating the file buffer) --- src/engine/fileOps/tfm.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index 8a6880b29..cebba053d 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -894,6 +894,7 @@ bool DivEngine::loadTFMv2(unsigned char* file, size_t len) { } catch(InvalidHeaderException& e) { lastError="invalid info header!"; } - + + delete[] file; return success; } From de6b5bb381d47518f961bb5de96292b62fdf9999 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Tue, 23 Apr 2024 07:23:08 +0100 Subject: [PATCH 28/29] Fix memory leak in TFMv1 --- src/engine/fileOps/tfm.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index cebba053d..b4535c762 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -698,6 +698,7 @@ bool DivEngine::loadTFMv1(unsigned char* file, size_t len) { lastError="invalid info header!"; } + delete[] file; return success; } From 0f1eee4a0c56ecec0df6d03139563bc817472cce Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Tue, 23 Apr 2024 19:51:35 +0100 Subject: [PATCH 29/29] Reset src/engine/playback.cpp --- src/engine/playback.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 971dac593..1e7d823c2 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -782,7 +782,6 @@ void DivEngine::processRow(int i, bool afterDelay) { chan[i].vibratoDepth=chan[i].lastVibrato&15; chan[i].vibratoRate=chan[i].lastVibrato>>4; } - dispatchCmd(DivCommand(DIV_CMD_HINT_VIBRATO,i,chan[i].vibratoDepth,chan[i].vibratoRate)); dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); // TODO: non-0x-or-x0 value should be treated as 00 @@ -1127,7 +1126,6 @@ void DivEngine::processRow(int i, bool afterDelay) { if (!song.continuousVibrato) { chan[i].vibratoPos=0; } - dispatchCmd(DivCommand(DIV_CMD_PITCH,i,chan[i].pitch+(((chan[i].vibratoDepth*vibTable[chan[i].vibratoPos]*chan[i].vibratoFine)>>4)/15))); if (chan[i].legato && (!chan[i].inPorta || song.brokenPortaLegato)) { dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); @@ -1148,7 +1146,7 @@ void DivEngine::processRow(int i, bool afterDelay) { dispatchCmd(DivCommand(DIV_CMD_NOTE_ON,i,chan[i].note,chan[i].volume>>8)); chan[i].releasing=false; if (song.resetArpPhaseOnNewNote) { - chan[i].arpStage=-1; + chan[i].arpStage=-1; } chan[i].goneThroughNote=true; chan[i].wentThroughNote=true; @@ -1156,7 +1154,6 @@ void DivEngine::processRow(int i, bool afterDelay) { } } chan[i].doNote=false; - if (!chan[i].keyOn && chan[i].scheduledSlideReset) { chan[i].portaNote=-1; chan[i].portaSpeed=-1;