2026-10-26 23:40:28 +01:00
|
|
|
#include "../../src/gui/main_window.hpp"
|
2026-10-26 23:55:35 +01:00
|
|
|
#include "../../src/gui/graph_widget.hpp"
|
2026-10-27 00:10:45 +01:00
|
|
|
#include "../../src/gui/sequencer_widget.hpp"
|
2026-10-27 00:53:41 +01:00
|
|
|
#include "../../src/gui/piano_roll.hpp"
|
2026-10-27 01:06:57 +01:00
|
|
|
#include "../../src/gui/mixer_widget.hpp"
|
2026-10-27 00:10:45 +01:00
|
|
|
#include <unistd.h>
|
2026-10-26 23:40:28 +01:00
|
|
|
#include <FL/Fl.H>
|
|
|
|
|
#include <FL/Fl_Box.H>
|
|
|
|
|
#include <FL/Fl_Button.H>
|
|
|
|
|
#include <FL/Fl_Group.H>
|
|
|
|
|
#include <FL/Fl_Menu_Bar.H>
|
|
|
|
|
#include <FL/Fl_Tabs.H>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
2026-10-26 23:55:35 +01:00
|
|
|
#include <cmath>
|
2026-10-26 23:40:28 +01:00
|
|
|
#include <initializer_list>
|
2026-10-27 04:00:27 +01:00
|
|
|
#include <cstdlib>
|
2026-10-26 23:40:28 +01:00
|
|
|
|
|
|
|
|
/* Fake only the public C backend boundary. Real FLTK widgets, callbacks and
|
|
|
|
|
* timers run under Xvfb; no server/device is required or touched. */
|
2026-10-27 00:53:41 +01:00
|
|
|
struct xtc_backend { int running = 0; xtc_plan *plan = nullptr; xtc_audio_plan *audio = nullptr; xtc_midi_plan *midi = nullptr; xtc_recorder *recorder = nullptr; };
|
2026-10-26 23:40:28 +01:00
|
|
|
namespace {
|
|
|
|
|
xtc_backend handle;
|
2026-10-27 03:25:04 +01:00
|
|
|
xtc_result seek_result = XTC_OK;
|
2026-10-26 23:40:28 +01:00
|
|
|
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result status_result = XTC_OK, submit_result = XTC_OK;
|
|
|
|
|
unsigned int submissions = 0U;
|
2026-10-26 23:40:28 +01:00
|
|
|
xtc_backend_status engine_status = {};
|
|
|
|
|
bool partial_open = false;
|
|
|
|
|
unsigned int opens = 0, starts = 0, stops = 0, closes = 0, collects = 0, alive = 0;
|
|
|
|
|
int failures = 0;
|
|
|
|
|
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); ++failures; } } while (0)
|
|
|
|
|
void pump()
|
|
|
|
|
{
|
|
|
|
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(180);
|
|
|
|
|
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
|
|
|
|
}
|
|
|
|
|
Fl_Widget *find(Fl_Group *group, const char *label)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group->children(); ++i) {
|
|
|
|
|
Fl_Widget *child = group->child(i);
|
|
|
|
|
if (child->label() != nullptr && std::strcmp(child->label(), label) == 0) return child;
|
|
|
|
|
auto *nested = dynamic_cast<Fl_Group *>(child);
|
|
|
|
|
if (nested != nullptr) if (auto *match = find(nested, label)) return match;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
bool contains(Fl_Group *group, const char *text)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group->children(); ++i) {
|
|
|
|
|
Fl_Widget *child = group->child(i);
|
|
|
|
|
if (child->label() != nullptr && std::strstr(child->label(), text) != nullptr) return true;
|
|
|
|
|
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (contains(nested, text)) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
SequencerWidget *timeline(Fl_Group *group)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group->children(); ++i) {
|
|
|
|
|
if (auto *s = dynamic_cast<SequencerWidget *>(group->child(i))) return s;
|
|
|
|
|
if (auto *g = dynamic_cast<Fl_Group *>(group->child(i))) if (auto *s = timeline(g)) return s;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
void gesture(Fl_Widget *s, int left, int top, int dx, int dy)
|
2026-10-27 00:10:45 +01:00
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
CHECK(s->handle(FL_PUSH) == 1);
|
|
|
|
|
Fl::e_x += dx; Fl::e_y += dy;
|
|
|
|
|
CHECK(s->handle(FL_DRAG) == 1);
|
|
|
|
|
CHECK(s->handle(FL_RELEASE) == 1);
|
|
|
|
|
Fl::e_x = old_x; Fl::e_y = old_y; Fl::e_keysym = old_key;
|
|
|
|
|
}
|
2026-10-26 23:40:28 +01:00
|
|
|
void press(Fl_Widget *widget)
|
|
|
|
|
{
|
|
|
|
|
CHECK(widget != nullptr);
|
|
|
|
|
if (widget != nullptr) widget->do_callback();
|
|
|
|
|
}
|
|
|
|
|
void reset()
|
|
|
|
|
{
|
|
|
|
|
CHECK(alive == 0U);
|
|
|
|
|
open_result = start_result = stop_result = close_result = status_result = XTC_OK;
|
2026-10-27 03:25:04 +01:00
|
|
|
submit_result = seek_result = XTC_OK;
|
2026-10-26 23:40:28 +01:00
|
|
|
engine_status = {};
|
|
|
|
|
engine_status.sample_rate = 48000U;
|
|
|
|
|
engine_status.buffer_frames = 1024U;
|
|
|
|
|
partial_open = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
extern "C" {
|
|
|
|
|
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
|
|
|
|
|
{
|
2026-10-27 01:06:57 +01:00
|
|
|
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 2U);
|
2026-10-26 23:40:28 +01:00
|
|
|
++opens;
|
|
|
|
|
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
|
2026-10-26 23:55:35 +01:00
|
|
|
if (*out != nullptr) { ++alive; handle.running = 0; CHECK(handle.plan == nullptr); engine_status.active_revision = engine_status.submitted_revision = 0U; }
|
2026-10-26 23:40:28 +01:00
|
|
|
return open_result;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_close(xtc_backend *backend)
|
|
|
|
|
{
|
|
|
|
|
if (backend == nullptr) return XTC_OK;
|
|
|
|
|
++closes;
|
2026-10-27 00:53:41 +01:00
|
|
|
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; }
|
2026-10-26 23:40:28 +01:00
|
|
|
return close_result;
|
|
|
|
|
}
|
|
|
|
|
xtc_result xtc_backend_start(xtc_backend *backend)
|
2026-10-27 00:31:03 +01:00
|
|
|
{ CHECK(backend == &handle); ++starts; if (start_result == XTC_OK) { handle.running = 1; engine_status.active_revision = engine_status.submitted_revision; } return start_result; }
|
2026-10-27 03:37:19 +01:00
|
|
|
xtc_result xtc_backend_set_transition_frames(xtc_backend *, size_t frames)
|
|
|
|
|
{ CHECK(frames == 240); return XTC_OK; }
|
|
|
|
|
xtc_result xtc_backend_request_stop(xtc_backend *backend)
|
|
|
|
|
{ return xtc_backend_stop(backend); }
|
2026-10-26 23:40:28 +01:00
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
CHECK(backend == &handle && alive == 1U);
|
|
|
|
|
*status = engine_status;
|
|
|
|
|
status->running = handle.running && !status->server_lost;
|
|
|
|
|
return status_result;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
xtc_result xtc_backend_seek(xtc_backend *, uint64_t sample)
|
2026-10-27 03:25:04 +01:00
|
|
|
{ if (handle.running) return XTC_ERR_INVALID_STATE; if (seek_result != XTC_OK) return seek_result; engine_status.sample_position = sample; return XTC_OK; }
|
2026-10-26 23:55:35 +01:00
|
|
|
xtc_result xtc_backend_submit(xtc_backend *backend, const xtc_graph *graph)
|
|
|
|
|
{
|
|
|
|
|
CHECK(backend == &handle && graph != nullptr);
|
|
|
|
|
++submissions;
|
|
|
|
|
if (submit_result != XTC_OK) return submit_result;
|
|
|
|
|
xtc_plan *plan = nullptr;
|
|
|
|
|
xtc_result result = xtc_graph_compile(graph, 48000.0, 32U, &plan);
|
|
|
|
|
if (result != XTC_OK) return result;
|
|
|
|
|
xtc_plan_destroy(handle.plan); handle.plan = plan;
|
|
|
|
|
++engine_status.submitted_revision;
|
|
|
|
|
return XTC_OK;
|
|
|
|
|
}
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_result xtc_backend_panic(xtc_backend *) { xtc_midi_plan_panic(handle.midi, engine_status.sample_position); return XTC_OK; }
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result xtc_backend_prepare_project(xtc_backend *backend, const xtc_project *project)
|
|
|
|
|
{
|
|
|
|
|
xtc_audio_plan *plan = nullptr;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan *midi = nullptr;
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_result r = xtc_audio_plan_create(project, 48000, &plan);
|
2026-10-27 00:53:41 +01:00
|
|
|
if (r == XTC_OK) r = xtc_midi_plan_create(project, 48000, &midi);
|
2026-10-27 00:31:03 +01:00
|
|
|
if (r == XTC_OK) r = xtc_backend_submit(backend, xtc_project_graph(project));
|
2026-10-27 00:53:41 +01:00
|
|
|
if (r != XTC_OK) { xtc_audio_plan_destroy(plan); xtc_midi_plan_destroy(midi); return r; }
|
2026-10-27 00:31:03 +01:00
|
|
|
xtc_audio_plan_destroy(handle.audio); handle.audio = plan;
|
2026-10-27 00:53:41 +01:00
|
|
|
xtc_midi_plan_destroy(handle.midi); handle.midi = midi;
|
2026-10-27 00:31:03 +01:00
|
|
|
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; }
|
2026-10-26 23:40:28 +01:00
|
|
|
void xtc_backend_collect(xtc_backend *backend)
|
|
|
|
|
{ CHECK(backend == &handle && alive == 1U); ++collects; }
|
|
|
|
|
}
|
|
|
|
|
int main(int argc, char **argv)
|
|
|
|
|
{
|
|
|
|
|
reset();
|
|
|
|
|
{
|
2026-10-27 00:10:45 +01:00
|
|
|
MainWindow window(false);
|
2026-10-26 23:40:28 +01:00
|
|
|
window.show(argc, argv);
|
|
|
|
|
auto *play = find(&window, "Play");
|
|
|
|
|
auto *stop = find(&window, "Stop");
|
|
|
|
|
auto *reconnect = find(&window, "Reconnect");
|
|
|
|
|
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
|
|
|
|
if (play == nullptr || stop == nullptr || reconnect == nullptr) return 1;
|
|
|
|
|
CHECK(play->active() && !stop->active());
|
|
|
|
|
CHECK(contains(&window, "ready / stopped"));
|
|
|
|
|
press(play);
|
|
|
|
|
CHECK(starts == 1U && !play->active() && stop->active());
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(contains(&window, "running — modular graph"));
|
2026-10-26 23:40:28 +01:00
|
|
|
press(stop);
|
|
|
|
|
CHECK(stops == 1U && play->active() && !stop->active());
|
2026-10-27 03:25:04 +01:00
|
|
|
auto *pause = find(&window, "Pause");
|
|
|
|
|
auto *home = find(&window, "To start");
|
|
|
|
|
CHECK(pause && home && !pause->active() && home->active());
|
|
|
|
|
engine_status.sample_position = 48000;
|
|
|
|
|
press(play);
|
|
|
|
|
CHECK(pause->active());
|
|
|
|
|
CHECK(window.pause_playback() == XTC_OK && engine_status.sample_position == 48000);
|
|
|
|
|
CHECK(timeline(&window)->position() == 1920 && play->active() && !pause->active());
|
|
|
|
|
press(play);
|
|
|
|
|
CHECK(engine_status.sample_position == 48000); // Resume does not rewind.
|
|
|
|
|
stop_result = XTC_ERR_IO;
|
|
|
|
|
CHECK(window.return_to_start() == XTC_ERR_IO && engine_status.sample_position == 48000);
|
|
|
|
|
CHECK(!play->active() && stop->active());
|
|
|
|
|
stop_result = XTC_OK;
|
|
|
|
|
seek_result = XTC_ERR_IO;
|
|
|
|
|
CHECK(window.return_to_start() == XTC_ERR_IO && engine_status.sample_position == 48000);
|
|
|
|
|
CHECK(play->active() && !stop->active());
|
|
|
|
|
seek_result = XTC_OK;
|
|
|
|
|
press(home);
|
|
|
|
|
CHECK(engine_status.sample_position == 0 && timeline(&window)->position() == 0);
|
2026-10-26 23:40:28 +01:00
|
|
|
start_result = XTC_ERR_IO;
|
|
|
|
|
press(play);
|
|
|
|
|
CHECK(contains(&window, "Start engine failed: I/O error"));
|
|
|
|
|
pump(); // A refresh must not erase a command error.
|
|
|
|
|
CHECK(contains(&window, "Start engine failed: I/O error"));
|
|
|
|
|
start_result = XTC_OK;
|
|
|
|
|
press(play);
|
|
|
|
|
CHECK(!contains(&window, "Start engine failed"));
|
|
|
|
|
stop_result = XTC_ERR_IO;
|
|
|
|
|
press(stop);
|
|
|
|
|
CHECK(contains(&window, "Stop engine failed") && stop->active());
|
|
|
|
|
stop_result = XTC_OK;
|
|
|
|
|
press(stop);
|
|
|
|
|
Fl_Menu_Bar *menu = nullptr;
|
|
|
|
|
Fl_Tabs *tabs = nullptr;
|
|
|
|
|
for (int i = 0; i < window.children(); ++i) {
|
|
|
|
|
if (auto *found = dynamic_cast<Fl_Menu_Bar *>(window.child(i))) menu = found;
|
|
|
|
|
if (auto *found = dynamic_cast<Fl_Tabs *>(window.child(i))) tabs = found;
|
|
|
|
|
}
|
|
|
|
|
CHECK(menu != nullptr && tabs != nullptr);
|
|
|
|
|
if (menu == nullptr || tabs == nullptr) return 1;
|
|
|
|
|
for (const char *view : {"View/Mixer", "View/Modular Graph", "View/Sequencer"}) {
|
|
|
|
|
const Fl_Menu_Item *item = menu->find_item(view);
|
|
|
|
|
CHECK(item != nullptr);
|
|
|
|
|
if (item != nullptr) {
|
|
|
|
|
menu->picked(item);
|
|
|
|
|
CHECK(std::strcmp(tabs->value()->label(), std::strchr(view, '/') + 1) == 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
GraphWidget *canvas = nullptr;
|
|
|
|
|
auto *graph_group = dynamic_cast<Fl_Group *>(find(&window, "Modular Graph"));
|
|
|
|
|
CHECK(graph_group != nullptr);
|
|
|
|
|
if (graph_group != nullptr) {
|
|
|
|
|
for (int i = 0; i < graph_group->children(); ++i) if (auto *scroll = dynamic_cast<Fl_Group *>(graph_group->child(i)))
|
|
|
|
|
for (int j = 0; j < scroll->children(); ++j)
|
|
|
|
|
if (auto *found = dynamic_cast<GraphWidget *>(scroll->child(j))) canvas = found;
|
|
|
|
|
}
|
|
|
|
|
CHECK(canvas != nullptr);
|
|
|
|
|
if (canvas == nullptr) return 1;
|
|
|
|
|
menu->picked(menu->find_item("View/Modular Graph"));
|
|
|
|
|
press(find(&window, "Add oscillator"));
|
|
|
|
|
press(find(&window, "Add gain"));
|
|
|
|
|
press(find(&window, "Add meter"));
|
|
|
|
|
press(find(&window, "Add output"));
|
|
|
|
|
size_t nodes, edges;
|
|
|
|
|
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 4U && edges == 0U);
|
|
|
|
|
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_OK);
|
|
|
|
|
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
|
|
|
|
CHECK(canvas->connect_ports({2U, 1U, 3U, 0U}) == XTC_OK);
|
|
|
|
|
float rendered[32];
|
|
|
|
|
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK);
|
|
|
|
|
CHECK(std::fabs(rendered[1] - static_cast<float>(std::sin(6.283185307179586 * 440.0 / 48000.0) * 0.1)) < 0.00001f);
|
|
|
|
|
const xtc_graph *before_invalid = canvas->graph();
|
|
|
|
|
unsigned int before_submit = submissions;
|
|
|
|
|
CHECK(canvas->connect_ports({2U, 1U, 1U, 0U}) == XTC_ERR_GRAPH_CYCLE);
|
|
|
|
|
CHECK(canvas->graph() == before_invalid && submissions == before_submit && contains(&window, "graph contains a cycle"));
|
|
|
|
|
CHECK(canvas->connect_ports({1U, 0U, 2U, 0U}) == XTC_ERR_INVALID_ARGUMENT);
|
|
|
|
|
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_ERR_INVALID_STATE);
|
|
|
|
|
submit_result = XTC_ERR_CAPACITY;
|
|
|
|
|
xtc_plan *before_plan = handle.plan;
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_CAPACITY);
|
|
|
|
|
CHECK(canvas->graph() == before_invalid && handle.plan == before_plan && contains(&window, "Graph unchanged: capacity"));
|
|
|
|
|
submit_result = XTC_ERR_IO;
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_IO && canvas->graph() == before_invalid);
|
|
|
|
|
submit_result = XTC_OK;
|
|
|
|
|
CHECK(canvas->select_node(0U) && canvas->move_node(0U, 65, 90));
|
|
|
|
|
CHECK(submissions == before_submit + 2U); // Moving is GUI state, no compilation.
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
|
|
|
|
int left, top;
|
|
|
|
|
CHECK(canvas->node_position(0U, left, top) && left == 65 && top == 90);
|
|
|
|
|
CHECK(canvas->delete_selected() == XTC_OK); // Newly added node.
|
|
|
|
|
CHECK(canvas->select_connection(1U, 2U) && canvas->delete_selected() == XTC_OK);
|
|
|
|
|
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK && rendered[1] == 0.0f);
|
|
|
|
|
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
|
|
|
|
CHECK(canvas->select_node(1U) && canvas->delete_selected() == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3U && edges == 1U);
|
|
|
|
|
xtc_node_info surviving{};
|
|
|
|
|
CHECK(xtc_graph_get_node(canvas->graph(), 1U, &surviving) == XTC_OK && surviving.id == 2U);
|
|
|
|
|
engine_status.active_revision = engine_status.submitted_revision;
|
|
|
|
|
press(play);
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "Graph: active in engine"));
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
|
|
|
|
CHECK(contains(&window, "Graph: queued"));
|
|
|
|
|
engine_status.active_revision = engine_status.submitted_revision;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "Graph: active in engine"));
|
|
|
|
|
press(stop);
|
|
|
|
|
for (size_t id : {0U, 2U, 3U, 5U}) { CHECK(canvas->select_node(id)); CHECK(canvas->delete_selected() == XTC_OK); }
|
|
|
|
|
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 0U && edges == 0U);
|
|
|
|
|
CHECK(xtc_plan_process(handle.plan, nullptr, rendered, 32U) == XTC_OK && rendered[1] == 0.0f);
|
2026-10-27 00:10:45 +01:00
|
|
|
auto *sequence = timeline(&window);
|
|
|
|
|
CHECK(sequence != nullptr);
|
|
|
|
|
char directory[] = "/tmp/xtc-gui-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
|
|
|
|
|
std::string filename = std::string(directory) + "/song.xtc";
|
|
|
|
|
press(find(&window, "Add track")); press(find(&window, "Add region"));
|
|
|
|
|
size_t track_count, clip_count;
|
|
|
|
|
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && track_count == 1 && clip_count == 1);
|
|
|
|
|
xtc_clip_info region{};
|
|
|
|
|
CHECK(xtc_project_get_clip(sequence->project(), 0, ®ion) == XTC_OK);
|
|
|
|
|
gesture(sequence, 150, 50, 144, 0); // Move by three beats.
|
|
|
|
|
gesture(sequence, 265, 50, 48, 0); // Trim left edge by one beat.
|
|
|
|
|
CHECK(xtc_project_get_clip(sequence->project(), 0, ®ion) == XTC_OK &&
|
|
|
|
|
region.start_tick == 3840 && region.length_tick == 2880 && region.offset_tick == 960);
|
|
|
|
|
int width = sequence->w();
|
|
|
|
|
press(find(&window, "Zoom +")); CHECK(sequence->w() > width);
|
|
|
|
|
press(find(&window, "Zoom -")); CHECK(sequence->w() == width);
|
|
|
|
|
CHECK(window.save_project(filename.c_str()) == XTC_OK);
|
|
|
|
|
const xtc_project *retained = sequence->project();
|
|
|
|
|
CHECK(window.open_project("/missing/xtc-project") == XTC_ERR_IO && sequence->project() == retained);
|
2026-10-27 03:25:04 +01:00
|
|
|
press(play);
|
2026-10-27 00:10:45 +01:00
|
|
|
submit_result = XTC_ERR_CAPACITY;
|
|
|
|
|
CHECK(window.open_project(filename.c_str()) == XTC_ERR_CAPACITY && sequence->project() == retained);
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(play->active() && !stop->active()); // Failed install refreshed stopped state.
|
2026-10-27 00:10:45 +01:00
|
|
|
submit_result = XTC_OK;
|
|
|
|
|
press(find(&window, "Delete region/track"));
|
|
|
|
|
CHECK(window.open_project(filename.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_get_clip(sequence->project(), 0, ®ion) == XTC_OK && region.start_tick == 3840 && region.offset_tick == 960);
|
|
|
|
|
engine_status.sample_position = 48000; pump();
|
|
|
|
|
CHECK(sequence->position() == 1920);
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
|
|
|
|
CHECK(sequence->position() == 1920); // Graph edits preserve the transport.
|
|
|
|
|
engine_status.sample_position = 0;
|
2026-10-27 00:31:03 +01:00
|
|
|
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);
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(!pause->active() && !home->active());
|
|
|
|
|
CHECK(window.pause_playback() == XTC_ERR_INVALID_STATE);
|
|
|
|
|
CHECK(window.return_to_start() == XTC_ERR_INVALID_STATE);
|
|
|
|
|
CHECK(window.new_project() == XTC_ERR_INVALID_STATE);
|
2026-10-27 00:31:03 +01:00
|
|
|
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);
|
2026-10-27 00:53:41 +01:00
|
|
|
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(), ¬e_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;
|
2026-10-27 01:06:57 +01:00
|
|
|
// Mixer edits and automation are undoable; failed backend install retains cursor.
|
|
|
|
|
auto *mixer_ui = window.mixer(); CHECK(mixer_ui != nullptr);
|
|
|
|
|
xtc_track_info mixed{};
|
|
|
|
|
CHECK(mixer_ui->mix(0.5f, -1, 0, 1) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1 && mixed.solo);
|
|
|
|
|
CHECK(mixer_ui->point(0, 0) == XTC_OK && mixer_ui->point(960, 1) == XTC_OK);
|
|
|
|
|
size_t points = 0;
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
|
|
|
|
CHECK(window.undo() == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
|
|
|
|
|
submit_result = XTC_ERR_CAPACITY;
|
|
|
|
|
CHECK(window.undo(true) == XTC_ERR_CAPACITY);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 1);
|
|
|
|
|
submit_result = XTC_OK;
|
|
|
|
|
CHECK(window.undo(true) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
|
|
|
|
CHECK(window.undo() == XTC_OK && mixer_ui->point(1920, 0.25f) == XTC_OK && window.undo(true) == XTC_ERR_NOT_FOUND);
|
|
|
|
|
CHECK(window.save_project(filename.c_str()) == XTC_OK && window.open_project(filename.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.gain == 0.5f && mixed.pan == -1);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
|
|
|
|
CHECK(window.undo() == XTC_ERR_NOT_FOUND); // Open resets history.
|
|
|
|
|
auto *lane = mixer_ui->child(mixer_ui->children() - 1);
|
|
|
|
|
saved_x = Fl::e_x; saved_y = Fl::e_y; saved_key = Fl::e_keysym;
|
|
|
|
|
Fl::e_x = lane->x() + (lane->w() - 1) / 4; Fl::e_y = lane->y() + lane->h() / 2;
|
|
|
|
|
Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
|
|
|
|
|
CHECK(lane->handle(FL_PUSH) == 1);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 3);
|
|
|
|
|
Fl::e_keysym = FL_Button + FL_RIGHT_MOUSE;
|
|
|
|
|
CHECK(lane->handle(FL_PUSH) == 1);
|
|
|
|
|
CHECK(xtc_project_automation_count(sequence->project(), &points) == XTC_OK && points == 2);
|
|
|
|
|
Fl::e_keysym = 'm'; CHECK(mixer_ui->handle(FL_KEYDOWN) == 1);
|
|
|
|
|
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && mixed.mute);
|
|
|
|
|
CHECK(window.undo() == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_get_track(sequence->project(), 0, &mixed) == XTC_OK && !mixed.mute);
|
|
|
|
|
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
|
2026-10-27 00:10:45 +01:00
|
|
|
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
|
2026-10-26 23:40:28 +01:00
|
|
|
engine_status.xruns = 3U;
|
|
|
|
|
engine_status.processed_blocks = 123U;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "123 blocks | 3 xruns"));
|
|
|
|
|
engine_status.sample_rate_changed = 1;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "sample rate changed") && !play->active());
|
|
|
|
|
engine_status.sample_rate_changed = 0;
|
|
|
|
|
engine_status.server_lost = 1;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "server lost") && !play->active());
|
|
|
|
|
engine_status.server_lost = 0;
|
|
|
|
|
press(reconnect);
|
|
|
|
|
CHECK(contains(&window, "ready / stopped") && play->active());
|
|
|
|
|
status_result = XTC_ERR_IO;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(contains(&window, "status unavailable") && !play->active());
|
|
|
|
|
status_result = XTC_OK;
|
|
|
|
|
press(reconnect);
|
|
|
|
|
close_result = XTC_ERR_IO;
|
|
|
|
|
press(reconnect);
|
|
|
|
|
CHECK(contains(&window, "Reconnect could not close JACK") && alive == 1U);
|
|
|
|
|
window.do_callback();
|
|
|
|
|
CHECK(window.shown() && alive == 1U && contains(&window, "window kept open for retry"));
|
|
|
|
|
close_result = XTC_OK;
|
|
|
|
|
menu->picked(menu->find_item("File/Quit"));
|
|
|
|
|
CHECK(!window.shown() && alive == 0U);
|
|
|
|
|
unsigned int before = collects;
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(collects == before); // Removed timer cannot call a closed backend.
|
|
|
|
|
}
|
|
|
|
|
for (xtc_result failure : {XTC_ERR_IO, XTC_ERR_UNSUPPORTED}) {
|
|
|
|
|
reset();
|
|
|
|
|
open_result = failure;
|
2026-10-27 00:10:45 +01:00
|
|
|
MainWindow window(false);
|
2026-10-26 23:40:28 +01:00
|
|
|
window.show(argc, argv);
|
|
|
|
|
CHECK(contains(&window, "Open JACK failed") && !find(&window, "Play")->active());
|
|
|
|
|
open_result = XTC_OK;
|
|
|
|
|
press(find(&window, "Reconnect"));
|
|
|
|
|
CHECK(find(&window, "Play")->active());
|
|
|
|
|
press(find(&window, "Play"));
|
|
|
|
|
window.do_callback(); // Close while running, without a preceding stop.
|
|
|
|
|
CHECK(!window.shown() && alive == 0U);
|
|
|
|
|
}
|
|
|
|
|
reset();
|
|
|
|
|
partial_open = true;
|
|
|
|
|
open_result = XTC_ERR_IO;
|
|
|
|
|
{
|
2026-10-27 00:10:45 +01:00
|
|
|
MainWindow window(false);
|
2026-10-26 23:40:28 +01:00
|
|
|
CHECK(alive == 1U && !find(&window, "Play")->active());
|
|
|
|
|
window.do_callback(); // Partially initialized handles are closed only.
|
|
|
|
|
CHECK(alive == 0U);
|
|
|
|
|
}
|
|
|
|
|
reset();
|
|
|
|
|
for (int i = 0; i < 10; ++i) {
|
2026-10-27 00:10:45 +01:00
|
|
|
MainWindow window(false);
|
2026-10-26 23:40:28 +01:00
|
|
|
press(find(&window, "Play"));
|
|
|
|
|
window.do_callback();
|
|
|
|
|
}
|
|
|
|
|
CHECK(alive == 0U);
|
2026-10-27 06:28:25 +01:00
|
|
|
reset();
|
|
|
|
|
{
|
|
|
|
|
MainWindow window(false); auto *s = timeline(&window);
|
|
|
|
|
CHECK(s->add_track() == XTC_OK && s->rename_selection("Keys") == XTC_OK);
|
|
|
|
|
CHECK(s->add_region() == XTC_OK && s->rename_selection("Verse") == XTC_OK);
|
|
|
|
|
CHECK(s->grid(120) == XTC_OK && s->grid() == 120 && s->grid(0) == XTC_ERR_INVALID_ARGUMENT);
|
|
|
|
|
CHECK(s->handle(FL_FOCUS) == 1);
|
|
|
|
|
int old_key = Fl::e_keysym, old_state = Fl::e_state;
|
|
|
|
|
Fl::e_keysym = 'd'; Fl::e_state = FL_CTRL;
|
|
|
|
|
CHECK(s->handle(FL_KEYDOWN) == 1);
|
|
|
|
|
Fl::e_keysym = old_key; Fl::e_state = old_state;
|
|
|
|
|
size_t tracks, clips; xtc_project_get_counts(s->project(),&tracks,&clips);
|
|
|
|
|
CHECK(clips == 2);
|
|
|
|
|
xtc_clip_info c{}; CHECK(xtc_project_get_clip(s->project(),1,&c) == XTC_OK && c.start_tick == 3840 && !std::strcmp(c.name,"Verse"));
|
|
|
|
|
CHECK(window.undo(false) == XTC_OK); xtc_project_get_counts(s->project(),&tracks,&clips); CHECK(clips == 1);
|
|
|
|
|
CHECK(window.undo(true) == XTC_OK); xtc_project_get_counts(s->project(),&tracks,&clips); CHECK(clips == 2);
|
|
|
|
|
CHECK(s->add_track() == XTC_OK && s->rename_selection("Bass") == XTC_OK && s->move_track(-1) == XTC_OK);
|
|
|
|
|
xtc_track_info t{}; CHECK(xtc_project_get_track(s->project(),0,&t) == XTC_OK && !std::strcmp(t.name,"Bass"));
|
|
|
|
|
CHECK(window.undo(false) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_get_track(s->project(),0,&t) == XTC_OK && !std::strcmp(t.name,"Keys"));
|
|
|
|
|
window.do_callback();
|
|
|
|
|
}
|
2026-10-27 04:00:27 +01:00
|
|
|
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
|
|
|
|
|
reset();
|
|
|
|
|
MainWindow window(false);
|
|
|
|
|
auto *sequence = timeline(&window);
|
|
|
|
|
size_t id = SIZE_MAX;
|
|
|
|
|
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
|
|
|
|
|
xtc_plugin_spec spec{};
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
|
|
|
|
|
CHECK(window.set_plugin_control(id,spec.ports[0],.5f) == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
|
|
|
|
CHECK(window.set_plugin_control(id,spec.ports[0],3) == XTC_ERR_UNSUPPORTED);
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
|
|
|
|
CHECK(window.undo(false) == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1);
|
|
|
|
|
CHECK(window.undo(true) == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
|
|
|
|
const xtc_project *before = sequence->project();
|
|
|
|
|
CHECK(window.add_plugin("/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
|
|
|
|
CHECK(window.add_plugin(module,2) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
|
2026-10-27 06:25:30 +01:00
|
|
|
xtc_plugin_point point{960,spec.ports[0],0,.25f};
|
|
|
|
|
CHECK(window.set_plugin_automation(id,point) == XTC_OK);
|
|
|
|
|
CHECK(window.set_plugin_automation(id,point,true) == XTC_OK);
|
|
|
|
|
CHECK(window.undo(false) == XTC_OK);
|
2026-10-27 04:00:27 +01:00
|
|
|
CHECK(window.save_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
|
|
|
|
|
CHECK(window.new_project() == XTC_OK);
|
|
|
|
|
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
2026-10-27 06:25:30 +01:00
|
|
|
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
|
2026-10-27 04:00:27 +01:00
|
|
|
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
|
|
|
|
|
CHECK(xtc_project_create(&missing) == XTC_OK);
|
|
|
|
|
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
|
|
|
|
|
std::strcpy(spec.path,"/absent/xtc-test-plugin.so");
|
|
|
|
|
CHECK(xtc_graph_add_plugin(graph,&spec,&absent) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_set_graph(missing,graph) == XTC_OK);
|
|
|
|
|
CHECK(xtc_project_save(missing,"/tmp/xtc-gui-missing-plugin.xtc") == XTC_OK);
|
|
|
|
|
before = sequence->project();
|
|
|
|
|
CHECK(window.open_project("/tmp/xtc-gui-missing-plugin.xtc") == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
|
|
|
|
CHECK(contains(&window,"/absent/xtc-test-plugin.so"));
|
|
|
|
|
CHECK(window.add_plugin(module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
|
|
|
|
|
xtc_project_destroy(missing); xtc_graph_destroy(graph);
|
|
|
|
|
unlink("/tmp/xtc-gui-missing-plugin.xtc");
|
|
|
|
|
unlink("/tmp/xtc-gui-plugin-test.xtc");
|
|
|
|
|
window.do_callback();
|
|
|
|
|
}
|
2026-10-26 23:40:28 +01:00
|
|
|
if (failures != 0) return 1;
|
|
|
|
|
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|