mirror of
				https://github.com/NixOS/nixos-hardware.git
				synced 2025-11-04 17:27:14 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			383 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
{ config, lib, pkgs, ... }:
 | 
						|
let
 | 
						|
  cfg = config.hardware.framework.laptop13.audioEnhancement;
 | 
						|
in
 | 
						|
{
 | 
						|
  options = {
 | 
						|
    hardware.framework.laptop13.audioEnhancement = {
 | 
						|
      enable = lib.mkOption {
 | 
						|
        type = lib.types.bool;
 | 
						|
        default = false;
 | 
						|
        description = ''
 | 
						|
          Create a new audio device called "Framework Speakers",
 | 
						|
          which applies sound tuning before sending the audio out to the speakers.
 | 
						|
          This option requires PipeWire and WirePlumber.
 | 
						|
 | 
						|
          The filter chain includes the following:
 | 
						|
            - Pyschoacoustic bass enhancement
 | 
						|
            - Loudness compensation
 | 
						|
            - Equalizer
 | 
						|
            - Slight compression
 | 
						|
 | 
						|
          This option has been optimised for the Framework Laptop 13 AMD 7040 series, but should work on all models.
 | 
						|
 | 
						|
          Before applying, ensure the speakers are set to 100%,
 | 
						|
          because the volumes compound and the raw speaker device will be hidden by default.
 | 
						|
 | 
						|
          You might also need to re-select the default output device.
 | 
						|
 | 
						|
          In some cases, the added bass will vibrate the keyboard cable leading to a rattling sound,
 | 
						|
          a piece of foam can be used to mitigate this.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      hideRawDevice = lib.mkOption {
 | 
						|
        type = lib.types.bool;
 | 
						|
        default = true;
 | 
						|
        description = ''
 | 
						|
          Hide the raw speaker device.
 | 
						|
          This option is enabled by default, because keeping the raw speaker device can lead to volume conflicts.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
 | 
						|
      rawDeviceName = lib.mkOption {
 | 
						|
        type = lib.types.str;
 | 
						|
        example = "alsa_output.pci-0000_c1_00.6.analog-stereo";
 | 
						|
        description = ''
 | 
						|
          The name of the raw speaker device. This will vary by device.
 | 
						|
          You can get this by running `pw-dump | grep -C 20 pci-0000`.
 | 
						|
        '';
 | 
						|
      };
 | 
						|
    };
 | 
						|
  };
 | 
						|
 | 
						|
  config = lib.mkIf cfg.enable (let
 | 
						|
    outputName = cfg.rawDeviceName;
 | 
						|
    prettyName = "Framework Speakers";
 | 
						|
 | 
						|
    # These are pre-made decibel to linear value conversions, since Nix doesn't have pow().
 | 
						|
    # Use the formula `10 ** (db / 20)` to calculate.
 | 
						|
    db = {
 | 
						|
      "-18.1" = 0.1244514611771385;
 | 
						|
      "-5.48" = 0.5321082592667942;
 | 
						|
      "-4.76" = 0.5780960474057181;
 | 
						|
      "8.1" = 2.5409727055493048;
 | 
						|
      "-36" = 1.5848931924611134e-2;
 | 
						|
    };
 | 
						|
 | 
						|
    json = pkgs.formats.json { };
 | 
						|
 | 
						|
    # The filter chain, heavily inspired by the asahi-audio project: https://github.com/AsahiLinux/asahi-audio
 | 
						|
    filter-chain = json.generate "filter-chain.json" {
 | 
						|
      "node.description" = prettyName;
 | 
						|
      "media.name" = prettyName;
 | 
						|
      "filter.graph" = {
 | 
						|
        nodes = [
 | 
						|
          # Psychoacoustic bass extension,
 | 
						|
          # it creates harmonics of the missing bass to fool our ears into hearing it.
 | 
						|
          {
 | 
						|
            type = "lv2";
 | 
						|
            plugin = "https://chadmed.au/bankstown";
 | 
						|
            name = "bassex";
 | 
						|
            control = {
 | 
						|
              bypass = 0;
 | 
						|
              amt = 1.2;
 | 
						|
              sat_second = 1.3;
 | 
						|
              sat_third = 2.5;
 | 
						|
              blend = 1.0;
 | 
						|
              ceil = 200.0;
 | 
						|
              floor = 20.0;
 | 
						|
            };
 | 
						|
          }
 | 
						|
          # Loudness compensation,
 | 
						|
          # it ensures that the sound profile stays consistent across different volumes.
 | 
						|
          {
 | 
						|
            type = "lv2";
 | 
						|
            plugin = "http://lsp-plug.in/plugins/lv2/loud_comp_stereo";
 | 
						|
            name = "el";
 | 
						|
            control = {
 | 
						|
              enabled = 1;
 | 
						|
              input = 1.0;
 | 
						|
              fft = 4;
 | 
						|
            };
 | 
						|
          }
 | 
						|
          # 8-band equalizer,
 | 
						|
          # it tries to lessen frequencies where the laptop might resonate,
 | 
						|
          # and tries to make the frequency curve more pleasing;
 | 
						|
          # this is the "Lappy McTopface" profile (https://github.com/ceiphr/ee-framework-presets)
 | 
						|
          # further tuned for the Framework Laptop 13 AMD 7040 series
 | 
						|
          # and might need some tuning on other models.
 | 
						|
          {
 | 
						|
            type = "lv2";
 | 
						|
            plugin = "http://lsp-plug.in/plugins/lv2/para_equalizer_x8_lr";
 | 
						|
            name = "fw13eq";
 | 
						|
            control = {
 | 
						|
              mode = 0;
 | 
						|
              react = 0.2;
 | 
						|
              zoom = db."-36";
 | 
						|
 | 
						|
              fl_0 = 101.0;
 | 
						|
              fml_0 = 0;
 | 
						|
              ftl_0 = 5;
 | 
						|
              gl_0 = db."-18.1";
 | 
						|
              huel_0 = 0.0;
 | 
						|
              ql_0 = 4.36;
 | 
						|
              sl_0 = 0;
 | 
						|
              wl_0 = 4.0;
 | 
						|
 | 
						|
              fl_1 = 451.0;
 | 
						|
              fml_1 = 0;
 | 
						|
              ftl_1 = 1;
 | 
						|
              gl_1 = db."-5.48";
 | 
						|
              huel_1 = 3.125e-2;
 | 
						|
              ql_1 = 2.46;
 | 
						|
              sl_1 = 0;
 | 
						|
              wl_1 = 4.0;
 | 
						|
 | 
						|
              fl_2 = 918.0;
 | 
						|
              fml_2 = 0;
 | 
						|
              ftl_2 = 1;
 | 
						|
              gl_2 = db."-4.76";
 | 
						|
              huel_2 = 6.25e-2;
 | 
						|
              ql_2 = 2.44;
 | 
						|
              sl_2 = 0;
 | 
						|
              wl_2 = 4.0;
 | 
						|
 | 
						|
              fl_3 = 9700.0;
 | 
						|
              fml_3 = 0;
 | 
						|
              ftl_3 = 1;
 | 
						|
              gl_3 = db."8.1";
 | 
						|
              huel_3 = 9.375e-2;
 | 
						|
              ql_3 = 2.0;
 | 
						|
              sl_3 = 0;
 | 
						|
              wl_3 = 4.0;
 | 
						|
 | 
						|
              fr_0 = 101.0;
 | 
						|
              fmr_0 = 0;
 | 
						|
              ftr_0 = 5;
 | 
						|
              gr_0 = db."-18.1";
 | 
						|
              huer_0 = 0.0;
 | 
						|
              qr_0 = 4.36;
 | 
						|
              sr_0 = 0;
 | 
						|
              wr_0 = 4.0;
 | 
						|
 | 
						|
              fr_1 = 451.0;
 | 
						|
              fmr_1 = 0;
 | 
						|
              ftr_1 = 1;
 | 
						|
              gr_1 = db."-5.48";
 | 
						|
              huer_1 = 3.125e-2;
 | 
						|
              qr_1 = 2.46;
 | 
						|
              sr_1 = 0;
 | 
						|
              wr_1 = 4.0;
 | 
						|
 | 
						|
              fr_2 = 918.0;
 | 
						|
              fmr_2 = 0;
 | 
						|
              ftr_2 = 1;
 | 
						|
              gr_2 = db."-4.76";
 | 
						|
              huer_2 = 6.25e-2;
 | 
						|
              qr_2 = 2.44;
 | 
						|
              sr_2 = 0;
 | 
						|
              wr_2 = 4.0;
 | 
						|
 | 
						|
              fr_3 = 9700.0;
 | 
						|
              fmr_3 = 0;
 | 
						|
              ftr_3 = 1;
 | 
						|
              gr_3 = db."8.1";
 | 
						|
              huer_3 = 9.375e-2;
 | 
						|
              qr_3 = 2.0;
 | 
						|
              sr_3 = 0;
 | 
						|
              wr_3 = 4.0;
 | 
						|
            };
 | 
						|
          }
 | 
						|
          # Compressors. The settings were taken from the asahi-audio project.
 | 
						|
          {
 | 
						|
            type = "lv2";
 | 
						|
            plugin = "http://lsp-plug.in/plugins/lv2/mb_compressor_stereo";
 | 
						|
            name = "woofer_bp";
 | 
						|
            control = {
 | 
						|
              mode = 0;
 | 
						|
              ce_0 = 1;
 | 
						|
              sla_0 = 5.0;
 | 
						|
              cr_0 = 1.75;
 | 
						|
              al_0 = 0.725;
 | 
						|
              at_0 = 1.0;
 | 
						|
              rt_0 = 100;
 | 
						|
              kn_0 = 0.125;
 | 
						|
              cbe_1 = 1;
 | 
						|
              sf_1 = 200.0;
 | 
						|
              ce_1 = 0;
 | 
						|
              cbe_2 = 0;
 | 
						|
              ce_2 = 0;
 | 
						|
              cbe_3 = 0;
 | 
						|
              ce_3 = 0;
 | 
						|
              cbe_4 = 0;
 | 
						|
              ce_4 = 0;
 | 
						|
              cbe_5 = 0;
 | 
						|
              ce_5 = 0;
 | 
						|
              cbe_6 = 0;
 | 
						|
              ce_6 = 0;
 | 
						|
            };
 | 
						|
          }
 | 
						|
          {
 | 
						|
            type = "lv2";
 | 
						|
            plugin = "http://lsp-plug.in/plugins/lv2/compressor_stereo";
 | 
						|
            name = "woofer_lim";
 | 
						|
            control = {
 | 
						|
              sla = 5.0;
 | 
						|
              al = 1.0;
 | 
						|
              at = 1.0;
 | 
						|
              rt = 100.0;
 | 
						|
              cr = 15.0;
 | 
						|
              kn = 0.5;
 | 
						|
            };
 | 
						|
          }
 | 
						|
        ];
 | 
						|
 | 
						|
        # Now, we're chaining together the modules instantiated above.
 | 
						|
        links = [
 | 
						|
          {
 | 
						|
            output = "bassex:out_l";
 | 
						|
            input = "el:in_l";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "bassex:out_r";
 | 
						|
            input = "el:in_r";
 | 
						|
          }
 | 
						|
 | 
						|
          {
 | 
						|
            output = "el:out_l";
 | 
						|
            input = "fw13eq:in_l";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "el:out_r";
 | 
						|
            input = "fw13eq:in_r";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "fw13eq:out_l";
 | 
						|
            input = "woofer_bp:in_l";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "fw13eq:out_r";
 | 
						|
            input = "woofer_bp:in_r";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "woofer_bp:out_l";
 | 
						|
            input = "woofer_lim:in_l";
 | 
						|
          }
 | 
						|
          {
 | 
						|
            output = "woofer_bp:out_r";
 | 
						|
            input = "woofer_lim:in_r";
 | 
						|
          }
 | 
						|
        ];
 | 
						|
 | 
						|
        inputs = [
 | 
						|
          "bassex:in_l"
 | 
						|
          "bassex:in_r"
 | 
						|
        ];
 | 
						|
        outputs = [
 | 
						|
          "woofer_lim:out_l"
 | 
						|
          "woofer_lim:out_r"
 | 
						|
        ];
 | 
						|
 | 
						|
        # This makes pipewire's volume control actually control the loudness comp module
 | 
						|
        "capture.volumes" = [
 | 
						|
          {
 | 
						|
            control = "el:volume";
 | 
						|
            min = -47.5;
 | 
						|
            max = 0.0;
 | 
						|
            scale = "cubic";
 | 
						|
          }
 | 
						|
        ];
 | 
						|
      };
 | 
						|
      "capture.props" = {
 | 
						|
        "node.name" = "audio_effect.laptop-convolver";
 | 
						|
        "media.class" = "Audio/Sink";
 | 
						|
        "audio.channels" = "2";
 | 
						|
        "audio.position" = [
 | 
						|
          "FL"
 | 
						|
          "FR"
 | 
						|
        ];
 | 
						|
        "audio.allowed-rates" = [
 | 
						|
          44100
 | 
						|
          48000
 | 
						|
          88200
 | 
						|
          96000
 | 
						|
          176400
 | 
						|
          192000
 | 
						|
        ];
 | 
						|
        "device.api" = "dsp";
 | 
						|
        "node.virtual" = "false";
 | 
						|
 | 
						|
        # Lower seems to mean "more preferred",
 | 
						|
        # bluetooth devices seem to be ~1000, speakers seem to be ~2000
 | 
						|
        # since this is between the two, bluetooth devices take over when they connect,
 | 
						|
        # and hand over to this instead of the speakers when they disconnect.
 | 
						|
        "priority.session" = 1500;
 | 
						|
        "priority.driver" = 1500;
 | 
						|
        "state.default-volume" = 0.343;
 | 
						|
        "device.icon-name" = "audio-card-analog-pci";
 | 
						|
      };
 | 
						|
      "playback.props" = {
 | 
						|
        "node.name" = "audio_effect.laptop-convolver";
 | 
						|
        "target.object" = outputName;
 | 
						|
        "node.passive" = "true";
 | 
						|
        "audio.channels" = "2";
 | 
						|
        "audio.allowed-rates" = [
 | 
						|
          44100
 | 
						|
          48000
 | 
						|
          88200
 | 
						|
          96000
 | 
						|
          176400
 | 
						|
          192000
 | 
						|
        ];
 | 
						|
        "audio.position" = [
 | 
						|
          "FL"
 | 
						|
          "FR"
 | 
						|
        ];
 | 
						|
        "device.icon-name" = "audio-card-analog-pci";
 | 
						|
      };
 | 
						|
    };
 | 
						|
 | 
						|
    configPackage =
 | 
						|
      (pkgs.writeTextDir "share/wireplumber/wireplumber.conf.d/99-laptop.conf" ''
 | 
						|
        monitor.alsa.rules = [
 | 
						|
          {
 | 
						|
            matches = [{ node.name = "${outputName}" }]
 | 
						|
            actions = {
 | 
						|
              update-props = {
 | 
						|
                audio.allowed-rates = [44100, 48000, 88200, 96000, 176400, 192000]
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }
 | 
						|
        ]
 | 
						|
 | 
						|
        node.software-dsp.rules = [
 | 
						|
          {
 | 
						|
            matches = [{ node.name = "${outputName}" }]
 | 
						|
            actions = {
 | 
						|
              create-filter = {
 | 
						|
                filter-path = "${filter-chain}"
 | 
						|
                hide-parent = ${lib.boolToString cfg.hideRawDevice}
 | 
						|
              }
 | 
						|
            }
 | 
						|
          }
 | 
						|
        ]
 | 
						|
 | 
						|
        wireplumber.profiles = {
 | 
						|
          main = { node.software-dsp = "required" }
 | 
						|
        }
 | 
						|
      '')
 | 
						|
      // {
 | 
						|
        passthru.requiredLv2Packages = with pkgs; [
 | 
						|
          lsp-plugins
 | 
						|
          bankstown-lv2
 | 
						|
        ];
 | 
						|
      };
 | 
						|
  in {
 | 
						|
    services.pipewire.wireplumber.configPackages = [ configPackage ];
 | 
						|
 | 
						|
    # Pipewire is needed for this.
 | 
						|
    services.pipewire.enable = lib.mkDefault true;
 | 
						|
  });
 | 
						|
}
 |