Compare commits

2 Commits
9 changed files with 241 additions and 60 deletions
+8 -1
View File
@@ -317,7 +317,14 @@ impl<W: Write + Seek> FileEncoder<W> {
pub fn new(out: W, opts: &EncodeOptions, channels: usize, sample_rate: u32, mask: Option<u32>) -> Result<Self> {
let (depth, dither) = (opts.bit_depth, opts.dither);
Ok(FileEncoder(match opts.format {
FileFormat::Wav => Writer::Wav(wav::Writer::new(out, channels, sample_rate, depth, dither, opts.bwf.as_ref(), mask, false)?),
FileFormat::Wav => Writer::Wav(wav::Writer::new(
out,
channels,
sample_rate,
depth,
dither,
wav::HeaderExtras { bwf: opts.bwf.as_ref(), mask, force_rf64: false },
)?),
FileFormat::Aiff => Writer::Aiff(aiff::Writer::new(out, channels, sample_rate, depth, dither)?),
FileFormat::Flac => Writer::Flac(flac::Writer::new(out, channels, sample_rate, depth, dither)?),
other => return Err(AudioError::Unsupported(format!("encoding {other:?} is not supported"))),
+11 -11
View File
@@ -355,17 +355,17 @@ pub(crate) struct Writer<W> {
failed: bool,
}
/// The optional header parts of a WAV file: BWF metadata, speaker mask, and whether to force RF64.
#[derive(Clone, Copy)]
pub(crate) struct HeaderExtras<'a> {
pub(crate) bwf: Option<&'a BwfInfo>,
pub(crate) mask: Option<u32>,
pub(crate) force_rf64: bool,
}
impl<W: Write + Seek> Writer<W> {
pub(crate) fn new(
mut out: W,
channels: usize,
sample_rate: u32,
depth: BitDepth,
dither: bool,
bwf: Option<&BwfInfo>,
mask: Option<u32>,
force_rf64: bool,
) -> Result<Self> {
pub(crate) fn new(mut out: W, channels: usize, sample_rate: u32, depth: BitDepth, dither: bool, extras: HeaderExtras<'_>) -> Result<Self> {
let HeaderExtras { bwf, mask, force_rf64 } = extras;
validate_layout(channels, sample_rate)?;
let format = sample_format(depth);
let start = out.stream_position()?;
@@ -449,7 +449,7 @@ pub(crate) fn encode(
let mut out = Vec::new();
out.try_reserve_exact(head.len().saturating_add(data_len).saturating_add(1))
.map_err(|_| AudioError::TooLarge(format!("cannot allocate {data_len} bytes for WAV output")))?;
let mut w = Writer::new(Cursor::new(out), channels, buf.sample_rate, depth, dither, bwf, mask, force_rf64)?;
let mut w = Writer::new(Cursor::new(out), channels, buf.sample_rate, depth, dither, HeaderExtras { bwf, mask, force_rf64 })?;
w.write(&buf.channels, frames)?;
Ok(w.finish()?.into_inner())
}
+171 -24
View File
@@ -1656,7 +1656,14 @@ fn draw_live_take(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &
let (body, _, wave) = clip_colors([t.rec.r(), t.rec.g(), t.rec.b()], false);
painter.rect(r, CornerRadius::same(2), body, Stroke::new(1.0, Color32::from_rgb(10, 10, 10)), StrokeKind::Inside);
if track.view != "blocks" {
crate::waveform::draw_live(&painter.with_clip_rect(r.intersect(lane)), s, tl, origin as f64, block_len, &take, r, wave);
crate::waveform::draw_live(
crate::waveform::Canvas { painter: &painter.with_clip_rect(r.intersect(lane)), s, tl },
origin as f64,
block_len,
&take,
r,
wave,
);
}
}
@@ -1722,7 +1729,14 @@ fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track
preview = Clip { fade_in, fade_out, ..clip.clone() };
&preview
};
crate::waveform::draw(&painter.with_clip_rect(vis), s, tl, shown, audio, track.channels(), body_r, wave);
crate::waveform::draw(
crate::waveform::Canvas { painter: &painter.with_clip_rect(vis), s, tl },
shown,
audio,
track.channels(),
body_r,
wave,
);
let n = track.channels().min(audio.peaks.len()).max(1);
for k in 1..n {
let y = body_r.min.y + body_r.height() / n as f32 * k as f32;
@@ -1776,7 +1790,7 @@ fn draw_clip(app: &SoundApp, painter: &egui::Painter, s: &Session, track: &Track
} else {
clip_colors(prev.color.unwrap_or(track.color), false).2.gamma_multiply(0.6)
};
crate::waveform::draw(&painter.with_clip_rect(xf), s, tl, prev, audio, track.channels(), body_r, tail);
crate::waveform::draw(crate::waveform::Canvas { painter: &painter.with_clip_rect(xf), s, tl }, prev, audio, track.channels(), body_r, tail);
}
// Clip gain line.
let show_gain = s.edit.flag("view.clip.gain_line") && (!clip.gain_env.is_empty() || clip.gain_db.abs() > 0.01 || track.view == "clip_gain");
@@ -2732,6 +2746,18 @@ fn clip_context_menu(app: &mut SoundApp, ui: &mut Ui, clip: Option<ClipId>) {
}
}
/// One frame's inputs to [`follow_update`].
struct FollowFrame {
playing: bool,
mode_follow: bool,
playhead_visible: bool,
cur: Samples,
last_scroll: Samples,
last_follow_to: Option<Samples>,
hold: bool,
pos: Samples,
}
/// Next follow state after observing one frame: `(hold, follow_to)`.
///
/// The view is ours to move only while nothing else moved it: when the
@@ -2741,16 +2767,8 @@ fn clip_context_menu(app: &mut SoundApp, ui: &mut Ui, clip: Option<ClipId>) {
/// jump is issued. Stopping, or scrolling back to the playhead, clears the
/// hold. Only `page`/`continuous`/`center` ever issue follow jumps while
/// playing; `none`/`after_playback` leave the view alone.
fn follow_update(
playing: bool,
mode_follow: bool,
playhead_visible: bool,
cur: Samples,
last_scroll: Samples,
last_follow_to: Option<Samples>,
hold: bool,
pos: Samples,
) -> (bool, Option<Samples>) {
fn follow_update(frame: FollowFrame) -> (bool, Option<Samples>) {
let FollowFrame { playing, mode_follow, playhead_visible, cur, last_scroll, last_follow_to, hold, pos } = frame;
if !playing || playhead_visible {
return (false, None);
}
@@ -2828,7 +2846,16 @@ fn overlay(app: &mut SoundApp, ui: &mut Ui, tl: Rect, area: Rect) {
let mode_follow = matches!(s.edit.scrolling.as_str(), "page" | "continuous" | "center");
let visible = x >= tl.min.x && x <= tl.max.x;
let layout = &mut app.edit_layout;
let (hold, follow_to) = follow_update(true, mode_follow, visible, cur, layout.last_scroll, layout.last_follow_to, layout.follow_hold, pos);
let (hold, follow_to) = follow_update(FollowFrame {
playing: true,
mode_follow,
playhead_visible: visible,
cur,
last_scroll: layout.last_scroll,
last_follow_to: layout.last_follow_to,
hold: layout.follow_hold,
pos,
});
layout.follow_hold = hold;
layout.last_scroll = cur;
layout.last_follow_to = follow_to;
@@ -2955,40 +2982,160 @@ mod tests {
#[test]
fn follow_jumps_to_offscreen_playhead_while_playing() {
// Steady view, follow mode, playhead left the screen: jump to it.
assert_eq!(follow_update(true, true, false, 1_000, 1_000, None, false, 96_000), (false, Some(96_000)));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: false,
cur: 1_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(false, Some(96_000))
);
}
#[test]
fn follow_leaves_onscreen_playhead_alone() {
assert_eq!(follow_update(true, true, true, 1_000, 1_000, None, false, 96_000), (false, None));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: true,
cur: 1_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(false, None)
);
}
#[test]
fn follow_never_scrolls_when_stopped_nor_in_free_modes() {
assert_eq!(follow_update(false, true, false, 1_000, 1_000, None, false, 96_000), (false, None));
assert_eq!(
follow_update(FollowFrame {
playing: false,
mode_follow: true,
playhead_visible: false,
cur: 1_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(false, None)
);
// `none`/`after_playback` never follow, even with a steady view.
assert_eq!(follow_update(true, false, false, 1_000, 1_000, None, false, 96_000), (false, None));
assert_eq!(follow_update(true, false, false, 9_000, 1_000, None, false, 96_000), (false, None));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: false,
playhead_visible: false,
cur: 1_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(false, None)
);
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: false,
playhead_visible: false,
cur: 9_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(false, None)
);
}
#[test]
fn outside_view_move_pauses_the_follow() {
// The view moved and it was not our own jump: pause, no jump.
assert_eq!(follow_update(true, true, false, 9_000, 1_000, None, false, 96_000), (true, None));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: false,
cur: 9_000,
last_scroll: 1_000,
last_follow_to: None,
hold: false,
pos: 96_000
}),
(true, None)
);
// A held view stays held while it sits still elsewhere.
assert_eq!(follow_update(true, true, false, 9_000, 9_000, None, true, 96_000), (true, None));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: false,
cur: 9_000,
last_scroll: 9_000,
last_follow_to: None,
hold: true,
pos: 96_000
}),
(true, None)
);
}
#[test]
fn own_follow_jump_is_not_mistaken_for_a_takeover() {
// The scroll equals the target we issued last frame: keep following.
assert_eq!(follow_update(true, true, false, 96_000, 1_000, Some(96_000), false, 97_000), (false, Some(97_000)));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: false,
cur: 96_000,
last_scroll: 1_000,
last_follow_to: Some(96_000),
hold: false,
pos: 97_000
}),
(false, Some(97_000))
);
}
#[test]
fn hold_clears_when_playhead_returns_or_transport_stops() {
assert_eq!(follow_update(true, true, true, 9_000, 9_000, None, true, 96_000), (false, None));
assert_eq!(follow_update(false, true, false, 9_000, 9_000, None, true, 96_000), (false, None));
assert_eq!(
follow_update(FollowFrame {
playing: true,
mode_follow: true,
playhead_visible: true,
cur: 9_000,
last_scroll: 9_000,
last_follow_to: None,
hold: true,
pos: 96_000
}),
(false, None)
);
assert_eq!(
follow_update(FollowFrame {
playing: false,
mode_follow: true,
playhead_visible: false,
cur: 9_000,
last_scroll: 9_000,
last_follow_to: None,
hold: true,
pos: 96_000
}),
(false, None)
);
}
fn harness(n: usize) -> Harness<'static, SoundApp> {
+14 -4
View File
@@ -4,10 +4,18 @@ use egui::{Color32, Mesh, Painter, Rect, Shape, Stroke, pos2, vec2};
use soundcraft_model::{Clip, Session, SourceAudio};
use soundcraft_time::Samples;
/// Where to draw: the painter, the session and the timeline rect that maps samples to x.
#[derive(Clone, Copy)]
pub struct Canvas<'a> {
pub painter: &'a Painter,
pub s: &'a Session,
pub tl: Rect,
}
/// Draw a clip's audio across `body`, one band per channel, `channels` of them at most.
pub fn draw(painter: &Painter, s: &Session, tl: Rect, clip: &Clip, audio: &SourceAudio, channels: usize, body: Rect, wave: Color32) {
pub fn draw(canvas: Canvas<'_>, clip: &Clip, audio: &SourceAudio, channels: usize, body: Rect, wave: Color32) {
for (ch, band) in bands(body, channels.min(audio.peaks.len()).max(1)).enumerate() {
channel(painter, s, tl, clip, audio, ch, band, wave);
channel(canvas, clip, audio, ch, band, wave);
}
}
@@ -16,7 +24,8 @@ fn bands(body: Rect, n: usize) -> impl Iterator<Item = Rect> {
(0..n).map(move |k| Rect::from_min_size(pos2(body.min.x, body.min.y + h * k as f32), vec2(body.width(), h)))
}
fn channel(painter: &Painter, s: &Session, tl: Rect, clip: &Clip, audio: &SourceAudio, ch: usize, band: Rect, wave: Color32) {
fn channel(canvas: Canvas<'_>, clip: &Clip, audio: &SourceAudio, ch: usize, band: Rect, wave: Color32) {
let Canvas { painter, s, tl } = canvas;
let (Some(samples), Some(peaks)) = (audio.channel(ch), audio.peaks.get(ch)) else { return };
let Some(g) = Grid::new(painter, s, tl, band) else { return };
let (rectified, power, outlines) = (s.edit.flag("waveform.rectified"), s.edit.flag("waveform.power"), s.edit.flag("waveform.outlines"));
@@ -59,7 +68,8 @@ fn channel(painter: &Painter, s: &Session, tl: Rect, clip: &Clip, audio: &Source
/// Draw a take while it records across `body`, one band per channel: `blocks` holds each channel's
/// (min, max) per block of input frames, the blocks `block_len` timeline samples long from
/// timeline position `origin`.
pub fn draw_live(painter: &Painter, s: &Session, tl: Rect, origin: f64, block_len: f64, blocks: &[Vec<(f32, f32)>], body: Rect, wave: Color32) {
pub fn draw_live(canvas: Canvas<'_>, origin: f64, block_len: f64, blocks: &[Vec<(f32, f32)>], body: Rect, wave: Color32) {
let Canvas { painter, s, tl } = canvas;
let zoom = s.edit.zoom.waveform_zoom;
for (blocks, band) in blocks.iter().zip(bands(body, blocks.len())) {
let Some(g) = Grid::new(painter, s, tl, band) else { continue };
+2 -2
View File
@@ -22,13 +22,13 @@ fn paint(samples_per_px: f64, f: impl Fn(&egui::Painter, &Session, Rect)) -> Vec
fn draw(samples: Vec<f32>, samples_per_px: f64) -> Vec<Shape> {
let clip = Clip::audio(ClipId(1), "c", SourceId(1), 0, 0, samples.len() as i64);
let audio = SourceAudio::new(AudioBuffer { sample_rate: 48_000, channels: vec![samples] });
paint(samples_per_px, |p, s, band| waveform::draw(p, s, band, &clip, &audio, 1, band, WAVE))
paint(samples_per_px, |p, s, band| waveform::draw(waveform::Canvas { painter: p, s, tl: band }, &clip, &audio, 1, band, WAVE))
}
#[test]
fn a_take_draws_its_overview_while_it_records() {
let blocks = vec![vec![(-0.5, 0.5); 100]];
let shapes = paint(128.0, |p, s, band| waveform::draw_live(p, s, band, 1280.0, 128.0, &blocks, band, WAVE));
let shapes = paint(128.0, |p, s, band| waveform::draw_live(waveform::Canvas { painter: p, s, tl: band }, 1280.0, 128.0, &blocks, band, WAVE));
let [Shape::Mesh(mesh)] = shapes.as_slice() else { panic!("{shapes:?}") };
let span = |f: fn(&Pos2) -> f32| mesh.vertices.iter().map(|v| f(&v.pos)).fold((f32::MAX, f32::MIN), |(a, b), v| (a.min(v), b.max(v)));
let ((left, right), (top, bottom)) = (span(|p| p.x), span(|p| p.y));
+17 -13
View File
@@ -54,20 +54,24 @@ impl Matrix {
}
}
/// The planes (and their geometry) of an 8-bit planar Y'CbCr picture for [`planar_to_rgba`].
#[derive(Clone, Copy)]
pub struct Planes<'a> {
pub width: usize,
pub height: usize,
pub y: &'a [u8],
pub y_stride: usize,
pub u: &'a [u8],
pub v: &'a [u8],
pub uv_stride: usize,
pub cx: usize,
pub cy: usize,
}
/// Planar 8-bit Y'CbCr with chroma subsampled by `cx` horizontally and `cy` vertically
/// (1 or 2 each) → packed RGBA. Missing samples (short planes) read as black / neutral.
pub fn planar_to_rgba(
width: usize,
height: usize,
y: &[u8],
y_stride: usize,
u: &[u8],
v: &[u8],
uv_stride: usize,
cx: usize,
cy: usize,
m: &Matrix,
) -> Vec<u8> {
pub fn planar_to_rgba(p: &Planes<'_>, m: &Matrix) -> Vec<u8> {
let Planes { width, height, y, y_stride, u, v, uv_stride, cx, cy } = *p;
let (cx, cy) = (cx.max(1), cy.max(1));
let mut out = vec![0u8; width.saturating_mul(height).saturating_mul(4)];
for (row, line) in out.chunks_exact_mut(width.max(1) * 4).enumerate().take(height) {
@@ -164,7 +168,7 @@ mod tests {
#[test]
fn planar_conversion_tolerates_short_planes() {
let m = Matrix::new(true, false);
let out = planar_to_rgba(4, 4, &[235; 3], 4, &[], &[], 2, 2, 2, &m);
let out = planar_to_rgba(&Planes { width: 4, height: 4, y: &[235; 3], y_stride: 4, u: &[], v: &[], uv_stride: 2, cx: 2, cy: 2 }, &m);
assert_eq!(out.len(), 64);
assert_eq!(&out[..4], &[255, 255, 255, 255]);
}
+1 -1
View File
@@ -268,7 +268,7 @@ impl Frame {
/// Copy the w x h window of chroma component `c` at (x0, y0) (clamped) into `out`.
#[inline]
pub fn chroma_window(&self, c: usize, x0: i32, y0: i32, w: usize, h: usize, out: &mut [u8], os: usize) {
pub fn chroma_window(&self, c: usize, (x0, y0): (i32, i32), w: usize, h: usize, out: &mut [u8], os: usize) {
let (cw, ch) = (self.cwidth, self.cheight);
for r in 0..h {
let yy = (y0 + r as i32).clamp(0, ch as i32 - 1) as usize;
+1 -1
View File
@@ -236,7 +236,7 @@ fn predict_block(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: u
let (fx, fy) = ((mv[0] & 7) as u32, (mv[1] & 7) as u32);
for (comp, out) in c.into_iter().enumerate() {
let mut w = [0u8; 16 * 9];
f.chroma_window(comp, cx, cy, cw + 1, ch + 1, &mut w, 16);
f.chroma_window(comp, (cx, cy), cw + 1, ch + 1, &mut w, 16);
inter::mc_chroma_win(&w, 16, fx, fy, cw, ch, out, cs);
}
}
+16 -3
View File
@@ -1,6 +1,6 @@
//! [`Movie`]: open an MP4/MOV, report its picture format, and decode frames at times to RGBA.
use crate::convert::{Matrix, planar_to_rgba, scale_rgba};
use crate::convert::{Matrix, Planes, planar_to_rgba, scale_rgba};
use crate::isobmff::{self, ByteSource, CodecConfig, Mp4File, TrackKind};
use crate::{Result, VideoError};
use std::collections::VecDeque;
@@ -411,7 +411,7 @@ impl Movie {
let (y, u, v) = (narrow(&f.y), narrow(&f.cb), narrow(&f.cr));
let cx = if cw < w { 2 } else { 1 };
let m = Matrix::for_code(f.color.matrix, f.height, false);
let rgba = planar_to_rgba(w, h, &y, w, &u, &v, cw, cx, 1, &m);
let rgba = planar_to_rgba(&Planes { width: w, height: h, y: &y, y_stride: w, u: &u, v: &v, uv_stride: cw, cx, cy: 1 }, &m);
Ok(Frame { width: f.width, height: f.height, rgba, index, time })
}
Codec::Jpeg => {
@@ -429,7 +429,20 @@ impl Movie {
fn h264_to_frame(&self, p: &crate::h264::Picture, index: usize) -> Frame {
let m = Matrix::for_code(p.color.matrix, p.height, p.color.full_range);
let rgba = planar_to_rgba(p.width as usize, p.height as usize, &p.y, p.y_stride, &p.u, &p.v, p.uv_stride, 2, 2, &m);
let rgba = planar_to_rgba(
&Planes {
width: p.width as usize,
height: p.height as usize,
y: &p.y,
y_stride: p.y_stride,
u: &p.u,
v: &p.v,
uv_stride: p.uv_stride,
cx: 2,
cy: 2,
},
&m,
);
Frame { width: p.width, height: p.height, rgba, index, time: self.times.get(index).copied().unwrap_or(0.0) }
}