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

@ -15,7 +15,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -287,3 +287,10 @@ $(BUILD_DIR)/quality-preview: $(BUILD_DIR)/examples/quality_preview.o $(BUILD_DI
quality-preview: $(BUILD_DIR)/quality-preview
./$(BUILD_DIR)/quality-preview "$(abspath $(BUILD_DIR))/quality"
-include $(BUILD_DIR)/examples/quality_preview.d
$(BUILD_DIR)/test_recording: $(BUILD_DIR)/tests/integration/test_recording.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm
.PHONY: test-recording
test-recording: $(BUILD_DIR)/test_recording
./$(BUILD_DIR)/test_recording
-include $(BUILD_DIR)/tests/integration/test_recording.d

View file

@ -80,7 +80,7 @@ error rather than hiding gaps or compressing time.
Stop first synchronizes the callback, detaches the recorder, drains remaining
samples, writes the final WAV header, flushes/fsyncs/closes and renames the
same-directory temporary over the destination. A successful nonempty take is
same-directory temporary using an atomic no-clobber link; an existing destination is refused. A successful nonempty take is
imported at its capture position on the selected track (or a new Recording
track), then the document becomes unsaved. Save the project after Stop. Seeking
back to the take and pressing Play auditions it. Recording continues existing
@ -92,7 +92,7 @@ until Stop finishes the take. Deactivation failure retains the recorder for a
safe retry. On overflow a finalized partial WAV is retained for manual recovery
and is not automatically added. On import/preparation failure the completed
take remains at its chosen path with an explicit recovery message. I/O errors
are reported; an unfinished temporary is removed on destruction. Recordings
are reported; failed temporary data is retained and its recovery path is reported. Recordings
are RIFF-limited to 1073741814 frames, but the smaller playback/import asset
limit still applies: longer takes may need external splitting before import.
There is no crash-recovery journal. The file is mode 0600; parent-directory
@ -109,3 +109,31 @@ make test-jack-audio routes only two software clients: known source -> recorded
WAV -> saved/reopened project -> graph playback -> capture assertion.
Physical input/output, device latency, slow-disk endurance and server loss
during a real recording remain unverified.
## Phase 13 recording protection
The default recorder and GUI require a new destination, including rejecting
symlinks. Publication also refuses files created during recording. The explicit
`xtc_recorder_create_replace` API retains authorized replacement for callers
that deliberately need it; the GUI never uses it. After a failed finish, copy
`xtc_recorder_recovery_path` before destruction. Disk-write/finalization failures
retain the temporary. Destruction after producer quiescence attempts to drain
and finalize accepted data; normal code should call finish and inspect errors.
The GUI refuses closure while recording and prints the retained/finalized path
on exceptional destruction.
Use `xtc-check --recover-recording TEMP NEW.wav` to copy complete float32 frames
from an interrupted xtc WAV into a new WAV. Source and existing destinations are
never modified. A trailing partial frame is ignored. This handles xtc's canonical
44-byte mono float32 header only, not arbitrary damaged WAVs. Recovery is explicit
and control-thread only; no orphan scan runs during playback.
`make test-recording` simulates 30 minutes at 8 kHz with repeated ring wrapping
and draining, destination races/symlinks, a child-local disk-size limit, prefix
recovery and incomplete/missing asset reopening. `test-recovery` retains process
kill coverage; `test_audio` covers saturation and concurrent drain. This is
virtual-duration testing, not a wall-clock hardware endurance qualification.
Recordings remain RIFF mono float32, bounded to roughly 4 GiB; the preloaded
player still limits each asset to 16M frames. Longer valid takes may require
external splitting before import. No directory fsync/power-loss guarantee is
claimed. WAV destinations are chosen explicitly by the user.

View file

@ -46,3 +46,14 @@ eight project writers while temporaries are visible, and kills a recorder after
draining but before finalization. Complete old/new destinations and explicit
orphan temporaries are verified. It does not cut power, terminate the shared
audio server, simulate a failing filesystem, or qualify disk/device endurance.
## Phase 13 retained recording prefixes
Recording now defaults to no-clobber publication; the GUI requires an unused
path. Failed nonempty temporary recordings survive destruction. Query the
recorder recovery path before destruction, or use the GUI's reported path.
`xtc-check --recover-recording TEMP NEW.wav` reconstructs a canonical mono
float32 WAV from complete on-disk frames, ignoring a trailing partial frame.
It never changes the source or overwrites an existing destination. This
supersedes the earlier lack of a WAV recovery tool described above. See
AUDIO_FILES.md for format/ownership/size limits and `make test-recording`.

View file

@ -233,6 +233,15 @@ limiter or project-format change is claimed.
- Incomplete recordings and missing media are detected and explained.
- Recording and recovery behavior is covered by automated or repeatable integration tests.
Implemented: no-clobber recording destinations and race-safe publication;
explicit opt-in replacement API; retained failed temporaries and recovery-path
reporting; streaming `xtc-check --recover-recording TEMP NEW.wav` salvage;
quiescent destructor draining and exceptional-shutdown reporting. Virtual
30-minute recording, saturation/concurrency, destination races, disk-write
failure/prefix recovery, killed writers and missing/incomplete media tests pass.
See AUDIO_FILES.md and RECOVERY.md. Physical endurance, power-loss guarantees
and playback of assets above the existing preload limit remain unqualified.
## Phase 14 — Native Plugin, Instrument, and Effect Architecture
**Goal:** Provide a stable internal extension interface before hosting external plugins.

View file

@ -27,7 +27,7 @@ static xtc_result preview(const char *prefix, int smooth)
if (result == XTC_OK) result = xtc_project_add_note(project, clip, 1440, 1440, 67, 100, 0, &note);
if (result == XTC_OK) result = xtc_midi_plan_create(project, 48000, &midi);
if (result == XTC_OK) result = xtc_midi_plan_set_fade_frames(midi, smooth ? 240U : 0U);
if (result == XTC_OK) result = xtc_recorder_create(path, 48000, 256, &recorder);
if (result == XTC_OK) result = xtc_recorder_create_replace(path, 48000, 256, 1, &recorder);
(void)xtc_gain_ramp_reset(&ramp, smooth ? 0 : 1);
(void)xtc_gain_ramp_target(&ramp, 1, smooth ? 240U : 0U);
for (position = 0; result == XTC_OK && position < 192000; position += 256) {

View file

@ -95,7 +95,7 @@ xtc_result xtc_audio_plan_render(const xtc_audio_plan *plan, uint64_t position,
* Ring capacity 1..2097152 frames. One callback producer, one control consumer.
* push accepts whole blocks; overflow is sticky and all later input is dropped.
* drain performs file I/O; call regularly. finish requires producer quiescence,
* drains, finalizes and atomically renames even on overflow (partial recovery).
* drains, finalizes and publishes without overwriting even on overflow (partial recovery).
* No source/recorder is freed until destroy, which also removes unfinished temp.
* Counts and sticky error can be observed while producing. */
/**
@ -139,11 +139,23 @@ xtc_result xtc_recorder_finish(xtc_recorder *recorder);
*/
xtc_result xtc_recorder_status(const xtc_recorder *recorder, uint64_t *out_frames);
/**
* @brief Releases recorder storage and removes any unfinished temporary file; accepts NULL.
* @brief Finalizes pending accepted frames after producer quiescence; preserves failed temporaries.
*
* @param recorder Recorder handle.
*/
void xtc_recorder_destroy(xtc_recorder *recorder);
/* Default create refuses any existing destination (including symlinks). Explicit
* replacement retains the old behavior for callers that obtained authorization.
* Final publication is race-safe: no-clobber link, or explicit replace/rename. */
xtc_result xtc_recorder_create_replace(const char *path, unsigned int rate, size_t capacity, int replace,
xtc_recorder **out_recorder);
/* Control-thread borrowed path. After a failed finish, copy before destroy.
* Useful temporary data survives destroy on failed finalization. */
const char *xtc_recorder_recovery_path(const xtc_recorder *recorder);
/* Salvage complete mono float32 frames from xtc's interrupted recording format
* to a NEW destination. Never modifies the source or overwrites an existing file. */
xtc_result xtc_recorder_recover(const char *temporary, const char *destination);
#ifdef __cplusplus
}

View file

@ -4,6 +4,8 @@
#include <float.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
struct xtc_recorder {
float *ring;
@ -12,7 +14,7 @@ struct xtc_recorder {
unsigned int error, rate;
FILE *file;
char *path, *temporary;
int finished;
int finished, replace, committed, preserve;
};
/**
@ -24,7 +26,7 @@ struct xtc_recorder {
* @param out Receives the newly allocated object; caller owns it on success.
* @return XTC_OK on success, or an error code describing the failure.
*/
xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out)
xtc_result xtc_recorder_create_replace(const char *path, unsigned int rate, size_t capacity, int replace, xtc_recorder **out)
{
xtc_recorder *r;
size_t length = 0;
@ -32,7 +34,7 @@ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capac
if (!out)
return XTC_ERR_INVALID_ARGUMENT;
*out = NULL;
if (!path || !rate || rate > 768000 || !capacity || capacity > 2097152U)
if ((replace != 0 && replace != 1) || !path || !rate || rate > 768000 || !capacity || capacity > 2097152U)
return XTC_ERR_INVALID_ARGUMENT;
while (length < 4096 && path[length])
++length;
@ -42,9 +44,14 @@ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capac
return XTC_ERR_UNSUPPORTED;
if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0))
return XTC_ERR_UNSUPPORTED;
if (!replace) {
struct stat info;
if (lstat(path, &info) == 0 || errno != ENOENT) return XTC_ERR_IO;
}
r = calloc(1, sizeof(*r));
if (!r)
return XTC_ERR_OUT_OF_MEMORY;
r->replace = replace;
r->capacity = capacity;
r->rate = rate;
r->ring = malloc(capacity * sizeof(float));
@ -166,8 +173,15 @@ xtc_result xtc_recorder_finish(xtc_recorder *r)
if (fclose(r->file))
failed = 1;
r->file = NULL;
if (!failed && rename(r->temporary, r->path))
failed = 1;
if (!failed) {
if (r->replace ? rename(r->temporary, r->path) : link(r->temporary, r->path))
failed = 1;
else {
r->committed = 1;
if (!r->replace) (void)unlink(r->temporary);
}
}
r->preserve = failed && (__atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE) != 0 || r->written != 0);
r->finished = 1;
return failed ? XTC_ERR_IO : result;
}
@ -181,12 +195,36 @@ void xtc_recorder_destroy(xtc_recorder *r)
{
if (!r)
return;
if (r->file)
fclose(r->file);
if (r->temporary)
if (r->file) {
if (__atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE))
(void)xtc_recorder_finish(r); /* Producer must already be quiescent. */
else { fclose(r->file); r->file = NULL; }
}
if (r->temporary && !r->preserve)
unlink(r->temporary);
free(r->temporary);
free(r->path);
free(r->ring);
free(r);
}
xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capacity, xtc_recorder **out)
{
return xtc_recorder_create_replace(path, rate, capacity, 0, out);
}
const char *xtc_recorder_recovery_path(const xtc_recorder *r)
{
if (!r) return "";
return r->committed ? r->path : r->temporary ? r->temporary : "";
}
/* Internal cancellation for a failed recovery transaction; never removes the
* destination, which may have been created by another process. */
void xtc_recorder_abort(xtc_recorder *r)
{
if (!r) return;
if (r->file) fclose(r->file);
r->file = NULL;
r->finished = 1;
r->preserve = 0;
}

