furnace/src/engine/zsm.cpp

407 lines
13 KiB
C++
Raw Normal View History

2022-05-26 05:24:21 +00:00
/**
* Furnace Tracker - multi-system chiptune tracker
2023-01-20 00:18:40 +00:00
* Copyright (C) 2021-2023 tildearrow and contributors
2022-05-26 05:24:21 +00:00
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "zsm.h"
#include "../ta-log.h"
#include "../utfutils.h"
#include "song.h"
DivZSM::DivZSM() {
2023-06-06 04:24:34 +00:00
w=NULL;
2022-05-26 05:24:21 +00:00
init();
}
DivZSM::~DivZSM() {
2022-05-26 05:24:21 +00:00
}
void DivZSM::init(unsigned int rate) {
2023-06-06 04:24:34 +00:00
if (w!=NULL) delete w;
w=new SafeWriter;
2022-05-26 05:24:21 +00:00
w->init();
// write default ZSM data header
w->write("zm",2); // magic header
w->writeC(ZSM_VERSION);
2022-05-26 05:24:21 +00:00
// no loop offset
w->writeS(0);
w->writeC(0);
// no PCM
w->writeS(0x00);
w->writeC(0x00);
// FM channel mask
w->writeC(0x00);
// PSG channel mask
w->writeS(0x00);
w->writeS((unsigned short)rate);
// 2 reserved bytes (set to zero)
w->writeS(0x00);
2023-06-06 04:24:34 +00:00
tickRate=rate;
2022-05-26 05:24:21 +00:00
loopOffset=-1;
numWrites=0;
ticks=0;
// Initialize YM/PSG states
2022-05-26 05:24:21 +00:00
memset(&ymState,-1,sizeof(ymState));
memset(&psgState,-1,sizeof(psgState));
// Initialize PCM states
pcmRateCache=-1;
pcmCtrlRVCache=-1;
pcmCtrlDCCache=-1;
// Channel masks
ymMask=0;
psgMask=0;
2022-05-26 05:24:21 +00:00
}
int DivZSM::getoffset() {
2022-05-26 05:24:21 +00:00
return w->tell();
}
void DivZSM::writeYM(unsigned char a, unsigned char v) {
2023-06-06 04:24:34 +00:00
int lastMask=ymMask;
2022-05-26 05:24:21 +00:00
if (a==0x19 && v>=0x80) a=0x1a; // AMD/PSD use same reg addr. store PMD as 0x1a
2023-06-06 04:24:34 +00:00
if (a==0x08 && (v&0xf8)) ymMask|=(1<<(v&0x07)); // mark chan as in-use if keyDN
if (a!=0x08) ymState[ym_NEW][a]=v; // cache the newly-written value
2022-05-26 05:24:21 +00:00
bool writeit=false; // used to suppress spurious writes to unused channels
2023-06-06 04:24:34 +00:00
if (a<0x20) {
if (a==0x08) {
2022-05-26 05:24:21 +00:00
// write keyUPDN messages if channel is active.
2023-06-06 04:24:34 +00:00
writeit=(ymMask&(1<<(v&0x07)))>0;
} else {
2022-05-26 05:24:21 +00:00
// do not suppress global registers
2023-06-06 04:24:34 +00:00
writeit=true;
2022-05-26 05:24:21 +00:00
}
} else {
2023-06-06 04:24:34 +00:00
writeit=(ymMask&(1<<(a&0x07)))>0; // a&0x07 = chan ID for regs >=0x20
2022-05-26 05:24:21 +00:00
}
2023-06-06 04:24:34 +00:00
if (lastMask!=ymMask) {
2022-05-26 05:24:21 +00:00
// if the ymMask just changed, then the channel has become active.
2023-06-06 04:24:34 +00:00
// This can only happen on a KeyDN event, so voice=v&0x07
2022-05-26 05:24:21 +00:00
// insert a keyUP just to be safe.
ymwrites.push_back(DivRegWrite(0x08,v&0x07));
numWrites++;
// flush the ym_NEW cached states for this channel into the ZSM....
2023-06-06 04:24:34 +00:00
for (int i=0x20+(v&0x07); i<=0xff; i+=8) {
if (ymState[ym_NEW][i]!=ymState[ym_PREV][i]) {
2022-05-26 05:24:21 +00:00
ymwrites.push_back(DivRegWrite(i,ymState[ym_NEW][i]));
numWrites++;
// ...and update the shadow
2023-06-06 04:24:34 +00:00
ymState[ym_PREV][i]=ymState[ym_NEW][i];
2022-05-26 05:24:21 +00:00
}
}
}
// Handle the current write if channel is active
2023-06-06 04:24:34 +00:00
if (writeit && ((ymState[ym_NEW][a]!=ymState[ym_PREV][a]) || a==0x08)) {
2022-05-26 05:24:21 +00:00
// update YM shadow if not the KeyUPDN register.
2023-06-06 04:24:34 +00:00
if (a!=8) ymState[ym_PREV][a]=ymState[ym_NEW][a];
// if reg=PMD, then change back to real register 0x19
2022-05-26 05:24:21 +00:00
if (a==0x1a) a=0x19;
ymwrites.push_back(DivRegWrite(a,v));
numWrites++;
}
}
void DivZSM::writePSG(unsigned char a, unsigned char v) {
2022-05-26 05:24:21 +00:00
// 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>=69) {
logD("ZSM: ignoring VERA PSG write a=%02x v=%02x",a,v);
2022-09-24 07:31:10 +00:00
return;
} else if (a==68) {
// Sync event
numWrites++;
return syncCache.push_back(v);
} else if (a>=64) {
return writePCM(a-64,v);
2022-05-26 05:24:21 +00:00
}
2023-06-06 04:24:34 +00:00
if (psgState[psg_PREV][a]==v) {
if (psgState[psg_NEW][a]!=v) {
2022-05-26 05:24:21 +00:00
// NEW value is being reset to the same as PREV value
// so it is no longer a new write.
2022-09-24 07:31:10 +00:00
numWrites--;
2023-06-06 04:24:34 +00:00
}
2022-05-26 05:24:21 +00:00
} else {
2023-06-06 04:24:34 +00:00
if (psgState[psg_PREV][a]==psgState[psg_NEW][a]) {
2022-05-26 05:24:21 +00:00
// if this write changes the NEW cached value to something other
// than the PREV value, then this is a new write.
numWrites++;
2023-06-06 04:24:34 +00:00
}
2022-05-26 05:24:21 +00:00
}
2023-06-06 04:24:34 +00:00
psgState[psg_NEW][a]=v;
// mark channel as used in the psgMask if volume is set>0.
if ((a%4==2) && (v&0x3f)) psgMask|=(1<<(a>>2));
2022-05-26 05:24:21 +00:00
}
void DivZSM::writePCM(unsigned char a, unsigned char v) {
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++;
}
2022-05-26 05:24:21 +00:00
}
void DivZSM::tick(int numticks) {
2022-05-26 05:24:21 +00:00
flushWrites();
2023-06-06 04:24:34 +00:00
ticks+=numticks;
2022-05-26 05:24:21 +00:00
}
void DivZSM::setLoopPoint() {
2022-05-26 05:24:21 +00:00
tick(0); // flush any ticks+writes
flushTicks(); // flush ticks incase no writes were pending
logI("ZSM: loop at file offset %d bytes",w->tell());
loopOffset=w->tell();
2022-09-24 07:31:10 +00:00
// update the ZSM header's loop offset value
2022-05-26 05:24:21 +00:00
w->seek(0x03,SEEK_SET);
w->writeS((short)(loopOffset&0xffff));
w->writeC((unsigned char)((loopOffset>>16)&0xff));
2022-05-26 05:24:21 +00:00
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;
2022-05-26 05:24:21 +00:00
// reset the YM shadow....
memset(&ymState[ym_PREV],-1,sizeof(ymState[ym_PREV]));
// ... and cache (except for unused channels)
memset(&ymState[ym_NEW],-1,0x20);
2023-06-06 04:24:34 +00:00
for (int chan=0; chan<8; chan++) {
2022-09-24 07:31:10 +00:00
// do not clear state for as-yet-unused channels
2023-06-06 04:24:34 +00:00
if (!(ymMask&(1<<chan))) continue;
2022-05-26 05:24:21 +00:00
// clear the state for channels in use so they match the unknown state
// of the YM shadow.
2023-06-06 04:24:34 +00:00
for (int i=0x20+chan; i<=0xff; i+=8) {
ymState[ym_NEW][i]=-1;
}
2022-05-26 05:24:21 +00:00
}
}
SafeWriter* DivZSM::finish() {
2022-05-26 05:24:21 +00:00
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);
}
2022-05-26 05:24:21 +00:00
// update channel use masks.
w->seek(0x09,SEEK_SET);
2023-06-06 04:24:34 +00:00
w->writeC((unsigned char)(ymMask&0xff));
w->writeS((short)(psgMask&0xffff));
2022-05-26 05:24:21 +00:00
return w;
}
void DivZSM::flushWrites() {
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());
2022-05-26 05:24:21 +00:00
if (numWrites==0) return;
flushTicks(); // only flush ticks if there are writes pending.
2023-06-06 04:24:34 +00:00
for (unsigned char i=0; i<64; i++) {
if (psgState[psg_NEW][i]==psgState[psg_PREV][i]) continue;
2022-05-26 05:24:21 +00:00
psgState[psg_PREV][i]=psgState[psg_NEW][i];
w->writeC(i);
w->writeC(psgState[psg_NEW][i]);
2022-05-26 05:24:21 +00:00
}
2023-06-06 04:24:34 +00:00
int n=0; // n=completed YM writes. used to determine when to write the CMD byte...
2022-05-26 05:24:21 +00:00
for (DivRegWrite& write: ymwrites) {
2023-06-06 04:24:34 +00:00
if (n%ZSM_YM_MAX_WRITES==0) {
if (ymwrites.size()-n>ZSM_YM_MAX_WRITES) {
2022-09-24 07:31:10 +00:00
w->writeC((unsigned char)(ZSM_YM_CMD+ZSM_YM_MAX_WRITES));
logD("ZSM: YM-write: %d (%02x) [max]",ZSM_YM_MAX_WRITES,ZSM_YM_MAX_WRITES+ZSM_YM_CMD);
2022-05-26 05:24:21 +00:00
} else {
2022-09-24 07:31:10 +00:00
w->writeC((unsigned char)(ZSM_YM_CMD+ymwrites.size()-n));
logD("ZSM: YM-write: %d (%02x)",ymwrites.size()-n,ZSM_YM_CMD+ymwrites.size()-n);
2022-05-26 05:24:21 +00:00
}
}
n++;
w->writeC(write.addr);
w->writeC(write.val);
2022-05-26 05:24:21 +00:00
}
ymwrites.clear();
unsigned int pcmInst=0;
int pcmOff=0;
int pcmLen=0;
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
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&=(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) {
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&=(unsigned char)~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();
extCmd0Len+=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 (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 (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);
}
pcmMeta.clear();
if (pcmLen) {
w->writeC(0x02); // 0x02 = Instrument trigger
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();
2022-05-26 05:24:21 +00:00
numWrites=0;
}
void DivZSM::flushTicks() {
2023-06-06 04:24:34 +00:00
while (ticks>ZSM_DELAY_MAX) {
2022-09-24 07:31:10 +00:00
logD("ZSM: write delay %d (max)",ZSM_DELAY_MAX);
w->writeC((unsigned char)(ZSM_DELAY_CMD+ZSM_DELAY_MAX));
2023-06-06 04:24:34 +00:00
ticks-=ZSM_DELAY_MAX;
2022-05-26 05:24:21 +00:00
}
if (ticks>0) {
2022-09-24 07:31:10 +00:00
logD("ZSM: write delay %d",ticks);
w->writeC(ZSM_DELAY_CMD+ticks);
2022-05-26 05:24:21 +00:00
}
ticks=0;
}