diff --git a/Makefile b/Makefile index d7578f3..7bf6b6b 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/docs/CREATIVE_WORKFLOWS.md b/docs/CREATIVE_WORKFLOWS.md index 331b1d1..40fd9be 100644 --- a/docs/CREATIVE_WORKFLOWS.md +++ b/docs/CREATIVE_WORKFLOWS.md @@ -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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index f5ccb10..ce9c653 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/include/xtc/xtc_plugin.h b/include/xtc/xtc_plugin.h index b306014..644ae09 100644 --- a/include/xtc/xtc_plugin.h +++ b/include/xtc/xtc_plugin.h @@ -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 diff --git a/src/gui/track_inserts.hpp b/src/gui/track_inserts.hpp index b1c0b47..a1f8a6f 100644 --- a/src/gui/track_inserts.hpp +++ b/src/gui/track_inserts.hpp @@ -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(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(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(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(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(); diff --git a/src/project/chain_tools.c b/src/project/chain_tools.c new file mode 100644 index 0000000..8eddec9 --- /dev/null +++ b/src/project/chain_tools.c @@ -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; +} diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 4f1f28e..1ced5fc 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -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() diff --git a/tests/unit/test_creative.c b/tests/unit/test_creative.c new file mode 100644 index 0000000..0f00f7c --- /dev/null +++ b/tests/unit/test_creative.c @@ -0,0 +1,19 @@ +#include "xtc/xtc_plugin.h" +#include +#include +#include +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