How to Organize Project Files¶
Structure your IVLS product using the base-features / impl-features pattern, keeping abstract interfaces separate from concrete implementations so that multiple products can share the same engine.
Prerequisites¶
- A product that is growing beyond a few files
- Familiarity with nodes and engine boxes
Steps¶
1. Create the folder structure¶
Organize your project into the following directory layout:
Code/
my-engine/
base-features/ { Abstract interfaces with required function contracts }
engine-ranges.ksp
engine-sound-data.ksp
engine-releases.ksp
features/
boxes/ { Engine box definitions (data, meta, ui) }
my-globals/
my-globals-data.ksp
my-globals-meta.ksp
my-globals-ui.ksp
my-layers/
my-layers-data.ksp
my-layers-meta.ksp
my-layers-ui.ksp
playback/ { Flow definitions, spawn, play event nodes }
events.ksp
flows.ksp
spawn.ksp
gui/ { UI logic and bindings }
gui-binds.ksp
gui-logic.ksp
api-modrix.ksp { Modrix API implementation }
purge.ksp
serialization.ksp
tact-assembly.ksp { TACT config if applicable }
ivls-product.ksp { Product assembly and IVLS_NODES }
my-product/
impl-features/ { Concrete implementations for this specific product }
my-engine-ranges.ksp
my-engine-sound-data.ksp
my-engine-releases.ksp
features/
flows.ksp { Product-specific flow overrides }
product-types.ksp
ivls-product.ksp { Final product assembly }
build.ksp { Top-level build entry point }The key separation is between my-engine/ (the reusable engine) and my-product/ (the product-specific overrides).
2. Separate abstract interfaces into base-features¶
Base-features files define the interface contract: they list required functions that must be implemented by any product using this engine. Use a comment block at the top of the file documenting the required functions:
{ Code/my-engine/base-features/engine-ranges.ksp }
define MyEngine.Features += MyEngine.Impl.Ranges
{
Required functions:
my.get_instr_min_note() -> min
- Return minimum MIDI note for the current instrument
my.get_instr_max_note() -> max
- Return maximum MIDI note for the current instrument
}The define += pattern adds the implementation node to a bundle without hardcoding which file provides it. The base-features file declares what is needed; the impl-features file delivers it.
3. Implement concrete constants in impl-features¶
In the product's impl-features/ directory, create the actual implementation of each base-feature interface:
{ Code/my-product/impl-features/my-engine-ranges.ksp }
node MyEngine.Impl.Ranges:
cb Init:
family my
declare instrument_ranges[6, 2] := (...
55, 98, ...
48, 86, ...
36, 76, ...
24, 67, ...
24, 98, ...
24, 98 ...
)
end family
cb Functions:
function my.get_instr_min_note() -> min
min := my.instrument_ranges[INSTRUMENT_ID, 0]
end function
function my.get_instr_max_note() -> max
max := my.instrument_ranges[INSTRUMENT_ID, 1]
end function
end nodeSimilarly for sound data, create the base-features contract:
{ Code/my-engine/base-features/engine-sound-data.ksp }
define MyEngine.SoundData += MyEngine.Impl.SoundData, ...
MyEngine.Base.SoundData
{
Required functions:
my.get_num_sounds.const()
- Return the total number of sounds available
my.get_play_range_min_key.const()
- Return the lower boundary of the playable range
my.get_play_range_max_key.const()
- Return the upper boundary of the playable range
}And the implementation:
{ Code/my-product/impl-features/my-engine-sound-data.ksp }
node MyEngine.Impl.SoundData:
cb Functions:
function my.get_num_sounds.const() -> return
return := 144
end function
function my.get_play_range_min_key.const() -> return
return := 82
end function
function my.get_play_range_max_key.const() -> return
return := 96
end function
end node4. Split engine boxes into the three-file pattern¶
Each engine box is split across three files in its own subdirectory:
Data file -- declares dimensions, members, and parameter names:
{ Code/my-engine/features/boxes/my-layers/my-layers-data.ksp }
define my.layers.NUM_DIMS := 2
define my.layers.DIMS := layer, par
define my.layers.MULTS := layer
define my.layers.SIZES := 8, 128
define my.layers.layer.MEMBERS := A, B, C, D
define my.layers.par.MEMBERS := ...
enable, sound_id, volume, pan, mute, solo, coarse_tune, fine_tune
node MyEngine.LayerData:
cb Init:
Data.InitializeDimension(my.layers, layer)
Data.InitializeDimension(my.layers, par)
Data.CreateMultiDim(my.layers)
cb Reload:
Data.PCCB(my.layers)
cb UICBS:
Data.UICBS(my.layers)
cb Functions:
Data.Functions(my.layers)
end nodeMeta file -- defines parameter ranges, defaults, and display properties:
{ Code/my-engine/features/boxes/my-layers/my-layers-meta.ksp }
node MyEngine.LayerMeta:
cb Init:
{ Set engine ranges and defaults for each parameter }
my.layers.par_meta[my.layers.par.ENABLE].engine_min := 0
my.layers.par_meta[my.layers.par.ENABLE].engine_max := 1
my.layers.par_meta[my.layers.par.ENABLE].default := 1
my.layers.par_meta[my.layers.par.VOLUME].engine_min := 0
my.layers.par_meta[my.layers.par.VOLUME].engine_max := 1000000
my.layers.par_meta[my.layers.par.VOLUME].default := 750000
end nodeUI file -- handles parameter display and UI callbacks:
{ Code/my-engine/features/boxes/my-layers/my-layers-ui.ksp }
node MyEngine.LayerUI:
cb Functions:
function my.layers.par_disp.response(layer, par, value, par_prefix_str, par_value_str)
par_prefix_str := f'Layer < char_up[layer] >'
select par
case my.layers.par.VOLUME
par_value_str := group[grp].par[EP(VOLUME)].disp & " dB"
case my.layers.par.PAN
par_value_str := group[grp].par[EP(PAN)].disp
end select
end function
end node5. Organize node bundles with define += pattern¶
Use define += to accumulate nodes into bundles without monolithic lists. Each feature file adds itself to the appropriate bundle:
{ In features/playback/flows.ksp }
define MyEngine.PlaybackFeatures += MyEngine.Flows
{ In features/playback/spawn.ksp }
define MyEngine.PlaybackFeatures += MyEngine.Spawn.Layers
{ In features/playback/events.ksp }
define MyEngine.PlaybackFeatures += MyEngine.PlayEventThen in the engine box data files:
{ In features/boxes/my-layers/my-layers-data.ksp }
define MyEngine.Engine += MyEngine.LayerDataThese bundles are referenced in IVLS_NODES:
define IVLS_NODES += ..., ...
MyEngine.Engine, ...
MyEngine.PlaybackFeatures, ...
MyEngine.UI.Bundle, ...
...6. Assemble the final product¶
The product-level ivls-product.ksp ties everything together:
{ Code/my-product/ivls-product.ksp }
define ivls.THREADS := 4
define IVLS_NODES += MyEngine.SoundData, ...
MyEngine.Engine, ...
MyEngine.PlaybackFeatures, ...
MyEngine.UI.Bundle, ...
MyProduct.Features
define PRODUCT_GUI_HEIGHT := 730
import '_IVLS/builder.ksp'Complete folder layout¶
Code/
my-engine/
base-features/
engine-ranges.ksp { interface: get_instr_min_note, get_instr_max_note }
engine-sound-data.ksp { interface: get_num_sounds, get_play_range }
engine-releases.ksp { interface: release config }
features/
boxes/
my-globals/
my-globals-data.ksp
my-globals-meta.ksp
my-globals-ui.ksp
my-layers/
my-layers-data.ksp
my-layers-meta.ksp
my-layers-ui.ksp
playback/
events.ksp
flows.ksp
spawn.ksp
gui/
gui-binds.ksp
gui-logic.ksp
api-modrix.ksp
purge.ksp
tact-assembly.ksp
ivls-product.ksp
my-product/
impl-features/
my-engine-ranges.ksp { implements: get_instr_min_note, get_instr_max_note }
my-engine-sound-data.ksp { implements: get_num_sounds, get_play_range }
features/
flows.ksp
ivls-product.ksp
build.kspVerify¶
- Build your product -- all base-feature required functions should be resolved by impl-features
- If a required function is missing, the build will fail with an undefined function error, pointing you to the missing implementation
- Check that adding a new product variant only requires creating a new
impl-features/directory and product-levelivls-product.ksp
Further reading¶
- Guide: Nodes for how
define +=bundles work - Guide: Multi-Dim Engine Boxes for the three-file box pattern
- Guide: run<< and read<< blocks for how Python packages integrate with the build