diff --git a/dispatcher.c b/dispatcher.c index 82607de..ab21e68 100644 --- a/dispatcher.c +++ b/dispatcher.c @@ -1,5 +1,6 @@ #include "dispatcher.h" #include "wav_io.h" +#include "fs.h" #include #include #include @@ -69,6 +70,11 @@ static bool pop_action(Action *action) { static void dispatch_function(Action action) { push_action(action); pthread_cond_signal(&dispatcher.action_cond); + + // Trigger autosave for non-quit actions + if (action.type != ACTION_QUIT) { + fs_trigger_autosave(); + } } // ============================================================ @@ -447,6 +453,48 @@ AppState reducer(AppState state, Action action) { case ACTION_PROCESS_AUDIO: return state; + case ACTION_SAVE_PROJECT: { + fs_save_project(action.data.save_project.filename, &state); + return state; + } + + case ACTION_LOAD_PROJECT: { + // Reset clips first + for (int i = 0; i < MAX_CLIPS; i++) { + Clip *clip = &state.clips[i]; + clip->state = CLIP_EMPTY; + clip->buffer_size = 0; + clip->write_position = 0; + clip->read_position = 0; + if (clip->buffer) memset(clip->buffer, 0, MAX_BUFFER_SIZE * sizeof(float)); + } + + // Reset Carla host + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + ChannelRack *rack = &state.carla_host.channel_racks[ch]; + for (int p = 0; p < rack->num_plugins; p++) { + PluginInfo *plugin = &rack->plugins[p]; + if (plugin->parameters) { + free(plugin->parameters); + plugin->parameters = NULL; + } + if (plugin->parameter_names) { + for (int pi = 0; pi < plugin->num_parameters; pi++) { + free(plugin->parameter_names[pi]); + } + free(plugin->parameter_names); + plugin->parameter_names = NULL; + } + } + rack->num_plugins = 0; + rack->volume = 1.0f; + rack->bypassed = false; + } + + fs_load_project(action.data.load_project.filename, &state); + return state; + } + case ACTION_QUIT: state.running = false; return state; diff --git a/dispatcher.h b/dispatcher.h index 2b3f2fa..9383dee 100644 --- a/dispatcher.h +++ b/dispatcher.h @@ -121,6 +121,8 @@ typedef enum { ACTION_RACK_SET_PARAMETER, ACTION_RACK_SET_VOLUME, ACTION_RACK_BYPASS, + ACTION_SAVE_PROJECT, + ACTION_LOAD_PROJECT, ACTION_PROCESS_AUDIO, ACTION_QUIT } ActionType; @@ -144,6 +146,8 @@ typedef struct { struct { int channel; int plugin_index; int param_index; float value; } rack_set_parameter; struct { int channel; float volume; } rack_set_volume; struct { int channel; bool bypass; } rack_bypass; + struct { char filename[512]; } save_project; + struct { char filename[512]; } load_project; struct { jack_nframes_t nframes; } process_audio; } data; } Action; diff --git a/fs.c b/fs.c new file mode 100644 index 0000000..1c269cb --- /dev/null +++ b/fs.c @@ -0,0 +1,661 @@ +#include "fs.h" +#include "wav_io.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ============================================================ +// Auto-save thread +// ============================================================ + +static pthread_t autosave_thread; +static pthread_mutex_t autosave_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t autosave_cond = PTHREAD_COND_INITIALIZER; +static volatile bool autosave_running = false; +static volatile bool autosave_pending = false; +static AppState *g_state = NULL; +static time_t last_autosave_time = 0; + +// Directory for autosave files +#define AUTOSAVE_DIR "/tmp/jack-looper-autosave" + +static void ensure_autosave_dir(void) { + struct stat st = {0}; + if (stat(AUTOSAVE_DIR, &st) == -1) { + mkdir(AUTOSAVE_DIR, 0700); + } +} + +static void* autosave_thread_func(void *arg) { + (void)arg; + + while (autosave_running) { + pthread_mutex_lock(&autosave_mutex); + + // Wait for trigger or shutdown + while (autosave_running && !autosave_pending) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 5; // Wake up every 5 seconds to check + pthread_cond_timedwait(&autosave_cond, &autosave_mutex, &ts); + } + + if (!autosave_running) { + pthread_mutex_unlock(&autosave_mutex); + break; + } + + autosave_pending = false; + + // Only autosave if at least 10 seconds have passed since last save + time_t now = time(NULL); + if (now - last_autosave_time >= 10) { + // Generate autosave filename with timestamp + char filename[512]; + time_t t = time(NULL); + struct tm *tm = localtime(&t); + snprintf(filename, sizeof(filename), "%s/autosave_%04d%02d%02d_%02d%02d%02d.wheel", + AUTOSAVE_DIR, + tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, + tm->tm_hour, tm->tm_min, tm->tm_sec); + + // Save the project + fs_save_project(filename, g_state); + last_autosave_time = now; + + // Remove old autosaves (keep last 10) + DIR *d = opendir(AUTOSAVE_DIR); + if (d) { + // Simple approach: just remove files older than 1 hour + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + if (strstr(entry->d_name, ".wheel")) { + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", AUTOSAVE_DIR, entry->d_name); + struct stat st; + if (stat(path, &st) == 0) { + if (now - st.st_mtime > 3600) { + unlink(path); + } + } + } + } + closedir(d); + } + } + + pthread_mutex_unlock(&autosave_mutex); + } + + return NULL; +} + +void fs_init(AppState *state) { + g_state = state; + autosave_running = true; + autosave_pending = false; + ensure_autosave_dir(); + pthread_create(&autosave_thread, NULL, autosave_thread_func, NULL); +} + +void fs_cleanup(void) { + autosave_running = false; + pthread_cond_signal(&autosave_cond); + pthread_join(autosave_thread, NULL); +} + +void fs_trigger_autosave(void) { + pthread_mutex_lock(&autosave_mutex); + autosave_pending = true; + pthread_cond_signal(&autosave_cond); + pthread_mutex_unlock(&autosave_mutex); +} + +// ============================================================ +// Project save/load +// ============================================================ + +// Simple JSON-like serialization (not full JSON, just enough for our needs) + +static void write_string(FILE *f, const char *key, const char *value) { + fprintf(f, "\"%s\": \"%s\",\n", key, value); +} + +static void write_int(FILE *f, const char *key, int value) { + fprintf(f, "\"%s\": %d,\n", key, value); +} + +static void write_float(FILE *f, const char *key, float value) { + fprintf(f, "\"%s\": %f,\n", key, value); +} + +static void write_double(FILE *f, const char *key, double value) { + fprintf(f, "\"%s\": %f,\n", key, value); +} + +static void write_bool(FILE *f, const char *key, bool value) { + fprintf(f, "\"%s\": %s,\n", key, value ? "true" : "false"); +} + +static void write_uint32(FILE *f, const char *key, uint32_t value) { + fprintf(f, "\"%s\": %u,\n", key, value); +} + +int fs_save_project(const char *filename, AppState *state) { + if (!filename || !state) return -1; + + // Create directory for samples if needed + char samples_dir[512]; + snprintf(samples_dir, sizeof(samples_dir), "%s.samples", filename); + mkdir(samples_dir, 0755); + + FILE *f = fopen(filename, "w"); + if (!f) return -1; + + fprintf(f, "{\n"); + + // Transport state + fprintf(f, "\"transport\": {\n"); + write_int(f, "state", state->transport_state); + write_int(f, "clock_source", state->clock_source); + write_uint32(f, "clock_count", state->clock_count); + write_uint32(f, "beat_position", state->beat_position); + write_uint32(f, "bar_position", state->bar_position); + write_uint32(f, "sample_position", state->sample_position); + write_double(f, "bpm", state->bpm); + write_double(f, "samples_per_beat", state->samples_per_beat); + write_double(f, "sample_accumulator", state->sample_accumulator); + fprintf(f, "\"quantize_mode\": %d,\n", state->quantize_mode); + fprintf(f, "\"quantize_threshold\": %u\n", state->quantize_threshold); + fprintf(f, "},\n"); + + // Clips + fprintf(f, "\"clips\": [\n"); + for (int i = 0; i < MAX_CLIPS; i++) { + Clip *clip = &state->clips[i]; + fprintf(f, " {\n"); + write_int(f, "index", i); + write_int(f, "state", clip->state); + write_uint32(f, "buffer_size", clip->buffer_size); + write_uint32(f, "write_position", clip->write_position); + write_uint32(f, "read_position", clip->read_position); + + // Save sample buffer to separate .wav file + if (clip->buffer && clip->buffer_size > 0) { + char wav_path[1024]; + snprintf(wav_path, sizeof(wav_path), "%s/clip_%d.wav", samples_dir, i); + save_wav_float(wav_path, clip->buffer, clip->buffer_size, state->sample_rate); + write_string(f, "sample_path", wav_path); + } else { + write_string(f, "sample_path", ""); + } + + fprintf(f, " },\n"); + } + fprintf(f, "],\n"); + + // Carla host state (simplified - just plugin names and parameters) + fprintf(f, "\"carla_host\": {\n"); + fprintf(f, "\"channel_racks\": [\n"); + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + ChannelRack *rack = &state->carla_host.channel_racks[ch]; + fprintf(f, " {\n"); + write_int(f, "channel", ch); + write_float(f, "volume", rack->volume); + write_bool(f, "bypassed", rack->bypassed); + write_int(f, "num_plugins", rack->num_plugins); + + fprintf(f, "\"plugins\": [\n"); + for (int p = 0; p < rack->num_plugins; p++) { + PluginInfo *plugin = &rack->plugins[p]; + fprintf(f, " {\n"); + write_string(f, "name", plugin->name); + write_string(f, "uri", plugin->uri); + write_int(f, "type", plugin->type); + write_int(f, "num_parameters", plugin->num_parameters); + + fprintf(f, "\"parameters\": ["); + for (int param = 0; param < plugin->num_parameters; param++) { + fprintf(f, "%f", plugin->parameters[param]); + if (param < plugin->num_parameters - 1) fprintf(f, ", "); + } + fprintf(f, "],\n"); + + fprintf(f, "\"parameter_names\": ["); + for (int param = 0; param < plugin->num_parameters; param++) { + fprintf(f, "\"%s\"", plugin->parameter_names[param]); + if (param < plugin->num_parameters - 1) fprintf(f, ", "); + } + fprintf(f, "]\n"); + + fprintf(f, " },\n"); + } + fprintf(f, "]\n"); + fprintf(f, " },\n"); + } + fprintf(f, "]\n"); + fprintf(f, "}\n"); + + fprintf(f, "}\n"); + + fclose(f); + return 0; +} + +// Simple string parsing helpers +static const char* skip_whitespace(const char *p) { + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + return p; +} + +static const char* parse_string(const char *p, char *out, size_t out_size) { + p = skip_whitespace(p); + if (*p != '"') return NULL; + p++; + + size_t i = 0; + while (*p && *p != '"' && i < out_size - 1) { + if (*p == '\\' && *(p+1)) { + p++; + switch (*p) { + case '"': out[i++] = '"'; break; + case '\\': out[i++] = '\\'; break; + case 'n': out[i++] = '\n'; break; + default: out[i++] = *p; break; + } + } else { + out[i++] = *p; + } + p++; + } + out[i] = '\0'; + + if (*p == '"') p++; + return p; +} + +static const char* parse_int(const char *p, int *out) { + p = skip_whitespace(p); + *out = atoi(p); + while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; + return p; +} + +static const char* parse_uint32(const char *p, uint32_t *out) { + p = skip_whitespace(p); + *out = (uint32_t)atoi(p); + while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; + return p; +} + +static const char* parse_float(const char *p, float *out) { + p = skip_whitespace(p); + *out = atof(p); + while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; + return p; +} + +static const char* parse_double(const char *p, double *out) { + p = skip_whitespace(p); + *out = atof(p); + while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; + return p; +} + +static const char* parse_bool(const char *p, bool *out) { + p = skip_whitespace(p); + *out = (strncmp(p, "true", 4) == 0); + while (*p && *p != ',' && *p != '}' && *p != ']' && *p != '\n') p++; + return p; +} + +int fs_load_project(const char *filename, AppState *state) { + if (!filename || !state) return -1; + + FILE *f = fopen(filename, "r"); + if (!f) return -1; + + // Read entire file + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + char *content = (char *)malloc(file_size + 1); + if (!content) { + fclose(f); + return -1; + } + + fread(content, 1, file_size, f); + content[file_size] = '\0'; + fclose(f); + + const char *p = content; + + // Parse top-level object + p = skip_whitespace(p); + if (*p != '{') { free(content); return -1; } + p++; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') break; + + char key[256]; + p = parse_string(p, key, sizeof(key)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(key, "transport") == 0) { + p = skip_whitespace(p); + if (*p == '{') p++; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') { p++; break; } + + char tkey[256]; + p = parse_string(p, tkey, sizeof(tkey)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(tkey, "state") == 0) { + int val; p = parse_int(p, &val); state->transport_state = (TransportState)val; + } else if (strcmp(tkey, "clock_source") == 0) { + int val; p = parse_int(p, &val); state->clock_source = (ClockSource)val; + } else if (strcmp(tkey, "clock_count") == 0) { + p = parse_uint32(p, &state->clock_count); + } else if (strcmp(tkey, "beat_position") == 0) { + p = parse_uint32(p, &state->beat_position); + } else if (strcmp(tkey, "bar_position") == 0) { + p = parse_uint32(p, &state->bar_position); + } else if (strcmp(tkey, "sample_position") == 0) { + p = parse_uint32(p, &state->sample_position); + } else if (strcmp(tkey, "bpm") == 0) { + p = parse_double(p, &state->bpm); + } else if (strcmp(tkey, "samples_per_beat") == 0) { + p = parse_double(p, &state->samples_per_beat); + } else if (strcmp(tkey, "sample_accumulator") == 0) { + p = parse_double(p, &state->sample_accumulator); + } else if (strcmp(tkey, "quantize_mode") == 0) { + int val; p = parse_int(p, &val); state->quantize_mode = (QuantizeMode)val; + } else if (strcmp(tkey, "quantize_threshold") == 0) { + p = parse_uint32(p, &state->quantize_threshold); + } else { + // Skip unknown value + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + } else if (strcmp(key, "clips") == 0) { + p = skip_whitespace(p); + if (*p == '[') p++; + + while (*p) { + p = skip_whitespace(p); + if (*p == ']') { p++; break; } + + if (*p == '{') p++; + + int clip_index = -1; + ClipState clip_state = CLIP_EMPTY; + uint32_t buffer_size = 0; + uint32_t write_position = 0; + uint32_t read_position = 0; + char sample_path[1024] = {0}; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') { p++; break; } + + char ckey[256]; + p = parse_string(p, ckey, sizeof(ckey)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(ckey, "index") == 0) { + p = parse_int(p, &clip_index); + } else if (strcmp(ckey, "state") == 0) { + int val; p = parse_int(p, &val); clip_state = (ClipState)val; + } else if (strcmp(ckey, "buffer_size") == 0) { + p = parse_uint32(p, &buffer_size); + } else if (strcmp(ckey, "write_position") == 0) { + p = parse_uint32(p, &write_position); + } else if (strcmp(ckey, "read_position") == 0) { + p = parse_uint32(p, &read_position); + } else if (strcmp(ckey, "sample_path") == 0) { + p = parse_string(p, sample_path, sizeof(sample_path)); + } else { + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + + if (clip_index >= 0 && clip_index < MAX_CLIPS) { + Clip *clip = &state->clips[clip_index]; + clip->state = clip_state; + clip->buffer_size = buffer_size; + clip->write_position = write_position; + clip->read_position = read_position; + + // Load sample buffer from .wav file if path exists + if (sample_path[0] != '\0') { + float *loaded_buffer = NULL; + size_t loaded_samples = 0; + unsigned int loaded_sr = 0; + if (load_wav_float(sample_path, &loaded_buffer, &loaded_samples, &loaded_sr) == 0) { + if (clip->buffer) free(clip->buffer); + clip->buffer = (float *)calloc(MAX_BUFFER_SIZE, sizeof(float)); + if (clip->buffer && loaded_buffer) { + size_t copy_size = (loaded_samples < MAX_BUFFER_SIZE) ? loaded_samples : MAX_BUFFER_SIZE; + memcpy(clip->buffer, loaded_buffer, copy_size * sizeof(float)); + clip->buffer_size = copy_size; + } + if (loaded_buffer) free(loaded_buffer); + } + } + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + } else if (strcmp(key, "carla_host") == 0) { + p = skip_whitespace(p); + if (*p == '{') p++; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') { p++; break; } + + char ckey[256]; + p = parse_string(p, ckey, sizeof(ckey)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(ckey, "channel_racks") == 0) { + p = skip_whitespace(p); + if (*p == '[') p++; + + while (*p) { + p = skip_whitespace(p); + if (*p == ']') { p++; break; } + + if (*p == '{') p++; + + int channel = -1; + float volume = 1.0f; + bool bypassed = false; + int num_plugins = 0; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') { p++; break; } + + char rkey[256]; + p = parse_string(p, rkey, sizeof(rkey)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(rkey, "channel") == 0) { + p = parse_int(p, &channel); + } else if (strcmp(rkey, "volume") == 0) { + p = parse_float(p, &volume); + } else if (strcmp(rkey, "bypassed") == 0) { + p = parse_bool(p, &bypassed); + } else if (strcmp(rkey, "num_plugins") == 0) { + p = parse_int(p, &num_plugins); + } else if (strcmp(rkey, "plugins") == 0) { + p = skip_whitespace(p); + if (*p == '[') p++; + + int plugin_idx = 0; + while (*p && plugin_idx < num_plugins) { + p = skip_whitespace(p); + if (*p == ']') { p++; break; } + + if (*p == '{') p++; + + char plugin_name[256] = {0}; + char plugin_uri[512] = {0}; + int plugin_type = PLUGIN_TYPE_INTERNAL; + int num_params = 0; + float params[64] = {0}; + char param_names[64][256] = {{0}}; + + while (*p) { + p = skip_whitespace(p); + if (*p == '}') { p++; break; } + + char pkey[256]; + p = parse_string(p, pkey, sizeof(pkey)); + if (!p) { free(content); return -1; } + + p = skip_whitespace(p); + if (*p != ':') { free(content); return -1; } + p++; + + if (strcmp(pkey, "name") == 0) { + p = parse_string(p, plugin_name, sizeof(plugin_name)); + } else if (strcmp(pkey, "uri") == 0) { + p = parse_string(p, plugin_uri, sizeof(plugin_uri)); + } else if (strcmp(pkey, "type") == 0) { + p = parse_int(p, &plugin_type); + } else if (strcmp(pkey, "num_parameters") == 0) { + p = parse_int(p, &num_params); + } else if (strcmp(pkey, "parameters") == 0) { + p = skip_whitespace(p); + if (*p == '[') p++; + for (int pi = 0; pi < num_params && pi < 64; pi++) { + p = parse_float(p, ¶ms[pi]); + p = skip_whitespace(p); + if (*p == ',') p++; + } + if (*p == ']') p++; + } else if (strcmp(pkey, "parameter_names") == 0) { + p = skip_whitespace(p); + if (*p == '[') p++; + for (int pi = 0; pi < num_params && pi < 64; pi++) { + p = parse_string(p, param_names[pi], sizeof(param_names[pi])); + p = skip_whitespace(p); + if (*p == ',') p++; + } + if (*p == ']') p++; + } else { + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + + // Add plugin to rack + if (channel >= 0 && channel < MAX_CHANNELS && plugin_idx < 16) { + ChannelRack *rack = &state->carla_host.channel_racks[channel]; + PluginInfo *plugin = &rack->plugins[plugin_idx]; + strncpy(plugin->name, plugin_name, sizeof(plugin->name) - 1); + strncpy(plugin->uri, plugin_uri, sizeof(plugin->uri) - 1); + plugin->type = (PluginType)plugin_type; + plugin->num_parameters = num_params; + + if (plugin->parameters) free(plugin->parameters); + if (plugin->parameter_names) { + for (int pi = 0; pi < plugin->num_parameters; pi++) { + free(plugin->parameter_names[pi]); + } + free(plugin->parameter_names); + } + + plugin->parameters = (float *)malloc(num_params * sizeof(float)); + plugin->parameter_names = (char **)malloc(num_params * sizeof(char *)); + for (int pi = 0; pi < num_params; pi++) { + plugin->parameters[pi] = params[pi]; + plugin->parameter_names[pi] = strdup(param_names[pi]); + } + + plugin_idx++; + rack->num_plugins = plugin_idx; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + } else { + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + + if (channel >= 0 && channel < MAX_CHANNELS) { + state->carla_host.channel_racks[channel].volume = volume; + state->carla_host.channel_racks[channel].bypassed = bypassed; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + } else { + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + } else { + // Skip unknown key + while (*p && *p != ',' && *p != '\n') p++; + } + + p = skip_whitespace(p); + if (*p == ',') p++; + } + + free(content); + return 0; +} diff --git a/fs.h b/fs.h new file mode 100644 index 0000000..7a444c2 --- /dev/null +++ b/fs.h @@ -0,0 +1,21 @@ +#ifndef FS_H +#define FS_H + +#include "dispatcher.h" + +// Initialize the auto-save thread +void fs_init(AppState *state); + +// Cleanup the auto-save thread +void fs_cleanup(void); + +// Trigger an auto-save (thread-safe, non-blocking) +void fs_trigger_autosave(void); + +// Save project to .wheel file (called from dispatcher thread) +int fs_save_project(const char *filename, AppState *state); + +// Load project from .wheel file (called from dispatcher thread) +int fs_load_project(const char *filename, AppState *state); + +#endif // FS_H diff --git a/main.c b/main.c index 1e86d54..f418041 100644 --- a/main.c +++ b/main.c @@ -9,6 +9,7 @@ #include "gui.h" #include "dispatcher.h" #include "carla.h" +#include "fs.h" static Engine engine; static volatile int keep_running = 1; @@ -95,6 +96,9 @@ int main(int argc, char *argv[]) { // Initialize dispatcher dispatch = dispatcher_init(&initial_state); + // Initialize filesystem module (auto-save thread) + fs_init(&initial_state); + // Initialize engine if (engine_init(&engine, client_name, dispatch) != 0) { fprintf(stderr, "Failed to initialize engine\n"); @@ -122,6 +126,18 @@ int main(int argc, char *argv[]) { printf("Sample rate: %u Hz\n", engine.sample_rate); printf("Press Ctrl+C to stop\n\n"); + // Load project file if specified + if (optind < argc) { + const char *wheel_file = argv[optind]; + size_t len = strlen(wheel_file); + if (len > 6 && strcmp(wheel_file + len - 6, ".wheel") == 0) { + Action action = { .type = ACTION_LOAD_PROJECT }; + strncpy(action.data.load_project.filename, wheel_file, sizeof(action.data.load_project.filename) - 1); + action.data.load_project.filename[sizeof(action.data.load_project.filename) - 1] = '\0'; + dispatch(action); + } + } + // Run selected frontend switch (frontend) { case FRONTEND_TUI: @@ -141,6 +157,7 @@ int main(int argc, char *argv[]) { printf("\nShutting down...\n"); engine_stop(&engine); engine_cleanup(&engine); + fs_cleanup(); dispatcher_stop(); return 0; diff --git a/makefile b/makefile index bc5f480..c110646 100644 --- a/makefile +++ b/makefile @@ -4,22 +4,22 @@ LDFLAGS = -ljack -lm -lncurses -lpthread all: jack-looper test_engine test_tui test_gui test_cli test_stress test_wav_io -jack-looper: main.o engine.o tui.o gui.o cli.o transport.o dispatcher.o lib/microui.o wav_io.o carla.o +jack-looper: main.o engine.o tui.o gui.o cli.o transport.o dispatcher.o lib/microui.o wav_io.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_engine: test_engine.o engine.o transport.o wav_io.o dispatcher.o carla.o +test_engine: test_engine.o engine.o transport.o wav_io.o dispatcher.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_tui: test_tui.o engine.o transport.o wav_io.o dispatcher.o carla.o +test_tui: test_tui.o engine.o transport.o wav_io.o dispatcher.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_gui: test_gui.o gui.o engine.o transport.o lib/microui.o wav_io.o dispatcher.o carla.o +test_gui: test_gui.o gui.o engine.o transport.o lib/microui.o wav_io.o dispatcher.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_cli: test_cli.o engine.o cli.o transport.o wav_io.o dispatcher.o carla.o +test_cli: test_cli.o engine.o cli.o transport.o wav_io.o dispatcher.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_stress: test_stress.o engine.o wav_io.o dispatcher.o carla.o +test_stress: test_stress.o engine.o wav_io.o dispatcher.o carla.o fs.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_wav_io: test_wav_io.o wav_io.o @@ -52,7 +52,10 @@ transport.o: transport.c transport.h carla.o: carla.c carla.h $(CC) $(CFLAGS) -c -o $@ $< -dispatcher.o: dispatcher.c dispatcher.h carla.h +dispatcher.o: dispatcher.c dispatcher.h carla.h fs.h + $(CC) $(CFLAGS) -c -o $@ $< + +fs.o: fs.c fs.h dispatcher.h wav_io.h $(CC) $(CFLAGS) -c -o $@ $< test_engine.o: test_engine.c engine.h transport.h dispatcher.h diff --git a/tui.c b/tui.c index 183347d..6e7dcd6 100644 --- a/tui.c +++ b/tui.c @@ -745,10 +745,23 @@ static bool handle_command_mode(void) { } } } else if (strncmp(cmd_buffer, "save ", 5) == 0) { - int clip_idx = atoi(cmd_buffer + 5); - Action action = { .type = ACTION_SAVE_CLIP, .data.save_clip = { .clip_index = clip_idx } }; - g_dispatch(action); - } + const char *filename = cmd_buffer + 5; + while (*filename == ' ') filename++; + if (*filename) { + Action action = { .type = ACTION_SAVE_PROJECT }; + strncpy(action.data.save_project.filename, filename, sizeof(action.data.save_project.filename) - 1); + action.data.save_project.filename[sizeof(action.data.save_project.filename) - 1] = '\0'; + g_dispatch(action); + } + } else if (strncmp(cmd_buffer, "load ", 5) == 0) { + const char *filename = cmd_buffer + 5; + while (*filename == ' ') filename++; + if (*filename) { + Action action = { .type = ACTION_LOAD_PROJECT }; + strncpy(action.data.load_project.filename, filename, sizeof(action.data.load_project.filename) - 1); + action.data.load_project.filename[sizeof(action.data.load_project.filename) - 1] = '\0'; + g_dispatch(action); + } nodelay(stdscr, prev_nodelay); return false;