From e449ccd907078b1318024ade5627e20b81fb22cd Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Fri, 10 May 2019 00:09:15 +0200 Subject: [PATCH] Crude sound support --- apple2/apple2.go | 11 ++- apple2/ioC0Page.go | 11 +++ apple2/softSwitches2.go | 9 ++- apple2sdl/run.go | 6 +- apple2sdl/sdlKeyboard.go | 4 +- apple2sdl/sdlSpeaker.go | 161 +++++++++++++++++++++++++++++++++++++++ core6502/execute.go | 6 +- main.go | 8 +- 8 files changed, 205 insertions(+), 11 deletions(-) create mode 100644 apple2sdl/sdlSpeaker.go diff --git a/apple2/apple2.go b/apple2/apple2.go index 891eebf..336cb3a 100644 --- a/apple2/apple2.go +++ b/apple2/apple2.go @@ -57,10 +57,10 @@ func NewApple2(romFile string, charRomFile string, clockMhz float64, isColor boo return &a } -// AddDisk2 insterts a DiskII controller on slot 6 -func (a *Apple2) AddDisk2(diskRomFile string, diskImage string) { +// AddDisk2 insterts a DiskII controller +func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) { d := newCardDisk2(diskRomFile) - d.cardBase.insert(a, 6) + d.cardBase.insert(a, slot) if diskImage != "" { diskette := loadDisquette(diskImage) @@ -90,6 +90,11 @@ func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) { a.io.setKeyboardProvider(kb) } +// SetSpeakerProvider attaches an external keyboard provider +func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) { + a.io.setSpeakerProvider(s) +} + const ( // CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed CommandToggleSpeed = iota + 1 diff --git a/apple2/ioC0Page.go b/apple2/ioC0Page.go index d73cccb..ea8f34c 100644 --- a/apple2/ioC0Page.go +++ b/apple2/ioC0Page.go @@ -9,6 +9,7 @@ type ioC0Page struct { softSwitchesW [256]softSwitchW softSwitchesData [128]uint8 keyboard KeyboardProvider + speaker SpeakerProvider apple2 *Apple2 } @@ -20,6 +21,12 @@ type KeyboardProvider interface { GetKey(strobe bool) (key uint8, ok bool) } +// SpeakerProvider declares the speaker implementation requirements +type SpeakerProvider interface { + // Click receives a speaker click. The argument is the CPU cycle when it is generated + Click(cycle uint64) +} + // See https://www.kreativekorp.com/miscpages/a2info/iomemory.shtml // See https://stason.org/TULARC/pc/apple2/programmer/004-I-d-like-to-do-some-serious-Apple-II-programming-Whe.html @@ -69,6 +76,10 @@ func (p *ioC0Page) setKeyboardProvider(kb KeyboardProvider) { p.keyboard = kb } +func (p *ioC0Page) setSpeakerProvider(s SpeakerProvider) { + p.speaker = s +} + func (p *ioC0Page) Peek(address uint8) uint8 { //fmt.Printf("Peek on $C0%02x ", address) ss := p.softSwitchesR[address] diff --git a/apple2/softSwitches2.go b/apple2/softSwitches2.go index 66da090..eb562fd 100644 --- a/apple2/softSwitches2.go +++ b/apple2/softSwitches2.go @@ -27,7 +27,7 @@ func addApple2SoftSwitches(io *ioC0Page) { io.addSoftSwitchRW(0x00, getKeySoftSwitch) // Keyboard io.addSoftSwitchRW(0x10, strobeKeyboardSoftSwitch) // Keyboard Strobe io.addSoftSwitchR(0x20, notImplementedSoftSwitchR) // Cassette Output - io.addSoftSwitchR(0x30, notImplementedSoftSwitchR) // Speaker + io.addSoftSwitchR(0x30, getSpeakerSoftSwitch) // Speaker io.addSoftSwitchR(0x40, notImplementedSoftSwitchR) // Game connector Strobe // Note: Some sources indicate that all these cover 16 positions // for read and write. But the Apple2e take over some of them, with @@ -93,6 +93,13 @@ func getSoftSwitch(ioFlag uint8, isSet bool) softSwitchR { } } +func getSpeakerSoftSwitch(io *ioC0Page) uint8 { + if io.speaker != nil { + io.speaker.Click(io.apple2.cpu.GetCycles()) + } + return 0 +} + func getKeySoftSwitch(io *ioC0Page) uint8 { strobed := (io.softSwitchesData[ioDataKeyboard] & (1 << 7)) == 0 if io.keyboard != nil { diff --git a/apple2sdl/run.go b/apple2sdl/run.go index d66ddcb..0598a31 100644 --- a/apple2sdl/run.go +++ b/apple2sdl/run.go @@ -10,6 +10,9 @@ import ( // SDLRun starts the Apple2 emulator on SDL func SDLRun(a *apple2.Apple2) { + s := newSdlSpeaker() + s.start() + window, renderer, err := sdl.CreateWindowAndRenderer(4*40*7, 4*24*8, sdl.WINDOW_SHOWN) if err != nil { @@ -22,7 +25,8 @@ func SDLRun(a *apple2.Apple2) { window.SetTitle("Apple2") kp := newSDLKeyBoard(a) - a.SetKeyboardProvider(&kp) + a.SetKeyboardProvider(kp) + a.SetSpeakerProvider(s) go a.Run(false) running := true diff --git a/apple2sdl/sdlKeyboard.go b/apple2sdl/sdlKeyboard.go index 534d284..ab80bf4 100644 --- a/apple2sdl/sdlKeyboard.go +++ b/apple2sdl/sdlKeyboard.go @@ -12,11 +12,11 @@ type sdlKeyboard struct { a *apple2.Apple2 } -func newSDLKeyBoard(a *apple2.Apple2) sdlKeyboard { +func newSDLKeyBoard(a *apple2.Apple2) *sdlKeyboard { var k sdlKeyboard k.keyChannel = make(chan uint8, 100) k.a = a - return k + return &k } func (k *sdlKeyboard) putText(textEvent *sdl.TextInputEvent) { diff --git a/apple2sdl/sdlSpeaker.go b/apple2sdl/sdlSpeaker.go new file mode 100644 index 0000000..b6c0af4 --- /dev/null +++ b/apple2sdl/sdlSpeaker.go @@ -0,0 +1,161 @@ +package apple2sdl + +/* +typedef unsigned char Uint8; +void SpeakerCallback(void *userdata, Uint8 *stream, int len); +*/ +import "C" +import ( + "go6502/apple2" + "log" + "reflect" + "unsafe" + + "github.com/veandco/go-sdl2/sdl" +) + +const ( + samplingHz = 48000 + bufferSize = 10000 + // bufferSize/samplingHz will be the max delay of the sound + sampleDurationCycles = 1000000 * apple2.CpuClockMhz / samplingHz + // each sample on the sound stream is 21.31 cpu cycles approx + maxOutOfSyncMs = 2000 +) + +type sdlSpeaker struct { + clickChannel chan uint64 + pendingClicks []uint64 + lastCycle uint64 + lastState bool +} + +/* +I have not found a way to encode the pointer to sdlSpeaker on the userdata of +the call to SpeakerCallback(). I use a global as workaround... +*/ +var theSdlSpeaker *sdlSpeaker + +func newSdlSpeaker() *sdlSpeaker { + var s sdlSpeaker + s.clickChannel = make(chan uint64, bufferSize) + s.pendingClicks = make([]uint64, 0, bufferSize) + return &s +} + +// Click receives a speaker click. The argument is the CPU cycle when it is generated +func (s *sdlSpeaker) Click(cycle uint64) { + s.clickChannel <- cycle +} + +func stateToLevel(state bool) C.Uint8 { + if state { + return 255 + } + return 0 +} + +//export SpeakerCallback +func SpeakerCallback(userdata unsafe.Pointer, stream *C.Uint8, length C.int) { + s := theSdlSpeaker + if s == nil { + return + } + + // Adapt C buffer + n := int(length) + hdr := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(stream)), Len: n, Cap: n} + buf := *(*[]C.Uint8)(unsafe.Pointer(&hdr)) + + //Read queued clicks + done := false + for !done { + select { + case cycle := <-s.clickChannel: + s.pendingClicks = append(s.pendingClicks, cycle) + default: + done = true + } + } + + // Verify that we are not too long behind + var maxOutOfSyncCyclesFloat = 1000 * apple2.CpuClockMhz * maxOutOfSyncMs + var maxOutOfSyncCycles = uint64(maxOutOfSyncCyclesFloat) + for _, pc := range s.pendingClicks { + if pc-s.lastCycle > maxOutOfSyncCycles { + // Fast forward + s.lastCycle = pc + } + } + + // Build wave + var i, p int + level := stateToLevel(s.lastState) + for p = 0; p < len(s.pendingClicks); p++ { + cycle := s.pendingClicks[p] + if cycle < s.lastCycle { + // Too old, ignore + continue + } + + // Fill with samples + samplesNeeded := int(float64(cycle-s.lastCycle) / sampleDurationCycles) + if samplesNeeded+i > bufferSize { + samplesNeeded = bufferSize - i + } + for j := 0; j < samplesNeeded; j++ { + buf[i] = level + i++ + } + + // Update state + s.lastCycle = cycle + s.lastState = !s.lastState + level = stateToLevel(s.lastState) + + if i == bufferSize { + // Buffer is complete + break + } + } + + // Complete the buffer if needed + for b := i; b < bufferSize; b++ { + buf[b] = level + } + + // Remove processed clicks, store the rest for later + remainingClicks := len(s.pendingClicks) - p + for r := 0; r < remainingClicks; r++ { + s.pendingClicks[r] = s.pendingClicks[p+r] + } + s.pendingClicks = s.pendingClicks[0:remainingClicks] +} + +func (s *sdlSpeaker) start() { + err := sdl.Init(sdl.INIT_AUDIO) + if err != nil { + log.Printf("Error starting SDL audio: %v.\n", err) + return + } + + spec := &sdl.AudioSpec{ + Freq: samplingHz, + Format: sdl.AUDIO_U8, + Channels: 1, + Samples: bufferSize, + Callback: sdl.AudioCallback(C.SpeakerCallback), + } + + if err := sdl.OpenAudio(spec, nil); err != nil { + log.Printf("Error opening the SDL audio channel: %v.\n", err) + return + } + sdl.PauseAudio(false) + theSdlSpeaker = s +} + +func (s *sdlSpeaker) close() { + sdl.CloseAudio() + sdl.Quit() +} diff --git a/core6502/execute.go b/core6502/execute.go index e53515b..38a3018 100644 --- a/core6502/execute.go +++ b/core6502/execute.go @@ -11,7 +11,7 @@ import "fmt" type State struct { reg registers mem Memory - cycles int64 + cycles uint64 opcodes *[256]opcode } @@ -60,7 +60,7 @@ func (s *State) ExecuteInstruction(log bool) { fmt.Printf("%#04x %-12s: ", pc, lineString(line, opcode)) } opcode.action(s, line, opcode) - s.cycles += int64(opcode.cycles) + s.cycles += uint64(opcode.cycles) if log { fmt.Printf("%v, [%02x]\n", s.reg, line) } @@ -74,7 +74,7 @@ func (s *State) Reset() { } // GetCycles returns the count of CPU cycles since last reset. -func (s *State) GetCycles() int64 { +func (s *State) GetCycles() uint64 { return s.cycles } diff --git a/main.go b/main.go index ec785f1..8834950 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,10 @@ func main() { "diskRom", "apple2/romdumps/DISK2.rom", "rom file for the disk drive controller") + disk2Slot := flag.Int( + "disk2Slot", + 6, + "slot for the disk driver. 0 for none.") diskImage := flag.String( "disk", "../dos33.dsk", @@ -59,7 +63,9 @@ func main() { log := false a := apple2.NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *panicSS) - a.AddDisk2(*disk2RomFile, *diskImage) + if *disk2Slot > 0 { + a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage) + } if *useSdl { a.ConfigureStdConsole(false, *stdoutScreen) apple2sdl.SDLRun(a)