ZSM export: Update format, implement PCM export support (#1191)

* ZSM export: suppress the extra tick before the loop

* ZSM: initial PCM export support

* Docs: update zsm-format.md with PCM format

* applied requested style changes from PR
This commit is contained in:
mooinglemur 2023-07-04 03:24:49 +00:00 committed by GitHub
parent a3a8dd7f0d
commit 93097b40e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 272 additions and 17 deletions

View file

@ -19,6 +19,7 @@
#include "vera.h"
#include "../engine.h"
#include "../../ta-log.h"
#include <string.h>
#include <math.h>
@ -32,8 +33,8 @@ extern "C" {
#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);}
#define rWritePCMRate(d) {regPool[65]=(d); pcm_write_rate(pcm,d);}
#define rWritePCMCtrl(d) {regPool[64]=(d); pcm_write_ctrl(pcm,d);if (dumpWrites) addWrite(64,(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 rWritePCMVol(d) rWritePCMCtrl((regPool[64]&(~0x8f))|((d)&15))
@ -226,6 +227,49 @@ void DivPlatformVERA::tick(bool sysTick) {
rWritePCMRate(chan[16].freq&0xff);
chan[16].freqChanged=false;
}
if (dumpWrites) {
DivSample* s=parent->getSample(chan[16].pcm.sample);
if (s->samples>0) {
while (true) {
short tmp_l=0;
short tmp_r=0;
if (!isMuted[16]) {
if (chan[16].pcm.depth16) {
tmp_l=s->data16[chan[16].pcm.pos];
tmp_r=tmp_l;
} else {
tmp_l=s->data8[chan[16].pcm.pos];
tmp_r=tmp_l;
}
if (!(chan[16].pan&1)) tmp_l=0;
if (!(chan[16].pan&2)) tmp_r=0;
}
if (chan[16].pcm.depth16) {
addWrite(66,tmp_l&0xff);
addWrite(66,(tmp_l>>8)&0xff);
addWrite(66,tmp_r&0xff);
addWrite(66,(tmp_r>>8)&0xff);
} else {
addWrite(66,tmp_l&0xff);
addWrite(66,tmp_r&0xff);
}
chan[16].pcm.pos++;
if (s->isLoopable() && chan[16].pcm.pos>=(unsigned int)s->loopEnd) {
//chan[16].pcm.pos=s->loopStart;
logI("VERA PCM export: treating looped sample as non-looped");
chan[16].pcm.sample=-1;
break;
}
if (chan[16].pcm.pos>=s->samples) {
chan[16].pcm.sample=-1;
break;
}
}
} else {
chan[16].pcm.sample=-1;
}
}
}
int DivPlatformVERA::dispatch(DivCommand c) {

View file

@ -53,9 +53,17 @@ void DivZSM::init(unsigned int rate) {
tickRate=rate;
loopOffset=-1;
numWrites=0;
ticks=0;
// Initialize YM/PSG states
memset(&ymState,-1,sizeof(ymState));
memset(&psgState,-1,sizeof(psgState));
ticks=0;
// Initialize PCM states
pcmRateCache=-1;
pcmCtrlRVCache=-1;
pcmCtrlDCCache=-1;
// Channel masks
ymMask=0;
psgMask=0;
}
int DivZSM::getoffset() {
@ -108,9 +116,13 @@ void DivZSM::writeYM(unsigned char a, unsigned char v) {
void DivZSM::writePSG(unsigned char a, unsigned char v) {
// TODO: suppress writes to PSG voice that is not audible (volume=0)
if (a>=64) {
// ^ 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);
return;
} else if (a>=64) {
return writePCM(a-64,v);
}
if (psgState[psg_PREV][a]==v) {
if (psgState[psg_NEW][a]!=v) {
@ -131,7 +143,26 @@ void DivZSM::writePSG(unsigned char a, unsigned char v) {
}
void DivZSM::writePCM(unsigned char a, unsigned char v) {
// ZSM standard for PCM playback has not been established yet.
if (a==0) { // PCM Ctrl
// cache the depth and channels but don't write it to the
// register queue
pcmCtrlDCCache=v&0x30;
// save only the reset bit and volume (if it isn't a dupe)
if (pcmCtrlRVCache!=(v&0x8f)) {
pcmMeta.push_back(DivRegWrite(a,(v&0x8f)));
pcmCtrlRVCache=v&0x8f;
numWrites++;
}
} else if (a==1) { // PCM Rate
if (pcmRateCache!=v) {
pcmMeta.push_back(DivRegWrite(a,v));
pcmRateCache=v;
numWrites++;
}
} else if (a==2) { // PCM data
pcmCache.push_back(v);
numWrites++;
}
}
void DivZSM::tick(int numticks) {
@ -151,6 +182,9 @@ void DivZSM::setLoopPoint() {
w->seek(loopOffset,SEEK_SET);
// reset the PSG shadow and write cache
memset(&psgState,-1,sizeof(psgState));
// reset the PCM caches that would inhibit dupes
pcmRateCache=-1;
pcmCtrlRVCache=-1;
// reset the YM shadow....
memset(&ymState[ym_PREV],-1,sizeof(ymState[ym_PREV]));
// ... and cache (except for unused channels)
@ -170,16 +204,62 @@ SafeWriter* DivZSM::finish() {
tick(0); // flush any pending writes / ticks
flushTicks(); // flush ticks in case there were no writes pending
w->writeC(ZSM_EOF);
if (pcmInsts.size()>256) {
logE("ZSM: more than the maximum number of PCM instruments exist. Skipping PCM export entirely.");
pcmData.clear();
pcmInsts.clear();
} else if (pcmData.size()) { // if exists, write PCM instruments and blob to the end of file
int pcmOff=w->tell();
w->writeC('P');
w->writeC('C');
w->writeC('M');
w->writeC((unsigned char)pcmInsts.size()-1);
int i=0;
for (S_pcmInst& inst: pcmInsts) {
// write out the instruments
// PCM playback location follows:
// <instrument number>
// <geometry (depth and channel)>
// <l m h> of PCM data offset
// <l m h> of length
w->writeC((unsigned char)i&0xff);
w->writeC((unsigned char)inst.geometry&0x30);
w->writeC((unsigned char)inst.offset&0xff);
w->writeC((unsigned char)(inst.offset>>8)&0xff);
w->writeC((unsigned char)(inst.offset>>16)&0xff);
w->writeC((unsigned char)inst.length&0xff);
w->writeC((unsigned char)(inst.length>>8)&0xff);
w->writeC((unsigned char)(inst.length>>16)&0xff);
// Feature mask: Lxxxxxxx
// L = Loop enabled
w->writeC(0);
// Loop point (not yet implemented)
w->writeC(0);
w->writeS(0);
// Reserved for future use
w->writeS(0);
w->writeS(0);
i++;
}
for (unsigned char& c: pcmData) {
w->writeC(c);
}
pcmData.clear();
// update PCM offset in file
w->seek(0x06,SEEK_SET);
w->writeC((unsigned char)pcmOff&0xff);
w->writeC((unsigned char)(pcmOff>>8)&0xff);
w->writeC((unsigned char)(pcmOff>>16)&0xff);
}
// 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());
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());
if (numWrites==0) return;
flushTicks(); // only flush ticks if there are writes pending.
for (unsigned char i=0; i<64; i++) {
@ -204,6 +284,95 @@ void DivZSM::flushWrites() {
w->writeC(write.val);
}
ymwrites.clear();
unsigned int pcmInst=0;
int pcmOff=0;
int pcmLen=0;
int extCmdLen=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
unsigned int e;
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) {
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) {
pcmCache[e]=pcmCache[e<<1];
pcmCache[e+1]=pcmCache[(e<<1)+1];
}
pcmCache.resize(pcmCache.size()>>1);
pcmCtrlDCCache &= ~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) {
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++) {
pcmCache[e]=pcmCache[e<<1];
}
pcmCache.resize(pcmCache.size()>>1);
pcmCtrlDCCache &= ~0x10; // clear stereo bit
}
}
}
}
// check to see if the most recent received blob matches any of the previous data
// and reuse it if there is a match, otherwise append the cache to the rest of
// the PCM data
std::vector<unsigned char>::iterator it;
it=std::search(pcmData.begin(),pcmData.end(),pcmCache.begin(),pcmCache.end());
pcmOff=std::distance(pcmData.begin(),it);
pcmLen=pcmCache.size();
logD("ZSM: pcmOff: %d pcmLen: %d",pcmOff,pcmLen);
if (it==pcmData.end()) {
pcmData.insert(pcmData.end(),pcmCache.begin(),pcmCache.end());
}
pcmCache.clear();
extCmdLen+=2;
// search for a matching PCM instrument definition
for (S_pcmInst& inst: pcmInsts) {
if (inst.offset==pcmOff && inst.length==pcmLen && inst.geometry==pcmCtrlDCCache)
break;
pcmInst++;
}
if (pcmInst==pcmInsts.size()) {
S_pcmInst inst;
inst.geometry=pcmCtrlDCCache;
inst.offset=pcmOff;
inst.length=pcmLen;
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;
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
for (DivRegWrite& write: pcmMeta) {
w->writeC(write.addr);
w->writeC(write.val);
}
pcmMeta.clear();
if (pcmLen) {
w->writeC(0x02); // 0x02 = Instrument trigger
w->writeC((unsigned char)pcmInst&0xff);
}
}
numWrites=0;
}

View file

@ -38,16 +38,26 @@ enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
struct S_pcmInst {
int geometry, offset, length;
};
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
int pcmRateCache;
int pcmCtrlRVCache;
int pcmCtrlDCCache;
std::vector<DivRegWrite> ymwrites;
std::vector<DivRegWrite> pcmMeta;
std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask = 0;
int psgMask = 0;
int ymMask;
int psgMask;
public:
DivZSM();
~DivZSM();

View file

@ -150,7 +150,10 @@ SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop) {
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);
if (i==VERA) {
if (done && write.addr >= 64) continue; // don't process any PCM events on the loop lookahead
zsm.writePSG(write.addr&0xff,write.val);
}
}
writes.clear();
}
@ -160,7 +163,7 @@ SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop) {
fracWait+=cycles&MASTER_CLOCK_MASK;
totalWait+=fracWait>>MASTER_CLOCK_PREC;
fracWait&=MASTER_CLOCK_MASK;
if (totalWait>0) {
if (totalWait>0 && !done) {
zsm.tick(totalWait);
//tickCount+=totalWait;
}