# v2 sound: SID-inspired, deliberately not a SID emulator This is new, dependency-free synthesis code. It does **not** reproduce a MOS 6581/8580, load `.sid` files, or claim an authentic rendition of classic music. Original examples and separately GPL-licensed Galway ports use this chip; see each music example’s linked source, license and attribution. A native player port does not turn this synthesizer into an authentic SID emulator. ## Register contract Byte address = `0x40 + offset`. Voices 1–3 start at offsets 0, 7 and 14. Each voice has seven byte registers: | Relative offset | Meaning | | --- | --- | | 0, 1 | Frequency word, low byte then high byte | | 2, 3 | Pulse width, low byte then low four bits of high byte (0–4095) | | 4 | Control: gate=1, sync=2, ring=4, test=8, triangle=16, saw=32, pulse=64, noise=128 | | 5 | Attack (high nibble), decay (low nibble) | | 6 | Sustain (high nibble), release (low nibble) | | Global offset | Meaning | | --- | --- | | 21, 22 | 11-bit filter cutoff: low three bits then high eight bits | | 23 | Resonance high nibble; route voices 1–3 through filter with bits 0–2 | | 24 | Master volume low nibble; lowpass=16, bandpass=32, highpass=64, bypassed voice-3 mute=128 | | 25, 26 | Reserved, no paddle inputs | | 27 (`0x5b`) | **Extension:** sine-enable mask, bits 0–2 for voices 1–3; not SID OSC3 readback | | 28–31 | Reserved | Reserved writes are accepted but have no audible effect. This is the original SID's 25 writable-register layout plus a separate sine extension; noise remains control bit 7. There is no external filter input, oscillator/envelope readback, SID bus behavior, or register mirroring in this module. `frequencyHz = frequencyWord * 985248 / 2^24` (PAL SID convention). For A4=440 Hz, use `round(440 * 2^24 / 985248)`. Pulse duty is width/4096. Voice output is scaled by 1/3, summed mono, master-volume scaled, and clamped to [-1,1] before storing `Float32` samples. Multiple enabled waveforms are averaged, not the SID's nonlinear combined-waveform behavior. No anti-aliasing is applied. Gate rising starts attack from the current envelope; gate falling starts release. Attack is linear 0→1 over the selected rate, decay/release are linear full-scale ramps; sustain is nibble/15. Rate durations in milliseconds: - Attack: 2, 8, 16, 24, 38, 56, 68, 80, 100, 250, 500, 800, 1000, 3000, 5000, 8000. - Decay/release: 6, 24, 48, 72, 114, 168, 204, 240, 300, 750, 1500, 2400, 3000, 9000, 15000, 24000. These approximate documented SID rates, **not** SID counter timing, exponential segments, delay bugs or gate quirks. Sustain increases can raise the envelope immediately. Test resets/holds phase, resets the noise seed and silences that voice. Sync resets a voice on the preceding voice's phase wrap (voice1←3, 2←1, 3←2). Ring modulation flips the triangle sign on the source's half-cycle. Both are sample-rate approximations, without cycle-accurate simultaneous-sync rules. Noise is a deterministic 23-bit LFSR, seeded `0x7fffff`, taps 22 xor 17, clocked 16 times per oscillator period; its full state maps linearly to bipolar output. This is not the SID's selected-tap output or combined-waveform noise behavior. The filter is two bounded one-pole stages, exponential coefficient, log cutoff 30–12000 Hz (capped at 0.4×sampleRate), with negative feedback 0–0.85. Lowpass is stage2, bandpass stage1−stage2, highpass input−stage1; selected modes sum. It is intentionally stable/simple, not a model of either SID analog circuit. No volume-register DC offset/digi sample trick, analog distortion, or chip aging. ## Producer and event timing `new SoundDevice(48000)`, `reset()`, `write(offset, byte)`, and `render(count, events?) → Float32Array`. Events are `{sampleOffset, register, value}`, ordered by integer sampleOffset; same-offset writes retain array/retirement order. An event applies **before** its boundary sample. Invalid event arrays fail before rendering any samples. Registers are offsets 0–31; finite values truncate to byte. The synth retains oscillator/envelope/filter state between blocks. Supported synth/output sample rates are 8000–384000 Hz; invalid rates fail rather than producing nonfinite phases or unbounded noise loops. At the 120 MHz VM clock, a 48 kHz sample spans 2500 cycles. Producer conversion: `sampleOffset = ceil((event.cycle - blockStartCycle) / 2500)`. This quantizes writes to the first sample boundary at or after the event, not video-frame boundaries. The producer owns events beyond the block: defer offsets >=count to the next block, retaining original order. A 400-sample block spans 1,000,000 cycles (1/120 second). Neither browser callbacks nor video frames need to be 120 Hz. On run/reset/pause, coordinate VM, synth, outstanding-event and worklet queue resets; never reuse old program audio. ## iPhone playback routing On explicit Run with Sound enabled, request `navigator.audioSession.type = "playback"` when supported, before creating/resuming Web Audio. Safari's ambient Web Audio session can otherwise remain inaudible in iPhone Silent Mode even while its audio-playing icon appears. The playback session is a native API, not a silent-media-element workaround; it can pause other media on the device. Unsupported or rejected requests fall back to ordinary Web Audio, with the session result visible in the running status. Hardware volume and output routing remain under user/platform control; this cannot prove speaker audibility. See [WebKit bug 237322](https://bugs.webkit.org/show_bug.cgi?id=237322), comment 6, and [MDN AudioSession.type](https://developer.mozilla.org/en-US/docs/Web/API/AudioSession/type). ## AudioWorklet transport Bundle `audio-worklet.ts` as a browser entry (its `./sound` import must be bundled), load with `audioContext.audioWorklet.addModule(url)`, then create an `AudioWorkletNode(context, 'fanco-sound', {outputChannelCount:[1]})`. Send `{type:'samples', samples:Float32Array}` with `[samples.buffer]` transfer. Input is always 48000 Hz mono; block sizes are not constrained to 400. Browser 128-sample output calls at actual context rate (including 44100) are linearly resampled with retained fractional phase. Additional output channels copy mono. The bounded queue holds at most 24000 source samples (0.5 seconds); overflow rejects the newest whole block and increments `droppedBlocks`. Nonfinite input samples become zero. Underrun writes zeros, clears incomplete history, and does not loop stale audio. Keep ~2400 samples/50 ms ahead to avoid underrun without long latency. Linear interpolation is not a high-quality anti-aliasing resampler. Messages `{type:'reset'}` flush, zero counters and unpause; `{type:'pause'}` flushes and ignores samples; `{type:'resume'}` unpauses without restoring discarded audio. The worklet reports immediately on startup/reset, then about every 20 ms while unpaused: `{type:'need', queuedSamples, receivedSamples, underrunSamples, droppedBlocks}`. `receivedSamples` counts delivered valid Float32Array lengths, **including overflow rejections**, since reset; it does not count ignored paused messages. Underrun counts output silence samples, not consumed source input. For race-resistant producer refill, estimate `queuedSamples + sentSamples - receivedSamples` plus requested/inflight samples; counters and sent count must share a reset generation. Malformed/oversized messages are rejected; this is a local browser transport, not a network-facing decoder. Worklet remains alive during silence. ## Filter revision driven by Wizball (2026-10-01) The original two-one-pole approximation defined LP=state2, BP=state1-state2 and HP=input-state1. Enabling all three modes therefore returned the unfiltered input exactly, at every resonance setting. Wizball’s HVSC title wrapper writes mode/volume `$7F`, exposing that cancellation. The revised filter is an ideal, topology-preserving state-variable filter with two integrator histories and distinct lowpass, bandpass and highpass outputs. Cutoff retains the experimental exponential 30–12,000 Hz mapping (capped at 0.4×sample rate). Resonance maps damping from 2 to 0.4, equivalent to Q=0.5..2.5. Coefficients are cached until cutoff or resonance changes. Summed modes can now retain a resonant response; they are not guaranteed to act as a neutral bypass. Every voice uses a fixed gain of 1/6 instead of the old 1/3, leaving a uniform 6 dB mixer budget for resonant peaks without changing routed/direct voice balance. This is fixed headroom, not adaptive normalization or a compressor. Existing patches are therefore 6 dB quieter at the same master volume. Mixed output is still hard-limited to [-1,1] as a final safety bound. The 235-second title render has no clipped samples and peaks at 0.773 with this mapping; arbitrary other programs can still exceed the headroom. This fixes a demonstrated FANCO behavior bug, **not** a measured 6581 filter model: its analog cutoff curve, nonlinearities, chip variation and DAC bias are not emulated. The register layout and timestamped-write contract are unchanged. Waveform averaging and linear envelopes remain separate known approximations; no speculative new chip registers were added for this port. ## Classic SID playback research (2026-10-01) - **Primary file-format specification:** HVSC, . PSID contains C64 binary data and init/play addresses; the play routine is machine code, not a portable sequence of notes. RSID explicitly requires a real C64-like environment, including interrupts and CIA/VIC setup. Flags can specify PAL/NTSC, 6581/8580 and multiple SIDs. Some MUS-format exceptions need an external C64 player merged; they are not native v2 code either. - **Register reference:** (secondary reference, links original MOS datasheets). Used to verify bit assignments and source-voice routing; no code or textual table copied into the implementation. - **HVSC rights notice:** . Indexed copyright notice states that HVSC tunes are copyrighted by authors/software companies. Free download and preservation are not a permissive license to redistribute/port. The page's readable fetch returns a browser-support notice; do not treat it as a verified blanket redistribution permission. - **First-person composer interview:** Mark Cooksey interviewed by Neil Carr, 2001-05-17, . Cooksey recalls Hubbard creating the Commando music at Elite; he distinguishes his own loading music and describes PWM, variable filters and cover versions. This is provenance/context, **not** a license for Commando or other music. For Hubbard's Commando or music by Martin Galway/Ben Daglish, merely remapping `$d400` to `0x40` cannot execute the original 6510 player on v2. Viable future paths: (1) emulate the 6510 and required C64 timing/devices, then send timestamped writes into an appropriate synth; (2) extract a timestamped register trace using an existing emulator and replay it, retaining sub-frame timing; or (3) manually reimplement composition/instruments as v2 code. Each path needs appropriate music rights. This approximate synth will still differ from a real SID even if register timing is correct. No classic tune has been ported or bundled here. No clearly permissive classic tune was verified during this research. Use an original SID-inspired demo motif and identify it as such. Do not vendor GPL SID emulator code, or copyrighted tune data, without explicit integration/licensing review and consent. This implementation has no such bundled dependencies. ## Verification `bun test v2/sound.test.ts` covers PAL pitch, gate/envelope, deterministic noise, block continuity, boundary event ordering, filter extremes, sync/ring behavior, sine/test, queue capacity/underrun/reset, 44100 interpolation, arbitrary chunk sizes, and worklet startup/periodic counters/pause/reset via mocked platform globals. Browser AudioWorklet integration and audible demo quality need end-to-end checks; the mocks do not claim those checks have happened.