Merge branch 'master' into spectrum

This commit is contained in:
Eknous-P 2025-11-01 17:19:30 +04:00
commit 4f85dec801
23 changed files with 488 additions and 124 deletions

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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;

View file

@ -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;
}

View file

@ -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();

View file

@ -84,6 +84,15 @@ void DivChannelData::wipePatterns() {
}
}
bool DivPattern::isEmpty() {
for (int i=0; i<DIV_MAX_ROWS; i++) {
for (int j=0; j<DIV_MAX_COLS; j++) {
if (newData[i][j]!=-1) return false;
}
}
return true;
}
void DivPattern::copyOn(DivPattern* dest) {
dest->name=name;
memcpy(dest->newData,newData,sizeof(newData));

View file

@ -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.
*/

View file

@ -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;

View file

@ -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();

View file

@ -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 {

View file

@ -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"));

View file

@ -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);
}

View file

@ -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);

View file

@ -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<bool*,64> 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<unsigned int,ImVec2>& 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);

View file

@ -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)),

View file

@ -6649,10 +6649,10 @@ void FurnaceGUI::drawInsEdit() {
for (size_t i=0; i<e->song.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; i<e->song.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;

102
src/gui/multiInsSetup.cpp Normal file
View file

@ -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();
}

View file

@ -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; k<e->curSubSong->ordersLen; k++) {
available[e->curOrders->ord[j][k]]=false;
}
for (int k=0; k<DIV_MAX_PATTERNS; k++) {
// don't accept a used pattern
if (!available[k]) continue;
// accept an unallocated pattern (guaranteed to be empty)
if (e->curPat[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) {

View file

@ -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; i<oct; i++) {
@ -413,6 +460,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);
}

View file

@ -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"));

View file

@ -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?");