Skip to content

Commit b6dad32

Browse files
committed
Support bandwidth-efficient RTP format for AMR-WB.
1 parent 27d39f4 commit b6dad32

4 files changed

Lines changed: 201 additions & 41 deletions

File tree

amrwb/amrwb.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ const (
3131
SampleRate = 16000
3232
)
3333

34+
type Format = amrwb.Format
35+
36+
const (
37+
Storage = amrwb.Storage
38+
RTPBandwidthEfficient = amrwb.RTPBandwidthEfficient
39+
)
40+
3441
func init() {
3542
media.RegisterCodec(media.NewAudioCodec(media.CodecInfo{
3643
SDPName: SDPNameAndRate,
@@ -39,7 +46,11 @@ func init() {
3946
Priority: -4,
4047
FileExt: "amrwb",
4148
Disabled: true,
42-
}, Decode, Encode))
49+
}, func(w media.PCM16Writer) media.WriteCloser[Sample] {
50+
return Decode(w, RTPBandwidthEfficient)
51+
}, func(w media.WriteCloser[Sample]) media.PCM16Writer {
52+
return Encode(w, RTPBandwidthEfficient)
53+
}))
4354
}
4455

4556
type Sample []byte
@@ -58,13 +69,13 @@ func (s Sample) CopyTo(dst []byte) (int, error) {
5869

5970
type Writer = media.WriteCloser[Sample]
6071

61-
func Decode(w media.PCM16Writer) Writer {
72+
func Decode(w media.PCM16Writer, format amrwb.Format) Writer {
6273
if w.SampleRate() != SampleRate {
6374
w = media.ResampleWriter(w, SampleRate)
6475
}
6576
return &Decoder{
6677
w: w,
67-
d: amrwb.NewDecoder(),
78+
d: amrwb.NewDecoder(format),
6879
}
6980
}
7081

@@ -108,13 +119,17 @@ func (d *Decoder) WriteSample(in Sample) error {
108119
return blockErr
109120
}
110121

111-
func Encode(w Writer) media.PCM16Writer {
122+
func Encode(w Writer, format amrwb.Format) media.PCM16Writer {
123+
return EncodeWith(w, format, amrwb.Best)
124+
}
125+
126+
func EncodeWith(w Writer, format amrwb.Format, mode amrwb.Mode) media.PCM16Writer {
112127
if w.SampleRate() != SampleRate {
113128
panic("unsupported sample rate")
114129
}
115130
return &Encoder{
116131
w: w,
117-
e: amrwb.NewEncoder(amrwb.Best),
132+
e: amrwb.NewEncoder(format, mode),
118133
}
119134
}
120135

amrwb/amrwb_test.go

Lines changed: 178 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"bufio"
55
"crypto/sha1"
66
"encoding/hex"
7+
"fmt"
78
"io"
89
"os"
910
"os/exec"
11+
"slices"
1012
"testing"
1113

1214
"github.com/livekit/amrwb-cgo"
@@ -15,14 +17,148 @@ import (
1517
"github.com/livekit/media-sdk/res/testdata"
1618
)
1719

20+
const rate = 16000
21+
1822
func TestAMRWB(t *testing.T) {
19-
const rate = 16000
20-
frames := res.ReadOggAudioFile(testdata.TestAudioOgg16K, rate, 1)
23+
frames := readOGG()
24+
25+
modes := [9]amrwb.Mode{
26+
amrwb.Mode6Kb,
27+
amrwb.Mode8Kb,
28+
amrwb.Mode12Kb,
29+
amrwb.Mode14Kb,
30+
amrwb.Mode16Kb,
31+
amrwb.Mode18Kb,
32+
amrwb.Mode20Kb,
33+
amrwb.Mode23Kb,
34+
amrwb.Mode24Kb,
35+
}
36+
37+
const selFrame = 41
38+
39+
t.Run("storage", func(t *testing.T) {
40+
const format = Storage
41+
hashes := [9][]string{
42+
0: {
43+
"0236b84c931bcf121da1a0587420d30d1aac0301",
44+
},
45+
1: {
46+
"dabd16afd6fcacefc26d221e3fa06a2bc6644076",
47+
},
48+
2: {
49+
"61d20f4e120dd13e2fbaaf74dd3c08863ed5a557",
50+
},
51+
3: {
52+
"bece54885e71b89dfcd86c8cbe7ce9fd2fefb4f7",
53+
},
54+
4: {
55+
"67fa8975e5757d3b83aba349a7cc1f4e5d06ff66",
56+
},
57+
5: {
58+
"6f9c014385c8e7dbde780ae2e4a89b03b5e03dcb",
59+
},
60+
6: {
61+
"039be8ddd5d2de4bb148d166552e6d59eebb3e40",
62+
},
63+
7: {
64+
"2d0346353617d23bb59a6519167707115be2245e",
65+
},
66+
8: {
67+
"266c17405ad9cb6057bfe1a3e0cf764c448a10a0",
68+
"b3d9349b01e93c523311e587d639142d0c79546b",
69+
},
70+
}
71+
for mi, mode := range modes {
72+
t.Run(fmt.Sprintf("mode %d", mi), func(t *testing.T) {
73+
hashes := hashes[mi]
74+
75+
blocks := encodeAMR(t, format, mode, frames)
76+
t.Logf("block: %x", blocks[selFrame])
77+
78+
name := fmt.Sprintf("testdata_mode_%d", mode)
79+
80+
hashAMR := dumpAMR(t, name+".amrwb", blocks)
81+
ffmpegAMRFileToOGG(t, name+".amrwb")
82+
if !slices.Contains(hashes, hashAMR) {
83+
t.Errorf("unexpected amrwb hash %s", hashAMR)
84+
}
85+
86+
out := decodeAMR(t, format, blocks)
87+
88+
hashOut := dumpPCM(t, name+".s16le", out)
89+
ffmpegPCMFileToOGG(t, name+".s16le")
90+
if hashOut != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
91+
t.Errorf("unexpected output hash %s", hashOut)
92+
}
93+
})
94+
}
95+
})
96+
t.Run("rtp", func(t *testing.T) {
97+
const format = RTPBandwidthEfficient
98+
hashes := [9][]string{
99+
0: {
100+
"a8e453a08c894e2c3d0f7bf62142d6eb63bfe5c8",
101+
},
102+
1: {
103+
"270a13237d862e9e20e725a0a9484edddc3b4f00",
104+
},
105+
2: {
106+
"607966a93720aaa1c8425e08b6471f58282c0884",
107+
},
108+
3: {
109+
"8a620e9c3ce1656ea07804be210befe5c878f3f3",
110+
},
111+
4: {
112+
"9121d8781a397f4784174f15309f6f48d86db338",
113+
},
114+
5: {
115+
"a6e005daf0e8040530157be090934376610beeb7",
116+
},
117+
6: {
118+
"4497873be7c5574468e68c0718a5e57d3c8ac099",
119+
},
120+
7: {
121+
"b73f2049b96be4793f611339e4d3435ec19a8760",
122+
},
123+
8: {
124+
"7aa1a489f7f951ab421f0e462abb18c7d4c00c68",
125+
},
126+
}
127+
for mi, mode := range modes {
128+
t.Run(fmt.Sprintf("mode %d", mi), func(t *testing.T) {
129+
hashes := hashes[mi]
130+
131+
blocks := encodeAMR(t, format, mode, frames)
132+
t.Logf("block: %x", blocks[selFrame])
133+
134+
name := fmt.Sprintf("testdata_rtp_mode_%d", mode)
135+
136+
hashAMR := dumpAMR(t, name+".amrwb", blocks)
137+
if !slices.Contains(hashes, hashAMR) {
138+
t.Errorf("unexpected amrwb hash %s", hashAMR)
139+
}
140+
141+
out := decodeAMR(t, format, blocks)
142+
143+
hashOut := dumpPCM(t, name+".s16le", out)
144+
ffmpegPCMFileToOGG(t, name+".s16le")
145+
if hashOut != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
146+
t.Errorf("unexpected output hash %s", hashOut)
147+
}
148+
})
149+
}
150+
})
151+
}
21152

153+
func readOGG() []media.PCM16Sample {
154+
return res.ReadOggAudioFile(testdata.TestAudioOgg16K, rate, 1)
155+
}
156+
157+
func encodeAMR(t testing.TB, format amrwb.Format, mode amrwb.Mode, frames []media.PCM16Sample) []Sample {
22158
blocks := make([]Sample, 0, len(frames))
23159
fw := media.NewFrameWriter(&blocks, rate)
24160

25-
enc := Encode(fw)
161+
enc := EncodeWith(fw, format, mode)
26162
t.Cleanup(func() {
27163
enc.Close()
28164
})
@@ -32,33 +168,32 @@ func TestAMRWB(t *testing.T) {
32168
t.Errorf("encoding frame %d/%d: %v", i+1, len(frames), err)
33169
}
34170
}
35-
famr, err := os.Create("testdata.amrwb")
171+
return blocks
172+
}
173+
174+
func dumpAMR(t testing.TB, name string, blocks []Sample) string {
175+
f, err := os.Create(name)
36176
if err != nil {
37177
t.Fatal(err)
38178
}
39-
defer famr.Close()
40-
41-
hamr := sha1.New()
179+
defer f.Close()
42180

43-
wbamr := bufio.NewWriter(io.MultiWriter(famr, hamr))
44-
wbamr.WriteString(amrwb.Magic)
181+
h := sha1.New()
182+
bw := bufio.NewWriter(io.MultiWriter(f, h))
183+
bw.WriteString(amrwb.Magic)
45184
for _, block := range blocks {
46-
wbamr.Write(block)
47-
}
48-
wbamr.Flush()
49-
50-
hashAMR := hex.EncodeToString(hamr.Sum(nil))
51-
switch hashAMR {
52-
default:
53-
t.Errorf("unexpected amrwb hash %s", hashAMR)
54-
case "266c17405ad9cb6057bfe1a3e0cf764c448a10a0",
55-
"b3d9349b01e93c523311e587d639142d0c79546b":
185+
bw.Write(block)
56186
}
187+
bw.Flush()
188+
189+
return hex.EncodeToString(h.Sum(nil))
190+
}
57191

