@vessel-dsp/compiler 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -2
- package/dist/bake.d.ts +38 -0
- package/dist/bake.d.ts.map +1 -0
- package/dist/bake.js +101 -0
- package/dist/bake.js.map +1 -0
- package/dist/bbd-clock.d.ts +171 -0
- package/dist/bbd-clock.d.ts.map +1 -0
- package/dist/bbd-clock.js +1750 -0
- package/dist/bbd-clock.js.map +1 -0
- package/dist/block-row.d.ts +51 -0
- package/dist/block-row.d.ts.map +1 -0
- package/dist/block-row.js +17 -0
- package/dist/block-row.js.map +1 -0
- package/dist/bypass.d.ts +3 -0
- package/dist/bypass.d.ts.map +1 -0
- package/dist/bypass.js +228 -0
- package/dist/bypass.js.map +1 -0
- package/dist/compile.d.ts +24 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +344 -0
- package/dist/compile.js.map +1 -0
- package/dist/converter-scale.d.ts +14 -0
- package/dist/converter-scale.d.ts.map +1 -0
- package/dist/converter-scale.js +133 -0
- package/dist/converter-scale.js.map +1 -0
- package/dist/couple.d.ts +28 -0
- package/dist/couple.d.ts.map +1 -0
- package/dist/couple.js +567 -0
- package/dist/couple.js.map +1 -0
- package/dist/dangling-active-terminal.d.ts +47 -0
- package/dist/dangling-active-terminal.d.ts.map +1 -0
- package/dist/dangling-active-terminal.js +542 -0
- package/dist/dangling-active-terminal.js.map +1 -0
- package/dist/declared-program.d.ts +23 -0
- package/dist/declared-program.d.ts.map +1 -0
- package/dist/declared-program.js +387 -0
- package/dist/declared-program.js.map +1 -0
- package/dist/denomination.d.ts +48 -0
- package/dist/denomination.d.ts.map +1 -0
- package/dist/denomination.js +27 -0
- package/dist/denomination.js.map +1 -0
- package/dist/device-laws.d.ts +237 -0
- package/dist/device-laws.d.ts.map +1 -0
- package/dist/device-laws.js +3497 -0
- package/dist/device-laws.js.map +1 -0
- package/dist/diagnostics.d.ts +13 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +19 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/emit.d.ts +8 -0
- package/dist/emit.d.ts.map +1 -0
- package/dist/emit.js +43 -0
- package/dist/emit.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/firmware-class.d.ts +69 -0
- package/dist/firmware-class.d.ts.map +1 -0
- package/dist/firmware-class.js +20 -0
- package/dist/firmware-class.js.map +1 -0
- package/dist/fixtures/circuits.d.ts +1721 -0
- package/dist/fixtures/circuits.d.ts.map +1 -0
- package/dist/fixtures/circuits.js +13404 -0
- package/dist/fixtures/circuits.js.map +1 -0
- package/dist/fixtures/expected.d.ts +257 -0
- package/dist/fixtures/expected.d.ts.map +1 -0
- package/dist/fixtures/expected.js +311 -0
- package/dist/fixtures/expected.js.map +1 -0
- package/dist/fixtures/index.d.ts +4 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +8 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/fixtures/registry.d.ts +52 -0
- package/dist/fixtures/registry.d.ts.map +1 -0
- package/dist/fixtures/registry.js +341 -0
- package/dist/fixtures/registry.js.map +1 -0
- package/dist/generic-tube-fit.d.ts +12 -0
- package/dist/generic-tube-fit.d.ts.map +1 -0
- package/dist/generic-tube-fit.js +45 -0
- package/dist/generic-tube-fit.js.map +1 -0
- package/dist/grounded-clock-supply.d.ts +13 -0
- package/dist/grounded-clock-supply.d.ts.map +1 -0
- package/dist/grounded-clock-supply.js +45 -0
- package/dist/grounded-clock-supply.js.map +1 -0
- package/dist/identify.d.ts +50 -0
- package/dist/identify.d.ts.map +1 -0
- package/dist/identify.js +319 -0
- package/dist/identify.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/inert-controls.d.ts +15 -0
- package/dist/inert-controls.d.ts.map +1 -0
- package/dist/inert-controls.js +266 -0
- package/dist/inert-controls.js.map +1 -0
- package/dist/latch-seed.d.ts +53 -0
- package/dist/latch-seed.d.ts.map +1 -0
- package/dist/latch-seed.js +257 -0
- package/dist/latch-seed.js.map +1 -0
- package/dist/link.d.ts +7 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +270 -0
- package/dist/link.js.map +1 -0
- package/dist/lower.d.ts +79 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +1855 -0
- package/dist/lower.js.map +1 -0
- package/dist/netlist.d.ts +79 -0
- package/dist/netlist.d.ts.map +1 -0
- package/dist/netlist.js +4746 -0
- package/dist/netlist.js.map +1 -0
- package/dist/non-executable-clock-drivers.d.ts +8 -0
- package/dist/non-executable-clock-drivers.d.ts.map +1 -0
- package/dist/non-executable-clock-drivers.js +33 -0
- package/dist/non-executable-clock-drivers.js.map +1 -0
- package/dist/open-category.d.ts +98 -0
- package/dist/open-category.d.ts.map +1 -0
- package/dist/open-category.js +111 -0
- package/dist/open-category.js.map +1 -0
- package/dist/over-driven-node.d.ts +33 -0
- package/dist/over-driven-node.d.ts.map +1 -0
- package/dist/over-driven-node.js +188 -0
- package/dist/over-driven-node.js.map +1 -0
- package/dist/part-catalog.d.ts +7 -0
- package/dist/part-catalog.d.ts.map +1 -0
- package/dist/part-catalog.js +6639 -0
- package/dist/part-catalog.js.map +1 -0
- package/dist/part-number.d.ts +32 -0
- package/dist/part-number.d.ts.map +1 -0
- package/dist/part-number.js +80 -0
- package/dist/part-number.js.map +1 -0
- package/dist/partition.d.ts +7 -0
- package/dist/partition.d.ts.map +1 -0
- package/dist/partition.js +242 -0
- package/dist/partition.js.map +1 -0
- package/dist/port-full-scale.d.ts +70 -0
- package/dist/port-full-scale.d.ts.map +1 -0
- package/dist/port-full-scale.js +235 -0
- package/dist/port-full-scale.js.map +1 -0
- package/dist/pot-orientation.d.ts +114 -0
- package/dist/pot-orientation.d.ts.map +1 -0
- package/dist/pot-orientation.js +551 -0
- package/dist/pot-orientation.js.map +1 -0
- package/dist/power-domain-control.d.ts +40 -0
- package/dist/power-domain-control.d.ts.map +1 -0
- package/dist/power-domain-control.js +142 -0
- package/dist/power-domain-control.js.map +1 -0
- package/dist/proxy-declaration.d.ts +121 -0
- package/dist/proxy-declaration.d.ts.map +1 -0
- package/dist/proxy-declaration.js +50 -0
- package/dist/proxy-declaration.js.map +1 -0
- package/dist/registry.d.ts +323 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +192 -0
- package/dist/registry.js.map +1 -0
- package/dist/sparse-schedule.d.ts +65 -0
- package/dist/sparse-schedule.d.ts.map +1 -0
- package/dist/sparse-schedule.js +421 -0
- package/dist/sparse-schedule.js.map +1 -0
- package/dist/speaker-load.d.ts +38 -0
- package/dist/speaker-load.d.ts.map +1 -0
- package/dist/speaker-load.js +97 -0
- package/dist/speaker-load.js.map +1 -0
- package/dist/stage-coverage.d.ts +21 -0
- package/dist/stage-coverage.d.ts.map +1 -0
- package/dist/stage-coverage.js +124 -0
- package/dist/stage-coverage.js.map +1 -0
- package/dist/stamp-partition.d.ts +314 -0
- package/dist/stamp-partition.d.ts.map +1 -0
- package/dist/stamp-partition.js +472 -0
- package/dist/stamp-partition.js.map +1 -0
- package/dist/supply-reference.d.ts +19 -0
- package/dist/supply-reference.d.ts.map +1 -0
- package/dist/supply-reference.js +66 -0
- package/dist/supply-reference.js.map +1 -0
- package/dist/supply-short.d.ts +32 -0
- package/dist/supply-short.d.ts.map +1 -0
- package/dist/supply-short.js +118 -0
- package/dist/supply-short.js.map +1 -0
- package/dist/supply-stamps.d.ts +51 -0
- package/dist/supply-stamps.d.ts.map +1 -0
- package/dist/supply-stamps.js +363 -0
- package/dist/supply-stamps.js.map +1 -0
- package/dist/terminal-role.d.ts +109 -0
- package/dist/terminal-role.d.ts.map +1 -0
- package/dist/terminal-role.js +46 -0
- package/dist/terminal-role.js.map +1 -0
- package/dist/terminal-roles.d.ts +59 -0
- package/dist/terminal-roles.d.ts.map +1 -0
- package/dist/terminal-roles.js +92 -0
- package/dist/terminal-roles.js.map +1 -0
- package/dist/transformer.d.ts +181 -0
- package/dist/transformer.d.ts.map +1 -0
- package/dist/transformer.js +476 -0
- package/dist/transformer.js.map +1 -0
- package/dist/types.d.ts +3157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/dist/unbounded-operating-point.d.ts +35 -0
- package/dist/unbounded-operating-point.d.ts.map +1 -0
- package/dist/unbounded-operating-point.js +181 -0
- package/dist/unbounded-operating-point.js.map +1 -0
- package/dist/unexecuted-region.d.ts +3 -0
- package/dist/unexecuted-region.d.ts.map +1 -0
- package/dist/unexecuted-region.js +122 -0
- package/dist/unexecuted-region.js.map +1 -0
- package/dist/unimplemented-control-role.d.ts +3 -0
- package/dist/unimplemented-control-role.d.ts.map +1 -0
- package/dist/unimplemented-control-role.js +61 -0
- package/dist/unimplemented-control-role.js.map +1 -0
- package/dist/unreachable-output.d.ts +3 -0
- package/dist/unreachable-output.d.ts.map +1 -0
- package/dist/unreachable-output.js +97 -0
- package/dist/unreachable-output.js.map +1 -0
- package/dist/unreadable-terminal-role.d.ts +67 -0
- package/dist/unreadable-terminal-role.d.ts.map +1 -0
- package/dist/unreadable-terminal-role.js +409 -0
- package/dist/unreadable-terminal-role.js.map +1 -0
- package/dist/unverified-pinout.d.ts +27 -0
- package/dist/unverified-pinout.d.ts.map +1 -0
- package/dist/unverified-pinout.js +121 -0
- package/dist/unverified-pinout.js.map +1 -0
- package/package.json +75 -5
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,3157 @@
|
|
|
1
|
+
/** Electrical node. 0 is always ground. */
|
|
2
|
+
export type NodeId = number;
|
|
3
|
+
export declare const GROUND: NodeId;
|
|
4
|
+
export type DeviceId = string;
|
|
5
|
+
export type ControlId = string;
|
|
6
|
+
/**
|
|
7
|
+
* What a device is electrically. Derived from the source's typed component kind, never
|
|
8
|
+
* from a name. For everything except `ic`, the constitutive law follows from this plus
|
|
9
|
+
* parameters, so no identification is required.
|
|
10
|
+
*/
|
|
11
|
+
export type DeviceKind = "resistor" | "capacitor" | "inductor" | "diode" | "potentiometer"
|
|
12
|
+
/** Two-terminal variable resistor. Not a pot: one element, not two halves. */
|
|
13
|
+
| "rheostat" | "opamp" | "bjt" | "jfet" | "mosfet"
|
|
14
|
+
/**
|
|
15
|
+
* A vacuum triode. Grid, cathode and plate — three terminals like a BJT and a
|
|
16
|
+
* fundamentally different law: no junction, no exponential, and plate current in one
|
|
17
|
+
* direction only.
|
|
18
|
+
*/
|
|
19
|
+
| "triode"
|
|
20
|
+
/**
|
|
21
|
+
* A vacuum pentode or beam tetrode. The triode's three electrodes plus a **screen**,
|
|
22
|
+
* which is what sets the cathode current and makes the plate characteristic flat. 42 of
|
|
23
|
+
* the corpus's 56 declare exactly `plate, grid, screen, cathode`.
|
|
24
|
+
*/
|
|
25
|
+
| "pentode"
|
|
26
|
+
/**
|
|
27
|
+
* A vacuum rectifier: plates and one cathode, no grid, and current in one direction only
|
|
28
|
+
* by the physics rather than by a junction. Every instance in the amp corpus is a **dual**
|
|
29
|
+
* diode — two plates sharing a directly heated cathode — which is one component and two
|
|
30
|
+
* elements.
|
|
31
|
+
*/
|
|
32
|
+
| "tube-diode" | "transformer" | "voltage-source" | "jack" | "ground" | "rail" | "switch" | "ic" | "ota" | "inverter" | "nand-gate" | "clock-driver" | "optocoupler" | "logic-divider" | "analog-switch" | "compandor" | "bbd" | "comparator" | "power-amp";
|
|
33
|
+
/**
|
|
34
|
+
* Evidence a part can be recognised by, in the order stage 2 consults it.
|
|
35
|
+
*
|
|
36
|
+
* **`partNumber` and `declaredType` are stage 2's alone.** They answer "what is this
|
|
37
|
+
* thing", and a later stage reading them would be deciding behaviour from identity.
|
|
38
|
+
* Everything downstream sees a resolved identity or a refusal, never that evidence.
|
|
39
|
+
*
|
|
40
|
+
* `terminalRoles` is different and is deliberately shared with stage 5, which reads it
|
|
41
|
+
* to tell a diode's anode from its cathode, a pot's wiper from its ends, an op-amp's
|
|
42
|
+
* inverting input, and a supply's polarity. That is not identification: it is which of a
|
|
43
|
+
* device's own terminals is which, it is a whole-token comparison against a closed set,
|
|
44
|
+
* and reading it by position instead is what silently inverted rails, exchanged
|
|
45
|
+
* transistor terminals and shorted pot tracks to ground. An earlier version of this
|
|
46
|
+
* comment said stage 2 only, which the code has never done.
|
|
47
|
+
*/
|
|
48
|
+
export type DeviceIdentity = {
|
|
49
|
+
/** Manufacturer part number as authored, compared whole and case-folded. */
|
|
50
|
+
readonly partNumber: string | null;
|
|
51
|
+
/** A value from the source's closed type vocabulary, e.g. `Circuit.DelayMemoryChip`. */
|
|
52
|
+
readonly declaredType: string | null;
|
|
53
|
+
/**
|
|
54
|
+
* Terminal role tokens, in terminal order. Compared as whole tokens against a
|
|
55
|
+
* closed alias list, never by substring. `null` where a terminal is bare-numbered.
|
|
56
|
+
*/
|
|
57
|
+
readonly terminalRoles: readonly (string | null)[];
|
|
58
|
+
/**
|
|
59
|
+
* The `role` each terminal **declares**, as core parsed it, or null where it declares none.
|
|
60
|
+
*
|
|
61
|
+
* Distinct from `terminalRoles`, which is the folded *name* token. A name is the pin's
|
|
62
|
+
* identity and a role is what the electrode does; conflating them is what made every stage
|
|
63
|
+
* carry a spelling table. Prefer this: it is the source's own statement, needs no vocabulary
|
|
64
|
+
* to interpret, and cannot be ambiguous.
|
|
65
|
+
*
|
|
66
|
+
* Null throughout for a document written before `role` existed, which is why the resolvers
|
|
67
|
+
* keep their declaration-order fallback.
|
|
68
|
+
*/
|
|
69
|
+
readonly declaredTerminalRoles: readonly (string | null)[];
|
|
70
|
+
/**
|
|
71
|
+
* The coupled coils this component **declares**, as terminal indices in coil order, or null
|
|
72
|
+
* where it declares none.
|
|
73
|
+
*
|
|
74
|
+
* A transformer's terminals belong to coils and the format used not to say which, so this
|
|
75
|
+
* stage reconstructed the grouping from terminal spellings -- a 110-entry table that covered
|
|
76
|
+
* the corpus because it was written from it, one packet at a time. `@vessel-dsp/core@0.6.35`
|
|
77
|
+
* makes the grouping a declaration, so it is read.
|
|
78
|
+
*
|
|
79
|
+
* Order is coil order: the ends are the terminals whose own role is `winding`, and a tap sits
|
|
80
|
+
* physically where the list puts it. That is the fact no spelling could carry --
|
|
81
|
+
* `secondary_4`/`secondary_8`/`secondary_16` are ordered by their names only by luck.
|
|
82
|
+
*/
|
|
83
|
+
readonly declaredWindings: readonly DeclaredWinding[] | null;
|
|
84
|
+
};
|
|
85
|
+
/** One declared coil: its role, its optional name, and its terminals in coil order. */
|
|
86
|
+
export type DeclaredWinding = Readonly<{
|
|
87
|
+
role: string;
|
|
88
|
+
id: string | null;
|
|
89
|
+
terminalIndices: readonly number[];
|
|
90
|
+
/**
|
|
91
|
+
* The index, within `terminalIndices`, of the coil's reference tap -- the terminal declaring
|
|
92
|
+
* `windingCenterTap` -- or null where it declares none.
|
|
93
|
+
*
|
|
94
|
+
* A centre tap and an output tap are the same shape and different circuits: about a centre
|
|
95
|
+
* tap both halves of the coil are live at once, which is what makes a full-wave rectifier
|
|
96
|
+
* full-wave, while output taps are alternatives of which a selector makes one live. Nothing
|
|
97
|
+
* this stage can compute separates them, so `@vessel-dsp/core@0.6.36` splits the role and
|
|
98
|
+
* this is the read.
|
|
99
|
+
*/
|
|
100
|
+
centerTapAt: number | null;
|
|
101
|
+
/**
|
|
102
|
+
* This coil's rated AC voltage in volts RMS, or null where the source states none.
|
|
103
|
+
*
|
|
104
|
+
* Replaced eight component-property spellings folding to five winding classes. A property
|
|
105
|
+
* keyed on a class holds one value per class, so `orange-rockerverb`'s two filament coils
|
|
106
|
+
* collapsed into a single `voltsFilament` and its 3.15-0-3.15 V winding was driven at 6 V per
|
|
107
|
+
* half. Per the coil, per the source (`@vessel-dsp/core@0.6.37`).
|
|
108
|
+
*
|
|
109
|
+
* **Per half where the coil declares a centre tap**, end to end otherwise -- the convention a
|
|
110
|
+
* transformer is printed by, made unambiguous by the declared tap.
|
|
111
|
+
*/
|
|
112
|
+
voltageRmsVolts: number | null;
|
|
113
|
+
/**
|
|
114
|
+
* Rated impedances, each with the two terminal indices it is rated across.
|
|
115
|
+
*
|
|
116
|
+
* The pair is carried because transformers are not rated by one convention: a primary is
|
|
117
|
+
* printed plate-to-plate, across its centre tap, and a secondary from its common to each tap.
|
|
118
|
+
* A coil may carry several -- `orange-rockerverb`'s output secondary rates two taps that are
|
|
119
|
+
* loaded at the same time.
|
|
120
|
+
*/
|
|
121
|
+
readonly impedances: readonly DeclaredWindingImpedance[];
|
|
122
|
+
}>;
|
|
123
|
+
/** One rated impedance of a coil: the terminal indices it spans, and the value in ohms. */
|
|
124
|
+
export type DeclaredWindingImpedance = Readonly<{
|
|
125
|
+
across: readonly [number, number];
|
|
126
|
+
ohms: number;
|
|
127
|
+
}>;
|
|
128
|
+
export type Device = {
|
|
129
|
+
readonly id: DeviceId;
|
|
130
|
+
readonly kind: DeviceKind;
|
|
131
|
+
/** Node per terminal, in terminal order. Terminal index is the port index. */
|
|
132
|
+
readonly nodes: readonly NodeId[];
|
|
133
|
+
/** Parameters in SI base units. `10k` has already become `10000`. */
|
|
134
|
+
readonly parameters: Readonly<Record<string, number>>;
|
|
135
|
+
/** Set when a control varies this device's parameters. */
|
|
136
|
+
readonly control: ControlId | null;
|
|
137
|
+
readonly identity: DeviceIdentity;
|
|
138
|
+
readonly isLed?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* This device's id **within its package**, when the source split the package itself.
|
|
141
|
+
*
|
|
142
|
+
* Set only by a `devices:` declaration read through core's `componentDevices`, never by a
|
|
143
|
+
* registry `sections` expansion -- those two splits must stay tellable apart, because a
|
|
144
|
+
* registry split already carries the part's law and a source split does not. What reads this
|
|
145
|
+
* is `packageDeviceParameters`, which is how a registered part's numbers reach a device its
|
|
146
|
+
* document, rather than its part entry, decided the shape of.
|
|
147
|
+
*/
|
|
148
|
+
readonly packageDeviceId?: string;
|
|
149
|
+
/**
|
|
150
|
+
* The program **this instance** of a reprogrammable chip is running, as the source declares
|
|
151
|
+
* it, or absent where it declares none.
|
|
152
|
+
*
|
|
153
|
+
* The second resolution key. A fixed-function part is identified and the registry supplies
|
|
154
|
+
* its model, which is a lookup keyed on the part number. That key is wrong for shared
|
|
155
|
+
* silicon and the corpus says so: `TC25SC080AU-104` is a delay in `boss-dd-5`, a reverb in
|
|
156
|
+
* `boss-rv-3` and a pitch shifter in `boss-hr-2`, so one catalog entry has to hold three
|
|
157
|
+
* answers and holds none. `@vessel-dsp/core@0.8.0` makes the program a declaration on the
|
|
158
|
+
* component, so it is read from there instead.
|
|
159
|
+
*
|
|
160
|
+
* **Carried verbatim and validated where it is used.** The ops arrive as core parsed them,
|
|
161
|
+
* an op name plus an untyped bag, because the format has no opinion about a vocabulary that
|
|
162
|
+
* belongs to the runtime. Turning that into a `PrimitiveOp` is this compiler's job and its
|
|
163
|
+
* refusals name the op they could not place -- which is the console/ROM invariant applied to
|
|
164
|
+
* the source rather than to the program.
|
|
165
|
+
*/
|
|
166
|
+
readonly program?: DeclaredProgram;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* One declared op, exactly as the source wrote it: a name and an untyped bag of arguments.
|
|
170
|
+
*
|
|
171
|
+
* Deliberately not `PrimitiveOp`. A declaration is source text until it has been checked, and
|
|
172
|
+
* typing it as the thing it hopes to become is how an unvalidated bag reaches lowering. The
|
|
173
|
+
* conversion is `validateDeclaredOp`, and it refuses by name.
|
|
174
|
+
*/
|
|
175
|
+
export type DeclaredProgramOp = Readonly<{
|
|
176
|
+
readonly op: string;
|
|
177
|
+
readonly [key: string]: unknown;
|
|
178
|
+
}>;
|
|
179
|
+
/**
|
|
180
|
+
* A declared numeric parameter with its citation.
|
|
181
|
+
*
|
|
182
|
+
* `source` is prose naming the document the numbers came from, following the triode registry that
|
|
183
|
+
* CLAUDE.md names as the provenance template. **Its absence is a statement, not a gap**: a
|
|
184
|
+
* parameter with no citation is not evidenced, so it is not set and the block makes no claim on
|
|
185
|
+
* that axis. That rule is what keeps an undocumented mode out of a landing by construction
|
|
186
|
+
* rather than by anyone remembering to leave it out.
|
|
187
|
+
*/
|
|
188
|
+
/**
|
|
189
|
+
* How a chip turns presses into a tempo (core 0.16.0 `ProgramTapLaw`): a run of `presses`, each
|
|
190
|
+
* within `timeoutSeconds` of the last, sets the tempo to the run's mean interval; a longer gap ends
|
|
191
|
+
* the run and keeps the tempo; `defaultSeconds` is the interval before any tempo is set.
|
|
192
|
+
*/
|
|
193
|
+
export type TapLaw = {
|
|
194
|
+
readonly presses: number;
|
|
195
|
+
readonly timeoutSeconds: number;
|
|
196
|
+
readonly defaultSeconds: number | null;
|
|
197
|
+
};
|
|
198
|
+
export type DeclaredProgramParameter = Readonly<{
|
|
199
|
+
readonly control?: string;
|
|
200
|
+
/**
|
|
201
|
+
* How the parameter reaches its control, exactly as `DeclaredProgramRouter.read`: `node`
|
|
202
|
+
* reads the control's wiper, `scanned` reads the control itself because a chip does and the
|
|
203
|
+
* path between them is not modelled. Core 0.11.0 refuses a scanned parameter with no reader,
|
|
204
|
+
* a reader not on the board, or no control.
|
|
205
|
+
*/
|
|
206
|
+
readonly read: "node" | "scanned" | "tapped";
|
|
207
|
+
readonly scannedBy: string | null;
|
|
208
|
+
/** `tapped` only (core 0.13.0): the value is the tapped interval times this. */
|
|
209
|
+
readonly ratio: number | null;
|
|
210
|
+
/** `tapped` only (core 0.16.0): the firmware's tap law, where a source states one. */
|
|
211
|
+
readonly tap?: TapLaw;
|
|
212
|
+
readonly min: number;
|
|
213
|
+
readonly max: number;
|
|
214
|
+
readonly source?: string;
|
|
215
|
+
}>;
|
|
216
|
+
/** One selectable program: what the chip computes while the selector sits here. */
|
|
217
|
+
export type DeclaredProgramPosition = Readonly<{
|
|
218
|
+
readonly id: string;
|
|
219
|
+
readonly label: string | null;
|
|
220
|
+
readonly ops: readonly DeclaredProgramOp[];
|
|
221
|
+
/** Delay lines by name, each carrying its cited parameters. */
|
|
222
|
+
readonly lines: Readonly<Record<string, Readonly<Record<string, DeclaredProgramParameter>>>>;
|
|
223
|
+
/** Named parameters the ops refer to by `{ parameter: <name> }` (core 0.12.0). */
|
|
224
|
+
readonly parameters: Readonly<Record<string, DeclaredProgramParameter>>;
|
|
225
|
+
}>;
|
|
226
|
+
/**
|
|
227
|
+
* A component's declared program: a selector over compositions.
|
|
228
|
+
*
|
|
229
|
+
* **Modes are separate compositions, not a wider knob.** HOLD and REVERSE are different programs
|
|
230
|
+
* rather than extreme settings of one. A single position needs no router; more than one without
|
|
231
|
+
* a router is a declaration that cannot be executed, and core refuses it at parse rather than
|
|
232
|
+
* silently taking the first.
|
|
233
|
+
*/
|
|
234
|
+
export type DeclaredProgramRoute = Readonly<{
|
|
235
|
+
readonly position: number;
|
|
236
|
+
readonly program: string;
|
|
237
|
+
}>;
|
|
238
|
+
/** How the panel chooses, as the source declares it. See `ComposedRouter` for what it becomes. */
|
|
239
|
+
export type DeclaredProgramRouter = Readonly<{
|
|
240
|
+
readonly control: string;
|
|
241
|
+
/** `node` reads the control's wiper; `scanned` reads the control itself. */
|
|
242
|
+
readonly read: "node" | "scanned";
|
|
243
|
+
/** The component that reads a scanned control. Core refuses a scanned router without one. */
|
|
244
|
+
readonly scannedBy: string | null;
|
|
245
|
+
readonly positions: number;
|
|
246
|
+
readonly routes: readonly DeclaredProgramRoute[];
|
|
247
|
+
}>;
|
|
248
|
+
export type DeclaredProgram = Readonly<{
|
|
249
|
+
readonly router: DeclaredProgramRouter | null;
|
|
250
|
+
readonly positions: readonly DeclaredProgramPosition[];
|
|
251
|
+
}>;
|
|
252
|
+
/**
|
|
253
|
+
* How a 0..1 control position maps to a fraction of a parameter's range.
|
|
254
|
+
*
|
|
255
|
+
* `reverse-linear` is a real track the corpus declares (`klon-centaur`,
|
|
256
|
+
* `vemuram-jan-ray`, `jhs-morning-glory`) and is not the same device as
|
|
257
|
+
* `reverse-logarithmic`: the track is linear, only its direction is reversed.
|
|
258
|
+
*/
|
|
259
|
+
export type TaperKind = "linear" | "logarithmic" | "reverse-logarithmic" | "reverse-linear";
|
|
260
|
+
export type Control = {
|
|
261
|
+
readonly id: ControlId;
|
|
262
|
+
readonly taper: TaperKind;
|
|
263
|
+
/** Position in 0..1. Units never cross this boundary. */
|
|
264
|
+
readonly defaultPosition: number;
|
|
265
|
+
/**
|
|
266
|
+
* Discrete position count for switches and selectors (e.g. 2 for SPST/SPDT toggle/footswitch,
|
|
267
|
+
* N for N-way rotary selector), or `null` for continuous controls (potentiometers, rheostats).
|
|
268
|
+
*/
|
|
269
|
+
readonly positions?: number | null;
|
|
270
|
+
/**
|
|
271
|
+
* The panel role the document declares for this control, verbatim, or `null` when it
|
|
272
|
+
* declares none. A closed vocabulary compared as a whole value -- never matched by
|
|
273
|
+
* substring, and never inferred from the control's id.
|
|
274
|
+
*
|
|
275
|
+
* It exists because two devices can straddle the same timing pins with nothing electrical
|
|
276
|
+
* to separate them: `mxr-carbon-copy` puts `VR3` (the DELAY knob) and `TR3` (a 500k internal
|
|
277
|
+
* trimmer) across the identical three nodes, so resistance, span and document order all
|
|
278
|
+
* pick arbitrarily. What the pedal knows and the netlist did not carry is that one of them
|
|
279
|
+
* is declared `delay-time` and the other is not.
|
|
280
|
+
*/
|
|
281
|
+
readonly role?: string | null;
|
|
282
|
+
/** The panel label declared in deviceInterface.controls, or null when absent. */
|
|
283
|
+
readonly label?: string | null;
|
|
284
|
+
/**
|
|
285
|
+
* **Derived, never declared**: the control is a contact a chip reads by its presses, so it
|
|
286
|
+
* springs back and a panel draws it as a button. True when some declared program reads it
|
|
287
|
+
* `tapped`. A user uploading a circuit is never asked to say a switch is momentary.
|
|
288
|
+
*/
|
|
289
|
+
readonly momentary?: boolean;
|
|
290
|
+
/**
|
|
291
|
+
* Present when this control is not a panel part but a **latch a controller's firmware keeps**
|
|
292
|
+
* (`@vessel-dsp/core` 0.14.0 `ComponentController`). A rising edge of `toggledBy` flips it
|
|
293
|
+
* between 0 and 1; a program reads it as it reads a knob, and a pin the controller drives
|
|
294
|
+
* follows it. It is state, so a panel shows it rather than offering it to turn.
|
|
295
|
+
*
|
|
296
|
+
* `initial` is the declared power-on state. `defaultPosition` is where a host and every
|
|
297
|
+
* instrument start it, which differs for exactly one case: a latch toggled by a
|
|
298
|
+
* `bypass`-role footswitch starts **engaged**, as every other pedal's bypass switch does in
|
|
299
|
+
* this corpus (see `switchesClosedForPortPath`), because a measurement of a pedal is a
|
|
300
|
+
* measurement of its effect. Engaged is derived: the end of the latch that a program reading
|
|
301
|
+
* it takes as its larger gain.
|
|
302
|
+
*/
|
|
303
|
+
readonly latch?: {
|
|
304
|
+
readonly toggledBy: ControlId;
|
|
305
|
+
readonly initial: 0 | 1;
|
|
306
|
+
};
|
|
307
|
+
/** The tap law a program reads this control's presses with, where its source states one. */
|
|
308
|
+
readonly tap?: TapLaw;
|
|
309
|
+
};
|
|
310
|
+
export type Ports = {
|
|
311
|
+
readonly input: NodeId;
|
|
312
|
+
readonly output: NodeId;
|
|
313
|
+
};
|
|
314
|
+
export type NetlistBypass = {
|
|
315
|
+
readonly declared: "none";
|
|
316
|
+
} | {
|
|
317
|
+
readonly declared: "switch";
|
|
318
|
+
readonly switch: string;
|
|
319
|
+
readonly engagedPosition?: string | number;
|
|
320
|
+
};
|
|
321
|
+
export type Netlist = {
|
|
322
|
+
readonly nodes: readonly NodeId[];
|
|
323
|
+
readonly devices: readonly Device[];
|
|
324
|
+
readonly controls: readonly Control[];
|
|
325
|
+
readonly ports: Ports;
|
|
326
|
+
readonly bypass: NetlistBypass;
|
|
327
|
+
/** What each port's own jack declares, for the chain's seam divider. `null` where absent. */
|
|
328
|
+
readonly portImpedanceOhms: {
|
|
329
|
+
readonly input: number | null;
|
|
330
|
+
readonly output: number | null;
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* The full scale a port's own jack declares via `V0dBFS`, in volts. `null` where absent.
|
|
334
|
+
*
|
|
335
|
+
* Preferred over the bound `port-full-scale.ts` derives, because it is the source saying
|
|
336
|
+
* directly what that derivation infers -- and it is the only number at all for a preamp
|
|
337
|
+
* monitor tap, which has no output transformer to step a rail down.
|
|
338
|
+
*/
|
|
339
|
+
readonly portDeclaredFullScaleVolts: {
|
|
340
|
+
readonly input: number | null;
|
|
341
|
+
readonly output: number | null;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* The document's own declaration that its blocks need symbolic elimination for
|
|
345
|
+
* **convergence**, not cost: the block is nonlinear and the dense-vs-sparse cost rule
|
|
346
|
+
* refuses it, yet the iterated solve does not converge on the uneliminated system.
|
|
347
|
+
*
|
|
348
|
+
* Read from the typed front-matter line `convergence-opt-in: true` (a closed-vocabulary
|
|
349
|
+
* flag compared whole, not prose -- core's interchange parser lifts a fixed allow-list
|
|
350
|
+
* of front-matter scalars and drops unknown keys, so the document's own text is read for
|
|
351
|
+
* the flag rather than a parsed field). It is an opt-in that overrides the cost veto in
|
|
352
|
+
* `shouldEliminate`, never the linear / zero-port guard.
|
|
353
|
+
*/
|
|
354
|
+
readonly convergenceOptIn: boolean;
|
|
355
|
+
readonly warnings?: readonly CompileWarning[];
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* A resolved part identity. There is deliberately **no confidence field**: a ranked
|
|
359
|
+
* guess has no consumer, and a wrong identity is worse than none because it renders a
|
|
360
|
+
* plausible wrong pedal instead of refusing.
|
|
361
|
+
*/
|
|
362
|
+
export type PartIdentity = {
|
|
363
|
+
readonly partId: string;
|
|
364
|
+
readonly evidence: "exact-part" | "declared-type" | "pinout";
|
|
365
|
+
};
|
|
366
|
+
/** A lumped constitutive relation the solver can stamp into a matrix. */
|
|
367
|
+
export type DeviceLaw = {
|
|
368
|
+
readonly kind: "conductance";
|
|
369
|
+
readonly siemens: number;
|
|
370
|
+
} | {
|
|
371
|
+
/** Conductance that depends on a control position, e.g. a pot. */
|
|
372
|
+
readonly kind: "controlled-conductance";
|
|
373
|
+
readonly control: ControlId;
|
|
374
|
+
readonly taper: TaperKind;
|
|
375
|
+
readonly totalOhms: number;
|
|
376
|
+
/** Which side of the wiper this element is. */
|
|
377
|
+
readonly side: "upper" | "lower";
|
|
378
|
+
/**
|
|
379
|
+
* Declared-only end resistance: the ohms a leg keeps when the wiper is driven
|
|
380
|
+
* fully to that end, because a real wiper cannot reach the track's last
|
|
381
|
+
* millimetre. 0 means an ideal pot whose leg reaches zero.
|
|
382
|
+
*
|
|
383
|
+
* Applied so the two legs still sum to `totalOhms` exactly -- each leg is
|
|
384
|
+
* `residualOhms + share * (totalOhms - 2 * residualOhms)`, so the wiper
|
|
385
|
+
* travels between the two residuals instead of end to end. That is what makes
|
|
386
|
+
* this different from `controlled-resistance`'s `minOhms`, which is the bottom
|
|
387
|
+
* of an independent sweep with no complementary half to conserve.
|
|
388
|
+
*/
|
|
389
|
+
readonly residualOhms: number;
|
|
390
|
+
} | {
|
|
391
|
+
/**
|
|
392
|
+
* A two-terminal rheostat, whose resistance sweeps `minOhms`..`maxOhms`.
|
|
393
|
+
*
|
|
394
|
+
* Deliberately not `controlled-conductance` with a third node left out. A
|
|
395
|
+
* pot's halves are complementary shares of one fixed track, so `side` is
|
|
396
|
+
* meaningful; a rheostat is one element sweeping a range, so it carries a
|
|
397
|
+
* range instead. Sharing the law would make both wrong.
|
|
398
|
+
*
|
|
399
|
+
* Both do carry an end resistance, under different names and different
|
|
400
|
+
* arithmetic: `minOhms` here is the floor of a free sweep, while a pot's
|
|
401
|
+
* `residualOhms` must be taken out of a fixed track that the other half
|
|
402
|
+
* still has to complete. An earlier version of this comment said a minimum
|
|
403
|
+
* was not meaningful for a pot, which confused those two arithmetics for the
|
|
404
|
+
* physical claim -- a real pot's wiper does stop short of the track end.
|
|
405
|
+
*/
|
|
406
|
+
readonly kind: "controlled-resistance";
|
|
407
|
+
readonly control: ControlId;
|
|
408
|
+
readonly taper: TaperKind;
|
|
409
|
+
readonly minOhms: number;
|
|
410
|
+
readonly maxOhms: number;
|
|
411
|
+
} | {
|
|
412
|
+
readonly kind: "fixed-selector";
|
|
413
|
+
readonly onOhms: number;
|
|
414
|
+
readonly offOhms: number;
|
|
415
|
+
readonly position: number;
|
|
416
|
+
} | {
|
|
417
|
+
/**
|
|
418
|
+
* A jack's switch contact, closed in the normal playing configuration.
|
|
419
|
+
*
|
|
420
|
+
* **Engagement is a property of the port, not a control.** A switched jack's
|
|
421
|
+
* contact makes or breaks when a plug goes in, and the *sense* follows from the
|
|
422
|
+
* port's role rather than from the terminal: an input jack closes its contact
|
|
423
|
+
* when a cable is inserted (grounding the battery, powering the pedal), while a
|
|
424
|
+
* power jack breaks its contact when an adaptor is inserted (isolating the
|
|
425
|
+
* battery). Opposite engage states, and both reach the same powered
|
|
426
|
+
* configuration -- which is why the abstraction has to be the port and not a
|
|
427
|
+
* switch element with a hardcoded polarity per packet.
|
|
428
|
+
*
|
|
429
|
+
* Treating a jack as a pure interface symbol severed the battery return on 5
|
|
430
|
+
* packets, leaving the supply floating and every op-amp pinned at 0 V.
|
|
431
|
+
*
|
|
432
|
+
* The default here is the powered configuration, which both roles agree on.
|
|
433
|
+
* When ports become first-class, this moves onto the port as an engage state
|
|
434
|
+
* with a role-derived sense, and the runtime can then unplug the cable.
|
|
435
|
+
*
|
|
436
|
+
* The terminals are carried as indices because they are *derived*, not named:
|
|
437
|
+
* `device-laws` finds the contact by which end of a supply was stranded on it,
|
|
438
|
+
* and `againstIndex` is whichever terminal completes that supply's path --
|
|
439
|
+
* the jack's return for a stranded return, the remaining terminal for a
|
|
440
|
+
* stranded drive. Lowering must not re-guess them from role names: there is no
|
|
441
|
+
* closed contact-name vocabulary, and the corpus's contacts carry opposite
|
|
442
|
+
* senses under the same name.
|
|
443
|
+
*/
|
|
444
|
+
readonly kind: "port-engage";
|
|
445
|
+
readonly contactIndex: number;
|
|
446
|
+
readonly againstIndex: number;
|
|
447
|
+
} | {
|
|
448
|
+
readonly kind: "capacitance";
|
|
449
|
+
readonly farads: number;
|
|
450
|
+
/**
|
|
451
|
+
* Declared-only DC leakage through the dielectric, as a parallel conductance.
|
|
452
|
+
* 0 means ideal, and ideal is the diagnostic default: at DC an open capacitor
|
|
453
|
+
* is what makes a mis-transcribed bias network show up as a floating operating
|
|
454
|
+
* point instead of rendering a plausible wrong answer. A capacitor only gets a
|
|
455
|
+
* leak when its source declares one, never from a class convention.
|
|
456
|
+
*/
|
|
457
|
+
readonly leakageSiemens: number;
|
|
458
|
+
} | {
|
|
459
|
+
readonly kind: "inductance";
|
|
460
|
+
readonly henries: number;
|
|
461
|
+
/**
|
|
462
|
+
* Winding DC resistance, ohms. **Absent is not zero**, and the distinction is the point:
|
|
463
|
+
* every real winding has copper resistance, so zero is physically impossible rather than
|
|
464
|
+
* merely unusual. A lossless inductor in a feedback loop has unbounded Q, which is how a
|
|
465
|
+
* marginally stable real amplifier becomes an unstable simulated one.
|
|
466
|
+
*
|
|
467
|
+
* Three states, the same three-outcome rule every screen in this corpus needed: a number
|
|
468
|
+
* (stamped), `"unknown"` (the packet states the source does not give it -- reported as an
|
|
469
|
+
* unmodelled parameter, stamped as lossless), and absent (nobody has said anything --
|
|
470
|
+
* a diagnostic). There is deliberately **no default**: defaulting to zero would preserve
|
|
471
|
+
* every current number and leave all 67 corpus windings quietly wrong, which is how this
|
|
472
|
+
* arrived.
|
|
473
|
+
*/
|
|
474
|
+
readonly seriesResistanceOhms?: number | "unknown";
|
|
475
|
+
} | {
|
|
476
|
+
readonly kind: "diode";
|
|
477
|
+
readonly saturationCurrent: number;
|
|
478
|
+
readonly emissionCoefficient: number;
|
|
479
|
+
readonly thermalVoltage: number;
|
|
480
|
+
/**
|
|
481
|
+
* Reverse breakdown voltage in volts, or 0 for an ordinary diode.
|
|
482
|
+
*
|
|
483
|
+
* A zener's whole purpose is to conduct in reverse at a defined voltage, so without
|
|
484
|
+
* this a zener-regulated rail never clamps and floats to the supply instead. 25 of
|
|
485
|
+
* the corpus's diodes declare it, across 18 packets.
|
|
486
|
+
*/
|
|
487
|
+
readonly breakdownVolts: number;
|
|
488
|
+
/**
|
|
489
|
+
* Series bulk resistance in ohms (Rs). Real diodes have ohmic resistance (e.g. 0.05 Ohm for
|
|
490
|
+
* power rectifiers like 1N4007, 0.5-1.0 Ohm for signal diodes like 1N4148). Solved in
|
|
491
|
+
* closed form via Lambert-W junction split with no extra matrix unknowns.
|
|
492
|
+
*/
|
|
493
|
+
readonly seriesResistance?: number;
|
|
494
|
+
readonly isLed?: boolean;
|
|
495
|
+
} | {
|
|
496
|
+
readonly kind: "voltage-source";
|
|
497
|
+
readonly volts: number;
|
|
498
|
+
/** Series source impedance in ohms. See `SUPPLY_SOURCE_OHMS` in `device-laws.ts`. */
|
|
499
|
+
readonly sourceOhms: number;
|
|
500
|
+
} | {
|
|
501
|
+
/**
|
|
502
|
+
* A sinusoidal EMF at a declared frequency: a mains inlet, a filament winding, a
|
|
503
|
+
* signal generator. The DC law above with a frequency is **not** the same device,
|
|
504
|
+
* which is why this is a separate kind rather than an optional field.
|
|
505
|
+
*
|
|
506
|
+
* The name asymmetry is historical: the DC law is `voltage-source` because it was the
|
|
507
|
+
* only one, and the two stamps it lowers to are `dc-source` and `ac-source`.
|
|
508
|
+
*/
|
|
509
|
+
readonly kind: "ac-source";
|
|
510
|
+
/**
|
|
511
|
+
* Peak amplitude in volts, for both the runtime's `sin()` evaluation and the ngspice
|
|
512
|
+
* deck's `SIN` amplitude parameter (itself peak). The source states an RMS magnitude
|
|
513
|
+
* and a unit (`{value: 120, unit: "V"}`) — that reading is a project convention, not
|
|
514
|
+
* a prose read: the word "RMS" lives only in `raw`, which no stage may read, and
|
|
515
|
+
* mains is quoted RMS by universal convention. `device-laws.ts`'s `ac-source` branch
|
|
516
|
+
* of `voltage-source` converts the declared RMS magnitude to this peak amplitude via
|
|
517
|
+
* `AC_SOURCE_RMS_TO_PEAK`; see that branch for the full reasoning and the escape
|
|
518
|
+
* hatch for a packet that ever means peak.
|
|
519
|
+
*/
|
|
520
|
+
readonly amplitudeVolts: number;
|
|
521
|
+
readonly frequencyHz: number;
|
|
522
|
+
/** Series source impedance in ohms, exactly as the DC law carries it. */
|
|
523
|
+
readonly sourceOhms: number;
|
|
524
|
+
} | {
|
|
525
|
+
/**
|
|
526
|
+
* A switch is a conductance controlled by a 0..1 position, which is why it
|
|
527
|
+
* needs no new solver capability: it is just another controlled element.
|
|
528
|
+
* Modelling it this way keeps one document yielding one netlist, instead of
|
|
529
|
+
* compiling a separate program per switch combination.
|
|
530
|
+
*/
|
|
531
|
+
readonly kind: "switch";
|
|
532
|
+
readonly control: ControlId;
|
|
533
|
+
readonly onOhms: number;
|
|
534
|
+
readonly offOhms: number;
|
|
535
|
+
} | {
|
|
536
|
+
/**
|
|
537
|
+
* A switch with a common terminal and two or more throws, which routes
|
|
538
|
+
* rather than opens. Distinct from `switch`, whose two terminals only make
|
|
539
|
+
* or break: a selector's throws are alternatives, so the same control that
|
|
540
|
+
* closes one opens the others.
|
|
541
|
+
*/
|
|
542
|
+
readonly kind: "selector";
|
|
543
|
+
readonly control: ControlId;
|
|
544
|
+
readonly onOhms: number;
|
|
545
|
+
readonly offOhms: number;
|
|
546
|
+
} | {
|
|
547
|
+
/** Ebers-Moll, transport form. NPN or PNP by polarity. */
|
|
548
|
+
readonly kind: "bjt";
|
|
549
|
+
readonly polarity: "npn" | "pnp";
|
|
550
|
+
readonly saturationCurrent: number;
|
|
551
|
+
readonly forwardBeta: number;
|
|
552
|
+
readonly reverseBeta: number;
|
|
553
|
+
readonly thermalVoltage: number;
|
|
554
|
+
/**
|
|
555
|
+
* Excess collector-base leakage beyond the ideal junction, in amps, or 0.
|
|
556
|
+
*
|
|
557
|
+
* This is germanium's defining electrical difference and the reason a Fuzz Face
|
|
558
|
+
* biases at all: the leakage flowing out of the base through its bias resistor is
|
|
559
|
+
* what sets the operating point, which is also why those pedals drift with
|
|
560
|
+
* temperature. Ebers-Moll's ideal reverse current is the declared saturation
|
|
561
|
+
* current, around 1 nA, and a real germanium part leaks 100 to 1000 times that.
|
|
562
|
+
*/
|
|
563
|
+
readonly leakageAmps: number;
|
|
564
|
+
} | {
|
|
565
|
+
/**
|
|
566
|
+
* Shichman-Hodges, shared by JFET and MOSFET: they differ in the sign and
|
|
567
|
+
* magnitude of the threshold, not in the equations, so one law serves both
|
|
568
|
+
* rather than two nearly-identical code paths.
|
|
569
|
+
*/
|
|
570
|
+
readonly kind: "fet";
|
|
571
|
+
readonly channel: "n" | "p";
|
|
572
|
+
/** Pinch-off (JFET, negative) or threshold (enhancement MOSFET, positive). */
|
|
573
|
+
readonly thresholdVolts: number;
|
|
574
|
+
readonly transconductance: number;
|
|
575
|
+
readonly channelLengthModulation: number;
|
|
576
|
+
/**
|
|
577
|
+
* Subthreshold slope in volts (natural-log): below threshold the channel
|
|
578
|
+
* conducts `~exp(drive/subthresholdVolts)` instead of cutting dead. JFETs
|
|
579
|
+
* carry 0.07 (≈150 mV/decade, order of real junction subthreshold — an
|
|
580
|
+
* approximation validated by effect and parity, not a measured part
|
|
581
|
+
* number); MOSFETs carry 0 (insulated gate, unchanged behavior). Without
|
|
582
|
+
* it every threshold crossing is a brick wall, and a knee-biased JFET
|
|
583
|
+
* pumps audio-rate distortion alongside any sweep it produces.
|
|
584
|
+
*/
|
|
585
|
+
readonly subthresholdVolts: number;
|
|
586
|
+
/**
|
|
587
|
+
* Gate-junction conduction, the same softplus the triode's grid uses:
|
|
588
|
+
* `Ig = gateSaturationCurrent * log1p(exp((Vacross - onset)/scale))`, with
|
|
589
|
+
* voltages already channel-signed so one law covers n and p. The runtime
|
|
590
|
+
* evaluates it at both channel ends (`Vgs` and `Vgs - Vds`) because a JFET
|
|
591
|
+
* gate meets the source and the drain.
|
|
592
|
+
*
|
|
593
|
+
* **`gateSaturationCurrent: 0` disables it, and that is the correct value for a
|
|
594
|
+
* MOSFET** — an insulated gate draws no current at any bias, which is what this
|
|
595
|
+
* law did for every FET before. A *JFET's* gate is a real PN junction, and
|
|
596
|
+
* without it the solver can park at a forward-biased `Vgs` no physical part can
|
|
597
|
+
* hold: `boss-cs-2`'s `Q2` sat at `+0.350 V` and `boss-ds-2`'s `Q15` at
|
|
598
|
+
* `+0.596 V`, both with zero gate current, which made two switch diagnoses
|
|
599
|
+
* unreadable until the junction was added.
|
|
600
|
+
*
|
|
601
|
+
* On the law rather than as runtime constants for the reason the triode's grid
|
|
602
|
+
* states: when a gate starts drawing current is a property of the device.
|
|
603
|
+
*/
|
|
604
|
+
readonly gateSaturationCurrent: number;
|
|
605
|
+
readonly gateOnsetVolts: number;
|
|
606
|
+
readonly gateScaleVolts: number;
|
|
607
|
+
} | {
|
|
608
|
+
/**
|
|
609
|
+
* Koren's triode: `Ip = (2/kg1) * E1^ex` with
|
|
610
|
+
* `E1 = (Vpk/kp) * ln(1 + exp(kp * (1/mu + Vgk/sqrt(kvb + Vpk^2))))`.
|
|
611
|
+
*
|
|
612
|
+
* Five parameters and no part number, which is the same shape the generic BJT
|
|
613
|
+
* already has: the law is the device class, and a registry entry refines it.
|
|
614
|
+
*/
|
|
615
|
+
readonly kind: "triode";
|
|
616
|
+
readonly mu: number;
|
|
617
|
+
readonly kg1: number;
|
|
618
|
+
readonly kp: number;
|
|
619
|
+
readonly kvb: number;
|
|
620
|
+
readonly ex: number;
|
|
621
|
+
/**
|
|
622
|
+
* Grid conduction: `Ig = gridSaturationCurrent * log1p(exp((Vgk - onset)/scale))`.
|
|
623
|
+
*
|
|
624
|
+
* On the law rather than as runtime constants, because when a grid starts drawing
|
|
625
|
+
* current is a property of the tube. The runtime plan's own audit lists
|
|
626
|
+
* `OPAMP_OPEN_LOOP_GAIN` living in the engine as a defect — "a *device parameter*"
|
|
627
|
+
* — and this would be the same mistake.
|
|
628
|
+
*/
|
|
629
|
+
readonly gridSaturationCurrent: number;
|
|
630
|
+
readonly gridOnsetVolts: number;
|
|
631
|
+
readonly gridScaleVolts: number;
|
|
632
|
+
/**
|
|
633
|
+
* Contact potential, added to `Vgk` inside the Koren exponent.
|
|
634
|
+
*
|
|
635
|
+
* A standard term in this model that this law omitted. It is load-bearing, not a
|
|
636
|
+
* correction: the published Koren 12AX7 set reproduces RCA's typical point to +2% with
|
|
637
|
+
* `Vct = 0.5` and to **-58%** without it. A fit made against a law that has no `Vct`
|
|
638
|
+
* must absorb it by distorting the other coefficients, which is what an `ex` of 1.0
|
|
639
|
+
* and a `kvb` of 9707 look like. `0` reproduces the previous behaviour exactly.
|
|
640
|
+
*/
|
|
641
|
+
readonly contactPotentialVolts: number;
|
|
642
|
+
} | {
|
|
643
|
+
/**
|
|
644
|
+
* Koren again, but **screen-referenced**: the screen sets the cathode current and
|
|
645
|
+
* the plate only gates it, which is why a pentode's plate curves are flat where a
|
|
646
|
+
* triode's are not.
|
|
647
|
+
*
|
|
648
|
+
* E1 = (Vsk/kp) * ln(1 + exp(kp * (1/mu + Vgk/sqrt(kvb + Vsk^2))))
|
|
649
|
+
* Ip = (2/kg1) * E1^ex * atan(Vpk / kvb)
|
|
650
|
+
*
|
|
651
|
+
* Note `Vsk` where the triode has `Vpk`. Exactly the triode's five coefficients --
|
|
652
|
+
* `kvb` does double duty, conditioning the screen term and setting the plate knee.
|
|
653
|
+
*
|
|
654
|
+
* **`atan`, not `tanh`, and this was worth 26-34% of plate current.** The gate used to
|
|
655
|
+
* be `tanh` against a shared, invented 40 V knee, which saturates at 1.0 by ~150 V.
|
|
656
|
+
* Koren's pentode gates with `atan(Vp/Kvb)`, which reaches 1.34-1.52 at 250 V and keeps
|
|
657
|
+
* rising, and every catalogued fit was generated against it -- so `kg1` already carried
|
|
658
|
+
* that factor and `tanh` discarded it. Switching the gate moved five tubes from 26-34%
|
|
659
|
+
* low at their own datasheet point to within 1.5% for four of them, each by a factor
|
|
660
|
+
* equal to `atan(Va/kvb)` for its *own* `kvb`, which is why one shared correction could
|
|
661
|
+
* not have faked it. `scripts/check-tube-curves.ts` is the check.
|
|
662
|
+
*
|
|
663
|
+
* It is also the more physical shape: `tanh` flattens to a dead-level top, so plate
|
|
664
|
+
* resistance went to infinity and needed a `1e-12` conductance floor to keep the matrix
|
|
665
|
+
* non-singular. `atan` keeps a gentle finite slope above the knee, which is what a real
|
|
666
|
+
* pentode's plate curves do.
|
|
667
|
+
*/
|
|
668
|
+
readonly kind: "pentode";
|
|
669
|
+
readonly mu: number;
|
|
670
|
+
readonly kg1: number;
|
|
671
|
+
readonly kp: number;
|
|
672
|
+
readonly kvb: number;
|
|
673
|
+
readonly ex: number;
|
|
674
|
+
readonly gridSaturationCurrent: number;
|
|
675
|
+
readonly gridOnsetVolts: number;
|
|
676
|
+
readonly gridScaleVolts: number;
|
|
677
|
+
/**
|
|
678
|
+
* Contact potential, added to `Vgk` inside the Koren exponent.
|
|
679
|
+
*
|
|
680
|
+
* A standard term in this model that this law omitted. It is load-bearing, not a
|
|
681
|
+
* correction: the published Koren 12AX7 set reproduces RCA's typical point to +2% with
|
|
682
|
+
* `Vct = 0.5` and to **-58%** without it. A fit made against a law that has no `Vct`
|
|
683
|
+
* must absorb it by distorting the other coefficients, which is what an `ex` of 1.0
|
|
684
|
+
* and a `kvb` of 9707 look like. `0` reproduces the previous behaviour exactly.
|
|
685
|
+
*/
|
|
686
|
+
readonly contactPotentialVolts: number;
|
|
687
|
+
/**
|
|
688
|
+
* The screen's share of the plate current, as a fraction. The screen draws a SHARE of
|
|
689
|
+
* the conduction, never a constant: a fixed Ig2 cannot be supplied through a
|
|
690
|
+
* high-impedance feed (245 V through 4.7 M is 52 uA at most), so it drove the screen to
|
|
691
|
+
* -2575 V and the solver from 3 to 1024 Newton iterations with 100 % held samples. The
|
|
692
|
+
* share is the 5:1 plate:screen split at the datasheet point (Ig2 0.6 mA / Ia 3.0 mA,
|
|
693
|
+
* i.e. the screen's 1/6 share of the total cathode current), so `screenShare = 0.2`
|
|
694
|
+
* gives `screenCurrent = plateCurrent * 0.2`. Self-consistent and degrading gracefully:
|
|
695
|
+
* as the screen sags, conduction falls, the share falls, the feed drop falls, and the
|
|
696
|
+
* loop settles instead of diverging. `0` or absent reproduces today's behaviour (no
|
|
697
|
+
* screen<->cathode source). Present only where a datasheet figure exists.
|
|
698
|
+
*/
|
|
699
|
+
readonly screenShare?: number;
|
|
700
|
+
} | {
|
|
701
|
+
/**
|
|
702
|
+
* Child-Langmuir space charge: `Ia = perveance * max(Vak, 0)^exponent`, and nothing in
|
|
703
|
+
* reverse.
|
|
704
|
+
*
|
|
705
|
+
* Two parameters where the triode has eight, because a rectifier has no grid to
|
|
706
|
+
* control it: what it does is drop a voltage that depends on the current drawn through
|
|
707
|
+
* it, which is the whole of tube-rectifier sag.
|
|
708
|
+
*/
|
|
709
|
+
readonly kind: "tube-diode";
|
|
710
|
+
/** `K` in amps per volt^exponent. Datasheet-anchored; see `GENERIC_TUBE_DIODE`. */
|
|
711
|
+
readonly perveance: number;
|
|
712
|
+
/**
|
|
713
|
+
* The space-charge exponent, 3/2.
|
|
714
|
+
*
|
|
715
|
+
* Carried on the law rather than hardcoded in the executor for the reason the runtime
|
|
716
|
+
* plan's audit names: a device parameter living in the engine is the
|
|
717
|
+
* `OPAMP_OPEN_LOOP_GAIN` defect. It is physics rather than a fit, and it is **held
|
|
718
|
+
* constant far from the anchor**, which is a known limitation of this law rather than a
|
|
719
|
+
* knob — a wide-tolerance datasheet cross-check on a GZ34 recorded ~22% drift from a
|
|
720
|
+
* constant exponent far from its anchor current.
|
|
721
|
+
*/
|
|
722
|
+
readonly exponent: number;
|
|
723
|
+
} | {
|
|
724
|
+
/** Ideal transformer: V_primary = ratio * V_secondary, power conserved. */
|
|
725
|
+
readonly kind: "transformer";
|
|
726
|
+
/**
|
|
727
|
+
* Winding DC resistance, ohms, per winding. Three states exactly as `inductance`:
|
|
728
|
+
* a number, `"unknown"` when the packet states the source does not print it, and
|
|
729
|
+
* absent. **No default** -- zero is physically impossible for a winding, and
|
|
730
|
+
* defaulting to it is how every corpus transformer came to be ideal.
|
|
731
|
+
*/
|
|
732
|
+
readonly seriesResistanceOhms?: number | "unknown";
|
|
733
|
+
/**
|
|
734
|
+
* The single declared ratio, from a source `Ratio` or a typed impedance pair.
|
|
735
|
+
* Applied uniformly to every secondary winding by `transformerWindings` -- exact
|
|
736
|
+
* for a transformer with one secondary (tapped or not), and the only path for a
|
|
737
|
+
* transformer whose primary carries signal, which is every output transformer
|
|
738
|
+
* and every reverb tank.
|
|
739
|
+
*/
|
|
740
|
+
readonly turnsRatio: number;
|
|
741
|
+
/**
|
|
742
|
+
* Each declared coil's own EMF as a **peak** amplitude in volts, **one entry per
|
|
743
|
+
* declared winding in declaration order**, `null` where the source states no
|
|
744
|
+
* voltage for that coil. Peak rather than RMS for the same reason `ac-source`'s
|
|
745
|
+
* `amplitudeVolts` is, and converted in the same one place: the source states
|
|
746
|
+
* RMS, `device-laws.ts` applies `sqrt(2)` once.
|
|
747
|
+
*
|
|
748
|
+
* **Positional, not keyed by winding class.** It was a `Record` keyed on a class
|
|
749
|
+
* name, and a class holds one value: `orange-rockerverb`'s two filament coils
|
|
750
|
+
* collapsed into one entry and its 3.15-0-3.15 V winding was driven at 6 V per
|
|
751
|
+
* half. Index alignment with `identity.declaredWindings` is by construction --
|
|
752
|
+
* both are built from the same array in the same order.
|
|
753
|
+
*
|
|
754
|
+
* Contains a non-null entry only for a transformer the source specifies by its
|
|
755
|
+
* **coil voltages** rather than by a ratio, which in practice is a mains power
|
|
756
|
+
* transformer. Then `turnsRatio` is meaningless and unused: the coils are lowered
|
|
757
|
+
* as independent `ac-source` EMFs and no primary is modelled at all. See
|
|
758
|
+
* `transformerWindings` in `transformer.ts` for what that models and what it
|
|
759
|
+
* cannot.
|
|
760
|
+
*
|
|
761
|
+
* A value is exact whether or not the coil is centre-tapped -- it is the voltage
|
|
762
|
+
* across the pair a stamp uses, which for a tapped coil is the per-half figure by
|
|
763
|
+
* the corpus's own convention (`370-0-370` is stated as `370`), so it is applied
|
|
764
|
+
* to each half directly and never doubled.
|
|
765
|
+
*/
|
|
766
|
+
readonly windingAmplitudeVolts: readonly (number | null)[];
|
|
767
|
+
/**
|
|
768
|
+
* The mains frequency in Hz the driven windings run at, or `null` when the
|
|
769
|
+
* document states none. Only read when `windingAmplitudeVolts` is non-empty, and
|
|
770
|
+
* a `null` there is a refusal rather than a default: a winding voltage with no
|
|
771
|
+
* frequency is not a waveform.
|
|
772
|
+
*/
|
|
773
|
+
readonly mainsFrequencyHz: number | null;
|
|
774
|
+
/** Series impedance of each driven winding, in ohms. See `SUPPLY_SOURCE_OHMS`. */
|
|
775
|
+
readonly windingSourceOhms: number;
|
|
776
|
+
} | {
|
|
777
|
+
readonly kind: "open";
|
|
778
|
+
} | {
|
|
779
|
+
/**
|
|
780
|
+
* Operational Transconductance Amplifier (OTA): output current is transconductance
|
|
781
|
+
* times the input voltage difference: I_out = gm * (V_in+ - V_in-).
|
|
782
|
+
*/
|
|
783
|
+
readonly kind: "ota";
|
|
784
|
+
readonly transconductance: number;
|
|
785
|
+
readonly saturationCurrent?: number;
|
|
786
|
+
readonly thermalVoltage?: number;
|
|
787
|
+
/**
|
|
788
|
+
* Index of the negative supply terminal.
|
|
789
|
+
*
|
|
790
|
+
* Which array it indexes depends on which path built this law, and the two
|
|
791
|
+
* frames are deliberately different: a `sections` catalog entry indexes the
|
|
792
|
+
* section's *own* mapped terminals, while the role-bound path below indexes
|
|
793
|
+
* the device's full declared terminal list. Each path sets it in its own
|
|
794
|
+
* frame, so never move a `veeIndex` between them.
|
|
795
|
+
*/
|
|
796
|
+
readonly veeIndex?: number;
|
|
797
|
+
/**
|
|
798
|
+
* Which declared terminals carry the signal pins, when they were bound by
|
|
799
|
+
* role rather than by position.
|
|
800
|
+
*
|
|
801
|
+
* Absent means "index positionally", which is what a `sections` catalog entry
|
|
802
|
+
* wants: it has already reordered the section's terminals, so 0/1/2/3 are
|
|
803
|
+
* correct there. Present means the device named its own terminals and those
|
|
804
|
+
* names were matched as whole tokens.
|
|
805
|
+
*
|
|
806
|
+
* `bias: null` is a positive statement, not a missing value: the device
|
|
807
|
+
* declares no amplifier-bias terminal, so this is a linear OTA. Falling back
|
|
808
|
+
* to position 3 there is what silently bound `moogerfooger-mf-102`'s bias to
|
|
809
|
+
* its positive supply rail.
|
|
810
|
+
*/
|
|
811
|
+
readonly terminalIndices?: {
|
|
812
|
+
readonly plus: number;
|
|
813
|
+
readonly minus: number;
|
|
814
|
+
readonly output: number;
|
|
815
|
+
readonly bias: number | null;
|
|
816
|
+
};
|
|
817
|
+
} | {
|
|
818
|
+
/** CMOS Inverter Gate: complementary MOSFET pair or transconductance current injection. */
|
|
819
|
+
readonly kind: "inverter";
|
|
820
|
+
readonly transconductance: number;
|
|
821
|
+
readonly biasVolts?: number;
|
|
822
|
+
readonly thresholdVolts?: number;
|
|
823
|
+
} | {
|
|
824
|
+
/** CMOS NAND Logic Gate: output is high (VDD) if not both inputs are high. */
|
|
825
|
+
readonly kind: "nand-gate";
|
|
826
|
+
readonly thresholdVolts: number;
|
|
827
|
+
readonly transconductance: number;
|
|
828
|
+
} | {
|
|
829
|
+
/**
|
|
830
|
+
* Linear-control VCA gain cell (M5207L01): output current is input
|
|
831
|
+
* current times max(minGain, (Vcontrol - Vminus) / vrefVolts),
|
|
832
|
+
* halved once: the datasheet's 0 dB condition (unity gain iff
|
|
833
|
+
* Vc = Vref AND Ro = 2*Ri) forces the cell current ratio to 1/2
|
|
834
|
+
* at Vc = Vref.
|
|
835
|
+
*
|
|
836
|
+
* The control input is voltage-sense only: unlike the `ota` law's
|
|
837
|
+
* bias diode it draws no current from the control node, which is
|
|
838
|
+
* what lets a high-impedance depth network drive it (the M5207L01
|
|
839
|
+
* datasheet specifies about 100 nA of control bias). The control
|
|
840
|
+
* is referenced to the minus input, which is the part's COM in
|
|
841
|
+
* single-supply use. Per-part numbers (Vref, input impedance,
|
|
842
|
+
* floor) live in the catalog entry, never here.
|
|
843
|
+
*/
|
|
844
|
+
readonly kind: "linear-vca";
|
|
845
|
+
/** Input resistance to the minus terminal, siemens. */
|
|
846
|
+
readonly inputSiemens: number;
|
|
847
|
+
/** Control volts for unity current gain (1 V for M5207L01). */
|
|
848
|
+
readonly vrefVolts: number;
|
|
849
|
+
/** Gain floor below cutoff (0 = mute, the -100 dB the ear hears). */
|
|
850
|
+
readonly minGain: number;
|
|
851
|
+
} | {
|
|
852
|
+
/** BBD Clock Driver: propagates the slowly-varying LFO control rate. */
|
|
853
|
+
readonly kind: "clock-driver";
|
|
854
|
+
readonly defaultFrequency?: number;
|
|
855
|
+
} | {
|
|
856
|
+
/** Optocoupler / Vactrol: input LED and output light-dependent resistor (LDR) */
|
|
857
|
+
readonly kind: "optocoupler";
|
|
858
|
+
readonly ledThresholdVolts: number;
|
|
859
|
+
readonly ledTransconductance: number;
|
|
860
|
+
readonly ldrMinOhms: number;
|
|
861
|
+
readonly ldrMaxOhms: number;
|
|
862
|
+
/**
|
|
863
|
+
* A part-specific LED-current -> LDR-resistance curve, `ohms = coefficient *
|
|
864
|
+
* amps^exponent`, clamped to `[ldrMinOhms, ldrMaxOhms]`. Both fields must be present
|
|
865
|
+
* together or both absent. Absent (the default for every part except one with a cited
|
|
866
|
+
* datasheet curve) keeps the existing `ldrMinOhms + (ldrMaxOhms-ldrMinOhms) *
|
|
867
|
+
* exp(-1000*amps)` law bit-identical -- see `runoffgroove-tremulus-lune`'s VTL5C2 entry
|
|
868
|
+
* in `part-catalog.ts` for the one part currently carrying this fit.
|
|
869
|
+
*/
|
|
870
|
+
readonly ldrPowerLawCoefficientOhms?: number;
|
|
871
|
+
readonly ldrPowerLawExponent?: number;
|
|
872
|
+
} | {
|
|
873
|
+
/**
|
|
874
|
+
* Op-amp: the output is driven so that v(+) == v(-), until it reaches a
|
|
875
|
+
* supply rail and saturates. The rails are not a part fact and do not need
|
|
876
|
+
* a part number -- they come from the supplies present in the circuit, so
|
|
877
|
+
* an unlabelled op-amp still clips correctly.
|
|
878
|
+
*/
|
|
879
|
+
readonly kind: "ideal-opamp";
|
|
880
|
+
readonly railHigh: number | null;
|
|
881
|
+
readonly railLow: number | null;
|
|
882
|
+
/**
|
|
883
|
+
* Open-loop DC gain, dimensionless.
|
|
884
|
+
*
|
|
885
|
+
* A class default of 1e5 was used for every op-amp in the corpus while 31 of 52 declared
|
|
886
|
+
* something else -- 200k on 21 of them, 300k on five, and 3000 on one. Every measurement of
|
|
887
|
+
* op-amp stiffness, the saturation band and the step limiter was taken at the default.
|
|
888
|
+
*/
|
|
889
|
+
readonly openLoopGain: number;
|
|
890
|
+
/**
|
|
891
|
+
* How far the output stops short of each supply rail, in volts. **A drop from the rail,
|
|
892
|
+
* not an absolute swing**: the output clamps to `railLow + drop .. railHigh - drop`, so the
|
|
893
|
+
* same figure is right on a 9 V pedal and an 18 V one. Absent means the part states none
|
|
894
|
+
* and the rails are used as they are. The pedal still owns the rails; the part owns only how
|
|
895
|
+
* close to them it gets. Symmetric, so it cannot say the LM358/LM324's asymmetric swing to
|
|
896
|
+
* ground; those parts state none.
|
|
897
|
+
*/
|
|
898
|
+
readonly outputSwingDropVolts?: number;
|
|
899
|
+
} | {
|
|
900
|
+
readonly kind: "logic-divider";
|
|
901
|
+
readonly thresholdVolts: number;
|
|
902
|
+
readonly highVolts: number;
|
|
903
|
+
} | {
|
|
904
|
+
readonly kind: "analog-switch";
|
|
905
|
+
readonly onOhms: number;
|
|
906
|
+
readonly offOhms: number;
|
|
907
|
+
readonly thresholdVolts: number;
|
|
908
|
+
} | {
|
|
909
|
+
/**
|
|
910
|
+
* One channel of an NE570/571-class compandor, as the datasheet's own block
|
|
911
|
+
* diagram draws it (onsemi NE570/D Rev. 4, Figure 5).
|
|
912
|
+
*
|
|
913
|
+
* **This law describes the chip, not one of its application circuits.** The
|
|
914
|
+
* datasheet builds an expander (Figure 6) and a compressor (Figure 7) from the
|
|
915
|
+
* same silicon -- "a compressor is essentially an expander placed in the
|
|
916
|
+
* feedback loop of the op amp" -- so the compression *direction* is a property
|
|
917
|
+
* of how the packet wires these pins, never of this law. An earlier version
|
|
918
|
+
* hard-coded gain proportional to `1/envelope`, which is to say it hard-coded
|
|
919
|
+
* the compressor: a packet wired as Figure 7 then compressed twice, and one
|
|
920
|
+
* wired as Figure 6 expanded not at all.
|
|
921
|
+
*
|
|
922
|
+
* All five resistors are internal to the chip and all are read from Figure 5,
|
|
923
|
+
* where each is drawn with its value. They are fields rather than constants
|
|
924
|
+
* because `R3` and `R4` are the two the datasheet explicitly invites the
|
|
925
|
+
* designer to change ("External resistors may be placed in series with R3 ...
|
|
926
|
+
* or in parallel with R4").
|
|
927
|
+
*/
|
|
928
|
+
readonly kind: "compandor";
|
|
929
|
+
/** Rectifier input resistor, 10 k. `IG = 2 * VIN(avg) / R1` (Figure 9). */
|
|
930
|
+
readonly r1: number;
|
|
931
|
+
/** G-cell input resistor, 20 k. `IIN = VIN / R2` (Figure 12). */
|
|
932
|
+
readonly r2: number;
|
|
933
|
+
/** Internal op-amp feedback resistor, 20 k, brought out on pin 6/11. */
|
|
934
|
+
readonly r3: number;
|
|
935
|
+
/**
|
|
936
|
+
* Summing-node bias resistor, 30 k, to **ground** rather than to VREF.
|
|
937
|
+
*
|
|
938
|
+
* Ground is not a guess: the datasheet states the expander output "will bias
|
|
939
|
+
* to 3.0 V" via `VOUT_DC = (1 + R3/R4) * VREF = (1 + 20k/30k) * 1.8 V`. That
|
|
940
|
+
* is the non-inverting-amplifier form, which only holds with R4 to ground --
|
|
941
|
+
* returned to VREF it would carry no current and leave the output at 1.8 V.
|
|
942
|
+
* The stated 3.0 V is therefore an arithmetic check on the topology, and it
|
|
943
|
+
* is asserted as one.
|
|
944
|
+
*/
|
|
945
|
+
readonly r4: number;
|
|
946
|
+
/**
|
|
947
|
+
* Rectifier averaging resistor, 10 k (Figure 8's `R5`).
|
|
948
|
+
*
|
|
949
|
+
* With the **external** CRECT on pin 1/16 this sets the detector time
|
|
950
|
+
* constant, `tau = R5 * CRECT`. Nothing here fixes that time constant: the
|
|
951
|
+
* capacitor is a real component in the packet and the MNA solve integrates
|
|
952
|
+
* it, which is the whole reason the chip brings the node out to a pin.
|
|
953
|
+
*/
|
|
954
|
+
readonly r5: number;
|
|
955
|
+
/** The G cell's reference current `I1`, 140 uA (Figure 12's `IB`). */
|
|
956
|
+
readonly iBias: number;
|
|
957
|
+
/** Internal band-gap reference, 1.8 V. The op-amp's non-inverting input. */
|
|
958
|
+
readonly vrefVolts: number;
|
|
959
|
+
/** Open-loop DC gain of the internal op-amp. */
|
|
960
|
+
readonly openLoopGain: number;
|
|
961
|
+
/**
|
|
962
|
+
* Supply rails for the internal op-amp, null in the registry and substituted by
|
|
963
|
+
* stage 3 from the package's own supply pins -- the same treatment a declared
|
|
964
|
+
* `ideal-opamp` section gets, and for the same reason: a registry describes a
|
|
965
|
+
* part, and the rails belong to the pedal it is fitted to.
|
|
966
|
+
*
|
|
967
|
+
* Not cosmetic. In the Figure 7 compressor the gain cell is the *only* AC
|
|
968
|
+
* feedback -- the external network is a T whose midpoint is shunted to ground,
|
|
969
|
+
* so it feeds back at DC and not at audio. Gain is therefore set by the cell
|
|
970
|
+
* alone and is unbounded until the detector charges, `R5 * CRECT` = 2.2 ms on
|
|
971
|
+
* an SA571 with the datasheet's 0.22 uF. Left unrailed, `mxr-carbon-copy`'s
|
|
972
|
+
* compressor answered a 2 ms burst with a **195 V** excursion out of a part
|
|
973
|
+
* running on 9 V, and drove 8 V of AC into a BBD input.
|
|
974
|
+
*/
|
|
975
|
+
readonly railHigh: number | null;
|
|
976
|
+
readonly railLow: number | null;
|
|
977
|
+
} | {
|
|
978
|
+
/**
|
|
979
|
+
* Spring reverb tank -- a **mechanical** delay medium, not a circuit.
|
|
980
|
+
*
|
|
981
|
+
* This is the same category as the BBD die: no arrangement of R, L and C in the
|
|
982
|
+
* netlist produces it, so MNA cannot make it emerge and it needs its own operator.
|
|
983
|
+
* A tank was previously admitted as a two-winding `transformer` (its declared
|
|
984
|
+
* `Circuit.Transformer` shell with `InputImpedance`/`OutputImpedance`), which is
|
|
985
|
+
* electrically the right *interface* and acoustically silent: an ideal transformer
|
|
986
|
+
* is memoryless, so send-to-return was a wire with a turns ratio and every Fender
|
|
987
|
+
* reverb amp rendered its reverb control as a mix of a signal with no reverb in it.
|
|
988
|
+
*
|
|
989
|
+
* The parameters are device facts keyed to an exact part number, the same standing
|
|
990
|
+
* `part-catalog.ts` entries have. Accutronics/Belton codes state them directly:
|
|
991
|
+
* `4AB3C1B` is input code `A` (8 Ohm), output code `B` (2250 Ohm), decay code `3`
|
|
992
|
+
* (long, 2.75-4.0 s). The impedances are cross-checked against the packet's own
|
|
993
|
+
* declared `InputImpedance`/`OutputImpedance` rather than replacing them.
|
|
994
|
+
*/
|
|
995
|
+
readonly kind: "spring-reverb";
|
|
996
|
+
readonly inputOhms: number;
|
|
997
|
+
readonly outputOhms: number;
|
|
998
|
+
/** Transducer-to-transducer transit time of the longest spring. */
|
|
999
|
+
readonly delaySeconds: number;
|
|
1000
|
+
/** Time to -60 dB, from the part number's decay code. */
|
|
1001
|
+
readonly decaySeconds: number;
|
|
1002
|
+
/**
|
|
1003
|
+
* Allpass sections per spring. A real spring is dispersive -- high frequencies
|
|
1004
|
+
* travel faster than low -- which is what makes the "boing" rather than an echo.
|
|
1005
|
+
*/
|
|
1006
|
+
readonly dispersionStages: number;
|
|
1007
|
+
} | {
|
|
1008
|
+
readonly kind: "comparator";
|
|
1009
|
+
readonly pullDownOhms: number;
|
|
1010
|
+
readonly floatOhms: number;
|
|
1011
|
+
readonly sensitivity: number;
|
|
1012
|
+
};
|
|
1013
|
+
/** A region the compiler does not solve. Exists only when the registry fills it. */
|
|
1014
|
+
export type MacroModel = {
|
|
1015
|
+
/**
|
|
1016
|
+
* Which DSP algorithm executes this region — **not which chip this is.**
|
|
1017
|
+
*
|
|
1018
|
+
* A runtime implements a closed set of algorithms and refuses any other by name (see
|
|
1019
|
+
* `IMPLEMENTED_MODELS` in `src/runtime/reference-runtime.ts`); a registry entry names one of
|
|
1020
|
+
* them and carries the part's own numbers in `parameters`. That split is what keeps the
|
|
1021
|
+
* console/ROM invariant true for *parts* as well as for stamps: an MN3007 and an MN3005 are
|
|
1022
|
+
* both a bucket-brigade delay line differing in `stages`, so adding either is registry data
|
|
1023
|
+
* and no runtime change — while a PT2399's digital core is a different algorithm, and
|
|
1024
|
+
* legitimately does need runtime work before it can be heard. (A compander or an OTA is
|
|
1025
|
+
* not an example: both are analog device laws — `compandor`, `ota` — stamped into the
|
|
1026
|
+
* MNA solve, not `modelId`s. The only `modelId: "compander"` in the tree is the fixture
|
|
1027
|
+
* registry's deliberately unimplemented negative control.)
|
|
1028
|
+
*
|
|
1029
|
+
* Deliberately `string` and not a union declared here. The vocabulary belongs to whichever
|
|
1030
|
+
* runtime is executing rather than to this compiler: a second implementer (C++, WASM,
|
|
1031
|
+
* ESP32) holds a shorter list than the reference runtime, and a `decode`d program's
|
|
1032
|
+
* `modelId` is untrusted text from another producer. A closed union here would make the
|
|
1033
|
+
* runtime's check read as vacuous — the same reasoning `unimplementedOperators` records for
|
|
1034
|
+
* taking `readonly string[]`.
|
|
1035
|
+
*/
|
|
1036
|
+
readonly modelId: string;
|
|
1037
|
+
readonly parameters: Readonly<Record<string, number>>;
|
|
1038
|
+
/** Port index -> terminal index on the device. `portTerminals[0]` is audio in, `[1]` audio out. */
|
|
1039
|
+
readonly portTerminals: readonly number[];
|
|
1040
|
+
/**
|
|
1041
|
+
* The `coupled` port's boundary impedances (build-order step 1, spec clause 2): `input` is
|
|
1042
|
+
* the admittance this macro presents to its driver (stamped as an ordinary `conductance`
|
|
1043
|
+
* into the driver's own block), `output` is the source impedance behind its write-back
|
|
1044
|
+
* (stamped as a `macro-audio-source`, which carries the same `sourceOhms` shape
|
|
1045
|
+
* `dc-source` already does). `null` when the part declares no audio boundary at all, which
|
|
1046
|
+
* keeps the port absent rather than defaulted to an ideal (zero-impedance) one nothing in
|
|
1047
|
+
* a registry ever measured.
|
|
1048
|
+
*/
|
|
1049
|
+
readonly audioPortImpedanceOhms: {
|
|
1050
|
+
readonly input: number;
|
|
1051
|
+
readonly output: number;
|
|
1052
|
+
} | null;
|
|
1053
|
+
/**
|
|
1054
|
+
* Device terminal index whose node feeds the `parameter` port's derivation (a clock
|
|
1055
|
+
* network setting a delay time), or `null` when this part has none. Distinct from
|
|
1056
|
+
* `portTerminals`, which is audio only.
|
|
1057
|
+
*/
|
|
1058
|
+
/**
|
|
1059
|
+
* The source node an LFO reaches a modulated BBD clock on, resolved by
|
|
1060
|
+
* `resolveClockModulationSource`, with the device that steers it.
|
|
1061
|
+
*
|
|
1062
|
+
* A **source node**, not a terminal index, because unlike `parameterTerminal` this node is
|
|
1063
|
+
* not on the delay device at all -- it is the control terminal of a transistor elsewhere in
|
|
1064
|
+
* the clock circuit. `couple.ts` maps it to a block row.
|
|
1065
|
+
*/
|
|
1066
|
+
readonly modulationNode?: number | null;
|
|
1067
|
+
readonly modulationSteeredBy?: string | null;
|
|
1068
|
+
/**
|
|
1069
|
+
* The source node the open-OX2 clock law reads (the slow diode's
|
|
1070
|
+
* anode-side divider node), with the discharge transistor, resolved by
|
|
1071
|
+
* `deriveOpenOx2ClockLaw`. A **source node**, like `modulationNode`:
|
|
1072
|
+
* `couple.ts` maps it to a block row.
|
|
1073
|
+
*/
|
|
1074
|
+
readonly clockLawNode?: number | null;
|
|
1075
|
+
readonly clockLawSteeredBy?: string | null;
|
|
1076
|
+
/** The open-OX2 law constants, or absent when the form was not recognised. */
|
|
1077
|
+
readonly clockLawParams?: ClockLawParams | null;
|
|
1078
|
+
readonly parameterTerminal: number | null;
|
|
1079
|
+
/**
|
|
1080
|
+
* The clock network's own nominal reference voltage -- part-intrinsic data, like the
|
|
1081
|
+
* impedances above, not a fact `couple.ts` could derive from the netlist. `null` exactly
|
|
1082
|
+
* when `parameterTerminal` is `null`.
|
|
1083
|
+
*/
|
|
1084
|
+
readonly parameterReferenceVolts: number | null;
|
|
1085
|
+
/**
|
|
1086
|
+
* Clock-governed control binding (Phase 4): when the delay macro's clock network contains
|
|
1087
|
+
* a potentiometer or rheostat bound to a control, this carries the physical parameters
|
|
1088
|
+
* so the runtime can dynamically evaluate the live delay time as the knob sweeps.
|
|
1089
|
+
*/
|
|
1090
|
+
readonly clockControl?: MacroClockControl | null;
|
|
1091
|
+
/**
|
|
1092
|
+
* Whether `parameters.delaySeconds` was DERIVED from the clock network in the packet, or
|
|
1093
|
+
* DECLARED as a `DelayMs` property and copied through. `null` for a macro whose model has no
|
|
1094
|
+
* delay at all. `clock-law` is the third state: the static base is declared (it sizes the
|
|
1095
|
+
* line and seeds the first samples) while the sweep is derived per sample by the open-OX2
|
|
1096
|
+
* law, so neither "derived" (the number is not) nor "declared" (the behaviour is not) is true.
|
|
1097
|
+
*
|
|
1098
|
+
* Carried so a consumer can tell a simulated delay from a reported one without re-deriving
|
|
1099
|
+
* it, and so `compile` can warn about the second without refusing it.
|
|
1100
|
+
*/
|
|
1101
|
+
readonly delayProvenance?: "derived" | "declared" | "clock-law" | null;
|
|
1102
|
+
/** Why derivation did not happen, when `delayProvenance` is `declared`. */
|
|
1103
|
+
readonly delayDeclaredReason?: string | null;
|
|
1104
|
+
/**
|
|
1105
|
+
* The programs a *declared* composition runs, already checked against the op
|
|
1106
|
+
* vocabulary, or absent for every macro the registry supplied.
|
|
1107
|
+
*
|
|
1108
|
+
* Carried on the macro rather than emitted as a composed block directly, because
|
|
1109
|
+
* `couple.ts` runs between lowering and composition and is what wires a macro's
|
|
1110
|
+
* audio and parameter ports across blocks. A declaration needs exactly that
|
|
1111
|
+
* wiring, so it travels the same road and composition stays the last step.
|
|
1112
|
+
*/
|
|
1113
|
+
readonly declaredPositions?: readonly ComposedPosition[];
|
|
1114
|
+
/** The router a declared program plans, or absent for every registry macro. */
|
|
1115
|
+
readonly declaredRouter?: DeclaredRouterPlan | null;
|
|
1116
|
+
/** The cited parameter control's node, planned the same way. */
|
|
1117
|
+
readonly declaredParameter?: DeclaredParameterPlan | null;
|
|
1118
|
+
};
|
|
1119
|
+
export type MacroClockControl = {
|
|
1120
|
+
readonly controlId: ControlId;
|
|
1121
|
+
readonly taper: TaperKind;
|
|
1122
|
+
readonly ohmsAtControlMin: number;
|
|
1123
|
+
readonly ohmsAtControlMax: number;
|
|
1124
|
+
readonly farads: number;
|
|
1125
|
+
readonly stages: number;
|
|
1126
|
+
readonly formulaConstant: number;
|
|
1127
|
+
/**
|
|
1128
|
+
* Additive intercept (seconds) of the knob-to-delay mapping, evaluated as
|
|
1129
|
+
* `delay = offsetSeconds + stages * formulaConstant * R * farads` everywhere
|
|
1130
|
+
* either runtime reads it. Laws through the origin set 0; the PT2399's
|
|
1131
|
+
* `29.7 ms + 11.46 ms * R_kOhm` sets 0.0297. Required, never defaulted.
|
|
1132
|
+
*/
|
|
1133
|
+
readonly offsetSeconds: number;
|
|
1134
|
+
};
|
|
1135
|
+
/**
|
|
1136
|
+
* Why an identification-requiring device was given an `open` law.
|
|
1137
|
+
*
|
|
1138
|
+
* Recorded at the one place that knows -- the branch in `resolveDevice` that chose it -- rather
|
|
1139
|
+
* than re-derived by the reporter from the same predicates. That is deliberate and it is the
|
|
1140
|
+
* lesson of two separate defects on 2026-09-03: a reporter that asks its own copy of the
|
|
1141
|
+
* resolver's question drifts from it silently, and the drift shows up as a device wired one way
|
|
1142
|
+
* and reported another. Here the reporter reads the decision instead of repeating it.
|
|
1143
|
+
*
|
|
1144
|
+
* `electrically-isolated` has its own dedicated warning (`electrically-isolated-ic`) and is
|
|
1145
|
+
* tagged only so `findIcsNotExecuted` can exclude it by the decision rather than by guessing.
|
|
1146
|
+
*/
|
|
1147
|
+
export type OpenIcReason = "source-boundary-shell" | "registry-arity-mismatch" | "electrically-isolated" | "charge-pump-declared-rail" | "registry-open" | "declared-class-without-model";
|
|
1148
|
+
/**
|
|
1149
|
+
* Which phase-3 bucket an unmodelled core belongs in (board-p3 row 1).
|
|
1150
|
+
*
|
|
1151
|
+
* `engineGap` used to mean "an IC was dropped"; phase 3 means "a programmable
|
|
1152
|
+
* chip has no program". Those are not the same set: a fixed-function support
|
|
1153
|
+
* chip behind an executing macro (`ibanez-dl5`'s 4164 DRAM) and a redundant
|
|
1154
|
+
* view of already-modelled behaviour (`boss-oc-2`'s single-terminal divider
|
|
1155
|
+
* placeholder) are phase-1 rows wearing the wrong flag. Only the first class
|
|
1156
|
+
* builds the phase-3 set.
|
|
1157
|
+
*/
|
|
1158
|
+
export type OpenIcGapClass = "programmable-no-program" | "support-chip-subsumed" | "part-number-miss";
|
|
1159
|
+
export type DeviceResolution = {
|
|
1160
|
+
readonly outcome: "law";
|
|
1161
|
+
readonly device: DeviceId;
|
|
1162
|
+
readonly law: DeviceLaw;
|
|
1163
|
+
/**
|
|
1164
|
+
* Set only on an `open` law for an `ic`/`power-amp`. Absent everywhere else, including
|
|
1165
|
+
* on the `open` a ground symbol or an unwired jack contact gets -- those carry no
|
|
1166
|
+
* element because the symbol has none, which is not a hole in the model.
|
|
1167
|
+
*/
|
|
1168
|
+
readonly openReason?: OpenIcReason;
|
|
1169
|
+
/**
|
|
1170
|
+
* For `declared-class-without-model`: the refusal this `open` replaced, verbatim.
|
|
1171
|
+
*
|
|
1172
|
+
* Carried so the warning is actionable. "No model bound" is a fact; *which* lookup
|
|
1173
|
+
* failed and how is what tells a reader whether the fix is a registry entry, a pinout,
|
|
1174
|
+
* or a packet correction.
|
|
1175
|
+
*/
|
|
1176
|
+
readonly insteadOfRefusal?: string;
|
|
1177
|
+
} | {
|
|
1178
|
+
readonly outcome: "macro";
|
|
1179
|
+
readonly device: DeviceId;
|
|
1180
|
+
readonly macro: MacroModel;
|
|
1181
|
+
} | {
|
|
1182
|
+
readonly outcome: "unsupported";
|
|
1183
|
+
readonly device: DeviceId;
|
|
1184
|
+
readonly reason: string;
|
|
1185
|
+
};
|
|
1186
|
+
export type LawedNetlist = {
|
|
1187
|
+
readonly netlist: Netlist;
|
|
1188
|
+
readonly resolutions: readonly DeviceResolution[];
|
|
1189
|
+
};
|
|
1190
|
+
export type RegionKind = "linear" | "nonlinear" | "macro";
|
|
1191
|
+
export type Region = {
|
|
1192
|
+
readonly id: string;
|
|
1193
|
+
readonly kind: RegionKind;
|
|
1194
|
+
readonly devices: readonly DeviceId[];
|
|
1195
|
+
readonly nodes: readonly NodeId[];
|
|
1196
|
+
/** Set when kind is "macro". */
|
|
1197
|
+
readonly macro: MacroModel | null;
|
|
1198
|
+
/** Controls whose devices sit inside this region. */
|
|
1199
|
+
readonly controls: readonly ControlId[];
|
|
1200
|
+
};
|
|
1201
|
+
export type Partitioning = {
|
|
1202
|
+
readonly regions: readonly Region[];
|
|
1203
|
+
/** Region ids this region's parameters depend on. A dependency graph, not a list. */
|
|
1204
|
+
readonly dependencies: Readonly<Record<string, readonly string[]>>;
|
|
1205
|
+
};
|
|
1206
|
+
/**
|
|
1207
|
+
* A stamp contributes to the MNA system. Values that depend on a control or on the
|
|
1208
|
+
* sample rate are left symbolic here and evaluated at initialization, which is what
|
|
1209
|
+
* makes a program rate-independent and knob-responsive without re-lowering.
|
|
1210
|
+
*/
|
|
1211
|
+
export type Stamp = {
|
|
1212
|
+
readonly kind: "conductance";
|
|
1213
|
+
readonly a: NodeId;
|
|
1214
|
+
readonly b: NodeId;
|
|
1215
|
+
readonly siemens: number;
|
|
1216
|
+
} | {
|
|
1217
|
+
readonly kind: "controlled-conductance";
|
|
1218
|
+
readonly a: NodeId;
|
|
1219
|
+
readonly b: NodeId;
|
|
1220
|
+
readonly control: ControlId;
|
|
1221
|
+
readonly taper: TaperKind;
|
|
1222
|
+
readonly totalOhms: number;
|
|
1223
|
+
readonly side: "upper" | "lower";
|
|
1224
|
+
/** Declared-only end resistance; see the law of the same name. 0 is ideal. */
|
|
1225
|
+
readonly residualOhms: number;
|
|
1226
|
+
} | {
|
|
1227
|
+
readonly kind: "controlled-resistance";
|
|
1228
|
+
readonly a: NodeId;
|
|
1229
|
+
readonly b: NodeId;
|
|
1230
|
+
readonly control: ControlId;
|
|
1231
|
+
readonly taper: TaperKind;
|
|
1232
|
+
readonly minOhms: number;
|
|
1233
|
+
readonly maxOhms: number;
|
|
1234
|
+
} | {
|
|
1235
|
+
readonly kind: "optocoupler";
|
|
1236
|
+
readonly ldrA: NodeId;
|
|
1237
|
+
readonly ldrB: NodeId;
|
|
1238
|
+
readonly ledAnode: NodeId;
|
|
1239
|
+
readonly ledCathode: NodeId;
|
|
1240
|
+
readonly ledThresholdVolts: number;
|
|
1241
|
+
readonly ledTransconductance: number;
|
|
1242
|
+
readonly ldrMinOhms: number;
|
|
1243
|
+
readonly ldrMaxOhms: number;
|
|
1244
|
+
readonly ldrPowerLawCoefficientOhms?: number;
|
|
1245
|
+
readonly ldrPowerLawExponent?: number;
|
|
1246
|
+
} | {
|
|
1247
|
+
/** Trapezoidal companion; the conductance is 2C/dt, so it needs the rate. */
|
|
1248
|
+
readonly kind: "capacitor";
|
|
1249
|
+
readonly a: NodeId;
|
|
1250
|
+
readonly b: NodeId;
|
|
1251
|
+
readonly farads: number;
|
|
1252
|
+
readonly stateIndex: number;
|
|
1253
|
+
} | {
|
|
1254
|
+
/** Trapezoidal companion; the conductance is dt/2L, so it needs the rate. */
|
|
1255
|
+
readonly kind: "inductor";
|
|
1256
|
+
readonly a: NodeId;
|
|
1257
|
+
readonly b: NodeId;
|
|
1258
|
+
readonly henries: number;
|
|
1259
|
+
readonly stateIndex: number;
|
|
1260
|
+
/**
|
|
1261
|
+
* Series winding resistance, ohms. Absent means the law carried no value -- either the
|
|
1262
|
+
* packet declared `unknown` or it said nothing -- and the branch is stamped lossless,
|
|
1263
|
+
* which is the behaviour every corpus packet has today. It is never defaulted to zero,
|
|
1264
|
+
* because absent and zero are different claims: see the `inductance` law.
|
|
1265
|
+
*/
|
|
1266
|
+
readonly seriesResistanceOhms?: number;
|
|
1267
|
+
} | {
|
|
1268
|
+
readonly kind: "diode";
|
|
1269
|
+
readonly anode: NodeId;
|
|
1270
|
+
readonly cathode: NodeId;
|
|
1271
|
+
readonly saturationCurrent: number;
|
|
1272
|
+
readonly emissionCoefficient: number;
|
|
1273
|
+
readonly thermalVoltage: number;
|
|
1274
|
+
readonly breakdownVolts: number;
|
|
1275
|
+
readonly seriesResistance?: number;
|
|
1276
|
+
readonly device?: string;
|
|
1277
|
+
readonly isLed?: boolean;
|
|
1278
|
+
} | {
|
|
1279
|
+
readonly kind: "switch";
|
|
1280
|
+
readonly a: NodeId;
|
|
1281
|
+
readonly b: NodeId;
|
|
1282
|
+
readonly control: ControlId;
|
|
1283
|
+
readonly onOhms: number;
|
|
1284
|
+
readonly offOhms: number;
|
|
1285
|
+
} | {
|
|
1286
|
+
/** One throw of a selector: closed only while the control selects it. */
|
|
1287
|
+
readonly kind: "selector";
|
|
1288
|
+
readonly common: NodeId;
|
|
1289
|
+
readonly throwNode: NodeId;
|
|
1290
|
+
readonly control: ControlId;
|
|
1291
|
+
readonly throwIndex: number;
|
|
1292
|
+
readonly throwCount: number;
|
|
1293
|
+
readonly onOhms: number;
|
|
1294
|
+
readonly offOhms: number;
|
|
1295
|
+
} | {
|
|
1296
|
+
/**
|
|
1297
|
+
* A supply between two nodes, not a potential asserted against ground.
|
|
1298
|
+
*
|
|
1299
|
+
* A battery names both terminals and 8 corpus packets return theirs to
|
|
1300
|
+
* something other than ground; asserting `V(positive) = volts` there ties the
|
|
1301
|
+
* wrong node to the supply and discards the span the source declares. A
|
|
1302
|
+
* one-terminal rail sets `negative` to ground, which is what it means.
|
|
1303
|
+
*/
|
|
1304
|
+
readonly kind: "dc-source";
|
|
1305
|
+
readonly positive: NodeId;
|
|
1306
|
+
readonly negative: NodeId;
|
|
1307
|
+
readonly volts: number;
|
|
1308
|
+
readonly sourceIndex: number;
|
|
1309
|
+
/**
|
|
1310
|
+
* Series source impedance in ohms. `0` is an ideal source and reduces the stamp
|
|
1311
|
+
* exactly to the ideal form, so this is safe to ignore only when zero.
|
|
1312
|
+
*
|
|
1313
|
+
* This is what makes a rail's voltage depend on what the circuit draws from it —
|
|
1314
|
+
* the "supply state" step of the power-domain rail/sag contract, and the thing
|
|
1315
|
+
* that separates a battery from an adapter. See
|
|
1316
|
+
* `thoughts/shared/experiments/power-domain-rail-sag-contract/`.
|
|
1317
|
+
*/
|
|
1318
|
+
readonly sourceOhms: number;
|
|
1319
|
+
} | {
|
|
1320
|
+
/**
|
|
1321
|
+
* A sine EMF between two nodes, in series with `sourceOhms`, exactly as `dc-source`
|
|
1322
|
+
* sits between two nodes rather than asserting a potential against ground.
|
|
1323
|
+
*
|
|
1324
|
+
* **A separate operator rather than a field on `dc-source`.** A runtime that predates
|
|
1325
|
+
* this kind refuses a program carrying it by name, which is the cartridge lockout the
|
|
1326
|
+
* plan requires; an extra field would have let such a runtime execute a mains inlet as
|
|
1327
|
+
* a 120 V battery — plausible, silent and completely wrong, which is the defect this
|
|
1328
|
+
* operator exists to remove.
|
|
1329
|
+
*
|
|
1330
|
+
* The waveform is `amplitudeVolts * sin(2*pi*frequencyHz*t)`, with `t` measured from
|
|
1331
|
+
* the runtime's `prepare`. There is no phase and no DC offset: nothing in the source
|
|
1332
|
+
* states either, and a field no packet can fill is a field nothing checks. At `t = 0`
|
|
1333
|
+
* the EMF is 0, which is also what ngspice's initial transient solution reports for
|
|
1334
|
+
* `SIN(0 a f)` — measured, not assumed.
|
|
1335
|
+
*/
|
|
1336
|
+
readonly kind: "ac-source";
|
|
1337
|
+
readonly positive: NodeId;
|
|
1338
|
+
readonly negative: NodeId;
|
|
1339
|
+
readonly amplitudeVolts: number;
|
|
1340
|
+
readonly frequencyHz: number;
|
|
1341
|
+
readonly sourceIndex: number;
|
|
1342
|
+
readonly sourceOhms: number;
|
|
1343
|
+
} | {
|
|
1344
|
+
readonly kind: "bjt";
|
|
1345
|
+
readonly base: NodeId;
|
|
1346
|
+
readonly collector: NodeId;
|
|
1347
|
+
readonly emitter: NodeId;
|
|
1348
|
+
readonly polarity: "npn" | "pnp";
|
|
1349
|
+
readonly saturationCurrent: number;
|
|
1350
|
+
readonly forwardBeta: number;
|
|
1351
|
+
readonly reverseBeta: number;
|
|
1352
|
+
readonly thermalVoltage: number;
|
|
1353
|
+
/**
|
|
1354
|
+
* Excess collector-base leakage beyond the ideal junction, in amps, or 0.
|
|
1355
|
+
*
|
|
1356
|
+
* This is germanium's defining electrical difference and the reason a Fuzz Face
|
|
1357
|
+
* biases at all: the leakage flowing out of the base through its bias resistor is
|
|
1358
|
+
* what sets the operating point, which is also why those pedals drift with
|
|
1359
|
+
* temperature. Ebers-Moll's ideal reverse current is the declared saturation
|
|
1360
|
+
* current, around 1 nA, and a real germanium part leaks 100 to 1000 times that.
|
|
1361
|
+
*/
|
|
1362
|
+
readonly leakageAmps: number;
|
|
1363
|
+
} | {
|
|
1364
|
+
readonly kind: "fet";
|
|
1365
|
+
readonly gate: NodeId;
|
|
1366
|
+
readonly drain: NodeId;
|
|
1367
|
+
readonly source: NodeId;
|
|
1368
|
+
readonly channel: "n" | "p";
|
|
1369
|
+
readonly thresholdVolts: number;
|
|
1370
|
+
readonly transconductance: number;
|
|
1371
|
+
readonly channelLengthModulation: number;
|
|
1372
|
+
/** See the `fet` device law: 0 keeps the legacy hard cutoff. */
|
|
1373
|
+
readonly subthresholdVolts: number;
|
|
1374
|
+
/** See the `fet` device law: 0 means an insulated gate and stamps nothing. */
|
|
1375
|
+
readonly gateSaturationCurrent: number;
|
|
1376
|
+
readonly gateOnsetVolts: number;
|
|
1377
|
+
readonly gateScaleVolts: number;
|
|
1378
|
+
} | {
|
|
1379
|
+
readonly kind: "triode";
|
|
1380
|
+
readonly grid: NodeId;
|
|
1381
|
+
readonly cathode: NodeId;
|
|
1382
|
+
readonly plate: NodeId;
|
|
1383
|
+
readonly mu: number;
|
|
1384
|
+
readonly kg1: number;
|
|
1385
|
+
readonly kp: number;
|
|
1386
|
+
readonly kvb: number;
|
|
1387
|
+
readonly ex: number;
|
|
1388
|
+
readonly gridSaturationCurrent: number;
|
|
1389
|
+
readonly gridOnsetVolts: number;
|
|
1390
|
+
readonly gridScaleVolts: number;
|
|
1391
|
+
/**
|
|
1392
|
+
* Contact potential, added to `Vgk` inside the Koren exponent.
|
|
1393
|
+
*
|
|
1394
|
+
* A standard term in this model that this law omitted. It is load-bearing, not a
|
|
1395
|
+
* correction: the published Koren 12AX7 set reproduces RCA's typical point to +2% with
|
|
1396
|
+
* `Vct = 0.5` and to **-58%** without it. A fit made against a law that has no `Vct`
|
|
1397
|
+
* must absorb it by distorting the other coefficients, which is what an `ex` of 1.0
|
|
1398
|
+
* and a `kvb` of 9707 look like. `0` reproduces the previous behaviour exactly.
|
|
1399
|
+
*/
|
|
1400
|
+
readonly contactPotentialVolts: number;
|
|
1401
|
+
} | {
|
|
1402
|
+
readonly kind: "pentode";
|
|
1403
|
+
readonly grid: NodeId;
|
|
1404
|
+
readonly cathode: NodeId;
|
|
1405
|
+
readonly plate: NodeId;
|
|
1406
|
+
readonly screen: NodeId;
|
|
1407
|
+
readonly mu: number;
|
|
1408
|
+
readonly kg1: number;
|
|
1409
|
+
readonly kp: number;
|
|
1410
|
+
readonly kvb: number;
|
|
1411
|
+
readonly ex: number;
|
|
1412
|
+
readonly gridSaturationCurrent: number;
|
|
1413
|
+
readonly gridOnsetVolts: number;
|
|
1414
|
+
readonly gridScaleVolts: number;
|
|
1415
|
+
/**
|
|
1416
|
+
* Contact potential, added to `Vgk` inside the Koren exponent.
|
|
1417
|
+
*
|
|
1418
|
+
* A standard term in this model that this law omitted. It is load-bearing, not a
|
|
1419
|
+
* correction: the published Koren 12AX7 set reproduces RCA's typical point to +2% with
|
|
1420
|
+
* `Vct = 0.5` and to **-58%** without it. A fit made against a law that has no `Vct`
|
|
1421
|
+
* must absorb it by distorting the other coefficients, which is what an `ex` of 1.0
|
|
1422
|
+
* and a `kvb` of 9707 look like. `0` reproduces the previous behaviour exactly.
|
|
1423
|
+
*/
|
|
1424
|
+
readonly contactPotentialVolts: number;
|
|
1425
|
+
/**
|
|
1426
|
+
* The screen's share of the plate current, stamped as a screen<->cathode source
|
|
1427
|
+
* `screenCurrent = plateCurrent * screenShare`. `0` is a no-op (no screen source), so a
|
|
1428
|
+
* law without a screen figure behaves exactly as before. See the DeviceLaw field for
|
|
1429
|
+
* why a share rather than a fixed current.
|
|
1430
|
+
*/
|
|
1431
|
+
readonly screenShare: number;
|
|
1432
|
+
} | {
|
|
1433
|
+
/**
|
|
1434
|
+
* One plate of a vacuum rectifier against its cathode. A dual diode is **two of
|
|
1435
|
+
* these** sharing a cathode node, which is what it is electrically -- one envelope,
|
|
1436
|
+
* two independent conduction paths -- so no operator has to know about envelopes.
|
|
1437
|
+
*
|
|
1438
|
+
* Needs no auxiliary row: it is a two-terminal nonlinear conductance, exactly like a
|
|
1439
|
+
* junction diode, and unlike one it carries no exponential.
|
|
1440
|
+
*/
|
|
1441
|
+
readonly kind: "tube-diode";
|
|
1442
|
+
readonly plate: NodeId;
|
|
1443
|
+
readonly cathode: NodeId;
|
|
1444
|
+
readonly perveance: number;
|
|
1445
|
+
readonly exponent: number;
|
|
1446
|
+
} | {
|
|
1447
|
+
readonly kind: "transformer";
|
|
1448
|
+
readonly primaryPlus: NodeId;
|
|
1449
|
+
readonly primaryMinus: NodeId;
|
|
1450
|
+
readonly secondaryPlus: NodeId;
|
|
1451
|
+
readonly secondaryMinus: NodeId;
|
|
1452
|
+
readonly turnsRatio: number;
|
|
1453
|
+
readonly sourceIndex: number;
|
|
1454
|
+
/**
|
|
1455
|
+
* Copper loss for this winding pair, as the loop resistance **referred to the
|
|
1456
|
+
* primary**: `R_primary + turnsRatio^2 * R_secondary`. One number rather than two,
|
|
1457
|
+
* because the constraint row carries a single auxiliary unknown (the primary current)
|
|
1458
|
+
* and a series impedance there is one matrix entry -- the same shape `input-source`
|
|
1459
|
+
* already uses. Absent means the windings declared nothing and the transformer is
|
|
1460
|
+
* ideal, which is every corpus packet today.
|
|
1461
|
+
*
|
|
1462
|
+
* **This is copper only.** A real iron-core transformer also has core loss
|
|
1463
|
+
* (hysteresis and eddy currents) and leakage inductance, and at low frequencies core
|
|
1464
|
+
* loss is typically the LARGER damping term. Neither is modelled here, so a null
|
|
1465
|
+
* result from adding this does not clear the ideal-transformer hypothesis -- see
|
|
1466
|
+
* ledger 27.
|
|
1467
|
+
*/
|
|
1468
|
+
readonly seriesResistanceOhms?: number;
|
|
1469
|
+
} | {
|
|
1470
|
+
readonly kind: "input-source";
|
|
1471
|
+
readonly node: NodeId;
|
|
1472
|
+
readonly sourceIndex: number;
|
|
1473
|
+
} | {
|
|
1474
|
+
readonly kind: "ideal-opamp";
|
|
1475
|
+
readonly plus: NodeId;
|
|
1476
|
+
readonly minus: NodeId;
|
|
1477
|
+
readonly output: NodeId;
|
|
1478
|
+
/**
|
|
1479
|
+
* Open-loop DC gain, dimensionless.
|
|
1480
|
+
*
|
|
1481
|
+
* A class default of 1e5 was used for every op-amp in the corpus while 31 of 52 declared
|
|
1482
|
+
* something else -- 200k on 21 of them, 300k on five, and 3000 on one. Every measurement of
|
|
1483
|
+
* op-amp stiffness, the saturation band and the step limiter was taken at the default.
|
|
1484
|
+
*/
|
|
1485
|
+
readonly openLoopGain: number;
|
|
1486
|
+
readonly sourceIndex: number;
|
|
1487
|
+
readonly railHigh: number | null;
|
|
1488
|
+
readonly railLow: number | null;
|
|
1489
|
+
} | {
|
|
1490
|
+
/**
|
|
1491
|
+
* The `coupled` port's load side (build-order step 1, spec clause 2): a same-sample
|
|
1492
|
+
* voltage source whose magnitude is a named macro's current write-back rather than a
|
|
1493
|
+
* value carried on the stamp, with a series `sourceOhms` exactly as `dc-source`
|
|
1494
|
+
* already has -- the port carries value **and** admittance, never a bare sample. The
|
|
1495
|
+
* runtime resolves `macroId` to whichever macro block declares `audioOut: true` with
|
|
1496
|
+
* that id; there is exactly one such block in every program this compiler emits,
|
|
1497
|
+
* because `couple.ts` only ever wires a macro's own output to one downstream stamp.
|
|
1498
|
+
*
|
|
1499
|
+
* Single-ended (implicit ground return), matching `input-source`'s shape rather than
|
|
1500
|
+
* `dc-source`'s two-node one: a macro's boundary equivalent is a tap into one net, not
|
|
1501
|
+
* a floating supply between two named terminals.
|
|
1502
|
+
*/
|
|
1503
|
+
readonly kind: "macro-audio-source";
|
|
1504
|
+
readonly node: NodeId;
|
|
1505
|
+
readonly macroId: string;
|
|
1506
|
+
readonly sourceOhms: number;
|
|
1507
|
+
readonly sourceIndex: number;
|
|
1508
|
+
} | {
|
|
1509
|
+
/** Voltage-Controlled Current Source (VCCS), representing an OTA transconductance cell. */
|
|
1510
|
+
readonly kind: "vccs";
|
|
1511
|
+
readonly outP: NodeId;
|
|
1512
|
+
readonly outN: NodeId;
|
|
1513
|
+
readonly inP: NodeId;
|
|
1514
|
+
readonly inN: NodeId;
|
|
1515
|
+
readonly transconductance: number;
|
|
1516
|
+
readonly biasVolts?: number;
|
|
1517
|
+
} | {
|
|
1518
|
+
readonly kind: "ota";
|
|
1519
|
+
readonly plus: NodeId;
|
|
1520
|
+
readonly minus: NodeId;
|
|
1521
|
+
readonly bias: NodeId;
|
|
1522
|
+
readonly output: NodeId;
|
|
1523
|
+
readonly vee: NodeId;
|
|
1524
|
+
readonly saturationCurrent: number;
|
|
1525
|
+
readonly thermalVoltage: number;
|
|
1526
|
+
} | {
|
|
1527
|
+
/**
|
|
1528
|
+
* Linear-control VCA cell (lowered from the `linear-vca` law).
|
|
1529
|
+
* Current proportional to the input current, scaled by the
|
|
1530
|
+
* control voltage over `vrefVolts`. Reads the control node and
|
|
1531
|
+
* draws nothing from it.
|
|
1532
|
+
*/
|
|
1533
|
+
readonly kind: "linear-vca";
|
|
1534
|
+
readonly plus: NodeId;
|
|
1535
|
+
readonly minus: NodeId;
|
|
1536
|
+
readonly output: NodeId;
|
|
1537
|
+
readonly control: NodeId;
|
|
1538
|
+
readonly inputSiemens: number;
|
|
1539
|
+
readonly vrefVolts: number;
|
|
1540
|
+
readonly minGain: number;
|
|
1541
|
+
} | {
|
|
1542
|
+
readonly kind: "logic-divider";
|
|
1543
|
+
readonly clockNode: NodeId;
|
|
1544
|
+
readonly qNode: NodeId;
|
|
1545
|
+
readonly gndNode: NodeId;
|
|
1546
|
+
readonly thresholdVolts: number;
|
|
1547
|
+
readonly highVolts: number;
|
|
1548
|
+
readonly sourceIndex: number;
|
|
1549
|
+
readonly stateIndex: number;
|
|
1550
|
+
} | {
|
|
1551
|
+
readonly kind: "clock-driver";
|
|
1552
|
+
readonly cp1: NodeId;
|
|
1553
|
+
readonly cp2: NodeId;
|
|
1554
|
+
readonly vgg: NodeId;
|
|
1555
|
+
readonly vdd: NodeId;
|
|
1556
|
+
readonly ox1: NodeId;
|
|
1557
|
+
/**
|
|
1558
|
+
* The part's own GND pin, which is **not** circuit ground.
|
|
1559
|
+
*
|
|
1560
|
+
* The clock phases swing between the two supply pins. Before this row existed the
|
|
1561
|
+
* stamp swung them between `vdd` and literal ground, which is right only when the
|
|
1562
|
+
* GND pin happens to sit there -- and a faithful +9 V transcription puts it on the
|
|
1563
|
+
* positive rail instead, leaving `vdd` at ground and every output pinned at zero.
|
|
1564
|
+
* `boss-ch-1` and `boss-dm-3` were both silent for exactly that reason.
|
|
1565
|
+
*/
|
|
1566
|
+
readonly gnd: NodeId;
|
|
1567
|
+
readonly defaultFrequency: number;
|
|
1568
|
+
readonly stateIndex: number;
|
|
1569
|
+
readonly sourceIndex: number;
|
|
1570
|
+
} | {
|
|
1571
|
+
readonly kind: "analog-switch";
|
|
1572
|
+
readonly a: NodeId;
|
|
1573
|
+
readonly b: NodeId;
|
|
1574
|
+
readonly control: NodeId;
|
|
1575
|
+
readonly onOhms: number;
|
|
1576
|
+
readonly offOhms: number;
|
|
1577
|
+
readonly thresholdVolts: number;
|
|
1578
|
+
} | {
|
|
1579
|
+
/**
|
|
1580
|
+
* The two *active* parts of a compandor channel: the full-wave averaging
|
|
1581
|
+
* rectifier and the variable-gain cell.
|
|
1582
|
+
*
|
|
1583
|
+
* Everything else in the datasheet's Figure 5 -- the five internal resistors,
|
|
1584
|
+
* the band-gap reference and the output op-amp -- lowers to ordinary
|
|
1585
|
+
* `conductance`, `dc-source` and `ideal-opamp` stamps, so the runtime gained
|
|
1586
|
+
* no new linear machinery for this part and the external circuit decides what
|
|
1587
|
+
* the chip does. See `lower.ts`'s `compandor` case.
|
|
1588
|
+
*/
|
|
1589
|
+
readonly kind: "compandor";
|
|
1590
|
+
/** Pin 2/15, RECT_IN. Drives the detector through `r1`. */
|
|
1591
|
+
readonly rectIn: NodeId;
|
|
1592
|
+
/** Pin 1/16, CRECT. The averaging node; the packet's own cap sets `tau`. */
|
|
1593
|
+
readonly rectCap: NodeId;
|
|
1594
|
+
/** Pin 3/14, G_CELL_IN. The gain cell's signal input, through `r2`. */
|
|
1595
|
+
readonly cellIn: NodeId;
|
|
1596
|
+
/**
|
|
1597
|
+
* Pin 5/12, INV_IN -- the internal op-amp's summing node, which is where the
|
|
1598
|
+
* gain cell delivers its output *current*.
|
|
1599
|
+
*
|
|
1600
|
+
* Not the chip's OUTPUT pin. The gain cell is a current source and the op-amp
|
|
1601
|
+
* converts that current to a voltage through whatever feedback the packet
|
|
1602
|
+
* hangs on pin 6; injecting into the output pin instead would make the gain
|
|
1603
|
+
* depend on the output loading rather than on the feedback resistor.
|
|
1604
|
+
*/
|
|
1605
|
+
readonly sumNode: NodeId;
|
|
1606
|
+
/** The internal 1.8 V band-gap node, allocated by lowering. */
|
|
1607
|
+
readonly vref: NodeId;
|
|
1608
|
+
readonly r1: number;
|
|
1609
|
+
readonly r2: number;
|
|
1610
|
+
readonly r5: number;
|
|
1611
|
+
readonly iBias: number;
|
|
1612
|
+
readonly stateIndex: number;
|
|
1613
|
+
} | {
|
|
1614
|
+
/**
|
|
1615
|
+
* Spring reverb tank. See the `spring-reverb` device law for why this is an
|
|
1616
|
+
* operator rather than something MNA can produce.
|
|
1617
|
+
*
|
|
1618
|
+
* Two single-ended ports, both stamped the way the rest of this file stamps a
|
|
1619
|
+
* source with an internal impedance: the input pair is the drive coil's load, and
|
|
1620
|
+
* the output pair is a Thevenin source at the pickup's rated impedance whose value
|
|
1621
|
+
* is the tank's own delayed, dispersed, decaying output. `sourceIndex` names the
|
|
1622
|
+
* auxiliary row that carries it, and doubles as this tank's runtime state key --
|
|
1623
|
+
* unique per stamp within a block, because `lower.ts` allocates it that way.
|
|
1624
|
+
*/
|
|
1625
|
+
readonly kind: "spring-reverb";
|
|
1626
|
+
readonly inputPlus: NodeId;
|
|
1627
|
+
readonly inputMinus: NodeId;
|
|
1628
|
+
readonly outputPlus: NodeId;
|
|
1629
|
+
readonly outputMinus: NodeId;
|
|
1630
|
+
readonly inputOhms: number;
|
|
1631
|
+
readonly outputOhms: number;
|
|
1632
|
+
readonly delaySeconds: number;
|
|
1633
|
+
readonly decaySeconds: number;
|
|
1634
|
+
readonly dispersionStages: number;
|
|
1635
|
+
readonly sourceIndex: number;
|
|
1636
|
+
} | {
|
|
1637
|
+
readonly kind: "comparator";
|
|
1638
|
+
readonly plus: NodeId;
|
|
1639
|
+
readonly minus: NodeId;
|
|
1640
|
+
readonly output: NodeId;
|
|
1641
|
+
readonly vee: NodeId;
|
|
1642
|
+
readonly pullDownOhms: number;
|
|
1643
|
+
readonly floatOhms: number;
|
|
1644
|
+
readonly sensitivity: number;
|
|
1645
|
+
};
|
|
1646
|
+
/**
|
|
1647
|
+
* One operator the runtime must implement to execute a program: a stamp's kind.
|
|
1648
|
+
*
|
|
1649
|
+
* Named separately from `Stamp` because it is the unit of the **cartridge lockout** —
|
|
1650
|
+
* `Program.requiredOperators` is a list of these, and a runtime missing one refuses the
|
|
1651
|
+
* program instead of executing the rest. Derived from the union rather than written out,
|
|
1652
|
+
* so a new stamp kind cannot be added without joining the declared set.
|
|
1653
|
+
*/
|
|
1654
|
+
export type OperatorKind = Stamp["kind"];
|
|
1655
|
+
export type StampPartition = {
|
|
1656
|
+
/**
|
|
1657
|
+
* The full nonlinear port set for this block: every row some stamp's law actually writes,
|
|
1658
|
+
* sorted and de-duplicated, strictly excluding ground (node 0).
|
|
1659
|
+
*/
|
|
1660
|
+
readonly portRows: readonly number[];
|
|
1661
|
+
/** Indices into `block.stamps` that are linear (`stampPortRows === null`). */
|
|
1662
|
+
readonly linearStampIndices: readonly number[];
|
|
1663
|
+
/** Indices into `block.stamps` that are nonlinear (`stampPortRows !== null`). */
|
|
1664
|
+
readonly nonlinearStampIndices: readonly number[];
|
|
1665
|
+
/** Indices into `block.stamps` that are constant (no control, state, or signal dependency). */
|
|
1666
|
+
readonly constantStampIndices: readonly number[];
|
|
1667
|
+
/** Indices into `block.stamps` that read a control knob. */
|
|
1668
|
+
readonly controlStampIndices: readonly number[];
|
|
1669
|
+
/** Indices into `block.stamps` that carry dynamic/reactive state (capacitors, inductors, logic dividers, etc.). */
|
|
1670
|
+
readonly dynamicStampIndices: readonly number[];
|
|
1671
|
+
};
|
|
1672
|
+
export type SparseSchedule = {
|
|
1673
|
+
readonly ops: readonly number[];
|
|
1674
|
+
/** Number of stored entries: the pattern plus its fill-in. */
|
|
1675
|
+
readonly slots: number;
|
|
1676
|
+
readonly factorCount: number;
|
|
1677
|
+
/** Slot -> matrix row/column, for gathering the stamped dense matrix into `values`. */
|
|
1678
|
+
readonly gatherRow: readonly number[];
|
|
1679
|
+
readonly gatherColumn: readonly number[];
|
|
1680
|
+
readonly size: number;
|
|
1681
|
+
/** Multiply-adds the schedule performs, against what the dense elimination would. */
|
|
1682
|
+
readonly sparseOps: number;
|
|
1683
|
+
readonly denseOps: number;
|
|
1684
|
+
/** Pivot steps that had to fall back to a not-provably-nonzero entry. Diagnostic. */
|
|
1685
|
+
readonly unprovenPivots: number;
|
|
1686
|
+
};
|
|
1687
|
+
export type Block = {
|
|
1688
|
+
readonly kind: "mna";
|
|
1689
|
+
readonly id: string;
|
|
1690
|
+
/**
|
|
1691
|
+
* How many node unknowns this block solves. Always `nodeIds.length` — the two are
|
|
1692
|
+
* set together at the single site that builds them (`lowerRegion`), and this one
|
|
1693
|
+
* exists because the runtime reads it in its innermost loops.
|
|
1694
|
+
*/
|
|
1695
|
+
readonly nodeCount: number;
|
|
1696
|
+
/**
|
|
1697
|
+
* Row index -> the source `NodeId` that row stands for. `nodeIds[0]` is always
|
|
1698
|
+
* `GROUND`, because the runtime pins row 0 and starts its gmin loop at 1.
|
|
1699
|
+
*
|
|
1700
|
+
* **A block's rows are numbered 0..nodeCount-1, not by the node ids the document
|
|
1701
|
+
* authored.** They used to be the same thing: the row index *was* the authored id,
|
|
1702
|
+
* so a block's matrix was sized by the largest node label in its region rather than
|
|
1703
|
+
* by how many nodes it has. Two things then inflated it independently — a document
|
|
1704
|
+
* numbering a net `900` (`jhs-424-gain-stage` labels its +10 V rail that, and got a
|
|
1705
|
+
* 909-unknown matrix for a 30-node circuit), and a multi-region document, where
|
|
1706
|
+
* regions are node-disjoint but each one's matrix still spanned the *global* id
|
|
1707
|
+
* range up to its own maximum (`boss-os-2`: 46 regions, the driven one sized 354).
|
|
1708
|
+
* Corpus-wide that was 77,062 analog rows where 4,829 nodes exist, and the padding
|
|
1709
|
+
* rows carried nothing but `gmin`.
|
|
1710
|
+
*
|
|
1711
|
+
* This array is the inverse map, and it is not optional bookkeeping: several
|
|
1712
|
+
* consumers bridge a solved row back to a declared net — the ngspice deck names
|
|
1713
|
+
* (`scripts/lib/program-to-spice.ts`, which must stay in the same namespace as
|
|
1714
|
+
* `source-to-spice.ts` or parity compares two different nodes and still reports
|
|
1715
|
+
* agreement), the declared-rail check in `report-transistor-bias.ts`, and the node
|
|
1716
|
+
* labels `report-signal-path.ts` and `render-v2-audio.ts --held` print. Use
|
|
1717
|
+
* `blockNodeIndex` for the forward direction.
|
|
1718
|
+
*
|
|
1719
|
+
* Every node of every device in the region is kept, including terminals no device
|
|
1720
|
+
* law stamps (an op-amp's `vcc`/`vee`). The renumbering is a bijection on the
|
|
1721
|
+
* region's node set, so no node is removed and nothing about what the block models
|
|
1722
|
+
* changes.
|
|
1723
|
+
*/
|
|
1724
|
+
readonly nodeIds: readonly NodeId[];
|
|
1725
|
+
/** Extra unknowns for voltage sources and op-amp outputs. */
|
|
1726
|
+
readonly auxCount: number;
|
|
1727
|
+
readonly stamps: readonly Stamp[];
|
|
1728
|
+
/**
|
|
1729
|
+
* Compile-time stamp partition for linear/nonlinear, control, and constant stamps.
|
|
1730
|
+
* Derived once at compile time and emitted on the program as data.
|
|
1731
|
+
*/
|
|
1732
|
+
readonly stampPartition: StampPartition;
|
|
1733
|
+
/**
|
|
1734
|
+
* Compile-time static sparse elimination schedule.
|
|
1735
|
+
* Derived once at compile time and emitted on the program as data.
|
|
1736
|
+
*/
|
|
1737
|
+
readonly sparseSchedule: SparseSchedule | null;
|
|
1738
|
+
readonly stateCount: number;
|
|
1739
|
+
/**
|
|
1740
|
+
* No stamp here has to be re-evaluated inside a Newton loop, so the block solves in
|
|
1741
|
+
* a single pass. Read by the runtime to choose between one solve and the iteration
|
|
1742
|
+
* cap, and derived from the stamps rather than from the region's device list — see
|
|
1743
|
+
* `stampNeedsNewton`.
|
|
1744
|
+
*
|
|
1745
|
+
* Stated positively rather than as `nonlinear` because it is a **licence**: the
|
|
1746
|
+
* false case costs iterations, and the true case is what later build steps are
|
|
1747
|
+
* allowed to exploit. A flag whose safe default is `false` should read as the thing
|
|
1748
|
+
* being claimed.
|
|
1749
|
+
*/
|
|
1750
|
+
readonly linear: boolean;
|
|
1751
|
+
/**
|
|
1752
|
+
* No stamp here reads a control, so this block's coefficients do not change when a
|
|
1753
|
+
* knob moves.
|
|
1754
|
+
*
|
|
1755
|
+
* The other half of "may this be factored once at load". `linear` alone is not
|
|
1756
|
+
* enough and the difference is not bookkeeping: gate 1c measured a state-space
|
|
1757
|
+
* realisation derived at one control position and evaluated at another to be wrong
|
|
1758
|
+
* by 80% of the signal (`4.19e-02` against a `5.23e-02` reference RMS). So a
|
|
1759
|
+
* precomputation gated on `linear` alone would be exact on the fixture that derived
|
|
1760
|
+
* it and wrong on the same circuit one knob-turn later.
|
|
1761
|
+
*
|
|
1762
|
+
* Also from the stamps, because a control-bearing *device* need not lower to a
|
|
1763
|
+
* control-bearing stamp: a switched jack's `port-engage` contact becomes a plain
|
|
1764
|
+
* conductance.
|
|
1765
|
+
*/
|
|
1766
|
+
readonly controlFree: boolean;
|
|
1767
|
+
/**
|
|
1768
|
+
* True when symbolic elimination (DK reduction) is applied to this block. The
|
|
1769
|
+
* admission rule is owned by `shouldEliminate` -- a nonlinear block with port rows
|
|
1770
|
+
* is admitted when the dense-vs-sparse cost rule admits it, or when the document
|
|
1771
|
+
* opts in for convergence (`convergenceOptIn`), which overrides the cost veto and
|
|
1772
|
+
* nothing else.
|
|
1773
|
+
*/
|
|
1774
|
+
readonly eliminate: boolean;
|
|
1775
|
+
/**
|
|
1776
|
+
* Set (to `true`) when the document declares `convergence-opt-in`, so that
|
|
1777
|
+
* `shouldEliminate` and its re-derivation in `withStamp` apply the opt-in to this
|
|
1778
|
+
* block. Compiler-internal: the runtime reads only `eliminate`. Absent (rather
|
|
1779
|
+
* than `false`) for a document that does not opt in, so a non-opting program is
|
|
1780
|
+
* byte-identical to one lowered before this flag existed.
|
|
1781
|
+
*/
|
|
1782
|
+
readonly convergenceOptIn?: boolean;
|
|
1783
|
+
/** Null when this region does not contain the circuit's input jack. */
|
|
1784
|
+
readonly inputNode: NodeId | null;
|
|
1785
|
+
/** Null when this region does not contain the circuit's output jack. */
|
|
1786
|
+
readonly outputNode: NodeId | null;
|
|
1787
|
+
/**
|
|
1788
|
+
* Where the operating-point solve starts, for nodes whose initial value decides
|
|
1789
|
+
* which of several DC solutions the block settles into. Empty for the ordinary
|
|
1790
|
+
* case of a circuit with one solution.
|
|
1791
|
+
*/
|
|
1792
|
+
readonly operatingPointSeeds: readonly OperatingPointSeed[];
|
|
1793
|
+
} | {
|
|
1794
|
+
readonly kind: "macro";
|
|
1795
|
+
/** See `MacroModel.declaredPositions`; consumed by composition. */
|
|
1796
|
+
readonly declaredPositions?: readonly ComposedPosition[] | null;
|
|
1797
|
+
/** The router as resolution planned it, before `couple.ts` can see other blocks. */
|
|
1798
|
+
readonly declaredRouter?: DeclaredRouterPlan | null;
|
|
1799
|
+
/** The cited parameter control's node, planned the same way. */
|
|
1800
|
+
readonly declaredParameter?: DeclaredParameterPlan | null;
|
|
1801
|
+
/** Wired by `couple.ts` from `declaredRouter`, the same way `parameter` is. */
|
|
1802
|
+
readonly router?: ComposedRouter | null;
|
|
1803
|
+
readonly id: string;
|
|
1804
|
+
/**
|
|
1805
|
+
* The DSP algorithm this block executes, copied from the registry's `MacroModel` —
|
|
1806
|
+
* see `MacroModel.modelId` for what it names and why it is not a chip number.
|
|
1807
|
+
*
|
|
1808
|
+
* This is the block's whole instruction: the runtime's dispatch reads it, and
|
|
1809
|
+
* `Program.requiredModels` declares it so a runtime that lacks the algorithm refuses
|
|
1810
|
+
* the program by name at load instead of executing some other algorithm's code.
|
|
1811
|
+
*/
|
|
1812
|
+
readonly modelId: string;
|
|
1813
|
+
readonly parameters: Readonly<Record<string, number>>;
|
|
1814
|
+
/**
|
|
1815
|
+
* The `coupled` port's driver side (build-order step 1, spec clause 2): the block and
|
|
1816
|
+
* node this macro taps for its audio input. Same-sample and read-only from here --
|
|
1817
|
+
* the *loading* this represents is stamped as a `conductance` into `block` itself, not
|
|
1818
|
+
* modelled at this port, which is why this port carries no admittance of its own: it
|
|
1819
|
+
* is a `Voc`-side tap, not the `Yin` (see `MacroModel.audioPortImpedanceOhms.input`,
|
|
1820
|
+
* which is where that admittance is declared and stamped from).
|
|
1821
|
+
*/
|
|
1822
|
+
readonly audioIn: {
|
|
1823
|
+
readonly block: string;
|
|
1824
|
+
readonly node: NodeId;
|
|
1825
|
+
} | null;
|
|
1826
|
+
/**
|
|
1827
|
+
* The `coupled` port's load side: `null` unless some block carries a
|
|
1828
|
+
* `macro-audio-source` stamp naming this macro's `id`. Carried on the macro rather
|
|
1829
|
+
* than derived, so the macro's own audio-out existence is a program fact rather than
|
|
1830
|
+
* something a consumer has to search every block's stamps to learn.
|
|
1831
|
+
*/
|
|
1832
|
+
readonly audioOut: boolean;
|
|
1833
|
+
/**
|
|
1834
|
+
* Whether this block's `parameters.delaySeconds` came from the packet's own clock
|
|
1835
|
+
* network or from a `DelayMs` typed into the source. See `MacroModel.delayProvenance`.
|
|
1836
|
+
*/
|
|
1837
|
+
readonly delayProvenance?: "derived" | "declared" | "clock-law" | null;
|
|
1838
|
+
readonly delayDeclaredReason?: string | null;
|
|
1839
|
+
/**
|
|
1840
|
+
* Set when this part declares a modulation input, the document wires it, and
|
|
1841
|
+
* admission refused it -- the difference between a flanger and a fixed comb filter.
|
|
1842
|
+
* `null`/absent when the port was admitted or the part has none.
|
|
1843
|
+
*/
|
|
1844
|
+
readonly modulationRefusal?: string | null;
|
|
1845
|
+
/**
|
|
1846
|
+
* The `parameter` port (build-order step 1, spec clause 1): present only when
|
|
1847
|
+
* admission proved the PURITY precondition -- the derivation reads nothing but this
|
|
1848
|
+
* node, and neither of gate 1b's two measured failure modes applies (no audio-path
|
|
1849
|
+
* active device shares this node's region, no control is reachable from it). `null`
|
|
1850
|
+
* on missing or negative evidence, which is the spec's safe default: this repository's
|
|
1851
|
+
* runtime does not read an unvalidated node as a delay-scale substitute, `stages`
|
|
1852
|
+
* alone is used instead, and no pedal is refused for lacking this port.
|
|
1853
|
+
*/
|
|
1854
|
+
readonly parameter: ParameterPort | null;
|
|
1855
|
+
/**
|
|
1856
|
+
* The `modulation` port: the **signal-rate** sibling of `parameter`, read every
|
|
1857
|
+
* sample rather than when a control moves.
|
|
1858
|
+
*
|
|
1859
|
+
* It exists because `parameter` deliberately cannot serve a chorus.
|
|
1860
|
+
* `experiments/clock-parameter-port-gate/README.md` measured the boundary and named
|
|
1861
|
+
* the failing case: an LFO-swept clock "turns the `parameter` port into a `signal`
|
|
1862
|
+
* by definition". That finding is respected rather than worked around -- the gate
|
|
1863
|
+
* stays narrow and this is a different port with a different contract.
|
|
1864
|
+
*
|
|
1865
|
+
* `null` unless the compiler resolved exactly one steering device with exactly one
|
|
1866
|
+
* control terminal off the timing network. See `resolveClockModulationSource`.
|
|
1867
|
+
*/
|
|
1868
|
+
readonly modulation?: ModulationPort | null;
|
|
1869
|
+
readonly clockControl?: MacroClockControl | null;
|
|
1870
|
+
/**
|
|
1871
|
+
* The `clock-law` port: the open-OX2 slow node, read every sample by a
|
|
1872
|
+
* `clock-law` length mode. `null` unless `deriveOpenOx2ClockLaw`
|
|
1873
|
+
* recognised the form. See `ClockLawPort`.
|
|
1874
|
+
*/
|
|
1875
|
+
readonly clockLaw?: ClockLawPort | null;
|
|
1876
|
+
/** The open-OX2 law constants, or absent when the form was not recognised. */
|
|
1877
|
+
readonly clockLawParams?: ClockLawParams | null;
|
|
1878
|
+
} | {
|
|
1879
|
+
/**
|
|
1880
|
+
* A data-driven composition of DSP primitives the packet (via the catalog)
|
|
1881
|
+
* supplies, executed by the composition interpreter instead of a dispatched
|
|
1882
|
+
* class (board-p3 row 4). `modelId` still names the algorithm -- the same
|
|
1883
|
+
* vocabulary `requiredModels` declares -- so admission pricing, the
|
|
1884
|
+
* implementer lockout, and traceability read it unchanged.
|
|
1885
|
+
*/
|
|
1886
|
+
readonly kind: "composed";
|
|
1887
|
+
/**
|
|
1888
|
+
* Where this composition's algorithm came from, and therefore whether a runtime
|
|
1889
|
+
* has to already implement it.
|
|
1890
|
+
*
|
|
1891
|
+
* `registry` means the compiler translated a named model into ops, and `modelId`
|
|
1892
|
+
* is that model: a runtime that does not know it cannot be trusted to have the
|
|
1893
|
+
* same translation, so the name is declared in `requiredModels` and refused at
|
|
1894
|
+
* load. `declared` means the **source** carried the ops, so there is no algorithm
|
|
1895
|
+
* to have: the program is the program, and requiring a runtime to recognise the
|
|
1896
|
+
* chip's part number would refuse the one case this whole mechanism exists for.
|
|
1897
|
+
*
|
|
1898
|
+
* The op vocabulary is still enforced, per op, by name -- that check does not move.
|
|
1899
|
+
*/
|
|
1900
|
+
readonly modelSource: "registry" | "declared";
|
|
1901
|
+
readonly id: string;
|
|
1902
|
+
readonly modelId: string;
|
|
1903
|
+
readonly parameters: Readonly<Record<string, number>>;
|
|
1904
|
+
readonly audioIn: {
|
|
1905
|
+
readonly block: string;
|
|
1906
|
+
readonly node: NodeId;
|
|
1907
|
+
} | null;
|
|
1908
|
+
readonly audioOut: boolean;
|
|
1909
|
+
readonly parameter: ParameterPort | null;
|
|
1910
|
+
readonly clockControl?: MacroClockControl | null;
|
|
1911
|
+
readonly modulation?: ModulationPort | null;
|
|
1912
|
+
/**
|
|
1913
|
+
* The `clock-law` port and law, preserved across the composition
|
|
1914
|
+
* migration like the provenance below: a composition never gains
|
|
1915
|
+
* these, it inherits them.
|
|
1916
|
+
*/
|
|
1917
|
+
readonly clockLaw?: ClockLawPort | null;
|
|
1918
|
+
readonly clockLawParams?: ClockLawParams | null;
|
|
1919
|
+
/**
|
|
1920
|
+
* Provenance the macro carried, preserved across the composition
|
|
1921
|
+
* migration so diagnostics read the same facts: whether the delay
|
|
1922
|
+
* was derived or declared and why, and any refused modulation
|
|
1923
|
+
* input. A composition never gains these, it inherits them.
|
|
1924
|
+
*/
|
|
1925
|
+
readonly delayProvenance?: "derived" | "declared" | "clock-law" | null;
|
|
1926
|
+
readonly delayDeclaredReason?: string | null;
|
|
1927
|
+
readonly modulationRefusal?: string | null;
|
|
1928
|
+
/**
|
|
1929
|
+
* The programs this block can run, in declaration order. **Never empty.**
|
|
1930
|
+
*
|
|
1931
|
+
* A composition derived from a registry macro has exactly one, because a
|
|
1932
|
+
* fixed-function part does one thing. A reprogrammable chip has one per
|
|
1933
|
+
* selector position, and `selector` says which pin chooses.
|
|
1934
|
+
*
|
|
1935
|
+
* This replaced a single `ops`/`out`/`lines` triple on the block itself.
|
|
1936
|
+
* Keeping both would have been the dual path the engineering principles
|
|
1937
|
+
* forbid, and the single-program case is not a different shape -- it is
|
|
1938
|
+
* this one with a length of 1.
|
|
1939
|
+
*/
|
|
1940
|
+
readonly positions: readonly ComposedPosition[];
|
|
1941
|
+
/**
|
|
1942
|
+
* How the panel chooses which position runs, or null where there is nothing
|
|
1943
|
+
* to choose.
|
|
1944
|
+
*
|
|
1945
|
+
* **The knob does not reach the runtime; a pin does.** The control is wired to
|
|
1946
|
+
* the chip through the circuit, so the mode is a node voltage the MNA already
|
|
1947
|
+
* solves, exactly as `parameter` is. Reading the control directly would model a
|
|
1948
|
+
* switch the schematic does not have, and would make a packet whose panel is
|
|
1949
|
+
* unwired appear to work.
|
|
1950
|
+
*/
|
|
1951
|
+
readonly router: ComposedRouter | null;
|
|
1952
|
+
};
|
|
1953
|
+
/**
|
|
1954
|
+
* One selectable program: the ops, the op whose output the block publishes, and the
|
|
1955
|
+
* delay lines those ops address.
|
|
1956
|
+
*
|
|
1957
|
+
* Lines are per position because two modes need not share a buffer. Prepare allocates
|
|
1958
|
+
* every position's lines, so a selector change is an index change and never an
|
|
1959
|
+
* allocation -- switching modes on the audio thread must not touch the heap.
|
|
1960
|
+
*/
|
|
1961
|
+
export type ComposedPosition = {
|
|
1962
|
+
/** The declaration's own id for this position, carried for diagnostics. */
|
|
1963
|
+
readonly id: string;
|
|
1964
|
+
/**
|
|
1965
|
+
* Ordered ops evaluated top to bottom each sample. A source may only
|
|
1966
|
+
* name the block input, a constant, or an *earlier* op's output: cycles
|
|
1967
|
+
* flow through delay-line state, never through temporaries, so a
|
|
1968
|
+
* forward reference reads 0 by construction.
|
|
1969
|
+
*/
|
|
1970
|
+
readonly ops: readonly PrimitiveOp[];
|
|
1971
|
+
/** Which op's output this position publishes. */
|
|
1972
|
+
readonly out: number;
|
|
1973
|
+
/** Delay-line capacities, allocated at prepare from delay time. */
|
|
1974
|
+
readonly lines: Readonly<Record<string, ComposedLine>>;
|
|
1975
|
+
};
|
|
1976
|
+
/**
|
|
1977
|
+
* The pin that selects a composed block's running position.
|
|
1978
|
+
*
|
|
1979
|
+
* Same shape and same reading as `ParameterPort`, deliberately: one mechanism for
|
|
1980
|
+
* "a solved node voltage drives this block", used twice. The fraction is
|
|
1981
|
+
* `voltage / referenceVolts`, and the position is that fraction across the
|
|
1982
|
+
* position count, clamped -- so a pin at or below 0 V selects the first position
|
|
1983
|
+
* and one at or above the reference selects the last.
|
|
1984
|
+
*/
|
|
1985
|
+
export type SelectorPort = {
|
|
1986
|
+
/** Which block's solved node the selection is read from. */
|
|
1987
|
+
readonly block: string;
|
|
1988
|
+
readonly node: NodeId;
|
|
1989
|
+
readonly referenceVolts: number;
|
|
1990
|
+
};
|
|
1991
|
+
/**
|
|
1992
|
+
* The router that turns a control's position into a running program.
|
|
1993
|
+
*
|
|
1994
|
+
* **This models the thing that exists.** On a DD-5 the MODE knob is an 11-detent pot
|
|
1995
|
+
* whose wiper reaches the CPU's analog input; the CPU quantizes that voltage and tells
|
|
1996
|
+
* the DSP which program to run. The CPU is a router. Naming it one is more honest than
|
|
1997
|
+
* pretending the DSP reads the knob, and it is the only shape that can say a detent runs
|
|
1998
|
+
* nothing.
|
|
1999
|
+
*
|
|
2000
|
+
* `routes` is dense and `positions` long: `routes[detent]` is an index into the block's
|
|
2001
|
+
* `positions`, or `-1` where the source declares no program for that detent. Dense so a
|
|
2002
|
+
* lookup is an array index rather than a search, because this runs every sample.
|
|
2003
|
+
*/
|
|
2004
|
+
/**
|
|
2005
|
+
* A router as stage 3 can plan it: the node carrying the selection, but not yet which block
|
|
2006
|
+
* owns that node.
|
|
2007
|
+
*
|
|
2008
|
+
* The split exists because `couple.ts` runs after every region is lowered and is the only
|
|
2009
|
+
* stage that can see across blocks. Resolution knows which node the control's wiper is;
|
|
2010
|
+
* only coupling knows which block's solution that node appears in.
|
|
2011
|
+
*/
|
|
2012
|
+
/**
|
|
2013
|
+
* A cited control's node, planned by resolution for `couple.ts` to turn into a port.
|
|
2014
|
+
*
|
|
2015
|
+
* Same split and same reason as `DeclaredRouterPlan`: stage 3 knows which node the control's
|
|
2016
|
+
* wiper is, and only coupling knows whose solution that node appears in.
|
|
2017
|
+
*/
|
|
2018
|
+
export type DeclaredParameterPlan = {
|
|
2019
|
+
readonly controlId: string;
|
|
2020
|
+
/** Null for a scanned control, which needs no node and creates no dependency. */
|
|
2021
|
+
readonly node: NodeId | null;
|
|
2022
|
+
readonly referenceVolts: number | null;
|
|
2023
|
+
};
|
|
2024
|
+
export type DeclaredRouterPlan = {
|
|
2025
|
+
readonly controlId: string;
|
|
2026
|
+
/** Null for a scanned control, which needs no node and creates no dependency. */
|
|
2027
|
+
readonly node: NodeId | null;
|
|
2028
|
+
readonly referenceVolts: number | null;
|
|
2029
|
+
readonly positions: number;
|
|
2030
|
+
readonly routes: readonly number[];
|
|
2031
|
+
};
|
|
2032
|
+
export type ComposedRouter = {
|
|
2033
|
+
/**
|
|
2034
|
+
* The panel control this router reads, for diagnostics only.
|
|
2035
|
+
*
|
|
2036
|
+
* Carried for the same reason `MacroClockControl` carries one: a control that reaches the
|
|
2037
|
+
* program through a port affects nothing the circuit can see, so the inert-control check
|
|
2038
|
+
* would report it dead. A mode pot read by a high-impedance input is exactly that shape --
|
|
2039
|
+
* its wiper drives nothing, and it still chooses the program.
|
|
2040
|
+
*/
|
|
2041
|
+
readonly controlId: string;
|
|
2042
|
+
/**
|
|
2043
|
+
* The node carrying the selection, or **null where the control is scanned** and the
|
|
2044
|
+
* runtime reads its position directly.
|
|
2045
|
+
*
|
|
2046
|
+
* A node is the stronger claim and stays the default. Null is for a control whose path to
|
|
2047
|
+
* the chip no source resolves, where modelling the transport would be precision about the
|
|
2048
|
+
* wrong thing: firmware quantizes that voltage by rules no dump exists for, and the panel
|
|
2049
|
+
* fact the router actually needs is documented.
|
|
2050
|
+
*/
|
|
2051
|
+
readonly port: SelectorPort | null;
|
|
2052
|
+
/** The control's detent count. The reading is quantized across this. */
|
|
2053
|
+
readonly positions: number;
|
|
2054
|
+
/** Program index per detent, or -1 where no program is declared. */
|
|
2055
|
+
readonly routes: readonly number[];
|
|
2056
|
+
};
|
|
2057
|
+
/**
|
|
2058
|
+
* A delay line a composition addresses, sized at prepare from a delay time
|
|
2059
|
+
* exactly like a macro's buffer (delaySeconds times the rate, plus headroom).
|
|
2060
|
+
*/
|
|
2061
|
+
export type ComposedLine = {
|
|
2062
|
+
readonly delaySeconds: number;
|
|
2063
|
+
/**
|
|
2064
|
+
* The shortest delay a `parameter`-mode tap may be asked for. The line is sized from
|
|
2065
|
+
* `delaySeconds`, the longest; a control sweeps between the two.
|
|
2066
|
+
*
|
|
2067
|
+
* **0 wherever the source states no floor**, which reproduces the old `capacity x fraction`
|
|
2068
|
+
* reading exactly -- every registry decomposition. A declared program cites both ends of each
|
|
2069
|
+
* range (`boss-dd-5` DELAY 2 is 50 to 200 ms), and reading only the top made the knob sweep
|
|
2070
|
+
* from zero: a quarter of that mode's travel produced delays its own panel legend excludes.
|
|
2071
|
+
*/
|
|
2072
|
+
readonly minSeconds: number;
|
|
2073
|
+
/**
|
|
2074
|
+
* The control that sweeps this line's length, read directly because a chip reads it and the
|
|
2075
|
+
* path is not modelled. **Per line, not per block**: a DD-5's DELAY positions read D.TIME and
|
|
2076
|
+
* its TEMPO positions read the tapped TEMPO jack, so one block-wide control cannot hold both.
|
|
2077
|
+
* Absent where no control sweeps the line, or where a solved node does (the block's
|
|
2078
|
+
* `parameter` port, a registry model's reading).
|
|
2079
|
+
*
|
|
2080
|
+
* `scanned`: length is `minSeconds + taper(position) x (delaySeconds - minSeconds)`.
|
|
2081
|
+
* `tapped`: length is the interval between the control's last two presses times `ratio`,
|
|
2082
|
+
* clamped to `minSeconds..delaySeconds`; until two presses, `delaySeconds`.
|
|
2083
|
+
*/
|
|
2084
|
+
readonly sweep?: ComposedLineSweep;
|
|
2085
|
+
};
|
|
2086
|
+
export type ComposedLineSweep = {
|
|
2087
|
+
readonly controlId: string;
|
|
2088
|
+
readonly read: "scanned" | "tapped";
|
|
2089
|
+
readonly ratio: number;
|
|
2090
|
+
};
|
|
2091
|
+
/** Where a composed op reads a sample from. */
|
|
2092
|
+
export type ComposedSource = {
|
|
2093
|
+
readonly kind: "input";
|
|
2094
|
+
} | {
|
|
2095
|
+
readonly kind: "const";
|
|
2096
|
+
readonly value: number;
|
|
2097
|
+
} | {
|
|
2098
|
+
readonly kind: "temp";
|
|
2099
|
+
readonly index: number;
|
|
2100
|
+
};
|
|
2101
|
+
/** One multiply-accumulate term: `gain` times `source`, summed left to right. */
|
|
2102
|
+
export type ComposedTerm = {
|
|
2103
|
+
readonly source: ComposedSource;
|
|
2104
|
+
readonly gain: number;
|
|
2105
|
+
/**
|
|
2106
|
+
* A knob that sweeps this gain, read by position through its taper: the gain is
|
|
2107
|
+
* `min + fraction x (max - min)` and `gain` is not used. **Absent means fixed**, which is every
|
|
2108
|
+
* registry decomposition, so their programs are unchanged by the field existing.
|
|
2109
|
+
*
|
|
2110
|
+
* From a declared program parameter (`{ parameter: <name> }` on the term), scanned only: a
|
|
2111
|
+
* DD-5's F.BACK and E.LEVEL reach the CPU the way D.TIME does, and a node-read gain has no port
|
|
2112
|
+
* to arrive through, so it is refused by name rather than guessed.
|
|
2113
|
+
*/
|
|
2114
|
+
readonly sweep?: ComposedGainSweep;
|
|
2115
|
+
};
|
|
2116
|
+
export type ComposedGainSweep = {
|
|
2117
|
+
readonly controlId: string;
|
|
2118
|
+
readonly min: number;
|
|
2119
|
+
readonly max: number;
|
|
2120
|
+
};
|
|
2121
|
+
/**
|
|
2122
|
+
* How a delay tap op derives its read length each sample. The modes mirror
|
|
2123
|
+
* the delay kernels' own branches in the same priority: a clock-smoothed
|
|
2124
|
+
* control first, then a signal-rate modulation scale, then a parameter-port
|
|
2125
|
+
* scale, else the full capacity. `clock-law` is the open-OX2 relaxation law:
|
|
2126
|
+
* the per-sample delay of a BBD whose clock derives from its own network
|
|
2127
|
+
* rather than from a declared number or a ratio around one.
|
|
2128
|
+
*/
|
|
2129
|
+
export type DelayLengthSpec = {
|
|
2130
|
+
readonly mode: "capacity";
|
|
2131
|
+
} | {
|
|
2132
|
+
readonly mode: "clock";
|
|
2133
|
+
} | {
|
|
2134
|
+
readonly mode: "modulation";
|
|
2135
|
+
} | {
|
|
2136
|
+
readonly mode: "clock-law";
|
|
2137
|
+
/** Pull-up resistance (Ω) charging the timing capacitor. */
|
|
2138
|
+
readonly rOhms: number;
|
|
2139
|
+
/** Timing capacitance (F) between OX3 and OX1. */
|
|
2140
|
+
readonly cFarads: number;
|
|
2141
|
+
/** Driver supply span (V) the capacitor charges toward. */
|
|
2142
|
+
readonly vddVolts: number;
|
|
2143
|
+
/** Inverter input threshold (V): specimen-calibrated, disclosed. */
|
|
2144
|
+
readonly vthVolts: number;
|
|
2145
|
+
/** Slow-diode forward drop (V): generic-diode typical, disclosed. */
|
|
2146
|
+
readonly vfVolts: number;
|
|
2147
|
+
/** Yank-minus-kick bottom (V) under datasheet OX3 drive. */
|
|
2148
|
+
readonly floorVolts: number;
|
|
2149
|
+
/** Brigade stages: delay = stages × charge time. */
|
|
2150
|
+
readonly stages: number;
|
|
2151
|
+
} | {
|
|
2152
|
+
readonly mode: "parameter";
|
|
2153
|
+
};
|
|
2154
|
+
/**
|
|
2155
|
+
* The closed primitive vocabulary (board-p3 row 4). Growing it is a format
|
|
2156
|
+
* change with a gate; an op the runtime does not implement is a refusal
|
|
2157
|
+
* naming it, never silence. Delay, mix, coupling filter, comb, allpass,
|
|
2158
|
+
* and pitch-shift are implemented; `converter` and `pitch-tracker` arrive
|
|
2159
|
+
* only if a decomposition (or row 6) needs them.
|
|
2160
|
+
*/
|
|
2161
|
+
export type PrimitiveOp = {
|
|
2162
|
+
readonly op: "delay-tap";
|
|
2163
|
+
readonly line: string;
|
|
2164
|
+
readonly length: DelayLengthSpec;
|
|
2165
|
+
readonly out: number;
|
|
2166
|
+
} | {
|
|
2167
|
+
readonly op: "delay-tap-fractional";
|
|
2168
|
+
readonly line: string;
|
|
2169
|
+
readonly length: DelayLengthSpec;
|
|
2170
|
+
readonly out: number;
|
|
2171
|
+
} | {
|
|
2172
|
+
/**
|
|
2173
|
+
* Reverse playback: the line's most recent `length` samples, read backwards, one
|
|
2174
|
+
* segment after another. **A stated approximation, not a recovered law** -- no
|
|
2175
|
+
* reverse-delay firmware in the corpus has been dumped, so segmentation and crossfade
|
|
2176
|
+
* are this op's own: two read heads half a segment apart, each weighted
|
|
2177
|
+
* `sin^2(pi phase / length)`, so the weights sum to exactly 1 and every segment jump
|
|
2178
|
+
* happens under a zero weight. A head at phase `p` reads `2p + 1` samples back, so the
|
|
2179
|
+
* line holds twice its declared length. At most one per line: the phase lives there.
|
|
2180
|
+
*/
|
|
2181
|
+
readonly op: "delay-tap-reverse";
|
|
2182
|
+
readonly line: string;
|
|
2183
|
+
readonly length: DelayLengthSpec;
|
|
2184
|
+
readonly out: number;
|
|
2185
|
+
} | {
|
|
2186
|
+
/**
|
|
2187
|
+
* A hold sampler, gated by a pedal. **A stated approximation of firmware nobody has
|
|
2188
|
+
* dumped**, taken from a manual's procedure: idle until the gate rises; then record
|
|
2189
|
+
* `input` into the line while the gate is held, up to the line's length; on release,
|
|
2190
|
+
* loop what was recorded; on the next rise, stop and erase, back to idle. Publishes 0
|
|
2191
|
+
* except while looping. The recording is also erased when the program is selected
|
|
2192
|
+
* afresh (a mode change). One per line, and no `delay-push` on the same line: the
|
|
2193
|
+
* record position lives there.
|
|
2194
|
+
*/
|
|
2195
|
+
readonly op: "hold-loop";
|
|
2196
|
+
readonly line: string;
|
|
2197
|
+
readonly input: ComposedSource;
|
|
2198
|
+
/** The pedal, read like a swept gain; the gate is on at 0.5 or above. */
|
|
2199
|
+
readonly gate: ComposedGainSweep;
|
|
2200
|
+
readonly out: number;
|
|
2201
|
+
} | {
|
|
2202
|
+
readonly op: "delay-push";
|
|
2203
|
+
readonly line: string;
|
|
2204
|
+
readonly input: ComposedSource;
|
|
2205
|
+
} | {
|
|
2206
|
+
readonly op: "mix";
|
|
2207
|
+
readonly terms: readonly ComposedTerm[];
|
|
2208
|
+
readonly out: number;
|
|
2209
|
+
} | {
|
|
2210
|
+
/** One-pole AC blocker: the BBD input coupling estimator as an op. */
|
|
2211
|
+
readonly op: "filter-dcblock";
|
|
2212
|
+
readonly input: ComposedSource;
|
|
2213
|
+
readonly out: number;
|
|
2214
|
+
} | {
|
|
2215
|
+
/**
|
|
2216
|
+
* Schroeder feedback comb: reads its delay, writes back input
|
|
2217
|
+
* plus gain times delayed. `index` addresses the interpreter's
|
|
2218
|
+
* fixed echo-density table (like the kernel it replaces); only
|
|
2219
|
+
* `decaySeconds` varies per fitting, carried here.
|
|
2220
|
+
*/
|
|
2221
|
+
readonly op: "comb";
|
|
2222
|
+
readonly index: number;
|
|
2223
|
+
readonly decaySeconds: number;
|
|
2224
|
+
readonly input: ComposedSource;
|
|
2225
|
+
readonly out: number;
|
|
2226
|
+
} | {
|
|
2227
|
+
/**
|
|
2228
|
+
* Schroeder allpass diffuser, same index-addressed table shape as
|
|
2229
|
+
* `comb`. The diffusion coefficient is interpreter-owned (as it
|
|
2230
|
+
* is kernel-owned today), not packet data.
|
|
2231
|
+
*/
|
|
2232
|
+
readonly op: "allpass";
|
|
2233
|
+
readonly index: number;
|
|
2234
|
+
readonly input: ComposedSource;
|
|
2235
|
+
readonly out: number;
|
|
2236
|
+
} | {
|
|
2237
|
+
/**
|
|
2238
|
+
* Static interval transposition by asynchronous resampling
|
|
2239
|
+
* (board-p3 row 5): the read pointer advances `ratio` samples per
|
|
2240
|
+
* output sample through a fixed history window, wrapping with a
|
|
2241
|
+
* documented discontinuity when it outruns or falls behind. Justified
|
|
2242
|
+
* as a primitive by the residual rule: no composition of fixed
|
|
2243
|
+
* delay, mix, and filter transposes -- a fixed delay preserves the
|
|
2244
|
+
* fundamental by construction -- so clause 1 has no compositional
|
|
2245
|
+
* route. `doesNotReproduce`: glitch-free transposition (no
|
|
2246
|
+
* crossfade), formant preservation.
|
|
2247
|
+
*/
|
|
2248
|
+
readonly op: "pitch-shift";
|
|
2249
|
+
readonly ratio: number;
|
|
2250
|
+
readonly input: ComposedSource;
|
|
2251
|
+
readonly out: number;
|
|
2252
|
+
} | {
|
|
2253
|
+
/**
|
|
2254
|
+
* Fundamental estimator by autocorrelation (board-p3 row 6): the
|
|
2255
|
+
* period with peak normalized correlation over a fixed window,
|
|
2256
|
+
* parabolically refined, graded against a reference with a
|
|
2257
|
+
* tolerance -- never a pin. Justified as a primitive by the same
|
|
2258
|
+
* residual rule: no composition of delay, mix, and filter
|
|
2259
|
+
* estimates a fundamental (they transform samples; none measures
|
|
2260
|
+
* periodicity). `doesNotReproduce`: octave certainty on all
|
|
2261
|
+
* timbres, overlapping voices (monophonic assumption throughout).
|
|
2262
|
+
*/
|
|
2263
|
+
readonly op: "pitch-tracker";
|
|
2264
|
+
readonly input: ComposedSource;
|
|
2265
|
+
readonly out: number;
|
|
2266
|
+
};
|
|
2267
|
+
/**
|
|
2268
|
+
* The `parameter` port: a scalar another operator derives, admitted only under the PURITY
|
|
2269
|
+
* precondition `clock-parameter-port-gate` measured -- gate 1b, and the compiler-plan's
|
|
2270
|
+
* operator-format spec clause 1. `referenceVolts` normalises the read node's solved voltage to
|
|
2271
|
+
* a ~1.0-scale factor the macro's own parameters (e.g. `stages`) are multiplied by, so a
|
|
2272
|
+
* fixture's registry data stays in its own natural units rather than volts.
|
|
2273
|
+
*/
|
|
2274
|
+
export type ParameterPort = {
|
|
2275
|
+
/** Which block's solved node this scalar is derived from. */
|
|
2276
|
+
readonly block: string;
|
|
2277
|
+
readonly node: NodeId;
|
|
2278
|
+
readonly referenceVolts: number;
|
|
2279
|
+
};
|
|
2280
|
+
/**
|
|
2281
|
+
* The `modulation` port: a solved node read every sample, scaling a delay macro's length.
|
|
2282
|
+
*
|
|
2283
|
+
* **The node is a steering device's control terminal, never the timing node itself.** A BBD's
|
|
2284
|
+
* timing node carries the oscillator at the clock rate -- ~100 kHz for a chorus -- which no
|
|
2285
|
+
* 48 kHz host can sample; reading it would alias. The control terminal carries the LFO alone.
|
|
2286
|
+
*
|
|
2287
|
+
* The law the runtime applies is `delay = base * (Vdc / V)`, where `Vdc` is the node's own
|
|
2288
|
+
* settled level. A steering transistor converts its control voltage into the charging current
|
|
2289
|
+
* of the oscillator's timing capacitor, so `f_clock` rises with `V` and the delay, being
|
|
2290
|
+
* `stages / (2 * f_clock)`, falls with it. Self-normalising by construction: at the operating
|
|
2291
|
+
* point `V == Vdc`, the scale is 1, and the delay is exactly the base the program carries.
|
|
2292
|
+
*
|
|
2293
|
+
* **What this does not claim.** The linear voltage-to-current assumption is the steering
|
|
2294
|
+
* device's small-signal behaviour, not a datasheet law, and no absolute clock frequency is
|
|
2295
|
+
* derived anywhere. The port modulates a base delay whose own provenance is unchanged and
|
|
2296
|
+
* separately reported by `delayProvenance` -- so a packet whose base is `declared` gets a sweep
|
|
2297
|
+
* around an unevidenced centre, which is an improvement in kind and not a fidelity claim.
|
|
2298
|
+
*/
|
|
2299
|
+
export type ModulationPort = {
|
|
2300
|
+
/** Which block's solved node this is read from, every sample. */
|
|
2301
|
+
readonly block: string;
|
|
2302
|
+
readonly node: NodeId;
|
|
2303
|
+
/** The device whose control terminal this is, carried for diagnostics. */
|
|
2304
|
+
readonly steeredBy: string;
|
|
2305
|
+
};
|
|
2306
|
+
/**
|
|
2307
|
+
* The `clock-law` port: a solved node read every sample, driving an open-OX2
|
|
2308
|
+
* relaxation law rather than a ratio around a declared delay.
|
|
2309
|
+
*
|
|
2310
|
+
* **The node is the slow diode's anode-side divider node, never a steering
|
|
2311
|
+
* transistor's control terminal.** The transistor's base carries the clock-rate
|
|
2312
|
+
* waveform (and on the rewire it floats at millivolts); the divider node
|
|
2313
|
+
* carries the LFO and nothing else. `steeredBy` names the discharge
|
|
2314
|
+
* transistor for diagnostics, mirroring `ModulationPort`.
|
|
2315
|
+
*/
|
|
2316
|
+
export type ClockLawPort = {
|
|
2317
|
+
/** Which block's solved node this is read from, every sample. */
|
|
2318
|
+
readonly block: string;
|
|
2319
|
+
readonly node: NodeId;
|
|
2320
|
+
/** The discharge transistor, carried for diagnostics. */
|
|
2321
|
+
readonly steeredBy: string;
|
|
2322
|
+
};
|
|
2323
|
+
/**
|
|
2324
|
+
* The open-OX2 law constants a `clock-law` length mode evaluates per sample.
|
|
2325
|
+
* R and C come from the packet's own network; the rest are the family
|
|
2326
|
+
* constants derived in `bbd-clock.ts` (specimen-calibrated threshold,
|
|
2327
|
+
* disclosed typicals), carried here so both consoles read the same numbers.
|
|
2328
|
+
*/
|
|
2329
|
+
export type ClockLawParams = {
|
|
2330
|
+
readonly rOhms: number;
|
|
2331
|
+
readonly cFarads: number;
|
|
2332
|
+
readonly vddVolts: number;
|
|
2333
|
+
readonly vthVolts: number;
|
|
2334
|
+
readonly vfVolts: number;
|
|
2335
|
+
readonly floorVolts: number;
|
|
2336
|
+
};
|
|
2337
|
+
/**
|
|
2338
|
+
* A `.nodeset`-style hint: where the operating-point solve starts, not a forced voltage.
|
|
2339
|
+
*
|
|
2340
|
+
* Newton still solves the circuit. This exists because a circuit with more than one DC
|
|
2341
|
+
* solution has its answer *chosen* by the initial guess, and `fill(0)` chooses the
|
|
2342
|
+
* symmetric one — see `latch-seed.ts`.
|
|
2343
|
+
*/
|
|
2344
|
+
export type OperatingPointSeed = {
|
|
2345
|
+
readonly node: NodeId;
|
|
2346
|
+
readonly volts: number;
|
|
2347
|
+
};
|
|
2348
|
+
/**
|
|
2349
|
+
* Rate-independent facts about the *executed* program (`order`'s blocks, not every
|
|
2350
|
+
* retained one — the same rule `requiredOperators` follows and for the identical reason:
|
|
2351
|
+
* a block `executionOrder` prunes never runs under any runtime, so its cost cannot be a
|
|
2352
|
+
* cost this program's execution needs to be admitted against) that a real-time admission
|
|
2353
|
+
* decision needs.
|
|
2354
|
+
*
|
|
2355
|
+
* **Deliberately excludes any bound on Newton iteration counts.** Peak and steady
|
|
2356
|
+
* iterations are properties of the signal and the control position, not of the circuit
|
|
2357
|
+
* alone — `scripts/report-newton-deadline.ts` measures them empirically for exactly this
|
|
2358
|
+
* reason, because there is no static derivation. Fabricating one here would be worse than
|
|
2359
|
+
* declaring none: an admission surface built on invented numbers is worse than no
|
|
2360
|
+
* admission surface. What bounds worst-case cost instead is the enforced iteration *cap*
|
|
2361
|
+
* (`prepare()`'s own `maxNewtonIterations`), which is a host/runtime parameter rather than
|
|
2362
|
+
* a program fact — it does not describe what this circuit needs, it describes how much
|
|
2363
|
+
* iteration any circuit is allowed before its sample is held rather than iterated further.
|
|
2364
|
+
* See `src/runtime/admission.ts` for how the cap and these predictors combine with a
|
|
2365
|
+
* host-supplied budget into a fail-closed decision, and why that combination — not this
|
|
2366
|
+
* type alone — is what the tension "cost is program x machine x rate x block size" means
|
|
2367
|
+
* in practice: this type is the program's honest half only.
|
|
2368
|
+
*/
|
|
2369
|
+
export type CostPredictors = {
|
|
2370
|
+
/** `order.length` — the blocks actually solved once per sample. */
|
|
2371
|
+
readonly executedBlockCount: number;
|
|
2372
|
+
/** Reactive/state memory summed across executed blocks. */
|
|
2373
|
+
readonly stateCount: number;
|
|
2374
|
+
/**
|
|
2375
|
+
* Every executed MNA block's solved system size, and whether it may iterate.
|
|
2376
|
+
* `linear: true` solves once per sample; `linear: false` may iterate up to the
|
|
2377
|
+
* runtime's enforced cap. A `macro` block contributes nothing here — its `modelId`
|
|
2378
|
+
* names DSP rather than a stamp, so it presents no system to solve. That is also why it
|
|
2379
|
+
* contributes nothing to `requiredOperators`, and why it is declared in
|
|
2380
|
+
* `requiredModels` instead rather than left undeclared.
|
|
2381
|
+
*
|
|
2382
|
+
* A macro's own per-sample cost is declared in `macroBlocks` instead — see that field for
|
|
2383
|
+
* why it is a parallel list rather than a fabricated `unknownCount` here.
|
|
2384
|
+
*/
|
|
2385
|
+
readonly solvedBlocks: readonly {
|
|
2386
|
+
readonly blockId: string;
|
|
2387
|
+
/** `nodeCount + auxCount` — the size of the dense system one solve involves. */
|
|
2388
|
+
readonly unknownCount: number;
|
|
2389
|
+
readonly linear: boolean;
|
|
2390
|
+
}[];
|
|
2391
|
+
/**
|
|
2392
|
+
* Every executed `macro` block, by id and by the algorithm it names.
|
|
2393
|
+
*
|
|
2394
|
+
* **This field exists because its absence was a hole in the gate, not a rounding error.**
|
|
2395
|
+
* A macro presents no system to solve, so it contributes nothing to `solvedBlocks` — and
|
|
2396
|
+
* admission summed `solvedBlocks` and nothing else, which meant a macro's per-sample work
|
|
2397
|
+
* was not merely under-counted but *invisible*: a program of nothing but macro blocks was
|
|
2398
|
+
* costed at zero and admitted unconditionally. That was survivable while one program held
|
|
2399
|
+
* one delay line; it is not survivable in a chain, where the whole premise is that costs
|
|
2400
|
+
* add and a slot whose cost reads zero silently buys headroom the host does not have.
|
|
2401
|
+
*
|
|
2402
|
+
* **It carries no number, deliberately, and that is the point.** Nothing in a `modelId` or
|
|
2403
|
+
* its `parameters` bounds an algorithm's work, so a per-model nanosecond constant invented
|
|
2404
|
+
* here would be the admission surface built on invented numbers this type's own doc
|
|
2405
|
+
* refuses. What a program can honestly declare is *which algorithms run, and how many
|
|
2406
|
+
* times per sample each one runs* — exactly the rate- and machine-independent half.
|
|
2407
|
+
* Pricing them is the host's half, supplied as `RealtimeBudget.nsPerMacroSample`
|
|
2408
|
+
* (`src/runtime/admission.ts`), which refuses by name when it has no price for a model
|
|
2409
|
+
* this list declares. Same division as `requiredModels`: the compiler declares, the
|
|
2410
|
+
* runtime refuses.
|
|
2411
|
+
*
|
|
2412
|
+
* **Still not gated: memory.** A bucket brigade's ring buffer is `delaySeconds x rate`
|
|
2413
|
+
* slots, which is not a program fact (the rate is the host's) and is not a *time* cost,
|
|
2414
|
+
* which is the only axis this budget has. Admission gates ns/sample; it does not gate
|
|
2415
|
+
* allocation, and a chain of long delay lines can therefore still exhaust memory while
|
|
2416
|
+
* passing this gate. Naming that here is the honest state rather than a second
|
|
2417
|
+
* host-supplied axis nothing has asked for yet.
|
|
2418
|
+
*/
|
|
2419
|
+
readonly macroBlocks: readonly {
|
|
2420
|
+
readonly blockId: string;
|
|
2421
|
+
/** The algorithm, as declared in `requiredModels`. */
|
|
2422
|
+
readonly modelId: string;
|
|
2423
|
+
}[];
|
|
2424
|
+
};
|
|
2425
|
+
export type Program = {
|
|
2426
|
+
readonly formatVersion: 6;
|
|
2427
|
+
/**
|
|
2428
|
+
* Every operator this program needs a runtime to implement, sorted and without
|
|
2429
|
+
* duplicates.
|
|
2430
|
+
*
|
|
2431
|
+
* **This is the version surface, and it is deliberately the whole of it.**
|
|
2432
|
+
* `formatVersion` versions the container — which keys exist, how they nest — and says
|
|
2433
|
+
* nothing about the instruction set, so a program using an operator the runtime
|
|
2434
|
+
* predates had no defined behaviour: the executor's dispatch was exhaustive over a
|
|
2435
|
+
* closed TypeScript union, which is a compile-time guarantee inside one repository and
|
|
2436
|
+
* no guarantee at all across a serialized program or a second implementer. Declaring
|
|
2437
|
+
* the set lets the refusal happen at load, by name, before a note is played, rather
|
|
2438
|
+
* than mid-buffer when execution first meets the operator.
|
|
2439
|
+
*
|
|
2440
|
+
* It is emitted from the stamps the *executed* blocks actually present -- `order`'s, not
|
|
2441
|
+
* every block `blocks` retains -- so it is exact rather than conservative: a program of
|
|
2442
|
+
* resistors declares one operator, adding a device to a packet widens the declaration by
|
|
2443
|
+
* exactly what the device stamps, and a block pruned for not reaching the output (never
|
|
2444
|
+
* executed, by any runtime) contributes nothing here even though it stays in `blocks`.
|
|
2445
|
+
* See `link.ts`'s `requiredOperators` for the corpus evidence this exclusion is load-
|
|
2446
|
+
* bearing on, not merely tidy.
|
|
2447
|
+
*
|
|
2448
|
+
* A `macro` block contributes nothing here, and its `modelId` is declared in
|
|
2449
|
+
* `requiredModels` instead — see that field for why the two sets are parallel rather
|
|
2450
|
+
* than merged.
|
|
2451
|
+
*/
|
|
2452
|
+
readonly requiredOperators: readonly OperatorKind[];
|
|
2453
|
+
/**
|
|
2454
|
+
* Every DSP algorithm this program needs a runtime to implement, sorted and without
|
|
2455
|
+
* duplicates — the `macro` half of the same version surface `requiredOperators` is the
|
|
2456
|
+
* stamp half of, and read at load by the same lockout for the same reason.
|
|
2457
|
+
*
|
|
2458
|
+
* **Why a parallel set rather than `modelId` joining `requiredOperators`** — the open
|
|
2459
|
+
* question the operator-format decision left behind, answered here rather than deferred
|
|
2460
|
+
* again. Three reasons, in the order they bite:
|
|
2461
|
+
*
|
|
2462
|
+
* - **They are two vocabularies, and one flat set cannot keep them apart.**
|
|
2463
|
+
* `requiredOperators` is typed `OperatorKind` = `Stamp["kind"]`, derived from the stamp
|
|
2464
|
+
* union precisely so a new stamp kind cannot be added without joining the declared set.
|
|
2465
|
+
* A `modelId` is free-form registry text (artifact-owned, see `MacroModel.modelId`), so
|
|
2466
|
+
* merging would have to widen that type to `string` and would delete the derivation that
|
|
2467
|
+
* makes the operator half honest.
|
|
2468
|
+
* - **Merged, a name collision is a silent false pass.** A registry entry whose `modelId`
|
|
2469
|
+
* happened to be `diode` would be satisfied by the runtime's *stamp* implementation of a
|
|
2470
|
+
* diode and load cleanly — a plausible, silent, completely wrong admission of exactly the
|
|
2471
|
+
* shape this file's `ac-source` note argues against.
|
|
2472
|
+
* - **The two demand different work from an implementer.** A missing operator is a stamp
|
|
2473
|
+
* function in the MNA assembler; a missing model is a DSP algorithm with its own state.
|
|
2474
|
+
* A refusal that cannot say which one is missing cannot say what a runtime would need to
|
|
2475
|
+
* gain, which is the whole purpose of naming it.
|
|
2476
|
+
*
|
|
2477
|
+
* Emitted from the *executed* macro blocks, the same exactness rule `requiredOperators`
|
|
2478
|
+
* follows. This compiler deliberately does **not** validate the strings: which algorithms
|
|
2479
|
+
* exist is a property of the runtime doing the executing, not of the program, so the
|
|
2480
|
+
* compiler declares and the runtime refuses — the same division that lets one artifact be
|
|
2481
|
+
* valid on a host whose instruction set is a superset and refused, by name, on one whose
|
|
2482
|
+
* is not.
|
|
2483
|
+
*/
|
|
2484
|
+
readonly requiredModels: readonly string[];
|
|
2485
|
+
/**
|
|
2486
|
+
* What this program can honestly declare about its own real-time cost, independent of
|
|
2487
|
+
* rate, host CPU, or block size. See `CostPredictors`'s own doc for what it omits and
|
|
2488
|
+
* why: a program alone cannot say whether it is playable, because playability is
|
|
2489
|
+
* program x machine x rate x block size, not a program property — this is the program's
|
|
2490
|
+
* honest half of that product, and `src/runtime/admission.ts` is where a host's half
|
|
2491
|
+
* joins it.
|
|
2492
|
+
*/
|
|
2493
|
+
readonly costPredictors: CostPredictors;
|
|
2494
|
+
readonly blocks: readonly Block[];
|
|
2495
|
+
/** Execution order. */
|
|
2496
|
+
readonly order: readonly string[];
|
|
2497
|
+
readonly controls: readonly Control[];
|
|
2498
|
+
/**
|
|
2499
|
+
* Absent by construction: there is no sampleRate field. A program is
|
|
2500
|
+
* rate-independent and is given a rate at initialization.
|
|
2501
|
+
*/
|
|
2502
|
+
readonly ports: Ports;
|
|
2503
|
+
/**
|
|
2504
|
+
* Which terminal of the supply this circuit references to ground, from the signs of its
|
|
2505
|
+
* lowered `dc-source` stamps. See `../compiler/supply-reference.ts` for the derivation and
|
|
2506
|
+
* for why barrel polarity (center-negative or center-positive) is deliberately not part of
|
|
2507
|
+
* it.
|
|
2508
|
+
*
|
|
2509
|
+
* Not a simulation input — a program is solved against its declared rails whichever this
|
|
2510
|
+
* says. It is here because it is the one supply fact a **chain** needs: pedals sharing one
|
|
2511
|
+
* physical supply must agree about which terminal is ground, and
|
|
2512
|
+
* `src/runtime/supply-ground.ts` is where a chain of programs can be checked for that.
|
|
2513
|
+
*/
|
|
2514
|
+
readonly supplyReference: "negative-ground" | "positive-ground" | "dual-rail" | "unpowered";
|
|
2515
|
+
/**
|
|
2516
|
+
* The largest voltage each port can physically present, or `null` where the program states no
|
|
2517
|
+
* supply to derive it from. See `./port-full-scale.ts` for the derivation and for why an
|
|
2518
|
+
* upper bound is the honest quantity here.
|
|
2519
|
+
*
|
|
2520
|
+
* This is what lets a chain compare two blocks: `src/runtime/chain.ts` scales between slots by
|
|
2521
|
+
* the ratio of one slot's output full scale to the next's input full scale, so a pedal, an amp
|
|
2522
|
+
* and a NAM profile are interchangeable in a slot instead of coincidentally compatible.
|
|
2523
|
+
*/
|
|
2524
|
+
/**
|
|
2525
|
+
* The 0 dBFS reference each port's own jack **declares** (`V0dBFS`), in volts, or `null`.
|
|
2526
|
+
*
|
|
2527
|
+
* Distinct from `portFullScaleVolts`, which is a *ceiling* derived from the rails: 98 of the
|
|
2528
|
+
* 142 corpus documents declare an input reference at 0.1 to 1 V while their derived output
|
|
2529
|
+
* ceiling is a 9 V supply, and dividing one by the other is not a level match. A consumer
|
|
2530
|
+
* reads whichever its question needs -- see `link.ts` for the measurement that separated them.
|
|
2531
|
+
*/
|
|
2532
|
+
readonly portReferenceVolts: {
|
|
2533
|
+
readonly input: number | null;
|
|
2534
|
+
readonly output: number | null;
|
|
2535
|
+
};
|
|
2536
|
+
readonly portFullScaleVolts: {
|
|
2537
|
+
readonly input: number | null;
|
|
2538
|
+
readonly output: number | null;
|
|
2539
|
+
};
|
|
2540
|
+
/**
|
|
2541
|
+
* How far down the amplification chain this program's output already is — see
|
|
2542
|
+
* `./stage-coverage.ts`. `miked` is never derived from a circuit; it exists for a NAM profile
|
|
2543
|
+
* and for the chain rule that a miked signal must not be sent through a cabinet again.
|
|
2544
|
+
*/
|
|
2545
|
+
readonly stageCoverage: "instrument" | "preamp" | "speaker-electrical" | "miked";
|
|
2546
|
+
/**
|
|
2547
|
+
* The impedance each port declares, for the chain's seam divider (§3.3). `null` per port where
|
|
2548
|
+
* the source states none, which is almost everywhere: zero corpus input jacks declare one.
|
|
2549
|
+
*
|
|
2550
|
+
* A circuit always *has* an electrical port even when its impedance is unstated, which is why
|
|
2551
|
+
* this is an object of nullables rather than a nullable object. The runtime's `SlotContract`
|
|
2552
|
+
* adds the second case -- a whole-object `null` for a NAM or an impulse response, whose ports
|
|
2553
|
+
* are digital and have no impedance to state.
|
|
2554
|
+
*/
|
|
2555
|
+
readonly portImpedanceOhms: {
|
|
2556
|
+
readonly input: number | null;
|
|
2557
|
+
readonly output: number | null;
|
|
2558
|
+
};
|
|
2559
|
+
/**
|
|
2560
|
+
* Bypass hardware characteristics derived from declared `audio.bypass` and circuit topology.
|
|
2561
|
+
*
|
|
2562
|
+
* A discriminated union:
|
|
2563
|
+
* - `{ declared: "none" }` for packets with no declared bypass switch (core-only models).
|
|
2564
|
+
* `kind` is strictly absent to prevent synthetic true-bypass assumptions.
|
|
2565
|
+
* - `{ declared: "switch", kind, control? }` where kind is derived from topological connectivity:
|
|
2566
|
+
* - `buffered`: bistable latch or electronic switching gates
|
|
2567
|
+
* - `true-bypass`: mechanical switch providing input and output isolation
|
|
2568
|
+
* - `hardwire`: mechanical switch switching output while leaving input circuit permanently loaded
|
|
2569
|
+
*/
|
|
2570
|
+
readonly bypass: ProgramBypass;
|
|
2571
|
+
};
|
|
2572
|
+
/**
|
|
2573
|
+
* How the declared bypass switch routes audio, derived from connectivity in `bypass.ts`.
|
|
2574
|
+
*
|
|
2575
|
+
* - `true-bypass`: a mechanical switch isolates both jacks from the effect core; the bypass
|
|
2576
|
+
* path is a wire.
|
|
2577
|
+
* - `buffered`: **electronic** switching — a cross-coupled bistable, an analog-switch IC, or
|
|
2578
|
+
* JFET audio gates with their BJT drivers. The bypass path runs through the pedal's own
|
|
2579
|
+
* input and output buffers (Boss, Ibanez, DOD).
|
|
2580
|
+
* - `buffered-mechanical`: a mechanical switch, but at least one port reaches the switch only
|
|
2581
|
+
* through an active stage, so the bypass path is buffered without any electronic switching
|
|
2582
|
+
* (Klon, Moog, a JFET buffer ahead of a DPDT). Added 2026-09-22: before it, such a pedal
|
|
2583
|
+
* derived as `hardwire` and the chain played its bypass as a wire, losing the buffer.
|
|
2584
|
+
* - `hardwire`: the switch selects the output while the input jack stays tied to the effect
|
|
2585
|
+
* core; the bypass path is a wire that still loads the input (vintage MXR, fuzzes).
|
|
2586
|
+
* - `not-in-audio-path`: the declared switch touches no audio path (an LED contact, or a
|
|
2587
|
+
* view-only shell that was dropped).
|
|
2588
|
+
*
|
|
2589
|
+
* `buffered` and `buffered-mechanical` both resolve to no default chain mode in
|
|
2590
|
+
* `resolveBypassMode` until the buffer program exists (board-p2 row 8 step 3).
|
|
2591
|
+
*/
|
|
2592
|
+
export type ProgramBypassKind = "true-bypass" | "buffered" | "buffered-mechanical" | "hardwire" | "not-in-audio-path";
|
|
2593
|
+
export type ProgramBypass = {
|
|
2594
|
+
readonly declared: "none";
|
|
2595
|
+
} | {
|
|
2596
|
+
readonly declared: "switch";
|
|
2597
|
+
readonly kind: ProgramBypassKind;
|
|
2598
|
+
readonly control?: ControlId;
|
|
2599
|
+
};
|
|
2600
|
+
export type CompileRefusal = {
|
|
2601
|
+
/**
|
|
2602
|
+
* Which stage refused. `device-laws` means the document was read and a part could
|
|
2603
|
+
* not be modelled; any other value means the document itself does not support a
|
|
2604
|
+
* program. A caller can tell those apart without catching anything.
|
|
2605
|
+
*/
|
|
2606
|
+
readonly stage: "netlist" | "device-laws" | "partition" | "lower" | "link";
|
|
2607
|
+
/** The component the refusal is about, or null when it is document-level. */
|
|
2608
|
+
readonly device: DeviceId | null;
|
|
2609
|
+
readonly reason: string;
|
|
2610
|
+
};
|
|
2611
|
+
export type CompileFailure = {
|
|
2612
|
+
readonly status: "unsupported";
|
|
2613
|
+
readonly reasons: readonly CompileRefusal[];
|
|
2614
|
+
};
|
|
2615
|
+
export type CompileSuccess = {
|
|
2616
|
+
readonly status: "ok";
|
|
2617
|
+
readonly program: Program;
|
|
2618
|
+
/**
|
|
2619
|
+
* Things that compiled but will not behave. A warning never blocks a program --
|
|
2620
|
+
* a pedal with a dead knob is still worth hearing -- but it must not be silent
|
|
2621
|
+
* either, which is what shipping `ibanez-ts808` with an inert Drive was.
|
|
2622
|
+
*/
|
|
2623
|
+
readonly warnings: readonly CompileWarning[];
|
|
2624
|
+
};
|
|
2625
|
+
/**
|
|
2626
|
+
* Why a control cannot move the circuit. A closed vocabulary: callers switch on `code`,
|
|
2627
|
+
* and the prose in `detail` is for a human reading a report, never for a test to match.
|
|
2628
|
+
*/
|
|
2629
|
+
export type InertControlWarning = {
|
|
2630
|
+
readonly code: "control-cannot-affect-circuit";
|
|
2631
|
+
readonly control: ControlId;
|
|
2632
|
+
/** Why the control cannot affect the circuit: structural defect (disconnected/floating) vs unscheduled region */
|
|
2633
|
+
readonly reasonKind?: "floating-wiper" | "disconnected" | "unscheduled-region";
|
|
2634
|
+
readonly isStructuralDefect?: boolean;
|
|
2635
|
+
/** The components carrying the evidence, in document order. */
|
|
2636
|
+
readonly devices: readonly DeviceId[];
|
|
2637
|
+
readonly detail: string;
|
|
2638
|
+
};
|
|
2639
|
+
/**
|
|
2640
|
+
* An op-amp on the signal path with no DC-conducting path from its output back to its
|
|
2641
|
+
* inverting input. Nothing pins its operating point, so the DC solution rests on the input
|
|
2642
|
+
* network alone. Deliberately does **not** claim the circuit diverges: `ibanez-pql` does and
|
|
2643
|
+
* `boss-mt-2` is flagged and solves.
|
|
2644
|
+
*/
|
|
2645
|
+
export type UnboundedOpampWarning = {
|
|
2646
|
+
readonly code: "opamp-operating-point-unbounded";
|
|
2647
|
+
readonly device: DeviceId;
|
|
2648
|
+
readonly detail: string;
|
|
2649
|
+
};
|
|
2650
|
+
/**
|
|
2651
|
+
* A diode wired forward across a supply: anode on a supply node, cathode on ground. The compiled
|
|
2652
|
+
* circuit contains a short, and the runtime renders it rather than diverging only because the
|
|
2653
|
+
* junction voltage is clamped.
|
|
2654
|
+
*/
|
|
2655
|
+
export type SupplyShortWarning = {
|
|
2656
|
+
readonly code: "diode-forward-across-supply";
|
|
2657
|
+
readonly device: DeviceId;
|
|
2658
|
+
readonly detail: string;
|
|
2659
|
+
};
|
|
2660
|
+
/**
|
|
2661
|
+
* Two or more ideal voltage sources -- op-amp outputs, supply drive terminals -- forcing one
|
|
2662
|
+
* node, which is over-determined rather than merely unusual. See `over-driven-node.ts`.
|
|
2663
|
+
*/
|
|
2664
|
+
export type OverDrivenNodeWarning = {
|
|
2665
|
+
readonly code: "node-driven-by-two-sources";
|
|
2666
|
+
/** Null: a stamp carries no device id, and the conflict is a property of the node. */
|
|
2667
|
+
readonly device: DeviceId | null;
|
|
2668
|
+
readonly detail: string;
|
|
2669
|
+
};
|
|
2670
|
+
/**
|
|
2671
|
+
* A switch throw or transformer winding tap alone on its node: a route that cannot carry anything.
|
|
2672
|
+
*
|
|
2673
|
+
* The general dangling-terminal rule was measured and rejected — 2020 findings, no signal-to-noise
|
|
2674
|
+
* (see `dangling-active-terminal.ts`). This is the same narrowing applied to routing rather than
|
|
2675
|
+
* to active devices, and measured the same way: of 1904 single-pin terminals in the corpus, 1858
|
|
2676
|
+
* are spare pins and test points, **25 are switch throws and 10 are winding taps** — 35 findings
|
|
2677
|
+
* across 18 packets. A throw that routes nowhere is a switch position that does nothing; a tap
|
|
2678
|
+
* joined to nothing is a tap that cannot be selected. Neither can be deliberate.
|
|
2679
|
+
*/
|
|
2680
|
+
export type UnroutableThrowWarning = {
|
|
2681
|
+
readonly code: "throw-or-winding-unwired";
|
|
2682
|
+
readonly device: DeviceId | null;
|
|
2683
|
+
readonly detail: string;
|
|
2684
|
+
};
|
|
2685
|
+
/**
|
|
2686
|
+
* An active device with a signal terminal on a node nothing else touches: it is stamped and cannot
|
|
2687
|
+
* conduct. See `dangling-active-terminal.ts`.
|
|
2688
|
+
*/
|
|
2689
|
+
export type DanglingActiveTerminalWarning = {
|
|
2690
|
+
readonly code: "active-device-terminal-unwired";
|
|
2691
|
+
/** Null: a stamp carries no device id, so the label in `detail` names it by nodes. */
|
|
2692
|
+
readonly device: DeviceId | null;
|
|
2693
|
+
readonly detail: string;
|
|
2694
|
+
};
|
|
2695
|
+
/**
|
|
2696
|
+
* A BBD clock driver or timing controller that is preserved in the netlist but bypassed
|
|
2697
|
+
* by the MNA analog solver, meaning its clock/timing/VGG behavior is not modeled dynamically.
|
|
2698
|
+
*/
|
|
2699
|
+
export type NonExecutableClockDriverWarning = {
|
|
2700
|
+
readonly code: "non-executable-clock-driver";
|
|
2701
|
+
readonly device: DeviceId | null;
|
|
2702
|
+
readonly detail: string;
|
|
2703
|
+
};
|
|
2704
|
+
/**
|
|
2705
|
+
* A clock driver with *both* supply terminals at ground, so its outputs render as zero.
|
|
2706
|
+
*
|
|
2707
|
+
* The MN3101/MN3102 run on a single **negative** supply: the datasheet's pin 1 is GND and pin 3
|
|
2708
|
+
* is VDD, so a +9 V pedal ties the part's GND pin to +9 V and its VDD pin to circuit ground. A
|
|
2709
|
+
* faithful transcription therefore puts `vdd` at node 0 and the GND pin on the rail, which the
|
|
2710
|
+
* stamp now models -- it binds both supply pins and swings the phases between them.
|
|
2711
|
+
*
|
|
2712
|
+
* What is left is the case with no supply at all: both pins at ground, nothing to swing between.
|
|
2713
|
+
* This warned on `vdd` alone for a few hours on 2026-09-07, while the stamp still swung against
|
|
2714
|
+
* literal ground; that reading was retired with the fix rather than left to fire falsely on the
|
|
2715
|
+
* two correctly-wired packets it was written for. See
|
|
2716
|
+
* `docs/troubleshootings/a-correctly-wired-bbd-clock-driver-outputs-nothing.md`.
|
|
2717
|
+
*/
|
|
2718
|
+
export type ClockDriverSupplyAtGroundWarning = {
|
|
2719
|
+
readonly code: "clock-driver-supply-at-ground";
|
|
2720
|
+
readonly device: DeviceId | null;
|
|
2721
|
+
readonly detail: string;
|
|
2722
|
+
};
|
|
2723
|
+
export type NonExecutableSupportShellWarning = {
|
|
2724
|
+
readonly code: "non-executable-support-shell";
|
|
2725
|
+
readonly device: DeviceId | null;
|
|
2726
|
+
readonly detail: string;
|
|
2727
|
+
};
|
|
2728
|
+
export type UnconnectedBehaviorComponentWarning = {
|
|
2729
|
+
readonly code: "unconnected-behavior-component";
|
|
2730
|
+
readonly device: DeviceId;
|
|
2731
|
+
readonly detail: string;
|
|
2732
|
+
};
|
|
2733
|
+
/**
|
|
2734
|
+
* A device that named its terminals in a spelling its lowering could not place, and was wired by
|
|
2735
|
+
* declaration order instead. See `unreadable-terminal-role.ts` for why this is a warning rather
|
|
2736
|
+
* than a refusal, and for what it does and does not claim about the resulting wiring.
|
|
2737
|
+
*/
|
|
2738
|
+
export type UnreadableTerminalRoleWarning = {
|
|
2739
|
+
readonly code: "terminal-role-unreadable";
|
|
2740
|
+
readonly device: DeviceId;
|
|
2741
|
+
readonly detail: string;
|
|
2742
|
+
};
|
|
2743
|
+
/**
|
|
2744
|
+
* A device whose source states, in its typed `sourceTypeName`, a device class this pipeline has no
|
|
2745
|
+
* law for — so it is not executed rather than modelled as the nearest kind the format could carry.
|
|
2746
|
+
* See `declaresUnimplementedDeviceLaw`.
|
|
2747
|
+
*/
|
|
2748
|
+
export type UnimplementedDeviceLawWarning = {
|
|
2749
|
+
readonly code: "device-law-not-implemented";
|
|
2750
|
+
readonly device: DeviceId;
|
|
2751
|
+
readonly detail: string;
|
|
2752
|
+
};
|
|
2753
|
+
export type UnexecutedActiveRegionWarning = {
|
|
2754
|
+
readonly code: "unexecuted-active-region";
|
|
2755
|
+
/** Null: a region is not a device, so `detail` names it by block id. */
|
|
2756
|
+
readonly device: DeviceId | null;
|
|
2757
|
+
readonly detail: string;
|
|
2758
|
+
};
|
|
2759
|
+
/**
|
|
2760
|
+
* A rail that declares no voltage while something is connected to it.
|
|
2761
|
+
*
|
|
2762
|
+
* Such a rail lowers to `open` on purpose -- a named support shell with nothing to assert -- and
|
|
2763
|
+
* three corpus packets rely on that. What must not be silent is **loads hanging off it**: they see
|
|
2764
|
+
* only gmin to ground, so a supply the sheet draws at, say, 8 V solves at 0 V and every stage it
|
|
2765
|
+
* powers renders dead or 20 dB down. Found 2026-09-23, when `boss-dd-5` v1.36 wired its output mixer
|
|
2766
|
+
* to the sheet's unlettered up-arrow supply without a voltage: dry muted, wet 20 dB down, no
|
|
2767
|
+
* diagnostic.
|
|
2768
|
+
*/
|
|
2769
|
+
export type RailWithoutVoltageWarning = {
|
|
2770
|
+
readonly code: "rail-without-voltage";
|
|
2771
|
+
readonly device: DeviceId;
|
|
2772
|
+
readonly detail: string;
|
|
2773
|
+
};
|
|
2774
|
+
/**
|
|
2775
|
+
* JFETs whose law runs on the fet law's own `thresholdVolts`/`transconductance`, because the
|
|
2776
|
+
* source types no `Vt0`/`Beta` for them. One warning per document, naming every such device.
|
|
2777
|
+
*
|
|
2778
|
+
* **Why it is named.** The defaults (`JFET_DEFAULT_THRESHOLD_VOLTS`,
|
|
2779
|
+
* `JFET_DEFAULT_TRANSCONDUCTANCE` in `device-laws.ts`) are not any part's values. For a
|
|
2780
|
+
* switching JFET that is fully on or off they barely matter; for one biased at its knee they
|
|
2781
|
+
* decide the behaviour. `mxr-phase-45`'s trim centres its gates at Vgs about -2.34 V, which the
|
|
2782
|
+
* default -2 V never reaches, so its sweep reads WEAK, whereas a datasheet-centre 2N5952 crosses
|
|
2783
|
+
* threshold every cycle (packet-study/mxr-phase-45, 2026-09-24). Measured the same day, 142 of
|
|
2784
|
+
* 151 corpus JFETs ran on these defaults and nothing said so.
|
|
2785
|
+
*
|
|
2786
|
+
* **What it does not claim.** Not that the source is wrong: a packet may leave a JFET untyped on
|
|
2787
|
+
* purpose, because the datasheet prints only grade limits (the 2N5952 case). The claim is only
|
|
2788
|
+
* that the stamped values came from the consumer, not the source.
|
|
2789
|
+
*/
|
|
2790
|
+
export type FetLawDefaultWarning = {
|
|
2791
|
+
readonly code: "fet-law-default-parameters";
|
|
2792
|
+
readonly device: null;
|
|
2793
|
+
readonly devices: readonly DeviceId[];
|
|
2794
|
+
readonly detail: string;
|
|
2795
|
+
};
|
|
2796
|
+
/**
|
|
2797
|
+
* BJTs whose law runs on the bjt law's own `saturationCurrent`/`forwardBeta`, because the
|
|
2798
|
+
* source types no `IS`/`BF` for them. One warning per document, naming every such device.
|
|
2799
|
+
*
|
|
2800
|
+
* **Why it is named.** The defaults (`SILICON_SATURATION_CURRENT`,
|
|
2801
|
+
* `SILICON_FORWARD_BETA` in `device-laws.ts`) are not any part's values: against a real
|
|
2802
|
+
* 2N3904 the saturation current is 100x low (~119 mV of Vbe) and beta 2x, and the corpus
|
|
2803
|
+
* registry holds per-part values the compiler has never read. For a saturated switch they
|
|
2804
|
+
* barely matter; for a biased stage they decide the operating point (`tycobrahe-octavia`'s
|
|
2805
|
+
* Q1 moves −115 mV Vbe on the registry IS, 17% on Ic).
|
|
2806
|
+
*
|
|
2807
|
+
* **What it does not claim.** Not that the source is wrong: most corpus BJTs predate any
|
|
2808
|
+
* typed parameter, and a packet may leave one untyped because its datasheet prints only a
|
|
2809
|
+
* beta bin. The claim is only that the stamped values came from the consumer, not the source.
|
|
2810
|
+
*/
|
|
2811
|
+
export type BjtLawDefaultWarning = {
|
|
2812
|
+
readonly code: "bjt-law-default-parameters";
|
|
2813
|
+
readonly device: null;
|
|
2814
|
+
readonly devices: readonly DeviceId[];
|
|
2815
|
+
readonly detail: string;
|
|
2816
|
+
};
|
|
2817
|
+
/**
|
|
2818
|
+
* Op-amps whose output runs the full supply, because the part states no distance it stops short
|
|
2819
|
+
* of the rails (`outputSwingDropVolts`). One warning per document, naming every such device.
|
|
2820
|
+
*
|
|
2821
|
+
* **Why it is named.** No real op-amp reaches its rails. `mxr-phase-90` read WEAK 79.6 deg on
|
|
2822
|
+
* rail-to-rail TL062s and SWEEPING 106.0 deg once they stopped 1.2 V short of each rail
|
|
2823
|
+
* (packet-study/mxr-phase-90, 2026-09-29), and three simulators agreed on the wrong answer
|
|
2824
|
+
* because the oracle deck follows the program's rails. A comparator or LFO near the rails, or a
|
|
2825
|
+
* clipper, is decided by this number.
|
|
2826
|
+
*
|
|
2827
|
+
* **What it does not claim.** Not that the source is wrong, and not that the stamped rails are
|
|
2828
|
+
* wrong for a circuit that never nears them; a unity-gain buffer at low level is untouched.
|
|
2829
|
+
* The claim is only that the swing came from the consumer's default (the supply), not the part.
|
|
2830
|
+
* LM358, LM324 and TL022 are listed until an asymmetric swing can be stated.
|
|
2831
|
+
*/
|
|
2832
|
+
export type OpampLawDefaultWarning = {
|
|
2833
|
+
readonly code: "opamp-law-default-parameters";
|
|
2834
|
+
readonly device: null;
|
|
2835
|
+
readonly devices: readonly DeviceId[];
|
|
2836
|
+
readonly detail: string;
|
|
2837
|
+
};
|
|
2838
|
+
export type ElectricallyIsolatedIcWarning = {
|
|
2839
|
+
readonly code: "electrically-isolated-ic";
|
|
2840
|
+
readonly device: DeviceId;
|
|
2841
|
+
readonly detail: string;
|
|
2842
|
+
};
|
|
2843
|
+
/**
|
|
2844
|
+
* An integrated circuit the source declares and the program does not execute.
|
|
2845
|
+
*
|
|
2846
|
+
* **The silent class this closes.** Five paths in `resolveDevice` give an `ic` an `open` law, and
|
|
2847
|
+
* before this only one of them said so: `electrically-isolated-ic` named the isolated chips, and
|
|
2848
|
+
* `non-executable-support-shell` names a *transistor* shell. Measured over the 142-document corpus
|
|
2849
|
+
* on 2026-09-03, 88 of 104 opened ICs were named by nothing at all -- a compandor, a DSP, three
|
|
2850
|
+
* memories and a 74HCU04 among them -- so a packet compiled `ok`, rendered, and sounded like
|
|
2851
|
+
* something with a chip missing from it and no line of output that mentioned the chip.
|
|
2852
|
+
*
|
|
2853
|
+
* **What it does not claim.** Not that the open is wrong. A charge pump whose rail the document
|
|
2854
|
+
* declares separately, and a shell the packet draws for continuity, are both correctly opened and
|
|
2855
|
+
* both stay opened. The claim is only that the component is in the source and not in the program,
|
|
2856
|
+
* which is a fact a reader is entitled to. `reason` says which path decided it and
|
|
2857
|
+
* `detail` says what a fix would have to change.
|
|
2858
|
+
*/
|
|
2859
|
+
/**
|
|
2860
|
+
* A compiled program that executes no blocks, so its output port is reachable by nothing.
|
|
2861
|
+
*
|
|
2862
|
+
* `device` is null because this is a property of the whole program rather than of one component;
|
|
2863
|
+
* the component responsible, where there is a single one, is named by the `ic-not-executed` or
|
|
2864
|
+
* `electrically-isolated-ic` warning sitting beside it.
|
|
2865
|
+
*/
|
|
2866
|
+
export type OutputPortUnreachableWarning = {
|
|
2867
|
+
readonly code: "output-port-unreachable";
|
|
2868
|
+
readonly device: DeviceId | null;
|
|
2869
|
+
readonly detail: string;
|
|
2870
|
+
};
|
|
2871
|
+
export type IcNotExecutedWarning = {
|
|
2872
|
+
readonly code: "ic-not-executed";
|
|
2873
|
+
readonly device: DeviceId;
|
|
2874
|
+
readonly reason: OpenIcReason | "unrecorded";
|
|
2875
|
+
readonly detail: string;
|
|
2876
|
+
/**
|
|
2877
|
+
* Present only for gap-candidate reasons (`registry-open`,
|
|
2878
|
+
* `declared-class-without-model`): the phase-3 bucket from
|
|
2879
|
+
* `classifyOpenIcGap`. Absent for declared opens and wiring states,
|
|
2880
|
+
* which were never gap candidates and stay out of the phase-3 set.
|
|
2881
|
+
*/
|
|
2882
|
+
readonly gapClass?: OpenIcGapClass;
|
|
2883
|
+
};
|
|
2884
|
+
/**
|
|
2885
|
+
* A component the source marks `InterfaceOnly` or `SourceOnly` — interface or source
|
|
2886
|
+
* context (a panel-LED shell, a footswitch whose mechanical switching the source declines to
|
|
2887
|
+
* trace, an insert loop the product omits) that is not part of the modeled graph. v1 carried
|
|
2888
|
+
* this as an admission disposition; v2 now reads the same typed flag and drops the component
|
|
2889
|
+
* from the solve, so an open placeholder the source says not to model stops refusing the pedal.
|
|
2890
|
+
*/
|
|
2891
|
+
export type InterfaceOrSourceOnlyWarning = {
|
|
2892
|
+
readonly code: "interface-or-source-only";
|
|
2893
|
+
readonly device: DeviceId;
|
|
2894
|
+
readonly detail: string;
|
|
2895
|
+
};
|
|
2896
|
+
/**
|
|
2897
|
+
* A board position the source marks `DNP` — the footprint is there and nothing is fitted in it.
|
|
2898
|
+
*
|
|
2899
|
+
* Kept distinct from `interface-or-source-only` rather than folded into it, because the two say
|
|
2900
|
+
* different things and a reader of these warnings should be able to tell them apart: that one
|
|
2901
|
+
* means "this component is context, not circuit", while this one means "this component is
|
|
2902
|
+
* genuinely absent from the board". An unpopulated two-terminal position is an **open
|
|
2903
|
+
* circuit** — a stated fact about the circuit, not a missing value — so the component is
|
|
2904
|
+
* dropped rather than refused for the value it correctly does not carry.
|
|
2905
|
+
*/
|
|
2906
|
+
export type NotPopulatedWarning = {
|
|
2907
|
+
readonly code: "not-populated";
|
|
2908
|
+
readonly device: DeviceId;
|
|
2909
|
+
readonly detail: string;
|
|
2910
|
+
};
|
|
2911
|
+
/**
|
|
2912
|
+
* A `DNP` component that also declares no value — so dropping it is the only reason the
|
|
2913
|
+
* document compiles.
|
|
2914
|
+
*
|
|
2915
|
+
* **`DNP` is the one property whose effect is to make a refusal disappear.** Every other missing
|
|
2916
|
+
* fact refuses and names itself; `DNP` removes the component instead, and a removed component
|
|
2917
|
+
* cannot refuse. That asymmetry makes it the path of least resistance when a packet will not
|
|
2918
|
+
* compile, which is a pressure no other field is under.
|
|
2919
|
+
*
|
|
2920
|
+
* The distinction this warning draws is between two shapes that look identical in a diff:
|
|
2921
|
+
*
|
|
2922
|
+
* - **Corroborated.** The component declares its value *and* is marked `DNP`: "this is a 10k
|
|
2923
|
+
* position, unfitted". Nothing is concealed — a reader sees what would be there, and the
|
|
2924
|
+
* claim can be checked against a board. This emits the ordinary `not-populated`.
|
|
2925
|
+
* - **Load-bearing.** The component declares no value *and* is marked `DNP`. Without the mark
|
|
2926
|
+
* it would have refused by name; with it, it silently leaves the circuit. The `DNP` is
|
|
2927
|
+
* carrying the compile, and if the mark is wrong the part is simply gone with nothing
|
|
2928
|
+
* reporting it.
|
|
2929
|
+
*
|
|
2930
|
+
* Load-bearing is not by itself a defect — an unpopulated position genuinely has no value to
|
|
2931
|
+
* declare, so this shape is exactly what a real DNP looks like. It is reported because it is the
|
|
2932
|
+
* shape in which a guess is indistinguishable from a fact, and the reader should get to see how
|
|
2933
|
+
* much of a document's compilation rests on it.
|
|
2934
|
+
*
|
|
2935
|
+
* Measured 2026-08-26: `mxr-carbon-copy` had eight components marked `DNP` in one edit, every one
|
|
2936
|
+
* of them carrying `SourceValue: visible-no-value-or-DNP` and `SourceStatus: defer` — the
|
|
2937
|
+
* tracer's own record that it could not tell whether the position was populated. All eight were
|
|
2938
|
+
* load-bearing, the packet compiled, and nothing in the pipeline said so.
|
|
2939
|
+
*/
|
|
2940
|
+
export type NotPopulatedWithoutValueWarning = {
|
|
2941
|
+
readonly code: "not-populated-without-value";
|
|
2942
|
+
readonly device: DeviceId;
|
|
2943
|
+
readonly detail: string;
|
|
2944
|
+
};
|
|
2945
|
+
/**
|
|
2946
|
+
* A component whose source states `kind: unsupported` — a real device class the format has no
|
|
2947
|
+
* typed entry for, so the source transcribed it as the nearest kind and recorded the truth in
|
|
2948
|
+
* `sourceTypeName`.
|
|
2949
|
+
*
|
|
2950
|
+
* **What happens.** The compiler skips the component from the solve rather than stamping it as
|
|
2951
|
+
* a different law (a relay is not a switch, not a resistor, and not an open circuit). Whatever
|
|
2952
|
+
* it does in the real pedal is absent from this program.
|
|
2953
|
+
*
|
|
2954
|
+
* **Why it is reported instead of refusing.** A component marked `unsupported` by the source
|
|
2955
|
+
* is a deliberate decision: the transcriber faced a class the format cannot carry and chose
|
|
2956
|
+
* transparency over misclassification. Refusing the pedal would punish the surrounding circuit
|
|
2957
|
+
* for a source boundary the transcriber already handled.
|
|
2958
|
+
*/
|
|
2959
|
+
export type UnsupportedComponentWarning = {
|
|
2960
|
+
readonly code: "unsupported-component";
|
|
2961
|
+
readonly device: DeviceId;
|
|
2962
|
+
readonly detail: string;
|
|
2963
|
+
};
|
|
2964
|
+
/**
|
|
2965
|
+
* The output port sits upstream of the output transformer: an amp measured before its power
|
|
2966
|
+
* stage. See `resolvePorts`, which prefers a transformer-coupled jack precisely so this is rare.
|
|
2967
|
+
*/
|
|
2968
|
+
export type OutputPortNotTransformerCoupledWarning = {
|
|
2969
|
+
readonly code: "output-port-not-transformer-coupled";
|
|
2970
|
+
/** The jack the port resolved to, when one owns that node. */
|
|
2971
|
+
readonly device: DeviceId | null;
|
|
2972
|
+
readonly detail: string;
|
|
2973
|
+
};
|
|
2974
|
+
/**
|
|
2975
|
+
* An amp-shaped document -- one declaring a high-voltage power transformer -- whose graph
|
|
2976
|
+
* contains no output transformer, so its render is a preamp level. A documented packet boundary
|
|
2977
|
+
* in the one case that trips it, and never a pedal.
|
|
2978
|
+
*/
|
|
2979
|
+
export type NoOutputTransformerWarning = {
|
|
2980
|
+
readonly code: "no-output-transformer";
|
|
2981
|
+
readonly device: DeviceId | null;
|
|
2982
|
+
readonly detail: string;
|
|
2983
|
+
};
|
|
2984
|
+
/**
|
|
2985
|
+
* A tube lowered with its device-class fit because no catalog entry matched its part number.
|
|
2986
|
+
*
|
|
2987
|
+
* Not a defect on its own -- an unregistered tube gets a class law the way an unregistered
|
|
2988
|
+
* transistor does. It is a boundary on what the render can be quoted for: the tube's bias and gain
|
|
2989
|
+
* are another tube's. Every amp in the corpus carried this silently until it was measured.
|
|
2990
|
+
*/
|
|
2991
|
+
export type GenericTubeFitWarning = {
|
|
2992
|
+
readonly code: "generic-tube-fit";
|
|
2993
|
+
readonly device: DeviceId;
|
|
2994
|
+
/** The part number the source declared, or `null` when it declared none. */
|
|
2995
|
+
readonly partNumber: string | null;
|
|
2996
|
+
readonly detail: string;
|
|
2997
|
+
};
|
|
2998
|
+
/**
|
|
2999
|
+
* A control whose declared taper this stage could not turn into a law, so it renders linear.
|
|
3000
|
+
*
|
|
3001
|
+
* **Not the same thing as a document that states no taper.** A pot with no `Taper` at all, or one
|
|
3002
|
+
* whose source explicitly records the marking as unknown, has nothing for this stage to have
|
|
3003
|
+
* failed to read and is silent. This fires only where the source states something: a part marking
|
|
3004
|
+
* whose law is not derivable here (`W20`, `Boss-G-taper`, `BH`), or a taper core's format supports
|
|
3005
|
+
* and this runtime has no curve for (`s-curve`, `stepped`, `custom`).
|
|
3006
|
+
*
|
|
3007
|
+
* Measured 2026-09-03: 69 of the corpus's 608 declared tapers fell through to linear, and all 69
|
|
3008
|
+
* did it silently. An audio pot rendered linear is audible across the whole knob sweep, which is
|
|
3009
|
+
* why this is reported rather than left to a reader of the source.
|
|
3010
|
+
*/
|
|
3011
|
+
export type TaperNotExecutableWarning = {
|
|
3012
|
+
readonly code: "taper-not-executable";
|
|
3013
|
+
readonly device: DeviceId;
|
|
3014
|
+
/** Exactly what the source declared, unfolded. */
|
|
3015
|
+
readonly declared: string;
|
|
3016
|
+
readonly detail: string;
|
|
3017
|
+
};
|
|
3018
|
+
/**
|
|
3019
|
+
* A document whose declared `nodes:` ledger and inline terminal `node:` refs place one terminal on
|
|
3020
|
+
* different nodes — two encodings of one circuit that disagree.
|
|
3021
|
+
*
|
|
3022
|
+
* This compiler and `@vessel-dsp/core` both treat the declared ledger as authoritative when a
|
|
3023
|
+
* terminal has both encodings. Divergence is still a source defect because one terminal is authored
|
|
3024
|
+
* with contradictory connectivity and the inline ref is ignored until reconciled. Reported per
|
|
3025
|
+
* terminal, because which terminal moved is the whole question.
|
|
3026
|
+
*/
|
|
3027
|
+
export type LedgerDivergenceWarning = {
|
|
3028
|
+
readonly code: "ledger-divergence";
|
|
3029
|
+
readonly device: DeviceId;
|
|
3030
|
+
/** `componentId/terminalName`, the key both encodings are indexed by. */
|
|
3031
|
+
readonly terminal: string;
|
|
3032
|
+
readonly detail: string;
|
|
3033
|
+
};
|
|
3034
|
+
/**
|
|
3035
|
+
* A speaker load modelled with a generic driver profile rather than the one the amp uses.
|
|
3036
|
+
*
|
|
3037
|
+
* Stage B's impedance curve has a real driver's *shape* — resonance peak, inductive rise — but not
|
|
3038
|
+
* this amp's driver, because no corpus packet identifies one in a form that resolves without a
|
|
3039
|
+
* forbidden name inference. Carried per jack so a render is never quoted as this amp's speaker.
|
|
3040
|
+
*/
|
|
3041
|
+
export type GenericSpeakerProfileWarning = {
|
|
3042
|
+
readonly code: "generic-speaker-profile";
|
|
3043
|
+
readonly device: DeviceId;
|
|
3044
|
+
/** The declared nominal the generic profile was scaled to. */
|
|
3045
|
+
readonly nominalOhms: number;
|
|
3046
|
+
readonly detail: string;
|
|
3047
|
+
};
|
|
3048
|
+
/**
|
|
3049
|
+
* A part whose terminals reach their law slots by declaration order, with nothing asserting what
|
|
3050
|
+
* that order means -- see `findUnverifiedPinoutBindings` for the measurement and for why no
|
|
3051
|
+
* connectivity rule replaces it.
|
|
3052
|
+
*/
|
|
3053
|
+
export type UnverifiedPinoutBindingWarning = {
|
|
3054
|
+
readonly code: "unverified-pinout-binding";
|
|
3055
|
+
readonly device: DeviceId;
|
|
3056
|
+
/** The part id the device identified to. */
|
|
3057
|
+
readonly partNumber: string;
|
|
3058
|
+
readonly detail: string;
|
|
3059
|
+
};
|
|
3060
|
+
/**
|
|
3061
|
+
* A sweepable control that is really power-domain hardware -- see `findPowerDomainControls` for
|
|
3062
|
+
* the measurement and for why structure cannot settle it.
|
|
3063
|
+
*/
|
|
3064
|
+
/**
|
|
3065
|
+
* A control whose declared ROLE the engine implements no law for: accepted, lowered, and then
|
|
3066
|
+
* ignored. Distinct from `control-cannot-affect-circuit`, which is a fact about the CIRCUIT; this
|
|
3067
|
+
* is a fact about the ENGINE.
|
|
3068
|
+
*/
|
|
3069
|
+
export type UnimplementedControlRoleWarning = {
|
|
3070
|
+
readonly code: "control-role-not-implemented";
|
|
3071
|
+
readonly control: ControlId;
|
|
3072
|
+
readonly role: string;
|
|
3073
|
+
readonly detail: string;
|
|
3074
|
+
};
|
|
3075
|
+
export type PowerDomainControlWarning = {
|
|
3076
|
+
readonly code: "power-domain-control";
|
|
3077
|
+
readonly device: DeviceId;
|
|
3078
|
+
readonly detail: string;
|
|
3079
|
+
};
|
|
3080
|
+
export type CompileWarning = UnroutableThrowWarning | GenericSpeakerProfileWarning | LedgerDivergenceWarning | GenericTubeFitWarning | InertControlWarning | UnboundedOpampWarning | SupplyShortWarning | TaperNotExecutableWarning | OverDrivenNodeWarning | DanglingActiveTerminalWarning | ClockDriverSupplyAtGroundWarning | NonExecutableClockDriverWarning | NonExecutableSupportShellWarning | UnconnectedBehaviorComponentWarning | UnreadableTerminalRoleWarning | UnverifiedPinoutBindingWarning | PowerDomainControlWarning | UnimplementedControlRoleWarning | UnimplementedDeviceLawWarning | ElectricallyIsolatedIcWarning | RailWithoutVoltageWarning | FetLawDefaultWarning | BjtLawDefaultWarning | OpampLawDefaultWarning | IcNotExecutedWarning | OutputPortUnreachableWarning | InterfaceOrSourceOnlyWarning | NotPopulatedWarning | NotPopulatedWithoutValueWarning | UnsupportedComponentWarning | UnexecutedActiveRegionWarning | OutputPortNotTransformerCoupledWarning | NoOutputTransformerWarning | DeclaredDelayWarning | ControlPositionDisagreesWarning | BypassEngagedDisagreesWarning | ModulationNotModelledWarning | ConverterScaleNotModelledWarning;
|
|
3081
|
+
/**
|
|
3082
|
+
* A composed program that touches a data converter but runs unity in volts, because one side's
|
|
3083
|
+
* converter is unwired, ambiguous, or has no cited full scale. See `converter-scale.ts`: the
|
|
3084
|
+
* pedal's gain there is the DAC's full scale over the ADC's, and the program cannot say it.
|
|
3085
|
+
*/
|
|
3086
|
+
export type ConverterScaleNotModelledWarning = {
|
|
3087
|
+
readonly code: "converter-scale-not-modelled";
|
|
3088
|
+
readonly device: DeviceId | null;
|
|
3089
|
+
readonly detail: string;
|
|
3090
|
+
};
|
|
3091
|
+
/**
|
|
3092
|
+
* A delay macro whose modulation input the registry declares and the document wires, but which
|
|
3093
|
+
* admission refused -- so the delay is a constant and whatever sweeps it in the real pedal does
|
|
3094
|
+
* not reach it.
|
|
3095
|
+
*
|
|
3096
|
+
* Measured 2026-08-27: **no packet in the corpus modulates a delay from a solved node**, so every
|
|
3097
|
+
* flanger, chorus, vibrato and rotary renders as a fixed comb filter. That was invisible in the
|
|
3098
|
+
* program, which is the part this warning fixes -- not the modulation itself.
|
|
3099
|
+
*/
|
|
3100
|
+
export type ModulationNotModelledWarning = {
|
|
3101
|
+
readonly code: "modulation-not-modelled";
|
|
3102
|
+
readonly device: DeviceId | null;
|
|
3103
|
+
readonly detail: string;
|
|
3104
|
+
};
|
|
3105
|
+
/**
|
|
3106
|
+
* A delay macro whose delay time was **typed into the source** rather than derived from the
|
|
3107
|
+
* clock network it is wired to.
|
|
3108
|
+
*
|
|
3109
|
+
* This is the difference between simulating a delay and reporting one. A derived delay is a
|
|
3110
|
+
* consequence of the packet's own R and C and moves when either does; a declared `DelayMs` is a
|
|
3111
|
+
* number that renders whatever it says and cannot be wrong in any way a render could reveal --
|
|
3112
|
+
* so it must never be read as evidence about the circuit. Measured 2026-08-27: of 16 BBD
|
|
3113
|
+
* packets, 4 derive and 12 do not, and nothing distinguished them.
|
|
3114
|
+
*
|
|
3115
|
+
* A warning rather than a refusal: dropping twelve packets to punish their sources helps nobody,
|
|
3116
|
+
* and the fallback still produces a pedal worth hearing. What was not acceptable is shipping it
|
|
3117
|
+
* silently.
|
|
3118
|
+
*/
|
|
3119
|
+
export type DeclaredDelayWarning = {
|
|
3120
|
+
readonly code: "declared-delay-not-derived";
|
|
3121
|
+
readonly device: DeviceId | null;
|
|
3122
|
+
readonly detail: string;
|
|
3123
|
+
};
|
|
3124
|
+
/**
|
|
3125
|
+
* The panel and the component disagree about where a control is set.
|
|
3126
|
+
*
|
|
3127
|
+
* `deviceInterface.controls[].defaultPosition` states where a knob ships; a component's `Wipe`
|
|
3128
|
+
* or `Position` states where that part's wiper physically sits. For a panel control they describe
|
|
3129
|
+
* the same thing, so a disagreement is a contradiction inside one document rather than a
|
|
3130
|
+
* precedence question. The panel declaration is used and the conflict is named -- silently
|
|
3131
|
+
* picking one is how a pedal ends up rendering at an operating point its own source denies.
|
|
3132
|
+
*/
|
|
3133
|
+
export type ControlPositionDisagreesWarning = {
|
|
3134
|
+
readonly code: "control-position-disagrees";
|
|
3135
|
+
readonly device: DeviceId | null;
|
|
3136
|
+
readonly detail: string;
|
|
3137
|
+
};
|
|
3138
|
+
/**
|
|
3139
|
+
* The bypass switch's declared engaged position disagrees with the default
|
|
3140
|
+
* the compiler derived for it.
|
|
3141
|
+
*
|
|
3142
|
+
* `audio.bypass.engagedPosition` states which switch position is the effect;
|
|
3143
|
+
* the default is derived from connectivity, panel, and component state. When
|
|
3144
|
+
* they differ, either the source misstates the engaged position (the field
|
|
3145
|
+
* is unaudited boilerplate in most of the corpus) or the default is wrong --
|
|
3146
|
+
* both are product defects (a pedal that shows engaged while rendering dry,
|
|
3147
|
+
* or the reverse), so the disagreement is named rather than silently
|
|
3148
|
+
* resolved either way. The TR-2 case (declared 1 applied as the default) is
|
|
3149
|
+
* the one direction that does not warn.
|
|
3150
|
+
*/
|
|
3151
|
+
export type BypassEngagedDisagreesWarning = {
|
|
3152
|
+
readonly code: "bypass-engaged-disagrees";
|
|
3153
|
+
readonly device: DeviceId | null;
|
|
3154
|
+
readonly detail: string;
|
|
3155
|
+
};
|
|
3156
|
+
export type CompileResult = CompileFailure | CompileSuccess;
|
|
3157
|
+
//# sourceMappingURL=types.d.ts.map
|