diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2dd9892..058fe16 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1925,12 +1925,12 @@ Implemented: generated serial graphs process summed pre-fader audio or MIDI per Offer a selected-track insert view alongside the global graph, with clear signal order and transactional Add effect actions. ### Tasks and acceptance criteria -- [ ] Implement the described workflow with validated errors and safe ownership. -- [ ] Pass relevant core and/or GUI tests. -- [ ] Document behavior and limitations; commit this phase separately. +- [x] Implement the described workflow with validated errors and safe ownership. +- [x] Pass relevant core and/or GUI tests. +- [x] Document behavior and limitations; commit this phase separately. ### Implementation status -Planned. +Implemented: Ctrl+4 opens the track insert rack and actual generated modular graph, alongside the global graph. Track selection and Add effect provide internal devices and the existing four-format plugin library; candidate preparation and undo are transactional. Warning-enabled GUI build and focused adoption/undo/failed preparation/missing module/save-reload tests passed. Generated wiring is read-only. --- diff --git a/docs/TRACK_INSERTS.md b/docs/TRACK_INSERTS.md index 8bdc4b6..fa9124c 100644 --- a/docs/TRACK_INSERTS.md +++ b/docs/TRACK_INSERTS.md @@ -27,3 +27,10 @@ allocation or I/O. Mono effect adapters require zero reported latency. External plugin state on seeks follows the existing host contract. Bus inserts and tracks containing both audio assets and MIDI reject preparation explicitly. Live unassigned sine monitoring remains a separate master input. + +Open **View → Track inserts** (Ctrl+4) and choose a track. **Add effect** offers +internal Delay/Reverb and the plugin library. The left rack shows signal order; +the graph displays the actual generated graph compiled by playback. Wiring in +this view is read-only: edit the rack to preserve the serial chain. The global +Modular Graph remains independent. A failed plugin probe or playback preparation +preserves the current project; successful additions are undoable. diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index face465..0d83e4c 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -12,6 +12,7 @@ #include "command_palette.hpp" #include #include "plugin_library.hpp" +#include "track_inserts.hpp" #include #include #include @@ -496,8 +497,10 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) dock->callback([](Fl_Widget *,void *c){auto *p=static_cast(c);p->owner->detach_editor(p->index,false);},&panel); panel.placeholder->end();panel.placeholder->hide();panel.page->end(); } + track_inserts_=new TrackInsertsPanel(216,158,1056,546,this); tabs_->end(); tabs_->value(sequencer); + menu->add("View/Track inserts",FL_CTRL+'4',view_callback,track_inserts_); menu->add("View/Sequencer", FL_CTRL + '1', view_callback, sequencer); menu->add("View/Mixer", FL_CTRL + '2', view_callback, mixer); menu->add("View/Modular Graph", FL_CTRL + '3', view_callback, graph); @@ -1948,6 +1951,7 @@ xtc_result MainWindow::create_track(bool midi) } void MainWindow::refresh_workspace() { + if(track_inserts_)track_inserts_->refresh(); if (loop_button_) { loop_button_->value(settings_.loop_enabled != 0); if (recorder_) @@ -2704,3 +2708,23 @@ void MainWindow::follow_playhead() if (screen > timeline_scroll_->x() + timeline_scroll_->w() - 48 || screen < timeline_scroll_->x() + sequencer_->header_width()) timeline_scroll_->scroll_to(std::max(0, pixel - 160), timeline_scroll_->yposition()); } + +xtc_result MainWindow::set_track_chain(uint64_t track,const xtc_track_chain &chain) +{ + auto r=before_arrangement_edit(this);if(r!=XTC_OK)return r; + xtc_project *candidate=nullptr;r=xtc_project_clone(project_.get(),&candidate); + if(r==XTC_OK)r=xtc_project_set_chain(candidate,track,&chain); + if(r==XTC_OK)r=install_project(candidate,false); + if(r!=XTC_OK){xtc_project_destroy(candidate);return r;} + mark_dirty();track_inserts_->refresh(true);return XTC_OK; +} +xtc_result MainWindow::add_track_effect(uint64_t track,const char *file,unsigned int index) +{ + xtc_track_chain chain{};auto r=xtc_project_get_chain(project_.get(),track,&chain); + if(r!=XTC_OK)return r; + if(chain.count==XTC_TRACK_INSERTS)return XTC_ERR_CAPACITY; + xtc_track_insert slot{};r=xtc_plugin_probe(file,index,sample_rate(),&slot.plugin); + if(r!=XTC_OK)return r; + slot.enabled=1;slot.wet=1;chain.slots[chain.count++]=slot; + return set_track_chain(track,chain); +} diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index b0ac464..5f1815c 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -29,8 +29,12 @@ class Fl_Value_Input; class Fl_Scroll; class Fl_Check_Button; +class TrackInsertsPanel; class MainWindow : public Fl_Double_Window { public: + xtc_result set_track_chain(uint64_t track,const xtc_track_chain &chain); + xtc_result add_track_effect(uint64_t track,const char *file,unsigned int index); + TrackInsertsPanel *track_inserts() const {return track_inserts_;} void show_track_inspector(bool visible); /* One undoable candidate edit. MIDI also adds a one-bar clip at the cursor. * Preparation stops/resets transport; recording and failed candidates retain @@ -218,6 +222,8 @@ class MainWindow : public Fl_Double_Window { xtc_result return_to_start(); private: + friend class TrackInsertsPanel; + TrackInsertsPanel *track_inserts_=nullptr; using MixSnapshot=std::unique_ptr; MixSnapshot mix_snapshots_[2]{{nullptr,xtc_mix_snapshot_destroy},{nullptr,xtc_mix_snapshot_destroy}}; struct EditorPanel { diff --git a/src/gui/track_inserts.hpp b/src/gui/track_inserts.hpp new file mode 100644 index 0000000..f1ce764 --- /dev/null +++ b/src/gui/track_inserts.hpp @@ -0,0 +1,62 @@ +#ifndef XTC_TRACK_INSERTS_HPP +#define XTC_TRACK_INSERTS_HPP +#include "main_window.hpp" +#include "graph_widget.hpp" +#include "plugin_library.hpp" +#include "plugin_dialog.hpp" +#include "sequencer_widget.hpp" +#include +#include +#include +#include +#include +#include +class TrackInsertsPanel : public Fl_Group { + MainWindow *owner_; + Fl_Choice *tracks_; + Fl_Hold_Browser *slots_; + Fl_Box *message_; + GraphWidget *graph_; + xtc_graph *model_=nullptr; + uint64_t track_=UINT64_MAX; + xtc_track_chain cached_{}; + std::vector ids_; + void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); } + public: + TrackInsertsPanel(int x,int y,int w,int h,MainWindow *owner):Fl_Group(x,y,w,h,"Track inserts"),owner_(owner) { + tracks_=new Fl_Choice(x+64,y+12,240,28,"Track");tracks_->callback([](Fl_Widget *,void *c){auto *p=static_cast(c);int i=p->tracks_->value();if(i>=0 && static_cast(i)ids_.size()){p->track_=p->ids_[static_cast(i)];p->refresh(true);}},this); + auto *add=new Fl_Menu_Button(x+320,y+12,150,28,"Add effect"); + add->add("Internal Delay",0,[](Fl_Widget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,2);},this); + add->add("Internal Reverb",0,[](Fl_Widget *,void *c){static_cast(c)->add(XTC_BUILTIN_PATH,3);},this); + add->add("Plugin library...",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);std::string file;if(!xtc_choose_plugin_file(p->owner_->settings(),file))return;unsigned int index=0;if(xtc_choose_plugin(file.c_str(),p->owner_->sample_rate(),index))p->add(file.c_str(),index);},this); + message_=new Fl_Box(x+16,y+50,w-32,32,"Track input > inserts > output gain > track fader > bus/master");message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); + slots_=new Fl_Hold_Browser(x+16,y+94,230,h-110);slots_->format_char(0); + graph_=new GraphWidget(x+260,y+94,w-276,h-110,nullptr,[](void *,const xtc_graph_edit &,size_t *){return XTC_ERR_UNSUPPORTED;},nullptr,message_); + resizable(graph_);end(); + } + ~TrackInsertsPanel() override { graph_->set_graph(nullptr);xtc_graph_destroy(model_); } + uint64_t selected_track() const {return track_;} + size_t selected_slot() const {return slots_->value()>0 ? static_cast(slots_->value()-1) : SIZE_MAX;} + void select_track(uint64_t id) {track_=id;refresh(true);} + const xtc_graph *graph() const {return model_;} + void add(const char *file,unsigned int index) { result(owner_->add_track_effect(track_,file,index)); } + void refresh(bool force=false) { + const xtc_project *project=owner_->project_.get();size_t count=0,clips=0;xtc_project_get_counts(project,&count,&clips); + std::vector ids;std::vector names; + for(size_t i=0;iclear();for(const auto &name:names) {tracks_->add("track");tracks_->replace(tracks_->size()-2,name.c_str());}} + if(std::find(ids_.begin(),ids_.end(),track_)==ids_.end())track_=ids_.empty() ? UINT64_MAX : ids_[0]; + for(size_t i=0;ivalue(static_cast(i)); + xtc_track_chain chain{};chain.output_gain=1; + if(track_!=UINT64_MAX)xtc_project_get_chain(project,track_,&chain); + if(!rebuild && !std::memcmp(&cached_,&chain,sizeof(chain)))return; + cached_=chain;int selected=slots_->value();slots_->clear(); + for(size_t i=0;iadd(label.c_str());} + if(chain.count)slots_->select(selected>0 && static_cast(selected)<=chain.count ? selected : 1); + xtc_graph *next=nullptr;auto r=xtc_track_chain_graph(&chain,&next); + if(r==XTC_OK){graph_->set_graph(next);xtc_graph_destroy(model_);model_=next;graph_->auto_layout();} + redraw(); + } +}; +#endif diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index d2d6f5b..221c802 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -9,6 +9,7 @@ #include "../../src/gui/command_palette.hpp" #include #include "../../src/gui/plugin_library.hpp" +#include "../../src/gui/track_inserts.hpp" #include #include #include @@ -215,8 +216,25 @@ static int sample_editor_tests() 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 track_insert_tests() +{ + 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.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 main(int argc, char **argv) { + 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();