1
0
Fork 0
forked from hanez/xtc

Phase 12: Improve audio transitions and verify sample-accurate timing

This commit is contained in:
Johannes Findeisen 2026-10-27 03:37:19 +01:00
commit 05140c2e93
29 changed files with 795 additions and 33 deletions

View file

@ -202,3 +202,13 @@ window, reopens in another window, compares stereo rendering exactly, verifies
playhead/meters and pause/resume/seek/home, and continues editing/saving.
Mock GUI tests inject failed stop/seek/project preparation and recording guards.
See [WORKFLOW.md](../../docs/WORKFLOW.md) for acceptance details and limitations.
## Phase 12 transitions
Backend fault tests verify start/stop ramp samples, repeated requests, callback
completion, silent/held blocks before collection and deactivation error/retry.
GUI tests keep immediate mocked stop semantics; real workflow/live GUI tests
wait for asynchronous fade acknowledgement. `test_quality` in the unit suite
checks exact source envelopes/timing and variable blocks, including tempo
snapshot changes. See [AUDIO_QUALITY.md](../../docs/AUDIO_QUALITY.md) for commands,
latency limitations and the pending listening checklist.

View file

@ -106,6 +106,10 @@ xtc_result xtc_backend_close(xtc_backend *backend)
}
xtc_result xtc_backend_start(xtc_backend *backend)
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) { handle.running = 1; engine_status.active_revision = engine_status.submitted_revision; } return start_result; }
xtc_result xtc_backend_set_transition_frames(xtc_backend *, size_t frames)
{ CHECK(frames == 240); return XTC_OK; }
xtc_result xtc_backend_request_stop(xtc_backend *backend)
{ return xtc_backend_stop(backend); }
xtc_result xtc_backend_stop(xtc_backend *backend)
{ CHECK(backend == &handle); ++stops; if (stop_result == XTC_OK) handle.running = 0; return stop_result; }
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *status)

View file

@ -83,7 +83,7 @@ int main(int argc, char **argv)
unsigned int rate = 0U, frames = 0U, blocks = 0U;
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();
stop->do_callback(); pump();
CHECK(find(window, "ready / stopped") != nullptr);
uint64_t paused = timeline->position(); pump();
CHECK(timeline->position() == paused);
@ -93,7 +93,7 @@ int main(int argc, char **argv)
play->do_callback();
pump();
CHECK(find(window, "running — modular graph") != nullptr);
stop->do_callback();
stop->do_callback(); pump();
char directory[] = "/tmp/xtc-live-record-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
std::string take = std::string(directory) + "/take.wav";
std::string song = std::string(directory) + "/song.xtc";

View file

