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.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/components/CPU.js +1170 -0
- package/dist/components/CPU.js.map +1 -0
- package/dist/components/Cart.js +23 -0
- package/dist/components/Cart.js.map +1 -0
- package/dist/components/IO/Empty.js +19 -0
- package/dist/components/IO/Empty.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
- package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
- package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
- package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
- package/dist/components/IO/GPIOCard.js +597 -0
- package/dist/components/IO/GPIOCard.js.map +1 -0
- package/dist/components/IO/InputBoard.js +19 -0
- package/dist/components/IO/InputBoard.js.map +1 -0
- package/dist/components/IO/LCDCard.js +19 -0
- package/dist/components/IO/LCDCard.js.map +1 -0
- package/dist/components/IO/RAMCard.js +63 -0
- package/dist/components/IO/RAMCard.js.map +1 -0
- package/dist/components/IO/RTCCard.js +483 -0
- package/dist/components/IO/RTCCard.js.map +1 -0
- package/dist/components/IO/SerialCard.js +282 -0
- package/dist/components/IO/SerialCard.js.map +1 -0
- package/dist/components/IO/SoundCard.js +620 -0
- package/dist/components/IO/SoundCard.js.map +1 -0
- package/dist/components/IO/StorageCard.js +428 -0
- package/dist/components/IO/StorageCard.js.map +1 -0
- package/dist/components/IO/VGACard.js +9 -0
- package/dist/components/IO/VGACard.js.map +1 -0
- package/dist/components/IO/VideoCard.js +623 -0
- package/dist/components/IO/VideoCard.js.map +1 -0
- package/dist/components/IO.js +3 -0
- package/dist/components/IO.js.map +1 -0
- package/dist/components/Machine.js +310 -0
- package/dist/components/Machine.js.map +1 -0
- package/dist/components/RAM.js +24 -0
- package/dist/components/RAM.js.map +1 -0
- package/dist/components/ROM.js +23 -0
- package/dist/components/ROM.js.map +1 -0
- package/dist/index.js +441 -0
- package/dist/index.js.map +1 -0
- package/dist/tests/CPU.test.js +1626 -0
- package/dist/tests/CPU.test.js.map +1 -0
- package/dist/tests/Cart.test.js +119 -0
- package/dist/tests/Cart.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
- package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
- package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
- package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
- package/dist/tests/IO/GPIOCard.test.js +503 -0
- package/dist/tests/IO/GPIOCard.test.js.map +1 -0
- package/dist/tests/IO/RAMCard.test.js +229 -0
- package/dist/tests/IO/RAMCard.test.js.map +1 -0
- package/dist/tests/IO/RTCCard.test.js +177 -0
- package/dist/tests/IO/RTCCard.test.js.map +1 -0
- package/dist/tests/IO/SerialCard.test.js +423 -0
- package/dist/tests/IO/SerialCard.test.js.map +1 -0
- package/dist/tests/IO/SoundCard.test.js +528 -0
- package/dist/tests/IO/SoundCard.test.js.map +1 -0
- package/dist/tests/IO/StorageCard.test.js +647 -0
- package/dist/tests/IO/StorageCard.test.js.map +1 -0
- package/dist/tests/IO/VideoCard.test.js +549 -0
- package/dist/tests/IO/VideoCard.test.js.map +1 -0
- package/dist/tests/Machine.test.js +383 -0
- package/dist/tests/Machine.test.js.map +1 -0
- package/dist/tests/RAM.test.js +160 -0
- package/dist/tests/RAM.test.js.map +1 -0
- package/dist/tests/ROM.test.js +123 -0
- package/dist/tests/ROM.test.js.map +1 -0
- package/jest.config.cjs +9 -0
- package/package.json +43 -0
- package/src/components/CPU.ts +1371 -0
- package/src/components/Cart.ts +20 -0
- package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
- package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
- package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
- package/src/components/IO/GPIOCard.ts +677 -0
- package/src/components/IO/RAMCard.ts +68 -0
- package/src/components/IO/RTCCard.ts +518 -0
- package/src/components/IO/SerialCard.ts +335 -0
- package/src/components/IO/SoundCard.ts +711 -0
- package/src/components/IO/StorageCard.ts +473 -0
- package/src/components/IO/VideoCard.ts +730 -0
- package/src/components/IO.ts +11 -0
- package/src/components/Machine.ts +364 -0
- package/src/components/RAM.ts +23 -0
- package/src/components/ROM.ts +19 -0
- package/src/index.ts +474 -0
- package/src/tests/CPU.test.ts +2045 -0
- package/src/tests/Cart.test.ts +149 -0
- package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
- package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
- package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
- package/src/tests/IO/GPIOCard.test.ts +644 -0
- package/src/tests/IO/RAMCard.test.ts +284 -0
- package/src/tests/IO/RTCCard.test.ts +222 -0
- package/src/tests/IO/SerialCard.test.ts +530 -0
- package/src/tests/IO/SoundCard.test.ts +659 -0
- package/src/tests/IO/StorageCard.test.ts +787 -0
- package/src/tests/IO/VideoCard.test.ts +668 -0
- package/src/tests/Machine.test.ts +437 -0
- package/src/tests/RAM.test.ts +196 -0
- package/src/tests/ROM.test.ts +154 -0
- 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
|
+
})
|