Skip to content

feat(emu): Mockingboard A/C sound card — 6522 VIA + AY-3-8910 PSG (GH #66)#91

Merged
relmer merged 7 commits into
masterfrom
018-mockingboard-support
Jul 12, 2026
Merged

feat(emu): Mockingboard A/C sound card — 6522 VIA + AY-3-8910 PSG (GH #66)#91
relmer merged 7 commits into
masterfrom
018-mockingboard-support

Conversation

@relmer

@relmer relmer commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Implements GH #66 — Mockingboard A/C sound card emulation, the de-facto Apple II audio standard.

What's in it

Two clean-room chip cores, written from the datasheets (no GPL emulator code copied):

  • 6522 VIA (Via6522) — full register file, Timer 1 (one-shot + continuous), Timer 2, IFR/IER with a level-sensitive IRQ line, ports A/B + DDRs. Generic/reusable: it knows nothing about the Mockingboard.
  • AY-3-8910 PSG (Ay8910) — 3 square-wave tone channels, 17-bit-LFSR noise, 16-level envelope, logarithmic DAC → float32 PCM resampled to the host rate. Frequencies match the datasheet formulas (validated by test).
  • MockingboardCard (MemoryDevice) — aggregates 2×VIA + 2×PSG in a slot's $Cn00 page (address bit 7 selects the VIA), translating each VIA's port A/B into the AY data bus + BC1/BDIR/RESET control lines.

Integration

  • IRQs — VIA Timer 1 (continuous mode) drives the existing InterruptController — the tempo source Mockingboard music players use. New InterruptController::ResetSources reclaims IRQ tokens on machine teardown so machine switches don't leak the 32-source pool.
  • Audio — the two PSGs mix to stereo (PSG fix(ops): ShiftLeft/ShiftRight dispatch calls RotateLeft/RotateRight instead of correct methods #1 hard-left, PSG refactor(core): replace CRT assert with EHM ASSERT and remove unused headers #2 hard-right) through a dedicated DriveAudioMixer, so the Options toggle is independent of Drive Audio. WasapiAudio::SubmitFrame sums it into the speaker bus with per-channel clamping (no clipping).
  • Machine configmockingboard registered in ComponentRegistry; installed in slot 4 of the Apple ][+ and //e profiles (JSON versions bumped). On the //e, CxxxRomRouter now delegates a slot's $Cn00 page to an active I/O card when INTCXROM=0, so the card is reachable behind the MMU.
  • Options toggle — a live "Mockingboard" mute checkbox threaded through the settings/persistence stack, mirroring the Drive Audio toggle. (The card can also be removed entirely from Settings → Machine.)

Verification

  • 35 new tests (11 VIA + 12 AY + 10 card + 2 router-delegation); full suite 2081 passing.
  • Build 0 errors, code analysis clean (warnings-as-errors).
  • Smoke-tested: both ][+ (bus-resident) and //e (router-delegated) boot with the Mockingboard installed, no crash.
  • Version.h1.7.0; CHANGELOG + README updated.

Deferred (per #66 scope)

SSI-263 speech, Phasor mode-switch, Mockingboard D (//c serial), and AY volume-modulation PCM hacks.

Closes #66

relmer added 7 commits July 11, 2026 09:23
…66)

Clean-room from the datasheets (no GPL emulator code copied):

- 6522 VIA: full register file, Timer 1 (one-shot + continuous) and
  Timer 2, IFR/IER with a level-sensitive IRQ line, ports A/B + DDRs.
  Generic and reusable — the card wires it, but it knows nothing about
  the Mockingboard.
- AY-3-8910 PSG: three square-wave tone channels, a 17-bit-LFSR noise
  generator, a 16-level envelope generator, and a logarithmic DAC
  rendered to float32 PCM resampled to the host rate via a fractional
  accumulator. Frequencies match the datasheet formulas.
- MockingboardCard: aggregates 2x VIA + 2x PSG in a slot's $Cn00 page
  (address bit 7 selects the VIA), translating each VIA's port A/B into
  the AY data bus and BC1/BDIR/RESET control lines.

Integration:
- VIA Timer 1 IRQs feed the existing InterruptController (the tempo
  source music players rely on). InterruptController::ResetSources
  reclaims tokens on machine teardown so switches don't leak the pool.
- The two PSGs mix to stereo (PSG #1 hard-left, PSG #2 hard-right)
  through a dedicated DriveAudioMixer so the Options toggle is
  independent of Drive Audio; WasapiAudio::SubmitFrame sums it into the
  speaker bus with per-channel clamping.
- Installed in slot 4 of the Apple ][+ and //e profiles. On the //e the
  CxxxRomRouter now delegates a slot's $Cn00 page to an active I/O card
  when INTCXROM=0 so the card is reachable behind the MMU.
- Live "Mockingboard" mute toggle threaded through the settings /
  persistence stack, mirroring the Drive Audio toggle.

Tests: 11 VIA + 12 AY + 10 card + 2 router-delegation; full suite 2081
passing, build clean, code analysis clean. Version 1.7.0.
The emulator run loop (ExecuteCpuSlices plus both single-step paths)
drove the CPU exclusively through EmuCpu::StepOne(), which executes one
opcode without checking the interrupt line -- the interrupt-aware
Cpu6502::Step() was never called. So no device interrupt was ever
serviced during emulation. This stayed latent because the Mockingboard
is the first device in Casso to assert a maskable IRQ; the keyboard,
Disk II and speaker never do. A Mockingboard music player would arm its
Timer1 IRQ and hang waiting on a handler that never ran.

Add Cpu6502::TryStepInterrupt(): dispatches a pending NMI edge or an
unmasked IRQ via DispatchVector, records the 7-cycle cost in
m_lastCycles, and (unlike TryDispatchInterrupt) leaves m_totalCycles to
the caller's AddCycles -- so an interrupt step and an instruction step
advance the counters through one identical path, with no double-count.
The unit-tested Step()/TryDispatchInterrupt() path is untouched. Gate
all three run-loop step sites on it: if TryStepInterrupt() dispatched,
skip StepOne() for that step.

Regression coverage (CpuIrqTests) -- the gap that let this ship:
  - three host-loop cases driving the exact StepOne pattern the shell
    uses (IRQ serviced, I-flag honored, NMI edge), and
  - an end-to-end Via6522 -> InterruptController -> 6502 test proving a
    Timer1 IRQ vectors the CPU through the StepOne loop.

Full suite 2085/2085; clean build; validated audibly with the
IRQ-driven Mockingboard test disk.
Two hand-crafted bootable //e disks that program the slot-4 Mockingboard
directly, for isolating the host audio path from any game's own card
detection:

  - mockingboard-test: programs AY channel A for a sustained tone and
    spins forever. Proves VIA -> AY -> audio source -> mixer -> WASAPI
    end to end with direct register writes.

  - mockingboard-irq-test: arms Timer1 in continuous mode, installs a
    handler at the Apple II user IRQ vector ($03FE) that sweeps tone-A
    each interrupt, and drops the drive motor. Reproduces the exact
    mechanism real music players use (IRQ-driven AY writes) and validates
    the interrupt fix audibly.

Sources (.a65) assemble with CassoCli; the .dsk files are the laid-out
143,360-byte boot images (boot code at track 0, sector 0). Both appear
in the disk picker via the bundled-demo scan.
Rework CxxxRomRouter to the project EHM conventions:

  - SetSlotRom / SetSlotIoDevice / HasSlotRom now assert an in-range slot
    index (CBRAEx E_INVALIDARG) instead of silently ignoring it. An
    out-of-range slot is a wiring/config bug, not a runtime value.
  - SlotIoDeviceFor and ResolveByte collapse their multiple returns into a
    single exit via BAIL_OUT_IF guards and an Error: label.
  - Write drops its early return for an if/else. Behaviour is identical: a
    delegated slot page and the $CFFF INTC8ROM clear are mutually exclusive
    by address.

Also names the address-decode magic numbers and normalises spacing. No
behaviour change; MMU/router tests unaffected.
…dards pass

Coding-standard and robustness pass over the Mockingboard device layer
(Ay8910, Via6522, MockingboardCard):

  - Single-exit: EnvelopeStep, SyncPsg, Tick / TickTimer1 / TickTimer2 and
    UpdateIrq lose their guard/early returns.
  - Assert on genuinely-unsupported conditions instead of ignoring them.
    Ay8910: GenerateSample requires a configured sample rate (E_UNEXPECTED),
    WriteRegister/ReadRegister require an in-range register (E_INVALIDARG).
    Via6522 WriteRegister flags unmodelled ACR features (T2 pulse counting,
    shift register, PB7 output, port latching) and any PCR handshake config
    (E_INVALIDARG). The register value is still stored first, so release
    builds stay graceful while debug builds surface a title that depends on
    something we do not model.
  - WriteData/ReadData keep their out-of-range-latch guards: a program
    latching an AY address >= 16 is inert on real hardware, so faithfully
    ignoring it is correct, not a bug to assert on.
  - PowerCycle / Create use UNREFERENCED_PARAMETER for their
    interface-mandated but unused Prng / MemoryBus arguments.

Plus blank-line and empty-paren spacing fixes. No behaviour change under
test; full suite green.
The Mockingboard audio-enable toggle was misplaced on the Disk settings
page. Remove it and its entire pref/command plumbing; the card's slot
entry in the Hardware tab's device tree is now the single control, and
audio follows card presence (the mixer goes silent with no registered
sources).

Drops the mockingboardEnabled pref, SetMockingboard, the ApplyMockingboard
sink method, the IDM_AUDIO_MOCKINGBOARD_ENABLE/DISABLE command ids and
their shell handler, and the DiskPage toggle.

Also, in the same file: wrap the audio SubmitFrame's discarded HRESULT in
IGNORE_RETURN_VALUE.
Follow-up to the maskable-IRQ fix (ab1192d):

  - No space before empty parens on the authored lines.
  - Trim the over-long TryStepInterrupt doc comment on the .h declaration
    (the full block lives in the .cpp).
  - Name the end-to-end test's loop bounds (kNopFieldBytes / kMaxHostSteps).
  - SetInterruptLine drops its early return for a single-exit if/else.
@relmer
relmer merged commit 8f8e161 into master Jul 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(emu): Mockingboard sound card support (6522 VIA + AY-3-8910 PSG)

1 participant