feat(inserts): duplicate effect slots with independent state (phase 91)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:03:36 +01:00
commit 062b33f55f
8 changed files with 69 additions and 5 deletions

View file

@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
LDFLAGS ?=
CORE_SOURCES := src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
CORE_SOURCES := src/project/chain_tools.c src/project/chain_preset.c src/mixer/track_fx.c src/audio/sample.c src/persistence/collect.c src/audio/normalize.c src/project/sections.c src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
ifeq ($(JACK_AVAILABLE),yes)
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
@ -586,3 +586,13 @@ test: test-track-fx
.PHONY: test-gui-track-fx
test-gui-track-fx: $(BUILD_DIR)/test_gui $(BUILD_DIR)/plugin-fixture.so $(BUILD_DIR)/clap-fixture.clap $(BUILD_DIR)/vst2-fixture.so $(BUILD_DIR)/vst3-fixture.so
XTC_TEST_TRACK_FX_ONLY=1 XTC_TEST_PLUGIN="$(abspath $(BUILD_DIR))/plugin-fixture.so" XTC_TEST_CLAP="$(abspath $(BUILD_DIR))/clap-fixture.clap" XTC_TEST_VST2="$(abspath $(BUILD_DIR))/vst2-fixture.so" XTC_TEST_VST3="$(abspath $(BUILD_DIR))/vst3-fixture.so" xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui
$(BUILD_DIR)/test_creative: $(BUILD_DIR)/tests/unit/test_creative.o $(BUILD_DIR)/libxtc.a
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
.PHONY: test-creative test-gui-creative
test-creative: $(BUILD_DIR)/test_creative
./$(BUILD_DIR)/test_creative
test: test-creative
test-gui-creative: $(BUILD_DIR)/test_gui
XTC_TEST_CREATIVE_ONLY=1 xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui
-include $(BUILD_DIR)/tests/unit/test_creative.d

View file

@ -10,3 +10,10 @@ or project-format change. Each phase is qualified separately in ROADMAP.md.
mixer selection directly. Bus inserts are unsupported; no selection leaves the
workspace unchanged. **View → Track inserts** still opens the rack independently.
Ctrl+4 remains the piano-roll command, resolving the earlier shortcut collision.
## Duplicate an effect
Select a slot, then **Rack actions → Duplicate effect**. The copy appears directly
after it, retaining controls, automation, wet amount and bypass. It is an
independent instance with fresh processing history. Eight-slot capacity and
plugin preparation errors preserve the old chain; Undo removes the copy.

View file

@ -2073,12 +2073,12 @@ Implemented: selected sequencer/mixer track opens directly in the insert workspa
Duplicate an effect adjacent to its slot with copied parameters, automation, wet amount and bypass; prepare transactionally, enforce the eight-slot limit and support undo.
### Tasks and acceptance criteria
- [ ] Implement the workflow with bounded validation, safe preparation and documented limits.
- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior.
- [ ] Document results and commit this phase separately.
- [x] Implement the workflow with bounded validation, safe preparation and documented limits.
- [x] Pass meaningful core and/or GUI checks, including error and undo behavior.
- [x] Document results and commit this phase separately.
### Implementation status
Planned.
Implemented: bounded C99 metadata duplication and Rack actions/Duplicate effect create an adjacent independent slot, preserving parameters, automation, wet/bypass and gain. Warning-enabled core checks cover aliasing, independent edits and capacity rollback; GUI duplication, selection and undo checks passed.
---

View file

