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 CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?= 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) JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes) ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack) 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 quality-preview: $(BUILD_DIR)/quality-preview
./$(BUILD_DIR)/quality-preview "$(abspath $(BUILD_DIR))/quality" ./$(BUILD_DIR)/quality-preview "$(abspath $(BUILD_DIR))/quality"
-include $(BUILD_DIR)/examples/quality_preview.d -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 Stop first synchronizes the callback, detaches the recorder, drains remaining
samples, writes the final WAV header, flushes/fsyncs/closes and renames the 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 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 track), then the document becomes unsaved. Save the project after Stop. Seeking
back to the take and pressing Play auditions it. Recording continues existing 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 safe retry. On overflow a finalized partial WAV is retained for manual recovery
and is not automatically added. On import/preparation failure the completed 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 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 are RIFF-limited to 1073741814 frames, but the smaller playback/import asset
limit still applies: longer takes may need external splitting before import. limit still applies: longer takes may need external splitting before import.
There is no crash-recovery journal. The file is mode 0600; parent-directory 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. WAV -> saved/reopened project -> graph playback -> capture assertion.
Physical input/output, device latency, slow-disk endurance and server loss Physical input/output, device latency, slow-disk endurance and server loss
during a real recording remain unverified. 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 draining but before finalization. Complete old/new destinations and explicit
orphan temporaries are verified. It does not cut power, terminate the shared orphan temporaries are verified. It does not cut power, terminate the shared
audio server, simulate a failing filesystem, or qualify disk/device endurance. 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. - Incomplete recordings and missing media are detected and explained.
- Recording and recovery behavior is covered by automated or repeatable integration tests. - 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 ## Phase 14 — Native Plugin, Instrument, and Effect Architecture
**Goal:** Provide a stable internal extension interface before hosting external plugins. **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_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_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_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_reset(&ramp, smooth ? 0 : 1);
(void)xtc_gain_ramp_target(&ramp, 1, smooth ? 240U : 0U); (void)xtc_gain_ramp_target(&ramp, 1, smooth ? 240U : 0U);
for (position = 0; result == XTC_OK && position < 192000; position += 256) { 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. * Ring capacity 1..2097152 frames. One callback producer, one control consumer.
* push accepts whole blocks; overflow is sticky and all later input is dropped. * push accepts whole blocks; overflow is sticky and all later input is dropped.
* drain performs file I/O; call regularly. finish requires producer quiescence, * 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. * No source/recorder is freed until destroy, which also removes unfinished temp.
* Counts and sticky error can be observed while producing. */ * 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); 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. * @param recorder Recorder handle.
*/ */
void xtc_recorder_destroy(xtc_recorder *recorder); 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 #ifdef __cplusplus
} }

View file

