feat(plugins): qualify host VST3 chains and refresh the DAW gallery (phase 89)

This commit is contained in:
Johannes Findeisen 2026-11-06 01:50:25 +01:00
commit 26ace28bb5
73 changed files with 344 additions and 18 deletions

View file

@ -582,3 +582,7 @@ test-track-fx: $(BUILD_DIR)/test_track_fx
./$(BUILD_DIR)/test_track_fx
test: test-track-fx
-include $(BUILD_DIR)/tests/unit/test_track_fx.d
.PHONY: test-gui-track-fx
test-gui-track-fx: $(BUILD_DIR)/test_gui $(BUILD_DIR)/plugin-fixture.so $(BUILD_DIR)/clap-fixture.clap $(BUILD_DIR)/vst2-fixture.so $(BUILD_DIR)/vst3-fixture.so
XTC_TEST_TRACK_FX_ONLY=1 XTC_TEST_PLUGIN="$(abspath $(BUILD_DIR))/plugin-fixture.so" XTC_TEST_CLAP="$(abspath $(BUILD_DIR))/clap-fixture.clap" XTC_TEST_VST2="$(abspath $(BUILD_DIR))/vst2-fixture.so" XTC_TEST_VST3="$(abspath $(BUILD_DIR))/vst3-fixture.so" xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui

View file

@ -170,3 +170,12 @@ and output, float32 and zero latency; they use default configuration. Instrument
modern parameter editing/automation, opaque state and custom editors are not
implemented. Projects now save v11 and read v1–v11. See PLUGIN_HOSTING.md for the
full contract and test/qualification limits.
Phases 80–89 add a compact sequencer Create menu, optional track inspector and
per-track modular effect chains. Open **View → Track inserts** (Ctrl+4) to add,
search, reorder, bypass, inspect and mix mono LADSPA/VST2/VST3/CLAP effects or
internal devices. Chains support undo, presets, audio/MIDI rendering and export.
Projects save format 17 and read formats 1–17. See
[track insert workflows and limits](docs/TRACK_INSERTS.md) and the
[screenshot gallery](screenshots/README.md).

View file

@ -112,3 +112,20 @@ Do not unload/replace modules during playback; candidate plans are prepared off
the callback and old instances are reclaimed on the control thread. Missing
references remain inspectable without loading; failed GUI preparation retains
the current project. Save a backup for applications limited to v11 or earlier.
### Track insert chains (Phases 80–89)
Track chains contain at most eight mono effects. Modern adapters require zero
reported latency, use default configuration and do not provide native plugin
windows, parameter editing or opaque state persistence. A bounded VST3 output
parameter sink retains the final notification per parameter per block (64
parameters); these notifications are not recorded or exposed as controls.
Track edits stop transport before candidate preparation. Bus inserts and tracks
with both audio assets and MIDI reject preparation. Unassigned live sine
monitoring stays on the master input. Stereo insert layouts, latency
compensation, sandboxed scanning and gapless live insert edits remain future
work. Internal history resets on discontinuities; third-party history follows
the existing host contract. Presets retain absolute plugin references, so
compatible binaries must be installed at the same paths on another machine.
The smaller optional inspector's visibility is session-local.

View file

@ -1,4 +1,4 @@
# Project format v16 (reads v1–v15)
# Project format v17 (reads v1–v17)
Phases 5–31 use a bounded text format implemented in C99 without a parser library.
The extension is `.xtc`. File operations run exclusively on the control thread.
@ -426,3 +426,39 @@ followed by `MARKER tick hex_name` records. Count is 0..128; ticks must be stric
increasing and at most MAX_TICK. Names use the existing bounded UTF-8 hex encoding.
Versions 1–15 migrate to an empty marker list. Older builds reject version 16.
Markers are copied by snapshots and history; the tick is their stable key.
## Version 17: per-track insert chains
After MARKERS/MARKER and before END, the writer emits a chain for every track,
in dense track order. Earlier versions migrate to empty chains with unity gain.
Floats use the existing eight-digit IEEE binary32 hex encoding; strings use the
existing bounded UTF-8/byte hex encoding, with `-` for an empty identity.
```text
CHAINS track_count
CHAIN track_id slot_count output_gain_bits
INSERT enabled wet_bits format descriptor_index unique_id inputs tempo hex_label
MODULE hex_absolute_binary_path
IDENTITY hex_stable_identifier_or_dash
CONTROLS control_count
CONTROL port value_bits
PARAMETERS point_count
PARAM tick port step value_bits
```
Each CHAIN has exactly slot_count INSERT blocks. CONTROLS/PARAMETERS determine
the subsequent CONTROL/PARAM record counts, including zero. Slots are ordered,
at most eight, and accept mono non-instrument effects only. Enabled is 0/1,
wet is finite 0–1, output gain finite 0–4; inputs must be 1. The format values
are LADSPA=0, CLAP=1, VST2=2, VST3=3 and Internal=4. Existing plugin specification
validation applies, including descriptor/identity/port/automation bounds and
20,000–300,000 milli-BPM. Bus chains must be empty. No sample reference is
permitted on an effect. Duplicate, missing, reordered or excessive chain
records, malformed strings and nonfinite values reject the whole load.
Loading and dependency inspection only validate metadata; no plugin module
is opened. Enabled plugins are verified when a render plan is prepared. Chain
preset files reuse this schema with exactly one non-bus track and no clips or
instrument. Track insert APIs are declared in `xtc_plugin.h`; see
[TRACK_INSERTS.md](TRACK_INSERTS.md) for playback restrictions and workflows.

