oxihuman_morph/
ear_cup_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum EarCupSide {
11 Left,
12 Right,
13 Both,
14}
15
16#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct EarCupState {
20 pub cup_left: f32,
22 pub cup_right: f32,
23 pub bias_left: f32,
25 pub bias_right: f32,
26}
27
28#[allow(dead_code)]
30#[derive(Clone, Debug)]
31pub struct EarCupConfig {
32 pub max_cup: f32,
33}
34
35impl Default for EarCupConfig {
36 fn default() -> Self {
37 Self { max_cup: 1.0 }
38 }
39}
40
41impl Default for EarCupState {
42 fn default() -> Self {
43 Self {
44 cup_left: 0.0,
45 cup_right: 0.0,
46 bias_left: 0.0,
47 bias_right: 0.0,
48 }
49 }
50}
51
52#[allow(dead_code)]
53pub fn new_ear_cup_state() -> EarCupState {
54 EarCupState::default()
55}
56
57#[allow(dead_code)]
58pub fn default_ear_cup_config() -> EarCupConfig {
59 EarCupConfig::default()
60}
61
62#[allow(dead_code)]
63pub fn ec_set_cup(state: &mut EarCupState, cfg: &EarCupConfig, side: EarCupSide, v: f32) {
64 let v = v.clamp(0.0, cfg.max_cup);
65 match side {
66 EarCupSide::Left => state.cup_left = v,
67 EarCupSide::Right => state.cup_right = v,
68 EarCupSide::Both => {
69 state.cup_left = v;
70 state.cup_right = v;
71 }
72 }
73}
74
75#[allow(dead_code)]
76pub fn ec_set_bias(state: &mut EarCupState, side: EarCupSide, bias: f32) {
77 let b = bias.clamp(-1.0, 1.0);
78 match side {
79 EarCupSide::Left => state.bias_left = b,
80 EarCupSide::Right => state.bias_right = b,
81 EarCupSide::Both => {
82 state.bias_left = b;
83 state.bias_right = b;
84 }
85 }
86}
87
88#[allow(dead_code)]
89pub fn ec_reset(state: &mut EarCupState) {
90 *state = EarCupState::default();
91}
92
93#[allow(dead_code)]
94pub fn ec_is_neutral(state: &EarCupState) -> bool {
95 state.cup_left < 1e-4 && state.cup_right < 1e-4
96}
97
98#[allow(dead_code)]
99pub fn ec_blend(a: &EarCupState, b: &EarCupState, t: f32) -> EarCupState {
100 let t = t.clamp(0.0, 1.0);
101 EarCupState {
102 cup_left: a.cup_left + (b.cup_left - a.cup_left) * t,
103 cup_right: a.cup_right + (b.cup_right - a.cup_right) * t,
104 bias_left: a.bias_left + (b.bias_left - a.bias_left) * t,
105 bias_right: a.bias_right + (b.bias_right - a.bias_right) * t,
106 }
107}
108
109#[allow(dead_code)]
110pub fn ec_symmetry(state: &EarCupState) -> f32 {
111 1.0 - (state.cup_left - state.cup_right).abs().min(1.0)
112}
113
114#[allow(dead_code)]
115pub fn ec_average_cup(state: &EarCupState) -> f32 {
116 (state.cup_left + state.cup_right) * 0.5
117}
118
119#[allow(dead_code)]
120pub fn ec_to_weights(state: &EarCupState) -> [f32; 4] {
121 [
122 state.cup_left,
123 state.cup_right,
124 state.bias_left,
125 state.bias_right,
126 ]
127}
128
129#[allow(dead_code)]
130pub fn ec_to_json(state: &EarCupState) -> String {
131 format!(
132 "{{\"cup_left\":{:.4},\"cup_right\":{:.4},\"bias_left\":{:.4},\"bias_right\":{:.4}}}",
133 state.cup_left, state.cup_right, state.bias_left, state.bias_right
134 )
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn default_neutral() {
143 assert!(ec_is_neutral(&new_ear_cup_state()));
144 }
145
146 #[test]
147 fn set_cup_clamps_max() {
148 let mut s = new_ear_cup_state();
149 let cfg = default_ear_cup_config();
150 ec_set_cup(&mut s, &cfg, EarCupSide::Left, 99.0);
151 assert!(s.cup_left <= cfg.max_cup);
152 }
153
154 #[test]
155 fn set_cup_not_negative() {
156 let mut s = new_ear_cup_state();
157 let cfg = default_ear_cup_config();
158 ec_set_cup(&mut s, &cfg, EarCupSide::Right, -1.0);
159 assert!(s.cup_right >= 0.0);
160 }
161
162 #[test]
163 fn both_sides_set() {
164 let mut s = new_ear_cup_state();
165 let cfg = default_ear_cup_config();
166 ec_set_cup(&mut s, &cfg, EarCupSide::Both, 0.5);
167 assert!((s.cup_left - s.cup_right).abs() < 1e-5);
168 }
169
170 #[test]
171 fn reset_clears() {
172 let mut s = new_ear_cup_state();
173 let cfg = default_ear_cup_config();
174 ec_set_cup(&mut s, &cfg, EarCupSide::Both, 0.8);
175 ec_reset(&mut s);
176 assert!(ec_is_neutral(&s));
177 }
178
179 #[test]
180 fn blend_midpoint() {
181 let cfg = default_ear_cup_config();
182 let mut a = new_ear_cup_state();
183 let mut b = new_ear_cup_state();
184 ec_set_cup(&mut a, &cfg, EarCupSide::Left, 0.0);
185 ec_set_cup(&mut b, &cfg, EarCupSide::Left, 1.0);
186 let m = ec_blend(&a, &b, 0.5);
187 assert!((m.cup_left - 0.5).abs() < 1e-4);
188 }
189
190 #[test]
191 fn symmetry_one_equal() {
192 let s = new_ear_cup_state();
193 assert!((ec_symmetry(&s) - 1.0).abs() < 1e-5);
194 }
195
196 #[test]
197 fn average_cup_zero_default() {
198 assert!((ec_average_cup(&new_ear_cup_state())).abs() < 1e-5);
199 }
200
201 #[test]
202 fn weights_len() {
203 assert_eq!(ec_to_weights(&new_ear_cup_state()).len(), 4);
204 }
205
206 #[test]
207 fn json_has_cup_left() {
208 assert!(ec_to_json(&new_ear_cup_state()).contains("cup_left"));
209 }
210}