ZSM export: move to ROM export framework

This commit is contained in:
tildearrow 2024-08-18 17:20:17 -05:00
parent 875ef08256
commit 39923742ab
11 changed files with 382 additions and 441 deletions

View file

@ -669,6 +669,7 @@ class DivEngine {
friend class DivROMExport;
friend class DivExportAmigaValidation;
friend class DivExportTiuna;
friend class DivExportZSM;
public:
DivSong song;
@ -721,8 +722,6 @@ class DivEngine {
// - -1 to auto-determine trailing
// - -2 to add a whole loop of trailing
SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171, bool patternHints=false, bool directStream=false, int trailingTicks=-1);
// dump to ZSM.
SafeWriter* saveZSM(unsigned int zsmrate=60, bool loop=true, bool optimize=true);
// dump to TIunA.
SafeWriter* saveTiuna(const bool* sysToExport, const char* baseLabel, int firstBankSize, int otherBankSize);
// dump command stream.

View file

@ -21,6 +21,7 @@
#include "export/amigaValidation.h"
#include "export/tiuna.h"
#include "export/zsm.h"
DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) {
DivROMExport* exporter=NULL;
@ -31,6 +32,9 @@ DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) {
case DIV_ROM_TIUNA:
exporter=new DivExportTiuna;
break;
case DIV_ROM_ZSM:
exporter=new DivExportZSM;
break;
default:
exporter=new DivROMExport;
break;

View file

@ -18,9 +18,77 @@
*/
#include "zsm.h"
#include "../engine.h"
#include "../ta-log.h"
#include "../utfutils.h"
#include "song.h"
#include <fmt/printf.h>
/// DivZSM definitions
#define ZSM_HEADER_SIZE 16
#define ZSM_VERSION 1
#define ZSM_YM_CMD 0x40
#define ZSM_DELAY_CMD 0x80
#define ZSM_YM_MAX_WRITES 63
#define ZSM_SYNC_MAX_WRITES 31
#define ZSM_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD
#define ZSM_EXT ZSM_YM_CMD
#define ZSM_EXT_PCM 0x00
#define ZSM_EXT_CHIP 0x40
#define ZSM_EXT_SYNC 0x80
#define ZSM_EXT_CUSTOM 0xC0
enum YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
struct S_pcmInst {
int geometry;
unsigned int offset, length, loopPoint;
bool isLooped;
};
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
int pcmRateCache;
int pcmCtrlRVCache;
int pcmCtrlDCCache;
unsigned int pcmLoopPointCache;
bool pcmIsLooped;
std::vector<DivRegWrite> ymwrites;
std::vector<DivRegWrite> pcmMeta;
std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts;
std::vector<DivRegWrite> syncCache;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask;
int psgMask;
bool optimize;
public:
DivZSM();
~DivZSM();
void init(unsigned int rate = 60);
int getoffset();
void writeYM(unsigned char a, unsigned char v);
void writePSG(unsigned char a, unsigned char v);
void writePCM(unsigned char a, unsigned char v);
void writeSync(unsigned char a, unsigned char v);
void setOptimize(bool o);
void tick(int numticks = 1);
void setLoopPoint();
SafeWriter* finish();
private:
void flushWrites();
void flushTicks();
};
/// DivZSM implementation
DivZSM::DivZSM() {
w=NULL;
@ -447,3 +515,240 @@ void DivZSM::flushTicks() {
}
ticks=0;
}
/// ZSM export
constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0;
constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0;
void DivExportZSM::run() {
// settings
unsigned int zsmrate=conf.getInt("zsmrate",60);
bool loop=conf.getBool("loop",true);
bool optimize=conf.getBool("optimize",true);
// system IDs
int VERA=-1;
int YM=-1;
int IGNORED=0;
// find indexes for YM and VERA. Ignore other systems.
for (int i=0; i<e->song.systemLen; i++) {
switch (e->song.system[i]) {
case DIV_SYSTEM_VERA:
if (VERA>=0) {
IGNORED++;
break;
}
VERA=i;
logAppendf("VERA detected as chip id %d",i);
break;
case DIV_SYSTEM_YM2151:
if (YM>=0) {
IGNORED++;
break;
}
YM=i;
logAppendf("YM detected as chip id %d",i);
break;
default:
IGNORED++;
logAppendf("Ignoring chip %d systemID %d",i,(int)e->song.system[i]);
break;
}
}
if (VERA<0 && YM<0) {
logAppend("ERROR: No supported systems for ZSM");
failed=true;
running=false;
return;
}
if (IGNORED>0) {
logAppendf("ZSM export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' ');
}
DivZSM zsm;
e->stop();
e->repeatPattern=false;
e->setOrder(0);
e->synchronizedSoft([&]() {
double origRate=e->got.rate;
e->got.rate=zsmrate&0xffff;
// determine loop point
int loopOrder=0;
int loopRow=0;
int loopEnd=0;
e->walkSong(loopOrder,loopRow,loopEnd);
logAppendf("loop point: %d %d",loopOrder,loopRow);
zsm.init(zsmrate);
// 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);
//size_t tickCount=0;
bool done=false;
bool loopNow=false;
int loopPos=-1;
int fracWait=0; // accumulates fractional ticks
if (VERA>=0) e->disCont[VERA].dispatch->toggleRegisterDump(true);
if (YM>=0) {
e->disCont[YM].dispatch->toggleRegisterDump(true);
// emit LFO initialization commands
zsm.writeYM(0x18,0); // freq=0
zsm.writeYM(0x19,0x7F); // AMD =7F
zsm.writeYM(0x19,0xFF); // PMD =7F
// TODO: incorporate the Furnace meta-command for init data and filter
// out writes to otherwise-unused channels.
}
// Indicate the song's tuning as a sync meta-event
// specified in terms of how many 1/256th semitones
// the song is offset from standard A-440 tuning.
// This is mainly to benefit visualizations in players
// for non-standard tunings so that they can avoid
// displaying the entire song held in pitch bend.
// Tunings offsets that exceed a half semitone
// will simply be represented in a different key
// by nature of overflowing the signed char value
signed char tuningoffset=(signed char)(round(3072*(log(e->song.tuning/440.0)/log(2))))&0xff;
zsm.writeSync(0x01,tuningoffset);
// Set optimize flag, which mainly buffers PSG writes
// whenever the channel is silent
zsm.setOptimize(optimize);
while (!done) {
if (loopPos==-1) {
if (loopOrder==e->curOrder && loopRow==e->curRow && loop)
loopNow=true;
if (loopNow) {
// If Virtual Tempo is in use, our exact loop point
// might be skipped due to quantization error.
// If this happens, the tick immediately following is our loop point.
if (e->ticks==1 || !(loopOrder==e->curOrder && loopRow==e->curRow)) {
loopPos=zsm.getoffset();
zsm.setLoopPoint();
loopNow=false;
}
}
}
if (e->nextTick() || !e->playing) {
done=true;
if (!loop) {
for (int i=0; i<e->song.systemLen; i++) {
e->disCont[i].dispatch->getRegisterWrites().clear();
}
break;
}
if (!e->playing) {
loopPos=-1;
}
}
// get register dumps
for (int j=0; j<2; j++) {
int i=0;
// dump YM writes first
if (j==0) {
if (YM<0) {
continue;
} else {
i=YM;
}
}
// dump VERA writes second
if (j==1) {
if (VERA<0) {
continue;
} else {
i=VERA;
}
}
std::vector<DivRegWrite>& writes=e->disCont[i].dispatch->getRegisterWrites();
if (writes.size()>0)
logD("zsmOps: Writing %d messages to chip %d",writes.size(),i);
for (DivRegWrite& write: writes) {
if (i==YM) zsm.writeYM(write.addr&0xff,write.val);
if (i==VERA) {
if (done && write.addr>=64) continue; // don't process any PCM or sync events on the loop lookahead
zsm.writePSG(write.addr&0xff,write.val);
}
}
writes.clear();
}
// write wait
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) {
zsm.tick(totalWait);
//tickCount+=totalWait;
}
}
// end of song
// done - close out.
e->got.rate=origRate;
if (VERA>=0) e->disCont[VERA].dispatch->toggleRegisterDump(false);
if (YM>=0) e->disCont[YM].dispatch->toggleRegisterDump(false);
e->remainingLoops=-1;
e->playing=false;
e->freelance=false;
e->extValuePresent=false;
});
progress[0].amount=1.0f;
logAppend("finished!");
output.push_back(DivROMExportOutput("out.zsm",zsm.finish()));
running=false;
}
/// DivExpottZSM - FRONTEND
bool DivExportZSM::go(DivEngine* eng) {
progress[0].name="Generate";
progress[0].amount=0.0f;
e=eng;
running=true;
failed=false;
mustAbort=false;
exportThread=new std::thread(&DivExportZSM::run,this);
return true;
}
void DivExportZSM::wait() {
if (exportThread!=NULL) {
exportThread->join();
delete exportThread;
}
}
void DivExportZSM::abort() {
mustAbort=true;
wait();
}
bool DivExportZSM::isRunning() {
return running;
}
bool DivExportZSM::hasFailed() {
return failed;
}
DivROMExportProgress DivExportZSM::getProgress(int index) {
if (index<0 || index>1) return progress[1];
return progress[index];
}

