feat: build custom MIDI chord voicings with atomic pitch and capacity checks
This commit is contained in:
parent
d034417c8c
commit
6be4f9803a
9 changed files with 76 additions and 5 deletions
|
|
@ -30,3 +30,7 @@ Edit/Consolidate MIDI clips merges two or more selected MIDI clips on one track.
|
|||
## Phase 64 — Deterministic MIDI Humanization
|
||||
|
||||
Edit/MIDI tools/Humanize applies timing variation up to 960 ticks and velocity variation up to 127, using a 32-bit seed and stable note IDs. Repeating from the same original state reproduces results; applying repeatedly accumulates edits. All source notes are affected, including hidden trimmed notes. Timing outside source bounds rejects the entire edit; velocities clamp to 1..127. This is offline editing, not live randomness.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1602,12 +1602,12 @@ Implemented: seeded note-ID keyed timing/velocity humanization, independent of s
|
|||
Generate bounded custom chord intervals from source notes with fresh IDs, preserved articulation and transactional pitch/capacity checks.
|
||||
|
||||
### 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: custom bounded chord intervals add independent notes with fresh IDs while retaining originals and articulation. Core pitch/duplicate-interval/capacity-bound rollback and GUI chord/history tests pass.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ xtc_result xtc_project_split_midi(xtc_project *project,uint64_t clip,uint64_t ti
|
|||
* boundaries, drops hidden notes, retains overlaps and gaps. Fresh IDs; atomic.
|
||||
* Cropped notes start anew; instrument envelopes/phase can change. */
|
||||
xtc_result xtc_project_merge_midi(xtc_project *project,const uint64_t *ids,size_t count,uint64_t *out);
|
||||
/* Adds 1..8 unique nonzero semitone intervals (-127..127) to EVERY source note
|
||||
* 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);
|
||||
/* 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/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_);
|
||||
menu->add("Edit/MIDI tools/Correct to scale...",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->scale_dialog();},sequencer_);
|
||||
|
|
|
|||
|
|
@ -618,6 +618,20 @@ void SequencerWidget::fades_dialog()
|
|||
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);
|
||||
}
|
||||
xtc_result SequencerWidget::make_chord(const std::vector<int> &intervals)
|
||||
{
|
||||
auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_chordify(project_,ids.data(),ids.size(),intervals.data(),intervals.size());
|
||||
changed(r);return r;
|
||||
}
|
||||
void SequencerWidget::chord_dialog()
|
||||
{
|
||||
const char *input=fl_input("Additional semitone intervals: 1..8 unique nonzero values (-127..127)","4 7");if(!input)return;
|
||||
std::istringstream values(input);std::vector<int> intervals;int interval;
|
||||
while(values>>interval){intervals.push_back(interval);if(intervals.size()>8)break;}
|
||||
if(!values.eof() || intervals.empty() || intervals.size()>8){fl_alert("Enter 1 to 8 semitone intervals.");return;}
|
||||
make_chord(intervals);
|
||||
}
|
||||
void SequencerWidget::humanize_dialog()
|
||||
{
|
||||
const char *input=fl_input("Timing variation ticks (0..960), velocity variation (0..127), seed","12 8 1");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ class SequencerWidget : public Fl_Group {
|
|||
void scale_dialog();
|
||||
void velocity_dialog();
|
||||
void humanize_dialog();
|
||||
xtc_result make_chord(const std::vector<int> &intervals);
|
||||
void chord_dialog();
|
||||
void fades_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
|
|
|
|||
|
|
@ -270,3 +270,31 @@ xtc_result xtc_project_merge_midi(xtc_project *p,const uint64_t *ids,size_t coun
|
|||
if(r==XTC_OK){publish(p,copy);*out=id;}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
||||
static xtc_result midi_selection(const xtc_project *p,const uint64_t *ids,size_t count)
|
||||
{
|
||||
size_t i;xtc_result r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
|
||||
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;}
|
||||
return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_chordify(xtc_project *p,const uint64_t *ids,size_t count,const int *intervals,size_t interval_count)
|
||||
{
|
||||
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(!intervals || !interval_count || interval_count>8)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<interval_count;++i){if(!intervals[i] || intervals[i]<-127 || intervals[i]>127)return XTC_ERR_INVALID_ARGUMENT;for(j=0;j<i;++j)if(intervals[j]==intervals[i])return XTC_ERR_INVALID_ARGUMENT;}
|
||||
r=xtc_project_clone(p,©);if(r!=XTC_OK)return r;
|
||||
for(i=0;r==XTC_OK && i<p->note_count;++i) {
|
||||
const xtc_note_info *n=p->notes+i;
|
||||
for(j=0;j<count;++j)if(n->clip_id==ids[j])break;
|
||||
if(j==count)continue;
|
||||
++matched;
|
||||
for(k=0;r==XTC_OK && k<interval_count;++k){int pitch=(int)n->pitch+intervals[k];uint64_t id;
|
||||
if(pitch<0 || pitch>127){r=XTC_ERR_INVALID_ARGUMENT;break;}
|
||||
r=xtc_project_add_note(copy,n->clip_id,n->start_tick,n->length_tick,(unsigned int)pitch,n->velocity,n->channel,&id);
|
||||
}
|
||||
}
|
||||
if(r==XTC_OK && !matched)r=XTC_ERR_NOT_FOUND;
|
||||
if(r==XTC_OK)publish(p,copy);
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1388,6 +1388,8 @@ int main(int argc, char **argv)
|
|||
CHECK(s->select(0,0));CHECK(window.open_piano_roll()==XTC_OK);CHECK(window.piano_roll()->add_note(480,240,60)==XTC_OK);
|
||||
xtc_midi_transform human{};human.type=XTC_MIDI_TRANSFORM_HUMANIZE;human.seed=42;human.timing_variation=12;human.velocity_variation=8;
|
||||
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);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -126,4 +126,20 @@ static void humanize(void)
|
|||
assert(xtc_project_get_note(p,0,&a)==XTC_OK && a.start_tick==0 && a.velocity==100);
|
||||
xtc_project_destroy(p);xtc_project_destroy(q);
|
||||
}
|
||||
int main(void){markers();sections();ripple();duplicate_track();split_midi();merge_midi();humanize();puts("Next DAW workflow tests passed.");return 0;}
|
||||
static void chords(void)
|
||||
{
|
||||
xtc_project *p=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,1920,&c)==XTC_OK && xtc_project_add_note(p,c,480,120,60,100,2,&n)==XTC_OK);
|
||||
assert(xtc_project_chordify(p,&c,1,intervals,2)==XTC_OK && xtc_project_note_count(p,&count)==XTC_OK && count==3);
|
||||
assert(xtc_project_get_note(p,2,¬e)==XTC_OK && note.pitch==67 && note.start_tick==480 && note.channel==2 && note.id!=n);
|
||||
assert(xtc_project_add_note(p,c,0,120,125,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_chordify(p,&c,1,intervals,2)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_note_count(p,&count)==XTC_OK && count==4);
|
||||
intervals[1]=4;assert(xtc_project_chordify(p,&c,1,intervals,2)==XTC_ERR_INVALID_ARGUMENT);
|
||||
while(count<XTC_PROJECT_MAX_NOTES){assert(xtc_project_add_note(p,c,0,1,60,100,0,&n)==XTC_OK);++count;}
|
||||
intervals[0]=1;assert(xtc_project_chordify(p,&c,1,intervals,1)==XTC_ERR_CAPACITY);
|
||||
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;}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue