diff --git a/Makefile b/Makefile index 176225f..5788d0a 100644 --- a/Makefile +++ b/Makefile @@ -336,3 +336,11 @@ test-editing: $(BUILD_DIR)/test_editing ./$(BUILD_DIR)/test_editing test: test-editing -include $(BUILD_DIR)/tests/unit/test_editing.d + +$(BUILD_DIR)/test_portability: $(BUILD_DIR)/tests/integration/test_portability.o $(BUILD_DIR)/libxtc.a + $(CC) $(LDFLAGS) -o $@ $^ -lm -ldl +.PHONY: test-portability +test-portability: $(BUILD_DIR)/test_portability + ./$(BUILD_DIR)/test_portability +test: test-portability +-include $(BUILD_DIR)/tests/integration/test_portability.d diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md index 51d94cb..d7d7148 100644 --- a/docs/PROJECT_FORMAT.md +++ b/docs/PROJECT_FORMAT.md @@ -1,4 +1,4 @@ -# Project format v5 (reads v1/v2/v3/v4) +# Project format v7 (reads v1–v6) Phases 5–15 use a bounded text format implemented in C99 without a parser library. The extension is `.xtc`. File operations run exclusively on the control thread. @@ -12,7 +12,7 @@ Fields use ASCII spaces. Integers are unsigned decimal, without signs. Counts determine the number of subsequent records: ```text -XTC_PROJECT 5 +XTC_PROJECT 7 APP 0 1 0 TEMPO 120000 IDS 1 1 @@ -82,8 +82,8 @@ node types 1..4, migrating their clips to empty asset paths. Version 2 accepts node types 1..5 and ASSET records. Both migrate to MIDI=0 and no notes. Version 3 accepts instrument nodes and MIDI/NOTES records. Versions 1–3 migrate all tracks to unity gain, centered pan, mute/solo off and no automation. -Saving always writes v5. Unknown versions are rejected without replacing the destination; older -applications cannot read v5. Migration and maximum-path roundtrips are tested. +Saving always writes v7. Unknown versions are rejected without replacing the destination; older +applications cannot read v7. Migration and maximum-path roundtrips are tested. ## Ownership and failures @@ -148,3 +148,43 @@ port/tick pairs, nonfinite values, unknown control ports and ticks beyond the project limit are rejected. Loaded plugin tempo comes from TEMPO. Range and discrete-mode compatibility are checked on control-thread compilation. Versions 1–5 migrate with empty plugin lanes. See CONTROL.md for timing/ordering. + +## Phase 18: version 7 and project folders + +Current writer emits v7; readers migrate v1–v6. Record ordering and field bounds +are otherwise unchanged from v6. v7 ASSET may contain a relative path, interpreted +against the actual containing directory of the named project file (resolved on +the control thread), never the launch working directory. After loading, all core +asset references are absolute, including missing media. Media inside the save +folder are written relative; references outside remain absolute. Save As resolves +against its new folder and preserves media identity rather than rebasing it by +accident. Copy/move a project folder together with its referenced media to retain +portability. Saving does not copy assets: import/record into that folder first. +Plugin binaries remain trusted absolute module references, not bundled resources. + +Empty assets still use `-`. Paths exclude embedded NUL/control bytes and must fit +511 bytes after resolution; overly long resolved references fail transactionally. +Relative references can name outside the folder using ordinary POSIX semantics; +this is not a sandbox. Symlinks follow filesystem behavior, so portability requires +moving their targets too. Old formats continue to require absolute media paths. +No unknown version/node/record is silently discarded; unsupported versions/types +or malformed/trailing data reject the candidate and retain the current project. +Parsing never opens media/plugins or constructs an audio engine. + +Save validates the complete model, writes/fsyncs a same-folder temporary, closes +it, atomically renames it, then fsyncs the containing directory. Failures before +rename retain the old destination. A directory sync failure *after* rename reports +I/O error but the new complete file may already be present: inspect/reopen it; +there is no rollback across that durability boundary. Filesystem/hardware power +loss guarantees still depend on the underlying storage. Recording destinations +have their separate no-clobber policy; ordinary project Save intentionally replaces +the selected document. In-memory models and the existing destination are preserved +on load failure. No automatic unknown-field stripping or media deletion occurs. + +`make test-portability test test-plugins test-recovery test-recording` covers checked-in +v1–v6 fixtures (including missing v5/v6 plugins), v7 exact roundtrips, moved-folder +playback from a different cwd, missing media retention, unknown records, every +truncation and deterministic byte mutations, malformed plugin metadata, interrupted +writers and write-limit failures. ASan/UBSan/static analysis additionally audit the +bounded parser. This is regression/fault-injection coverage, not exhaustive fuzzing +or a claim of power-cut qualification. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6e054e4..6643723 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -385,6 +385,14 @@ screen-reader/accessibility qualification remain explicit limits. - Missing media and plugins are reported clearly. - Save/load round trips and format migrations have automated tests. +Implemented: v7 folder-relative media persistence with absolute runtime references, +transactional resolution independent of launch cwd, Save As identity preservation, +and parent-directory fsync after atomic publication. Checked-in v1–v6 migrations +(including missing plugins), moved-folder exact playback, missing-media retention, +truncation/mutation and unknown-record/error tests pass. See PROJECT_FORMAT.md. +No media is copied/deleted automatically; absolute external/plugin dependencies, +path bounds, post-rename sync errors and physical power-loss limits are explicit. + ## Phase 19 — Performance, Scalability, and Stress Testing **Goal:** Measure performance and establish predictable behavior as projects grow. diff --git a/include/xtc/xtc_project.h b/include/xtc/xtc_project.h index c3a88eb..a391531 100644 --- a/include/xtc/xtc_project.h +++ b/include/xtc/xtc_project.h @@ -208,7 +208,9 @@ xtc_result xtc_project_sample_to_tick(const xtc_project *project, uint64_t sampl uint64_t *out_tick); /* Control-thread I/O. Versioned, bounded format; see docs/PROJECT_FORMAT.md. * Load is transactional. Save fsyncs a same-directory temporary and renames it; - * failures before rename preserve the previous destination. No asset I/O. */ + * failures before rename preserve the previous destination. No asset I/O. v7 saves local media paths relative to the project folder; load + * resolves them to absolute references. Post-rename directory-sync failure can + * report IO with the complete new document already published. */ /** * @brief Saves a versioned project through a synced temporary file and atomic rename. * diff --git a/src/persistence/project_file.c b/src/persistence/project_file.c index 4de685d..beeec26 100644 --- a/src/persistence/project_file.c +++ b/src/persistence/project_file.c @@ -1,3 +1,4 @@ +#define _XOPEN_SOURCE 700 #define _POSIX_C_SOURCE 200809L #include "../project/project_internal.h" #include "../graph/graph_internal.h" @@ -10,6 +11,7 @@ #include #include #include +#include typedef struct { FILE *file; @@ -184,14 +186,14 @@ static int decode_float(const char *text, float *value) * @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) +static xtc_result read_project(reader *input, xtc_project *project, const char *directory) { 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) + if (version < 1U || version > 7U) return XTC_ERR_UNSUPPORTED; format_version = version; READ("APP", 4U); @@ -402,6 +404,15 @@ static xtc_result read_project(reader *input, xtc_project *project) return XTC_ERR_FORMAT; if (ferror(input->file)) return XTC_ERR_IO; + 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] = '/'; + } + } return xtc_project_validate(project); } @@ -428,7 +439,7 @@ static int write_name(FILE *file, const char *name) * @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) +static xtc_result write_project(FILE *file, const xtc_project *project, const char *directory) { const xtc_graph *graph = project->graph; size_t i; @@ -437,7 +448,7 @@ static xtc_result write_project(FILE *file, const xtc_project *project) 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", + if (fprintf(file, "XTC_PROJECT 7\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) @@ -493,7 +504,10 @@ static xtc_result write_project(FILE *file, const xtc_project *project) if (fputs("ASSET ", file) == EOF) return XTC_ERR_IO; if (clip->asset_path[0]) { - if (!write_name(file, clip->asset_path)) + 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)) return XTC_ERR_IO; } else if (fputs("-\n", file) == EOF) return XTC_ERR_IO; @@ -537,6 +551,19 @@ static size_t path_length(const char *path) return size == 4096U ? 0U : size; } +/* 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; +} + /** * @brief Loads and validates a project before transactionally replacing the current state. * @@ -548,15 +575,19 @@ xtc_result xtc_project_load(xtc_project *project, const char *path) { xtc_project *candidate = NULL; reader input = {0}; + char *directory; 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; + directory = project_folder(path); + if (!directory) { fclose(input.file); return XTC_ERR_IO; } result = xtc_project_create(&candidate); if (result == XTC_OK) - result = read_project(&input, candidate); + result = read_project(&input, candidate, directory); + free(directory); if (fclose(input.file) != 0 && result == XTC_OK) result = XTC_ERR_IO; if (result == XTC_OK) { @@ -578,7 +609,7 @@ xtc_result xtc_project_load(xtc_project *project, const char *path) xtc_result xtc_project_save(const xtc_project *project, const char *path) { size_t length = path_length(path); - char *temporary; + char *temporary, *directory; FILE *file; int descriptor; xtc_result result; @@ -604,7 +635,8 @@ xtc_result xtc_project_save(const xtc_project *project, const char *path) free(temporary); return XTC_ERR_IO; } - result = write_project(file, project); + directory = project_folder(path); + result = directory ? write_project(file, project, directory) : XTC_ERR_IO; if (result == XTC_OK && fflush(file) != 0) result = XTC_ERR_IO; if (result == XTC_OK && fsync(descriptor) != 0) @@ -615,6 +647,12 @@ xtc_result xtc_project_save(const xtc_project *project, const char *path) result = XTC_ERR_IO; if (result != XTC_OK) (void)unlink(temporary); + 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); free(temporary); return result; } diff --git a/tests/fixtures/project-plugin-v5.xtc b/tests/fixtures/project-plugin-v5.xtc new file mode 100644 index 0000000..8044740 --- /dev/null +++ b/tests/fixtures/project-plugin-v5.xtc @@ -0,0 +1,14 @@ +XTC_PROJECT 5 +APP 0 1 0 +TEMPO 120000 +IDS 0 0 +GRAPH 1 1 1 0 +NODE 0 8 00000000 +PLUGIN 900000 0 1 1 7874635f746573745f6761696e +MODULE 2f6d697373696e672f746573742e736f +CONTROL 2 3f800000 +TRACKS 0 +CLIPS 0 +NOTES 0 0 +AUTOMATION 0 +END diff --git a/tests/fixtures/project-plugin-v6.xtc b/tests/fixtures/project-plugin-v6.xtc new file mode 100644 index 0000000..1eba6e2 --- /dev/null +++ b/tests/fixtures/project-plugin-v6.xtc @@ -0,0 +1,16 @@ +XTC_PROJECT 6 +APP 0 1 0 +TEMPO 120000 +IDS 0 0 +GRAPH 1 1 1 0 +NODE 0 8 00000000 +PLUGIN 900000 0 1 1 7874635f746573745f6761696e +MODULE 2f6d697373696e672f746573742e736f +CONTROL 2 3f800000 +PLUGIN_AUTOMATION 1 +PARAM 960 2 0 3f000000 +TRACKS 0 +CLIPS 0 +NOTES 0 0 +AUTOMATION 0 +END diff --git a/tests/fixtures/project-v1.xtc b/tests/fixtures/project-v1.xtc new file mode 100644 index 0000000..41297f8 --- /dev/null +++ b/tests/fixtures/project-v1.xtc @@ -0,0 +1,13 @@ +XTC_PROJECT 1 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +END diff --git a/tests/fixtures/project-v2.xtc b/tests/fixtures/project-v2.xtc new file mode 100644 index 0000000..1a44905 --- /dev/null +++ b/tests/fixtures/project-v2.xtc @@ -0,0 +1,14 @@ +XTC_PROJECT 2 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +ASSET - +END diff --git a/tests/fixtures/project-v3.xtc b/tests/fixtures/project-v3.xtc new file mode 100644 index 0000000..fdc8e88 --- /dev/null +++ b/tests/fixtures/project-v3.xtc @@ -0,0 +1,17 @@ +XTC_PROJECT 3 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +ASSET - +MIDI 1 +NOTES 1 1 +NOTE 0 0 0 480 69 100 0 +END diff --git a/tests/fixtures/project-v4.xtc b/tests/fixtures/project-v4.xtc new file mode 100644 index 0000000..28b4fb1 --- /dev/null +++ b/tests/fixtures/project-v4.xtc @@ -0,0 +1,20 @@ +XTC_PROJECT 4 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +MIX 3f800000 00000000 0 0 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +ASSET - +MIDI 1 +NOTES 1 1 +NOTE 0 0 0 480 69 100 0 +AUTOMATION 1 +POINT 0 0 0 3f800000 +END diff --git a/tests/fixtures/project-v5.xtc b/tests/fixtures/project-v5.xtc new file mode 100644 index 0000000..2ca6ed3 --- /dev/null +++ b/tests/fixtures/project-v5.xtc @@ -0,0 +1,20 @@ +XTC_PROJECT 5 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +MIX 3f800000 00000000 0 0 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +ASSET - +MIDI 1 +NOTES 1 1 +NOTE 0 0 0 480 69 100 0 +AUTOMATION 1 +POINT 0 0 0 3f800000 +END diff --git a/tests/fixtures/project-v6.xtc b/tests/fixtures/project-v6.xtc new file mode 100644 index 0000000..c6c8564 --- /dev/null +++ b/tests/fixtures/project-v6.xtc @@ -0,0 +1,20 @@ +XTC_PROJECT 6 +APP 0 1 0 +TEMPO 120000 +IDS 1 1 +GRAPH 1 2 2 1 +NODE 0 1 3f000000 +NODE 1 4 00000000 +EDGE 0 1 +TRACKS 1 +TRACK 0 4b657973 +MIX 3f800000 00000000 0 0 +CLIPS 1 +CLIP 0 0 0 960 0 960 5665727365 +ASSET - +MIDI 1 +NOTES 1 1 +NOTE 0 0 0 480 69 100 0 +AUTOMATION 1 +POINT 0 0 0 3f800000 +END diff --git a/tests/integration/test_portability.c b/tests/integration/test_portability.c new file mode 100644 index 0000000..bea2dcd --- /dev/null +++ b/tests/integration/test_portability.c @@ -0,0 +1,80 @@ +#define _XOPEN_SOURCE 700 +#include "xtc/xtc_audio.h" +#include "xtc/xtc_mixer.h" +#include "xtc/xtc_midi.h" +#include "xtc/xtc_plugin.h" +#include +#include +#include +#include +#include +#include +int main(void) +{ + char root[] = "/tmp/xtc-portable-XXXXXX", moved[512], file[512], media[512], old[512]; + xtc_project *p = NULL; unsigned version; uint64_t track, clip; + float in[64], expected[64], actual[64]; size_t i; + xtc_recorder *rec = NULL; xtc_audio_plan *audio = NULL; + assert(mkdtemp(root)); + assert(xtc_project_create(&p) == XTC_OK); + snprintf(file,sizeof(file),"%s/migration.xtc",root); + for (version = 1; version <= 6; ++version) { + xtc_track_info t; xtc_clip_info c; size_t notes, points; + snprintf(old,sizeof(old),"tests/fixtures/project-v%u.xtc",version); + assert(xtc_project_load(p,old) == XTC_OK); + assert(xtc_project_get_track(p,0,&t) == XTC_OK && !strcmp(t.name,"Keys") && t.gain == 1); + assert(xtc_project_get_clip(p,0,&c) == XTC_OK && c.midi == (version >= 3)); + assert(xtc_project_note_count(p,¬es) == XTC_OK && notes == (size_t)(version >= 3)); + assert(xtc_project_automation_count(p,&points) == XTC_OK && points == (size_t)(version >= 4)); + assert(xtc_project_save(p,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK); + } + for (version = 5; version <= 6; ++version) { + xtc_plugin_spec spec; xtc_plan *plan = NULL; + snprintf(old,sizeof(old),"tests/fixtures/project-plugin-v%u.xtc",version); + assert(xtc_project_load(p,old) == XTC_OK); + assert(xtc_graph_get_plugin(xtc_project_graph(p),0,&spec) == XTC_OK && spec.tempo == 120000 && spec.point_count == version-5); + assert(xtc_project_save(p,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK); + { xtc_result r = xtc_graph_compile(xtc_project_graph(p),48000,64,&plan); + assert((r == XTC_ERR_NOT_FOUND || r == XTC_ERR_UNSUPPORTED) && !plan); } + } + xtc_project_destroy(p); p = NULL; + assert(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p,"Audio",&track) == XTC_OK); + snprintf(media,sizeof(media),"%s/media.wav",root); + for (i = 0; i < 64; ++i) in[i] = (float)i / 100; + assert(xtc_recorder_create(media,48000,64,&rec) == XTC_OK); + xtc_recorder_push(rec,in,64); assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec); + assert(xtc_project_import_audio(p,track,media,0,&clip) == XTC_OK); + assert(xtc_audio_plan_create(p,48000,&audio) == XTC_OK); + assert(xtc_audio_plan_render(audio,0,expected,64) == XTC_OK); xtc_audio_plan_destroy(audio); + assert(xtc_project_save(p,file) == XTC_OK); + { FILE *f = fopen(file,"r"); char text[4096]; size_t n; assert(f); + n = fread(text,1,sizeof(text)-1,f); text[n] = 0; fclose(f); + assert(strstr(text,"ASSET 6d656469612e776176\n")); } + { + char sub[512], copy[512]; xtc_clip_info c; + snprintf(sub,sizeof(sub),"%s/elsewhere",root); assert(mkdir(sub,0700) == 0); + snprintf(copy,sizeof(copy),"%s/elsewhere/copy.xtc",root); + assert(xtc_project_save(p,copy) == XTC_OK && xtc_project_load(p,copy) == XTC_OK); + assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && !strcmp(c.asset_path,media)); + unlink(copy); rmdir(sub); + } + snprintf(moved,sizeof(moved),"%s-moved",root); assert(rename(root,moved) == 0); + snprintf(file,sizeof(file),"%s-moved/migration.xtc",root); + assert(xtc_project_load(p,file) == XTC_OK); + { char *cwd = getcwd(NULL,0); assert(cwd && chdir("/") == 0); + assert(xtc_audio_plan_create(p,48000,&audio) == XTC_OK && xtc_audio_plan_issues(audio,NULL) == 0); + assert(xtc_audio_plan_render(audio,0,actual,64) == XTC_OK); + for (i = 0; i < 64; ++i) assert(expected[i] == actual[i]); + xtc_audio_plan_destroy(audio); assert(chdir(cwd) == 0); free(cwd); } + snprintf(media,sizeof(media),"%s-moved/media.wav",root); assert(unlink(media) == 0); + assert(xtc_project_save(p,file) == XTC_OK && xtc_project_load(p,file) == XTC_OK); + assert(xtc_audio_plan_create(p,48000,&audio) == XTC_OK && xtc_audio_plan_issues(audio,NULL) == 1); + xtc_audio_plan_destroy(audio); + /* Unknown records/versions fail without discarding the retained clip. */ + { FILE *f = fopen(file,"a"); assert(f); fputs("FUTURE 1\n",f); fclose(f); + assert(xtc_project_load(p,file) == XTC_ERR_FORMAT); + { xtc_clip_info c; assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && c.asset_path[0]); } } + xtc_project_destroy(p); unlink(file); rmdir(moved); + puts("v1-v6 migration, v7 folder relocation, cwd independence and missing-media preservation passed."); + return 0; +} diff --git a/tests/unit/test_mixer.c b/tests/unit/test_mixer.c index e6e9949..f525ada 100644 --- a/tests/unit/test_mixer.c +++ b/tests/unit/test_mixer.c @@ -190,6 +190,12 @@ int main(void) assert(fgets(line, sizeof(line), f)); assert(fputs("XTC_PROJECT 3\n", legacy) >= 0); while (fgets(line, sizeof(line), f)) { + /* Old formats use absolute assets, unlike the v7 writer. */ + if (!strncmp(line,"ASSET ",6) && line[6] != '-') { + size_t k; fputs("ASSET ",legacy); + for (k = 0; wave[k]; ++k) fprintf(legacy,"%02x",(unsigned char)wave[k]); + fputc('\n',legacy); continue; + } if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) && strncmp(line, "POINT ", 6)) assert(fputs(line, legacy) >= 0);