Skip to content

Commit 734ede1

Browse files
committed
add vendored WebRTC APM and PortAudio native sources
1 parent cf644a5 commit 734ede1

1,470 files changed

Lines changed: 265333 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
pkg/provider/resources/*.h264 filter=lfs diff=lfs merge=lfs -text
22
pkg/provider/resources/*.ivf filter=lfs diff=lfs merge=lfs -text
33
pkg/provider/resources/*.ogg filter=lfs diff=lfs merge=lfs -text
4+
5+
# Vendored C/C++ source (WebRTC APM, PortAudio, abseil, pffft, rnnoise)
6+
pkg/apm/webrtc/**/*.cc linguist-vendored
7+
pkg/apm/webrtc/**/*.c linguist-vendored
8+
pkg/apm/webrtc/**/*.h linguist-vendored
9+
pkg/apm/webrtc/**/*.m linguist-vendored
10+
pkg/apm/webrtc/**/*.inc linguist-vendored
11+
pkg/portaudio/pa_src/** linguist-vendored

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "pkg/portaudio/pa_src"]
2+
path = pkg/portaudio/pa_src
3+
url = https://github.com/PortAudio/portaudio.git

pkg/apm/apm.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//go:build console
2+
3+
// Package apm provides Go bindings for the WebRTC Audio Processing Module (APM).
4+
// It supports echo cancellation (AEC3), noise suppression, automatic gain control,
5+
// and high-pass filtering. Audio must be 48kHz int16 PCM in 10ms frames (480 samples/channel).
6+
package apm
7+
8+
// #include "bridge.h"
9+
import "C"
10+
11+
import (
12+
"errors"
13+
"runtime"
14+
"unsafe"
15+
)
16+
17+
type APMConfig struct {
18+
EchoCanceller bool
19+
GainController bool
20+
HighPassFilter bool
21+
NoiseSuppressor bool
22+
CaptureChannels int
23+
RenderChannels int
24+
}
25+
26+
func DefaultConfig() APMConfig {
27+
return APMConfig{
28+
EchoCanceller: true,
29+
GainController: true,
30+
HighPassFilter: true,
31+
NoiseSuppressor: true,
32+
CaptureChannels: 1,
33+
RenderChannels: 1,
34+
}
35+
}
36+
37+
type APM struct {
38+
handle C.ApmHandle
39+
}
40+
41+
func NewAPM(config APMConfig) (*APM, error) {
42+
capCh := config.CaptureChannels
43+
if capCh == 0 {
44+
capCh = 1
45+
}
46+
renCh := config.RenderChannels
47+
if renCh == 0 {
48+
renCh = 1
49+
}
50+
51+
var cerr C.int
52+
handle := C.apm_create(
53+
boolToInt(config.EchoCanceller),
54+
boolToInt(config.GainController),
55+
boolToInt(config.HighPassFilter),
56+
boolToInt(config.NoiseSuppressor),
57+
C.int(capCh),
58+
C.int(renCh),
59+
&cerr,
60+
)
61+
if handle == nil {
62+
return nil, errors.New("apm: failed to create audio processing module")
63+
}
64+
65+
a := &APM{handle: handle}
66+
runtime.SetFinalizer(a, func(a *APM) { a.Close() })
67+
return a, nil
68+
}
69+
70+
// ProcessCapture processes a 10ms capture (microphone) frame in-place.
71+
// samples must contain exactly 480 * numChannels int16 values.
72+
func (a *APM) ProcessCapture(samples []int16) error {
73+
if a.handle == nil {
74+
return errors.New("apm: closed")
75+
}
76+
if len(samples) == 0 {
77+
return nil
78+
}
79+
numChannels := len(samples) / 480
80+
if numChannels == 0 {
81+
numChannels = 1
82+
}
83+
ret := C.apm_process_capture(
84+
a.handle,
85+
(*C.int16_t)(unsafe.Pointer(&samples[0])),
86+
C.int(numChannels),
87+
)
88+
if ret != 0 {
89+
return errors.New("apm: ProcessCapture failed")
90+
}
91+
return nil
92+
}
93+
94+
// ProcessRender processes a 10ms render (speaker/far-end) frame in-place.
95+
// This feeds the echo canceller with the signal being played back.
96+
// samples must contain exactly 480 * numChannels int16 values.
97+
func (a *APM) ProcessRender(samples []int16) error {
98+
if a.handle == nil {
99+
return errors.New("apm: closed")
100+
}
101+
if len(samples) == 0 {
102+
return nil
103+
}
104+
numChannels := len(samples) / 480
105+
if numChannels == 0 {
106+
numChannels = 1
107+
}
108+
ret := C.apm_process_render(
109+
a.handle,
110+
(*C.int16_t)(unsafe.Pointer(&samples[0])),
111+
C.int(numChannels),
112+
)
113+
if ret != 0 {
114+
return errors.New("apm: ProcessRender failed")
115+
}
116+
return nil
117+
}
118+
119+
// SetStreamDelayMs sets the delay in milliseconds between the far-end signal
120+
// being rendered and arriving at the near-end microphone.
121+
func (a *APM) SetStreamDelayMs(ms int) {
122+
if a.handle == nil {
123+
return
124+
}
125+
C.apm_set_stream_delay_ms(a.handle, C.int(ms))
126+
}
127+
128+
func (a *APM) StreamDelayMs() int {
129+
if a.handle == nil {
130+
return 0
131+
}
132+
return int(C.apm_stream_delay_ms(a.handle))
133+
}
134+
135+
// Stats holds AEC statistics from the WebRTC APM.
136+
type Stats struct {
137+
EchoReturnLoss float64 // ERL in dB (higher = more echo removed)
138+
EchoReturnLossEnhancement float64 // ERLE in dB (higher = better cancellation)
139+
DivergentFilterFraction float64 // 0-1, fraction of time filter is divergent
140+
DelayMs int // Estimated echo path delay
141+
ResidualEchoLikelihood float64 // 0-1, likelihood of residual echo
142+
HasERL bool
143+
HasERLE bool
144+
HasDelay bool
145+
HasResidualEcho bool
146+
HasDivergent bool
147+
}
148+
149+
// GetStats returns the current AEC statistics.
150+
func (a *APM) GetStats() Stats {
151+
if a.handle == nil {
152+
return Stats{}
153+
}
154+
var cs C.ApmStats
155+
C.apm_get_stats(a.handle, &cs)
156+
return Stats{
157+
EchoReturnLoss: float64(cs.echo_return_loss),
158+
EchoReturnLossEnhancement: float64(cs.echo_return_loss_enhancement),
159+
DivergentFilterFraction: float64(cs.divergent_filter_fraction),
160+
DelayMs: int(cs.delay_ms),
161+
ResidualEchoLikelihood: float64(cs.residual_echo_likelihood),
162+
HasERL: cs.has_erl != 0,
163+
HasERLE: cs.has_erle != 0,
164+
HasDelay: cs.has_delay != 0,
165+
HasResidualEcho: cs.has_residual_echo != 0,
166+
HasDivergent: cs.has_divergent != 0,
167+
}
168+
}
169+
170+
func (a *APM) Close() {
171+
if a.handle != nil {
172+
C.apm_destroy(a.handle)
173+
a.handle = nil
174+
}
175+
}
176+
177+
func boolToInt(b bool) C.int {
178+
if b {
179+
return 1
180+
}
181+
return 0
182+
}

pkg/apm/bridge.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "bridge.h"
2+
3+
#include "api/audio/builtin_audio_processing_builder.h"
4+
#include "api/environment/environment_factory.h"
5+
#include "api/scoped_refptr.h"
6+
#include "modules/audio_processing/include/audio_processing.h"
7+
8+
#include <memory>
9+
10+
struct ApmInstance {
11+
webrtc::scoped_refptr<webrtc::AudioProcessing> apm;
12+
};
13+
14+
extern "C" {
15+
16+
ApmHandle apm_create(int echo, int gain, int hpf, int ns,
17+
int capture_ch, int render_ch, int* err) {
18+
(void)capture_ch;
19+
(void)render_ch;
20+
21+
auto apm = webrtc::BuiltinAudioProcessingBuilder().Build(
22+
webrtc::CreateEnvironment());
23+
if (!apm) {
24+
if (err) *err = -1;
25+
return nullptr;
26+
}
27+
28+
webrtc::AudioProcessing::Config config;
29+
config.echo_canceller.enabled = (echo != 0);
30+
config.gain_controller1.enabled = false;
31+
config.gain_controller2.enabled = (gain != 0);
32+
config.high_pass_filter.enabled = (hpf != 0);
33+
config.noise_suppression.enabled = (ns != 0);
34+
if (ns) {
35+
config.noise_suppression.level =
36+
webrtc::AudioProcessing::Config::NoiseSuppression::kHigh;
37+
}
38+
39+
apm->ApplyConfig(config);
40+
apm->Initialize();
41+
42+
auto* inst = new ApmInstance{std::move(apm)};
43+
if (err) *err = 0;
44+
return static_cast<ApmHandle>(inst);
45+
}
46+
47+
void apm_destroy(ApmHandle h) {
48+
if (h) {
49+
delete static_cast<ApmInstance*>(h);
50+
}
51+
}
52+
53+
int apm_process_capture(ApmHandle h, int16_t* samples, int num_channels) {
54+
auto* inst = static_cast<ApmInstance*>(h);
55+
// 10ms at 48kHz = 480 samples per channel
56+
webrtc::StreamConfig stream_cfg(48000, num_channels);
57+
return inst->apm->ProcessStream(samples, stream_cfg, stream_cfg, samples);
58+
}
59+
60+
int apm_process_render(ApmHandle h, int16_t* samples, int num_channels) {
61+
auto* inst = static_cast<ApmInstance*>(h);
62+
webrtc::StreamConfig stream_cfg(48000, num_channels);
63+
return inst->apm->ProcessReverseStream(samples, stream_cfg, stream_cfg, samples);
64+
}
65+
66+
void apm_set_stream_delay_ms(ApmHandle h, int delay_ms) {
67+
auto* inst = static_cast<ApmInstance*>(h);
68+
inst->apm->set_stream_delay_ms(delay_ms);
69+
}
70+
71+
int apm_stream_delay_ms(ApmHandle h) {
72+
auto* inst = static_cast<ApmInstance*>(h);
73+
return inst->apm->stream_delay_ms();
74+
}
75+
76+
void apm_get_stats(ApmHandle h, ApmStats* out) {
77+
if (!h || !out) return;
78+
auto* inst = static_cast<ApmInstance*>(h);
79+
auto stats = inst->apm->GetStatistics();
80+
81+
out->has_erl = stats.echo_return_loss.has_value() ? 1 : 0;
82+
out->echo_return_loss = stats.echo_return_loss.value_or(0.0);
83+
84+
out->has_erle = stats.echo_return_loss_enhancement.has_value() ? 1 : 0;
85+
out->echo_return_loss_enhancement = stats.echo_return_loss_enhancement.value_or(0.0);
86+
87+
out->has_divergent = stats.divergent_filter_fraction.has_value() ? 1 : 0;
88+
out->divergent_filter_fraction = stats.divergent_filter_fraction.value_or(0.0);
89+
90+
out->has_delay = stats.delay_ms.has_value() ? 1 : 0;
91+
out->delay_ms = stats.delay_ms.value_or(0);
92+
93+
out->has_residual_echo = stats.residual_echo_likelihood.has_value() ? 1 : 0;
94+
out->residual_echo_likelihood = stats.residual_echo_likelihood.value_or(0.0);
95+
}
96+
97+
} // extern "C"

pkg/apm/bridge.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//go:build console
2+
3+
package apm
4+
5+
// #cgo CXXFLAGS: -I${SRCDIR}/webrtc -I${SRCDIR}/webrtc/third_party/abseil-cpp -std=c++17 -fno-rtti -DWEBRTC_APM_DEBUG_DUMP=0 -DWEBRTC_AUDIO_PROCESSING_ONLY_BUILD -DNDEBUG -Wno-unused-parameter -Wno-missing-field-initializers -Wno-sign-compare -Wno-deprecated-declarations -Wno-nullability-completeness -Wno-shorten-64-to-32
6+
// #cgo darwin CXXFLAGS: -DWEBRTC_MAC -DWEBRTC_POSIX
7+
// #cgo linux CXXFLAGS: -DWEBRTC_LINUX -DWEBRTC_POSIX
8+
// #cgo windows CXXFLAGS: -DWEBRTC_WIN
9+
// #cgo arm64 CXXFLAGS: -DWEBRTC_HAS_NEON -DWEBRTC_ARCH_ARM64
10+
// #cgo darwin LDFLAGS: -lc++
11+
// #cgo linux LDFLAGS: -lc++ -lm -lpthread
12+
// #cgo windows LDFLAGS: -lc++
13+
// #include "bridge.h"
14+
import "C"
15+
16+
import _ "github.com/livekit/livekit-cli/v2/pkg/apm/webrtc"

pkg/apm/bridge.h

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#ifndef LK_APM_BRIDGE_H
2+
#define LK_APM_BRIDGE_H
3+
4+
#include <stdint.h>
5+
6+
#ifdef __cplusplus
7+
extern "C" {
8+
#endif
9+
10+
typedef void* ApmHandle;
11+
12+
// Create an APM instance. Returns NULL on error, sets *err to non-zero.
13+
ApmHandle apm_create(int echo, int gain, int hpf, int ns,
14+
int capture_ch, int render_ch, int* err);
15+
16+
// Destroy an APM instance.
17+
void apm_destroy(ApmHandle h);
18+
19+
// Process a 10ms capture frame in-place. Returns 0 on success.
20+
int apm_process_capture(ApmHandle h, int16_t* samples, int num_channels);
21+
22+
// Process a 10ms render (far-end/playback) frame in-place. Returns 0 on success.
23+
int apm_process_render(ApmHandle h, int16_t* samples, int num_channels);
24+
25+
// Set the stream delay in milliseconds for echo cancellation.
26+
void apm_set_stream_delay_ms(ApmHandle h, int delay_ms);
27+
28+
// Get the current stream delay in milliseconds.
29+
int apm_stream_delay_ms(ApmHandle h);
30+
31+
// AEC statistics returned by apm_get_stats.
32+
typedef struct {
33+
int has_erl;
34+
double echo_return_loss; // ERL in dB
35+
int has_erle;
36+
double echo_return_loss_enhancement; // ERLE in dB
37+
int has_divergent;
38+
double divergent_filter_fraction;
39+
int has_delay;
40+
int delay_ms;
41+
int has_residual_echo;
42+
double residual_echo_likelihood;
43+
} ApmStats;
44+
45+
// Get current AEC statistics.
46+
void apm_get_stats(ApmHandle h, ApmStats* out);
47+
48+
#ifdef __cplusplus
49+
}
50+
#endif
51+
52+
#endif // LK_APM_BRIDGE_H

0 commit comments

Comments
 (0)