1
0
Fork 0
forked from hanez/xtc

Phase 8: Add stereo track mixer, automation and undo/redo

This commit is contained in:
Johannes Findeisen 2026-10-27 01:06:57 +01:00
commit 7db5fb7738
45 changed files with 1611 additions and 130 deletions

View file

@ -135,3 +135,24 @@ draw/move/resize notes, save/reopen and render them. The live MIDI target connec
only software clients and checks on/off, panic, disconnect, and saved-note
playback/seek. It needs an existing JACK-compatible server. No hardware routes
or system changes are made. MIDI recording/SMF and physical devices are deferred.
## Phase 8 verification
```sh
make all test gui debug test-gui test-gui-jack test-jack-audio test-jack-midi
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
```
`test_mixer` covers stereo gain/pan, mute/solo, sample automation, block
independence, WAV and graph routing, v4 persistence failures and bounded history.
GUI tests add automation mouse edits, mixer keys, history rollback/branching and
save/reopen. The existing live MIDI test now uses two software capture ports and
checks hard-right saved-note playback and output meter snapshots; mono audio
record/play acceptance remains covered. A native Xvfb mouse smoke check confirms
sliders and automation drawing. See MIXER.md for capacities and deferred features.
The fresh Phase 8 verification build, no-JACK core/GUI suite and Clang
ASan/UBSan core/GUI suite passed without compiler warnings. GUI leak checking
retains only the existing fontconfig suppression. ThreadSanitizer also passed
the recorder concurrency and realtime publication tests. Physical devices and
long-running stereo performance remain unverified.

View file

@ -2,6 +2,7 @@
#include "../../src/gui/graph_widget.hpp"
#include "../../src/gui/sequencer_widget.hpp"
#include "../../src/gui/piano_roll.hpp"
#include "../../src/gui/mixer_widget.hpp"
#include <unistd.h>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
@ -89,7 +90,7 @@ void reset()
extern "C" {
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
{
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 1U);
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 2U);
++opens;
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
if (*out != nullptr) { ++alive; handle.running = 0; CHECK(handle.plan == nullptr); engine_status.active_revision = engine_status.submitted_revision = 0U; }
@ -328,6 +329,41 @@ int main(int argc, char **argv)
press(find(&window, "Panic"));
CHECK(xtc_midi_plan_render(handle.midi, 18032, nullptr, 0, instrument, 32) == XTC_OK && instrument[0] == 0);
engine_status.sample_position = 0;
// Mixer edits and automation are undoable; failed backend install retains cursor.
auto *mixer_ui = window.mixer(); CHECK(mixer_ui != nullptr);
xtc_track_info mixed{};
CHECK(mixer_ui->mix(0.5f, -1, 0, 1) == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1 && mixed.solo);
CHECK(mixer_ui->point(0, 0) == XTC_OK && mixer_ui->point(960, 1) == XTC_OK);
size_t points = 0;
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
submit_result = XTC_ERR_CAPACITY;
CHECK(window.undo(true) == XTC_ERR_CAPACITY);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
submit_result = XTC_OK;
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
CHECK(window.undo() == XTC_OK && mixer_ui->point(1920, 0.25f) == XTC_OK && window.undo(true) == XTC_ERR_NOT_FOUND);
CHECK(window.save_project(filename.c_str()) == XTC_OK && window.open_project(filename.c_str()) == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
CHECK(window.undo() == XTC_ERR_NOT_FOUND); // Open resets history.
auto *lane = mixer_ui->child(mixer_ui->children() - 1);
saved_x = Fl::e_x; saved_y = Fl::e_y; saved_key = Fl::e_keysym;
Fl::e_x = lane->x() + (lane->w() - 1) / 4; Fl::e_y = lane->y() + lane->h() / 2;
Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
CHECK(lane->handle(FL_PUSH) == 1);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 3);
Fl::e_keysym = FL_Button + FL_RIGHT_MOUSE;
CHECK(lane->handle(FL_PUSH) == 1);
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
Fl::e_keysym = 'm'; CHECK(mixer_ui->handle(FL_KEYDOWN) == 1);
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.mute);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && !mixed.mute);
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
engine_status.xruns = 3U;
engine_status.processed_blocks = 123U;

View file

