VERA, ZSM Export: Add EFxx event as synchronization message, add sync message support in ZSM export

This commit is contained in:
MooingLemur 2023-07-05 15:07:44 -07:00
parent 23b65c61ce
commit fba48149a5
10 changed files with 67 additions and 24 deletions

View file

@ -13,3 +13,5 @@ currently Furnace does not support the PCM channel's stereo mode, though (except
- `2`: triangle - `2`: triangle
- `3`: noise - `3`: noise
- `22xx`: **set duty cycle.** range is `0` to `3F`. - `22xx`: **set duty cycle.** range is `0` to `3F`.
- `EFxx`: **ZSM synchronization event.**
- Where `xx` is the event payload. This has no effect in how the music is played in Furnace, but the ZSMKit library for the Commander X16 interprets these events inside ZSM files and optionally triggers a callback routine. This can be used, for instance, to cause game code to respond to beats or at certain points in the music.

View file

@ -99,7 +99,7 @@ Any offset values contained in the PCM data header block are relative to the beg
### PCM Sample Data ### PCM Sample Data
This is blob of PCM data with no internal formatting. Offsets into this blob are provided via the PCM header. The end of this blob will be the end of the ZSM file. This is a blob of PCM data with no internal formatting. Offsets into this blob are provided via the PCM header. The end of this blob will be the end of the ZSM file.
## EXTCMD Channel Scifications ## EXTCMD Channel Scifications
@ -149,7 +149,7 @@ Players implementing this channel should implement detection routines during ini
An expansion HW write will contain the following data: An expansion HW write will contain the following data:
Chip ID|Nuber of writes (`N`)| `N` tuples of data Chip ID|Number of writes (`N`)| `N` tuples of data
--|--|-- --|--|--
one byte|one byte|N * tuple_size bytes one byte|one byte|N * tuple_size bytes
@ -162,7 +162,14 @@ There are currently no supported expansion HW IDs assigned.
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 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. The synchronization format currently defines this one event type:
Event Type|Description|Message Format
--|--|--
`0x00`|Generic sync message|`xx` (any value from `0x00`-`0xff`)
An example of an EXTCMD containing one sync event might look as follows: `0x40 0x82 0x00 0x05`
#### 3: Custom #### 3: Custom

View file

