feat: apply reversible boundary fades across audio clip selections

This commit is contained in:
Johannes Findeisen 2026-11-06 00:20:37 +01:00
commit 310b81ec4d
9 changed files with 47 additions and 7 deletions

View file

@ -39,3 +39,7 @@ Edit/MIDI tools/Correct to scale takes a root (0=C through 11=B) and chromatic,
## Phase 55 ## Phase 55
Edit/MIDI tools/Shape velocities scales from 0 to 400 percent, rounds, then applies an offset from -127 to 127. Results clamp to 1..127, keeping every note audible rather than introducing velocity-zero note-offs. Use 0 percent with a positive offset for a fixed velocity. No randomization is applied. Edit/MIDI tools/Shape velocities scales from 0 to 400 percent, rounds, then applies an offset from -127 to 127. Results clamp to 1..127, keeping every note audible rather than introducing velocity-zero note-offs. Use 0 percent with a positive offset for a fixed velocity. No randomization is applied.
## Phase 56
Edit/Audio tools/Set boundary fades sets linear fade-in and fade-out lengths in ticks; 0 or Remove boundary fades clears them. Each fade must fit within half of every selected clip duration. Gain and source offsets stay unchanged. These are boundary envelopes, not overlap-aware crossfades, equal-power curves or destructive WAV processing.

View file

