/** * 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" bool DivEngine::loadXM(unsigned char* file, size_t len) { struct InvalidHeaderException {}; bool success=false; char magic[32]; unsigned char sampleVol[256][256]; unsigned short patLen[256]; bool doesPitchSlide[128]; bool doesVibrato[128]; bool doesPanning[128]; bool doesVolSlide[128]; bool doesArp[128]; SafeReader reader=SafeReader(file,len); warnings=""; memset(sampleVol,0,256*256); memset(patLen,0,256*sizeof(unsigned short)); memset(doesPitchSlide,0,128*sizeof(bool)); memset(doesVibrato,0,128*sizeof(bool)); memset(doesPanning,0,128*sizeof(bool)); memset(doesVolSlide,0,128*sizeof(bool)); memset(doesArp,0,128*sizeof(bool)); try { DivSong ds; ds.version=DIV_VERSION_XM; //ds.linearPitch=0; //ds.pitchMacroIsLinear=false; ds.noSlidesOnFirstTick=true; ds.rowResetsArpPos=true; ds.ignoreJumpAtEnd=false; ds.pitchSlideSpeed=12; logV("Extended Module"); // load here if (!reader.seek(0,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } reader.read(magic,17); if (memcmp(magic,DIV_XM_MAGIC,17)!=0) { logW("invalid magic"); throw EndOfFileException(&reader,reader.tell()); } ds.name=reader.readStringLatin1(20); // 0x1a reader.readC(); String trackerName=reader.readStringLatin1(20); unsigned short trackerVer=reader.readS(); if (trackerName!="") logV("made with %s",trackerName); logV("version %x",trackerVer); unsigned int headerSeek=reader.tell(); headerSeek+=reader.readI(); ds.subsong[0]->ordersLen=(unsigned short)reader.readS(); ds.subsong[0]->patLen=1; unsigned short loopPos=reader.readS(); unsigned short totalChans=reader.readS(); unsigned short patCount=reader.readS(); ds.insLen=(unsigned short)reader.readS(); ds.linearPitch=(reader.readS()&1)?2:0; ds.subsong[0]->speeds.val[0]=reader.readS(); ds.subsong[0]->speeds.len=1; double bpm=(unsigned short)reader.readS(); ds.subsong[0]->hz=(double)bpm/2.5; if (ds.insLen<0 || ds.insLen>256) { logE("invalid instrument count!"); lastError="invalid instrument count"; delete[] file; return false; } logV("channels: %d",totalChans); if (totalChans>127) { logE("invalid channel count!"); lastError="invalid channel count"; delete[] file; return false; } logV("repeat pos: %d",loopPos); logV("reading orders..."); for (int i=0; i<256; i++) { unsigned char val=reader.readC(); for (int j=0; jorders.ord[j][i]=val; } } for (int i=0; i<(totalChans+31)>>5; i++) { ds.system[i]=DIV_SYSTEM_ES5506; ds.systemFlags[i].set("amigaVol",true); ds.systemFlags[i].set("amigaPitch",(ds.linearPitch==0)); } ds.systemLen=(totalChans+31)>>5; size_t patBegin=headerSeek; logV("seeking to %x...",patBegin); if (!reader.seek(patBegin,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } // scan pattern data for effect use logD("scanning patterns..."); for (unsigned short i=0; ids.subsong[0]->patLen) ds.subsong[0]->patLen=totalRows; patLen[i]=totalRows; if (totalRows>256) { logE("too many rows! %d",totalRows); lastError="too many rows"; delete[] file; return false; } unsigned int packedSeek=headerSeek+(unsigned short)reader.readS(); logV("seeking to %x...",headerSeek); if (!reader.seek(headerSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } // read data for (int j=0; j>4) { case 0x6: // vol slide down doesVolSlide[k]=true; break; case 0x7: // vol slide up doesVolSlide[k]=true; break; case 0x8: // vol slide down (fine) doesVolSlide[k]=true; break; case 0x9: // vol slide up (fine) doesVolSlide[k]=true; break; case 0xa: // vibrato speed doesVibrato[k]=true; break; case 0xb: // vibrato depth doesVibrato[k]=true; break; case 0xc: // panning doesPanning[k]=true; break; case 0xd: // pan slide left doesPanning[k]=true; break; case 0xe: // pan slide right doesPanning[k]=true; break; case 0xf: // porta doesPitchSlide[k]=true; break; } } if (hasEffect) { effect=reader.readC(); switch (effect) { case 0: doesArp[k]=true; break; case 1: doesPitchSlide[k]=true; break; case 2: doesPitchSlide[k]=true; break; case 3: doesPitchSlide[k]=true; break; case 4: doesVibrato[k]=true; break; case 5: doesPitchSlide[k]=true; doesVolSlide[k]=true; break; case 6: doesVibrato[k]=true; doesVolSlide[k]=true; break; case 8: doesPanning[k]=true; break; case 0xe: doesPanning[k]=true; break; case 0x19: // P doesPanning[k]=true; break; case 0x21: // X doesPitchSlide[k]=true; break; } } if (hasEffectVal) { effectVal=reader.readC(); if (effect==0xe) { switch (effectVal>>4) { case 1: doesPitchSlide[k]=true; break; case 2: doesPitchSlide[k]=true; break; case 0xa: doesVolSlide[k]=true; break; case 0xb: doesVolSlide[k]=true; break; } } } } } logV("seeking to %x...",packedSeek); if (!reader.seek(packedSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } } if (!reader.seek(patBegin,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } // read patterns logD("reading patterns..."); for (unsigned short i=0; ids.subsong[0]->patLen) ds.subsong[0]->patLen=totalRows; patLen[i]=totalRows; if (totalRows>256) { logE("too many rows! %d",totalRows); lastError="too many rows"; delete[] file; return false; } unsigned int packedSeek=headerSeek+(unsigned short)reader.readS(); logV("seeking to %x...",headerSeek); if (!reader.seek(headerSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } // read data for (int j=0; jpat[k].getPattern(i,true); unsigned char note=reader.readC(); unsigned char ins=0; unsigned char vol=0; unsigned char effect=0; unsigned char effectVal=0; bool hasNote=false; bool hasIns=false; bool hasVol=false; bool hasEffect=false; bool hasEffectVal=false; if (note&0x80) { // packed hasNote=note&1; hasIns=note&2; hasVol=note&4; hasEffect=note&8; hasEffectVal=note&16; if (hasNote) { note=reader.readC(); } } else { // unpacked hasNote=true; hasIns=true; hasVol=true; hasEffect=true; hasEffectVal=true; } if (hasNote) { if (note>=96) { p->data[j][0]=100; p->data[j][1]=0; } else { p->data[j][0]=note%12; p->data[j][1]=note/12; if (p->data[j][0]==0) { p->data[j][0]=12; p->data[j][1]=(unsigned char)(p->data[j][1]-1); } } } if (hasIns) { ins=reader.readC(); p->data[j][2]=((int)ins)-1; } if (hasVol) { vol=reader.readC(); if (vol>=0x10 && vol<=0x50) { p->data[j][3]=vol-0x10; } else { // effects in volume column // TODO } } if (vol==0) { if (hasNote && hasIns && note<96 && ins>0) { // TODO: default volume p->data[j][3]=0x40; } } if (hasEffect) { effect=reader.readC(); } if (hasEffectVal) { effectVal=reader.readC(); } if (hasEffect) { switch (effect) { case 0: // arp if (effectVal!=0) { arpStatus[k]=effectVal; arpStatusChanged[k]=true; } arping[k]=true; break; case 1: // pitch up if (effectVal!=0) { portaStatus[k]=effectVal; portaStatusChanged[k]=true; } portaType[k]=1; porting[k]=true; break; case 2: // pitch down if (effectVal!=0) { portaStatus[k]=effectVal; portaStatusChanged[k]=true; } portaType[k]=2; porting[k]=true; break; case 3: // porta if (effectVal!=0) { portaStatus[k]=effectVal; portaStatusChanged[k]=true; } portaType[k]=3; porting[k]=true; break; case 4: // vibrato if (effectVal!=0) { vibStatus[k]=effectVal; vibStatusChanged[k]=true; } vibing[k]=true; break; case 5: // vol slide + porta if (effectVal!=0) { volSlideStatus[k]=effectVal; volSlideStatusChanged[k]=true; } volSliding[k]=true; porting[k]=true; portaType[k]=3; break; case 6: // vol slide + vibrato if (effectVal!=0) { volSlideStatus[k]=effectVal; volSlideStatusChanged[k]=true; } volSliding[k]=true; vibing[k]=true; break; case 7: // tremolo break; case 8: // panning p->data[j][effectCol[k]++]=0x80; p->data[j][effectCol[k]++]=effectVal; break; case 9: // offset p->data[j][effectCol[k]++]=0x91; p->data[j][effectCol[k]++]=effectVal; break; case 0xa: // vol slide if (effectVal!=0) { volSlideStatus[k]=effectVal; volSlideStatusChanged[k]=true; } if (hasIns) { volSlideStatusChanged[k]=true; } volSliding[k]=true; break; case 0xb: // go to order p->data[j][effectCol[k]++]=0x0b; p->data[j][effectCol[k]++]=effectVal; break; case 0xc: // set volume p->data[j][3]=effectVal; break; case 0xd: // next order p->data[j][effectCol[k]++]=0x0d; p->data[j][effectCol[k]++]=effectVal; break; case 0xe: // special... // TODO: implement the rest switch (effectVal>>4) { case 0xc: p->data[j][effectCol[k]++]=0xec; p->data[j][effectCol[k]++]=effectVal&15; break; case 0xd: p->data[j][effectCol[k]++]=0xed; p->data[j][effectCol[k]++]=effectVal&15; break; } break; case 0x10: // G: global volume (!) break; case 0xf: // speed/tempp if (effectVal>=0x20) { p->data[j][effectCol[k]++]=0xf0; } else { p->data[j][effectCol[k]++]=0x0f; } p->data[j][effectCol[k]++]=effectVal; break; case 0x11: // H: global volume slide (!) break; case 0x14: // K: key off p->data[j][effectCol[k]++]=0xe7; p->data[j][effectCol[k]++]=effectVal; break; case 0x15: // L: set envelope position (!) break; case 0x19: // P: pan slide break; case 0x1b: // R: retrigger p->data[j][effectCol[k]++]=0x0c; p->data[j][effectCol[k]++]=effectVal; break; case 0x1d: // T: tremor (!) break; case 0x21: // X: extra fine volume break; } } } // commit effects for (int k=0; kpat[k].getPattern(i,true); if (vibing[k]!=vibingOld[k] || vibStatusChanged[k]) { p->data[j][effectCol[k]++]=0x04; p->data[j][effectCol[k]++]=vibing[k]?vibStatus[k]:0; doesVibrato[k]=true; } else if (doesVibrato[k] && mustCommitInitial) { p->data[j][effectCol[k]++]=0x04; p->data[j][effectCol[k]++]=0; } if (volSliding[k]!=volSlidingOld[k] || volSlideStatusChanged[k]) { if (volSlideStatus[k]>=0xf1 && volSliding[k]) { p->data[j][effectCol[k]++]=0xf9; p->data[j][effectCol[k]++]=volSlideStatus[k]&15; volSliding[k]=false; } else if ((volSlideStatus[k]&15)==15 && volSlideStatus[k]>=0x10 && volSliding[k]) { p->data[j][effectCol[k]++]=0xf8; p->data[j][effectCol[k]++]=volSlideStatus[k]>>4; volSliding[k]=false; } else { p->data[j][effectCol[k]++]=0xfa; p->data[j][effectCol[k]++]=volSliding[k]?volSlideStatus[k]:0; } doesVolSlide[k]=true; } else if (doesVolSlide[k] && mustCommitInitial) { p->data[j][effectCol[k]++]=0xfa; p->data[j][effectCol[k]++]=0; } if (porting[k]!=portingOld[k] || portaStatusChanged[k]) { if (portaStatus[k]>=0xe0 && portaType[k]!=3 && porting[k]) { p->data[j][effectCol[k]++]=portaType[k]|0xf0; p->data[j][effectCol[k]++]=(portaStatus[k]&15)*((portaStatus[k]>=0xf0)?1:1); porting[k]=false; } else { p->data[j][effectCol[k]++]=portaType[k]; p->data[j][effectCol[k]++]=porting[k]?portaStatus[k]:0; } doesPitchSlide[k]=true; } else if (doesPitchSlide[k] && mustCommitInitial) { p->data[j][effectCol[k]++]=0x01; p->data[j][effectCol[k]++]=0; } if (arping[k]!=arpingOld[k] || arpStatusChanged[k]) { p->data[j][effectCol[k]++]=0x00; p->data[j][effectCol[k]++]=arping[k]?arpStatus[k]:0; doesArp[k]=true; } else if (doesArp[k] && mustCommitInitial) { p->data[j][effectCol[k]++]=0x00; p->data[j][effectCol[k]++]=0; } if ((effectCol[k]>>1)-2>ds.subsong[0]->pat[k].effectCols) { ds.subsong[0]->pat[k].effectCols=(effectCol[k]>>1)-1; } } memset(effectCol,4,64); memcpy(vibingOld,vibing,64*sizeof(bool)); memcpy(volSlidingOld,volSliding,64*sizeof(bool)); memcpy(portingOld,porting,64*sizeof(bool)); memcpy(arpingOld,arping,64*sizeof(bool)); memset(vibStatusChanged,0,64*sizeof(bool)); memset(volSlideStatusChanged,0,64*sizeof(bool)); memset(portaStatusChanged,0,64*sizeof(bool)); memset(arpStatusChanged,0,64*sizeof(bool)); memset(vibing,0,64*sizeof(bool)); memset(volSliding,0,64*sizeof(bool)); memset(porting,0,64*sizeof(bool)); memset(arping,0,64*sizeof(bool)); mustCommitInitial=false; if (j==totalRows-1) { // place end of pattern marker DivPattern* p=ds.subsong[0]->pat[0].getPattern(i,true); p->data[j][effectCol[0]++]=0x0d; p->data[j][effectCol[0]++]=0; if ((effectCol[0]>>1)-2>ds.subsong[0]->pat[0].effectCols) { ds.subsong[0]->pat[0].effectCols=(effectCol[0]>>1)-1; } } } logV("seeking to %x...",packedSeek); if (!reader.seek(packedSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } } // read instruments for (int i=0; iname=reader.readStringLatin1(22); ins->type=DIV_INS_ES5506; ins->amiga.useNoteMap=true; unsigned char insType=reader.readC(); /* if (insType!=0) { logE("unknown instrument type!"); lastError="unknown instrument type"; delete ins; song.unload(); delete[] file; return false; }*/ logV("type: %d",insType); unsigned short sampleCount=reader.readS(); logV("%d samples",sampleCount); if (sampleCount>0) { unsigned int sampleHeaderSize=reader.readI(); logV("sample header size: %d",sampleHeaderSize); for (int j=0; j<96; j++) { unsigned char nextMap=reader.readC(); if (nextMap==0) { ins->amiga.noteMap[j].map=-1; } else { ins->amiga.noteMap[j].map=ds.sample.size()+nextMap-1; } } reader.read(volEnv,48); reader.read(panEnv,48); unsigned char volEnvLen=reader.readC(); unsigned char panEnvLen=reader.readC(); unsigned char volSusPoint=reader.readC(); unsigned char volLoopStart=reader.readC(); unsigned char volLoopEnd=reader.readC(); unsigned char panSusPoint=reader.readC(); unsigned char panLoopStart=reader.readC(); unsigned char panLoopEnd=reader.readC(); unsigned char volType=reader.readC(); unsigned char panType=reader.readC(); unsigned char vibType=reader.readC(); unsigned char vibSweep=reader.readC(); unsigned char vibDepth=reader.readC(); unsigned char vibRate=reader.readC(); unsigned short volFade=reader.readS(); reader.readS(); // reserved logV("%d",volEnvLen); logV("%d",panEnvLen); logV("%d",volSusPoint); logV("%d",volLoopStart); logV("%d",volLoopEnd); logV("%d",panSusPoint); logV("%d",panLoopStart); logV("%d",panLoopEnd); logV("%d",volType); logV("%d",panType); logV("%d",vibType); logV("%d",vibSweep); logV("%d",vibDepth); logV("%d",vibRate); logV("%d",volFade); if (!reader.seek(headerSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } // read samples for this instrument std::vector toAdd; for (int j=0; j16777216) { logE("abnormal sample size! %x",reader.tell()); lastError="bad sample size"; delete s; delete[] file; return false; } s->loopStart=reader.readI(); s->loopEnd=reader.readI()+s->loopStart; sampleVol[i][j]=reader.readC(); signed char fine=reader.readC(); unsigned char flags=reader.readC(); unsigned char pan=reader.readC(); unsigned char note=reader.readC(); logV("%d %d %d",fine,pan,note); switch (flags&3) { case 0: s->loop=false; break; case 1: s->loop=true; s->loopMode=DIV_SAMPLE_LOOP_FORWARD; break; case 2: s->loop=true; s->loopMode=DIV_SAMPLE_LOOP_PINGPONG; break; } reader.readC(); // reserved s->name=reader.readStringLatin1(22); s->depth=(flags&4)?DIV_SAMPLE_DEPTH_16BIT:DIV_SAMPLE_DEPTH_8BIT; s->init(numSamples); // seek here??? toAdd.push_back(s); } for (int j=0; jdepth==DIV_SAMPLE_DEPTH_16BIT) { short next=0; for (unsigned int i=0; isamples; i++) { next+=reader.readS(); s->data16[i]=next; } } else { signed char next=0; for (unsigned int i=0; isamples; i++) { next+=reader.readC(); s->data8[i]=next; } } } for (DivSample* i: toAdd) { ds.sample.push_back(i); } toAdd.clear(); } else { if (!reader.seek(headerSeek,SEEK_SET)) { logE("premature end of file!"); lastError="incomplete file"; delete[] file; return false; } } ds.ins.push_back(ins); } ds.sampleLen=ds.sample.size(); if (ds.sampleLen>256) { logE("too many samples!"); lastError="too many samples"; ds.unload(); delete[] file; return false; } // find subsongs ds.findSubSongs(totalChans); 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 (EndOfFileException& e) { //logE("premature end of file!"); lastError="incomplete file"; } catch (InvalidHeaderException& e) { //logE("invalid header!"); lastError="invalid header!"; } return success; }