xtc/tests/integration/test_gui.cpp

1610 lines
110 KiB
C++

#include "../../src/gui/main_window.hpp"
#include "../../src/gui/sample_editor.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"
#include "xtc/xtc_midi_file.h"
#include "../../src/gui/command_palette.hpp"
#include <FL/Fl_Value_Input.H>
#include "../../src/gui/plugin_library.hpp"
#include "../../src/gui/track_inserts.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_Menu_Button.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_set_midi_options(xtc_backend *,int input,int output,int thru)
{CHECK(input>=-1 && input<16 && output>=-1 && output<16 && thru>=0 && thru<2);return handle.running ? XTC_ERR_INVALID_STATE : XTC_OK;}
xtc_result xtc_backend_visit_midi_ports(xtc_backend *,xtc_port_direction d,xtc_backend_port_visitor visit,void *c)
{visit(d==XTC_PORT_OUTPUT ? "Keyboard:out" : "Synth:in",c);return XTC_OK;}
xtc_result xtc_backend_route_midi(xtc_backend *,xtc_port_direction,const char *name,int)
{return !strcmp(name,"missing:port") ? XTC_ERR_NOT_FOUND : 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; }
}
static int sample_editor_tests()
{
reset();
MainWindow window(false,"");window.show();auto *s=timeline(&window);char path[4096];CHECK(realpath("examples/projects/original-pluck.wav",path));
CHECK(window.import_audio(path)==XTC_OK);
Fl::add_timeout(.1,[](void *){auto *e=dynamic_cast<SampleEditor *>(Fl::modal());CHECK(e!=nullptr);if(e){CHECK(e->select(10,100)==XTC_OK);float before=0,after=0;CHECK(xtc_sample_read(e->sample(),10,&before)==XTC_OK);
CHECK(e->process(XTC_SAMPLE_NORMALIZE,.8)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
CHECK(e->process(XTC_SAMPLE_FADE_IN_POWER)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
CHECK(e->process(XTC_SAMPLE_REMOVE_DC)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
CHECK(e->process(XTC_SAMPLE_INVERT)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);
CHECK(e->process(XTC_SAMPLE_REVERSE)==XTC_OK);CHECK(e->undo_sample()==XTC_OK);CHECK(xtc_sample_read(e->sample(),10,&after)==XTC_OK && before==after);CHECK(e->undo_sample()==XTC_OK);CHECK(e->select(100,10)==XTC_ERR_INVALID_ARGUMENT);CHECK(e->view(10,8)==XTC_OK);size_t b=e->begin(),z=e->end();CHECK(e->select_audible(1,0)==XTC_ERR_NOT_FOUND && e->begin()==b && e->end()==z);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->select_audible(.01,2)==XTC_OK);CHECK(e->select(0,e->frames())==XTC_OK);CHECK(e->snap_zero_crossings(65536)==XTC_OK);CHECK(e->detect_attacks(.1,2400)==XTC_OK && !e->attacks().empty());CHECK(e->view(e->frames(),1)==XTC_ERR_INVALID_ARGUMENT);unsigned int rate=0;size_t frames=0;CHECK(xtc_sample_info(e->sample(),&rate,&frames)==XTC_OK);CHECK(e->select(rate/10,rate*4/10)==XTC_OK);CHECK(e->trim_clip()==XTC_OK);e->hide();}},nullptr);
CHECK(window.open_sample_editor()==XTC_OK);CHECK(s->selected_clip()!=UINT64_MAX);
uint64_t id=s->selected_clip();
xtc_clip_info clip{};CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==192 && clip.length_tick==576);
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && clip.offset_tick==0);
{xtc_sample *data=nullptr;CHECK(xtc_sample_load(path,&data)==XTC_OK);std::string copy="/tmp/xtc-sample-gui-"+std::to_string(getpid())+".wav";
submit_result=XTC_ERR_OUT_OF_MEMORY;CHECK(window.adopt_sample_copy(id,data,copy.c_str())==XTC_ERR_OUT_OF_MEMORY && access(copy.c_str(),F_OK)!=0);submit_result=XTC_OK;
CHECK(window.adopt_sample_copy(id,data,copy.c_str())==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && !strcmp(clip.asset_path,copy.c_str()));
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_find_clip(s->project(),id,&clip)==XTC_OK && !strcmp(clip.asset_path,path));
xtc_sample_destroy(data);CHECK(unlink(copy.c_str())==0);}
CHECK(window.slice_sample_clip(id,22050,{2400,4800,7200})==XTC_OK);
size_t tracks=0,clips=0;CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==4);
CHECK(window.undo()==XTC_OK);CHECK(xtc_project_get_counts(s->project(),&tracks,&clips)==XTC_OK && clips==1);
window.do_callback();std::puts("Sample editor GUI selection/processing/adoption/slicing/undo passed.");return failures ? 1 : 0;
}
int creative_workflow_tests()
{
reset();MainWindow window(false,"");window.show();pump();auto *s=timeline(&window);auto *panel=window.track_inserts();
CHECK(window.open_track_inserts()==XTC_ERR_NOT_FOUND);
CHECK(window.create_track(false)==XTC_OK);auto first_track=s->selected_track();
CHECK(window.create_track(false)==XTC_OK);auto second_track=s->selected_track();
CHECK(window.open_track_inserts()==XTC_OK && panel->selected_track()==second_track);
auto *bar=dynamic_cast<Fl_Menu_Bar *>(window.child(0));CHECK(bar);
CHECK(bar->find_item("Track/Show selected track inserts")->shortcut()==FL_CTRL+'5');
CHECK(bar->find_item("View/Piano roll")->shortcut()==FL_CTRL+'4');
window.mixer()->select_track(first_track);bar->picked(bar->find_item("View/Mixer"));
CHECK(window.open_track_inserts()==XTC_OK && panel->selected_track()==first_track);
CHECK(window.add_track_effect(first_track,XTC_BUILTIN_PATH,2)==XTC_OK);
CHECK(panel->duplicate_slot()==XTC_OK && panel->selected_slot()==1);
xtc_track_chain chain{};CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==2);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==1);
CHECK(!panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
CHECK(panel->copy_chain()==XTC_OK && panel->clipboard_ready());
CHECK(panel->set_mix(.2,.6)==XTC_OK);
panel->select_track(second_track);CHECK(panel->paste_chain()==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==1 && chain.output_gain==1 && chain.slots[0].wet==1);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0);
submit_result=XTC_ERR_INVALID_STATE;auto *unchanged=s->project();CHECK(panel->paste_chain()==XTC_ERR_INVALID_STATE && s->project()==unchanged);submit_result=XTC_OK;
panel->select_track(first_track);CHECK(panel->comparison(0,false)==XTC_ERR_NOT_FOUND);
CHECK(panel->comparison(0,true)==XTC_OK && panel->comparison_ready(0));
CHECK(panel->set_mix(.7,.9)==XTC_OK && panel->comparison(1,true)==XTC_OK);
CHECK(panel->comparison(0,false)==XTC_OK && xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.output_gain==.6f);
CHECK(panel->comparison(1,false)==XTC_OK && xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.output_gain==.9f);
CHECK(window.undo()==XTC_OK);panel->refresh(true);
CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.output_gain==.6f);
panel->select_track(second_track);CHECK(!panel->comparison_ready(0) && panel->comparison(0,false)==XTC_ERR_NOT_FOUND);
CHECK(panel->apply_template(1)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==2 && chain.slots[0].plugin.values[0]==375);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0);
CHECK(panel->apply_template(1)==XTC_OK && panel->bulk(XTC_CHAIN_BYPASS_ALL)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && !chain.slots[0].enabled && !chain.slots[1].enabled);
submit_result=XTC_ERR_INVALID_STATE;unchanged=s->project();CHECK(panel->bulk(XTC_CHAIN_ENABLE_ALL)==XTC_ERR_INVALID_STATE && s->project()==unchanged);submit_result=XTC_OK;
CHECK(panel->bulk(XTC_CHAIN_ENABLE_ALL)==XTC_OK && panel->bulk(XTC_CHAIN_CLEAR)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0 && chain.output_gain==.8f);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==2);
xtc_meter_snapshot peaks{};peaks.count=1;peaks.tracks[0]={second_track,.5f,.25f};panel->meters(peaks,true);
CHECK(std::fabs(panel->meter()->left_db()+6.0206f)<.001f && std::fabs(panel->meter()->right_db()+12.0412f)<.001f);
peaks.tracks[0].left=1.2f;panel->meters(peaks,true);CHECK(panel->meter()->clipped());
panel->meters(peaks,false);CHECK(panel->meter()->left_db()==-90 && panel->meter()->clipped());
panel->meter()->do_callback();CHECK(!panel->meter()->clipped());
panel->select_track(first_track);CHECK(panel->meter()->left_db()==-90 && !panel->meter()->clipped());
CHECK(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
CHECK(find(&window,"Tap"));CHECK(window.tap_tempo(0)==XTC_ERR_NOT_FOUND && window.tap_tempo(.4)==XTC_OK);
CHECK(xtc_project_tempo(s->project())==150000 && window.undo()==XTC_OK && xtc_project_tempo(s->project())==120000);
CHECK(window.new_project()==XTC_OK && window.tap_tempo(.9)==XTC_ERR_NOT_FOUND);
CHECK(window.create_track(false)==XTC_OK);auto audio_track=s->selected_track();s->position(0);CHECK(s->add_region()==XTC_OK);auto left_clip=s->selected_clip();
engine_status.sample_position=48000;s->position(1920);CHECK(s->add_region()==XTC_OK);auto right_clip=s->selected_clip();CHECK(s->select(audio_track,left_clip) && s->toggle_clip(right_clip));
CHECK(s->crossfade_selection()==XTC_OK);xtc_clip_info audio{};CHECK(xtc_project_find_clip(s->project(),left_clip,&audio)==XTC_OK && audio.fade_out_tick==1920);
CHECK(window.undo()==XTC_OK && xtc_project_find_clip(s->project(),left_clip,&audio)==XTC_OK && !audio.fade_out_tick);
engine_status.sample_position=0;
CHECK(window.create_track(true)==XTC_OK);auto midi_track=s->selected_track();
CHECK(s->add_midi_clip()==XTC_OK);auto chord_clip=s->selected_clip();
CHECK(window.open_piano_roll()==XTC_OK);
CHECK(window.piano_roll()->add_note(0,200,60,100,0)==XTC_OK);
CHECK(window.piano_roll()->add_note(0,200,67,100,0)==XTC_OK);
CHECK(s->select(midi_track,chord_clip) && s->strum_chords(30,false)==XTC_OK);
xtc_note_info strummed{};size_t note_count=0;CHECK(xtc_project_note_count(s->project(),&note_count)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),note_count-1,&strummed)==XTC_OK && strummed.start_tick==30 && strummed.length_tick==170);
CHECK(window.undo()==XTC_OK && xtc_project_get_note(s->project(),note_count-1,&strummed)==XTC_OK && strummed.start_tick==0 && strummed.length_tick==200);
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
}
int track_insert_tests()
{
CHECK(xtc_plugin_file_matches("/usr/lib/vst3/ZamEQ2.vst3/Contents/x86_64-linux/ZamEQ2.so","zameq",3));
CHECK(!xtc_plugin_file_matches("/usr/lib/vst3/ZamEQ2.vst3/Contents/x86_64-linux/ZamEQ2.so","zameq",4));
CHECK(xtc_plugin_file_matches("/usr/lib/clap/room.clap","ROOM",4));
CHECK(xtc_plugin_file_matches("/usr/lib/ladspa/gain.so","gain",1) && xtc_plugin_file_matches("/usr/lib/vst/gain.so","gain",2));
CHECK(!xtc_plugin_file_matches("/usr/lib/vst/gain.so","absent",0));
reset();MainWindow window(false,"");window.show();pump();auto *s=timeline(&window);
CHECK(window.create_track(false)==XTC_OK);auto track=s->selected_track();auto *panel=window.track_inserts();CHECK(panel);
panel->select_track(track);CHECK(panel->selected_track()==track && panel->graph());
CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,2)==XTC_OK);
xtc_track_chain chain{};CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1 && chain.slots[0].enabled);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==0);
CHECK(window.undo(true)==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1);
auto *before=s->project();submit_result=XTC_ERR_INVALID_STATE;
CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,3)==XTC_ERR_INVALID_STATE && s->project()==before);submit_result=XTC_OK;
CHECK(window.add_track_effect(track,"/missing/plugin.so",0)!=XTC_OK && s->project()==before);
CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,3)==XTC_OK);
CHECK(panel->edit_slot(-1)==XTC_ERR_INVALID_ARGUMENT);
CHECK(panel->edit_slot(1)==XTC_OK && panel->selected_slot()==1);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.slots[1].plugin.index==2);
CHECK(panel->edit_slot(2)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && !chain.slots[1].enabled);
CHECK(panel->edit_slot(3)==XTC_OK && window.undo()==XTC_OK);panel->refresh(true);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==2);
CHECK(panel->edit_slot(3)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK);chain.slots[0].plugin.values[0]=10;
CHECK(window.set_track_chain(track,chain)==XTC_OK);
Fl::add_timeout(.05,[](void *){auto *dialog=Fl::modal();CHECK(dialog);press(find(dialog,"Reset defaults"));press(find(dialog,"Apply"));});
CHECK(panel->inspect_slot()==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.slots[0].plugin.values[0]!=10);
const auto *cancel_before=s->project();
Fl::add_timeout(.05,[](void *){press(find(Fl::modal(),"Cancel"));});CHECK(panel->inspect_slot()==XTC_OK && s->project()==cancel_before);
CHECK(panel->set_mix(.3,.8)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.slots[0].wet==.3f && chain.output_gain==.8f);
auto *mix_before=s->project();CHECK(panel->set_mix(NAN,1)==XTC_ERR_INVALID_ARGUMENT && s->project()==mix_before);
CHECK(window.set_tempo(135000)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.slots[0].plugin.tempo==135000);
CHECK(panel->save_preset("/tmp/xtc-inserts-preset.xtc")==XTC_OK);
CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,3)==XTC_OK && panel->load_preset("/tmp/xtc-inserts-preset.xtc")==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1);
auto *preset_before=s->project();CHECK(panel->load_preset("/missing/preset.xtc")!=XTC_OK && s->project()==preset_before);
unlink("/tmp/xtc-inserts-preset.xtc");
for(const char *variable:{"XTC_TEST_PLUGIN","XTC_TEST_CLAP","XTC_TEST_VST2","XTC_TEST_VST3"}) {
if(const char *binary=std::getenv(variable)) {
CHECK(window.add_track_effect(track,binary,0)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==2);
CHECK(window.undo()==XTC_OK);panel->refresh(true);
}
}
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK);
auto restored_chain=chain;for(size_t i=1;i<XTC_TRACK_INSERTS;++i)chain.slots[i]=chain.slots[0];chain.count=XTC_TRACK_INSERTS;
for(auto &slot:chain.slots){slot.enabled=1;slot.wet=.5f;}
CHECK(window.set_track_chain(track,chain)==XTC_OK);
auto *insert_canvas=graph_canvas(panel);auto *insert_scroll=dynamic_cast<Fl_Scroll *>(insert_canvas->parent());
CHECK(insert_scroll && insert_canvas->w()>insert_scroll->w());
insert_scroll->scroll_to(insert_canvas->w()-insert_scroll->w(),0);CHECK(insert_scroll->xposition()>0);
const auto *full_before=s->project();CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,2)==XTC_ERR_CAPACITY && s->project()==full_before);
CHECK(window.set_track_chain(track,restored_chain)==XTC_OK);
CHECK(window.save_project("/tmp/xtc-inserts-gui.xtc")==XTC_OK && window.new_project()==XTC_OK && window.open_project("/tmp/xtc-inserts-gui.xtc")==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1);unlink("/tmp/xtc-inserts-gui.xtc");
window.do_callback();puts("Track inserts GUI adoption/undo/rollback/reload passed.");return failures ? 1 : 0;
}
int midi_workflow_tests()
{
char dir[]="/tmp/xtc-midi-settings-XXXXXX";CHECK(mkdtemp(dir));std::string config=std::string(dir)+"/.xtc.conf";
reset();{
MainWindow window(false,config.c_str());window.show();auto *s=timeline(&window);
auto settings=window.settings();strcpy(settings.midi_input_port,"Keyboard:out");strcpy(settings.midi_output_port,"Synth:in");settings.midi_input_channel=2;settings.midi_output_channel=4;settings.midi_thru=1;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.midi_devices(true)==std::vector<std::string>{"Keyboard:out"} && window.midi_devices(false)==std::vector<std::string>{"Synth:in"});
CHECK(window.start_configured_backend()==XTC_OK && handle.running);CHECK(xtc_backend_stop(&handle)==XTC_OK);
PreferencesDialog dialog(settings,config,[&](const xtc_settings &v){return window.apply_preferences(v);},[&](bool input){return window.midi_devices(input);});
CHECK(find(&dialog,"Refresh MIDI devices"));press(find(&dialog,"Refresh MIDI devices"));
CHECK(window.settings().midi_output_channel==4);
auto missing=settings;strcpy(missing.midi_input_port,"missing:port");CHECK(window.apply_preferences(missing)==XTC_OK);
CHECK(window.start_configured_backend()==XTC_ERR_NOT_FOUND && !handle.running);CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.create_track(true)==XTC_OK && window.open_piano_roll()==XTC_OK);
CHECK(window.piano_roll()->add_note(0,240,64,90,2)==XTC_OK);
std::string midi=std::string(dir)+"/clip.mid";auto *original=s->project();
CHECK(window.export_midi_clip(midi.c_str())==XTC_OK && s->project()==original);
{char signature[4];FILE *f=fopen(midi.c_str(),"rb");CHECK(f && fread(signature,1,4,f)==4 && !memcmp(signature,"MThd",4));if(f)fclose(f);}
unlink(midi.c_str());window.detach_piano(false);
s->grid(480);window.show_track_inspector(true);window.resize(20,30,1300,820);
CHECK(window.set_loop(960,3840,true)==XTC_OK);window.do_callback();CHECK(!window.shown());
}
xtc_settings saved{};CHECK(xtc_settings_load(&saved,config.c_str())==XTC_OK && saved.sequencer_snap==480 && saved.inspector_visible && saved.loop_start==960 && saved.width==1300 && saved.midi_thru && !strcmp(saved.midi_input_port,"Keyboard:out"));
reset();{
MainWindow window(false,config.c_str());CHECK(window.settings().sequencer_snap==480 && window.settings().inspector_visible && window.settings().loop_enabled);
window.do_callback();
}
{FILE *f=fopen(config.c_str(),"wb");CHECK(f);if(f){fputs("[invalid]\n",f);fclose(f);}}
reset();{MainWindow window(false,config.c_str());CHECK(window.save_preferences()==XTC_ERR_INVALID_STATE);window.do_callback();}
CHECK(xtc_settings_load(&saved,config.c_str())==XTC_ERR_FORMAT);
unlink(config.c_str());rmdir(dir);puts("MIDI workflow/configuration GUI checks passed.");return failures ? 1 : 0;
}
int main(int argc, char **argv)
{
if(std::getenv("XTC_TEST_MIDI_WORKFLOWS_ONLY"))return midi_workflow_tests();
if(std::getenv("XTC_TEST_CREATIVE_ONLY"))return creative_workflow_tests();
if(std::getenv("XTC_TEST_TRACK_FX_ONLY"))return track_insert_tests();
if(std::getenv("XTC_TEST_SAMPLE_EDITOR_ONLY"))return sample_editor_tests();
if(!std::getenv("XTC_TEST_MODERN_EDITING_ONLY")) {
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 failed; check audio and saved MIDI devices in Preferences: I/O error"));
pump(); // A refresh must not erase a command error.
CHECK(contains(&window, "Start failed; check audio and saved MIDI devices in Preferences: 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 = graph_canvas(&window);
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";
auto *creation=dynamic_cast<Fl_Menu_Button *>(find(&window,"Create")); CHECK(creation); if(creation)creation->picked(creation->find_item("Empty 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);
char sample[1024];CHECK(getcwd(sample,sizeof(sample))!=nullptr);std::strcat(sample,"/examples/projects/original-pluck.wav");
CHECK(window.load_sampler(track,sample)==XTC_OK);
CHECK(xtc_project_get_instrument(sequence->project(),track,&spec)==XTC_OK && spec.index==4 && !std::strcmp(spec.sample_path,sample));
CHECK(window.save_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-sampler-gui.xtc")==XTC_OK);
CHECK(window.load_sampler(track,"/absent/sample.wav")==XTC_OK);
CHECK(window.load_sampler(track,sample)==XTC_OK); // Relink is one prepared history edit.
CHECK(window.undo()==XTC_OK && window.undo(true)==XTC_OK);unlink("/tmp/xtc-sampler-gui.xtc");
size_t added=SIZE_MAX;CHECK(window.add_plugin(XTC_BUILTIN_PATH,effect,&added)==XTC_OK);
CHECK(added!=id);CHECK(window.undo()==XTC_OK);
unsigned int stereo=instrument+5;
CHECK(window.add_plugin(XTC_BUILTIN_PATH,stereo,&added)==XTC_OK);
CHECK(window.set_plugin_control(added,stereo==5 ? 3U : 4U,1)==XTC_OK);
CHECK(window.save_project("/tmp/xtc-stereo-gui.xtc")==XTC_OK);
CHECK(window.new_project()==XTC_OK && window.open_project("/tmp/xtc-stereo-gui.xtc")==XTC_OK);
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),added,&spec)==XTC_OK && spec.index==stereo);
unlink("/tmp/xtc-stereo-gui.xtc");
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);
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 *create=dynamic_cast<Fl_Menu_Button *>(find(&window,"Create"));
CHECK(create && create->find_item("New MIDI track") && create->find_item("Import WAV"));
CHECK(sequence->x()<100);
window.show_track_inspector(true);CHECK(sequence->x()>200);
window.show_track_inspector(false);CHECK(sequence->x()<100);
create->picked(create->find_item("New MIDI track"));
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"));
create->picked(create->find_item("New audio track"));
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(create->x() >= 0 && create->x()+create->w() <= window.w());
CHECK(sequence->parent()->x()+sequence->parent()->w() <= window.w());
CHECK(find(&window,"Play")->visible_r() && create->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=window.settings();settings.loop_enabled=0;settings.stop_after_tail=1;settings.tail_ms=0;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(window.create_track(true)==XTC_OK);press(find(&window,"Play"));
engine_status.sample_position=1000000;pump();CHECK(!handle.running);
settings.loop_enabled=1;settings.stop_after_tail=0;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);
}
{
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()->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());
}
reset();
{
std::string path="/tmp/xtc-panel-layout-"+std::to_string(getpid())+".conf";
{
MainWindow window(false,path.c_str());window.show();
window.detach_transport(true);window.detach_mixer(true);
window.transport_window()->resize(40,40,400,400);window.mixer_window()->resize(80,80,700,460);pump();
auto *group=dynamic_cast<Fl_Group *>(find(window.transport_window(),"Play")->parent());CHECK(group);
for(int i=0;i<group->children();++i) {
auto *widget=group->child(i);CHECK(widget->x()>=0 && widget->x()+widget->w()<=400);
CHECK(widget->y()+widget->h()<=window.transport_window()->h());
}
Fl::add_timeout(.05,[](void *) {auto *d=Fl::first_window();press(find(d,"Apply"));press(find(d,"Close"));});
window.show_preferences();xtc_settings saved{};CHECK(xtc_settings_load(&saved,path.c_str())==XTC_OK);
CHECK(saved.transport_detached && saved.mixer_detached && saved.transport_width==400 && saved.mixer_width==700);
saved.transport_x=30000;saved.transport_y=-30000;CHECK(xtc_settings_save(&saved,path.c_str())==XTC_OK);
window.do_callback();
}
{
MainWindow window(false,path.c_str());window.show();pump();
CHECK(window.transport_window() && window.transport_window()->shown());
CHECK(window.mixer_window() && window.mixer_window()->shown());
int sx,sy,sw,sh;Fl::screen_work_area(sx,sy,sw,sh,0);
CHECK(window.transport_window()->x()>=sx && window.transport_window()->x()+window.transport_window()->w()<=sx+sw);
CHECK(window.transport_window()->y()>=sy && window.transport_window()->y()+window.transport_window()->h()<=sy+sh);
window.detach_transport(false);window.detach_mixer(false);window.do_callback();
}
unlink(path.c_str());
}
reset();
{
std::string path="/tmp/xtc-editors-"+std::to_string(getpid())+".conf";
{
MainWindow window(false,path.c_str());window.show();CHECK(window.create_track(true)==XTC_OK);
auto *sequence=timeline(&window);auto *graph=graph_canvas(&window);const xtc_project *project=sequence->project();
uint64_t track=sequence->selected_track(),clip=sequence->selected_clip();
auto *scroll=dynamic_cast<Fl_Scroll *>(sequence->parent());CHECK(scroll);scroll->scroll_to(400,0);pump();
int pinned=sequence->header_left();CHECK(pinned==scroll->x());
sequence->header_width(260);pump();CHECK(sequence->header_width()==260 && scroll->xposition()==400);
CHECK(sequence->instrument_choice(track)->x()==pinned+10 && sequence->instrument_choice(track)->w()==238);
gesture(sequence,pinned-sequence->x()+258,16,40,0);CHECK(sequence->header_width()==300);
sequence->header_width(260);
CHECK(window.open_piano_roll()==XTC_OK);auto *piano=window.piano_roll();
CHECK(piano->add_note(0,240,60)==XTC_OK);uint64_t note=piano->selected_note();
project=sequence->project();
auto *piano_scroll=dynamic_cast<Fl_Scroll *>(piano->parent());CHECK(piano_scroll);piano_scroll->scroll_to(10,500);
for(int pass=0;pass<3;++pass) {
window.detach_editor(0,true);window.detach_editor(1,true);window.detach_piano(false);pump();
CHECK(sequence->window()==window.editor_window(0) && graph->window()==window.editor_window(1));
CHECK(find(window.editor_window(0),"Snap")!=nullptr);
CHECK(piano->window()==&window && piano->selected_note()==note && sequence->project()==project);
CHECK(sequence->selected_track()==track && sequence->selected_clip()==clip);
CHECK(scroll->xposition()==400 && piano_scroll->yposition()==500);
window.editor_window(0)->resize(20,20,1200,700);window.editor_window(1)->resize(30,30,1000,500);pump();
window.detach_piano(true);CHECK(piano->window()!=&window);
auto *piano_window=piano->window();piano_window->do_callback();CHECK(piano->window()==&window);
window.editor_window(0)->do_callback();window.editor_window(1)->do_callback();pump();
CHECK(sequence->window()==&window && graph->window()==&window && scroll->xposition()==400);
CHECK(piano->selected_note()==note && sequence->project()==project);
}
window.detach_editor(0,true);window.detach_editor(1,true);window.detach_piano(false);
Fl::add_timeout(.05,[](void *) {auto *d=Fl::first_window();press(find(d,"Apply"));press(find(d,"Close"));});
window.show_preferences();xtc_settings saved{};CHECK(xtc_settings_load(&saved,path.c_str())==XTC_OK);
CHECK(saved.sequencer_detached && saved.graph_detached && !saved.piano_detached && saved.track_header_width==260);
close_result=XTC_ERR_IO;window.do_callback();CHECK(window.editor_window(0)->shown() && window.editor_window(1)->shown());
close_result=XTC_OK;window.do_callback();CHECK(!window.editor_window(0)->shown() && !window.editor_window(1)->shown());
}
{
MainWindow window(false,path.c_str());window.show();pump();
CHECK(window.editor_window(0)->shown() && window.editor_window(1)->shown());
CHECK(window.create_track(true)==XTC_OK && window.open_piano_roll()==XTC_OK && window.piano_roll()->window()==&window);
window.reset_layout();pump();CHECK(timeline(&window) && graph_canvas(&window));
CHECK(!window.editor_window(0)->shown() && !window.editor_window(1)->shown());
CHECK(timeline(&window)->header_width()==180);window.do_callback();
}
unlink(path.c_str());
}
}
if(!std::getenv("XTC_TEST_NEXT_DAW_ONLY")) {
reset();
{
MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK);
auto *s=timeline(&window);uint64_t first=s->selected_clip();CHECK(s->duplicate_selection()==XTC_OK);
uint64_t second=s->selected_clip();CHECK(s->toggle_clip(first));CHECK(s->selected_clips().size()==2);
CHECK(s->clip_selected(first) && s->clip_selected(second));CHECK(!s->toggle_clip(UINT64_MAX));
window.detach_editor(0,true);CHECK(s->selected_clips().size()==2);window.detach_editor(0,false);
CHECK(s->toggle_clip(first) && !s->clip_selected(first));s->select_all_clips();CHECK(s->selected_clips().size()==2);
CHECK(s->nudge_selection(1)==XTC_OK);xtc_clip_info moved{};
CHECK(xtc_project_find_clip(s->project(),first,&moved)==XTC_OK && moved.start_tick==240);
CHECK(s->duplicate_selection()==XTC_OK && s->selected_clips().size()==2);
size_t tc=0,cc=0;xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(s->delete_selection()==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==2);
s->select_all_clips();CHECK(s->copy_selection()==XTC_OK);s->position(7680);
CHECK(s->paste_selection()==XTC_OK && s->selected_clips().size()==2);
xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(s->repeat_selection(2)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8);
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
CHECK(window.undo(true)==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==8);
s->select_all_clips();CHECK(s->transform_selection(XTC_CLIPS_ALIGN,3840)==XTC_OK);
CHECK(xtc_project_find_clip(s->project(),first,&moved)==XTC_OK && moved.start_tick==3840);
CHECK(s->transform_selection(XTC_CLIPS_SLIP,0)==XTC_OK);
CHECK(s->select(s->selected_track(),first));CHECK(window.open_piano_roll()==XTC_OK);
CHECK(window.piano_roll()->add_note(230,120,61)==XTC_OK);
xtc_midi_transform edit{};edit.grid=240;edit.swing=50;edit.strength=100;
CHECK(s->transform_midi(edit)==XTC_OK);xtc_note_info quantized{};
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.start_tick==300);
edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=50;CHECK(s->transform_midi(edit)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.length_tick==60);
edit.type=XTC_MIDI_TRANSFORM_LEGATO;CHECK(s->transform_midi(edit)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.length_tick==3540);
edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=0;edit.scale=1;CHECK(s->transform_midi(edit)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.pitch==60);
edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=50;edit.velocity_offset=10;CHECK(s->transform_midi(edit)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.velocity==60);
CHECK(window.create_track(false)==XTC_OK && s->add_region()==XTC_OK);
CHECK(s->transform_selection(XTC_CLIPS_FADE,10)==XTC_OK);
CHECK(xtc_project_find_clip(s->project(),s->selected_clip(),&moved)==XTC_OK && moved.fade_in_tick==10 && moved.fade_out_tick==10);
CHECK(s->transform_selection(XTC_CLIPS_FADE,0)==XTC_OK);
uint64_t mix_track=s->selected_track();CHECK(s->toggle_clip(first) && s->toggle_clip(first));
CHECK(s->selected_track()==mix_track);
CHECK(window.mix_snapshot(0,true)==XTC_OK);
CHECK(xtc_project_set_mix(s->project(),mix_track,.5f,-.5f,0,0)==XTC_OK);
CHECK(window.mix_snapshot(1,true)==XTC_OK && window.mix_snapshot(0,false)==XTC_OK);
xtc_track_info mix{};xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==1);
CHECK(window.mix_snapshot(1,false)==XTC_OK);xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==.5f);
CHECK(window.undo()==XTC_OK);xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==1);
CHECK(window.new_project()==XTC_OK && window.mix_snapshot(0,false)==XTC_ERR_NOT_FOUND);
window.do_callback();
}
}
reset();
{
MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK);
auto *s=timeline(&window);s->position(960);CHECK(s->marker("Verse")==XTC_OK);
size_t markers=0;xtc_project_marker_count(s->project(),&markers);CHECK(markers==1);
CHECK(window.undo()==XTC_OK);xtc_project_marker_count(s->project(),&markers);CHECK(markers==0);
CHECK(window.undo(true)==XTC_OK);xtc_project_marker_count(s->project(),&markers);CHECK(markers==1);
window.detach_editor(0,true);pump();window.detach_editor(0,false);
s->position(1920);CHECK(s->marker("Chorus")==XTC_OK);s->position(0);
CHECK(window.navigate_marker(1)==XTC_OK && s->position()==960);
CHECK(window.loop_section()==XTC_OK);CHECK(window.navigate_marker(1)==XTC_OK && s->position()==1920);
CHECK(window.navigate_marker(-1)==XTC_OK && s->position()==960);
s->position(3840);CHECK(s->ripple_time(960,false)==XTC_OK);
xtc_marker moved{};xtc_project_get_marker(s->project(),1,&moved);CHECK(moved.tick==1920);
CHECK(window.undo()==XTC_OK);
CHECK(s->select(0));CHECK(s->duplicate_track("Keys copy")==XTC_OK);
size_t tracks=0,clips=0;xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(tracks==2 && clips==2);
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(tracks==1 && clips==1);
CHECK(s->select(0,0));s->position(1920);CHECK(s->split_selection()==XTC_OK);
xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==2);
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==1);
CHECK(s->select(0,0));CHECK(s->duplicate_selection()==XTC_OK);s->select_all_clips();CHECK(s->merge_midi()==XTC_OK);
xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==1);
CHECK(window.undo()==XTC_OK);xtc_project_get_counts(s->project(),&tracks,&clips);CHECK(clips==2);
CHECK(s->select(0,0));CHECK(window.open_piano_roll()==XTC_OK);CHECK(window.piano_roll()->add_note(480,240,60)==XTC_OK);
xtc_midi_transform human{};human.type=XTC_MIDI_TRANSFORM_HUMANIZE;human.seed=42;human.timing_variation=12;human.velocity_variation=8;
CHECK(s->transform_midi(human)==XTC_OK);CHECK(window.undo()==XTC_OK);
CHECK(s->select(0,0));CHECK(s->make_chord({4,7})==XTC_OK);
size_t notes=0;xtc_project_note_count(s->project(),&notes);CHECK(notes==3);
CHECK(s->arpeggiate(80,50,false)==XTC_OK);xtc_project_note_count(s->project(),&notes);CHECK(notes==3);
CHECK(window.undo()==XTC_OK);xtc_project_note_count(s->project(),&notes);CHECK(notes==3);
char sample[4096];CHECK(realpath("examples/projects/original-pluck.wav",sample)!=nullptr);
CHECK(window.import_audio(sample)==XTC_OK);CHECK(s->normalize_audio(.25f)==XTC_OK);
xtc_clip_info normalized{};xtc_project_find_clip(s->project(),s->selected_clip(),&normalized);CHECK(normalized.gain>0 && normalized.gain<=4);
CHECK(window.undo()==XTC_OK);
std::string folder="/tmp/xtc-gui-collect-"+std::to_string(getpid());CHECK(window.collect_project(folder.c_str())==XTC_OK);
CHECK(access((folder+"/project.xtc").c_str(),F_OK)==0);unlink((folder+"/project.xtc").c_str());unlink((folder+"/media-000.wav").c_str());rmdir(folder.c_str());
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;
}