diff --git a/framework/13-inch/11th-gen-intel/default.nix b/framework/13-inch/11th-gen-intel/default.nix index a29966f..2ea3396 100644 --- a/framework/13-inch/11th-gen-intel/default.nix +++ b/framework/13-inch/11th-gen-intel/default.nix @@ -7,4 +7,6 @@ # Requires at least 5.16 for working wi-fi and bluetooth. # https://community.frame.work/t/using-the-ax210-with-linux-on-the-framework-laptop/1844/89 boot.kernelPackages = lib.mkIf (lib.versionOlder pkgs.linux.version "5.16") (lib.mkDefault pkgs.linuxPackages_latest); + + hardware.framework.laptop13.audioEnhancement.rawDeviceName = lib.mkDefault "alsa_output.pci-0000_00_1f.3.analog-stereo"; } diff --git a/framework/13-inch/12th-gen-intel/default.nix b/framework/13-inch/12th-gen-intel/default.nix index e2fdcfd..4cce444 100644 --- a/framework/13-inch/12th-gen-intel/default.nix +++ b/framework/13-inch/12th-gen-intel/default.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, ... }: { imports = [ ../common @@ -8,17 +8,19 @@ config = lib.mkMerge [ { hardware.intelgpu.loadInInitrd = lib.versionOlder config.boot.kernelPackages.kernel.version "6.2"; + # same as 13th gen framework 13-inch + hardware.framework.laptop13.audioEnhancement.rawDeviceName = lib.mkDefault "alsa_output.pci-0000_00_1f.3.analog-stereo"; } # https://community.frame.work/t/tracking-hard-freezing-on-fedora-36-with-the-new-12th-gen-system/20675/391 (lib.mkIf (lib.versionOlder config.boot.kernelPackages.kernel.version "6.2") { - boot.kernelParams = [ + boot.kernelParams = [ # Workaround iGPU hangs # https://discourse.nixos.org/t/intel-12th-gen-igpu-freezes/21768/4 - "i915.enable_psr=1" + "i915.enable_psr=1" ]; }) (lib.mkIf (lib.versionOlder config.boot.kernelPackages.kernel.version "6.8") { - boot.blacklistedKernelModules = [ + boot.blacklistedKernelModules = [ # This enables the brightness and airplane mode keys to work # https://community.frame.work/t/12th-gen-not-sending-xf86monbrightnessup-down/20605/11 "hid-sensor-hub" @@ -27,7 +29,7 @@ (lib.mkIf (config.hardware.framework.enableKmod == false) "cros_ec_lpcs") ]; - boot.kernelParams = [ + boot.kernelParams = [ # For Power consumption # https://kvark.github.io/linux/framework/2021/10/17/framework-nixos.html # Update 04/2024: Combined with acpi_osi from framework-intel it increases the idle power-usage in my test (SebTM) diff --git a/framework/13-inch/7040-amd/default.nix b/framework/13-inch/7040-amd/default.nix index 6841d68..9ec5e72 100644 --- a/framework/13-inch/7040-amd/default.nix +++ b/framework/13-inch/7040-amd/default.nix @@ -32,5 +32,7 @@ in # https://community.frame.work/t/tracking-framework-amd-ryzen-7040-series-lid-wakeup-behavior-feedback/39128/45 ACTION=="add", SUBSYSTEM=="serio", DRIVERS=="atkbd", ATTR{power/wakeup}="disabled" ''; + + hardware.framework.laptop13.audioEnhancement.rawDeviceName = lib.mkDefault "alsa_output.pci-0000_c1_00.6.analog-stereo"; }; } diff --git a/framework/13-inch/common/audio.nix b/framework/13-inch/common/audio.nix new file mode 100644 index 0000000..b6aa6d6 --- /dev/null +++ b/framework/13-inch/common/audio.nix @@ -0,0 +1,382 @@ +{ 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; + }); +} diff --git a/framework/13-inch/common/default.nix b/framework/13-inch/common/default.nix index f9c7808..5d228d0 100644 --- a/framework/13-inch/common/default.nix +++ b/framework/13-inch/common/default.nix @@ -5,6 +5,7 @@ ../../bluetooth.nix ../../kmod.nix ../../framework-tool.nix + ./audio.nix ]; # Fix TRRS headphones missing a mic