@ -4,6 +4,8 @@
#include <float.h> #include <float.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
struct xtc_recorder { struct xtc_recorder {
float *ring; float *ring;
@ -12,7 +14,7 @@ struct xtc_recorder {
unsigned int error, rate; unsigned int error, rate;
FILE *file; FILE *file;
char *path, *temporary; 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. * @param out Receives the newly allocated object; caller owns it on success.
* @return XTC_OK on success, or an error code describing the failure. * @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; xtc_recorder *r;
size_t length = 0; size_t length = 0;
@ -32,7 +34,7 @@ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capac
if (!out) if (!out)
return XTC_ERR_INVALID_ARGUMENT; return XTC_ERR_INVALID_ARGUMENT;
*out = NULL; *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; return XTC_ERR_INVALID_ARGUMENT;
while (length < 4096 && path[length]) while (length < 4096 && path[length])
++length; ++length;
@ -42,9 +44,14 @@ xtc_result xtc_recorder_create(const char *path, unsigned int rate, size_t capac
return XTC_ERR_UNSUPPORTED; return XTC_ERR_UNSUPPORTED;
if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0)) if (!__atomic_always_lock_free(sizeof(uint64_t), 0) || !__atomic_always_lock_free(sizeof(unsigned int), 0))
return XTC_ERR_UNSUPPORTED; 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)); r = calloc(1, sizeof(*r));
if (!r) if (!r)
return XTC_ERR_OUT_OF_MEMORY; return XTC_ERR_OUT_OF_MEMORY;
r->replace = replace;
r->capacity = capacity; r->capacity = capacity;
r->rate = rate; r->rate = rate;
r->ring = malloc(capacity * sizeof(float)); r->ring = malloc(capacity * sizeof(float));
@ -166,8 +173,15 @@ xtc_result xtc_recorder_finish(xtc_recorder *r)
if (fclose(r->file)) if (fclose(r->file))
failed = 1; failed = 1;
r->file = NULL; r->file = NULL;
if (!failed && rename(r->temporary, r->path)) if (!failed) {
failed = 1; 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; r->finished = 1;
return failed ? XTC_ERR_IO : result; return failed ? XTC_ERR_IO : result;
} }
@ -181,12 +195,36 @@ void xtc_recorder_destroy(xtc_recorder *r)
{ {
if (!r) if (!r)
return; return;
if (r->file) if (r->file) {
fclose(r->file); if (__atomic_load_n(&r->write_position, __ATOMIC_ACQUIRE))
if (r->temporary) (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); unlink(r->temporary);
free(r->temporary); free(r->temporary);
free(r->path); free(r->path);
free(r->ring); free(r->ring);
free(r); 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. * @return Nonzero if all samples were written, zero on a write failure.
*/ */
int xtc_wave_write(FILE *file, const float *input, size_t frames); int xtc_wave_write(FILE *file, const float *input, size_t frames);
void xtc_recorder_abort(xtc_recorder *recorder);
#endif #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", std::fputs("Failed to close JACK during GUI destruction; backend and recording retained until process exit.\n",
stderr); stderr);
else if (recorder_) { 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_); xtc_recorder_destroy(recorder_);
} }
} }
@ -1091,13 +1093,14 @@ void MainWindow::finish_recording()
uint64_t frames = 0; uint64_t frames = 0;
(void)xtc_recorder_status(recorder_, &frames); (void)xtc_recorder_status(recorder_, &frames);
xtc_result r = xtc_recorder_finish(recorder_); xtc_result r = xtc_recorder_finish(recorder_);
std::string recovery_path = xtc_recorder_recovery_path(recorder_);
xtc_recorder_destroy(recorder_); xtc_recorder_destroy(recorder_);
recorder_ = nullptr; recorder_ = nullptr;
if (r != XTC_OK) { if (r != XTC_OK) {
char text[768]; char text[768];
std::snprintf(text, sizeof(text), std::snprintf(text, sizeof(text),
"Recording failed (%s). Partial take, if finalized: %s. It was not added to the project.", "Recording failed (%s). Retained take/temporary: %s. Recover with xtc-check --recover-recording. Not added.",
xtc_result_string(r), recording_path_.c_str()); xtc_result_string(r), recovery_path.c_str());
set_label(error_, text); set_label(error_, text);
return; 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.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.type(recording ? Fl_Native_File_Chooser::BROWSE_SAVE_FILE : Fl_Native_File_Chooser::BROWSE_FILE);
chooser.filter("WAV audio\t*.wav"); chooser.filter("WAV audio\t*.wav");
if (recording) // Recordings require a new destination; existing files are never replaced.
chooser.options(Fl_Native_File_Chooser::SAVEAS_CONFIRM);
int chosen = chooser.show(); int chosen = chooser.show();
if (chosen == 1) if (chosen == 1)
return; return;
@ -1173,7 +1175,8 @@ void MainWindow::audio_file_callback(Fl_Widget *widget, void *context)
} else if (r == XTC_OK) } else if (r == XTC_OK)
r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename()); r = recording ? window->record_audio(chooser.filename()) : window->import_audio(chooser.filename());
if (r != XTC_OK) 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 <stdlib.h>
#include <string.h> #include <string.h>
#include "xtc/xtc_project.h" #include "xtc/xtc_project.h"
#include "xtc/xtc_audio.h"
/** /**
* @brief Runs the xtc-check command-line entry point. * @brief Runs the xtc-check command-line entry point.
@ -25,10 +26,20 @@ int main(int argc, char **argv)
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
if (argc == 2 && !strcmp(argv[1], "--help")) { 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."); "Validation checks metadata only.");
return EXIT_SUCCESS; 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")) { if (argc == 3 && !strcmp(argv[1], "--validate")) {
xtc_project *project = NULL; xtc_project *project = NULL;
xtc_result result = xtc_project_create(&project); xtc_result result = xtc_project_create(&project);
@ -43,7 +54,7 @@ int main(int argc, char **argv)
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
if (argc != 1) { 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; 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]; float samples[128];
for (i = 0; i < 128; ++i) for (i = 0; i < 128; ++i)
samples[i] = 0.25f; 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); xtc_recorder_push(recorder, samples, 128);
assert(xtc_recorder_finish(recorder) == XTC_OK); assert(xtc_recorder_finish(recorder) == XTC_OK);
xtc_recorder_destroy(recorder); xtc_recorder_destroy(recorder);
@ -125,7 +125,7 @@ int main(void)
assert(child >= 0); assert(child >= 0);
if (!child) { if (!child) {
close(pipefd[0]); 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); _exit(6);
xtc_recorder_push(recorder, samples, 128); xtc_recorder_push(recorder, samples, 128);
if (xtc_recorder_drain(recorder) != XTC_OK) 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. */ /* Recording ring wraps, exact samples survive WAV -> project -> reopen. */
for (i = 0; i < 64; ++i) input[i] = i % 2 ? -0.5f : 0.25f; 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) { for (i = 0; i < 10; ++i) {
xtc_recorder_push(rec, input, 64); xtc_recorder_push(rec, input, 64);
assert(xtc_recorder_drain(rec) == XTC_OK); assert(xtc_recorder_drain(rec) == XTC_OK);
@ -137,18 +137,18 @@ int main(void)
} }
xtc_project_destroy(p); xtc_project_destroy(q); xtc_project_destroy(p); xtc_project_destroy(q);
/* Overflow is sticky, partial file recoverable, destination unchanged until finish. */ /* 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); 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_status(rec, &frames) == XTC_ERR_CAPACITY && frames == 64);
assert(xtc_recorder_finish(rec) == XTC_ERR_CAPACITY); xtc_recorder_destroy(rec); 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); xtc_recorder_push(rec, NULL, 1);
assert(xtc_recorder_status(rec, &frames) == XTC_ERR_IO); assert(xtc_recorder_status(rec, &frames) == XTC_ERR_IO);
xtc_recorder_destroy(rec); xtc_recorder_destroy(rec);
/* Actual concurrent producer/control consumer; no GUI or JACK required. */ /* Actual concurrent producer/control consumer; no GUI or JACK required. */
{ {
stress s = {0}; pthread_t thread; 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); assert(pthread_create(&thread, NULL, producer, &s) == 0);
while (!__atomic_load_n(&s.complete, __ATOMIC_ACQUIRE)) assert(xtc_recorder_drain(rec) == XTC_OK); while (!__atomic_load_n(&s.complete, __ATOMIC_ACQUIRE)) assert(xtc_recorder_drain(rec) == XTC_OK);
assert(pthread_join(thread, NULL) == 0); 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_project_add_track(p, "Take", &track) == XTC_OK);
CHECK(xtc_backend_open(&config, &backend) == XTC_OK); CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_prepare_project(backend, p) == 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_set_recorder(backend, recorder) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK); CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_set_recorder(backend, NULL) == XTC_ERR_INVALID_STATE); 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_finish(recorder) == XTC_OK);
CHECK(xtc_recorder_status(recorder, &captured) == XTC_OK && captured == 64); CHECK(xtc_recorder_status(recorder, &captured) == XTC_OK && captured == 64);
xtc_recorder_destroy(recorder); 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); 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; 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. */ CHECK(process_cb(32, context) == 0); /* Never silently resume after missing callback data. */