@@ -119,9 +119,11 @@ impl AudioReader {
119119 pub fn is_closed ( & self ) -> bool { self . sender . is_closed ( ) }
120120
121121 /// Feed one 20 ms frame. Called from the recorder's feed point in the
122- /// tick (same cache state, same timing). Samples are already narrowband
123- /// 8 kHz linear — wideband and wire-byte formats are pulled directly
124- /// from `codecx`.
122+ /// tick (same cache state, same timing). Samples are narrowband 8 kHz
123+ /// linear; wideband (16 kHz) and wire-byte formats are pulled from
124+ /// `codecx` for the inbound side. The outbound side has no `codecx`, so a
125+ /// caller that holds wideband for the out side (the mixer, where out is
126+ /// the peer's inbound) supplies it via `feed_with`.
125127 ///
126128 /// Never blocks. On full queue or closed consumer the frame is silently
127129 /// dropped (bumping `drops`). Returns `true` if the frame was queued.
@@ -131,7 +133,21 @@ impl AudioReader {
131133 in_samples_8k : Option < & [ i16 ] > ,
132134 out_samples_8k : Option < & [ i16 ] > ,
133135 ) -> bool {
134- let Some ( bytes) = self . build_frame ( codecx, in_samples_8k, out_samples_8k) else {
136+ self . feed_with ( codecx, in_samples_8k, out_samples_8k, None )
137+ }
138+
139+ /// As [`feed`](Self::feed), but with the outbound side's wideband (16 kHz)
140+ /// samples supplied by the caller. Used by the mixer so an `out`/`both`
141+ /// reader at 16 kHz emits the peer's true wideband instead of silence.
142+ /// Ignored by 8 kHz readers and wire-byte formats.
143+ pub fn feed_with (
144+ & mut self ,
145+ codecx : & mut CodecBundle ,
146+ in_samples_8k : Option < & [ i16 ] > ,
147+ out_samples_8k : Option < & [ i16 ] > ,
148+ out_samples_16k : Option < & [ i16 ] > ,
149+ ) -> bool {
150+ let Some ( bytes) = self . build_frame ( codecx, in_samples_8k, out_samples_8k, out_samples_16k) else {
135151 return false ;
136152 } ;
137153 match self . sender . try_send ( bytes) {
@@ -145,9 +161,10 @@ impl AudioReader {
145161 codecx : & mut CodecBundle ,
146162 in_samples_8k : Option < & [ i16 ] > ,
147163 out_samples_8k : Option < & [ i16 ] > ,
164+ out_samples_16k : Option < & [ i16 ] > ,
148165 ) -> Option < Vec < u8 > > {
149166 match self . cfg . format {
150- ReaderFormat :: L16 => self . build_l16 ( codecx, in_samples_8k, out_samples_8k) ,
167+ ReaderFormat :: L16 => self . build_l16 ( codecx, in_samples_8k, out_samples_8k, out_samples_16k ) ,
151168 ReaderFormat :: Pcma => codecx. require_wire_as ( 8 ) . map ( |b| b. to_vec ( ) ) ,
152169 ReaderFormat :: Pcmu => codecx. require_wire_as ( 0 ) . map ( |b| b. to_vec ( ) ) ,
153170 ReaderFormat :: G722 => codecx. require_wire_as ( 9 ) . map ( |b| b. to_vec ( ) ) ,
@@ -162,16 +179,20 @@ impl AudioReader {
162179 codecx : & mut CodecBundle ,
163180 in_samples_8k : Option < & [ i16 ] > ,
164181 out_samples_8k : Option < & [ i16 ] > ,
182+ out_samples_16k : Option < & [ i16 ] > ,
165183 ) -> Option < Vec < u8 > > {
166184 // Resolve the two sides at the requested sample rate.
167185 let ( in_samples, out_samples) : ( Option < Vec < i16 > > , Option < Vec < i16 > > ) = match self . cfg . samplerate {
168186 16000 => {
169- // Wideband is cached on codecx; upsamples from narrowband if the
170- // wire is a narrowband codec. Only the "in" side lives on codecx
171- // — the "out" side can't sensibly become wideband in v1 (no
172- // upsample cache for player frames). Treat out=None at 16k.
173- let wb = codecx. require_wideband_16k ( ) . map ( |s| s. to_vec ( ) ) ;
174- ( wb, None )
187+ // Inbound wideband is cached on codecx (decoded from G.722, or
188+ // upsampled from narrowband). The outbound side has no codecx
189+ // here, so its wideband must be supplied by the caller — the
190+ // mixer passes the peer's wideband for a bridged call. When it
191+ // isn't supplied (e.g. a non-bridged channel whose out is an
192+ // 8 kHz player) the out side is silent at 16k.
193+ let wb_in = codecx. require_wideband_16k ( ) . map ( |s| s. to_vec ( ) ) ;
194+ let wb_out = out_samples_16k. map ( |s| s. to_vec ( ) ) ;
195+ ( wb_in, wb_out)
175196 }
176197 _ => (
177198 in_samples_8k. map ( |s| s. to_vec ( ) ) ,
@@ -257,3 +278,50 @@ pub struct ForwarderHandle {
257278 pub id : u64 ,
258279 pub cancel : Arc < tokio:: sync:: Notify > ,
259280}
281+
282+ #[ cfg( test) ]
283+ mod tests {
284+ use super :: * ;
285+ use crate :: codec:: CodecBundle ;
286+
287+ fn reader ( direction : ReaderDirection , samplerate : u32 ) -> AudioReader {
288+ let ( tx, _rx) = make_channel ( ) ;
289+ let cfg = ReaderConfig { direction, samplerate, ..Default :: default ( ) } ;
290+ AudioReader :: new ( 1 , cfg, tx)
291+ }
292+
293+ // The fix: at 16 kHz an `out` reader emits the caller-supplied peer
294+ // wideband. Previously the out side was hard-coded to silence at 16 kHz.
295+ #[ test]
296+ fn out_reader_16k_uses_supplied_wideband ( ) {
297+ let r = reader ( ReaderDirection :: Out , 16000 ) ;
298+ let mut cx = CodecBundle :: new ( ) ;
299+ let out_wb: Vec < i16 > = vec ! [ 1234 ; 320 ] ; // 20 ms @ 16 kHz mono
300+ let in_8k = vec ! [ 0i16 ; 160 ] ;
301+ let out_8k = vec ! [ 0i16 ; 160 ] ;
302+
303+ let bytes = r
304+ . build_frame ( & mut cx, Some ( & in_8k) , Some ( & out_8k) , Some ( & out_wb) )
305+ . expect ( "frame produced" ) ;
306+
307+ assert_eq ! ( bytes. len( ) , 320 * 2 , "16k mono 20ms = 640 bytes" ) ;
308+ assert_eq ! ( i16 :: from_le_bytes( [ bytes[ 0 ] , bytes[ 1 ] ] ) , 1234 ) ;
309+ }
310+
311+ // The 8 kHz path is unchanged: the out side uses the 8 kHz samples directly
312+ // and ignores any supplied wideband.
313+ #[ test]
314+ fn out_reader_8k_uses_narrowband ( ) {
315+ let r = reader ( ReaderDirection :: Out , 8000 ) ;
316+ let mut cx = CodecBundle :: new ( ) ;
317+ let in_8k = vec ! [ 0i16 ; 160 ] ;
318+ let out_8k = vec ! [ 321i16 ; 160 ] ;
319+
320+ let bytes = r
321+ . build_frame ( & mut cx, Some ( & in_8k) , Some ( & out_8k) , None )
322+ . expect ( "frame produced" ) ;
323+
324+ assert_eq ! ( bytes. len( ) , 160 * 2 , "8k mono 20ms = 320 bytes" ) ;
325+ assert_eq ! ( i16 :: from_le_bytes( [ bytes[ 0 ] , bytes[ 1 ] ] ) , 321 ) ;
326+ }
327+ }
0 commit comments