@ -66,6 +66,8 @@ static std::vector<float> render(const xtc_project *project)
CHECK(xtc_graph_compile(xtc_project_graph(project), 48000, 256, &graph) == XTC_OK);
CHECK(xtc_audio_plan_create(project, 48000, &audio) == XTC_OK);
CHECK(xtc_midi_plan_create(project, 48000, &midi) == XTC_OK);
CHECK(xtc_audio_plan_set_fade_frames(audio, 240) == XTC_OK);
CHECK(xtc_midi_plan_set_fade_frames(midi, 240) == XTC_OK);
std::vector<float> result;
for (uint64_t pos = 0; pos < 96000; pos += 256) {
float al[256], ar[256], ml[256], mr[256], left[256], right[256];
@ -113,7 +115,7 @@ int main(int argc, char **argv)
CHECK(arranged.start_tick == 1920 && arranged.length_tick == 3840);
CHECK(window.import_audio(wave.c_str()) == XTC_OK);
expected = render(timeline->project());
CHECK(expected[0] > 0); // Imported audio is routed without graph-file edits.
CHECK(expected[0] == 0 && expected[2] > 0); // Imported audio is routed without graph-file edits.
CHECK(expected[2 * 48001] != 0); // Arranged notes also reach the output.
CHECK(xtc_graph_get_counts(xtc_project_graph(timeline->project()), &nodes, &edges) == XTC_OK);
CHECK(nodes == 3 && edges == 2); // Audio and instrument -> output.
@ -144,12 +146,13 @@ int main(int argc, char **argv)
auto *right_meter = dynamic_cast<Fl_Progress *>(button(window, "Output R"));
CHECK(left_meter && right_meter && left_meter->value() > 0 && right_meter->value() > 0);
CHECK(window.pause_playback() == XTC_OK);
pump(); // Fade completion is acknowledged by the GUI timer.
CHECK(left_meter->value() == 0 && right_meter->value() == 0);
uint64_t paused = timeline->position(); pump();
CHECK(timeline->position() == paused);
press(window, "Play"); pump();
CHECK(timeline->position() > paused);
press(window, "Stop");
press(window, "Stop"); pump();
paused = timeline->position(); pump(); CHECK(timeline->position() == paused);
press(window, "To start"); CHECK(timeline->position() == 0);
press(window, "Add MIDI clip");

View file

@ -216,6 +216,7 @@ int main(void)
CHECK(xtc_backend_set_recorder(backend, recorder) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_set_recorder(backend, NULL) == XTC_ERR_INVALID_STATE);
CHECK(xtc_backend_request_stop(backend) == XTC_ERR_INVALID_STATE);
CHECK(xtc_backend_prepare_project(backend, p) == XTC_ERR_INVALID_STATE);
for (i = 0; i < 32; ++i) input[i] = i % 2 ? -0.5f : 0.25f;
CHECK(process_cb(32, context) == 0);
@ -318,6 +319,39 @@ int main(void)
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.peak_right == 0);
CHECK(xtc_backend_close(backend) == XTC_OK); xtc_project_destroy(p);
}
{
uint64_t held;
config.channel_count = 2;
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_set_transition_frames(backend, 65537) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_backend_set_transition_frames(backend, 4) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
for (i = 0; i < 128; ++i) input[i] = 1;
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_set_transition_frames(backend, 4) == XTC_ERR_INVALID_STATE);
CHECK(process_cb(4, context) == 0);
CHECK(output[0] == 0.125f && output[3] == 0.5f && output_right[3] == output[3]);
CHECK(xtc_backend_request_stop(backend) == XTC_OK);
CHECK(process_cb(2, context) == 0 && output[0] == 0.375f && output[1] == 0.25f);
CHECK(xtc_backend_request_stop(backend) == XTC_OK); /* Must not restart ramp. */
CHECK(process_cb(2, context) == 0 && output[0] == 0.125f && output[1] == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.running && status.stopping);
held = status.sample_position;
CHECK(process_cb(32, context) == 0 && output[0] == 0 && output[31] == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == held);
deactivate_fail = 1;
xtc_backend_collect(backend);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.running && status.process_result == XTC_ERR_IO);
deactivate_fail = 0;
xtc_backend_collect(backend);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running && !status.stopping);
CHECK(xtc_backend_start(backend) == XTC_OK && process_cb(4, context) == 0 && output[3] == 0.5f);
CHECK(xtc_backend_stop(backend) == XTC_OK); /* Immediate escape stays available. */
CHECK(xtc_backend_set_transition_frames(backend, 0) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK && xtc_backend_request_stop(backend) == XTC_OK);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running);
CHECK(xtc_backend_close(backend) == XTC_OK);
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);

201
tests/unit/test_quality.c Normal file
View file

