Phase 35: add persistent preferences and sequencer loop playback

This commit is contained in:
Johannes Findeisen 2026-10-28 10:37:01 +01:00
commit 47b7d38a66
21 changed files with 852 additions and 108 deletions

View file

@ -4,6 +4,8 @@
#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 "../../src/gui/plugin_library.hpp"
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Choice.H>
#include <FL/Fl_Value_Slider.H>
@ -126,6 +128,8 @@ xtc_result xtc_backend_close(xtc_backend *backend)
}
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)
@ -183,7 +187,7 @@ int main(int argc, char **argv)
{
reset();
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *play = find(&window, "Play");
auto *stop = find(&window, "Stop");
@ -535,7 +539,7 @@ int main(int argc, char **argv)
for (xtc_result failure : {XTC_ERR_IO, XTC_ERR_UNSUPPORTED}) {
reset();
open_result = failure;
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
CHECK(contains(&window, "Open JACK failed") && !find(&window, "Play")->active());
open_result = XTC_OK;
@ -549,21 +553,21 @@ int main(int argc, char **argv)
partial_open = true;
open_result = XTC_ERR_IO;
{
MainWindow window(false);
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);
MainWindow window(false,"");
press(find(&window, "Play"));
window.do_callback();
}
CHECK(alive == 0U);
reset();
{
MainWindow window(false); auto *s = timeline(&window);
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);
@ -585,7 +589,7 @@ int main(int argc, char **argv)
}
{
reset();
MainWindow window(false);
MainWindow window(false,"");
auto *canvas = graph_canvas(&window);
CHECK(canvas != nullptr);
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
@ -628,7 +632,7 @@ int main(int argc, char **argv)
window.do_callback();
}
{
reset(); MainWindow window(false);
reset(); MainWindow window(false,"");
auto *sequence = timeline(&window); auto *mixer = window.mixer();
CHECK(sequence->add_track() == XTC_OK);
CHECK(mixer->add_bus() == XTC_OK);
@ -649,7 +653,7 @@ int main(int argc, char **argv)
window.do_callback();
}
{
reset(); MainWindow window(false);
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);
@ -674,7 +678,7 @@ int main(int argc, char **argv)
unlink("/tmp/xtc-gui-controls-test.xtc"); window.do_callback();
}
{
reset(); MainWindow window(false);
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();
@ -705,7 +709,7 @@ int main(int argc, char **argv)
}
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
reset();
MainWindow window(false);
MainWindow window(false,"");
auto *sequence = timeline(&window);
size_t id = SIZE_MAX;
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
@ -807,7 +811,7 @@ int main(int argc, char **argv)
}
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);
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);
@ -831,7 +835,7 @@ int main(int argc, char **argv)
}
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);
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);
@ -867,7 +871,7 @@ int main(int argc, char **argv)
// the current model. Exercise real sidebar buttons rather than mock actions.
reset();
{
MainWindow window(false); window.show();
MainWindow window(false,""); window.show();
auto *sequence = timeline(&window);
CHECK(sequence != nullptr);
auto *midi = find(&window,"New MIDI track");
@ -908,7 +912,7 @@ int main(int argc, char **argv)
}
reset();
{
MainWindow window(false); window.show(); auto *sequence = timeline(&window);
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);
@ -964,6 +968,44 @@ int main(int argc, char **argv)
xtc_project_get_note(sequence->project(),0,&note); CHECK(note.velocity > 29 && note.pitch == 60 && note.channel == 2);
unlink("/tmp/xtc-grid-velocity.xtc"); window.do_callback();
}
{
char dir[]="/tmp/xtc-preferences-XXXXXX"; CHECK(mkdtemp(dir));
std::string path=std::string(dir)+"/.xtc.conf";
reset();
{
MainWindow window(false,path.c_str());window.show();
xtc_settings settings=window.settings();settings.note_velocity=73;settings.sequencer_snap=480;
CHECK(window.apply_preferences(settings)==XTC_OK);
CHECK(timeline(&window)->grid()==480);
int old_state=Fl::e_state;Fl::e_state=FL_SHIFT;
gesture(timeline(&window),228,12,96,0);Fl::e_state=old_state;
CHECK(window.settings().loop_start==960 && window.settings().loop_end==2880);
CHECK(window.set_loop(960,3840,true)==XTC_OK);
CHECK(window.settings().loop_enabled==1 && window.settings().loop_start==960);
CHECK(window.set_loop(4000,2000,true)==XTC_ERR_INVALID_ARGUMENT);
settings=window.settings();CHECK(window.apply_preferences(settings)==XTC_OK);
settings.note_velocity=128;CHECK(window.apply_preferences(settings)==XTC_ERR_INVALID_ARGUMENT);
PreferencesDialog dialog(window.settings(),path,[&](const xtc_settings &v){return window.apply_preferences(v);});
auto *input=dynamic_cast<Fl_Input *>(find(&dialog,"New note velocity"));CHECK(input);
if(input)input->value("81");
press(find(&dialog,"Apply"));CHECK(window.settings().note_velocity==81);
if(input)input->value("82");
press(find(&dialog,"Close"));CHECK(window.settings().note_velocity==81);
window.do_callback();
}
reset();
{
MainWindow window(false,path.c_str());CHECK(window.settings().note_velocity==81);
CHECK(window.settings().loop_start==960 && window.settings().loop_enabled==1);
xtc_settings paths=window.settings();
std::string binary=std::string(dir)+"/test.clap";FILE *f=fopen(binary.c_str(),"wb");CHECK(f);if(f)fclose(f);
std::snprintf(paths.clap_paths,sizeof(paths.clap_paths),"%s:%s",dir,dir);
paths.vst2_paths[0]=paths.vst3_paths[0]=paths.ladspa_paths[0]=0;
auto files=xtc_plugin_files(paths);CHECK(files.size()==1 && files[0]==binary);
unlink(binary.c_str());window.do_callback();
}
unlink(path.c_str());rmdir(dir);
}
if (failures != 0) return 1;
std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes);
return 0;

