Crude sound support
This commit is contained in:
parent
6fca02da6b
commit
e449ccd907
|
@ -57,10 +57,10 @@ func NewApple2(romFile string, charRomFile string, clockMhz float64, isColor boo
|
||||||
return &a
|
return &a
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDisk2 insterts a DiskII controller on slot 6
|
// AddDisk2 insterts a DiskII controller
|
||||||
func (a *Apple2) AddDisk2(diskRomFile string, diskImage string) {
|
func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) {
|
||||||
d := newCardDisk2(diskRomFile)
|
d := newCardDisk2(diskRomFile)
|
||||||
d.cardBase.insert(a, 6)
|
d.cardBase.insert(a, slot)
|
||||||
|
|
||||||
if diskImage != "" {
|
if diskImage != "" {
|
||||||
diskette := loadDisquette(diskImage)
|
diskette := loadDisquette(diskImage)
|
||||||
|
@ -90,6 +90,11 @@ func (a *Apple2) SetKeyboardProvider(kb KeyboardProvider) {
|
||||||
a.io.setKeyboardProvider(kb)
|
a.io.setKeyboardProvider(kb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetSpeakerProvider attaches an external keyboard provider
|
||||||
|
func (a *Apple2) SetSpeakerProvider(s SpeakerProvider) {
|
||||||
|
a.io.setSpeakerProvider(s)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
|
// CommandToggleSpeed toggles cpu speed between full speed and actual Apple II speed
|
||||||
CommandToggleSpeed = iota + 1
|
CommandToggleSpeed = iota + 1
|
||||||
|
|
|
@ -9,6 +9,7 @@ type ioC0Page struct {
|
||||||
softSwitchesW [256]softSwitchW
|
softSwitchesW [256]softSwitchW
|
||||||
softSwitchesData [128]uint8
|
softSwitchesData [128]uint8
|
||||||
keyboard KeyboardProvider
|
keyboard KeyboardProvider
|
||||||
|
speaker SpeakerProvider
|
||||||
apple2 *Apple2
|
apple2 *Apple2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +21,12 @@ type KeyboardProvider interface {
|
||||||
GetKey(strobe bool) (key uint8, ok bool)
|
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://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
|
// 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
|
p.keyboard = kb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ioC0Page) setSpeakerProvider(s SpeakerProvider) {
|
||||||
|
p.speaker = s
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ioC0Page) Peek(address uint8) uint8 {
|
func (p *ioC0Page) Peek(address uint8) uint8 {
|
||||||
//fmt.Printf("Peek on $C0%02x ", address)
|
//fmt.Printf("Peek on $C0%02x ", address)
|
||||||
ss := p.softSwitchesR[address]
|
ss := p.softSwitchesR[address]
|
||||||
|
|
|
@ -27,7 +27,7 @@ func addApple2SoftSwitches(io *ioC0Page) {
|
||||||
io.addSoftSwitchRW(0x00, getKeySoftSwitch) // Keyboard
|
io.addSoftSwitchRW(0x00, getKeySoftSwitch) // Keyboard
|
||||||
io.addSoftSwitchRW(0x10, strobeKeyboardSoftSwitch) // Keyboard Strobe
|
io.addSoftSwitchRW(0x10, strobeKeyboardSoftSwitch) // Keyboard Strobe
|
||||||
io.addSoftSwitchR(0x20, notImplementedSoftSwitchR) // Cassette Output
|
io.addSoftSwitchR(0x20, notImplementedSoftSwitchR) // Cassette Output
|
||||||
io.addSoftSwitchR(0x30, notImplementedSoftSwitchR) // Speaker
|
io.addSoftSwitchR(0x30, getSpeakerSoftSwitch) // Speaker
|
||||||
io.addSoftSwitchR(0x40, notImplementedSoftSwitchR) // Game connector Strobe
|
io.addSoftSwitchR(0x40, notImplementedSoftSwitchR) // Game connector Strobe
|
||||||
// Note: Some sources indicate that all these cover 16 positions
|
// Note: Some sources indicate that all these cover 16 positions
|
||||||
// for read and write. But the Apple2e take over some of them, with
|
// 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 {
|
func getKeySoftSwitch(io *ioC0Page) uint8 {
|
||||||
strobed := (io.softSwitchesData[ioDataKeyboard] & (1 << 7)) == 0
|
strobed := (io.softSwitchesData[ioDataKeyboard] & (1 << 7)) == 0
|
||||||
if io.keyboard != nil {
|
if io.keyboard != nil {
|
||||||
|
|
|
@ -10,6 +10,9 @@ import (
|
||||||
|
|
||||||
// SDLRun starts the Apple2 emulator on SDL
|
// SDLRun starts the Apple2 emulator on SDL
|
||||||
func SDLRun(a *apple2.Apple2) {
|
func SDLRun(a *apple2.Apple2) {
|
||||||
|
s := newSdlSpeaker()
|
||||||
|
s.start()
|
||||||
|
|
||||||
window, renderer, err := sdl.CreateWindowAndRenderer(4*40*7, 4*24*8,
|
window, renderer, err := sdl.CreateWindowAndRenderer(4*40*7, 4*24*8,
|
||||||
sdl.WINDOW_SHOWN)
|
sdl.WINDOW_SHOWN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -22,7 +25,8 @@ func SDLRun(a *apple2.Apple2) {
|
||||||
window.SetTitle("Apple2")
|
window.SetTitle("Apple2")
|
||||||
|
|
||||||
kp := newSDLKeyBoard(a)
|
kp := newSDLKeyBoard(a)
|
||||||
a.SetKeyboardProvider(&kp)
|
a.SetKeyboardProvider(kp)
|
||||||
|
a.SetSpeakerProvider(s)
|
||||||
go a.Run(false)
|
go a.Run(false)
|
||||||
|
|
||||||
running := true
|
running := true
|
||||||
|
|
|
@ -12,11 +12,11 @@ type sdlKeyboard struct {
|
||||||
a *apple2.Apple2
|
a *apple2.Apple2
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSDLKeyBoard(a *apple2.Apple2) sdlKeyboard {
|
func newSDLKeyBoard(a *apple2.Apple2) *sdlKeyboard {
|
||||||
var k sdlKeyboard
|
var k sdlKeyboard
|
||||||
k.keyChannel = make(chan uint8, 100)
|
k.keyChannel = make(chan uint8, 100)
|
||||||
k.a = a
|
k.a = a
|
||||||
return k
|
return &k
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *sdlKeyboard) putText(textEvent *sdl.TextInputEvent) {
|
func (k *sdlKeyboard) putText(textEvent *sdl.TextInputEvent) {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import "fmt"
|
||||||
type State struct {
|
type State struct {
|
||||||
reg registers
|
reg registers
|
||||||
mem Memory
|
mem Memory
|
||||||
cycles int64
|
cycles uint64
|
||||||
opcodes *[256]opcode
|
opcodes *[256]opcode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ func (s *State) ExecuteInstruction(log bool) {
|
||||||
fmt.Printf("%#04x %-12s: ", pc, lineString(line, opcode))
|
fmt.Printf("%#04x %-12s: ", pc, lineString(line, opcode))
|
||||||
}
|
}
|
||||||
opcode.action(s, line, opcode)
|
opcode.action(s, line, opcode)
|
||||||
s.cycles += int64(opcode.cycles)
|
s.cycles += uint64(opcode.cycles)
|
||||||
if log {
|
if log {
|
||||||
fmt.Printf("%v, [%02x]\n", s.reg, line)
|
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.
|
// GetCycles returns the count of CPU cycles since last reset.
|
||||||
func (s *State) GetCycles() int64 {
|
func (s *State) GetCycles() uint64 {
|
||||||
return s.cycles
|
return s.cycles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
main.go
8
main.go
|
@ -15,6 +15,10 @@ func main() {
|
||||||
"diskRom",
|
"diskRom",
|
||||||
"apple2/romdumps/DISK2.rom",
|
"apple2/romdumps/DISK2.rom",
|
||||||
"rom file for the disk drive controller")
|
"rom file for the disk drive controller")
|
||||||
|
disk2Slot := flag.Int(
|
||||||
|
"disk2Slot",
|
||||||
|
6,
|
||||||
|
"slot for the disk driver. 0 for none.")
|
||||||
diskImage := flag.String(
|
diskImage := flag.String(
|
||||||
"disk",
|
"disk",
|
||||||
"../dos33.dsk",
|
"../dos33.dsk",
|
||||||
|
@ -59,7 +63,9 @@ func main() {
|
||||||
|
|
||||||
log := false
|
log := false
|
||||||
a := apple2.NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *panicSS)
|
a := apple2.NewApple2(*romFile, *charRomFile, *cpuClock, !*mono, *panicSS)
|
||||||
a.AddDisk2(*disk2RomFile, *diskImage)
|
if *disk2Slot > 0 {
|
||||||
|
a.AddDisk2(*disk2Slot, *disk2RomFile, *diskImage)
|
||||||
|
}
|
||||||
if *useSdl {
|
if *useSdl {
|
||||||
a.ConfigureStdConsole(false, *stdoutScreen)
|
a.ConfigureStdConsole(false, *stdoutScreen)
|
||||||
apple2sdl.SDLRun(a)
|
apple2sdl.SDLRun(a)
|
||||||
|
|
Loading…
Reference in New Issue