Merge pull request #491 from ZeroByteOrg/ZSMv1

Commander X16 Native Export Format: ZSM
This commit is contained in:
tildearrow 2022-09-24 02:28:25 -05:00 committed by GitHub
commit 98cebf92f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 696 additions and 13 deletions

View File

@ -460,6 +460,8 @@ src/engine/sysDef.cpp
src/engine/wavetable.cpp
src/engine/waveSynth.cpp
src/engine/vgmOps.cpp
src/engine/zsmOps.cpp
src/engine/zsm.cpp
src/engine/platform/abstract.cpp
src/engine/platform/genesis.cpp
src/engine/platform/genesisext.cpp

142
papers/zsm-format.md Normal file
View File

@ -0,0 +1,142 @@
# ZSM format specification
#### Zsound Repo
ZSM is part of the Zsound suite of Commander X16 audio tools found at:</br>
https://github.com/ZeroByteOrg/zsound/
#### Current ZSM Revision: 1
ZSM is a standard specifying both a data stream format and a file structure for containing the data stream. This document provides the standard for both the ZSM stream format and for the ZSM container file format.
Whenever it becomes necessary to modify the ZSM standard in such a way that existing software will not be compatible with files using the newer standard, this version number will be incremented, up to a maximum value of 254.
Version 255 (-1) is reserved for internal use by the player
#### Headerless Data File Format:
Since Kernal version r39, it is possible to load data files that do not have the CBM 2-byte load-to-address header. As of version r41, this functionality is equally accessible in the standard interactive BASIC interface. As the "PRG" header is no longer necessary, ZSM files will NOT contain this header in order to appear as any other common data file such as ``.wav``, ``.png``, etc. As such, users and programs must use the "headerless mode" when loading a ZSM into memory on the Commander X16. The previously-suggested dummy PRG header has been incorporated to the ZSM header as a magic header for file identity verification purposes.
## ZSM file composition
Offset|Length|Field
--|--|--
0x00|16|ZSM HEADER
0x10|variable|ZSM STREAM
?|?|(optional) PCM HEADER
?|variable|(optional) PCM DATA
### ZSM Header
The ZSM header is 16 bytes long.
- All multi-byte values are little endian unless specified otherwise
- All offsets are relative to the beginning of the ZSM header
Offset|Length|Field|Description
---|---|---|---
0x00|2|Magic Header| The string 'zm' (binary 0x7a 0x6d)
0x02|1|Version| ZSM Version. 0-0xFE (0xFF is reserved)
0x03|3|Loop Point|Offset to the starting point of song loop. 0 = no loop.
0x06|3|PCM offset|Offset to the beginning of the PCM index table (if present). 0 = no PCM header or data is present.
0x09|1|FM channel mask|Bit 0-7 are set if the corresponding OPM channel is used by the music.
0x0a|2|PSG channel mask|Bits 0-15 are set if the corresponding PSG channel is used by the music.
0x0c|2|Tick Rate|The rate (in Hz) for song delay ticks. *60Hz (one tick per frame) is recommended.*
0x0e|2|reserved| Reserved for future use. Set to zero.
### ZSM Music Data Stream Format
Byte 0|Byte 1 (variable)|Byte n|Byte n+1 (variable)|...|End of stream
---|---|---|---|---|---
CMD|DATA|CMD|DATA|...|0x80
#### CMD (command) byte values
CMD bytes are bit-packed to hold a command Type ID and a value (n) as follows:
CMD|Bit Pattern|Type|Arg. Bytes|Action
---|--|--|--|-----
0x00-0x3F|`00nnnnnn`|PSG write|1 | Write the following byte into PSG register offset *n*. (from 0x1F9C0 in VRAM)
0x40 |`01000000`|EXTCMD |1+?| The following byte is an extension command. (see below for EXTCMD syntax)
0x41-0x7F|`01nnnnnn`|FM write |2*n* | Write the following *n* reg/val pairs into the YM2151.
0x80|`10000000`|EOF |0 |This byte MUST be present at the end of the data stream. Player may loop or halt as necessary.
0x81-0xFF|`1nnnnnnn`|Delay |0 |Delay *n* ticks.
#### EXTCMD:
The EXTCMD byte is formatted as `ccnnnnnn` where `c`=channel and `n`=number of bytes that follow. If the player wishes to ignore a channel, it can simply advance `n` bytes and continue processing. See EXTCMD Channel Specifications below for more details.
### PCM Header
The size and contents of the PCM header table is not yet decided. This will depend largely on the strucure of EXTCMD channel 0, and be covered in detail in that specification.
Any offset values contained in the PCM data header block will be relative to the beginning of the PCM header, not the ZSM header. The intention is to present the digital audio portion as a set of digi clips ("samples" in tracker terminology) whose playback can be triggered by EXTCMD channel zero.
### PCM Sample Data
This will be a blob of PCM data with no internal formatting. Indeces / format information / loop points / etc regarding this blob will be provided via the PCM header. The end of this blob will be the end of the ZSM file.
## EXTCMD Channel Scifications
Extension commands provide optional functionality within a ZSM music file. EXTCMD may be ignored by any player. EXTCMD defines 4 "channels" of message streams. Players may implement support for any, all, or none of the channels as desired. An EXTCMD may specify up to 63 bytes of data. If more data than this is required, then it must be broken up into multiple EXTCMDs.
##### EXTCMD in ZSM stream context:
...|CMD 0x40|EXTCMD|N bytes|CMD|...
---|---|---|---|---|---
##### EXTCMD byte format:
Bit Pattern|C|N
--|--|--
`ccnnnnnn`|Extension Channel ID|Number of bytes that follow
##### EXTCMD Channels:
0. PCM instrument channel
1. Expansion Sound Devices
2. Synchronization events
3. Custom
The formatting of the data within these 4 channels is presently a work in progress. Definitions for channels 0-3 will be part of the official ZSM specifications and implemented in the Zsound library. Significant changes within one of these three channels' structure may result in a new ZSM version number being issued. The formatting and content of the 3 official EXTCMD channels will be covered here.
The Custom channel data may take whatever format is desired for any particular purpose with the understanding that the general ecosystem of ZSM-aware applications will most likely ignore them.
### EXTCMD Channel:
#### 0: PCM audio
The structure of data within this channel is not yet defined.
#### 1: Expansion Sound Devices
This channel is for data intended for "well-known" expansion hardware used with the Commander X16. As the community adopts various expansion hardware, such devices will be given a standard "ID" number so that all ZSM files will agree on which device is being referenced by expansion HW data.
The specification of new chip IDs should not affect the format of ZSM itself, and thus will not result in a ZSM version update. Players will simply need to update their list of known hardware.
Players implementing this channel should implement detection routines during init to determine which (if any) expansion hardware is present. Any messages intended for a chip that is not present in the system should be skipped.
An expansion HW write will contain the following data:
Chip ID|Nuber of writes (`N`)| `N` tuples of data
--|--|--
one byte|one byte|N * tuple_size bytes
- The total number of bytes MUST equal exactly the number of bytes specified in the preceding EXTCMD.
- The tuple_size is determined by the needs of the device, and thus will be specified per-device along with its chip ID assignment. This is likely to be 1-3 bytes for most devices.
There are currently no supported expansion HW IDs assigned.
#### 2: Synchronization Events
The purpose of this channel is to provide for music synchronization cues that applications may use to perform operations in sync with the music (such as when the Goombas jump in New Super Mario Bros in time with the BOP! BOP! notes in the music). It is intended for the reference player to provide a sync channel callback, passing the data bytes to the callback function, and then to proceed with playback.
The data structure within this channel is not yet defined. It is our intention to work with the community in order to collaborate on a useful structure.
#### 3: Custom
The purpose for this channel is that any project with an idea that does not fit neatly into the above categories may pack data into the project's music files in whatever form is required. It should be understood that these ZSMs will not be expected to use the extended behaviors outside of the project they were designed for. The music itself, however, should play properly. The only constraint is that the data must conform to the EXTCMD byte - supplying exactly the specified number of bytes per EXTCMD.
The reference playback library in Zsound will implement this channel as a simple callback passing the memory location and data size to the referenced function, and take no further action internally.

