Commander X16 file export: ZSM format

This commit is contained in:
ZeroByteOrg 2022-05-26 00:24:21 -05:00
parent 68cc84253c
commit b034e3c5da
12 changed files with 733 additions and 16 deletions

View File

@ -375,6 +375,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

@ -458,6 +458,8 @@ class DivEngine {
SafeWriter* buildROM(int sys);
// dump to VGM.
SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171);
// dump to ZSM.
SafeWriter* saveZSM(unsigned int zsmrate=60, bool loop=true);
// export to an audio file
bool saveAudio(const char* path, int loops, DivAudioExportModes mode);
// wait for audio export to finish

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);}

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"
ZSM::ZSM() {
w = NULL;
init();
}
ZSM::~ZSM() {
}
void ZSM::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 ZSM::getoffset() {
return w->tell();
}
void ZSM::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 ZSM::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 ZSM::writePCM(unsigned char a, unsigned char v) {
// ZSM standard for PCM playback has not been established yet.
}
void ZSM::tick(int numticks) {
flushWrites();
ticks += numticks;
}
void ZSM::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((short)((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* ZSM::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((short)(ymMask & 0xff));
w->writeS((short)(psgMask & 0xffff));
// todo: put PCM offset/data writes here once defined in ZSM standard.
return w;
}
void ZSM::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 (int 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(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(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 ZSM::flushTicks() {
while (ticks > ZSM_DELAY_MAX) {
logD("ZSM: write delay %d (max)",ZSM_DELAY_MAX);
w->writeC(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 ZSM {
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:
ZSM();
~ZSM();
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="";
ZSM 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

@ -860,7 +860,7 @@ void FurnaceGUI::stopPreviewNote(SDL_Scancode scancode, bool autoNote) {
void FurnaceGUI::noteInput(int num, int key, int vol) {
DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],true);
prepareUndo(GUI_UNDO_PATTERN_EDIT);
if (key==100) { // note off
@ -1399,6 +1399,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_ROM:
showError("Coming soon!");
break;
@ -2023,7 +2033,7 @@ void FurnaceGUI::editOptions(bool topMenu) {
snprintf(id,63,"%.2x##LatchFX",data);
ImGui::PushStyleColor(ImGuiCol_Text,uiColors[fxColors[data]]);
}
if (ImGui::Selectable(id,latchTarget==3,ImGuiSelectableFlags_DontClosePopups)) {
latchTarget=3;
latchNibble=false;
@ -2096,7 +2106,7 @@ void FurnaceGUI::editOptions(bool topMenu) {
doTranspose(transposeAmount,opMaskTransposeValue);
ImGui::CloseCurrentPopup();
}
ImGui::Separator();
if (ImGui::MenuItem("interpolate",BIND_FOR(GUI_ACTION_PAT_INTERPOLATE))) doInterpolate();
if (ImGui::BeginMenu("change instrument...")) {
@ -2212,7 +2222,7 @@ void FurnaceGUI::toggleMobileUI(bool enable, bool force) {
if (mobileUI!=enable || force) {
if (!mobileUI && enable) {
ImGui::SaveIniSettingsToDisk(finalLayoutPath);
}
}
mobileUI=enable;
if (mobileUI) {
ImGui::GetIO().IniFilename=NULL;
@ -2220,7 +2230,7 @@ void FurnaceGUI::toggleMobileUI(bool enable, bool force) {
ImGui::GetIO().IniFilename=finalLayoutPath;
ImGui::LoadIniSettingsFromDisk(finalLayoutPath);
}
}
}
}
int _processEvent(void* instance, SDL_Event* event) {
@ -2569,7 +2579,7 @@ bool FurnaceGUI::loop() {
if (ImGui::GetIO().MouseDown[0] || ImGui::GetIO().MouseDown[1] || ImGui::GetIO().MouseDown[2] || ImGui::GetIO().MouseDown[3] || ImGui::GetIO().MouseDown[4]) {
WAKE_UP;
}
while (true) {
midiLock.lock();
if (midiQueue.empty()) {
@ -2722,10 +2732,13 @@ bool FurnaceGUI::loop() {
midiLock.unlock();
}
<<<<<<< HEAD
eventTimeEnd=SDL_GetPerformanceCounter();
layoutTimeBegin=SDL_GetPerformanceCounter();
=======
>>>>>>> Commander X16 file export: ZSM format
ImGui_ImplSDLRenderer_NewFrame();
ImGui_ImplSDL2_NewFrame(sdlWin);
ImGui::NewFrame();
@ -2824,6 +2837,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(" Go ")) {
openFileDialog(GUI_FILE_EXPORT_ZSM);
ImGui::CloseCurrentPopup();
}
ImGui::EndMenu();
}
}
ImGui::Separator();
if (ImGui::BeginMenu("add system...")) {
for (int j=0; availableSystems[j]; j++) {
@ -2939,7 +2972,7 @@ bool FurnaceGUI::loop() {
if (ImGui::MenuItem("register view",BIND_FOR(GUI_ACTION_WINDOW_REGISTER_VIEW),regViewOpen)) regViewOpen=!regViewOpen;
if (ImGui::MenuItem("log viewer",BIND_FOR(GUI_ACTION_WINDOW_LOG),logOpen)) logOpen=!logOpen;
if (ImGui::MenuItem("statistics",BIND_FOR(GUI_ACTION_WINDOW_STATS),statsOpen)) statsOpen=!statsOpen;
ImGui::EndMenu();
}
if (ImGui::BeginMenu("help")) {
@ -3022,8 +3055,39 @@ bool FurnaceGUI::loop() {
}
if (mobileUI) {
globalWinFlags=ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoBringToFrontOnFocus;
drawMobileControls();
ImGuiViewport* mainView=ImGui::GetMainViewport();
ImGui::SetNextWindowPos(mainView->Pos);
ImGui::SetNextWindowSize(mainView->Size);
ImGui::SetNextWindowViewport(mainView->ID);
ImGuiID dockID=ImGui::GetID("MobileUISpace");
ImGuiWindowFlags muiFlags=ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoCollapse|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoDocking|ImGuiWindowFlags_NoBringToFrontOnFocus|ImGuiWindowFlags_NoNavFocus;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("MobileUI",NULL,muiFlags);
ImGui::PopStyleVar(3);
if (ImGui::DockBuilderGetNode(dockID)==NULL) {
ImGui::DockBuilderRemoveNode(dockID);
ImGuiID dn=ImGui::DockBuilderAddNode(dockID);
ImGuiID upper, lower, left, right;
ImGui::DockBuilderSplitNode(dn,ImGuiDir_Left,0.1f,&left,&right);
ImGui::DockBuilderSplitNode(right,ImGuiDir_Down,0.2f,&lower,&upper);
ImGui::DockBuilderDockWindow("Mobile Controls",left);
ImGui::DockBuilderDockWindow("Pattern",upper);
ImGui::DockBuilderDockWindow("Piano",lower);
ImGui::DockBuilderFinish(dn);
}
ImGui::DockSpace(dockID);
ImGui::End();
if (ImGui::Begin("Mobile Controls")) {
ImGui::Text("Hi!");
if (ImGui::Button("Get me out of here")) {
toggleMobileUI(false);
}
}
ImGui::End();
drawPattern();
drawPiano();
} else {
@ -3127,6 +3191,9 @@ bool FurnaceGUI::loop() {
case GUI_FILE_EXPORT_ROM:
workingDirVGMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
break;
case GUI_FILE_EXPORT_ZSM:
workingDirZSMExport=fileDialog->getPath()+DIR_SEPARATOR_STR;
break;
case GUI_FILE_LOAD_MAIN_FONT:
case GUI_FILE_LOAD_PAT_FONT:
workingDirFont=fileDialog->getPath()+DIR_SEPARATOR_STR;
@ -3175,6 +3242,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_COLORS) {
checkExtension(".cfgc");
}
@ -3337,6 +3407,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;
@ -3759,7 +3849,7 @@ bool FurnaceGUI::loop() {
}
logD("saving backup...");
SafeWriter* w=e->saveFur(true);
if (w!=NULL) {
FILE* outFile=ps_fopen(backupPath.c_str(),"wb");
if (outFile!=NULL) {
@ -3834,6 +3924,7 @@ bool FurnaceGUI::init() {
workingDirSample=e->getConfString("lastDirSample",workingDir);
workingDirAudioExport=e->getConfString("lastDirAudioExport",workingDir);
workingDirVGMExport=e->getConfString("lastDirVGMExport",workingDir);
workingDirZSMExport=e->getConfString("lastDirZSMExport",workingDir);
workingDirFont=e->getConfString("lastDirFont",workingDir);
workingDirColors=e->getConfString("lastDirColors",workingDir);
workingDirKeybinds=e->getConfString("lastDirKeybinds",workingDir);
@ -4042,6 +4133,7 @@ bool FurnaceGUI::finish() {
e->setConf("lastDirSample",workingDirSample);
e->setConf("lastDirAudioExport",workingDirAudioExport);
e->setConf("lastDirVGMExport",workingDirVGMExport);
e->setConf("lastDirZSMExport",workingDirZSMExport);
e->setConf("lastDirFont",workingDirFont);
e->setConf("lastDirColors",workingDirColors);
e->setConf("lastDirKeybinds",workingDirKeybinds);
@ -4125,6 +4217,7 @@ FurnaceGUI::FurnaceGUI():
displayError(false),
displayExporting(false),
vgmExportLoop(true),
zsmExportLoop(true),
wantCaptureKeyboard(false),
oldWantCaptureKeyboard(false),
displayMacroMenu(false),
@ -4134,6 +4227,7 @@ FurnaceGUI::FurnaceGUI():
wantScrollList(false),
vgmExportVersion(0x171),
drawHalt(10),
zsmExportTickRate(60),
macroPointSize(16),
globalWinFlags(0),
curFileDialog(GUI_FILE_OPEN),

View File

@ -73,6 +73,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,
@ -260,6 +261,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_ROM,
GUI_FILE_LOAD_MAIN_FONT,
GUI_FILE_LOAD_PAT_FONT,
@ -807,15 +809,16 @@ class FurnaceGUI {
bool updateSampleTex;
String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile;
String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport, workingDirVGMExport, workingDirFont, workingDirColors, workingDirKeybinds, workingDirLayout, workingDirROM;
String mmlString[32];
String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport, workingDirVGMExport, workingDirZSMExport, workingDirFont, workingDirColors, workingDirKeybinds, workingDirLayout, workingDirROM;
String mmlString[17];
String mmlStringW;
bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu;
bool quit, warnQuit, willCommit, edit, modified, displayError, displayExporting, vgmExportLoop, zsmExportLoop, wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu;
bool displayNew, fullScreen, preserveChanPos, wantScrollList;
bool willExport[32];
int vgmExportVersion;
int drawHalt;
int zsmExportTickRate;
int macroPointSize;
ImGuiWindowFlags globalWinFlags;
@ -1196,7 +1199,7 @@ class FurnaceGUI {
int chanToMove;
ImVec2 patWindowPos, patWindowSize;
// pattern view specific
ImVec2 fourChars, threeChars, twoChars;
ImVec2 noteCellSize, insCellSize, volCellSize, effectCellSize, effectValCellSize;
@ -1250,7 +1253,7 @@ class FurnaceGUI {
// visualizer
float keyHit[DIV_MAX_CHANS];
int lastIns[DIV_MAX_CHANS];
// log window
bool followLog;

View File

@ -679,6 +679,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

@ -1320,6 +1320,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();
@ -2917,6 +2918,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

@ -47,6 +47,7 @@ FurnaceGUI g;
String outName;
String vgmOutName;
String zsmOutName;
int loops=1;
DivAudioExportModes outMode=DIV_EXPORT_MODE_ONE;
@ -213,6 +214,12 @@ TAParamResult pVGMOut(String val) {
return TA_PARAM_SUCCESS;
}
TAParamResult pZSMOut(String val) {
zsmOutName=val;
e.setAudio(DIV_AUDIO_DUMMY);
return TA_PARAM_SUCCESS;
}
bool needsValue(String param) {
for (size_t i=0; i<params.size(); i++) {
if (params[i].name==param) {
@ -228,6 +235,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("L","loglevel",true,pLogLevel,"debug|info|warning|error","set the log level (info by default)"));
params.push_back(TAParam("v","view",true,pView,"pattern|commands|nothing","set visualization (pattern by default)"));
params.push_back(TAParam("c","console",false,pConsole,"","enable console mode"));
@ -251,6 +259,7 @@ int main(int argc, char** argv) {
#endif
outName="";
vgmOutName="";
zsmOutName="";
initParams();