|
| 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 | +} |
0 commit comments