Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions Apple2/Demos/mockingboard-irq-test.a65
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
; ============================================================================
; mockingboard-irq-test.a65 -- IRQ-driven Mockingboard smoke test
;
; Reproduces the mechanism real Mockingboard music players (e.g. Rescue
; Raiders) actually use: a 6522 Timer1 continuous interrupt whose handler
; writes the AY registers. This exercises the full chain that the plain
; tone test does NOT -- CPU interrupt dispatch + IRQ-handler AY writes +
; audio -- so a clean glide here means the only thing left that could
; keep a real game silent is that game's own card detection.
;
; What you should hear: a steady tone whose pitch glides downward ~once a
; second (the handler bumps tone-A's period each interrupt), with NO disk
; motor hum (we switch the drive off first).
;
; Boot: slot-6 Disk2.rom loads track 0 sector 0 to $0800 and JMPs $0801.
;
; Interrupt path: hardware IRQ -> ROM handler saves A at $45, sees the B
; flag clear, and JMP ($03FE) -- the Apple II user IRQ vector we install
; below. Our handler acknowledges Timer1, rewrites the AY, restores A and
; RTIs.
; ============================================================================

.org $0801

; -- Slot 4 Mockingboard VIA #1 (direct registers) --------------------------
ORB = $C400 ; port B: AY control lines
ORA = $C401 ; port A: AY register number / data
DDRB = $C402
DDRA = $C403
T1CL = $C404 ; Timer1 counter low (read clears the T1 flag)
T1CH = $C405 ; Timer1 counter high (write loads + arms)
ACR = $C40B ; auxiliary control (Timer1 mode)
IER = $C40E ; interrupt enable

; -- AY control-line states on port B ---------------------------------------
LATCH = $07 ; BDIR|BC1|/RESET -> latch register address
WRITE = $06 ; BDIR|/RESET -> write data byte
IDLE = $04 ; /RESET only -> bus idle, chip live

; -- AY-3-8910 register numbers ---------------------------------------------
R_TONEA_LO = $00
R_TONEA_HI = $01
R_MIXER = $07
R_AMP_A = $08

; -- VIA control values -----------------------------------------------------
T1_CONT = $40 ; ACR bit 6: Timer1 continuous (periodic IRQ)
IER_T1 = $C0 ; IER: set-bit ($80) | Timer1 enable ($40)

; -- Apple II system locations ----------------------------------------------
IRQV = $03FE ; user IRQ vector the ROM JMPs through
MOTOROFF = $C0E8 ; slot 6 drive motor OFF
ACCSAVE = $45 ; ROM IRQ handler stashes A here

; -- Zero-page scratch ------------------------------------------------------
savex = $06
savey = $07
sweep = $08


start
sei ; hold off interrupts while we wire things up

lda MOTOROFF ; drop the drive motor so only the AY is audible

; VIA ports to outputs, AY control idle with /RESET released.
lda #$FF
sta DDRB
sta DDRA
lda #IDLE
sta ORB

; Initial voice: tone A enabled, period $0100, full fixed volume.
lda #R_MIXER
ldx #$3E
jsr writeay
lda #R_TONEA_HI
ldx #$01
jsr writeay
lda #R_TONEA_LO
ldx #$00
jsr writeay
lda #R_AMP_A
ldx #$0F
jsr writeay

lda #$00
sta sweep

; Install the interrupt handler at the Apple II user IRQ vector.
lda #irqhandler & $FF
sta IRQV
lda #irqhandler >> 8
sta IRQV+1

; Arm Timer1: continuous mode, T1 interrupt enabled, period $1000
; (~4 KHz phi2 / period -> roughly 250 interrupts per second).
lda #T1_CONT
sta ACR
lda #IER_T1
sta IER
lda #$00
sta T1CL ; latch low
lda #$10
sta T1CH ; load counter = $1000, arm, start counting

cli ; interrupts on -- Timer1 now drives the handler

spin
jmp spin ; foreground idles; the ISR does all the work


; ----------------------------------------------------------------------------
; writeay -- latch AY register (A) then write data (X) through the VIA bus.
; A = register number, X = value. Clobbers A; preserves X and Y.
; ----------------------------------------------------------------------------
writeay
sta ORA
lda #LATCH
sta ORB
lda #IDLE
sta ORB
stx ORA
lda #WRITE
sta ORB
lda #IDLE
sta ORB
rts


; ----------------------------------------------------------------------------
; irqhandler -- Timer1 continuous IRQ. The ROM handler already saved A at $45
; and vectored here. Acknowledge the timer, glide the pitch by
; rewriting tone-A fine, then restore and RTI.
; ----------------------------------------------------------------------------
irqhandler
stx savex
sty savey
lda T1CL ; read T1C-L clears the Timer1 flag, de-asserts IRQ
inc sweep
lda #R_TONEA_LO
ldx sweep
jsr writeay
ldy savey
ldx savex
lda ACCSAVE ; restore A saved by the ROM IRQ handler
rti

; --- end ---
Binary file added Apple2/Demos/mockingboard-irq-test.dsk
Binary file not shown.
92 changes: 92 additions & 0 deletions Apple2/Demos/mockingboard-test.a65
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
; ============================================================================
; mockingboard-test.a65 -- minimal Mockingboard tone smoke test
;
; A single boot-sector program that UNCONDITIONALLY programs the slot-4
; Mockingboard's first AY-3-8910 (via VIA #1 at $C400) for a sustained
; square-wave tone on channel A, then spins forever. There is no card
; detection and no game logic: if you hear a steady tone, the host
; Mockingboard audio path (VIA -> AY -> audio source -> mixer -> WASAPI)
; works end-to-end. Silence means the fault is on the host side; a
; real game (e.g. Rescue Raiders) going silent while this tone plays
; isolates the problem to that title's own card detection.
;
; Boot: the slot-6 Disk2.rom firmware reads track 0 sector 0 into
; $0800-$08FF and JMPs $0801 -- same contract casso-rocks.a65 relies on.
;
; AY write protocol (mirrors MockingboardCard SyncPsg control lines):
; PB0 = BC1, PB1 = BDIR, PB2 = /RESET (high = released).
; latch address : BDIR|BC1|/RESET = $07
; write data : BDIR|/RESET = $06
; idle : /RESET = $04
; Register number / data byte are driven on port A (DDRA = $FF).
; ============================================================================

.org $0801

; -- Slot 4 Mockingboard VIA #1 registers -----------------------------------
ORB = $C400 ; port B: AY control lines (BC1/BDIR//RESET)
ORA = $C401 ; port A: AY register number / data byte
DDRB = $C402
DDRA = $C403

; -- AY control-line states on port B ---------------------------------------
LATCH = $07 ; BDIR=1 BC1=1 /RESET=1 -> latch register address
WRITE = $06 ; BDIR=1 BC1=0 /RESET=1 -> write data byte
IDLE = $04 ; BDIR=0 BC1=0 /RESET=1 -> bus idle, chip live

; -- AY-3-8910 register numbers ---------------------------------------------
R_TONEA_LO = $00 ; channel A tone period, fine
R_TONEA_HI = $01 ; channel A tone period, coarse (low 4 bits)
R_MIXER = $07 ; 0 = enabled; bit0 tone A, bit3 noise A ...
R_AMP_A = $08 ; bits 0-3 = level; bit4 = follow envelope


start
; Both VIA ports to outputs, control lines idle with /RESET released
; (releases the AY from power-on reset so it will accept writes).
lda #$FF
sta DDRB
sta DDRA
lda #IDLE
sta ORB

; Mixer: enable tone A only (bit0=0), everything else disabled ($3E).
lda #R_MIXER
ldx #$3E
jsr writeay

; Tone A period = $0100 -> a clear low-mid square wave.
lda #R_TONEA_LO
ldx #$00
jsr writeay
lda #R_TONEA_HI
ldx #$01
jsr writeay

; Channel A amplitude = full fixed volume (envelope bit clear).
lda #R_AMP_A
ldx #$0F
jsr writeay

spin
jmp spin ; hold forever; the AY keeps sounding on its own


; ----------------------------------------------------------------------------
; writeay -- latch AY register (A) then write data (X) through the VIA bus.
; A = AY register number on entry, X = value to write. Clobbers A.
; ----------------------------------------------------------------------------
writeay
sta ORA ; register number onto port A
lda #LATCH
sta ORB ; BDIR|BC1 -> AY latches the address
lda #IDLE
sta ORB ; back to idle
stx ORA ; data byte onto port A
lda #WRITE
sta ORB ; BDIR -> AY writes the latched register
lda #IDLE
sta ORB ; back to idle
rts

; --- end ---
Binary file added Apple2/Demos/mockingboard-test.dsk
Binary file not shown.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
Versioned entries use `MAJOR.MINOR.PATCH` from [Version.h](CassoCore/Version.h).
Entries before versioning was introduced use dates only.

## [1.7.0] — Mockingboard sound card (GH #66)

### Added
- **feat(emu): Mockingboard A/C sound card emulation.** Adds the de-facto
Apple II audio standard: two clean-room chip cores written from the
datasheets — a generic **6522 VIA** (full register file, Timer 1
one-shot/continuous, Timer 2, IFR/IER and a level-sensitive IRQ line,
ports A/B with data-direction registers) and an **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). A **MockingboardCard** aggregates two of
each in a slot's `$Cn00` I/O page, decoding address bit 7 to the two VIAs
and translating each VIA's port A/B into the AY data bus and BC1/BDIR/RESET
control lines. Timer 1 IRQs — the tempo source Mockingboard music players
rely on — feed the existing interrupt controller, and the two PSGs mix
into the stereo bus (PSG #1 hard-left, PSG #2 hard-right) through a
dedicated audio mixer so they sum cleanly with the speaker and Disk II
audio. The card is installed in slot 4 of the Apple ][+ and //e profiles
and can be muted live via **Settings → Disk → Mockingboard** (mirroring
the Drive Audio toggle) or removed entirely from **Settings → Machine**.
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. Clean-room throughout: no GPL emulator code was copied.

## [1.6.4] — Disk ][ Debug panel virtualization (GH #88)

### Fixed
Expand Down
54 changes: 45 additions & 9 deletions Casso/EmulatorShell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "Devices/Apple2eSoftSwitchBank.h"
#include "Devices/AppleSpeaker.h"
#include "Devices/Disk2Controller.h"
#include "Devices/Mockingboard/MockingboardCard.h"
#include "Devices/LanguageCard.h"
#include "Devices/Apple2eMmu.h"
#include "Core/Prng.h"
Expand Down Expand Up @@ -2887,6 +2888,14 @@ void EmulatorShell::OnCpuThreadStart()
HRESULT hrLoad = m_driveAudioMixer.SetMechanism (m_driveAudioMixer.GetMechanism());
IGNORE_RETURN_VALUE (hrLoad, S_OK);
}

// The initial machine is built before WASAPI comes up, so its PSGs
// do not yet know the host sample rate. Seed it now (machine switches
// after this point pick it up at build time in MachineManager).
if (m_wasapiAudio.IsInitialized() && m_refs.mockingboard != nullptr)
{
m_refs.mockingboard->SetSampleRate (m_wasapiAudio.GetSampleRate());
}
}


