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
2 changes: 1 addition & 1 deletion .github/workflows/cmake_full_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ jobs:
run: |
curl -LO "https://github.com/ZL-Audio/pluginval/releases/download/v1.0.4-temp/pluginval_${{ runner.os }}.zip"
7z x pluginval_${{ runner.os }}.zip
echo "PLUGINVAL_DISABLED=Plugin state restoration" >> $GITHUB_ENV
echo "PLUGINVAL_DISABLED=" >> $GITHUB_ENV # comma-separated tests to skip: none

- name: Setup Pluginval random seed
id: setup_seed
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cmake_heavy_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ jobs:
run: |
curl -LO "https://github.com/ZL-Audio/pluginval/releases/download/v1.0.4-temp/pluginval_${{ runner.os }}.zip"
7z x pluginval_${{ runner.os }}.zip
echo "PLUGINVAL_DISABLED=Plugin state restoration" >> $GITHUB_ENV
echo "PLUGINVAL_DISABLED=" >> $GITHUB_ENV # comma-separated tests to skip: none

- name: Setup Pluginval random seed
id: setup_seed
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/sanitizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,22 @@ jobs:
os: macos-latest
pluginval-binary: ./pluginval/Builds/pluginval_artefacts/RelWithDebugInfo/pluginval.app/Contents/MacOS/pluginval
cmake_extra_flags: -DWITH_ADDRESS_SANITIZER=ON -DWITH_THREAD_SANITIZER=OFF -DALTVANE_FORMATS="VST3;AU" -DALTVANE_SIMD_TARGET="NEON"
pluginval_disabled: Plugin state restoration
pluginval_disabled: ""
- name: macOS Thread Sanitizer
os: macos-latest
pluginval-binary: ./pluginval/Builds/pluginval_artefacts/RelWithDebugInfo/pluginval.app/Contents/MacOS/pluginval
cmake_extra_flags: -DWITH_ADDRESS_SANITIZER=OFF -DWITH_THREAD_SANITIZER=ON -DALTVANE_FORMATS="VST3;AU" -DALTVANE_SIMD_TARGET="NEON"
pluginval_disabled: Plugin state restoration
pluginval_disabled: ""
- name: Linux Address Sanitizer
os: ubuntu-latest
pluginval-binary: ./pluginval/Builds/pluginval_artefacts/RelWithDebugInfo/pluginval
cmake_extra_flags: -DCMAKE_C_COMPILER=clang-21 -DCMAKE_CXX_COMPILER=clang++-21 -DWITH_ADDRESS_SANITIZER=ON -DWITH_THREAD_SANITIZER=OFF -DALTVANE_FORMATS="VST3" -DALTVANE_SIMD_TARGET="AVX2"
pluginval_disabled: Plugin state restoration
pluginval_disabled: ""
- name: Linux Thread Sanitizer
os: ubuntu-latest
pluginval-binary: ./pluginval/Builds/pluginval_artefacts/RelWithDebugInfo/pluginval
cmake_extra_flags: -DCMAKE_C_COMPILER=clang-21 -DCMAKE_CXX_COMPILER=clang++-21 -DWITH_ADDRESS_SANITIZER=OFF -DWITH_THREAD_SANITIZER=ON -DALTVANE_FORMATS="VST3" -DALTVANE_SIMD_TARGET="AVX2"
pluginval_disabled: Plugin state restoration,Background thread state
pluginval_disabled: Background thread state

steps:
- name: Set up Clang (Linux)
Expand Down
58 changes: 58 additions & 0 deletions libs/ui/source/altvane_ui/session/state_schema.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (C) 2026 - Altvane
// This file is part of Altvane
//
// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation.
//
// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see <https://www.gnu.org/licenses/>.
#include "state_schema.hpp"

namespace altvane::ui::state_schema {
int read(const juce::XmlElement& state) {
return std::max(state.getIntAttribute(kAttribute, 0), 0);
}

void write(juce::XmlElement& state, const int version) {
state.setAttribute(kAttribute, version);
}

int upgrade(juce::XmlElement& state, const int current, const std::span<const Migration> migrations) {
const auto saved = read(state);
if (saved > current) {
return saved;
}
// version 0 is version 1's layout, so the migrations start from 1
auto version = std::max(saved, 1);
for (const auto& migration : migrations) {
jassert(migration.from >= 1 && migration.from < current);
if (migration.from == version) {
migration.apply(state);
++version;
}
}
// anything short of current is a missing migration
jassert(version == current);
return saved;
}

juce::ValueTree knownParameters(const juce::ValueTree& saved, const juce::AudioProcessorValueTreeState& parameters) {
juce::ValueTree result(saved.getType());
for (const auto& child : saved) {
if (child.hasType("PARAM") && parameters.getParameter(child.getProperty("id").toString()) != nullptr) {
result.appendChild(child.createCopy(), nullptr);
}
}
return result;
}

void settleSwitches(const juce::AudioProcessor& owner) {
for (auto* para : owner.getParameters()) {
if (const auto* toggle = dynamic_cast<juce::AudioParameterBool*>(para)) {
if (const auto settled = toggle->get() ? 1.f : 0.f; !juce::exactlyEqual(para->getValue(), settled)) {
para->setValueNotifyingHost(settled);
}
}
}
}
}
65 changes: 65 additions & 0 deletions libs/ui/source/altvane_ui/session/state_schema.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (C) 2026 - Altvane
// This file is part of Altvane
//
// Altvane is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License Version 3 as published by the Free Software Foundation.
//
// Altvane is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License along with Altvane. If not, see <https://www.gnu.org/licenses/>.
#pragma once

#include <span>
#include <juce_audio_processors/juce_audio_processors.h>

