From a3858b1c8284a72011ad19564f1aedd771fb9df4 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 01:41:23 +0100 Subject: [PATCH] feat(mixer): control insert wet mix and chain output gain (phase 88) --- docs/ROADMAP.md | 8 ++++---- docs/TRACK_INSERTS.md | 7 +++++++ src/gui/track_inserts.hpp | 27 ++++++++++++++++++++++----- src/project/project.c | 5 ++++- tests/integration/test_gui.cpp | 5 +++++ tests/unit/test_track_fx.c | 8 +++++++- 6 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 83028be..b9fcfcb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2000,12 +2000,12 @@ Implemented: atomic chain preset save/load reuses the versioned project containe Expose per-slot wet/dry and chain output gain with finite bounded values and render tests for endpoints and ordering. ### 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: selected insert wet amount (0–1) and chain output gain (0–4) controls; parallel dry/wet gain branches and pre-fader output scaling. Track-chain tempo follows project tempo during assignment and changes. Exact dry/wet endpoint and fractional impulse tests, full core/reliability suite and GUI mix/invalid-value/tempo/preset tests passed. Mono mix controls use linear amplitude blending. --- diff --git a/docs/TRACK_INSERTS.md b/docs/TRACK_INSERTS.md index 3889c07..6094dda 100644 --- a/docs/TRACK_INSERTS.md +++ b/docs/TRACK_INSERTS.md @@ -58,3 +58,10 @@ applying it verifies all enabled plugins before replacing the current chain. Preset files reference absolute installed binary paths, so another machine must provide compatible binaries at those paths. No plugin binaries or opaque state chunks are embedded. + +**Insert wet** blends the selected effect with its input (0 dry, 1 fully wet). +**Chain gain** scales the complete chain before the track fader (0–4 linear). +Edits are prepared and undoable, with finite range validation. Internal sync +parameters follow project tempo. This is mono amplitude blending; stereo track +insert layouts, latency compensation and gapless live insert edits are future +work. Editing stops transport before preparing the replacement plan. diff --git a/src/gui/track_inserts.hpp b/src/gui/track_inserts.hpp index b5b7824..34cdd44 100644 --- a/src/gui/track_inserts.hpp +++ b/src/gui/track_inserts.hpp @@ -9,13 +9,16 @@ #include #include #include +#include #include #include +#include class TrackInsertsPanel : public Fl_Group { MainWindow *owner_; Fl_Choice *tracks_; Fl_Hold_Browser *slots_; Fl_Box *message_; + Fl_Value_Input *wet_,*gain_; GraphWidget *graph_; xtc_graph *model_=nullptr; uint64_t track_=UINT64_MAX; @@ -40,9 +43,12 @@ class TrackInsertsPanel : public Fl_Group { auto *presets=new Fl_Menu_Button(x+978,y+12,68,28,"Preset"); presets->add("Save chain...",0,[](Fl_Widget *,void *c){static_cast(c)->preset(true);},this); presets->add("Load chain...",0,[](Fl_Widget *,void *c){static_cast(c)->preset(false);},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_); + wet_=new Fl_Value_Input(x+110,y+52,100,26,"Insert wet");wet_->bounds(0,1);wet_->step(.01);wet_->when(FL_WHEN_RELEASE); + gain_=new Fl_Value_Input(x+360,y+52,100,26,"Chain gain");gain_->bounds(0,4);gain_->step(.01);gain_->when(FL_WHEN_RELEASE); + for(auto *input:{wet_,gain_})input->callback([](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this); + message_=new Fl_Box(x+16,y+84,w-32,30,"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+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast(c)->update_mix();},this); + graph_=new GraphWidget(x+260,y+126,w-276,h-142,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_); } @@ -51,6 +57,17 @@ class TrackInsertsPanel : public Fl_Group { 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 update_mix() { + xtc_track_chain chain{};if(xtc_project_get_chain(owner_->project_.get(),track_,&chain)!=XTC_OK){wet_->deactivate();gain_->deactivate();return;} + gain_->activate();gain_->value(chain.output_gain);size_t index=selected_slot(); + if(indexactivate();wet_->value(chain.slots[index].wet);}else wet_->deactivate(); + } + xtc_result set_mix(double wet,double gain) { + if(!std::isfinite(wet) || wet<0 || wet>1 || !std::isfinite(gain) || gain<0 || gain>4)return XTC_ERR_INVALID_ARGUMENT; + xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);if(r!=XTC_OK)return r; + size_t index=selected_slot();if(index(wet); + chain.output_gain=static_cast(gain);return owner_->set_track_chain(track_,chain); + } xtc_result save_preset(const char *file) { xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain); return r==XTC_OK ? xtc_track_chain_preset_save(&chain,file) : r; @@ -89,7 +106,7 @@ class TrackInsertsPanel : public Fl_Group { else return XTC_ERR_INVALID_ARGUMENT; r=owner_->set_track_chain(track_,chain); if(r==XTC_OK && chain.count)slots_->select(static_cast(std::min(selected,chain.count-1)+1)); - return r; + update_mix();return r; } 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); @@ -107,7 +124,7 @@ class TrackInsertsPanel : public Fl_Group { 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(); + update_mix();redraw(); } }; #endif diff --git a/src/project/project.c b/src/project/project.c index b0198cb..990f8e5 100644 --- a/src/project/project.c +++ b/src/project/project.c @@ -471,6 +471,7 @@ xtc_result xtc_project_set_tempo(xtc_project *project, unsigned int tempo) if (project == NULL || tempo < 20000U || tempo > 300000U) return XTC_ERR_INVALID_ARGUMENT; project->tempo = tempo; + {size_t i,j;for(i=0;itrack_count;++i)for(j=0;jchains[i].count;++j)project->chains[i].slots[j].plugin.tempo=tempo;} {size_t i;for(i=0;itrack_count;++i)if(project->instruments[i].instrument)project->instruments[i].tempo=tempo;} { size_t i; for (i = 0; i < project->graph->node_count; ++i) if (project->graph->nodes[i].type == XTC_NODE_PLUGIN) project->graph->nodes[i].plugin.tempo = tempo; } @@ -806,5 +807,7 @@ xtc_result xtc_project_set_chain(xtc_project *p,uint64_t track,const xtc_track_c size_t i;if(!p || xtc_track_chain_validate(c)!=XTC_OK)return XTC_ERR_INVALID_ARGUMENT; i=track_index(p,track);if(i==SIZE_MAX)return XTC_ERR_NOT_FOUND; if(p->tracks[i].is_bus && c->count)return XTC_ERR_UNSUPPORTED; - p->chains[i]=*c;return XTC_OK; + p->chains[i]=*c; + {size_t j;for(j=0;jcount;++j)p->chains[i].slots[j].plugin.tempo=p->tempo;} + return XTC_OK; } diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 871c0f2..68c2ca1 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -249,6 +249,11 @@ int track_insert_tests() 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); diff --git a/tests/unit/test_track_fx.c b/tests/unit/test_track_fx.c index 71b3611..a5d4663 100644 --- a/tests/unit/test_track_fx.c +++ b/tests/unit/test_track_fx.c @@ -30,13 +30,19 @@ static void rendering(void) c.slots[0].enabled=0;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_audio_plan_create(p,8000,&audio)==XTC_OK); assert(xtc_audio_plan_set_fade_frames(audio,0)==XTC_OK && xtc_audio_plan_render(audio,0,out,32)==XTC_OK && out[0]==1 && out[8]==0); xtc_audio_plan_destroy(audio);audio=NULL; + c.slots[0].enabled=1;c.slots[0].wet=.25f; + assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_audio_plan_create(p,8000,&audio)==XTC_OK); + assert(xtc_audio_plan_set_fade_frames(audio,0)==XTC_OK && xtc_audio_plan_render(audio,0,out,32)==XTC_OK); + assert(fabsf(out[0]-.75f)<.00001f && fabsf(out[8]-.25f)<.00001f);xtc_audio_plan_destroy(audio);audio=NULL; + c.slots[0].wet=0;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_audio_plan_create(p,8000,&audio)==XTC_OK); + assert(xtc_audio_plan_set_fade_frames(audio,0)==XTC_OK && xtc_audio_plan_render(audio,0,out,32)==XTC_OK && out[0]==1 && out[8]==0);xtc_audio_plan_destroy(audio);audio=NULL; /* Reject split ownership of a nonlinear chain on a hybrid track. */ assert(xtc_project_add_midi_clip(p,track,0,960,&clip)==XTC_OK); assert(xtc_audio_plan_create(p,8000,&audio)==XTC_ERR_UNSUPPORTED && audio==NULL); xtc_project_destroy(p);p=NULL; assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"MIDI",&track)==XTC_OK && xtc_project_add_midi_clip(p,track,0,960,&clip)==XTC_OK); assert(xtc_project_add_note(p,clip,0,960,69,127,0,¬e)==XTC_OK); - c.slots[0].enabled=1;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_midi_plan_create(p,8000,&midi)==XTC_OK); + c.slots[0].enabled=1;c.slots[0].wet=1;assert(xtc_project_set_chain(p,track,&c)==XTC_OK && xtc_midi_plan_create(p,8000,&midi)==XTC_OK); assert(xtc_midi_plan_set_fade_frames(midi,0)==XTC_OK && xtc_midi_plan_render(midi,0,NULL,0,out,32)==XTC_OK); for(size_t j=0;j<32;++j)assert(fabsf(out[j]-(j<8 ? 0.f : .05f*sinf(6.28318530718f*440.f*(float)(j-8)/8000.f)))<.00001f); xtc_midi_plan_destroy(midi);xtc_project_destroy(p);unlink(wave);