38
src/engine/export/zsm.h Normal file
View file

@ -0,0 +1,38 @@
/**
* 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 "../export.h"
#include <thread>
class DivExportZSM: 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);
~DivExportZSM() {}
};

View file

@ -1,92 +0,0 @@
/**
* 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.
*/
#ifndef _ZSM_H
#define _ZSM_H
//#include "engine.h"
#include "safeWriter.h"
#include "dispatch.h"
#include <stdlib.h>
#define ZSM_HEADER_SIZE 16
#define ZSM_VERSION 1
#define ZSM_YM_CMD 0x40
#define ZSM_DELAY_CMD 0x80
#define ZSM_YM_MAX_WRITES 63
#define ZSM_SYNC_MAX_WRITES 31
#define ZSM_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD
#define ZSM_EXT ZSM_YM_CMD
#define ZSM_EXT_PCM 0x00
#define ZSM_EXT_CHIP 0x40
#define ZSM_EXT_SYNC 0x80
#define ZSM_EXT_CUSTOM 0xC0
enum YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
struct S_pcmInst {
int geometry;
unsigned int offset, length, loopPoint;
bool isLooped;
};
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
int pcmRateCache;
int pcmCtrlRVCache;
int pcmCtrlDCCache;
unsigned int pcmLoopPointCache;
bool pcmIsLooped;
std::vector<DivRegWrite> ymwrites;
std::vector<DivRegWrite> pcmMeta;
std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts;
std::vector<DivRegWrite> syncCache;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask;
int psgMask;
bool optimize;
public:
DivZSM();
~DivZSM();
void init(unsigned int rate = 60);
int getoffset();
void writeYM(unsigned char a, unsigned char v);
void writePSG(unsigned char a, unsigned char v);
void writePCM(unsigned char a, unsigned char v);
void writeSync(unsigned char a, unsigned char v);
void setOptimize(bool o);
void tick(int numticks = 1);
void setLoopPoint();
SafeWriter* finish();
private:
void flushWrites();
void flushTicks();
};
#endif

