ac6502 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/components/CPU.js +1170 -0
  4. package/dist/components/CPU.js.map +1 -0
  5. package/dist/components/Cart.js +23 -0
  6. package/dist/components/Cart.js.map +1 -0
  7. package/dist/components/IO/Empty.js +19 -0
  8. package/dist/components/IO/Empty.js.map +1 -0
  9. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
  10. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
  11. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
  12. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
  13. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
  14. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
  15. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
  16. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
  17. package/dist/components/IO/GPIOCard.js +597 -0
  18. package/dist/components/IO/GPIOCard.js.map +1 -0
  19. package/dist/components/IO/InputBoard.js +19 -0
  20. package/dist/components/IO/InputBoard.js.map +1 -0
  21. package/dist/components/IO/LCDCard.js +19 -0
  22. package/dist/components/IO/LCDCard.js.map +1 -0
  23. package/dist/components/IO/RAMCard.js +63 -0
  24. package/dist/components/IO/RAMCard.js.map +1 -0
  25. package/dist/components/IO/RTCCard.js +483 -0
  26. package/dist/components/IO/RTCCard.js.map +1 -0
  27. package/dist/components/IO/SerialCard.js +282 -0
  28. package/dist/components/IO/SerialCard.js.map +1 -0
  29. package/dist/components/IO/SoundCard.js +620 -0
  30. package/dist/components/IO/SoundCard.js.map +1 -0
  31. package/dist/components/IO/StorageCard.js +428 -0
  32. package/dist/components/IO/StorageCard.js.map +1 -0
  33. package/dist/components/IO/VGACard.js +9 -0
  34. package/dist/components/IO/VGACard.js.map +1 -0
  35. package/dist/components/IO/VideoCard.js +623 -0
  36. package/dist/components/IO/VideoCard.js.map +1 -0
  37. package/dist/components/IO.js +3 -0
  38. package/dist/components/IO.js.map +1 -0
  39. package/dist/components/Machine.js +310 -0
  40. package/dist/components/Machine.js.map +1 -0
  41. package/dist/components/RAM.js +24 -0
  42. package/dist/components/RAM.js.map +1 -0
  43. package/dist/components/ROM.js +23 -0
  44. package/dist/components/ROM.js.map +1 -0
  45. package/dist/index.js +441 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tests/CPU.test.js +1626 -0
  48. package/dist/tests/CPU.test.js.map +1 -0
  49. package/dist/tests/Cart.test.js +119 -0
  50. package/dist/tests/Cart.test.js.map +1 -0
  51. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
  52. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
  53. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
  54. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
  55. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
  56. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
  57. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
  58. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
  59. package/dist/tests/IO/GPIOCard.test.js +503 -0
  60. package/dist/tests/IO/GPIOCard.test.js.map +1 -0
  61. package/dist/tests/IO/RAMCard.test.js +229 -0
  62. package/dist/tests/IO/RAMCard.test.js.map +1 -0
  63. package/dist/tests/IO/RTCCard.test.js +177 -0
  64. package/dist/tests/IO/RTCCard.test.js.map +1 -0
  65. package/dist/tests/IO/SerialCard.test.js +423 -0
  66. package/dist/tests/IO/SerialCard.test.js.map +1 -0
  67. package/dist/tests/IO/SoundCard.test.js +528 -0
  68. package/dist/tests/IO/SoundCard.test.js.map +1 -0
  69. package/dist/tests/IO/StorageCard.test.js +647 -0
  70. package/dist/tests/IO/StorageCard.test.js.map +1 -0
  71. package/dist/tests/IO/VideoCard.test.js +549 -0
  72. package/dist/tests/IO/VideoCard.test.js.map +1 -0
  73. package/dist/tests/Machine.test.js +383 -0
  74. package/dist/tests/Machine.test.js.map +1 -0
  75. package/dist/tests/RAM.test.js +160 -0
  76. package/dist/tests/RAM.test.js.map +1 -0
  77. package/dist/tests/ROM.test.js +123 -0
  78. package/dist/tests/ROM.test.js.map +1 -0
  79. package/jest.config.cjs +9 -0
  80. package/package.json +43 -0
  81. package/src/components/CPU.ts +1371 -0
  82. package/src/components/Cart.ts +20 -0
  83. package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
  84. package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
  85. package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
  86. package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
  87. package/src/components/IO/GPIOCard.ts +677 -0
  88. package/src/components/IO/RAMCard.ts +68 -0
  89. package/src/components/IO/RTCCard.ts +518 -0
  90. package/src/components/IO/SerialCard.ts +335 -0
  91. package/src/components/IO/SoundCard.ts +711 -0
  92. package/src/components/IO/StorageCard.ts +473 -0
  93. package/src/components/IO/VideoCard.ts +730 -0
  94. package/src/components/IO.ts +11 -0
  95. package/src/components/Machine.ts +364 -0
  96. package/src/components/RAM.ts +23 -0
  97. package/src/components/ROM.ts +19 -0
  98. package/src/index.ts +474 -0
  99. package/src/tests/CPU.test.ts +2045 -0
  100. package/src/tests/Cart.test.ts +149 -0
  101. package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
  102. package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
  103. package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
  104. package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
  105. package/src/tests/IO/GPIOCard.test.ts +644 -0
  106. package/src/tests/IO/RAMCard.test.ts +284 -0
  107. package/src/tests/IO/RTCCard.test.ts +222 -0
  108. package/src/tests/IO/SerialCard.test.ts +530 -0
  109. package/src/tests/IO/SoundCard.test.ts +659 -0
  110. package/src/tests/IO/StorageCard.test.ts +787 -0
  111. package/src/tests/IO/VideoCard.test.ts +668 -0
  112. package/src/tests/Machine.test.ts +437 -0
  113. package/src/tests/RAM.test.ts +196 -0
  114. package/src/tests/ROM.test.ts +154 -0
  115. package/tsconfig.json +12 -0
