feat: generate bounded ascending and descending MIDI arpeggio patterns
This commit is contained in:
parent
6be4f9803a
commit
ea0b4ca3da
9 changed files with 89 additions and 5 deletions
|
|
@ -34,3 +34,7 @@ Edit/MIDI tools/Humanize applies timing variation up to 960 ticks and velocity v
|
|||
## Phase 65 — Chord Construction
|
||||
|
||||
Edit/MIDI tools/Build chords accepts 1..8 unique nonzero semitone intervals, such as 4 7 for a major triad or 3 7 for minor. Originals remain; added notes copy timing, velocity and channel. Negative intervals support lower voicings. All source notes are processed, including hidden trimmed notes. Pitch overflow or capacity rejects the whole operation; existing unisons are not deduplicated.
|
||||
|
||||
## Phase 66 — Offline Arpeggio Generation
|
||||
|
||||
Edit/MIDI tools/Generate arpeggio replaces source notes with a written pattern. Equal-onset notes on the same channel form a chord lasting until its longest note end. The pattern cycles sorted pitches upward or downward, with the selected step and gate. Each step inherits its chosen note velocity/channel. All source notes, including hidden trimmed notes, participate. This is offline generation with undo, not a live arpeggiator or tempo-synced MIDI effect.
|
||||
|
|
|
|||
|
|
@ -1617,12 +1617,12 @@ Implemented: custom bounded chord intervals add independent notes with fresh IDs
|
|||
Convert source note chords into bounded ascending/descending stepped patterns with gate controls, deterministic ordering and undo.
|
||||
|
||||
### Tasks and acceptance criteria
|
||||
- [ ] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [ ] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [ ] Verify the feature with automated model/GUI tests and document limitations.
|
||||
- [x] Implement bounded C99 model/control APIs and accessible GUI commands.
|
||||
- [x] Preserve transactional errors, history, compatibility and callback safety.
|
||||
- [x] Verify the feature with automated model/GUI tests and document limitations.
|
||||
|
||||
### Implementation status
|
||||
Planned.
|
||||
Implemented: bounded offline ascending/descending arpeggio generation with equal-onset/channel chord grouping, step/gate controls and fresh note IDs. Core ordering/gate/capacity rollback and GUI generation/undo tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ xtc_result xtc_project_merge_midi(xtc_project *project,const uint64_t *ids,size_
|
|||
* in selected MIDI clips, retaining originals/articulation and allocating fresh
|
||||
* note IDs. Mixed selections, pitch overflow and capacity reject atomically. */
|
||||
xtc_result xtc_project_chordify(xtc_project *project,const uint64_t *ids,size_t count,const int *intervals,size_t interval_count);
|
||||
/* Offline arpeggio: replaces all source notes in selected MIDI clips with fresh
|
||||
* IDs. Equal-onset/channel notes form a chord lasting to its longest note end.
|
||||
* step 1..3840, gate 1..100 percent, descending 0/1. Cycles sorted pitches (ties
|
||||
* by original ID); each step inherits its chosen note velocity/channel. Atomic. */
|
||||
xtc_result xtc_project_arpeggiate(xtc_project *project,const uint64_t *ids,size_t count,uint64_t step,unsigned int gate,int descending);
|
||||
/* MIDI transforms affect ALL source notes in selected MIDI clips, including
|
||||
* notes hidden by trims. Mixed audio/MIDI selections reject; IDs and unaffected
|
||||
* fields persist. Initialize the edit struct to zero and set the fields for its
|
||||
|
|
|
|||
|
|
@ -331,6 +331,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
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/Generate arpeggio...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->arpeggio_dialog();},sequencer_);
|
||||
menu->add("Edit/MIDI tools/Build chords...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->chord_dialog();},sequencer_);
|
||||
menu->add("Edit/MIDI tools/Humanize...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->humanize_dialog();},sequencer_);
|
||||
menu->add("Edit/MIDI tools/Shape velocities...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->velocity_dialog();},sequencer_);
|
||||
|
|
|
|||
|
|
@ -624,6 +624,19 @@ xtc_result SequencerWidget::make_chord(const std::vector<int> &intervals)
|
|||
if(r==XTC_OK)r=xtc_project_chordify(project_,ids.data(),ids.size(),intervals.data(),intervals.size());
|
||||
changed(r);return r;
|
||||
}
|
||||
xtc_result SequencerWidget::arpeggiate(uint64_t step,unsigned int gate,bool descending)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_arpeggiate(project_,ids.data(),ids.size(),step,gate,descending);
|
||||
changed(r);return r;
|
||||
}
|
||||
void SequencerWidget::arpeggio_dialog()
|
||||
{
|
||||
const char *input=fl_input("Step ticks (1..3840), gate percent (1..100), direction (0=up, 1=down)","240 80 0");if(!input)return;
|
||||
uint64_t step=0;unsigned int gate=0;int down=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>step>>gate>>down) || (values>>extra) || !step || step>3840 || !gate || gate>100 || down<0 || down>1){fl_alert("Enter a valid step, gate and direction.");return;}
|
||||
arpeggiate(step,gate,down!=0);
|
||||
}
|
||||
void SequencerWidget::chord_dialog()
|
||||
{
|
||||
const char *input=fl_input("Additional semitone intervals: 1..8 unique nonzero values (-127..127)","4 7");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class SequencerWidget : public Fl_Group {
|
|||
void humanize_dialog();
|
||||
xtc_result make_chord(const std::vector<int> &intervals);
|
||||
void chord_dialog();
|
||||
xtc_result arpeggiate(uint64_t step,unsigned int gate,bool descending);
|
||||
void arpeggio_dialog();
|
||||
void fades_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
|
|
|
|||
|
|
@ -298,3 +298,39 @@ xtc_result xtc_project_chordify(xtc_project *p,const uint64_t *ids,size_t count,
|
|||
if(r==XTC_OK)publish(p,copy);
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
||||
static int chord_order(const void *a,const void *b)
|
||||
{
|
||||
const xtc_note_info *x=*(const xtc_note_info *const *)a,*y=*(const xtc_note_info *const *)b;
|
||||
if(x->pitch!=y->pitch)return x->pitch<y->pitch ? -1 : 1;
|
||||
return x->id==y->id ? 0 : x->id<y->id ? -1 : 1;
|
||||
}
|
||||
xtc_result xtc_project_arpeggiate(xtc_project *p,const uint64_t *ids,size_t count,uint64_t step,unsigned int gate,int descending)
|
||||
{
|
||||
xtc_project *copy=NULL;size_t i,j,k,matched=0;xtc_result r=midi_selection(p,ids,count);
|
||||
if(r!=XTC_OK)return r;
|
||||
if(!step || step>3840 || !gate || gate>100 || (descending!=0 && descending!=1))return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=copy->note_count;i>0;--i){for(j=0;j<count;++j)if(copy->notes[i-1].clip_id==ids[j])break;if(j<count)r=xtc_project_remove_note(copy,copy->notes[i-1].id);}
|
||||
for(i=0;r==XTC_OK && i<p->note_count;++i) {
|
||||
const xtc_note_info *first=p->notes+i,*chord[XTC_PROJECT_MAX_NOTES];size_t voices=0,index=0;uint64_t end=0,at;
|
||||
for(j=0;j<count;++j)if(first->clip_id==ids[j])break;
|
||||
if(j==count)continue;
|
||||
for(j=0;j<i;++j)if(p->notes[j].clip_id==first->clip_id && p->notes[j].start_tick==first->start_tick && p->notes[j].channel==first->channel)break;
|
||||
if(j<i)continue;
|
||||
++matched;
|
||||
for(j=i;j<p->note_count;++j){const xtc_note_info *n=p->notes+j;
|
||||
if(n->clip_id==first->clip_id && n->start_tick==first->start_tick && n->channel==first->channel){chord[voices++]=n;if(n->start_tick+n->length_tick>end)end=n->start_tick+n->length_tick;}
|
||||
}
|
||||
if(!voices){r=XTC_ERR_INVALID_STATE;break;}
|
||||
qsort(chord,voices,sizeof(*chord),chord_order);
|
||||
for(at=first->start_tick;r==XTC_OK && at<end;at+=step,++index) {
|
||||
uint64_t length=(step*gate+50)/100,note;if(!length)length=1;if(length>end-at)length=end-at;
|
||||
k=index%voices;if(descending)k=voices-1-k;
|
||||
r=xtc_project_add_note(copy,first->clip_id,at,length,chord[k]->pitch,chord[k]->velocity,chord[k]->channel,¬e);
|
||||
}
|
||||
}
|
||||
if(r==XTC_OK && !matched)r=XTC_ERR_NOT_FOUND;
|
||||
if(r==XTC_OK)publish(p,copy);
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1390,6 +1390,8 @@ int main(int argc, char **argv)
|
|||
CHECK(s->transform_midi(human)==XTC_OK);CHECK(window.undo()==XTC_OK);
|
||||
CHECK(s->select(0,0));CHECK(s->make_chord({4,7})==XTC_OK);
|
||||
size_t notes=0;xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
CHECK(s->arpeggiate(80,50,false)==XTC_OK);xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
CHECK(window.undo()==XTC_OK);xtc_project_note_count(s->project(),¬es);CHECK(notes==3);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -142,4 +142,25 @@ static void chords(void)
|
|||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==XTC_PROJECT_MAX_NOTES);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void arpeggio(void)
|
||||
{
|
||||
xtc_project *p=NULL,*q=NULL;uint64_t t,c,n;size_t count;xtc_note_info note;int intervals[2]={4,7};
|
||||
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 && xtc_project_add_note(p,c,0,960,60,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_chordify(p,&c,1,intervals,2)==XTC_OK);
|
||||
assert(xtc_project_clone(p,&q)==XTC_OK && xtc_project_arpeggiate(q,&c,1,240,50,1)==XTC_OK);
|
||||
assert(xtc_project_get_note(q,0,¬e)==XTC_OK && note.pitch==67);xtc_project_destroy(q);
|
||||
assert(xtc_project_arpeggiate(p,&c,1,240,50,0)==XTC_OK);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==4);
|
||||
assert(xtc_project_get_note(p,1,¬e)==XTC_OK && note.start_tick==240 && note.pitch==64 && note.length_tick==120);
|
||||
assert(xtc_project_get_note(p,3,¬e)==XTC_OK && note.start_tick==720 && note.pitch==60);
|
||||
assert(xtc_project_arpeggiate(p,&c,1,1,100,0)==XTC_OK);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==480);
|
||||
assert(xtc_project_arpeggiate(p,&c,1,0,100,0)==XTC_ERR_INVALID_ARGUMENT);
|
||||
xtc_project_destroy(p);p=NULL;
|
||||
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,XTC_PROJECT_MAX_TICK,&c)==XTC_OK && xtc_project_add_note(p,c,0,XTC_PROJECT_MAX_TICK,60,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_arpeggiate(p,&c,1,1,100,0)==XTC_ERR_CAPACITY);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==1);xtc_project_destroy(p);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();chords();arpeggio();puts("Next DAW workflow tests passed.");return 0;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue