diff --git a/papers/format.md b/papers/format.md index af2c4a5c3..fc954ba05 100644 --- a/papers/format.md +++ b/papers/format.md @@ -29,6 +29,7 @@ furthermore, an `or reserved` indicates this field is always present, but is res the format versions are: +- 71: Furnace dev71 - 70: Furnace dev70 - 69: Furnace dev69 - 68: Furnace dev68 @@ -238,7 +239,10 @@ size | description | this is 2.0f for modules before 59 --- | **extended compatibility flags** (>=70) 1 | broken speed selection - 31 | reserved + 1 | no slides on first tick (>=71) or reserved + 1 | next row reset arp pos (>=71) or reserved + 1 | ignore jump at end (>=71) or reserved + 28 | reserved ``` # instrument diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 48f88bfa1..2d7b84d95 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -144,7 +144,7 @@ void DivEngine::walkSong(int& loopOrder, int& loopRow, int& loopEnd) { effectVal=pat[k]->data[j][5+(l<<1)]; if (effectVal<0) effectVal=0; if (pat[k]->data[j][4+(l<<1)]==0x0d) { - if (nextOrder==-1 && i24) { @@ -825,6 +828,11 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<69) { ds.arp0Reset=false; } + if (ds.version<71) { + ds.noSlidesOnFirstTick=false; + ds.rowResetsArpPos=false; + ds.ignoreJumpAtEnd=true; + } ds.isDMF=false; reader.readS(); // reserved @@ -1071,7 +1079,16 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version>=70) { // extended compat flags ds.brokenSpeedSel=reader.readC(); - for (int i=0; i<31; i++) { + if (ds.version>=71) { + song.noSlidesOnFirstTick=reader.readC(); + song.rowResetsArpPos=reader.readC(); + song.ignoreJumpAtEnd=reader.readC(); + } else { + reader.readC(); + reader.readC(); + reader.readC(); + } + for (int i=0; i<28; i++) { reader.readC(); } } @@ -1282,6 +1299,9 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { DivSong ds; ds.tuning=436.0; ds.version=DIV_VERSION_MOD; + ds.noSlidesOnFirstTick=true; + ds.rowResetsArpPos=true; + ds.ignoreJumpAtEnd=false; int insCount=31; bool bypassLimits=false; @@ -1916,7 +1936,10 @@ SafeWriter* DivEngine::saveFur(bool notPrimary) { // extended compat flags w->writeC(song.brokenSpeedSel); - for (int i=0; i<31; i++) { + w->writeC(song.noSlidesOnFirstTick); + w->writeC(song.rowResetsArpPos); + w->writeC(song.ignoreJumpAtEnd); + for (int i=0; i<28; i++) { w->writeC(0); } diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 1d7f24175..22e95b02d 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -821,7 +821,7 @@ void DivEngine::processRow(int i, bool afterDelay) { } break; case 0x0d: // next order - if (changeOrd<0 && curOrder<(song.ordersLen-1)) { + if (changeOrd<0 && (curOrder<(song.ordersLen-1) || !song.ignoreJumpAtEnd)) { changeOrd=-2; changePos=effectVal; } @@ -1229,6 +1229,7 @@ void DivEngine::nextRow() { } if (haltOn==DIV_HALT_ROW) halted=true; + firstTick=true; } bool DivEngine::nextTick(bool noAccum) { @@ -1281,23 +1282,25 @@ bool DivEngine::nextTick(bool noAccum) { keyHit[i]=true; } } - 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].volume>chan[i].volMax) { - chan[i].volume=chan[i].volMax; - chan[i].volSpeed=0; - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); - } else if (chan[i].volume<0) { - chan[i].volSpeed=0; - if (song.legacyVolumeSlides) { - chan[i].volume=chan[i].volMax+1; + if (!song.noSlidesOnFirstTick || !firstTick) { + 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].volume>chan[i].volMax) { + chan[i].volume=chan[i].volMax; + chan[i].volSpeed=0; + dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); + } else if (chan[i].volume<0) { + chan[i].volSpeed=0; + if (song.legacyVolumeSlides) { + chan[i].volume=chan[i].volMax+1; + } else { + chan[i].volume=0; + } + dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } else { - chan[i].volume=0; + dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); - } else { - dispatchCmd(DivCommand(DIV_CMD_VOLUME,i,chan[i].volume>>8)); } } if (chan[i].vibratoDepth>0) { @@ -1315,13 +1318,15 @@ bool DivEngine::nextTick(bool noAccum) { break; } } - if ((chan[i].keyOn || chan[i].keyOff) && chan[i].portaSpeed>0) { - if (dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed,chan[i].portaNote))==2 && chan[i].portaStop && song.targetResetsSlides) { - chan[i].portaSpeed=0; - chan[i].oldNote=chan[i].note; - chan[i].note=chan[i].portaNote; - chan[i].inPorta=false; - dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + if (!song.noSlidesOnFirstTick || !firstTick) { + if ((chan[i].keyOn || chan[i].keyOff) && chan[i].portaSpeed>0) { + if (dispatchCmd(DivCommand(DIV_CMD_NOTE_PORTA,i,chan[i].portaSpeed,chan[i].portaNote))==2 && chan[i].portaStop && song.targetResetsSlides) { + chan[i].portaSpeed=0; + chan[i].oldNote=chan[i].note; + chan[i].note=chan[i].portaNote; + chan[i].inPorta=false; + dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); + } } } if (chan[i].cut>0) { @@ -1354,6 +1359,9 @@ bool DivEngine::nextTick(bool noAccum) { dispatchCmd(DivCommand(DIV_CMD_LEGATO,i,chan[i].note)); chan[i].resetArp=false; } + if (song.rowResetsArpPos && firstTick) { + chan[i].arpStage=-1; + } if (chan[i].arp!=0 && !chan[i].arpYield && chan[i].portaSpeed<1) { if (--chan[i].arpTicks<1) { chan[i].arpTicks=song.arpLen; @@ -1377,6 +1385,8 @@ bool DivEngine::nextTick(bool noAccum) { } } + firstTick=false; + // system tick for (int i=0; itick(); diff --git a/src/engine/song.h b/src/engine/song.h index c6d52840b..19a89cfa4 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -302,6 +302,9 @@ struct DivSong { bool newInsTriggersInPorta; bool arp0Reset; bool brokenSpeedSel; + bool noSlidesOnFirstTick; + bool rowResetsArpPos; + bool ignoreJumpAtEnd; DivOrders orders; std::vector ins; @@ -375,7 +378,10 @@ struct DivSong { oneTickCut(false), newInsTriggersInPorta(true), arp0Reset(true), - brokenSpeedSel(false) { + brokenSpeedSel(false), + noSlidesOnFirstTick(false), + rowResetsArpPos(false), + ignoreJumpAtEnd(false) { for (int i=0; i<32; i++) { system[i]=DIV_SYSTEM_NULL; systemVol[i]=64; diff --git a/src/gui/compatFlags.cpp b/src/gui/compatFlags.cpp index b49c251f8..ca0538ea6 100644 --- a/src/gui/compatFlags.cpp +++ b/src/gui/compatFlags.cpp @@ -85,6 +85,18 @@ void FurnaceGUI::drawCompatFlags() { if (ImGui::IsItemHovered()) { ImGui::SetTooltip("determines next speed based on whether the row is odd/even instead of alternating between speeds."); } + ImGui::Checkbox("Don't slide on the first tick of a row",&e->song.noSlidesOnFirstTick); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulates ProTracker's behavior of not applying volume/pitch slides on the first tick of a row."); + } + ImGui::Checkbox("Reset arpeggio position on row change",&e->song.rowResetsArpPos); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("simulates ProTracker's behavior of arpeggio being bound to the current tick of a row."); + } + ImGui::Checkbox("Ignore 0Dxx on the last order",&e->song.ignoreJumpAtEnd); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("if this is on, a jump to next row effect will not take place when it is on the last order of a song."); + } ImGui::Text("Loop modality:"); if (ImGui::RadioButton("Reset channels",e->song.loopModality==0)) {