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

This commit is contained in:
LTVA1 2024-08-24 19:15:27 +03:00
commit 12bd2d3829
28 changed files with 541 additions and 76 deletions

View file

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

View file

@ -34,7 +34,7 @@ struct DivCSChannelState {
int lastWaitLen; int lastWaitLen;
int note, pitch; int note, pitch;
int volume, volMax, volSpeed; int volume, volMax, volSpeed, volSpeedTarget;
int vibratoDepth, vibratoRate, vibratoPos; int vibratoDepth, vibratoRate, vibratoPos;
int portaTarget, portaSpeed; int portaTarget, portaSpeed;
unsigned char arp, arpStage, arpTicks; unsigned char arp, arpStage, arpTicks;
@ -56,6 +56,7 @@ struct DivCSChannelState {
volume(0x7f00), volume(0x7f00),
volMax(0), volMax(0),
volSpeed(0), volSpeed(0),
volSpeedTarget(-1),
vibratoDepth(0), vibratoDepth(0),
vibratoRate(0), vibratoRate(0),
vibratoPos(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_VOLUME:
case DIV_CMD_HINT_PORTA: case DIV_CMD_HINT_PORTA:
case DIV_CMD_HINT_VOL_SLIDE: case DIV_CMD_HINT_VOL_SLIDE:
case DIV_CMD_HINT_VOL_SLIDE_TARGET:
case DIV_CMD_HINT_LEGATO: case DIV_CMD_HINT_LEGATO:
w->writeC((unsigned char)c.cmd+0xb4); w->writeC((unsigned char)c.cmd+0xb4);
break; break;
@ -100,6 +101,10 @@ void writePackedCommandValues(SafeWriter* w, const DivCommand& c) {
case DIV_CMD_HINT_VOL_SLIDE: case DIV_CMD_HINT_VOL_SLIDE:
w->writeS(c.value); w->writeS(c.value);
break; 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_MODE:
case DIV_CMD_SAMPLE_FREQ: case DIV_CMD_SAMPLE_FREQ:
case DIV_CMD_SAMPLE_BANK: case DIV_CMD_SAMPLE_BANK:

View file

@ -67,6 +67,7 @@ enum DivDispatchCmds {
DIV_CMD_HINT_ARPEGGIO, // (note1, note2) DIV_CMD_HINT_ARPEGGIO, // (note1, note2)
DIV_CMD_HINT_VOLUME, // (vol) DIV_CMD_HINT_VOLUME, // (vol)
DIV_CMD_HINT_VOL_SLIDE, // (amount, oneTick) DIV_CMD_HINT_VOL_SLIDE, // (amount, oneTick)
DIV_CMD_HINT_VOL_SLIDE_TARGET, // (amount, target)
DIV_CMD_HINT_PORTA, // (target, speed) DIV_CMD_HINT_PORTA, // (target, speed)
DIV_CMD_HINT_LEGATO, // (note) DIV_CMD_HINT_LEGATO, // (note)

View file

@ -98,6 +98,10 @@ const char* DivEngine::getEffectDesc(unsigned char effect, int chan, bool notNul
break; break;
case 0xc0: case 0xc1: case 0xc2: case 0xc3: case 0xc0: case 0xc1: case 0xc2: case 0xc3:
return _("Cxxx: Set tick rate (hz)"); return _("Cxxx: Set tick rate (hz)");
case 0xd3:
return _("D3xx: Volume portamento");
case 0xd4:
return _("D4xx: Volume portamento (fast)");
case 0xdc: case 0xdc:
return _("DCxx: Delayed mute"); return _("DCxx: Delayed mute");
case 0xe0: case 0xe0:

View file

@ -133,7 +133,7 @@ struct DivAudioExportOptions {
struct DivChannelState { struct DivChannelState {
std::vector<DivDelayedCommand> delayed; std::vector<DivDelayedCommand> delayed;
int note, oldNote, lastIns, pitch, portaSpeed, portaNote; 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 delayOrder, delayRow, retrigSpeed, retrigTick;
int vibratoDepth, vibratoRate, vibratoPos, vibratoPosGiant, vibratoShape, vibratoFine; int vibratoDepth, vibratoRate, vibratoPos, vibratoPosGiant, vibratoShape, vibratoFine;
int tremoloDepth, tremoloRate, tremoloPos; int tremoloDepth, tremoloRate, tremoloPos;
@ -157,6 +157,7 @@ struct DivChannelState {
portaNote(-1), portaNote(-1),
volume(0x7f00), volume(0x7f00),
volSpeed(0), volSpeed(0),
volSpeedTarget(-1),
cut(-1), cut(-1),
volCut(-1), volCut(-1),
legatoDelay(-1), legatoDelay(-1),

View file

@ -409,6 +409,141 @@ void DivInstrument::writeFeatureFM(SafeWriter* w, bool fui) {
FEATURE_END; 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) { void DivInstrument::writeMacro(SafeWriter* w, const DivInstrumentMacro& m) {
if (!m.len) return; if (!m.len) return;
@ -3538,3 +3673,28 @@ bool DivInstrument::saveDMP(const char* path) {
w->finish(); w->finish();
return true; 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 "dataErrors.h"
#include "../ta-utils.h" #include "../ta-utils.h"
#include "../pch.h" #include "../pch.h"
#include "../fixedQueue.h"
struct DivSong; struct DivSong;
struct DivInstrument;
// NOTICE! // NOTICE!
// before adding new instrument types to this struct, please ask me first. // before adding new instrument types to this struct, please ask me first.
@ -951,7 +953,7 @@ struct DivInstrumentSID3
} }
}; };
struct DivInstrument { struct DivInstrumentPOD {
String name; String name;
DivInstrumentType type; DivInstrumentType type;
DivInstrumentFM fm; DivInstrumentFM fm;
@ -972,6 +974,77 @@ struct DivInstrument {
DivInstrumentSID2 sid2; DivInstrumentSID2 sid2;
DivInstrumentSID3 sid3; DivInstrumentSID3 sid3;
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. * these are internal functions.
*/ */
@ -1058,9 +1131,5 @@ struct DivInstrument {
* @return whether it was successful. * @return whether it was successful.
*/ */
bool saveDMP(const char* path); bool saveDMP(const char* path);
DivInstrument():
name(""),
type(DIV_INS_FM) {
}
}; };
#endif #endif

View file

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

View file

@ -67,6 +67,7 @@ const char* cmdName[]={
"HINT_ARPEGGIO", "HINT_ARPEGGIO",
"HINT_VOLUME", "HINT_VOLUME",
"HINT_VOL_SLIDE", "HINT_VOL_SLIDE",
"HINT_VOL_SLIDE_TARGET",
"HINT_PORTA", "HINT_PORTA",
"HINT_LEGATO", "HINT_LEGATO",
@ -662,7 +663,23 @@ void DivEngine::processRow(int i, bool afterDelay) {
} }
// volume // 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 (!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) { if (pat->data[whatRow][0]==0 && pat->data[whatRow][1]==0) {
chan[i].midiAftertouch=true; chan[i].midiAftertouch=true;
@ -851,6 +868,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else { } else {
chan[i].volSpeed=0; chan[i].volSpeed=0;
} }
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0x06: // vol slide + porta case 0x06: // vol slide + porta
@ -892,6 +910,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else { } else {
chan[i].volSpeed=0; chan[i].volSpeed=0;
} }
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0x07: // tremolo case 0x07: // tremolo
@ -902,6 +921,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloRate=effectVal>>4; chan[i].tremoloRate=effectVal>>4;
if (chan[i].tremoloDepth!=0) { if (chan[i].tremoloDepth!=0) {
chan[i].volSpeed=0; chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
} else { } else {
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
@ -921,6 +941,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else { } else {
chan[i].volSpeed=0; chan[i].volSpeed=0;
} }
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0x00: // arpeggio case 0x00: // arpeggio
@ -963,6 +984,22 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].cutType=0; chan[i].cutType=0;
} }
break; 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 case 0xe0: // arp speed
if (effectVal>0) { if (effectVal>0) {
curSubSong->arpLen=effectVal; curSubSong->arpLen=effectVal;
@ -1100,6 +1137,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloDepth=0; chan[i].tremoloDepth=0;
chan[i].tremoloRate=0; chan[i].tremoloRate=0;
chan[i].volSpeed=effectVal; chan[i].volSpeed=effectVal;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0xf4: // fine volume ramp down case 0xf4: // fine volume ramp down
@ -1107,6 +1145,7 @@ void DivEngine::processRow(int i, bool afterDelay) {
chan[i].tremoloDepth=0; chan[i].tremoloDepth=0;
chan[i].tremoloRate=0; chan[i].tremoloRate=0;
chan[i].volSpeed=-effectVal; chan[i].volSpeed=-effectVal;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0xf5: // disable macro case 0xf5: // disable macro
@ -1120,12 +1159,14 @@ void DivEngine::processRow(int i, bool afterDelay) {
break; break;
case 0xf8: // single volume ramp up case 0xf8: // single volume ramp up
chan[i].volSpeed=0; // add compat flag? chan[i].volSpeed=0; // add compat flag?
chan[i].volSpeedTarget=-1;
chan[i].volume=MIN(chan[i].volume+effectVal*256,chan[i].volMax); 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_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
break; break;
case 0xf9: // single volume ramp down case 0xf9: // single volume ramp down
chan[i].volSpeed=0; // add compat flag? chan[i].volSpeed=0; // add compat flag?
chan[i].volSpeedTarget=-1;
chan[i].volume=MAX(chan[i].volume-effectVal*256,0); chan[i].volume=MAX(chan[i].volume-effectVal*256,0);
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
@ -1143,9 +1184,9 @@ void DivEngine::processRow(int i, bool afterDelay) {
} else { } else {
chan[i].volSpeed=0; chan[i].volSpeed=0;
} }
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,chan[i].volSpeed));
break; break;
case 0xfc: // delayed note release case 0xfc: // delayed note release
if (song.delayBehavior==2 || effectVal<nextSpeed) { if (song.delayBehavior==2 || effectVal<nextSpeed) {
chan[i].cut=effectVal+1; chan[i].cut=effectVal+1;
@ -1613,9 +1654,30 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
if (chan[i].volSpeed!=0) { 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].volume&0xff)|(dispatchCmd(DivCommand(DIV_CMD_GET_VOLUME,i))<<8);
chan[i].volume+=chan[i].volSpeed; 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) { if (chan[i].volume>chan[i].volMax) {
chan[i].volume=chan[i].volMax; chan[i].volume=chan[i].volMax;
chan[i].volSpeed=0; chan[i].volSpeed=0;
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); 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_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOL_SLIDE,i,0));
@ -1627,6 +1689,7 @@ bool DivEngine::nextTick(bool noAccum, bool inhibitLowLat) {
} else { } else {
chan[i].volume=0; chan[i].volume=0;
} }
chan[i].volSpeedTarget=-1;
dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8));
dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8)); dispatchCmd(DivCommand(DIV_CMD_HINT_VOLUME,i,chan[i].volume>>8));
} else { } else {

View file

@ -2169,8 +2169,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeI(0); w->writeI(0);
w->write(writeADPCM_Y8950[i]->getSampleMem(0),writeADPCM_Y8950[i]->getSampleMemUsage(0)); w->write(writeADPCM_Y8950[i]->getSampleMem(0),writeADPCM_Y8950[i]->getSampleMemUsage(0));
} }
if (writeQSound[i]!=NULL && writeQSound[i]->getSampleMemUsage()>0) { if (writeQSound[i]!=NULL && writeQSound[i]->getSampleMemUsage(1)>0) {
unsigned int blockSize=(writeQSound[i]->getSampleMemUsage()+0xffff)&(~0xffff); unsigned int blockSize=(writeQSound[i]->getSampleMemUsage(1)+0xffff)&(~0xffff);
if (blockSize > 0x1000000) { if (blockSize > 0x1000000) {
blockSize = 0x1000000; blockSize = 0x1000000;
} }
@ -2178,7 +2178,7 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeC(0x66); w->writeC(0x66);
w->writeC(0x8F); w->writeC(0x8F);
w->writeI((blockSize+8)|(i*0x80000000)); w->writeI((blockSize+8)|(i*0x80000000));
w->writeI(writeQSound[i]->getSampleMemCapacity()); w->writeI(writeQSound[i]->getSampleMemCapacity(1));
w->writeI(0); w->writeI(0);
w->write(writeQSound[i]->getSampleMem(),blockSize); w->write(writeQSound[i]->getSampleMem(),blockSize);
} }
@ -2679,7 +2679,8 @@ SafeWriter* DivEngine::saveVGM(bool* sysToExport, bool loop, int version, bool p
w->writeWString(ws,false); // japanese author name w->writeWString(ws,false); // japanese author name
w->writeS(0); // date w->writeS(0); // date
w->writeWString(L"Furnace (chiptune tracker)",false); // ripper 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; int gd3Len=w->tell()-gd3Off-12;

View file

@ -42,6 +42,7 @@ template<typename T, size_t items> struct FixedQueue {
void clear(); void clear();
bool empty(); bool empty();
size_t size(); size_t size();
size_t capacity();
FixedQueue(): FixedQueue():
readPos(0), readPos(0),
writePos(0) {} writePos(0) {}
@ -177,4 +178,8 @@ template <typename T, size_t items> size_t FixedQueue<T,items>::size() {
return writePos-readPos; return writePos-readPos;
} }
template <typename T, size_t items> size_t FixedQueue<T,items>::capacity() {
return items-1;
}
#endif #endif

View file

@ -35,6 +35,7 @@ const char* aboutLine[]={
_N("-- program --"), _N("-- program --"),
"tildearrow", "tildearrow",
_N("A M 4 N (intro tune)"), _N("A M 4 N (intro tune)"),
"Adam Lederer",
"akumanatt", "akumanatt",
"cam900", "cam900",
"djtuBIG-MaliceX", "djtuBIG-MaliceX",

View file

@ -161,6 +161,8 @@ void FurnaceGUI::drawCSPlayer() {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text(_("vols")); ImGui::Text(_("vols"));
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text(_("volst"));
ImGui::TableNextColumn();
ImGui::Text(_("vib")); ImGui::Text(_("vib"));
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text(_("porta")); ImGui::Text(_("porta"));
@ -189,6 +191,8 @@ void FurnaceGUI::drawCSPlayer() {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("%+d",state->volSpeed); ImGui::Text("%+d",state->volSpeed);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("%+d",state->volSpeedTarget);
ImGui::TableNextColumn();
ImGui::Text("%d/%d (%d)",state->vibratoDepth,state->vibratoRate,state->vibratoPos); ImGui::Text("%d/%d (%d)",state->vibratoDepth,state->vibratoRate,state->vibratoPos);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Text("-> %d (%d)",state->portaTarget,state->portaSpeed); ImGui::Text("-> %d (%d)",state->portaTarget,state->portaSpeed);

View file

@ -145,6 +145,7 @@ void FurnaceGUI::drawDebug() {
ImGui::Text("- portaNote = %d",ch->portaNote); ImGui::Text("- portaNote = %d",ch->portaNote);
ImGui::Text("- volume = %.4x",ch->volume); ImGui::Text("- volume = %.4x",ch->volume);
ImGui::Text("- volSpeed = %d",ch->volSpeed); ImGui::Text("- volSpeed = %d",ch->volSpeed);
ImGui::Text("- volSpeedTarget = %d",ch->volSpeedTarget);
ImGui::Text("- cut = %d",ch->cut); ImGui::Text("- cut = %d",ch->cut);
ImGui::Text("- rowDelay = %d",ch->rowDelay); ImGui::Text("- rowDelay = %d",ch->rowDelay);
ImGui::Text("- volMax = %.4x",ch->volMax); ImGui::Text("- volMax = %.4x",ch->volMax);

View file

@ -73,6 +73,8 @@ void FurnaceGUI::doAction(int what) {
case GUI_ACTION_UNDO: case GUI_ACTION_UNDO:
if (curWindow==GUI_WINDOW_SAMPLE_EDIT) { if (curWindow==GUI_WINDOW_SAMPLE_EDIT) {
doUndoSample(); doUndoSample();
} else if (curWindow==GUI_WINDOW_INS_EDIT) {
doUndoInstrument();
} else { } else {
doUndo(); doUndo();
} }
@ -80,6 +82,8 @@ void FurnaceGUI::doAction(int what) {
case GUI_ACTION_REDO: case GUI_ACTION_REDO:
if (curWindow==GUI_WINDOW_SAMPLE_EDIT) { if (curWindow==GUI_WINDOW_SAMPLE_EDIT) {
doRedoSample(); doRedoSample();
} else if (curWindow==GUI_WINDOW_INS_EDIT) {
doRedoInstrument();
} else { } else {
doRedo(); doRedo();
} }
@ -677,30 +681,7 @@ void FurnaceGUI::doAction(int what) {
latchNibble=false; latchNibble=false;
break; break;
case GUI_ACTION_PAT_ABSORB_INSTRUMENT: { case GUI_ACTION_PAT_ABSORB_INSTRUMENT: {
DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][curOrder],false); doAbsorbInstrument();
if (!pat) break;
bool foundIns=false;
bool foundOctave=false;
for (int i=cursor.y; i>=0 && !(foundIns && foundOctave); i--) {
// absorb most recent instrument
if (!foundIns && pat->data[i][2] >= 0) {
curIns=pat->data[i][2];
foundIns=true;
}
// absorb most recent octave (i.e. set curOctave such that the "main row" (QWERTY) of notes
// will result in an octave number equal to the previous note).
if (!foundOctave && pat->data[i][0] != 0) {
// decode octave data (was signed cast to unsigned char)
int octave=pat->data[i][1];
if (octave>128) octave-=256;
// @NOTE the special handling when note==12, which is really an octave above what's
// stored in the octave data. without this handling, if you press Q, then
// "ABSORB_INSTRUMENT", then Q again, you'd get a different octave!
if (pat->data[i][0]==12) octave++;
curOctave=CLAMP(octave-1, GUI_EDIT_OCTAVE_MIN, GUI_EDIT_OCTAVE_MAX);
foundOctave=true;
}
}
break; break;
} }

View file

@ -508,7 +508,7 @@ void FurnaceGUI::drawMobileControls() {
mobileMenuOpen=false; mobileMenuOpen=false;
doAction(GUI_ACTION_SAVE_AS); doAction(GUI_ACTION_SAVE_AS);
} }
ImGui::SameLine();
if (ImGui::Button(_("Export"))) { if (ImGui::Button(_("Export"))) {
doAction(GUI_ACTION_EXPORT); doAction(GUI_ACTION_EXPORT);
} }
@ -533,6 +533,10 @@ void FurnaceGUI::drawMobileControls() {
drawSpeed(true); drawSpeed(true);
ImGui::EndTabItem(); ImGui::EndTabItem();
} }
if (ImGui::BeginTabItem(_("Comments"))) {
drawNotes(true);
ImGui::EndTabItem();
}
ImGui::EndTabBar(); ImGui::EndTabBar();
} }
break; break;

View file

@ -1823,6 +1823,52 @@ void FurnaceGUI::doExpandSong(int multiplier) {
if (e->isPlaying()) e->play(); if (e->isPlaying()) e->play();
} }
void FurnaceGUI::doAbsorbInstrument() {
bool foundIns=false;
bool foundOctave=false;
auto foundAll = [&]() { return foundIns && foundOctave; };
// search this order and all prior until we find all the data we need
int orderIdx=curOrder;
for (; orderIdx>=0 && !foundAll(); orderIdx--) {
DivPattern* pat=e->curPat[cursor.xCoarse].getPattern(e->curOrders->ord[cursor.xCoarse][orderIdx],false);
if (!pat) continue;
// start on current row when searching current order, but start from end when searching
// prior orders.
int searchStartRow=orderIdx==curOrder ? cursor.y : e->curSubSong->patLen-1;
for (int i=searchStartRow; i>=0 && !foundAll(); i--) {
// absorb most recent instrument
if (!foundIns && pat->data[i][2] >= 0) {
foundIns=true;
curIns=pat->data[i][2];
}
// absorb most recent octave (i.e. set curOctave such that the "main row" (QWERTY) of
// notes will result in an octave number equal to the previous note).
if (!foundOctave && pat->data[i][0] != 0) {
foundOctave=true;
// decode octave data (was signed cast to unsigned char)
int octave=pat->data[i][1];
if (octave>128) octave-=256;
// @NOTE the special handling when note==12, which is really an octave above what's
// stored in the octave data. without this handling, if you press Q, then
// "ABSORB_INSTRUMENT", then Q again, you'd get a different octave!
if (pat->data[i][0]==12) octave++;
curOctave=CLAMP(octave-1, GUI_EDIT_OCTAVE_MIN, GUI_EDIT_OCTAVE_MAX);
}
}
}
// if no instrument has been set at this point, the only way to match it is to use "none"
if (!foundIns) curIns=-1;
logD("doAbsorbInstrument -- searched %d orders", curOrder-orderIdx);
}
void FurnaceGUI::doDrag() { void FurnaceGUI::doDrag() {
int len=dragEnd.xCoarse-dragStart.xCoarse+1; int len=dragEnd.xCoarse-dragStart.xCoarse+1;

View file

@ -3739,6 +3739,7 @@ bool FurnaceGUI::loop() {
ImGui::GetIO().AddKeyEvent(ImGuiKey_Backspace,false); ImGui::GetIO().AddKeyEvent(ImGuiKey_Backspace,false);
injectBackUp=false; injectBackUp=false;
} }
while (SDL_PollEvent(&ev)) { while (SDL_PollEvent(&ev)) {
WAKE_UP; WAKE_UP;
ImGui_ImplSDL2_ProcessEvent(&ev); ImGui_ImplSDL2_ProcessEvent(&ev);
@ -3755,13 +3756,16 @@ bool FurnaceGUI::loop() {
} }
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
pointUp(ev.button.x,ev.button.y,ev.button.button); pointUp(ev.button.x,ev.button.y,ev.button.button);
insEditMayBeDirty=true;
break; break;
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONDOWN:
pointDown(ev.button.x,ev.button.y,ev.button.button); pointDown(ev.button.x,ev.button.y,ev.button.button);
insEditMayBeDirty=true;
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
wheelX+=ev.wheel.x; wheelX+=ev.wheel.x;
wheelY+=ev.wheel.y; wheelY+=ev.wheel.y;
insEditMayBeDirty=true;
break; break;
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
switch (ev.window.event) { switch (ev.window.event) {
@ -3838,12 +3842,14 @@ bool FurnaceGUI::loop() {
if (!ImGui::GetIO().WantCaptureKeyboard) { if (!ImGui::GetIO().WantCaptureKeyboard) {
keyDown(ev); keyDown(ev);
} }
insEditMayBeDirty=true;
#ifdef IS_MOBILE #ifdef IS_MOBILE
injectBackUp=true; injectBackUp=true;
#endif #endif
break; break;
case SDL_KEYUP: case SDL_KEYUP:
// for now // for now
insEditMayBeDirty=true;
break; break;
case SDL_DROPFILE: case SDL_DROPFILE:
if (ev.drop.file!=NULL) { if (ev.drop.file!=NULL) {
@ -4482,7 +4488,7 @@ bool FurnaceGUI::loop() {
} else { } else {
if (ImGui::BeginMenu(_("add chip..."))) { if (ImGui::BeginMenu(_("add chip..."))) {
exitDisabledTimer=1; exitDisabledTimer=1;
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
if (!e->addSystem(picked)) { if (!e->addSystem(picked)) {
showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError())); showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError()));
@ -4513,7 +4519,7 @@ bool FurnaceGUI::loop() {
ImGui::Checkbox(_("Preserve channel positions"),&preserveChanPos); ImGui::Checkbox(_("Preserve channel positions"),&preserveChanPos);
for (int i=0; i<e->song.systemLen; i++) { for (int i=0; i<e->song.systemLen; i++) {
if (ImGui::BeginMenu(fmt::sprintf("%d. %s##_SYSC%d",i+1,getSystemName(e->song.system[i]),i).c_str())) { if (ImGui::BeginMenu(fmt::sprintf("%d. %s##_SYSC%d",i+1,getSystemName(e->song.system[i]),i).c_str())) {
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
if (e->changeSystem(i,picked,preserveChanPos)) { if (e->changeSystem(i,picked,preserveChanPos)) {
MARK_MODIFIED; MARK_MODIFIED;
@ -5856,6 +5862,7 @@ bool FurnaceGUI::loop() {
centerNextWindow(_("Rendering..."),canvasW,canvasH); centerNextWindow(_("Rendering..."),canvasW,canvasH);
if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { if (ImGui::BeginPopupModal(_("Rendering..."),NULL,ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) {
WAKE_UP;
if(audioExportOptions.mode != DIV_EXPORT_MODE_MANY_CHAN) if(audioExportOptions.mode != DIV_EXPORT_MODE_MANY_CHAN)
{ {
ImGui::Text(_("Please wait...")); ImGui::Text(_("Please wait..."));
@ -5916,7 +5923,7 @@ bool FurnaceGUI::loop() {
ImGui::Text(_("Channel %d of %d"), curFile + 1, totalFiles); ImGui::Text(_("Channel %d of %d"), curFile + 1, totalFiles);
} }
ImGui::ProgressBar(curProgress,ImVec2(-FLT_MIN,0), fmt::sprintf("%.2f%%", curProgress * 100.0f).c_str()); ImGui::ProgressBar(curProgress,ImVec2(320.0f*dpiScale,0), fmt::sprintf("%.2f%%", curProgress * 100.0f).c_str());
if (ImGui::Button(_("Abort"))) { if (ImGui::Button(_("Abort"))) {
if (e->haltAudioFile()) { if (e->haltAudioFile()) {
@ -7241,6 +7248,11 @@ bool FurnaceGUI::loop() {
willCommit=false; willCommit=false;
} }
// To check for instrument editor modification, we need an up-to-date `insEditMayBeDirty`
// (based on incoming user input events), and we want any possible instrument modifications
// to already have been made.
checkRecordInstrumentUndoStep();
if (shallDetectScale) { if (shallDetectScale) {
if (--shallDetectScale<1) { if (--shallDetectScale<1) {
if (settings.dpiScale<0.5f) { if (settings.dpiScale<0.5f) {
@ -8414,6 +8426,8 @@ FurnaceGUI::FurnaceGUI():
localeRequiresChineseTrad(false), localeRequiresChineseTrad(false),
localeRequiresKorean(false), localeRequiresKorean(false),
prevInsData(NULL), prevInsData(NULL),
cachedCurInsPtr(NULL),
insEditMayBeDirty(false),
pendingLayoutImport(NULL), pendingLayoutImport(NULL),
pendingLayoutImportLen(0), pendingLayoutImportLen(0),
pendingLayoutImportStep(0), pendingLayoutImportStep(0),

View file

@ -2270,6 +2270,9 @@ class FurnaceGUI {
std::vector<ImWchar> localeExtraRanges; std::vector<ImWchar> localeExtraRanges;
DivInstrument* prevInsData; DivInstrument* prevInsData;
DivInstrument cachedCurIns;
DivInstrument* cachedCurInsPtr;
bool insEditMayBeDirty;
unsigned char* pendingLayoutImport; unsigned char* pendingLayoutImport;
size_t pendingLayoutImportLen; size_t pendingLayoutImportLen;
@ -2834,7 +2837,7 @@ class FurnaceGUI {
void drawMemory(); void drawMemory();
void drawCompatFlags(); void drawCompatFlags();
void drawPiano(); void drawPiano();
void drawNotes(); void drawNotes(bool asChild=false);
void drawChannels(); void drawChannels();
void drawPatManager(); void drawPatManager();
void drawSysManager(); void drawSysManager();
@ -2924,13 +2927,14 @@ class FurnaceGUI {
void doExpand(int multiplier, const SelectionPoint& sStart, const SelectionPoint& sEnd); void doExpand(int multiplier, const SelectionPoint& sStart, const SelectionPoint& sEnd);
void doCollapseSong(int divider); void doCollapseSong(int divider);
void doExpandSong(int multiplier); void doExpandSong(int multiplier);
void doAbsorbInstrument();
void doUndo(); void doUndo();
void doRedo(); void doRedo();
void doFind(); void doFind();
void doReplace(); void doReplace();
void doDrag(); void doDrag();
void editOptions(bool topMenu); void editOptions(bool topMenu);
DivSystem systemPicker(); DivSystem systemPicker(bool fullWidth);
void noteInput(int num, int key, int vol=-1); void noteInput(int num, int key, int vol=-1);
void valueInput(int num, bool direct=false, int target=-1); void valueInput(int num, bool direct=false, int target=-1);
void orderInput(int num); void orderInput(int num);
@ -2940,6 +2944,10 @@ class FurnaceGUI {
void doUndoSample(); void doUndoSample();
void doRedoSample(); void doRedoSample();
void checkRecordInstrumentUndoStep();
void doUndoInstrument();
void doRedoInstrument();
void play(int row=0); void play(int row=0);
void setOrder(unsigned char order, bool forced=false); void setOrder(unsigned char order, bool forced=false);
void stop(); void stop();

View file

@ -474,8 +474,8 @@ const FurnaceGUIColors fxColors[256]={
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_VOLUME,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_VOLUME,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,
GUI_COLOR_PATTERN_EFFECT_INVALID, GUI_COLOR_PATTERN_EFFECT_INVALID,

View file

@ -6438,6 +6438,7 @@ void FurnaceGUI::drawInsEdit() {
ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(canvasW,canvasH)); ImGui::SetNextWindowSizeConstraints(ImVec2(440.0f*dpiScale,400.0f*dpiScale),ImVec2(canvasW,canvasH));
} }
if (ImGui::Begin("Instrument Editor",&insEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking),_("Instrument Editor"))) { if (ImGui::Begin("Instrument Editor",&insEditOpen,globalWinFlags|(settings.allowEditDocking?0:ImGuiWindowFlags_NoDocking),_("Instrument Editor"))) {
DivInstrument* ins=NULL;
if (curIns==-2) { if (curIns==-2) {
ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()+ImGui::GetStyle().ItemSpacing.y)*0.5f); ImGui::SetCursorPosY(ImGui::GetCursorPosY()+(ImGui::GetContentRegionAvail().y-ImGui::GetFrameHeightWithSpacing()+ImGui::GetStyle().ItemSpacing.y)*0.5f);
CENTER_TEXT(_("waiting...")); CENTER_TEXT(_("waiting..."));
@ -6465,6 +6466,7 @@ void FurnaceGUI::drawInsEdit() {
curIns=i; curIns=i;
wavePreviewInit=true; wavePreviewInit=true;
updateFMPreview=true; updateFMPreview=true;
ins = e->song.ins[curIns];
} }
} }
ImGui::EndCombo(); ImGui::EndCombo();
@ -6487,7 +6489,7 @@ void FurnaceGUI::drawInsEdit() {
ImGui::EndTable(); ImGui::EndTable();
} }
} else { } else {
DivInstrument* ins=e->song.ins[curIns]; ins=e->song.ins[curIns];
if (updateFMPreview) { if (updateFMPreview) {
renderFMPreview(ins); renderFMPreview(ins);
updateFMPreview=false; updateFMPreview=false;
@ -8146,6 +8148,8 @@ void FurnaceGUI::drawInsEdit() {
macroList.push_back(FurnaceGUIMacroDesc(_("Noise"),&ins->std.dutyMacro,0,8,160,uiColors[GUI_COLOR_MACRO_NOISE])); macroList.push_back(FurnaceGUIMacroDesc(_("Noise"),&ins->std.dutyMacro,0,8,160,uiColors[GUI_COLOR_MACRO_NOISE]));
} }
macroList.push_back(FurnaceGUIMacroDesc(_("Waveform"),&ins->std.waveMacro,0,waveCount,160,uiColors[GUI_COLOR_MACRO_WAVE],false,NULL,NULL,false,NULL)); macroList.push_back(FurnaceGUIMacroDesc(_("Waveform"),&ins->std.waveMacro,0,waveCount,160,uiColors[GUI_COLOR_MACRO_WAVE],false,NULL,NULL,false,NULL));
macroList.push_back(FurnaceGUIMacroDesc(_("Panning (left)"),&ins->std.panLMacro,0,15,46,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL));
macroList.push_back(FurnaceGUIMacroDesc(_("Panning (right)"),&ins->std.panRMacro,0,15,46,uiColors[GUI_COLOR_MACRO_OTHER]));
macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode)); macroList.push_back(FurnaceGUIMacroDesc(_("Pitch"),&ins->std.pitchMacro,-2048,2047,160,uiColors[GUI_COLOR_MACRO_PITCH],true,macroRelativeMode));
macroList.push_back(FurnaceGUIMacroDesc(_("Phase Reset"),&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true)); macroList.push_back(FurnaceGUIMacroDesc(_("Phase Reset"),&ins->std.phaseResetMacro,0,1,32,uiColors[GUI_COLOR_MACRO_OTHER],false,NULL,NULL,true));
break; break;
@ -8703,6 +8707,63 @@ void FurnaceGUI::drawInsEdit() {
ImGui::EndPopup(); ImGui::EndPopup();
} }
} }
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_EDIT; if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_INS_EDIT;
ImGui::End(); ImGui::End();
} }
void FurnaceGUI::checkRecordInstrumentUndoStep() {
if (insEditOpen && curIns>=0 && curIns<(int)e->song.ins.size()) {
DivInstrument* ins=e->song.ins[curIns];
// invalidate cachedCurIns/any possible changes if the cachedCurIns was referencing a different
// instrument altgoether
bool insChanged=ins!=cachedCurInsPtr;
if (insChanged) {
insEditMayBeDirty=false;
cachedCurInsPtr=ins;
cachedCurIns=*ins;
}
cachedCurInsPtr=ins;
// check against the last cached to see if diff -- note that modifications to instruments
// happen outside drawInsEdit (e.g. cursor inputs are processed and can directly modify
// macro data). but don't check until we think the user input is complete.
bool delayDiff=ImGui::IsMouseDown(ImGuiMouseButton_Left) || ImGui::IsMouseDown(ImGuiMouseButton_Right) || ImGui::GetIO().WantCaptureKeyboard;
if (!delayDiff && insEditMayBeDirty) {
bool hasChange=ins->recordUndoStepIfChanged(e->processTime, &cachedCurIns);
if (hasChange) {
cachedCurIns=*ins;
}
insEditMayBeDirty=false;
}
} else {
cachedCurInsPtr=NULL;
insEditMayBeDirty=false;
}
}
void FurnaceGUI::doUndoInstrument() {
if (!insEditOpen) return;
if (curIns<0 || curIns>=(int)e->song.ins.size()) return;
DivInstrument* ins=e->song.ins[curIns];
// is locking the engine necessary? copied from doUndoSample
e->lockEngine([this,ins]() {
ins->undo();
cachedCurInsPtr=ins;
cachedCurIns=*ins;
});
}
void FurnaceGUI::doRedoInstrument() {
if (!insEditOpen) return;
if (curIns<0 || curIns>=(int)e->song.ins.size()) return;
DivInstrument* ins=e->song.ins[curIns];
// is locking the engine necessary? copied from doRedoSample
e->lockEngine([this,ins]() {
ins->redo();
cachedCurInsPtr=ins;
cachedCurIns=*ins;
});
}

View file

@ -1507,6 +1507,7 @@ void FurnaceGUI::drawPattern() {
i.cmd==DIV_CMD_HINT_PORTA || i.cmd==DIV_CMD_HINT_PORTA ||
i.cmd==DIV_CMD_HINT_LEGATO || i.cmd==DIV_CMD_HINT_LEGATO ||
i.cmd==DIV_CMD_HINT_VOL_SLIDE || i.cmd==DIV_CMD_HINT_VOL_SLIDE ||
i.cmd==DIV_CMD_HINT_VOL_SLIDE_TARGET ||
i.cmd==DIV_CMD_HINT_ARPEGGIO || i.cmd==DIV_CMD_HINT_ARPEGGIO ||
i.cmd==DIV_CMD_HINT_PITCH || i.cmd==DIV_CMD_HINT_PITCH ||
i.cmd==DIV_CMD_HINT_VIBRATO || i.cmd==DIV_CMD_HINT_VIBRATO ||

View file

@ -1084,15 +1084,19 @@ void FurnaceGUI::drawSettings() {
ImGui::PushID(i); ImGui::PushID(i);
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(_("Invert")).x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(_("Invert")).x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0);
if (ImGui::BeginCombo("##System",getSystemName(sysID))) { if (ImGui::BeginCombo("##System",getSystemName(sysID),ImGuiComboFlags_HeightLargest)) {
for (int j=0; availableSystems[j]; j++) {
if (ImGui::Selectable(getSystemName((DivSystem)availableSystems[j]),sysID==availableSystems[j])) { sysID=systemPicker(true);
sysID=(DivSystem)availableSystems[j];
settings.initialSys.set(fmt::sprintf("id%d",i),(int)e->systemToFileFur(sysID)); if (sysID!=DIV_SYSTEM_NULL)
settings.initialSys.set(fmt::sprintf("flags%d",i),""); {
settingsChanged=true; settings.initialSys.set(fmt::sprintf("id%d",i),(int)e->systemToFileFur(sysID));
} settings.initialSys.set(fmt::sprintf("flags%d",i),"");
settingsChanged=true;
ImGui::CloseCurrentPopup();
} }
ImGui::EndCombo(); ImGui::EndCombo();
} }

View file

@ -22,18 +22,23 @@
// NOTE: please don't ask me to enable text wrap. // NOTE: please don't ask me to enable text wrap.
// Dear ImGui doesn't have that feature. D: // Dear ImGui doesn't have that feature. D:
void FurnaceGUI::drawNotes() { void FurnaceGUI::drawNotes(bool asChild) {
if (nextWindow==GUI_WINDOW_NOTES) { if (nextWindow==GUI_WINDOW_NOTES) {
notesOpen=true; notesOpen=true;
ImGui::SetNextWindowFocus(); ImGui::SetNextWindowFocus();
nextWindow=GUI_WINDOW_NOTHING; nextWindow=GUI_WINDOW_NOTHING;
} }
if (!notesOpen) return; if (!notesOpen && !asChild) return;
if (ImGui::Begin("Song Comments",&notesOpen,globalWinFlags,_("Song Comments"))) { bool began=asChild?ImGui::BeginChild("Song Info##Song Information"):ImGui::Begin("Song Comments",&notesOpen,globalWinFlags,_("Song Comments"));
if (began) {
if (ImGui::InputTextMultiline("##SongNotes",&e->song.notes,ImGui::GetContentRegionAvail(),ImGuiInputTextFlags_UndoRedo)) { if (ImGui::InputTextMultiline("##SongNotes",&e->song.notes,ImGui::GetContentRegionAvail(),ImGuiInputTextFlags_UndoRedo)) {
MARK_MODIFIED; MARK_MODIFIED;
} }
} }
if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_NOTES; if (!asChild && ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_NOTES;
ImGui::End(); if (asChild) {
ImGui::EndChild();
} else {
ImGui::End();
}
} }

View file

@ -102,7 +102,7 @@ void FurnaceGUI::drawSysManager() {
ImGui::SameLine(); ImGui::SameLine();
ImGui::Button(_("Change##SysChange")); ImGui::Button(_("Change##SysChange"));
if (ImGui::BeginPopupContextItem("SysPickerC",ImGuiPopupFlags_MouseButtonLeft)) { if (ImGui::BeginPopupContextItem("SysPickerC",ImGuiPopupFlags_MouseButtonLeft)) {
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
if (e->changeSystem(i,picked,preserveChanPos)) { if (e->changeSystem(i,picked,preserveChanPos)) {
MARK_MODIFIED; MARK_MODIFIED;
@ -138,7 +138,7 @@ void FurnaceGUI::drawSysManager() {
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::Button(ICON_FA_PLUS "##SysAdd"); ImGui::Button(ICON_FA_PLUS "##SysAdd");
if (ImGui::BeginPopupContextItem("SysPickerA",ImGuiPopupFlags_MouseButtonLeft)) { if (ImGui::BeginPopupContextItem("SysPickerA",ImGuiPopupFlags_MouseButtonLeft)) {
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
if (!e->addSystem(picked)) { if (!e->addSystem(picked)) {
showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError())); showError(fmt::sprintf(_("cannot add chip! (%s)"),e->getLastError()));

View file

@ -23,7 +23,7 @@
#include "guiConst.h" #include "guiConst.h"
#include <imgui.h> #include <imgui.h>
DivSystem FurnaceGUI::systemPicker() { DivSystem FurnaceGUI::systemPicker(bool fullWidth) {
DivSystem ret=DIV_SYSTEM_NULL; DivSystem ret=DIV_SYSTEM_NULL;
DivSystem hoveredSys=DIV_SYSTEM_NULL; DivSystem hoveredSys=DIV_SYSTEM_NULL;
bool reissueSearch=false; bool reissueSearch=false;
@ -61,7 +61,7 @@ DivSystem FurnaceGUI::systemPicker() {
} }
} }
} }
if (ImGui::BeginTable("SysList",1,ImGuiTableFlags_ScrollY,ImVec2(500.0f*dpiScale,200.0*dpiScale))) { if (ImGui::BeginTable("SysList",1,ImGuiTableFlags_ScrollY,ImVec2(fullWidth ? ImGui::GetContentRegionAvail().x : 500.0f*dpiScale,200.0f*dpiScale))) {
if (sysSearchQuery.empty()) { if (sysSearchQuery.empty()) {
// display chip list // display chip list
for (int j=0; curSysSection[j]; j++) { for (int j=0; curSysSection[j]; j++) {

View file

@ -392,7 +392,7 @@ void FurnaceGUI::drawUserPresets() {
tempID=fmt::sprintf("%s##USystem",getSystemName(chip.sys)); tempID=fmt::sprintf("%s##USystem",getSystemName(chip.sys));
ImGui::Button(tempID.c_str(),ImVec2(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(_("Invert")).x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0,0)); ImGui::Button(tempID.c_str(),ImVec2(ImGui::GetContentRegionAvail().x-ImGui::CalcTextSize(_("Invert")).x-ImGui::GetFrameHeightWithSpacing()*2.0-ImGui::GetStyle().ItemSpacing.x*2.0,0));
if (ImGui::BeginPopupContextItem("SysPickerCU",ImGuiPopupFlags_MouseButtonLeft)) { if (ImGui::BeginPopupContextItem("SysPickerCU",ImGuiPopupFlags_MouseButtonLeft)) {
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
chip.sys=picked; chip.sys=picked;
mustBake=true; mustBake=true;
@ -456,7 +456,7 @@ void FurnaceGUI::drawUserPresets() {
ImGui::Button(ICON_FA_PLUS "##SysAddU"); ImGui::Button(ICON_FA_PLUS "##SysAddU");
if (ImGui::BeginPopupContextItem("SysPickerU",ImGuiPopupFlags_MouseButtonLeft)) { if (ImGui::BeginPopupContextItem("SysPickerU",ImGuiPopupFlags_MouseButtonLeft)) {
DivSystem picked=systemPicker(); DivSystem picked=systemPicker(false);
if (picked!=DIV_SYSTEM_NULL) { if (picked!=DIV_SYSTEM_NULL) {
preset->orig.push_back(FurnaceGUISysDefChip(picked,1.0f,0.0f,"")); preset->orig.push_back(FurnaceGUISysDefChip(picked,1.0f,0.0f,""));
mustBake=true; mustBake=true;