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,659 @@
1
+ import { SoundCard, SIDVoice, EnvelopeState, SID_CLOCK_NTSC } from '../../components/IO/SoundCard'
2
+
3
+ // Voice register offsets (relative to voice base)
4
+ const VOICE1_BASE = 0x00
5
+ const VOICE2_BASE = 0x07
6
+ const VOICE3_BASE = 0x0E
7
+
8
+ // Register offsets within each voice
9
+ const REG_FREQ_LO = 0
10
+ const REG_FREQ_HI = 1
11
+ const REG_PW_LO = 2
12
+ const REG_PW_HI = 3
13
+ const REG_CONTROL = 4
14
+ const REG_AD = 5
15
+ const REG_SR = 6
16
+
17
+ // Global registers
18
+ const REG_FC_LO = 0x15
19
+ const REG_FC_HI = 0x16
20
+ const REG_RES_FILT = 0x17
21
+ const REG_MODE_VOL = 0x18
22
+ const REG_POTX = 0x19
23
+ const REG_POTY = 0x1A
24
+ const REG_OSC3 = 0x1B
25
+ const REG_ENV3 = 0x1C
26
+
27
+ // Control register bits
28
+ const CTRL_GATE = 0x01
29
+ const CTRL_SYNC = 0x02
30
+ const CTRL_RING_MOD = 0x04
31
+ const CTRL_TEST = 0x08
32
+ const CTRL_TRIANGLE = 0x10
33
+ const CTRL_SAWTOOTH = 0x20
34
+ const CTRL_PULSE = 0x40
35
+ const CTRL_NOISE = 0x80
36
+
37
+ /**
38
+ * Helper: tick the SoundCard for a given number of macro-ticks
39
+ * Each tick processes 128 SID clock cycles internally
40
+ */
41
+ const tickN = (sid: SoundCard, n: number): void => {
42
+ for (let i = 0; i < n; i++) {
43
+ sid.tick(SID_CLOCK_NTSC)
44
+ }
45
+ }
46
+
47
+ describe('SoundCard (MOS 6581 SID)', () => {
48
+
49
+ let sid: SoundCard
50
+
51
+ beforeEach(() => {
52
+ sid = new SoundCard()
53
+ sid.sampleRate = 44100
54
+ sid.sidClock = SID_CLOCK_NTSC
55
+ })
56
+
57
+ // ================================================================
58
+ // Initialization & Reset
59
+ // ================================================================
60
+
61
+ describe('initialization', () => {
62
+ test('should initialize with all registers zero', () => {
63
+ for (let i = 0; i < 29; i++) {
64
+ expect(sid.getRegister(i)).toBe(0)
65
+ }
66
+ })
67
+
68
+ test('should initialize with zero master volume', () => {
69
+ expect(sid.getMasterVolume()).toBe(0)
70
+ })
71
+
72
+ test('should initialize voices in release state with zero level', () => {
73
+ for (let i = 0; i < 3; i++) {
74
+ const voice = sid.getVoice(i)
75
+ expect(voice.envelopeLevel).toBe(0)
76
+ expect(voice.envelopeState).toBe(EnvelopeState.RELEASE)
77
+ expect(voice.frequency).toBe(0)
78
+ expect(voice.pulseWidth).toBe(0)
79
+ expect(voice.control).toBe(0)
80
+ }
81
+ })
82
+ })
83
+
84
+ describe('reset', () => {
85
+ test('should clear all registers on reset', () => {
86
+ // Write some register values
87
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0xAB)
88
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0xCD)
89
+ sid.write(REG_MODE_VOL, 0x1F)
90
+
91
+ sid.reset(true)
92
+
93
+ for (let i = 0; i < 29; i++) {
94
+ expect(sid.getRegister(i)).toBe(0)
95
+ }
96
+ expect(sid.getMasterVolume()).toBe(0)
97
+ })
98
+
99
+ test('should reset all voice state', () => {
100
+ // Configure a voice and gate on
101
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0xFF)
102
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0xFF)
103
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
104
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
105
+ tickN(sid, 10)
106
+
107
+ sid.reset(true)
108
+
109
+ const voice = sid.getVoice(0)
110
+ expect(voice.accumulator).toBe(0)
111
+ expect(voice.frequency).toBe(0)
112
+ expect(voice.envelopeLevel).toBe(0)
113
+ expect(voice.envelopeState).toBe(EnvelopeState.RELEASE)
114
+ })
115
+ })
116
+
117
+ // ================================================================
118
+ // Register Read / Write
119
+ // ================================================================
120
+
121
+ describe('register writes', () => {
122
+
123
+ test('should set voice 1 frequency (16-bit)', () => {
124
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0x34)
125
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x12)
126
+ expect(sid.getVoice(0).frequency).toBe(0x1234)
127
+ })
128
+
129
+ test('should set voice 2 frequency', () => {
130
+ sid.write(VOICE2_BASE + REG_FREQ_LO, 0xCD)
131
+ sid.write(VOICE2_BASE + REG_FREQ_HI, 0xAB)
132
+ expect(sid.getVoice(1).frequency).toBe(0xABCD)
133
+ })
134
+
135
+ test('should set voice 3 frequency', () => {
136
+ sid.write(VOICE3_BASE + REG_FREQ_LO, 0xFF)
137
+ sid.write(VOICE3_BASE + REG_FREQ_HI, 0xFF)
138
+ expect(sid.getVoice(2).frequency).toBe(0xFFFF)
139
+ })
140
+
141
+ test('should set pulse width (12-bit)', () => {
142
+ sid.write(VOICE1_BASE + REG_PW_LO, 0xFF)
143
+ sid.write(VOICE1_BASE + REG_PW_HI, 0x0F)
144
+ expect(sid.getVoice(0).pulseWidth).toBe(0xFFF)
145
+ })
146
+
147
+ test('should mask pulse width high byte to 4 bits', () => {
148
+ sid.write(VOICE1_BASE + REG_PW_LO, 0x00)
149
+ sid.write(VOICE1_BASE + REG_PW_HI, 0xFF) // Only lower 4 bits should matter
150
+ expect(sid.getVoice(0).pulseWidth).toBe(0x0F00)
151
+ })
152
+
153
+ test('should set attack/decay', () => {
154
+ sid.write(VOICE1_BASE + REG_AD, 0xA5)
155
+ const voice = sid.getVoice(0)
156
+ expect(voice.attackRate).toBe(0x0A)
157
+ expect(voice.decayRate).toBe(0x05)
158
+ })
159
+
160
+ test('should set sustain/release', () => {
161
+ sid.write(VOICE1_BASE + REG_SR, 0xC3)
162
+ const voice = sid.getVoice(0)
163
+ expect(voice.sustainLevel).toBe(0x0C)
164
+ expect(voice.releaseRate).toBe(0x03)
165
+ })
166
+
167
+ test('should set control register', () => {
168
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
169
+ expect(sid.getVoice(0).control).toBe(CTRL_SAWTOOTH | CTRL_GATE)
170
+ })
171
+
172
+ test('should set master volume', () => {
173
+ sid.write(REG_MODE_VOL, 0x0F)
174
+ expect(sid.getMasterVolume()).toBe(15)
175
+ })
176
+
177
+ test('should set filter cutoff (11-bit)', () => {
178
+ sid.write(REG_FC_LO, 0x07) // Low 3 bits
179
+ sid.write(REG_FC_HI, 0xFF) // High 8 bits
180
+ // Result: (0xFF << 3) | 0x07 = 0x7FF = 2047
181
+ expect(sid.getFilterCutoffHz()).toBeGreaterThan(30)
182
+ })
183
+
184
+ test('should set filter resonance and routing', () => {
185
+ sid.write(REG_RES_FILT, 0xF7) // Resonance=15, route voices 1-3
186
+ expect(sid.getFilterRouting()).toBe(0x07)
187
+ })
188
+
189
+ test('should ignore writes to addresses >= 29', () => {
190
+ sid.write(0x1D, 0xFF) // Out of range
191
+ // Should not crash
192
+ })
193
+ })
194
+
195
+ describe('register reads', () => {
196
+ test('should return 0 for write-only registers', () => {
197
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0xFF)
198
+ expect(sid.read(VOICE1_BASE + REG_FREQ_LO)).toBe(0)
199
+ })
200
+
201
+ test('should read POTX', () => {
202
+ // POTX is stored in register file
203
+ expect(sid.read(REG_POTX)).toBe(0)
204
+ })
205
+
206
+ test('should read POTY', () => {
207
+ expect(sid.read(REG_POTY)).toBe(0)
208
+ })
209
+
210
+ test('should read OSC3 (voice 3 waveform output)', () => {
211
+ // Initially 0
212
+ expect(sid.read(REG_OSC3)).toBe(0)
213
+ })
214
+
215
+ test('should read ENV3 (voice 3 envelope level)', () => {
216
+ expect(sid.read(REG_ENV3)).toBe(0)
217
+ })
218
+
219
+ test('should read ENV3 after voice 3 attack', () => {
220
+ // Set voice 3 to fastest attack, sawtooth waveform
221
+ sid.write(VOICE3_BASE + REG_FREQ_HI, 0x10)
222
+ sid.write(VOICE3_BASE + REG_AD, 0x00) // fastest attack
223
+ sid.write(VOICE3_BASE + REG_SR, 0xF0) // max sustain
224
+ sid.write(VOICE3_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
225
+
226
+ // Tick enough for the envelope to rise
227
+ tickN(sid, 50)
228
+
229
+ expect(sid.read(REG_ENV3)).toBeGreaterThan(0)
230
+ })
231
+
232
+ test('should wrap register address to 5 bits', () => {
233
+ // Address 0x3B should wrap to 0x1B (OSC3)
234
+ expect(sid.read(0x3B)).toBe(sid.read(REG_OSC3))
235
+ })
236
+ })
237
+
238
+ // ================================================================
239
+ // Gate / Envelope
240
+ // ================================================================
241
+
242
+ describe('envelope generator', () => {
243
+
244
+ test('should start attack on gate on', () => {
245
+ sid.write(VOICE1_BASE + REG_AD, 0x00) // fastest attack/decay
246
+ sid.write(VOICE1_BASE + REG_SR, 0xF0) // max sustain
247
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
248
+
249
+ const voice = sid.getVoice(0)
250
+ expect(voice.envelopeState).toBe(EnvelopeState.ATTACK)
251
+ })
252
+
253
+ test('should increase envelope level during attack', () => {
254
+ sid.write(VOICE1_BASE + REG_AD, 0x00) // fastest attack
255
+ sid.write(VOICE1_BASE + REG_SR, 0xF0) // max sustain
256
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
257
+
258
+ tickN(sid, 10)
259
+
260
+ expect(sid.getVoice(0).envelopeLevel).toBeGreaterThan(0)
261
+ })
262
+
263
+ test('should reach max level and transition to decay', () => {
264
+ sid.write(VOICE1_BASE + REG_AD, 0x00) // fastest attack/decay
265
+ sid.write(VOICE1_BASE + REG_SR, 0x80) // sustain=8, release=0
266
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
267
+
268
+ // Tick enough times for attack to complete (fastest = 2 cycles per step, 255 steps)
269
+ tickN(sid, 20)
270
+
271
+ const voice = sid.getVoice(0)
272
+ // Should have reached 255 and started decay
273
+ expect(voice.envelopeLevel).toBeLessThanOrEqual(255)
274
+ })
275
+
276
+ test('should transition to release on gate off', () => {
277
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
278
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
279
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
280
+
281
+ tickN(sid, 20) // Let attack progress
282
+
283
+ // Gate off
284
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE) // gate off
285
+
286
+ const voice = sid.getVoice(0)
287
+ expect(voice.envelopeState).toBe(EnvelopeState.RELEASE)
288
+ })
289
+
290
+ test('should decrease envelope during release', () => {
291
+ // Gate on with fast attack
292
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
293
+ sid.write(VOICE1_BASE + REG_SR, 0xF0) // max sustain, fastest release
294
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
295
+
296
+ tickN(sid, 20)
297
+
298
+ const levelBeforeRelease = sid.getVoice(0).envelopeLevel
299
+ expect(levelBeforeRelease).toBeGreaterThan(0)
300
+
301
+ // Gate off
302
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE)
303
+
304
+ tickN(sid, 20)
305
+
306
+ expect(sid.getVoice(0).envelopeLevel).toBeLessThan(levelBeforeRelease)
307
+ })
308
+ })
309
+
310
+ // ================================================================
311
+ // Control Register Features
312
+ // ================================================================
313
+
314
+ describe('control register', () => {
315
+
316
+ test('test bit should reset accumulator', () => {
317
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0xFF)
318
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
319
+ tickN(sid, 5)
320
+
321
+ // Set test bit
322
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE | CTRL_TEST)
323
+ expect(sid.getVoice(0).accumulator).toBe(0)
324
+ })
325
+
326
+ test('test bit should reset noise LFSR', () => {
327
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_NOISE | CTRL_GATE)
328
+ tickN(sid, 10)
329
+
330
+ // Set test bit
331
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_NOISE | CTRL_GATE | CTRL_TEST)
332
+ expect(sid.getVoice(0).noiseShift).toBe(0x7FFFF8)
333
+ })
334
+
335
+ test('should handle multiple waveform selection', () => {
336
+ // Select both triangle and sawtooth (combined waveforms use AND)
337
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
338
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_SAWTOOTH | CTRL_GATE)
339
+ sid.write(REG_MODE_VOL, 0x0F)
340
+
341
+ tickN(sid, 5)
342
+
343
+ // Should not crash and voice should produce output
344
+ const voice = sid.getVoice(0)
345
+ expect(voice.waveformOutput).toBeDefined()
346
+ })
347
+ })
348
+
349
+ // ================================================================
350
+ // Oscillator / Waveforms
351
+ // ================================================================
352
+
353
+ describe('oscillator', () => {
354
+
355
+ test('accumulator should advance by frequency each cycle', () => {
356
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0x00)
357
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x01) // freq = 256
358
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
359
+
360
+ // After one tick (128 cycles), accumulator should be 256 * 128 = 32768
361
+ tickN(sid, 1)
362
+
363
+ const voice = sid.getVoice(0)
364
+ expect(voice.accumulator).toBe(256 * 128)
365
+ })
366
+
367
+ test('accumulator should wrap at 24 bits', () => {
368
+ sid.write(VOICE1_BASE + REG_FREQ_LO, 0xFF)
369
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0xFF) // max frequency
370
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
371
+
372
+ // Many ticks should cause wrapping
373
+ tickN(sid, 200)
374
+
375
+ const voice = sid.getVoice(0)
376
+ expect(voice.accumulator).toBeLessThan(0x1000000)
377
+ })
378
+
379
+ test('sawtooth should produce non-zero output', () => {
380
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
381
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
382
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
383
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
384
+ sid.write(REG_MODE_VOL, 0x0F)
385
+
386
+ tickN(sid, 10)
387
+
388
+ const voice = sid.getVoice(0)
389
+ // Sawtooth = acc >> 12, with some frequency should be non-zero
390
+ expect(voice.waveformOutput).toBeGreaterThanOrEqual(0)
391
+ })
392
+
393
+ test('pulse waveform should depend on pulse width', () => {
394
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
395
+ sid.write(VOICE1_BASE + REG_PW_LO, 0x00)
396
+ sid.write(VOICE1_BASE + REG_PW_HI, 0x08) // 50% duty cycle (0x800)
397
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
398
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
399
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_PULSE | CTRL_GATE)
400
+
401
+ tickN(sid, 20)
402
+
403
+ // Pulse output should be either 0x000 or 0xFFF
404
+ const voice = sid.getVoice(0)
405
+ const output = voice.waveformOutput
406
+ expect(output === 0x000 || output === 0xFFF).toBe(true)
407
+ })
408
+
409
+ test('no waveform selected should output 0', () => {
410
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
411
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_GATE) // gate on but no waveform
412
+
413
+ tickN(sid, 5)
414
+
415
+ expect(sid.getVoice(0).waveformOutput).toBe(0)
416
+ })
417
+ })
418
+
419
+ // ================================================================
420
+ // Filter
421
+ // ================================================================
422
+
423
+ describe('filter', () => {
424
+
425
+ test('should set filter cutoff from registers', () => {
426
+ sid.write(REG_FC_LO, 0x07)
427
+ sid.write(REG_FC_HI, 0xFF)
428
+
429
+ expect(sid.getFilterCutoffHz()).toBeGreaterThan(30)
430
+ })
431
+
432
+ test('should return minimum cutoff for zero register value', () => {
433
+ sid.write(REG_FC_LO, 0x00)
434
+ sid.write(REG_FC_HI, 0x00)
435
+
436
+ expect(sid.getFilterCutoffHz()).toBe(30)
437
+ })
438
+
439
+ test('should set filter voice routing', () => {
440
+ sid.write(REG_RES_FILT, 0x03) // Route voices 1 and 2 to filter
441
+ expect(sid.getFilterRouting()).toBe(0x03)
442
+ })
443
+
444
+ test('should set filter mode in volume register', () => {
445
+ sid.write(REG_MODE_VOL, 0x1F) // Low-pass, volume=15
446
+ expect(sid.getMasterVolume()).toBe(15)
447
+ })
448
+ })
449
+
450
+ // ================================================================
451
+ // Audio Output / pushSamples Callback
452
+ // ================================================================
453
+
454
+ describe('audio output', () => {
455
+
456
+ test('should call pushSamples during tick', () => {
457
+ const receivedSamples: Float32Array[] = []
458
+ sid.pushSamples = (samples) => {
459
+ receivedSamples.push(new Float32Array(samples))
460
+ }
461
+
462
+ // Set up a voice producing sound
463
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
464
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
465
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
466
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
467
+ sid.write(REG_MODE_VOL, 0x0F)
468
+
469
+ tickN(sid, 100)
470
+
471
+ // Should have received some sample data
472
+ expect(receivedSamples.length).toBeGreaterThan(0)
473
+
474
+ // Samples should be valid floats
475
+ const firstBatch = receivedSamples[0]
476
+ for (let i = 0; i < firstBatch.length; i++) {
477
+ expect(firstBatch[i]).toBeGreaterThanOrEqual(-1)
478
+ expect(firstBatch[i]).toBeLessThanOrEqual(1)
479
+ }
480
+ })
481
+
482
+ test('should produce silence with zero volume', () => {
483
+ const receivedSamples: Float32Array[] = []
484
+ sid.pushSamples = (samples) => {
485
+ receivedSamples.push(new Float32Array(samples))
486
+ }
487
+
488
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
489
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
490
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
491
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
492
+ sid.write(REG_MODE_VOL, 0x00) // Volume = 0
493
+
494
+ tickN(sid, 100)
495
+
496
+ // All samples should be near zero (silence)
497
+ for (const batch of receivedSamples) {
498
+ for (let i = 0; i < batch.length; i++) {
499
+ expect(Math.abs(batch[i])).toBeLessThan(0.001)
500
+ }
501
+ }
502
+ })
503
+
504
+ test('should not call pushSamples if callback not set', () => {
505
+ sid.pushSamples = undefined
506
+
507
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
508
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
509
+ sid.write(REG_MODE_VOL, 0x0F)
510
+
511
+ // Should not throw
512
+ expect(() => tickN(sid, 10)).not.toThrow()
513
+ })
514
+
515
+ test('should produce sound from all three voices simultaneously', () => {
516
+ const receivedSamples: Float32Array[] = []
517
+ sid.pushSamples = (samples) => {
518
+ receivedSamples.push(new Float32Array(samples))
519
+ }
520
+
521
+ // Voice 1: triangle
522
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
523
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
524
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
525
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_TRIANGLE | CTRL_GATE)
526
+
527
+ // Voice 2: sawtooth
528
+ sid.write(VOICE2_BASE + REG_FREQ_HI, 0x20)
529
+ sid.write(VOICE2_BASE + REG_AD, 0x00)
530
+ sid.write(VOICE2_BASE + REG_SR, 0xF0)
531
+ sid.write(VOICE2_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
532
+
533
+ // Voice 3: pulse
534
+ sid.write(VOICE3_BASE + REG_FREQ_HI, 0x30)
535
+ sid.write(VOICE3_BASE + REG_PW_HI, 0x08)
536
+ sid.write(VOICE3_BASE + REG_AD, 0x00)
537
+ sid.write(VOICE3_BASE + REG_SR, 0xF0)
538
+ sid.write(VOICE3_BASE + REG_CONTROL, CTRL_PULSE | CTRL_GATE)
539
+
540
+ sid.write(REG_MODE_VOL, 0x0F)
541
+
542
+ tickN(sid, 100)
543
+
544
+ expect(receivedSamples.length).toBeGreaterThan(0)
545
+
546
+ // Check that some samples are non-zero (sound is being produced)
547
+ const hasNonZero = receivedSamples.some(batch => {
548
+ for (let i = 0; i < batch.length; i++) {
549
+ if (Math.abs(batch[i]) > 0.001) return true
550
+ }
551
+ return false
552
+ })
553
+ expect(hasNonZero).toBe(true)
554
+ })
555
+ })
556
+
557
+ // ================================================================
558
+ // Multi-voice & Special Features
559
+ // ================================================================
560
+
561
+ describe('special features', () => {
562
+
563
+ test('voice 3 mute (3OFF) should suppress voice 3 audio', () => {
564
+ const samplesWithVoice3: Float32Array[] = []
565
+ const samplesWithout: Float32Array[] = []
566
+
567
+ // First: voice 3 unmuted
568
+ sid.write(VOICE3_BASE + REG_FREQ_HI, 0x20)
569
+ sid.write(VOICE3_BASE + REG_AD, 0x00)
570
+ sid.write(VOICE3_BASE + REG_SR, 0xF0)
571
+ sid.write(VOICE3_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
572
+ sid.write(REG_MODE_VOL, 0x0F) // No 3OFF
573
+
574
+ sid.pushSamples = (samples) => {
575
+ samplesWithVoice3.push(new Float32Array(samples))
576
+ }
577
+ tickN(sid, 50)
578
+
579
+ // Now mute voice 3 (set 3OFF = 0x80 in mode/vol)
580
+ sid.reset(true)
581
+ sid.write(VOICE3_BASE + REG_FREQ_HI, 0x20)
582
+ sid.write(VOICE3_BASE + REG_AD, 0x00)
583
+ sid.write(VOICE3_BASE + REG_SR, 0xF0)
584
+ sid.write(VOICE3_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE)
585
+ sid.write(REG_MODE_VOL, 0x8F) // 3OFF + volume=15
586
+
587
+ sid.pushSamples = (samples) => {
588
+ samplesWithout.push(new Float32Array(samples))
589
+ }
590
+ tickN(sid, 50)
591
+
592
+ // The muted version should have less total energy
593
+ const energy = (batches: Float32Array[]) => {
594
+ let sum = 0
595
+ for (const b of batches) {
596
+ for (let i = 0; i < b.length; i++) sum += b[i] * b[i]
597
+ }
598
+ return sum
599
+ }
600
+
601
+ const energyWith = energy(samplesWithVoice3)
602
+ const energyWithout = energy(samplesWithout)
603
+
604
+ // When voice 3 is muted via 3OFF and NOT routed to filter, it should be silent
605
+ expect(energyWithout).toBeLessThan(energyWith)
606
+ })
607
+
608
+ test('should handle rapid gate on/off', () => {
609
+ sid.write(VOICE1_BASE + REG_FREQ_HI, 0x10)
610
+ sid.write(VOICE1_BASE + REG_AD, 0x00)
611
+ sid.write(VOICE1_BASE + REG_SR, 0xF0)
612
+ sid.write(REG_MODE_VOL, 0x0F)
613
+
614
+ // Rapid gate toggling
615
+ for (let i = 0; i < 20; i++) {
616
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH | CTRL_GATE) // on
617
+ tickN(sid, 2)
618
+ sid.write(VOICE1_BASE + REG_CONTROL, CTRL_SAWTOOTH) // off
619
+ tickN(sid, 2)
620
+ }
621
+
622
+ // Should not crash or produce invalid state
623
+ const voice = sid.getVoice(0)
624
+ expect(voice.envelopeLevel).toBeLessThanOrEqual(255)
625
+ expect(voice.envelopeLevel).toBeGreaterThanOrEqual(0)
626
+ })
627
+ })
628
+
629
+ // ================================================================
630
+ // IO Interface Compliance
631
+ // ================================================================
632
+
633
+ describe('IO interface', () => {
634
+
635
+ test('should have raiseIRQ and raiseNMI callbacks', () => {
636
+ expect(sid.raiseIRQ).toBeDefined()
637
+ expect(sid.raiseNMI).toBeDefined()
638
+ expect(typeof sid.raiseIRQ).toBe('function')
639
+ expect(typeof sid.raiseNMI).toBe('function')
640
+ })
641
+
642
+ test('read should handle any address value', () => {
643
+ // Should not throw for any address
644
+ for (let addr = 0; addr < 64; addr++) {
645
+ expect(() => sid.read(addr)).not.toThrow()
646
+ }
647
+ })
648
+
649
+ test('write should handle any address/data combination', () => {
650
+ for (let addr = 0; addr < 64; addr++) {
651
+ expect(() => sid.write(addr, 0xFF)).not.toThrow()
652
+ }
653
+ })
654
+
655
+ test('tick should not throw when called repeatedly', () => {
656
+ expect(() => tickN(sid, 100)).not.toThrow()
657
+ })
658
+ })
659
+ })