diff --git a/src/main.cpp b/src/main.cpp index 09ccebc..a3a521d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -267,31 +267,147 @@ static void build_midi_panel(B32 show, MidiEngine *midi) { .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, .padding = { 8, 8, 6, 6 }, - .childGap = 4, + .childGap = 6, .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { // Refresh button - CLAY(CLAY_ID("MidiRefreshBtn"), + Clay_ElementId refresh_eid = CLAY_ID("MidiRefreshBtn"); + B32 refresh_hovered = Clay_PointerOver(refresh_eid); + CLAY(refresh_eid, .layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) }, .padding = { 12, 12, 0, 0 }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, }, - .backgroundColor = Clay_Hovered() ? g_theme.accent_hover : g_theme.bg_lighter, + .backgroundColor = refresh_hovered ? g_theme.accent_hover : g_theme.bg_lighter, .cornerRadius = CLAY_CORNER_RADIUS(3) ) { CLAY_TEXT(CLAY_STRING("Refresh"), &g_text_config_normal); } + if (refresh_hovered && g_wstate.mouse_clicked) { + midi_refresh_devices(midi); + } - // Device list - use static buffers so strings persist for Clay rendering static char device_bufs[64][128]; int32_t device_count = midi_get_device_count(midi); + + // --- Inputs section --- + CLAY_TEXT(CLAY_STRING("Inputs"), &g_text_config_dim); + + static const char *note_names[] = {"C","Db","D","Eb","E","F","Gb","G","Ab","A","Bb","B"}; + static char note_bufs[64][8]; + static char vel_bufs[64][8]; + static Clay_TextElementConfig box_text_config; + box_text_config = {}; + box_text_config.textColor = Clay_Color{255, 255, 255, 255}; + box_text_config.fontSize = 12; + box_text_config.wrapMode = CLAY_TEXT_WRAP_NONE; + + B32 has_inputs = 0; for (int32_t i = 0; i < device_count && i < 64; i++) { MidiDeviceInfo *dev = midi_get_device(midi, i); - int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "[%s] %s", dev->is_input ? "IN" : "OUT", dev->name); + if (!dev->is_input) continue; + has_inputs = 1; + + int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; - CLAY(CLAY_IDI("MidiDevice", i), + + // Velocity-based color: blue (vel 0) → green (mid) → red (vel 127) + // Idle = dark gray + Clay_Color box_color; + if (dev->active) { + float t = (float)dev->velocity / 127.0f; + float r, g, b; + if (t < 0.5f) { + float s = t * 2.0f; + r = 40.0f + s * (76.0f - 40.0f); + g = 120.0f + s * (175.0f - 120.0f); + b = 220.0f + s * (80.0f - 220.0f); + } else { + float s = (t - 0.5f) * 2.0f; + r = 76.0f + s * (220.0f - 76.0f); + g = 175.0f + s * (50.0f - 175.0f); + b = 80.0f + s * (40.0f - 80.0f); + } + box_color = Clay_Color{r, g, b, 255}; + } else if (dev->releasing) { + box_color = Clay_Color{255, 255, 255, 255}; + } else { + box_color = Clay_Color{60, 60, 60, 255}; + } + + // Box text: note name when held, "OFF" when releasing, "---" when idle + int nlen; + if (dev->active) { + int pitch = dev->note % 12; + int octave = (dev->note / 12) - 1; + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "%s%d", note_names[pitch], octave); + } else if (dev->releasing) { + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "OFF"); + } else { + nlen = snprintf(note_bufs[i], sizeof(note_bufs[i]), "---"); + } + Clay_String note_str = { .isStaticallyAllocated = false, .length = nlen, .chars = note_bufs[i] }; + + // Box text color: dark for white bg (releasing), white otherwise + Clay_TextElementConfig *box_txt = &box_text_config; + static Clay_TextElementConfig box_text_dark; + box_text_dark = box_text_config; + box_text_dark.textColor = Clay_Color{30, 30, 30, 255}; + if (dev->releasing) box_txt = &box_text_dark; + + // Velocity text + int vlen; + if (dev->active) + vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), "%d", dev->velocity); + else + vlen = snprintf(vel_bufs[i], sizeof(vel_bufs[i]), ""); + Clay_String vel_str = { .isStaticallyAllocated = false, .length = vlen, .chars = vel_bufs[i] }; + + CLAY(CLAY_IDI("MidiIn", i), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, + .padding = { 4, 4, 2, 2 }, + .childGap = 6, + .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + } + ) { + // Note name box (colored by velocity) + CLAY(CLAY_IDI("MidiInNote", i), + .layout = { + .sizing = { .width = CLAY_SIZING_FIXED(36), .height = CLAY_SIZING_FIXED(18) }, + .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, + }, + .backgroundColor = box_color, + .cornerRadius = CLAY_CORNER_RADIUS(3) + ) { + CLAY_TEXT(note_str, box_txt); + } + // Velocity number + CLAY_TEXT(vel_str, &g_text_config_dim); + // Device name + CLAY_TEXT(device_str, &g_text_config_normal); + } + } + if (!has_inputs) { + CLAY_TEXT(CLAY_STRING(" No MIDI inputs"), &g_text_config_dim); + } + + // --- Outputs section --- + CLAY_TEXT(CLAY_STRING("Outputs"), &g_text_config_dim); + + B32 has_outputs = 0; + for (int32_t i = 0; i < device_count && i < 64; i++) { + MidiDeviceInfo *dev = midi_get_device(midi, i); + if (dev->is_input) continue; + has_outputs = 1; + + int len = snprintf(device_bufs[i], sizeof(device_bufs[i]), "%s", dev->name); + Clay_String device_str = { .isStaticallyAllocated = false, .length = len, .chars = device_bufs[i] }; + + CLAY(CLAY_IDI("MidiOut", i), .layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, .padding = { 4, 4, 2, 2 }, @@ -300,8 +416,8 @@ static void build_midi_panel(B32 show, MidiEngine *midi) { CLAY_TEXT(device_str, &g_text_config_normal); } } - if (device_count == 0) { - CLAY_TEXT(CLAY_STRING("No MIDI devices found"), &g_text_config_dim); + if (!has_outputs) { + CLAY_TEXT(CLAY_STRING(" No MIDI outputs"), &g_text_config_dim); } } } @@ -395,6 +511,9 @@ static void do_frame(AppState *app) { if (!renderer_begin_frame(app->renderer)) return; + // Update MIDI activity timers + midi_update(app->midi, dt); + // Gather input PlatformInput input = platform_get_input(app->window); ui_widgets_begin_frame(input); diff --git a/src/midi/midi.h b/src/midi/midi.h index 68af76b..311baed 100644 --- a/src/midi/midi.h +++ b/src/midi/midi.h @@ -9,6 +9,10 @@ struct MidiDeviceInfo { char name[64]; int32_t id; bool is_input; + bool active; // true when note(s) currently held + bool releasing; // true during release flash + int32_t velocity; // last note-on velocity (0-127) + int32_t note; // last MIDI note number (0-127) }; MidiEngine *midi_create(); @@ -16,3 +20,8 @@ void midi_destroy(MidiEngine *engine); void midi_refresh_devices(MidiEngine *engine); int32_t midi_get_device_count(MidiEngine *engine); MidiDeviceInfo *midi_get_device(MidiEngine *engine, int32_t index); + +void midi_open_all_inputs(MidiEngine *engine); +void midi_close_all_inputs(MidiEngine *engine); +void midi_update(MidiEngine *engine, float dt); +bool midi_is_input_active(MidiEngine *engine, int32_t device_index); diff --git a/src/midi/midi_win32.cpp b/src/midi/midi_win32.cpp index 7c8db22..fdea168 100644 --- a/src/midi/midi_win32.cpp +++ b/src/midi/midi_win32.cpp @@ -4,24 +4,164 @@ #include #define MIDI_MAX_DEVICES 64 +#define MIDI_RELEASE_FLASH_DURATION 0.15f struct MidiEngine { MidiDeviceInfo devices[MIDI_MAX_DEVICES]; int32_t device_count; + + HMIDIIN input_handles[MIDI_MAX_DEVICES]; + + // Set atomically from callback thread + volatile LONG pending_note_on_vel[MIDI_MAX_DEVICES]; // last note-on velocity (0 = consumed) + volatile LONG pending_note_num[MIDI_MAX_DEVICES]; // last note-on note number + 1 (0 = consumed) + volatile LONG pending_note_off[MIDI_MAX_DEVICES]; // note-off received flag + volatile LONG held_note_count[MIDI_MAX_DEVICES]; // number of notes currently held + + // Main thread only + int32_t display_velocity[MIDI_MAX_DEVICES]; + int32_t display_note[MIDI_MAX_DEVICES]; + float release_timers[MIDI_MAX_DEVICES]; }; +//////////////////////////////// +// MIDI input callback — called from Win32 MIDI driver thread + +static MidiEngine *g_midi_engine = nullptr; + +static void CALLBACK midi_in_callback(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, + DWORD_PTR dwParam1, DWORD_PTR dwParam2) { + (void)hMidiIn; + (void)dwParam2; + + if (wMsg != MIM_DATA) return; + if (!g_midi_engine) return; + + int32_t idx = (int32_t)dwInstance; + if (idx < 0 || idx >= MIDI_MAX_DEVICES) return; + + BYTE status = (BYTE)(dwParam1 & 0xFF); + BYTE note = (BYTE)((dwParam1 >> 8) & 0xFF); + BYTE velocity = (BYTE)((dwParam1 >> 16) & 0xFF); + BYTE kind = status & 0xF0; + + // Note-on with velocity > 0 + if (kind == 0x90 && velocity > 0) { + InterlockedExchange(&g_midi_engine->pending_note_on_vel[idx], (LONG)velocity); + InterlockedExchange(&g_midi_engine->pending_note_num[idx], (LONG)(note + 1)); // +1 so 0 means "no pending" + InterlockedIncrement(&g_midi_engine->held_note_count[idx]); + } + // Note-off (0x80) or note-on with velocity 0 (running status note-off) + else if (kind == 0x80 || (kind == 0x90 && velocity == 0)) { + InterlockedExchange(&g_midi_engine->pending_note_off[idx], 1); + LONG count = InterlockedDecrement(&g_midi_engine->held_note_count[idx]); + if (count < 0) InterlockedExchange(&g_midi_engine->held_note_count[idx], 0); + } +} + MidiEngine *midi_create() { MidiEngine *engine = new MidiEngine(); - engine->device_count = 0; + memset(engine, 0, sizeof(*engine)); + g_midi_engine = engine; midi_refresh_devices(engine); return engine; } void midi_destroy(MidiEngine *engine) { + midi_close_all_inputs(engine); + if (g_midi_engine == engine) g_midi_engine = nullptr; delete engine; } +void midi_open_all_inputs(MidiEngine *engine) { + for (int32_t i = 0; i < engine->device_count; i++) { + MidiDeviceInfo *dev = &engine->devices[i]; + if (!dev->is_input) continue; + if (engine->input_handles[i]) continue; // already open + + HMIDIIN handle = nullptr; + MMRESULT res = midiInOpen(&handle, (UINT)dev->id, + (DWORD_PTR)midi_in_callback, + (DWORD_PTR)i, CALLBACK_FUNCTION); + if (res == MMSYSERR_NOERROR) { + engine->input_handles[i] = handle; + midiInStart(handle); + } + } +} + +void midi_close_all_inputs(MidiEngine *engine) { + for (int32_t i = 0; i < MIDI_MAX_DEVICES; i++) { + if (engine->input_handles[i]) { + midiInStop(engine->input_handles[i]); + midiInClose(engine->input_handles[i]); + engine->input_handles[i] = nullptr; + } + engine->pending_note_on_vel[i] = 0; + engine->pending_note_num[i] = 0; + engine->pending_note_off[i] = 0; + engine->held_note_count[i] = 0; + engine->display_velocity[i] = 0; + engine->display_note[i] = 0; + engine->release_timers[i] = 0.0f; + } +} + +void midi_update(MidiEngine *engine, float dt) { + for (int32_t i = 0; i < engine->device_count; i++) { + if (!engine->devices[i].is_input) continue; + + // Consume pending note-on velocity and note number + LONG vel = InterlockedExchange(&engine->pending_note_on_vel[i], 0); + LONG note_p1 = InterlockedExchange(&engine->pending_note_num[i], 0); + if (vel > 0) { + engine->display_velocity[i] = (int32_t)vel; + } + if (note_p1 > 0) { + engine->display_note[i] = (int32_t)(note_p1 - 1); + } + + // Consume pending note-off + LONG off = InterlockedExchange(&engine->pending_note_off[i], 0); + + // Read held note count + LONG held = engine->held_note_count[i]; + + if (held > 0) { + engine->devices[i].active = true; + engine->devices[i].releasing = false; + engine->release_timers[i] = 0.0f; + } else if (off || (engine->devices[i].active && held <= 0)) { + // All notes just released — start release flash + engine->devices[i].active = false; + engine->devices[i].releasing = true; + engine->release_timers[i] = MIDI_RELEASE_FLASH_DURATION; + } + + // Decay release flash timer + if (engine->release_timers[i] > 0.0f) { + engine->release_timers[i] -= dt; + if (engine->release_timers[i] <= 0.0f) { + engine->release_timers[i] = 0.0f; + engine->devices[i].releasing = false; + engine->display_velocity[i] = 0; + engine->display_note[i] = 0; + } + } + + engine->devices[i].velocity = engine->display_velocity[i]; + engine->devices[i].note = engine->display_note[i]; + } +} + +bool midi_is_input_active(MidiEngine *engine, int32_t device_index) { + if (device_index < 0 || device_index >= engine->device_count) + return false; + return engine->devices[device_index].active; +} + void midi_refresh_devices(MidiEngine *engine) { + midi_close_all_inputs(engine); engine->device_count = 0; UINT num_in = midiInGetNumDevs(); @@ -32,6 +172,7 @@ void midi_refresh_devices(MidiEngine *engine) { strncpy_s(dev->name, sizeof(dev->name), caps.szPname, _TRUNCATE); dev->id = (int32_t)i; dev->is_input = true; + dev->active = false; } } @@ -43,8 +184,11 @@ void midi_refresh_devices(MidiEngine *engine) { strncpy_s(dev->name, sizeof(dev->name), caps.szPname, _TRUNCATE); dev->id = (int32_t)i; dev->is_input = false; + dev->active = false; } } + + midi_open_all_inputs(engine); } int32_t midi_get_device_count(MidiEngine *engine) {