View file

@ -46,7 +46,7 @@ int main(int argc, char **argv)
{
// Real JACK backend and real FLTK event loop. No routes, no playback data.
for (int iteration = 0; iteration < 3; ++iteration) {
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
CHECK(find(window, "ready / stopped") != nullptr);
auto *play = find(window, "Play");

View file

@ -98,7 +98,7 @@ int main(int argc, char **argv)
xtc_clip_info arranged{};
size_t nodes, edges;
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *timeline = widget<SequencerWidget>(window);
auto *tabs = widget<Fl_Tabs>(window);
@ -123,7 +123,7 @@ int main(int argc, char **argv)
window.do_callback(); CHECK(!window.shown());
} // Destroy all widgets/engine state before opening a fresh window.
{
MainWindow window(false);
MainWindow window(false,"");
window.show(argc, argv);
auto *timeline = widget<SequencerWidget>(window);
widget<Fl_Tabs>(window)->value(widget<Fl_Tabs>(window)->child(2));

View file

@ -361,6 +361,46 @@ int main(void)
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && !status.running);
CHECK(xtc_backend_close(backend) == XTC_OK);
}
{
config.channel_count = 1;config.max_frames = 128;
CHECK(xtc_backend_open(&config, &backend) == XTC_OK);
CHECK(xtc_backend_submit(backend, graph) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 100, 163, 1) == XTC_ERR_INVALID_ARGUMENT);
CHECK(xtc_backend_set_loop(backend, 100, 164, 1) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 0, 64, 0) == XTC_ERR_INVALID_STATE);
CHECK(process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 132);
CHECK(process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 100);
CHECK(process_cb(17, context) == 0 && process_cb(32, context) == 0 && process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 117);
for(i=0;i<128;++i)input[i]=(float)i;
CHECK(process_cb(128,context)==0);
for(i=0;i<128;++i)CHECK(output[i]==input[i]*0.5f);
CHECK(xtc_backend_get_status(backend,&status)==XTC_OK && status.sample_position==117);
CHECK(xtc_backend_stop(backend) == XTC_OK);
CHECK(xtc_backend_set_loop(backend, 100, 164, 0) == XTC_OK);
CHECK(xtc_backend_start(backend) == XTC_OK && process_cb(32, context) == 0);
CHECK(xtc_backend_get_status(backend, &status) == XTC_OK && status.sample_position == 149);
CHECK(xtc_backend_stop(backend) == XTC_OK && xtc_backend_close(backend) == XTC_OK);
}
{
xtc_project *p = NULL;uint64_t track,clip,note;
config.max_frames=128;config.channel_count=1;
CHECK(xtc_backend_open(&config,&backend)==XTC_OK);
CHECK(xtc_project_create(&p)==XTC_OK);
CHECK(xtc_project_add_track(p,"Loop instrument",&track)==XTC_OK);
CHECK(xtc_project_add_midi_clip(p,track,0,3840,&clip)==XTC_OK);
CHECK(xtc_project_add_note(p,clip,0,960,69,100,0,&note)==XTC_OK);
CHECK(xtc_backend_prepare_project(backend,p)==XTC_OK);
CHECK(xtc_backend_set_loop(backend,0,64,1)==XTC_OK);
CHECK(xtc_backend_start(backend)==XTC_OK && process_cb(128,context)==0);
CHECK(output[1]!=0);
for(i=0;i<64;++i)CHECK(output[i]==output[i+64]);
CHECK(xtc_backend_stop(backend)==XTC_OK && xtc_backend_close(backend)==XTC_OK);
xtc_project_destroy(p);
}
CHECK(closed >= 104);
CHECK(xtc_backend_close(NULL) == XTC_OK);
xtc_graph_destroy(invalid_graph);

