620 lines
23 KiB
C
620 lines
23 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include "../project/project_internal.h"
|
|
#include "../graph/graph_internal.h"
|
|
#include "xtc/xtc.h"
|
|
#include <float.h>
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
typedef struct {
|
|
FILE *file;
|
|
size_t bytes;
|
|
char line[1536];
|
|
char *tokens[12];
|
|
size_t count;
|
|
} reader;
|
|
|
|
/**
|
|
* @brief Reads and tokenizes one bounded printable-ASCII project record.
|
|
*
|
|
* @param input Reader holding the file and reusable line/token buffers.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
static xtc_result line(reader *input)
|
|
{
|
|
size_t used = 0U;
|
|
int byte;
|
|
char *cursor;
|
|
input->count = 0U;
|
|
while ((byte = fgetc(input->file)) != EOF) {
|
|
if (++input->bytes > 4194304U)
|
|
return XTC_ERR_FORMAT;
|
|
if (byte == '\n')
|
|
break;
|
|
if (byte < 32 || byte > 126 || used == sizeof(input->line) - 1U)
|
|
return XTC_ERR_FORMAT;
|
|
input->line[used++] = (char)byte;
|
|
}
|
|
if (byte == EOF)
|
|
return ferror(input->file) ? XTC_ERR_IO : XTC_ERR_FORMAT;
|
|
input->line[used] = '\0';
|
|
cursor = input->line;
|
|
while (*cursor != '\0') {
|
|
while (*cursor == ' ')
|
|
++cursor;
|
|
if (*cursor == '\0')
|
|
break;
|
|
if (input->count == 12U)
|
|
return XTC_ERR_FORMAT;
|
|
input->tokens[input->count++] = cursor;
|
|
while (*cursor != '\0' && *cursor != ' ')
|
|
++cursor;
|
|
if (*cursor == ' ')
|
|
*cursor++ = '\0';
|
|
}
|
|
return input->count == 0U ? XTC_ERR_FORMAT : XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses an unsigned decimal integer with overflow detection.
|
|
*
|
|
* @param text NUL-terminated encoded text.
|
|
* @param value Receives the decoded value.
|
|
* @return Nonzero for a valid decimal value, zero for empty, malformed, or overflowing input.
|
|
*/
|
|
static int number(const char *text, uint64_t *value)
|
|
{
|
|
uint64_t result = 0U;
|
|
const unsigned char *cursor = (const unsigned char *)text;
|
|
if (*cursor == '\0')
|
|
return 0;
|
|
while (*cursor != '\0') {
|
|
unsigned int digit;
|
|
if (*cursor < '0' || *cursor > '9')
|
|
return 0;
|
|
digit = (unsigned int)(*cursor++ - '0');
|
|
if (result > (UINT64_MAX - digit) / 10U)
|
|
return 0;
|
|
result = result * 10U + digit;
|
|
}
|
|
*value = result;
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* @brief Converts one ASCII hexadecimal digit to its numeric value.
|
|
*
|
|
* @param byte ASCII character to decode.
|
|
* @return Digit value from 0 to 15, or -1 for a non-hexadecimal character.
|
|
*/
|
|
static int hex_digit(char byte)
|
|
{
|
|
if (byte >= '0' && byte <= '9')
|
|
return byte - '0';
|
|
if (byte >= 'a' && byte <= 'f')
|
|
return byte - 'a' + 10;
|
|
if (byte >= 'A' && byte <= 'F')
|
|
return byte - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* @brief Decodes a hexadecimal project name and validates its UTF-8 contents.
|
|
*
|
|
* @param text NUL-terminated encoded text.
|
|
* @param name NUL-terminated name.
|
|
* @return Nonzero for a valid decoded name, zero for malformed or invalid input.
|
|
*/
|
|
static int decode_name(const char *text, char *name)
|
|
{
|
|
size_t length = strlen(text), i;
|
|
if (length == 0U || length % 2U != 0U || length > 126U)
|
|
return 0;
|
|
for (i = 0U; i < length / 2U; ++i) {
|
|
int high = hex_digit(text[i * 2U]), low = hex_digit(text[i * 2U + 1U]);
|
|
if (high < 0 || low < 0 || (high == 0 && low == 0))
|
|
return 0;
|
|
name[i] = (char)(high * 16 + low);
|
|
}
|
|
name[length / 2U] = '\0';
|
|
return xtc_project_valid_name(name);
|
|
}
|
|
|
|
/* Binary-safe nonempty hexadecimal strings, excluding embedded NUL. */
|
|
static int decode_bytes(const char *text, char *out, size_t capacity)
|
|
{
|
|
size_t n = strlen(text), i;
|
|
if (!n || n % 2 || n / 2 >= capacity) return 0;
|
|
for (i = 0; i < n/2; ++i) {
|
|
int hi = hex_digit(text[2*i]), lo = hex_digit(text[2*i+1]);
|
|
if (hi < 0 || lo < 0 || (!hi && !lo)) return 0;
|
|
out[i] = (char)(hi*16+lo);
|
|
}
|
|
out[n/2] = 0; return 1;
|
|
}
|
|
|
|
/**
|
|
* @brief Decodes eight hexadecimal IEEE-754 bits into a finite float.
|
|
*
|
|
* @param text NUL-terminated encoded text.
|
|
* @param value Receives the decoded value.
|
|
* @return Nonzero for a finite decoded float, zero for an unsupported representation or invalid input.
|
|
*/
|
|
static int decode_float(const char *text, float *value)
|
|
{
|
|
uint32_t bits = 0U;
|
|
size_t i;
|
|
if (strlen(text) != 8U ||
|
|
(sizeof(*value) != sizeof(bits) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128))
|
|
return 0;
|
|
for (i = 0U; i < 8U; ++i) {
|
|
int digit = hex_digit(text[i]);
|
|
if (digit < 0)
|
|
return 0;
|
|
bits = (bits << 4U) | (uint32_t)digit;
|
|
}
|
|
memcpy(value, &bits, sizeof(bits));
|
|
return isfinite(*value);
|
|
}
|
|
|
|
#define READ(tag, total) \
|
|
do { \
|
|
xtc_result read_result = line(input); \
|
|
if (read_result != XTC_OK) \
|
|
return read_result; \
|
|
if (input->count != (total) || strcmp(input->tokens[0], (tag)) != 0) \
|
|
return XTC_ERR_FORMAT; \
|
|
} while (0)
|
|
|
|
#define NUM(index, variable) \
|
|
do { \
|
|
if (!number(input->tokens[(index)], &(variable))) \
|
|
return XTC_ERR_FORMAT; \
|
|
} while (0)
|
|
|
|
/**
|
|
* @brief Parses versioned project records into a temporary project.
|
|
*
|
|
* @param input Reader holding the file and reusable line/token buffers.
|
|
* @param project Project model; borrowed for this call.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
static xtc_result read_project(reader *input, xtc_project *project)
|
|
{
|
|
uint64_t version, value, next_id, node_count, edge_count, format_version;
|
|
size_t i, j;
|
|
xtc_graph *graph = project->graph;
|
|
READ("XTC_PROJECT", 2U);
|
|
NUM(1U, version);
|
|
if (version < 1U || version > 6U)
|
|
return XTC_ERR_UNSUPPORTED;
|
|
format_version = version;
|
|
READ("APP", 4U);
|
|
for (i = 1U; i < 4U; ++i) {
|
|
NUM(i, value);
|
|
if (value > UINT_MAX)
|
|
return XTC_ERR_FORMAT;
|
|
}
|
|
READ("TEMPO", 2U);
|
|
NUM(1U, value);
|
|
if (value < 20000U || value > 300000U)
|
|
return XTC_ERR_FORMAT;
|
|
project->tempo = (unsigned int)value;
|
|
READ("IDS", 3U);
|
|
NUM(1U, project->next_track_id);
|
|
NUM(2U, project->next_clip_id);
|
|
READ("GRAPH", 5U);
|
|
NUM(1U, value);
|
|
NUM(2U, next_id);
|
|
NUM(3U, node_count);
|
|
NUM(4U, edge_count);
|
|
if (value > 1U || next_id > SIZE_MAX || node_count > 64U || edge_count > node_count * node_count)
|
|
return XTC_ERR_FORMAT;
|
|
graph->require_output_nodes = (int)value;
|
|
graph->next_id = (size_t)next_id;
|
|
for (i = 0U; i < (size_t)node_count; ++i) {
|
|
xtc_node node = {0};
|
|
READ("NODE", 4U);
|
|
NUM(1U, value);
|
|
NUM(2U, version);
|
|
if (value >= next_id || value >= SIZE_MAX || version < XTC_NODE_GAIN ||
|
|
version > (format_version == 1U ? XTC_NODE_OUTPUT
|
|
: format_version == 2U ? XTC_NODE_AUDIO
|
|
: format_version < 5U ? XTC_NODE_INSTRUMENT
|
|
: XTC_NODE_PLUGIN) ||
|
|
!decode_float(input->tokens[3], &node.value))
|
|
return XTC_ERR_FORMAT;
|
|
if (version == XTC_NODE_NATIVE) return XTC_ERR_UNSUPPORTED;
|
|
node.id = (size_t)value;
|
|
node.type = (xtc_node_type)version;
|
|
if (node.type == XTC_NODE_OSCILLATOR && node.value < 0.0f)
|
|
return XTC_ERR_FORMAT;
|
|
if ((node.type == XTC_NODE_METER || node.type == XTC_NODE_OUTPUT || node.type == XTC_NODE_AUDIO ||
|
|
node.type == XTC_NODE_INSTRUMENT) &&
|
|
node.value != 0.0f)
|
|
return XTC_ERR_FORMAT;
|
|
if (node.type == XTC_NODE_PLUGIN) {
|
|
xtc_plugin_spec *p = &node.plugin;
|
|
size_t k;
|
|
if (node.value != 0) return XTC_ERR_FORMAT;
|
|
READ("PLUGIN",6U);
|
|
NUM(1U,value); if (value > UINT32_MAX) return XTC_ERR_FORMAT; p->unique_id = (unsigned int)value;
|
|
NUM(2U,value); if (value >= 1024) return XTC_ERR_FORMAT; p->index = (unsigned int)value;
|
|
NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; p->inputs = (unsigned int)value;
|
|
NUM(4U,value); if (value > XTC_PLUGIN_CONTROLS) return XTC_ERR_FORMAT; p->control_count = (unsigned int)value;
|
|
if (!decode_bytes(input->tokens[5],p->label,sizeof(p->label))) return XTC_ERR_FORMAT;
|
|
READ("MODULE",2U);
|
|
if (!decode_bytes(input->tokens[1],p->path,sizeof(p->path))) return XTC_ERR_FORMAT;
|
|
for (k = 0; k < p->control_count; ++k) {
|
|
READ("CONTROL",3U); NUM(1U,value);
|
|
if (value >= 64 || !decode_float(input->tokens[2],p->values+k)) return XTC_ERR_FORMAT;
|
|
p->ports[k] = (unsigned int)value;
|
|
}
|
|
p->tempo = project->tempo;
|
|
if (format_version >= 6) {
|
|
READ("PLUGIN_AUTOMATION",2U); NUM(1U,value);
|
|
if (value > XTC_PLUGIN_POINTS) return XTC_ERR_FORMAT;
|
|
p->point_count = (unsigned int)value;
|
|
for (k = 0; k < p->point_count; ++k) {
|
|
xtc_plugin_point *a = p->points+k;
|
|
READ("PARAM",5U); NUM(1U,a->tick); NUM(2U,value);
|
|
if (value >= 64) return XTC_ERR_FORMAT;
|
|
a->port = (unsigned int)value;
|
|
NUM(3U,value); if (value > 1) return XTC_ERR_FORMAT; a->step = (unsigned int)value;
|
|
if (!decode_float(input->tokens[4],&a->value)) return XTC_ERR_FORMAT;
|
|
}
|
|
}
|
|
if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT;
|
|
}
|
|
for (j = 0U; j < i; ++j)
|
|
if (graph->nodes[j].id == node.id)
|
|
return XTC_ERR_FORMAT;
|
|
graph->nodes[graph->node_count++] = node;
|
|
}
|
|
for (i = 0U; i < (size_t)edge_count; ++i) {
|
|
uint64_t source, dest;
|
|
READ("EDGE", 3U);
|
|
NUM(1U, source);
|
|
NUM(2U, dest);
|
|
if (source >= SIZE_MAX || dest >= SIZE_MAX || xtc_graph_connect(graph, (size_t)source, (size_t)dest) != XTC_OK)
|
|
return XTC_ERR_FORMAT;
|
|
}
|
|
READ("TRACKS", 2U);
|
|
NUM(1U, value);
|
|
if (value > XTC_PROJECT_MAX_TRACKS)
|
|
return XTC_ERR_FORMAT;
|
|
project->track_count = (size_t)value;
|
|
for (i = 0U; i < project->track_count; ++i) {
|
|
READ("TRACK", 3U);
|
|
NUM(1U, project->tracks[i].id);
|
|
if (!decode_name(input->tokens[2], project->tracks[i].name))
|
|
return XTC_ERR_FORMAT;
|
|
project->tracks[i].gain = 1;
|
|
if (format_version >= 4U) {
|
|
READ("MIX", 5U);
|
|
if (!decode_float(input->tokens[1], &project->tracks[i].gain) ||
|
|
!decode_float(input->tokens[2], &project->tracks[i].pan))
|
|
return XTC_ERR_FORMAT;
|
|
NUM(3U, value);
|
|
if (value > 1)
|
|
return XTC_ERR_FORMAT;
|
|
project->tracks[i].mute = (int)value;
|
|
NUM(4U, value);
|
|
if (value > 1)
|
|
return XTC_ERR_FORMAT;
|
|
project->tracks[i].solo = (int)value;
|
|
}
|
|
}
|
|
READ("CLIPS", 2U);
|
|
NUM(1U, value);
|
|
if (value > XTC_PROJECT_MAX_CLIPS)
|
|
return XTC_ERR_FORMAT;
|
|
project->clip_count = (size_t)value;
|
|
for (i = 0U; i < project->clip_count; ++i) {
|
|
xtc_clip_info *clip = project->clips + i;
|
|
READ("CLIP", 8U);
|
|
NUM(1U, clip->id);
|
|
NUM(2U, clip->track_id);
|
|
NUM(3U, clip->start_tick);
|
|
NUM(4U, clip->length_tick);
|
|
NUM(5U, clip->offset_tick);
|
|
NUM(6U, clip->source_length_tick);
|
|
if (!decode_name(input->tokens[7], clip->name))
|
|
return XTC_ERR_FORMAT;
|
|
if (format_version >= 2U) {
|
|
size_t k, length;
|
|
READ("ASSET", 2U);
|
|
if (strcmp(input->tokens[1], "-")) {
|
|
length = strlen(input->tokens[1]);
|
|
if (!length || length % 2 || length >= XTC_ASSET_PATH_BYTES * 2U)
|
|
return XTC_ERR_FORMAT;
|
|
for (k = 0; k < length / 2; ++k) {
|
|
int high = hex_digit(input->tokens[1][k * 2]), low = hex_digit(input->tokens[1][k * 2 + 1]);
|
|
if (high < 0 || low < 0 || (high == 0 && low == 0))
|
|
return XTC_ERR_FORMAT;
|
|
clip->asset_path[k] = (char)(high * 16 + low);
|
|
}
|
|
clip->asset_path[length / 2] = 0;
|
|
}
|
|
}
|
|
if (format_version >= 3U) {
|
|
READ("MIDI", 2U);
|
|
NUM(1U, value);
|
|
if (value > 1)
|
|
return XTC_ERR_FORMAT;
|
|
clip->midi = (int)value;
|
|
}
|
|
}
|
|
if (format_version >= 3U) {
|
|
READ("NOTES", 3U);
|
|
NUM(1U, project->next_note_id);
|
|
NUM(2U, value);
|
|
if (value > XTC_PROJECT_MAX_NOTES)
|
|
return XTC_ERR_FORMAT;
|
|
project->note_count = (size_t)value;
|
|
for (i = 0; i < project->note_count; ++i) {
|
|
xtc_note_info *n = project->notes + i;
|
|
READ("NOTE", 8U);
|
|
NUM(1U, n->id);
|
|
NUM(2U, n->clip_id);
|
|
NUM(3U, n->start_tick);
|
|
NUM(4U, n->length_tick);
|
|
NUM(5U, value);
|
|
if (value > 127)
|
|
return XTC_ERR_FORMAT;
|
|
n->pitch = (unsigned int)value;
|
|
NUM(6U, value);
|
|
if (!value || value > 127)
|
|
return XTC_ERR_FORMAT;
|
|
n->velocity = (unsigned int)value;
|
|
NUM(7U, value);
|
|
if (value > 15)
|
|
return XTC_ERR_FORMAT;
|
|
n->channel = (unsigned int)value;
|
|
}
|
|
}
|
|
if (format_version >= 4U) {
|
|
READ("AUTOMATION", 2U);
|
|
NUM(1U, value);
|
|
if (value > XTC_MAX_AUTOMATION_POINTS)
|
|
return XTC_ERR_FORMAT;
|
|
project->automation_count = (size_t)value;
|
|
for (i = 0; i < project->automation_count; ++i) {
|
|
xtc_automation_point *a = project->automation + i;
|
|
READ("POINT", 5U);
|
|
NUM(1U, a->track_id);
|
|
NUM(2U, value);
|
|
if (value > 1)
|
|
return XTC_ERR_FORMAT;
|
|
a->parameter = (xtc_automation_parameter)value;
|
|
NUM(3U, a->tick);
|
|
if (!decode_float(input->tokens[4], &a->value))
|
|
return XTC_ERR_FORMAT;
|
|
}
|
|
}
|
|
READ("END", 1U);
|
|
if (fgetc(input->file) != EOF)
|
|
return XTC_ERR_FORMAT;
|
|
if (ferror(input->file))
|
|
return XTC_ERR_IO;
|
|
return xtc_project_validate(project);
|
|
}
|
|
|
|
/**
|
|
* @brief Writes a name as hexadecimal bytes without a terminator.
|
|
*
|
|
* @param file Open file stream at the required read or write position.
|
|
* @param name NUL-terminated name.
|
|
* @return Nonzero if every encoded byte was written, zero on a write failure.
|
|
*/
|
|
static int write_name(FILE *file, const char *name)
|
|
{
|
|
const unsigned char *cursor = (const unsigned char *)name;
|
|
while (*cursor != '\0')
|
|
if (fprintf(file, "%02x", (unsigned int)*cursor++) < 0)
|
|
return 0;
|
|
return fputc('\n', file) != EOF;
|
|
}
|
|
|
|
/**
|
|
* @brief Serializes project metadata and graph records in the current format.
|
|
*
|
|
* @param file Open file stream at the required read or write position.
|
|
* @param project Project model; borrowed for this call.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
static xtc_result write_project(FILE *file, const xtc_project *project)
|
|
{
|
|
const xtc_graph *graph = project->graph;
|
|
size_t i;
|
|
for (i = 0; i < graph->node_count; ++i)
|
|
if (graph->nodes[i].type == XTC_NODE_NATIVE)
|
|
return XTC_ERR_UNSUPPORTED;
|
|
if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)
|
|
return XTC_ERR_UNSUPPORTED;
|
|
if (fprintf(file, "XTC_PROJECT 6\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
|
|
XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id,
|
|
project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count,
|
|
graph->edge_count) < 0)
|
|
return XTC_ERR_IO;
|
|
for (i = 0U; i < graph->node_count; ++i) {
|
|
uint32_t bits;
|
|
memcpy(&bits, &graph->nodes[i].value, sizeof(bits));
|
|
if (fprintf(file, "NODE %zu %u %08" PRIx32 "\n", graph->nodes[i].id, (unsigned int)graph->nodes[i].type, bits) <
|
|
0)
|
|
return XTC_ERR_IO;
|
|
if (graph->nodes[i].type == XTC_NODE_PLUGIN) {
|
|
const xtc_plugin_spec *p = &graph->nodes[i].plugin;
|
|
size_t k;
|
|
if (xtc_plugin_spec_validate(p) != XTC_OK) return XTC_ERR_FORMAT;
|
|
if (fprintf(file,"PLUGIN %u %u %u %u ",p->unique_id,p->index,p->inputs,p->control_count) < 0 ||
|
|
!write_name(file,p->label) || fputs("MODULE ",file) == EOF || !write_name(file,p->path)) return XTC_ERR_IO;
|
|
for (k = 0; k < p->control_count; ++k) {
|
|
uint32_t value; memcpy(&value,p->values+k,4);
|
|
if (fprintf(file,"CONTROL %u %08" PRIx32 "\n",p->ports[k],value) < 0) return XTC_ERR_IO;
|
|
}
|
|
if (fprintf(file,"PLUGIN_AUTOMATION %u\n",p->point_count) < 0) return XTC_ERR_IO;
|
|
for (k = 0; k < p->point_count; ++k) {
|
|
uint32_t value; memcpy(&value,&p->points[k].value,4);
|
|
if (fprintf(file,"PARAM %" PRIu64 " %u %u %08" PRIx32 "\n",p->points[k].tick,p->points[k].port,
|
|
p->points[k].step,value) < 0) return XTC_ERR_IO;
|
|
}
|
|
}
|
|
}
|
|
for (i = 0U; i < graph->edge_count; ++i)
|
|
if (fprintf(file, "EDGE %zu %zu\n", graph->nodes[graph->edges[i].source].id,
|
|
graph->nodes[graph->edges[i].destination].id) < 0)
|
|
return XTC_ERR_IO;
|
|
if (fprintf(file, "TRACKS %zu\n", project->track_count) < 0)
|
|
return XTC_ERR_IO;
|
|
for (i = 0U; i < project->track_count; ++i) {
|
|
uint32_t gain, pan;
|
|
const xtc_track_info *t = project->tracks + i;
|
|
memcpy(&gain, &t->gain, sizeof(gain));
|
|
memcpy(&pan, &t->pan, sizeof(pan));
|
|
if (fprintf(file, "TRACK %" PRIu64 " ", t->id) < 0 || !write_name(file, t->name) ||
|
|
fprintf(file, "MIX %08" PRIx32 " %08" PRIx32 " %d %d\n", gain, pan, t->mute, t->solo) < 0)
|
|
return XTC_ERR_IO;
|
|
}
|
|
if (fprintf(file, "CLIPS %zu\n", project->clip_count) < 0)
|
|
return XTC_ERR_IO;
|
|
for (i = 0U; i < project->clip_count; ++i) {
|
|
const xtc_clip_info *clip = project->clips + i;
|
|
if (fprintf(file, "CLIP %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " ", clip->id,
|
|
clip->track_id, clip->start_tick, clip->length_tick, clip->offset_tick,
|
|
clip->source_length_tick) < 0 ||
|
|
!write_name(file, clip->name))
|
|
return XTC_ERR_IO;
|
|
if (fputs("ASSET ", file) == EOF)
|
|
return XTC_ERR_IO;
|
|
if (clip->asset_path[0]) {
|
|
if (!write_name(file, clip->asset_path))
|
|
return XTC_ERR_IO;
|
|
} else if (fputs("-\n", file) == EOF)
|
|
return XTC_ERR_IO;
|
|
if (fprintf(file, "MIDI %d\n", clip->midi) < 0)
|
|
return XTC_ERR_IO;
|
|
}
|
|
if (fprintf(file, "NOTES %" PRIu64 " %zu\n", project->next_note_id, project->note_count) < 0)
|
|
return XTC_ERR_IO;
|
|
for (i = 0; i < project->note_count; ++i) {
|
|
const xtc_note_info *n = project->notes + i;
|
|
if (fprintf(file, "NOTE %" PRIu64 " %" PRIu64 " %" PRIu64 " %" PRIu64 " %u %u %u\n", n->id, n->clip_id,
|
|
n->start_tick, n->length_tick, n->pitch, n->velocity, n->channel) < 0)
|
|
return XTC_ERR_IO;
|
|
}
|
|
if (fprintf(file, "AUTOMATION %zu\n", project->automation_count) < 0)
|
|
return XTC_ERR_IO;
|
|
for (i = 0; i < project->automation_count; ++i) {
|
|
uint32_t bits;
|
|
const xtc_automation_point *a = project->automation + i;
|
|
memcpy(&bits, &a->value, sizeof(bits));
|
|
if (fprintf(file, "POINT %" PRIu64 " %u %" PRIu64 " %08" PRIx32 "\n", a->track_id, (unsigned int)a->parameter,
|
|
a->tick, bits) < 0)
|
|
return XTC_ERR_IO;
|
|
}
|
|
return fputs("END\n", file) == EOF ? XTC_ERR_IO : XTC_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Measures a file path up to the supported 4096-byte bound.
|
|
*
|
|
* @param path NUL-terminated file or asset path.
|
|
* @return Path length, capped at 4096; zero for NULL.
|
|
*/
|
|
static size_t path_length(const char *path)
|
|
{
|
|
size_t size = 0U;
|
|
if (path == NULL)
|
|
return 0U;
|
|
while (size < 4096U && path[size] != '\0')
|
|
++size;
|
|
return size == 4096U ? 0U : size;
|
|
}
|
|
|
|
/**
|
|
* @brief Loads and validates a project before transactionally replacing the current state.
|
|
*
|
|
* @param project Project model; borrowed for this call.
|
|
* @param path NUL-terminated file or asset path.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_project_load(xtc_project *project, const char *path)
|
|
{
|
|
xtc_project *candidate = NULL;
|
|
reader input = {0};
|
|
xtc_result result;
|
|
if (project == NULL || path_length(path) == 0U)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
input.file = fopen(path, "rb");
|
|
if (input.file == NULL)
|
|
return XTC_ERR_IO;
|
|
result = xtc_project_create(&candidate);
|
|
if (result == XTC_OK)
|
|
result = read_project(&input, candidate);
|
|
if (fclose(input.file) != 0 && result == XTC_OK)
|
|
result = XTC_ERR_IO;
|
|
if (result == XTC_OK) {
|
|
xtc_graph *old = project->graph;
|
|
*project = *candidate;
|
|
candidate->graph = old;
|
|
}
|
|
xtc_project_destroy(candidate);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @brief Saves a versioned project through a synced temporary file and atomic rename.
|
|
*
|
|
* @param project Project model; borrowed for this call.
|
|
* @param path NUL-terminated file or asset path.
|
|
* @return XTC_OK on success, or an error code describing the failure.
|
|
*/
|
|
xtc_result xtc_project_save(const xtc_project *project, const char *path)
|
|
{
|
|
size_t length = path_length(path);
|
|
char *temporary;
|
|
FILE *file;
|
|
int descriptor;
|
|
xtc_result result;
|
|
if (project == NULL || length == 0U)
|
|
return XTC_ERR_INVALID_ARGUMENT;
|
|
result = xtc_project_validate(project);
|
|
if (result != XTC_OK)
|
|
return result;
|
|
temporary = malloc(length + 12U);
|
|
if (temporary == NULL)
|
|
return XTC_ERR_OUT_OF_MEMORY;
|
|
memcpy(temporary, path, length);
|
|
memcpy(temporary + length, ".tmp.XXXXXX", 12U);
|
|
descriptor = mkstemp(temporary);
|
|
if (descriptor < 0) {
|
|
free(temporary);
|
|
return XTC_ERR_IO;
|
|
}
|
|
file = fdopen(descriptor, "wb");
|
|
if (file == NULL) {
|
|
(void)close(descriptor);
|
|
(void)unlink(temporary);
|
|
free(temporary);
|
|
return XTC_ERR_IO;
|
|
}
|
|
result = write_project(file, project);
|
|
if (result == XTC_OK && fflush(file) != 0)
|
|
result = XTC_ERR_IO;
|
|
if (result == XTC_OK && fsync(descriptor) != 0)
|
|
result = XTC_ERR_IO;
|
|
if (fclose(file) != 0 && result == XTC_OK)
|
|
result = XTC_ERR_IO;
|
|
if (result == XTC_OK && rename(temporary, path) != 0)
|
|
result = XTC_ERR_IO;
|
|
if (result != XTC_OK)
|
|
(void)unlink(temporary);
|
|
free(temporary);
|
|
return result;
|
|
}
|