@ -236,6 +236,8 @@ enum DivDispatchCmds {
DIV_CMD_NES_LINEAR_LENGTH, DIV_CMD_NES_LINEAR_LENGTH,
DIV_CMD_SYNC_MESSAGE, // (value)
DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol
DIV_CMD_MAX DIV_CMD_MAX

View file

@ -37,6 +37,7 @@ extern "C" {
#define rWritePCMRate(d) {regPool[65]=(d); pcm_write_rate(pcm,d);if (dumpWrites) addWrite(65,(d));} #define rWritePCMRate(d) {regPool[65]=(d); pcm_write_rate(pcm,d);if (dumpWrites) addWrite(65,(d));}
#define rWritePCMData(d) {regPool[66]=(d); pcm_write_fifo(pcm,d);} #define rWritePCMData(d) {regPool[66]=(d); pcm_write_fifo(pcm,d);}
#define rWritePCMVol(d) rWritePCMCtrl((regPool[64]&(~0x8f))|((d)&15)) #define rWritePCMVol(d) rWritePCMCtrl((regPool[64]&(~0x8f))|((d)&15))
#define rWriteZSMSync(d) {if (dumpWrites) addWrite(68,(d));}
const char* regCheatSheetVERA[]={ const char* regCheatSheetVERA[]={
"CHxFreq", "00+x*4", "CHxFreq", "00+x*4",
@ -46,6 +47,7 @@ const char* regCheatSheetVERA[]={
"AUDIO_CTRL", "40", "AUDIO_CTRL", "40",
"AUDIO_RATE", "41", "AUDIO_RATE", "41",
"AUDIO_DATA", "42", "AUDIO_DATA", "42",
"ZSM_SYNC", "44",
NULL NULL
}; };
@ -414,6 +416,9 @@ int DivPlatformVERA::dispatch(DivCommand c) {
case DIV_CMD_MACRO_ON: case DIV_CMD_MACRO_ON:
chan[c.chan].std.mask(c.value,false); chan[c.chan].std.mask(c.value,false);
break; break;
case DIV_CMD_SYNC_MESSAGE:
rWriteZSMSync(c.value);
break;
case DIV_ALWAYS_SET_VOLUME: case DIV_ALWAYS_SET_VOLUME:
return 0; return 0;
break; break;

View file

@ -51,7 +51,7 @@ class DivPlatformVERA: public DivDispatch {
Channel chan[17]; Channel chan[17];
DivDispatchOscBuffer* oscBuf[17]; DivDispatchOscBuffer* oscBuf[17];
bool isMuted[17]; bool isMuted[17];
unsigned char regPool[67]; unsigned char regPool[69];
struct VERA_PSG* psg; struct VERA_PSG* psg;
struct VERA_PCM* pcm; struct VERA_PCM* pcm;

View file

@ -236,6 +236,8 @@ const char* cmdName[]={
"NES_LINEAR_LENGTH", "NES_LINEAR_LENGTH",
"SYNC_MESSAGE",
"ALWAYS_SET_VOLUME" "ALWAYS_SET_VOLUME"
}; };

View file

@ -1460,6 +1460,7 @@ void DivEngine::registerSystems() {
{ {
{0x20, {DIV_CMD_WAVE, "20xx: Set waveform"}}, {0x20, {DIV_CMD_WAVE, "20xx: Set waveform"}},
{0x22, {DIV_CMD_STD_NOISE_MODE, "22xx: Set duty cycle (0 to 3F)"}}, {0x22, {DIV_CMD_STD_NOISE_MODE, "22xx: Set duty cycle (0 to 3F)"}},
{0xEF, {DIV_CMD_SYNC_MESSAGE, "EFxx: ZSM sync event"}},
} }
); );

View file

@ -118,9 +118,13 @@ void DivZSM::writePSG(unsigned char a, unsigned char v) {
// TODO: suppress writes to PSG voice that is not audible (volume=0) // TODO: suppress writes to PSG voice that is not audible (volume=0)
// ^ Let's leave these alone, ZSMKit has a feature that can benefit // ^ Let's leave these alone, ZSMKit has a feature that can benefit
// from silent channels. // from silent channels.
if (a>=67) { if (a>=69) {
logD ("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v); logD("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v);
return; return;
} else if (a==68) {
// Sync event
numWrites++;
return syncCache.push_back(v);
} else if (a>=64) { } else if (a>=64) {
return writePCM(a-64,v); return writePCM(a-64,v);
} }
@ -259,7 +263,7 @@ SafeWriter* DivZSM::finish() {
} }
void DivZSM::flushWrites() { void DivZSM::flushWrites() {
logD("ZSM: flushWrites.... numwrites=%d ticks=%d ymwrites=%d pcmMeta=%d pcmCache=%d pcmData=%d",numWrites,ticks,ymwrites.size(),pcmMeta.size(),pcmCache.size(),pcmData.size()); logD("ZSM: flushWrites.... numwrites=%d ticks=%d ymwrites=%d pcmMeta=%d pcmCache=%d pcmData=%d syncCache=%d",numWrites,ticks,ymwrites.size(),pcmMeta.size(),pcmCache.size(),pcmData.size(),syncCache.size());
if (numWrites==0) return; if (numWrites==0) return;
flushTicks(); // only flush ticks if there are writes pending. flushTicks(); // only flush ticks if there are writes pending.
for (unsigned char i=0; i<64; i++) { for (unsigned char i=0; i<64; i++) {
@ -287,43 +291,43 @@ void DivZSM::flushWrites() {
unsigned int pcmInst=0; unsigned int pcmInst=0;
int pcmOff=0; int pcmOff=0;
int pcmLen=0; int pcmLen=0;
int extCmdLen=pcmMeta.size()*2; int extCmd0Len=pcmMeta.size()*2;
if (pcmCache.size()) { if (pcmCache.size()) {
// collapse stereo data to mono if both channels are fully identical // collapse stereo data to mono if both channels are fully identical
// which cuts PCM data size in half for center-panned PCM events // which cuts PCM data size in half for center-panned PCM events
if (pcmCtrlDCCache & 0x10) { // stereo bit is on if (pcmCtrlDCCache&0x10) { // stereo bit is on
unsigned int e; unsigned int e;
if (pcmCtrlDCCache & 0x20) { // 16-bit if (pcmCtrlDCCache&0x20) { // 16-bit
// for 16-bit PCM data, the size must be a multiple of 4 // for 16-bit PCM data, the size must be a multiple of 4
if (pcmCache.size()%4==0) { if (pcmCache.size()%4==0) {
// check for identical L+R channels // check for identical L+R channels
for (e=0;e<pcmCache.size();e+=4) { for (e=0; e<pcmCache.size(); e+=4) {
if (pcmCache[e]!=pcmCache[e+2] || pcmCache[e+1]!=pcmCache[e+3]) break; if (pcmCache[e]!=pcmCache[e+2] || pcmCache[e+1]!=pcmCache[e+3]) break;
} }
if (e==pcmCache.size()) { // did not find a mismatch if (e==pcmCache.size()) { // did not find a mismatch
// collapse the data to mono 16-bit // collapse the data to mono 16-bit
for (e=0;e<pcmCache.size()>>1;e+=2) { for (e=0; e<pcmCache.size()>>1; e+=2) {
pcmCache[e]=pcmCache[e<<1]; pcmCache[e]=pcmCache[e<<1];
pcmCache[e+1]=pcmCache[(e<<1)+1]; pcmCache[e+1]=pcmCache[(e<<1)+1];
} }
pcmCache.resize(pcmCache.size()>>1); pcmCache.resize(pcmCache.size()>>1);
pcmCtrlDCCache &= ~0x10; // clear stereo bit pcmCtrlDCCache&=(unsigned char)~0x10; // clear stereo bit
} }
} }
} else { // 8-bit } else { // 8-bit
// for 8-bit PCM data, the size must be a multiple of 2 // for 8-bit PCM data, the size must be a multiple of 2
if (pcmCache.size()%2==0) { if (pcmCache.size()%2==0) {
// check for identical L+R channels // check for identical L+R channels
for (e=0;e<pcmCache.size();e+=2) { for (e=0; e<pcmCache.size(); e+=2) {
if (pcmCache[e]!=pcmCache[e+1]) break; if (pcmCache[e]!=pcmCache[e+1]) break;
} }
if (e==pcmCache.size()) { // did not find a mismatch if (e==pcmCache.size()) { // did not find a mismatch
// collapse the data to mono 8-bit // collapse the data to mono 8-bit
for (e=0;e<pcmCache.size()>>1;e++) { for (e=0; e<pcmCache.size()>>1; e++) {
pcmCache[e]=pcmCache[e<<1]; pcmCache[e]=pcmCache[e<<1];
} }
pcmCache.resize(pcmCache.size()>>1); pcmCache.resize(pcmCache.size()>>1);
pcmCtrlDCCache &= ~0x10; // clear stereo bit pcmCtrlDCCache&=(unsigned char)~0x10; // clear stereo bit
} }
} }
} }
@ -340,7 +344,7 @@ void DivZSM::flushWrites() {
pcmData.insert(pcmData.end(),pcmCache.begin(),pcmCache.end()); pcmData.insert(pcmData.end(),pcmCache.begin(),pcmCache.end());
} }
pcmCache.clear(); pcmCache.clear();
extCmdLen+=2; extCmd0Len+=2;
// search for a matching PCM instrument definition // search for a matching PCM instrument definition
for (S_pcmInst& inst: pcmInsts) { for (S_pcmInst& inst: pcmInsts) {
if (inst.offset==pcmOff && inst.length==pcmLen && inst.geometry==pcmCtrlDCCache) if (inst.offset==pcmOff && inst.length==pcmLen && inst.geometry==pcmCtrlDCCache)
@ -355,14 +359,14 @@ void DivZSM::flushWrites() {
pcmInsts.push_back(inst); pcmInsts.push_back(inst);
} }
} }
if (extCmdLen>63) { // this would be bad, but will almost certainly never happen if (extCmd0Len>63) { // this would be bad, but will almost certainly never happen
logE("ZSM: extCmd exceeded maximum length of 63: %d",extCmdLen); logE("ZSM: extCmd 0 exceeded maximum length of 63: %d",extCmd0Len);
extCmdLen=0; extCmd0Len=0;
pcmMeta.clear(); pcmMeta.clear();
} }
if (extCmdLen) { // we have some PCM events to write if (extCmd0Len) { // we have some PCM events to write
w->writeC(0x40); w->writeC(ZSM_EXT);
w->writeC((unsigned char)extCmdLen); // the high two bits are guaranteed to be zero, meaning this is a PCM command w->writeC(ZSM_EXT_PCM|(unsigned char)extCmd0Len);
for (DivRegWrite& write: pcmMeta) { for (DivRegWrite& write: pcmMeta) {
w->writeC(write.addr); w->writeC(write.addr);
w->writeC(write.val); w->writeC(write.val);
@ -373,6 +377,18 @@ void DivZSM::flushWrites() {
w->writeC((unsigned char)pcmInst&0xff); w->writeC((unsigned char)pcmInst&0xff);
} }
} }
n=0;
while (n<(long)syncCache.size()) { // we have one or more sync events to write
int writes=syncCache.size()-n;
w->writeC(ZSM_EXT);
if (writes>ZSM_SYNC_MAX_WRITES) writes=ZSM_SYNC_MAX_WRITES;
w->writeC(ZSM_EXT_SYNC|(writes<<1));
for (; writes>0; writes--) {
w->writeC(0x00); // 0x00 = Arbitrary sync message
w->writeC(syncCache[n++]);
}
}
syncCache.clear();
numWrites=0; numWrites=0;
} }

View file

@ -30,9 +30,16 @@
#define ZSM_YM_CMD 0x40 #define ZSM_YM_CMD 0x40
#define ZSM_DELAY_CMD 0x80 #define ZSM_DELAY_CMD 0x80
#define ZSM_YM_MAX_WRITES 63 #define ZSM_YM_MAX_WRITES 63
#define ZSM_SYNC_MAX_WRITES 31
#define ZSM_DELAY_MAX 127 #define ZSM_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD #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 YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES }; enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
@ -52,6 +59,7 @@ class DivZSM {
std::vector<unsigned char> pcmData; std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache; std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts; std::vector<S_pcmInst> pcmInsts;
std::vector<unsigned char> syncCache;
int loopOffset; int loopOffset;
int numWrites; int numWrites;
int ticks; int ticks;

View file

@ -151,7 +151,7 @@ SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop) {
for (DivRegWrite& write: writes) { for (DivRegWrite& write: writes) {
if (i==YM) zsm.writeYM(write.addr&0xff,write.val); if (i==YM) zsm.writeYM(write.addr&0xff,write.val);
if (i==VERA) { if (i==VERA) {
if (done && write.addr >= 64) continue; // don't process any PCM events on the loop lookahead 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); zsm.writePSG(write.addr&0xff,write.val);
} }
} }