@@ -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
7881impl 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}
0 commit comments