From 68979270ad5bec5e6161e380d2a3ad133a5e02b1 Mon Sep 17 00:00:00 2001 From: AArt1256 Date: Thu, 22 Jan 2026 16:40:25 +0100 Subject: [PATCH] basic export implemented (still need to implement TIA and samples) --- CMakeLists.txt | 1 + src/engine/engine.h | 1 + src/engine/export.cpp | 4 + src/engine/export.h | 1 + src/engine/export/saxotone.cpp | 311 +++++++++++++++++++++++++++++++++ src/engine/export/saxotone.h | 38 ++++ src/engine/exportDef.cpp | 11 ++ 7 files changed, 367 insertions(+) create mode 100644 src/engine/export/saxotone.cpp create mode 100644 src/engine/export/saxotone.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 60463c231..3c57f052c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -896,6 +896,7 @@ src/engine/export/tiuna.cpp src/engine/export/zsm.cpp src/engine/export/ipod.cpp src/engine/export/grub.cpp +src/engine/export/saxotone.cpp src/engine/effect/abstract.cpp src/engine/effect/dummy.cpp diff --git a/src/engine/engine.h b/src/engine/engine.h index 58607e1ae..408296c2c 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -585,6 +585,7 @@ class DivEngine { friend class DivExportZSM; friend class DivExportiPod; friend class DivExportGRUB; + friend class DivExportSaxotone; public: DivSong song; diff --git a/src/engine/export.cpp b/src/engine/export.cpp index cbfbb475c..65355939a 100644 --- a/src/engine/export.cpp +++ b/src/engine/export.cpp @@ -25,6 +25,7 @@ #include "export/zsm.h" #include "export/ipod.h" #include "export/grub.h" +#include "export/saxotone.h" DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) { DivROMExport* exporter=NULL; @@ -47,6 +48,9 @@ DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) { case DIV_ROM_GRUB: exporter=new DivExportGRUB; break; + case DIV_ROM_SAXOTONE: + exporter=new DivExportSaxotone; + break; default: exporter=new DivROMExport; break; diff --git a/src/engine/export.h b/src/engine/export.h index 497e56b0a..b54f39a6d 100644 --- a/src/engine/export.h +++ b/src/engine/export.h @@ -34,6 +34,7 @@ enum DivROMExportOptions { DIV_ROM_SAP_R, DIV_ROM_IPOD, DIV_ROM_GRUB, + DIV_ROM_SAXOTONE, DIV_ROM_MAX }; diff --git a/src/engine/export/saxotone.cpp b/src/engine/export/saxotone.cpp new file mode 100644 index 000000000..2de098e81 --- /dev/null +++ b/src/engine/export/saxotone.cpp @@ -0,0 +1,311 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2026 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 "saxotone.h" +#include "../engine.h" +#include "../ta-log.h" +#include +#include +#include + +void saxotone_write_table_lo(SafeWriter *w, unsigned int *offs, size_t off_amt) { + w->writeText(".byte "); + for (size_t i = 0; i < off_amt; i++) { + w->writeText(fmt::sprintf("<($%04x+sample_dir)%c",offs[i],i==(off_amt-1)?'\n':',')); + } +} + +void saxotone_write_table_hi(SafeWriter *w, unsigned int *offs, size_t off_amt) { + w->writeText(".byte "); + for (size_t i = 0; i < off_amt; i++) { + w->writeText(fmt::sprintf("<($%04x+sample_dir)%c",offs[i],i==(off_amt-1)?'\n':',')); + } +} + +void DivExportSaxotone::run() { + int SAXOTONE=-1; + int TIA=-1; + int IGNORED=0; + + // Locate system index. + for (int i=0; isong.systemLen; i++) { + if (e->song.system[i] == DIV_SYSTEM_SAXOTONE) { + if (SAXOTONE>=0) { + IGNORED++; + logAppendf("Ignoring duplicate Saxotone id %d",i); + continue; + } + SAXOTONE=i; + logAppendf("Saxotone detected as chip id %d",i); + continue; + } else if (e->song.system[i] == DIV_SYSTEM_TIA) { + if (TIA>=0) { + IGNORED++; + logAppendf("Ignoring duplicate TIA id %d",i); + continue; + } + TIA=i; + logAppendf("TIA detected as chip id %d",i); + continue; + } else { + IGNORED++; + logAppendf("Ignoring chip id %d, system id %d",i,(int)e->song.system[i]); + continue; + } + } + if (SAXOTONE<0) { + logAppendf("ERROR: Could not find Saxotone"); + failed=true; + running=false; + return; + } + if (TIA<0) { + logAppendf("ERROR: Could not find TIA"); + failed=true; + running=false; + return; + } + if (IGNORED>0) { + logAppendf("WARNING: ASM export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' '); + } + + e->stop(); + e->setOrder(0); + + logAppend("playing and logging register writes..."); + + e->synchronizedSoft([&]() { + e->warnings=""; + auto w = new SafeWriter; + w->init(); + + e->disCont[SAXOTONE].dispatch->renderSamples(0); + const DivMemoryComposition *memCompo = e->disCont[SAXOTONE].dispatch->getMemCompo(0); + unsigned int wtOff[256]; + unsigned int sampOff[256]; + unsigned int sampEnd[256]; + int wtOff_ind = 0; + int samp_ind = 0; + for (const DivMemoryEntry& k: memCompo->entries) { + if (k.type == DIV_MEMORY_WAVE_STATIC) { + wtOff[wtOff_ind++] = k.begin; + } else if (k.type == DIV_MEMORY_SAMPLE) { + sampOff[samp_ind] = k.begin; + sampEnd[samp_ind] = k.end; + samp_ind++; + } + } + + // wavetab O + // samptab O + // samptabend O + // instab/wav O + // freqtab O + // ords(0-4) ? + // ordswait X + int wave_amt = e->song.waveLen; + int samp_amt = e->song.sampleLen; + int ins_amt = e->song.insLen; + + // wavetable pointers + w->writeText(".page $100\n"); + w->writeText("wavetablo\n"); + saxotone_write_table_lo(w, wtOff, wave_amt); + + w->writeText("wavetabhi\n"); + saxotone_write_table_hi(w, wtOff, wave_amt); + + w->writeText(".endpage\n\n"); + + // sample pointers + w->writeText("samptablo\n"); + saxotone_write_table_lo(w, sampOff, samp_amt); + + w->writeText("samptabhi\n"); + saxotone_write_table_hi(w, sampOff, samp_amt); + + w->writeText("samptabend\n"); + saxotone_write_table_hi(w, sampEnd, samp_amt); + + w->writeText("\n"); + + // waveform MACRO pointers + w->writeText("instablo\n"); + w->writeText(".byte "); + for (int i = 0; i < ins_amt; i++) { + w->writeText(fmt::sprintf("<(inswav_%d)%c",i,i==(ins_amt-1)?'\n':',')); + } + w->writeText("instabhi\n"); + w->writeText(".byte "); + for (int i = 0; i < ins_amt; i++) { + w->writeText(fmt::sprintf(">(inswav_%d)%c",i,i==(ins_amt-1)?'\n':',')); + } + w->writeText("\n"); + + for (int i = 0; i < ins_amt; i++) { + w->writeText(fmt::sprintf("inswav_%d\n.byte ",i)); + unsigned char wave_len = e->song.ins[i]->std.waveMacro.len; + unsigned char wave_loop = e->song.ins[i]->std.waveMacro.loop; + // if one-shot, set the loop point the last wave macro position + if (wave_loop == 0xFF) wave_loop = wave_len-1; + for (int x = 0; x < wave_len; x++) { + w->writeText(fmt::sprintf("$%02x,", e->song.ins[i]->std.waveMacro.val[x]+1)); + } + w->writeText(fmt::sprintf("$00,$%02x\n", wave_loop)); // loop + } + + // get wave length (for the freq table) + int waveWidth; + switch(e->song.systemFlags[SAXOTONE].getInt("waveWidth",1)) { + case 0: waveWidth=16; break; + case 2: waveWidth=64; break; + case 3: waveWidth=128; break; + case 4: waveWidth=256; break; + default: waveWidth=32; break; + } + int CHIP_FREQBASE = 32*76*waveWidth; + unsigned short note_freq[256]; + int used_note_index[256]; + for (int i = 0; i < 256; i++) used_note_index[i] = -1; + unsigned char note_ind = 0; + int ord_len = e->curSubSong->ordersLen; + // order pointers + // TOOD: what to do for the TIA channel? + for (int ch = 0; ch < 4; ch++) { + int pat_amt = 0; + for (int pat = 0; pat < ord_len; pat++) { + if (e->curSubSong->orders.ord[ch][pat] >= pat_amt) + pat_amt = e->curSubSong->orders.ord[ch][pat]; + } + pat_amt++; + + w->writeText(fmt::sprintf("ord%dlo\n",ch)); + w->writeText(".byte "); + for (int i = 0; i < ord_len; i++) { + w->writeText(fmt::sprintf("<(ord%d_%02x)%c",ch,i,i==(ord_len-1)?'\n':',')); + } + w->writeText(fmt::sprintf("ord%dhi\n",ch)); + w->writeText(".byte "); + for (int i = 0; i < ord_len; i++) { + w->writeText(fmt::sprintf(">(ord%d_%02x)%c",ch,i,i==(ord_len-1)?'\n':',')); + } + w->writeText("\n"); + + // get the OPTMIZED frequency table and pattern data + for (int pat = 0; pat < pat_amt; pat++) { + w->writeText(fmt::sprintf("ord%d_%02x\n",ch,pat)); + for (int row = 0; row < e->curSubSong->patLen; row++) { + short *cur_row = e->curSubSong->pat[ch].getPattern(pat, false)->newData[row]; + short note = cur_row[DIV_PAT_NOTE]; + if (note >= 0 && note < 180) { + if (used_note_index[note] == -1) { + // note from C-(-5) to B-9 + // NOTE_FREQUENCY(parent->calcArp(chan[i].note,chan[i].std.arp.val)); + double baseFreq = e->calcBaseFreq(e->disCont[SAXOTONE].dispatch->chipClock, + CHIP_FREQBASE, + note-60, + false); + // parent->calcFreq(chan[i].baseFreq,chan[i].pitch,chan[i].fixedArp?chan[i].baseNoteOverride:chan[i].arpOff,chan[i].fixedArp,false,2,chan[i].pitch2,chipClock,CHIP_FREQBASE); + int freq = e->calcFreq(baseFreq,0,0,false,false,2,0,e->disCont[SAXOTONE].dispatch->chipClock,CHIP_FREQBASE); + printf("%d\n",freq); + if (freq<0) freq=0; + if (freq>((int)waveWidth<<7)) freq=(int)waveWidth<<7; + note_freq[note_ind] = (unsigned short)freq; + used_note_index[note] = note_ind; + note_ind++; + } + note = used_note_index[note]+1; + } else { + note = 0; + } + + // get instrument byte + unsigned char inst = cur_row[DIV_PAT_INS]; + if (inst == -1) inst = 0; + else inst++; + + // write the note and instrument bytes + w->writeText(fmt::sprintf(" .byte $%02x,$%02x\n", inst, note)); + } + } + } + + // write the OPTIMIZED frequency table + w->writeText("freqtablo\n"); + w->writeText(".byte "); + for (int i = 0; i < note_ind; i++) { + w->writeText(fmt::sprintf("<($%04x)%c",note_freq[i],i==(note_ind-1)?'\n':',')); + } + w->writeText("freqtabhi\n"); + w->writeText(".byte "); + for (int i = 0; i < note_ind; i++) { + w->writeText(fmt::sprintf(">($%04x)%c",note_freq[i],i==(note_ind-1)?'\n':',')); + } + w->writeText("\n"); + + output.push_back(DivROMExportOutput("music.asm",w)); + }); + + logAppend("writing data..."); + progress[0].amount=0.95f; + + progress[0].amount=1.0f; + + logAppend("finished!"); + + running=false; +} + +bool DivExportSaxotone::go(DivEngine* eng) { + progress[0].name="Progress"; + progress[0].amount=0.0f; + + e=eng; + running=true; + failed=false; + mustAbort=false; + exportThread=new std::thread(&DivExportSaxotone::run,this); + return true; +} + +void DivExportSaxotone::wait() { + if (exportThread!=NULL) { + logV("waiting for export thread..."); + exportThread->join(); + delete exportThread; + } +} + +void DivExportSaxotone::abort() { + mustAbort=true; + wait(); +} + +bool DivExportSaxotone::isRunning() { + return running; +} + +bool DivExportSaxotone::hasFailed() { + return failed; +} + +DivROMExportProgress DivExportSaxotone::getProgress(int index) { + if (index<0 || index>1) return progress[1]; + return progress[index]; +} diff --git a/src/engine/export/saxotone.h b/src/engine/export/saxotone.h new file mode 100644 index 000000000..68f5e9941 --- /dev/null +++ b/src/engine/export/saxotone.h @@ -0,0 +1,38 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2026 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 DivExportSaxotone: 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); + ~DivExportSaxotone() {} +}; diff --git a/src/engine/exportDef.cpp b/src/engine/exportDef.cpp index f66bc59b0..2a552906b 100644 --- a/src/engine/exportDef.cpp +++ b/src/engine/exportDef.cpp @@ -142,4 +142,15 @@ void DivEngine::registerROMExports() { }, false, DIV_REQPOL_ANY ); + + romExportDefs[DIV_ROM_SAXOTONE]=new DivROMExportDef( + "Saxotone", "Natt & AArt1256", + "Saxotone export\n" + "for use with the Saxotone Atari 2600 beeper engine", + "Text/Binary files", NULL, + { + DIV_SYSTEM_SAXOTONE, DIV_SYSTEM_TIA + }, + false, DIV_REQPOL_ANY + ); }