diff --git a/papers/zsm-format.md b/papers/zsm-format.md index 73928125..5957e3c6 100644 --- a/papers/zsm-format.md +++ b/papers/zsm-format.md @@ -2,9 +2,11 @@ #### Zsound Repo -ZSM is part of the Zsound suite of Commander X16 audio tools found at:
+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 diff --git a/src/engine/platform/vera.cpp b/src/engine/platform/vera.cpp index 0db7660d..3e3ac42c 100644 --- a/src/engine/platform/vera.cpp +++ b/src/engine/platform/vera.cpp @@ -19,6 +19,7 @@ #include "vera.h" #include "../engine.h" +#include "../../ta-log.h" #include #include @@ -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) { diff --git a/src/engine/zsm.cpp b/src/engine/zsm.cpp index b65bbcbb..912dc630 100644 --- a/src/engine/zsm.cpp +++ b/src/engine/zsm.cpp @@ -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: + // + // + // of PCM data offset + // 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>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>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::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; } diff --git a/src/engine/zsm.h b/src/engine/zsm.h index b452171f..17a1fd06 100644 --- a/src/engine/zsm.h +++ b/src/engine/zsm.h @@ -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 ymwrites; + std::vector pcmMeta; + std::vector pcmData; + std::vector pcmCache; + std::vector pcmInsts; int loopOffset; int numWrites; int ticks; int tickRate; - int ymMask = 0; - int psgMask = 0; + int ymMask; + int psgMask; public: DivZSM(); ~DivZSM(); diff --git a/src/engine/zsmOps.cpp b/src/engine/zsmOps.cpp index deddd4ec..c59efeae 100644 --- a/src/engine/zsmOps.cpp +++ b/src/engine/zsmOps.cpp @@ -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; }