@ -36,6 +36,9 @@ typedef struct {
#define XTC_TRACK_INSERTS 8U
typedef struct { xtc_plugin_spec plugin; unsigned int enabled; float wet; } xtc_track_insert;
typedef struct { size_t count; float output_gain; xtc_track_insert slots[XTC_TRACK_INSERTS]; } xtc_track_chain;
/* Copies the selected slot immediately after itself, preserving all metadata.
* Input/output may alias; errors preserve output. */
xtc_result xtc_track_chain_duplicate(const xtc_track_chain *chain,size_t index,xtc_track_chain *out);
xtc_result xtc_track_chain_preset_save(const xtc_track_chain *chain,const char *path);
xtc_result xtc_track_chain_preset_load(const char *path,xtc_track_chain *out);
/* Metadata only; control thread. Empty chain has unity output. No module is

View file

@ -17,6 +17,7 @@
class TrackInsertsPanel : public Fl_Group {
MainWindow *owner_;
Fl_Choice *tracks_;
Fl_Menu_Button *actions_;
Fl_Hold_Browser *slots_;
Fl_Box *message_;
Fl_Value_Input *wet_,*gain_;
@ -49,6 +50,8 @@ class TrackInsertsPanel : public Fl_Group {
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);
actions_=new Fl_Menu_Button(x+486,y+52,150,26,"Rack actions");
actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->duplicate_slot());},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);
scroll_=new Fl_Scroll(x+260,y+126,w-276,h-142);
@ -61,6 +64,13 @@ 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)); }
xtc_result duplicate_slot() {
xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot();
if(r==XTC_OK)r=xtc_track_chain_duplicate(&chain,index,&chain);
if(r==XTC_OK)r=owner_->set_track_chain(track_,chain);
if(r==XTC_OK){slots_->select(static_cast<int>(index+2));update_mix();}
return r;
}
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();

11
src/project/chain_tools.c Normal file
View file

@ -0,0 +1,11 @@
#include "xtc/xtc_plugin.h"
/* Metadata transforms never load modules; aliases are safe and errors atomic. */
xtc_result xtc_track_chain_duplicate(const xtc_track_chain *chain,size_t index,xtc_track_chain *out)
{
xtc_track_chain copy;size_t i;
if(!out || xtc_track_chain_validate(chain)!=XTC_OK || index>=chain->count)return XTC_ERR_INVALID_ARGUMENT;
if(chain->count==XTC_TRACK_INSERTS)return XTC_ERR_CAPACITY;
copy=*chain;
for(i=copy.count;i>index+1;--i)copy.slots[i]=copy.slots[i-1];
copy.slots[index+1]=copy.slots[index];++copy.count;*out=copy;return XTC_OK;
}

View file

@ -228,6 +228,10 @@ int creative_workflow_tests()
CHECK(bar->find_item("View/Piano roll")->shortcut()==FL_CTRL+'4');
window.mixer()->select_track(first_track);bar->picked(bar->find_item("View/Mixer"));
CHECK(window.open_track_inserts()==XTC_OK && panel->selected_track()==first_track);
CHECK(window.add_track_effect(first_track,XTC_BUILTIN_PATH,2)==XTC_OK);
CHECK(panel->duplicate_slot()==XTC_OK && panel->selected_slot()==1);
xtc_track_chain chain{};CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==2);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==1);
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
}
int track_insert_tests()

View file

@ -0,0 +1,19 @@
#include "xtc/xtc_plugin.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
xtc_track_chain c={0},out={0};
c.output_gain=.8f;c.count=1;c.slots[0].enabled=1;c.slots[0].wet=.4f;
assert(xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,2,48000,&c.slots[0].plugin)==XTC_OK);
assert(xtc_track_chain_duplicate(&c,0,&out)==XTC_OK && out.count==2 && out.output_gain==.8f);
assert(!memcmp(out.slots,out.slots+1,sizeof(out.slots[0])));
out.slots[1].plugin.values[0]=10;assert(c.slots[0].plugin.values[0]!=10 && out.slots[0].plugin.values[0]!=10);
assert(xtc_track_chain_duplicate(&out,1,&out)==XTC_OK && out.count==3 && out.slots[2].plugin.values[0]==10);
c=out;assert(xtc_track_chain_duplicate(&out,3,&out)==XTC_ERR_INVALID_ARGUMENT && !memcmp(&c,&out,sizeof(c)));
for(size_t i=3;i<XTC_TRACK_INSERTS;++i)out.slots[i]=out.slots[0];
out.count=XTC_TRACK_INSERTS;c=out;
assert(xtc_track_chain_duplicate(&out,0,&out)==XTC_ERR_CAPACITY && !memcmp(&c,&out,sizeof(c)));
puts("Creative editing core checks passed.");return 0;
}