├── .gitignore ├── LICENSE ├── README.md ├── devmidi.cpp ├── devmidi.h ├── devmidi_custom.h ├── midi_wrap.cpp └── midi_wrap.h /.gitignore: -------------------------------------------------------------------------------- 1 | notes -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | ------------------------------------------------------------------------------ 3 | This software is available under 2 licenses -- choose whichever you prefer. 4 | ------------------------------------------------------------------------------ 5 | ALTERNATIVE A - MIT License 6 | Copyright (c) 2019 Anton Mikhailov 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 11 | of the Software, and to permit persons to whom the Software is furnished to do 12 | so, subject to the following conditions: 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | ------------------------------------------------------------------------------ 23 | ALTERNATIVE B - Public Domain (www.unlicense.org) 24 | This is free and unencumbered software released into the public domain. 25 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 26 | software, either in source code form or as a compiled binary, for any purpose, 27 | commercial or non-commercial, and by any means. 28 | In jurisdictions that recognize copyright laws, the author or authors of this 29 | software dedicate any and all copyright interest in the software to the public 30 | domain. We make this dedication for the benefit of the public at large and to 31 | the detriment of our heirs and successors. We intend this dedication to be an 32 | overt act of relinquishment in perpetuity of all present and future rights to 33 | this software under copyright law. 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 38 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 39 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 40 | ------------------------------------------------------------------------------ 41 | */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devmidi 2 | A simple MIDI input wrapper that dovetails nicely into [Dear ImGui](https://github.com/ocornut/imgui). 3 | It currently supports the two controllers I've found most useful in my own personal work: 4 | * [Midi Fighter Twister](https://www.midifighter.com/#Twister) for knobs 5 | * [Midi Fighter 3D](https://www.midifighter.com/#3D) for buttons 6 | 7 | It's easy to hack it to add your own. 8 | 9 | The lib currently lets you do the following: 10 | * Bind float values to knobs and ImGui sliders simultaneously. 11 | * Click the knobs for various convenient behavior like resetting to a default, printing, toggling. 12 | * Bind buttons to ImGui buttons, checkboxes, and radio buttons. 13 | * Read the raw data of the devices in a simple way. 14 | 15 | ## Motivation 16 | I've found it very handy to use MIDI controllers for various tuning and debugging tasks during development: 17 | * Color Grading - keep your eyes on the screen while tuning multiple parameters, without looking over at sliders or hunting with a mouse. 18 | * Visualize Graphics Buffers - press and hold a button to temporarily visualize an internal buffer or texture (like depth, normals or other such info). 19 | * Tune Gameplay Parameters - change common 'high frequency' tunings like speed, jump height, strengths. 20 | * Toggle Between Versions - use buttons as hardware 'radio buttons' to quickly toggle between various versions of code to compare them. 21 | * VR - do all of the above while wearing a VR headset, because hardware knobs and buttons can be operated easily without seeing them. 22 | 23 | Bringing in a full blown MIDI lib into projects that I work on often feels like overkill (especially if it's a tiny personal one), so this is my attempt to remedy this and make it easy to just drop the functionality in. I've also found that simply binding to a MIDI controller has some downsides: 24 | * You can't see the numeric value without logging it somewhere, and that adds some friction. 25 | * You can't set an exact numeric value, which is useful sometimes. 26 | * If your controller isn't plugged in (or if a teammate doesn't have a MIDI device) then that tuning is inaccessible. 27 | * Sometimes I don't have my controller plugged in (or am at a different machine) and just want to quickly use an app that requires it. 28 | 29 | So in order to address these downsides, I've made it easy to bind both to a knob/button and Dear ImGui widgets simultaneously. This gives it all the advantages of a hardware MIDI workflow, but with the fallback onto the already excellent ImGui workflow. ImGui thus handles numeric input and output, visualizes the colorpickers and other such things, for when that's useful. 30 | 31 | ## Goals 32 | This is meant to be a simple 'drop in' library, specifically for developer MIDI usage described above. It's not meant to be a generic MIDI lib or support any arbitrary device, though code is designed such that it's easy to add your own devices by hand. In practice, there seems to be a handful of useful midi devices for this particular purpose anyway. 33 | 34 | ## Usage 35 | The header should be pretty self explanatory. Functions with a 'twister' prefix use the Midi Fighter Twister knobs, and those with the 'fighter' prefix use the Midi Fighter 3D buttons. Some cool things to call out though: 36 | * When using the basic `twisterSliderFloat`, clicking the knob will copy the value to clipboard. This is handy when you just want to quickly bind a knob to some magic constant to tune it, and then copy paste the result back into code. The click will also print to console, if you supplied a print function in **devmidi_custom.h**. 37 | * Use `twisterSliderClickDefault` to set a value to a provided default when the knob is clicked. 38 | * Use `twisterSliderClickToggle` to quickly jump between the provided min and max values (in addition to tuning between them). 39 | * Use `twisterColorEdit` to edit colors. In HSV mode, the hue will wrap. 40 | * Use `fighterRadioButton` with multiple buttons to quickly toggle options. 41 | * Use `fighterCheckboxMomentary` to enable a checkbox only when a button is held down. Useful for momentarily turning on a visualization to quickly check for problems, rather than toggling it on permanently. 42 | 43 | ## "Architecture" (it's just some functions really) 44 | The library is actually in two pieces: the device specific API that is **devmidi.h/cpp**, and the underlying device agnostic **midi_wrap.h/cpp which** just listens on the devices and wrangles the output into a convenient format. Right now, it only listens for Continuous Controller and Note messages, since that's what I've found to be useful. This breakdown abstracts the bits of MIDI that I care about and makes it easier to add binds for new devices. 45 | The devmidi layer is meant to be hacked on so you can quickly add your own particular device and functionality. 46 | **devmidi_custom.h** is the place to provide your own print function for values, and also to define convenience wrappers for your own app specific vector structs (like vec3, float4, color3 that sort of thing). 47 | 48 | ## Building 49 | It should 'just work' if you drop it into a Visual Studio project. You might have to adjust the path to your ImGui in the code, but that's probably it. 50 | Right now, **midi_wrap** is Windows only, so while **devmidi** is platform agnostic, the whole thing only works on Windows. Presumably it shouldn't be hard to add Mac/Linux support to **midi_wrap**, since the MIDI protocol is the same, it's just the OS calls that need to change (and on Windows at least, it's like a half dozen API calls). I don't plan to do this work since I don't have machines to test on, but if this seems useful to you and you want to do it, I'm happy to help in spirit and discussion of what needs doing :) 51 | 52 | ## Future Work 53 | * It would be cool to actually send color commands to the twister/fighter to reflect the app values. 54 | * Both the fighter and twister have buttons to switch between 'banks' which would allow them to present 64 knobs/buttons instead of the current 16. This might be handy to support for larger apps, which might dedicate a given bank to a given task (like graphics or gameplay). In practice, it seems more useful to bank the actual data in software, but it would still be nice to use the hardware buttons to switch the banks. 55 | 56 | ## Which controller should I get? 57 | I get asked sometimes which controllers are best for this sort of workflow. 58 | I am not at all affiliated with Midi Fighter or DJ Tech Tools, but I've personally found their devices to be the best so far. I'm always on the lookout for new ones :) 59 | 60 | Here are some criteria to consider: 61 | * Absolute vs Relative Knobs - **This is the most important point:** I much prefer **endless** knobs that output **relative** values for this work. You almost certainly will want relative because you never know what value your app starts at, and you don't want to 'sync' your controller to those values. On the flipside, you don't want values you 'left' on your controller to be the 'defaults' for your app (the next day after tuning, you will forget that knob9 controlling brightness was left at 0.72 and be very confused that your screen seems 'a bit dark for some reason?'). You also don't want to have to deal with 'pickup' or 'syncing' for bounded knobs, best to just let it spin (the twister can be configured into absolute or relative, and has LED display for bounds, so in theory you can sync to them if you feel like it). Motorized knobs solve some of these issues, but they're extremely expensive for this niche usecase. Also with endless relative knobs you can easily control unbounded values like log-scale. 62 | * Knob encoder precision - You want the best obviously, but this directly impacts price (the twister has good bang-for-buck encoders). Lower precision encoder means that you have to turn the knob a lot more to cover a broader range with the same accuracy, which can get really tedious. 63 | * Knob click - This is very useful to 'reset' values to defaults, to print values, or to change precision. Can also be used as buttons. Clicking the knobs might change the values slightly (the code attempts to mitigate this, but it can still happen). If you want a totally 'rock solid' reset button, consider getting a controller with dedicated buttons next to each knob for that. 64 | * Knob count - You probably will end up using at least 8 (colors take 3-4 each). I've maxed out 16 in some apps, but it's hard to remember what they all do past that. 65 | * Knob arrangement - The 4x4 layout of the twister is nice and compact, and it's good for logical sets of 4 values (nice for colors). But it's worse for tuning an 8-knot easing spline (because you have to span two rows), for that a linear 1x8 or 1x16 layout is much more intuitive. 66 | * Knob/Button size - If you're doing this in VR, you might want them easy to find and press. 67 | 68 | Knobs are far more useful than buttons, so if you're only going to get one, just get the knobs. If the knobs are clickable, you can use them as buttons too in a pinch, but they don't feel nearly as nice. Knob rotation can also be used to set radio buttons, but again it's not as nice as doing it with buttons. Buttons are mainly useful for debugging (visualize a buffer, fire off an action), whereas knobs are most useful for tuning floats, colors, vectors, integers etc. -------------------------------------------------------------------------------- /devmidi.cpp: -------------------------------------------------------------------------------- 1 | #include "devmidi.h" 2 | #include "midi_wrap.h" 3 | #include "imgui/imgui.h" 4 | #include // for sprintf_s 5 | 6 | /*----------------------------*/ 7 | /* Core init/update/teardown. */ 8 | /*----------------------------*/ 9 | 10 | static MIDIDeviceConfig midi_device_config[] = 11 | { 12 | MIDIDeviceConfig("Midi Fighter Twister", MIDI_CONTINUOUS_CONTROLLER_RELATIVE), 13 | MIDIDeviceConfig("Midi Fighter 3D"), 14 | }; 15 | 16 | static int write_idx = 0; 17 | static MIDIState twister[2]; 18 | static MIDIState fighter[2]; 19 | 20 | static MIDIState* twister_prev; 21 | static MIDIState* twister_curr; 22 | static MIDIState* fighter_prev; 23 | static MIDIState* fighter_curr; 24 | 25 | void devmidiInit() { 26 | midiInit(midi_device_config, sizeof(midi_device_config) / sizeof(midi_device_config[0])); 27 | } 28 | void devmidiTerm() { 29 | midiTerm(); 30 | } 31 | void devmidiUpdate() { 32 | if (midiGetState(&twister[write_idx], 0)) { 33 | } 34 | if (midiGetState(&fighter[write_idx], 1)) { 35 | } 36 | twister_curr = &twister[write_idx]; 37 | twister_prev = &twister[1 - write_idx]; 38 | fighter_curr = &fighter[write_idx]; 39 | fighter_prev = &fighter[1 - write_idx]; 40 | write_idx = 1 - write_idx; 41 | } 42 | 43 | /*--------------------*/ 44 | /* Generic functions. */ 45 | /*--------------------*/ 46 | 47 | static bool Press(MIDIState* curr, MIDIState* prev, int button) { 48 | return (curr->note_ons[button] - prev->note_ons[button]) > 0; 49 | } 50 | static bool Release(MIDIState* curr, MIDIState* prev, int button) { 51 | return (curr->note_offs[button] - prev->note_offs[button]) > 0; 52 | } 53 | static bool Down(MIDIState* curr, MIDIState* prev, int button) { 54 | return curr->note_ons[button] > prev->note_offs[button]; 55 | } 56 | static bool ButtonPress(const char* id, MIDIState* curr, MIDIState* prev, int button) { 57 | return ImGui::Button(id) || Press(curr, prev, button); 58 | } 59 | static bool ButtonRelease(const char* id, MIDIState* curr, MIDIState* prev, int button) { 60 | return ImGui::Button(id) || Release(curr, prev, button); 61 | } 62 | static bool RadioButton(const char* id, MIDIState* curr, MIDIState* prev, int button, int* v, int v_button) { 63 | bool touched = false; 64 | if (Press(curr, prev, button)) { 65 | *v = v_button; 66 | touched = true; 67 | } 68 | return ImGui::RadioButton(id, v, v_button) || touched; 69 | } 70 | static bool Checkbox(const char* id, MIDIState* curr, MIDIState* prev, int button, bool* v) { 71 | bool touched = false; 72 | if (Press(curr, prev, button)) { 73 | *v = !*v; 74 | touched = true; 75 | } 76 | return ImGui::Checkbox(id, v) | touched; 77 | } 78 | static bool CheckboxMomentary(const char* id, MIDIState* curr, MIDIState* prev, int button, bool* v) { 79 | bool touched = false; 80 | if (Press(curr, prev, button) || Release(curr, prev, button)) { 81 | *v = !*v; 82 | touched = true; 83 | } 84 | return ImGui::Checkbox(id, v) | touched; 85 | } 86 | static bool SliderFloat(const char* id, MIDIState* curr, MIDIState* prev, int knob, float* v, float v_min, float v_max, const char* format, float power) { 87 | float knob_scale = 1.0f / 100.0f; 88 | if (format) { 89 | const char* dig = format; 90 | while (*dig && (*dig++ != '.')); 91 | if (*dig) { 92 | knob_scale = 1.0f / powf(10.0f, float(atoi(dig))); 93 | } 94 | } 95 | int del = curr->value[knob] - prev->value[knob]; 96 | float linear_v = power == 1.0f ? *v : powf(*v, 1.0f / power); 97 | linear_v = min(v_max, max(v_min, linear_v + del * knob_scale)); 98 | *v = power == 1.0f ? linear_v : powf(linear_v, power); 99 | 100 | return ImGui::SliderFloat(id, v, v_min, v_max, format, power) || del != 0; 101 | } 102 | static bool SliderFloatN(int components, const char* id, MIDIState* curr, MIDIState* prev, const int* knobs, float* v, const float* v_defaults, float v_min, float v_max, const char* format, float power) { 103 | ImGui::BeginGroup(); 104 | ImGui::PushID(id); 105 | ImGui::PushItemWidth(ImGui::CalcItemWidth() / components); 106 | ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImGui::GetStyle().ItemInnerSpacing); 107 | bool touched = false; 108 | for (int i = 0; i < components; ++i) { 109 | ImGui::PushID(i); 110 | touched |= SliderFloat("", curr, prev, knobs[i], v + i, v_min, v_max, format, power); 111 | ImGui::SameLine(); 112 | if (v_defaults) { 113 | if (ButtonRelease("def", curr, prev, knobs[i])) { 114 | v[i] = v_defaults[i]; 115 | touched = true; 116 | } 117 | ImGui::SameLine(); 118 | } 119 | ImGui::PopID(); 120 | } 121 | ImGui::Text(id); 122 | ImGui::PopStyleVar(); 123 | ImGui::PopItemWidth(); 124 | ImGui::PopID(); 125 | ImGui::EndGroup(); 126 | return touched; 127 | } 128 | static void Print(int components, const char* id, MIDIState* curr, MIDIState* prev, const int* knobs, float* v, const char* format) { 129 | bool any_pressed = false; 130 | for (int i = 0; i < components; ++i) 131 | any_pressed |= Press(curr, prev, knobs[i]); 132 | if (any_pressed) { 133 | static char buff[256]; 134 | sprintf_s(buff, sizeof(buff), "%s: ", id); 135 | DEVMIDI_PRINT(buff); 136 | char* w = buff; 137 | for (int i = 0; i < components; ++i) { 138 | if (i > 0) 139 | w += sprintf_s(w, sizeof(buff), ", "); 140 | w += sprintf_s(w, sizeof(buff), format, v[i]); 141 | } 142 | DEVMIDI_PRINT(buff); 143 | DEVMIDI_PRINT("\n"); 144 | ImGui::SetClipboardText(buff); 145 | } 146 | } 147 | static bool SliderFloatNClickPrint(int components, const char* id, MIDIState* curr, MIDIState* prev, const int* knobs, float* v, float* v_defaults, float v_min, float v_max, const char* format, float power) { 148 | (void)v_defaults; 149 | Print(components, id, curr, prev, knobs, v, format); 150 | return SliderFloatN(components, id, curr, prev, knobs, v, 0, v_min, v_max, format, power); 151 | } 152 | static bool SliderFloatNClickToggle(int components, const char* id, MIDIState* curr, MIDIState* prev, const int* knobs, float* v, float* v_defaults, float v_min, float v_max, const char* format, float power) { 153 | (void)v_defaults; 154 | bool touched = false; 155 | for (int i = 0; i < components; ++i) { 156 | if (Release(curr, prev, knobs[i])) { 157 | // Toggle to the extreme that is farthest from the current value. 158 | if (fabs(v[i] - v_min) < fabs(v[i] - v_max)) 159 | v[i] = v_max; 160 | else 161 | v[i] = v_min; 162 | touched = true; 163 | } 164 | } 165 | return SliderFloatN(components, id, curr, prev, knobs, v, 0, v_min, v_max, format, power) || touched; 166 | } 167 | static bool ColorEditN(int components, const char* id, MIDIState* curr, MIDIState* prev, const int* knobs, float* v, float* v_defaults, ImGuiColorEditFlags flags) { 168 | if (!v_defaults) 169 | Print(components, id, curr, prev, knobs, v, "%.3f"); 170 | 171 | bool touched = false; 172 | float edit[4]; 173 | /* Copy color into the edit by default. */ 174 | for (int i = 0; i < components; ++i) 175 | edit[i] = v[i]; 176 | /* If we are editing in HSV and it's not the native format, convert into that first. */ 177 | bool convert_to_hsv = (flags & ImGuiColorEditFlags_DisplayHSV) && !(flags & ImGuiColorEditFlags_InputHSV); 178 | if (convert_to_hsv) 179 | ImGui::ColorConvertRGBtoHSV(v[0], v[1], v[2], edit[0], edit[1], edit[2]); 180 | /* Perform the editing operation. */ 181 | for (int i = 0; i < components; ++i) { 182 | int del = curr->value[knobs[i]] - prev->value[knobs[i]]; 183 | edit[i] += del * 1.0f / 256.0f; 184 | /* In HSV, loop the color. In RGB, saturate it. */ 185 | if ((convert_to_hsv || (flags & ImGuiColorEditFlags_InputHSV)) && i == 0) 186 | edit[i] = edit[i] - floorf(edit[i]); 187 | else 188 | edit[i] = max(0.0f, min(1.0f, edit[i])); 189 | if (v_defaults) { 190 | if (Release(curr, prev, knobs[i])) { 191 | edit[i] = v_defaults[i]; 192 | touched = true; 193 | } 194 | } 195 | touched |= del != 0; 196 | } 197 | /* Convert back to RGB if we converted to HSV earlier. */ 198 | if (convert_to_hsv) { 199 | ImGui::ColorConvertHSVtoRGB(edit[0], edit[1], edit[2], v[0], v[1], v[2]); 200 | v[3] = edit[3]; 201 | } 202 | else { 203 | for (int i = 0; i < components; ++i) 204 | v[i] = edit[i]; 205 | } 206 | 207 | /* Perform reset to defaults, if needed. */ 208 | if (v_defaults) { 209 | for (int i = 0; i < components; ++i) { 210 | if (Release(curr, prev, knobs[i])) { 211 | edit[i] = v_defaults[i]; 212 | touched = true; 213 | } 214 | } 215 | } 216 | 217 | if (components == 3) 218 | return ImGui::ColorEdit3(id, v, flags) || touched; 219 | else if (components == 4) 220 | return ImGui::ColorEdit4(id, v, flags) || touched; 221 | else { 222 | assert(false); 223 | return false; 224 | } 225 | } 226 | 227 | /*----------------------------------------------*/ 228 | /* Specific functions tuned to the midi device. */ 229 | /*----------------------------------------------*/ 230 | 231 | // I have my device with it's cord up and away from me, and I want knob 0 to be the lower left hand corner. 232 | static int twisterKnobRemap(int knob) { 233 | // I don't know how to deal with more than 16 knobs yet. 234 | if (knob > 15) return 0; 235 | // First we need to flip the button upside down. So remap it to a 4x4, flip the y, then go back to idx. 236 | int x = knob % 4; 237 | int y = knob / 4; 238 | y = 3 - y; 239 | return x + y * 4; 240 | } 241 | // I have my device with it's cord up away from me, and I want button 0 to be the lower left hand corner. 242 | static int fighterButtonRemap(int button) { 243 | // I don't know how to deal with more than 16 buttons yet. 244 | if (button > 15) return 0; 245 | return 51 - button; // first set of fighter buttons go from 36 to 51 246 | } 247 | 248 | bool twisterSliderFloat(const char* id, int knob, float* v, float v_min, float v_max, const char* format, float power) { 249 | int knobs[] = { twisterKnobRemap(knob) }; 250 | return SliderFloatNClickPrint(1, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 251 | } 252 | bool twisterSliderFloat2(const char* id, int knob0, int knob1, float* v, float v_min, float v_max, const char* format, float power) { 253 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1) }; 254 | return SliderFloatNClickPrint(2, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 255 | } 256 | bool twisterSliderFloat3(const char* id, int knob0, int knob1, int knob2, float* v, float v_min, float v_max, const char* format, float power) { 257 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2) }; 258 | return SliderFloatNClickPrint(3, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 259 | } 260 | bool twisterSliderFloat4(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float v_min, float v_max, const char* format, float power) { 261 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2), twisterKnobRemap(knob3) }; 262 | return SliderFloatNClickPrint(4, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 263 | } 264 | bool twisterSliderFloatClickDefault(const char* id, int knob, float* v, float v_default, float v_min, float v_max, const char* format, float power) { 265 | int knobs[] = { twisterKnobRemap(knob) }; 266 | return SliderFloatN(1, id, twister_curr, twister_prev, knobs, v, &v_default, v_min, v_max, format, power); 267 | } 268 | bool twisterSliderFloat2ClickDefault(const char* id, int knob0, int knob1, float* v, float* v_defaults, float v_min, float v_max, const char* format, float power) { 269 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1) }; 270 | return SliderFloatN(2, id, twister_curr, twister_prev, knobs, v, v_defaults, v_min, v_max, format, power); 271 | } 272 | bool twisterSliderFloat3ClickDefault(const char* id, int knob0, int knob1, int knob2, float* v, float* v_defaults, float v_min, float v_max, const char* format, float power) { 273 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2) }; 274 | return SliderFloatN(3, id, twister_curr, twister_prev, knobs, v, v_defaults, v_min, v_max, format, power); 275 | } 276 | bool twisterSliderFloat4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float* v_defaults, float v_min, float v_max, const char* format, float power) { 277 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2), twisterKnobRemap(knob3) }; 278 | return SliderFloatN(4, id, twister_curr, twister_prev, knobs, v, v_defaults, v_min, v_max, format, power); 279 | } 280 | bool twisterSliderFloatClickToggle(const char* id, int knob, float* v, float v_min, float v_max, const char* format, float power) { 281 | int knobs[] = { twisterKnobRemap(knob) }; 282 | return SliderFloatNClickToggle(1, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 283 | } 284 | bool twisterSliderFloat2ClickToggle(const char* id, int knob0, int knob1, float* v, float v_min, float v_max, const char* format, float power) { 285 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1) }; 286 | return SliderFloatNClickToggle(2, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 287 | } 288 | bool twisterSliderFloat3ClickToggle(const char* id, int knob0, int knob1, int knob2, float* v, float v_min, float v_max, const char* format, float power) { 289 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2) }; 290 | return SliderFloatNClickToggle(3, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 291 | } 292 | bool twisterSliderFloat4ClickToggle(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float v_min, float v_max, const char* format, float power) { 293 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2), twisterKnobRemap(knob3) }; 294 | return SliderFloatNClickToggle(4, id, twister_curr, twister_prev, knobs, v, 0, v_min, v_max, format, power); 295 | } 296 | bool twisterColorEdit3(const char* id, int knob0, int knob1, int knob2, float col[3], ImGuiColorEditFlags flags) { 297 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2) }; 298 | return ColorEditN(3, id, twister_curr, twister_prev, knobs, col, 0, flags); 299 | } 300 | bool twisterColorEdit4(const char* id, int knob0, int knob1, int knob2, int knob3, float col[4], ImGuiColorEditFlags flags) { 301 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2), twisterKnobRemap(knob3) }; 302 | return ColorEditN(4, id, twister_curr, twister_prev, knobs, col, 0, flags); 303 | } 304 | bool twisterColorEdit3ClickDefault(const char* id, int knob0, int knob1, int knob2, float col[3], float col_default[3], ImGuiColorEditFlags flags) { 305 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2) }; 306 | return ColorEditN(3, id, twister_curr, twister_prev, knobs, col, col_default, flags); 307 | } 308 | bool twisterColorEdit4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, float col[4], float col_default[4], ImGuiColorEditFlags flags) { 309 | int knobs[] = { twisterKnobRemap(knob0), twisterKnobRemap(knob1), twisterKnobRemap(knob2), twisterKnobRemap(knob3) }; 310 | return ColorEditN(4, id, twister_curr, twister_prev, knobs, col, col_default, flags); 311 | } 312 | bool twisterKnobButton(const char* id, int knob) { 313 | return ButtonPress(id, twister_curr, twister_prev, twisterKnobRemap(knob)); 314 | } 315 | bool fighterRadioButton(const char* id, int button, int* v, int v_button) { 316 | return RadioButton(id, fighter_curr, fighter_prev, fighterButtonRemap(button), v, v_button); 317 | } 318 | bool fighterCheckbox(const char* id, int button, bool* v) { 319 | return Checkbox(id, fighter_curr, fighter_prev, fighterButtonRemap(button), v); 320 | } 321 | bool fighterCheckboxMomentary(const char* id, int button, bool* v) { 322 | return CheckboxMomentary(id, fighter_curr, fighter_prev, fighterButtonRemap(button), v); 323 | } 324 | int twisterKnobValue(int knob) { 325 | return twister_curr->value[twisterKnobRemap(knob)]; 326 | } 327 | int twisterKnobDelta(int knob) { 328 | return twister_curr->value[twisterKnobRemap(knob)] - twister_prev->value[twisterKnobRemap(knob)]; 329 | } 330 | bool twisterKnobPress(int knob) { 331 | return Press(twister_curr, twister_prev, twisterKnobRemap(knob)); 332 | } 333 | bool twisterKnobRelease(int knob) { 334 | return Release(twister_curr, twister_prev, twisterKnobRemap(knob)); 335 | } 336 | bool twisterKnobDown(int knob) { 337 | return Down(twister_curr, twister_prev, twisterKnobRemap(knob)); 338 | } 339 | bool fighterButton(const char* id, int button) { 340 | return ButtonPress(id, fighter_curr, fighter_prev, fighterButtonRemap(button)); 341 | } 342 | bool fighterPress(int button) { 343 | return Press(fighter_curr, fighter_prev, fighterButtonRemap(button)); 344 | } 345 | bool fighterRelease(int button) { 346 | return Release(fighter_curr, fighter_prev, fighterButtonRemap(button)); 347 | } 348 | bool fighterDown(int button) { 349 | return Down(fighter_curr, fighter_prev, fighterButtonRemap(button)); 350 | } -------------------------------------------------------------------------------- /devmidi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "imgui/imgui.h" // for ImGuiColorEditFlags 4 | 5 | /* Simple API to address my particular dev midi setup. */ 6 | /* Allows binding values to a particular midi controller and ImGui at the same time. */ 7 | 8 | /* Initialize the library (opens midi devices). Call once at app start. */ 9 | void devmidiInit(); 10 | /* Poll the midi data. Call once every frame, before the app reads the inputs. */ 11 | void devmidiUpdate(); 12 | /* Terminate the library (closes midi devices). Call once at app end. */ 13 | void devmidiTerm(); 14 | 15 | /* Standard ImGui Slider. On knob click, print the value to console and to clipboard. */ 16 | bool twisterSliderFloat(const char* id, int knob, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 17 | bool twisterSliderFloat2(const char* id, int knob0, int knob1, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 18 | bool twisterSliderFloat3(const char* id, int knob0, int knob1, int knob2, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 19 | bool twisterSliderFloat4(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 20 | /* On knob click, reset value to the specified default. */ 21 | bool twisterSliderFloatClickDefault(const char* id, int knob, float* v, float v_default, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 22 | bool twisterSliderFloat2ClickDefault(const char* id, int knob0, int knob1, float* v, float* v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 23 | bool twisterSliderFloat3ClickDefault(const char* id, int knob0, int knob1, int knob2, float* v, float* v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 24 | bool twisterSliderFloat4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float* v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 25 | /* On knob click, toggle the value to min or max (whichever is farther from the current value). */ 26 | bool twisterSliderFloatClickToggle(const char* id, int knob, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 27 | bool twisterSliderFloat2ClickToggle(const char* id, int knob0, int knob1, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 28 | bool twisterSliderFloat3ClickToggle(const char* id, int knob0, int knob1, int knob2, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 29 | bool twisterSliderFloat4ClickToggle(const char* id, int knob0, int knob1, int knob2, int knob3, float* v, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f); 30 | /* Standard ImGui Color Edit. On knob click, print the value to console and to clipboard. */ 31 | bool twisterColorEdit3(const char* id, int knob0, int knob1, int knob2, float col[3], ImGuiColorEditFlags flags = 0); 32 | bool twisterColorEdit4(const char* id, int knob0, int knob1, int knob2, int knob3, float col[4], ImGuiColorEditFlags flags = 0); 33 | /* On knob click, reset value to the specified default. */ 34 | bool twisterColorEdit3ClickDefault(const char* id, int knob0, int knob1, int knob2, float col[3], float col_default[3], ImGuiColorEditFlags flags = 0); 35 | bool twisterColorEdit4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, float col[4], float col_default[4], ImGuiColorEditFlags flags = 0); 36 | 37 | /* Standard ImGui Button operated by knob click. */ 38 | bool twisterKnobButton(const char* id, int knob); 39 | /* Raw knob value. */ 40 | int twisterKnobValue(int knob); 41 | /* Raw knob value delta from previous to current frame. */ 42 | int twisterKnobDelta(int knob); 43 | /* Was the knob pressed. */ 44 | bool twisterKnobPress(int knob); 45 | /* Was the knob released. */ 46 | bool twisterKnobRelease(int knob); 47 | /* Is the knob being held down. */ 48 | bool twisterKnobDown(int knob); 49 | 50 | /* Standard ImGui Button. */ 51 | bool fighterButton(const char* id, int button); 52 | /* Standard ImGui Radio Button. */ 53 | bool fighterRadioButton(const char* id, int button, int* v, int v_button); 54 | /* Standard ImGui Checkbox. */ 55 | bool fighterCheckbox(const char* id, int button, bool* v); 56 | /* A checkbox that is toggled both on Press and Release. The effect is a 'down' or 'momentary' checkbox. */ 57 | bool fighterCheckboxMomentary(const char* id, int button, bool* v); 58 | /* Was the button pressed. */ 59 | bool fighterPress(int button); 60 | /* Was the button released. */ 61 | bool fighterRelease(int button); 62 | /* Is the button down. */ 63 | bool fighterDown(int button); 64 | 65 | /* - Custom convenience wrappers based on app-specific vector structs. */ 66 | /* - Printing functions for when a knob is clicked. */ 67 | #include "devmidi_custom.h" -------------------------------------------------------------------------------- /devmidi_custom.h: -------------------------------------------------------------------------------- 1 | /* Function print the knob values to console or elsewhere. */ 2 | /* Define to something like printf, but it only needs to handle a single const char* argument, not formatting. */ 3 | #define DEVMIDI_PRINT(str) 4 | 5 | /* Some convenience wrappers, specific to this app's vector structs. */ 6 | /* For example: 7 | inline bool twisterSliderFloat2ClickDefault(const char* id, int knob0, int knob1, float* v, vec2 v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f) { 8 | return twisterSliderFloat2ClickDefault(id, knob0, knob1, v, (float*)&v_defaults, v_min, v_max, format, power); 9 | } 10 | inline bool twisterSliderFloat3ClickDefault(const char* id, int knob0, int knob1, int knob2, float* v, vec3 v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f) { 11 | return twisterSliderFloat3ClickDefault(id, knob0, knob1, knob2, v, (float*)&v_defaults, v_min, v_max, format, power); 12 | } 13 | inline bool twisterSliderFloat4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, vec4 v, float* v_defaults, float v_min = 0.0f, float v_max = 1.0f, const char* format = "%.2f", float power = 1.0f) { 14 | return twisterSliderFloat4ClickDefault(id, knob0, knob1, knob2, knob3, v, (float*)&v_defaults, v_min, v_max, format, power); 15 | } 16 | inline bool twisterColorEdit3ClickDefault(const char* id, int knob0, int knob1, int knob2, float col[3], vec3 col_default, ImGuiColorEditFlags flags = 0) { 17 | return twisterColorEdit3ClickDefault(id, knob0, knob1, knob2, col, (float*)&col_default, flags); 18 | } 19 | inline bool twisterColorEdit4ClickDefault(const char* id, int knob0, int knob1, int knob2, int knob3, float col[4], vec4 col_default, ImGuiColorEditFlags flags = 0) { 20 | return twisterColorEdit4ClickDefault(id, knob0, knob1, knob2, knob3, col, (float*)&col_default, flags); 21 | } 22 | */ -------------------------------------------------------------------------------- /midi_wrap.cpp: -------------------------------------------------------------------------------- 1 | #include "midi_wrap.h" 2 | 3 | // order dependent :( 4 | #include // first, for UINT 5 | #include //second, for midi 6 | #pragma comment(lib,"winmm.lib") // for midi 7 | 8 | #include 9 | #include 10 | 11 | /* Define to something like printf in order to log status messages (found devices, errors, etc.) */ 12 | #define MIDIWRAP_LOG_PRINTF(...) 13 | 14 | /* Define to something like printf in order to print all incoming midi messages. Useful for working out mappings for new devices. */ 15 | #define MIDIWRAP_MESSAGE_PRINTF(...) 16 | 17 | namespace { 18 | 19 | struct Device { 20 | int config_id; 21 | const char* name; 22 | int continuous_mode; 23 | HMIDIIN handle; 24 | MIDIState state; 25 | Device(int _id, const char* _name, int _continuous_mode) : config_id(_id), name(_name), continuous_mode(_continuous_mode), handle(0) { 26 | memset(&state, 0, sizeof(state)); 27 | } 28 | }; 29 | 30 | } 31 | 32 | /* State */ 33 | static std::mutex cb_mutex; 34 | static std::vector devices; 35 | 36 | void CALLBACK midicb(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dev_idx, DWORD_PTR dwParam1, DWORD_PTR dwParam2) { 37 | (void)hMidiIn; 38 | (void)dwParam2; 39 | if (wMsg == MIM_DATA) { 40 | Device* device = (Device*)&devices[dev_idx]; 41 | int type = (dwParam1 >> 4) & 0xf; 42 | if (type == 0xb) { // Continuous Controller 43 | std::unique_lock lock(cb_mutex); 44 | int idx = (dwParam1 >> 8) & 0xff; 45 | int val = (dwParam1 >> 16) & 0xff; 46 | if (device->continuous_mode == MIDI_CONTINUOUS_CONTROLLER_ABSOLUTE) 47 | device->state.value[idx] = val; 48 | else 49 | device->state.value[idx] += val - 64; // relative control is signed and biased around 64 50 | MIDIWRAP_MESSAGE_PRINTF("dev%d : [0xb = Cont Ctrl] [idx %d] [val %d]\n", dev_idx, idx, val); 51 | } else if (type == 0x8) { // Note Off 52 | std::unique_lock lock(cb_mutex); 53 | int idx = (dwParam1 >> 8) & 0xff; 54 | device->state.note_offs[idx] += 1; 55 | MIDIWRAP_MESSAGE_PRINTF("dev%d : [0x8 = Note Off ] [idx %d] [vel %d]\n", dev_idx, idx, ((dwParam1 >> 16) & 0xff)); 56 | } else if (type == 0x9) { // Note On 57 | std::unique_lock lock(cb_mutex); 58 | int idx = (dwParam1 >> 8) & 0xff; 59 | device->state.note_ons[idx] += 1; 60 | MIDIWRAP_MESSAGE_PRINTF("dev%d : [0x9 = Note On ] [idx %d] [vel %d]\n", dev_idx, idx, ((dwParam1 >> 16) & 0xff)); 61 | } else { 62 | MIDIWRAP_MESSAGE_PRINTF("[MIDI] Unknown input: %08x (type=%08x idx=%d val=%d)\n", dwParam1, (dwParam1 >> 4) & 0xf, (dwParam1 >> 8) & 0xff, (dwParam1 >> 16) & 0xff); 63 | } 64 | } 65 | } 66 | 67 | void midiInit(MIDIDeviceConfig* configs, int configs_count) { 68 | UINT numdevs = midiInGetNumDevs(); 69 | for (UINT d = 0; d < numdevs; ++d) { 70 | MIDIINCAPS caps; 71 | if (midiInGetDevCaps(d, &caps, sizeof(caps)) == MMSYSERR_NOERROR) { 72 | MIDIWRAP_LOG_PRINTF("[MIDI] %d - Device '%s'\n", d, caps.szPname); 73 | } 74 | } 75 | for (UINT d = 0; d < numdevs; ++d) { 76 | MIDIINCAPS caps; 77 | if (midiInGetDevCaps(d, &caps, sizeof(caps)) == MMSYSERR_NOERROR) { 78 | for (int c = 0; c < configs_count; ++c) { 79 | if (strcmp(caps.szPname, configs[c].name) == 0) { 80 | MIDIWRAP_LOG_PRINTF("[MIDI] Selecting Device %d: '%s' %x\n", d, caps.szPname, caps.dwSupport); 81 | devices.push_back(Device(c, configs[c].name, configs[c].continuous_mode)); 82 | if (midiInOpen(&devices.back().handle, d, (DWORD_PTR)midicb, devices.size()-1, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { 83 | devices.pop_back(); 84 | MIDIWRAP_LOG_PRINTF("[MIDI] midiOpen failed!\n"); 85 | continue; 86 | } 87 | if (midiInStart(devices.back().handle) != MMSYSERR_NOERROR) { 88 | midiInClose(devices.back().handle); 89 | devices.pop_back(); 90 | MIDIWRAP_LOG_PRINTF("[MIDI] midiStart failed!\n"); 91 | continue; 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | void midiTerm() { 99 | for (Device& dev : devices) { 100 | midiInClose(dev.handle); 101 | } 102 | } 103 | 104 | bool midiGetState(MIDIState* state_out, int device_id, int num) { 105 | for (Device& dev : devices) { 106 | if (dev.config_id == device_id) { 107 | if (num == 0) { 108 | std::unique_lock lock(cb_mutex); 109 | memcpy(state_out, &dev.state, sizeof(MIDIState)); 110 | return true; 111 | } else { 112 | num -= 1; // Keep searching for our device. 113 | } 114 | } 115 | } 116 | return false; 117 | } -------------------------------------------------------------------------------- /midi_wrap.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /* The maximum number of inputs in one MIDI device (spec). */ 4 | #define MIDI_INPUT_COUNT_MAX 128 5 | 6 | enum { 7 | MIDI_CONTINUOUS_CONTROLLER_ABSOLUTE = 0, 8 | MIDI_CONTINUOUS_CONTROLLER_RELATIVE = 1, 9 | }; 10 | 11 | struct MIDIDeviceConfig { 12 | const char* name; 13 | int continuous_mode; 14 | MIDIDeviceConfig(const char* _name, int _continuous_mode = MIDI_CONTINUOUS_CONTROLLER_ABSOLUTE) : name(_name), continuous_mode(_continuous_mode) {} 15 | }; 16 | 17 | struct MIDIState { 18 | int note_ons[MIDI_INPUT_COUNT_MAX]; 19 | int note_offs[MIDI_INPUT_COUNT_MAX]; 20 | int value[MIDI_INPUT_COUNT_MAX]; 21 | }; 22 | 23 | /* Initialize given a list of device configurations. */ 24 | /* The order of the configurations determines the device id which is used to get state. */ 25 | void midiInit(MIDIDeviceConfig* configs, int configs_count); 26 | 27 | /* Close all active midi devices. */ 28 | void midiTerm(); 29 | 30 | /* Get the state for a given config index, which corresponds to the array order that was passed to init. */ 31 | /* If there is more than one device of that type, use num to select which one. */ 32 | /* Returns true if the device was found and valid state was written. Otherwise, false is returned, and state is untouched. */ 33 | bool midiGetState(MIDIState* state_out, int config_idx, int num = 0); --------------------------------------------------------------------------------