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 {
|
type Recorder interface {
|
||||||
RecordAudioFrame(source uint32, samples []int16)
|
RecordAudioFrame(source uint32, samples []int16, stereo bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
const recorderOutgoingSource uint32 = ^uint32(0)
|
const recorderOutgoingSource uint32 = ^uint32(0)
|
||||||
@@ -742,7 +742,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if recorder != nil && recordPtr > 0 {
|
if recorder != nil && recordPtr > 0 {
|
||||||
recorder.RecordAudioFrame(user.Session, recordBuffer[:recordPtr])
|
recorder.RecordAudioFrame(user.Session, recordBuffer[:recordPtr], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(emptyBufs) == 0 {
|
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
|
// Send stereo buffer when file is playing
|
||||||
outgoing <- gumble.AudioBuffer(outputBuffer)
|
outgoing <- gumble.AudioBuffer(outputBuffer)
|
||||||
if recorder := s.getRecorder(); recorder != nil {
|
if recorder := s.getRecorder(); recorder != nil {
|
||||||
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer)
|
recorder.RecordAudioFrame(recorderOutgoingSource, outputBuffer, true)
|
||||||
}
|
}
|
||||||
} else if hasMicInput {
|
} else if hasMicInput {
|
||||||
// Send mic when no file is playing. If the microphone is
|
// 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)
|
outgoing <- gumble.AudioBuffer(outBuf)
|
||||||
if recorder := s.getRecorder(); recorder != nil {
|
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
|
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 {
|
if r == nil || len(samples) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(r.input) >= cap(r.input) {
|
if len(r.input) >= cap(r.input) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
frame := NormalizeStereoFrame(samples, r.frameSize)
|
frame := NormalizeStereoFrame(samples, stereo)
|
||||||
select {
|
select {
|
||||||
case r.input <- sourceFrame{source: source, samples: frame}:
|
case r.input <- sourceFrame{source: source, samples: frame}:
|
||||||
default:
|
default:
|
||||||
@@ -192,31 +192,35 @@ func (r *Recorder) run() {
|
|||||||
defer close(r.done)
|
defer close(r.done)
|
||||||
ticker := time.NewTicker(r.interval)
|
ticker := time.NewTicker(r.interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
queues := make(map[uint32][][]int16)
|
// Per-source accumulated stereo samples. Incoming frames of any size are
|
||||||
frame := make([]int16, r.frameSize*gumble.AudioChannels)
|
// 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 {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-r.stop:
|
case <-r.stop:
|
||||||
r.closeEncoder()
|
r.closeEncoder()
|
||||||
return
|
return
|
||||||
case item := <-r.input:
|
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:
|
case <-ticker.C:
|
||||||
clear(frame)
|
clear(chunk)
|
||||||
for source, queue := range queues {
|
for source, buffer := range queues {
|
||||||
if len(queue) == 0 {
|
if len(buffer) == 0 {
|
||||||
delete(queues, source)
|
delete(queues, source)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mix(frame, queue[0])
|
// Mix one chunk worth of samples from this source.
|
||||||
queue = queue[1:]
|
if len(buffer) <= chunkSize {
|
||||||
if len(queue) == 0 {
|
mix(chunk, buffer)
|
||||||
delete(queues, source)
|
delete(queues, source)
|
||||||
} else {
|
} 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.setError(err)
|
||||||
r.closeEncoder()
|
r.closeEncoder()
|
||||||
return
|
return
|
||||||
@@ -248,19 +252,19 @@ func (r *Recorder) setError(err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NormalizeStereoFrame(samples []int16, frameSize int) []int16 {
|
// NormalizeStereoFrame ensures samples are in stereo interleaved format.
|
||||||
out := make([]int16, frameSize*gumble.AudioChannels)
|
// If stereo is true the samples are returned as-is (already interleaved).
|
||||||
if len(samples) >= frameSize*gumble.AudioChannels && len(samples)%gumble.AudioChannels == 0 {
|
// Mono input is duplicated to both channels. The returned slice preserves
|
||||||
copy(out, samples[:frameSize*gumble.AudioChannels])
|
// all input audio without truncation.
|
||||||
return out
|
func NormalizeStereoFrame(samples []int16, stereo bool) []int16 {
|
||||||
|
if stereo {
|
||||||
|
return samples
|
||||||
}
|
}
|
||||||
limit := frameSize
|
// Convert mono to stereo by duplicating each sample.
|
||||||
if len(samples) < limit {
|
out := make([]int16, len(samples)*gumble.AudioChannels)
|
||||||
limit = len(samples)
|
for i, s := range samples {
|
||||||
}
|
out[i*2] = s
|
||||||
for i := 0; i < limit; i++ {
|
out[i*2+1] = s
|
||||||
out[i*2] = samples[i]
|
|
||||||
out[i*2+1] = samples[i]
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,16 +93,36 @@ func TestReservePathPreventsConcurrentRecordingCollisions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeStereoFrame(t *testing.T) {
|
func TestNormalizeStereoFrame(t *testing.T) {
|
||||||
mono := NormalizeStereoFrame([]int16{1, -2}, 3)
|
// Mono input duplicating each sample to both channels.
|
||||||
wantMono := []int16{1, 1, -2, -2, 0, 0}
|
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 {
|
for i := range wantMono {
|
||||||
if mono[i] != wantMono[i] {
|
if mono[i] != wantMono[i] {
|
||||||
t.Fatalf("mono[%d] = %d, want %d", i, 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)
|
// Even-length mono must not be mistaken for stereo.
|
||||||
wantStereo := []int16{1, 2, 3, 4}
|
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 {
|
for i := range wantStereo {
|
||||||
if stereo[i] != wantStereo[i] {
|
if stereo[i] != wantStereo[i] {
|
||||||
t.Fatalf("stereo[%d] = %d, want %d", i, stereo[i], wantStereo[i])
|
t.Fatalf("stereo[%d] = %d, want %d", i, stereo[i], wantStereo[i])
|
||||||
|
|||||||
Reference in New Issue
Block a user