50
src/audio/recovery.c Normal file
View file

@ -0,0 +1,50 @@
#define _POSIX_C_SOURCE 200809L
#include "wave_internal.h"
#include <math.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static uint32_t little(const unsigned char *p)
{ return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; }
xtc_result xtc_recorder_recover(const char *source, const char *destination)
{
unsigned char header[44], bytes[1024];
struct stat info;
FILE *file;
xtc_recorder *recorder = NULL;
xtc_result result = XTC_ERR_FORMAT;
uint64_t remaining;
unsigned int rate;
if (!source || !destination) return XTC_ERR_INVALID_ARGUMENT;
file = fopen(source, "rb");
if (!file) return XTC_ERR_IO;
if (fstat(fileno(file), &info) || !S_ISREG(info.st_mode) || info.st_size < 48 ||
(uint64_t)info.st_size > UINT64_C(4294967292) || fread(header, 1, 44, file) != 44) goto done;
rate = little(header + 24);
if (memcmp(header, "RIFF", 4) || memcmp(header + 8, "WAVEfmt ", 8) || little(header + 16) != 16 ||
header[20] != 3 || header[21] || header[22] != 1 || header[23] || !rate || rate > 768000 ||
little(header + 28) != rate * 4 || header[32] != 4 || header[33] || header[34] != 32 || header[35] ||
memcmp(header + 36, "data", 4)) goto done;
remaining = ((uint64_t)info.st_size - 44) / 4;
result = xtc_recorder_create(destination, rate, 256, &recorder);
while (result == XTC_OK && remaining) {
float samples[256];
size_t i, count = remaining > 256 ? 256 : (size_t)remaining;
if (fread(bytes, 4, count, file) != count) { result = XTC_ERR_IO; break; }
for (i = 0; i < count; ++i) {
uint32_t bits = little(bytes + i * 4);
memcpy(samples + i, &bits, sizeof(bits));
if (!isfinite(samples[i])) { result = XTC_ERR_FORMAT; break; }
}
if (result != XTC_OK) break;
xtc_recorder_push(recorder, samples, count);
result = xtc_recorder_drain(recorder);
remaining -= count;
}
if (result == XTC_OK) result = xtc_recorder_finish(recorder);
if (result != XTC_OK) xtc_recorder_abort(recorder);
xtc_recorder_destroy(recorder);
done:
if (fclose(file) && result == XTC_OK) result = XTC_ERR_IO;
return result;
}

