pub const SPEED_LOOKUP: [u8; 280] = [
255, 255, 255, 255, 255, 253, 249, 245, 242, 238, 235, 232, 228, 225, 222, 218, 216, 213, 210, 207, 204, 201, 198, 196, 193, 191, 188, 186, 183, 181, 179, 176, 174, 172, 169, 168, 165, 163, 161, 159, 158, 155, 153, 152, 150, 148, 146, 145, 143, 141, 139, 137, 136, 135, 133, 131, 130, 129, 127, 126, 124, 123, 122, 120, 119, 118, 117, 115, 114, 113, 112, 111, 110, 109, 107, 106, 105, 104, 103, 102, 101, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 89, 88, 87, 86, 85, 84, 83, 82, 82, 81, 80, 80, 79, 78, 77, 76, 76, 75, 75, 74, 73, 72, 71, 71, 70, 69, 69, 68, 67, 67, 66, 66, 65, 64, 64, 63, 62, 62, 61, 61, 60, 59, 59, 58, 58, 57, 57, 56, 56, 55, 54, 54, 53, 53, 52, 52, 52, 51, 50, 50, 49, 49, 48, 48, 47, 47, 46, 46, 46, 45, 45, 44, 44, 44, 43, 43, 42, 41, 40, 40, 40, 39, 39, 39, 38, 38, 38, 37, 37, 37, 36, 36, 35, 35, 35, 35, 34, 34, 34, 33, 33, 33, 32, 32, 31, 31, 31, 30, 30, 30, 29, 29, 29, 29, 28, 28, 27, 27, 27, 27, 26, 26, 26, 26, 25, 25, 25, 24, 24, 24, 24, 23, 23, 23, 23, 22, 22, 22, 21, 21, 21, 21, 20, 20, 20, 20, 19, 19, 19, 18, 18, 17, 17, 17, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 14, 14, 14, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, ];
pub fn speed_multiplier(wpm: u32) -> u32 {
const MIN: u32 = 80;
let wpm = wpm.min(450);
let ix = wpm.clamp(MIN, 359) - MIN;
let mut x = SPEED_LOOKUP[ix as usize] as u32;
if wpm >= 380 {
x = 7;
}
if wpm >= 400 {
x = 6;
}
x.max(1)
}
pub fn speed_duration_factor(wpm: u32) -> f64 {
const FIXED: f64 = 0.502;
const SCALED: f64 = 0.3546;
let ratio = speed_multiplier(wpm) as f64 / speed_multiplier(175) as f64;
FIXED + SCALED * ratio
}
pub const STRESS_LENGTHS_EN: [u32; 8] = [182, 140, 220, 220, 220, 240, 260, 280];
pub const STRESS_AMPS_EN: [u8; 8] = [18, 18, 20, 20, 20, 22, 22, 20];
pub const LENGTHEN_TONIC_EN: u32 = 20;
pub const MAXAMP_EOC_EN: u8 = 19;
pub const LENGTH_MODS_EN: [u8; 100] = [
100, 120, 100, 105, 100, 110, 110, 100, 95, 100, 105, 120, 105, 110, 125, 130, 135, 115, 125, 100, 105, 120, 75, 100, 75, 105, 120, 85, 75, 100, 105, 120, 85, 105, 95, 115, 120, 100, 95, 100, 110, 120, 95, 105, 100, 115, 120, 100, 100, 100, 105, 120, 100, 105, 95, 115, 120, 110, 95, 100, 105, 120, 100, 105, 105, 122, 125, 110, 105, 100, 105, 120, 100, 105, 105, 122, 125, 110, 105, 100, 105, 120, 95, 105, 100, 115, 120, 110, 100, 100, 100, 120, 100, 100, 100, 100, 100, 100, 100, 100, ];
pub const LENGTH_MODS_EN0: [u8; 100] = [
100, 150, 100, 105, 110, 115, 110, 110, 110, 100, 105, 150, 105, 110, 125, 135, 140, 115, 135, 100, 105, 150, 90, 105, 90, 122, 135, 100, 90, 100, 105, 150, 100, 105, 100, 122, 135, 100, 100, 100, 105, 150, 100, 105, 105, 115, 135, 110, 105, 100, 105, 150, 100, 105, 105, 122, 130, 120, 125, 100, 105, 150, 100, 105, 110, 122, 125, 115, 110, 100, 105, 150, 100, 105, 105, 122, 135, 120, 105, 100, 105, 150, 100, 105, 105, 115, 135, 110, 105, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, ];
pub fn calc_vowel_length_mod(
stress: u8,
next_lm: u8,
next2_lm: u8,
more_syllables: bool,
end_of_clause: bool,
std_length: u8,
stress_lengths: Option<&[u32; 8]>,
) -> u32 {
let stress = (stress as usize).min(7);
let table = if more_syllables { &LENGTH_MODS_EN } else { &LENGTH_MODS_EN0 };
let n2 = (next2_lm as usize).min(9);
let n1 = (next_lm as usize).min(9);
let base_mod = table[n2 * 10 + n1] as u32;
let stress_len = stress_lengths.unwrap_or(&STRESS_LENGTHS_EN)[stress];
let mut length_mod = base_mod * stress_len / 128;
if stress >= 7 {
length_mod += LENGTHEN_TONIC_EN;
}
if end_of_clause {
let len = (std_length as u32) * 2; let eoc_num = 280u32.saturating_sub(len);
length_mod = length_mod * (256 + eoc_num / 3) / 256;
}
length_mod.clamp(8, 500)
}
pub fn length_mod_to_samples(length_mod: u32, samplerate: u32, speed_factor: f64) -> usize {
if length_mod <= 45 {
return 0;
}
let length_std = length_mod - 45;
let total = (length_std as f64 / 1000.0)
* samplerate as f64
* (length_mod as f64 / 256.0)
* speed_factor;
total.round() as usize
}
pub fn stress_code_to_level(code: u8) -> u8 {
let raw = match code {
0 => 0, 2 => 0, 3 => 2, 4 => 3, 5 => 4, 6 => 6, 7 => 7, _ => 0,
};
if raw <= 1 { raw ^ 1 } else { raw }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn primary_stress_eoc_gives_long_vowel() {
let length_mod = calc_vowel_length_mod(7, 1, 1, false, true, 110, None);
assert!(length_mod >= 300, "tonic EOC vowel should be long: {length_mod}");
let samp = length_mod_to_samples(length_mod, 22050, 1.0);
assert!(samp > 5000, "tonic EOC vowel > 200ms: {samp}");
}
#[test]
fn unstressed_vowel_is_short() {
let length_mod = calc_vowel_length_mod(1, 7, 0, true, false, 60, None);
let samp = length_mod_to_samples(length_mod, 22050, 1.0);
assert!(samp < 2205, "unstressed schwa should be short: {samp}");
}
#[test]
fn stress_code_mapping() {
assert_eq!(stress_code_to_level(6), 6); assert_eq!(stress_code_to_level(7), 7); assert_eq!(stress_code_to_level(0), 1); assert_eq!(stress_code_to_level(2), 1); }
}