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", "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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "configparser" name = "configparser"
version = "3.0.2" version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a" checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a"
dependencies = [
"indexmap",
]
[[package]] [[package]]
name = "crunchy" name = "crunchy"
@ -46,6 +130,27 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 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]] [[package]]
name = "half" name = "half"
version = "2.1.0" version = "2.1.0"
@ -55,12 +160,98 @@ dependencies = [
"crunchy", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.137"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 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]] [[package]]
name = "nix" name = "nix"
version = "0.24.2" version = "0.24.2"
@ -72,6 +263,27 @@ dependencies = [
"libc", "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]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.26" version = "0.3.26"
@ -79,10 +291,268 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]] [[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" version = "0.1.0"
dependencies = [ dependencies = [
"alsa", "alsa",
"clap",
"clap-verbosity-flag",
"configparser", "configparser",
"half", "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] [package]
name = "speakersafety-2" name = "speakersafetyd"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
@ -8,4 +8,8 @@ edition = "2021"
[dependencies] [dependencies]
half = "^2.1.0" half = "^2.1.0"
alsa = { path = "./alsa" } 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] [Globals]
r_shunt = 1 visense_pcm = 2
r_dc = 2 t_ambient = 50.0
r_amp = 3 t_safe_max = 100
tau_coil = 4 t_hysteresis = 10
tau_magnet = 5 channels = 12
tr_coil = 6 period = 4096
ramp_factor = 7
temp_limit = 100 [Speaker/Left Woofer 1]
vs_chan = 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 is_chan = 0
vs_chan = 1
[Right Tweeter] [Speaker/Right Woofer 1]
r_shunt = 1 group = 1
r_dc = 2 tr_coil = 28.09
r_amp = 3 tr_magnet = 34.43
tau_coil = 4 tau_coil = 3.05
tau_magnet = 5 tau_magnet = 192.45
tr_coil = 6 t_limit = 140.0
ramp_factor = 7 t_headroom = 10.0
temp_limit = 100 z_nominal = 3.2
vs_chan = 3 is_scale = 3.75
vs_scale = 14
is_chan = 2 is_chan = 2
vs_chan = 3
[Left Woofer 1] [Speaker/Left Tweeter]
r_shunt = 1 group = 0
r_dc = 2 tr_coil = 34.5
r_amp = 3 tr_magnet = 48.2
tau_coil = 4 tau_coil = 2.31
tau_magnet = 5 tau_magnet = 61.4
tr_coil = 6 t_limit = 140.0
ramp_factor = 7 t_headroom = 10.0
temp_limit = 100 z_nominal = 3.2
vs_chan = 5 is_scale = 3.75
vs_scale = 14
is_chan = 4 is_chan = 4
vs_chan = 5
[Right Woofer 1] [Speaker/Right Tweeter]
r_shunt = 1 group = 0
r_dc = 2 tr_coil = 34.5
r_amp = 3 tr_magnet = 48.2
tau_coil = 4 tau_coil = 2.31
tau_magnet = 5 tau_magnet = 61.4
tr_coil = 6 t_limit = 140.0
ramp_factor = 7 t_headroom = 10.0
temp_limit = 100 z_nominal = 3.2
vs_chan = 7 is_scale = 3.75
vs_scale = 14
is_chan = 6 is_chan = 6
vs_chan = 7
[Left Woofer 2] [Speaker/Left Woofer 2]
r_shunt = 1 group = 1
r_dc = 2 tr_coil = 28.09
r_amp = 3 tr_magnet = 34.43
tau_coil = 4 tau_coil = 3.05
tau_magnet = 5 tau_magnet = 192.45
tr_coil = 6 t_limit = 140.0
ramp_factor = 7 t_headroom = 10.0
temp_limit = 100 z_nominal = 3.2
vs_chan = 9 is_scale = 3.75
vs_scale = 14
is_chan = 8 is_chan = 8
vs_chan = 9
[Right Woofer 2] [Speaker/Right Woofer 2]
r_shunt = 1 group = 1
r_dc = 2 tr_coil = 28.09
r_amp = 3 tr_magnet = 34.43
tau_coil = 4 tau_coil = 3.05
tau_magnet = 5 tau_magnet = 192.45
tr_coil = 6 t_limit = 140.0
ramp_factor = 7 t_headroom = 10.0
temp_limit = 100 z_nominal = 3.2
vs_chan = 11 is_scale = 3.75
vs_scale = 14
is_chan = 10 is_chan = 10
vs_chan = 11

View file

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors // (C) 2022 The Asahi Linux Contributors
use configparser::ini::Ini;
use alsa::mixer::MilliBel;
use alsa; use alsa;
use alsa::mixer::MilliBel;
use configparser::ini::Ini;
/** /**
Failsafe: Limit speaker volume massively and bail. 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); println!("{}: Could not open sound card! Error: {}", card, e);
fail(); fail();
std::process::exit(1); std::process::exit(1);
}, }
}; };
return ctldev; return ctldev;
} }
pub fn open_pcm(dev: &str, chans: &u32) -> alsa::pcm::PCM { 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) let pcm = alsa::pcm::PCM::new(dev, alsa::Direction::Capture, false).unwrap();
.unwrap();
{ {
let params = alsa::pcm::HwParams::any(&pcm).unwrap(); let params = alsa::pcm::HwParams::any(&pcm).unwrap();
params.set_channels(*chans).unwrap(); params.set_channels(chans).unwrap();
params.set_rate(48000, alsa::ValueOr::Nearest).unwrap(); params
.set_rate(sample_rate, alsa::ValueOr::Nearest)
.unwrap();
params.set_format(alsa::pcm::Format::s16()).unwrap(); params.set_format(alsa::pcm::Format::s16()).unwrap();
params.set_access(alsa::pcm::Access::RWInterleaved).unwrap(); params.set_access(alsa::pcm::Access::RWInterleaved).unwrap();
pcm.hw_params(&params).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() 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. it.
*/ */
pub fn parse_int(config: &Ini, section: &str, key: &str) -> i64 { pub fn parse_int<T: TryFrom<i64>>(config: &Ini, section: &str, key: &str) -> T
let _result: Option<i64> = match config.getint(section, key) { where
Ok(result) => match result{ <T as TryFrom<i64>>::Error: std::fmt::Debug,
Some(inner) => { {
let integer: i64 = inner; config
return integer; .getint(section, key)
}, .expect(&format!("{}/{}: Invalid value", section, key))
None => { .expect(&format!("{}/{}: Missing key", section, key))
println!("{}: Failed to parse {}", section, key); .try_into()
fail(); .expect("{}/{}: Out of bounds")
std::process::exit(1);
},
},
Err(e) => {
println!("{}: Invalid value for {}. Error: {}", section, key, e);
fail();
std::process::exit(1);
},
};
} }
/** /**
@ -89,25 +80,13 @@ pub fn parse_int(config: &Ini, section: &str, key: &str) -> i64 {
it. it.
*/ */
pub fn parse_float(config: &Ini, section: &str, key: &str) -> f32 { pub fn parse_float(config: &Ini, section: &str, key: &str) -> f32 {
let _result: Option<f64> = match config.getfloat(section, key) { let val = config
Ok(result) => match result{ .getfloat(section, key)
Some(inner) => { .expect(&format!("{}/{}: Invalid value", section, key))
let float: f32 = inner as f32; .expect(&format!("{}/{}: Missing key", section, key)) 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);
},
};
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!"); println!("Could not open a handle to an element!");
fail(); fail();
std::process::exit(1); std::process::exit(1);
}, }
}; };
return val; return val;
} }
/** /**
Wrapper for alsa::ctl::Ctl::elem_read(). Wrapper for alsa::ctl::Ctl::elem_read().
*/ */
pub fn read_ev(card: &alsa::ctl::Ctl, ev: &mut alsa::ctl::ElemValue, name: &str) { 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, Ok(val) => val,
Err(e) => { 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(); fail();
std::process::exit(1); 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(). Wrapper for alsa::ctl::Ctl::elem_write().
*/ */
pub fn write_ev(card: &alsa::ctl::Ctl, ev: &alsa::ctl::ElemValue, name: &str) { 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, Ok(val) => val,
Err(e) => { 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(); fail();
std::process::exit(1); 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()) { let db = match card.convert_to_db(id, val.into()) {
Ok(inner) => inner, Ok(inner) => inner,
Err(e) => { 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(); fail();
std::process::exit(1); std::process::exit(1);
}, }
}; };
return db; 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) { let new_int = match card.convert_from_db(id, mb, alsa::Round::Floor) {
Ok(inner) => inner as i32, Ok(inner) => inner as i32,
Err(e) => { 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(); fail();
std::process::exit(1); std::process::exit(1);
}, }
}; };
return new_int; return new_int;

