speakersafetyd-1.0.0/.cargo_vcs_info.json0000644000000001360000000000100140440ustar { "git": { "sha1": "7faaaa5a0bd949e993560da82d1594c17b75712c" }, "path_in_vcs": "" }speakersafetyd-1.0.0/.gitignore000064400000000000000000000000651046102023000146250ustar 00000000000000/target *.kate-swp */target testing/*.wav testing/j* speakersafetyd-1.0.0/95-speakersafetyd.rules000064400000000000000000000037211046102023000171600ustar 00000000000000SUBSYSTEM=="sound", DRIVERS=="snd-soc-macaudio", GOTO="speakersafetyd_macaudio" GOTO="speakersafetyd_end" LABEL="speakersafetyd_macaudio" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ*", ENV{ACP_IGNORE}="1" #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ180", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ274", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ293", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ313", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ314", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ316", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ375", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ413", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ414", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ415", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ416", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ456", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ457", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ473", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ474", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ475", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ493", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" LABEL="speakersafetyd_end" speakersafetyd-1.0.0/Cargo.lock0000644000000454220000000000100120260ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "alsa" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce34de545ad29bcc00cb1b87a94c132256dcf83aa7eeb9674482568405a6ff0a" dependencies = [ "alsa-sys", "bitflags 2.6.0", "libc", "nix", ] [[package]] name = "alsa-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" dependencies = [ "libc", "pkg-config", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys", ] [[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.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets", ] [[package]] name = "clap" version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap-verbosity-flag" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63d19864d6b68464c59f7162c9914a0b569ddc2926b4a2d71afe62a9738eff53" dependencies = [ "clap", "log", ] [[package]] name = "clap_builder" version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_derive" version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "colored" version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" dependencies = [ "is-terminal", "lazy_static", "winapi", ] [[package]] name = "configparser" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" dependencies = [ "indexmap", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[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.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "indexmap" version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is-terminal" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ "hermit-abi 0.3.9", "libc", "windows-sys", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "json" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_threads" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pkg-config" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "serde" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "simple_logger" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45b60258a35dc3cb8a16890b8fd6723349bfa458d7960e25e633f1b1c19d7b5e" dependencies = [ "atty", "colored", "log", "time", "winapi", ] [[package]] name = "speakersafetyd" version = "1.0.0" dependencies = [ "alsa", "chrono", "clap", "clap-verbosity-flag", "configparser", "json", "libc", "log", "signal-hook", "simple_logger", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "time" version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[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-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" speakersafetyd-1.0.0/Cargo.toml0000644000000022440000000000100120440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "speakersafetyd" version = "1.0.0" description = "Speaker protection daemon for embedded Linux systems" readme = "README.md" license = "MIT" repository = "https://github.com/AsahiLinux/speakersafetyd/" [dependencies.alsa] version = "^0.8.1" [dependencies.chrono] version = "0.4.31" [dependencies.clap] version = "4.1.6" features = ["derive"] [dependencies.clap-verbosity-flag] version = "2.0.0" [dependencies.configparser] version = "^3.0.3" features = ["indexmap"] [dependencies.json] version = "0.12.4" [dependencies.libc] version = "0.2.150" [dependencies.log] version = "0.4.17" [dependencies.signal-hook] version = "0.3.17" [dependencies.simple_logger] version = "1.16.0" speakersafetyd-1.0.0/Cargo.toml.orig000064400000000000000000000007661046102023000155340ustar 00000000000000[package] name = "speakersafetyd" version = "1.0.0" edition = "2021" license = "MIT" description = "Speaker protection daemon for embedded Linux systems" repository = "https://github.com/AsahiLinux/speakersafetyd/" [dependencies] alsa = "^0.8.1" configparser = { version = "^3.0.3", features=["indexmap"] } clap = { version = "4.1.6", features=["derive"] } log = "0.4.17" clap-verbosity-flag = "2.0.0" simple_logger = "1.16.0" chrono = "0.4.31" json = "0.12.4" signal-hook = "0.3.17" libc = "0.2.150" speakersafetyd-1.0.0/LICENSE000064400000000000000000000020471046102023000136440ustar 00000000000000Copyright The Asahi Linux Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. speakersafetyd-1.0.0/Makefile000064400000000000000000000024231046102023000142750ustar 00000000000000# SPDX-Licence-Identifier: MIT # Copyright The Asahi Linux Contributors BINDIR ?= /usr/bin UNITDIR ?= /lib/systemd/system UDEVDIR ?= /lib/udev/rules.d TMPFILESDIR ?= /usr/lib/tmpfiles.d SHAREDIR ?= /usr/share/ VARDIR ?= /var/ all: cargo build --release install: install-data install -dDm0755 $(DESTDIR)/$(BINDIR) install -pm0755 target/release/speakersafetyd $(DESTDIR)/$(BINDIR)/speakersafetyd install-data: install -dDm0755 $(DESTDIR)/$(UNITDIR) install -pm0644 speakersafetyd.service $(DESTDIR)/$(UNITDIR)/speakersafetyd.service install -dDm0755 $(DESTDIR)/$(UDEVDIR) install -pm0644 95-speakersafetyd.rules $(DESTDIR)/$(UDEVDIR)/95-speakersafetyd.rules install -dDm0755 $(DESTDIR)/$(SHAREDIR)/speakersafetyd/apple install -pm0644 -t $(DESTDIR)/$(SHAREDIR)/speakersafetyd/apple $(wildcard conf/apple/*) install -dDm0755 $(DESTDIR)/$(VARDIR)/lib/speakersafetyd/blackbox install -dDm0755 $(DESTDIR)/$(TMPFILESDIR) install -pm0644 speakersafetyd.tmpfiles $(DESTDIR)/$(TMPFILESDIR)/speakersafetyd.conf uninstall: rm -f $(DESTDIR)/$(BINDIR)/speakersafetyd $(DESTDIR)/$(UNITDIR)/speakersafetyd.service $(DESTDIR)/$(UDEVDIR)/95-speakersafetyd.rules $(DESTDIR)/$(TMPFILESDIR)/speakersafetyd.conf rm -rf $(DESTDIR)/$(SHAREDIR)/speakersafetyd .PHONY: all install install-data uninstall speakersafetyd-1.0.0/README.md000064400000000000000000000071721046102023000141220ustar 00000000000000## speakersafetyd - a software Smart Amp implementation speakersafetyd is a userspace daemon written in Rust that implements an analogue of the Texas Instruments Smart Amp speaker protection model. Apple Silicon Macs mostly use the Texas Instruments TAS2764 amp chip (codec in ALSA parlance), which provides sense lines for the voltage and current across the voice coil of the connected speaker. These codecs are designed to be used in embedded applications where device firmware takes this information and uses it to protect the speaker from damage. Apple instead implement this as machine-specific plugins to the userspace half of CoreAudio. An increasing number of other vendors in both the desktop and embedded/Android worlds are choosing to go down a similar route, folding this functionality into proprietary driver/userspace blobs that usually also bundle niceties like EQ (we have a solution for this too, see [asahi-audio](https://github.com/AsahiLinux/asahi-audio)). This puts users at serious risk of permanently destroying their expensive devices if they choose to run custom software, such as Asahi Linux or an Open Source Android ROM. speakersafetyd is the first (as far as we know) FOSS implementation of a speaker protection model. It solves the problem described above by allowing parties interested in compatible devices to quickly and easily implement a speaker protection model for those devices. Only Apple Silicon Macs under Linux are currently supported, however the model applies to all loudspeakers. The daemon itself should be easy enough to adapt for any device that provides V/ISENSE data in a manner similar to TAS2764. ### Dependencies * Rust stable * alsa-lib * An Apple Silicon Mac running Asahi Linux ### Some background on Smart Amps The cheap component speaker elements used in modern devices like Bluetooth speakers, TVs, laptops, etc. are very fragile. In order to eke the highest possible sound quality out of them, they need to be driven *hard*. This leaves us with a dilemma - how do we drive these speakers hard enough to get a loud, high-quality output but not hard enough to destroy them? A speaker's electromechanical characteristics can be modelled and boiled down to a set of parameters - the Thiele/Small Parameters. These can be used to predict what the speaker will do with certain inputs. When we add measured properties like the time constant of the speaker's voice coil's and magnet's temperature curve, we can accurately model a speaker's temperature for any given voltage/current across the coil. When the speaker is getting too hot, we just reduce the power going to it until it cools down. This lets us fearlessly drive the speakers as hard as they physically can be without being permanently damaged. This is extremely useful, as without it the output level on these devices would have to be hard limited to a very low level that is known to be safe for the worst possible input. Instead, we can simply duck the output in those cases and allow the speakers to operate at full power where possible. Many integrated amplifier chips implement this functionality in hardware, as well as additional advanced DSP features like compressors and limiters. Texas Instruments call their implementation "Smart Amp." Integrators need only communicate the parameter set to the chip for the speaker it is connected to, and it does the rest. Many do not however, and instead only provide facilities for measuring the voltage and current across the speaker's voice coil. It is up to the implementer to capture this data and do something with it. speakersafetyd is (as far as we know) the first and only FOSS implementation of the Smart Amp protection model. speakersafetyd-1.0.0/conf/apple/j180.conf000064400000000000000000000013241046102023000162130ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 4 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Woofer] group = 1 tr_coil = 22.20 tr_magnet = 43.90 tau_coil = 7.40 tau_magnet = 530.00 t_limit = 130.0 t_headroom = 10.0 z_nominal = 3.80 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Tweeter] group = 0 tr_coil = 51.40 tr_magnet = 57.90 tau_coil = 2.10 tau_magnet = 225.00 t_limit = 120.0 t_headroom = 10.0 z_nominal = 3.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 speakersafetyd-1.0.0/conf/apple/j274.conf000064400000000000000000000007551046102023000162260ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 46.0 t_hysteresis = 5.0 t_window = 20.0 channels = 2 period = 4096 link_gains = False uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Playback Volume [Speaker/Mono] group = 0 tr_coil = 40.00 tr_magnet = 60.00 tau_coil = 3.70 tau_magnet = 250.00 t_limit = 140.0 t_headroom = 40.0 z_nominal = 4.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 speakersafetyd-1.0.0/conf/apple/j293.conf000064400000000000000000000023011046102023000162140ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 8 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Playback Volume [Speaker/Left Front] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Front] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Rear] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Rear] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 speakersafetyd-1.0.0/conf/apple/j313.conf000064400000000000000000000013261046102023000162130ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 4 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Playback Volume [Speaker/Left] group = 0 tr_coil = 29.00 tr_magnet = 36.00 tau_coil = 2.40 tau_magnet = 80.00 t_limit = 120.0 t_headroom = 15.0 z_nominal = 4.90 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right] group = 0 tr_coil = 29.00 tr_magnet = 36.00 tau_coil = 2.40 tau_magnet = 80.00 t_limit = 120.0 t_headroom = 15.0 z_nominal = 4.90 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 speakersafetyd-1.0.0/conf/apple/j314.conf000064400000000000000000000034741046102023000162220ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 12 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 34.50 tr_magnet = 48.20 tau_coil = 2.31 tau_magnet = 61.40 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.20 z_shunt = 0.09 a_t_20c = 0.00354779 a_t_35c = 0.00329538 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 34.50 tr_magnet = 48.20 tau_coil = 2.31 tau_magnet = 61.40 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.20 z_shunt = 0.09 a_t_20c = 0.00354779 a_t_35c = 0.00329538 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 8 vs_chan = 9 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 10 vs_chan = 11 speakersafetyd-1.0.0/conf/apple/j316.conf000064400000000000000000000034701046102023000162200ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 12 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Left Woofer 1] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Woofer 1] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 45.0 tr_magnet = 50.00 tau_coil = 1.3 tau_magnet = 73.26 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00382045 a_t_35c = 0.00361650 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 45.0 tr_magnet = 50.00 tau_coil = 1.3 tau_magnet = 73.26 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00382045 a_t_35c = 0.00361650 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 [Speaker/Left Woofer 2] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 8 vs_chan = 9 [Speaker/Right Woofer 2] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 10 vs_chan = 11 speakersafetyd-1.0.0/conf/apple/j375.conf000064400000000000000000000007441046102023000162260ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 46.0 t_hysteresis = 5.0 t_window = 20.0 channels = 2 period = 4096 link_gains = False uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Mono] group = 0 tr_coil = 40.00 tr_magnet = 60.00 tau_coil = 3.70 tau_magnet = 250.00 t_limit = 140.0 t_headroom = 40.0 z_nominal = 4.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 speakersafetyd-1.0.0/conf/apple/j413.conf000064400000000000000000000024361046102023000162170ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 8 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Left Woofer] group = 1 tr_coil = 32.10 tr_magnet = 20.00 tau_coil = 4.70 tau_magnet = 66.50 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.00 z_shunt = 0.00 a_t_20c = 0.00370036 a_t_35c = 0.00351349 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Woofer] group = 1 tr_coil = 32.10 tr_magnet = 20.00 tau_coil = 4.70 tau_magnet = 66.50 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.00 z_shunt = 0.00 a_t_20c = 0.00370036 a_t_35c = 0.00351349 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 104.90 tr_magnet = 200.0 tau_coil = 1.85 tau_magnet = 70.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.72 z_shunt = 0.00 a_t_20c = 0.00371053 a_t_35c = 0.00350437 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 104.90 tr_magnet = 200.0 tau_coil = 1.85 tau_magnet = 70.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.72 z_shunt = 0.00 a_t_20c = 0.00371053 a_t_35c = 0.00350437 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 speakersafetyd-1.0.0/conf/apple/j414.conf000064400000000000000000000034741046102023000162230ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 12 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 34.50 tr_magnet = 48.20 tau_coil = 2.31 tau_magnet = 61.40 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.20 z_shunt = 0.09 a_t_20c = 0.00354779 a_t_35c = 0.00329538 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 34.50 tr_magnet = 48.20 tau_coil = 2.31 tau_magnet = 61.40 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.20 z_shunt = 0.09 a_t_20c = 0.00354779 a_t_35c = 0.00329538 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 8 vs_chan = 9 [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.20 z_shunt = 0.09 a_t_20c = 0.00383214 a_t_35c = 0.00362404 is_scale = 3.75 vs_scale = 14 is_chan = 10 vs_chan = 11 speakersafetyd-1.0.0/conf/apple/j415.conf000064400000000000000000000034721046102023000162220ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 12 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Left Woofer 1] group = 1 tr_coil = 40.00 tr_magnet = 26.00 tau_coil = 3.00 tau_magnet = 35.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.20 z_shunt = 0.00 a_t_20c = 0.00352082 a_t_35c = 0.00328147 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Woofer 1] group = 1 tr_coil = 40.00 tr_magnet = 26.00 tau_coil = 3.00 tau_magnet = 37.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.20 z_shunt = 0.00 a_t_20c = 0.00352082 a_t_35c = 0.00328147 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 124.00 tr_magnet = 89.80 tau_coil = 1.80 tau_magnet = 28.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.76 z_shunt = 0.00 a_t_20c = 0.00347480 a_t_35c = 0.00336649 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 129.00 tr_magnet = 87.20 tau_coil = 1.80 tau_magnet = 28.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.76 z_shunt = 0.00 a_t_20c = 0.00347480 a_t_35c = 0.00336649 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 [Speaker/Left Woofer 2] group = 1 tr_coil = 35.00 tr_magnet = 24.50 tau_coil = 3.00 tau_magnet = 35.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.00 z_shunt = 0.00 a_t_20c = 0.00352082 a_t_35c = 0.00328147 is_scale = 3.75 vs_scale = 14 is_chan = 8 vs_chan = 9 [Speaker/Right Woofer 2] group = 1 tr_coil = 35.00 tr_magnet = 24.50 tau_coil = 3.00 tau_magnet = 35.00 t_limit = 140.0 t_headroom = 10.0 z_nominal = 4.00 z_shunt = 0.00 a_t_20c = 0.00352082 a_t_35c = 0.00328147 is_scale = 3.75 vs_scale = 14 is_chan = 10 vs_chan = 11 speakersafetyd-1.0.0/conf/apple/j416.conf000064400000000000000000000034701046102023000162210ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 12 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Left Woofer 1] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Woofer 1] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Tweeter] group = 0 tr_coil = 45.0 tr_magnet = 50.00 tau_coil = 1.3 tau_magnet = 73.26 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00382045 a_t_35c = 0.00361650 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Tweeter] group = 0 tr_coil = 45.0 tr_magnet = 50.00 tau_coil = 1.3 tau_magnet = 73.26 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00382045 a_t_35c = 0.00361650 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 [Speaker/Left Woofer 2] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 8 vs_chan = 9 [Speaker/Right Woofer 2] group = 1 tr_coil = 23.00 tr_magnet = 40.00 tau_coil = 5.60 tau_magnet = 197.25 t_limit = 140.0 t_headroom = 10.0 z_nominal = 3.60 z_shunt = 0.09 a_t_20c = 0.00374895 a_t_35c = 0.00354903 is_scale = 3.75 vs_scale = 14 is_chan = 10 vs_chan = 11 speakersafetyd-1.0.0/conf/apple/j456.conf000064400000000000000000000023561046102023000162270ustar 00000000000000[Globals] # NO VISENSE! TODO t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 8 period = 4096 link_gains = True uclamp_max = 64 [Controls] # vsense = # isense = # amp_gain = # volume = [Speaker/A_ch2] group = 0 tr_coil = 20.00 tr_magnet = 28.00 tau_coil = 10.00 tau_magnet = 391.00 t_limit = 120.0 t_headroom = 20.0 z_nominal = 7.00 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 4 # TODO vs_chan = 5 [Speaker/A_ch3] group = 0 tr_coil = 20.00 tr_magnet = 28.00 tau_coil = 10.00 tau_magnet = 391.00 t_limit = 120.0 t_headroom = 20.0 z_nominal = 7.00 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 6 # TODO vs_chan = 7 [Speaker/B_ch0] group = 1 tr_coil = 46.00 tr_magnet = 76.00 tau_coil = 1.30 tau_magnet = 115.00 t_limit = 120.0 t_headroom = 40.0 z_nominal = 5.50 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 0 # TODO vs_chan = 1 [Speaker/B_ch1] group = 1 tr_coil = 46.00 tr_magnet = 76.00 tau_coil = 1.30 tau_magnet = 115.00 t_limit = 120.0 t_headroom = 40.0 z_nominal = 5.50 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 2 # TODO vs_chan = 3 speakersafetyd-1.0.0/conf/apple/j457.conf000064400000000000000000000023561046102023000162300ustar 00000000000000[Globals] # NO VISENSE! TODO t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 8 period = 4096 link_gains = True uclamp_max = 64 [Controls] # vsense = # isense = # amp_gain = # volume = [Speaker/A_ch2] group = 0 tr_coil = 20.00 tr_magnet = 28.00 tau_coil = 10.00 tau_magnet = 391.00 t_limit = 120.0 t_headroom = 20.0 z_nominal = 7.00 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 4 # TODO vs_chan = 5 [Speaker/A_ch3] group = 0 tr_coil = 20.00 tr_magnet = 28.00 tau_coil = 10.00 tau_magnet = 391.00 t_limit = 120.0 t_headroom = 20.0 z_nominal = 7.00 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 6 # TODO vs_chan = 7 [Speaker/B_ch0] group = 1 tr_coil = 46.00 tr_magnet = 76.00 tau_coil = 1.30 tau_magnet = 115.00 t_limit = 120.0 t_headroom = 40.0 z_nominal = 5.50 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 0 # TODO vs_chan = 1 [Speaker/B_ch1] group = 1 tr_coil = 46.00 tr_magnet = 76.00 tau_coil = 1.30 tau_magnet = 115.00 t_limit = 120.0 t_headroom = 40.0 z_nominal = 5.50 a_t_20c = 0.0037 a_t_35c = 0.0037 # TODO is_scale = 3.75 # TODO vs_scale = 14 # TODO is_chan = 2 # TODO vs_chan = 3 speakersafetyd-1.0.0/conf/apple/j473.conf000064400000000000000000000007441046102023000162250ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 46.0 t_hysteresis = 5.0 t_window = 20.0 channels = 2 period = 4096 link_gains = False uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Mono] group = 0 tr_coil = 40.00 tr_magnet = 60.00 tau_coil = 3.70 tau_magnet = 250.00 t_limit = 140.0 t_headroom = 40.0 z_nominal = 4.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 speakersafetyd-1.0.0/conf/apple/j474.conf000064400000000000000000000007441046102023000162260ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 46.0 t_hysteresis = 5.0 t_window = 20.0 channels = 2 period = 4096 link_gains = False uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Mono] group = 0 tr_coil = 40.00 tr_magnet = 60.00 tau_coil = 3.70 tau_magnet = 250.00 t_limit = 140.0 t_headroom = 40.0 z_nominal = 4.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 speakersafetyd-1.0.0/conf/apple/j475.conf000064400000000000000000000007441046102023000162270ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 46.0 t_hysteresis = 5.0 t_window = 20.0 channels = 2 period = 4096 link_gains = False uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Mono] group = 0 tr_coil = 40.00 tr_magnet = 60.00 tau_coil = 3.70 tau_magnet = 250.00 t_limit = 140.0 t_headroom = 40.0 z_nominal = 4.60 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 speakersafetyd-1.0.0/conf/apple/j493.conf000064400000000000000000000022701046102023000162230ustar 00000000000000[Globals] visense_pcm = 2 t_ambient = 50.0 t_hysteresis = 5.0 t_window = 20.0 channels = 8 period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume [Speaker/Left Front] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 0 vs_chan = 1 [Speaker/Right Front] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 2 vs_chan = 3 [Speaker/Left Rear] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 4 vs_chan = 5 [Speaker/Right Rear] group = 0 tr_coil = 38.30 tr_magnet = 49.10 tau_coil = 2.80 tau_magnet = 79.70 t_limit = 130.0 t_headroom = 10.0 z_nominal = 9.70 a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = 6 vs_chan = 7 speakersafetyd-1.0.0/docs/audump.py000064400000000000000000000234421046102023000154360ustar 00000000000000import struct, sys, os.path, plistlib, pprint LABELS = { "spp3": { 0: { 0: "thermal protection enabled", 1: "displacement protection enabled", 2: "thermal/power control gain attack time (s)", 3: "thermal/power control gain release time (s)", 4: "ambient temperature", 5: "SafeTlim", 6: "SafeTlimTimeMin", 7: "SafeTlimOffset", 8: "LookaheadDelay_ms", 9: "peak attack time (s)", 10: "peak decay time (s)", 11: "feedback integration time", 12: "thermal gain (dB)", 13: "displacement gain (dB)", 14: "spk pwr averaging window time (s)", 15: "modeled speaker power", 16: "measured speaker power", 17: "power control gain", 18: "CPMS power control enabled", 19: "CPMS power control closed loop", }, 4: { 0: "temperature limit", 1: "hard temp limit headroom", 2: "T_sett_vc", 3: "T_sett_mg", 4: "tau_Tvc", 5: "tau_Tmg", 6: "ThermalFFSpeedupFactor", 7: "temperature", 8: "OL temperature", 9: "Reb_ref", 10: "Rshunt", 11: "Rampout", 12: "mt", 13: "ct", 14: "kt", 15: "ag", 16: "g_bw", 17: "Q_d", 18: "phi", 19: "x_lim", 20: "ThermalMeasurementMethod", 21: "pilot tone enabled", 22: "CL thermal feedback enabled", 23: "TlimErrDecayTime", 24: "TempSenseWindowTime", 25: "TempSenseSmoothingTau", 26: "a_t_inv", 27: "PilotAmplHi_dB", 28: "PilotAmplLo_dB", 29: "PilotUpperThres", 30: "PilotLowerThres", 31: "PilotDecayTime", 32: "PilotFreq", 33: "LPMLSPreGain", 34: "LPMLSPostGain", 35: "LPMLSLowerCorner", 36: "LPMLS pre clip level", 37: "mu_Re", 38: "mu_Le", 39: "mu mechanical (PU)", 40: "Max relative displacement", 41: "abs(Min relative displacement)", 42: "DisplacementProtectionType", 64: "thermal gain", 65: "displacement gain", 66: "power control gain", 67: "PilotDecayTimeStage2", 68: "PilotEnableThres", }, }, "atsp": { 0: { 0: "Bypass", 40: "Gain link all audio channels", 1: "speakerType A: Amplifier sensitivity [V/Fs]", 2: "speakerType A: VoiceCoil: DC resistance [Ohms]", 3: "speakerType A: VoiceCoil: thermal resistance [C/Watt]", 4: "speakerType A: Voice Coil: thermal time constant [s]", 5: "speakerType A: Magnet: thermal resistance [C/Watt]", 6: "speakerType A: Magnet: thermal time constant [s]", 7: "speakerType A: Ambient temperature, [C]", # The target temperature of the speakers 8: "speakerType A: Temperature limit [C]", 9: "speakerType A: Attack time (ms)", 10: "speakerType A: Release time (ms)", 11: "speakerType A: Temperature hard limit headroom [C]", 12: "speakerType A: Gain link", 13: "speakerType A: Audio channel assignment", 14: "speakerType B: Amplifier sensitivity [V/Fs", 15: "speakerType B: VoiceCoil: DC resistance [Ohms]", 16: "speakerType B: VoiceCoil: thermal resistance [C/Watt]", 17: "speakerType B: Voice Coil: thermal time constant [s]", 18: "speakerType B: Magnet: thermal resistance [C/Watt]", 19: "speakerType B: Magnet: thermal time constant [s]", 20: "speakerType B: Ambient temperature, [C]", 21: "speakerType B: Temperature limit [C]", 22: "speakerType B: Attack time (ms)", 23: "speakerType B: Release time (ms)", 24: "speakerType B: Temperature hard limit headroom [C]", 25: "speakerType B: Gain link", 26: "speakerType B: Audio channel assignment", 27: "speakerType C: Amplifier sensitivity [V/Fs]", 28: "speakerType C: VoiceCoil: DC resistance [Ohms]", 29: "speakerType C: VoiceCoil: thermal resistance [C/Watt]", 30: "speakerType C: Voice Coil: thermal time constant [s]", 31: "speakerType C: Magnet: thermal resistance [C/Watt]", 32: "speakerType C: Magnet: thermal time constant [s]", 33: "speakerType C: Ambient temperature, [C]", 34: "speakerType C: Temperature limit [C]", 35: "speakerType C: Attack time (ms)", 36: "speakerType C: Release time (ms)", 37: "speakerType C: Temperature hard limit headroom [C]", 38: "speakerType C: Gain link", 39: "speakerType C: Audio channel assignment", } } } """ ATSP protection behavior: Max gain reduction is 20dB. "Temperature limit" is the target temperature If temperature exceeds limit + "Temperature hard limit headroom", protection goes into panic mode and triggers 20dB reduction. For settings: amp = 12 r = 4 rVc = 50 aVc = 2 rMg = 1 aMg = 1 Ta = 50 Tlim = 150 Theadroom = 5 We see this limiter behavior: In Out 0 -9.7 -8 -9.7 -9 -9.6 -9.5 -9.7 -9.8 -9.9 -10 -10 In other words, it behaves like a hard limit / compressor with infinite ratio. Theadroom has no influence on the gain reduction, it just affects stability (temperature does exceed Tlim transiently, if the transient is > Theadroom it panics). Too low a Theadroom leads to unstable behavior. """ def dump_audata(labels, data): top = {} while data: hdr = data[:0xc] data = data[0xc:] typ, grp, cnt = struct.unpack(">III", hdr) d = {} for i in range(cnt): blk = data[:0x8] data = data[0x8:] key, val = struct.unpack(">If", blk) if typ in labels: if key in labels[typ]: key = labels[typ][key] d[key] = val top[(typ, grp)] = d pprint.pprint(top, stream=sys.stderr) return top def process_spp3(e): # Grab the plist file, which is mostly redundant but contains # some details not in the au preset for i in prop["Boxes"]: if i["Name"] == e["displayname"]: for p in i["Properties"]: if p["Number"] == 64003: path = os.path.join(base, "DSP", p["Path"].split("/DSP/")[1]) pl = plistlib.load(open(path, "rb")) d = dump_audata(LABELS["spp3"], e["aupreset"]["data"]) spkrs = "" channels = 0 gbl = d[(0, 0)] for (typ, ch), p in sorted(d.items()): if typ != 4: continue chp = pl["ChannelSpecificParams"][f"Channel{ch}"] channels += 2 spkrs += f""" [Speaker/{chp["SpeakerName"]}] group = {chp["SpeakerGroup"]} tr_coil = {p["T_sett_vc"]:.2f} tr_magnet = {p["T_sett_mg"]:.2f} tau_coil = {p["tau_Tvc"]:.2f} tau_magnet = {p["tau_Tmg"]:.2f} t_limit = {p["temperature limit"]:.1f} t_headroom = {p["hard temp limit headroom"]:.1f} z_nominal = {p["Reb_ref"]:.2f} z_shunt = {p["Rshunt"]:.2f} a_t_20c = {chp["CL"]["a_t_20C"]:.8f} a_t_35c = {chp["CL"]["a_t_35C"]:.8f} is_scale = 3.75 vs_scale = 14 is_chan = {2 * ch} vs_chan = {2 * ch + 1}""" print(f"""\ [Globals] visense_pcm = 2 t_ambient = {gbl["ambient temperature"]} t_hysteresis = 5.0 t_window = 20.0 channels = {channels} period = 4096 link_gains = True uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume{spkrs}""") def process_atsp(e): # print(e) d = dump_audata(LABELS["atsp"], e["aupreset"]["data"])[(0,0)] t_ambient = None spkrs = "" channels = 0 for gid, gn in enumerate("ABC"): p = f"speakerType {gn}: " ch = int(d[p + "Audio channel assignment"]) if not ch: continue if ch == 0xffff: ch = 1 ambient = d[p + "Ambient temperature, [C]"] assert t_ambient is None or t_ambient == ambient t_ambient = ambient for i in range(16): if ch & (1 << i): channels += 2 spkrs += f""" [Speaker/{gn}_ch{i}] group = {gid} tr_coil = {d[p + "VoiceCoil: thermal resistance [C/Watt]"]:.2f} tr_magnet = {d[p + "Magnet: thermal resistance [C/Watt]"]:.2f} tau_coil = {d[p + "Voice Coil: thermal time constant [s]"]:.2f} tau_magnet = {d[p + "Magnet: thermal time constant [s]"]:.2f} t_limit = {d[p + "Temperature limit [C]"]:.1f} t_headroom = {d[p + "Temperature hard limit headroom [C]"]:.1f} z_nominal = {d[p + "VoiceCoil: DC resistance [Ohms]"]:.2f} a_t_20c = 0.0037 a_t_35c = 0.0037 is_scale = 3.75 vs_scale = 14 is_chan = {2 * i} vs_chan = {2 * i + 1}""" print(f"""\ [Globals] visense_pcm = 2 t_ambient = {t_ambient} t_hysteresis = 5.0 t_window = 20.0 channels = {channels} period = 4096 link_gains = {bool(d["Gain link all audio channels"])} uclamp_max = 64 [Controls] vsense = VSENSE Switch isense = ISENSE Switch amp_gain = Amp Gain Volume volume = Speaker Volume{spkrs}""") if __name__ == "__main__": base = sys.argv[1] au = plistlib.load(open(os.path.join(base, "DSP/Strips/builtin_speaker_out_general.austrip"), "rb")) try: prop = plistlib.load(open(os.path.join(base, "DSP/Strips/builtin_speaker_out_general.propstrip"), "rb")) except: prop = None for s in au["strips"]: for e in s["effects"]: if e["unit"]["subtype"].to_bytes(4) == b"spp3": process_spp3(e) if e["unit"]["subtype"].to_bytes(4) == b"atsp": process_atsp(e) speakersafetyd-1.0.0/docs/speakers.txt000064400000000000000000000021541046102023000161440ustar 00000000000000Speaker configs Model ID Amp Gain Speakers Sense Prot j180 AID19 sn012776 10 1× 1W+1T no atsp j274 AID6 tas5770 20 1× 1W no atsp j293 AID3 tas5770 15 2× 2W no atsp j313 AID4 tas5770 10 2× 1W no atsp j314 AID8 sn012776 15 2× 2W+1T yes spp3 j316 AID9 sn012776 15 2× 2W+1T yes spp3 j375 AID10 sn012776 20 1× 1W no atsp j413 AID13 sn012776 15 2× 1W+1T yes spp3 j414 AID14 sn012776 15 2× 2W+1T yes spp3 j415 AID27 sn012776 15 2× 2W+1T yes spp3 j416 AID15 sn012776 15 2× 2W+1T yes spp3 j456 AID5 ssm3515 15 2× 1W+1T no atsp j457 AID7 ssm3515 15 2× 1W+1T no atsp j473 AID12 sn012776 20 1× 1W no atsp j474 AID26 sn012776 20 1× 1W no atsp j475 AID25 sn012776 20 1× 1W no atsp j493 AID18 sn012776 15 2× 2W unused? atsp speakersafetyd-1.0.0/speakersafetyd.service000064400000000000000000000004471046102023000172350ustar 00000000000000[Unit] Description=Speaker Protection Daemon [Service] Type=simple ExecStart=/usr/bin/speakersafetyd -c /usr/share/speakersafetyd/ -b /var/lib/speakersafetyd/blackbox -m 7 UMask=0066 Restart=on-failure RestartSec=1 StartLimitInterval=60 StartLimitBurst=10 [Install] WantedBy=multi-user.target speakersafetyd-1.0.0/speakersafetyd.tmpfiles000064400000000000000000000000641046102023000174130ustar 00000000000000d /var/lib/speakersafetyd/blackbox 0755 root root - speakersafetyd-1.0.0/src/blackbox.rs000064400000000000000000000066431046102023000155670ustar 00000000000000use crate::types::SpeakerState; use chrono; use log::warn; use std::fs::File; use std::io; use std::io::Write; use std::path::Path; use std::slice; use json::object; struct Block { sample_rate: i32, state: Vec>, data: Vec, } pub struct Blackbox { machine: String, globals: crate::types::Globals, path: Box, blocks: Vec, } /// Maximum number of blocks in the ring buffer (around 30 seconds at 4096/48000) const MAX_BLOCKS: usize = 330; impl Blackbox { pub fn new(machine: &str, path: &Path, globals: &crate::types::Globals) -> Blackbox { Blackbox { machine: machine.into(), globals: globals.clone(), path: path.into(), blocks: Vec::new(), } } pub fn reset(&mut self) { self.blocks.clear(); } pub fn push(&mut self, sample_rate: i32, data: Vec, state: Vec>) { while self.blocks.len() >= MAX_BLOCKS { self.blocks.remove(0); } self.blocks.push(Block { sample_rate, state, data, }) } pub fn preserve(&mut self, reason: String) -> io::Result<()> { if self.blocks.is_empty() { warn!("Blackbox is empty, nothing to save"); return Ok(()); } let now = chrono::Local::now().to_rfc3339(); let meta_name = self.path.join(now.clone() + ".fdr"); let data_name = self.path.join(now.clone() + ".cvr"); warn!("Preserving blackbox {}", now); let mut metafd = File::create(meta_name)?; let mut datafd = File::create(data_name)?; for blk in self.blocks.iter() { // meh unsafe let slice_u8: &[u8] = unsafe { slice::from_raw_parts( blk.data.as_ptr() as *const u8, blk.data.len() * std::mem::size_of::(), ) }; datafd.write_all(slice_u8)?; } let mut meta = object! { message: reason, machine: self.machine.clone(), sample_rate: self.blocks[0].sample_rate, channels: self.globals.channels, t_ambient: self.globals.t_ambient, t_window: self.globals.t_window, t_hysteresis: self.globals.t_hysteresis, blocks: null }; let mut blocks = json::JsonValue::new_array(); for block in self.blocks.iter() { let mut info = object! { sample_rate: block.sample_rate, sample_count: block.data.len() / self.globals.channels, speakers: null, }; let mut speakers = json::JsonValue::new_array(); for group in block.state.iter() { for speaker in group.iter() { let _ = speakers.push(object! { t_coil: speaker.t_coil, t_magnet: speaker.t_magnet, t_coil_hyst: speaker.t_coil_hyst, t_magnet_hyst: speaker.t_magnet_hyst, min_gain: speaker.min_gain, gain: speaker.gain, }); } } info["speakers"] = speakers; let _ = blocks.push(info); } meta["blocks"] = blocks; metafd.write_all(meta.dump().as_bytes())?; Ok(()) } } speakersafetyd-1.0.0/src/helpers.rs000064400000000000000000000126371046102023000154440ustar 00000000000000// SPDX-License-Identifier: MIT // (C) 2022 The Asahi Linux Contributors use alsa; use alsa::mixer::MilliBel; use configparser::ini::Ini; pub fn open_card(card: &str) -> alsa::ctl::Ctl { let ctldev: alsa::ctl::Ctl = match alsa::ctl::Ctl::new(card, false) { Ok(ctldev) => ctldev, Err(e) => { panic!("{}: Could not open sound card! Error: {}", card, e); } }; return ctldev; } pub fn open_pcm(dev: &str, chans: u32, mut 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(); let rate_max = params.get_rate_max().unwrap(); let rate_min = params.get_rate_min().unwrap(); println!("PCM rate: {}..{}", rate_min, rate_max); if sample_rate == 0 { sample_rate = rate_min; } 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(¶ms).unwrap(); } return pcm; } /** Wrapper around configparser::ini::Ini.getint() to safely unwrap the Result, E> returned by it. */ pub fn parse_int>(config: &Ini, section: &str, key: &str) -> T where >::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") } pub fn parse_opt_int>(config: &Ini, section: &str, key: &str) -> Option where >::Error: std::fmt::Debug, { config .getint(section, key) .expect(&format!("{}/{}: Invalid value", section, key)) .map(|a| a.try_into().expect("{}/{}: Out of bounds")) } /** Wrapper around configparser::ini::Ini.getfloat() to safely unwrap the Result, E> returned by it. */ pub fn parse_float(config: &Ini, section: &str, key: &str) -> f32 { 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 } /** Wrapper around configparser::ini::Ini.getfloat() to safely unwrap the Result, E> returned by it. */ pub fn parse_string(config: &Ini, section: &str, key: &str) -> String { config .get(section, key) .expect(&format!("{}/{}: Missing key", section, key)) } /** Wrapper around alsa::ctl::ElemValue::new(). Lets us bail on errors and pass in the Bytes type for V/ISENSE */ pub fn new_elemvalue(t: alsa::ctl::ElemType) -> alsa::ctl::ElemValue { let val = match alsa::ctl::ElemValue::new(t) { Ok(val) => val, Err(_e) => { panic!("Could not open a handle to an element!"); } }; 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<()> Ok(val) => val, Err(e) => { panic!( "Could not read elem value {}. alsa-lib error: {:?}", name, e ); } }; } /** 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<()> Ok(val) => val, Err(e) => { panic!( "Could not write elem value {}. alsa-lib error: {:?}", name, e ); } }; } /** Wrapper for alsa::ctl::Ctl::elem_write(). */ pub fn get_range_db( card: &alsa::ctl::Ctl, el: &alsa::ctl::ElemId, name: &str, ) -> (MilliBel, MilliBel) { match card.get_db_range(el) { // alsa:Result<()> Ok(val) => val, Err(e) => { panic!( "Could not get elem db range {}. alsa-lib error: {:?}", name, e ); } } } /** Wrapper for alsa::ctl::Ctl::elem_read(). */ pub fn lock_el(card: &alsa::ctl::Ctl, el: &alsa::ctl::ElemId, name: &str) { let _val = match card.elem_lock(el) { // alsa:Result<()> Ok(val) => val, Err(e) => { panic!("Could not lock elem {}. alsa-lib error: {:?}", name, e); } }; } pub fn int_to_db(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: i32) -> MilliBel { let db = match card.convert_to_db(id, val.into()) { Ok(inner) => inner, Err(e) => { panic!( "Could not convert val {} to dB! alsa-lib error: {:?}", val, e ); } }; return db; } pub fn db_to_int(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: f32) -> i32 { let mb: MilliBel = MilliBel((val * 100.0) as i64); let new_int = match card.convert_from_db(id, mb, alsa::Round::Floor) { Ok(inner) => inner as i32, Err(e) => { panic!( "Could not convert MilliBel {:?} to int! alsa-lib error: {:?}", val, e ); } }; return new_int; } speakersafetyd-1.0.0/src/main.rs000064400000000000000000000312321046102023000147160ustar 00000000000000// 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 kernel keeps the speakers capped at a low volume level until this daemon initializes. If at any time we run into an unrecoverable error or a timeout, we panic and let the kernel put the speakers back into a safe state. */ use std::collections::BTreeMap; use std::fs; use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Instant; use alsa::nix::errno::Errno; use clap::Parser; use clap_verbosity_flag::{InfoLevel, Verbosity}; use configparser::ini::Ini; use log; use log::{debug, info, warn}; use simple_logger::SimpleLogger; mod blackbox; mod helpers; mod types; mod uclamp; const DEFAULT_CONFIG_PATH: &str = "share/speakersafetyd"; const UNLOCK_MAGIC: i32 = 0xdec1be15u32 as i32; const FLAGFILE: &str = "/run/speakersafetyd.flag"; /// 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, /// Increase the log level #[command(flatten)] verbose: Verbosity, /// Path to the blackbox dump directory #[arg(short, long)] blackbox_path: Option, /// Maximum gain reduction before panicing (for debugging) #[arg(short, long)] max_reduction: Option, } fn get_machine() -> String { fs::read_to_string("/proc/device-tree/compatible") .expect("Could not read device tree compatible") .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 { config .sections() .iter() .filter_map(|a| a.strip_prefix("Speaker/")) .map(|a| a.to_string()) .collect() } struct SpeakerGroup { speakers: Vec, gain: f32, } impl Default for SpeakerGroup { fn default() -> Self { Self { speakers: Default::default(), gain: f32::NAN, } } } fn main() { let args = Options::parse(); let sigquit = Arc::new(AtomicBool::new(false)); signal_hook::flag::register(signal_hook::consts::SIGQUIT, Arc::clone(&sigquit)).unwrap(); // signal_hook insists on using SA_RESTART, which we don't want. Override it. unsafe { let mut act: libc::sigaction = core::mem::zeroed(); assert!(libc::sigaction(signal_hook::consts::SIGQUIT, core::ptr::null(), &mut act) == 0); act.sa_flags &= !libc::SA_RESTART; assert!( libc::sigaction( signal_hook::consts::SIGQUIT, &mut act, core::ptr::null_mut() ) == 0 ); } 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 machine: String = get_machine(); info!("Machine: {}", machine); let (maker, model) = machine .split_once(",") .expect("Unexpected machine name format"); config_path.push(&maker); config_path.push(&model); config_path.set_extension("conf"); info!("Config file: {:?}", config_path); let maker_titlecase = maker[0..1].to_ascii_uppercase() + &maker[1..]; let device = format!("hw:{}{}", maker_titlecase, model.to_ascii_uppercase()); info!("Device: {}", device); let mut cfg: Ini = Ini::new_cs(); cfg.load(config_path).expect("Failed to read config file"); let globals = types::Globals::parse(&cfg); if globals.uclamp_min.is_some() || globals.uclamp_max.is_some() { uclamp::set_uclamp( globals.uclamp_min.unwrap_or(0).try_into().unwrap(), globals.uclamp_max.unwrap_or(1024).try_into().unwrap(), ); } let mut blackbox = args.blackbox_path.map(|p| { info!("Enabling blackbox, path: {:?}", p); blackbox::Blackbox::new(&machine, &p, &globals) }); let mut blackbox_ref = AssertUnwindSafe(&mut blackbox); let result = catch_unwind(move || { 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 flag_path = Path::new(FLAGFILE); let cold_boot = match flag_path.try_exists() { Ok(true) => { info!("Startup mode: Warm boot"); false } Ok(false) => { info!("Startup mode: Cold boot"); if fs::write(flag_path, b"started").is_err() { warn!("Failed to write flag file, continuing as warm boot"); false } else { true } } Err(_) => { warn!("Failed to test flag file, continuing as warm boot"); false } }; let mut groups: BTreeMap = BTreeMap::new(); for i in speaker_names { let speaker: types::Speaker = types::Speaker::new(&globals, &i, &cfg, &ctl, cold_boot); groups .entry(speaker.group) .or_default() .speakers .push(speaker); } 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 mut pcm: Option = Some(helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 0)); let mut io = Some(pcm.as_ref().unwrap().io_i16().unwrap()); let mut sample_rate_elem = types::Elem::new( "Speaker Sample Rate".to_string(), &ctl, alsa::ctl::ElemType::Integer, ); let mut sample_rate = sample_rate_elem.read_int(&ctl); let mut unlock_elem = types::Elem::new( "Speaker Volume Unlock".to_string(), &ctl, alsa::ctl::ElemType::Integer, ); unlock_elem.write_int(&ctl, UNLOCK_MAGIC); for (_idx, group) in groups.iter_mut() { if cold_boot { // Preset the gains to no reduction on cold boot group.speakers.iter_mut().for_each(|s| s.update(&ctl, 0.0)); group.gain = 0.0; } else { // Leave the gains at whatever the kernel limit is, use anything // random for group.gain so the gains will update on the first cycle. group.gain = -999.0; } } let mut last_update = Instant::now(); let mut buf = Vec::new(); buf.resize(globals.period * globals.channels, 0i16); let mut once_nominal = false; loop { if sigquit.load(Ordering::Relaxed) { panic!("SIGQUIT received"); } // Block while we're reading into the buffer let read = io.as_ref().unwrap().readi(&mut buf); #[allow(unused_mut)] #[allow(unused_assignments)] let read = match read { Ok(a) => Ok(a), Err(e) => { if sigquit.load(Ordering::Relaxed) { panic!("SIGQUIT received"); } if e.errno() == Errno::ESTRPIPE { warn!("Suspend detected!"); /* // Resume handling loop { match pcm.resume() { Ok(_) => break Ok(0), Err(e) if e.errno() == Errno::EAGAIN => continue, Err(e) => break Err(e), } } .unwrap(); warn!("Resume successful"); */ // Work around kernel issue: resume sometimes breaks visense warn!("Reinitializing PCM to work around kernel bug..."); io = None; pcm = None; pcm = Some(helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 0)); io = Some(pcm.as_ref().unwrap().io_i16().unwrap()); continue; } Err(e) } } .unwrap(); if read != globals.period { warn!("Expected {} samples, got {}", globals.period, read); } if sigquit.load(Ordering::Relaxed) { panic!("SIGQUIT received"); } let buf_read = &buf[0..read * globals.channels]; let cur_sample_rate = sample_rate_elem.read_int(&ctl); if cur_sample_rate != 0 { if cur_sample_rate != sample_rate { sample_rate = cur_sample_rate; info!("Sample rate: {}", sample_rate); blackbox_ref.as_mut().map(|bb| bb.reset()); } } if sample_rate == 0 { panic!("Invalid sample rate"); } let now = Instant::now(); let dt = (now - last_update).as_secs_f64(); assert!(dt > 0f64); let pt = globals.period as f64 / sample_rate as f64; /* If we skipped at least 4 periods, run catchup for that minus one */ if dt > (4f64 * pt) { let skip = dt - pt; debug!("Skipping {:.2} seconds", skip); for (_, group) in groups.iter_mut() { group.speakers.iter_mut().for_each(|s| s.skip_model(skip)); } blackbox_ref.as_mut().map(|bb| bb.reset()); } last_update = now; if let Some(bb) = blackbox_ref.as_mut() { let max_idx = *groups.iter().map(|g| g.0).max().unwrap(); let gstates = (0..=max_idx) .map(|i| groups[&i].speakers.iter().map(|s| s.s.clone()).collect()) .collect(); bb.push(sample_rate, buf_read.to_vec(), gstates); } let mut all_nominal = true; for (idx, group) in groups.iter_mut() { let gain = group .speakers .iter_mut() .map(|s| s.run_model(buf_read, sample_rate as f32)) .reduce(f32::min) .unwrap(); if gain != group.gain { if gain == 0. { info!("Speaker group {} gain nominal", idx); } else { info!("Speaker group {} gain limited to {:.2} dBFS", idx, gain); } group.speakers.iter_mut().for_each(|s| s.update(&ctl, gain)); group.gain = gain; } if gain != 0. { all_nominal = false; } if let Some(max_reduction) = args.max_reduction { if once_nominal && gain < -max_reduction { panic!("Gain reduction exceeded threshold"); } } } if all_nominal { once_nominal = true; } unlock_elem.write_int(&ctl, UNLOCK_MAGIC); } }); if let Err(e) = result { warn!("Panic!"); let mut reason: String = "Unknown panic".into(); if let Some(s) = e.downcast_ref::<&'static str>() { reason = (*s).into(); } else if let Some(s) = e.downcast_ref::() { reason = s.clone(); } blackbox.as_mut().map(|bb| { if bb.preserve(reason).is_err() { warn!("Failed to write blackbox"); } }); resume_unwind(e); } } speakersafetyd-1.0.0/src/types.rs000064400000000000000000000333551046102023000151460ustar 00000000000000// SPDX-License-Identifier: MIT // (C) 2022 The Asahi Linux Contributors use alsa::ctl::Ctl; use configparser::ini::Ini; use log::{debug, info}; use std::ffi::{CStr, CString}; use crate::helpers; /** Struct with fields necessary for manipulating an ALSA elem. The val field is created using a wrapper so that we can handle any errors. */ pub struct Elem { elem_name: String, id: alsa::ctl::ElemId, val: alsa::ctl::ElemValue, } impl Elem { pub 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 { 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(); new_elem.id.set_name(cstr); new_elem.val.set_id(&new_elem.id); helpers::lock_el(card, &new_elem.id, &new_elem.elem_name); helpers::read_ev(card, &mut new_elem.val, &new_elem.elem_name); return new_elem; } pub fn read_int(&mut self, card: &Ctl) -> i32 { helpers::read_ev(card, &mut self.val, &self.elem_name); self.val .get_integer(0) .expect(&format!("Could not read {}", self.elem_name)) } pub fn write_int(&mut self, card: &Ctl, value: i32) { self.val .set_integer(0, value) .expect(&format!("Could not set {}", self.elem_name)); helpers::write_ev(card, &mut self.val, &self.elem_name); } } /** Mixer struct representing the controls associated with a given Speaker. Populated with the important ALSA controls at runtime. level: mixer volume control vsense: VSENSE switch isense: ISENSE switch */ struct Mixer { drv: String, level: Elem, amp_gain: Elem, } impl Mixer { // TODO: implement turning on V/ISENSE fn new(name: &str, card: &Ctl, globals: &Globals) -> Mixer { let prefix = if name == "Mono" { "".to_string() } else { name.to_owned() + " " }; let mut vs = Elem::new( prefix.clone() + &globals.ctl_vsense, card, alsa::ctl::ElemType::Boolean, ); 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( prefix.clone() + &globals.ctl_isense, 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()); let mut ret = Mixer { drv: name.to_owned(), level: Elem::new( prefix.clone() + &globals.ctl_volume, card, alsa::ctl::ElemType::Integer, ), amp_gain: Elem::new( prefix + &globals.ctl_amp_gain, card, alsa::ctl::ElemType::Integer, ), }; /* * Set amp gain to max available (kernel should've clamped). * alsa-rs only has bindings for range in dB, so we go through * that. */ let (_min, max) = helpers::get_range_db(card, &mut ret.amp_gain.id, &ret.amp_gain.elem_name); let max_int = card .convert_from_db(&mut ret.amp_gain.id, max, alsa::Round::Floor) .unwrap(); ret.amp_gain.val.set_integer(0, max_int.try_into().unwrap()); helpers::write_ev(card, &ret.amp_gain.val, &ret.amp_gain.elem_name); ret } 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 = self .level .val .get_integer(0) .expect(&format!("Could not read level for {}", self.drv)); 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(_) => {} None => { panic!("Could not set level for {}", self.drv); } }; helpers::write_ev(card, &self.level.val, &self.level.elem_name); } } #[derive(Clone)] pub struct Globals { pub visense_pcm: usize, pub channels: usize, pub period: usize, pub t_ambient: f32, pub t_window: f32, pub t_hysteresis: f32, pub ctl_vsense: String, pub ctl_isense: String, pub ctl_amp_gain: String, pub ctl_volume: String, pub uclamp_min: Option, pub uclamp_max: Option, } 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_window: helpers::parse_float(config, "Globals", "t_window"), t_hysteresis: helpers::parse_float(config, "Globals", "t_hysteresis"), ctl_vsense: helpers::parse_string(config, "Controls", "vsense"), ctl_isense: helpers::parse_string(config, "Controls", "isense"), ctl_amp_gain: helpers::parse_string(config, "Controls", "amp_gain"), ctl_volume: helpers::parse_string(config, "Controls", "volume"), uclamp_min: helpers::parse_opt_int(config, "Globals", "uclamp_min"), uclamp_max: helpers::parse_opt_int(config, "Globals", "uclamp_max"), } } } /** Struct representing a driver. Parameters are parsed out of a config file, which is loaded at runtime based on the machine's DT compatible string. name: driver name as it appears in ALSA alsa_iface: Mixer struct with handles to the driver's control elements r_dc: dc resistance of the voice coil (ohms) tau_coil: voice coil ramp time constant (seconds) tau_magnet: magnet ramp time constant (seconds) tr_coil: thermal resistance of voice coil (*C/W) t_limit: absolute max temp of the voice coil (*C) Borrows the handle to the control interface to do calculations. */ #[derive(Debug, Default, Copy, Clone)] pub struct SpeakerState { pub t_coil: f64, pub t_magnet: f64, pub t_coil_hyst: f32, pub t_magnet_hyst: f32, pub min_gain: f32, pub gain: f32, } pub struct Speaker { pub name: String, pub group: usize, alsa_iface: Mixer, tau_coil: f32, tau_magnet: f32, tr_coil: f32, 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, pub s: SpeakerState, } impl Speaker { pub fn new(globals: &Globals, name: &str, config: &Ini, ctl: &Ctl, cold_boot: bool) -> Speaker { info!("Speaker [{}]:", name); let section = "Speaker/".to_owned() + name; let mut new_speaker: Speaker = Speaker { name: name.to_string(), alsa_iface: Mixer::new(&name, ctl, globals), group: helpers::parse_int(config, §ion, "group"), tau_coil: helpers::parse_float(config, §ion, "tau_coil"), tau_magnet: helpers::parse_float(config, §ion, "tau_magnet"), tr_coil: helpers::parse_float(config, §ion, "tr_coil"), tr_magnet: helpers::parse_float(config, §ion, "tr_magnet"), t_limit: helpers::parse_float(config, §ion, "t_limit"), t_headroom: helpers::parse_float(config, §ion, "t_headroom"), z_nominal: helpers::parse_float(config, §ion, "z_nominal"), is_scale: helpers::parse_float(config, §ion, "is_scale"), vs_scale: helpers::parse_float(config, §ion, "vs_scale"), is_chan: helpers::parse_int(config, §ion, "is_chan"), vs_chan: helpers::parse_int(config, §ion, "vs_chan"), g: globals.clone(), s: Default::default(), }; let s = &mut new_speaker.s; s.t_coil = if cold_boot { // Assume warm but not warm enough to limit (new_speaker.t_limit - globals.t_window) as f64 - 1f64 } else { // Worst case startup assumption new_speaker.t_limit as f64 }; s.t_magnet = globals.t_ambient as f64 + (s.t_coil - globals.t_ambient as f64) * (new_speaker.tr_magnet / (new_speaker.tr_magnet + new_speaker.tr_coil)) as f64; let max_dt = new_speaker.t_limit - 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 - globals.t_window > globals.t_ambient); 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 } pub fn run_model(&mut self, buf: &[i16], sample_rate: f32) -> f32 { let s = &mut self.s; 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 + self.t_headroom) as f64 { panic!( "{}: Coil temperature limit exceeded ({} > {})", self.name, s.t_coil, self.t_limit ); } if s.t_magnet > (self.t_limit + self.t_headroom) as f64 { panic!( "{}: Magnet temperature limit exceeded ({} > {})", self.name, s.t_magnet, self.t_limit ); } pwr_sum += p; } let pwr_avg: f32 = pwr_sum / ((buf.len() / self.g.channels) as f32); /* * This really shouldn't happen other than rounding error, * if it does there's probably something wrong with the ivsense * data. */ if pwr_avg < -0.01 { panic!( "{}: Negative power, bad ivsense data? ({})", self.name, pwr_avg ); } let pwr_avg = pwr_avg.max(0.0); 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.t_limit - self.g.t_window)) / self.g.t_window; let gain = s.min_gain * reduction.max(0.); s.gain = gain; if s.gain > -0.01 { s.gain = 0.; } debug!( "{:>15}: Coil {:>6.2} °C Magnet {:>6.2} °C Power {:>5.2} W Gain {:>6.2} dB", self.name, s.t_coil, s.t_magnet, pwr_avg, s.gain ); s.gain } pub fn skip_model(&mut self, time: f64) { let s = &mut self.s; let t_coil = s.t_coil - self.g.t_ambient as f64; let t_magnet = s.t_magnet - self.g.t_ambient as f64; let eta = 1f64 / (1f64 - (self.tau_coil / self.tau_magnet) as f64); let a = (-time / self.tau_coil as f64).exp() * (t_coil - eta * t_magnet); let b = (-time / self.tau_magnet as f64).exp() * t_magnet; s.t_coil = self.g.t_ambient as f64 + a + b * eta; s.t_magnet = self.g.t_ambient as f64 + b; debug!( "{}: SKIP: Coil {:.2} °C Magnet {:.2} °C ({:.2} seconds)", self.name, s.t_coil, s.t_magnet, time ); } pub fn update(&mut self, ctl: &Ctl, gain: f32) { self.alsa_iface.set_lvl(ctl, gain); } } speakersafetyd-1.0.0/src/uclamp.rs000064400000000000000000000022441046102023000152540ustar 00000000000000use log::{info, warn}; #[derive(Default)] #[repr(C)] struct SchedAttr { size: u32, sched_policy: u32, sched_flags: u64, sched_nice: i32, sched_priority: u32, sched_runtime: u64, sched_deadline: u64, sched_period: u64, sched_util_min: u32, sched_util_max: u32, } pub fn set_uclamp(uclamp_min: u32, uclamp_max: u32) { let mut attr: SchedAttr = Default::default(); let pid = unsafe { libc::getpid() }; if unsafe { libc::syscall( libc::SYS_sched_getattr, pid, &mut attr, core::mem::size_of::(), 0, ) } != 0 { warn!("Failed to set uclamp"); return; } /* SCHED_FLAG_KEEP_POLICY | * SCHED_FLAG_KEEP_PARAMS | * SCHED_FLAG_UTIL_CLAMP_MIN | * SCHED_FLAG_UTIL_CLAMP_MAX */ attr.sched_flags = 0x8 | 0x10 | 0x20 | 0x40; attr.sched_util_min = uclamp_min; attr.sched_util_max = uclamp_max; if unsafe { libc::syscall(libc::SYS_sched_setattr, pid, &mut attr, 0) } != 0 { warn!("Failed to set uclamp"); return; } info!("Set task uclamp to {}:{}", uclamp_min, uclamp_max); } speakersafetyd-1.0.0/testing/analyze.py000064400000000000000000000173341046102023000163360ustar 00000000000000import json, sys, os.path, configparser import numpy as np import matplotlib.pyplot as plt from scipy.signal import butter, sosfilt, freqz CONFDIR = os.path.join(os.path.dirname(__file__), "../conf") # This information is not in the blackbox file DEFAULT_AMP_GAIN = 18.50 AMP_GAIN = { "apple,j180": 16.0, "apple,j313": 16.0, "apple,j274": 21.0, "apple,j375": 21.0, "apple,j473": 21.0, "apple,j474": 21.0, "apple,j475": 21.0, } def db(x): return 10 ** (x / 20) def smooth(a, n=3): l = len(a) ret = np.cumsum(a, dtype=float) ret[n:] = ret[n:] - ret[:-n] ret = ret[n - 1:] / n pad = l - len(ret) return np.pad(ret, (pad//2, (pad + 1)//2), "edge") def butter_lowpass(cutoff, fs, order=5): return butter(order, cutoff, fs=fs, btype='low', output="sos", analog=False) def butter_highpass(cutoff, fs, order=5): return butter(order, cutoff, fs=fs, btype='high', output="sos", analog=False) def butter_lowpass_filter(data, cutoff, fs, order=5): sos = butter_lowpass(cutoff, fs, order=order) y = sosfilt(sos, data) return y def butter_highpass_filter(data, cutoff, fs, order=5): sos = butter_highpass(cutoff, fs, order=order) y = sosfilt(sos, data) return y def pilot_filter(data, fs): data = butter_lowpass_filter(data, 100, fs, 6) return butter_highpass_filter(data, 10, fs, 3) class Model: def __init__(self, idx, an, name, conf): self.idx = idx self.an = an self.name = name self.conf = conf self.tr_coil = float(conf["tr_coil"]) self.tr_magnet = float(conf["tr_magnet"]) self.tau_coil = float(conf["tau_coil"]) self.tau_magnet = float(conf["tau_magnet"]) self.t_limit = float(conf["t_limit"]) self.t_headroom = float(conf["t_headroom"]) self.z_nominal = float(conf["z_nominal"]) self.z_shunt = float(conf.get("z_shunt", 0)) self.is_scale = float(conf["is_scale"]) self.vs_scale = float(conf["vs_scale"]) self.a_t_20c = float(conf["a_t_20c"]) self.a_t_35c = float(conf["a_t_35c"]) self.is_chan = int(conf["is_chan"]) self.vs_chan = int(conf["vs_chan"]) self.t_ambient = an.fdr["t_ambient"] self.t_coil = an.fdr["blocks"][0]["speakers"][self.idx]["t_coil"] self.t_magnet = an.fdr["blocks"][0]["speakers"][self.idx]["t_magnet"] self.m_x = [] self.m_t_coil_tg = [] self.m_t_coil = [] self.m_t_magnet_tg = [] self.m_t_magnet = [] self.l_x = [] self.l_t_coil = [] self.l_t_magnet = [] def run_model(self): off = 0 t = 0 for blk in self.an.fdr["blocks"]: sr = blk["sample_rate"] cnt = blk["sample_count"] data = blk["speakers"][self.idx] isense = self.an.cvr[off:off+cnt, self.is_chan] * self.is_scale vsense = self.an.cvr[off:off+cnt, self.vs_chan] * self.vs_scale dt = 1 / self.an.sr alpha_coil = dt / (dt + self.tau_coil) alpha_magnet = dt / (dt + self.tau_magnet) self.l_x.append(t) self.l_t_coil.append(data["t_coil"]) self.l_t_magnet.append(data["t_magnet"]) for x, (i, v) in enumerate(zip(isense, vsense)): self.m_x.append(t + x / sr) p = i * v tvc_tgt = self.t_magnet + p * self.tr_coil self.t_coil = tvc_tgt * alpha_coil + self.t_coil * (1 - alpha_coil) tmag_tgt = self.t_ambient + p * self.tr_magnet self.t_magnet = tmag_tgt * alpha_magnet + self.t_magnet * (1 - alpha_magnet) self.m_t_coil_tg.append(tvc_tgt) self.m_t_coil.append(self.t_coil) self.m_t_magnet_tg.append(tmag_tgt) self.m_t_magnet.append(self.t_magnet) t += cnt / sr off += cnt def analyze(self, outfile): plt.clf() fig, ax1 = plt.subplots(figsize=(30,15)) ax1.set_title(self.name) ax1.set_xlabel('time (s)') ax1.set_ylabel('temperature') ax1.tick_params(axis='y') ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis color = 'tab:red' ax2.set_ylabel('power', color=color) ax2.tick_params(axis='y', labelcolor=color) ax1.plot(self.m_x, self.m_t_coil, "r") # ax1.plot(self.m_x, smooth(self.m_t_coil_tg, 1000), "y") ax1.plot(self.m_x, self.m_t_magnet, "b") # ax1.plot(self.m_x, smooth(self.m_t_magnet_tg, 1000), "c") ax1.plot(self.l_x, self.l_t_coil, "om") ax1.plot(self.l_x, self.l_t_magnet, "og") i = self.an.cvr[:, self.is_chan] * self.is_scale v = self.an.cvr[:, self.vs_chan] * self.vs_scale sr = self.an.fdr["sample_rate"] ilp = pilot_filter(i, sr) vlp = pilot_filter(v, sr) p = butter_lowpass_filter(i * v, 10, sr, 1) p = smooth(p, 4000) plp = butter_lowpass_filter(ilp * vlp, 10, sr, 1) plp = smooth(plp, 4000) vlprms_sq = butter_lowpass_filter(vlp * vlp, 10, sr, 1) vlprms_sq = smooth(vlprms_sq, 4000) r = vlprms_sq / plp # ax2.plot(self.m_x, p, "b") rref = np.average(r[1 * sr:2 * sr]) print(f"Initial resistance: {rref} ohms") # Clear out the first second, since it tends to contain garbage r[:1*sr] = rref #r = butter_lowpass_filter(r - rref, 2, sr, 2) + rref a = self.a_t_35c # XXX why are there two values at different temperatures? tref = self.l_t_coil[0] t = ((r - self.z_shunt) / (rref - self.z_shunt) - 1) / a + tref ax1.plot(self.m_x, t, "k") ax2.plot(self.m_x, p, "r") # ax2.plot(self.m_x, plp, "g") # ax2.plot(self.m_x, vlprms_sq, "b") for level in (-1000, -6, -10, -15): gain = AMP_GAIN.get(self.an.fdr["machine"], DEFAULT_AMP_GAIN) pbase = (db(gain - 30) ** 2) / (self.z_nominal + self.z_shunt) ptest = (db(gain + level) ** 2) / (self.z_nominal + self.z_shunt) p = pbase + ptest ax2.axhline(y=p, color='r', linestyle='--') fig.tight_layout() # otherwise the right y-label is slightly clipped plt.savefig(outfile) class Analyzer: def __init__(self, base): self.fdr = json.load(open(base + ".fdr")) data = open(base + ".cvr", "rb").read() cvr = np.frombuffer(data, dtype="int16").astype("float") / 32768 maker, model = self.fdr["machine"].split(",") cf = os.path.join(CONFDIR, maker, model + ".conf") print(f"Using config file: {cf}") self.conf = configparser.ConfigParser() self.conf.read(cf) ch = int(self.conf["Globals"]["channels"]) samples = len(cvr) // ch self.cvr = cvr.reshape((samples, ch)) print(f"Got {samples} samples ({ch} channels)") assert ch == self.fdr["channels"] self.sr = self.fdr["sample_rate"] speaker_configs = [] for key in self.conf.sections(): if not key.startswith("Speaker/"): continue print(key) name = key.split("/")[1] speaker_configs.append((name, self.conf[key])) # Match the order that speakersafetyd uses (by group) speaker_configs.sort(key=lambda x: int(x[1]["group"])) self.speakers = [] for i, cfg in enumerate(speaker_configs): self.speakers.append(Model(i, self, cfg[0], cfg[1])) def analyze(self): for i, spk in enumerate(self.speakers): print(f"Processing speaker {i}") spk.run_model() spk.analyze(f"speaker_{i}.png") # break if __name__ == "__main__": a = Analyzer(sys.argv[1]) a.analyze() speakersafetyd-1.0.0/testing/make_test_file.py000064400000000000000000000016271046102023000176440ustar 00000000000000#!/usr/bin/python import scipy, sys import numpy as np ch = int(sys.argv[1]) out = sys.argv[2] FS = 48000 PILOT_DB = -30 PILOT_FREQ = 43 TEST0 = (500, 1, -10) TEST1 = (500, 10, -15) TEST2 = (43, 1.5, -6) TEST3 = (1000, 1.5, -6) def db(x): return 10 ** (x / 20) def silence(t): return np.zeros(int(FS * t)) def sine(f, t, v): space = np.linspace(0, t, int(FS * t), endpoint=False) return np.sin(2 * np.pi * f * space) * db(v) signal = np.concatenate(( silence(3), sine(*TEST0), silence(2), sine(*TEST1), silence(2), sine(*TEST2), silence(2), sine(*TEST3), silence(5) )) space = np.linspace(0, len(signal) / FS, len(signal), endpoint=False) signal += np.sin(2 * np.pi * PILOT_FREQ * space) * db(PILOT_DB) signal = np.concatenate((silence(60), signal)) signal = np.repeat(signal, ch).reshape((-1, ch)) scipy.io.wavfile.write(out, FS, signal.astype("float32"))