334 lines
8.7 KiB
Go
334 lines
8.7 KiB
Go
package diskwriter
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/skald/conn"
|
|
"git.stormux.org/storm/skald/hall"
|
|
)
|
|
|
|
func TestSanitise(t *testing.T) {
|
|
tests := []struct{ a, b string }{
|
|
{"Alas", "Alas"},
|
|
{", poor Horatio", ", poor Horatio"},
|
|
{"I/knew\\him/well", "I-slash-knew-backslash-him-slash-well"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
c := sanitise(tt.a)
|
|
if c != tt.b {
|
|
t.Errorf("sanitise(%v): got %v, expected %v",
|
|
tt.a, c, tt.b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenDiskFileUsesSkaldOggName(t *testing.T) {
|
|
dir := t.TempDir()
|
|
f, err := openDiskFile(dir, "ogg")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
name := filepath.Base(f.Name())
|
|
f.Close()
|
|
|
|
if !strings.HasPrefix(name, "skald-recording-") {
|
|
t.Fatalf("filename %q does not use Skald recording prefix", name)
|
|
}
|
|
if !strings.HasSuffix(name, ".ogg") {
|
|
t.Fatalf("filename %q does not use Ogg extension", name)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, name)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupOldRecordings(t *testing.T) {
|
|
oldDirectory := Directory
|
|
defer func() {
|
|
Directory = oldDirectory
|
|
}()
|
|
|
|
Directory = t.TempDir()
|
|
hallDir := filepath.Join(Directory, "test")
|
|
if err := os.MkdirAll(hallDir, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldRecording := filepath.Join(hallDir, "skald-recording-20000101-000000.ogg")
|
|
activeRecording := filepath.Join(hallDir, "skald-recording-20000101-010000.ogg")
|
|
newRecording := filepath.Join(hallDir, "skald-recording-29990101-000000.ogg")
|
|
otherFile := filepath.Join(hallDir, "notes.txt")
|
|
for _, name := range []string{oldRecording, activeRecording, newRecording, otherFile} {
|
|
if err := os.WriteFile(name, []byte("test"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
oldTime := time.Now().Add(-48 * time.Hour)
|
|
newTime := time.Now()
|
|
if err := os.Chtimes(oldRecording, oldTime, oldTime); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(activeRecording, oldTime, oldTime); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(newRecording, newTime, newTime); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chtimes(otherFile, oldTime, oldTime); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
activeRecordings.Store(activeRecording, struct{}{})
|
|
defer activeRecordings.Delete(activeRecording)
|
|
|
|
if err := CleanupOldRecordings(24 * time.Hour); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(oldRecording); !os.IsNotExist(err) {
|
|
t.Fatalf("old recording still exists or stat failed unexpectedly: %v", err)
|
|
}
|
|
if _, err := os.Stat(newRecording); err != nil {
|
|
t.Fatalf("new recording was removed: %v", err)
|
|
}
|
|
if _, err := os.Stat(activeRecording); err != nil {
|
|
t.Fatalf("active recording was removed: %v", err)
|
|
}
|
|
if _, err := os.Stat(otherFile); err != nil {
|
|
t.Fatalf("non-recording file was removed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMixFrameAppliesHeadroomForMultipleSources(t *testing.T) {
|
|
a := make(chan []byte, 1)
|
|
b := make(chan []byte, 1)
|
|
a <- pcmFrame(20000)
|
|
b <- pcmFrame(20000)
|
|
|
|
out := mixFrame([]chan []byte{a, b})
|
|
if len(out) != bytesPerFrame {
|
|
t.Fatalf("mixed frame has length %d, expected %d", len(out), bytesPerFrame)
|
|
}
|
|
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got != 28071 {
|
|
t.Fatalf("sample %d = %d, expected headroom-adjusted 28071", i/2, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixFrameLimiterPreservesNormalSpeechLevel(t *testing.T) {
|
|
a := make(chan []byte, 1)
|
|
a <- pcmFrame(12000)
|
|
|
|
out := mixFrame([]chan []byte{a})
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got != 12000 {
|
|
t.Fatalf("sample %d = %d, expected unchanged 12000", i/2, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixFrameHandlesQuietAndLoudSources(t *testing.T) {
|
|
a := make(chan []byte, 1)
|
|
b := make(chan []byte, 1)
|
|
a <- pcmFrame(1000)
|
|
b <- pcmFrame(20000)
|
|
|
|
out := mixFrame([]chan []byte{a, b})
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got != 14849 {
|
|
t.Fatalf("sample %d = %d, expected mixed quiet/loud sample 14849", i/2, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixFrameDoesNotAttenuateSpeakerForSilentOpenMics(t *testing.T) {
|
|
speaker := make(chan []byte, 1)
|
|
silent1 := make(chan []byte, 1)
|
|
silent2 := make(chan []byte, 1)
|
|
speaker <- pcmFrame(12000)
|
|
silent1 <- pcmFrame(0)
|
|
silent2 <- pcmFrame(0)
|
|
|
|
out := mixFrame([]chan []byte{speaker, silent1, silent2})
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got != 12000 {
|
|
t.Fatalf("sample %d = %d, expected unattenuated speaker 12000", i/2, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixFrameLimiterBoundsOverloadedSamples(t *testing.T) {
|
|
a := make(chan []byte, 1)
|
|
b := make(chan []byte, 1)
|
|
a <- pcmFrame(32000)
|
|
b <- pcmFrame(32000)
|
|
|
|
out := mixFrame([]chan []byte{a, b})
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got <= 0 || got > 32767 {
|
|
t.Fatalf("sample %d = %d, expected bounded positive sample", i/2, got)
|
|
}
|
|
if got >= 32767 {
|
|
t.Fatalf("sample %d = %d, expected limiter below hard clip", i/2, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMixFrameConsumesOldestQueuedFrame(t *testing.T) {
|
|
source := make(chan []byte, 2)
|
|
source <- pcmFrame(1000)
|
|
source <- pcmFrame(2000)
|
|
|
|
out := mixFrame([]chan []byte{source})
|
|
if len(out) != bytesPerFrame {
|
|
t.Fatalf("mixed frame has length %d, expected %d", len(out), bytesPerFrame)
|
|
}
|
|
|
|
for i := 0; i < len(out); i += 2 {
|
|
got := int16(binary.LittleEndian.Uint16(out[i:]))
|
|
if got != 1000 {
|
|
t.Fatalf("sample %d = %d, expected oldest queued sample 1000", i/2, got)
|
|
}
|
|
}
|
|
if queued := len(source); queued != 1 {
|
|
t.Fatalf("queued frames after mix = %d, expected 1", queued)
|
|
}
|
|
}
|
|
|
|
func TestMixFrameReturnsNilForSilence(t *testing.T) {
|
|
if out := mixFrame(nil); out != nil {
|
|
t.Fatalf("expected nil silent frame, got %d bytes", len(out))
|
|
}
|
|
}
|
|
|
|
func TestNewDiskConnIgnoresConnectionWithoutRecordableTracks(t *testing.T) {
|
|
down, err := newDiskConn(&Client{}, &fakeUp{id: "up1"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if down != nil {
|
|
t.Fatalf("expected no disk connection for no tracks, got %#v", down)
|
|
}
|
|
}
|
|
|
|
func TestPushConnDoesNotStoreConnectionWithoutRecordableTracks(t *testing.T) {
|
|
g := &hall.Hall{}
|
|
client := &Client{
|
|
hall: g,
|
|
recorder: &recorder{},
|
|
down: make(map[string]*diskConn),
|
|
}
|
|
err := client.PushConn(g, "up1", &fakeUp{id: "up1"}, nil, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(client.down) != 0 {
|
|
t.Fatalf("stored %d down connections, expected 0", len(client.down))
|
|
}
|
|
}
|
|
|
|
func TestRecorderCreatesPlayableOgg(t *testing.T) {
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
t.Skip("ffmpeg not installed")
|
|
}
|
|
if _, err := exec.LookPath("ffprobe"); err != nil {
|
|
t.Skip("ffprobe not installed")
|
|
}
|
|
|
|
r, err := newRecorder(nil, t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
if err := r.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cmd := exec.Command(
|
|
"ffprobe",
|
|
"-v", "error",
|
|
"-select_streams", "a:0",
|
|
"-show_entries", "stream=codec_name",
|
|
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
r.path,
|
|
)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.TrimSpace(string(out)) != "opus" {
|
|
t.Fatalf("expected opus stream, got %q", strings.TrimSpace(string(out)))
|
|
}
|
|
}
|
|
|
|
func TestSyntheticRecordingCreatesPlayableOgg(t *testing.T) {
|
|
if _, err := exec.LookPath("ffmpeg"); err != nil {
|
|
t.Skip("ffmpeg not installed")
|
|
}
|
|
if _, err := exec.LookPath("ffprobe"); err != nil {
|
|
t.Skip("ffprobe not installed")
|
|
}
|
|
|
|
result, err := RunSyntheticRecording(t.TempDir(), 3, 100*time.Millisecond)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Metrics.DecoderStarts != 3 {
|
|
t.Fatalf("decoder starts = %d, expected 3", result.Metrics.DecoderStarts)
|
|
}
|
|
if result.Metrics.DecoderStops != 3 {
|
|
t.Fatalf("decoder stops = %d, expected 3", result.Metrics.DecoderStops)
|
|
}
|
|
if result.Metrics.DroppedMixedFrames != 0 {
|
|
t.Fatalf("dropped mixed frames = %d, expected 0", result.Metrics.DroppedMixedFrames)
|
|
}
|
|
|
|
cmd := exec.Command(
|
|
"ffprobe",
|
|
"-v", "error",
|
|
"-select_streams", "a:0",
|
|
"-show_entries", "stream=codec_name",
|
|
"-of", "default=noprint_wrappers=1:nokey=1",
|
|
result.Path,
|
|
)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.TrimSpace(string(out)) != "opus" {
|
|
t.Fatalf("expected opus stream, got %q", strings.TrimSpace(string(out)))
|
|
}
|
|
}
|
|
|
|
func pcmFrame(sample int16) []byte {
|
|
out := make([]byte, bytesPerFrame)
|
|
for i := 0; i < len(out); i += 2 {
|
|
binary.LittleEndian.PutUint16(out[i:], uint16(sample))
|
|
}
|
|
return out
|
|
}
|
|
|
|
type fakeUp struct {
|
|
id string
|
|
}
|
|
|
|
func (u *fakeUp) AddLocal(conn.Down) error { return nil }
|
|
func (u *fakeUp) DelLocal(conn.Down) bool { return true }
|
|
func (u *fakeUp) Id() string { return u.id }
|
|
func (u *fakeUp) Label() string { return "audio" }
|
|
func (u *fakeUp) User() (string, string) { return "user-id", "Username" }
|