xtc/src/project/editing.c

336 lines
19 KiB
C
Raw Normal View History

#include "project_internal.h"
#include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include <stdlib.h>
#include <string.h>
static xtc_result selection_valid(const xtc_project *p,const uint64_t *ids,size_t count)
{
size_t i,j;xtc_clip_info c;
if(!p || !ids || !count || count>XTC_PROJECT_MAX_CLIPS)return XTC_ERR_INVALID_ARGUMENT;
for(i=0;i<count;++i) {
if(xtc_project_find_clip(p,ids[i],&c)!=XTC_OK)return XTC_ERR_NOT_FOUND;
for(j=0;j<i;++j)if(ids[i]==ids[j])return XTC_ERR_INVALID_ARGUMENT;
}
return XTC_OK;
}
static void publish(xtc_project *p,xtc_project *copy)
{
xtc_graph *old=p->graph;*p=*copy;copy->graph=old;
}
xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t count,
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids)
{
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_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,&copy);
for(i=0;r==XTC_OK && i<count;++i) {
xtc_clip_info c;int64_t start;
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) {
int64_t offset=(int64_t)c.offset_tick+amount;
r=offset<0 ? XTC_ERR_INVALID_ARGUMENT : xtc_project_edit_clip(copy,ids[i],c.track_id,c.start_tick,c.length_tick,(uint64_t)offset);
}
else if(start<0 || start>(int64_t)XTC_PROJECT_MAX_TICK)r=XTC_ERR_INVALID_ARGUMENT;
else if(operation==XTC_CLIPS_DUPLICATE)r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,(uint64_t)start,output+i);
else r=xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)start,c.length_tick,c.offset_tick);
}
if(r==XTC_OK){publish(p,copy);if(out_ids)memcpy(out_ids,output,count*sizeof(*output));}
xtc_project_destroy(copy);return r;
}
struct xtc_clipboard {xtc_project *source;size_t count,first_track;uint64_t begin,ids[XTC_PROJECT_MAX_CLIPS];};
void xtc_clipboard_destroy(xtc_clipboard *b)
{
if(b){xtc_project_destroy(b->source);free(b);}
}
size_t xtc_clipboard_count(const xtc_clipboard *b){return b ? b->count : 0;}
xtc_result xtc_clipboard_create(const xtc_project *p,const uint64_t *ids,size_t count,xtc_clipboard **out)
{
xtc_clipboard *b;size_t i,j;xtc_result r;
if(!out)return XTC_ERR_INVALID_ARGUMENT;
r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
b=calloc(1,sizeof(*b));if(!b)return XTC_ERR_OUT_OF_MEMORY;
r=xtc_project_clone(p,&b->source);
if(r!=XTC_OK){free(b);return r;}
b->count=count;b->begin=XTC_PROJECT_MAX_TICK;b->first_track=p->track_count;
memcpy(b->ids,ids,count*sizeof(*ids));
for(i=0;i<count;++i){xtc_clip_info c;xtc_project_find_clip(p,ids[i],&c);
if(c.start_tick<b->begin)b->begin=c.start_tick;
for(j=0;j<p->track_count;++j)if(p->tracks[j].id==c.track_id && j<b->first_track)b->first_track=j;
}
*out=b;return XTC_OK;
}
xtc_result xtc_clipboard_paste(const xtc_clipboard *b,xtc_project *p,uint64_t track,uint64_t start,uint64_t *out_ids)
{
xtc_project *copy=NULL;size_t base,i,j,k;uint64_t output[XTC_PROJECT_MAX_CLIPS];xtc_result r;
if(!b || !p || start>XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
for(base=0;base<p->track_count;++base)if(p->tracks[base].id==track)break;
if(base==p->track_count)return XTC_ERR_NOT_FOUND;
r=xtc_project_clone(p,&copy);
for(i=0;r==XTC_OK && i<b->count;++i) {
xtc_clip_info c;uint64_t id,target;xtc_project_find_clip(b->source,b->ids[i],&c);
for(j=0;j<b->source->track_count;++j)if(b->source->tracks[j].id==c.track_id)break;
k=base+j-b->first_track;
if(k>=copy->track_count){r=XTC_ERR_NOT_FOUND;break;}
if(copy->tracks[k].is_bus){r=XTC_ERR_INVALID_STATE;break;}
if(c.start_tick-b->begin>XTC_PROJECT_MAX_TICK-start){r=XTC_ERR_INVALID_ARGUMENT;break;}
target=start+c.start_tick-b->begin;
r=xtc_project_add_clip(copy,copy->tracks[k].id,c.name,target,c.length_tick,&id);
if(r!=XTC_OK)break;
c.id=id;c.track_id=copy->tracks[k].id;c.start_tick=target;copy->clips[copy->clip_count-1]=c;output[i]=id;
if(c.midi)r=xtc_project_ensure_midi_route(copy);
for(j=0;r==XTC_OK && j<b->source->note_count;++j)if(b->source->notes[j].clip_id==b->ids[i]) {
xtc_note_info n=b->source->notes[j];uint64_t note;
r=xtc_project_add_note(copy,id,n.start_tick,n.length_tick,n.pitch,n.velocity,n.channel,&note);
}
}
if(r==XTC_OK){publish(p,copy);if(out_ids)memcpy(out_ids,output,b->count*sizeof(*output));}
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_repeat_clips(xtc_project *p,const uint64_t *ids,size_t count,unsigned int copies)
{
xtc_project *copy=NULL;size_t i;unsigned int k;uint64_t begin=XTC_PROJECT_MAX_TICK,end=0,span;
xtc_result r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
if(!copies || copies>XTC_PROJECT_MAX_CLIPS)return XTC_ERR_INVALID_ARGUMENT;
if(count>(XTC_PROJECT_MAX_CLIPS-p->clip_count)/copies)return XTC_ERR_CAPACITY;
for(i=0;i<count;++i){xtc_clip_info c;xtc_project_find_clip(p,ids[i],&c);
if(c.start_tick<begin)begin=c.start_tick;
if(c.start_tick+c.length_tick>end)end=c.start_tick+c.length_tick;
}
span=end-begin;
if(span>(XTC_PROJECT_MAX_TICK-end)/copies)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_clone(p,&copy);
for(k=1;r==XTC_OK && k<=copies;++k)for(i=0;r==XTC_OK && i<count;++i) {
xtc_clip_info c;uint64_t id;xtc_project_find_clip(p,ids[i],&c);
r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,c.start_tick+span*k,&id);
}
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}
static uint64_t swung_start(uint64_t start,const xtc_midi_transform *edit)
{
uint64_t base=start/edit->grid,best=0,distance=UINT64_MAX;int offset;
for(offset=-2;offset<=2;++offset) {
int64_t index=(int64_t)base+offset;uint64_t candidate,d;
if(index<0)continue;
candidate=(uint64_t)index*edit->grid;
if(index%2)candidate+=edit->grid*edit->swing/200;
d=start>candidate ? start-candidate : candidate-start;
if(d<distance || (d==distance && candidate>best)){distance=d;best=candidate;}
}
return (start*(100-edit->strength)+best*edit->strength+50)/100;
}
xtc_result xtc_project_transform_midi(xtc_project *p,const uint64_t *ids,size_t count,const xtc_midi_transform *edit)
{
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_HUMANIZE)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_HUMANIZE && (edit->timing_variation>960 || edit->velocity_variation>127))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,&copy);if(r!=XTC_OK)return r;
for(i=0;r==XTC_OK && i<copy->note_count;++i) {
xtc_note_info *n=copy->notes+i;xtc_clip_info c;
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);
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 if(edit->type==XTC_MIDI_TRANSFORM_LEGATO) {
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;
} else if(edit->type==XTC_MIDI_TRANSFORM_SCALE) {
static const unsigned int masks[]={4095,2741,1453};
unsigned int pitch,best=n->pitch,distance=128;
for(pitch=0;pitch<128;++pitch)if(masks[edit->scale] & (1U<<((pitch+12-edit->root)%12))) {
unsigned int d=pitch>n->pitch ? pitch-n->pitch : n->pitch-pitch;
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(edit->type==XTC_MIDI_TRANSFORM_HUMANIZE) {
uint32_t random=(uint32_t)(n->id^(n->id>>32))^(uint32_t)edit->seed^UINT32_C(0x9e3779b9);
int64_t start;int velocity;
random^=random<<13;random^=random>>17;random^=random<<5;
start=(int64_t)n->start_tick+(int64_t)(random%(2*edit->timing_variation+1))-(int64_t)edit->timing_variation;
random^=random<<13;random^=random>>17;random^=random<<5;
velocity=(int)n->velocity+(int)(random%(2*edit->velocity_variation+1))-(int)edit->velocity_variation;
if(start<0){r=XTC_ERR_INVALID_ARGUMENT;break;}
n->start_tick=(uint64_t)start;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;
}
if(r==XTC_OK && !matched)r=XTC_ERR_NOT_FOUND;
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}
struct xtc_mix_snapshot {size_t count;xtc_track_info tracks[XTC_PROJECT_MAX_TRACKS];};
void xtc_mix_snapshot_destroy(xtc_mix_snapshot *snapshot){free(snapshot);}
xtc_result xtc_mix_snapshot_create(const xtc_project *p,xtc_mix_snapshot **out)
{
xtc_mix_snapshot *snapshot;
if(!p || !out)return XTC_ERR_INVALID_ARGUMENT;
snapshot=calloc(1,sizeof(*snapshot));if(!snapshot)return XTC_ERR_OUT_OF_MEMORY;
snapshot->count=p->track_count;memcpy(snapshot->tracks,p->tracks,p->track_count*sizeof(*p->tracks));
*out=snapshot;return XTC_OK;
}
xtc_result xtc_mix_snapshot_recall(const xtc_mix_snapshot *snapshot,xtc_project *p)
{
xtc_project *copy=NULL;size_t i;xtc_result r;
if(!snapshot || !p)return XTC_ERR_INVALID_ARGUMENT;
if(snapshot->count!=p->track_count)return XTC_ERR_INVALID_STATE;
r=xtc_project_clone(p,&copy);
for(i=0;r==XTC_OK && i<snapshot->count;++i) {
const xtc_track_info *track=snapshot->tracks+i;
r=xtc_project_set_mix(copy,track->id,track->gain,track->pan,track->mute,track->solo);
}
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_duplicate_track(xtc_project *p,uint64_t track,const char *name,uint64_t *out)
{
xtc_project *copy=NULL;size_t i,index;uint64_t id=0;xtc_result r;
if(!p || !out || !xtc_project_valid_name(name))return XTC_ERR_INVALID_ARGUMENT;
for(index=0;index<p->track_count;++index)if(p->tracks[index].id==track)break;
if(index==p->track_count)return XTC_ERR_NOT_FOUND;
if(p->tracks[index].is_bus)return XTC_ERR_INVALID_STATE;
r=xtc_project_clone(p,&copy);if(r!=XTC_OK)return r;
r=xtc_project_add_track(copy,name,&id);
if(r==XTC_OK){xtc_track_info *target=copy->tracks+copy->track_count-1;*target=p->tracks[index];target->id=id;memcpy(target->name,name,strlen(name)+1);copy->instruments[copy->track_count-1]=p->instruments[index];}
for(i=0;r==XTC_OK && i<p->clip_count;++i)if(p->clips[i].track_id==track){uint64_t clip;r=xtc_project_duplicate_clip(copy,p->clips[i].id,id,p->clips[i].start_tick,&clip);}
for(i=0;r==XTC_OK && i<p->automation_count;++i)if(p->automation[i].track_id==track){xtc_automation_point point=p->automation[i];point.track_id=id;r=xtc_project_set_automation(copy,&point);}
if(r==XTC_OK)r=xtc_project_move_track(copy,id,index+1);
if(r==XTC_OK){publish(p,copy);*out=id;}
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_split_midi(xtc_project *p,uint64_t clip,uint64_t tick,uint64_t *out)
{
xtc_clip_info c;xtc_project *copy=NULL;uint64_t id=0,left;xtc_result r;
if(!p || !out)return XTC_ERR_INVALID_ARGUMENT;
r=xtc_project_find_clip(p,clip,&c);if(r!=XTC_OK)return r;
if(!c.midi || tick<=c.start_tick || tick>=c.start_tick+c.length_tick)return XTC_ERR_INVALID_ARGUMENT;
left=tick-c.start_tick;r=xtc_project_clone(p,&copy);if(r!=XTC_OK)return r;
r=xtc_project_duplicate_clip(copy,clip,c.track_id,c.start_tick,&id);
if(r==XTC_OK)r=xtc_project_edit_clip(copy,id,c.track_id,tick,c.length_tick-left,c.offset_tick+left);
if(r==XTC_OK)r=xtc_project_edit_clip(copy,clip,c.track_id,c.start_tick,left,c.offset_tick);
if(r==XTC_OK){publish(p,copy);*out=id;}
xtc_project_destroy(copy);return r;
}
xtc_result xtc_project_merge_midi(xtc_project *p,const uint64_t *ids,size_t count,uint64_t *out)
{
xtc_project *copy=NULL;size_t i,j;uint64_t begin=XTC_PROJECT_MAX_TICK,end=0,track=UINT64_MAX,id=0;
xtc_result r=selection_valid(p,ids,count);if(r!=XTC_OK)return r;
if(count<2 || !out)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 || (i && c.track_id!=track))return XTC_ERR_INVALID_STATE;
track=c.track_id;if(c.start_tick<begin)begin=c.start_tick;if(c.start_tick+c.length_tick>end)end=c.start_tick+c.length_tick;
}
r=xtc_project_clone(p,&copy);if(r!=XTC_OK)return r;
for(i=0;r==XTC_OK && i<count;++i)r=xtc_project_remove_clip(copy,ids[i]);
if(r==XTC_OK)r=xtc_project_add_midi_clip(copy,track,begin,end-begin,&id);
if(r==XTC_OK)r=xtc_project_rename_clip(copy,id,"Consolidated MIDI");
for(i=0;r==XTC_OK && i<p->note_count;++i) {
const xtc_note_info *n=p->notes+i;xtc_clip_info c;uint64_t a,b,note;
for(j=0;j<count;++j)if(n->clip_id==ids[j])break;
if(j==count)continue;
xtc_project_find_clip(p,n->clip_id,&c);
a=n->start_tick>c.offset_tick ? n->start_tick : c.offset_tick;
b=n->start_tick+n->length_tick<c.offset_tick+c.length_tick ? n->start_tick+n->length_tick : c.offset_tick+c.length_tick;
if(b>a)r=xtc_project_add_note(copy,id,c.start_tick-begin+a-c.offset_tick,b-a,n->pitch,n->velocity,n->channel,&note);
}
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,&copy);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;
}
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,&copy);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,&note);
}
}
if(r==XTC_OK && !matched)r=XTC_ERR_NOT_FOUND;
if(r==XTC_OK)publish(p,copy);
xtc_project_destroy(copy);return r;
}