mirror of
https://github.com/tildearrow/furnace.git
synced 2024-11-22 20:45:11 +00:00
VERA, ZSM Export: Add EFxx event as synchronization message, add sync message support in ZSM export
This commit is contained in:
parent
23b65c61ce
commit
fba48149a5
10 changed files with 67 additions and 24 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -236,6 +236,8 @@ enum DivDispatchCmds {
|
|||
|
||||
DIV_CMD_NES_LINEAR_LENGTH,
|
||||
|
||||
DIV_CMD_SYNC_MESSAGE, // (value)
|
||||
|
||||
DIV_ALWAYS_SET_VOLUME, // () -> alwaysSetVol
|
||||
|
||||
DIV_CMD_MAX
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -236,6 +236,8 @@ const char* cmdName[]={
|
|||
|
||||
"NES_LINEAR_LENGTH",
|
||||
|
||||
"SYNC_MESSAGE",
|
||||
|
||||
"ALWAYS_SET_VOLUME"
|
||||
};
|
||||
|
||||
|
|
|
@ -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"}},
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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<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 (e==pcmCache.size()) { // did not find a mismatch
|
||||
// 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+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<pcmCache.size();e+=2) {
|
||||
for (e=0; e<pcmCache.size(); e+=2) {
|
||||
if (pcmCache[e]!=pcmCache[e+1]) break;
|
||||
}
|
||||
if (e==pcmCache.size()) { // did not find a mismatch
|
||||
// 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.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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<unsigned char> pcmData;
|
||||
std::vector<unsigned char> pcmCache;
|
||||
std::vector<S_pcmInst> pcmInsts;
|
||||
std::vector<unsigned char> syncCache;
|
||||
int loopOffset;
|
||||
int numWrites;
|
||||
int ticks;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue