mirror of
https://github.com/tildearrow/furnace.git
synced 2024-11-04 20:05:05 +00:00
FT
-----____ | -----____ | FFFFF -----____ | FFFFFFFFF TTTTTTT | | FF TTTTTTTT | | FF TT | | FF TT | | FFFFFFFFF TT | | FFFFFFFFF TT | | FF TT | | FF TT | | FF TT | | FF TT | | FF __________------| |---------
This commit is contained in:
parent
d8471ce937
commit
f20da6b202
3 changed files with 256 additions and 23 deletions
|
@ -58,6 +58,7 @@
|
||||||
#define DIV_VERSION_MOD 0xff01
|
#define DIV_VERSION_MOD 0xff01
|
||||||
#define DIV_VERSION_FC 0xff02
|
#define DIV_VERSION_FC 0xff02
|
||||||
#define DIV_VERSION_S3M 0xff03
|
#define DIV_VERSION_S3M 0xff03
|
||||||
|
#define DIV_VERSION_FTM 0xff04
|
||||||
|
|
||||||
// "Namco C163"
|
// "Namco C163"
|
||||||
#define DIV_C163_DEFAULT_NAME "Namco 163"
|
#define DIV_C163_DEFAULT_NAME "Namco 163"
|
||||||
|
|
|
@ -4061,12 +4061,58 @@ bool DivEngine::loadFC(unsigned char* file, size_t len) {
|
||||||
|
|
||||||
#define CHECK_BLOCK_VERSION(x) \
|
#define CHECK_BLOCK_VERSION(x) \
|
||||||
if (blockVersion>x) { \
|
if (blockVersion>x) { \
|
||||||
logE("incompatible block version %d for %s!",blockVersion,blockName); \
|
logW("incompatible block version %d for %s!",blockVersion,blockName); \
|
||||||
lastError="incompatible block version"; \
|
|
||||||
delete[] file; \
|
|
||||||
return false; \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int ftEffectMap[]={
|
||||||
|
-1, // none
|
||||||
|
0x0f,
|
||||||
|
0x0b,
|
||||||
|
0x0d,
|
||||||
|
0xff,
|
||||||
|
-1, // volume? not supported in Furnace yet
|
||||||
|
0x03,
|
||||||
|
0x03, // unused?
|
||||||
|
0x13,
|
||||||
|
0x14,
|
||||||
|
0x00,
|
||||||
|
0x04,
|
||||||
|
0x07,
|
||||||
|
0xe5,
|
||||||
|
0xed,
|
||||||
|
0x11,
|
||||||
|
0x01, // porta up
|
||||||
|
0x02, // porta down
|
||||||
|
0x12,
|
||||||
|
0x90, // sample offset - not supported yet
|
||||||
|
0xe1,
|
||||||
|
0xe2,
|
||||||
|
0x0a,
|
||||||
|
0xec,
|
||||||
|
0x0c,
|
||||||
|
-1, // delayed volume - not supported yet
|
||||||
|
0x11, // FDS
|
||||||
|
0x12,
|
||||||
|
0x13,
|
||||||
|
0x20, // DPCM pitch
|
||||||
|
0x22, // 5B
|
||||||
|
0x24,
|
||||||
|
0x23,
|
||||||
|
0x21,
|
||||||
|
-1, // VRC7 "custom patch port" - not supported?
|
||||||
|
-1, // VRC7 "custom patch write"
|
||||||
|
-1, // release - not supported yet
|
||||||
|
0x09, // select groove
|
||||||
|
-1, // transpose - not supported
|
||||||
|
0x10, // Namco 163
|
||||||
|
-1, // FDS vol env - not supported
|
||||||
|
-1, // FDS auto FM - not supported yet
|
||||||
|
-1, // phase reset - not supported
|
||||||
|
-1, // harmonic - not supported
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr int ftEffectMapSize=sizeof(ftEffectMap)/sizeof(int);
|
||||||
|
|
||||||
bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
SafeReader reader=SafeReader(file,len);
|
SafeReader reader=SafeReader(file,len);
|
||||||
warnings="";
|
warnings="";
|
||||||
|
@ -4078,6 +4124,9 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
unsigned int n163Chans=0;
|
unsigned int n163Chans=0;
|
||||||
bool hasSequence[256][8];
|
bool hasSequence[256][8];
|
||||||
unsigned char sequenceIndex[256][8];
|
unsigned char sequenceIndex[256][8];
|
||||||
|
unsigned int hilightA=4;
|
||||||
|
unsigned int hilightB=16;
|
||||||
|
double customHz=60;
|
||||||
|
|
||||||
memset(hasSequence,0,256*8*sizeof(bool));
|
memset(hasSequence,0,256*8*sizeof(bool));
|
||||||
memset(sequenceIndex,0,256*8);
|
memset(sequenceIndex,0,256*8);
|
||||||
|
@ -4098,6 +4147,14 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (DivSubSong* i: ds.subsong) {
|
||||||
|
i->clearData();
|
||||||
|
delete i;
|
||||||
|
}
|
||||||
|
ds.subsong.clear();
|
||||||
|
|
||||||
|
ds.linearPitch=0;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
blockName=reader.readString(3);
|
blockName=reader.readString(3);
|
||||||
if (blockName=="END") {
|
if (blockName=="END") {
|
||||||
|
@ -4115,7 +4172,8 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
|
|
||||||
logD("reading block %s (version %d, %d bytes)",blockName,blockVersion,blockSize);
|
logD("reading block %s (version %d, %d bytes)",blockName,blockVersion,blockSize);
|
||||||
if (blockName=="PARAMS") {
|
if (blockName=="PARAMS") {
|
||||||
CHECK_BLOCK_VERSION(6);
|
// versions 7-9 don't change anything?
|
||||||
|
CHECK_BLOCK_VERSION(9);
|
||||||
unsigned int oldSpeedTempo=0;
|
unsigned int oldSpeedTempo=0;
|
||||||
if (blockVersion<=1) {
|
if (blockVersion<=1) {
|
||||||
oldSpeedTempo=reader.readI();
|
oldSpeedTempo=reader.readI();
|
||||||
|
@ -4125,15 +4183,32 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
}
|
}
|
||||||
tchans=reader.readI();
|
tchans=reader.readI();
|
||||||
unsigned int pal=reader.readI();
|
unsigned int pal=reader.readI();
|
||||||
unsigned int customHz=reader.readI();
|
if (blockVersion>=7) {
|
||||||
|
// advanced Hz control
|
||||||
|
int controlType=reader.readI();
|
||||||
|
switch (controlType) {
|
||||||
|
case 1:
|
||||||
|
customHz=1000000.0/(double)reader.readI();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.readI();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customHz=reader.readI();
|
||||||
|
}
|
||||||
unsigned int newVibrato=0;
|
unsigned int newVibrato=0;
|
||||||
|
bool sweepReset=false;
|
||||||
unsigned int speedSplitPoint=0;
|
unsigned int speedSplitPoint=0;
|
||||||
if (blockVersion>=3) {
|
if (blockVersion>=3) {
|
||||||
newVibrato=reader.readI();
|
newVibrato=reader.readI();
|
||||||
}
|
}
|
||||||
if (blockVersion>=4) {
|
if (blockVersion>=9) {
|
||||||
ds.subsong[0]->hilightA=reader.readI();
|
sweepReset=reader.readI();
|
||||||
ds.subsong[0]->hilightB=reader.readI();
|
}
|
||||||
|
if (blockVersion>=4 && blockVersion<7) {
|
||||||
|
hilightA=reader.readI();
|
||||||
|
hilightB=reader.readI();
|
||||||
}
|
}
|
||||||
if (expansions&8) if (blockVersion>=5) { // N163 channels
|
if (expansions&8) if (blockVersion>=5) { // N163 channels
|
||||||
n163Chans=reader.readI();
|
n163Chans=reader.readI();
|
||||||
|
@ -4142,20 +4217,24 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
speedSplitPoint=reader.readI();
|
speedSplitPoint=reader.readI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blockVersion>=8) {
|
||||||
|
int fineTuneCents=reader.readC()*100;
|
||||||
|
fineTuneCents+=reader.readC();
|
||||||
|
|
||||||
|
ds.tuning=440.0*pow(2.0,(double)fineTuneCents/1200.0);
|
||||||
|
}
|
||||||
|
|
||||||
logV("old speed/tempo: %d",oldSpeedTempo);
|
logV("old speed/tempo: %d",oldSpeedTempo);
|
||||||
logV("expansions: %x",expansions);
|
logV("expansions: %x",expansions);
|
||||||
logV("channels: %d",tchans);
|
logV("channels: %d",tchans);
|
||||||
logV("PAL: %d",pal);
|
logV("PAL: %d",pal);
|
||||||
logV("custom Hz: %d",customHz);
|
logV("custom Hz: %f",customHz);
|
||||||
logV("new vibrato: %d",newVibrato);
|
logV("new vibrato: %d",newVibrato);
|
||||||
logV("N163 channels: %d",n163Chans);
|
logV("N163 channels: %d",n163Chans);
|
||||||
logV("highlight 1: %d",ds.subsong[0]->hilightA);
|
logV("highlight 1: %d",hilightA);
|
||||||
logV("highlight 2: %d",ds.subsong[0]->hilightB);
|
logV("highlight 2: %d",hilightB);
|
||||||
logV("split point: %d",speedSplitPoint);
|
logV("split point: %d",speedSplitPoint);
|
||||||
|
logV("sweep reset: %d",sweepReset);
|
||||||
if (customHz!=0) {
|
|
||||||
ds.subsong[0]->hz=customHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize channels
|
// initialize channels
|
||||||
int systemID=0;
|
int systemID=0;
|
||||||
|
@ -4200,28 +4279,46 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
CHECK_BLOCK_VERSION(1);
|
CHECK_BLOCK_VERSION(1);
|
||||||
ds.name=reader.readString(32);
|
ds.name=reader.readString(32);
|
||||||
ds.author=reader.readString(32);
|
ds.author=reader.readString(32);
|
||||||
ds.copyright=reader.readString(32);
|
ds.category=reader.readString(32);
|
||||||
|
ds.systemName="NES";
|
||||||
} else if (blockName=="HEADER") {
|
} else if (blockName=="HEADER") {
|
||||||
CHECK_BLOCK_VERSION(3);
|
CHECK_BLOCK_VERSION(4);
|
||||||
unsigned char totalSongs=reader.readC();
|
unsigned char totalSongs=reader.readC();
|
||||||
logV("%d songs:",totalSongs+1);
|
logV("%d songs:",totalSongs+1);
|
||||||
for (int i=0; i<=totalSongs; i++) {
|
for (int i=0; i<=totalSongs; i++) {
|
||||||
String subSongName=reader.readString();
|
String subSongName=reader.readString();
|
||||||
|
ds.subsong.push_back(new DivSubSong);
|
||||||
|
ds.subsong[i]->name=subSongName;
|
||||||
|
ds.subsong[i]->hilightA=hilightA;
|
||||||
|
ds.subsong[i]->hilightB=hilightB;
|
||||||
|
if (customHz!=0) {
|
||||||
|
ds.subsong[i]->hz=customHz;
|
||||||
|
}
|
||||||
logV("- %s",subSongName);
|
logV("- %s",subSongName);
|
||||||
}
|
}
|
||||||
for (unsigned int i=0; i<tchans; i++) {
|
for (unsigned int i=0; i<tchans; i++) {
|
||||||
|
// TODO: obey channel ID
|
||||||
unsigned char chID=reader.readC();
|
unsigned char chID=reader.readC();
|
||||||
logV("for channel ID %d",chID);
|
logV("for channel ID %d",chID);
|
||||||
for (int j=0; j<=totalSongs; j++) {
|
for (int j=0; j<=totalSongs; j++) {
|
||||||
unsigned char effectCols=reader.readC();
|
unsigned char effectCols=reader.readC();
|
||||||
if (j==0) {
|
ds.subsong[j]->pat[i].effectCols=effectCols+1;
|
||||||
ds.subsong[0]->pat[i].effectCols=effectCols+1;
|
|
||||||
}
|
|
||||||
logV("- song %d has %d effect columns",j,effectCols);
|
logV("- song %d has %d effect columns",j,effectCols);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (blockVersion>=4) {
|
||||||
|
for (int i=0; i<=totalSongs; i++) {
|
||||||
|
ds.subsong[i]->hilightA=(unsigned char)reader.readC();
|
||||||
|
ds.subsong[i]->hilightB=(unsigned char)reader.readC();
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (blockName=="INSTRUMENTS") {
|
} else if (blockName=="INSTRUMENTS") {
|
||||||
CHECK_BLOCK_VERSION(6);
|
CHECK_BLOCK_VERSION(6);
|
||||||
|
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
|
|
||||||
|
/*
|
||||||
ds.insLen=reader.readI();
|
ds.insLen=reader.readI();
|
||||||
if (ds.insLen<0 || ds.insLen>256) {
|
if (ds.insLen<0 || ds.insLen>256) {
|
||||||
logE("too many instruments/out of range!");
|
logE("too many instruments/out of range!");
|
||||||
|
@ -4381,21 +4478,131 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
ins->name=reader.readString((unsigned int)reader.readI());
|
ins->name=reader.readString((unsigned int)reader.readI());
|
||||||
logV("- %d: %s",insIndex,ins->name);
|
logV("- %d: %s",insIndex,ins->name);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
} else if (blockName=="SEQUENCES") {
|
} else if (blockName=="SEQUENCES") {
|
||||||
CHECK_BLOCK_VERSION(6);
|
CHECK_BLOCK_VERSION(6);
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
} else if (blockName=="FRAMES") {
|
} else if (blockName=="FRAMES") {
|
||||||
CHECK_BLOCK_VERSION(3);
|
CHECK_BLOCK_VERSION(3);
|
||||||
|
|
||||||
|
for (size_t i=0; i<ds.subsong.size(); i++) {
|
||||||
|
DivSubSong* s=ds.subsong[i];
|
||||||
|
|
||||||
|
s->ordersLen=reader.readI();
|
||||||
|
if (blockVersion>=3) {
|
||||||
|
s->speeds.val[0]=reader.readI();
|
||||||
|
}
|
||||||
|
if (blockVersion>=2) {
|
||||||
|
s->virtualTempoN=reader.readI();
|
||||||
|
s->patLen=reader.readI();
|
||||||
|
}
|
||||||
|
int why=tchans;
|
||||||
|
if (blockVersion==1) {
|
||||||
|
why=reader.readI();
|
||||||
|
}
|
||||||
|
logV("reading %d and %d orders",tchans,s->ordersLen);
|
||||||
|
|
||||||
|
for (int j=0; j<s->ordersLen; j++) {
|
||||||
|
for (int k=0; k<why; k++) {
|
||||||
|
unsigned char o=reader.readC();
|
||||||
|
logV("%.2x",o);
|
||||||
|
s->orders.ord[k][j]=o;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (blockName=="PATTERNS") {
|
} else if (blockName=="PATTERNS") {
|
||||||
CHECK_BLOCK_VERSION(5);
|
CHECK_BLOCK_VERSION(6);
|
||||||
|
|
||||||
|
size_t blockEnd=reader.tell()+blockSize;
|
||||||
|
|
||||||
|
if (blockVersion==1) {
|
||||||
|
int patLenOld=reader.readI();
|
||||||
|
for (DivSubSong* i: ds.subsong) {
|
||||||
|
i->patLen=patLenOld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// so it appears .ftm doesn't keep track of how many patterns are stored in the file....
|
||||||
|
while (reader.tell()<blockEnd) {
|
||||||
|
int subs=0;
|
||||||
|
if (blockVersion>=2) subs=reader.readI();
|
||||||
|
int ch=reader.readI();
|
||||||
|
int patNum=reader.readI();
|
||||||
|
int numRows=reader.readI();
|
||||||
|
|
||||||
|
DivPattern* pat=ds.subsong[subs]->pat[ch].getPattern(patNum,true);
|
||||||
|
for (int i=0; i<numRows; i++) {
|
||||||
|
unsigned int row=0;
|
||||||
|
if (blockVersion>=2 && blockVersion<6) { // row index
|
||||||
|
row=reader.readI();
|
||||||
|
} else {
|
||||||
|
row=reader.readC();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char nextNote=reader.readC();
|
||||||
|
unsigned char nextOctave=reader.readC();
|
||||||
|
if (nextNote==0x0d) {
|
||||||
|
pat->data[row][0]=100;
|
||||||
|
} else if (nextNote==0x0e) {
|
||||||
|
pat->data[row][0]=101;
|
||||||
|
} else if (nextNote==0x01) {
|
||||||
|
pat->data[row][0]=12;
|
||||||
|
pat->data[row][1]=nextOctave-1;
|
||||||
|
} else if (nextNote==0) {
|
||||||
|
pat->data[row][0]=0;
|
||||||
|
} else if (nextNote<0x0d) {
|
||||||
|
pat->data[row][0]=nextNote-1;
|
||||||
|
pat->data[row][1]=nextOctave;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char nextIns=reader.readC();
|
||||||
|
if (nextIns<0x40) {
|
||||||
|
pat->data[row][2]=nextIns;
|
||||||
|
} else {
|
||||||
|
pat->data[row][2]=-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char nextVol=reader.readC();
|
||||||
|
if (nextVol<0x10) {
|
||||||
|
pat->data[row][3]=nextVol;
|
||||||
|
} else {
|
||||||
|
pat->data[row][3]=-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int effectCols=ds.subsong[subs]->pat[ch].effectCols;
|
||||||
|
if (blockVersion>=6) effectCols=4;
|
||||||
|
|
||||||
|
for (int j=0; j<effectCols; j++) {
|
||||||
|
unsigned char nextEffect=reader.readC();
|
||||||
|
unsigned char nextEffectVal=0;
|
||||||
|
if (nextEffect!=0 || blockVersion<6) nextEffectVal=reader.readC();
|
||||||
|
if (nextEffect==0 && nextEffectVal==0) {
|
||||||
|
pat->data[row][4+(j*2)]=-1;
|
||||||
|
pat->data[row][5+(j*2)]=-1;
|
||||||
|
} else {
|
||||||
|
if (nextEffect<ftEffectMapSize) {
|
||||||
|
pat->data[row][4+(j*2)]=ftEffectMap[nextEffect];
|
||||||
|
} else {
|
||||||
|
pat->data[row][4+(j*2)]=-1;
|
||||||
|
}
|
||||||
|
pat->data[row][5+(j*2)]=nextEffectVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (blockName=="DPCM SAMPLES") {
|
} else if (blockName=="DPCM SAMPLES") {
|
||||||
CHECK_BLOCK_VERSION(1);
|
CHECK_BLOCK_VERSION(1);
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
} else if (blockName=="SEQUENCES_VRC6") {
|
} else if (blockName=="SEQUENCES_VRC6") {
|
||||||
// where are the 5B and FDS sequences?
|
// where are the 5B and FDS sequences?
|
||||||
CHECK_BLOCK_VERSION(6);
|
CHECK_BLOCK_VERSION(6);
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
} else if (blockName=="SEQUENCES_N163") {
|
} else if (blockName=="SEQUENCES_N163") {
|
||||||
CHECK_BLOCK_VERSION(1);
|
CHECK_BLOCK_VERSION(1);
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
} else if (blockName=="COMMENTS") {
|
} else if (blockName=="COMMENTS") {
|
||||||
CHECK_BLOCK_VERSION(1);
|
CHECK_BLOCK_VERSION(1);
|
||||||
|
reader.seek(blockSize,SEEK_CUR);
|
||||||
} else {
|
} else {
|
||||||
logE("block %s is unknown!",blockName);
|
logE("block %s is unknown!",blockName);
|
||||||
lastError="unknown block "+blockName;
|
lastError="unknown block "+blockName;
|
||||||
|
@ -4410,6 +4617,27 @@ bool DivEngine::loadFTM(unsigned char* file, size_t len) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addWarning("FamiTracker import is experimental!");
|
||||||
|
|
||||||
|
ds.version=DIV_VERSION_FTM;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
} catch (EndOfFileException& e) {
|
} catch (EndOfFileException& e) {
|
||||||
logE("premature end of file!");
|
logE("premature end of file!");
|
||||||
lastError="incomplete file";
|
lastError="incomplete file";
|
||||||
|
|
|
@ -236,10 +236,14 @@ String SafeReader::readString(size_t stlen) {
|
||||||
#endif
|
#endif
|
||||||
size_t curPos=0;
|
size_t curPos=0;
|
||||||
if (isEOF()) throw EndOfFileException(this, len);
|
if (isEOF()) throw EndOfFileException(this, len);
|
||||||
|
bool zero=false;
|
||||||
|
|
||||||
while (!isEOF() && curPos<stlen) {
|
while (!isEOF() && curPos<stlen) {
|
||||||
unsigned char c=readC();
|
unsigned char c=readC();
|
||||||
if (c!=0) ret.push_back(c);
|
if (c==0) {
|
||||||
|
zero=true;
|
||||||
|
}
|
||||||
|
if (!zero) ret.push_back(c);
|
||||||
curPos++;
|
curPos++;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
|
|
Loading…
Reference in a new issue