Phase 8: Add stereo track mixer, automation and undo/redo
This commit is contained in:
parent
cacc086805
commit
7db5fb7738
45 changed files with 1611 additions and 130 deletions
|
|
@ -3,6 +3,7 @@
|
|||
* Processing is driven synchronously here; realtime concurrency is tested
|
||||
* separately by test_realtime. No fake header replaces installed declarations. */
|
||||
#include "xtc/xtc_backend.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <jack/jack.h>
|
||||
#include <jack/midiport.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -18,8 +19,8 @@ static JackBufferSizeCallback size_cb;
|
|||
static JackXRunCallback xrun_cb;
|
||||
static JackShutdownCallback shutdown_cb;
|
||||
static void *context;
|
||||
static float input[128], output[128];
|
||||
static int client_token, input_token, output_token, midi_token;
|
||||
static float input[128], output[128], output_right[128];
|
||||
static int client_token, input_token, output_token, right_token, midi_token;
|
||||
static unsigned int midi_count;
|
||||
static jack_midi_event_t midi_events[256];
|
||||
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
|
||||
|
|
@ -44,6 +45,7 @@ jack_port_t *jack_port_register(jack_client_t *client, const char *name, const c
|
|||
(void)client; (void)name; (void)type; (void)buffer_size;
|
||||
++port_calls;
|
||||
if (register_fail == port_calls) return NULL;
|
||||
if (!strcmp(name, "output_right")) return (jack_port_t *)&right_token;
|
||||
if (!strcmp(type, JACK_DEFAULT_MIDI_TYPE)) return (jack_port_t *)&midi_token;
|
||||
return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token);
|
||||
}
|
||||
|
|
@ -66,12 +68,12 @@ int jack_activate(jack_client_t *client)
|
|||
int jack_deactivate(jack_client_t *client)
|
||||
{ (void)client; if (deactivate_fail) return -1; active = 0; return 0; }
|
||||
void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames)
|
||||
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; return port == (jack_port_t *)&input_token ? input : output; }
|
||||
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; if (port == (jack_port_t *)&right_token) return output_right; return port == (jack_port_t *)&input_token ? input : output; }
|
||||
uint32_t jack_midi_get_event_count(void *buffer) { (void)buffer; return midi_count; }
|
||||
int jack_midi_event_get(jack_midi_event_t *event, void *buffer, uint32_t index)
|
||||
{ (void)buffer; if (index >= midi_count || index >= 256) return -1; *event = midi_events[index]; return 0; }
|
||||
const char *jack_port_name(const jack_port_t *port)
|
||||
{ if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
|
||||
{ if (port == (jack_port_t *)&right_token) return "test:output_right"; if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
|
||||
const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags)
|
||||
{
|
||||
const char **list = malloc(2U * sizeof(*list));
|
||||
|
|
@ -97,7 +99,7 @@ int main(void)
|
|||
const char *name;
|
||||
int visits = 0, iteration;
|
||||
CHECK(xtc_backend_open(NULL, &backend) == XTC_ERR_INVALID_ARGUMENT && backend == NULL);
|
||||
config.channel_count = 2U;
|
||||
config.channel_count = 3U;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_INVALID_ARGUMENT);
|
||||
config.channel_count = 1U;
|
||||
config.kind = (xtc_backend_kind)999;
|
||||
|
|
@ -297,6 +299,25 @@ int main(void)
|
|||
CHECK(xtc_backend_close(backend) == XTC_OK);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
{
|
||||
xtc_project *p = NULL; uint64_t track, clip, note;
|
||||
config.channel_count = 2;
|
||||
register_fail = 4;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
|
||||
register_fail = 0;
|
||||
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
|
||||
CHECK(xtc_backend_output_name(backend, 1, &name) == XTC_OK && strcmp(name, "test:output_right") == 0);
|
||||
CHECK(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "Right", &track) == XTC_OK);
|
||||
CHECK(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
|
||||
CHECK(xtc_project_add_note(p, clip, 0, 960, 69, 127, 0, ¬e) == XTC_OK);
|
||||
CHECK(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
|
||||
CHECK(xtc_backend_prepare_project(backend, p) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(process_cb(32, context) == 0 && output[1] == 0 && output_right[1] > 0);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_right == 0);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK); xtc_project_destroy(p);
|
||||
}
|
||||
CHECK(closed >= 104);
|
||||
CHECK(xtc_backend_close(NULL) == XTC_OK);
|
||||
xtc_graph_destroy(invalid_graph);
|
||||
|
|
|
|||
254
tests/unit/test_mixer.c
Normal file
254
tests/unit/test_mixer.c
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
#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, ¬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};
|
||||
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};
|
||||
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", "POINT 0 2 0 3f800000",
|
||||
"POINT 0 0 0 7f800000", "POINT 0 0 20 3f800000"};
|
||||
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)) {
|
||||
if (strncmp(line, "MIX ", 4) && strncmp(line, "AUTOMATION ", 11) &&
|
||||
strncmp(line, "POINT ", 6))
|
||||
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};
|
||||
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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue