1//! When each character of a spoken line is said: what the app needs to show the words as they are
2//! spoken (the bouncing ball over the lyrics, the mouth). This is Whiskers' own vocabulary. It is
3//! deliberately not the shape any voice vendor happens to send; an adapter converts into it and nothing
4//! vendor-shaped crosses the port.
5
6use std::fmt;
7
8use serde::Serialize;
9
10use crate::voice::{Audio, SpeechLine};
11
12/// A moment inside one spoken line's audio, in milliseconds from its start. Not a [`Millis`](crate::Millis),
13/// which is an instant on the clock: the two cannot be confused or compared.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
15#[serde(transparent)]
16pub struct AudioMs(u32);
17
18impl AudioMs {
19    pub const fn new(ms: u32) -> Self {
20        Self(ms)
21    }
22
23    pub const fn get(self) -> u32 {
24        self.0
25    }
26}
27
28/// The time one character is spoken in: it starts at `start` and is done at `end`, never before.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct Span {
31    start: AudioMs,
32    end: AudioMs,
33}
34
35impl Span {
36    pub const fn start(self) -> AudioMs {
37        self.start
38    }
39
40    pub const fn end(self) -> AudioMs {
41        self.end
42    }
43}
44
45/// Why a timing, or a timed line, was refused. `index` counts characters (Unicode scalar values, the unit
46/// [`SpeechLine::chars`] counts in) from zero.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum TimingError {
49    /// There is no text to time.
50    Empty,
51    /// The text has `characters` characters and `spans` spans were given: one each, or it is not a timing.
52    Count { characters: usize, spans: usize },
53    /// Character `index` is done before it starts.
54    Reversed { index: usize },
55    /// Character `index` starts, or is done, before the one ahead of it does. Speech does not run backwards.
56    OutOfOrder { index: usize },
57    /// The timing is of some other text than the line that was spoken.
58    NotThisLine,
59}
60
61impl fmt::Display for TimingError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            TimingError::Empty => write!(f, "the timing has no text"),
65            TimingError::Count { characters, spans } => write!(f, "the timing has {spans} spans for {characters} characters"),
66            TimingError::Reversed { index } => write!(f, "character {index} is done before it starts"),
67            TimingError::OutOfOrder { index } => write!(f, "character {index} is timed before the one ahead of it"),
68            TimingError::NotThisLine => write!(f, "the timing is of other text than the line that was spoken"),
69        }
70    }
71}
72
73impl std::error::Error for TimingError {}
74
75/// A line's text with the time each of its characters is said.
76///
77/// One [`Span`] per character, by construction (they are pairs of the text's characters and their spans,
78/// not two lists that must agree). Every span ends no earlier than it starts, and starts and ends never
79/// run backwards from one character to the next. Spans may touch or overlap a little (a space can be
80/// said under the end of the word before it); they may not go back in time.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct LineTiming {
83    text: String,
84    spans: Vec<Span>,
85}
86
87impl LineTiming {
88    /// `spans` are `(start, end)` per character of `text`, in order.
89    pub fn new(text: String, spans: Vec<(AudioMs, AudioMs)>) -> Result<Self, TimingError> {
90        let characters = text.chars().count();
91        if characters == 0 {
92            return Err(TimingError::Empty);
93        }
94        if spans.len() != characters {
95            return Err(TimingError::Count { characters, spans: spans.len() });
96        }
97        let mut out: Vec<Span> = Vec::with_capacity(spans.len());
98        for (index, (start, end)) in spans.into_iter().enumerate() {
99            if end < start {
100                return Err(TimingError::Reversed { index });
101            }
102            if let Some(before) = out.last() {
103                if start < before.start || end < before.end {
104                    return Err(TimingError::OutOfOrder { index });
105                }
106            }
107            out.push(Span { start, end });
108        }
109        Ok(Self { text, spans: out })
110    }
111
112    pub fn text(&self) -> &str {
113        &self.text
114    }
115
116    /// One per character of [`text`](Self::text).
117    pub fn spans(&self) -> &[Span] {
118        &self.spans
119    }
120
121    /// Each character with the time it is said.
122    pub fn characters(&self) -> impl Iterator<Item = (char, Span)> + '_ {
123        self.text.chars().zip(self.spans.iter().copied())
124    }
125}
126
127/// Speech (MP3) with the time each character of the line is said.
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub struct TimedAudio {
130    audio: Audio,
131    timing: LineTiming,
132}
133
134impl TimedAudio {
135    /// `audio` is `line` spoken, and `timing` is of exactly that line's text.
136    pub fn new(audio: Audio, timing: LineTiming, line: &SpeechLine) -> Result<Self, TimingError> {
137        if timing.text() != line.as_str() {
138            return Err(TimingError::NotThisLine);
139        }
140        Ok(Self { audio, timing })
141    }
142
143    pub fn audio(&self) -> &Audio {
144        &self.audio
145    }
146
147    pub fn timing(&self) -> &LineTiming {
148        &self.timing
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    fn ms(a: &[(u32, u32)]) -> Vec<(AudioMs, AudioMs)> {
157        a.iter().map(|&(s, e)| (AudioMs::new(s), AudioMs::new(e))).collect()
158    }
159
160    #[test]
161    fn a_timing_is_one_span_per_character_in_order() {
162        let t = LineTiming::new("Hi!".into(), ms(&[(0, 100), (100, 180), (180, 400)])).unwrap();
163        let said: Vec<(char, u32, u32)> = t.characters().map(|(c, s)| (c, s.start().get(), s.end().get())).collect();
164        assert_eq!(said, [('H', 0, 100), ('i', 100, 180), ('!', 180, 400)]);
165    }
166
167    #[test]
168    fn characters_are_scalar_values_not_bytes() {
169        assert!(LineTiming::new("h\u{e9}\u{1F408}".into(), ms(&[(0, 1), (1, 2), (2, 3)])).is_ok());
170        assert_eq!(LineTiming::new("h\u{e9}".into(), ms(&[(0, 1), (1, 2), (2, 3)])), Err(TimingError::Count { characters: 2, spans: 3 }));
171    }
172
173    #[test]
174    fn what_is_not_a_timing_is_refused_and_says_where() {
175        assert_eq!(LineTiming::new(String::new(), vec![]), Err(TimingError::Empty));
176        assert_eq!(LineTiming::new("ab".into(), ms(&[(0, 1)])), Err(TimingError::Count { characters: 2, spans: 1 }));
177        assert_eq!(LineTiming::new("ab".into(), ms(&[(0, 1), (5, 4)])), Err(TimingError::Reversed { index: 1 }));
178        assert_eq!(LineTiming::new("ab".into(), ms(&[(10, 20), (5, 30)])), Err(TimingError::OutOfOrder { index: 1 }), "starts early");
179        assert_eq!(LineTiming::new("ab".into(), ms(&[(10, 20), (12, 15)])), Err(TimingError::OutOfOrder { index: 1 }), "ends early");
180    }
181
182    #[test]
183    fn spans_may_touch_overlap_and_be_instants() {
184        assert!(LineTiming::new("abc".into(), ms(&[(0, 10), (10, 10), (10, 30)])).is_ok());
185        assert!(LineTiming::new("ab".into(), ms(&[(0, 10), (5, 20)])).is_ok());
186    }
187
188    #[test]
189    fn a_timed_line_is_the_line_that_was_spoken() {
190        let line = SpeechLine::new("Hi".into()).unwrap();
191        let t = LineTiming::new("Hi".into(), ms(&[(0, 1), (1, 2)])).unwrap();
192        assert!(TimedAudio::new(Audio(vec![1]), t.clone(), &line).is_ok());
193        let other = SpeechLine::new("Ho".into()).unwrap();
194        assert_eq!(TimedAudio::new(Audio(vec![1]), t, &other), Err(TimingError::NotThisLine));
195    }
196}