├── breadboard.png ├── stripboard.png ├── hx-stomp-midi-switcher-breadboard.fzz ├── hx-stomp-midi-switcher-stripboard.fzz ├── README.md └── hxsMidiSwitcher.ino /breadboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoonoon/hxsMidiSwitcher/HEAD/breadboard.png -------------------------------------------------------------------------------- /stripboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoonoon/hxsMidiSwitcher/HEAD/stripboard.png -------------------------------------------------------------------------------- /hx-stomp-midi-switcher-breadboard.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoonoon/hxsMidiSwitcher/HEAD/hx-stomp-midi-switcher-breadboard.fzz -------------------------------------------------------------------------------- /hx-stomp-midi-switcher-stripboard.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stoonoon/hxsMidiSwitcher/HEAD/hx-stomp-midi-switcher-stripboard.fzz -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hxsMidiSwitcher 2 | Simple 8 button MIDI controller for Line 6 HX Stomp using Nano clone 3 | 4 | I'd originally shared this at https://www.basschat.co.uk/topic/338238-build-your-own-midi-controller-i-did/ but it's been pointed out (Thanks Josh!) that this would be a more sensible place for it. 5 | 6 | It works OK and has served me fairly well over the last ~8 months I've been using it, but v2 is in the works with a touchscreen and expression pedal input... 7 | 8 | Basic description (from that thread - although there's more info there if you want to trawl through the entire thread... 9 | 10 | I wanted to add some more control options to my Stomp and was looking at controllers like the Morningstar MC6, but couldn't justify the £200 outlay.. so looked into making one myself - and it's turned out to be far easier and cheaper than I expected. 11 | 12 | Total cost for all the electronics and switches is about £25 -30 depending on how confident you are at slightly fiddly soldering... the extra few quid gets you a pre-soldered microcontroller and an adaptor board with screw terminals, so the only soldering you'd need to do is for the wires to the tags on the footswitches and on the MIDI socket. 13 | 14 | And then you'll need something to house it in. I made a 3d printed enclosure for mine, but any box you can buy/build/modify will do. 15 | 16 | If anyone's interested in making something similar for themselves, I'd be happy to share wiring diagrams and the code I used. I'm no electronic engineer or programmer, so no doubt there's plenty of room for improvement, but I've got it to work well enough to do what I need it to, and maybe it'll work for you too? I'm happy to help with tweaking it for slightly different configurations if you'd want a different layout. 17 | 18 | What it does: 19 | 20 | You press a footswitch, and it sends a MIDI message. The way I have mine setup is that it either sends a MIDI PC (Program Change) message to change preset patch on my HX stomp, or a MIDI CC (Control Change) message to change any of a number of other settings or parameters. It could potentially send MIDI notes as well if you wanted to use it as a pedal keyboard to play a synth with, I suppose. 21 | 22 | If you hold a footswitch down, it can send a different MIDI message (I only use this for activating the tuner by holding the tap tempo button so far, but it could be setup for any of the switches) 23 | 24 | If you press more than one footswitch down at once, it can do something else again depending on which ones you press. Mine is set so that pressing 1+2 or 2+3 switches through different pages / banks of button configurations, and pressing 7 + 8 is a sort of panic mode which resets the HX stomp to preset 1, and resets the controller back to the first page. 25 | 26 | What it can't do: 27 | 28 | It's not programmable from the unit. To change the way the buttons are configured you need to tweak the code and then re-upload it. There's probably a more elegant way to deal with this, but it works well enough for me as is - I don't expect to need to reconfigure it very often once I've got it set up. 29 | 30 | Right - I'm gonna stop there for now.... if no-one's interested then I'm just rambling into the void for nowt. If anyone is interested though, let us know and I can post more details. Or, if you know more than me and can spot where I've gone wrong or what I should have done better - lemme know and I'll try and improve it! 31 | 32 | 33 | 34 | Cheers, 35 | 36 | 37 | 38 | stoo 39 | -------------------------------------------------------------------------------- /hxsMidiSwitcher.ino: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * --------------------------------------------------- 4 | * 5 | * Simple 8 button MIDI controller for Line 6 HX Stomp using Nano clone 6 | * 7 | * MIDI connectivity based on https://www.instructables.com/id/Send-and-Receive-MIDI-with-Arduino/ 8 | * MIDI pin 4 to TX1 9 | * 10 | * 128x64 I2C OLED - the one I used was https://smile.amazon.co.uk/gp/product/B076PL474K 11 | * SDA to A4 12 | * SCL to A5 13 | * 14 | * 8x momentary SPST switches connected between GND and D2 - D9 15 | * 16 | * --------------------------------------------------- 17 | */ 18 | 19 | 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) 29 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 30 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 31 | #define OLED_RESET -1 // Reset pin # ((was 4) or -1 if sharing Arduino reset pin) 32 | #define OLED_ADDR 0x3C // address of my 128x64 OLED 33 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 34 | 35 | // error blink codes 36 | #define ERR_DISP_ALLOC 3 // display allocation error 37 | 38 | static const unsigned ledPin = LED_BUILTIN; // use onboard LED as activity indicator 39 | static const byte switchPin[] = {2,3,4,5,6,7,8,9}; // pins for footswitch inputs 40 | static const byte switchCount = 8; // number of footswitches used 41 | static bool switchPressed[switchCount]; // current state of footswitches 42 | static bool switchLastState[switchCount]; //previous state of footswitches (used for long press detection) 43 | static unsigned long lastPressMillis[switchCount]; // when the last button press was detected 44 | static unsigned long lastReleaseMillis[switchCount]; // when the last button was released 45 | 46 | // Created and binds the MIDI interface to the default hardware Serial port 47 | MIDI_CREATE_DEFAULT_INSTANCE(); 48 | 49 | 50 | void errBlink(int errCode) { 51 | byte blinkTime = 200; // duration for each blink 52 | byte blinkGap = 200; // gap between each blink 53 | int burstWait = 1000; // wait time between bursts 54 | for (;;) { // loop forever 55 | for (int i = 1; i <= errCode; i++) { 56 | digitalWrite(ledPin,HIGH); 57 | delay(blinkTime); 58 | digitalWrite(ledPin,LOW); 59 | delay(blinkGap); 60 | } 61 | delay(burstWait); 62 | } 63 | } // end of errBlink() 64 | 65 | 66 | void setup() { 67 | pinMode(ledPin, OUTPUT); // setup activity LED pin for output 68 | 69 | MIDI.begin(MIDI_CHANNEL_OMNI); // Listen to all incoming messages 70 | 71 | // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally 72 | if(!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) { // Address 0x3C for my 128x64 variant 73 | errBlink(ERR_DISP_ALLOC); 74 | } 75 | 76 | // Show initial display buffer contents on the screen -- 77 | // the library initializes this with an Adafruit splash screen. 78 | display.display(); 79 | delay(500); // Pause for 0.5 seconds 80 | display.setTextSize(1); // Normal 1:1 pixel scale 81 | display.setTextColor(WHITE); // Draw white text 82 | display.setCursor(0, 0); // Start at top-left corner 83 | display.cp437(true); // Use full 256 char 'Code Page 437' font 84 | display.clearDisplay(); 85 | 86 | // Initialise switches and related variable arrays 87 | for (int i=0;i (lastPressMillis[i] + debounceTime) ) { // genuine press and not switch bounce 136 | lastPressMillis[i] = millis(); 137 | switchLastState[i] = true; 138 | nextCommand = i; 139 | commandMillis = millis(); 140 | } 141 | } 142 | else if (!switchPressed[i]) { //potential switch release detected 143 | if ( millis() > (lastReleaseMillis[i] + debounceTime ) ) { // genuine release and not switch bounce 144 | lastReleaseMillis[i] = millis(); 145 | switchLastState[i] = false; 146 | } 147 | } 148 | } 149 | if (switchPressed[i]) { 150 | switchPressedCounter++; //increment counter used to check multiple presses 151 | if ( millis() > (lastPressMillis[i] + longPressTime) ) { // long press detected 152 | lastPressMillis[i] = millis(); // reset timer so it doesn't re-trigger every loop 153 | nextCommand = i + switchCount; // use the next n numbers as a second bank of commands representing long press actions 154 | } 155 | } 156 | } 157 | static bool comboActive = false; // remembers whether multiple presses were detected to avoid re-triggering every loop 158 | if (switchPressedCounter > 1 ) { // multiple presses detected 159 | if (!comboActive) { 160 | comboActive = true; 161 | if ( switchPressed[0] && switchPressed[1]) { // first two switches -> Page Down 162 | nextCommand = pageDnCmd; 163 | changePageDown(); 164 | } 165 | else if ( switchPressed[1] && switchPressed[2]) { // second two switches -> Page Up 166 | nextCommand = pageUpCmd; 167 | changePageUp(); 168 | } 169 | else if ( switchPressed[2] && switchPressed[3]) { // 3rd 2 switches -> tuner 170 | nextCommand = tunerCmd; 171 | } 172 | 173 | 174 | else if ( switchPressed[6] && switchPressed[7]) { // last two switches - reset to page 0 and patch 0 175 | nextCommand = pagePatchReset; 176 | currentPage = 0; 177 | } 178 | } 179 | } 180 | else { 181 | comboActive = false; // we can reset this as no more than one switch currently pressed 182 | } 183 | lastCommand = nextCommand; 184 | } // end of read_buttons() 185 | 186 | void changePageUp() { 187 | currentPage++; 188 | if (currentPage >= pageCount) { // we have gone past the last page 189 | currentPage = 0; // reset to first page 190 | } 191 | } 192 | 193 | void changePageDown() { 194 | currentPage--; 195 | if (currentPage > pageCount) { // we have scrolled back past the first page 196 | currentPage = (pageCount -1); // reset to last page 197 | } 198 | } 199 | 200 | 201 | 202 | 203 | /* 204 | * 205 | * Display related functions 206 | * 207 | */ 208 | 209 | 210 | void invSelection(int i=(lastCommand+1)) { 211 | if (lastCommand == i) { // highlight the last switch pressed by inverting the colours 212 | display.setTextColor(BLACK, WHITE); 213 | } 214 | else { 215 | display.setTextColor(WHITE, BLACK); 216 | } 217 | } 218 | 219 | 220 | void displayLine(const __FlashStringHelper *str0, const __FlashStringHelper *str1, const __FlashStringHelper *str2, const __FlashStringHelper *str3, int startBtn) { 221 | display.print(F("|")); 222 | invSelection(0+startBtn); 223 | display.print(str0); 224 | invSelection(); 225 | display.print(F("|")); 226 | invSelection(1+startBtn); 227 | display.print(str1); 228 | invSelection(); 229 | display.print(F("|")); 230 | invSelection(2+startBtn); 231 | display.print(str2); 232 | invSelection(); 233 | display.print(F("|")); 234 | invSelection(3+startBtn); 235 | display.print(str3); 236 | invSelection(); 237 | display.println(F("|")); 238 | } 239 | 240 | 241 | void displayUpdate(void) { // maybe change this to put labels in arrays, but this will do for now 242 | display.clearDisplay(); 243 | display.setCursor(0, 0); // Start at top-left corner 244 | switch (currentPage) { 245 | case 0: 246 | displayLine(F("SNAP"),F("SNAP"),F("SNAP"),F("FS5 "),4); 247 | displayLine(F("SHOT"),F("SHOT"),F("SHOT"),F(" / "),4); 248 | displayLine(F(" 1 "),F(" 2 "),F(" 3 "),F(" UP "),4); 249 | display.println(F("---------------------")); 250 | displayLine(F(" "),F(" "),F(" "),F("FS4 "),0); 251 | displayLine(F("FS1 "),F("FS2 "),F("FS3 "),F(" / "),0); 252 | displayLine(F(" "),F(" "),F(" "),F("DOWN"),0); 253 | break; 254 | 255 | case 1: 256 | displayLine(F("FS4 "),F("FS5 "),F("TAP "),F(" "),4); 257 | displayLine(F(" / "),F(" / "),F("TMPO"),F("TUNE"),4); 258 | displayLine(F("DOWN"),F(" UP "),F(" "),F(" "),4); 259 | display.println(F("---------------------")); 260 | displayLine(F(" "),F(" "),F(" "),F("NEXT"),0); 261 | displayLine(F("FS1 "),F("FS2 "),F("FS3 "),F(" FS "),0); 262 | displayLine(F(" "),F(" "),F(" "),F("MODE"),0); 263 | break; 264 | 265 | } 266 | display.display(); 267 | } 268 | 269 | /* 270 | * 271 | * MIDI output related functions 272 | * 273 | */ 274 | 275 | 276 | void midiSend() { 277 | // do something 278 | if (nextCommand >=0) { 279 | if (nextCommand == pagePatchReset) { // SW7 & SW8 should reset page and patch to 0 regardless of which page/patch currently active 280 | MIDI.sendProgramChange(0,1); 281 | } 282 | else if (nextCommand == tunerCmd) { 283 | MIDI.sendControlChange(68,68,1); //tuner 284 | } 285 | 286 | else { 287 | switch(currentPage) { 288 | case 0: // menu page 0 (1 of 2) 289 | switch(nextCommand) { 290 | case 0: 291 | MIDI.sendControlChange(49,0,1); //FS1 292 | break; 293 | case 1: 294 | MIDI.sendControlChange(50,0,1); //FS2 295 | break; 296 | case 2: 297 | MIDI.sendControlChange(51,0,1); //FS3 298 | break; 299 | case 3: 300 | MIDI.sendControlChange(52,0,1); //FS4 301 | break; 302 | case 4: 303 | MIDI.sendControlChange(69,0,1); //snapshot 1 304 | break; 305 | case 5: 306 | MIDI.sendControlChange(69,1,1); // snapshot 2 307 | break; 308 | case 6: 309 | MIDI.sendControlChange(69,2,1); //snapshot 3 310 | break; 311 | case 7: 312 | MIDI.sendControlChange(53,0,1); //FS5 313 | break; 314 | 315 | } // end of menu page 0 316 | break; 317 | case 1: // menu page 1 (2 of 2) 318 | switch(nextCommand) { 319 | case 0: 320 | // -> FS1 321 | MIDI.sendControlChange(49,0,1); //FS1 322 | break; 323 | case 1: 324 | // -> FS2 325 | MIDI.sendControlChange(50,0,1); //FS2 326 | break; 327 | case 2: 328 | // -> FS3 329 | MIDI.sendControlChange(51,0,1); //FS3 330 | break; 331 | case 3: 332 | // -> PRESET MODE PAGE 333 | MIDI.sendControlChange(71,4,1); //next footswitch mode (temp functionality until I work out if I can change currentPage while in switch block) 334 | break; 335 | case 4: 336 | MIDI.sendControlChange(52,0,1); //FS4 337 | break; 338 | case 5: 339 | MIDI.sendControlChange(53,0,1); //FS5 340 | break; 341 | case 6: 342 | MIDI.sendControlChange(64,64,1); //tap tempo 343 | break; 344 | case 7: 345 | MIDI.sendControlChange(68,68,1); //tuner 346 | break; 347 | } // end of menu page 1 348 | break; 349 | 350 | 351 | break; 352 | } // end of outer nested switch case 353 | } 354 | nextCommand = -1; // re-initialise this so we don't send the same message repeatedly 355 | } 356 | } // end midisend 357 | --------------------------------------------------------------------------------