use serde::{Deserialize, Serialize};
use crate::synced_lyrics::{LyricParseError, LyricSpan};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LyricLineType {
Break,
Lyric(LyricLine),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LyricLine {
pub start_us: u32,
pub vocals: Vec<LyricSpan>,
}
pub struct ClassifiedSpans<'a> {
pub said: &'a [LyricSpan],
pub current: Option<(&'a LyricSpan, f32)>,
pub unsaid: &'a [LyricSpan],
}
impl LyricLine {
pub fn line_end_us(&self, lines: &[&LyricLine], duration_us: u32) -> u32 {
let pos = lines
.iter()
.position(|l| std::ptr::eq(self, *l))
.expect("self (LyricLine) is not contained in the slice");
lines.get(pos + 1).map_or(duration_us, |l| l.start_us)
}
pub fn time_spans<'a>(
&'a self,
current_us: u32,
lines: &[&LyricLine],
duration_us: u32,
) -> ClassifiedSpans<'a> {
let line_end = self.line_end_us(lines, duration_us);
let sung_end = self
.vocals
.iter()
.take_while(|span| current_us >= span.span_end_us(&self.vocals, line_end))
.count();
let current = self.vocals.get(sung_end).and_then(|span| {
if current_us < span.start_us {
return None;
}
let span_end = span.span_end_us(&self.vocals, line_end);
let progress = (current_us - span.start_us) as f32 / (span_end - span.start_us) as f32;
Some((span, progress))
});
let unsung_start = sung_end + current.is_some() as usize;
ClassifiedSpans {
said: &self.vocals[..sung_end],
current,
unsaid: &self.vocals[unsung_start..],
}
}
}
impl LyricLine {
fn timestamp_string(&self) -> String {
let mm = self.start_us / 60_000_000;
let ss = (self.start_us / 1_000_000) % 60;
let ms = (self.start_us / 10_000) % 100;
format!("[{mm:02}:{ss:02}:{ms:02}] ")
}
pub fn to_lrc_string(&self) -> String {
let mut buf = self.timestamp_string();
for span in &self.vocals {
buf.push_str(&span.vocals);
}
buf
}
pub fn to_plain_string(&self) -> String {
let mut buf = String::new();
for span in &self.vocals {
buf.push_str(&span.vocals);
}
buf
}
}
pub enum LyricLineParseResult {
Line(LyricLine),
Break,
Offset(u32),
Ignored,
}
impl LyricLineType {
pub fn from_lyric_line(line: &str) -> Result<LyricLineParseResult, LyricParseError> {
if line.is_empty() {
return Ok(LyricLineParseResult::Break);
}
if line.strip_prefix('#').is_some() {
return Ok(LyricLineParseResult::Ignored);
}
let line = line.trim();
let close = line
.find(']')
.ok_or(LyricParseError::CorruptLineTag)?;
let tag = &line[1..close];
let vocals = line[close + 1..].trim();
let (tag_left, tag_right) = tag
.split_once(':')
.ok_or(LyricParseError::CorruptLineTag)?;
let Ok(mins) = tag_left.parse::<f32>() else {
if !vocals.is_empty() {
return Err(LyricParseError::CorruptLineTag);
}
let key = tag_left.trim();
let value = tag_right.trim();
if key == "offset"
&& let Ok(offset) = value.parse::<u32>()
{
return Ok(LyricLineParseResult::Offset(offset));
} else {
return Ok(LyricLineParseResult::Ignored);
}
};
let secs: f32 = tag_right
.parse()
.map_err(|_| LyricParseError::InvalidLineTimestamp(format!("{mins}:{tag_right}")))?;
let start_us = ((mins * 60.0 + secs) * 1_000_000.0) as u32;
let vocals = LyricSpan::from_lyric_str(vocals, start_us)?;
Ok(LyricLineParseResult::Line(LyricLine { start_us, vocals }))
}
}