View file

@ -0,0 +1,29 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_settings.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"%d: %s\n",__LINE__,#x);return 1;}}while(0)
int main(void)
{
char path[]="/tmp/xtc-settings-XXXXXX",value[4096]; int fd=mkstemp(path); size_t i,n;
xtc_settings a,b,before; struct stat st; FILE *f;
CHECK(fd>=0);close(fd);unlink(path);
xtc_settings_defaults(&a);CHECK(xtc_settings_validate(&a)==XTC_OK);
b=a;CHECK(xtc_settings_load(&b,path)==XTC_ERR_NOT_FOUND);
strcpy(a.clap_paths,"~/space folder:/tmp/quoted\"name\\path");a.loop_enabled=1;a.note_velocity=87;
CHECK(xtc_settings_save(&a,path)==XTC_OK);CHECK(stat(path,&st)==0 && (st.st_mode&0777)==0600);
CHECK(xtc_settings_load(&b,path)==XTC_OK);CHECK(memcmp(&a,&b,sizeof(a))==0);
xtc_settings_schema(&n);
for(i=0;i<n;++i){CHECK(xtc_settings_get(&a,i,value,sizeof(value))==XTC_OK);CHECK(xtc_settings_set(&b,i,value)==XTC_OK);}
CHECK(memcmp(&a,&b,sizeof(a))==0);
{const char *bad[]={"[unknown]\n","[audio]\nchannels=3\n","[audio]\nchannels=1\nchannels=2\n","[audio]\nchannels=9999999999999999999999\n","[plugins]\nclap_paths=\"unterminated\n","[transport]\nloop_start=4000\nloop_end=2\n"};
for(i=0;i<sizeof(bad)/sizeof(bad[0]);++i){f=fopen(path,"wb");CHECK(f);CHECK(fputs(bad[i],f)>=0);CHECK(fclose(f)==0);before=b;CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);CHECK(memcmp(&before,&b,sizeof(b))==0);}}
f=fopen(path,"wb");CHECK(f);CHECK(fwrite(";x\0hidden",1,9,f)==9);fclose(f);CHECK(xtc_settings_load(&b,path)==XTC_ERR_FORMAT);
CHECK(xtc_settings_save(&a,path)==XTC_OK);b=a;b.loop_end=b.loop_start;CHECK(xtc_settings_save(&b,path)==XTC_ERR_INVALID_ARGUMENT);CHECK(xtc_settings_load(&b,path)==XTC_OK && b.loop_end==a.loop_end);
CHECK(xtc_settings_save(&a,"/no-such-xtc-directory/settings")==XTC_ERR_IO);
CHECK(xtc_settings_path(value,sizeof(value))==XTC_OK && strstr(value,"/.xtc.conf"));
unlink(path);puts("Preferences INI validation, roundtrip, permissions and failure tests passed.");return 0;
}