Fix recording truncation and rate mismatch causing static
The recording pipeline had two bugs that combined to produce buzzing/static in recorded audio: 1. NormalizeStereoFrame truncated incoming audio frames to frameSize*AudioChannels samples. When the Opus decoder produced 20ms frames (1920 stereo samples) but the recorder used a 10ms frameSize (960 samples), half the audio from every packet was silently dropped. 2. The recorder's run() loop dequeued one fixed-length frame per source per tick. After truncation, the remaining 10ms of each 20ms packet was gone, so every other tick produced silence. This 50 Hz on/off pattern sounded like static. Fixes: - NormalizeStereoFrame no longer truncates; it only converts mono to stereo and preserves all audio data - RecordAudioFrame now accepts an explicit stereo flag from callers instead of guessing from sample count (which failed for even-length mono data like 480-sample mic frames) - run() accumulates variable-length frames per source and consumes them in fixed-size chunks, preserving any leftover for the next tick
This commit is contained in:
committed by
Brandon McGinty
parent
cd003d7ac4
commit
90f8a1ca5f
@@ -45,7 +45,7 @@ type FilePlayer interface {
|
||||
}
|
||||
|
||||
type Recorder interface {
|
||||
RecordAudioFrame(source uint32, samples []int16)
|
||||
RecordAudioFrame(source uint32, samples []int16, stereo bool)
|
||||
}
|
||||
|
||||
const recorderOutgoingSource uint32 = ^uint32(0)
|
||||
@@ -742,7 +742,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
||||
}
|
||||
}
|
||||
if recorder != nil && recordPtr > 0 {
|
||||
recorder.RecordAudioFrame(user.Session, recordBuffer[:recordPtr])
|
||||
recorder.RecordAudioFrame(user.Session, recordBuffer[:recordPtr], true)
|
||||
}
|
||||
|
||||
if len(emptyBufs) == 0 {
|
||||
@@ -932,7 +932,7 @@ func (s *Stream) sourceRoutine(inputDevice *string, stop chan bool, done chan st
|
||||
// Send stereo buffer when file is playing
|
||||
outgoing <- gumble.AudioBuffer(outputBuffer)
|
||||
if recorder := s.getRecorder(); recorder != nil {
|
||||
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer)
|
||||
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer, true)
|
||||
}
|
||||
} else if hasMicInput {
|
||||
// Send mic when no file is playing. If the microphone is
|
||||
@@ -949,7 +949,7 @@ func (s *Stream) sourceRoutine(inputDevice *string, stop chan bool, done chan st
|
||||
}
|
||||
outgoing <- gumble.AudioBuffer(outBuf)
|
||||
if recorder := s.getRecorder(); recorder != nil {
|
||||
recorder.RecordAudioFrame(recorderOutgoingSource, outBuf)
|
||||
recorder.RecordAudioFrame(recorderOutgoingSource, outBuf, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-25
@@ -152,14 +152,14 @@ func (r *Recorder) Path() string {
|
||||
return r.path
|
||||
}
|
||||
|
||||
func (r *Recorder) RecordAudioFrame(source uint32, samples []int16) {
|
||||
func (r *Recorder) RecordAudioFrame(source uint32, samples []int16, stereo bool) {
|
||||
if r == nil || len(samples) == 0 {
|
||||
return
|
||||
}
|
||||
if len(r.input) >= cap(r.input) {
|
||||
return
|
||||
}
|
||||
frame := NormalizeStereoFrame(samples, r.frameSize)
|
||||
frame := NormalizeStereoFrame(samples, stereo)
|
||||
select {
|
||||
case r.input <- sourceFrame{source: source, samples: frame}:
|
||||
default:
|
||||
@@ -192,31 +192,35 @@ func (r *Recorder) run() {
|
||||
defer close(r.done)
|
||||
ticker := time.NewTicker(r.interval)
|
||||
defer ticker.Stop()
|
||||
queues := make(map[uint32][][]int16)
|
||||
frame := make([]int16, r.frameSize*gumble.AudioChannels)
|
||||
// Per-source accumulated stereo samples. Incoming frames of any size are
|
||||
// appended and then consumed in frameSize*AudioChannels chunks each tick.
|
||||
queues := make(map[uint32][]int16)
|
||||
chunkSize := r.frameSize * gumble.AudioChannels
|
||||
chunk := make([]int16, chunkSize)
|
||||
for {
|
||||
select {
|
||||
case <-r.stop:
|
||||
r.closeEncoder()
|
||||
return
|
||||
case item := <-r.input:
|
||||
queues[item.source] = append(queues[item.source], item.samples)
|
||||
queues[item.source] = append(queues[item.source], item.samples...)
|
||||
case <-ticker.C:
|
||||
clear(frame)
|
||||
for source, queue := range queues {
|
||||
if len(queue) == 0 {
|
||||
clear(chunk)
|
||||
for source, buffer := range queues {
|
||||
if len(buffer) == 0 {
|
||||
delete(queues, source)
|
||||
continue
|
||||
}
|
||||
mix(frame, queue[0])
|
||||
queue = queue[1:]
|
||||
if len(queue) == 0 {
|
||||
// Mix one chunk worth of samples from this source.
|
||||
if len(buffer) <= chunkSize {
|
||||
mix(chunk, buffer)
|
||||
delete(queues, source)
|
||||
} else {
|
||||
queues[source] = queue
|
||||
mix(chunk, buffer[:chunkSize])
|
||||
queues[source] = buffer[chunkSize:]
|
||||
}
|
||||
}
|
||||
if err := writePCM(r.stdin, frame); err != nil {
|
||||
if err := writePCM(r.stdin, chunk); err != nil {
|
||||
r.setError(err)
|
||||
r.closeEncoder()
|
||||
return
|
||||
@@ -248,19 +252,19 @@ func (r *Recorder) setError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeStereoFrame(samples []int16, frameSize int) []int16 {
|
||||
out := make([]int16, frameSize*gumble.AudioChannels)
|
||||
if len(samples) >= frameSize*gumble.AudioChannels && len(samples)%gumble.AudioChannels == 0 {
|
||||
copy(out, samples[:frameSize*gumble.AudioChannels])
|
||||
return out
|
||||
// NormalizeStereoFrame ensures samples are in stereo interleaved format.
|
||||
// If stereo is true the samples are returned as-is (already interleaved).
|
||||
// Mono input is duplicated to both channels. The returned slice preserves
|
||||
// all input audio without truncation.
|
||||
func NormalizeStereoFrame(samples []int16, stereo bool) []int16 {
|
||||
if stereo {
|
||||
return samples
|
||||
}
|
||||
limit := frameSize
|
||||
if len(samples) < limit {
|
||||
limit = len(samples)
|
||||
}
|
||||
for i := 0; i < limit; i++ {
|
||||
out[i*2] = samples[i]
|
||||
out[i*2+1] = samples[i]
|
||||
// Convert mono to stereo by duplicating each sample.
|
||||
out := make([]int16, len(samples)*gumble.AudioChannels)
|
||||
for i, s := range samples {
|
||||
out[i*2] = s
|
||||
out[i*2+1] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -93,16 +93,36 @@ func TestReservePathPreventsConcurrentRecordingCollisions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNormalizeStereoFrame(t *testing.T) {
|
||||
mono := NormalizeStereoFrame([]int16{1, -2}, 3)
|
||||
wantMono := []int16{1, 1, -2, -2, 0, 0}
|
||||
// Mono input duplicating each sample to both channels.
|
||||
mono := NormalizeStereoFrame([]int16{1, -2, 3}, false)
|
||||
wantMono := []int16{1, 1, -2, -2, 3, 3}
|
||||
if len(mono) != len(wantMono) {
|
||||
t.Fatalf("mono len = %d, want %d", len(mono), len(wantMono))
|
||||
}
|
||||
for i := range wantMono {
|
||||
if mono[i] != wantMono[i] {
|
||||
t.Fatalf("mono[%d] = %d, want %d", i, mono[i], wantMono[i])
|
||||
}
|
||||
}
|
||||
|
||||
stereo := NormalizeStereoFrame([]int16{1, 2, 3, 4, 5, 6}, 2)
|
||||
wantStereo := []int16{1, 2, 3, 4}
|
||||
// Even-length mono must not be mistaken for stereo.
|
||||
monoEven := NormalizeStereoFrame([]int16{1, -2}, false)
|
||||
wantMonoEven := []int16{1, 1, -2, -2}
|
||||
if len(monoEven) != len(wantMonoEven) {
|
||||
t.Fatalf("monoEven len = %d, want %d", len(monoEven), len(wantMonoEven))
|
||||
}
|
||||
for i := range wantMonoEven {
|
||||
if monoEven[i] != wantMonoEven[i] {
|
||||
t.Fatalf("monoEven[%d] = %d, want %d", i, monoEven[i], wantMonoEven[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Stereo input passes through unchanged.
|
||||
stereo := NormalizeStereoFrame([]int16{1, 2, 3, 4, 5, 6}, true)
|
||||
wantStereo := []int16{1, 2, 3, 4, 5, 6}
|
||||
if len(stereo) != len(wantStereo) {
|
||||
t.Fatalf("stereo len = %d, want %d", len(stereo), len(wantStereo))
|
||||
}
|
||||
for i := range wantStereo {
|
||||
if stereo[i] != wantStereo[i] {
|
||||
t.Fatalf("stereo[%d] = %d, want %d", i, stereo[i], wantStereo[i])
|
||||
|
||||
Reference in New Issue
Block a user