Fix patch browser keyboard navigation to cover directory rows
Arrow keys now navigate the full virtual row list (../, subdirs, patches) rather than only patch rows. Landing on a patch auto-loads it as before; landing on .. or a subdir highlights it and Enter confirms navigation. Added patchVCursor to UIState to track keyboard focus independently of the loaded patch. Mouse-click on a patch now also syncs the cursor so subsequent arrow-key movement starts from the right position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
16
Readme.md
16
Readme.md
@@ -1,16 +0,0 @@
|
|||||||
# soundThing
|
|
||||||
|
|
||||||
A basic demonstration of generating sound samples and feeding an audio buffer, along with opening a MIDI port with ALSA, and playing back tones from the sound generator using MIDI events.
|
|
||||||
|
|
||||||
## Building:
|
|
||||||
|
|
||||||
Depends on Raylib and probably libasound2-dev, so install those if you don't have them already.
|
|
||||||
|
|
||||||
1) Clone the repository.
|
|
||||||
2) run make from inside the repository directory.
|
|
||||||
|
|
||||||
### Running and use:
|
|
||||||
|
|
||||||
1) Run make run from inside the repository directory.
|
|
||||||
2) Specify the client and port for the MIDI device you want to use to control soundThing.
|
|
||||||
3) You should now be able to play back notes by sending soundThing MIDI data. Use the up and down arrows to change waveforms.
|
|
||||||
@@ -72,6 +72,7 @@ typedef struct {
|
|||||||
char patchCurrentDir[256];
|
char patchCurrentDir[256];
|
||||||
char patchSubDirs[PATCH_MAX_DIRS][PATCH_NAME_LEN];
|
char patchSubDirs[PATCH_MAX_DIRS][PATCH_NAME_LEN];
|
||||||
int patchSubDirCount;
|
int patchSubDirCount;
|
||||||
|
int patchVCursor;
|
||||||
|
|
||||||
// Control registry — built lazily by uiKnob/uiSlider for CC mapping persistence
|
// Control registry — built lazily by uiKnob/uiSlider for CC mapping persistence
|
||||||
UIControlEntry controlRegistry[128];
|
UIControlEntry controlRegistry[128];
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"osc0_waveform": 0,
|
|
||||||
"osc0_dutyCycle": 0.500000,
|
|
||||||
"osc0_detune": 0.000000,
|
|
||||||
"osc0_gain": 4.000000,
|
|
||||||
"osc0_active": 1,
|
|
||||||
"osc0_octave": 0,
|
|
||||||
"osc0_modRouting0": 0,
|
|
||||||
"osc0_modRouting1": 3,
|
|
||||||
"osc0_modRouting2": 0,
|
|
||||||
"osc0_modDepth0": 0.000000,
|
|
||||||
"osc0_modDepth1": 10.000000,
|
|
||||||
"osc0_modDepth2": 0.000000,
|
|
||||||
"osc1_waveform": 0,
|
|
||||||
"osc1_dutyCycle": 0.500000,
|
|
||||||
"osc1_detune": 2.000000,
|
|
||||||
"osc1_gain": 0.300000,
|
|
||||||
"osc1_active": 1,
|
|
||||||
"osc1_octave": 1,
|
|
||||||
"osc1_modRouting0": 0,
|
|
||||||
"osc1_modRouting1": 0,
|
|
||||||
"osc1_modRouting2": 2,
|
|
||||||
"osc1_modDepth0": 0.000000,
|
|
||||||
"osc1_modDepth1": 0.000000,
|
|
||||||
"osc1_modDepth2": 1.500000,
|
|
||||||
"ampEnv_attack": 0.001000,
|
|
||||||
"ampEnv_decay": 2.000000,
|
|
||||||
"ampEnv_sustain": 0.000000,
|
|
||||||
"ampEnv_release": 0.600000,
|
|
||||||
"modEnv_attack": 0.001000,
|
|
||||||
"modEnv_decay": 0.080000,
|
|
||||||
"modEnv_sustain": 0.000000,
|
|
||||||
"modEnv_release": 0.040000,
|
|
||||||
"lfo0_rate": 4.500000,
|
|
||||||
"lfo0_waveform": 0,
|
|
||||||
"lfo0_active": 1,
|
|
||||||
"lfo1_rate": 1.000000,
|
|
||||||
"lfo1_waveform": 0,
|
|
||||||
"lfo1_active": 0,
|
|
||||||
"filter_cutoff": 10000.000000,
|
|
||||||
"filter_resonance": 0.100000,
|
|
||||||
"filter_type": 0,
|
|
||||||
"filter_active": 1,
|
|
||||||
"filter_modRouting": 0,
|
|
||||||
"filter_modDepth": 0.000000,
|
|
||||||
"filter_resModRouting": 0,
|
|
||||||
"filter_resModDepth": 0.000000,
|
|
||||||
"master_volume": 0.800000,
|
|
||||||
"master_pitchBendRange": 2.000000
|
|
||||||
}
|
|
||||||
100
source/ui.c
100
source/ui.c
@@ -137,6 +137,7 @@ void uiInit(UIState *ui, MidiState *midi, Camera2D *camera)
|
|||||||
ui->patchFavCount = patchLoadFavs(ui->patchFavs, PATCH_MAX_FILES);
|
ui->patchFavCount = patchLoadFavs(ui->patchFavs, PATCH_MAX_FILES);
|
||||||
ui->patchCurrentDir[0] = '\0';
|
ui->patchCurrentDir[0] = '\0';
|
||||||
ui->patchSubDirCount = 0;
|
ui->patchSubDirCount = 0;
|
||||||
|
ui->patchVCursor = -1;
|
||||||
ui->controlRegistryCount = 0;
|
ui->controlRegistryCount = 0;
|
||||||
ui->pendingCount = 0;
|
ui->pendingCount = 0;
|
||||||
}
|
}
|
||||||
@@ -827,6 +828,7 @@ static void patchRescan(UIState *ui)
|
|||||||
ui->patchFiles, PATCH_MAX_FILES,
|
ui->patchFiles, PATCH_MAX_FILES,
|
||||||
ui->patchSubDirs, PATCH_MAX_DIRS, &ui->patchSubDirCount);
|
ui->patchSubDirs, PATCH_MAX_DIRS, &ui->patchSubDirCount);
|
||||||
ui->patchScrollOffset = 0;
|
ui->patchScrollOffset = 0;
|
||||||
|
ui->patchVCursor = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
||||||
@@ -1042,32 +1044,69 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
|||||||
ui->patchScrollOffset -= (int)GetMouseWheelMove();
|
ui->patchScrollOffset -= (int)GetMouseWheelMove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arrow key navigation: move selection through patches only
|
// Arrow key navigation over all virtual rows: [..], subdirs, patches
|
||||||
if (ui->focusedTextInput == -1 && !ui->patchConfirmOverwrite && !ui->patchConfirmClear
|
if (ui->focusedTextInput == -1 && !ui->patchConfirmOverwrite && !ui->patchConfirmClear
|
||||||
&& filteredCount > 0) {
|
&& totalVirtual > 0) {
|
||||||
int curFi = -1;
|
// Sync cursor from currently loaded patch if unset
|
||||||
for (int fi = 0; fi < filteredCount; fi++) {
|
if (ui->patchVCursor < 0 && ui->patchCurrentName[0]) {
|
||||||
if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) {
|
for (int fi = 0; fi < filteredCount; fi++) {
|
||||||
curFi = fi;
|
if (strcmp(ui->patchFiles[filteredIdx[fi]], ui->patchCurrentName) == 0) {
|
||||||
break;
|
ui->patchVCursor = fi + backRows + ui->patchSubDirCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int nextFi = curFi;
|
int next = ui->patchVCursor;
|
||||||
if (IsKeyPressed(KEY_DOWN) && curFi < filteredCount - 1) nextFi = curFi + 1;
|
if (IsKeyPressed(KEY_DOWN))
|
||||||
if (IsKeyPressed(KEY_UP) && curFi > 0) nextFi = curFi - 1;
|
next = (next < 0) ? 0 : (next < totalVirtual - 1 ? next + 1 : next);
|
||||||
if (nextFi != curFi) {
|
if (IsKeyPressed(KEY_UP))
|
||||||
int i = filteredIdx[nextFi];
|
next = (next < 0) ? totalVirtual - 1 : (next > 0 ? next - 1 : next);
|
||||||
char path[512];
|
if (next != ui->patchVCursor) {
|
||||||
buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]);
|
ui->patchVCursor = next;
|
||||||
if (patchLoad(s, path)) {
|
// Auto-load when landing on a patch row
|
||||||
snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
if (next >= backRows + ui->patchSubDirCount && filteredCount > 0) {
|
||||||
snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
int fi = next - backRows - ui->patchSubDirCount;
|
||||||
|
int i = filteredIdx[fi];
|
||||||
|
char path[512];
|
||||||
|
buildPatchPath(path, sizeof(path), ui->patchCurrentDir, ui->patchFiles[i]);
|
||||||
|
if (patchLoad(s, path)) {
|
||||||
|
snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
||||||
|
snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
int vRow = nextFi + backRows + ui->patchSubDirCount;
|
}
|
||||||
if (vRow < ui->patchScrollOffset)
|
// Enter activates navigation rows (.. and subdirs)
|
||||||
ui->patchScrollOffset = vRow;
|
if (IsKeyPressed(KEY_ENTER) && ui->patchVCursor >= 0) {
|
||||||
if (vRow >= ui->patchScrollOffset + MAX_VIS)
|
int vc = ui->patchVCursor;
|
||||||
ui->patchScrollOffset = vRow - MAX_VIS + 1;
|
if (!isRoot && vc == 0) {
|
||||||
|
char *lastSlash = strrchr(ui->patchCurrentDir, '/');
|
||||||
|
if (lastSlash) *lastSlash = '\0';
|
||||||
|
else ui->patchCurrentDir[0] = '\0';
|
||||||
|
ui->patchCurrentName[0] = '\0';
|
||||||
|
patchRescan(ui);
|
||||||
|
return;
|
||||||
|
} else if (vc >= backRows && vc < backRows + ui->patchSubDirCount) {
|
||||||
|
int d = vc - backRows;
|
||||||
|
if (ui->patchCurrentDir[0]) {
|
||||||
|
size_t curLen = strlen(ui->patchCurrentDir);
|
||||||
|
snprintf(ui->patchCurrentDir + curLen,
|
||||||
|
sizeof(ui->patchCurrentDir) - curLen,
|
||||||
|
"/%s", ui->patchSubDirs[d]);
|
||||||
|
} else {
|
||||||
|
snprintf(ui->patchCurrentDir, sizeof(ui->patchCurrentDir),
|
||||||
|
"%s", ui->patchSubDirs[d]);
|
||||||
|
}
|
||||||
|
ui->patchCurrentName[0] = '\0';
|
||||||
|
patchRescan(ui);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scroll to keep cursor visible
|
||||||
|
if (ui->patchVCursor >= 0) {
|
||||||
|
if (ui->patchVCursor < ui->patchScrollOffset)
|
||||||
|
ui->patchScrollOffset = ui->patchVCursor;
|
||||||
|
if (ui->patchVCursor >= ui->patchScrollOffset + MAX_VIS)
|
||||||
|
ui->patchScrollOffset = ui->patchVCursor - MAX_VIS + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,10 +1138,10 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
|||||||
|
|
||||||
if (!isRoot && r == 0) {
|
if (!isRoot && r == 0) {
|
||||||
// ".." — navigate to parent
|
// ".." — navigate to parent
|
||||||
DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2),
|
Color dotBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255};
|
||||||
(Color){20, 30, 50, 255});
|
Color dotEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255};
|
||||||
DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2),
|
DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotBg);
|
||||||
(Color){80, 120, 180, 255});
|
DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dotEdge);
|
||||||
DRAW_TEXT("..", (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE);
|
DRAW_TEXT("..", (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE);
|
||||||
if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) &&
|
if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) &&
|
||||||
mouse.x >= x + padding && mouse.x <= x + padding + btnW &&
|
mouse.x >= x + padding && mouse.x <= x + padding + btnW &&
|
||||||
@@ -1119,10 +1158,10 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
|||||||
int d = r - backRows;
|
int d = r - backRows;
|
||||||
char label[PATCH_NAME_LEN + 4];
|
char label[PATCH_NAME_LEN + 4];
|
||||||
snprintf(label, sizeof(label), "> %s", ui->patchSubDirs[d]);
|
snprintf(label, sizeof(label), "> %s", ui->patchSubDirs[d]);
|
||||||
DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2),
|
Color dirBg = (ui->patchVCursor == r) ? (Color){40, 60, 100, 255} : (Color){20, 30, 50, 255};
|
||||||
(Color){20, 30, 50, 255});
|
Color dirEdge = (ui->patchVCursor == r) ? SKYBLUE : (Color){80, 120, 180, 255};
|
||||||
DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2),
|
DrawRectangle((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirBg);
|
||||||
(Color){80, 120, 180, 255});
|
DrawRectangleLines((int)(x + padding), (int)contentY, (int)btnW, (int)(rowH - 2), dirEdge);
|
||||||
DRAW_TEXT(label, (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE);
|
DRAW_TEXT(label, (int)(x + padding + 4), (int)(contentY + 2), fontSize, SKYBLUE);
|
||||||
if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) &&
|
if (!ui->patchConfirmClear && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) &&
|
||||||
mouse.x >= x + padding && mouse.x <= x + padding + btnW &&
|
mouse.x >= x + padding && mouse.x <= x + padding + btnW &&
|
||||||
@@ -1190,6 +1229,7 @@ void uiPatchMenu(UIState *ui, float x, float y, float width, Synth *s)
|
|||||||
if (patchLoad(s, path)) {
|
if (patchLoad(s, path)) {
|
||||||
snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
snprintf(ui->patchCurrentName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
||||||
snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
snprintf(ui->patchSaveName, PATCH_NAME_LEN, "%s", ui->patchFiles[i]);
|
||||||
|
ui->patchVCursor = r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user