2026-10-26 23:40:28 +01:00
|
|
|
#include "../../src/gui/main_window.hpp"
|
2026-10-26 23:55:35 +01:00
|
|
|
#include "../../src/gui/graph_widget.hpp"
|
2026-10-27 00:10:45 +01:00
|
|
|
#include "../../src/gui/sequencer_widget.hpp"
|
2026-10-27 00:53:41 +01:00
|
|
|
#include "../../src/gui/piano_roll.hpp"
|
2026-10-26 23:40:28 +01:00
|
|
|
#include <FL/Fl.H>
|
|
|
|
|
#include <FL/Fl_Group.H>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
2026-10-27 00:31:03 +01:00
|
|
|
#include <unistd.h>
|
2026-10-26 23:40:28 +01:00
|
|
|
|
|
|
|
|
namespace {
|
2026-10-27 03:25:04 +01:00
|
|
|
Fl_Widget *find(Fl_Group &group, const char *label, bool exact = false)
|
2026-10-26 23:40:28 +01:00
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group.children(); ++i) {
|
|
|
|
|
Fl_Widget *child = group.child(i);
|
2026-10-27 03:25:04 +01:00
|
|
|
if (child->label() != nullptr && (exact ? std::strcmp(child->label(), label) == 0 : std::strstr(child->label(), label) != nullptr)) return child;
|
|
|
|
|
if (auto *nested = dynamic_cast<Fl_Group *>(child)) if (auto *match = find(*nested, label, exact)) return match;
|
2026-10-26 23:40:28 +01:00
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
GraphWidget *canvas_in(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 = canvas_in(*nested)) return canvas;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-10-27 00:10:45 +01:00
|
|
|
SequencerWidget *timeline_in(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_in(*g)) return s;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
2026-10-26 23:40:28 +01:00
|
|
|
void pump()
|
|
|
|
|
{
|
|
|
|
|
const auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);
|
|
|
|
|
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr,"FAIL %s:%d: %s\n",__FILE__,__LINE__,#c); return 1; } } while (0)
|
|
|
|
|
int main(int argc, char **argv)
|
|
|
|
|
{
|
|
|
|
|
// Real JACK backend and real FLTK event loop. No routes, no playback data.
|
|
|
|
|
for (int iteration = 0; iteration < 3; ++iteration) {
|
2026-10-27 00:10:45 +01:00
|
|
|
MainWindow window(false);
|
2026-10-26 23:40:28 +01:00
|
|
|
window.show(argc, argv);
|
|
|
|
|
CHECK(find(window, "ready / stopped") != nullptr);
|
|
|
|
|
auto *play = find(window, "Play");
|
|
|
|
|
auto *stop = find(window, "Stop");
|
|
|
|
|
auto *reconnect = find(window, "Reconnect");
|
|
|
|
|
CHECK(play != nullptr && stop != nullptr && reconnect != nullptr);
|
2026-10-26 23:55:35 +01:00
|
|
|
auto *canvas = canvas_in(window);
|
|
|
|
|
CHECK(canvas != nullptr);
|
2026-10-27 00:10:45 +01:00
|
|
|
auto *timeline = timeline_in(window); CHECK(timeline != nullptr);
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
|
|
|
|
CHECK(canvas->add_node(XTC_NODE_OUTPUT) == XTC_OK);
|
|
|
|
|
CHECK(canvas->connect_ports({0U, 1U, 1U, 0U}) == XTC_OK);
|
|
|
|
|
CHECK(canvas->connect_ports({1U, 1U, 2U, 0U}) == XTC_OK);
|
2026-10-26 23:40:28 +01:00
|
|
|
for (int restart = 0; restart < 3; ++restart) {
|
2026-10-27 00:10:45 +01:00
|
|
|
uint64_t previous = timeline->position();
|
2026-10-26 23:40:28 +01:00
|
|
|
CHECK(play->active());
|
|
|
|
|
play->do_callback();
|
|
|
|
|
pump();
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(find(window, "running — modular graph") != nullptr);
|
2026-10-27 00:10:45 +01:00
|
|
|
CHECK(timeline->position() > previous);
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
|
|
|
|
const xtc_graph *unchanged = canvas->graph();
|
|
|
|
|
CHECK(canvas->connect_ports({1U, 1U, 1U, 0U}) == XTC_ERR_GRAPH_CYCLE);
|
|
|
|
|
CHECK(canvas->graph() == unchanged);
|
2026-10-27 16:00:29 +01:00
|
|
|
// Replace a live wire by a complete unity-gain chain at one publication.
|
|
|
|
|
CHECK(canvas->select_connection(1,2));
|
|
|
|
|
CHECK(canvas->insert_gain() == XTC_OK);
|
|
|
|
|
size_t inserted = canvas->selected_node();
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
|
|
|
|
CHECK(find(window, "running — modular graph") != nullptr);
|
|
|
|
|
xtc_routing_info route{};
|
|
|
|
|
CHECK(canvas->routing(inserted, route) && route.inputs == 1 && route.outputs == 1 && route.reaches_output);
|
|
|
|
|
CHECK(canvas->connect_ports({inserted,1,1,0}) == XTC_ERR_GRAPH_CYCLE);
|
|
|
|
|
CHECK(canvas->select_connection(1,inserted) && canvas->delete_selected() == XTC_OK); pump();
|
|
|
|
|
CHECK(canvas->select_connection(inserted,2) && canvas->delete_selected() == XTC_OK); pump();
|
|
|
|
|
CHECK(canvas->connect_ports({1,1,2,0}) == XTC_OK); pump();
|
|
|
|
|
CHECK(canvas->select_node(inserted) && canvas->delete_selected() == XTC_OK);
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(canvas->add_node(XTC_NODE_METER) == XTC_OK);
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
|
|
|
|
CHECK(canvas->delete_selected() == XTC_OK);
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
2026-10-26 23:40:28 +01:00
|
|
|
auto *status = find(window, "JACK |");
|
|
|
|
|
CHECK(status != nullptr);
|
|
|
|
|
unsigned int rate = 0U, frames = 0U, blocks = 0U;
|
2026-10-27 01:06:57 +01:00
|
|
|
CHECK(std::sscanf(status->label(), "JACK | %u Hz | %u frames | stereo | %u blocks", &rate, &frames, &blocks) == 3);
|
2026-10-26 23:40:28 +01:00
|
|
|
CHECK(rate > 0U && frames > 0U && blocks > 0U);
|
2026-10-27 03:37:19 +01:00
|
|
|
stop->do_callback(); pump();
|
2026-10-26 23:40:28 +01:00
|
|
|
CHECK(find(window, "ready / stopped") != nullptr);
|
2026-10-27 00:10:45 +01:00
|
|
|
uint64_t paused = timeline->position(); pump();
|
|
|
|
|
CHECK(timeline->position() == paused);
|
2026-10-26 23:40:28 +01:00
|
|
|
}
|
|
|
|
|
reconnect->do_callback();
|
|
|
|
|
CHECK(find(window, "ready / stopped") != nullptr);
|
|
|
|
|
play->do_callback();
|
|
|
|
|
pump();
|
2026-10-26 23:55:35 +01:00
|
|
|
CHECK(find(window, "running — modular graph") != nullptr);
|
2026-10-27 03:37:19 +01:00
|
|
|
stop->do_callback(); pump();
|
2026-10-27 00:31:03 +01:00
|
|
|
char directory[] = "/tmp/xtc-live-record-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
|
|
|
|
|
std::string take = std::string(directory) + "/take.wav";
|
|
|
|
|
std::string song = std::string(directory) + "/song.xtc";
|
|
|
|
|
CHECK(window.record_audio(take.c_str()) == XTC_OK);
|
|
|
|
|
pump();
|
|
|
|
|
CHECK(find(window, "recording JACK input") != nullptr);
|
|
|
|
|
stop->do_callback();
|
|
|
|
|
size_t tracks, clips;
|
|
|
|
|
CHECK(xtc_project_get_counts(timeline->project(), &tracks, &clips) == XTC_OK && clips == 1);
|
|
|
|
|
CHECK(window.save_project(song.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(window.open_project(song.c_str()) == XTC_OK);
|
|
|
|
|
play->do_callback(); pump();
|
|
|
|
|
CHECK(find(window, "running — modular graph") != nullptr);
|
2026-10-27 03:25:04 +01:00
|
|
|
auto *add_midi = find(window, "Add MIDI clip", true); CHECK(add_midi != nullptr);
|
2026-10-27 00:53:41 +01:00
|
|
|
add_midi->do_callback(); CHECK(window.open_piano_roll() == XTC_OK);
|
|
|
|
|
CHECK(window.piano_roll()->add_note(0, 960, 69) == XTC_OK);
|
|
|
|
|
CHECK(window.save_project(song.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(window.open_project(song.c_str()) == XTC_OK);
|
|
|
|
|
size_t notes;
|
|
|
|
|
CHECK(xtc_project_note_count(timeline->project(), ¬es) == XTC_OK && notes == 1);
|
|
|
|
|
play->do_callback(); pump();
|
|
|
|
|
CHECK(find(window, "Graph: active in engine") != nullptr);
|
2026-10-27 00:31:03 +01:00
|
|
|
CHECK(unlink(take.c_str()) == 0 && unlink(song.c_str()) == 0 && rmdir(directory) == 0);
|
2026-10-26 23:40:28 +01:00
|
|
|
window.do_callback(); // Close with an active client; synchronizes callbacks.
|
|
|
|
|
CHECK(!window.shown());
|
|
|
|
|
pump(); // No timer may access the closed client.
|
|
|
|
|
}
|
2026-10-26 23:55:35 +01:00
|
|
|
std::puts("Live GUI/JACK graph edits/revision acknowledgements/start/stop/reconnect/active-close tests passed.");
|
2026-10-26 23:40:28 +01:00
|
|
|
return 0;
|
|
|
|
|
}
|