#define _POSIX_C_SOURCE 200809L #include "xtc/xtc_audio.h" #include "xtc/xtc_midi.h" #include "xtc/xtc_mixer.h" #include "xtc/xtc_realtime.h" #include #include #include #include #include #include static void close_to(float a, float b) { assert(fabsf(a - b) < 0.000001f); } static void render(xtc_project *p, uint64_t position, float *l, float *r, size_t n) { xtc_midi_plan *plan = NULL; assert(xtc_midi_plan_create(p, 48000, &plan) == XTC_OK); assert(xtc_midi_plan_render_stereo(plan, position, NULL, 0, l, r, n) == XTC_OK); xtc_midi_plan_destroy(plan); } int main(void) { xtc_project *p = NULL, *q = NULL, *candidate = NULL; xtc_history *history = NULL; uint64_t a, b, clip, note; float l[512], r[512], base[512], split[512], split_r[512]; xtc_automation_point point; xtc_track_info track; xtc_midi_plan *plan = NULL; xtc_graph *g = NULL; xtc_plan *graph = NULL; xtc_realtime *engine = NULL; size_t source, gain, out, i, count; char dir[] = "/tmp/xtc-mix-XXXXXX", path[256], copy[256], wave[256]; FILE *f; char bytes[8192] = {0}, roundtrip[8192]; size_t length; assert(mkdtemp(dir)); snprintf(path, sizeof(path), "%s/song.xtc", dir); snprintf(copy, sizeof(copy), "%s/copy.xtc", dir); snprintf(wave, sizeof(wave), "%s/take.wav", dir); assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK); assert(xtc_project_add_track(p, "One", &a) == XTC_OK && xtc_project_add_track(p, "Two", &b) == XTC_OK); assert(xtc_project_add_midi_clip(p, a, 0, 3840, &clip) == XTC_OK); assert(xtc_project_add_note(p, clip, 0, 3840, 69, 127, 0, ¬e) == XTC_OK); render(p, 0, base, r, 512); assert(memcmp(base, r, sizeof(base)) == 0); assert(xtc_project_set_mix(p, a, 0.5f, -1, 0, 0) == XTC_OK); render(p, 0, l, r, 512); for (i = 0; i < 512; ++i) { close_to(l[i], base[i] * 0.5f); assert(r[i] == 0); } assert(xtc_project_set_mix(p, a, 1, 1, 0, 0) == XTC_OK); render(p, 0, l, r, 512); assert(memcmp(base, r, sizeof(base)) == 0); for (i = 0; i < 512; ++i) assert(l[i] == 0); assert(xtc_project_set_mix(p, b, 1, 0, 0, 1) == XTC_OK); render(p, 0, l, r, 512); for (i = 0; i < 512; ++i) assert(l[i] == 0 && r[i] == 0); assert(xtc_project_set_mix(p, a, 1, 0, 1, 1) == XTC_OK); render(p, 0, l, r, 512); for (i = 0; i < 512; ++i) assert(l[i] == 0); assert(xtc_project_set_mix(p, a, 1, 0, 0, 1) == XTC_OK); assert(xtc_project_set_mix(p, a, NAN, 0, 0, 0) == XTC_ERR_INVALID_ARGUMENT); assert(xtc_project_set_mix(p, a, 1, 2, 0, 0) == XTC_ERR_INVALID_ARGUMENT); assert(xtc_project_set_mix(p, a, 1, 0, 2, 0) == XTC_ERR_INVALID_ARGUMENT); point = (xtc_automation_point){a, 0, XTC_AUTOMATION_GAIN, 0,0}; assert(xtc_project_set_automation(p, &point) == XTC_OK); point.tick = 20; point.value = 1; assert(xtc_project_set_automation(p, &point) == XTC_OK); /* sample 500 */ point = (xtc_automation_point){a, 0, XTC_AUTOMATION_PAN, -1,0}; assert(xtc_project_set_automation(p, &point) == XTC_OK); point.tick = 20; point.value = 1; assert(xtc_project_set_automation(p, &point) == XTC_OK); render(p, 0, l, r, 512); for (i = 0; i < 512; ++i) { float t = i < 500 ? (float)i / 500 : 1, pan = -1 + 2 * t; close_to(l[i], base[i] * t * (pan > 0 ? 1 - pan : 1)); close_to(r[i], base[i] * t * (pan < 0 ? 1 + pan : 1)); } assert(xtc_midi_plan_create(p, 48000, &plan) == XTC_OK); for (i = 0; i < 512; i += 64) assert(xtc_midi_plan_render_stereo(plan, i, NULL, 0, split + i, split_r + i, 64) == XTC_OK); assert(memcmp(l, split, sizeof(l)) == 0 && memcmp(r, split_r, sizeof(r)) == 0); xtc_midi_plan_destroy(plan); /* Real graph routing preserves stereo; oscillator phases advance only once. */ assert(xtc_graph_create(4, &g) == XTC_OK); assert(xtc_graph_add_audio(g, &source) == XTC_OK); assert(xtc_graph_add_gain(g, 0.5f, &gain) == XTC_OK); assert(xtc_graph_add_output(g, &out) == XTC_OK); assert(xtc_graph_connect(g, source, gain) == XTC_OK && xtc_graph_connect(g, gain, out) == XTC_OK); assert(xtc_graph_compile(g, 48000, 512, &graph) == XTC_OK); assert(xtc_plan_process_stereo(graph, NULL, l, r, NULL, NULL, split, split_r, 512) == XTC_OK); for (i = 0; i < 512; ++i) { close_to(split[i], l[i] * 0.5f); close_to(split_r[i], r[i] * 0.5f); } xtc_plan_destroy(graph); assert(xtc_realtime_create(48000, 512, &engine) == XTC_OK && xtc_realtime_submit(engine, g) == XTC_OK); assert(xtc_realtime_process_stereo(engine, NULL, l, r, NULL, NULL, split, split_r, 512) == XTC_OK); for (i = 0; i < 512; ++i) close_to(split_r[i], r[i] * 0.5f); xtc_realtime_destroy(engine); xtc_graph_destroy(g); assert(xtc_graph_create(1, &g) == XTC_OK && xtc_graph_add_oscillator(g, 440, &source) == XTC_OK && xtc_graph_compile(g, 48000, 512, &graph) == XTC_OK); assert(xtc_plan_process_stereo(graph, NULL, NULL, NULL, NULL, NULL, l, r, 64) == XTC_OK && memcmp(l, r, 64 * sizeof(float)) == 0); assert(xtc_plan_process_stereo(graph, NULL, NULL, NULL, NULL, NULL, l + 64, r + 64, 64) == XTC_OK && memcmp(l, r, 128 * sizeof(float)) == 0); close_to(l[64], (float)sin(6.283185307179586 * 440 * 64 / 48000)); xtc_plan_destroy(graph); xtc_graph_destroy(g); /* Audio track uses the same mix/automation evaluator. */ { xtc_recorder *rec = NULL; xtc_audio_plan *audio = NULL; uint64_t frames; for (i = 0; i < 512; ++i) base[i] = 0.25f; assert(xtc_recorder_create(wave, 48000, 1024, &rec) == XTC_OK); xtc_recorder_push(rec, base, 512); assert(xtc_recorder_finish(rec) == XTC_OK); assert(xtc_recorder_status(rec, &frames) == XTC_OK && frames == 512); xtc_recorder_destroy(rec); assert(xtc_project_import_audio(p, a, wave, 0, &clip) == XTC_OK && xtc_audio_plan_create(p, 48000, &audio) == XTC_OK); assert(xtc_audio_plan_render_stereo(audio, 0, l, r, 512) == XTC_OK); close_to(l[250], 0.125f); close_to(r[250], 0.125f); assert(l[500] == 0 && r[500] == 0.25f); xtc_audio_plan_destroy(audio); } assert(xtc_project_save(p, path) == XTC_OK && xtc_project_load(q, path) == XTC_OK && xtc_project_save(q, copy) == XTC_OK); f = fopen(path, "rb"); assert(f); length = fread(bytes, 1, sizeof(bytes), f); assert(length < sizeof(bytes)); fclose(f); f = fopen(copy, "rb"); assert(f && fread(roundtrip, 1, sizeof(roundtrip), f) == length); fclose(f); assert(memcmp(bytes, roundtrip, length) == 0); for (i = 0; i < length; ++i) { f = fopen(copy, "wb"); assert(f && fwrite(bytes, 1, i, f) == i && fclose(f) == 0); assert(xtc_project_load(q, copy) != XTC_OK); } /* Invalid mixer and automation records retain destination. */ { const char *bad[] = {"MIX 7fc00000 00000000 0 0", "MIX 40800001 00000000 0 0", "MIX 3f800000 40000000 0 0", "MIX 3f800000 00000000 2 0", "POINT 999 0 0 3f800000 0", "POINT 0 2 0 3f800000 0", "POINT 0 0 0 7f800000 0", "POINT 0 0 20 3f800000 0", "POINT 0 0 0 3f800000 2"}; for (i = 0; i < sizeof(bad) / sizeof(*bad); ++i) { char *start = strstr(bytes, i < 4 ? "MIX " : "POINT "), *end; assert(start); end = strchr(start, '\n'); assert(end); f = fopen(copy, "wb"); assert(f); assert(fwrite(bytes, 1, (size_t)(start - bytes), f) == (size_t)(start - bytes)); assert(fprintf(f, "%s%s", bad[i], end) > 0); fclose(f); assert(xtc_project_load(q, copy) == XTC_ERR_FORMAT); } } /* v3 retains notes but supplies default mix and no automation. */ { char line[1536]; FILE *legacy; f = fopen(path, "rb"); legacy = fopen(copy, "wb"); assert(f && legacy); 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) && strncmp(line, "AUDIO ", 6) && strncmp(line, "ROUTE ", 6) && strncmp(line,"CONTROL_MAPS ",13) && strncmp(line,"MAP ",4) && strncmp(line,"TRACK_INSTRUMENTS ",18) && strncmp(line,"LIVE_INSTRUMENT ",16) && strncmp(line,"MARKERS ",8) && strncmp(line,"MARKER ",7)) assert(fputs(line, legacy) >= 0); } assert(fclose(f) == 0 && fclose(legacy) == 0); assert(xtc_project_load(q, copy) == XTC_OK); assert(xtc_project_get_track(q, 0, &track) == XTC_OK && track.gain == 1 && track.pan == 0 && !track.mute && !track.solo); assert(xtc_project_automation_count(q, &count) == XTC_OK && count == 0); assert(xtc_project_note_count(q, &count) == XTC_OK && count == 1); } assert(xtc_history_create(p, &history) == XTC_OK); for (i = 0; i < 40; ++i) { assert(xtc_project_set_mix(p, a, (float)i / 10, 0, 0, 0) == XTC_OK && xtc_history_record(history, p) == XTC_OK); } for (i = 0; i < 32; ++i) { assert(xtc_history_checkout(history, 0, &candidate) == XTC_OK); xtc_project_destroy(candidate); assert(xtc_history_accept(history, 0) == XTC_OK); } assert(xtc_history_checkout(history, 0, &candidate) == XTC_ERR_NOT_FOUND && candidate == NULL); assert(xtc_history_checkout(history, 1, &candidate) == XTC_OK); assert(xtc_project_get_track(candidate, 0, &track) == XTC_OK); close_to(track.gain, 0.8f); xtc_project_destroy(candidate); assert(xtc_history_accept(history, 1) == XTC_OK); assert(xtc_history_record(history, p) == XTC_OK && xtc_history_checkout(history, 1, &candidate) == XTC_ERR_NOT_FOUND); { uint64_t discarded, replacement; assert(xtc_project_add_track(p, "Discarded", &discarded) == XTC_OK); assert(xtc_history_record(history, p) == XTC_OK); assert(xtc_history_checkout(history, 0, &candidate) == XTC_OK); assert(xtc_project_add_track(candidate, "New branch", &replacement) == XTC_OK && replacement > discarded); xtc_project_destroy(candidate); } xtc_history_destroy(history); assert(xtc_project_remove_track(p, a) == XTC_OK && xtc_project_automation_count(p, &count) == XTC_OK && count == 0); point = (xtc_automation_point){b, 0, XTC_AUTOMATION_GAIN, 1,0}; for (i = 0; i < XTC_MAX_AUTOMATION_POINTS; ++i) { point.tick = i; assert(xtc_project_set_automation(p, &point) == XTC_OK); } point.tick = i; assert(xtc_project_set_automation(p, &point) == XTC_ERR_CAPACITY); point.tick = 0; point.value = 2; assert(xtc_project_set_automation(p, &point) == XTC_OK); assert(xtc_project_remove_automation(p, b, XTC_AUTOMATION_GAIN, 0) == XTC_OK); assert(xtc_project_remove_automation(p, b, XTC_AUTOMATION_GAIN, 0) == XTC_ERR_NOT_FOUND); xtc_project_destroy(p); xtc_project_destroy(q); unlink(path); unlink(copy); unlink(wave); rmdir(dir); puts("Mixer stereo/gain/pan/mute/solo/automation/history/persistence passed."); return 0; }