Skip to main content

moq_audio/decode/
consumer.rs

1//! Subscribe to an encoded audio track and emit raw PCM.
2
3use std::collections::VecDeque;
4
5use bytes::Bytes;
6
7use super::decoder::{Config, Decoder, Start};
8use crate::resample::{Resampler, remix, validate_channels};
9use crate::{Activity, Error, Frame};
10
11/// Subscribe to a moq-mux audio track and emit decoded PCM in the layout
12/// declared by [`Config`].
13///
14/// The mirror of [`encode::Producer`](crate::encode::Producer): output format /
15/// sample rate / channel count are fixed at construction, and
16/// [`read`](Self::read) returns [`Frame`]s carrying the codec activity they
17/// were decoded from.
18pub struct Consumer {
19	decoder: Decoder,
20	track: moq_mux::container::Consumer<moq_mux::catalog::hang::Container>,
21	resampler: Option<Resampler>,
22	config: Config,
23	latency_max: std::time::Duration,
24	resolved_sample_rate: u32,
25	resolved_channels: u32,
26	/// Where the next packet's timestamp should land: the last packet's timestamp
27	/// plus the media it covered, including the codec delay the decoder trimmed off
28	/// the front. A packet that misses it is a hole nobody declared.
29	next_start: Option<moq_net::Timestamp>,
30	/// Frames decoded and not yet handed back, so a gap's tail can be returned
31	/// ahead of the packet that exposed it.
32	ready: VecDeque<Frame>,
33	/// Codec activity spans the resampler's buffered output still covers.
34	spans: VecDeque<ActivitySpan>,
35	/// Activity of the last span the output ran past, for the rounding samples the
36	/// filter leaves beyond the final input boundary.
37	trailing: Activity,
38	/// Timestamp of the first encoded packet in this decoder epoch, used to
39	/// interpret codec delay and a terminal marker.
40	epoch: Option<moq_net::Timestamp>,
41	/// Codec delay trimmed since the current decoder epoch began.
42	delay_trimmed: usize,
43	/// Codec-rate terminal frames emitted since `terminal_start`.
44	frames_decoded: usize,
45	/// Logical endpoint carried by an empty legacy frame before terminal packets.
46	end: Option<moq_net::Timestamp>,
47	/// Presentation time of the first decoded terminal frame.
48	terminal_start: Option<moq_net::Timestamp>,
49	/// Last container discontinuity applied to codec and resampler state.
50	discontinuity: u64,
51}
52
53struct ActivitySpan {
54	end: moq_net::Timestamp,
55	activity: Activity,
56}
57
58impl Consumer {
59	/// Subscribe to `name` in `broadcast`, using the catalog entry to pick the
60	/// codec.
61	pub async fn new(
62		broadcast: &moq_net::broadcast::Consumer,
63		catalog: &hang::catalog::AudioConfig,
64		name: impl Into<String>,
65		config: Config,
66	) -> Result<Self, Error> {
67		let decoder = Decoder::new(catalog)?;
68		let sample_rate = config.sample_rate.unwrap_or_else(|| decoder.sample_rate());
69		let channels = config.channels.unwrap_or_else(|| decoder.channel_count());
70		validate_channels(channels)?;
71
72		let resampler = if sample_rate == decoder.sample_rate() {
73			None
74		} else {
75			let chunk_frames = (decoder.sample_rate() as usize * 20) / 1000;
76			Some(Resampler::new(
77				decoder.sample_rate(),
78				sample_rate,
79				decoder.channel_count(),
80				chunk_frames,
81			)?)
82		};
83
84		let name = name.into();
85		let track = broadcast.track(&name)?;
86		let mut subscriber = track
87			.subscribe(moq_net::track::Subscription::default().with_priority(hang::catalog::PRIORITY.audio))
88			.await?;
89		// A decoder often opens on a track that is already cached: a replacement
90		// decoder subscribes while its predecessor still holds groups, and a
91		// rendition switched away from and back to stays warm for
92		// `TRACK_IDLE_LINGER`. A caller that asked for `Start::Latest` wants
93		// none of that backlog, because a cursor starting at sequence zero
94		// replays every cached group at decode speed before reaching live
95		// media, which on a thirty-second retention is half a minute of sound raced
96		// through.
97		//
98		// This moves the local read cursor and deliberately not
99		// `Subscription::group_start`. That field is a request to the publisher,
100		// aggregated across every live subscriber, so naming a stale cached
101		// sequence there asks the publisher to rewind the track for everyone
102		// reading it. What a player wants is to skip what it already has.
103		if config.start == Start::Latest
104			&& let Some(live_edge) = track.latest()
105		{
106			subscriber.start_at(live_edge);
107		}
108		let track = subscriber;
109		let latency_max = config.latency_max.unwrap_or_default().min(track.info().latency_max);
110		// The catalog says how the track is framed, and it is not always the legacy
111		// wire: `moq import fmp4` publishes CMAF. Reading a moof+mdat fragment as a
112		// varint timestamp plus a payload decodes to garbage rather than failing.
113		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container)?;
114		let mut track = moq_mux::container::Consumer::new(track, container);
115		if let Some(latency) = config.latency_max {
116			track = track.with_latency(latency);
117		}
118
119		Ok(Self {
120			decoder,
121			track,
122			resampler,
123			config,
124			latency_max,
125			resolved_sample_rate: sample_rate,
126			resolved_channels: channels,
127			next_start: None,
128			ready: VecDeque::new(),
129			spans: VecDeque::new(),
130			trailing: Activity::Active,
131			epoch: None,
132			delay_trimmed: 0,
133			frames_decoded: 0,
134			end: None,
135			terminal_start: None,
136			discontinuity: 0,
137		})
138	}
139
140	/// The config this consumer was built with.
141	pub fn config(&self) -> &Config {
142		&self.config
143	}
144
145	/// The effective latency budget after clamping to the publisher's retention window.
146	pub fn latency_max(&self) -> std::time::Duration {
147		self.latency_max
148	}
149
150	/// Sample rate samples are actually delivered at, which is
151	/// [`Config::sample_rate`] resolved against the catalog.
152	pub fn sample_rate(&self) -> u32 {
153		self.resolved_sample_rate
154	}
155
156	/// Channel count samples are actually delivered at, which is
157	/// [`Config::channels`] resolved against the catalog.
158	pub fn channels(&self) -> u32 {
159		self.resolved_channels
160	}
161
162	/// Read the next decoded PCM frame, or `None` when the track ends.
163	///
164	/// [`Frame::activity`] reports whether the packet these samples came from
165	/// coded audio. It describes where the frame begins, so a resampled
166	/// frame that straddles a change carries the activity its first sample came
167	/// from and the next frame carries the new one.
168	///
169	/// A timestamp that doesn't continue the previous packet is a hole in the
170	/// output, not a splice: nothing is carried across it, and the frames on either
171	/// side stay anchored to their own packet timeline, so the hole is there to
172	/// see. "Doesn't continue" allows for the quantization the stamps carry, which
173	/// on a millisecond-stamped ingest is most of a millisecond.
174	pub async fn read(&mut self) -> Result<Option<Frame>, Error> {
175		loop {
176			if let Some(frame) = self.ready.pop_front() {
177				return Ok(Some(frame));
178			}
179
180			let mux_frame = self.track.read().await?;
181			self.apply_discontinuity()?;
182			let Some(mux_frame) = mux_frame else {
183				return self.flush();
184			};
185
186			if let Some(end) = self.track.end()
187				&& self.end != Some(end)
188			{
189				self.end = Some(end);
190				self.frames_decoded = 0;
191				self.terminal_start = None;
192			}
193
194			// Undeclared holes are routine: a skipped stalled group, a packet the
195			// decoder refused, an ingest that resynced. Drop every stage's state at
196			// the edge, before the packet after it goes anywhere near the decoder.
197			//
198			// Skipped once an end marker arrives, because from there the terminal
199			// phase reconstructs each batch's time from the marker rather than
200			// reading it off the packet, so there is nothing left to compare.
201			if self.end.is_none()
202				&& self
203					.next_start
204					.is_some_and(|next| discontinuous(next, mux_frame.timestamp))
205				&& let Some(frame) = self.gap()?
206			{
207				self.ready.push_back(frame);
208			}
209
210			let rate = self.decoder.sample_rate();
211			let epoch = *self.epoch.get_or_insert(mux_frame.timestamp);
212			let delay = self.decoder.delay_remaining();
213			let decoded = self.decoder.decode(&mux_frame.payload)?;
214			// Codec delay trimmed off the front is media this packet covered even
215			// though no samples came out, so it still moves the packet after it along.
216			let trimmed = delay - self.decoder.delay_remaining();
217			self.delay_trimmed += trimmed;
218			let activity = decoded.activity;
219			let mut decoded = decoded.samples;
220			if let Some(end) = self.end {
221				let terminal_start = *self
222					.terminal_start
223					.get_or_insert(rewind(mux_frame.timestamp, self.delay_trimmed, rate)?.max(epoch));
224				let total = frames_between(terminal_start, end, rate)?;
225				let remaining = total.saturating_sub(self.frames_decoded);
226				decoded.truncate(remaining.saturating_mul(self.decoder.channel_count() as usize));
227			}
228
229			let frames = decoded.len() / self.decoder.channel_count().max(1) as usize;
230			let decoded_at = if let Some(terminal_start) = self.terminal_start {
231				advance(terminal_start, self.frames_decoded, rate)?
232			} else {
233				// The codec delay is padding before the epoch, not a hole after the
234				// first short frame. Keep later output contiguous by moving it back over
235				// everything trimmed since this decoder epoch began.
236				rewind(mux_frame.timestamp, self.delay_trimmed, rate)?.max(epoch)
237			};
238			if self.end.is_some() {
239				self.frames_decoded += frames;
240			}
241			// Packet continuity stays on the encoded timeline. `decoded_at` may be
242			// earlier because codec pre-skip is padding before the decoded epoch.
243			self.next_start = Some(advance(mux_frame.timestamp, frames + trimmed, rate)?);
244			if decoded.is_empty() {
245				continue;
246			}
247
248			let (pcm, timestamp) = match self.resampler.as_mut() {
249				// The resampler works in fixed chunks, so it holds back whatever didn't
250				// fill one. What comes out next starts with those held-back samples, which
251				// arrived before this packet did, so it is stamped where they arrived.
252				// Reading that off this packet instead would place the audio late by up to
253				// a chunk, sawtoothing A/V sync, and drag it the whole way whenever the
254				// source jumps forward without declaring a hole.
255				Some(r) => {
256					let held = if r.pending_frames() == 0 {
257						decoded_at
258					} else {
259						r.held_at().unwrap_or(decoded_at)
260					};
261					let skipped = r.skipped();
262					let pcm = r.process(&decoded, decoded_at)?;
263					(pcm, rewind(held, skipped, self.resolved_sample_rate)?)
264				}
265				None => (decoded, decoded_at),
266			};
267
268			let decoded_end = advance(decoded_at, frames, rate)?;
269
270			// The resampler hands back samples it was holding from earlier packets,
271			// so what comes out starts before the packet that filled its chunk. Track
272			// where each packet's activity ends so the output can be labelled by
273			// where it actually begins, not by the packet just submitted.
274			let resampled = self.resampler.is_some();
275			if resampled {
276				self.spans.push_back(ActivitySpan {
277					end: decoded_end,
278					activity,
279				});
280			}
281
282			// A packet shorter than the resampler's chunk leaves nothing to hand
283			// over yet. Read on rather than returning a frame with no samples, which
284			// a caller would otherwise see as audio arriving.
285			if pcm.is_empty() {
286				continue;
287			}
288
289			let activity = if resampled {
290				self.activity_at(timestamp)
291			} else {
292				activity
293			};
294			// Queued rather than returned, so a tail drained at a gap earlier in this
295			// same iteration still comes out first. The next turn of the loop pops it.
296			let frame = self.frame(pcm, timestamp, activity)?;
297			self.ready.push_back(frame);
298		}
299	}
300
301	/// Reset every stateful decode stage before the first packet of a new epoch.
302	fn apply_discontinuity(&mut self) -> Result<(), Error> {
303		let discontinuity = self.track.discontinuity();
304		if discontinuity == self.discontinuity {
305			return Ok(());
306		}
307
308		self.discontinuity = discontinuity;
309		self.decoder.reset()?;
310		if let Some(resampler) = self.resampler.as_mut() {
311			resampler.reset();
312		}
313		self.next_start = None;
314		self.spans.clear();
315		self.trailing = Activity::Active;
316		self.epoch = None;
317		self.delay_trimmed = 0;
318		self.frames_decoded = 0;
319		self.end = None;
320		self.terminal_start = None;
321		Ok(())
322	}
323
324	/// Reset codec prediction and resampling state at a hole, returning whatever the
325	/// resampler was still holding from before it.
326	///
327	/// Those samples arrived before the hole and belong before it, so they come
328	/// out as their own frame rather than being filtered together with the audio
329	/// on the far side. The resampler starts over from there, which is what makes
330	/// the next packet's output stamp from the packet itself: nothing is buffered
331	/// to reach back over.
332	fn gap(&mut self) -> Result<Option<Frame>, Error> {
333		self.decoder.reset_prediction()?;
334
335		let mut frame = None;
336		if let Some(resampler) = self.resampler.as_mut() {
337			let held = resampler.held_at();
338			let skipped = resampler.skipped();
339			let pcm = resampler.drain()?;
340			frame = self.tail(pcm, held, skipped)?;
341		}
342
343		self.next_start = None;
344		self.spans.clear();
345		self.trailing = Activity::Active;
346		self.epoch = None;
347		self.delay_trimmed = 0;
348		Ok(frame)
349	}
350
351	/// The tail the resampler is still holding when the track ends, once.
352	///
353	/// Without it the last partial chunk is dropped, which is up to a chunk of
354	/// audio missing from the end of every resampled track. Flushing consumes the
355	/// resampler, which is what makes calling this on every later poll return
356	/// `None` rather than more tails.
357	fn flush(&mut self) -> Result<Option<Frame>, Error> {
358		let Some(resampler) = self.resampler.take() else {
359			return Ok(None);
360		};
361
362		let held = resampler.held_at();
363		let skipped = resampler.skipped();
364		self.tail(resampler.flush()?, held, skipped)
365	}
366
367	/// Stamp and pack a tail the resampler handed back, if it handed back one.
368	///
369	/// `held` is where the samples it was holding arrived, which is where the tail
370	/// begins once the startup frames it dropped of its own are taken off. `None`
371	/// there means the resampler never ran, so there is nothing to place.
372	fn tail(
373		&mut self,
374		pcm: Vec<f32>,
375		held: Option<moq_net::Timestamp>,
376		skipped: usize,
377	) -> Result<Option<Frame>, Error> {
378		let Some(held) = held.filter(|_| !pcm.is_empty()) else {
379			return Ok(None);
380		};
381
382		let timestamp = rewind(held, skipped, self.resolved_sample_rate)?;
383		let activity = self.activity_at(timestamp);
384		Ok(Some(self.frame(pcm, timestamp, activity)?))
385	}
386
387	/// The codec activity covering `timestamp`, dropping the spans it has passed.
388	fn activity_at(&mut self, timestamp: moq_net::Timestamp) -> Activity {
389		while let Some(span) = self.spans.front().filter(|span| span.end <= timestamp) {
390			self.trailing = span.activity;
391			self.spans.pop_front();
392		}
393
394		self.spans.front().map_or(self.trailing, |span| span.activity)
395	}
396
397	/// Remix and pack decoded PCM into an output frame.
398	fn frame(&self, pcm: Vec<f32>, timestamp: moq_net::Timestamp, activity: Activity) -> Result<Frame, Error> {
399		let pcm = if self.decoder.channel_count() == self.resolved_channels {
400			pcm
401		} else {
402			remix(&pcm, self.decoder.channel_count(), self.resolved_channels)?
403		};
404
405		let bytes = self.config.format.from_interleaved_f32(&pcm, self.resolved_channels)?;
406		Ok(Frame {
407			timestamp,
408			data: Bytes::from(bytes),
409			activity,
410		})
411	}
412}
413
414/// Whether `timestamp` fails to continue `expected`, leaving a hole (or an
415/// overlap) rather than the next packet in line.
416///
417/// Exact contiguity cannot be the test. RTMP stamps in whole milliseconds while a
418/// 1024-sample AAC frame at 44.1 kHz runs 23.22 ms, so on the most common ingest
419/// path every packet lands beside where its predecessor ended.
420///
421/// The slack is the quantization the stamps carry, and nothing else. A frame
422/// duration would be far too much: a single lost packet lands exactly one frame
423/// off, and Opus packets run anywhere from 2.5 ms to 60 ms with no duration
424/// declared in the catalog, so a half-frame rule read off a 20 ms neighbour would
425/// splice straight across a lost 2.5 ms one.
426///
427/// So a packet is discontinuous when it misses `expected` by more than one unit of
428/// the coarsest timescale on the path, plus one unit of the stamp's own scale for
429/// the rounding in the arithmetic that produced `expected`. The coarsest timescale
430/// is the stamp's own scale floored at [`Timescale::default`](moq_net::Timescale):
431/// the legacy hang container re-stamps every frame in microseconds whatever the
432/// source used, and a wire that cannot carry a timescale at all (moq-lite before
433/// 05, IETF moq-transport) falls back to milliseconds, so a millisecond is the
434/// finest quantization a packet can be assumed to have kept. That floor stays under
435/// the shortest packet anything here can send, 2.5 ms of Opus, so it never
436/// swallows a lost one.
437fn discontinuous(expected: moq_net::Timestamp, timestamp: moq_net::Timestamp) -> bool {
438	let scale = expected.scale().max(timestamp.scale());
439	let quantum = scale.min(moq_net::Timescale::default());
440	let tolerance = (scale.as_u64() as u128).div_ceil(quantum.as_u64() as u128) + 1;
441	expected.as_scale(scale).abs_diff(timestamp.as_scale(scale)) > tolerance
442}
443
444/// `timestamp` moved forward by `frames` at `sample_rate`, in its own timescale.
445fn advance(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
446	if frames == 0 {
447		return Ok(timestamp);
448	}
449
450	let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
451	Ok(timestamp.checked_add(offset)?)
452}
453
454/// Codec-rate frames in the interval, rounding a microsecond marker to the nearest frame.
455fn frames_between(start: moq_net::Timestamp, end: moq_net::Timestamp, sample_rate: u32) -> Result<usize, Error> {
456	let duration = end.checked_sub(start)?;
457	let frames = (std::time::Duration::from(duration).as_nanos() * sample_rate as u128 + 500_000_000) / 1_000_000_000;
458	usize::try_from(frames).map_err(|_| Error::Unsupported("audio duration does not fit in memory".into()))
459}
460
461/// `timestamp` moved back by `frames` at `sample_rate`, in its own timescale.
462///
463/// Saturates at zero rather than failing: a publisher whose first timestamps
464/// don't advance is odd, but it isn't a reason to end the track.
465fn rewind(timestamp: moq_net::Timestamp, frames: usize, sample_rate: u32) -> Result<moq_net::Timestamp, Error> {
466	if frames == 0 {
467		return Ok(timestamp);
468	}
469
470	let offset = moq_net::Timestamp::from_scale(frames as u64, sample_rate as u64)?.convert(timestamp.scale())?;
471	Ok(timestamp
472		.checked_sub(offset)
473		.unwrap_or(moq_net::Timestamp::new(0, timestamp.scale())?))
474}
475
476#[cfg(test)]
477mod tests {
478	use moq_net::Timestamp;
479
480	use super::*;
481	use crate::Format;
482	use crate::encode::{Encoder, Input, Options, Producer};
483
484	#[tokio::test]
485	async fn remixes_mono_stream_to_stereo_output() {
486		let mut broadcast = moq_net::broadcast::Info::new().produce();
487		let catalog = moq_mux::catalog::Producer::new(&mut broadcast).unwrap();
488		let subscriber = broadcast.consume();
489		let input = Input {
490			format: Format::F32,
491			sample_rate: 48_000,
492			channels: 1,
493		};
494		let options = Options {
495			track: Some("audio".to_string()),
496			..Options::default()
497		};
498		let mut producer = Producer::new(&mut broadcast, catalog, input.clone(), &options).unwrap();
499		let catalog = Encoder::new(&crate::encode::Config::new(input)).unwrap().catalog();
500		let mut consumer = Consumer::new(
501			&subscriber,
502			&catalog,
503			"audio",
504			Config {
505				channels: Some(2),
506				..Config::new()
507			},
508		)
509		.await
510		.unwrap();
511
512		let samples = vec![0.1f32; 960];
513		let mut data = Vec::with_capacity(samples.len() * size_of::<f32>());
514		for sample in samples {
515			data.extend_from_slice(&sample.to_le_bytes());
516		}
517		producer.write(&Frame::new(data.into(), Timestamp::ZERO)).unwrap();
518
519		let frame = consumer.read().await.unwrap().expect("decoded frame");
520		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
521		assert_eq!(samples.len(), (960 - 312) * 2);
522		for pair in samples.chunks_exact(2) {
523			assert_eq!(pair[0], pair[1]);
524		}
525	}
526
527	/// A packet whose sample count isn't a multiple of the resampler's chunk leaves
528	/// samples buffered, and the next output starts with those. Stamping that
529	/// output with the packet that completed the chunk puts it up to a chunk late,
530	/// which is a sawtooth in A/V sync rather than a constant offset. Any codec
531	/// whose frame is not a whole number of chunks reaches it: a 1024-sample frame
532	/// at 44.1 kHz never fills the 882-frame chunk evenly.
533	#[tokio::test]
534	async fn resampled_timestamps_follow_the_samples() {
535		let mut broadcast = moq_net::broadcast::Info::new().produce();
536		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
537		let subscriber = broadcast.consume();
538
539		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
540		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
541
542		let mut consumer = Consumer::new(
543			&subscriber,
544			&catalog,
545			"audio",
546			Config {
547				sample_rate: Some(48_000),
548				..Config::new()
549			},
550		)
551		.await
552		.unwrap();
553
554		// Two 1024-sample packets, back to back at the codec's own rate.
555		const FRAMES: u64 = 1024;
556		let payload: Bytes = vec![0u8; FRAMES as usize * size_of::<f32>()].into();
557		for packet in 0..2 {
558			producer
559				.write(moq_mux::container::Frame {
560					timestamp: moq_net::Timestamp::from_scale(packet * FRAMES, 44_100).unwrap(),
561					duration: None,
562					payload: payload.clone(),
563					keyframe: true,
564				})
565				.unwrap();
566		}
567
568		let first = consumer.read().await.unwrap().expect("decoded frame");
569		assert_eq!(first.timestamp.as_micros(), 0);
570
571		// Continuity, not a fixed number: the second frame starts where the first
572		// one's samples end, whatever they came to. Within a few frames rather than
573		// exactly, because the resampler emits whole frames and its count per chunk
574		// wobbles around the nominal ratio; a real hole (the samples it held back, or
575		// the startup silence it dropped) is twenty times this tolerance.
576		let second = consumer.read().await.unwrap().expect("decoded frame");
577		let first_frames = (first.data.len() / size_of::<f32>()) as u128;
578		let ends_at = first_frames * 1_000_000 / 48_000;
579		let gap = second.timestamp.as_micros().abs_diff(ends_at);
580		assert!(gap < 100, "expected the frames to meet, got a {gap} us gap");
581	}
582
583	/// The resampler only converts whole chunks, so the last partial one has to be
584	/// flushed at end of track or its audio is simply gone. A 1024-sample frame at
585	/// 44.1 kHz guarantees a remainder, never filling the 882-frame chunk evenly.
586	#[tokio::test]
587	async fn resampled_tail_survives_the_end_of_the_track() {
588		let mut broadcast = moq_net::broadcast::Info::new().produce();
589		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
590		let subscriber = broadcast.consume();
591
592		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 44_100, 1);
593		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
594
595		let mut consumer = Consumer::new(
596			&subscriber,
597			&catalog,
598			"audio",
599			Config {
600				sample_rate: Some(48_000),
601				..Config::new()
602			},
603		)
604		.await
605		.unwrap();
606
607		// One 1024-frame packet: 882 fill a chunk, 142 are left holding.
608		const FRAMES: usize = 1024;
609		let payload: Bytes = vec![0u8; FRAMES * size_of::<f32>()].into();
610		producer
611			.write(moq_mux::container::Frame {
612				timestamp: moq_net::Timestamp::ZERO,
613				duration: None,
614				payload,
615				keyframe: true,
616			})
617			.unwrap();
618		producer.finish().unwrap();
619
620		let first = consumer.read().await.unwrap().expect("decoded frame");
621		let first_frames = first.data.len() / size_of::<f32>();
622
623		let tail = consumer.read().await.unwrap().expect("flushed tail");
624		let tail_frames = tail.data.len() / size_of::<f32>();
625
626		// The 142 held-back frames at 44.1 kHz are ~155 at 48 kHz, plus the 69 the
627		// sinc filter still owes: it runs centred, so the end of the track only
628		// emerges once the flush has fed it silence to push it out.
629		assert!((215..=230).contains(&tail_frames), "unexpected tail: {tail_frames}");
630		// It picks up where the first frame's samples ended, within the same few
631		// frames of whole-frame rounding as above.
632		let ends_at = (first_frames as u128) * 1_000_000 / 48_000;
633		let gap = tail.timestamp.as_micros().abs_diff(ends_at);
634		assert!(gap < 100, "expected the tail to meet the body, got a {gap} us gap");
635
636		// Together they cover the packet and no more: 1024 frames at 44.1 kHz is
637		// ~1114 at 48 kHz. The filter's delay does not extend the stream, because
638		// what the drain adds here is what the start dropped off the front.
639		let total = first_frames + tail_frames;
640		assert!((1105..=1120).contains(&total), "unexpected total: {total}");
641		assert!(consumer.read().await.unwrap().is_none());
642	}
643
644	#[tokio::test]
645	async fn resampling_keeps_the_activity_boundary_on_its_source() {
646		let mut encoder = Encoder::new(&crate::encode::Config {
647			dtx: true,
648			bitrate: Some(24_000),
649			frame_duration: std::time::Duration::from_millis(10),
650			..crate::encode::Config::new(Input {
651				channels: 1,
652				..Input::default()
653			})
654		})
655		.unwrap();
656		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 1);
657
658		let mut broadcast = moq_net::broadcast::Info::new().produce();
659		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
660		let subscriber = broadcast.consume();
661		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
662		let mut consumer = Consumer::new(
663			&subscriber,
664			&catalog,
665			"audio",
666			Config {
667				sample_rate: Some(44_100),
668				..Config::new()
669			},
670		)
671		.await
672		.unwrap();
673
674		let active = vec![0.5; encoder.frame_size()];
675		let silence = vec![0.0; encoder.frame_size()];
676		let mut first_dtx = None;
677		for index in 0..40u64 {
678			let packet = encoder.encode(if index == 0 { &active } else { &silence }).unwrap();
679			let timestamp = Timestamp::from_scale(index * encoder.frame_size() as u64, 48_000).unwrap();
680			if first_dtx.is_none() && packet.activity.is_dtx() {
681				first_dtx = Some(timestamp);
682			}
683			producer
684				.write(moq_mux::container::Frame {
685					timestamp,
686					payload: packet.payload,
687					keyframe: true,
688					duration: None,
689				})
690				.unwrap();
691			producer.cut(None).unwrap();
692		}
693		producer.finish().unwrap();
694
695		let expected = first_dtx.expect("silence should enter Opus DTX");
696		let mut actual = None;
697		while let Some(frame) = consumer.read().await.unwrap() {
698			// 10 ms packets do not fill the 20 ms chunk, so the resampler hands back
699			// nothing every other packet. Those must not surface as frames: a frame
700			// with no samples reads as audio arriving, and carries an activity
701			// describing samples that are not there.
702			assert!(!frame.data.is_empty(), "read returned a frame with no samples");
703			if frame.activity.is_dtx() {
704				actual = Some(frame.timestamp);
705				break;
706			}
707		}
708		let actual = actual.expect("consumer should report Opus DTX");
709
710		// Each frame carries the activity its first sample came from, so the label
711		// can lag its source by up to the frame it lands in, but it must never lead
712		// it: leading means samples that are still active got labelled DTX. That is
713		// what labelling by the packet most recently submitted does, since the
714		// resampler is handing back audio from before that packet. It puts the
715		// boundary a chunk early instead of a fraction of a chunk late.
716		let delay = actual.as_micros() as i128 - expected.as_micros() as i128;
717		let chunk_us = 20_000i128;
718		assert!(
719			(0..chunk_us).contains(&delay),
720			"DTX label landed {delay} us from its source, outside [0, {chunk_us})"
721		);
722	}
723
724	/// Publish PCM packets of `frames` samples each at the given stamps, and read
725	/// back every decoded frame as `(microseconds, output frames)`.
726	async fn pcm_gaps(rate: u32, out_rate: u32, frames: usize, stamps: &[Timestamp]) -> Vec<(u128, usize)> {
727		let mut broadcast = moq_net::broadcast::Info::new().produce();
728		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
729		let subscriber = broadcast.consume();
730
731		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, rate, 1);
732		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
733		let mut consumer = Consumer::new(
734			&subscriber,
735			&catalog,
736			"audio",
737			Config {
738				sample_rate: Some(out_rate),
739				..Config::new()
740			},
741		)
742		.await
743		.unwrap();
744
745		let payload: Bytes = vec![0u8; frames * size_of::<f32>()].into();
746		for stamp in stamps {
747			producer
748				.write(moq_mux::container::Frame {
749					timestamp: *stamp,
750					duration: None,
751					payload: payload.clone(),
752					keyframe: true,
753				})
754				.unwrap();
755		}
756		producer.finish().unwrap();
757
758		let mut read = Vec::new();
759		while let Some(frame) = consumer.read().await.unwrap() {
760			read.push((frame.timestamp.as_micros(), frame.data.len() / size_of::<f32>()));
761		}
762		read
763	}
764
765	/// A packet that doesn't continue the last one is a hole, not a splice: the
766	/// resampler hands back what it was holding from before the gap as its own
767	/// frame, and the audio after it is stamped from the packet that carried it
768	/// rather than rewound over samples that no longer exist.
769	#[tokio::test]
770	async fn a_missing_packet_leaves_a_hole() {
771		const FRAMES: usize = 1024;
772		// Packets at sample 0 and sample 2048: the one at 1024 never arrived.
773		let stamps = [
774			Timestamp::from_scale(0, 44_100).unwrap(),
775			Timestamp::from_scale(2 * FRAMES as u64, 44_100).unwrap(),
776		];
777		let read = pcm_gaps(44_100, 48_000, FRAMES, &stamps).await;
778
779		// The first packet's chunk, then the tail drained at the gap, then the
780		// second packet's chunk. The flush at end of track adds the last tail.
781		assert_eq!(read.len(), 4, "unexpected frames: {read:?}");
782
783		// Everything the first packet carried comes out before the hole: 1024 frames
784		// at 44.1 kHz is ~1114 at 48 kHz, whole-frame rounding aside.
785		let before: usize = read[..2].iter().map(|(_, frames)| frames).sum();
786		assert!((1105..=1120).contains(&before), "unexpected pre-gap audio: {before}");
787
788		// The audio after the hole is stamped by its own packet. Rewinding over the
789		// resampler's buffer instead would put it ~3 ms early, in the middle of the
790		// hole, and splice the two sides together through the filter.
791		assert_eq!(read[2].0, stamps[1].as_micros());
792
793		// And the hole is the packet that never arrived: 1024 frames at 44.1 kHz.
794		let ends_at = read[1].0 + (read[1].1 as u128) * 1_000_000 / 48_000;
795		let hole = read[2].0 - ends_at;
796		assert!((23_100..=23_350).contains(&hole), "unexpected hole: {hole} us");
797	}
798
799	/// A packet can land a hair past where the last one ended without being a hole:
800	/// the stamps are quantized, so `discontinuous` allows a millisecond of slack.
801	/// The jump is still a jump, and the resampler is holding samples from before
802	/// it. Deriving their stamp by counting back from the packet drags them forward
803	/// by the whole jump; reading it off the packet they arrived with does not.
804	#[tokio::test]
805	async fn a_jump_inside_the_slack_leaves_the_held_samples_alone() {
806		// 441 frames at 44.1 kHz is 10 ms, half of the 20 ms chunk, so the first
807		// packet is held whole and the second is what completes the chunk.
808		const FRAMES: usize = 441;
809		// A millisecond past where the first packet ended, which is the slack the
810		// legacy container's microsecond re-stamping is allowed.
811		let stamps = [
812			Timestamp::from_micros(0).unwrap(),
813			Timestamp::from_micros(11_000).unwrap(),
814		];
815		let read = pcm_gaps(44_100, 48_000, FRAMES, &stamps).await;
816
817		// The chunk, then the flush at the end of the track: no hole was declared, so
818		// nothing was drained in between.
819		assert_eq!(read.len(), 2, "unexpected frames: {read:?}");
820		// It starts with the first packet's samples, so it is stamped where that
821		// packet was. Rewinding from the second one instead puts it a millisecond late.
822		assert_eq!(read[0].0, 0, "held samples moved with the jump: {read:?}");
823	}
824
825	#[tokio::test]
826	async fn a_jump_after_a_full_chunk_uses_the_new_packet_timestamp() {
827		let stamps = [
828			Timestamp::from_micros(0).unwrap(),
829			Timestamp::from_micros(21_000).unwrap(),
830		];
831		let read = pcm_gaps(44_100, 48_000, 882, &stamps).await;
832		let mut r = crate::Resampler::new(44_100, 48_000, 1, 882).unwrap();
833		r.process(&[0.25; 882], stamps[0]).unwrap();
834		let expected = rewind(stamps[1], r.skipped(), 48_000).unwrap().as_micros();
835		assert_eq!(read[1].0, expected);
836	}
837
838	/// Once an end marker arrives the gap check stops running, because from there
839	/// each batch's time is reconstructed from the marker rather than read off the
840	/// packet. A packet that jumps forward then still moves whatever the resampler
841	/// is holding, and the activity that lands with it: those samples came from
842	/// before the jump and are labelled by the packet they came from.
843	#[tokio::test]
844	async fn a_terminal_jump_leaves_the_held_samples_alone() {
845		let mut encoder = Encoder::new(&crate::encode::Config {
846			dtx: true,
847			bitrate: Some(24_000),
848			..crate::encode::Config::new(Input {
849				channels: 1,
850				..Input::default()
851			})
852		})
853		.unwrap();
854		let catalog = encoder.catalog();
855
856		// One coded packet, then a withheld one to follow it. Taken from the same
857		// encoder rather than published in between, so nothing fills the resampler's
858		// chunk between the two.
859		let active = encoder.encode(&vec![0.5f32; encoder.frame_size()]).unwrap();
860		assert!(active.activity.is_active());
861		let silence = vec![0.0f32; encoder.frame_size()];
862		let dtx = (0..200)
863			.map(|_| encoder.encode(&silence).unwrap())
864			.find(|packet| packet.activity.is_dtx())
865			.expect("silence should enter Opus DTX");
866
867		let mut broadcast = moq_net::broadcast::Info::new().produce();
868		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
869		let subscriber = broadcast.consume();
870		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
871		let mut consumer = Consumer::new(
872			&subscriber,
873			&catalog,
874			"audio",
875			Config {
876				sample_rate: Some(44_100),
877				..Config::new()
878			},
879		)
880		.await
881		.unwrap();
882
883		// A 20 ms Opus packet decodes 960 frames, less the pre-skip on the first one,
884		// so it doesn't fill the 960-frame chunk and is held whole.
885		let write = |producer: &mut moq_mux::container::Producer<_>, frames: u64, payload: Bytes| {
886			producer
887				.write(moq_mux::container::Frame {
888					timestamp: Timestamp::from_scale(frames, 48_000).unwrap(),
889					duration: None,
890					payload,
891					keyframe: true,
892				})
893				.unwrap();
894		};
895		write(&mut producer, 0, active.payload);
896		// The end marker, then the terminal packet a second past where it belongs.
897		write(&mut producer, 3 * 48_000, Bytes::new());
898		write(&mut producer, 48_000, dtx.payload);
899		producer.finish().unwrap();
900
901		let frame = consumer.read().await.unwrap().expect("decoded frame");
902		// The output begins with the first packet's samples, so it is stamped and
903		// labelled from that packet. Rewinding from the terminal one instead drops it
904		// most of a second into the future, carrying the DTX label with it.
905		assert_eq!(frame.timestamp.as_micros(), 0, "held samples moved with the jump");
906		assert!(
907			frame.activity.is_active(),
908			"held samples took the terminal packet's label"
909		);
910	}
911
912	/// Every packet on the RTMP path lands beside where the last one ended: FLV
913	/// stamps in whole milliseconds and a 1024-sample AAC frame at 44.1 kHz runs
914	/// 23.22 ms, so the stamps drift up to a millisecond either way. Reading that as
915	/// a hole would reset the codec and the resampler on nearly every packet.
916	///
917	/// PCM stands in for AAC, which needs an encoder this crate doesn't have: the
918	/// arithmetic that matters is the packet length and the millisecond stamps.
919	#[tokio::test]
920	async fn millisecond_stamps_are_not_a_gap() {
921		const FRAMES: u64 = 1024;
922		const PACKETS: u64 = 32;
923
924		// What an FLV ingest sends: each packet stamped in whole milliseconds.
925		let stamps: Vec<_> = (0..PACKETS)
926			.map(|packet| Timestamp::from_millis(packet * FRAMES * 1_000 / 44_100).unwrap())
927			.collect();
928		let read = pcm_gaps(44_100, 48_000, FRAMES as usize, &stamps).await;
929
930		// One frame per packet, since 1024 frames always fill at least one 882-frame
931		// chunk, plus the flush at the end of the track. Reading a gap would drain
932		// the resampler as well, adding a frame at every packet it fired on.
933		assert_eq!(read.len(), stamps.len() + 1, "unexpected frames: {read:?}");
934
935		// And the output stays continuous across all of them, within the millisecond
936		// the stamps themselves are quantized to.
937		for pair in read.windows(2) {
938			let ends_at = pair[0].0 + (pair[0].1 as u128) * 1_000_000 / 48_000;
939			assert!(
940				pair[1].0.abs_diff(ends_at) <= 1_100,
941				"frames at {} and {} do not meet",
942				pair[0].0,
943				pair[1].0
944			);
945		}
946	}
947
948	/// The tolerance can't come from a frame duration. Opus packets run from 2.5 ms
949	/// to 60 ms with nothing in the catalog to say which, so a rule read off the
950	/// 20 ms packet before it would splice straight across a lost 2.5 ms one.
951	#[tokio::test]
952	async fn a_lost_opus_packet_shorter_than_its_neighbour_is_a_gap() {
953		let input = Input {
954			format: Format::F32,
955			sample_rate: 48_000,
956			channels: 1,
957		};
958		let mut encoder = Encoder::new(&crate::encode::Config::new(input)).unwrap();
959		let catalog = encoder.catalog();
960
961		let mut broadcast = moq_net::broadcast::Info::new().produce();
962		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
963		let subscriber = broadcast.consume();
964		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
965		let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
966			.await
967			.unwrap();
968
969		// A 20 ms packet at 0, then the next one at 22.5 ms: the 2.5 ms packet
970		// between them was lost.
971		let pcm = vec![0.25f32; encoder.frame_size()];
972		for timestamp in [
973			Timestamp::from_micros(0).unwrap(),
974			Timestamp::from_micros(22_500).unwrap(),
975			Timestamp::from_micros(42_500).unwrap(),
976		] {
977			producer
978				.write(moq_mux::container::Frame {
979					timestamp,
980					duration: None,
981					payload: encoder.encode(&pcm).unwrap().payload,
982					keyframe: true,
983				})
984				.unwrap();
985			producer.cut(None).unwrap();
986		}
987
988		// The pre-skip is trimmed off the first packet, so it decodes short. That
989		// shortfall is codec delay, not a hole: without counting it the packet after
990		// every stream start would read as a gap.
991		let first = consumer.read().await.unwrap().expect("decoded frame");
992		let frames = first.data.len() / size_of::<f32>();
993		assert!(frames < 960, "the pre-skip should be trimmed, got {frames} frames");
994
995		// The hole is real, so codec prediction starts over but stream-level pre-skip
996		// does not. The audio is stamped where the packet says rather than 2.5 ms early.
997		let second = consumer.read().await.unwrap().expect("decoded frame");
998		assert_eq!(second.timestamp.as_micros(), 22_500);
999		assert_eq!(second.data.len() / size_of::<f32>(), 960, "pre-skip was reapplied");
1000
1001		let third = consumer.read().await.unwrap().expect("decoded frame after gap");
1002		let second_frames = second.data.len() / size_of::<f32>();
1003		assert_eq!(
1004			third.timestamp,
1005			advance(second.timestamp, second_frames, 48_000).unwrap()
1006		);
1007	}
1008
1009	#[tokio::test]
1010	async fn latency_max_is_clamped_to_publisher_retention() {
1011		let mut broadcast = moq_net::broadcast::Info::new().produce();
1012		let info = hang::container::track_info().with_latency_max(std::time::Duration::from_millis(100));
1013		let _track = broadcast.create_track("audio", info).unwrap();
1014		let subscriber = broadcast.consume();
1015		let catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
1016
1017		let consumer = Consumer::new(
1018			&subscriber,
1019			&catalog,
1020			"audio",
1021			Config {
1022				latency_max: Some(std::time::Duration::from_millis(500)),
1023				..Config::new()
1024			},
1025		)
1026		.await
1027		.unwrap();
1028
1029		assert_eq!(consumer.latency_max(), std::time::Duration::from_millis(100));
1030	}
1031
1032	/// Opus pre-skip is padding before the decoded epoch, not missing media after
1033	/// the first short frame. The second frame must meet the first or playback
1034	/// fills the codec delay with silence and creates a startup glitch.
1035	#[tokio::test]
1036	async fn opus_pre_skip_does_not_leave_a_timestamp_hole() {
1037		let input = Input {
1038			format: Format::F32,
1039			sample_rate: 48_000,
1040			channels: 1,
1041		};
1042		let mut encoder = Encoder::new(&crate::encode::Config::new(input)).unwrap();
1043		let catalog = encoder.catalog();
1044
1045		let mut broadcast = moq_net::broadcast::Info::new().produce();
1046		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1047		let subscriber = broadcast.consume();
1048		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);
1049		let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
1050			.await
1051			.unwrap();
1052
1053		let pcm = vec![0.25f32; encoder.frame_size()];
1054		for packet in 0..2 {
1055			producer
1056				.write(moq_mux::container::Frame {
1057					timestamp: Timestamp::from_scale(packet * encoder.frame_size() as u64, 48_000).unwrap(),
1058					duration: None,
1059					payload: encoder.encode(&pcm).unwrap().payload,
1060					keyframe: true,
1061				})
1062				.unwrap();
1063			producer.cut(None).unwrap();
1064		}
1065
1066		let first = consumer.read().await.unwrap().expect("first decoded frame");
1067		let second = consumer.read().await.unwrap().expect("second decoded frame");
1068		let first_frames = first.data.len() / size_of::<f32>();
1069		let expected = advance(first.timestamp, first_frames, 48_000).unwrap();
1070		assert_eq!(second.timestamp, expected);
1071	}
1072
1073	#[tokio::test]
1074	async fn reads_the_container_the_catalog_declares() {
1075		let mut broadcast = moq_net::broadcast::Info::new().produce();
1076		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1077		let subscriber = broadcast.consume();
1078
1079		let mut catalog = hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Pcm, 48_000, 1);
1080		catalog.container = hang::catalog::Container::Loc;
1081
1082		let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Loc);
1083		let mut consumer = Consumer::new(
1084			&subscriber,
1085			&catalog,
1086			"audio",
1087			Config {
1088				format: Format::F32,
1089				..Config::new()
1090			},
1091		)
1092		.await
1093		.unwrap();
1094
1095		let samples = [0.25f32, -0.5, 0.75, -1.0];
1096		let payload: Vec<u8> = samples.iter().flat_map(|sample| sample.to_le_bytes()).collect();
1097		producer
1098			.write(moq_mux::container::Frame {
1099				timestamp: Timestamp::ZERO,
1100				duration: None,
1101				payload: payload.into(),
1102				keyframe: true,
1103			})
1104			.unwrap();
1105
1106		let frame = consumer.read().await.unwrap().expect("decoded frame");
1107		assert_eq!(
1108			Format::F32.as_interleaved_f32(&frame.data, 1).unwrap().as_ref(),
1109			samples
1110		);
1111	}
1112
1113	/// The catalog picks the framing, not this crate. Hardcoding the legacy wire
1114	/// read a CMAF fragment as a varint timestamp plus a payload, which handed the
1115	/// codec garbage instead of failing, so anything published by `moq import
1116	/// fmp4` was undecodable.
1117	#[tokio::test]
1118	async fn decodes_a_cmaf_framed_track() {
1119		let input = Input {
1120			format: Format::F32,
1121			sample_rate: 48_000,
1122			channels: 2,
1123		};
1124
1125		// One real Opus packet, so a mis-framed read can't accidentally decode.
1126		let mut encoder = Encoder::new(&crate::encode::Config::new(input.clone())).unwrap();
1127		let mut catalog = encoder.catalog();
1128		let pcm = vec![0.0f32; encoder.frame_size() * encoder.codec_channels() as usize];
1129		let packet = encoder.encode(&pcm).unwrap();
1130
1131		// Re-describe the same rendition as CMAF and publish it that way.
1132		let muxer = moq_mux::container::fmp4::Muxer::audio(&catalog).unwrap();
1133		let init = muxer.init().unwrap().expect("an out-of-band codec has an init segment");
1134		catalog.container = hang::catalog::Container::Cmaf { init };
1135
1136		let mut broadcast = moq_net::broadcast::Info::new().produce();
1137		let subscriber = broadcast.consume();
1138		let track = broadcast.create_track("audio", hang::container::track_info()).unwrap();
1139		let container = moq_mux::catalog::hang::Container::try_from(&catalog.container).unwrap();
1140		let mut producer = moq_mux::container::Producer::new(track, container);
1141
1142		let mut consumer = Consumer::new(&subscriber, &catalog, "audio", Config::new())
1143			.await
1144			.unwrap();
1145
1146		producer
1147			.write(moq_mux::container::Frame {
1148				timestamp: Timestamp::ZERO,
1149				payload: packet.payload,
1150				keyframe: true,
1151				duration: None,
1152			})
1153			.unwrap();
1154		producer.cut(None).unwrap();
1155
1156		// The whole packet decodes: one 20 ms Opus frame at 48 kHz, less the pre-skip
1157		// trimmed off the first packet. Reading the fragment as legacy hands the codec
1158		// a slice of the moof instead, which still decodes, just to a shorter buffer.
1159		let frame = consumer.read().await.unwrap().expect("decoded frame");
1160		// `as_micros`, not `==`: the CMAF path carries the fmp4 timescale and
1161		// `Timestamp`'s equality is structural, so the scales would have to match too.
1162		assert_eq!(frame.timestamp.as_micros(), 0);
1163		let samples = Format::F32.as_interleaved_f32(&frame.data, 2).unwrap();
1164		assert_eq!(samples.len(), (960 - 312) * 2);
1165	}
1166}