feat: shape MIDI note gates and create channel-aware legato phrases
This commit is contained in:
parent
2ba14f6728
commit
5acd3b65ab
9 changed files with 63 additions and 9 deletions
|
|
@ -27,3 +27,7 @@ Edit/Align clip starts to cursor places every selected start at the cursor. Slip
|
|||
## Phase 52
|
||||
|
||||
Select MIDI clips and use Edit/MIDI tools/Swing quantize. Grid is in source ticks; swing delays alternate subdivisions by 0..half a grid step, while strength blends the old start toward the nearest swung point. Durations stay fixed. All source notes are affected, including notes outside visible trims. Mixed audio/MIDI selections and notes that would exceed source bounds reject the entire edit. This is destructive note editing with undo, not a live groove engine.
|
||||
|
||||
## Phase 53
|
||||
|
||||
Edit/MIDI tools/Note gate scales lengths from 1 to 400 percent, rounded to at least one tick. Legato ends notes at the next strictly later onset in the same clip and channel; notes in a chord end together. The final onset extends to source end. Gate expansion beyond the source rejects the whole edit. Undo restores original durations.
|
||||
|
|
|
|||
|
|
@ -1408,12 +1408,12 @@ Implemented: deterministic nearest swung-grid quantization with 0..100 strength
|
|||
Set note gates or extend notes to the next onset on the same channel, preserving IDs and validating all notes before publication.
|
||||
|
||||
### 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: atomic gate scaling and same-channel legato over selected MIDI clip sources. Chords share the next distinct onset; final notes extend to source end. Core channel/chord/bounds rollback and GUI duration/refresh tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,
|
|||
* fields persist. No matching notes returns NOT_FOUND. Edits are atomic.
|
||||
* Swing: grid 1..3840, swing 0..100 delays alternate grid points by up to half
|
||||
* a grid; strength 0..100 blends toward the nearest swung point (ties later).
|
||||
* Durations stay fixed. Out-of-source results reject the complete operation. */
|
||||
typedef enum { XTC_MIDI_TRANSFORM_SWING } xtc_midi_transform_type;
|
||||
* Swing durations stay fixed. LEGATO ends at the next strictly later onset in
|
||||
* the same clip/channel (chords stay together), or source end for the last onset. Out-of-source results reject the complete operation. */
|
||||
typedef enum { XTC_MIDI_TRANSFORM_SWING, XTC_MIDI_TRANSFORM_GATE, XTC_MIDI_TRANSFORM_LEGATO } xtc_midi_transform_type;
|
||||
typedef struct {
|
||||
xtc_midi_transform_type type;
|
||||
uint64_t grid;
|
||||
unsigned int swing,strength;
|
||||
unsigned int gate_percent; /* GATE: 1..400, rounded length, minimum 1 tick. */
|
||||
} xtc_midi_transform;
|
||||
xtc_result xtc_project_transform_midi(xtc_project *project,const uint64_t *ids,size_t count,const xtc_midi_transform *edit);
|
||||
/* Repeat the bounding phrase span, including gaps, 1..1024 additional copies.
|
||||
|
|
|
|||
|
|
@ -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/MIDI tools/Note gate...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->gate_dialog();},sequencer_);
|
||||
menu->add("Edit/MIDI tools/Legato",0,[](Fl_Widget *,void *c){xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_LEGATO;static_cast<SequencerWidget *>(c)->transform_midi(edit);},sequencer_);
|
||||
menu->add("Edit/MIDI tools/Swing quantize...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->swing_dialog();},sequencer_);
|
||||
menu->add("Edit/Align clip starts to cursor",0,[](Fl_Widget *,void *c){auto *s=static_cast<SequencerWidget *>(c);s->transform_selection(XTC_CLIPS_ALIGN,static_cast<int64_t>(s->position()));},sequencer_);
|
||||
menu->add("Edit/Slip sources earlier",0,[](Fl_Widget *,void *c){auto *s=static_cast<SequencerWidget *>(c);s->transform_selection(XTC_CLIPS_SLIP,-static_cast<int64_t>(s->grid()));},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::gate_dialog()
|
||||
{
|
||||
const char *input=fl_input("Note length percentage (1..400)","50");if(!input)return;
|
||||
unsigned int percent=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>percent) || (values>>extra) || percent<1 || percent>400){fl_alert("Enter a whole percentage from 1 to 400.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=percent;transform_midi(edit);
|
||||
}
|
||||
void SequencerWidget::swing_dialog()
|
||||
{
|
||||
const char *input=fl_input("Grid ticks (1..3840), swing (0..100), strength (0..100)","240 50 100");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class SequencerWidget : public Fl_Group {
|
|||
xtc_result transform_selection(xtc_clip_operation operation,int64_t amount);
|
||||
xtc_result transform_midi(const xtc_midi_transform &edit);
|
||||
void swing_dialog();
|
||||
void gate_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
xtc_result copy_selection();
|
||||
|
|
|
|||
|
|
@ -133,7 +133,9 @@ xtc_result xtc_project_transform_midi(xtc_project *p,const uint64_t *ids,size_t
|
|||
{
|
||||
xtc_project *copy=NULL;size_t i,j,matched=0;xtc_result r=selection_valid(p,ids,count);
|
||||
if(r!=XTC_OK)return r;
|
||||
if(!edit || edit->type!=XTC_MIDI_TRANSFORM_SWING || !edit->grid || edit->grid>3840 || edit->swing>100 || edit->strength>100)return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(!edit || edit->type<XTC_MIDI_TRANSFORM_SWING || edit->type>XTC_MIDI_TRANSFORM_LEGATO)return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(edit->type==XTC_MIDI_TRANSFORM_SWING && (!edit->grid || edit->grid>3840 || edit->swing>100 || edit->strength>100))return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(edit->type==XTC_MIDI_TRANSFORM_GATE && (!edit->gate_percent || edit->gate_percent>400))return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i){xtc_clip_info c;xtc_project_find_clip(p,ids[i],&c);if(!c.midi)return XTC_ERR_INVALID_STATE;}
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=0;r==XTC_OK && i<copy->note_count;++i) {
|
||||
|
|
@ -141,7 +143,18 @@ xtc_result xtc_project_transform_midi(xtc_project *p,const uint64_t *ids,size_t
|
|||
for(j=0;j<count;++j)if(n->clip_id==ids[j])break;
|
||||
if(j==count)continue;
|
||||
++matched;xtc_project_find_clip(copy,n->clip_id,&c);
|
||||
n->start_tick=swung_start(n->start_tick,edit);
|
||||
if(edit->type==XTC_MIDI_TRANSFORM_SWING)n->start_tick=swung_start(n->start_tick,edit);
|
||||
else if(edit->type==XTC_MIDI_TRANSFORM_GATE) {
|
||||
n->length_tick=(n->length_tick*edit->gate_percent+50)/100;
|
||||
if(!n->length_tick)n->length_tick=1;
|
||||
} else {
|
||||
uint64_t next=c.source_length_tick;
|
||||
for(j=0;j<p->note_count;++j) {
|
||||
const xtc_note_info *other=p->notes+j;
|
||||
if(other->clip_id==n->clip_id && other->channel==n->channel && other->start_tick>n->start_tick && other->start_tick<next)next=other->start_tick;
|
||||
}
|
||||
n->length_tick=next-n->start_tick;
|
||||
}
|
||||
if(n->start_tick>=c.source_length_tick || n->length_tick>c.source_length_tick-n->start_tick)r=XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
if(r==XTC_OK && !matched)r=XTC_ERR_NOT_FOUND;
|
||||
|
|
|
|||
|
|
@ -1336,6 +1336,10 @@ int main(int argc, char **argv)
|
|||
xtc_midi_transform edit{};edit.grid=240;edit.swing=50;edit.strength=100;
|
||||
CHECK(s->transform_midi(edit)==XTC_OK);xtc_note_info quantized{};
|
||||
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.start_tick==300);
|
||||
edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=50;CHECK(s->transform_midi(edit)==XTC_OK);
|
||||
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.length_tick==60);
|
||||
edit.type=XTC_MIDI_TRANSFORM_LEGATO;CHECK(s->transform_midi(edit)==XTC_OK);
|
||||
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.length_tick==3540);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,27 @@
|
|||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static void test_gate_legato(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,c,n;xtc_note_info note;xtc_midi_transform edit={0};
|
||||
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p,t,0,960,&c)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,c,0,120,60,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,c,0,120,64,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,c,240,120,67,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,c,120,120,72,100,1,&n)==XTC_OK);
|
||||
edit.type=XTC_MIDI_TRANSFORM_LEGATO;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.length_tick==240);
|
||||
assert(xtc_project_get_note(p,1,¬e)==XTC_OK && note.length_tick==240);
|
||||
assert(xtc_project_get_note(p,2,¬e)==XTC_OK && note.length_tick==720);
|
||||
assert(xtc_project_get_note(p,3,¬e)==XTC_OK && note.length_tick==840);
|
||||
edit.type=XTC_MIDI_TRANSFORM_GATE;edit.gate_percent=200;
|
||||
assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.length_tick==240);
|
||||
edit.gate_percent=50;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.length_tick==120);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
static void test_swing(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,c,n;xtc_note_info note;xtc_midi_transform edit={0};
|
||||
|
|
@ -54,7 +75,7 @@ static void test_repeat(void)
|
|||
}
|
||||
int main(void)
|
||||
{
|
||||
test_repeat();test_align_slip();test_swing();
|
||||
test_repeat();test_align_slip();test_swing();test_gate_legato();
|
||||
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