@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
|
@@ -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
|