Skip to content

Commit 0c78395

Browse files
committed
feat(conference): add route gain management for supervisor modes
1 parent 28ab63d commit 0c78395

3 files changed

Lines changed: 335 additions & 29 deletions

File tree

src/media/conference_mixer.rs

Lines changed: 155 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ pub struct ConferenceAudioMixer {
7373
cancel_token: CancellationToken,
7474
/// Mixing task handle
7575
mixing_task: Arc<std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>>,
76+
/// Per-(source, destination) gain overrides for supervisor modes.
77+
/// Key: (src_leg_id, dst_leg_id), Value: gain (0.0 = silent, 1.0 = normal)
78+
route_gains: Arc<tokio::sync::Mutex<HashMap<(LegId, LegId), f32>>>,
7679
}
7780

7881
impl std::fmt::Debug for ConferenceAudioMixer {
@@ -98,6 +101,7 @@ impl ConferenceAudioMixer {
98101
frame_size,
99102
cancel_token: CancellationToken::new(),
100103
mixing_task: Arc::new(std::sync::Mutex::new(None)),
104+
route_gains: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
101105
}
102106
}
103107

@@ -189,6 +193,31 @@ impl ConferenceAudioMixer {
189193
Ok(())
190194
}
191195

196+
/// Set per-route gain for supervisor modes.
197+
/// A gain of 0.0 means the source participant is silent for the destination.
198+
pub async fn set_route_gain(&self, src: &LegId, dst: &LegId, gain: f32) {
199+
let mut gains = self.route_gains.lock().await;
200+
if (gain - 1.0).abs() < f32::EPSILON {
201+
gains.remove(&(src.clone(), dst.clone()));
202+
} else {
203+
gains.insert((src.clone(), dst.clone()), gain);
204+
}
205+
info!(
206+
conf_id = %self.conf_id,
207+
src = %src,
208+
dst = %dst,
209+
gain = gain,
210+
"Route gain set"
211+
);
212+
}
213+
214+
/// Clear all route gains (reset to default N-1 mixing).
215+
pub async fn clear_route_gains(&self) {
216+
let mut gains = self.route_gains.lock().await;
217+
gains.clear();
218+
info!(conf_id = %self.conf_id, "Route gains cleared");
219+
}
220+
192221
/// Update audio routing for all participants
193222
/// Each participant hears all other participants (N-1 mixing)
194223
async fn update_routing(&self) -> Result<()> {
@@ -211,15 +240,23 @@ impl ConferenceAudioMixer {
211240

212241
/// Start the conference mixing
213242
pub fn start(&self) {
214-
// Start the conference mixing loop
215243
let cancel_token = self.cancel_token.clone();
216244
let participants = self.participants.clone();
217245
let frame_size = self.frame_size;
218246
let sample_rate = self.sample_rate;
219247
let conf_id = self.conf_id.clone();
248+
let route_gains = self.route_gains.clone();
220249

221250
let task = crate::utils::spawn(async move {
222-
Self::mixing_loop(conf_id, participants, cancel_token, frame_size, sample_rate).await;
251+
Self::mixing_loop(
252+
conf_id,
253+
participants,
254+
cancel_token,
255+
frame_size,
256+
sample_rate,
257+
route_gains,
258+
)
259+
.await;
223260
});
224261

225262
let mut mixing_task = self.mixing_task.lock().unwrap();
@@ -253,6 +290,7 @@ impl ConferenceAudioMixer {
253290
cancel_token: CancellationToken,
254291
frame_size: usize,
255292
sample_rate: u32,
293+
route_gains: Arc<tokio::sync::Mutex<HashMap<(LegId, LegId), f32>>>,
256294
) {
257295
let interval_ms = (frame_size as f64 / sample_rate as f64 * 1000.0) as u64;
258296
let interval = tokio::time::Duration::from_millis(interval_ms.max(1));
@@ -265,7 +303,6 @@ impl ConferenceAudioMixer {
265303
"Conference mixing loop started"
266304
);
267305

268-
// Audio mixer for combining frames
269306
let audio_mixer = AudioMixer::new(sample_rate, 1);
270307

271308
loop {
@@ -275,28 +312,22 @@ impl ConferenceAudioMixer {
275312
break;
276313
}
277314
_ = tokio::time::sleep(interval) => {
278-
// Collect audio from all participants
279315
let participant_audio = {
280316
let mut participants_guard = participants.lock().await;
281317
let mut frames = HashMap::new();
282318

283319
for (leg_id, participant) in participants_guard.iter_mut() {
284-
// Collect all available frames from this participant (non-blocking)
285-
// Use try_recv to drain the buffer without waiting
286320
loop {
287321
match participant.input_rx.try_recv() {
288322
Ok(frame) => {
289323
if !participant.muted {
290-
// Keep only the latest frame (overwrite previous)
291324
frames.insert(leg_id.clone(), frame);
292325
}
293326
}
294327
Err(mpsc::error::TryRecvError::Empty) => {
295-
// No more frames available
296328
break;
297329
}
298330
Err(mpsc::error::TryRecvError::Disconnected) => {
299-
// Channel closed, participant left
300331
break;
301332
}
302333
}
@@ -306,28 +337,30 @@ impl ConferenceAudioMixer {
306337
frames
307338
};
308339

309-
// Mix and distribute audio to each participant
340+
let gains_map = route_gains.lock().await;
310341
let participants_guard = participants.lock().await;
311342
let participant_ids: Vec<LegId> = participants_guard.keys().cloned().collect();
312343
drop(participants_guard);
313344

314-
// Only process if there's participant audio to mix
315345
if !participant_audio.is_empty() {
316346
for output_leg in &participant_ids {
317-
// Collect frames from all OTHER participants
318347
let mut input_frames = Vec::new();
319348
let mut gains = Vec::new();
320349

321350
for (input_leg, frame) in &participant_audio {
322351
if input_leg != output_leg {
323-
input_frames.push(frame.samples.clone());
324-
gains.push(1.0); // Equal gain mixing
352+
let gain = gains_map
353+
.get(&(input_leg.clone(), output_leg.clone()))
354+
.copied()
355+
.unwrap_or(1.0);
356+
if gain > 0.0 {
357+
input_frames.push(frame.samples.clone());
358+
gains.push(gain);
359+
}
325360
}
326361
}
327362

328-
// Only send if there are input frames (don't send silence)
329363
if !input_frames.is_empty() {
330-
// Ensure all frames have the same size
331364
let mut normalized_frames = Vec::new();
332365
for mut frame in input_frames {
333366
if frame.len() < frame_size {
@@ -339,19 +372,15 @@ impl ConferenceAudioMixer {
339372
}
340373
let mixed_samples = audio_mixer.mix_frames(normalized_frames, &gains);
341374

342-
// Prepare output frame
343375
let output_frame = AudioFrame::new(mixed_samples, sample_rate);
344376

345-
// Send mixed audio to the output participant
346-
// Clone the sender to avoid holding the lock across await
347377
let output_tx = {
348378
let participants_guard = participants.lock().await;
349379
participants_guard.get(output_leg).map(|p| p.output_tx.clone())
350380
};
351381

352382
if let Some(tx) = output_tx
353383
&& tx.send(output_frame).await.is_err() {
354-
// Channel closed
355384
}
356385
}
357386
}
@@ -720,19 +749,15 @@ mod tests {
720749
.await
721750
.unwrap();
722751

723-
// Send audio with known amplitude
724752
let amplitude = 1000i16;
725753
let samples = vec![amplitude; 160];
726754
tx1.send(AudioFrame::new(samples, 8000)).await.unwrap();
727755

728756
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
729757

730-
// Verify received audio
731758
let frame = rx2.try_recv().expect("Should receive audio");
732759
assert_eq!(frame.samples.len(), 160, "Frame size should be 160 samples");
733760

734-
// The received samples should be approximately the same as sent
735-
// (allowing for mixing gains which default to 1.0)
736761
let avg_amplitude: i16 =
737762
(frame.samples.iter().map(|&s| s as i32).sum::<i32>() / 160) as i16;
738763
assert!(
@@ -744,4 +769,110 @@ mod tests {
744769

745770
mixer.stop().await;
746771
}
772+
773+
#[tokio::test]
774+
async fn test_route_gain_supervisor_listen() {
775+
let mixer = ConferenceAudioMixer::new("test-route-listen".to_string(), 8000);
776+
mixer.start();
777+
778+
let customer = LegId::new("customer");
779+
let agent = LegId::new("agent");
780+
let supervisor = LegId::new("supervisor");
781+
782+
let (tx_cust, mut rx_cust) = mixer
783+
.add_participant(customer.clone(), CodecType::PCMU)
784+
.await
785+
.unwrap();
786+
let (tx_agent, mut rx_agent) = mixer
787+
.add_participant(agent.clone(), CodecType::PCMU)
788+
.await
789+
.unwrap();
790+
let (_tx_sup, mut rx_sup) = mixer
791+
.add_participant(supervisor.clone(), CodecType::PCMU)
792+
.await
793+
.unwrap();
794+
795+
// Listen mode: supervisor sends nothing to customer or agent
796+
mixer.set_route_gain(&supervisor, &customer, 0.0).await;
797+
mixer.set_route_gain(&supervisor, &agent, 0.0).await;
798+
799+
// Supervisor speaks (should be blocked by route gain)
800+
let sup_samples = vec![5000i16; 160];
801+
_tx_sup.send(AudioFrame::new(sup_samples, 8000)).await.unwrap();
802+
803+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
804+
805+
// Customer should NOT hear supervisor
806+
if let Ok(frame) = rx_cust.try_recv() {
807+
let has_supervisor_audio = frame.samples.iter().any(|&s| s.abs() > 100);
808+
assert!(!has_supervisor_audio, "Customer should not hear supervisor in listen mode");
809+
}
810+
811+
// Agent should NOT hear supervisor
812+
if let Ok(frame) = rx_agent.try_recv() {
813+
let has_supervisor_audio = frame.samples.iter().any(|&s| s.abs() > 100);
814+
assert!(!has_supervisor_audio, "Agent should not hear supervisor in listen mode");
815+
}
816+
817+
// Customer speaks - agent and supervisor should hear
818+
let cust_samples = vec![1000i16; 160];
819+
tx_cust.send(AudioFrame::new(cust_samples, 8000)).await.unwrap();
820+
821+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
822+
823+
assert!(rx_agent.try_recv().is_ok(), "Agent should hear customer");
824+
assert!(rx_sup.try_recv().is_ok(), "Supervisor should hear customer");
825+
826+
mixer.stop().await;
827+
}
828+
829+
#[tokio::test]
830+
async fn test_route_gain_supervisor_whisper() {
831+
let mixer = ConferenceAudioMixer::new("test-route-whisper".to_string(), 8000);
832+
mixer.start();
833+
834+
let customer = LegId::new("customer");
835+
let agent = LegId::new("agent");
836+
let supervisor = LegId::new("supervisor");
837+
838+
let (_tx_cust, mut rx_cust) = mixer
839+
.add_participant(customer.clone(), CodecType::PCMU)
840+
.await
841+
.unwrap();
842+
let (tx_agent, _rx_agent) = mixer
843+
.add_participant(agent.clone(), CodecType::PCMU)
844+
.await
845+
.unwrap();
846+
let (tx_sup, mut rx_sup) = mixer
847+
.add_participant(supervisor.clone(), CodecType::PCMU)
848+
.await
849+
.unwrap();
850+
851+
// Whisper mode: supervisor speaks to agent only, not customer
852+
mixer.set_route_gain(&supervisor, &customer, 0.0).await;
853+
// supervisor -> agent stays at 1.0 (default)
854+
855+
// Supervisor speaks
856+
let sup_samples = vec![5000i16; 160];
857+
tx_sup.send(AudioFrame::new(sup_samples, 8000)).await.unwrap();
858+
859+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
860+
861+
// Customer should NOT hear supervisor
862+
if let Ok(frame) = rx_cust.try_recv() {
863+
let has_supervisor_audio = frame.samples.iter().any(|&s| s.abs() > 100);
864+
assert!(!has_supervisor_audio, "Customer should not hear supervisor in whisper mode");
865+
}
866+
867+
// Agent speaks - customer and supervisor should hear
868+
let agent_samples = vec![2000i16; 160];
869+
tx_agent.send(AudioFrame::new(agent_samples, 8000)).await.unwrap();
870+
871+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
872+
873+
assert!(rx_cust.try_recv().is_ok(), "Customer should hear agent");
874+
assert!(rx_sup.try_recv().is_ok(), "Supervisor should hear agent");
875+
876+
mixer.stop().await;
877+
}
747878
}

src/models/alter_rewrite_columns_length.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ pub struct Migration;
66
#[async_trait::async_trait]
77
impl MigrationTrait for Migration {
88
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
9-
let table_name = "rustpbx_call_records";
9+
let db = manager.get_connection();
10+
let db_type = manager.get_database_backend();
1011

11-
if manager.has_column(table_name, "rewrite_original_from").await? {
12+
if db_type == sea_orm::DatabaseBackend::Sqlite {
13+
return Ok(());
14+
}
15+
16+
if manager
17+
.has_column("rustpbx_call_records", "rewrite_original_from")
18+
.await?
19+
{
1220
manager
1321
.alter_table(
1422
Table::alter()
@@ -20,10 +28,14 @@ impl MigrationTrait for Migration {
2028
)
2129
.to_owned(),
2230
)
23-
.await?;
31+
.await
32+
.ok();
2433
}
2534

26-
if manager.has_column(table_name, "rewrite_original_to").await? {
35+
if manager
36+
.has_column("rustpbx_call_records", "rewrite_original_to")
37+
.await?
38+
{
2739
manager
2840
.alter_table(
2941
Table::alter()
@@ -35,7 +47,8 @@ impl MigrationTrait for Migration {
3547
)
3648
.to_owned(),
3749
)
38-
.await?;
50+
.await
51+
.ok();
3952
}
4053
Ok(())
4154
}

0 commit comments

Comments
 (0)