Internal · SpaceMusic Engineering · Plan 033
File loading without the patch-side machinery
One CSV row replaces three, the file dialog stops touching the patch, and patches still get a node to drive it.
The least elegant corner of SpaceMusic
Every plugin we have loads a file from somewhere. There are about a dozen distinct load sites across the system — Plugin1D, 2D, 3D, Light, Stage, PostFX, Placement, Layer-Object, AudioPlayer, MidiPlayer, plus the scene save/load. For a system that loads as many files as we do, the way we declare a load site is the part of the codebase the user finds least elegant.
Declaring a load site today costs us three CSV rows, a sub-patch (FileNameManager) that exists only to keep dropdowns in sync with the disk, and a vvvv-side record (FolderFileData) held in a public-channel slot for the scene save to iterate. We've already replaced the OS file dialog with our own Avalonia browser; everything else around it is still patch glue.
How a load site looks today
For every plugin that loads a file, the parameter CSV carries three rows. Concretely, for a Plugin1D instance:
Plugin1DNFolder,Dropdown,5,...,Environment,Plugins 1D,Plugin 1D N,Files,...
Plugin1DNFile,Dropdown,5,...,Environment,Plugins 1D,Plugin 1D N,Files,rep:true,...
Plugin1DNLoad,Button,2,...,Environment,Plugins 1D,Plugin 1D N,Files,...
The first declares a Library subfolder dropdown. The second declares a file-within-folder dropdown. The third declares the load button. Each plugin family — twelve of them — repeats this triple. The two dropdowns are populated at runtime by a patch (FileNameManager in SpaceMusic_UI.vl) that enumerates the disk on each frame and writes the results to the dropdown channels.
The load button fires the Avalonia browser through SmBrowserDialog, which works well — that's not the problem. The problem is everything that happens before and after the dialog: the dropdown sync patch, the per-plugin three-row decoration, and a separate ProjectFiles record (lifted from a fixed channel slot at SpaceMusic.vl ~34817) that SaveAndCopyProjectFiles walks at scene-save time to copy each loaded file into the scene folder.
This bookkeeping has worked, but it leaks. There is no single place in the codebase that says "a Plugin1D loads audio." The fact is implicit in the default folder name, the file-dropdown contents, and a constant in a helper sub-patch. Adding a new plugin family means writing the three rows by hand, wiring the file-dropdown into the plugin's own logic, and remembering to register the load site with the registry so scene-save sees it.
What's good, what isn't
The Avalonia browser itself is a finished story. Plan 021 replaced the OS dialog with an in-app picker that knows about content types (ImageFormat, AudioFormat, VideoFormat, SceneFormat, ObjectFormat, DocumentFormat). It opens through IBrowserService.OpenDialogAsync, accepts a root folder and a format filter, and returns a BrowserDialogResult { FilePath, Ok }. Single instance, modal, no surprises.
What hasn't followed the browser into C# is the wiring around it. The click-to-dialog path still goes through a patch-side bang. The dialog-to-channel path is a per-plugin sub-patch. The registry is a vvvv record. Three different abstractions for what's logically one thing: a file got loaded — remember what and where.
The eight *Dropdown01..08 rows on each plugin look like they might be related — they aren't. Those are general-purpose parameter dropdowns each plugin owns for its own logic. This plan leaves them alone.
One row, one load site
The plan introduces a new CSV TypeID, FileLoader, whose job is to be the only thing a plugin author needs to write to declare that the plugin loads a file. One row replaces the existing three. The row carries everything — display label, default format, default subfolder, opt-in dropdown flag:
Plugin1DNFile,FileLoader,30,,Load File..,...,format:Audio;subfolder:Audio;dropdown:true,...,Plugin1D,...
From that single row, codegen produces four sibling fields on the plugin model and registers the site in a manifest the runtime consumes. The Pro UI renderer learns one new control kind. A new LoadFileService in C# takes over the click-to-dialog-to-channel-write path. A small FileLoader ProcessNode exposes the same site to vvvv patches that want to subscribe or drive it. The patch-side FileNameManager and the ProjectFiles record both retire.
Figure 1 · What one FileLoader CSV row becomes
The dashed boxes are overrides — empty by default, populated only when a patch wants to deviate from what the CSV declared. The opt-in *FolderContents exists only for rows that opted into the file-flip dropdown. LoadFileService and Project.Files are coral because they're the load-bearing additions; everything else is either codegen plumbing or a rename.
From row to working load site
The plumbing is mostly extension points we already use, in the same shape. Five steps walk a CSV row to a working load.
Codegen
SMCodeGen's Models.cs:57 CSharpParamType switch already maps every existing CSV type to a leaf class (ParamFloat, ParamUnit, ParamOptions<TEnum>, etc.). Adding the FileLoader case is one new entry that returns ParamFile — a hand-written record carrying the file path and the bang counter. UserData (format:, subfolder:, dropdown:true) is parsed on the same row and stored on the model entry so downstream emitters can read it.
Generated channels
For each FileLoader row, codegen emits four sibling fields on the plugin model: the ParamFile itself plus the two override channels (*FileFormat, *RootFolder) and — for dropdown:true rows — a bare Spread<string> *FolderContents field. The overrides are hidden from the Pro UI but visible to the patch: a patch that wants a Layer-Object loader to start at Library/3DObjects writes that string to LayerRootFolder and the next click respects it.
Renderer
The Pro UI renderer's single dispatch point is CsvPageView.BuildParamRow at :298. Adding a case UiControlKind.FileLoader there hands off to a new UiFactory.FileLoaderRow. Button label is the loaded file's name, or "Load File.." when the path is empty. When dropdown:true, a ComboBox sits above the button, bound to *FolderContents; selecting an entry writes the full path to ParamFile.Path without opening the dialog.
Runtime
A new LoadFileService in SpaceMusic.UI.Stride/Channels follows the existing DynamicEnumCoordinator pattern: per-frame Update(hub), retry-bind on first sight, subscribe once bound. For every site it watches ParamFile.Load — the bang counter — and on rising edge resolves format and start folder through the override chain (override → last-folder → CSV default), opens IBrowserService.OpenDialogAsync, and on Ok writes the result back. The same write atomically appends or replaces the matching ProjectFile entry in Project.Files.
Registry
Project.Files is one top-level channel — Spread<ProjectFile>, owner-keyed ("Plugin1D[2]", "AudioPlayer1"). Scene save iterates it and copies each owner's source file into a per-owner subfolder. Scene load repopulates it. The old vvvv-side ProjectFiles record and the FolderFileData sub-record disappear; SaveAndCopyProjectFiles rewires to read the new channel and keeps its existing copy logic.
The escape hatches
Three pieces keep this from being a one-trick design.
-
Per-plugin override channels.
*FileFormatand*RootFolderare siblings on every load site. CSV is the default; the channel is the override. A patch can pin a Layer-Object loader toLibrary/3DObjectswithout touching the CSV; empty override falls back to CSV declaration. Plugin1DFileFormat · Plugin1DRootFolder -
The
dropdown:trueflag. The file-flip workflow lives. Opt-in per row: the load button gets a ComboBox above it that lists siblings of the current file matching the resolved format. Picking an entry writes the path directly — no dialog.AudioPlayer1Prev/NextandMidiPlayer1Prev/Nextiterate this channel to keep their semantics. UserData: format:Audio;subfolder:Audio;dropdown:true -
A vvvv
FileLoadernode, bind-only. Patches that want to subscribe to a load site by name drop this node, setOwner = "Plugin1D[0]", and readPathlive. A rising edge onTriggerfires the dialog. The node cannot create new sites at runtime — only bind to ones the CSV declared. That keeps structural change on the same side as every other structural decision in SpaceMusic. Inputs: Owner, Trigger · Output: Path
Why this matters
"The Avalonia browser stopped being the limiting factor a year ago. Everything around it is patch glue — and patch glue costs us every time we add a new plugin."
This is a small plan in code terms. One new TypeID. One hand-written record (ParamFile). One service class. One ProcessNode. One renderer case. A patch deletion (FileNameManager) and a rewire (SaveAndCopyProjectFiles). The codegen extension points were already there for every other parameter type; we're just adding one more entry to switches that already accept entries.
What it removes is a recurring tax. The cost of adding "this plugin loads a file" to a new plugin family drops from three CSV rows plus a sub-patch entry to one CSV row. The patch loses an entire sub-patch and a hand-iterated record that exists nowhere outside SpaceMusic. The user-facing experience improves quietly: per-plugin format pinning, per-plugin start-folder pinning, and a file-flip dropdown that's now declared in the CSV instead of patched in by hand.
The implementation plan ships in thirteen checkpoint-able steps starting with the ParamFile leaf type and ending with the patch-side cleanup. Plugin1D is the pilot; the bulk migration follows once the codegen and renderer work is proven on one family. None of this is irreversible — the new TypeID and the old three-row pattern can coexist mid-migration if we want to stage the rollout — but the plan calls for full replacement in Phase 1 to avoid carrying two shapes of the same idea.
Settled
FileLoader TypeID + bind-only vvvv node
New CSV row replaces three. *FileFormat and *RootFolder stay as per-row overrides. The vvvv node binds to CSV-declared sites by Owner — no ad-hoc creation. Full replacement in Phase 1, current branch.
Next step
Step 1 — ParamFile + FileFormatEnum
Add the hand-written ParamFile record in SpaceMusicMeta. Add the six-entry FileFormatEnum to the Dropdowns CSV. Build and confirm the type compiles. Steps 2–6 are pure codegen and renderer wiring on top of it.
Future
Phase 2 — Scene file copier in C#
Once the new registry is in place, SaveAndCopyProjectFiles moves from vvvv to a C# service. Cross-session last-folder persistence and the SaveScene-target browser dialog round out the file story.