feat: apply reversible boundary fades across audio clip selections
This commit is contained in:
parent
4af18fc33d
commit
310b81ec4d
9 changed files with 47 additions and 7 deletions
|
|
@ -39,3 +39,7 @@ Edit/MIDI tools/Correct to scale takes a root (0=C through 11=B) and chromatic,
|
|||
## 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.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Provide the control/model behavior and accessible GUI commands.
|
||||
- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
|
||||
- [ ] Test the new workflow and document scope and limitations.
|
||||
- [x] Provide the control/model behavior and accessible GUI commands.
|
||||
- [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety.
|
||||
- [x] Test the new workflow and document scope and limitations.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ extern "C" {
|
|||
* DUPLICATE retains tracks, source ranges, notes and media with fresh IDs.
|
||||
* ALIGN amount is an absolute timeline start; SLIP amount is a signed source
|
||||
* 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. */
|
||||
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_clip_operation operation,int64_t amount,uint64_t *out_ids);
|
||||
/* MIDI transforms affect ALL source notes in selected MIDI clips, including
|
||||
|
|
|
|||
|
|
@ -322,6 +322,8 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
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 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/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_);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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()
|
||||
{
|
||||
const char *input=fl_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ class SequencerWidget : public Fl_Group {
|
|||
void gate_dialog();
|
||||
void scale_dialog();
|
||||
void velocity_dialog();
|
||||
void fades_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
xtc_result copy_selection();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "project_internal.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <stdlib.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_result r=selection_valid(p,ids,count);
|
||||
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;
|
||||
r=xtc_project_clone(p,©);
|
||||
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;
|
||||
start=(int64_t)c.start_tick+amount;output[i]=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) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1344,6 +1344,10 @@ int main(int argc, char **argv)
|
|||
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);
|
||||
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();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
#include "xtc/xtc_editing.h"
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <assert.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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
size_t tracks,clips,notes;
|
||||
assert(xtc_project_create(&p)==XTC_OK);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue