use derive_more::IsVariant;
use indexmap::IndexMap;
use mediaframe::{
audio::{
BitRateMode, ChannelLayout, CoverArt, Fingerprint, Loudness, ReplayGain, SampleFormat, Tags,
},
codec::AudioCodec,
disposition::TrackDisposition,
lang::Language,
};
use mediatime::Timestamp;
use smol_str::SmolStr;
use crate::domain::{
bitflags::AudioIndexStatus, enums::AudioContentKind, primitives::ErrorInfo, vo::Provenance, Uuid7,
};
#[inline]
const fn is_valid_ratio(v: Option<f32>) -> bool {
match v {
None => true,
Some(r) => r.is_finite() && r >= 0.0 && r <= 1.0,
}
}
#[inline]
const fn is_negative_duration(d: Option<Timestamp>) -> bool {
match d {
None => false,
Some(ts) => ts.pts() < 0,
}
}
#[inline]
const fn status_asserts_descriptor(s: AudioIndexStatus) -> bool {
!s.is_empty()
}
const fn validate_status_topology(s: AudioIndexStatus) -> Result<(), AudioTrackError> {
use AudioIndexStatus as S;
if s.bits() & !S::all().bits() != 0 {
return Err(AudioTrackError::UnknownStatusBits);
}
if s.is_empty() {
return Ok(());
}
if !s.contains(S::EXTRACTED) {
return Err(AudioTrackError::StatusOutOfOrder);
}
if s.contains(S::STT_DONE) && !s.intersects(S::CLASSIFIED.union(S::VAD_DONE)) {
return Err(AudioTrackError::StatusOutOfOrder);
}
if s.contains(S::SPEAKER_DONE) && !s.contains(S::STT_DONE) {
return Err(AudioTrackError::StatusOutOfOrder);
}
if s.contains(S::TEXT_EMBED) && !s.contains(S::SPEAKER_DONE) {
return Err(AudioTrackError::StatusOutOfOrder);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub struct AudioTrack<Id = Uuid7> {
id: Id,
audio_id: Id,
stream_index: Option<u32>,
container_track_id: Option<u64>,
codec: AudioCodec,
profile: SmolStr,
sample_rate: u32,
channels: u16,
channel_layout: ChannelLayout,
sample_format: SampleFormat,
bit_rate: u64,
bit_rate_mode: Option<BitRateMode>,
bits_per_sample: Option<u16>,
is_lossless: bool,
duration: Option<Timestamp>,
start_pts: Option<Timestamp>,
language: Option<Language>,
detected_language: Option<Language>,
disposition: TrackDisposition,
is_primary: bool,
auto_selected: bool,
content: Option<AudioContentKind>,
speech_ratio: Option<f32>,
is_silent: bool,
loudness: Option<Loudness>,
replay_gain: Option<ReplayGain>,
fingerprint: Option<Fingerprint>,
isrc: SmolStr,
acoustid: SmolStr,
musicbrainz_recording_id: SmolStr,
speakers: std::vec::Vec<Id>,
tags: Option<Tags>,
cover_art: Option<CoverArt>,
segments: std::vec::Vec<Id>,
metadata: IndexMap<SmolStr, SmolStr>,
provenance: Provenance,
index_status: AudioIndexStatus,
index_errors: std::vec::Vec<ErrorInfo>,
}
impl AudioTrack<Uuid7> {
pub fn try_new(id: Uuid7, audio_id: Uuid7) -> Result<Self, AudioTrackError> {
if id.is_nil() {
return Err(AudioTrackError::NilId);
}
if audio_id.is_nil() {
return Err(AudioTrackError::NilAudioId);
}
Ok(Self {
id,
audio_id,
stream_index: None,
container_track_id: None,
codec: AudioCodec::Other(SmolStr::default()),
profile: SmolStr::default(),
sample_rate: 0,
channels: 0,
channel_layout: ChannelLayout::default(),
sample_format: SampleFormat::default(),
bit_rate: 0,
bit_rate_mode: None,
bits_per_sample: None,
is_lossless: false,
duration: None,
start_pts: None,
language: None,
detected_language: None,
disposition: TrackDisposition::empty(),
is_primary: false,
auto_selected: false,
content: None,
speech_ratio: None,
is_silent: false,
loudness: None,
replay_gain: None,
fingerprint: None,
isrc: SmolStr::default(),
acoustid: SmolStr::default(),
musicbrainz_recording_id: SmolStr::default(),
speakers: std::vec::Vec::new(),
tags: None,
cover_art: None,
segments: std::vec::Vec::new(),
metadata: IndexMap::new(),
provenance: Provenance::new(),
index_status: AudioIndexStatus::empty(),
index_errors: std::vec::Vec::new(),
})
}
}
impl<Id> AudioTrack<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn audio_id_ref(&self) -> &Id {
&self.audio_id
}
#[inline(always)]
pub const fn stream_index(&self) -> Option<u32> {
self.stream_index
}
#[inline(always)]
pub const fn container_track_id(&self) -> Option<u64> {
self.container_track_id
}
#[inline(always)]
pub const fn codec_ref(&self) -> &AudioCodec {
&self.codec
}
#[inline(always)]
pub fn profile(&self) -> &str {
self.profile.as_str()
}
#[inline(always)]
pub const fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[inline(always)]
pub const fn channels(&self) -> u16 {
self.channels
}
#[inline(always)]
pub const fn channel_layout_ref(&self) -> &ChannelLayout {
&self.channel_layout
}
#[inline(always)]
pub const fn sample_format_ref(&self) -> &SampleFormat {
&self.sample_format
}
#[inline(always)]
pub const fn bit_rate(&self) -> u64 {
self.bit_rate
}
#[inline(always)]
pub const fn bit_rate_mode(&self) -> Option<BitRateMode> {
self.bit_rate_mode
}
#[inline(always)]
pub const fn bits_per_sample(&self) -> Option<u16> {
self.bits_per_sample
}
#[inline(always)]
pub const fn is_lossless(&self) -> bool {
self.is_lossless
}
#[inline(always)]
pub const fn duration_ref(&self) -> Option<&Timestamp> {
self.duration.as_ref()
}
#[inline(always)]
pub const fn start_pts_ref(&self) -> Option<&Timestamp> {
self.start_pts.as_ref()
}
#[inline(always)]
pub const fn language(&self) -> Option<Language> {
self.language
}
#[inline(always)]
pub const fn detected_language(&self) -> Option<Language> {
self.detected_language
}
#[inline]
pub fn language_mismatch(&self) -> bool {
match (self.language, self.detected_language) {
(Some(declared), Some(detected)) => declared != detected,
_ => false,
}
}
#[inline(always)]
pub const fn disposition(&self) -> TrackDisposition {
self.disposition
}
#[inline(always)]
pub const fn is_primary(&self) -> bool {
self.is_primary
}
#[inline(always)]
pub const fn auto_selected(&self) -> bool {
self.auto_selected
}
#[inline(always)]
pub const fn content(&self) -> Option<AudioContentKind> {
self.content
}
#[inline(always)]
pub const fn speech_ratio(&self) -> Option<f32> {
self.speech_ratio
}
#[inline(always)]
pub const fn is_silent(&self) -> bool {
self.is_silent
}
#[inline(always)]
pub const fn loudness_ref(&self) -> Option<&Loudness> {
self.loudness.as_ref()
}
#[inline(always)]
pub const fn replay_gain_ref(&self) -> Option<&ReplayGain> {
self.replay_gain.as_ref()
}
#[inline(always)]
pub const fn fingerprint_ref(&self) -> Option<&Fingerprint> {
self.fingerprint.as_ref()
}
#[inline(always)]
pub fn isrc(&self) -> &str {
self.isrc.as_str()
}
#[inline(always)]
pub fn acoustid(&self) -> &str {
self.acoustid.as_str()
}
#[inline(always)]
pub fn musicbrainz_recording_id(&self) -> &str {
self.musicbrainz_recording_id.as_str()
}
#[inline(always)]
pub const fn speakers_slice(&self) -> &[Id] {
self.speakers.as_slice()
}
#[inline(always)]
pub const fn tags_ref(&self) -> Option<&Tags> {
self.tags.as_ref()
}
#[inline(always)]
pub const fn cover_art_ref(&self) -> Option<&CoverArt> {
self.cover_art.as_ref()
}
#[inline(always)]
pub const fn metadata_ref(&self) -> &IndexMap<SmolStr, SmolStr> {
&self.metadata
}
#[inline(always)]
pub const fn segments_slice(&self) -> &[Id] {
self.segments.as_slice()
}
#[inline(always)]
pub const fn provenance_ref(&self) -> &Provenance {
&self.provenance
}
#[inline(always)]
pub const fn index_status(&self) -> AudioIndexStatus {
self.index_status
}
#[inline(always)]
pub fn index_errors_slice(&self) -> &[ErrorInfo] {
self.index_errors.as_slice()
}
#[inline(always)]
#[must_use]
pub const fn with_stream_index(mut self, v: Option<u32>) -> Self {
self.stream_index = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_container_track_id(mut self, v: Option<u64>) -> Self {
self.container_track_id = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_codec(mut self, v: AudioCodec) -> Self {
self.codec = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_profile(mut self, v: impl Into<SmolStr>) -> Self {
self.profile = v.into();
self
}
#[inline]
pub fn try_with_sample_rate(mut self, hz: u32) -> Result<Self, AudioTrackError> {
if hz == 0 && status_asserts_descriptor(self.index_status) {
return Err(AudioTrackError::ProbedDescriptorCleared);
}
self.sample_rate = hz;
Ok(self)
}
#[inline]
pub fn try_with_channels(mut self, channels: u16) -> Result<Self, AudioTrackError> {
if channels == 0 && status_asserts_descriptor(self.index_status) {
return Err(AudioTrackError::ProbedDescriptorCleared);
}
self.channels = channels;
Ok(self)
}
#[inline(always)]
#[must_use]
pub fn with_channel_layout(mut self, v: ChannelLayout) -> Self {
self.channel_layout = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_sample_format(mut self, v: SampleFormat) -> Self {
self.sample_format = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_bit_rate(mut self, bps: u64) -> Self {
self.bit_rate = bps;
self
}
#[inline(always)]
#[must_use]
pub const fn with_bit_rate_mode(mut self, v: Option<BitRateMode>) -> Self {
self.bit_rate_mode = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_bits_per_sample(mut self, v: Option<u16>) -> Self {
self.bits_per_sample = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_lossless(mut self, v: bool) -> Self {
self.is_lossless = v;
self
}
#[inline]
pub fn try_with_duration(mut self, v: Option<Timestamp>) -> Result<Self, AudioTrackError> {
if is_negative_duration(v) {
return Err(AudioTrackError::NegativeDuration);
}
self.duration = v;
Ok(self)
}
#[inline(always)]
#[must_use]
pub fn with_start_pts(mut self, v: Option<Timestamp>) -> Self {
self.start_pts = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_language(mut self, v: Option<Language>) -> Self {
self.language = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_detected_language(mut self, v: Option<Language>) -> Self {
self.detected_language = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_disposition(mut self, v: TrackDisposition) -> Self {
self.disposition = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_primary(mut self, v: bool) -> Self {
self.is_primary = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_auto_selected(mut self, v: bool) -> Self {
self.auto_selected = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_content(mut self, v: Option<AudioContentKind>) -> Self {
self.content = v;
self
}
#[inline]
pub fn try_with_speech_ratio(mut self, v: Option<f32>) -> Result<Self, AudioTrackError> {
if !is_valid_ratio(v) {
return Err(AudioTrackError::SpeechRatioOutOfRange);
}
self.speech_ratio = v;
Ok(self)
}
#[inline(always)]
#[must_use]
pub const fn with_silent(mut self, v: bool) -> Self {
self.is_silent = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_loudness(mut self, v: Option<Loudness>) -> Self {
self.loudness = v;
self
}
#[inline(always)]
#[must_use]
pub const fn with_replay_gain(mut self, v: Option<ReplayGain>) -> Self {
self.replay_gain = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_fingerprint(mut self, v: Option<Fingerprint>) -> Self {
self.fingerprint = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_isrc(mut self, v: impl Into<SmolStr>) -> Self {
self.isrc = v.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_acoustid(mut self, v: impl Into<SmolStr>) -> Self {
self.acoustid = v.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_musicbrainz_recording_id(mut self, v: impl Into<SmolStr>) -> Self {
self.musicbrainz_recording_id = v.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_tags(mut self, v: Option<Tags>) -> Self {
self.tags = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_cover_art(mut self, v: Option<CoverArt>) -> Self {
self.cover_art = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_metadata(mut self, v: IndexMap<SmolStr, SmolStr>) -> Self {
self.metadata = v;
self
}
#[inline(always)]
pub fn set_sample_format(&mut self, v: SampleFormat) -> &mut Self {
self.sample_format = v;
self
}
#[inline(always)]
pub fn set_metadata(&mut self, v: IndexMap<SmolStr, SmolStr>) -> &mut Self {
self.metadata = v;
self
}
#[inline(always)]
#[must_use]
pub fn with_speakers(mut self, v: impl Into<std::vec::Vec<Id>>) -> Self {
self.speakers = v.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_segments(mut self, v: impl Into<std::vec::Vec<Id>>) -> Self {
self.segments = v.into();
self
}
#[inline(always)]
#[must_use]
pub fn with_provenance(mut self, v: Provenance) -> Self {
self.provenance = v;
self
}
#[inline]
pub fn try_with_index_status(mut self, v: AudioIndexStatus) -> Result<Self, AudioTrackError> {
validate_status_topology(v)?;
if status_asserts_descriptor(v) && (self.sample_rate == 0 || self.channels == 0) {
return Err(AudioTrackError::ExtractedWithoutDescriptor);
}
self.index_status = v;
Ok(self)
}
#[inline(always)]
#[must_use]
pub fn with_index_errors(mut self, v: impl Into<std::vec::Vec<ErrorInfo>>) -> Self {
self.index_errors = v.into();
self
}
#[inline(always)]
pub const fn set_stream_index(&mut self, v: Option<u32>) -> &mut Self {
self.stream_index = v;
self
}
#[inline(always)]
pub const fn set_container_track_id(&mut self, v: Option<u64>) -> &mut Self {
self.container_track_id = v;
self
}
#[inline(always)]
pub fn set_codec(&mut self, v: AudioCodec) -> &mut Self {
self.codec = v;
self
}
#[inline(always)]
pub fn set_profile(&mut self, v: impl Into<SmolStr>) -> &mut Self {
self.profile = v.into();
self
}
#[inline]
pub const fn try_set_sample_rate(&mut self, hz: u32) -> Result<&mut Self, AudioTrackError> {
if hz == 0 && status_asserts_descriptor(self.index_status) {
return Err(AudioTrackError::ProbedDescriptorCleared);
}
self.sample_rate = hz;
Ok(self)
}
#[inline]
pub const fn try_set_channels(&mut self, channels: u16) -> Result<&mut Self, AudioTrackError> {
if channels == 0 && status_asserts_descriptor(self.index_status) {
return Err(AudioTrackError::ProbedDescriptorCleared);
}
self.channels = channels;
Ok(self)
}
#[inline(always)]
pub fn set_channel_layout(&mut self, v: ChannelLayout) -> &mut Self {
self.channel_layout = v;
self
}
#[inline(always)]
pub const fn set_bit_rate(&mut self, bps: u64) -> &mut Self {
self.bit_rate = bps;
self
}
#[inline(always)]
pub const fn set_bit_rate_mode(&mut self, v: Option<BitRateMode>) -> &mut Self {
self.bit_rate_mode = v;
self
}
#[inline(always)]
pub const fn set_bits_per_sample(&mut self, v: Option<u16>) -> &mut Self {
self.bits_per_sample = v;
self
}
#[inline(always)]
pub const fn set_lossless(&mut self, v: bool) -> &mut Self {
self.is_lossless = v;
self
}
#[inline]
pub const fn try_set_duration(
&mut self,
v: Option<Timestamp>,
) -> Result<&mut Self, AudioTrackError> {
if is_negative_duration(v) {
return Err(AudioTrackError::NegativeDuration);
}
self.duration = v;
Ok(self)
}
#[inline(always)]
pub fn set_start_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
self.start_pts = v;
self
}
#[inline(always)]
pub const fn set_language(&mut self, v: Option<Language>) -> &mut Self {
self.language = v;
self
}
#[inline(always)]
pub const fn set_detected_language(&mut self, v: Option<Language>) -> &mut Self {
self.detected_language = v;
self
}
#[inline(always)]
pub const fn set_disposition(&mut self, v: TrackDisposition) -> &mut Self {
self.disposition = v;
self
}
#[inline(always)]
pub const fn set_primary(&mut self, v: bool) -> &mut Self {
self.is_primary = v;
self
}
#[inline(always)]
pub const fn set_auto_selected(&mut self, v: bool) -> &mut Self {
self.auto_selected = v;
self
}
#[inline(always)]
pub const fn set_content(&mut self, v: Option<AudioContentKind>) -> &mut Self {
self.content = v;
self
}
#[inline]
pub const fn try_set_speech_ratio(
&mut self,
v: Option<f32>,
) -> Result<&mut Self, AudioTrackError> {
if !is_valid_ratio(v) {
return Err(AudioTrackError::SpeechRatioOutOfRange);
}
self.speech_ratio = v;
Ok(self)
}
#[inline(always)]
pub const fn set_silent(&mut self, v: bool) -> &mut Self {
self.is_silent = v;
self
}
#[inline(always)]
pub const fn set_loudness(&mut self, v: Option<Loudness>) -> &mut Self {
self.loudness = v;
self
}
#[inline(always)]
pub const fn set_replay_gain(&mut self, v: Option<ReplayGain>) -> &mut Self {
self.replay_gain = v;
self
}
#[inline(always)]
pub fn set_fingerprint(&mut self, v: Option<Fingerprint>) -> &mut Self {
self.fingerprint = v;
self
}
#[inline(always)]
pub fn set_isrc(&mut self, v: impl Into<SmolStr>) -> &mut Self {
self.isrc = v.into();
self
}
#[inline(always)]
pub fn set_acoustid(&mut self, v: impl Into<SmolStr>) -> &mut Self {
self.acoustid = v.into();
self
}
#[inline(always)]
pub fn set_musicbrainz_recording_id(&mut self, v: impl Into<SmolStr>) -> &mut Self {
self.musicbrainz_recording_id = v.into();
self
}
#[inline(always)]
pub fn set_tags(&mut self, v: Option<Tags>) -> &mut Self {
self.tags = v;
self
}
#[inline(always)]
pub fn set_cover_art(&mut self, v: Option<CoverArt>) -> &mut Self {
self.cover_art = v;
self
}
#[inline(always)]
pub fn set_speakers(&mut self, v: impl Into<std::vec::Vec<Id>>) -> &mut Self {
self.speakers = v.into();
self
}
#[inline(always)]
pub fn set_segments(&mut self, v: impl Into<std::vec::Vec<Id>>) -> &mut Self {
self.segments = v.into();
self
}
#[inline(always)]
pub fn set_provenance(&mut self, v: Provenance) -> &mut Self {
self.provenance = v;
self
}
#[inline]
pub const fn try_set_index_status(
&mut self,
v: AudioIndexStatus,
) -> Result<&mut Self, AudioTrackError> {
if let Err(e) = validate_status_topology(v) {
return Err(e);
}
if status_asserts_descriptor(v) && (self.sample_rate == 0 || self.channels == 0) {
return Err(AudioTrackError::ExtractedWithoutDescriptor);
}
self.index_status = v;
Ok(self)
}
#[inline(always)]
pub fn set_index_errors(&mut self, v: impl Into<std::vec::Vec<ErrorInfo>>) -> &mut Self {
self.index_errors = v.into();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum AudioTrackError {
#[error("AudioTrack id must not be the nil UUID")]
NilId,
#[error("AudioTrack `audio_id` (FK → Audio) must not be the nil UUID")]
NilAudioId,
#[error("AudioTrack speech_ratio must be finite and within [0, 1]")]
SpeechRatioOutOfRange,
#[error("AudioTrack index_status reached EXTRACTED while sample_rate/channels are still 0")]
ExtractedWithoutDescriptor,
#[error("AudioTrack sample_rate/channels cannot be cleared to 0 once the track is EXTRACTED")]
ProbedDescriptorCleared,
#[error(
"AudioTrack index_status mask is out of order: a stage bit is set without its prerequisites"
)]
StatusOutOfOrder,
#[error("AudioTrack index_status mask contains unknown bits outside AudioIndexStatus")]
UnknownStatusBits,
#[error("AudioTrack duration must not be negative")]
NegativeDuration,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::ErrorCode;
#[test]
fn try_new_happy_path() {
let audio_id = Uuid7::new();
let t = AudioTrack::try_new(Uuid7::new(), audio_id).expect("valid construction must succeed");
assert_eq!(t.audio_id_ref(), &audio_id);
assert_eq!(t.sample_rate(), 0);
assert_eq!(t.channels(), 0);
assert!(t.codec_ref().as_str().is_empty());
assert!(t.tags_ref().is_none());
assert!(t.cover_art_ref().is_none());
assert!(t.speakers_slice().is_empty());
assert!(t.segments_slice().is_empty());
assert_eq!(t.index_status(), AudioIndexStatus::empty());
assert!(t.provenance_ref().is_empty());
}
#[test]
fn try_new_rejects_nil_id() {
let r = AudioTrack::try_new(Uuid7::nil(), Uuid7::new());
assert_eq!(r.err(), Some(AudioTrackError::NilId));
assert!(AudioTrackError::NilId.is_nil_id());
}
#[test]
fn try_new_rejects_nil_audio_id() {
let r = AudioTrack::try_new(Uuid7::new(), Uuid7::nil());
assert_eq!(r.err(), Some(AudioTrackError::NilAudioId));
assert!(AudioTrackError::NilAudioId.is_nil_audio_id());
}
#[test]
fn descriptor_builders_chain() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_codec(AudioCodec::Aac)
.with_profile("LC")
.try_with_sample_rate(48_000)
.unwrap()
.try_with_channels(2)
.unwrap()
.with_channel_layout(ChannelLayout::Stereo)
.with_bit_rate(192_000)
.with_lossless(false)
.with_primary(true);
assert_eq!(t.codec_ref(), &AudioCodec::Aac);
assert_eq!(t.codec_ref().as_str(), "aac");
assert_eq!(t.profile(), "LC");
assert_eq!(t.sample_rate(), 48_000);
assert_eq!(t.channels(), 2);
assert_eq!(t.channel_layout_ref(), &ChannelLayout::Stereo);
assert_eq!(t.channel_layout_ref().as_str(), "stereo");
assert_eq!(t.bit_rate(), 192_000);
assert!(!t.is_lossless());
assert!(t.is_primary());
}
#[test]
fn tags_and_cover_art_attach() {
let tags = Tags::new()
.with_title("Track 1")
.with_artist("Artist A")
.with_album("Album X")
.with_track_number(1)
.with_track_total(12);
let cover = CoverArt::try_new("image/jpeg", std::vec![0xFFu8, 0xD8, 0xFF]).unwrap();
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_tags(Some(tags))
.with_cover_art(Some(cover));
let tags = t.tags_ref().expect("tags attached");
assert_eq!(tags.title(), "Track 1");
assert_eq!(tags.artist(), "Artist A");
assert_eq!(tags.track_number(), 1);
assert_eq!(tags.track_total(), 12);
let cover = t.cover_art_ref().expect("cover attached");
assert_eq!(cover.mime(), "image/jpeg");
assert_eq!(cover.data(), &[0xFFu8, 0xD8, 0xFF]);
}
#[test]
fn loudness_and_fingerprint_attach() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_loudness(Some(Loudness::new(-23.0, 7.5, -1.0, -3.0)))
.with_fingerprint(Some(
Fingerprint::try_new("chromaprint", std::vec![1u8, 2, 3, 4]).unwrap(),
));
let l = t.loudness_ref().expect("loudness present");
assert!((l.integrated_lufs() - -23.0).abs() < f32::EPSILON);
assert!((l.true_peak_dbtp() - -1.0).abs() < f32::EPSILON);
assert!((l.range_lu() - 7.5).abs() < f32::EPSILON);
let fp = t.fingerprint_ref().expect("fingerprint present");
assert_eq!(fp.algorithm(), "chromaprint");
assert_eq!(fp.value(), &[1u8, 2, 3, 4]);
}
#[test]
fn provenance_is_per_track() {
let prov = Provenance::from_parts("asry", "1.2.3", "v0", "indexer-0.4");
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_provenance(prov.clone());
assert_eq!(t.provenance_ref(), &prov);
assert_eq!(t.provenance_ref().model_name(), "asry");
}
#[test]
fn index_status_and_errors_roundtrip() {
let err = ErrorInfo::new(ErrorCode::ProbeCorrupt, "could not probe");
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_sample_rate(48_000)
.unwrap()
.try_with_channels(2)
.unwrap()
.try_with_index_status(AudioIndexStatus::EXTRACTED | AudioIndexStatus::VAD_DONE)
.unwrap()
.with_index_errors(std::vec![err.clone()]);
assert!(t.index_status().contains(AudioIndexStatus::EXTRACTED));
assert!(t.index_status().contains(AudioIndexStatus::VAD_DONE));
assert_eq!(t.index_errors_slice().len(), 1);
assert_eq!(t.index_errors_slice()[0], err);
}
#[test]
fn speakers_and_segments_lists() {
let s1 = Uuid7::new();
let g1 = Uuid7::new();
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.with_speakers(std::vec![s1])
.with_segments(std::vec![g1]);
assert_eq!(t.speakers_slice(), &[s1]);
assert_eq!(t.segments_slice(), &[g1]);
}
#[test]
fn setters_mutate_in_place() {
let s1 = Uuid7::new();
let g1 = Uuid7::new();
let mut t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
t.set_codec(AudioCodec::Opus);
t.try_set_sample_rate(48_000).unwrap();
t.try_set_channels(2).unwrap();
t.set_lossless(false);
t.set_silent(true);
t.set_content(Some(AudioContentKind::Music));
t.set_speakers(std::vec![s1]);
t.set_segments(std::vec![g1]);
t.try_set_index_status(AudioIndexStatus::EXTRACTED).unwrap();
assert_eq!(t.codec_ref(), &AudioCodec::Opus);
assert_eq!(t.sample_rate(), 48_000);
assert_eq!(t.channels(), 2);
assert!(!t.is_lossless());
assert!(t.is_silent());
assert_eq!(t.content(), Some(AudioContentKind::Music));
assert_eq!(t.speakers_slice(), &[s1]);
assert_eq!(t.segments_slice(), &[g1]);
assert_eq!(t.index_status(), AudioIndexStatus::EXTRACTED);
}
#[test]
fn try_with_speech_ratio_rejects_non_finite_or_out_of_range() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.01, 1.5] {
let r = t.clone().try_with_speech_ratio(Some(bad));
assert_eq!(r.err(), Some(AudioTrackError::SpeechRatioOutOfRange));
}
assert!(t.clone().try_with_speech_ratio(Some(0.0)).is_ok());
assert!(t.clone().try_with_speech_ratio(Some(1.0)).is_ok());
assert!(t.clone().try_with_speech_ratio(None).is_ok());
let t = t.try_with_speech_ratio(Some(0.6)).unwrap();
assert!((t.speech_ratio().unwrap() - 0.6).abs() < f32::EPSILON);
assert!(AudioTrackError::SpeechRatioOutOfRange.is_speech_ratio_out_of_range());
}
#[test]
fn try_set_speech_ratio_rejects_and_leaves_value_unchanged() {
let mut t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
t.try_set_speech_ratio(Some(0.4)).unwrap();
assert_eq!(
t.try_set_speech_ratio(Some(f32::NAN)).err(),
Some(AudioTrackError::SpeechRatioOutOfRange)
);
assert!((t.speech_ratio().unwrap() - 0.4).abs() < f32::EPSILON);
assert_eq!(
t.try_set_speech_ratio(Some(2.0)).err(),
Some(AudioTrackError::SpeechRatioOutOfRange)
);
assert!((t.speech_ratio().unwrap() - 0.4).abs() < f32::EPSILON);
t.try_set_speech_ratio(None).unwrap();
assert!(t.speech_ratio().is_none());
}
#[test]
fn extracted_status_rejected_without_descriptor() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert_eq!(
t.clone()
.try_with_index_status(AudioIndexStatus::EXTRACTED)
.err(),
Some(AudioTrackError::ExtractedWithoutDescriptor)
);
assert_eq!(
t.clone()
.try_with_index_status(
AudioIndexStatus::EXTRACTED | AudioIndexStatus::VAD_DONE | AudioIndexStatus::STT_DONE
)
.err(),
Some(AudioTrackError::ExtractedWithoutDescriptor)
);
let half = t.try_with_sample_rate(48_000).unwrap();
assert_eq!(
half
.try_with_index_status(AudioIndexStatus::EXTRACTED)
.err(),
Some(AudioTrackError::ExtractedWithoutDescriptor)
);
assert!(AudioTrackError::ExtractedWithoutDescriptor.is_extracted_without_descriptor());
}
#[test]
fn extracted_status_accepted_with_descriptor() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_sample_rate(48_000)
.unwrap()
.try_with_channels(2)
.unwrap()
.try_with_index_status(AudioIndexStatus::EXTRACTED)
.unwrap();
assert_eq!(t.index_status(), AudioIndexStatus::EXTRACTED);
let mut fresh = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
fresh
.try_set_index_status(AudioIndexStatus::empty())
.unwrap();
}
#[test]
fn clearing_descriptor_on_extracted_track_rejected() {
let mut t = AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_sample_rate(48_000)
.unwrap()
.try_with_channels(2)
.unwrap()
.try_with_index_status(AudioIndexStatus::EXTRACTED)
.unwrap();
assert_eq!(
t.try_set_sample_rate(0).err(),
Some(AudioTrackError::ProbedDescriptorCleared)
);
assert_eq!(t.sample_rate(), 48_000);
assert_eq!(
t.try_set_channels(0).err(),
Some(AudioTrackError::ProbedDescriptorCleared)
);
assert_eq!(t.channels(), 2);
assert_eq!(
t.clone().try_with_sample_rate(0).err(),
Some(AudioTrackError::ProbedDescriptorCleared)
);
t.try_set_sample_rate(44_100).unwrap();
assert_eq!(t.sample_rate(), 44_100);
let mut fresh = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
fresh.try_set_sample_rate(0).unwrap();
fresh.try_set_channels(0).unwrap();
assert!(AudioTrackError::ProbedDescriptorCleared.is_probed_descriptor_cleared());
}
fn probed_track() -> AudioTrack<Uuid7> {
AudioTrack::try_new(Uuid7::new(), Uuid7::new())
.unwrap()
.try_with_sample_rate(48_000)
.unwrap()
.try_with_channels(2)
.unwrap()
}
#[test]
fn index_status_rejects_stage_bit_without_prerequisites() {
use AudioIndexStatus as S;
let t = probed_track();
assert_eq!(
t.clone().try_with_index_status(S::STT_DONE).err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert_eq!(
t.clone()
.try_with_index_status(S::EXTRACTED | S::STT_DONE)
.err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert_eq!(
t.clone()
.try_with_index_status(S::EXTRACTED | S::VAD_DONE | S::STT_DONE | S::TEXT_EMBED)
.err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert_eq!(
t.clone()
.try_with_index_status(S::EXTRACTED | S::VAD_DONE | S::SPEAKER_DONE)
.err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert_eq!(
t.try_with_index_status(S::FPRINT_DONE).err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert!(AudioTrackError::StatusOutOfOrder.is_status_out_of_order());
}
#[test]
fn index_status_set_rejects_out_of_order_and_leaves_value_unchanged() {
use AudioIndexStatus as S;
let mut t = probed_track();
t.try_set_index_status(S::EXTRACTED | S::VAD_DONE).unwrap();
assert_eq!(
t.try_set_index_status(S::STT_DONE).err(),
Some(AudioTrackError::StatusOutOfOrder)
);
assert_eq!(t.index_status(), S::EXTRACTED | S::VAD_DONE);
}
#[test]
fn index_status_accepts_valid_contiguous_masks() {
use AudioIndexStatus as S;
let t = probed_track();
for mask in [
S::empty(),
S::EXTRACTED,
S::EXTRACTED | S::CLASSIFIED,
S::EXTRACTED | S::VAD_DONE,
S::EXTRACTED | S::CLASSIFIED | S::VAD_DONE | S::STT_DONE,
S::EXTRACTED | S::VAD_DONE | S::STT_DONE | S::SPEAKER_DONE,
S::EXTRACTED | S::VAD_DONE | S::STT_DONE | S::SPEAKER_DONE | S::TEXT_EMBED,
S::fully_indexed_mask(),
] {
assert!(
t.clone().try_with_index_status(mask).is_ok(),
"valid contiguous mask {mask:?} must be accepted"
);
}
}
fn unknown_bit() -> AudioIndexStatus {
let b = AudioIndexStatus::from_bits_retain(0x800);
assert!(
b.bits() & !AudioIndexStatus::all().bits() != 0,
"0x800 must lie outside the declared mask"
);
b
}
#[test]
fn index_status_rejects_unknown_bits() {
let t = probed_track();
assert_eq!(
t.clone().try_with_index_status(unknown_bit()).err(),
Some(AudioTrackError::UnknownStatusBits)
);
assert_eq!(
t.clone()
.try_with_index_status(AudioIndexStatus::EXTRACTED | unknown_bit())
.err(),
Some(AudioTrackError::UnknownStatusBits)
);
let mut m = probed_track();
m.try_set_index_status(AudioIndexStatus::EXTRACTED).unwrap();
assert_eq!(
m.try_set_index_status(AudioIndexStatus::EXTRACTED | unknown_bit())
.err(),
Some(AudioTrackError::UnknownStatusBits)
);
assert_eq!(m.index_status(), AudioIndexStatus::EXTRACTED);
assert!(AudioTrackError::UnknownStatusBits.is_unknown_status_bits());
}
#[test]
fn index_status_accepts_all_known_masks() {
use AudioIndexStatus as S;
let t = probed_track();
for mask in [
S::empty(),
S::EXTRACTED,
S::EXTRACTED | S::VAD_DONE,
S::EXTRACTED | S::CLASSIFIED | S::VAD_DONE | S::STT_DONE | S::SPEAKER_DONE,
S::all(),
] {
assert!(
t.clone().try_with_index_status(mask).is_ok(),
"all-known mask {mask:?} must be accepted"
);
}
}
fn tb() -> mediatime::Timebase {
mediatime::Timebase::new(1, core::num::NonZeroU32::new(1000).expect("nonzero"))
}
#[test]
fn try_with_duration_rejects_negative() {
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert_eq!(
t.clone()
.try_with_duration(Some(Timestamp::new(-1, tb())))
.err(),
Some(AudioTrackError::NegativeDuration)
);
assert!(AudioTrackError::NegativeDuration.is_negative_duration());
let z = t
.clone()
.try_with_duration(Some(Timestamp::new(0, tb())))
.expect("zero accepted");
assert_eq!(z.duration_ref().unwrap().pts(), 0);
let p = t
.clone()
.try_with_duration(Some(Timestamp::new(5000, tb())))
.expect("positive accepted");
assert_eq!(p.duration_ref().unwrap().pts(), 5000);
let n = t.try_with_duration(None).expect("None accepted");
assert!(n.duration_ref().is_none());
}
#[test]
fn try_set_duration_rejects_and_leaves_value_unchanged() {
let mut t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
t.try_set_duration(Some(Timestamp::new(3000, tb())))
.unwrap();
assert_eq!(
t.try_set_duration(Some(Timestamp::new(-10, tb()))).err(),
Some(AudioTrackError::NegativeDuration)
);
assert_eq!(t.duration_ref().unwrap().pts(), 3000);
t.try_set_duration(Some(Timestamp::new(0, tb()))).unwrap();
assert_eq!(t.duration_ref().unwrap().pts(), 0);
t.try_set_duration(None).unwrap();
assert!(t.duration_ref().is_none());
}
#[test]
fn language_mismatch_is_derived_from_languages() {
use mediaframe::lang::Language;
let en = Language::from_bcp47("en").unwrap();
let fr = Language::from_bcp47("fr").unwrap();
let t = AudioTrack::try_new(Uuid7::new(), Uuid7::new()).unwrap();
assert!(!t.language_mismatch());
let t = t.with_language(Some(en));
assert!(!t.language_mismatch());
let t = t.with_detected_language(Some(en));
assert!(!t.language_mismatch());
let t = t.with_detected_language(Some(fr));
assert!(t.language_mismatch());
}
}