Expand Down Expand Up @@ -2965,13 +2974,21 @@ void EmulatorShell::DispatchCpuCommand (const EmulatorCommand & cmd)
{
if (m_cpu)
{
m_cpu->StepOne();
if (!m_cpu->TryStepInterrupt())
{
m_cpu->StepOne();
}

if (m_refs.diskController != nullptr)
{
m_refs.diskController->Tick (m_cpu->GetLastInstructionCycles());
}

if (m_refs.mockingboard != nullptr)
{
m_refs.mockingboard->Tick (m_cpu->GetLastInstructionCycles());
}

if (m_refs.keyboard != nullptr)
{
m_refs.keyboard->Tick (m_cpu->GetLastInstructionCycles());
Expand Down Expand Up @@ -3213,13 +3230,21 @@ void EmulatorShell::StepInstructionWhilePaused()
return;
}

m_cpu->StepOne();
if (!m_cpu->TryStepInterrupt())
{
m_cpu->StepOne();
}

if (m_refs.diskController != nullptr)
{
m_refs.diskController->Tick (m_cpu->GetLastInstructionCycles());
}

if (m_refs.mockingboard != nullptr)
{
m_refs.mockingboard->Tick (m_cpu->GetLastInstructionCycles());
}

if (m_refs.keyboard != nullptr)
{
m_refs.keyboard->Tick (m_cpu->GetLastInstructionCycles());
Expand Down Expand Up @@ -3281,6 +3306,7 @@ void EmulatorShell::ExecuteCpuSlices()
{
static constexpr uint32_t kSliceCycles = 1023;

HRESULT hr = S_OK;
uint32_t targetCycles = m_cyclesPerFrame;
SpeedMode speed = m_cpuManager.GetSpeedMode();
bool audioActive = (m_refs.speaker != nullptr && m_wasapiAudio.IsInitialized());
Expand Down Expand Up @@ -3321,7 +3347,10 @@ void EmulatorShell::ExecuteCpuSlices()

while (sliceActual < sliceTarget)
{
m_cpu->StepOne();
if (!m_cpu->TryStepInterrupt())
{
m_cpu->StepOne();
}

cycles = m_cpu->GetLastInstructionCycles();

Expand All @@ -3340,6 +3369,11 @@ void EmulatorShell::ExecuteCpuSlices()
m_refs.diskController->Tick (cycles);
}

if (m_refs.mockingboard != nullptr)
{
m_refs.mockingboard->Tick (cycles);
}

if (m_refs.keyboard != nullptr)
{
m_refs.keyboard->Tick (cycles);
Expand All @@ -3355,12 +3389,14 @@ void EmulatorShell::ExecuteCpuSlices()

m_sampleRemainder = exactSamples - static_cast<double> (numSamples);

m_wasapiAudio.SubmitFrame (m_refs.speaker->GetToggleTimestamps(),
sliceActual,
m_refs.speaker->GetFrameInitialState(),
numSamples,
&m_driveAudioMixer,
m_cpu->GetTotalCycles());
hr = m_wasapiAudio.SubmitFrame (m_refs.speaker->GetToggleTimestamps(),
sliceActual,
m_refs.speaker->GetFrameInitialState(),
numSamples,
&m_driveAudioMixer,
m_cpu->GetTotalCycles(),
&m_mockingboardAudioMixer);
IGNORE_RETURN_VALUE (hr, S_OK);

m_refs.speaker->ClearTimestamps();
m_refs.speaker->BeginFrame();
Expand Down
Loading
Loading