@audiofab-io/fv1-core 0.1.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.
@@ -0,0 +1,1081 @@
1
+ /**
2
+ * FV-1 DSP Simulator
3
+ *
4
+ * Simulates the Spin Semiconductor FV-1 DSP chip.
5
+ * This class is designed to be platform-agnostic (Node.js or Browser/AudioWorklet).
6
+ */
7
+ import { FV1Assembler } from '../assembler/FV1Assembler.js';
8
+ export class FV1Simulator {
9
+ // Register aliases (Getters/Setters)
10
+ // 0-7: Parameters
11
+ // These are now handled by updateStateRegisters for debugger view
12
+ // 8-13: Internal State accumulators (aliased as registers for the debugger)
13
+ // These are now handled by updateStateRegisters for debugger view
14
+ constructor() {
15
+ // Constants
16
+ // Capabilities (Configurable)
17
+ this.delaySize = 32768;
18
+ this.delayMask = 32767; // For efficient circular buffer
19
+ this.regCount = 32;
20
+ this.progSize = 128;
21
+ // --- Float-based LFO state (from Expert Sleepers C port) ---
22
+ this.sin0 = 0;
23
+ this.cos0 = 0;
24
+ this.sin1 = 0;
25
+ this.cos1 = 0;
26
+ this.rmp0 = 0;
27
+ this.rmp1 = 0;
28
+ this.sin0_rate = 0;
29
+ this.sin0_range = 0;
30
+ this.sin1_rate = 0;
31
+ this.sin1_range = 0;
32
+ this.rmp0_rate = 0;
33
+ this.rmp0_range = 0;
34
+ this.rmp1_rate = 0;
35
+ this.rmp1_range = 0;
36
+ this.acc = 0;
37
+ this.pacc = 0;
38
+ this.lr = 0; // Last Read register
39
+ this.lfo = 0; // Internal LFO fregister (for CHO)
40
+ this.delayPointer = 0; // Circular buffer pointer
41
+ this.firstRun = true;
42
+ this.pc = 0; // Program Counter
43
+ this.breakpoints = new Set();
44
+ // Symbol metadata (optional, for debugging)
45
+ this.symbols = [];
46
+ this.memories = [];
47
+ this.fv1AsmMemBug = false;
48
+ // --- Trace Logging ---
49
+ this.traceEnabled = false;
50
+ this.traceWriter = null; // Streaming callback
51
+ this.cycleCount = 0;
52
+ this.traceCycle = 0;
53
+ this.traceMaxCycles = 0; // 0 = unlimited
54
+ this.traceRowCount = 0;
55
+ this.traceDelayAddr = -1; // computed physical address in delay RAM
56
+ this.traceDelayOffset = -1; // raw offset from instruction (identifies MEM block)
57
+ this.traceDelayOp = ''; // 'R' for read, 'W' for write
58
+ this.traceDelayValue = 0; // value read/written
59
+ this.traceInstructionPC = -1; // PC of currently executing instruction
60
+ this.currentReadOffsets = new Set(); // Tracks read offsets in current sample cycle
61
+ this.traceOnComplete = null;
62
+ this.delayRam = new Float32Array(this.delaySize);
63
+ this.registers = new Float32Array(32 + this.regCount);
64
+ this.program = new Uint32Array(this.progSize);
65
+ }
66
+ /**
67
+ * Configures simulator hardware limits.
68
+ */
69
+ setCapabilities(delaySize, regCount, progSize) {
70
+ this.delaySize = delaySize;
71
+ this.delayMask = delaySize - 1; // Assuming power of 2 for now, but modulo is fallback
72
+ this.regCount = regCount; // Number of user registers
73
+ this.progSize = progSize;
74
+ // Reallocate if needed
75
+ this.delayRam = new Float32Array(this.delaySize);
76
+ this.registers = new Float32Array(32 + this.regCount); // 32 system + N user registers
77
+ this.program = new Uint32Array(this.progSize);
78
+ this.reset();
79
+ }
80
+ getDelayPointer() {
81
+ return this.delayPointer;
82
+ }
83
+ getDelaySize() {
84
+ return this.delaySize;
85
+ }
86
+ /**
87
+ * Loads the machine code into the simulator.
88
+ * @param code Array of 32-bit integers representing the assembled program.
89
+ */
90
+ loadProgram(code) {
91
+ if (code.length > this.progSize) {
92
+ console.warn(`Program size (${code.length}) exceeds max size (${this.progSize}). Truncating.`);
93
+ }
94
+ this.program.fill(0);
95
+ this.program.set(code.slice(0, this.progSize));
96
+ this.reset();
97
+ }
98
+ /**
99
+ * Resets the simulator state (clears memory, registers, accumulator).
100
+ */
101
+ reset() {
102
+ this.delayRam.fill(0);
103
+ this.registers.fill(0);
104
+ this.acc = 0;
105
+ this.pacc = 0;
106
+ this.lr = 0;
107
+ this.lfo = 0;
108
+ this.pc = 0;
109
+ this.delayPointer = 0;
110
+ this.firstRun = true;
111
+ this.sin0 = 0;
112
+ this.sin1 = 0;
113
+ // Peak amplitude from Java SinLFO.java initialized to -0x7fff00 mapped to float (-1.0)
114
+ // BUT wait, in C the initialization is not explicit, we can just use 1.0 or -1.0
115
+ // SpinCAD sets cos to -0x7fff00l which is approx -1.0
116
+ this.cos0 = -1.0;
117
+ this.cos1 = -1.0;
118
+ this.rmp0 = 0;
119
+ this.rmp1 = 0;
120
+ this.sin0_rate = 0;
121
+ this.sin0_range = 0;
122
+ this.sin1_rate = 0;
123
+ this.sin1_range = 0;
124
+ this.rmp0_rate = 0;
125
+ this.rmp0_range = 0;
126
+ this.rmp1_rate = 0;
127
+ this.rmp1_range = 0;
128
+ // Default POT values to 0.5
129
+ this.registers[16] = 0.5;
130
+ this.registers[17] = 0.5;
131
+ this.registers[18] = 0.5;
132
+ }
133
+ /**
134
+ * Set breakpoints at specific instruction addresses.
135
+ * @param addresses Set of addresses to break at.
136
+ */
137
+ setBreakpoints(addresses) {
138
+ this.breakpoints = addresses;
139
+ }
140
+ /**
141
+ * Set symbol and memory metadata for expression evaluation.
142
+ */
143
+ setSymbols(symbols, memories, fv1AsmMemBug = false) {
144
+ this.symbols = symbols;
145
+ this.memories = memories;
146
+ this.fv1AsmMemBug = fv1AsmMemBug;
147
+ }
148
+ /**
149
+ * Process a block of audio samples.
150
+ * Useful for real-time audio processing in AudioWorklets.
151
+ */
152
+ processBlock(inputL, inputR, outputL, outputR, pot0, pot1, pot2) {
153
+ const len = inputL.length;
154
+ for (let i = 0; i < len; i++) {
155
+ const [outL, outR] = this.step(inputL[i], inputR[i], pot0, pot1, pot2);
156
+ outputL[i] = outL;
157
+ outputR[i] = outR;
158
+ }
159
+ }
160
+ /**
161
+ * Process a single sample frame.
162
+ * Executes instructions until the end of the program or a breakpoint is hit.
163
+ * @param skipCurrentBreakpoint If true, will not break on the instruction at the current PC.
164
+ * @returns [outL, outR, breakpointHit]
165
+ */
166
+ step(inL, inR, pot0, pot1, pot2, skipCurrentBreakpoint = false) {
167
+ // Only begin a new frame if we are at PC 0 (either start or after wrap around)
168
+ if (this.pc === 0) {
169
+ this.beginFrame(inL, inR, pot0, pot1, pot2);
170
+ }
171
+ let firstInstruction = true;
172
+ while (this.pc < this.progSize) {
173
+ if (this.breakpoints.has(this.pc)) {
174
+ if (!firstInstruction || !skipCurrentBreakpoint) {
175
+ return [...this.getOutputs(), true];
176
+ }
177
+ }
178
+ this.stepInstruction();
179
+ firstInstruction = false;
180
+ }
181
+ return [...this.endFrame(), false];
182
+ }
183
+ getOutputs() {
184
+ return [this.registers[22], this.registers[23]];
185
+ }
186
+ beginFrame(inL = 0, inR = 0, pot0 = 0, pot1 = 0, pot2 = 0) {
187
+ this.cycleCount++;
188
+ this.currentReadOffsets.clear();
189
+ // Saturate inputs (ADC is -1.0 to 0.999..., POT is 0 to 0.999...)
190
+ // POT has 10-bit resolution (1024 levels)
191
+ const sat = (v) => Math.max(-1.0, Math.min(FV1Simulator.MAX_ACC, v));
192
+ const satPot = (v) => {
193
+ const quantized = Math.floor(Math.max(0, Math.min(0.9999999, v)) * 1024) / 1024;
194
+ return quantized;
195
+ };
196
+ // Execute Program Setup
197
+ this.acc = 0; // Accumulator is cleared at start of run
198
+ this.lr = 0; // LR is transient
199
+ this.pacc = 0;
200
+ this.pc = 0;
201
+ // Map inputs to registers (Standard FV-1 mapping)
202
+ this.registers[20] = sat(inL);
203
+ this.registers[21] = sat(inR);
204
+ this.registers[16] = satPot(pot0);
205
+ this.registers[17] = satPot(pot1);
206
+ this.registers[18] = satPot(pot2);
207
+ this.updateStateRegisters();
208
+ }
209
+ endFrame() {
210
+ this.pc = 0;
211
+ this.firstRun = false;
212
+ this.updateLFOs();
213
+ this.updateStateRegisters();
214
+ this.advanceTraceCycle();
215
+ // Advance Delay Pointer (Circular Buffer)
216
+ this.delayPointer = (this.delayPointer - 1 + this.delaySize) % this.delaySize;
217
+ // Outputs (DACL = REG22, DACR = REG23)
218
+ return [this.registers[22], this.registers[23]];
219
+ }
220
+ /**
221
+ * Executes a single instruction at the current PC.
222
+ * @returns The next PC address.
223
+ */
224
+ stepInstruction() {
225
+ if (this.pc >= this.progSize)
226
+ return this.pc;
227
+ const inst = this.program[this.pc];
228
+ const opcode = inst & 0x1F;
229
+ const preOpAcc = this.acc;
230
+ // Reset per-instruction trace state
231
+ this.traceDelayAddr = -1;
232
+ this.traceDelayOffset = -1;
233
+ this.traceDelayOp = '';
234
+ this.traceDelayValue = 0;
235
+ this.traceInstructionPC = this.pc;
236
+ const skip = this.executeInstruction(inst);
237
+ // Trace log AFTER instruction executes so ACC reflects the result
238
+ if (this.traceEnabled) {
239
+ this.logTrace(this.traceInstructionPC, opcode, inst, preOpAcc);
240
+ }
241
+ if (opcode !== 0x11) { // Not SKP
242
+ this.pacc = preOpAcc;
243
+ }
244
+ this.pc += 1 + skip;
245
+ return this.pc;
246
+ }
247
+ executeInstruction(inst) {
248
+ const opcode = inst & 0x1F; // Bottom 5 bits for opcode
249
+ let skip = 0;
250
+ switch (opcode) {
251
+ case 0x00: // RDA (Read Delay Accumulate)
252
+ this.opRDA(inst);
253
+ break;
254
+ case 0x01: // RMPA (Read Memory Pointer Accumulate)
255
+ this.opRMPA(inst);
256
+ break;
257
+ case 0x02: // WRA (Write Delay Accumulate)
258
+ this.opWRA(inst);
259
+ break;
260
+ case 0x03: // WRAP (Write Delay Accumulate & Pointer)
261
+ this.opWRAP(inst);
262
+ break;
263
+ case 0x04: // RDAX (Read Register Accumulate)
264
+ this.opRDAX(inst);
265
+ break;
266
+ case 0x05: // RDFX (Read Register Filter)
267
+ this.opRDFX(inst);
268
+ break;
269
+ case 0x06: // WRAX (Write Register Accumulate)
270
+ this.opWRAX(inst);
271
+ break;
272
+ case 0x07: // WRHX (Write Register High)
273
+ this.opWRHX(inst);
274
+ break;
275
+ case 0x08: // WRLX (Write Register Low)
276
+ this.opWRLX(inst);
277
+ break;
278
+ case 0x09: // MAXX
279
+ this.opMAXX(inst);
280
+ break;
281
+ case 0x0A: // MULX
282
+ this.opMULX(inst);
283
+ break;
284
+ case 0x0B: // LOG
285
+ this.opLOG(inst);
286
+ break;
287
+ case 0x0C: // EXP
288
+ this.opEXP(inst);
289
+ break;
290
+ case 0x0D: // SOF
291
+ this.opSOF(inst);
292
+ break;
293
+ case 0x0E: // AND
294
+ this.opAND(inst);
295
+ break;
296
+ case 0x0F: // OR
297
+ this.opOR(inst);
298
+ break;
299
+ case 0x10: // XOR
300
+ this.opXOR(inst);
301
+ break;
302
+ case 0x11: // SKP
303
+ skip = this.opSKP(inst);
304
+ break;
305
+ case 0x12: // WLDS / WLDR
306
+ // Check bit 30 to distinguish WLDS (0) and WLDR (1)
307
+ if ((inst >>> 30) & 1) {
308
+ this.opWLDR(inst);
309
+ }
310
+ else {
311
+ this.opWLDS(inst);
312
+ }
313
+ break;
314
+ case 0x13: // JAM
315
+ this.opJAM(inst);
316
+ break;
317
+ case 0x14: // CHO
318
+ this.opCHO(inst);
319
+ break;
320
+ default:
321
+ // console.warn(`Unknown opcode: ${opcode.toString(16)}`);
322
+ break;
323
+ }
324
+ return skip;
325
+ }
326
+ // --- Opcode Implementations ---
327
+ opRDA(inst) {
328
+ // ... (existing comments)
329
+ const addr = (inst >>> 5) & 0x7FFF;
330
+ const coeff = this.decodeS1_9((inst >>> 21) & 0x7FF);
331
+ // Address is relative to current delay pointer in circular buffer
332
+ const readAddr = (this.delayPointer + addr) & this.delayMask;
333
+ const val = this.delayRam[readAddr];
334
+ this.traceDelayAddr = readAddr;
335
+ this.traceDelayOffset = addr;
336
+ this.traceDelayOp = 'R';
337
+ this.traceDelayValue = val;
338
+ this.currentReadOffsets.add(addr);
339
+ this.lr = val;
340
+ this.acc += val * coeff;
341
+ this.acc = this.saturate(this.acc);
342
+ }
343
+ opWRA(inst) {
344
+ // WRA addr, coeff
345
+ // Delay[addr] = ACC; ACC = ACC * coeff
346
+ // Encoding: CCCCCCCCCCCAAAAAAAAAAAAAAAA00010
347
+ const addr = (inst >>> 5) & 0x7FFF;
348
+ const coeff = this.decodeS1_9((inst >>> 21) & 0x7FF);
349
+ const writeAddr = (this.delayPointer + addr) & this.delayMask;
350
+ this.traceDelayAddr = writeAddr;
351
+ this.traceDelayOffset = addr;
352
+ this.traceDelayOp = 'W';
353
+ this.traceDelayValue = this.acc;
354
+ this.delayRam[writeAddr] = this.acc;
355
+ this.acc *= coeff;
356
+ this.acc = this.saturate(this.acc);
357
+ }
358
+ opWRAP(inst) {
359
+ // WRAP addr, coeff
360
+ // Delay[addr] = ACC; ACC = ACC * coeff + LR
361
+ // Note: Pointer decrement happens at end of step, not here.
362
+ // Encoding: CCCCCCCCCCCAAAAAAAAAAAAAAAA00011
363
+ const addr = (inst >>> 5) & 0x7FFF;
364
+ const coeff = this.decodeS1_9((inst >>> 21) & 0x7FF);
365
+ const writeAddr = (this.delayPointer + addr) & this.delayMask;
366
+ this.traceDelayAddr = writeAddr;
367
+ this.traceDelayOffset = addr;
368
+ this.traceDelayOp = 'W';
369
+ this.traceDelayValue = this.acc;
370
+ this.delayRam[writeAddr] = this.acc;
371
+ // ACC = ACC * coeff + LR
372
+ this.acc = this.acc * coeff + this.lr;
373
+ this.acc = this.saturate(this.acc);
374
+ }
375
+ opRDAX(inst) {
376
+ // RDAX reg, coeff
377
+ // ACC = ACC + (Reg[reg] * coeff)
378
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA00100
379
+ const reg = (inst >>> 5) & 0x3F;
380
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
381
+ const val = this.registers[reg];
382
+ this.acc += val * coeff;
383
+ this.acc = this.saturate(this.acc);
384
+ }
385
+ opWRAX(inst) {
386
+ // WRAX reg, coeff
387
+ // Reg[reg] = ACC; ACC = ACC * coeff
388
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA00110
389
+ const reg = (inst >>> 5) & 0x3F;
390
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
391
+ this.registers[reg] = this.acc;
392
+ this.acc *= coeff;
393
+ this.acc = this.saturate(this.acc);
394
+ }
395
+ opSOF(inst) {
396
+ // SOF c, d
397
+ // ACC = ACC * c + d
398
+ // Encoding: CCCCCCCCCCCCCCCCDDDDDDDDDDD01101
399
+ const d = this.decodeS_10((inst >>> 5) & 0x7FF);
400
+ const c = this.decodeS1_14((inst >>> 16) & 0xFFFF);
401
+ this.acc = this.acc * c + d;
402
+ this.acc = this.saturate(this.acc);
403
+ }
404
+ opRMPA(inst) {
405
+ // RMPA coeff
406
+ // Read memory pointer. ADDR_PTR is mapped to REG24.
407
+ // Encoding: CCCCCCCCCCC000000000001100000001 (or similar, coeff is top)
408
+ const coeff = this.decodeS1_9((inst >>> 21) & 0x7FF);
409
+ const ptr = Math.floor(this.registers[24] * this.delaySize);
410
+ const readAddr = (this.delayPointer + ptr) % this.delaySize;
411
+ const val = this.delayRam[readAddr];
412
+ this.traceDelayAddr = readAddr;
413
+ this.traceDelayOffset = ptr;
414
+ this.traceDelayOp = 'R';
415
+ this.traceDelayValue = val;
416
+ this.currentReadOffsets.add(ptr);
417
+ this.lr = val;
418
+ this.acc += val * coeff;
419
+ this.acc = this.saturate(this.acc);
420
+ }
421
+ opMULX(inst) {
422
+ // Encoding: 000000000000000000000AAAAAA01010
423
+ const reg = (inst >>> 5) & 0x3F;
424
+ this.acc = this.acc * this.registers[reg];
425
+ this.acc = this.saturate(this.acc);
426
+ }
427
+ opLOG(inst) {
428
+ // ACC = log2(|ACC|) * coeff + d
429
+ // Encoding: CCCCCCCCCCCCCCCCDDDDDDDDDDD01011
430
+ const d = this.decodeS4_6((inst >>> 5) & 0x7FF);
431
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
432
+ const val = Math.abs(this.acc);
433
+ let logVal;
434
+ if (val > 1.52587890625e-5) { // 2^-16, approx 96dB limit
435
+ logVal = Math.log2(val);
436
+ }
437
+ else {
438
+ logVal = -16.0;
439
+ }
440
+ // Result is in S4.19 format, so we divide by 16 to keep it in our S.23 float space
441
+ this.acc = (logVal * coeff + d) / 16.0;
442
+ this.acc = this.saturate(this.acc);
443
+ }
444
+ opEXP(inst) {
445
+ // ACC = 2^ACC
446
+ // Encoding: CCCCCCCCCCCCCCCCDDDDDDDDDDD01100
447
+ const d = this.decodeS_10((inst >>> 5) & 0x7FF);
448
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
449
+ const valS419 = this.acc * 16.0;
450
+ // Result is linear S.23, which naturally fits our -1..1 float range
451
+ this.acc = Math.pow(2, valS419) * coeff + d;
452
+ this.acc = this.saturate(this.acc);
453
+ }
454
+ opRDFX(inst) {
455
+ // RDFX reg, coeff
456
+ // ACC = (REG[reg] - ACC) * coeff + REG[reg]
457
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA00101
458
+ const reg = (inst >>> 5) & 0x3F;
459
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
460
+ const rx = this.registers[reg];
461
+ this.acc = (this.acc - rx) * coeff + rx;
462
+ this.acc = this.saturate(this.acc);
463
+ }
464
+ opMAXX(inst) {
465
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA01001
466
+ const reg = (inst >>> 5) & 0x3F;
467
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
468
+ const a = Math.abs(this.acc);
469
+ const b = Math.abs(this.registers[reg] * coeff);
470
+ // MAXX result is always the magnitude (absolute value)
471
+ this.acc = Math.max(a, b);
472
+ this.acc = this.saturate(this.acc);
473
+ }
474
+ opSKP(inst) {
475
+ // SKP condition, n
476
+ // Encoding: CCCCCNNNNNN000000000000000010001
477
+ const n = (inst >>> 21) & 0x3F;
478
+ const flags = inst & 0xF8000000;
479
+ let conditionMet = false;
480
+ // Flags: RUN=0x80000000, ZRC=0x40000000, ZRO=0x20000000, GEZ=0x10000000, NEG=0x08000000
481
+ if ((flags & 0x08000000) && this.acc < 0)
482
+ conditionMet = true; // NEG
483
+ if ((flags & 0x10000000) && this.acc >= 0)
484
+ conditionMet = true; // GEZ
485
+ if ((flags & 0x20000000) && this.acc === 0)
486
+ conditionMet = true; // ZRO
487
+ // ZRC (Zero Crossing) requires previous acc
488
+ if ((flags & 0x40000000) && (this.acc * this.pacc < 0))
489
+ conditionMet = true;
490
+ // RUN flag: Skip if NOT first run
491
+ if ((flags & 0x80000000) && !this.firstRun)
492
+ conditionMet = true;
493
+ return conditionMet ? n : 0;
494
+ }
495
+ opWRLX(inst) {
496
+ // WRLX reg, coeff
497
+ // Reg[reg] = ACC; ACC = (PACC - ACC) * coeff + PACC
498
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA01000
499
+ const reg = (inst >>> 5) & 0x3F;
500
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
501
+ this.registers[reg] = this.acc;
502
+ this.acc = (this.pacc - this.acc) * coeff + this.pacc;
503
+ this.acc = this.saturate(this.acc);
504
+ }
505
+ opWRHX(inst) {
506
+ // WRHX reg, coeff
507
+ // Reg[reg] = ACC; ACC = PACC + ACC * coeff
508
+ // Encoding: CCCCCCCCCCCCCCCC00000AAAAAA00111
509
+ const reg = (inst >>> 5) & 0x3F;
510
+ const coeff = this.decodeS1_14((inst >>> 16) & 0xFFFF);
511
+ this.registers[reg] = this.acc;
512
+ this.acc = this.pacc + this.acc * coeff;
513
+ this.acc = this.saturate(this.acc);
514
+ }
515
+ opAND(inst) {
516
+ // AND mask
517
+ // Encoding: MMMMMMMMMMMMMMMMMMMMMMMM00001110
518
+ const mask = (inst >>> 8) & 0xFFFFFF;
519
+ // Convert ACC to 24-bit int, apply mask, convert back
520
+ let iAcc = Math.floor(this.acc * 8388608.0); // 2^23
521
+ iAcc &= mask;
522
+ this.acc = iAcc / 8388608.0;
523
+ }
524
+ opOR(inst) {
525
+ // OR mask
526
+ // Encoding: MMMMMMMMMMMMMMMMMMMMMMMM00001111
527
+ const mask = (inst >>> 8) & 0xFFFFFF;
528
+ let iAcc = Math.floor(this.acc * 8388608.0);
529
+ iAcc |= mask;
530
+ this.acc = iAcc / 8388608.0;
531
+ }
532
+ opXOR(inst) {
533
+ // XOR mask
534
+ // Encoding: MMMMMMMMMMMMMMMMMMMMMMMM00010000
535
+ const mask = (inst >>> 8) & 0xFFFFFF;
536
+ let iAcc = Math.floor(this.acc * 8388608.0);
537
+ iAcc ^= mask;
538
+ this.acc = iAcc / 8388608.0;
539
+ }
540
+ opJAM(inst) {
541
+ // JAM lfo
542
+ // Encoding: 0000000000000000000000001N010011
543
+ const lfo = (inst >>> 6) & 0x3; // 0=RMP0, 1=RMP1
544
+ if (lfo === 0)
545
+ this.rmp0 = 0;
546
+ else if (lfo === 1)
547
+ this.rmp1 = 0;
548
+ }
549
+ opWLDS(inst) {
550
+ // WLDS lfo, freq, amp
551
+ const lfoSelect = (inst >>> 29) & 0x1;
552
+ const freqRaw = (inst >>> 20) & 0x1FF;
553
+ const ampRaw = (inst >>> 5) & 0x7FFF;
554
+ // Freq is an unsigned 9-bit value (0 to 511), amp is unsigned 15-bit (0 to 32767).
555
+ // The FV-1 hardware uses power-of-2 fixed-point arithmetic (bit shifts),
556
+ // so amplitude is divided by 2^15=32768, not 32767. Using 32767 causes the
557
+ // computed delay address range to slightly exceed memory block boundaries.
558
+ const f = freqRaw / 512.0;
559
+ const a = ampRaw / 32768.0;
560
+ if (lfoSelect === 0) {
561
+ this.sin0_rate = f;
562
+ this.sin0_range = a;
563
+ this.registers[0] = f; // SIN0_RATE (reg0)
564
+ this.registers[1] = a; // SIN0_RANGE (reg1)
565
+ this.cos0 = -1.0;
566
+ this.sin0 = 0.0;
567
+ }
568
+ else {
569
+ this.sin1_rate = f;
570
+ this.sin1_range = a;
571
+ this.registers[2] = f; // SIN1_RATE (reg2)
572
+ this.registers[3] = a; // SIN1_RANGE (reg3)
573
+ this.cos1 = -1.0;
574
+ this.sin1 = 0.0;
575
+ }
576
+ }
577
+ opWLDR(inst) {
578
+ // WLDR lfo, freq, amp
579
+ // Encoding: 01NFFFFFFFFFFFFFFFF000000AA10010
580
+ const lfoSelect = (inst >>> 29) & 0x1; // 0=RMP0, 1=RMP1
581
+ // Freq is signed 16-bit
582
+ let freq = (inst >>> 13) & 0xFFFF;
583
+ if (freq & 0x8000)
584
+ freq -= 65536;
585
+ // Amp is a 2-bit code
586
+ const ampCode = (inst >>> 5) & 0x3;
587
+ const amp = 4096 >> ampCode; // 0->4096, 1->2048, 2->1024, 3->512
588
+ // C logic: rate = f/16384.0, range = a/8192.0
589
+ const f_val = freq / 16384.0;
590
+ const a_val = amp / 8192.0;
591
+ if (lfoSelect === 0) {
592
+ this.rmp0_rate = f_val;
593
+ this.rmp0_range = a_val;
594
+ this.registers[4] = f_val; // RMP0_RATE (reg4)
595
+ this.registers[5] = a_val; // RMP0_RANGE (reg5)
596
+ this.rmp0 = 0.0;
597
+ }
598
+ else {
599
+ this.rmp1_rate = f_val;
600
+ this.rmp1_range = a_val;
601
+ this.registers[6] = f_val; // RMP1_RATE (reg6)
602
+ this.registers[7] = a_val; // RMP1_RANGE (reg7)
603
+ this.rmp1 = 0.0;
604
+ }
605
+ }
606
+ getLfoVal(flags, lfoSelect) {
607
+ if (lfoSelect === 0) {
608
+ return (flags & 1) ? this.cos0 : this.sin0;
609
+ }
610
+ else if (lfoSelect === 1) {
611
+ return (flags & 1) ? this.cos1 : this.sin1;
612
+ }
613
+ else if (lfoSelect === 2) {
614
+ return this.rmp0;
615
+ }
616
+ else {
617
+ return this.rmp1;
618
+ }
619
+ }
620
+ getLfoRange(lfoSelect) {
621
+ if (lfoSelect === 0)
622
+ return this.sin0_range;
623
+ if (lfoSelect === 1)
624
+ return this.sin1_range;
625
+ if (lfoSelect === 2)
626
+ return this.rmp0_range;
627
+ return this.rmp1_range;
628
+ }
629
+ opCHO(inst) {
630
+ const mode = (inst >>> 30) & 0x3;
631
+ if (mode === 0) {
632
+ this.opCHO_RDA(inst);
633
+ }
634
+ else if (mode === 2) {
635
+ this.opCHO_SOF(inst);
636
+ }
637
+ else if (mode === 3) {
638
+ this.opCHO_RDAL(inst);
639
+ }
640
+ }
641
+ opCHO_RDA(inst) {
642
+ const flags = (inst >>> 24) & 0x3F;
643
+ const lfoSelect = (inst >>> 21) & 0x3;
644
+ const offset = (inst >>> 5) & 0x7FFF;
645
+ const lfoIn = this.getLfoVal(flags, lfoSelect);
646
+ let range = this.getLfoRange(lfoSelect);
647
+ range *= 8192.0;
648
+ if (flags & 2) { // cho_reg
649
+ this.lfo = lfoIn;
650
+ }
651
+ let v = this.lfo;
652
+ if (flags & 16) { // cho_rptr2
653
+ v += 0.5;
654
+ if (v >= 1.0)
655
+ v -= 1.0;
656
+ }
657
+ if (flags & 8) { // cho_compa
658
+ v = -v;
659
+ }
660
+ let index;
661
+ let c;
662
+ if (flags & 32) { // cho_na
663
+ index = offset;
664
+ c = Math.min(v, 1.0 - v);
665
+ c = Math.max(0.0, Math.min(1.0, 4.0 * c - 0.5));
666
+ }
667
+ else {
668
+ const lfoVal = v * range;
669
+ const lfoInteger = Math.trunc(lfoVal);
670
+ c = lfoVal - lfoInteger;
671
+ index = lfoInteger + offset;
672
+ }
673
+ const readAddr = (this.delayPointer + index) & this.delayMask;
674
+ this.lr = this.delayRam[readAddr];
675
+ this.traceDelayAddr = readAddr;
676
+ this.traceDelayOffset = index;
677
+ this.traceDelayOp = 'R';
678
+ this.traceDelayValue = this.lr;
679
+ this.currentReadOffsets.add(index);
680
+ if (flags & 4) { // cho_compc
681
+ c = 1.0 - c;
682
+ }
683
+ this.acc += this.lr * c;
684
+ this.acc = this.saturate(this.acc);
685
+ }
686
+ opCHO_SOF(inst) {
687
+ const flags = (inst >>> 24) & 0x3F;
688
+ const lfoSelect = (inst >>> 21) & 0x3;
689
+ const coeffRaw = (inst >>> 5) & 0xFFFF;
690
+ const coeff = this.decodeS_15(coeffRaw);
691
+ const lfoIn = this.getLfoVal(flags, lfoSelect);
692
+ const range = this.getLfoRange(lfoSelect);
693
+ if (flags & 2) { // cho_reg
694
+ this.lfo = lfoIn;
695
+ }
696
+ let v = this.lfo;
697
+ if (flags & 32) { // cho_na
698
+ v = Math.min(v, 1.0 - v);
699
+ v = Math.max(0.0, Math.min(1.0, 4.0 * v - 0.5));
700
+ }
701
+ else {
702
+ v *= range;
703
+ }
704
+ if (flags & 4) { // cho_compc
705
+ v = 1.0 - v;
706
+ }
707
+ this.acc = v * this.acc + coeff;
708
+ this.acc = this.saturate(this.acc);
709
+ }
710
+ opCHO_RDAL(inst) {
711
+ const flags = (inst >>> 24) & 0x3F;
712
+ const lfoSelect = (inst >>> 21) & 0x3;
713
+ const lfoIn = this.getLfoVal(flags, lfoSelect);
714
+ this.acc = lfoIn;
715
+ this.acc = this.saturate(this.acc);
716
+ }
717
+ // --- Debugging / State Access ---
718
+ getPC() {
719
+ return this.pc;
720
+ }
721
+ getProgSize() {
722
+ return this.progSize;
723
+ }
724
+ setPC(pc) {
725
+ this.pc = Math.max(0, Math.min(this.progSize - 1, pc));
726
+ }
727
+ setAcc(val) {
728
+ this.acc = this.saturate(val);
729
+ }
730
+ setPacc(val) {
731
+ this.pacc = this.saturate(val);
732
+ }
733
+ /**
734
+ * Sets a register value with hardware-accurate saturation and quantization.
735
+ * @param idx Register index (0-63)
736
+ * @param val Raw value (float)
737
+ */
738
+ setRegister(idx, val) {
739
+ if (idx < 0 || idx >= (32 + this.regCount))
740
+ return;
741
+ // POT registers (16, 17, 18) are 10-bit quantized 0..1
742
+ if (idx >= 16 && idx <= 18) {
743
+ val = Math.floor(Math.max(0, Math.min(0.9999999, val)) * 1024) / 1024;
744
+ }
745
+ else {
746
+ // Other registers (ADC, DAC, User) are S.23 saturated
747
+ val = this.saturate(val);
748
+ }
749
+ this.registers[idx] = val;
750
+ }
751
+ /**
752
+ * Evaluates a string expression (register name, symbol, memory suffix).
753
+ * @returns { result: string, value: number } or null
754
+ */
755
+ evaluateExpression(expr) {
756
+ let expression = expr.trim().toUpperCase();
757
+ // Handle suffixes ^ and #
758
+ let suffix = "";
759
+ if (expression.endsWith("^")) {
760
+ suffix = "^";
761
+ expression = expression.slice(0, -1);
762
+ }
763
+ else if (expression.endsWith("#")) {
764
+ suffix = "#";
765
+ expression = expression.slice(0, -1);
766
+ }
767
+ // 1. Check if it's a register name
768
+ const state = this.getState();
769
+ if (suffix === "" && state.registers[expression] !== undefined) {
770
+ return { label: expression, value: state.registers[expression] };
771
+ }
772
+ // 2. Check if it's ACC/PACC
773
+ if (suffix === "" && expression === "ACC") {
774
+ return { label: "ACC", value: this.acc };
775
+ }
776
+ if (suffix === "" && expression === "PACC") {
777
+ return { label: "PACC", value: this.pacc };
778
+ }
779
+ // 3. Check symbols (EQU)
780
+ const sym = this.symbols.find(s => s.name.toUpperCase() === expression);
781
+ if (sym && suffix === "") {
782
+ const addr = parseInt(sym.value);
783
+ if (!isNaN(addr) && addr >= 0 && addr <= 63) {
784
+ return { label: `REG[${addr}] (${sym.name})`, value: this.registers[addr] };
785
+ }
786
+ }
787
+ // 4. Check memories (MEM)
788
+ const mem = this.memories.find(m => m.name.toUpperCase() === expression);
789
+ if (mem && mem.start !== undefined) {
790
+ let addr = mem.start;
791
+ let typeLabel = "";
792
+ if (suffix === "^") {
793
+ addr = FV1Assembler.getMiddleAddr(mem.start, mem.size);
794
+ typeLabel = " (Middle)";
795
+ }
796
+ else if (suffix === "#") {
797
+ addr = FV1Assembler.getEndAddr(mem.start, mem.size, this.fv1AsmMemBug);
798
+ typeLabel = " (End)";
799
+ }
800
+ if (addr >= 0 && addr < this.delaySize) {
801
+ return { label: `MEM[${addr}] (${mem.name}${typeLabel})`, value: this.delayRam[addr] };
802
+ }
803
+ }
804
+ // 5. Check DELAY[idx]
805
+ if (suffix === "") {
806
+ const delayMatch = expression.match(/^DELAY\[(\d+)\]$/);
807
+ if (delayMatch) {
808
+ const idx = parseInt(delayMatch[1]);
809
+ if (idx >= 0 && idx < this.delaySize) {
810
+ return { label: `DELAY[${idx}]`, value: this.delayRam[idx] };
811
+ }
812
+ }
813
+ }
814
+ return null;
815
+ }
816
+ getState() {
817
+ return {
818
+ pc: this.pc,
819
+ acc: this.acc,
820
+ pacc: this.pacc,
821
+ lr: this.lr,
822
+ lfo: this.lfo,
823
+ // Official Register Naming
824
+ registers: Object.fromEntries(Array.from({ length: 32 + this.regCount }, (_, i) => {
825
+ let name = `[${i}]`;
826
+ if (i === 0)
827
+ name = "SIN0_RATE";
828
+ else if (i === 1)
829
+ name = "SIN0_RANGE";
830
+ else if (i === 2)
831
+ name = "SIN1_RATE";
832
+ else if (i === 3)
833
+ name = "SIN1_RANGE";
834
+ else if (i === 4)
835
+ name = "RMP0_RATE";
836
+ else if (i === 5)
837
+ name = "RMP0_RANGE";
838
+ else if (i === 6)
839
+ name = "RMP1_RATE";
840
+ else if (i === 7)
841
+ name = "RMP1_RANGE";
842
+ else if (i === 8)
843
+ name = "SIN0";
844
+ else if (i === 9)
845
+ name = "COS0";
846
+ else if (i === 10)
847
+ name = "SIN1";
848
+ else if (i === 11)
849
+ name = "COS1";
850
+ else if (i === 12)
851
+ name = "RMP0";
852
+ else if (i === 13)
853
+ name = "RMP1";
854
+ else if (i === 16)
855
+ name = "POT0";
856
+ else if (i === 17)
857
+ name = "POT1";
858
+ else if (i === 18)
859
+ name = "POT2";
860
+ else if (i === 20)
861
+ name = "ADCL";
862
+ else if (i === 21)
863
+ name = "ADCR";
864
+ else if (i === 22)
865
+ name = "DACL";
866
+ else if (i === 23)
867
+ name = "DACR";
868
+ else if (i === 24)
869
+ name = "ADDR_PTR";
870
+ else if (i >= 32 && i <= 63)
871
+ name = `REG${i - 32}`;
872
+ return [name, this.registers[i]];
873
+ })),
874
+ // Flags
875
+ flags: {
876
+ RUN: this.firstRun,
877
+ ZRC: (this.acc * this.pacc < 0),
878
+ ZRO: (this.acc === 0),
879
+ GEZ: (this.acc >= 0),
880
+ NEG: (this.acc < 0)
881
+ },
882
+ // LFO Internal positions
883
+ lfoState: {
884
+ sin0: this.sin0, cos0: this.cos0,
885
+ sin1: this.sin1, cos1: this.cos1,
886
+ rmp0: this.rmp0, rmp1: this.rmp1
887
+ }
888
+ };
889
+ }
890
+ getCycleCount() {
891
+ return this.cycleCount;
892
+ }
893
+ getReadOffsets() {
894
+ return Array.from(this.currentReadOffsets);
895
+ }
896
+ getRegisters() {
897
+ return this.registers;
898
+ }
899
+ updateLFOs() {
900
+ // Sync internal LFO modulations explicitly modified by WRAX or WRHX
901
+ this.sin0_rate = this.registers[0];
902
+ this.sin0_range = this.registers[1];
903
+ this.sin1_rate = this.registers[2];
904
+ this.sin1_range = this.registers[3];
905
+ this.rmp0_rate = this.registers[4];
906
+ this.rmp0_range = this.registers[5];
907
+ this.rmp1_rate = this.registers[6];
908
+ this.rmp1_range = this.registers[7];
909
+ // C logic update_rmp0/1
910
+ this.rmp0 -= this.rmp0_rate * (1.0 / 4096.0);
911
+ while (this.rmp0 >= 1.0)
912
+ this.rmp0 -= 1.0;
913
+ while (this.rmp0 < 0.0)
914
+ this.rmp0 += 1.0;
915
+ this.rmp1 -= this.rmp1_rate * (1.0 / 4096.0);
916
+ while (this.rmp1 >= 1.0)
917
+ this.rmp1 -= 1.0;
918
+ while (this.rmp1 < 0.0)
919
+ this.rmp1 += 1.0;
920
+ // SIN LFO update using coupled-form oscillator.
921
+ // The FV-1 hardware updates cos first, then uses the updated cos for sin.
922
+ // Clamp to [-1.0, MAX_ACC] to prevent float drift from exceeding the
923
+ // FV-1's 24-bit fixed-point register range.
924
+ const x0 = this.sin0_rate * (1.0 / 256.0);
925
+ this.cos0 += x0 * this.sin0;
926
+ this.sin0 -= x0 * this.cos0;
927
+ this.sin0 = Math.max(FV1Simulator.MIN_ACC, Math.min(FV1Simulator.MAX_ACC, this.sin0));
928
+ this.cos0 = Math.max(FV1Simulator.MIN_ACC, Math.min(FV1Simulator.MAX_ACC, this.cos0));
929
+ const x1 = this.sin1_rate * (1.0 / 256.0);
930
+ this.cos1 += x1 * this.sin1;
931
+ this.sin1 -= x1 * this.cos1;
932
+ this.sin1 = Math.max(FV1Simulator.MIN_ACC, Math.min(FV1Simulator.MAX_ACC, this.sin1));
933
+ this.cos1 = Math.max(FV1Simulator.MIN_ACC, Math.min(FV1Simulator.MAX_ACC, this.cos1));
934
+ }
935
+ updateStateRegisters() {
936
+ // ONLY update accumulators here! Rate/Range is dictated by the program loop!
937
+ // Writing registers[0..7] here would explicitly overwrite mid-frame WRAX modulations!
938
+ // LFO State Accumulators
939
+ this.registers[8] = this.sin0;
940
+ this.registers[9] = this.cos0;
941
+ this.registers[10] = this.sin1;
942
+ this.registers[11] = this.cos1;
943
+ this.registers[12] = this.rmp0;
944
+ this.registers[13] = this.rmp1;
945
+ }
946
+ getDelayRam() {
947
+ return this.delayRam;
948
+ }
949
+ // --- Helpers ---
950
+ decodeS1_14(raw) {
951
+ // 16 bits: 1 sign, 1 integer, 14 fractional
952
+ if (raw & 0x8000)
953
+ return (raw - 0x10000) / 16384.0;
954
+ return raw / 16384.0;
955
+ }
956
+ decodeS1_9(raw) {
957
+ // 11 bits: 1 sign, 1 integer, 9 fractional
958
+ if (raw & 0x400)
959
+ return (raw - 0x800) / 512.0;
960
+ return raw / 512.0;
961
+ }
962
+ decodeS_10(raw) {
963
+ // 11 bits: 1 sign, 0 integer, 10 fractional
964
+ if (raw & 0x400)
965
+ return (raw - 0x800) / 1024.0;
966
+ return raw / 1024.0;
967
+ }
968
+ decodeS4_6(raw) {
969
+ // 11 bits: 1 sign, 4 integer, 6 fractional
970
+ if (raw & 0x400)
971
+ return (raw - 0x800) / 64.0;
972
+ return raw / 64.0;
973
+ }
974
+ decodeS_15(raw) {
975
+ // 16 bits: 1 sign, 0 integer, 15 fractional
976
+ if (raw & 0x8000)
977
+ return (raw - 0x10000) / 32768.0;
978
+ return raw / 32768.0;
979
+ }
980
+ saturate(val) {
981
+ if (val > FV1Simulator.MAX_ACC)
982
+ return FV1Simulator.MAX_ACC;
983
+ if (val < FV1Simulator.MIN_ACC)
984
+ return FV1Simulator.MIN_ACC;
985
+ return val;
986
+ }
987
+ /**
988
+ * Enable trace logging with a streaming writer callback.
989
+ * Each row is emitted immediately via the writer — nothing is stored in memory.
990
+ * @param writer Callback that receives each CSV row (including header).
991
+ * @param maxCycles Maximum number of sample cycles to log (0 = unlimited).
992
+ * @param onComplete Optional callback when trace auto-completes.
993
+ */
994
+ enableTrace(writer, maxCycles = 0, onComplete) {
995
+ this.traceEnabled = true;
996
+ this.traceWriter = writer;
997
+ this.traceMaxCycles = maxCycles;
998
+ this.traceCycle = 0;
999
+ this.traceRowCount = 0;
1000
+ this.traceOnComplete = onComplete || null;
1001
+ // Emit CSV Header immediately (Reduced set for delay debugging)
1002
+ writer(`cycle,pc,opcode,delay_op,delay_offset,delay_addr,delay_ptr`);
1003
+ console.log(`[FV1 Trace] Logging enabled (maxCycles=${maxCycles || 'unlimited'})`);
1004
+ }
1005
+ /**
1006
+ * Disable trace logging.
1007
+ */
1008
+ disableTrace() {
1009
+ this.traceEnabled = false;
1010
+ this.traceWriter = null;
1011
+ console.log(`[FV1 Trace] Logging disabled. ${this.traceRowCount} rows written.`);
1012
+ }
1013
+ /**
1014
+ * Returns whether trace logging is currently active.
1015
+ */
1016
+ isTraceEnabled() {
1017
+ return this.traceEnabled;
1018
+ }
1019
+ /**
1020
+ * Returns the number of trace rows emitted so far.
1021
+ */
1022
+ getTraceRowCount() {
1023
+ return this.traceRowCount;
1024
+ }
1025
+ /**
1026
+ * Called at end-of-frame to advance the trace cycle counter.
1027
+ * If maxCycles is reached, trace is automatically disabled.
1028
+ */
1029
+ advanceTraceCycle() {
1030
+ if (!this.traceEnabled)
1031
+ return;
1032
+ this.traceCycle++;
1033
+ if (this.traceMaxCycles > 0 && this.traceCycle >= this.traceMaxCycles) {
1034
+ const onComplete = this.traceOnComplete;
1035
+ this.disableTrace();
1036
+ if (onComplete)
1037
+ onComplete();
1038
+ }
1039
+ }
1040
+ logTrace(pc, opcode, inst, accBefore) {
1041
+ if (!this.traceWriter)
1042
+ return;
1043
+ // FILTER: Only log delay memory operations
1044
+ if (this.traceDelayOp === '')
1045
+ return;
1046
+ const opName = FV1Simulator.OPCODE_NAMES[opcode] || `?${opcode.toString(16)}`;
1047
+ // Resolve CHO sub-type
1048
+ let fullOpName = opName;
1049
+ if (opcode === 0x14) {
1050
+ const mode = (inst >>> 30) & 0x3;
1051
+ if (mode === 0)
1052
+ fullOpName = 'CHO_RDA';
1053
+ else if (mode === 2) {
1054
+ // CHO_SOF does not access delay RAM, so we filter it out
1055
+ return;
1056
+ }
1057
+ else if (mode === 3)
1058
+ fullOpName = 'CHO_RDAL';
1059
+ }
1060
+ else if (opcode === 0x12) {
1061
+ // WLDS/WLDR do not access delay RAM directly in a way we track here
1062
+ return;
1063
+ }
1064
+ const row = `${this.traceCycle},${pc},${fullOpName},` +
1065
+ `${this.traceDelayOp},${this.traceDelayOp !== '' ? this.traceDelayOffset : ''},${this.traceDelayAddr >= 0 ? this.traceDelayAddr : ''},${this.delayPointer}`;
1066
+ this.traceWriter(row);
1067
+ this.traceRowCount++;
1068
+ }
1069
+ }
1070
+ FV1Simulator.MAX_ACC = 1.0 - (1.0 / 8388608.0); // 24-bit S.23: 1 - 2^-23
1071
+ FV1Simulator.MIN_ACC = -FV1Simulator.MAX_ACC; // Symmetrically bound to prevent limits bias
1072
+ // --- Trace Logging System ---
1073
+ FV1Simulator.OPCODE_NAMES = {
1074
+ 0x00: 'RDA', 0x01: 'RMPA', 0x02: 'WRA', 0x03: 'WRAP',
1075
+ 0x04: 'RDAX', 0x05: 'RDFX', 0x06: 'WRAX', 0x07: 'WRHX',
1076
+ 0x08: 'WRLX', 0x09: 'MAXX', 0x0A: 'MULX', 0x0B: 'LOG',
1077
+ 0x0C: 'EXP', 0x0D: 'SOF', 0x0E: 'AND', 0x0F: 'OR',
1078
+ 0x10: 'XOR', 0x11: 'SKP', 0x12: 'WLDx', 0x13: 'JAM',
1079
+ 0x14: 'CHO'
1080
+ };
1081
+ //# sourceMappingURL=FV1Simulator.js.map