/** * 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 "engine.h" #include "../ta-log.h" #include /* #define WRITE_TICK(x) \ if (tick-lastTick[x]>255) { \ chanStream[x]->writeC(0xfc); \ chanStream[x]->writeS(tick-lastTick[x]); \ } else if (tick-lastTick[x]>1) { \ delayPopularity[tick-lastTick[x]]++; \ chanStream[x]->writeC(0xfd); \ chanStream[x]->writeC(tick-lastTick[x]); \ } else if (tick-lastTick[x]>0) { \ chanStream[x]->writeC(0xfe); \ } \ lastTick[x]=tick; \ */ int getInsLength(unsigned char ins) { switch (ins) { case 0xb8: // ins case 0xc0: // pre porta case 0xc3: // vib range case 0xc4: // vib shape case 0xc5: // pitch case 0xc7: // volume case 0xca: // legato case 0xfd: // waitc return 2; case 0xbe: // pan case 0xc2: // vibrato case 0xc6: // arpeggio case 0xc8: // vol slide case 0xc9: // porta return 3; // speed dial commands case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7: case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf: return 0; case 0xf0: // opt return 4; case 0xf2: // opt command case 0xf7: // cmd return 0; case 0xf4: // callsym case 0xf8: // callb16 case 0xfc: // waits return 3; case 0xf5: // call case 0xf6: // callb32 case 0xfa: // jmp case 0xfb: // rate return 5; } return 1; } void writePackedCommandValues(SafeWriter* w, const DivCommand& c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: if (c.value==DIV_NOTE_NULL) { w->writeC(0xb4); } else { w->writeC(CLAMP(c.value+60,0,0xb3)); } break; case DIV_CMD_NOTE_OFF: case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: case DIV_CMD_INSTRUMENT: case DIV_CMD_PANNING: case DIV_CMD_PRE_PORTA: case DIV_CMD_HINT_VIBRATO: case DIV_CMD_HINT_VIBRATO_RANGE: case DIV_CMD_HINT_VIBRATO_SHAPE: case DIV_CMD_HINT_PITCH: case DIV_CMD_HINT_ARPEGGIO: case DIV_CMD_HINT_VOLUME: case DIV_CMD_HINT_PORTA: case DIV_CMD_HINT_VOL_SLIDE: case DIV_CMD_HINT_VOL_SLIDE_TARGET: case DIV_CMD_HINT_LEGATO: w->writeC((unsigned char)c.cmd+0xb4); break; default: return; // quit for now... we'll implement this later w->writeC(0xf2); // unoptimized extended command w->writeC(c.cmd); break; } switch (c.cmd) { case DIV_CMD_HINT_LEGATO: if (c.value==DIV_NOTE_NULL) { w->writeC(0xff); } else { w->writeC(c.value+60); } break; case DIV_CMD_NOTE_ON: case DIV_CMD_NOTE_OFF: case DIV_CMD_NOTE_OFF_ENV: case DIV_CMD_ENV_RELEASE: break; case DIV_CMD_INSTRUMENT: case DIV_CMD_HINT_VIBRATO_RANGE: case DIV_CMD_HINT_VIBRATO_SHAPE: case DIV_CMD_HINT_PITCH: case DIV_CMD_HINT_VOLUME: w->writeC(c.value); break; case DIV_CMD_PANNING: case DIV_CMD_HINT_VIBRATO: case DIV_CMD_HINT_ARPEGGIO: case DIV_CMD_HINT_PORTA: w->writeC(c.value); w->writeC(c.value2); break; case DIV_CMD_PRE_PORTA: w->writeC((c.value?0x80:0)|(c.value2?0x40:0)); break; case DIV_CMD_HINT_VOL_SLIDE: w->writeS(c.value); break; case DIV_CMD_HINT_VOL_SLIDE_TARGET: w->writeS(c.value); w->writeS(c.value2); break; case DIV_CMD_SAMPLE_MODE: case DIV_CMD_SAMPLE_FREQ: case DIV_CMD_SAMPLE_BANK: case DIV_CMD_SAMPLE_POS: case DIV_CMD_SAMPLE_DIR: case DIV_CMD_FM_HARD_RESET: case DIV_CMD_FM_LFO: case DIV_CMD_FM_LFO_WAVE: case DIV_CMD_FM_FB: case DIV_CMD_FM_EXTCH: case DIV_CMD_FM_AM_DEPTH: case DIV_CMD_FM_PM_DEPTH: case DIV_CMD_STD_NOISE_FREQ: case DIV_CMD_STD_NOISE_MODE: case DIV_CMD_WAVE: case DIV_CMD_GB_SWEEP_TIME: case DIV_CMD_GB_SWEEP_DIR: case DIV_CMD_PCE_LFO_MODE: case DIV_CMD_PCE_LFO_SPEED: case DIV_CMD_NES_DMC: case DIV_CMD_C64_CUTOFF: case DIV_CMD_C64_RESONANCE: case DIV_CMD_C64_FILTER_MODE: case DIV_CMD_C64_RESET_TIME: case DIV_CMD_C64_RESET_MASK: case DIV_CMD_C64_FILTER_RESET: case DIV_CMD_C64_DUTY_RESET: case DIV_CMD_C64_EXTENDED: case DIV_CMD_AY_ENVELOPE_SET: case DIV_CMD_AY_ENVELOPE_LOW: case DIV_CMD_AY_ENVELOPE_HIGH: case DIV_CMD_AY_ENVELOPE_SLIDE: case DIV_CMD_AY_NOISE_MASK_AND: case DIV_CMD_AY_NOISE_MASK_OR: case DIV_CMD_AY_AUTO_ENVELOPE: case DIV_CMD_FDS_MOD_DEPTH: case DIV_CMD_FDS_MOD_HIGH: case DIV_CMD_FDS_MOD_LOW: case DIV_CMD_FDS_MOD_POS: case DIV_CMD_FDS_MOD_WAVE: case DIV_CMD_SAA_ENVELOPE: case DIV_CMD_AMIGA_FILTER: case DIV_CMD_AMIGA_AM: case DIV_CMD_AMIGA_PM: case DIV_CMD_MACRO_OFF: case DIV_CMD_MACRO_ON: case DIV_CMD_MACRO_RESTART: case DIV_CMD_HINT_ARP_TIME: w->writeC(1); // length w->writeC(c.value); break; case DIV_CMD_FM_TL: case DIV_CMD_FM_AM: case DIV_CMD_FM_AR: case DIV_CMD_FM_DR: case DIV_CMD_FM_SL: case DIV_CMD_FM_D2R: case DIV_CMD_FM_RR: case DIV_CMD_FM_DT: case DIV_CMD_FM_DT2: case DIV_CMD_FM_RS: case DIV_CMD_FM_KSR: case DIV_CMD_FM_VIB: case DIV_CMD_FM_SUS: case DIV_CMD_FM_WS: case DIV_CMD_FM_SSG: case DIV_CMD_FM_REV: case DIV_CMD_FM_EG_SHIFT: case DIV_CMD_FM_MULT: case DIV_CMD_FM_FINE: case DIV_CMD_AY_IO_WRITE: case DIV_CMD_AY_AUTO_PWM: case DIV_CMD_SURROUND_PANNING: w->writeC(2); // length w->writeC(c.value); w->writeC(c.value2); break; case DIV_CMD_C64_FINE_DUTY: case DIV_CMD_C64_FINE_CUTOFF: case DIV_CMD_LYNX_LFSR_LOAD: w->writeC(2); // length w->writeS(c.value); break; case DIV_CMD_FM_FIXFREQ: w->writeC(2); // length w->writeS((c.value<<12)|(c.value2&0x7ff)); break; case DIV_CMD_NES_SWEEP: w->writeC(1); // length w->writeC((c.value?8:0)|(c.value2&0x77)); break; default: logW("unimplemented command %s!",cmdName[c.cmd]); w->writeC(0); // length break; } } void reloc(unsigned char* buf, size_t len, unsigned int sourceAddr, unsigned int destAddr) { unsigned int delta=destAddr-sourceAddr; for (size_t i=0; i>8)&0xff; buf[i+3]=(addr>>16)&0xff; buf[i+4]=(addr>>24)&0xff; break; } } i+=insLen; } } SafeWriter* stripNops(SafeWriter* s) { std::unordered_map addrTable; SafeWriter* oldStream=s; unsigned char* buf=oldStream->getFinalBuf(); s=new SafeWriter; s->init(); // prepare address map size_t addr=0; for (size_t i=0; isize();) { int insLen=getInsLength(buf[i]); if (insLen<1) { logE("INS %x NOT IMPLEMENTED...",buf[i]); break; } addrTable[i]=addr; if (buf[i]!=0xf1) addr+=insLen; i+=insLen; } // translate addresses for (size_t i=0; isize();) { int insLen=getInsLength(buf[i]); if (insLen<1) { logE("INS %x NOT IMPLEMENTED...",buf[i]); break; } switch (buf[i]) { case 0xf5: // call case 0xfa: { // jmp unsigned int addr=buf[i+1]|(buf[i+2]<<8)|(buf[i+3]<<8)|(buf[i+4]<<24); try { addr=addrTable[addr]; buf[i+1]=addr&0xff; buf[i+2]=(addr>>8)&0xff; buf[i+3]=(addr>>16)&0xff; buf[i+4]=(addr>>24)&0xff; } catch (std::out_of_range& e) { logW("address %x is not mappable!",addr); } break; } } if (buf[i]!=0xf1) { s->write(&buf[i],insLen); } i+=insLen; } oldStream->finish(); delete oldStream; return s; } SafeWriter* DivEngine::saveCommand() { stop(); repeatPattern=false; shallStop=false; setOrder(0); BUSY_BEGIN_SOFT; // determine loop point int loopOrder=0; int loopRow=0; int loopEnd=0; walkSong(loopOrder,loopRow,loopEnd); logI("loop point: %d %d",loopOrder,loopRow); int cmdPopularity[256]; int delayPopularity[256]; int sortedCmdPopularity[16]; int sortedDelayPopularity[16]; unsigned char sortedCmd[16]; unsigned char sortedDelay[16]; SafeWriter* chanStream[DIV_MAX_CHANS]; unsigned int chanStreamOff[DIV_MAX_CHANS]; std::vector tickPos[DIV_MAX_CHANS]; int loopTick=-1; memset(cmdPopularity,0,256*sizeof(int)); memset(delayPopularity,0,256*sizeof(int)); memset(chanStream,0,DIV_MAX_CHANS*sizeof(void*)); memset(chanStreamOff,0,DIV_MAX_CHANS*sizeof(unsigned int)); memset(sortedCmdPopularity,0,16*sizeof(int)); memset(sortedDelayPopularity,0,16*sizeof(int)); memset(sortedCmd,0,16); memset(sortedDelay,0,16); SafeWriter* w=new SafeWriter; w->init(); // write header w->write("FCS",4); w->writeI(chans); // offsets for (int i=0; iinit(); w->writeI(0); } // preset delays and speed dial for (int i=0; i<32; i++) { w->writeC(0); } // play the song ourselves bool done=false; playSub(false); int tick=0; bool oldCmdStreamEnabled=cmdStreamEnabled; cmdStreamEnabled=true; double curDivider=divider; // PASS 0: play the song and log channel command streams while (!done) { for (int i=0; itell()); } if (loopTick==-1) { if (loopOrder==curOrder && loopRow==curRow) { if ((ticks-((tempoAccum+virtualTempoN)/virtualTempoD))<=0) { logI("loop is on tick %d",tick); loopTick=tick; // marker for (int i=0; iwriteC(0xf0); chanStream[i]->writeC(0x00); chanStream[i]->writeC(0x00); chanStream[i]->writeC(0x00); } } } } if (nextTick(false,true) || !playing) { done=true; break; } // get command stream if (curDivider!=divider) { curDivider=divider; chanStream[0]->writeC(0xfb); chanStream[0]->writeI((int)(curDivider*65536)); } for (DivCommand& i: cmdStream) { switch (i.cmd) { // strip away hinted/useless commands case DIV_CMD_GET_VOLUME: break; case DIV_CMD_VOLUME: break; case DIV_CMD_NOTE_PORTA: break; case DIV_CMD_LEGATO: break; case DIV_CMD_PITCH: break; case DIV_CMD_PRE_NOTE: break; default: cmdPopularity[i.cmd]++; writePackedCommandValues(chanStream[i.chan],i); break; } } cmdStream.clear(); for (int i=0; iwriteC(0xfe); } tick++; } if (!playing || loopTick<0) { for (int i=0; iwriteC(0xff); } } else { for (int i=0; iloopTick) { chanStream[i]->writeC(0xfa); chanStream[i]->writeI(tickPos[i][loopTick]); logD("chan %d loop addr: %x",i,tickPos[i][loopTick]); } else { logW("chan %d unable to find loop addr!",i); chanStream[i]->writeC(0xff); } } } logV("%d",tick); cmdStreamEnabled=oldCmdStreamEnabled; // PASS 1: condense delays // calculate delay usage for (int h=0; hgetFinalBuf(); int delayCount=0; for (size_t i=0; isize();) { int insLen=getInsLength(buf[i]); if (insLen<1) { logE("INS %x NOT IMPLEMENTED...",buf[i]); break; } if (buf[i]==0xfe) { delayCount++; } else { if (delayCount>1 && delayCount<=255) { delayPopularity[delayCount]++; } delayCount=0; } i+=insLen; } } // preset delays int sortCand=-1; int sortPos=0; while (sortPos<16) { sortCand=-1; for (int i=0; i<256; i++) { if (delayPopularity[i]) { if (sortCand==-1) { sortCand=i; } else if (delayPopularity[sortCand]getFinalBuf(); int delayPos=-1; int delayCount=0; int delayLast=0; for (size_t i=0; isize();) { int insLen=getInsLength(buf[i]); if (insLen<1) { logE("INS %x NOT IMPLEMENTED...",buf[i]); break; } if (buf[i]==0xfe) { if (delayPos==-1) delayPos=i; delayCount++; delayLast=i; } else { // finish the last delay if any if (delayPos!=-1) { if (delayCount>1) { if (delayLast255) { buf[delayPos++]=0xfc; buf[delayPos++]=delayCount&0xff; buf[delayPos++]=(delayCount>>8)&0xff; } else { bool foundShort=false; for (int j=0; j<16; j++) { if (sortedDelay[j]==delayCount) { buf[delayPos++]=0xe0+j; foundShort=true; break; } } if (!foundShort) { buf[delayPos++]=0xfd; buf[delayPos++]=delayCount; } } // fill with nop for (int j=delayPos; j<=delayLast; j++) { buf[j]=0xf1; } } } delayPos=-1; delayCount=0; } } i+=insLen; } } // PASS 2: remove nop's // this includes modifying call addresses to compensate for (int h=0; hgetFinalBuf(); unsigned char group[256]; // max offset is -255 size_t groupLen=0; memset(group,0,256); // 3 is the minimum loop size that can be reliably optimized logI("finding loop in chan %d",h); for (int groupSize=3; groupSize<256; groupSize++) { bool foundSomething=false; //logD("...try size %d",groupSize); for (size_t searchPos=0; searchPossize();) { int insLen=getInsLength(buf[searchPos]); groupLen=0; if (insLen<1) { logE("INS %x NOT IMPLEMENTED...",buf[searchPos]); break; } // copy a block for (int i=0; isize();) { int insLenI=getInsLength(buf[searchPos+i]); if (insLenI<1) { logE("INS %x NOT IMPLEMENTED...",buf[searchPos+i]); break; } i+=insLenI; if ((int)groupLen+insLenI>groupSize) break; groupLen+=insLenI; } // don't do anything if we don't have a block if (!groupLen) { searchPos+=insLen; continue; } memcpy(group,&buf[searchPos],groupLen); // find contiguous blocks size_t searchPos1=searchPos+groupLen; size_t posOfFirstBlock=searchPos1; int loopCount=0; while (true) { // stop if we're near the end if (searchPos1>=chanStream[h]->size()) break; // compare next block to group if (memcmp(&buf[searchPos1],group,groupLen)!=0) break; // if we're here, we found a contiguous block searchPos1+=groupLen; loopCount++; // don't loop more than 255 times if (loopCount>=255) break; } if (loopCount>0) { // write loop command logD("- LOOP: %x (size %d, %d times)",searchPos,groupLen,loopCount); buf[posOfFirstBlock++]=0xf3; buf[posOfFirstBlock++]=groupLen; buf[posOfFirstBlock++]=loopCount; // set the rest to nop while (posOfFirstBlockgetFinalBuf(); } } } // PASS 3: optimize command calls /* int sortCand=-1; int sortPos=0; while (sortPos<16) { sortCand=-1; for (int i=DIV_CMD_SAMPLE_MODE; i<256; i++) { if (cmdPopularity[i]) { if (sortCand==-1) { sortCand=i; } else if (cmdPopularity[sortCand]toReader(); chanStream[i]=new SafeWriter; chanStream[i]->init(); while (1) { try { unsigned char next=reader->readC(); switch (next) { case 0xb8: // instrument case 0xc0: // pre porta case 0xc3: // vibrato range case 0xc4: // vibrato shape case 0xc5: // pitch case 0xc7: // volume case 0xca: // legato chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); break; case 0xbe: // panning case 0xc2: // vibrato case 0xc6: // arpeggio case 0xc8: // vol slide case 0xc9: // porta chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); break; case 0xf2: { // full command (pre) unsigned char cmd=reader->readC(); bool foundShort=false; for (int j=0; j<16; j++) { if (sortedCmd[j]==cmd) { chanStream[i]->writeC(0xd0+j); foundShort=true; break; } } if (!foundShort) { chanStream[i]->writeC(0xf7); // full command chanStream[i]->writeC(cmd); } unsigned char cmdLen=reader->readC(); logD("cmdLen: %d",cmdLen); for (unsigned char j=0; jreadC(); chanStream[i]->writeC(next); } break; } case 0xfb: // tick rate chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); next=reader->readC(); chanStream[i]->writeC(next); break; case 0xfc: { // 16-bit wait unsigned short delay=reader->readS(); bool foundShort=false; for (int j=0; j<16; j++) { if (sortedDelay[j]==delay) { chanStream[i]->writeC(0xe0+j); foundShort=true; break; } } if (!foundShort) { chanStream[i]->writeC(next); chanStream[i]->writeS(delay); } break; } case 0xfd: { // 8-bit wait unsigned char delay=reader->readC(); bool foundShort=false; for (int j=0; j<16; j++) { if (sortedDelay[j]==delay) { chanStream[i]->writeC(0xe0+j); foundShort=true; break; } } if (!foundShort) { chanStream[i]->writeC(next); chanStream[i]->writeC(delay); } break; } default: chanStream[i]->writeC(next); break; } } catch (EndOfFileException& e) { break; } } oldStream->finish(); delete oldStream; }*/ // write results for (int i=0; itell(); logI("- %d: off %x size %ld",i,chanStreamOff[i],chanStream[i]->size()); reloc(chanStream[i]->getFinalBuf(),chanStream[i]->size(),0,w->tell()); w->write(chanStream[i]->getFinalBuf(),chanStream[i]->size()); chanStream[i]->finish(); delete chanStream[i]; } w->seek(8,SEEK_SET); for (int i=0; iwriteI(chanStreamOff[i]); } logD("delay popularity:"); for (int i=0; i<16; i++) { w->writeC(sortedDelay[i]); if (sortedDelayPopularity[i]) logD("- %d: %d",sortedDelay[i],sortedDelayPopularity[i]); } logD("command popularity:"); for (int i=0; i<16; i++) { w->writeC(sortedCmd[i]); if (sortedCmdPopularity[i]) logD("- %s: %d",cmdName[sortedCmd[i]],sortedCmdPopularity[i]); } remainingLoops=-1; playing=false; freelance=false; extValuePresent=false; BUSY_END; return w; }