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,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
|
+
}
|