#include "sequencer_widget.hpp" #include "snap.hpp" #include "xtc/xtc_midi.h" #include #include #include #include #include #include // User-facing editing commands use the master's single history/publication path. xtc_result SequencerWidget::add_track() { if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } uint64_t id; xtc_result r = xtc_project_add_track(project_, "Track", &id); if (r == XTC_OK) { track_ = id; clip_ = UINT64_MAX;selection_.clear(); } changed(r); return r; } xtc_result SequencerWidget::add_region() { if (before_edit) { xtc_result r = before_edit(context_); if (r != XTC_OK) { changed(r); return r; } } size_t tracks, clips; xtc_project_get_counts(project_, &tracks, &clips); if (track_ == UINT64_MAX && tracks) { xtc_track_info t{}; xtc_project_get_track(project_, 0, &t); track_ = t.id; } uint64_t id; xtc_result r = xtc_project_add_clip(project_, track_, "Region (silent)", cursor_, new_clip_ticks, &id); if (r == XTC_OK) {clip_ = id;selection_.clear();} changed(r); return r; } xtc_result SequencerWidget::add_midi_clip() { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; uint64_t id; if (r == XTC_OK) { if (track_ == UINT64_MAX) { xtc_track_info t{}; r = xtc_project_get_track(project_, 0, &t); track_ = r == XTC_OK ? t.id : UINT64_MAX; } if (r == XTC_OK) r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id); if (r == XTC_OK) {clip_ = id;selection_.clear();} } changed(r); return r; } xtc_result SequencerWidget::delete_selection() { if(!selected_clips().empty())return transform_selection(XTC_CLIPS_DELETE,0); xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_remove_track(project_,track_); if(r==XTC_OK)track_=clip_=UINT64_MAX; changed(r);return r; } xtc_result SequencerWidget::merge_midi() { auto ids=selected_clips();uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_merge_midi(project_,ids.data(),ids.size(),&id); if(r==XTC_OK){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);select(c.track_id,id);} changed(r);return r; } xtc_result SequencerWidget::duplicate_track(const char *name) { const uint64_t source=track_;uint64_t id=0;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_duplicate_track(project_,source,name,&id); if(r==XTC_OK)select(id); changed(r);return r; } void SequencerWidget::duplicate_track_dialog() { const char *name=fl_input("Name for complete track duplicate","Track copy");if(name)duplicate_track(name); } xtc_result SequencerWidget::ripple_time(uint64_t length,bool remove) { const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_ripple_time(project_,at,length,remove); changed(r);return r; } void SequencerWidget::ripple_dialog(bool remove) { const char *input=fl_input(remove ? "Remove time from cursor: ticks (split crossing clips first)" : "Insert time at cursor: ticks (split crossing clips first)","3840");if(!input)return; uint64_t length=0;char extra;std::istringstream values(input); if(!(values>>length) || (values>>extra) || !length || length>XTC_PROJECT_MAX_TICK){fl_alert("Enter a positive bounded tick duration.");return;} ripple_time(length,remove); } xtc_result SequencerWidget::marker(const char *name) { const uint64_t at=cursor_;xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=name ? xtc_project_set_marker(project_,at,name) : xtc_project_remove_marker(project_,at); changed(r);return r; } void SequencerWidget::marker_dialog() { const char *name=fl_input("Marker at cursor (1..63 UTF-8 bytes)","Section");if(name)marker(name); } std::vector SequencerWidget::selected_clips() const { std::vector ids;xtc_clip_info c{}; for(auto id:selection_)if(xtc_project_find_clip(project_,id,&c)==XTC_OK)ids.push_back(id); if(ids.empty() && clip_!=UINT64_MAX && xtc_project_find_clip(project_,clip_,&c)==XTC_OK)ids.push_back(clip_); return ids; } bool SequencerWidget::clip_selected(uint64_t id) const { return selection_.empty() ? id==clip_ : std::find(selection_.begin(),selection_.end(),id)!=selection_.end(); } bool SequencerWidget::toggle_clip(uint64_t id) { xtc_clip_info c{};if(xtc_project_find_clip(project_,id,&c)!=XTC_OK)return false; if(selection_.empty() && clip_!=UINT64_MAX)selection_.push_back(clip_); auto found=std::find(selection_.begin(),selection_.end(),id); if(found==selection_.end())selection_.push_back(id);else selection_.erase(found); clip_=selection_.empty() ? UINT64_MAX : selection_.back(); if(clip_!=UINT64_MAX)xtc_project_find_clip(project_,clip_,&c); track_=c.track_id;redraw();return true; } void SequencerWidget::select_all_clips() { selection_.clear();size_t tracks=0,clips=0;xtc_project_get_counts(project_,&tracks,&clips); for(size_t i=0;i output(ids.size()); xtc_result r=ids.empty() ? XTC_ERR_NOT_FOUND : XTC_OK; if(r==XTC_OK && before_edit)r=before_edit(context_); if(r==XTC_OK)r=xtc_project_transform_clips(project_,ids.data(),ids.size(),op,amount,output.data()); if(r==XTC_OK) { selection_=op==XTC_CLIPS_DELETE ? std::vector{} : output; clip_=selection_.empty() ? UINT64_MAX : selection_.back(); } changed(r);return r; } xtc_result SequencerWidget::transform_midi(const xtc_midi_transform &edit) { auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_transform_midi(project_,ids.data(),ids.size(),&edit); changed(r);return r; } void SequencerWidget::fades_dialog() { const char *input=fl_input("Symmetric fade length in ticks (0 removes; at most half each clip)","10");if(!input)return; int64_t ticks=0;char extra;std::istringstream values(input); if(!(values>>ticks) || (values>>extra) || ticks<0 || ticks>static_cast(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 &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; } 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; } xtc_result SequencerWidget::normalize_audio(float target) { auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_normalize_audio(project_,ids.data(),ids.size(),target); changed(r);return r; } void SequencerWidget::normalize_dialog() { const char *input=fl_input("Source sample peak target in dBFS (-60..0; excludes fades and mixer)","-1");if(!input)return; float db=0;char extra;std::istringstream values(input); if(!(values>>db) || (values>>extra) || !std::isfinite(db) || db < -60 || db>0){fl_alert("Enter a finite sample peak target from -60 to 0 dBFS.");return;} normalize_audio(std::pow(10.0f,db/20.0f)); } 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; std::istringstream values(input);std::vector 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; unsigned int timing=0,velocity=0,seed=0;char extra;std::istringstream values(input); if(!(values>>timing>>velocity>>seed) || (values>>extra) || timing>960 || velocity>127){fl_alert("Enter bounded timing/velocity variation and a 32-bit seed.");return;} xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_HUMANIZE;edit.timing_variation=timing;edit.velocity_variation=velocity;edit.seed=seed;transform_midi(edit); } 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; unsigned int root=0,scale=0;char extra;std::istringstream values(input); if(!(values>>root>>scale) || (values>>extra) || root>11 || scale>2){fl_alert("Enter a root from 0 to 11 and scale from 0 to 2.");return;} xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.root=root;edit.scale=scale;transform_midi(edit); } 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; int grid=0,swing=-1,strength=-1;char extra; std::istringstream values(input); if(!(values>>grid>>swing>>strength) || (values>>extra) || grid<1 || grid>3840 || swing<0 || swing>100 || strength<0 || strength>100){fl_alert("Enter a valid grid, swing and strength.");return;} xtc_midi_transform edit{};edit.type=XTC_MIDI_TRANSFORM_SWING;edit.grid=static_cast(grid);edit.swing=static_cast(swing);edit.strength=static_cast(strength);transform_midi(edit); } xtc_result SequencerWidget::repeat_selection(unsigned int copies) { auto ids=selected_clips();xtc_result r=before_edit ? before_edit(context_) : XTC_OK; if(r==XTC_OK)r=xtc_project_repeat_clips(project_,ids.data(),ids.size(),copies); changed(r);return r; } void SequencerWidget::repeat_dialog() { const char *input=fl_input("Additional phrase copies (1..1024)","3");if(!input)return; char *end=nullptr;unsigned long copies=std::strtoul(input,&end,10); if(end==input || *end || copies<1 || copies>1024){fl_alert("Enter a whole number from 1 to 1024.");return;} repeat_selection(static_cast(copies)); } xtc_result SequencerWidget::copy_selection() { auto ids=selected_clips();xtc_clipboard *copy=nullptr; xtc_result r=xtc_clipboard_create(project_,ids.data(),ids.size(),©); if(r==XTC_OK)clipboard_.reset(copy); else fl_alert("Copy unchanged: %s",xtc_result_string(r)); return r; } xtc_result SequencerWidget::paste_selection() { const uint64_t start=cursor_,track=track_; std::vector output(xtc_clipboard_count(clipboard_.get())); xtc_result r=output.empty() ? XTC_ERR_INVALID_STATE : XTC_OK; if(r==XTC_OK && before_edit)r=before_edit(context_); if(r==XTC_OK)r=xtc_clipboard_paste(clipboard_.get(),project_,track,start,output.data()); if(r==XTC_OK){selection_=output;clip_=selection_.back();xtc_clip_info c{};xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;} changed(r);return r; } xtc_result SequencerWidget::nudge_selection(int direction) { return transform_selection(XTC_CLIPS_MOVE,direction<0 ? -static_cast(grid_) : static_cast(grid_)); } xtc_result SequencerWidget::duplicate_selection() { auto ids=selected_clips();uint64_t begin=XTC_PROJECT_MAX_TICK,end=0; for(auto id:ids){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);begin=std::min(begin,c.start_tick);end=std::max(end,c.start_tick+c.length_tick);} return transform_selection(XTC_CLIPS_DUPLICATE,static_cast(end)-static_cast(begin)); } xtc_result SequencerWidget::rename_selection(const char *name) { xtc_result r = before_edit ? before_edit(context_) : XTC_OK; if (r == XTC_OK) r = clip_ != UINT64_MAX ? xtc_project_rename_clip(project_,clip_,name) : xtc_project_rename_track(project_,track_,name); changed(r); return r; } xtc_result SequencerWidget::move_track(int direction) { size_t tracks, clips, i; xtc_project_get_counts(project_,&tracks,&clips); xtc_track_info track{}; for (i = 0; i < tracks; ++i) { xtc_project_get_track(project_,i,&track); if (track.id == track_) break; } if (i == tracks || (direction < 0 ? i == 0 : i+1 == tracks)) return XTC_ERR_NOT_FOUND; xtc_result r = before_edit ? before_edit(context_) : XTC_OK; if (r == XTC_OK) r = xtc_project_move_track(project_,track_,direction < 0 ? i-1 : i+1); changed(r); return r; } xtc_result SequencerWidget::split_selection() { uint64_t right; const uint64_t split_tick = cursor_; // Stop/status refresh may move the playhead. xtc_result result = before_edit ? before_edit(context_) : XTC_OK; if (result == XTC_OK) { xtc_clip_info c{};result=xtc_project_find_clip(project_,clip_,&c); if(result==XTC_OK)result=c.midi ? xtc_project_split_midi(project_,clip_,split_tick,&right) : xtc_project_split_audio(project_,clip_,split_tick,&right); } if (result == XTC_OK) {clip_ = right;selection_.clear();} changed(result); return result; } xtc_result SequencerWidget::audio_properties(float gain, uint64_t in, uint64_t out) { xtc_result result = before_edit ? before_edit(context_) : XTC_OK; if (result == XTC_OK) result = xtc_project_set_clip_audio(project_, clip_, gain, in, out); changed(result); return result; } void SequencerWidget::audio_properties_dialog() { xtc_clip_info clip{}; if (xtc_project_find_clip(project_, clip_, &clip) != XTC_OK || clip.midi) { fl_alert("Select an audio clip or silent region first."); return; } char defaults[128]; std::snprintf(defaults, sizeof(defaults), "%.9g %llu %llu", static_cast(clip.gain), static_cast(clip.fade_in_tick), static_cast(clip.fade_out_tick)); const char *text = fl_input("Linear gain (0..4), fade-in ticks, fade-out ticks (0 disables)", defaults); if (!text) return; std::istringstream input(text); float gain; uint64_t in, out; if (!(input >> gain >> in >> out) || !(input >> std::ws).eof()) { fl_alert("Enter gain and two fade durations in ticks."); return; } audio_properties(gain, in, out); } xtc_result SequencerWidget::crossfade_selection() { auto ids=selected_clips();xtc_result r=ids.size()==2 ? XTC_OK : XTC_ERR_INVALID_ARGUMENT; if(r==XTC_OK && before_edit)r=before_edit(context_); if(r==XTC_OK)r=xtc_project_crossfade(project_,ids[0],ids[1]); changed(r);return r; } xtc_result SequencerWidget::strum_chords(uint64_t spacing,bool descending) { auto ids=selected_clips();xtc_result r=XTC_OK; if(before_edit)r=before_edit(context_); if(r==XTC_OK)r=xtc_project_strum(project_,ids.data(),ids.size(),spacing,descending ? 1 : 0); changed(r);return r; } void SequencerWidget::strum_dialog() { const char *input=fl_input("Spacing ticks (1..960), direction (0=up, 1=down)","30 0");if(!input)return; uint64_t spacing=0;int down=0;char extra;std::istringstream values(input); if(!(values>>spacing>>down) || (values>>extra) || !spacing || spacing>960 || down<0 || down>1){fl_alert("Enter a valid spacing and direction.");return;} strum_chords(spacing,down!=0); }