Phase 6: Add WAV clips, graph playback and bounded recording
This commit is contained in:
parent
65640c4aad
commit
8a37090b81
40 changed files with 1450 additions and 69 deletions
|
|
@ -102,3 +102,20 @@ test_project additionally covers serialization, exact graph samples/IDs,
|
|||
truncation/mutation input, capacity, save failures and cleanup (part of make test).
|
||||
The core/backend tests remain unsuppressed under ASan/UBSan; GUI checks retain
|
||||
the narrowly scoped fontconfig-only suppression described above.
|
||||
|
||||
|
||||
## Phase 6 audio acceptance
|
||||
|
||||
make test-jack-audio requires a running JACK-compatible server. It creates only
|
||||
software routes between its probe and xtc backend: known 0.25 source, input
|
||||
capture through the recorder ring, WAV finalization, project save/reopen,
|
||||
then clip source graph playback observed at the probe input. All test ports,
|
||||
files and clients are removed. It never connects hardware or changes server
|
||||
configuration.
|
||||
|
||||
test_gui covers record/Stop, exact take playback, saving/reopening, failed import,
|
||||
missing assets, and blocked edits/file actions/close while recording. The live
|
||||
test_gui_jack records a silent take, finalizes and saves/reopens it, then starts
|
||||
playback before callback-safe close. Decoder and producer/consumer concurrency
|
||||
checks are part of make test; run test_audio and test_realtime under TSan as in
|
||||
BUILD.md. Physical input/output and latency compensation are not tested.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
|
||||
* timers run under Xvfb; no server/device is required or touched. */
|
||||
struct xtc_backend { int running = 0; xtc_plan *plan = nullptr; };
|
||||
struct xtc_backend { int running = 0; xtc_plan *plan = nullptr; xtc_audio_plan *audio = nullptr; xtc_recorder *recorder = nullptr; };
|
||||
namespace {
|
||||
xtc_backend handle;
|
||||
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
|
||||
|
|
@ -98,11 +98,11 @@ xtc_result xtc_backend_close(xtc_backend *backend)
|
|||
{
|
||||
if (backend == nullptr) return XTC_OK;
|
||||
++closes;
|
||||
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; xtc_plan_destroy(handle.plan); handle.plan = nullptr; }
|
||||
if (close_result == XTC_OK) { CHECK(alive == 1U); --alive; handle.running = 0; xtc_plan_destroy(handle.plan); handle.plan = nullptr; xtc_audio_plan_destroy(handle.audio); handle.audio = nullptr; }
|
||||
return close_result;
|
||||
}
|
||||
xtc_result xtc_backend_start(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) handle.running = 1; return start_result; }
|
||||
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) { handle.running = 1; engine_status.active_revision = engine_status.submitted_revision; } return start_result; }
|
||||
xtc_result xtc_backend_stop(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle); ++stops; if (stop_result == XTC_OK) handle.running = 0; return stop_result; }
|
||||
xtc_result xtc_backend_get_status(const xtc_backend *backend, xtc_backend_status *status)
|
||||
|
|
@ -126,6 +126,20 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|||
++engine_status.submitted_revision;
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
||||
{
|
||||
xtc_audio_plan *plan = nullptr;
|
||||
xtc_result r = xtc_audio_plan_create(project, 48000, &plan);
|
||||
if (r == XTC_OK) r = xtc_backend_submit(backend, xtc_project_graph(project));
|
||||
if (r != XTC_OK) { xtc_audio_plan_destroy(plan); return r; }
|
||||
xtc_audio_plan_destroy(handle.audio); handle.audio = plan;
|
||||
engine_status.asset_issues = static_cast<unsigned int>(xtc_audio_plan_issues(plan, nullptr));
|
||||
return XTC_OK;
|
||||
}
|
||||
const char *xtc_backend_asset_issue(const xtc_backend *)
|
||||
{ const char *path = ""; xtc_audio_plan_issues(handle.audio, &path); return path; }
|
||||
xtc_result xtc_backend_set_recorder(xtc_backend *, xtc_recorder *recorder)
|
||||
{ if (handle.running) return XTC_ERR_INVALID_STATE; handle.recorder = recorder; return XTC_OK; }
|
||||
void xtc_backend_collect(xtc_backend *backend)
|
||||
{ CHECK(backend == &handle && alive == 1U); ++collects; }
|
||||
}
|
||||
|
|
@ -268,6 +282,24 @@ int main(int argc, char **argv)
|
|||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
||||
CHECK(sequence->position() == 1920); // Graph edits preserve the transport.
|
||||
engine_status.sample_position = 0;
|
||||
std::string take = std::string(directory) + "/take.wav";
|
||||
CHECK(window.record_audio(take.c_str()) == XTC_OK && handle.recorder != nullptr);
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_ERR_INVALID_STATE);
|
||||
window.do_callback();
|
||||
CHECK(window.shown() && contains(&window, "Stop recording before closing"));
|
||||
float recorded[64]; for (float &value : recorded) value = 0.25f;
|
||||
xtc_recorder_push(handle.recorder, recorded, 64);
|
||||
pump();
|
||||
press(stop);
|
||||
CHECK(handle.recorder == nullptr);
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 2);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_OK);
|
||||
CHECK(window.open_project(filename.c_str()) == XTC_OK);
|
||||
float mix[64]; CHECK(xtc_audio_plan_render(handle.audio, 0, mix, 64) == XTC_OK && mix[0] == 0.25f);
|
||||
CHECK(unlink(take.c_str()) == 0);
|
||||
CHECK(window.open_project(filename.c_str()) == XTC_OK && contains(&window, "Missing/unreadable audio"));
|
||||
CHECK(window.import_audio("/missing/xtc-audio.wav") == XTC_ERR_IO);
|
||||
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
|
||||
engine_status.xruns = 3U;
|
||||
engine_status.processed_blocks = 123U;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace {
|
||||
Fl_Widget *find(Fl_Group &group, const char *label)
|
||||
|
|
@ -91,6 +92,21 @@ int main(int argc, char **argv)
|
|||
play->do_callback();
|
||||
pump();
|
||||
CHECK(find(window, "running — modular graph") != nullptr);
|
||||
stop->do_callback();
|
||||
char directory[] = "/tmp/xtc-live-record-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
|
||||
std::string take = std::string(directory) + "/take.wav";
|
||||
std::string song = std::string(directory) + "/song.xtc";
|
||||
CHECK(window.record_audio(take.c_str()) == XTC_OK);
|
||||
pump();
|
||||
CHECK(find(window, "recording JACK input") != nullptr);
|
||||
stop->do_callback();
|
||||
size_t tracks, clips;
|
||||
CHECK(xtc_project_get_counts(timeline->project(), &tracks, &clips) == XTC_OK && clips == 1);
|
||||
CHECK(window.save_project(song.c_str()) == XTC_OK);
|
||||
CHECK(window.open_project(song.c_str()) == XTC_OK);
|
||||
play->do_callback(); pump();
|
||||
CHECK(find(window, "running — modular graph") != nullptr);
|
||||
CHECK(unlink(take.c_str()) == 0 && unlink(song.c_str()) == 0 && rmdir(directory) == 0);
|
||||
window.do_callback(); // Close with an active client; synchronizes callbacks.
|
||||
CHECK(!window.shown());
|
||||
pump(); // No timer may access the closed client.
|
||||
|
|
|
|||
90
tests/integration/test_jack_audio.c
Normal file
90
tests/integration/test_jack_audio.c
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_backend.h"
|
||||
#include <jack/jack.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
typedef struct { jack_port_t *input, *output; unsigned int heard, bad; } probe;
|
||||
static int process(jack_nframes_t frames, void *context)
|
||||
{
|
||||
probe *p = context;
|
||||
const float *input = jack_port_get_buffer(p->input, frames);
|
||||
float *output = jack_port_get_buffer(p->output, frames);
|
||||
jack_nframes_t i;
|
||||
if (!input || !output) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
|
||||
for (i = 0; i < frames; ++i) {
|
||||
output[i] = 0.25f;
|
||||
if (!isfinite(input[i]) || fabsf(input[i]) > 0.251f)
|
||||
__atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
|
||||
if (fabsf(input[i] - 0.25f) < 0.0001f)
|
||||
__atomic_store_n(&p->heard, 1U, __ATOMIC_RELAXED);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#define REQUIRE(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); goto cleanup; } } while (0)
|
||||
int main(void)
|
||||
{
|
||||
char directory[] = "/tmp/xtc-live-audio-XXXXXX", take[256] = "", song[256] = "", name[64];
|
||||
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 1};
|
||||
xtc_backend *backend = NULL;
|
||||
xtc_backend_status status;
|
||||
xtc_recorder *recorder = NULL;
|
||||
xtc_project *p = NULL, *q = NULL;
|
||||
jack_client_t *client = NULL;
|
||||
jack_status_t flags = 0;
|
||||
probe state = {0};
|
||||
struct timespec pause = {0, 50000000L};
|
||||
uint64_t track, clip, frames;
|
||||
int i, result = EXIT_FAILURE;
|
||||
REQUIRE(mkdtemp(directory));
|
||||
snprintf(take, sizeof(take), "%s/take.wav", directory);
|
||||
snprintf(song, sizeof(song), "%s/song.xtc", directory);
|
||||
snprintf(name, sizeof(name), "xtc-audio-%ld", (long)getpid());
|
||||
REQUIRE(xtc_backend_open(&config, &backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK);
|
||||
REQUIRE(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
||||
REQUIRE(xtc_project_add_track(p, "Recording", &track) == XTC_OK);
|
||||
REQUIRE(xtc_backend_prepare_project(backend, p) == XTC_OK);
|
||||
client = jack_client_open("xtc-audio-probe", JackNoStartServer, &flags);
|
||||
REQUIRE(client);
|
||||
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
|
||||
state.output = jack_port_register(client, "output", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
|
||||
REQUIRE(state.input && state.output);
|
||||
REQUIRE(jack_set_process_callback(client, process, &state) == 0);
|
||||
REQUIRE(jack_activate(client) == 0);
|
||||
REQUIRE(xtc_recorder_create(take, status.sample_rate, (size_t)status.sample_rate * 2, &recorder) == XTC_OK);
|
||||
REQUIRE(xtc_backend_set_recorder(backend, recorder) == XTC_OK);
|
||||
REQUIRE(xtc_backend_start(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_connect(backend, XTC_PORT_INPUT, jack_port_name(state.output)) == XTC_OK);
|
||||
for (i = 0; i < 20; ++i) {
|
||||
nanosleep(&pause, NULL);
|
||||
REQUIRE(xtc_recorder_drain(recorder) == XTC_OK);
|
||||
REQUIRE(xtc_backend_get_status(backend, &status) == XTC_OK && status.process_result == XTC_OK);
|
||||
}
|
||||
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_set_recorder(backend, NULL) == XTC_OK);
|
||||
REQUIRE(xtc_recorder_finish(recorder) == XTC_OK);
|
||||
REQUIRE(xtc_recorder_status(recorder, &frames) == XTC_OK && frames > status.sample_rate / 2);
|
||||
xtc_recorder_destroy(recorder); recorder = NULL;
|
||||
REQUIRE(xtc_project_import_audio(p, track, take, 0, &clip) == XTC_OK);
|
||||
REQUIRE(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
|
||||
REQUIRE(xtc_backend_prepare_project(backend, q) == XTC_OK);
|
||||
REQUIRE(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
|
||||
for (i = 0; i < 10; ++i) nanosleep(&pause, NULL);
|
||||
REQUIRE(__atomic_load_n(&state.heard, __ATOMIC_RELAXED) && !__atomic_load_n(&state.bad, __ATOMIC_RELAXED));
|
||||
puts("Live JACK software source -> recording -> save/reopen -> graph playback passed.");
|
||||
result = EXIT_SUCCESS;
|
||||
cleanup:
|
||||
/* Close synchronizes producers before recorder/model cleanup. */
|
||||
if (backend && xtc_backend_close(backend) != XTC_OK) return EXIT_FAILURE;
|
||||
if (client && jack_client_close(client) != 0) return EXIT_FAILURE;
|
||||
xtc_recorder_destroy(recorder);
|
||||
xtc_project_destroy(p); xtc_project_destroy(q);
|
||||
if (take[0]) unlink(take);
|
||||
if (song[0]) unlink(song);
|
||||
rmdir(directory);
|
||||
return result;
|
||||
}
|
||||
169
tests/unit/test_audio.c
Normal file
169
tests/unit/test_audio.c
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
static void le(unsigned char *p, uint32_t value, size_t count)
|
||||
{ size_t i; for (i = 0; i < count; ++i) { p[i] = (unsigned char)(value & 255U); value >>= 8U; } }
|
||||
static void fixture(const char *path, unsigned int bits, unsigned int channels, unsigned int rate)
|
||||
{
|
||||
unsigned char header[44] = {0}, sample[8] = {0};
|
||||
size_t i; FILE *f = fopen(path, "wb");
|
||||
unsigned int align = channels * bits / 8;
|
||||
assert(f);
|
||||
memcpy(header, "RIFF", 4); le(header + 4, 36 + 480 * align, 4);
|
||||
memcpy(header + 8, "WAVEfmt ", 8); le(header + 16, 16, 4);
|
||||
le(header + 20, 1, 2); le(header + 22, channels, 2);
|
||||
le(header + 24, rate, 4); le(header + 28, rate * align, 4);
|
||||
le(header + 32, align, 2); le(header + 34, bits, 2);
|
||||
memcpy(header + 36, "data", 4); le(header + 40, 480 * align, 4);
|
||||
assert(fwrite(header, 1, 44, f) == 44);
|
||||
le(sample, UINT32_C(1) << (bits - 3), bits / 8); /* +0.25 */
|
||||
if (channels == 2) le(sample + bits / 8, UINT32_C(3) << (bits - 3), bits / 8); /* +0.75 */
|
||||
for (i = 0; i < 480; ++i) assert(fwrite(sample, 1, align, f) == align);
|
||||
assert(fclose(f) == 0);
|
||||
}
|
||||
typedef struct { xtc_recorder *r; unsigned int complete; } stress;
|
||||
static void *producer(void *context)
|
||||
{
|
||||
stress *s = context;
|
||||
float samples[32]; size_t i, b;
|
||||
for (i = 0; i < 32; ++i) samples[i] = (float)i / 32;
|
||||
for (b = 0; b < 10000; ++b) xtc_recorder_push(s->r, samples, 32);
|
||||
__atomic_store_n(&s->complete, 1, __ATOMIC_RELEASE);
|
||||
return NULL;
|
||||
}
|
||||
int main(void)
|
||||
{
|
||||
char directory[] = "/tmp/xtc-audio-XXXXXX", wave[256], take[256], project[256], moved[256];
|
||||
xtc_project *p = NULL, *q = NULL;
|
||||
xtc_audio_plan *plan = NULL;
|
||||
xtc_recorder *rec = NULL;
|
||||
uint64_t track, clip, frames;
|
||||
xtc_clip_info info;
|
||||
float output[64], input[64], timeline[64];
|
||||
xtc_plan *graph_plan = NULL;
|
||||
unsigned int bits;
|
||||
size_t i;
|
||||
assert(mkdtemp(directory));
|
||||
snprintf(wave, sizeof(wave), "%s/input.wav", directory);
|
||||
snprintf(take, sizeof(take), "%s/take.wav", directory);
|
||||
snprintf(project, sizeof(project), "%s/song.xtc", directory);
|
||||
snprintf(moved, sizeof(moved), "%s/moved.wav", directory);
|
||||
for (bits = 16; bits <= 32; bits += 8) {
|
||||
fixture(wave, bits, 2, 48000);
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "Audio", &track) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, wave, 960, &clip) == XTC_OK);
|
||||
assert(xtc_project_find_clip(p, clip, &info) == XTC_OK && info.length_tick == 20);
|
||||
assert(xtc_audio_plan_create(p, 48000, &plan) == XTC_OK && xtc_audio_plan_issues(plan, NULL) == 0);
|
||||
assert(xtc_audio_plan_render(plan, 23998, output, 8) == XTC_OK);
|
||||
assert(output[0] == 0 && output[1] == 0 && output[2] == 0.5f && output[7] == 0.5f);
|
||||
assert(xtc_graph_compile(xtc_project_graph(p), 48000, 64, &graph_plan) == XTC_OK);
|
||||
assert(xtc_plan_process_audio(graph_plan, NULL, output, timeline, 8) == XTC_OK && timeline[2] == 0.5f);
|
||||
xtc_plan_destroy(graph_plan);
|
||||
xtc_audio_plan_destroy(plan);
|
||||
assert(xtc_audio_plan_create(p, 24000, &plan) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(plan, 11999, output, 8) == XTC_OK && output[0] == 0 && output[1] == 0.5f);
|
||||
xtc_audio_plan_destroy(plan);
|
||||
/* Trim offset and overlap summing. */
|
||||
assert(xtc_project_edit_clip(p, clip, track, 0, 10, 10) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, wave, 0, &clip) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(p, 48000, &plan) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(plan, 0, output, 64) == XTC_OK && output[0] == 1.0f);
|
||||
assert(rename(wave, moved) == 0);
|
||||
assert(xtc_audio_plan_render(plan, 0, output, 64) == XTC_OK && output[0] == 1.0f); /* no callback file access */
|
||||
xtc_audio_plan_destroy(plan);
|
||||
assert(xtc_audio_plan_create(p, 48000, &plan) == XTC_OK && xtc_audio_plan_issues(plan, NULL) == 1);
|
||||
assert(xtc_audio_plan_render(plan, 0, output, 64) == XTC_OK && output[0] == 0);
|
||||
xtc_audio_plan_destroy(plan);
|
||||
assert(xtc_project_set_clip_asset(p, clip, moved) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(p, 48000, &plan) == XTC_OK && xtc_audio_plan_issues(plan, NULL) == 1);
|
||||
assert(xtc_audio_plan_render(plan, 0, output, 64) == XTC_OK && output[0] == 0.5f);
|
||||
xtc_audio_plan_destroy(plan);
|
||||
assert(unlink(moved) == 0);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
/* 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);
|
||||
for (i = 0; i < 10; ++i) {
|
||||
xtc_recorder_push(rec, input, 64);
|
||||
assert(xtc_recorder_drain(rec) == XTC_OK);
|
||||
}
|
||||
assert(xtc_recorder_status(rec, &frames) == XTC_OK && frames == 640);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK); xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "Take", &track) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, take, 0, &clip) == XTC_OK);
|
||||
assert(xtc_project_save(p, project) == XTC_OK && xtc_project_load(q, project) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(q, 48000, &plan) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(plan, 0, output, 64) == XTC_OK && memcmp(input, output, sizeof(input)) == 0);
|
||||
assert(xtc_audio_plan_render(plan, 639, output, 8) == XTC_OK && output[0] == -0.5f && output[1] == 0);
|
||||
xtc_audio_plan_destroy(plan);
|
||||
/* Truncated/unsupported imports do not change project IDs/clip counts. */
|
||||
{
|
||||
FILE *f = fopen(wave, "wb"); size_t tracks, clips; assert(f); fputs("RIFF", f); assert(fclose(f) == 0);
|
||||
assert(xtc_project_import_audio(p, track, wave, 0, &clip) == XTC_ERR_FORMAT);
|
||||
assert(xtc_project_get_counts(p, &tracks, &clips) == XTC_OK && clips == 1);
|
||||
}
|
||||
{
|
||||
unsigned char bytes[2048];
|
||||
FILE *f;
|
||||
size_t count, n, tracks, clips;
|
||||
fixture(wave, 16, 2, 48000);
|
||||
f = fopen(wave, "rb"); assert(f);
|
||||
count = fread(bytes, 1, sizeof(bytes), f); assert(fclose(f) == 0);
|
||||
for (n = 0; n < count; ++n) {
|
||||
f = fopen(wave, "wb"); assert(f);
|
||||
assert(fwrite(bytes, 1, n, f) == n && fclose(f) == 0);
|
||||
assert(xtc_project_import_audio(p, track, wave, 0, &clip) != XTC_OK);
|
||||
}
|
||||
assert(xtc_project_get_counts(p, &tracks, &clips) == XTC_OK && clips == 1);
|
||||
bytes[20] = 6; /* Unsupported codec. */
|
||||
f = fopen(wave, "wb"); assert(f);
|
||||
assert(fwrite(bytes, 1, count, f) == count && fclose(f) == 0);
|
||||
assert(xtc_project_import_audio(p, track, wave, 0, &clip) == XTC_ERR_UNSUPPORTED);
|
||||
fixture(wave, 32, 1, 48000);
|
||||
f = fopen(wave, "r+b"); assert(f);
|
||||
assert(fseek(f, 20, SEEK_SET) == 0 && fputc(3, f) != EOF);
|
||||
le(bytes, UINT32_C(0x7fc00000), 4);
|
||||
assert(fseek(f, 44, SEEK_SET) == 0 && fwrite(bytes, 1, 4, f) == 4 && fclose(f) == 0);
|
||||
assert(xtc_project_import_audio(p, track, wave, 0, &clip) == XTC_ERR_FORMAT);
|
||||
}
|
||||
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);
|
||||
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);
|
||||
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(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);
|
||||
assert(xtc_recorder_finish(rec) == XTC_OK);
|
||||
assert(xtc_recorder_status(rec, &frames) == XTC_OK && frames == 320000);
|
||||
xtc_recorder_destroy(rec);
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_project_add_track(p, "Concurrent take", &track) == XTC_OK);
|
||||
assert(xtc_project_import_audio(p, track, take, 0, &clip) == XTC_OK);
|
||||
assert(xtc_audio_plan_create(p, 48000, &plan) == XTC_OK);
|
||||
assert(xtc_audio_plan_render(plan, 319936, output, 64) == XTC_OK);
|
||||
for (i = 0; i < 64; ++i) assert(output[i] == (float)(i % 32) / 32);
|
||||
xtc_audio_plan_destroy(plan); xtc_project_destroy(p);
|
||||
}
|
||||
assert(unlink(wave) == 0 && unlink(take) == 0 && unlink(project) == 0 && rmdir(directory) == 0);
|
||||
puts("Audio import/render/trim/resample/missing assets and recording roundtrip/overflow/concurrency passed.");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
/* JACK ABI fake: deterministic fault injection, linked without libjack.
|
||||
* Processing is driven synchronously here; realtime concurrency is tested
|
||||
* separately by test_realtime. No fake header replaces installed declarations. */
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#define CHECK(c) do { if (!(c)) { fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
|
||||
static JackProcessCallback process_cb;
|
||||
static JackSampleRateCallback rate_cb;
|
||||
|
|
@ -182,6 +184,58 @@ int main(void)
|
|||
close_fail = 0;
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK);
|
||||
}
|
||||
{
|
||||
char directory[] = "/tmp/xtc-backend-audio-XXXXXX", take[256], song[256];
|
||||
xtc_project *p = NULL, *q = NULL;
|
||||
xtc_recorder *recorder = NULL;
|
||||
uint64_t track, clip, captured;
|
||||
CHECK(mkdtemp(directory) != NULL);
|
||||
snprintf(take, sizeof(take), "%s/take.wav", directory);
|
||||
snprintf(song, sizeof(song), "%s/song.xtc", directory);
|
||||
CHECK(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
|
||||
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_backend_set_recorder(backend, recorder) == XTC_OK);
|
||||
CHECK(xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_set_recorder(backend, NULL) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(xtc_backend_prepare_project(backend, p) == XTC_ERR_INVALID_STATE);
|
||||
for (i = 0; i < 32; ++i) input[i] = i % 2 ? -0.5f : 0.25f;
|
||||
CHECK(process_cb(32, context) == 0);
|
||||
deactivate_fail = 1;
|
||||
CHECK(xtc_backend_stop(backend) == XTC_ERR_IO);
|
||||
CHECK(process_cb(32, context) == 0);
|
||||
deactivate_fail = 0;
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_set_recorder(backend, NULL) == XTC_OK);
|
||||
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_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. */
|
||||
CHECK(xtc_recorder_status(recorder, &captured) == XTC_ERR_IO && captured == 0);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK && xtc_backend_set_recorder(backend, NULL) == XTC_OK);
|
||||
CHECK(xtc_recorder_finish(recorder) == XTC_ERR_IO);
|
||||
xtc_recorder_destroy(recorder); /* Prior destination take must survive. */
|
||||
CHECK(xtc_project_import_audio(p, track, take, 0, &clip) == XTC_OK);
|
||||
CHECK(xtc_project_save(p, song) == XTC_OK && xtc_project_load(q, song) == XTC_OK);
|
||||
CHECK(xtc_backend_prepare_project(backend, q) == XTC_OK);
|
||||
CHECK(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(unlink(take) == 0); /* Immutable plan is already loaded. */
|
||||
CHECK(process_cb(32, context) == 0 && memcmp(input, output, 32 * sizeof(float)) == 0);
|
||||
CHECK(xtc_backend_stop(backend) == XTC_OK);
|
||||
CHECK(xtc_backend_prepare_project(backend, q) == XTC_OK);
|
||||
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.asset_issues == 1);
|
||||
CHECK(strcmp(xtc_backend_asset_issue(backend), take) == 0);
|
||||
CHECK(xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
|
||||
CHECK(process_cb(32, context) == 0 && output[0] == 0.0f);
|
||||
CHECK(xtc_backend_close(backend) == XTC_OK);
|
||||
xtc_project_destroy(p); xtc_project_destroy(q);
|
||||
CHECK(unlink(song) == 0 && rmdir(directory) == 0);
|
||||
}
|
||||
CHECK(closed >= 104);
|
||||
CHECK(xtc_backend_close(NULL) == XTC_OK);
|
||||
xtc_graph_destroy(invalid_graph);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
#include "xtc/xtc_project.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -110,9 +110,20 @@ int main(void)
|
|||
for (i = 1; i < XTC_PROJECT_MAX_TRACKS; ++i) assert(xtc_project_add_track(q, "Track", &id) == XTC_OK);
|
||||
assert(xtc_project_add_track(q, "Full", &id) == XTC_ERR_CAPACITY);
|
||||
assert(xtc_project_get_track(q, 0, &(xtc_track_info){0}) == XTC_OK);
|
||||
for (i = 0; i < XTC_PROJECT_MAX_CLIPS; ++i) assert(xtc_project_add_clip(q, 2, "Region", 0, 960, &id) == XTC_OK);
|
||||
for (i = 0; i < XTC_PROJECT_MAX_CLIPS; ++i) {
|
||||
char asset[XTC_ASSET_PATH_BYTES];
|
||||
memset(asset, 'a', sizeof(asset)); asset[0] = '/'; asset[sizeof(asset) - 1] = 0;
|
||||
assert(xtc_project_add_clip(q, 2, "Region", 0, 960, &id) == XTC_OK);
|
||||
assert(xtc_project_set_clip_asset(q, id, asset) == XTC_OK);
|
||||
}
|
||||
assert(xtc_project_add_clip(q, 2, "Full", 0, 960, &id) == XTC_ERR_CAPACITY);
|
||||
assert(xtc_project_save(q, other) == XTC_OK && xtc_project_load(p, other) == XTC_OK);
|
||||
{
|
||||
const char *v1 = "XTC_PROJECT 1\nAPP 0 1 0\nTEMPO 120000\nIDS 1 1\nGRAPH 1 0 0 0\nTRACKS 1\nTRACK 0 41\nCLIPS 1\nCLIP 0 0 0 960 0 960 42\nEND\n";
|
||||
write_bytes(bad, v1, strlen(v1));
|
||||
assert(xtc_project_load(p, bad) == XTC_OK);
|
||||
assert(xtc_project_get_clip(p, 0, &info) == XTC_OK && info.asset_path[0] == 0);
|
||||
}
|
||||
xtc_project_destroy(p); xtc_project_destroy(q);
|
||||
assert(unlink(path) == 0 && unlink(other) == 0 && unlink(bad) == 0);
|
||||
assert(rmdir(dir) == 0); /* Also verifies no abandoned save temporaries. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue