izapple2/izapple2sdl/sdlSpeaker.go

187 lines
3.9 KiB
Go

package main
/*
typedef unsigned char Uint8;
void SpeakerCallback(void *userdata, Uint8 *stream, int len);
*/
import "C"
import (
"fmt"
"reflect"
"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:
// The channel is full, the click is lost.
}
}
func stateToLevel(state bool) C.Uint8 {
if state {
return 255
}
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
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 * izapple2.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 := 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
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
}
}
// 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
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 {
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()
}