Skip to content

Instantly share code, notes, and snippets.

@lightrush
Last active March 2, 2022 23:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lightrush/cf1a1c4c7e9bee1045c419d8b8e0fe29 to your computer and use it in GitHub Desktop.
Save lightrush/cf1a1c4c7e9bee1045c419d8b8e0fe29 to your computer and use it in GitHub Desktop.
Mic echo cancellation and noise suppression with PulseAudio and RNNoise
#!/bin/sh
# Usage:
# 1. Select the input (mic) and output (sound card) you want to eliminate echo
# and denoise in your sound settings as default input and output devices.
# 2. Run `denoise`.
#
# This process can be repeated without restarting PulseAudio. Just repeat the steps.
# Location of RNNoise LADSPA lib on the system
# Get it from https://github.com/werman/noise-suppression-for-voice/releases
# and place librnnoise_ladspa.so at the path below or adjust it to match the lib location.
LIBRNNOISE="${HOME}/.local/opt/rnnoise/librnnoise_ladspa.so"
if [ ! -e "${LIBRNNOISE}" ]; then
echo "Couldn't find ${LIBRNNOISE}. Have you installed it?"
exit 1
fi
# By default assume generic mono mic.
AEC_ARGS="'noise_suppression=false voice_detection=false extended_filter=true'"
MIC_CHANNELS="1"
LADSPA_PLUGIN_LABEL="noise_suppressor_mono"
# pacmd does not currently support retreiving default sink directly so we need to grep for it.
DEFAULT_SINK=$(pacmd info | grep -i "default sink" | head -n1 | cut -f4 -d" ")
DEFAULT_SOURCE=$(pacmd info | grep -i "default source" | head -n1 | cut -f4 -d" ")
# Special mic config for some interesting mic. This block can be repeated for other special mics.
# Run `pactl list sources short` and pick your special mic's name from the second column. It usually
# starts with "alsa_input.*" then assign the value to SPECIAL_MIC and do your custom config inside
# the `if` block. You can place multiple such blocks if you have multiple special mic configs. E.g.
# you're plugging multiple mics for different purposes like a web cam mic and a headset mic which
# require different configs.
# The following is configuring the Framework Laptop's stereo mic array.
SPECIAL_MIC="alsa_input.pci-0000_00_1f.3.analog-stereo"
if [ "${DEFAULT_SOURCE}" = "${SPECIAL_MIC}" ]; then
# Turn on beamforming and configure mix separation to be 7cm.
AEC_ARGS="'noise_suppression=false voice_detection=false extended_filter=true beamforming=true mic_geometry=-0.035,0,0,0.035,0,0'"
# We need 2 channels.
MIC_CHANNELS="2"
# Use the stereo suppressor plugin.
LADSPA_PLUGIN_LABEL="noise_suppressor_stereo"
fi
# Logitech 922 Pro
SPECIAL_MIC="alsa_input.usb-046d_C922_Pro_Stream_Webcam_192F2ADF-02.analog-stereo"
if [ "${DEFAULT_SOURCE}" = "${SPECIAL_MIC}" ]; then
# Can't get beamforming to work correctly yet so don't enable it.
# The right channel is significantly queter than the left at the same level adjustment so it
# could be defective and thus affecting beamforming on this particular unit. YMMV
# Output is very low without AGC.
AEC_ARGS="'experimental_agc=true agc_start_volume=200 noise_suppression=false voice_detection=false extended_filter=true'"
MIC_CHANNELS="2"
LADSPA_PLUGIN_LABEL="noise_suppressor_stereo"
fi
# Unload all modules we loaded in reverse order.
# This provides idempotency and therefore we can be re-run on the fly.
# Note that if any of the modules have been loaded multiples times, we won't unload the
# right module. This can be improved.
INDEX="$(pactl list modules short | grep module-remap-source | grep 'source_name=denoised' | cut -f1 | tail -n1)"
if [ -n "${INDEX}" ]; then
echo Unloading... "${INDEX}"
pactl unload-module "${INDEX}"
fi
INDEX="$(pactl list modules short | grep module-loopback | grep "sink=mic_raw_in" | cut -f1 | tail -n1)"
if [ -n "${INDEX}" ]; then
echo Unloading... "${INDEX}"
pactl unload-module "${INDEX}"
fi
INDEX="$(pactl list modules short | grep module-ladspa-sink | grep "sink_name=mic_raw_in" | cut -f1 | tail -n1)"
if [ -n "${INDEX}" ]; then
echo Unloading... "${INDEX}"
pactl unload-module "${INDEX}"
fi
INDEX="$(pactl list modules short | grep module-null-sink | grep 'sink_name=mic_denoised_out' | cut -f1 | tail -n1)"
if [ -n "${INDEX}" ]; then
echo Unloading... "${INDEX}"
pactl unload-module "${INDEX}"
fi
INDEX="$(pactl list modules short | grep module-echo-cancel | grep source_name=echo_cancel_source | cut -f1 | tail -n1)"
if [ -n "${INDEX}" ]; then
echo Unloading... "${INDEX}"
pactl unload-module "${INDEX}"
fi
# Load echo-cancel
if ! pactl load-module module-echo-cancel use_volume_sharing=1 use_master_format=1 aec_method=webrtc aec_args="${AEC_ARGS}" source_master="${DEFAULT_SOURCE}" sink_master="${DEFAULT_SINK}" source_name=echo_cancel_source sink_name=echo_cancel_sink; then
echo "Couldn't load module-echo-cancel."
exit 1
fi
sleep 1
# This is needed to get the echo module to actually work.
if ! pactl set-default-sink echo_cancel_sink; then
echo "Couldn't set-default-sink echo_cancel_sink."
exit 1
fi
if ! pactl set-default-source echo_cancel_source; then
echo "Couldn't set-default-source echo_cancel_source."
exit 1
fi
sleep 1
# Load RNNoise
if ! pactl load-module module-null-sink sink_name=mic_denoised_out; then
echo "Couldn't load module-null-sink."
exit 1
fi
if ! pactl load-module module-ladspa-sink sink_name=mic_raw_in sink_master=mic_denoised_out label="${LADSPA_PLUGIN_LABEL}" plugin="${LIBRNNOISE}" control=50; then
echo "Couldn't load module-ladspa-sink. Is librnnoise_ladspa.so in the right place?"
exit 1
fi
if ! pactl load-module module-loopback source="echo_cancel_source" sink=mic_raw_in channels="${MIC_CHANNELS}" source_dont_move=true sink_dont_move=true latency_msec=30; then
echo "Couldn't load module-loopback."
exit 1
fi
if ! pactl load-module module-remap-source source_name=denoised master=mic_denoised_out.monitor channels="${MIC_CHANNELS}"; then
echo "Couldn't load module-remap-source."
exit 1
fi
sleep 1
if ! pactl set-default-source denoised; then
echo "Couldn't set-default-source denoised."
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment