Wheee it works

Signed-off-by: Hector Martin <marcan@marcan.st>
This commit is contained in:
Hector Martin 2023-02-24 01:17:41 +09:00
parent 1bb490871f
commit 21b3e49456
6 changed files with 983 additions and 291 deletions

472
Cargo.lock generated
View file

@ -22,23 +22,107 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0b0588d44d4d63a87dbd75c136c166bbfd9a86a31cb89e09906521c7d3f5e3"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex",
"is-terminal",
"once_cell",
"strsim",
"termcolor",
]
[[package]]
name = "clap-verbosity-flag"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e2b6c3dcdb73299f48ae05b294da14e2f560b3ed2c09e742269eb1b22af231"
dependencies = [
"clap",
"log",
]
[[package]]
name = "clap_derive"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "colored"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd"
dependencies = [
"atty",
"lazy_static",
"winapi",
]
[[package]]
name = "configparser"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
dependencies = [
"indexmap",
]
[[package]]
name = "crunchy"
@ -46,6 +130,27 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "half"
version = "2.1.0"
@ -55,12 +160,98 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "io-lifetimes"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3"
dependencies = [
"libc",
"windows-sys 0.45.0",
]
[[package]]
name = "is-terminal"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0a45d56fe973d6db23972bf5bc46f988a4a2385deac9cc29572f09daef"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.45.0",
]
[[package]]
name = "itoa"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "nix"
version = "0.24.2"
@ -72,6 +263,27 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
name = "os_str_bytes"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
name = "pkg-config"
version = "0.3.26"
@ -79,10 +291,268 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "speakersafety-2"
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustix"
version = "0.36.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644"
dependencies = [
"bitflags",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.45.0",
]
[[package]]
name = "serde"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
[[package]]
name = "simple_logger"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e190a521c2044948158666916d9e872cbb9984f755e9bb3b5b75a836205affcd"
dependencies = [
"atty",
"colored",
"log",
"time",
"windows-sys 0.42.0",
]
[[package]]
name = "speakersafetyd"
version = "0.1.0"
dependencies = [
"alsa",
"clap",
"clap-verbosity-flag",
"configparser",
"half",
"log",
"simple_logger",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56e159d99e6c2b93995d171050271edb50ecc5288fbc7cc17de8fdce4e58c14"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
dependencies = [
"winapi-util",
]
[[package]]
name = "time"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2"
dependencies = [
"itoa",
"libc",
"num_threads",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c"
dependencies = [
"time-core",
]
[[package]]
name = "unicode-ident"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"

View file

@ -1,5 +1,5 @@
[package]
name = "speakersafety-2"
name = "speakersafetyd"
version = "0.1.0"
edition = "2021"
@ -8,4 +8,8 @@ edition = "2021"
[dependencies]
half = "^2.1.0"
alsa = { path = "./alsa" }
configparser = "^3.0.2"
configparser = { version = "^3.0.2", features=["indexmap"] }
clap = { version = "4.1.6", features=["derive"] }
log = "0.4.17"
clap-verbosity-flag = "2.0.0"
simple_logger = "4.0.0"

141
j314.conf
View file

@ -1,71 +1,92 @@
[Left Tweeter]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 1
[Globals]
visense_pcm = 2
t_ambient = 50.0
t_safe_max = 100
t_hysteresis = 10
channels = 12
period = 4096
[Speaker/Left Woofer 1]
group = 1
tr_coil = 28.09
tr_magnet = 34.43
tau_coil = 3.05
tau_magnet = 192.45
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 0
vs_chan = 1
[Right Tweeter]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 3
[Speaker/Right Woofer 1]
group = 1
tr_coil = 28.09
tr_magnet = 34.43
tau_coil = 3.05
tau_magnet = 192.45
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 2
vs_chan = 3
[Left Woofer 1]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 5
[Speaker/Left Tweeter]
group = 0
tr_coil = 34.5
tr_magnet = 48.2
tau_coil = 2.31
tau_magnet = 61.4
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 4
vs_chan = 5
[Right Woofer 1]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 7
[Speaker/Right Tweeter]
group = 0
tr_coil = 34.5
tr_magnet = 48.2
tau_coil = 2.31
tau_magnet = 61.4
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 6
vs_chan = 7
[Left Woofer 2]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 9
[Speaker/Left Woofer 2]
group = 1
tr_coil = 28.09
tr_magnet = 34.43
tau_coil = 3.05
tau_magnet = 192.45
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 8
vs_chan = 9
[Right Woofer 2]
r_shunt = 1
r_dc = 2
r_amp = 3
tau_coil = 4
tau_magnet = 5
tr_coil = 6
ramp_factor = 7
temp_limit = 100
vs_chan = 11
[Speaker/Right Woofer 2]
group = 1
tr_coil = 28.09
tr_magnet = 34.43
tau_coil = 3.05
tau_magnet = 192.45
t_limit = 140.0
t_headroom = 10.0
z_nominal = 3.2
is_scale = 3.75
vs_scale = 14
is_chan = 10
vs_chan = 11

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors
use configparser::ini::Ini;
use alsa::mixer::MilliBel;
use alsa;
use alsa::mixer::MilliBel;
use configparser::ini::Ini;
/**
Failsafe: Limit speaker volume massively and bail.
@ -34,20 +34,21 @@ pub fn open_card(card: &str) -> alsa::ctl::Ctl {
println!("{}: Could not open sound card! Error: {}", card, e);
fail();
std::process::exit(1);
},
}
};
return ctldev;
}
pub fn open_pcm(dev: &str, chans: &u32) -> alsa::pcm::PCM {
let pcm = alsa::pcm::PCM::new(dev, alsa::Direction::Capture, false)
.unwrap();
pub fn open_pcm(dev: &str, chans: u32, sample_rate: u32) -> alsa::pcm::PCM {
let pcm = alsa::pcm::PCM::new(dev, alsa::Direction::Capture, false).unwrap();
{
let params = alsa::pcm::HwParams::any(&pcm).unwrap();
params.set_channels(*chans).unwrap();
params.set_rate(48000, alsa::ValueOr::Nearest).unwrap();
params.set_channels(chans).unwrap();
params
.set_rate(sample_rate, alsa::ValueOr::Nearest)
.unwrap();
params.set_format(alsa::pcm::Format::s16()).unwrap();
params.set_access(alsa::pcm::Access::RWInterleaved).unwrap();
pcm.hw_params(&params).unwrap();
@ -58,29 +59,19 @@ pub fn open_pcm(dev: &str, chans: &u32) -> alsa::pcm::PCM {
/**
Wrapper around configparser::ini::Ini.getint()
to safely unwrap the Result<Option<f64>, E> returned by
to safely unwrap the Result<Option<i64>, E> returned by
it.
*/
pub fn parse_int(config: &Ini, section: &str, key: &str) -> i64 {
let _result: Option<i64> = match config.getint(section, key) {
Ok(result) => match result{
Some(inner) => {
let integer: i64 = inner;
return integer;
},
None => {
println!("{}: Failed to parse {}", section, key);
fail();
std::process::exit(1);
},
},
Err(e) => {
println!("{}: Invalid value for {}. Error: {}", section, key, e);
fail();
std::process::exit(1);
},
};
pub fn parse_int<T: TryFrom<i64>>(config: &Ini, section: &str, key: &str) -> T
where
<T as TryFrom<i64>>::Error: std::fmt::Debug,
{
config
.getint(section, key)
.expect(&format!("{}/{}: Invalid value", section, key))
.expect(&format!("{}/{}: Missing key", section, key))
.try_into()
.expect("{}/{}: Out of bounds")
}
/**
@ -89,25 +80,13 @@ pub fn parse_int(config: &Ini, section: &str, key: &str) -> i64 {
it.
*/
pub fn parse_float(config: &Ini, section: &str, key: &str) -> f32 {
let _result: Option<f64> = match config.getfloat(section, key) {
Ok(result) => match result{
Some(inner) => {
let float: f32 = inner as f32;
return float;
},
None => {
println!("{}: Failed to parse {}", section, key);
fail();
std::process::exit(1);
},
},
Err(e) => {
println!("{}: Invalid value for {}. Error: {}", section, key, e);
fail();
std::process::exit(1);
},
};
let val = config
.getfloat(section, key)
.expect(&format!("{}/{}: Invalid value", section, key))
.expect(&format!("{}/{}: Missing key", section, key)) as f32;
assert!(val.is_finite());
val
}
/**
@ -121,24 +100,27 @@ pub fn new_elemvalue(t: alsa::ctl::ElemType) -> alsa::ctl::ElemValue {
println!("Could not open a handle to an element!");
fail();
std::process::exit(1);
},
}
};
return val;
}
/**
Wrapper for alsa::ctl::Ctl::elem_read().
*/
pub fn read_ev(card: &alsa::ctl::Ctl, ev: &mut alsa::ctl::ElemValue, name: &str) {
let _val = match card.elem_read(ev) { // alsa:Result<()>
let _val = match card.elem_read(ev) {
// alsa:Result<()>
Ok(val) => val,
Err(e) => {
println!("Could not read elem value {}. alsa-lib error: {:?}", name, e);
println!(
"Could not read elem value {}. alsa-lib error: {:?}",
name, e
);
fail();
std::process::exit(1);
},
}
};
}
@ -146,13 +128,17 @@ pub fn read_ev(card: &alsa::ctl::Ctl, ev: &mut alsa::ctl::ElemValue, name: &str)
Wrapper for alsa::ctl::Ctl::elem_write().
*/
pub fn write_ev(card: &alsa::ctl::Ctl, ev: &alsa::ctl::ElemValue, name: &str) {
let _val = match card.elem_write(ev) { // alsa:Result<()>
let _val = match card.elem_write(ev) {
// alsa:Result<()>
Ok(val) => val,
Err(e) => {
println!("Could not write elem value {}. alsa-lib error: {:?}", name, e);
println!(
"Could not write elem value {}. alsa-lib error: {:?}",
name, e
);
fail();
std::process::exit(1);
},
}
};
}
@ -160,10 +146,13 @@ pub fn int_to_db(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: i32) -> Mil
let db = match card.convert_to_db(id, val.into()) {
Ok(inner) => inner,
Err(e) => {
println!("Could not convert val {} to dB! alsa-lib error: {:?}", val, e);
println!(
"Could not convert val {} to dB! alsa-lib error: {:?}",
val, e
);
fail();
std::process::exit(1);
},
}
};
return db;
@ -174,10 +163,13 @@ pub fn db_to_int(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: f32) -> i32
let new_int = match card.convert_from_db(id, mb, alsa::Round::Floor) {
Ok(inner) => inner as i32,
Err(e) => {
println!("Could not convert MilliBel {:?} to int! alsa-lib error: {:?}", val, e);
println!(
"Could not convert MilliBel {:?} to int! alsa-lib error: {:?}",
val, e
);
fail();
std::process::exit(1);
},
}
};
return new_int;

View file

@ -1,88 +1,172 @@
// SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors
/**
/*!
Handles speaker safety on Apple Silicon machines. This code is designed to
fail safe. The speaker should not be enabled until this daemon has successfully
initialised. If at any time we run into an unrecoverable error (we shouldn't),
we gracefully bail and use an IOCTL to shut off the speakers.
*/
use std::io;
use std::collections::BTreeMap;
use std::fs::read_to_string;
use std::io;
use std::path::PathBuf;
use std::{thread::sleep, time};
use clap::Parser;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use configparser::ini::Ini;
use log;
use log::{debug, error, info, trace, warn};
use simple_logger::SimpleLogger;
mod types;
mod helpers;
mod types;
use crate::types::SafetyMonitor;
static VERSION: &str = "0.0.1";
static ASAHI_DEVICE: &str = "hw:0";
static VISENSE_PCM: &str = "hw:0,2";
const DEFAULT_CONFIG_PATH: &str = "share/speakersafetyd";
// Will eventually be /etc/speakersafetyd/ or similar
static CONFIG_DIR: &str = "./";
static SUPPORTED: [&str; 1] = [
"j314",
];
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Options {
/// Path to the configuration file base directory
#[arg(short, long)]
config_path: Option<PathBuf>,
const BUF_SZ: usize = 128 * 6 * 2;
/// Increase the log level
#[command(flatten)]
verbose: Verbosity<InfoLevel>,
}
fn get_machine() -> String {
let _compat: io::Result<String> = match read_to_string("/proc/device-tree/compatible") {
Ok(compat) => {
return compat[6..10].to_string();
},
Err(e) => {
println!("Could not read devicetree compatible: {}", e);
std::process::exit(1);
read_to_string("/proc/device-tree/compatible")
.expect("Could not read device tree compatible")
.strip_prefix("apple,")
.expect("Unexpected compatible format")
.split_once("\0")
.expect("Unexpected compatible format")
.0
.trim_end_matches(|c: char| c.is_ascii_alphabetic())
.to_string()
}
fn get_speakers(config: &Ini) -> Vec<String> {
config
.sections()
.iter()
.filter_map(|a| a.strip_prefix("Speaker/"))
.map(|a| a.to_string())
.collect()
}
struct SpeakerGroup {
speakers: Vec<types::Speaker>,
gain: f32,
}
impl Default for SpeakerGroup {
fn default() -> Self {
Self {
speakers: Default::default(),
gain: f32::NAN,
}
}
};
}
fn get_drivers(config: &Ini) -> Vec<String> {
let drivers = config.sections();
return drivers;
}
fn main() {
let args = Options::parse();
SimpleLogger::new()
.with_level(args.verbose.log_level_filter())
.without_timestamps()
.init()
.unwrap();
info!("Starting up");
let mut config_path = args.config_path.unwrap_or_else(|| {
let mut path = PathBuf::new();
path.push(option_env!("PREFIX").unwrap_or("/usr/local"));
path.push(DEFAULT_CONFIG_PATH);
path
});
info!("Config base: {:?}", config_path);
let model: String = get_machine();
info!("Model: {}", model);
config_path.push(&model);
config_path.set_extension("conf");
info!("Config file: {:?}", config_path);
let device = format!("hw:{}", model.to_ascii_uppercase());
info!("Device: {}", device);
let mut cfg: Ini = Ini::new_cs();
let mut speakers: Vec<types::Speaker> = Vec::new();
let card: alsa::ctl::Ctl = helpers::open_card(&ASAHI_DEVICE);
cfg.load(config_path).expect("Failed to read config file");
if SUPPORTED.contains(&model.as_str()) {
cfg.load(CONFIG_DIR.to_owned() + &model + ".conf").unwrap();
} else {
println!("Unsupported machine {}", model);
std::process::exit(1);
let globals = types::Globals::parse(&cfg);
let speaker_names = get_speakers(&cfg);
let speaker_count = speaker_names.len();
info!("Found {} speakers", speaker_count);
info!("Opening control device");
let ctl: alsa::ctl::Ctl = helpers::open_card(&device);
let mut groups: BTreeMap<usize, SpeakerGroup> = BTreeMap::new();
for i in speaker_names {
let speaker: types::Speaker = types::Speaker::new(&globals, &i, &cfg, &ctl);
groups
.entry(speaker.group)
.or_default()
.speakers
.push(speaker);
}
let list_drivers = get_drivers(&cfg);
for i in list_drivers {
let new_speaker: types::Speaker = types::SafetyMonitor::new(&i, &cfg, &card);
speakers.push(new_speaker);
}
let num_chans: u32 = speakers.len().try_into().unwrap();
assert!(
groups
.values()
.map(|a| a.speakers.len())
.fold(0, |a, b| a + b)
== speaker_count
);
assert!(2 * speaker_count <= globals.channels);
let pcm_name = format!("{},{}", device, globals.visense_pcm);
// Set up PCM to buffer in V/ISENSE
let cap: alsa::pcm::PCM = helpers::open_pcm(&VISENSE_PCM, &num_chans);
let mut buf = [0i16; BUF_SZ]; // 128 samples from V and I for 6 channels
let io = cap.io_i16().unwrap();
let pcm: alsa::pcm::PCM =
helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 48000);
let mut buf = Vec::new();
buf.resize(globals.period * globals.channels, 0i16);
let io = pcm.io_i16().unwrap();
let hwp = pcm.hw_params_current().unwrap();
let sample_rate = hwp.get_rate().unwrap();
info!("Sample rate: {}", sample_rate);
loop {
// Block while we're reading into the buffer
io.readi(&mut buf).unwrap();
for i in &mut speakers {
i.run(&card, &buf);
for (idx, group) in groups.iter_mut() {
let gain = group
.speakers
.iter_mut()
.map(|s| s.run_model(&buf, sample_rate as f32))
.reduce(f32::min)
.unwrap();
if gain != group.gain {
if group.gain == 0. {
warn!("Speaker group {} gain limited to {}", idx, gain);
}
group.speakers.iter_mut().for_each(|s| s.update(&ctl, gain));
group.gain = gain;
}
}
buf = [0i16; BUF_SZ];
}
}

View file

@ -1,9 +1,10 @@
// SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors
use std::ffi::{CString, CStr};
use configparser::ini::Ini;
use alsa::ctl::Ctl;
use configparser::ini::Ini;
use log::{debug, error, info, trace, warn};
use std::ffi::{CStr, CString};
use crate::helpers;
@ -19,21 +20,19 @@ struct Elem {
val: alsa::ctl::ElemValue,
}
trait ALSAElem {
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Self;
}
impl ALSAElem for Elem {
impl Elem {
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Elem {
// CString::new() cannot borrow a String. We want name for the elem
// for error identification though, so it can't consume name directly.
let borrow: String = name.clone();
let mut new_elem: Elem = { Elem {
let mut new_elem: Elem = {
Elem {
elem_name: name,
id: alsa::ctl::ElemId::new(alsa::ctl::ElemIface::Mixer),
val: helpers::new_elemvalue(t),
}};
}
};
let cname: CString = CString::new(borrow).unwrap();
let cstr: &CStr = cname.as_c_str();
@ -58,68 +57,111 @@ impl ALSAElem for Elem {
struct Mixer {
drv: String,
level: Elem,
vsense: Elem,
isense: Elem,
amp_gain: Elem,
}
trait ALSACtl {
fn new(name: &str, card: &Ctl) -> Self;
fn get_lvl(&mut self, card: &Ctl) -> f32;
fn set_lvl(&mut self, card: &Ctl, lvl: f32);
}
impl ALSACtl for Mixer {
impl Mixer {
// TODO: implement turning on V/ISENSE
fn new(name: &str, card: &Ctl) -> Mixer {
let new_mixer: Mixer = { Mixer {
drv: name.to_owned(),
level: ALSAElem::new(name.to_owned() + " Speaker Volume", card,
alsa::ctl::ElemType::Integer),
vsense: ALSAElem::new(name.to_owned() + " VSENSE Switch", card,
alsa::ctl::ElemType::Boolean),
isense: ALSAElem::new(name.to_owned() + " ISENSE Switch", card,
alsa::ctl::ElemType::Boolean),
}};
let mut vs = Elem::new(
name.to_owned() + " VSENSE Switch",
card,
alsa::ctl::ElemType::Boolean,
);
return new_mixer;
vs.val.set_boolean(0, true);
helpers::write_ev(card, &vs.val, &vs.elem_name);
helpers::read_ev(card, &mut vs.val, &vs.elem_name);
assert!(vs.val.get_boolean(0).unwrap());
let mut is = Elem::new(
name.to_owned() + " ISENSE Switch",
card,
alsa::ctl::ElemType::Boolean,
);
is.val.set_boolean(0, true);
helpers::write_ev(card, &is.val, &is.elem_name);
helpers::read_ev(card, &mut vs.val, &vs.elem_name);
assert!(vs.val.get_boolean(0).unwrap());
Mixer {
drv: name.to_owned(),
level: Elem::new(
name.to_owned() + " Speaker Volume",
card,
alsa::ctl::ElemType::Integer,
),
amp_gain: Elem::new(
name.to_owned() + " Amp Gain Volume",
card,
alsa::ctl::ElemType::Integer,
),
}
}
fn get_amp_gain(&mut self, card: &Ctl) -> f32 {
helpers::read_ev(card, &mut self.amp_gain.val, &self.amp_gain.elem_name);
let val = self
.amp_gain
.val
.get_integer(0)
.expect(&format!("Could not read amp gain for {}", self.drv));
helpers::int_to_db(card, &self.amp_gain.id, val).to_db()
}
fn get_lvl(&mut self, card: &Ctl) -> f32 {
helpers::read_ev(card, &mut self.level.val, &self.level.elem_name);
let val: i32 = match self.level.val.get_integer(0) {
Some(inner) => inner,
None => {
println!("Could not read level from {}", self.drv);
helpers::fail();
std::process::exit(1);
},
};
let val = self
.level
.val
.get_integer(0)
.expect(&format!("Could not read level for {}", self.drv));
let db: f32 = helpers::int_to_db(card, &self.level.id, val).to_db();
return db;
helpers::int_to_db(card, &self.level.id, val).to_db()
}
fn set_lvl(&mut self, card: &Ctl, lvl: f32) {
let new_val: i32 = helpers::db_to_int(card, &self.level.id, lvl);
match self.level.val.set_integer(0, new_val) {
Some(_) => {},
Some(_) => {}
None => {
println!("Could not set level for {}", self.drv);
helpers::fail();
std::process::exit(1);
},
}
};
helpers::write_ev(card, &self.level.val, &self.level.elem_name);
}
}
#[derive(Copy, Clone)]
pub struct Globals {
pub visense_pcm: usize,
pub channels: usize,
pub period: usize,
pub t_ambient: f32,
pub t_safe_max: f32,
pub t_hysteresis: f32,
}
impl Globals {
pub fn parse(config: &Ini) -> Self {
Self {
visense_pcm: helpers::parse_int(config, "Globals", "visense_pcm"),
channels: helpers::parse_int(config, "Globals", "channels"),
period: helpers::parse_int(config, "Globals", "period"),
t_ambient: helpers::parse_float(config, "Globals", "t_ambient"),
t_safe_max: helpers::parse_float(config, "Globals", "t_safe_max"),
t_hysteresis: helpers::parse_float(config, "Globals", "t_hysteresis"),
}
}
}
/**
Struct representing a driver. Parameters are parsed out of a config
@ -132,91 +174,170 @@ impl ALSACtl for Mixer {
tau_coil: voice coil ramp time constant (seconds)
tau_magnet: magnet ramp time constant (seconds)
tr_coil: thermal resistance of voice coil (*C/W)
temp_limit: absolute max temp of the voice coil (*C)
t_limit: absolute max temp of the voice coil (*C)
Borrows the handle to the control interface to do calculations.
*/
#[derive(Debug, Default)]
pub struct SpeakerState {
t_coil: f64,
t_magnet: f64,
t_coil_hyst: f32,
t_magnet_hyst: f32,
min_gain: f32,
gain: f32,
}
pub struct Speaker {
name: String,
pub name: String,
pub group: usize,
alsa_iface: Mixer,
tau_coil: f32,
tau_magnet: f32,
tr_coil: f32,
temp_limit: f32,
vs_chan: i64,
is_chan: i64,
tr_magnet: f32,
t_limit: f32,
t_headroom: f32,
z_nominal: f32,
is_scale: f32,
vs_scale: f32,
is_chan: usize,
vs_chan: usize,
g: Globals,
s: SpeakerState,
}
impl Speaker {
pub fn new(globals: &Globals, name: &str, config: &Ini, ctl: &Ctl) -> Speaker {
info!("Speaker [{}]:", name);
pub trait SafetyMonitor {
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Self;
fn power_now(&mut self, vs: &[i16], is: &[i16]) -> f32;
fn run(&mut self, card: &Ctl, buf: &[i16; 128 * 6 * 2]);
}
let section = "Speaker/".to_owned() + name;
let mut new_speaker: Speaker = Speaker {
name: name.to_string(),
alsa_iface: Mixer::new(&name, ctl),
group: helpers::parse_int(config, &section, "group"),
tau_coil: helpers::parse_float(config, &section, "tau_coil"),
tau_magnet: helpers::parse_float(config, &section, "tau_magnet"),
tr_coil: helpers::parse_float(config, &section, "tr_coil"),
tr_magnet: helpers::parse_float(config, &section, "tr_magnet"),
t_limit: helpers::parse_float(config, &section, "t_limit"),
t_headroom: helpers::parse_float(config, &section, "t_headroom"),
z_nominal: helpers::parse_float(config, &section, "z_nominal"),
is_scale: helpers::parse_float(config, &section, "is_scale"),
vs_scale: helpers::parse_float(config, &section, "vs_scale"),
is_chan: helpers::parse_int(config, &section, "is_chan"),
vs_chan: helpers::parse_int(config, &section, "vs_chan"),
g: *globals,
s: Default::default(),
};
impl SafetyMonitor for Speaker {
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Speaker {
let new_speaker: Speaker = { Speaker {
name: driver_name.to_string(),
alsa_iface: ALSACtl::new(&driver_name, card),
tau_coil: helpers::parse_float(config, driver_name, "tau_coil"),
tau_magnet: helpers::parse_float(config, driver_name, "tau_magnet"),
tr_coil: helpers::parse_float(config, driver_name, "tr_coil"),
temp_limit: helpers::parse_float(config, driver_name, "temp_limit"),
vs_chan: helpers::parse_int(config, driver_name, "vs_chan"),
is_chan: helpers::parse_int(config, driver_name, "is_chan"),
let s = &mut new_speaker.s;
}};
// Worst case startup assumption
s.t_coil = (new_speaker.t_limit - new_speaker.t_headroom) as f64;
s.t_magnet = s.t_coil
* (new_speaker.tr_magnet / (new_speaker.tr_magnet + new_speaker.tr_coil)) as f64;
return new_speaker;
// s.t_coil = globals.t_ambient as f64;
// s.t_magnet = globals.t_ambient as f64;
let max_dt = new_speaker.t_limit - new_speaker.t_headroom - globals.t_ambient;
let max_pwr = max_dt / (new_speaker.tr_magnet + new_speaker.tr_coil);
let amp_gain = new_speaker.alsa_iface.get_amp_gain(ctl);
// Worst-case peak power is 2x RMS power
let peak_pwr = 10f32.powf(amp_gain / 10.) / new_speaker.z_nominal * 2.;
s.min_gain = ((max_pwr / peak_pwr).log10() * 10.).min(0.);
assert!(new_speaker.is_chan < globals.channels);
assert!(new_speaker.vs_chan < globals.channels);
assert!(new_speaker.t_limit - new_speaker.t_headroom > globals.t_safe_max);
info!(" Group: {}", new_speaker.group);
info!(" Max temperature: {:.1} °C", new_speaker.t_limit);
info!(" Amp gain: {} dBV", amp_gain);
info!(" Max power: {:.2} W", max_pwr);
info!(" Peak power: {} W", peak_pwr);
info!(" Min gain: {:.2} dB", s.min_gain);
new_speaker
}
fn power_now(&mut self, vs: &[i16], is: &[i16]) -> f32 {
let v_avg: f32 = helpers::average(vs) * (14 / (2 ^ 15)) as f32;
let i_avg: f32 = helpers::average(is) * (3.75 / (2 ^ 15) as f32) as f32;
pub fn run_model(&mut self, buf: &[i16], sample_rate: f32) -> f32 {
let s = &mut self.s;
return v_avg * i_avg;
let step = 1. / sample_rate;
let alpha_coil = (step / (self.tau_coil + step)) as f64;
let alpha_magnet = (step / (self.tau_magnet + step)) as f64;
let mut pwr_sum = 0f32;
for sample in buf.chunks(self.g.channels) {
assert!(sample.len() == self.g.channels);
let v = sample[self.vs_chan] as f32 / 32768.0 * self.vs_scale;
let i = sample[self.is_chan] as f32 / 32768.0 * self.is_scale;
let p = v * i;
let t_coil_target = s.t_magnet + (p * self.tr_coil) as f64;
let t_magnet_target = (self.g.t_ambient + p * self.tr_magnet) as f64;
s.t_coil = t_coil_target * alpha_coil + s.t_coil * (1. - alpha_coil);
s.t_magnet = t_magnet_target * alpha_magnet + s.t_magnet * (1. - alpha_magnet);
if s.t_coil > self.t_limit as f64 {
panic!(
"{}: Coil temperature limit exceeded ({} > {})",
self.name, s.t_coil, self.t_limit
);
}
if s.t_magnet > self.t_limit as f64 {
panic!(
"{}: Magnet temperature limit exceeded ({} > {})",
self.name, s.t_magnet, self.t_limit
);
}
// I'm not sure on the maths here for determining when to start dropping the volume.
fn run(&mut self, card: &Ctl, buf: &[i16; 128 * 6 * 2]) {
let lvl: f32 = self.alsa_iface.get_lvl(card);
let vsense = &buf[(128 * self.vs_chan) as usize .. (128 * (self.vs_chan + 1) - 1) as usize];
let isense = &buf[(128 * self.is_chan) as usize .. (128 * (self.is_chan + 1) - 1) as usize];
// Estimate temperature of VC and magnet
let temp0: f32 = 35f32;
let mut temp_vc: f32 = temp0;
let mut temp_magnet: f32 = temp0;
let alpha_vc: f32 = 0.01 / (temp_vc + 0.01);
let alpha_magnet: f32 = 0.01 / (temp_magnet + 0.01);
// Power through the voice coil (average of most recent 128 samples)
let pwr: f32 = self.power_now(&vsense, &isense);
println!("Power now is {:.2} mW", pwr);
let vc_target: f32 = temp_magnet + pwr * self.tau_coil;
temp_vc = vc_target * alpha_vc + temp_vc * (1.0 - alpha_vc);
println!("Current voice coil temp: {:.2} *C", temp_vc);
let magnet_target: f32 = temp0 + pwr * self.tau_magnet;
temp_magnet = magnet_target * alpha_magnet + temp_magnet * (1.0 - alpha_magnet);
println!("Current magnet temp: {:.2} *C", temp_magnet);
if temp_vc < self.temp_limit {
println!("Voice coil for {} below temp limit, ramping back up.", self.name);
// For every degree below temp_limit, raise level by 0.5 dB
let new_lvl: f32 = lvl + ((self.temp_limit - temp_vc) * 0.5);
self.alsa_iface.set_lvl(card, new_lvl);
pwr_sum += p;
}
if temp_vc > (self.temp_limit - 15f32) {
println!("Voice coil at {}*C on {}! Dropping volume!", temp_vc, self.name);
// For every degree above temp_limit, drop the level by 1.5 dB
let new_lvl: f32 = lvl - ((temp_vc - (self.temp_limit - 15f32)) * 1.5);
self.alsa_iface.set_lvl(card, new_lvl);
let pwr_avg: f32 = pwr_sum / ((buf.len() / self.g.channels) as f32);
s.t_coil_hyst = s
.t_coil_hyst
.max(s.t_coil as f32)
.min(s.t_coil as f32 + self.g.t_hysteresis);
s.t_magnet_hyst = s
.t_magnet_hyst
.max(s.t_magnet as f32)
.min(s.t_magnet as f32 + self.g.t_hysteresis);
let temp = s.t_coil_hyst.max(s.t_magnet_hyst);
let reduction =
(temp - self.g.t_safe_max) / (self.t_limit - self.t_headroom - self.g.t_safe_max);
let gain = s.min_gain * reduction.max(0.);
s.gain = gain;
debug!(
"{}: Coil {:.2} °C Magnet {:.2} °C Power {:.2} W Gain {:.2} dB",
self.name, s.t_coil, s.t_magnet, pwr_avg, gain
);
if s.gain > -0.01 {
s.gain = 0.;
}
println!("Volume on {} is now {} dB", self.name, self.alsa_iface.get_lvl(card));
s.gain
}
pub fn update(&mut self, ctl: &Ctl, gain: f32) {
self.alsa_iface.set_lvl(ctl, gain);
}
}