Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ set(HALIONBRIDGE_DIAGNOSTIC_DEFINITIONS

set(HALIONBRIDGE_LIBRARY_SOURCES
src/Bridge.cpp
src/BuildWorker.cpp
src/ChildProcessOutput.cpp
src/CrashDiagnostics.cpp
src/Log.cpp
Expand Down
15 changes: 8 additions & 7 deletions DEVELOPMENT.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions HALION-LUA.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ return {

Module names may be listed with or without `.lua`. They are loaded with `require`, so they must be resolvable from the build directory passed to halionbridge. The host-side helper `Bridge::parseBuildFileModuleNames()` inspects the common top-level list shape `return { "module_a", "module_b" }`; it is not a full Lua parser, and it ignores quoted strings in line comments, Lua long comments, nested tables, local variables, and metadata fields.

For statically parseable build files, halionbridge runs the list in host-controlled chunks of one module by default. Each chunk launches a fresh HALion instance and the generated runtime module sets `HALIONBRIDGE_BUILD_SLICE_START`, `HALIONBRIDGE_BUILD_SLICE_COUNT`, and `HALIONBRIDGE_BUILD_TOTAL` before loading this builder. The builder uses those globals only to select the requested list slice and to report file-level progress against the full list. Build script modules and the `ctx` API are unchanged. If the host cannot statically parse the build file, it falls back to one full-list invocation.
For statically parseable build files, halionbridge runs the list in host-controlled chunks of up to 15 modules by default. Each chunk launches a fresh HALion instance and the generated runtime module sets `HALIONBRIDGE_BUILD_SLICE_START`, `HALIONBRIDGE_BUILD_SLICE_COUNT`, and `HALIONBRIDGE_BUILD_TOTAL` before loading this builder. The builder uses those globals only to select the requested list slice and to report file-level progress against the full list. Build script modules and the `ctx` API are unchanged. If the host cannot statically parse the build file, it falls back to one full-list invocation.

`halionbridge init <build-directory>` can generate a simple `halionbridge_build.lua` from top-level `.lua` files. It sorts filenames, keeps the `.lua` suffix in each entry, and excludes halionbridge infrastructure files such as `halionbridge_runtime.lua`, `halionbridge_builder.lua`, `halionbridge_build.lua`, and `builder_bootstrap.lua`; converter-owned infrastructure helpers may also be excluded. It does not recurse into subdirectories and does not launch HALion. Review the generated list before building: ordinary helper modules such as `helpers.lua` or `shared_mapping.lua` are still top-level Lua files, but they should be removed from `halionbridge_build.lua` unless they return a valid build script entrypoint.

Expand Down Expand Up @@ -92,7 +92,7 @@ ctx.progress(done, total, message)

`ctx.progress` is build-script-defined progress. `done` and `total` can represent files, zones, presets, phases, or any other work unit meaningful to that build script.

Before running the batch, the builder raises HALion's controller script execution timeout to 600000 ms when HALion exposes `getScriptExecTimeOut()` and `setScriptExecTimeOut()`, then restores the previous value after writing the final status marker. Build scripts should not call HALion `wait()` for this purpose; HALion only allows `wait()` inside callbacks, while build script entrypoints run as controller global/module code.
Before running the batch, the builder raises HALion's controller script execution timeout to 600000 ms when HALion exposes `getScriptExecTimeOut()` and `setScriptExecTimeOut()`, then restores the previous value after the final status-marker write attempt even if marker writing fails. Build scripts should not call HALion `wait()` for this purpose; HALion only allows `wait()` inside callbacks, while build script entrypoints run as controller global/module code.

## Build Script Result

Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ For source formats such as SFZ, halionbridge can also generate the Lua build dir
## What This Is

- A batch automation host for HALion Lua build scripts.
- A JUCE-free C++23 library interface with a command-line frontend for running the same builder workflow.
- A C++23 library interface with a command-line frontend for running the same builder workflow.
- Infrastructure for converters and scripted HALion instrument builds.
- A way to produce real HALion `.vstpreset` artifacts through HALion itself. The original HALion plugin is required and must be installed.

Expand All @@ -34,15 +34,21 @@ For source formats such as SFZ, halionbridge can also generate the Lua build dir
- A graphical preset authoring tool.
- A direct writer for undocumented HALion preset file internals.

## Use Cases
### Use Cases

- **Batch-generate HALion presets from structured instrument data:** Generate Lua build scripts from another format or database (e.g. CSV files), run them through halionbridge, and let HALion save the resulting `.vstpreset` files.
- **Build sample-mapped instruments programmatically:** Create layers, zones, key ranges, velocity ranges, sample assignments, loop settings, and HALion parameters from Lua instead of assembling them manually in the GUI.
- **Converter workflows:** Generate Lua from source formats such as SFZ, run that Lua inside HALion with halionbridge, and let HALion handle the preset-writing step.
- **SFZ-to-HALion Lua conversion:** Convert a directory of `.sfz` files into a halionbridge build directory, then run that build directory to create layer `.vstpreset` files.
- **Regenerate preset libraries reproducibly:** Treat HALion presets as build artifacts that can be recreated from source Lua build scripts and sample files.
- **Run HALion Lua build scripts in CI-like or headless workflows:** Execute scripted HALion builds without opening an audio device, with success/failure reported through marker presets (requires the licensed and activated HALion plugin on the build server).
- **Debug scripted HALion builds with optional GUI inspection:** Use `--gui` or `--nokill` when a build script needs visual inspection after running.
* **Programmatic Instrument Creation:** Build sample-mapped instruments—including layers, zones, key ranges, velocity ranges, sample assignments, and parameters—directly via Lua instead of manually assembling them in the HALion GUI.
* **Automated Converter Workflows:** Generate `.vstpreset` files automatically from structured data (e.g., CSV databases) or existing source formats (like SFZ) by bridging them through Lua build scripts.
* **Reproducible Preset Libraries:** Treat HALion presets as reliable build artifacts that can be entirely regenerated from source Lua scripts and sample files.
* **CI and Headless Automation:** Execute scripted HALion builds seamlessly on build servers or CI pipelines without opening an audio device.
* **Script Debugging:** Visually inspect scripted builds by using `--gui` or `--nokill` flags to keep the HALion GUI open after a run.

### Features

* **Headless & Offline Processing Loop:** Runs as an embeddable standalone console app. It utilizes an offline manual processing loop to keep the HALion plugin alive while Lua scripts execute, completely bypassing the need for an active audio device.
* **Native Format Conversion Setup:** Commands like `halionbridge convert sfz` parse source directories and generate a flat or explicitly routed `halionbridge` build directory. Users can review or edit these generated Lua scripts before triggering the final build.
* **Marker-Based Status Detection:** Tracks build progress and completion by waiting for HALion to write `.vstpreset` status markers into the build directory. Temporary progress markers are automatically cleaned up, while failure markers are preserved for diagnostics.
* **Embedded Bootstrap:** Automatically applies the bundled HALion bootstrap `.vstpreset` internally, meaning users only ever need to pass their target build directory to the CLI.
* **Generic Script Delegation:** The embedded builder acts as a blank slate; it simply loads the modules listed in `halionbridge_build.lua`, leaving the actual decision of *what* to build entirely to the Lua scripts.

## Usage

Expand Down Expand Up @@ -80,8 +86,11 @@ The build directory must contain `halionbridge_build.lua` and the Lua build scri
# Run the generic HALion Lua builder against a build directory
./halionbridge /path/to/build-directory

# By default, halionbridge waits forever and prints a warning. Set a timeout when desired.
./halionbridge /path/to/build-directory --timeout-seconds 3600
# Builds time out after 3600 seconds by default.
./halionbridge /path/to/build-directory --timeout-seconds 1800

# Explicitly wait forever when manually inspecting a problematic build.
./halionbridge /path/to/build-directory --no-timeout

# Build scripts run up to 15 scripts per HALion process by default and continue after failed chunks.
# Use --build-chunk-size 1 for maximum isolation, or tune chunk size when desired.
Expand All @@ -99,22 +108,13 @@ The build directory must contain `halionbridge_build.lua` and the Lua build scri
./halionbridge /path/to/build-directory --gui --nokill
```

The SFZ converter is a setup step: it generates normal Lua build files and does not launch HALion. When no output directory is passed, generated files are written flat beside the source `.sfz` files; otherwise they are written to the requested build directory. Existing generated files are refused unless `--overwrite` is supplied, and conversion fails before writing if two inputs would produce the same output preset name.
The SFZ converter is a setup step: it generates normal Lua build files and does not launch HALion. When no output directory is passed, generated files are written flat beside the source `.sfz` files; otherwise they are written to the requested build directory. Existing generated files are refused unless `--overwrite` is supplied, and conversion fails before writing if two inputs would produce the same output preset name. Generated build files are staged before commit so a write failure does not leave a half-updated build directory.

Generated SFZ output includes an inspectable helper module, `halionbridge-sfz.lua`, plus one build entrypoint script per source `.sfz`. The current converter covers the common sample-mapping path: sample filenames, key/velocity ranges, root keys, playback ranges, sustain loops, gain, pan, static amplitude envelopes, static pitch/tuning, a simple static pitch-envelope subset, and rough static filter approximations. Unsupported or unverified SFZ features are reported as warnings instead of being silently treated as exact conversions. Detailed mapping notes and current parity limits live in `DEVELOPMENT.md`.

halionbridge prints timestamped console logs. The default log level is `info`, which keeps build script progress and important state changes visible while hiding host internals. Set `HALIONBRIDGE_LOGLEVEL=debug` when you need plugin loading, VST3 preset, and cleanup diagnostics. Supported values are `trace`, `debug`, `info`, `warn`, `error`, and `off`.

Only one halionbridge build can run at a time for a user account. HALion resolves temporary runtime modules from the shared HALion user script directory, so a second overlapping run exits with a clear error instead of corrupting build output.

Press Ctrl+C to request a graceful stop. Conversion commands stop at converter checkpoints before writing more generated files. HALion builds stop as soon as control returns from the currently running HALion script or between chunks, then normal cleanup runs.

## Features
- **Headless Execution:** Runs as a standalone console app.
- **Embedded Bootstrap VSTPreset:** Applies the bundled HALion bootstrap preset internally; users only pass a build directory.
- **Offline Processing Loop:** Runs a manual processing loop without opening an audio device, to keep HALion alive while LUA instrument scripts execute.
- **Generic HALion Lua Build Scripts:** The embedded builder loads build script modules listed in `halionbridge_build.lua`; the build script modules decide what to build.
- **Converter Setup Commands:** `halionbridge convert sfz` creates a normal halionbridge build directory from `.sfz` files without launching HALion. When no output directory is provided, generated Lua/build files are written flat into the source directory; an explicit output directory can still be passed. The generated Lua build scripts can be reviewed or edited before running `halionbridge <build-directory>`. Converter-generated Lua filenames are kept inside the build root; unsafe generated paths are rejected. Converter-generated helper modules may be written beside build scripts without being listed in `halionbridge_build.lua`.
- **Build Completion Detection:** Runs static `halionbridge_build.lua` lists in chunks of up to 15 scripts per HALion process by default, relaunching HALion for each chunk so later chunks can continue after a failed chunk. Waits for HALion-written `.vstpreset` status marker presets in the build directory in order to know when each chunk is finished. Temporary progress marker presets are cleaned after logging; failed status markers may remain after failed builds for diagnostics.
Press Ctrl+C to stop a run. Conversion commands stop at converter checkpoints before writing more generated files. Normal headless HALion builds stop the active worker process and do not start later chunks. GUI and `--nokill` inspection runs remain cooperative so HALion can clean up normally.

See `HALION-LUA.md` for the Lua build script API used by the generic builder workflow.
35 changes: 31 additions & 4 deletions cli/Main.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "halionbridge/Bridge.h"
#include "halionbridge/BuildInfo.h"
#include "halionbridge/CrashDiagnostics.h"
#include "BuildWorker.h"
#include "BuildFile.h"
#include "Log.h"
#include "PathUtils.h"
Expand Down Expand Up @@ -85,7 +86,8 @@ void printHelp()
<< " --plugin <path> Override the HALion 7 VST3 path.\n"
<< "\n"
<< "Runtime options:\n"
<< " --timeout-seconds <n> Build completion timeout. Defaults to 0, which waits forever.\n"
<< " --timeout-seconds <n> Build completion timeout. Defaults to 3600 seconds.\n"
<< " --no-timeout Wait indefinitely. Equivalent to --timeout-seconds 0.\n"
<< " --build-chunk-size <n> Number of Lua build scripts per HALion run. Defaults to 15.\n"
<< " --fail-fast Stop after the first failed Lua build chunk.\n"
<< " --gui Use JUCE's GUI-capable VST3 host format and show HALion's editor.\n"
Expand Down Expand Up @@ -114,6 +116,11 @@ void printVersion()
<< "source: " << (buildInfo.isDirty ? "modified" : "clean") << "\n";
}

bool isInternalWorkerCommand(const juce::StringArray& args)
{
return args.size() > 0 && (args[0] == halionbridge::detail::kBuildWorkerArgument || args[0] == "--halionbridge-scan-plugin");
}

#if HALIONBRIDGE_ENABLE_CONVERTERS
void printConvertHelp()
{
Expand Down Expand Up @@ -257,14 +264,13 @@ int main(int argc, char* argv[])
args.emplace_back(argv[i]);
}

if ((juceArgs.isEmpty() || juceArgs[0] == "--help" || juceArgs[0] == "-h") &&
(juceArgs.size() == 0 || juceArgs[0] != "--halionbridge-scan-plugin"))
if ((juceArgs.isEmpty() || juceArgs[0] == "--help" || juceArgs[0] == "-h") && !isInternalWorkerCommand(juceArgs))
{
printHelp();
return 0;
}

if (juceArgs[0] == "--version" && (juceArgs.size() == 1 || juceArgs[0] != "--halionbridge-scan-plugin"))
if (juceArgs[0] == "--version" && !isInternalWorkerCommand(juceArgs))
{
printVersion();
return 0;
Expand Down Expand Up @@ -316,9 +322,30 @@ int main(int argc, char* argv[])
{
const auto exitCode = halionbridge::detail::runPluginScanWorker(juceArgs);
juce::Logger::setCurrentLogger(nullptr);
halionbridge::log::flush();
return exitCode;
}

if (juceArgs.size() > 0 && juceArgs[0] == halionbridge::detail::kBuildWorkerArgument)
{
auto options = halionbridge::detail::parseBuildWorkerArguments(args);
if (!options)
{
juce::Logger::setCurrentLogger(nullptr);
halionbridge::log::flush();
return halionbridge::detail::runResultToBuildWorkerExitCode(halionbridge::RunResult::invalidOptions);
}

const auto executableFile = juce::File::getSpecialLocation(juce::File::currentExecutableFile);
options->executableFile = halionbridge::detail::toStdPath(executableFile);

halionbridge::Bridge app;
const auto runResult = app.runDetailed(*options);
juce::Logger::setCurrentLogger(nullptr);
halionbridge::log::flush();
return halionbridge::detail::runResultToBuildWorkerExitCode(runResult);
}

auto options = halionbridge::Bridge::parseArguments(args);
if (!options)
{
Expand Down
Loading