View file

@ -1,208 +0,0 @@
/**
* 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"
#include "../utfutils.h"
#include "song.h"
#include "zsm.h"
constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0;
constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0;
SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop, bool optimize) {
int VERA=-1;
int YM=-1;
int IGNORED=0;
// find indexes for YM and VERA. Ignore other systems.
for (int i=0; i<song.systemLen; i++) {
switch (song.system[i]) {
case DIV_SYSTEM_VERA:
if (VERA>=0) {
IGNORED++;
break;
}
VERA=i;
logD("VERA detected as chip id %d",i);
break;
case DIV_SYSTEM_YM2151:
if (YM>=0) {
IGNORED++;
break;
}
YM=i;
logD("YM detected as chip id %d",i);
break;
default:
IGNORED++;
logD("Ignoring chip %d systemID %d",i,(int)song.system[i]);
break;
}
}
if (VERA<0 && YM<0) {
logE("No supported systems for ZSM");
return NULL;
}
if (IGNORED>0) {
logW("ZSM export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' ');
}
stop();
repeatPattern=false;
setOrder(0);
BUSY_BEGIN_SOFT;
double origRate=got.rate;
got.rate=zsmrate&0xffff;
// determine loop point
int loopOrder=0;
int loopRow=0;
int loopEnd=0;
walkSong(loopOrder,loopRow,loopEnd);
logI("loop point: %d %d",loopOrder,loopRow);
warnings="";
DivZSM zsm;
zsm.init(zsmrate);
// reset the playback state
curOrder=0;
freelance=false;
playing=false;
extValuePresent=false;
remainingLoops=-1;
// Prepare to write song data
playSub(false);
//size_t tickCount=0;
bool done=false;
bool loopNow=false;
int loopPos=-1;
int fracWait=0; // accumulates fractional ticks
if (VERA>=0) disCont[VERA].dispatch->toggleRegisterDump(true);
if (YM>=0) {
disCont[YM].dispatch->toggleRegisterDump(true);
// emit LFO initialization commands
zsm.writeYM(0x18,0); // freq=0
zsm.writeYM(0x19,0x7F); // AMD =7F
zsm.writeYM(0x19,0xFF); // PMD =7F
// TODO: incorporate the Furnace meta-command for init data and filter
// out writes to otherwise-unused channels.
}
// Indicate the song's tuning as a sync meta-event
// specified in terms of how many 1/256th semitones
// the song is offset from standard A-440 tuning.
// This is mainly to benefit visualizations in players
// for non-standard tunings so that they can avoid
// displaying the entire song held in pitch bend.
// Tunings offsets that exceed a half semitone
// will simply be represented in a different key
// by nature of overflowing the signed char value
signed char tuningoffset=(signed char)(round(3072*(log(song.tuning/440.0)/log(2))))&0xff;
zsm.writeSync(0x01,tuningoffset);
// Set optimize flag, which mainly buffers PSG writes
// whenever the channel is silent
zsm.setOptimize(optimize);
while (!done) {
if (loopPos==-1) {
if (loopOrder==curOrder && loopRow==curRow && loop)
loopNow=true;
if (loopNow) {
// If Virtual Tempo is in use, our exact loop point
// might be skipped due to quantization error.
// If this happens, the tick immediately following is our loop point.
if (ticks==1 || !(loopOrder==curOrder && loopRow==curRow)) {
loopPos=zsm.getoffset();
zsm.setLoopPoint();
loopNow=false;
}
}
}
if (nextTick() || !playing) {
done=true;
if (!loop) {
for (int i=0; i<song.systemLen; i++) {
disCont[i].dispatch->getRegisterWrites().clear();
}
break;
}
if (!playing) {
loopPos=-1;
}
}
// get register dumps
for (int j=0; j<2; j++) {
int i=0;
// dump YM writes first
if (j==0) {
if (YM<0) {
continue;
} else {
i=YM;
}
}
// dump VERA writes second
if (j==1) {
if (VERA<0) {
continue;
} else {
i=VERA;
}
}
std::vector<DivRegWrite>& writes=disCont[i].dispatch->getRegisterWrites();
if (writes.size()>0)
logD("zsmOps: Writing %d messages to chip %d",writes.size(),i);
for (DivRegWrite& write: writes) {
if (i==YM) zsm.writeYM(write.addr&0xff,write.val);
if (i==VERA) {
if (done && write.addr>=64) continue; // don't process any PCM or sync events on the loop lookahead
zsm.writePSG(write.addr&0xff,write.val);
}
}
writes.clear();
}
// write wait
int totalWait=cycles>>MASTER_CLOCK_PREC;
fracWait+=cycles&MASTER_CLOCK_MASK;
totalWait+=fracWait>>MASTER_CLOCK_PREC;
fracWait&=MASTER_CLOCK_MASK;
if (totalWait>0 && !done) {
zsm.tick(totalWait);
//tickCount+=totalWait;
}
}
// end of song
// done - close out.
got.rate=origRate;
if (VERA>=0) disCont[VERA].dispatch->toggleRegisterDump(false);
if (YM>=0) disCont[YM].dispatch->toggleRegisterDump(false);
remainingLoops=-1;
playing=false;
freelance=false;
extValuePresent=false;
BUSY_END;
return zsm.finish();
}