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() }