mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-22 00:02:27 +00:00
Mockinboard card
This commit is contained in:
1 parent
4a608231dd
commit
a7782ad49b
28 files changed
+1525
-252
No files matched your search
@@ -17,8 +17,10 @@ type Apple2 struct {
|
||||
io *ioC0Page
|
||||
video screen.VideoSource
|
||||
cg *CharacterGenerator
|
||||
cards [8]Card
|
||||
tracers []executionTracer
|
||||
cards [8]Card
|
||||
tracers []executionTracer
|
||||
tickerCards []cardTicker
|
||||
irqRequests uint8 // Bitmask of the slots asserting the IRQ line
|
||||
|
||||
softVideoSwitch softVideoSwitchProvider
|
||||
board string
|
||||
@@ -50,15 +52,32 @@ func (a *Apple2) GetCards() [8]Card {
|
||||
return a.cards
|
||||
}
|
||||
|
||||
// A cardTicker is a card that needs to run logic on every instruction
|
||||
type cardTicker interface {
|
||||
tick()
|
||||
}
|
||||
|
||||
func (a *Apple2) registerTickerCard(c cardTicker) {
|
||||
a.tickerCards = append(a.tickerCards, c)
|
||||
}
|
||||
|
||||
// requestIRQ sets or clears the IRQ request of a slot. The CPU IRQ line
|
||||
// is asserted while any slot requests it. To be called only from the
|
||||
// emulation goroutine.
|
||||
func (a *Apple2) requestIRQ(slot int, asserted bool) {
|
||||
if asserted {
|
||||
a.irqRequests |= 1 << slot
|
||||
} else {
|
||||
a.irqRequests &^= 1 << slot
|
||||
}
|
||||
a.cpu.SetIRQ(a.irqRequests != 0)
|
||||
}
|
||||
|
||||
// SetKeyboardProvider attaches an external keyboard provider
|
||||
func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
|
||||
a.io.setKeyboardProvider(kb)
|
||||
}
|
||||
|
||||
// SetSpeakerProvider attaches an external speaker provider
|
||||
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
|
||||
a.io.setSpeakerProvider(s)
|
||||
}
|
||||
|
||||
// SetJoysticksProvider attaches an external joysticks provider
|
||||
func (a *Apple2) SetJoysticksProvider(j JoysticksProvider) {
|
||||
|
||||
@@ -48,6 +48,7 @@ func (a *Apple2) Start(paused bool) {
|
||||
a.cpu.ExecuteInstruction()
|
||||
a.cycles += a.cpu.GetCycles() - startCycles
|
||||
|
||||
a.tickCards()
|
||||
a.executionTrace()
|
||||
}
|
||||
} else {
|
||||
@@ -57,6 +58,7 @@ func (a *Apple2) Start(paused bool) {
|
||||
card.runDMACycle()
|
||||
a.cycles++
|
||||
|
||||
a.tickCards()
|
||||
a.executionTrace()
|
||||
}
|
||||
}
|
||||
@@ -137,6 +139,12 @@ func (a *Apple2) reset() {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) tickCards() {
|
||||
for _, c := range a.tickerCards {
|
||||
c.tick()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Apple2) executionTrace() {
|
||||
for _, v := range a.tracers {
|
||||
v.inspect()
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
Package audio synthesizes the sound of the Apple II.
|
||||
|
||||
The emulation core reports the sound activity as events with the CPU
|
||||
cycle when they happen: speaker clicks and output level changes of the
|
||||
sound cards. This package reconstructs a mixed 48 kHz float32 mono
|
||||
stream from those events. It is shared by the frontends, that only need
|
||||
to send the samples to their audio device.
|
||||
|
||||
The synthesis works as follows:
|
||||
|
||||
- The Mixer owns a render clock, measured in CPU cycles, that advances
|
||||
exactly one sample period per sample generated, whether there is
|
||||
sound or not.
|
||||
- Each Source holds the timestamped level changes of one sound
|
||||
generator. For every output sample, the level of each source is
|
||||
averaged over the sample window of ~21.31 CPU cycles, with the
|
||||
changes applied at their exact fractional position (box filter).
|
||||
This avoids most of the aliasing of naive point sampling.
|
||||
- The render clock lags the newest received event by a target latency,
|
||||
building a cushion against jitter between the emulation and audio
|
||||
clocks. If the events get too far ahead (fast forward) or fall
|
||||
behind (pause, hiccup) the render clock resynchronizes.
|
||||
- The sources are summed and a DC-blocking high-pass filter models the
|
||||
AC coupling of a real speaker: constant levels decay to silence.
|
||||
*/
|
||||
package audio
|
||||
|
||||
const (
|
||||
// SampleRate is the sample rate of the generated audio stream
|
||||
SampleRate = 48000
|
||||
|
||||
// The render clock stays this far behind the newest event, as a
|
||||
// cushion against jitter of the emulation clock
|
||||
targetLatencyMs = 30
|
||||
|
||||
// Resynchronize if the newest event is further ahead than this
|
||||
maxLatencyMs = 200
|
||||
|
||||
// Pole of the DC-blocking filter, ~30 Hz cutoff at 48 kHz
|
||||
dcFilterPole = 0.996
|
||||
|
||||
eventChannelSize = 10_000
|
||||
)
|
||||
|
||||
type levelEvent struct {
|
||||
cycle uint64
|
||||
level float32
|
||||
}
|
||||
|
||||
// Source receives the output level changes of one sound generator. It
|
||||
// implements the izapple2.AudioSink interface.
|
||||
type Source struct {
|
||||
events chan levelEvent
|
||||
pending []levelEvent
|
||||
consumed int
|
||||
level float64 // Current output level of the generator
|
||||
}
|
||||
|
||||
// PushLevel reports that the generator output changed to level at the
|
||||
// given CPU cycle. It can be called from any goroutine.
|
||||
func (s *Source) PushLevel(cycle uint64, level float32) {
|
||||
select {
|
||||
case s.events <- levelEvent{cycle, level}:
|
||||
default:
|
||||
// The channel is full, the event is lost
|
||||
}
|
||||
}
|
||||
|
||||
// renderWindow returns the average level over the sample window,
|
||||
// applying the pending events that fall inside it
|
||||
func (s *Source) renderWindow(start, end float64) float64 {
|
||||
acc := 0.0
|
||||
pos := start
|
||||
for s.consumed < len(s.pending) {
|
||||
event := s.pending[s.consumed]
|
||||
cycle := float64(event.cycle)
|
||||
if cycle >= end {
|
||||
break
|
||||
}
|
||||
if cycle > pos {
|
||||
acc += s.level * (cycle - pos)
|
||||
pos = cycle
|
||||
}
|
||||
s.level = float64(event.level)
|
||||
s.consumed++
|
||||
}
|
||||
acc += s.level * (end - pos)
|
||||
return acc / (end - start)
|
||||
}
|
||||
|
||||
// Mixer generates the audio stream of the machine, summing the streams
|
||||
// of its sources.
|
||||
type Mixer struct {
|
||||
sources []*Source
|
||||
|
||||
// Conversions for the configured CPU clock
|
||||
cyclesPerSample float64 // ~21.31 cycles at the standard clock
|
||||
targetLatencyCycles float64
|
||||
maxLatencyCycles float64
|
||||
|
||||
synced bool // False until the first event arrives
|
||||
renderCycle float64 // Render clock, the CPU cycle of the next sample
|
||||
|
||||
// DC-blocking filter state
|
||||
dcPrevIn float64
|
||||
dcPrevOut float64
|
||||
}
|
||||
|
||||
// NewMixer creates a Mixer for a CPU running at clockMhz
|
||||
func NewMixer(clockMhz float64) *Mixer {
|
||||
var m Mixer
|
||||
m.cyclesPerSample = 1000 * clockMhz * 1000 / SampleRate
|
||||
m.targetLatencyCycles = 1000 * clockMhz * targetLatencyMs
|
||||
m.maxLatencyCycles = 1000 * clockMhz * maxLatencyMs
|
||||
return &m
|
||||
}
|
||||
|
||||
// NewSource adds a sound source to the mix. All the sources must be
|
||||
// created before the audio device starts calling ReadSamples.
|
||||
func (m *Mixer) NewSource() *Source {
|
||||
s := &Source{
|
||||
events: make(chan levelEvent, eventChannelSize),
|
||||
pending: make([]levelEvent, 0, eventChannelSize),
|
||||
}
|
||||
m.sources = append(m.sources, s)
|
||||
return s
|
||||
}
|
||||
|
||||
// ReadSamples fills buf with the next samples of the audio stream. Mono,
|
||||
// values in [-1.0, 1.0]. It must always be called from the same goroutine
|
||||
// or audio callback thread.
|
||||
func (m *Mixer) ReadSamples(buf []float32) {
|
||||
// Take the queued events of each source
|
||||
someEvents := false
|
||||
var oldest, newest float64
|
||||
for _, s := range m.sources {
|
||||
for done := false; !done; {
|
||||
select {
|
||||
case event := <-s.events:
|
||||
s.pending = append(s.pending, event)
|
||||
default:
|
||||
done = true
|
||||
}
|
||||
}
|
||||
if len(s.pending) > 0 {
|
||||
first := float64(s.pending[0].cycle)
|
||||
last := float64(s.pending[len(s.pending)-1].cycle)
|
||||
if !someEvents || first < oldest {
|
||||
oldest = first
|
||||
}
|
||||
if !someEvents || last > newest {
|
||||
newest = last
|
||||
}
|
||||
someEvents = true
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize the render clock with the event timestamps
|
||||
if someEvents {
|
||||
if !m.synced || oldest < m.renderCycle || newest > m.renderCycle+m.maxLatencyCycles {
|
||||
m.renderCycle = newest - m.targetLatencyCycles
|
||||
m.synced = true
|
||||
}
|
||||
}
|
||||
|
||||
if !m.synced {
|
||||
// No events received yet, keep silent
|
||||
for i := range buf {
|
||||
buf[i] = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range buf {
|
||||
windowEnd := m.renderCycle + m.cyclesPerSample
|
||||
sample := 0.0
|
||||
for _, s := range m.sources {
|
||||
sample += s.renderWindow(m.renderCycle, windowEnd)
|
||||
}
|
||||
|
||||
// DC-blocking filter
|
||||
out := sample - m.dcPrevIn + dcFilterPole*m.dcPrevOut
|
||||
m.dcPrevIn = sample
|
||||
m.dcPrevOut = out
|
||||
|
||||
buf[i] = float32(out)
|
||||
m.renderCycle = windowEnd
|
||||
}
|
||||
|
||||
// Keep the events not rendered yet for the next call
|
||||
for _, s := range m.sources {
|
||||
s.pending = s.pending[:copy(s.pending, s.pending[s.consumed:])]
|
||||
s.consumed = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func waveAmplitude(buf []float32) float32 {
|
||||
peak := float32(0)
|
||||
for _, v := range buf {
|
||||
if v > peak {
|
||||
peak = v
|
||||
}
|
||||
}
|
||||
return peak
|
||||
}
|
||||
|
||||
func TestMixerSumsSources(t *testing.T) {
|
||||
// The same square wave on one speaker and on two in-phase speakers
|
||||
single := newWaveDriver(1_000_000, 1)
|
||||
double := newWaveDriver(1_000_000, 2)
|
||||
|
||||
singlePeak := waveAmplitude(single.run(512, 20))
|
||||
doublePeak := waveAmplitude(double.run(512, 20))
|
||||
|
||||
ratio := doublePeak / singlePeak
|
||||
if ratio < 1.8 || ratio > 2.2 {
|
||||
t.Errorf("expected two in-phase sources to double the amplitude, got a ratio of %v", ratio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixerSilentSourceDoesNotBlock(t *testing.T) {
|
||||
// An extra source with no events must not prevent the others from
|
||||
// playing
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
d.m.NewSource()
|
||||
|
||||
out := d.run(512, 20)
|
||||
if waveAmplitude(out) < 0.1 {
|
||||
t.Error("expected the speaker to play with a silent source attached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixerSourceHoldsLevel(t *testing.T) {
|
||||
// A source that reports one level change holds it, and the DC filter
|
||||
// slowly brings the output back to zero
|
||||
m := NewMixer(testClockMhz)
|
||||
s := m.NewSource()
|
||||
|
||||
s.PushLevel(1_000_000, 0.5)
|
||||
buf := make([]float32, SampleRate) // One second
|
||||
m.ReadSamples(buf)
|
||||
|
||||
if waveAmplitude(buf) < 0.4 {
|
||||
t.Error("expected the level step to reach the output")
|
||||
}
|
||||
tail := buf[len(buf)-100:]
|
||||
for _, v := range tail {
|
||||
if v > 0.01 || v < -0.01 {
|
||||
t.Fatalf("expected the DC filter to decay a constant level, got %v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
Package audio synthesizes the sound of the Apple II speaker.
|
||||
|
||||
The Apple II speaker is a 1-bit device: each access to $C030 toggles the
|
||||
speaker cone position. The emulator core reports those accesses as clicks
|
||||
with the CPU cycle when they happen. This package reconstructs a 48 kHz
|
||||
float32 audio stream from that stream of click timestamps. It is shared by
|
||||
the frontends, that only need to send the samples to their audio device.
|
||||
|
||||
The synthesis works as follows:
|
||||
|
||||
- A render clock, measured in CPU cycles, advances exactly one sample
|
||||
period per sample generated, whether there is sound or not.
|
||||
- Each output sample is the average of the speaker position over its
|
||||
window of ~21.31 CPU cycles, with the toggles placed at their exact
|
||||
fractional position (box filter). This avoids most of the aliasing of
|
||||
naive point sampling.
|
||||
- The render clock lags the newest received click by a target latency,
|
||||
building a cushion against jitter between the emulation and audio
|
||||
clocks. If the clicks get too far ahead (fast forward) or fall behind
|
||||
(pause, hiccup) the render clock resynchronizes.
|
||||
- A DC-blocking high-pass filter models the AC coupling of a real
|
||||
speaker: constant levels decay to silence and no artificial decay or
|
||||
resting level is needed.
|
||||
*/
|
||||
package audio
|
||||
|
||||
const (
|
||||
// SampleRate is the sample rate of the generated audio stream
|
||||
SampleRate = 48000
|
||||
|
||||
// The render clock stays this far behind the newest click, as a
|
||||
// cushion against jitter of the emulation clock
|
||||
targetLatencyMs = 30
|
||||
|
||||
// Resynchronize if the newest click is further ahead than this
|
||||
maxLatencyMs = 200
|
||||
|
||||
// Speaker cone positions, giving half of the full scale peak to peak
|
||||
speakerLevel = 0.25
|
||||
|
||||
// Pole of the DC-blocking filter, ~30 Hz cutoff at 48 kHz
|
||||
dcFilterPole = 0.996
|
||||
|
||||
clickChannelSize = 10_000
|
||||
)
|
||||
|
||||
// Speaker generates the audio stream of the Apple II speaker. It
|
||||
// implements the izapple2.SpeakerProvider interface.
|
||||
type Speaker struct {
|
||||
clickChannel chan uint64
|
||||
pendingClicks []uint64
|
||||
|
||||
// Conversions for the configured CPU clock
|
||||
cyclesPerSample float64 // ~21.31 cycles at the standard clock
|
||||
targetLatencyCycles float64
|
||||
maxLatencyCycles float64
|
||||
|
||||
synced bool // False until the first click arrives
|
||||
renderCycle float64 // Render clock, the CPU cycle of the next sample
|
||||
level float64 // Current speaker position
|
||||
|
||||
// DC-blocking filter state
|
||||
dcPrevIn float64
|
||||
dcPrevOut float64
|
||||
}
|
||||
|
||||
// NewSpeaker creates a Speaker for a CPU running at clockMhz
|
||||
func NewSpeaker(clockMhz float64) *Speaker {
|
||||
var s Speaker
|
||||
s.clickChannel = make(chan uint64, clickChannelSize)
|
||||
s.pendingClicks = make([]uint64, 0, clickChannelSize)
|
||||
s.cyclesPerSample = 1000 * clockMhz * 1000 / SampleRate
|
||||
s.targetLatencyCycles = 1000 * clockMhz * targetLatencyMs
|
||||
s.maxLatencyCycles = 1000 * clockMhz * maxLatencyMs
|
||||
s.level = speakerLevel
|
||||
return &s
|
||||
}
|
||||
|
||||
// Click receives a speaker click. The argument is the CPU cycle when it is
|
||||
// generated. It can be called from any goroutine.
|
||||
func (s *Speaker) Click(cycle uint64) {
|
||||
select {
|
||||
case s.clickChannel <- cycle:
|
||||
default:
|
||||
// The channel is full, the click is lost
|
||||
}
|
||||
}
|
||||
|
||||
// ReadSamples fills buf with the next samples of the audio stream. Mono,
|
||||
// values in [-1.0, 1.0]. It must always be called from the same goroutine
|
||||
// or audio callback thread.
|
||||
func (s *Speaker) ReadSamples(buf []float32) {
|
||||
// Take the queued clicks
|
||||
for done := false; !done; {
|
||||
select {
|
||||
case cycle := <-s.clickChannel:
|
||||
s.pendingClicks = append(s.pendingClicks, cycle)
|
||||
default:
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize the render clock with the click timestamps
|
||||
if len(s.pendingClicks) > 0 {
|
||||
oldest := float64(s.pendingClicks[0])
|
||||
newest := float64(s.pendingClicks[len(s.pendingClicks)-1])
|
||||
if !s.synced || oldest < s.renderCycle || newest > s.renderCycle+s.maxLatencyCycles {
|
||||
s.renderCycle = newest - s.targetLatencyCycles
|
||||
s.synced = true
|
||||
}
|
||||
}
|
||||
|
||||
if !s.synced {
|
||||
// No clicks received yet, keep silent
|
||||
for i := range buf {
|
||||
buf[i] = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
consumed := 0
|
||||
for i := range buf {
|
||||
windowEnd := s.renderCycle + s.cyclesPerSample
|
||||
|
||||
// Average the speaker position over the sample window, toggling
|
||||
// at the exact click positions
|
||||
acc := 0.0
|
||||
pos := s.renderCycle
|
||||
for consumed < len(s.pendingClicks) {
|
||||
click := float64(s.pendingClicks[consumed])
|
||||
if click >= windowEnd {
|
||||
break
|
||||
}
|
||||
if click > pos {
|
||||
acc += s.level * (click - pos)
|
||||
pos = click
|
||||
}
|
||||
s.level = -s.level
|
||||
consumed++
|
||||
}
|
||||
acc += s.level * (windowEnd - pos)
|
||||
sample := acc / s.cyclesPerSample
|
||||
|
||||
// DC-blocking filter
|
||||
out := sample - s.dcPrevIn + dcFilterPole*s.dcPrevOut
|
||||
s.dcPrevIn = sample
|
||||
s.dcPrevOut = out
|
||||
|
||||
buf[i] = float32(out)
|
||||
s.renderCycle = windowEnd
|
||||
}
|
||||
|
||||
// Keep the clicks not rendered yet for the next call
|
||||
s.pendingClicks = s.pendingClicks[:copy(s.pendingClicks, s.pendingClicks[consumed:])]
|
||||
}
|
||||
@@ -8,14 +8,32 @@ import (
|
||||
// The standard NTSC Apple II clock
|
||||
const testClockMhz = 14.318 / 14
|
||||
|
||||
func TestSilenceBeforeFirstClick(t *testing.T) {
|
||||
s := NewSpeaker(testClockMhz)
|
||||
// testToggler pushes speaker-like level toggles to a source
|
||||
type testToggler struct {
|
||||
source *Source
|
||||
high bool
|
||||
}
|
||||
|
||||
const testSpeakerLevel = 0.25
|
||||
|
||||
func (tg *testToggler) click(cycle uint64) {
|
||||
tg.high = !tg.high
|
||||
if tg.high {
|
||||
tg.source.PushLevel(cycle, testSpeakerLevel)
|
||||
} else {
|
||||
tg.source.PushLevel(cycle, -testSpeakerLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSilenceBeforeFirstEvent(t *testing.T) {
|
||||
m := NewMixer(testClockMhz)
|
||||
m.NewSource()
|
||||
buf := make([]float32, 1024)
|
||||
buf[10] = 42.0 // Ensure the buffer is overwritten
|
||||
s.ReadSamples(buf)
|
||||
m.ReadSamples(buf)
|
||||
for i, v := range buf {
|
||||
if v != 0 {
|
||||
t.Fatalf("expected silence before the first click, got %v at sample %v", v, i)
|
||||
t.Fatalf("expected silence before the first event, got %v at sample %v", v, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,35 +42,42 @@ func TestSilenceBeforeFirstClick(t *testing.T) {
|
||||
// parallel: on each step it queues the clicks of a square wave for the
|
||||
// next chunk of emulated time and reads the equivalent audio samples.
|
||||
type waveDriver struct {
|
||||
s *Speaker
|
||||
m *Mixer
|
||||
togglers []*testToggler
|
||||
nextClick uint64
|
||||
cycle uint64
|
||||
}
|
||||
|
||||
const driverChunkSamples = 480 // 10 ms
|
||||
|
||||
func newWaveDriver(startCycle uint64) *waveDriver {
|
||||
return &waveDriver{
|
||||
s: NewSpeaker(testClockMhz),
|
||||
func newWaveDriver(startCycle uint64, togglers int) *waveDriver {
|
||||
d := &waveDriver{
|
||||
m: NewMixer(testClockMhz),
|
||||
nextClick: startCycle,
|
||||
cycle: startCycle,
|
||||
}
|
||||
for range togglers {
|
||||
d.togglers = append(d.togglers, &testToggler{source: d.m.NewSource()})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// step advances 10 ms, toggling every halfPeriodCycles, 0 for silence
|
||||
func (d *waveDriver) step(halfPeriodCycles uint64) []float32 {
|
||||
chunkCycles := float64(driverChunkSamples) * d.s.cyclesPerSample
|
||||
chunkCycles := float64(driverChunkSamples) * d.m.cyclesPerSample
|
||||
d.cycle += uint64(chunkCycles)
|
||||
if halfPeriodCycles != 0 {
|
||||
for d.nextClick < d.cycle {
|
||||
d.s.Click(d.nextClick)
|
||||
for _, tg := range d.togglers {
|
||||
tg.click(d.nextClick)
|
||||
}
|
||||
d.nextClick += halfPeriodCycles
|
||||
}
|
||||
} else {
|
||||
d.nextClick = d.cycle
|
||||
}
|
||||
buf := make([]float32, driverChunkSamples)
|
||||
d.s.ReadSamples(buf)
|
||||
d.m.ReadSamples(buf)
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -66,7 +91,7 @@ func (d *waveDriver) run(halfPeriodCycles uint64, steps int) []float32 {
|
||||
|
||||
func TestSquareWaveFrequency(t *testing.T) {
|
||||
// A square wave toggling every 512 cycles, ~977 Hz, for 400 ms
|
||||
d := newWaveDriver(1_000_000)
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
out := d.run(512, 40)
|
||||
|
||||
// Check the average period on the second half, once in steady state
|
||||
@@ -82,14 +107,14 @@ func TestSquareWaveFrequency(t *testing.T) {
|
||||
}
|
||||
|
||||
gotPeriod := float64(risings[len(risings)-1]-risings[0]) / float64(len(risings)-1)
|
||||
wantPeriod := 2 * 512 / d.s.cyclesPerSample // ~48 samples
|
||||
wantPeriod := 2 * 512 / d.m.cyclesPerSample // ~48 samples
|
||||
if math.Abs(gotPeriod-wantPeriod) > 0.1 {
|
||||
t.Errorf("expected a period of %.2f samples, got %.2f", wantPeriod, gotPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAmplitudeAndDCRemoval(t *testing.T) {
|
||||
d := newWaveDriver(1_000_000)
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
out := d.run(512, 40)
|
||||
|
||||
sum := 0.0
|
||||
@@ -106,7 +131,7 @@ func TestAmplitudeAndDCRemoval(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSilenceDecaysAfterSound(t *testing.T) {
|
||||
d := newWaveDriver(1_000_000)
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
d.run(512, 20)
|
||||
|
||||
// A second of silence
|
||||
@@ -121,7 +146,7 @@ func TestSilenceDecaysAfterSound(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSoundResumesAfterPause(t *testing.T) {
|
||||
d := newWaveDriver(1_000_000)
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
d.run(512, 20)
|
||||
d.run(0, 100) // A second of silence
|
||||
|
||||
@@ -140,7 +165,7 @@ func TestSoundResumesAfterPause(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResyncAfterFastForward(t *testing.T) {
|
||||
d := newWaveDriver(1_000_000)
|
||||
d := newWaveDriver(1_000_000, 1)
|
||||
d.run(512, 20)
|
||||
|
||||
// The emulation fast-forwards ten seconds ahead of the audio clock
|
||||
@@ -0,0 +1,64 @@
|
||||
package izapple2
|
||||
|
||||
/*
|
||||
Audio interface between the sound generators and the frontends.
|
||||
|
||||
The built-in speaker and the cards that generate sound implement
|
||||
AudioSource. The frontend attaches an AudioSink to each source and
|
||||
renders the reported level changes, typically with the audio package
|
||||
Mixer.
|
||||
*/
|
||||
|
||||
// AudioSink receives the output level changes of a sound source with the
|
||||
// CPU cycle when they happen. Implemented by the frontends.
|
||||
type AudioSink interface {
|
||||
PushLevel(cycle uint64, level float32)
|
||||
}
|
||||
|
||||
// AudioSource is implemented by the cards that generate sound.
|
||||
type AudioSource interface {
|
||||
GetAudioSourceName() string
|
||||
SetAudioSink(sink AudioSink)
|
||||
}
|
||||
|
||||
// GetAudioSources returns the sound generators of the machine: the
|
||||
// built-in speaker and the sound cards
|
||||
func (a *Apple2) GetAudioSources() []AudioSource {
|
||||
sources := []AudioSource{&a.io.speaker}
|
||||
for _, card := range a.cards {
|
||||
if source, ok := card.(AudioSource); ok {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
// Position of the speaker cone, giving half of the full scale peak to peak
|
||||
const speakerLevel = 0.25
|
||||
|
||||
// speakerAudioSource is the AudioSource of the built-in speaker. Each
|
||||
// access to $C030 toggles the position of the speaker cone.
|
||||
type speakerAudioSource struct {
|
||||
sink AudioSink
|
||||
high bool // Position of the speaker cone
|
||||
}
|
||||
|
||||
func (s *speakerAudioSource) GetAudioSourceName() string {
|
||||
return "speaker"
|
||||
}
|
||||
|
||||
func (s *speakerAudioSource) SetAudioSink(sink AudioSink) {
|
||||
s.sink = sink
|
||||
}
|
||||
|
||||
func (s *speakerAudioSource) click(cycle uint64) {
|
||||
if s.sink == nil {
|
||||
return
|
||||
}
|
||||
s.high = !s.high
|
||||
if s.high {
|
||||
s.sink.PushLevel(cycle, speakerLevel)
|
||||
} else {
|
||||
s.sink.PushLevel(cycle, -speakerLevel)
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -123,9 +123,7 @@ func getSoftSwitch(io *ioC0Page, ioFlag uint8, isSet bool) softSwitchR {
|
||||
|
||||
func buildSpeakerSoftSwitch(io *ioC0Page) softSwitchR {
|
||||
return func() uint8 {
|
||||
if io.speaker != nil {
|
||||
io.speaker.Click(io.apple2.GetCycles())
|
||||
}
|
||||
io.speaker.click(io.apple2.GetCycles())
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ func getCardFactory() map[string]*cardBuilder {
|
||||
cardFactory["language"] = newCardLanguageBuilder()
|
||||
cardFactory["softswitchlogger"] = newCardLoggerBuilder()
|
||||
cardFactory["memexp"] = newCardMemoryExpansionBuilder()
|
||||
cardFactory["mockingboard"] = newCardMockingboardBuilder()
|
||||
cardFactory["mouse"] = newCardMouseBuilder()
|
||||
cardFactory["multirom"] = newMultiRomCardBuilder()
|
||||
cardFactory["parallel"] = newCardParallelPrinterBuilder()
|
||||
|
||||
@@ -80,6 +80,10 @@ func TestCardsDetected(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "4096", "ramworks", 55_000_000, "RAMWorks 4096K Card in Aux Slot")
|
||||
})
|
||||
|
||||
t.Run("test Mockingboard card", func(t *testing.T) {
|
||||
testCardDetectedInternal(t, "2enh", "mockingboard", "s4", 55_000_000, "4 MockingBoard Card")
|
||||
})
|
||||
|
||||
// Swyftcard not compatible with Card Cat
|
||||
// Unknonw cards: prodosromcard3
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2/component"
|
||||
)
|
||||
|
||||
/*
|
||||
Mockingboard sound card. Two MOS 6522 VIAs driving two AY-3-8913
|
||||
programmable sound generators.
|
||||
|
||||
See:
|
||||
https://en.wikipedia.org/wiki/Mockingboard
|
||||
https://github.com/AppleWin/AppleWin/blob/master/source/Mockingboard.cpp
|
||||
|
||||
The card is addressed on its slot ROM page, there is no ROM:
|
||||
|
||||
$Cn00-$Cn0F: VIA 1 registers, mirrored up to $Cn7F
|
||||
$Cn80-$Cn8F: VIA 2 registers, mirrored up to $CnFF
|
||||
|
||||
Each VIA drives one AY-3-8913:
|
||||
|
||||
Port A: data bus
|
||||
Port B bit 0: BC1
|
||||
Port B bit 1: BDIR
|
||||
Port B bit 2: /RESET, active low
|
||||
|
||||
The IRQ outputs of both VIAs are wired to the 6502 IRQ line. The VIA
|
||||
timers are caught up on every access and on every instruction, so the
|
||||
timer reads used by the Mockingboard detection routines are cycle exact.
|
||||
|
||||
The mixed output level of both PSGs is reported to the frontend as an
|
||||
AudioSource, stepping the chips every 8 CPU cycles.
|
||||
*/
|
||||
type CardMockingboard struct {
|
||||
cardBase
|
||||
via [2]component.MOS6522
|
||||
psg [2]component.AY38913
|
||||
lastBusOp [2]uint8
|
||||
|
||||
sink AudioSink
|
||||
lastCycle uint64 // The chips are caught up to this cycle
|
||||
psgCycle uint64 // Start of the next PSG synthesis step
|
||||
lastLevel float32
|
||||
}
|
||||
|
||||
const (
|
||||
mockingboardBusRead uint8 = 1
|
||||
mockingboardBusWrite uint8 = 2
|
||||
mockingboardBusLatch uint8 = 3
|
||||
|
||||
mockingboardPsgStepCycles = 8
|
||||
)
|
||||
|
||||
func newCardMockingboardBuilder() *cardBuilder {
|
||||
return &cardBuilder{
|
||||
name: "Mockingboard",
|
||||
description: "Mockingboard sound card with two AY-3-8913 sound generators",
|
||||
buildFunc: func(params map[string]string) (Card, error) {
|
||||
var c CardMockingboard
|
||||
return &c, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CardMockingboard) assign(a *Apple2, slot int) {
|
||||
c.cardBase.assign(a, slot)
|
||||
if slot != 0 {
|
||||
a.mmu.setCardROM(slot, traceMemory(c, c.name, c.traceMemory))
|
||||
}
|
||||
a.registerTickerCard(c)
|
||||
c.lastCycle = a.GetCycles()
|
||||
c.psgCycle = c.lastCycle
|
||||
c.psg[0].Reset()
|
||||
c.psg[1].Reset()
|
||||
}
|
||||
|
||||
func (c *CardMockingboard) reset() {
|
||||
for i := range 2 {
|
||||
c.via[i].Reset()
|
||||
c.psg[i].Reset()
|
||||
c.lastBusOp[i] = 0
|
||||
}
|
||||
if c.a != nil {
|
||||
c.a.requestIRQ(c.slot, false)
|
||||
}
|
||||
}
|
||||
|
||||
// peek and poke serve the $Cn00-$CnFF page, implementing memoryHandler
|
||||
func (c *CardMockingboard) peek(address uint16) uint8 {
|
||||
c.catchUp()
|
||||
n := int(address>>7) & 1
|
||||
value := c.via[n].Read(uint8(address & 0x0f))
|
||||
c.updateIRQ() // Some register reads clear interrupt flags
|
||||
return value
|
||||
}
|
||||
|
||||
func (c *CardMockingboard) poke(address uint16, value uint8) {
|
||||
c.catchUp()
|
||||
n := int(address>>7) & 1
|
||||
reg := uint8(address & 0x0f)
|
||||
c.via[n].Write(reg, value)
|
||||
if reg == 0 || reg == 2 {
|
||||
// ORB or DDRB writes can change the PSG bus control lines
|
||||
c.updatePsgBus(n)
|
||||
}
|
||||
c.updateIRQ()
|
||||
}
|
||||
|
||||
// updatePsgBus runs the AY-3-8913 bus protocol wired to the VIA port B
|
||||
func (c *CardMockingboard) updatePsgBus(n int) {
|
||||
portB := c.via[n].GetPortB()
|
||||
if portB&0x04 == 0 {
|
||||
// /RESET is active low
|
||||
c.psg[n].Reset()
|
||||
c.lastBusOp[n] = 0
|
||||
return
|
||||
}
|
||||
|
||||
// The operations execute when BDIR-BC1 change
|
||||
busOp := portB & 0x03
|
||||
if busOp != c.lastBusOp[n] {
|
||||
switch busOp {
|
||||
case mockingboardBusRead:
|
||||
c.via[n].SetInputA(c.psg[n].ReadData())
|
||||
case mockingboardBusWrite:
|
||||
c.psg[n].WriteData(c.via[n].GetPortA())
|
||||
case mockingboardBusLatch:
|
||||
c.psg[n].LatchAddress(c.via[n].GetPortA())
|
||||
}
|
||||
c.lastBusOp[n] = busOp
|
||||
}
|
||||
}
|
||||
|
||||
// tick is called on every instruction to keep the timers, the interrupt
|
||||
// line and the sound synthesis up to date
|
||||
func (c *CardMockingboard) tick() {
|
||||
c.catchUp()
|
||||
}
|
||||
|
||||
func (c *CardMockingboard) catchUp() {
|
||||
current := c.a.GetCycles()
|
||||
if current <= c.lastCycle {
|
||||
return
|
||||
}
|
||||
elapsed := current - c.lastCycle
|
||||
c.lastCycle = current
|
||||
|
||||
c.via[0].Tick(elapsed)
|
||||
c.via[1].Tick(elapsed)
|
||||
c.updateIRQ()
|
||||
c.synthesize(current)
|
||||
}
|
||||
|
||||
func (c *CardMockingboard) updateIRQ() {
|
||||
c.a.requestIRQ(c.slot, c.via[0].InterruptAsserted() || c.via[1].InterruptAsserted())
|
||||
}
|
||||
|
||||
// synthesize advances both PSGs up to the given cycle in steps of 8 CPU
|
||||
// cycles, reporting the changes of the mixed level to the audio sink
|
||||
func (c *CardMockingboard) synthesize(toCycle uint64) {
|
||||
if c.sink == nil {
|
||||
c.psgCycle = toCycle
|
||||
return
|
||||
}
|
||||
|
||||
for ; c.psgCycle+mockingboardPsgStepCycles <= toCycle; c.psgCycle += mockingboardPsgStepCycles {
|
||||
// Each PSG ranges from 0.0 to 3.0, scale the sum of both to a
|
||||
// range comparable to the speaker
|
||||
level := (c.psg[0].Step() + c.psg[1].Step()) * 0.25
|
||||
if level != c.lastLevel {
|
||||
c.sink.PushLevel(c.psgCycle, level)
|
||||
c.lastLevel = level
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetAudioSourceName implements the AudioSource interface
|
||||
func (c *CardMockingboard) GetAudioSourceName() string {
|
||||
return "mockingboard"
|
||||
}
|
||||
|
||||
// SetAudioSink implements the AudioSource interface
|
||||
func (c *CardMockingboard) SetAudioSink(sink AudioSink) {
|
||||
c.sink = sink
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package izapple2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func makeMockingboardTester(t *testing.T) (*Apple2, *CardMockingboard) {
|
||||
overrides := newConfiguration()
|
||||
overrides.set(confS4, "mockingboard")
|
||||
at, err := makeApple2Tester("2plus", overrides)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := at.a
|
||||
card, ok := a.cards[4].(*CardMockingboard)
|
||||
if !ok {
|
||||
t.Fatal("The Mockingboard should be in slot 4")
|
||||
}
|
||||
return a, card
|
||||
}
|
||||
|
||||
func TestCardMockingboardTimerReadIsCycleExact(t *testing.T) {
|
||||
a, _ := makeMockingboardTester(t)
|
||||
|
||||
// Start VIA 1 T1 with 0x2000 cycles
|
||||
a.mmu.Poke(0xc404, 0x00)
|
||||
a.mmu.Poke(0xc405, 0x20)
|
||||
|
||||
a.cycles += 100
|
||||
counter := uint16(a.mmu.Peek(0xc404)) | uint16(a.mmu.Peek(0xc405))<<8
|
||||
if counter != 0x2000-100 {
|
||||
t.Errorf("The timer should decrement with the CPU cycles, got 0x%04x", counter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardMockingboardTimerIRQ(t *testing.T) {
|
||||
a, card := makeMockingboardTester(t)
|
||||
|
||||
// Program VIA 1 T1 free running with interrupts every 0x1000 cycles
|
||||
a.mmu.Poke(0xc40b, 0x40) // ACR: T1 free running
|
||||
a.mmu.Poke(0xc40e, 0xc0) // IER: enable T1
|
||||
a.mmu.Poke(0xc404, 0x00)
|
||||
a.mmu.Poke(0xc405, 0x10)
|
||||
|
||||
a.cycles += 0x800
|
||||
card.tick()
|
||||
if a.irqRequests != 0 {
|
||||
t.Error("The IRQ should not be requested before the timer underflows")
|
||||
}
|
||||
|
||||
a.cycles += 0x900
|
||||
card.tick()
|
||||
if a.irqRequests == 0 {
|
||||
t.Error("The IRQ should be requested when the timer underflows")
|
||||
}
|
||||
|
||||
// Reading T1C-L clears the interrupt
|
||||
a.mmu.Peek(0xc404)
|
||||
if a.irqRequests != 0 {
|
||||
t.Error("The IRQ should be released after reading T1C-L")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCardMockingboardSecondVIA(t *testing.T) {
|
||||
a, card := makeMockingboardTester(t)
|
||||
|
||||
// The second VIA is at $Cn80, enable its T2
|
||||
a.mmu.Poke(0xc48e, 0xa0) // IER: enable T2
|
||||
a.mmu.Poke(0xc488, 0x50)
|
||||
a.mmu.Poke(0xc489, 0x00)
|
||||
|
||||
a.cycles += 0x100
|
||||
card.tick()
|
||||
if a.irqRequests == 0 {
|
||||
t.Error("The IRQ should be requested by the second VIA")
|
||||
}
|
||||
|
||||
a.mmu.Poke(0xc48d, 0x20) // Clear the T2 flag on IFR
|
||||
if a.irqRequests != 0 {
|
||||
t.Error("The IRQ should be released after clearing IFR")
|
||||
}
|
||||
}
|
||||
|
||||
// writePsg drives the AY bus protocol like the Mockingboard drivers do
|
||||
func writePsg(a *Apple2, base uint16, reg uint8, value uint8) {
|
||||
a.mmu.Poke(base+1, reg) // ORA: register number
|
||||
a.mmu.Poke(base, 4|3) // Latch address
|
||||
a.mmu.Poke(base, 4) // Inactive
|
||||
a.mmu.Poke(base+1, value)
|
||||
a.mmu.Poke(base, 4|2) // Write data
|
||||
a.mmu.Poke(base, 4) // Inactive
|
||||
}
|
||||
|
||||
func TestCardMockingboardPsgBus(t *testing.T) {
|
||||
a, _ := makeMockingboardTester(t)
|
||||
|
||||
// Set the VIA 1 ports as the drivers do
|
||||
a.mmu.Poke(0xc403, 0xff) // DDRA: all output
|
||||
a.mmu.Poke(0xc402, 0x07) // DDRB: control lines output
|
||||
|
||||
writePsg(a, 0xc400, 7, 0x3e) // Mixer: only tone A
|
||||
|
||||
// Read the register back through the bus
|
||||
a.mmu.Poke(0xc401, 7)
|
||||
a.mmu.Poke(0xc400, 4|3) // Latch address
|
||||
a.mmu.Poke(0xc400, 4)
|
||||
a.mmu.Poke(0xc403, 0x00) // DDRA: all input to read
|
||||
a.mmu.Poke(0xc400, 4|1) // Read data
|
||||
if v := a.mmu.Peek(0xc401); v != 0x3e {
|
||||
t.Errorf("The PSG register should read back 0x3e, got 0x%02x", v)
|
||||
}
|
||||
}
|
||||
|
||||
type testAudioSink struct {
|
||||
events int
|
||||
}
|
||||
|
||||
func (s *testAudioSink) PushLevel(cycle uint64, level float32) {
|
||||
s.events++
|
||||
}
|
||||
|
||||
func TestCardMockingboardGeneratesSound(t *testing.T) {
|
||||
a, card := makeMockingboardTester(t)
|
||||
|
||||
var card2 AudioSource
|
||||
for _, source := range a.GetAudioSources() {
|
||||
if source.GetAudioSourceName() == "mockingboard" {
|
||||
card2 = source
|
||||
}
|
||||
}
|
||||
if card2 == nil {
|
||||
t.Fatal("The card should be published as an audio source")
|
||||
}
|
||||
|
||||
var sink testAudioSink
|
||||
card2.SetAudioSink(&sink)
|
||||
|
||||
a.mmu.Poke(0xc403, 0xff)
|
||||
a.mmu.Poke(0xc402, 0x07)
|
||||
writePsg(a, 0xc400, 0, 100) // Tone A period
|
||||
writePsg(a, 0xc400, 7, 0x3e) // Mixer: only tone A
|
||||
writePsg(a, 0xc400, 8, 15) // Full volume
|
||||
|
||||
// Run 10000 cycles, the tone toggles every 800 cycles
|
||||
a.cycles += 10_000
|
||||
card.tick()
|
||||
if sink.events < 10 {
|
||||
t.Errorf("The sink should receive the tone level changes, got %v", sink.events)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package component
|
||||
|
||||
/*
|
||||
General Instrument AY-3-8913 Programmable Sound Generator
|
||||
See:
|
||||
|
||||
https://map.grauw.nl/resources/sound/generalinstrument_ay-3-8910.pdf
|
||||
https://en.wikipedia.org/wiki/General_Instrument_AY-3-8910
|
||||
|
||||
The 8913 is the variant of the AY-3-8910 without I/O ports. Two of them
|
||||
are used by the Mockingboard card, driven by the 6522 VIAs.
|
||||
|
||||
Registers:
|
||||
|
||||
R0-R1: channel A tone period, 12 bits
|
||||
R2-R3: channel B tone period, 12 bits
|
||||
R4-R5: channel C tone period, 12 bits
|
||||
R6: noise period, 5 bits
|
||||
R7: mixer, bits 0-2 disable tone A-C, bits 3-5 disable noise A-C
|
||||
R8-R10: channel A-C amplitude, bit 4 selects the envelope
|
||||
R11-R12: envelope period, 16 bits
|
||||
R13: envelope shape: CONTINUE, ATTACK, ALTERNATE, HOLD
|
||||
|
||||
The bus operations selected by the BDIR and BC1 pins are exposed as the
|
||||
LatchAddress, WriteData and ReadData methods.
|
||||
|
||||
Synthesis is done in steps of 8 chip clocks with Step(), the granularity
|
||||
of the tone generators. A tone with period TP completes a full cycle
|
||||
every 2*TP steps (16*TP clocks as per the datasheet).
|
||||
*/
|
||||
type AY38913 struct {
|
||||
regs [16]uint8
|
||||
address uint8
|
||||
|
||||
toneCounter [3]uint16
|
||||
toneOut [3]bool
|
||||
|
||||
noiseCounter uint16
|
||||
noiseShift uint32 // 17 bit LFSR
|
||||
|
||||
envCounter uint32
|
||||
envPosition uint8 // 0 to 15 within a ramp
|
||||
envAttack bool // Direction of the current ramp
|
||||
envHolding bool
|
||||
envHeldLevel uint8
|
||||
}
|
||||
|
||||
var ay38913RegisterMasks = [16]uint8{
|
||||
0xff, 0x0f, 0xff, 0x0f, 0xff, 0x0f, 0x1f, 0xff,
|
||||
0x1f, 0x1f, 0x1f, 0xff, 0xff, 0x0f, 0xff, 0xff,
|
||||
}
|
||||
|
||||
/*
|
||||
Volume of each of the 16 amplitude levels. The DAC is logarithmic with
|
||||
steps of ~3dB per level. Normalized so that one channel at full volume
|
||||
gives 1.0.
|
||||
*/
|
||||
var ay38913Volumes = [16]float32{
|
||||
0.0000, 0.0106, 0.0150, 0.0222, 0.0320, 0.0466, 0.0665, 0.1039,
|
||||
0.1237, 0.1986, 0.2803, 0.3548, 0.4702, 0.6030, 0.7530, 1.0000,
|
||||
}
|
||||
|
||||
// LatchAddress selects the register for the next data operations
|
||||
func (ay *AY38913) LatchAddress(value uint8) {
|
||||
// The chip compares the high nibble with its hardwired mask
|
||||
if value&0xf0 == 0 {
|
||||
ay.address = value & 0x0f
|
||||
}
|
||||
}
|
||||
|
||||
// WriteData writes to the selected register
|
||||
func (ay *AY38913) WriteData(value uint8) {
|
||||
ay.regs[ay.address] = value & ay38913RegisterMasks[ay.address]
|
||||
|
||||
if ay.address == 13 {
|
||||
// Writing the envelope shape restarts the envelope
|
||||
ay.envCounter = 0
|
||||
ay.envPosition = 0
|
||||
ay.envAttack = value&0x04 != 0
|
||||
ay.envHolding = false
|
||||
}
|
||||
}
|
||||
|
||||
// ReadData reads the selected register
|
||||
func (ay *AY38913) ReadData() uint8 {
|
||||
return ay.regs[ay.address]
|
||||
}
|
||||
|
||||
// Reset clears the registers and the generators
|
||||
func (ay *AY38913) Reset() {
|
||||
for i := range ay.regs {
|
||||
ay.regs[i] = 0
|
||||
}
|
||||
ay.address = 0
|
||||
for i := range 3 {
|
||||
ay.toneCounter[i] = 0
|
||||
ay.toneOut[i] = false
|
||||
}
|
||||
ay.noiseCounter = 0
|
||||
ay.noiseShift = 1
|
||||
ay.envCounter = 0
|
||||
ay.envPosition = 0
|
||||
ay.envAttack = false
|
||||
ay.envHolding = false
|
||||
}
|
||||
|
||||
// Step advances the generators 8 chip clocks and returns the mixed level
|
||||
// of the three channels, from 0.0 to 3.0
|
||||
func (ay *AY38913) Step() float32 {
|
||||
// Tone generators, a half period is TP steps
|
||||
for i := range 3 {
|
||||
period := uint16(ay.regs[2*i]) | uint16(ay.regs[2*i+1])<<8
|
||||
if period == 0 {
|
||||
period = 1
|
||||
}
|
||||
ay.toneCounter[i]++
|
||||
if ay.toneCounter[i] >= period {
|
||||
ay.toneCounter[i] = 0
|
||||
ay.toneOut[i] = !ay.toneOut[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Noise generator, the LFSR shifts every 2*NP steps
|
||||
noisePeriod := 2 * uint16(ay.regs[6])
|
||||
if noisePeriod == 0 {
|
||||
noisePeriod = 2
|
||||
}
|
||||
if ay.noiseShift == 0 {
|
||||
ay.noiseShift = 1 // Make the zero value usable
|
||||
}
|
||||
ay.noiseCounter++
|
||||
if ay.noiseCounter >= noisePeriod {
|
||||
ay.noiseCounter = 0
|
||||
// 17 bit LFSR with taps on bits 0 and 3
|
||||
bit := (ay.noiseShift ^ (ay.noiseShift >> 3)) & 1
|
||||
ay.noiseShift = (ay.noiseShift >> 1) | (bit << 16)
|
||||
}
|
||||
noiseOut := ay.noiseShift&1 != 0
|
||||
|
||||
// Envelope generator, one of the 16 levels every 2*EP steps
|
||||
if !ay.envHolding {
|
||||
envPeriod := 2 * (uint32(ay.regs[11]) | uint32(ay.regs[12])<<8)
|
||||
if envPeriod == 0 {
|
||||
envPeriod = 2
|
||||
}
|
||||
ay.envCounter++
|
||||
if ay.envCounter >= envPeriod {
|
||||
ay.envCounter = 0
|
||||
if ay.envPosition < 15 {
|
||||
ay.envPosition++
|
||||
} else {
|
||||
ay.endOfEnvelopeRamp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mix the channels
|
||||
level := float32(0)
|
||||
mixer := ay.regs[7]
|
||||
for i := range 3 {
|
||||
toneEnabled := mixer&(1<<i) == 0
|
||||
noiseEnabled := mixer&(1<<(3+i)) == 0
|
||||
// The channel is high unless an enabled generator drives it low
|
||||
high := (ay.toneOut[i] || !toneEnabled) && (noiseOut || !noiseEnabled)
|
||||
if high {
|
||||
amplitude := ay.regs[8+i]
|
||||
var volume uint8
|
||||
if amplitude&0x10 != 0 {
|
||||
volume = ay.envelopeLevel()
|
||||
} else {
|
||||
volume = amplitude & 0x0f
|
||||
}
|
||||
level += ay38913Volumes[volume]
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func (ay *AY38913) envelopeLevel() uint8 {
|
||||
if ay.envHolding {
|
||||
return ay.envHeldLevel
|
||||
}
|
||||
if ay.envAttack {
|
||||
return ay.envPosition
|
||||
}
|
||||
return 15 - ay.envPosition
|
||||
}
|
||||
|
||||
// endOfEnvelopeRamp applies the shape bits when a 16 level ramp completes
|
||||
func (ay *AY38913) endOfEnvelopeRamp() {
|
||||
shape := ay.regs[13]
|
||||
continues := shape&0x08 != 0
|
||||
attack := shape&0x04 != 0
|
||||
alternate := shape&0x02 != 0
|
||||
hold := shape&0x01 != 0
|
||||
|
||||
if !continues {
|
||||
ay.envHolding = true
|
||||
ay.envHeldLevel = 0
|
||||
} else if hold {
|
||||
ay.envHolding = true
|
||||
if attack != alternate {
|
||||
ay.envHeldLevel = 15
|
||||
} else {
|
||||
ay.envHeldLevel = 0
|
||||
}
|
||||
} else {
|
||||
// Restart the ramp, reversing the direction when alternating
|
||||
ay.envPosition = 0
|
||||
if alternate {
|
||||
ay.envAttack = !ay.envAttack
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ay38913SetRegister(ay *AY38913, reg uint8, value uint8) {
|
||||
ay.LatchAddress(reg)
|
||||
ay.WriteData(value)
|
||||
}
|
||||
|
||||
func TestAY38913RegisterMasks(t *testing.T) {
|
||||
var ay AY38913
|
||||
|
||||
ay38913SetRegister(&ay, 1, 0xff)
|
||||
if v := ay.ReadData(); v != 0x0f {
|
||||
t.Errorf("The coarse tone period should be masked to 4 bits, got 0x%02x", v)
|
||||
}
|
||||
|
||||
ay38913SetRegister(&ay, 6, 0xff)
|
||||
if v := ay.ReadData(); v != 0x1f {
|
||||
t.Errorf("The noise period should be masked to 5 bits, got 0x%02x", v)
|
||||
}
|
||||
|
||||
ay38913SetRegister(&ay, 8, 0xff)
|
||||
if v := ay.ReadData(); v != 0x1f {
|
||||
t.Errorf("The amplitude should be masked to 5 bits, got 0x%02x", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913AddressLatch(t *testing.T) {
|
||||
var ay AY38913
|
||||
|
||||
ay38913SetRegister(&ay, 2, 0x12)
|
||||
ay.LatchAddress(0x1f) // Invalid, high nibble not zero
|
||||
ay.LatchAddress(0x02)
|
||||
if v := ay.ReadData(); v != 0x12 {
|
||||
t.Errorf("The invalid address should be ignored, got 0x%02x", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913Tone(t *testing.T) {
|
||||
var ay AY38913
|
||||
ay38913SetRegister(&ay, 0, 5) // Channel A tone period 5
|
||||
ay38913SetRegister(&ay, 7, 0x3e) // Only tone A enabled
|
||||
ay38913SetRegister(&ay, 8, 15) // Channel A at full fixed volume
|
||||
|
||||
// The output must toggle between 0 and 1 every 5 steps
|
||||
last := ay.Step()
|
||||
changes := []int{}
|
||||
for i := range 50 {
|
||||
level := ay.Step()
|
||||
if level != last {
|
||||
if level != 0.0 && level != 1.0 {
|
||||
t.Fatalf("The level should toggle between 0 and 1, got %v", level)
|
||||
}
|
||||
changes = append(changes, i)
|
||||
last = level
|
||||
}
|
||||
}
|
||||
if len(changes) < 8 {
|
||||
t.Fatalf("The tone should toggle, got %v changes", len(changes))
|
||||
}
|
||||
for i := 1; i < len(changes); i++ {
|
||||
if changes[i]-changes[i-1] != 5 {
|
||||
t.Errorf("The half period should be 5 steps, got %v", changes[i]-changes[i-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913ConstantLevel(t *testing.T) {
|
||||
var ay AY38913
|
||||
ay38913SetRegister(&ay, 7, 0x3f) // Everything disabled
|
||||
ay38913SetRegister(&ay, 8, 15) // Channel A at full fixed volume
|
||||
|
||||
// With tone and noise disabled the channel stays high, this is how
|
||||
// the volume register is used to play digitized samples
|
||||
for range 20 {
|
||||
if level := ay.Step(); level != 1.0 {
|
||||
t.Fatalf("The channel should stay at full level, got %v", level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913EnvelopeDecay(t *testing.T) {
|
||||
var ay AY38913
|
||||
ay38913SetRegister(&ay, 7, 0x3f) // Everything disabled, channels high
|
||||
ay38913SetRegister(&ay, 8, 0x10) // Channel A on envelope
|
||||
ay38913SetRegister(&ay, 11, 1) // Envelope period 1
|
||||
ay38913SetRegister(&ay, 13, 0x00) // Decay then off
|
||||
|
||||
first := ay.Step()
|
||||
if first != 1.0 {
|
||||
t.Errorf("The envelope should start at full level, got %v", first)
|
||||
}
|
||||
|
||||
last := first
|
||||
for range 100 {
|
||||
level := ay.Step()
|
||||
if level > last {
|
||||
t.Fatalf("The envelope should never raise on decay, got %v after %v", level, last)
|
||||
}
|
||||
last = level
|
||||
}
|
||||
if last != 0.0 {
|
||||
t.Errorf("The envelope should end at zero, got %v", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913EnvelopeAttackHold(t *testing.T) {
|
||||
var ay AY38913
|
||||
ay38913SetRegister(&ay, 7, 0x3f)
|
||||
ay38913SetRegister(&ay, 8, 0x10)
|
||||
ay38913SetRegister(&ay, 11, 1)
|
||||
ay38913SetRegister(&ay, 13, 0x0d) // Attack then hold at full level
|
||||
|
||||
var last float32
|
||||
for range 100 {
|
||||
last = ay.Step()
|
||||
}
|
||||
if last != 1.0 {
|
||||
t.Errorf("The envelope should hold at full level, got %v", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAY38913Noise(t *testing.T) {
|
||||
var ay AY38913
|
||||
ay38913SetRegister(&ay, 6, 1) // Fastest noise
|
||||
ay38913SetRegister(&ay, 7, 0x37) // Only noise A enabled
|
||||
ay38913SetRegister(&ay, 8, 15)
|
||||
|
||||
changes := 0
|
||||
last := ay.Step()
|
||||
for range 1000 {
|
||||
level := ay.Step()
|
||||
if level != last {
|
||||
changes++
|
||||
last = level
|
||||
}
|
||||
}
|
||||
if changes < 100 {
|
||||
t.Errorf("The noise should toggle the output often, got %v changes", changes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package component
|
||||
|
||||
/*
|
||||
MOS 6522 Versatile Interface Adapter (VIA)
|
||||
See:
|
||||
|
||||
http://archive.6502.org/datasheets/mos_6522_preliminary_nov_1977.pdf
|
||||
http://archive.6502.org/datasheets/rockwell_r6522_via.pdf
|
||||
|
||||
Used by the Mockingboard card to drive the AY-3-8913 sound generators
|
||||
and to raise interrupts with the timers.
|
||||
|
||||
Implemented: ports A and B, timers T1 and T2, the IFR/IER interrupt logic.
|
||||
Not implemented: the shift register, the CA/CB control lines, handshaking
|
||||
and the T2 pulse counting mode.
|
||||
|
||||
Registers:
|
||||
|
||||
0: ORB/IRB 1: ORA/IRA 2: DDRB 3: DDRA
|
||||
4: T1C-L 5: T1C-H 6: T1L-L 7: T1L-H
|
||||
8: T2C-L 9: T2C-H 10: SR 11: ACR
|
||||
12: PCR 13: IFR 14: IER 15: ORA no handshake
|
||||
|
||||
The timers advance with Tick(elapsedCycles). Catching up with several
|
||||
cycles at a time is supported, including multiple T1 underflows in free
|
||||
running mode.
|
||||
*/
|
||||
type MOS6522 struct {
|
||||
ora, orb uint8 // Output registers
|
||||
ira, irb uint8 // Input registers, the values on the input pins
|
||||
ddra, ddrb uint8 // Data direction registers, 1 is output
|
||||
|
||||
t1counter int64 // Signed to detect underflows when catching up
|
||||
t1latch uint16
|
||||
t1fired bool
|
||||
t2counter int64
|
||||
t2latchL uint8
|
||||
t2fired bool
|
||||
|
||||
sr, acr, pcr uint8
|
||||
ifr, ier uint8
|
||||
}
|
||||
|
||||
const (
|
||||
mos6522IntT1 uint8 = 1 << 6
|
||||
mos6522IntT2 uint8 = 1 << 5
|
||||
|
||||
mos6522AcrT1FreeRunning uint8 = 1 << 6
|
||||
)
|
||||
|
||||
// Read returns the value of a register
|
||||
func (v *MOS6522) Read(reg uint8) uint8 {
|
||||
switch reg & 0x0f {
|
||||
case 0:
|
||||
return (v.irb &^ v.ddrb) | (v.orb & v.ddrb)
|
||||
case 1, 15:
|
||||
return (v.ira &^ v.ddra) | (v.ora & v.ddra)
|
||||
case 2:
|
||||
return v.ddrb
|
||||
case 3:
|
||||
return v.ddra
|
||||
case 4:
|
||||
v.ifr &^= mos6522IntT1
|
||||
return uint8(uint16(v.t1counter))
|
||||
case 5:
|
||||
return uint8(uint16(v.t1counter) >> 8)
|
||||
case 6:
|
||||
return uint8(v.t1latch)
|
||||
case 7:
|
||||
return uint8(v.t1latch >> 8)
|
||||
case 8:
|
||||
v.ifr &^= mos6522IntT2
|
||||
return uint8(uint16(v.t2counter))
|
||||
case 9:
|
||||
return uint8(uint16(v.t2counter) >> 8)
|
||||
case 10:
|
||||
return v.sr
|
||||
case 11:
|
||||
return v.acr
|
||||
case 12:
|
||||
return v.pcr
|
||||
case 13:
|
||||
ifr := v.ifr & 0x7f
|
||||
if ifr&v.ier != 0 {
|
||||
ifr |= 0x80
|
||||
}
|
||||
return ifr
|
||||
case 14:
|
||||
return v.ier | 0x80
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Write sets the value of a register
|
||||
func (v *MOS6522) Write(reg uint8, value uint8) {
|
||||
switch reg & 0x0f {
|
||||
case 0:
|
||||
v.orb = value
|
||||
case 1, 15:
|
||||
v.ora = value
|
||||
case 2:
|
||||
v.ddrb = value
|
||||
case 3:
|
||||
v.ddra = value
|
||||
case 4, 6:
|
||||
v.t1latch = (v.t1latch & 0xff00) | uint16(value)
|
||||
case 5:
|
||||
// Load the counter from the latch and start counting
|
||||
v.t1latch = (v.t1latch & 0x00ff) | uint16(value)<<8
|
||||
v.t1counter = int64(v.t1latch)
|
||||
v.t1fired = false
|
||||
v.ifr &^= mos6522IntT1
|
||||
case 7:
|
||||
v.t1latch = (v.t1latch & 0x00ff) | uint16(value)<<8
|
||||
v.ifr &^= mos6522IntT1
|
||||
case 8:
|
||||
v.t2latchL = value
|
||||
case 9:
|
||||
v.t2counter = int64(value)<<8 | int64(v.t2latchL)
|
||||
v.t2fired = false
|
||||
v.ifr &^= mos6522IntT2
|
||||
case 10:
|
||||
v.sr = value
|
||||
case 11:
|
||||
v.acr = value
|
||||
case 12:
|
||||
v.pcr = value
|
||||
case 13:
|
||||
// Writing ones clears the flags
|
||||
v.ifr &^= value & 0x7f
|
||||
case 14:
|
||||
if value&0x80 != 0 {
|
||||
v.ier |= value & 0x7f
|
||||
} else {
|
||||
v.ier &^= value & 0x7f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tick advances the timers by the elapsed CPU cycles
|
||||
func (v *MOS6522) Tick(elapsedCycles uint64) {
|
||||
v.t1counter -= int64(elapsedCycles)
|
||||
if v.t1counter < 0 {
|
||||
if v.acr&mos6522AcrT1FreeRunning != 0 {
|
||||
// Reload from the latch and interrupt on each underflow
|
||||
period := int64(v.t1latch) + 2
|
||||
v.t1counter += period * (1 + (-v.t1counter-1)/period)
|
||||
v.ifr |= mos6522IntT1
|
||||
} else {
|
||||
// One shot: interrupt once, the counter rolls over
|
||||
if !v.t1fired {
|
||||
v.ifr |= mos6522IntT1
|
||||
v.t1fired = true
|
||||
}
|
||||
v.t1counter = int64(uint16(v.t1counter))
|
||||
}
|
||||
}
|
||||
|
||||
v.t2counter -= int64(elapsedCycles)
|
||||
if v.t2counter < 0 {
|
||||
// One shot: interrupt once, the counter rolls over
|
||||
if !v.t2fired {
|
||||
v.ifr |= mos6522IntT2
|
||||
v.t2fired = true
|
||||
}
|
||||
v.t2counter = int64(uint16(v.t2counter))
|
||||
}
|
||||
}
|
||||
|
||||
// InterruptAsserted returns the state of the IRQ output line
|
||||
func (v *MOS6522) InterruptAsserted() bool {
|
||||
return v.ifr&v.ier&0x7f != 0
|
||||
}
|
||||
|
||||
// Reset clears the registers as the RES pin. The timers and the shift
|
||||
// register are not affected
|
||||
func (v *MOS6522) Reset() {
|
||||
v.ora, v.orb = 0, 0
|
||||
v.ddra, v.ddrb = 0, 0
|
||||
v.acr, v.pcr = 0, 0
|
||||
v.ifr, v.ier = 0, 0
|
||||
}
|
||||
|
||||
// GetPortA returns the values on the port A pins
|
||||
func (v *MOS6522) GetPortA() uint8 {
|
||||
return (v.ora & v.ddra) | (v.ira &^ v.ddra)
|
||||
}
|
||||
|
||||
// GetPortB returns the values on the port B pins
|
||||
func (v *MOS6522) GetPortB() uint8 {
|
||||
return (v.orb & v.ddrb) | (v.irb &^ v.ddrb)
|
||||
}
|
||||
|
||||
// SetInputA sets the values on the port A input pins
|
||||
func (v *MOS6522) SetInputA(value uint8) {
|
||||
v.ira = value
|
||||
}
|
||||
|
||||
// SetInputB sets the values on the port B input pins
|
||||
func (v *MOS6522) SetInputB(value uint8) {
|
||||
v.irb = value
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMOS6522Timer1OneShot(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(14, 0x80|0x40) // Enable the T1 interrupt
|
||||
v.Write(4, 100) // T1 latch low
|
||||
v.Write(5, 0) // T1 counter high, starts the timer
|
||||
|
||||
v.Tick(100)
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("The interrupt should not fire before the timer underflows")
|
||||
}
|
||||
|
||||
v.Tick(2)
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should fire when the timer underflows")
|
||||
}
|
||||
|
||||
// In one shot mode it must fire only once
|
||||
v.Write(13, 0x40) // Clear the T1 flag
|
||||
v.Tick(1000)
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("The one shot timer should not fire again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522Timer1FreeRunning(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(11, 0x40) // ACR: T1 free running
|
||||
v.Write(14, 0x80|0x40) // Enable the T1 interrupt
|
||||
v.Write(4, 100)
|
||||
v.Write(5, 0)
|
||||
|
||||
v.Tick(102)
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should fire on the first underflow")
|
||||
}
|
||||
|
||||
v.Write(13, 0x40) // Clear the T1 flag
|
||||
v.Tick(102) // A full period without reprogramming the timer
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should fire again in free running mode")
|
||||
}
|
||||
|
||||
// Catching up with many periods at once must not hang and must
|
||||
// leave the counter in range
|
||||
v.Write(13, 0x40)
|
||||
v.Tick(1_000_000)
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should fire after catching up many periods")
|
||||
}
|
||||
counter := uint16(v.Read(4)) | uint16(v.Read(5))<<8
|
||||
if counter > 101 {
|
||||
t.Errorf("The counter should stay in the period range, got %v", counter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522Timer1ReadClearsFlag(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(14, 0x80|0x40)
|
||||
v.Write(4, 10)
|
||||
v.Write(5, 0)
|
||||
v.Tick(50)
|
||||
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should be asserted")
|
||||
}
|
||||
v.Read(4) // Reading T1C-L clears the flag
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("Reading T1C-L should clear the T1 flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522Timer2(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(14, 0x80|0x20) // Enable the T2 interrupt
|
||||
v.Write(8, 50) // T2 latch low
|
||||
v.Write(9, 0) // T2 counter high, starts the timer
|
||||
|
||||
v.Tick(50)
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("The interrupt should not fire before the timer underflows")
|
||||
}
|
||||
v.Tick(2)
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should fire when the timer underflows")
|
||||
}
|
||||
|
||||
v.Write(13, 0x20)
|
||||
v.Tick(1000)
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("T2 should not fire again")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522InterruptEnable(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(4, 10)
|
||||
v.Write(5, 0)
|
||||
v.Tick(50) // T1 underflows but the interrupt is not enabled
|
||||
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("The interrupt should be masked by IER")
|
||||
}
|
||||
if v.Read(13)&0x40 == 0 {
|
||||
t.Error("The T1 flag should be set on IFR even when masked")
|
||||
}
|
||||
if v.Read(13)&0x80 != 0 {
|
||||
t.Error("The IFR bit 7 should be clear when masked")
|
||||
}
|
||||
|
||||
v.Write(14, 0x80|0x40) // Enable the T1 interrupt afterwards
|
||||
if !v.InterruptAsserted() {
|
||||
t.Error("The interrupt should be asserted once enabled")
|
||||
}
|
||||
if v.Read(13)&0x80 == 0 {
|
||||
t.Error("The IFR bit 7 should be set when interrupting")
|
||||
}
|
||||
|
||||
v.Write(14, 0x40) // Disable the T1 interrupt
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("The interrupt should be masked again")
|
||||
}
|
||||
if v.Read(14) != 0x80 {
|
||||
t.Errorf("IER should read as 0x80, got 0x%02x", v.Read(14))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522Ports(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(3, 0x0f) // DDRA: half output, half input
|
||||
v.Write(1, 0xa5) // ORA
|
||||
v.SetInputA(0x5a)
|
||||
|
||||
if port := v.GetPortA(); port != 0x55 {
|
||||
t.Errorf("Port A should mix output and input bits, got 0x%02x", port)
|
||||
}
|
||||
if value := v.Read(1); value != 0x55 {
|
||||
t.Errorf("Reading port A should mix output and input bits, got 0x%02x", value)
|
||||
}
|
||||
|
||||
v.Write(2, 0xff) // DDRB: all output
|
||||
v.Write(0, 0x07)
|
||||
if port := v.GetPortB(); port != 0x07 {
|
||||
t.Errorf("Port B should return the output register, got 0x%02x", port)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMOS6522Reset(t *testing.T) {
|
||||
var v MOS6522
|
||||
v.Write(14, 0x80|0x40)
|
||||
v.Write(4, 10)
|
||||
v.Write(5, 0)
|
||||
v.Tick(50)
|
||||
|
||||
v.Reset()
|
||||
if v.InterruptAsserted() {
|
||||
t.Error("Reset should clear the interrupt state")
|
||||
}
|
||||
if v.Read(2) != 0 || v.Read(3) != 0 {
|
||||
t.Error("Reset should clear the data direction registers")
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ nsc: main
|
||||
s0: language
|
||||
s2: vidhd
|
||||
s3: fastchip
|
||||
s4: mockingboard
|
||||
s6: diskii,disk1=<internal>/dos33.dsk
|
||||
+2
-1
@@ -240,7 +240,7 @@ Usage: izapple2 [file]
|
||||
-s3 string
|
||||
slot 3 configuration. (default "fastchip")
|
||||
-s4 string
|
||||
slot 4 configuration. (default "empty")
|
||||
slot 4 configuration. (default "mockingboard")
|
||||
-s5 string
|
||||
slot 5 configuration. (default "empty")
|
||||
-s6 string
|
||||
@@ -282,6 +282,7 @@ The available cards are:
|
||||
fujinet: SmartPort interface card hosting the Fujinet
|
||||
language: Language card with 16 extra KB for the Apple ][ and ][+
|
||||
memexp: Memory expansion card
|
||||
mockingboard: Mockingboard sound card with two AY-3-8913 sound generators
|
||||
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
|
||||
multirom: Multiple Image ROM card
|
||||
parallel: Card to dump to a file what would be printed to a parallel printer
|
||||
|
||||
+2
-1
@@ -32,7 +32,7 @@ Usage: izapple2 [file]
|
||||
-s3 string
|
||||
slot 3 configuration. (default "fastchip")
|
||||
-s4 string
|
||||
slot 4 configuration. (default "empty")
|
||||
slot 4 configuration. (default "mockingboard")
|
||||
-s5 string
|
||||
slot 5 configuration. (default "empty")
|
||||
-s6 string
|
||||
@@ -74,6 +74,7 @@ The available cards are:
|
||||
fujinet: SmartPort interface card hosting the Fujinet
|
||||
language: Language card with 16 extra KB for the Apple ][ and ][+
|
||||
memexp: Memory expansion card
|
||||
mockingboard: Mockingboard sound card with two AY-3-8913 sound generators
|
||||
mouse: Mouse card implementation, does not emulate a real card, only the firmware behaviour
|
||||
multirom: Multiple Image ROM card
|
||||
parallel: Card to dump to a file what would be printed to a parallel printer
|
||||
|
||||
@@ -9,27 +9,24 @@ import (
|
||||
a2audio "github.com/ivanizag/izapple2/audio"
|
||||
)
|
||||
|
||||
// ebitenSpeaker sends the audio from the shared synthesizer to the ebiten
|
||||
// audio player. It implements izapple2.SpeakerProvider.
|
||||
type ebitenSpeaker struct {
|
||||
speaker *a2audio.Speaker
|
||||
// ebitenAudio sends the mixed audio of the machine to the ebiten audio
|
||||
// player. The audio sources of the machine attach to the mixer.
|
||||
type ebitenAudio struct {
|
||||
mixer *a2audio.Mixer
|
||||
|
||||
audioContext *audio.Context
|
||||
audioPlayer *audio.Player
|
||||
samples []float32
|
||||
}
|
||||
|
||||
func newEbitenSpeaker(clockMhz float64) *ebitenSpeaker {
|
||||
return &ebitenSpeaker{speaker: a2audio.NewSpeaker(clockMhz)}
|
||||
}
|
||||
|
||||
// Click receives a speaker click. The argument is the CPU cycle when it is generated
|
||||
func (s *ebitenSpeaker) Click(cycle uint64) {
|
||||
s.speaker.Click(cycle)
|
||||
func newEbitenAudio(clockMhz float64) *ebitenAudio {
|
||||
return &ebitenAudio{
|
||||
mixer: a2audio.NewMixer(clockMhz),
|
||||
}
|
||||
}
|
||||
|
||||
// Read is io.Reader's Read, it fills the buffer with audio samples
|
||||
func (s *ebitenSpeaker) Read(buf []byte) (n int, err error) {
|
||||
func (s *ebitenAudio) Read(buf []byte) (n int, err error) {
|
||||
const bytesPerSample = 8 // Two float32, one for each channel
|
||||
samples := len(buf) / bytesPerSample
|
||||
|
||||
@@ -37,7 +34,7 @@ func (s *ebitenSpeaker) Read(buf []byte) (n int, err error) {
|
||||
s.samples = make([]float32, samples)
|
||||
}
|
||||
s.samples = s.samples[:samples]
|
||||
s.speaker.ReadSamples(s.samples)
|
||||
s.mixer.ReadSamples(s.samples)
|
||||
|
||||
for i, v := range s.samples {
|
||||
putFloat32InBuffer(buf, i, v)
|
||||
@@ -57,7 +54,7 @@ func putFloat32InBuffer(buf []byte, i int, f float32) {
|
||||
buf[i*8+7] = byte(v >> 24)
|
||||
}
|
||||
|
||||
func (s *ebitenSpeaker) update() error {
|
||||
func (s *ebitenAudio) update() error {
|
||||
if s.audioContext == nil {
|
||||
s.audioContext = audio.NewContext(a2audio.SampleRate)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ type Game struct {
|
||||
a *izapple2.Apple2
|
||||
image *ebiten.Image
|
||||
keyboard *ebitenKeyboard
|
||||
speaker *ebitenSpeaker
|
||||
speaker *ebitenAudio
|
||||
fontSource *text.GoTextFaceSource
|
||||
|
||||
paused bool
|
||||
@@ -132,9 +132,11 @@ func ebitenRun(a *izapple2.Apple2) {
|
||||
game := &Game{
|
||||
a: a,
|
||||
keyboard: newEbitenKeyBoard(a),
|
||||
speaker: newEbitenSpeaker(a.GetClockMhz()),
|
||||
speaker: newEbitenAudio(a.GetClockMhz()),
|
||||
}
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(game.speaker.mixer.NewSource())
|
||||
}
|
||||
a.SetSpeakerProvider(game.speaker)
|
||||
|
||||
var err error
|
||||
game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||
|
||||
@@ -49,9 +49,11 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
|
||||
kp := newSDLKeyBoard(a)
|
||||
|
||||
s := newSDLSpeaker(a.GetClockMhz())
|
||||
s := newSDLAudio(a.GetClockMhz())
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(s.mixer.NewSource())
|
||||
}
|
||||
s.start()
|
||||
a.SetSpeakerProvider(s)
|
||||
|
||||
j := newSDLJoysticks(!a.UsesMouse())
|
||||
a.SetJoysticksProvider(j)
|
||||
|
||||
@@ -17,26 +17,23 @@ import (
|
||||
// Samples per SDL audio buffer, ~21 ms
|
||||
const bufferSamples = 1024
|
||||
|
||||
// sdlSpeaker sends the audio from the shared synthesizer to the SDL audio
|
||||
// device. It implements izapple2.SpeakerProvider.
|
||||
type sdlSpeaker struct {
|
||||
speaker *audio.Speaker
|
||||
// sdlAudio sends the mixed audio of the machine to the SDL audio device.
|
||||
// The audio sources of the machine attach to the mixer.
|
||||
type sdlAudio struct {
|
||||
mixer *audio.Mixer
|
||||
}
|
||||
|
||||
/*
|
||||
I have not found a way to encode the pointer to sdlSpeaker on the userdata of
|
||||
I have not found a way to encode the pointer to sdlAudio on the userdata of
|
||||
the call to SpeakerCallback(). I use a global as workaround... It is atomic
|
||||
because the callback runs on the SDL audio thread.
|
||||
*/
|
||||
var theSDLSpeaker atomic.Pointer[sdlSpeaker]
|
||||
var theSDLAudio atomic.Pointer[sdlAudio]
|
||||
|
||||
func newSDLSpeaker(clockMhz float64) *sdlSpeaker {
|
||||
return &sdlSpeaker{speaker: audio.NewSpeaker(clockMhz)}
|
||||
}
|
||||
|
||||
// Click receives a speaker click. The argument is the CPU cycle when it is generated
|
||||
func (s *sdlSpeaker) Click(cycle uint64) {
|
||||
s.speaker.Click(cycle)
|
||||
func newSDLAudio(clockMhz float64) *sdlAudio {
|
||||
return &sdlAudio{
|
||||
mixer: audio.NewMixer(clockMhz),
|
||||
}
|
||||
}
|
||||
|
||||
// SpeakerCallback is called to get more sound buffer data
|
||||
@@ -46,7 +43,7 @@ func SpeakerCallback(userdata unsafe.Pointer, stream *C.Uint8, length C.int) {
|
||||
// Adapt the C buffer to a slice of float32 samples
|
||||
buf := unsafe.Slice((*float32)(unsafe.Pointer(stream)), int(length)/4)
|
||||
|
||||
s := theSDLSpeaker.Load()
|
||||
s := theSDLAudio.Load()
|
||||
if s == nil {
|
||||
// SDL does not guarantee the buffer to be initialized
|
||||
for i := range buf {
|
||||
@@ -55,10 +52,10 @@ func SpeakerCallback(userdata unsafe.Pointer, stream *C.Uint8, length C.int) {
|
||||
return
|
||||
}
|
||||
|
||||
s.speaker.ReadSamples(buf)
|
||||
s.mixer.ReadSamples(buf)
|
||||
}
|
||||
|
||||
func (s *sdlSpeaker) start() {
|
||||
func (s *sdlAudio) start() {
|
||||
err := sdl.Init(sdl.INIT_AUDIO)
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting SDL audio: %v.\n", err)
|
||||
@@ -77,11 +74,11 @@ func (s *sdlSpeaker) start() {
|
||||
fmt.Printf("Error opening the SDL audio channel: %v.\n", err)
|
||||
return
|
||||
}
|
||||
theSDLSpeaker.Store(s)
|
||||
theSDLAudio.Store(s)
|
||||
sdl.PauseAudio(false)
|
||||
}
|
||||
|
||||
func (s *sdlSpeaker) close() {
|
||||
func (s *sdlAudio) close() {
|
||||
sdl.CloseAudio()
|
||||
sdl.Quit()
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type Game struct {
|
||||
a *izapple2.Apple2
|
||||
image *ebiten.Image
|
||||
keyboard *wasmKeyboard
|
||||
speaker *wasmSpeaker
|
||||
speaker *wasmAudio
|
||||
|
||||
updates uint64
|
||||
screenMode int
|
||||
@@ -107,13 +107,15 @@ func ebitenRun(a *izapple2.Apple2) {
|
||||
|
||||
game := &Game{
|
||||
a: a,
|
||||
speaker: newWasmSpeaker(a.GetClockMhz()),
|
||||
speaker: newWasmAudio(a.GetClockMhz()),
|
||||
keyboard: newWasmKeyBoard(a),
|
||||
screenMode: a_screen.ScreenModeNTSC,
|
||||
}
|
||||
|
||||
// Set up providers
|
||||
a.SetSpeakerProvider(game.speaker)
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(game.speaker.mixer.NewSource())
|
||||
}
|
||||
|
||||
// Setup API exports for React
|
||||
setupAPI(a, game)
|
||||
|
||||
@@ -11,27 +11,24 @@ import (
|
||||
a2audio "github.com/ivanizag/izapple2/audio"
|
||||
)
|
||||
|
||||
// wasmSpeaker sends the audio from the shared synthesizer to the ebiten
|
||||
// audio player. It implements izapple2.SpeakerProvider.
|
||||
type wasmSpeaker struct {
|
||||
speaker *a2audio.Speaker
|
||||
// wasmAudio sends the mixed audio of the machine to the ebiten audio
|
||||
// player. The audio sources of the machine attach to the mixer.
|
||||
type wasmAudio struct {
|
||||
mixer *a2audio.Mixer
|
||||
|
||||
audioContext *audio.Context
|
||||
audioPlayer *audio.Player
|
||||
samples []float32
|
||||
}
|
||||
|
||||
func newWasmSpeaker(clockMhz float64) *wasmSpeaker {
|
||||
return &wasmSpeaker{speaker: a2audio.NewSpeaker(clockMhz)}
|
||||
}
|
||||
|
||||
// Click receives a speaker click. The argument is the CPU cycle when it is generated
|
||||
func (s *wasmSpeaker) Click(cycle uint64) {
|
||||
s.speaker.Click(cycle)
|
||||
func newWasmAudio(clockMhz float64) *wasmAudio {
|
||||
return &wasmAudio{
|
||||
mixer: a2audio.NewMixer(clockMhz),
|
||||
}
|
||||
}
|
||||
|
||||
// Read is io.Reader's Read, it fills the buffer with audio samples
|
||||
func (s *wasmSpeaker) Read(buf []byte) (n int, err error) {
|
||||
func (s *wasmAudio) Read(buf []byte) (n int, err error) {
|
||||
const bytesPerSample = 8 // Two float32, one for each channel
|
||||
samples := len(buf) / bytesPerSample
|
||||
|
||||
@@ -39,7 +36,7 @@ func (s *wasmSpeaker) Read(buf []byte) (n int, err error) {
|
||||
s.samples = make([]float32, samples)
|
||||
}
|
||||
s.samples = s.samples[:samples]
|
||||
s.speaker.ReadSamples(s.samples)
|
||||
s.mixer.ReadSamples(s.samples)
|
||||
|
||||
for i, v := range s.samples {
|
||||
putFloat32InBuffer(buf, i, v)
|
||||
@@ -59,7 +56,7 @@ func putFloat32InBuffer(buf []byte, i int, f float32) {
|
||||
buf[i*8+7] = byte(v >> 24)
|
||||
}
|
||||
|
||||
func (s *wasmSpeaker) update() error {
|
||||
func (s *wasmAudio) update() error {
|
||||
if s.audioContext == nil {
|
||||
s.audioContext = audio.NewContext(a2audio.SampleRate)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ go 1.26.0
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.1
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
|
||||
github.com/ivanizag/iz6502 v1.4.0
|
||||
github.com/ivanizag/iz6502 v1.5.0
|
||||
github.com/koron-go/z80 v0.10.1
|
||||
github.com/pkg/profile v1.7.0
|
||||
github.com/veandco/go-sdl2 v0.4.40
|
||||
|
||||
@@ -72,8 +72,8 @@ github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrx
|
||||
github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/ivanizag/iz6502 v1.4.0 h1:7eYygUkCPwFRH0tf2JSg1k+Sy27wwPi3ActuVNVv1Uc=
|
||||
github.com/ivanizag/iz6502 v1.4.0/go.mod h1:h4gbw3IK6WCYawi00kBhQ4ACeQkGWgqbUeAgDaQpy6s=
|
||||
github.com/ivanizag/iz6502 v1.5.0 h1:hWNfjuZHbSjBZr1lzlFNgF6O1gCI/1NIHaQwa+W2+cQ=
|
||||
github.com/ivanizag/iz6502 v1.5.0/go.mod h1:h4gbw3IK6WCYawi00kBhQ4ACeQkGWgqbUeAgDaQpy6s=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
|
||||
+1
-11
@@ -11,7 +11,7 @@ type ioC0Page struct {
|
||||
softSwitchesWName [256]string
|
||||
softSwitchesData [128]uint8
|
||||
keyboard KeyboardProvider
|
||||
speaker SpeakerProvider
|
||||
speaker speakerAudioSource
|
||||
paddlesStrobeCycle uint64
|
||||
joysticks JoysticksProvider
|
||||
mouse MouseProvider
|
||||
@@ -24,12 +24,6 @@ type ioC0Page struct {
|
||||
type softSwitchR func() uint8
|
||||
type softSwitchW func(value uint8)
|
||||
|
||||
// SpeakerProvider provides a speaker implementation
|
||||
type SpeakerProvider interface {
|
||||
// Click receives a speaker click. The argument is the CPU cycle when it is generated
|
||||
Click(cycle uint64)
|
||||
}
|
||||
|
||||
// JoysticksProvider abstracts the joysticks
|
||||
type JoysticksProvider interface {
|
||||
ReadButton(i int) bool
|
||||
@@ -114,10 +108,6 @@ func (p *ioC0Page) setKeyboardProvider(kb KeyboardProvider) {
|
||||
p.keyboard = kb
|
||||
}
|
||||
|
||||
func (p *ioC0Page) setSpeakerProvider(s SpeakerProvider) {
|
||||
p.speaker = s
|
||||
}
|
||||
|
||||
func (p *ioC0Page) setJoysticksProvider(j JoysticksProvider) {
|
||||
p.joysticks = j
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user