{ pkgs, config, lib, ... }: with lib; let cfg = config.xyno.desktop.audio; json = pkgs.formats.json { }; toFloat = builtins.fromJSON; readFilterTxt = text: let lines = splitString "\n" text; consumeLine = line: let parts = splitString " " line; content = foldl ( props: str: if props.lastNode == "Filter" then props // { nr = toInt (removeSuffix ":" str); lastNode = str; } else if props.lastNode == "Preamp:" then props // { nr = 0; gain = toFloat str; label = "HSC"; lastNode = str; freq = 0; q = 1.0; } else if props.lastNode == "ON" then props // { label = str; lastNode = str; } else if props.lastNode == "Fc" then props // { freq = toInt str; lastNode = str; } else if props.lastNode == "Gain" then props // { gain = toFloat str; lastNode = str; } else if props.lastNode == "Q" then props // { q = toFloat str; lastNode = str; } else props // { lastNode = str; } ) { lastNode = null; } parts; in content; in filter (x: x ? nr) (map consumeLine lines); genFilterGraph = filters: { nodes = ( map (filter: { type = "builtin"; name = "eq_band_${toString (filter.nr + 1)}"; label = if filter.label == "HSC" then "bq_highshelf" else if filter.label == "LSC" then "bq_lowshelf" else "bq_peaking"; control = { Freq = filter.freq; Q = filter.q; Gain = filter.gain; }; }) filters ); links = map (x: { output = "eq_band_${toString x}:Out"; input = "eq_band_${toString (x + 1)}:In"; }) (range 1 ((builtins.length filters) - 1)); }; genfilterChain = cfg': json.generate "filter-chain.json" ( recursiveUpdate { "node.description" = cfg'.displayName; "media.name" = cfg'.displayName; "filter.graph" = genFilterGraph (readFilterTxt cfg'.filterText); "audio.channels" = 2; "audio.position" = [ "FL" "FR" ]; "capture.props" = { "node.name" = "effect_input.${cfg'.internalName}"; "media.class" = "Audio/Sink"; }; "playback.props" = { "node.name" = "effect_output.${cfg'.internalName}"; "node.passive" = true; "target.object" = cfg'.hwAddr; }; } cfg'.extraConfig ); eqPkg = cfg': (pkgs.writeTextDir "share/wireplumber/wireplumber.conf.d/99-${cfg'.internalName}.conf" '' node.software-dsp.rules = [ { matches = [{ node.name = "${cfg'.hwAddr}" }] actions = { create-filter = { filter-path = "${genfilterChain cfg'}" hide-parent = ${boolToString cfg'.hideParent} } } } ] wireplumber.profiles = { main = { node.software-dsp = "required" } } '') // { passthru.requiredLv2Packages = with pkgs; [ lsp-plugins bankstown-lv2 ]; }; eqSubmodule = { name, ... }: { options = { internalName = mkOption { type = types.str; default = name; internal = true; }; displayName = mkOption { type = types.str; }; hwAddr = mkOption { type = types.str; }; filterText = mkOption { type = types.str; }; hideParent = mkOption { type = types.bool; default = true; }; extraConfig = mkOption { type = types.attrsOf types.anything; default = { }; }; }; }; in { options.xyno.desktop.audio.enable = mkEnableOption "enable pipewire and stuff"; options.xyno.desktop.audio.eq = mkOption { type = types.attrsOf (types.submodule eqSubmodule); default = { }; }; config = mkIf cfg.enable { # https://github.com/jaakkopasanen/AutoEq/blob/master/results/oratory1990/in-ear/Shure%20SE215/Shure%20SE215%20ParametricEQ.txt xyno.desktop.audio.eq.se215 = { displayName = "Shure SE215"; hwAddr = "alsa_output.usb-Synaptics_CX31993_384Khz_HIFI_AUDIO-00.analog-stereo"; # hideParent = false; extraConfig."capture.props"."device.icon-name" = "audio-headphones"; filterText = '' Preamp: -5.7 dB Filter 1: ON LSC Fc 105 Hz Gain 0.3 dB Q 0.70 Filter 2: ON PK Fc 174 Hz Gain -4.6 dB Q 0.68 Filter 3: ON PK Fc 811 Hz Gain 3.1 dB Q 1.42 Filter 4: ON PK Fc 3430 Hz Gain 3.8 dB Q 4.37 Filter 5: ON PK Fc 6943 Hz Gain 5.4 dB Q 2.76 Filter 6: ON HSC Fc 10000 Hz Gain 3.8 dB Q 0.70 Filter 7: ON PK Fc 5725 Hz Gain 8.1 dB Q 1.13 Filter 8: ON PK Fc 5056 Hz Gain -10.9 dB Q 2.93 Filter 9: ON PK Fc 7269 Hz Gain -6.3 dB Q 3.47 Filter 10: ON PK Fc 2278 Hz Gain -2.1 dB Q 2.94 ''; }; services.pipewire = { enable = true; # raopOpenFirewall = true; # airplay pulse.enable = true; extraConfig.pipewire = { "9-clock-allow-higher" = { "context.properties" = { "default.clock.allowed-rates" = [ "44100" "48000" "88200" "96000" "176400" "192000" ]; }; }; # "10-raop-discover" = { # "context.modules" = [ # { # name = "libpipewire-module-raop-discover"; # args = { }; # } # ]; # }; }; extraConfig.pipewire-pulse = { "10-zeroconf" = { "pulse.cmd" = [ { cmd = "load-module"; args = "module-zeroconf-discover"; } ]; }; }; wireplumber.extraConfig."98-bluetooth"."wireplumber.settings"."bluetooth.autoswitch-to-headset-profile" = false; wireplumber.configPackages = mapAttrsToList (n: v: eqPkg v) cfg.eq; }; }; }