fix(audio): copy Opus frame data before passing to consumer

DecodeOggOpus passes a slice from bytes.Buffer.Bytes() to the onFrame
callback, then calls packet.Reset(). Reset reuses the underlying array,
so when the next packet is assembled the previous slice's data is
overwritten. In Discord voice playback the frames are sent to a buffered
channel (OpusSend, cap 16); by the time opusSender reads a frame, its
backing memory has been mutated by subsequent decoder iterations.

The resulting Opus frame corruption causes audible artifacts: garbled or
whispery audio depending on how much of each frame is overwritten before
the voice sender transmits it.

Fix: allocate a dedicated copy of each frame before handing it to the
callback, decoupling the consumer's data from the reusable buffer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
johncranneyscw 2026-04-13 23:25:37 +10:00
parent 2e149f44dd
commit 989e5a554c

View file

@ -40,11 +40,17 @@ func DecodeOggOpus(r io.Reader, onFrame func([]byte) error) error {
// If lacing is less than 255, the packet is complete // If lacing is less than 255, the packet is complete
if lacing < 255 { if lacing < 255 {
if packet.Len() > 0 { if packet.Len() > 0 {
packetBytes := packet.Bytes() raw := packet.Bytes()
// Ignore Ogg Opus headers // Ignore Ogg Opus headers
if !bytes.HasPrefix(packetBytes, []byte("OpusHead")) && if !bytes.HasPrefix(raw, []byte("OpusHead")) &&
!bytes.HasPrefix(packetBytes, []byte("OpusTags")) { !bytes.HasPrefix(raw, []byte("OpusTags")) {
if err := onFrame(packetBytes); err != nil { // Copy the frame data: packet.Reset() reuses the
// underlying array, so the slice would be
// overwritten by subsequent packets before the
// consumer (e.g. OpusSend channel) reads it.
frame := make([]byte, len(raw))
copy(frame, raw)
if err := onFrame(frame); err != nil {
return err return err
} }
} }