feat(emu): Mockingboard A/C sound card — 6522 VIA + AY-3-8910 PSG (GH #66)#91
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):
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.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).MemoryDevice) — aggregates 2×VIA + 2×PSG in a slot's$Cn00page (address bit 7 selects the VIA), translating each VIA's port A/B into the AY data bus + BC1/BDIR/RESET control lines.Integration
InterruptController— the tempo source Mockingboard music players use. NewInterruptController::ResetSourcesreclaims IRQ tokens on machine teardown so machine switches don't leak the 32-source pool.DriveAudioMixer, so the Options toggle is independent of Drive Audio.WasapiAudio::SubmitFramesums it into the speaker bus with per-channel clamping (no clipping).mockingboardregistered inComponentRegistry; installed in slot 4 of the Apple ][+ and //e profiles (JSON versions bumped). On the //e,CxxxRomRouternow delegates a slot's$Cn00page to an active I/O card whenINTCXROM=0, so the card is reachable behind the MMU.Verification
Version.h→ 1.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