feat: add project save/load with .wheel files and auto-save thread
Co-authored-by: aider (deepseek/deepseek-coder) <aider@aider.chat>
This commit is contained in:
48
dispatcher.c
48
dispatcher.c
@@ -1,5 +1,6 @@
|
||||
#include "dispatcher.h"
|
||||
#include "wav_io.h"
|
||||
#include "fs.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
661
fs.c
Normal file
661
fs.c
Normal file
@@ -0,0 +1,661 @@
|
||||
#include "fs.h"
|
||||
#include "wav_io.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>
|
||||
|
||||
// ============================================================
|
||||
// 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;
|
||||
}
|
||||
21
fs.h
Normal file
21
fs.h
Normal file
@@ -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
|
||||
17
main.c
17
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;
|
||||
|
||||
17
makefile
17
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
|
||||
|
||||
17
tui.c
17
tui.c
@@ -745,8 +745,21 @@ 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 } };
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user