diff --git a/go.mod b/go.mod index 7ca1f332..df2ddd3b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.8.4 github.com/urfave/cli/v2 v2.25.7 - github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index d1a0c16e..f4e8ada3 100644 --- a/go.sum +++ b/go.sum @@ -217,8 +217,6 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRT github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM= -github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= diff --git a/pkg/media/media.go b/pkg/media/media.go index 6063cbf9..2bcb29dc 100644 --- a/pkg/media/media.go +++ b/pkg/media/media.go @@ -14,6 +14,8 @@ package media +import "sync/atomic" + type Writer[T any] interface { WriteSample(sample T) error } @@ -23,3 +25,43 @@ type WriterFunc[T any] func(in T) error func (fnc WriterFunc[T]) WriteSample(in T) error { return fnc(in) } + +type SwitchWriter[T any] struct { + ptr atomic.Pointer[Writer[T]] +} + +func (s *SwitchWriter[T]) Get() Writer[T] { + ptr := s.ptr.Load() + if ptr == nil { + return nil + } + return *ptr +} + +func (s *SwitchWriter[T]) Set(w Writer[T]) { + if w == nil { + s.ptr.Store(nil) + } else { + s.ptr.Store(&w) + } +} + +func (s *SwitchWriter[T]) WriteSample(sample T) error { + w := s.Get() + if w == nil { + return nil + } + return w.WriteSample(sample) +} + +type MultiWriter[T any] []Writer[T] + +func (s MultiWriter[T]) WriteSample(sample T) error { + var last error + for _, w := range s { + if err := w.WriteSample(sample); err != nil { + last = err + } + } + return last +} diff --git a/pkg/media/pcm.go b/pkg/media/pcm.go index cf46585d..ae2c5009 100644 --- a/pkg/media/pcm.go +++ b/pkg/media/pcm.go @@ -15,7 +15,6 @@ package media import ( - "encoding/binary" "time" "github.com/pion/webrtc/v3/pkg/media" @@ -37,38 +36,8 @@ func PlayAudio[T any](w Writer[T], sampleDur time.Duration, frames []T) error { return nil } -type LPCM16Sample []byte - -func (s LPCM16Sample) Decode() PCM16Sample { - out := make(PCM16Sample, len(s)/2) - for i := 0; i < len(s); i += 2 { - out[i/2] = int16(binary.LittleEndian.Uint16(s[i:])) - } - return out -} - type PCM16Sample []int16 -func (s PCM16Sample) Encode() LPCM16Sample { - out := make(LPCM16Sample, len(s)*2) - for i, v := range s { - binary.LittleEndian.PutUint16(out[2*i:], uint16(v)) - } - return out -} - -func DecodePCM(w Writer[PCM16Sample]) Writer[LPCM16Sample] { - return WriterFunc[LPCM16Sample](func(in LPCM16Sample) error { - return w.WriteSample(in.Decode()) - }) -} - -func EncodePCM(w Writer[LPCM16Sample]) Writer[PCM16Sample] { - return WriterFunc[PCM16Sample](func(in PCM16Sample) error { - return w.WriteSample(in.Encode()) - }) -} - type MediaSampleWriter interface { WriteSample(sample media.Sample) error } diff --git a/pkg/media/ulaw/g711.go b/pkg/media/ulaw/g711.go new file mode 100644 index 00000000..f39ee470 --- /dev/null +++ b/pkg/media/ulaw/g711.go @@ -0,0 +1,155 @@ +/* + Copyright (C) 2016 - 2017, Lefteris Zafiris + + This program is free software, distributed under the terms of + the BSD 3-Clause License. See the LICENSE file + at the top of the source tree. + + Package g711 implements encoding and decoding of G711 PCM sound data. + G.711 is an ITU-T standard for audio companding. +*/ + +package ulaw + +const ( + uLawBias = 0x84 + uLawClip = 0x7F7B +) + +var ( + // u-law quantization segment lookup table + ulawSegment = [256]uint8{ + 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + } + // u-law to LPCM conversion lookup table + ulaw2lpcm = [256]int16{ + -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956, + -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764, + -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412, + -11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316, + -7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140, + -5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092, + -3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004, + -2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980, + -1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436, + -1372, -1308, -1244, -1180, -1116, -1052, -988, -924, + -876, -844, -812, -780, -748, -716, -684, -652, + -620, -588, -556, -524, -492, -460, -428, -396, + -372, -356, -340, -324, -308, -292, -276, -260, + -244, -228, -212, -196, -180, -164, -148, -132, + -120, -112, -104, -96, -88, -80, -72, -64, + -56, -48, -40, -32, -24, -16, -8, 0, + 32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956, + 23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764, + 15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412, + 11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316, + 7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140, + 5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092, + 3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004, + 2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980, + 1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436, + 1372, 1308, 1244, 1180, 1116, 1052, 988, 924, + 876, 844, 812, 780, 748, 716, 684, 652, + 620, 588, 556, 524, 492, 460, 428, 396, + 372, 356, 340, 324, 308, 292, 276, 260, + 244, 228, 212, 196, 180, 164, 148, 132, + 120, 112, 104, 96, 88, 80, 72, 64, + 56, 48, 40, 32, 24, 16, 8, 0, + } + // u-law to A-law conversion lookup table based on the ITU-T G.711 specification + ulaw2alaw = [256]uint8{ + 42, 43, 40, 41, 46, 47, 44, 45, 34, 35, 32, 33, 38, 39, 36, 37, + 58, 59, 56, 57, 62, 63, 60, 61, 50, 51, 48, 49, 54, 55, 52, 53, + 10, 11, 8, 9, 14, 15, 12, 13, 2, 3, 0, 1, 6, 7, 4, 26, + 27, 24, 25, 30, 31, 28, 29, 18, 19, 16, 17, 22, 23, 20, 21, 106, + 104, 105, 110, 111, 108, 109, 98, 99, 96, 97, 102, 103, 100, 101, 122, 120, + 126, 127, 124, 125, 114, 115, 112, 113, 118, 119, 116, 117, 75, 73, 79, 77, + 66, 67, 64, 65, 70, 71, 68, 69, 90, 91, 88, 89, 94, 95, 92, 93, + 82, 82, 83, 83, 80, 80, 81, 81, 86, 86, 87, 87, 84, 84, 85, 85, + 170, 171, 168, 169, 174, 175, 172, 173, 162, 163, 160, 161, 166, 167, 164, 165, + 186, 187, 184, 185, 190, 191, 188, 189, 178, 179, 176, 177, 182, 183, 180, 181, + 138, 139, 136, 137, 142, 143, 140, 141, 130, 131, 128, 129, 134, 135, 132, 154, + 155, 152, 153, 158, 159, 156, 157, 146, 147, 144, 145, 150, 151, 148, 149, 234, + 232, 233, 238, 239, 236, 237, 226, 227, 224, 225, 230, 231, 228, 229, 250, 248, + 254, 255, 252, 253, 242, 243, 240, 241, 246, 247, 244, 245, 203, 201, 207, 205, + 194, 195, 192, 193, 198, 199, 196, 197, 218, 219, 216, 217, 222, 223, 220, 221, + 210, 210, 211, 211, 208, 208, 209, 209, 214, 214, 215, 215, 212, 212, 213, 213, + } +) + +// EncodeUlaw encodes 16bit LPCM data to G711 u-law PCM +func EncodeUlaw(lpcm []int16) []byte { + out := make([]byte, len(lpcm)) + for i := range lpcm { + out[i] = EncodeUlawFrame(lpcm[i]) + } + return out +} + +// EncodeUlawFrame encodes a 16bit LPCM frame to G711 u-law PCM +func EncodeUlawFrame(frame int16) uint8 { + /* + The algorithm first stores off the sign. It then adds in a bias value + which (due to wrapping) will cause high valued samples to lose precision. + The top five most significant bits are pulled out of the sample. + Then, the bottom three bits of the compressed byte are generated using the + segment look-up table, based on the biased value of the source sample. + The 8-bit compressed sample is then finally created by logically OR'ing together + the 5 most important bits, the 3 lower bits, and the sign when applicable. The bits + are then logically NOT'ed for transmission. + */ + sign := (frame >> 8) & 0x80 + if sign != 0 { + frame = -frame + } + if frame > uLawClip { + frame = uLawClip + } + frame += uLawBias + segment := ulawSegment[(frame>>7)&0xFF] + bottom := (frame >> (segment + 3)) & 0x0F + return uint8(^(sign | (int16(segment) << 4) | bottom)) +} + +// DecodeUlaw decodes u-law PCM data to 16bit PCM. +func DecodeUlaw(pcm []byte) []int16 { + out := make([]int16, len(pcm)) + for i := 0; i < len(pcm); i++ { + out[i] = ulaw2lpcm[pcm[i]] + } + return out +} + +// DecodeUlawFrame decodes a u-law PCM frame to 16bit LPCM +func DecodeUlawFrame(frame uint8) int16 { + return ulaw2lpcm[frame] +} + +// Ulaw2Alaw performs direct u-law to A-law data conversion +func Ulaw2Alaw(ulaw []byte) []byte { + alaw := make([]byte, len(ulaw)) + for i := 0; i < len(alaw); i++ { + alaw[i] = ulaw2alaw[ulaw[i]] + } + return ulaw +} + +// Ulaw2AlawFrame directly converts a u-law frame to A-law +func Ulaw2AlawFrame(frame uint8) uint8 { + return ulaw2alaw[frame] +} diff --git a/pkg/media/ulaw/ulaw.go b/pkg/media/ulaw/ulaw.go index 787150ae..a35e6d80 100644 --- a/pkg/media/ulaw/ulaw.go +++ b/pkg/media/ulaw/ulaw.go @@ -15,30 +15,28 @@ package ulaw import ( - "github.com/zaf/g711" - "github.com/livekit/sip/pkg/media" ) type Sample []byte -func (s Sample) Decode() media.LPCM16Sample { - return g711.DecodeUlaw(s) +func (s Sample) Decode() media.PCM16Sample { + return DecodeUlaw(s) } -func (s *Sample) Encode(data media.LPCM16Sample) { - *s = g711.EncodeUlaw(data) +func (s *Sample) Encode(data media.PCM16Sample) { + *s = EncodeUlaw(data) } -func Encode(w media.Writer[media.LPCM16Sample]) media.Writer[Sample] { +func Decode(w media.Writer[media.PCM16Sample]) media.Writer[Sample] { return media.WriterFunc[Sample](func(in Sample) error { out := in.Decode() return w.WriteSample(out) }) } -func Decode(w media.Writer[Sample]) media.Writer[media.LPCM16Sample] { - return media.WriterFunc[media.LPCM16Sample](func(in media.LPCM16Sample) error { +func Encode(w media.Writer[Sample]) media.Writer[media.PCM16Sample] { + return media.WriterFunc[media.PCM16Sample](func(in media.PCM16Sample) error { var s Sample s.Encode(in) return w.WriteSample(s) diff --git a/pkg/mixer/mixer.go b/pkg/mixer/mixer.go index e158422f..fbbbde0b 100644 --- a/pkg/mixer/mixer.go +++ b/pkg/mixer/mixer.go @@ -43,8 +43,8 @@ type Input struct { type Mixer struct { mu sync.Mutex - onSample func([]byte) - inputs map[*Input]struct{} + out media.Writer[media.PCM16Sample] + inputs map[*Input]struct{} ticker *time.Ticker mixSize int @@ -52,21 +52,21 @@ type Mixer struct { stopped core.Fuse } -func NewMixer(onSample func([]byte), sampleRate int) *Mixer { - m := createMixer(onSample, sampleRate) +func NewMixer(out media.Writer[media.PCM16Sample], sampleRate int) *Mixer { + m := createMixer(out, sampleRate) go m.start() return m } -func createMixer(onSample func([]byte), sampleRate int) *Mixer { +func createMixer(out media.Writer[media.PCM16Sample], sampleRate int) *Mixer { m := &Mixer{ - onSample: onSample, - ticker: time.NewTicker(mixerTickDuration), - mixSize: int(time.Duration(sampleRate) * mixerTickDuration / time.Second), - stopped: core.NewFuse(), - inputs: make(map[*Input]struct{}), + out: out, + ticker: time.NewTicker(mixerTickDuration), + mixSize: int(time.Duration(sampleRate) * mixerTickDuration / time.Second), + stopped: core.NewFuse(), + inputs: make(map[*Input]struct{}), } return m @@ -107,7 +107,7 @@ func (m *Mixer) doMix() { input.mu.Unlock() } - out := make([]byte, 2*m.mixSize) + out := make(media.PCM16Sample, m.mixSize) for i, sample := range mixed { if sample > 0x7FFF { sample = 0x7FFF @@ -115,13 +115,10 @@ func (m *Mixer) doMix() { if sample < -0x7FFF { sample = -0x7FFF } - - // Encoder expects little endian data (???) - out[2*i] = byte(sample & 0xFF) - out[2*i+1] = byte(sample >> 8) + out[i] = int16(sample) } - m.onSample(out) + _ = m.out.WriteSample(out) } func (m *Mixer) start() { diff --git a/pkg/mixer/mixer_test.go b/pkg/mixer/mixer_test.go index ac40bafb..e8a30800 100644 --- a/pkg/mixer/mixer_test.go +++ b/pkg/mixer/mixer_test.go @@ -15,17 +15,19 @@ package mixer import ( - "bytes" "testing" "github.com/stretchr/testify/require" + + "github.com/livekit/sip/pkg/media" ) func TestMixer(t *testing.T) { - sample := []byte{} - m := createMixer(func(s []byte) { + var sample media.PCM16Sample + m := createMixer(media.WriterFunc[media.PCM16Sample](func(s media.PCM16Sample) error { sample = s - }, 8000) + return nil + }), 8000) require.Equal(t, 160, m.mixSize) @@ -33,9 +35,7 @@ func TestMixer(t *testing.T) { t.Run("No Input", func(t *testing.T) { m.doMix() - if !bytes.Equal(sample, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) { - t.Fatal() - } + require.Equal(t, media.PCM16Sample{0, 0, 0, 0, 0}, sample) }) t.Run("One Input", func(t *testing.T) { @@ -44,7 +44,7 @@ func TestMixer(t *testing.T) { input.Push([]int16{0xA, 0xB, 0xC, 0xD, 0xE}) m.doMix() - require.Equal(t, []byte{10, 0, 11, 0, 12, 0, 13, 0, 14, 0}, sample) + require.Equal(t, media.PCM16Sample{10, 11, 12, 13, 14}, sample) m.RemoveInput(input) }) @@ -58,13 +58,13 @@ func TestMixer(t *testing.T) { secondInput.Push([]int16{0xA, 0xB, 0xC, 0xD, 0xE}) m.doMix() - require.Equal(t, []byte{24, 0, 24, 0, 24, 0, 24, 0, 24, 0}, sample) + require.Equal(t, media.PCM16Sample{24, 24, 24, 24, 24}, sample) firstInput.Push([]int16{0x7FFF, 0x1, -0x7FFF, -0x1, 0x0}) secondInput.Push([]int16{0x1, 0x7FFF, -0x1, -0x7FFF, 0x0}) m.doMix() - require.Equal(t, []byte{0xFF, 0x7F, 0xFF, 0x7F, 0x1, 0x80, 0x1, 0x80, 0x0, 0x0}, sample) + require.Equal(t, media.PCM16Sample{0x7FFF, 0x7FFF, -0x7FFF, -0x7FFF, 0x0}, sample) m.RemoveInput(firstInput) m.RemoveInput(secondInput) @@ -77,9 +77,9 @@ func TestMixer(t *testing.T) { input.Push([]int16{0, 1, 2, 3, 4}) m.doMix() - expected := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + expected := media.PCM16Sample{0, 0, 0, 0, 0} if i == 4 { - expected = []byte{0, 0, 1, 0, 2, 0, 3, 0, 4, 0} + expected = media.PCM16Sample{0, 1, 2, 3, 4} } require.Equal(t, expected, sample) @@ -98,9 +98,9 @@ func TestMixer(t *testing.T) { for i := 0; i < 8; i++ { m.doMix() - expected := []byte{0, 0, 1, 0, 2, 0, 3, 0, 4, 0} + expected := media.PCM16Sample{0, 1, 2, 3, 4} if i == 7 { - expected = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + expected = media.PCM16Sample{0, 0, 0, 0, 0} } require.Equal(t, expected, sample) diff --git a/pkg/sip/inbound.go b/pkg/sip/inbound.go index 9986d143..28f502a3 100644 --- a/pkg/sip/inbound.go +++ b/pkg/sip/inbound.go @@ -238,7 +238,7 @@ func (c *inboundCall) runMediaConn(offerData []byte, conf *config.Config) (answe // Encoding pipeline (LK -> SIP) // Need to be created earlier to send the pin prompts. s := rtp.NewMediaStreamOut[ulaw.Sample](conn, rtpPacketDur) - c.lkRoom.SetOutput(ulaw.Decode(s)) + c.lkRoom.SetOutput(ulaw.Encode(s)) return sdpGenerateAnswer(offer, c.s.signalingIp, conn.LocalAddr().Port) } @@ -335,8 +335,7 @@ func (c *inboundCall) createLiveKitParticipant(roomName, participantIdentity, ws } // Decoding pipeline (SIP -> LK) - lpcm := media.DecodePCM(local) - law := ulaw.Encode(lpcm) + law := ulaw.Decode(local) var h rtp.Handler = rtp.NewMediaStreamIn(law) c.audioHandler.Store(&h) diff --git a/pkg/sip/media_file.go b/pkg/sip/media_file.go index 942bea23..363fd3b2 100644 --- a/pkg/sip/media_file.go +++ b/pkg/sip/media_file.go @@ -51,8 +51,7 @@ func readMkvAudioFile(data []byte) []media.PCM16Sample { for _, block := range cluster.SimpleBlock { for _, data := range block.Data { lpcm := ulaw.Sample(data).Decode() - pcm := lpcm.Decode() - frames = append(frames, pcm) + frames = append(frames, lpcm) } } } diff --git a/pkg/sip/outbound.go b/pkg/sip/outbound.go index fe58ef55..c80d5f05 100644 --- a/pkg/sip/outbound.go +++ b/pkg/sip/outbound.go @@ -207,11 +207,10 @@ func (c *outboundCall) relinkMedia() { } // Encoding pipeline (LK -> SIP) s := rtp.NewMediaStreamOut[ulaw.Sample](c.rtpConn, rtpPacketDur) - c.lkRoom.SetOutput(ulaw.Decode(s)) + c.lkRoom.SetOutput(ulaw.Encode(s)) // Decoding pipeline (SIP -> LK) - lpcm := media.DecodePCM(c.lkRoomIn) - law := ulaw.Encode(lpcm) + law := ulaw.Decode(c.lkRoomIn) c.rtpConn.OnRTP(rtp.NewMediaStreamIn(law)) } diff --git a/pkg/sip/room.go b/pkg/sip/room.go index 8bf3597a..e471c1ab 100644 --- a/pkg/sip/room.go +++ b/pkg/sip/room.go @@ -15,8 +15,6 @@ package sip import ( - "sync/atomic" - "github.com/livekit/protocol/logger" lksdk "github.com/livekit/server-sdk-go" "github.com/pion/webrtc/v3" @@ -31,7 +29,7 @@ import ( type Room struct { room *lksdk.Room mix *mixer.Mixer - out atomic.Pointer[media.Writer[media.LPCM16Sample]] + out media.SwitchWriter[media.PCM16Sample] identity string } @@ -42,12 +40,7 @@ type lkRoomConfig struct { func NewRoom() *Room { r := &Room{} - r.mix = mixer.NewMixer(func(data []byte) { - sample := media.LPCM16Sample(data) - if out := r.Output(); out != nil { - _ = out.WriteSample(sample) - } - }, sampleRate) + r.mix = mixer.NewMixer(&r.out, sampleRate) return r } @@ -107,23 +100,15 @@ func ConnectToRoom(conf *config.Config, roomName string, identity string) (*Room return r, nil } -func (r *Room) Output() media.Writer[media.LPCM16Sample] { - ptr := r.out.Load() - if ptr == nil { - return nil - } - return *ptr +func (r *Room) Output() media.Writer[media.PCM16Sample] { + return r.out.Get() } -func (r *Room) SetOutput(out media.Writer[media.LPCM16Sample]) { +func (r *Room) SetOutput(out media.Writer[media.PCM16Sample]) { if r == nil { return } - if out == nil { - r.out.Store(nil) - } else { - r.out.Store(&out) - } + r.out.Set(out) } func (r *Room) Close() error {