/** * 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 "engine.h" #include "../ta-log.h" #define WRITE_TICK(x) \ if (binary) { \ if (!wroteTick[x]) { \ wroteTick[x]=true; \ 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; \ } \ } else { \ if (!wroteTickGlobal) { \ wroteTickGlobal=true; \ w->writeText(fmt::sprintf(">> TICK %d\n",tick)); \ } \ } 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_LEGATO: w->writeC((unsigned char)c.cmd+0xb4); break; default: w->writeC(0xf0); // 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_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_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; } } SafeWriter* DivEngine::saveCommand(bool binary) { 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]; bool wroteTick[DIV_MAX_CHANS]; 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 if (binary) { 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); } } else { w->writeText("# Furnace Command Stream\n\n"); w->writeText("[Information]\n"); w->writeText(fmt::sprintf("name: %s\n",song.name)); w->writeText(fmt::sprintf("author: %s\n",song.author)); w->writeText(fmt::sprintf("category: %s\n",song.category)); w->writeText(fmt::sprintf("system: %s\n",song.systemName)); w->writeText("\n"); w->writeText("[SubSongInformation]\n"); w->writeText(fmt::sprintf("name: %s\n",curSubSong->name)); w->writeText(fmt::sprintf("tickRate: %f\n",curSubSong->hz)); w->writeText("\n"); w->writeText("[SysDefinition]\n"); // TODO w->writeText("\n"); } // play the song ourselves bool done=false; playSub(false); if (!binary) { w->writeText("[Stream]\n"); } int tick=0; bool oldCmdStreamEnabled=cmdStreamEnabled; cmdStreamEnabled=true; double curDivider=divider; int lastTick[DIV_MAX_CHANS]; memset(lastTick,0,DIV_MAX_CHANS*sizeof(int)); while (!done) { if (nextTick(false,true) || !playing) { done=true; } // get command stream bool wroteTickGlobal=false; memset(wroteTick,0,DIV_MAX_CHANS*sizeof(bool)); if (curDivider!=divider) { curDivider=divider; WRITE_TICK(0); if (binary) { chanStream[0]->writeC(0xfb); chanStream[0]->writeI((int)(curDivider*65536)); } else { w->writeText(fmt::sprintf(">> SET_RATE %f\n",curDivider)); } } for (DivCommand& i: cmdStream) { switch (i.cmd) { // strip away hinted/useless commands case DIV_ALWAYS_SET_VOLUME: break; 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: WRITE_TICK(i.chan); if (binary) { cmdPopularity[i.cmd]++; writePackedCommandValues(chanStream[i.chan],i); } else { w->writeText(fmt::sprintf(" %d: %s %d %d\n",i.chan,cmdName[i.cmd],i.value,i.value2)); } break; } } cmdStream.clear(); tick++; } cmdStreamEnabled=oldCmdStreamEnabled; if (binary) { 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]writeC(0xff); // optimize stream SafeWriter* oldStream=chanStream[i]; SafeReader* reader=oldStream->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 0xf0: { // 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; } for (int i=0; itell(); logI("- %d: off %x size %ld",i,chanStreamOff[i],chanStream[i]->size()); 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]); } } else { if (!playing) { w->writeText(">> END\n"); } else { w->writeText(">> LOOP 0\n"); } } remainingLoops=-1; playing=false; freelance=false; extValuePresent=false; BUSY_END; return w; }