1
0
Fork 0
forked from hanez/xtc

Phase 7: Add MIDI clips, instrument and piano roll

This commit is contained in:
Johannes Findeisen 2026-10-27 00:53:41 +01:00
commit cacc086805
38 changed files with 1373 additions and 74 deletions

View file

@ -119,3 +119,19 @@ 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.
## Phase 7 verification
```sh
make all test gui debug test-gui test-gui-jack test-jack-audio test-jack-midi
make BUILD_DIR=build/nojack PKG_CONFIG=false all test gui test-gui
```
`test_midi` adds exact sample scheduling, trim/seek/panic, block independence,
voice capacities, stable notes and v3 malformed-file/roundtrip/v2 migration
coverage. Backend fault injection checks event offsets, dropped/malformed input,
disconnection, xruns, missing output buffers and stop/restart cleanup. GUI tests
draw/move/resize notes, save/reopen and render them. The live MIDI target connects
only software clients and checks on/off, panic, disconnect, and saved-note
playback/seek. It needs an existing JACK-compatible server. No hardware routes
or system changes are made. MIDI recording/SMF and physical devices are deferred.

View file

@ -1,6 +1,7 @@
#include "../../src/gui/main_window.hpp"
#include "../../src/gui/graph_widget.hpp"
#include "../../src/gui/sequencer_widget.hpp"
#include "../../src/gui/piano_roll.hpp"
#include <unistd.h>
#include <FL/Fl.H>
#include <FL/Fl_Box.H>
@ -16,7 +17,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; xtc_audio_plan *audio = nullptr; xtc_recorder *recorder = nullptr; };
struct xtc_backend { int running = 0; xtc_plan *plan = nullptr; xtc_audio_plan *audio = nullptr; xtc_midi_plan *midi = 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;
@ -59,7 +60,7 @@ SequencerWidget *timeline(Fl_Group *group)
}
return nullptr;
}
void gesture(SequencerWidget *s, int left, int top, int dx, int dy)
void gesture(Fl_Widget *s, int left, int top, int dx, int dy)
{
int old_x = Fl::e_x, old_y = Fl::e_y, old_key = Fl::e_keysym;
Fl::e_x = s->x() + left; Fl::e_y = s->y() + top; Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
@ -98,7 +99,7 @@ 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; xtc_audio_plan_destroy(handle.audio); handle.audio = 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; xtc_midi_plan_destroy(handle.midi); handle.midi = nullptr; }
return close_result;
}
xtc_result xtc_backend_start(xtc_backend *backend)
@ -126,13 +127,17 @@ xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
++engine_status.submitted_revision;
return XTC_OK;
}
xtc_result xtc_backend_panic(xtc_backend *) { xtc_midi_plan_panic(handle.midi, engine_status.sample_position); return XTC_OK; }
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
{
xtc_audio_plan *plan = nullptr;
xtc_midi_plan *midi = nullptr;
xtc_result r = xtc_audio_plan_create(project, 48000, &plan);
if (r == XTC_OK) r = xtc_midi_plan_create(project, 48000, &midi);
if (r == XTC_OK) r = xtc_backend_submit(backend, xtc_project_graph(project));
if (r != XTC_OK) { xtc_audio_plan_destroy(plan); return r; }
if (r != XTC_OK) { xtc_audio_plan_destroy(plan); xtc_midi_plan_destroy(midi); return r; }
xtc_audio_plan_destroy(handle.audio); handle.audio = plan;
xtc_midi_plan_destroy(handle.midi); handle.midi = midi;
engine_status.asset_issues = static_cast<unsigned int>(xtc_audio_plan_issues(plan, nullptr));
return XTC_OK;
}
@ -300,6 +305,29 @@ int main(int argc, char **argv)
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);
press(find(&window, "Add MIDI clip"));
CHECK(window.open_piano_roll() == XTC_OK);
auto *roll = window.piano_roll(); CHECK(roll != nullptr);
int saved_x = Fl::e_x, saved_y = Fl::e_y, saved_key = Fl::e_keysym;
Fl::e_x = roll->x() + 112; Fl::e_y = roll->y() + 1110; Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
CHECK(roll->handle(FL_PUSH) == 1); (void)roll->handle(FL_RELEASE);
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
gesture(roll, 130, 1110, 24, -16); // Move one sixteenth and a semitone.
gesture(roll, 230, 1094, 48, 0); // Extend right edge by half a beat.
xtc_note_info drawn{}; size_t note_count;
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 1);
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK &&
drawn.start_tick == 720 && drawn.length_tick == 1440 && drawn.pitch == 61);
CHECK(window.save_project(filename.c_str()) == XTC_OK);
CHECK(window.open_project(filename.c_str()) == XTC_OK);
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 61 && drawn.start_tick == 720);
float instrument[32], heard[32];
CHECK(xtc_midi_plan_render(handle.midi, 18000, nullptr, 0, instrument, 32) == XTC_OK && instrument[1] > 0);
CHECK(xtc_plan_process_sources(handle.plan, nullptr, nullptr, instrument, heard, 32) == XTC_OK && heard[1] == instrument[1]);
engine_status.sample_position = 18032;
press(find(&window, "Panic"));
CHECK(xtc_midi_plan_render(handle.midi, 18032, nullptr, 0, instrument, 32) == XTC_OK && instrument[0] == 0);
engine_status.sample_position = 0;
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
engine_status.xruns = 3U;
engine_status.processed_blocks = 123U;

