izapple2/frontend/a2sdl/sdlSpeaker.go

185 lines
3.9 KiB
Go

package main
/*
typedef unsigned char Uint8;
void SpeakerCallback(void *userdata, Uint8 *stream, int len);
*/
import "C"
import (
"fmt"
"unsafe"
"github.com/ivanizag/izapple2"
"github.com/veandco/go-sdl2/sdl"
)
const (
samplingHz = 48000
bufferSize = 1000
// bufferSize/samplingHz will be the max delay of the sound
sampleDurationCycles = 1000000 * izapple2.CPUClockMhz / samplingHz
// each sample on the sound stream is 21.31 cpu cycles approx
maxOutOfSyncMs = 2000
decayLevel = 128
)
type sdlSpeaker struct {
clickChannel chan uint64
pendingClicks []uint64
lastCycle uint64
lastState bool
lastLevel C.Uint8
}
/*
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)
s.lastLevel = decayLevel // Mid position to avoid starting clicks.
return &s
}
// Click receives a speaker click. The argument is the CPU cycle when it is generated
func (s *sdlSpeaker) Click(cycle uint64) {
select {
case s.clickChannel <- cycle:
// Sent
default:
fmt.Printf("Speaker click dropped in channel.\n")
// The channel is full, the click is lost.
}
}
func stateToLevel(state bool) C.Uint8 {
if state {
return 200
}
return 0
}
// SpeakerCallback is called to get more sound buffer data
//export SpeakerCallback
func SpeakerCallback(userdata unsafe.Pointer, stream *C.Uint8, length C.int) {
s := theSDLSpeaker
if s == nil {
return
}
// Adapt C buffer
buf := unsafe.Slice(stream, length)
//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 * izapple2.CPUClockMhz * maxOutOfSyncMs
var maxOutOfSyncCycles = uint64(maxOutOfSyncCyclesFloat)
for _, pc := range s.pendingClicks {
if pc-s.lastCycle > maxOutOfSyncCycles {
// Fast forward
s.lastCycle = pc
fmt.Printf("Speaker fast forward.\n")
}
}
// Build wave
var i, r int
level := s.lastLevel
for p := 0; p < len(s.pendingClicks); p++ {
cycle := s.pendingClicks[p]
if cycle < s.lastCycle {
// Too old, ignore
continue
}
// Fill with samples
level = stateToLevel(s.lastState)
samplesNeeded := int(float64(cycle-s.lastCycle) / sampleDurationCycles)
if samplesNeeded+i > bufferSize {
// Partial fill, to be completed on the next callback
samplesNeeded = bufferSize - i
s.lastCycle = cycle - uint64(float64(samplesNeeded)*sampleDurationCycles)
} else {
s.lastCycle = cycle
s.lastState = !s.lastState
r++ // Remove this pending click
}
for j := 0; j < samplesNeeded; j++ {
buf[i] = level
i++
}
if i == bufferSize {
// Buffer is complete
break
}
}
// If the buffer is empty lets decay the signal
if i == 0 {
for level != decayLevel && i < bufferSize {
if i%100 == 0 {
if level > decayLevel {
level--
} else {
level++
}
}
buf[i] = level
i++
}
}
// Complete the buffer if needed
for b := i; b < bufferSize; b++ {
buf[b] = level
}
s.lastLevel = level
// Remove processed clicks, store the rest for later
s.pendingClicks = s.pendingClicks[r:]
}
func (s *sdlSpeaker) start() {
err := sdl.Init(sdl.INIT_AUDIO)
if err != nil {
fmt.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 {
fmt.Printf("Error opening the SDL audio channel: %v.\n", err)
return
}
sdl.PauseAudio(false)
theSDLSpeaker = s
}
func (s *sdlSpeaker) close() {
sdl.CloseAudio()
sdl.Quit()
}