Phase 25: improve LADSPA discovery, parameter editing and plugin lifecycle
This commit is contained in:
parent
2fd7724065
commit
7bff7b535d
18 changed files with 591 additions and 42 deletions
10
Makefile
10
Makefile
|
|
@ -139,14 +139,14 @@ endif
|
|||
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)/libxtc.a
|
||||
$(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
|
||||
|
||||
test-gui: $(BUILD_DIR)/test_gui
|
||||
xvfb-run -a -s "-screen 0 1280x800x24" ./$(BUILD_DIR)/test_gui
|
||||
|
||||
ifeq ($(JACK_AVAILABLE),yes)
|
||||
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.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)/libxtc.a
|
||||
$(BUILD_DIR)/test_gui_jack: $(BUILD_DIR)/tests/integration/test_gui_jack.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) $(JACK_LIBS) -lm -ldl
|
||||
|
||||
test-gui-jack: $(BUILD_DIR)/test_gui_jack
|
||||
|
|
@ -157,7 +157,7 @@ test-gui-jack:
|
|||
@exit 1
|
||||
endif
|
||||
|
||||
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.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)/libxtc.a
|
||||
$(BUILD_DIR)/xtc: $(BUILD_DIR)/src/gui/main.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) $(JACK_LIBS) -lm -ldl
|
||||
else
|
||||
test-gui-jack test-gui:
|
||||
|
|
@ -261,7 +261,7 @@ analyze:
|
|||
|
||||
.PHONY: test-workflow
|
||||
ifeq ($(GUI_AVAILABLE)$(JACK_AVAILABLE),yesyes)
|
||||
$(BUILD_DIR)/test_workflow: $(BUILD_DIR)/tests/integration/test_workflow.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)/libxtc.a
|
||||
$(BUILD_DIR)/test_workflow: $(BUILD_DIR)/tests/integration/test_workflow.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) $(JACK_LIBS) -lm -ldl
|
||||
|
||||
test-workflow: $(BUILD_DIR)/test_workflow
|
||||
|
|
@ -407,3 +407,5 @@ test-buses: $(BUILD_DIR)/test_buses
|
|||
./$(BUILD_DIR)/test_buses
|
||||
test: test-buses
|
||||
-include $(BUILD_DIR)/tests/unit/test_buses.d
|
||||
|
||||
-include $(BUILD_DIR)/src/gui/plugin_dialog.d
|
||||
|
|
|
|||
|
|
@ -141,3 +141,7 @@ indicators and atomic unity-gain insertion into a wire. See
|
|||
|
||||
Phase 24 adds one-level mix buses, routing, coherent track/bus peaks and master
|
||||
clipping indicators, persisted in v9 (reading v1–v8). See [MIXER.md](docs/MIXER.md).
|
||||
|
||||
Phase 25 adds LADSPA descriptor browsing, an identity/parameter editor with
|
||||
reset, transactional replacement and insertion into graph wires. See
|
||||
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md) for compatibility and lifecycle limits.
|
||||
|
|
|
|||
|
|
@ -47,3 +47,10 @@ PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.
|
|||
bus plugin inserts, per-track aux sends and sidechains remain deferred. Track
|
||||
meters are pre-bus; bus meters are pre-graph; master is post-graph. Sample peaks
|
||||
and sticky clipping do not provide loudness, true-peak analysis or limiting.
|
||||
|
||||
- Plugin discovery is an explicit-file descriptor browser, not a directory scan.
|
||||
Generic parameter dialogs own copied metadata; no native plugin editors or
|
||||
custom MIDI instrument chooser. LADSPA units are unspecified. Replacement
|
||||
requires the same mono audio layout and resets controls/automation. Missing
|
||||
modules reject Open while retaining the current project; GUI module relink for
|
||||
unopened documents is deferred. In-process plugin crashes/hangs are uncontained.
|
||||
|
|
|
|||
|
|
@ -30,13 +30,17 @@ LADSPA 1.1 host. `xtc_plugin.h` is the C API; no C++ engine or FLTK type is need
|
|||
|
||||
1. Choose **File → Load LADSPA plugin** and a trusted absolute `.so` path. Loading
|
||||
executes native code immediately, including constructors during discovery.
|
||||
2. The dialog lists compatible descriptor indices/labels. Select an index.
|
||||
2. The descriptor browser lists names, labels, makers and source/effect roles.
|
||||
Select a row and Choose. Counts report compatible and skipped descriptors.
|
||||
3. In the Modular Graph, connect the new source/effect output to an output or
|
||||
other effect; effects have an optional mono input. Unconnected effect inputs
|
||||
receive external input, as other graph processors do.
|
||||
4. Select its node and use **Edit → Selected plugin control**. The dialog shows
|
||||
actual port IDs, names, values and rate-adjusted ranges. Enter a port and value.
|
||||
Toggled controls require 0/1 and integer controls require integral values.
|
||||
4. Select its node and use **Edit → Selected plugin control**. The editor shows
|
||||
name, maker, copyright, module path, descriptor index and unique ID, then
|
||||
actual port IDs, editable values, rate-adjusted bounds, defaults and discrete/
|
||||
logarithmic hints. Apply validates all values and candidate initialization;
|
||||
Cancel and window close preserve the document. Toggled controls require 0/1
|
||||
and integer controls require integral values.
|
||||
5. Save normally, reopen, reconnect external JACK playback if needed, and Play.
|
||||
Plugin changes stop/rebuild playback and participate in ordinary undo/redo.
|
||||
|
||||
|
|
@ -103,3 +107,89 @@ bundled or installed by the target):
|
|||
Tested packages: ladspa 1.17-7 and swh-plugins 0.4.17-7. These zero-latency
|
||||
processors define the initial compatible set; other plugins remain unqualified.
|
||||
Physical listening and plugin CPU/endurance qualification remain pending.
|
||||
|
||||
## Phase 25: discovery, editing and graph lifecycle
|
||||
|
||||
Discovery remains explicit-file only, bounded to 1024 descriptor indices; no
|
||||
recursive scan, plugin database or new dependency is introduced. Unsupported
|
||||
layouts are skipped with counts; source/effect roles describe audio ports, not
|
||||
MIDI instrument capabilities. Built-in MIDI synthesis remains the only GUI MIDI
|
||||
instrument. LADSPA sources can be identified, edited, replaced and removed in
|
||||
the graph but cannot receive MIDI events.
|
||||
|
||||
**Edit → Insert LADSPA on selected wire** replaces one existing connection with
|
||||
`source -> effect -> destination`, as one undo step. Only one-input effects are
|
||||
accepted; stale wires, capacity failures and initialization failures retain the
|
||||
original graph. **Edit → Replace selected plugin** accepts the same audio input
|
||||
layout, keeps the node ID and all wires, and installs the new descriptor's default
|
||||
controls with no inherited automation. It can replace from another module.
|
||||
A source/effect layout mismatch is rejected. Undo restores the old identity,
|
||||
values and automation. Every candidate is compiled before adoption even when
|
||||
the backend is disconnected; no intermediate graph is published.
|
||||
|
||||
Ordering is graph signal flow, not descriptor order, creation order or a flat
|
||||
rack. Use the graph's connection/disconnection tools to place effects in series
|
||||
or parallel. Insertion on a wire preserves its endpoints and changes only that
|
||||
stage. Select a plugin and Delete to remove it and incident wires, then reconnect
|
||||
as desired; deletion does not automatically bridge or sum routes. A now-unwired
|
||||
effect receives external audio, following existing graph semantics. Undo/redo
|
||||
restores removal and routing. Independent bus inserts are still outside the
|
||||
shared-source graph model.
|
||||
|
||||
Reset defaults in the editor previews every control's declared/default-derived
|
||||
value. Apply also clears that plugin's automation; Cancel preserves it. The
|
||||
**Edit → Reset selected plugin** command does the same in one undo step. The
|
||||
public control-thread `xtc_plugin_reset` can reset one port (removing only its
|
||||
lane) or UINT32_MAX for all controls. Automation otherwise overrides static
|
||||
values, so editing a static control does not remove an existing lane.
|
||||
|
||||
`xtc_plugin_describe` returns copied name/maker/copyright (bounded/truncated),
|
||||
parameter ranges/defaults and integer/toggle/logarithmic/sample-rate/boundedness
|
||||
flags. Missing maker/copyright receive explicit fallback labels. LADSPA has no
|
||||
standard unit metadata: unit stays empty and the editor says unspecified rather
|
||||
than guessing Hz/dB from names. Numeric inputs use linear parameter values;
|
||||
logarithmic is a descriptor hint, not an extra host conversion. Unbounded ends
|
||||
are labeled unbounded; finite float extrema remain the validation bounds.
|
||||
Sample-rate hints scale bounds and defaults once at the engine's actual rate;
|
||||
nonrepresentable scaled bounds are rejected. Discrete defaults are rounded and
|
||||
validated; impossible defaults fail discovery rather than reaching the engine.
|
||||
|
||||
The generic modal editor owns copied metadata and temporary widget values only.
|
||||
It never owns or retains module handles, descriptors or audio instance pointers.
|
||||
Closing it cannot unload active DSP; Apply builds a new immutable plan before
|
||||
installation and existing retirement/quiescence rules release old instances.
|
||||
No embedded third-party editor UI is supported.
|
||||
|
||||
### Compatibility and recovery
|
||||
|
||||
LADSPA 1.1 descriptor identity is module path + index + label + unique ID, with
|
||||
exact supported port/control layout verified on restore. A unique ID is not a
|
||||
binary version/hash guarantee. Parameter values and bounded automation persist
|
||||
in existing project v9 records; this phase adds no file format change. Transient
|
||||
history/tails and opaque state remain unsupported. Native processor ABI 2 and
|
||||
its exact `struct_size` contract are unchanged; it remains parameter-only state.
|
||||
Consumers rebuild against public headers; the new copied details API adds no
|
||||
callbacks or GUI types to the C core.
|
||||
|
||||
Missing/incompatible modules reject GUI Open with the offending label/path and
|
||||
leave the current project intact. Restore a compatible module at that path and
|
||||
retry; use metadata-only validation/save to preserve an unavailable module's
|
||||
records. No dummy replacement silently removes its controls. Failed discovery,
|
||||
replacement or instantiate reports an error and retains the current document.
|
||||
A LADSPA `run` callback has no error return; finite-sample sanitation does not
|
||||
contain crashes, hangs, memory corruption or prohibited work inside a plugin.
|
||||
Only trusted in-process plugins are supported. See RECOVERY.md.
|
||||
|
||||
### Phase 25 verification
|
||||
|
||||
Fixture tests cover copied identity/hints, integer and toggle defaults and range
|
||||
rejection, logarithmic/sample-rate hints, scaled-range overflow rejection, reset
|
||||
and unchanged-on-error behavior, parameter/automation save/reopen and exact
|
||||
rendering. Noncommutative gain/offset effects prove that graph order changes
|
||||
output as expected. GUI tests drive the actual chooser/editor, reset/Apply and
|
||||
Cancel, replacement layout/init/missing-module rollback, insertion/stale-wire
|
||||
failure, deletion and undo. Existing fixture tests exercise 100 live plan
|
||||
replacements with host allocation/free/dlopen/dlclose guards. Native ABI tests
|
||||
cover process-error silence/reset. The separately installed amp/inverter/sine
|
||||
modules remain the qualified LADSPA set. Human listening, arbitrary third-party
|
||||
editors/plugins and extended realtime endurance remain unverified.
|
||||
|
|
|
|||
|
|
@ -57,3 +57,21 @@ float32 WAV from complete on-disk frames, ignoring a trailing partial frame.
|
|||
It never changes the source or overwrites an existing destination. This
|
||||
supersedes the earlier lack of a WAV recovery tool described above. See
|
||||
AUDIO_FILES.md for format/ownership/size limits and `make test-recording`.
|
||||
|
||||
## Plugin load and replacement failures (Phase 25)
|
||||
|
||||
GUI Open reports an unavailable/incompatible plugin's label and absolute module
|
||||
path and retains the current document. Preserve the saved file; restore a
|
||||
compatible module at its original path and retry Open. `xtc-check --validate`
|
||||
checks metadata without executing plugins, so success does not certify that a
|
||||
module exists or initializes. Metadata-only C load/save preserves unavailable
|
||||
plugin records. There is no automatic missing-plugin substitution or GUI relink
|
||||
for an unopened document.
|
||||
|
||||
For an already loaded project, Replace selected plugin preserves wires and node
|
||||
ID only for the same audio layout. New defaults replace controls/automation;
|
||||
Undo restores the original configuration. Failed discovery/compile/instantiate
|
||||
retains the original graph, controls and history position. Cancel in the generic
|
||||
parameter editor discards its temporary values. Reset defaults clears automation
|
||||
only when applied. No host recovery can contain a crash or hang in trusted native
|
||||
plugin code; LADSPA has no runtime error-return channel.
|
||||
|
|
|
|||
|
|
@ -625,10 +625,35 @@ Make instruments and effects practical to use, manage, and diagnose in real proj
|
|||
- Document plugin API versioning and compatibility expectations.
|
||||
|
||||
### Acceptance criteria
|
||||
- Users can identify and manage loaded instruments and effects without editing configuration files.
|
||||
- Missing or incompatible plugins produce actionable diagnostics and do not silently corrupt projects.
|
||||
- Plugin state restoration is tested for supported plugin types.
|
||||
- The engine remains safe if plugin initialization or processing fails, within the documented failure model.
|
||||
- [x] Users can identify and manage loaded instruments and effects without editing configuration files.
|
||||
- [x] Missing or incompatible plugins produce actionable diagnostics and do not silently corrupt projects.
|
||||
- [x] Plugin state restoration is tested for supported plugin types.
|
||||
- [x] The engine remains safe if plugin initialization or processing fails, within the documented failure model.
|
||||
|
||||
Implemented for the supported LADSPA 1.1 mono effects/audio sources: explicit
|
||||
module descriptor browser, copied identity and control metadata, generic parameter
|
||||
editor with validated Apply/Cancel and defaults reset, compatible-layout
|
||||
replacement preserving node ID/wires, and atomic effect insertion on a selected
|
||||
wire. Graph connections define order and existing deletion removes incident
|
||||
wires. Edits integrate with undo/redo and compile before adoption. State uses
|
||||
existing v9 parameter/automation records; missing/incompatible plugins retain the
|
||||
current document and report recovery guidance. Native processor ABI 2 remains
|
||||
unchanged. See PLUGIN_HOSTING.md and RECOVERY.md.
|
||||
|
||||
Validation: warning-enabled core/GUI builds; full unit/integration suite;
|
||||
fixture and installed amp/inverter/sine plugin tests; actual chooser/editor GUI
|
||||
tests; reset/cancel, insertion, replacement, removal and undo; layout/init/missing
|
||||
module rollback; exact persisted rendering and noncommutative processing order;
|
||||
callback allocation/module-lifetime guards, reliability and static analysis;
|
||||
software JACK GUI/audio/MIDI and workflow; dependency-free build; installation
|
||||
and package relocation; ASan/UBSan including generic-dialog lifetimes (existing
|
||||
fontconfig leak suppression for GUI/system font allocations).
|
||||
|
||||
Qualification remains the documented in-process failure model: LADSPA cannot
|
||||
signal run failure or contain crashes/hangs. No external editor, opaque state,
|
||||
MIDI-capable plugin instrument, recursive scanner or new format was added.
|
||||
Physical listening and arbitrary third-party plugin/endurance qualification
|
||||
remain pending.
|
||||
|
||||
## Phase 26 — Automation and Control Mapping
|
||||
|
||||
|
|
|
|||
|
|
@ -130,3 +130,11 @@ Phase 24: Mixer also offers Add bus and a Route choice for ordinary tracks.
|
|||
Select Track/Bus strips to edit their controls and automation. Master and selected
|
||||
strip meters show dBFS with 20 dB/s release; Reset peaks clears latched clipping.
|
||||
See MIXER.md for solo rules, gain staging and meter taps.
|
||||
|
||||
Phase 25: File → Load LADSPA plugin offers a descriptor browser. Select a graph
|
||||
plugin and Edit → Selected plugin control for identity, values, ranges and
|
||||
Reset defaults; Apply commits, Cancel discards. Edit also offers replacement,
|
||||
reset and insertion of an effect on a selected wire. Graph wires determine
|
||||
processing order; Delete removes a selected plugin and its wires. Changes are
|
||||
undoable. Replacement keeps compatible audio wiring but resets plugin controls
|
||||
and automation. See PLUGIN_HOSTING.md for supported formats and failure behavior.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,28 @@ typedef struct {
|
|||
unsigned int tempo, point_count; /* milli-BPM, 20000..300000 */
|
||||
xtc_plugin_point points[XTC_PLUGIN_POINTS];
|
||||
} xtc_plugin_spec;
|
||||
/* Copied discovery metadata, never descriptor/module pointers. LADSPA provides
|
||||
* no standardized units; unit is empty. Flags describe input control hints. */
|
||||
#define XTC_PLUGIN_INTEGER 1U
|
||||
#define XTC_PLUGIN_TOGGLED 2U
|
||||
#define XTC_PLUGIN_LOGARITHMIC 4U
|
||||
#define XTC_PLUGIN_SAMPLE_RATE 8U
|
||||
#define XTC_PLUGIN_BOUNDED_BELOW 16U
|
||||
#define XTC_PLUGIN_BOUNDED_ABOVE 32U
|
||||
typedef struct {
|
||||
char name[128], maker[128], copyright[128];
|
||||
unsigned int hard_realtime;
|
||||
xtc_parameter_info parameters[XTC_PLUGIN_CONTROLS];
|
||||
unsigned int flags[XTC_PLUGIN_CONTROLS];
|
||||
} xtc_plugin_details;
|
||||
/* Control-thread identity/layout verification and copied human-readable hints. */
|
||||
xtc_result xtc_plugin_describe(const xtc_plugin_spec *spec, double rate, xtc_plugin_details *out);
|
||||
/* Reset static values to defaults, removing automation for the chosen port.
|
||||
* UINT32_MAX resets all controls. Failure leaves spec unchanged. */
|
||||
xtc_result xtc_plugin_reset(xtc_plugin_spec *spec, double rate, unsigned int port);
|
||||
/* Replace identity/controls at a stable node ID; only identical audio layouts
|
||||
* are accepted, preserving all wires. Metadata-only; compile before adoption. */
|
||||
xtc_result xtc_graph_replace_plugin(xtc_graph *graph, size_t id, const xtc_plugin_spec *spec);
|
||||
typedef struct xtc_plugin xtc_plugin;
|
||||
/* Control thread only. Probe explicitly enumerates a file by descriptor index;
|
||||
* NOT_FOUND means end, UNSUPPORTED means descriptor/layout unsupported. Loading
|
||||
|
|
|
|||
|
|
@ -597,6 +597,18 @@ xtc_result xtc_graph_set_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *
|
|||
g->nodes[i].plugin = *s; return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_graph_replace_plugin(xtc_graph *g, size_t id, const xtc_plugin_spec *s)
|
||||
{
|
||||
size_t i;
|
||||
if (!g || xtc_plugin_spec_validate(s) != XTC_OK) return XTC_ERR_INVALID_ARGUMENT;
|
||||
i = node_index(g,id);
|
||||
if (i == SIZE_MAX) return XTC_ERR_NOT_FOUND;
|
||||
if (g->nodes[i].type != XTC_NODE_PLUGIN) return XTC_ERR_INVALID_ARGUMENT;
|
||||
if (g->nodes[i].plugin.inputs != s->inputs) return XTC_ERR_UNSUPPORTED;
|
||||
g->nodes[i].plugin = *s;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
||||
xtc_result xtc_graph_analyze(const xtc_graph *graph, xtc_routing_info *items, size_t capacity, size_t *count)
|
||||
{
|
||||
xtc_routing_info info[XTC_MAX_NODES] = {{0}};
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class GraphWidget : public Fl_Widget {
|
|||
*/
|
||||
xtc_result add_node(xtc_node_type type);
|
||||
xtc_result insert_gain();
|
||||
bool selected_connection(xtc_connection_info &out) const { if (!edge_selected_) return false; out = selected_edge_; return true; }
|
||||
bool auto_layout(); // Presentation only, no engine publication or undo step.
|
||||
bool routing(size_t id, xtc_routing_info &info) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "sequencer_widget.hpp"
|
||||
#include "piano_roll.hpp"
|
||||
#include "mixer_widget.hpp"
|
||||
#include "plugin_dialog.hpp"
|
||||
#include <FL/Fl_Native_File_Chooser.H>
|
||||
#include "xtc/xtc_version.h"
|
||||
|
||||
|
|
@ -76,6 +77,9 @@ MainWindow::MainWindow(bool prompt_unsaved)
|
|||
menu->add("View/Piano roll", FL_CTRL + '4', piano_callback, this);
|
||||
menu->add("Audio/Record WAV", FL_SHIFT + FL_F + 5, audio_file_callback, this);
|
||||
menu->add("File/Load LADSPA plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Replace selected plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Insert LADSPA on selected wire", 0, plugin_callback, this);
|
||||
menu->add("Edit/Reset selected plugin", 0, plugin_callback, this);
|
||||
menu->add("Edit/Selected plugin automation", 0, plugin_callback, this);
|
||||
menu->add("Edit/Selected plugin control", 0, plugin_callback, this);
|
||||
menu->add("File/Quit", FL_CTRL + 'q', close_callback, this);
|
||||
|
|
@ -1383,11 +1387,58 @@ xtc_result MainWindow::add_plugin(const char *path, unsigned int index, size_t *
|
|||
xtc_graph *graph = nullptr; size_t added = SIZE_MAX;
|
||||
if (r == XTC_OK) r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
||||
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
|
||||
xtc_plan *check = nullptr;
|
||||
if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check);
|
||||
xtc_plan_destroy(check);
|
||||
if (r == XTC_OK) r = install_plugin_graph(graph);
|
||||
xtc_graph_destroy(graph);
|
||||
if (r == XTC_OK) graph_widget_->select_node(added);
|
||||
if (r == XTC_OK && id) *id = added;
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::replace_plugin(size_t id, const char *path, unsigned int index)
|
||||
{
|
||||
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;
|
||||
xtc_plugin_spec spec{};
|
||||
xtc_result r = xtc_plugin_probe(path,index,rate,&spec);
|
||||
if (r == XTC_OK) r = apply_plugin_spec(id,spec,true);
|
||||
if (r == XTC_OK) graph_widget_->select_node(id);
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::reset_plugin(size_t id, unsigned int port)
|
||||
{
|
||||
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;
|
||||
xtc_plugin_spec spec{};
|
||||
xtc_result r = xtc_graph_get_plugin(xtc_project_graph(project_.get()),id,&spec);
|
||||
if (r == XTC_OK) r = xtc_plugin_reset(&spec,rate,port);
|
||||
return r == XTC_OK ? apply_plugin_spec(id,spec) : r;
|
||||
}
|
||||
xtc_result MainWindow::insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id)
|
||||
{
|
||||
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;
|
||||
xtc_plugin_spec spec{};
|
||||
xtc_result r = xtc_plugin_probe(path,index,rate,&spec);
|
||||
if (r == XTC_OK && !spec.inputs) r = XTC_ERR_UNSUPPORTED;
|
||||
xtc_graph *graph = nullptr; size_t added = SIZE_MAX;
|
||||
xtc_graph_edit edit{}; edit.kind = XTC_EDIT_DISCONNECT; edit.data.connection = wire;
|
||||
if (r == XTC_OK) r = xtc_graph_apply_edit(xtc_project_graph(project_.get()),&edit,&graph,nullptr);
|
||||
if (r == XTC_OK) r = xtc_graph_add_plugin(graph,&spec,&added);
|
||||
if (r == XTC_OK) r = xtc_graph_connect_ports(graph,wire.source_node,wire.source_port,added,0);
|
||||
if (r == XTC_OK) r = xtc_graph_connect_ports(graph,added,1,wire.destination_node,wire.destination_port);
|
||||
xtc_plan *check = nullptr;
|
||||
if (r == XTC_OK) r = xtc_graph_compile(graph,rate,256,&check);
|
||||
xtc_plan_destroy(check);
|
||||
if (r == XTC_OK) r = install_plugin_graph(graph);
|
||||
xtc_graph_destroy(graph);
|
||||
if (r == XTC_OK) { graph_widget_->select_node(added); if (id) *id = added; }
|
||||
return r;
|
||||
}
|
||||
xtc_result MainWindow::set_plugin_control(size_t id, unsigned int port, float value)
|
||||
{
|
||||
if (recorder_) return XTC_ERR_INVALID_STATE;
|
||||
|
|
@ -1419,11 +1470,11 @@ xtc_result MainWindow::set_plugin_automation(size_t id, const xtc_plugin_point &
|
|||
}
|
||||
return apply_plugin_spec(id,spec);
|
||||
}
|
||||
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec)
|
||||
xtc_result MainWindow::apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace)
|
||||
{
|
||||
xtc_graph *graph = nullptr;
|
||||
xtc_result r = xtc_graph_clone(xtc_project_graph(project_.get()),&graph);
|
||||
if (r == XTC_OK) r = xtc_graph_set_plugin(graph,id,&spec);
|
||||
if (r == XTC_OK) r = replace ? xtc_graph_replace_plugin(graph,id,&spec) : xtc_graph_set_plugin(graph,id,&spec);
|
||||
/* Compile before adoption even with a mocked/disconnected backend. */
|
||||
xtc_plan *check = nullptr;
|
||||
xtc_backend_status status{}; double rate = 48000;
|
||||
|
|
@ -1440,30 +1491,52 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
|||
auto *menu = static_cast<Fl_Menu_Bar *>(widget);
|
||||
const char *label = menu->mvalue()->label();
|
||||
xtc_result r;
|
||||
if (std::strcmp(label,"Load LADSPA plugin") == 0) {
|
||||
if (std::strcmp(label,"Reset selected plugin") == 0) {
|
||||
r = w->reset_plugin(w->graph_widget_->selected_node());
|
||||
} else if (std::strcmp(label,"Load LADSPA plugin") == 0 ||
|
||||
std::strcmp(label,"Replace selected plugin") == 0 ||
|
||||
std::strcmp(label,"Insert LADSPA on selected wire") == 0) {
|
||||
const size_t selected = w->graph_widget_->selected_node();
|
||||
xtc_connection_info wire{};
|
||||
const bool insert = std::strcmp(label,"Insert LADSPA on selected wire") == 0;
|
||||
const bool replace = std::strcmp(label,"Replace selected plugin") == 0;
|
||||
if (insert && !w->graph_widget_->selected_connection(wire)) {
|
||||
w->report_error("Select an existing wire before inserting an effect",XTC_ERR_INVALID_STATE); return;
|
||||
}
|
||||
if (replace) {
|
||||
xtc_plugin_spec old{};
|
||||
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),selected,&old);
|
||||
if (r != XTC_OK) { w->report_error("Select a LADSPA node to replace",r); return; }
|
||||
}
|
||||
Fl_Native_File_Chooser chooser;
|
||||
chooser.title("Load trusted LADSPA module (executes native code)"); chooser.filter("LADSPA\t*.so");
|
||||
chooser.title("Choose trusted LADSPA module (executes native code)"); chooser.filter("LADSPA\t*.so");
|
||||
if (chooser.show() != 0 || !chooser.filename()) return;
|
||||
double rate = 48000; xtc_backend_status status{};
|
||||
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||
std::string choices = "Compatible descriptors (index: label):\n";
|
||||
for (unsigned i = 0; i < 1024; ++i) {
|
||||
xtc_plugin_spec spec{};
|
||||
r = xtc_plugin_probe(chooser.filename(),i,rate,&spec);
|
||||
if (r == XTC_ERR_NOT_FOUND) break;
|
||||
if (r == XTC_OK) choices += std::to_string(i) + ": " + spec.label + "\n";
|
||||
unsigned int index = 0;
|
||||
xtc_plugin_spec probe{};
|
||||
r = xtc_plugin_probe(chooser.filename(),0,rate,&probe);
|
||||
if (r != XTC_OK && r != XTC_ERR_UNSUPPORTED) {
|
||||
w->report_error("Cannot discover LADSPA module; check path, dependencies and ladspa_descriptor export",r); return;
|
||||
}
|
||||
choices += "Enter descriptor index:";
|
||||
const char *answer = fl_input("%s","0",choices.c_str());
|
||||
if (!answer) return;
|
||||
char *end = nullptr; errno = 0; unsigned long index = std::strtoul(answer,&end,10);
|
||||
if (errno || !answer[0] || *end || index >= 1024) r = XTC_ERR_INVALID_ARGUMENT;
|
||||
else r = w->add_plugin(chooser.filename(),static_cast<unsigned int>(index));
|
||||
if (!xtc_choose_plugin(chooser.filename(),rate,index)) return;
|
||||
if (replace) r = w->replace_plugin(selected,chooser.filename(),index);
|
||||
else if (insert) r = w->insert_plugin(wire,chooser.filename(),index);
|
||||
else r = w->add_plugin(chooser.filename(),index);
|
||||
} else {
|
||||
size_t id = w->graph_widget_->selected_node(); xtc_plugin_spec spec{};
|
||||
r = xtc_graph_get_plugin(xtc_project_graph(w->project_.get()),id,&spec);
|
||||
if (r == XTC_OK) {
|
||||
std::string prompt = std::string(spec.label) + " control ports and values:\n";
|
||||
if (std::strcmp(label,"Selected plugin control") == 0) {
|
||||
double rate = 48000; xtc_backend_status status{};
|
||||
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||
xtc_plugin_details details{}; xtc_plugin_spec edited{};
|
||||
r = xtc_plugin_describe(&spec,rate,&details);
|
||||
if (r == XTC_OK && xtc_edit_plugin(spec,details,edited)) r = w->apply_plugin_spec(id,edited);
|
||||
if (r != XTC_OK) w->report_error("Plugin editor failed; configuration retained. Check module identity and control ranges",r);
|
||||
return;
|
||||
}
|
||||
xtc_parameter_info info[XTC_PLUGIN_CONTROLS]{};
|
||||
double rate = 48000; xtc_backend_status status{};
|
||||
if (w->ready_ && xtc_backend_get_status(w->backend_,&status) == XTC_OK) rate = status.sample_rate;
|
||||
|
|
@ -1487,12 +1560,6 @@ void MainWindow::plugin_callback(Fl_Widget *widget, void *context)
|
|||
if (r != XTC_OK) w->report_error("Plugin automation rejected: check tick, port, range and discrete mode",r);
|
||||
return;
|
||||
}
|
||||
prompt += "Enter port and value (for example: 2 0.5):";
|
||||
const char *answer = fl_input("%s","",prompt.c_str());
|
||||
if (!answer) return;
|
||||
unsigned port; float value; char extra;
|
||||
if (std::sscanf(answer,"%u %f %c",&port,&value,&extra) != 2 || !std::isfinite(value)) r = XTC_ERR_INVALID_ARGUMENT;
|
||||
else r = w->set_plugin_control(id,port,value);
|
||||
}
|
||||
}
|
||||
if (r != XTC_OK) w->report_error("Plugin change failed: check module, mono layout, control port/range and JACK format",r);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ class MainWindow : public Fl_Double_Window {
|
|||
/* Control-thread plugin commands, transactional model adoption. */
|
||||
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
|
||||
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
|
||||
xtc_result replace_plugin(size_t id, const char *path, unsigned int index);
|
||||
xtc_result reset_plugin(size_t id, unsigned int port = UINT32_MAX);
|
||||
xtc_result insert_plugin(const xtc_connection_info &wire, const char *path, unsigned int index, size_t *id = nullptr);
|
||||
xtc_result set_plugin_control(size_t id, unsigned int port, float value);
|
||||
/**
|
||||
* @brief Constructs the window, its widgets, and the initial project.
|
||||
|
|
@ -290,7 +293,7 @@ class MainWindow : public Fl_Double_Window {
|
|||
* @param context MainWindow * owning the project and file path.
|
||||
*/
|
||||
static void plugin_callback(Fl_Widget *widget, void *context);
|
||||
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec);
|
||||
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false);
|
||||
xtc_result install_plugin_graph(xtc_graph *graph);
|
||||
static void file_callback(Fl_Widget * widget, void *context);
|
||||
GraphWidget *graph_widget_ = nullptr;
|
||||
|
|
|
|||
103
src/gui/plugin_dialog.cpp
Normal file
103
src/gui/plugin_dialog.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "plugin_dialog.hpp"
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Double_Window.H>
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
#include <FL/Fl_Button.H>
|
||||
#include <FL/Fl_Return_Button.H>
|
||||
#include <FL/Fl_Value_Input.H>
|
||||
#include <FL/Fl_Scroll.H>
|
||||
#include <FL/fl_ask.H>
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
struct Dialog : Fl_Double_Window {
|
||||
bool accepted = false;
|
||||
Dialog(int width, int height, const char *title) : Fl_Double_Window(width,height,title) { set_modal(); }
|
||||
static void accept(Fl_Widget *, void *data) { auto *d = static_cast<Dialog *>(data); d->accepted = true; d->hide(); }
|
||||
static void cancel(Fl_Widget *, void *data) { static_cast<Dialog *>(data)->hide(); }
|
||||
bool run() { callback(cancel,this); end(); show(); while (shown()) Fl::wait(); return accepted; }
|
||||
};
|
||||
}
|
||||
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index)
|
||||
{
|
||||
Dialog dialog(760,450,"LADSPA descriptors");
|
||||
auto *heading = new Fl_Box(15,10,730,40);
|
||||
heading->copy_label(path); heading->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_CLIP);
|
||||
auto *list = new Fl_Hold_Browser(15,55,730,325);
|
||||
list->format_char(0); // Plugin names are data, never browser formatting.
|
||||
std::vector<unsigned int> indices;
|
||||
size_t rejected = 0;
|
||||
for (unsigned int i = 0; i < 1024; ++i) {
|
||||
xtc_plugin_spec spec{}; xtc_plugin_details details{};
|
||||
xtc_result r = xtc_plugin_probe(path,i,rate,&spec);
|
||||
if (r == XTC_ERR_NOT_FOUND) break;
|
||||
if (r != XTC_OK) { ++rejected; continue; }
|
||||
if (xtc_plugin_describe(&spec,rate,&details) != XTC_OK) { ++rejected; continue; }
|
||||
std::string row = std::to_string(i) + ": " + details.name + " (" + spec.label + ") — " + details.maker +
|
||||
(spec.inputs ? " [effect]" : " [source]");
|
||||
list->add(row.c_str()); indices.push_back(i);
|
||||
}
|
||||
auto *status = new Fl_Box(15,385,520,50);
|
||||
std::string summary = std::to_string(indices.size()) + " compatible; " + std::to_string(rejected) +
|
||||
" unsupported/unavailable. Select a descriptor.";
|
||||
status->copy_label(summary.c_str()); status->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
|
||||
auto *cancel = new Fl_Button(540,400,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog);
|
||||
auto *choose = new Fl_Return_Button(645,400,100,30,"Choose"); choose->callback(Dialog::accept,&dialog);
|
||||
if (indices.empty()) choose->deactivate(); else list->select(1);
|
||||
if (!dialog.run() || list->value() < 1) return false;
|
||||
index = indices[static_cast<size_t>(list->value()-1)]; return true;
|
||||
}
|
||||
bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details, xtc_plugin_spec &edited)
|
||||
{
|
||||
Dialog dialog(800,600,"Plugin identity and controls");
|
||||
auto *identity = new Fl_Box(15,10,770,125);
|
||||
std::string text = std::string(details.name) + " — " + details.maker + "\n" + spec.label +
|
||||
" | LADSPA ID " + std::to_string(spec.unique_id) + " | descriptor " + std::to_string(spec.index) +
|
||||
"\n" + spec.path + "\n" + details.copyright +
|
||||
"\nMetadata verified; Apply validates initialization. Units unspecified by LADSPA.";
|
||||
identity->copy_label(text.c_str()); identity->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE|FL_ALIGN_WRAP);
|
||||
auto *scroll = new Fl_Scroll(10,140,780,370);
|
||||
std::array<Fl_Value_Input *,XTC_PLUGIN_CONTROLS> inputs{};
|
||||
for (unsigned int i = 0; i < spec.control_count; ++i) {
|
||||
int y = 150 + static_cast<int>(i)*65;
|
||||
const auto &p = details.parameters[i];
|
||||
std::string name = std::to_string(p.id) + ": " + p.name;
|
||||
auto *label = new Fl_Box(25,y,425,25); label->copy_label(name.c_str()); label->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||
inputs[i] = new Fl_Value_Input(460,y,150,25); inputs[i]->value(spec.values[i]);
|
||||
inputs[i]->step((details.flags[i] & (XTC_PLUGIN_INTEGER|XTC_PLUGIN_TOGGLED)) ? 1 : 0.001);
|
||||
std::string hint = "Range " + std::string(details.flags[i] & XTC_PLUGIN_BOUNDED_BELOW ? std::to_string(p.minimum) : "unbounded") +
|
||||
" .. " + (details.flags[i] & XTC_PLUGIN_BOUNDED_ABOVE ? std::to_string(p.maximum) : "unbounded") +
|
||||
"; default " + std::to_string(p.default_value) +
|
||||
((details.flags[i] & XTC_PLUGIN_TOGGLED) ? "; toggle 0/1" : (details.flags[i] & XTC_PLUGIN_INTEGER) ? "; integer" : "; continuous") +
|
||||
((details.flags[i] & XTC_PLUGIN_LOGARITHMIC) ? "; logarithmic hint" : "");
|
||||
auto *range = new Fl_Box(25,y+27,715,25); range->copy_label(hint.c_str()); range->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
|
||||
}
|
||||
scroll->end();
|
||||
auto *note = new Fl_Box(15,515,770,30,"Automation overrides static controls. Reset defaults also clears all plugin automation.");
|
||||
note->labelsize(12);
|
||||
struct Reset { const xtc_plugin_details *details; std::array<Fl_Value_Input *,XTC_PLUGIN_CONTROLS> *inputs; unsigned int count; bool used; } reset{&details,&inputs,spec.control_count,false};
|
||||
auto *defaults = new Fl_Button(20,555,170,30,"Reset defaults");
|
||||
defaults->callback([](Fl_Widget *, void *data) {
|
||||
auto *r = static_cast<Reset *>(data); r->used = true;
|
||||
for (unsigned int i = 0; i < r->count; ++i) (*r->inputs)[i]->value(r->details->parameters[i].default_value);
|
||||
},&reset);
|
||||
auto *cancel = new Fl_Button(570,555,90,30,"Cancel"); cancel->callback(Dialog::cancel,&dialog);
|
||||
auto *apply = new Fl_Return_Button(675,555,105,30,"Apply"); apply->callback(Dialog::accept,&dialog);
|
||||
if (!dialog.run()) return false;
|
||||
xtc_plugin_spec candidate = spec;
|
||||
for (unsigned int i = 0; i < spec.control_count; ++i) {
|
||||
double value = inputs[i]->value(); const auto &p = details.parameters[i]; unsigned int flags = details.flags[i];
|
||||
if (!std::isfinite(value) || value < p.minimum || value > p.maximum ||
|
||||
((flags & XTC_PLUGIN_INTEGER) && value != std::floor(value)) ||
|
||||
((flags & XTC_PLUGIN_TOGGLED) && value != 0 && value != 1)) {
|
||||
fl_alert("Invalid value for port %u (%s); no controls were changed.",p.id,p.name); return false;
|
||||
}
|
||||
candidate.values[i] = static_cast<float>(value);
|
||||
}
|
||||
if (reset.used) candidate.point_count = 0;
|
||||
edited = candidate; return true;
|
||||
}
|
||||
9
src/gui/plugin_dialog.hpp
Normal file
9
src/gui/plugin_dialog.hpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#ifndef XTC_PLUGIN_DIALOG_HPP
|
||||
#define XTC_PLUGIN_DIALOG_HPP
|
||||
#include "xtc/xtc_plugin.h"
|
||||
/* Modal, control-thread-only views. They own copied metadata and widget state,
|
||||
* never engine/module handles. Cancel/close leaves caller configuration intact. */
|
||||
bool xtc_choose_plugin(const char *path, double rate, unsigned int &index);
|
||||
bool xtc_edit_plugin(const xtc_plugin_spec &spec, const xtc_plugin_details &details,
|
||||
xtc_plugin_spec &edited);
|
||||
#endif
|
||||
|
|
@ -116,6 +116,9 @@ static xtc_result inspect(const LADSPA_Descriptor *d, double rate, xtc_plugin_sp
|
|||
const LADSPA_PortRangeHint *h = d->PortRangeHints+i;
|
||||
if ((LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && !isfinite(h->LowerBound)) ||
|
||||
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && !isfinite(h->UpperBound)) ||
|
||||
(LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) &&
|
||||
((LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && fabs((double)h->LowerBound*rate) > FLT_MAX) ||
|
||||
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) && fabs((double)h->UpperBound*rate) > FLT_MAX))) ||
|
||||
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) && LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) &&
|
||||
h->LowerBound > h->UpperBound)) return XTC_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
|
@ -135,10 +138,10 @@ xtc_result xtc_plugin_probe(const char *path, unsigned int index, double rate, x
|
|||
if (r == XTC_OK) { strcpy(s.path,path); s.index = index; *out = s; }
|
||||
dlclose(module); return r;
|
||||
}
|
||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||
xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double rate, xtc_plugin_details *out)
|
||||
{
|
||||
void *module = NULL; const LADSPA_Descriptor *d; xtc_plugin_spec actual = {0};
|
||||
xtc_result r; size_t i;
|
||||
xtc_result r; size_t i; xtc_plugin_details details = {0};
|
||||
if (!out || xtc_plugin_spec_validate(s) != XTC_OK || !isfinite(rate) || rate < 1 || rate > 768000)
|
||||
return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = load(s->path,s->index,&module,&d); if (r != XTC_OK) return r;
|
||||
|
|
@ -153,7 +156,20 @@ xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_para
|
|||
snprintf(p.name,sizeof(p.name),"%s",d->PortNames && d->PortNames[p.id] ? d->PortNames[p.id] : "Control");
|
||||
p.minimum = LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) ? (float)(h->LowerBound*scale) : -FLT_MAX;
|
||||
p.maximum = LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? (float)(h->UpperBound*scale) : FLT_MAX;
|
||||
p.default_value = actual.values[i]; out[i] = p;
|
||||
p.default_value = actual.values[i]; details.parameters[i] = p;
|
||||
details.flags[i] = (LADSPA_IS_HINT_INTEGER(h->HintDescriptor) ? XTC_PLUGIN_INTEGER : 0U) |
|
||||
(LADSPA_IS_HINT_TOGGLED(h->HintDescriptor) ? XTC_PLUGIN_TOGGLED : 0U) |
|
||||
(LADSPA_IS_HINT_LOGARITHMIC(h->HintDescriptor) ? XTC_PLUGIN_LOGARITHMIC : 0U) |
|
||||
(LADSPA_IS_HINT_SAMPLE_RATE(h->HintDescriptor) ? XTC_PLUGIN_SAMPLE_RATE : 0U) |
|
||||
(LADSPA_IS_HINT_BOUNDED_BELOW(h->HintDescriptor) ? XTC_PLUGIN_BOUNDED_BELOW : 0U) |
|
||||
(LADSPA_IS_HINT_BOUNDED_ABOVE(h->HintDescriptor) ? XTC_PLUGIN_BOUNDED_ABOVE : 0U);
|
||||
}
|
||||
if (r == XTC_OK) {
|
||||
snprintf(details.name,sizeof(details.name),"%s",d->Name ? d->Name : d->Label);
|
||||
snprintf(details.maker,sizeof(details.maker),"%s",d->Maker ? d->Maker : "Unknown");
|
||||
snprintf(details.copyright,sizeof(details.copyright),"%s",d->Copyright ? d->Copyright : "Unspecified");
|
||||
details.hard_realtime = LADSPA_IS_HARD_RT_CAPABLE(d->Properties) ? 1U : 0U;
|
||||
*out = details;
|
||||
}
|
||||
dlclose(module); return r;
|
||||
}
|
||||
|
|
@ -254,7 +270,7 @@ xtc_result xtc_plugin_process(xtc_plugin *p, const float *input, float *output,
|
|||
{ return xtc_plugin_process_at(p,input,output,frames,p ? p->next : 0); }
|
||||
|
||||
#else
|
||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double r, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||
xtc_result xtc_plugin_describe(const xtc_plugin_spec *s, double r, xtc_plugin_details *out)
|
||||
{ (void)s; (void)r; (void)out; return XTC_ERR_UNSUPPORTED; }
|
||||
xtc_result xtc_plugin_probe(const char *p, unsigned int i, double r, xtc_plugin_spec *s)
|
||||
{ (void)p; (void)i; (void)r; (void)s; return XTC_ERR_UNSUPPORTED; }
|
||||
|
|
@ -266,3 +282,33 @@ xtc_result xtc_plugin_process_at(xtc_plugin *p, const float *i, float *o, size_t
|
|||
xtc_result xtc_plugin_process(xtc_plugin *p, const float *i, float *o, size_t f)
|
||||
{ (void)p; (void)i; (void)o; (void)f; return XTC_ERR_UNSUPPORTED; }
|
||||
#endif
|
||||
|
||||
xtc_result xtc_plugin_parameters(const xtc_plugin_spec *s, double rate, xtc_parameter_info out[XTC_PLUGIN_CONTROLS])
|
||||
{
|
||||
xtc_plugin_details details;
|
||||
xtc_result r;
|
||||
if (!out) return XTC_ERR_INVALID_ARGUMENT;
|
||||
r = xtc_plugin_describe(s,rate,&details);
|
||||
if (r == XTC_OK) memcpy(out,details.parameters,sizeof(details.parameters));
|
||||
return r;
|
||||
}
|
||||
xtc_result xtc_plugin_reset(xtc_plugin_spec *s, double rate, unsigned int port)
|
||||
{
|
||||
xtc_plugin_details details;
|
||||
xtc_plugin_spec candidate;
|
||||
size_t i, count = 0;
|
||||
int found = port == UINT32_MAX;
|
||||
xtc_result r = xtc_plugin_describe(s,rate,&details);
|
||||
if (r != XTC_OK) return r;
|
||||
candidate = *s;
|
||||
for (i = 0; i < s->control_count; ++i) if (port == UINT32_MAX || s->ports[i] == port) {
|
||||
candidate.values[i] = details.parameters[i].default_value;
|
||||
found = 1;
|
||||
}
|
||||
if (!found) return XTC_ERR_NOT_FOUND;
|
||||
for (i = 0; i < s->point_count; ++i)
|
||||
if (port != UINT32_MAX && s->points[i].port != port) candidate.points[count++] = s->points[i];
|
||||
candidate.point_count = (unsigned int)count;
|
||||
*s = candidate;
|
||||
return XTC_OK;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include "../../src/gui/sequencer_widget.hpp"
|
||||
#include "../../src/gui/piano_roll.hpp"
|
||||
#include "../../src/gui/mixer_widget.hpp"
|
||||
#include "../../src/gui/plugin_dialog.hpp"
|
||||
#include <FL/Fl_Hold_Browser.H>
|
||||
#include <unistd.h>
|
||||
#include <FL/Fl.H>
|
||||
#include <FL/Fl_Box.H>
|
||||
|
|
@ -635,6 +637,37 @@ int main(int argc, char **argv)
|
|||
size_t id = SIZE_MAX;
|
||||
CHECK(window.add_plugin(module,0,&id) == XTC_OK);
|
||||
xtc_plugin_spec spec{};
|
||||
// Exercise the actual modal chooser/editor with copied metadata only.
|
||||
Fl::add_timeout(.05,[](void *) {
|
||||
auto *dialog = Fl::first_window();
|
||||
CHECK(dialog && dialog->label() && std::strcmp(dialog->label(),"LADSPA descriptors") == 0);
|
||||
if (!dialog) return;
|
||||
for (int i = 0; i < dialog->children(); ++i)
|
||||
if (auto *list = dynamic_cast<Fl_Hold_Browser *>(dialog->child(i))) {
|
||||
CHECK(list->size() == 7); list->select(2);
|
||||
}
|
||||
auto *choose = find(dialog,"Choose"); CHECK(choose); if (choose) choose->do_callback();
|
||||
});
|
||||
unsigned int chosen = UINT32_MAX;
|
||||
CHECK(xtc_choose_plugin(module,48000,chosen) && chosen == 1);
|
||||
xtc_plugin_details details{}; xtc_plugin_spec edited{};
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
|
||||
CHECK(xtc_plugin_describe(&spec,48000,&details) == XTC_OK);
|
||||
spec.values[0] = .25f; spec.point_count = 1; spec.points[0] = {0,spec.ports[0],0,.5f};
|
||||
Fl::add_timeout(.05,[](void *) {
|
||||
auto *dialog = Fl::first_window(); CHECK(dialog);
|
||||
CHECK(contains(dialog,"Test gain"));
|
||||
auto *reset = find(dialog,"Reset defaults"); CHECK(reset); if (reset) reset->do_callback();
|
||||
auto *apply = find(dialog,"Apply"); CHECK(apply); if (apply) apply->do_callback();
|
||||
});
|
||||
CHECK(xtc_edit_plugin(spec,details,edited) && edited.values[0] == 1 && !edited.point_count);
|
||||
edited.values[0] = .75f;
|
||||
Fl::add_timeout(.05,[](void *) {
|
||||
auto *dialog = Fl::first_window(); auto *cancel = find(dialog,"Cancel");
|
||||
CHECK(cancel); if (cancel) cancel->do_callback();
|
||||
});
|
||||
CHECK(!xtc_edit_plugin(spec,details,edited) && edited.values[0] == .75f);
|
||||
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK);
|
||||
CHECK(window.set_plugin_control(id,spec.ports[0],.5f) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
||||
|
|
@ -656,6 +689,33 @@ int main(int argc, char **argv)
|
|||
CHECK(window.open_project("/tmp/xtc-gui-plugin-test.xtc") == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f);
|
||||
CHECK(spec.point_count == 1 && spec.points[0].tick == 960 && spec.points[0].value == .25f);
|
||||
CHECK(window.reset_plugin(id) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == 1 && !spec.point_count);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.values[0] == .5f && spec.point_count == 1);
|
||||
// Replace an effect while preserving node ID and wires; undo restores state.
|
||||
auto *canvas = graph_canvas(&window);
|
||||
CHECK(canvas->add_node(XTC_NODE_OSCILLATOR) == XTC_OK);
|
||||
const size_t source_id = canvas->selected_node();
|
||||
CHECK(canvas->connect_ports({source_id,1,id,0}) == XTC_OK);
|
||||
CHECK(window.replace_plugin(id,module,4) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 4 && spec.values[0] == 2 && !spec.point_count);
|
||||
before = sequence->project();
|
||||
CHECK(window.replace_plugin(id,module,1) == XTC_ERR_UNSUPPORTED && sequence->project() == before);
|
||||
CHECK(window.replace_plugin(id,module,3) == XTC_ERR_INVALID_STATE && sequence->project() == before);
|
||||
CHECK(window.replace_plugin(id,"/absent/plugin.so",0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),id,&spec) == XTC_OK && spec.index == 0 && spec.values[0] == .5f && spec.point_count == 1);
|
||||
const xtc_connection_info wire{source_id,1,id,0}; size_t inserted = SIZE_MAX;
|
||||
CHECK(window.insert_plugin(wire,module,0,&inserted) == XTC_OK && inserted != id);
|
||||
xtc_routing_info routing{};
|
||||
CHECK(canvas->routing(inserted,routing) && routing.inputs == 1 && routing.outputs == 1);
|
||||
before = sequence->project();
|
||||
CHECK(window.insert_plugin(wire,module,0) == XTC_ERR_NOT_FOUND && sequence->project() == before);
|
||||
CHECK(canvas->select_node(inserted) && canvas->delete_selected() == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_ERR_NOT_FOUND);
|
||||
CHECK(window.undo(false) == XTC_OK);
|
||||
CHECK(xtc_graph_get_plugin(xtc_project_graph(sequence->project()),inserted,&edited) == XTC_OK);
|
||||
xtc_project *missing = nullptr; xtc_graph *graph = nullptr; size_t absent;
|
||||
CHECK(xtc_project_create(&missing) == XTC_OK);
|
||||
CHECK(xtc_graph_create(8,&graph) == XTC_OK);
|
||||
|
|
|
|||
|
|
@ -174,7 +174,58 @@ int main(int argc, char **argv)
|
|||
assert(xtc_plugin_probe(argv[1],2,48000,&s) == XTC_ERR_UNSUPPORTED);
|
||||
assert(xtc_plugin_probe(argv[1],3,48000,&s) == XTC_OK);
|
||||
assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_INVALID_STATE && !p);
|
||||
assert(xtc_plugin_probe(argv[1],4,48000,&s) == XTC_ERR_NOT_FOUND);
|
||||
{
|
||||
xtc_plugin_details details;
|
||||
xtc_plugin_spec saved;
|
||||
assert(xtc_plugin_probe(argv[1],0,48000,&s) == XTC_OK);
|
||||
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK);
|
||||
assert(!strcmp(details.name,"Test gain") && !strcmp(details.maker,"xtc tests") &&
|
||||
!strcmp(details.copyright,"MIT") && !details.parameters[0].unit[0]);
|
||||
assert(details.parameters[0].minimum == 0 && details.parameters[0].maximum == 2);
|
||||
assert(details.flags[0] == (XTC_PLUGIN_BOUNDED_BELOW|XTC_PLUGIN_BOUNDED_ABOVE));
|
||||
s.values[0] = .25f;
|
||||
s.point_count = 1; s.points[0] = (xtc_plugin_point){0,s.ports[0],0,.5f};
|
||||
saved = s;
|
||||
assert(xtc_plugin_reset(&s,48000,63) == XTC_ERR_NOT_FOUND && !memcmp(&s,&saved,sizeof(s)));
|
||||
assert(xtc_plugin_reset(&s,48000,s.ports[0]) == XTC_OK && s.values[0] == 1 && !s.point_count);
|
||||
assert(xtc_plugin_probe(argv[1],4,48000,&s) == XTC_OK && s.values[0] == 2);
|
||||
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK && (details.flags[0] & XTC_PLUGIN_INTEGER));
|
||||
s.values[0] = .5f; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
|
||||
assert(xtc_plugin_reset(&s,48000,UINT32_MAX) == XTC_OK && s.values[0] == 2);
|
||||
assert(xtc_plugin_probe(argv[1],5,48000,&s) == XTC_OK && s.values[0] == 1);
|
||||
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK && (details.flags[0] & XTC_PLUGIN_TOGGLED));
|
||||
s.values[0] = .5f; assert(xtc_plugin_create(&s,48000,17,&p) == XTC_ERR_UNSUPPORTED && !p);
|
||||
assert(xtc_plugin_probe(argv[1],6,48000,&s) == XTC_OK);
|
||||
assert(xtc_plugin_describe(&s,48000,&details) == XTC_OK &&
|
||||
(details.flags[0] & XTC_PLUGIN_SAMPLE_RATE) && (details.flags[0] & XTC_PLUGIN_LOGARITHMIC));
|
||||
assert(fabsf(details.parameters[0].minimum-48) < .001f && fabsf(details.parameters[0].maximum-4800) < .01f);
|
||||
assert(xtc_plugin_probe(argv[1],7,48000,&s) == XTC_ERR_UNSUPPORTED);
|
||||
assert(xtc_plugin_probe(argv[1],9,48000,&s) == XTC_ERR_NOT_FOUND);
|
||||
// Noncommutative effects demonstrate that wires define DSP order,
|
||||
// independent of node creation order; persisted graphs retain it.
|
||||
{
|
||||
xtc_graph *g = NULL; xtc_plan *plan = NULL;
|
||||
xtc_plugin_spec gain, offset; size_t first, second, output;
|
||||
float input[1] = {.5f}, out[1];
|
||||
assert(xtc_plugin_probe(argv[1],0,48000,&gain) == XTC_OK);
|
||||
assert(xtc_plugin_probe(argv[1],8,48000,&offset) == XTC_OK);
|
||||
gain.values[0] = 2;
|
||||
assert(xtc_graph_create(8,&g) == XTC_OK);
|
||||
assert(xtc_graph_add_plugin(g,&gain,&first) == XTC_OK);
|
||||
assert(xtc_graph_add_plugin(g,&offset,&second) == XTC_OK);
|
||||
assert(xtc_graph_add_output(g,&output) == XTC_OK);
|
||||
assert(xtc_graph_connect(g,first,second) == XTC_OK && xtc_graph_connect(g,second,output) == XTC_OK);
|
||||
assert(xtc_graph_compile(g,48000,1,&plan) == XTC_OK);
|
||||
audio = 1; assert(xtc_plan_process(plan,input,out,1) == XTC_OK && out[0] == 2); audio = 0;
|
||||
xtc_plan_destroy(plan); plan = NULL;
|
||||
// Swap identities at stable IDs: gain -> offset becomes offset -> gain.
|
||||
assert(xtc_graph_replace_plugin(g,first,&offset) == XTC_OK);
|
||||
assert(xtc_graph_replace_plugin(g,second,&gain) == XTC_OK);
|
||||
assert(xtc_graph_compile(g,48000,1,&plan) == XTC_OK);
|
||||
audio = 1; assert(xtc_plan_process(plan,input,out,1) == XTC_OK && out[0] == 3); audio = 0;
|
||||
xtc_plan_destroy(plan); xtc_graph_destroy(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
puts("Plugin lifecycle, identity, layouts, processing and persisted graph checks passed.");
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/* MIT test fixture; not a redistributed third-party plugin. */
|
||||
#include <ladspa.h>
|
||||
#include <stdlib.h>
|
||||
#include <float.h>
|
||||
typedef struct { float *in, *out, *gain; } instance;
|
||||
static unsigned alive, runs;
|
||||
unsigned xtc_fixture_alive(void) { return alive; }
|
||||
|
|
@ -24,6 +25,8 @@ static void run(LADSPA_Handle h, unsigned long frames)
|
|||
instance *p = h; unsigned long i; ++runs;
|
||||
for (i = 0; i < frames; ++i) p->out[i] = p->in[i] * *p->gain;
|
||||
}
|
||||
static void offset(LADSPA_Handle h, unsigned long frames)
|
||||
{ instance *p = h; unsigned long i; ++runs; for (i = 0; i < frames; ++i) p->out[i] = p->in[i] + *p->gain; }
|
||||
static void source(LADSPA_Handle h, unsigned long frames)
|
||||
{ instance *p = h; unsigned long i; ++runs; for (i = 0; i < frames; ++i) p->in[i] = .25f; }
|
||||
static void destroy(LADSPA_Handle h) { if (h) { --alive; free(h); } }
|
||||
|
|
@ -37,15 +40,33 @@ static const LADSPA_PortDescriptor stereo_ports[] = {
|
|||
static const char * const names[] = {"Input","Output","Gain","Right output"};
|
||||
static const LADSPA_PortRangeHint hints[4] = {{0,0,0},{0,0,0},
|
||||
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_DEFAULT_1,0,2},{0,0,0}};
|
||||
static const LADSPA_PortRangeHint discrete_hints[3] = {{0,0,0},{0,0,0},
|
||||
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_INTEGER|LADSPA_HINT_DEFAULT_MIDDLE,0,3}};
|
||||
static const LADSPA_PortRangeHint toggle_hints[3] = {{0,0,0},{0,0,0},
|
||||
{LADSPA_HINT_TOGGLED|LADSPA_HINT_DEFAULT_1,0,0}};
|
||||
static const LADSPA_PortRangeHint scaled_hints[3] = {{0,0,0},{0,0,0},
|
||||
{LADSPA_HINT_BOUNDED_BELOW|LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_SAMPLE_RATE|LADSPA_HINT_LOGARITHMIC|LADSPA_HINT_DEFAULT_MIDDLE,.001f,.1f}};
|
||||
static const LADSPA_PortRangeHint overflow_hints[3] = {{0,0,0},{0,0,0},
|
||||
{LADSPA_HINT_BOUNDED_ABOVE|LADSPA_HINT_SAMPLE_RATE,0,FLT_MAX}};
|
||||
static const LADSPA_Descriptor descriptors[] = {
|
||||
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.PortCount=3,.PortDescriptors=ports,
|
||||
{.UniqueID=900000,.Label="xtc_test_gain",.Name="Test gain",.Maker="xtc tests",.Copyright="MIT",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900001,.Label="xtc_test_source",.Name="Test source",.PortCount=1,.PortDescriptors=source_ports,
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=source,.cleanup=destroy},
|
||||
{.UniqueID=900002,.Label="xtc_test_stereo",.Name="Unsupported stereo",.PortCount=4,.PortDescriptors=stereo_ports,
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900003,.Label="xtc_test_fail",.Name="Initialization failure",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy}
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900004,.Label="xtc_test_integer",.Name="Integer gain",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=discrete_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900005,.Label="xtc_test_toggle",.Name="Toggle gain",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=toggle_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900006,.Label="xtc_test_scaled",.Name="Scaled gain",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=scaled_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900007,.Label="xtc_test_overflow",.Name="Overflow hint",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=overflow_hints,.instantiate=create,.connect_port=connect,.run=run,.cleanup=destroy},
|
||||
{.UniqueID=900008,.Label="xtc_test_offset",.Name="Offset effect",.PortCount=3,.PortDescriptors=ports,
|
||||
.PortNames=names,.PortRangeHints=hints,.instantiate=create,.connect_port=connect,.run=offset,.cleanup=destroy}
|
||||
};
|
||||
const LADSPA_Descriptor *ladspa_descriptor(unsigned long index)
|
||||
{ return index < sizeof(descriptors)/sizeof(descriptors[0]) ? descriptors+index : NULL; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue