feat: compare mixer A/B snapshots with undo and qualify modern editing workflows

This commit is contained in:
Johannes Findeisen 2026-11-06 00:25:23 +01:00
commit 1b668068de
14 changed files with 261 additions and 34 deletions

View file

@ -117,8 +117,10 @@ track header to add a one-bar region there at the cursor. Select a region to
delete it; deleting a selected track cascades to its regions. Delete also works
with canvas focus. Drag a region body to move it in time or between tracks;
drag either edge to trim within its original source bounds. Edits commit on
release with 1/16-note snapping; Escape cancels. There is no drag preview, undo,
multiselection, rename or tempo editor yet. Overlapping regions are permitted.
release with the selected snap grid; Escape cancels. There is no drag preview.
Undo, rename, tempo editing and additive clip selection are supported. Group
commands, clipboard and musical transforms are described in
[MODERN_EDITING.md](MODERN_EDITING.md). Overlapping regions are permitted.
The scrollable canvas expands to cover the arrangement plus a margin; zoom
ranges from 24 to 384 pixels/quarter. Names, tempo and 4/4 meter are drawn.

View file

@ -163,8 +163,10 @@ does not allocate. C clients using unreserved history record must handle errors.
With mixer focus, M/S toggles mute/solo, Left/Right changes gain by 0.05,
Shift+Left/Right changes pan, and Up/Down selects a track. Native sliders also
support keyboard navigation; Ctrl+1/2/3 switches views, Ctrl+4 opens Piano roll,
F5/F8/Space controls transport and F9 panics, as before. There is no multiselection,
clipboard editing, track rename or automation-point dragging yet.
F5/F8/Space controls transport and F9 panics, as before. Mixer channels remain
singly selected; automation-point dragging is not implemented. Track rename and
arrangement clipboard editing are available. See MODERN_EDITING.md for static
mixer A/B comparison and its automation limits.
## Verification and limits

View file

@ -4,42 +4,68 @@ These control-thread edits preserve project format 15. Session selections,
clipboard and comparison snapshots are not saved in projects. Audio callbacks
continue to consume prepared plans; edits use the existing stop/history workflow.
## Phase 47
## Phase 47 — Arrangement Multi-selection
Shift-click clips to toggle selection; Ctrl+A in the sequencer or Edit/Select all clips selects every clip. Plain clicking replaces the selection. Selection is session UI state; no marquee selection yet.
## Phase 48
## Phase 48 — Transactional Group Editing
Use Left/Right in the sequencer (or Edit/Nudge selection) to move the selected phrase by the current snap step. Ctrl+D duplicates after its complete bounding span; Delete removes selected clips. Edits are atomic and undoable. Mouse dragging remains a single-clip gesture.
## Phase 49
## Phase 49 — Arrangement Clipboard
Ctrl+C/Ctrl+V in the sequencer, or Edit/Copy clips and Paste clips at cursor, copy/paste the selection. Earliest clip lands on the cursor. The earliest source track maps to the currently selected destination track; intervening track spacing is retained. All required tracks must already exist and must not be buses. Instruments, automation and media bytes are not copied. The clipboard survives document changes within the window but is not the OS clipboard.
## Phase 50
## Phase 50 — Repeat Arrangement Patterns
Edit/Repeat selected phrase asks for 1..1024 additional copies. The bounding span includes gaps and overlaps; copies remain on their original tracks. The entire repeat is one undo step. Clip/note capacity and timeline endpoints can limit the requested count.
## Phase 51
## Phase 51 — Align and Slip Editing
Edit/Align clip starts to cursor places every selected start at the cursor. Slip sources earlier/later changes source offsets by the snap step while retaining timeline positions and durations. Trimming must leave source material available; an invalid slip rejects the whole selection. No time stretching or destructive media edits are performed.
## Phase 52
## Phase 52 — MIDI Swing Quantization
Select MIDI clips and use Edit/MIDI tools/Swing quantize. Grid is in source ticks; swing delays alternate subdivisions by 0..half a grid step, while strength blends the old start toward the nearest swung point. Durations stay fixed. All source notes are affected, including notes outside visible trims. Mixed audio/MIDI selections and notes that would exceed source bounds reject the entire edit. This is destructive note editing with undo, not a live groove engine.
## Phase 53
## Phase 53 — MIDI Legato and Gate Tools
Edit/MIDI tools/Note gate scales lengths from 1 to 400 percent, rounded to at least one tick. Legato ends notes at the next strictly later onset in the same clip and channel; notes in a chord end together. The final onset extends to source end. Gate expansion beyond the source rejects the whole edit. Undo restores original durations.
## Phase 54
## Phase 54 — MIDI Scale Correction
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
## Phase 55 — MIDI Velocity Shaping
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.
## Phase 56
## Phase 56 — Batch Audio Fade Tools
Edit/Audio tools/Set boundary fades sets linear fade-in and fade-out lengths in ticks; 0 or Remove boundary fades clears them. Each fade must fit within half of every selected clip duration. Gain and source offsets stay unchanged. These are boundary envelopes, not overlap-aware crossfades, equal-power curves or destructive WAV processing.
## Phase 57 — Mixer A/B Comparison
Edit/Mixer comparison captures or recalls A and B. Capture stores static gain, pan, mute and solo for every track and bus without changing the document. Recall requires the same stable track IDs, stops playback through the normal edit guard and creates one undo step. Track order may change. Routing, plugins and automation are excluded; automation still overrides static gain and pan. Slots are session-only and clear on New/Open. This is static mix comparison, not seamless live scene morphing.
## Validation and remaining scope
Each phase was built with warnings enabled and tested before its implementation
commit. Reproduce the core, transaction and allocation-failure checks with:
```sh
make BUILD_DIR=build/roadmap-audit all gui test-reliability
make BUILD_DIR=build/roadmap-audit test-gui-modern-editing
```
The full GUI, instrument/effect plugin fixtures, software JACK lifecycle, Clang
static analysis, AddressSanitizer/UBSan and staged default/custom installation
checks also cover the integrated changes. GUI leak checks use only the existing
system Fontconfig suppression. Save/reload comparisons render identical nonzero
MIDI output after musical transforms; no project format migration is required.
These phases improve arrangement and musical editing workflows. They do not
provide DAW feature parity: time stretching, comping, a clip launcher, tempo maps,
track insert chains and customizable shortcuts need separate designs. No new
dependencies or audio-callback operations are introduced. Human listening,
physical devices and arbitrary third-party plugins remain unqualified.

View file

@ -1065,9 +1065,9 @@ track in other views. Command search lists active menu actions; it is not a
plugin scanner or a key-binding editor. See WORKSPACE.md.
### Future usability work
Arrangement multiselection, independent track inserts and customizable shortcuts
remain future work. Dockable editor panels and resizable pinned track headers
are implemented in Phase 46.
Independent track inserts and customizable shortcuts remain future work.
Dockable editors and pinned headers are implemented in Phase 46; arrangement
multiselection and phrase editing follow in Phases 47–51.
---
@ -1468,12 +1468,26 @@ Implemented: selection-wide symmetric boundary fade and remove-fade tools preser
Capture and recall two session mixer snapshots by stable track ID, with transactional validation, undo and explicit automation limitations.
### 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: two independent GUI mixer comparison slots and public C snapshot capture/recall. Recall validates the stable track ID set, tolerates reorder and creates one undoable edit; New/Open clears slots. Core rollback/reorder, GUI recall/undo/document lifecycle, allocation-failure, persistence/render, sanitizer, static-analysis and installation checks qualify the integrated work. See MODERN_EDITING.md for scope and validation.
---
### Integrated validation for Phases 47–57
Each phase has a separate implementation commit and warning-enabled model/GUI
checks. The final suite includes allocation-failure rollback and ID preservation,
clipboard track mapping, note/source bounds, grouped undo, persistence/render
comparison, the complete GUI/plugin fixture matrix and software JACK lifecycle.
ASan/UBSan, Clang analysis and both staged installation layouts cover integration.
Project format remains 15; no new dependencies or callback work were introduced.
See [MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and remaining DAW
features. Human listening, physical hardware and arbitrary third-party plugins
remain unqualified.
---

View file

@ -211,3 +211,8 @@ Phase 34: use the sequencer Snap dropdown for straight/triplet/bar grids or Off.
Double-click MIDI clips to edit notes, then use the Velocity lane or selected-note
slider for dynamics. Each non-bus track header has an instrument dropdown offering
built-in sine, project instruments and explicit plugin loading. See WORKSPACE.md.
Phases 47–57 add additive clip selection, atomic group commands, phrase clipboard
and repeat, alignment/slip tools, MIDI swing/gate/legato/scale/velocity transforms,
batch audio fades and static mixer A/B recall. See
[MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and limitations.

View file

@ -230,3 +230,8 @@ GUI regressions cover repeated detach/dock and close-to-dock, toolbar ownership,
minimum/large resize, shared model/note/clip selection, scrolling, header dragging,
short-clip hit tests, Apply/restart, reset layout and failed/successful shutdown.
ASan/UBSan checks retain only the existing system Fontconfig leak suppression.
Phases 47–57 add additive clip selection, atomic group commands, phrase clipboard
and repeat, alignment/slip tools, MIDI swing/gate/legato/scale/velocity transforms,
batch audio fades and static mixer A/B recall. See
[MODERN_EDITING.md](MODERN_EDITING.md) for commands, scope and limitations.

View file

@ -19,11 +19,13 @@ xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids);
/* 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. No matching notes returns NOT_FOUND. Edits are atomic.
* fields persist. Initialize the edit struct to zero and set the fields for its
* type; other fields are ignored. No matching notes returns NOT_FOUND. Edits are atomic.
* Swing: grid 1..3840, swing 0..100 delays alternate grid points by up to half
* 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. */
* 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_VELOCITY } xtc_midi_transform_type;
typedef struct {
xtc_midi_transform_type type;
@ -35,6 +37,15 @@ typedef struct {
unsigned int gate_percent; /* GATE: 1..400, rounded length, minimum 1 tick. */
} xtc_midi_transform;
xtc_result xtc_project_transform_midi(xtc_project *project,const uint64_t *ids,size_t count,const xtc_midi_transform *edit);
/* Session static mixer comparison. Capture owns an independent snapshot of
* gain/pan/mute/solo for all tracks/buses. Recall requires the exact same stable
* track ID set (order may change), and publishes atomically. Routing, automation,
* instrument/plugin state are excluded; automation still overrides static gain
* and pan. Snapshots are not serialized. out is unchanged on capture failure. */
typedef struct xtc_mix_snapshot xtc_mix_snapshot;
xtc_result xtc_mix_snapshot_create(const xtc_project *project,xtc_mix_snapshot **out);
void xtc_mix_snapshot_destroy(xtc_mix_snapshot *snapshot);
xtc_result xtc_mix_snapshot_recall(const xtc_mix_snapshot *snapshot,xtc_project *project);
/* Repeat the bounding phrase span, including gaps, 1..1024 additional copies.
* Originals remain selected by the caller; all copies publish together. */
xtc_result xtc_project_repeat_clips(xtc_project *project,const uint64_t *ids,size_t count,unsigned int copies);

View file

@ -133,6 +133,10 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
menu->add("Audio/MIDI controller mapping", 0, control_map_callback, this);
menu->add("Audio/Panic", FL_F + 9, panic_callback, this);
menu->add("Edit/Mixer comparison/Capture A",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(0,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this);
menu->add("Edit/Mixer comparison/Recall A",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(0,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this);
menu->add("Edit/Mixer comparison/Capture B",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(1,true);if(r!=XTC_OK)w->report_error("Mixer capture failed",r);},this);
menu->add("Edit/Mixer comparison/Recall B",0,[](Fl_Widget *,void *c){auto *w=static_cast<MainWindow *>(c);auto r=w->mix_snapshot(1,false);if(r!=XTC_OK)w->report_error("Mixer recall failed",r);},this);
menu->add("View/Commands",FL_CTRL+'p',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->show_commands();},this);
menu->add("View/Fit song",FL_CTRL+'0',[](Fl_Widget *,void *c){static_cast<MainWindow *>(c)->fit_song();},this);
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
@ -1374,8 +1378,10 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
piano_->set_project(candidate, UINT64_MAX);
project_.reset(candidate);
mixer_->set_project(candidate);
if (reset_history)
if (reset_history) {
history_.swap(new_history);
for(auto &snapshot:mix_snapshots_)snapshot.reset();
}
graph_widget_->reset_layout();
graph_widget_->set_graph(xtc_project_graph(candidate));
sequencer_->set_project(candidate);
@ -2539,6 +2545,20 @@ void MainWindow::inspector_callback(Fl_Widget *widget, void *context)
w->inspected_track_ = UINT64_MAX;
w->refresh_status();
}
xtc_result MainWindow::mix_snapshot(unsigned int slot,bool capture)
{
if(slot>=2)return XTC_ERR_INVALID_ARGUMENT;
if(capture) {
xtc_mix_snapshot *snapshot=nullptr;xtc_result r=xtc_mix_snapshot_create(project_.get(),&snapshot);
if(r==XTC_OK)mix_snapshots_[slot].reset(snapshot);
return r;
}
if(!mix_snapshots_[slot])return XTC_ERR_NOT_FOUND;
xtc_result r=before_arrangement_edit(this);
if(r==XTC_OK)r=xtc_mix_snapshot_recall(mix_snapshots_[slot].get(),project_.get());
if(r==XTC_OK)mark_dirty();
return r;
}
void MainWindow::show_commands()
{
std::vector<std::string> commands;

View file

@ -6,6 +6,7 @@
#include <vector>
#include <string>
#include "xtc/xtc_project.h"
#include "xtc/xtc_editing.h"
#include "xtc/xtc_backend.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
@ -35,6 +36,7 @@ class MainWindow : public Fl_Double_Window {
xtc_result create_track(bool midi);
xtc_result set_tempo(unsigned int milli_bpm);
void show_commands();
xtc_result mix_snapshot(unsigned int slot,bool capture);
/* Reparent the existing controls on the UI thread; closing a panel docks it. */
void detach_editor(unsigned int index,bool detached); /* 0 sequencer, 1 graph */
void detach_piano(bool detached);
@ -207,6 +209,8 @@ class MainWindow : public Fl_Double_Window {
xtc_result return_to_start();
private:
using MixSnapshot=std::unique_ptr<xtc_mix_snapshot,decltype(&xtc_mix_snapshot_destroy)>;
MixSnapshot mix_snapshots_[2]{{nullptr,xtc_mix_snapshot_destroy},{nullptr,xtc_mix_snapshot_destroy}};
struct EditorPanel {
MainWindow *owner=nullptr;unsigned int index=0;
Fl_Group *page=nullptr,*content=nullptr,*placeholder=nullptr;

View file

@ -147,7 +147,7 @@ xtc_result SequencerWidget::add_track()
xtc_result r = xtc_project_add_track(project_, "Track", &id);
if (r == XTC_OK) {
track_ = id;
clip_ = UINT64_MAX;
clip_ = UINT64_MAX;selection_.clear();
}
changed(r);
return r;
@ -175,8 +175,7 @@ xtc_result SequencerWidget::add_region()
}
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;
if (r == XTC_OK) {clip_ = id;selection_.clear();}
changed(r);
return r;
}
@ -197,8 +196,7 @@ xtc_result SequencerWidget::add_midi_clip()
}
if (r == XTC_OK)
r = xtc_project_add_midi_clip(project_, track_, cursor_, new_clip_ticks, &id);
if (r == XTC_OK)
clip_ = id;
if (r == XTC_OK) {clip_ = id;selection_.clear();}
}
changed(r);
return r;
@ -530,13 +528,15 @@ bool SequencerWidget::toggle_clip(uint64_t id)
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();track_=c.track_id;redraw();return true;
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())clip_=selection_.back();
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)
@ -626,7 +626,7 @@ xtc_result SequencerWidget::paste_selection()
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();}
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)
@ -662,7 +662,7 @@ xtc_result SequencerWidget::split_selection()
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) result = xtc_project_split_audio(project_, clip_, split_tick, &right);
if (result == XTC_OK) clip_ = right;
if (result == XTC_OK) {clip_ = right;selection_.clear();}
changed(result);
return result;
}

View file

@ -178,3 +178,27 @@ xtc_result xtc_project_transform_midi(xtc_project *p,const uint64_t *ids,size_t
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;
}

View file

@ -1348,6 +1348,15 @@ int main(int argc, char **argv)
CHECK(s->transform_selection(XTC_CLIPS_FADE,10)==XTC_OK);
CHECK(xtc_project_find_clip(s->project(),s->selected_clip(),&moved)==XTC_OK && moved.fade_in_tick==10 && moved.fade_out_tick==10);
CHECK(s->transform_selection(XTC_CLIPS_FADE,0)==XTC_OK);
uint64_t mix_track=s->selected_track();CHECK(s->toggle_clip(first) && s->toggle_clip(first));
CHECK(s->selected_track()==mix_track);
CHECK(window.mix_snapshot(0,true)==XTC_OK);
CHECK(xtc_project_set_mix(s->project(),mix_track,.5f,-.5f,0,0)==XTC_OK);
CHECK(window.mix_snapshot(1,true)==XTC_OK && window.mix_snapshot(0,false)==XTC_OK);
xtc_track_info mix{};xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==1);
CHECK(window.mix_snapshot(1,false)==XTC_OK);xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==.5f);
CHECK(window.undo()==XTC_OK);xtc_project_get_track(s->project(),1,&mix);CHECK(mix.gain==1);
CHECK(window.new_project()==XTC_OK && window.mix_snapshot(0,false)==XTC_ERR_NOT_FOUND);
window.do_callback();
}
if (failures != 0) return 1;

View file

@ -4,6 +4,7 @@
#include "xtc/xtc_native.h"
#include "xtc/xtc_control.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_editing.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
@ -177,8 +178,52 @@ static void builtin_failures(void)
}
}
static void modern_edit_failures(void)
{
xtc_project *p=NULL;xtc_clipboard *board=NULL;xtc_mix_snapshot *mix=NULL;
uint64_t track,clip,note;int operation,failure;
xtc_midi_transform edit={0};edit.grid=240;edit.swing=50;edit.strength=100;
assert(xtc_project_create(&p)==XTC_OK);
assert(xtc_project_add_track(p,"Track",&track)==XTC_OK);
assert(xtc_project_add_midi_clip(p,track,960,1920,&clip)==XTC_OK);
assert(xtc_project_add_note(p,clip,230,120,61,100,0,&note)==XTC_OK);
assert(xtc_clipboard_create(p,&clip,1,&board)==XTC_OK);
assert(xtc_mix_snapshot_create(p,&mix)==XTC_OK);
for(operation=0;operation<7;++operation) {
int succeeded=0;
for(failure=0;failure<32;++failure) {
xtc_project *copy=NULL;xtc_clipboard *created=NULL;xtc_mix_snapshot *captured=NULL;
uint64_t output=999;xtc_clip_info c;xtc_note_info n;size_t tracks,clips;xtc_result r;
assert(xtc_project_clone(p,&copy)==XTC_OK);
fail_after=failure;
switch(operation) {
case 0:r=xtc_project_transform_clips(copy,&clip,1,XTC_CLIPS_DUPLICATE,1920,&output);break;
case 1:r=xtc_clipboard_create(copy,&clip,1,&created);break;
case 2:r=xtc_clipboard_paste(board,copy,track,3840,&output);break;
case 3:r=xtc_project_repeat_clips(copy,&clip,1,2);break;
case 4:r=xtc_project_transform_midi(copy,&clip,1,&edit);break;
case 5:r=xtc_mix_snapshot_create(copy,&captured);break;
default:r=xtc_mix_snapshot_recall(mix,copy);break;
}
fail_after=-1;
if(r==XTC_OK)succeeded=1;
else {
assert(r==XTC_ERR_OUT_OF_MEMORY && output==999 && !created && !captured);
assert(xtc_project_get_counts(copy,&tracks,&clips)==XTC_OK && clips==1);
assert(xtc_project_find_clip(copy,clip,&c)==XTC_OK && c.start_tick==960);
assert(xtc_project_get_note(copy,0,&n)==XTC_OK && n.start_tick==230 && n.id==note);
{uint64_t probe;assert(xtc_project_duplicate_clip(copy,clip,track,3840,&probe)==XTC_OK && probe==clip+1);}
}
xtc_clipboard_destroy(created);xtc_mix_snapshot_destroy(captured);xtc_project_destroy(copy);
if(succeeded)break;
}
assert(succeeded);
}
xtc_clipboard_destroy(board);xtc_mix_snapshot_destroy(mix);xtc_project_destroy(p);
}
int main(void)
{
modern_edit_failures();
builtin_failures();
graph_limits();
history_reservations();

View file

@ -1,8 +1,68 @@
#define _POSIX_C_SOURCE 200809L
#include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_mixer.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static void test_persistence_render(void)
{
xtc_project *p=NULL,*q=NULL;xtc_midi_plan *a=NULL,*b=NULL;
uint64_t t,c,n;char path[]="/tmp/xtc-modern-XXXXXX";float left[137],right[137];
xtc_midi_transform edit={0};size_t i;int nonzero=0,fd=mkstemp(path);assert(fd>=0);assert(close(fd)==0);
assert(xtc_project_create(&p)==XTC_OK && xtc_project_create(&q)==XTC_OK);
assert(xtc_project_add_track(p,"Track",&t)==XTC_OK && xtc_project_add_midi_clip(p,t,0,1920,&c)==XTC_OK);
assert(xtc_project_add_note(p,c,230,240,61,100,0,&n)==XTC_OK);
edit.grid=240;edit.swing=50;edit.strength=100;
assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
edit.type=XTC_MIDI_TRANSFORM_SCALE;edit.scale=1;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
edit.type=XTC_MIDI_TRANSFORM_VELOCITY;edit.velocity_percent=50;assert(xtc_project_transform_midi(p,&c,1,&edit)==XTC_OK);
assert(xtc_project_repeat_clips(p,&c,1,2)==XTC_OK);
assert(xtc_project_save(p,path)==XTC_OK && xtc_project_load(q,path)==XTC_OK);
assert(xtc_midi_plan_create(p,48000,&a)==XTC_OK && xtc_midi_plan_create(q,48000,&b)==XTC_OK);
assert(xtc_midi_plan_render(a,8000,NULL,0,left,137)==XTC_OK);
assert(xtc_midi_plan_render(b,8000,NULL,0,right,137)==XTC_OK);
for(i=0;i<137;++i){assert(left[i]==right[i]);if(left[i]!=0)nonzero=1;}
assert(nonzero);xtc_midi_plan_destroy(a);xtc_midi_plan_destroy(b);
xtc_project_destroy(p);xtc_project_destroy(q);assert(unlink(path)==0);
}
static void test_clipboard_tracks(void)
{
xtc_project *p=NULL,*q=NULL;xtc_clipboard *board=NULL;uint64_t t,u,ids[2],out[2]={999,999};xtc_clip_info c;size_t tracks,clips;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_create(&q)==XTC_OK);
assert(xtc_project_add_track(p,"One",&t)==XTC_OK && xtc_project_add_track(p,"Two",&u)==XTC_OK);
assert(xtc_project_add_clip(p,t,"A",100,240,&ids[0])==XTC_OK && xtc_project_add_clip(p,u,"B",200,240,&ids[1])==XTC_OK);
assert(xtc_clipboard_create(p,ids,2,&board)==XTC_OK);
assert(xtc_project_add_track(q,"Destination",&t)==XTC_OK);
assert(xtc_clipboard_paste(board,q,t,960,out)==XTC_ERR_NOT_FOUND && out[0]==999);
assert(xtc_project_get_counts(q,&tracks,&clips)==XTC_OK && clips==0);
assert(xtc_project_add_bus(q,"Bus",&u)==XTC_OK);
assert(xtc_clipboard_paste(board,q,t,960,out)==XTC_ERR_INVALID_STATE && out[0]==999);
assert(xtc_project_remove_track(q,u)==XTC_OK && xtc_project_add_track(q,"Next",&u)==XTC_OK);
assert(xtc_clipboard_paste(board,q,t,960,out)==XTC_OK);
assert(xtc_project_find_clip(q,out[1],&c)==XTC_OK && c.track_id==u && c.start_tick==1060);
xtc_clipboard_destroy(board);xtc_project_destroy(p);xtc_project_destroy(q);
}
static void test_mix_snapshot(void)
{
xtc_project *p=NULL;xtc_mix_snapshot *a=NULL,*b=NULL;uint64_t t,u;xtc_track_info track;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Track",&t)==XTC_OK);
assert(xtc_project_add_bus(p,"Bus",&u)==XTC_OK);
assert(xtc_mix_snapshot_create(p,&a)==XTC_OK);
assert(xtc_project_set_mix(p,t,0.5f,-0.5f,1,1)==XTC_OK);
assert(xtc_mix_snapshot_create(p,&b)==XTC_OK);
assert(xtc_project_move_track(p,t,1)==XTC_OK && xtc_mix_snapshot_recall(a,p)==XTC_OK);
assert(xtc_project_get_track(p,1,&track)==XTC_OK && track.id==t && track.gain==1 && !track.mute);
assert(xtc_mix_snapshot_recall(b,p)==XTC_OK);
assert(xtc_project_get_track(p,1,&track)==XTC_OK && track.gain==0.5f && track.pan==-0.5f && track.mute && track.solo);
assert(xtc_project_remove_track(p,u)==XTC_OK && xtc_project_add_track(p,"Different",&u)==XTC_OK);
assert(xtc_mix_snapshot_recall(a,p)==XTC_ERR_NOT_FOUND);
assert(xtc_project_get_track(p,0,&track)==XTC_OK && track.id==t && track.gain==0.5f);
xtc_mix_snapshot_destroy(a);xtc_mix_snapshot_destroy(b);xtc_project_destroy(p);
}
static void test_fades(void)
{
xtc_project *p=NULL;uint64_t t,ids[2];xtc_clip_info c;
@ -122,7 +182,7 @@ static void test_repeat(void)
}
int main(void)
{
test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();test_velocity();test_fades();
test_repeat();test_align_slip();test_swing();test_gate_legato();test_scale();test_velocity();test_fades();test_mix_snapshot();test_clipboard_tracks();test_persistence_render();
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);