werman / noise-suppression-for-voice

Noise suppression plugin based on Xiph's RNNoise
GNU General Public License v3.0
5.09k stars 234 forks source link

Pipewire Setup, affecting output, crackling sound #124

Closed rushvora closed 2 years ago

rushvora commented 2 years ago

I followed the Pipewire section, and I got it working successfully. The input is a USB microphone, and the output is to a USB DAC. If I select the the "Noise cancelling source" in audio settings, the audio cancellation works great, both the fan and AC sound is suppressed, even when I am speaking. But when playing music, or in Discord voice, or any sound really, there is a crackling sound that I can hear, along with the audio. If I select the USB microphone as the output instead of the Noise cancelling source, the crackling sound stops immediately.

Software Setup OS: EndeavourOS Linux x86_64 Kernel: 5.18.5-arch1-1 DE: Plasma 5.25.0

The pipewire config

# Daemon config file for PipeWire version "0.3.52" #
#
# Copy and edit this file in /etc/pipewire for system-wide changes
# or in ~/.config/pipewire for local changes.
#
# It is also possible to place a file with an updated section in
# /etc/pipewire/pipewire.conf.d/ for system-wide changes or in
# ~/.config/pipewire/pipewire.conf.d/ for local changes.
#

context.properties = {
    ## Configure properties in the system.
    #library.name.system                   = support/libspa-support
    #context.data-loop.library.name.system = support/libspa-support
    #support.dbus                          = true
    #link.max-buffers                      = 64
    link.max-buffers                       = 16                       # version < 3 clients can't handle more
    #mem.warn-mlock                        = false
    #mem.allow-mlock                       = true
    #mem.mlock-all                         = false
    #clock.power-of-two-quantum            = true
    #log.level                             = 2
    #cpu.zero.denormals                    = false

    core.daemon = true              # listening for socket connections
    core.name   = pipewire-0        # core name and socket name

    ## Properties for the DSP configuration.
    #default.clock.rate          = 48000
    #default.clock.allowed-rates = [ 44100 48000 ]
    #default.clock.quantum       = 1024
    default.clock.min-quantum   = 16
    #default.clock.max-quantum   = 2048
    #default.clock.quantum-limit = 8192
    #default.video.width         = 640
    #default.video.height        = 480
    #default.video.rate.num      = 25
    #default.video.rate.denom    = 1
    #
    #settings.check-quantum      = false
    #settings.check-rate         = false
    #
    # These overrides are only applied when running in a vm.
    vm.overrides = {
        default.clock.min-quantum = 1024
    }
}

context.spa-libs = {
    #<factory-name regex> = <library-name>
    #
    # Used to find spa factory names. It maps an spa factory name
    # regular expression to a library name that should contain
    # that factory.
    #
    audio.convert.* = audioconvert/libspa-audioconvert
    api.alsa.*      = alsa/libspa-alsa
    api.v4l2.*      = v4l2/libspa-v4l2
    api.libcamera.* = libcamera/libspa-libcamera
    api.bluez5.*    = bluez5/libspa-bluez5
    api.vulkan.*    = vulkan/libspa-vulkan
    api.jack.*      = jack/libspa-jack
    support.*       = support/libspa-support
    #videotestsrc   = videotestsrc/libspa-videotestsrc
    #audiotestsrc   = audiotestsrc/libspa-audiotestsrc
}

context.modules = [
    #{ name = <module-name>
    #    [ args  = { <key> = <value> ... } ]
    #    [ flags = [ [ ifexists ] [ nofail ] ]
    #}
    #
    # Loads a module with the given parameters.
    # If ifexists is given, the module is ignored when it is not found.
    # If nofail is given, module initialization failures are ignored.
    #

    # Uses realtime scheduling to boost the audio thread priorities. This uses
    # RTKit if the user doesn't have permission to use regular realtime
    # scheduling.
    { name = libpipewire-module-rt
        args = {
            nice.level    = -11
            #rt.prio      = 88
            #rt.time.soft = -1
            #rt.time.hard = -1
        }
        flags = [ ifexists nofail ]
    }

    # The native communication protocol.
    { name = libpipewire-module-protocol-native }

    # The profile module. Allows application to access profiler
    # and performance data. It provides an interface that is used
    # by pw-top and pw-profiler.
    { name = libpipewire-module-profiler }

    # Allows applications to create metadata objects. It creates
    # a factory for Metadata objects.
    { name = libpipewire-module-metadata }

    # Creates a factory for making devices that run in the
    # context of the PipeWire server.
    { name = libpipewire-module-spa-device-factory }

    # Creates a factory for making nodes that run in the
    # context of the PipeWire server.
    { name = libpipewire-module-spa-node-factory }

    # Allows creating nodes that run in the context of the
    # client. Is used by all clients that want to provide
    # data to PipeWire.
    { name = libpipewire-module-client-node }

    # Allows creating devices that run in the context of the
    # client. Is used by the session manager.
    { name = libpipewire-module-client-device }

    # The portal module monitors the PID of the portal process
    # and tags connections with the same PID as portal
    # connections.
    { name = libpipewire-module-portal
        flags = [ ifexists nofail ]
    }

    # The access module can perform access checks and block
    # new clients.
    { name = libpipewire-module-access
        args = {
            # access.allowed to list an array of paths of allowed
            # apps.
            #access.allowed = [
            #    /usr/bin/pipewire-media-session
            #]

            # An array of rejected paths.
            #access.rejected = [ ]

            # An array of paths with restricted access.
            #access.restricted = [ ]

            # Anything not in the above lists gets assigned the
            # access.force permission.
            #access.force = flatpak
        }
    }

    # Makes a factory for wrapping nodes in an adapter with a
    # converter and resampler.
    { name = libpipewire-module-adapter }

    # Makes a factory for creating links between ports.
    { name = libpipewire-module-link-factory }

    # Provides factories to make session manager objects.
    { name = libpipewire-module-session-manager }

    {   name = libpipewire-module-filter-chain
    args = {
        node.description =  "Noise Canceling source"
        media.name =  "Noise Canceling source"
        filter.graph = {
            nodes = [
                    {
                    type = ladspa
                    name = rnnoise
                    plugin = /home/rushvora/ladspa/librnnoise_ladspa.so
                    label = noise_suppressor_stereo
                    control = {
                        "VAD Threshold (%)" 50.0
                        }
                    }
                ]
            }
        capture.props = {
            node.name =  "capture.rnnoise_source"
            node.passive = true
            }
        playback.props = {
            node.name =  "rnnoise_source"
            media.class = Audio/Source
            }
        }
    }

    # Use libcanberra to play X11 Bell
    #{ name = libpipewire-module-x11-bell
    #  args = {
    #      #sink.name = ""
    #      #sample.name = "bell-window-system"
    #      #x11.display = null
    #      #x11.xauthority = null
    #  }
    #}
]

