Media · Audio and controls: player, waveform, spectrum, volume, speed, transport and playlist
AudioPlayer sets a song's cover, title and artist over a scrubber with the time played and left, the transport and the volume. AudioWaveform draws peaks gathered to the bars that fit, dark as far as it has played, and seeks by press, drag and keys, with the time tip the scrubber shares. AudioSpectrum shows levels by pitch. VolumeControl mutes from its speaker, and moving its slider while muted unmutes. PlaybackSpeedControl picks from a menu. MediaControls holds shuffle, back, play, forward and repeat. Playlist builds on SelectableList: the song under way is lit with moving bars, and Enter or a double press plays another. The video player's bar now rests while the stage itself holds focus, a key it takes wakes the bar, and a press on a resting bar only wakes it.
This commit is contained in:
1 parent
1ac50e4002
commit
04f88812dc
24 files changed
+1824
-81
No files matched your search
@@ -0,0 +1,253 @@
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use gpui::{
|
||||
App, Bounds, Corners, DragMoveEvent, ElementId, EmptyView, EntityId, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, Pixels, Point, RenderOnce, StatefulInteractiveElement,
|
||||
Styled, Window, canvas, div, fill, point, prelude::*, relative, size, transparent_black,
|
||||
};
|
||||
|
||||
use super::scrubber::{OnTime, clock, time_at, time_tip};
|
||||
use crate::{
|
||||
forms::keyed,
|
||||
primitives::{FocusRing, tab_stop},
|
||||
theme::{ActiveTheme, Radius},
|
||||
};
|
||||
|
||||
/// Seconds an arrow key moves the time.
|
||||
const STEP: f64 = 5.0;
|
||||
|
||||
/// `peaks` gathered into `count` bars, each the loudest peak it covers.
|
||||
pub(crate) fn resample(peaks: &[f32], count: usize) -> Vec<f32> {
|
||||
assert!(
|
||||
!peaks.is_empty() && count > 0,
|
||||
"{} peaks into {count}",
|
||||
peaks.len()
|
||||
);
|
||||
(0..count)
|
||||
.map(|bar| {
|
||||
let from = bar * peaks.len() / count;
|
||||
let to = ((bar + 1) * peaks.len() / count).max(from + 1);
|
||||
peaks[from..to].iter().copied().fold(0.0, f32::max)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A seeking drag on a waveform, marked with its waveform.
|
||||
struct Seek(EntityId);
|
||||
|
||||
/// The waveform's bounds and the share under the pointer.
|
||||
#[derive(Default)]
|
||||
struct Wave {
|
||||
bounds: Bounds<Pixels>,
|
||||
hover: Option<f32>,
|
||||
}
|
||||
|
||||
/// A sound's waveform, its peaks from 0 to 1 in time order, dark as far as it has played; the time under the pointer shows above it. A press or a drag seeks; with focus the arrows step five seconds, the Page keys fifty, Home and End go to the ends. The host plays it.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AudioWaveform {
|
||||
id: ElementId,
|
||||
peaks: Rc<Vec<f32>>,
|
||||
length: Duration,
|
||||
at: Duration,
|
||||
on_seek: Option<OnTime>,
|
||||
}
|
||||
|
||||
impl AudioWaveform {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
peaks: impl Into<Vec<f32>>,
|
||||
length: Duration,
|
||||
at: Duration,
|
||||
) -> Self {
|
||||
let peaks: Vec<f32> = peaks.into();
|
||||
assert!(
|
||||
!peaks.is_empty() && peaks.iter().all(|peak| (0.0..=1.0).contains(peak)),
|
||||
"peaks lie within 0 to 1"
|
||||
);
|
||||
assert!(!length.is_zero() && at <= length, "{at:?} of {length:?}");
|
||||
Self {
|
||||
id: id.into(),
|
||||
peaks: Rc::new(peaks),
|
||||
length,
|
||||
at,
|
||||
on_seek: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the time to seek to.
|
||||
pub fn on_seek(mut self, handler: impl Fn(Duration, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_seek = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AudioWaveform {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let wave = window.use_keyed_state((self.id.clone(), "wave"), cx, |_, _| Wave::default());
|
||||
let focus = tab_stop(
|
||||
(self.id.clone(), "focus").into(),
|
||||
self.on_seek.is_some(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let owner = wave.entity_id();
|
||||
let (length, at, peaks) = (self.length, self.at, self.peaks);
|
||||
let played = at.as_secs_f32() / length.as_secs_f32();
|
||||
let hover = wave.read(cx).hover;
|
||||
let theme = cx.theme();
|
||||
let sizes = theme.media();
|
||||
let rem = window.rem_size();
|
||||
let (bar, gap) = (sizes.wave_bar.to_pixels(rem), sizes.wave_gap.to_pixels(rem));
|
||||
let (dark, light) = (theme.colors.fg, theme.colors.fg_subtle);
|
||||
let tip = hover.map(|share| time_tip(share, clock(time_at(share, length)), cx));
|
||||
let measured = wave.clone();
|
||||
let drawn = canvas(
|
||||
move |bounds, window, cx| {
|
||||
if measured.read(cx).bounds != bounds {
|
||||
measured.update(cx, |wave, cx| {
|
||||
wave.bounds = bounds;
|
||||
cx.notify();
|
||||
});
|
||||
window.request_animation_frame();
|
||||
}
|
||||
},
|
||||
move |bounds, _, window, _| {
|
||||
let width = f32::from(bounds.size.width);
|
||||
let pitch = f32::from(bar + gap);
|
||||
let count = ((width + f32::from(gap)) / pitch).floor().max(1.0) as usize;
|
||||
let inset = (width - (pitch * count as f32 - f32::from(gap))) / 2.0;
|
||||
for (ix, peak) in resample(&peaks, count).into_iter().enumerate() {
|
||||
let left = inset + pitch * ix as f32;
|
||||
let tall = (bounds.size.height * peak).max(bar);
|
||||
let top = bounds.top() + (bounds.size.height - tall) / 2.0;
|
||||
let origin = point(bounds.left() + Pixels::from(left), top);
|
||||
let lit = (left + f32::from(bar) / 2.0) / width <= played;
|
||||
window.paint_quad(
|
||||
fill(
|
||||
Bounds::new(origin, size(bar, tall)),
|
||||
if lit { dark } else { light },
|
||||
)
|
||||
.corner_radii(Corners::all(bar / 2.0)),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.size_full();
|
||||
let spot = move |bounds: Bounds<Pixels>, pointer: Point<Pixels>| {
|
||||
(f32::from(pointer.x - bounds.left()) / f32::from(bounds.size.width)).clamp(0.0, 1.0)
|
||||
};
|
||||
let (moved, left) = (wave.clone(), wave.clone());
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.debug_selector(|| "audio-waveform".into())
|
||||
.relative()
|
||||
.w_full()
|
||||
.h(sizes.waveform)
|
||||
.border_1()
|
||||
.border_color(transparent_black())
|
||||
.rounded(theme.radius(Radius::Sm))
|
||||
.child(drawn)
|
||||
.children(tip)
|
||||
.on_mouse_move(move |event, _, cx| {
|
||||
moved.update(cx, |wave, cx| {
|
||||
wave.hover = Some(spot(wave.bounds, event.position));
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.on_hover(move |inside, _, cx| {
|
||||
if !inside {
|
||||
left.update(cx, |wave, cx| {
|
||||
wave.hover = None;
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
.when_some(self.on_seek, |shown, seek| {
|
||||
let (pressed, dragged, keys) = (seek.clone(), seek.clone(), seek);
|
||||
let (pressed_wave, dragged_wave) = (wave.clone(), wave.clone());
|
||||
shown
|
||||
.track_focus(&focus)
|
||||
.focus_ring(cx)
|
||||
.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |event, window, cx| {
|
||||
let time =
|
||||
time_at(spot(pressed_wave.read(cx).bounds, event.position), length);
|
||||
log::info!("audio waveform: seek to {}", clock(time));
|
||||
pressed(time, window, cx);
|
||||
})
|
||||
.on_drag(Seek(owner), |_, _, _, cx| cx.new(|_| EmptyView))
|
||||
.on_drag_move(move |event: &DragMoveEvent<Seek>, window, cx| {
|
||||
if event.drag(cx).0 != owner {
|
||||
return;
|
||||
}
|
||||
let bounds = dragged_wave.read(cx).bounds;
|
||||
dragged(
|
||||
time_at(spot(bounds, event.event.position), length),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.on_key_down(move |event, window, cx| {
|
||||
let (now, end) = (at.as_secs_f64(), length.as_secs_f64());
|
||||
let Some(next) = keyed(&event.keystroke.key, now, 0.0, end, STEP) else {
|
||||
return;
|
||||
};
|
||||
cx.stop_propagation();
|
||||
keys(Duration::from_secs_f64(next), window, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Sound by pitch, low to high: bars rising from a line, each level from 0 to 1. The host sends new levels as it plays.
|
||||
#[derive(IntoElement)]
|
||||
pub struct AudioSpectrum {
|
||||
levels: Vec<f32>,
|
||||
}
|
||||
|
||||
impl AudioSpectrum {
|
||||
pub fn new(levels: impl Into<Vec<f32>>) -> Self {
|
||||
let levels: Vec<f32> = levels.into();
|
||||
assert!(
|
||||
!levels.is_empty() && levels.iter().all(|level| (0.0..=1.0).contains(level)),
|
||||
"levels lie within 0 to 1"
|
||||
);
|
||||
Self { levels }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AudioSpectrum {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let (color, round) = (theme.colors.fg_muted, theme.radius(Radius::Sm));
|
||||
div()
|
||||
.debug_selector(|| "audio-spectrum".into())
|
||||
.w_full()
|
||||
.h(theme.media().waveform)
|
||||
.flex()
|
||||
.items_end()
|
||||
.gap_0p5()
|
||||
.children(self.levels.into_iter().map(move |level| {
|
||||
div()
|
||||
.flex_1()
|
||||
.h(relative(level.max(0.02)))
|
||||
.rounded_t(round)
|
||||
.bg(color)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::resample;
|
||||
|
||||
#[test]
|
||||
fn peaks_gather_into_bars_by_their_loudest() {
|
||||
assert_eq!(resample(&[0.1, 0.9, 0.3, 0.2], 2), [0.9, 0.3]);
|
||||
assert_eq!(resample(&[0.4, 0.8], 4), [0.4, 0.4, 0.8, 0.8]);
|
||||
assert_eq!(resample(&[0.5], 3), [0.5, 0.5, 0.5]);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user