/**
* The schema version of a product's saved state: host sessions and preset files alike, since presets are processor
* states too. Each product writes an integer on the root element of the state it saves, its own kStateVersion, and
* reads it back when it loads one:
* - no version (a state saved before versions existed) is version 0, whose layout is version 1's: it loads unchanged
* - an older version is brought up to date by the product's migrations, one version at a time, then loads
* - a newer version (saved by a later build) loads what this build knows of it: each parameter it has, by ID (IDs
* never change meaning, see each product's "parameter IDs never change" test), and whatever else the product reads
* by name (the Leveler's session part). Everything else is dropped, so the state saves again as this build's
* version, which a later build migrates like any other older state, without anything newer in it labelled older.
* Parameters it lacks keep their current values, as they do for an older state
* A product raises kStateVersion only when a layout change needs older states rewritten, and adds the migration from
* the previous version with it.
*/
namespace altvane::ui::state_schema {
inline constexpr auto kAttribute = "schema_version";

/**
* rewrites a state saved at version `from` (1 or later) into version from + 1's layout, in place
*/
struct Migration {
int from;
void (*apply)(juce::XmlElement& state);
};

/**
* the version a state was saved at: 0 if it has none
*/
int read(const juce::XmlElement& state);

void write(juce::XmlElement& state, int version);

/**
* brings a state up to `current` with the migrations from its version on (in order of `from`), and returns the
* version it was saved at; a newer state is left as it is, for the product to keep what it knows of it
*/
int upgrade(juce::XmlElement& state, int current, std::span<const Migration> migrations);

/**
* a parameter tree from a newer state, cut down to the parameters `parameters` has: the same type, with only their
* PARAM children
*/
juce::ValueTree knownParameters(const juce::ValueTree& saved, const juce::AudioProcessorValueTreeState& parameters);

/**
* after replaceState(): each switch exactly on the value the state gave it. A switch keeps whatever a host wrote to
* it (0.4 is off), unlike every other parameter type, which keeps its plain value; and replaceState() leaves a
* parameter alone when its plain value is already the saved one. So a switch the host had set to 0.4 would still
* report 0.4 once a state saying "off" was restored, rather than the 0 it was saved at.
*/
void settleSwitches(const juce::AudioProcessor& owner);
}
5 changes: 5 additions & 0 deletions products/compressor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ First version of Altvane Compressor, based on the ZL Compressor engine (forked a
### Compatibility

- Same parameters and state format as ZL Compressor: presets and sessions load unchanged.
- Sessions and presets record the layout of the state they were saved in (`schema_version="1"` on the state's root),
so a later version can upgrade them. A state without it, saved before, loads as it always has; one saved by a
newer version loads every setting this version has and leaves the rest.
- Restoring a session or preset puts each switch exactly where it was saved. One a host had left between on and off
(0.4 is off) went on reporting that value to the host.
- Installs alongside ZL Compressor (different plug-in codes and bundle identifier).

The upstream changelog is kept in [docs/UPSTREAM_CHANGELOG.md](docs/UPSTREAM_CHANGELOG.md).
18 changes: 16 additions & 2 deletions products/compressor/source/PluginProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
// You should have received a copy of the GNU Affero General Public License along with ZLCompressor. If not, see <https://www.gnu.org/licenses/>.

#include "PluginProcessor.hpp"

#include <altvane_ui/session/state_schema.hpp>

#include "ui/editor.hpp"

namespace {
// Altvane: upgrades for states older than kStateVersion, in order (version 0, which ZL's releases and Altvane's
// before versions wrote, is version 1)
constexpr std::array<altvane::ui::state_schema::Migration, 0> kStateMigrations{};

juce::ValueTree copyWithType(const juce::ValueTree& source, const juce::Identifier& type) {
if (!source.isValid()) {
return {};
Expand Down Expand Up @@ -587,6 +594,7 @@ void PluginProcessor::getStateInformation(juce::MemoryBlock& dest_data) {
temp_tree.appendChild(parameters_.copyState(), nullptr);
temp_tree.appendChild(na_parameters_.copyState(), nullptr);
const std::unique_ptr<juce::XmlElement> xml(temp_tree.createXml());
altvane::ui::state_schema::write(*xml, kStateVersion);
copyXmlToBinary(*xml, dest_data);
}

Expand All @@ -598,6 +606,7 @@ void PluginProcessor::setStateInformation(const void* data, int size_in_bytes) {
return;
}

const auto version = altvane::ui::state_schema::upgrade(*xml_state, kStateVersion, kStateMigrations);
const auto temp_tree = juce::ValueTree::fromXml(*xml_state);
const auto parameter_state = getChildWithLegacyFallback(
temp_tree,
Expand All @@ -611,10 +620,15 @@ void PluginProcessor::setStateInformation(const void* data, int size_in_bytes) {
return;
}

parameters_.replaceState(copyWithType(parameter_state,
// Altvane: from a newer build, only the parameters this one has
const auto known = [version](const juce::ValueTree& saved, const juce::AudioProcessorValueTreeState& tree) {
return version > kStateVersion ? altvane::ui::state_schema::knownParameters(saved, tree) : saved;
};
parameters_.replaceState(copyWithType(known(parameter_state, parameters_),
juce::Identifier(zlstate::schema::kParameterState)));
na_parameters_.replaceState(copyWithType(non_automatable_state,
na_parameters_.replaceState(copyWithType(known(non_automatable_state, na_parameters_),
juce::Identifier(zlstate::schema::kNonAutomatableState)));
altvane::ui::state_schema::settleSwitches(*this);
}

juce::AudioProcessor* JUCE_CALLTYPE
Expand Down
6 changes: 6 additions & 0 deletions products/compressor/source/PluginProcessor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@

class PluginProcessor : public juce::AudioProcessor, private juce::AudioProcessorListener {
public:
/**
* the layout of the saved state, sessions and presets alike: raise it only with a migration in
* PluginProcessor.cpp (see altvane_ui/session/state_schema.hpp)
*/
static constexpr int kStateVersion = 1;

zlstate::DummyProcessor dummy_processor_;
juce::AudioProcessorValueTreeState parameters_;
juce::AudioProcessorValueTreeState na_parameters_;
Expand Down
4 changes: 4 additions & 0 deletions products/compressor/source/ui/factory_presets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
// You should have received a copy of the GNU Affero General Public License along with Altvane Compressor. If not, see <https://www.gnu.org/licenses/>.
#include "factory_presets.hpp"

#include <altvane_ui/session/state_schema.hpp>

#include "../PluginProcessor.hpp"
#include "../state/preset_json.hpp"

Expand Down Expand Up @@ -150,6 +152,8 @@ namespace altvane::compressor {
juce::ValueTree state(zlstate::schema::kProcessorState);
state.appendChild(parameters, nullptr);
state.appendChild(defaults(p.na_parameters_), nullptr);
// written in this build's layout, so it says so
state.setProperty(altvane::ui::state_schema::kAttribute, PluginProcessor::kStateVersion, nullptr);
result.emplace_back(juce::String("Factory/") + preset.name, zlstate::PresetJson::toText(state));
}
return result;
Expand Down
Loading
Loading