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,303 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use ely_gpui_component::media::{
|
||||
AudioPlayer, AudioSpectrum, AudioWaveform, MediaControls, PlaybackSpeedControl, Playlist,
|
||||
Repeat, Track, VolumeControl,
|
||||
};
|
||||
use gpui::{App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
|
||||
use crate::{
|
||||
probe::probe,
|
||||
ui::{keep, noise, section},
|
||||
};
|
||||
|
||||
const COVER: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/examples/gallery/assets/dunes-square.jpg"
|
||||
);
|
||||
/// How often the demo's clock moves while a song plays.
|
||||
const TICK: Duration = Duration::from_millis(100);
|
||||
const SONGS: [(&str, &str, &str, u64); 5] = [
|
||||
("tall-windows", "Tall Windows", "Aster Quartet", 222),
|
||||
("stone-stair", "Stone Stair", "Aster Quartet", 245),
|
||||
("olive-court", "Olive Court", "Lumen Trio", 178),
|
||||
("dune-hours", "Dune Hours", "Lumen Trio", 311),
|
||||
("low-sun", "Low Sun", "Marram", 207),
|
||||
];
|
||||
|
||||
/// The demo's listening: the song under way and where, how it plays, and its volume.
|
||||
#[derive(Clone)]
|
||||
struct Listening {
|
||||
song: usize,
|
||||
at: Duration,
|
||||
playing: bool,
|
||||
ticking: bool,
|
||||
speed: f32,
|
||||
shuffle: bool,
|
||||
repeat: Repeat,
|
||||
level: f32,
|
||||
muted: bool,
|
||||
}
|
||||
|
||||
impl Listening {
|
||||
fn length(&self) -> Duration {
|
||||
Duration::from_secs(SONGS[self.song].3)
|
||||
}
|
||||
|
||||
/// Moves `by` songs along the list, from the start of the song.
|
||||
fn skip(&mut self, by: isize) {
|
||||
self.song = (self.song as isize + by).rem_euclid(SONGS.len() as isize) as usize;
|
||||
self.at = Duration::ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
fn tracks() -> Vec<Track> {
|
||||
SONGS
|
||||
.iter()
|
||||
.map(|(key, title, artist, seconds)| Track {
|
||||
key: SharedString::from(*key),
|
||||
title: SharedString::from(*title),
|
||||
artist: SharedString::from(*artist),
|
||||
length: Duration::from_secs(*seconds),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A song's peaks, the same each run: sections that swell and fall, faded at both ends.
|
||||
fn peaks(song: usize) -> Vec<f32> {
|
||||
let mut next = noise(song as u64 + 7);
|
||||
let count = 180;
|
||||
(0..count)
|
||||
.map(|ix| {
|
||||
let fade = (ix.min(count - 1 - ix) as f32 / 10.0).min(1.0);
|
||||
let swell = 0.55 + 0.45 * (ix as f32 / 23.0 + song as f32).sin();
|
||||
(fade * swell * (0.35 + 0.65 * next() as f32)).clamp(0.0, 1.0)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Levels by pitch at a moment of the song: lows louder, each band beating at its own rate.
|
||||
fn levels(at: Duration) -> Vec<f32> {
|
||||
let time = at.as_secs_f32();
|
||||
(0..40)
|
||||
.map(|band| {
|
||||
let band = band as f32;
|
||||
let beat = (time * 2.3 + band * 0.7).sin() * (time * 0.9 + band * 0.31).cos();
|
||||
((1.0 - band / 52.0) * (0.3 + 0.35 * (1.0 + beat))).clamp(0.0, 1.0)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Runs the demo's clock while a song plays, one loop at a time; at the end the next song starts, or the same one on repeat.
|
||||
fn tick(state: &Entity<Listening>, window: &mut Window, cx: &mut App) {
|
||||
if state.update(cx, |now, _| std::mem::replace(&mut now.ticking, true)) {
|
||||
return;
|
||||
}
|
||||
let state = state.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(TICK).await;
|
||||
let going = state.update(cx, |now, cx| {
|
||||
now.at = (now.at + TICK.mul_f32(now.speed)).min(now.length());
|
||||
if now.at == now.length() {
|
||||
match now.repeat {
|
||||
Repeat::One => now.at = Duration::ZERO,
|
||||
Repeat::All => now.skip(1),
|
||||
Repeat::Off if now.song + 1 < SONGS.len() => now.skip(1),
|
||||
Repeat::Off => now.playing = false,
|
||||
}
|
||||
}
|
||||
now.ticking = now.playing;
|
||||
cx.notify();
|
||||
now.playing
|
||||
})?;
|
||||
if !going {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn state(window: &mut Window, cx: &mut App) -> Entity<Listening> {
|
||||
keep(
|
||||
"media-audio",
|
||||
|| Listening {
|
||||
song: 0,
|
||||
at: Duration::from_secs(64),
|
||||
playing: false,
|
||||
ticking: false,
|
||||
speed: 1.0,
|
||||
shuffle: false,
|
||||
repeat: Repeat::All,
|
||||
level: 0.7,
|
||||
muted: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Changes the demo's listening and redraws.
|
||||
fn change(state: &Entity<Listening>, cx: &mut App, edit: impl FnOnce(&mut Listening)) {
|
||||
state.update(cx, |now, cx| {
|
||||
edit(now);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn controls(id: &'static str, state: &Entity<Listening>, now: &Listening) -> MediaControls {
|
||||
let (play, back, forward, shuffle, repeat) = (
|
||||
state.clone(),
|
||||
state.clone(),
|
||||
state.clone(),
|
||||
state.clone(),
|
||||
state.clone(),
|
||||
);
|
||||
MediaControls::new(id, now.playing)
|
||||
.on_play(move |on, window, cx| {
|
||||
change(&play, cx, |now| now.playing = on);
|
||||
if on {
|
||||
tick(&play, window, cx);
|
||||
}
|
||||
})
|
||||
.on_back(move |_, cx| change(&back, cx, |now| now.skip(-1)))
|
||||
.on_forward(move |_, cx| change(&forward, cx, |now| now.skip(1)))
|
||||
.shuffle(now.shuffle, move |on, _, cx| {
|
||||
change(&shuffle, cx, |now| now.shuffle = on)
|
||||
})
|
||||
.repeat(now.repeat, move |next, _, cx| {
|
||||
change(&repeat, cx, |now| now.repeat = next)
|
||||
})
|
||||
}
|
||||
|
||||
fn volume(id: &'static str, state: &Entity<Listening>, now: &Listening) -> VolumeControl {
|
||||
let (level, mute) = (state.clone(), state.clone());
|
||||
VolumeControl::new(id, now.level, now.muted)
|
||||
.on_level(move |to, _, cx| change(&level, cx, |now| now.level = to as f32))
|
||||
.on_mute(move |on, _, cx| change(&mute, cx, |now| now.muted = on))
|
||||
}
|
||||
|
||||
pub fn player(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
let (title, artist) = (SONGS[now.song].1, SONGS[now.song].2);
|
||||
let seek = state.clone();
|
||||
let player = AudioPlayer::new("media-audio", title, artist, now.length(), now.at)
|
||||
.artwork(Path::new(COVER))
|
||||
.controls(controls("media-audio-transport", &state, &now))
|
||||
.volume(volume("media-audio-volume", &state, &now))
|
||||
.on_seek(move |at, _, cx| change(&seek, cx, |now| now.at = at));
|
||||
section(
|
||||
"AudioPlayer",
|
||||
"A song the host plays: its cover, the time played and left along a scrubber, the transport and the volume.",
|
||||
cx,
|
||||
)
|
||||
.child(probe("media-audio", div().w(px(360.)).child(player)))
|
||||
}
|
||||
|
||||
pub fn waveform(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
let seek = state.clone();
|
||||
section(
|
||||
"AudioWaveform",
|
||||
"A sound's peaks, dark as far as it has played. A press or a drag seeks, the time under the pointer shows above it, and with focus the arrows step five seconds.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-waveform",
|
||||
div().w(px(640.)).pt_6().child(
|
||||
AudioWaveform::new("media-waveform", peaks(now.song), now.length(), now.at)
|
||||
.on_seek(move |at, _, cx| change(&seek, cx, |now| now.at = at)),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn spectrum(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let at = state(window, cx).read(cx).at;
|
||||
section(
|
||||
"AudioSpectrum / Visualizer",
|
||||
"Sound by pitch, low to high. The host sends new levels as it plays; they hold while it pauses.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-spectrum",
|
||||
div().w(px(360.)).child(AudioSpectrum::new(levels(at))),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn volume_control(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
section(
|
||||
"VolumeControl",
|
||||
"A speaker that mutes, drawn by the level, beside a slider. Muted, the slider rests at zero; moving it unmutes.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-volume",
|
||||
div().w(px(240.)).child(volume("media-volume", &state, &now)),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn speed(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).speed;
|
||||
let pace = state.clone();
|
||||
section(
|
||||
"PlaybackSpeedControl",
|
||||
"How fast it plays, from half to double, chosen from a menu.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-speed",
|
||||
div().child(
|
||||
PlaybackSpeedControl::new("media-speed", now)
|
||||
.on_change(move |to, _, cx| change(&pace, cx, |now| now.speed = to)),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn transport(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
section(
|
||||
"MediaControls",
|
||||
"Shuffle, back, play, forward and repeat. Shuffle and repeat stay lit while on; repeat steps from all to one to off.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-transport",
|
||||
div().w(px(360.)).child(controls("media-transport", &state, &now)),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn playlist(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let state = state(window, cx);
|
||||
let now = state.read(cx).clone();
|
||||
let pick = state.clone();
|
||||
section(
|
||||
"Playlist",
|
||||
"Songs in order. The one under way is lit, its bars moving while it plays; Enter or a double press plays another.",
|
||||
cx,
|
||||
)
|
||||
.child(probe(
|
||||
"media-playlist",
|
||||
div().w(px(420.)).child(
|
||||
Playlist::new("media-playlist", tracks())
|
||||
.current(SONGS[now.song].0, now.playing)
|
||||
.on_play(move |key, window, cx| {
|
||||
let song = SONGS
|
||||
.iter()
|
||||
.position(|(each, ..)| *each == key.as_ref())
|
||||
.expect("a listed song");
|
||||
change(&pick, cx, |now| {
|
||||
(now.song, now.at, now.playing) = (song, Duration::ZERO, true)
|
||||
});
|
||||
tick(&pick, window, cx);
|
||||
}),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod audio;
|
||||
mod edit;
|
||||
mod images;
|
||||
mod video;
|
||||
@@ -68,6 +69,26 @@ const SCRIPT: &[Step] = &[
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("retimed"),
|
||||
Step::HoverAt("media-waveform", 400.0, 48.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("wave-tip"),
|
||||
Step::DownAt("media-speed", 30.0, 16.0),
|
||||
Step::UpAt("media-speed", 30.0, 16.0),
|
||||
Step::Wait(300),
|
||||
Step::Shot("speeds"),
|
||||
Step::Key("escape"),
|
||||
Step::DownAt("media-volume", 14.0, 14.0),
|
||||
Step::UpAt("media-volume", 14.0, 14.0),
|
||||
Step::Wait(200),
|
||||
Step::Shot("muted"),
|
||||
Step::DownAt("media-playlist", 200.0, 124.0),
|
||||
Step::UpAt("media-playlist", 200.0, 124.0),
|
||||
Step::Key("enter"),
|
||||
Step::Wait(300),
|
||||
Step::Shot("played"),
|
||||
Step::DownAt("media-transport", 180.0, 20.0),
|
||||
Step::UpAt("media-transport", 180.0, 20.0),
|
||||
Step::Wait(300),
|
||||
];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
@@ -81,5 +102,12 @@ fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
.child(video::scrubber(window, cx))
|
||||
.child(video::strip(window, cx))
|
||||
.child(video::subtitles(window, cx))
|
||||
.child(audio::player(window, cx))
|
||||
.child(audio::waveform(window, cx))
|
||||
.child(audio::spectrum(window, cx))
|
||||
.child(audio::volume_control(window, cx))
|
||||
.child(audio::speed(window, cx))
|
||||
.child(audio::transport(window, cx))
|
||||
.child(audio::playlist(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Reference in new issue
Block a user