@audiofab-io/fv1-core 0.2.2 → 0.4.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/blocks/ATL_DEVELOPER_REFERENCE.md +156 -0
- package/blocks/control/constant.atl +36 -0
- package/blocks/control/entropy_lfo.atl +74 -0
- package/blocks/control/envelope.atl +121 -0
- package/blocks/control/invert.atl +33 -0
- package/blocks/control/pot.atl +150 -0
- package/blocks/control/power.atl +77 -0
- package/blocks/control/ramp_lfo.atl +122 -0
- package/blocks/control/scale_offset.atl +84 -0
- package/blocks/control/sincos_lfo.atl +126 -0
- package/blocks/control/smoother.atl +48 -0
- package/blocks/control/tremolizer.atl +55 -0
- package/blocks/effects/delay/micro_stutter.atl +77 -0
- package/blocks/effects/delay/mn3011.atl +281 -0
- package/blocks/effects/delay/simple_delay.atl +96 -0
- package/blocks/effects/delay/triple_tap_delay.atl +176 -0
- package/blocks/effects/lo-fi/bit_mangler.atl +74 -0
- package/blocks/effects/lo-fi/chiptune.atl +311 -0
- package/blocks/effects/lo-fi/tape_degrade.atl +181 -0
- package/blocks/effects/modulation/chorus.atl +141 -0
- package/blocks/effects/modulation/chorus_4voice.atl +188 -0
- package/blocks/effects/modulation/flanger.atl +184 -0
- package/blocks/effects/modulation/guitar_synth.atl +350 -0
- package/blocks/effects/modulation/harmonic_trem.atl +129 -0
- package/blocks/effects/modulation/organ_synth.atl +326 -0
- package/blocks/effects/modulation/phaser.atl +300 -0
- package/blocks/effects/pitch/octave_up_down.atl +80 -0
- package/blocks/effects/pitch/pitch_offset.atl +149 -0
- package/blocks/effects/pitch/pitch_offset_dual.atl +197 -0
- package/blocks/effects/pitch/pitch_shift.atl +115 -0
- package/blocks/effects/pitch/sub_octave.atl +100 -0
- package/blocks/effects/reverb/ducking_reverb.atl +145 -0
- package/blocks/effects/reverb/min_reverb.atl +132 -0
- package/blocks/effects/reverb/plate_reverb.atl +344 -0
- package/blocks/effects/reverb/room_reverb.atl +293 -0
- package/blocks/effects/reverb/smear.atl +90 -0
- package/blocks/effects/reverb/spring_reverb.atl +353 -0
- package/blocks/filter/1p_high_pass.atl +63 -0
- package/blocks/filter/1p_low_pass.atl +59 -0
- package/blocks/filter/auto_wah.atl +207 -0
- package/blocks/filter/bbd_loss.atl +79 -0
- package/blocks/filter/shelving_high_pass.atl +76 -0
- package/blocks/filter/shelving_low_pass.atl +76 -0
- package/blocks/filter/svf_2p.atl +116 -0
- package/blocks/gain_mix/crossfade.atl +93 -0
- package/blocks/gain_mix/crossfade2.atl +86 -0
- package/blocks/gain_mix/crossfade3.atl +71 -0
- package/blocks/gain_mix/gainboost.atl +54 -0
- package/blocks/gain_mix/mixer2.atl +76 -0
- package/blocks/gain_mix/mixer3.atl +109 -0
- package/blocks/gain_mix/mixer4.atl +152 -0
- package/blocks/gain_mix/volume.atl +51 -0
- package/blocks/io/adc.atl +53 -0
- package/blocks/io/dac.atl +61 -0
- package/blocks/other/stickynote.atl +24 -0
- package/blocks/other/tone_gen_adjustable.atl +137 -0
- package/blocks/other/tone_gen_fixed.atl +109 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts +13 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.js +44 -0
- package/dist/blockDiagram/blocks/BlockDirectoryLoader.js.map +1 -0
- package/dist/blockDiagram/blocks/BlockRegistry.d.ts +48 -0
- package/dist/blockDiagram/blocks/BlockRegistry.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/BlockRegistry.js +109 -0
- package/dist/blockDiagram/blocks/BlockRegistry.js.map +1 -0
- package/dist/blockDiagram/blocks/TemplateBlock.d.ts +20 -0
- package/dist/blockDiagram/blocks/TemplateBlock.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/TemplateBlock.js +82 -0
- package/dist/blockDiagram/blocks/TemplateBlock.js.map +1 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.d.ts +248 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.d.ts.map +1 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.js +402 -0
- package/dist/blockDiagram/blocks/base/BaseBlock.js.map +1 -0
- package/dist/blockDiagram/builtinBlocks.d.ts +9 -0
- package/dist/blockDiagram/builtinBlocks.d.ts.map +1 -0
- package/dist/blockDiagram/builtinBlocks.js +4912 -0
- package/dist/blockDiagram/builtinBlocks.js.map +1 -0
- package/dist/blockDiagram/compiler/BlockTemplate.d.ts +37 -0
- package/dist/blockDiagram/compiler/BlockTemplate.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/BlockTemplate.js +860 -0
- package/dist/blockDiagram/compiler/BlockTemplate.js.map +1 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.d.ts +75 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.js +443 -0
- package/dist/blockDiagram/compiler/CodeOptimizer.js.map +1 -0
- package/dist/blockDiagram/compiler/GraphCompiler.d.ts +63 -0
- package/dist/blockDiagram/compiler/GraphCompiler.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/GraphCompiler.js +656 -0
- package/dist/blockDiagram/compiler/GraphCompiler.js.map +1 -0
- package/dist/blockDiagram/compiler/TopologicalSort.d.ts +63 -0
- package/dist/blockDiagram/compiler/TopologicalSort.d.ts.map +1 -0
- package/dist/blockDiagram/compiler/TopologicalSort.js +268 -0
- package/dist/blockDiagram/compiler/TopologicalSort.js.map +1 -0
- package/dist/blockDiagram/index.d.ts +30 -0
- package/dist/blockDiagram/index.d.ts.map +1 -0
- package/dist/blockDiagram/index.js +29 -0
- package/dist/blockDiagram/index.js.map +1 -0
- package/dist/blockDiagram/types/Block.d.ts +178 -0
- package/dist/blockDiagram/types/Block.d.ts.map +1 -0
- package/dist/blockDiagram/types/Block.js +5 -0
- package/dist/blockDiagram/types/Block.js.map +1 -0
- package/dist/blockDiagram/types/CodeGenContext.d.ts +235 -0
- package/dist/blockDiagram/types/CodeGenContext.d.ts.map +1 -0
- package/dist/blockDiagram/types/CodeGenContext.js +554 -0
- package/dist/blockDiagram/types/CodeGenContext.js.map +1 -0
- package/dist/blockDiagram/types/Connection.d.ts +17 -0
- package/dist/blockDiagram/types/Connection.d.ts.map +1 -0
- package/dist/blockDiagram/types/Connection.js +5 -0
- package/dist/blockDiagram/types/Connection.js.map +1 -0
- package/dist/blockDiagram/types/Graph.d.ts +28 -0
- package/dist/blockDiagram/types/Graph.d.ts.map +1 -0
- package/dist/blockDiagram/types/Graph.js +24 -0
- package/dist/blockDiagram/types/Graph.js.map +1 -0
- package/dist/blockDiagram/types/IR.d.ts +79 -0
- package/dist/blockDiagram/types/IR.d.ts.map +1 -0
- package/dist/blockDiagram/types/IR.js +6 -0
- package/dist/blockDiagram/types/IR.js.map +1 -0
- package/dist/blockDiagram/utils/SpinCADConverter.d.ts +17 -0
- package/dist/blockDiagram/utils/SpinCADConverter.d.ts.map +1 -0
- package/dist/blockDiagram/utils/SpinCADConverter.js +307 -0
- package/dist/blockDiagram/utils/SpinCADConverter.js.map +1 -0
- package/dist/effect/compileEffect.d.ts +51 -0
- package/dist/effect/compileEffect.d.ts.map +1 -0
- package/dist/effect/compileEffect.js +133 -0
- package/dist/effect/compileEffect.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/simulator/FV1Simulator.d.ts.map +1 -1
- package/dist/simulator/FV1Simulator.js +7 -4
- package/dist/simulator/FV1Simulator.js.map +1 -1
- package/package.json +17 -5
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BlockTemplate Engine
|
|
3
|
+
* Processes declarative block definitions and emits IR nodes.
|
|
4
|
+
*/
|
|
5
|
+
import { AlgebraicCompiler } from '../../assembler/AlgebraicCompiler.js';
|
|
6
|
+
export class BlockTemplate {
|
|
7
|
+
constructor(definition) {
|
|
8
|
+
this.definition = definition;
|
|
9
|
+
this.algebraicCompiler = new AlgebraicCompiler();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Generate IR nodes for a specific block instance
|
|
13
|
+
*/
|
|
14
|
+
generateIR(block, ctx) {
|
|
15
|
+
const ir = [];
|
|
16
|
+
const params = this.evaluateParameters(block, ctx);
|
|
17
|
+
const inputs = this.resolveInputs(block, ctx);
|
|
18
|
+
const outputs = this.resolveOutputs(block, ctx);
|
|
19
|
+
const internalRegs = this.resolveInternalRegisters(block, ctx);
|
|
20
|
+
const templateLines = this.definition.template.split('\n');
|
|
21
|
+
// Pre-process lines to remove empty lines and trim
|
|
22
|
+
const processedLines = templateLines.map(line => line.trim()).filter(line => line !== '');
|
|
23
|
+
let currentSection = 'main';
|
|
24
|
+
// This will be reset later
|
|
25
|
+
// Initial pass: find section boundaries and parse EQU declarations
|
|
26
|
+
for (const line of processedLines) {
|
|
27
|
+
if (line.startsWith('@section')) {
|
|
28
|
+
currentSection = line.substring(8).trim();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (currentSection === 'header') {
|
|
32
|
+
const parts = line.split(/[,\s\t]+/);
|
|
33
|
+
if (parts[0].toLowerCase() === 'equ') {
|
|
34
|
+
let name = parts[1];
|
|
35
|
+
const value = parts.slice(2).join(' ');
|
|
36
|
+
if (name && value) {
|
|
37
|
+
// If name is a token, resolve it (e.g. ${local.X} -> blockId_X)
|
|
38
|
+
if (name.startsWith('${') && name.endsWith('}')) {
|
|
39
|
+
const key = name.substring(2, name.length - 1);
|
|
40
|
+
if (key.startsWith('local.')) {
|
|
41
|
+
name = `${ctx.getShortId(block.id)}_${key.split('.')[1]}`;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
name = key;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
ctx.setVariable(name, value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (line.startsWith('@')) {
|
|
51
|
+
// Handle core macros that might define variables needed for memory sizing.
|
|
52
|
+
// Only process calculation/variable macros in the pre-pass.
|
|
53
|
+
const macro = line.substring(1).split(/[,\s]+/)[0].toLowerCase();
|
|
54
|
+
const calculationMacros = [
|
|
55
|
+
'equals', 'multiplydouble', 'multiplyint', 'dividedouble',
|
|
56
|
+
'plusdouble', 'minusdouble', 'equalsbool'
|
|
57
|
+
];
|
|
58
|
+
if (calculationMacros.includes(macro)) {
|
|
59
|
+
// Pass a dummy IR array to avoid duplicate instructions during the pre-pass.
|
|
60
|
+
this.expandMacro(line, currentSection, [], ctx, block);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Memory must be resolved AFTER the header pre-pass to allow constants/EQU/equals
|
|
66
|
+
// defined in the header to be used for memory sizes.
|
|
67
|
+
const memory = this.resolveMemoryObjects(block, ctx);
|
|
68
|
+
// Reset for main pass
|
|
69
|
+
currentSection = 'main';
|
|
70
|
+
const sectionStack = [];
|
|
71
|
+
for (let line of processedLines) {
|
|
72
|
+
// line is already trimmed and filtered, so no need for:
|
|
73
|
+
// line = line.trim();
|
|
74
|
+
// if (!line || line.startsWith(';')) continue;
|
|
75
|
+
// Handle comments
|
|
76
|
+
if (line.startsWith(';') || line.startsWith('//')) {
|
|
77
|
+
if (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].skip)
|
|
78
|
+
continue;
|
|
79
|
+
// Normalize comment character to ; and strip it for the IR node
|
|
80
|
+
const commentText = line.startsWith('//') ? line.substring(2) : line.substring(1);
|
|
81
|
+
// Still process substitutions in comments
|
|
82
|
+
const resolvedComment = this.performSubstitutions(commentText.trim(), params, inputs, outputs, internalRegs, memory, block, ctx);
|
|
83
|
+
ir.push({ op: ';', args: [resolvedComment], section: currentSection });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Handle section directives
|
|
87
|
+
if (line.startsWith('@section')) {
|
|
88
|
+
const section = line.split(' ')[1];
|
|
89
|
+
if (['header', 'init', 'input', 'main', 'output'].includes(section)) {
|
|
90
|
+
currentSection = section;
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Handle @if/@else/@endif
|
|
95
|
+
if (line.startsWith('@if')) {
|
|
96
|
+
const condition = this.evaluateCondition(line.substring(3).trim(), block, ctx);
|
|
97
|
+
const parentSkip = sectionStack.length > 0 ? sectionStack[sectionStack.length - 1].skip : false;
|
|
98
|
+
sectionStack.push({
|
|
99
|
+
condition,
|
|
100
|
+
skip: parentSkip || !condition,
|
|
101
|
+
hasElse: false
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.startsWith('@else')) {
|
|
106
|
+
if (sectionStack.length > 0) {
|
|
107
|
+
const top = sectionStack[sectionStack.length - 1];
|
|
108
|
+
const parentSkip = sectionStack.length > 1 ? sectionStack[sectionStack.length - 2].skip : false;
|
|
109
|
+
top.skip = parentSkip || top.condition; // Skip else if condition was true
|
|
110
|
+
top.hasElse = true;
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (line.startsWith('@endif')) {
|
|
115
|
+
sectionStack.pop();
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Skip if in a false conditional branch
|
|
119
|
+
if (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].skip) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Handle @assert directives
|
|
123
|
+
if (line.startsWith('@assert')) {
|
|
124
|
+
const assertContent = line.substring(7).trim();
|
|
125
|
+
const commaIdx = assertContent.indexOf(',');
|
|
126
|
+
if (commaIdx !== -1) {
|
|
127
|
+
let conditionStr = assertContent.substring(0, commaIdx).trim();
|
|
128
|
+
let messageStr = assertContent.substring(commaIdx + 1).trim();
|
|
129
|
+
// Resolve ${...} tokens in both the condition and message
|
|
130
|
+
conditionStr = this.performSubstitutions(conditionStr, params, inputs, outputs, internalRegs, memory, block, ctx);
|
|
131
|
+
messageStr = this.performSubstitutions(messageStr, params, inputs, outputs, internalRegs, memory, block, ctx);
|
|
132
|
+
if (messageStr.startsWith('"') && messageStr.endsWith('"')) {
|
|
133
|
+
messageStr = messageStr.substring(1, messageStr.length - 1);
|
|
134
|
+
}
|
|
135
|
+
else if (messageStr.startsWith("'") && messageStr.endsWith("'")) {
|
|
136
|
+
messageStr = messageStr.substring(1, messageStr.length - 1);
|
|
137
|
+
}
|
|
138
|
+
if (!this.evaluateCondition(conditionStr, block, ctx)) {
|
|
139
|
+
ctx.addError(messageStr);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
ctx.addError(`Invalid @assert syntax. Expected: @assert condition, "message". Got: ${line}`);
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const trimmed = line.trim().toLowerCase();
|
|
148
|
+
const isEqu = trimmed.startsWith('equ');
|
|
149
|
+
const firstTokenIndex = line.indexOf('${');
|
|
150
|
+
const processedLine = this.performSubstitutions(line, params, inputs, outputs, internalRegs, memory, block, ctx);
|
|
151
|
+
// Split into code and comment (support both ; and //)
|
|
152
|
+
let commentIndex = processedLine.indexOf(';');
|
|
153
|
+
const doubleSlashIndex = processedLine.indexOf('//');
|
|
154
|
+
let commentMarkerLen = 1;
|
|
155
|
+
if (doubleSlashIndex !== -1 && (commentIndex === -1 || doubleSlashIndex < commentIndex)) {
|
|
156
|
+
commentIndex = doubleSlashIndex;
|
|
157
|
+
commentMarkerLen = 2;
|
|
158
|
+
}
|
|
159
|
+
let codePart = processedLine;
|
|
160
|
+
let comment = undefined;
|
|
161
|
+
if (commentIndex !== -1) {
|
|
162
|
+
codePart = processedLine.substring(0, commentIndex).trim();
|
|
163
|
+
comment = processedLine.substring(commentIndex + commentMarkerLen).trim();
|
|
164
|
+
}
|
|
165
|
+
// Try compiling as algebraic statement first
|
|
166
|
+
if (codePart.length > 0 && (codePart.includes('=') || codePart.includes('@acc') || codePart.includes('acc'))) {
|
|
167
|
+
const isMemoryCheck = (id) => {
|
|
168
|
+
// Check if identifier is in memory objects or refers to delay RAM
|
|
169
|
+
if (memory[id] !== undefined)
|
|
170
|
+
return true;
|
|
171
|
+
if (id.startsWith('mem.') || id.startsWith('MEM'))
|
|
172
|
+
return true;
|
|
173
|
+
if (id.toLowerCase() === 'delayl' || id.toLowerCase() === 'delayr')
|
|
174
|
+
return true;
|
|
175
|
+
return false;
|
|
176
|
+
};
|
|
177
|
+
const compiled = this.algebraicCompiler.compileLine(codePart, isMemoryCheck, (msg) => ctx.addError(msg));
|
|
178
|
+
if (compiled) {
|
|
179
|
+
codePart = compiled; // Replace codePart with the standard FV-1 assembly
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Handle Standard Macros
|
|
183
|
+
if (codePart.startsWith('@')) {
|
|
184
|
+
this.expandMacro(codePart, currentSection, ir, ctx, block);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Parse assembly-like line to IR
|
|
188
|
+
const firstSpaceIndex = codePart.indexOf(' ');
|
|
189
|
+
const firstTabIndex = codePart.indexOf('\t');
|
|
190
|
+
let splitIndex = -1;
|
|
191
|
+
if (firstSpaceIndex !== -1 && firstTabIndex !== -1)
|
|
192
|
+
splitIndex = Math.min(firstSpaceIndex, firstTabIndex);
|
|
193
|
+
else if (firstSpaceIndex !== -1)
|
|
194
|
+
splitIndex = firstSpaceIndex;
|
|
195
|
+
else if (firstTabIndex !== -1)
|
|
196
|
+
splitIndex = firstTabIndex;
|
|
197
|
+
if (splitIndex !== -1) {
|
|
198
|
+
const op = codePart.substring(0, splitIndex).trim().toUpperCase();
|
|
199
|
+
const argsPart = codePart.substring(splitIndex).trim();
|
|
200
|
+
// Split by comma, but ignore commas inside parentheses
|
|
201
|
+
const args = [];
|
|
202
|
+
let currentArg = '';
|
|
203
|
+
let parenLevel = 0;
|
|
204
|
+
for (let i = 0; i < argsPart.length; i++) {
|
|
205
|
+
const char = argsPart[i];
|
|
206
|
+
if (char === '(')
|
|
207
|
+
parenLevel++;
|
|
208
|
+
else if (char === ')')
|
|
209
|
+
parenLevel--;
|
|
210
|
+
else if (char === ',' && parenLevel === 0) {
|
|
211
|
+
args.push(currentArg.trim());
|
|
212
|
+
currentArg = '';
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
currentArg += char;
|
|
216
|
+
}
|
|
217
|
+
if (currentArg.trim().length > 0)
|
|
218
|
+
args.push(currentArg.trim());
|
|
219
|
+
ir.push({
|
|
220
|
+
op,
|
|
221
|
+
args,
|
|
222
|
+
section: currentSection,
|
|
223
|
+
comment
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (codePart.length > 0) {
|
|
227
|
+
// Opcode only
|
|
228
|
+
ir.push({
|
|
229
|
+
op: codePart.toUpperCase(),
|
|
230
|
+
args: [],
|
|
231
|
+
section: currentSection,
|
|
232
|
+
comment
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
else if (comment) {
|
|
236
|
+
// Just a comment line
|
|
237
|
+
ir.push({ op: ';', args: [comment], section: currentSection });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return ir;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Resolve a label template for the UI
|
|
244
|
+
*/
|
|
245
|
+
resolveLabel(parameters, ctx, blockId) {
|
|
246
|
+
const template = this.definition.labelTemplate;
|
|
247
|
+
if (template === undefined)
|
|
248
|
+
return null;
|
|
249
|
+
const params = this.evaluateParametersFromMap(parameters, ctx, true);
|
|
250
|
+
// Prepare evaluation context
|
|
251
|
+
const evalCtx = {
|
|
252
|
+
param: params,
|
|
253
|
+
inputConnected: {}
|
|
254
|
+
};
|
|
255
|
+
if (ctx && blockId) {
|
|
256
|
+
for (const input of this.definition.inputs) {
|
|
257
|
+
// If we have a context, we can check if the input is connected
|
|
258
|
+
try {
|
|
259
|
+
evalCtx.inputConnected[input.id] = ctx.getInputRegister(blockId, input.id) !== undefined;
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
evalCtx.inputConnected[input.id] = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return template.replace(/\$\{([^}]+)\}/g, (match, expr) => {
|
|
267
|
+
try {
|
|
268
|
+
// Safely-ish evaluate the expression using the provided context
|
|
269
|
+
const f = new Function('param', 'inputConnected', `return (${expr});`);
|
|
270
|
+
const result = f(evalCtx.param, evalCtx.inputConnected);
|
|
271
|
+
return result !== undefined && result !== null ? result.toString() : '';
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
console.warn(`Failed to evaluate label expression: ${expr}`, e);
|
|
275
|
+
return match;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
resolveValue(v, block, ctx) {
|
|
280
|
+
if (v === 'true')
|
|
281
|
+
return true;
|
|
282
|
+
if (v === 'false')
|
|
283
|
+
return false;
|
|
284
|
+
// 1. Check parameters
|
|
285
|
+
const params = this.evaluateParameters(block, ctx);
|
|
286
|
+
if (params[v] !== undefined)
|
|
287
|
+
return params[v];
|
|
288
|
+
// 2. Check local variables (namespaced by block short ID)
|
|
289
|
+
const shortId = ctx.getShortId(block.id);
|
|
290
|
+
const localV = ctx.getVariable(`${shortId}_${v}`);
|
|
291
|
+
if (localV !== undefined) {
|
|
292
|
+
const f = parseFloat(localV);
|
|
293
|
+
return isNaN(f) ? localV : f;
|
|
294
|
+
}
|
|
295
|
+
// 3. Check direct context variables
|
|
296
|
+
const directV = ctx.getVariable(v);
|
|
297
|
+
if (directV !== undefined) {
|
|
298
|
+
const f = parseFloat(directV);
|
|
299
|
+
return isNaN(f) ? directV : f;
|
|
300
|
+
}
|
|
301
|
+
// 4. Try as decimal
|
|
302
|
+
const f = parseFloat(v);
|
|
303
|
+
if (!isNaN(f))
|
|
304
|
+
return f;
|
|
305
|
+
return v;
|
|
306
|
+
}
|
|
307
|
+
evaluateCondition(condition, block, ctx) {
|
|
308
|
+
// Handle OR clauses (split before variable resolution to support
|
|
309
|
+
// pinConnected() and SpinCAD macros on either side)
|
|
310
|
+
const orClauses = condition.split('||');
|
|
311
|
+
if (orClauses.length > 1) {
|
|
312
|
+
return orClauses.some(clause => this.evaluateCondition(clause.trim(), block, ctx));
|
|
313
|
+
}
|
|
314
|
+
// Handle AND clauses
|
|
315
|
+
const andClauses = condition.split('&&');
|
|
316
|
+
if (andClauses.length > 1) {
|
|
317
|
+
return andClauses.every(clause => this.evaluateCondition(clause.trim(), block, ctx));
|
|
318
|
+
}
|
|
319
|
+
// Handle pinConnected(portId)
|
|
320
|
+
const pinMatch = condition.match(/pinConnected\(([^)]+)\)/);
|
|
321
|
+
if (pinMatch) {
|
|
322
|
+
let rawPinName = pinMatch[1].trim();
|
|
323
|
+
const wrapperMatch = rawPinName.match(/\$\{(?:input|output)\.([^}]+)\}/);
|
|
324
|
+
if (wrapperMatch) {
|
|
325
|
+
rawPinName = wrapperMatch[1];
|
|
326
|
+
}
|
|
327
|
+
let pinId = rawPinName;
|
|
328
|
+
const targetInput = this.definition.inputs.find(i => i.id === rawPinName || i.name === rawPinName);
|
|
329
|
+
if (targetInput)
|
|
330
|
+
pinId = targetInput.id;
|
|
331
|
+
else {
|
|
332
|
+
const targetOutput = this.definition.outputs.find(o => o.id === rawPinName || o.name === rawPinName);
|
|
333
|
+
if (targetOutput)
|
|
334
|
+
pinId = targetOutput.id;
|
|
335
|
+
}
|
|
336
|
+
return ctx.getInputRegister(block.id, pinId) !== null ||
|
|
337
|
+
ctx.isOutputConnected(block.id, pinId);
|
|
338
|
+
}
|
|
339
|
+
// Handle legacy SpinCAD macros used as conditions
|
|
340
|
+
const parts = condition.split(/\s+/);
|
|
341
|
+
const macro = parts[0].toLowerCase();
|
|
342
|
+
if (macro === 'isgreaterthan') {
|
|
343
|
+
return this.resolveValue(parts[1], block, ctx) > this.resolveValue(parts[2], block, ctx);
|
|
344
|
+
}
|
|
345
|
+
if (macro === 'isequalto') {
|
|
346
|
+
return this.resolveValue(parts[1], block, ctx) === this.resolveValue(parts[2], block, ctx);
|
|
347
|
+
}
|
|
348
|
+
if (macro === 'isor') {
|
|
349
|
+
const v1 = this.resolveValue(parts[1], block, ctx);
|
|
350
|
+
const v2 = this.resolveValue(parts[2], block, ctx);
|
|
351
|
+
const expected = this.resolveValue(parts[3], block, ctx);
|
|
352
|
+
return v1 === expected || v2 === expected;
|
|
353
|
+
}
|
|
354
|
+
// General expression evaluation:
|
|
355
|
+
// Resolve all variable references, then evaluate the resulting expression.
|
|
356
|
+
// This handles mixed variable types, arithmetic expressions, and comparisons
|
|
357
|
+
// like: ${param.depth} > width, (a * 2) > (b / 2), inRange > 0
|
|
358
|
+
const resolved = this.resolveConditionExpression(condition, block, ctx);
|
|
359
|
+
try {
|
|
360
|
+
return !!new Function(`return (${resolved});`)();
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Resolve all variable references in a condition expression to their numeric/string values.
|
|
368
|
+
* Handles ${param.X}, ${local.X}, param.X, and bare identifier lookups.
|
|
369
|
+
* The result is a plain JavaScript expression string ready for evaluation.
|
|
370
|
+
*/
|
|
371
|
+
resolveConditionExpression(expr, block, ctx) {
|
|
372
|
+
const params = this.evaluateParameters(block, ctx);
|
|
373
|
+
// 1. Replace ${param.X.max}, ${param.X.min} property access tokens
|
|
374
|
+
expr = expr.replace(/\$\{(?:param\.)?([^.}]+)\.(max|min)\}/g, (_, paramId, prop) => {
|
|
375
|
+
const paramDef = this.definition.parameters.find(p => p.id === paramId);
|
|
376
|
+
if (paramDef) {
|
|
377
|
+
let val = prop === 'max' ? paramDef.max : paramDef.min;
|
|
378
|
+
if (val !== undefined) {
|
|
379
|
+
if (paramDef.conversion && ctx) {
|
|
380
|
+
val = this.applyConversion(paramDef.conversion, val, ctx);
|
|
381
|
+
}
|
|
382
|
+
return String(val);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return '0';
|
|
386
|
+
});
|
|
387
|
+
// 2. Replace ${param.X} tokens (using evaluated params with conversions applied)
|
|
388
|
+
expr = expr.replace(/\$\{param\.([^}]+)\}/g, (_, paramName) => {
|
|
389
|
+
const val = params[paramName];
|
|
390
|
+
if (val === undefined)
|
|
391
|
+
return '0';
|
|
392
|
+
if (typeof val === 'number' || typeof val === 'boolean')
|
|
393
|
+
return String(val);
|
|
394
|
+
return JSON.stringify(String(val));
|
|
395
|
+
});
|
|
396
|
+
// 3. Replace ${local.X} tokens
|
|
397
|
+
expr = expr.replace(/\$\{local\.([^}]+)\}/g, (_, varName) => {
|
|
398
|
+
const shortId = ctx.getShortId(block.id);
|
|
399
|
+
const val = ctx.getVariable(`${shortId}_${varName}`);
|
|
400
|
+
if (val === undefined)
|
|
401
|
+
return '0';
|
|
402
|
+
const num = parseFloat(val);
|
|
403
|
+
return isNaN(num) ? JSON.stringify(val) : val;
|
|
404
|
+
});
|
|
405
|
+
// Helper: emit a resolved value as valid JS (numbers/booleans unquoted, strings quoted)
|
|
406
|
+
const toJS = (val) => {
|
|
407
|
+
if (typeof val === 'number' || typeof val === 'boolean')
|
|
408
|
+
return String(val);
|
|
409
|
+
return JSON.stringify(String(val));
|
|
410
|
+
};
|
|
411
|
+
// 4. Replace remaining ${X} tokens
|
|
412
|
+
expr = expr.replace(/\$\{([^}]+)\}/g, (match, key) => {
|
|
413
|
+
const val = this.resolveValue(key, block, ctx);
|
|
414
|
+
if (val === key)
|
|
415
|
+
return match; // unresolved
|
|
416
|
+
return toJS(val);
|
|
417
|
+
});
|
|
418
|
+
// 5. Replace bare identifiers (param names, context variables, param.X dotted refs)
|
|
419
|
+
// Match word characters and dots, but not numbers at the start, and not inside quotes.
|
|
420
|
+
expr = expr.replace(/\b([a-zA-Z_][a-zA-Z0-9_.]*)\b/g, (match) => {
|
|
421
|
+
// Skip JS literals
|
|
422
|
+
if (['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'].includes(match))
|
|
423
|
+
return match;
|
|
424
|
+
// Try param.X dotted syntax
|
|
425
|
+
if (match.startsWith('param.')) {
|
|
426
|
+
const paramName = match.substring(6);
|
|
427
|
+
// Check for .max/.min suffix
|
|
428
|
+
const propMatch = paramName.match(/^(.+)\.(max|min)$/);
|
|
429
|
+
if (propMatch) {
|
|
430
|
+
const paramDef = this.definition.parameters.find(p => p.id === propMatch[1]);
|
|
431
|
+
if (paramDef) {
|
|
432
|
+
let val = propMatch[2] === 'max' ? paramDef.max : paramDef.min;
|
|
433
|
+
if (val !== undefined) {
|
|
434
|
+
if (paramDef.conversion && ctx) {
|
|
435
|
+
val = this.applyConversion(paramDef.conversion, val, ctx);
|
|
436
|
+
}
|
|
437
|
+
return String(val);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const val = params[paramName];
|
|
442
|
+
if (val !== undefined)
|
|
443
|
+
return toJS(val);
|
|
444
|
+
}
|
|
445
|
+
// Try resolveValue (checks params, local vars, context vars, numeric literal)
|
|
446
|
+
const val = this.resolveValue(match, block, ctx);
|
|
447
|
+
if (val !== match)
|
|
448
|
+
return toJS(val);
|
|
449
|
+
return match;
|
|
450
|
+
});
|
|
451
|
+
return expr;
|
|
452
|
+
}
|
|
453
|
+
expandMacro(line, section, ir, ctx, block) {
|
|
454
|
+
const parts = line.substring(1).split(/[,\s]+/).filter(p => p.length > 0);
|
|
455
|
+
const macro = parts[0].toLowerCase();
|
|
456
|
+
const args = parts.slice(1);
|
|
457
|
+
switch (macro) {
|
|
458
|
+
case 'equals': {
|
|
459
|
+
const eqName = args[0];
|
|
460
|
+
const eqNameIndex = line.indexOf(eqName);
|
|
461
|
+
let expr = args[1];
|
|
462
|
+
if (eqNameIndex !== -1) {
|
|
463
|
+
expr = line.substring(eqNameIndex + eqName.length).trim();
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
expr = args.slice(1).join(' ');
|
|
467
|
+
}
|
|
468
|
+
const eqValue = this.resolveValue(expr, block, ctx);
|
|
469
|
+
ctx.setVariable(eqName, eqValue.toString());
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
472
|
+
case 'multiplydouble':
|
|
473
|
+
case 'multiplyint': {
|
|
474
|
+
const mulA = this.resolveValue(args[1], block, ctx);
|
|
475
|
+
const mulB = this.resolveValue(args[2], block, ctx);
|
|
476
|
+
const mulVal = (typeof mulA === 'number' ? mulA : 0) * (typeof mulB === 'number' ? mulB : 0);
|
|
477
|
+
ctx.setVariable(args[0], mulVal.toString());
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
case 'dividedouble': {
|
|
481
|
+
const divA = this.resolveValue(args[1], block, ctx);
|
|
482
|
+
const divB = this.resolveValue(args[2], block, ctx);
|
|
483
|
+
const divVal = (typeof divA === 'number' ? divA : 0) / (typeof divB === 'number' ? divB : 1);
|
|
484
|
+
ctx.setVariable(args[0], divVal.toString());
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
case 'plusdouble': {
|
|
488
|
+
const pA = this.resolveValue(args[1], block, ctx);
|
|
489
|
+
const pB = this.resolveValue(args[2], block, ctx);
|
|
490
|
+
const pVal = (typeof pA === 'number' ? pA : 0) + (typeof pB === 'number' ? pB : 0);
|
|
491
|
+
ctx.setVariable(args[0], pVal.toString());
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
case 'minusdouble': {
|
|
495
|
+
const mA = this.resolveValue(args[1], block, ctx);
|
|
496
|
+
const mB = this.resolveValue(args[2], block, ctx);
|
|
497
|
+
const mVal = (typeof mA === 'number' ? mA : 0) - (typeof mB === 'number' ? mB : 0);
|
|
498
|
+
ctx.setVariable(args[0], mVal.toString());
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
case 'isgreaterthan':
|
|
502
|
+
// @isGreaterThan a, b
|
|
503
|
+
if (parseFloat(args[0]) > parseFloat(args[1])) {
|
|
504
|
+
// This is tricky because @isGreaterThan is usually followed by @endif
|
|
505
|
+
// but it's handled by the parent line-by-line processor for @if.
|
|
506
|
+
// Actually, SpinCAD uses it like an @if.
|
|
507
|
+
// We should probably have handled this in generateIR's loop.
|
|
508
|
+
}
|
|
509
|
+
break;
|
|
510
|
+
case 'isequaltobool':
|
|
511
|
+
case 'equalsbool':
|
|
512
|
+
// @equalsBool var, val
|
|
513
|
+
ctx.setVariable(args[0], args[1]);
|
|
514
|
+
break;
|
|
515
|
+
case 'cv': {
|
|
516
|
+
// @cv portId
|
|
517
|
+
// Reads a control input, using the port's associated parameter as the range/default.
|
|
518
|
+
// - Unconnected: SOF 0.0, equName → ACC = paramValue (constant)
|
|
519
|
+
// - Connected: CLR; SOF 0.0, equName; MULX reg → ACC ∈ [0..paramValue]
|
|
520
|
+
// (CLR is omitted if the previous instruction already set ACC = 0)
|
|
521
|
+
// - Connected + zero-bypassed: 5-instruction bypass adapted for range scaling.
|
|
522
|
+
// RDAX reads pot scaled by paramValue directly (no separate MULX needed).
|
|
523
|
+
// When pot < threshold: loads zeroValScaled (pot's zeroValue * paramValue).
|
|
524
|
+
const portId = args[0];
|
|
525
|
+
if (!portId) {
|
|
526
|
+
ctx.addError(`@cv requires a port ID argument`);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
// Get the associated parameter from the input definition
|
|
530
|
+
const inputDef = this.definition.inputs.find(i => i.id === portId);
|
|
531
|
+
const paramId = inputDef?.parameter;
|
|
532
|
+
if (!paramId) {
|
|
533
|
+
ctx.addError(`@cv: input port '${portId}' has no 'parameter' field defined in the block JSON`);
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
// Evaluate parameters (with conversions applied)
|
|
537
|
+
const cvParams = this.evaluateParameters(block, ctx);
|
|
538
|
+
const paramValue = cvParams[paramId];
|
|
539
|
+
if (paramValue === undefined || paramValue === null) {
|
|
540
|
+
ctx.addError(`@cv: parameter '${paramId}' not found on block`);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
// EQU name for the range constant
|
|
544
|
+
const shortId = ctx.getShortId(block.id);
|
|
545
|
+
const equName = `cv_${shortId}_${portId}`;
|
|
546
|
+
// Push EQU to IR header section only (idempotent), NOT to equDeclarations.
|
|
547
|
+
// Using registerHeaderEqu avoids duplicating the EQU in the init/Constants block.
|
|
548
|
+
if (!ctx.hasHeaderEqu(equName)) {
|
|
549
|
+
ctx.pushIR({ op: 'EQU', args: [equName, paramValue.toString()], section: 'header' });
|
|
550
|
+
ctx.registerHeaderEqu(equName);
|
|
551
|
+
}
|
|
552
|
+
const reg = ctx.getInputRegister(block.id, portId);
|
|
553
|
+
const isZeroBypassed = ctx.isInputZeroBypassed(block.id, portId);
|
|
554
|
+
if (reg) {
|
|
555
|
+
if (isZeroBypassed) {
|
|
556
|
+
const zeroVal = ctx.getInputZeroValue(block.id, portId);
|
|
557
|
+
ir.push({ op: 'RDAX', args: [reg, '1.0'], section, comment: `Read pot` });
|
|
558
|
+
ir.push({ op: 'SOF', args: ['1.0', '-0.02'], section, comment: 'Subtract bypass threshold' });
|
|
559
|
+
ir.push({ op: 'SKP', args: ['GEZ', '1'], section, comment: 'Skip if pot >= threshold' });
|
|
560
|
+
ir.push({ op: 'SOF', args: ['0.0', `${(zeroVal - 0.02).toFixed(6)}`], section, comment: `Zero fallback` });
|
|
561
|
+
ir.push({ op: 'SOF', args: ['1.0', '0.02'], section, comment: 'Re-add threshold' });
|
|
562
|
+
ir.push({ op: 'SOF', args: [equName, '0.0'], section, comment: `Scale CV by ${paramId}` });
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Standard connected: CLR (if needed), load range, scale by pot.
|
|
566
|
+
// Check if ACC is already 0 from the previous instruction so we can skip the CLR.
|
|
567
|
+
const prevNode = [...ir].reverse().find(n => n.op !== ';');
|
|
568
|
+
const prevClearsAcc = prevNode && (prevNode.op === 'CLR' ||
|
|
569
|
+
(prevNode.op === 'WRAX' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)) ||
|
|
570
|
+
(prevNode.op === 'WRA' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)) ||
|
|
571
|
+
(prevNode.op === 'WRHX' && (prevNode.args[1] === '0.0' || prevNode.args[1] === '0' || parseFloat(prevNode.args[1]) === 0)));
|
|
572
|
+
if (!prevClearsAcc) {
|
|
573
|
+
ir.push({ op: 'CLR', args: [], section });
|
|
574
|
+
}
|
|
575
|
+
ir.push({ op: 'SOF', args: ['0.0', equName], section, comment: `CV range: ${paramId}` });
|
|
576
|
+
ir.push({ op: 'MULX', args: [reg], section });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
// Unconnected: use parameter value as a constant
|
|
581
|
+
ir.push({ op: 'SOF', args: ['0.0', equName], section, comment: `CV default: ${paramId}` });
|
|
582
|
+
}
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case 'mulcv': {
|
|
586
|
+
// @mulcv portId
|
|
587
|
+
// Scale the current ACC by the CV value without clearing first.
|
|
588
|
+
// Assumes ACC already contains a signal to be modulated.
|
|
589
|
+
// - Unconnected: SOF paramValue, 0.0 → ACC = ACC * paramValue (1 instruction)
|
|
590
|
+
// - Connected: SOF paramValue, 0.0; MULX reg → ACC = ACC * paramValue * pot (2 instructions)
|
|
591
|
+
// - Connected + zero-bypassed: same bypass pattern as @cv but applied to existing ACC
|
|
592
|
+
const mulcvPortId = args[0];
|
|
593
|
+
if (!mulcvPortId) {
|
|
594
|
+
ctx.addError(`@mulcv requires a port ID argument`);
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
const mulcvInputDef = this.definition.inputs.find(i => i.id === mulcvPortId);
|
|
598
|
+
const mulcvParamId = mulcvInputDef?.parameter;
|
|
599
|
+
if (!mulcvParamId) {
|
|
600
|
+
ctx.addError(`@mulcv: input port '${mulcvPortId}' has no 'parameter' field defined in the block JSON`);
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
const mulcvParams = this.evaluateParameters(block, ctx);
|
|
604
|
+
const mulcvParamValue = mulcvParams[mulcvParamId];
|
|
605
|
+
if (mulcvParamValue === undefined || mulcvParamValue === null) {
|
|
606
|
+
ctx.addError(`@mulcv: parameter '${mulcvParamId}' not found on block`);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
const mulcvShortId = ctx.getShortId(block.id);
|
|
610
|
+
const mulcvEquName = `cv_${mulcvShortId}_${mulcvPortId}`;
|
|
611
|
+
// Shared EQU registration with @cv (idempotent)
|
|
612
|
+
if (!ctx.hasHeaderEqu(mulcvEquName)) {
|
|
613
|
+
ctx.pushIR({ op: 'EQU', args: [mulcvEquName, mulcvParamValue.toString()], section: 'header' });
|
|
614
|
+
ctx.registerHeaderEqu(mulcvEquName);
|
|
615
|
+
}
|
|
616
|
+
const mulcvReg = ctx.getInputRegister(block.id, mulcvPortId);
|
|
617
|
+
const mulcvZeroBypassed = ctx.isInputZeroBypassed(block.id, mulcvPortId);
|
|
618
|
+
if (mulcvReg) {
|
|
619
|
+
if (mulcvZeroBypassed) {
|
|
620
|
+
const scratch = ctx.getScratchRegister();
|
|
621
|
+
const mulcvZeroVal = ctx.getInputZeroValue(block.id, mulcvPortId);
|
|
622
|
+
ir.push({ op: 'WRAX', args: [scratch, '0.0'], section, comment: 'Save ACC for @mulcv bypass' });
|
|
623
|
+
ir.push({ op: 'RDAX', args: [mulcvReg, '1.0'], section, comment: `Read pot` });
|
|
624
|
+
ir.push({ op: 'SOF', args: ['1.0', '-0.02'], section, comment: 'Subtract bypass threshold' });
|
|
625
|
+
ir.push({ op: 'SKP', args: ['GEZ', '1'], section, comment: 'Skip if pot >= threshold' });
|
|
626
|
+
ir.push({ op: 'SOF', args: ['0.0', `${(mulcvZeroVal - 0.02).toFixed(6)}`], section, comment: `Zero fallback` });
|
|
627
|
+
ir.push({ op: 'SOF', args: ['1.0', '0.02'], section, comment: 'Re-add threshold' });
|
|
628
|
+
ir.push({ op: 'MULX', args: [scratch], section, comment: 'Scale saved signal by CV' });
|
|
629
|
+
ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `Scale by ${mulcvParamId}` });
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
// Connected: scale ACC by paramValue, then scale by pot
|
|
633
|
+
ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `@mulcv: scale by ${mulcvParamId}` });
|
|
634
|
+
ir.push({ op: 'MULX', args: [mulcvReg], section, comment: 'Scale by pot' });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
// Unconnected: scale ACC by the parameter constant
|
|
639
|
+
ir.push({ op: 'SOF', args: [mulcvEquName, '0.0'], section, comment: `@mulcv default: ${mulcvParamId}` });
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 'readchorustap':
|
|
644
|
+
// @readChorusTap lfoSel flags center length offset
|
|
645
|
+
const lfoIdx = args[0] === '0' ? 'SIN0' : 'SIN1';
|
|
646
|
+
const tapFlags = args[1] === '0' ? 'REG|COMPC' : args[1]; // simplified
|
|
647
|
+
const tapMemo = args[4]; // assuming memory ID
|
|
648
|
+
ir.push({ op: 'CHO', args: ['RDA', lfoIdx, tapFlags, tapMemo], section });
|
|
649
|
+
ir.push({ op: 'CHO', args: ['RDA', lfoIdx, '0', `${tapMemo}+1`], section });
|
|
650
|
+
break;
|
|
651
|
+
case 'getbaseaddress':
|
|
652
|
+
// usually a no-op in our managed system
|
|
653
|
+
break;
|
|
654
|
+
case 'gain':
|
|
655
|
+
// @gain result, input, gain
|
|
656
|
+
ir.push({ op: 'RDAX', args: [args[1], args[2]], section });
|
|
657
|
+
break;
|
|
658
|
+
case 'comment':
|
|
659
|
+
// @comment text
|
|
660
|
+
const text = args.join(' ');
|
|
661
|
+
ir.push({ op: ';', args: [text], section: 'init' });
|
|
662
|
+
break;
|
|
663
|
+
default:
|
|
664
|
+
ctx.addError(`Unsupported directive or macro: @${macro}`);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
evaluateParameters(block, ctx) {
|
|
669
|
+
return this.evaluateParametersFromMap(block.parameters, ctx);
|
|
670
|
+
}
|
|
671
|
+
evaluateParametersFromMap(parameters, ctx, skipConversion = false) {
|
|
672
|
+
const evaluated = {};
|
|
673
|
+
for (const param of this.definition.parameters) {
|
|
674
|
+
let val = parameters[param.id] ?? param.default;
|
|
675
|
+
// Apply modern conversions
|
|
676
|
+
if (param.conversion && ctx && !skipConversion) {
|
|
677
|
+
val = this.applyConversion(param.conversion, val, ctx);
|
|
678
|
+
}
|
|
679
|
+
evaluated[param.id] = val;
|
|
680
|
+
}
|
|
681
|
+
return evaluated;
|
|
682
|
+
}
|
|
683
|
+
applyConversion(type, val, ctx) {
|
|
684
|
+
const Fs = 32768; // Default, should be pulled from context if dynamic
|
|
685
|
+
switch (type) {
|
|
686
|
+
case 'LOGFREQ':
|
|
687
|
+
return (1.0 - Math.exp(-2.0 * Math.PI * val / Fs)).toFixed(6);
|
|
688
|
+
case 'SVFFREQ':
|
|
689
|
+
// Returns sin(2 * pi * f / Fs), matching SpinCAD's base frequency coefficient
|
|
690
|
+
return (Math.sin(2.0 * Math.PI * val / Fs)).toFixed(6);
|
|
691
|
+
case 'SVF_DAMP':
|
|
692
|
+
// Returns 1.0 / Q (Chamberlin damping d). Used for Min/Max bounds.
|
|
693
|
+
return (1.0 / val).toFixed(6);
|
|
694
|
+
case 'SINLFOFREQ':
|
|
695
|
+
case 'HZ_TO_LFO_RATE':
|
|
696
|
+
return Math.round((1 << 18) * Math.PI * val / Fs);
|
|
697
|
+
case 'DBLEVEL':
|
|
698
|
+
return Math.pow(10.0, val / 20.0).toFixed(6);
|
|
699
|
+
case 'LENGTHTOTIME':
|
|
700
|
+
case 'MS_TO_SAMPLES':
|
|
701
|
+
return Math.round((val * Fs / 1000));
|
|
702
|
+
case 'MS_TO_LFO_RANGE':
|
|
703
|
+
return Math.round((val * Fs / 1000) * 32767 / 16385);
|
|
704
|
+
case 'SEMITONES_TO_RATE':
|
|
705
|
+
return ((Math.pow(2, val / 12) - 1.0) * 16384).toFixed(6);
|
|
706
|
+
case 'HZ_TO_HILBERT_SHIFT':
|
|
707
|
+
return (2 * Math.PI * val / Fs).toFixed(6);
|
|
708
|
+
default:
|
|
709
|
+
return typeof val === 'number' ? val.toFixed(6) : val;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
resolveInputs(block, ctx) {
|
|
713
|
+
const resolved = {};
|
|
714
|
+
for (const input of this.definition.inputs) {
|
|
715
|
+
const reg = ctx.getInputRegister(block.id, input.id);
|
|
716
|
+
if (reg)
|
|
717
|
+
resolved[input.id] = reg;
|
|
718
|
+
}
|
|
719
|
+
return resolved;
|
|
720
|
+
}
|
|
721
|
+
resolveOutputs(block, ctx) {
|
|
722
|
+
const resolved = {};
|
|
723
|
+
for (const output of this.definition.outputs) {
|
|
724
|
+
// Only allocate a register if this output is connected downstream
|
|
725
|
+
if (ctx.isOutputConnected(block.id, output.id)) {
|
|
726
|
+
const reg = ctx.allocateRegister(block.id, output.id);
|
|
727
|
+
resolved[output.id] = reg;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return resolved;
|
|
731
|
+
}
|
|
732
|
+
resolveInternalRegisters(block, ctx) {
|
|
733
|
+
const resolved = {};
|
|
734
|
+
if (this.definition.registers) {
|
|
735
|
+
for (const regId of this.definition.registers) {
|
|
736
|
+
resolved[regId] = ctx.allocateRegister(block.id, `local_${regId}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return resolved;
|
|
740
|
+
}
|
|
741
|
+
resolveMemoryObjects(block, ctx) {
|
|
742
|
+
const resolved = {};
|
|
743
|
+
if (this.definition.memories) {
|
|
744
|
+
const params = this.evaluateParameters(block, ctx);
|
|
745
|
+
for (const mem of this.definition.memories) {
|
|
746
|
+
let size = 0;
|
|
747
|
+
if (typeof mem.size === 'string' || typeof mem.size === 'number') {
|
|
748
|
+
const sizeStr = mem.size.toString();
|
|
749
|
+
// If the size is a raw parameter name without interpolation (e.g. "preDelay" vs "${preDelay}"), wrap it natively.
|
|
750
|
+
// This handles old-style ATL blocks that define memory size as just the string identifier.
|
|
751
|
+
const evalStr = sizeStr.includes('${') ? sizeStr : /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(sizeStr) ? `\${${sizeStr}}` : sizeStr;
|
|
752
|
+
// Substitute parameter variables first (e.g. "${delayLength} * 0.5")
|
|
753
|
+
const substitutedSizeExpr = this.performSubstitutions(evalStr, params, {}, {}, {}, {}, block, ctx);
|
|
754
|
+
if (substitutedSizeExpr.trim() === '') {
|
|
755
|
+
ctx.addError(`Memory size expression '${sizeStr}' evaluated to empty.`);
|
|
756
|
+
size = 1;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
const evalResult = this.algebraicCompiler.evaluateConstantExpression(substitutedSizeExpr);
|
|
760
|
+
if (evalResult !== null) {
|
|
761
|
+
if (isNaN(evalResult) || evalResult < 0) {
|
|
762
|
+
ctx.addError(`Memory size expression '${sizeStr}' evaluated to invalid number: ${evalResult}`);
|
|
763
|
+
size = 1;
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
size = Math.round(evalResult);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
size = parseInt(substitutedSizeExpr, 10);
|
|
771
|
+
if (isNaN(size) || size < 0) {
|
|
772
|
+
ctx.addError(`Memory size expression '${sizeStr}' could not be parsed as a valid size.`);
|
|
773
|
+
size = 1;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
size = mem.size;
|
|
780
|
+
}
|
|
781
|
+
const alloc = ctx.allocateMemory(mem.id, size);
|
|
782
|
+
resolved[mem.id] = alloc;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return resolved;
|
|
786
|
+
}
|
|
787
|
+
performSubstitutions(line, params, inputs, outputs, internalRegs, memory, block, ctx) {
|
|
788
|
+
const trimmed = line.trim().toLowerCase();
|
|
789
|
+
const isEqu = trimmed.startsWith('equ');
|
|
790
|
+
const firstTokenIndex = line.indexOf('${');
|
|
791
|
+
return line.replace(/\$\{([^}]+)\}/g, (match, key, offset) => {
|
|
792
|
+
// Check for property modifiers like .max and .min
|
|
793
|
+
const propMatch = key.match(/^(?:param\.)?([^.]+)\.(max|min)$/);
|
|
794
|
+
if (propMatch) {
|
|
795
|
+
const paramId = propMatch[1];
|
|
796
|
+
const prop = propMatch[2];
|
|
797
|
+
const paramDef = this.definition.parameters.find(p => p.id === paramId);
|
|
798
|
+
if (paramDef) {
|
|
799
|
+
let val = prop === 'max' ? paramDef.max : paramDef.min;
|
|
800
|
+
if (val !== undefined) {
|
|
801
|
+
if (paramDef.conversion && ctx) {
|
|
802
|
+
val = this.applyConversion(paramDef.conversion, val, ctx);
|
|
803
|
+
}
|
|
804
|
+
return val?.toString() ?? '';
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (key.startsWith('param.'))
|
|
809
|
+
return params[key.split('.')[1]]?.toString() || '';
|
|
810
|
+
if (key.startsWith('input.'))
|
|
811
|
+
return inputs[key.split('.')[1]] || '';
|
|
812
|
+
if (key.startsWith('output.'))
|
|
813
|
+
return outputs[key.split('.')[1]] || '';
|
|
814
|
+
if (key.startsWith('reg.'))
|
|
815
|
+
return internalRegs[key.split('.')[1]] || '';
|
|
816
|
+
if (key === 'Fs' || key === 'samplingRate')
|
|
817
|
+
return '32768';
|
|
818
|
+
if (key.startsWith('mem.')) {
|
|
819
|
+
const parts = key.split('.');
|
|
820
|
+
const memId = parts[1];
|
|
821
|
+
const prop = parts[2];
|
|
822
|
+
const alloc = memory[memId];
|
|
823
|
+
if (!alloc)
|
|
824
|
+
return '';
|
|
825
|
+
if (!prop || prop === 'start')
|
|
826
|
+
return alloc.name;
|
|
827
|
+
if (prop === 'size')
|
|
828
|
+
return alloc.size.toString();
|
|
829
|
+
if (prop === 'end')
|
|
830
|
+
return `(${alloc.name} + ${alloc.size} - 1)`;
|
|
831
|
+
if (prop === 'middle')
|
|
832
|
+
return `(${alloc.name} + ${alloc.size} / 2)`;
|
|
833
|
+
return alloc.name;
|
|
834
|
+
}
|
|
835
|
+
if (key.startsWith('local.')) {
|
|
836
|
+
const shortId = ctx.getShortId(block.id);
|
|
837
|
+
const varName = `${shortId}_${key.split('.')[1]}`;
|
|
838
|
+
// If this is the definition part of an EQU line, return the name only
|
|
839
|
+
if (isEqu && offset === firstTokenIndex) {
|
|
840
|
+
return varName;
|
|
841
|
+
}
|
|
842
|
+
const val = ctx.getVariable(varName);
|
|
843
|
+
return val !== undefined ? val : varName;
|
|
844
|
+
}
|
|
845
|
+
// Check direct parameter names
|
|
846
|
+
if (params[key] !== undefined)
|
|
847
|
+
return params[key].toString();
|
|
848
|
+
// Check dynamic variables set by macros
|
|
849
|
+
const v = ctx.getVariable(key);
|
|
850
|
+
if (v !== undefined)
|
|
851
|
+
return v;
|
|
852
|
+
// Check if it's a raw local name that was already expanded
|
|
853
|
+
const localV = ctx.getVariable(`${block.id}_${key}`);
|
|
854
|
+
if (localV !== undefined)
|
|
855
|
+
return localV;
|
|
856
|
+
return match;
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
//# sourceMappingURL=BlockTemplate.js.map
|