@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.
- package/dist/assembler/AlgebraicCompiler.d.ts +74 -0
- package/dist/assembler/AlgebraicCompiler.d.ts.map +1 -0
- package/dist/assembler/AlgebraicCompiler.js +473 -0
- package/dist/assembler/AlgebraicCompiler.js.map +1 -0
- package/dist/assembler/FV1Assembler.d.ts +71 -0
- package/dist/assembler/FV1Assembler.d.ts.map +1 -0
- package/dist/assembler/FV1Assembler.js +343 -0
- package/dist/assembler/FV1Assembler.js.map +1 -0
- package/dist/assembler/FV1Encoder.d.ts +18 -0
- package/dist/assembler/FV1Encoder.d.ts.map +1 -0
- package/dist/assembler/FV1Encoder.js +226 -0
- package/dist/assembler/FV1Encoder.js.map +1 -0
- package/dist/assembler/FV1Parser.d.ts +87 -0
- package/dist/assembler/FV1Parser.d.ts.map +1 -0
- package/dist/assembler/FV1Parser.js +285 -0
- package/dist/assembler/FV1Parser.js.map +1 -0
- package/dist/hex/IntelHexParser.d.ts +19 -0
- package/dist/hex/IntelHexParser.d.ts.map +1 -0
- package/dist/hex/IntelHexParser.js +184 -0
- package/dist/hex/IntelHexParser.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/simulator/FV1Simulator.d.ts +206 -0
- package/dist/simulator/FV1Simulator.d.ts.map +1 -0
- package/dist/simulator/FV1Simulator.js +1081 -0
- package/dist/simulator/FV1Simulator.js.map +1 -0
- package/package.json +30 -0
|
@@ -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
|