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:
Loic Coenen
2026-05-02 22:53:59 +00:00
parent bee7f6d22b
commit 1487619cc2
7 changed files with 778 additions and 11 deletions

View File

@@ -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;

View File

@@ -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
View 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, &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;
}

21
fs.h Normal file
View 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
View File

@@ -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;

View File

@@ -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

21
tui.c
View File

@@ -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;