lyrics-core 0.4.0

Core types, traits, and helpers for lyrics processing
Documentation
use serde::{Deserialize, Serialize};

/// 单个音节信息,包含文本内容和时间范围。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyllableInfo {
    /// 音节文本内容
    pub text: String,
    /// 开始时间(毫秒)
    pub start_time: i32,
    /// 结束时间(毫秒)
    pub end_time: i32,
}

impl SyllableInfo {
    /// 创建新的音节信息。
    pub fn new(text: String, start_time: i32, end_time: i32) -> Self {
        Self {
            text,
            start_time,
            end_time,
        }
    }

    /// 返回音节持续时长(毫秒)。
    pub fn duration(&self) -> i32 {
        self.end_time - self.start_time
    }
}

/// 完整音节信息,由多个 [`SyllableInfo`] 子项组成。
///
/// 聚合文本与首尾时间始终由子项即时推导,因此直接修改 [`FullSyllableInfo::sub_items`]
/// 不会让任何派生值失效。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullSyllableInfo {
    /// 子音节列表
    pub sub_items: Vec<SyllableInfo>,
}

impl FullSyllableInfo {
    /// 创建新的完整音节信息。
    pub fn new(sub_items: Vec<SyllableInfo>) -> Self {
        Self { sub_items }
    }

    /// 返回子音节列表的只读切片。
    pub fn sub_items(&self) -> &[SyllableInfo] {
        &self.sub_items
    }

    /// 消耗自身并返回子音节列表。
    pub fn into_sub_items(self) -> Vec<SyllableInfo> {
        self.sub_items
    }

    /// 返回子音节列表的可变引用。
    pub fn sub_items_mut(&mut self) -> &mut Vec<SyllableInfo> {
        &mut self.sub_items
    }

    /// 替换子音节列表。
    pub fn set_sub_items(&mut self, sub_items: Vec<SyllableInfo>) {
        self.sub_items = sub_items;
    }

    /// 追加子音节。
    pub fn extend_sub_items(&mut self, items: impl IntoIterator<Item = SyllableInfo>) {
        self.sub_items.extend(items);
    }

    /// 返回所有子音节拼接后的完整文本。
    pub fn text(&self) -> String {
        get_text_from_syllable_list(&self.sub_items)
    }

    /// 返回第一个子音节的开始时间,无子音节时返回 0。
    pub fn start_time(&self) -> i32 {
        self.sub_items.first().map_or(0, |s| s.start_time)
    }

    /// 返回最后一个子音节的结束时间,无子音节时返回 0。
    pub fn end_time(&self) -> i32 {
        self.sub_items.last().map_or(0, |s| s.end_time)
    }

    /// 返回总持续时长(毫秒)。
    pub fn duration(&self) -> i32 {
        self.end_time() - self.start_time()
    }
}

/// 将音节列表拼接为完整文本字符串。
pub fn get_text_from_syllable_list(syllables: &[SyllableInfo]) -> String {
    syllables.iter().map(|s| s.text.as_str()).collect()
}

/// 音节项,对应 C# 的 `ISyllableInfo`。
///
/// 普通音节为 [`SyllableInfo`],同一单词内合并后的音节为 [`FullSyllableInfo`]
/// (保留各自的子音节时间信息,聚合文本与时间由子项推导)。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SyllableItem {
    /// 普通音节
    Syllable(SyllableInfo),
    /// 合并后的完整音节
    Full(FullSyllableInfo),
}

impl SyllableItem {
    /// 返回音节文本。
    pub fn text(&self) -> String {
        match self {
            Self::Syllable(s) => s.text.clone(),
            Self::Full(f) => f.text(),
        }
    }

    /// 返回开始时间(毫秒)。
    pub fn start_time(&self) -> i32 {
        match self {
            Self::Syllable(s) => s.start_time,
            Self::Full(f) => f.start_time(),
        }
    }

    /// 返回结束时间(毫秒)。
    pub fn end_time(&self) -> i32 {
        match self {
            Self::Syllable(s) => s.end_time,
            Self::Full(f) => f.end_time(),
        }
    }

    /// 返回持续时长(毫秒)。
    pub fn duration(&self) -> i32 {
        self.end_time() - self.start_time()
    }

    /// 是否为合并后的完整音节。
    pub fn is_full(&self) -> bool {
        matches!(self, Self::Full(_))
    }

    /// 返回生成时使用的普通音节序列:普通音节返回自身,合并音节返回全部子音节。
    pub fn parts(&self) -> &[SyllableInfo] {
        match self {
            Self::Syllable(syllable) => std::slice::from_ref(syllable),
            Self::Full(full) => full.sub_items(),
        }
    }

    /// 返回普通音节的只读引用,合并音节返回 `None`。
    pub fn as_syllable(&self) -> Option<&SyllableInfo> {
        match self {
            Self::Syllable(s) => Some(s),
            Self::Full(_) => None,
        }
    }

    /// 返回普通音节的可变引用,合并音节返回 `None`。
    pub fn as_syllable_mut(&mut self) -> Option<&mut SyllableInfo> {
        match self {
            Self::Syllable(s) => Some(s),
            Self::Full(_) => None,
        }
    }

    /// 返回合并音节的只读引用,普通音节返回 `None`。
    pub fn as_full(&self) -> Option<&FullSyllableInfo> {
        match self {
            Self::Syllable(_) => None,
            Self::Full(f) => Some(f),
        }
    }

    /// 返回合并音节的可变引用,普通音节返回 `None`。
    pub fn as_full_mut(&mut self) -> Option<&mut FullSyllableInfo> {
        match self {
            Self::Syllable(_) => None,
            Self::Full(f) => Some(f),
        }
    }
}

impl From<SyllableInfo> for SyllableItem {
    fn from(value: SyllableInfo) -> Self {
        Self::Syllable(value)
    }
}

impl From<FullSyllableInfo> for SyllableItem {
    fn from(value: FullSyllableInfo) -> Self {
        Self::Full(value)
    }
}

/// 将音节项列表拼接为完整文本字符串。
pub fn get_text_from_syllable_items(syllables: &[SyllableItem]) -> String {
    let mut text = String::new();
    for item in syllables {
        for part in item.parts() {
            text.push_str(&part.text);
        }
    }
    text
}

/// 将普通音节列表包装为音节项列表。
pub fn to_syllable_items(syllables: impl IntoIterator<Item = SyllableInfo>) -> Vec<SyllableItem> {
    syllables.into_iter().map(SyllableItem::from).collect()
}

/// 将音节项列表展开为普通音节列表(合并音节展开为其子音节)。
pub fn flatten_syllable_items(syllables: &[SyllableItem]) -> Vec<SyllableInfo> {
    let mut items = Vec::with_capacity(syllables.len());
    for syllable in syllables {
        items.extend_from_slice(syllable.parts());
    }
    items
}

/// 为音节项列表中的每个音节添加时间偏移量。
pub fn add_offset_to_syllable_items(syllables: &mut [SyllableItem], offset: i32) {
    for syllable in syllables.iter_mut() {
        match syllable {
            SyllableItem::Syllable(s) => {
                s.start_time -= offset;
                s.end_time -= offset;
            }
            SyllableItem::Full(f) => {
                for sub in f.sub_items_mut() {
                    sub.start_time -= offset;
                    sub.end_time -= offset;
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::{LineInfo, LyricsData};

    /// 歌词模型不含内部可变性,可以跨线程共享(0.4.0 起)。
    #[test]
    fn model_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync>() {}

        assert_send_sync::<SyllableInfo>();
        assert_send_sync::<FullSyllableInfo>();
        assert_send_sync::<SyllableItem>();
        assert_send_sync::<LineInfo>();
        assert_send_sync::<LyricsData>();
    }

    /// 聚合值由子音节即时推导,直接改 `sub_items` 也不会读到过期结果。
    #[test]
    fn aggregates_follow_sub_items() {
        let mut full = FullSyllableInfo::new(vec![
            SyllableInfo::new("He".to_string(), 0, 100),
            SyllableInfo::new("llo".to_string(), 100, 300),
        ]);
        assert_eq!(full.text(), "Hello");
        assert_eq!(
            (full.start_time(), full.end_time(), full.duration()),
            (0, 300, 300)
        );

        full.sub_items_mut()
            .push(SyllableInfo::new("!".to_string(), 300, 350));
        assert_eq!(full.text(), "Hello!");
        assert_eq!(
            (full.start_time(), full.end_time(), full.duration()),
            (0, 350, 350)
        );

        let empty = FullSyllableInfo::new(Vec::new());
        assert_eq!(empty.text(), "");
        assert_eq!((empty.start_time(), empty.end_time()), (0, 0));
    }
}