View file

@ -1,6 +1,7 @@
#include "../../src/gui/main_window.hpp"
#include "../../src/gui/graph_widget.hpp"
#include "../../src/gui/sequencer_widget.hpp"
#include "../../src/gui/piano_roll.hpp"
#include <FL/Fl.H>
#include <FL/Fl_Group.H>
#include <chrono>
@ -106,6 +107,15 @@ int main(int argc, char **argv)
CHECK(window.open_project(song.c_str()) == XTC_OK);
play->do_callback(); pump();
CHECK(find(window, "running — modular graph") != nullptr);
auto *add_midi = find(window, "Add MIDI clip"); CHECK(add_midi != nullptr);
add_midi->do_callback(); CHECK(window.open_piano_roll() == XTC_OK);
CHECK(window.piano_roll()->add_note(0, 960, 69) == XTC_OK);
CHECK(window.save_project(song.c_str()) == XTC_OK);
CHECK(window.open_project(song.c_str()) == XTC_OK);
size_t notes;
CHECK(xtc_project_note_count(timeline->project(), &notes) == XTC_OK && notes == 1);
play->do_callback(); pump();
CHECK(find(window, "Graph: active in engine") != 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());

View file

@ -0,0 +1,108 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_backend.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
typedef struct {
jack_port_t *input, *midi;
unsigned int command, seen, loud, 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);
void *midi = jack_port_get_buffer(p->midi, frames);
unsigned int command = __atomic_load_n(&p->command, __ATOMIC_ACQUIRE), loud = 0;
jack_nframes_t i;
if (!input || !midi) { __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED); return 0; }
jack_midi_clear_buffer(midi);
if (command && command != p->seen) {
unsigned char bytes[3] = {0x90, 69, command % 2 ? 127 : 0};
if (jack_midi_event_write(midi, frames > 8 ? 8 : 0, bytes, 3) != 0)
__atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
p->seen = command;
}
for (i = 0; i < frames; ++i) {
if (!isfinite(input[i]) || fabsf(input[i]) > 0.101f) __atomic_store_n(&p->bad, 1U, __ATOMIC_RELAXED);
if (fabsf(input[i]) > 0.01f) loud = 1;
}
__atomic_store_n(&p->loud, loud, __ATOMIC_RELAXED);
if (loud) __atomic_store_n(&p->heard, 1U, __ATOMIC_RELAXED);
return 0;
}
static void wait_blocks(void)
{ struct timespec pause = {0, 200000000L}; nanosleep(&pause, NULL); }
#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-midi-XXXXXX", song[256] = "", name[64];
xtc_backend_config config = {XTC_BACKEND_JACK, name, 65536, 1};
xtc_backend *backend = NULL;
xtc_project *p = NULL, *q = NULL;
jack_client_t *client = NULL;
jack_status_t flags = 0;
probe state = {0};
const char *destination;
uint64_t track, clip, note;
int result = EXIT_FAILURE;
REQUIRE(mkdtemp(directory));
snprintf(song, sizeof(song), "%s/song.xtc", directory);
snprintf(name, sizeof(name), "xtc-midi-%ld", (long)getpid());
REQUIRE(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
REQUIRE(xtc_project_add_track(p, "Keys", &track) == XTC_OK);
REQUIRE(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
REQUIRE(xtc_backend_open(&config, &backend) == XTC_OK);
REQUIRE(xtc_backend_prepare_project(backend, p) == XTC_OK);
client = jack_client_open("xtc-midi-probe", JackNoStartServer, &flags); REQUIRE(client);
state.input = jack_port_register(client, "input", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
state.midi = jack_port_register(client, "midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
REQUIRE(state.input && state.midi);
REQUIRE(jack_set_process_callback(client, process, &state) == 0 && jack_activate(client) == 0);
REQUIRE(xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_midi_port_name(backend, &destination) == XTC_OK);
wait_blocks(); /* PipeWire JACK publishes newly activated ports asynchronously. */
{
int connected = jack_connect(client, jack_port_name(state.midi), destination);
if (connected && connected != EEXIST) fprintf(stderr, "MIDI connection returned %d: %s -> %s\n", connected, jack_port_name(state.midi), destination);
REQUIRE(connected == 0 || connected == EEXIST);
}
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
__atomic_store_n(&state.command, 1U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
__atomic_store_n(&state.command, 2U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
__atomic_store_n(&state.command, 3U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(xtc_backend_panic(backend) == XTC_OK); wait_blocks();
REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
__atomic_store_n(&state.command, 5U, __ATOMIC_RELEASE); wait_blocks();
REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(jack_disconnect(client, jack_port_name(state.midi), destination) == 0);
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_project_add_note(p, clip, 0, 960, 69, 100, 0, &note) == 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);
wait_blocks(); REQUIRE(__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(xtc_backend_stop(backend) == XTC_OK);
REQUIRE(xtc_backend_seek(backend, 7680000) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
REQUIRE(xtc_backend_connect(backend, XTC_PORT_OUTPUT, jack_port_name(state.input)) == XTC_OK);
wait_blocks(); REQUIRE(!__atomic_load_n(&state.loud, __ATOMIC_RELAXED));
REQUIRE(__atomic_load_n(&state.heard, __ATOMIC_RELAXED) && !__atomic_load_n(&state.bad, __ATOMIC_RELAXED));
puts("Live JACK MIDI input/note-off/panic and saved/reopened scheduled instrument playback passed.");
result = EXIT_SUCCESS;
cleanup:
if (backend && xtc_backend_close(backend) != XTC_OK) return EXIT_FAILURE;
if (client && jack_client_close(client) != 0) return EXIT_FAILURE;
xtc_project_destroy(p); xtc_project_destroy(q);
if (song[0]) unlink(song);
rmdir(directory);
return result;
}

View file

@ -4,6 +4,7 @@
* separately by test_realtime. No fake header replaces installed declarations. */
#include "xtc/xtc_backend.h"
#include <jack/jack.h>
#include <jack/midiport.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -11,13 +12,16 @@
#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 JackPortConnectCallback connection_cb;
static JackSampleRateCallback rate_cb;
static JackBufferSizeCallback size_cb;
static JackXRunCallback xrun_cb;
static JackShutdownCallback shutdown_cb;
static void *context;
static float input[128], output[128];
static int client_token, input_token, output_token;
static int client_token, input_token, output_token, midi_token;
static unsigned int midi_count;
static jack_midi_event_t midi_events[256];
static int open_fail, register_fail, callback_fail, activate_fail, deactivate_fail, close_fail;
static int active, closed, port_calls, null_buffer;
static unsigned int server_frames = 32U;
@ -40,8 +44,13 @@ jack_port_t *jack_port_register(jack_client_t *client, const char *name, const c
(void)client; (void)name; (void)type; (void)buffer_size;
++port_calls;
if (register_fail == port_calls) return NULL;
if (!strcmp(type, JACK_DEFAULT_MIDI_TYPE)) return (jack_port_t *)&midi_token;
return (jack_port_t *)(flags == JackPortIsInput ? &input_token : &output_token);
}
int jack_set_port_connect_callback(jack_client_t *client, JackPortConnectCallback callback, void *arg)
{ (void)client; (void)arg; connection_cb = callback; return 0; }
jack_port_t *jack_port_by_id(jack_client_t *client, jack_port_id_t id)
{ (void)client; return id == 3 ? (jack_port_t *)&midi_token : NULL; }
int jack_set_process_callback(jack_client_t *client, JackProcessCallback callback, void *arg)
{ (void)client; process_cb = callback; context = arg; return callback_fail ? -1 : 0; }
int jack_set_sample_rate_callback(jack_client_t *client, JackSampleRateCallback callback, void *arg)
@ -57,9 +66,12 @@ int jack_activate(jack_client_t *client)
int jack_deactivate(jack_client_t *client)
{ (void)client; if (deactivate_fail) return -1; active = 0; return 0; }
void *jack_port_get_buffer(jack_port_t *port, jack_nframes_t frames)
{ (void)frames; if (null_buffer) return NULL; return port == (jack_port_t *)&input_token ? input : output; }
{ (void)frames; if (null_buffer) return NULL; if (port == (jack_port_t *)&midi_token) return &midi_token; return port == (jack_port_t *)&input_token ? input : output; }
uint32_t jack_midi_get_event_count(void *buffer) { (void)buffer; return midi_count; }
int jack_midi_event_get(jack_midi_event_t *event, void *buffer, uint32_t index)
{ (void)buffer; if (index >= midi_count || index >= 256) return -1; *event = midi_events[index]; return 0; }
const char *jack_port_name(const jack_port_t *port)
{ return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
{ if (port == (jack_port_t *)&midi_token) return "test:midi_in"; return port == (jack_port_t *)&input_token ? "test:input" : "test:output"; }
const char **jack_get_ports(jack_client_t *client, const char *pattern, const char *type, unsigned long flags)
{
const char **list = malloc(2U * sizeof(*list));
@ -99,6 +111,8 @@ int main(void)
server_frames = 32U;
register_fail = 2;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
register_fail = 3;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
register_fail = 0;
callback_fail = 1;
CHECK(xtc_backend_open(&config, &backend) == XTC_ERR_IO && backend == NULL);
@ -236,6 +250,53 @@ int main(void)
xtc_project_destroy(p); xtc_project_destroy(q);
CHECK(unlink(song) == 0 && rmdir(directory) == 0);
}
{
xtc_project *p = NULL;
uint64_t track, clip;
unsigned char on[3] = {0x90, 69, 127}, off[3] = {0x90, 69, 0}, bad[3] = {0x90, 255, 127};
CHECK(xtc_project_create(&p) == XTC_OK && xtc_project_add_track(p, "MIDI", &track) == XTC_OK);
CHECK(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
CHECK(xtc_backend_open(&config, &backend) == XTC_OK && xtc_backend_prepare_project(backend, p) == XTC_OK);
CHECK(xtc_backend_midi_port_name(backend, &name) == XTC_OK && strcmp(name, "test:midi_in") == 0);
CHECK(xtc_backend_start(backend) == XTC_OK);
midi_count = 1; midi_events[0].time = 4; midi_events[0].size = 3; midi_events[0].buffer = on;
CHECK(process_cb(32, context) == 0 && output[0] == 0 && output[4] == 0 && output[5] > 0);
midi_events[0].time = 8; midi_events[0].buffer = off;
CHECK(process_cb(32, context) == 0 && output[0] != 0 && output[8] == 0 && output[31] == 0);
midi_events[0].time = 0; midi_events[0].buffer = on;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 0;
CHECK(xtc_backend_panic(backend) == XTC_OK);
CHECK(process_cb(32, context) == 0 && output[0] == 0 && output[31] == 0);
midi_count = 1;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 257;
CHECK(process_cb(32, context) == 0 && output[1] == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.midi_dropped == 257);
midi_count = 1; midi_events[0].buffer = bad;
CHECK(process_cb(32, context) == 0 && output[1] == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.midi_dropped == 258);
midi_events[0].buffer = on;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 0;
connection_cb(999, 3, 0, context);
CHECK(process_cb(32, context) == 0 && output[1] == 0);
midi_count = 1;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 0; CHECK(xrun_cb(context) == 0);
CHECK(process_cb(32, context) == 0 && output[1] == 0);
midi_count = 1;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 0; null_buffer = 1; CHECK(process_cb(32, context) == 0); null_buffer = 0;
CHECK(process_cb(32, context) == 0 && output[1] == 0);
midi_count = 1;
CHECK(process_cb(32, context) == 0 && output[1] > 0);
midi_count = 0;
CHECK(xtc_backend_stop(backend) == XTC_OK && xtc_backend_seek(backend, 0) == XTC_OK && xtc_backend_start(backend) == XTC_OK);
CHECK(process_cb(32, context) == 0 && output[1] == 0);
CHECK(xtc_backend_close(backend) == XTC_OK);
xtc_project_destroy(p);
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);

167
tests/unit/test_midi.c Normal file
View file

@ -0,0 +1,167 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_midi.h"
#include "xtc/xtc_audio.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define TAU 6.28318530717958647692
static int silent(const float *p, size_t n)
{ size_t i; for (i = 0; i < n; ++i) if (p[i] != 0) return 0; return 1; }
int main(void)
{
xtc_project *p = NULL, *q = NULL;
xtc_midi_plan *a = NULL, *b = NULL;
xtc_plan *graph = NULL;
uint64_t track, clip, note, other;
size_t count, i;
float whole[512], split[512], output[512], routed[512];
xtc_note_info n;
xtc_midi_event event = {4, XTC_MIDI_NOTE_ON, 0, 69, 127}, decoded = {0};
unsigned char on[] = {0x92, 69, 100}, off[] = {0x92, 69, 0}, panic[] = {0xb2, 123, 0}, bad[] = {0x90, 255, 127};
char directory[] = "/tmp/xtc-midi-XXXXXX", path[256], copy[256], bytes[8192], roundtrip[8192];
FILE *f; size_t length;
assert(mkdtemp(directory));
snprintf(path, sizeof(path), "%s/song.xtc", directory);
snprintf(copy, sizeof(copy), "%s/copy.xtc", directory);
assert(xtc_midi_decode(on, 3, 17, &decoded) == XTC_OK && decoded.channel == 2 && decoded.offset == 17 && decoded.type == XTC_MIDI_NOTE_ON);
assert(xtc_midi_decode(off, 3, 0, &decoded) == XTC_OK && decoded.type == XTC_MIDI_NOTE_OFF);
assert(xtc_midi_decode(panic, 3, 0, &decoded) == XTC_OK && decoded.type == XTC_MIDI_ALL_OFF);
assert(xtc_midi_decode(bad, 3, 0, &decoded) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_midi_decode(on, 2, 0, &decoded) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_create(&p) == XTC_OK && xtc_project_create(&q) == XTC_OK);
assert(xtc_project_add_track(p, "Keys", &track) == XTC_OK);
assert(xtc_project_add_midi_clip(p, track, 1, 3840, &clip) == XTC_OK);
assert(xtc_project_add_note(p, clip, 1, 10, 69, 127, 0, &note) == XTC_OK);
assert(xtc_project_set_clip_asset(p, clip, "/bad.wav") == XTC_ERR_INVALID_STATE);
assert(xtc_project_add_note(p, clip, 3840, 1, 69, 127, 0, &other) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_add_note(p, clip, 0, 1, 128, 127, 0, &other) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_edit_note(p, note, 0, UINT64_MAX, 69, 127, 0) == XTC_ERR_INVALID_ARGUMENT);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK && xtc_midi_plan_create(p, 48000, &b) == XTC_OK);
assert(xtc_midi_plan_render(a, 0, NULL, 0, whole, 512) == XTC_OK);
assert(silent(whole, 51) && whole[51] > 0 && silent(whole + 300, 212));
assert(fabs((double)whole[51] - sin(TAU * 440 / 48000) * 0.1) < 0.000001);
assert(xtc_midi_plan_render(b, 0, NULL, 0, split, 53) == XTC_OK);
assert(xtc_midi_plan_render(b, 53, NULL, 0, split + 53, 247) == XTC_OK);
assert(xtc_midi_plan_render(b, 300, NULL, 0, split + 300, 212) == XTC_OK);
assert(memcmp(whole, split, sizeof(whole)) == 0); /* Block partition independent. */
assert(xtc_graph_compile(xtc_project_graph(p), 48000, 512, &graph) == XTC_OK);
assert(xtc_plan_process_sources(graph, NULL, NULL, whole, routed, 512) == XTC_OK && memcmp(whole, routed, sizeof(whole)) == 0);
xtc_plan_destroy(graph);
xtc_midi_plan_panic(a, 100);
assert(xtc_midi_plan_render(a, 100, NULL, 0, output, 128) == XTC_OK && silent(output, 128));
xtc_midi_plan_reset(a);
assert(xtc_midi_plan_render(a, 100, NULL, 0, output, 128) == XTC_OK && output[0] == whole[100]);
assert(xtc_project_save(p, path) == XTC_OK && xtc_project_load(q, path) == XTC_OK);
assert(xtc_project_get_note(q, 0, &n) == XTC_OK && n.id == note && n.pitch == 69 && n.start_tick == 1);
assert(xtc_project_save(q, copy) == XTC_OK);
f = fopen(path, "rb"); assert(f); length = fread(bytes, 1, sizeof(bytes), f); assert(fclose(f) == 0);
f = fopen(copy, "rb"); assert(f); assert(fread(roundtrip, 1, sizeof(roundtrip), f) == length && fclose(f) == 0);
assert(memcmp(bytes, roundtrip, length) == 0);
/* All truncations rejected without changing the loaded notes. */
for (i = 0; i < length; ++i) {
f = fopen(copy, "wb"); assert(f);
assert(fwrite(bytes, 1, i, f) == i && fclose(f) == 0);
assert(xtc_project_load(q, copy) != XTC_OK);
assert(xtc_project_get_note(q, 0, &n) == XTC_OK && n.id == note);
}
bytes[length] = 0;
{
const char *invalid[] = {
"NOTES 1 1\nNOTE 0 999 1 10 69 127 0\nEND\n",
"NOTES 1 1\nNOTE 0 0 3840 10 69 127 0\nEND\n",
"NOTES 1 1\nNOTE 0 0 1 10 128 127 0\nEND\n",
"NOTES 1 1\nNOTE 0 0 1 10 69 0 0\nEND\n",
"NOTES 1 1\nNOTE 0 0 1 10 69 127 16\nEND\n",
"NOTES 1 1\nNOTE 1 0 1 10 69 127 0\nEND\n",
"NOTES 2 2\nNOTE 0 0 1 10 69 127 0\nNOTE 0 0 1 10 69 127 0\nEND\n"
};
const char *notes = strstr(bytes, "NOTES "); assert(notes);
size_t prefix = (size_t)(notes - bytes), k;
for (k = 0; k < sizeof(invalid) / sizeof(invalid[0]); ++k) {
f = fopen(copy, "wb"); assert(f);
assert(fwrite(bytes, 1, prefix, f) == prefix && fputs(invalid[k], f) >= 0 && fclose(f) == 0);
assert(xtc_project_load(q, copy) == XTC_ERR_FORMAT);
assert(xtc_project_get_note(q, 0, &n) == XTC_OK && n.id == note && n.pitch == 69);
}
}
assert(xtc_project_remove_note(q, note) == XTC_OK);
assert(xtc_project_add_note(q, clip, 0, 960, 60, 100, 1, &other) == XTC_OK && other > note);
assert(xtc_project_remove_clip(q, clip) == XTC_OK && xtc_project_note_count(q, &count) == XTC_OK && count == 0);
assert(xtc_project_edit_clip(p, clip, track, 0, 5, 5) == XTC_OK);
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK);
assert(xtc_midi_plan_render(a, 0, NULL, 0, output, 128) == XTC_OK);
assert(fabs((double)output[0] - sin(TAU * 100 * 440 / 48000) * 0.1) < 0.000001);
assert(silent(output + 125, 3)); /* Trim hard-stops note, no stuck tail. */
xtc_midi_plan_destroy(a);
assert(xtc_project_remove_track(p, track) == XTC_OK && xtc_project_note_count(p, &count) == XTC_OK && count == 0);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK);
assert(xtc_midi_plan_render(a, 0, &event, 1, output, 32) == XTC_OK);
assert(silent(output, 5) && output[5] > 0);
event.offset = 8; event.type = XTC_MIDI_NOTE_OFF;
assert(xtc_midi_plan_render(a, 32, &event, 1, output, 32) == XTC_OK && silent(output + 8, 24));
event.offset = 0; event.type = XTC_MIDI_NOTE_ON;
assert(xtc_midi_plan_render(a, 64, &event, 1, output, 32) == XTC_OK);
event.offset = 7; event.type = XTC_MIDI_ALL_OFF;
assert(xtc_midi_plan_render(a, 96, &event, 1, output, 32) == XTC_OK && silent(output + 7, 25));
event.offset = 0; event.type = XTC_MIDI_NOTE_ON;
assert(xtc_midi_plan_render(a, 128, &event, 1, output, 32) == XTC_OK);
assert(xtc_midi_plan_render(a, 500, NULL, 0, output, 32) == XTC_OK && silent(output, 32)); /* Seek kills live. */
for (i = 0; i < 512; ++i) output[i] = 123;
event.offset = 32;
assert(xtc_midi_plan_render(a, 532, &event, 1, output, 32) == XTC_ERR_INVALID_ARGUMENT && output[0] == 123);
xtc_midi_plan_destroy(a);
assert(xtc_project_add_track(p, "Poly", &track) == XTC_OK);
assert(xtc_project_add_midi_clip(p, track, 0, 3840, &clip) == XTC_OK);
for (i = 0; i < 33; ++i) assert(xtc_project_add_note(p, clip, 0, 960, (unsigned int)i + 48, 100, 0, &note) == XTC_OK);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_ERR_CAPACITY && a == NULL);
assert(xtc_project_remove_note(p, note) == XTC_OK);
assert(xtc_midi_plan_create(p, 48000, &a) == XTC_OK); xtc_midi_plan_destroy(a);
for (i = 32; i < XTC_PROJECT_MAX_NOTES; ++i)
assert(xtc_project_add_note(p, clip, 0, 1, 60, 100, 0, &note) == XTC_OK);
assert(xtc_project_add_note(p, clip, 0, 1, 60, 100, 0, &note) == XTC_ERR_CAPACITY);
assert(xtc_project_save(p, path) == XTC_OK && xtc_project_load(p, path) == XTC_OK);
assert(xtc_project_remove_track(p, track) == XTC_OK && xtc_project_note_count(p, &count) == XTC_OK && count == 0);
assert(xtc_midi_plan_create(q, 48000, &a) == XTC_OK);
{
xtc_midi_event many[33];
for (i = 0; i < 33; ++i) {
many[i].offset = 0; many[i].type = XTC_MIDI_NOTE_ON; many[i].channel = 0;
many[i].pitch = (unsigned int)i + 48; many[i].velocity = 127;
}
assert(xtc_midi_plan_render(a, 0, many, 33, output, 32) == XTC_OK && output[1] > 0);
many[0].type = XTC_MIDI_ALL_OFF;
assert(xtc_midi_plan_render(a, 32, many, 1, output, 32) == XTC_OK && silent(output, 32));
many[0].type = many[1].type = XTC_MIDI_NOTE_ON;
many[0].pitch = many[1].pitch = 69; many[1].channel = 1;
assert(xtc_midi_plan_render(a, 64, many, 2, output, 32) == XTC_OK);
many[0].type = XTC_MIDI_ALL_OFF; many[0].offset = 8;
assert(xtc_midi_plan_render(a, 96, many, 1, output, 32) == XTC_OK);
assert(fabs((double)output[8] - sin(TAU * 40 * 440 / 48000) * 0.1) < 0.000001);
xtc_midi_plan_panic(a, 128);
assert(xtc_midi_plan_render(a, 128, NULL, 0, output, 32) == XTC_OK && silent(output, 32));
}
xtc_midi_plan_destroy(a);
/* Explicit migration from the previous audio format. */
f = fopen(copy, "wb"); assert(f);
assert(fputs("XTC_PROJECT 2\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\nASSET -\nEND\n", f) >= 0 && fclose(f) == 0);
assert(xtc_project_load(q, copy) == XTC_OK && xtc_project_note_count(q, &count) == XTC_OK && count == 0);
{
xtc_graph *full = NULL;
size_t id, tracks, clips;
uint64_t unchanged = UINT64_MAX;
assert(xtc_graph_create(64, &full) == XTC_OK);
for (i = 0; i < 64; ++i) assert(xtc_graph_add_gain(full, 1, &id) == XTC_OK);
assert(xtc_project_set_graph(q, full) == XTC_OK);
xtc_graph_destroy(full);
assert(xtc_project_add_midi_clip(q, 0, 0, 3840, &unchanged) == XTC_ERR_CAPACITY && unchanged == UINT64_MAX);
assert(xtc_project_get_counts(q, &tracks, &clips) == XTC_OK && tracks == 1 && clips == 1);
}
xtc_project_destroy(p); xtc_project_destroy(q);
assert(unlink(path) == 0 && unlink(copy) == 0 && rmdir(directory) == 0);
puts("MIDI notes/persistence/scheduling/trim/seek/panic/live offsets and polyphony passed.");
return 0;
}