@ -0,0 +1,201 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_dsp.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_midi.h"
#include <assert.h>
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void timing(void)
{
unsigned int rates[] = {1, 8000, 44100, 48000, 96000, 192000, 768000};
unsigned int tempos[] = {20000, 120000, 123457, 300000};
xtc_project *p = NULL;
size_t r, t;
assert(xtc_project_create(&p) == XTC_OK);
for (r = 0; r < sizeof(rates)/sizeof(*rates); ++r)
for (t = 0; t < sizeof(tempos)/sizeof(*tempos); ++t) {
uint64_t tick, sample, back, expected, previous = 0;
assert(xtc_project_set_tempo(p, tempos[t]) == XTC_OK);
for (tick = 0; tick <= XTC_PROJECT_MAX_TICK; tick += 9973) {
expected = tick * rates[r] * UINT64_C(60000) / ((uint64_t)tempos[t] * 960);
assert(xtc_project_tick_to_sample(p, tick, rates[r], &sample) == XTC_OK);
assert(sample == expected && sample >= previous);
previous = sample;
assert(xtc_project_sample_to_tick(p, sample, rates[r], &back) == XTC_OK);
assert(back == sample * tempos[t] * 960 / ((uint64_t)rates[r] * 60000));
}
assert(xtc_project_tick_to_sample(p, XTC_PROJECT_MAX_TICK, rates[r], &sample) == XTC_OK);
}
{
uint64_t unchanged = 123;
assert(xtc_project_sample_to_tick(p, UINT64_MAX, 1, &unchanged) == XTC_ERR_CAPACITY && unchanged == 123);
assert(xtc_project_set_tempo(p, 0) == XTC_ERR_INVALID_ARGUMENT);
}
xtc_project_destroy(p);
}
static void ramps(void)
{
xtc_gain_ramp a, b;
float whole[257], split[257], right[257];
size_t i;
assert(xtc_gain_ramp_reset(&a, 0) == XTC_OK);
b = a;
assert(xtc_gain_ramp_target(&a, 2, 17) == XTC_OK);
assert(xtc_gain_ramp_target(&b, 2, 17) == XTC_OK);
for (i = 0; i < 257; ++i) whole[i] = split[i] = right[i] = 1;
assert(xtc_gain_ramp_process(&a, whole, right, 257) == XTC_OK);
for (i = 0; i < 257; ++i)
assert(xtc_gain_ramp_process(&b, split + i, NULL, 1) == XTC_OK);
assert(memcmp(whole, split, sizeof(whole)) == 0 && memcmp(whole, right, sizeof(whole)) == 0);
assert(whole[16] == 2 && whole[256] == 2); /* Headroom, no implicit clipping. */
assert(xtc_gain_ramp_target(&a, 0, 3) == XTC_OK);
whole[0] = whole[1] = whole[2] = 1;
assert(xtc_gain_ramp_process(&a, whole, NULL, 3) == XTC_OK && whole[2] == 0);
assert(xtc_gain_ramp_target(&a, NAN, 1) == XTC_ERR_INVALID_ARGUMENT && a.target == 0);
assert(xtc_dsp_finite(NAN) == 0 && xtc_dsp_finite(INFINITY) == 0);
assert(xtc_dsp_finite(nextafterf(0, 1)) == 0 && xtc_dsp_finite(FLT_MIN) == FLT_MIN);
assert(xtc_dsp_finite(4) == 4);
assert(xtc_gain_ramp_reset(&a, 0) == XTC_OK);
assert(xtc_gain_ramp_target(&a, 1, 4) == XTC_OK);
whole[0] = 1;
assert(xtc_gain_ramp_process(&a, whole, NULL, 1) == XTC_OK && whole[0] == 0.25f);
assert(xtc_gain_ramp_target(&a, -0.25f, 2) == XTC_OK);
whole[0] = whole[1] = 1;
assert(xtc_gain_ramp_process(&a, whole, NULL, 2) == XTC_OK && whole[0] == 0 && whole[1] == -0.25f);
assert(xtc_gain_ramp_target(&a, 1, 0) == XTC_OK && a.current == 1);
whole[0] = 9;
assert(xtc_gain_ramp_process(&a, whole, whole, 1) == XTC_ERR_INVALID_ARGUMENT && whole[0] == 9);
}
static void events(void)
{
xtc_project *p = NULL;
xtc_midi_plan *a = NULL, *b = NULL;
xtc_midi_event events[] = {{7, XTC_MIDI_NOTE_ON, 0, 69, 127}, {33, XTC_MIDI_NOTE_OFF, 0, 69, 0}};
float whole[257], split[257], right[257];
size_t i;
assert(xtc_project_create(&p) == XTC_OK);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK && xtc_midi_plan_create(p, 48000, &b) == XTC_OK);
assert(xtc_midi_plan_render_stereo(a, 0, events, 2, whole, right, 257) == XTC_OK);
for (i = 0; i < 257; ++i) {
xtc_midi_event event;
const xtc_midi_event *e = NULL;
if (i == 7 || i == 33) { event = events[i == 7 ? 0 : 1]; event.offset = 0; e = &event; }
assert(xtc_midi_plan_render(b, i, e, e ? 1U : 0U, split + i, 1) == XTC_OK);
}
assert(memcmp(whole, split, sizeof(whole)) == 0 && memcmp(whole, right, sizeof(whole)) == 0);
for (i = 0; i <= 7; ++i) assert(whole[i] == 0);
assert(whole[8] > 0);
for (i = 33; i < 257; ++i) assert(whole[i] == 0);
/* Equal-offset input events retain caller order: off then on starts a voice. */
events[0].offset = events[1].offset = 0;
xtc_midi_plan_reset(a);
assert(xtc_midi_plan_render(a, 0, events, 2, whole, 257) == XTC_OK && whole[1] == 0);
{ xtc_midi_event temp = events[0]; events[0] = events[1]; events[1] = temp; }
xtc_midi_plan_reset(a);
assert(xtc_midi_plan_render(a, 0, events, 2, whole, 257) == XTC_OK && whole[1] > 0);
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b); xtc_project_destroy(p);
}
static void clips(void)
{
xtc_project *p = NULL;
xtc_audio_plan *a = NULL;
xtc_midi_plan *old = NULL, *fresh = NULL;
xtc_recorder *rec = NULL;
uint64_t track, clip, note;
float input[1003], whole[4097], split[4097], midi[4097];
size_t i, offset;
char dir[] = "/tmp/xtc-quality-XXXXXX", path[256];
assert(mkdtemp(dir)); snprintf(path, sizeof(path), "%s/source.wav", dir);
for (i = 0; i < 1003; ++i) input[i] = 1;
assert(xtc_recorder_create(path, 48000, 1003, &rec) == XTC_OK);
xtc_recorder_push(rec, input, 1003);
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
assert(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "Test", &track) == XTC_OK);
assert(xtc_project_import_audio(p, track, path, 1, &clip) == XTC_OK);
assert(xtc_audio_plan_create(p, 48000, &a) == XTC_OK);
assert(xtc_audio_plan_set_fade_frames(a, 8) == XTC_OK);
assert(xtc_audio_plan_render(a, 0, whole, 4097) == XTC_OK);
for (offset = 0; offset < 4097;) {
size_t n = offset % 131 + 1;
if (n > 4097 - offset) n = 4097 - offset;
assert(xtc_audio_plan_render(a, offset, split + offset, n) == XTC_OK);
offset += n;
}
assert(memcmp(whole, split, sizeof(whole)) == 0);
assert(whole[24] == 0 && whole[25] == 0 && whole[26] == 0.125f && whole[33] == 1);
/* Fade at the true media end, before the imported clip's rounded-up tick end. */
assert(whole[1026] == 0.125f && whole[1027] == 0 && whole[1028] == 0);
assert(xtc_audio_plan_render(a, 26, split, 1) == XTC_OK && split[0] == whole[26]);
{
xtc_audio_plan *resampled = NULL;
assert(xtc_audio_plan_create(p, 8000, &resampled) == XTC_OK);
assert(xtc_audio_plan_set_fade_frames(resampled, 8) == XTC_OK);
assert(xtc_audio_plan_render(resampled, 0, split, 200) == XTC_OK);
assert(split[4] == 0 && split[5] == 0.125f && split[170] == 0.125f && split[171] == 0 && split[172] == 0);
xtc_audio_plan_destroy(resampled);
}
assert(xtc_project_add_midi_clip(p, track, 1, 100, &clip) == XTC_OK);
assert(xtc_project_add_note(p, clip, 0, 80, 69, 127, 0, &note) == XTC_OK);
assert(xtc_midi_plan_create(p, 48000, &old) == XTC_OK);
assert(xtc_midi_plan_set_fade_frames(old, 8) == XTC_OK);
assert(xtc_midi_plan_render(old, 0, NULL, 0, whole, 4097) == XTC_OK);
xtc_midi_plan_reset(old);
for (offset = 0; offset < 4097;) {
size_t n = offset % 131 + 1;
if (n > 4097 - offset) n = 4097 - offset;
assert(xtc_midi_plan_render(old, offset, NULL, 0, split + offset, n) == XTC_OK);
offset += n;
}
assert(memcmp(whole, split, sizeof(whole)) == 0);
assert(whole[25] == 0 && whole[26] > 0 && whole[2024] == 0 && whole[2025] == 0);
assert(xtc_project_set_tempo(p, 60000) == XTC_OK);
{
xtc_audio_plan *new_audio = NULL;
assert(xtc_audio_plan_create(p, 48000, &new_audio) == XTC_OK);
assert(xtc_audio_plan_set_fade_frames(new_audio, 8) == XTC_OK);
assert(xtc_audio_plan_render(new_audio, 0, midi, 4097) == XTC_OK);
assert(midi[26] == 0 && midi[50] == 0 && midi[51] == 0.125f);
assert(xtc_audio_plan_render(a, 26, split, 1) == XTC_OK && split[0] == 0.125f);
xtc_audio_plan_destroy(new_audio);
}
assert(xtc_midi_plan_create(p, 48000, &fresh) == XTC_OK);
assert(xtc_midi_plan_render(fresh, 0, NULL, 0, midi, 4097) == XTC_OK);
assert(midi[26] == 0 && midi[51] > 0); /* Newly prepared plan follows new tempo. */
xtc_midi_plan_reset(old);
assert(xtc_midi_plan_render(old, 0, NULL, 0, split, 4097) == XTC_OK);
assert(memcmp(whole, split, sizeof(whole)) == 0); /* Existing snapshot unchanged. */
xtc_audio_plan_destroy(a); xtc_midi_plan_destroy(old); xtc_midi_plan_destroy(fresh); xtc_project_destroy(p);
assert(unlink(path) == 0 && rmdir(dir) == 0);
}
static void layouts(void)
{
size_t sizes[] = {1, 2, 3, 17, 257, 4097, 65535, 65536}, i, j, node;
xtc_graph *g = NULL;
xtc_plan *plan = NULL;
float *l = malloc(65536 * sizeof(float)), *r = malloc(65536 * sizeof(float));
assert(l && r);
assert(xtc_graph_create(1, &g) == XTC_OK && xtc_graph_add_gain(g, 2, &node) == XTC_OK);
assert(xtc_graph_compile(g, 48000, 65536, &plan) == XTC_OK);
for (i = 0; i < sizeof(sizes)/sizeof(*sizes); ++i) {
for (j = 0; j < sizes[i]; ++j) l[j] = j % 3 == 0 ? nextafterf(0, 1) : j % 3 == 1 ? 2 : INFINITY;
assert(xtc_plan_process_stereo(plan, l, NULL, NULL, NULL, NULL, l, r, sizes[i]) == XTC_OK);
for (j = 0; j < sizes[i]; ++j) assert(l[j] == (j % 3 == 1 ? 4 : 0) && r[j] == l[j]);
assert(xtc_plan_process_stereo(plan, NULL, NULL, NULL, NULL, NULL, l, r, sizes[i]) == XTC_OK);
for (j = 0; j < sizes[i]; ++j) assert(l[j] == 0 && r[j] == 0);
}
assert(xtc_plan_process_stereo(plan, NULL, NULL, NULL, NULL, NULL, l, l, 1) == XTC_ERR_INVALID_ARGUMENT);
free(l); free(r); xtc_plan_destroy(plan); xtc_graph_destroy(g);
}
int main(void)
{
timing(); ramps(); events(); clips(); layouts();
puts("Timing, sample-offset events, tempo snapshots, fades, ramps and block invariance passed.");
return 0;
}