feat: shape MIDI phrase dynamics with bounded velocity scaling
This commit is contained in:
parent
77fc5b4c0e
commit
4af18fc33d
9 changed files with 44 additions and 7 deletions
|
|
@ -35,3 +35,7 @@ Edit/MIDI tools/Note gate scales lengths from 1 to 400 percent, rounded to at le
|
|||
## Phase 54
|
||||
|
||||
Edit/MIDI tools/Correct to scale takes a root (0=C through 11=B) and chromatic, major or natural minor. Each pitch moves to the nearest permitted MIDI note; ties choose the lower pitch. Timing, channel and velocity remain unchanged. Corrections can produce unison notes; notes are not merged. This changes MIDI notes, not audio pitch or a live scale filter.
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1438,12 +1438,12 @@ Implemented: root-aware nearest-pitch correction to chromatic, major or natural
|
|||
Apply bounded velocity scaling and offsets to selected MIDI clips, with explicit clipping to valid MIDI velocities.
|
||||
|
||||
### 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: rounded velocity percentage and offset transforms with explicit 1..127 saturation. Core scale/offset/extreme/invalid-input and GUI velocity-refresh tests pass; timing, IDs, pitch and channel remain unchanged.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,
|
|||
* a grid; strength 0..100 blends toward the nearest swung point (ties later).
|
||||
* 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_SCALE } xtc_midi_transform_type;
|
||||
typedef enum { XTC_MIDI_TRANSFORM_SWING, XTC_MIDI_TRANSFORM_GATE, XTC_MIDI_TRANSFORM_LEGATO, XTC_MIDI_TRANSFORM_SCALE, XTC_MIDI_TRANSFORM_VELOCITY } xtc_midi_transform_type;
|
||||
typedef struct {
|
||||
xtc_midi_transform_type type;
|
||||
uint64_t grid;
|
||||
unsigned int swing,strength;
|
||||
unsigned int velocity_percent; /* VELOCITY: 0..400, rounded scaling then offset, clamp to 1..127. */
|
||||
int velocity_offset; /* -127..127 */
|
||||
unsigned int root,scale; /* SCALE: root 0..11 (C..B), scale 0 chromatic, 1 major, 2 natural minor; nearest ties lower. */
|
||||
unsigned int gate_percent; /* GATE: 1..400, rounded length, minimum 1 tick. */
|
||||
} xtc_midi_transform;
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ 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/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_);
|
||||
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_);
|
||||
|
|
|
|||
|
|
@ -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::velocity_dialog()
|
||||
{
|
||||
const char *input=fl_input("Velocity percentage (0..400), then offset (-127..127)","100 0");if(!input)return;
|
||||
unsigned int percent=0;int offset=0;char extra;std::istringstream values(input);
|
||||
if(!(values>>percent>>offset) || (values>>extra) || percent>400 || offset < -127 || offset>127){fl_alert("Enter a valid percentage and velocity offset.");return;}
|
||||
xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=percent;edit.velocity_offset=offset;transform_midi(edit);
|
||||
}
|
||||
void SequencerWidget::scale_dialog()
|
||||
{
|
||||
const char *input=fl_input("Root (0=C..11=B), scale (0=chromatic, 1=major, 2=natural minor)","0 1");if(!input)return;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class SequencerWidget : public Fl_Group {
|
|||
void swing_dialog();
|
||||
void gate_dialog();
|
||||
void scale_dialog();
|
||||
void velocity_dialog();
|
||||
xtc_result repeat_selection(unsigned int copies);
|
||||
void repeat_dialog();
|
||||
xtc_result copy_selection();
|
||||
|
|
|
|||
|
|
@ -133,9 +133,10 @@ 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->type>XTC_MIDI_TRANSFORM_SCALE)return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(!edit || edit->type<XTC_MIDI_TRANSFORM_SWING || edit->type>XTC_MIDI_TRANSFORM_VELOCITY)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;
|
||||
if(edit->type==XTC_MIDI_TRANSFORM_VELOCITY && (edit->velocity_percent>400 || edit->velocity_offset < -127 || edit->velocity_offset>127))return XTC_ERR_INVALID_ARGUMENT;
|
||||
if(edit->type==XTC_MIDI_TRANSFORM_SCALE && (edit->root>11 || edit->scale>2))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;
|
||||
|
|
@ -163,6 +164,9 @@ xtc_result xtc_project_transform_midi(xtc_project *p,const uint64_t *ids,size_t
|
|||
if(d<distance){distance=d;best=pitch;}
|
||||
}
|
||||
n->pitch=best;
|
||||
} else if(edit->type==XTC_MIDI_TRANSFORM_VELOCITY) {
|
||||
int velocity=(int)((n->velocity*edit->velocity_percent+50)/100)+edit->velocity_offset;
|
||||
n->velocity=(unsigned int)(velocity<1 ? 1 : velocity>127 ? 127 : velocity);
|
||||
}
|
||||
if(n->start_tick>=c.source_length_tick || n->length_tick>c.source_length_tick-n->start_tick)r=XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1342,6 +1342,8 @@ int main(int argc, char **argv)
|
|||
CHECK(xtc_project_get_note(s->project(),0,&quantized)==XTC_OK && quantized.length_tick==3540);
|
||||
edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=0;edit.scale=1;CHECK(s->transform_midi(edit)==XTC_OK);
|
||||
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);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@
|
|||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static void test_velocity(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,61,100,0,&n)==XTC_OK);
|
||||
edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=50;edit.velocity_offset=10;
|
||||
assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.velocity==60 && note.pitch==61);
|
||||
edit.velocity_percent=400;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.velocity==127);
|
||||
edit.velocity_percent=0;edit.velocity_offset=-127;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,¬e)==XTC_OK && note.velocity==1 && note.id==n);
|
||||
edit.velocity_percent=401;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_ERR_INVALID_ARGUMENT);
|
||||
xtc_project_destroy(p);
|
||||
}
|
||||
static void test_scale(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t t,c,n;xtc_note_info note;xtc_midi_transform edit={0};
|
||||
|
|
@ -90,7 +106,7 @@ static void test_repeat(void)
|
|||
}
|
||||
int main(void)
|
||||
{
|
||||
test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();
|
||||
test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();test_velocity();
|
||||
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