192+
func decodeAMR(t testing.TB, format amrwb.Format, blocks []Sample) []media.PCM16Sample {
58193
var out []media.PCM16Sample
59194
pw := media.NewPCM16FrameWriter(&out, rate)
60195

61-
dec := Decode(pw)
196+
dec := Decode(pw, format)
62197
t.Cleanup(func() {
63198
dec.Close()
64199
})
@@ -68,44 +203,54 @@ func TestAMRWB(t *testing.T) {
68203
t.Error(err)
69204
}
70205
}
206+
return out
207+
}
71208

72-
f, err := os.Create("testdata.s16le")
209+
func dumpPCM(t testing.TB, name string, data []media.PCM16Sample) string {
210+
f, err := os.Create(name)
73211
if err != nil {
74212
t.Fatal(err)
75213
}
76214
defer f.Close()
77215

78216
h := sha1.New()
79217
bw := bufio.NewWriter(io.MultiWriter(f, h))
80-
err = media.DumpFramesPCM16(f, rate, out)
218+
err = media.DumpFramesPCM16(f, rate, data)
81219
if err != nil {
82220
t.Fatal(err)
83221
}
84222
bw.Flush()
85223

86-
hashOut := hex.EncodeToString(h.Sum(nil))
87-
if hashOut != "da39a3ee5e6b4b0d3255bfef95601890afd80709" {
88-
t.Errorf("unexpected output hash %s", hashOut)
89-
}
224+
return hex.EncodeToString(h.Sum(nil))
225+
}
90226

