feat(mixer): control insert wet mix and chain output gain (phase 88)

This commit is contained in:
Johannes Findeisen 2026-11-06 01:41:23 +01:00
commit a3858b1c82
6 changed files with 49 additions and 11 deletions

View file

@ -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.
---

View file

@ -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.

View file

@ -9,13 +9,16 @@
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Value_Input.H>
#include <cstring>
#include <algorithm>
#include <cmath>
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<TrackInsertsPanel *>(c)->preset(true);},this);
presets->add("Load chain...",0,[](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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<TrackInsertsPanel *>(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(index<chain.count){wet_->activate();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<chain.count)chain.slots[index].wet=static_cast<float>(wet);
chain.output_gain=static_cast<float>(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<int>(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<size_t>(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

View file

@ -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;i<project->track_count;++i)for(j=0;j<project->chains[i].count;++j)project->chains[i].slots[j].plugin.tempo=tempo;}
{size_t i;for(i=0;i<project->track_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;j<c->count;++j)p->chains[i].slots[j].plugin.tempo=p->tempo;}
return XTC_OK;
}

View file

@ -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);

View file

@ -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,&note)==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);