From 6b70749c52dcb22220b6e8a864587c6ddff82901 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 10 May 2025 12:32:34 +0100 Subject: [PATCH 1/9] Seperate the TFM RLE Reader and primitives into a different header file --- src/engine/fileOps/tfm.cpp | 164 +---------------------------- src/engine/fileOps/tfmCommon.h | 187 +++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 163 deletions(-) create mode 100644 src/engine/fileOps/tfmCommon.h diff --git a/src/engine/fileOps/tfm.cpp b/src/engine/fileOps/tfm.cpp index fc3665dd29..7553dd3c75 100644 --- a/src/engine/fileOps/tfm.cpp +++ b/src/engine/fileOps/tfm.cpp @@ -17,169 +17,7 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include "fileOpsCommon.h" - -class TFMRLEReader; - -struct TFMEndOfFileException { - TFMRLEReader* reader; - size_t finalSize; - TFMEndOfFileException(TFMRLEReader* r, size_t fs): - reader(r), - finalSize(fs) {} -}; - - -class TFMRLEReader { - const unsigned char* buf; - size_t len; - size_t curSeek; - bool inTag; - int tagLenLeft; - signed char tagChar; - - void decodeRLE(unsigned char prevChar) { - int lenShift=0; - tagLenLeft=0; - unsigned char rleTag=0; - - do { - rleTag=readCNoRLE(); - tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); - - unsigned char ret=buf[curSeek++]; - - // MISLEADING DOCUMENTATION: while TFM music maker's documentation says if the next byte - // is zero, then it's not a tag but just 0x80 (for example: 0x00 0x80 0x00 = 0x00 0x80) - // this is actually wrong - // through research and experimentation, there are times that TFM music maker - // will use 0x80 0x00 for actual tags (for example: 0x00 0x80 0x00 0x84 = 512 times 0x00 - // in certain parts of the header and footer) - // TFM music maker actually uses double 0x80 to escape the 0x80 - // for example: 0xDA 0x80 0x80 0x00 0x23 = 0xDA 0x80 0x00 0x23) - if (ret==0x80) { - decodeRLE(buf[curSeek-2]); - tagLenLeft--; - return tagChar; - } - return ret; - } - - signed char readCNoRLE() { - if (curSeek>len) throw TFMEndOfFileException(this,len); - return buf[curSeek++]; - } - - void read(unsigned char* b, size_t l) { - int i=0; - while(l--) { - unsigned char nextChar=readC(); - b[i++]=nextChar; - //logD("read next char: %x, index: %d",nextChar,i); - } - } - - void readNoRLE(unsigned char *b, size_t l) { - int i=0; - while (l--) { - b[i++]=buf[curSeek++]; - if (curSeek>len) throw TFMEndOfFileException(this,len); - } - } - - short readS() { - return readC()|readC()<<8; - } - - short readSNoRLE() { - if (curSeek+2>len) throw TFMEndOfFileException(this,len); - short ret=buf[curSeek]|buf[curSeek+1]<<8; - curSeek+=2; - return ret; - } - - String readString(size_t l) { - String ret; - ret.reserve(l); - while (l--) { - unsigned char byte=readC(); - if (!byte) { - skip(l); - break; - } - ret += byte; - } - return ret; - } - void skip(size_t l) { - // quick and dirty - while (l--) { - //logD("skipping l %d",l); - readC(); - } - } - -}; - -String TFMparseDate(short date) { - return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); -} - -struct TFMSpeed { - unsigned char speedEven; - unsigned char speedOdd; - unsigned char interleaveFactor; - - bool operator==(const TFMSpeed &s) const { - return speedEven==s.speedEven && speedOdd==s.speedOdd && interleaveFactor==s.interleaveFactor; - } -}; - -// to make it work with map -namespace std { - template<> struct hash - { - size_t operator()(const TFMSpeed& s) const noexcept { - return s.speedEven<<16|s.speedOdd<<8|s.interleaveFactor; - } - }; -} +#include "tfmCommon.h" struct TFMParsePatternInfo { TFMRLEReader* reader; diff --git a/src/engine/fileOps/tfmCommon.h b/src/engine/fileOps/tfmCommon.h new file mode 100644 index 0000000000..16ef99a9dc --- /dev/null +++ b/src/engine/fileOps/tfmCommon.h @@ -0,0 +1,187 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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. + */ + +#ifndef TFM_COMMON_H +#define TFM_COMMON_H + +#include "fileOpsCommon.h" + +class TFMRLEReader; + +struct TFMEndOfFileException { + TFMRLEReader* reader; + size_t finalSize; + TFMEndOfFileException(TFMRLEReader* r, size_t fs): + reader(r), + finalSize(fs) {} +}; + + +class TFMRLEReader { + const unsigned char* buf; + size_t len; + size_t curSeek; + bool inTag; + int tagLenLeft; + signed char tagChar; + + void decodeRLE(unsigned char prevChar) { + int lenShift=0; + tagLenLeft=0; + unsigned char rleTag=0; + + do { + rleTag=readCNoRLE(); + tagLenLeft|=(rleTag&0x7F)<len) throw TFMEndOfFileException(this,len); + + unsigned char ret=buf[curSeek++]; + + // MISLEADING DOCUMENTATION: while TFM music maker's documentation says if the next byte + // is zero, then it's not a tag but just 0x80 (for example: 0x00 0x80 0x00 = 0x00 0x80) + // this is actually wrong + // through research and experimentation, there are times that TFM music maker + // will use 0x80 0x00 for actual tags (for example: 0x00 0x80 0x00 0x84 = 512 times 0x00 + // in certain parts of the header and footer) + // TFM music maker actually uses double 0x80 to escape the 0x80 + // for example: 0xDA 0x80 0x80 0x00 0x23 = 0xDA 0x80 0x00 0x23) + if (ret==0x80) { + decodeRLE(buf[curSeek-2]); + tagLenLeft--; + return tagChar; + } + return ret; + } + + signed char readCNoRLE() { + if (curSeek>len) throw TFMEndOfFileException(this,len); + return buf[curSeek++]; + } + + void read(unsigned char* b, size_t l) { + int i=0; + while(l--) { + unsigned char nextChar=readC(); + b[i++]=nextChar; + //logD("read next char: %x, index: %d",nextChar,i); + } + } + + void readNoRLE(unsigned char *b, size_t l) { + int i=0; + while (l--) { + b[i++]=buf[curSeek++]; + if (curSeek>len) throw TFMEndOfFileException(this,len); + } + } + + short readS() { + return readC()|readC()<<8; + } + + short readSNoRLE() { + if (curSeek+2>len) throw TFMEndOfFileException(this,len); + short ret=buf[curSeek]|buf[curSeek+1]<<8; + curSeek+=2; + return ret; + } + + String readString(size_t l) { + String ret; + ret.reserve(l); + while (l--) { + unsigned char byte=readC(); + if (!byte) { + skip(l); + break; + } + ret += byte; + } + return ret; + } + void skip(size_t l) { + // quick and dirty + while (l--) { + //logD("skipping l %d",l); + readC(); + } + } + +}; + +String TFMparseDate(short date) { + return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); +} + +struct TFMSpeed { + unsigned char speedEven; + unsigned char speedOdd; + unsigned char interleaveFactor; + + bool operator==(const TFMSpeed &s) const { + return speedEven==s.speedEven && speedOdd==s.speedOdd && interleaveFactor==s.interleaveFactor; + } +}; + +// to make it work with map +namespace std { + template<> struct hash + { + size_t operator()(const TFMSpeed& s) const noexcept { + return s.speedEven<<16|s.speedOdd<<8|s.interleaveFactor; + } + }; +} + +#endif From e17b0f8ebe2690ca8c3b573449686df4be4e7857 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 10 May 2025 13:00:52 +0100 Subject: [PATCH 2/9] Write a preliminary VGM Music Maker parser --- src/engine/fileOps/fileOpsCommon.h | 1 + src/engine/fileOps/tfmCommon.h | 12 + src/engine/fileOps/vge.cpp | 598 +++++++++++++++++++++++++++++ 3 files changed, 611 insertions(+) create mode 100644 src/engine/fileOps/vge.cpp diff --git a/src/engine/fileOps/fileOpsCommon.h b/src/engine/fileOps/fileOpsCommon.h index 9a42c370cb..d661f55e65 100644 --- a/src/engine/fileOps/fileOpsCommon.h +++ b/src/engine/fileOps/fileOpsCommon.h @@ -56,6 +56,7 @@ struct NotZlibException { #define DIV_XM_MAGIC "Extended Module: " #define DIV_IT_MAGIC "IMPM" #define DIV_TFM_MAGIC "TFMfmtV2" +#define DIV_VGE_MAGIC "VGEfmtV3" #define DIV_FUR_MAGIC_DS0 "Furnace-B module" diff --git a/src/engine/fileOps/tfmCommon.h b/src/engine/fileOps/tfmCommon.h index 16ef99a9dc..0916fff4c6 100644 --- a/src/engine/fileOps/tfmCommon.h +++ b/src/engine/fileOps/tfmCommon.h @@ -150,6 +150,18 @@ class TFMRLEReader { } return ret; } + + unsigned int readI() { + return readC()|readC()<<8|readC()<<16|readC()<<24; + } + + short readINoRLE() { + if (curSeek+4>len) throw TFMEndOfFileException(this,len); + short ret=buf[curSeek]|buf[curSeek+1]<<8|buf[curSeek+2]<<16|buf[curSeek+3]<<24; + curSeek+=4; + return ret; + } + void skip(size_t l) { // quick and dirty while (l--) { diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp new file mode 100644 index 0000000000..8de61c8923 --- /dev/null +++ b/src/engine/fileOps/vge.cpp @@ -0,0 +1,598 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 "tfmCommon.h" + +struct VGEParsePatternInfo { + 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; + unsigned char loopPos; +}; + +void VGEParsePattern(struct VGEParsePatternInfo info) { + // PATTERN DATA FORMAT (not described properly in the documentation) + // for each channel in a pattern: + // - note data (256 bytes) + // - volume data (256 bytes, values always 0x00-0x1F) + // - instrument number data (256 bytes) + // - effect number (256 bytes, values 0x0-0x23 (to represent 0-F and G-Z)) + // - effect value (256 bytes) + // - extra 3 effects (1536 bytes 256x3x2) (ONLY ON V2) + // notes are stored as an inverted value of note+octave*12 + // key-offs are stored in the note data as 0x01 + unsigned char patDataBuf[256]; + unsigned short lastSlide=0; + unsigned short lastVibrato=0; + + struct TFMSpeed speed; + DivGroovePattern groove; + speed.speedEven=info.speedEven; + speed.speedOdd=info.speedOdd; + speed.interleaveFactor=info.interleaveFactor; + int speedGrooveIndex=1; + + int usedEffectsCol=0; + std::unordered_map speeds({{speed, 0}}); + + // initialize the global groove pattern first + if (speed.interleaveFactor>8) { + logW("speed interleave factor is bigger than 8, speed information may be inaccurate"); + speed.interleaveFactor=8; + } + for (int i=0; igrooves.push_back(groove); + + for (int i=0; i<256; i++) { + if (i>info.maxPat) break; + else if (!info.patExists[i]) { + logD("skipping pattern %d",i); + info.reader->skip((info.v2) ? 16896 : 7680); + continue; + } + + logD("parsing pattern %d",i); + for (int j=0; j<6; j++) { + DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; + + // notes + info.reader->read(patDataBuf,256); + + logD("parsing notes of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + else if (patDataBuf[k]==1) { + // note off + pat->data[k][0]=100; + } else { + unsigned char invertedNote=~patDataBuf[k]; + pat->data[k][0]=invertedNote%12; + pat->data[k][1]=(invertedNote/12)-1; + + if (pat->data[k][0]==0) { + pat->data[k][0]=12; + pat->data[k][1]--; + } + } + } + + // volume + info.reader->read(patDataBuf,256); + + logD("parsing volumes of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + else pat->data[k][3]=0x60+patDataBuf[k]; + } + + // instrument + info.reader->read(patDataBuf,256); + + logD("parsing instruments of pattern %d channel %d",i,j); + for (int k=0; k<256; k++) { + if (patDataBuf[k]==0) continue; + pat->data[k][2]=info.insNumMaps[patDataBuf[k]-1]; + } + + // effects + + int numEffectsCol=(info.v2) ? 4 : 1; + for (int l=0; lread(effectNum,256); + info.reader->read(effectVal,256); + + for (int k=0; k<256; k++) { + if (effectNum[k] || effectVal[k]) usedEffectsCol=l+1; + switch (effectNum[k]) { + case 0: + // arpeggio or no effect (if effect val is 0) + if (effectVal[k]==0) break; + pat->data[k][4+(l*2)]=effectNum[k]; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 1: + // pitch slide up + case 2: + // pitch slide down + pat->data[k][4+(l*2)]=effectNum[k]; + if (effectVal[k]) { + lastSlide=effectVal[k]; + pat->data[k][5+(l*2)]=effectVal[k]; + } else { + pat->data[k][5+(l*2)]=lastSlide; + } + break; + case 3: + // portamento + case 4: + // vibrato + pat->data[k][5+(l*2)]=0; + if (effectVal[k]&0xF0) { + pat->data[k][5+(l*2)]|=effectVal[k]&0xF0; + } else { + pat->data[k][5+(l*2)]|=lastVibrato&0xF0; + } + if (effectVal[k]&0x0F) { + pat->data[k][5+(l*2)]|=effectVal[k]&0x0F; + } else { + pat->data[k][5+(l*2)]|=lastVibrato&0x0F; + } + pat->data[k][4+(l*2)]=effectNum[k]; + lastVibrato=pat->data[k][5+(l*2)]; + break; + case 5: + // poramento + volume slide + pat->data[k][4+(l*2)]=0x06; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 6: + // vibrato + volume slide + pat->data[k][4+(l*2)]=0x05; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 8: + // modify TL of operator 1 + pat->data[k][4+(l*2)]=0x12; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 9: + // modify TL of operator 2 + pat->data[k][4+(l*2)]=0x13; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 10: + // volume slide + pat->data[k][4+(l*2)]=0xA; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 11: + // multi-frequency mode of CH3 control + // TODO + case 12: + // modify TL of operator 3 + pat->data[k][4+(l*2)]=0x14; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 13: + // modify TL of operator 4 + pat->data[k][4+(l*2)]=0x15; + pat->data[k][5+(l*2)]=effectVal[k]; + break; + case 14: + switch (effectVal[k]>>4) { + case 0: + case 1: + case 2: + case 3: + // modify multiplier of operators + pat->data[k][4+(l*2)]=0x16; + pat->data[k][5+(l*2)]=((effectVal[k]&0xF0)+0x100)|(effectVal[k]&0xF); + break; + case 8: + // pan + pat->data[k][4+(l*2)]=0x80; + if ((effectVal[k]&0xF)==1) { + pat->data[k][5+(l*2)]=0; + } else if ((effectVal[k]&0xF)==2) { + pat->data[k][5+(l*2)]=0xFF; + } else { + pat->data[k][5+(l*2)]=0x80; + } + break; + } + break; + case 15: + // speed + + if (effectVal[k]==0) { + // if speed is set to zero (reset to global values) + speed.speedEven=info.speedEven; + speed.speedOdd=info.speedOdd; + speed.interleaveFactor=info.interleaveFactor; + } else if (effectVal[k]>>4==0) { + // if the top nibble is set to zero (set interleave factor) + speed.interleaveFactor=effectVal[k]&0xF; + } else if ((effectVal[k]>>4)==(effectVal[k]&0xF)) { + // if both speeds are equal + pat->data[k][4+(l*2)]=0x0F; + unsigned char speedSet=effectVal[k]>>4; + pat->data[k][5+(l*2)]=speedSet; + break; + } else { + speed.speedEven=effectVal[k]>>4; + speed.speedOdd=effectVal[k]&0xF; + } + + auto speedIndex = speeds.find(speed); + if (speedIndex != speeds.end()) { + pat->data[k][4+(l*2)]=0x09; + pat->data[k][5+(l*2)]=speedIndex->second; + break; + } + if (speed.interleaveFactor>8) { + logW("speed interleave factor is bigger than 8, speed information may be inaccurate"); + speed.interleaveFactor=8; + } + for (int i=0; igrooves.push_back(groove); + speeds[speed]=speedGrooveIndex; + + pat->data[k][4+(l*2)]=0x09; + pat->data[k][5+(l*2)]=speedGrooveIndex; + speedGrooveIndex++; + break; + } + } + info.ds->subsong[0]->pat[j].effectCols=(usedEffectsCol*2)+1; + + // put a "jump to next pattern" effect if the pattern is smaller than the maximum pattern length + if (info.patLens[i]!=0 && info.patLens[i]subsong[0]->patLen) { + pat->data[info.patLens[i]-1][4+(usedEffectsCol*4)]=0x0D; + pat->data[info.patLens[i]-1][5+(usedEffectsCol*4)]=0x00; + } + } + } + } + + // 2nd pass: fixing pitch slides, arpeggios, etc. so the result doesn't sound weird. + + bool chArpeggio[6]={false}; + bool chVibrato[6]={false}; + bool chPorta[6]={false}; + bool chVolumeSlide[6]={false}; + int lastPatSeen=0; + + for (int i=0; isubsong[0]->ordersLen; i++) { + // this is if the last pattern is used more than once + if (info.orderList[i] == info.orderList[info.ds->subsong[0]->ordersLen - 1]) { + lastPatSeen++; + } + for (int j=0; j<6; j++) { + for (int l=0; lsubsong[0]->pat[j].data[info.orderList[i]]; + unsigned char truePatLen=(info.patLens[info.orderList[i]]subsong[0]->patLen) ? info.patLens[info.orderList[i]] : info.ds->subsong[0]->patLen; + + // default instrument + if (i==0 && pat->data[0][2]==-1) pat->data[0][2]=0; + + for (int k=0; kdata[k][4+(l*2)]!=0x00 && pat->data[k][0]!=-1) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x00; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chArpeggio[j]=false; + } else if (chPorta[j] && pat->data[k][4+(l*2)]!=0x03 && pat->data[k][4+(l*2)]!=0x01 && pat->data[k][4+(l*2)]!=0x02) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x03; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chPorta[j]=false; + } else if (chVibrato[j] && pat->data[k][4+(l*2)]!=0x04 && pat->data[k][0]!=-1) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x04; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chVibrato[j]=false; + } else if (chVolumeSlide[j] && pat->data[k][4+(l*2)]!=0x0A) { + pat->data[k][4+usedEffectsCol*2+(l*2)]=0x0A; + pat->data[k][5+usedEffectsCol*2+(l*2)]=0; + chVolumeSlide[j]=false; + } + + switch (pat->data[k][4+l]) { + case 0: + chArpeggio[j]=true; + break; + case 1: + case 2: + case 3: + chPorta[j]=true; + break; + case 4: + chVibrato[j]=true; + break; + case 0xA: + chVolumeSlide[j]=true; + break; + default: + break; + } + } + } + } + } + + if (lastPatSeen>1) { + // clone the last pattern + info.maxPat++; + for (int i=0;i<6;i++) { + int lastPatNum=info.ds->subsong[0]->orders.ord[i][info.ds->subsong[0]->ordersLen - 1]; + DivPattern* newPat=info.ds->subsong[0]->pat[i].getPattern(info.maxPat,true); + DivPattern* lastPat=info.ds->subsong[0]->pat[i].getPattern(lastPatNum, false); + lastPat->copyOn(newPat); + + info.ds->subsong[0]->orders.ord[i][info.ds->subsong[0]->ordersLen - 1] = info.maxPat; + newPat->data[info.patLens[lastPatNum]-1][4+(usedEffectsCol*4)] = 0x0B; + newPat->data[info.patLens[lastPatNum]-1][5+(usedEffectsCol*4)] = info.loopPos; + info.ds->subsong[0]->pat[i].data[info.maxPat] = newPat; + } + } else { + for (int i=0;i<6;i++) { + int lastPatNum=info.ds->subsong[0]->orders.ord[i][info.ds->subsong[0]->ordersLen - 1]; + DivPattern* lastPat=info.ds->subsong[0]->pat[i].getPattern(lastPatNum, false); + lastPat->data[info.patLens[lastPatNum]-1][4+(usedEffectsCol*4)] = 0x0B; + lastPat->data[info.patLens[lastPatNum]-1][5+(usedEffectsCol*4)] = info.loopPos; + } + } +} + +bool DivEngine::loadVGE(unsigned char* file, size_t len) { + struct InvalidHeaderException {}; + bool success=false; + TFMRLEReader reader=TFMRLEReader(file,len); + + try { + DivSong ds; + ds.version=DIV_VERSION_TFE; + ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; + ds.systemLen=2; + + ds.system[0]=DIV_SYSTEM_YM2612; + ds.system[1]=DIV_SYSTEM_SMS; + ds.loopModality=1; + + unsigned char magic[8]={0}; + + reader.readNoRLE(magic,8); + if (memcmp(magic,DIV_VGE_MAGIC,8)!=0) throw InvalidHeaderException(); + + // Read the size of the header + unsigned int headerSize=reader.readINoRLE(); + unsigned int sampleDescSize=reader.readINoRLE(); + + // Read the clock rates + ds.systemFlags[0].set("customClock", reader.readINoRLE()); + ds.systemFlags[1].set("customClock", reader.readINoRLE()); + ds.subsong[0]->hz = reader.readSNoRLE(); + + unsigned char speedEven=reader.readCNoRLE(); + unsigned char speedOdd=reader.readCNoRLE(); + unsigned char interleaveFactor=reader.readCNoRLE(); + + // TODO: due to limitations with the groove pattern, only interleave factors up to 8 + // are allowed in furnace + if (interleaveFactor>8) { + addWarning("interleave factor is bigger than 8, speed information may be inaccurate"); + interleaveFactor=8; + } + + if (speedEven==speedOdd) { + ds.subsong[0]->speeds.val[0]=speedEven; + ds.subsong[0]->speeds.len=1; + } else { + for (int i=0; ispeeds.val[i]=speedEven; + ds.subsong[0]->speeds.val[i+interleaveFactor]=speedOdd; + } + ds.subsong[0]->speeds.len=interleaveFactor*2; + } + + unsigned short globalPCMQuality=reader.readSNoRLE(); + + ds.createdDate=TFMparseDate(reader.readSNoRLE()); + ds.revisionDate=TFMparseDate(reader.readSNoRLE()); + + // TODO: use this for something, number of saves + (void)reader.readSNoRLE(); + + ds.subsong[0]->ordersLen=reader.readCNoRLE(); + + // order loop position + unsigned char loopPos = reader.readCNoRLE(); + + // author + logD("parsing author"); + ds.author=reader.readString(64); + + // name + logD("parsing name"); + ds.name=reader.readString(64); + + // notes + logD("parsing notes"); + String notes=reader.readString(384); + + // fix \r\n to \n + for (auto& c : notes) { + if (c=='\r') { + notes.erase(c,1); + } + } + + // order list + logD("parsing order list"); + unsigned char orderList[256]; + reader.read(orderList,256); + + bool patExists[256]; + unsigned char maxPat=0; + for (int i=0; iordersLen; i++) { + patExists[orderList[i]]=true; + if (maxPatorders.ord[j][i]=orderList[i]; + ds.subsong[0]->pat[j].data[orderList[i]]=new DivPattern; + } + } + + DivInstrument* insMaps[256]; + int insNumMaps[256]; + + // instrument names + logD("parsing instruments"); + unsigned char insName[16]; + int insCount=0; + for (int i=0; i<255; i++) { + reader.read(insName,16); + + if (memcmp(insName,"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF",16)==0) { + logD("instrument unused"); + insNumMaps[i]=i; + insMaps[i]=NULL; + continue; + } + + DivInstrument* ins=new DivInstrument; + ins->type=DIV_INS_FM; + ins->name=String((const char*)insName,strnlen((const char*)insName,16)); + ds.ins.push_back(ins); + + insNumMaps[i]=insCount; + insCount++; + + insMaps[i]=ins; + } + + ds.insLen=insCount; + + // instrument data + for (int i=0; i<255; i++) { + if (!insMaps[i]) { + reader.skip(42); + continue; + } + + insMaps[i]->fm.alg=reader.readC(); + insMaps[i]->fm.fb=reader.readC(); + + for (int j=0; j<4; j++) { + insMaps[i]->fm.op[j].mult=reader.readC(); + insMaps[i]->fm.op[j].dt=reader.readC(); + insMaps[i]->fm.op[j].tl=reader.readC()^0x7F; + insMaps[i]->fm.op[j].rs=reader.readC(); + insMaps[i]->fm.op[j].ar=reader.readC()^0x1F; + unsigned char dr=reader.readC()^0x1F; + insMaps[i]->fm.op[j].dr=dr&0x7F; + insMaps[i]->fm.op[j].am=dr>>7; + 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(); + } + } + + // sample instrument data + // TODO: actually implement this. + reader.skip((255 * 16) + (255 * 4)); + + 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 VGEParsePatternInfo 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; + info.loopPos=loopPos; + VGEParsePattern(info); + + if (active) quitDispatch(); + BUSY_BEGIN_SOFT; + saveLock.lock(); + song.unload(); + song=ds; + changeSong(0); + recalcChans(); + saveLock.unlock(); + BUSY_END; + if (active) { + initDispatch(); + BUSY_BEGIN; + renderSamples(); + reset(); + BUSY_END; + } + success=true; + } catch(TFMEndOfFileException& e) { + lastError="incomplete file!"; + } catch(InvalidHeaderException& e) { + lastError="invalid info header!"; + } + + delete[] file; + return success; +} From a2d817724c6b7e69f60ffc7883a2441912802a17 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 10 May 2025 20:12:09 +0100 Subject: [PATCH 3/9] Some progress on VGE parsing --- src/engine/fileOps/fileOpsCommon.cpp | 6 +++-- src/engine/fileOps/tfmCommon.cpp | 24 ++++++++++++++++++++ src/engine/fileOps/tfmCommon.h | 10 ++++---- src/engine/fileOps/vge.cpp | 34 +++++++++++++++------------- 4 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 src/engine/fileOps/tfmCommon.cpp diff --git a/src/engine/fileOps/fileOpsCommon.cpp b/src/engine/fileOps/fileOpsCommon.cpp index 023f7f1ff7..301f19783e 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -141,7 +141,7 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { // step 2: try loading as .fur, .dmf, or another magic-ful format if (memcmp(file,DIV_DMF_MAGIC,16)==0) { - return loadDMF(file,len); + return loadDMF(file,len); } else if (memcmp(file,DIV_FTM_MAGIC,18)==0) { return loadFTM(file,len,(extS==".dnm"),false,(extS==".eft")); } else if (memcmp(file,DIV_DNM_MAGIC,21)==0) { @@ -156,6 +156,8 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { return loadTFMv2(file,len); } else if (memcmp(file,DIV_IT_MAGIC,4)==0) { return loadIT(file,len); + } else if (memcmp(file,DIV_VGE_MAGIC,4)==0) { + return loadVGE(file,len); } else if (len>=48) { if (memcmp(&file[0x2c],DIV_S3M_MAGIC,4)==0) { return loadS3M(file,len); @@ -171,7 +173,7 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { delete[] f; return true; } - + // step 4: not a valid file logE("not a valid module!"); lastError="not a compatible song"; diff --git a/src/engine/fileOps/tfmCommon.cpp b/src/engine/fileOps/tfmCommon.cpp new file mode 100644 index 0000000000..877d8917a9 --- /dev/null +++ b/src/engine/fileOps/tfmCommon.cpp @@ -0,0 +1,24 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 "tfmCommon.h" + +String TFMparseDate(short date) { + return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); +} diff --git a/src/engine/fileOps/tfmCommon.h b/src/engine/fileOps/tfmCommon.h index 0916fff4c6..14a8aaeb5a 100644 --- a/src/engine/fileOps/tfmCommon.h +++ b/src/engine/fileOps/tfmCommon.h @@ -151,13 +151,13 @@ class TFMRLEReader { return ret; } - unsigned int readI() { + int readI() { return readC()|readC()<<8|readC()<<16|readC()<<24; } - short readINoRLE() { + int readINoRLE() { if (curSeek+4>len) throw TFMEndOfFileException(this,len); - short ret=buf[curSeek]|buf[curSeek+1]<<8|buf[curSeek+2]<<16|buf[curSeek+3]<<24; + int ret=buf[curSeek]|buf[curSeek+1]<<8|buf[curSeek+2]<<16|buf[curSeek+3]<<24; curSeek+=4; return ret; } @@ -172,9 +172,7 @@ class TFMRLEReader { }; -String TFMparseDate(short date) { - return fmt::sprintf("%02d.%02d.%02d",date>>11,(date>>7)&0xF,date&0x7F); -} +String TFMparseDate(short date); struct TFMSpeed { unsigned char speedEven; diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index 8de61c8923..b1ea5b7c71 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -30,7 +30,6 @@ struct VGEParsePatternInfo { bool* patExists; DivSong* ds; int* insNumMaps; - bool v2; unsigned char loopPos; }; @@ -42,7 +41,7 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { // - instrument number data (256 bytes) // - effect number (256 bytes, values 0x0-0x23 (to represent 0-F and G-Z)) // - effect value (256 bytes) - // - extra 3 effects (1536 bytes 256x3x2) (ONLY ON V2) + // - extra 3 effects (1536 bytes 256x3x2) // 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]; @@ -76,12 +75,12 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { if (i>info.maxPat) break; else if (!info.patExists[i]) { logD("skipping pattern %d",i); - info.reader->skip((info.v2) ? 16896 : 7680); + info.reader->skip((256 * 11) * 10); continue; } logD("parsing pattern %d",i); - for (int j=0; j<6; j++) { + for (int j=0; j<10; j++) { DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; // notes @@ -125,7 +124,7 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { // effects - int numEffectsCol=(info.v2) ? 4 : 1; + int numEffectsCol=4; for (int l=0; lsubsong[0]->ordersLen; i++) { @@ -305,7 +304,7 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { if (info.orderList[i] == info.orderList[info.ds->subsong[0]->ordersLen - 1]) { lastPatSeen++; } - for (int j=0; j<6; j++) { + for (int j=0; j<10; j++) { for (int l=0; lsubsong[0]->pat[j].data[info.orderList[i]]; unsigned char truePatLen=(info.patLens[info.orderList[i]]subsong[0]->patLen) ? info.patLens[info.orderList[i]] : info.ds->subsong[0]->patLen; @@ -358,7 +357,7 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { if (lastPatSeen>1) { // clone the last pattern info.maxPat++; - for (int i=0;i<6;i++) { + for (int i=0;i<10;i++) { int lastPatNum=info.ds->subsong[0]->orders.ord[i][info.ds->subsong[0]->ordersLen - 1]; DivPattern* newPat=info.ds->subsong[0]->pat[i].getPattern(info.maxPat,true); DivPattern* lastPat=info.ds->subsong[0]->pat[i].getPattern(lastPatNum, false); @@ -370,7 +369,7 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { info.ds->subsong[0]->pat[i].data[info.maxPat] = newPat; } } else { - for (int i=0;i<6;i++) { + for (int i=0;i<10;i++) { int lastPatNum=info.ds->subsong[0]->orders.ord[i][info.ds->subsong[0]->ordersLen - 1]; DivPattern* lastPat=info.ds->subsong[0]->pat[i].getPattern(lastPatNum, false); lastPat->data[info.patLens[lastPatNum]-1][4+(usedEffectsCol*4)] = 0x0B; @@ -473,7 +472,7 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { patExists[orderList[i]]=true; if (maxPatorders.ord[j][i]=orderList[i]; ds.subsong[0]->pat[j].data[orderList[i]]=new DivPattern; } @@ -512,12 +511,15 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { // instrument data for (int i=0; i<255; i++) { if (!insMaps[i]) { - reader.skip(42); + reader.skip(43); continue; } insMaps[i]->fm.alg=reader.readC(); insMaps[i]->fm.fb=reader.readC(); + unsigned char ams_fms=reader.readC(); + insMaps[i]->fm.fms = ams_fms&0xF; + insMaps[i]->fm.ams = ams_fms>>4; for (int j=0; j<4; j++) { insMaps[i]->fm.op[j].mult=reader.readC(); @@ -537,7 +539,8 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { // sample instrument data // TODO: actually implement this. - reader.skip((255 * 16) + (255 * 4)); + reader.skip(254 * 16); + reader.skip(254 * 4); ds.notes=notes; @@ -566,7 +569,6 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { info.interleaveFactor=interleaveFactor; info.patLens=patLens; info.reader=&reader; - info.v2=true; info.loopPos=loopPos; VGEParsePattern(info); From 56a01956a0c79379e0a5de1dc36bb5e9965d0217 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 10 May 2025 20:12:32 +0100 Subject: [PATCH 4/9] Add loadVGE to engine.h and the source files to CMakeLists --- CMakeLists.txt | 2 ++ src/engine/engine.h | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a26de4a0b..84b67a31f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -694,7 +694,9 @@ src/engine/fileOps/it.cpp src/engine/fileOps/mod.cpp src/engine/fileOps/s3m.cpp src/engine/fileOps/text.cpp +src/engine/fileOps/tfmCommon.cpp src/engine/fileOps/tfm.cpp +src/engine/fileOps/vge.cpp src/engine/fileOps/xm.cpp src/engine/fileOps/p.cpp diff --git a/src/engine/engine.h b/src/engine/engine.h index e630def7a9..7e62b5cc8e 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -608,6 +608,7 @@ class DivEngine { bool loadFC(unsigned char* file, size_t len); bool loadTFMv1(unsigned char* file, size_t len); bool loadTFMv2(unsigned char* file, size_t len); + bool loadVGE(unsigned char* file, size_t len); void loadDMP(SafeReader& reader, std::vector& ret, String& stripPath); void loadTFI(SafeReader& reader, std::vector& ret, String& stripPath); @@ -623,7 +624,7 @@ class DivEngine { void loadFF(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPL(SafeReader& reader, std::vector& ret, String& stripPath); void loadWOPN(SafeReader& reader, std::vector& ret, String& stripPath); - + //sample banks void loadP(SafeReader& reader, std::vector& ret, String& stripPath); void loadPPC(SafeReader& reader, std::vector& ret, String& stripPath); From 2e20605610ce7c0fe16e4b697d091f05bed973e7 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 10 May 2025 20:20:58 +0100 Subject: [PATCH 5/9] VGE file parsing now working, but no samples --- src/engine/fileOps/vge.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index b1ea5b7c71..49a995c25e 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -539,8 +539,8 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { // sample instrument data // TODO: actually implement this. - reader.skip(254 * 16); - reader.skip(254 * 4); + reader.skip(255 * 16); + reader.skip(255 * 4); ds.notes=notes; From 253a9453e8fe524b9e0aec316224d5a97baffbc1 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 11 May 2025 14:53:25 +0100 Subject: [PATCH 6/9] Add support for VGEv1 and VGEv2 --- src/engine/fileOps/fileOpsCommon.cpp | 2 +- src/engine/fileOps/fileOpsCommon.h | 4 +++- src/engine/fileOps/vge.cpp | 33 +++++++++++++++++++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/engine/fileOps/fileOpsCommon.cpp b/src/engine/fileOps/fileOpsCommon.cpp index 301f19783e..f9cb5a1864 100644 --- a/src/engine/fileOps/fileOpsCommon.cpp +++ b/src/engine/fileOps/fileOpsCommon.cpp @@ -156,7 +156,7 @@ bool DivEngine::load(unsigned char* f, size_t slen, const char* nameHint) { return loadTFMv2(file,len); } else if (memcmp(file,DIV_IT_MAGIC,4)==0) { return loadIT(file,len); - } else if (memcmp(file,DIV_VGE_MAGIC,4)==0) { + } else if (memcmp(file,DIV_VGEV3_MAGIC,7)==0) { return loadVGE(file,len); } else if (len>=48) { if (memcmp(&file[0x2c],DIV_S3M_MAGIC,4)==0) { diff --git a/src/engine/fileOps/fileOpsCommon.h b/src/engine/fileOps/fileOpsCommon.h index d661f55e65..d556dd7cce 100644 --- a/src/engine/fileOps/fileOpsCommon.h +++ b/src/engine/fileOps/fileOpsCommon.h @@ -56,7 +56,9 @@ struct NotZlibException { #define DIV_XM_MAGIC "Extended Module: " #define DIV_IT_MAGIC "IMPM" #define DIV_TFM_MAGIC "TFMfmtV2" -#define DIV_VGE_MAGIC "VGEfmtV3" +#define DIV_VGEV1_MAGIC "VGEfmtV1" +#define DIV_VGEV2_MAGIC "VGEfmtV2" +#define DIV_VGEV3_MAGIC "VGEfmtV3" #define DIV_FUR_MAGIC_DS0 "Furnace-B module" diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index 49a995c25e..21b8a9f56a 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -393,19 +393,35 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { ds.system[1]=DIV_SYSTEM_SMS; ds.loopModality=1; + bool preVGEV3; + bool preVGEV2; + unsigned char magic[8]={0}; reader.readNoRLE(magic,8); - if (memcmp(magic,DIV_VGE_MAGIC,8)!=0) throw InvalidHeaderException(); + if (memcmp(magic,DIV_VGEV3_MAGIC,8)==0) { + preVGEV3=false; + preVGEV2=false; + } else if (memcmp(magic,DIV_VGEV2_MAGIC,8)==0) { + preVGEV3=true; + preVGEV2=false; + } else if (memcmp(magic,DIV_VGEV1_MAGIC,8)==0) { + preVGEV3=true; + preVGEV2=true; + } else { + throw InvalidHeaderException(); + } // Read the size of the header unsigned int headerSize=reader.readINoRLE(); unsigned int sampleDescSize=reader.readINoRLE(); // Read the clock rates - ds.systemFlags[0].set("customClock", reader.readINoRLE()); - ds.systemFlags[1].set("customClock", reader.readINoRLE()); - ds.subsong[0]->hz = reader.readSNoRLE(); + if (!preVGEV3) { + ds.systemFlags[0].set("customClock", reader.readINoRLE()); + ds.systemFlags[1].set("customClock", reader.readINoRLE()); + ds.subsong[0]->hz = reader.readSNoRLE(); + } unsigned char speedEven=reader.readCNoRLE(); unsigned char speedOdd=reader.readCNoRLE(); @@ -539,8 +555,13 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { // sample instrument data // TODO: actually implement this. - reader.skip(255 * 16); - reader.skip(255 * 4); + if (preVGEV2) { + reader.skip(255 * 16); + reader.skip(255 * 2); + } else { + reader.skip(255 * 16); + reader.skip(255 * 4); + } ds.notes=notes; From c801c0c4593c414e1b834e1371db7fcba7d8bbab Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 11 May 2025 17:10:59 +0100 Subject: [PATCH 7/9] Set VGE framerate to 50Hz by default --- src/engine/fileOps/vge.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index 21b8a9f56a..b3fcd36102 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -388,6 +388,8 @@ bool DivEngine::loadVGE(unsigned char* file, size_t len) { ds.version=DIV_VERSION_TFE; ds.systemName="Sega Genesis/Mega Drive or TurboSound FM"; ds.systemLen=2; + // Set it to 50Hz by default. + ds.subsong[0]->hz=50; ds.system[0]=DIV_SYSTEM_YM2612; ds.system[1]=DIV_SYSTEM_SMS; From 7e50375a23984552cd35302914619092a8a648ee Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 14 May 2025 12:34:20 +0100 Subject: [PATCH 8/9] Add noise support --- src/engine/fileOps/vge.cpp | 70 ++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index b3fcd36102..b00a4192e0 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -79,6 +79,10 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { continue; } + // For fixing noise notes + unsigned char noiseNotes[256]; + unsigned char noiseNotesType[256]; + unsigned int noiseNotesVal=0; logD("parsing pattern %d",i); for (int j=0; j<10; j++) { DivPattern* pat = info.ds->subsong[0]->pat[j].data[i]; @@ -94,12 +98,44 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { 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 (j==9) { + // noise channel. + switch (invertedNote & 0b11) { + case 0b00: + // high noise + pat->data[k][0]=2; + pat->data[k][1]=0; + noiseNotes[noiseNotesVal]=k; + noiseNotesType[noiseNotesVal++]=invertedNote&0b11; + break; + case 0b01: + // mid noise + pat->data[k][0]=1; + pat->data[k][1]=0; + noiseNotes[noiseNotesVal]=k; + noiseNotesType[noiseNotesVal++]=invertedNote&0b11; + break; + case 0b10: + // low noise + pat->data[k][0]=0; + pat->data[k][1]=0; + noiseNotes[noiseNotesVal]=k; + noiseNotesType[noiseNotesVal++]=invertedNote&0b11; + break; + case 0b11: + // ch3 noise + noiseNotes[noiseNotesVal]=k; + noiseNotesType[noiseNotesVal++]=invertedNote&0b11; + break; + } + } else { + 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]--; + if (pat->data[k][0]==0) { + pat->data[k][0]=12; + pat->data[k][1]--; + } } } } @@ -280,13 +316,35 @@ void VGEParsePattern(struct VGEParsePatternInfo info) { break; } } - info.ds->subsong[0]->pat[j].effectCols=(usedEffectsCol*2)+1; + + if (j==9) { + // add an extra col for the noise channel + info.ds->subsong[0]->pat[j].effectCols=(usedEffectsCol*2)+2; + } else { + 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; } + + // fix the notes in the noise channel + for (int i=0; idata[noiseNotes[i]][4+(usedEffectsCol*4)+2]=0x20; + pat->data[noiseNotes[i]][5+(usedEffectsCol*4)+2]=0x01; + break; + case 0b11: + pat->data[noiseNotes[i]][4+(usedEffectsCol*4)+2]=0x20; + pat->data[noiseNotes[i]][5+(usedEffectsCol*4)+2]=0x11; + break; + } + } } } } From 3e34960dfef6f7c02c4e303dcb2d1084f3d5857e Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 20 Dec 2025 20:13:39 +0100 Subject: [PATCH 9/9] Handle PSG noise notes properly (they're just hi/mid/lo/ch3) --- src/engine/fileOps/vge.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/engine/fileOps/vge.cpp b/src/engine/fileOps/vge.cpp index b00a4192e0..1734664979 100644 --- a/src/engine/fileOps/vge.cpp +++ b/src/engine/fileOps/vge.cpp @@ -146,7 +146,15 @@ void VGEParsePattern(struct VGEParsePatternInfo 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]=0x60+patDataBuf[k]; + else { + if (j <= 5) { + // fm + pat->data[k][3]=0x60+patDataBuf[k]; + } else { + // psg + pat->data[k][3]=patDataBuf[k]>>1; + } + } } // instrument