From 5b145b71210e46f9741cb87132f8074da5defe49 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 31 Oct 2025 19:36:13 -0500 Subject: [PATCH] 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?");