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
143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
package recording
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type trackingWriteCloser struct{ closed bool }
|
|
|
|
func (w *trackingWriteCloser) Write([]byte) (int, error) { return 0, nil }
|
|
func (w *trackingWriteCloser) Close() error { w.closed = true; return nil }
|
|
|
|
var _ io.WriteCloser = (*trackingWriteCloser)(nil)
|
|
|
|
// Regression: Stop closed ffmpeg stdin while the worker could still write,
|
|
// creating a spurious closed-pipe recording failure.
|
|
func TestStopLeavesEncoderClosureToWorker(t *testing.T) {
|
|
stdin := &trackingWriteCloser{}
|
|
done := make(chan struct{})
|
|
close(done)
|
|
r := &Recorder{stdin: stdin, stop: make(chan struct{}), done: done}
|
|
if err := r.Stop(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stdin.closed {
|
|
t.Fatal("Stop closed stdin instead of the worker")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeFormat(t *testing.T) {
|
|
tests := map[string]string{
|
|
"": "flac",
|
|
" FLAC ": "flac",
|
|
".opus": "opus",
|
|
}
|
|
for input, want := range tests {
|
|
if got := NormalizeFormat(input); got != want {
|
|
t.Fatalf("NormalizeFormat(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUniquePathAvoidsCollision(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now := time.Date(2026, 5, 14, 12, 30, 0, 0, time.Local)
|
|
first := filepath.Join(dir, "barnard-recording-20260514-123000.flac")
|
|
if err := os.WriteFile(first, []byte{}, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := UniquePath(dir, now, "flac")
|
|
want := filepath.Join(dir, "barnard-recording-20260514-123000-2.flac")
|
|
if got != want {
|
|
t.Fatalf("UniquePath() = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestReservePathPreventsConcurrentRecordingCollisions(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now := time.Date(2026, 5, 14, 12, 30, 0, 0, time.Local)
|
|
paths := make(chan string, 2)
|
|
errs := make(chan error, 2)
|
|
var wg sync.WaitGroup
|
|
for range 2 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
path, err := reservePath(dir, now, "flac")
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
paths <- path
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(paths)
|
|
close(errs)
|
|
for err := range errs {
|
|
t.Fatal(err)
|
|
}
|
|
var reserved []string
|
|
for path := range paths {
|
|
reserved = append(reserved, path)
|
|
}
|
|
if len(reserved) != 2 || reserved[0] == reserved[1] {
|
|
t.Fatalf("reserved paths = %#v", reserved)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeStereoFrame(t *testing.T) {
|
|
// 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])
|
|
}
|
|
}
|
|
|
|
// 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])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixSaturates(t *testing.T) {
|
|
dst := []int16{32000, -32000, 10}
|
|
mix(dst, []int16{2000, -2000, -20})
|
|
want := []int16{32767, -32768, -10}
|
|
for i := range want {
|
|
if dst[i] != want[i] {
|
|
t.Fatalf("dst[%d] = %d, want %d", i, dst[i], want[i])
|
|
}
|
|
}
|
|
}
|