#include "fs.h" #include "wav_io.h" #include "dispatcher.h" #include #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 atomic_bool autosave_running = false; static volatile bool autosave_pending = false; 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 (atomic_load(&autosave_running)) { pthread_mutex_lock(&autosave_mutex); // Wait for trigger or shutdown while (atomic_load(&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) { // Get current state from dispatcher AppState state; dispatcher_get_state(&state); // 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, &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(void) { atomic_store(&autosave_running, true); autosave_pending = false; ensure_autosave_dir(); pthread_create(&autosave_thread, NULL, autosave_thread_func, NULL); } void fs_cleanup(void) { atomic_store(&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; }