diff --git a/.gitignore b/.gitignore index 0af4da6..a689f8a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,6 @@ pkg forge-* confdeps.* conftest.* -forge_saved_state.txt +*.txt error.glsl draft/ \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 23d2a97..fed0f33 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -120,7 +120,8 @@ make -f Makefile.dev release-arch - [ ] Extra features - [x] `--auto-random-cycle=4` - [x] Arrows (up-down: bpm / left-right: cycle) - - [ ] Save states (numkey: load / shift + numkey: save) + - [x] Save states (numkey: load / shift + numkey: save) + - [ ] (clean) static functions at top of files - [ ] Configurable key codes - [ ] Key codes as inputs - [ ] Mouse position and scroll as inputs diff --git a/README.md b/README.md index b5806ce..6b18920 100644 --- a/README.md +++ b/README.md @@ -166,11 +166,13 @@ When running, the following hotkeys are available: | A | Auto Random mode On/Off | | / | Auto Random Cycle -/+ 1 | | / | BPM +/- 1 | +| 0-9 | Load state 0 to 9 | +| Shift + 0-9 | Save state 0 to 9 | ### CLI arguments ```txt -usage: forge [-h] [-v] [-p=PROJECT_PATH] [-c=CFG_FILE] [-hr] [-s=SCREEN] [-m=SCREEN] [-mo] [-w] [-t=TEMPO] [-d] [-ar / -nar] [-arc=CYCLES] [-v=FILE] [-vs=SIZE] [-is=SIZE] [-sf=STATE_PATH] [-ls / -nls] [-ss / -nss] [-tm] [-tf] +usage: forge [-h] [-v] [-p=PROJECT_PATH] [-c=CFG_FILE] [-hr] [-s=SCREEN] [-m=SCREEN] [-mo] [-w] [-t=TEMPO] [-d] [-ar / -nar] [-arc=CYCLES] [-v=FILE] [-vs=SIZE] [-is=SIZE] [-ls / -nls] [-ss / -nss] [-tm] [-tf] Fusion Of Real-time Generative Effects. @@ -192,7 +194,6 @@ options: -v, --video-in path to video capture device (multiple allowed) -vs, --video-size video capture desired height (default: internal texture height) -is, --internal-size internal texture height (default: 720) - -sf, --state-file saved state file (default: forge_saved_state.txt) -ls, --load-state load saved state (default) -nls, --no-load-state do not load saved state -ss, --save-state save state (default) diff --git a/default/forge_project.cfg b/default/forge_project.cfg index c807a5f..edb7a51 100644 --- a/default/forge_project.cfg +++ b/default/forge_project.cfg @@ -264,3 +264,9 @@ MIDI_3_1_Z= MIDI_3_2_X=0 MIDI_3_2_Y=16 MIDI_3_2_Z= + +# ===== +# OTHER +# ===== + +SAVE_FILE_PREFIX=forge_default_save \ No newline at end of file diff --git a/src/args.c b/src/args.c index e45625e..3cbd8b2 100644 --- a/src/args.c +++ b/src/args.c @@ -33,7 +33,6 @@ static void print_help(int status_code) { "[-v=FILE] " "[-vs=SIZE] " "[-is=SIZE] " - "[-sf=STATE_PATH] " "[-ls / -nls] " "[-ss / -nss] " "[-tm] " @@ -63,8 +62,6 @@ static void print_help(int status_code) { " -vs, --video-size video capture desired height (default: " "internal texture height)\n" " -is, --internal-size internal texture height (default: 720)\n" - " -sf, --state-file saved state file (default: " - "forge_saved_state.txt)\n" " -ls, --load-state load saved state (default)\n" " -nls, --no-load-state do not load saved state\n" " -ss, --save-state save state (default)\n" @@ -131,7 +128,6 @@ void args_parse(Parameters *params, int argc, char **argv) { params->video_in.length = 0; params->video_size = 0; params->internal_size = 720; - strlcpy(params->state_file, "forge_saved_state.txt", STR_LEN); params->load_state = true; params->save_state = true; params->trace_midi = false; @@ -194,8 +190,6 @@ void args_parse(Parameters *params, int argc, char **argv) { if (params->internal_size == 0) { invalid_value(arg, value); } - } else if (is_arg(arg, "-sf") || is_arg(arg, "--state-file")) { - strlcpy(params->state_file, value, STR_LEN); } else if (is_arg(arg, "-ls") || is_arg(arg, "--load-state")) { params->load_state = true; } else if (is_arg(arg, "-nls") || is_arg(arg, "--no-load-state")) { diff --git a/src/config_file.c b/src/config_file.c index a3bf6de..749cdfe 100644 --- a/src/config_file.c +++ b/src/config_file.c @@ -75,9 +75,12 @@ void config_file_read(ConfigFile *config, const char *path) { file_read(&file, path); if (file.error) { + config->error = true; return; } + config->error = false; + line = strtok_r(file.content, "\n", &rest); while (line != NULL) { diff --git a/src/forge.c b/src/forge.c index b572206..b126c08 100644 --- a/src/forge.c +++ b/src/forge.c @@ -59,8 +59,7 @@ static void compute_fps(bool trace_fps) { static void init_context(const Parameters *params, unsigned int in_count) { state_init(context, &project.state_config, params->demo, params->auto_random, - params->auto_random_cycle, params->base_tempo, params->state_file, - params->load_state); + params->auto_random_cycle, params->base_tempo, params->load_state); context->monitor = params->monitor; @@ -247,7 +246,7 @@ void forge_run(const Parameters *params) { context->stop = true; if (params->save_state) { - state_save(context, &project.state_config, params->state_file); + state_save(context, &project.state_config); } shaders_free(&program); diff --git a/src/midi.c b/src/midi.c index a4274a7..ff7dc7b 100644 --- a/src/midi.c +++ b/src/midi.c @@ -21,6 +21,10 @@ void midi_open(MidiDevice *device, const char *name) { void midi_write(const MidiDevice *device, unsigned char code, unsigned char value) { + if (device->error) { + return; + } + unsigned char buffer[3]; buffer[0] = 0xB0; diff --git a/src/state.c b/src/state.c index 5a9226c..eaef068 100644 --- a/src/state.c +++ b/src/state.c @@ -12,6 +12,232 @@ #include "state.h" #include "tempo.h" +static void safe_midi_write(const MidiDevice *midi, unsigned int code, + unsigned char value) { + if (code != UNSET_MIDI_CODE) { + midi_write(midi, code, value); + } +} + +static void update_page(const SharedContext *context, + const StateConfig *state_config, + const MidiDevice *midi) { + unsigned int page_item_min; + unsigned int page_item_max; + // SHOW PAGE + for (unsigned int i = 0; i < state_config->select_page_codes.length; i++) { + safe_midi_write(midi, state_config->select_page_codes.values[i], + i == context->page ? MIDI_MAX : 0); + } + + // SHOW PAGE ITEM + page_item_min = state_config->select_item_codes.length * context->page; + page_item_max = page_item_min + state_config->select_item_codes.length; + + if (context->state.values[context->selected] >= page_item_min && + context->state.values[context->selected] < page_item_max) { + for (unsigned int i = 0; i < state_config->select_item_codes.length; i++) { + safe_midi_write(midi, state_config->select_item_codes.values[i], + i == context->state.values[context->selected] - + page_item_min + ? MIDI_MAX + : 0); + } + } else { + for (unsigned int i = 0; i < state_config->select_item_codes.length; i++) { + safe_midi_write(midi, state_config->select_item_codes.values[i], 0); + } + } +} + +static void update_active(const SharedContext *context, + const StateConfig *state_config, + const MidiDevice *midi) { + unsigned int k; + + for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { + for (unsigned int j = 0; j < state_config->midi_active_counts.values[i]; + j++) { + k = state_config->midi_active_offsets.values[i] + j; + safe_midi_write(midi, state_config->midi_active_codes.values[k], + context->active[i] == j ? MIDI_MAX : 0); + } + } +} + +static void update_values(const SharedContext *context, + const StateConfig *state_config, + const MidiDevice *midi) { + unsigned int j; + unsigned int k; + unsigned int part; + + for (unsigned int i = 0; i < state_config->midi_codes.length; i++) { + j = i / 3; + part = arr_uint_remap_index(state_config->midi_offsets, &j); + k = state_config->values_offsets.values[part] + + context->active[part] * state_config->midi_counts.values[part] + j; + safe_midi_write(midi, state_config->midi_codes.values[i], + context->values[k][i % 3] * MIDI_MAX); + } +} + +static void reset(SharedContext *context) { + memset(context->values, 0, sizeof(context->values)); + memset(context->state.values, 0, sizeof(context->state.values)); +} + +static void randomize(SharedContext *context, const StateConfig *state_config) { + unsigned int j; + unsigned int l; + unsigned int part; + + for (unsigned int i = 0; i < state_config->midi_codes.length; i++) { + j = i / 3; + part = arr_uint_remap_index(state_config->midi_offsets, &j); + for (unsigned int k = 0; k < state_config->midi_active_counts.values[part]; + k++) { + l = state_config->values_offsets.values[part] + + k * state_config->midi_counts.values[part] + j; + + if (arr_uint_index_of(state_config->fader_codes, + state_config->midi_codes.values[i]) != + ARRAY_NOT_FOUND) { + context->values[l][i % 3] = (float)rand_uint(MIDI_MAX + 1) / MIDI_MAX; + } else { + context->values[l][i % 3] = rand_uint(2) == 1 ? 1 : 0; + } + } + } + + for (unsigned int i = 0; i < context->state.length; i++) { + context->state.values[i] = rand_uint(state_config->state_max); + } +} + +static void load_from_file(SharedContext *context, + const StateConfig *state_config, char *state_file) { + ConfigFile saved_state; + char key[STR_LEN]; + + config_file_read(&saved_state, state_file); + + if (saved_state.error) { + return; + } + + tempo_set(&context->tempo, + config_file_get_int(&saved_state, "tempo", context->tempo.tempo)); + context->page = config_file_get_int(&saved_state, "page", 0); + context->selected = config_file_get_int(&saved_state, "selected", 0); + + for (unsigned int i = 0; i < context->state.length; i++) { + snprintf(key, STR_LEN, "seed_%d", i); + context->seeds[i] = + config_file_get_int(&saved_state, key, context->seeds[i]); + snprintf(key, STR_LEN, "state_%d", i); + context->state.values[i] = config_file_get_int(&saved_state, key, 0); + } + + for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { + snprintf(key, STR_LEN, "active_%d", i); + context->active[i] = config_file_get_int(&saved_state, key, 0); + } + + for (unsigned int i = 0; i < state_config->value_count; i++) { + snprintf(key, STR_LEN, "value_%d_x", i); + context->values[i][0] = + (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; + snprintf(key, STR_LEN, "value_%d_y", i); + context->values[i][1] = + (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; + snprintf(key, STR_LEN, "value_%d_z", i); + context->values[i][2] = + (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; + } + + config_file_free(&saved_state); +} + +static void load_from_default_file(SharedContext *context, + const StateConfig *state_config) { + char state_file[STR_LEN]; + + snprintf(state_file, STR_LEN, "%s.txt", state_config->save_file_prefix); + + load_from_file(context, state_config, state_file); +} + +static void load_from_index_file(SharedContext *context, + const StateConfig *state_config, + unsigned int index) { + char state_file[STR_LEN]; + + snprintf(state_file, STR_LEN, "%s.%d.txt", state_config->save_file_prefix, + index); + + load_from_file(context, state_config, state_file); +} + +static void save_to_file(const SharedContext *context, + const StateConfig *state_config, + const char *state_file) { + StringArray lines; + + log_info("Saving state to '%s'...", state_file); + + lines.length = 0; + + snprintf(lines.values[lines.length++], STR_LEN, "tempo=%d", + (unsigned int)context->tempo.tempo); + snprintf(lines.values[lines.length++], STR_LEN, "page=%d", context->page); + snprintf(lines.values[lines.length++], STR_LEN, "selected=%d", + context->selected); + + for (unsigned int i = 0; i < context->state.length; i++) { + snprintf(lines.values[lines.length++], STR_LEN, "seed_%d=%d", i, + context->seeds[i]); + snprintf(lines.values[lines.length++], STR_LEN, "state_%d=%d", i, + context->state.values[i]); + } + + for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { + snprintf(lines.values[lines.length++], STR_LEN, "active_%d=%d", i, + context->active[i]); + } + + for (unsigned int i = 0; i < state_config->value_count; i++) { + snprintf(lines.values[lines.length++], STR_LEN, "value_%d_x=%d", i, + (unsigned int)(context->values[i][0] * MIDI_MAX)); + snprintf(lines.values[lines.length++], STR_LEN, "value_%d_y=%d", i, + (unsigned int)(context->values[i][1] * MIDI_MAX)); + snprintf(lines.values[lines.length++], STR_LEN, "value_%d_z=%d", i, + (unsigned int)(context->values[i][2] * MIDI_MAX)); + } + + file_write(state_file, &lines); +} + +static void save_to_default_file(const SharedContext *context, + const StateConfig *state_config) { + char state_file[STR_LEN]; + + snprintf(state_file, STR_LEN, "%s.txt", state_config->save_file_prefix); + + save_to_file(context, state_config, state_file); +} + +static void save_to_index_file(const SharedContext *context, + const StateConfig *state_config, + unsigned int index) { + char state_file[STR_LEN]; + + snprintf(state_file, STR_LEN, "%s.%d.txt", state_config->save_file_prefix, + index); + + save_to_file(context, state_config, state_file); +} + void state_parse_config(StateConfig *state_config, const ConfigFile *config) { unsigned int offset; unsigned int count; @@ -118,76 +344,10 @@ void state_parse_config(StateConfig *state_config, const ConfigFile *config) { state_config->tap_tempo_code = config_file_get_int(config, "TAP_TEMPO", UNSET_MIDI_CODE); -} -static void safe_midi_write(const MidiDevice *midi, unsigned int code, - unsigned char value) { - if (code != UNSET_MIDI_CODE) { - midi_write(midi, code, value); - } -} - -static void update_page(const SharedContext *context, - const StateConfig *state_config, - const MidiDevice *midi) { - unsigned int page_item_min; - unsigned int page_item_max; - // SHOW PAGE - for (unsigned int i = 0; i < state_config->select_page_codes.length; i++) { - safe_midi_write(midi, state_config->select_page_codes.values[i], - i == context->page ? MIDI_MAX : 0); - } - - // SHOW PAGE ITEM - page_item_min = state_config->select_item_codes.length * context->page; - page_item_max = page_item_min + state_config->select_item_codes.length; - - if (context->state.values[context->selected] >= page_item_min && - context->state.values[context->selected] < page_item_max) { - for (unsigned int i = 0; i < state_config->select_item_codes.length; i++) { - safe_midi_write(midi, state_config->select_item_codes.values[i], - i == context->state.values[context->selected] - - page_item_min - ? MIDI_MAX - : 0); - } - } else { - for (unsigned int i = 0; i < state_config->select_item_codes.length; i++) { - safe_midi_write(midi, state_config->select_item_codes.values[i], 0); - } - } -} - -static void update_active(const SharedContext *context, - const StateConfig *state_config, - const MidiDevice *midi) { - unsigned int k; - - for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { - for (unsigned int j = 0; j < state_config->midi_active_counts.values[i]; - j++) { - k = state_config->midi_active_offsets.values[i] + j; - safe_midi_write(midi, state_config->midi_active_codes.values[k], - context->active[i] == j ? MIDI_MAX : 0); - } - } -} - -static void update_values(const SharedContext *context, - const StateConfig *state_config, - const MidiDevice *midi) { - unsigned int j; - unsigned int k; - unsigned int part; - - for (unsigned int i = 0; i < state_config->midi_codes.length; i++) { - j = i / 3; - part = arr_uint_remap_index(state_config->midi_offsets, &j); - k = state_config->values_offsets.values[part] + - context->active[part] * state_config->midi_counts.values[part] + j; - safe_midi_write(midi, state_config->midi_codes.values[i], - context->values[k][i % 3] * MIDI_MAX); - } + strlcpy(state_config->save_file_prefix, + config_file_get_str(config, "SAVE_FILE_PREFIX", "forge_save"), + STR_LEN); } void state_midi_event(SharedContext *context, const StateConfig *state_config, @@ -281,39 +441,6 @@ void state_midi_event(SharedContext *context, const StateConfig *state_config, } } -static void reset(SharedContext *context) { - memset(context->values, 0, sizeof(context->values)); - memset(context->state.values, 0, sizeof(context->state.values)); -} - -static void randomize(SharedContext *context, const StateConfig *state_config) { - unsigned int j; - unsigned int l; - unsigned int part; - - for (unsigned int i = 0; i < state_config->midi_codes.length; i++) { - j = i / 3; - part = arr_uint_remap_index(state_config->midi_offsets, &j); - for (unsigned int k = 0; k < state_config->midi_active_counts.values[part]; - k++) { - l = state_config->values_offsets.values[part] + - k * state_config->midi_counts.values[part] + j; - - if (arr_uint_index_of(state_config->fader_codes, - state_config->midi_codes.values[i]) != - ARRAY_NOT_FOUND) { - context->values[l][i % 3] = (float)rand_uint(MIDI_MAX + 1) / MIDI_MAX; - } else { - context->values[l][i % 3] = rand_uint(2) == 1 ? 1 : 0; - } - } - } - - for (unsigned int i = 0; i < context->state.length; i++) { - context->state.values[i] = rand_uint(state_config->state_max); - } -} - void state_key_event(SharedContext *context, const StateConfig *state_config, unsigned int code, const MidiDevice *midi) { if (code == 82) { @@ -354,6 +481,12 @@ void state_key_event(SharedContext *context, const StateConfig *state_config, tempo_set(&context->tempo, context->tempo.tempo - 1); } log_info("[DOWN] Tempo: %f", context->tempo); + } else if (code >= 48 && code <= 57) { + log_info("[%d] Loading state %d", code - 48, code - 48); + load_from_index_file(context, state_config, code - 48); + } else if (code >= 1048 && code <= 1057) { + log_info("[%d] Saving state %d", code - 1048, code - 1048); + save_to_index_file(context, state_config, code - 1048); } else { log_info("[%d] No hotkey defined", code); } @@ -414,50 +547,9 @@ bool state_background_write(SharedContext *context, return false; } -static void state_load(SharedContext *context, const StateConfig *state_config, - const char *state_file) { - ConfigFile saved_state; - char key[STR_LEN]; - - config_file_read(&saved_state, state_file); - - tempo_set(&context->tempo, - config_file_get_int(&saved_state, "tempo", context->tempo.tempo)); - context->page = config_file_get_int(&saved_state, "page", 0); - context->selected = config_file_get_int(&saved_state, "selected", 0); - - for (unsigned int i = 0; i < context->state.length; i++) { - snprintf(key, STR_LEN, "seed_%d", i); - context->seeds[i] = - config_file_get_int(&saved_state, key, context->seeds[i]); - snprintf(key, STR_LEN, "state_%d", i); - context->state.values[i] = config_file_get_int(&saved_state, key, 0); - } - - for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { - snprintf(key, STR_LEN, "active_%d", i); - context->active[i] = config_file_get_int(&saved_state, key, 0); - } - - for (unsigned int i = 0; i < state_config->value_count; i++) { - snprintf(key, STR_LEN, "value_%d_x", i); - context->values[i][0] = - (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; - snprintf(key, STR_LEN, "value_%d_y", i); - context->values[i][1] = - (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; - snprintf(key, STR_LEN, "value_%d_z", i); - context->values[i][2] = - (float)config_file_get_int(&saved_state, key, 0) / MIDI_MAX; - } - - config_file_free(&saved_state); -} - void state_init(SharedContext *context, const StateConfig *state_config, bool demo, bool auto_random, unsigned int auto_random_cycles, - unsigned int base_tempo, const char *state_file, - bool load_state) { + unsigned int base_tempo, bool load_state) { tempo_init(&context->tempo); tempo_set(&context->tempo, base_tempo); context->demo = demo; @@ -484,44 +576,10 @@ void state_init(SharedContext *context, const StateConfig *state_config, } if (load_state) { - state_load(context, state_config, state_file); + load_from_default_file(context, state_config); } } -void state_save(const SharedContext *context, const StateConfig *state_config, - const char *state_file) { - StringArray lines; - - log_info("Saving state to '%s'...", state_file); - - lines.length = 0; - - snprintf(lines.values[lines.length++], STR_LEN, "tempo=%d", - (unsigned int)context->tempo.tempo); - snprintf(lines.values[lines.length++], STR_LEN, "page=%d", context->page); - snprintf(lines.values[lines.length++], STR_LEN, "selected=%d", - context->selected); - - for (unsigned int i = 0; i < context->state.length; i++) { - snprintf(lines.values[lines.length++], STR_LEN, "seed_%d=%d", i, - context->seeds[i]); - snprintf(lines.values[lines.length++], STR_LEN, "state_%d=%d", i, - context->state.values[i]); - } - - for (unsigned int i = 0; i < state_config->midi_active_counts.length; i++) { - snprintf(lines.values[lines.length++], STR_LEN, "active_%d=%d", i, - context->active[i]); - } - - for (unsigned int i = 0; i < state_config->value_count; i++) { - snprintf(lines.values[lines.length++], STR_LEN, "value_%d_x=%d", i, - (unsigned int)(context->values[i][0] * MIDI_MAX)); - snprintf(lines.values[lines.length++], STR_LEN, "value_%d_y=%d", i, - (unsigned int)(context->values[i][1] * MIDI_MAX)); - snprintf(lines.values[lines.length++], STR_LEN, "value_%d_z=%d", i, - (unsigned int)(context->values[i][2] * MIDI_MAX)); - } - - file_write(state_file, &lines); +void state_save(const SharedContext *context, const StateConfig *state_config) { + save_to_default_file(context, state_config); } diff --git a/src/state.h b/src/state.h index 438063e..1fae705 100644 --- a/src/state.h +++ b/src/state.h @@ -18,10 +18,8 @@ bool state_background_write(SharedContext *context, void state_init(SharedContext *context, const StateConfig *state_config, bool demo, bool auto_random, unsigned int auto_random_cycles, - unsigned int base_tempo, const char *state_file, - bool load_state); + unsigned int base_tempo, bool load_state); -void state_save(const SharedContext *context, const StateConfig *state_config, - const char *state_file); +void state_save(const SharedContext *context, const StateConfig *state_config); #endif /* STATE_H */ \ No newline at end of file diff --git a/src/types.h b/src/types.h index 7867e26..1db22d3 100644 --- a/src/types.h +++ b/src/types.h @@ -47,7 +47,6 @@ typedef struct Parameters { StringArray video_in; unsigned int video_size; unsigned int internal_size; - char state_file[STR_LEN]; bool load_state; bool save_state; bool trace_midi; @@ -213,6 +212,8 @@ typedef struct StateConfig { unsigned int value_count; unsigned int tap_tempo_code; + + char save_file_prefix[STR_LEN]; } StateConfig; // timer.c @@ -227,6 +228,7 @@ typedef struct Timer { typedef struct ConfigFile { struct hashmap *map; + bool error; } ConfigFile; typedef struct ConfigFileItem {