174 lines
17 KiB
C++
174 lines
17 KiB
C++
/* Optional documentation tool: real FLTK widgets and real backend, isolated data. */
|
|
#include "../src/gui/main_window.hpp"
|
|
#include "../src/gui/sample_editor.hpp"
|
|
#include "../src/gui/sequencer_widget.hpp"
|
|
#include "../src/gui/mixer_widget.hpp"
|
|
#include "../src/gui/piano_roll.hpp"
|
|
#include "../src/gui/plugin_dialog.hpp"
|
|
#include "../src/gui/plugin_library.hpp"
|
|
#include "../src/gui/track_inserts.hpp"
|
|
#include <FL/Fl_Menu_Button.H>
|
|
#include <FL/Fl.H>
|
|
#include <FL/Fl_Tabs.H>
|
|
#include <FL/Fl_Menu_Bar.H>
|
|
#include <FL/Fl_Button.H>
|
|
#include <FL/Fl_Choice.H>
|
|
#include <FL/Fl_Input_Choice.H>
|
|
#include <FL/Fl_Scroll.H>
|
|
#include <FL/x.H>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <stdexcept>
|
|
#include <functional>
|
|
#include <unistd.h>
|
|
|
|
namespace {
|
|
MainWindow *main_window;
|
|
std::string destination;
|
|
void check(xtc_result r) {if(r!=XTC_OK)throw std::runtime_error(xtc_result_string(r));}
|
|
void pump() {auto end=std::chrono::steady_clock::now()+std::chrono::milliseconds(180);while(std::chrono::steady_clock::now()<end)Fl::wait(.01);}
|
|
template<class T> T *first(Fl_Group &g) {for(int i=0;i<g.children();++i){auto *c=g.child(i);if(auto *t=dynamic_cast<T *>(c))return t;if(auto *n=dynamic_cast<Fl_Group *>(c))if(auto *t=first<T>(*n))return t;}return nullptr;}
|
|
Fl_Widget *named(Fl_Group &g,const char *label) {for(int i=0;i<g.children();++i){auto *c=g.child(i);if(c->label()&&!strcmp(c->label(),label))return c;if(auto *n=dynamic_cast<Fl_Group *>(c))if(auto *w=named(*n,label))return w;}return nullptr;}
|
|
void tab(const char *label) {auto *page=named(*main_window,label);if(!page)throw std::runtime_error(label);auto *tabs=dynamic_cast<Fl_Tabs *>(page->parent());if(!tabs)throw std::runtime_error("Tab parent");tabs->value(page);tabs->redraw();pump();}
|
|
void capture(const char *name) {if(std::getenv("XTC_CAPTURE_SAMPLE_ONLY")&&std::strncmp(name,"sample-",7))return;Fl::flush();pump();std::string command="import -window root '"+destination+"/"+name+".png'";if(system(command.c_str()))throw std::runtime_error("ImageMagick capture failed");std::printf("Captured %s\n",name);std::fflush(stdout);}
|
|
struct Modal {const char *name;const char *page;};
|
|
void modal_capture(void *data) {auto *job=static_cast<Modal *>(data);auto *w=Fl::modal();if(!w||w==main_window)throw std::runtime_error("Expected a modal dialog");if(auto *editor=dynamic_cast<SampleEditor *>(w)){editor->select(2400,std::min(size_t(12000),editor->frames()));if(!strcmp(job->name,"sample-transients")){editor->select(0,editor->frames());check(editor->detect_attacks(.05,2400));}}
|
|
if(job->page){auto *tabs=first<Fl_Tabs>(*w);auto *page=named(*w,job->page);if(!tabs||!page)throw std::runtime_error(job->page);tabs->value(page);tabs->redraw();}capture(job->name);auto *cancel=named(*w,"Cancel");if(!cancel)cancel=named(*w,"Close");if(!cancel)cancel=named(*w,"OK");if(cancel)cancel->do_callback();else w->do_callback();}
|
|
void dialog(const char *name,std::function<void()> action,const char *page=nullptr) {Modal job{name,page};Fl::add_timeout(.3,modal_capture,&job);action();Fl::remove_timeout(modal_capture,&job);pump();}
|
|
void menu(const char *path) {auto *bar=first<Fl_Menu_Bar>(*main_window);const auto *item=bar->find_item(path);if(!item)throw std::runtime_error(path);bar->picked(item);}
|
|
void piano_view() {
|
|
check(main_window->open_piano_roll());
|
|
auto *piano=main_window->piano_roll();
|
|
if(auto *scroll=dynamic_cast<Fl_Scroll *>(piano->parent()))scroll->scroll_to(0,50*16);
|
|
size_t count=0;check(xtc_project_note_count(piano->project(),&count));
|
|
for(size_t i=0;i<count;++i){xtc_note_info note{};check(xtc_project_get_note(piano->project(),i,¬e));if(note.clip_id==piano->clip()){piano->select_note(note.id);break;}}
|
|
piano->redraw();pump();
|
|
}
|
|
void button(const char *name) {auto *b=named(*main_window,name);if(!b)throw std::runtime_error(name);b->do_callback();pump();}
|
|
}
|
|
int main(int argc,char **argv) {
|
|
if(argc!=2){std::fprintf(stderr,"Usage: capture_screenshots OUTPUT_DIRECTORY (absolute, no apostrophes)\n");return 1;}
|
|
destination=argv[1];if(destination.empty()||destination[0]!='/'||destination.find('\'')!=std::string::npos)return 1;
|
|
try {
|
|
std::filesystem::create_directories(destination);
|
|
char temp[]="/tmp/xtc-screenshots-XXXXXX";if(!mkdtemp(temp))return 1;std::string root=temp;
|
|
struct Cleanup {std::string path;~Cleanup(){std::filesystem::remove_all(path);}} cleanup{root};
|
|
xtc_project *p=nullptr;check(xtc_project_create(&p));check(xtc_project_load(p,"examples/projects/modern-session.xtc"));
|
|
for(auto pair:{std::pair<uint64_t,const char *>{0,"Intro"},{3840,"Verse"},{7680,"Chorus"},{11520,"Outro"}})check(xtc_project_set_marker(p,pair.first,pair.second));
|
|
uint64_t bus;check(xtc_project_add_bus(p,"Music bus",&bus));check(xtc_project_route_track(p,0,bus));
|
|
xtc_automation_point a{0,0,XTC_AUTOMATION_GAIN,.65f,0};check(xtc_project_set_automation(p,&a));a.tick=3840;a.value=1;check(xtc_project_set_automation(p,&a));a.tick=7680;a.value=.8f;check(xtc_project_set_automation(p,&a));
|
|
std::string project=root+"/composition.xtc";check(xtc_project_save(p,project.c_str()));xtc_project_destroy(p);
|
|
MainWindow window(false,"");main_window=&window;window.resize(0,0,1400,900);window.show();check(window.open_project(project.c_str()));pump();
|
|
auto *s=first<SequencerWidget>(window);if(!s)throw std::runtime_error("Sequencer missing");
|
|
auto reset=[&](){window.reset_layout();check(window.open_project(project.c_str()));window.resize(0,20,1400,900);tab("Sequencer");s->select(0,0);s->position(0);check(window.set_loop(3840,7680,true));window.fit_song();pump();};
|
|
if(std::getenv("XTC_CAPTURE_MIDI_INPUT_ONLY")) {
|
|
reset();
|
|
if(window.midi_devices(true).empty())throw std::runtime_error("No real MIDI input sources exposed by the host");
|
|
Fl::add_timeout(.3,[](void*){
|
|
auto *preferences=Fl::modal();if(!preferences)throw std::runtime_error("Preferences missing");
|
|
auto *tabs=first<Fl_Tabs>(*preferences);auto *page=named(*preferences,"midi");
|
|
if(!tabs || !page)throw std::runtime_error("MIDI preferences missing");
|
|
tabs->value(page);tabs->redraw();pump();
|
|
auto *choice=dynamic_cast<Fl_Input_Choice *>(named(*preferences,"Default MIDI input source (next Play)"));
|
|
if(!choice || choice->menubutton()->size()<3)throw std::runtime_error("MIDI source list is empty");
|
|
Fl::add_timeout(.3,[](void*){
|
|
capture("preferences-midi-input-devices");
|
|
if(system("xdotool key Escape"))std::abort();
|
|
});
|
|
choice->menubutton()->popup();
|
|
auto *close=named(*preferences,"Close");if(!close)throw std::runtime_error("Preferences close missing");close->do_callback();
|
|
});
|
|
window.show_preferences();window.do_callback();
|
|
std::puts("Real host MIDI input device dropdown captured.");return 0;
|
|
}
|
|
if(std::getenv("XTC_CAPTURE_MIDI_ONLY")) {
|
|
reset();dialog("preferences-midi",[&]{window.show_preferences();},"midi");
|
|
dialog("preferences-midi-help",[&]{window.show_preferences();},"MIDI help");
|
|
auto *bar=first<Fl_Menu_Bar>(window);auto *file=bar->find_item("File");if(!file)throw std::runtime_error("File menu missing");
|
|
Fl::add_timeout(.3,[](void*){capture("midi-file-menu");if(system("xdotool key Escape"))std::abort();});
|
|
(file+1)->pulldown(0,bar->y()+bar->h(),0,0,nullptr,bar);
|
|
std::string midi=root+"/exported-keys.mid";check(window.export_midi_clip(midi.c_str()));
|
|
check(window.new_project());check(window.import_midi_file(midi.c_str()));window.fit_song();capture("midi-import-arrangement");
|
|
piano_view();capture("midi-import-piano-roll");
|
|
window.do_callback();std::puts("MIDI workflow captures completed.");return 0;
|
|
}
|
|
if(std::getenv("XTC_CAPTURE_CREATIVE_ONLY")) {
|
|
reset();check(window.open_track_inserts(0));auto *inserts=window.track_inserts();
|
|
check(inserts->apply_template(1));tab("Track inserts");capture("creative-dub-rack");
|
|
Fl::add_timeout(.3,[](void*){capture("creative-rack-actions");if(system("xdotool key Escape"))std::abort();});
|
|
auto *actions=dynamic_cast<Fl_Menu_Button *>(named(window,"Rack actions"));if(!actions)throw std::runtime_error("Missing rack actions");
|
|
actions->menu()->pulldown(actions->x(),actions->y(),actions->w(),actions->h(),nullptr,actions);
|
|
reset();dialog("creative-strum-dialog",[&]{s->strum_dialog();});
|
|
check(s->make_chord({4,7}));check(s->strum_chords(30,false));piano_view();capture("creative-strum-result");
|
|
window.do_callback();std::puts("Creative workflow captures completed.");return 0;
|
|
}
|
|
if(const char *sdk=std::getenv("XTC_CAPTURE_STEINBERG_BUNDLE")) {
|
|
const char *juce=std::getenv("XTC_CAPTURE_JUCE_BUNDLE");
|
|
if(!juce || !std::filesystem::exists(sdk) || !std::filesystem::exists(juce))
|
|
throw std::runtime_error("Supply existing Steinberg and JUCE bundles for SDK captures");
|
|
reset();
|
|
check(window.add_track_effect(0,sdk,0));
|
|
auto *inserts=window.track_inserts();inserts->select_track(0);tab("Track inserts");
|
|
capture("steinberg-sdk-track-insert");
|
|
dialog("steinberg-sdk-identity",[&]{check(inserts->inspect_slot());});
|
|
dialog("steinberg-sdk-instrument",[&]{unsigned int index=0;xtc_choose_plugin(sdk,48000,index,true);});
|
|
dialog("juce-vst3-instrument",[&]{unsigned int index=0;xtc_choose_plugin(juce,48000,index,true);});
|
|
window.do_callback();std::puts("Real Steinberg/JUCE SDK host captures completed.");return 0;
|
|
}
|
|
if(std::getenv("XTC_CAPTURE_HOST_VST3")) {
|
|
reset();capture("compact-workspace");
|
|
auto *create=dynamic_cast<Fl_Menu_Button *>(named(window,"Create"));if(!create)throw std::runtime_error("Missing Create dropdown");
|
|
Fl::add_timeout(.3,[](void*){capture("create-track-menu");if(system("xdotool key Escape"))std::abort();});
|
|
create->menu()->pulldown(create->x(),create->y(),create->w(),create->h(),nullptr,create);
|
|
const char *effects[]={"ZamEQ2","ZamAutoSat","ZamDelay"};
|
|
for(const char *effect:effects){std::string binary=std::string("/usr/lib/vst3/")+effect+".vst3/Contents/x86_64-linux/"+effect+".so";check(window.add_track_effect(0,binary.c_str(),0));}
|
|
auto *inserts=window.track_inserts();inserts->select_track(0);tab("Track inserts");capture("track-inserts-vst3");
|
|
dialog("track-insert-vst3-identity",[&]{check(inserts->inspect_slot());});
|
|
check(inserts->set_mix(.65,1));capture("track-inserts-parallel");
|
|
window.do_callback();std::puts("Installed host VST3 captures completed.");return 0;
|
|
}
|
|
if(std::getenv("XTC_CAPTURE_SAMPLE_ONLY")) {
|
|
reset();char source[4096];if(!realpath("examples/projects/original-pluck.wav",source))throw std::runtime_error("Missing sample WAV");
|
|
check(window.create_track(false));check(window.import_audio(source));
|
|
dialog("sample-editor",[&]{check(window.open_sample_editor());});dialog("sample-transients",[&]{check(window.open_sample_editor());});
|
|
window.do_callback();std::puts("Sample-editor captures completed.");return 0;
|
|
}
|
|
reset();capture("overview");capture("sequencer-loop");capture("arrangement-markers");
|
|
s->position(4800);check(window.loop_section());capture("section-navigation");
|
|
dialog("marker-edit",[&]{s->marker_dialog();});
|
|
dialog("ripple-time",[&]{s->ripple_dialog(false);});
|
|
s->position(7680);check(s->ripple_time(1920,false));capture("ripple-result");reset();
|
|
check(s->duplicate_track("Keys layered"));capture("track-duplication");reset();
|
|
s->position(1920);check(s->split_selection());capture("midi-split");
|
|
s->select_all_clips(); // Consolidation requires one track; reset to just the two halves.
|
|
s->select(0,0);size_t tracks=0,clips=0;xtc_project_get_counts(s->project(),&tracks,&clips);
|
|
for(size_t i=0;i<clips;++i){xtc_clip_info c{};xtc_project_get_clip(s->project(),i,&c);if(c.track_id==0&&c.start_tick==1920)s->toggle_clip(c.id);}
|
|
check(s->merge_midi());capture("midi-consolidation");reset();
|
|
dialog("midi-humanize",[&]{s->humanize_dialog();});dialog("midi-chord-builder",[&]{s->chord_dialog();});dialog("midi-arpeggio",[&]{s->arpeggio_dialog();});
|
|
check(s->make_chord({4,7}));piano_view();capture("midi-chords-result");check(s->arpeggiate(240,70,false));piano_view();capture("midi-arpeggio-result");reset();
|
|
piano_view();capture("piano-roll-velocity");window.detach_piano(false);reset();
|
|
dialog("commands",[&]{window.show_commands();});
|
|
tab("Mixer");capture("mixer-automation");button("Play");pump();capture("mixer-playing");button("Stop");
|
|
window.resize(0,0,1220,720);pump();capture("mixer-minimum-size");window.resize(0,0,1400,900);
|
|
for(int i=0;i<12;++i){check(window.create_track(false));}
|
|
tab("Mixer");auto *scroll=first<Fl_Scroll>(*window.mixer());if(scroll)scroll->scroll_to(std::max(0,static_cast<int>(window.mixer()->strip_count())*156-scroll->w()),0);pump();capture("mixer-scrolled-session");reset();
|
|
tab("Modular Graph");capture("modular-graph");reset();
|
|
window.detach_transport(true);pump();capture("transport-detached");window.detach_mixer(true);pump();capture("workspace-detached");capture("mixer-detached");reset();
|
|
window.detach_editor(0,true);pump();capture("sequencer-detached");reset();tab("Modular Graph");window.detach_editor(1,true);pump();capture("graph-detached");reset();
|
|
char wave[4096];if(!realpath("examples/projects/original-pluck.wav",wave))throw std::runtime_error("Missing WAV");check(window.create_track(false));check(window.import_audio(wave));check(s->set_zoom(192));check(s->audio_properties(.8f,240,480));pump();capture("sequencer-audio-waveform");
|
|
dialog("sample-editor",[&]{check(window.open_sample_editor());});dialog("sample-transients",[&]{check(window.open_sample_editor());});
|
|
dialog("audio-clip-properties",[&]{s->audio_properties_dialog();});dialog("audio-normalization",[&]{s->normalize_dialog();});check(s->normalize_audio(.25f));capture("audio-normalization-result");
|
|
dialog("project-collection",[&]{menu("File/Collect project media...");});check(window.collect_project((root+"/portable").c_str()));
|
|
dialog("loop-range",[&]{button("Loop range...");});dialog("midi-controller-mapping",[&]{menu("Audio/MIDI controller mapping");});dialog("about",[&]{menu("Help/About");});
|
|
for(const char *section:{"audio","plugins","midi","interface","editing","project_defaults","transport"}){std::string file="preferences-"+std::string(section);for(char &c:file)if(c==' ')c='_';dialog(file.c_str(),[&]{window.show_preferences();},section);}
|
|
xtc_settings settings=window.settings();std::string fixtures=std::filesystem::canonical(argv[0]).parent_path().string();snprintf(settings.ladspa_paths,sizeof(settings.ladspa_paths),"%s",fixtures.c_str());settings.clap_paths[0]=settings.vst2_paths[0]=settings.vst3_paths[0]=0;
|
|
dialog("plugin-library",[&]{std::string path;xtc_choose_plugin_file(settings,path);});
|
|
for(auto entry:{std::pair<const char *,const char *>{"plugin-descriptors","plugin-fixture.so"},{"clap-instrument-descriptors","clap-synth-fixture.clap"},{"vst2-instrument-descriptors","vst2-synth-fixture.so"},{"vst3-instrument-descriptors","vst3-synth-fixture.so"}}){unsigned int index=0;dialog(entry.first,[&]{xtc_choose_plugin((fixtures+"/"+entry.second).c_str(),48000,index,strcmp(entry.first,"plugin-descriptors")!=0);});}
|
|
for(auto entry:{std::pair<const char *,unsigned int>{"internal-delay",2},{"internal-reverb",3},{"plugin-controls",2}}){xtc_plugin_spec spec{},edited{};xtc_plugin_details details{};if(!strcmp(entry.first,"plugin-controls"))check(xtc_plugin_probe((fixtures+"/plugin-fixture.so").c_str(),0,48000,&spec));else check(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,entry.second,48000,&spec));check(xtc_plugin_describe(&spec,48000,&details));dialog(entry.first,[&]{xtc_edit_plugin(spec,details,edited);});}
|
|
reset();auto *choice=s->instrument_choice(0);if(!choice)throw std::runtime_error("Instrument menu missing");Fl::add_timeout(.3,[](void*){capture("internal-instruments");if(system("xdotool key Escape"))std::abort();});choice->menu()->pulldown(choice->x(),choice->y(),choice->w(),choice->h(),choice->mvalue(),choice);
|
|
window.do_callback();std::puts("All documentation captures completed.");return 0;
|
|
} catch(const std::exception &e){std::fprintf(stderr,"Screenshot capture failed: %s\n",e.what());return 1;}
|
|
}
|