From 6ab47140a8c573b3f7fd2f1384cdd342c858d74e Mon Sep 17 00:00:00 2001 From: Holger Wirtz Date: Thu, 16 Apr 2020 15:27:19 +0200 Subject: [PATCH] Added menu structure document from Thierry. Fixed load/save functions. --- MicroDexed.ino | 2 + UI.hpp | 441 +++++++++++++++++++----------- UI_1_FX.h | 43 +-- config.h | 9 +- dexed_sd.cpp | 20 +- dexed_sd.h | 4 +- doc/MicroDexed_Menu_Structure.odt | Bin 0 -> 14131 bytes 7 files changed, 324 insertions(+), 195 deletions(-) create mode 100644 doc/MicroDexed_Menu_Structure.odt diff --git a/MicroDexed.ino b/MicroDexed.ino index 23fea40..0de3a21 100644 --- a/MicroDexed.ino +++ b/MicroDexed.ino @@ -1345,6 +1345,7 @@ void check_configuration(void) configuration.dexed[instance_id].midi_channel = constrain(configuration.dexed[instance_id].midi_channel, MIDI_CHANNEL_MIN, MIDI_CHANNEL_MAX); configuration.performance.bank[instance_id] = constrain(configuration.performance.bank[instance_id], 0, MAX_BANKS - 1); configuration.performance.voice[instance_id] = constrain(configuration.performance.voice[instance_id], 0, MAX_VOICES - 1); + configuration.performance.voiceconfig_number[instance_id] = constrain(configuration.performance.voiceconfig_number[instance_id], VOICECONFIG_NUM_MIN, VOICECONFIG_NUM_MAX); configuration.dexed[instance_id].lowest_note = constrain(configuration.dexed[instance_id].lowest_note, INSTANCE_LOWEST_NOTE_MIN, INSTANCE_LOWEST_NOTE_MAX); configuration.dexed[instance_id].highest_note = constrain(configuration.dexed[instance_id].highest_note, INSTANCE_HIGHEST_NOTE_MIN, INSTANCE_HIGHEST_NOTE_MAX); configuration.dexed[instance_id].reverb_send = constrain(configuration.dexed[instance_id].reverb_send, REVERB_SEND_MIN, REVERB_SEND_MAX); @@ -1423,6 +1424,7 @@ void init_configuration(void) configuration.dexed[instance_id].midi_channel = DEFAULT_MIDI_CHANNEL; configuration.performance.bank[instance_id] = SYSEXBANK_DEFAULT; configuration.performance.voice[instance_id] = SYSEXSOUND_DEFAULT; + configuration.performance.voiceconfig_number[instance_id] = VOICECONFIG_NUM_DEFAULT; configuration.dexed[instance_id].lowest_note = INSTANCE_LOWEST_NOTE_MIN; configuration.dexed[instance_id].highest_note = INSTANCE_HIGHEST_NOTE_MAX; configuration.dexed[instance_id].reverb_send = REVERB_SEND_DEFAULT; diff --git a/UI.hpp b/UI.hpp index 02bdf8c..81039ce 100644 --- a/UI.hpp +++ b/UI.hpp @@ -3168,8 +3168,12 @@ void UI_func_volume(uint8_t param) void UI_func_load_performance(uint8_t param) { + static uint8_t mode; + if (LCDML.FUNC_setup()) // ****** SETUP ********* { + mode = 0; + encoderDir[ENC_R].reset(); lcd.setCursor(0, 0); @@ -3187,7 +3191,7 @@ void UI_func_load_performance(uint8_t param) if (LCDML.FUNC_loop()) // ****** LOOP ********* { - if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up())) + if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up()) || (LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort())) { if (LCDML.BT_checkDown()) { @@ -3197,6 +3201,23 @@ void UI_func_load_performance(uint8_t param) { configuration.sys.performance_number = constrain(configuration.sys.performance_number - ENCODER[ENC_L].speed(), PERFORMANCE_NUM_MIN, PERFORMANCE_NUM_MAX); } + else if (LCDML.BT_checkEnter()) + { + mode = 0xff; + + eeprom_write(); + lcd.setCursor(0, 1); + if (load_sd_performance(configuration.sys.performance_number) == false) + lcd.print("Does not exist."); + else + lcd.print("Done. "); + delay(500); + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); + } lcd.setCursor(0, 1); if (configuration.sys.performance_number == 0) @@ -3212,11 +3233,13 @@ void UI_func_load_performance(uint8_t param) if (LCDML.FUNC_close()) // ****** STABLE END ********* { - eeprom_write(); - load_sd_performance(configuration.sys.performance_number); - lcd.setCursor(0, 1); - lcd.print("Done. "); - delay(500); + if (mode < 0xff) + { + lcd.setCursor(0, 1); + lcd.print("Canceled "); + delay(500); + } + encoderDir[ENC_R].reset(); } } @@ -3282,7 +3305,20 @@ void UI_func_save_performance(uint8_t param) } else { - LCDML.BT_quit(); // does not help :( + mode = 0xff; + eeprom_write(); + if (overwrite == false || yesno == true) + { + save_sd_performance(configuration.performance.fx_number); + lcd.setCursor(0, 1); + lcd.print("Done. "); + delay(500); + } + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); } } @@ -3319,14 +3355,13 @@ void UI_func_save_performance(uint8_t param) if (LCDML.FUNC_close()) // ****** STABLE END ********* { - eeprom_write(); - if (overwrite == false || yesno == true) + if (mode < 0xff) { - save_sd_performance(configuration.sys.performance_number); lcd.setCursor(0, 1); - lcd.print("Done. "); + lcd.print("Canceled "); delay(500); } + encoderDir[ENC_R].reset(); } } @@ -3335,92 +3370,99 @@ void UI_func_load_voiceconfig(uint8_t param) { #if NUMDEXED > 1 static int8_t instance_id; - static uint8_t chosen; #else uint8_t instance_id = 0; #endif - static bool yesno; + + static uint8_t mode; if (LCDML.FUNC_setup()) // ****** SETUP ********* { + instance_id = 0; + encoderDir[ENC_R].reset(); lcd.setCursor(0, 0); - lcd.print(F("Load Config SD")); - + lcd.print(F("Load Voice Cfg")); #if NUMDEXED > 1 - instance_id = -1; - chosen = 0; + mode = 0; lcd.setCursor(0, 1); - lcd.print(F("Instance [1]")); + lcd.print(F("Instance [0]")); #else + mode = 1; lcd.setCursor(0, 1); - lcd.print(F("[NO ]")); + if (configuration.performance.voiceconfig_number[instance_id] == 0) + lcd.print(F("[DEFAULT]")); + else + { + char tmp[10]; + sprintf(tmp, "[%7d]", configuration.performance.voiceconfig_number[instance_id]); + lcd.print(tmp); + } #endif - - yesno = false; } if (LCDML.FUNC_loop()) // ****** LOOP ********* { - if ((LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort()) || (LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up())) + if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up()) || (LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort())) { -#if NUMDEXED > 1 - if ((LCDML.BT_checkDown() || LCDML.BT_checkUp()) && instance_id < 0) + if (LCDML.BT_checkDown()) { - chosen = (chosen + 1) % 2; + if (mode == 0) + instance_id = (instance_id + 1) % 2; + else if (mode == 1) + configuration.performance.voiceconfig_number[instance_id] = constrain(configuration.performance.voiceconfig_number[instance_id] + ENCODER[ENC_L].speed(), VOICECONFIG_NUM_MIN, VOICECONFIG_NUM_MAX); } - else if (LCDML.BT_checkEnter() && instance_id < 0) + else if (LCDML.BT_checkUp()) { - instance_id = chosen; - lcd.setCursor(0, 1); - lcd.print(F("[NO ] ")); + if (mode == 0) + instance_id = (instance_id - 1) % 2; + else if (mode == 1) + configuration.performance.voiceconfig_number[instance_id] = constrain(configuration.performance.voiceconfig_number[instance_id] - ENCODER[ENC_L].speed(), VOICECONFIG_NUM_MIN, VOICECONFIG_NUM_MAX); } - else if ((LCDML.BT_checkDown() || LCDML.BT_checkUp()) && instance_id >= 0) -#else - if ((LCDML.BT_checkDown() || LCDML.BT_checkUp())) -#endif + else if (LCDML.BT_checkEnter()) { - yesno = !yesno; + mode = 0xff; + lcd.setCursor(0, 1); + if (load_sd_voiceconfig(configuration.performance.voiceconfig_number[instance_id], instance_id) == false) + lcd.print("Does not exist."); + else + lcd.print("Done. "); + delay(500); + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); } -#if NUMDEXED > 1 - if (instance_id < 0) + if (mode == 0) { lcd.setCursor(10, 1); - lcd.print(chosen + 1); + lcd.print(instance_id); } - else + else if (mode == 1) { -#endif - if (yesno == true) - { - lcd.setCursor(1, 1); - lcd.print(F("YES")); - } + lcd.setCursor(0, 1); + if (configuration.performance.voiceconfig_number[instance_id] == 0) + lcd.print(F("[DEFAULT]")); else { - lcd.setCursor(1, 1); - lcd.print(F("NO ")); + char tmp[10]; + sprintf(tmp, "[%7d]", configuration.performance.voiceconfig_number[instance_id]); + lcd.print(tmp); } -#if NUMDEXED > 1 } -#endif - //encoderDir[ENC_R].reset(); } + encoderDir[ENC_R].reset(); } if (LCDML.FUNC_close()) // ****** STABLE END ********* { - if (yesno == true) + if (mode < 0xff) { - LCDML.DISP_clear(); - lcd.print(F("Save Cfg SD")); - - load_sd_voiceconfig(configuration.performance.bank[instance_id], configuration.performance.voice[instance_id], instance_id); - lcd.setCursor(0, 1); - lcd.print(F("Done.")); + lcd.print("Canceled "); delay(500); } @@ -3432,92 +3474,139 @@ void UI_func_save_voiceconfig(uint8_t param) { #if NUMDEXED > 1 static int8_t instance_id; - static uint8_t chosen; #else uint8_t instance_id = 0; #endif + + static bool overwrite; static bool yesno; + static uint8_t mode; if (LCDML.FUNC_setup()) // ****** SETUP ********* { + yesno = false; + instance_id = 0; + encoderDir[ENC_R].reset(); lcd.setCursor(0, 0); lcd.print(F("Save Config SD")); - #if NUMDEXED > 1 - instance_id = -1; - chosen = 0; + mode = 0; lcd.setCursor(0, 1); - lcd.print(F("Instance [1]")); + lcd.print(F("Instance [0]")); #else + mode = 1; lcd.setCursor(0, 1); - lcd.print(F("[NO ]")); -#endif + if (configuration.performance.voice[instance_id] == 0) + lcd.print(F("[DEFAULT]")); + else + { + char tmp[10]; + sprintf(tmp, "[%7d]", configuration.performance.voice[instance_id]); + lcd.print(tmp); + } - yesno = false; + char tmp[FILENAME_LEN]; + sprintf(tmp, "/%s%d.syx", VOICE_CONFIG_NAME, configuration.performance.voiceconfig_number[instance_id]); + if (SD.exists(tmp)) + overwrite = true; + else + overwrite = false; +#endif } if (LCDML.FUNC_loop()) // ****** LOOP ********* { - if ((LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort()) || (LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up())) + if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up()) || (LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort())) { -#if NUMDEXED > 1 - if ((LCDML.BT_checkDown() || LCDML.BT_checkUp()) && instance_id < 0) + if (LCDML.BT_checkDown()) { - chosen = (chosen + 1) % 2; + if (mode == 0) + instance_id = (instance_id + 1) % 2; + else if (mode == 1) + configuration.performance.voiceconfig_number[instance_id] = constrain(configuration.performance.voiceconfig_number[instance_id] + ENCODER[ENC_L].speed(), VOICECONFIG_NUM_MIN, VOICECONFIG_NUM_MAX); + else + yesno = true; } - else if (LCDML.BT_checkEnter() && instance_id < 0) + else if (LCDML.BT_checkUp()) { - instance_id = chosen; - lcd.setCursor(0, 1); - lcd.print(F("[NO ] ")); + if (mode == 0) + instance_id = (instance_id - 1) % 2; + else if (mode == 1) + configuration.performance.voiceconfig_number[instance_id] = constrain(configuration.performance.voiceconfig_number[instance_id] - ENCODER[ENC_L].speed(), VOICECONFIG_NUM_MIN, VOICECONFIG_NUM_MAX); + else + yesno = false; } - else if ((LCDML.BT_checkDown() || LCDML.BT_checkUp()) && instance_id >= 0) -#else - if ((LCDML.BT_checkDown() || LCDML.BT_checkUp())) -#endif + else if (LCDML.BT_checkEnter()) { - yesno = !yesno; + if (mode == 1 && overwrite == true) + { + mode = 2; + lcd.setCursor(0, 1); + lcd.print(F("Overwrite: [ ]")); + } + else + { + mode = 0xff; + eeprom_write(); + if (overwrite == false || yesno == true) + { + save_sd_voiceconfig(configuration.performance.voiceconfig_number[instance_id], instance_id); + lcd.setCursor(0, 1); + lcd.print("Done. "); + delay(500); + } + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); + } } -#if NUMDEXED > 1 - if (instance_id < 0) + if (mode == 0) { lcd.setCursor(10, 1); - lcd.print(chosen + 1); + lcd.print(configuration.performance.voiceconfig_number[instance_id]); } - else + else if (mode == 1) { -#endif - if (yesno == true) + char tmp[FILENAME_LEN]; + sprintf(tmp, "/%s%d.syx", VOICE_CONFIG_NAME, configuration.performance.voiceconfig_number[instance_id]); + if (SD.exists(tmp)) + overwrite = true; + else + overwrite = false; + + lcd.setCursor(0, 1); + if (configuration.performance.voiceconfig_number[instance_id] == 0) + lcd.print(F("[DEFAULT]")); + else { - lcd.setCursor(1, 1); - lcd.print(F("YES")); + char tmp[10]; + sprintf(tmp, "[%7d]", configuration.performance.voiceconfig_number[instance_id]); + lcd.print(tmp); } + } + else if (mode == 2) + { + lcd.setCursor(12, 1); + if (yesno == true) + lcd.print(F("YES")); else - { - lcd.setCursor(1, 1); lcd.print(F("NO ")); - } -#if NUMDEXED > 1 } -#endif - //encoderDir[ENC_R].reset(); } + encoderDir[ENC_R].reset(); } if (LCDML.FUNC_close()) // ****** STABLE END ********* { - if (yesno == true) + if (mode < 0xff) { - LCDML.DISP_clear(); - lcd.print(F("Save Cfg SD")); - - save_sd_voiceconfig(configuration.performance.bank[instance_id], configuration.performance.voice[instance_id], instance_id); - lcd.setCursor(0, 1); - lcd.print(F("Done.")); + lcd.print("Canceled "); delay(500); } @@ -3525,60 +3614,14 @@ void UI_func_save_voiceconfig(uint8_t param) } } -void UI_func_save_voice(uint8_t param) -{ - static bool yesno; - - if (LCDML.FUNC_setup()) // ****** SETUP ********* - { - encoderDir[ENC_R].reset(); - - lcd.setCursor(0, 0); - lcd.print(F("Save Voice")); - lcd.setCursor(0, 1); - lcd.print(F("[NO ]")); - - if (LCDML.FUNC_loop()) // ****** LOOP ********* - { - if ((LCDML.BT_checkDown() || LCDML.BT_checkUp())) - { - yesno = !yesno; - if (yesno == true) - { - lcd.setCursor(1, 1); - lcd.print(F("YES")); - } - else - { - lcd.setCursor(1, 1); - lcd.print(F("NO ")); - } - } - } - - if (LCDML.FUNC_close()) // ****** STABLE END ********* - { - if (yesno == true) - { - LCDML.DISP_clear(); - lcd.print(F("Save Cfg default")); - - // Storing on inside bank TBD - - lcd.setCursor(0, 1); - lcd.print(F("Done.")); - delay(500); - } - - encoderDir[ENC_R].reset(); - } - } -} - void UI_func_load_fx(uint8_t param) { + static uint8_t mode; + if (LCDML.FUNC_setup()) // ****** SETUP ********* { + mode = 0; + encoderDir[ENC_R].reset(); lcd.setCursor(0, 0); @@ -3596,7 +3639,7 @@ void UI_func_load_fx(uint8_t param) if (LCDML.FUNC_loop()) // ****** LOOP ********* { - if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up())) + if ((LCDML.BT_checkDown() && encoderDir[ENC_R].Down()) || (LCDML.BT_checkUp() && encoderDir[ENC_R].Up()) || (LCDML.BT_checkEnter() && encoderDir[ENC_R].ButtonShort())) { if (LCDML.BT_checkDown()) { @@ -3606,6 +3649,22 @@ void UI_func_load_fx(uint8_t param) { configuration.performance.fx_number = constrain(configuration.performance.fx_number - ENCODER[ENC_L].speed(), FX_NUM_MIN, FX_NUM_MAX); } + else if (LCDML.BT_checkEnter()) + { + mode = 0xff; + eeprom_write(); + + if (load_sd_fx(configuration.performance.fx_number) == false) + lcd.print("Does not exist."); + else + lcd.print("Done. "); + delay(500); + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); + } lcd.setCursor(0, 1); if (configuration.performance.fx_number == 0) @@ -3621,11 +3680,13 @@ void UI_func_load_fx(uint8_t param) if (LCDML.FUNC_close()) // ****** STABLE END ********* { - eeprom_write(); - load_sd_fx(configuration.performance.fx_number); - lcd.setCursor(0, 1); - lcd.print("Done. "); - delay(500); + if (mode < 0xff) + { + lcd.setCursor(0, 1); + lcd.print("Canceled "); + delay(500); + } + encoderDir[ENC_R].reset(); } } @@ -3691,7 +3752,20 @@ void UI_func_save_fx(uint8_t param) } else { - LCDML.BT_quit(); // does not help :( + mode = 0xff; + eeprom_write(); + if (overwrite == false || yesno == true) + { + save_sd_fx(configuration.performance.fx_number); + lcd.setCursor(0, 1); + lcd.print("Done. "); + delay(500); + } + + if (LCDML.MENU_getLastActivFunctionID() < 0xff) + LCDML.OTHER_jumpToID(LCDML.MENU_getLastActivFunctionID()); + else + LCDML.OTHER_setCursorToID(LCDML.MENU_getLastCursorPositionID()); } } @@ -3728,15 +3802,64 @@ void UI_func_save_fx(uint8_t param) if (LCDML.FUNC_close()) // ****** STABLE END ********* { - eeprom_write(); - if (overwrite == false || yesno == true) + if (mode < 0xff) { - save_sd_fx(configuration.performance.fx_number); lcd.setCursor(0, 1); - lcd.print("Done. "); + lcd.print("Canceled "); delay(500); } + + encoderDir[ENC_R].reset(); + } +} + +void UI_func_save_voice(uint8_t param) +{ + static bool yesno; + + if (LCDML.FUNC_setup()) // ****** SETUP ********* + { encoderDir[ENC_R].reset(); + + lcd.setCursor(0, 0); + lcd.print(F("Save Voice")); + lcd.setCursor(0, 1); + lcd.print(F("[NO ]")); + + if (LCDML.FUNC_loop()) // ****** LOOP ********* + { + if ((LCDML.BT_checkDown() || LCDML.BT_checkUp())) + { + yesno = !yesno; + if (yesno == true) + { + lcd.setCursor(1, 1); + lcd.print(F("YES")); + } + else + { + lcd.setCursor(1, 1); + lcd.print(F("NO ")); + } + } + } + + if (LCDML.FUNC_close()) // ****** STABLE END ********* + { + if (yesno == true) + { + LCDML.DISP_clear(); + lcd.print(F("Save Cfg default")); + + // Storing on inside bank TBD + + lcd.setCursor(0, 1); + lcd.print(F("Done.")); + delay(500); + } + + encoderDir[ENC_R].reset(); + } } } diff --git a/UI_1_FX.h b/UI_1_FX.h index d02df26..31b8d64 100644 --- a/UI_1_FX.h +++ b/UI_1_FX.h @@ -26,7 +26,7 @@ #define _UI_H_ LCDML_add(0, LCDML_0, 1, "Voice", NULL); -LCDML_add(1, LCDML_0_1, 1, "Patch", UI_func_voice_select); +LCDML_add(1, LCDML_0_1, 1, "Select", UI_func_voice_select); LCDML_add(2, LCDML_0_1, 2, "Audio", NULL); LCDML_add(3, LCDML_0_1_2, 1, "Voice Level", UI_func_sound_intensity); LCDML_add(4, LCDML_0_1_2, 2, "Panorama", UI_func_panorama); @@ -99,25 +99,26 @@ LCDML_add(70, LCDML_0_2_3, 2, "Damping", UI_func_reverb_damping); LCDML_add(71, LCDML_0_2_3, 3, "Level", UI_func_reverb_level); LCDML_add(72, LCDML_0, 3, "Load/Save", NULL); LCDML_add(73, LCDML_0_3, 1, "Performance", NULL); -LCDML_add(74, LCDML_0_3_1, 1, "Load Perf. SD", UI_func_load_performance); -LCDML_add(75, LCDML_0_3_1, 2, "Save Perf. SD", UI_func_save_performance); -LCDML_add(76, LCDML_0_3, 2, "Voice", NULL); -LCDML_add(77, LCDML_0_3_2, 1, "Load Config SD", UI_func_load_voiceconfig); -LCDML_add(78, LCDML_0_3_2, 2, "Save Config SD", UI_func_save_voiceconfig); -LCDML_add(79, LCDML_0_3_2, 3, "Save Voice SD", UI_func_save_voice); -LCDML_add(80, LCDML_0_3, 3, "FX", NULL); -LCDML_add(81, LCDML_0_3_3, 1, "Load FX SD", UI_func_load_fx); -LCDML_add(82, LCDML_0_3_3, 2, "Save FX SD", UI_func_save_fx); -LCDML_add(83, LCDML_0_3, 4, "MIDI", NULL); -LCDML_add(84, LCDML_0_3_4, 1, "MIDI Recv Bank", UI_func_sysex_receive_bank); -LCDML_add(85, LCDML_0_3_4, 2, "MIDI Send Bank", UI_func_sysex_send_bank); -LCDML_add(86, LCDML_0_3, 5, "EEPROM Reset", UI_func_eeprom_reset); -LCDML_add(87, LCDML_0, 4, "System", NULL); -LCDML_add(88, LCDML_0_4, 3, "Volume", UI_func_volume); -LCDML_add(89, LCDML_0_4, 1, "Stereo/Mono", UI_func_stereo_mono); -LCDML_add(90, LCDML_0_4, 2, "MIDI Soft THRU", UI_func_midi_soft_thru); -LCDML_add(91, LCDML_0, 6, "Info", UI_func_information); -#define _LCDML_DISP_cnt 91 +LCDML_add(74, LCDML_0_3_1, 1, "Load Performance", UI_func_load_performance); +LCDML_add(75, LCDML_0_3_1, 2, "Save Performance", UI_func_save_performance); +LCDML_add(76, LCDML_0_3, 2, "Voice Config", NULL); +LCDML_add(77, LCDML_0_3_2, 1, "Load Voice Cfg", UI_func_load_voiceconfig); +LCDML_add(78, LCDML_0_3_2, 2, "Save Voice Cfg", UI_func_save_voiceconfig); +LCDML_add(79, LCDML_0_3, 3, "FX", NULL); +LCDML_add(80, LCDML_0_3_3, 1, "Load FX Cfg", UI_func_load_fx); +LCDML_add(81, LCDML_0_3_3, 2, "Save FX Cfg", UI_func_save_fx); +LCDML_add(82, LCDML_0_3, 4, "Voice", NULL); +LCDML_add(83, LCDML_0_3_4, 1, "Save Voice", UI_func_save_voice); +LCDML_add(84, LCDML_0_3, 5, "MIDI", NULL); +LCDML_add(85, LCDML_0_3_5, 1, "MIDI Recv Bank", UI_func_sysex_receive_bank); +LCDML_add(86, LCDML_0_3_5, 2, "MIDI Send Bank", UI_func_sysex_send_bank); +LCDML_add(87, LCDML_0_3, 6, "EEPROM Reset", UI_func_eeprom_reset); +LCDML_add(88, LCDML_0, 4, "System", NULL); +LCDML_add(89, LCDML_0_4, 3, "Volume", UI_func_volume); +LCDML_add(90, LCDML_0_4, 1, "Stereo/Mono", UI_func_stereo_mono); +LCDML_add(91, LCDML_0_4, 2, "MIDI Soft THRU", UI_func_midi_soft_thru); +LCDML_add(92, LCDML_0, 6, "Info", UI_func_information); +#define _LCDML_DISP_cnt 92 #define MENU_ID_OF_INSTANCE_2 58 -#define VOLUME_MENU_ID 88 +#define VOLUME_MENU_ID 89 #endif diff --git a/config.h b/config.h index 564868f..e5af379 100644 --- a/config.h +++ b/config.h @@ -223,12 +223,13 @@ #define MAX_VOICES 32 // voices per bank #define MAX_FX 15 #define MAX_PERFORMANCE 15 +#define MAX_VOICECONFIG 15 #define BANK_NAME_LEN 13 // FAT12 filenames (plus '\0') #define VOICE_NAME_LEN 11 // 10 (plus '\0') #define FILENAME_LEN BANK_NAME_LEN + VOICE_NAME_LEN -#define VOICE_CONFIG_NAME "VCFG" #define FX_CONFIG_NAME "FXCFG" #define PERFORMANCE_CONFIG_NAME "PCFG" +#define VOICE_CONFIG_NAME "VCFG" //************************************************************************************************* //* DO NO CHANGE ANYTHING BEYOND IF YOU DON'T KNOW WHAT YOU ARE DOING !!! @@ -517,6 +518,11 @@ enum { DEXED, CHORUS, DELAY, REVERB}; #define FX_NUM_MIN 0 #define FX_NUM_MAX MAX_FX #define FX_NUM_DEFAULT 0 + +#define VOICECONFIG_NUM_MIN 0 +#define VOICECONFIG_NUM_MAX MAX_VOICECONFIG +#define VOICECONFIG_NUM_DEFAULT 0 + // typedef struct { uint32_t checksum; @@ -575,6 +581,7 @@ typedef struct { uint32_t checksum; uint8_t bank[NUM_DEXED]; uint8_t voice[NUM_DEXED]; + uint8_t voiceconfig_number[NUM_DEXED]; uint8_t fx_number; } performance_t; diff --git a/dexed_sd.cpp b/dexed_sd.cpp index 3c89982..1156acc 100644 --- a/dexed_sd.cpp +++ b/dexed_sd.cpp @@ -76,7 +76,7 @@ bool load_sd_voice(uint8_t b, uint8_t v, uint8_t instance_id) #endif configuration.dexed[instance_id].transpose = MicroDexed[instance_id]->data[DEXED_VOICE_OFFSET + DEXED_TRANSPOSE]; - load_sd_voiceconfig(b, v, instance_id); // check for coice config to load + load_sd_voiceconfig(v, instance_id); // check for coice config to load return (ret); } @@ -176,17 +176,16 @@ bool get_sd_voice(File sysex, uint8_t voice_number, uint8_t* data) /****************************************************************************** SD VOICECONFIG ******************************************************************************/ -bool load_sd_voiceconfig(uint8_t b, uint8_t v, uint8_t instance_id) +bool load_sd_voiceconfig(uint8_t vc, uint8_t instance_id) { - v = constrain(v, 0, MAX_VOICES - 1); - b = constrain(b, 0, MAX_BANKS - 1); + vc = constrain(vc, 0, MAX_VOICECONFIG); if (sd_card > 0) { File sysex; char filename[FILENAME_LEN]; - sprintf(filename, "/%d/%s%d.syx", b, VOICE_CONFIG_NAME, v); + sprintf(filename, "/%s%d.syx", VOICE_CONFIG_NAME, vc); // first check if file exists... if (SD.exists(filename)) @@ -225,23 +224,20 @@ bool load_sd_voiceconfig(uint8_t b, uint8_t v, uint8_t instance_id) return (false); } -bool save_sd_voiceconfig(uint8_t b, uint8_t v, uint8_t instance_id) +bool save_sd_voiceconfig(uint8_t vc, uint8_t instance_id) { - v = constrain(v, 0, MAX_VOICES - 1); - b = constrain(b, 0, MAX_BANKS - 1); + vc = constrain(vc, 0, MAX_VOICECONFIG); if (sd_card > 0) { File sysex; char filename[FILENAME_LEN]; - sprintf(filename, "/%d/%s%d.syx", b, VOICE_CONFIG_NAME, v); + sprintf(filename, "/%s%d.syx", VOICE_CONFIG_NAME, vc); #ifdef DEBUG Serial.print(F("Saving voice config ")); - Serial.print(b); - Serial.print(F("/")); - Serial.print(v); + Serial.print(vc); Serial.print(F("[")); Serial.print(instance_id); Serial.print(F("]")); diff --git a/dexed_sd.h b/dexed_sd.h index 5aee272..71e7115 100644 --- a/dexed_sd.h +++ b/dexed_sd.h @@ -48,8 +48,8 @@ extern uint32_t crc32(byte * calc_start, uint16_t calc_bytes); bool load_sd_voice(uint8_t b, uint8_t v, uint8_t instance_id); bool get_sd_voice(File sysex, uint8_t voice_number, uint8_t* data); -bool load_sd_voiceconfig(uint8_t b, uint8_t v, uint8_t instance_id); -bool save_sd_voiceconfig(uint8_t b, uint8_t v, uint8_t instance_id); +bool load_sd_voiceconfig(uint8_t vc, uint8_t instance_id); +bool save_sd_voiceconfig(uint8_t vc, uint8_t instance_id); bool load_sd_fx(uint8_t fx); bool save_sd_fx(uint8_t fx); diff --git a/doc/MicroDexed_Menu_Structure.odt b/doc/MicroDexed_Menu_Structure.odt new file mode 100644 index 0000000000000000000000000000000000000000..3148fac5ba6cc3f4b4f02b93b09ae121819eb47a GIT binary patch literal 14131 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