From c45e4d762e1db4646b06dfee7df02c3be8cf8e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Michelle?= Date: Thu, 19 Mar 2026 08:01:17 +0100 Subject: [PATCH] updated plan --- package-lock.json | 192 +++++++++++++++++------------------ plans/custom-mapping.md | 220 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+), 96 deletions(-) create mode 100644 plans/custom-mapping.md diff --git a/package-lock.json b/package-lock.json index 6fc0122f..fe60bcf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2990,6 +2990,90 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/@turbo/darwin-64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/darwin-64/-/darwin-64-2.8.18.tgz", + "integrity": "sha512-3QNJ8VR2y1+AZX1HRB2uAvBWHWYpyWMYuDPAPHwhmpC74YweQmPh41dQZ9fkNcgT1eZNgYKf6Q3Hq7i0XK3Ybg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/darwin-arm64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/darwin-arm64/-/darwin-arm64-2.8.18.tgz", + "integrity": "sha512-kiFEHQ+l64yzRtFcBSdHG9s/EF3aEzzmkuR8e1ygv7ol1YnWwz5EQEbZz6Ajt1sNYYRlo0GXw/d1YanVS9KWPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@turbo/linux-64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.8.18.tgz", + "integrity": "sha512-GlMHBGfob6pOQv7t6imx11O0WJapVVNcJxw+HkliIr1ez8vr8SskM7njqOtqKBTaooCmGA71BFInNRZNYO9qlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/linux-arm64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/linux-arm64/-/linux-arm64-2.8.18.tgz", + "integrity": "sha512-ogWEZ8K/NznjnJU1idHYOA62Y7xYyMnt+kKTZzRNJ9Ye2japtjDkixC31yHaA9QJayb+yS5V8gNDjVfh2yGrIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@turbo/windows-64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/windows-64/-/windows-64-2.8.18.tgz", + "integrity": "sha512-29g6qryc18ls7BKUmVsjjHAbx2eDgab5CUSPhbCKZYCBfzaW+i79wCzyVoC0NJ8IRNwbLch2QEzgGEoRC6uGOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@turbo/windows-arm64": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/@turbo/windows-arm64/-/windows-arm64-2.8.18.tgz", + "integrity": "sha512-hLsgNDzDDO+Tq2YJ9uvmB2PFTYCgeNj6cyAOOBZP9rsOx0kxNBo4/OvDIgYl/cfo7wsoRBxNQ9giYR+rZMcAjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", @@ -8948,9 +9032,9 @@ "license": "ISC" }, "node_modules/nan": { - "version": "2.26.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.1.tgz", - "integrity": "sha512-vodKprLlmaKmraa9E/TxHQwpH4eKYTJbLdeQE49pb9GOmrLs68zESjJu0LQOz1W6JwJmftOWD5Ls4dpd/elQtQ==", + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", "dev": true, "license": "MIT", "optional": true @@ -11791,107 +11875,23 @@ } }, "node_modules/turbo": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.17.tgz", - "integrity": "sha512-YwPsNSqU2f/RXU/+Kcb7cPkPZARxom4+me7LKEdN5jsvy2tpfze3zDZ4EiGrJnvOm9Avu9rK0aaYsP7qZ3iz7A==", + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.8.18.tgz", + "integrity": "sha512-ZO5aYR8wXIcZ+KRL8W7RFWbqIRDyYHtesF/g9zPMndmq+KkX34qrZgG9M+YswJ6BGX4jlYC1FlJjleKENagJdw==", "dev": true, "license": "MIT", "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "2.8.17", - "turbo-darwin-arm64": "2.8.17", - "turbo-linux-64": "2.8.17", - "turbo-linux-arm64": "2.8.17", - "turbo-windows-64": "2.8.17", - "turbo-windows-arm64": "2.8.17" + "@turbo/darwin-64": "2.8.18", + "@turbo/darwin-arm64": "2.8.18", + "@turbo/linux-64": "2.8.18", + "@turbo/linux-arm64": "2.8.18", + "@turbo/windows-64": "2.8.18", + "@turbo/windows-arm64": "2.8.18" } }, - "node_modules/turbo-darwin-64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-2.8.17.tgz", - "integrity": "sha512-ZFkv2hv7zHpAPEXBF6ouRRXshllOavYc+jjcrYyVHvxVTTwJWsBZwJ/gpPzmOKGvkSjsEyDO5V6aqqtZzwVF+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-darwin-arm64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-2.8.17.tgz", - "integrity": "sha512-5DXqhQUt24ycEryXDfMNKEkW5TBHs+QmU23a2qxXwwFDaJsWcPo2obEhBxxdEPOv7qmotjad+09RGeWCcJ9JDw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/turbo-linux-64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-2.8.17.tgz", - "integrity": "sha512-KLUbz6w7F73D/Ihh51hVagrKR0/CTsPEbRkvXLXvoND014XJ4BCrQUqSxlQ4/hu+nqp1v5WlM85/h3ldeyujuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-linux-arm64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-2.8.17.tgz", - "integrity": "sha512-pJK67XcNJH40lTAjFu7s/rUlobgVXyB3A3lDoq+/JccB3hf+SysmkpR4Itlc93s8LEaFAI4mamhFuTV17Z6wOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo-windows-64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.8.17.tgz", - "integrity": "sha512-EijeQ6zszDMmGZLP2vT2RXTs/GVi9rM0zv2/G4rNu2SSRSGFapgZdxgW4b5zUYLVaSkzmkpWlGfPfj76SW9yUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/turbo-windows-arm64": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-2.8.17.tgz", - "integrity": "sha512-crpfeMPkfECd4V1PQ/hMoiyVcOy04+bWedu/if89S15WhOalHZ2BYUi6DOJhZrszY+mTT99OwpOsj4wNfb/GHQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/plans/custom-mapping.md b/plans/custom-mapping.md new file mode 100644 index 00000000..b52d146e --- /dev/null +++ b/plans/custom-mapping.md @@ -0,0 +1,220 @@ +# Custom Parameter Mapping for Werkstatt & Spielwerk + +## Goal + +Enable `@param` declarations to specify value mapping (linear, exp, int, bool), range, default in mapped space, and optional unit. Parameters are displayed with their mapped values and units on the knobs, and `paramChanged` delivers the mapped value to user code. + +--- + +## @param Format + +``` +// @param [default] [min max type [unit]] +``` + +### Parsing Rules + +| Format | Interpretation | +|---|---| +| `// @param gain` | unipolar 0–1, default 0 | +| `// @param gain 0.5` | unipolar 0–1, default 0.5 | +| `// @param bypass true` | bool, default 1 (true) | +| `// @param bypass false` | bool, default 0 (false) | +| `// @param bypass bool` | bool, default 0 (false) | +| `// @param bypass true bool` | bool, default 1 (true), redundant type allowed | +| `// @param cutoff 1000 20 20000 exp` | exponential 20–20000, default 1000 | +| `// @param cutoff 1000 20 20000 exp Hz` | same with unit "Hz" | +| `// @param mode 0 0 3 int` | integer 0–3, default 0 | +| `// @param time 500 1 2000 linear ms` | linear 1–2000, default 500, unit "ms" | + +Detection logic: +1. If second token is `true` or `false` → **bool** (optional trailing `bool` keyword ignored) +2. If second token is `bool` → **bool**, default false +3. If exactly 1 token → **unipolar**, default 0 +4. If exactly 2 tokens (second is numeric) → **unipolar**, default = token +5. If 5+ tokens → **mapped**: `name default min max type [unit]` +6. Anything else → **error** + +### Supported Mapping Types + +| Type | ValueMapping | StringMapping | paramChanged receives | +|---|---|---|---| +| `unipolar` (default) | `ValueMapping.unipolar()` | `StringMapping.percent()` | `number` (0–1) | +| `linear` | `ValueMapping.linear(min, max)` | `StringMapping.numeric({unit})` | `number` (min–max) | +| `exp` | `ValueMapping.exponential(min, max)` | `StringMapping.numeric({unit})` | `number` (min–max) | +| `int` | `ValueMapping.linearInteger(min, max)` | `StringMapping.numeric({unit, fractionDigits: 0})` | `number` (integer) | +| `bool` | `ValueMapping.linearInteger(0, 1)` | `StringMapping.values("", [0, 1], ["Off", "On"])` | `number` (0 or 1) | + +Bool uses `linearInteger(0, 1)` so the type stays `number` everywhere — no signature change to `paramChanged`. + +--- + +## Storage + +### How openDAW Stores Parameter Values + +openDAW stores **mapped values** in typed fields, not unitValues: +- `Float32Field` stores the actual float value (e.g., 1000.0 Hz) +- `Int32Field` stores the actual integer (e.g., 3) +- `BooleanField` stores true/false + +The `ValueMapping` converts between the stored value (type T) and unitValue (0–1) for knob position, automation lanes, and modulation. The field type `T` and the ValueMapping type `T` must match — a type mismatch crashes (e.g., `ValueMapping.bool` on `Float32Field` tries to store `true` in a number field). + +### WerkstattParameterBox — Forge Schema Change Required + +The `value` and `defaultValue` fields must change from `"unipolar"` constraint to `"any"`: + +```typescript +// Before: float32 unipolar — clamps to [0, 1], cannot store mapped values +4: {type: "float32", name: "value", constraints: "unipolar", unit: "%", pointerRules: ParameterPointerRules}, +5: {type: "float32", name: "defaultValue", constraints: "unipolar", unit: "%"} + +// After: float32 any — stores the mapped value directly +4: {type: "float32", name: "value", constraints: "any", unit: "", pointerRules: ParameterPointerRules}, +5: {type: "float32", name: "defaultValue", constraints: "any", unit: ""} +``` + +This is required because with `ValueMapping.linear(20, 20000)`, calling `setUnitValue(0.5)` stores `10010.0` in the field — the unipolar constraint would clamp this to `1.0`. + +**Backward compatible**: existing projects have values in 0–1 range, which are valid `"any"` values. With `ValueMapping.unipolar()` (identity mapping), behavior is unchanged. + +### Bool Uses linearInteger(0, 1), Not ValueMapping.bool + +`ValueMapping.bool` returns `boolean`, which cannot be stored in a `Float32Field` (type mismatch crash). Instead, bool parameters use `ValueMapping.linearInteger(0, 1)`: +- Stores `0.0` or `1.0` in the Float32Field ✓ +- `paramChanged` receives `0` or `1` (numbers) ✓ +- Knob snaps between two positions (`floating()` returns `false`) ✓ +- No new box type needed ✓ + +### Int Uses linearInteger(min, max) + +`ValueMapping.linearInteger(min, max)` returns `int` (branded `number`), compatible with `Float32Field`. Integers are stored as floats (e.g., `3.0`). Float32 represents all integers up to 2^23 exactly — sufficient for any practical Werkstatt parameter range. + +### No New Boxes Needed + +A single `WerkstattParameterBox` with `Float32Field "any"` handles all mapping types: + +| Mapping | Field stores | ValueMapping | +|---|---|---| +| unipolar | 0.5 | `unipolar()` | +| linear(20, 20000) | 1000.0 | `linear(20, 20000)` | +| exp(20, 20000) | 1000.0 | `exponential(20, 20000)` | +| int(0, 127) | 64.0 | `linearInteger(0, 127)` | +| bool | 0.0 or 1.0 | `linearInteger(0, 1)` | + +### Mapping Metadata + +The mapping info (min, max, type, unit) is **not stored in the box**. It is derived from the `@param` comment in the code string at two points: +1. **ScriptCompiler.reconcileParameters** — parses params, stores the mapped default directly in the box +2. **Adapter constructor** — parses params from the code to determine the ValueMapping for each parameter by label + +The code is the single source of truth for mapping info. + +--- + +## Changes + +### 0. WerkstattParameterBox Forge Schema + +Change fields 4 and 5 in `packages/studio/forge-boxes/src/schema/devices/audio-effects/WerkstattParameterBox.ts`: +- `value`: constraint `"unipolar"` → `"any"`, unit `"%"` → `""` +- `defaultValue`: constraint `"unipolar"` → `"any"`, unit `"%"` → `""` + +Then regenerate the box class. + +### 1. ScriptCompiler.ts — Extended Parsing + +**ParamDeclaration** — extend the interface: +```typescript +interface ParamDeclaration { + label: string + defaultValue: number // in mapped space (e.g., 1000 for Hz) + min: number // mapping range min + max: number // mapping range max + mapping: "unipolar" | "linear" | "exp" | "int" | "bool" + unit: string // display unit (e.g., "Hz", "ms") +} +``` + +**parseParams** — rewrite to handle the extended format. Return the full declaration including mapping info. + +**reconcileParameters** — store the mapped default value directly in the box (the field is now `"any"`, not `"unipolar"`): +```typescript +paramBox.value.setValue(declaration.defaultValue) +paramBox.defaultValue.setValue(declaration.defaultValue) +``` + +**New helper** — `resolveValueMapping(declaration)` returns the appropriate `ValueMapping` based on the declaration's mapping type, min, and max. + +**New helper** — `resolveStringMapping(declaration)` returns the appropriate `StringMapping` based on the mapping type and unit. + +Export both helpers so adapters can reuse them. + +### 2. WerkstattDeviceBoxAdapter.ts / SpielwerkDeviceBoxAdapter.ts — Dynamic Mapping + +In the `onAdded` callback, instead of hardcoding `ValueMapping.unipolar()`: +1. Parse the params from `box.code.getValue()` (using the compiler's `parseParams`) +2. Find the declaration matching the parameter's label +3. Create the appropriate `ValueMapping` and `StringMapping` from the declaration + +```typescript +onAdded: ({box: parameterBox}) => { + const paramBox = asInstanceOf(parameterBox, WerkstattParameterBox) + const label = paramBox.label.getValue() + const declaration = declarations.find(decl => decl.label === label) + const valueMapping = resolveValueMapping(declaration) + const stringMapping = resolveStringMapping(declaration) + this.#parametric.createParameter(paramBox.value, valueMapping, stringMapping, label) +} +``` + +The `declarations` array is parsed once from `box.code.getValue()` and refreshed on code changes. + +### 3. Processors — No Change + +`WerkstattDeviceProcessor.parameterChanged` and `SpielwerkDeviceProcessor.parameterChanged` already call `parameter.getValue()`, which returns the mapped value through the `ValueMapping`. Since the adapter now sets the correct mapping, the processor automatically delivers mapped values to `paramChanged`. + +### 4. Manual Pages — Update @param Documentation + +Update the Werkstatt and Spielwerk manual pages to document the extended `@param` format, supported types, and examples. + +### 5. Examples — Update to Use Mapped Parameters + +Update the `.js` example files to use the new format where appropriate. For example: +```javascript +// @param cutoff 1000 20 20000 exp Hz +// @param resonance 0.707 0.1 10 linear +``` + +Instead of the current manual scaling in `paramChanged`. + +--- + +## Backward Compatibility + +- `// @param name` and `// @param name default` continue to work as unipolar (0–1) +- Existing projects with unipolar parameters load unchanged — values in 0–1 range are valid `"any"` values, and `ValueMapping.unipolar()` is identity +- Forge schema change (unipolar → any) is backward compatible: existing float32 values in [0, 1] are valid in "any" constraint + +--- + +## Reconciliation Behavior + +When the user recompiles with changed parameters: + +- **Label unchanged, mapping changed** — The unitValue stays. The adapter recreates the parameter with the new ValueMapping. The processor receives a `paramChanged` call with the newly mapped value. If the default value changed, the control is reset to the new default. +- **Label unchanged, default changed** — The value is reset to the new default (converted to unitValue). The processor receives `paramChanged` with the new default. +- **Label added** — New parameter box created, value set to default. Processor receives `paramChanged`. +- **Label removed** — Parameter box deleted, knob removed. + +After every recompile, all parameter values are re-pushed to the processor via `paramChanged` (the `#pushAllParameters` call in `#swapProcessor` already handles this). + +--- + +## Validation + +The compiler validates `@param` declarations and throws on: +- `default` outside `[min, max]` (with tolerance for floating-point error, e.g., `Math.abs(default - min) > 1e-6`) +- `min >= max` (with same tolerance) +- Unknown mapping type (not one of `linear`, `exp`, `int`, `bool`) +- Malformed syntax (wrong number of tokens, non-numeric values where numbers expected)