context.objects = [
    #{ factory = <factory-name>
    #    [ args  = { <key> = <value> ... } ]
    #    [ flags = [ [ nofail ] ]
    #}
    #
    # Creates an object from a PipeWire factory with the given parameters.
    # If nofail is given, errors are ignored (and no object is created).
    #
    #{ factory = spa-node-factory   args = { factory.name = videotestsrc node.name = videotestsrc Spa:Pod:Object:Param:Props:patternType = 1 } }
    #{ factory = spa-device-factory args = { factory.name = api.jack.device foo=bar } flags = [ nofail ] }
    #{ factory = spa-device-factory args = { factory.name = api.alsa.enum.udev } }
    #{ factory = spa-node-factory   args = { factory.name = api.alsa.seq.bridge node.name = Internal-MIDI-Bridge } }
    #{ factory = adapter            args = { factory.name = audiotestsrc node.name = my-test } }
    #{ factory = spa-node-factory   args = { factory.name = api.vulkan.compute.source node.name = my-compute-source } }

    # A default dummy driver. This handles nodes marked with the "node.always-driver"
    # property when no other driver is currently active. JACK clients need this.
    { factory = spa-node-factory
        args = {
            factory.name    = support.node.driver
            node.name       = Dummy-Driver
            node.group      = pipewire.dummy
            priority.driver = 20000
        }
    }
    { factory = spa-node-factory
        args = {
            factory.name    = support.node.driver
            node.name       = Freewheel-Driver
            priority.driver = 19000
            node.group      = pipewire.freewheel
            node.freewheel  = true
        }
    }
    # This creates a new Source node. It will have input ports
    # that you can link, to provide audio for this source.
    #{ factory = adapter
    #    args = {
    #        factory.name     = support.null-audio-sink
    #        node.name        = "my-mic"
    #        node.description = "Microphone"
    #        media.class      = "Audio/Source/Virtual"
    #        audio.position   = "FL,FR"
    #    }
    #}

    # This creates a single PCM source device for the given
    # alsa device path hw:0. You can change source to sink
    # to make a sink in the same way.
    #{ factory = adapter
    #    args = {
    #        factory.name           = api.alsa.pcm.source
    #        node.name              = "alsa-source"
    #        node.description       = "PCM Source"
    #        media.class            = "Audio/Source"
    #        api.alsa.path          = "hw:0"
    #        api.alsa.period-size   = 1024
    #        api.alsa.headroom      = 0
    #        api.alsa.disable-mmap  = false
    #        api.alsa.disable-batch = false
    #        audio.format           = "S16LE"
    #        audio.rate             = 48000
    #        audio.channels         = 2
    #        audio.position         = "FL,FR"
    #    }
    #}
]

context.exec = [
    #{ path = <program-name> [ args = "<arguments>" ] }
    #
    # Execute the given program with arguments.
    #
    # You can optionally start the session manager here,
    # but it is better to start it as a systemd service.
    # Run the session manager with -h for options.
    #
    #{ path = "/usr/bin/pipewire-media-session" args = "" }
    #
    # You can optionally start the pulseaudio-server here as well
    # but it is better to start it as a systemd service.
    # It can be interesting to start another daemon here that listens
    # on another address with the -a option (eg. -a tcp:4713).
    #
    #{ path = "/usr/bin/pipewire" args = "-c pipewire-pulse.conf" }
]

Picture of the audio settings

image

Anyone can help with figuring out why the crackling is happening, and how I can fix it? Is the noise suppression plugin affecting the output as well?

werman commented 2 years ago

Hmm, maybe it forces 44000 sample rate? Try changing config to:

    default.clock.rate          = 48000
    default.clock.allowed-rates = [ 48000 ]
rushvora commented 2 years ago

I uncommented those lines and changed it to the below

    default.clock.rate          = 48000
    default.clock.allowed-rates = [ 44100 48000 ]

It seemed to have helped, there is still crackling that comes rarely in Discord, but Spotify and other audio seems fine. Let me know if you have any other ideas, I'm closing it for now since it's not as big of an issue now.

werman commented 2 years ago

Great, I think I was optimistic in readme saying that 44100 should work. The sample rate should be forced to 48000 (there is no really point in using 44100 unless you listen to CDs, and even then...). There is also a matter of buffer size - pipewire uses 512 or other powers-of-two, which current version of plugin may not handle gracefully.

Anyway, I'll update the plugin soon to correctly handle different buffer sizes and would update readme to state that 48000 sample rate should be forced.