xtc/src/gui/sequencer_commands.cpp

412 lines
18 KiB
C++
Raw Normal View History

#include "sequencer_widget.hpp"
#include "snap.hpp"
#include "xtc/xtc_midi.h"
#include <FL/fl_ask.H>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <new>
// 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<uint64_t> SequencerWidget::selected_clips() const
{
std::vector<uint64_t> 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<clips;++i){xtc_clip_info c{};xtc_project_get_clip(project_,i,&c);selection_.push_back(c.id);}
if(!selection_.empty()){xtc_clip_info c{};clip_=selection_.back();xtc_project_find_clip(project_,clip_,&c);track_=c.track_id;}
redraw();
}
xtc_result SequencerWidget::grid(uint64_t ticks)
{
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
grid_ = ticks; redraw(); return XTC_OK;
}
xtc_result SequencerWidget::transform_selection(xtc_clip_operation op,int64_t amount)
{
auto ids=selected_clips();std::vector<uint64_t> 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<uint64_t>{} : 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<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;
}
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<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;
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<uint64_t>(grid);edit.swing=static_cast<unsigned int>(swing);edit.strength=static_cast<unsigned int>(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<unsigned int>(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(),&copy);
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<uint64_t> 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<int64_t>(grid_) : static_cast<int64_t>(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<int64_t>(end)-static_cast<int64_t>(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<double>(clip.gain),
static_cast<unsigned long long>(clip.fade_in_tick), static_cast<unsigned long long>(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);
}