1
0
Fork 0
mirror of https://github.com/NixOS/nixos-hardware synced 2024-12-29 13:09:45 +01:00
nixos-hardware/framework/13-inch/common/audio.nix

383 lines
11 KiB
Nix
Raw Normal View History

{ 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;
});
}