View file

@ -36,4 +36,5 @@ int xtc_wave_header(FILE *file, unsigned int rate, uint32_t frames);
* @return Nonzero if all samples were written, zero on a write failure.
*/
int xtc_wave_write(FILE *file, const float *input, size_t frames);
void xtc_recorder_abort(xtc_recorder *recorder);
#endif

View file

@ -282,7 +282,9 @@ MainWindow::~MainWindow()
std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n",
stderr);
else if (recorder_) {
(void)xtc_recorder_finish(recorder_);
xtc_result result = xtc_recorder_finish(recorder_);
std::fprintf(stderr, "Recording finalized during shutdown (%s): %s\n", xtc_result_string(result),
xtc_recorder_recovery_path(recorder_));
xtc_recorder_destroy(recorder_);
}
}
@ -1091,13 +1093,14 @@ void MainWindow::finish_recording()
uint64_t frames = 0;
(void)xtc_recorder_status(recorder_, &frames);
xtc_result r = xtc_recorder_finish(recorder_);
std::string recovery_path = xtc_recorder_recovery_path(recorder_);
xtc_recorder_destroy(recorder_);
recorder_ = nullptr;
if (r != XTC_OK) {
char text[768];
std::snprintf(text, sizeof(text),
"Recording failed (%s). Partial take, if finalized: %s. It was not added to the project.",
xtc_result_string(r), recording_path_.c_str());
"Recording failed (%s). Retained take/temporary: %s. Recover with xtc-check --recover-recording. Not added.",
xtc_result_string(r), recovery_path.c_str());
set_label(error_, text);
return;
}
@ -1155,8 +1158,7 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context)
chooser.title(recording ? "Record JACK input to WAV" : relink ? "Relink selected clip" : "Import WAV");
chooser.type(recording ? Fl_Native_File_Chooser::BROWSE_SAVE_FILE : Fl_Native_File_Chooser::BROWSE_FILE);
chooser.filter("WAV audio\t*.wav");
if (recording)
chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM);
// Recordings require a new destination; existing files are never replaced.
int chosen = chooser.show();
if (chosen == 1)
return;
@ -1173,7 +1175,8 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context)
} else if (r == XTC_OK)
r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename());
if (r != XTC_OK)
window->report_error("Project audio operation failed", r);
window->report_error(recording ? "Project recording failed; choose a new WAV path in a writable directory"
: "Project audio operation failed", r);
}
/**

View file

@ -5,6 +5,7 @@
#include <stdlib.h>
#include <string.h>
#include "xtc/xtc_project.h"
#include "xtc/xtc_audio.h"
/**
* @brief Runs the xtc-check command-line entry point.
@ -25,10 +26,20 @@ int main(int argc, char **argv)
return EXIT_SUCCESS;
}
if (argc == 2 && !strcmp(argv[1], "--help")) {
puts("Usage: xtc-check [--version | --help | --validate PROJECT]\nNo arguments checks core startup/shutdown. "
puts("Usage: xtc-check [--version | --help | --validate PROJECT | --recover-recording TEMP NEW.wav]\nNo arguments checks core startup/shutdown. "
"Validation checks metadata only.");
return EXIT_SUCCESS;
}
if (argc == 4 && !strcmp(argv[1], "--recover-recording")) {
xtc_result result = xtc_recorder_recover(argv[2], argv[3]);
if (result != XTC_OK) {
fprintf(stderr, "Recording recovery failed: %s. Source retained; choose a new writable destination.\n",
xtc_result_string(result));
return EXIT_FAILURE;
}
puts("Complete recorded frames recovered; source retained.");
return EXIT_SUCCESS;
}
if (argc == 3 && !strcmp(argv[1], "--validate")) {
xtc_project *project = NULL;
xtc_result result = xtc_project_create(&project);
@ -43,7 +54,7 @@ int main(int argc, char **argv)
return EXIT_SUCCESS;
}
if (argc != 1) {
fputs("Usage: xtc-check [--version | --help | --validate PROJECT]\n", stderr);
fputs("Usage: xtc-check [--version | --help | --validate PROJECT | --recover-recording TEMP NEW.wav]\n", stderr);
return EXIT_FAILURE;
}

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. */