diff --git a/README.md b/README.md index 027757e..4f68ed3 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ Every panel is a tab you can drag, split, or stack. | Guests | Invite a guest, show their join link as a QR code, see who joined, and take a guest out of every scene. | | Filters | Remove the selected source's background, crop the selected source or item, add, order, and set its filters, set an item's blend mode, and copy or paste filters. | | Keyframes | Animate the selected item: add, time, ease, and remove its keyframes, and set how far each moves, resizes, crops, and filters it (see [Keyframes](#keyframes)). **Play** plays the preview scene's keyframes again. | -| Audio | Pick the audio scene on program and the one to listen to and on which output, set each sound's gain, mute, and delay, and send each sound and audio scene into audio scenes, with a meter on every one. | +| Audio | Pick the audio scene on program and the one to listen to and on which output, set each sound's gain, pan, mute, and delay, and send each sound and audio scene into audio scenes, with a meter on every one. | | Graph | See the sources, scenes, and audio scenes as nodes, wired left to right to what draws or holds them, and wire them there (see [Scene graph](#scene-graph)). | | Output | Start and stop recording, pick the picture and the audio scene it records, set the canvas size and frame rate and each scene's size, see the broadcast path, whether the program is encoding, the encoder sessions open, and the encoder work of the program, the recording, and each published scene, and turn the virtual camera of the program or of each published scene on. | | Health | See how the stream reaches viewers, relay by relay and, for the program and each published scene, track by track, and the cause of each problem, and which recordings a crash left unfinished were repaired at launch. | @@ -132,7 +132,7 @@ Drag from a node's right edge onto a scene's, the top layer's, or an audio scene's left edge to wire them: a picture is placed filling the scene, and a sound or audio scene is sent into the audio scene. Click a wire, then **Remove**, to take every item of that source out of the scene, or stop the -send. Each sound's node has its strip's mute, gain, and delay and a waveform +send. Each sound's node has its strip's mute, gain, pan, and delay and a waveform of its last few seconds, each audio scene's node **On program**, **Listen**, and the waveform of its mix, and each scene's node **Publish** and the audio scene it carries. Clicking a scene previews it, and clicking a source picks @@ -305,7 +305,7 @@ once, and an audio scene never holds itself, even through others. Add, rename, reorder, and remove audio scenes in the Scenes panel, where they are tagged `audio` and listed after the scenes. -The Audio panel is a mixer. Each sound has a strip, with its gain, mute, +The Audio panel is a mixer. Each sound has a strip, with its gain, pan, mute, delay, and meter, which apply in every audio scene holding it, and a send to each audio scene; each audio scene has a strip with its meter and a send to each other audio scene. A source added from the Sources or Devices panel, or @@ -618,6 +618,7 @@ loop = true [audio.guest] gain = 0.5 +pan = -0.5 delay = "80ms" [sources.camera-faded] @@ -722,7 +723,8 @@ audio `scenes` it holds. Exactly one is on `program = true`, and `recorded = true` has recordings hold one rather than the one on program. Without `[[audio_scenes]]`, the collection has one audio scene, `Program`, on program and holding nothing. `[audio.]` sets a source's linear `gain`, -`muted`, and `delay`, in every audio scene holding it. +`muted`, `pan` (`-1.0` hard left to `1.0` hard right, centered by +default), and `delay`, in every audio scene holding it. `[video.]` sets a source's `background`, `crop`, and `filters`, wherever it is shown, and an item's `crop`, `filters`, and `blend` its own. diff --git a/quest/m0/README.md b/quest/m0/README.md index d6c63cb..3b9276e 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -92,7 +92,6 @@ repository. - [Studio on Windows](/quest/m0/app-windows.md) - the studio runs on Windows, not only Linux - [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 -- [Pan each sound](/quest/m0/audio-pan.md) - the Audio panel and `[audio.]` 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 diff --git a/quest/m0/audio-pan.md b/quest/m0/audio-pan.md deleted file mode 100644 index 507e6e1..0000000 --- a/quest/m0/audio-pan.md +++ /dev/null @@ -1,27 +0,0 @@ -# [S] Pan each sound - -## Goal - -Each sound's strip in the Audio panel, and its node in the Graph panel, has -a pan beside its fader, saved as `pan` in `[audio.]` 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/pull/8180): - 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. -- Both panels draw a sound's mute, gain, and delay with `strip_controls` in - `rs/moq-studio/src/ui.rs`; a pan added there shows in both. diff --git a/rs/moq-scene/src/collection.rs b/rs/moq-scene/src/collection.rs index 62e37d6..0a060aa 100644 --- a/rs/moq-scene/src/collection.rs +++ b/rs/moq-scene/src/collection.rs @@ -514,6 +514,12 @@ impl Collection { if !audio.gain.is_finite() || audio.gain < 0.0 { return Err(Error::Invalid(format!("`{name}` has gain {}", audio.gain))); } + if !(-1.0..=1.0).contains(&audio.pan) { + return Err(Error::Invalid(format!( + "`{name}` has pan {}, outside -1 to 1", + audio.pan + ))); + } if audio.delay > moq_audio::bus::MAX_DELAY { return Err(Error::Invalid(format!( "`{name}` is held back {:?}, past the longest, {:?}", @@ -1897,7 +1903,7 @@ impl AudioScene { } } -/// The gain, mute, and sync delay of one source with sound, in every audio +/// The gain, mute, pan, and sync delay of one source with sound, in every audio /// scene that holds it. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -1907,6 +1913,10 @@ pub struct Audio { /// Whether the source is silenced in every audio scene, keeping its gain. #[serde(default)] pub muted: bool, + /// Where the source sits between left (`-1.0`) and right (`1.0`), as + /// [`Strip::set_pan`](moq_audio::bus::Strip::set_pan) places it. + #[serde(default, skip_serializing_if = "is_center")] + pub pan: f32, /// How long the source is held back in every audio scene, so a microphone lines up /// with a camera that lags it. At most the bus's /// [`MAX_DELAY`](moq_audio::bus::MAX_DELAY). @@ -1919,11 +1929,16 @@ impl Default for Audio { Self { gain: 1.0, muted: false, + pan: 0.0, delay: Duration::ZERO, } } } +fn is_center(pan: &f32) -> bool { + *pan == 0.0 +} + /// An sRGB color with straight alpha, written `#rrggbb` or `#rrggbbaa`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, SerializeDisplay, DeserializeFromStr)] pub struct Rgba(pub [u8; 4]); @@ -2034,6 +2049,7 @@ mod tests { Audio { gain: 0.5, muted: true, + pan: 0.25, delay: Duration::from_millis(80), }, ); @@ -2203,6 +2219,7 @@ rect = { x = 0, y = 680, width = 1280, height = 40 } Audio { gain: 2.0, muted: false, + pan: 0.0, delay: Duration::from_millis(120), } ); diff --git a/rs/moq-scene/src/edit.rs b/rs/moq-scene/src/edit.rs index 333224b..7e134f7 100644 --- a/rs/moq-scene/src/edit.rs +++ b/rs/moq-scene/src/edit.rs @@ -533,7 +533,7 @@ impl Draft { }) } - /// Set or clear the gain, mute, and delay of the source called `name`. + /// Set or clear the gain, mute, pan, and delay of the source called `name`. pub fn set_audio(&mut self, name: &str, audio: Option