1
0
Fork 0
forked from hanez/xtc
xtc/tests/integration/test_gui.cpp

574 lines
32 KiB
C++
Raw Normal View History

#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 "../../src/gui/mixer_widget.hpp"
#include <unistd.h>
#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>
#include <cmath>
#include <initializer_list>
#include <cstdlib>
/* 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_midi_plan *midi = nullptr; xtc_recorder *recorder = nullptr; };
namespace {
xtc_backend handle;
xtc_result seek_result = XTC_OK;
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
xtc_result status_result = XTC_OK, submit_result = XTC_OK;
unsigned int submissions = 0U;
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;
}
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;
}
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;
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;
}
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;
submit_result = seek_result = XTC_OK;
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)
{
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 2U);
++opens;
*out = open_result == XTC_OK || partial_open ? &handle : nullptr;
if (*out != nullptr) { ++alive; handle.running = 0; CHECK(handle.plan == nullptr); engine_status.active_revision = engine_status.submitted_revision = 0U; }
return open_result;
}
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; xtc_midi_plan_destroy(handle.midi); handle.midi = nullptr; }
return close_result;
}
xtc_result xtc_backend_start(xtc_backend *backend)
{ 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_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); }
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;
}
xtc_result xtc_backend_seek(xtc_backend *, uint64_t sample)
{ if (handle.running) return XTC_ERR_INVALID_STATE; if (seek_result != XTC_OK) return seek_result; engine_status.sample_position = sample; return XTC_OK; }
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;
}
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); 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;
}
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; }
}
int main(int argc, char **argv)
{
reset();
{
MainWindow window(false);
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());
CHECK(contains(&window, "running — modular graph"));
press(stop);
CHECK(stops == 1U && play->active() && !stop->active());
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);
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);
}
}
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);
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, &region) == 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, &region) == 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);
press(play);
submit_result = XTC_ERR_CAPACITY;
CHECK(window.open_project(filename.c_str()) == XTC_ERR_CAPACITY && sequence->project() == retained);
CHECK(play->active() && !stop->active()); // Failed install refreshed stopped state.
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, &region) == 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;
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);
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);
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);
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);
// Piano-roll filtered commands use one undo step and preserve durations.
uint64_t edited_clip = roll->clip();
CHECK(roll->set_grid(160) == XTC_OK && roll->set_grid(320) == XTC_OK);
CHECK(roll->set_grid(7) == XTC_ERR_INVALID_ARGUMENT);
CHECK(roll->set_filter(0, 60, 72) == XTC_OK);
int note_key = Fl::e_keysym, note_state = Fl::e_state;
Fl::e_keysym = 'a'; Fl::e_state = FL_CTRL;
CHECK(roll->handle(FL_KEYDOWN) == 1);
Fl::e_keysym = note_key; Fl::e_state = note_state;
xtc_note_edit properties{XTC_NOTES_EDIT, 0, 0, 77, 2, 0, 0};
CHECK(roll->apply(properties) == XTC_OK);
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.velocity == 77 && drawn.channel == 2);
CHECK(roll->set_filter(-1, 0, 127) == XTC_OK);
roll->select_filtered();
Fl::e_keysym = 'q'; Fl::e_state = 0;
CHECK(roll->handle(FL_KEYDOWN) == 1);
Fl::e_keysym = note_key; Fl::e_state = note_state;
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.start_tick == 640 && drawn.length_tick == 1440);
Fl::e_keysym = FL_Up; Fl::e_state = 0;
CHECK(roll->handle(FL_KEYDOWN) == 1);
Fl::e_keysym = note_key; Fl::e_state = note_state;
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 62);
xtc_note_edit repeat{XTC_NOTES_REPEAT, 0, 0, -1, -1, 1, 960};
CHECK(roll->apply(repeat) == XTC_OK);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 2);
CHECK(roll->delete_selected() == XTC_OK);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 0);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 2);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 0);
for (int step = 0; step < 5; ++step) CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.pitch == 61 && drawn.start_tick == 720 && drawn.velocity == 100 && drawn.channel == 0);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 1);
roll->set_project(sequence->project(), edited_clip);
CHECK(roll->set_grid(160) == XTC_OK && roll->set_filter(5, 60, 60) == XTC_OK);
Fl::e_x = roll->x() + 104; 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;
CHECK(xtc_project_get_note(sequence->project(), 1, &drawn) == XTC_OK && drawn.start_tick == 320 && drawn.channel == 5 && drawn.pitch == 60);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_note_count(sequence->project(), &note_count) == XTC_OK && note_count == 1);
CHECK(roll->set_grid(240) == XTC_OK && roll->set_filter(-1, 0, 127) == XTC_OK);
roll->set_project(sequence->project(), edited_clip);
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;
// 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;
CHECK(unlink(filename.c_str()) == 0 && rmdir(directory) == 0);
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;
MainWindow window(false);
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;
{
MainWindow window(false);
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) {
MainWindow window(false);
press(find(&window, "Play"));
window.do_callback();
}
CHECK(alive == 0U);
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();
}
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);
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);
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);
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
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();
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;
}