mirror of
https://github.com/tildearrow/furnace.git
synced 2024-11-22 20:45:11 +00:00
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:
parent
a3a8dd7f0d
commit
93097b40e5
5 changed files with 272 additions and 17 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue