Phase 17: add undoable duplication, naming, ordering and timeline commands
This commit is contained in:
parent
7fac0a4f6a
commit
440e98f2b5
10 changed files with 231 additions and 5 deletions
8
Makefile
8
Makefile
|
|
@ -328,3 +328,11 @@ test-control: $(BUILD_DIR)/test_control
|
|||
./$(BUILD_DIR)/test_control
|
||||
test: test-control
|
||||
-include $(BUILD_DIR)/tests/unit/test_control.d
|
||||
|
||||
$(BUILD_DIR)/test_editing: $(BUILD_DIR)/tests/unit/test_editing.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
|
||||
.PHONY: test-editing
|
||||
test-editing: $(BUILD_DIR)/test_editing
|
||||
./$(BUILD_DIR)/test_editing
|
||||
test: test-editing
|
||||
-include $(BUILD_DIR)/tests/unit/test_editing.d
|
||||
|
|
|
|||
|
|
@ -355,6 +355,14 @@ instrument timbres and physical listening remain outside the supported scope.
|
|||
- GUI operations do not block the real-time audio thread.
|
||||
- Interaction behavior is documented and covered by relevant tests.
|
||||
|
||||
Implemented: transactional full-source clip duplication, clip/track names,
|
||||
track ordering, configurable timeline snap, Ctrl+D/F2/Alt+arrow commands and a
|
||||
context menu; kind/name clip labels and focus regressions. Snapshot/GUI tests
|
||||
verify duplicate/rename/order undo/redo, source-note retention, capacity rollback
|
||||
and existing graph/backend history behavior. Recording undo removes references,
|
||||
never media. See WORKFLOW.md. Background loading, persisted UI layout and broad
|
||||
screen-reader/accessibility qualification remain explicit limits.
|
||||
|
||||
## Phase 18 — Project Format, Compatibility, and Portability
|
||||
|
||||
**Goal:** Keep project files valid, recoverable, and maintainable over time.
|
||||
|
|
|
|||
|
|
@ -100,3 +100,34 @@ These checks cover a minimal composition workflow, not physical listening or
|
|||
endurance qualification. No new dependency, project-format change, plugin host,
|
||||
MIDI capture, tempo editor, loop playback, device-routing GUI, or persisted editor
|
||||
layout was introduced. See GUI.md, MIDI.md, MIXER.md and LINUX.md for details.
|
||||
|
||||
## Phase 17 editing workflow
|
||||
|
||||
Select a clip and press Ctrl+D (or Edit/Duplicate clip) to duplicate it immediately
|
||||
after its end. Complete source notes, trim offsets and media references are
|
||||
retained with fresh stable IDs. No audio file is copied or overwritten. F2 or
|
||||
Edit/Rename clip or track names the selection. Clip labels show both kind and
|
||||
user name. Alt+Up/Down with timeline focus reorders its selected track without
|
||||
changing IDs, clip membership or routing. Right-click a timeline clip/track for
|
||||
Duplicate, Rename and Delete. Invalid/capacity edits retain the existing model.
|
||||
|
||||
View/Snap selects 1, 120, 240 (default), 480 or 960 ticks. It affects ruler seeks,
|
||||
clip moves and trims; 1 tick is the finest grid. Existing +/- zoom and scrollbars
|
||||
retain their behavior. Piano roll and mixer lanes retain their documented 240-tick
|
||||
grid. Grid/zoom/selection are session UI preferences, not undoable music edits.
|
||||
Escape cancels a drag; Delete affects the focused editor; text fields keep their
|
||||
own editing keys. Commands are named in menus/tooltips and timeline focus accepts
|
||||
keyboard editing; screen-reader and broad accessibility qualification are pending.
|
||||
|
||||
Create/delete/move/trim/duplicate, names/order, graph connects/deletes, notes,
|
||||
mixer/automation and plugin edits use the 32-edit snapshot history. Ctrl+Z undoes;
|
||||
Ctrl+Shift+Z or Ctrl+Y redoes. Backend preparation precedes accepting an undo/redo
|
||||
snapshot; failure retains its cursor/document. Model edits stop playback before
|
||||
mutation; compilation/file work occurs on the control thread and may pause the
|
||||
GUI, never block the audio callback. This is not a background-loading editor.
|
||||
|
||||
A successful recorded take adds a reference as one undoable edit. Undo/removing
|
||||
its region never deletes the WAV; recording failures retain their recovery path.
|
||||
There is no destructive media-delete operation. New/Open reset history after
|
||||
unsaved-change handling. The capacity limit and allocation-error reporting in
|
||||
MIXER.md still apply. Tests: `make test-editing test-gui-plugins test-workflow`.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ extern "C" {
|
|||
#define XTC_TICKS_PER_QUARTER 960U
|
||||
#define XTC_PROJECT_MAX_TICK UINT64_C(38400000)
|
||||
typedef struct xtc_project xtc_project;
|
||||
/* Control-thread edits, transactional validation; duplicate retains complete
|
||||
* source notes/trim/media references, allocates fresh IDs, copies no audio. */
|
||||
xtc_result xtc_project_duplicate_clip(xtc_project *project, uint64_t clip, uint64_t track, uint64_t start, uint64_t *out);
|
||||
xtc_result xtc_project_rename_track(xtc_project *project, uint64_t track, const char *name);
|
||||
xtc_result xtc_project_rename_clip(xtc_project *project, uint64_t clip, const char *name);
|
||||
xtc_result xtc_project_move_track(xtc_project *project, uint64_t track, size_t index);
|
||||
|
||||
typedef struct {
|
||||
uint64_t id;
|
||||
|
|
|
|||
|
|
@ -146,6 +146,15 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
sequencer_ = new SequencerWidget(20, 188, project_.get(), seek, this);
|
||||
timeline_scroll->end();
|
||||
sequencer_->before_edit = before_arrangement_edit;
|
||||
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
|
||||
menu->add("Edit/Rename clip or track",FL_F+2,[](Fl_Widget *, void *c) {
|
||||
const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) (void)static_cast<SequencerWidget *>(c)->rename_selection(name);
|
||||
},sequencer_);
|
||||
for (const char *label : {"View/Snap/1 tick","View/Snap/120 ticks","View/Snap/240 ticks","View/Snap/480 ticks","View/Snap/960 ticks"})
|
||||
menu->add(label,0,[](Fl_Widget *widget,void *c) {
|
||||
auto *m = static_cast<Fl_Menu_Bar *>(widget);
|
||||
(void)static_cast<SequencerWidget *>(c)->grid(std::strtoull(m->mvalue()->label(),nullptr,10));
|
||||
},sequencer_);
|
||||
sequencer_->callback(
|
||||
[](Fl_Widget *, void *c) {
|
||||
static_cast<MainWindow *>(c)->mark_dirty();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <FL/Fl_Scroll.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <FL/Fl_Menu_Item.H>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ void SequencerWidget::set_project(xtc_project *project, bool reset)
|
|||
uint64_t SequencerWidget::tick(int px) const
|
||||
{
|
||||
return std::min(XTC_PROJECT_MAX_TICK, static_cast<uint64_t>(std::max(0, px - x() - 120)) * 960U /
|
||||
static_cast<unsigned int>(scale_) / 240U * 240U);
|
||||
static_cast<unsigned int>(scale_) / grid_ * grid_);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -244,7 +245,9 @@ void SequencerWidget::draw()
|
|||
fl_rectf(a, py + 5, b - a, 44);
|
||||
fl_push_clip(a, py + 5, b - a, 44);
|
||||
fl_color(FL_WHITE);
|
||||
fl_draw(c.midi ? "MIDI" : c.asset_path[0] ? "Audio" : c.name, std::max(a, left + 6), py + 31);
|
||||
char label[96];
|
||||
std::snprintf(label,sizeof(label),"%s: %s",c.midi ? "MIDI" : c.asset_path[0] ? "Audio" : "Region",c.name);
|
||||
fl_draw(label,std::max(a,left+6),py+31);
|
||||
fl_pop_clip();
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +267,7 @@ void SequencerWidget::draw()
|
|||
*/
|
||||
int SequencerWidget::handle(int event)
|
||||
{
|
||||
if (event == FL_PUSH && Fl::event_button() == FL_LEFT_MOUSE) {
|
||||
if (event == FL_PUSH && (Fl::event_button() == FL_LEFT_MOUSE || Fl::event_button() == FL_RIGHT_MOUSE)) {
|
||||
take_focus();
|
||||
mode_ = 0;
|
||||
if (Fl::event_y() < y() + 28) {
|
||||
|
|
@ -299,6 +302,15 @@ int SequencerWidget::handle(int event)
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (Fl::event_button() == FL_RIGHT_MOUSE) {
|
||||
mode_ = 0;
|
||||
Fl_Menu_Item items[4]{};
|
||||
items[0].text = "Duplicate clip (Ctrl+D)"; items[1].text = "Rename (F2)"; items[2].text = "Delete (undoable)";
|
||||
const Fl_Menu_Item *picked = items->popup(Fl::event_x(),Fl::event_y());
|
||||
if (picked == items) duplicate_selection();
|
||||
else if (picked == items+1) { const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); }
|
||||
else if (picked == items+2) delete_selection();
|
||||
}
|
||||
redraw();
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -307,7 +319,7 @@ int SequencerWidget::handle(int event)
|
|||
if (event == FL_RELEASE && mode_) {
|
||||
int mode = mode_;
|
||||
mode_ = 0;
|
||||
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale_ / 240 * 240;
|
||||
int64_t delta = static_cast<int64_t>(Fl::event_x() - drag_x_) * 960 / scale_ / static_cast<int64_t>(grid_) * static_cast<int64_t>(grid_);
|
||||
int64_t start = static_cast<int64_t>(original_.start_tick);
|
||||
int64_t length = static_cast<int64_t>(original_.length_tick);
|
||||
int64_t offset = static_cast<int64_t>(original_.offset_tick);
|
||||
|
|
@ -337,6 +349,12 @@ int SequencerWidget::handle(int event)
|
|||
static_cast<uint64_t>(length), static_cast<uint64_t>(offset)));
|
||||
return 1;
|
||||
}
|
||||
if (event == FL_KEYDOWN && Fl::event_state(FL_CTRL) && Fl::event_key() == 'd') { duplicate_selection(); return 1; }
|
||||
if (event == FL_KEYDOWN && Fl::event_key() == FL_F+2) {
|
||||
const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); return 1;
|
||||
}
|
||||
if (event == FL_KEYDOWN && Fl::event_state(FL_ALT) &&
|
||||
(Fl::event_key() == FL_Up || Fl::event_key() == FL_Down)) { move_track(Fl::event_key() == FL_Up ? -1 : 1); return 1; }
|
||||
if (event == FL_KEYDOWN && Fl::event_key() == FL_Escape) {
|
||||
mode_ = 0;
|
||||
return 1;
|
||||
|
|
@ -349,3 +367,34 @@ int SequencerWidget::handle(int event)
|
|||
return 1;
|
||||
return Fl_Widget::handle(event);
|
||||
}
|
||||
|
||||
xtc_result SequencerWidget::grid(uint64_t ticks)
|
||||
{
|
||||
if (ticks != 1 && ticks != 120 && ticks != 240 && ticks != 480 && ticks != 960) return XTC_ERR_INVALID_ARGUMENT;
|
||||
grid_ = ticks; redraw(); return XTC_OK;
|
||||
}
|
||||
xtc_result SequencerWidget::duplicate_selection()
|
||||
{
|
||||
xtc_clip_info c{}; uint64_t id;
|
||||
xtc_result r = xtc_project_find_clip(project_,clip_,&c);
|
||||
if (r == XTC_OK && before_edit) r = before_edit(context_);
|
||||
if (r == XTC_OK) r = xtc_project_duplicate_clip(project_,clip_,c.track_id,c.start_tick+c.length_tick,&id);
|
||||
if (r == XTC_OK) clip_ = id;
|
||||
changed(r); return r;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ class SequencerWidget : public Fl_Widget {
|
|||
*
|
||||
* @return XTC_OK to allow the edit, or an error to cancel it.
|
||||
*/
|
||||
xtc_result duplicate_selection();
|
||||
xtc_result rename_selection(const char *name);
|
||||
xtc_result move_track(int direction);
|
||||
xtc_result grid(uint64_t ticks);
|
||||
uint64_t grid() const { return grid_; }
|
||||
using BeforeEdit = xtc_result (*)(void *);
|
||||
BeforeEdit before_edit = nullptr;
|
||||
|
||||
|
|
@ -129,7 +134,7 @@ class SequencerWidget : public Fl_Widget {
|
|||
xtc_project *project_;
|
||||
Seek seek_;
|
||||
void *context_;
|
||||
uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0;
|
||||
uint64_t track_ = UINT64_MAX, clip_ = UINT64_MAX, cursor_ = 0, grid_ = 240;
|
||||
int scale_ = 48, drag_x_ = 0, mode_ = 0;
|
||||
xtc_clip_info original_{};
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -600,3 +600,49 @@ xtc_result xtc_project_set_clip_asset(xtc_project *project, uint64_t id, const c
|
|||
memmove(project->clips[index].asset_path, path, length + 1U);
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_project_duplicate_clip(xtc_project *p, uint64_t id, uint64_t track, uint64_t start, uint64_t *out)
|
||||
{
|
||||
size_t index, i, count = 0, original_notes;
|
||||
xtc_clip_info c;
|
||||
if (!p || !out) return XTC_ERR_INVALID_ARGUMENT;
|
||||
index = clip_index(p,id);
|
||||
if (index == SIZE_MAX || track_index(p,track) == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
c = p->clips[index];
|
||||
if (!valid_range(start,c.length_tick,c.offset_tick,c.source_length_tick)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
for (i = 0; i < p->note_count; ++i) if (p->notes[i].clip_id == id) ++count;
|
||||
if (p->clip_count == XTC_PROJECT_MAX_CLIPS || p->next_clip_id == UINT64_MAX ||
|
||||
count > XTC_PROJECT_MAX_NOTES-p->note_count || p->next_note_id > UINT64_MAX-count) return XTC_ERR_CAPACITY;
|
||||
c.id = p->next_clip_id++; c.track_id = track; c.start_tick = start;
|
||||
p->clips[p->clip_count++] = c;
|
||||
original_notes = p->note_count;
|
||||
for (i = 0; i < original_notes; ++i) if (p->notes[i].clip_id == id) {
|
||||
xtc_note_info n = p->notes[i]; n.id = p->next_note_id++; n.clip_id = c.id;
|
||||
p->notes[p->note_count++] = n;
|
||||
}
|
||||
*out = c.id; return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_rename_track(xtc_project *p, uint64_t id, const char *name)
|
||||
{
|
||||
size_t i;
|
||||
if (!p || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
memmove(p->tracks[i].name,name,strlen(name)+1); return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_rename_clip(xtc_project *p, uint64_t id, const char *name)
|
||||
{
|
||||
size_t i;
|
||||
if (!p || !xtc_project_valid_name(name)) return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = clip_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
memmove(p->clips[i].name,name,strlen(name)+1); return XTC_OK;
|
||||
}
|
||||
xtc_result xtc_project_move_track(xtc_project *p, uint64_t id, size_t destination)
|
||||
{
|
||||
size_t i; xtc_track_info track;
|
||||
if (!p || destination >= p->track_count) return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = track_index(p,id); if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
track = p->tracks[i];
|
||||
if (i < destination) memmove(p->tracks+i,p->tracks+i+1,(destination-i)*sizeof(track));
|
||||
else if (i > destination) memmove(p->tracks+destination+1,p->tracks+destination,(i-destination)*sizeof(track));
|
||||
p->tracks[destination] = track; return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,6 +457,28 @@ int main(int argc, char **argv)
|
|||
window.do_callback();
|
||||
}
|
||||
CHECK(alive == 0U);
|
||||
reset();
|
||||
{
|
||||
MainWindow window(false); auto *s = timeline(&window);
|
||||
CHECK(s->add_track() == XTC_OK && s->rename_selection("Keys") == XTC_OK);
|
||||
CHECK(s->add_region() == XTC_OK && s->rename_selection("Verse") == XTC_OK);
|
||||
CHECK(s->grid(120) == XTC_OK && s->grid() == 120 && s->grid(0) == XTC_ERR_INVALID_ARGUMENT);
|
||||
CHECK(s->handle(FL_FOCUS) == 1);
|
||||
int old_key = Fl::e_keysym, old_state = Fl::e_state;
|
||||
Fl::e_keysym = 'd'; Fl::e_state = FL_CTRL;
|
||||
CHECK(s->handle(FL_KEYDOWN) == 1);
|
||||
Fl::e_keysym = old_key; Fl::e_state = old_state;
|
||||
size_t tracks, clips; xtc_project_get_counts(s->project(),&tracks,&clips);
|
||||
CHECK(clips == 2);
|
||||
xtc_clip_info c{}; CHECK(xtc_project_get_clip(s->project(),1,&c) == XTC_OK && c.start_tick == 3840 && !std::strcmp(c.name,"Verse"));
|
||||
CHECK(window.undo(false) == XTC_OK); xtc_project_get_counts(s->project(),&tracks,&clips); CHECK(clips == 1);
|
||||
CHECK(window.undo(true) == XTC_OK); xtc_project_get_counts(s->project(),&tracks,&clips); CHECK(clips == 2);
|
||||
CHECK(s->add_track() == XTC_OK && s->rename_selection("Bass") == XTC_OK && s->move_track(-1) == XTC_OK);
|
||||
xtc_track_info t{}; CHECK(xtc_project_get_track(s->project(),0,&t) == XTC_OK && !std::strcmp(t.name,"Bass"));
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_project_get_track(s->project(),0,&t) == XTC_OK && !std::strcmp(t.name,"Keys"));
|
||||
window.do_callback();
|
||||
}
|
||||
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
|
||||
reset();
|
||||
MainWindow window(false);
|
||||
|
|
|
|||
42
tests/unit/test_editing.c
Normal file
42
tests/unit/test_editing.c
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#include "xtc/xtc_midi.h"
|
||||
#include "xtc/xtc_mixer.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
int main(void)
|
||||
{
|
||||
xtc_project *p = NULL, *undo = NULL; xtc_history *h = NULL;
|
||||
uint64_t track, second, clip, duplicate, note; size_t notes, tracks, clips;
|
||||
xtc_clip_info c; xtc_note_info n, original;
|
||||
assert(xtc_project_create(&p) == XTC_OK);
|
||||
assert(xtc_project_add_track(p,"Keys",&track) == XTC_OK && xtc_project_add_track(p,"Bass",&second) == XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p,track,0,3840,&clip) == XTC_OK);
|
||||
assert(xtc_project_add_note(p,clip,0,960,69,100,1,¬e) == XTC_OK);
|
||||
assert(xtc_project_edit_clip(p,clip,track,960,1920,960) == XTC_OK);
|
||||
assert(xtc_project_get_note(p,0,&original) == XTC_OK);
|
||||
assert(xtc_history_create(p,&h) == XTC_OK);
|
||||
assert(xtc_project_duplicate_clip(p,clip,second,3840,&duplicate) == XTC_OK && duplicate != clip);
|
||||
assert(xtc_project_find_clip(p,duplicate,&c) == XTC_OK && c.offset_tick == 960 && c.source_length_tick == 3840 && c.midi);
|
||||
assert(xtc_project_get_note(p,1,&n) == XTC_OK && n.clip_id == duplicate && n.id != original.id && n.start_tick == original.start_tick);
|
||||
assert(xtc_project_rename_clip(p,duplicate,"Chorus ä") == XTC_OK);
|
||||
assert(xtc_project_rename_track(p,second,"Bass 2") == XTC_OK);
|
||||
assert(xtc_project_move_track(p,second,0) == XTC_OK);
|
||||
assert(xtc_history_record(h,p) == XTC_OK && xtc_history_checkout(h,0,&undo) == XTC_OK);
|
||||
assert(xtc_project_find_clip(undo,duplicate,&c) == XTC_ERR_NOT_FOUND);
|
||||
assert(xtc_project_get_counts(undo,&tracks,&clips) == XTC_OK && clips == 1);
|
||||
xtc_project_destroy(undo);
|
||||
assert(xtc_project_duplicate_clip(p,clip,track,XTC_PROJECT_MAX_TICK,&duplicate) == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_rename_track(p,track,"Bad\n") == XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(xtc_project_clone(p,&undo) == XTC_OK);
|
||||
xtc_project_destroy(undo);
|
||||
for (;;) {
|
||||
assert(xtc_project_note_count(p,¬es) == XTC_OK);
|
||||
if (notes == XTC_PROJECT_MAX_NOTES) break;
|
||||
assert(xtc_project_add_note(p,clip,0,1,60,100,0,¬e) == XTC_OK);
|
||||
}
|
||||
assert(xtc_project_duplicate_clip(p,clip,track,0,&duplicate) == XTC_ERR_CAPACITY);
|
||||
assert(xtc_project_get_counts(p,&tracks,&clips) == XTC_OK && clips == 2);
|
||||
xtc_history_destroy(h); xtc_project_destroy(p);
|
||||
puts("Duplicate source notes/trim, naming/order, undo and capacity rollback passed.");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue