use std::sync::atomic::{AtomicU64, Ordering};
use kithara_bufpool::HasPool;
use kithara_events::TrackId;
#[cfg(test)]
use kithara_play::PlaybackShared;
pub use kithara_play::player::PlaybackView;
use kithara_play::{CrossfadeSettings, ResourceSrc, SelectionPlayback};
use crate::track::TrackSource;
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Transition {
None,
Crossfade,
CrossfadeWith { settings: CrossfadeSettings },
}
impl Transition {
#[must_use]
pub const fn settings(self, default: CrossfadeSettings) -> CrossfadeSettings {
match self {
Self::None => CrossfadeSettings {
duration: 0.0,
..default
},
Self::Crossfade => default,
Self::CrossfadeWith { settings } => settings,
}
}
}
#[derive(Clone, Copy, Debug)]
pub(super) struct PendingSelect {
pub(super) reason: crate::AdvanceReason,
pub(super) settings: CrossfadeSettings,
pub(super) playback: SelectionPlayback,
pub(super) id: TrackId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CrossfadeArm {
Disarmed,
Armed { for_track: TrackId },
}
impl CrossfadeArm {
pub(super) const fn armed(for_track: TrackId) -> Self {
Self::Armed { for_track }
}
pub(super) fn is_armed_for(self, id: TrackId) -> bool {
matches!(self, Self::Armed { for_track } if for_track == id)
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum SelectPhase {
Idle,
Pending(PendingSelect),
}
#[derive(Clone, Copy, Debug)]
pub(super) enum CachedPosition {
Unknown,
Known { seconds: f64 },
}
impl CachedPosition {
pub(super) const fn known(seconds: f64) -> Self {
if seconds.is_nan() {
Self::Unknown
} else {
Self::Known { seconds }
}
}
}
impl From<CachedPosition> for Option<f64> {
fn from(pos: CachedPosition) -> Self {
match pos {
CachedPosition::Known { seconds } => Some(seconds),
CachedPosition::Unknown => None,
}
}
}
pub(super) struct AtomicTrackId(AtomicU64);
impl AtomicTrackId {
const NONE_BITS: u64 = u64::MAX;
pub(super) fn arm_if_disarmed(&self, track: TrackId) -> bool {
self.0
.compare_exchange(
Self::NONE_BITS,
track.as_u64(),
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
}
const fn decode(bits: u64) -> CrossfadeArm {
if bits == Self::NONE_BITS {
CrossfadeArm::Disarmed
} else {
CrossfadeArm::Armed {
for_track: TrackId(bits),
}
}
}
pub(super) fn disarm_if_matches(&self, track: TrackId) -> bool {
self.0
.compare_exchange(
track.as_u64(),
Self::NONE_BITS,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
}
pub(super) const fn disarmed() -> Self {
Self(AtomicU64::new(Self::NONE_BITS))
}
const fn encode(arm: CrossfadeArm) -> u64 {
match arm {
CrossfadeArm::Disarmed => Self::NONE_BITS,
CrossfadeArm::Armed { for_track } => for_track.as_u64(),
}
}
pub(super) fn load(&self) -> CrossfadeArm {
Self::decode(self.0.load(Ordering::Acquire))
}
pub(super) fn store(&self, arm: CrossfadeArm) {
self.0.store(Self::encode(arm), Ordering::Release);
}
pub(super) fn take_if_matches(&self, track: TrackId) -> bool {
self.0
.compare_exchange(
track.as_u64(),
Self::NONE_BITS,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_ok()
}
}
pub(super) struct AtomicCachedPosition(AtomicU64);
impl AtomicCachedPosition {
pub(super) fn load(&self) -> CachedPosition {
let seconds = f64::from_bits(self.0.load(Ordering::Acquire));
if seconds.is_nan() {
CachedPosition::Unknown
} else {
CachedPosition::Known { seconds }
}
}
pub(super) fn store(&self, pos: CachedPosition) {
let bits = match pos {
CachedPosition::Unknown => f64::NAN.to_bits(),
CachedPosition::Known { seconds } => seconds.to_bits(),
};
self.0.store(bits, Ordering::Release);
}
pub(super) const fn unknown() -> Self {
Self(AtomicU64::new(f64::NAN.to_bits()))
}
}
#[derive(Clone, Copy, Debug)]
pub(super) enum Placement {
Append,
At(usize),
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct PlaybackTime {
pub(crate) dur: f64,
pub(crate) pos: f64,
}
pub(crate) fn should_arm_crossfade(
time: PlaybackTime,
crossfade: f32,
current_id: TrackId,
armed_for: CrossfadeArm,
) -> bool {
let PlaybackTime { pos, dur } = time;
crossfade > 0.0
&& dur > 0.0
&& pos > 0.0
&& dur - pos <= f64::from(crossfade)
&& !armed_for.is_armed_for(current_id)
}
pub(super) fn extract_track_name<S>(source: &TrackSource<S>) -> String
where
S: HasPool<u8> + HasPool<f32> + Send + Sync + 'static,
{
let raw = match source {
TrackSource::Uri(s) => s.as_str(),
TrackSource::Config(cfg) => return name_from_src(cfg.source()),
};
name_from_raw(raw)
}
fn name_from_src(src: &ResourceSrc) -> String {
match src {
ResourceSrc::Url(url) => {
let path = url.path();
name_from_raw(path)
}
ResourceSrc::Path(p) => p.file_name().map_or_else(
|| "Unknown".to_string(),
|n| n.to_string_lossy().into_owned(),
),
}
}
fn name_from_raw(s: &str) -> String {
s.rsplit('/')
.find(|seg| !seg.is_empty())
.unwrap_or("Unknown")
.to_string()
}
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering;
use kithara_test_utils::kithara;
use super::*;
#[kithara::test]
fn atomic_track_id_disarmed_loads_disarmed() {
let cell = AtomicTrackId::disarmed();
assert_eq!(cell.load(), CrossfadeArm::Disarmed);
}
#[kithara::test]
fn atomic_track_id_take_if_matches_only_disarms_matching_track() {
let cell = AtomicTrackId::disarmed();
cell.store(CrossfadeArm::armed(TrackId(7)));
assert_eq!(
cell.load(),
CrossfadeArm::Armed {
for_track: TrackId(7),
}
);
assert!(!cell.take_if_matches(TrackId(8)));
assert_eq!(
cell.load(),
CrossfadeArm::Armed {
for_track: TrackId(7),
}
);
assert!(cell.take_if_matches(TrackId(7)));
assert_eq!(cell.load(), CrossfadeArm::Disarmed);
}
#[kithara::test]
fn atomic_track_id_cas_arm_then_disarm() {
let cell = AtomicTrackId::disarmed();
cell.arm_if_disarmed(TrackId(3));
cell.arm_if_disarmed(TrackId(4));
assert_eq!(
cell.load(),
CrossfadeArm::Armed {
for_track: TrackId(3),
},
"a second arm must not replace the armed track"
);
assert!(!cell.disarm_if_matches(TrackId(4)));
assert!(cell.disarm_if_matches(TrackId(3)));
assert_eq!(cell.load(), CrossfadeArm::Disarmed);
}
#[kithara::test]
fn atomic_cached_position_unknown_loads_none() {
let cell = AtomicCachedPosition::unknown();
assert_eq!(Option::<f64>::from(cell.load()), None);
}
#[kithara::test]
fn atomic_cached_position_round_trip_zero() {
let cell = AtomicCachedPosition::unknown();
cell.store(CachedPosition::known(0.0));
assert_eq!(Option::<f64>::from(cell.load()), Some(0.0));
}
#[kithara::test]
fn cached_position_known_nan_canonicalises_to_unknown() {
assert!(matches!(
CachedPosition::known(f64::NAN),
CachedPosition::Unknown
));
}
fn view_of(frontier: f64, cached: f64) -> PlaybackView {
let shared = PlaybackShared::default();
shared.duration.store(200.0, Ordering::Relaxed);
shared.frontier.store(frontier, Ordering::Relaxed);
shared.cached.store(cached, Ordering::Relaxed);
PlaybackView::from(shared.snapshot())
}
#[kithara::test]
fn buffered_covers_the_cached_span() {
assert_eq!(view_of(4.0, 120.0).buffered, Some(120.0));
}
#[kithara::test]
fn buffered_never_falls_behind_the_decoded_frontier() {
assert_eq!(view_of(90.0, 12.0).buffered, Some(90.0));
}
#[kithara::test]
fn buffered_is_zero_when_nothing_is_available() {
assert_eq!(view_of(0.0, 0.0).buffered, Some(0.0));
}
#[kithara::test]
fn crossfade_arm_is_armed_for_matches_track() {
let arm = CrossfadeArm::armed(TrackId(2));
assert!(arm.is_armed_for(TrackId(2)));
assert!(!arm.is_armed_for(TrackId(3)));
}
#[kithara::test]
fn select_phase_pending_carries_captured_policy() {
let phase = SelectPhase::Pending(PendingSelect {
id: TrackId(5),
settings: CrossfadeSettings {
duration: 0.0,
..CrossfadeSettings::default()
},
playback: SelectionPlayback::Play,
reason: crate::AdvanceReason::UserSelect,
});
match phase {
SelectPhase::Pending(p) => {
assert_eq!(p.id, TrackId(5));
assert_eq!(p.settings.duration, 0.0);
assert_eq!(p.playback, SelectionPlayback::Play);
assert_eq!(p.reason, crate::AdvanceReason::UserSelect);
}
SelectPhase::Idle => panic!("expected Pending"),
}
}
}