@ -81,7 +81,7 @@ int main(int argc, char **argv)
auto *status = find(window, "JACK |");
CHECK(status != nullptr);
unsigned int rate = 0U, frames = 0U, blocks = 0U;
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | mono | %u blocks", &rate, &frames, &blocks) == 3);
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | stereo | %u blocks", &rate, &frames, &blocks) == 3);
CHECK(rate > 0U && frames > 0U && blocks > 0U);
stop->do_callback();
CHECK(find(window, "ready / stopped") != nullptr);

View file

@ -1,5 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_backend.h"
#include "xtc/xtc_mixer.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <errno.h>
@ -9,17 +10,19 @@
#include <time.h>
#include <unistd.h>
typedef struct {
jack_port_t *input, *midi;
unsigned int command, seen, loud, heard, bad;
jack_port_t *input, *right, *midi;
unsigned int command, seen, loud, right_loud, heard, bad;
} probe;
static int process(jack_nframes_t frames, void *context)
{
probe *p = context;
const float *input = jack_port_get_buffer(p->input, frames);
const float *right = jack_port_get_buffer(p->right, frames);
void *midi = jack_port_get_buffer(p->midi, frames);
unsigned int right_loud = 0;
unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0;
jack_nframes_t i;
if (!input || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
if (!input || !right || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
jack_midi_clear_buffer(midi);
if (command && command != p->seen) {
unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0};
@ -30,8 +33,11 @@ static int process(jack_nframes_t frames, void *context)
for (i = 0; i < frames; ++i) {
if (!isfinite(input[i]) || fabsf(input[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
if (fabsf(input[i]) > 0.01f) loud = 1;
if (!isfinite(right[i]) || fabsf(right[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
if (fabsf(right[i]) > 0.01f) right_loud = 1;
}
__atomic_store_n(&p->loud, loud, __ATOMIC_RELAXED);
__atomic_store_n(&p->right_loud, right_loud, __ATOMIC_RELAXED);
if (loud) __atomic_store_n(&p->heard, 1U, __ATOMIC_RELAXED);
return 0;
}
@ -41,7 +47,7 @@ static void wait_blocks(void)
int main(void)
{
char directory[] = "/tmp/xtc-live-midi-XXXXXX", song[256] = "", name[64];
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 1};
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 2};
xtc_backend *backend = NULL;
xtc_project *p = NULL, *q = NULL;
jack_client_t *client = NULL;
@ -60,8 +66,9 @@ int main(void)
REQUIRE(xtc_backend_prepare_project(backend, p) == XTC_OK);
client = jack_client_open("xtc-midi-probe", JackNoStartServer, &flags); REQUIRE(client);
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
state.right = jack_port_register(client, "right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
REQUIRE(state.input && state.midi);
REQUIRE(state.input && state.right && state.midi);
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
@ -72,6 +79,10 @@ int main(void)
REQUIRE(connected == 0 || connected == EEXIST);
}
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
__atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
__atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks();
@ -86,17 +97,23 @@ int main(void)
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_project_add_note(p, clip, 0, 960, 69, 100, 0, &note) == XTC_OK);
REQUIRE(xtc_project_set_mix(p, track, 0.5f, 1, 0, 0) == XTC_OK);
REQUIRE(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
REQUIRE(xtc_backend_prepare_project(backend, q) == XTC_OK);
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
wait_blocks(); REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED) && __atomic_load_n(&state.right_loud, __ATOMIC_RELAXED));
{ xtc_backend_status status; REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_left == 0 && status.peak_right > 0); }
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_backend_seek(backend, 7680000) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
{ const char *own; REQUIRE(xtc_backend_output_name(backend, 1, &own) == XTC_OK);
int connected = jack_connect(client, own, jack_port_name(state.right)); REQUIRE(connected == 0 || connected == EEXIST); }
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(__atomic_load_n(&state.heard, __ATOMIC_RELAXED) && !__atomic_load_n(&state.bad, __ATOMIC_RELAXED));
puts("Live JACK MIDI input/note-off/panic and saved/reopened scheduled instrument playback passed.");
puts("Live JACK MIDI/panic/disconnect and saved stereo gain/pan playback/meters passed.");
result = EXIT_SUCCESS;
cleanup:
if (backend && xtc_backend_close(backend) != XTC_OK) return EXIT_FAILURE;

View file

@ -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, &note) == 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
View 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, &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};
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;
}