View file

@ -2015,12 +2015,40 @@ Implemented: selected insert wet amount (0–1) and chain output gain (0–4) co
Load compatible installed host VST3 effects in real track chains, capture screenshots, update usage documentation and qualify integration, persistence and real-time boundaries.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
- [x] Implement the described workflow with validated errors and safe ownership.
- [x] Pass relevant core and/or GUI tests.
- [x] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
Implemented: installed host ZamEQ2, ZamAutoSat and ZamDelay VST3 effects load together, render finite nonzero output and export a non-silent WAV. A bounded VST3 output-parameter sink fixes host assertion logging; the fixture verifies notification/reset/capacity behavior. Actual format labels and scrollable compact insert graphs are verified. All 52 prior screenshots were refreshed and five real compact/VST3 captures added. Full core/reliability, four-format insert GUI, full effect/instrument GUI matrix, JACK lifecycle, ASan/UBSan core and focused/full GUI, static analysis and both staged install layouts passed. See TRACK_INSERTS.md and screenshots/README.md for exact limits and reproduction.
---
### Integrated validation for Phases 80–89
All ten phases have separate tested implementation commits. Verification used
`make BUILD_DIR=build/audio-editor all test test-reliability gui`,
`test-gui-track-fx`, the full `test-gui`, `test-gui-modern-plugins`,
`test-gui-instruments`, `test-gui-plugins` and `test-gui-jack` targets,
`analyze`, `test-install` and `test-install-layout`. Clang ASan/UBSan core,
allocation guards and focused/full GUI checks passed using only the existing
system Fontconfig leak suppression. The normal GCC build is warning-free;
Clang/FLTK emits the existing redundant-instrumentation warning.
The opt-in `XTC_TEST_HOST_VST3=1` core check loaded the installed Zam VST3
binaries, rendered their serial MIDI-track chain and validated non-silent WAV
export. The screenshot tool refreshed the complete prior gallery; its
`XTC_CAPTURE_HOST_VST3=1` mode captured the real loaded chain, identity view,
parallel branches, compact workspace and creation menu. Metadata-only CLI
inspection tests cover insert dependencies, malformed/overflow records,
nonfinite values, missing modules and v16 migration without executing modules.
No dependency was added. Writer format is 17, reads 1–17. Inserts remain bounded
mono effects with zero reported latency, control-thread edits and no native
plugin editors or modern parameter/state persistence. Bus/hybrid inserts reject
preparation. These tests qualify host ownership and the named configurations;
physical hardware, arbitrary third-party plugins and feature parity with mature
DAWs are not claimed. See TRACK_INSERTS.md and KNOWN_LIMITATIONS.md.
---

View file

@ -65,3 +65,30 @@ Edits are prepared and undoable, with finite range validation. Internal sync
parameters follow project tempo. This is mono amplitude blending; stereo track
insert layouts, latency compensation and gapless live insert edits are future
work. Editing stops transport before preparing the replacement plan.
The CLI `xtc-check --inspect project.xtc` lists insert format, slot order,
bypass/wet settings and missing binary references without loading plugins.
VST3 processing supplies a fixed output-parameter sink (64 IDs, final point per
ID/block) for plugins that emit meter/parameter notifications; notifications are
not recorded or exposed as controls.
Host qualification on 2026-11-06 used the installed Linux x86_64 ZamEQ2,
ZamAutoSat and ZamDelay VST3 binaries under `/usr/lib/vst3`. All three were
prepared together, rendered with nonzero finite output and exported to WAV.
This verifies those default mono configurations, not arbitrary plugins or
physical hardware. The repository fixtures cover LADSPA, CLAP, VST2 and VST3
insert adoption and VST3 output notification bounds. Internal-plan allocation
failure and callback guards, sanitizers, GUI/JACK lifecycle and install checks
cover host ownership and cleanup.
Reproduce focused verification with:
```sh
make BUILD_DIR=build/audio-editor test-track-fx test-gui-track-fx
XTC_TEST_HOST_VST3=1 ./build/audio-editor/test_track_fx
```
The host opt-in requires the three named Zam bundles; the normal tests use
internal devices and controlled fixtures. The screenshot tool's
`XTC_CAPTURE_HOST_VST3=1` mode captures those real host plugins; see
[screenshots/README.md](../screenshots/README.md).

View file

@ -1,7 +1,7 @@
# xtc screenshots
The main gallery was refreshed after Phase 68 on 2026-11-06. Two additional
Phase 79 captures show the new sample editor and transient markers.
The complete gallery was refreshed after Phase 89 on 2026-11-06, including
the sample editor, compact workspace and real installed VST3 insert chains.
These are actual rendered application windows and menus on a 1440 × 960 virtual
X11 screen, without mockups, compositing or image edits.
@ -14,6 +14,21 @@ APIs and GUI history/preparation paths. Original projects, media and user
configuration are not overwritten. Black margins are the virtual desktop;
floating views show the actual detached windows together.
## Compact workspace and installed VST3 effects (Phases 80–89)
- [Compact sequencer workspace](compact-workspace.png)
- [Create dropdown over the sequencer](create-track-menu.png)
- [ZamEQ2 → ZamAutoSat → ZamDelay on the Keys track](track-inserts-vst3.png)
- [Verified VST3 identity and adapter capabilities](track-insert-vst3-identity.png)
- [Parallel wet/dry branches in the actual insert graph](track-inserts-parallel.png)
The three VST3 effects are installed on this host under `/usr/lib/vst3`, loaded
through the real host and prepared in the track's chain. These are host graph
and identity views; native plugin editor windows are not available. The
identity view explicitly reports default configuration and absent parameter
controls. Long graphs scroll horizontally. See
[TRACK_INSERTS.md](../docs/TRACK_INSERTS.md) for precise compatibility limits.
## New arrangement and composition workflows
| Phase | Feature | Captures |
@ -50,8 +65,8 @@ See [AUDIO_EDITOR.md](../docs/AUDIO_EDITOR.md) for limits and frame/tick roundin
All pre-existing images below were recaptured. Mixer playback meters come from
real JACK callbacks. The minimum-size capture uses a 1220 × 720 main window;
the scrolled view adds twelve tracks. Audio views import the repository's
`original-pluck.wav` and apply clip gain/fades. Plugin views use repository test
fixtures, not commercial plugins. Internal instrument menus and delay/reverb
`original-pluck.wav` and apply clip gain/fades. The older descriptor/control views use repository test
fixtures. The track-insert views below use real installed host VST3 binaries. Internal instrument menus and delay/reverb
editors show the real available devices and controls. The command palette uses
the current application action list.
@ -115,3 +130,15 @@ nor starts/reconfigures the audio server.
Set `XTC_CAPTURE_SAMPLE_ONLY=1` for the capture tool (or the make target) to write
only the two sample-editor images while retaining the existing gallery.
To reproduce the host captures where those Zam bundles are installed:
```sh
XTC_CAPTURE_HOST_VST3=1 xvfb-run -a -s '-screen 0 1440x960x24' \
./build/audio-editor/capture_screenshots "$PWD/screenshots"
```
Run `make BUILD_DIR=build/audio-editor screenshots` first to build the tool and
refresh the full fixture/demo gallery. All captures use temporary projects and
preserve user settings and original media.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Before After
Before After

View file

@ -29,7 +29,7 @@ const char *node_name(xtc_node_type type)
case XTC_NODE_METER:
return "Meter";
case XTC_NODE_PLUGIN:
return "LADSPA plugin";
return "Effect plugin";
case XTC_NODE_NATIVE:
return "Native processor";
case XTC_NODE_INSTRUMENT:
@ -394,7 +394,10 @@ void GraphWidget::draw()
fl_color(FL_WHITE);
fl_font(FL_HELVETICA_BOLD, 12);
char text[96];
std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id);
xtc_plugin_spec title_plugin{};
if(node.type==XTC_NODE_PLUGIN && xtc_graph_get_plugin(graph_,node.id,&title_plugin)==XTC_OK)
std::snprintf(text,sizeof(text),"%s effect #%zu",xtc_plugin_format_name(title_plugin.format),node.id);
else std::snprintf(text, sizeof(text), "%s #%zu", node_name(node.type), node.id);
fl_push_clip(left+6, top+2, node_width-12, 40);
fl_draw(text, left + 10, top + 20);
fl_font(FL_HELVETICA, 12);

View file

@ -10,6 +10,7 @@
#include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Value_Input.H>
#include <FL/Fl_Scroll.H>
#include <cstring>
#include <algorithm>
#include <cmath>
@ -20,10 +21,12 @@ class TrackInsertsPanel : public Fl_Group {
Fl_Box *message_;
Fl_Value_Input *wet_,*gain_;
GraphWidget *graph_;
Fl_Scroll *scroll_;
xtc_graph *model_=nullptr;
uint64_t track_=UINT64_MAX;
xtc_track_chain cached_{};
std::vector<uint64_t> ids_;
std::vector<std::string> names_;
void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); }
public:
TrackInsertsPanel(int x,int y,int w,int h,MainWindow *owner):Fl_Group(x,y,w,h,"Track inserts"),owner_(owner) {
@ -48,8 +51,9 @@ class TrackInsertsPanel : public Fl_Group {
for(auto *input:{wet_,gain_})input->callback([](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this);
message_=new Fl_Box(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
slots_=new Fl_Hold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(c)->update_mix();},this);
graph_=new GraphWidget(x+260,y+126,w-276,h-142,nullptr,[](void *,const xtc_graph_edit &,size_t *){return XTC_ERR_UNSUPPORTED;},nullptr,message_);
resizable(graph_);end();
scroll_=new Fl_Scroll(x+260,y+126,w-276,h-142);
graph_=new GraphWidget(x+260,y+126,2000,4000,nullptr,[](void *,const xtc_graph_edit &,size_t *){return XTC_ERR_UNSUPPORTED;},nullptr,message_);
scroll_->end();resizable(scroll_);end();
}
~TrackInsertsPanel() override { graph_->set_graph(nullptr);xtc_graph_destroy(model_); }
uint64_t selected_track() const {return track_;}
@ -112,8 +116,8 @@ class TrackInsertsPanel : public Fl_Group {
const xtc_project *project=owner_->project_.get();size_t count=0,clips=0;xtc_project_get_counts(project,&count,&clips);
std::vector<uint64_t> ids;std::vector<std::string> names;
for(size_t i=0;i<count;++i){xtc_track_info t{};xtc_project_get_track(project,i,&t);if(!t.is_bus){ids.push_back(t.id);names.emplace_back(t.name);}}
bool rebuild=ids!=ids_ || force;
if(rebuild){ids_=ids;tracks_->clear();for(const auto &name:names) {tracks_->add("track");tracks_->replace(tracks_->size()-2,name.c_str());}}
bool rebuild=ids!=ids_ || names!=names_ || force;
if(rebuild){ids_=ids;names_=names;tracks_->clear();for(const auto &name:names) {tracks_->add("track");tracks_->replace(tracks_->size()-2,name.c_str());}}
if(std::find(ids_.begin(),ids_.end(),track_)==ids_.end())track_=ids_.empty() ? UINT64_MAX : ids_[0];
for(size_t i=0;i<ids_.size();++i)if(ids_[i]==track_)tracks_->value(static_cast<int>(i));
xtc_track_chain chain{};chain.output_gain=1;
@ -123,7 +127,14 @@ class TrackInsertsPanel : public Fl_Group {
for(size_t i=0;i<chain.count;++i){std::string label=std::to_string(i+1)+" "+chain.slots[i].plugin.label+(chain.slots[i].enabled ? "" : " [bypass]");slots_->add(label.c_str());}
if(chain.count)slots_->select(selected>0 && static_cast<size_t>(selected)<=chain.count ? selected : 1);
xtc_graph *next=nullptr;auto r=xtc_track_chain_graph(&chain,&next);
if(r==XTC_OK){graph_->set_graph(next);xtc_graph_destroy(model_);model_=next;graph_->auto_layout();}
if(r==XTC_OK){graph_->set_graph(next);xtc_graph_destroy(model_);model_=next;graph_->auto_layout();
size_t nodes=0,edges=0,max_level=0,max_row=0;std::array<size_t,256> rows{};
xtc_graph_get_counts(model_,&nodes,&edges);
for(size_t i=0;i<nodes;++i){xtc_node_info node{};xtc_routing_info route{};xtc_graph_get_node(model_,i,&node);if(graph_->routing(node.id,route))max_level=std::max(max_level,route.level);}
graph_->size(std::max(scroll_->w()-20,static_cast<int>(max_level+1)*174+40),4000);
for(size_t i=0;i<nodes;++i){xtc_node_info node{};xtc_routing_info route{};xtc_graph_get_node(model_,i,&node);if(graph_->routing(node.id,route) && route.level<rows.size()) {size_t row=rows[route.level]++;max_row=std::max(max_row,row);graph_->move_node(node.id,20+static_cast<int>(route.level)*174,24+static_cast<int>(row)*104);}}
graph_->size(graph_->w(),std::max(scroll_->h()-20,static_cast<int>(max_row+1)*104+48));scroll_->scroll_to(0,0);
message_->copy_label("Track input > inserts > output gain > track fader > bus/master. Scroll to inspect long chains.");}
update_mix();redraw();
}
};

View file

@ -78,6 +78,19 @@ static int inspect(const xtc_project *project, uint64_t version)
}
}
}
for(i=0;i<tracks;++i) {
xtc_track_info track;xtc_track_chain chain;size_t slot;
xtc_project_get_track(project,i,&track);xtc_project_get_chain(project,track.id,&chain);
if(!chain.count)continue;
printf("Track %" PRIu64 " insert chain: %zu slots, output gain %.6g\n",track.id,chain.count,(double)chain.output_gain);
for(slot=0;slot<chain.count;++slot) {
const xtc_track_insert *insert=chain.slots+slot;const xtc_plugin_spec *spec=&insert->plugin;
int available=spec->format==XTC_PLUGIN_BUILTIN || readable_file(spec->path);
printf("%s insert track %" PRIu64 " slot %zu %s wet %.6g ",xtc_plugin_format_name(spec->format),track.id,slot+1,insert->enabled ? "enabled" : "bypassed",(double)insert->wet);
quoted(spec->label);printf(" %s: ",available ? "available" : "MISSING");quoted(spec->path);putchar('\n');
if(!available)++missing;
}
}
printf("Dependency references missing/unreadable: %zu.\n",missing);
puts("File availability only: WAV contents, plugin identity/initialization, JACK and hardware not checked; no plugin code executed.");
return missing ? 2 : EXIT_SUCCESS;

