xtc/tests/integration/test_gui.cpp

1223 lines
76 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 "../../src/gui/plugin_dialog.hpp"
#include "../../src/gui/preferences_dialog.hpp"
2026-10-28 17:16:01 +01:00
#include "../../src/gui/command_palette.hpp"
#include <FL/Fl_Value_Input.H>
#include "../../src/gui/plugin_library.hpp"
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Value_Slider.H>
#include <FL/Fl_Dial.H>
#include <FL/Fl_Check_Button.H>
#include <FL/Fl_Scroll.H>
#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 <utility>
#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, reserve_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;
}
GraphWidget *graph_canvas(Fl_Group *group)
{
for (int i = 0; i < group->children(); ++i) {
if (auto *canvas = dynamic_cast<GraphWidget *>(group->child(i))) return canvas;
if (auto *nested = dynamic_cast<Fl_Group *>(group->child(i)))
if (auto *canvas = graph_canvas(nested)) return canvas;
}
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 = reserve_result = XTC_OK;
engine_status = {};
engine_status.sample_rate = 48000U;
engine_status.buffer_frames = 1024U;
partial_open = false;
}
}
extern "C" {
xtc_result __real_xtc_history_reserve(xtc_history *history);
xtc_result __wrap_xtc_history_reserve(xtc_history *history) {
return reserve_result == XTC_OK ? __real_xtc_history_reserve(history) : reserve_result;
}
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_loop(xtc_backend *,uint64_t start,uint64_t end,int enabled)
{ return enabled && (end<=start || end-start<64) ? XTC_ERR_INVALID_ARGUMENT : XTC_OK; }
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_read_meters(xtc_backend *, xtc_meter_snapshot *out)
{
*out = {}; out->master_left = engine_status.peak_left; out->master_right = engine_status.peak_right;
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, 210, 50, 144, 0); // Move by three beats.
gesture(sequence, 325, 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";
size_t before_take_nodes = 0, before_take_edges = 0;
CHECK(xtc_graph_get_counts(canvas->graph(),&before_take_nodes,&before_take_edges) == XTC_OK);
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.undo(false) == XTC_OK);
CHECK(xtc_project_get_counts(sequence->project(),&track_count,&clip_count) == XTC_OK && clip_count == 1);
CHECK(access(take.c_str(),F_OK) == 0); // Undo never deletes the external take.
CHECK(window.undo(false) == XTC_OK);
size_t after_take_nodes = 0, after_take_edges = 0;
CHECK(xtc_graph_get_counts(canvas->graph(),&after_take_nodes,&after_take_edges) == XTC_OK && after_take_nodes+1 == before_take_nodes);
CHECK(window.undo(true) == XTC_OK && window.undo(true) == XTC_OK);
CHECK(xtc_project_get_counts(sequence->project(),&track_count,&clip_count) == XTC_OK && clip_count == 2);
CHECK(sequence->waveform_count() == 1);
// Short clips use their four-pixel draw width for hit testing too.
xtc_clip_info audio_clip{};
CHECK(xtc_project_get_clip(sequence->project(), 1, &audio_clip) == XTC_OK && audio_clip.asset_path[0]);
size_t audio_row = 0;
for (; audio_row < track_count; ++audio_row) {
xtc_track_info t{}; xtc_project_get_track(sequence->project(), audio_row, &t);
if (t.id == audio_clip.track_id) break;
}
gesture(sequence, 181 + static_cast<int>(audio_clip.start_tick*48/960), 50 + static_cast<int>(audio_row)*56, 0, 0);
CHECK(sequence->selected_clip() == audio_clip.id);
CHECK(sequence->audio_properties(0.5f, 1, 1) == XTC_OK);
sequence->position(audio_clip.start_tick+1);
int split_key = Fl::e_keysym, split_state = Fl::e_state;
Fl::e_keysym = 's'; Fl::e_state = 0;
CHECK(sequence->handle(FL_KEYDOWN) == 1);
Fl::e_keysym = split_key; Fl::e_state = split_state;
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 3);
CHECK(sequence->duplicate_selection() == XTC_OK && sequence->waveform_count() == 1);
CHECK(sequence->delete_selection() == XTC_OK);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 4);
CHECK(window.undo() == XTC_OK && window.undo() == XTC_OK && window.undo() == XTC_OK);
CHECK(xtc_project_find_clip(sequence->project(), audio_clip.id, &audio_clip) == XTC_OK && audio_clip.gain == 1 && audio_clip.fade_in_tick == 0 && audio_clip.length_tick == 3);
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();
}
{
reset();
MainWindow window(false,"");
auto *canvas = graph_canvas(&window);
CHECK(canvas != nullptr);
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
CHECK(canvas->add_node(XTC_NODE_OUTPUT) == XTC_OK);
CHECK(canvas->connect_ports({0,1,1,0}) == XTC_OK);
CHECK(canvas->select_connection(0,1) && contains(&window, "Wire: #0 audio out -> #1 sum in"));
const xtc_graph *original = canvas->graph();
unsigned int submitted = submissions;
submit_result = XTC_ERR_CAPACITY;
CHECK(canvas->insert_gain() == XTC_ERR_CAPACITY && canvas->graph() == original && submissions == submitted+1);
submit_result = XTC_OK;
CHECK(canvas->insert_gain() == XTC_OK);
size_t inserted = canvas->selected_node(), nodes, edges;
CHECK(inserted == 2);
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3 && edges == 2);
xtc_routing_info route{};
CHECK(canvas->routing(inserted,route) && route.level == 1 && route.reaches_output && route.inputs == 1);
submitted = submissions;
CHECK(canvas->auto_layout() && submissions == submitted);
int sx = 0, sy = 0, gx = 0, gy = 0, ox = 0, oy = 0;
CHECK(canvas->node_position(0,sx,sy) && canvas->node_position(inserted,gx,gy) && canvas->node_position(1,ox,oy));
CHECK(sx < gx && gx < ox);
CHECK(canvas->select_node(inserted) && contains(&window,"1 incoming, 1 outgoing"));
CHECK(canvas->select_connection(0,inserted) && canvas->delete_selected() == XTC_OK);
CHECK(canvas->select_node(inserted) && contains(&window,"unwired input uses external audio"));
CHECK(window.undo() == XTC_OK); // Disconnect is independently undoable.
CHECK(window.undo() == XTC_OK); // Whole insertion is one undo step.
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 2 && edges == 1);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_graph_get_counts(canvas->graph(), &nodes, &edges) == XTC_OK && nodes == 3 && edges == 2);
CHECK(canvas->select_node(inserted));
int key = Fl::e_keysym, state = Fl::e_state;
Fl::e_keysym = FL_Right; Fl::e_state = 0;
CHECK(canvas->handle(FL_KEYDOWN) == 1 && canvas->selected_node() == 0);
Fl::e_keysym = key; Fl::e_state = state;
// Whole-project binding must give every new node a distinct position.
canvas->reset_layout(); canvas->set_graph(canvas->graph());
CHECK(canvas->node_position(0,sx,sy) && canvas->node_position(inserted,gx,gy) && canvas->node_position(1,ox,oy));
CHECK((sx != gx || sy != gy) && (sx != ox || sy != oy) && (gx != ox || gy != oy));
window.do_callback();
}
{
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window); auto *mixer = window.mixer();
CHECK(sequence->add_track() == XTC_OK);
CHECK(mixer->add_bus() == XTC_OK);
uint64_t bus = mixer->selected_track();
xtc_track_info track{};
CHECK(xtc_project_get_track(sequence->project(),1,&track) == XTC_OK && track.is_bus);
CHECK(mixer->mix(.5f,1,0,1) == XTC_OK);
CHECK(mixer->select_track(0) && mixer->route(bus) == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == bus);
CHECK(window.undo() == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == UINT64_MAX);
CHECK(window.undo(true) == XTC_OK);
CHECK(xtc_project_get_track(sequence->project(),0,&track) == XTC_OK && track.bus_id == bus);
mixer->peaks(1.25f,.5f); mixer->track_peaks(.5f,.25f);
CHECK(mixer->master_clipped() && contains(&window,"dBFS CLIP"));
mixer->peaks(0,0); CHECK(mixer->master_clipped());
mixer->reset_meters(); CHECK(!mixer->master_clipped());
window.do_callback();
}
{
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window);
unsigned int bindings[3] = {1,74,11}, actual[3];
CHECK(window.set_control_map(2,bindings) == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 1);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 7);
CHECK(window.undo(true) == XTC_OK);
const xtc_project *before = sequence->project();
bindings[1] = 1;
CHECK(window.set_control_map(2,bindings) == XTC_ERR_INVALID_ARGUMENT && sequence->project() == before);
auto *mixer_ui = window.mixer();
CHECK(sequence->add_track() == XTC_OK);
auto *mode = dynamic_cast<Fl_Choice *>(find(&window,"Interpolation")); CHECK(mode);
if (mode) mode->value(1);
CHECK(mixer_ui->point(0,.25f) == XTC_OK);
xtc_automation_point point{};
CHECK(xtc_project_get_automation(sequence->project(),0,&point) == XTC_OK && point.step == 1);
CHECK(window.save_project("/tmp/xtc-gui-controls-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-gui-controls-test.xtc") == XTC_OK);
CHECK(xtc_project_get_control_map(sequence->project(),2,actual) == XTC_OK && actual[0] == 1 && actual[1] == 74);
CHECK(xtc_project_get_automation(sequence->project(),0,&point) == XTC_OK && point.step == 1);
unlink("/tmp/xtc-gui-controls-test.xtc"); window.do_callback();
}
{
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window); auto *canvas = graph_canvas(&window);
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
const uint64_t track = sequence->selected_track(), clip = sequence->selected_clip();
CHECK(window.open_piano_roll() == XTC_OK);
CHECK(window.piano_roll()->add_note(0,240,60) == XTC_OK);
const uint64_t note = window.piano_roll()->selected_note();
CHECK(window.piano_roll()->add_note(240,240,64) == XTC_OK);
CHECK(window.piano_roll()->select_note(note));
CHECK(window.undo(false) == XTC_OK);
CHECK(sequence->selected_track() == track && sequence->selected_clip() == clip);
CHECK(window.piano_roll()->clip() == clip && window.piano_roll()->selected_note() == note);
const xtc_project *before = sequence->project(); unsigned int bindings[3] = {1,74,11};
unsigned int old_submissions = submissions;
reserve_result = XTC_ERR_OUT_OF_MEMORY;
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
CHECK(window.set_control_map(0,bindings) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
CHECK(window.import_audio("/absent/not-loaded.wav") == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
CHECK(submissions == old_submissions);
reserve_result = XTC_OK;
CHECK(window.undo(true) == XTC_OK); // Failed commands preserved redo.
size_t count = 0;
CHECK(xtc_project_note_count(sequence->project(),&count) == XTC_OK && count == 2);
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
const size_t node = canvas->selected_node(); int left, top;
CHECK(canvas->node_position(node,left,top) && canvas->select_at(left+50,top+30) && canvas->selected_node() == node);
CHECK(!canvas->select_at(-20,-20) && canvas->selected_node() == SIZE_MAX);
window.do_callback();
}
// Internal devices must work without plugin fixture environment variables.
for (unsigned int instrument=0;instrument<2;++instrument) {
reset(); MainWindow window(false,""); window.show();
CHECK(window.create_track(true)==XTC_OK);
auto *sequence=timeline(&window); auto *canvas=graph_canvas(&window);
uint64_t track=sequence->selected_track();
auto *selector=sequence->instrument_choice(track); CHECK(selector);
selector->value(static_cast<int>(instrument+1));selector->do_callback();
xtc_plugin_spec spec{};
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==instrument);
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);
spec.values[1]=25;spec.point_count=1;spec.points[0]={100,0,0,.4f};
CHECK(window.set_instrument_spec(track,spec)==XTC_OK);
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.values[1]==25 && spec.point_count==1);
auto *bar=dynamic_cast<Fl_Menu_Bar *>(window.child(0));CHECK(bar);
Fl::add_timeout(.05,[](void *) {press(find(Fl::first_window(),"Apply"));});
bar->picked(bar->find_item("Edit/Selected instrument controls"));
xtc_connection_info wire{};
CHECK(xtc_graph_get_connection(xtc_project_graph(sequence->project()),0,&wire)==XTC_OK);
unsigned int effect=instrument+2;size_t id=SIZE_MAX;
CHECK(window.insert_plugin(wire,XTC_BUILTIN_PATH,effect,&id)==XTC_OK);
CHECK(canvas->select_node(id));
CHECK(window.set_plugin_control(id,2,.75f)==XTC_OK);
CHECK(window.set_plugin_automation(id,{960,2,0,.4f},false)==XTC_OK);
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec)==XTC_OK);
CHECK(spec.index==effect && spec.values[2]==.75f && spec.point_count==1);
xtc_plugin_details details{};xtc_plugin_spec edited{};
CHECK(xtc_plugin_describe(&spec,48000,&details)==XTC_OK);
Fl::add_timeout(.05,[](void *) {press(find(Fl::first_window(),"Apply"));});
CHECK(xtc_edit_plugin(spec,details,edited) && edited.index==effect);
CHECK(window.save_project("/tmp/xtc-internal-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-internal-gui.xtc")==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==instrument);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec)==XTC_OK && spec.index==effect && spec.point_count==1);
size_t added=SIZE_MAX;CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect,&added)==XTC_OK);
CHECK(added!=id);CHECK(window.undo()==XTC_OK);
const xtc_project *before=sequence->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;
CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect)==XTC_ERR_OUT_OF_MEMORY && sequence->project()==before);
reserve_result=XTC_OK;unlink("/tmp/xtc-internal-gui.xtc");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{};
// Exercise the actual modal chooser/editor with copied metadata only.
Fl::add_timeout(.05,[](void *) {
auto *dialog = Fl::first_window();
CHECK(dialog && dialog->label() && std::strcmp(dialog->label(),"Plugin descriptors") == 0);
if (!dialog) return;
for (int i = 0; i < dialog->children(); ++i)
if (auto *list = dynamic_cast<Fl_Hold_Browser *>(dialog->child(i))) {
CHECK(list->size() == 7); list->select(2);
}
auto *choose = find(dialog,"Choose"); CHECK(choose); if (choose) choose->do_callback();
});
unsigned int chosen = UINT32_MAX;
CHECK(xtc_choose_plugin(module,48000,chosen) && chosen == 1);
xtc_plugin_details details{}; xtc_plugin_spec edited{};
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
CHECK(xtc_plugin_describe(&spec,48000,&details) == XTC_OK);
spec.values[0] = .25f; spec.point_count = 1; spec.points[0] = {0,spec.ports[0],0,.5f};
Fl::add_timeout(.05,[](void *) {
auto *dialog = Fl::first_window(); CHECK(dialog);
CHECK(contains(dialog,"Test gain"));
auto *reset = find(dialog,"Reset defaults"); CHECK(reset); if (reset) reset->do_callback();
auto *apply = find(dialog,"Apply"); CHECK(apply); if (apply) apply->do_callback();
});
CHECK(xtc_edit_plugin(spec,details,edited) && edited.values[0] == 1 && !edited.point_count);
edited.values[0] = .75f;
Fl::add_timeout(.05,[](void *) {
auto *dialog = Fl::first_window(); auto *cancel = find(dialog,"Cancel");
CHECK(cancel); if (cancel) cancel->do_callback();
});
CHECK(!xtc_edit_plugin(spec,details,edited) && edited.values[0] == .75f);
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);
CHECK(window.reset_plugin(id) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1 && !spec.point_count);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f && spec.point_count == 1);
// Replace an effect while preserving node ID and wires; undo restores state.
auto *canvas = graph_canvas(&window);
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
const size_t source_id = canvas->selected_node();
CHECK(canvas->connect_ports({source_id,1,id,0}) == XTC_OK);
CHECK(window.replace_plugin(id,module,4) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 4 && spec.values[0] == 2 && !spec.point_count);
before = sequence->project();
CHECK(window.replace_plugin(id,module,1) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
CHECK(window.replace_plugin(id,module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
CHECK(window.replace_plugin(id,"/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 0 && spec.values[0] == .5f && spec.point_count == 1);
const xtc_connection_info wire{source_id,1,id,0}; size_t inserted = SIZE_MAX;
CHECK(window.insert_plugin(wire,module,0,&inserted) == XTC_OK && inserted != id);
xtc_routing_info routing{};
CHECK(canvas->routing(inserted,routing) && routing.inputs == 1 && routing.outputs == 1);
before = sequence->project();
CHECK(window.insert_plugin(wire,module,0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(canvas->select_node(inserted) && canvas->delete_selected() == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_ERR_NOT_FOUND);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_OK);
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();
}
for (const char *variable : {"XTC_TEST_CLAP","XTC_TEST_VST2","XTC_TEST_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false,""); auto *sequence = timeline(&window);
size_t id = SIZE_MAX; xtc_plugin_spec spec{};
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
CHECK(spec.format != XTC_PLUGIN_LADSPA && spec.inputs == 1 && !spec.control_count);
CHECK(window.undo(false) == XTC_OK && window.undo(true) == XTC_OK);
Fl::add_timeout(.05,[](void *) {
auto *dialog = Fl::first_window(); CHECK(dialog);
auto *choose = find(dialog,"Choose"); CHECK(choose); if (choose) choose->do_callback();
});
unsigned int chosen = UINT32_MAX;
CHECK(xtc_choose_plugin(module,48000,chosen) && chosen == 0);
CHECK(window.save_project("/tmp/xtc-gui-modern-test.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK);
CHECK(window.open_project("/tmp/xtc-gui-modern-test.xtc") == XTC_OK);
xtc_plugin_spec loaded{};
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&loaded) == XTC_OK && loaded.format == spec.format);
const xtc_project *before = sequence->project();
CHECK(window.replace_plugin(id,"/absent/modern.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
unlink("/tmp/xtc-gui-modern-test.xtc"); window.do_callback();
}
}
for (const char *variable : {"XTC_TEST_SYNTH_CLAP","XTC_TEST_SYNTH_VST2","XTC_TEST_SYNTH_VST3"}) {
if (const char *module = std::getenv(variable)) {
reset(); MainWindow window(false,""); auto *sequence = timeline(&window);
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
uint64_t track = sequence->selected_track(); xtc_plugin_spec spec{};
CHECK(window.assign_instrument(track,module,0) == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK && spec.instrument);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_ERR_NOT_FOUND);
CHECK(window.undo(true) == XTC_OK);
CHECK(window.select_live_instrument(UINT64_MAX) == XTC_OK);
CHECK(window.select_live_instrument(track) == XTC_OK);
const xtc_project *before = sequence->project();
CHECK(window.assign_instrument(track,"/absent/instrument.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
CHECK(window.save_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-gui-instrument.xtc") == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK && spec.instrument);
CHECK(window.create_track(true) == XTC_OK);
uint64_t second = sequence->selected_track();
auto *selector = sequence->instrument_choice(second); CHECK(selector && selector->value() == 0);
2026-10-28 17:16:01 +01:00
CHECK(selector->size() == 6); // Sine, two internal presets, project instrument, load, terminator.
selector->value(1); selector->do_callback();
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_OK && spec.instrument);
CHECK(sequence->selected_track() == second);
CHECK(sequence->move_track(-1) == XTC_OK);
selector = sequence->instrument_choice(second); selector->value(0); selector->do_callback();
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_ERR_NOT_FOUND);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec) == XTC_OK);
CHECK(window.undo(false) == XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),second,&spec) == XTC_OK);
CHECK(window.assign_instrument(track,nullptr,0) == XTC_OK);
unlink("/tmp/xtc-gui-instrument.xtc"); window.do_callback();
}
}
// Flat workspace creation is one history edit, and failed preparation keeps
// the current model. Exercise real sidebar buttons rather than mock actions.
reset();
{
MainWindow window(false,""); window.show();
auto *sequence = timeline(&window);
CHECK(sequence != nullptr);
auto *midi = find(&window,"New MIDI track");
auto *audio = find(&window,"New audio track");
CHECK(midi && audio && find(&window,"Import WAV") && find(&window,"Edit MIDI notes"));
press(midi);
size_t tracks = 0, clips = 0;
xtc_project_get_counts(sequence->project(),&tracks,&clips);
CHECK(tracks == 1 && clips == 1);
uint64_t track = sequence->selected_track(), clip = sequence->selected_clip();
xtc_clip_info info{}; CHECK(xtc_project_find_clip(sequence->project(),clip,&info) == XTC_OK && info.midi);
CHECK(window.mixer()->selected_track() == track);
CHECK(window.open_piano_roll() == XTC_OK);
CHECK(window.undo(false) == XTC_OK);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 0 && clips == 0);
CHECK(window.undo(true) == XTC_OK);
CHECK(sequence->select(track,clip)); pump();
CHECK(contains(&window,"MIDI track"));
press(audio);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
const xtc_project *before = sequence->project();
reserve_result = XTC_ERR_OUT_OF_MEMORY;
CHECK(window.create_track(true) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
reserve_result = XTC_OK; submit_result = XTC_ERR_INVALID_STATE;
CHECK(window.create_track(false) == XTC_ERR_INVALID_STATE && sequence->project() == before);
submit_result = XTC_OK;
// Exercise the supported minimum and a larger workspace with real layout.
for (auto size : {std::pair<int,int>{1220,720},{1600,1000}}) {
window.resize(window.x(),window.y(),size.first,size.second); pump();
CHECK(midi->x() >= 0 && midi->x()+midi->w() <= sequence->parent()->x());
CHECK(sequence->parent()->x()+sequence->parent()->w() <= window.w());
CHECK(find(&window,"Play")->visible_r() && midi->visible_r());
}
CHECK(window.save_project("/tmp/xtc-workspace.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-workspace.xtc") == XTC_OK);
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
unlink("/tmp/xtc-workspace.xtc"); window.do_callback();
}
reset();
{
MainWindow window(false,""); window.show(); auto *sequence = timeline(&window);
CHECK(window.create_track(true) == XTC_OK);
uint64_t clip = sequence->selected_clip(), track = sequence->selected_track();
auto *snap = dynamic_cast<Fl_Choice *>(find(&window,"Snap")); CHECK(snap && snap->value() == 3);
CHECK(!std::strcmp(snap->text(3),"1/16") && !std::strcmp(snap->text(8),"1 bar"));
snap->value(5); snap->do_callback(); CHECK(sequence->grid() == 480);
CHECK(xtc_project_edit_clip(sequence->project(),clip,track,100,3840,0) == XTC_OK);
CHECK(sequence->rename_selection("Off-grid") == XTC_OK);
gesture(sequence,205,50,100,0);
xtc_clip_info info{}; xtc_project_find_clip(sequence->project(),clip,&info);
CHECK(info.start_tick == 1920); // Snap absolute position, including an off-grid source.
snap->value(0); snap->do_callback(); CHECK(sequence->grid() == 1);
gesture(sequence,300,50,1,0); xtc_project_find_clip(sequence->project(),clip,&info);
CHECK(info.start_tick == 1940); // Off retains pointer precision rather than a musical grid.
CHECK(window.undo(false) == XTC_OK);
CHECK(sequence->select(track,clip));
snap->value(5); snap->do_callback();
gesture(sequence,466,50,-13,0); xtc_project_find_clip(sequence->project(),clip,&info);
CHECK(info.start_tick+info.length_tick == 5280);
gesture(sequence,277,50,13,0); xtc_project_find_clip(sequence->project(),clip,&info);
CHECK(info.start_tick == 2400 && info.offset_tick == 480 && info.length_tick == 2880);
int old_x = Fl::e_x, old_y = Fl::e_y, old_key = Fl::e_keysym, old_clicks = Fl::e_clicks;
Fl::e_x = sequence->x()+320; Fl::e_y = sequence->y()+50;
Fl::e_keysym = FL_Button+FL_LEFT_MOUSE; Fl::e_clicks = 1;
CHECK(sequence->handle(FL_PUSH) == 1); CHECK(window.piano_roll() && window.piano_roll()->clip() == clip);
Fl::e_x = old_x; Fl::e_y = old_y; Fl::e_keysym = old_key; Fl::e_clicks = old_clicks;
auto *piano = window.piano_roll();
CHECK(piano->add_note(0,960,60,100,2) == XTC_OK);
uint64_t note_id = piano->selected_note();
CHECK(piano->add_note(0,960,64,90,2) == XTC_OK); // Coincident notes remain individually editable.
uint64_t chord_id = piano->selected_note();
CHECK(piano->select_note(note_id)); pump();
auto *velocity = dynamic_cast<Fl_Value_Slider *>(find(piano->window(),"Note velocity"));
CHECK(velocity && velocity->active()); velocity->value(29); velocity->do_callback();
xtc_note_info note{}, chord{};
for (size_t i = 0; i < 2; ++i) { xtc_note_info n{}; xtc_project_get_note(sequence->project(),i,&n);
if (n.id == note_id) note = n;
if (n.id == chord_id) chord = n;
}
CHECK(note.velocity == 29 && note.start_tick == 0 && note.length_tick == 960 && note.pitch == 60 && note.channel == 2);
CHECK(chord.velocity == 90);
CHECK(piano->set_velocity(note_id,0) == XTC_ERR_INVALID_ARGUMENT);
CHECK(piano->set_velocity(note_id,128) == XTC_ERR_INVALID_ARGUMENT);
CHECK(piano->set_velocity(UINT64_MAX,40) == XTC_ERR_NOT_FOUND);
auto *lane = window.velocity_lane(); CHECK(lane);
gesture(lane,piano->note_x(0)-lane->x(),70,0,-30);
xtc_project_get_note(sequence->project(),0,&note); CHECK(note.id == note_id && note.velocity > 29);
xtc_project_get_note(sequence->project(),1,&chord); CHECK(chord.id == chord_id && chord.velocity == 90);
CHECK(window.undo(false) == XTC_OK);
xtc_project_get_note(sequence->project(),0,&note); CHECK(note.velocity == 29);
CHECK(window.undo(true) == XTC_OK);
CHECK(window.save_project("/tmp/xtc-grid-velocity.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-grid-velocity.xtc") == XTC_OK);
xtc_project_get_note(sequence->project(),0,&note); CHECK(note.velocity > 29 && note.pitch == 60 && note.channel == 2);
unlink("/tmp/xtc-grid-velocity.xtc"); window.do_callback();
}
{
char dir[]="/tmp/xtc-preferences-XXXXXX"; CHECK(mkdtemp(dir));
std::string path=std::string(dir)+"/.xtc.conf";
reset();
{
MainWindow window(false,path.c_str());window.show();
xtc_settings settings=window.settings();settings.note_velocity=73;settings.sequencer_snap=480;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(timeline(&window)->grid()==480);
int old_state=Fl::e_state;Fl::e_state=FL_SHIFT;
gesture(timeline(&window),228,12,96,0);Fl::e_state=old_state;
CHECK(window.settings().loop_start==960 && window.settings().loop_end==2880);
CHECK(window.set_loop(960,3840,true)==XTC_OK);
CHECK(window.settings().loop_enabled==1 && window.settings().loop_start==960);
CHECK(window.set_loop(4000,2000,true)==XTC_ERR_INVALID_ARGUMENT);
settings=window.settings();CHECK(window.apply_preferences(settings)==XTC_OK);
settings.note_velocity=128;CHECK(window.apply_preferences(settings)==XTC_ERR_INVALID_ARGUMENT);
PreferencesDialog dialog(window.settings(),path,[&](const xtc_settings &v){return window.apply_preferences(v);});
auto *input=dynamic_cast<Fl_Input *>(find(&dialog,"New note velocity"));CHECK(input);
if(input)input->value("81");
press(find(&dialog,"Apply"));CHECK(window.settings().note_velocity==81);
if(input)input->value("82");
press(find(&dialog,"Close"));CHECK(window.settings().note_velocity==81);
window.do_callback();
}
reset();
{
MainWindow window(false,path.c_str());CHECK(window.settings().note_velocity==81);
CHECK(window.settings().loop_start==960 && window.settings().loop_enabled==1);
xtc_settings paths=window.settings();
std::string binary=std::string(dir)+"/test.clap";FILE *f=fopen(binary.c_str(),"wb");CHECK(f);if(f)fclose(f);
std::snprintf(paths.clap_paths,sizeof(paths.clap_paths),"%s:%s",dir,dir);
paths.vst2_paths[0]=paths.vst3_paths[0]=paths.ladspa_paths[0]=0;
auto files=xtc_plugin_files(paths);CHECK(files.size()==1 && files[0]==binary);
unlink(binary.c_str());window.do_callback();
}
unlink(path.c_str());rmdir(dir);
}
{
reset();MainWindow window(false,"");window.show();auto *m=window.mixer();
CHECK(m->strip_count()==0 && m->strip(UINT64_MAX)==nullptr);
CHECK(window.create_track(true)==XTC_OK && window.create_track(true)==XTC_OK);
xtc_track_info a{},b{};xtc_project_get_track(m->project(),0,&a);xtc_project_get_track(m->project(),1,&b);
CHECK(m->strip_count()==2 && m->strip(a.id) && m->strip(b.id));
auto *first=m->strip(a.id);
auto *fader=dynamic_cast<Fl_Value_Slider *>(find(first,"dB"));CHECK(fader);
CHECK(fader && fader->type()==FL_VERT_SLIDER && fader->h()>fader->w());
if(fader){fader->value(-6);fader->do_callback();}
xtc_project_get_track(m->project(),0,&a);xtc_project_get_track(m->project(),1,&b);
CHECK(std::fabs(a.gain-std::pow(10.f,-6.f/20))<.0001f && b.gain==1);
CHECK(first==m->strip(a.id)); // Editing must not delete its callback's widget.
if(fader){gesture(fader,20,40,0,30);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain<.5f);CHECK(window.undo()==XTC_OK);}
if(fader){
reserve_result=XTC_ERR_OUT_OF_MEMORY;
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();CHECK(dialog && contains(dialog,"Mixer edit failed"));if(dialog)dialog->hide();});
fader->value(-12);fader->do_callback();reserve_result=XTC_OK;
xtc_project_get_track(m->project(),0,&a);CHECK(std::fabs(a.gain-std::pow(10.f,-6.f/20))<.0001f);
CHECK(std::fabs(fader->value()+6)<.001);
}
press(find(m->strip(b.id),b.name));CHECK(m->selected_track()==b.id);
CHECK(window.undo()==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain==1);
CHECK(window.undo(true)==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.gain<.51f);
Fl_Dial *pan=nullptr;for(int i=0;i<m->strip(b.id)->children();++i)if(auto *d=dynamic_cast<Fl_Dial *>(m->strip(b.id)->child(i)))pan=d;
CHECK(pan);if(pan){pan->value(-.75);pan->do_callback();}
xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==-.75f && b.gain==1);
auto *mute=dynamic_cast<Fl_Check_Button *>(find(m->strip(b.id),"Mute"));CHECK(mute);
if(mute){mute->value(1);mute->do_callback();}xtc_project_get_track(m->project(),1,&b);CHECK(b.mute && b.pan==-.75f);
if(pan){int old=Fl::e_clicks;Fl::e_clicks=1;gesture(pan,20,20,0,0);Fl::e_clicks=old;}
xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==0 && b.mute);
CHECK(m->add_bus()==XTC_OK);uint64_t bus=m->selected_track();CHECK(m->strip_count()==3 && contains(m->strip(bus),"Bus:"));
Fl_Choice *route=nullptr;for(int i=0;i<m->strip(a.id)->children();++i)if(auto *c=dynamic_cast<Fl_Choice *>(m->strip(a.id)->child(i)))route=c;
CHECK(route && route->size()==3); // Master, bus, terminator.
if(route){route->value(1);route->do_callback();}xtc_project_get_track(m->project(),0,&a);CHECK(a.bus_id==bus);
CHECK(window.undo()==XTC_OK);xtc_project_get_track(m->project(),0,&a);CHECK(a.bus_id==UINT64_MAX);
CHECK(m->add_bus()==XTC_OK);CHECK(m->strip_count()==4);
auto *routes=dynamic_cast<Fl_Choice *>(find(m,"Route"));CHECK(routes && routes->size()==4);
CHECK(window.undo()==XTC_OK);CHECK(m->strip_count()==3);
xtc_meter_snapshot snapshot{};snapshot.count=2;snapshot.tracks[0]={a.id,.5f,.25f};snapshot.tracks[1]={b.id,1.2f,.125f};
m->meters(snapshot);CHECK(contains(m->strip(a.id),"L -6.0") && contains(m->strip(b.id),"CLIP"));
m->select_track(a.id);CHECK(contains(m->strip(b.id),"CLIP"));m->meters(xtc_meter_snapshot{});CHECK(contains(m->strip(b.id),"CLIP"));
m->reset_meters();CHECK(!contains(m->strip(b.id),"CLIP"));
CHECK(window.save_project("/tmp/xtc-mixer-strips.xtc")==XTC_OK && window.open_project("/tmp/xtc-mixer-strips.xtc")==XTC_OK);
CHECK(m->strip_count()==3);xtc_project_get_track(m->project(),1,&b);CHECK(b.pan==0 && b.mute);
CHECK(m->select_track(b.id) && m->mix(.00001f,0,0,0)==XTC_OK);
mute=dynamic_cast<Fl_Check_Button *>(find(m->strip(b.id),"Mute"));CHECK(mute);
if(mute){mute->value(1);mute->do_callback();}xtc_project_get_track(m->project(),1,&b);CHECK(b.gain==.00001f && b.mute);
unlink("/tmp/xtc-mixer-strips.xtc");window.do_callback();
}
{
xtc_project *project=nullptr;CHECK(xtc_project_create(&project)==XTC_OK);
uint64_t first=UINT64_MAX,last=UINT64_MAX;
for(size_t i=0;i<XTC_PROJECT_MAX_TRACKS;++i){uint64_t id;char name[40];std::snprintf(name,sizeof(name),"Duplicate/name & _track");
CHECK(xtc_project_add_track(project,name,&id)==XTC_OK);if(i==0)first=id;last=id;}
{
Fl_Double_Window window(1100,600);window.begin();MixerWidget m(0,0,1032,522,project,nullptr,nullptr);window.end();window.show();
CHECK(m.strip_count()==XTC_PROJECT_MAX_TRACKS && m.select_track(last));
auto *track_choice=dynamic_cast<Fl_Choice *>(find(&m,"Track"));
CHECK(track_choice && track_choice->size()==static_cast<int>(XTC_PROJECT_MAX_TRACKS)+1);
CHECK(track_choice && track_choice->value()==static_cast<int>(XTC_PROJECT_MAX_TRACKS)-1);
CHECK(m.strip(last)->x()>=m.x() && m.strip(last)->x()+m.strip(last)->w()<m.x()+m.w()-144);
CHECK(xtc_project_move_track(project,last,0)==XTC_OK);m.set_project(project);
CHECK(m.selected_track()==last && m.strip_count()==XTC_PROJECT_MAX_TRACKS);
CHECK(xtc_project_remove_track(project,first)==XTC_OK);m.set_project(project);CHECK(!m.strip(first));
m.resize(0,0,972,442);m.select_track(last);auto *f=dynamic_cast<Fl_Value_Slider *>(find(m.strip(last),"dB"));CHECK(f && f->h()>40);
m.resize(0,0,1500,900);CHECK(f && f->h()>400);
window.hide();
}
xtc_project_destroy(project);
}
2026-10-28 17:16:01 +01:00
{
reset();MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK);
auto *seq=timeline(&window);auto clip=seq->selected_clip(),track=seq->selected_track();
CHECK(window.set_tempo(135500)==XTC_OK && xtc_project_tempo(seq->project())==135500);
CHECK(seq->selected_clip()==clip && seq->selected_track()==track);
CHECK(window.undo()==XTC_OK && xtc_project_tempo(seq->project())==120000);
CHECK(window.undo(true)==XTC_OK && xtc_project_tempo(seq->project())==135500);
const auto *before=seq->project();reserve_result=XTC_ERR_OUT_OF_MEMORY;
CHECK(window.set_tempo(100000)==XTC_ERR_OUT_OF_MEMORY && seq->project()==before);
reserve_result=XTC_OK;CHECK(window.set_tempo(0)==XTC_ERR_INVALID_ARGUMENT);
submit_result=XTC_ERR_CAPACITY;CHECK(window.set_tempo(100000)==XTC_ERR_CAPACITY && seq->project()==before);submit_result=XTC_OK;
CHECK(window.set_tempo(135500)==XTC_OK);
auto *bpm=dynamic_cast<Fl_Value_Input *>(find(&window,"BPM"));CHECK(bpm);
if(bpm){bpm->value(140);bpm->do_callback();CHECK(xtc_project_tempo(seq->project())==140000);
bpm->value(900);bpm->do_callback();CHECK(xtc_project_tempo(seq->project())==140000 && bpm->value()==140);}
auto *gain=dynamic_cast<Fl_Value_Input *>(find(&window,"Gain"));CHECK(gain);pump();
if(gain){gain->value(.6);gain->do_callback();}xtc_track_info info{};xtc_project_get_track(seq->project(),0,&info);CHECK(info.gain==.6f);
if(gain){Fl::focus(&gain->input);gain->input.value("0.123");pump();CHECK(std::strcmp(gain->input.value(),"0.123")==0);Fl::focus(nullptr);}
CHECK(window.undo()==XTC_OK);xtc_project_get_track(seq->project(),0,&info);CHECK(info.gain==1);
CHECK(seq->set_zoom(384)==XTC_OK);window.fit_song();CHECK(seq->zoom_level()<384);
CHECK(contains(&window,"1 tracks / 1 clips"));
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();auto *search=dynamic_cast<Fl_Input *>(find(dialog,"Search commands"));CHECK(search);
if(search){search->value("view/mixer");search->do_callback();}press(find(dialog,"Run command"));if(dialog->shown()){CHECK(false);dialog->hide();}});
window.show_commands();CHECK(window.mixer()->visible_r());
auto *bar=dynamic_cast<Fl_Menu_Bar *>(window.child(0));CHECK(bar);if(bar)bar->picked(bar->find_item("View/Sequencer"));
auto *follow=dynamic_cast<Fl_Check_Button *>(find(&window,"Follow playhead"));CHECK(follow);
if(follow){follow->value(1);follow->do_callback();CHECK(window.settings().follow_playhead==1);}
press(find(&window,"Play"));engine_status.sample_position=960000;pump();
auto *scroll=dynamic_cast<Fl_Scroll *>(seq->parent());CHECK(scroll && scroll->xposition()>0);
press(find(&window,"Stop"));window.fit_song();CHECK(scroll && scroll->xposition()==0);
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::first_window();auto *search=dynamic_cast<Fl_Input *>(find(dialog,"Search commands"));
CHECK(search);if(search){search->value("does not exist");search->do_callback();}press(find(dialog,"Run command"));CHECK(dialog->shown());press(find(dialog,"Cancel"));});
window.show_commands();
CHECK(window.create_track(true)==XTC_OK);uint64_t second=seq->selected_track();
window.mixer()->select_track(track);if(bar)bar->picked(bar->find_item("View/Mixer"));pump();
if(gain){gain->value(.4);gain->do_callback();}xtc_project_get_track(seq->project(),0,&info);CHECK(info.id==track && info.gain==.4f);
xtc_project_get_track(seq->project(),1,&info);CHECK(info.id==second && info.gain==1);
window.do_callback();
}
{
CommandPalette palette({"View/Mixer","File/Open"});
Fl::add_timeout(.05,[](void *context){auto *p=static_cast<CommandPalette *>(context);auto *search=dynamic_cast<Fl_Input *>(find(p,"Search commands"));CHECK(search);
if(search){search->value("FILE");search->do_callback();}int old=Fl::e_keysym;Fl::e_keysym=FL_Enter;CHECK(p->handle(FL_KEYDOWN)==1);Fl::e_keysym=old;},&palette);
CHECK(palette.run()=="File/Open");
}
{
CommandPalette palette({"View/Mixer"});
Fl::add_timeout(.05,[](void *context){int old=Fl::e_keysym;Fl::e_keysym=FL_Escape;static_cast<CommandPalette *>(context)->handle(FL_KEYDOWN);Fl::e_keysym=old;},&palette);
CHECK(palette.run().empty());
}
reset();
{
MainWindow window(false,""); window.show();
CHECK(window.create_track(true)==XTC_OK);
auto *sequence=timeline(&window); auto *mix=window.mixer();
auto *play=find(&window,"Play");
auto *bpm=dynamic_cast<Fl_Value_Input *>(find(&window,"BPM"));
auto *loop=dynamic_cast<Fl_Check_Button *>(find(&window,"Loop"));
auto *tabs=dynamic_cast<Fl_Tabs *>(sequence->parent()->parent()->parent());
CHECK(play && bpm && loop && tabs);
int original_y=tabs->y();
for (int pass=0;pass<3;++pass) {
window.detach_transport(true); window.detach_mixer(true); pump();
CHECK(window.transport_window()->shown() && window.mixer_window()->shown());
CHECK(play->window()==window.transport_window() && mix->window()==window.mixer_window());
CHECK(bpm->window()==window.transport_window() && loop->window()==window.transport_window());
CHECK(tabs->y()==original_y-90);
Fl::focus(mix); int old_key=Fl::e_keysym, old_state=Fl::e_state;
Fl::e_keysym=' '; Fl::e_state=0; unsigned int old_starts=starts;
CHECK(window.mixer_window()->handle(FL_SHORTCUT)==1 && starts==old_starts+1);
CHECK(window.transport_window()->handle(FL_SHORTCUT)==1);
Fl::e_keysym=old_key; Fl::e_state=old_state;
bpm->value(132); bpm->do_callback();
CHECK(xtc_project_tempo(sequence->project())==132000);
CHECK(window.set_loop(0,3840,true)==XTC_OK); pump(); CHECK(loop->value()==1);
CHECK(window.set_loop(0,3840,false)==XTC_OK); pump(); CHECK(loop->value()==0);
window.mixer_window()->resize(10,10,800,500); pump();
CHECK(mix->x()+mix->w()<=window.mixer_window()->w());
window.resize(window.x(),window.y(),pass==0 ? 1600 : 1220,pass==0 ? 1000 : 720);
window.transport_window()->do_callback(); window.mixer_window()->do_callback(); pump();
CHECK(!window.transport_window()->shown() && !window.mixer_window()->shown());
CHECK(play->window()==&window && mix->window()==&window);
CHECK(tabs->y()==original_y && mix->w()>0 && mix->h()>0);
}
window.detach_transport(true); window.detach_mixer(true);
close_result=XTC_ERR_IO; window.do_callback();
CHECK(window.shown() && window.transport_window()->shown() && window.mixer_window()->shown());
close_result=XTC_OK; window.do_callback();
CHECK(!window.shown() && !window.transport_window()->shown() && !window.mixer_window()->shown());
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;
}