33//! This module provides real-time audio mixing for conference calls.
44//! It connects MediaPeers to the mixer and routes mixed audio back to participants.
55
6- use crate :: call :: domain :: LegId ;
6+ use crate :: media :: LegId ;
77use crate :: media:: mixer:: AudioMixer ;
88use anyhow:: { Result , anyhow} ;
99use audio_codec:: CodecType ;
10+ use dashmap:: DashMap ;
1011use parking_lot:: Mutex as ParkMutex ;
11- use std:: collections:: HashMap ;
1212use std:: sync:: Arc ;
1313use tokio:: sync:: mpsc;
1414use tokio_util:: sync:: CancellationToken ;
@@ -77,8 +77,8 @@ pub struct ConferenceParticipantAudio {
7777pub struct ConferenceAudioMixer {
7878 /// Conference ID
7979 conf_id : String ,
80- /// Participant audio channels
81- participants : Arc < tokio :: sync :: Mutex < HashMap < LegId , ConferenceParticipantAudio > > > ,
80+ /// Participant audio channels (DashMap for concurrent access)
81+ participants : Arc < DashMap < LegId , ConferenceParticipantAudio > > ,
8282 /// Cached participant count for sync access
8383 participant_count : Arc < std:: sync:: atomic:: AtomicUsize > ,
8484 /// Audio sample rate
@@ -91,10 +91,10 @@ pub struct ConferenceAudioMixer {
9191 mixing_task : Arc < ParkMutex < Option < tokio:: task:: JoinHandle < ( ) > > > > ,
9292 /// Per-(source, destination) gain overrides for supervisor modes.
9393 /// Key: (src_leg_id, dst_leg_id), Value: gain (0.0 = silent, 1.0 = normal)
94- route_gains : Arc < tokio :: sync :: Mutex < HashMap < ( LegId , LegId ) , f32 > > > ,
94+ route_gains : Arc < DashMap < ( LegId , LegId ) , f32 > > ,
9595 /// Optional recorder tap: each participant's input PCM is cloned here
9696 /// before mixing so the recorder can write per-leg audio.
97- recorder_sink : Arc < tokio :: sync :: Mutex < Option < mpsc:: Sender < RecorderFrame > > > > ,
97+ recorder_sink : Arc < parking_lot :: Mutex < Option < mpsc:: Sender < RecorderFrame > > > > ,
9898}
9999
100100impl std:: fmt:: Debug for ConferenceAudioMixer {
@@ -123,22 +123,22 @@ impl ConferenceAudioMixer {
123123
124124 Self {
125125 conf_id,
126- participants : Arc :: new ( tokio :: sync :: Mutex :: new ( HashMap :: new ( ) ) ) ,
126+ participants : Arc :: new ( DashMap :: new ( ) ) ,
127127 participant_count : Arc :: new ( std:: sync:: atomic:: AtomicUsize :: new ( 0 ) ) ,
128128 sample_rate,
129129 frame_size,
130130 cancel_token : CancellationToken :: new ( ) ,
131131 mixing_task : Arc :: new ( ParkMutex :: new ( None ) ) ,
132- route_gains : Arc :: new ( tokio :: sync :: Mutex :: new ( HashMap :: new ( ) ) ) ,
133- recorder_sink : Arc :: new ( tokio :: sync :: Mutex :: new ( None ) ) ,
132+ route_gains : Arc :: new ( DashMap :: new ( ) ) ,
133+ recorder_sink : Arc :: new ( parking_lot :: Mutex :: new ( None ) ) ,
134134 }
135135 }
136136
137137 /// Install a recorder tap. Every 20ms tick, the mixer will send each
138138 /// participant's raw input PCM to the provided channel **before**
139139 /// N-1 mixing, so the recorder can write per-leg audio.
140140 pub async fn set_recorder_sink ( & self , sink : Option < mpsc:: Sender < RecorderFrame > > ) {
141- let mut guard = self . recorder_sink . lock ( ) . await ;
141+ let mut guard = self . recorder_sink . lock ( ) ;
142142 * guard = sink;
143143 }
144144
@@ -150,14 +150,11 @@ impl ConferenceAudioMixer {
150150 codec : CodecType ,
151151 ) -> Result < ( mpsc:: Sender < AudioFrame > , mpsc:: Receiver < AudioFrame > ) > {
152152 // Reject duplicate participants
153- {
154- let participants = self . participants . lock ( ) . await ;
155- if participants. contains_key ( & leg_id) {
156- return Err ( anyhow ! (
157- "Participant {} already exists in conference" ,
158- leg_id
159- ) ) ;
160- }
153+ if self . participants . contains_key ( & leg_id) {
154+ return Err ( anyhow ! (
155+ "Participant {} already exists in conference" ,
156+ leg_id
157+ ) ) ;
161158 }
162159
163160 let ( input_tx, input_rx) = mpsc:: channel :: < AudioFrame > ( 100 ) ;
@@ -171,10 +168,7 @@ impl ConferenceAudioMixer {
171168 muted : false ,
172169 } ;
173170
174- {
175- let mut participants = self . participants . lock ( ) . await ;
176- participants. insert ( leg_id. clone ( ) , participant) ;
177- }
171+ self . participants . insert ( leg_id. clone ( ) , participant) ;
178172
179173 // Update cached count
180174 self . participant_count
@@ -194,10 +188,7 @@ impl ConferenceAudioMixer {
194188
195189 /// Remove a participant from the conference
196190 pub async fn remove_participant ( & self , leg_id : & LegId ) -> Result < ( ) > {
197- let was_present = {
198- let mut participants = self . participants . lock ( ) . await ;
199- participants. remove ( leg_id) . is_some ( )
200- } ;
191+ let was_present = self . participants . remove ( leg_id) . is_some ( ) ;
201192
202193 // Only adjust the count if the participant actually existed, to avoid
203194 // underflowing to usize::MAX on duplicate/erroneous remove calls (which
@@ -209,12 +200,10 @@ impl ConferenceAudioMixer {
209200 // Prune any route gains that referenced the leaving leg (as source
210201 // or destination), so the routing table does not grow monotonically
211202 // as participants churn through a long-running conference.
212- let mut gains = self . route_gains . lock ( ) . await ;
213- let before = gains. len ( ) ;
214- gains. retain ( |( src, dst) , _| src != leg_id && dst != leg_id) ;
215- let pruned = before - gains. len ( ) ;
203+ let before = self . route_gains . len ( ) ;
204+ self . route_gains . retain ( |( src, dst) , _| src != leg_id && dst != leg_id) ;
205+ let pruned = before - self . route_gains . len ( ) ;
216206 if pruned > 0 {
217- drop ( gains) ;
218207 debug ! (
219208 conf_id = %self . conf_id,
220209 leg_id = %leg_id,
@@ -238,8 +227,7 @@ impl ConferenceAudioMixer {
238227
239228 /// Mute/unmute a participant
240229 pub async fn set_muted ( & self , leg_id : & LegId , muted : bool ) -> Result < ( ) > {
241- let mut participants = self . participants . lock ( ) . await ;
242- if let Some ( participant) = participants. get_mut ( leg_id) {
230+ if let Some ( mut participant) = self . participants . get_mut ( leg_id) {
243231 participant. muted = muted;
244232 info ! (
245233 conf_id = %self . conf_id,
@@ -254,11 +242,10 @@ impl ConferenceAudioMixer {
254242 /// Set per-route gain for supervisor modes.
255243 /// A gain of 0.0 means the source participant is silent for the destination.
256244 pub async fn set_route_gain ( & self , src : & LegId , dst : & LegId , gain : f32 ) {
257- let mut gains = self . route_gains . lock ( ) . await ;
258245 if ( gain - 1.0 ) . abs ( ) < f32:: EPSILON {
259- gains . remove ( & ( src. clone ( ) , dst. clone ( ) ) ) ;
246+ self . route_gains . remove ( & ( src. clone ( ) , dst. clone ( ) ) ) ;
260247 } else {
261- gains . insert ( ( src. clone ( ) , dst. clone ( ) ) , gain) ;
248+ self . route_gains . insert ( ( src. clone ( ) , dst. clone ( ) ) , gain) ;
262249 }
263250 info ! (
264251 conf_id = %self . conf_id,
@@ -271,25 +258,22 @@ impl ConferenceAudioMixer {
271258
272259 /// Clear all route gains (reset to default N-1 mixing).
273260 pub async fn clear_route_gains ( & self ) {
274- let mut gains = self . route_gains . lock ( ) . await ;
275- gains. clear ( ) ;
261+ self . route_gains . clear ( ) ;
276262 info ! ( conf_id = %self . conf_id, "Route gains cleared" ) ;
277263 }
278264
279265 /// Update audio routing for all participants
280266 /// Each participant hears all other participants (N-1 mixing)
281267 async fn update_routing ( & self ) -> Result < ( ) > {
282- let participants = self . participants . lock ( ) . await ;
283- let leg_ids: Vec < LegId > = participants. keys ( ) . cloned ( ) . collect ( ) ;
284- drop ( participants) ;
268+ let participant_count = self . participants . len ( ) ;
285269
286270 // ConferenceAudioMixer uses its own mixing loop (N-1 mixing)
287271 // Each participant receives mixed audio from all other participants
288272 // The actual mixing happens in mixing_loop(), not via MediaMixer routing
289273
290274 debug ! (
291275 conf_id = %self . conf_id,
292- participant_count = leg_ids . len ( ) ,
276+ participant_count,
293277 "Updated conference routing"
294278 ) ;
295279
@@ -346,12 +330,12 @@ impl ConferenceAudioMixer {
346330 /// Collects audio from all participants, mixes, and distributes
347331 async fn mixing_loop (
348332 conf_id : String ,
349- participants : Arc < tokio :: sync :: Mutex < HashMap < LegId , ConferenceParticipantAudio > > > ,
333+ participants : Arc < DashMap < LegId , ConferenceParticipantAudio > > ,
350334 cancel_token : CancellationToken ,
351335 frame_size : usize ,
352336 sample_rate : u32 ,
353- route_gains : Arc < tokio :: sync :: Mutex < HashMap < ( LegId , LegId ) , f32 > > > ,
354- recorder_sink : Arc < tokio :: sync :: Mutex < Option < mpsc:: Sender < RecorderFrame > > > > ,
337+ route_gains : Arc < DashMap < ( LegId , LegId ) , f32 > > ,
338+ recorder_sink : Arc < parking_lot :: Mutex < Option < mpsc:: Sender < RecorderFrame > > > > ,
355339 ) {
356340 let interval_ms = ( frame_size as f64 / sample_rate as f64 * 1000.0 ) as u64 ;
357341 let interval = tokio:: time:: Duration :: from_millis ( interval_ms. max ( 1 ) ) ;
@@ -373,39 +357,28 @@ impl ConferenceAudioMixer {
373357 break ;
374358 }
375359 _ = tokio:: time:: sleep( interval) => {
376- let participant_audio = {
377- let mut participants_guard = participants. lock( ) . await ;
378- let mut frames = HashMap :: new( ) ;
379-
380- for ( leg_id, participant) in participants_guard. iter_mut( ) {
381- loop {
382- match participant. input_rx. try_recv( ) {
383- Ok ( frame) => {
384- if !participant. muted {
385- frames. insert( leg_id. clone( ) , frame) ;
386- }
387- }
388- Err ( mpsc:: error:: TryRecvError :: Empty ) => {
389- break ;
390- }
391- Err ( mpsc:: error:: TryRecvError :: Disconnected ) => {
392- break ;
360+ // Phase 1: drain latest audio from every participant (no locks held across await)
361+ let mut participant_audio = std:: collections:: HashMap :: new( ) ;
362+ for mut entry in participants. iter_mut( ) {
363+ loop {
364+ match entry. input_rx. try_recv( ) {
365+ Ok ( frame) => {
366+ if !entry. muted {
367+ participant_audio. insert( entry. key( ) . clone( ) , frame) ;
393368 }
394369 }
370+ Err ( mpsc:: error:: TryRecvError :: Empty ) => break ,
371+ Err ( mpsc:: error:: TryRecvError :: Disconnected ) => break ,
395372 }
396373 }
374+ }
397375
398- frames
399- } ;
400-
401- let gains_map = route_gains. lock( ) . await ;
402- let participants_guard = participants. lock( ) . await ;
403- let participant_ids: Vec <LegId > = participants_guard. keys( ) . cloned( ) . collect( ) ;
404- drop( participants_guard) ;
376+ let participant_ids: Vec <LegId > =
377+ participants. iter( ) . map( |e| e. key( ) . clone( ) ) . collect( ) ;
405378
406379 // ── Recorder tap: forward raw per-leg PCM before mixing ─────
407380 if !participant_audio. is_empty( ) {
408- let sink_guard = recorder_sink. lock( ) . await ;
381+ let sink_guard = recorder_sink. lock( ) ;
409382 if let Some ( ref sink) = * sink_guard {
410383 for ( leg_id, frame) in & participant_audio {
411384 let rf = RecorderFrame {
@@ -427,9 +400,9 @@ impl ConferenceAudioMixer {
427400
428401 for ( input_leg, frame) in & participant_audio {
429402 if input_leg != output_leg {
430- let gain = gains_map
403+ let gain = route_gains
431404 . get( & ( input_leg. clone( ) , output_leg. clone( ) ) )
432- . copied ( )
405+ . map ( |r| * r )
433406 . unwrap_or( 1.0 ) ;
434407 if gain > 0.0 {
435408 input_frames. push( frame. samples. clone( ) ) ;
@@ -452,10 +425,10 @@ impl ConferenceAudioMixer {
452425
453426 let output_frame = AudioFrame :: new( mixed_samples, sample_rate) ;
454427
455- let output_tx = {
456- let participants_guard = participants. lock ( ) . await ;
457- participants_guard . get( output_leg) . map ( |p| p . output_tx . clone ( ) )
458- } ;
428+ // Get the output_tx with a short-lived DashMap lookup
429+ let output_tx = participants
430+ . get( output_leg)
431+ . map ( |e| e . output_tx . clone ( ) ) ;
459432
460433 if let Some ( tx) = output_tx
461434 && tx. send( output_frame) . await . is_err( ) {
0 commit comments