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

@ -2,9 +2,11 @@
#### Zsound Repo
ZSM is part of the Zsound suite of Commander X16 audio tools found at:</br>
ZSM is part of the Zsound suite of Commander X16 audio tools found at:
https://github.com/ZeroByteOrg/zsound/
An alternative library with PCM support, ZSMKit, is avalable at:
https://github.com/mooinglemur/ZSMKit
#### Current ZSM Revision: 1
@ -69,14 +71,35 @@ The EXTCMD byte is formatted as `ccnnnnnn` where `c`=channel and `n`=number of b
### 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.
If the PCM header exists in the ZSM file, it will immediately follow the 0x80 end-of-data marker. The PCM header exists only if at least one PCM instrument exists.
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.
Since each instrument defined is 16 bytes, the size of the PCM header can be calculated
as 4+(16*(last_instrument_index+1)).
Offset|Type|Value
--|--|--
0x00-0x02|String|"PCM"
0x03|Byte|The last PCM instrument index
0x04-0x13|Mixed|Instrument definition for instrument 0x00
0x14-0x23|Mixed|(optional) Instrument definition for instrument 0x01
...
### Instrument definition
Offset|Type|Value
--|--|--
0x00|Byte|This instrument's index
0x01|Bitmask|AUDIO_CTRL: 00**DC**0000: D is set for 16-bit, and clear for 8-bit. C is set for stereo and clear for mono
0x02-0x04|24-bit int|Little-endian offset into the PCM data block
0x05-0x07|24-bit int|Little-endian length of PCM data
0x08|Bitmask|Features: **L**xxxxxxx: L is set if the sample is looped
0x09-0x0b|24-bit int|Little-endian loop point offset (relative, 0 is the beginning of this instrument's sample)
0x0c-0x0f|...|Reserved for expansion
Any offset values contained in the PCM data header block are relative to the beginning of the PCM sample data section, not to the PCM header or 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.
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.
## EXTCMD Channel Scifications
@ -108,7 +131,13 @@ The Custom channel data may take whatever format is desired for any particular p
### EXTCMD Channel:
#### 0: PCM audio
The structure of data within this channel is not yet defined.
This EXTCMD stream can contain one or more command + argument pairs.
command|meaning|argument|description
---|---|---|---
0x00|AUDIO_CTRL byte|byte|This byte sets PCM channel volume and/or clears the FIFO
0x01|AUDIO_RATE byte|byte|A value from 0x00-0x80 to set the sample rate (playback speed)
0x02|Instrument trigger|byte|Triggers the PCM instrument specified by this byte index
#### 1: Expansion Sound Devices

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