2026-10-27 06:31:59 +01:00
# define _XOPEN_SOURCE 700
2026-10-27 00:10:45 +01:00
# 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>
2026-10-27 06:31:59 +01:00
# include <fcntl.h>
2026-10-27 16:58:26 +01:00
# include <sys/stat.h>
2026-10-27 00:10:45 +01:00
2026-10-27 01:37:21 +01:00
typedef struct {
FILE * file ;
2026-10-27 16:58:26 +01:00
size_t bytes , line_number , record_line ;
uint64_t version ;
char expected [ 32 ] ;
2026-10-27 01:37:21 +01:00
char line [ 1536 ] ;
char * tokens [ 12 ] ;
size_t count ;
} reader ;
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static xtc_result line ( reader * input )
{
size_t used = 0U ;
int byte ;
char * cursor ;
input - > count = 0U ;
2026-10-27 16:58:26 +01:00
input - > record_line = + + input - > line_number ;
2026-10-27 00:10:45 +01:00
while ( ( byte = fgetc ( input - > file ) ) ! = EOF ) {
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
input - > line [ used + + ] = ( char ) byte ;
}
2026-10-27 01:37:21 +01:00
if ( byte = = EOF )
return ferror ( input - > file ) ? XTC_ERR_IO : XTC_ERR_FORMAT ;
input - > line [ used ] = ' \0 ' ;
cursor = input - > line ;
2026-10-27 00:10:45 +01:00
while ( * cursor ! = ' \0 ' ) {
2026-10-27 01:37:21 +01:00
while ( * cursor = = ' ' )
+ + cursor ;
if ( * cursor = = ' \0 ' )
break ;
if ( input - > count = = 12U )
return XTC_ERR_FORMAT ;
2026-10-27 00:10:45 +01:00
input - > tokens [ input - > count + + ] = cursor ;
2026-10-27 01:37:21 +01:00
while ( * cursor ! = ' \0 ' & & * cursor ! = ' ' )
+ + cursor ;
if ( * cursor = = ' ' )
* cursor + + = ' \0 ' ;
2026-10-27 00:10:45 +01:00
}
return input - > count = = 0U ? XTC_ERR_FORMAT : XTC_OK ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static int number ( const char * text , uint64_t * value )
{
uint64_t result = 0U ;
const unsigned char * cursor = ( const unsigned char * ) text ;
2026-10-27 01:37:21 +01:00
if ( * cursor = = ' \0 ' )
return 0 ;
2026-10-27 00:10:45 +01:00
while ( * cursor ! = ' \0 ' ) {
unsigned int digit ;
2026-10-27 01:37:21 +01:00
if ( * cursor < ' 0 ' | | * cursor > ' 9 ' )
return 0 ;
2026-10-27 00:10:45 +01:00
digit = ( unsigned int ) ( * cursor + + - ' 0 ' ) ;
2026-10-27 01:37:21 +01:00
if ( result > ( UINT64_MAX - digit ) / 10U )
return 0 ;
2026-10-27 00:10:45 +01:00
result = result * 10U + digit ;
}
2026-10-27 01:37:21 +01:00
* value = result ;
return 1 ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static int hex_digit ( char byte )
{
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
return - 1 ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static int decode_name ( const char * text , char * name )
{
size_t length = strlen ( text ) , i ;
2026-10-27 01:37:21 +01:00
if ( length = = 0U | | length % 2U ! = 0U | | length > 126U )
return 0 ;
2026-10-27 00:10:45 +01:00
for ( i = 0U ; i < length / 2U ; + + i ) {
int high = hex_digit ( text [ i * 2U ] ) , low = hex_digit ( text [ i * 2U + 1U ] ) ;
2026-10-27 01:37:21 +01:00
if ( high < 0 | | low < 0 | | ( high = = 0 & & low = = 0 ) )
return 0 ;
2026-10-27 00:10:45 +01:00
name [ i ] = ( char ) ( high * 16 + low ) ;
}
2026-10-27 01:37:21 +01:00
name [ length / 2U ] = ' \0 ' ;
return xtc_project_valid_name ( name ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 04:00:27 +01:00
/* 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 ;
}
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static int decode_float ( const char * text , float * value )
{
uint32_t bits = 0U ;
size_t i ;
2026-10-27 01:37:21 +01:00
if ( strlen ( text ) ! = 8U | |
( sizeof ( * value ) ! = sizeof ( bits ) | | FLT_RADIX ! = 2 | | FLT_MANT_DIG ! = 24 | | FLT_MAX_EXP ! = 128 ) )
return 0 ;
2026-10-27 00:10:45 +01:00
for ( i = 0U ; i < 8U ; + + i ) {
int digit = hex_digit ( text [ i ] ) ;
2026-10-27 01:37:21 +01:00
if ( digit < 0 )
return 0 ;
2026-10-27 00:10:45 +01:00
bits = ( bits < < 4U ) | ( uint32_t ) digit ;
}
2026-10-27 01:37:21 +01:00
memcpy ( value , & bits , sizeof ( bits ) ) ;
return isfinite ( * value ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
# define READ(tag, total) \
do { \
2026-10-27 16:58:26 +01:00
xtc_result read_result ; \
snprintf ( input - > expected , sizeof ( input - > expected ) , " %s " , ( tag ) ) ; \
read_result = line ( input ) ; \
2026-10-27 01:37:21 +01:00
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 )
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 06:31:59 +01:00
static xtc_result read_project ( reader * input , xtc_project * project , const char * directory )
2026-10-27 00:10:45 +01:00
{
2026-10-27 00:31:03 +01:00
uint64_t version , value , next_id , node_count , edge_count , format_version ;
2026-10-27 00:10:45 +01:00
size_t i , j ;
xtc_graph * graph = project - > graph ;
2026-10-27 01:37:21 +01:00
READ ( " XTC_PROJECT " , 2U ) ;
NUM ( 1U , version ) ;
2026-10-27 16:58:26 +01:00
input - > version = version ;
2026-10-27 06:44:22 +01:00
if ( version < 1U | | version > XTC_PROJECT_FORMAT_VERSION )
2026-10-27 01:37:21 +01:00
return XTC_ERR_UNSUPPORTED ;
2026-10-27 00:31:03 +01:00
format_version = version ;
2026-10-27 00:10:45 +01:00
READ ( " APP " , 4U ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
project - > tempo = ( unsigned int ) value ;
2026-10-27 01:37:21 +01:00
READ ( " IDS " , 3U ) ;
NUM ( 1U , project - > next_track_id ) ;
NUM ( 2U , project - > next_clip_id ) ;
2026-10-27 00:10:45 +01:00
READ ( " GRAPH " , 5U ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
for ( i = 0U ; i < ( size_t ) node_count ; + + i ) {
2026-10-27 01:21:04 +01:00
xtc_node node = { 0 } ;
2026-10-27 01:37:21 +01:00
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
2026-10-27 04:00:27 +01:00
: format_version < 5U ? XTC_NODE_INSTRUMENT
: XTC_NODE_PLUGIN ) | |
2026-10-27 01:37:21 +01:00
! decode_float ( input - > tokens [ 3 ] , & node . value ) )
return XTC_ERR_FORMAT ;
2026-10-27 04:00:27 +01:00
if ( version = = XTC_NODE_NATIVE ) return XTC_ERR_UNSUPPORTED ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 04:00:27 +01:00
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 ;
}
2026-10-27 06:25:30 +01:00
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 ;
}
}
2026-10-27 04:00:27 +01:00
if ( xtc_plugin_spec_validate ( p ) ! = XTC_OK ) return XTC_ERR_FORMAT ;
}
2026-10-27 01:37:21 +01:00
for ( j = 0U ; j < i ; + + j )
if ( graph - > nodes [ j ] . id = = node . id )
return XTC_ERR_FORMAT ;
2026-10-27 00:10:45 +01:00
graph - > nodes [ graph - > node_count + + ] = node ;
}
for ( i = 0U ; i < ( size_t ) edge_count ; + + i ) {
uint64_t source , dest ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
READ ( " TRACKS " , 2U ) ;
NUM ( 1U , value ) ;
if ( value > XTC_PROJECT_MAX_TRACKS )
return XTC_ERR_FORMAT ;
2026-10-27 00:10:45 +01:00
project - > track_count = ( size_t ) value ;
for ( i = 0U ; i < project - > track_count ; + + i ) {
2026-10-27 01:37:21 +01:00
READ ( " TRACK " , 3U ) ;
NUM ( 1U , project - > tracks [ i ] . id ) ;
if ( ! decode_name ( input - > tokens [ 2 ] , project - > tracks [ i ] . name ) )
return XTC_ERR_FORMAT ;
2026-10-27 01:06:57 +01:00
project - > tracks [ i ] . gain = 1 ;
if ( format_version > = 4U ) {
READ ( " MIX " , 5U ) ;
if ( ! decode_float ( input - > tokens [ 1 ] , & project - > tracks [ i ] . gain ) | |
2026-10-27 01:37:21 +01:00
! 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 ;
2026-10-27 01:06:57 +01:00
}
2026-10-27 16:15:34 +01:00
project - > tracks [ i ] . bus_id = UINT64_MAX ;
if ( format_version > = 9U ) {
READ ( " ROUTE " , 3U ) ;
NUM ( 1U , value ) ;
if ( value > 1 ) return XTC_ERR_FORMAT ;
project - > tracks [ i ] . is_bus = ( int ) value ;
NUM ( 2U , project - > tracks [ i ] . bus_id ) ;
}
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
READ ( " CLIPS " , 2U ) ;
NUM ( 1U , value ) ;
if ( value > XTC_PROJECT_MAX_CLIPS )
return XTC_ERR_FORMAT ;
2026-10-27 00:10:45 +01:00
project - > clip_count = ( size_t ) value ;
for ( i = 0U ; i < project - > clip_count ; + + i ) {
xtc_clip_info * clip = project - > clips + i ;
READ ( " CLIP " , 8U ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:53:41 +01:00
if ( format_version > = 2U ) {
2026-10-27 00:31:03 +01:00
size_t k , length ;
READ ( " ASSET " , 2U ) ;
2026-10-27 00:53:41 +01:00
if ( strcmp ( input - > tokens [ 1 ] , " - " ) ) {
length = strlen ( input - > tokens [ 1 ] ) ;
2026-10-27 01:37:21 +01:00
if ( ! length | | length % 2 | | length > = XTC_ASSET_PATH_BYTES * 2U )
return XTC_ERR_FORMAT ;
2026-10-27 00:53:41 +01:00
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 ] ) ;
2026-10-27 01:37:21 +01:00
if ( high < 0 | | low < 0 | | ( high = = 0 & & low = = 0 ) )
return XTC_ERR_FORMAT ;
2026-10-27 00:53:41 +01:00
clip - > asset_path [ k ] = ( char ) ( high * 16 + low ) ;
}
clip - > asset_path [ length / 2 ] = 0 ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 00:53:41 +01:00
}
if ( format_version > = 3U ) {
2026-10-27 01:37:21 +01:00
READ ( " MIDI " , 2U ) ;
NUM ( 1U , value ) ;
if ( value > 1 )
return XTC_ERR_FORMAT ;
2026-10-27 00:53:41 +01:00
clip - > midi = ( int ) value ;
}
2026-10-27 15:46:41 +01:00
clip - > gain = 1.0f ;
if ( format_version > = 8U ) {
READ ( " AUDIO " , 4U ) ;
if ( ! decode_float ( input - > tokens [ 1 ] , & clip - > gain ) ) return XTC_ERR_FORMAT ;
NUM ( 2U , clip - > fade_in_tick ) ;
NUM ( 3U , clip - > fade_out_tick ) ;
}
2026-10-27 00:53:41 +01:00
}
if ( format_version > = 3U ) {
2026-10-27 01:37:21 +01:00
READ ( " NOTES " , 3U ) ;
NUM ( 1U , project - > next_note_id ) ;
NUM ( 2U , value ) ;
if ( value > XTC_PROJECT_MAX_NOTES )
return XTC_ERR_FORMAT ;
2026-10-27 00:53:41 +01:00
project - > note_count = ( size_t ) value ;
for ( i = 0 ; i < project - > note_count ; + + i ) {
xtc_note_info * n = project - > notes + i ;
READ ( " NOTE " , 8U ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:31:03 +01:00
}
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:06:57 +01:00
if ( format_version > = 4U ) {
2026-10-27 01:37:21 +01:00
READ ( " AUTOMATION " , 2U ) ;
NUM ( 1U , value ) ;
if ( value > XTC_MAX_AUTOMATION_POINTS )
return XTC_ERR_FORMAT ;
2026-10-27 01:06:57 +01:00
project - > automation_count = ( size_t ) value ;
for ( i = 0 ; i < project - > automation_count ; + + i ) {
xtc_automation_point * a = project - > automation + i ;
2026-10-27 16:37:59 +01:00
READ ( " POINT " , format_version > = 10U ? 6U : 5U ) ;
2026-10-27 01:37:21 +01:00
NUM ( 1U , a - > track_id ) ;
NUM ( 2U , value ) ;
if ( value > 1 )
return XTC_ERR_FORMAT ;
2026-10-27 01:06:57 +01:00
a - > parameter = ( xtc_automation_parameter ) value ;
NUM ( 3U , a - > tick ) ;
2026-10-27 01:37:21 +01:00
if ( ! decode_float ( input - > tokens [ 4 ] , & a - > value ) )
return XTC_ERR_FORMAT ;
2026-10-27 16:37:59 +01:00
if ( format_version > = 10U ) {
NUM ( 5U , value ) ; if ( value > 1 ) return XTC_ERR_FORMAT ;
a - > step = ( unsigned int ) value ;
}
}
}
if ( format_version > = 10U ) {
READ ( " CONTROL_MAPS " , 2U ) ; NUM ( 1U , value ) ;
if ( value ! = 16 ) return XTC_ERR_FORMAT ;
for ( i = 0 ; i < 16 ; + + i ) {
READ ( " MAP " , 5U ) ; NUM ( 1U , value ) ;
if ( value ! = i ) return XTC_ERR_FORMAT ;
for ( j = 0 ; j < 3 ; + + j ) {
NUM ( j + 2 , value ) ; if ( value > 128 ) return XTC_ERR_FORMAT ;
project - > control_map [ i ] [ j ] = ( unsigned char ) value ;
}
2026-10-27 01:06:57 +01:00
}
}
2026-10-27 00:10:45 +01:00
READ ( " END " , 1U ) ;
2026-10-27 01:37:21 +01:00
if ( fgetc ( input - > file ) ! = EOF )
return XTC_ERR_FORMAT ;
if ( ferror ( input - > file ) )
return XTC_ERR_IO ;
2026-10-27 16:58:26 +01:00
input - > record_line = 0 ;
snprintf ( input - > expected , sizeof ( input - > expected ) , " %s " , " ASSET_PATH " ) ;
2026-10-27 06:31:59 +01:00
if ( format_version > = 7 ) for ( i = 0 ; i < project - > clip_count ; + + i ) {
char * asset = project - > clips [ i ] . asset_path ;
if ( asset [ 0 ] & & asset [ 0 ] ! = ' / ' ) {
size_t root = strlen ( directory ) , length = strlen ( asset ) ;
if ( root + length + 2 > XTC_ASSET_PATH_BYTES ) return XTC_ERR_CAPACITY ;
memmove ( asset + root + 1 , asset , length + 1 ) ;
memcpy ( asset , directory , root ) ; asset [ root ] = ' / ' ;
}
}
2026-10-27 16:58:26 +01:00
snprintf ( input - > expected , sizeof ( input - > expected ) , " %s " , " MODEL " ) ;
2026-10-27 00:10:45 +01:00
return xtc_project_validate ( project ) ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static int write_name ( FILE * file , const char * name )
{
const unsigned char * cursor = ( const unsigned char * ) name ;
2026-10-27 01:37:21 +01:00
while ( * cursor ! = ' \0 ' )
if ( fprintf ( file , " %02x " , ( unsigned int ) * cursor + + ) < 0 )
return 0 ;
2026-10-27 00:10:45 +01:00
return fputc ( ' \n ' , file ) ! = EOF ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 06:31:59 +01:00
static xtc_result write_project ( FILE * file , const xtc_project * project , const char * directory )
2026-10-27 00:10:45 +01:00
{
const xtc_graph * graph = project - > graph ;
size_t i ;
2026-10-27 01:21:04 +01:00
for ( i = 0 ; i < graph - > node_count ; + + i )
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 06:44:22 +01:00
if ( fprintf ( file , " XTC_PROJECT %u \n APP %d %d %d \n TEMPO %u \n IDS % " PRIu64 " % " PRIu64 " \n GRAPH %d %zu %zu %zu \n " ,
XTC_PROJECT_FORMAT_VERSION , XTC_VERSION_MAJOR , XTC_VERSION_MINOR , XTC_VERSION_PATCH , project - > tempo , project - > next_track_id ,
2026-10-27 01:37:21 +01:00
project - > next_clip_id , graph - > require_output_nodes , graph - > next_id , graph - > node_count ,
graph - > edge_count ) < 0 )
return XTC_ERR_IO ;
2026-10-27 00:10:45 +01:00
for ( i = 0U ; i < graph - > node_count ; + + i ) {
uint32_t bits ;
memcpy ( & bits , & graph - > nodes [ i ] . value , sizeof ( bits ) ) ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 04:00:27 +01:00
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 ;
}
2026-10-27 06:25:30 +01:00
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 ;
}
2026-10-27 04:00:27 +01:00
}
2026-10-27 00:10:45 +01:00
}
for ( i = 0U ; i < graph - > edge_count ; + + i )
if ( fprintf ( file , " EDGE %zu %zu \n " , graph - > nodes [ graph - > edges [ i ] . source ] . id ,
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 01:06:57 +01:00
for ( i = 0U ; i < project - > track_count ; + + i ) {
uint32_t gain , pan ;
const xtc_track_info * t = project - > tracks + i ;
2026-10-27 01:37:21 +01:00
memcpy ( & gain , & t - > gain , sizeof ( gain ) ) ;
memcpy ( & pan , & t - > pan , sizeof ( pan ) ) ;
2026-10-27 01:06:57 +01:00
if ( fprintf ( file , " TRACK % " PRIu64 " " , t - > id ) < 0 | | ! write_name ( file , t - > name ) | |
2026-10-27 16:15:34 +01:00
fprintf ( file , " MIX %08 " PRIx32 " %08 " PRIx32 " %d %d \n ROUTE %d % " PRIu64 " \n " , gain , pan , t - > mute , t - > solo , t - > is_bus , t - > bus_id ) < 0 )
2026-10-27 01:37:21 +01:00
return XTC_ERR_IO ;
2026-10-27 01:06:57 +01:00
}
2026-10-27 01:37:21 +01:00
if ( fprintf ( file , " CLIPS %zu \n " , project - > clip_count ) < 0 )
return XTC_ERR_IO ;
2026-10-27 00:10:45 +01:00
for ( i = 0U ; i < project - > clip_count ; + + i ) {
const xtc_clip_info * clip = project - > clips + i ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:31:03 +01:00
if ( clip - > asset_path [ 0 ] ) {
2026-10-27 06:31:59 +01:00
const char * asset = clip - > asset_path ;
size_t root = strlen ( directory ) ;
if ( ! strncmp ( asset , directory , root ) & & asset [ root ] = = ' / ' & & asset [ root + 1 ] ) asset + = root + 1 ;
if ( ! write_name ( file , asset ) )
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 15:46:41 +01:00
{
uint32_t bits ;
memcpy ( & bits , & clip - > gain , sizeof ( bits ) ) ;
if ( fprintf ( file , " AUDIO %08 " PRIx32 " % " PRIu64 " % " PRIu64 " \n " , bits ,
clip - > fade_in_tick , clip - > fade_out_tick ) < 0 ) return XTC_ERR_IO ;
}
2026-10-27 00:53:41 +01:00
}
2026-10-27 01:37:21 +01:00
if ( fprintf ( file , " NOTES % " PRIu64 " %zu \n " , project - > next_note_id , project - > note_count ) < 0 )
return XTC_ERR_IO ;
2026-10-27 00:53:41 +01:00
for ( i = 0 ; i < project - > note_count ; + + i ) {
const xtc_note_info * n = project - > notes + i ;
2026-10-27 01:37:21 +01:00
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 ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
if ( fprintf ( file , " AUTOMATION %zu \n " , project - > automation_count ) < 0 )
return XTC_ERR_IO ;
2026-10-27 01:06:57 +01:00
for ( i = 0 ; i < project - > automation_count ; + + i ) {
2026-10-27 01:37:21 +01:00
uint32_t bits ;
const xtc_automation_point * a = project - > automation + i ;
2026-10-27 01:06:57 +01:00
memcpy ( & bits , & a - > value , sizeof ( bits ) ) ;
2026-10-27 16:37:59 +01:00
if ( fprintf ( file , " POINT % " PRIu64 " %u % " PRIu64 " %08 " PRIx32 " %u \n " , a - > track_id , ( unsigned int ) a - > parameter ,
a - > tick , bits , a - > step ) < 0 )
2026-10-27 01:37:21 +01:00
return XTC_ERR_IO ;
2026-10-27 01:06:57 +01:00
}
2026-10-27 16:37:59 +01:00
if ( fputs ( " CONTROL_MAPS 16 \n " , file ) = = EOF ) return XTC_ERR_IO ;
for ( i = 0 ; i < 16 ; + + i )
if ( fprintf ( file , " MAP %zu %u %u %u \n " , i , ( unsigned int ) project - > control_map [ i ] [ 0 ] ,
( unsigned int ) project - > control_map [ i ] [ 1 ] , ( unsigned int ) project - > control_map [ i ] [ 2 ] ) < 0 ) return XTC_ERR_IO ;
2026-10-27 00:10:45 +01:00
return fputs ( " END \n " , file ) = = EOF ? XTC_ERR_IO : XTC_OK ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
static size_t path_length ( const char * path )
{
size_t size = 0U ;
2026-10-27 01:37:21 +01:00
if ( path = = NULL )
return 0U ;
while ( size < 4096U & & path [ size ] ! = ' \0 ' )
+ + size ;
2026-10-27 00:10:45 +01:00
return size = = 4096U ? 0U : size ;
}
2026-10-27 01:37:21 +01:00
2026-10-27 06:31:59 +01:00
/* Resolve only the containing folder: media need not exist. Save replaces the
* named entry ( including a symlink ) , so do not resolve the final component . */
static char * project_folder ( const char * path )
{
char * copy = malloc ( strlen ( path ) + 2 ) , * slash , * folder ;
if ( ! copy ) return NULL ;
strcpy ( copy , path ) ; slash = strrchr ( copy , ' / ' ) ;
if ( ! slash ) strcpy ( copy , " . " ) ;
else if ( slash = = copy ) slash [ 1 ] = 0 ;
else * slash = 0 ;
folder = realpath ( copy , NULL ) ; free ( copy ) ; return folder ;
}
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 16:58:26 +01:00
static xtc_result load_report ( const reader * input , xtc_result result , xtc_project_diagnostic * out )
{
xtc_project_diagnostic d = { 0 } ;
d . result = result ;
d . format_version = input - > version ;
d . line = result = = XTC_OK ? 0 : input - > record_line ;
if ( result ! = XTC_OK ) snprintf ( d . record , sizeof ( d . record ) , " %s " , input - > expected ) ;
if ( result = = XTC_OK ) snprintf ( d . message , sizeof ( d . message ) , " Metadata valid; external media/plugins not loaded. " ) ;
else if ( result = = XTC_ERR_UNSUPPORTED & & ! strcmp ( input - > expected , " XTC_PROJECT " ) )
snprintf ( d . message , sizeof ( d . message ) , " Project format % " PRIu64 " is unsupported; this build reads versions 1..%u. " , input - > version , XTC_PROJECT_FORMAT_VERSION ) ;
else if ( result = = XTC_ERR_UNSUPPORTED ) snprintf ( d . message , sizeof ( d . message ) , " Unsupported project feature or node type ; no data was discarded . " );
else if ( result = = XTC_ERR_FORMAT & & ! strcmp ( input - > expected , " SIZE " ) ) snprintf ( d . message , sizeof ( d . message ) , " Project exceeds the 4 MiB input limit. " ) ;
else if ( result = = XTC_ERR_FORMAT & & ! strcmp ( input - > expected , " MODEL " ) ) snprintf ( d . message , sizeof ( d . message ) , " Invalid project references, IDs, graph, routing or values. " ) ;
else if ( result = = XTC_ERR_FORMAT ) snprintf ( d . message , sizeof ( d . message ) , " Invalid, unknown or truncated record ; expected % s . Unknown fields are rejected . " ,input->expected);
else if ( result = = XTC_ERR_IO ) snprintf ( d . message , sizeof ( d . message ) , " Cannot read project or resolve its containing directory ; check path and permissions . " );
else if ( result = = XTC_ERR_CAPACITY ) snprintf ( d . message , sizeof ( d . message ) , " Project capacity or resolved media path exceeds supported limits. " ) ;
else snprintf ( d . message , sizeof ( d . message ) , " Project load failed: %s. " , xtc_result_string ( result ) ) ;
if ( out ) * out = d ;
return result ;
}
xtc_result xtc_project_load_diagnostic ( xtc_project * project , const char * path , xtc_project_diagnostic * diagnostic )
2026-10-27 00:10:45 +01:00
{
xtc_project * candidate = NULL ;
reader input = { 0 } ;
2026-10-27 06:31:59 +01:00
char * directory ;
2026-10-27 00:10:45 +01:00
xtc_result result ;
2026-10-27 16:58:26 +01:00
struct stat info ;
snprintf ( input . expected , sizeof ( input . expected ) , " %s " , " OPEN " ) ;
2026-10-27 01:37:21 +01:00
if ( project = = NULL | | path_length ( path ) = = 0U )
2026-10-27 16:58:26 +01:00
return load_report ( & input , XTC_ERR_INVALID_ARGUMENT , diagnostic ) ;
2026-10-27 00:10:45 +01:00
input . file = fopen ( path , " rb " ) ;
2026-10-27 16:58:26 +01:00
if ( input . file = = NULL ) return load_report ( & input , XTC_ERR_IO , diagnostic ) ;
if ( fstat ( fileno ( input . file ) , & info ) = = 0 & & S_ISREG ( info . st_mode ) & & info . st_size > 4194304 ) {
snprintf ( input . expected , sizeof ( input . expected ) , " %s " , " SIZE " ) ;
fclose ( input . file ) ;
return load_report ( & input , XTC_ERR_FORMAT , diagnostic ) ;
}
2026-10-27 06:44:22 +01:00
{
char * canonical = realpath ( path , NULL ) ;
directory = canonical ? project_folder ( canonical ) : NULL ;
free ( canonical ) ;
}
2026-10-27 16:58:26 +01:00
if ( ! directory ) { fclose ( input . file ) ; return load_report ( & input , XTC_ERR_IO , diagnostic ) ; }
2026-10-27 00:10:45 +01:00
result = xtc_project_create ( & candidate ) ;
2026-10-27 16:58:26 +01:00
if ( result = = XTC_OK ) result = read_project ( & input , candidate , directory ) ;
2026-10-27 06:31:59 +01:00
free ( directory ) ;
2026-10-27 16:58:26 +01:00
if ( fclose ( input . file ) ! = 0 & & result = = XTC_OK ) result = XTC_ERR_IO ;
2026-10-27 00:10:45 +01:00
if ( result = = XTC_OK ) {
xtc_graph * old = project - > graph ;
* project = * candidate ;
candidate - > graph = old ;
}
2026-10-27 01:37:21 +01:00
xtc_project_destroy ( candidate ) ;
2026-10-27 16:58:26 +01:00
return load_report ( & input , result , diagnostic ) ;
}
xtc_result xtc_project_load ( xtc_project * project , const char * path )
{
return xtc_project_load_diagnostic ( project , path , NULL ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 01:37:21 +01:00
2026-10-27 01:58:27 +01:00
/**
* @ 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 .
*/
2026-10-27 00:10:45 +01:00
xtc_result xtc_project_save ( const xtc_project * project , const char * path )
{
size_t length = path_length ( path ) ;
2026-10-27 06:31:59 +01:00
char * temporary , * directory ;
2026-10-27 00:10:45 +01:00
FILE * file ;
int descriptor ;
xtc_result result ;
2026-10-27 01:37:21 +01:00
if ( project = = NULL | | length = = 0U )
return XTC_ERR_INVALID_ARGUMENT ;
2026-10-27 00:10:45 +01:00
result = xtc_project_validate ( project ) ;
2026-10-27 01:37:21 +01:00
if ( result ! = XTC_OK )
return result ;
2026-10-27 00:10:45 +01:00
temporary = malloc ( length + 12U ) ;
2026-10-27 01:37:21 +01:00
if ( temporary = = NULL )
return XTC_ERR_OUT_OF_MEMORY ;
memcpy ( temporary , path , length ) ;
memcpy ( temporary + length , " .tmp.XXXXXX " , 12U ) ;
2026-10-27 00:10:45 +01:00
descriptor = mkstemp ( temporary ) ;
2026-10-27 01:37:21 +01:00
if ( descriptor < 0 ) {
free ( temporary ) ;
return XTC_ERR_IO ;
}
2026-10-27 00:10:45 +01:00
file = fdopen ( descriptor , " wb " ) ;
2026-10-27 01:37:21 +01:00
if ( file = = NULL ) {
( void ) close ( descriptor ) ;
( void ) unlink ( temporary ) ;
free ( temporary ) ;
return XTC_ERR_IO ;
}
2026-10-27 06:31:59 +01:00
directory = project_folder ( path ) ;
result = directory ? write_project ( file , project , directory ) : XTC_ERR_IO ;
2026-10-27 01:37:21 +01:00
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 ) ;
2026-10-27 06:31:59 +01:00
if ( result = = XTC_OK ) {
int folder = open ( directory , O_RDONLY ) ;
if ( folder < 0 ) result = XTC_ERR_IO ;
else { if ( fsync ( folder ) ) result = XTC_ERR_IO ; if ( close ( folder ) ) result = XTC_ERR_IO ; }
}
free ( directory ) ;
2026-10-27 01:37:21 +01:00
free ( temporary ) ;
return result ;
2026-10-27 00:10:45 +01:00
}