mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 02:26:46 -06:00
Merge pull request #719 from reoniazx/fix/mono-device-fold-down
Fix mono device playback dropping right-channel audio
This commit is contained in:
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~5% and ~110–180 h away (the alpha gate's three failing workflows)
|
||||
|
||||
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** trivial (master fader automation block boundaries) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** trivial (mono device playback fold-down) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with
|
||||
Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This page is
|
||||
@@ -122,6 +122,7 @@ mutated and truncated movies. Field reports from the first public week are track
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-18:** Mono audio devices receive the average of the stereo channels before output clipping, retaining right-only audio. Surround mixes use the same average after their ITU stereo fold-down. Regression tests cover channel balance, polarity cancellation, clipping, centered mono tracks, each 5.1 channel and several callback sizes. No readiness percentage change.
|
||||
- **2026-10-16:** Master fader volume automation uses each render block's actual last sample, matching track faders. Breakpoints in the next block no longer pull the preceding block's level toward them, and ramps reach their last-sample value. Regression tests cover nonzero starts, one-sample callbacks and several block sizes. No readiness percentage change.
|
||||
- **2026-10-16:** Aux, folder and master latency calculations exclude inserts already taken out for producing NaN or infinity, matching audio tracks. Subsequent blocks no longer compensate for an insert that is bypassed after failure. Regression tests check impulse timing, healthy insert latency and reset/retry at 64- and 512-sample blocks. No readiness percentage change.
|
||||
- **2026-10-14:** MCP `batch` rejects more than 1000 calls before executing any command, instead of silently running only the first 1000 (#460). No readiness percentage change.
|
||||
@@ -160,6 +161,7 @@ mutated and truncated movies. Field reports from the first public week are track
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | Mono device playback retains both stereo sides and surround fold-down channels; readiness estimates unchanged |
|
||||
| 2026-10-16 | trivial | Master volume automation block boundaries corrected; readiness estimates unchanged |
|
||||
| 2026-10-16 | trivial | Failed inserts no longer contribute aux, folder or master latency after containment; readiness estimates unchanged |
|
||||
| 2026-10-14 | trivial | Progress log: oversized MCP batches rejected before execution (#460) |
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
//!
|
||||
//! Surround: the device is opened with as many output channels as it offers, up to the width of
|
||||
//! the session's main format. Main channels (SMPTE/WAV order) go to device channels in order; when
|
||||
//! the device has fewer channels than the main mix, the mix is folded down to stereo (ITU-R
|
||||
//! BS.775) on the first two device channels.
|
||||
//! the device has fewer channels than the main mix, surround is folded down to stereo (ITU-R
|
||||
//! BS.775). A mono device receives the average of the stereo channels, before output clipping.
|
||||
//!
|
||||
//! Plugins and threads: hosted third-party plugins (CLAP/VST3) are created, given their stored
|
||||
//! state and prepared on the thread that calls [`Player::update_session`] (the UI thread), and
|
||||
@@ -365,18 +365,26 @@ impl AudioState {
|
||||
let b = ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a);
|
||||
a + (b - a) * fr
|
||||
};
|
||||
if nch > 2 && channels < nch {
|
||||
// Fewer device channels than the main mix: ITU fold-down to stereo.
|
||||
if channels < nch {
|
||||
// Fold surround to stereo first; a mono device averages the two sides.
|
||||
let (mut l, mut r) = (0.0f32, 0.0f32);
|
||||
if sounding {
|
||||
for (c, (gl, gr)) in self.fold.iter().enumerate().take(nch) {
|
||||
let v = sample(c);
|
||||
l += v * gl;
|
||||
r += v * gr;
|
||||
if nch == 2 {
|
||||
(l, r) = (sample(0), sample(1));
|
||||
} else {
|
||||
for (c, (gl, gr)) in self.fold.iter().enumerate().take(nch) {
|
||||
let v = sample(c);
|
||||
l += v * gl;
|
||||
r += v * gr;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
*o = match c {
|
||||
0 if channels == 1 => {
|
||||
let mono = (0.5 * l + 0.5 * r).clamp(-1.0, 1.0);
|
||||
if mono.is_finite() { mono } else { 0.0 }
|
||||
}
|
||||
0 => l.clamp(-1.0, 1.0),
|
||||
1 => r.clamp(-1.0, 1.0),
|
||||
_ => 0.0,
|
||||
@@ -384,7 +392,6 @@ impl AudioState {
|
||||
}
|
||||
} else {
|
||||
for (c, o) in frame.iter_mut().enumerate() {
|
||||
// Stereo mixes on a mono device play the left channel (as before).
|
||||
*o = if c < nch.max(1) && sounding { sample(c).clamp(-1.0, 1.0) } else { 0.0 };
|
||||
}
|
||||
}
|
||||
@@ -1163,6 +1170,80 @@ mod tests {
|
||||
assert_eq!(shared.position.load(Ordering::Relaxed), 64);
|
||||
}
|
||||
|
||||
fn audio_output_state(format: soundcraft_model::ChannelFormat, channels: Vec<Vec<f32>>) -> AudioState {
|
||||
use soundcraft_model::{Clip, SourceAudio, SourceId, TrackKind};
|
||||
let mut s = Session::default();
|
||||
s.edit.click = false;
|
||||
s.outputs[0].format = format;
|
||||
let t = s.add_track(TrackKind::Audio, format, None);
|
||||
let frames = channels[0].len();
|
||||
s.pool.insert(SourceId(5), Arc::new(SourceAudio::new(soundcraft_audio_io::AudioBuffer { sample_rate: 48_000, channels })));
|
||||
let id = s.new_clip_id();
|
||||
s.track_mut(t).unwrap().playlist_mut().unwrap().clips.push(Clip::audio(id, "output", SourceId(5), 0, 0, frames as i64));
|
||||
let shared = Arc::new(Shared {
|
||||
position: AtomicI64::new(0),
|
||||
playing: AtomicBool::new(false),
|
||||
speed: AtomicU32::new(1000),
|
||||
meters: Mutex::new(MeterSnapshot::default()),
|
||||
});
|
||||
let (tx, rx) = channel();
|
||||
let st = AudioState::new(rx, shared, Arc::new(s), 48_000.0, channel().0);
|
||||
tx.send(Cmd::Play { from: 0, end: None, looped: None }).unwrap();
|
||||
st
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mono_device_averages_stereo_before_clipping() {
|
||||
use soundcraft_model::ChannelFormat;
|
||||
// Right only, left only, identical, opposite polarity, and levels that must be averaged
|
||||
// before clipping either side to the device's range.
|
||||
let left = [0.0, 0.8, 0.8, 0.8, 2.0, -2.0, 2.0];
|
||||
let right = [0.8, 0.0, 0.8, -0.8, -0.5, 0.5, 2.0];
|
||||
let expected = [0.4, 0.4, 0.8, 0.0, 0.75, -0.75, 1.0];
|
||||
for callback in [1, 37, 512] {
|
||||
let channels = [left, right].map(|pattern| (0..2048).map(|i| pattern[i % pattern.len()]).collect());
|
||||
let mut st = audio_output_state(ChannelFormat::Stereo, channels.into());
|
||||
let mut out = vec![0.0; callback];
|
||||
for block in 0..3 {
|
||||
st.fill(&mut out, 1);
|
||||
for (i, actual) in out.iter().enumerate() {
|
||||
let at = block * callback + i;
|
||||
assert!((actual - expected[at % expected.len()]).abs() < 1e-6, "callback {callback}, sample {at}: {actual}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mono_device_preserves_centered_mono_level_and_includes_surround_right_channels() {
|
||||
use soundcraft_model::ChannelFormat;
|
||||
let h = std::f32::consts::FRAC_1_SQRT_2;
|
||||
let mut mono = audio_output_state(ChannelFormat::Mono, vec![vec![0.8; 512]]);
|
||||
let mut out = [0.0; 128];
|
||||
mono.fill(&mut out, 1);
|
||||
let centered = 0.8 * soundcraft_dsp::db_to_gain(-3.0);
|
||||
assert!(out.iter().all(|x| (*x - centered).abs() < 1e-6), "a centered mono track keeps its rendered level under the -3 dB pan law");
|
||||
|
||||
// Independently exercise each SMPTE 5.1 channel: L, R, C, LFE, Ls, Rs.
|
||||
let channels = (0..6).map(|c| (0..512).map(|i| if i % 6 == c { 0.8 } else { 0.0 }).collect()).collect();
|
||||
let mut surround = audio_output_state(ChannelFormat::Surround51, channels);
|
||||
surround.fill(&mut out, 1);
|
||||
let expected = [0.4, 0.4, 0.8 * h, 0.0, 0.4 * h, 0.4 * h];
|
||||
for (i, actual) in out.iter().enumerate() {
|
||||
assert!((actual - expected[i % expected.len()]).abs() < 1e-6, "surround sample {i}: {actual}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mono_device_contains_overflow_from_opposite_surround_sides() {
|
||||
use soundcraft_model::ChannelFormat;
|
||||
let channels = [f32::MAX, -f32::MAX, 0.0, 0.0, f32::MAX, -f32::MAX].map(|v| vec![v; 128]);
|
||||
let mut st = audio_output_state(ChannelFormat::Surround51, channels.into());
|
||||
let mut out = [0.5; 128];
|
||||
st.fill(&mut out, 1);
|
||||
assert!(out.iter().all(|x| *x == 0.0), "overflow in the stereo fold-down must not send NaN to a mono device");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surround_main_maps_to_device_channels_or_folds_down() {
|
||||
use soundcraft_model::{ChannelFormat, Clip, SourceAudio, SourceId, TrackKind};
|
||||
|
||||
+7
-1
@@ -1,6 +1,6 @@
|
||||
# Where SoundCraft falls short of Pro Tools
|
||||
|
||||
> **Last reviewed:** 2026-10-16 · **Last updated:** 2026-10-16 · **Change:** trivial (G-04: master fader automation block boundaries) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** trivial (G-04: mono device playback fold-down) · **Target:** Avid Pro Tools Ultimate 2026.4.1
|
||||
|
||||
Every known shortfall, one entry each, ranked by how much it stops a Pro Tools user from doing
|
||||
real work in SoundCraft. This is the work list: pick from the top. Rows marked **alpha blocker**
|
||||
@@ -80,6 +80,7 @@ When you close a gap, delete its entry (or shrink it) in the same PR, and bump t
|
||||
|
||||
### G-04. Correctness bugs in "done" features
|
||||
|
||||
**Fixed (2026-10-18):** Mono device playback averages the stereo channels before output clipping,
|
||||
**Fixed (2026-10-16):** Master fader volume automation uses the first and last samples of each
|
||||
**Fixed (2026-10-16):** Aux, folder and master latency calculations now skip inserts already
|
||||
render block, matching track faders. A breakpoint at the next block no longer ramps the preceding
|
||||
@@ -90,6 +91,10 @@ disabled for producing NaN or infinity. Subsequent blocks compensate only for he
|
||||
regression tests verify rendered impulse timing, retained healthy latency and reset/retry at
|
||||
64- and 512-sample blocks, with audio tracks as the control.
|
||||
|
||||
instead of discarding the right side. Surround mixes average both sides of the ITU stereo
|
||||
fold-down. Regression tests cover right-only and left-only audio, equal and opposite-polarity
|
||||
channels, clipping order, centered mono tracks, each 5.1 channel and 1-, 37- and 512-frame callbacks.
|
||||
|
||||
Open on 2026-10-10 (several have PRs open):
|
||||
editing #3, #5, #140, #144; clip/session #86 (PR #123), #88 (PR #130), #91 (PR #124), #92 (PR #132);
|
||||
automation #104 (PR #134); mix/bounce #59 (PR #131), #103 (PR #133), #152; playback #101 (PR #129);
|
||||
@@ -209,6 +214,7 @@ missing. Out of scope unless the owner decides otherwise.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-18 | trivial | G-04: mono device stereo/surround fold-down fixed and regression-tested; estimates unchanged |
|
||||
| 2026-10-16 | trivial | G-04: master volume automation block boundaries fixed and regression-tested; estimates unchanged |
|
||||
| 2026-10-16 | trivial | G-04: failed aux/folder/master inserts excluded from latency compensation after containment; estimates unchanged |
|
||||
| 2026-10-12 | minor | G-01 shrunk: audio device, rate and buffer choice; opt-in ASIO |
|
||||
|
||||
Reference in New Issue
Block a user