From 10172e0489d7194b036e48ff2817112b78c31ec2 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 13 Oct 2023 19:57:36 -0500 Subject: [PATCH] GUI: better DPCM mapping, part 2 --- doc/4-instrument/nes.md | 37 ++++++++++++++++++++++++++++++- doc/4-instrument/sample.md | 2 +- papers/format.md | 3 ++- papers/newIns.md | 23 ++++++++++++++++++++ src/engine/fileOps.cpp | 14 ++++++++++-- src/engine/instrument.h | 30 ++++++++++++++++++++++++-- src/engine/platform/nes.cpp | 43 ++++++++++++++++++++++++++++++++----- src/engine/platform/nes.h | 2 ++ src/engine/song.h | 4 +++- src/gui/compatFlags.cpp | 4 ++++ src/gui/insEdit.cpp | 34 ++++++++++++++++++++++++++--- 11 files changed, 180 insertions(+), 16 deletions(-) diff --git a/doc/4-instrument/nes.md b/doc/4-instrument/nes.md index 53c8560f6..64f5c0d08 100644 --- a/doc/4-instrument/nes.md +++ b/doc/4-instrument/nes.md @@ -1,6 +1,41 @@ # NES instrument editor -the instrument editor for NES consists of these macros: +the NES instrument editor consists of two tabs. + +## DPCM + +this tab is somewhat similar to [the Sample instrument editor](sample.md), but it has been tailored for use with NES' DPCM channel. + +- **Sample**: specifies which sample should be assigned to the instrument. +- **Use sample map**: enables mapping different samples to notes. see next section for more information. + - when this option is disabled, 16 notes (from C-0 to D#1 and repeating) will map to the DPCM channel's 16 possible pitches. + +### sample map + +the sample map allows you to set a sample for each note. + +after enabling this option, a table appears with the contents of the sample map. +- the first column represents the input note. +- the second column allows you to type in a sample number for each note. + - you may press Delete to clear it. +- the third one is used to set the DPCM pitch at which the specified sample will play. + - for possible values, refer to the table below. + - you may press Delete to clear it. if no value is specified, the last pitch is used. +- the fourth column allows you to set the initial delta counter value when playing the sample. + - this is an hexadecimal number. + - the range is `00` to `7F`. + - you may press Delete to clear it. if no value is specified, the delta counter isn't altered. +- the fifth and last column provides a combo box for selecting a sample. + +you may right-click anywhere in the number, pitch and delta columns for additional options: +- **set entire map to this pitch**: sets the DPCM pitch of all notes to the selected cell's. +- **set entire map to this delta counter value**: sets the initial delta counter value of all notes to the selected cell's. +- **set entire map to this sample**: sets the sample number of all notes to the selected cell's. +- **reset pitches**: resets the sample map's DPCM pitches to defaults (15). +- **clear delta counter values**: removes all delta counter values from the map. +- **clear map samples**: removes all samples from the map. + +## Macros - **Volume**: volume sequence. - **Arpeggio**: pitch sequence. diff --git a/doc/4-instrument/sample.md b/doc/4-instrument/sample.md index 8c3f7545e..e3c29fb48 100644 --- a/doc/4-instrument/sample.md +++ b/doc/4-instrument/sample.md @@ -4,7 +4,7 @@ the Generic Sample instrument editor consists of a sample selector and several m ## Sample -- **Initial sample**: specifies which sample should be assigned to the instrument. +- **Sample**: specifies which sample should be assigned to the instrument. - **Use wavetable**: uses wavetable instead of a sample. - only available in Amiga and Generic PCM DAC. - **Use sample map**: enables mapping different samples to notes. see next section for more information. diff --git a/papers/format.md b/papers/format.md index 175183ee4..16d447c71 100644 --- a/papers/format.md +++ b/papers/format.md @@ -360,7 +360,8 @@ size | description 1 | broken portamento during legato 1 | broken macro during note off in some FM chips (>=155) 1 | pre note (C64) does not compensate for portamento or legato (>=168) - 5 | reserved + 1 | disable new NES DPCM features (>=183) + 4 | reserved --- | **speed pattern of first song** (>=139) 1 | length of speed pattern (fail if this is lower than 0 or higher than 16) 16 | speed pattern (this overrides speed 1 and speed 2 settings) diff --git a/papers/newIns.md b/papers/newIns.md index 7b948d23d..557bb7119 100644 --- a/papers/newIns.md +++ b/papers/newIns.md @@ -145,6 +145,7 @@ the following feature codes are recognized: - `SU`: Sound Unit ins data - `ES`: ES5506 ins data - `X1`: X1-010 ins data +- `NE`: NES DPCM sample map data - `EN`: end of features - if you find this feature code, stop reading the instrument. - it will usually appear only when there sample/wave lists. @@ -585,3 +586,25 @@ size | description -----|------------------------------------ 4 | bank slot ``` + +# NES DPCM sample map data (NE) + +``` +size | description +-----|------------------------------------ + 1 | use sample map + 2?? | DPCM sample map... (120 entries) + | - only read if sample map is enabled +``` + +the DPCM sample map format: + +``` +size | description +-----|------------------------------------ + 1 | pitch (0-15; otherwise no change) + 1 | delta counter value (0-127; otherwise no change) +``` + +if some fields are missing, that's because they are defined in the SM feature. +NES instruments with DPCM sample maps have both SM and NE features. diff --git a/src/engine/fileOps.cpp b/src/engine/fileOps.cpp index 6f7065ef4..14dfe18ff 100644 --- a/src/engine/fileOps.cpp +++ b/src/engine/fileOps.cpp @@ -184,6 +184,7 @@ bool DivEngine::loadDMF(unsigned char* file, size_t len) { ds.snNoLowPeriods=true; ds.disableSampleMacro=true; ds.preNoteNoEffect=true; + ds.oldDPCM=true; ds.delayBehavior=0; ds.jumpTreatment=2; @@ -1856,6 +1857,9 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { if (ds.version<168) { ds.preNoteNoEffect=true; } + if (ds.version<183) { + ds.oldDPCM=true; + } ds.isDMF=false; reader.readS(); // reserved @@ -2374,7 +2378,12 @@ bool DivEngine::loadFur(unsigned char* file, size_t len) { } else { reader.readC(); } - for (int i=0; i<5; i++) { + if (ds.version>=183) { + ds.oldDPCM=reader.readC(); + } else { + reader.readC(); + } + for (int i=0; i<4; i++) { reader.readC(); } } @@ -5438,7 +5447,8 @@ SafeWriter* DivEngine::saveFur(bool notPrimary, bool newPatternFormat) { w->writeC(song.brokenPortaLegato); w->writeC(song.brokenFMOff); w->writeC(song.preNoteNoEffect); - for (int i=0; i<5; i++) { + w->writeC(song.oldDPCM); + for (int i=0; i<4; i++) { w->writeC(0); } diff --git a/src/engine/instrument.h b/src/engine/instrument.h index 41b61ae48..477962dfa 100644 --- a/src/engine/instrument.h +++ b/src/engine/instrument.h @@ -486,8 +486,8 @@ struct DivInstrumentAmiga { } /** - * get the sample frequency at specified note. - * @return the frequency, or -1 if not using note map. + * get the sample playback note at specified note. + * @return the note, or -1 if not using note map. */ inline int getFreq(int note) { if (useNoteMap) { @@ -498,6 +498,32 @@ struct DivInstrumentAmiga { return note; } + /** + * get the DPCM pitch at specified note. + * @return the pitch, or -1 if not using note map. + */ + inline signed char getDPCMFreq(int note) { + if (useNoteMap) { + if (note<0) note=0; + if (note>119) note=119; + return noteMap[note].dpcmFreq; + } + return -1; + } + + /** + * get the DPCM delta counter value at specified note. + * @return the delta counter value, or -1 if not using note map. + */ + inline signed char getDPCMDelta(int note) { + if (useNoteMap) { + if (note<0) note=0; + if (note>119) note=119; + return noteMap[note].dpcmDelta; + } + return -1; + } + DivInstrumentAmiga(): initSample(0), useNoteMap(false), diff --git a/src/engine/platform/nes.cpp b/src/engine/platform/nes.cpp index 92f7edfa8..32319fcbb 100644 --- a/src/engine/platform/nes.cpp +++ b/src/engine/platform/nes.cpp @@ -343,6 +343,10 @@ void DivPlatformNES::tick(bool sysTick) { } else { rWrite(0x4010,calcDPCMRate(dacRate)|(goingToLoop?0x40:0)); } + if (nextDPCMDelta>=0) { + rWrite(0x4011,nextDPCMDelta); + nextDPCMDelta=-1; + } rWrite(0x4012,(dpcmAddr>>6)&0xff); rWrite(0x4013,dpcmLen&0xff); rWrite(0x4015,31); @@ -373,11 +377,38 @@ int DivPlatformNES::dispatch(DivCommand c) { switch (c.cmd) { case DIV_CMD_NOTE_ON: if (c.chan==4) { // PCM - DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_STD); - if (ins->type==DIV_INS_AMIGA) { + DivInstrument* ins=parent->getIns(chan[c.chan].ins,DIV_INS_NES); + if (ins->type==DIV_INS_AMIGA || (ins->type==DIV_INS_NES && !parent->song.oldDPCM)) { + if (ins->type==DIV_INS_NES) { + if (!dpcmMode) { + dpcmMode=true; + if (dumpWrites) addWrite(0xffff0002,0); + dacSample=-1; + rWrite(0x4015,15); + rWrite(0x4010,0); + rWrite(0x4012,0); + rWrite(0x4013,0); + rWrite(0x4015,31); + } + + if (ins->amiga.useNoteMap) { + nextDPCMFreq=ins->amiga.getDPCMFreq(c.value); + if (nextDPCMFreq<0 || nextDPCMFreq>15) nextDPCMFreq=lastDPCMFreq; + lastDPCMFreq=nextDPCMFreq; + nextDPCMDelta=ins->amiga.getDPCMDelta(c.value); + } else { + if (c.value==DIV_NOTE_NULL) { + nextDPCMFreq=lastDPCMFreq; + } else { + nextDPCMFreq=c.value&15; + } + } + } if (c.value!=DIV_NOTE_NULL) { dacSample=ins->amiga.getSample(c.value); - c.value=ins->amiga.getFreq(c.value); + if (ins->type==DIV_INS_AMIGA) { + c.value=ins->amiga.getFreq(c.value); + } } if (dacSample<0 || dacSample>=parent->song.sampleLen) { dacSample=-1; @@ -452,7 +483,7 @@ int DivPlatformNES::dispatch(DivCommand c) { } chan[c.chan].active=true; chan[c.chan].keyOn=true; - chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); + chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_NES)); if (!parent->song.brokenOutVol && !chan[c.chan].std.vol.will) { chan[c.chan].outVol=chan[c.chan].vol; } @@ -614,7 +645,7 @@ int DivPlatformNES::dispatch(DivCommand c) { break; case DIV_CMD_PRE_PORTA: if (chan[c.chan].active && c.value2) { - if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_STD)); + if (parent->song.resetMacroOnPorta) chan[c.chan].macroInit(parent->getIns(chan[c.chan].ins,DIV_INS_NES)); } if (!chan[c.chan].inPorta && c.value && !parent->song.brokenPortaArp && chan[c.chan].std.arp.will && !NEW_ARP_STRAT) chan[c.chan].baseFreq=NOTE_PERIODIC(chan[c.chan].note); chan[c.chan].inPorta=c.value; @@ -700,6 +731,8 @@ void DivPlatformNES::reset() { goingToLoop=false; countMode=false; nextDPCMFreq=-1; + nextDPCMDelta=-1; + lastDPCMFreq=15; linearCount=255; if (useNP) { diff --git a/src/engine/platform/nes.h b/src/engine/platform/nes.h index da0a848dd..48ed6a1b8 100644 --- a/src/engine/platform/nes.h +++ b/src/engine/platform/nes.h @@ -54,6 +54,8 @@ class DivPlatformNES: public DivDispatch { unsigned char apuType; unsigned char linearCount; signed char nextDPCMFreq; + signed char nextDPCMDelta; + signed char lastDPCMFreq; bool dpcmMode; bool dpcmModeDefault; bool dacAntiClickOn; diff --git a/src/engine/song.h b/src/engine/song.h index ec316464c..00a7dbbf6 100644 --- a/src/engine/song.h +++ b/src/engine/song.h @@ -377,6 +377,7 @@ struct DivSong { bool brokenPortaLegato; bool brokenFMOff; bool preNoteNoEffect; + bool oldDPCM; std::vector ins; std::vector wave; @@ -496,7 +497,8 @@ struct DivSong { patchbayAuto(true), brokenPortaLegato(false), brokenFMOff(false), - preNoteNoEffect(false) { + preNoteNoEffect(false), + oldDPCM(false) { for (int i=0; isong.oldDPCM); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("behavior changed in 0.6.1"); + } ImGui::EndTabItem(); } if (ImGui::BeginTabItem(".mod import")) { diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index 71b56d005..3d158b7af 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -2313,6 +2313,16 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) { if (ins->type==DIV_INS_SU) sampleTabName="Sound Unit"; if (ins->type==DIV_INS_NES) sampleTabName="DPCM"; if (ImGui::BeginTabItem(sampleTabName)) { + if (ins->type==DIV_INS_NES && e->song.oldDPCM) { + ImGui::Text("new DPCM features disabled (compatibility)!"); + if (ImGui::Button("click here to enable them.")) { + e->song.oldDPCM=false; + MARK_MODIFIED; + } + ImGui::EndTabItem(); + return; + } + String sName; bool wannaOpenSMPopup=false; if (ins->amiga.initSample<0 || ins->amiga.initSample>=e->song.sampleLen) { @@ -2401,7 +2411,7 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) { ImGui::Text("#"); if (ins->type==DIV_INS_NES) { ImGui::TableNextColumn(); - ImGui::Text("freq"); + ImGui::Text("pitch"); ImGui::TableNextColumn(); ImGui::Text("delta"); } else { @@ -2674,9 +2684,21 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) { if (ImGui::BeginPopup("SampleMapUtils",ImGuiWindowFlags_NoMove|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings)) { if (sampleMapSelStart==sampleMapSelEnd && sampleMapSelStart>=0 && sampleMapSelStart<120) { if (ins->type==DIV_INS_NES) { - if (ImGui::MenuItem("set entire map to this frequency")) { + if (ImGui::MenuItem("set entire map to this pitch")) { + if (sampleMapSelStart>=0 && sampleMapSelStart<120) { + for (int i=0; i<120; i++) { + if (i==sampleMapSelStart) continue; + ins->amiga.noteMap[i].dpcmFreq=ins->amiga.noteMap[sampleMapSelStart].dpcmFreq; + } + } } if (ImGui::MenuItem("set entire map to this delta counter value")) { + if (sampleMapSelStart>=0 && sampleMapSelStart<120) { + for (int i=0; i<120; i++) { + if (i==sampleMapSelStart) continue; + ins->amiga.noteMap[i].dpcmDelta=ins->amiga.noteMap[sampleMapSelStart].dpcmDelta; + } + } } } else { if (ImGui::MenuItem("set entire map to this note")) { @@ -2698,9 +2720,15 @@ void FurnaceGUI::insTabSample(DivInstrument* ins) { } } if (ins->type==DIV_INS_NES) { - if (ImGui::MenuItem("clear frequencies")) { + if (ImGui::MenuItem("reset pitches")) { + for (int i=0; i<120; i++) { + ins->amiga.noteMap[i].dpcmFreq=15; + } } if (ImGui::MenuItem("clear delta counter values")) { + for (int i=0; i<120; i++) { + ins->amiga.noteMap[i].dpcmDelta=-1; + } } } else { if (ImGui::MenuItem("reset notes")) {