2026-10-27 03:25:04 +01:00
|
|
|
/* Full GUI workflow against a running JACK-compatible server. All edits use
|
|
|
|
|
* widget commands/gestures; public C render APIs compare saved musical output. */
|
|
|
|
|
#include "../../src/gui/main_window.hpp"
|
|
|
|
|
#include "../../src/gui/sequencer_widget.hpp"
|
|
|
|
|
#include "../../src/gui/piano_roll.hpp"
|
|
|
|
|
#include "../../src/gui/mixer_widget.hpp"
|
|
|
|
|
#include <FL/Fl.H>
|
|
|
|
|
#include <FL/Fl_Group.H>
|
|
|
|
|
#include <FL/Fl_Tabs.H>
|
|
|
|
|
#include <FL/Fl_Progress.H>
|
|
|
|
|
#include <chrono>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <string>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
#define CHECK(c) do { if (!(c)) { std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #c); std::exit(1); } } while (0)
|
|
|
|
|
|
|
|
|
|
template<class T> static T *widget(Fl_Group &group)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group.children(); ++i) {
|
|
|
|
|
if (auto *match = dynamic_cast<T *>(group.child(i))) return match;
|
|
|
|
|
if (auto *nested = dynamic_cast<Fl_Group *>(group.child(i)))
|
|
|
|
|
if (auto *match = widget<T>(*nested)) return match;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
static Fl_Widget *button(Fl_Group &group, const std::string &label)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < group.children(); ++i) {
|
|
|
|
|
auto *child = group.child(i);
|
2026-10-27 16:15:34 +01:00
|
|
|
if (child->label() && (label == child->label() || (label.compare(0, 7, "Master ") == 0 && std::string(child->label()).compare(0, label.size(), label) == 0))) return child;
|
2026-10-27 03:25:04 +01:00
|
|
|
if (auto *nested = dynamic_cast<Fl_Group *>(child))
|
|
|
|
|
if (auto *match = button(*nested, label)) return match;
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
static void press(MainWindow &window, const char *label)
|
|
|
|
|
{
|
|
|
|
|
auto *control = button(window, label);
|
|
|
|
|
CHECK(control && control->active());
|
|
|
|
|
control->do_callback();
|
|
|
|
|
}
|
|
|
|
|
static void pump()
|
|
|
|
|
{
|
|
|
|
|
auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(400);
|
|
|
|
|
while (std::chrono::steady_clock::now() < end) Fl::wait(0.02);
|
|
|
|
|
}
|
|
|
|
|
static void gesture(SequencerWidget &timeline, int x, int y, int delta)
|
|
|
|
|
{
|
|
|
|
|
int saved_x = Fl::e_x, saved_y = Fl::e_y, saved_key = Fl::e_keysym;
|
|
|
|
|
Fl::e_x = timeline.x() + x; Fl::e_y = timeline.y() + y;
|
|
|
|
|
Fl::e_keysym = FL_Button + FL_LEFT_MOUSE;
|
|
|
|
|
CHECK(timeline.handle(FL_PUSH) == 1);
|
|
|
|
|
Fl::e_x += delta;
|
|
|
|
|
(void)timeline.handle(FL_DRAG);
|
|
|
|
|
(void)timeline.handle(FL_RELEASE);
|
|
|
|
|
Fl::e_x = saved_x; Fl::e_y = saved_y; Fl::e_keysym = saved_key;
|
|
|
|
|
}
|
|
|
|
|
static std::vector<float> render(const xtc_project *project)
|
|
|
|
|
{
|
|
|
|
|
xtc_plan *graph = nullptr;
|
|
|
|
|
xtc_audio_plan *audio = nullptr;
|
|
|
|
|
xtc_midi_plan *midi = nullptr;
|
|
|
|
|
CHECK(xtc_graph_compile(xtc_project_graph(project), 48000, 256, &graph) == XTC_OK);
|
|
|
|
|
CHECK(xtc_audio_plan_create(project, 48000, &audio) == XTC_OK);
|
|
|
|
|
CHECK(xtc_midi_plan_create(project, 48000, &midi) == XTC_OK);
|
2026-10-27 03:37:19 +01:00
|
|
|
CHECK(xtc_audio_plan_set_fade_frames(audio, 240) == XTC_OK);
|
|
|
|
|
CHECK(xtc_midi_plan_set_fade_frames(midi, 240) == XTC_OK);
|
2026-10-27 03:25:04 +01:00
|
|
|
std::vector<float> result;
|
|
|
|
|
for (uint64_t pos = 0; pos < 96000; pos += 256) {
|
|
|
|
|
float al[256], ar[256], ml[256], mr[256], left[256], right[256];
|
|
|
|
|
CHECK(xtc_audio_plan_render_stereo(audio, pos, al, ar, 256) == XTC_OK);
|
|
|
|
|
CHECK(xtc_midi_plan_render_stereo(midi, pos, nullptr, 0, ml, mr, 256) == XTC_OK);
|
|
|
|
|
CHECK(xtc_plan_process_stereo(graph, nullptr, al, ar, ml, mr, left, right, 256) == XTC_OK);
|
|
|
|
|
for (size_t i = 0; i < 256; ++i) {
|
|
|
|
|
CHECK(std::isfinite(left[i]) && std::isfinite(right[i]));
|
|
|
|
|
result.push_back(left[i]); result.push_back(right[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
xtc_plan_destroy(graph); xtc_audio_plan_destroy(audio); xtc_midi_plan_destroy(midi);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
int main(int argc, char **argv)
|
|
|
|
|
{
|
|
|
|
|
char directory[] = "/tmp/xtc-workflow-XXXXXX";
|
|
|
|
|
CHECK(mkdtemp(directory));
|
|
|
|
|
std::string path = std::string(directory) + "/song.xtc";
|
|
|
|
|
std::string wave = std::string(directory) + "/source.wav";
|
|
|
|
|
xtc_recorder *recorder = nullptr;
|
|
|
|
|
CHECK(xtc_recorder_create(wave.c_str(), 48000, 256, &recorder) == XTC_OK);
|
|
|
|
|
float source[256]; for (float &sample : source) sample = 0.125f;
|
|
|
|
|
xtc_recorder_push(recorder, source, 256);
|
|
|
|
|
CHECK(xtc_recorder_finish(recorder) == XTC_OK);
|
|
|
|
|
xtc_recorder_destroy(recorder);
|
|
|
|
|
std::vector<float> expected;
|
|
|
|
|
xtc_clip_info arranged{};
|
|
|
|
|
size_t nodes, edges;
|
|
|
|
|
{
|
|
|
|
|
MainWindow window(false);
|
|
|
|
|
window.show(argc, argv);
|
|
|
|
|
auto *timeline = widget<SequencerWidget>(window);
|
|
|
|
|
auto *tabs = widget<Fl_Tabs>(window);
|
|
|
|
|
CHECK(timeline && tabs && std::string(tabs->value()->label()) == "Sequencer");
|
|
|
|
|
CHECK(window.new_project() == XTC_OK);
|
|
|
|
|
press(window, "Add track");
|
|
|
|
|
press(window, "Add MIDI clip");
|
|
|
|
|
press(window, "Piano roll");
|
|
|
|
|
CHECK(window.piano_roll());
|
|
|
|
|
CHECK(window.piano_roll()->add_note(0, 960, 60) == XTC_OK);
|
|
|
|
|
CHECK(window.piano_roll()->add_note(960, 960, 64) == XTC_OK);
|
2026-10-27 18:04:39 +01:00
|
|
|
gesture(*timeline, 210, 50, 96); // Move the clip two beats using its body.
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(xtc_project_get_clip(timeline->project(), 0, &arranged) == XTC_OK);
|
|
|
|
|
CHECK(arranged.start_tick == 1920 && arranged.length_tick == 3840);
|
|
|
|
|
CHECK(window.import_audio(wave.c_str()) == XTC_OK);
|
|
|
|
|
expected = render(timeline->project());
|
2026-10-27 03:37:19 +01:00
|
|
|
CHECK(expected[0] == 0 && expected[2] > 0); // Imported audio is routed without graph-file edits.
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(expected[2 * 48001] != 0); // Arranged notes also reach the output.
|
|
|
|
|
CHECK(xtc_graph_get_counts(xtc_project_graph(timeline->project()), &nodes, &edges) == XTC_OK);
|
|
|
|
|
CHECK(nodes == 3 && edges == 2); // Audio and instrument -> output.
|
|
|
|
|
CHECK(window.save_project(path.c_str()) == XTC_OK);
|
|
|
|
|
window.do_callback(); CHECK(!window.shown());
|
|
|
|
|
} // Destroy all widgets/engine state before opening a fresh window.
|
|
|
|
|
{
|
|
|
|
|
MainWindow window(false);
|
|
|
|
|
window.show(argc, argv);
|
|
|
|
|
auto *timeline = widget<SequencerWidget>(window);
|
|
|
|
|
widget<Fl_Tabs>(window)->value(widget<Fl_Tabs>(window)->child(2));
|
|
|
|
|
CHECK(window.open_project(path.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(widget<Fl_Tabs>(window)->value() == widget<Fl_Tabs>(window)->child(0));
|
|
|
|
|
CHECK(render(timeline->project()) == expected);
|
|
|
|
|
xtc_clip_info reopened{};
|
|
|
|
|
CHECK(xtc_project_get_clip(timeline->project(), 0, &reopened) == XTC_OK);
|
|
|
|
|
CHECK(reopened.id == arranged.id && reopened.track_id == arranged.track_id &&
|
|
|
|
|
reopened.start_tick == arranged.start_tick && reopened.length_tick == arranged.length_tick);
|
|
|
|
|
size_t actual_nodes, actual_edges;
|
|
|
|
|
CHECK(xtc_graph_get_counts(xtc_project_graph(timeline->project()), &actual_nodes, &actual_edges) == XTC_OK);
|
|
|
|
|
CHECK(actual_nodes == nodes && actual_edges == edges);
|
2026-10-27 18:04:39 +01:00
|
|
|
gesture(*timeline, 276, 10, 0); // Ruler seeks two beats while stopped.
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(timeline->position() == 1920);
|
|
|
|
|
press(window, "Play"); pump();
|
|
|
|
|
CHECK(!button(window, "Play")->active() && button(window, "Pause")->active());
|
|
|
|
|
CHECK(timeline->position() > 1920);
|
2026-10-27 16:15:34 +01:00
|
|
|
auto *left_meter = dynamic_cast<Fl_Progress *>(button(window, "Master L"));
|
|
|
|
|
auto *right_meter = dynamic_cast<Fl_Progress *>(button(window, "Master R"));
|
|
|
|
|
CHECK(left_meter && right_meter && left_meter->value() > -90 && right_meter->value() > -90);
|
|
|
|
|
const double playing_left = left_meter->value(), playing_right = right_meter->value();
|
2026-10-27 03:25:04 +01:00
|
|
|
CHECK(window.pause_playback() == XTC_OK);
|
2026-10-27 03:37:19 +01:00
|
|
|
pump(); // Fade completion is acknowledged by the GUI timer.
|
2026-10-27 16:15:34 +01:00
|
|
|
CHECK(left_meter->value() < playing_left && right_meter->value() < playing_right);
|
2026-10-27 03:25:04 +01:00
|
|
|
uint64_t paused = timeline->position(); pump();
|
|
|
|
|
CHECK(timeline->position() == paused);
|
|
|
|
|
press(window, "Play"); pump();
|
|
|
|
|
CHECK(timeline->position() > paused);
|
2026-10-27 03:37:19 +01:00
|
|
|
press(window, "Stop"); pump();
|
2026-10-27 03:25:04 +01:00
|
|
|
paused = timeline->position(); pump(); CHECK(timeline->position() == paused);
|
|
|
|
|
press(window, "To start"); CHECK(timeline->position() == 0);
|
|
|
|
|
press(window, "Add MIDI clip");
|
|
|
|
|
press(window, "Piano roll");
|
|
|
|
|
CHECK(window.piano_roll()->add_note(0, 480, 67) == XTC_OK);
|
|
|
|
|
CHECK(window.undo() == XTC_OK && window.undo(true) == XTC_OK);
|
|
|
|
|
CHECK(window.save_project(path.c_str()) == XTC_OK);
|
|
|
|
|
CHECK(window.new_project() == XTC_OK);
|
|
|
|
|
CHECK(window.open_project(path.c_str()) == XTC_OK);
|
|
|
|
|
size_t notes;
|
|
|
|
|
CHECK(xtc_project_note_count(timeline->project(), ¬es) == XTC_OK && notes == 3);
|
|
|
|
|
CHECK(widget<Fl_Tabs>(window)->value() == widget<Fl_Tabs>(window)->child(0));
|
|
|
|
|
window.do_callback(); CHECK(!window.shown());
|
|
|
|
|
}
|
|
|
|
|
CHECK(unlink(path.c_str()) == 0 && unlink(wave.c_str()) == 0 && rmdir(directory) == 0);
|
|
|
|
|
std::puts("GUI create/arrange/route/save/close/reopen/render/play/pause/seek/edit workflow passed.");
|
|
|
|
|
}
|