xtc/tests/unit/test_mixer.c

260 lines
12 KiB
C
Raw Normal View History

#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 <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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, &note) == 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,"CHAINS ",7) && strncmp(line,"CHAIN ",6) && 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;
}