feat(presets): add creative internal effect chain templates (phase 94)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:06:27 +01:00
commit c65ac61a98
7 changed files with 62 additions and 4 deletions

View file

@ -35,3 +35,12 @@ copies metadata without affecting undo or loading modules. Recall prepares a
fresh chain and is undoable; histories and opaque plugin states are excluded.
The two comparison slots clear on successful New/Open. Comparisons stop
transport when recalled, following the existing safe insert-edit workflow.
## Creative starting points
**Add effect → Templates (replace)** offers Slapback (80 ms delay), Dub Echo
(375 ms feedback delay into a short reverb), and Ambient Space (long reverb into
500 ms delay). They replace the current chain in one undoable edit and require
no external plugins. All controls remain editable. Times are milliseconds;
feedback and wet levels are deliberate starting points, without automatic
loudness matching or a claim that the presets suit every source.

View file

@ -2118,12 +2118,12 @@ Implemented: two independent track-bound chain comparison slots, with disabled R
Offer original dependency-free Slapback, Dub Echo and Ambient Space starting points using internal devices, including safe replacement and editable controls.
### 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: Slapback, Dub Echo and Ambient Space templates use original internal Delay/Reverb configurations with editable controls and explicit chain replacement. C99 generation validates tempo/index, preserves errors and loads no external modules. All templates compile/render finite nonzero output; GUI template adoption and undo checks passed.
---

View file

@ -38,6 +38,11 @@ typedef struct { xtc_plugin_spec plugin; unsigned int enabled; float wet; } xtc_
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. */
/* Original internal-device starting points: 0 Slapback, 1 Dub Echo,
* 2 Ambient Space. Name returns NULL for an unknown index. No modules opened;
* template errors preserve out. Delay times are milliseconds, not beat sync. */
const char *xtc_track_chain_template_name(unsigned int index);
xtc_result xtc_track_chain_template(unsigned int index,unsigned int tempo,xtc_track_chain *out);
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);

View file

@ -39,6 +39,10 @@ class TrackInsertsPanel : public Fl_Group {
add->add("Internal Delay",0,[](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(c)->add(XTC_BUILTIN_PATH,2);},this);
add->add("Internal Reverb",0,[](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(c)->add(XTC_BUILTIN_PATH,3);},this);
add->add("Plugin library...",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(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);
for(unsigned int preset=0;preset<3;++preset) {
std::string label=std::string("Templates (replace)/")+xtc_track_chain_template_name(preset);
add->add(label.c_str(),0,[](Fl_Widget *w,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);auto *menu=static_cast<Fl_Menu_Button *>(w);unsigned int index=0;for(;index<3;++index)if(!strcmp(menu->mvalue()->label(),xtc_track_chain_template_name(index)))break;p->result(p->apply_template(index));},this);
}
struct Action {const char *name;int command;};
int left=x+486;
for(const auto &action:{Action{"Up",-1},Action{"Down",1},Action{"Bypass",2},Action{"Remove",3}}) {
@ -73,6 +77,11 @@ 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 apply_template(unsigned int index) {
xtc_track_chain chain{};auto r=xtc_track_chain_template(index,xtc_project_tempo(owner_->project_.get()),&chain);
if(r==XTC_OK)r=owner_->set_track_chain(track_,chain);
return r;
}
void clear_session() {clipboard_={};clipboard_ready_=false;for(auto &slot:comparisons_)slot=Comparison{};update_actions();}
bool clipboard_ready() const {return clipboard_ready_;}
bool comparison_ready(unsigned int slot) const {return slot<2 && comparisons_[slot].ready && comparisons_[slot].track==track_;}

View file

@ -9,3 +9,25 @@ xtc_result xtc_track_chain_duplicate(const xtc_track_chain *chain,size_t index,x
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;
}
const char *xtc_track_chain_template_name(unsigned int index)
{
static const char *names[]={"Slapback","Dub Echo","Ambient Space"};
return index<3 ? names[index] : NULL;
}
xtc_result xtc_track_chain_template(unsigned int index,unsigned int tempo,xtc_track_chain *out)
{
xtc_track_chain c={0};size_t i;xtc_result r=XTC_OK;
if(!out || index>=3 || tempo<20000 || tempo>300000)return XTC_ERR_INVALID_ARGUMENT;
c.output_gain=index ? .8f : 1;c.count=index ? 2 : 1;
for(i=0;r==XTC_OK && i<c.count;++i) {
unsigned int device=index==2 ? (i ? 2U : 3U) : (i ? 3U : 2U);
r=xtc_plugin_probe_format(XTC_PLUGIN_BUILTIN,XTC_BUILTIN_PATH,device,48000,&c.slots[i].plugin);
c.slots[i].enabled=1;c.slots[i].wet=1;c.slots[i].plugin.tempo=tempo;
}
if(r!=XTC_OK)return r;
if(index==0){c.slots[0].plugin.values[0]=80;c.slots[0].plugin.values[1]=.1f;c.slots[0].plugin.values[2]=.3f;}
if(index==1){c.slots[0].plugin.values[0]=375;c.slots[0].plugin.values[1]=.65f;c.slots[0].plugin.values[2]=.4f;c.slots[1].plugin.values[0]=.55f;c.slots[1].plugin.values[1]=.5f;c.slots[1].plugin.values[2]=.15f;}
if(index==2){c.slots[0].plugin.values[0]=.88f;c.slots[0].plugin.values[1]=.6f;c.slots[0].plugin.values[2]=.6f;c.slots[1].plugin.values[0]=500;c.slots[1].plugin.values[1]=.35f;c.slots[1].plugin.values[2]=.25f;}
r=xtc_track_chain_validate(&c);if(r==XTC_OK)*out=c;return r;
}

View file

@ -247,6 +247,9 @@ int creative_workflow_tests()
CHECK(window.undo()==XTC_OK);panel->refresh(true);
CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.output_gain==.6f);
panel->select_track(second_track);CHECK(!panel->comparison_ready(0) && panel->comparison(0,false)==XTC_ERR_NOT_FOUND);
CHECK(panel->apply_template(1)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==2 && chain.slots[0].plugin.values[0]==375);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0);
CHECK(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
CHECK(!panel->comparison_ready(0) && !panel->comparison_ready(1));
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;

View file

@ -2,6 +2,7 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
int main(void)
{
xtc_track_chain c={0},out={0};
@ -15,5 +16,14 @@ int main(void)
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)));
for(unsigned int preset=0;preset<3;++preset) {
xtc_graph *g=NULL;xtc_plan *plan=NULL;float input[512]={1},output[512];double energy=0;
assert(xtc_track_chain_template_name(preset) && xtc_track_chain_template(preset,135000,&c)==XTC_OK && xtc_track_chain_validate(&c)==XTC_OK);
assert(c.slots[0].plugin.tempo==135000 && xtc_track_chain_graph(&c,&g)==XTC_OK && xtc_graph_compile(g,8000,512,&plan)==XTC_OK);
assert(xtc_plan_process(plan,input,output,512)==XTC_OK);
for(size_t i=0;i<512;++i){assert(isfinite(output[i]));energy+=output[i]*output[i];}assert(energy>0);
xtc_plan_destroy(plan);xtc_graph_destroy(g);
}
out=c;assert(!xtc_track_chain_template_name(3) && xtc_track_chain_template(3,120000,&c)==XTC_ERR_INVALID_ARGUMENT && !memcmp(&out,&c,sizeof(c)));
puts("Creative editing core checks passed.");return 0;
}