@vessel-dsp/compiler 0.0.0-stage → 0.3.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.
Files changed (226) hide show
  1. package/README.md +118 -2
  2. package/dist/bake.d.ts +38 -0
  3. package/dist/bake.d.ts.map +1 -0
  4. package/dist/bake.js +101 -0
  5. package/dist/bake.js.map +1 -0
  6. package/dist/bbd-clock.d.ts +171 -0
  7. package/dist/bbd-clock.d.ts.map +1 -0
  8. package/dist/bbd-clock.js +1750 -0
  9. package/dist/bbd-clock.js.map +1 -0
  10. package/dist/block-row.d.ts +51 -0
  11. package/dist/block-row.d.ts.map +1 -0
  12. package/dist/block-row.js +17 -0
  13. package/dist/block-row.js.map +1 -0
  14. package/dist/bypass.d.ts +3 -0
  15. package/dist/bypass.d.ts.map +1 -0
  16. package/dist/bypass.js +228 -0
  17. package/dist/bypass.js.map +1 -0
  18. package/dist/compile.d.ts +24 -0
  19. package/dist/compile.d.ts.map +1 -0
  20. package/dist/compile.js +348 -0
  21. package/dist/compile.js.map +1 -0
  22. package/dist/converter-scale.d.ts +14 -0
  23. package/dist/converter-scale.d.ts.map +1 -0
  24. package/dist/converter-scale.js +133 -0
  25. package/dist/converter-scale.js.map +1 -0
  26. package/dist/couple.d.ts +28 -0
  27. package/dist/couple.d.ts.map +1 -0
  28. package/dist/couple.js +567 -0
  29. package/dist/couple.js.map +1 -0
  30. package/dist/dangling-active-terminal.d.ts +47 -0
  31. package/dist/dangling-active-terminal.d.ts.map +1 -0
  32. package/dist/dangling-active-terminal.js +542 -0
  33. package/dist/dangling-active-terminal.js.map +1 -0
  34. package/dist/declared-program.d.ts +23 -0
  35. package/dist/declared-program.d.ts.map +1 -0
  36. package/dist/declared-program.js +387 -0
  37. package/dist/declared-program.js.map +1 -0
  38. package/dist/denomination.d.ts +48 -0
  39. package/dist/denomination.d.ts.map +1 -0
  40. package/dist/denomination.js +27 -0
  41. package/dist/denomination.js.map +1 -0
  42. package/dist/device-laws.d.ts +237 -0
  43. package/dist/device-laws.d.ts.map +1 -0
  44. package/dist/device-laws.js +3513 -0
  45. package/dist/device-laws.js.map +1 -0
  46. package/dist/diagnostics.d.ts +13 -0
  47. package/dist/diagnostics.d.ts.map +1 -0
  48. package/dist/diagnostics.js +19 -0
  49. package/dist/diagnostics.js.map +1 -0
  50. package/dist/emit.d.ts +8 -0
  51. package/dist/emit.d.ts.map +1 -0
  52. package/dist/emit.js +43 -0
  53. package/dist/emit.js.map +1 -0
  54. package/dist/errors.d.ts +22 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/errors.js +39 -0
  57. package/dist/errors.js.map +1 -0
  58. package/dist/firmware-class.d.ts +69 -0
  59. package/dist/firmware-class.d.ts.map +1 -0
  60. package/dist/firmware-class.js +20 -0
  61. package/dist/firmware-class.js.map +1 -0
  62. package/dist/fixtures/circuits.d.ts +1742 -0
  63. package/dist/fixtures/circuits.d.ts.map +1 -0
  64. package/dist/fixtures/circuits.js +13896 -0
  65. package/dist/fixtures/circuits.js.map +1 -0
  66. package/dist/fixtures/expected.d.ts +261 -0
  67. package/dist/fixtures/expected.d.ts.map +1 -0
  68. package/dist/fixtures/expected.js +315 -0
  69. package/dist/fixtures/expected.js.map +1 -0
  70. package/dist/fixtures/index.d.ts +4 -0
  71. package/dist/fixtures/index.d.ts.map +1 -0
  72. package/dist/fixtures/index.js +8 -0
  73. package/dist/fixtures/index.js.map +1 -0
  74. package/dist/fixtures/registry.d.ts +52 -0
  75. package/dist/fixtures/registry.d.ts.map +1 -0
  76. package/dist/fixtures/registry.js +341 -0
  77. package/dist/fixtures/registry.js.map +1 -0
  78. package/dist/generic-tube-fit.d.ts +12 -0
  79. package/dist/generic-tube-fit.d.ts.map +1 -0
  80. package/dist/generic-tube-fit.js +45 -0
  81. package/dist/generic-tube-fit.js.map +1 -0
  82. package/dist/grounded-clock-supply.d.ts +13 -0
  83. package/dist/grounded-clock-supply.d.ts.map +1 -0
  84. package/dist/grounded-clock-supply.js +45 -0
  85. package/dist/grounded-clock-supply.js.map +1 -0
  86. package/dist/identify.d.ts +50 -0
  87. package/dist/identify.d.ts.map +1 -0
  88. package/dist/identify.js +319 -0
  89. package/dist/identify.js.map +1 -0
  90. package/dist/index.d.ts +10 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +19 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/inert-controls.d.ts +15 -0
  95. package/dist/inert-controls.d.ts.map +1 -0
  96. package/dist/inert-controls.js +266 -0
  97. package/dist/inert-controls.js.map +1 -0
  98. package/dist/latch-seed.d.ts +53 -0
  99. package/dist/latch-seed.d.ts.map +1 -0
  100. package/dist/latch-seed.js +257 -0
  101. package/dist/latch-seed.js.map +1 -0
  102. package/dist/link.d.ts +7 -0
  103. package/dist/link.d.ts.map +1 -0
  104. package/dist/link.js +270 -0
  105. package/dist/link.js.map +1 -0
  106. package/dist/lower.d.ts +79 -0
  107. package/dist/lower.d.ts.map +1 -0
  108. package/dist/lower.js +1855 -0
  109. package/dist/lower.js.map +1 -0
  110. package/dist/netlist.d.ts +79 -0
  111. package/dist/netlist.d.ts.map +1 -0
  112. package/dist/netlist.js +4746 -0
  113. package/dist/netlist.js.map +1 -0
  114. package/dist/non-executable-clock-drivers.d.ts +8 -0
  115. package/dist/non-executable-clock-drivers.d.ts.map +1 -0
  116. package/dist/non-executable-clock-drivers.js +33 -0
  117. package/dist/non-executable-clock-drivers.js.map +1 -0
  118. package/dist/open-category.d.ts +98 -0
  119. package/dist/open-category.d.ts.map +1 -0
  120. package/dist/open-category.js +111 -0
  121. package/dist/open-category.js.map +1 -0
  122. package/dist/over-driven-node.d.ts +33 -0
  123. package/dist/over-driven-node.d.ts.map +1 -0
  124. package/dist/over-driven-node.js +188 -0
  125. package/dist/over-driven-node.js.map +1 -0
  126. package/dist/part-catalog.d.ts +15 -0
  127. package/dist/part-catalog.d.ts.map +1 -0
  128. package/dist/part-catalog.js +6712 -0
  129. package/dist/part-catalog.js.map +1 -0
  130. package/dist/part-number.d.ts +32 -0
  131. package/dist/part-number.d.ts.map +1 -0
  132. package/dist/part-number.js +80 -0
  133. package/dist/part-number.js.map +1 -0
  134. package/dist/partition.d.ts +7 -0
  135. package/dist/partition.d.ts.map +1 -0
  136. package/dist/partition.js +242 -0
  137. package/dist/partition.js.map +1 -0
  138. package/dist/port-full-scale.d.ts +70 -0
  139. package/dist/port-full-scale.d.ts.map +1 -0
  140. package/dist/port-full-scale.js +235 -0
  141. package/dist/port-full-scale.js.map +1 -0
  142. package/dist/pot-orientation.d.ts +114 -0
  143. package/dist/pot-orientation.d.ts.map +1 -0
  144. package/dist/pot-orientation.js +551 -0
  145. package/dist/pot-orientation.js.map +1 -0
  146. package/dist/power-domain-control.d.ts +40 -0
  147. package/dist/power-domain-control.d.ts.map +1 -0
  148. package/dist/power-domain-control.js +142 -0
  149. package/dist/power-domain-control.js.map +1 -0
  150. package/dist/proxy-declaration.d.ts +121 -0
  151. package/dist/proxy-declaration.d.ts.map +1 -0
  152. package/dist/proxy-declaration.js +50 -0
  153. package/dist/proxy-declaration.js.map +1 -0
  154. package/dist/registry.d.ts +323 -0
  155. package/dist/registry.d.ts.map +1 -0
  156. package/dist/registry.js +192 -0
  157. package/dist/registry.js.map +1 -0
  158. package/dist/sparse-schedule.d.ts +65 -0
  159. package/dist/sparse-schedule.d.ts.map +1 -0
  160. package/dist/sparse-schedule.js +421 -0
  161. package/dist/sparse-schedule.js.map +1 -0
  162. package/dist/speaker-load.d.ts +38 -0
  163. package/dist/speaker-load.d.ts.map +1 -0
  164. package/dist/speaker-load.js +97 -0
  165. package/dist/speaker-load.js.map +1 -0
  166. package/dist/stage-coverage.d.ts +21 -0
  167. package/dist/stage-coverage.d.ts.map +1 -0
  168. package/dist/stage-coverage.js +124 -0
  169. package/dist/stage-coverage.js.map +1 -0
  170. package/dist/stamp-partition.d.ts +314 -0
  171. package/dist/stamp-partition.d.ts.map +1 -0
  172. package/dist/stamp-partition.js +472 -0
  173. package/dist/stamp-partition.js.map +1 -0
  174. package/dist/supply-reference.d.ts +19 -0
  175. package/dist/supply-reference.d.ts.map +1 -0
  176. package/dist/supply-reference.js +66 -0
  177. package/dist/supply-reference.js.map +1 -0
  178. package/dist/supply-short.d.ts +32 -0
  179. package/dist/supply-short.d.ts.map +1 -0
  180. package/dist/supply-short.js +118 -0
  181. package/dist/supply-short.js.map +1 -0
  182. package/dist/supply-stamps.d.ts +51 -0
  183. package/dist/supply-stamps.d.ts.map +1 -0
  184. package/dist/supply-stamps.js +363 -0
  185. package/dist/supply-stamps.js.map +1 -0
  186. package/dist/terminal-role.d.ts +109 -0
  187. package/dist/terminal-role.d.ts.map +1 -0
  188. package/dist/terminal-role.js +46 -0
  189. package/dist/terminal-role.js.map +1 -0
  190. package/dist/terminal-roles.d.ts +59 -0
  191. package/dist/terminal-roles.d.ts.map +1 -0
  192. package/dist/terminal-roles.js +92 -0
  193. package/dist/terminal-roles.js.map +1 -0
  194. package/dist/transformer.d.ts +181 -0
  195. package/dist/transformer.d.ts.map +1 -0
  196. package/dist/transformer.js +476 -0
  197. package/dist/transformer.js.map +1 -0
  198. package/dist/types.d.ts +3157 -0
  199. package/dist/types.d.ts.map +1 -0
  200. package/dist/types.js +11 -0
  201. package/dist/types.js.map +1 -0
  202. package/dist/unbounded-operating-point.d.ts +35 -0
  203. package/dist/unbounded-operating-point.d.ts.map +1 -0
  204. package/dist/unbounded-operating-point.js +181 -0
  205. package/dist/unbounded-operating-point.js.map +1 -0
  206. package/dist/unexecuted-region.d.ts +3 -0
  207. package/dist/unexecuted-region.d.ts.map +1 -0
  208. package/dist/unexecuted-region.js +122 -0
  209. package/dist/unexecuted-region.js.map +1 -0
  210. package/dist/unimplemented-control-role.d.ts +3 -0
  211. package/dist/unimplemented-control-role.d.ts.map +1 -0
  212. package/dist/unimplemented-control-role.js +61 -0
  213. package/dist/unimplemented-control-role.js.map +1 -0
  214. package/dist/unreachable-output.d.ts +3 -0
  215. package/dist/unreachable-output.d.ts.map +1 -0
  216. package/dist/unreachable-output.js +97 -0
  217. package/dist/unreachable-output.js.map +1 -0
  218. package/dist/unreadable-terminal-role.d.ts +67 -0
  219. package/dist/unreadable-terminal-role.d.ts.map +1 -0
  220. package/dist/unreadable-terminal-role.js +409 -0
  221. package/dist/unreadable-terminal-role.js.map +1 -0
  222. package/dist/unverified-pinout.d.ts +27 -0
  223. package/dist/unverified-pinout.d.ts.map +1 -0
  224. package/dist/unverified-pinout.js +121 -0
  225. package/dist/unverified-pinout.js.map +1 -0
  226. package/package.json +75 -5
@@ -0,0 +1,1750 @@
1
+ // BBD clock derivation: resolves the timing network of dedicated BBD clock drivers
2
+ // (CD4047, MN3101, MN3102) to derive BBD macro delaySeconds = stages / (2 * f_clock).
3
+ //
4
+ // Sourced datasheet formulas:
5
+ // - Texas Instruments CD4047B (SCHS044C, Astable Mode):
6
+ // "The output frequency (at the Q and Q̄ outputs) of this circuit can be made fixed or variable by varying R or C.
7
+ // The output waveform is a 50% duty cycle square wave of period equal to 4.40 RC. f_Q = 1 / (4.40 * R * C)."
8
+ // Driven from complementary outputs Q/Q̄: f_clock = 1 / (4.40 * R * C) => delay = stages * 2.20 * R * C.
9
+ // - Panasonic MN3101/MN3102 (MN3101 datasheet p.2):
10
+ // Oscillation frequency f_clock ≈ 1 / (5.0 * R * C) at CP1/CP2 => delay = stages * 2.50 * R * C.
11
+ //
12
+ // Refusals are strictly raised for:
13
+ // - collapsed-timing-network (shorted timing pins)
14
+ // - no-timing-capacitor / no-timing-resistor
15
+ // - grounded timing pins
16
+ // - ambiguous multi-path timing networks across different oscillator node pairs
17
+ // - unconnected oscillator pins
18
+ import { GROUND } from "./types.js";
19
+ import { foldToken, registryEntryFor } from "./registry.js";
20
+ const CD4047_PART_IDS = new Set([
21
+ "cd4047",
22
+ "cd4047b",
23
+ "cd4047be",
24
+ "cd4047bm",
25
+ "cd4047bc",
26
+ "hcf4047",
27
+ "hef4047",
28
+ "mc14047b",
29
+ "mc14047bd",
30
+ ]);
31
+ const MN3101_PART_IDS = new Set([
32
+ "mn3101",
33
+ "mn3101a",
34
+ "upd3101",
35
+ "upd3101c",
36
+ "upd3101d",
37
+ ]);
38
+ const MN3102_PART_IDS = new Set([
39
+ "mn3102",
40
+ "mn3102a",
41
+ "upd3102",
42
+ "upd3102c",
43
+ "upd3102d",
44
+ ]);
45
+ const CD4047_C_ROLES = new Set(["c", "pin1", "1", "cext", "c_ext"]);
46
+ const CD4047_R_ROLES = new Set(["r", "pin2", "2", "rext", "r_ext"]);
47
+ const CD4047_RC_ROLES = new Set(["rccommon", "pin3", "3", "rc_common", "rc", "common"]);
48
+ const PT2399_VCO_ROLES = new Set(["vco", "vcopin", "vco_pin", "pin6", "6"]);
49
+ const INVERTER_OSCILLATOR_PART_IDS = new Set([
50
+ "74hc04",
51
+ "74hcu04",
52
+ "74hc04n",
53
+ "74hcu04n",
54
+ "74hc04d",
55
+ "74hcu04d",
56
+ "74hc14",
57
+ "74hc14n",
58
+ "cd4069",
59
+ "cd4069ub",
60
+ "cd4069ube",
61
+ "cd4069ubm",
62
+ "hcf4069",
63
+ "hef4069",
64
+ "hef4069ub",
65
+ "tc4069",
66
+ "tc4069ub",
67
+ ]);
68
+ const OX_CANONICAL_ROLES = new Set(["ox1", "ox2", "ox3"]);
69
+ const isPositionalToken = (token) => /^(?:pin)?\d+$/.test(token);
70
+ function oscillatorRoleTokens(device, registry) {
71
+ const tokens = new Set(OX_CANONICAL_ROLES);
72
+ const partNumber = device.identity.partNumber;
73
+ if (partNumber === null) {
74
+ return tokens;
75
+ }
76
+ const entry = registryEntryFor(registry, { partId: partNumber, evidence: "exact-part" }, device.nodes.length);
77
+ if (entry === null) {
78
+ return tokens;
79
+ }
80
+ for (const group of entry.terminalRoleGroups) {
81
+ const folded = group.map(foldToken);
82
+ if (!folded.some((token) => OX_CANONICAL_ROLES.has(token))) {
83
+ continue;
84
+ }
85
+ for (const token of folded) {
86
+ if (!isPositionalToken(token)) {
87
+ tokens.add(token);
88
+ }
89
+ }
90
+ }
91
+ return tokens;
92
+ }
93
+ /**
94
+ * Device kinds that can *drive* a timing node rather than merely set it.
95
+ *
96
+ * A resistor, a capacitor and a potentiometer are the timing network — a pot is a knob, which is
97
+ * control-rate and already carried as `controlId`. Anything on this list sitting on the timing
98
+ * network is steering it at signal rate, which is how an LFO reaches a BBD clock.
99
+ *
100
+ * `diode` is deliberately absent: `mxr-carbon-copy` clamps its CD4047 timing node with `D5` to
101
+ * ground, and a clamp is not a modulator. `ic` is absent because the clock driver is itself an
102
+ * `ic` on its own timing pins.
103
+ */
104
+ export const ACTIVE_TIMING_KINDS = new Set([
105
+ "bjt",
106
+ "jfet",
107
+ "mosfet",
108
+ "opamp",
109
+ "triode",
110
+ "pentode",
111
+ ]);
112
+ /**
113
+ * Devices actively steering this clock driver's timing network, by id.
114
+ *
115
+ * The discrimination this exists for, measured 2026-08-29 across the corpus: `mxr-carbon-copy`
116
+ * and `boss-dm-2` carry only resistors, capacitors and pots on their timing nodes, so their
117
+ * derived delays are a property of the circuit. `boss-ce-5` carries **`Q8`'s emitter directly
118
+ * on oscillator node 42**, so the frequency is set by that transistor's current and the R·C
119
+ * arithmetic reads a resistor that is not what times the oscillator.
120
+ */
121
+ export function activeTimingDrivers(clockDevice, netlist, registry) {
122
+ const timing = clockTimingNodes(clockDevice, registry);
123
+ if (timing.size === 0) {
124
+ return [];
125
+ }
126
+ const found = new Set();
127
+ for (const candidate of netlist.devices) {
128
+ if (candidate.id === clockDevice.id) {
129
+ continue;
130
+ }
131
+ if (!ACTIVE_TIMING_KINDS.has(candidate.kind)) {
132
+ continue;
133
+ }
134
+ if (candidate.nodes.some((n) => timing.has(n))) {
135
+ found.add(candidate.id);
136
+ }
137
+ }
138
+ return [...found].sort();
139
+ }
140
+ /**
141
+ * Logic parts that generate or divide a BBD clock, by exact part id.
142
+ *
143
+ * A closed vocabulary, matched as whole folded values — never a substring, never a declared-type
144
+ * or name heuristic. Two corpus packets clock their brigade this way instead of from a dedicated
145
+ * CD4047/MN310x: `mxr-micro-flanger` (MC14069UB hex inverter, a relaxation oscillator) and
146
+ * `mxr-m117r-flanger` (CD4013AE dual D flip-flop, dividing an upstream oscillator into the two
147
+ * phases the SAD1024 wants).
148
+ *
149
+ * Recognising them buys a **correct diagnosis, not a delay**: both are flangers, so both sweep,
150
+ * and both therefore reach the modulated refusal below. That is the whole point — before this
151
+ * they reported "no clock driver device is wired to this delay line", which is false.
152
+ */
153
+ const GATE_CLOCK_PART_IDS = new Set([
154
+ ...INVERTER_OSCILLATOR_PART_IDS,
155
+ "mc14069",
156
+ "mc14069b",
157
+ "mc14069ub",
158
+ "mc14069ubcp",
159
+ "cd4013",
160
+ "cd4013a",
161
+ "cd4013ae",
162
+ "cd4013b",
163
+ "cd4013be",
164
+ "hef4013",
165
+ "hcf4013",
166
+ "mc14013",
167
+ "mc14013b",
168
+ "cd4011",
169
+ "cd4011b",
170
+ "cd4011be",
171
+ "hef4011",
172
+ "mc14011",
173
+ "mc14011b",
174
+ "hd14011",
175
+ "hd14011bp",
176
+ "cd4043",
177
+ "cd4043b",
178
+ "scl4043",
179
+ "scl4043a",
180
+ ]);
181
+ /** Role tokens naming a supply pin, which is never part of a timing network. */
182
+ const SUPPLY_ROLE_TOKENS = new Set([
183
+ "vss",
184
+ "vdd",
185
+ "vcc",
186
+ "vee",
187
+ "gnd",
188
+ "ground",
189
+ "v+",
190
+ "v-",
191
+ "vbb",
192
+ ]);
193
+ /**
194
+ * Role tokens naming a bucket brigade's clock pins.
195
+ *
196
+ * Closed and compared as whole values. The `a`/`b` suffixes are the SAD1024's two independent
197
+ * halves, which `mxr-m117r-flanger` declares as `phi1a`/`phi2a`/`phi1b`/`phi2b`.
198
+ */
199
+ const BBD_CLOCK_ROLE_TOKENS = new Set([
200
+ "clk",
201
+ "clk1",
202
+ "clk2",
203
+ "clock",
204
+ "clock1",
205
+ "clock2",
206
+ "cp1",
207
+ "cp2",
208
+ "phi1",
209
+ "phi2",
210
+ "phi1a",
211
+ "phi2a",
212
+ "phi1b",
213
+ "phi2b",
214
+ ]);
215
+ /** The nodes a bucket brigade takes its clock on. */
216
+ function bbdClockNodes(bbdDevice) {
217
+ const nodes = new Set();
218
+ (bbdDevice.identity.terminalRoles ?? []).forEach((role, index) => {
219
+ if (role === null || !BBD_CLOCK_ROLE_TOKENS.has(foldToken(role))) {
220
+ return;
221
+ }
222
+ const node = bbdDevice.nodes[index];
223
+ if (node !== undefined && node !== GROUND) {
224
+ nodes.add(node);
225
+ }
226
+ });
227
+ return nodes;
228
+ }
229
+ /** A logic part generating or dividing this brigade's clock, or `null`. */
230
+ function findGateClockSource(bbdDevice, netlist) {
231
+ const clockNodes = bbdClockNodes(bbdDevice);
232
+ if (clockNodes.size === 0) {
233
+ return null;
234
+ }
235
+ for (const candidate of netlist.devices) {
236
+ if (candidate.id === bbdDevice.id) {
237
+ continue;
238
+ }
239
+ if (!GATE_CLOCK_PART_IDS.has(foldToken(candidate.identity.partNumber ?? ""))) {
240
+ continue;
241
+ }
242
+ if (candidate.nodes.some((n) => clockNodes.has(n))) {
243
+ return candidate;
244
+ }
245
+ }
246
+ return null;
247
+ }
248
+ /** A gate part's supply nodes, by role token. */
249
+ function gateSupplyNodes(device) {
250
+ const nodes = new Set();
251
+ (device.identity.terminalRoles ?? []).forEach((role, index) => {
252
+ if (role === null || !SUPPLY_ROLE_TOKENS.has(foldToken(role))) {
253
+ return;
254
+ }
255
+ const node = device.nodes[index];
256
+ if (node !== undefined) {
257
+ nodes.add(node);
258
+ }
259
+ });
260
+ return nodes;
261
+ }
262
+ /**
263
+ * Every `GATE_CLOCK_PART_IDS` device electrically chained to `entryGate` through shared
264
+ * non-supply pins -- the whole relaxation-oscillator loop, not just the one gate whose output
265
+ * happens to land on the BBD's clock pin.
266
+ *
267
+ * `mxr-micro-flanger` clocks its SAD512D from `U2.G2` (an MC14069 inverter), but the actual
268
+ * oscillator is **two** inverters in series: `U2.G1`'s output (node 2) feeds `U2.G2`'s input,
269
+ * and the timing capacitor `C15` bridges back from that same node to `U2.G1`'s own input (node
270
+ * 1) -- the node `Q3` steers to sweep the rate. Scoping the timing network to `U2.G2` alone drops
271
+ * node 1 from it entirely, so `Q3` reads as two passive hops away instead of one and
272
+ * `resolveClockModulationSource` returns "none" for a pedal that plainly sweeps.
273
+ *
274
+ * The walk follows only the closed `GATE_CLOCK_PART_IDS` vocabulary and only actual shared
275
+ * connectivity -- never a name, an id, or a declared type -- so it generalises to any oscillator
276
+ * built from more than one package section: a fan-in/fan-out ambiguity is not resolved by
277
+ * guessing, it just does not extend the walk past it. `U2.G3`..`U2.G6` on this same package are
278
+ * tied-off spare gates with no node in common with `U2.G1`/`U2.G2`'s signal nodes, so they are
279
+ * correctly left out.
280
+ */
281
+ function gateOscillatorChain(entryGate, netlist) {
282
+ const chain = new Map([[entryGate.id, entryGate]]);
283
+ const stack = [entryGate];
284
+ while (stack.length > 0) {
285
+ const current = stack.pop();
286
+ const currentSignal = gateSignalNodes(current);
287
+ for (const candidate of netlist.devices) {
288
+ if (chain.has(candidate.id)) {
289
+ continue;
290
+ }
291
+ if (!GATE_CLOCK_PART_IDS.has(foldToken(candidate.identity.partNumber ?? ""))) {
292
+ continue;
293
+ }
294
+ const candidateSignal = gateSignalNodes(candidate);
295
+ const connected = [...candidateSignal].some((n) => currentSignal.has(n));
296
+ if (connected) {
297
+ chain.set(candidate.id, candidate);
298
+ stack.push(candidate);
299
+ }
300
+ }
301
+ }
302
+ return [...chain.values()];
303
+ }
304
+ /** The signal nodes of every gate chained into `entryGate`'s oscillator loop. */
305
+ function gateOscillatorSignalNodes(entryGate, netlist) {
306
+ const nodes = new Set();
307
+ for (const device of gateOscillatorChain(entryGate, netlist)) {
308
+ for (const n of gateSignalNodes(device)) {
309
+ nodes.add(n);
310
+ }
311
+ }
312
+ return nodes;
313
+ }
314
+ /** The supply nodes of every gate chained into `entryGate`'s oscillator loop. */
315
+ function gateOscillatorSupplyNodes(entryGate, netlist) {
316
+ const nodes = new Set();
317
+ for (const device of gateOscillatorChain(entryGate, netlist)) {
318
+ for (const n of gateSupplyNodes(device)) {
319
+ nodes.add(n);
320
+ }
321
+ }
322
+ return nodes;
323
+ }
324
+ /** A gate part's non-supply nodes: its oscillator, once the rails are excluded by role. */
325
+ function gateSignalNodes(device) {
326
+ const nodes = new Set();
327
+ (device.identity.terminalRoles ?? []).forEach((role, index) => {
328
+ if (role !== null && SUPPLY_ROLE_TOKENS.has(foldToken(role))) {
329
+ return;
330
+ }
331
+ const node = device.nodes[index];
332
+ if (node !== undefined && node !== GROUND) {
333
+ nodes.add(node);
334
+ }
335
+ });
336
+ return nodes;
337
+ }
338
+ /** Two-terminal passives a steering current reaches a timing node through. */
339
+ const PASSIVE_HOP_KINDS = new Set([
340
+ "resistor",
341
+ "capacitor",
342
+ "potentiometer",
343
+ "rheostat",
344
+ ]);
345
+ const TIMING_RESISTANCE_KINDS = new Set([
346
+ "resistor",
347
+ "potentiometer",
348
+ "rheostat",
349
+ ]);
350
+ /**
351
+ * Active devices on a timing network, optionally reached through one passive component.
352
+ *
353
+ * **The one-hop form is used only on the gate-oscillator branch, and that restriction is
354
+ * load-bearing.** `mxr-micro-flanger` steers its MC14069 oscillator with `Q3`, whose collector
355
+ * feeds oscillator node 1 through `R27`/`R29` rather than sitting on it — direct contact cannot
356
+ * see that. But widening the dedicated-driver check the same way would reach past the timing
357
+ * components of circuits that currently derive honestly, and there the cost of a false positive
358
+ * is a lost delay rather than a slightly less precise refusal. The gate branch refuses either
359
+ * way, so a false positive there costs only wording.
360
+ */
361
+ function activeDevicesOn(timing, netlist, exclude, throughOnePassive = false, blockedNodes = new Set()) {
362
+ const reachable = new Set(timing);
363
+ if (throughOnePassive) {
364
+ for (const d of netlist.devices) {
365
+ if (!PASSIVE_HOP_KINDS.has(d.kind)) {
366
+ continue;
367
+ }
368
+ if (!d.nodes.some((n) => timing.has(n))) {
369
+ continue;
370
+ }
371
+ for (const n of d.nodes) {
372
+ // **Never hop onto a supply rail.** `mxr-micro-flanger` decouples its MC14069
373
+ // with `C21` from VDD to oscillator node 4, so without this the rail joins the
374
+ // reachable set and every transistor with an emitter on it -- `Q1`, `Q2` --
375
+ // reads as steering the oscillator. Only `Q3` actually does.
376
+ if (n !== GROUND && !blockedNodes.has(n)) {
377
+ reachable.add(n);
378
+ }
379
+ }
380
+ }
381
+ }
382
+ const found = new Set();
383
+ for (const candidate of netlist.devices) {
384
+ if (exclude.has(candidate.id)) {
385
+ continue;
386
+ }
387
+ if (!ACTIVE_TIMING_KINDS.has(candidate.kind)) {
388
+ continue;
389
+ }
390
+ if (candidate.nodes.some((n) => reachable.has(n))) {
391
+ found.add(candidate.id);
392
+ }
393
+ }
394
+ return [...found].sort();
395
+ }
396
+ /** Terminal roles that *control* an active device, as opposed to carrying its current. */
397
+ const CONTROL_ROLE_TOKENS = new Set(["base", "gate"]);
398
+ /**
399
+ * The node an LFO reaches a modulated BBD clock on: the steering device's **control** terminal.
400
+ *
401
+ * **Not the timing node.** That is the point of this function. The timing node carries the
402
+ * oscillator's own waveform at the clock rate — for a chorus, ~100 kHz, far above anything a
403
+ * 48 kHz host can sample — so reading it as a modulation source would alias garbage into the
404
+ * delay length. The control terminal carries the LFO and nothing else: on `boss-ce-2` that is
405
+ * `Q5`'s base, which the DEPTH pot reaches through `Q4` and `D1`.
406
+ *
407
+ * Deliberately strict. Exactly one steering device with one resolvable control terminal, and
408
+ * that terminal may not sit on ground or on the timing network itself. Anything else refuses by
409
+ * name and leaves the delay static, because a guess here is a sweep of the wrong thing.
410
+ */
411
+ export function resolveClockModulationSource(bbdDevice, netlist, registry) {
412
+ const dedicated = findClockDriverDevice(netlist);
413
+ let timing;
414
+ let steering;
415
+ let sourceId;
416
+ if (dedicated !== null) {
417
+ timing = clockTimingNodes(dedicated, registry);
418
+ steering = activeTimingDrivers(dedicated, netlist, registry);
419
+ sourceId = dedicated.id;
420
+ }
421
+ else {
422
+ const gate = findGateClockSource(bbdDevice, netlist);
423
+ if (gate === null) {
424
+ return { outcome: "none" };
425
+ }
426
+ const chain = gateOscillatorChain(gate, netlist);
427
+ timing = gateOscillatorSignalNodes(gate, netlist);
428
+ steering = activeDevicesOn(timing, netlist, new Set([...chain.map((d) => d.id), bbdDevice.id]), true, gateOscillatorSupplyNodes(gate, netlist));
429
+ sourceId = gate.id;
430
+ }
431
+ if (steering.length === 0) {
432
+ return { outcome: "none" };
433
+ }
434
+ if (steering.length > 1) {
435
+ return {
436
+ outcome: "refused",
437
+ reason: `${sourceId}'s timing network is steered by ${steering.length} devices ` +
438
+ `(${steering.join(", ")}); a modulation port needs exactly one control terminal ` +
439
+ `and choosing between them would be a guess`,
440
+ };
441
+ }
442
+ const steerer = netlist.devices.find((d) => d.id === steering[0]);
443
+ if (steerer === undefined) {
444
+ return { outcome: "none" };
445
+ }
446
+ const controls = [];
447
+ (steerer.identity.terminalRoles ?? []).forEach((role, index) => {
448
+ if (role === null || !CONTROL_ROLE_TOKENS.has(foldToken(role))) {
449
+ return;
450
+ }
451
+ const node = steerer.nodes[index];
452
+ if (node !== undefined) {
453
+ controls.push(node);
454
+ }
455
+ });
456
+ if (controls.length !== 1) {
457
+ return {
458
+ outcome: "refused",
459
+ reason: `${steerer.id} steers ${sourceId}'s timing network but exposes ` +
460
+ `${controls.length} control terminals, so the node its modulation arrives on is ` +
461
+ `not determined`,
462
+ };
463
+ }
464
+ const node = controls[0];
465
+ if (node === GROUND) {
466
+ return {
467
+ outcome: "refused",
468
+ reason: `${steerer.id}'s control terminal is grounded, so it carries no modulation`,
469
+ };
470
+ }
471
+ if (timing.has(node)) {
472
+ return {
473
+ outcome: "refused",
474
+ reason: `${steerer.id}'s control terminal sits on the timing network itself (node ` +
475
+ `${node}), so it carries the oscillator's own waveform rather than a modulation`,
476
+ };
477
+ }
478
+ return { outcome: "resolved", steeredBy: steerer.id, node };
479
+ }
480
+ /** The refusal an actively-steered timing network earns, or `null` when the network is passive. */
481
+ function modulatedClockRefusal(clockDevice, netlist, registry) {
482
+ const drivers = activeTimingDrivers(clockDevice, netlist, registry);
483
+ if (drivers.length === 0) {
484
+ return null;
485
+ }
486
+ return (`timing network is steered by ${drivers.join(", ")}, so this clock is modulated and its ` +
487
+ `delay is not a single number: R and C do not time this oscillator, and reducing it to ` +
488
+ `stages / (2 * f(R,C)) would report one arbitrary point of a sweep as though it were the ` +
489
+ `delay. Needs the signal-rate modulation port (see ` +
490
+ `thoughts/shared/plans/2026-08-29-bbd-modulated-delay-architecture.md, M3)`);
491
+ }
492
+ /**
493
+ * The nodes a clock driver's own timing network hangs on, by family.
494
+ *
495
+ * Exported because `scripts/report-bbd-architecture.ts` asks the same question and answered it
496
+ * with a private copy of the role vocabulary, which **under-reported immediately**: an MN3102
497
+ * spelling its pins `oscillatorinput`/`oscillatoroutput`/`clockcontrol` (`boss-bf-2`) matched
498
+ * none of the canonical `ox1`/`ox2`/`ox3` tokens, so a driver the compiler finds read as absent
499
+ * in the report. One enumerator, shared, is the same conclusion `stampNodes` reached.
500
+ *
501
+ * Role tokens are compared as whole values after folding, never by substring, and the MN310x
502
+ * branch admits the registry's own alias spellings through `oscillatorRoleTokens`.
503
+ */
504
+ export function clockTimingNodes(device, registry) {
505
+ const family = identifyClockFamily(device);
506
+ const roles = device.identity.terminalRoles ?? [];
507
+ const wanted = family === "CD4047"
508
+ ? new Set([...CD4047_C_ROLES, ...CD4047_R_ROLES, ...CD4047_RC_ROLES])
509
+ : oscillatorRoleTokens(device, registry);
510
+ const nodes = new Set();
511
+ roles.forEach((role, index) => {
512
+ if (role === null) {
513
+ return;
514
+ }
515
+ const token = foldToken(role);
516
+ // A positional token (`pin3`, `3`) is only meaningful for the CD4047, whose timing pins
517
+ // are fixed by the datasheet. Admitting them for an MN310x would pull in whichever pin
518
+ // happened to sit at that index.
519
+ if (family !== "CD4047" && isPositionalToken(token)) {
520
+ return;
521
+ }
522
+ if (!wanted.has(token)) {
523
+ return;
524
+ }
525
+ const node = device.nodes[index];
526
+ if (node !== undefined && node !== GROUND) {
527
+ nodes.add(node);
528
+ }
529
+ });
530
+ return nodes;
531
+ }
532
+ export function identifyClockFamily(device) {
533
+ const part = foldToken(device.identity.partNumber ?? "");
534
+ if (CD4047_PART_IDS.has(part))
535
+ return "CD4047";
536
+ if (MN3101_PART_IDS.has(part))
537
+ return "MN3101";
538
+ if (MN3102_PART_IDS.has(part))
539
+ return "MN3102";
540
+ return "unknown";
541
+ }
542
+ export function findClockDriverDevice(netlist) {
543
+ for (const d of netlist.devices) {
544
+ const family = identifyClockFamily(d);
545
+ if (family !== "unknown")
546
+ return d;
547
+ const declared = foldToken(d.identity.declaredType ?? "");
548
+ if (declared === "circuit.bbdclockdriver" || declared === "circuit.clockdriver") {
549
+ return d;
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+ function resolveCd4047Pins(device) {
555
+ let pin1_C = null;
556
+ let pin2_R = null;
557
+ let pin3_RC = null;
558
+ for (let i = 0; i < device.identity.terminalRoles.length; i++) {
559
+ const role = foldToken(device.identity.terminalRoles[i] ?? "");
560
+ const node = device.nodes[i];
561
+ if (node === undefined)
562
+ continue;
563
+ if (CD4047_C_ROLES.has(role))
564
+ pin1_C = node;
565
+ if (CD4047_R_ROLES.has(role))
566
+ pin2_R = node;
567
+ if (CD4047_RC_ROLES.has(role))
568
+ pin3_RC = node;
569
+ }
570
+ const isBarePinOrEmpty = (role, pinNum) => {
571
+ const f = foldToken(role ?? "");
572
+ return f === "" || f === String(pinNum) || f === `pin${pinNum}`;
573
+ };
574
+ const roles = device.identity.terminalRoles;
575
+ if (device.nodes.length >= 14) {
576
+ if (pin1_C === null && isBarePinOrEmpty(roles[0], 1)) {
577
+ pin1_C = device.nodes[0] ?? null;
578
+ }
579
+ if (pin2_R === null && isBarePinOrEmpty(roles[1], 2)) {
580
+ pin2_R = device.nodes[1] ?? null;
581
+ }
582
+ if (pin3_RC === null && isBarePinOrEmpty(roles[2], 3)) {
583
+ pin3_RC = device.nodes[2] ?? null;
584
+ }
585
+ }
586
+ return { pin1_C, pin2_R, pin3_RC };
587
+ }
588
+ /**
589
+ * Which canonical OX role each of a part's role aliases stands for.
590
+ *
591
+ * The set returned by `oscillatorRoleTokens` answers "is this terminal an oscillator pin", which
592
+ * is all the old two-pin bridge test needed. The datasheet oscillator is a star and its three
593
+ * legs are not interchangeable -- C1 hangs on OX3, the timing R2 on OX2, and R1 on OX1 is not a
594
+ * timing element at all -- so the legs have to be told apart by name, not just counted.
595
+ */
596
+ function oscillatorRoleByAlias(device, registry) {
597
+ const byAlias = new Map();
598
+ for (const canonical of OX_CANONICAL_ROLES)
599
+ byAlias.set(canonical, canonical);
600
+ const partNumber = device.identity.partNumber;
601
+ if (partNumber === null)
602
+ return byAlias;
603
+ const entry = registryEntryFor(registry, { partId: partNumber, evidence: "exact-part" }, device.nodes.length);
604
+ if (entry === null)
605
+ return byAlias;
606
+ for (const group of entry.terminalRoleGroups) {
607
+ const folded = group.map(foldToken);
608
+ const canonical = folded.find((token) => OX_CANONICAL_ROLES.has(token));
609
+ if (canonical === undefined)
610
+ continue;
611
+ for (const token of folded) {
612
+ if (!isPositionalToken(token))
613
+ byAlias.set(token, canonical);
614
+ }
615
+ }
616
+ return byAlias;
617
+ }
618
+ function resolveMn310xPins(device, registry) {
619
+ const oxRoles = oscillatorRoleTokens(device, registry);
620
+ const byAlias = oscillatorRoleByAlias(device, registry);
621
+ const oxNodes = [];
622
+ const byCanonical = new Map();
623
+ for (let i = 0; i < device.identity.terminalRoles.length; i++) {
624
+ const role = foldToken(device.identity.terminalRoles[i] ?? "");
625
+ const node = device.nodes[i];
626
+ if (node !== undefined && node !== GROUND && oxRoles.has(role)) {
627
+ if (!oxNodes.includes(node))
628
+ oxNodes.push(node);
629
+ const canonical = byAlias.get(role);
630
+ if (canonical !== undefined && !byCanonical.has(canonical)) {
631
+ byCanonical.set(canonical, node);
632
+ }
633
+ }
634
+ }
635
+ const isBarePinOrEmpty = (role, pinNum) => {
636
+ const f = foldToken(role ?? "");
637
+ return f === "" || f === String(pinNum) || f === `pin${pinNum}`;
638
+ };
639
+ if (oxNodes.length === 0 && device.nodes.length >= 8) {
640
+ // Bare or empty roles on pins 5/6/7. The datasheet's pin order is OX3, OX2, OX1, so the
641
+ // positional fallback can still name the legs.
642
+ const positional = ["ox3", "ox2", "ox1"];
643
+ for (let index = 4; index <= 6; index += 1) {
644
+ if (!isBarePinOrEmpty(device.identity.terminalRoles[index], index + 1)) {
645
+ continue;
646
+ }
647
+ const node = device.nodes[index];
648
+ if (node !== undefined && node !== GROUND && !oxNodes.includes(node)) {
649
+ oxNodes.push(node);
650
+ const canonical = positional[index - 4];
651
+ if (!byCanonical.has(canonical))
652
+ byCanonical.set(canonical, node);
653
+ }
654
+ }
655
+ }
656
+ return {
657
+ oxNodes,
658
+ ox1: byCanonical.get("ox1") ?? null,
659
+ ox2: byCanonical.get("ox2") ?? null,
660
+ ox3: byCanonical.get("ox3") ?? null,
661
+ };
662
+ }
663
+ /**
664
+ * The resistance a meter would read between two nodes of the resistive sub-network.
665
+ *
666
+ * Written because the MN3101's timing resistance is not one component and not one series or
667
+ * parallel run of them: on `boss-dm-2` it is `(VR3 + R47) || (R48 + VR6)`, two series branches in
668
+ * parallel, and the old candidate-list reduction could only do one of those two shapes at a time.
669
+ * Solving the conductance matrix costs a few dozen flops on a network this size and is right for
670
+ * any shape, including ones no corpus packet has yet.
671
+ *
672
+ * `ohmsFor` supplies each device's resistance at whatever control position the caller is
673
+ * evaluating, so a sweep is three calls rather than three special cases. A three-terminal
674
+ * potentiometer is entered as its two track halves, which makes a rheostat wiring -- wiper shorted
675
+ * to an end lug -- reduce on its own: the shorted half becomes a self-loop and drops out.
676
+ *
677
+ * Returns null when the two nodes are not resistively connected, which is a refusal, not a zero.
678
+ */
679
+ function resistanceBetween(devices, from, to, blocked, ohmsFor) {
680
+ if (from === to)
681
+ return 0;
682
+ const edges = [];
683
+ for (const d of devices) {
684
+ const usable = d.nodes.every((n) => n !== GROUND && !blocked.has(n));
685
+ if (!usable)
686
+ continue;
687
+ const { upper, lower } = ohmsFor(d);
688
+ if (d.nodes.length === 3) {
689
+ const [end1, wiper, end2] = d.nodes;
690
+ if (end1 !== wiper)
691
+ edges.push({ a: end1, b: wiper, ohms: upper });
692
+ if (wiper !== end2)
693
+ edges.push({ a: wiper, b: end2, ohms: lower });
694
+ }
695
+ else if (d.nodes.length === 2) {
696
+ const [a, b] = d.nodes;
697
+ if (a !== b)
698
+ edges.push({ a, b, ohms: upper });
699
+ }
700
+ else {
701
+ // Any other terminal count that still lands on exactly two nodes. `boss-dm-3`'s VR3 is
702
+ // declared with four terminals -- `lug1, lug2, lug3, wiper` -- on a three-lug part, with
703
+ // three of them shorted, and dropping it silently left the oscillator with no resistor.
704
+ // The conducting portion is one track half, so it sweeps 0..R; which end of the knob is
705
+ // which cannot be recovered from a declaration that duplicates a terminal, and the
706
+ // caller reports the sweep's extremes, so only the direction is lost, not the range.
707
+ const distinct = Array.from(new Set(d.nodes));
708
+ if (distinct.length === 2) {
709
+ edges.push({ a: distinct[0], b: distinct[1], ohms: lower });
710
+ }
711
+ }
712
+ }
713
+ if (edges.length === 0)
714
+ return null;
715
+ // **Only the component `from` and `to` sit in.** `devices` is the whole pedal's resistive
716
+ // network, most of which has no path to this oscillator; indexing all of it would put dozens of
717
+ // rows in the matrix with no route to the reference node, and every one of those is a zero pivot.
718
+ // Measured: the first version of this solver indexed every edge and refused `boss-dm-2` for
719
+ // "no resistive path" while the path was plainly there.
720
+ const adjacency = new Map();
721
+ for (const e of edges) {
722
+ if (!adjacency.has(e.a))
723
+ adjacency.set(e.a, []);
724
+ if (!adjacency.has(e.b))
725
+ adjacency.set(e.b, []);
726
+ adjacency.get(e.a).push(e.b);
727
+ adjacency.get(e.b).push(e.a);
728
+ }
729
+ if (!adjacency.has(from) || !adjacency.has(to))
730
+ return null;
731
+ const component = new Set([from]);
732
+ const queue = [from];
733
+ while (queue.length > 0) {
734
+ const node = queue.pop();
735
+ for (const next of adjacency.get(node) ?? []) {
736
+ if (!component.has(next)) {
737
+ component.add(next);
738
+ queue.push(next);
739
+ }
740
+ }
741
+ }
742
+ if (!component.has(to))
743
+ return null;
744
+ const index = new Map();
745
+ for (const node of component)
746
+ index.set(node, index.size);
747
+ // Inject 1 A at `from`, hold `to` at 0 V, read V(from). Gaussian elimination with partial
748
+ // pivoting on a matrix whose largest corpus instance is 4x4.
749
+ const n = index.size;
750
+ const ref = index.get(to);
751
+ const g = Array.from({ length: n }, () => new Array(n).fill(0));
752
+ const rhs = new Array(n).fill(0);
753
+ for (const e of edges) {
754
+ const a = index.get(e.a);
755
+ const b = index.get(e.b);
756
+ if (a === undefined || b === undefined)
757
+ continue;
758
+ const conductance = 1 / Math.max(e.ohms, 1e-9);
759
+ g[a][a] += conductance;
760
+ g[b][b] += conductance;
761
+ g[a][b] -= conductance;
762
+ g[b][a] -= conductance;
763
+ }
764
+ rhs[index.get(from)] = 1;
765
+ // Ground the reference node by replacing its row with V(ref) = 0.
766
+ g[ref] = new Array(n).fill(0);
767
+ g[ref][ref] = 1;
768
+ rhs[ref] = 0;
769
+ for (let col = 0; col < n; col += 1) {
770
+ let pivot = col;
771
+ for (let row = col + 1; row < n; row += 1) {
772
+ if (Math.abs(g[row][col]) > Math.abs(g[pivot][col]))
773
+ pivot = row;
774
+ }
775
+ if (Math.abs(g[pivot][col]) < 1e-18)
776
+ return null;
777
+ if (pivot !== col) {
778
+ [g[col], g[pivot]] = [g[pivot], g[col]];
779
+ [rhs[col], rhs[pivot]] = [rhs[pivot], rhs[col]];
780
+ }
781
+ const diagonal = g[col][col];
782
+ for (let row = col + 1; row < n; row += 1) {
783
+ const factor = g[row][col] / diagonal;
784
+ if (factor === 0)
785
+ continue;
786
+ for (let k = col; k < n; k += 1)
787
+ g[row][k] -= factor * g[col][k];
788
+ rhs[row] -= factor * rhs[col];
789
+ }
790
+ }
791
+ const v = new Array(n).fill(0);
792
+ for (let row = n - 1; row >= 0; row -= 1) {
793
+ let sum = rhs[row];
794
+ for (let k = row + 1; k < n; k += 1)
795
+ sum -= g[row][k] * v[k];
796
+ v[row] = sum / g[row][row];
797
+ }
798
+ const resistance = v[index.get(from)];
799
+ return Number.isFinite(resistance) && resistance >= 0 ? resistance : null;
800
+ }
801
+ /**
802
+ * Which of the timing network's controls a player is actually turning.
803
+ *
804
+ * **The declared role first, and resistance span only as a fallback.** Span is not evidence about
805
+ * which control a player turns: `mxr-carbon-copy` puts `VR3`, the DELAY knob, and `TR3`, a 500k
806
+ * internal trimmer, across the *identical* three nodes, so the larger span is the trimmer and
807
+ * picking it bound the delay to a part nobody can reach. Measured: the derived delay did not move
808
+ * when VR3's value changed from 230k to 100k, because VR3 was not driving it.
809
+ *
810
+ * `role` is a typed panel declaration compared as a whole value -- not a substring of the control's
811
+ * id, which is what the previous `startsWith("VR")` did and what this repository's own rules
812
+ * forbid. 13 corpus documents declare `delay-time`.
813
+ */
814
+ function primaryTimingControl(candidates, netlist) {
815
+ const taperByControl = new Map();
816
+ if (netlist.controls) {
817
+ for (const ctrl of netlist.controls) {
818
+ taperByControl.set(ctrl.id, ctrl.taper);
819
+ }
820
+ }
821
+ const controlled = candidates.filter((d) => d.control !== null);
822
+ let primaryDev = null;
823
+ if (controlled.length === 1) {
824
+ primaryDev = controlled[0];
825
+ }
826
+ else if (controlled.length > 1) {
827
+ const byRole = controlled.filter((d) => d.control !== null &&
828
+ netlist.controls.find((c) => c.id === d.control)?.role === "delay-time");
829
+ const pool = byRole.length > 0 ? byRole : controlled;
830
+ let maxSpan = -1;
831
+ for (const d of pool) {
832
+ const r0 = d.parameters.ohmsAt0 ?? 0;
833
+ const r1 = d.parameters.ohmsAt1 ?? (d.parameters.ohms ?? 0);
834
+ const span = Math.abs(r1 - r0);
835
+ if (span > maxSpan) {
836
+ maxSpan = span;
837
+ primaryDev = d;
838
+ }
839
+ }
840
+ primaryDev = primaryDev ?? controlled[0];
841
+ }
842
+ const controlId = primaryDev?.control ?? null;
843
+ return {
844
+ device: primaryDev,
845
+ controlId,
846
+ taper: controlId !== null ? (taperByControl.get(controlId) ?? "linear") : "linear",
847
+ };
848
+ }
849
+ function resolveTimingResistanceNetwork(candidates, netlist, isSeries = false) {
850
+ const taperByControl = new Map();
851
+ if (netlist.controls) {
852
+ for (const ctrl of netlist.controls) {
853
+ taperByControl.set(ctrl.id, ctrl.taper);
854
+ }
855
+ }
856
+ const primaryDev = primaryTimingControl(candidates, netlist).device;
857
+ let boundControlId = primaryDev?.control ?? null;
858
+ let boundTaper = boundControlId !== null
859
+ ? (taperByControl.get(boundControlId) ?? "linear")
860
+ : "linear";
861
+ if (isSeries) {
862
+ let totalRMin = 0;
863
+ let totalRMax = 0;
864
+ let totalRNom = 0;
865
+ for (const d of candidates) {
866
+ const { rMin, rMax, rNom } = resistanceSpanForTimingDevice(d, primaryDev);
867
+ totalRMin += rMin;
868
+ totalRMax += rMax;
869
+ totalRNom += rNom;
870
+ }
871
+ return {
872
+ rOhms: totalRNom,
873
+ ohmsAtControlMin: boundControlId !== null ? totalRMin : totalRNom,
874
+ ohmsAtControlMax: boundControlId !== null ? totalRMax : totalRNom,
875
+ controlId: boundControlId,
876
+ taper: boundTaper,
877
+ };
878
+ }
879
+ let totalConductanceMin = 0;
880
+ let totalConductanceMax = 0;
881
+ let totalConductanceNom = 0;
882
+ for (const d of candidates) {
883
+ const { rMin, rMax, rNom } = resistanceSpanForTimingDevice(d, primaryDev);
884
+ if (rMin > 0)
885
+ totalConductanceMin += 1 / rMin;
886
+ else if (rMin === 0)
887
+ totalConductanceMin = Infinity;
888
+ if (rMax > 0)
889
+ totalConductanceMax += 1 / rMax;
890
+ else if (rMax === 0)
891
+ totalConductanceMax = Infinity;
892
+ if (rNom > 0)
893
+ totalConductanceNom += 1 / rNom;
894
+ else if (rNom === 0)
895
+ totalConductanceNom = Infinity;
896
+ }
897
+ const ohmsAtControlMin = totalConductanceMin === Infinity
898
+ ? 0
899
+ : totalConductanceMin > 0
900
+ ? 1 / totalConductanceMin
901
+ : 0;
902
+ const ohmsAtControlMax = totalConductanceMax === Infinity
903
+ ? 0
904
+ : totalConductanceMax > 0
905
+ ? 1 / totalConductanceMax
906
+ : 0;
907
+ const rOhms = totalConductanceNom === Infinity
908
+ ? 0
909
+ : totalConductanceNom > 0
910
+ ? 1 / totalConductanceNom
911
+ : 0;
912
+ return {
913
+ rOhms,
914
+ ohmsAtControlMin: boundControlId !== null ? ohmsAtControlMin : rOhms,
915
+ ohmsAtControlMax: boundControlId !== null ? ohmsAtControlMax : rOhms,
916
+ controlId: boundControlId,
917
+ taper: boundTaper,
918
+ };
919
+ }
920
+ function resistanceSpanForTimingDevice(device, primaryDev) {
921
+ if (device.kind === "resistor") {
922
+ const ohms = device.parameters.ohms ?? 0;
923
+ return { rMin: ohms, rMax: ohms, rNom: ohms };
924
+ }
925
+ if (device.kind === "potentiometer") {
926
+ const ohms = device.parameters.ohms ?? 0;
927
+ if (device === primaryDev && device.control !== null) {
928
+ return { rMin: 0, rMax: ohms, rNom: ohms * 0.5 };
929
+ }
930
+ const held = ohms * 0.5;
931
+ return { rMin: held, rMax: held, rNom: held };
932
+ }
933
+ if (device.kind === "rheostat") {
934
+ const minOhms = device.parameters.minOhms ?? 0;
935
+ const maxOhms = device.parameters.maxOhms ?? 0;
936
+ if (device === primaryDev && device.control !== null) {
937
+ return {
938
+ rMin: minOhms,
939
+ rMax: maxOhms,
940
+ rNom: (minOhms + maxOhms) * 0.5,
941
+ };
942
+ }
943
+ const held = (minOhms + maxOhms) * 0.5;
944
+ return { rMin: held, rMax: held, rNom: held };
945
+ }
946
+ return { rMin: 0, rMax: 0, rNom: 0 };
947
+ }
948
+ function nodePairKey(device) {
949
+ const nonGround = device.nodes.filter((n) => n !== GROUND).sort((a, b) => a - b);
950
+ return nonGround.join(":");
951
+ }
952
+ /**
953
+ * Derive the BBD delay in seconds from the netlist's clock network.
954
+ */
955
+ /**
956
+ * The lowest clock a bucket brigade can be running and still carry audio.
957
+ *
958
+ * A BBD is a sampled system clocked at `f_clock`, so its own Nyquist limit is `f_clock / 2`.
959
+ * A guitar's highest fundamental is about 1.3 kHz (high E, 24th fret), so a brigade clocked
960
+ * below 2 kHz cannot reproduce even the fundamentals of what is fed to it -- whatever such a
961
+ * derivation produced, it is not this circuit's clock. Real BBD delays run from about 5 kHz at
962
+ * their longest setting to 100 kHz at their shortest, so this floor is far below any working
963
+ * pedal and rejects only derivations that are physically impossible.
964
+ *
965
+ * Measured across the corpus when this was added: `electro-harmonix-deluxe-memory-man` derives
966
+ * 17.0 kHz and `mxr-carbon-copy` 6.1 kHz, both comfortably clear, while
967
+ * `electro-harmonix-deluxe-memory-man-eh7550` derived **97 Hz** -- a 48 Hz Nyquist -- from a
968
+ * `C_DELAY` placeholder in a packet whose own `schematics/` and `sources/` are empty. That
969
+ * derivation then silently overrode the 125 ms the packet itself declares.
970
+ *
971
+ * Refusing here rather than clamping: a clamped delay would still be a fabricated number, and
972
+ * the caller's fallback to the declared `DelayMs` is now reported as declared rather than
973
+ * passed off as derived.
974
+ */
975
+ const MIN_PLAUSIBLE_CLOCK_HZ = 2000;
976
+ /** A derivation whose implied clock cannot carry audio is not a clock reading. */
977
+ function implausibleClock(fClockHz, stages) {
978
+ if (!Number.isFinite(fClockHz) || fClockHz <= 0) {
979
+ return `derived clock frequency is ${fClockHz}, which is not a frequency`;
980
+ }
981
+ if (fClockHz >= MIN_PLAUSIBLE_CLOCK_HZ) {
982
+ return null;
983
+ }
984
+ return (`derived clock is ${fClockHz.toFixed(1)} Hz, so this ${stages}-stage brigade would sample ` +
985
+ `audio at a ${(fClockHz / 2).toFixed(1)} Hz Nyquist limit and cannot carry any of it; ` +
986
+ `a working BBD delay clocks between about 5 kHz and 100 kHz`);
987
+ }
988
+ export function deriveBbdDelayFromNetlist(bbdDevice, netlist, stages, registry) {
989
+ const clockDev = findClockDriverDevice(netlist);
990
+ if (!clockDev) {
991
+ // No dedicated CD4047/MN310x. Before reporting that nothing clocks this brigade -- which
992
+ // on two corpus packets is simply false -- look for a logic part on its clock pins.
993
+ const gate = findGateClockSource(bbdDevice, netlist);
994
+ if (gate === null) {
995
+ return { outcome: "no-clock-driver" };
996
+ }
997
+ const chain = gateOscillatorChain(gate, netlist);
998
+ const timing = gateOscillatorSignalNodes(gate, netlist);
999
+ const steering = activeDevicesOn(timing, netlist, new Set([...chain.map((d) => d.id), bbdDevice.id]), true, gateOscillatorSupplyNodes(gate, netlist));
1000
+ if (steering.length > 0) {
1001
+ return {
1002
+ outcome: "refused",
1003
+ reason: `clock is generated by ${gate.id} (${gate.identity.partNumber ?? "logic part"}) ` +
1004
+ `and its network is steered by ${steering.join(", ")}, so this clock is ` +
1005
+ `modulated and its delay is not a single number. Needs the signal-rate ` +
1006
+ `modulation port (see ` +
1007
+ `thoughts/shared/plans/2026-08-29-bbd-modulated-delay-architecture.md, M3)`,
1008
+ };
1009
+ }
1010
+ // Passive gate oscillator. **Deliberately not derived**: a ring oscillator, a Schmitt
1011
+ // relaxation oscillator and a divided master clock have three different formulas, and
1012
+ // this repository has no corpus case to check any of them against -- every gate-clocked
1013
+ // brigade it holds is swept, and refused above. Inventing one here would be the guess
1014
+ // M1 exists to avoid.
1015
+ return {
1016
+ outcome: "refused",
1017
+ reason: `clock is generated by ${gate.id} (${gate.identity.partNumber ?? "logic part"}), ` +
1018
+ `a logic oscillator with no registered frequency formula; only CD4047 and ` +
1019
+ `MN3101/MN3102 timing networks are reducible today`,
1020
+ };
1021
+ }
1022
+ const clockFamily = identifyClockFamily(clockDev);
1023
+ // **Before any R·C arithmetic: is this oscillator even timed by R and C?**
1024
+ //
1025
+ // Placed here, ahead of both family branches, because the alternative is worse in two
1026
+ // different ways and the corpus shows both. Downstream of the missing-component refusals,
1027
+ // `boss-ce-2` and `boss-ch-1` report "No timing resistor/pot found strictly across
1028
+ // oscillator pins", which reads as a capture defect and sends a reader to add a resistor to
1029
+ // a correct `.vdsp` -- their timing resistance is a transistor, by design. And downstream of
1030
+ // the success path, `boss-ce-5` **derives 19.97 ms at 25.6 kHz from `R31`/`C21`+`C22` while
1031
+ // `Q8`'s emitter sits directly on oscillator node 42**, which is a plausible number for a
1032
+ // circuit whose frequency that arithmetic does not describe. A wrong number carrying a
1033
+ // `derived` provenance is more dangerous than a refusal, because every consumer downstream
1034
+ // is entitled to trust it.
1035
+ const modulated = modulatedClockRefusal(clockDev, netlist, registry);
1036
+ if (modulated !== null) {
1037
+ return { outcome: "refused", reason: modulated };
1038
+ }
1039
+ // 1. CD4047 Family
1040
+ if (clockFamily === "CD4047") {
1041
+ const { pin1_C, pin2_R, pin3_RC } = resolveCd4047Pins(clockDev);
1042
+ if (pin1_C === null || pin2_R === null || pin3_RC === null) {
1043
+ return {
1044
+ outcome: "refused",
1045
+ reason: "CD4047 timing terminals (pins 1, 2, 3) could not be resolved",
1046
+ };
1047
+ }
1048
+ if (pin1_C === pin2_R && pin2_R === pin3_RC) {
1049
+ return {
1050
+ outcome: "refused",
1051
+ reason: `CD4047 pins 1, 2, 3 shorted together to node ${pin1_C}`,
1052
+ };
1053
+ }
1054
+ if (pin1_C === GROUND && pin3_RC === GROUND) {
1055
+ return {
1056
+ outcome: "refused",
1057
+ reason: "CD4047 pin 1 (C) and pin 3 (RC common) both grounded; no timing capacitor",
1058
+ };
1059
+ }
1060
+ // Find timing resistors strictly across pin 2 (R) and pin 3 (RC common)
1061
+ let rCandidates = [];
1062
+ let isSeries = false;
1063
+ for (const d of netlist.devices) {
1064
+ if (d.kind !== "resistor" && d.kind !== "potentiometer" && d.kind !== "rheostat")
1065
+ continue;
1066
+ const touches2 = d.nodes.includes(pin2_R);
1067
+ const touches3 = d.nodes.includes(pin3_RC);
1068
+ if (touches2 && touches3) {
1069
+ rCandidates.push(d);
1070
+ }
1071
+ }
1072
+ if (rCandidates.length === 0) {
1073
+ // Check for two-device series chain: pin2_R <-> mid <-> pin3_RC
1074
+ const pin2Devices = netlist.devices.filter((d) => {
1075
+ if (d.kind !== "resistor" && d.kind !== "potentiometer" && d.kind !== "rheostat")
1076
+ return false;
1077
+ return d.nodes.includes(pin2_R) && !d.nodes.includes(pin3_RC);
1078
+ });
1079
+ for (const d1 of pin2Devices) {
1080
+ const midNodes = d1.nodes.filter((n) => n !== pin2_R && n !== GROUND);
1081
+ for (const mid of midNodes) {
1082
+ const midDevices = netlist.devices.filter((d) => {
1083
+ if (d === d1)
1084
+ return false;
1085
+ if (d.kind !== "resistor" && d.kind !== "potentiometer" && d.kind !== "rheostat")
1086
+ return false;
1087
+ return d.nodes.includes(mid) && d.nodes.includes(pin3_RC);
1088
+ });
1089
+ if (midDevices.length === 1) {
1090
+ rCandidates = [d1, midDevices[0]];
1091
+ isSeries = true;
1092
+ break;
1093
+ }
1094
+ }
1095
+ if (isSeries)
1096
+ break;
1097
+ }
1098
+ }
1099
+ // Find timing capacitors strictly across pin 1 (C) and pin 3 (RC common)
1100
+ const cCandidates = [];
1101
+ for (const d of netlist.devices) {
1102
+ if (d.kind !== "capacitor")
1103
+ continue;
1104
+ const touches1 = d.nodes.includes(pin1_C);
1105
+ const touches3 = d.nodes.includes(pin3_RC);
1106
+ if (touches1 && touches3) {
1107
+ cCandidates.push(d);
1108
+ }
1109
+ }
1110
+ if (rCandidates.length === 0) {
1111
+ return {
1112
+ outcome: "refused",
1113
+ reason: `No timing resistor/pot found strictly across CD4047 pins 2 (node ${pin2_R}) and 3 (node ${pin3_RC})`,
1114
+ };
1115
+ }
1116
+ if (cCandidates.length === 0) {
1117
+ return {
1118
+ outcome: "refused",
1119
+ reason: `No timing capacitor found strictly across CD4047 pins 1 (node ${pin1_C}) and 3 (node ${pin3_RC})`,
1120
+ };
1121
+ }
1122
+ const { rOhms, ohmsAtControlMin, ohmsAtControlMax, controlId, taper } = resolveTimingResistanceNetwork(rCandidates, netlist, isSeries);
1123
+ const cFarads = cCandidates.reduce((acc, c) => acc + (c.parameters.farads ?? 0), 0);
1124
+ if (rOhms <= 0) {
1125
+ return {
1126
+ outcome: "refused",
1127
+ reason: "Resolved timing resistance is <= 0 ohms",
1128
+ };
1129
+ }
1130
+ if (cFarads <= 0) {
1131
+ return {
1132
+ outcome: "refused",
1133
+ reason: "Resolved timing capacitance is <= 0 farads",
1134
+ };
1135
+ }
1136
+ // CD4047: f_clock = 1 / (4.40 * R * C) (TI CD4047B SCHS044C)
1137
+ const formulaConstant = 2.2;
1138
+ const fClockHz = 1 / (4.4 * rOhms * cFarads);
1139
+ const delaySeconds = stages * formulaConstant * rOhms * cFarads;
1140
+ const implausible = implausibleClock(fClockHz, stages);
1141
+ if (implausible !== null) {
1142
+ return { outcome: "refused", reason: implausible };
1143
+ }
1144
+ return {
1145
+ outcome: "derived",
1146
+ delaySeconds,
1147
+ rOhms,
1148
+ cFarads,
1149
+ fClockHz,
1150
+ family: "CD4047",
1151
+ stages,
1152
+ formulaConstant,
1153
+ offsetSeconds: 0,
1154
+ controlId,
1155
+ taper,
1156
+ ohmsAtControlMin,
1157
+ ohmsAtControlMax,
1158
+ };
1159
+ }
1160
+ // 2. MN3101 / MN3102 Family
1161
+ if (clockFamily === "MN3101" || clockFamily === "MN3102") {
1162
+ const { oxNodes, ox1, ox2, ox3 } = resolveMn310xPins(clockDev, registry);
1163
+ if (oxNodes.length === 0) {
1164
+ // Name the roles it actually declared. "Found 0 ox pins" reads as "the pins are
1165
+ // unconnected", and on `boss-bf-2` that is the wrong diagnosis: its MN3102 spells the
1166
+ // oscillator pins `oscillatorinput`/`oscillatoroutput`/`clockcontrol`, which the
1167
+ // registry entry does not carry as aliases for `ox1`/`ox2`/`ox3` -- so they were never
1168
+ // looked at, connected or not. Whether the fix is a registry alias or a re-capture is
1169
+ // the reader's call, and they cannot make it without seeing the spellings.
1170
+ const declared = (clockDev.identity.terminalRoles ?? [])
1171
+ .filter((r) => r !== null)
1172
+ .join(", ");
1173
+ return {
1174
+ outcome: "refused",
1175
+ reason: `${clockFamily} oscillator pins not resolved: no terminal of ${clockDev.id} ` +
1176
+ `matched ox1/ox2/ox3 or a registered alias. It declares [${declared}]`,
1177
+ };
1178
+ }
1179
+ // **The oscillator is a star, not a bridge.** MN3101 datasheet p.3, "Example of Oscillation
1180
+ // Generation Circuit": R1 hangs off OX1, R2 off OX2 and C1 off OX3, and all three return to
1181
+ // one common node. The text under it is explicit about which two set the rate -- "oscillation
1182
+ // frequency is defined by the time constant of C1 and R2" -- so R1 is not a timing element.
1183
+ //
1184
+ // The previous model looked for an R and a C each bridging two OX pins directly. Nothing in
1185
+ // the datasheet circuit does that, so a correctly captured MN3101 refused and only a
1186
+ // mis-captured one derived: `boss-dm-2` produced 0..341 ms against its printed 20..300 ms
1187
+ // spec from a capture that had C39 strung between the two oscillator pins.
1188
+ if (ox2 === null || ox3 === null) {
1189
+ const declared = (clockDev.identity.terminalRoles ?? [])
1190
+ .filter((r) => r !== null)
1191
+ .join(", ");
1192
+ return {
1193
+ outcome: "refused",
1194
+ reason: `${clockFamily} oscillator legs not named: the star needs OX2 (timing R) and OX3 ` +
1195
+ `(timing C) told apart, and ${clockDev.id} resolved ` +
1196
+ `ox2=${ox2 ?? "none"} ox3=${ox3 ?? "none"} from [${declared}]`,
1197
+ };
1198
+ }
1199
+ // C1: the capacitor from OX3 to the common node. Its far end *is* the common node -- that is
1200
+ // how the common node is found, rather than by assuming a name for it.
1201
+ const cCandidates = netlist.devices.filter((d) => d.kind === "capacitor" &&
1202
+ (d.parameters.farads ?? 0) > 0 &&
1203
+ (d.parameters.farads ?? 0) < 1e-6 &&
1204
+ d.nodes.length === 2 &&
1205
+ d.nodes.includes(ox3) &&
1206
+ !d.nodes.every((n) => n === ox3));
1207
+ if (cCandidates.length === 0) {
1208
+ return {
1209
+ outcome: "refused",
1210
+ reason: `No timing capacitor (<1µF) found on the ${clockFamily} OX3 pin (node ${ox3})`,
1211
+ };
1212
+ }
1213
+ const commonNodes = new Set(cCandidates.map((c) => c.nodes.find((n) => n !== ox3)));
1214
+ if (commonNodes.size > 1) {
1215
+ return {
1216
+ outcome: "refused",
1217
+ reason: `Ambiguous ${clockFamily} oscillator common node: the capacitors on OX3 return to ` +
1218
+ `nodes [${Array.from(commonNodes).join(", ")}], so there is no single RC junction`,
1219
+ };
1220
+ }
1221
+ const commonNode = Array.from(commonNodes)[0];
1222
+ const cFarads = cCandidates.reduce((acc, c) => acc + (c.parameters.farads ?? 0), 0);
1223
+ // R2: whatever a meter would read from OX2 to that common node. OX3 is blocked so the
1224
+ // capacitor's own pin cannot be walked through, and OX1 is blocked so the MN3101's R1 is
1225
+ // not counted as timing resistance.
1226
+ //
1227
+ // **Except when OX1 *is* the common node.** The two parts differ here and the MN3102 data
1228
+ // sheet (p.3, "Example of Oscillation Circuit") draws it plainly: the MN3101 has three legs
1229
+ // and R1 sits on OX1, while the MN3102 has two and OX1 wires straight to the common rail
1230
+ // with no R1 at all. Blocking OX1 unconditionally therefore blocked the common node itself
1231
+ // on every MN3102, and `boss-dm-3` refused for "no resistive path" when its capture already
1232
+ // had the data sheet's own circuit.
1233
+ const resistive = netlist.devices.filter((d) => TIMING_RESISTANCE_KINDS.has(d.kind));
1234
+ const blocked = new Set([ox3]);
1235
+ if (ox1 !== null && ox1 !== commonNode)
1236
+ blocked.add(ox1);
1237
+ const primary = primaryTimingControl(resistive.filter((d) => d.control !== null && !d.nodes.some((n) => blocked.has(n))), netlist);
1238
+ // Where a control that is *not* being swept sits. A factory trimmer on the timing network
1239
+ // moves the answer as much as the panel knob does, and holding it at mid-scale when the
1240
+ // document says where it is set is a silent guess. `boss-dm-2`'s VR3 is the case in point:
1241
+ // it is a 1M clock trim in series with R47 and the whole branch is in parallel with the
1242
+ // Repeat Rate branch, so mid-scale versus its real setting is the difference between a
1243
+ // 387 ms and a 301 ms maximum.
1244
+ const defaultPositionByControl = new Map();
1245
+ for (const ctrl of netlist.controls ?? []) {
1246
+ defaultPositionByControl.set(ctrl.id, ctrl.defaultPosition);
1247
+ }
1248
+ const heldPosition = (d) => (d.control !== null ? defaultPositionByControl.get(d.control) : undefined) ?? 0.5;
1249
+ const ohmsAtPosition = (position) => (d) => {
1250
+ const held = d === primary.device ? position : heldPosition(d);
1251
+ if (d.kind === "resistor") {
1252
+ const ohms = d.parameters.ohms ?? 0;
1253
+ return { upper: ohms, lower: ohms };
1254
+ }
1255
+ if (d.kind === "rheostat") {
1256
+ const minOhms = d.parameters.minOhms ?? 0;
1257
+ const maxOhms = d.parameters.maxOhms ?? 0;
1258
+ const ohms = minOhms + (maxOhms - minOhms) * held;
1259
+ return { upper: ohms, lower: ohms };
1260
+ }
1261
+ const total = d.parameters.ohms ?? 0;
1262
+ // Same split as the pot stamp in `lower.ts`: `upper` is end1..wiper and carries
1263
+ // `1 - fraction`, `lower` is wiper..end2 and carries `fraction`.
1264
+ return { upper: total * (1 - held), lower: total * held };
1265
+ };
1266
+ const rAt = (position) => resistanceBetween(resistive, ox2, commonNode, blocked, ohmsAtPosition(position));
1267
+ const rOhms = rAt(0.5);
1268
+ if (rOhms === null) {
1269
+ return {
1270
+ outcome: "refused",
1271
+ reason: `No resistive path from the ${clockFamily} OX2 pin (node ${ox2}) to the ` +
1272
+ `oscillator's common RC node (node ${commonNode}), so the timing resistance ` +
1273
+ `cannot be read off the circuit`,
1274
+ };
1275
+ }
1276
+ const rEnd0 = rAt(0);
1277
+ const rEnd1 = rAt(1);
1278
+ const controlId = primary.controlId;
1279
+ const taper = primary.taper;
1280
+ const ohmsAtControlMin = controlId !== null && rEnd0 !== null && rEnd1 !== null
1281
+ ? Math.min(rEnd0, rEnd1)
1282
+ : rOhms;
1283
+ const ohmsAtControlMax = controlId !== null && rEnd0 !== null && rEnd1 !== null
1284
+ ? Math.max(rEnd0, rEnd1)
1285
+ : rOhms;
1286
+ if (rOhms <= 0) {
1287
+ return {
1288
+ outcome: "refused",
1289
+ reason: "Resolved timing resistance is <= 0 ohms",
1290
+ };
1291
+ }
1292
+ if (cFarads <= 0) {
1293
+ return {
1294
+ outcome: "refused",
1295
+ reason: "Resolved timing capacitance is <= 0 farads",
1296
+ };
1297
+ }
1298
+ // MN3101 / MN3102: f_clock = 1 / (5.0 * R * C)
1299
+ const formulaConstant = 2.5;
1300
+ const fClockHz = 1 / (5.0 * rOhms * cFarads);
1301
+ const delaySeconds = stages * formulaConstant * rOhms * cFarads;
1302
+ const implausible = implausibleClock(fClockHz, stages);
1303
+ if (implausible !== null) {
1304
+ return { outcome: "refused", reason: implausible };
1305
+ }
1306
+ return {
1307
+ outcome: "derived",
1308
+ delaySeconds,
1309
+ rOhms,
1310
+ cFarads,
1311
+ fClockHz,
1312
+ family: clockFamily,
1313
+ stages,
1314
+ formulaConstant,
1315
+ offsetSeconds: 0,
1316
+ controlId,
1317
+ taper,
1318
+ ohmsAtControlMin,
1319
+ ohmsAtControlMax,
1320
+ };
1321
+ }
1322
+ return {
1323
+ outcome: "refused",
1324
+ reason: `Clock driver device ${clockDev.id} (${clockDev.identity.partNumber ?? "unknown"}) has unsupported clock family`,
1325
+ };
1326
+ }
1327
+ /**
1328
+ * Open-OX2 clock law (CE-2 first slice, 2026-09-28).
1329
+ *
1330
+ * The form the MN3101 datasheet never drew: OX2 left open (N.C.) and a single
1331
+ * relaxation loop across the OX1→OX3 inverter pair — a timing capacitor
1332
+ * between OX3 and OX1 charged through a pull-up on OX1 and discharged by a
1333
+ * transistor the OX3 side drives, with the LFO setting the restart level
1334
+ * through a diode. `boss-ce-2` v1.25 is the corpus case.
1335
+ *
1336
+ * Family constants (per-packet R and C come from the netlist):
1337
+ * - `OPEN_OX2_VDD_VOLTS`: the driver's supply span, 9 V nominal from the
1338
+ * packet's own supply port (the filtered rail measures 8.967 V, 0.4%,
1339
+ * disclosed and immaterial beside the threshold sensitivity).
1340
+ * - `OPEN_OX2_VTH_VOLTS`: solved 2026-09-28 for 110 kHz at the measured
1341
+ * Depth-0 divider level 1.565 V against a scope specimen (DIYstompboxes
1342
+ * 2011, one rough forum reading, disclosed), inside the 3.4–5.6 V window
1343
+ * the MN3101 p.60 example table allows. Re-solve, never tune: procedure in
1344
+ * `docs/todo-archive/2026-09-28-ce2-clock-law-analysis.md`.
1345
+ * - `OPEN_OX2_VF_VOLTS`: generic-diode forward drop, disclosed typical.
1346
+ * - `OPEN_OX2_FLOOR_VOLTS`: yank-minus-kick bottom under datasheet OX3 drive
1347
+ * (R_OH ≈ 667 Ω / R_OL ≈ 500 Ω from the I_OH2/I_OL2 minimums): a ~7 V OX3
1348
+ * step kicks ~6.2 V through 47 pF against cited diode maxima, landing near
1349
+ * −5.3 V — below the D1 floor at every measured divider level, so the D1
1350
+ * branch binds everywhere. Same max() form as the analysis, not a deletion.
1351
+ */
1352
+ export const OPEN_OX2_VDD_VOLTS = 9;
1353
+ export const OPEN_OX2_VTH_VOLTS = 4.757;
1354
+ export const OPEN_OX2_VF_VOLTS = 0.65;
1355
+ export const OPEN_OX2_FLOOR_VOLTS = -5.35;
1356
+ /** How many device terminals in the whole document touch `node`. */
1357
+ function nodeDegree(netlist, node) {
1358
+ let count = 0;
1359
+ for (const device of netlist.devices) {
1360
+ for (const terminal of device.nodes) {
1361
+ if (terminal === node) {
1362
+ count += 1;
1363
+ }
1364
+ }
1365
+ }
1366
+ return count;
1367
+ }
1368
+ /**
1369
+ * The node on exactly one terminal carrying `token` (whole folded value), or
1370
+ * null when zero or several do. Orientation and identity come from the
1371
+ * resolved roles, never from terminal order.
1372
+ */
1373
+ function terminalNodeByRole(device, token) {
1374
+ let found = null;
1375
+ device.identity.terminalRoles.forEach((role, index) => {
1376
+ if (role === null || foldToken(role) !== token) {
1377
+ return;
1378
+ }
1379
+ const node = device.nodes[index];
1380
+ if (node === undefined) {
1381
+ return;
1382
+ }
1383
+ if (found !== null) {
1384
+ found = null;
1385
+ return;
1386
+ }
1387
+ found = node;
1388
+ });
1389
+ return found;
1390
+ }
1391
+ function isTwoTerminalBetween(device, a, b) {
1392
+ return (device.nodes.length === 2 &&
1393
+ ((device.nodes[0] === a && device.nodes[1] === b) ||
1394
+ (device.nodes[0] === b && device.nodes[1] === a)));
1395
+ }
1396
+ /**
1397
+ * Recognise the open-OX2 relaxation form by connectivity, never by names or
1398
+ * prose: an MN3101-family driver with OX2 touched by nothing but its own pin,
1399
+ * one capacitor bridging OX3 to OX1, one pull-up resistor from OX1 to the
1400
+ * driver's own supply pin, one resistor from OX3 into a grounded-emitter
1401
+ * transistor's base, that transistor's collector on a pulled-up discharge
1402
+ * node back into OX1 through a diode, and one diode from OX1 into a
1403
+ * divider-fed slow node.
1404
+ *
1405
+ * Anything else is `absent` with the failed check named, and the caller falls
1406
+ * back to the existing star/gate derivation untouched. In particular the
1407
+ * pre-rewire collapse (capacitor on OX1–OX2), a transistor sitting directly
1408
+ * on a timing node (the modulated refusal owns that), and the MN3102 two-leg
1409
+ * form (a different oscillator; no corpus case) all read absent here.
1410
+ */
1411
+ export function deriveOpenOx2ClockLaw(netlist, registry) {
1412
+ const clockDev = findClockDriverDevice(netlist);
1413
+ if (clockDev === null ||
1414
+ identifyClockFamily(clockDev) !== "MN3101") {
1415
+ return { outcome: "absent", reason: "no MN3101 clock driver device" };
1416
+ }
1417
+ const { ox1, ox2, ox3 } = resolveMn310xPins(clockDev, registry);
1418
+ if (ox1 === null || ox2 === null || ox3 === null) {
1419
+ return {
1420
+ outcome: "absent",
1421
+ reason: "MN3101 oscillator legs not told apart (OX1/OX2/OX3)",
1422
+ };
1423
+ }
1424
+ if (ox1 === ox3 || ox1 === ox2 || ox2 === ox3) {
1425
+ return {
1426
+ outcome: "absent",
1427
+ reason: `MN3101 oscillator pins shorted together (OX1 ${ox1}, OX2 ${ox2}, OX3 ${ox3})`,
1428
+ };
1429
+ }
1430
+ if (nodeDegree(netlist, ox2) !== 1) {
1431
+ return {
1432
+ outcome: "absent",
1433
+ reason: `MN3101 OX2 (node ${ox2}) is wired into the circuit, so this is not the open-OX2 form`,
1434
+ };
1435
+ }
1436
+ const timingCaps = netlist.devices.filter((d) => d.kind === "capacitor" &&
1437
+ isTwoTerminalBetween(d, ox3, ox1) &&
1438
+ (d.parameters.farads ?? 0) > 0 &&
1439
+ (d.parameters.farads ?? 0) < 1e-6);
1440
+ if (timingCaps.length !== 1) {
1441
+ return {
1442
+ outcome: "absent",
1443
+ reason: `expected one timing capacitor between OX3 and OX1, found ${timingCaps.length}`,
1444
+ };
1445
+ }
1446
+ const cFarads = timingCaps[0].parameters.farads ?? 0;
1447
+ // The pull-up returns to the driver's own supply pin: role `vdd` (whole
1448
+ // value), else datasheet pin 3 by position. A pull-up to any other node is
1449
+ // a different circuit, and the law's VDD is that rail.
1450
+ let vddNode = terminalNodeByRole(clockDev, "vdd");
1451
+ if (vddNode === null && clockDev.nodes.length >= 8) {
1452
+ vddNode = clockDev.nodes[2] ?? null;
1453
+ }
1454
+ if (vddNode === null || vddNode === GROUND) {
1455
+ return {
1456
+ outcome: "absent",
1457
+ reason: "MN3101 supply pin not resolved, so the OX1 pull-up has no rail to return to",
1458
+ };
1459
+ }
1460
+ const pullUps = netlist.devices.filter((d) => d.kind === "resistor" &&
1461
+ isTwoTerminalBetween(d, ox1, vddNode) &&
1462
+ (d.parameters.ohms ?? 0) > 0);
1463
+ if (pullUps.length !== 1) {
1464
+ return {
1465
+ outcome: "absent",
1466
+ reason: `expected one pull-up resistor from OX1 to the supply rail, found ${pullUps.length}`,
1467
+ };
1468
+ }
1469
+ const rOhms = pullUps[0].parameters.ohms ?? 0;
1470
+ // The base-drive resistor: one end on OX3, the other on a node carrying
1471
+ // exactly one transistor base. A self-loop back onto OX3 is the collapsed
1472
+ // form, not this one.
1473
+ const baseRs = netlist.devices.filter((d) => d.kind === "resistor" &&
1474
+ d.nodes.length === 2 &&
1475
+ (d.nodes[0] === ox3 || d.nodes[1] === ox3));
1476
+ if (baseRs.length !== 1) {
1477
+ return {
1478
+ outcome: "absent",
1479
+ reason: `expected one base-drive resistor off OX3, found ${baseRs.length}`,
1480
+ };
1481
+ }
1482
+ const baseNode = baseRs[0].nodes.find((n) => n !== ox3) ?? null;
1483
+ if (baseNode === null || baseNode === GROUND) {
1484
+ return {
1485
+ outcome: "absent",
1486
+ reason: "the OX3 base-drive resistor returns to OX3 itself or to ground",
1487
+ };
1488
+ }
1489
+ const steerers = netlist.devices.filter((d) => d.kind === "bjt" && terminalNodeByRole(d, "base") === baseNode);
1490
+ if (steerers.length !== 1) {
1491
+ return {
1492
+ outcome: "absent",
1493
+ reason: `expected one transistor base on node ${baseNode}, found ${steerers.length}`,
1494
+ };
1495
+ }
1496
+ const steerer = steerers[0];
1497
+ const dischargeNode = terminalNodeByRole(steerer, "collector");
1498
+ const emitterNode = terminalNodeByRole(steerer, "emitter");
1499
+ if (dischargeNode === null ||
1500
+ emitterNode !== GROUND ||
1501
+ dischargeNode === ox1 ||
1502
+ dischargeNode === ox2 ||
1503
+ dischargeNode === ox3 ||
1504
+ dischargeNode === GROUND) {
1505
+ return {
1506
+ outcome: "absent",
1507
+ reason: `${steerer.id} is not a grounded-emitter switch onto its own discharge node`,
1508
+ };
1509
+ }
1510
+ const collectorPulls = netlist.devices.filter((d) => d.kind === "resistor" &&
1511
+ isTwoTerminalBetween(d, dischargeNode, vddNode));
1512
+ if (collectorPulls.length !== 1) {
1513
+ return {
1514
+ outcome: "absent",
1515
+ reason: `expected one collector pull-up on node ${dischargeNode}, found ${collectorPulls.length}`,
1516
+ };
1517
+ }
1518
+ const dischargeDiodes = netlist.devices.filter((d) => d.kind === "diode" &&
1519
+ terminalNodeByRole(d, "anode") === ox1 &&
1520
+ terminalNodeByRole(d, "cathode") === dischargeNode);
1521
+ if (dischargeDiodes.length !== 1) {
1522
+ return {
1523
+ outcome: "absent",
1524
+ reason: `expected one discharge diode from OX1 to node ${dischargeNode}, found ${dischargeDiodes.length}`,
1525
+ };
1526
+ }
1527
+ // The slow diode: cathode on OX1, anode on a divider-fed node that is
1528
+ // neither supply, ground, timing, nor the discharge node, and that no
1529
+ // transistor collector drives.
1530
+ const slowCandidates = netlist.devices.filter((d) => d.kind === "diode" && terminalNodeByRole(d, "cathode") === ox1);
1531
+ const slowDiodes = slowCandidates.filter((d) => {
1532
+ const anodeNode = terminalNodeByRole(d, "anode");
1533
+ if (anodeNode === null ||
1534
+ anodeNode === GROUND ||
1535
+ anodeNode === vddNode ||
1536
+ anodeNode === ox1 ||
1537
+ anodeNode === ox2 ||
1538
+ anodeNode === ox3 ||
1539
+ anodeNode === dischargeNode) {
1540
+ return false;
1541
+ }
1542
+ const dividerArms = netlist.devices.filter((candidate) => TIMING_RESISTANCE_KINDS.has(candidate.kind) &&
1543
+ candidate.nodes.includes(anodeNode));
1544
+ if (dividerArms.length < 2) {
1545
+ return false;
1546
+ }
1547
+ const driven = netlist.devices.some((candidate) => candidate.kind === "voltage-source" &&
1548
+ candidate.nodes.includes(anodeNode));
1549
+ if (driven) {
1550
+ return false;
1551
+ }
1552
+ const collectorDriven = netlist.devices.some((candidate) => candidate.kind === "bjt" &&
1553
+ terminalNodeByRole(candidate, "collector") === anodeNode);
1554
+ return !collectorDriven;
1555
+ });
1556
+ if (slowDiodes.length !== 1) {
1557
+ return {
1558
+ outcome: "absent",
1559
+ reason: `expected one slow diode from a divider-fed node into OX1, found ${slowDiodes.length}`,
1560
+ };
1561
+ }
1562
+ const slowNode = terminalNodeByRole(slowDiodes[0], "anode");
1563
+ if (slowNode === null) {
1564
+ return {
1565
+ outcome: "absent",
1566
+ reason: "the slow diode's anode node is not determined",
1567
+ };
1568
+ }
1569
+ return {
1570
+ outcome: "recognized",
1571
+ slowNode,
1572
+ steeredBy: steerer.id,
1573
+ rOhms,
1574
+ cFarads,
1575
+ };
1576
+ }
1577
+ /**
1578
+ * Princeton Technology PT2399 delay time from external VCO resistance R (in Ohms).
1579
+ *
1580
+ * Princeton PT2399 datasheet, TABLE 1: RESISTOR/DELAY TIME VALUES (V1.4, Oct 2005;
1581
+ * the same 28-row table is reprinted in V1.1, V1.2 and V1.6; the packet-local V1.9,
1582
+ * Mar 2022, drops the table but still says "Please refer to Table 1 for the
1583
+ * Resistor/Delay Time values"). Note 1 under the table states R in Ohms, so the
1584
+ * last row's "0.5" is 0.5 Ohms (pin 6 effectively grounded, 31.3 ms), not 0.5 kOhms.
1585
+ *
1586
+ * The table is linear in R: with pin 6 held at 2.5 V, the page
1587
+ * https://electrosmash.mas-effects.com/pt2399-analysis gives
1588
+ * I_pin6 (mA) = 28.65 / (Delay_ms - 29.70), i.e. Delay_ms = 29.70 + 11.46 * R_kOhms,
1589
+ * which reproduces all 28 cited rows within 4 ms (worst row: 27.6 kOhms, 346.0 ms
1590
+ * predicted vs 342 ms cited). That linear form is what this returns.
1591
+ *
1592
+ * Deliberately no clamp and no hard maximum: the cited source states none. Quality
1593
+ * degrades past about 350 ms (the table's own THD reaches 1.0% at 342 ms), but the
1594
+ * delay keeps growing with R, so this extrapolates the line rather than clipping it.
1595
+ */
1596
+ export function pt2399DelayFromOhms(rOhms) {
1597
+ return 0.0297 + (0.01146 * rOhms) / 1000;
1598
+ }
1599
+ /**
1600
+ * Derives delaySeconds and dynamic control mapping for PT2399 digital delay chips
1601
+ * by tracing the series resistor/potentiometer network connected to VCO Pin 6.
1602
+ */
1603
+ export function derivePt2399DelayFromNetlist(device, netlist) {
1604
+ // Find VCO node (pin 6 / role in PT2399_VCO_ROLES)
1605
+ let vcoNode = null;
1606
+ for (let i = 0; i < device.identity.terminalRoles.length; i++) {
1607
+ const role = foldToken(device.identity.terminalRoles[i] ?? "");
1608
+ if (PT2399_VCO_ROLES.has(role)) {
1609
+ vcoNode = device.nodes[i] ?? null;
1610
+ break;
1611
+ }
1612
+ }
1613
+ if (vcoNode === null && device.nodes.length >= 6) {
1614
+ vcoNode = device.nodes[5] ?? null;
1615
+ }
1616
+ if (vcoNode === null || vcoNode === GROUND) {
1617
+ return {
1618
+ outcome: "refused",
1619
+ reason: "PT2399 VCO pin 6 is unconnected or grounded",
1620
+ };
1621
+ }
1622
+ // Trace series path of resistors/pots from vcoNode to GROUND
1623
+ const visitedNodes = new Set([vcoNode]);
1624
+ const pathDevices = [];
1625
+ let currentNode = vcoNode;
1626
+ while (currentNode !== GROUND) {
1627
+ const connecting = netlist.devices.filter((d) => d.id !== device.id &&
1628
+ !pathDevices.includes(d) &&
1629
+ TIMING_RESISTANCE_KINDS.has(d.kind) &&
1630
+ d.nodes.includes(currentNode));
1631
+ if (connecting.length !== 1) {
1632
+ break;
1633
+ }
1634
+ const nextDev = connecting[0];
1635
+ pathDevices.push(nextDev);
1636
+ const otherNodes = nextDev.nodes.filter((n) => n !== currentNode && !visitedNodes.has(n));
1637
+ if (otherNodes.length === 0) {
1638
+ if (nextDev.nodes.includes(GROUND)) {
1639
+ currentNode = GROUND;
1640
+ }
1641
+ break;
1642
+ }
1643
+ currentNode = otherNodes[0];
1644
+ visitedNodes.add(currentNode);
1645
+ }
1646
+ if (currentNode !== GROUND || pathDevices.length === 0) {
1647
+ return {
1648
+ outcome: "refused",
1649
+ reason: "PT2399 VCO pin 6 timing network does not form a closed path to ground",
1650
+ };
1651
+ }
1652
+ const timingR = resolveTimingResistanceNetwork(pathDevices, netlist, true);
1653
+ if (timingR.rOhms <= 0) {
1654
+ return {
1655
+ outcome: "refused",
1656
+ reason: "Resolved PT2399 VCO timing resistance is <= 0 ohms",
1657
+ };
1658
+ }
1659
+ const delaySeconds = pt2399DelayFromOhms(timingR.rOhms);
1660
+ const fClockHz = 1 / (delaySeconds / 2048);
1661
+ return {
1662
+ outcome: "derived",
1663
+ delaySeconds,
1664
+ rOhms: timingR.rOhms,
1665
+ cFarads: 1,
1666
+ fClockHz,
1667
+ family: "unknown",
1668
+ stages: 1,
1669
+ formulaConstant: 0.01146 / 1000,
1670
+ // The datasheet law's intercept, carried so the knob sweeps the law
1671
+ // itself (`29.7 ms + 11.46 ms * R_kOhm`) rather than a line through
1672
+ // the origin fitted at the default resistance.
1673
+ offsetSeconds: 0.0297,
1674
+ controlId: timingR.controlId,
1675
+ taper: timingR.taper,
1676
+ ohmsAtControlMin: timingR.ohmsAtControlMin,
1677
+ ohmsAtControlMax: timingR.ohmsAtControlMax,
1678
+ };
1679
+ }
1680
+ /**
1681
+ * Derives delaySeconds and dynamic control mapping for Mitsubishi M50195P digital echo LSI
1682
+ * by tracing the inverter oscillator timing network connected to CLK1.
1683
+ */
1684
+ export function deriveM50195DelayFromNetlist(device, netlist) {
1685
+ let clkNode = null;
1686
+ for (let i = 0; i < device.identity.terminalRoles.length; i++) {
1687
+ const role = foldToken(device.identity.terminalRoles[i] ?? "");
1688
+ if (role === "clk1" || role === "clk" || role === "clock") {
1689
+ clkNode = device.nodes[i] ?? null;
1690
+ break;
1691
+ }
1692
+ }
1693
+ if (clkNode === null || clkNode === GROUND) {
1694
+ return {
1695
+ outcome: "refused",
1696
+ reason: "M50195P CLK1 pin is unconnected or grounded",
1697
+ };
1698
+ }
1699
+ const invNodes = new Set([clkNode]);
1700
+ for (const d of netlist.devices) {
1701
+ if (d.kind === "ic" && d.nodes.includes(clkNode)) {
1702
+ const part = foldToken(d.identity.partNumber ?? "");
1703
+ if (INVERTER_OSCILLATOR_PART_IDS.has(part)) {
1704
+ for (const n of d.nodes) {
1705
+ if (n !== GROUND)
1706
+ invNodes.add(n);
1707
+ }
1708
+ }
1709
+ }
1710
+ }
1711
+ const rCandidates = netlist.devices.filter((d) => TIMING_RESISTANCE_KINDS.has(d.kind) &&
1712
+ d.nodes.some((n) => invNodes.has(n)));
1713
+ const timingDevices = rCandidates.filter((d) => d.nodes.filter((n) => invNodes.has(n)).length >= 2 ||
1714
+ d.nodes.includes(clkNode) ||
1715
+ (d.kind === "potentiometer" && d.nodes.some((n) => invNodes.has(n))));
1716
+ const timingR = resolveTimingResistanceNetwork(timingDevices, netlist, true);
1717
+ if (timingR.rOhms <= 0) {
1718
+ return {
1719
+ outcome: "refused",
1720
+ reason: "Resolved M50195P timing resistance is <= 0 ohms",
1721
+ };
1722
+ }
1723
+ const rMax = timingR.ohmsAtControlMax > 0 ? timingR.ohmsAtControlMax : 780000;
1724
+ const delaySeconds = Math.max(0.03, Math.min(0.4, (timingR.rOhms / rMax) * 0.4));
1725
+ const fClockHz = 1 / (delaySeconds / 2048);
1726
+ return {
1727
+ outcome: "derived",
1728
+ delaySeconds,
1729
+ rOhms: timingR.rOhms,
1730
+ cFarads: 5e-12,
1731
+ fClockHz,
1732
+ family: "unknown",
1733
+ stages: 2048,
1734
+ // **Scaled into the runtime's units, not left as delay-per-ohm.** `clockControl` is
1735
+ // consumed as `delay = offsetSeconds + stages * formulaConstant * R * farads`, so a constant that
1736
+ // already folds in `stages` and `C` gets them applied a second time. This path
1737
+ // declared physical `stages: 2048` and `cFarads: 5e-12` beside a `delaySeconds / R`
1738
+ // constant, which multiplied every delay by 2048 * 5e-12 = 1.024e-8: `ibanez-dl5`
1739
+ // declared 350 ms and rendered about 3 ns, a delay pedal with no delay. The PT2399
1740
+ // path above is correct only because its neutral `stages: 1` / `cFarads: 1` make the
1741
+ // two conventions coincide.
1742
+ formulaConstant: delaySeconds / (2048 * timingR.rOhms * 5e-12),
1743
+ offsetSeconds: 0,
1744
+ controlId: timingR.controlId,
1745
+ taper: timingR.taper,
1746
+ ohmsAtControlMin: timingR.ohmsAtControlMin,
1747
+ ohmsAtControlMax: timingR.ohmsAtControlMax,
1748
+ };
1749
+ }
1750
+ //# sourceMappingURL=bbd-clock.js.map