diff --git a/CMakeLists.txt b/CMakeLists.txt index 8735e88da..5bb3b6f96 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/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/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. 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. 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; 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/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/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 de578f833..e85ef3f07 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; @@ -366,6 +368,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; @@ -477,6 +482,9 @@ void FurnaceGUI::doAction(int what) { case GUI_WINDOW_REF_PLAYER: refPlayerOpen=false; break; + case GUI_WINDOW_MULTI_INS_SETUP: + multiInsSetupOpen=false; + break; case GUI_WINDOW_TUNER: tunerOpen=false; break; @@ -751,7 +759,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 { @@ -780,7 +788,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 { @@ -832,13 +840,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; @@ -1727,7 +1737,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/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/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 3cef5c3ba..a3d5f5b50 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -868,6 +868,39 @@ 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 (or if polyphony is mono) + if (curIns<0 || noteInputMode==GUI_NOTE_INPUT_MONO) { + 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; @@ -1338,6 +1371,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]); + } + } }); } @@ -1367,7 +1405,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; @@ -1434,7 +1472,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)); @@ -1474,7 +1512,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; } @@ -1854,10 +1892,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) { @@ -1982,7 +2018,7 @@ void FurnaceGUI::openFileDialog(FurnaceGUIFileDialogs type) { if (curIns!=-2) { prevIns=curIns; } - curIns=-2; + setCurIns(-2); } } for (DivInstrument* i: instruments) delete i; @@ -3865,6 +3901,7 @@ bool FurnaceGUI::loop() { DECLARE_METRIC(effectList) DECLARE_METRIC(userPresets) DECLARE_METRIC(refPlayer) + DECLARE_METRIC(multiInsSetup) DECLARE_METRIC(popup) #ifdef IS_MOBILE @@ -4052,7 +4089,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; @@ -4208,7 +4245,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) { @@ -4246,8 +4283,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; } @@ -4489,6 +4526,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) { @@ -4863,6 +4901,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(); @@ -5063,6 +5102,7 @@ bool FurnaceGUI::loop() { MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); MEASURE(refPlayer,drawRefPlayer()); + MEASURE(multiInsSetup,drawMultiInsSetup()); MEASURE(patManager,drawPatManager()); } else { @@ -5111,6 +5151,7 @@ bool FurnaceGUI::loop() { MEASURE(effectList,drawEffectList()); MEASURE(userPresets,drawUserPresets()); MEASURE(refPlayer,drawRefPlayer()); + MEASURE(multiInsSetup,drawMultiInsSetup()); } @@ -5180,7 +5221,7 @@ bool FurnaceGUI::loop() { } } } else { - curIns=prevIns; + setCurIns(prevIns); wavePreviewInit=true; updateFMPreview=true; } @@ -5686,7 +5727,7 @@ bool FurnaceGUI::loop() { MARK_MODIFIED; } if (instrumentCount>=0 && settings.selectAssetOnLoad) { - curIns=instrumentCount-1; + setCurIns(instrumentCount-1); } } } @@ -6563,7 +6604,7 @@ bool FurnaceGUI::loop() { e->lockEngine([this]() { e->song.clearInstruments(); }); - curIns=-1; + setCurIns(-1); MARK_MODIFIED; ImGui::CloseCurrentPopup(); } @@ -6768,7 +6809,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 { @@ -7628,7 +7669,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) @@ -8264,6 +8305,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); @@ -8298,8 +8340,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; @@ -8329,6 +8373,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); @@ -8437,6 +8482,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); @@ -8467,8 +8513,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); @@ -8498,6 +8543,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); @@ -8677,8 +8723,7 @@ FurnaceGUI::FurnaceGUI(): preserveChanPos(false), sysDupCloneChannels(true), sysDupEnd(false), - noteInputPoly(true), - noteInputChord(false), + noteInputMode(GUI_NOTE_INPUT_POLY), notifyWaveChange(false), notifySampleChange(false), recalcTimestamps(true), @@ -8827,6 +8872,7 @@ FurnaceGUI::FurnaceGUI(): curPaletteChoice(0), curPaletteType(0), soloTimeout(0.0f), + mobileMultiInsToggle(false), purgeYear(2021), purgeMonth(4), purgeDay(4), @@ -8877,6 +8923,7 @@ FurnaceGUI::FurnaceGUI(): cvOpen(false), userPresetsOpen(false), refPlayerOpen(false), + multiInsSetupOpen(false), cvNotSerious(false), shortIntro(false), insListDir(false), @@ -9178,6 +9225,7 @@ FurnaceGUI::FurnaceGUI(): pianoOffsetEdit(9), pianoView(PIANO_LAYOUT_AUTOMATIC), pianoInputPadMode(PIANO_INPUT_PAD_SPLIT_AUTO), + pianoLabelsMode(PIANO_LABELS_OCTAVE), #else pianoOctaves(7), pianoOctavesEdit(4), @@ -9188,6 +9236,7 @@ FurnaceGUI::FurnaceGUI(): pianoOffsetEdit(6), pianoView(PIANO_LAYOUT_STANDARD), pianoInputPadMode(PIANO_INPUT_PAD_DISABLE), + pianoLabelsMode(PIANO_LABELS_OCTAVE), #endif hasACED(false), waveGenBaseShape(0), @@ -9345,6 +9394,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 d83a8da60..ed26f6310 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, @@ -574,6 +582,7 @@ enum FurnaceGUIWindows { GUI_WINDOW_CS_PLAYER, GUI_WINDOW_USER_PRESETS, GUI_WINDOW_REF_PLAYER, + GUI_WINDOW_MULTI_INS_SETUP, GUI_WINDOW_SPOILER }; @@ -782,6 +791,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, @@ -1671,6 +1681,12 @@ struct CSDisAsmIns { } }; +enum NoteInputModes: unsigned char { + GUI_NOTE_INPUT_MONO=0, + GUI_NOTE_INPUT_POLY, + GUI_NOTE_INPUT_CHORD +}; + struct FurnaceCV; class FurnaceGUI { @@ -1721,7 +1737,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; @@ -2386,13 +2403,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; @@ -2405,6 +2428,7 @@ class FurnaceGUI { bool pianoOpen, notesOpen, tunerOpen, spectrumOpen, 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; @@ -2803,13 +2827,22 @@ class FurnaceGUI { PIANO_INPUT_PAD_MAX }; + enum PianoLabelsMode { + PIANO_LABELS_OFF=0, + PIANO_LABELS_OCTAVE, + PIANO_LABELS_NOTE, + PIANO_LABELS_NOTE_C, + 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]; @@ -2937,6 +2970,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(); @@ -3045,6 +3081,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); @@ -3200,6 +3237,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 4ab37c8af..84300b6e7 100644 --- a/src/gui/guiConst.cpp +++ b/src/gui/guiConst.cpp @@ -675,6 +675,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), @@ -1023,6 +1024,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..003a8dd6b --- /dev/null +++ b/src/gui/multiInsSetup.cpp @@ -0,0 +1,102 @@ +/** + * 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]++; + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (i>0) multiInsTranspose[i-1]+=12; + } + 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)); + 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); + } + + 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]--; + } + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + if (i>0) multiInsTranspose[i-1]-=12; + } + 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/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) { diff --git a/src/gui/piano.cpp b/src/gui/piano.cpp index 0efd98c14..6bcfbdda1 100644 --- a/src/gui/piano.cpp +++ b/src/gui/piano.cpp @@ -33,6 +33,46 @@ } \ } +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; @@ -114,6 +154,27 @@ void FurnaceGUI::drawPiano() { pianoInputPadMode=PIANO_INPUT_PAD_SPLIT_VISIBLE; } ImGui::Unindent(); + ImGui::Text(_("Key labels:")); + ImGui::Indent(); + if (ImGui::RadioButton(_("Off##keyLabel0"),pianoLabelsMode==PIANO_LABELS_OFF)) { + pianoLabelsMode=PIANO_LABELS_OFF; + } + if (ImGui::RadioButton(_("Octaves##keyLabel1"),pianoLabelsMode==PIANO_LABELS_OCTAVE)) { + pianoLabelsMode=PIANO_LABELS_OCTAVE; + } + if (ImGui::RadioButton(_("Notes##keyLabel2"),pianoLabelsMode==PIANO_LABELS_NOTE)) { + pianoLabelsMode=PIANO_LABELS_NOTE; + } + 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##keyLabel5"),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(); @@ -251,14 +312,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=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()); - } + if (isTopKey[i%12]) pianoLabel(dl,p0,p1,note); } } else { int bottomNotes=7*oct; @@ -318,14 +372,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; isynchronized([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 fa388399e..ded7ca1f9 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -4177,6 +4177,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?");