700 lines
28 KiB
C
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, ¶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;
|
|
}
|