1
0
Fork 0
forked from hanez/xtc

Phase 13: protect recordings and recover retained audio after write failures

This commit is contained in:
Johannes Findeisen 2026-10-27 03:46:09 +01:00
commit 01d9db90fd
15 changed files with 308 additions and 30 deletions

View file

@ -0,0 +1,108 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_audio.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
static void file(const char *path, const char *text)
{ FILE *f = fopen(path, "wb"); assert(f); assert(fputs(text, f) >= 0 && fclose(f) == 0); }
int main(void)
{
char dir[] = "/tmp/xtc-recording-XXXXXX", take[256], recovered[256], retained[4096], song[256];
xtc_recorder *r = NULL;
float samples[4096];
struct stat info;
uint64_t position;
size_t i;
assert(mkdtemp(dir));
snprintf(take, sizeof(take), "%s/take.wav", dir);
snprintf(recovered, sizeof(recovered), "%s/recovered.wav", dir);
snprintf(song, sizeof(song), "%s/song.xtc", dir);
for (i = 0; i < 4096; ++i) samples[i] = 0.25f;
file(take, "existing");
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_ERR_IO && !r);
assert(unlink(take) == 0 && symlink("missing", take) == 0);
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_ERR_IO && !r);
assert(unlink(take) == 0);
assert(xtc_recorder_create("/missing/xtc/take.wav", 8000, 4096, &r) == XTC_ERR_IO && !r);
/* Destination appears after creation: no-clobber publication must preserve it. */
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_OK);
xtc_recorder_push(r, samples, 4096);
file(take, "racing writer");
assert(xtc_recorder_finish(r) == XTC_ERR_IO);
snprintf(retained, sizeof(retained), "%s", xtc_recorder_recovery_path(r));
xtc_recorder_destroy(r);
assert(stat(take, &info) == 0 && info.st_size == 13);
assert(xtc_recorder_recover(retained, take) == XTC_ERR_IO);
assert(xtc_recorder_recover(retained, recovered) == XTC_OK);
assert(stat(recovered, &info) == 0 && info.st_size == 44 + 4096 * 4);
assert(unlink(retained) == 0 && unlink(recovered) == 0 && unlink(take) == 0);
/* Last-resort destruction drains accepted samples after producer quiescence. */
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_OK);
xtc_recorder_push(r, samples, 4096); xtc_recorder_destroy(r);
assert(stat(take, &info) == 0 && info.st_size == 44 + 4096 * 4);
assert(unlink(take) == 0);
/* Thirty minutes of virtual 8 kHz audio, repeated ring wrap/drain, no sleep. */
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_OK);
for (position = 0; position < 14400000;) {
size_t count = 14400000 - position > 4096 ? 4096 : (size_t)(14400000 - position);
xtc_recorder_push(r, samples, count);
assert(xtc_recorder_drain(r) == XTC_OK);
position += count;
}
assert(xtc_recorder_finish(r) == XTC_OK); xtc_recorder_destroy(r);
assert(stat(take, &info) == 0 && info.st_size == 57600044);
assert(unlink(take) == 0);
/* Child-local disk-size failure preserves a recoverable on-disk prefix. */
{
int pipes[2], status;
pid_t child;
assert(pipe(pipes) == 0);
child = fork(); assert(child >= 0);
if (!child) {
struct rlimit limit = {128, 128};
close(pipes[0]); signal(SIGXFSZ, SIG_IGN);
assert(setrlimit(RLIMIT_FSIZE, &limit) == 0);
assert(xtc_recorder_create(take, 8000, 4096, &r) == XTC_OK);
xtc_recorder_push(r, samples, 4096);
(void)xtc_recorder_drain(r);
assert(xtc_recorder_finish(r) == XTC_ERR_IO);
snprintf(retained, sizeof(retained), "%s", xtc_recorder_recovery_path(r));
xtc_recorder_destroy(r);
assert(write(pipes[1], retained, sizeof(retained)) == (ssize_t)sizeof(retained));
_exit(0);
}
close(pipes[1]);
assert(read(pipes[0], retained, sizeof(retained)) == (ssize_t)sizeof(retained)); close(pipes[0]);
assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
assert(access(take, F_OK) != 0 && stat(retained, &info) == 0 && info.st_size == 128);
assert(xtc_recorder_recover(retained, recovered) == XTC_OK);
assert(stat(recovered, &info) == 0 && info.st_size == 128);
assert(unlink(retained) == 0 && unlink(recovered) == 0);
}
/* Incomplete and missing references survive save/reopen and are reported. */
{
xtc_project *p = NULL;
xtc_audio_plan *plan = NULL;
uint64_t track, clip;
const char *issue;
assert(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "Incomplete", &track) == XTC_OK);
assert(xtc_project_add_clip(p, track, "Broken take", 0, 960, &clip) == XTC_OK);
assert(xtc_project_set_clip_asset(p, clip, take) == XTC_OK);
file(take, "RIFF incomplete");
assert(xtc_project_save(p, song) == XTC_OK && xtc_project_load(p, song) == XTC_OK);
assert(xtc_audio_plan_create(p, 8000, &plan) == XTC_OK && xtc_audio_plan_issues(plan, &issue) == 1);
assert(!strcmp(issue, take)); xtc_audio_plan_destroy(plan);
assert(unlink(take) == 0);
assert(xtc_audio_plan_create(p, 8000, &plan) == XTC_OK && xtc_audio_plan_issues(plan, &issue) == 1);
xtc_audio_plan_destroy(plan); xtc_project_destroy(p);
assert(unlink(song) == 0);
}
assert(rmdir(dir) == 0);
puts("30-minute virtual recording, no-clobber races, disk failure recovery and incomplete/missing media passed.");
}

