1use super::processor::Processor;
2use crate::media::{AudioFrame, INTERNAL_SAMPLERATE, Samples};
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::sync::{Arc, Mutex};
6use tracing::info;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9#[serde(rename_all = "camelCase")]
10pub struct AmbianceOption {
11 pub path: Option<String>,
12 pub duck_level: Option<f32>,
13 pub normal_level: Option<f32>,
14 pub transition_speed: Option<f32>,
15 pub enabled: Option<bool>,
16}
17
18impl AmbianceOption {
19 pub fn merge(&mut self, other: &AmbianceOption) {
20 if self.path.is_none() {
21 self.path = other.path.clone();
22 }
23 if self.duck_level.is_none() {
24 self.duck_level = other.duck_level;
25 }
26 if self.normal_level.is_none() {
27 self.normal_level = other.normal_level;
28 }
29 if self.transition_speed.is_none() {
30 self.transition_speed = other.transition_speed;
31 }
32 if self.enabled.is_none() {
33 self.enabled = other.enabled;
34 }
35 }
36}
37
38pub struct AmbianceProcessor {
39 samples: Vec<i16>,
40 cursor: usize,
41 duck_level: f32,
42 normal_level: f32,
43 enabled: bool,
44 current_level: f32,
45 transition_speed: f32,
46 resample_phase: u32,
47 resample_step: u32,
48}
49
50impl AmbianceProcessor {
51 pub async fn new(option: AmbianceOption) -> Result<Self> {
52 let path = option
53 .path
54 .ok_or_else(|| anyhow::anyhow!("Ambiance path required"))?;
55
56 let samples =
57 crate::media::loader::load_audio_as_pcm(&path, INTERNAL_SAMPLERATE, true).await?;
58
59 info!("Loading ambiance {}: samples={}", path, samples.len());
60
61 let normal_level = option.normal_level.unwrap_or(0.3);
62 Ok(Self {
63 samples,
64 cursor: 0,
65 duck_level: option.duck_level.unwrap_or(0.1),
66 normal_level,
67 enabled: option.enabled.unwrap_or(true),
68 current_level: normal_level,
69 transition_speed: option.transition_speed.unwrap_or(0.01),
70 resample_phase: 0,
71 resample_step: 1 << 16,
72 })
73 }
74
75 pub fn set_enabled(&mut self, enabled: bool) {
76 self.enabled = enabled;
77 }
78
79 pub fn set_levels(&mut self, normal: f32, duck: f32) {
80 self.normal_level = normal;
81 self.duck_level = duck;
82 }
83
84 #[inline]
85 fn get_ambient_sample_with_rate(&mut self, target_sample_rate: u32) -> i16 {
86 if self.samples.is_empty() {
87 return 0;
88 }
89
90 self.resample_step =
91 (((INTERNAL_SAMPLERATE as u64) << 16) / target_sample_rate as u64) as u32;
92 let sample = self.samples[self.cursor];
93
94 self.resample_phase += self.resample_step;
95 while self.resample_phase >= (1 << 16) {
96 self.resample_phase -= 1 << 16;
97 self.cursor = (self.cursor + 1) % self.samples.len();
98 }
99
100 sample
101 }
102
103 #[inline]
104 fn soft_mix(signal: i16, ambient: i16, level: f32) -> i16 {
105 let ambient_scaled = (ambient as i32 * (level * 256.0) as i32) >> 8;
106 let signal_i32 = signal as i32;
107 let mixed = signal_i32 + ambient_scaled;
108
109 if mixed > 32767 {
110 let over = mixed - 32767;
111 (32767 - (over >> 2)) as i16
112 } else if mixed < -32768 {
113 let under = -32768 - mixed;
114 (-32768 + (under >> 2)) as i16
115 } else {
116 mixed as i16
117 }
118 }
119}
120
121impl Processor for AmbianceProcessor {
122 fn process_frame(&mut self, frame: &mut AudioFrame) -> Result<()> {
123 if !self.enabled || self.samples.is_empty() {
124 return Ok(());
125 }
126
127 let is_server_side_speaking = match &frame.samples {
128 Samples::PCM { samples } => !samples.is_empty(),
129 Samples::RTP { .. } => true,
130 Samples::Empty => false,
131 };
132
133 let target_level = if is_server_side_speaking {
134 self.duck_level
135 } else {
136 self.normal_level
137 };
138
139 if (self.current_level - target_level).abs() > 0.001 {
140 if self.current_level < target_level {
141 self.current_level = (self.current_level + self.transition_speed).min(target_level);
142 } else {
143 self.current_level = (self.current_level - self.transition_speed).max(target_level);
144 }
145 }
146
147 let sample_rate = if frame.sample_rate > 0 {
148 frame.sample_rate
149 } else {
150 INTERNAL_SAMPLERATE
151 };
152 let channels = frame.channels.max(1) as usize;
153
154 match &mut frame.samples {
155 Samples::PCM { samples } => {
156 let frame_sample_count = samples.len() / channels;
157 for i in 0..frame_sample_count {
158 let ambient = self.get_ambient_sample_with_rate(sample_rate);
159 for c in 0..channels {
160 let idx = i * channels + c;
161 if idx < samples.len() {
162 samples[idx] =
163 Self::soft_mix(samples[idx], ambient, self.current_level);
164 }
165 }
166 }
167 }
168 Samples::Empty => {
169 let frame_size = (sample_rate as usize * 20) / 1000;
170 let mut ambient_samples = Vec::with_capacity(frame_size * channels);
171 for _ in 0..frame_size {
172 let ambient = self.get_ambient_sample_with_rate(sample_rate);
173 let ambient_scaled =
174 ((ambient as i32 * (self.current_level * 256.0) as i32) >> 8) as i16;
175 for _ in 0..channels {
176 ambient_samples.push(ambient_scaled);
177 }
178 }
179 frame.samples = Samples::PCM {
180 samples: ambient_samples,
181 };
182 frame.sample_rate = sample_rate;
183 frame.channels = channels as u16;
184 }
185 _ => {}
186 }
187
188 Ok(())
189 }
190}
191
192#[derive(Clone)]
194pub struct SharedAmbianceProcessor {
195 inner: Arc<Mutex<AmbianceProcessor>>,
196}
197
198impl SharedAmbianceProcessor {
199 pub fn new(inner: Arc<Mutex<AmbianceProcessor>>) -> Self {
200 Self { inner }
201 }
202
203 pub fn inner(&self) -> Arc<Mutex<AmbianceProcessor>> {
204 self.inner.clone()
205 }
206}
207
208impl Processor for SharedAmbianceProcessor {
209 fn process_frame(&mut self, frame: &mut AudioFrame) -> Result<()> {
210 self.inner.lock().unwrap().process_frame(frame)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::media::AudioFrame;
218
219 fn loud_processor() -> AmbianceProcessor {
220 AmbianceProcessor {
221 samples: vec![8000i16; INTERNAL_SAMPLERATE as usize],
222 cursor: 0,
223 duck_level: 0.5,
224 normal_level: 1.0,
225 enabled: true,
226 current_level: 1.0,
227 transition_speed: 1.0,
228 resample_phase: 0,
229 resample_step: 1 << 16,
230 }
231 }
232
233 #[test]
234 fn empty_frame_becomes_ambiance_pcm() {
235 let mut processor = loud_processor();
236 let mut frame = AudioFrame {
237 track_id: "server-side-track".to_string(),
238 samples: Samples::Empty,
239 timestamp: 0,
240 sample_rate: INTERNAL_SAMPLERATE,
241 channels: 1,
242 ..Default::default()
243 };
244 processor.process_frame(&mut frame).unwrap();
245 match frame.samples {
246 Samples::PCM { samples } => {
247 assert_eq!(samples.len(), 320);
248 assert!(
249 samples.iter().any(|s| *s != 0),
250 "idle frame should carry ambiance"
251 );
252 }
253 other => panic!("expected PCM, got {:?}", other),
254 }
255 }
256
257 #[test]
258 fn pcm_frame_is_mixed() {
259 let mut processor = loud_processor();
260 let mut frame = AudioFrame {
261 track_id: "server-side-track".to_string(),
262 samples: Samples::PCM {
263 samples: vec![1000; 320],
264 },
265 timestamp: 0,
266 sample_rate: INTERNAL_SAMPLERATE,
267 channels: 1,
268 ..Default::default()
269 };
270 processor.process_frame(&mut frame).unwrap();
271 match frame.samples {
272 Samples::PCM { samples } => {
273 assert!(
274 samples.iter().any(|s| *s != 1000),
275 "tts frame should mix ambiance"
276 );
277 }
278 other => panic!("expected PCM, got {:?}", other),
279 }
280 }
281
282 #[test]
285 fn perf_idle_mix_cost() {
286 if cfg!(debug_assertions) {
287 println!("Skipping ambiance idle mix perf test in debug mode.");
288 return;
289 }
290 let mut processor = loud_processor();
291 let budget_us = 100.0;
292 let iterations = 10_000u32;
293
294 let start = std::time::Instant::now();
295 for i in 0..iterations {
296 let mut frame = AudioFrame {
297 track_id: "server-side-track".to_string(),
298 samples: Samples::Empty,
299 timestamp: i as u64,
300 sample_rate: INTERNAL_SAMPLERATE,
301 channels: 1,
302 ..Default::default()
303 };
304 processor.process_frame(&mut frame).unwrap();
305 }
306 let per_call_us = start.elapsed().as_micros() as f64 / iterations as f64;
307 println!("ambiance idle mix: {:.2} µs per 20ms frame", per_call_us);
308 assert!(
309 per_call_us < budget_us,
310 "idle mix {:.2} µs/frame exceeds {:.0} µs budget",
311 per_call_us,
312 budget_us
313 );
314 }
315}