View file

@ -26,6 +26,17 @@ typedef struct module {
static module *modules;
typedef union { clap_event_header_t header; clap_event_note_t note; clap_event_midi_t midi; } clap_note;
typedef struct { Steinberg_Vst_IEventList api; struct xtc_modern *owner; } vst_events;
/* Bounded output-notification sink. Keep the final point per parameter/block;
* this host has no parameter automation recorder or native editor consumer. */
#define VST_OUTPUT_PARAMETERS 64U
typedef struct {
Steinberg_Vst_IParamValueQueue api;Steinberg_Vst_ParamID id;
Steinberg_int32 offset;Steinberg_Vst_ParamValue value;int present;
} vst_parameter_queue;
typedef struct {
Steinberg_Vst_IParameterChanges api;size_t count;
vst_parameter_queue queues[VST_OUTPUT_PARAMETERS];
} vst_parameter_changes;
struct xtc_modern {
module *module;
xtc_plugin_format format;
@ -48,6 +59,7 @@ struct xtc_modern {
size_t vst_count;
unsigned char keys[16][128];
vst_events event_list;
vst_parameter_changes parameters;
unsigned int callback, restart;
struct xtc_modern *next;
};
@ -208,6 +220,51 @@ static Steinberg_tresult host_query(void *self, const Steinberg_TUID iid, void *
return Steinberg_kNoInterface;
}
static Steinberg_uint32 host_ref(void *self) { (void)self; return 1; }
static Steinberg_tresult parameter_query(void *self,const Steinberg_TUID iid,void **out)
{
if(!iid || !out)return Steinberg_kInvalidArgument;
*out=NULL;
if(!memcmp(iid,Steinberg_Vst_IParameterChanges_iid,16) || !memcmp(iid,Steinberg_FUnknown_iid,16)){*out=self;return 0;}
return Steinberg_kNoInterface;
}
static Steinberg_tresult queue_query(void *self,const Steinberg_TUID iid,void **out)
{
if(!iid || !out)return Steinberg_kInvalidArgument;
*out=NULL;
if(!memcmp(iid,Steinberg_Vst_IParamValueQueue_iid,16) || !memcmp(iid,Steinberg_FUnknown_iid,16)){*out=self;return 0;}
return Steinberg_kNoInterface;
}
static Steinberg_Vst_ParamID queue_id(void *self){return ((vst_parameter_queue *)self)->id;}
static Steinberg_int32 queue_count(void *self){return ((vst_parameter_queue *)self)->present;}
static Steinberg_tresult queue_get(void *self,Steinberg_int32 index,Steinberg_int32 *offset,Steinberg_Vst_ParamValue *value)
{
vst_parameter_queue *q=self;
if(index || !q->present || !offset || !value)return Steinberg_kInvalidArgument;
*offset=q->offset;*value=q->value;return 0;
}
static Steinberg_tresult queue_add(void *self,Steinberg_int32 offset,Steinberg_Vst_ParamValue value,Steinberg_int32 *index)
{
vst_parameter_queue *q=self;
if(!index || offset<0 || !isfinite(value) || value<0 || value>1)return Steinberg_kInvalidArgument;
q->offset=offset;q->value=value;q->present=1;*index=0;return 0;
}
static Steinberg_Vst_IParamValueQueueVtbl parameter_queue_vtable={queue_query,host_ref,host_ref,queue_id,queue_count,queue_get,queue_add};
static Steinberg_int32 parameter_count(void *self){return (Steinberg_int32)((vst_parameter_changes *)self)->count;}
static Steinberg_Vst_IParamValueQueue *parameter_get(void *self,Steinberg_int32 index)
{
vst_parameter_changes *c=self;
return index<0 || (size_t)index>=c->count ? NULL : &c->queues[index].api;
}
static Steinberg_Vst_IParamValueQueue *parameter_add(void *self,const Steinberg_Vst_ParamID *id,Steinberg_int32 *index)
{
vst_parameter_changes *c=self;size_t i;
if(!id || !index)return NULL;
for(i=0;i<c->count;++i)if(c->queues[i].id==*id)break;
if(i==VST_OUTPUT_PARAMETERS)return NULL;
if(i==c->count){++c->count;c->queues[i].api.lpVtbl=&parameter_queue_vtable;c->queues[i].id=*id;c->queues[i].present=0;}
*index=(Steinberg_int32)i;return &c->queues[i].api;
}
static Steinberg_Vst_IParameterChangesVtbl parameter_changes_vtable={parameter_query,host_ref,host_ref,parameter_count,parameter_get,parameter_add};
static Steinberg_tresult host_name(void *self, Steinberg_Vst_String128 name)
{ (void)self; if (!name) return Steinberg_kInvalidArgument; memset(name,0,sizeof(Steinberg_Vst_String128)); name[0] = 'x'; name[1] = 't'; name[2] = 'c'; return 0; }
static Steinberg_tresult host_create(void *self, Steinberg_TUID cid, Steinberg_TUID iid, void **out)
@ -343,6 +400,8 @@ xtc_result xtc_modern_process(xtc_modern *p, const float *input, float *output,
in.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->input;
out.Steinberg_Vst_AudioBusBuffers_channelBuffers32 = &p->output;
data.numSamples = (Steinberg_int32)frames; data.numInputs = p->instrument ? 0 : 1; data.numOutputs = 1; data.inputs = p->instrument ? NULL : &in; data.outputs = &out;
p->parameters.api.lpVtbl=&parameter_changes_vtable;p->parameters.count=0;
data.outputParameterChanges=&p->parameters.api;
if (p->instrument) data.inputEvents = &p->event_list.api;
ok = p->processor->lpVtbl->process(p->processor,&data) == 0;
}

View file

@ -259,6 +259,22 @@ int track_insert_tests()
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1);
auto *preset_before=s->project();CHECK(panel->load_preset("/missing/preset.xtc")!=XTC_OK && s->project()==preset_before);
unlink("/tmp/xtc-inserts-preset.xtc");
for(const char *variable:{"XTC_TEST_PLUGIN","XTC_TEST_CLAP","XTC_TEST_VST2","XTC_TEST_VST3"}) {
if(const char *binary=std::getenv(variable)) {
CHECK(window.add_track_effect(track,binary,0)==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==2);
CHECK(window.undo()==XTC_OK);panel->refresh(true);
}
}
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK);
auto restored_chain=chain;for(size_t i=1;i<XTC_TRACK_INSERTS;++i)chain.slots[i]=chain.slots[0];chain.count=XTC_TRACK_INSERTS;
for(auto &slot:chain.slots){slot.enabled=1;slot.wet=.5f;}
CHECK(window.set_track_chain(track,chain)==XTC_OK);
auto *insert_canvas=graph_canvas(panel);auto *insert_scroll=dynamic_cast<Fl_Scroll *>(insert_canvas->parent());
CHECK(insert_scroll && insert_canvas->w()>insert_scroll->w());
insert_scroll->scroll_to(insert_canvas->w()-insert_scroll->w(),0);CHECK(insert_scroll->xposition()>0);
const auto *full_before=s->project();CHECK(window.add_track_effect(track,XTC_BUILTIN_PATH,2)==XTC_ERR_CAPACITY && s->project()==full_before);
CHECK(window.set_track_chain(track,restored_chain)==XTC_OK);
CHECK(window.save_project("/tmp/xtc-inserts-gui.xtc")==XTC_OK && window.new_project()==XTC_OK && window.open_project("/tmp/xtc-inserts-gui.xtc")==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),track,&chain)==XTC_OK && chain.count==1);unlink("/tmp/xtc-inserts-gui.xtc");
window.do_callback();puts("Track inserts GUI adoption/undo/rollback/reload passed.");return failures ? 1 : 0;

View file

@ -83,6 +83,26 @@ with tempfile.TemporaryDirectory(prefix="xtc-inspect-") as directory:
):
project.write_text(malformed)
check("--validate", project, 1)
# V17 track-chain dependency inspection must not dlopen the readable fake module.
for number, name, identity in ((1, "CLAP", "org.xtc.fixture"), (2, "VST2", ""),
(3, "VST3", "0123456789abcdef0123456789abcdef")):
chain = ("CHAINS 1\nCHAIN 0 1 3f800000\n"
f"INSERT 1 3f800000 {number} 0 42 1 120000 {b'effect'.hex()}\n"
f"MODULE {str(module).encode().hex()}\nIDENTITY {identity.encode().hex() if identity else '-'}\n"
"CONTROLS 0\nPARAMETERS 0\n")
document = base.replace("XTC_PROJECT 10", "XTC_PROJECT 17").replace(
"END\n", "TRACK_INSTRUMENTS 0\nLIVE_INSTRUMENT 18446744073709551615\nMARKERS 0\n" + chain + "END\n")
project.write_text(document)
check("--inspect", project, 0, name + " insert track 0 slot 1", "no plugin code executed", "missing/unreadable: 0")
for malformed in (document.replace("CHAIN 0 1", "CHAIN 0 9"),
document.replace("INSERT 1 3f800000", "INSERT 1 7f800000"),
document.replace("CHAIN 0 1", "CHAIN 99 1")):
project.write_text(malformed)
check("--validate", project, 1)
project.write_text(document.replace(str(module).encode().hex(), b"/missing/effect.so".hex()))
check("--inspect", project, 2, name + " insert track 0", "missing/unreadable: 1")
project.write_text(document.replace("XTC_PROJECT 17", "XTC_PROJECT 16").replace(chain, ""))
check("--validate", project, 0, "metadata valid")
for text, messages in [
(base.replace("XTC_PROJECT 10", "XTC_PROJECT 999"), ("line 1", "999", "1..17")),
("XTC_PROJECT 10\n", ("line 2", "APP")),

View file

@ -45,7 +45,21 @@ static Steinberg_uint32 latency(void *self) { (void)self; return 0; }
static Steinberg_tresult setup(void *self, struct Steinberg_Vst_ProcessSetup *data) { (void)self; return data->maxSamplesPerBlock > 0 && data->sampleRate > 0 ? 0 : -1; }
static Steinberg_tresult processing(void *self, Steinberg_TBool value) { instance *p = from_processor(self); assert(p->active); p->started = value; return 0; }
static Steinberg_tresult process(void *self, struct Steinberg_Vst_ProcessData *data)
{ instance *p = from_processor(self); int32_t i; assert(p->started); for (i = 0; i < data->numSamples; ++i) data->outputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i] = data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i]*.5f; return 0; }
{
instance *p=from_processor(self);int32_t i;Steinberg_int32 index=-1,offset=-1;Steinberg_Vst_ParamValue value=-1;
Steinberg_Vst_IParameterChanges *changes=data->outputParameterChanges;Steinberg_Vst_IParamValueQueue *queue;Steinberg_Vst_ParamID id=7;
assert(p->started && changes && changes->lpVtbl->getParameterCount(changes)==0);
queue=changes->lpVtbl->addParameterData(changes,&id,&index);assert(queue && index==0);
assert(queue->lpVtbl->getParameterId(queue)==7 && queue->lpVtbl->getPointCount(queue)==0);
assert(queue->lpVtbl->addPoint(queue,0,.2,&index)==0 && queue->lpVtbl->addPoint(queue,1,.8,&index)==0);
assert(queue->lpVtbl->getPointCount(queue)==1 && queue->lpVtbl->getPoint(queue,0,&offset,&value)==0 && offset==1 && value==.8);
assert(changes->lpVtbl->getParameterData(changes,0)==queue && changes->lpVtbl->getParameterData(changes,-1)==NULL);
for(id=8;id<71;++id)assert(changes->lpVtbl->addParameterData(changes,&id,&index));
assert(changes->lpVtbl->getParameterCount(changes)==64);
assert(changes->lpVtbl->addParameterData(changes,&id,&index)==NULL);
for(i=0;i<data->numSamples;++i)data->outputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i]=data->inputs[0].Steinberg_Vst_AudioBusBuffers_channelBuffers32[0][i]*.5f;
return 0;
}
static Steinberg_Vst_IComponentVtbl component_vtable = {cq,add,release,init,terminate,controller,mode,buses,bus_info,routing,activate_bus,active,state,state};
static Steinberg_Vst_IAudioProcessorVtbl processor_vtable = {pq,pa,pr,arrangements,arrangement,sample_size,latency,setup,processing,process,latency};
static Steinberg_tresult fq(void *self, const Steinberg_TUID iid, void **out) { (void)iid; *out = self; return 0; }

View file

@ -2,6 +2,7 @@
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_editing.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_sample.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>
@ -47,9 +48,36 @@ static void rendering(void)
for(size_t j=0;j<32;++j)assert(fabsf(out[j]-(j<8 ? 0.f : .05f*sinf(6.28318530718f*440.f*(float)(j-8)/8000.f)))<.00001f);
xtc_midi_plan_destroy(midi);xtc_project_destroy(p);unlink(wave);
}
static void host_vst3(void)
{
xtc_project *p=NULL;xtc_track_chain c={0};xtc_midi_plan *plan=NULL;uint64_t t,clip,note;float out[256];double energy=0;
if(!getenv("XTC_TEST_HOST_VST3"))return;
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Host VST3",&t)==XTC_OK && xtc_project_add_midi_clip(p,t,0,960,&clip)==XTC_OK && xtc_project_add_note(p,clip,0,960,69,100,0,&note)==XTC_OK);
const char *names[]={"ZamEQ2","ZamAutoSat","ZamDelay"};c.count=3;c.output_gain=1;
for(size_t i=0;i<3;++i) {
char binary[512];snprintf(binary,sizeof(binary),"/usr/lib/vst3/%s.vst3/Contents/x86_64-linux/%s.so",names[i],names[i]);
assert(xtc_plugin_probe_format(XTC_PLUGIN_VST3,binary,0,48000,&c.slots[i].plugin)==XTC_OK);
c.slots[i].enabled=1;c.slots[i].wet=1;printf("Host VST3 loaded: %s (%s)\n",c.slots[i].plugin.label,c.slots[i].plugin.identifier);
}
assert(xtc_project_set_chain(p,t,&c)==XTC_OK && xtc_midi_plan_create(p,48000,&plan)==XTC_OK);
for(size_t block=0;block<32;++block) {
assert(xtc_midi_plan_render(plan,block*256,NULL,0,out,256)==XTC_OK);
for(size_t i=0;i<256;++i){assert(isfinite(out[i]));energy+=out[i]*out[i];}
}
assert(energy>0);xtc_midi_plan_destroy(plan);
{char wave[]="/tmp/xtc-vst3-export-XXXXXX";int fd=mkstemp(wave),done=0;xtc_export *job=NULL;xtc_sample *sample=NULL;xtc_sample_stats stats;size_t frames;unsigned int rate;
assert(fd>=0);close(fd);unlink(wave);
assert(xtc_export_create(p,wave,48000,100,&job)==XTC_OK);
while(!done)assert(xtc_export_step(job,&done)==XTC_OK);
xtc_export_destroy(job);
assert(xtc_sample_load(wave,&sample)==XTC_OK && xtc_sample_info(sample,&rate,&frames)==XTC_OK && rate==48000 && frames>0);
assert(xtc_sample_analyze(sample,0,frames,&stats)==XTC_OK && stats.rms>0);
xtc_sample_destroy(sample);unlink(wave);}
xtc_project_destroy(p);
}
int main(void)
{
rendering();
rendering();host_vst3();
xtc_project *p=NULL,*q=NULL;uint64_t a,b,duplicate;xtc_track_chain c={0},got={0};
char file[]="/tmp/xtc-chain-XXXXXX";int fd=mkstemp(file);assert(fd>=0);close(fd);
assert(xtc_project_create(&p)==XTC_OK && xtc_project_add_track(p,"Audio",&a)==XTC_OK && xtc_project_add_track(p,"Other",&b)==XTC_OK);

View file

@ -6,6 +6,8 @@
#include "../src/gui/piano_roll.hpp"
#include "../src/gui/plugin_dialog.hpp"
#include "../src/gui/plugin_library.hpp"
#include "../src/gui/track_inserts.hpp"
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl.H>
#include <FL/Fl_Tabs.H>
#include <FL/Fl_Menu_Bar.H>
@ -61,6 +63,18 @@ int main(int argc,char **argv) {
MainWindow window(false,"");main_window=&window;window.resize(0,0,1400,900);window.show();check(window.open_project(project.c_str()));pump();
auto *s=first<SequencerWidget>(window);if(!s)throw std::runtime_error("Sequencer missing");
auto reset=[&](){window.reset_layout();check(window.open_project(project.c_str()));window.resize(0,20,1400,900);tab("Sequencer");s->select(0,0);s->position(0);check(window.set_loop(3840,7680,true));window.fit_song();pump();};
if(std::getenv("XTC_CAPTURE_HOST_VST3")) {
reset();capture("compact-workspace");
auto *create=dynamic_cast<Fl_Menu_Button *>(named(window,"Create"));if(!create)throw std::runtime_error("Missing Create dropdown");
Fl::add_timeout(.3,[](void*){capture("create-track-menu");if(system("xdotool key Escape"))std::abort();});
create->menu()->pulldown(create->x(),create->y(),create->w(),create->h(),nullptr,create);
const char *effects[]={"ZamEQ2","ZamAutoSat","ZamDelay"};
for(const char *effect:effects){std::string binary=std::string("/usr/lib/vst3/")+effect+".vst3/Contents/x86_64-linux/"+effect+".so";check(window.add_track_effect(0,binary.c_str(),0));}
auto *inserts=window.track_inserts();inserts->select_track(0);tab("Track inserts");capture("track-inserts-vst3");
dialog("track-insert-vst3-identity",[&]{check(inserts->inspect_slot());});
check(inserts->set_mix(.65,1));capture("track-inserts-parallel");
window.do_callback();std::puts("Installed host VST3 captures completed.");return 0;
}
if(std::getenv("XTC_CAPTURE_SAMPLE_ONLY")) {
reset();char source[4096];if(!realpath("examples/projects/original-pluck.wav",source))throw std::runtime_error("Missing sample WAV");
check(window.create_track(false));check(window.import_audio(source));