227+
func ffmpegAMRFileToOGG(t testing.TB, name string) {
91228
if _, err := exec.LookPath("ffmpeg"); err != nil {
92229
t.Log("ffmpeg not found in $PATH")
93230
return
94231
}
95-
96-
err = exec.Command("ffmpeg",
97-
"-i", "testdata.amrwb",
98-
"testdata.amrwb.ogg",
232+
err := exec.Command("ffmpeg",
233+
"-i", name, name+".ogg",
99234
).Run()
100235
if err != nil {
101236
t.Error(err)
237+
} else {
238+
os.Remove(name)
239+
}
240+
}
241+
242+
func ffmpegPCMFileToOGG(t testing.TB, name string) {
243+
if _, err := exec.LookPath("ffmpeg"); err != nil {
244+
t.Log("ffmpeg not found in $PATH")
245+
return
102246
}
103-
err = exec.Command("ffmpeg",
247+
err := exec.Command("ffmpeg",
104248
"-f", "s16le", "-ar", "16000", "-ac", "1",
105-
"-i", "testdata.s16le",
106-
"testdata.s16le.ogg",
249+
"-i", name, name+".ogg",
107250
).Run()
108251
if err != nil {
109252
t.Error(err)
253+
} else {
254+
os.Remove(name)
110255
}
111256
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/go-logr/logr v1.4.3
1111
github.com/gotranspile/g722 v0.0.0-20240123003956-384a1bb16a19
1212
github.com/jfreymuth/oggvorbis v1.0.5
13-
github.com/livekit/amrwb-cgo v0.0.0-20260527094629-96c39974d532
13+
github.com/livekit/amrwb-cgo v0.0.0-20260612153743-6d4b69dc1470
1414
github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84
1515
github.com/pion/interceptor v0.1.45
1616
github.com/pion/rtp v1.10.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
4444
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
4545
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
4646
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
47-
github.com/livekit/amrwb-cgo v0.0.0-20260527094629-96c39974d532 h1:q0Gj2RmsBLAcsDJ6u9GWfXGKAhU88r66AOT//DCQ/XU=
48-
github.com/livekit/amrwb-cgo v0.0.0-20260527094629-96c39974d532/go.mod h1:nGBFrVVLyO0RlyM2pWnP/VKaZO6W3rzecL/YlkTPkW8=
47+
github.com/livekit/amrwb-cgo v0.0.0-20260612153743-6d4b69dc1470 h1:pYml12Ue55jDhrjJoQg+w8W+0PY9wlNsTo6INGwzOfU=
48+
github.com/livekit/amrwb-cgo v0.0.0-20260612153743-6d4b69dc1470/go.mod h1:nGBFrVVLyO0RlyM2pWnP/VKaZO6W3rzecL/YlkTPkW8=
4949
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
5050
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
5151
github.com/livekit/protocol v1.46.7-0.20260605212020-c0615a2f6f84 h1:dkHHthyor9dwxxdBmbeG1ZUI4bPHpTEk9DjYJSSSIl4=

0 commit comments

Comments
 (0)