Files
jack-looper/fs.c
2026-05-04 12:14:15 +00:00

700 lines
28 KiB
C

#include "fs.h"
#include "wav_io.h"
#include "dispatcher.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <time.h>
#include <stdatomic.h>
// ============================================================
// 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);
// Only autosave if at least one clip has a valid buffer
bool has_valid_clip = false;
for (int i = 0; i < MAX_CLIPS; i++) {
if (state.clips[i].buffer != NULL && state.clips[i].buffer_size > 0) {
has_valid_clip = true;
break;
}
}
if (!has_valid_clip) {
pthread_mutex_unlock(&autosave_mutex);
continue;
}
// Deep-copy the audio buffers to avoid race conditions
float *buffer_copies[MAX_CLIPS] = {NULL};
for (int i = 0; i < MAX_CLIPS; i++) {
if (state.clips[i].buffer != NULL && state.clips[i].buffer_size > 0) {
buffer_copies[i] = (float *)malloc(state.clips[i].buffer_size * sizeof(float));
if (buffer_copies[i]) {
memcpy(buffer_copies[i], state.clips[i].buffer, state.clips[i].buffer_size * sizeof(float));
// Temporarily point to our copy
state.clips[i].buffer = buffer_copies[i];
}
}
}
// 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;
// Free the buffer copies
for (int i = 0; i < MAX_CLIPS; i++) {
if (buffer_copies[i]) {
free(buffer_copies[i]);
buffer_copies[i] = NULL;
}
}
// 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 != NULL && 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, &params[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;
}