diff --git a/internal/apu/apu.go b/internal/apu/apu.go index 59c75c4e..11b6686c 100644 --- a/internal/apu/apu.go +++ b/internal/apu/apu.go @@ -1,10 +1,8 @@ package apu import ( - "bytes" "log/slog" "math" - "sync" "gabe565.com/gones/internal/config" "gabe565.com/gones/internal/consts" @@ -59,13 +57,13 @@ func New(conf *config.Config) *APU { Enabled: true, SampleRate: DefaultSampleRate, conf: &conf.Audio, + buf: newRingBuffer(BufferCap), Square: [2]Square{{Channel1: true}, {}}, Noise: Noise{ShiftRegister: 1}, FramePeriod: 4, } - a.buf.Grow(BufferCap) return a } @@ -73,6 +71,7 @@ type APU struct { Enabled bool `msgpack:"-"` SampleRate float64 `msgpack:"-"` conf *config.Audio + buf *ringBuffer Square [2]Square Triangle Triangle @@ -85,9 +84,6 @@ type APU struct { IRQEnabled bool `msgpack:"alias:IrqEnabled"` IRQPending bool `msgpack:"alias:IrqPending"` - - buf bytes.Buffer - mu sync.Mutex } func (a *APU) WriteMem(addr uint16, data byte) { @@ -265,31 +261,21 @@ func (a *APU) output() float32 { func (a *APU) sendSample() { result := a.output() b := math.Float32bits(result) - a.mu.Lock() - defer a.mu.Unlock() - if a.buf.Len() < BufferCap { - a.buf.Write([]byte{ - byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24), - byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24), - }) - } + a.buf.Write([]byte{ + byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24), + byte(b), byte(b >> 8), byte(b >> 16), byte(b >> 24), + }) } func (a *APU) Clear() { - a.mu.Lock() - defer a.mu.Unlock() a.buf.Reset() } func (a *APU) Read(p []byte) (int, error) { - a.mu.Lock() - n, err := a.buf.Read(p) - a.mu.Unlock() - if err != nil { - if n == 0 { - clear(p) - } + n := a.buf.Read(p) + if n == 0 { + clear(p) return len(p), nil } - return n, err + return n, nil } diff --git a/internal/apu/ringbuffer.go b/internal/apu/ringbuffer.go new file mode 100644 index 00000000..4209bcf7 --- /dev/null +++ b/internal/apu/ringbuffer.go @@ -0,0 +1,132 @@ +package apu + +import ( + "sync" +) + +type ringBuffer struct { + buf []byte + size int + r, w int + full bool + mu sync.Mutex +} + +func newRingBuffer(size int) *ringBuffer { + return &ringBuffer{ + buf: make([]byte, size), + size: size, + } +} + +func (r *ringBuffer) Read(p []byte) int { + r.mu.Lock() + defer r.mu.Unlock() + + var n int + switch { + case r.w == r.r && !r.full: + // Buffer empty + return 0 + case r.w > r.r: + // Writer ahead of reader + if n = r.w - r.r; n > len(p) { + n = len(p) + } + copy(p, r.buf[r.r:r.r+n]) + default: + // Reader ahead of writer + if n = r.size - r.r + r.w; n > len(p) { + n = len(p) + } + + if r.r+n <= r.size { + // End of buffer has enough elements + copy(p, r.buf[r.r:r.r+n]) + } else { + // End of buffer does not have enough elements; read will wrap around + c1 := r.size - r.r + copy(p, r.buf[r.r:r.size]) + c2 := n - c1 + copy(p[c1:], r.buf[0:c2]) + } + + r.full = false + } + r.r = (r.r + n) % r.size + return n +} + +func (r *ringBuffer) Write(p []byte) { + r.mu.Lock() + defer r.mu.Unlock() + + n := len(p) + switch { + case r.free() < len(p): + // Buffer too full; discard write + return + case r.w >= r.r: + // Writer ahead of reader + if c1 := r.size - r.w; c1 >= n { + // Slice fits in the end of the buffer + copy(r.buf[r.w:], p) + r.w += n + } else { + // Slice does not fit in the end of the buffer; write will wrap around + copy(r.buf[r.w:], p[:c1]) + c2 := n - c1 + copy(r.buf[0:], p[c1:]) + r.w = c2 + } + default: + // Reader is ahead of writer + copy(r.buf[r.w:], p) + r.w += n + } + + if r.w == r.size { + r.w = 0 + } + r.full = r.w == r.r +} + +func (r *ringBuffer) len() int { + r.mu.Lock() + defer r.mu.Unlock() + + switch { + case r.w == r.r: + if r.full { + return r.size + } + return 0 + case r.w > r.r: + return r.w - r.r + default: + return r.size - r.r + r.w + } +} + +func (r *ringBuffer) free() int { + switch { + case r.w == r.r: + if r.full { + return 0 + } + return r.size + case r.w < r.r: + return r.r - r.w + default: + return r.size - r.w + r.r + } +} + +func (r *ringBuffer) Reset() { + r.mu.Lock() + defer r.mu.Unlock() + + r.r = 0 + r.w = 0 + r.full = false +} diff --git a/internal/apu/ringbuffer_test.go b/internal/apu/ringbuffer_test.go new file mode 100644 index 00000000..3075f407 --- /dev/null +++ b/internal/apu/ringbuffer_test.go @@ -0,0 +1,40 @@ +package apu + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ringBuffer(t *testing.T) { + const bufSize = 16 + + buf := newRingBuffer(16) + p := make([]byte, bufSize) + n := buf.Read(p) + assert.Equal(t, 0, n) + assert.Equal(t, p, make([]byte, bufSize)) + assert.Equal(t, bufSize, buf.free()) + assert.Equal(t, 0, buf.len()) + + const message = "hello world" + for range 3 { + buf.Write([]byte(message)) + assert.Equal(t, bufSize-len(message), buf.free()) + assert.Equal(t, len(message), buf.len()) + + n = buf.Read(p) + assert.Equal(t, len(message), n) + assert.Equal(t, []byte(message), p[:n]) + assert.Equal(t, bufSize, buf.free()) + assert.Equal(t, 0, buf.len()) + } + + buf.Write([]byte(message)) + p = make([]byte, 5) + n = buf.Read(p) + assert.Equal(t, bufSize-len(message)+5, buf.free()) + assert.Equal(t, len(message)-5, buf.len()) + assert.Equal(t, 5, n) + assert.Equal(t, []byte("hello"), p[:n]) +}