/** * 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. */ // thanks asiekierka! // I have ported your code to this ROM export framework. #include "sapr.h" #include "../engine.h" #include "../ta-log.h" #include #include #include constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0; constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0; static String ticksToTime(double rate, int ticks) { double timing = ticks / rate; return fmt::sprintf("%02d:%02d.%03d", (int) timing / 60, (int) timing % 60, (int) (timing * 1000) % 1000 ); } void DivExportSAPR::run() { int sapScanlines=0; // TODO: property! int POKEY=-1; int IGNORED=0; // Locate system index. for (int i=0; isong.systemLen; i++) { if (e->song.system[i] == DIV_SYSTEM_POKEY) { if (POKEY>=0) { IGNORED++; logAppendf("Ignoring duplicate POKEY id %d",i); break; } POKEY=i; logAppendf("POKEY detected as chip id %d",i); break; } else { IGNORED++; logAppendf("Ignoring chip id %d, system id %d",i,(int)e->song.system[i]); break; } } if (POKEY<0) { logAppendf("ERROR: Could not find POKEY"); failed=true; running=false; return; } if (IGNORED>0) { logAppendf("WARNING: SAP export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' '); } bool palTiming=(e->song.systemFlags[POKEY].getInt("clockSel",0) != 0); int scanlinesPerFrame = (palTiming?312:262); size_t tickCount=0; std::vector> regs; if (sapScanlines <= 0) { sapScanlines = scanlinesPerFrame; } //double sapRate = (palTiming?49.86:59.92) * scanlinesPerFrame / sapScanlines; double sapRate = (palTiming?50:60) * (double)scanlinesPerFrame / (double)sapScanlines; e->stop(); e->repeatPattern=false; e->setOrder(0); logAppend("playing and logging register writes..."); e->synchronizedSoft([&]() { double origRate = e->got.rate; e->got.rate=sapRate; // Determine loop point. int loopOrder=0; int loopRow=0; int loopEnd=0; e->walkSong(loopOrder,loopRow,loopEnd); logAppendf("loop point: %d %d",loopOrder,loopRow); e->warnings=""; // Reset the playback state. e->curOrder=0; e->freelance=false; e->playing=false; e->extValuePresent=false; e->remainingLoops=-1; // Prepare to write song data. e->playSub(false); bool done=false; int fracWait=0; // accumulates fractional ticks e->disCont[POKEY].dispatch->toggleRegisterDump(true); std::array currRegs; while (!done) { if (e->nextTick(false,true) || !e->playing) { done=true; for (int i=0; isong.systemLen; i++) { e->disCont[i].dispatch->getRegisterWrites().clear(); } break; } // get register dumps std::vector& writes=e->disCont[POKEY].dispatch->getRegisterWrites(); if (writes.size() > 0) { logAppendf("saprOps: found %d messages",writes.size()); for (DivRegWrite& write: writes) if ((write.addr & 0xF) < 9) currRegs[write.addr & 0xF] = write.val; writes.clear(); } // write wait tickCount++; int totalWait=e->cycles>>MASTER_CLOCK_PREC; fracWait+=e->cycles&MASTER_CLOCK_MASK; totalWait+=fracWait>>MASTER_CLOCK_PREC; fracWait&=MASTER_CLOCK_MASK; if (totalWait>0 && !done) { while (totalWait) { regs.push_back(currRegs); totalWait--; tickCount++; } } } // end of song // done - close out. e->got.rate=origRate; e->disCont[POKEY].dispatch->toggleRegisterDump(false); e->remainingLoops=-1; e->playing=false; e->freelance=false; e->extValuePresent=false; }); logAppend("writing data..."); progress[0].amount=0.95f; auto w = new SafeWriter; w->init(); // Write SAP header: Author, name, timing, type. w->writeText(fmt::sprintf("SAP\r\nAUTHOR \"%s\"\r\nNAME \"%s\"\r\n%s\r\nTYPE R\r\n", e->song.author, e->song.name, palTiming ? "PAL" : "NTSC" )); if (sapScanlines != scanlinesPerFrame) { // Fastplay. w->writeText(fmt::sprintf("FASTPLAY %d\r\n", sapScanlines)); } // Time. w->writeText(fmt::sprintf("TIME %s\r\n", ticksToTime(sapRate, tickCount))); w->writeText("\r\n"); // Registers. for (auto atRegs : regs) { w->write(atRegs.data(), 9 * sizeof(uint8_t)); } output.push_back(DivROMExportOutput("export.sap",w)); progress[0].amount=1.0f; logAppend("finished!"); running=false; } bool DivExportSAPR::go(DivEngine* eng) { progress[0].name="Progress"; progress[0].amount=0.0f; e=eng; running=true; failed=false; mustAbort=false; exportThread=new std::thread(&DivExportSAPR::run,this); return true; } void DivExportSAPR::wait() { if (exportThread!=NULL) { logV("waiting for export thread..."); exportThread->join(); delete exportThread; } } void DivExportSAPR::abort() { mustAbort=true; wait(); } bool DivExportSAPR::isRunning() { return running; } bool DivExportSAPR::hasFailed() { return failed; } DivROMExportProgress DivExportSAPR::getProgress(int index) { if (index<0 || index>1) return progress[1]; return progress[index]; }