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
163 changes: 100 additions & 63 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ repository.
- [Wayland display and window sources](/quest/m0/wayland-sources.md) - portal-picked sources that reopen without the picker
- [Destinations](/quest/m0/destinations.md) - RTMP, RTMPS, and WHIP push with per-destination codecs and shared encoders, on a UI-free egress library a hosted service can reuse
- [Composable scenes](/quest/m0/scenes/README.md) - scenes nest scenes and the program, any scene publishes at its own size, and mixes become audio scenes
- [Pan each sound](/quest/m0/audio-pan.md) - the Audio panel and `[audio.<source>]` pan each sound wherever it plays
- [Clap-to-measure sync delay](/quest/m0/sync-clap.md) - a clap sets a microphone's delay against its camera
- [Audio filters](/quest/m0/audio-filters.md) - per-source noise suppression, gate, compressor, limiter, and plugins, configured from the studio
- [Browser sources on macOS and Windows](/quest/m0/browser-source.md) - the out-of-process CEF pages Linux has, with shared GPU textures on the other desktops
Expand All @@ -111,6 +112,7 @@ repository.
- [moq-video's browser backends are pinned](/quest/m0/pin-browser-video.md) - the web studio's video slice and browser composition
- [moq-audio's browser backends are pinned](/quest/m0/pin-browser-audio.md) - the web studio's audio slice
- [moq-audio's source tap is pinned](/quest/m0/pin-audio-tap.md) - the samples clap-to-measure sync times a clap from
- [moq-audio's strip pan is pinned](/quest/m0/pin-audio-pan.md) - the bus strip pan the studio's pan sets

## Related

Expand Down
29 changes: 29 additions & 0 deletions quest/m0/audio-pan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [S] Pan each sound

## Goal

Each sound's strip in the Audio panel has a pan beside its fader, saved as
`pan` in `[audio.<source>]` and applied wherever the sound plays, in every
audio scene holding it, as gain, mute, and delay are. `README.md` documents
it.

## Plan

Decided 2026-11-07 with the maintainer, on
[moq-dev/studio#77](https://github.com/moq-dev/studio/pull/77), which landed
the rest of audio scenes:

- ✅ Pan is a property of the source's bus strip, like its gain, set in
moq-audio; the studio only sets it and saves it. Rejected: a pan per audio
scene a sound is sent to.
- ✅ Settled moq-side in
[Pan a bus source](https://github.com/moq-dev/moq/blob/main/quest/m3/audio-strip-pan.md):
a constant-power law, -3 dB at center; a stereo source is balanced, the
far side attenuated and each channel kept where it is; a bus that is not
stereo refuses a pan other than center.
- Changing pan never drops or repeats audio, ramped across a block as gain
is.

## Required

- [moq-audio's strip pan is pinned](/quest/m0/pin-audio-pan.md) - the `Strip` pan the studio sets
20 changes: 20 additions & 0 deletions quest/m0/pin-audio-pan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# [XS] moq-audio's strip pan is pinned

## Goal

The `moq` submodule includes a pan on moq-audio's bus `Strip`, from
[Pan a bus source](https://github.com/moq-dev/moq/blob/main/quest/m3/audio-strip-pan.md).

## Plan

Once the wait clears, advance it with `just upgrade-moq` in its own PR,
fixing whatever the new API breaks here.

## Waiting

- moq-dev/moq: the upstream quest above lands. Check: that quest's PR has
merged on moq's `main`, deleting its file, and `moq_audio::bus::Strip`
there has a pan. A quest file that moved, or went away without its work
landing, does not count. The quest itself is planned in
[moq-dev/moq#8133](https://github.com/moq-dev/moq/pull/8133), not merged
yet.
9 changes: 5 additions & 4 deletions quest/m0/scenes/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Composable scenes
# [M] Composable scenes

## Goal

Expand Down Expand Up @@ -49,6 +49,7 @@ picks a picture scene and an audio scene; a scene no panel shows and no output
reads skips drawing. Per-scene cost grows only with what that scene draws;
`compositor::bench` measures it.

## Required

- [Audio scenes](/quest/m0/scenes/audio.md) - mixes become audio-only scenes that nest
Changed 2026-11-07 with the maintainer, on
[moq-dev/studio#77](https://github.com/moq-dev/studio/pull/77): pan left
this epic for [its own quest](/quest/m0/audio-pan.md), since it waits on
moq-audio, so it no longer holds up this end-to-end test.
25 changes: 0 additions & 25 deletions quest/m0/scenes/audio.md

This file was deleted.

2 changes: 1 addition & 1 deletion quest/m2/publish-independently.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rendition, as every source track does, while the program keeps its
two sizes. It counts as used, so it encodes for as long as it is published
(decided 2026-11-04, maintainer): a source encodes while it is on program
(shown in a scene on air, or, for a microphone or system audio, played by the
mix on program) or while something reads its track, and naming a published
audio scene on program) or while something reads its track, and naming a published
source among those on program (`Publish::set_on_program` in
`rs/moq-scene/src/publish.rs`) keeps it encoding.

Expand Down
46 changes: 15 additions & 31 deletions rs/moq-scene/src/clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ use std::path::Path;
use serde::{Deserialize, Serialize};

use crate::Error;
use crate::collection::{
Audio, Collection, Filter, Item, Layer, Mixes, Scene, Source, Video, validate_filters, versioned,
};
use crate::collection::{Audio, Collection, Filter, Item, Layer, Scene, Source, Video, validate_filters, versioned};
use crate::edit::Draft;

/// What a copy holds: items, or a filter chain.
Expand Down Expand Up @@ -101,13 +99,9 @@ impl Clip {
let clip = Self::deserialize(table).map_err(|err| Error::Format(err.to_string()))?;
match &clip {
// The rules a collection holding just the clip would break, such as
// an item showing a source the clip lacks. It has whatever mixes the
// clip's sources are left out of, and an empty scene for each its
// scene sources draw, which pasting checks.
// an item showing a source the clip lacks. It has an empty scene for
// each its scene sources draw, which pasting checks.
Self::Items(items) => {
let excluded = items.audio.values().flat_map(|audio| &audio.exclude);
let mut names: BTreeSet<String> = excluded.cloned().collect();
names.insert(Mixes::DEFAULT.into());
let drawn: BTreeSet<&str> = items
.sources
.values()
Expand All @@ -124,11 +118,6 @@ impl Clip {
let drawn = drawn.into_iter().map(Scene::new);
Collection {
sources: items.sources.clone(),
mixes: Mixes {
names: names.into_iter().collect(),
program: Mixes::DEFAULT.into(),
recording: None,
},
audio: items.audio.clone(),
video: items.video.clone(),
// In a scene, where keyframes play; pasting into the top
Expand Down Expand Up @@ -172,8 +161,8 @@ impl Draft {
/// one shown, so items pasted within a collection are linked to the ones
/// copied. A source whose name is taken by a different one is added under
/// a new name, which clones and filters naming it follow, and an added
/// source brings its audio and video settings, left out of only the mixes
/// both collections have. An image or LUT under `dir`
/// source brings its audio and video settings, its sound joining the
/// audio scene on program. An image or LUT under `dir`
/// is named relative to it, as the scene file would.
pub fn paste(&mut self, layer: &Layer, items: &Items, dir: &Path) -> Result<(), Error> {
let mut names = BTreeMap::new();
Expand Down Expand Up @@ -225,11 +214,10 @@ impl Draft {
};
for (name, renamed) in added {
if let Some(audio) = items.audio.get(name) {
// Left out only of the mixes this collection has too.
let mut audio = audio.clone();
let mixes = &self.collection().mixes.names;
audio.exclude.retain(|mix| mixes.contains(mix));
self.set_audio(&renamed, Some(audio))?;
self.set_audio(&renamed, Some(audio.clone()))?;
}
if items.sources[name].has_audio() {
self.hear(&renamed)?;
}
if let Some(video) = items.video.get(name) {
let filters = rename(&video.filters);
Expand Down Expand Up @@ -391,12 +379,12 @@ mod tests {
assert_eq!(pasted.scenes[0].items, source.scenes[0].items);
}

/// A pasted source keeps its sound's settings, left out of only the mixes
/// the collection it lands in has.
/// A pasted source keeps its sound's settings, and is heard on program
/// wherever it lands.
#[test]
fn pastes_audio_into_the_mixes_there() {
fn pastes_audio_into_the_audio_scene_on_program() {
let mut source = Collection::new();
source.mixes.names.push("Stream".into());
source.audio_scenes[0].name = "Stream".into();
source.sources.insert(
"guest".into(),
Source::Broadcast {
Expand All @@ -405,7 +393,6 @@ mod tests {
);
let audio = crate::Audio {
gain: 0.5,
exclude: ["Program".to_string(), "Stream".to_string()].into(),
..Default::default()
};
source.audio.insert("guest".into(), audio.clone());
Expand All @@ -418,11 +405,8 @@ mod tests {
.unwrap();

let pasted = paste(&Collection::new(), &Layer::top(), &text, DIR);
let expected = crate::Audio {
exclude: ["Program".to_string()].into(),
..audio
};
assert_eq!(pasted.audio["guest"], expected);
assert_eq!(pasted.audio["guest"], audio);
assert_eq!(pasted.heard(crate::AudioScene::DEFAULT), ["guest"].into());
}

/// A relative image path names the same file wherever the clip is pasted,
Expand Down
Loading