View file

@ -1,88 +1,172 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors // (C) 2022 The Asahi Linux Contributors
/** /*!
Handles speaker safety on Apple Silicon machines. This code is designed to 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 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), 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. 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::fs::read_to_string;
use std::io;
use std::path::PathBuf;
use std::{thread::sleep, time}; use std::{thread::sleep, time};
use clap::Parser;
use clap_verbosity_flag::{InfoLevel, Verbosity};
use configparser::ini::Ini; use configparser::ini::Ini;
use log;
use log::{debug, error, info, trace, warn};
use simple_logger::SimpleLogger;
mod types;
mod helpers; mod helpers;
mod types;
use crate::types::SafetyMonitor; static VERSION: &str = "0.0.1";
static ASAHI_DEVICE: &str = "hw:0"; const DEFAULT_CONFIG_PATH: &str = "share/speakersafetyd";
static VISENSE_PCM: &str = "hw:0,2";
// Will eventually be /etc/speakersafetyd/ or similar /// Simple program to greet a person
static CONFIG_DIR: &str = "./"; #[derive(Parser, Debug)]
static SUPPORTED: [&str; 1] = [ #[command(version, about, long_about = None)]
"j314", 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 { fn get_machine() -> String {
let _compat: io::Result<String> = match read_to_string("/proc/device-tree/compatible") { read_to_string("/proc/device-tree/compatible")
Ok(compat) => { .expect("Could not read device tree compatible")
return compat[6..10].to_string(); .strip_prefix("apple,")
}, .expect("Unexpected compatible format")
Err(e) => { .split_once("\0")
println!("Could not read devicetree compatible: {}", e); .expect("Unexpected compatible format")
std::process::exit(1); .0
} .trim_end_matches(|c: char| c.is_ascii_alphabetic())
}; .to_string()
} }
fn get_speakers(config: &Ini) -> Vec<String> {
fn get_drivers(config: &Ini) -> Vec<String> { config
.sections()
let drivers = config.sections(); .iter()
.filter_map(|a| a.strip_prefix("Speaker/"))
return drivers; .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 main() { 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(); 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 cfg: Ini = Ini::new_cs();
let mut speakers: Vec<types::Speaker> = Vec::new(); cfg.load(config_path).expect("Failed to read config file");
let card: alsa::ctl::Ctl = helpers::open_card(&ASAHI_DEVICE);
if SUPPORTED.contains(&model.as_str()) { let globals = types::Globals::parse(&cfg);
cfg.load(CONFIG_DIR.to_owned() + &model + ".conf").unwrap();
} else { let speaker_names = get_speakers(&cfg);
println!("Unsupported machine {}", model); let speaker_count = speaker_names.len();
std::process::exit(1); 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); assert!(
groups
for i in list_drivers { .values()
let new_speaker: types::Speaker = types::SafetyMonitor::new(&i, &cfg, &card); .map(|a| a.speakers.len())
speakers.push(new_speaker); .fold(0, |a, b| a + b)
} == speaker_count
);
let num_chans: u32 = speakers.len().try_into().unwrap(); assert!(2 * speaker_count <= globals.channels);
let pcm_name = format!("{},{}", device, globals.visense_pcm);
// Set up PCM to buffer in V/ISENSE // Set up PCM to buffer in V/ISENSE
let cap: alsa::pcm::PCM = helpers::open_pcm(&VISENSE_PCM, &num_chans); let pcm: alsa::pcm::PCM =
let mut buf = [0i16; BUF_SZ]; // 128 samples from V and I for 6 channels helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 48000);
let io = cap.io_i16().unwrap(); 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 { loop {
// Block while we're reading into the buffer // Block while we're reading into the buffer
io.readi(&mut buf).unwrap(); 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 // SPDX-License-Identifier: MIT
// (C) 2022 The Asahi Linux Contributors // (C) 2022 The Asahi Linux Contributors
use std::ffi::{CString, CStr};
use configparser::ini::Ini;
use alsa::ctl::Ctl; use alsa::ctl::Ctl;
use configparser::ini::Ini;
use log::{debug, error, info, trace, warn};
use std::ffi::{CStr, CString};
use crate::helpers; use crate::helpers;
@ -19,21 +20,19 @@ struct Elem {
val: alsa::ctl::ElemValue, val: alsa::ctl::ElemValue,
} }
trait ALSAElem { impl Elem {
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Self;
}
impl ALSAElem for Elem {
fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Elem { fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Elem {
// CString::new() cannot borrow a String. We want name for the elem // CString::new() cannot borrow a String. We want name for the elem
// for error identification though, so it can't consume name directly. // for error identification though, so it can't consume name directly.
let borrow: String = name.clone(); let borrow: String = name.clone();
let mut new_elem: Elem = { Elem { let mut new_elem: Elem = {
Elem {
elem_name: name, elem_name: name,
id: alsa::ctl::ElemId::new(alsa::ctl::ElemIface::Mixer), id: alsa::ctl::ElemId::new(alsa::ctl::ElemIface::Mixer),
val: helpers::new_elemvalue(t), val: helpers::new_elemvalue(t),
}}; }
};
let cname: CString = CString::new(borrow).unwrap(); let cname: CString = CString::new(borrow).unwrap();
let cstr: &CStr = cname.as_c_str(); let cstr: &CStr = cname.as_c_str();
@ -58,68 +57,111 @@ impl ALSAElem for Elem {
struct Mixer { struct Mixer {
drv: String, drv: String,
level: Elem, level: Elem,
vsense: Elem, amp_gain: Elem,
isense: Elem,
} }
trait ALSACtl { impl Mixer {
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 {
// TODO: implement turning on V/ISENSE // TODO: implement turning on V/ISENSE
fn new(name: &str, card: &Ctl) -> Mixer { fn new(name: &str, card: &Ctl) -> Mixer {
let new_mixer: Mixer = { Mixer { let mut vs = Elem::new(
drv: name.to_owned(), name.to_owned() + " VSENSE Switch",
level: ALSAElem::new(name.to_owned() + " Speaker Volume", card, card,
alsa::ctl::ElemType::Integer), alsa::ctl::ElemType::Boolean,
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),
}};
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 { fn get_lvl(&mut self, card: &Ctl) -> f32 {
helpers::read_ev(card, &mut self.level.val, &self.level.elem_name); helpers::read_ev(card, &mut self.level.val, &self.level.elem_name);
let val: i32 = match self.level.val.get_integer(0) { let val = self
Some(inner) => inner, .level
None => { .val
println!("Could not read level from {}", self.drv); .get_integer(0)
helpers::fail(); .expect(&format!("Could not read level for {}", self.drv));
std::process::exit(1);
},
};
let db: f32 = helpers::int_to_db(card, &self.level.id, val).to_db(); helpers::int_to_db(card, &self.level.id, val).to_db()
return db;
} }
fn set_lvl(&mut self, card: &Ctl, lvl: f32) { fn set_lvl(&mut self, card: &Ctl, lvl: f32) {
let new_val: i32 = helpers::db_to_int(card, &self.level.id, lvl); let new_val: i32 = helpers::db_to_int(card, &self.level.id, lvl);
match self.level.val.set_integer(0, new_val) { match self.level.val.set_integer(0, new_val) {
Some(_) => {}, Some(_) => {}
None => { None => {
println!("Could not set level for {}", self.drv); println!("Could not set level for {}", self.drv);
helpers::fail(); helpers::fail();
std::process::exit(1); std::process::exit(1);
}, }
}; };
helpers::write_ev(card, &self.level.val, &self.level.elem_name); 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 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_coil: voice coil ramp time constant (seconds)
tau_magnet: magnet ramp time constant (seconds) tau_magnet: magnet ramp time constant (seconds)
tr_coil: thermal resistance of voice coil (*C/W) 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. 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 { pub struct Speaker {
name: String, pub name: String,
pub group: usize,
alsa_iface: Mixer, alsa_iface: Mixer,
tau_coil: f32, tau_coil: f32,
tau_magnet: f32, tau_magnet: f32,
tr_coil: f32, tr_coil: f32,
temp_limit: f32, tr_magnet: f32,
vs_chan: i64, t_limit: f32,
is_chan: i64, 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 { let section = "Speaker/".to_owned() + name;
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Self; let mut new_speaker: Speaker = Speaker {
fn power_now(&mut self, vs: &[i16], is: &[i16]) -> f32; name: name.to_string(),
fn run(&mut self, card: &Ctl, buf: &[i16; 128 * 6 * 2]); 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(),
};
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;
// 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
} }
impl SafetyMonitor for Speaker { pub fn run_model(&mut self, buf: &[i16], sample_rate: f32) -> f32 {
fn new(driver_name: &str, config: &Ini, card: &Ctl) -> Speaker { let s = &mut self.s;
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 step = 1. / sample_rate;
let alpha_coil = (step / (self.tau_coil + step)) as f64;
let alpha_magnet = (step / (self.tau_magnet + step)) as f64;
return new_speaker; 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
);
} }
fn power_now(&mut self, vs: &[i16], is: &[i16]) -> f32 { pwr_sum += p;
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;
return v_avg * i_avg;
} }
// I'm not sure on the maths here for determining when to start dropping the volume. let pwr_avg: f32 = pwr_sum / ((buf.len() / self.g.channels) as f32);
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 s.t_coil_hyst = s
let temp0: f32 = 35f32; .t_coil_hyst
let mut temp_vc: f32 = temp0; .max(s.t_coil as f32)
let mut temp_magnet: f32 = temp0; .min(s.t_coil as f32 + self.g.t_hysteresis);
let alpha_vc: f32 = 0.01 / (temp_vc + 0.01); s.t_magnet_hyst = s
let alpha_magnet: f32 = 0.01 / (temp_magnet + 0.01); .t_magnet_hyst
.max(s.t_magnet as f32)
.min(s.t_magnet as f32 + self.g.t_hysteresis);
// Power through the voice coil (average of most recent 128 samples) let temp = s.t_coil_hyst.max(s.t_magnet_hyst);
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; let reduction =
temp_vc = vc_target * alpha_vc + temp_vc * (1.0 - alpha_vc); (temp - self.g.t_safe_max) / (self.t_limit - self.t_headroom - self.g.t_safe_max);
println!("Current voice coil temp: {:.2} *C", temp_vc); let gain = s.min_gain * reduction.max(0.);
let magnet_target: f32 = temp0 + pwr * self.tau_magnet; s.gain = gain;
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 { debug!(
println!("Voice coil for {} below temp limit, ramping back up.", self.name); "{}: Coil {:.2} °C Magnet {:.2} °C Power {:.2} W Gain {:.2} dB",
// For every degree below temp_limit, raise level by 0.5 dB self.name, s.t_coil, s.t_magnet, pwr_avg, gain
let new_lvl: f32 = lvl + ((self.temp_limit - temp_vc) * 0.5); );
self.alsa_iface.set_lvl(card, new_lvl);
if s.gain > -0.01 {
s.gain = 0.;
} }
if temp_vc > (self.temp_limit - 15f32) { s.gain
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);
} }
println!("Volume on {} is now {} dB", self.name, self.alsa_iface.get_lvl(card)); pub fn update(&mut self, ctl: &Ctl, gain: f32) {
self.alsa_iface.set_lvl(ctl, gain);
} }
} }