Phase 13: protect recordings and recover retained audio after write failures
This commit is contained in:
parent
05140c2e93
commit
01d9db90fd
15 changed files with 308 additions and 30 deletions
9
Makefile
9
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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, ¬e);
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
50
src/audio/recovery.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
15
src/main.c
15
src/main.c
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
108
tests/integration/test_recording.c
Normal file
108
tests/integration/test_recording.c
Normal 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.");
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue