Files
serumalt/README.md
T
biggy 4b0db50271 refactor(source): move every implementation into its module directory
- 36 files move into the module whose AGENTS.md already claimed them:
  Engine/, Synth/, Modulation/, Params/, Plugin/, GUI/, EffectUnits/, Resources/
- Source/engine, modulation, params, plugin and synth become Engine,
  Modulation, Params, Plugin and Synth, matching the directories that already
  hold code
- ExactCompare.h stays at the Source/ root as the one shared header
- SERUMALT_INCLUDE_DIRS adds one include directory per module, so the 31
  parent-relative includes become plain names with no path prefix
- the module AGENTS.md files list owned files instead of describing a move
  that had not happened, and README.md and AGENTS.md describe the real tree

Verified: the harness passes CTest, the Linux VST3 and Standalone link, the
Windows cross-build passes its module checks, each configure exposes only its
own targets, no first-party warning line appears, and the generated Windows
manifest is unchanged.
2026-10-06 09:17:49 +02:00

261 lines
11 KiB
Markdown

# SerumAlt
A wavetable synthesiser experiment built with JUCE 7, CMake and C++17. It targets
VST3, AU and Standalone formats, with a workflow inspired by Serum and Vital.
This is a fun proof of concept, not a commercial release. Development favors
working builds and hands-on listening over elaborate test infrastructure.
![SerumAlt](Source/Resources/logo.svg)
## Features
- Two wavetable oscillators, each with 256 frames of 2048 samples, bilinear
morphing, seven warp effects plus bypass, and up to 16 unison voices.
- Ten factory wavetables, built before audio rendering and kept in memory.
- Sine/triangle sub oscillator and white/pink noise.
- Two filter slots with seven models, drive, key tracking and serial, parallel
or split routing.
- Four ADSR envelopes and four free-running or tempo-synced LFOs, including
editable step and freehand shapes.
- A modulation matrix and four assignable macros.
- Nine effect types in eight reorderable slots, with independent state per slot.
- A non-destructive RAVE boost for unison, spread, drive, Hyper and reverb.
- Sixteen factory presets and a scalable vector GUI with waveform, envelope,
LFO and schematic filter previews.
## Architecture
```
Source/
Engine/ Voice pool, MIDI timing, control snapshots, FX rack
Synth/ Wavetables, oscillators, filters, envelopes, LFOs, voice
Modulation/ Matrix, macros, RAVE
Params/ Parameter IDs, indexed groups, enums, mappings
Plugin/ AudioProcessor: APVTS, presets, state, RAVE/UI-scale glue
GUI/ Editor, controls and previews
EffectUnits/ FX rack and the nine effect units
Presets/ Factory preset definitions
Resources/ Theme, formatters and SVG assets
Tests/ Headless QA harness
ExactCompare.h Shared exact float comparison helpers
```
The engine splits rendering at MIDI event positions and into segments of at most
64 samples. Buffers and effect instances are prepared ahead of time. GUI edits
use a control-side copy; audio tries to capture that copy once per host block and
keeps its previous settings if an edit is still in progress.
### Behavior notes
- Loading a factory preset resets sound parameters before applying its settings,
while preserving UI scale.
- RAVE is automatable and saved with the project. It changes the rendered sound
without moving the underlying knobs; turning it off removes the boost.
- Modulation of Master and FX mixes uses global sources directly. Envelope,
velocity, note and random sources for these destinations use the newest active voice.
- Effect types can repeat across slots. Reordering swaps settings, not running
delay or reverb tails. Preconstructing effects uses more memory but avoids
allocating effect instances while audio is running.
- Filter graphs are schematic previews, not measurements of the nonlinear DSP.
- LFO shape data is stored as JSON text inside the project's XML state. Older
saves containing only the literal `[Array]` did not preserve the shape data
and fall back to the default pattern.
## Building
The scripts live in `scripts/`. Each configures and builds its own directory under
`build/`. Production builds disable the QA harness explicitly; the harness script
creates no plugin or standalone targets. JUCE 7.0.12 is vendored under
`third_party/JUCE` and is not downloaded by the scripts.
### Prerequisites
The build scripts require Bash, CMake 3.22+, Ninja, and C/C++17 compilers. The
harness script also uses CTest, included with CMake, when invoked with `--run`.
On Linux, install GCC or Clang, `pkg-config`, and the ALSA, FreeType, X11 and OpenGL
development libraries. For Debian/Ubuntu:
```bash
sudo apt update
sudo apt install build-essential cmake ninja-build pkg-config \
libasound2-dev libfreetype6-dev libx11-dev libxcomposite-dev \
libxcursor-dev libxext-dev libxinerama-dev libxrandr-dev libxrender-dev \
libgl1-mesa-dev
```
For Arch/CachyOS:
```bash
sudo pacman -S --needed base-devel cmake ninja pkgconf alsa-lib freetype2 \
libx11 libxcomposite libxcursor libxext libxinerama libxrandr libxrender libglvnd
```
JACK is disabled by JUCE's default `JUCE_JACK=0`. Curl and the web browser are
explicitly disabled in both project targets. Their development packages are not
required for these builds, even though JUCE probes for them during configuration.
See `third_party/JUCE/docs/Linux Dependencies.md` if enabling additional JUCE modules.
On macOS, install Apple's Command Line Tools with `xcode-select --install`, or
select an installed Xcode toolchain. Install CMake and Ninja, for example with
`brew install cmake ninja` if Homebrew is already installed. The macOS script checks
that `xcrun` can locate the macOS SDK. It must run on a Mac; JUCE does not provide
an Apple cross-compilation toolchain for Linux.
For Windows cross-compilation, also install MinGW-w64 on the Linux host:
```bash
sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 binutils-mingw-w64-x86-64
```
On Arch/CachyOS, use `sudo pacman -S --needed mingw-w64-gcc` instead. Keep the
native Linux tools and libraries installed: JUCE builds `juceaide` for the host
before compiling the Windows plugin. The checked-in `cmake/mingw64-toolchain.cmake`
selects the `x86_64-w64-mingw32-*` tools and isolates target library searches.
### Production builds
| Host | Command | Build directory | Formats |
| --- | --- | --- | --- |
| Linux | `./scripts/build_linux.sh` | `build/linux/` | VST3, Standalone |
| macOS | `./scripts/build_macos.sh` | `build/macos/` | VST3, AU, Standalone |
| Linux targeting Windows x64 | `./scripts/build_windows.sh` | `build/windows/` | VST3 only |
Default Release artifacts:
- Linux: `build/linux/SerumAlt_artefacts/Release/VST3/SerumAlt.vst3` and
`build/linux/SerumAlt_artefacts/Release/Standalone/SerumAlt`.
- macOS: `build/macos/SerumAlt_artefacts/Release/VST3/SerumAlt.vst3`,
`build/macos/SerumAlt_artefacts/Release/AU/SerumAlt.component`, and
`build/macos/SerumAlt_artefacts/Release/Standalone/SerumAlt.app`.
- Windows: `build/windows/SerumAlt_artefacts/Release/VST3/SerumAlt.vst3`, also
copied to `SerumAlt_Windows/SerumAlt.vst3`.
The Windows script reuses the toolchain file without rewriting it. A cross-build
cannot run JUCE's manifest helper, because that helper is a host tool, so the
script copies the `moduleinfo.json` that CMake writes from
`cmake/moduleinfo.json.in`. The plugin name, version, vendor, class IDs and VST3
SDK version come from variables in `cmake/Plugin.cmake`, so a version bump reaches
the manifest on its own. MinGW links its C++ and threading runtimes statically via
`-static-libgcc -static-libstdc++ -static`. Before staging, the script verifies the
linked module is x86-64, imports none of those MinGW runtimes, and exports the VST3
entry points, so a module that a stock Windows host could not load fails the build
instead of being staged silently.
macOS builds target the host architecture by default. To request a universal binary:
```bash
CMAKE_OSX_ARCHITECTURES='arm64;x86_64' ./scripts/build_macos.sh
```
`CMAKE_OSX_DEPLOYMENT_TARGET` can also be passed to that script. Architecture and
SDK settings persist in CMake's cache. Use a separate build directory when changing
toolchains. These scripts build local artifacts, not signed or notarized releases,
and do not install plugins into a DAW's plugin directories.
### Independent QA harness
On Linux or macOS, build the console harness without production targets:
```bash
./scripts/build_harness.sh
```
Build and run the checks:
```bash
./scripts/build_harness.sh --run
```
Or rerun an already-built Release harness:
```bash
ctest --test-dir build/harness --build-config Release --output-on-failure --no-tests=error
```
The executable is `build/harness/SerumAltTest_artefacts/Release/SerumAltTest`.
It renders the initial state and every factory preset, checks for non-finite
samples and aggregate non-silence, and exercises the RAVE toggle. A failed check
returns a nonzero exit status. This is a processor integration smoke test, not a
complete DSP, GUI, or real-time safety test suite.
The harness does not open an editor or audio device, but it still compiles the real
processor, editor and JUCE GUI dependencies. Its separate build directory and
CMake options isolate its outputs from production bundles; it is not a DSP-only
library. It is built and run natively, not as a Windows cross-compiled executable.
### Build options and layout
- `BUILD_TYPE` defaults to `Release`; set it to `Debug` for a debug build.
- `BUILD_DIR` overrides a script's build directory. Relative paths are resolved
from the repository root, even when the script is invoked elsewhere.
- `CMAKE_BUILD_PARALLEL_LEVEL` defaults to `2` to limit memory use, including JUCE's
configure-time helper build. Increase it if memory permits.
- Windows-only `OUTPUT_DIR` overrides the staging directory.
For example:
```bash
BUILD_TYPE=Debug BUILD_DIR=build/harness_debug ./scripts/build_harness.sh --run
CMAKE_BUILD_PARALLEL_LEVEL=4 ./scripts/build_linux.sh
```
`CMakeLists.txt` owns the shared source list and target configuration.
`cmake/Plugin.cmake` owns production formats and platform-specific linking.
`cmake/Harness.cmake` owns the test executable and its CTest registration.
For direct CMake use, `SERUMALT_BUILD_PLUGIN` defaults to `ON` and
`SERUMALT_BUILD_TESTS` defaults to `OFF`. A harness-only configure is:
```bash
cmake -S . -B build/harness -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DSERUMALT_BUILD_PLUGIN=OFF -DSERUMALT_BUILD_TESTS=ON
cmake --build build/harness --target SerumAltTest
```
An existing CMake cache retains its old option values; changing the defaults does
not turn tests off in an old `build/` directory. Use the scripts to set the flags
explicitly. An alternate JUCE checkout can be selected with `-DJUCE_ROOT=/path/to/JUCE`
when configuring directly with CMake.
### Fresh builds
The scripts reuse their build directories without deleting files, resetting Git,
or installing packages. `build/` and `SerumAlt_Windows/` are git-ignored. Leave
existing builds intact and select an unused build directory for a fresh configure:
```bash
BUILD_DIR=build/linux_fresh ./scripts/build_linux.sh
```
Windows staging updates generated files without deleting extra files already in
the destination. For a clean distribution bundle, choose unused build and output
paths together:
```bash
BUILD_DIR=build/windows_fresh OUTPUT_DIR=build/windows_fresh/staged ./scripts/build_windows.sh
```
### Cleaning
`scripts/clean.sh` removes generated output. A plain run removes the build trees
under `build/`; `--staging` adds `SerumAlt_Windows/`, `--legacy` adds the
`build_*` and `cmake-build-*` trees that earlier versions of the scripts left in
the repository root, and `--all` is both. `--dry-run` prints every path with its
size and removes nothing.
```bash
./scripts/clean.sh --dry-run --all
./scripts/clean.sh --all
```
The script never removes a tracked file, anything under `Source/`, `third_party/`
or `.git`, or a path outside the repository. It refuses to run when `BUILD_DIR` or
`OUTPUT_DIR` names a directory outside the repository, because such a directory
belongs to another checkout.
## License
JUCE is licensed separately (see `third_party/JUCE/LICENSE.md`). This project is
provided for evaluation/development purposes.