From fba48149a5bbdc0e37ba71cb6ccc22b003ac1254 Mon Sep 17 00:00:00 2001 From: MooingLemur Date: Wed, 5 Jul 2023 15:07:44 -0700 Subject: [PATCH 1/2] VERA, ZSM Export: Add EFxx event as synchronization message, add sync message support in ZSM export --- doc/7-systems/vera.md | 2 ++ papers/zsm-format.md | 13 +++++++-- src/engine/dispatch.h | 2 ++ src/engine/platform/vera.cpp | 5 ++++ src/engine/platform/vera.h | 2 +- src/engine/playback.cpp | 2 ++ src/engine/sysDef.cpp | 1 + src/engine/zsm.cpp | 54 +++++++++++++++++++++++------------- src/engine/zsm.h | 8 ++++++ src/engine/zsmOps.cpp | 2 +- 10 files changed, 67 insertions(+), 24 deletions(-) diff --git a/doc/7-systems/vera.md b/doc/7-systems/vera.md index 8f50e0e7..4497887b 100644 --- a/doc/7-systems/vera.md +++ b/doc/7-systems/vera.md @@ -13,3 +13,5 @@ currently Furnace does not support the PCM channel's stereo mode, though (except - `2`: triangle - `3`: noise - `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. diff --git a/papers/zsm-format.md b/papers/zsm-format.md index 5957e3c6..e0fd2c02 100644 --- a/papers/zsm-format.md +++ b/papers/zsm-format.md @@ -99,7 +99,7 @@ Any offset values contained in the PCM data header block are relative to the beg ### 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 @@ -149,7 +149,7 @@ Players implementing this channel should implement detection routines during ini 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 @@ -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 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 diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index c19e7d95..9a7a067d 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -236,6 +236,8 @@ enum DivDispatchCmds { DIV_CMD_NES_LINEAR_LENGTH, + DIV_CMD_SYNC_MESSAGE, // (value) + DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol DIV_CMD_MAX diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp index 3e3ac42c..8c1bf948 100644 --- a/src/engine/platform/vera.cpp +++ b/src/engine/platform/vera.cpp @@ -37,6 +37,7 @@ extern "C" { #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 rWritePCMVol(d) rWritePCMCtrl((regPool[64]&(~0x8f))|((d)&15)) +#define rWriteZSMSync(d) {if (dumpWrites) addWrite(68,(d));} const char* regCheatSheetVERA[]={ "CHxFreq", "00+x*4", @@ -46,6 +47,7 @@ const char* regCheatSheetVERA[]={ "AUDIO_CTRL", "40", "AUDIO_RATE", "41", "AUDIO_DATA", "42", + "ZSM_SYNC", "44", NULL }; @@ -414,6 +416,9 @@ int DivPlatformVERA::dispatch(DivCommand c) { case DIV_CMD_MACRO_ON: chan[c.chan].std.mask(c.value,false); break; + case DIV_CMD_SYNC_MESSAGE: + rWriteZSMSync(c.value); + break; case DIV_ALWAYS_SET_VOLUME: return 0; break; diff --git a/src/engine/platform/vera.h b/src/engine/platform/vera.h index 7476a316..227512a7 100644 --- a/src/engine/platform/vera.h +++ b/src/engine/platform/vera.h @@ -51,7 +51,7 @@ class DivPlatformVERA: public DivDispatch { Channel chan[17]; DivDispatchOscBuffer* oscBuf[17]; bool isMuted[17]; - unsigned char regPool[67]; + unsigned char regPool[69]; struct VERA_PSG* psg; struct VERA_PCM* pcm; diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index ce94b883..c70844a1 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -236,6 +236,8 @@ const char* cmdName[]={ "NES_LINEAR_LENGTH", + "SYNC_MESSAGE", + "ALWAYS_SET_VOLUME" }; diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index f4d76ceb..6b94a66f 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1460,6 +1460,7 @@ void DivEngine::registerSystems() { { {0x20, {DIV_CMD_WAVE, "20xx: Set waveform"}}, {0x22, {DIV_CMD_STD_NOISE_MODE, "22xx: Set duty cycle (0 to 3F)"}}, + {0xEF, {DIV_CMD_SYNC_MESSAGE, "EFxx: ZSM sync event"}}, } ); diff --git a/src/engine/zsm.cpp b/src/engine/zsm.cpp index 912dc630..b0f42111 100644 --- a/src/engine/zsm.cpp +++ b/src/engine/zsm.cpp @@ -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) // ^ Let's leave these alone, ZSMKit has a feature that can benefit // from silent channels. - if (a>=67) { - logD ("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v); + if (a>=69) { + logD("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v); return; + } else if (a==68) { + // Sync event + numWrites++; + return syncCache.push_back(v); } else if (a>=64) { return writePCM(a-64,v); } @@ -259,7 +263,7 @@ SafeWriter* DivZSM::finish() { } 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; flushTicks(); // only flush ticks if there are writes pending. for (unsigned char i=0; i<64; i++) { @@ -287,43 +291,43 @@ void DivZSM::flushWrites() { unsigned int pcmInst=0; int pcmOff=0; int pcmLen=0; - int extCmdLen=pcmMeta.size()*2; + int extCmd0Len=pcmMeta.size()*2; if (pcmCache.size()) { // collapse stereo data to mono if both channels are fully identical // 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; - if (pcmCtrlDCCache & 0x20) { // 16-bit + if (pcmCtrlDCCache&0x20) { // 16-bit // for 16-bit PCM data, the size must be a multiple of 4 if (pcmCache.size()%4==0) { // check for identical L+R channels - for (e=0;e>1;e+=2) { + for (e=0; e>1; e+=2) { pcmCache[e]=pcmCache[e<<1]; pcmCache[e+1]=pcmCache[(e<<1)+1]; } pcmCache.resize(pcmCache.size()>>1); - pcmCtrlDCCache &= ~0x10; // clear stereo bit + pcmCtrlDCCache&=(unsigned char)~0x10; // clear stereo bit } } } else { // 8-bit // for 8-bit PCM data, the size must be a multiple of 2 if (pcmCache.size()%2==0) { // check for identical L+R channels - for (e=0;e>1;e++) { + for (e=0; e>1; e++) { pcmCache[e]=pcmCache[e<<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()); } pcmCache.clear(); - extCmdLen+=2; + extCmd0Len+=2; // search for a matching PCM instrument definition for (S_pcmInst& inst: pcmInsts) { if (inst.offset==pcmOff && inst.length==pcmLen && inst.geometry==pcmCtrlDCCache) @@ -355,14 +359,14 @@ void DivZSM::flushWrites() { pcmInsts.push_back(inst); } } - if (extCmdLen>63) { // this would be bad, but will almost certainly never happen - logE("ZSM: extCmd exceeded maximum length of 63: %d",extCmdLen); - extCmdLen=0; + if (extCmd0Len>63) { // this would be bad, but will almost certainly never happen + logE("ZSM: extCmd 0 exceeded maximum length of 63: %d",extCmd0Len); + extCmd0Len=0; pcmMeta.clear(); } - if (extCmdLen) { // we have some PCM events to write - w->writeC(0x40); - w->writeC((unsigned char)extCmdLen); // the high two bits are guaranteed to be zero, meaning this is a PCM command + if (extCmd0Len) { // we have some PCM events to write + w->writeC(ZSM_EXT); + w->writeC(ZSM_EXT_PCM|(unsigned char)extCmd0Len); for (DivRegWrite& write: pcmMeta) { w->writeC(write.addr); w->writeC(write.val); @@ -373,6 +377,18 @@ void DivZSM::flushWrites() { 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; } diff --git a/src/engine/zsm.h b/src/engine/zsm.h index 17a1fd06..42300cf7 100644 --- a/src/engine/zsm.h +++ b/src/engine/zsm.h @@ -30,9 +30,16 @@ #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 }; @@ -52,6 +59,7 @@ class DivZSM { std::vector pcmData; std::vector pcmCache; std::vector pcmInsts; + std::vector syncCache; int loopOffset; int numWrites; int ticks; diff --git a/src/engine/zsmOps.cpp b/src/engine/zsmOps.cpp index c59efeae..85ddb5d4 100644 --- a/src/engine/zsmOps.cpp +++ b/src/engine/zsmOps.cpp @@ -151,7 +151,7 @@ SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop) { 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 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); } } From a8a02b9ebbb3e898b573017cca279fd0812ac7fc Mon Sep 17 00:00:00 2001 From: MooingLemur Date: Wed, 5 Jul 2023 15:29:11 -0700 Subject: [PATCH 2/2] Changed EFxx to EExx at request of tildearrow --- doc/7-systems/vera.md | 2 +- src/engine/dispatch.h | 2 +- src/engine/platform/vera.cpp | 2 +- src/engine/playback.cpp | 3 ++- src/engine/sysDef.cpp | 1 - 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/7-systems/vera.md b/doc/7-systems/vera.md index 4497887b..e89744a9 100644 --- a/doc/7-systems/vera.md +++ b/doc/7-systems/vera.md @@ -13,5 +13,5 @@ currently Furnace does not support the PCM channel's stereo mode, though (except - `2`: triangle - `3`: noise - `22xx`: **set duty cycle.** range is `0` to `3F`. -- `EFxx`: **ZSM synchronization event.** +- `EExx`: **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. diff --git a/src/engine/dispatch.h b/src/engine/dispatch.h index 9a7a067d..c458cf9c 100644 --- a/src/engine/dispatch.h +++ b/src/engine/dispatch.h @@ -236,7 +236,7 @@ enum DivDispatchCmds { DIV_CMD_NES_LINEAR_LENGTH, - DIV_CMD_SYNC_MESSAGE, // (value) + DIV_CMD_EXTERNAL, // (value) DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp index 8c1bf948..fe34b8bc 100644 --- a/src/engine/platform/vera.cpp +++ b/src/engine/platform/vera.cpp @@ -416,7 +416,7 @@ int DivPlatformVERA::dispatch(DivCommand c) { case DIV_CMD_MACRO_ON: chan[c.chan].std.mask(c.value,false); break; - case DIV_CMD_SYNC_MESSAGE: + case DIV_CMD_EXTERNAL: rWriteZSMSync(c.value); break; case DIV_ALWAYS_SET_VOLUME: diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index c70844a1..4d9e12ea 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -236,7 +236,7 @@ const char* cmdName[]={ "NES_LINEAR_LENGTH", - "SYNC_MESSAGE", + "EXTERNAL", "ALWAYS_SET_VOLUME" }; @@ -915,6 +915,7 @@ void DivEngine::processRow(int i, bool afterDelay) { //printf("\x1b[1;36m%d: extern command %d\x1b[m\n",i,effectVal); extValue=effectVal; extValuePresent=true; + dispatchCmd(DivCommand(DIV_CMD_EXTERNAL,effectVal)); break; case 0xef: // global pitch globalPitch+=(signed char)(effectVal-0x80); diff --git a/src/engine/sysDef.cpp b/src/engine/sysDef.cpp index 6b94a66f..f4d76ceb 100644 --- a/src/engine/sysDef.cpp +++ b/src/engine/sysDef.cpp @@ -1460,7 +1460,6 @@ void DivEngine::registerSystems() { { {0x20, {DIV_CMD_WAVE, "20xx: Set waveform"}}, {0x22, {DIV_CMD_STD_NOISE_MODE, "22xx: Set duty cycle (0 to 3F)"}}, - {0xEF, {DIV_CMD_SYNC_MESSAGE, "EFxx: ZSM sync event"}}, } );