🎉 live server seems to be working now
This commit is contained in:
14
third_party/sendspin-go/pkg/audio/encode/doc.go
vendored
Normal file
14
third_party/sendspin-go/pkg/audio/encode/doc.go
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// ABOUTME: Audio encoder package for encoding PCM to various formats
|
||||
// ABOUTME: Provides Encoder interface and implementations for PCM, Opus
|
||||
// Package encode provides audio encoders for various codecs.
|
||||
//
|
||||
// Supports: PCM (16-bit and 24-bit), Opus
|
||||
//
|
||||
// All encoders accept int32 samples in 24-bit range and encode
|
||||
// to wire format.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// encoder, err := encode.NewPCM(format)
|
||||
// data, err := encoder.Encode(samples)
|
||||
package encode
|
||||
9
third_party/sendspin-go/pkg/audio/encode/encoder.go
vendored
Normal file
9
third_party/sendspin-go/pkg/audio/encode/encoder.go
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
// ABOUTME: Encoder interface definition
|
||||
// ABOUTME: Common interface for all audio encoders
|
||||
package encode
|
||||
|
||||
// Encoder encodes PCM int32 samples to various formats
|
||||
type Encoder interface {
|
||||
Encode(samples []int32) ([]byte, error)
|
||||
Close() error
|
||||
}
|
||||
64
third_party/sendspin-go/pkg/audio/encode/opus.go
vendored
Normal file
64
third_party/sendspin-go/pkg/audio/encode/opus.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// ABOUTME: Opus audio encoder
|
||||
// ABOUTME: Encodes int32 samples to Opus bytes
|
||||
package encode
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/audio"
|
||||
"gopkg.in/hraban/opus.v2"
|
||||
)
|
||||
|
||||
type OpusEncoder struct {
|
||||
encoder *opus.Encoder
|
||||
sampleRate int
|
||||
channels int
|
||||
frameSize int
|
||||
pcmBuf []int16 // reusable conversion buffer
|
||||
outBuf []byte // reusable encode output buffer
|
||||
}
|
||||
|
||||
func NewOpus(format audio.Format) (Encoder, error) {
|
||||
if format.Codec != "opus" {
|
||||
return nil, fmt.Errorf("invalid codec for Opus encoder: %s", format.Codec)
|
||||
}
|
||||
|
||||
encoder, err := opus.NewEncoder(format.SampleRate, format.Channels, opus.AppAudio)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create opus encoder: %w", err)
|
||||
}
|
||||
|
||||
// Opus frame size depends on sample rate
|
||||
frameSize := format.SampleRate / 50 // 20ms frame
|
||||
|
||||
return &OpusEncoder{
|
||||
encoder: encoder,
|
||||
sampleRate: format.SampleRate,
|
||||
channels: format.Channels,
|
||||
frameSize: frameSize,
|
||||
pcmBuf: make([]int16, frameSize*format.Channels),
|
||||
outBuf: make([]byte, 4000),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *OpusEncoder) Encode(samples []int32) ([]byte, error) {
|
||||
if len(samples) > len(e.pcmBuf) {
|
||||
e.pcmBuf = make([]int16, len(samples))
|
||||
}
|
||||
|
||||
pcm := e.pcmBuf[:len(samples)]
|
||||
for i, sample := range samples {
|
||||
pcm[i] = audio.SampleToInt16(sample)
|
||||
}
|
||||
|
||||
n, err := e.encoder.Encode(pcm, e.outBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opus encode error: %w", err)
|
||||
}
|
||||
|
||||
return append([]byte(nil), e.outBuf[:n]...), nil
|
||||
}
|
||||
|
||||
func (e *OpusEncoder) Close() error {
|
||||
return nil
|
||||
}
|
||||
159
third_party/sendspin-go/pkg/audio/encode/opus_test.go
vendored
Normal file
159
third_party/sendspin-go/pkg/audio/encode/opus_test.go
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
// ABOUTME: Unit tests for Opus encoder
|
||||
// ABOUTME: Tests Opus encoding functionality
|
||||
package encode
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/audio"
|
||||
)
|
||||
|
||||
func TestNewOpus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format audio.Format
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid Opus 48kHz stereo",
|
||||
format: audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid Opus 48kHz mono",
|
||||
format: audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 1,
|
||||
BitDepth: 16,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid codec",
|
||||
format: audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid codec",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
encoder, err := NewOpus(tt.format)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("NewOpus() expected error, got nil")
|
||||
} else if tt.errContains != "" && !contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("NewOpus() error = %v, want error containing %v", err, tt.errContains)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("NewOpus() unexpected error = %v", err)
|
||||
}
|
||||
if encoder == nil {
|
||||
t.Errorf("NewOpus() returned nil encoder")
|
||||
}
|
||||
if encoder != nil {
|
||||
encoder.Close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpusEncoder_Encode(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
}
|
||||
|
||||
encoder, err := NewOpus(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOpus() failed: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
// 20ms frame at 48kHz = 960 samples per channel
|
||||
frameSize := 48000 / 50 // 20ms
|
||||
samples := make([]int32, frameSize*2) // stereo
|
||||
|
||||
for i := 0; i < len(samples); i++ {
|
||||
samples[i] = int32((i % 1000) * 8388) // Simple pattern
|
||||
}
|
||||
|
||||
output, err := encoder.Encode(samples)
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(output) == 0 {
|
||||
t.Errorf("Encode() returned empty output")
|
||||
}
|
||||
if len(output) > 4000 {
|
||||
t.Errorf("Encode() output size %d exceeds max Opus packet size 4000", len(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpusEncoder_EncodeSilence(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
}
|
||||
|
||||
encoder, err := NewOpus(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOpus() failed: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
// 20ms frame at 48kHz = 960 samples per channel
|
||||
frameSize := 48000 / 50 // 20ms
|
||||
samples := make([]int32, frameSize*2) // stereo, all zeros
|
||||
|
||||
output, err := encoder.Encode(samples)
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() failed: %v", err)
|
||||
}
|
||||
|
||||
// Even silence should produce valid Opus packets
|
||||
if len(output) == 0 {
|
||||
t.Errorf("Encode() returned empty output for silence")
|
||||
}
|
||||
if len(output) > 4000 {
|
||||
t.Errorf("Encode() output size %d exceeds max Opus packet size 4000", len(output))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpusEncoder_Close(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
}
|
||||
|
||||
encoder, err := NewOpus(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOpus() failed: %v", err)
|
||||
}
|
||||
|
||||
err = encoder.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
52
third_party/sendspin-go/pkg/audio/encode/pcm.go
vendored
Normal file
52
third_party/sendspin-go/pkg/audio/encode/pcm.go
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
// ABOUTME: PCM audio encoder
|
||||
// ABOUTME: Encodes int32 samples to 16-bit or 24-bit PCM bytes
|
||||
package encode
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/audio"
|
||||
)
|
||||
|
||||
type PCMEncoder struct {
|
||||
bitDepth int
|
||||
}
|
||||
|
||||
func NewPCM(format audio.Format) (Encoder, error) {
|
||||
if format.Codec != "pcm" {
|
||||
return nil, fmt.Errorf("invalid codec for PCM encoder: %s", format.Codec)
|
||||
}
|
||||
|
||||
if format.BitDepth != 16 && format.BitDepth != 24 {
|
||||
return nil, fmt.Errorf("unsupported bit depth: %d (supported: 16, 24)", format.BitDepth)
|
||||
}
|
||||
|
||||
return &PCMEncoder{
|
||||
bitDepth: format.BitDepth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *PCMEncoder) Encode(samples []int32) ([]byte, error) {
|
||||
if e.bitDepth == 24 {
|
||||
output := make([]byte, len(samples)*3)
|
||||
for i, sample := range samples {
|
||||
bytes := audio.SampleTo24Bit(sample)
|
||||
output[i*3] = bytes[0]
|
||||
output[i*3+1] = bytes[1]
|
||||
output[i*3+2] = bytes[2]
|
||||
}
|
||||
return output, nil
|
||||
} else {
|
||||
output := make([]byte, len(samples)*2)
|
||||
for i, sample := range samples {
|
||||
sample16 := audio.SampleToInt16(sample)
|
||||
binary.LittleEndian.PutUint16(output[i*2:], uint16(sample16))
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *PCMEncoder) Close() error {
|
||||
return nil
|
||||
}
|
||||
201
third_party/sendspin-go/pkg/audio/encode/pcm_test.go
vendored
Normal file
201
third_party/sendspin-go/pkg/audio/encode/pcm_test.go
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
// ABOUTME: Unit tests for PCM encoder
|
||||
// ABOUTME: Tests 16-bit and 24-bit PCM encoding
|
||||
package encode
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
|
||||
"github.com/Sendspin/sendspin-go/pkg/audio"
|
||||
)
|
||||
|
||||
func TestNewPCM(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
format audio.Format
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid 16-bit PCM",
|
||||
format: audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 24-bit PCM",
|
||||
format: audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 24,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid codec",
|
||||
format: audio.Format{
|
||||
Codec: "opus",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid codec",
|
||||
},
|
||||
{
|
||||
name: "unsupported bit depth",
|
||||
format: audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 32,
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "unsupported bit depth",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
encoder, err := NewPCM(tt.format)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("NewPCM() expected error, got nil")
|
||||
} else if tt.errContains != "" && !contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("NewPCM() error = %v, want error containing %v", err, tt.errContains)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("NewPCM() unexpected error = %v", err)
|
||||
}
|
||||
if encoder == nil {
|
||||
t.Errorf("NewPCM() returned nil encoder")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMEncoder_Encode16Bit(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
}
|
||||
|
||||
encoder, err := NewPCM(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPCM() failed: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
samples := []int32{
|
||||
0, // silence
|
||||
0x7FFF00, // max positive 16-bit (left-justified in 24-bit)
|
||||
-0x800000, // max negative 16-bit (left-justified in 24-bit)
|
||||
0x123400, // arbitrary positive value
|
||||
-0x567800, // arbitrary negative value
|
||||
}
|
||||
|
||||
output, err := encoder.Encode(samples)
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() failed: %v", err)
|
||||
}
|
||||
|
||||
expectedSize := len(samples) * 2
|
||||
if len(output) != expectedSize {
|
||||
t.Errorf("Encode() output size = %d, want %d", len(output), expectedSize)
|
||||
}
|
||||
|
||||
for i, sample := range samples {
|
||||
expected := audio.SampleToInt16(sample)
|
||||
actual := int16(binary.LittleEndian.Uint16(output[i*2:]))
|
||||
if actual != expected {
|
||||
t.Errorf("Sample %d: got %d, want %d", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMEncoder_Encode24Bit(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 24,
|
||||
}
|
||||
|
||||
encoder, err := NewPCM(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPCM() failed: %v", err)
|
||||
}
|
||||
defer encoder.Close()
|
||||
|
||||
samples := []int32{
|
||||
0, // silence
|
||||
0x7FFFFF, // max positive 24-bit
|
||||
-0x800000, // max negative 24-bit
|
||||
0x123456, // arbitrary positive value
|
||||
-0x567890, // arbitrary negative value
|
||||
}
|
||||
|
||||
output, err := encoder.Encode(samples)
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() failed: %v", err)
|
||||
}
|
||||
|
||||
expectedSize := len(samples) * 3
|
||||
if len(output) != expectedSize {
|
||||
t.Errorf("Encode() output size = %d, want %d", len(output), expectedSize)
|
||||
}
|
||||
|
||||
for i, sample := range samples {
|
||||
expected := audio.SampleTo24Bit(sample)
|
||||
actual := [3]byte{
|
||||
output[i*3],
|
||||
output[i*3+1],
|
||||
output[i*3+2],
|
||||
}
|
||||
if actual != expected {
|
||||
t.Errorf("Sample %d: got %v, want %v", i, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCMEncoder_Close(t *testing.T) {
|
||||
format := audio.Format{
|
||||
Codec: "pcm",
|
||||
SampleRate: 48000,
|
||||
Channels: 2,
|
||||
BitDepth: 16,
|
||||
}
|
||||
|
||||
encoder, err := NewPCM(format)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPCM() failed: %v", err)
|
||||
}
|
||||
|
||||
err = encoder.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Close() unexpected error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && indexOf(s, substr) >= 0))
|
||||
}
|
||||
|
||||
func indexOf(s, substr string) int {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
Reference in New Issue
Block a user