├── conf └── apple │ ├── j414.conf │ ├── j416.conf │ ├── j457.conf │ ├── j473.conf │ ├── j474.conf │ ├── j475.conf │ ├── j375.conf │ ├── j274.conf │ ├── j313.conf │ ├── j180.conf │ ├── j493.conf │ ├── j293.conf │ ├── j456.conf │ ├── j413.conf │ ├── j316.conf │ ├── j314.conf │ └── j415.conf ├── .gitignore ├── speakersafetyd.tmpfiles ├── speakersafetyd.service ├── Cargo.toml ├── LICENSE ├── testing ├── make_test_file.py └── analyze.py ├── docs ├── speakers.txt └── audump.py ├── src ├── uclamp.rs ├── blackbox.rs ├── helpers.rs ├── main.rs └── types.rs ├── Makefile ├── 95-speakersafetyd.rules ├── README.md └── Cargo.lock /conf/apple/j414.conf: -------------------------------------------------------------------------------- 1 | j314.conf -------------------------------------------------------------------------------- /conf/apple/j416.conf: -------------------------------------------------------------------------------- 1 | j316.conf -------------------------------------------------------------------------------- /conf/apple/j457.conf: -------------------------------------------------------------------------------- 1 | j456.conf -------------------------------------------------------------------------------- /conf/apple/j473.conf: -------------------------------------------------------------------------------- 1 | j375.conf -------------------------------------------------------------------------------- /conf/apple/j474.conf: -------------------------------------------------------------------------------- 1 | j375.conf -------------------------------------------------------------------------------- /conf/apple/j475.conf: -------------------------------------------------------------------------------- 1 | j375.conf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.kate-swp 3 | */target 4 | testing/*.wav 5 | testing/j* 6 | -------------------------------------------------------------------------------- /speakersafetyd.tmpfiles: -------------------------------------------------------------------------------- 1 | d /var/lib/speakersafetyd 755 speakersafetyd speakersafetyd - 2 | d /var/lib/speakersafetyd/blackbox 0700 speakersafetyd speakersafetyd - 3 | d /run/speakersafetyd 0755 speakersafetyd speakersafetyd - 4 | -------------------------------------------------------------------------------- /speakersafetyd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Speaker Protection Daemon 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/usr/bin/speakersafetyd -c /usr/share/speakersafetyd/ -b /var/lib/speakersafetyd/blackbox -m 7 7 | User=speakersafetyd 8 | AmbientCapabilities=CAP_SYS_NICE 9 | CapabilityBoundingSet=CAP_SYS_NICE 10 | UMask=0066 11 | Restart=on-failure 12 | RestartSec=1 13 | StartLimitInterval=60 14 | StartLimitBurst=10 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "speakersafetyd" 3 | version = "1.1.2" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "Speaker protection daemon for embedded Linux systems" 7 | repository = "https://github.com/AsahiLinux/speakersafetyd/" 8 | 9 | [dependencies] 10 | alsa = "^0.9.1" 11 | configparser = { version = "^3.1.0", features=["indexmap"] } 12 | clap = { version = "^4.1.6", features=["derive"] } 13 | log = "^0.4.17" 14 | clap-verbosity-flag = "^2.0.0" 15 | simple_logger = "^4.3.3" 16 | chrono = "^0.4.31" 17 | json = "^0.12.4" 18 | signal-hook = "^0.3.17" 19 | libc = "^0.2.150" 20 | -------------------------------------------------------------------------------- /conf/apple/j375.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 46.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 2 7 | period = 4096 8 | link_gains = False 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Mono] 18 | group = 0 19 | tr_coil = 40.00 20 | tr_magnet = 60.00 21 | tau_coil = 3.70 22 | tau_magnet = 250.00 23 | t_limit = 140.0 24 | t_headroom = 40.0 25 | z_nominal = 4.60 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | -------------------------------------------------------------------------------- /conf/apple/j274.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 46.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 2 7 | period = 4096 8 | link_gains = False 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Playback Volume 16 | 17 | [Speaker/Mono] 18 | group = 0 19 | tr_coil = 40.00 20 | tr_magnet = 60.00 21 | tau_coil = 3.70 22 | tau_magnet = 250.00 23 | t_limit = 140.0 24 | t_headroom = 40.0 25 | z_nominal = 4.60 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | -------------------------------------------------------------------------------- /conf/apple/j313.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 4 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Playback Volume 16 | 17 | [Speaker/Left] 18 | group = 0 19 | tr_coil = 29.00 20 | tr_magnet = 36.00 21 | tau_coil = 2.40 22 | tau_magnet = 80.00 23 | t_limit = 120.0 24 | t_headroom = 15.0 25 | z_nominal = 4.90 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | 33 | [Speaker/Right] 34 | group = 0 35 | tr_coil = 29.00 36 | tr_magnet = 36.00 37 | tau_coil = 2.40 38 | tau_magnet = 80.00 39 | t_limit = 120.0 40 | t_headroom = 15.0 41 | z_nominal = 4.90 42 | a_t_20c = 0.0037 43 | a_t_35c = 0.0037 44 | is_scale = 3.75 45 | vs_scale = 14 46 | is_chan = 2 47 | vs_chan = 3 48 | -------------------------------------------------------------------------------- /conf/apple/j180.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 4 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Woofer] 18 | group = 1 19 | tr_coil = 22.20 20 | tr_magnet = 43.90 21 | tau_coil = 7.40 22 | tau_magnet = 530.00 23 | t_limit = 130.0 24 | t_headroom = 10.0 25 | z_nominal = 3.80 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | 33 | [Speaker/Tweeter] 34 | group = 0 35 | tr_coil = 51.40 36 | tr_magnet = 57.90 37 | tau_coil = 2.10 38 | tau_magnet = 225.00 39 | t_limit = 120.0 40 | t_headroom = 10.0 41 | z_nominal = 3.60 42 | a_t_20c = 0.0037 43 | a_t_35c = 0.0037 44 | is_scale = 3.75 45 | vs_scale = 14 46 | is_chan = 2 47 | vs_chan = 3 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright The Asahi Linux Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /testing/make_test_file.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import scipy, sys 3 | import numpy as np 4 | 5 | ch = int(sys.argv[1]) 6 | out = sys.argv[2] 7 | 8 | FS = 48000 9 | PILOT_DB = -30 10 | PILOT_FREQ = 43 11 | TEST0 = (500, 1, -10) 12 | TEST1 = (500, 10, -15) 13 | TEST2 = (43, 1.5, -6) 14 | TEST3 = (1000, 1.5, -6) 15 | 16 | def db(x): 17 | return 10 ** (x / 20) 18 | 19 | def silence(t): 20 | return np.zeros(int(FS * t)) 21 | 22 | def sine(f, t, v): 23 | space = np.linspace(0, t, int(FS * t), endpoint=False) 24 | return np.sin(2 * np.pi * f * space) * db(v) 25 | 26 | signal = np.concatenate(( 27 | silence(3), 28 | sine(*TEST0), 29 | silence(2), 30 | sine(*TEST1), 31 | silence(2), 32 | sine(*TEST2), 33 | silence(2), 34 | sine(*TEST3), 35 | silence(5) 36 | )) 37 | 38 | space = np.linspace(0, len(signal) / FS, len(signal), endpoint=False) 39 | signal += np.sin(2 * np.pi * PILOT_FREQ * space) * db(PILOT_DB) 40 | 41 | signal = np.concatenate((silence(60), signal)) 42 | 43 | signal = np.repeat(signal, ch).reshape((-1, ch)) 44 | 45 | scipy.io.wavfile.write(out, FS, signal.astype("float32")) 46 | -------------------------------------------------------------------------------- /docs/speakers.txt: -------------------------------------------------------------------------------- 1 | Speaker configs 2 | 3 | Model ID Amp Gain Speakers Sense Prot 4 | j180 AID19 sn012776 10 1× 1W+1T no atsp 5 | j274 AID6 tas5770 20 1× 1W no atsp 6 | j293 AID3 tas5770 15 2× 2W no atsp 7 | j313 AID4 tas5770 10 2× 1W no atsp 8 | j314 AID8 sn012776 15 2× 2W+1T yes spp3 9 | j316 AID9 sn012776 15 2× 2W+1T yes spp3 10 | j375 AID10 sn012776 20 1× 1W no atsp 11 | j413 AID13 sn012776 15 2× 1W+1T yes spp3 12 | j414 AID14 sn012776 15 2× 2W+1T yes spp3 13 | j415 AID27 sn012776 15 2× 2W+1T yes spp3 14 | j416 AID15 sn012776 15 2× 2W+1T yes spp3 15 | j456 AID5 ssm3515 15 2× 1W+1T no atsp 16 | j457 AID7 ssm3515 15 2× 1W+1T no atsp 17 | j473 AID12 sn012776 20 1× 1W no atsp 18 | j474 AID26 sn012776 20 1× 1W no atsp 19 | j475 AID25 sn012776 20 1× 1W no atsp 20 | j493 AID18 sn012776 15 2× 2W unused? atsp 21 | -------------------------------------------------------------------------------- /src/uclamp.rs: -------------------------------------------------------------------------------- 1 | use log::{info, warn}; 2 | 3 | #[derive(Default)] 4 | #[repr(C)] 5 | struct SchedAttr { 6 | size: u32, 7 | sched_policy: u32, 8 | sched_flags: u64, 9 | sched_nice: i32, 10 | sched_priority: u32, 11 | sched_runtime: u64, 12 | sched_deadline: u64, 13 | sched_period: u64, 14 | sched_util_min: u32, 15 | sched_util_max: u32, 16 | } 17 | 18 | pub fn set_uclamp(uclamp_min: u32, uclamp_max: u32) { 19 | let mut attr: SchedAttr = Default::default(); 20 | let pid = unsafe { libc::getpid() }; 21 | 22 | if unsafe { 23 | libc::syscall( 24 | libc::SYS_sched_getattr, 25 | pid, 26 | &mut attr, 27 | core::mem::size_of::(), 28 | 0, 29 | ) 30 | } != 0 31 | { 32 | warn!("Failed to set uclamp"); 33 | return; 34 | } 35 | 36 | /* SCHED_FLAG_KEEP_POLICY | 37 | * SCHED_FLAG_KEEP_PARAMS | 38 | * SCHED_FLAG_UTIL_CLAMP_MIN | 39 | * SCHED_FLAG_UTIL_CLAMP_MAX */ 40 | attr.sched_flags = 0x8 | 0x10 | 0x20 | 0x40; 41 | attr.sched_util_min = uclamp_min; 42 | attr.sched_util_max = uclamp_max; 43 | 44 | if unsafe { libc::syscall(libc::SYS_sched_setattr, pid, &mut attr, 0) } != 0 { 45 | warn!("Failed to set uclamp"); 46 | return; 47 | } 48 | 49 | info!("Set task uclamp to {}:{}", uclamp_min, uclamp_max); 50 | } 51 | -------------------------------------------------------------------------------- /conf/apple/j493.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 8 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Left Front] 18 | group = 0 19 | tr_coil = 38.30 20 | tr_magnet = 49.10 21 | tau_coil = 2.80 22 | tau_magnet = 79.70 23 | t_limit = 130.0 24 | t_headroom = 10.0 25 | z_nominal = 9.70 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | 33 | [Speaker/Right Front] 34 | group = 0 35 | tr_coil = 38.30 36 | tr_magnet = 49.10 37 | tau_coil = 2.80 38 | tau_magnet = 79.70 39 | t_limit = 130.0 40 | t_headroom = 10.0 41 | z_nominal = 9.70 42 | a_t_20c = 0.0037 43 | a_t_35c = 0.0037 44 | is_scale = 3.75 45 | vs_scale = 14 46 | is_chan = 2 47 | vs_chan = 3 48 | 49 | [Speaker/Left Rear] 50 | group = 0 51 | tr_coil = 38.30 52 | tr_magnet = 49.10 53 | tau_coil = 2.80 54 | tau_magnet = 79.70 55 | t_limit = 130.0 56 | t_headroom = 10.0 57 | z_nominal = 9.70 58 | a_t_20c = 0.0037 59 | a_t_35c = 0.0037 60 | is_scale = 3.75 61 | vs_scale = 14 62 | is_chan = 4 63 | vs_chan = 5 64 | 65 | [Speaker/Right Rear] 66 | group = 0 67 | tr_coil = 38.30 68 | tr_magnet = 49.10 69 | tau_coil = 2.80 70 | tau_magnet = 79.70 71 | t_limit = 130.0 72 | t_headroom = 10.0 73 | z_nominal = 9.70 74 | a_t_20c = 0.0037 75 | a_t_35c = 0.0037 76 | is_scale = 3.75 77 | vs_scale = 14 78 | is_chan = 6 79 | vs_chan = 7 80 | -------------------------------------------------------------------------------- /conf/apple/j293.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 8 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Playback Volume 16 | 17 | [Speaker/Left Front] 18 | group = 0 19 | tr_coil = 38.30 20 | tr_magnet = 49.10 21 | tau_coil = 2.80 22 | tau_magnet = 79.70 23 | t_limit = 130.0 24 | t_headroom = 10.0 25 | z_nominal = 9.70 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | is_scale = 3.75 29 | vs_scale = 14 30 | is_chan = 0 31 | vs_chan = 1 32 | 33 | [Speaker/Right Front] 34 | group = 0 35 | tr_coil = 38.30 36 | tr_magnet = 49.10 37 | tau_coil = 2.80 38 | tau_magnet = 79.70 39 | t_limit = 130.0 40 | t_headroom = 10.0 41 | z_nominal = 9.70 42 | a_t_20c = 0.0037 43 | a_t_35c = 0.0037 44 | is_scale = 3.75 45 | vs_scale = 14 46 | is_chan = 2 47 | vs_chan = 3 48 | 49 | [Speaker/Left Rear] 50 | group = 0 51 | tr_coil = 38.30 52 | tr_magnet = 49.10 53 | tau_coil = 2.80 54 | tau_magnet = 79.70 55 | t_limit = 130.0 56 | t_headroom = 10.0 57 | z_nominal = 9.70 58 | a_t_20c = 0.0037 59 | a_t_35c = 0.0037 60 | is_scale = 3.75 61 | vs_scale = 14 62 | is_chan = 4 63 | vs_chan = 5 64 | 65 | [Speaker/Right Rear] 66 | group = 0 67 | tr_coil = 38.30 68 | tr_magnet = 49.10 69 | tau_coil = 2.80 70 | tau_magnet = 79.70 71 | t_limit = 130.0 72 | t_headroom = 10.0 73 | z_nominal = 9.70 74 | a_t_20c = 0.0037 75 | a_t_35c = 0.0037 76 | is_scale = 3.75 77 | vs_scale = 14 78 | is_chan = 6 79 | vs_chan = 7 80 | -------------------------------------------------------------------------------- /conf/apple/j456.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | # NO VISENSE! TODO 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 8 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | # vsense = 13 | # isense = 14 | # amp_gain = 15 | # volume = 16 | 17 | [Speaker/A_ch2] 18 | group = 0 19 | tr_coil = 20.00 20 | tr_magnet = 28.00 21 | tau_coil = 10.00 22 | tau_magnet = 391.00 23 | t_limit = 120.0 24 | t_headroom = 20.0 25 | z_nominal = 7.00 26 | a_t_20c = 0.0037 27 | a_t_35c = 0.0037 28 | # TODO is_scale = 3.75 29 | # TODO vs_scale = 14 30 | # TODO is_chan = 4 31 | # TODO vs_chan = 5 32 | 33 | [Speaker/A_ch3] 34 | group = 0 35 | tr_coil = 20.00 36 | tr_magnet = 28.00 37 | tau_coil = 10.00 38 | tau_magnet = 391.00 39 | t_limit = 120.0 40 | t_headroom = 20.0 41 | z_nominal = 7.00 42 | a_t_20c = 0.0037 43 | a_t_35c = 0.0037 44 | # TODO is_scale = 3.75 45 | # TODO vs_scale = 14 46 | # TODO is_chan = 6 47 | # TODO vs_chan = 7 48 | 49 | [Speaker/B_ch0] 50 | group = 1 51 | tr_coil = 46.00 52 | tr_magnet = 76.00 53 | tau_coil = 1.30 54 | tau_magnet = 115.00 55 | t_limit = 120.0 56 | t_headroom = 40.0 57 | z_nominal = 5.50 58 | a_t_20c = 0.0037 59 | a_t_35c = 0.0037 60 | # TODO is_scale = 3.75 61 | # TODO vs_scale = 14 62 | # TODO is_chan = 0 63 | # TODO vs_chan = 1 64 | 65 | [Speaker/B_ch1] 66 | group = 1 67 | tr_coil = 46.00 68 | tr_magnet = 76.00 69 | tau_coil = 1.30 70 | tau_magnet = 115.00 71 | t_limit = 120.0 72 | t_headroom = 40.0 73 | z_nominal = 5.50 74 | a_t_20c = 0.0037 75 | a_t_35c = 0.0037 76 | # TODO is_scale = 3.75 77 | # TODO vs_scale = 14 78 | # TODO is_chan = 2 79 | # TODO vs_chan = 3 80 | -------------------------------------------------------------------------------- /conf/apple/j413.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 8 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Left Woofer] 18 | group = 1 19 | tr_coil = 32.10 20 | tr_magnet = 20.00 21 | tau_coil = 4.70 22 | tau_magnet = 66.50 23 | t_limit = 140.0 24 | t_headroom = 10.0 25 | z_nominal = 4.00 26 | z_shunt = 0.00 27 | a_t_20c = 0.00370036 28 | a_t_35c = 0.00351349 29 | is_scale = 3.75 30 | vs_scale = 14 31 | is_chan = 0 32 | vs_chan = 1 33 | 34 | [Speaker/Right Woofer] 35 | group = 1 36 | tr_coil = 32.10 37 | tr_magnet = 20.00 38 | tau_coil = 4.70 39 | tau_magnet = 66.50 40 | t_limit = 140.0 41 | t_headroom = 10.0 42 | z_nominal = 4.00 43 | z_shunt = 0.00 44 | a_t_20c = 0.00370036 45 | a_t_35c = 0.00351349 46 | is_scale = 3.75 47 | vs_scale = 14 48 | is_chan = 2 49 | vs_chan = 3 50 | 51 | [Speaker/Left Tweeter] 52 | group = 0 53 | tr_coil = 104.90 54 | tr_magnet = 200.0 55 | tau_coil = 1.85 56 | tau_magnet = 70.00 57 | t_limit = 140.0 58 | t_headroom = 10.0 59 | z_nominal = 3.72 60 | z_shunt = 0.00 61 | a_t_20c = 0.00371053 62 | a_t_35c = 0.00350437 63 | is_scale = 3.75 64 | vs_scale = 14 65 | is_chan = 4 66 | vs_chan = 5 67 | 68 | [Speaker/Right Tweeter] 69 | group = 0 70 | tr_coil = 104.90 71 | tr_magnet = 200.0 72 | tau_coil = 1.85 73 | tau_magnet = 70.00 74 | t_limit = 140.0 75 | t_headroom = 10.0 76 | z_nominal = 3.72 77 | z_shunt = 0.00 78 | a_t_20c = 0.00371053 79 | a_t_35c = 0.00350437 80 | is_scale = 3.75 81 | vs_scale = 14 82 | is_chan = 6 83 | vs_chan = 7 84 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-Licence-Identifier: MIT 2 | # Copyright The Asahi Linux Contributors 3 | 4 | BINDIR ?= /usr/bin 5 | UNITDIR ?= /lib/systemd/system 6 | UDEVDIR ?= /lib/udev/rules.d 7 | TMPFILESDIR ?= /usr/lib/tmpfiles.d 8 | SHAREDIR ?= /usr/share/ 9 | VARDIR ?= /var/ 10 | SPEAKERSAFETYD_GROUP ?= speakersafetyd 11 | SPEAKERSAFETYD_USER ?= speakersafetyd 12 | 13 | all: 14 | cargo build --release 15 | 16 | install: install-data 17 | install -dDm0755 $(DESTDIR)/$(BINDIR) 18 | install -pm0755 target/release/speakersafetyd $(DESTDIR)/$(BINDIR)/speakersafetyd 19 | 20 | install-data: 21 | install -dDm0755 $(DESTDIR)/$(UNITDIR) 22 | install -pm0644 speakersafetyd.service $(DESTDIR)/$(UNITDIR)/speakersafetyd.service 23 | install -dDm0755 $(DESTDIR)/$(UDEVDIR) 24 | install -pm0644 95-speakersafetyd.rules $(DESTDIR)/$(UDEVDIR)/95-speakersafetyd.rules 25 | install -dDm0755 $(DESTDIR)/$(SHAREDIR)/speakersafetyd/apple 26 | install -pm0644 -t $(DESTDIR)/$(SHAREDIR)/speakersafetyd/apple $(wildcard conf/apple/*) 27 | install -dDm0755 -o $(SPEAKERSAFETYD_USER) -g $(SPEAKERSAFETYD_GROUP) $(DESTDIR)/$(VARDIR)/lib/speakersafetyd 28 | install -dDm0700 -o $(SPEAKERSAFETYD_USER) -g $(SPEAKERSAFETYD_GROUP) $(DESTDIR)/$(VARDIR)/lib/speakersafetyd/blackbox 29 | install -dDm0755 $(DESTDIR)/$(TMPFILESDIR) 30 | install -pm0644 speakersafetyd.tmpfiles $(DESTDIR)/$(TMPFILESDIR)/speakersafetyd.conf 31 | install -dDm0755 -o $(SPEAKERSAFETYD_USER) -g $(SPEAKERSAFETYD_GROUP) $(DESTDIR)/run/speakersafetyd 32 | 33 | uninstall: 34 | rm -f $(DESTDIR)/$(BINDIR)/speakersafetyd $(DESTDIR)/$(UNITDIR)/speakersafetyd.service $(DESTDIR)/$(UDEVDIR)/95-speakersafetyd.rules $(DESTDIR)/$(TMPFILESDIR)/speakersafetyd.conf 35 | rm -rf $(DESTDIR)/$(SHAREDIR)/speakersafetyd 36 | 37 | .PHONY: all install install-data uninstall 38 | -------------------------------------------------------------------------------- /95-speakersafetyd.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM=="sound", DRIVERS=="snd-soc-macaudio", GOTO="speakersafetyd_macaudio" 2 | GOTO="speakersafetyd_end" 3 | 4 | LABEL="speakersafetyd_macaudio" 5 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ*", ENV{ACP_IGNORE}="1" 6 | 7 | #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ180", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 8 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ274", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 9 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ293", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 10 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ313", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 11 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ314", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 12 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ316", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 13 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ375", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 14 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ413", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 15 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ414", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 16 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ415", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 17 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ416", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 18 | #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ456", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 19 | #KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ457", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 20 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ473", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 21 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ474", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 22 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ475", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 23 | KERNEL=="pcmC*D2c", ATTRS{id}=="AppleJ493", TAG+="systemd", ENV{SYSTEMD_WANTS}="speakersafetyd.service" 24 | 25 | LABEL="speakersafetyd_end" 26 | -------------------------------------------------------------------------------- /conf/apple/j316.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 12 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Left Woofer 1] 18 | group = 1 19 | tr_coil = 23.00 20 | tr_magnet = 40.00 21 | tau_coil = 5.60 22 | tau_magnet = 197.25 23 | t_limit = 140.0 24 | t_headroom = 10.0 25 | z_nominal = 3.60 26 | z_shunt = 0.09 27 | a_t_20c = 0.00374895 28 | a_t_35c = 0.00354903 29 | is_scale = 3.75 30 | vs_scale = 14 31 | is_chan = 0 32 | vs_chan = 1 33 | 34 | [Speaker/Right Woofer 1] 35 | group = 1 36 | tr_coil = 23.00 37 | tr_magnet = 40.00 38 | tau_coil = 5.60 39 | tau_magnet = 197.25 40 | t_limit = 140.0 41 | t_headroom = 10.0 42 | z_nominal = 3.60 43 | z_shunt = 0.09 44 | a_t_20c = 0.00374895 45 | a_t_35c = 0.00354903 46 | is_scale = 3.75 47 | vs_scale = 14 48 | is_chan = 2 49 | vs_chan = 3 50 | 51 | [Speaker/Left Tweeter] 52 | group = 0 53 | tr_coil = 45.0 54 | tr_magnet = 50.00 55 | tau_coil = 1.3 56 | tau_magnet = 73.26 57 | t_limit = 140.0 58 | t_headroom = 10.0 59 | z_nominal = 3.60 60 | z_shunt = 0.09 61 | a_t_20c = 0.00382045 62 | a_t_35c = 0.00361650 63 | is_scale = 3.75 64 | vs_scale = 14 65 | is_chan = 4 66 | vs_chan = 5 67 | 68 | [Speaker/Right Tweeter] 69 | group = 0 70 | tr_coil = 45.0 71 | tr_magnet = 50.00 72 | tau_coil = 1.3 73 | tau_magnet = 73.26 74 | t_limit = 140.0 75 | t_headroom = 10.0 76 | z_nominal = 3.60 77 | z_shunt = 0.09 78 | a_t_20c = 0.00382045 79 | a_t_35c = 0.00361650 80 | is_scale = 3.75 81 | vs_scale = 14 82 | is_chan = 6 83 | vs_chan = 7 84 | 85 | [Speaker/Left Woofer 2] 86 | group = 1 87 | tr_coil = 23.00 88 | tr_magnet = 40.00 89 | tau_coil = 5.60 90 | tau_magnet = 197.25 91 | t_limit = 140.0 92 | t_headroom = 10.0 93 | z_nominal = 3.60 94 | z_shunt = 0.09 95 | a_t_20c = 0.00374895 96 | a_t_35c = 0.00354903 97 | is_scale = 3.75 98 | vs_scale = 14 99 | is_chan = 8 100 | vs_chan = 9 101 | 102 | [Speaker/Right Woofer 2] 103 | group = 1 104 | tr_coil = 23.00 105 | tr_magnet = 40.00 106 | tau_coil = 5.60 107 | tau_magnet = 197.25 108 | t_limit = 140.0 109 | t_headroom = 10.0 110 | z_nominal = 3.60 111 | z_shunt = 0.09 112 | a_t_20c = 0.00374895 113 | a_t_35c = 0.00354903 114 | is_scale = 3.75 115 | vs_scale = 14 116 | is_chan = 10 117 | vs_chan = 11 118 | -------------------------------------------------------------------------------- /conf/apple/j314.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 12 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Left Woofer 1] 18 | group = 1 19 | tr_coil = 28.09 20 | tr_magnet = 34.43 21 | tau_coil = 3.05 22 | tau_magnet = 192.45 23 | t_limit = 140.0 24 | t_headroom = 10.0 25 | z_nominal = 3.20 26 | z_shunt = 0.09 27 | a_t_20c = 0.00383214 28 | a_t_35c = 0.00362404 29 | is_scale = 3.75 30 | vs_scale = 14 31 | is_chan = 0 32 | vs_chan = 1 33 | 34 | [Speaker/Right Woofer 1] 35 | group = 1 36 | tr_coil = 28.09 37 | tr_magnet = 34.43 38 | tau_coil = 3.05 39 | tau_magnet = 192.45 40 | t_limit = 140.0 41 | t_headroom = 10.0 42 | z_nominal = 3.20 43 | z_shunt = 0.09 44 | a_t_20c = 0.00383214 45 | a_t_35c = 0.00362404 46 | is_scale = 3.75 47 | vs_scale = 14 48 | is_chan = 2 49 | vs_chan = 3 50 | 51 | [Speaker/Left Tweeter] 52 | group = 0 53 | tr_coil = 34.50 54 | tr_magnet = 48.20 55 | tau_coil = 2.31 56 | tau_magnet = 61.40 57 | t_limit = 140.0 58 | t_headroom = 10.0 59 | z_nominal = 3.20 60 | z_shunt = 0.09 61 | a_t_20c = 0.00354779 62 | a_t_35c = 0.00329538 63 | is_scale = 3.75 64 | vs_scale = 14 65 | is_chan = 4 66 | vs_chan = 5 67 | 68 | [Speaker/Right Tweeter] 69 | group = 0 70 | tr_coil = 34.50 71 | tr_magnet = 48.20 72 | tau_coil = 2.31 73 | tau_magnet = 61.40 74 | t_limit = 140.0 75 | t_headroom = 10.0 76 | z_nominal = 3.20 77 | z_shunt = 0.09 78 | a_t_20c = 0.00354779 79 | a_t_35c = 0.00329538 80 | is_scale = 3.75 81 | vs_scale = 14 82 | is_chan = 6 83 | vs_chan = 7 84 | 85 | [Speaker/Left Woofer 2] 86 | group = 1 87 | tr_coil = 28.09 88 | tr_magnet = 34.43 89 | tau_coil = 3.05 90 | tau_magnet = 192.45 91 | t_limit = 140.0 92 | t_headroom = 10.0 93 | z_nominal = 3.20 94 | z_shunt = 0.09 95 | a_t_20c = 0.00383214 96 | a_t_35c = 0.00362404 97 | is_scale = 3.75 98 | vs_scale = 14 99 | is_chan = 8 100 | vs_chan = 9 101 | 102 | [Speaker/Right Woofer 2] 103 | group = 1 104 | tr_coil = 28.09 105 | tr_magnet = 34.43 106 | tau_coil = 3.05 107 | tau_magnet = 192.45 108 | t_limit = 140.0 109 | t_headroom = 10.0 110 | z_nominal = 3.20 111 | z_shunt = 0.09 112 | a_t_20c = 0.00383214 113 | a_t_35c = 0.00362404 114 | is_scale = 3.75 115 | vs_scale = 14 116 | is_chan = 10 117 | vs_chan = 11 118 | -------------------------------------------------------------------------------- /conf/apple/j415.conf: -------------------------------------------------------------------------------- 1 | [Globals] 2 | visense_pcm = 2 3 | t_ambient = 50.0 4 | t_hysteresis = 5.0 5 | t_window = 20.0 6 | channels = 12 7 | period = 4096 8 | link_gains = True 9 | uclamp_max = 64 10 | 11 | [Controls] 12 | vsense = VSENSE Switch 13 | isense = ISENSE Switch 14 | amp_gain = Amp Gain Volume 15 | volume = Speaker Volume 16 | 17 | [Speaker/Left Woofer 1] 18 | group = 1 19 | tr_coil = 40.00 20 | tr_magnet = 26.00 21 | tau_coil = 3.00 22 | tau_magnet = 35.00 23 | t_limit = 140.0 24 | t_headroom = 10.0 25 | z_nominal = 4.20 26 | z_shunt = 0.00 27 | a_t_20c = 0.00352082 28 | a_t_35c = 0.00328147 29 | is_scale = 3.75 30 | vs_scale = 14 31 | is_chan = 0 32 | vs_chan = 1 33 | 34 | [Speaker/Right Woofer 1] 35 | group = 1 36 | tr_coil = 40.00 37 | tr_magnet = 26.00 38 | tau_coil = 3.00 39 | tau_magnet = 37.00 40 | t_limit = 140.0 41 | t_headroom = 10.0 42 | z_nominal = 4.20 43 | z_shunt = 0.00 44 | a_t_20c = 0.00352082 45 | a_t_35c = 0.00328147 46 | is_scale = 3.75 47 | vs_scale = 14 48 | is_chan = 2 49 | vs_chan = 3 50 | 51 | [Speaker/Left Tweeter] 52 | group = 0 53 | tr_coil = 124.00 54 | tr_magnet = 89.80 55 | tau_coil = 1.80 56 | tau_magnet = 28.00 57 | t_limit = 140.0 58 | t_headroom = 10.0 59 | z_nominal = 3.76 60 | z_shunt = 0.00 61 | a_t_20c = 0.00347480 62 | a_t_35c = 0.00336649 63 | is_scale = 3.75 64 | vs_scale = 14 65 | is_chan = 4 66 | vs_chan = 5 67 | 68 | [Speaker/Right Tweeter] 69 | group = 0 70 | tr_coil = 129.00 71 | tr_magnet = 87.20 72 | tau_coil = 1.80 73 | tau_magnet = 28.00 74 | t_limit = 140.0 75 | t_headroom = 10.0 76 | z_nominal = 3.76 77 | z_shunt = 0.00 78 | a_t_20c = 0.00347480 79 | a_t_35c = 0.00336649 80 | is_scale = 3.75 81 | vs_scale = 14 82 | is_chan = 6 83 | vs_chan = 7 84 | 85 | [Speaker/Left Woofer 2] 86 | group = 1 87 | tr_coil = 35.00 88 | tr_magnet = 24.50 89 | tau_coil = 3.00 90 | tau_magnet = 35.00 91 | t_limit = 140.0 92 | t_headroom = 10.0 93 | z_nominal = 4.00 94 | z_shunt = 0.00 95 | a_t_20c = 0.00352082 96 | a_t_35c = 0.00328147 97 | is_scale = 3.75 98 | vs_scale = 14 99 | is_chan = 8 100 | vs_chan = 9 101 | 102 | [Speaker/Right Woofer 2] 103 | group = 1 104 | tr_coil = 35.00 105 | tr_magnet = 24.50 106 | tau_coil = 3.00 107 | tau_magnet = 35.00 108 | t_limit = 140.0 109 | t_headroom = 10.0 110 | z_nominal = 4.00 111 | z_shunt = 0.00 112 | a_t_20c = 0.00352082 113 | a_t_35c = 0.00328147 114 | is_scale = 3.75 115 | vs_scale = 14 116 | is_chan = 10 117 | vs_chan = 11 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## speakersafetyd - a software Smart Amp implementation 2 | speakersafetyd is a userspace daemon written in Rust that implements an 3 | analogue of the Texas Instruments Smart Amp speaker protection model. 4 | 5 | Apple Silicon Macs mostly use the Texas Instruments TAS2764 amp chip (codec 6 | in ALSA parlance), which provides sense lines for the voltage and current across 7 | the voice coil of the connected speaker. These codecs are designed to be used 8 | in embedded applications where device firmware takes this information and uses 9 | it to protect the speaker from damage. Apple instead implement this as machine-specific 10 | plugins to the userspace half of CoreAudio. An increasing number of other 11 | vendors in both the desktop and embedded/Android worlds are choosing to go down a similar 12 | route, folding this functionality into proprietary driver/userspace blobs that usually 13 | also bundle niceties like EQ (we have a solution for this too, see [asahi-audio](https://github.com/AsahiLinux/asahi-audio)). 14 | This puts users at serious risk of permanently destroying their expensive devices if they choose 15 | to run custom software, such as Asahi Linux or an Open Source Android ROM. 16 | 17 | speakersafetyd is the first (as far as we know) FOSS implementation of a speaker 18 | protection model. It solves the problem described above by allowing parties interested 19 | in compatible devices to quickly and easily implement a speaker protection model for those 20 | devices. Only Apple Silicon Macs under Linux are currently supported, 21 | however the model applies to all loudspeakers. The daemon itself should be easy enough to 22 | adapt for any device that provides V/ISENSE data in a manner similar to TAS2764. 23 | 24 | ### Dependencies 25 | * Rust stable 26 | * alsa-lib 27 | * An Apple Silicon Mac running Asahi Linux 28 | * A `speakersafetyd` user in the `audio` group 29 | 30 | ### Some background on Smart Amps 31 | The cheap component speaker elements used in modern devices like 32 | Bluetooth speakers, TVs, laptops, etc. are very fragile. In order 33 | to eke the highest possible sound quality out of them, they need to be 34 | driven *hard*. This leaves us with a dilemma - how do we drive these 35 | speakers hard enough to get a loud, high-quality output but not hard 36 | enough to destroy them? 37 | 38 | A speaker's electromechanical characteristics can be modelled 39 | and boiled down to a set of parameters - the Thiele/Small Parameters. 40 | These can be used to predict what the speaker will do with certain 41 | inputs. When we add measured properties like the time constant of 42 | the speaker's voice coil's and magnet's temperature curve, we can 43 | accurately model a speaker's temperature for any given voltage/current 44 | across the coil. When the speaker is getting too hot, we just reduce the 45 | power going to it until it cools down. 46 | 47 | This lets us fearlessly drive the speakers as hard as they physically can 48 | be without being permanently damaged. This is extremely useful, as without it 49 | the output level on these devices would have to be hard limited to a very low 50 | level that is known to be safe for the worst possible input. Instead, we can 51 | simply duck the output in those cases and allow the speakers to operate at 52 | full power where possible. 53 | 54 | Many integrated amplifier chips implement this functionality in hardware, as well 55 | as additional advanced DSP features like compressors and limiters. Texas Instruments 56 | call their implementation "Smart Amp." Integrators need only communicate the parameter set 57 | to the chip for the speaker it is connected to, and it does the rest. Many do not however, 58 | and instead only provide facilities for measuring the voltage and current across the speaker's 59 | voice coil. It is up to the implementer to capture this data and do something with it. 60 | 61 | speakersafetyd is (as far as we know) the first and only FOSS implementation of the 62 | Smart Amp protection model. 63 | -------------------------------------------------------------------------------- /src/blackbox.rs: -------------------------------------------------------------------------------- 1 | use crate::types::SpeakerState; 2 | use log::warn; 3 | use std::fs::File; 4 | use std::io; 5 | use std::io::Write; 6 | use std::path::Path; 7 | use std::slice; 8 | 9 | use json::object; 10 | 11 | struct Block { 12 | sample_rate: i32, 13 | state: Vec>, 14 | data: Vec, 15 | } 16 | 17 | pub struct Blackbox { 18 | machine: String, 19 | globals: crate::types::Globals, 20 | path: Box, 21 | blocks: Vec, 22 | } 23 | 24 | /// Maximum number of blocks in the ring buffer (around 30 seconds at 4096/48000) 25 | const MAX_BLOCKS: usize = 330; 26 | 27 | impl Blackbox { 28 | pub fn new(machine: &str, path: &Path, globals: &crate::types::Globals) -> Blackbox { 29 | Blackbox { 30 | machine: machine.into(), 31 | globals: globals.clone(), 32 | path: path.into(), 33 | blocks: Vec::new(), 34 | } 35 | } 36 | 37 | pub fn reset(&mut self) { 38 | self.blocks.clear(); 39 | } 40 | 41 | pub fn push(&mut self, sample_rate: i32, data: Vec, state: Vec>) { 42 | while self.blocks.len() >= MAX_BLOCKS { 43 | self.blocks.remove(0); 44 | } 45 | self.blocks.push(Block { 46 | sample_rate, 47 | state, 48 | data, 49 | }) 50 | } 51 | 52 | pub fn preserve(&mut self, reason: String) -> io::Result<()> { 53 | if self.blocks.is_empty() { 54 | warn!("Blackbox is empty, nothing to save"); 55 | return Ok(()); 56 | } 57 | 58 | let now = chrono::Local::now().to_rfc3339(); 59 | let meta_name = self.path.join(now.clone() + ".fdr"); 60 | let data_name = self.path.join(now.clone() + ".cvr"); 61 | 62 | warn!("Preserving blackbox {}", now); 63 | 64 | let mut metafd = File::create(meta_name)?; 65 | let mut datafd = File::create(data_name)?; 66 | 67 | for blk in self.blocks.iter() { 68 | // meh unsafe 69 | let slice_u8: &[u8] = unsafe { 70 | slice::from_raw_parts( 71 | blk.data.as_ptr() as *const u8, 72 | blk.data.len() * std::mem::size_of::(), 73 | ) 74 | }; 75 | datafd.write_all(slice_u8)?; 76 | } 77 | 78 | let mut meta = object! { 79 | message: reason, 80 | machine: self.machine.clone(), 81 | sample_rate: self.blocks[0].sample_rate, 82 | channels: self.globals.channels, 83 | t_ambient: self.globals.t_ambient, 84 | t_window: self.globals.t_window, 85 | t_hysteresis: self.globals.t_hysteresis, 86 | blocks: null 87 | }; 88 | 89 | let mut blocks = json::JsonValue::new_array(); 90 | 91 | for block in self.blocks.iter() { 92 | let mut info = object! { 93 | sample_rate: block.sample_rate, 94 | sample_count: block.data.len() / self.globals.channels, 95 | speakers: null, 96 | }; 97 | let mut speakers = json::JsonValue::new_array(); 98 | 99 | for group in block.state.iter() { 100 | for speaker in group.iter() { 101 | let _ = speakers.push(object! { 102 | t_coil: speaker.t_coil, 103 | t_magnet: speaker.t_magnet, 104 | t_coil_hyst: speaker.t_coil_hyst, 105 | t_magnet_hyst: speaker.t_magnet_hyst, 106 | min_gain: speaker.min_gain, 107 | gain: speaker.gain, 108 | }); 109 | } 110 | } 111 | info["speakers"] = speakers; 112 | let _ = blocks.push(info); 113 | } 114 | 115 | meta["blocks"] = blocks; 116 | 117 | metafd.write_all(meta.dump().as_bytes())?; 118 | 119 | Ok(()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // (C) 2022 The Asahi Linux Contributors 3 | 4 | use alsa::mixer::MilliBel; 5 | use configparser::ini::Ini; 6 | 7 | pub fn open_card(card: &str) -> alsa::ctl::Ctl { 8 | let ctldev: alsa::ctl::Ctl = match alsa::ctl::Ctl::new(card, false) { 9 | Ok(ctldev) => ctldev, 10 | Err(e) => { 11 | panic!("{}: Could not open sound card! Error: {}", card, e); 12 | } 13 | }; 14 | 15 | ctldev 16 | } 17 | 18 | pub fn open_pcm(dev: &str, chans: u32, mut sample_rate: u32) -> alsa::pcm::PCM { 19 | let pcm = alsa::pcm::PCM::new(dev, alsa::Direction::Capture, false).unwrap(); 20 | { 21 | let params = alsa::pcm::HwParams::any(&pcm).unwrap(); 22 | 23 | let rate_max = params.get_rate_max().unwrap(); 24 | let rate_min = params.get_rate_min().unwrap(); 25 | println!("PCM rate: {}..{}", rate_min, rate_max); 26 | 27 | if sample_rate == 0 { 28 | sample_rate = rate_min; 29 | } 30 | 31 | params.set_channels(chans).unwrap(); 32 | params 33 | .set_rate(sample_rate, alsa::ValueOr::Nearest) 34 | .unwrap(); 35 | params.set_format(alsa::pcm::Format::s16()).unwrap(); 36 | params.set_access(alsa::pcm::Access::RWInterleaved).unwrap(); 37 | pcm.hw_params(¶ms).unwrap(); 38 | } 39 | 40 | pcm 41 | } 42 | 43 | /** 44 | Wrapper around configparser::ini::Ini.getint() 45 | to safely unwrap the Result, E> returned by 46 | it. 47 | */ 48 | pub fn parse_int>(config: &Ini, section: &str, key: &str) -> T 49 | where 50 | >::Error: std::fmt::Debug, 51 | { 52 | config 53 | .getint(section, key) 54 | .unwrap_or_else(|_| panic!("{}/{}: Invalid value", section, key)) 55 | .unwrap_or_else(|| panic!("{}/{}: Missing key", section, key)) 56 | .try_into() 57 | .expect("{}/{}: Out of bounds") 58 | } 59 | 60 | pub fn parse_opt_int>(config: &Ini, section: &str, key: &str) -> Option 61 | where 62 | >::Error: std::fmt::Debug, 63 | { 64 | config 65 | .getint(section, key) 66 | .unwrap_or_else(|_| panic!("{}/{}: Invalid value", section, key)) 67 | .map(|a| a.try_into().expect("{}/{}: Out of bounds")) 68 | } 69 | 70 | /** 71 | Wrapper around configparser::ini::Ini.getfloat() 72 | to safely unwrap the Result, E> returned by 73 | it. 74 | */ 75 | pub fn parse_float(config: &Ini, section: &str, key: &str) -> f32 { 76 | let val = config 77 | .getfloat(section, key) 78 | .unwrap_or_else(|_| panic!("{}/{}: Invalid value", section, key)) 79 | .unwrap_or_else(|| panic!("{}/{}: Missing key", section, key)) as f32; 80 | 81 | assert!(val.is_finite()); 82 | val 83 | } 84 | 85 | /** 86 | Wrapper around configparser::ini::Ini.getfloat() 87 | to safely unwrap the Result, E> returned by 88 | it. 89 | */ 90 | pub fn parse_string(config: &Ini, section: &str, key: &str) -> String { 91 | config 92 | .get(section, key) 93 | .unwrap_or_else(|| panic!("{}/{}: Missing key", section, key)) 94 | } 95 | 96 | /** 97 | Wrapper around alsa::ctl::ElemValue::new(). Lets us bail on errors and 98 | pass in the Bytes type for V/ISENSE 99 | */ 100 | pub fn new_elemvalue(t: alsa::ctl::ElemType) -> alsa::ctl::ElemValue { 101 | 102 | 103 | match alsa::ctl::ElemValue::new(t) { 104 | Ok(val) => val, 105 | Err(_e) => { 106 | panic!("Could not open a handle to an element!"); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | Wrapper for alsa::ctl::Ctl::elem_read(). 113 | */ 114 | pub fn read_ev(card: &alsa::ctl::Ctl, ev: &mut alsa::ctl::ElemValue, name: &str) { 115 | match card.elem_read(ev) { 116 | // alsa:Result<()> 117 | Ok(val) => val, 118 | Err(e) => { 119 | panic!( 120 | "Could not read elem value {}. alsa-lib error: {:?}", 121 | name, e 122 | ); 123 | } 124 | }; 125 | } 126 | 127 | /** 128 | Wrapper for alsa::ctl::Ctl::elem_write(). 129 | */ 130 | pub fn write_ev(card: &alsa::ctl::Ctl, ev: &alsa::ctl::ElemValue, name: &str) { 131 | match card.elem_write(ev) { 132 | // alsa:Result<()> 133 | Ok(val) => val, 134 | Err(e) => { 135 | panic!( 136 | "Could not write elem value {}. alsa-lib error: {:?}", 137 | name, e 138 | ); 139 | } 140 | }; 141 | } 142 | 143 | /** 144 | Wrapper for alsa::ctl::Ctl::elem_write(). 145 | */ 146 | pub fn get_range_db( 147 | card: &alsa::ctl::Ctl, 148 | el: &alsa::ctl::ElemId, 149 | name: &str, 150 | ) -> (MilliBel, MilliBel) { 151 | match card.get_db_range(el) { 152 | // alsa:Result<()> 153 | Ok(val) => val, 154 | Err(e) => { 155 | panic!( 156 | "Could not get elem db range {}. alsa-lib error: {:?}", 157 | name, e 158 | ); 159 | } 160 | } 161 | } 162 | 163 | /** 164 | Wrapper for alsa::ctl::Ctl::elem_read(). 165 | */ 166 | pub fn lock_el(card: &alsa::ctl::Ctl, el: &alsa::ctl::ElemId, name: &str) { 167 | let _val = match card.elem_lock(el) { 168 | // alsa:Result<()> 169 | Ok(val) => val, 170 | Err(e) => { 171 | panic!("Could not lock elem {}. alsa-lib error: {:?}", name, e); 172 | } 173 | }; 174 | } 175 | 176 | pub fn int_to_db(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: i32) -> MilliBel { 177 | 178 | 179 | match card.convert_to_db(id, val.into()) { 180 | Ok(inner) => inner, 181 | Err(e) => { 182 | panic!( 183 | "Could not convert val {} to dB! alsa-lib error: {:?}", 184 | val, e 185 | ); 186 | } 187 | } 188 | } 189 | 190 | pub fn db_to_int(card: &alsa::ctl::Ctl, id: &alsa::ctl::ElemId, val: f32) -> i32 { 191 | let mb: MilliBel = MilliBel((val * 100.0) as i64); 192 | 193 | 194 | match card.convert_from_db(id, mb, alsa::Round::Floor) { 195 | Ok(inner) => inner as i32, 196 | Err(e) => { 197 | panic!( 198 | "Could not convert MilliBel {:?} to int! alsa-lib error: {:?}", 199 | val, e 200 | ); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /testing/analyze.py: -------------------------------------------------------------------------------- 1 | import json, sys, os.path, configparser 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from scipy.signal import butter, sosfilt, freqz 5 | 6 | CONFDIR = os.path.join(os.path.dirname(__file__), "../conf") 7 | 8 | # This information is not in the blackbox file 9 | DEFAULT_AMP_GAIN = 18.50 10 | AMP_GAIN = { 11 | "apple,j180": 16.0, 12 | "apple,j313": 16.0, 13 | "apple,j274": 21.0, 14 | "apple,j375": 21.0, 15 | "apple,j473": 21.0, 16 | "apple,j474": 21.0, 17 | "apple,j475": 21.0, 18 | } 19 | 20 | def db(x): 21 | return 10 ** (x / 20) 22 | 23 | def smooth(a, n=3): 24 | l = len(a) 25 | ret = np.cumsum(a, dtype=float) 26 | ret[n:] = ret[n:] - ret[:-n] 27 | ret = ret[n - 1:] / n 28 | pad = l - len(ret) 29 | return np.pad(ret, (pad//2, (pad + 1)//2), "edge") 30 | 31 | def butter_lowpass(cutoff, fs, order=5): 32 | return butter(order, cutoff, fs=fs, btype='low', output="sos", analog=False) 33 | 34 | def butter_highpass(cutoff, fs, order=5): 35 | return butter(order, cutoff, fs=fs, btype='high', output="sos", analog=False) 36 | 37 | def butter_lowpass_filter(data, cutoff, fs, order=5): 38 | sos = butter_lowpass(cutoff, fs, order=order) 39 | y = sosfilt(sos, data) 40 | return y 41 | 42 | def butter_highpass_filter(data, cutoff, fs, order=5): 43 | sos = butter_highpass(cutoff, fs, order=order) 44 | y = sosfilt(sos, data) 45 | return y 46 | 47 | def pilot_filter(data, fs): 48 | data = butter_lowpass_filter(data, 100, fs, 6) 49 | return butter_highpass_filter(data, 10, fs, 3) 50 | 51 | class Model: 52 | def __init__(self, idx, an, name, conf): 53 | self.idx = idx 54 | self.an = an 55 | self.name = name 56 | self.conf = conf 57 | self.tr_coil = float(conf["tr_coil"]) 58 | self.tr_magnet = float(conf["tr_magnet"]) 59 | self.tau_coil = float(conf["tau_coil"]) 60 | self.tau_magnet = float(conf["tau_magnet"]) 61 | self.t_limit = float(conf["t_limit"]) 62 | self.t_headroom = float(conf["t_headroom"]) 63 | self.z_nominal = float(conf["z_nominal"]) 64 | self.z_shunt = float(conf.get("z_shunt", 0)) 65 | self.is_scale = float(conf["is_scale"]) 66 | self.vs_scale = float(conf["vs_scale"]) 67 | self.a_t_20c = float(conf["a_t_20c"]) 68 | self.a_t_35c = float(conf["a_t_35c"]) 69 | 70 | self.is_chan = int(conf["is_chan"]) 71 | self.vs_chan = int(conf["vs_chan"]) 72 | 73 | self.t_ambient = an.fdr["t_ambient"] 74 | 75 | self.t_coil = an.fdr["blocks"][0]["speakers"][self.idx]["t_coil"] 76 | self.t_magnet = an.fdr["blocks"][0]["speakers"][self.idx]["t_magnet"] 77 | 78 | self.m_x = [] 79 | self.m_t_coil_tg = [] 80 | self.m_t_coil = [] 81 | self.m_t_magnet_tg = [] 82 | self.m_t_magnet = [] 83 | 84 | self.l_x = [] 85 | self.l_t_coil = [] 86 | self.l_t_magnet = [] 87 | 88 | def run_model(self): 89 | off = 0 90 | t = 0 91 | for blk in self.an.fdr["blocks"]: 92 | sr = blk["sample_rate"] 93 | cnt = blk["sample_count"] 94 | data = blk["speakers"][self.idx] 95 | 96 | isense = self.an.cvr[off:off+cnt, self.is_chan] * self.is_scale 97 | vsense = self.an.cvr[off:off+cnt, self.vs_chan] * self.vs_scale 98 | 99 | dt = 1 / self.an.sr 100 | alpha_coil = dt / (dt + self.tau_coil) 101 | alpha_magnet = dt / (dt + self.tau_magnet) 102 | 103 | self.l_x.append(t) 104 | self.l_t_coil.append(data["t_coil"]) 105 | self.l_t_magnet.append(data["t_magnet"]) 106 | for x, (i, v) in enumerate(zip(isense, vsense)): 107 | self.m_x.append(t + x / sr) 108 | 109 | p = i * v 110 | 111 | tvc_tgt = self.t_magnet + p * self.tr_coil 112 | self.t_coil = tvc_tgt * alpha_coil + self.t_coil * (1 - alpha_coil) 113 | tmag_tgt = self.t_ambient + p * self.tr_magnet 114 | self.t_magnet = tmag_tgt * alpha_magnet + self.t_magnet * (1 - alpha_magnet) 115 | 116 | self.m_t_coil_tg.append(tvc_tgt) 117 | self.m_t_coil.append(self.t_coil) 118 | self.m_t_magnet_tg.append(tmag_tgt) 119 | self.m_t_magnet.append(self.t_magnet) 120 | 121 | t += cnt / sr 122 | off += cnt 123 | 124 | def analyze(self, outfile): 125 | plt.clf() 126 | 127 | fig, ax1 = plt.subplots(figsize=(30,15)) 128 | 129 | ax1.set_title(self.name) 130 | 131 | ax1.set_xlabel('time (s)') 132 | ax1.set_ylabel('temperature') 133 | ax1.tick_params(axis='y') 134 | ax2 = ax1.twinx() # instantiate a second axes that shares the same x-axis 135 | 136 | color = 'tab:red' 137 | ax2.set_ylabel('power', color=color) 138 | ax2.tick_params(axis='y', labelcolor=color) 139 | 140 | ax1.plot(self.m_x, self.m_t_coil, "r") 141 | # ax1.plot(self.m_x, smooth(self.m_t_coil_tg, 1000), "y") 142 | ax1.plot(self.m_x, self.m_t_magnet, "b") 143 | # ax1.plot(self.m_x, smooth(self.m_t_magnet_tg, 1000), "c") 144 | ax1.plot(self.l_x, self.l_t_coil, "om") 145 | ax1.plot(self.l_x, self.l_t_magnet, "og") 146 | 147 | i = self.an.cvr[:, self.is_chan] * self.is_scale 148 | v = self.an.cvr[:, self.vs_chan] * self.vs_scale 149 | 150 | sr = self.an.fdr["sample_rate"] 151 | ilp = pilot_filter(i, sr) 152 | vlp = pilot_filter(v, sr) 153 | 154 | p = butter_lowpass_filter(i * v, 10, sr, 1) 155 | p = smooth(p, 4000) 156 | plp = butter_lowpass_filter(ilp * vlp, 10, sr, 1) 157 | plp = smooth(plp, 4000) 158 | vlprms_sq = butter_lowpass_filter(vlp * vlp, 10, sr, 1) 159 | vlprms_sq = smooth(vlprms_sq, 4000) 160 | r = vlprms_sq / plp 161 | 162 | # ax2.plot(self.m_x, p, "b") 163 | 164 | rref = np.average(r[1 * sr:2 * sr]) 165 | print(f"Initial resistance: {rref} ohms") 166 | 167 | # Clear out the first second, since it tends to contain garbage 168 | r[:1*sr] = rref 169 | #r = butter_lowpass_filter(r - rref, 2, sr, 2) + rref 170 | a = self.a_t_35c # XXX why are there two values at different temperatures? 171 | tref = self.l_t_coil[0] 172 | t = ((r - self.z_shunt) / (rref - self.z_shunt) - 1) / a + tref 173 | 174 | ax1.plot(self.m_x, t, "k") 175 | ax2.plot(self.m_x, p, "r") 176 | # ax2.plot(self.m_x, plp, "g") 177 | # ax2.plot(self.m_x, vlprms_sq, "b") 178 | 179 | for level in (-1000, -6, -10, -15): 180 | gain = AMP_GAIN.get(self.an.fdr["machine"], DEFAULT_AMP_GAIN) 181 | pbase = (db(gain - 30) ** 2) / (self.z_nominal + self.z_shunt) 182 | ptest = (db(gain + level) ** 2) / (self.z_nominal + self.z_shunt) 183 | p = pbase + ptest 184 | ax2.axhline(y=p, color='r', linestyle='--') 185 | 186 | fig.tight_layout() # otherwise the right y-label is slightly clipped 187 | 188 | plt.savefig(outfile) 189 | 190 | 191 | class Analyzer: 192 | def __init__(self, base): 193 | self.fdr = json.load(open(base + ".fdr")) 194 | data = open(base + ".cvr", "rb").read() 195 | cvr = np.frombuffer(data, dtype="int16").astype("float") / 32768 196 | 197 | maker, model = self.fdr["machine"].split(",") 198 | cf = os.path.join(CONFDIR, maker, model + ".conf") 199 | print(f"Using config file: {cf}") 200 | self.conf = configparser.ConfigParser() 201 | self.conf.read(cf) 202 | 203 | ch = int(self.conf["Globals"]["channels"]) 204 | samples = len(cvr) // ch 205 | self.cvr = cvr.reshape((samples, ch)) 206 | print(f"Got {samples} samples ({ch} channels)") 207 | 208 | assert ch == self.fdr["channels"] 209 | self.sr = self.fdr["sample_rate"] 210 | 211 | speaker_configs = [] 212 | 213 | for key in self.conf.sections(): 214 | if not key.startswith("Speaker/"): 215 | continue 216 | print(key) 217 | name = key.split("/")[1] 218 | speaker_configs.append((name, self.conf[key])) 219 | 220 | # Match the order that speakersafetyd uses (by group) 221 | speaker_configs.sort(key=lambda x: int(x[1]["group"])) 222 | 223 | self.speakers = [] 224 | for i, cfg in enumerate(speaker_configs): 225 | self.speakers.append(Model(i, self, cfg[0], cfg[1])) 226 | 227 | def analyze(self): 228 | for i, spk in enumerate(self.speakers): 229 | print(f"Processing speaker {i}") 230 | spk.run_model() 231 | spk.analyze(f"speaker_{i}.png") 232 | # break 233 | 234 | if __name__ == "__main__": 235 | a = Analyzer(sys.argv[1]) 236 | a.analyze() 237 | -------------------------------------------------------------------------------- /docs/audump.py: -------------------------------------------------------------------------------- 1 | import struct, sys, os.path, plistlib, pprint 2 | 3 | LABELS = { 4 | "spp3": { 5 | 0: { 6 | 0: "thermal protection enabled", 7 | 1: "displacement protection enabled", 8 | 2: "thermal/power control gain attack time (s)", 9 | 3: "thermal/power control gain release time (s)", 10 | 4: "ambient temperature", 11 | 5: "SafeTlim", 12 | 6: "SafeTlimTimeMin", 13 | 7: "SafeTlimOffset", 14 | 8: "LookaheadDelay_ms", 15 | 9: "peak attack time (s)", 16 | 10: "peak decay time (s)", 17 | 11: "feedback integration time", 18 | 12: "thermal gain (dB)", 19 | 13: "displacement gain (dB)", 20 | 14: "spk pwr averaging window time (s)", 21 | 15: "modeled speaker power", 22 | 16: "measured speaker power", 23 | 17: "power control gain", 24 | 18: "CPMS power control enabled", 25 | 19: "CPMS power control closed loop", 26 | }, 27 | 4: { 28 | 0: "temperature limit", 29 | 1: "hard temp limit headroom", 30 | 2: "T_sett_vc", 31 | 3: "T_sett_mg", 32 | 4: "tau_Tvc", 33 | 5: "tau_Tmg", 34 | 6: "ThermalFFSpeedupFactor", 35 | 7: "temperature", 36 | 8: "OL temperature", 37 | 9: "Reb_ref", 38 | 10: "Rshunt", 39 | 11: "Rampout", 40 | 12: "mt", 41 | 13: "ct", 42 | 14: "kt", 43 | 15: "ag", 44 | 16: "g_bw", 45 | 17: "Q_d", 46 | 18: "phi", 47 | 19: "x_lim", 48 | 20: "ThermalMeasurementMethod", 49 | 21: "pilot tone enabled", 50 | 22: "CL thermal feedback enabled", 51 | 23: "TlimErrDecayTime", 52 | 24: "TempSenseWindowTime", 53 | 25: "TempSenseSmoothingTau", 54 | 26: "a_t_inv", 55 | 27: "PilotAmplHi_dB", 56 | 28: "PilotAmplLo_dB", 57 | 29: "PilotUpperThres", 58 | 30: "PilotLowerThres", 59 | 31: "PilotDecayTime", 60 | 32: "PilotFreq", 61 | 33: "LPMLSPreGain", 62 | 34: "LPMLSPostGain", 63 | 35: "LPMLSLowerCorner", 64 | 36: "LPMLS pre clip level", 65 | 37: "mu_Re", 66 | 38: "mu_Le", 67 | 39: "mu mechanical (PU)", 68 | 40: "Max relative displacement", 69 | 41: "abs(Min relative displacement)", 70 | 42: "DisplacementProtectionType", 71 | 64: "thermal gain", 72 | 65: "displacement gain", 73 | 66: "power control gain", 74 | 67: "PilotDecayTimeStage2", 75 | 68: "PilotEnableThres", 76 | }, 77 | }, 78 | "atsp": { 79 | 0: { 80 | 0: "Bypass", 81 | 40: "Gain link all audio channels", 82 | 1: "speakerType A: Amplifier sensitivity [V/Fs]", 83 | 2: "speakerType A: VoiceCoil: DC resistance [Ohms]", 84 | 3: "speakerType A: VoiceCoil: thermal resistance [C/Watt]", 85 | 4: "speakerType A: Voice Coil: thermal time constant [s]", 86 | 5: "speakerType A: Magnet: thermal resistance [C/Watt]", 87 | 6: "speakerType A: Magnet: thermal time constant [s]", 88 | 7: "speakerType A: Ambient temperature, [C]", 89 | # The target temperature of the speakers 90 | 8: "speakerType A: Temperature limit [C]", 91 | 9: "speakerType A: Attack time (ms)", 92 | 10: "speakerType A: Release time (ms)", 93 | 11: "speakerType A: Temperature hard limit headroom [C]", 94 | 12: "speakerType A: Gain link", 95 | 13: "speakerType A: Audio channel assignment", 96 | 14: "speakerType B: Amplifier sensitivity [V/Fs", 97 | 15: "speakerType B: VoiceCoil: DC resistance [Ohms]", 98 | 16: "speakerType B: VoiceCoil: thermal resistance [C/Watt]", 99 | 17: "speakerType B: Voice Coil: thermal time constant [s]", 100 | 18: "speakerType B: Magnet: thermal resistance [C/Watt]", 101 | 19: "speakerType B: Magnet: thermal time constant [s]", 102 | 20: "speakerType B: Ambient temperature, [C]", 103 | 21: "speakerType B: Temperature limit [C]", 104 | 22: "speakerType B: Attack time (ms)", 105 | 23: "speakerType B: Release time (ms)", 106 | 24: "speakerType B: Temperature hard limit headroom [C]", 107 | 25: "speakerType B: Gain link", 108 | 26: "speakerType B: Audio channel assignment", 109 | 27: "speakerType C: Amplifier sensitivity [V/Fs]", 110 | 28: "speakerType C: VoiceCoil: DC resistance [Ohms]", 111 | 29: "speakerType C: VoiceCoil: thermal resistance [C/Watt]", 112 | 30: "speakerType C: Voice Coil: thermal time constant [s]", 113 | 31: "speakerType C: Magnet: thermal resistance [C/Watt]", 114 | 32: "speakerType C: Magnet: thermal time constant [s]", 115 | 33: "speakerType C: Ambient temperature, [C]", 116 | 34: "speakerType C: Temperature limit [C]", 117 | 35: "speakerType C: Attack time (ms)", 118 | 36: "speakerType C: Release time (ms)", 119 | 37: "speakerType C: Temperature hard limit headroom [C]", 120 | 38: "speakerType C: Gain link", 121 | 39: "speakerType C: Audio channel assignment", 122 | } 123 | } 124 | } 125 | 126 | """ 127 | ATSP protection behavior: 128 | 129 | Max gain reduction is 20dB. 130 | "Temperature limit" is the target temperature 131 | If temperature exceeds limit + "Temperature hard limit headroom", 132 | protection goes into panic mode and triggers 20dB reduction. 133 | 134 | For settings: 135 | amp = 12 r = 4 136 | rVc = 50 aVc = 2 rMg = 1 aMg = 1 137 | Ta = 50 Tlim = 150 Theadroom = 5 138 | 139 | We see this limiter behavior: 140 | In Out 141 | 0 -9.7 142 | -8 -9.7 143 | -9 -9.6 144 | -9.5 -9.7 145 | -9.8 -9.9 146 | -10 -10 147 | 148 | In other words, it behaves like a hard limit / compressor with infinite ratio. 149 | 150 | Theadroom has no influence on the gain reduction, it just affects stability 151 | (temperature does exceed Tlim transiently, if the transient is > Theadroom 152 | it panics). Too low a Theadroom leads to unstable behavior. 153 | """ 154 | 155 | 156 | def dump_audata(labels, data): 157 | top = {} 158 | while data: 159 | hdr = data[:0xc] 160 | data = data[0xc:] 161 | typ, grp, cnt = struct.unpack(">III", hdr) 162 | d = {} 163 | for i in range(cnt): 164 | blk = data[:0x8] 165 | data = data[0x8:] 166 | key, val = struct.unpack(">If", blk) 167 | if typ in labels: 168 | if key in labels[typ]: 169 | key = labels[typ][key] 170 | d[key] = val 171 | top[(typ, grp)] = d 172 | pprint.pprint(top, stream=sys.stderr) 173 | return top 174 | 175 | def process_spp3(e): 176 | # Grab the plist file, which is mostly redundant but contains 177 | # some details not in the au preset 178 | for i in prop["Boxes"]: 179 | if i["Name"] == e["displayname"]: 180 | for p in i["Properties"]: 181 | if p["Number"] == 64003: 182 | path = os.path.join(base, "DSP", p["Path"].split("/DSP/")[1]) 183 | 184 | pl = plistlib.load(open(path, "rb")) 185 | 186 | d = dump_audata(LABELS["spp3"], e["aupreset"]["data"]) 187 | spkrs = "" 188 | channels = 0 189 | gbl = d[(0, 0)] 190 | for (typ, ch), p in sorted(d.items()): 191 | if typ != 4: 192 | continue 193 | chp = pl["ChannelSpecificParams"][f"Channel{ch}"] 194 | channels += 2 195 | spkrs += f""" 196 | 197 | [Speaker/{chp["SpeakerName"]}] 198 | group = {chp["SpeakerGroup"]} 199 | tr_coil = {p["T_sett_vc"]:.2f} 200 | tr_magnet = {p["T_sett_mg"]:.2f} 201 | tau_coil = {p["tau_Tvc"]:.2f} 202 | tau_magnet = {p["tau_Tmg"]:.2f} 203 | t_limit = {p["temperature limit"]:.1f} 204 | t_headroom = {p["hard temp limit headroom"]:.1f} 205 | z_nominal = {p["Reb_ref"]:.2f} 206 | z_shunt = {p["Rshunt"]:.2f} 207 | a_t_20c = {chp["CL"]["a_t_20C"]:.8f} 208 | a_t_35c = {chp["CL"]["a_t_35C"]:.8f} 209 | is_scale = 3.75 210 | vs_scale = 14 211 | is_chan = {2 * ch} 212 | vs_chan = {2 * ch + 1}""" 213 | 214 | print(f"""\ 215 | [Globals] 216 | visense_pcm = 2 217 | t_ambient = {gbl["ambient temperature"]} 218 | t_hysteresis = 5.0 219 | t_window = 20.0 220 | channels = {channels} 221 | period = 4096 222 | link_gains = True 223 | uclamp_max = 64 224 | 225 | [Controls] 226 | vsense = VSENSE Switch 227 | isense = ISENSE Switch 228 | amp_gain = Amp Gain Volume 229 | volume = Speaker Volume{spkrs}""") 230 | 231 | def process_atsp(e): 232 | # print(e) 233 | d = dump_audata(LABELS["atsp"], e["aupreset"]["data"])[(0,0)] 234 | t_ambient = None 235 | 236 | spkrs = "" 237 | channels = 0 238 | for gid, gn in enumerate("ABC"): 239 | p = f"speakerType {gn}: " 240 | ch = int(d[p + "Audio channel assignment"]) 241 | if not ch: 242 | continue 243 | if ch == 0xffff: 244 | ch = 1 245 | 246 | ambient = d[p + "Ambient temperature, [C]"] 247 | assert t_ambient is None or t_ambient == ambient 248 | t_ambient = ambient 249 | 250 | for i in range(16): 251 | if ch & (1 << i): 252 | channels += 2 253 | spkrs += f""" 254 | 255 | [Speaker/{gn}_ch{i}] 256 | group = {gid} 257 | tr_coil = {d[p + "VoiceCoil: thermal resistance [C/Watt]"]:.2f} 258 | tr_magnet = {d[p + "Magnet: thermal resistance [C/Watt]"]:.2f} 259 | tau_coil = {d[p + "Voice Coil: thermal time constant [s]"]:.2f} 260 | tau_magnet = {d[p + "Magnet: thermal time constant [s]"]:.2f} 261 | t_limit = {d[p + "Temperature limit [C]"]:.1f} 262 | t_headroom = {d[p + "Temperature hard limit headroom [C]"]:.1f} 263 | z_nominal = {d[p + "VoiceCoil: DC resistance [Ohms]"]:.2f} 264 | a_t_20c = 0.0037 265 | a_t_35c = 0.0037 266 | is_scale = 3.75 267 | vs_scale = 14 268 | is_chan = {2 * i} 269 | vs_chan = {2 * i + 1}""" 270 | 271 | print(f"""\ 272 | [Globals] 273 | visense_pcm = 2 274 | t_ambient = {t_ambient} 275 | t_hysteresis = 5.0 276 | t_window = 20.0 277 | channels = {channels} 278 | period = 4096 279 | link_gains = {bool(d["Gain link all audio channels"])} 280 | uclamp_max = 64 281 | 282 | [Controls] 283 | vsense = VSENSE Switch 284 | isense = ISENSE Switch 285 | amp_gain = Amp Gain Volume 286 | volume = Speaker Volume{spkrs}""") 287 | 288 | if __name__ == "__main__": 289 | base = sys.argv[1] 290 | 291 | au = plistlib.load(open(os.path.join(base, "DSP/Strips/builtin_speaker_out_general.austrip"), "rb")) 292 | try: 293 | prop = plistlib.load(open(os.path.join(base, "DSP/Strips/builtin_speaker_out_general.propstrip"), "rb")) 294 | except: 295 | prop = None 296 | 297 | for s in au["strips"]: 298 | for e in s["effects"]: 299 | if e["unit"]["subtype"].to_bytes(4) == b"spp3": 300 | process_spp3(e) 301 | if e["unit"]["subtype"].to_bytes(4) == b"atsp": 302 | process_atsp(e) 303 | 304 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // (C) 2022 The Asahi Linux Contributors 3 | /*! 4 | Handles speaker safety on Apple Silicon machines. This code is designed to 5 | fail safe. The kernel keeps the speakers capped at a low volume level until 6 | this daemon initializes. If at any time we run into an unrecoverable error 7 | or a timeout, we panic and let the kernel put the speakers back into a safe 8 | state. 9 | */ 10 | use std::collections::BTreeMap; 11 | use std::fs; 12 | use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe}; 13 | use std::path::{Path, PathBuf}; 14 | use std::sync::atomic::{AtomicBool, Ordering}; 15 | use std::sync::Arc; 16 | use std::time::Instant; 17 | 18 | use clap::Parser; 19 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 20 | use configparser::ini::Ini; 21 | use log::{debug, info, warn}; 22 | use simple_logger::SimpleLogger; 23 | 24 | mod blackbox; 25 | mod helpers; 26 | mod types; 27 | mod uclamp; 28 | 29 | const DEFAULT_CONFIG_PATH: &str = "share/speakersafetyd"; 30 | 31 | const UNLOCK_MAGIC: i32 = 0xdec1be15u32 as i32; 32 | 33 | const FLAGFILE: &str = "/run/speakersafetyd/speakersafetyd.flag"; 34 | 35 | /// Simple program to greet a person 36 | #[derive(Parser, Debug)] 37 | #[command(version, about, long_about = None)] 38 | struct Options { 39 | /// Path to the configuration file base directory 40 | #[arg(short, long)] 41 | config_path: Option, 42 | 43 | /// Increase the log level 44 | #[command(flatten)] 45 | verbose: Verbosity, 46 | 47 | /// Path to the blackbox dump directory 48 | #[arg(short, long)] 49 | blackbox_path: Option, 50 | 51 | /// Maximum gain reduction before panicing (for debugging) 52 | #[arg(short, long)] 53 | max_reduction: Option, 54 | } 55 | 56 | fn get_machine() -> String { 57 | fs::read_to_string("/proc/device-tree/compatible") 58 | .expect("Could not read device tree compatible") 59 | .split_once("\0") 60 | .expect("Unexpected compatible format") 61 | .0 62 | .trim_end_matches(|c: char| c.is_ascii_alphabetic()) 63 | .to_string() 64 | } 65 | 66 | fn get_speakers(config: &Ini) -> Vec { 67 | config 68 | .sections() 69 | .iter() 70 | .filter_map(|a| a.strip_prefix("Speaker/")) 71 | .map(|a| a.to_string()) 72 | .collect() 73 | } 74 | 75 | struct SpeakerGroup { 76 | speakers: Vec, 77 | gain: f32, 78 | } 79 | 80 | impl Default for SpeakerGroup { 81 | fn default() -> Self { 82 | Self { 83 | speakers: Default::default(), 84 | gain: f32::NAN, 85 | } 86 | } 87 | } 88 | 89 | fn main() { 90 | let args = Options::parse(); 91 | 92 | let sigquit = Arc::new(AtomicBool::new(false)); 93 | signal_hook::flag::register(signal_hook::consts::SIGQUIT, Arc::clone(&sigquit)).unwrap(); 94 | // signal_hook insists on using SA_RESTART, which we don't want. Override it. 95 | unsafe { 96 | let mut act: libc::sigaction = core::mem::zeroed(); 97 | assert!(libc::sigaction(signal_hook::consts::SIGQUIT, core::ptr::null(), &mut act) == 0); 98 | act.sa_flags &= !libc::SA_RESTART; 99 | assert!( 100 | libc::sigaction( 101 | signal_hook::consts::SIGQUIT, 102 | &mut act, 103 | core::ptr::null_mut() 104 | ) == 0 105 | ); 106 | } 107 | 108 | SimpleLogger::new() 109 | .with_level(args.verbose.log_level_filter()) 110 | .without_timestamps() 111 | .init() 112 | .unwrap(); 113 | info!("Starting up"); 114 | 115 | let mut config_path = args.config_path.unwrap_or_else(|| { 116 | let mut path = PathBuf::new(); 117 | path.push(option_env!("PREFIX").unwrap_or("/usr/local")); 118 | path.push(DEFAULT_CONFIG_PATH); 119 | path 120 | }); 121 | info!("Config base: {:?}", config_path); 122 | 123 | let machine: String = get_machine(); 124 | info!("Machine: {}", machine); 125 | 126 | let (maker, model) = machine 127 | .split_once(",") 128 | .expect("Unexpected machine name format"); 129 | 130 | config_path.push(maker); 131 | config_path.push(model); 132 | config_path.set_extension("conf"); 133 | info!("Config file: {:?}", config_path); 134 | 135 | let maker_titlecase = maker[0..1].to_ascii_uppercase() + &maker[1..]; 136 | 137 | let device = format!("hw:{}{}", maker_titlecase, model.to_ascii_uppercase()); 138 | info!("Device: {}", device); 139 | 140 | let mut cfg: Ini = Ini::new_cs(); 141 | cfg.load(config_path).expect("Failed to read config file"); 142 | 143 | let globals = types::Globals::parse(&cfg); 144 | 145 | if globals.uclamp_min.is_some() || globals.uclamp_max.is_some() { 146 | uclamp::set_uclamp( 147 | globals.uclamp_min.unwrap_or(0).try_into().unwrap(), 148 | globals.uclamp_max.unwrap_or(1024).try_into().unwrap(), 149 | ); 150 | } 151 | 152 | let mut blackbox = args.blackbox_path.map(|p| { 153 | info!("Enabling blackbox, path: {:?}", p); 154 | blackbox::Blackbox::new(&machine, &p, &globals) 155 | }); 156 | 157 | let mut blackbox_ref = AssertUnwindSafe(&mut blackbox); 158 | let result = catch_unwind(move || { 159 | let speaker_names = get_speakers(&cfg); 160 | let speaker_count = speaker_names.len(); 161 | info!("Found {} speakers", speaker_count); 162 | 163 | info!("Opening control device"); 164 | let ctl: alsa::ctl::Ctl = helpers::open_card(&device); 165 | 166 | let flag_path = Path::new(FLAGFILE); 167 | 168 | let cold_boot = match flag_path.try_exists() { 169 | Ok(true) => { 170 | info!("Startup mode: Warm boot"); 171 | false 172 | } 173 | Ok(false) => { 174 | info!("Startup mode: Cold boot"); 175 | if fs::write(flag_path, b"started").is_err() { 176 | warn!("Failed to write flag file, continuing as warm boot"); 177 | false 178 | } else { 179 | true 180 | } 181 | } 182 | Err(_) => { 183 | warn!("Failed to test flag file, continuing as warm boot"); 184 | false 185 | } 186 | }; 187 | 188 | let mut groups: BTreeMap = BTreeMap::new(); 189 | 190 | for i in speaker_names { 191 | let speaker: types::Speaker = types::Speaker::new(&globals, &i, &cfg, &ctl, cold_boot); 192 | 193 | groups 194 | .entry(speaker.group) 195 | .or_default() 196 | .speakers 197 | .push(speaker); 198 | } 199 | 200 | assert!( 201 | groups 202 | .values() 203 | .map(|a| a.speakers.len()) 204 | .sum::() 205 | == speaker_count 206 | ); 207 | assert!(2 * speaker_count <= globals.channels); 208 | 209 | let pcm_name = format!("{},{}", device, globals.visense_pcm); 210 | // Set up PCM to buffer in V/ISENSE 211 | let mut pcm: Option = 212 | Some(helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 0)); 213 | let mut io = Some(pcm.as_ref().unwrap().io_i16().unwrap()); 214 | 215 | let mut sample_rate_elem = types::Elem::new( 216 | "Speaker Sample Rate".to_string(), 217 | &ctl, 218 | alsa::ctl::ElemType::Integer, 219 | ); 220 | let mut sample_rate = sample_rate_elem.read_int(&ctl); 221 | 222 | let mut unlock_elem = types::Elem::new( 223 | "Speaker Volume Unlock".to_string(), 224 | &ctl, 225 | alsa::ctl::ElemType::Integer, 226 | ); 227 | 228 | unlock_elem.write_int(&ctl, UNLOCK_MAGIC); 229 | 230 | for (_idx, group) in groups.iter_mut() { 231 | if cold_boot { 232 | // Preset the gains to no reduction on cold boot 233 | group.speakers.iter_mut().for_each(|s| s.update(&ctl, 0.0)); 234 | group.gain = 0.0; 235 | } else { 236 | // Leave the gains at whatever the kernel limit is, use anything 237 | // random for group.gain so the gains will update on the first cycle. 238 | group.gain = -999.0; 239 | } 240 | } 241 | 242 | let mut last_update = Instant::now(); 243 | 244 | let mut buf = Vec::new(); 245 | buf.resize(globals.period * globals.channels, 0i16); 246 | 247 | let mut once_nominal = false; 248 | 249 | loop { 250 | if sigquit.load(Ordering::Relaxed) { 251 | panic!("SIGQUIT received"); 252 | } 253 | // Block while we're reading into the buffer 254 | let read = io.as_ref().unwrap().readi(&mut buf); 255 | 256 | #[allow(unused_mut)] 257 | #[allow(unused_assignments)] 258 | let read = match read { 259 | Ok(a) => Ok(a), 260 | Err(e) => { 261 | if sigquit.load(Ordering::Relaxed) { 262 | panic!("SIGQUIT received"); 263 | } 264 | if e.errno() == libc::ESTRPIPE { 265 | warn!("Suspend detected!"); 266 | /* 267 | // Resume handling 268 | loop { 269 | match pcm.resume() { 270 | Ok(_) => break Ok(0), 271 | Err(e) if e.errno() == Errno::EAGAIN => continue, 272 | Err(e) => break Err(e), 273 | } 274 | } 275 | .unwrap(); 276 | warn!("Resume successful"); 277 | */ 278 | // Work around kernel issue: resume sometimes breaks visense 279 | warn!("Reinitializing PCM to work around kernel bug..."); 280 | io = None; 281 | pcm = None; 282 | pcm = Some(helpers::open_pcm(&pcm_name, globals.channels.try_into().unwrap(), 0)); 283 | io = Some(pcm.as_ref().unwrap().io_i16().unwrap()); 284 | continue; 285 | } 286 | Err(e) 287 | } 288 | } 289 | .unwrap(); 290 | 291 | if read != globals.period { 292 | warn!("Expected {} samples, got {}", globals.period, read); 293 | } 294 | 295 | if sigquit.load(Ordering::Relaxed) { 296 | panic!("SIGQUIT received"); 297 | } 298 | 299 | let buf_read = &buf[0..read * globals.channels]; 300 | 301 | let cur_sample_rate = sample_rate_elem.read_int(&ctl); 302 | 303 | if cur_sample_rate != 0 && cur_sample_rate != sample_rate { 304 | sample_rate = cur_sample_rate; 305 | info!("Sample rate: {}", sample_rate); 306 | if let Some(bb) = blackbox_ref.as_mut() { bb.reset() } 307 | } 308 | 309 | if sample_rate == 0 { 310 | panic!("Invalid sample rate"); 311 | } 312 | 313 | let now = Instant::now(); 314 | let dt = (now - last_update).as_secs_f64(); 315 | assert!(dt > 0f64); 316 | 317 | let pt = globals.period as f64 / sample_rate as f64; 318 | /* If we skipped at least 4 periods, run catchup for that minus one */ 319 | if dt > (4f64 * pt) { 320 | let skip = dt - pt; 321 | debug!("Skipping {:.2} seconds", skip); 322 | for (_, group) in groups.iter_mut() { 323 | group.speakers.iter_mut().for_each(|s| s.skip_model(skip)); 324 | } 325 | if let Some(bb) = blackbox_ref.as_mut() { bb.reset() } 326 | } 327 | 328 | last_update = now; 329 | 330 | if let Some(bb) = blackbox_ref.as_mut() { 331 | let max_idx = *groups.iter().map(|g| g.0).max().unwrap(); 332 | let gstates = (0..=max_idx) 333 | .map(|i| groups[&i].speakers.iter().map(|s| s.s).collect()) 334 | .collect(); 335 | bb.push(sample_rate, buf_read.to_vec(), gstates); 336 | } 337 | 338 | let mut all_nominal = true; 339 | for (idx, group) in groups.iter_mut() { 340 | let gain = group 341 | .speakers 342 | .iter_mut() 343 | .map(|s| s.run_model(buf_read, sample_rate as f32)) 344 | .reduce(f32::min) 345 | .unwrap(); 346 | if gain != group.gain { 347 | if gain == 0. { 348 | info!("Speaker group {} gain nominal", idx); 349 | } else { 350 | info!("Speaker group {} gain limited to {:.2} dBFS", idx, gain); 351 | } 352 | group.speakers.iter_mut().for_each(|s| s.update(&ctl, gain)); 353 | group.gain = gain; 354 | } 355 | if gain != 0. { 356 | all_nominal = false; 357 | } 358 | if let Some(max_reduction) = args.max_reduction { 359 | if once_nominal && gain < -max_reduction { 360 | panic!("Gain reduction exceeded threshold"); 361 | } 362 | } 363 | } 364 | 365 | if all_nominal { 366 | once_nominal = true; 367 | } 368 | 369 | unlock_elem.write_int(&ctl, UNLOCK_MAGIC); 370 | } 371 | }); 372 | if let Err(e) = result { 373 | warn!("Panic!"); 374 | 375 | let mut reason: String = "Unknown panic".into(); 376 | 377 | if let Some(s) = e.downcast_ref::<&'static str>() { 378 | reason = (*s).into(); 379 | } else if let Some(s) = e.downcast_ref::() { 380 | reason = s.clone(); 381 | } 382 | 383 | blackbox.as_mut().map(|bb| { 384 | if bb.preserve(reason).is_err() { 385 | warn!("Failed to write blackbox"); 386 | } 387 | }); 388 | 389 | resume_unwind(e); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // (C) 2022 The Asahi Linux Contributors 3 | 4 | use alsa::ctl::Ctl; 5 | use configparser::ini::Ini; 6 | use log::{debug, info}; 7 | use std::ffi::{CStr, CString}; 8 | 9 | use crate::helpers; 10 | 11 | /** 12 | Struct with fields necessary for manipulating an ALSA elem. 13 | 14 | The val field is created using a wrapper so that we can handle 15 | any errors. 16 | */ 17 | pub struct Elem { 18 | elem_name: String, 19 | id: alsa::ctl::ElemId, 20 | val: alsa::ctl::ElemValue, 21 | } 22 | 23 | impl Elem { 24 | pub fn new(name: String, card: &Ctl, t: alsa::ctl::ElemType) -> Elem { 25 | // CString::new() cannot borrow a String. We want name for the elem 26 | // for error identification though, so it can't consume name directly. 27 | let borrow: String = name.clone(); 28 | 29 | let mut new_elem: Elem = { 30 | Elem { 31 | elem_name: name, 32 | id: alsa::ctl::ElemId::new(alsa::ctl::ElemIface::Mixer), 33 | val: helpers::new_elemvalue(t), 34 | } 35 | }; 36 | 37 | let cname: CString = CString::new(borrow).unwrap(); 38 | let cstr: &CStr = cname.as_c_str(); 39 | 40 | new_elem.id.set_name(cstr); 41 | new_elem.val.set_id(&new_elem.id); 42 | helpers::lock_el(card, &new_elem.id, &new_elem.elem_name); 43 | helpers::read_ev(card, &mut new_elem.val, &new_elem.elem_name); 44 | 45 | new_elem 46 | } 47 | 48 | pub fn read_int(&mut self, card: &Ctl) -> i32 { 49 | helpers::read_ev(card, &mut self.val, &self.elem_name); 50 | 51 | self.val 52 | .get_integer(0) 53 | .unwrap_or_else(|| panic!("Could not read {}", self.elem_name)) 54 | } 55 | 56 | pub fn write_int(&mut self, card: &Ctl, value: i32) { 57 | self.val 58 | .set_integer(0, value) 59 | .unwrap_or_else(|| panic!("Could not set {}", self.elem_name)); 60 | helpers::write_ev(card, &mut self.val, &self.elem_name); 61 | } 62 | } 63 | 64 | /** 65 | Mixer struct representing the controls associated with a given 66 | Speaker. Populated with the important ALSA controls at runtime. 67 | 68 | level: mixer volume control 69 | vsense: VSENSE switch 70 | isense: ISENSE switch 71 | 72 | */ 73 | struct Mixer { 74 | drv: String, 75 | level: Elem, 76 | amp_gain: Elem, 77 | } 78 | 79 | impl Mixer { 80 | // TODO: implement turning on V/ISENSE 81 | fn new(name: &str, card: &Ctl, globals: &Globals) -> Mixer { 82 | let prefix = if name == "Mono" { 83 | "".to_string() 84 | } else { 85 | name.to_owned() + " " 86 | }; 87 | 88 | let mut vs = Elem::new( 89 | prefix.clone() + &globals.ctl_vsense, 90 | card, 91 | alsa::ctl::ElemType::Boolean, 92 | ); 93 | 94 | vs.val.set_boolean(0, true); 95 | helpers::write_ev(card, &vs.val, &vs.elem_name); 96 | helpers::read_ev(card, &mut vs.val, &vs.elem_name); 97 | assert!(vs.val.get_boolean(0).unwrap()); 98 | 99 | let mut is = Elem::new( 100 | prefix.clone() + &globals.ctl_isense, 101 | card, 102 | alsa::ctl::ElemType::Boolean, 103 | ); 104 | 105 | is.val.set_boolean(0, true); 106 | helpers::write_ev(card, &is.val, &is.elem_name); 107 | helpers::read_ev(card, &mut vs.val, &vs.elem_name); 108 | assert!(vs.val.get_boolean(0).unwrap()); 109 | 110 | let mut ret = Mixer { 111 | drv: name.to_owned(), 112 | level: Elem::new( 113 | prefix.clone() + &globals.ctl_volume, 114 | card, 115 | alsa::ctl::ElemType::Integer, 116 | ), 117 | amp_gain: Elem::new( 118 | prefix + &globals.ctl_amp_gain, 119 | card, 120 | alsa::ctl::ElemType::Integer, 121 | ), 122 | }; 123 | 124 | /* 125 | * Set amp gain to max available (kernel should've clamped). 126 | * alsa-rs only has bindings for range in dB, so we go through 127 | * that. 128 | */ 129 | 130 | let (_min, max) = 131 | helpers::get_range_db(card, &mut ret.amp_gain.id, &ret.amp_gain.elem_name); 132 | let max_int = card 133 | .convert_from_db(&mut ret.amp_gain.id, max, alsa::Round::Floor) 134 | .unwrap(); 135 | 136 | ret.amp_gain.val.set_integer(0, max_int.try_into().unwrap()); 137 | 138 | helpers::write_ev(card, &ret.amp_gain.val, &ret.amp_gain.elem_name); 139 | 140 | ret 141 | } 142 | 143 | fn get_amp_gain(&mut self, card: &Ctl) -> f32 { 144 | helpers::read_ev(card, &mut self.amp_gain.val, &self.amp_gain.elem_name); 145 | 146 | let val = self 147 | .amp_gain 148 | .val 149 | .get_integer(0) 150 | .unwrap_or_else(|| panic!("Could not read amp gain for {}", self.drv)); 151 | 152 | helpers::int_to_db(card, &self.amp_gain.id, val).to_db() 153 | } 154 | 155 | /* 156 | fn get_lvl(&mut self, card: &Ctl) -> f32 { 157 | helpers::read_ev(card, &mut self.level.val, &self.level.elem_name); 158 | 159 | let val = self 160 | .level 161 | .val 162 | .get_integer(0) 163 | .expect(&format!("Could not read level for {}", self.drv)); 164 | 165 | helpers::int_to_db(card, &self.level.id, val).to_db() 166 | } 167 | */ 168 | 169 | fn set_lvl(&mut self, card: &Ctl, lvl: f32) { 170 | let new_val: i32 = helpers::db_to_int(card, &self.level.id, lvl); 171 | 172 | match self.level.val.set_integer(0, new_val) { 173 | Some(_) => {} 174 | None => { 175 | panic!("Could not set level for {}", self.drv); 176 | } 177 | }; 178 | 179 | helpers::write_ev(card, &self.level.val, &self.level.elem_name); 180 | } 181 | } 182 | 183 | #[derive(Clone)] 184 | pub struct Globals { 185 | pub visense_pcm: usize, 186 | pub channels: usize, 187 | pub period: usize, 188 | pub t_ambient: f32, 189 | pub t_window: f32, 190 | pub t_hysteresis: f32, 191 | pub ctl_vsense: String, 192 | pub ctl_isense: String, 193 | pub ctl_amp_gain: String, 194 | pub ctl_volume: String, 195 | pub uclamp_min: Option, 196 | pub uclamp_max: Option, 197 | } 198 | 199 | impl Globals { 200 | pub fn parse(config: &Ini) -> Self { 201 | Self { 202 | visense_pcm: helpers::parse_int(config, "Globals", "visense_pcm"), 203 | channels: helpers::parse_int(config, "Globals", "channels"), 204 | period: helpers::parse_int(config, "Globals", "period"), 205 | t_ambient: helpers::parse_float(config, "Globals", "t_ambient"), 206 | t_window: helpers::parse_float(config, "Globals", "t_window"), 207 | t_hysteresis: helpers::parse_float(config, "Globals", "t_hysteresis"), 208 | ctl_vsense: helpers::parse_string(config, "Controls", "vsense"), 209 | ctl_isense: helpers::parse_string(config, "Controls", "isense"), 210 | ctl_amp_gain: helpers::parse_string(config, "Controls", "amp_gain"), 211 | ctl_volume: helpers::parse_string(config, "Controls", "volume"), 212 | uclamp_min: helpers::parse_opt_int(config, "Globals", "uclamp_min"), 213 | uclamp_max: helpers::parse_opt_int(config, "Globals", "uclamp_max"), 214 | } 215 | } 216 | } 217 | 218 | /** 219 | Struct representing a driver. Parameters are parsed out of a config 220 | file, which is loaded at runtime based on the machine's DT compatible 221 | string. 222 | 223 | name: driver name as it appears in ALSA 224 | alsa_iface: Mixer struct with handles to the driver's control elements 225 | r_dc: dc resistance of the voice coil (ohms) 226 | tau_coil: voice coil ramp time constant (seconds) 227 | tau_magnet: magnet ramp time constant (seconds) 228 | tr_coil: thermal resistance of voice coil (*C/W) 229 | t_limit: absolute max temp of the voice coil (*C) 230 | 231 | Borrows the handle to the control interface to do calculations. 232 | */ 233 | #[derive(Debug, Default, Copy, Clone)] 234 | pub struct SpeakerState { 235 | pub t_coil: f64, 236 | pub t_magnet: f64, 237 | 238 | pub t_coil_hyst: f32, 239 | pub t_magnet_hyst: f32, 240 | 241 | pub min_gain: f32, 242 | pub gain: f32, 243 | } 244 | 245 | pub struct Speaker { 246 | pub name: String, 247 | pub group: usize, 248 | alsa_iface: Mixer, 249 | tau_coil: f32, 250 | tau_magnet: f32, 251 | tr_coil: f32, 252 | tr_magnet: f32, 253 | t_limit: f32, 254 | t_headroom: f32, 255 | z_nominal: f32, 256 | is_scale: f32, 257 | vs_scale: f32, 258 | is_chan: usize, 259 | vs_chan: usize, 260 | 261 | g: Globals, 262 | pub s: SpeakerState, 263 | } 264 | 265 | impl Speaker { 266 | pub fn new(globals: &Globals, name: &str, config: &Ini, ctl: &Ctl, cold_boot: bool) -> Speaker { 267 | info!("Speaker [{}]:", name); 268 | 269 | let section = "Speaker/".to_owned() + name; 270 | let mut new_speaker: Speaker = Speaker { 271 | name: name.to_string(), 272 | alsa_iface: Mixer::new(name, ctl, globals), 273 | group: helpers::parse_int(config, §ion, "group"), 274 | tau_coil: helpers::parse_float(config, §ion, "tau_coil"), 275 | tau_magnet: helpers::parse_float(config, §ion, "tau_magnet"), 276 | tr_coil: helpers::parse_float(config, §ion, "tr_coil"), 277 | tr_magnet: helpers::parse_float(config, §ion, "tr_magnet"), 278 | t_limit: helpers::parse_float(config, §ion, "t_limit"), 279 | t_headroom: helpers::parse_float(config, §ion, "t_headroom"), 280 | z_nominal: helpers::parse_float(config, §ion, "z_nominal"), 281 | is_scale: helpers::parse_float(config, §ion, "is_scale"), 282 | vs_scale: helpers::parse_float(config, §ion, "vs_scale"), 283 | is_chan: helpers::parse_int(config, §ion, "is_chan"), 284 | vs_chan: helpers::parse_int(config, §ion, "vs_chan"), 285 | g: globals.clone(), 286 | s: Default::default(), 287 | }; 288 | 289 | let s = &mut new_speaker.s; 290 | 291 | s.t_coil = if cold_boot { 292 | // Assume warm but not warm enough to limit 293 | (new_speaker.t_limit - globals.t_window) as f64 - 1f64 294 | } else { 295 | // Worst case startup assumption 296 | new_speaker.t_limit as f64 297 | }; 298 | s.t_magnet = globals.t_ambient as f64 299 | + (s.t_coil - globals.t_ambient as f64) 300 | * (new_speaker.tr_magnet / (new_speaker.tr_magnet + new_speaker.tr_coil)) as f64; 301 | 302 | let max_dt = new_speaker.t_limit - globals.t_ambient; 303 | let max_pwr = max_dt / (new_speaker.tr_magnet + new_speaker.tr_coil); 304 | 305 | let amp_gain = new_speaker.alsa_iface.get_amp_gain(ctl); 306 | 307 | // Worst-case peak power is 2x RMS power 308 | let peak_pwr = 10f32.powf(amp_gain / 10.) / new_speaker.z_nominal * 2.; 309 | 310 | s.min_gain = ((max_pwr / peak_pwr).log10() * 10.).min(0.); 311 | 312 | assert!(new_speaker.is_chan < globals.channels); 313 | assert!(new_speaker.vs_chan < globals.channels); 314 | assert!(new_speaker.t_limit - globals.t_window > globals.t_ambient); 315 | 316 | info!(" Group: {}", new_speaker.group); 317 | info!(" Max temperature: {:.1} °C", new_speaker.t_limit); 318 | info!(" Amp gain: {} dBV", amp_gain); 319 | info!(" Max power: {:.2} W", max_pwr); 320 | info!(" Peak power: {} W", peak_pwr); 321 | info!(" Min gain: {:.2} dB", s.min_gain); 322 | 323 | new_speaker 324 | } 325 | 326 | pub fn run_model(&mut self, buf: &[i16], sample_rate: f32) -> f32 { 327 | let s = &mut self.s; 328 | 329 | let step = 1. / sample_rate; 330 | let alpha_coil = (step / (self.tau_coil + step)) as f64; 331 | let alpha_magnet = (step / (self.tau_magnet + step)) as f64; 332 | 333 | let mut pwr_sum = 0f32; 334 | 335 | for sample in buf.chunks(self.g.channels) { 336 | assert!(sample.len() == self.g.channels); 337 | 338 | let v = sample[self.vs_chan] as f32 / 32768.0 * self.vs_scale; 339 | let i = sample[self.is_chan] as f32 / 32768.0 * self.is_scale; 340 | let p = v * i; 341 | 342 | let t_coil_target = s.t_magnet + (p * self.tr_coil) as f64; 343 | let t_magnet_target = (self.g.t_ambient + p * self.tr_magnet) as f64; 344 | 345 | s.t_coil = t_coil_target * alpha_coil + s.t_coil * (1. - alpha_coil); 346 | s.t_magnet = t_magnet_target * alpha_magnet + s.t_magnet * (1. - alpha_magnet); 347 | 348 | if s.t_coil > (self.t_limit + self.t_headroom) as f64 { 349 | panic!( 350 | "{}: Coil temperature limit exceeded ({} > {})", 351 | self.name, s.t_coil, self.t_limit 352 | ); 353 | } 354 | if s.t_magnet > (self.t_limit + self.t_headroom) as f64 { 355 | panic!( 356 | "{}: Magnet temperature limit exceeded ({} > {})", 357 | self.name, s.t_magnet, self.t_limit 358 | ); 359 | } 360 | 361 | pwr_sum += p; 362 | } 363 | 364 | let pwr_avg: f32 = pwr_sum / ((buf.len() / self.g.channels) as f32); 365 | /* 366 | * This really shouldn't happen other than rounding error, 367 | * if it does there's probably something wrong with the ivsense 368 | * data. 369 | */ 370 | if pwr_avg < -0.01 { 371 | panic!( 372 | "{}: Negative power, bad ivsense data? ({})", 373 | self.name, pwr_avg 374 | ); 375 | } 376 | let pwr_avg = pwr_avg.max(0.0); 377 | 378 | s.t_coil_hyst = s 379 | .t_coil_hyst 380 | .max(s.t_coil as f32) 381 | .min(s.t_coil as f32 + self.g.t_hysteresis); 382 | s.t_magnet_hyst = s 383 | .t_magnet_hyst 384 | .max(s.t_magnet as f32) 385 | .min(s.t_magnet as f32 + self.g.t_hysteresis); 386 | 387 | let temp = s.t_coil_hyst.max(s.t_magnet_hyst); 388 | 389 | let reduction = (temp - (self.t_limit - self.g.t_window)) / self.g.t_window; 390 | let gain = s.min_gain * reduction.max(0.); 391 | 392 | s.gain = gain; 393 | 394 | if s.gain > -0.01 { 395 | s.gain = 0.; 396 | } 397 | 398 | debug!( 399 | "{:>15}: Coil {:>6.2} °C Magnet {:>6.2} °C Power {:>5.2} W Gain {:>6.2} dB", 400 | self.name, s.t_coil, s.t_magnet, pwr_avg, s.gain 401 | ); 402 | 403 | s.gain 404 | } 405 | 406 | pub fn skip_model(&mut self, time: f64) { 407 | let s = &mut self.s; 408 | let t_coil = s.t_coil - self.g.t_ambient as f64; 409 | let t_magnet = s.t_magnet - self.g.t_ambient as f64; 410 | 411 | let eta = 1f64 / (1f64 - (self.tau_coil / self.tau_magnet) as f64); 412 | let a = (-time / self.tau_coil as f64).exp() * (t_coil - eta * t_magnet); 413 | let b = (-time / self.tau_magnet as f64).exp() * t_magnet; 414 | 415 | s.t_coil = self.g.t_ambient as f64 + a + b * eta; 416 | s.t_magnet = self.g.t_ambient as f64 + b; 417 | 418 | debug!( 419 | "{}: SKIP: Coil {:.2} °C Magnet {:.2} °C ({:.2} seconds)", 420 | self.name, s.t_coil, s.t_magnet, time 421 | ); 422 | } 423 | 424 | pub fn update(&mut self, ctl: &Ctl, gain: f32) { 425 | self.alsa_iface.set_lvl(ctl, gain); 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "alsa" 7 | version = "0.9.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" 10 | dependencies = [ 11 | "alsa-sys", 12 | "bitflags", 13 | "cfg-if", 14 | "libc", 15 | ] 16 | 17 | [[package]] 18 | name = "alsa-sys" 19 | version = "0.3.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" 22 | dependencies = [ 23 | "libc", 24 | "pkg-config", 25 | ] 26 | 27 | [[package]] 28 | name = "android-tzdata" 29 | version = "0.1.1" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 32 | 33 | [[package]] 34 | name = "android_system_properties" 35 | version = "0.1.5" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 38 | dependencies = [ 39 | "libc", 40 | ] 41 | 42 | [[package]] 43 | name = "anstream" 44 | version = "0.6.18" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 47 | dependencies = [ 48 | "anstyle", 49 | "anstyle-parse", 50 | "anstyle-query", 51 | "anstyle-wincon", 52 | "colorchoice", 53 | "is_terminal_polyfill", 54 | "utf8parse", 55 | ] 56 | 57 | [[package]] 58 | name = "anstyle" 59 | version = "1.0.10" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 62 | 63 | [[package]] 64 | name = "anstyle-parse" 65 | version = "0.2.6" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 68 | dependencies = [ 69 | "utf8parse", 70 | ] 71 | 72 | [[package]] 73 | name = "anstyle-query" 74 | version = "1.1.2" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 77 | dependencies = [ 78 | "windows-sys 0.59.0", 79 | ] 80 | 81 | [[package]] 82 | name = "anstyle-wincon" 83 | version = "3.0.7" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 86 | dependencies = [ 87 | "anstyle", 88 | "once_cell", 89 | "windows-sys 0.59.0", 90 | ] 91 | 92 | [[package]] 93 | name = "autocfg" 94 | version = "1.4.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 97 | 98 | [[package]] 99 | name = "bitflags" 100 | version = "2.9.0" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 103 | 104 | [[package]] 105 | name = "bumpalo" 106 | version = "3.17.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 109 | 110 | [[package]] 111 | name = "cc" 112 | version = "1.2.17" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" 115 | dependencies = [ 116 | "shlex", 117 | ] 118 | 119 | [[package]] 120 | name = "cfg-if" 121 | version = "1.0.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 124 | 125 | [[package]] 126 | name = "chrono" 127 | version = "0.4.40" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 130 | dependencies = [ 131 | "android-tzdata", 132 | "iana-time-zone", 133 | "js-sys", 134 | "num-traits", 135 | "wasm-bindgen", 136 | "windows-link", 137 | ] 138 | 139 | [[package]] 140 | name = "clap" 141 | version = "4.5.34" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" 144 | dependencies = [ 145 | "clap_builder", 146 | "clap_derive", 147 | ] 148 | 149 | [[package]] 150 | name = "clap-verbosity-flag" 151 | version = "2.2.3" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "34c77f67047557f62582784fd7482884697731b2932c7d37ced54bce2312e1e2" 154 | dependencies = [ 155 | "clap", 156 | "log", 157 | ] 158 | 159 | [[package]] 160 | name = "clap_builder" 161 | version = "4.5.34" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" 164 | dependencies = [ 165 | "anstream", 166 | "anstyle", 167 | "clap_lex", 168 | "strsim", 169 | ] 170 | 171 | [[package]] 172 | name = "clap_derive" 173 | version = "4.5.32" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 176 | dependencies = [ 177 | "heck", 178 | "proc-macro2", 179 | "quote", 180 | "syn", 181 | ] 182 | 183 | [[package]] 184 | name = "clap_lex" 185 | version = "0.7.4" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 188 | 189 | [[package]] 190 | name = "colorchoice" 191 | version = "1.0.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 194 | 195 | [[package]] 196 | name = "colored" 197 | version = "2.2.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" 200 | dependencies = [ 201 | "lazy_static", 202 | "windows-sys 0.59.0", 203 | ] 204 | 205 | [[package]] 206 | name = "configparser" 207 | version = "3.1.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" 210 | dependencies = [ 211 | "indexmap", 212 | ] 213 | 214 | [[package]] 215 | name = "core-foundation-sys" 216 | version = "0.8.7" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 219 | 220 | [[package]] 221 | name = "deranged" 222 | version = "0.4.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" 225 | dependencies = [ 226 | "powerfmt", 227 | ] 228 | 229 | [[package]] 230 | name = "equivalent" 231 | version = "1.0.2" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 234 | 235 | [[package]] 236 | name = "hashbrown" 237 | version = "0.15.2" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 240 | 241 | [[package]] 242 | name = "heck" 243 | version = "0.5.0" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 246 | 247 | [[package]] 248 | name = "iana-time-zone" 249 | version = "0.1.62" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" 252 | dependencies = [ 253 | "android_system_properties", 254 | "core-foundation-sys", 255 | "iana-time-zone-haiku", 256 | "js-sys", 257 | "log", 258 | "wasm-bindgen", 259 | "windows-core", 260 | ] 261 | 262 | [[package]] 263 | name = "iana-time-zone-haiku" 264 | version = "0.1.2" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 267 | dependencies = [ 268 | "cc", 269 | ] 270 | 271 | [[package]] 272 | name = "indexmap" 273 | version = "2.8.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 276 | dependencies = [ 277 | "equivalent", 278 | "hashbrown", 279 | ] 280 | 281 | [[package]] 282 | name = "is_terminal_polyfill" 283 | version = "1.70.1" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 286 | 287 | [[package]] 288 | name = "itoa" 289 | version = "1.0.15" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 292 | 293 | [[package]] 294 | name = "js-sys" 295 | version = "0.3.77" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 298 | dependencies = [ 299 | "once_cell", 300 | "wasm-bindgen", 301 | ] 302 | 303 | [[package]] 304 | name = "json" 305 | version = "0.12.4" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" 308 | 309 | [[package]] 310 | name = "lazy_static" 311 | version = "1.5.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 314 | 315 | [[package]] 316 | name = "libc" 317 | version = "0.2.171" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 320 | 321 | [[package]] 322 | name = "log" 323 | version = "0.4.27" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 326 | 327 | [[package]] 328 | name = "num-conv" 329 | version = "0.1.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 332 | 333 | [[package]] 334 | name = "num-traits" 335 | version = "0.2.19" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 338 | dependencies = [ 339 | "autocfg", 340 | ] 341 | 342 | [[package]] 343 | name = "num_threads" 344 | version = "0.1.7" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 347 | dependencies = [ 348 | "libc", 349 | ] 350 | 351 | [[package]] 352 | name = "once_cell" 353 | version = "1.21.3" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 356 | 357 | [[package]] 358 | name = "pkg-config" 359 | version = "0.3.32" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 362 | 363 | [[package]] 364 | name = "powerfmt" 365 | version = "0.2.0" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 368 | 369 | [[package]] 370 | name = "proc-macro2" 371 | version = "1.0.94" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 374 | dependencies = [ 375 | "unicode-ident", 376 | ] 377 | 378 | [[package]] 379 | name = "quote" 380 | version = "1.0.40" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 383 | dependencies = [ 384 | "proc-macro2", 385 | ] 386 | 387 | [[package]] 388 | name = "rustversion" 389 | version = "1.0.20" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 392 | 393 | [[package]] 394 | name = "serde" 395 | version = "1.0.219" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 398 | dependencies = [ 399 | "serde_derive", 400 | ] 401 | 402 | [[package]] 403 | name = "serde_derive" 404 | version = "1.0.219" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 407 | dependencies = [ 408 | "proc-macro2", 409 | "quote", 410 | "syn", 411 | ] 412 | 413 | [[package]] 414 | name = "shlex" 415 | version = "1.3.0" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 418 | 419 | [[package]] 420 | name = "signal-hook" 421 | version = "0.3.17" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 424 | dependencies = [ 425 | "libc", 426 | "signal-hook-registry", 427 | ] 428 | 429 | [[package]] 430 | name = "signal-hook-registry" 431 | version = "1.4.2" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 434 | dependencies = [ 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "simple_logger" 440 | version = "4.3.3" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "8e7e46c8c90251d47d08b28b8a419ffb4aede0f87c2eea95e17d1d5bacbf3ef1" 443 | dependencies = [ 444 | "colored", 445 | "log", 446 | "time", 447 | "windows-sys 0.48.0", 448 | ] 449 | 450 | [[package]] 451 | name = "speakersafetyd" 452 | version = "1.1.2" 453 | dependencies = [ 454 | "alsa", 455 | "chrono", 456 | "clap", 457 | "clap-verbosity-flag", 458 | "configparser", 459 | "json", 460 | "libc", 461 | "log", 462 | "signal-hook", 463 | "simple_logger", 464 | ] 465 | 466 | [[package]] 467 | name = "strsim" 468 | version = "0.11.1" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 471 | 472 | [[package]] 473 | name = "syn" 474 | version = "2.0.100" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 477 | dependencies = [ 478 | "proc-macro2", 479 | "quote", 480 | "unicode-ident", 481 | ] 482 | 483 | [[package]] 484 | name = "time" 485 | version = "0.3.41" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 488 | dependencies = [ 489 | "deranged", 490 | "itoa", 491 | "libc", 492 | "num-conv", 493 | "num_threads", 494 | "powerfmt", 495 | "serde", 496 | "time-core", 497 | "time-macros", 498 | ] 499 | 500 | [[package]] 501 | name = "time-core" 502 | version = "0.1.4" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 505 | 506 | [[package]] 507 | name = "time-macros" 508 | version = "0.2.22" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 511 | dependencies = [ 512 | "num-conv", 513 | "time-core", 514 | ] 515 | 516 | [[package]] 517 | name = "unicode-ident" 518 | version = "1.0.18" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 521 | 522 | [[package]] 523 | name = "utf8parse" 524 | version = "0.2.2" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 527 | 528 | [[package]] 529 | name = "wasm-bindgen" 530 | version = "0.2.100" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 533 | dependencies = [ 534 | "cfg-if", 535 | "once_cell", 536 | "rustversion", 537 | "wasm-bindgen-macro", 538 | ] 539 | 540 | [[package]] 541 | name = "wasm-bindgen-backend" 542 | version = "0.2.100" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 545 | dependencies = [ 546 | "bumpalo", 547 | "log", 548 | "proc-macro2", 549 | "quote", 550 | "syn", 551 | "wasm-bindgen-shared", 552 | ] 553 | 554 | [[package]] 555 | name = "wasm-bindgen-macro" 556 | version = "0.2.100" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 559 | dependencies = [ 560 | "quote", 561 | "wasm-bindgen-macro-support", 562 | ] 563 | 564 | [[package]] 565 | name = "wasm-bindgen-macro-support" 566 | version = "0.2.100" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 569 | dependencies = [ 570 | "proc-macro2", 571 | "quote", 572 | "syn", 573 | "wasm-bindgen-backend", 574 | "wasm-bindgen-shared", 575 | ] 576 | 577 | [[package]] 578 | name = "wasm-bindgen-shared" 579 | version = "0.2.100" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 582 | dependencies = [ 583 | "unicode-ident", 584 | ] 585 | 586 | [[package]] 587 | name = "windows-core" 588 | version = "0.52.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 591 | dependencies = [ 592 | "windows-targets 0.52.6", 593 | ] 594 | 595 | [[package]] 596 | name = "windows-link" 597 | version = "0.1.1" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 600 | 601 | [[package]] 602 | name = "windows-sys" 603 | version = "0.48.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 606 | dependencies = [ 607 | "windows-targets 0.48.5", 608 | ] 609 | 610 | [[package]] 611 | name = "windows-sys" 612 | version = "0.59.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 615 | dependencies = [ 616 | "windows-targets 0.52.6", 617 | ] 618 | 619 | [[package]] 620 | name = "windows-targets" 621 | version = "0.48.5" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 624 | dependencies = [ 625 | "windows_aarch64_gnullvm 0.48.5", 626 | "windows_aarch64_msvc 0.48.5", 627 | "windows_i686_gnu 0.48.5", 628 | "windows_i686_msvc 0.48.5", 629 | "windows_x86_64_gnu 0.48.5", 630 | "windows_x86_64_gnullvm 0.48.5", 631 | "windows_x86_64_msvc 0.48.5", 632 | ] 633 | 634 | [[package]] 635 | name = "windows-targets" 636 | version = "0.52.6" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 639 | dependencies = [ 640 | "windows_aarch64_gnullvm 0.52.6", 641 | "windows_aarch64_msvc 0.52.6", 642 | "windows_i686_gnu 0.52.6", 643 | "windows_i686_gnullvm", 644 | "windows_i686_msvc 0.52.6", 645 | "windows_x86_64_gnu 0.52.6", 646 | "windows_x86_64_gnullvm 0.52.6", 647 | "windows_x86_64_msvc 0.52.6", 648 | ] 649 | 650 | [[package]] 651 | name = "windows_aarch64_gnullvm" 652 | version = "0.48.5" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 655 | 656 | [[package]] 657 | name = "windows_aarch64_gnullvm" 658 | version = "0.52.6" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 661 | 662 | [[package]] 663 | name = "windows_aarch64_msvc" 664 | version = "0.48.5" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 667 | 668 | [[package]] 669 | name = "windows_aarch64_msvc" 670 | version = "0.52.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 673 | 674 | [[package]] 675 | name = "windows_i686_gnu" 676 | version = "0.48.5" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 679 | 680 | [[package]] 681 | name = "windows_i686_gnu" 682 | version = "0.52.6" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 685 | 686 | [[package]] 687 | name = "windows_i686_gnullvm" 688 | version = "0.52.6" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 691 | 692 | [[package]] 693 | name = "windows_i686_msvc" 694 | version = "0.48.5" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 697 | 698 | [[package]] 699 | name = "windows_i686_msvc" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 703 | 704 | [[package]] 705 | name = "windows_x86_64_gnu" 706 | version = "0.48.5" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 709 | 710 | [[package]] 711 | name = "windows_x86_64_gnu" 712 | version = "0.52.6" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 715 | 716 | [[package]] 717 | name = "windows_x86_64_gnullvm" 718 | version = "0.48.5" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 721 | 722 | [[package]] 723 | name = "windows_x86_64_gnullvm" 724 | version = "0.52.6" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 727 | 728 | [[package]] 729 | name = "windows_x86_64_msvc" 730 | version = "0.48.5" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 733 | 734 | [[package]] 735 | name = "windows_x86_64_msvc" 736 | version = "0.52.6" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 739 | --------------------------------------------------------------------------------