Skip to main content

moq/
audio.rs

1//! Raw-audio import/export via [`moq_audio`].
2//!
3//! Sibling to `moq_publish_media_*` / `moq_consume_audio`
4//! (those handle already-encoded frames). These functions accept and
5//! return raw PCM, with Opus encode/decode happening inside the FFI
6//! boundary.
7//!
8//! Format / sample rate / channel count are fixed at producer or
9//! consumer construction via [`moq_audio_encoder_input`] /
10//! [`moq_audio_encoder_output`] / [`moq_audio_decoder_output`], so
11//! each [`moq_audio_frame`] carries only payload bytes and a
12//! timestamp.
13
14use std::ffi::{c_char, c_void};
15use std::sync::Arc;
16use std::time::Duration;
17
18use bytes::Bytes;
19use tokio::sync::oneshot;
20
21use crate::ffi::OnStatus;
22use crate::{Error, Id, NonZeroSlab, Shared, State, ffi};
23
24// ---- C-visible types ----
25
26/// Raw PCM sample layout, mirroring WebCodecs `AudioData.format`.
27///
28/// The enum is exposed in the C header for readability, but ABI
29/// fields/parameters that carry it are typed `u32`. A C caller
30/// passing an unknown discriminant gets `Error::InvalidCode` instead
31/// of UB.
32///
33/// <https://developer.mozilla.org/en-US/docs/Web/API/AudioData/format>
34#[repr(C)]
35#[allow(non_camel_case_types)]
36#[derive(Clone, Copy, Debug)]
37pub enum moq_audio_sample_format {
38	MOQ_AUDIO_SAMPLE_FORMAT_U8 = 0,
39	MOQ_AUDIO_SAMPLE_FORMAT_S16 = 1,
40	MOQ_AUDIO_SAMPLE_FORMAT_S32 = 2,
41	MOQ_AUDIO_SAMPLE_FORMAT_F32 = 3,
42	MOQ_AUDIO_SAMPLE_FORMAT_U8_PLANAR = 4,
43	MOQ_AUDIO_SAMPLE_FORMAT_S16_PLANAR = 5,
44	MOQ_AUDIO_SAMPLE_FORMAT_S32_PLANAR = 6,
45	MOQ_AUDIO_SAMPLE_FORMAT_F32_PLANAR = 7,
46}
47
48fn audio_format_from_u32(value: u32) -> Result<moq_audio::Format, Error> {
49	use moq_audio::Format;
50	Ok(match value {
51		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_U8 as u32 => Format::U8,
52		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_S16 as u32 => Format::S16,
53		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_S32 as u32 => Format::S32,
54		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_F32 as u32 => Format::F32,
55		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_U8_PLANAR as u32 => Format::U8Planar,
56		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_S16_PLANAR as u32 => Format::S16Planar,
57		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_S32_PLANAR as u32 => Format::S32Planar,
58		v if v == moq_audio_sample_format::MOQ_AUDIO_SAMPLE_FORMAT_F32_PLANAR as u32 => Format::F32Planar,
59		_ => return Err(Error::InvalidCode),
60	})
61}
62
63/// PCM layout the caller hands to [`moq_encode_audio_frame`].
64#[repr(C)]
65#[allow(non_camel_case_types)]
66pub struct moq_audio_encoder_input {
67	/// `moq_audio_sample_format` discriminant.
68	pub format: u32,
69	pub sample_rate: u32,
70	pub channels: u32,
71}
72
73/// Codec-side configuration. `sample_rate` / `channels` = 0 means
74/// "match the input (snapping the rate up to a libopus-supported
75/// value if necessary)".
76#[repr(C)]
77#[allow(non_camel_case_types)]
78pub struct moq_audio_encoder_output {
79	/// Codec id, UTF-8 (currently only "opus").
80	pub codec: *const c_char,
81	pub codec_len: usize,
82	/// 0 = derive from input.
83	pub sample_rate: u32,
84	/// 0 = derive from input.
85	pub channels: u32,
86	/// 0 = libopus default.
87	pub bitrate: u32,
88	/// Encoded frame duration in microseconds. Opus accepts exactly
89	/// 2500/5000/10000/20000/40000/60000 us. 0 = the 20 ms default, which
90	/// matches the JS publish path.
91	pub frame_duration_us: u32,
92}
93
94/// PCM layout the caller wants out of [`moq_decode_audio`].
95#[repr(C)]
96#[allow(non_camel_case_types)]
97pub struct moq_audio_decoder_output {
98	pub format: u32,
99	/// 0 = deliver at the codec's native sample rate.
100	pub sample_rate: u32,
101	/// 0 = deliver at the codec's native channel count.
102	pub channels: u32,
103	/// Upper bound on buffering before skipping a stalled group, in
104	/// microseconds. Same congestion-control knob as
105	/// `moq_consume_audio`'s `max_age_us`. 0 = skip
106	/// aggressively (the moq-mux default); set to your playout
107	/// buffer (tens to a few hundred ms) for a softer skip. Named
108	/// `_max` to leave room for a future `min_buffer_us`, a
109	/// jitter-buffer floor rather than a staleness bound.
110	pub max_age_us: u64,
111}
112
113/// One audio frame: payload bytes plus a presentation timestamp.
114///
115/// `data` is owned by the consume slab (see
116/// [`moq_decode_audio_frame_free`]) or borrowed by the publish call
117/// (the publisher copies before returning).
118#[repr(C)]
119#[allow(non_camel_case_types)]
120pub struct moq_audio_frame {
121	pub timestamp_us: u64,
122	pub data: *const u8,
123	pub data_size: usize,
124}
125
126// ---- State extensions (used internally by lib.rs) ----
127
128/// An audio producer, shared so the Opus encode in `write` runs with the global
129/// lock released. See [`Shared`].
130pub(crate) struct AudioEncoder {
131	producer: moq_audio::encode::Producer<moq_mux::catalog::hang::Extra>,
132	reservation: Option<Arc<moq_net::bandwidth::Reservation>>,
133}
134
135type AudioProducer = Shared<AudioEncoder>;
136
137#[derive(Default)]
138pub struct Audio {
139	producers: NonZeroSlab<AudioProducer>,
140	consumer_tasks: NonZeroSlab<Option<AudioTaskEntry>>,
141	frames: NonZeroSlab<moq_audio::Frame>,
142}
143
144/// A spawned task entry: `close` signals shutdown, `callback` delivers status.
145///
146/// `close` is an `Option` so `consume_close` can drop just the sender without
147/// removing the entry. The task delivers one final terminal callback and then
148/// removes itself, so `user_data` stays valid until that callback fires.
149struct AudioTaskEntry {
150	close: Option<oneshot::Sender<()>>,
151	callback: OnStatus,
152}
153
154impl Audio {
155	pub fn publish(
156		&mut self,
157		broadcast: &mut moq_net::broadcast::Producer,
158		catalog: moq_mux::catalog::Producer<moq_mux::catalog::hang::Extra>,
159		input: moq_audio::encode::Input,
160		options: moq_audio::encode::Options,
161		reserve: bool,
162	) -> Result<Id, Error> {
163		let producer = moq_audio::encode::Producer::new(broadcast, catalog, input, &options)?;
164		let reservation = reserve.then(|| Arc::new(options.bandwidth.reserve(&producer.demand(), producer.bitrate())));
165		self.producers
166			.insert(Shared::new(AudioEncoder { producer, reservation }))
167	}
168
169	pub(crate) fn reservation(&self, id: Id) -> Result<Option<Arc<moq_net::bandwidth::Reservation>>, Error> {
170		Ok(self
171			.producer(id)?
172			.lock()
173			.as_ref()
174			.ok_or(Error::MediaNotFound)?
175			.reservation
176			.clone())
177	}
178
179	/// A watch-only handle to the encoded track's subscriber demand.
180	pub(crate) fn demand(&self, id: Id) -> Result<moq_net::track::Demand, Error> {
181		Ok(self
182			.producer(id)?
183			.lock()
184			.as_ref()
185			.ok_or(Error::MediaNotFound)?
186			.producer
187			.demand())
188	}
189
190	/// Resolve a producer handle, so the caller can encode with the global lock
191	/// released.
192	///
193	/// Bind the result before locking it: a temporary [`State`] guard lives to the
194	/// end of the statement that created it, so resolving and locking in one
195	/// expression would put the encode back under the global lock.
196	pub(crate) fn producer(&self, id: Id) -> Result<AudioProducer, Error> {
197		self.producers.get(id).cloned().ok_or(Error::MediaNotFound)
198	}
199
200	/// Resolve a producer and drop its id, so nothing can be published to it after.
201	pub(crate) fn remove(&mut self, id: Id) -> Result<AudioProducer, Error> {
202		self.producers.remove(id).ok_or(Error::MediaNotFound)
203	}
204
205	pub fn consume(
206		&mut self,
207		broadcast: &moq_net::broadcast::Consumer,
208		catalog: &hang::catalog::AudioConfig,
209		name: &str,
210		config: moq_audio::decode::Options,
211		on_frame: OnStatus,
212	) -> Result<Id, Error> {
213		let broadcast = broadcast.clone();
214		let catalog = catalog.clone();
215		let name = name.to_string();
216
217		let channel = oneshot::channel();
218		let entry = AudioTaskEntry {
219			close: Some(channel.0),
220			callback: on_frame,
221		};
222		let id = self.consumer_tasks.insert(Some(entry))?;
223
224		// `decode::Consumer::new` subscribes (blocking on SUBSCRIBE_OK), so run it
225		// inside the task to keep this entrypoint non-blocking.
226		tokio::spawn(async move {
227			let res = async move {
228				let consumer = moq_audio::decode::Consumer::new(&broadcast, &catalog, name, config).await?;
229				Self::run(on_frame, consumer, channel.1).await
230			}
231			.await;
232
233			// Deliver one final terminal callback (code <= 0), then drop the entry.
234			// Pull it out from under the lock so the callback never runs while held.
235			let entry = State::lock().audio.consumer_tasks.remove(id).flatten();
236			if let Some(entry) = entry {
237				entry.callback.call(res);
238			}
239		});
240
241		Ok(id)
242	}
243
244	async fn run(
245		callback: OnStatus,
246		mut consumer: moq_audio::decode::Consumer,
247		mut close: oneshot::Receiver<()>,
248	) -> Result<(), Error> {
249		loop {
250			// `biased` so a pending close always wins over a ready frame.
251			let frame = tokio::select! {
252				biased;
253				_ = &mut close => return Ok(()),
254				frame = consumer.read() => match frame {
255					Ok(Some(frame)) => frame,
256					Ok(None) => return Ok(()),
257					// One packet the codec rejected is that packet's problem: the
258					// decoder stays usable, so drop it and keep the subscription rather
259					// than ending the caller's stream over a single bad frame.
260					Err(moq_audio::Error::Decode(err)) => {
261						tracing::warn!(%err, "dropping an audio frame");
262						continue;
263					}
264					Err(err) => return Err(err.into()),
265				},
266			};
267
268			// Hold the lock only to buffer the frame; release it before the callback.
269			let frame_id = State::lock().audio.frames.insert(frame)?;
270			callback.call(Ok(frame_id));
271		}
272	}
273
274	pub fn consume_close(&mut self, id: Id) -> Result<(), Error> {
275		// Signal shutdown; the task delivers a final callback and removes itself.
276		self.consumer_tasks
277			.get_mut(id)
278			.and_then(|entry| entry.as_mut())
279			.ok_or(Error::TrackNotFound)?
280			.close
281			.take()
282			.ok_or(Error::TrackNotFound)?;
283		Ok(())
284	}
285
286	pub fn frame_info(&self, id: Id, dst: &mut moq_audio_frame) -> Result<(), Error> {
287		let frame = self.frames.get(id).ok_or(Error::FrameNotFound)?;
288		*dst = moq_audio_frame {
289			// The C ABI carries plain microseconds, so flatten the scaled
290			// `Timestamp` here at the boundary. Saturating rather than erroring:
291			// this is a getter on a frame we already decoded, and a u64 overflow
292			// needs a timestamp ~580,000 years out.
293			timestamp_us: u64::try_from(frame.timestamp.as_micros()).unwrap_or(u64::MAX),
294			data: frame.data.as_ptr(),
295			data_size: frame.data.len(),
296		};
297		Ok(())
298	}
299
300	pub fn frame_free(&mut self, id: Id) -> Result<(), Error> {
301		self.frames.remove(id).ok_or(Error::FrameNotFound)?;
302		Ok(())
303	}
304}
305
306// ---- C entry points ----
307
308/// Open an audio track on a broadcast.
309///
310/// The encoder configuration is fixed at construction; subsequent
311/// frame writes pass only payload + timestamp via
312/// [`moq_encode_audio_frame`].
313///
314/// Returns a non-zero handle on success or a negative error code.
315///
316/// # Safety
317/// - `name` must point to `name_len` bytes of UTF-8.
318/// - `input` / `output` must point to fully populated structs.
319/// - `output->codec` must point to `output->codec_len` bytes of UTF-8.
320/// - `bandwidth` is a handle from [`crate::moq_session_bandwidth`], or 0 to leave the
321///   configured bitrate unclaimed.
322#[unsafe(no_mangle)]
323pub unsafe extern "C" fn moq_encode_audio(
324	broadcast: u32,
325	name: *const c_char,
326	name_len: usize,
327	input: *const moq_audio_encoder_input,
328	output: *const moq_audio_encoder_output,
329	bandwidth: u32,
330) -> i32 {
331	ffi::enter(move || {
332		let broadcast = ffi::parse_id(broadcast)?;
333		let name = unsafe { ffi::parse_str(name, name_len)? }.to_string();
334		let raw_input = unsafe { input.as_ref() }.ok_or(Error::InvalidPointer)?;
335		let raw_output = unsafe { output.as_ref() }.ok_or(Error::InvalidPointer)?;
336		let codec_str = unsafe { ffi::parse_str(raw_output.codec, raw_output.codec_len)? };
337
338		let layout = moq_audio::Layout::from_channels(raw_input.channels)?;
339		let mut encoder_input = moq_audio::encode::Input::new(raw_input.sample_rate, layout);
340		encoder_input.format = audio_format_from_u32(raw_input.format)?;
341
342		// The C ABI takes an explicit track name and spells "unset" as 0, so map
343		// both onto the Rust options here rather than leaking either convention.
344		let mut options = moq_audio::encode::Options::default();
345		options.track = Some(name);
346		let codec = codec_str
347			.parse()
348			.map_err(|_| Error::UnknownFormat(codec_str.to_string()))?;
349		options.settings = moq_audio::encode::Settings::from_input(codec, &encoder_input);
350		if let Some(sample_rate) = zeroable(raw_output.sample_rate) {
351			options.settings.sample_rate = sample_rate;
352		}
353		if let Some(channels) = zeroable(raw_output.channels) {
354			options.settings.layout = moq_audio::Layout::from_channels(channels)?;
355		}
356		options.settings.bitrate =
357			zeroable(raw_output.bitrate).map(|bps| moq_net::bandwidth::Rate::from_bps(bps.into()));
358		if let Some(micros) = zeroable(raw_output.frame_duration_us) {
359			options.settings.frame_duration = Duration::from_micros(micros.into());
360		}
361
362		let bandwidth = ffi::parse_id_optional(bandwidth)?;
363		let mut state = State::lock();
364		if let Some(id) = bandwidth {
365			options.bandwidth = state.bandwidth.allocator(id)?;
366		}
367		let State { publish, audio, .. } = &mut *state;
368		let (broadcast_producer, catalog) = publish.pair_mut(broadcast)?;
369
370		audio.publish(
371			broadcast_producer,
372			catalog.clone(),
373			encoder_input,
374			options,
375			bandwidth.is_some(),
376		)
377	})
378}
379
380/// This encoder's bandwidth reservation, or 0 if it was published without one.
381///
382/// Closing the returned handle does not release the encoder's claim; that lasts
383/// until [moq_encode_audio_finish].
384#[unsafe(no_mangle)]
385pub extern "C" fn moq_encode_audio_reservation(producer: u32) -> i32 {
386	ffi::enter(move || {
387		let producer = ffi::parse_id(producer)?;
388		let mut state = State::lock();
389		match state.audio.reservation(producer)? {
390			Some(reservation) => Ok(i32::from(state.bandwidth.hold(reservation)?)),
391			None => Ok(0),
392		}
393	})
394}
395
396/// Watch whether the encoded audio track has subscribers, so the microphone and
397/// encoder run only while someone listens. See [`crate::moq_publish_media_demand`]
398/// for the callback contract.
399///
400/// Returns a non-zero watcher handle on success, or a negative code on failure.
401///
402/// # Safety
403/// - `on_demand` must be non-NULL.
404/// - The caller must keep `user_data` valid until the terminal (`<= 0`) `on_demand` callback.
405#[unsafe(no_mangle)]
406pub unsafe extern "C" fn moq_encode_audio_demand(
407	producer: u32,
408	on_demand: crate::moq_status_callback,
409	user_data: *mut c_void,
410) -> i32 {
411	ffi::enter(move || {
412		let producer = ffi::parse_id(producer)?;
413		let on_demand = unsafe { OnStatus::new(user_data, on_demand)? };
414		let mut state = State::lock();
415		let demand = state.audio.demand(producer)?;
416		state.publish.demand(demand, on_demand)
417	})
418}
419
420/// The C ABI spells an unset `u32` knob as 0, which no field here accepts as a
421/// real value.
422fn zeroable(value: u32) -> Option<u32> {
423	(value != 0).then_some(value)
424}
425
426/// Push one audio frame.
427///
428/// `frame->data` is borrowed for the duration of the call; the
429/// producer copies before returning.
430///
431/// # Safety
432/// - `frame` must point to a valid [`moq_audio_frame`].
433/// - `frame->data` must point to `frame->data_size` bytes.
434#[unsafe(no_mangle)]
435pub unsafe extern "C" fn moq_encode_audio_frame(producer: u32, frame: *const moq_audio_frame) -> i32 {
436	ffi::enter(move || {
437		let producer = ffi::parse_id(producer)?;
438		let frame = unsafe { frame.as_ref() }.ok_or(Error::InvalidPointer)?;
439		let data = unsafe { ffi::parse_slice(frame.data, frame.data_size)? };
440
441		// The C ABI carries plain microseconds; scale them at the boundary.
442		let timestamp = moq_net::Timestamp::from_micros(frame.timestamp_us).map_err(moq_audio::Error::from)?;
443		let owned = moq_audio::Frame::new(Bytes::copy_from_slice(data), timestamp);
444
445		let producer = State::lock().audio.producer(producer)?;
446		producer
447			.lock()
448			.as_mut()
449			.ok_or(Error::MediaNotFound)?
450			.producer
451			.write(&owned)?;
452		Ok(())
453	})
454}
455
456/// Flush any pending samples and finalize an audio producer.
457#[unsafe(no_mangle)]
458pub extern "C" fn moq_encode_audio_finish(producer: u32) -> i32 {
459	ffi::enter(move || {
460		let producer = ffi::parse_id(producer)?;
461		// The id is dropped first, so nothing new queues behind the flush; whatever
462		// is mid-encode still finishes before this takes the producer.
463		let producer = State::lock().audio.remove(producer)?;
464		let mut producer = producer.take().ok_or(Error::MediaNotFound)?.producer;
465		producer.finish()?;
466		Ok(())
467	})
468}
469
470/// Subscribe to an audio track and decode it into PCM.
471///
472/// The catalog `index` identifies which audio rendition to subscribe
473/// to, matching the existing `moq_consume_audio` selection
474/// model. TODO: a future API will pick the right rendition
475/// automatically (ABR).
476///
477/// Returns a non-zero handle on success or a negative error code.
478///
479/// `on_frame` is called with a positive frame ID per frame, then exactly once
480/// more with a terminal code: `0` (closed cleanly) or a negative error. After
481/// the terminal (`<= 0`) callback, `on_frame` is never called again and
482/// `user_data` is never touched again, so release `user_data` there. The
483/// terminal callback fires even after [`moq_decode_audio_cancel`].
484///
485/// Starts at the newest cached group so reopening live playback skips the backlog.
486///
487/// A packet the codec cannot decode is logged and skipped rather than ending
488/// the subscription, so a single bad frame costs that frame and not the stream.
489///
490/// # Safety
491/// - `output` must point to a valid [`moq_audio_decoder_output`].
492/// - `user_data` must stay valid until the terminal (`<= 0`) `on_frame` callback.
493#[unsafe(no_mangle)]
494pub unsafe extern "C" fn moq_decode_audio(
495	catalog: u32,
496	index: u32,
497	output: *const moq_audio_decoder_output,
498	on_frame: crate::moq_status_callback,
499	user_data: *mut c_void,
500) -> i32 {
501	ffi::enter(move || {
502		let catalog = ffi::parse_id(catalog)?;
503		let raw = unsafe { output.as_ref() }.ok_or(Error::InvalidPointer)?;
504
505		let mut config = moq_audio::decode::Options::default();
506		config.start = moq_audio::decode::Start::Latest;
507		config.output.format = audio_format_from_u32(raw.format)?;
508		config.output.sample_rate = zeroable(raw.sample_rate);
509		config.output.layout = zeroable(raw.channels)
510			.map(moq_audio::Layout::from_channels)
511			.transpose()?;
512		config.max_age = Duration::from_micros(raw.max_age_us);
513
514		let on_frame = unsafe { OnStatus::new(user_data, on_frame)? };
515
516		let mut state = State::lock();
517		let (broadcast, audio_cfg, name) = state.consume.audio_rendition(catalog, index as usize)?;
518
519		let State { audio, .. } = &mut *state;
520		audio.consume(&broadcast, &audio_cfg, &name, config, on_frame)
521	})
522}
523
524/// Stop an audio (raw PCM) consumer's background task.
525///
526/// Returns immediately: zero on success, or a negative code if already closed.
527/// Does NOT free `user_data`; the on-frame callback still fires once more with a
528/// terminal `0` (or a negative error), which is where `user_data` should be
529/// released. Frame IDs already delivered to the callback are likewise not freed;
530/// release each with [`moq_decode_audio_frame_free`].
531#[unsafe(no_mangle)]
532pub extern "C" fn moq_decode_audio_cancel(consumer: u32) -> i32 {
533	ffi::enter(move || {
534		let consumer = ffi::parse_id(consumer)?;
535		State::lock().audio.consume_close(consumer)
536	})
537}
538
539/// Copy a delivered frame's metadata into `dst`.
540///
541/// The written `dst->data` pointer remains valid until the same `id`
542/// is released with [`moq_decode_audio_frame_free`].
543///
544/// # Safety
545/// - `dst` must point to a writable [`moq_audio_frame`].
546#[unsafe(no_mangle)]
547pub unsafe extern "C" fn moq_decode_audio_frame(id: u32, dst: *mut moq_audio_frame) -> i32 {
548	ffi::enter(move || {
549		let id = ffi::parse_id(id)?;
550		let dst = unsafe { dst.as_mut() }.ok_or(Error::InvalidPointer)?;
551		State::lock().audio.frame_info(id, dst)
552	})
553}
554
555/// Free a frame previously delivered through the consume callback.
556/// Required for every delivered frame ID; closing the parent consumer
557/// is not enough.
558#[unsafe(no_mangle)]
559pub extern "C" fn moq_decode_audio_frame_free(id: u32) -> i32 {
560	ffi::enter(move || {
561		let id = ffi::parse_id(id)?;
562		State::lock().audio.frame_free(id)
563	})
564}