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
154 changes: 148 additions & 6 deletions docs/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,42 @@ tab structure used by the application (`project_info`, `configuration`, `impleme
`user_experience`, and `development_workflow`). Each section contains simple elements with
string, integer, or boolean values.

### Schema Versioning

The `<preset>` element carries a `schema_version` attribute. This allows the application to
detect and handle format changes across releases. The current schema version is **1.0**.

When loading a preset file the application reads this attribute and defaults to `"1.0"` if
the attribute is absent (backwards-compatible with older preset files).

### Formal XSD Schema

A machine-readable W3C XML Schema Definition is bundled at:

```text
src/resources/presets/preset_schema.xsd
```

The XSD can be used by XML-aware IDEs (VS Code, IntelliJ, etc.) and external validators to
check preset files before loading them into the application. Python code can validate files
programmatically using `PresetXSDValidator` from `core.preset_validator`:

```python
from core.preset_validator import PresetXSDValidator

validator = PresetXSDValidator()
ok, errors = validator.validate_file("path/to/my_preset.xml")
if not ok:
print("\n".join(errors))
```

`validate_string(xml_content: str)` is also available for validating in-memory XML.

### Full Preset Example

```xml
<?xml version="1.0" encoding="utf-8"?>
<preset name="PresetName">
<preset name="PresetName" schema_version="1.0">
<meta>
<description>Optional human-friendly description</description>
</meta>
Expand All @@ -29,6 +62,16 @@ string, integer, or boolean values.
<au>true</au>
<auv3>false</auv3>
<clap>true</clap>
<!-- Audio Unit component descriptor -->
<au_component_type>aufx</au_component_type>
<au_component_subtype>plug</au_component_subtype>
<au_component_manufacturer>DkFX</au_component_manufacturer>
<au_version>1.0.0</au_version>
<!-- CLAP identifiers (comma-separated lists) -->
<clap_extensions>note-ports,state</clap_extensions>
<clap_features>audio-effect</clap_features>
<!-- AUv3 target platform -->
<auv3_platform>iOS</auv3_platform>
<gui_width>1100</gui_width>
<gui_height>700</gui_height>
<resizable>true</resizable>
Expand Down Expand Up @@ -66,15 +109,114 @@ string, integer, or boolean values.
</preset>
```

### Schema Field Reference

#### `<preset>` attributes

| Attribute | Required | Description |
|------------------|----------|------------------------------------------------------|
| `name` | No | Human-readable preset display name. |
| `schema_version` | No | Schema format version; defaults to `"1.0"`. |

#### `<meta>` section (optional)

| Element | Type | Description |
|---------------|--------|-----------------------------------------|
| `description` | string | Free-text description of the preset. |

#### `<project_info>` section

| Element | Type | Required* | Description |
|----------------------|--------|-----------|---------------------------------------------------|
| `template_name` | string | | Display name of the JUCE project template. |
| `template_url` | string | | Git URL of the project template repository. |
| `project_name` | string | yes | CamelCase project identifier (no spaces). |
| `product_name` | string | yes | Human-readable plugin display name. |
| `version` | string | | Semantic version string, e.g. `1.0.0`. |
| `company_name` | string | yes | Developer or studio name. |
| `bundle_id` | string | yes | Reverse-domain ID, e.g. `com.company.plugin`. |
| `manufacturer_code` | string | yes | 4-character JUCE manufacturer code. |
| `plugin_code` | string | | 4-character JUCE plugin code (auto-generated if empty). |
| `output_directory` | string | yes | Filesystem path for the generated project. |

#### `<configuration>` section

| Element | Type | Default | Description |
|----------------------------|---------|-------------------|----------------------------------------------------|
| `standalone` | boolean | `false` | Build a standalone application target. |
| `vst3` | boolean | `true` | Build a VST3 target. |
| `au` | boolean | `true` | Build an Audio Unit (AU) target. |
| `auv3` | boolean | `false` | Build an AUv3 target. |
| `clap` | boolean | `true` | Build a CLAP target. |
| `au_component_type` | string | `aufx` | Four-char AU component type code. |
| `au_component_subtype` | string | `plug` | Four-char AU component subtype code. |
| `au_component_manufacturer`| string | `Ddsp` | Four-char AU manufacturer code. |
| `au_version` | string | `1.0.0` | AU bundle version string. |
| `clap_extensions` | string | `note-ports,state`| Comma-separated CLAP extension identifiers. |
| `clap_features` | string | `audio-effect` | Comma-separated CLAP feature identifiers. |
| `auv3_platform` | string | `iOS` | Target platform for AUv3 (`iOS` or `macOS`). |
| `gui_width` | integer | `800` | Default plugin window width in pixels (> 0). |
| `gui_height` | integer | `600` | Default plugin window height in pixels (> 0). |
| `resizable` | boolean | `false` | Allow the plugin window to be resized. |
| `background_image` | string | *(empty)* | Path to an optional background image asset. |
| `code_signing` | boolean | `false` | Enable macOS/iOS code signing. |
| `installer` | boolean | `false` | Generate an installer package. |
| `default_bypass` | boolean | `false` | Start with the plugin bypassed. |
| `input_gain` | boolean | `false` | Include an input gain stage. |
| `output_gain` | boolean | `false` | Include an output gain stage. |

#### `<implementations>` section

| Element | Type | Default | Description |
|------------------------|---------|---------|---------------------------------------------------------|
| `moonbase_licensing` | boolean | `false` | Integrate Moonbase licensing module. |
| `melatonin_inspector` | boolean | `false` | Include Melatonin component inspector. |
| `custom_gui_framework` | boolean | `false` | Use a custom GUI framework instead of JUCE defaults. |
| `logging_framework` | boolean | `false` | Add structured logging support. |
| `clap_builds` | boolean | `false` | Configure CLAP-specific build scripts. |
| `preset_management` | boolean | `false` | Enable built-in preset management. |
| `preset_format` | string | *(empty)*| Preset serialisation format: `XML`, `Binary`, or empty. |
| `ab_comparison` | boolean | `false` | Include A/B comparison functionality. |
| `state_management` | boolean | `false` | Enable explicit plugin state management. |
| `gpu_audio` | boolean | `false` | Enable GPU Audio processing integration. |

#### `<user_experience>` section

| Element | Type | Default | Description |
|--------------------|---------|---------|--------------------------------------------|
| `wizard` | boolean | `false` | Show a setup wizard on first launch. |
| `preview` | boolean | `false` | Enable in-app audio preview functionality. |
| `preset_management`| boolean | `false` | Show preset management UI controls. |

#### `<development_workflow>` section

| Element | Type | Default | Description |
|--------------------|---------|---------|-------------------------------------------------|
| `vcs` | boolean | `false` | Initialise a Git repository. |
| `testing` | boolean | `false` | Add a unit-test scaffold. |
| `code_quality` | boolean | `false` | Configure linting and formatting tools. |
| `validation_tools` | boolean | `false` | Include static analysis and sanitiser builds. |
| `scaffolding` | boolean | `false` | Generate project scaffolding scripts. |

*Required fields are enforced at the Python semantic-validation level (not by XSD structure
alone), because the XSD marks all elements as optional to allow partial presets to be loaded
with defaults filled in by `ConfigManager`.

### Validation Rules

- All five sections must be present.
- All five sections (`project_info`, `configuration`, `implementations`, `user_experience`,
`development_workflow`) must be present.
- Required fields (project names, company identifiers, and `output_directory`) must not be
empty.
empty strings.
- Types:
- Booleans: `true`/`false` (case-insensitive; `1/0/yes/on` also accepted)
- Integers: whole numbers (GUI sizes)
- Strings: everything else
- **Booleans**: `true`/`false` (case-insensitive; `1`/`0`/`yes`/`no`/`on`/`off` also
accepted when loading).
Comment on lines +212 to +213

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The boolean rules here say “case-insensitive”, but the XSD’s BooleanField ultimately relies on xs:boolean + lowercase enumerations (yes/no/on/off), which are case-sensitive. If XSD validation is intended to run (as described above), either update the docs to clarify that XSD validation requires lowercase boolean tokens, or expand the schema to accept the uppercase/mixed-case variants you want to support.

Suggested change
- **Booleans**: `true`/`false` (case-insensitive; `1`/`0`/`yes`/`no`/`on`/`off` also
accepted when loading).
- **Booleans**: XSD validation accepts canonical boolean tokens: `true`/`false`,
`1`/`0`, and lowercase `yes`/`no`/`on`/`off` where defined by the schema. Uppercase or
mixed-case variants are not guaranteed to pass XSD validation when loading preset XML.

Copilot uses AI. Check for mistakes.
- **Integers**: whole positive numbers (GUI dimensions).
- **Strings**: everything else.
- Two-layer validation is applied on every load and save:
1. **XSD structural validation** via `PresetXSDValidator` (element names, nesting, types).
2. **Semantic validation** via `ConfigManager.validate_config` (required fields, value
constraints).
Comment on lines +216 to +219

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs claim “Two-layer validation is applied on every load and save” via PresetXSDValidator, but the current code path (ConfigManager.load_config / _load_structured_preset / _save_structured_config) does not invoke PresetXSDValidator anywhere (and ConfigManager.validate_preset_file still only does semantic validation). Either wire XSD validation into the load/save/validate flow as described, or adjust this section so it doesn’t promise behavior the application doesn’t perform.

Copilot uses AI. Check for mistakes.

### Bundled Example Presets

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies = [
"certifi==2025.1.31",
"charset-normalizer==3.4.1",
"idna==3.10",
"lxml>=6.0.2",
"pyside6==6.8.3",
"pyside6-addons==6.8.3",
"pyside6-essentials==6.8.3",
Expand Down
3 changes: 2 additions & 1 deletion src/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"""

from .base_tab import BaseTab, TabSignals
from .preset_validator import PresetXSDValidator

__all__ = ["BaseTab", "TabSignals"]
__all__ = ["BaseTab", "PresetXSDValidator", "TabSignals"]
21 changes: 20 additions & 1 deletion src/core/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,15 @@ class ConfigManager:
},
}

SCHEMA_VERSION: ClassVar[str] = "1.0"

META_FIELDS: ClassVar[dict[str, dict[str, Any]]] = {
"name": {"type": str, "default": ""},
"description": {"type": str, "default": ""},
# schema_version is stored as an attribute on the root <preset> element,
# not as a child of <meta>. It is included here so that _apply_defaults
# preserves it in the meta dict when round-tripping config through save/load.
"schema_version": {"type": str, "default": "1.0"},

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

META_FIELDS["schema_version"] hard-codes the default as "1.0" even though SCHEMA_VERSION was introduced just above. To keep schema versioning as a true single source of truth, set this default from SCHEMA_VERSION (and similarly avoid other duplicated literals) so bumps don’t require multiple edits.

Suggested change
"schema_version": {"type": str, "default": "1.0"},
"schema_version": {"type": str, "default": SCHEMA_VERSION},

Copilot uses AI. Check for mistakes.
}

def __init__(self, preset_dir: Path | None = None):
Expand Down Expand Up @@ -372,11 +378,23 @@ def _coerce_value(self, value: str | None, expected_type: type, default: Any) ->
return value

def _load_structured_preset(self, root: ET.Element, file_path: Path) -> dict:
config: dict[str, Any] = {"meta": {"name": root.attrib.get("name", file_path.stem)}}
config: dict[str, Any] = {
"meta": {
"name": root.attrib.get("name", file_path.stem),
# schema_version is stored as a root-element attribute, not as a
# child element of <meta>. Read it here before the META_FIELDS
# loop so the loop's "elif key not in config['meta']" guard will
# not overwrite it.
"schema_version": root.attrib.get("schema_version", self.SCHEMA_VERSION),
}
Comment on lines +381 to +389

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_load_structured_preset() now reads schema_version but never checks whether the value is supported/recognized. Since versioning is being introduced specifically to detect breaking format changes, consider rejecting (or explicitly warning/migrating) presets with an unknown schema_version rather than silently proceeding (which could load an incompatible preset incorrectly).

Copilot uses AI. Check for mistakes.
}

meta_element = root.find("meta")
if meta_element is not None:
for key, meta_def in self.META_FIELDS.items():
if key == "schema_version":
# Already populated from the root attribute above.
continue
elem = meta_element.find(key)
if elem is not None and elem.text is not None:
config["meta"][key] = elem.text
Expand Down Expand Up @@ -432,6 +450,7 @@ def _save_structured_config(self, config: Mapping[str, Any], file_path: Path) ->
meta = config_with_defaults.get("meta", {})
if meta.get("name"):
root.set("name", str(meta.get("name")))
root.set("schema_version", str(meta.get("schema_version", self.SCHEMA_VERSION)))
if meta.get("description"):
meta_elem = ET.SubElement(root, "meta")
desc_elem = ET.SubElement(meta_elem, "description")
Expand Down
105 changes: 105 additions & 0 deletions src/core/preset_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""XSD-based validation utilities for preset XML files.

This module provides :class:`PresetXSDValidator`, which validates preset XML
files and strings against the formal ``preset_schema.xsd`` W3C schema bundled
with the application. It requires the ``lxml`` library.

Typical usage::

from core.preset_validator import PresetXSDValidator

validator = PresetXSDValidator()

ok, errors = validator.validate_file("path/to/preset.xml")
if not ok:
print("\\n".join(errors))

ok, errors = validator.validate_string(xml_content)
"""

from __future__ import annotations

from pathlib import Path
from typing import ClassVar

from lxml import etree


class PresetXSDValidator:
"""Validates preset XML files against the bundled XSD schema.

The compiled :class:`lxml.etree.XMLSchema` object is cached on the class so
that the schema file is parsed only once per interpreter session.

Attributes:
SCHEMA_VERSION: The preset schema version this validator understands.
XSD_PATH: Absolute path to the bundled ``preset_schema.xsd`` file.
"""

SCHEMA_VERSION: ClassVar[str] = "1.0"
XSD_PATH: ClassVar[Path] = (
Path(__file__).resolve().parents[1] / "resources" / "presets" / "preset_schema.xsd"
)
Comment on lines +39 to +42

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PresetXSDValidator.SCHEMA_VERSION duplicates ConfigManager.SCHEMA_VERSION (added in this PR), which undermines the stated “single source of truth” for schema versioning and risks the constants drifting apart. Consider importing/deriving the validator’s version from ConfigManager.SCHEMA_VERSION (or moving the constant to a shared module) so only one value needs updating.

Copilot uses AI. Check for mistakes.

_schema: ClassVar[etree.XMLSchema | None] = None

# ------------------------------------------------------------------
# Construction / schema loading
# ------------------------------------------------------------------

@classmethod
def _get_schema(cls) -> etree.XMLSchema:
"""Return the compiled XMLSchema, loading it on first access."""
if cls._schema is None:
schema_doc = etree.parse(str(cls.XSD_PATH))
cls._schema = etree.XMLSchema(schema_doc)
return cls._schema
Comment on lines +50 to +56

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_schema() can raise (e.g., missing/unreadable XSD file, invalid XSD, XMLSchemaParseError), which will escape from validate_file() / validate_string() despite those methods documenting a (is_valid, errors) return. Consider catching schema load/compile exceptions and returning (False, [...]) with a clear error message so callers don’t need to wrap every call in try/except.

Copilot uses AI. Check for mistakes.

# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------

def validate_file(self, file_path: Path | str) -> tuple[bool, list[str]]:
"""Validate a preset XML file against the XSD schema.

Args:
file_path: Path to the XML preset file to validate.

Returns:
A ``(is_valid, errors)`` tuple. *errors* is an empty list when
*is_valid* is ``True``.
"""
try:
doc = etree.parse(str(file_path))
except etree.XMLSyntaxError as exc:
return False, [f"XML syntax error: {exc}"]
except OSError as exc:
return False, [f"Cannot read file: {exc}"]
return self._validate_doc(doc)

def validate_string(self, xml_content: str) -> tuple[bool, list[str]]:
"""Validate an XML string against the XSD schema.

Args:
xml_content: XML text to validate.

Returns:
A ``(is_valid, errors)`` tuple. *errors* is an empty list when
*is_valid* is ``True``.
"""
try:
doc = etree.fromstring(xml_content.encode())
except etree.XMLSyntaxError as exc:
return False, [f"XML syntax error: {exc}"]
return self._validate_doc(etree.ElementTree(doc))

# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------

def _validate_doc(self, doc: etree._ElementTree) -> tuple[bool, list[str]]: # type: ignore[name-defined]
"""Run the schema against a parsed lxml document tree."""
schema = self._get_schema()
is_valid = schema.validate(doc)
errors = [str(err) for err in schema.error_log]
return is_valid, errors
9 changes: 8 additions & 1 deletion src/resources/presets/Instrument_Preset.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<preset name="Instrument">
<preset name="Instrument" schema_version="1.0">
<meta>
<description>Instrument-focused configuration with CLAP/AUv3 targets and GPU-friendly options.</description>
</meta>
Expand All @@ -21,6 +21,13 @@
<au>true</au>
<auv3>true</auv3>
<clap>true</clap>
<au_component_type>aumu</au_component_type>
<au_component_subtype>inst</au_component_subtype>
<au_component_manufacturer>DkIN</au_component_manufacturer>
<au_version>1.1.0</au_version>
<clap_extensions>note-ports,state</clap_extensions>
<clap_features>instrument,synthesizer</clap_features>
<auv3_platform>iOS</auv3_platform>
<gui_width>1280</gui_width>
<gui_height>720</gui_height>
<resizable>true</resizable>
Expand Down
9 changes: 8 additions & 1 deletion src/resources/presets/MinimalPlugin_Preset.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<preset name="MinimalPlugin">
<preset name="MinimalPlugin" schema_version="1.0">
<meta>
<description>The lightest configuration: VST3-only with no optional modules, ideal for quick scaffolding.</description>
</meta>
Expand All @@ -21,6 +21,13 @@
<au>false</au>
<auv3>false</auv3>
<clap>false</clap>
<au_component_type>aufx</au_component_type>
<au_component_subtype>plug</au_component_subtype>
<au_component_manufacturer>Ddsp</au_component_manufacturer>
<au_version>0.9.0</au_version>
<clap_extensions>note-ports,state</clap_extensions>
<clap_features>audio-effect</clap_features>
<auv3_platform>iOS</auv3_platform>
<gui_width>800</gui_width>
<gui_height>500</gui_height>
<resizable>false</resizable>
Expand Down
Loading
Loading