Merge branch 'master' of https://github.com/tildearrow/furnace into ymf278b

This commit is contained in:
cam900 2024-08-25 12:50:51 +09:00
commit 3e1e2fc2a6
73 changed files with 1657 additions and 663 deletions

View file

@ -215,6 +215,10 @@ bool DivCSPlayer::tick() {
case DIV_CMD_HINT_VOL_SLIDE:
arg0=(short)stream.readS();
break;
case DIV_CMD_HINT_VOL_SLIDE_TARGET:
arg0=(short)stream.readS();
arg1=(short)stream.readS();
break;
case DIV_CMD_HINT_LEGATO:
arg0=(unsigned char)stream.readC();
if (arg0==0xff) {
@ -321,6 +325,11 @@ bool DivCSPlayer::tick() {
break;
case DIV_CMD_HINT_VOL_SLIDE:
chan[i].volSpeed=arg0;
chan[i].volSpeedTarget=-1;
break;
case DIV_CMD_HINT_VOL_SLIDE_TARGET:
chan[i].volSpeed=arg0;
chan[i].volSpeedTarget=arg0==0 ? -1 : arg1;
break;
case DIV_CMD_HINT_PITCH:
chan[i].pitch=arg0;
@ -356,13 +365,29 @@ bool DivCSPlayer::tick() {
if (sendVolume || chan[i].volSpeed!=0) {
chan[i].volume+=chan[i].volSpeed;
if (chan[i].volSpeedTarget!=-1) {
bool atTarget=false;
if (chan[i].volSpeed>0) {
atTarget=(chan[i].volume>=chan[i].volSpeedTarget);
} else if (chan[i].volSpeed<0) {
atTarget=(chan[i].volume<=chan[i].volSpeedTarget);
} else {
atTarget=true;
chan[i].volSpeedTarget=chan[i].volume;
}
if (atTarget) {
chan[i].volume=chan[i].volSpeedTarget;
chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
}
}
if (chan[i].volume<0) {
chan[i].volume=0;
}
if (chan[i].volume>chan[i].volMax) {
chan[i].volume=chan[i].volMax;
}
e->dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
}

View file

@ -34,7 +34,7 @@ struct DivCSChannelState {
int lastWaitLen;
int note, pitch;
int volume, volMax, volSpeed;
int volume, volMax, volSpeed, volSpeedTarget;
int vibratoDepth, vibratoRate, vibratoPos;
int portaTarget, portaSpeed;
unsigned char arp, arpStage, arpTicks;
@ -56,6 +56,7 @@ struct DivCSChannelState {
volume(0x7f00),
volMax(0),
volSpeed(0),
volSpeedTarget(-1),
vibratoDepth(0),
vibratoRate(0),
vibratoPos(0),

View file

@ -59,6 +59,7 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) {
case DIV_CMD_HINT_VOLUME:
case DIV_CMD_HINT_PORTA:
case DIV_CMD_HINT_VOL_SLIDE:
case DIV_CMD_HINT_VOL_SLIDE_TARGET:
case DIV_CMD_HINT_LEGATO:
w->writeC((unsigned char)c.cmd+0xb4);
break;
@ -100,6 +101,10 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) {
case DIV_CMD_HINT_VOL_SLIDE:
w->writeS(c.value);
break;
case DIV_CMD_HINT_VOL_SLIDE_TARGET:
w->writeS(c.value);
w->writeS(c.value2);
break;
case DIV_CMD_SAMPLE_MODE:
case DIV_CMD_SAMPLE_FREQ:
case DIV_CMD_SAMPLE_BANK:

View file

@ -67,6 +67,7 @@ enum DivDispatchCmds {
DIV_CMD_HINT_ARPEGGIO, // (note1, note2)
DIV_CMD_HINT_VOLUME, // (vol)
DIV_CMD_HINT_VOL_SLIDE, // (amount, oneTick)
DIV_CMD_HINT_VOL_SLIDE_TARGET, // (amount, target)
DIV_CMD_HINT_PORTA, // (target, speed)
DIV_CMD_HINT_LEGATO, // (note)
@ -265,6 +266,8 @@ enum DivDispatchCmds {
DIV_CMD_FDS_MOD_AUTO,
DIV_CMD_FM_OPMASK, // (mask)
DIV_CMD_MULTIPCM_MIX_FM, // (value)
DIV_CMD_MULTIPCM_MIX_PCM, // (value)
DIV_CMD_MULTIPCM_LFO, // (value)

View file

@ -98,6 +98,10 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul
break;
case 0xc0: case 0xc1: case 0xc2: case 0xc3:
return _("Cxxx: Set tick rate (hz)");
case 0xd3:
return _("D3xx: Volume portamento");
case 0xd4:
return _("D4xx: Volume portamento (fast)");
case 0xdc:
return _("DCxx: Delayed mute");
case 0xe0:
@ -199,6 +203,12 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) {
}
}
void DivEngine::findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length) {
if (curSubSong!=NULL) {
curSubSong->findLength(loopOrder,loopRow,fadeoutLen,rowsForFadeout,hasFFxx,orders,song.grooves,length,chans,song.jumpTreatment,song.ignoreJumpAtEnd);
}
}
#define EXPORT_BUFSIZE 2048
double DivEngine::benchmarkPlayback() {

View file

@ -133,7 +133,7 @@ struct DivAudioExportOptions {
struct DivChannelState {
std::vector<DivDelayedCommand> delayed;
int note, oldNote, lastIns, pitch, portaSpeed, portaNote;
int volume, volSpeed, cut, volCut, legatoDelay, legatoTarget, rowDelay, volMax;
int volume, volSpeed, volSpeedTarget, cut, volCut, legatoDelay, legatoTarget, rowDelay, volMax;
int delayOrder, delayRow, retrigSpeed, retrigTick;
int vibratoDepth, vibratoRate, vibratoPos, vibratoPosGiant, vibratoShape, vibratoFine;
int tremoloDepth, tremoloRate, tremoloPos;
@ -157,6 +157,7 @@ struct DivChannelState {
portaNote(-1),
volume(0x7f00),
volSpeed(0),
volSpeedTarget(-1),
cut(-1),
volCut(-1),
legatoDelay(-1),
@ -474,7 +475,7 @@ class DivEngine {
int midiOutTimeRate;
float midiVolExp;
int softLockCount;
int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, nextSpeed, elapsedBars, elapsedBeats, curSpeed;
int subticks, ticks, curRow, curOrder, prevRow, prevOrder, remainingLoops, totalLoops, lastLoopPos, exportLoopCount, curExportChan, nextSpeed, elapsedBars, elapsedBeats, curSpeed;
size_t curSubSongIndex;
size_t bufferPos;
double divider;
@ -498,6 +499,7 @@ class DivEngine {
DivAudioExportModes exportMode;
DivAudioExportFormats exportFormat;
double exportFadeOut;
bool isFadingOut;
int exportOutputs;
bool exportChannelMask[DIV_MAX_CHANS];
DivConfig conf;
@ -579,7 +581,7 @@ class DivEngine {
void processRow(int i, bool afterDelay);
void nextOrder();
void nextRow();
void performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write, int streamOff, double* loopTimer, double* loopFreq, int* loopSample, bool* sampleDir, bool isSecond, int* pendingFreq, int* playingSample, int* setPos, unsigned int* sampleOff8, unsigned int* sampleLen8, size_t bankOffset, bool directStream);
void performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write, int streamOff, double* loopTimer, double* loopFreq, int* loopSample, bool* sampleDir, bool isSecond, int* pendingFreq, int* playingSample, int* setPos, unsigned int* sampleOff8, unsigned int* sampleLen8, size_t bankOffset, bool directStream, bool* sampleStoppable);
// returns true if end of song.
bool nextTick(bool noAccum=false, bool inhibitLowLat=false);
bool perSystemEffect(int ch, unsigned char effect, unsigned char effectVal);
@ -669,6 +671,7 @@ class DivEngine {
friend class DivROMExport;
friend class DivExportAmigaValidation;
friend class DivExportTiuna;
friend class DivExportZSM;
public:
DivSong song;
@ -721,8 +724,6 @@ class DivEngine {
// - -1 to auto-determine trailing
// - -2 to add a whole loop of trailing
SafeWriter* saveVGM(bool* sysToExport=NULL, bool loop=true, int version=0x171, bool patternHints=false, bool directStream=false, int trailingTicks=-1);
// dump to ZSM.
SafeWriter* saveZSM(unsigned int zsmrate=60, bool loop=true, bool optimize=true);
// dump to TIunA.
SafeWriter* saveTiuna(const bool* sysToExport, const char* baseLabel, int firstBankSize, int otherBankSize);
// dump command stream.
@ -817,6 +818,9 @@ class DivEngine {
// find song loop position
void walkSong(int& loopOrder, int& loopRow, int& loopEnd);
// find song length in rows (up to specified loop point), and find length of every order
void findSongLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, int& length);
// play (returns whether successful)
bool play();
@ -1008,6 +1012,24 @@ class DivEngine {
// is exporting
bool isExporting();
// get how many loops is left
void getLoopsLeft(int& loops);
// get how many loops in total export needs to do
void getTotalLoops(int& loops);
// get current position in song
void getCurSongPos(int& row, int& order);
// get how many files export needs to create
void getTotalAudioFiles(int& files);
// get which file is processed right now (progress for e.g. per-channel export)
void getCurFileIndex(int& file);
// get fadeout state
bool getIsFadingOut();
// add instrument
int addInstrument(int refChan=0, DivInstrumentType fallbackType=DIV_INS_STD);
@ -1401,6 +1423,7 @@ class DivEngine {
totalLoops(0),
lastLoopPos(0),
exportLoopCount(0),
curExportChan(0),
nextSpeed(3),
elapsedBars(0),
elapsedBeats(0),
@ -1439,6 +1462,7 @@ class DivEngine {
exportMode(DIV_EXPORT_MODE_ONE),
exportFormat(DIV_EXPORT_FORMAT_S16),
exportFadeOut(0.0),
isFadingOut(false),
exportOutputs(2),
cmdStreamInt(NULL),
midiBaseChan(0),

View file

@ -21,6 +21,7 @@
#include "export/amigaValidation.h"
#include "export/tiuna.h"
#include "export/zsm.h"
DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) {
DivROMExport* exporter=NULL;
@ -31,6 +32,9 @@ DivROMExport* DivEngine::buildROM(DivROMExportOptions sys) {
case DIV_ROM_TIUNA:
exporter=new DivExportTiuna;
break;
case DIV_ROM_ZSM:
exporter=new DivExportZSM;
break;
default:
exporter=new DivROMExport;
break;

View file

@ -18,9 +18,77 @@
*/
#include "zsm.h"
#include "../engine.h"
#include "../ta-log.h"
#include "../utfutils.h"
#include "song.h"
#include <fmt/printf.h>
/// DivZSM definitions
#define ZSM_HEADER_SIZE 16
#define ZSM_VERSION 1
#define ZSM_YM_CMD 0x40
#define ZSM_DELAY_CMD 0x80
#define ZSM_YM_MAX_WRITES 63
#define ZSM_SYNC_MAX_WRITES 31
#define ZSM_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD
#define ZSM_EXT ZSM_YM_CMD
#define ZSM_EXT_PCM 0x00
#define ZSM_EXT_CHIP 0x40
#define ZSM_EXT_SYNC 0x80
#define ZSM_EXT_CUSTOM 0xC0
enum YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
struct S_pcmInst {
int geometry;
unsigned int offset, length, loopPoint;
bool isLooped;
};
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
int pcmRateCache;
int pcmCtrlRVCache;
int pcmCtrlDCCache;
unsigned int pcmLoopPointCache;
bool pcmIsLooped;
std::vector<DivRegWrite> ymwrites;
std::vector<DivRegWrite> pcmMeta;
std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts;
std::vector<DivRegWrite> syncCache;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask;
int psgMask;
bool optimize;
public:
DivZSM();
~DivZSM();
void init(unsigned int rate = 60);
int getoffset();
void writeYM(unsigned char a, unsigned char v);
void writePSG(unsigned char a, unsigned char v);
void writePCM(unsigned char a, unsigned char v);
void writeSync(unsigned char a, unsigned char v);
void setOptimize(bool o);
void tick(int numticks = 1);
void setLoopPoint();
SafeWriter* finish();
private:
void flushWrites();
void flushTicks();
};
/// DivZSM implementation
DivZSM::DivZSM() {
w=NULL;
@ -447,3 +515,240 @@ void DivZSM::flushTicks() {
}
ticks=0;
}
/// ZSM export
constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0;
constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0;
void DivExportZSM::run() {
// settings
unsigned int zsmrate=conf.getInt("zsmrate",60);
bool loop=conf.getBool("loop",true);
bool optimize=conf.getBool("optimize",true);
// system IDs
int VERA=-1;
int YM=-1;
int IGNORED=0;
// find indexes for YM and VERA. Ignore other systems.
for (int i=0; i<e->song.systemLen; i++) {
switch (e->song.system[i]) {
case DIV_SYSTEM_VERA:
if (VERA>=0) {
IGNORED++;
break;
}
VERA=i;
logAppendf("VERA detected as chip id %d",i);
break;
case DIV_SYSTEM_YM2151:
if (YM>=0) {
IGNORED++;
break;
}
YM=i;
logAppendf("YM detected as chip id %d",i);
break;
default:
IGNORED++;
logAppendf("Ignoring chip %d systemID %d",i,(int)e->song.system[i]);
break;
}
}
if (VERA<0 && YM<0) {
logAppend("ERROR: No supported systems for ZSM");
failed=true;
running=false;
return;
}
if (IGNORED>0) {
logAppendf("ZSM export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' ');
}
DivZSM zsm;
e->stop();
e->repeatPattern=false;
e->setOrder(0);
e->synchronizedSoft([&]() {
double origRate=e->got.rate;
e->got.rate=zsmrate&0xffff;
// determine loop point
int loopOrder=0;
int loopRow=0;
int loopEnd=0;
e->walkSong(loopOrder,loopRow,loopEnd);
logAppendf("loop point: %d %d",loopOrder,loopRow);
zsm.init(zsmrate);
// reset the playback state
e->curOrder=0;
e->freelance=false;
e->playing=false;
e->extValuePresent=false;
e->remainingLoops=-1;
// Prepare to write song data
e->playSub(false);
//size_t tickCount=0;
bool done=false;
bool loopNow=false;
int loopPos=-1;
int fracWait=0; // accumulates fractional ticks
if (VERA>=0) e->disCont[VERA].dispatch->toggleRegisterDump(true);
if (YM>=0) {
e->disCont[YM].dispatch->toggleRegisterDump(true);
// emit LFO initialization commands
zsm.writeYM(0x18,0); // freq=0
zsm.writeYM(0x19,0x7F); // AMD =7F
zsm.writeYM(0x19,0xFF); // PMD =7F
// TODO: incorporate the Furnace meta-command for init data and filter
// out writes to otherwise-unused channels.
}
// Indicate the song's tuning as a sync meta-event
// specified in terms of how many 1/256th semitones
// the song is offset from standard A-440 tuning.
// This is mainly to benefit visualizations in players
// for non-standard tunings so that they can avoid
// displaying the entire song held in pitch bend.
// Tunings offsets that exceed a half semitone
// will simply be represented in a different key
// by nature of overflowing the signed char value
signed char tuningoffset=(signed char)(round(3072*(log(e->song.tuning/440.0)/log(2))))&0xff;
zsm.writeSync(0x01,tuningoffset);
// Set optimize flag, which mainly buffers PSG writes
// whenever the channel is silent
zsm.setOptimize(optimize);
while (!done) {
if (loopPos==-1) {
if (loopOrder==e->curOrder && loopRow==e->curRow && loop)
loopNow=true;
if (loopNow) {
// If Virtual Tempo is in use, our exact loop point
// might be skipped due to quantization error.
// If this happens, the tick immediately following is our loop point.
if (e->ticks==1 || !(loopOrder==e->curOrder && loopRow==e->curRow)) {
loopPos=zsm.getoffset();
zsm.setLoopPoint();
loopNow=false;
}
}
}
if (e->nextTick() || !e->playing) {
done=true;
if (!loop) {
for (int i=0; i<e->song.systemLen; i++) {
e->disCont[i].dispatch->getRegisterWrites().clear();
}
break;
}
if (!e->playing) {
loopPos=-1;
}
}
// get register dumps
for (int j=0; j<2; j++) {
int i=0;
// dump YM writes first
if (j==0) {
if (YM<0) {
continue;
} else {
i=YM;
}
}
// dump VERA writes second
if (j==1) {
if (VERA<0) {
continue;
} else {
i=VERA;
}
}
std::vector<DivRegWrite>& writes=e->disCont[i].dispatch->getRegisterWrites();
if (writes.size()>0)
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) {
if (done && write.addr>=64) continue; // don't process any PCM or sync events on the loop lookahead
zsm.writePSG(write.addr&0xff,write.val);
}
}
writes.clear();
}
// write wait
int totalWait=e->cycles>>MASTER_CLOCK_PREC;
fracWait+=e->cycles&MASTER_CLOCK_MASK;
totalWait+=fracWait>>MASTER_CLOCK_PREC;
fracWait&=MASTER_CLOCK_MASK;
if (totalWait>0 && !done) {
zsm.tick(totalWait);
//tickCount+=totalWait;
}
}
// end of song
// done - close out.
e->got.rate=origRate;
if (VERA>=0) e->disCont[VERA].dispatch->toggleRegisterDump(false);
if (YM>=0) e->disCont[YM].dispatch->toggleRegisterDump(false);
e->remainingLoops=-1;
e->playing=false;
e->freelance=false;
e->extValuePresent=false;
});
progress[0].amount=1.0f;
logAppend("finished!");
output.push_back(DivROMExportOutput("out.zsm",zsm.finish()));
running=false;
}
/// DivExpottZSM - FRONTEND
bool DivExportZSM::go(DivEngine* eng) {
progress[0].name="Generate";
progress[0].amount=0.0f;
e=eng;
running=true;
failed=false;
mustAbort=false;
exportThread=new std::thread(&DivExportZSM::run,this);
return true;
}
void DivExportZSM::wait() {
if (exportThread!=NULL) {
exportThread->join();
delete exportThread;
}
}
void DivExportZSM::abort() {
mustAbort=true;
wait();
}
bool DivExportZSM::isRunning() {
return running;
}
bool DivExportZSM::hasFailed() {
return failed;
}
DivROMExportProgress DivExportZSM::getProgress(int index) {
if (index<0 || index>1) return progress[1];
return progress[index];
}

38
src/engine/export/zsm.h Normal file
View file

@ -0,0 +1,38 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2024 tildearrow and contributors
*
* 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 "../export.h"
#include <thread>
class DivExportZSM: public DivROMExport {
DivEngine* e;
std::thread* exportThread;
DivROMExportProgress progress[2];
bool running, failed, mustAbort;
void run();
public:
bool go(DivEngine* e);
bool isRunning();
bool hasFailed();
void abort();
void wait();
DivROMExportProgress getProgress(int index=0);
~DivExportZSM() {}
};

View file

@ -401,8 +401,8 @@ std::vector<DivSample*> DivEngine::sampleFromFile(const char* path) {
sample->loop=false;
}
if (sample->centerRate<4000) sample->centerRate=4000;
if (sample->centerRate>64000) sample->centerRate=64000;
if (sample->centerRate<100) sample->centerRate=100;
if (sample->centerRate>384000) sample->centerRate=384000;
sfWrap.doClose();
BUSY_END;
ret.push_back(sample);

View file

@ -367,6 +367,141 @@ void DivInstrument::writeFeatureFM(SafeWriter* w, bool fui) {
FEATURE_END;
}
bool MemPatch::calcDiff(const void* pre, const void* post, size_t inputSize) {
bool diffValid=false;
size_t firstDiff=0;
size_t lastDiff=0;
const unsigned char* preBytes=(const unsigned char*)pre;
const unsigned char* postBytes=(const unsigned char*)post;
// @NOTE: consider/profile using a memcmp==0 check to early-out, if it's potentially faster
// for the common case, which is no change
for (size_t ii=0; ii<inputSize; ++ii) {
if (preBytes[ii] != postBytes[ii]) {
lastDiff=ii;
firstDiff=diffValid ? firstDiff : ii;
diffValid=true;
}
}
if (diffValid) {
offset=firstDiff;
size=lastDiff - firstDiff + 1;
data=new unsigned char[size];
// the diff is to make pre into post (MemPatch is general, not specific to
// undo), so copy from postBytes
memcpy(data, postBytes+offset, size);
}
return diffValid;
}
void MemPatch::applyAndReverse(void* target, size_t targetSize) {
if (size==0) return;
if (offset+size>targetSize) {
logW("MemPatch (offset %d, size %d) exceeds target size (%d), can't apply!",offset,size,targetSize);
return;
}
unsigned char* targetBytes=(unsigned char*)target;
// swap this->data and its segment on target
for (size_t ii=0; ii<size; ++ii) {
unsigned char tmp=targetBytes[offset+ii];
targetBytes[offset+ii] = data[ii];
data[ii] = tmp;
}
}
void DivInstrumentUndoStep::applyAndReverse(DivInstrument* target) {
if (nameValid) {
name.swap(target->name);
}
podPatch.applyAndReverse((DivInstrumentPOD*)target, sizeof(DivInstrumentPOD));
}
bool DivInstrumentUndoStep::makeUndoPatch(size_t processTime_, const DivInstrument* pre, const DivInstrument* post) {
processTime=processTime_;
// create the patch that will make post into pre
podPatch.calcDiff((const DivInstrumentPOD*)post, (const DivInstrumentPOD*)pre, sizeof(DivInstrumentPOD));
if (pre->name!=post->name) {
nameValid=true;
name=pre->name;
}
return nameValid || podPatch.isValid();
}
bool DivInstrument::recordUndoStepIfChanged(size_t processTime, const DivInstrument* old) {
DivInstrumentUndoStep step;
// generate a patch to go back to old
if (step.makeUndoPatch(processTime, old, this)) {
// make room
if (undoHist.size()>=undoHist.capacity()) {
delete undoHist.front();
undoHist.pop_front();
}
// clear redo
while (!redoHist.empty()) {
delete redoHist.back();
redoHist.pop_back();
}
DivInstrumentUndoStep* stepPtr=new DivInstrumentUndoStep;
*stepPtr=step;
step.podPatch.data=NULL; // don't let it delete the data ptr that's been copied!
undoHist.push_back(stepPtr);
// logI("DivInstrument::undoHist push (%u off, %u size)", stepPtr->podPatch.offset, stepPtr->podPatch.size);
return true;
}
return false;
}
int DivInstrument::undo() {
if (undoHist.empty()) return 0;
DivInstrumentUndoStep* step=undoHist.back();
undoHist.pop_back();
// logI("DivInstrument::undo (%u off, %u size)", step->podPatch.offset, step->podPatch.size);
step->applyAndReverse(this);
// make room
if (redoHist.size()>=redoHist.capacity()) {
DivInstrumentUndoStep* step=redoHist.front();
delete step;
redoHist.pop_front();
}
redoHist.push_back(step);
return 1;
}
int DivInstrument::redo() {
if (redoHist.empty()) return 0;
DivInstrumentUndoStep* step = redoHist.back();
redoHist.pop_back();
// logI("DivInstrument::redo (%u off, %u size)", step->podPatch.offset, step->podPatch.size);
step->applyAndReverse(this);
// make room
if (undoHist.size()>=undoHist.capacity()) {
DivInstrumentUndoStep* step=undoHist.front();
delete step;
undoHist.pop_front();
}
undoHist.push_back(step);
return 1;
}
void DivInstrument::writeMacro(SafeWriter* w, const DivInstrumentMacro& m) {
if (!m.len) return;
@ -3345,3 +3480,28 @@ bool DivInstrument::saveDMP(const char* path) {
w->finish();
return true;
}
DivInstrument::~DivInstrument() {
// free undoHist/redoHist
while (!undoHist.empty()) {
delete undoHist.back();
undoHist.pop_back();
}
while (!redoHist.empty()) {
delete redoHist.back();
redoHist.pop_back();
}
}
DivInstrument::DivInstrument( const DivInstrument& ins ) {
// undo/redo history is specifically not copied
*(DivInstrumentPOD*)this=ins;
name=ins.name;
}
DivInstrument& DivInstrument::operator=( const DivInstrument& ins ) {
// undo/redo history is specifically not copied
*(DivInstrumentPOD*)this=ins;
name=ins.name;
return *this;
}

View file

@ -23,8 +23,10 @@
#include "dataErrors.h"
#include "../ta-utils.h"
#include "../pch.h"
#include "../fixedQueue.h"
struct DivSong;
struct DivInstrument;
// NOTICE!
// before adding new instrument types to this struct, please ask me first.
@ -865,8 +867,7 @@ struct DivInstrumentSID2 {
noiseMode(0) {}
};
struct DivInstrument {
String name;
struct DivInstrumentPOD {
DivInstrumentType type;
DivInstrumentFM fm;
DivInstrumentSTD std;
@ -885,6 +886,77 @@ struct DivInstrument {
DivInstrumentPowerNoise powernoise;
DivInstrumentSID2 sid2;
DivInstrumentPOD() :
type(DIV_INS_FM) {
}
};
struct MemPatch {
MemPatch() :
data(NULL)
, offset(0)
, size(0) {
}
~MemPatch() {
if (data) {
delete[] data;
data=NULL;
}
}
bool calcDiff(const void* pre, const void* post, size_t size);
void applyAndReverse(void* target, size_t inputSize);
bool isValid() const { return size>0; }
unsigned char* data;
size_t offset;
size_t size;
};
struct DivInstrumentUndoStep {
DivInstrumentUndoStep() :
name(""),
nameValid(false),
processTime(0) {
}
MemPatch podPatch;
String name;
bool nameValid;
size_t processTime;
void applyAndReverse(DivInstrument* target);
bool makeUndoPatch(size_t processTime_, const DivInstrument* pre, const DivInstrument* post);
};
struct DivInstrument : DivInstrumentPOD {
String name;
DivInstrument() :
name("") {
// clear and construct DivInstrumentPOD so it doesn't have any garbage in the padding
memset((unsigned char*)(DivInstrumentPOD*)this, 0, sizeof(DivInstrumentPOD));
new ((DivInstrumentPOD*)this) DivInstrumentPOD;
}
~DivInstrument();
/**
* copy/assignment to specifically avoid leaking or dangling pointers to undo step
*/
DivInstrument( const DivInstrument& ins );
DivInstrument& operator=( const DivInstrument& ins );
/**
* undo stuff
*/
FixedQueue<DivInstrumentUndoStep*, 128> undoHist;
FixedQueue<DivInstrumentUndoStep*, 128> redoHist;
bool recordUndoStepIfChanged(size_t processTime, const DivInstrument* old);
int undo();
int redo();
/**
* these are internal functions.
*/
@ -969,9 +1041,5 @@ struct DivInstrument {
* @return whether it was successful.
*/
bool saveDMP(const char* path);
DivInstrument():
name(""),
type(DIV_INS_FM) {
}
};
#endif

View file

@ -761,6 +761,12 @@ int DivPlatformArcade::dispatch(DivCommand c) {
immWrite(0x19,0x80|pmDepth);
break;
}
case DIV_CMD_FM_OPMASK:
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -423,11 +423,11 @@ void DivPlatformAY8910::tick(bool sysTick) {
chan[i].tfx.counter = 0;
chan[i].tfx.out = 0;
if (chan[i].nextPSGMode.val&8) {
if (dumpWrites) addWrite(0xffff0002+(i<<8),0);
//if (dumpWrites) addWrite(0xffff0002+(i<<8),0);
if (chan[i].dac.sample<0 || chan[i].dac.sample>=parent->song.sampleLen) {
if (dumpWrites) {
rWrite(0x08+i,0);
addWrite(0xffff0000+(i<<8),chan[i].dac.sample);
//addWrite(0xffff0000+(i<<8),chan[i].dac.sample);
}
if (chan[i].dac.setPos) {
chan[i].dac.setPos=false;
@ -517,7 +517,7 @@ void DivPlatformAY8910::tick(bool sysTick) {
}
}
chan[i].dac.rate=((double)rate*((sunsoft||clockSel)?8.0:16.0))/(double)(MAX(1,off*chan[i].freq));
if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dac.rate);
//if (dumpWrites) addWrite(0xffff0001+(i<<8),chan[i].dac.rate);
}
if (chan[i].freq<0) chan[i].freq=0;
if (chan[i].freq>4095) chan[i].freq=4095;
@ -604,12 +604,12 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
}
if (chan[c.chan].dac.sample<0 || chan[c.chan].dac.sample>=parent->song.sampleLen) {
chan[c.chan].dac.sample=-1;
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
//if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
break;
} else {
if (dumpWrites) {
rWrite(0x08+c.chan,0);
addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
//addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
}
}
if (chan[c.chan].dac.setPos) {
@ -637,10 +637,10 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
chan[c.chan].dac.sample=12*sampleBank+chan[c.chan].note%12;
if (chan[c.chan].dac.sample>=parent->song.sampleLen) {
chan[c.chan].dac.sample=-1;
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
//if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
break;
} else {
if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
//if (dumpWrites) addWrite(0xffff0000+(c.chan<<8),chan[c.chan].dac.sample);
}
if (chan[c.chan].dac.setPos) {
chan[c.chan].dac.setPos=false;
@ -651,7 +651,7 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
chan[c.chan].dac.rate=parent->getSample(chan[c.chan].dac.sample)->rate*2048;
if (dumpWrites) {
rWrite(0x08+c.chan,0);
addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate);
//addWrite(0xffff0001+(c.chan<<8),chan[c.chan].dac.rate);
}
chan[c.chan].dac.furnaceDAC=false;
}
@ -686,7 +686,7 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
}
case DIV_CMD_NOTE_OFF:
chan[c.chan].dac.sample=-1;
if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
//if (dumpWrites) addWrite(0xffff0002+(c.chan<<8),0);
chan[c.chan].nextPSGMode.val&=~8;
chan[c.chan].keyOff=true;
chan[c.chan].active=false;
@ -867,7 +867,7 @@ int DivPlatformAY8910::dispatch(DivCommand c) {
case DIV_CMD_SAMPLE_POS:
chan[c.chan].dac.pos=c.value;
chan[c.chan].dac.setPos=true;
if (dumpWrites) addWrite(0xffff0005,chan[c.chan].dac.pos);
//if (dumpWrites) addWrite(0xffff0005,chan[c.chan].dac.pos);
break;
case DIV_CMD_MACRO_OFF:
chan[c.chan].std.mask(c.value,true);

View file

@ -1463,6 +1463,13 @@ int DivPlatformGenesis::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
if (c.chan>=6) break;
chan[c.chan].hardReset=c.value;

View file

@ -28,7 +28,7 @@ struct _nla_table nla_table;
#define CHIP_DIVIDER 16
#define rWrite(a,v) if (!skipRegisterWrites) {doWrite(a,v); regPool[(a)&0x7f]=v; if (dumpWrites) {addWrite(a,v);} }
#define rWrite(a,v) if (!skipRegisterWrites) {writes.push(QueuedWrite((a),v)); if (dumpWrites) {addWrite((a),v);} }
const char* regCheatSheetNES[]={
"S0Volume", "4000",
@ -86,10 +86,10 @@ void DivPlatformNES::doWrite(unsigned short addr, unsigned char data) {
unsigned char next=((unsigned char)s->data8[dacPos]+0x80)>>1; \
if (dacAntiClickOn && dacAntiClick<next) { \
dacAntiClick+=8; \
rWrite(0x4011,dacAntiClick); \
doWrite(0x4011,dacAntiClick); \
} else { \
dacAntiClickOn=false; \
rWrite(0x4011,next); \
doWrite(0x4011,next); \
} \
} \
dacPos++; \
@ -108,6 +108,13 @@ void DivPlatformNES::doWrite(unsigned short addr, unsigned char data) {
void DivPlatformNES::acquire_puNES(short** buf, size_t len) {
for (size_t i=0; i<len; i++) {
doPCM;
if (!writes.empty()) {
QueuedWrite w=writes.front();
doWrite(w.addr,w.val);
regPool[w.addr&0x1f]=w.val;
writes.pop();
}
apu_tick(nes,NULL);
nes->apu.odd_cycle=!nes->apu.odd_cycle;
@ -134,6 +141,13 @@ void DivPlatformNES::acquire_NSFPlay(short** buf, size_t len) {
int out2[2];
for (size_t i=0; i<len; i++) {
doPCM;
if (!writes.empty()) {
QueuedWrite w=writes.front();
doWrite(w.addr,w.val);
regPool[w.addr&0x1f]=w.val;
writes.pop();
}
nes1_NP->Tick(8);
nes2_NP->TickFrameSequence(8);
@ -297,7 +311,7 @@ void DivPlatformNES::tick(bool sysTick) {
if (chan[i].sweepChanged) {
chan[i].sweepChanged=false;
if (i==0) {
//rWrite(16+i*5,chan[i].sweep);
// rWrite(16+i*5,chan[i].sweep);
}
}
if (i<3) if (chan[i].std.phaseReset.had) {
@ -338,7 +352,7 @@ void DivPlatformNES::tick(bool sysTick) {
}
}
if (chan[i].keyOff) {
//rWrite(16+i*5+2,8);
// rWrite(16+i*5+2,8);
if (i==2) { // triangle
rWrite(0x4000+i*4,0x00);
} else {
@ -400,6 +414,34 @@ void DivPlatformNES::tick(bool sysTick) {
logV("switching bank to %d",dpcmBank);
if (dumpWrites) addWrite(0xffff0004,dpcmBank);
}
// sample custom loop point...
DivSample* lsamp=parent->getSample(dacSample);
// how it works:
// when the initial sample info is written (see above) and playback is launched,
// the parameters (start point in memory and length) are locked until sample end
// is reached.
// thus, if we write new data after just several APU clock cycles, it will be used only when
// sample finishes one full loop.
// thus we can write sample's loop point as "start address" and sample's looped part length
// as "full sample length".
// APU will play full sample once and then repeatedly cycle through the looped part.
// sources:
// https://www.nesdev.org/wiki/APU_DMC
// https://www.youtube.com/watch?v=vB4P8x2Am6Y
if (lsamp->loopEnd>lsamp->loopStart && goingToLoop) {
int loopStartAddr=(sampleOffDPCM[dacSample]+lsamp->loopStart)>>3;
int loopLen=(lsamp->loopEnd-lsamp->loopStart)>>3;
rWrite(0x4012,(loopStartAddr>>6)&0xff);
rWrite(0x4013,(loopLen>>4)&0xff);
}
}
} else {
if (nextDPCMFreq>=0) {
@ -790,6 +832,7 @@ float DivPlatformNES::getPostAmp() {
}
void DivPlatformNES::reset() {
while (!writes.empty()) writes.pop();
for (int i=0; i<5; i++) {
chan[i]=DivPlatformNES::Channel();
chan[i].std.setEngine(parent);

View file

@ -24,6 +24,7 @@
#include "sound/nes_nsfplay/nes_apu.h"
#include "sound/nes_nsfplay/5e01_apu.h"
#include "../../fixedQueue.h"
class DivPlatformNES: public DivDispatch {
struct Channel: public SharedChannel<signed char> {
@ -44,6 +45,13 @@ class DivPlatformNES: public DivDispatch {
Channel chan[5];
DivDispatchOscBuffer* oscBuf[5];
bool isMuted[5];
struct QueuedWrite {
unsigned short addr;
unsigned char val;
QueuedWrite(): addr(0), val(0) {}
QueuedWrite(unsigned short a, unsigned char v): addr(a), val(v) {}
};
FixedQueue<QueuedWrite,128> writes;
int dacPeriod, dacRate, dpcmPos;
unsigned int dacPos, dacAntiClick;
int dacSample;

View file

@ -165,8 +165,12 @@ int DivPlatformSMS::snCalcFreq(int ch) {
if (ch==3) CHIP_DIVIDER=noiseDivider;
int easyStartingPeriod=16;
int easyThreshold=round(128.0*12.0*log((chipClock/(easyStartingPeriod*CHIP_DIVIDER))/(0.0625*parent->song.tuning))/log(2.0))-384+64;
if (parent->song.linearPitch==2 && easyNoise && chan[ch].baseFreq+chan[ch].pitch+chan[ch].pitch2>(easyThreshold)) {
int ret=(((easyStartingPeriod<<7))-(chan[ch].baseFreq+chan[ch].pitch+chan[ch].pitch2-(easyThreshold)))>>7;
int curFreq=chan[ch].baseFreq+chan[ch].pitch+chan[ch].pitch2+(chan[ch].arpOff<<7);
if (chan[ch].fixedArp) {
curFreq=chan[ch].baseNoteOverride<<7;
}
if (parent->song.linearPitch==2 && easyNoise && curFreq>easyThreshold) {
int ret=(((easyStartingPeriod<<7))-(curFreq-(easyThreshold)))>>7;
if (ret<0) ret=0;
return ret;
}

View file

@ -841,6 +841,8 @@ void DivPlatformSNES::reset() {
memcpy(sampleMem,copyOfSampleMem,65536);
dsp.init(sampleMem);
dsp.set_output(NULL,0);
dsp.setupInterpolation(!interpolationOff);
memset(regPool,0,128);
// this can't be 0 or channel 1 won't play
// this can't be 0x100 either as that's used by SPC700 page 1 and the stack
@ -1023,6 +1025,8 @@ void DivPlatformSNES::setFlags(const DivConfig& flags) {
initEchoFIR[7]=flags.getInt("echoFilter7",0);
initEchoMask=flags.getInt("echoMask",0);
interpolationOff=flags.getBool("interpolationOff",false);
}
int DivPlatformSNES::init(DivEngine* p, int channels, int sugRate, const DivConfig& flags) {

View file

@ -69,6 +69,7 @@ class DivPlatformSNES: public DivDispatch {
bool writeEcho;
bool writeDryVol;
bool echoOn;
bool interpolationOff;
bool initEchoOn;
signed char initEchoVolL;

View file

@ -135,24 +135,30 @@ static short const gauss [512] =
1299,1300,1300,1301,1302,1302,1303,1303,1303,1304,1304,1304,1304,1304,1305,1305,
};
void SPC_DSP::setupInterpolation(bool interpolate){for(int i=0;i<voice_count;i++){m.voices[i].interpolate=interpolate;}}
inline int SPC_DSP::interpolate( voice_t const* v )
{
// Make pointers into gaussian based on fractional position between samples
int offset = v->interp_pos >> 4 & 0xFF;
short const* fwd = gauss + 255 - offset;
short const* rev = gauss + offset; // mirror left half of gaussian
if (v->interpolate) {
int offset = v->interp_pos >> 4 & 0xFF;
short const* fwd = gauss + 255 - offset;
short const* rev = gauss + offset; // mirror left half of gaussian
int const* in = &v->buf [(v->interp_pos >> 12) + v->buf_pos];
int out;
out = (fwd [ 0] * in [0]) >> 11;
out += (fwd [256] * in [1]) >> 11;
out += (rev [256] * in [2]) >> 11;
out = (int16_t) out;
out += (rev [ 0] * in [3]) >> 11;
int const* in = &v->buf [(v->interp_pos >> 12) + v->buf_pos];
int out;
out = (fwd [ 0] * in [0]) >> 11;
out += (fwd [256] * in [1]) >> 11;
out += (rev [256] * in [2]) >> 11;
out = (int16_t) out;
out += (rev [ 0] * in [3]) >> 11;
CLAMP16( out );
out &= ~1;
return out;
CLAMP16( out );
out &= ~1;
return out;
} else {
return v->buf [(v->interp_pos >> 12) + v->buf_pos]; //Furnace addition -- no interpolation
}
}

View file

@ -27,6 +27,9 @@ public:
// output buffer could hold.
int sample_count() const;
// Furnace addition: disable/enable Gaussian interpolation
void setupInterpolation(bool interpolate);
// Emulation
// Resets DSP to power-on state
@ -122,6 +125,7 @@ public:
int hidden_env; // used by GAIN mode 7, very obscure quirk
uint8_t t_envx_out;
sample_t out[2]; // Furnace addition, for per-channel oscilloscope
bool interpolate; // Furnace addition, to disable interpolation
};
// Furnace addition, gets a voice

View file

@ -858,6 +858,9 @@ int DivPlatformTX81Z::dispatch(DivCommand c) {
immWrite(0x17,0x80|pmDepth);
break;
}
case DIV_CMD_FM_OPMASK:
// TODO: if OPZ supports op mask
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1004,6 +1004,13 @@ int DivPlatformYM2203::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1537,6 +1537,13 @@ int DivPlatformYM2608::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1507,6 +1507,13 @@ int DivPlatformYM2610::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -1576,6 +1576,13 @@ int DivPlatformYM2610B::dispatch(DivCommand c) {
}
break;
}
case DIV_CMD_FM_OPMASK:
if (c.chan>=psgChanOffs) break;
chan[c.chan].opMask=c.value&15;
if (chan[c.chan].active) {
chan[c.chan].opMaskChanged=true;
}
break;
case DIV_CMD_FM_HARD_RESET:
chan[c.chan].hardReset=c.value;
break;

View file

@ -67,6 +67,7 @@ const char* cmdName[]={
"HINT_ARPEGGIO",
"HINT_VOLUME",
"HINT_VOL_SLIDE",
"HINT_VOL_SLIDE_TARGET",
"HINT_PORTA",
"HINT_LEGATO",
@ -265,6 +266,8 @@ const char* cmdName[]={
"FDS_MOD_AUTO",
"FM_OPMASK",
"MULTIPCM_MIX_FM",
"MULTIPCM_MIX_PCM",
"MULTIPCM_LFO",
@ -655,7 +658,23 @@ void DivEngine::processRow(int i, bool afterDelay) {
}
// volume
if (pat->data[whatRow][3]!=-1) {
int volPortaTarget=-1;
bool noApplyVolume=false;
for (int j=0; j<curPat[i].effectCols; j++) {
short effect=pat->data[whatRow][4+(j<<1)];
if (effect==0xd3 || effect==0xd4) { // vol porta
volPortaTarget=pat->data[whatRow][3]<<8; // can be -256
short effectVal=pat->data[whatRow][5+(j<<1)];
if (effectVal==-1) effectVal=0;
effectVal&=255;
noApplyVolume=effectVal>0; // "D3.." or "D300" shouldn't stop volume from applying
break; // technically you could have both D3 and D4... let's not care
}
}
if (pat->data[whatRow][3]!=-1 && !noApplyVolume) {
if (!song.oldAlwaysSetVolume || disCont[dispatchOfChan[i]].dispatch->getLegacyAlwaysSetVolume() || (MIN(chan[i].volMax,chan[i].volume)>>8)!=pat->data[whatRow][3]) {
if (pat->data[whatRow][0]==0 && pat->data[whatRow][1]==0) {
chan[i].midiAftertouch=true;
@ -844,6 +863,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else {
chan[i].volSpeed=0;
}
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0x06: // vol slide + porta
@ -885,6 +905,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else {
chan[i].volSpeed=0;
}
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0x07: // tremolo
@ -895,6 +916,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloRate=effectVal>>4;
if (chan[i].tremoloDepth!=0) {
chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
} else {
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
@ -914,6 +936,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else {
chan[i].volSpeed=0;
}
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0x00: // arpeggio
@ -956,6 +979,22 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].cutType=0;
}
break;
case 0xd3: // volume portamento (vol porta)
// tremolo and vol slides are incompatible
chan[i].tremoloDepth=0;
chan[i].tremoloRate=0;
chan[i].volSpeed=volPortaTarget<0 ? 0 : volPortaTarget>chan[i].volume ? effectVal : -effectVal;
chan[i].volSpeedTarget=chan[i].volSpeed==0 ? -1 : volPortaTarget;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE_TARGET,i,chan[i].volSpeed,chan[i].volSpeedTarget));
break;
case 0xd4: // volume portamento fast (vol porta fast)
// tremolo and vol slides are incompatible
chan[i].tremoloDepth=0;
chan[i].tremoloRate=0;
chan[i].volSpeed=volPortaTarget<0 ? 0 : volPortaTarget>chan[i].volume ? 256*effectVal : -256*effectVal;
chan[i].volSpeedTarget=chan[i].volSpeed==0 ? -1 : volPortaTarget;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE_TARGET,i,chan[i].volSpeed,chan[i].volSpeedTarget));
break;
case 0xe0: // arp speed
if (effectVal>0) {
curSubSong->arpLen=effectVal;
@ -1093,6 +1132,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloDepth=0;
chan[i].tremoloRate=0;
chan[i].volSpeed=effectVal;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0xf4: // fine volume ramp down
@ -1100,6 +1140,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloDepth=0;
chan[i].tremoloRate=0;
chan[i].volSpeed=-effectVal;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0xf5: // disable macro
@ -1113,12 +1154,14 @@ void DivEngine::processRow(int i, bool afterDelay) {
break;
case 0xf8: // single volume ramp up
chan[i].volSpeed=0; // add compat flag?
chan[i].volSpeedTarget=-1;
chan[i].volume=MIN(chan[i].volume+effectVal*256,chan[i].volMax);
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
break;
case 0xf9: // single volume ramp down
chan[i].volSpeed=0; // add compat flag?
chan[i].volSpeedTarget=-1;
chan[i].volume=MAX(chan[i].volume-effectVal*256,0);
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
@ -1136,9 +1179,9 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else {
chan[i].volSpeed=0;
}
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break;
case 0xfc: // delayed note release
if (song.delayBehavior==2 || effectVal<nextSpeed) {
chan[i].cut=effectVal+1;
@ -1606,9 +1649,30 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
if (chan[i].volSpeed!=0) {
chan[i].volume=(chan[i].volume&0xff)|(dispatchCmd(DivCommand(DIV_CMD_GET_VOLUME,i))<<8);
chan[i].volume+=chan[i].volSpeed;
if (chan[i].volSpeedTarget!=-1) {
bool atTarget=false;
if (chan[i].volSpeed>0) {
atTarget=(chan[i].volume>=chan[i].volSpeedTarget);
} else if (chan[i].volSpeed<0) {
atTarget=(chan[i].volume<=chan[i].volSpeedTarget);
} else {
atTarget=true;
chan[i].volSpeedTarget=chan[i].volume;
}
if (atTarget) {
chan[i].volume=chan[i].volSpeedTarget;
chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0));
}
}
if (chan[i].volume>chan[i].volMax) {
chan[i].volume=chan[i].volMax;
chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0));
@ -1620,6 +1684,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
} else {
chan[i].volume=0;
}
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
} else {

View file

@ -19,6 +19,7 @@
#include "sfWrapper.h"
#include "../fileutils.h"
#include "../ta-log.h"
#include "sndfile.h"
sf_count_t _vioGetSize(void* user) {
@ -80,6 +81,7 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) {
vio.seek=_vioSeek;
vio.tell=_vioTell;
vio.write=_vioWrite;
logV("SFWrapper: opening %s",path);
const char* modeC="rb";
if (mode==SFM_WRITE) {
@ -91,10 +93,12 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) {
f=ps_fopen(path,modeC);
if (f==NULL) {
logE("SFWrapper: failed to open (%s)",strerror(errno));
return NULL;
}
if (fseek(f,0,SEEK_END)==-1) {
logE("SFWrapper: failed to seek to end (%s)",strerror(errno));
fclose(f);
f=NULL;
return NULL;
@ -102,6 +106,7 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) {
len=ftell(f);
if (len==(SIZE_MAX>>1)) {
logE("SFWrapper: failed to tell (%s)",strerror(errno));
len=0;
fclose(f);
f=NULL;
@ -109,6 +114,7 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) {
}
if (fseek(f,0,SEEK_SET)==-1) {
logE("SFWrapper: failed to seek to beginning (%s)",strerror(errno));
len=0;
fclose(f);
f=NULL;
@ -117,5 +123,8 @@ SNDFILE* SFWrapper::doOpen(const char* path, int mode, SF_INFO* sfinfo) {
sf=sf_open_virtual(&vio,mode,sfinfo,this);
if (sf!=NULL) fileMode=mode;
if (sf==NULL) {
logE("SFWrapper: WHY IS IT NULL?!");
}
return sf;
}
}

View file

@ -102,6 +102,173 @@ bool DivSubSong::walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int
return false;
}
double calcRowLenInSeconds(const DivGroovePattern& speeds, float hz, int vN, int vD, int timeBaseFromSong) {
double hl=1; //count for 1 row
if (hl<=0.0) hl=4.0;
double timeBase=timeBaseFromSong+1;
double speedSum=0;
for (int i=0; i<MIN(16,speeds.len); i++) {
speedSum+=speeds.val[i];
}
speedSum/=MAX(1,speeds.len);
if (timeBase<1.0) timeBase=1.0;
if (speedSum<1.0) speedSum=1.0;
if (vD<1) vD=1;
return 1.0/((60.0*hz/(timeBase*hl*speedSum))*(double)vN/(double)vD/60.0);
}
void DivSubSong::findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders_vec, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat) {
length=0;
hasFFxx=false;
rowsForFadeout=0;
float secondsPerThisRow=0.0f;
DivGroovePattern curSpeeds=speeds; //simulate that we are playing the song, track all speed/BPM/tempo/engine rate changes
short curVirtualTempoN=virtualTempoN;
short curVirtualTempoD=virtualTempoD;
float curHz=hz;
double curDivider=(double)timeBase;
double curLen=0.0; //how many seconds passed since the start of song
int nextOrder=-1;
int nextRow=0;
int effectVal=0;
int lastSuspectedLoopEnd=-1;
DivPattern* subPat[DIV_MAX_CHANS];
unsigned char wsWalked[8192];
memset(wsWalked,0,8192);
if (firstPat>0) {
memset(wsWalked,255,32*firstPat);
}
for (int i=firstPat; i<ordersLen; i++) {
bool jumped=false;
for (int j=0; j<chans; j++) {
subPat[j]=pat[j].getPattern(orders.ord[j][i],false);
}
if (i>lastSuspectedLoopEnd) {
lastSuspectedLoopEnd=i;
}
for (int j=nextRow; j<patLen; j++) {
nextRow=0;
bool changingOrder=false;
bool jumpingOrder=false;
if (wsWalked[((i<<5)+(j>>3))&8191]&(1<<(j&7))) {
return;
}
for (int k=0; k<chans; k++) {
for (int l=0; l<pat[k].effectCols; l++) {
effectVal=subPat[k]->data[j][5+(l<<1)];
if (effectVal<0) effectVal=0;
if (subPat[k]->data[j][4+(l<<1)]==0xff) {
hasFFxx=true;
// FFxx makes YOU SHALL NOT PASS!!! move
orders_vec.push_back(j+1); // order len
length+=j+1; // add length of order to song length
return;
}
switch (subPat[k]->data[j][4+(l<<1)]) {
case 0x09: { // select groove pattern/speed 1
if (grooves.empty()) {
if (effectVal>0) curSpeeds.val[0]=effectVal;
} else {
if (effectVal<(short)grooves.size()) {
curSpeeds=grooves[effectVal];
//curSpeed=0;
}
}
break;
}
case 0x0f: { // speed 1/speed 2
if (curSpeeds.len==2 && grooves.empty()) {
if (effectVal>0) curSpeeds.val[1]=effectVal;
} else {
if (effectVal>0) curSpeeds.val[0]=effectVal;
}
break;
}
case 0xfd: { // virtual tempo num
if (effectVal>0) curVirtualTempoN=effectVal;
break;
}
case 0xfe: { // virtual tempo den
if (effectVal>0) curVirtualTempoD=effectVal;
break;
}
case 0xf0: { // set Hz by tempo (set bpm)
curDivider=(double)effectVal*2.0/5.0;
if (curDivider<1) curDivider=1;
break;
}
}
if (subPat[k]->data[j][4+(l<<1)]==0x0d) {
if (jumpTreatment==2) {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
} else if (jumpTreatment==1) {
if (nextOrder==-1 && (i<ordersLen-1 || !ignoreJumpAtEnd)) {
nextOrder=i+1;
nextRow=effectVal;
jumpingOrder=true;
}
} else {
if ((i<ordersLen-1 || !ignoreJumpAtEnd)) {
if (!changingOrder) {
nextOrder=i+1;
}
jumpingOrder=true;
nextRow=effectVal;
}
}
} else if (subPat[k]->data[j][4+(l<<1)]==0x0b) {
if (nextOrder==-1 || jumpTreatment==0) {
nextOrder=effectVal;
if (jumpTreatment==1 || jumpTreatment==2 || !jumpingOrder) {
nextRow=0;
}
changingOrder=true;
}
}
}
}
if (i>loopOrder || (i==loopOrder && j>loopRow)) {
// we count each row fadeout lasts. When our time is greater than fadeout length we successfully counted the number of fadeout rows
if (curLen<=fadeoutLen && fadeoutLen>0.0) {
secondsPerThisRow=calcRowLenInSeconds(speeds,curHz,curVirtualTempoN,curVirtualTempoD,curDivider);
curLen+=secondsPerThisRow;
rowsForFadeout++;
}
}
wsWalked[((i<<5)+(j>>3))&8191]|=1<<(j&7);
if (nextOrder!=-1) {
i=nextOrder-1;
orders_vec.push_back(j+1); // order len
length+=j+1; // add length of order to song length
jumped=true;
nextOrder=-1;
break;
}
}
if (!jumped) { // if no jump occured we add full pattern length
orders_vec.push_back(patLen); // order len
length+=patLen; // add length of order to song length
}
}
}
void DivSubSong::clearData() {
for (int i=0; i<DIV_MAX_CHANS; i++) {
pat[i].wipePatterns();

View file

@ -185,6 +185,11 @@ struct DivSubSong {
*/
bool walk(int& loopOrder, int& loopRow, int& loopEnd, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
/**
* find song length in rows (up to specified loop point).
*/
void findLength(int loopOrder, int loopRow, double fadeoutLen, int& rowsForFadeout, bool& hasFFxx, std::vector<int>& orders, std::vector<DivGroovePattern>& grooves, int& length, int chans, int jumpTreatment, int ignoreJumpAtEnd, int firstPat=0);
void clearData();
void optimizePatterns();
void rearrangePatterns();

View file

@ -504,6 +504,7 @@ void DivEngine::registerSystems() {
{0x5d, {DIV_CMD_FM_D2R, _("5Dxx: Set decay 2 of operator 2 (0 to 1F)"), constVal<1>, effectValAnd<31>}},
{0x5e, {DIV_CMD_FM_D2R, _("5Exx: Set decay 2 of operator 3 (0 to 1F)"), constVal<2>, effectValAnd<31>}},
{0x5f, {DIV_CMD_FM_D2R, _("5Fxx: Set decay 2 of operator 4 (0 to 1F)"), constVal<3>, effectValAnd<31>}},
{0x60, {DIV_CMD_FM_OPMASK, _("60xx: Set operator mask (bits 0-3)")}},
};
EffectHandlerMap fmOPMPostEffectHandlerMap(fmOPNPostEffectHandlerMap);
@ -514,6 +515,7 @@ void DivEngine::registerSystems() {
{0x1e, {DIV_CMD_FM_AM_DEPTH, _("1Exx: Set AM depth (0 to 7F)"), effectValAnd<127>}},
{0x1f, {DIV_CMD_FM_PM_DEPTH, _("1Fxx: Set PM depth (0 to 7F)"), effectValAnd<127>}},
{0x55, {DIV_CMD_FM_SSG, _("55xy: Set detune 2 (x: operator from 1 to 4 (0 for all ops); y: detune from 0 to 3)"), effectOpVal<4>, effectValAnd<3>}},
{0x60, {DIV_CMD_FM_OPMASK, _("60xx: Set operator mask (bits 0-3)")}},
});
EffectHandlerMap fmOPZPostEffectHandlerMap(fmOPMPostEffectHandlerMap);

View file

@ -24,7 +24,9 @@
constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0;
void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write, int streamOff, double* loopTimer, double* loopFreq, int* loopSample, bool* sampleDir, bool isSecond, int* pendingFreq, int* playingSample, int* setPos, unsigned int* sampleOff8, unsigned int* sampleLen8, size_t bankOffset, bool directStream) {
// this function is so long
// may as well make it something else
void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write, int streamOff, double* loopTimer, double* loopFreq, int* loopSample, bool* sampleDir, bool isSecond, int* pendingFreq, int* playingSample, int* setPos, unsigned int* sampleOff8, unsigned int* sampleLen8, size_t bankOffset, bool directStream, bool* sampleStoppable) {
unsigned char baseAddr1=isSecond?0xa0:0x50;
unsigned char baseAddr2=isSecond?0x80:0;
unsigned short baseAddr2S=isSecond?0x8000:0;
@ -737,6 +739,7 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
logD("writing stream command %x:%x with stream ID %d",write.addr,write.val,streamID);
switch (write.addr&0xff) {
case 0: // play sample
sampleStoppable[streamID]=true;
if (write.val<(unsigned int)song.sampleLen) {
if (playingSample[streamID]!=(int)write.val) {
pendingFreq[streamID]=write.val;
@ -775,6 +778,7 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
}
break;
case 1: { // set sample freq
sampleStoppable[streamID]=true;
int realFreq=write.val;
if (realFreq<0) realFreq=0;
if (realFreq>44100) realFreq=44100;
@ -818,11 +822,14 @@ void DivEngine::performVGMWrite(SafeWriter* w, DivSystem sys, DivRegWrite& write
break;
}
case 2: // stop sample
w->writeC(0x94);
w->writeC(streamID);
loopSample[streamID]=-1;
playingSample[streamID]=-1;
pendingFreq[streamID]=-1;
if (sampleStoppable[streamID]) {
w->writeC(0x94);
w->writeC(streamID);
loopSample[streamID]=-1;
playingSample[streamID]=-1;
pendingFreq[streamID]=-1;
sampleStoppable[streamID]=false;
}
break;
case 3: // set sample direction
sampleDir[streamID]=write.val;
@ -1321,6 +1328,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
bool sampleDir[DIV_MAX_CHANS];
int pendingFreq[DIV_MAX_CHANS];
int playingSample[DIV_MAX_CHANS];
bool sampleStoppable[DIV_MAX_CHANS];
int setPos[DIV_MAX_CHANS];
std::vector<unsigned int> chipVol;
std::vector<DivDelayedWrite> delayedWrites[DIV_MAX_CHIPS];
@ -1343,6 +1351,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
playingSample[i]=-1;
setPos[i]=0;
sampleDir[i]=false;
sampleStoppable[i]=true;
}
bool writeDACSamples=false;
@ -2274,8 +2283,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeI(0);
w->write(writeADPCM_Y8950[i]->getSampleMem(0),writeADPCM_Y8950[i]->getSampleMemUsage(0));
}
if (writeQSound[i]!=NULL && writeQSound[i]->getSampleMemUsage()>0) {
unsigned int blockSize=(writeQSound[i]->getSampleMemUsage()+0xffff)&(~0xffff);
if (writeQSound[i]!=NULL && writeQSound[i]->getSampleMemUsage(1)>0) {
unsigned int blockSize=(writeQSound[i]->getSampleMemUsage(1)+0xffff)&(~0xffff);
if (blockSize > 0x1000000) {
blockSize = 0x1000000;
}
@ -2283,7 +2292,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeC(0x66);
w->writeC(0x8F);
w->writeI((blockSize+8)|(i*0x80000000));
w->writeI(writeQSound[i]->getSampleMemCapacity());
w->writeI(writeQSound[i]->getSampleMemCapacity(1));
w->writeI(0);
w->write(writeQSound[i]->getSampleMem(),blockSize);
}
@ -2638,7 +2647,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
for (int i=0; i<song.systemLen; i++) {
std::vector<DivRegWrite>& writes=disCont[i].dispatch->getRegisterWrites();
for (DivRegWrite& j: writes) {
performVGMWrite(w,song.system[i],j,streamIDs[i],loopTimer,loopFreq,loopSample,sampleDir,isSecond[i],pendingFreq,playingSample,setPos,sampleOff8,sampleLen8,bankOffset[i],directStream);
performVGMWrite(w,song.system[i],j,streamIDs[i],loopTimer,loopFreq,loopSample,sampleDir,isSecond[i],pendingFreq,playingSample,setPos,sampleOff8,sampleLen8,bankOffset[i],directStream,sampleStoppable);
writeCount++;
}
writes.clear();
@ -2678,7 +2687,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
lastOne=i.second.time;
}
// write write
performVGMWrite(w,song.system[i.first],i.second.write,streamIDs[i.first],loopTimer,loopFreq,loopSample,sampleDir,isSecond[i.first],pendingFreq,playingSample,setPos,sampleOff8,sampleLen8,bankOffset[i.first],directStream);
performVGMWrite(w,song.system[i.first],i.second.write,streamIDs[i.first],loopTimer,loopFreq,loopSample,sampleDir,isSecond[i.first],pendingFreq,playingSample,setPos,sampleOff8,sampleLen8,bankOffset[i.first],directStream,sampleStoppable);
// handle global Furnace commands
writeCount++;
@ -2794,7 +2803,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeWString(ws,false); // japanese author name
w->writeS(0); // date
w->writeWString(L"Furnace (chiptune tracker)",false); // ripper
w->writeS(0); // notes
ws=utf8To16(song.notes.c_str());
w->writeWString(ws,false); // notes
int gd3Len=w->tell()-gd3Off-12;

View file

@ -33,11 +33,78 @@ bool DivEngine::isExporting() {
return exporting;
}
void DivEngine::getLoopsLeft(int &loops) {
if (totalLoops<0 || exportLoopCount==0) {
loops=0;
return;
}
loops=exportLoopCount-1-totalLoops;
}
void DivEngine::getTotalLoops(int &loops) {
loops=exportLoopCount-1;
}
void DivEngine::getCurSongPos(int &row, int &order) {
row=curRow;
order=curOrder;
}
void DivEngine::getTotalAudioFiles(int &files) {
files=0;
switch (exportMode) {
case DIV_EXPORT_MODE_ONE: {
files=1;
break;
}
case DIV_EXPORT_MODE_MANY_SYS: {
files=1; // there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
break;
}
case DIV_EXPORT_MODE_MANY_CHAN: {
for (int i=0; i<chans; i++) {
if (exportChannelMask[i]) {
files++;
}
}
break;
}
default:
break;
}
}
void DivEngine::getCurFileIndex(int &file) {
file=0;
switch (exportMode) {
case DIV_EXPORT_MODE_ONE: {
file=0;
break;
}
case DIV_EXPORT_MODE_MANY_SYS: {
file=0; // there actually are several files but they are processed in the same loop, so to correctly draw progress we think of them as one file
break;
}
case DIV_EXPORT_MODE_MANY_CHAN: {
file=curExportChan;
break;
}
default:
break;
}
}
bool DivEngine::getIsFadingOut() {
return isFadingOut;
}
#ifdef HAVE_SNDFILE
void DivEngine::runExportThread() {
size_t fadeOutSamples=got.rate*exportFadeOut;
size_t curFadeOutSample=0;
bool isFadingOut=false;
isFadingOut=false;
switch (exportMode) {
case DIV_EXPORT_MODE_ONE: {
@ -140,7 +207,11 @@ void DivEngine::runExportThread() {
sf[i]=NULL;
si[i].samplerate=got.rate;
si[i].channels=disCont[i].dispatch->getOutputCount();
si[i].format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
if (exportFormat==DIV_EXPORT_FORMAT_S16) {
si[i].format=SF_FORMAT_WAV|SF_FORMAT_PCM_16;
} else {
si[i].format=SF_FORMAT_WAV|SF_FORMAT_FLOAT;
}
}
for (int i=0; i<song.systemLen; i++) {
@ -247,6 +318,8 @@ void DivEngine::runExportThread() {
// take control of audio output
deinitAudioBackend();
curExportChan=0;
float* outBuf[DIV_MAX_OUTPUTS];
float* outBufFinal;
for (int i=0; i<exportOutputs; i++) {
@ -258,6 +331,7 @@ void DivEngine::runExportThread() {
for (int i=0; i<chans; i++) {
if (!exportChannelMask[i]) continue;
SNDFILE* sf;
SF_INFO si;
SFWrapper sfWrap;
@ -338,6 +412,8 @@ void DivEngine::runExportThread() {
}
}
curExportChan++;
if (sfWrap.doClose()!=0) {
logE("could not close audio file!");
}
@ -378,6 +454,7 @@ void DivEngine::runExportThread() {
}
logI("done!");
exporting=false;
curExportChan=0;
break;
}
}

View file

@ -1,92 +0,0 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2024 tildearrow and contributors
*
* 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.
*/
#ifndef _ZSM_H
#define _ZSM_H
//#include "engine.h"
#include "safeWriter.h"
#include "dispatch.h"
#include <stdlib.h>
#define ZSM_HEADER_SIZE 16
#define ZSM_VERSION 1
#define ZSM_YM_CMD 0x40
#define ZSM_DELAY_CMD 0x80
#define ZSM_YM_MAX_WRITES 63
#define ZSM_SYNC_MAX_WRITES 31
#define ZSM_DELAY_MAX 127
#define ZSM_EOF ZSM_DELAY_CMD
#define ZSM_EXT ZSM_YM_CMD
#define ZSM_EXT_PCM 0x00
#define ZSM_EXT_CHIP 0x40
#define ZSM_EXT_SYNC 0x80
#define ZSM_EXT_CUSTOM 0xC0
enum YM_STATE { ym_PREV, ym_NEW, ym_STATES };
enum PSG_STATE { psg_PREV, psg_NEW, psg_STATES };
class DivZSM {
private:
struct S_pcmInst {
int geometry;
unsigned int offset, length, loopPoint;
bool isLooped;
};
SafeWriter* w;
int ymState[ym_STATES][256];
int psgState[psg_STATES][64];
int pcmRateCache;
int pcmCtrlRVCache;
int pcmCtrlDCCache;
unsigned int pcmLoopPointCache;
bool pcmIsLooped;
std::vector<DivRegWrite> ymwrites;
std::vector<DivRegWrite> pcmMeta;
std::vector<unsigned char> pcmData;
std::vector<unsigned char> pcmCache;
std::vector<S_pcmInst> pcmInsts;
std::vector<DivRegWrite> syncCache;
int loopOffset;
int numWrites;
int ticks;
int tickRate;
int ymMask;
int psgMask;
bool optimize;
public:
DivZSM();
~DivZSM();
void init(unsigned int rate = 60);
int getoffset();
void writeYM(unsigned char a, unsigned char v);
void writePSG(unsigned char a, unsigned char v);
void writePCM(unsigned char a, unsigned char v);
void writeSync(unsigned char a, unsigned char v);
void setOptimize(bool o);
void tick(int numticks = 1);
void setLoopPoint();
SafeWriter* finish();
private:
void flushWrites();
void flushTicks();
};
#endif

View file

@ -1,208 +0,0 @@
/**
* Furnace Tracker - multi-system chiptune tracker
* Copyright (C) 2021-2024 tildearrow and contributors
*
* 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 "engine.h"
#include "../ta-log.h"
#include "../utfutils.h"
#include "song.h"
#include "zsm.h"
constexpr int MASTER_CLOCK_PREC=(sizeof(void*)==8)?8:0;
constexpr int MASTER_CLOCK_MASK=(sizeof(void*)==8)?0xff:0;
SafeWriter* DivEngine::saveZSM(unsigned int zsmrate, bool loop, bool optimize) {
int VERA=-1;
int YM=-1;
int IGNORED=0;
// find indexes for YM and VERA. Ignore other systems.
for (int i=0; i<song.systemLen; i++) {
switch (song.system[i]) {
case DIV_SYSTEM_VERA:
if (VERA>=0) {
IGNORED++;
break;
}
VERA=i;
logD("VERA detected as chip id %d",i);
break;
case DIV_SYSTEM_YM2151:
if (YM>=0) {
IGNORED++;
break;
}
YM=i;
logD("YM detected as chip id %d",i);
break;
default:
IGNORED++;
logD("Ignoring chip %d systemID %d",i,(int)song.system[i]);
break;
}
}
if (VERA<0 && YM<0) {
logE("No supported systems for ZSM");
return NULL;
}
if (IGNORED>0) {
logW("ZSM export ignoring %d unsupported system%c",IGNORED,IGNORED>1?'s':' ');
}
stop();
repeatPattern=false;
setOrder(0);
BUSY_BEGIN_SOFT;
double origRate=got.rate;
got.rate=zsmrate&0xffff;
// determine loop point
int loopOrder=0;
int loopRow=0;
int loopEnd=0;
walkSong(loopOrder,loopRow,loopEnd);
logI("loop point: %d %d",loopOrder,loopRow);
warnings="";
DivZSM zsm;
zsm.init(zsmrate);
// reset the playback state
curOrder=0;
freelance=false;
playing=false;
extValuePresent=false;
remainingLoops=-1;
// Prepare to write song data
playSub(false);
//size_t tickCount=0;
bool done=false;
bool loopNow=false;
int loopPos=-1;
int fracWait=0; // accumulates fractional ticks
if (VERA>=0) disCont[VERA].dispatch->toggleRegisterDump(true);
if (YM>=0) {
disCont[YM].dispatch->toggleRegisterDump(true);
// emit LFO initialization commands
zsm.writeYM(0x18,0); // freq=0
zsm.writeYM(0x19,0x7F); // AMD =7F
zsm.writeYM(0x19,0xFF); // PMD =7F
// TODO: incorporate the Furnace meta-command for init data and filter
// out writes to otherwise-unused channels.
}
// Indicate the song's tuning as a sync meta-event
// specified in terms of how many 1/256th semitones
// the song is offset from standard A-440 tuning.
// This is mainly to benefit visualizations in players
// for non-standard tunings so that they can avoid
// displaying the entire song held in pitch bend.
// Tunings offsets that exceed a half semitone
// will simply be represented in a different key
// by nature of overflowing the signed char value
signed char tuningoffset=(signed char)(round(3072*(log(song.tuning/440.0)/log(2))))&0xff;
zsm.writeSync(0x01,tuningoffset);
// Set optimize flag, which mainly buffers PSG writes
// whenever the channel is silent
zsm.setOptimize(optimize);
while (!done) {
if (loopPos==-1) {
if (loopOrder==curOrder && loopRow==curRow && loop)
loopNow=true;
if (loopNow) {
// If Virtual Tempo is in use, our exact loop point
// might be skipped due to quantization error.
// If this happens, the tick immediately following is our loop point.
if (ticks==1 || !(loopOrder==curOrder && loopRow==curRow)) {
loopPos=zsm.getoffset();
zsm.setLoopPoint();
loopNow=false;
}
}
}
if (nextTick() || !playing) {
done=true;
if (!loop) {
for (int i=0; i<song.systemLen; i++) {
disCont[i].dispatch->getRegisterWrites().clear();
}
break;
}
if (!playing) {
loopPos=-1;
}
}
// get register dumps
for (int j=0; j<2; j++) {
int i=0;
// dump YM writes first
if (j==0) {
if (YM<0) {
continue;
} else {
i=YM;
}
}
// dump VERA writes second
if (j==1) {
if (VERA<0) {
continue;
} else {
i=VERA;
}
}
std::vector<DivRegWrite>& writes=disCont[i].dispatch->getRegisterWrites();
if (writes.size()>0)
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) {
if (done && write.addr>=64) continue; // don't process any PCM or sync events on the loop lookahead
zsm.writePSG(write.addr&0xff,write.val);
}
}
writes.clear();
}
// write wait
int totalWait=cycles>>MASTER_CLOCK_PREC;
fracWait+=cycles&MASTER_CLOCK_MASK;
totalWait+=fracWait>>MASTER_CLOCK_PREC;
fracWait&=MASTER_CLOCK_MASK;
if (totalWait>0 && !done) {
zsm.tick(totalWait);
//tickCount+=totalWait;
}
}
// end of song
// done - close out.
got.rate=origRate;
if (VERA>=0) disCont[VERA].dispatch->toggleRegisterDump(false);
if (YM>=0) disCont[YM].dispatch->toggleRegisterDump(false);
remainingLoops=-1;
playing=false;
freelance=false;
extValuePresent=false;
BUSY_END;
return zsm.finish();
}