#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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(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(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(group->child(i))) return s; if (auto *g = dynamic_cast(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(group->child(i))) return canvas; if (auto *nested = dynamic_cast(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_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(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(window.child(i))) menu = found; if (auto *found = dynamic_cast(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(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(graph_group->child(i))) for (int j = 0; j < scroll->children(); ++j) if (auto *found = dynamic_cast(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(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, ®ion) == 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, ®ion) == XTC_OK && region.start_tick == 3840 && region.length_tick == 2880 && region.offset_tick == 960); int width = sequence->w(); press(find(&window, "Zoom +")); CHECK(sequence->w() > width); press(find(&window, "Zoom -")); CHECK(sequence->w() == width); CHECK(window.save_project(filename.c_str()) == XTC_OK); const xtc_project *retained = sequence->project(); CHECK(window.open_project("/missing/xtc-project") == XTC_ERR_IO && sequence->project() == retained); 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, ®ion) == XTC_OK && region.start_tick == 3840 && region.offset_tick == 960); engine_status.sample_position = 48000; pump(); CHECK(sequence->position() == 1920); CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK); CHECK(sequence->position() == 1920); // Graph edits preserve the transport. engine_status.sample_position = 0; 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(audio_clip.start_tick*48/960), 50 + static_cast(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(), ¬e_count) == XTC_OK && note_count == 1); CHECK(xtc_project_get_note(sequence->project(), 0, &drawn) == XTC_OK && drawn.start_tick == 720 && drawn.length_tick == 1440 && drawn.pitch == 61); // 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(), ¬e_count) == XTC_OK && note_count == 2); CHECK(roll->delete_selected() == XTC_OK); CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 0); CHECK(window.undo() == XTC_OK); CHECK(xtc_project_note_count(sequence->project(), ¬e_count) == XTC_OK && note_count == 2); CHECK(window.undo(true) == XTC_OK); CHECK(xtc_project_note_count(sequence->project(), ¬e_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(), ¬e_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(), ¬e_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(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(); } 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(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); CHECK(selector->size() == 4); // Sine, current 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{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(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(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,¬e); 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,¬e); 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,¬e); CHECK(note.velocity > 29 && note.pitch == 60 && note.channel == 2); unlink("/tmp/xtc-grid-velocity.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; }