View file

@ -115,7 +115,7 @@ int main(void)
float samples[128];
for (i = 0; i < 128; ++i)
samples[i] = 0.25f;
assert(xtc_recorder_create(wave, 48000, 256, &recorder) == XTC_OK);
assert(xtc_recorder_create_replace(wave, 48000, 256, 1, &recorder) == XTC_OK);
xtc_recorder_push(recorder, samples, 128);
assert(xtc_recorder_finish(recorder) == XTC_OK);
xtc_recorder_destroy(recorder);
@ -125,7 +125,7 @@ int main(void)
assert(child >= 0);
if (!child) {
close(pipefd[0]);
if (xtc_recorder_create(wave, 48000, 256, &recorder) != XTC_OK)
if (xtc_recorder_create_replace(wave, 48000, 256, 1, &recorder) != XTC_OK)
_exit(6);
xtc_recorder_push(recorder, samples, 128);
if (xtc_recorder_drain(recorder) != XTC_OK)

View file

@ -90,7 +90,7 @@ int main(void)
}
/* Recording ring wraps, exact samples survive WAV -> project -> reopen. */
for (i = 0; i < 64; ++i) input[i] = i % 2 ? -0.5f : 0.25f;
assert(xtc_recorder_create(take, 48000, 128, &rec) == XTC_OK);
assert(xtc_recorder_create_replace(take, 48000, 128, 1, &rec) == XTC_OK);
for (i = 0; i < 10; ++i) {
xtc_recorder_push(rec, input, 64);
assert(xtc_recorder_drain(rec) == XTC_OK);
@ -137,18 +137,18 @@ int main(void)
}
xtc_project_destroy(p); xtc_project_destroy(q);
/* Overflow is sticky, partial file recoverable, destination unchanged until finish. */
assert(xtc_recorder_create(take, 48000, 64, &rec) == XTC_OK);
assert(xtc_recorder_create_replace(take, 48000, 64, 1, &rec) == XTC_OK);
xtc_recorder_push(rec, input, 64); xtc_recorder_push(rec, input, 1);
assert(xtc_recorder_status(rec, &frames) == XTC_ERR_CAPACITY && frames == 64);
assert(xtc_recorder_finish(rec) == XTC_ERR_CAPACITY); xtc_recorder_destroy(rec);
assert(xtc_recorder_create(take, 48000, 64, &rec) == XTC_OK);
assert(xtc_recorder_create_replace(take, 48000, 64, 1, &rec) == XTC_OK);
xtc_recorder_push(rec, NULL, 1);
assert(xtc_recorder_status(rec, &frames) == XTC_ERR_IO);
xtc_recorder_destroy(rec);
/* Actual concurrent producer/control consumer; no GUI or JACK required. */
{
stress s = {0}; pthread_t thread;
assert(xtc_recorder_create(take, 48000, 524288, &rec) == XTC_OK); s.r = rec;
assert(xtc_recorder_create_replace(take, 48000, 524288, 1, &rec) == XTC_OK); s.r = rec;
assert(pthread_create(&thread, NULL, producer, &s) == 0);
while (!__atomic_load_n(&s.complete, __ATOMIC_ACQUIRE)) assert(xtc_recorder_drain(rec) == XTC_OK);
assert(pthread_join(thread, NULL) == 0);

View file

@ -212,7 +212,7 @@ int main(void)
CHECK(xtc_project_add_track(p, "Take", &track) == XTC_OK);
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_prepare_project(backend, p) == XTC_OK);
CHECK(xtc_recorder_create(take, 48000, 256, &recorder) == XTC_OK);
CHECK(xtc_recorder_create_replace(take, 48000, 256, 1, &recorder) == XTC_OK);
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);
@ -229,7 +229,7 @@ int main(void)
CHECK(xtc_recorder_finish(recorder) == XTC_OK);
CHECK(xtc_recorder_status(recorder, &captured) == XTC_OK && captured == 64);
xtc_recorder_destroy(recorder);
CHECK(xtc_recorder_create(take, 48000, 256, &recorder) == XTC_OK);
CHECK(xtc_recorder_create_replace(take, 48000, 256, 1, &recorder) == XTC_OK);
CHECK(xtc_backend_set_recorder(backend, recorder) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
null_buffer = 1; CHECK(process_cb(32, context) == 0); null_buffer = 0;
CHECK(process_cb(32, context) == 0); /* Never silently resume after missing callback data. */