View File

@ -64,7 +64,7 @@ enum DivStatusView {
enum DivAudioEngines {
DIV_AUDIO_JACK=0,
DIV_AUDIO_SDL=1,
DIV_AUDIO_NULL=126,
DIV_AUDIO_DUMMY=127
};
@ -505,6 +505,8 @@ class DivEngine {
SafeWriter* buildROM(int sys);
// dump to VGM.
SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171, bool patternHints=false);
// dump to ZSM.
SafeWriter* saveZSM(unsigned int zsmrate=60, bool loop=true);
// dump command stream.
SafeWriter* saveCommand(bool binary=false);
// export to an audio file
@ -630,10 +632,10 @@ class DivEngine {
// get japanese system name
const char* getSystemNameJ(DivSystem sys);
// get sys definition
const DivSysDef* getSystemDef(DivSystem sys);
// convert sample rate format
int fileToDivRate(int frate);
int divToFileRate(int drate);
@ -710,7 +712,7 @@ class DivEngine {
// is playing
bool isPlaying();
// is running
bool isRunning();
@ -797,7 +799,7 @@ class DivEngine {
void autoNoteOn(int chan, int ins, int note, int vol=-1);
void autoNoteOff(int chan, int note, int vol=-1);
void autoNoteOffAll();
// set whether autoNoteIn is mono or poly
void setAutoNotePoly(bool poly);
@ -818,7 +820,7 @@ class DivEngine {
// get dispatch channel state
void* getDispatchChanState(int chan);
// get register pool
unsigned char* getRegisterPool(int sys, int& size, int& depth);
@ -854,7 +856,7 @@ class DivEngine {
// set the console mode.
void setConsoleMode(bool enable);
// get metronome
bool getMetronome();
@ -918,7 +920,7 @@ class DivEngine {
// move system
bool swapSystem(int src, int dest, bool preserveOrder=true);
// write to register on system
void poke(int sys, unsigned int addr, unsigned short val);
@ -930,7 +932,7 @@ class DivEngine {
// get warnings
String getWarnings();
// switch master
bool switchMaster();

View File

@ -27,7 +27,9 @@ extern "C" {
#include "sound/vera_pcm.h"
}
#define rWrite(c,a,d) {regPool[(c)*4+(a)]=(d); psg_writereg(psg,((c)*4+(a)),(d));}
//if (dumpWrites) {addWrite(((c)*4+(a)),(d));}
//#define rWrite(c,a,d) {regPool[(c)*4+(a)]=(d); psg_writereg(psg,((c)*4+(a)),(d));}
#define rWrite(c,a,d) {regPool[(c)*4+(a)]=(d); psg_writereg(psg,((c)*4+(a)),(d));if (dumpWrites) {addWrite(((c)*4+(a)),(d));}}
#define rWriteLo(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0x3f))|((d)&0x3f))
#define rWriteHi(c,a,d) rWrite(c,a,(regPool[(c)*4+(a)]&(~0xc0))|(((d)<<6)&0xc0))
#define rWritePCMCtrl(d) {regPool[64]=(d); pcm_write_ctrl(pcm,d);}

View File

@ -204,4 +204,4 @@ void SafeWriter::finish() {
delete[] buf;
buf=NULL;
operative=false;
}
}

218
src/engine/zsm.cpp Normal file
View File

@ -0,0 +1,218 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 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 "zsm.h"
#include "../ta-log.h"
#include "../utfutils.h"
#include "song.h"
DivZSM::DivZSM() {
w = NULL;
init();
}
DivZSM::~DivZSM() {
}
void DivZSM::init(unsigned int rate) {
if (w != NULL) delete w;
w = new SafeWriter;
w->init();
// write default ZSM data header
w->write("zm",2); // magic header
w->writeC(ZSM_VERSION);
// no loop offset
w->writeS(0);
w->writeC(0);
// no PCM
w->writeS(0x00);
w->writeC(0x00);
// FM channel mask
w->writeC(0x00);
// PSG channel mask
w->writeS(0x00);
w->writeS((unsigned short)rate);
// 2 reserved bytes (set to zero)
w->writeS(0x00);
tickRate = rate;
loopOffset=-1;
numWrites=0;
memset(&ymState,-1,sizeof(ymState));
memset(&psgState,-1,sizeof(psgState));
ticks=0;
}
int DivZSM::getoffset() {
return w->tell();
}
void DivZSM::writeYM(unsigned char a, unsigned char v) {
int lastMask = ymMask;
if (a==0x19 && v>=0x80) a=0x1a; // AMD/PSD use same reg addr. store PMD as 0x1a
if (a==0x08 && (v&0xf8)) ymMask |= (1 << (v & 0x07)); // mark chan as in-use if keyDN
if (a!=0x08) ymState[ym_NEW][a] = v; // cache the newly-written value
bool writeit=false; // used to suppress spurious writes to unused channels
if (a < 0x20) {
if (a == 0x08) {
// write keyUPDN messages if channel is active.
writeit = (ymMask & (1 << (v & 0x07))) > 0;
}
else {
// do not suppress global registers
writeit = true;
}
} else {
writeit = (ymMask & (1 << (a & 0x07))) > 0; // a&0x07 = chan ID for regs >=0x20
}
if (lastMask != ymMask) {
// if the ymMask just changed, then the channel has become active.
// This can only happen on a KeyDN event, so voice = v & 0x07
// insert a keyUP just to be safe.
ymwrites.push_back(DivRegWrite(0x08,v&0x07));
numWrites++;
// flush the ym_NEW cached states for this channel into the ZSM....
for ( int i=0x20 + (v&0x07); i <= 0xff ; i+=8) {
if (ymState[ym_NEW][i] != ymState[ym_PREV][i]) {
ymwrites.push_back(DivRegWrite(i,ymState[ym_NEW][i]));
numWrites++;
// ...and update the shadow
ymState[ym_PREV][i] = ymState[ym_NEW][i];
}
}
}
// Handle the current write if channel is active
if (writeit && ((ymState[ym_NEW][a] != ymState[ym_PREV][a])||a==0x08) ) {
// update YM shadow if not the KeyUPDN register.
if (a!=0x008) ymState[ym_PREV][a] = ymState[ym_NEW][a];
// if reg = PMD, then change back to real register 0x19
if (a==0x1a) a=0x19;
ymwrites.push_back(DivRegWrite(a,v));
numWrites++;
}
}
void DivZSM::writePSG(unsigned char a, unsigned char v) {
// TODO: suppress writes to PSG voice that is not audible (volume=0)
if (a >= 64) {
logD ("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v);
return;
}
if(psgState[psg_PREV][a] == v) {
if (psgState[psg_NEW][a] != v)
// NEW value is being reset to the same as PREV value
// so it is no longer a new write.
numWrites--;
} else {
if (psgState[psg_PREV][a] == psgState[psg_NEW][a])
// if this write changes the NEW cached value to something other
// than the PREV value, then this is a new write.
numWrites++;
}
psgState[psg_NEW][a] = v;
// mark channel as used in the psgMask if volume is set > 0.
if ((a % 4 == 2) && (v & 0x3f)) psgMask |= (1 << (a>>2));
}
void DivZSM::writePCM(unsigned char a, unsigned char v) {
// ZSM standard for PCM playback has not been established yet.
}
void DivZSM::tick(int numticks) {
flushWrites();
ticks += numticks;
}
void DivZSM::setLoopPoint() {
tick(0); // flush any ticks+writes
flushTicks(); // flush ticks incase no writes were pending
logI("ZSM: loop at file offset %d bytes",w->tell());
loopOffset=w->tell();
//update the ZSM header's loop offset value
w->seek(0x03,SEEK_SET);
w->writeS((short)(loopOffset&0xffff));
w->writeC((unsigned char)((loopOffset>>16)&0xff));
w->seek(loopOffset,SEEK_SET);
// reset the PSG shadow and write cache
memset(&psgState,-1,sizeof(psgState));
// reset the YM shadow....
memset(&ymState[ym_PREV],-1,sizeof(ymState[ym_PREV]));
// ... and cache (except for unused channels)
memset(&ymState[ym_NEW],-1,0x20);
for (int chan=0; chan<8 ; chan++) {
//do not clear state for as-yet-unused channels
if (!(ymMask & (1<<chan))) continue;
// clear the state for channels in use so they match the unknown state
// of the YM shadow.
for (int i=0x20+chan; i<=0xff; i+= 8) ymState[ym_NEW][i] = -1;
}
}
SafeWriter* DivZSM::finish() {
tick(0); // flush any pending writes / ticks
flushTicks(); // flush ticks in case there were no writes pending
w->writeC(ZSM_EOF);
// update channel use masks.
w->seek(0x09,SEEK_SET);
w->writeC((unsigned char)(ymMask & 0xff));
w->writeS((short)(psgMask & 0xffff));
// todo: put PCM offset/data writes here once defined in ZSM standard.
return w;
}
void DivZSM::flushWrites() {
logD("ZSM: flushWrites.... numwrites=%d ticks=%d ymwrites=%d",numWrites,ticks,ymwrites.size());
if (numWrites==0) return;
flushTicks(); // only flush ticks if there are writes pending.
for (unsigned char i=0;i<64;i++) {
if (psgState[psg_NEW][i] == psgState[psg_PREV][i]) continue;
psgState[psg_PREV][i]=psgState[psg_NEW][i];
w->writeC(i);
w->writeC(psgState[psg_NEW][i]);
}
int n=0; // n = completed YM writes. used to determine when to write the CMD byte...
for (DivRegWrite& write: ymwrites) {
if (n%ZSM_YM_MAX_WRITES == 0) {
if(ymwrites.size()-n > ZSM_YM_MAX_WRITES) {
w->writeC((unsigned char)(ZSM_YM_CMD+ZSM_YM_MAX_WRITES));
logD("ZSM: YM-write: %d (%02x) [max]",ZSM_YM_MAX_WRITES,ZSM_YM_MAX_WRITES+ZSM_YM_CMD);
} else {
w->writeC((unsigned char)(ZSM_YM_CMD+ymwrites.size()-n));
logD("ZSM: YM-write: %d (%02x)",ymwrites.size()-n,ZSM_YM_CMD+ymwrites.size()-n);
}
}
n++;
w->writeC(write.addr);
w->writeC(write.val);
}
ymwrites.clear();
numWrites=0;
}
void DivZSM::flushTicks() {
while (ticks > ZSM_DELAY_MAX) {
logD("ZSM: write delay %d (max)",ZSM_DELAY_MAX);
w->writeC((unsigned char)(ZSM_DELAY_CMD+ZSM_DELAY_MAX));
ticks -= ZSM_DELAY_MAX;
}
if (ticks>0) {
logD("ZSM: write delay %d",ticks);
w->writeC(ZSM_DELAY_CMD+ticks);
}
ticks=0;
}

67
src/engine/zsm.h Normal file
View File

@ -0,0 +1,67 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 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_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD
enum YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
std::vector<DivRegWrite> ymwrites;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask = 0;
int psgMask = 0;
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 tick(int numticks = 1);
void setLoopPoint();
SafeWriter* finish();
private:
void flushWrites();
void flushTicks();
};
#endif

175
src/engine/zsmOps.cpp Normal file
View File

@ -0,0 +1,175 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2022 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) {
int VERA = -1;
int YM = -1;
int IGNORED = 0;
//loop = false;
// 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,song.system[i]);
}
}
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;
int loopPos=-1;
int writeCount=0;
int fracWait=0; // accumulates fractional ticks
if (VERA >= 0) disCont[VERA].dispatch->toggleRegisterDump(true);
if (YM >= 0) {
disCont[YM].dispatch->toggleRegisterDump(true);
zsm.writeYM(0x18,0); // initialize the LFO freq to 0
// note - I think there's a bug where Furnace writes AMD/PMD=max
// that shouldn't be there, requiring this initialization that shouldn't
// be there for ZSM.
}
while (!done) {
if (loopPos==-1) {
if (loopOrder==curOrder && loopRow==curRow && ticks==1 && loop) {
loopPos=zsm.getoffset();
zsm.setLoopPoint();
}
}
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) zsm.writePSG(write.addr&0xff, write.val);
writeCount++;
}
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) {
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();
}

View File

@ -1624,6 +1624,16 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) {
dpiScale
);
break;
case GUI_FILE_EXPORT_ZSM:
if (!dirExists(workingDirZSMExport)) workingDirZSMExport=getHomeDir();
hasOpened=fileDialog->openSave(
"Export ZSM",
{"ZSM file", "*.zsm"},
"ZSM file{.zsm}",
workingDirZSMExport,
dpiScale
);
break;
case GUI_FILE_EXPORT_CMDSTREAM:
if (!dirExists(workingDirROMExport)) workingDirROMExport=getHomeDir();
hasOpened=fileDialog->openSave(
@ -3377,6 +3387,26 @@ bool FurnaceGUI::loop() {
}
ImGui::EndMenu();
}
int numZSMCompat=0;
for (int i=0; i<e->song.systemLen; i++) {
if ((e->song.system[i] == DIV_SYSTEM_VERA) || (e->song.system[i] == DIV_SYSTEM_YM2151)) numZSMCompat++;
}
if (numZSMCompat > 0) {
if (ImGui::BeginMenu("export ZSM...")) {
ImGui::Text("Commander X16 Zsound Music File");
if (ImGui::InputInt("Tick Rate (Hz)",&zsmExportTickRate,1,2)) {
if (zsmExportTickRate<1) zsmExportTickRate=1;
if (zsmExportTickRate>44100) zsmExportTickRate=44100;
}
ImGui::Checkbox("loop",&zsmExportLoop);
ImGui::SameLine();
if (ImGui::Button("Begin Export")) {
openFileDialog(GUI_FILE_EXPORT_ZSM);
ImGui::CloseCurrentPopup();
}
ImGui::EndMenu();
}
}
if (ImGui::BeginMenu("export command stream...")) {
ImGui::Text(
"this option exports a text or binary file which\n"
@ -3777,6 +3807,9 @@ bool FurnaceGUI::loop() {
case GUI_FILE_EXPORT_VGM:
workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
break;
case GUI_FILE_EXPORT_ZSM:
workingDirZSMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
break;
case GUI_FILE_EXPORT_ROM:
case GUI_FILE_EXPORT_CMDSTREAM:
workingDirROMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
@ -3855,6 +3888,9 @@ bool FurnaceGUI::loop() {
if (curFileDialog==GUI_FILE_EXPORT_VGM) {
checkExtension(".vgm");
}
if (curFileDialog==GUI_FILE_EXPORT_ZSM) {
checkExtension(".zsm");
}
if (curFileDialog==GUI_FILE_EXPORT_CMDSTREAM) {
// we can't tell whether the user chose .txt or .bin in the system file picker
const char* fallbackExt=(settings.sysFileDialog || ImGuiFileDialog::Instance()->GetCurrentFilter()=="text file")?".txt":".bin";
@ -4106,6 +4142,26 @@ bool FurnaceGUI::loop() {
}
break;
}
case GUI_FILE_EXPORT_ZSM: {
SafeWriter* w=e->saveZSM(zsmExportTickRate,zsmExportLoop);
if (w!=NULL) {
FILE* f=ps_fopen(copyOfName.c_str(),"wb");
if (f!=NULL) {
fwrite(w->getFinalBuf(),1,w->size(),f);
fclose(f);
} else {
showError("could not open file!");
}
w->finish();
delete w;
if (!e->getWarnings().empty()) {
showWarning(e->getWarnings(),GUI_WARN_GENERIC);
}
} else {
showError(fmt::sprintf("Could not write ZSM! (%s)",e->getLastError()));
}
break;
}
case GUI_FILE_EXPORT_ROM:
showError("Coming soon!");
break;
@ -4797,6 +4853,7 @@ bool FurnaceGUI::init() {
workingDirSample=e->getConfString("lastDirSample",workingDir);
workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir);
workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir);
workingDirZSMExport=e->getConfString("lastDirZSMExport",workingDir);
workingDirROMExport=e->getConfString("lastDirROMExport",workingDir);
workingDirFont=e->getConfString("lastDirFont",workingDir);
workingDirColors=e->getConfString("lastDirColors",workingDir);
@ -5075,6 +5132,7 @@ bool FurnaceGUI::finish() {
e->setConf("lastDirSample",workingDirSample);
e->setConf("lastDirAudioExport",workingDirAudioExport);
e->setConf("lastDirVGMExport",workingDirVGMExport);
e->setConf("lastDirZSMExport",workingDirZSMExport);
e->setConf("lastDirROMExport",workingDirROMExport);
e->setConf("lastDirFont",workingDirFont);
e->setConf("lastDirColors",workingDirColors);
@ -5200,6 +5258,7 @@ FurnaceGUI::FurnaceGUI():
displayError(false),
displayExporting(false),
vgmExportLoop(true),
zsmExportLoop(true),
vgmExportPatternHints(false),
portrait(false),
mobileMenuOpen(false),
@ -5216,6 +5275,7 @@ FurnaceGUI::FurnaceGUI():
displayPendingRawSample(false),
vgmExportVersion(0x171),
drawHalt(10),
zsmExportTickRate(60),
macroPointSize(16),
waveEditStyle(0),
mobileMenuPos(0.0f),

View File

@ -80,6 +80,7 @@ enum FurnaceGUIColors {
GUI_COLOR_FILE_AUDIO,
GUI_COLOR_FILE_WAVE,
GUI_COLOR_FILE_VGM,
GUI_COLOR_FILE_ZSM,
GUI_COLOR_FILE_FONT,
GUI_COLOR_FILE_OTHER,
@ -311,6 +312,7 @@ enum FurnaceGUIFileDialogs {
GUI_FILE_EXPORT_AUDIO_PER_SYS,
GUI_FILE_EXPORT_AUDIO_PER_CHANNEL,
GUI_FILE_EXPORT_VGM,
GUI_FILE_EXPORT_ZSM,
GUI_FILE_EXPORT_CMDSTREAM,
GUI_FILE_EXPORT_ROM,
GUI_FILE_LOAD_MAIN_FONT,
@ -1011,7 +1013,7 @@ class FurnaceGUI {
String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile, sysSearchQuery, newSongQuery;
String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport;
String workingDirVGMExport, workingDirROMExport, workingDirFont, workingDirColors, workingDirKeybinds;
String workingDirVGMExport, workingDirZSMExport, workingDirROMExport, workingDirFont, workingDirColors, workingDirKeybinds;
String workingDirLayout, workingDirROM, workingDirTest;
String mmlString[32];
String mmlStringW;
@ -1020,7 +1022,7 @@ class FurnaceGUI {
std::vector<FurnaceGUISysDef> newSongSearchResults;
std::deque<String> recentFile;
bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, vgmExportPatternHints;
bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, zsmExportLoop, vgmExportPatternHints;
bool portrait, mobileMenuOpen;
bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu;
bool displayNew, fullScreen, preserveChanPos, wantScrollList, noteInputPoly;
@ -1028,6 +1030,7 @@ class FurnaceGUI {
bool willExport[32];
int vgmExportVersion;
int drawHalt;
int zsmExportTickRate;
int macroPointSize;
int waveEditStyle;
float mobileMenuPos, autoButtonSize;

View File

@ -709,6 +709,7 @@ const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={
D(GUI_COLOR_FILE_AUDIO,"",ImVec4(1.0f,1.0f,0.5f,1.0f)),
D(GUI_COLOR_FILE_WAVE,"",ImVec4(1.0f,0.75f,0.5f,1.0f)),
D(GUI_COLOR_FILE_VGM,"",ImVec4(1.0f,1.0f,0.5f,1.0f)),
D(GUI_COLOR_FILE_ZSM,"",ImVec4(1.0f,1.0f,0.5f,1.0f)),
D(GUI_COLOR_FILE_FONT,"",ImVec4(0.3f,1.0f,0.6f,1.0f)),
D(GUI_COLOR_FILE_OTHER,"",ImVec4(0.7f,0.7f,0.7f,1.0f)),

View File

@ -1569,6 +1569,7 @@ void FurnaceGUI::drawSettings() {
UI_COLOR_CONFIG(GUI_COLOR_FILE_AUDIO,"Audio");
UI_COLOR_CONFIG(GUI_COLOR_FILE_WAVE,"Wavetable");
UI_COLOR_CONFIG(GUI_COLOR_FILE_VGM,"VGM");
UI_COLOR_CONFIG(GUI_COLOR_FILE_ZSM,"ZSM");
UI_COLOR_CONFIG(GUI_COLOR_FILE_FONT,"Font");
UI_COLOR_CONFIG(GUI_COLOR_FILE_OTHER,"Other");
ImGui::TreePop();
@ -3296,6 +3297,7 @@ void FurnaceGUI::applyUISettings(bool updateFonts) {
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".wav",uiColors[GUI_COLOR_FILE_AUDIO],ICON_FA_FILE_AUDIO_O);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".dmc",uiColors[GUI_COLOR_FILE_AUDIO],ICON_FA_FILE_AUDIO_O);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".vgm",uiColors[GUI_COLOR_FILE_VGM],ICON_FA_FILE_AUDIO_O);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".zsm",uiColors[GUI_COLOR_FILE_ZSM],ICON_FA_FILE_AUDIO_O);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttf",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".otf",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT);
ImGuiFileDialog::Instance()->SetFileStyle(IGFD_FileStyleByExtension,".ttc",uiColors[GUI_COLOR_FILE_FONT],ICON_FA_FONT);

View File

@ -52,6 +52,7 @@ FurnaceCLI cli;
String outName;
String vgmOutName;
String zsmOutName;
String cmdOutName;
int loops=1;
int benchMode=0;
@ -258,6 +259,12 @@ TAParamResult pVGMOut(String val) {
return TA_PARAM_SUCCESS;
}
TAParamResult pZSMOut(String val) {
zsmOutName=val;
e.setAudio(DIV_AUDIO_DUMMY);
return TA_PARAM_SUCCESS;
}
TAParamResult pCmdOut(String val) {
cmdOutName=val;
e.setAudio(DIV_AUDIO_DUMMY);
@ -279,6 +286,7 @@ void initParams() {
params.push_back(TAParam("a","audio",true,pAudio,"jack|sdl","set audio engine (SDL by default)"));
params.push_back(TAParam("o","output",true,pOutput,"<filename>","output audio to file"));
params.push_back(TAParam("O","vgmout",true,pVGMOut,"<filename>","output .vgm data"));
params.push_back(TAParam("Z","zsmout",true,pZSMOut,"<filename>","output .zsm data for Commander X16 Zsound"));
params.push_back(TAParam("C","cmdout",true,pCmdOut,"<filename>","output command stream"));
params.push_back(TAParam("b","binary",false,pBinary,"","set command stream output format to binary"));
params.push_back(TAParam("L","loglevel",true,pLogLevel,"debug|info|warning|error","set the log level (info by default)"));
@ -323,6 +331,7 @@ int main(int argc, char** argv) {
#endif
outName="";
vgmOutName="";
zsmOutName="";
cmdOutName="";
initParams();