From 5b145b71210e46f9741cb87132f8074da5defe49 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 19:36:13 -0500 Subject: [PATCH 01/11] multiple instrument playback, part 1 not implemented for MIDI yet --- CMakeLists.txt | 3 +- src/engine/engine.cpp | 6 +-- src/engine/engine.h | 2 +- src/gui/commandPalette.cpp | 2 +- src/gui/dataList.cpp | 48 +++++++++++++++++-- src/gui/doAction.cpp | 28 ++++++++---- src/gui/editing.cpp | 4 +- src/gui/gui.cpp | 67 +++++++++++++++++++++++---- src/gui/gui.h | 24 +++++++++- src/gui/guiConst.cpp | 9 ++++ src/gui/insEdit.cpp | 6 +-- src/gui/multiInsSetup.cpp | 94 ++++++++++++++++++++++++++++++++++++++ src/gui/piano.cpp | 5 ++ src/gui/settings.cpp | 10 ++++ src/gui/sysEx.cpp | 2 +- 15 files changed, 276 insertions(+), 34 deletions(-) create mode 100644 src/gui/multiInsSetup.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d37334bdd..5dbdc9041 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -969,6 +969,7 @@ src/gui/about.cpp src/gui/channels.cpp src/gui/chanOsc.cpp src/gui/clock.cpp +src/gui/commandPalette.cpp src/gui/compatFlags.cpp src/gui/csPlayer.cpp src/gui/cursor.cpp @@ -988,8 +989,8 @@ src/gui/log.cpp src/gui/memory.cpp src/gui/mixer.cpp src/gui/midiMap.cpp +src/gui/multiInsSetup.cpp src/gui/newSong.cpp -src/gui/commandPalette.cpp src/gui/orders.cpp src/gui/osc.cpp src/gui/patManager.cpp diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 98b518456..e69ca25eb 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -3665,7 +3665,7 @@ int DivEngine::getViableChannel(int chan, int off, int ins) { return (chan+off)%chans; } -bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol) { +bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol, int transpose) { bool isViable[DIV_MAX_CHANS]; bool canPlayAnyway=false; bool notInViableChannel=false; @@ -3708,7 +3708,7 @@ bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol) { if ((!midiPoly) || (isViable[finalChan] && chan[finalChan].midiNote==-1 && (insInst->type==DIV_INS_OPL || getChannelType(finalChan)==finalChanType || notInViableChannel))) { chan[finalChan].midiNote=note; chan[finalChan].midiAge=midiAgeCounter++; - pendingNotes.push_back(DivNoteEvent(finalChan,ins,note,vol,true)); + pendingNotes.push_back(DivNoteEvent(finalChan,ins,note+transpose,vol,true)); return true; } if (++finalChan>=chans) { @@ -3729,7 +3729,7 @@ bool DivEngine::autoNoteOn(int ch, int ins, int note, int vol) { chan[candidate].midiNote=note; chan[candidate].midiAge=midiAgeCounter++; - pendingNotes.push_back(DivNoteEvent(candidate,ins,note,vol,true)); + pendingNotes.push_back(DivNoteEvent(candidate,ins,note+transpose,vol,true)); return true; } diff --git a/src/engine/engine.h b/src/engine/engine.h index 81698fdf6..21414afbc 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -1195,7 +1195,7 @@ class DivEngine { void noteOff(int chan); // returns whether it could - bool autoNoteOn(int chan, int ins, int note, int vol=-1); + bool autoNoteOn(int chan, int ins, int note, int vol=-1, int transpose=0); void autoNoteOff(int chan, int note, int vol=-1); void autoNoteOffAll(); diff --git a/src/gui/commandPalette.cpp b/src/gui/commandPalette.cpp index f55673db8..49e708078 100644 --- a/src/gui/commandPalette.cpp +++ b/src/gui/commandPalette.cpp @@ -367,7 +367,7 @@ void FurnaceGUI::drawPalette() { openRecentFile(recentFile[i]); break; case CMDPAL_TYPE_INSTRUMENTS: - curIns=i-1; + setCurIns(i-1); break; case CMDPAL_TYPE_SAMPLES: curSample=i; diff --git a/src/gui/dataList.cpp b/src/gui/dataList.cpp index f8a38f1d6..0c99b9f61 100644 --- a/src/gui/dataList.cpp +++ b/src/gui/dataList.cpp @@ -150,10 +150,42 @@ void FurnaceGUI::insListItem(int i, int dir, int asset) { } else { ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); } - bool insReleased=ImGui::Selectable(name.c_str(),(i==-1)?(curIns<0 || curIns>=e->song.insLen):(curIns==i)); + bool insSelected=(curIns==i); + int multiInsColor=-1; + if (i==-1) { + insSelected=(curIns<0 || curIns>=e->song.insLen); + } else { + for (int j=0; j<7; j++) { + if (multiIns[j]==i) { + insSelected=true; + multiInsColor=j; + break; + } + } + } + + // push multi ins colors if necessary + if (multiInsColor>=0) { + ImVec4 colorActive=uiColors[GUI_COLOR_MULTI_INS_1+multiInsColor]; + ImVec4 colorHover=ImVec4(colorActive.x,colorActive.y,colorActive.z,colorActive.w*0.5); + ImVec4 color=ImVec4(colorActive.x,colorActive.y,colorActive.z,colorActive.w*0.25); + ImGui::PushStyleColor(ImGuiCol_Header,color); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered,colorHover); + ImGui::PushStyleColor(ImGuiCol_HeaderActive,colorActive); + } + + bool insReleased=ImGui::Selectable(name.c_str(),insSelected); + + if (multiInsColor>=0) { + ImGui::PopStyleColor(3); + } bool insPressed=ImGui::IsItemActivated(); if (insReleased || (!insListDir && insPressed && !settings.draggableDataView)) { - curIns=i; + if (mobileMultiInsToggle || ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift)) { + setMultiIns(i); + } else { + setCurIns(i); + } if (!insReleased || insListDir || settings.draggableDataView) { wavePreviewInit=true; updateFMPreview=true; @@ -181,7 +213,7 @@ void FurnaceGUI::insListItem(int i, int dir, int asset) { } if (ImGui::BeginPopupContextItem("InsRightMenu")) { - curIns=i; + setCurIns(i); updateFMPreview=true; ImGui::PushStyleColor(ImGuiCol_Text,uiColors[GUI_COLOR_TEXT]); if (ImGui::MenuItem(_("edit"))) { @@ -732,6 +764,16 @@ void FurnaceGUI::drawInsList(bool asChild) { } } + if (mobileUI) { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + pushToggleColors(mobileMultiInsToggle); + if (ImGui::SmallButton("Select multiple")) { + mobileMultiInsToggle=!mobileMultiInsToggle; + } + popToggleColors(); + } + if (settings.unifiedDataView) { ImGui::Unindent(); diff --git a/src/gui/doAction.cpp b/src/gui/doAction.cpp index 7061396dc..1a3358b96 100644 --- a/src/gui/doAction.cpp +++ b/src/gui/doAction.cpp @@ -143,16 +143,18 @@ void FurnaceGUI::doAction(int what) { } break; case GUI_ACTION_INS_UP: - if (--curIns<-1) { - curIns=-1; + setCurIns(curIns-1); + if (curIns<-1) { + setCurIns(-1); } wavePreviewInit=true; wantScrollListIns=true; updateFMPreview=true; break; case GUI_ACTION_INS_DOWN: - if (++curIns>=(int)e->song.ins.size()) { - curIns=((int)e->song.ins.size())-1; + setCurIns(curIns+1); + if (curIns>=(int)e->song.ins.size()) { + setCurIns(((int)e->song.ins.size())-1); } wavePreviewInit=true; wantScrollListIns=true; @@ -360,6 +362,9 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_WINDOW_REF_PLAYER: nextWindow=GUI_WINDOW_REF_PLAYER; break; + case GUI_ACTION_WINDOW_MULTI_INS_SETUP: + nextWindow=GUI_WINDOW_MULTI_INS_SETUP; + break; case GUI_ACTION_COLLAPSE_WINDOW: collapseWindow=true; @@ -471,6 +476,9 @@ void FurnaceGUI::doAction(int what) { case GUI_WINDOW_REF_PLAYER: refPlayerOpen=false; break; + case GUI_WINDOW_MULTI_INS_SETUP: + multiInsSetupOpen=false; + break; default: break; } @@ -739,7 +747,7 @@ void FurnaceGUI::doAction(int what) { break; } } - curIns=e->addInstrument(cursor.xCoarse); + setCurIns(e->addInstrument(cursor.xCoarse)); if (curIns==-1) { showError(_("too many instruments!")); } else { @@ -768,7 +776,7 @@ void FurnaceGUI::doAction(int what) { case GUI_ACTION_INS_LIST_DUPLICATE: if (curIns>=0 && curIns<(int)e->song.ins.size()) { int prevIns=curIns; - curIns=e->addInstrument(cursor.xCoarse); + setCurIns(e->addInstrument(cursor.xCoarse)); if (curIns==-1) { showError(_("too many instruments!")); } else { @@ -820,13 +828,15 @@ void FurnaceGUI::doAction(int what) { insEditOpen=true; break; case GUI_ACTION_INS_LIST_UP: - if (--curIns<0) curIns=0; + setCurIns(curIns-1); + if (curIns<0) setCurIns(0); wantScrollListIns=true; wavePreviewInit=true; updateFMPreview=true; break; case GUI_ACTION_INS_LIST_DOWN: - if (++curIns>=(int)e->song.ins.size()) curIns=((int)e->song.ins.size())-1; + setCurIns(curIns+1); + if (curIns>=(int)e->song.ins.size()) setCurIns(((int)e->song.ins.size())-1); wantScrollListIns=true; wavePreviewInit=true; updateFMPreview=true; @@ -1715,7 +1725,7 @@ void FurnaceGUI::doAction(int what) { } DivSample* sample=e->song.sample[curSample]; - curIns=e->addInstrument(cursor.xCoarse); + setCurIns(e->addInstrument(cursor.xCoarse)); if (curIns==-1) { showError(_("too many instruments!")); } else { diff --git a/src/gui/editing.cpp b/src/gui/editing.cpp index 9df50c377..b05152b86 100644 --- a/src/gui/editing.cpp +++ b/src/gui/editing.cpp @@ -1912,7 +1912,7 @@ void FurnaceGUI::doAbsorbInstrument() { // absorb most recent instrument if (!foundIns && pat->newData[i][DIV_PAT_INS] >= 0) { foundIns=true; - curIns=pat->newData[i][DIV_PAT_INS]; + setCurIns(pat->newData[i][DIV_PAT_INS]); } // absorb most recent octave (i.e. set curOctave such that the "main row" (QWERTY) of @@ -1932,7 +1932,7 @@ void FurnaceGUI::doAbsorbInstrument() { } // if no instrument has been set at this point, the only way to match it is to use "none" - if (!foundIns) curIns=-1; + if (!foundIns) setCurIns(-1); logD("doAbsorbInstrument -- searched %d orders", curOrder-orderIdx); } diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 302b07dfa..57c0aa4bb 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -868,6 +868,38 @@ void FurnaceGUI::autoDetectSystem() { } } +void FurnaceGUI::setCurIns(int newIns) { curIns=newIns; + memset(multiIns,-1,7*sizeof(int)); +} + +bool FurnaceGUI::setMultiIns(int newIns) { + // set primary instrument if not set + if (curIns<0) { + setCurIns(newIns); + return true; + } + // don't allow using the primary instrument twice + if (curIns==newIns) return false; + for (int i=0; i<7; i++) { + // don't allow using an instrument more than once + if (multiIns[i]==newIns) return false; + // if slot is empty, assign instrument to it + if (multiIns[i]==-1) { + multiIns[i]=newIns; + return true; + } + } + // no more slots available + return false; +} + +bool FurnaceGUI::isMultiInsActive() { + for (int i=0; i<7; i++) { + if (multiIns[i]>=0) return true; + } + return false; +} + void FurnaceGUI::updateROMExportAvail() { memset(romExportAvail,0,sizeof(bool)*DIV_ROM_MAX); romExportExists=false; @@ -1333,6 +1365,11 @@ void FurnaceGUI::previewNote(int refChan, int note, bool autoNote) { e->setMidiBaseChan(refChan); e->synchronized([this,note]() { if (!e->autoNoteOn(-1,curIns,note)) failedNoteOn=true; + for (int mi=0; mi<7; mi++) { + if (multiIns[mi]!=-1) { + e->autoNoteOn(-1,multiIns[mi],note,-1,multiInsTranspose[mi]); + } + } }); } @@ -1469,7 +1506,7 @@ void FurnaceGUI::valueInput(int num, bool direct, int target) { } } if (settings.absorbInsInput) { - curIns=pat->newData[y][target]; + setCurIns(pat->newData[y][target]); wavePreviewInit=true; updateFMPreview=true; } @@ -1977,7 +2014,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (curIns!=-2) { prevIns=curIns; } - curIns=-2; + setCurIns(-2); } } for (DivInstrument* i: instruments) delete i; @@ -3858,6 +3895,7 @@ bool FurnaceGUI::loop() { DECLARE_METRIC(effectList) DECLARE_METRIC(userPresets) DECLARE_METRIC(refPlayer) + DECLARE_METRIC(multiInsSetup) DECLARE_METRIC(popup) #ifdef IS_MOBILE @@ -4045,7 +4083,7 @@ bool FurnaceGUI::loop() { instrumentCount=e->addInstrumentPtr(i); } if (instrumentCount>=0 && settings.selectAssetOnLoad) { - curIns=instrumentCount-1; + setCurIns(instrumentCount-1); } nextWindow=GUI_WINDOW_INS_LIST; MARK_MODIFIED; @@ -4239,8 +4277,8 @@ bool FurnaceGUI::loop() { break; case TA_MIDI_PROGRAM: if (midiMap.programChange && !(midiMap.directChannel && midiMap.directProgram)) { - curIns=msg.data[0]; - if (curIns>=(int)e->song.ins.size()) curIns=e->song.ins.size()-1; + setCurIns(msg.data[0]); + if (curIns>=(int)e->song.ins.size()) setCurIns(e->song.ins.size()-1); wavePreviewInit=true; updateFMPreview=true; } @@ -4480,6 +4518,7 @@ bool FurnaceGUI::loop() { IMPORT_CLOSE(csPlayerOpen); IMPORT_CLOSE(userPresetsOpen); IMPORT_CLOSE(refPlayerOpen); + IMPORT_CLOSE(multiInsSetupOpen); } else if (pendingLayoutImportStep==1) { // let the UI settle } else if (pendingLayoutImportStep==2) { @@ -4852,6 +4891,7 @@ bool FurnaceGUI::loop() { if (ImGui::MenuItem(_("play/edit controls"),BIND_FOR(GUI_ACTION_WINDOW_EDIT_CONTROLS),editControlsOpen)) editControlsOpen=!editControlsOpen; if (ImGui::MenuItem(_("piano/input pad"),BIND_FOR(GUI_ACTION_WINDOW_PIANO),pianoOpen)) pianoOpen=!pianoOpen; if (ImGui::MenuItem(_("reference music player"),BIND_FOR(GUI_ACTION_WINDOW_REF_PLAYER),refPlayerOpen)) refPlayerOpen=!refPlayerOpen; + if (ImGui::MenuItem(_("multi-ins setup"),BIND_FOR(GUI_ACTION_WINDOW_MULTI_INS_SETUP),multiInsSetupOpen)) multiInsSetupOpen=!multiInsSetupOpen; if (spoilerOpen) if (ImGui::MenuItem(_("spoiler"),NULL,spoilerOpen)) spoilerOpen=!spoilerOpen; ImGui::EndMenu(); @@ -5052,6 +5092,7 @@ bool FurnaceGUI::loop() { MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); MEASURE(refPlayer,drawRefPlayer()); + MEASURE(multiInsSetup,drawMultiInsSetup()); MEASURE(patManager,drawPatManager()); } else { @@ -5098,6 +5139,7 @@ bool FurnaceGUI::loop() { MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); MEASURE(refPlayer,drawRefPlayer()); + MEASURE(multiInsSetup,drawMultiInsSetup()); } @@ -5167,7 +5209,7 @@ bool FurnaceGUI::loop() { } } } else { - curIns=prevIns; + setCurIns(prevIns); wavePreviewInit=true; updateFMPreview=true; } @@ -5673,7 +5715,7 @@ bool FurnaceGUI::loop() { MARK_MODIFIED; } if (instrumentCount>=0 && settings.selectAssetOnLoad) { - curIns=instrumentCount-1; + setCurIns(instrumentCount-1); } } } @@ -6550,7 +6592,7 @@ bool FurnaceGUI::loop() { e->lockEngine([this]() { e->song.clearInstruments(); }); - curIns=-1; + setCurIns(-1); MARK_MODIFIED; ImGui::CloseCurrentPopup(); } @@ -6755,7 +6797,7 @@ bool FurnaceGUI::loop() { strncpy(temp,insTypes[i][0],1023); if (ImGui::MenuItem(temp)) { // create ins - curIns=e->addInstrument(-1,i); + setCurIns(e->addInstrument(-1,i)); if (curIns==-1) { showError(_("too many instruments!")); } else { @@ -8249,6 +8291,7 @@ void FurnaceGUI::syncState() { spoilerOpen=e->getConfBool("spoilerOpen",false); userPresetsOpen=e->getConfBool("userPresetsOpen",false); refPlayerOpen=e->getConfBool("refPlayerOpen",false); + multiInsSetupOpen=e->getConfBool("multiInsSetupOpen",false); insListDir=e->getConfBool("insListDir",false); waveListDir=e->getConfBool("waveListDir",false); @@ -8414,6 +8457,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("spoilerOpen",spoilerOpen); conf.set("userPresetsOpen",userPresetsOpen); conf.set("refPlayerOpen",refPlayerOpen); + conf.set("multiInsSetupOpen",multiInsSetupOpen); // commit dir state conf.set("insListDir",insListDir); @@ -8769,6 +8813,7 @@ FurnaceGUI::FurnaceGUI(): curPaletteChoice(0), curPaletteType(0), soloTimeout(0.0f), + mobileMultiInsToggle(false), purgeYear(2021), purgeMonth(4), purgeDay(4), @@ -8818,6 +8863,7 @@ FurnaceGUI::FurnaceGUI(): cvOpen(false), userPresetsOpen(false), refPlayerOpen(false), + multiInsSetupOpen(false), cvNotSerious(false), shortIntro(false), insListDir(false), @@ -9285,6 +9331,9 @@ FurnaceGUI::FurnaceGUI(): memset(romExportAvail,0,sizeof(bool)*DIV_ROM_MAX); + memset(multiIns,-1,7*sizeof(int)); + memset(multiInsTranspose,0,7*sizeof(int)); + strncpy(noteOffLabel,"OFF",32); strncpy(noteRelLabel,"===",32); strncpy(macroRelLabel,"REL",32); diff --git a/src/gui/gui.h b/src/gui/gui.h index f9ebdac2f..8618ef51b 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -309,6 +309,14 @@ enum FurnaceGUIColors { GUI_COLOR_MACRO_ENVELOPE, GUI_COLOR_MACRO_GLOBAL, + GUI_COLOR_MULTI_INS_1, + GUI_COLOR_MULTI_INS_2, + GUI_COLOR_MULTI_INS_3, + GUI_COLOR_MULTI_INS_4, + GUI_COLOR_MULTI_INS_5, + GUI_COLOR_MULTI_INS_6, + GUI_COLOR_MULTI_INS_7, + GUI_COLOR_INSTR_STD, GUI_COLOR_INSTR_FM, GUI_COLOR_INSTR_GB, @@ -568,6 +576,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_CS_PLAYER, GUI_WINDOW_USER_PRESETS, GUI_WINDOW_REF_PLAYER, + GUI_WINDOW_MULTI_INS_SETUP, GUI_WINDOW_SPOILER }; @@ -774,6 +783,7 @@ enum FurnaceGUIActions { GUI_ACTION_WINDOW_CS_PLAYER, GUI_ACTION_WINDOW_USER_PRESETS, GUI_ACTION_WINDOW_REF_PLAYER, + GUI_ACTION_WINDOW_MULTI_INS_SETUP, GUI_ACTION_COLLAPSE_WINDOW, GUI_ACTION_CLOSE_WINDOW, @@ -2376,13 +2386,19 @@ class FurnaceGUI { int pendingLayoutImportStep; FixedQueue pendingLayoutImportReopen; - int curIns, curWave, curSample, curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, soloChan, orderEditMode, orderCursor; + // do not set curIns directly! use setCurIns() instead. + int curIns, curWave, curSample; + int curOctave, curOrder, playOrder, prevIns, oldRow, editStep, editStepCoarse, soloChan, orderEditMode, orderCursor; int isClipping, newSongCategory, latchTarget, undoOrder; int wheelX, wheelY, dragSourceX, dragSourceXFine, dragSourceY, dragSourceOrder, dragDestinationX, dragDestinationXFine, dragDestinationY, dragDestinationOrder, oldBeat, oldBar; int curGroove, exitDisabledTimer; int curPaletteChoice, curPaletteType; float soloTimeout; + int multiIns[7]; + int multiInsTranspose[7]; + bool mobileMultiInsToggle; + int purgeYear, purgeMonth, purgeDay; bool patExtraButtons, patChannelNames, patChannelPairs; @@ -2395,6 +2411,7 @@ class FurnaceGUI { bool pianoOpen, notesOpen, channelsOpen, regViewOpen, logOpen, effectListOpen, chanOscOpen; bool subSongsOpen, findOpen, spoilerOpen, patManagerOpen, sysManagerOpen, clockOpen, speedOpen; bool groovesOpen, xyOscOpen, memoryOpen, csPlayerOpen, cvOpen, userPresetsOpen, refPlayerOpen; + bool multiInsSetupOpen; bool cvNotSerious; @@ -3000,6 +3017,7 @@ class FurnaceGUI { void drawXYOsc(); void drawUserPresets(); void drawRefPlayer(); + void drawMultiInsSetup(); float drawSystemChannelInfo(const DivSysDef* whichDef, int keyHitOffset=-1, float width=-1.0f); void drawSystemChannelInfoText(const DivSysDef* whichDef); @@ -3155,6 +3173,10 @@ class FurnaceGUI { const char* getSystemName(DivSystem which); const char* getSystemPartNumber(DivSystem sys, DivConfig& flags); + void setCurIns(int newIns); + bool setMultiIns(int newIns); + bool isMultiInsActive(); + public: void editStr(String* which); void showWarning(String what, FurnaceGUIWarnings type); diff --git a/src/gui/guiConst.cpp b/src/gui/guiConst.cpp index f34292887..5d9f50605 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -657,6 +657,7 @@ const FurnaceGUIActionDef guiActions[GUI_ACTION_MAX]={ D("WINDOW_CS_PLAYER", _N("Command Stream Player"), 0), D("WINDOW_USER_PRESETS", _N("User Presets"), 0), D("WINDOW_REF_PLAYER", _N("Reference Music Player"), 0), + D("MULTI_INS_SETUP", _N("Multi-Instrument Setup"), 0), D("COLLAPSE_WINDOW", _N("Collapse/expand current window"), 0), D("CLOSE_WINDOW", _N("Close current window"), FURKMOD_SHIFT|SDLK_ESCAPE), @@ -1005,6 +1006,14 @@ const FurnaceGUIColorDef guiColors[GUI_COLOR_MAX]={ D(GUI_COLOR_MACRO_ENVELOPE,"",ImVec4(0.0f,1.0f,0.5f,1.0f)), D(GUI_COLOR_MACRO_GLOBAL,"",ImVec4(1.0f,0.1f,0.1f,1.0f)), + D(GUI_COLOR_MULTI_INS_1,"",ImVec4(0.2f,1.0f,1.0f,1.0f)), + D(GUI_COLOR_MULTI_INS_2,"",ImVec4(0.2f,1.0f,0.2f,1.0f)), + D(GUI_COLOR_MULTI_INS_3,"",ImVec4(0.7f,1.0f,0.2f,1.0f)), + D(GUI_COLOR_MULTI_INS_4,"",ImVec4(1.0f,0.7f,0.2f,1.0f)), + D(GUI_COLOR_MULTI_INS_5,"",ImVec4(1.0f,0.2f,0.2f,1.0f)), + D(GUI_COLOR_MULTI_INS_6,"",ImVec4(1.0f,0.2f,1.0f,1.0f)), + D(GUI_COLOR_MULTI_INS_7,"",ImVec4(0.4f,0.2f,1.0f,1.0f)), + D(GUI_COLOR_INSTR_STD,"",ImVec4(0.6f,1.0f,0.5f,1.0f)), D(GUI_COLOR_INSTR_FM,"",ImVec4(0.6f,0.9f,1.0f,1.0f)), D(GUI_COLOR_INSTR_GB,"",ImVec4(1.0f,1.0f,0.5f,1.0f)), diff --git a/src/gui/insEdit.cpp b/src/gui/insEdit.cpp index bb8c9b525..5dca05399 100644 --- a/src/gui/insEdit.cpp +++ b/src/gui/insEdit.cpp @@ -6649,10 +6649,10 @@ void FurnaceGUI::drawInsEdit() { for (size_t i=0; isong.ins.size(); i++) { name=fmt::sprintf("%.2X: %s##_INSS%d",i,e->song.ins[i]->name,i); if (ImGui::Selectable(name.c_str(),curIns==(int)i)) { - curIns=i; + setCurIns(i); wavePreviewInit=true; updateFMPreview=true; - ins = e->song.ins[curIns]; + ins=e->song.ins[curIns]; } } ImGui::EndCombo(); @@ -6700,7 +6700,7 @@ void FurnaceGUI::drawInsEdit() { for (size_t i=0; isong.ins.size(); i++) { name=fmt::sprintf("%.2X: %s##_INSS%d",i,e->song.ins[i]->name,i); if (ImGui::Selectable(name.c_str(),curIns==(int)i)) { - curIns=i; + setCurIns(i); ins=e->song.ins[curIns]; wavePreviewInit=true; updateFMPreview=true; diff --git a/src/gui/multiInsSetup.cpp b/src/gui/multiInsSetup.cpp new file mode 100644 index 000000000..e056b4e50 --- /dev/null +++ b/src/gui/multiInsSetup.cpp @@ -0,0 +1,94 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2025 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 "gui.h" +#include "imgui.h" +#include "IconsFontAwesome4.h" + +void FurnaceGUI::drawMultiInsSetup() { + if (nextWindow==GUI_WINDOW_MULTI_INS_SETUP) { + multiInsSetupOpen=true; + ImGui::SetNextWindowFocus(); + nextWindow=GUI_WINDOW_NOTHING; + } + if (!multiInsSetupOpen && !isMultiInsActive()) return; + if (ImGui::Begin("Multi-Ins Setup",isMultiInsActive()?NULL:&multiInsSetupOpen,globalWinFlags,_("Multi-Ins Setup"))) { + if (ImGui::BeginTable("MultiInsSlots",8,ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + for (int i=0; i<8; i++) { + ImGui::TableNextColumn(); + ImGui::Text("%d",i+1); + } + ImGui::TableNextRow(); + for (int i=0; i<8; i++) { + String id; + int tr=(i==0)?0:multiInsTranspose[i-1]; + bool thisInsOn=(i==0)?true:(multiIns[i-1]!=-1); + if (tr==0) { + id=fmt::sprintf("±%d###TrAmount",tr); + } else if (tr>0) { + id=fmt::sprintf("+%d###TrAmount",tr); + } else { + id=fmt::sprintf("%d###TrAmount",tr); + } + ImGui::TableNextColumn(); + ImGui::PushID(i); + + ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat,true); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); + if (ImGui::Button(ICON_FA_CHEVRON_UP "##Up",ImVec2(ImGui::GetContentRegionAvail().x,0))) { + if (i>0) multiInsTranspose[i-1]++; + } + ImGui::PopStyleVar(); + ImGui::PopItemFlag(); + + if (i>0) { + ImVec4 colorActive=uiColors[GUI_COLOR_MULTI_INS_1+i-1]; + ImVec4 colorHover=ImVec4(colorActive.x,colorActive.y,colorActive.z,colorActive.w*0.5); + ImVec4 color=ImVec4(colorActive.x,colorActive.y,colorActive.z,colorActive.w*0.25); + ImGui::PushStyleColor(ImGuiCol_Header,color); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered,colorHover); + ImGui::PushStyleColor(ImGuiCol_HeaderActive,colorActive); + } + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign,ImVec2(0.5f,0.5f)); + ImGui::Selectable(id.c_str(),thisInsOn,0,ImVec2( + ImGui::GetContentRegionAvail().x, + ImGui::GetContentRegionAvail().y-ImGui::GetTextLineHeightWithSpacing() + )); + ImGui::PopStyleVar(); + if (i>0) { + ImGui::PopStyleColor(3); + } + + ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat,true); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,ImVec2(0.0f,0.0f)); + if (ImGui::Button(ICON_FA_CHEVRON_DOWN "##Down",ImVec2(ImGui::GetContentRegionAvail().x,0))) { + if (i>0) multiInsTranspose[i-1]--; + } + ImGui::PopStyleVar(); + ImGui::PopItemFlag(); + + ImGui::PopID(); + } + ImGui::EndTable(); + } + } + if (ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) curWindow=GUI_WINDOW_MULTI_INS_SETUP; + ImGui::End(); +} diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp index 186a1b718..18ef208bb 100644 --- a/src/gui/piano.cpp +++ b/src/gui/piano.cpp @@ -429,6 +429,11 @@ void FurnaceGUI::drawPiano() { } else { e->synchronized([this,note]() { if (!e->autoNoteOn(-1,curIns,note)) failedNoteOn=true; + for (int mi=0; mi<7; mi++) { + if (multiIns[mi]!=-1) { + e->autoNoteOn(-1,multiIns[mi],note,-1,multiInsTranspose[mi]); + } + } }); if (edit && curWindow!=GUI_WINDOW_INS_LIST && curWindow!=GUI_WINDOW_INS_EDIT) noteInput(note,0); } diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 9971d5f08..1cbade92b 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -4166,6 +4166,16 @@ void FurnaceGUI::drawSettings() { UI_COLOR_CONFIG(GUI_COLOR_MACRO_HIGHLIGHT,_("Step Highlight")); ImGui::TreePop(); } + if (ImGui::TreeNode(_("Multi-instrument Play"))) { + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_1,_("Second instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_2,_("Third instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_3,_("Fourth instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_4,_("Fifth instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_5,_("Sixth instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_6,_("Seventh instrument")); + UI_COLOR_CONFIG(GUI_COLOR_MULTI_INS_7,_("Eighth instrument")); + ImGui::TreePop(); + } if (ImGui::TreeNode(_("Instrument Types"))) { UI_COLOR_CONFIG(GUI_COLOR_INSTR_FM,_("FM (OPN)")); UI_COLOR_CONFIG(GUI_COLOR_INSTR_STD,_("SN76489/Sega PSG")); diff --git a/src/gui/sysEx.cpp b/src/gui/sysEx.cpp index aa86363f8..24e5643c8 100644 --- a/src/gui/sysEx.cpp +++ b/src/gui/sysEx.cpp @@ -233,7 +233,7 @@ bool FurnaceGUI::parseSysEx(unsigned char* data, size_t len) { for (DivInstrument* i: instruments) { logI("got instrument from MIDI: %s",i->name); e->addInstrumentPtr(i); - curIns=e->song.insLen-1; + setCurIns(e->song.insLen-1); } } catch (EndOfFileException e) { logW("end of data already?"); From 465328c952d4b0906f90451eb1c7ca651e8b1ce3 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 20:12:37 -0500 Subject: [PATCH 02/11] GUI: set order to free pattern on middle-click --- src/engine/pattern.cpp | 9 +++++++++ src/engine/pattern.h | 6 ++++++ src/gui/orders.cpp | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/engine/pattern.cpp b/src/engine/pattern.cpp index cce45e65c..74dd72f3b 100644 --- a/src/engine/pattern.cpp +++ b/src/engine/pattern.cpp @@ -84,6 +84,15 @@ void DivChannelData::wipePatterns() { } } +bool DivPattern::isEmpty() { + for (int i=0; iname=name; memcpy(dest->newData,newData,sizeof(newData)); diff --git a/src/engine/pattern.h b/src/engine/pattern.h index ff2417fef..3402b01c5 100644 --- a/src/engine/pattern.h +++ b/src/engine/pattern.h @@ -39,6 +39,12 @@ struct DivPattern { */ short newData[DIV_MAX_ROWS][DIV_MAX_COLS]; + /** + * check whether this pattern is empty. + * @return whether it is. + */ + bool isEmpty(); + /** * clear the pattern. */ diff --git a/src/gui/orders.cpp b/src/gui/orders.cpp index 2f85a69b0..8d1ed9f08 100644 --- a/src/gui/orders.cpp +++ b/src/gui/orders.cpp @@ -407,6 +407,43 @@ void FurnaceGUI::drawOrders() { if (!pat->name.empty() && ImGui::IsItemHovered()) { ImGui::SetTooltip("%s",pat->name.c_str()); } + bool findFreePat=ImGui::IsItemClicked(ImGuiMouseButton_Middle); + if (ImGui::IsItemHovered() && CHECK_LONG_HOLD) { + NOTIFY_LONG_HOLD; + findFreePat=true; + } + if (findFreePat) { + // find free pattern and assign it + prepareUndo(GUI_UNDO_CHANGE_ORDER); + e->lockSave([this,i,j]() { + bool foundOne=false; + bool available[DIV_MAX_PATTERNS]; + memset(available,1,DIV_MAX_PATTERNS*sizeof(bool)); + for (int k=0; kcurSubSong->ordersLen; k++) { + available[e->curOrders->ord[j][k]]=false; + } + for (int k=0; kcurPat[j].data[k]==NULL) { + e->curOrders->ord[j][i]=k; + foundOne=true; + break; + } else { + // check whether this pattern is empty and accept it if so + DivPattern* p=e->curPat[j].getPattern(k,false); + if (p->isEmpty()) { + e->curOrders->ord[j][i]=k; + foundOne=true; + break; + } + } + } + if (!foundOne) showError(_("no free patterns available on this channel!")); + }); + makeUndo(GUI_UNDO_CHANGE_ORDER); + } if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { if (curOrder==i) { if (orderEditMode==0) { From a843d0e2acac697b6847f4822f63184a1addf0d3 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 20:16:40 -0500 Subject: [PATCH 03/11] document middle click feature --- doc/2-interface/order-list.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/2-interface/order-list.md b/doc/2-interface/order-list.md index d118c3fa4..065a590a0 100644 --- a/doc/2-interface/order-list.md +++ b/doc/2-interface/order-list.md @@ -8,7 +8,8 @@ along the top are the available channels. their abbreviations can be set in the along the left are the order numbers. the highlighted row follows the order the pattern view cursor is in. -each entry in the table is the pattern that will play during that order. these can be changed according to the order edit mode. +each cell in the table is the pattern that will play during that order. these can be changed according to the order edit mode. +middle-clicking on a cell will set it to a new, unique empty pattern. hovering over a pattern number will pop up a tooltip showing the name of that pattern, if it has one. From a7324cc931ed9fc89d0e85b484bb8f9e44d2b9b7 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 20:24:11 -0500 Subject: [PATCH 04/11] document multi-ins feature --- doc/2-interface/asset-list.md | 9 +++++++++ doc/8-advanced/multi-ins.md | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 doc/8-advanced/multi-ins.md diff --git a/doc/2-interface/asset-list.md b/doc/2-interface/asset-list.md index 49eb7d3e5..8770414a9 100644 --- a/doc/2-interface/asset-list.md +++ b/doc/2-interface/asset-list.md @@ -6,6 +6,15 @@ an "asset" refers to an instrument, wavetable or sample. ![instruments window](instruments.png) +this window displays the list of instruments. each entry contains an icon representing an instrument's type and its name. + +the following actions can be done when hovering on an entry: + +- left click to set it as the current instrument. +- double click to open the instrument editor. +- right click to open a menu with options. +- shift-left click to start multi-instrument playback. in this mode you will be able to play more than one instrument at once. see [multi-instrument](../8-advanced/multi-ins.md) for more information. + buttons from left to right: - **Add**: pops up a menu to select which type of instrument to add. if only one instrument type is available, the menu is skipped. diff --git a/doc/8-advanced/multi-ins.md b/doc/8-advanced/multi-ins.md new file mode 100644 index 000000000..30f22c17b --- /dev/null +++ b/doc/8-advanced/multi-ins.md @@ -0,0 +1,11 @@ +# multi-instrument playback + +the instrument list allows you to select more than one instrument by shift-clicking. doing so engages multi-instrument playback mode, where note input will play multiple instruments at once. + +the following window also appears, allowing you to set the note offset (in semitones) for each instrument: + +``` +TODO: IMAGE! +``` + +up to 8 instruments can be played at a time. From e23c11b2fd4e442a5c61afe5002fd6d96abf6ec1 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 20:44:11 -0500 Subject: [PATCH 05/11] GUI: prepare to add different piano label modes --- src/gui/gui.cpp | 4 ++++ src/gui/gui.h | 10 +++++++++- src/gui/piano.cpp | 25 ++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 57c0aa4bb..c1a90b5cc 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -8351,6 +8351,7 @@ void FurnaceGUI::syncState() { pianoOffsetEdit=e->getConfInt("pianoOffsetEdit",pianoOffsetEdit); pianoView=e->getConfInt("pianoView",pianoView); pianoInputPadMode=e->getConfInt("pianoInputPadMode",pianoInputPadMode); + pianoLabelsMode=e->getConfInt("pianoLabelsMode",pianoLabelsMode); chanOscCols=e->getConfInt("chanOscCols",3); chanOscAutoColsType=e->getConfInt("chanOscAutoColsType",0); @@ -8512,6 +8513,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("pianoOffsetEdit",pianoOffsetEdit); conf.set("pianoView",pianoView); conf.set("pianoInputPadMode",pianoInputPadMode); + conf.set("pianoLabelsMode",pianoLabelsMode); // commit per-chan osc state conf.set("chanOscCols",chanOscCols); @@ -9164,6 +9166,7 @@ FurnaceGUI::FurnaceGUI(): pianoOffsetEdit(9), pianoView(PIANO_LAYOUT_AUTOMATIC), pianoInputPadMode(PIANO_INPUT_PAD_SPLIT_AUTO), + pianoLabelsMode(PIANO_LABELS_OCTAVE), #else pianoOctaves(7), pianoOctavesEdit(4), @@ -9174,6 +9177,7 @@ FurnaceGUI::FurnaceGUI(): pianoOffsetEdit(6), pianoView(PIANO_LAYOUT_STANDARD), pianoInputPadMode(PIANO_INPUT_PAD_DISABLE), + pianoLabelsMode(PIANO_LABELS_OCTAVE), #endif hasACED(false), waveGenBaseShape(0), diff --git a/src/gui/gui.h b/src/gui/gui.h index 8618ef51b..b181bd8eb 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2777,13 +2777,21 @@ class FurnaceGUI { PIANO_INPUT_PAD_MAX }; + enum PianoLabelsMode { + PIANO_LABELS_OFF=0, + PIANO_LABELS_OCTAVE, + PIANO_LABELS_NOTE, + PIANO_LABELS_OCTAVE_C, + PIANO_LABELS_OCTAVE_NOTE + }; + int pianoOctaves, pianoOctavesEdit; bool pianoOptions, pianoSharePosition, pianoOptionsSet; float pianoKeyHit[180]; bool pianoKeyPressed[180]; bool pianoReadonly; int pianoOffset, pianoOffsetEdit; - int pianoView, pianoInputPadMode; + int pianoView, pianoInputPadMode, pianoLabelsMode; // effect sorting / searching bool effectsShow[10]; diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp index 18ef208bb..23c1d4d91 100644 --- a/src/gui/piano.cpp +++ b/src/gui/piano.cpp @@ -130,6 +130,24 @@ void FurnaceGUI::drawPiano() { pianoInputPadMode=PIANO_INPUT_PAD_SPLIT_VISIBLE; } ImGui::Unindent(); + ImGui::Text(_("Key labels:")); + ImGui::Indent(); + if (ImGui::RadioButton(_("Disabled"),pianoLabelsMode==PIANO_LABELS_OFF)) { + pianoLabelsMode=PIANO_LABELS_OFF; + } + if (ImGui::RadioButton(_("Octaves"),pianoLabelsMode==PIANO_LABELS_OCTAVE)) { + pianoLabelsMode=PIANO_LABELS_OCTAVE; + } + if (ImGui::RadioButton(_("Notes"),pianoLabelsMode==PIANO_LABELS_NOTE)) { + pianoLabelsMode=PIANO_LABELS_NOTE; + } + if (ImGui::RadioButton(_("Octaves (with C)"),pianoLabelsMode==PIANO_LABELS_OCTAVE_C)) { + pianoLabelsMode=PIANO_LABELS_OCTAVE_C; + } + if (ImGui::RadioButton(_("Notes + Octaves"),pianoLabelsMode==PIANO_LABELS_OCTAVE_NOTE)) { + pianoLabelsMode=PIANO_LABELS_OCTAVE_NOTE; + } + ImGui::Unindent(); ImGui::Checkbox(_("Share play/edit offset/range"),&pianoSharePosition); ImGui::Checkbox(_("Read-only (can't input notes)"),&pianoReadonly); ImGui::EndPopup(); @@ -268,7 +286,12 @@ void FurnaceGUI::drawPiano() { p1.x-=dpiScale; dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); if ((i%12)==0) { - String label=fmt::sprintf("%d",(note-60)/12); + String label=""; + switch (pianoLabelsMode) { + case PIANO_LABELS_OCTAVE: + label=fmt::sprintf("%d",(note-60)/12); + break; + } ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); pText.x-=labelSize.x*0.5f; From c07859376824742d3b83ec37845a698bca37ab2a Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Nov 2025 01:18:17 -0500 Subject: [PATCH 06/11] finish the labels --- src/gui/gui.h | 4 +++ src/gui/piano.cpp | 76 +++++++++++++++++++++++++++++++---------------- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/gui/gui.h b/src/gui/gui.h index b181bd8eb..0add23b69 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -2781,6 +2781,7 @@ class FurnaceGUI { PIANO_LABELS_OFF=0, PIANO_LABELS_OCTAVE, PIANO_LABELS_NOTE, + PIANO_LABELS_NOTE_C, PIANO_LABELS_OCTAVE_C, PIANO_LABELS_OCTAVE_NOTE }; @@ -2919,6 +2920,9 @@ class FurnaceGUI { ImVec2 calcPortSetSize(String label, int ins, int outs); bool portSet(String label, unsigned int portSetID, int ins, int outs, int activeIns, int activeOuts, int& clickedPort, std::map& portPos); + // piano + void pianoLabel(ImDrawList* dl, ImVec2& p0, ImVec2& p1, int note); + void updateWindowTitle(); void updateROMExportAvail(); void autoDetectSystem(); diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp index 23c1d4d91..cac916965 100644 --- a/src/gui/piano.cpp +++ b/src/gui/piano.cpp @@ -49,6 +49,46 @@ const bool isTopKey[12]={ } \ } +void FurnaceGUI::pianoLabel(ImDrawList* dl, ImVec2& p0, ImVec2& p1, int note) { + switch (pianoLabelsMode) { + case PIANO_LABELS_OFF: + return; + case PIANO_LABELS_OCTAVE: + case PIANO_LABELS_OCTAVE_C: + if (note%12) return; + } + String label=""; + float padding=0.0f; + switch (pianoLabelsMode) { + case PIANO_LABELS_OCTAVE: + label=fmt::sprintf("%d",(note-60)/12); + padding=ImGui::GetStyle().ItemSpacing.y; + break; + case PIANO_LABELS_NOTE: + label=noteNames[60+(note%12)][0]; + padding=ImGui::GetStyle().ItemSpacing.y; + break; + case PIANO_LABELS_NOTE_C: + if ((note%12)==0) { + label+=fmt::sprintf("%d\nC",(note-60)/12); + } else { + label=noteNames[60+(note%12)][0]; + } + break; + case PIANO_LABELS_OCTAVE_C: + label=fmt::sprintf("C\n%d",(note-60)/12); + break; + case PIANO_LABELS_OCTAVE_NOTE: + label=fmt::sprintf("%c\n%d",noteNames[60+(note%12)][0],(note-60)/12); + break; + } + ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); + ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); + pText.x-=labelSize.x*0.5f; + pText.y-=labelSize.y+padding; + dl->AddText(pText,0xff404040,label.c_str()); +} + void FurnaceGUI::drawPiano() { if (nextWindow==GUI_WINDOW_PIANO) { pianoOpen=true; @@ -132,19 +172,22 @@ void FurnaceGUI::drawPiano() { ImGui::Unindent(); ImGui::Text(_("Key labels:")); ImGui::Indent(); - if (ImGui::RadioButton(_("Disabled"),pianoLabelsMode==PIANO_LABELS_OFF)) { + if (ImGui::RadioButton(_("Off##keyLabel0"),pianoLabelsMode==PIANO_LABELS_OFF)) { pianoLabelsMode=PIANO_LABELS_OFF; } - if (ImGui::RadioButton(_("Octaves"),pianoLabelsMode==PIANO_LABELS_OCTAVE)) { + if (ImGui::RadioButton(_("Octaves##keyLabel1"),pianoLabelsMode==PIANO_LABELS_OCTAVE)) { pianoLabelsMode=PIANO_LABELS_OCTAVE; } - if (ImGui::RadioButton(_("Notes"),pianoLabelsMode==PIANO_LABELS_NOTE)) { + if (ImGui::RadioButton(_("Notes##keyLabel2"),pianoLabelsMode==PIANO_LABELS_NOTE)) { pianoLabelsMode=PIANO_LABELS_NOTE; } - if (ImGui::RadioButton(_("Octaves (with C)"),pianoLabelsMode==PIANO_LABELS_OCTAVE_C)) { + if (ImGui::RadioButton(_("Notes (with octave)##keyLabel3"),pianoLabelsMode==PIANO_LABELS_NOTE_C)) { + pianoLabelsMode=PIANO_LABELS_NOTE_C; + } + if (ImGui::RadioButton(_("Octaves (with C)##keyLabel4"),pianoLabelsMode==PIANO_LABELS_OCTAVE_C)) { pianoLabelsMode=PIANO_LABELS_OCTAVE_C; } - if (ImGui::RadioButton(_("Notes + Octaves"),pianoLabelsMode==PIANO_LABELS_OCTAVE_NOTE)) { + if (ImGui::RadioButton(_("Notes + Octaves##keyLabel5"),pianoLabelsMode==PIANO_LABELS_OCTAVE_NOTE)) { pianoLabelsMode=PIANO_LABELS_OCTAVE_NOTE; } ImGui::Unindent(); @@ -285,19 +328,7 @@ void FurnaceGUI::drawPiano() { ImVec2 p1=ImLerp(rect.Min,rect.Max,ImVec2((float)(i+1)/notes,1.0f)); p1.x-=dpiScale; dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); - if ((i%12)==0) { - String label=""; - switch (pianoLabelsMode) { - case PIANO_LABELS_OCTAVE: - label=fmt::sprintf("%d",(note-60)/12); - break; - } - ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); - ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); - pText.x-=labelSize.x*0.5f; - pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; - dl->AddText(pText,0xff404040,label.c_str()); - } + if (isTopKey[i%12]) pianoLabel(dl,p0,p1,note); } } else { int bottomNotes=7*oct; @@ -357,14 +388,7 @@ void FurnaceGUI::drawPiano() { p1.x-=dpiScale; dl->AddRectFilled(p0,p1,ImGui::ColorConvertFloat4ToU32(color)); - if ((i%7)==0) { - String label=fmt::sprintf("%d",(note-60)/12); - ImVec2 pText=ImLerp(p0,p1,ImVec2(0.5f,1.0f)); - ImVec2 labelSize=ImGui::CalcTextSize(label.c_str()); - pText.x-=labelSize.x*0.5f; - pText.y-=labelSize.y+ImGui::GetStyle().ItemSpacing.y; - dl->AddText(pText,0xff404040,label.c_str()); - } + pianoLabel(dl,p0,p1,note); } for (int i=0; i Date: Sat, 1 Nov 2025 03:20:34 -0500 Subject: [PATCH 07/11] adpcm-xq: try decoding anyway thanks Architect! --- extern/adpcm-xq-s/adpcm-lib.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extern/adpcm-xq-s/adpcm-lib.c b/extern/adpcm-xq-s/adpcm-lib.c index bd2016e5b..5215803e6 100644 --- a/extern/adpcm-xq-s/adpcm-lib.c +++ b/extern/adpcm-xq-s/adpcm-lib.c @@ -352,8 +352,10 @@ int adpcm_decode_block (int16_t *outbuf, const uint8_t *inbuf, size_t inbufsize, *outbuf++ = pcmdata[0] = (int16_t) (inbuf [0] | (inbuf [1] << 8)); index[0] = inbuf [2]; + // tildearrow: don't return if this fails. try decoding a corrupt sample anyway. + // thanks Architect! if (index [0] < 0 || index [0] > 88 || inbuf [3]) // sanitize the input a little... - return 0; + index[0] = 0; inbufsize -= 4; inbuf += 4; From 173f59dc78d5adb1bc1709e807485260e0da3875 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Nov 2025 03:38:11 -0500 Subject: [PATCH 08/11] GUI: refactor the poly button --- src/gui/editControls.cpp | 82 +++++++++++++--------------------------- src/gui/gui.cpp | 18 ++++----- src/gui/gui.h | 9 ++++- 3 files changed, 43 insertions(+), 66 deletions(-) diff --git a/src/gui/editControls.cpp b/src/gui/editControls.cpp index d0c0fd833..04e4da807 100644 --- a/src/gui/editControls.cpp +++ b/src/gui/editControls.cpp @@ -169,6 +169,20 @@ const bool mobileButtonPersist[32]={ false, }; +const char* noteInputModes[4]={ + _N("Mono##PolyInput"), + _N("Poly##PolyInput"), + _N("Chord##PolyInput"), + // unused + _N("Of fuckin' course!##PolyInput") +}; + +#define CHANGE_NOTE_INPUT_MODE \ + noteInputMode++; \ + if (noteInputMode>GUI_NOTE_INPUT_CHORD) noteInputMode=GUI_NOTE_INPUT_MONO; \ + if (noteInputMode==GUI_NOTE_INPUT_MONO) memset(multiIns,-1,7*sizeof(int)); \ + e->setAutoNotePoly(noteInputMode!=GUI_NOTE_INPUT_MONO); + void FurnaceGUI::drawMobileControls() { float timeScale=60.0*ImGui::GetIO().DeltaTime; if (dragMobileMenu) { @@ -758,20 +772,9 @@ void FurnaceGUI::drawEditControls() { } ImGui::SameLine(); - pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { - if (noteInputPoly) { - if (noteInputChord) { - noteInputPoly=false; - noteInputChord=false; - } else { - noteInputChord=true; - } - } else { - noteInputPoly=true; - noteInputChord=false; - } - e->setAutoNotePoly(noteInputPoly); + pushToggleColors(noteInputMode!=GUI_NOTE_INPUT_MONO); + if (ImGui::Button(_(noteInputModes[noteInputMode&3]))) { + CHANGE_NOTE_INPUT_MODE; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Polyphony")); @@ -898,20 +901,9 @@ void FurnaceGUI::drawEditControls() { unimportant(ImGui::Checkbox(_("Pattern"),&followPattern)); ImGui::SameLine(); - pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { - if (noteInputPoly) { - if (noteInputChord) { - noteInputPoly=false; - noteInputChord=false; - } else { - noteInputChord=true; - } - } else { - noteInputPoly=true; - noteInputChord=false; - } - e->setAutoNotePoly(noteInputPoly); + pushToggleColors(noteInputMode!=GUI_NOTE_INPUT_MONO); + if (ImGui::Button(_(noteInputModes[noteInputMode&3]))) { + CHANGE_NOTE_INPUT_MODE; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Polyphony")); @@ -1046,20 +1038,9 @@ void FurnaceGUI::drawEditControls() { } popToggleColors(); - pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { - if (noteInputPoly) { - if (noteInputChord) { - noteInputPoly=false; - noteInputChord=false; - } else { - noteInputChord=true; - } - } else { - noteInputPoly=true; - noteInputChord=false; - } - e->setAutoNotePoly(noteInputPoly); + pushToggleColors(noteInputMode!=GUI_NOTE_INPUT_MONO); + if (ImGui::Button(_(noteInputModes[noteInputMode&3]))) { + CHANGE_NOTE_INPUT_MODE; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Polyphony")); @@ -1156,20 +1137,9 @@ void FurnaceGUI::drawEditControls() { popToggleColors(); ImGui::SameLine(); - pushToggleColors(noteInputPoly); - if (ImGui::Button(noteInputPoly?(noteInputChord?(_("Chord##PolyInput")):(_("Poly##PolyInput"))):(_("Mono##PolyInput")))) { - if (noteInputPoly) { - if (noteInputChord) { - noteInputPoly=false; - noteInputChord=false; - } else { - noteInputChord=true; - } - } else { - noteInputPoly=true; - noteInputChord=false; - } - e->setAutoNotePoly(noteInputPoly); + pushToggleColors(noteInputMode!=GUI_NOTE_INPUT_MONO); + if (ImGui::Button(_(noteInputModes[noteInputMode&3]))) { + CHANGE_NOTE_INPUT_MODE; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(_("Polyphony")); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index c1a90b5cc..d5560ccec 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1399,7 +1399,7 @@ void FurnaceGUI::noteInput(int num, int key, int vol, int chanOff) { int tick=0; int speed=0; - if (chanOff>0 && noteInputChord) { + if (chanOff>0 && noteInputMode==GUI_NOTE_INPUT_CHORD) { ch=e->getViableChannel(ch,chanOff,curIns); if ((!e->isPlaying() || !followPattern)) { y-=editStep; @@ -1466,7 +1466,7 @@ void FurnaceGUI::noteInput(int num, int key, int vol, int chanOff) { pat->newData[y][DIV_PAT_VOL]=-1; } } - if ((!e->isPlaying() || !followPattern) && (chanOff<1 || !noteInputChord)) { + if ((!e->isPlaying() || !followPattern) && (chanOff<1 || noteInputMode!=GUI_NOTE_INPUT_CHORD)) { editAdvance(); } makeUndo(GUI_UNDO_PATTERN_EDIT,UndoRegion(ord,ch,y,ord,ch,y)); @@ -7657,7 +7657,7 @@ bool FurnaceGUI::init() { initSystemPresets(); - e->setAutoNotePoly(noteInputPoly); + e->setAutoNotePoly(noteInputMode!=GUI_NOTE_INPUT_MONO); SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER,"1"); #if SDL_VERSION_ATLEAST(2,0,17) @@ -8326,8 +8326,10 @@ void FurnaceGUI::syncState() { orderLock=e->getConfBool("orderLock",false); followOrders=e->getConfBool("followOrders",true); followPattern=e->getConfBool("followPattern",true); - noteInputPoly=e->getConfBool("noteInputPoly",true); - noteInputChord=e->getConfBool("noteInputChord",false); + noteInputMode=e->getConfInt("noteInputMode",GUI_NOTE_INPUT_POLY); + if (noteInputMode!=GUI_NOTE_INPUT_MONO && noteInputMode!=GUI_NOTE_INPUT_POLY && noteInputMode!=GUI_NOTE_INPUT_CHORD) { + noteInputMode=GUI_NOTE_INPUT_POLY; + } filePlayerSync=e->getConfBool("filePlayerSync",true); audioExportOptions.loops=e->getConfInt("exportLoops",0); if (audioExportOptions.loops<0) audioExportOptions.loops=0; @@ -8489,8 +8491,7 @@ void FurnaceGUI::commitState(DivConfig& conf) { conf.set("followOrders",followOrders); conf.set("followPattern",followPattern); conf.set("orderEditMode",orderEditMode); - conf.set("noteInputPoly",noteInputPoly); - conf.set("noteInputChord",noteInputChord); + conf.set("noteInputMode",(int)noteInputMode); conf.set("filePlayerSync",filePlayerSync); if (settings.persistFadeOut) { conf.set("exportLoops",audioExportOptions.loops); @@ -8665,8 +8666,7 @@ FurnaceGUI::FurnaceGUI(): preserveChanPos(false), sysDupCloneChannels(true), sysDupEnd(false), - noteInputPoly(true), - noteInputChord(false), + noteInputMode(GUI_NOTE_INPUT_POLY), notifyWaveChange(false), notifySampleChange(false), recalcTimestamps(true), diff --git a/src/gui/gui.h b/src/gui/gui.h index 0add23b69..f980f0feb 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1673,6 +1673,12 @@ struct CSDisAsmIns { } }; +enum NoteInputModes: unsigned char { + GUI_NOTE_INPUT_MONO=0, + GUI_NOTE_INPUT_POLY, + GUI_NOTE_INPUT_CHORD +}; + struct FurnaceCV; class FurnaceGUI { @@ -1723,7 +1729,8 @@ class FurnaceGUI { bool vgmExportDirectStream, displayInsTypeList, displayWaveSizeList; bool portrait, injectBackUp, mobileMenuOpen, warnColorPushed; bool wantCaptureKeyboard, oldWantCaptureKeyboard, displayMacroMenu; - bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd, noteInputPoly, noteInputChord; + bool displayNew, displayExport, displayPalette, fullScreen, preserveChanPos, sysDupCloneChannels, sysDupEnd; + unsigned char noteInputMode; bool notifyWaveChange, notifySampleChange; bool recalcTimestamps; bool wantScrollListIns, wantScrollListWave, wantScrollListSample; From 18b7ff1fb773a665152a95f4e46cf6964f884b66 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Nov 2025 03:42:21 -0500 Subject: [PATCH 09/11] GUI: just set chordInputOffset to 0 --- src/gui/gui.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index d5560ccec..182e17330 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -1886,10 +1886,8 @@ void FurnaceGUI::keyDown(SDL_Event& ev) { } void FurnaceGUI::keyUp(SDL_Event& ev) { - // this is very, very lazy... - if (--chordInputOffset<0) { - chordInputOffset=0; - } + // this is very, very lazy... but it works. + chordInputOffset=0; } bool dirExists(String s) { @@ -4239,7 +4237,7 @@ bool FurnaceGUI::loop() { doAction(action); } else switch (msg.type&0xf0) { case TA_MIDI_NOTE_OFF: - if (--chordInputOffset<0) chordInputOffset=0; + chordInputOffset=0; break; case TA_MIDI_NOTE_ON: if (midiMap.valueInputStyle==0 || midiMap.valueInputStyle>3 || cursor.xFine==0) { From 53b30d4b8bcd09d8dba47913336c0aae5f478637 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Nov 2025 04:05:16 -0500 Subject: [PATCH 10/11] don't allow multi-ins when mono --- src/gui/gui.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 182e17330..174c705ce 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -868,13 +868,14 @@ void FurnaceGUI::autoDetectSystem() { } } -void FurnaceGUI::setCurIns(int newIns) { curIns=newIns; +void FurnaceGUI::setCurIns(int newIns) { + curIns=newIns; memset(multiIns,-1,7*sizeof(int)); } bool FurnaceGUI::setMultiIns(int newIns) { - // set primary instrument if not set - if (curIns<0) { + // set primary instrument if not set (or if polyphony is mono) + if (curIns<0 || noteInputMode==GUI_NOTE_INPUT_MONO) { setCurIns(newIns); return true; } From 0a958a17d39b24ec4662ecdd81b95ed365e551ae Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sat, 1 Nov 2025 05:04:09 -0500 Subject: [PATCH 11/11] make some of these buttons more useful --- src/gui/multiInsSetup.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/gui/multiInsSetup.cpp b/src/gui/multiInsSetup.cpp index e056b4e50..003a8dd6b 100644 --- a/src/gui/multiInsSetup.cpp +++ b/src/gui/multiInsSetup.cpp @@ -55,6 +55,9 @@ void FurnaceGUI::drawMultiInsSetup() { if (ImGui::Button(ICON_FA_CHEVRON_UP "##Up",ImVec2(ImGui::GetContentRegionAvail().x,0))) { if (i>0) multiInsTranspose[i-1]++; } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (i>0) multiInsTranspose[i-1]+=12; + } ImGui::PopStyleVar(); ImGui::PopItemFlag(); @@ -67,10 +70,12 @@ void FurnaceGUI::drawMultiInsSetup() { ImGui::PushStyleColor(ImGuiCol_HeaderActive,colorActive); } ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign,ImVec2(0.5f,0.5f)); - ImGui::Selectable(id.c_str(),thisInsOn,0,ImVec2( - ImGui::GetContentRegionAvail().x, - ImGui::GetContentRegionAvail().y-ImGui::GetTextLineHeightWithSpacing() - )); + if (ImGui::Selectable(id.c_str(),thisInsOn,0,ImVec2( + ImGui::GetContentRegionAvail().x, + ImGui::GetContentRegionAvail().y-ImGui::GetTextLineHeightWithSpacing() + ))) { + if (i>0) multiInsTranspose[i-1]=0; + } ImGui::PopStyleVar(); if (i>0) { ImGui::PopStyleColor(3); @@ -81,6 +86,9 @@ void FurnaceGUI::drawMultiInsSetup() { if (ImGui::Button(ICON_FA_CHEVRON_DOWN "##Down",ImVec2(ImGui::GetContentRegionAvail().x,0))) { if (i>0) multiInsTranspose[i-1]--; } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (i>0) multiInsTranspose[i-1]-=12; + } ImGui::PopStyleVar(); ImGui::PopItemFlag();