@ -1453,12 +1453,12 @@ Implemented: rounded velocity percentage and offset transforms with explicit 1..
Apply symmetric boundary fades or remove fades on selected audio clips, preserving clip gain, media and timing. Apply symmetric boundary fades or remove fades on selected audio clips, preserving clip gain, media and timing.
### Tasks and acceptance criteria ### Tasks and acceptance criteria
- [ ] Provide the control/model behavior and accessible GUI commands. - [x] Provide the control/model behavior and accessible GUI commands.
- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. - [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
- [ ] Test the new workflow and document scope and limitations. - [x] Test the new workflow and document scope and limitations.
### Implementation status ### Implementation status
Planned. Implemented: selection-wide symmetric boundary fade and remove-fade tools preserve gain, references and timing. Fade lengths must fit half every current clip duration; mixed MIDI selections reject atomically. Core late-failure/property tests, existing audio rendering tests and focused GUI fade workflows pass.
--- ---

View file

@ -11,8 +11,10 @@ extern "C" {
* DUPLICATE retains tracks, source ranges, notes and media with fresh IDs. * DUPLICATE retains tracks, source ranges, notes and media with fresh IDs.
* ALIGN amount is an absolute timeline start; SLIP amount is a signed source * ALIGN amount is an absolute timeline start; SLIP amount is a signed source
* offset change (audio or MIDI), retaining timeline start/length. * offset change (audio or MIDI), retaining timeline start/length.
* FADE sets both audio boundary fades to amount ticks (0 removes); each must
* fit within half the current clip duration. MIDI clips reject this operation.
* out_ids receives resulting IDs (removed IDs for DELETE). May be NULL. */ * out_ids receives resulting IDs (removed IDs for DELETE). May be NULL. */
typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE, XTC_CLIPS_ALIGN, XTC_CLIPS_SLIP } xtc_clip_operation; typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE, XTC_CLIPS_ALIGN, XTC_CLIPS_SLIP, XTC_CLIPS_FADE } xtc_clip_operation;
xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count, xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count,
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids); xtc_clip_operation operation,int64_t amount,uint64_t *out_ids);
/* MIDI transforms affect ALL source notes in selected MIDI clips, including /* MIDI transforms affect ALL source notes in selected MIDI clips, including

View file

@ -322,6 +322,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
Fl_Group::current(parent); Fl_Group::current(parent);
menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_); menu->add("Edit/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_);
menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_); menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_);
menu->add("Edit/Audio tools/Set boundary fades...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->fades_dialog();},sequencer_);
menu->add("Edit/Audio tools/Remove boundary fades",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->transform_selection(XTC_CLIPS_FADE,0);},sequencer_);
menu->add("Edit/MIDI tools/Shape velocities...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->velocity_dialog();},sequencer_); menu->add("Edit/MIDI tools/Shape velocities...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->velocity_dialog();},sequencer_);
menu->add("Edit/MIDI tools/Correct to scale...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->scale_dialog();},sequencer_); menu->add("Edit/MIDI tools/Correct to scale...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->scale_dialog();},sequencer_);
menu->add("Edit/MIDI tools/Note gate...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->gate_dialog();},sequencer_); menu->add("Edit/MIDI tools/Note gate...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->gate_dialog();},sequencer_);

View file

@ -562,6 +562,13 @@ xtc_result SequencerWidget::transform_midi(const xtc_midi_transform &edit)
if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit); if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit);
changed(r);return r; changed(r);return r;
} }
void SequencerWidget::fades_dialog()
{
const char *input=fl_input("Symmetric fade length in ticks (0 removes; at most half each clip)","10");if(!input)return;
int64_t ticks=0;char extra;std::istringstream values(input);
if(!(values>>ticks) || (values>>extra) || ticks<0 || ticks>static_cast<int64_t>(XTC_PROJECT_MAX_TICK)){fl_alert("Enter a valid nonnegative tick length.");return;}
transform_selection(XTC_CLIPS_FADE,ticks);
}
void SequencerWidget::velocity_dialog() void SequencerWidget::velocity_dialog()
{ {
const char *input=fl_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return; const char *input=fl_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return;

View file

@ -43,6 +43,7 @@ class SequencerWidget : public Fl_Group {
void gate_dialog(); void gate_dialog();
void scale_dialog(); void scale_dialog();
void velocity_dialog(); void velocity_dialog();
void fades_dialog();
xtc_result repeat_selection(unsigned int copies); xtc_result repeat_selection(unsigned int copies);
void repeat_dialog(); void repeat_dialog();
xtc_result copy_selection(); xtc_result copy_selection();

View file

@ -1,5 +1,6 @@
#include "project_internal.h" #include "project_internal.h"
#include "xtc/xtc_editing.h" #include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -23,7 +24,7 @@ xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t
xtc_project *copy=NULL;size_t i;uint64_t output[XTC_PROJECT_MAX_CLIPS]; xtc_project *copy=NULL;size_t i;uint64_t output[XTC_PROJECT_MAX_CLIPS];
xtc_result r=selection_valid(p,ids,count); xtc_result r=selection_valid(p,ids,count);
if(r!=XTC_OK)return r; if(r!=XTC_OK)return r;
if(operation<XTC_CLIPS_MOVE || operation>XTC_CLIPS_SLIP || if(operation<XTC_CLIPS_MOVE || operation>XTC_CLIPS_FADE ||
amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT; amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_clone(p,&copy); r=xtc_project_clone(p,&copy);
for(i=0;r==XTC_OK && i<count;++i) { for(i=0;r==XTC_OK && i<count;++i) {
@ -31,6 +32,9 @@ xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t
r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break; r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break;
start=(int64_t)c.start_tick+amount;output[i]=ids[i]; start=(int64_t)c.start_tick+amount;output[i]=ids[i];
if(operation==XTC_CLIPS_DELETE)r=xtc_project_remove_clip(copy,ids[i]); if(operation==XTC_CLIPS_DELETE)r=xtc_project_remove_clip(copy,ids[i]);
else if(operation==XTC_CLIPS_FADE) {
r=amount<0 || (uint64_t)amount>c.length_tick/2 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_set_clip_audio(copy,ids[i],c.gain,(uint64_t)amount,(uint64_t)amount);
}
else if(operation==XTC_CLIPS_ALIGN) { else if(operation==XTC_CLIPS_ALIGN) {
r=amount<0 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)amount,c.length_tick,c.offset_tick); r=amount<0 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)amount,c.length_tick,c.offset_tick);
} else if(operation==XTC_CLIPS_SLIP) { } else if(operation==XTC_CLIPS_SLIP) {

View file

@ -1344,6 +1344,10 @@ int main(int argc, char **argv)
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.pitch==60); CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.pitch==60);
edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=50;edit.velocity_offset=10;CHECK(s->transform_midi(edit)==XTC_OK); edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=50;edit.velocity_offset=10;CHECK(s->transform_midi(edit)==XTC_OK);
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.velocity==60); CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.velocity==60);
CHECK(window.create_track(false)==XTC_OK && s->add_region()==XTC_OK);
CHECK(s->transform_selection(XTC_CLIPS_FADE,10)==XTC_OK);
CHECK(xtc_project_find_clip(s->project(),s->selected_clip(),&moved)==XTC_OK && moved.fade_in_tick==10 && moved.fade_out_tick==10);
CHECK(s->transform_selection(XTC_CLIPS_FADE,0)==XTC_OK);
window.do_callback(); window.do_callback();
} }
if (failures != 0) return 1; if (failures != 0) return 1;

View file

@ -1,7 +1,23 @@
#include "xtc/xtc_editing.h" #include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
static void test_fades(void)
{
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_add_clip(p,t,"A",0,960,&ids[0])==XTC_OK);
assert(xtc_project_add_clip(p,t,"B",960,240,&ids[1])==XTC_OK);
assert(xtc_project_set_clip_audio(p,ids[0],0.5f,0,0)==XTC_OK);
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_FADE,240,NULL)==XTC_ERR_INVALID_ARGUMENT);
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.fade_in_tick==0);
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_FADE,120,NULL)==XTC_OK);
assert(xtc_project_find_clip(p,ids[0],&c)==XTC_OK && c.gain==0.5f && c.fade_in_tick==120 && c.fade_out_tick==120 && c.start_tick==0);
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_FADE,0,NULL)==XTC_OK);
assert(xtc_project_find_clip(p,ids[1],&c)==XTC_OK && c.fade_in_tick==0 && c.fade_out_tick==0);
xtc_project_destroy(p);
}
static void test_velocity(void) static void test_velocity(void)
{ {
xtc_project *p=NULL;uint64_t t,c,n;xtc_note_info note;xtc_midi_transform edit={0}; xtc_project *p=NULL;uint64_t t,c,n;xtc_note_info note;xtc_midi_transform edit={0};
@ -106,7 +122,7 @@ static void test_repeat(void)
} }
int main(void) int main(void)
{ {
test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();test_velocity(); test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();test_velocity();test_fades();
xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c; xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c;
size_t tracks,clips,notes; size_t tracks,clips,notes;
assert(xtc_project_create(&p)==XTC_OK); assert(xtc_project_create(&p)==XTC_OK);