From 055266090af6eb319f1e14d0dd646f121352c0e3 Mon Sep 17 00:00:00 2001 From: AnnoyedArt1256 <66080161+AnnoyedArt1256@users.noreply.github.com> Date: Sun, 15 Jun 2025 01:29:59 +0300 Subject: [PATCH] add iPod and GRUB bootloader beeper tune export (#2441) --- CMakeLists.txt | 2 + src/engine/engine.h | 2 + src/engine/export.cpp | 8 ++ src/engine/export.h | 2 + src/engine/export/grub.cpp | 204 +++++++++++++++++++++++++++++++++++++ src/engine/export/grub.h | 38 +++++++ src/engine/export/ipod.cpp | 194 +++++++++++++++++++++++++++++++++++ src/engine/export/ipod.h | 38 +++++++ src/engine/exportDef.cpp | 23 +++++ src/gui/about.cpp | 1 + src/gui/exportOptions.cpp | 10 ++ 11 files changed, 522 insertions(+) create mode 100644 src/engine/export/grub.cpp create mode 100644 src/engine/export/grub.h create mode 100644 src/engine/export/ipod.cpp create mode 100644 src/engine/export/ipod.h diff --git a/CMakeLists.txt b/CMakeLists.txt index acd047599..179b27c11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -822,6 +822,8 @@ src/engine/export/amigaValidation.cpp src/engine/export/sapr.cpp src/engine/export/tiuna.cpp src/engine/export/zsm.cpp +src/engine/export/ipod.cpp +src/engine/export/grub.cpp src/engine/effect/abstract.cpp src/engine/effect/dummy.cpp diff --git a/src/engine/engine.h b/src/engine/engine.h index 2ec8a7029..264349447 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -675,6 +675,8 @@ class DivEngine { friend class DivExportSAPR; friend class DivExportTiuna; friend class DivExportZSM; + friend class DivExportiPod; + friend class DivExportGRUB; public: DivSong song; diff --git a/src/engine/export.cpp b/src/engine/export.cpp index c7d1e6257..c50bb7132 100644 --- a/src/engine/export.cpp +++ b/src/engine/export.cpp @@ -23,6 +23,8 @@ #include "export/sapr.h" #include "export/tiuna.h" #include "export/zsm.h" +#include "export/ipod.h" +#include "export/grub.h" DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) { DivROMExport* exporter=NULL; @@ -39,6 +41,12 @@ DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) { case DIV_ROM_SAP_R: exporter=new DivExportSAPR; break; + case DIV_ROM_IPOD: + exporter=new DivExportiPod; + break; + case DIV_ROM_GRUB: + exporter=new DivExportGRUB; + break; default: exporter=new DivROMExport; break; diff --git a/src/engine/export.h b/src/engine/export.h index 0e9e2d8ba..fc05ed949 100644 --- a/src/engine/export.h +++ b/src/engine/export.h @@ -32,6 +32,8 @@ enum DivROMExportOptions { DIV_ROM_ZSM, DIV_ROM_TIUNA, DIV_ROM_SAP_R, + DIV_ROM_IPOD, + DIV_ROM_GRUB, DIV_ROM_MAX }; diff --git a/src/engine/export/grub.cpp b/src/engine/export/grub.cpp new file mode 100644 index 000000000..d299ed2a0 --- /dev/null +++ b/src/engine/export/grub.cpp @@ -0,0 +1,204 @@ +/** + * 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 "grub.h" +#include "../engine.h" +#include "../ta-log.h" +#include +#include +#include + +void DivExportGRUB::run() { + bool grubExportBin=conf.getBool("exportBin",false); + + int BEEPER=-1; + int IGNORED=0; + + // Locate system index. + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i] == DIV_SYSTEM_PCSPKR) { + if (BEEPER>=0) { + IGNORED++; + logAppendf("Ignoring duplicate Beeper id %d",i); + break; + } + BEEPER=i; + logAppendf("PC Speaker 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 (BEEPER<0) { + logAppendf("ERROR: Could not find PC Speaker/Beeper"); + failed=true; + running=false; + return; + } + if (IGNORED>0) { + logAppendf("WARNING: iPod .tone export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' '); + } + + size_t tickCount=0; + + e->stop(); + e->repeatPattern=false; + e->setOrder(0); + + logAppend("playing and logging register writes..."); + + int oldFreq = 0; + int freq = 0; + + e->synchronizedSoft([&]() { + double origRate = e->got.rate; + double rate = MIN(e->curSubSong->hz,1000.0); + logAppendf("export rate is %d hz",(int)rate); + int tempo = (int)(60000.0/(1000.0/rate)); + e->got.rate=rate; + + // 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=""; + + auto w = new SafeWriter; + w->init(); + + // Reset the playback state. + e->curOrder=0; + e->freelance=false; + e->playing=false; + e->extValuePresent=false; + e->remainingLoops=-1; + + e->disCont[BEEPER].dispatch->toggleRegisterDump(true); + + // Prepare to write song data. + e->playSub(false); + bool done=false; + + logAppend("writing data..."); + progress[0].amount=0.15f; + + int wait_tempo = 0; + if (grubExportBin) + w->writeI(tempo); // write tempo + else + w->writeText(fmt::sprintf("%d",tempo)); // write tempo + + while (!done) { + if (e->nextTick(false,true) || !e->playing) { + done=true; + } + + // get register dumps + uint8_t* regPool = e->disCont[BEEPER].dispatch->getRegisterPool(); + int chipClock = e->disCont[BEEPER].dispatch->chipClock; + freq = (int)(regPool[0]|(regPool[1]<<8)); + if (freq > 0) freq = chipClock/freq; + + // write wait + tickCount++; + int totalWait=e->cycles; + if (totalWait>0 && !done) { + while (totalWait) { + wait_tempo++; + if (freq != oldFreq || wait_tempo == 65535) { + if (grubExportBin) { + w->writeS(oldFreq); // pitch + w->writeS(wait_tempo); // duration + } else { + w->writeText(fmt::sprintf(" %d %d", oldFreq, wait_tempo)); + } + oldFreq = freq; + wait_tempo = 0; + } + totalWait--; + tickCount++; + } + } + } + + if (!grubExportBin) w->writeText(fmt::sprintf("\n")); // end song + // end of song + + // done - close out. + e->got.rate=origRate; + e->disCont[BEEPER].dispatch->getRegisterWrites().clear(); + e->disCont[BEEPER].dispatch->toggleRegisterDump(false); + + e->remainingLoops=-1; + e->playing=false; + e->freelance=false; + e->extValuePresent=false; + + output.push_back(DivROMExportOutput(grubExportBin?"export.bin":"export.txt",w)); + }); + + + progress[0].amount=1.0f; + + logAppend("finished!"); + + running=false; +} + +bool DivExportGRUB::go(DivEngine* eng) { + progress[0].name="Progress"; + progress[0].amount=0.0f; + + e=eng; + running=true; + failed=false; + mustAbort=false; + exportThread=new std::thread(&DivExportGRUB::run,this); + return true; +} + +void DivExportGRUB::wait() { + if (exportThread!=NULL) { + logV("waiting for export thread..."); + exportThread->join(); + delete exportThread; + } +} + +void DivExportGRUB::abort() { + mustAbort=true; + wait(); +} + +bool DivExportGRUB::isRunning() { + return running; +} + +bool DivExportGRUB::hasFailed() { + return failed; +} + +DivROMExportProgress DivExportGRUB::getProgress(int index) { + if (index<0 || index>1) return progress[1]; + return progress[index]; +} diff --git a/src/engine/export/grub.h b/src/engine/export/grub.h new file mode 100644 index 000000000..bb5efa76c --- /dev/null +++ b/src/engine/export/grub.h @@ -0,0 +1,38 @@ +/** + * 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 "../export.h" + +#include + +class DivExportGRUB: public DivROMExport { + DivEngine* e; + std::thread* exportThread; + DivROMExportProgress progress[2]; + bool running, failed, mustAbort; + void run(); + public: + bool go(DivEngine* e); + bool isRunning(); + bool hasFailed(); + void abort(); + void wait(); + DivROMExportProgress getProgress(int index=0); + ~DivExportGRUB() {} +}; diff --git a/src/engine/export/ipod.cpp b/src/engine/export/ipod.cpp new file mode 100644 index 000000000..deadebc17 --- /dev/null +++ b/src/engine/export/ipod.cpp @@ -0,0 +1,194 @@ +/** + * 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 used your SAP-R export code as a base for this) + +#include "ipod.h" +#include "../engine.h" +#include "../ta-log.h" +#include +#include +#include + +void DivExportiPod::run() { + int BEEPER=-1; + int IGNORED=0; + + // Locate system index. + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i] == DIV_SYSTEM_PCSPKR) { + if (BEEPER>=0) { + IGNORED++; + logAppendf("Ignoring duplicate Beeper id %d",i); + break; + } + BEEPER=i; + logAppendf("PC Speaker 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 (BEEPER<0) { + logAppendf("ERROR: Could not find PC Speaker/Beeper"); + failed=true; + running=false; + return; + } + if (IGNORED>0) { + logAppendf("WARNING: iPod .tone export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' '); + } + + size_t tickCount=0; + + double rate = 1000.0; + + e->stop(); + e->repeatPattern=false; + e->setOrder(0); + + logAppend("playing and logging register writes..."); + + int oldFreq = 0; + int freq = 0; + + e->synchronizedSoft([&]() { + double origRate = e->got.rate; + e->got.rate=rate; + + // 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=""; + + auto w = new SafeWriter; + w->init(); + + w->writeText(fmt::sprintf("%s\n", e->song.name)); + + // Reset the playback state. + e->curOrder=0; + e->freelance=false; + e->playing=false; + e->extValuePresent=false; + e->remainingLoops=-1; + + e->disCont[BEEPER].dispatch->toggleRegisterDump(true); + + // Prepare to write song data. + e->playSub(false); + bool done=false; + + logAppend("writing data..."); + progress[0].amount=0.15f; + + int wait_ms = 0; + + while (!done) { + if (e->nextTick(false,true) || !e->playing) { + done=true; + } + + // get register dumps + uint8_t* regPool = e->disCont[BEEPER].dispatch->getRegisterPool(); + int chipClock = e->disCont[BEEPER].dispatch->chipClock; + freq = (int)(regPool[0]|(regPool[1]<<8)); + if (freq > 0) freq = chipClock/freq; + + // write wait + tickCount++; + int totalWait=e->cycles; + if (totalWait>0 && !done) { + while (totalWait) { + wait_ms++; + if (freq != oldFreq) { + w->writeText(fmt::sprintf("%d %d\n", oldFreq, wait_ms)); + oldFreq = freq; + wait_ms = 0; + } + totalWait--; + tickCount++; + } + } + } + // end of song + + // done - close out. + e->got.rate=origRate; + e->disCont[BEEPER].dispatch->getRegisterWrites().clear(); + e->disCont[BEEPER].dispatch->toggleRegisterDump(false); + + e->remainingLoops=-1; + e->playing=false; + e->freelance=false; + e->extValuePresent=false; + + output.push_back(DivROMExportOutput("export.tone",w)); + }); + + + progress[0].amount=1.0f; + + logAppend("finished!"); + + running=false; +} + +bool DivExportiPod::go(DivEngine* eng) { + progress[0].name="Progress"; + progress[0].amount=0.0f; + + e=eng; + running=true; + failed=false; + mustAbort=false; + exportThread=new std::thread(&DivExportiPod::run,this); + return true; +} + +void DivExportiPod::wait() { + if (exportThread!=NULL) { + logV("waiting for export thread..."); + exportThread->join(); + delete exportThread; + } +} + +void DivExportiPod::abort() { + mustAbort=true; + wait(); +} + +bool DivExportiPod::isRunning() { + return running; +} + +bool DivExportiPod::hasFailed() { + return failed; +} + +DivROMExportProgress DivExportiPod::getProgress(int index) { + if (index<0 || index>1) return progress[1]; + return progress[index]; +} diff --git a/src/engine/export/ipod.h b/src/engine/export/ipod.h new file mode 100644 index 000000000..4d3d5e794 --- /dev/null +++ b/src/engine/export/ipod.h @@ -0,0 +1,38 @@ +/** + * 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 "../export.h" + +#include + +class DivExportiPod: public DivROMExport { + DivEngine* e; + std::thread* exportThread; + DivROMExportProgress progress[2]; + bool running, failed, mustAbort; + void run(); + public: + bool go(DivEngine* e); + bool isRunning(); + bool hasFailed(); + void abort(); + void wait(); + DivROMExportProgress getProgress(int index=0); + ~DivExportiPod() {} +}; diff --git a/src/engine/exportDef.cpp b/src/engine/exportDef.cpp index 68a3be16d..31766f954 100644 --- a/src/engine/exportDef.cpp +++ b/src/engine/exportDef.cpp @@ -119,4 +119,27 @@ void DivEngine::registerROMExports() { }, false, DIV_REQPOL_EXACT ); + + romExportDefs[DIV_ROM_IPOD]=new DivROMExportDef( + "iPod .tone alarm", "AArt1256", + "iPod Beeper (.tone) Alarm export\n" + "for playback, you can drag the resulting file\n" + "into iPod_Control/Tones to your iPod IN DISK MODE", + "alarm tone files", ".tone", + { + DIV_SYSTEM_PCSPKR + }, + false, DIV_REQPOL_ANY + ); + + romExportDefs[DIV_ROM_GRUB]=new DivROMExportDef( + "GRUB_INIT_TUNE", "AArt1256", + "GRUB_INIT_TUNE export\n" + "for use with the GRUB bootloader using the \"play\" command", + "Text/Binary files", NULL, + { + DIV_SYSTEM_PCSPKR + }, + false, DIV_REQPOL_ANY + ); } diff --git a/src/gui/about.cpp b/src/gui/about.cpp index 6ea41b999..c17f14a13 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -34,6 +34,7 @@ const char* aboutLine[]={ "", _N("-- program --"), "tildearrow", + "AArt1256", _N("A M 4 N (intro tune)"), "Adam Lederer", "akumanatt", diff --git a/src/gui/exportOptions.cpp b/src/gui/exportOptions.cpp index 71f0f90fa..9e162282f 100644 --- a/src/gui/exportOptions.cpp +++ b/src/gui/exportOptions.cpp @@ -371,6 +371,16 @@ void FurnaceGUI::drawExportROM(bool onWindow) { } break; } + case DIV_ROM_GRUB: { + bool grubExportBin=romConfig.getBool("exportBin",false); + if (ImGui::Checkbox(_("export binary file"),&grubExportBin)) { + altered=true; + } + if (altered) { + romConfig.set("exportBin",grubExportBin); + } + break; + } case DIV_ROM_ABSTRACT: ImGui::TextWrapped("%s",_("select a target from the menu at the top of this dialog.")); break;