Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions quest/m0/pin-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ stall does on it.
## Plan

Decided 2026-11-09 (maintainer, /quest-audit): ✅ crossing #8272 is this
quest, after
[Playout writes land on positioned bus channels](/quest/m0/playout-positioned.md)
(rejected: crossing it in the
positioned bus pin). Both rewrite
quest, after playout's writes moved onto positioned bus channels
([moq-dev/studio#137](https://github.com/moq-dev/studio/pull/137))
(rejected: crossing it in the positioned bus pin). Both rewrite
`playout.rs`, so the positioned work finishes on the API it was planned
against, then this crosses.

Expand Down Expand Up @@ -81,7 +80,3 @@ kio's `Instant` loses `+` and `-` with a `Duration`, which `playout.rs` and

Check: the maintainer answers in `/quest-unblock`, and the choice is
recorded here.

## Required

- [Playout writes land on positioned bus channels](/quest/m0/playout-positioned.md) - the `playout.rs` rewrite this crosses on top of
72 changes: 0 additions & 72 deletions quest/m0/playout-positioned.md

This file was deleted.

54 changes: 38 additions & 16 deletions rs/moq-scene/src/compositor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2195,7 +2195,7 @@ mod tests {

use super::*;
use crate::collection::{Blend, MaskMode, Rgba, Scene, Size, Video};
use crate::source::test::{context, listen, local, start, until, until_time};
use crate::source::test::{context, local, start, until, until_time};

/// A fade over a second.
const FADE: Transition = Transition::Fade {
Expand Down Expand Up @@ -2256,15 +2256,29 @@ mod tests {
collection
}

/// A compositor over running sources, read back to check pixels.
/// Mix the bus sample-exact on the clock from `start`, as the program's
/// audio clock does.
async fn mix(mut mixer: crate::source::Mixer, start: Instant) {
let rate = u128::from(moq_audio::bus::Config::default().sample_rate);
let mut clock = kio::clock::interval_at(start, Duration::from_millis(10));
clock.set_missed_tick_behavior(kio::clock::MissedTickBehavior::Skip);
let mut mixed = 0;
loop {
let tick = clock.tick().await;
let due = tick.saturating_duration_since(start).as_nanos() * rate / 1_000_000_000;
mixer.advance((due - mixed) as usize);
mixed = due;
}
}

/// A compositor over running sources, read back to check pixels, whose
/// sound mixes along with the clock.
pub(super) struct Studio {
pub(super) collection: Collection,
pub(super) sources: Sources,
pub(super) compositor: Compositor,
pub(super) device: wgpu::Device,
queue: wgpu::Queue,
/// Mixes the sources' sound, only as a test advances it.
mixer: moq_audio::bus::Driver,
_origin: moq_net::origin::Producer,
_dir: tempfile::TempDir,
}
Expand All @@ -2279,8 +2293,12 @@ mod tests {
let (device, queue) = gpu().await;
let (mut context, origin) = context(dir.path());
context.output = moq_video::render::output(&device);
let (bus, mixer) = moq_audio::bus::Bus::new(Default::default()).unwrap();
let (bus, driver) = moq_audio::bus::Bus::new(Default::default()).unwrap();
let begin = Instant::now();
let (mixer, mixed) = crate::source::Mixer::new(driver, begin);
context.bus = bus;
context.mixed = mixed;
tokio::task::spawn_local(mix(mixer, begin));
let mut sources = Sources::new(context);
let drivers = sources.sync(&collection);
start(&sources, drivers).await;
Expand All @@ -2290,12 +2308,22 @@ mod tests {
queue,
collection,
sources,
mixer,
_origin: origin,
_dir: dir,
}
}

/// The loudest `mix` gets from now until `until`.
async fn hear(mix: &moq_audio::bus::Mix, until: Instant) -> f32 {
mix.level();
let mut loudest = 0.0f32;
while Instant::now() < until {
kio::clock::sleep(Duration::from_millis(10)).await;
loudest = loudest.max(mix.level().peak());
}
loudest
}

pub(super) fn render(&mut self, now: Instant) {
self.compositor.render(&self.collection, &self.sources, now).unwrap();
}
Expand Down Expand Up @@ -3219,17 +3247,14 @@ mod tests {
local(async {
let mut studio = stinging(crate::media::tests::CLIP).await;
let program = studio.sources.mix(crate::AudioScene::DEFAULT).unwrap().clone();
let mut at = Instant::now();
studio.mixer.advance(480);
let transition = studio.collection.transition.clone();
studio
.compositor
.transition(&studio.collection, transition, &studio.sources, Instant::now())
.unwrap();
loop {
studio.render(Instant::now());
let until = at + Duration::from_millis(10);
if listen(&mut studio.mixer, &program, &mut at, until).await > 0.0 {
if Studio::hear(&program, Instant::now() + Duration::from_millis(10)).await > 0.0 {
break;
}
}
Expand All @@ -3239,8 +3264,7 @@ mod tests {
while feed.status() != Status::Ended {
kio::clock::sleep(Duration::from_millis(5)).await;
}
let until = at + Duration::from_millis(500);
let heard = listen(&mut studio.mixer, &program, &mut at, until).await;
let heard = Studio::hear(&program, Instant::now() + Duration::from_millis(500)).await;
assert_eq!(heard, 0.0, "the clip's sound stops with the cut");

// Stopped from the Sources panel, the stinger ends with it.
Expand Down Expand Up @@ -3274,16 +3298,14 @@ mod tests {
.unwrap();
while studio.compositor.transitioning() {
studio.render(Instant::now());
let until = at + Duration::from_millis(10);
listen(&mut studio.mixer, &program, &mut at, until).await;
Studio::hear(&program, Instant::now() + Duration::from_millis(10)).await;
}
let feed = studio.sources.get("sting").unwrap();
assert!(feed.run().ended.is_some(), "after its first pass");
while feed.status() != Status::Ended {
kio::clock::sleep(Duration::from_millis(5)).await;
}
let until = at + Duration::from_millis(500);
let heard = listen(&mut studio.mixer, &program, &mut at, until).await;
let heard = Studio::hear(&program, Instant::now() + Duration::from_millis(500)).await;
assert_eq!(heard, 0.0, "the loop stops with the stinger");
})
.await;
Expand Down
Loading