fixed some bugs
This commit is contained in:
parent
97d2d6bd75
commit
464aad571a
|
|
@ -32,14 +32,14 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.2</string>
|
||||
<string>1.0.3</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
<string>iPhoneSimulator</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.2</string>
|
||||
<string>1.0.3</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|||
use cpal::{BufferSize, SampleFormat, SampleRate, Stream, StreamConfig};
|
||||
#[cfg(all(not(target_os = "ios"), not(target_os = "android")))]
|
||||
use cpal::SupportedBufferSize;
|
||||
use crossbeam_channel::{unbounded, Sender};
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
|
||||
use crate::track::TrackData;
|
||||
|
||||
/// upstream notifications from the audio callback to the App tick loop.
|
||||
pub enum EngineEvent {
|
||||
TrackEnded { id: u64 },
|
||||
}
|
||||
|
||||
/// lazy-initialises the engine clock's reference instant.
|
||||
fn engine_clock() -> Instant {
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
|
|
@ -36,7 +41,7 @@ pub struct EngineState {
|
|||
|
||||
/// transport messages crossing from the ui thread into the cpal callback.
|
||||
enum Cmd {
|
||||
Load(Arc<TrackData>),
|
||||
Load { td: Arc<TrackData>, id: u64 },
|
||||
Play,
|
||||
Pause,
|
||||
Stop,
|
||||
|
|
@ -47,6 +52,7 @@ enum Cmd {
|
|||
pub struct AudioEngine {
|
||||
pub state: Arc<EngineState>,
|
||||
cmd_tx: Sender<Cmd>,
|
||||
event_rx: Receiver<EngineEvent>,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
_stream: Stream,
|
||||
#[cfg(target_os = "android")]
|
||||
|
|
@ -132,9 +138,12 @@ impl AudioEngine {
|
|||
last_callback_nanos: AtomicU64::new(0),
|
||||
});
|
||||
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
||||
let (event_tx, event_rx) = unbounded::<EngineEvent>();
|
||||
|
||||
let cb_state = state.clone();
|
||||
let mut current: Option<Arc<TrackData>> = None;
|
||||
let mut current_id: u64 = 0;
|
||||
let mut emitted_end: bool = false;
|
||||
let mut local_pos: u64 = 0;
|
||||
let mut local_playing = false;
|
||||
|
||||
|
|
@ -146,11 +155,13 @@ impl AudioEngine {
|
|||
move |out: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||
match cmd {
|
||||
Cmd::Load(td) => {
|
||||
Cmd::Load { td, id } => {
|
||||
cb_state
|
||||
.total_frames
|
||||
.store(td.total_samples() as u64, Ordering::Release);
|
||||
current = Some(td);
|
||||
current_id = id;
|
||||
emitted_end = false;
|
||||
local_pos = 0;
|
||||
cb_state.frame_pos.store(0, Ordering::Release);
|
||||
cb_state.last_callback_pos.store(0, Ordering::Release);
|
||||
|
|
@ -213,6 +224,10 @@ impl AudioEngine {
|
|||
for f in 0..frames_out {
|
||||
let dst = f * device_ch;
|
||||
if local_pos >= total {
|
||||
if !emitted_end {
|
||||
let _ = event_tx.send(EngineEvent::TrackEnded { id: current_id });
|
||||
emitted_end = true;
|
||||
}
|
||||
for c in 0..device_ch {
|
||||
out[dst + c] = 0.0;
|
||||
}
|
||||
|
|
@ -259,6 +274,7 @@ impl AudioEngine {
|
|||
Ok(AudioEngine {
|
||||
state,
|
||||
cmd_tx,
|
||||
event_rx,
|
||||
_stream: stream,
|
||||
})
|
||||
}
|
||||
|
|
@ -286,9 +302,12 @@ impl AudioEngine {
|
|||
last_callback_nanos: AtomicU64::new(0),
|
||||
});
|
||||
let (cmd_tx, cmd_rx) = unbounded::<Cmd>();
|
||||
let (event_tx, event_rx) = unbounded::<EngineEvent>();
|
||||
|
||||
let cb_state = state.clone();
|
||||
let mut current: Option<Arc<TrackData>> = None;
|
||||
let mut current_id: u64 = 0;
|
||||
let mut emitted_end: bool = false;
|
||||
let mut local_pos: u64 = 0;
|
||||
let mut local_playing = false;
|
||||
|
||||
|
|
@ -299,11 +318,13 @@ impl AudioEngine {
|
|||
move |stream: &AudioStream, audio_data: *mut c_void, num_frames: i32| {
|
||||
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||
match cmd {
|
||||
Cmd::Load(td) => {
|
||||
Cmd::Load { td, id } => {
|
||||
cb_state
|
||||
.total_frames
|
||||
.store(td.total_samples() as u64, Ordering::Release);
|
||||
current = Some(td);
|
||||
current_id = id;
|
||||
emitted_end = false;
|
||||
local_pos = 0;
|
||||
cb_state.frame_pos.store(0, Ordering::Release);
|
||||
cb_state.last_callback_pos.store(0, Ordering::Release);
|
||||
|
|
@ -382,6 +403,10 @@ impl AudioEngine {
|
|||
for f in 0..frames {
|
||||
let dst = f * device_ch;
|
||||
if local_pos >= total {
|
||||
if !emitted_end {
|
||||
let _ = event_tx.send(EngineEvent::TrackEnded { id: current_id });
|
||||
emitted_end = true;
|
||||
}
|
||||
for c in 0..device_ch {
|
||||
out[dst + c] = 0.0;
|
||||
}
|
||||
|
|
@ -451,6 +476,7 @@ impl AudioEngine {
|
|||
Ok(AudioEngine {
|
||||
state,
|
||||
cmd_tx,
|
||||
event_rx,
|
||||
_stream: stream,
|
||||
})
|
||||
}
|
||||
|
|
@ -460,9 +486,14 @@ impl AudioEngine {
|
|||
self.state.sample_rate.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
/// hands incoming pcm to the audio callback and rewinds the playhead to zero.
|
||||
pub fn load(&self, track: Arc<TrackData>) {
|
||||
let _ = self.cmd_tx.send(Cmd::Load(track));
|
||||
/// hands incoming pcm to the audio callback and rewinds the playhead to zero, tagged with a load id matched against TrackEnded events.
|
||||
pub fn load(&self, track: Arc<TrackData>, id: u64) {
|
||||
let _ = self.cmd_tx.send(Cmd::Load { td: track, id });
|
||||
}
|
||||
|
||||
/// drains pending engine-to-app events.
|
||||
pub fn drain_events(&self) -> Vec<EngineEvent> {
|
||||
self.event_rx.try_iter().collect()
|
||||
}
|
||||
|
||||
/// resumes output from the current frame position.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use iced_widget::scrollable::AbsoluteOffset;
|
|||
|
||||
use crate::analyzer::FrameData;
|
||||
use crate::analyzer_worker::AnalyzerWorker;
|
||||
use crate::engine::AudioEngine;
|
||||
use crate::engine::{AudioEngine, EngineEvent};
|
||||
use crate::library::{self, Track};
|
||||
use crate::library_worker::{LibraryUpdate, LibraryWorker};
|
||||
|
||||
|
|
@ -41,13 +41,10 @@ pub struct App {
|
|||
/// modal status message shown over the UI while waiting on iOS file-coordinator caching.
|
||||
pub coordinating_message: Option<String>,
|
||||
|
||||
/// sidebar scroll offset mirrored from the on_scroll callback.
|
||||
pub sidebar_scroll: AbsoluteOffset,
|
||||
|
||||
/// settings panel scroll offset mirrored from the on_scroll callback.
|
||||
pub settings_scroll: AbsoluteOffset,
|
||||
|
||||
/// pending sidebar scroll restore against the live scrollable widget.
|
||||
/// pending sidebar snap to the currently playing track row.
|
||||
pub restore_sidebar_scroll: bool,
|
||||
|
||||
/// pending settings scroll restore against the live scrollable widget.
|
||||
|
|
@ -86,7 +83,7 @@ impl Default for Settings {
|
|||
glass: true,
|
||||
entropy_on: false,
|
||||
album_colors: false,
|
||||
mirrored: false,
|
||||
mirrored: true,
|
||||
inverted: false,
|
||||
entropy_strength: 0.0,
|
||||
hue: 0.9,
|
||||
|
|
@ -124,7 +121,7 @@ pub enum Message {
|
|||
ToggleImmersive,
|
||||
ToggleChrome,
|
||||
ToggleSettings,
|
||||
SidebarScrolled(AbsoluteOffset),
|
||||
NoOp,
|
||||
SettingsScrolled(AbsoluteOffset),
|
||||
SetGlass(bool),
|
||||
SetEntropy(bool),
|
||||
|
|
@ -177,7 +174,6 @@ impl App {
|
|||
track_loading: false,
|
||||
library_progress: None,
|
||||
coordinating_message: None,
|
||||
sidebar_scroll: AbsoluteOffset::default(),
|
||||
settings_scroll: AbsoluteOffset::default(),
|
||||
restore_sidebar_scroll: false,
|
||||
restore_settings_scroll: false,
|
||||
|
|
@ -198,7 +194,7 @@ impl App {
|
|||
|
||||
/// checks whether a logical-coords point falls inside the scrollable sidebar region.
|
||||
pub fn point_in_sidebar(&self, x: f32, y: f32, viewport_height: f32) -> bool {
|
||||
if self.immersive || self.show_settings {
|
||||
if self.immersive {
|
||||
return false;
|
||||
}
|
||||
x >= 0.0
|
||||
|
|
@ -360,7 +356,6 @@ impl App {
|
|||
|
||||
/// drains worker updates, advances tracks, and refreshes analyzer frames each frame.
|
||||
pub fn tick(&mut self) {
|
||||
let mut needs_resort = false;
|
||||
for upd in self.library_worker.drain_updates() {
|
||||
match upd {
|
||||
LibraryUpdate::Meta {
|
||||
|
|
@ -382,10 +377,7 @@ impl App {
|
|||
t.album = album;
|
||||
}
|
||||
if let Some(tn) = track_number {
|
||||
if t.track_number != Some(tn) {
|
||||
t.track_number = Some(tn);
|
||||
needs_resort = true;
|
||||
}
|
||||
t.track_number = Some(tn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,7 +429,7 @@ impl App {
|
|||
match result {
|
||||
Ok(td) => {
|
||||
if let Some(eng) = &self.engine {
|
||||
eng.load(td.clone());
|
||||
eng.load(td.clone(), self.current_decode_id);
|
||||
if self.playing {
|
||||
eng.play();
|
||||
} else {
|
||||
|
|
@ -452,17 +444,37 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
if needs_resort {
|
||||
self.resort_library();
|
||||
}
|
||||
|
||||
self.advance_if_finished();
|
||||
self.handle_engine_events();
|
||||
|
||||
self.worker.publish_playhead(self.position());
|
||||
self.frame_data = self.worker.latest_frames();
|
||||
}
|
||||
|
||||
/// drains engine end-of-track notifications and advances when the id matches the current load.
|
||||
fn handle_engine_events(&mut self) {
|
||||
let Some(eng) = self.engine.as_ref() else { return };
|
||||
for ev in eng.drain_events() {
|
||||
match ev {
|
||||
EngineEvent::TrackEnded { id } => {
|
||||
if id == self.current_decode_id && self.playing && !self.track_loading {
|
||||
self.advance_to_next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wraps the playhead onto the next track in the current sidebar order.
|
||||
fn advance_to_next(&mut self) {
|
||||
let Some(idx) = self.selected_track else { return };
|
||||
if self.library.tracks.is_empty() { return; }
|
||||
let next = (idx + 1) % self.library.tracks.len();
|
||||
self.selected_track = Some(next);
|
||||
self.load_index(next);
|
||||
}
|
||||
|
||||
/// re-sorts the library on arrival of track-number tags and preserves the current selection.
|
||||
#[allow(dead_code)]
|
||||
fn resort_library(&mut self) {
|
||||
let selected_path = self
|
||||
.selected_track
|
||||
|
|
@ -474,27 +486,6 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
/// rolls onto the next track once the playhead reaches the end, pausing past the final entry.
|
||||
fn advance_if_finished(&mut self) {
|
||||
if !self.playing {
|
||||
return;
|
||||
}
|
||||
let Some(idx) = self.selected_track else { return };
|
||||
if self.position() < 0.999 {
|
||||
return;
|
||||
}
|
||||
let next = idx + 1;
|
||||
if next >= self.library.tracks.len() {
|
||||
self.playing = false;
|
||||
if let Some(eng) = &self.engine {
|
||||
eng.pause();
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.selected_track = Some(next);
|
||||
self.load_index(next);
|
||||
}
|
||||
|
||||
/// dispatches a UI message into the matching state mutation and worker call.
|
||||
pub fn update(&mut self, msg: Message) {
|
||||
match msg {
|
||||
|
|
@ -541,6 +532,7 @@ impl App {
|
|||
Message::SelectTrack(idx) => {
|
||||
if idx < self.library.tracks.len() {
|
||||
self.selected_track = Some(idx);
|
||||
self.playing = true;
|
||||
self.load_index(idx);
|
||||
}
|
||||
}
|
||||
|
|
@ -596,7 +588,7 @@ impl App {
|
|||
self.restore_settings_scroll = true;
|
||||
}
|
||||
}
|
||||
Message::SidebarScrolled(off) => self.sidebar_scroll = off,
|
||||
Message::NoOp => {}
|
||||
Message::SettingsScrolled(off) => self.settings_scroll = off,
|
||||
Message::SetGlass(on) => self.settings.glass = on,
|
||||
Message::SetEntropy(on) => self.settings.entropy_on = on,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ use super::theme::palette;
|
|||
pub const SIDEBAR_W: f32 = 280.0;
|
||||
pub const TOP_BAR_H: f32 = 44.0;
|
||||
pub const TRANSPORT_H: f32 = 72.0;
|
||||
const ROW_H: f32 = 56.0;
|
||||
pub const ROW_H: f32 = 56.0;
|
||||
pub const ROW_SPACING: f32 = 2.0;
|
||||
pub const SIDEBAR_PADDING_TOP: f32 = 8.0;
|
||||
const THUMB: f32 = 40.0;
|
||||
|
||||
/// stable id of the sidebar scrollable.
|
||||
|
|
@ -180,14 +182,13 @@ fn sidebar(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Renderer> {
|
|||
let selected = app.selected_track;
|
||||
|
||||
let inner = lazy(key, move |_| {
|
||||
let mut col = column![].spacing(2).padding(Padding::from([8, 8]));
|
||||
let mut col = column![]
|
||||
.spacing(ROW_SPACING)
|
||||
.padding(Padding::from([SIDEBAR_PADDING_TOP as u16, 8]));
|
||||
for (i, t) in tracks.iter().enumerate() {
|
||||
col = col.push(track_row_owned(i, t.clone(), selected == Some(i)));
|
||||
}
|
||||
scrollable(col)
|
||||
.id(sidebar_scroll_id())
|
||||
.on_scroll(|vp| Message::SidebarScrolled(vp.absolute_offset()))
|
||||
.height(Length::Fill)
|
||||
scrollable(col).id(sidebar_scroll_id()).height(Length::Fill)
|
||||
});
|
||||
|
||||
container(inner)
|
||||
|
|
@ -253,17 +254,11 @@ fn track_row_owned(
|
|||
.padding(0)
|
||||
.on_press(Message::SelectTrack(idx))
|
||||
.width(Length::Fill)
|
||||
.style(move |_t: &Theme, status: ButtonStatus| {
|
||||
let bg = match (active, status) {
|
||||
(true, _) => Some(Background::Color(Color {
|
||||
a: 0.20,
|
||||
..palette::accent()
|
||||
})),
|
||||
(false, ButtonStatus::Hovered) => Some(Background::Color(Color {
|
||||
a: 0.06,
|
||||
..palette::accent()
|
||||
})),
|
||||
_ => None,
|
||||
.style(move |_t: &Theme, _status: ButtonStatus| {
|
||||
let bg = if active {
|
||||
Some(Background::Color(Color { a: 0.20, ..palette::playing() }))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
button::Style {
|
||||
background: bg,
|
||||
|
|
@ -524,10 +519,13 @@ fn settings_overlay(app: &App) -> Element<'_, Message, Theme, iced_wgpu::Rendere
|
|||
.align_x(iced_wgpu::core::alignment::Horizontal::Right)
|
||||
.align_y(iced_wgpu::core::alignment::Vertical::Center);
|
||||
|
||||
let panel = container(stack![scroll, close])
|
||||
.width(Length::Fixed(SETTINGS_W))
|
||||
.height(Length::Fill)
|
||||
.style(settings_panel_style);
|
||||
let panel = mouse_area(
|
||||
container(stack![scroll, close])
|
||||
.width(Length::Fixed(SETTINGS_W))
|
||||
.height(Length::Fill)
|
||||
.style(settings_panel_style)
|
||||
)
|
||||
.on_press(Message::NoOp);
|
||||
|
||||
container(panel)
|
||||
.width(Length::Fill)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod palette {
|
|||
const TEXT_C: Color = Color::from_rgb(0.92, 0.92, 0.95);
|
||||
const TEXT_DIM_C: Color = Color::from_rgb(0.55, 0.55, 0.62);
|
||||
const ACCENT_C: Color = Color::from_rgb(0.78, 0.62, 0.95);
|
||||
const PLAYING_C: Color = Color::from_rgb(0.40, 0.70, 1.00);
|
||||
|
||||
pub fn bg() -> Color { BG_C }
|
||||
pub fn sidebar() -> Color { SIDEBAR_C }
|
||||
|
|
@ -17,6 +18,7 @@ pub mod palette {
|
|||
pub fn text() -> Color { TEXT_C }
|
||||
pub fn text_dim() -> Color { TEXT_DIM_C }
|
||||
pub fn accent() -> Color { ACCENT_C }
|
||||
pub fn playing() -> Color { PLAYING_C }
|
||||
}
|
||||
|
||||
/// supplies the clear color handed to the wgpu compositor each frame.
|
||||
|
|
|
|||
|
|
@ -206,17 +206,17 @@ impl ViewportHandle {
|
|||
let logical = self.viewport.logical_size();
|
||||
let h = logical.height;
|
||||
let w = logical.width;
|
||||
self.touch_in_sidebar = self.state.point_in_sidebar(x, y, h);
|
||||
let in_settings = self.state.point_in_settings(x, y, w);
|
||||
self.touch_in_sidebar = !in_settings && self.state.point_in_sidebar(x, y, h);
|
||||
self.touch_start = Some((x, y));
|
||||
self.touch_drift = 0.0;
|
||||
self.last_touch = Some((x, y));
|
||||
if self.touch_in_sidebar {
|
||||
self.push_mouse_move(x, y);
|
||||
self.settings_touch = SettingsTouch::None;
|
||||
} else if in_settings {
|
||||
if in_settings {
|
||||
self.settings_touch = SettingsTouch::Pending;
|
||||
self.push_mouse_move(x, y);
|
||||
} else if self.touch_in_sidebar {
|
||||
self.push_mouse_move(x, y);
|
||||
self.settings_touch = SettingsTouch::None;
|
||||
} else {
|
||||
self.settings_touch = SettingsTouch::None;
|
||||
self.push_mouse_button(x, y, 0, true);
|
||||
|
|
@ -245,11 +245,9 @@ impl ViewportHandle {
|
|||
match self.settings_touch {
|
||||
SettingsTouch::Scroll => {
|
||||
if dy.abs() > 0.0 {
|
||||
let p = Point::new(x, y);
|
||||
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
||||
}));
|
||||
self.cursor = mouse::Cursor::Available(p);
|
||||
}
|
||||
}
|
||||
SettingsTouch::Drag => {
|
||||
|
|
@ -257,13 +255,14 @@ impl ViewportHandle {
|
|||
}
|
||||
SettingsTouch::Pending => {}
|
||||
SettingsTouch::None => {
|
||||
self.push_mouse_move(x, y);
|
||||
if self.touch_in_sidebar && dy.abs() > 0.0 {
|
||||
let p = Point::new(x, y);
|
||||
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
||||
}));
|
||||
self.cursor = mouse::Cursor::Available(p);
|
||||
if self.touch_in_sidebar {
|
||||
if dy.abs() > 0.0 {
|
||||
self.events.push(Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Pixels { x: 0.0, y: dy },
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
self.push_mouse_move(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -597,13 +596,24 @@ fn render(handle: &mut ViewportHandle) {
|
|||
|
||||
/// drains scroll-restore flags from App state into a side-channel struct.
|
||||
fn take_pending_scroll_restores(state: &mut App) -> ScrollRestore {
|
||||
let r = ScrollRestore {
|
||||
sidebar: state.restore_sidebar_scroll.then_some(state.sidebar_scroll),
|
||||
settings: state.restore_settings_scroll.then_some(state.settings_scroll),
|
||||
let sidebar = if state.restore_sidebar_scroll {
|
||||
Some(sidebar_snap_offset(state.selected_track))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let settings = state.restore_settings_scroll.then_some(state.settings_scroll);
|
||||
state.restore_sidebar_scroll = false;
|
||||
state.restore_settings_scroll = false;
|
||||
r
|
||||
ScrollRestore { sidebar, settings }
|
||||
}
|
||||
|
||||
/// vertical offset placing the playing row at the top of the sidebar viewport.
|
||||
fn sidebar_snap_offset(selected: Option<usize>) -> AbsoluteOffset {
|
||||
let idx = selected.unwrap_or(0) as f32;
|
||||
AbsoluteOffset {
|
||||
x: 0.0,
|
||||
y: player::SIDEBAR_PADDING_TOP + idx * (player::ROW_H + player::ROW_SPACING),
|
||||
}
|
||||
}
|
||||
|
||||
/// pushes the stored sidebar and settings scroll offsets onto the UserInterface.
|
||||
|
|
|
|||
Loading…
Reference in New Issue