@@ -0,0 +1,711 @@
1
+ import { IO } from '../IO'
2
+
3
+ /**
4
+ * MOS 6581 SID (Sound Interface Device) Emulation
5
+ *
6
+ * Register Map ($00-$1C):
7
+ * Voice 1: $00-$06
8
+ * Voice 2: $07-$0D
9
+ * Voice 3: $0E-$14
10
+ * Filter: $15-$17
11
+ * Volume: $18
12
+ * Paddle: $19-$1A (read-only)
13
+ * OSC 3: $1B (read-only)
14
+ * ENV 3: $1C (read-only)
15
+ *
16
+ * Each voice has:
17
+ * Frequency (16-bit, lo/hi)
18
+ * Pulse Width (12-bit, lo/hi)
19
+ * Control Register (waveform select, gate, sync, ring mod, test)
20
+ * Attack/Decay (4-bit each)
21
+ * Sustain/Release (4-bit each)
22
+ *
23
+ * Waveforms: Triangle, Sawtooth, Pulse, Noise
24
+ * Filter: 12-bit cutoff, resonance, low/band/high-pass, voice routing
25
+ *
26
+ * Clock rate: ~1 MHz (NTSC: 1,022,727 Hz, PAL: 985,248 Hz)
27
+ * Output: mono audio samples passed via callback to the host emulator
28
+ *
29
+ * Reference: MOS 6581 SID datasheet, reSID by Dag Lem
30
+ */
31
+
32
+ // ================================================================
33
+ // Constants
34
+ // ================================================================
35
+
36
+ /** Default SID clock rate (NTSC) */
37
+ export const SID_CLOCK_NTSC = 1022727
38
+ export const SID_CLOCK_PAL = 985248
39
+
40
+ /** Number of SID registers */
41
+ const NUM_REGISTERS = 29
42
+
43
+ /** Cycles per tick from Machine.ts ioTickInterval */
44
+ const CYCLES_PER_TICK = 128
45
+
46
+ // Register offsets within each voice (relative to voice base)
47
+ const REG_FREQ_LO = 0
48
+ const REG_FREQ_HI = 1
49
+ const REG_PW_LO = 2
50
+ const REG_PW_HI = 3
51
+ const REG_CONTROL = 4
52
+ const REG_AD = 5
53
+ const REG_SR = 6
54
+
55
+ // Control register bits
56
+ const CTRL_GATE = 0x01
57
+ const CTRL_SYNC = 0x02
58
+ const CTRL_RING_MOD = 0x04
59
+ const CTRL_TEST = 0x08
60
+ const CTRL_TRIANGLE = 0x10
61
+ const CTRL_SAWTOOTH = 0x20
62
+ const CTRL_PULSE = 0x40
63
+ const CTRL_NOISE = 0x80
64
+
65
+ // Global register addresses
66
+ const REG_FC_LO = 0x15
67
+ const REG_FC_HI = 0x16
68
+ const REG_RES_FILT = 0x17
69
+ const REG_MODE_VOL = 0x18
70
+ const REG_POTX = 0x19
71
+ const REG_POTY = 0x1A
72
+ const REG_OSC3 = 0x1B
73
+ const REG_ENV3 = 0x1C
74
+
75
+ // ADSR timing tables: cycles per increment/decrement
76
+ // Based on the real SID chip's exponential envelope counter rates
77
+ // Index = 4-bit register value (0-15)
78
+ const ATTACK_RATES: ReadonlyArray<number> = [
79
+ 2, 8, 16, 24, 38, 56, 68, 80,
80
+ 100, 250, 500, 800, 1000, 3000, 5000, 8000,
81
+ ]
82
+
83
+ const DECAY_RELEASE_RATES: ReadonlyArray<number> = [
84
+ 6, 24, 48, 72, 114, 168, 204, 240,
85
+ 300, 750, 1500, 2400, 3000, 9000, 15000, 24000,
86
+ ]
87
+
88
+ // Sustain level table: maps 4-bit value to 8-bit level
89
+ const SUSTAIN_LEVELS: ReadonlyArray<number> = [
90
+ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
91
+ 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
92
+ ]
93
+
94
+ // Envelope states
95
+ export const enum EnvelopeState {
96
+ ATTACK = 0,
97
+ DECAY = 1,
98
+ SUSTAIN = 2,
99
+ RELEASE = 3,
100
+ }
101
+
102
+ // Filter constants
103
+ const FILTER_LOWPASS = 0x10
104
+ const FILTER_BANDPASS = 0x20
105
+ const FILTER_HIGHPASS = 0x40
106
+ const FILTER_3OFF = 0x80
107
+
108
+ // ================================================================
109
+ // Voice
110
+ // ================================================================
111
+
112
+ export class SIDVoice {
113
+ // Oscillator
114
+ accumulator: number = 0 // 24-bit phase accumulator
115
+ frequency: number = 0 // 16-bit frequency
116
+ pulseWidth: number = 0 // 12-bit pulse width
117
+ control: number = 0 // Control register
118
+ prevGate: boolean = false // Previous gate state for edge detection
119
+
120
+ // Noise LFSR (23-bit, initial value)
121
+ noiseShift: number = 0x7FFFF8 // 23-bit noise shift register
122
+
123
+ // Waveform output (12-bit, 0-4095)
124
+ waveformOutput: number = 0
125
+
126
+ // Envelope
127
+ envelopeState: EnvelopeState = EnvelopeState.RELEASE
128
+ envelopeLevel: number = 0 // 0-255
129
+ envelopeCounter: number = 0 // Cycle counter for rate timing
130
+ attackRate: number = 0 // 4-bit attack value
131
+ decayRate: number = 0 // 4-bit decay value
132
+ sustainLevel: number = 0 // 4-bit sustain value
133
+ releaseRate: number = 0 // 4-bit release value
134
+
135
+ // Exponential counter for decay/release (models SID's exponential behavior)
136
+ exponentialCounter: number = 0
137
+ exponentialCounterPeriod: number = 1
138
+
139
+ reset(): void {
140
+ this.accumulator = 0
141
+ this.frequency = 0
142
+ this.pulseWidth = 0
143
+ this.control = 0
144
+ this.prevGate = false
145
+ this.noiseShift = 0x7FFFF8
146
+ this.waveformOutput = 0
147
+ this.envelopeState = EnvelopeState.RELEASE
148
+ this.envelopeLevel = 0
149
+ this.envelopeCounter = 0
150
+ this.attackRate = 0
151
+ this.decayRate = 0
152
+ this.sustainLevel = 0
153
+ this.releaseRate = 0
154
+ this.exponentialCounter = 0
155
+ this.exponentialCounterPeriod = 1
156
+ }
157
+ }
158
+
159
+ // ================================================================
160
+ // SoundCard (SID)
161
+ // ================================================================
162
+
163
+ export class SoundCard implements IO {
164
+
165
+ raiseIRQ = () => {}
166
+ raiseNMI = () => {}
167
+
168
+ /** Callback to push audio samples to the host emulator */
169
+ pushSamples?: (samples: Float32Array) => void
170
+
171
+ // ---- SID Internal State ----
172
+
173
+ /** Raw register file (write-only from CPU perspective, except reads at $19-$1C) */
174
+ private registers = new Uint8Array(NUM_REGISTERS)
175
+
176
+ /** Three oscillator voices */
177
+ private voices: [SIDVoice, SIDVoice, SIDVoice] = [
178
+ new SIDVoice(),
179
+ new SIDVoice(),
180
+ new SIDVoice(),
181
+ ]
182
+
183
+ /** Filter state */
184
+ private filterCutoff: number = 0 // 11-bit filter cutoff (register value)
185
+ private filterResonance: number = 0 // 4-bit resonance
186
+ private filterRouting: number = 0 // Which voices feed the filter (bits 0-2)
187
+ private filterMode: number = 0 // LP/BP/HP/3OFF flags
188
+ private masterVolume: number = 0 // 4-bit master volume
189
+
190
+ /** Filter integrator state (continuous) */
191
+ private filterLP: number = 0
192
+ private filterBP: number = 0
193
+ private filterHP: number = 0
194
+
195
+ /** Cycle accumulator for sample rate conversion */
196
+ private cycleAccumulator: number = 0
197
+
198
+ /** Target audio sample rate */
199
+ sampleRate: number = 44100
200
+
201
+ /** SID clock rate */
202
+ sidClock: number = SID_CLOCK_NTSC
203
+
204
+ /** Internal sample buffer for pushing to host */
205
+ private sampleBuffer: Float32Array = new Float32Array(4096)
206
+ private sampleBufferIndex: number = 0
207
+
208
+ // ================================================================
209
+ // IO Interface
210
+ // ================================================================
211
+
212
+ read(address: number): number {
213
+ const reg = address & 0x1F
214
+
215
+ switch (reg) {
216
+ case REG_POTX:
217
+ return this.registers[REG_POTX]
218
+ case REG_POTY:
219
+ return this.registers[REG_POTY]
220
+ case REG_OSC3:
221
+ // Return upper 8 bits of voice 3 waveform
222
+ return (this.voices[2].waveformOutput >> 4) & 0xFF
223
+ case REG_ENV3:
224
+ // Return voice 3 envelope level
225
+ return this.voices[2].envelopeLevel
226
+ default:
227
+ // All other SID registers are write-only
228
+ return 0
229
+ }
230
+ }
231
+
232
+ write(address: number, data: number): void {
233
+ const reg = address & 0x1F
234
+ if (reg >= NUM_REGISTERS) return
235
+
236
+ this.registers[reg] = data
237
+
238
+ // Update internal state from register writes
239
+ if (reg <= 0x14) {
240
+ // Voice registers
241
+ const voiceIndex = Math.floor(reg / 7) as 0 | 1 | 2
242
+ const voiceReg = reg % 7
243
+ this.updateVoiceRegister(voiceIndex, voiceReg, data)
244
+ } else {
245
+ // Global registers
246
+ this.updateGlobalRegister(reg, data)
247
+ }
248
+ }
249
+
250
+ tick(frequency: number): void {
251
+ // Each tick represents CYCLES_PER_TICK SID clock cycles
252
+ const cycles = CYCLES_PER_TICK
253
+
254
+ for (let c = 0; c < cycles; c++) {
255
+ this.clockOneCycle()
256
+
257
+ // Sample rate conversion: accumulate and downsample
258
+ this.cycleAccumulator += this.sampleRate
259
+ if (this.cycleAccumulator >= this.sidClock) {
260
+ this.cycleAccumulator -= this.sidClock
261
+
262
+ const sample = this.generateSample()
263
+ this.sampleBuffer[this.sampleBufferIndex++] = sample
264
+
265
+ // Buffer full - push to host
266
+ if (this.sampleBufferIndex >= this.sampleBuffer.length) {
267
+ this.flushSampleBuffer()
268
+ }
269
+ }
270
+ }
271
+
272
+ // Flush remaining samples
273
+ if (this.sampleBufferIndex > 0) {
274
+ this.flushSampleBuffer()
275
+ }
276
+ }
277
+
278
+ reset(coldStart: boolean): void {
279
+ this.registers.fill(0)
280
+ this.voices[0].reset()
281
+ this.voices[1].reset()
282
+ this.voices[2].reset()
283
+ this.filterCutoff = 0
284
+ this.filterResonance = 0
285
+ this.filterRouting = 0
286
+ this.filterMode = 0
287
+ this.masterVolume = 0
288
+ this.filterLP = 0
289
+ this.filterBP = 0
290
+ this.filterHP = 0
291
+ this.cycleAccumulator = 0
292
+ this.sampleBufferIndex = 0
293
+ }
294
+
295
+ // ================================================================
296
+ // Register Update Helpers
297
+ // ================================================================
298
+
299
+ private updateVoiceRegister(voiceIndex: number, reg: number, data: number): void {
300
+ const voice = this.voices[voiceIndex]
301
+
302
+ switch (reg) {
303
+ case REG_FREQ_LO:
304
+ voice.frequency = (voice.frequency & 0xFF00) | data
305
+ break
306
+ case REG_FREQ_HI:
307
+ voice.frequency = (voice.frequency & 0x00FF) | (data << 8)
308
+ break
309
+ case REG_PW_LO:
310
+ voice.pulseWidth = (voice.pulseWidth & 0x0F00) | data
311
+ break
312
+ case REG_PW_HI:
313
+ voice.pulseWidth = (voice.pulseWidth & 0x00FF) | ((data & 0x0F) << 8)
314
+ break
315
+ case REG_CONTROL: {
316
+ const gate = !!(data & CTRL_GATE)
317
+ const prevGate = voice.prevGate
318
+ voice.control = data
319
+
320
+ // Test bit resets accumulator and noise LFSR
321
+ if (data & CTRL_TEST) {
322
+ voice.accumulator = 0
323
+ voice.noiseShift = 0x7FFFF8
324
+ }
325
+
326
+ // Gate edge detection
327
+ if (gate && !prevGate) {
328
+ // Gate on: start attack
329
+ voice.envelopeState = EnvelopeState.ATTACK
330
+ voice.envelopeCounter = 0
331
+ voice.exponentialCounter = 0
332
+ voice.exponentialCounterPeriod = 1
333
+ } else if (!gate && prevGate) {
334
+ // Gate off: start release
335
+ voice.envelopeState = EnvelopeState.RELEASE
336
+ voice.envelopeCounter = 0
337
+ }
338
+ voice.prevGate = gate
339
+ break
340
+ }
341
+ case REG_AD:
342
+ voice.attackRate = (data >> 4) & 0x0F
343
+ voice.decayRate = data & 0x0F
344
+ break
345
+ case REG_SR:
346
+ voice.sustainLevel = (data >> 4) & 0x0F
347
+ voice.releaseRate = data & 0x0F
348
+ break
349
+ }
350
+ }
351
+
352
+ private updateGlobalRegister(reg: number, data: number): void {
353
+ switch (reg) {
354
+ case REG_FC_LO:
355
+ // Filter cutoff low 3 bits
356
+ this.filterCutoff = (this.filterCutoff & 0x7F8) | (data & 0x07)
357
+ break
358
+ case REG_FC_HI:
359
+ // Filter cutoff high 8 bits
360
+ this.filterCutoff = (this.filterCutoff & 0x07) | (data << 3)
361
+ break
362
+ case REG_RES_FILT:
363
+ this.filterResonance = (data >> 4) & 0x0F
364
+ this.filterRouting = data & 0x0F // bits 0-2: voice routing, bit 3: external input
365
+ break
366
+ case REG_MODE_VOL:
367
+ this.filterMode = data & 0xF0
368
+ this.masterVolume = data & 0x0F
369
+ break
370
+ }
371
+ }
372
+
373
+ // ================================================================
374
+ // Clock / Oscillator
375
+ // ================================================================
376
+
377
+ private clockOneCycle(): void {
378
+ for (let i = 0; i < 3; i++) {
379
+ this.clockOscillator(i)
380
+ this.clockEnvelope(i)
381
+ }
382
+ }
383
+
384
+ private clockOscillator(voiceIndex: number): void {
385
+ const voice = this.voices[voiceIndex]
386
+
387
+ // Don't clock if test bit is set
388
+ if (voice.control & CTRL_TEST) return
389
+
390
+ const prevAccBit19 = (voice.accumulator >> 19) & 1
391
+
392
+ // Advance phase accumulator (24-bit)
393
+ voice.accumulator = (voice.accumulator + voice.frequency) & 0xFFFFFF
394
+
395
+ const currAccBit19 = (voice.accumulator >> 19) & 1
396
+
397
+ // Clock noise LFSR on bit 19 transition (0->1)
398
+ if (!prevAccBit19 && currAccBit19) {
399
+ // LFSR feedback: bit 17 XOR bit 22
400
+ const bit0 = ((voice.noiseShift >> 17) ^ (voice.noiseShift >> 22)) & 1
401
+ voice.noiseShift = ((voice.noiseShift << 1) | bit0) & 0x7FFFFF
402
+ }
403
+
404
+ // Hard sync: if this voice syncs to the previous voice,
405
+ // reset accumulator when sync source MSB transitions 0->1
406
+ if (voice.control & CTRL_SYNC) {
407
+ const syncSource = this.voices[(voiceIndex + 2) % 3]
408
+ const prevMSB = ((syncSource.accumulator - syncSource.frequency) >> 23) & 1
409
+ const currMSB = (syncSource.accumulator >> 23) & 1
410
+ if (!prevMSB && currMSB) {
411
+ voice.accumulator = 0
412
+ }
413
+ }
414
+
415
+ // Generate waveform output (12-bit, 0-4095)
416
+ voice.waveformOutput = this.generateWaveform(voiceIndex)
417
+ }
418
+
419
+ private generateWaveform(voiceIndex: number): number {
420
+ const voice = this.voices[voiceIndex]
421
+ const acc = voice.accumulator
422
+ const control = voice.control
423
+
424
+ // No waveform selected: output 0
425
+ if (!(control & 0xF0)) return 0
426
+
427
+ let output = 0
428
+ let waveformCount = 0
429
+
430
+ // Triangle waveform (12-bit)
431
+ if (control & CTRL_TRIANGLE) {
432
+ let tri: number
433
+
434
+ // Ring modulation: XOR with MSB of modulating voice
435
+ if (control & CTRL_RING_MOD) {
436
+ const modVoice = this.voices[(voiceIndex + 2) % 3]
437
+ const msb = ((acc >> 23) ^ (modVoice.accumulator >> 23)) & 1
438
+ tri = msb
439
+ ? (~(acc >> 11) & 0xFFF)
440
+ : ((acc >> 11) & 0xFFF)
441
+ } else {
442
+ const msb = (acc >> 23) & 1
443
+ tri = msb
444
+ ? (~(acc >> 11) & 0xFFF)
445
+ : ((acc >> 11) & 0xFFF)
446
+ }
447
+
448
+ output |= tri
449
+ waveformCount++
450
+ }
451
+
452
+ // Sawtooth waveform (12-bit)
453
+ if (control & CTRL_SAWTOOTH) {
454
+ const saw = (acc >> 12) & 0xFFF
455
+ if (waveformCount > 0) {
456
+ output &= saw // Combined waveforms use AND (SID behavior)
457
+ } else {
458
+ output = saw
459
+ }
460
+ waveformCount++
461
+ }
462
+
463
+ // Pulse waveform (12-bit)
464
+ if (control & CTRL_PULSE) {
465
+ const testBit = !!(control & CTRL_TEST)
466
+ const pulse = testBit
467
+ ? 0xFFF // Test bit forces pulse high
468
+ : ((acc >> 12) >= voice.pulseWidth ? 0xFFF : 0x000)
469
+ if (waveformCount > 0) {
470
+ output &= pulse
471
+ } else {
472
+ output = pulse
473
+ }
474
+ waveformCount++
475
+ }
476
+
477
+ // Noise waveform (12-bit, selected bits from LFSR)
478
+ if (control & CTRL_NOISE) {
479
+ // Extract bits from noise shift register
480
+ const noise =
481
+ ((voice.noiseShift >> 12) & 0x800) |
482
+ ((voice.noiseShift >> 10) & 0x400) |
483
+ ((voice.noiseShift >> 7) & 0x200) |
484
+ ((voice.noiseShift >> 5) & 0x100) |
485
+ ((voice.noiseShift >> 4) & 0x080) |
486
+ ((voice.noiseShift >> 1) & 0x040) |
487
+ ((voice.noiseShift << 1) & 0x020) |
488
+ ((voice.noiseShift << 2) & 0x010)
489
+
490
+ if (waveformCount > 0) {
491
+ output &= noise
492
+ } else {
493
+ output = noise
494
+ }
495
+ waveformCount++
496
+ }
497
+
498
+ return output & 0xFFF
499
+ }
500
+
501
+ // ================================================================
502
+ // Envelope Generator (ADSR)
503
+ // ================================================================
504
+
505
+ private clockEnvelope(voiceIndex: number): void {
506
+ const voice = this.voices[voiceIndex]
507
+
508
+ voice.envelopeCounter++
509
+
510
+ switch (voice.envelopeState) {
511
+ case EnvelopeState.ATTACK: {
512
+ const rate = ATTACK_RATES[voice.attackRate]
513
+ if (voice.envelopeCounter >= rate) {
514
+ voice.envelopeCounter = 0
515
+ voice.envelopeLevel++
516
+ if (voice.envelopeLevel >= 0xFF) {
517
+ voice.envelopeLevel = 0xFF
518
+ voice.envelopeState = EnvelopeState.DECAY
519
+ voice.envelopeCounter = 0
520
+ voice.exponentialCounter = 0
521
+ this.updateExponentialPeriod(voice)
522
+ }
523
+ }
524
+ break
525
+ }
526
+
527
+ case EnvelopeState.DECAY: {
528
+ const rate = DECAY_RELEASE_RATES[voice.decayRate]
529
+ if (voice.envelopeCounter >= rate) {
530
+ voice.envelopeCounter = 0
531
+ voice.exponentialCounter++
532
+
533
+ if (voice.exponentialCounter >= voice.exponentialCounterPeriod) {
534
+ voice.exponentialCounter = 0
535
+
536
+ if (voice.envelopeLevel > SUSTAIN_LEVELS[voice.sustainLevel]) {
537
+ voice.envelopeLevel--
538
+ this.updateExponentialPeriod(voice)
539
+ }
540
+
541
+ if (voice.envelopeLevel <= SUSTAIN_LEVELS[voice.sustainLevel]) {
542
+ voice.envelopeLevel = SUSTAIN_LEVELS[voice.sustainLevel]
543
+ voice.envelopeState = EnvelopeState.SUSTAIN
544
+ }
545
+ }
546
+ }
547
+ break
548
+ }
549
+
550
+ case EnvelopeState.SUSTAIN:
551
+ // Sustain stays at level until gate off
552
+ // Level tracks changes to sustain register
553
+ voice.envelopeLevel = SUSTAIN_LEVELS[voice.sustainLevel]
554
+ break
555
+
556
+ case EnvelopeState.RELEASE: {
557
+ const rate = DECAY_RELEASE_RATES[voice.releaseRate]
558
+ if (voice.envelopeCounter >= rate) {
559
+ voice.envelopeCounter = 0
560
+ voice.exponentialCounter++
561
+
562
+ if (voice.exponentialCounter >= voice.exponentialCounterPeriod) {
563
+ voice.exponentialCounter = 0
564
+
565
+ if (voice.envelopeLevel > 0) {
566
+ voice.envelopeLevel--
567
+ this.updateExponentialPeriod(voice)
568
+ }
569
+ }
570
+ }
571
+ break
572
+ }
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Update the exponential counter period based on current envelope level.
578
+ * The real SID uses an exponential curve for decay/release by varying
579
+ * the counter period at specific envelope level thresholds.
580
+ */
581
+ private updateExponentialPeriod(voice: SIDVoice): void {
582
+ if (voice.envelopeLevel >= 0x5D) {
583
+ voice.exponentialCounterPeriod = 1
584
+ } else if (voice.envelopeLevel >= 0x36) {
585
+ voice.exponentialCounterPeriod = 2
586
+ } else if (voice.envelopeLevel >= 0x1A) {
587
+ voice.exponentialCounterPeriod = 4
588
+ } else if (voice.envelopeLevel >= 0x0E) {
589
+ voice.exponentialCounterPeriod = 8
590
+ } else if (voice.envelopeLevel >= 0x06) {
591
+ voice.exponentialCounterPeriod = 16
592
+ } else if (voice.envelopeLevel >= 0x00) {
593
+ voice.exponentialCounterPeriod = 30
594
+ }
595
+ }
596
+
597
+ // ================================================================
598
+ // Sample Generation & Filter
599
+ // ================================================================
600
+
601
+ private generateSample(): number {
602
+ let filtered = 0
603
+ let direct = 0
604
+
605
+ for (let i = 0; i < 3; i++) {
606
+ const voice = this.voices[i]
607
+
608
+ // Voice output: waveform (12-bit) * envelope (8-bit)
609
+ // Center around zero: subtract 0x800 from waveform to make it bipolar
610
+ const waveform = voice.waveformOutput - 0x800
611
+ const output = (waveform * voice.envelopeLevel) / 256
612
+
613
+ // Voice 3 mute (3OFF bit) - mutes voice 3 from audio but envelope still runs
614
+ if (i === 2 && (this.filterMode & FILTER_3OFF) && !(this.filterRouting & (1 << 2))) {
615
+ continue
616
+ }
617
+
618
+ // Route to filter or direct output
619
+ if (this.filterRouting & (1 << i)) {
620
+ filtered += output
621
+ } else {
622
+ direct += output
623
+ }
624
+ }
625
+
626
+ // Apply state-variable filter (SVF)
627
+ const cutoffFreq = this.computeFilterCutoff()
628
+ const w0 = (2 * Math.PI * cutoffFreq) / this.sampleRate
629
+ const f = Math.min(w0, 0.9) // Clamp to avoid instability
630
+
631
+ // Resonance: Q ranges from ~0.7 to ~20 as register goes 0-15
632
+ const q = 1.0 / (1.0 - (this.filterResonance / 17.0))
633
+
634
+ // State variable filter update
635
+ this.filterHP = filtered - this.filterLP - (this.filterBP / q)
636
+ this.filterBP += f * this.filterHP
637
+ this.filterLP += f * this.filterBP
638
+
639
+ // Sum selected filter outputs
640
+ let filterOutput = 0
641
+ if (this.filterMode & FILTER_LOWPASS) filterOutput += this.filterLP
642
+ if (this.filterMode & FILTER_BANDPASS) filterOutput += this.filterBP
643
+ if (this.filterMode & FILTER_HIGHPASS) filterOutput += this.filterHP
644
+
645
+ // Mix filtered and direct, apply master volume
646
+ const mixed = (filterOutput + direct) * (this.masterVolume / 15)
647
+
648
+ // Normalize to -1..1 range
649
+ const normalized = mixed / 4096
650
+
651
+ // Clamp
652
+ return Math.max(-1, Math.min(1, normalized))
653
+ }
654
+
655
+ /**
656
+ * Convert the 11-bit filter cutoff register value to a frequency in Hz.
657
+ * The SID's cutoff mapping is complex and varies between chips.
658
+ * This approximation covers the usable range (~30 Hz to ~12 kHz).
659
+ */
660
+ private computeFilterCutoff(): number {
661
+ const fc = this.filterCutoff
662
+ if (fc === 0) return 30
663
+
664
+ // Piecewise approximation matching 6581 filter characteristics
665
+ const base = 30
666
+ const maxFreq = 12000
667
+ const normalized = fc / 2047
668
+ return base + (maxFreq - base) * normalized * normalized
669
+ }
670
+
671
+ // ================================================================
672
+ // Sample Buffer Management
673
+ // ================================================================
674
+
675
+ private flushSampleBuffer(): void {
676
+ if (this.pushSamples && this.sampleBufferIndex > 0) {
677
+ const samples = this.sampleBuffer.subarray(0, this.sampleBufferIndex)
678
+ this.pushSamples(new Float32Array(samples))
679
+ }
680
+ this.sampleBufferIndex = 0
681
+ }
682
+
683
+ // ================================================================
684
+ // Getters for testing / debug
685
+ // ================================================================
686
+
687
+ /** Get a voice for inspection */
688
+ getVoice(index: number): SIDVoice {
689
+ return this.voices[index]
690
+ }
691
+
692
+ /** Get current register value */
693
+ getRegister(reg: number): number {
694
+ return this.registers[reg & 0x1F]
695
+ }
696
+
697
+ /** Get current filter cutoff frequency in Hz */
698
+ getFilterCutoffHz(): number {
699
+ return this.computeFilterCutoff()
700
+ }
701
+
702
+ /** Get master volume (0-15) */
703
+ getMasterVolume(): number {
704
+ return this.masterVolume
705
+ }
706
+
707
+ /** Get filter routing bitmask */
708
+ getFilterRouting(): number {
709
+ return this.filterRouting
710
+ }
711
+ }