108 lines
2.7 KiB
Go
108 lines
2.7 KiB
Go
//go:build overlord_webrtc
|
|||
|
|
|
||
|
|
package webrtcpub
|
||
|
|
|
||
|
|
import (
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/pion/webrtc/v4"
|
||
|
|
"github.com/pion/webrtc/v4/pkg/media"
|
||
|
|
)
|
||
|
|
|
||
|
|
// G.711 μ-law parameters: 8 kHz, mono, 8-bit, 64 kbps. Universally supported
|
||
|
|
// in browsers without needing libopus / a separate Opus encoder dependency.
|
||
|
|
//
|
||
|
|
// We accept 16 kHz int16 PCM (the format malgo captures) and emit μ-law in
|
||
|
|
// 20 ms RTP packets — 160 samples per packet at 8 kHz.
|
||
|
|
|
||
|
|
const (
|
||
|
|
pcmuSourceRate = 16000
|
||
|
|
pcmuTargetRate = 8000
|
||
|
|
pcmuPacketMs = 20
|
||
|
|
pcmuSamplesPerPacket = pcmuTargetRate * pcmuPacketMs / 1000 // 160
|
||
|
|
)
|
||
|
|
|
||
|
|
// pcmuAudioWriter buffers incoming 16 kHz PCM, downsamples to 8 kHz, μ-law
|
||
|
|
// encodes, and emits 20 ms RTP samples to the wrapped track. Thread-safe.
|
||
|
|
type pcmuAudioWriter struct {
|
||
|
|
t *webrtc.TrackLocalStaticSample
|
||
|
|
mu sync.Mutex
|
||
|
|
// buf8 holds 8 kHz mono int16 samples awaiting packetization.
|
||
|
|
buf8 []int16
|
||
|
|
// carry preserves the second sample of a 16 kHz pair across calls so
|
||
|
|
// the decimation phase stays correct when callers hand us odd-length
|
||
|
|
// buffers.
|
||
|
|
carryValid bool
|
||
|
|
carry int16
|
||
|
|
}
|
||
|
|
|
||
|
|
func newPCMUAudioWriter(t *webrtc.TrackLocalStaticSample) *pcmuAudioWriter {
|
||
|
|
return &pcmuAudioWriter{t: t, buf8: make([]int16, 0, pcmuSamplesPerPacket*4)}
|
||
|
|
}
|
||
|
|
|
||
|
|
// WriteAudio is called with a 16 kHz mono int16 frame. Internally it
|
||
|
|
// downsamples by 2 (pick every other sample — adequate for screen-share
|
||
|
|
// audio; if you really need anti-aliasing, run a 1-pole IIR first) and
|
||
|
|
// μ-law-encodes 20 ms slices.
|
||
|
|
func (w *pcmuAudioWriter) WriteAudio(pcm16 []int16) error {
|
||
|
|
if len(pcm16) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
w.mu.Lock()
|
||
|
|
defer w.mu.Unlock()
|
||
|
|
|
||
|
|
start := 0
|
||
|
|
if w.carryValid {
|
||
|
|
w.carryValid = false
|
||
|
|
start = 1
|
||
|
|
_ = w.carry // value discarded — we keep one of each pair
|
||
|
|
}
|
||
|
|
for i := start; i < len(pcm16); i += 2 {
|
||
|
|
w.buf8 = append(w.buf8, pcm16[i])
|
||
|
|
}
|
||
|
|
if (len(pcm16)-start)%2 == 1 {
|
||
|
|
w.carryValid = true
|
||
|
|
w.carry = pcm16[len(pcm16)-1]
|
||
|
|
}
|
||
|
|
|
||
|
|
for len(w.buf8) >= pcmuSamplesPerPacket {
|
||
|
|
slice := w.buf8[:pcmuSamplesPerPacket]
|
||
|
|
encoded := make([]byte, pcmuSamplesPerPacket)
|
||
|
|
for i, s := range slice {
|
||
|
|
encoded[i] = linearToMulaw(s)
|
||
|
|
}
|
||
|
|
if err := w.t.WriteSample(media.Sample{
|
||
|
|
Data: encoded,
|
||
|
|
Duration: pcmuPacketMs * time.Millisecond,
|
||
|
|
}); err != nil {
|
||
|
|
w.buf8 = w.buf8[:0]
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
w.buf8 = w.buf8[pcmuSamplesPerPacket:]
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func linearToMulaw(sample int16) byte {
|
||
|
|
const (
|
||
|
|
cBias = 0x84
|
||
|
|
cClip = 32635
|
||
|
|
)
|
||
|
|
sign := int16(0)
|
||
|
|
if sample < 0 {
|
||
|
|
sample = -sample
|
||
|
|
sign = 0x80
|
||
|
|
}
|
||
|
|
if sample > cClip {
|
||
|
|
sample = cClip
|
||
|
|
}
|
||
|
|
sample += cBias
|
||
|
|
exponent := int16(7)
|
||
|
|
for mask := int16(0x4000); sample&mask == 0 && exponent > 0; mask >>= 1 {
|
||
|
|
exponent--
|
||
|
|
}
|
||
|
|
mantissa := (sample >> (exponent + 3)) & 0x0F
|
||
|
|
return ^byte(sign | (exponent << 4) | mantissa)
|
||
|
|
}
|