Phase 27: strengthen undo transactions and improve editor workflow
This commit is contained in:
parent
901a536f10
commit
c4daef7c42
21 changed files with 460 additions and 27 deletions
2
Makefile
2
Makefile
|
|
@ -140,7 +140,7 @@ ifeq ($(GUI_AVAILABLE),yes)
|
|||
gui: $(BUILD_DIR)/xtc
|
||||
|
||||
$(BUILD_DIR)/test_gui: $(BUILD_DIR)/tests/integration/test_gui.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/src/gui/graph_widget.o $(BUILD_DIR)/src/gui/sequencer_widget.o $(BUILD_DIR)/src/gui/piano_roll.o $(BUILD_DIR)/src/gui/mixer_widget.o $(BUILD_DIR)/src/gui/plugin_dialog.o $(BUILD_DIR)/libxtc.a
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(FLTK_LIBS) -lm -ldl -Wl,--wrap=xtc_history_reserve
|
||||
|
||||
test-gui: $(BUILD_DIR)/test_gui
|
||||
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||
|
|
|
|||
|
|
@ -149,3 +149,7 @@ reset, transactional replacement and insertion into graph wires. See
|
|||
Phase 26 adds common parameter targets, Linear/Hold mixer automation and saved
|
||||
per-channel live MIDI CC bindings. Project v10 reads v1–v9. See
|
||||
[CONTROL.md](docs/CONTROL.md) for timing, precedence and qualification limits.
|
||||
|
||||
Phase 27 reserves undo storage before edits/publication, groups completed takes,
|
||||
preserves surviving editor selections and improves context menus/focus feedback.
|
||||
See [EDITING.md](docs/EDITING.md) for the workflow audit and failure boundaries.
|
||||
|
|
|
|||
115
docs/EDITING.md
Normal file
115
docs/EDITING.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Editing commands and undo (Phase 27 audit)
|
||||
|
||||
The control thread owns project edits and history. Audio plans are immutable
|
||||
snapshots; widgets never pass editable project pointers into the callback.
|
||||
Graph commands and batch note commands use the existing public C command APIs.
|
||||
The GUI groups a completed gesture or accepted dialog as one history step.
|
||||
|
||||
## Operation audit
|
||||
|
||||
| Operation | History / grouping | Engine and external effects |
|
||||
| --- | --- | --- |
|
||||
| Track create/delete/name/order; clip create/delete/move/trim/name | One accepted command/drag | Stops playback before mutation; track removal cascades clips/notes/lanes, never files |
|
||||
| MIDI clip creation and default source routing | One command, including automatic route | Core candidate graph is validated before model adoption |
|
||||
| Audio duplicate/split/gain/fades | One command including copied metadata/notes | Source WAV is shared; split does not rewrite samples |
|
||||
| Note add/delete/drag/resize, filtered quantize/transpose/properties/repeat | One gesture/batch, including fresh note IDs | Stops playback; validates all matched notes/capacity before applying |
|
||||
| Graph add/remove/connect/disconnect/insert gain | One graph command | Candidate and history storage prepared before engine submission; active graph remains immutable |
|
||||
| Mixer gain/pan/mute/solo, bus create/route, automation points | One released slider/command/point | Stops playback; bus deletion reroutes members without deleting their media |
|
||||
| Plugin add/replace/control/reset/automation/insert | One accepted command/dialog including wiring | Candidate compiles before installation; old module instances retire on control thread |
|
||||
| MIDI controller bindings | One channel-map command | Rebuilds immutable MIDI plan; no callback mutation |
|
||||
| Import/relink audio | One accepted edit, including newly needed track/routes | Import candidate prepared before adoption; source media never copied/deleted by undo |
|
||||
| Recording start/stop | Start has no document history step; completed take adds one | Undo removes take reference, never finalized WAV; failed add retains saved take for manual import |
|
||||
| Save / Save As | No history step; retains existing history | Writes document; undo does not restore earlier on-disk file versions |
|
||||
| New / Open | Fresh baseline after unsaved-change handling | Parse/prepare failure retains old document/history; engine may already be stopped/reset after preparation failure |
|
||||
| Transport, panic, external connections/reconnect | Not document history | Undo does not reverse playback, live voices, device connections or capture side effects |
|
||||
| Selection, tabs, snap/grid, zoom/scroll, node layout, meters/reset peaks | Session UI state, no history | No music or media changes |
|
||||
|
||||
No arbitrary cross-command grouping or automation recording mode is introduced.
|
||||
An accepted no-op model edit can still consume a step. History holds 32 edits
|
||||
plus a baseline; new accepted edits discard redo, and eviction drops the oldest
|
||||
baseline. Monotonic ID high-water marks survive undo and branches. History is
|
||||
session-only; restart/New/Open discards it.
|
||||
|
||||
## History ownership and failure boundary
|
||||
|
||||
`xtc_history_reserve` allocates one empty fixed-capacity project/graph snapshot
|
||||
before an edit mutates the document or a candidate is submitted to the engine.
|
||||
Failure aborts the command without changing history or dropping redo. The call
|
||||
is idempotent: storage left by a rejected command is reused. It neither records
|
||||
an edit nor moves the cursor. GUI arrangement/note/mixer guards reserve before
|
||||
stopping playback; candidate graph/plugin/map/take paths reserve before
|
||||
publication. File/media work and compilation can still fail before adoption.
|
||||
|
||||
The next `xtc_history_record` copies model arrays and the bounded 64-node/4096-
|
||||
edge graph into reserved storage without allocating. Successful record takes
|
||||
ownership of that storage, remembers IDs, then releases old redo/evicted states.
|
||||
The snapshot contains metadata/graph configuration only, never sample buffers,
|
||||
DSP instances, module handles or GUI pointers. An unused reservation is freed
|
||||
with history. This adds at most one pending metadata snapshot to the 33 retained
|
||||
states. C clients that call record without reserving retain the original
|
||||
clone-and-report-allocation-failure behavior; they must handle that result.
|
||||
All history work is serialized on the control thread, never the audio callback.
|
||||
|
||||
Undo/redo checks out an owned candidate without moving the cursor, stops/resets
|
||||
transport and prepares its assets/plans, then accepts the adjacent history step
|
||||
only after installation succeeds. Failed checkout/preparation retains the
|
||||
cursor/document, so retry is possible. Preparation failure may leave transport
|
||||
stopped at zero; it does not resume playback automatically. Snapshot restoration
|
||||
marks the document dirty conservatively, even if it matches a saved file.
|
||||
|
||||
## Selection, focus and menus
|
||||
|
||||
Ctrl+Z undoes; Ctrl+Shift+Z and Ctrl+Y redo. The main Edit menu disables unavailable
|
||||
steps and disables history while recording. Piano roll also has Edit/Undo/Redo
|
||||
shortcuts. Undo preserves currently selected track/clip, mixer strip, graph node
|
||||
and piano-roll clip/note when their stable IDs still exist (notes must remain
|
||||
visible under the current filter). Clip selection follows its restored track. A removed selection
|
||||
clears, with track fallback for a removed clip. A visible piano roll stays open
|
||||
when its clip survives; missing clips hide it. Historical selections/layouts are
|
||||
not saved in snapshots; undo does not resurrect a selection already cleared by
|
||||
a deletion. Open/New retain their reset behavior.
|
||||
|
||||
Editor-local shortcuts act on focused editors. Text inputs retain Space and
|
||||
text undo/editing keys; numeric mixer inputs do not invoke fader shortcuts.
|
||||
Timeline Ctrl+D duplicates, F2 renames, Alt+Up/Down reorders tracks, S splits
|
||||
audio at the cursor, A opens audio properties and Delete removes the selection.
|
||||
Graph Left/Right chooses a node, L arranges by flow, I inserts unity gain in the
|
||||
selected wire, Delete removes the selected node/wire and Escape cancels gestures.
|
||||
Piano-roll commands are listed in its Notes menu. Escape cancels gestures;
|
||||
ordinary deletion is reversible and does not add a confirmation dialog.
|
||||
|
||||
Graph right-click now selects the node/wire under the pointer before showing
|
||||
Inspect/Arrange/Insert/Delete; empty space clears a stale selection. Piano-roll
|
||||
right-click selects the clicked note and offers Properties/Quantize/Repeat/Delete.
|
||||
It no longer immediately deletes the note. Timeline context menus retain their
|
||||
selection and duplicate/rename/delete/audio commands. Dismissing a context menu
|
||||
does not edit the document. Context menus and widgets remain C++/FLTK UI code.
|
||||
|
||||
Open/import/plugin load/replacement paint a busy cursor and operation status
|
||||
before synchronous work, flushing drawing without dispatching new input or
|
||||
reentering commands. This is feedback, not background loading or cancellable
|
||||
progress. Failure messages distinguish capacity, memory, target/value, format/
|
||||
optional-support and audio errors. Unsaved New/Open/Quit and Save As overwrite
|
||||
keep their existing confirmations; undoable deletes and external-media reference
|
||||
edits do not request extra approval. No operation here deletes source media.
|
||||
|
||||
## Evidence and manual procedure
|
||||
|
||||
`make all test gui test-gui-plugins test-reliability analyze test-workflow
|
||||
test-gui-jack` covers core commands, grouped gestures/batches, backend rollback,
|
||||
selection retention, take undo/redo and real software JACK state restoration.
|
||||
The GUI fixture injects history-reservation failure before graph/map/import
|
||||
publication and verifies unchanged document, submission count and redo. The
|
||||
allocation-interposed reliability suite fails each reservation allocation, then
|
||||
fails all allocations while capturing a reserved edit; redo branching and
|
||||
snapshot restoration still succeed. ASan/UBSan cover the same ownership paths.
|
||||
|
||||
For native manual inspection: right-click different graph nodes/wires and empty
|
||||
space, dismiss the menu, and confirm only selection changes. Right-click a note,
|
||||
dismiss, then explicitly Delete and undo; confirm the note returns. Type spaces
|
||||
and use text undo in a name/input dialog, and edit numeric mixer fields without
|
||||
triggering fader keys. Record a short take, stop, undo once and confirm its WAV
|
||||
still exists; redo restores the reference. Watch operation feedback on a large
|
||||
import/Open. These manual popup/focus/large-file and physical-device listening
|
||||
checks remain pending; automation does not claim broad accessibility or human
|
||||
usability qualification. Existing native-plugin failure limits still apply.
|
||||
|
|
@ -19,7 +19,8 @@
|
|||
Automated LADSPA controls use run(1) and can be expensive.
|
||||
- GUI loading/compilation is synchronous on the control thread. Timeline grid/
|
||||
zoom/selection and graph positions are session UI state. Undo retains 32 edits
|
||||
plus baseline; allocation failures are reported, not silently hidden. Media is
|
||||
plus baseline and at most one reserved metadata snapshot; GUI edits reserve
|
||||
history before mutation/publication. Media is
|
||||
never removed by undo/delete. Broad screen-reader/accessibility testing is pending.
|
||||
- Format v10 reads v1–v9 and uses relative local media references. External absolute
|
||||
media/module paths need manual repair after relocation. Saving does not bundle
|
||||
|
|
@ -59,3 +60,8 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
|
|||
for live sine volume/pan/sustain. Live mappings do not drive arranged lanes,
|
||||
faders or plugins. No automation capture, touch/latch/write modes or loop
|
||||
transport. Backward jumps are deterministic for lanes, not opaque plugin tails.
|
||||
|
||||
- Editing feedback remains synchronous; busy cursors are not background loading
|
||||
or cancellable progress. Surviving selections are retained across undo but
|
||||
historical selection/layout is not stored. Native context-menu/focus manual
|
||||
qualification and broad accessibility testing remain pending. See EDITING.md.
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ plus a baseline, cloning metadata and graph, never audio samples. Common graph,
|
|||
track, clip, note, mixer, import/relink and automation edits are included. One
|
||||
accepted gesture is one step; selection, scrolling and graph-node placement are
|
||||
GUI state and are not undoable. New/Open resets history; Save retains it.
|
||||
Undo/redo stops and resets transport and resets editor selections/layout. The
|
||||
Undo/redo stops and resets transport; Phase 27 preserves surviving selections
|
||||
by stable ID (layout remains session state). The
|
||||
candidate is installed before moving the history cursor: validation, asset-plan
|
||||
or backend failure retains the current document and history position. Missing
|
||||
assets retain the existing silent/reporting behavior. A new edit after Undo
|
||||
|
|
@ -112,8 +113,9 @@ removes the redo branch; the oldest state is evicted at capacity. Allocation
|
|||
ID high-water marks survive Undo/branching so abandoned IDs are not reused. History is
|
||||
session-only and Undo marks the document dirty even if it matches a saved state.
|
||||
External WAV files are never deleted or restored by Undo; recording itself cannot
|
||||
be undone while active. An allocation failure retaining a new history snapshot
|
||||
reports an error; the accepted edit remains in the document.
|
||||
be undone while active. Phase 27 reserves snapshot storage before GUI commands mutate/publish, so an
|
||||
allocation failure rejects the command before adoption; reserved capture itself
|
||||
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
|
||||
|
|
|
|||
|
|
@ -720,10 +720,37 @@ Make common editing actions quick, discoverable, and safe to reverse.
|
|||
- Keep presentation logic out of the core C engine.
|
||||
|
||||
### Acceptance criteria
|
||||
- Documented editing operations have predictable undo/redo behavior.
|
||||
- Undo and redo restore the project model and relevant engine state consistently.
|
||||
- Failed operations do not leave partial changes behind.
|
||||
- Common tasks can be completed using documented keyboard and mouse interactions.
|
||||
- [x] Documented editing operations have predictable undo/redo behavior.
|
||||
- [x] Undo and redo restore the project model and relevant engine state consistently.
|
||||
- [x] Failed operations do not leave partial changes behind.
|
||||
- [x] Common tasks can be completed using documented keyboard and mouse interactions.
|
||||
|
||||
Implemented: operation-by-operation audit in EDITING.md; reserved history storage
|
||||
before GUI command mutation/candidate publication, followed by allocation-free
|
||||
snapshot capture; recording start creates no empty history entry and each
|
||||
completed take is one undoable reference edit; unavailable main-menu history
|
||||
steps disabled; surviving editor selections and open piano-roll context retained
|
||||
on undo; piano-roll history shortcuts/context menu and pointer-targeted graph
|
||||
context menus; input focus guards and synchronous busy feedback. Existing C
|
||||
graph/batch note commands and snapshot history remain the model boundary; FLTK
|
||||
presentation stays in C++. No project format change (v10 retained).
|
||||
|
||||
Validation: warning-enabled core/GUI builds and full tests; interposed allocation
|
||||
failure for each history reservation and capture with all allocations disabled;
|
||||
GUI history-reservation failure retains document, engine submission count and
|
||||
redo; completed-take undo grouping retains WAV; selection/context-target tests;
|
||||
existing grouped gestures/batches, candidate/backend rollback and save/reopen;
|
||||
workflow/software JACK GUI integration, reliability/callback guards, Clang static
|
||||
analysis, core/GUI ASan+UBSan, optional-dependency-free build, installation and
|
||||
package relocation. GUI sanitizer runs retain the existing fontconfig-only leak
|
||||
suppression. EDITING.md supplies a reproducible native manual procedure for
|
||||
popup dismissal, text focus, large synchronous operations and recorded takes.
|
||||
|
||||
Native manual popup/focus/large-file usability checks, broad accessibility and
|
||||
physical-device listening/endurance remain pending. History remains bounded and
|
||||
session-only; selection/layout history, arbitrary command grouping, background
|
||||
loading and destructive source-media editing are not supplied. Preparation
|
||||
failure can stop/reset transport while retaining the old document/history.
|
||||
|
||||
## Phase 28 — Project Format, Compatibility, and Portability
|
||||
|
||||
|
|
|
|||
|
|
@ -145,3 +145,10 @@ replaced. Audio → MIDI controller mapping lists all channel bindings; enter
|
|||
must be distinct, 120/123 reserved). Maps are undoable and saved. Controllers
|
||||
modify live sine voices; arranged track/bus/plugin automation takes precedence
|
||||
over its static controls and is unaffected by those MIDI bindings. See CONTROL.md.
|
||||
|
||||
Phase 27: unavailable main-menu undo/redo actions are disabled, including while
|
||||
recording. Piano roll has Edit/Undo/Redo shortcuts and a right-click note menu;
|
||||
right-click no longer immediately deletes notes. Graph right-click targets the
|
||||
node/wire under the pointer. Undo preserves surviving selections. Completed
|
||||
takes add one undo step; undo removes references and keeps WAV files. See
|
||||
EDITING.md for grouped commands, failure handling and keyboard/focus behavior.
|
||||
|
|
|
|||
|
|
@ -131,3 +131,15 @@ 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`.
|
||||
|
||||
## Phase 27 editing audit
|
||||
|
||||
See [EDITING.md](EDITING.md) for the operation-by-operation undo matrix, command
|
||||
ownership, allocation-failure boundary, external-media behavior and manual
|
||||
context-menu/focus procedure. History now reserves storage before edits and
|
||||
publication, preserving redo when commands fail. Recording start has no empty
|
||||
undo entry; a completed take is one step and undo never deletes its WAV.
|
||||
Undo preserves surviving selections and an open piano roll; the piano window
|
||||
also offers Edit/Undo/Redo. Graph context menus select under the pointer and
|
||||
piano-roll right-click now opens a menu instead of immediately deleting a note.
|
||||
Load/import/plugin work paints busy feedback while remaining synchronous.
|
||||
|
|
|
|||
|
|
@ -113,6 +113,14 @@ void xtc_history_destroy(xtc_history * h);
|
|||
* @param p Project model; borrowed for this call.
|
||||
* @return XTC_OK on success, or an error code describing the failure.
|
||||
*/
|
||||
/* Reserve fixed-capacity snapshot storage before a user command mutates a
|
||||
* project or publishes a candidate. Idempotent; failure retains history/redo.
|
||||
* The next record uses this storage without allocating. It remains reserved
|
||||
* across rejected commands and is freed on record or history destruction.
|
||||
* All calls are serialized on the control thread, never in the callback. */
|
||||
xtc_result xtc_history_reserve(xtc_history *history);
|
||||
/* Available adjacent steps; metadata-only, no cloning/allocation. */
|
||||
xtc_result xtc_history_counts(const xtc_history *history, size_t *undo_count, size_t *redo_count);
|
||||
xtc_result xtc_history_record(xtc_history * h, const xtc_project * p);
|
||||
/**
|
||||
* @brief Clones the adjacent undo or redo state without moving the history cursor.
|
||||
|
|
|
|||
|
|
@ -445,6 +445,26 @@ void GraphWidget::draw()
|
|||
* @return 1 when the event was consumed, 0 for unsupported left-button input,
|
||||
* or the result of Fl_Widget::handle() for unhandled events.
|
||||
*/
|
||||
bool GraphWidget::select_at(int left, int top)
|
||||
{
|
||||
for (size_t i = count_; i > 0; --i) {
|
||||
const auto &pos = positions_[i-1];
|
||||
if (left >= pos.left && left <= pos.left+node_width && top >= pos.top && top <= pos.top+node_height)
|
||||
return select_node(pos.id);
|
||||
}
|
||||
size_t nodes = 0, edges = 0;
|
||||
xtc_graph_get_counts(graph_,&nodes,&edges);
|
||||
for (size_t i = 0; i < edges; ++i) {
|
||||
xtc_connection_info edge{}; int sx, sy, dx, dy;
|
||||
xtc_graph_get_connection(graph_,i,&edge);
|
||||
if (!port_position(edge.source_node,1,sx,sy) || !port_position(edge.destination_node,0,dx,dy)) continue;
|
||||
double vx = dx-sx, vy = dy-sy, length = vx*vx+vy*vy;
|
||||
double t = length == 0 ? 0 : std::clamp(((left-sx)*vx+(top-sy)*vy)/length,0.0,1.0);
|
||||
if (std::hypot(left-sx-t*vx,top-sy-t*vy) <= 6) return select_connection(edge.source_node,edge.destination_node);
|
||||
}
|
||||
selected_node_ = SIZE_MAX; edge_selected_ = false; describe_selection(); redraw(); return false;
|
||||
}
|
||||
|
||||
int GraphWidget::handle(int event)
|
||||
{
|
||||
const int left = Fl::event_x() - x(), top = Fl::event_y() - y();
|
||||
|
|
@ -454,6 +474,7 @@ int GraphWidget::handle(int event)
|
|||
return 1;
|
||||
case FL_PUSH: {
|
||||
if (Fl::event_button() == FL_RIGHT_MOUSE) {
|
||||
take_focus(); select_at(left,top);
|
||||
Fl_Menu_Item items[5]{};
|
||||
items[0].text = "Inspect selection";
|
||||
items[1].text = "Arrange by signal flow (L)";
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ class GraphWidget : public Fl_Widget {
|
|||
* @return True when the node has a cached position, false otherwise.
|
||||
*/
|
||||
bool select_node(size_t id);
|
||||
bool select_at(int left, int top); // Canvas-local coordinates; selection only.
|
||||
|
||||
/**
|
||||
* @brief Selects a wire between two nodes and clears the node selection.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <FL/Fl_Group.H>
|
||||
#include <FL/Fl_Scroll.H>
|
||||
#include <FL/Fl_Menu_Bar.H>
|
||||
#include <FL/Fl_Input_.H>
|
||||
#include <FL/Fl_Tabs.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <cstdio>
|
||||
|
|
@ -37,6 +38,15 @@ void set_label(Fl_Box *box, const char *text)
|
|||
if (box->label() == nullptr || std::strcmp(box->label(), text) != 0)
|
||||
box->copy_label(text);
|
||||
}
|
||||
/* Paint progress without dispatching input/reentering edit commands. */
|
||||
class BusyOperation {
|
||||
Fl_Window &window_;
|
||||
public:
|
||||
BusyOperation(Fl_Window &window, Fl_Box *status, const char *message) : window_(window) {
|
||||
set_label(status,message); window_.cursor(FL_CURSOR_WAIT); Fl::flush();
|
||||
}
|
||||
~BusyOperation() { window_.cursor(FL_CURSOR_DEFAULT); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
|
|
@ -64,6 +74,7 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
history_.reset(history);
|
||||
begin();
|
||||
auto *menu = new Fl_Menu_Bar(0, 0, 1000, 28);
|
||||
menu_ = menu;
|
||||
menu->add("Edit/Undo", FL_CTRL + 'z', history_callback, this);
|
||||
menu->add("Edit/Redo", FL_CTRL + FL_SHIFT + 'z', history_callback, this);
|
||||
menu->add("Edit/Redo alternate", FL_CTRL + 'y', history_callback, this);
|
||||
|
|
@ -325,8 +336,11 @@ void MainWindow::report_error(const char *operation, xtc_result result)
|
|||
char text[384];
|
||||
const char *hint = std::strstr(operation, "Project") != nullptr
|
||||
? "Check the project path, file format and write permissions."
|
||||
: result == XTC_ERR_UNSUPPORTED ? "Rebuild with JACK development support."
|
||||
: "Check the JACK-compatible server and use Reconnect to retry.";
|
||||
: result == XTC_ERR_OUT_OF_MEMORY ? "Insufficient memory; the command was not accepted."
|
||||
: result == XTC_ERR_CAPACITY ? "A project capacity limit was reached."
|
||||
: result == XTC_ERR_NOT_FOUND || result == XTC_ERR_INVALID_ARGUMENT ? "Check the selected target and entered values."
|
||||
: result == XTC_ERR_UNSUPPORTED ? "Check optional build support and the requested format or plugin layout."
|
||||
: "Check the operation details; for audio failures use Reconnect to retry.";
|
||||
std::snprintf(text, sizeof(text), "%s: %s. %s", operation, xtc_result_string(result), hint);
|
||||
set_label(error_, text);
|
||||
}
|
||||
|
|
@ -360,6 +374,7 @@ xtc_result MainWindow::edit_graph(void *context, const xtc_graph_edit &edit, siz
|
|||
if (result == XTC_OK)
|
||||
result = xtc_project_set_graph(replacement, candidate);
|
||||
xtc_graph_destroy(candidate);
|
||||
if (result == XTC_OK) result = xtc_history_reserve(window->history_.get());
|
||||
if (result == XTC_OK && window->ready_)
|
||||
result = xtc_backend_submit(window->backend_, xtc_project_graph(replacement));
|
||||
if (result != XTC_OK) {
|
||||
|
|
@ -524,8 +539,18 @@ void MainWindow::reconnect_engine()
|
|||
* error indicators from the latest engine status, then collects backend
|
||||
* work.
|
||||
*/
|
||||
void MainWindow::update_history_controls()
|
||||
{
|
||||
size_t undo_count = 0, redo_count = 0;
|
||||
if (!menu_ || xtc_history_counts(history_.get(),&undo_count,&redo_count) != XTC_OK) return;
|
||||
for (const char *path : {"Edit/Undo","Edit/Redo","Edit/Redo alternate"}) {
|
||||
int index = menu_->find_index(path);
|
||||
if (index >= 0) menu_->mode(index,(!recorder_ && (std::strcmp(path,"Edit/Undo") == 0 ? undo_count : redo_count)) ? 0 : FL_MENU_INACTIVE);
|
||||
}
|
||||
}
|
||||
void MainWindow::refresh_status()
|
||||
{
|
||||
update_history_controls();
|
||||
if (mixer_ && !ready_) { mixer_->peaks(0, 0); mixer_->track_peaks(0, 0); }
|
||||
bool can_play = false, can_record = false;
|
||||
if (!ready_) {
|
||||
|
|
@ -652,6 +677,9 @@ bool MainWindow::close_window()
|
|||
*/
|
||||
int MainWindow::handle(int event)
|
||||
{
|
||||
if (event == FL_SHORTCUT && dynamic_cast<Fl_Input_ *>(Fl::focus()) &&
|
||||
(Fl::event_key() == ' ' || (Fl::event_state(FL_CTRL) &&
|
||||
(Fl::event_key() == 'z' || Fl::event_key() == 'y' || Fl::event_key() == 'd')))) return 0;
|
||||
if (event == FL_SHORTCUT && Fl::event_key() == ' ' && (Fl::event_state() & (FL_CTRL | FL_ALT | FL_META)) == 0) {
|
||||
if (running_)
|
||||
stop_engine();
|
||||
|
|
@ -766,6 +794,7 @@ void MainWindow::mark_dirty()
|
|||
if (piano_)
|
||||
piano_->set_project(project_.get(), piano_->clip());
|
||||
dirty_ = true;
|
||||
update_history_controls();
|
||||
copy_label("xtc * — unsaved changes");
|
||||
}
|
||||
|
||||
|
|
@ -925,6 +954,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
|
|||
*/
|
||||
xtc_result MainWindow::open_project(const char *path)
|
||||
{
|
||||
BusyOperation busy(*this,status_,"Loading and preparing project…");
|
||||
if (path == nullptr)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
project_issue_.clear();
|
||||
|
|
@ -1047,11 +1077,9 @@ void MainWindow::file_callback(Fl_Widget *widget, void *context)
|
|||
xtc_result MainWindow::before_arrangement_edit(void *context)
|
||||
{
|
||||
auto *window = static_cast<MainWindow *>(context);
|
||||
if (window->recorder_)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
if (!window->ready_)
|
||||
return XTC_OK;
|
||||
xtc_result r = xtc_backend_stop(window->backend_);
|
||||
if (window->recorder_) return XTC_ERR_INVALID_STATE;
|
||||
xtc_result r = xtc_history_reserve(window->history_.get());
|
||||
if (r == XTC_OK && window->ready_) r = xtc_backend_stop(window->backend_);
|
||||
window->refresh_status();
|
||||
return r;
|
||||
}
|
||||
|
|
@ -1069,6 +1097,7 @@ xtc_result MainWindow::before_arrangement_edit(void *context)
|
|||
*/
|
||||
xtc_result MainWindow::import_audio(const char *path)
|
||||
{
|
||||
BusyOperation busy(*this,status_,"Importing audio and preparing playback…");
|
||||
xtc_result r = before_arrangement_edit(this);
|
||||
if (r != XTC_OK)
|
||||
return r;
|
||||
|
|
@ -1131,8 +1160,7 @@ xtc_result MainWindow::record_audio(const char *path)
|
|||
(void)xtc_backend_set_recorder(backend_, nullptr);
|
||||
xtc_recorder_destroy(recorder_);
|
||||
recorder_ = nullptr;
|
||||
} else
|
||||
mark_dirty();
|
||||
}
|
||||
refresh_status();
|
||||
return r;
|
||||
}
|
||||
|
|
@ -1174,6 +1202,7 @@ void MainWindow::finish_recording()
|
|||
r = xtc_project_add_track(candidate, "Recording", &track);
|
||||
if (r == XTC_OK)
|
||||
r = xtc_project_import_audio(candidate, track, recording_path_.c_str(), recording_tick_, &clip);
|
||||
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
|
||||
if (r == XTC_OK)
|
||||
r = xtc_backend_prepare_project(backend_, candidate);
|
||||
if (r != XTC_OK) {
|
||||
|
|
@ -1264,6 +1293,9 @@ xtc_result MainWindow::open_piano_roll()
|
|||
auto *bar = static_cast<Fl_Menu_Bar *>(widget);
|
||||
owner->piano_->command(static_cast<int>(bar->mvalue() - bar->menu()) - 1);
|
||||
}, this);
|
||||
menu->add("Edit/Undo",FL_CTRL+'z',history_callback,this);
|
||||
menu->add("Edit/Redo",FL_CTRL+FL_SHIFT+'z',history_callback,this);
|
||||
menu->add("Edit/Redo alternate",FL_CTRL+'y',history_callback,this);
|
||||
auto *scroll = new Fl_Scroll(12, 48, 876, 560);
|
||||
piano_ = new PianoRollWidget(12, 48, project_.get(), clip.id, before_arrangement_edit, this);
|
||||
piano_->callback(
|
||||
|
|
@ -1334,6 +1366,12 @@ xtc_result MainWindow::undo(bool redo)
|
|||
{
|
||||
if (recorder_)
|
||||
return XTC_ERR_INVALID_STATE;
|
||||
const uint64_t track = sequencer_->selected_track(), clip = sequencer_->selected_clip();
|
||||
const uint64_t mixer_track = mixer_->selected_track();
|
||||
const size_t node = graph_widget_->selected_node();
|
||||
const uint64_t piano_clip = piano_ ? piano_->clip() : UINT64_MAX;
|
||||
const uint64_t note = piano_ ? piano_->selected_note() : UINT64_MAX;
|
||||
const bool piano_visible = piano_window_ && piano_window_->shown();
|
||||
xtc_project *candidate = nullptr;
|
||||
xtc_result r = xtc_history_checkout(history_.get(), redo ? 1 : 0, &candidate);
|
||||
if (r == XTC_OK)
|
||||
|
|
@ -1342,8 +1380,20 @@ xtc_result MainWindow::undo(bool redo)
|
|||
xtc_project_destroy(candidate);
|
||||
return r;
|
||||
}
|
||||
xtc_clip_info selected_clip{};
|
||||
if (xtc_project_find_clip(project_.get(),clip,&selected_clip) == XTC_OK)
|
||||
sequencer_->select(selected_clip.track_id,clip);
|
||||
else sequencer_->select(track);
|
||||
mixer_->select_track(mixer_track);
|
||||
graph_widget_->select_node(node);
|
||||
xtc_clip_info info{};
|
||||
if (piano_ && xtc_project_find_clip(project_.get(),piano_clip,&info) == XTC_OK && info.midi) {
|
||||
piano_->set_project(project_.get(),piano_clip); piano_->select_note(note);
|
||||
if (piano_visible) piano_window_->show();
|
||||
}
|
||||
r = xtc_history_accept(history_.get(), redo ? 1 : 0);
|
||||
dirty_ = true;
|
||||
update_history_controls();
|
||||
copy_label("xtc * — unsaved changes");
|
||||
return r;
|
||||
}
|
||||
|
|
@ -1373,6 +1423,7 @@ xtc_result MainWindow::set_control_map(unsigned int channel, const unsigned int
|
|||
xtc_project *candidate = nullptr;
|
||||
xtc_result r = xtc_project_clone(project_.get(),&candidate);
|
||||
if (r == XTC_OK) r = xtc_project_set_control_map(candidate,channel,controllers);
|
||||
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
|
||||
if (r == XTC_OK) r = install_project(candidate,false);
|
||||
if (r != XTC_OK) xtc_project_destroy(candidate); else mark_dirty();
|
||||
return r;
|
||||
|
|
@ -1402,6 +1453,7 @@ xtc_result MainWindow::install_plugin_graph(xtc_graph *graph)
|
|||
xtc_project *candidate = nullptr;
|
||||
xtc_result r = xtc_project_clone(project_.get(),&candidate);
|
||||
if (r == XTC_OK) r = xtc_project_set_graph(candidate,graph);
|
||||
if (r == XTC_OK) r = xtc_history_reserve(history_.get());
|
||||
if (r == XTC_OK) r = install_project(candidate,false);
|
||||
if (r != XTC_OK) xtc_project_destroy(candidate);
|
||||
else mark_dirty();
|
||||
|
|
@ -1409,6 +1461,7 @@ xtc_result MainWindow::install_plugin_graph(xtc_graph *graph)
|
|||
}
|
||||
xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *id)
|
||||
{
|
||||
BusyOperation busy(*this,status_,"Loading plugin and preparing playback…");
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
double rate = 48000;
|
||||
xtc_backend_status status{};
|
||||
|
|
@ -1429,6 +1482,7 @@ xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *
|
|||
}
|
||||
xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int index)
|
||||
{
|
||||
BusyOperation busy(*this,status_,"Replacing plugin and preparing playback…");
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
double rate = 48000; xtc_backend_status status{};
|
||||
if (ready_ && xtc_backend_get_status(backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class SequencerWidget;
|
|||
class Fl_Box;
|
||||
class Fl_Button;
|
||||
class Fl_Tabs;
|
||||
class Fl_Menu_Bar;
|
||||
|
||||
class MainWindow : public Fl_Double_Window {
|
||||
public:
|
||||
|
|
@ -168,6 +169,8 @@ class MainWindow : public Fl_Double_Window {
|
|||
|
||||
private:
|
||||
MixerWidget *mixer_ = nullptr;
|
||||
Fl_Menu_Bar *menu_ = nullptr;
|
||||
void update_history_controls();
|
||||
std::unique_ptr<xtc_history, decltype(&xtc_history_destroy)> history_{nullptr, xtc_history_destroy};
|
||||
/**
|
||||
* @brief FLTK menu callback for the Undo and Redo menu items.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <FL/Fl_Choice.H>
|
||||
#include <FL/Fl_Progress.H>
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
#include <FL/Fl_Input_.H>
|
||||
#include <FL/Fl_Value_Slider.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <FL/fl_draw.H>
|
||||
|
|
@ -459,6 +460,7 @@ void MixerWidget::select(Fl_Widget *, void *c)
|
|||
*/
|
||||
int MixerWidget::handle(int e)
|
||||
{
|
||||
if (e == FL_KEYDOWN && dynamic_cast<Fl_Input_ *>(Fl::focus())) return Fl_Group::handle(e);
|
||||
if (e == FL_KEYDOWN && !(Fl::event_state() & (FL_CTRL | FL_ALT | FL_META))) {
|
||||
int key = Fl::event_key();
|
||||
xtc_track_info t{};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <FL/Fl.H>
|
||||
#include <FL/fl_draw.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <FL/Fl_Menu_Item.H>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
|
|
@ -46,6 +47,19 @@ PianoRollWidget::PianoRollWidget(int left, int top, xtc_project *project, uint64
|
|||
* @param project Borrowed project that must outlive the widget.
|
||||
* @param clip ID of the MIDI clip to display.
|
||||
*/
|
||||
bool PianoRollWidget::select_note(uint64_t id)
|
||||
{
|
||||
size_t count = 0;
|
||||
xtc_project_note_count(project_,&count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
xtc_note_info note{};
|
||||
if (xtc_project_get_note(project_,i,¬e) == XTC_OK && note.id == id && visible(note)) {
|
||||
selected_ = id; all_ = false; mode_ = 0; redraw(); return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PianoRollWidget::set_project(xtc_project *project, uint64_t clip)
|
||||
{
|
||||
if (project != project_ || clip != clip_) {
|
||||
|
|
@ -248,8 +262,17 @@ int PianoRollWidget::handle(int event)
|
|||
}
|
||||
}
|
||||
if (selected_ != UINT64_MAX && Fl::event_button() == FL_RIGHT_MOUSE) {
|
||||
delete_selected();
|
||||
mode_ = 0;
|
||||
Fl_Menu_Item items[5]{};
|
||||
items[0].text = "Velocity and channel (V)";
|
||||
items[1].text = "Quantize (Q)";
|
||||
items[2].text = "Repeat (Ctrl+D)";
|
||||
items[3].text = "Delete (undoable)";
|
||||
const auto *picked = items->popup(Fl::event_x(),Fl::event_y());
|
||||
if (picked == items) command(2);
|
||||
else if (picked == items+1) command(3);
|
||||
else if (picked == items+2) command(5);
|
||||
else if (picked == items+3) delete_selected();
|
||||
} else if (Fl::event_button() == FL_LEFT_MOUSE && selected_ == UINT64_MAX && Fl::event_x() >= x() + key_width &&
|
||||
Fl::event_y() >= y() + header) {
|
||||
int pitch = 127 - (Fl::event_y() - y() - header) / row_height;
|
||||
|
|
@ -293,11 +316,11 @@ int PianoRollWidget::handle(int event)
|
|||
if (event == FL_KEYDOWN) {
|
||||
int key = Fl::event_key();
|
||||
if (key == 'a' && (Fl::event_state() & FL_CTRL)) { select_filtered(); return 1; }
|
||||
if (key == 'q') { command(3); return 1; }
|
||||
if (key == 'v') { command(2); return 1; }
|
||||
if (key == 'g') { command(0); return 1; }
|
||||
if (key == 'f') { command(1); return 1; }
|
||||
if (key == 't') { command(4); return 1; }
|
||||
if (!Fl::event_state(FL_CTRL|FL_ALT|FL_META) && key == 'q') { command(3); return 1; }
|
||||
if (!Fl::event_state(FL_CTRL|FL_ALT|FL_META) && key == 'v') { command(2); return 1; }
|
||||
if (!Fl::event_state(FL_CTRL|FL_ALT|FL_META) && key == 'g') { command(0); return 1; }
|
||||
if (!Fl::event_state(FL_CTRL|FL_ALT|FL_META) && key == 'f') { command(1); return 1; }
|
||||
if (!Fl::event_state(FL_CTRL|FL_ALT|FL_META) && key == 't') { command(4); return 1; }
|
||||
if (key == 'd' && (Fl::event_state() & FL_CTRL)) { command(5); return 1; }
|
||||
if (key == FL_Up || key == FL_Down) {
|
||||
xtc_note_edit edit{};
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ class PianoRollWidget : public Fl_Widget {
|
|||
*
|
||||
* @return XTC_OK on success, the guard's error, or the project error.
|
||||
*/
|
||||
uint64_t selected_note() const { return selected_; }
|
||||
bool select_note(uint64_t id);
|
||||
xtc_result delete_selected();
|
||||
/* UI preferences; independent of persisted project state. */
|
||||
xtc_result set_grid(uint64_t ticks);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,24 @@ SequencerWidget::SequencerWidget(int left, int top, xtc_project *project, Seek s
|
|||
* @param project Borrowed project; must outlive the widget's use of it.
|
||||
* @param reset Whether to clear selection, cursor, and drag state.
|
||||
*/
|
||||
bool SequencerWidget::select(uint64_t track, uint64_t clip)
|
||||
{
|
||||
size_t tracks = 0, clips = 0;
|
||||
xtc_project_get_counts(project_,&tracks,&clips);
|
||||
bool found = false;
|
||||
for (size_t i = 0; i < tracks; ++i) {
|
||||
xtc_track_info info{};
|
||||
if (xtc_project_get_track(project_,i,&info) == XTC_OK && info.id == track) found = true;
|
||||
}
|
||||
if (!found) return false;
|
||||
if (clip != UINT64_MAX) {
|
||||
xtc_clip_info info{};
|
||||
if (xtc_project_find_clip(project_,clip,&info) != XTC_OK || info.track_id != track) return false;
|
||||
}
|
||||
track_ = track; clip_ = clip; mode_ = 0; redraw();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SequencerWidget::set_project(xtc_project *project, bool reset)
|
||||
{
|
||||
project_ = project;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class SequencerWidget : public Fl_Widget {
|
|||
*
|
||||
* @return XTC_OK to allow the edit, or an error to cancel it.
|
||||
*/
|
||||
bool select(uint64_t track, uint64_t clip = UINT64_MAX);
|
||||
xtc_result duplicate_selection();
|
||||
xtc_result split_selection();
|
||||
xtc_result audio_properties(float gain, uint64_t fade_in, uint64_t fade_out);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
struct xtc_history {
|
||||
xtc_project *states[XTC_HISTORY_EDITS + 1];
|
||||
size_t count, cursor;
|
||||
xtc_project *reserved;
|
||||
uint64_t next_track, next_clip, next_note;
|
||||
size_t next_node;
|
||||
};
|
||||
|
|
@ -40,6 +41,7 @@ void xtc_history_destroy(xtc_history *h)
|
|||
return;
|
||||
for (i = 0; i < h->count; ++i)
|
||||
xtc_project_destroy(h->states[i]);
|
||||
xtc_project_destroy(h->reserved);
|
||||
free(h);
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,41 @@ xtc_result xtc_history_create(const xtc_project *p, xtc_history **out)
|
|||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_history_reserve(xtc_history *h)
|
||||
{
|
||||
if (!h) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (h->reserved) return XTC_OK;
|
||||
return xtc_project_create(&h->reserved);
|
||||
}
|
||||
xtc_result xtc_history_counts(const xtc_history *h, size_t *undo_count, size_t *redo_count)
|
||||
{
|
||||
if (!h || !undo_count || !redo_count || undo_count == redo_count) return XTC_ERR_INVALID_ARGUMENT;
|
||||
*undo_count = h->cursor;
|
||||
*redo_count = h->count-h->cursor-1;
|
||||
return XTC_OK;
|
||||
}
|
||||
/* Copy into the preallocated project and 64-node/4096-edge graph. No allocation
|
||||
* can fail after an accepted model command or successful engine publication. */
|
||||
static xtc_result capture(xtc_history *h, const xtc_project *p, xtc_project **out)
|
||||
{
|
||||
xtc_project *copy = h->reserved;
|
||||
xtc_graph *graph;
|
||||
xtc_node *nodes;
|
||||
xtc_edge *edges;
|
||||
if (!copy) return xtc_project_clone(p,out);
|
||||
if (p->graph->node_count > 64 || p->graph->edge_count > 4096) return XTC_ERR_CAPACITY;
|
||||
graph = copy->graph; nodes = graph->nodes; edges = graph->edges;
|
||||
*copy = *p;
|
||||
copy->graph = graph;
|
||||
*graph = *p->graph;
|
||||
graph->max_nodes = 64; graph->nodes = nodes; graph->edges = edges;
|
||||
memcpy(nodes,p->graph->nodes,p->graph->node_count*sizeof(*nodes));
|
||||
memcpy(edges,p->graph->edges,p->graph->edge_count*sizeof(*edges));
|
||||
h->reserved = NULL;
|
||||
*out = copy;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a project snapshot, discarding redo states and evicting the oldest state if full.
|
||||
*
|
||||
|
|
@ -87,7 +124,7 @@ xtc_result xtc_history_record(xtc_history *h, const xtc_project *p)
|
|||
xtc_result r;
|
||||
if (!h || !p)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = xtc_project_clone(p, ©);
|
||||
r = capture(h,p,©);
|
||||
if (r != XTC_OK)
|
||||
return r;
|
||||
remember_ids(h, p);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ namespace {
|
|||
xtc_backend handle;
|
||||
xtc_result seek_result = XTC_OK;
|
||||
xtc_result open_result = XTC_OK, start_result = XTC_OK, stop_result = XTC_OK, close_result = XTC_OK;
|
||||
xtc_result status_result = XTC_OK, submit_result = XTC_OK;
|
||||
xtc_result status_result = XTC_OK, submit_result = XTC_OK, reserve_result = XTC_OK;
|
||||
unsigned int submissions = 0U;
|
||||
xtc_backend_status engine_status = {};
|
||||
bool partial_open = false;
|
||||
|
|
@ -94,7 +94,7 @@ void reset()
|
|||
{
|
||||
CHECK(alive == 0U);
|
||||
open_result = start_result = stop_result = close_result = status_result = XTC_OK;
|
||||
submit_result = seek_result = XTC_OK;
|
||||
submit_result = seek_result = reserve_result = XTC_OK;
|
||||
engine_status = {};
|
||||
engine_status.sample_rate = 48000U;
|
||||
engine_status.buffer_frames = 1024U;
|
||||
|
|
@ -102,6 +102,11 @@ void reset()
|
|||
}
|
||||
}
|
||||
extern "C" {
|
||||
xtc_result __real_xtc_history_reserve(xtc_history *history);
|
||||
xtc_result __wrap_xtc_history_reserve(xtc_history *history) {
|
||||
return reserve_result == XTC_OK ? __real_xtc_history_reserve(history) : reserve_result;
|
||||
}
|
||||
|
||||
xtc_result xtc_backend_open(const xtc_backend_config *config, xtc_backend **out)
|
||||
{
|
||||
CHECK(config->kind == XTC_BACKEND_JACK && config->channel_count == 2U);
|
||||
|
|
@ -334,6 +339,8 @@ int main(int argc, char **argv)
|
|||
CHECK(sequence->position() == 1920); // Graph edits preserve the transport.
|
||||
engine_status.sample_position = 0;
|
||||
std::string take = std::string(directory) + "/take.wav";
|
||||
size_t before_take_nodes = 0, before_take_edges = 0;
|
||||
CHECK(xtc_graph_get_counts(canvas->graph(),&before_take_nodes,&before_take_edges) == XTC_OK);
|
||||
CHECK(window.record_audio(take.c_str()) == XTC_OK && handle.recorder != nullptr);
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_INVALID_STATE);
|
||||
CHECK(window.save_project(filename.c_str()) == XTC_ERR_INVALID_STATE);
|
||||
|
|
@ -349,6 +356,14 @@ int main(int argc, char **argv)
|
|||
press(stop);
|
||||
CHECK(handle.recorder == nullptr);
|
||||
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && clip_count == 2);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_project_get_counts(sequence->project(),&track_count,&clip_count) == XTC_OK && clip_count == 1);
|
||||
CHECK(access(take.c_str(),F_OK) == 0); // Undo never deletes the external take.
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
size_t after_take_nodes = 0, after_take_edges = 0;
|
||||
CHECK(xtc_graph_get_counts(canvas->graph(),&after_take_nodes,&after_take_edges) == XTC_OK && after_take_nodes+1 == before_take_nodes);
|
||||
CHECK(window.undo(true) == XTC_OK && window.undo(true) == XTC_OK);
|
||||
CHECK(xtc_project_get_counts(sequence->project(),&track_count,&clip_count) == XTC_OK && clip_count == 2);
|
||||
CHECK(sequence->waveform_count() == 1);
|
||||
// Short clips use their four-pixel draw width for hit testing too.
|
||||
xtc_clip_info audio_clip{};
|
||||
|
|
@ -656,6 +671,36 @@ int main(int argc, char **argv)
|
|||
CHECK(xtc_project_get_automation(sequence->project(),0,&point) == XTC_OK && point.step == 1);
|
||||
unlink("/tmp/xtc-gui-controls-test.xtc"); window.do_callback();
|
||||
}
|
||||
{
|
||||
reset(); MainWindow window(false);
|
||||
auto *sequence = timeline(&window); auto *canvas = graph_canvas(&window);
|
||||
CHECK(sequence->add_track() == XTC_OK && sequence->add_midi_clip() == XTC_OK);
|
||||
const uint64_t track = sequence->selected_track(), clip = sequence->selected_clip();
|
||||
CHECK(window.open_piano_roll() == XTC_OK);
|
||||
CHECK(window.piano_roll()->add_note(0,240,60) == XTC_OK);
|
||||
const uint64_t note = window.piano_roll()->selected_note();
|
||||
CHECK(window.piano_roll()->add_note(240,240,64) == XTC_OK);
|
||||
CHECK(window.piano_roll()->select_note(note));
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(sequence->selected_track() == track && sequence->selected_clip() == clip);
|
||||
CHECK(window.piano_roll()->clip() == clip && window.piano_roll()->selected_note() == note);
|
||||
const xtc_project *before = sequence->project(); unsigned int bindings[3] = {1,74,11};
|
||||
unsigned int old_submissions = submissions;
|
||||
reserve_result = XTC_ERR_OUT_OF_MEMORY;
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
|
||||
CHECK(window.set_control_map(0,bindings) == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
|
||||
CHECK(window.import_audio("/absent/not-loaded.wav") == XTC_ERR_OUT_OF_MEMORY && sequence->project() == before);
|
||||
CHECK(submissions == old_submissions);
|
||||
reserve_result = XTC_OK;
|
||||
CHECK(window.undo(true) == XTC_OK); // Failed commands preserved redo.
|
||||
size_t count = 0;
|
||||
CHECK(xtc_project_note_count(sequence->project(),&count) == XTC_OK && count == 2);
|
||||
CHECK(canvas->add_node(XTC_NODE_GAIN) == XTC_OK);
|
||||
const size_t node = canvas->selected_node(); int left, top;
|
||||
CHECK(canvas->node_position(node,left,top) && canvas->select_at(left+50,top+30) && canvas->selected_node() == node);
|
||||
CHECK(!canvas->select_at(-20,-20) && canvas->selected_node() == SIZE_MAX);
|
||||
window.do_callback();
|
||||
}
|
||||
if (const char *module = std::getenv("XTC_TEST_PLUGIN")) {
|
||||
reset();
|
||||
MainWindow window(false);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* reject host allocation/reclamation inside processing. No production hooks. */
|
||||
#include "xtc/xtc_realtime.h"
|
||||
#include "xtc/xtc_native.h"
|
||||
#include "xtc/xtc_control.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -83,9 +84,53 @@ static void graph_limits(void)
|
|||
xtc_plan_destroy(plan);
|
||||
}
|
||||
|
||||
static void history_reservations(void)
|
||||
{
|
||||
xtc_project *project = NULL, *snapshot = NULL;
|
||||
xtc_history *history = NULL;
|
||||
uint64_t track, bus;
|
||||
unsigned int map[3] = {1,74,11}, actual[3];
|
||||
size_t undo_count, redo_count, tracks, clips;
|
||||
int failure, succeeded = 0;
|
||||
assert(xtc_project_create(&project) == XTC_OK);
|
||||
assert(xtc_project_add_track(project,"History",&track) == XTC_OK);
|
||||
assert(xtc_history_create(project,&history) == XTC_OK);
|
||||
assert(xtc_project_set_mix(project,track,.5f,0,0,0) == XTC_OK);
|
||||
assert(xtc_history_record(history,project) == XTC_OK);
|
||||
assert(xtc_history_checkout(history,0,&snapshot) == XTC_OK && xtc_history_accept(history,0) == XTC_OK);
|
||||
xtc_project_destroy(project); project = snapshot; snapshot = NULL;
|
||||
for (failure = 0; failure < 12; ++failure) {
|
||||
xtc_result r;
|
||||
fail_after = failure;
|
||||
r = xtc_history_reserve(history);
|
||||
fail_after = -1;
|
||||
assert(xtc_history_counts(history,&undo_count,&redo_count) == XTC_OK && undo_count == 0 && redo_count == 1);
|
||||
if (r == XTC_OK) { succeeded = 1; break; }
|
||||
assert(r == XTC_ERR_OUT_OF_MEMORY);
|
||||
}
|
||||
assert(succeeded);
|
||||
fail_after = 0; // Fail every allocation: reserved record must still succeed.
|
||||
assert(xtc_history_reserve(history) == XTC_OK);
|
||||
assert(xtc_project_add_bus(project,"Bus",&bus) == XTC_OK);
|
||||
assert(xtc_project_route_track(project,track,bus) == XTC_OK);
|
||||
assert(xtc_project_set_control_map(project,0,map) == XTC_OK);
|
||||
assert(xtc_history_record(history,project) == XTC_OK);
|
||||
fail_after = -1;
|
||||
assert(xtc_history_counts(history,&undo_count,&redo_count) == XTC_OK && undo_count == 1 && redo_count == 0);
|
||||
assert(xtc_history_checkout(history,0,&snapshot) == XTC_OK);
|
||||
assert(xtc_project_get_counts(snapshot,&tracks,&clips) == XTC_OK && tracks == 1 && clips == 0);
|
||||
assert(xtc_project_get_control_map(snapshot,0,actual) == XTC_OK && actual[0] == 7);
|
||||
xtc_project_destroy(snapshot); snapshot = NULL;
|
||||
assert(xtc_history_accept(history,0) == XTC_OK && xtc_history_checkout(history,1,&snapshot) == XTC_OK);
|
||||
assert(xtc_project_get_counts(snapshot,&tracks,&clips) == XTC_OK && tracks == 2);
|
||||
assert(xtc_project_get_control_map(snapshot,0,actual) == XTC_OK && actual[0] == 1 && actual[1] == 74);
|
||||
xtc_project_destroy(snapshot); xtc_project_destroy(project); xtc_history_destroy(history);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
graph_limits();
|
||||
history_reservations();
|
||||
xtc_native_descriptor descriptor = {XTC_NATIVE_ABI_VERSION, sizeof(descriptor),
|
||||
"reliability.gain", XTC_NATIVE_EFFECT, prepare, reset, process, destroy};
|
||||
xtc_graph *graph = NULL;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue