diff --git a/README.md b/README.md index 374d3b1..8743d9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Arduino ATmega32u4 MIDI Octopot -Arduino SysEx programmable 8 Knobs MIDI Controller inspired by [Crius Octapot Midi Controller](https://www.instructables.com/Crius-OctaPot-Midi-Controller) but using ATmega32u4 microcontroller in order to use MIDI USB. +Arduino SysEx programmable MIDI Controller inspired by [Crius Octapot Midi Controller](https://www.instructables.com/Crius-OctaPot-Midi-Controller). +This MIDI controller uses an ATmega32u4 microcontroller in order to use MIDI USB. + +This version has: +- 8 Knobs +- 4 Push buttons + +To get the simple version with only 8 Knobs, go to the [main branch](https://github.com/gwilherm/arduino-atmega32u4-midi-octopot/tree/main). ## Photo ![](doc/photo.jpg) diff --git a/doc/octopot-config-app.png b/doc/octopot-config-app.png index 6d6e90b..93e6e49 100644 Binary files a/doc/octopot-config-app.png and b/doc/octopot-config-app.png differ diff --git a/doc/photo.jpg b/doc/photo.jpg index e7498cb..83db5ab 100644 Binary files a/doc/photo.jpg and b/doc/photo.jpg differ diff --git a/doc/schematics/midi-octopot_bb.png b/doc/schematics/midi-octopot_bb.png index 0f93b9f..c0037da 100644 Binary files a/doc/schematics/midi-octopot_bb.png and b/doc/schematics/midi-octopot_bb.png differ diff --git a/doc/schematics/midi-octopot_schema.png b/doc/schematics/midi-octopot_schema.png index 7d5784a..c5acc66 100644 Binary files a/doc/schematics/midi-octopot_schema.png and b/doc/schematics/midi-octopot_schema.png differ diff --git a/midi-octopot.fzz b/midi-octopot.fzz index dcd6dcc..2342402 100644 Binary files a/midi-octopot.fzz and b/midi-octopot.fzz differ diff --git a/midi-octopot/midi-octopot.ino b/midi-octopot/midi-octopot.ino index 75f7230..c2f9be6 100644 --- a/midi-octopot/midi-octopot.ino +++ b/midi-octopot/midi-octopot.ino @@ -8,65 +8,109 @@ enum { MIDI_CC_VOLUME = 0x07, MIDI_CC_PAN_MSB = 0x0A, + MIDI_CC_PORTAMENTO_ON_OFF = 0x41, MIDI_CC_SOUND_CONTROLLER_2 = 0x47, // Timbre/Harmonic Intensity MIDI_CC_SOUND_CONTROLLER_3 = 0x48, // Release Time MIDI_CC_SOUND_CONTROLLER_4 = 0x49, // Attack Time MIDI_CC_SOUND_CONTROLLER_5 = 0x4A, // Brightness MIDI_CC_EFFECTS_1_DEPTH = 0x5B, // Reverb amount MIDI_CC_EFFECTS_4_DEPTH = 0x5F, // Detune amount + MIDI_CC_ALL_NOTES_OFF = 0x7B, // Midi Panic }; #define MIDI_CHANNEL 1 #define POT_NB 8 - -byte pot_pin[] = {A10, A9, A8, A7, - A0, A1, A2, A3}; - -byte default_pot_mcc[] = {MIDI_CC_SOUND_CONTROLLER_2, MIDI_CC_SOUND_CONTROLLER_3, MIDI_CC_PORTAMENTO_TIME, MIDI_CC_EFFECTS_1_DEPTH, - MIDI_CC_SOUND_CONTROLLER_5, MIDI_CC_SOUND_CONTROLLER_4, MIDI_CC_EFFECTS_4_DEPTH, MIDI_CC_PAN_MSB}; - -byte pot_mcc[] = {0, 0, 0, 0, 0, 0, 0, 0}; -byte pot_val[] = {0, 0, 0, 0, 0, 0, 0, 0}; +#define BTN_NB 4 enum { - PATCH_REQ, // In: Request for current configuration - PATCH_STS, // Out: Send configuration - PATCH_CMD, // In: New patch command - SAVE_CMD, // In: Save current configuration command - RESET_CMD // In: Restore default configuration + PATCH_REQ, // In: Request for current configuration + PATCH_STS, // Out: Send configuration + PATCH_POT_CMD, // In: Change a pot patch + PATCH_BTN_CMD, // In: Change a button patch + TOGGLE_BTN_CMD, // In: Change a button toggle + SAVE_CMD, // In: Save current configuration command + RESET_CMD // In: Restore default configuration }; +typedef struct +{ + byte mcc; // MIDI CC + bool tog; // Is toggle button +} btn_t; + typedef struct { byte msg_idx; - byte pot_mcc[POT_NB]; + byte pot_mcc[POT_NB]; + btn_t btn_cfg[BTN_NB]; } patch_sts_t; typedef struct { byte syx_hdr; // 0xF0 byte msg_idx; - byte pot_idx; - byte pot_mcc; + byte idx; + byte val; byte syx_ftr; // 0xF7 } patch_cmd_t; +// Pots +byte pot_pin[] = {A10, A9, A8, A7, + A0, A1, A2, A3}; + +byte default_pot_mcc[] = {MIDI_CC_SOUND_CONTROLLER_2, MIDI_CC_SOUND_CONTROLLER_3, MIDI_CC_PORTAMENTO_TIME, MIDI_CC_EFFECTS_1_DEPTH, + MIDI_CC_SOUND_CONTROLLER_5, MIDI_CC_SOUND_CONTROLLER_4, MIDI_CC_EFFECTS_4_DEPTH, MIDI_CC_PAN_MSB}; + +byte pot_mcc[] = {0, 0, 0, 0, 0, 0, 0, 0}; +byte pot_val[] = {0, 0, 0, 0, 0, 0, 0, 0}; + +// Buttons +byte btn_pin[] = {2, 3, 4, 5}; + +byte default_btn_mcc[] = {MIDI_CC_SOUND_CONTROLLER_2, MIDI_CC_SOUND_CONTROLLER_5, MIDI_CC_PORTAMENTO_ON_OFF, MIDI_CC_ALL_NOTES_OFF}; +byte default_btn_tog[] = { false, false, true, false}; + +btn_t btn_cfg[BTN_NB]; +int btn_state[] = {0, 0, 0, 0}; +bool btn_value[] = {0, 0, 0, 0}; + void sendPatchStatus() { patch_sts_t sts; sts.msg_idx = PATCH_STS; memcpy(&sts.pot_mcc, &pot_mcc, POT_NB); + memcpy(&sts.btn_cfg, &btn_cfg, BTN_NB*sizeof(btn_t)); MIDI.sendSysEx(sizeof(patch_sts_t), (byte*)&sts); } -void updatePatch(byte* array, unsigned size) +void updatePotPatch(byte* array, unsigned size) +{ + if (size == sizeof(patch_cmd_t)) + { + patch_cmd_t* patch = (patch_cmd_t*)array; + if ((patch->idx < POT_NB) && (patch->val <= 127)) + pot_mcc[patch->idx] = patch->val; + } +} + +void updateBtnPatch(byte* array, unsigned size) +{ + if (size == sizeof(patch_cmd_t)) + { + patch_cmd_t* patch = (patch_cmd_t*)array; + if ((patch->idx < BTN_NB) && (patch->val <= 127)) + btn_cfg[patch->idx].mcc = patch->val; + } +} + +void updateBtnToggle(byte* array, unsigned size) { if (size == sizeof(patch_cmd_t)) { patch_cmd_t* patch = (patch_cmd_t*)array; - if (patch->pot_idx < POT_NB) - pot_mcc[patch->pot_idx] = patch->pot_mcc; + if ((patch->idx < BTN_NB) && (patch->val <= 1)) + btn_cfg[patch->idx].tog = patch->val; } } @@ -74,6 +118,11 @@ void saveConfig() { for (int i = 0; i < POT_NB; i++) EEPROM.update(i, pot_mcc[i]); + for (int i = 0; i < BTN_NB; i++) + { + EEPROM.update(POT_NB+i, btn_cfg[i].mcc); + EEPROM.update(POT_NB+i+1, btn_cfg[i].tog); + } } void restoreConfig() @@ -86,12 +135,33 @@ void restoreConfig() else pot_mcc[i] = default_pot_mcc[i]; } + + for (int i = 0; i < BTN_NB; i++) + { + byte value = EEPROM.read(POT_NB+i); + if (value <= 127) + btn_cfg[i].mcc = value; + else + btn_cfg[i].mcc = default_btn_mcc[i]; + + value = EEPROM.read(POT_NB+i+1); + if (value <= 127) + btn_cfg[i].tog = (bool)value; + else + btn_cfg[i].tog = default_btn_tog[i]; + } } void resetConfig() { for (int i = 0; i < POT_NB; i++) pot_mcc[i] = default_pot_mcc[i]; + + for (int i = 0; i < BTN_NB; i++) + { + btn_cfg[i].mcc = default_btn_mcc[i]; + btn_cfg[i].tog = default_btn_tog[i]; + } } void handleSysEx(byte* array, unsigned size) @@ -102,8 +172,14 @@ void handleSysEx(byte* array, unsigned size) case PATCH_REQ: sendPatchStatus(); break; - case PATCH_CMD: - updatePatch(array, size); + case PATCH_POT_CMD: + updatePotPatch(array, size); + break; + case PATCH_BTN_CMD: + updateBtnPatch(array, size); + break; + case TOGGLE_BTN_CMD: + updateBtnToggle(array, size); break; case SAVE_CMD: saveConfig(); @@ -119,18 +195,24 @@ void handleSysEx(byte* array, unsigned size) void setup() { Serial.begin(115200); + memset(&btn_cfg, 0, sizeof(btn_t)*BTN_NB); + restoreConfig(); MIDI.begin(MIDI_CHANNEL_OMNI); MIDI.setHandleSystemExclusive(handleSysEx); + + for (byte i = 0; i < BTN_NB; i++) + pinMode(btn_pin[i], INPUT); } void loop() { MIDI.read(); - char serial_output[32]; + char serial_output[POT_NB*4 + BTN_NB*4 + 1]; bool display_update = false; + // Pots for (byte i = 0; i < POT_NB; i++) { byte val = analogRead(pot_pin[i]) / 8; @@ -141,9 +223,40 @@ void loop() { MIDI.sendControlChange(pot_mcc[i], pot_val[i], MIDI_CHANNEL); } - sprintf(serial_output+3*i+i, "%03d ", (int)pot_val[i]); + sprintf(serial_output+4*i, "%03d ", (int)pot_val[i]); } + // Buttons + for (byte i = 0; i < BTN_NB; i++) + { + bool update = false; + int state = digitalRead(btn_pin[i]); + if (state != btn_state[i]) + { + btn_state[i] = state; + if (btn_cfg[i].tog) + { + if (state == HIGH) + { + update = true; + btn_value[i] = !btn_value[i]; + } + } + else + { + update = true; + btn_value[i] = btn_state[i]; + } + + if (update) + { + display_update = true; + MIDI.sendControlChange(btn_cfg[i].mcc, btn_value[i]*127, MIDI_CHANNEL); + } + } + sprintf(serial_output+POT_NB*4+2*i, "%d ", btn_value[i]); + } + if (display_update) Serial.println(serial_output); } diff --git a/octopot-config-app/octopot-config-app.py b/octopot-config-app/octopot-config-app.py index 00dd457..a3962c1 100755 --- a/octopot-config-app/octopot-config-app.py +++ b/octopot-config-app/octopot-config-app.py @@ -10,14 +10,17 @@ import os POT_NB = 8 +BTN_NB = 4 REQUEST_REC = 2000 class SysExMsg(IntEnum): PATCH_REQ = 0 # Out: Request for current config PATCH_STS = 1 # In: Send the current config - PATCH_CMD = 2 # Out: Change a patch - SAVE_CMD = 3 # Out: Save the current config - RESET_CMD = 4 # Out: Save the current config + PATCH_POT_CMD = 2 # Out: Change a pot patch + PATCH_BTN_CMD = 3 # Out: Change a button patch + TOGGLE_BTN_CMD = 4 # Out: Change a button toggle + SAVE_CMD = 5 # Out: Save the current config + RESET_CMD = 6 # Out: Save the current config class Root: midi_in = None @@ -58,16 +61,32 @@ def __init__(self): pady=300 for i in range(int(POT_NB / 2)): self.pot += [tk.Entry()] - self.pot[i].place(y=pady, w=50) - self.pot[i].bind('', lambda event, idx=i: self.on_change_cc(event,idx)) + self.pot[i].place(y=pady, w=30) + self.pot[i].bind('', lambda event, idx=i: self.on_change_cc(event, SysExMsg.PATCH_POT_CMD, idx)) pady += 105 # Left side text boxes pady=300 for i in range(int(POT_NB / 2), POT_NB): self.pot += [tk.Entry()] - self.pot[i].place(relx=1, y=pady, w=50, anchor='ne') - self.pot[i].bind('', lambda event, idx=i: self.on_change_cc(event,idx)) + self.pot[i].place(relx=1, y=pady, w=30, anchor='ne') + self.pot[i].bind('', lambda event, idx=i: self.on_change_cc(event, SysExMsg.PATCH_POT_CMD, idx)) + pady += 105 + + self.btn_mcc = [] + self.var_tog = [] + + # Buttons + pady=360 + padx=265 + for i in range(BTN_NB): + self.btn_mcc += [tk.Entry()] + self.btn_mcc[i].place(x=padx, y=pady, w=30) + self.btn_mcc[i].bind('', lambda event, idx=i: self.on_change_cc(event, SysExMsg.PATCH_BTN_CMD, idx)) + self.var_tog += [tk.IntVar()] + self.var_tog[i].trace('w', lambda *args,idx=i: self.on_change_btn_tog(idx)) + btn_tog = tk.Checkbutton(text='Tog', variable=self.var_tog[i]) + btn_tog.place(x=padx+35, y=pady, w=50) pady += 105 save_btn = tk.Button(text='Save patch into EEPROM', command = self.on_save) @@ -108,12 +127,13 @@ def on_change_output_conn(self, *args): self.midi_out.close() self.midi_out = mido.open_output(self.output_conn.get()) - def on_change_cc(self, e, idx): + def on_change_cc(self, e, msg, idx): """ Calback to validate Entry input - Sends the new patch for a knob. + Sends the new patch for a controller. @param e: event + @param msg: sysex message id (PATCH_POT_CMD or PATCH_BTN_CMD) @param idx: index of the Entry """ @@ -123,11 +143,18 @@ def on_change_cc(self, e, idx): print(str(idx) + ' => ' + midi_cc) # Send the SysEx message - self.midi_out.send(mido.Message('sysex', data=[SysExMsg.PATCH_CMD, idx, int(midi_cc)])) + self.midi_out.send(mido.Message('sysex', data=[msg, idx, int(midi_cc)])) # Lose focus to be refreshed by the timer self.root.focus() + def on_change_btn_tog(self, idx): + tog = self.var_tog[idx].get() + print(str(idx) + ' => ' + str(tog)) + + # Send the SysEx message + self.midi_out.send(mido.Message('sysex', data=[SysExMsg.TOGGLE_BTN_CMD, idx, tog])) + def on_midi_receive(self, midi_msg): """ Callback to be called on MIDI in event. @@ -136,12 +163,25 @@ def on_midi_receive(self, midi_msg): """ if midi_msg.type == 'sysex' and midi_msg.data[0] == SysExMsg.PATCH_STS: - midi_cc = midi_msg.data[1:9] + pot_mcc = midi_msg.data[1:POT_NB+1] for i in range(POT_NB): # Do not update an Entry that being edited if self.root.focus_get() != self.pot[i]: self.pot[i].delete(0,"end") - self.pot[i].insert(0, midi_cc[i]) + self.pot[i].insert(0, pot_mcc[i]) + btn_cfg = midi_msg.data[POT_NB+1:POT_NB+1+BTN_NB*2] + for i in range(BTN_NB): + btn_mcc = btn_cfg[i*2] + btn_tog = btn_cfg[i*2+1] + # Do not update an Entry that being edited + if self.root.focus_get() != self.btn_mcc[i]: + self.btn_mcc[i].delete(0,"end") + self.btn_mcc[i].insert(0, btn_mcc) + # Remove callback on toggle + self.var_tog[i].trace_remove(*self.var_tog[i].trace_info()[0]) + self.var_tog[i].set(btn_tog) + # Set back the callback + self.var_tog[i].trace('w', lambda *args,idx=i: self.on_change_btn_tog(idx)) def on_close(self): """ @@ -197,4 +237,4 @@ def update(self): print('Using: ' + mido.backend.api) - root = Root() \ No newline at end of file + root = Root() diff --git a/octopot-config-app/octopot.png b/octopot-config-app/octopot.png index 1e7c63f..6de159c 100644 Binary files a/octopot-config-app/octopot.png and b/octopot-config-app/octopot.png differ diff --git a/octopot-config-app/octopot.svg b/octopot-config-app/octopot.svg index 62fce9c..7752ac4 100644 --- a/octopot-config-app/octopot.svg +++ b/octopot-config-app/octopot.svg @@ -14,6 +14,7 @@ inkscape:export-ydpi="96" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + + + + + id="g4689"> + transform="matrix(0,0.04528364,-0.04528364,0,94.911534,11.373676)"> @@ -389,7 +429,7 @@ gorn="0.1.0.0.0.0.0.0.0.0.1.2.0" fill="#b3b3b3" id="_x30_.3.0" - points="0.486108,482.431 0.486108,220.722 238.333,220.722 238.333,482.458 " /> + points="238.333,220.722 238.333,482.458 0.486108,482.431 0.486108,220.722 " /> + points="230.458,217.514 230.458,238.764 2.09722,223.139 2.09722,217.514 " /> + points="230.458,469.431 230.458,485.222 2.09722,485.222 154.569,477.333 " /> + points="1233.12,428.236 1320.66,428.236 1320.66,274.528 1233.12,274.528 1233.12,320.778 1235.16,320.778 1235.16,381.986 1233.12,381.986 " /> + points="1334.69,274.528 1247.8,274.528 1247.8,428.236 1334.69,428.236 1334.69,381.986 1332.67,381.986 1332.67,320.778 1334.69,320.778 " /> + transform="matrix(0,-0.62806707,0.62806707,0,9.7391865,104.72328)"> @@ -3311,7 +3351,7 @@ id="line7126" /> @@ -3387,7 +3427,7 @@ + transform="matrix(0,-0.62806707,0.62806707,0,9.7391865,132.69096)"> @@ -3693,7 +3733,7 @@ id="line7126-4" /> @@ -3769,7 +3809,7 @@ + transform="matrix(0,-0.62806707,0.62806707,0,9.7391865,160.65864)"> @@ -4075,7 +4115,7 @@ id="line7126-3" /> @@ -4151,7 +4191,7 @@ + transform="matrix(0,-0.62806707,0.62806707,0,9.7391865,188.62632)"> @@ -4457,7 +4497,7 @@ id="line7126-4-2" /> @@ -4533,7 +4573,7 @@ + transform="matrix(0,-0.62806707,-0.62806707,0,148.26081,188.62632)"> @@ -4851,7 +4891,7 @@ id="line10480" /> @@ -4927,7 +4967,7 @@ + transform="matrix(0,-0.62806707,-0.62806707,0,148.26081,104.72328)"> @@ -5251,7 +5291,7 @@ id="line10090" /> @@ -5327,7 +5367,7 @@ + transform="matrix(0,-0.62806707,-0.62806707,0,148.26081,132.69096)"> @@ -5633,7 +5673,7 @@ id="line10220" /> @@ -5709,7 +5749,7 @@ + transform="matrix(0,-0.62806707,-0.62806707,0,148.26081,160.65864)"> @@ -6015,7 +6055,7 @@ id="line10350" /> @@ -6091,7 +6131,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - -