The timed voice: what a [LineTiming] and a [TimedAudio] may and may not be, and what an adapter's speak_timed must hand back. The first is a law of the port's own types (no adapter can break it, so a failing one means the type changed); the second holds an adapter to it.

5use ::log::info;
6use whiskers_ports::{Audio, AudioMs, LineTiming, SpeechLine, TimedAudio, TimingError, Voice, run_ready};
8fn spans(pairs: &[(u32, u32)]) -> Vec<(AudioMs, AudioMs)> {
9    pairs.iter().map(|&(s, e)| (AudioMs::new(s), AudioMs::new(e))).collect()
10}

Runs every law of the types.

13pub fn timing_types() {
14    info!("conformance: timing types");
15    a_timing_has_one_span_per_character();
16    a_span_never_ends_before_it_starts();
17    speech_never_runs_backwards();
18    a_timed_line_is_of_the_line_spoken();
19}
21pub fn a_timing_has_one_span_per_character() {
22    // Characters are Unicode scalar values (the unit SpeechLine::chars counts in), not bytes or UTF-16 units.
23    let text = "h\u{e9}\u{1F408}";
24    assert_eq!(text.chars().count(), 3);
25    assert!(LineTiming::new(text.into(), spans(&[(0, 1), (1, 2), (2, 3)])).is_ok());
26    for wrong in [spans(&[(0, 1), (1, 2)]), spans(&[(0, 1), (1, 2), (2, 3), (3, 4)]), vec![]] {
27        let n = wrong.len();
28        assert_eq!(LineTiming::new(text.into(), wrong), Err(TimingError::Count { characters: 3, spans: n }));
29    }
30    assert_eq!(LineTiming::new(String::new(), vec![]), Err(TimingError::Empty), "there is nothing to time in no text");
31}
32
33pub fn a_span_never_ends_before_it_starts() {
34    assert!(LineTiming::new("a".into(), spans(&[(5, 5)])).is_ok(), "an instant is a span");
35    assert_eq!(LineTiming::new("ab".into(), spans(&[(0, 9), (20, 10)])), Err(TimingError::Reversed { index: 1 }));
36}
37
38pub fn speech_never_runs_backwards() {
39    assert!(LineTiming::new("ab".into(), spans(&[(0, 10), (10, 20)])).is_ok(), "touching is fine");
40    assert!(LineTiming::new("ab".into(), spans(&[(0, 10), (4, 20)])).is_ok(), "so is a little overlap");
41    assert_eq!(LineTiming::new("ab".into(), spans(&[(10, 20), (9, 30)])), Err(TimingError::OutOfOrder { index: 1 }), "a start going back");
42    assert_eq!(LineTiming::new("ab".into(), spans(&[(10, 20), (12, 19)])), Err(TimingError::OutOfOrder { index: 1 }), "an end going back");
43}
44
45pub fn a_timed_line_is_of_the_line_spoken() {
46    let line = SpeechLine::new("Hi".into()).unwrap();
47    let timing = LineTiming::new("Hi".into(), spans(&[(0, 50), (50, 90)])).unwrap();
48    let timed = TimedAudio::new(Audio(vec![1, 2]), timing.clone(), &line).unwrap();
49    assert_eq!((timed.audio().0.as_slice(), timed.timing()), (&[1u8, 2][..], &timing));
50    let other = SpeechLine::new("Ha".into()).unwrap();
51    assert_eq!(TimedAudio::new(Audio(vec![1, 2]), timing, &other), Err(TimingError::NotThisLine), "timing of another line is not this line's");
52}

A voice that can speak answers speak_timed with its audio and a timing of exactly the line asked, one span per character. (Needs a voice that works, so an adapter that spends money on a call runs it only when its operator has agreed to the spend.)

57pub fn speaks_timed<V: Voice>(v: &V) {
58    info!("conformance: timed voice");
59    let line = SpeechLine::new("Hi Paula, it is a sunny day.".into()).unwrap();
60    let timed = run_ready(v.speak_timed(&line)).expect("a configured voice speaks");
61    assert!(!timed.audio().0.is_empty(), "there is sound");
62    assert_eq!(timed.timing().text(), line.as_str(), "the timing is of the line that was asked for");
63    assert_eq!(timed.timing().spans().len() as u32, line.chars(), "a span for every character, counted as the allowance counts them");
64}