#[cfg(doc)]
use crate::Haqumei;
#[cfg(doc)]
use crate::HaqumeiOptions;
use haqumei_macros::phonemes;
phonemes! {
UnvoicedA = "A",
UnvoicedE = "E",
UnvoicedI = "I",
UnvoicedO = "O",
UnvoicedU = "U",
Nn = "N",
Nm = "Nm",
Ng = "Ng",
Nd = "Nd",
Nq = "Nq",
Npl = "Npl",
Nr = "Nr",
A = "a",
B = "b",
By = "by",
Ch = "ch",
Cl = "cl",
ClP = "clp",
ClT = "clt",
ClK = "clk",
ClS = "cls",
ClV = "clv",
ClQ = "clq",
D = "d",
Dy = "dy",
E = "e",
F = "f",
Fy = "fy",
G = "g",
Gw = "gw",
Gy = "gy",
H = "h",
Hy = "hy",
I = "i",
J = "j",
K = "k",
Kw = "kw",
Ky = "ky",
M = "m",
My = "my",
N = "n",
Ny = "ny",
O = "o",
P = "p",
Py = "py",
R = "r",
Ry = "ry",
S = "s",
Sh = "sh",
T = "t",
Ts = "ts",
Ty = "ty",
U = "u",
V = "v",
W = "w",
Y = "y",
Z = "z",
Sp = "sp",
Pau = "pau",
Unk = "unk",
}
impl Phoneme {
pub fn possible_phonemes(
split_n_allophones: bool,
split_n_before_r: bool,
split_n_before_palatal_affricate: bool,
split_q_allophones: bool,
enable_final_glottal_stop: bool,
) -> &'static [Phoneme] {
let mut idx = 0;
if split_n_allophones {
idx |= 1;
}
if split_n_before_r {
idx |= 2;
}
if split_n_before_palatal_affricate {
idx |= 4;
}
if split_q_allophones {
idx |= 8;
}
if enable_final_glottal_stop {
idx |= 16;
}
let list = &POSSIBLE_PHONEMES_TABLE[idx];
&list.data[..list.len]
}
pub const fn is_unvoiced(&self) -> bool {
self.is_unvoiced_vowel() || self.is_unvoiced_consonant()
}
pub const fn is_voiced(&self) -> bool {
self.is_voiced_vowel() || self.is_voiced_consonant() || self.is_moraic_nasal()
}
pub const fn is_voicing_underspecified(&self) -> bool {
self.is_continuant_sokuon()
}
pub const fn is_vowel(&self) -> bool {
self.is_voiced_vowel() || self.is_unvoiced_vowel()
}
pub const fn is_voiced_vowel(&self) -> bool {
matches!(self, Self::A | Self::E | Self::I | Self::O | Self::U)
}
pub const fn is_unvoiced_vowel(&self) -> bool {
matches!(
self,
Self::UnvoicedA | Self::UnvoicedE | Self::UnvoicedI | Self::UnvoicedO | Self::UnvoicedU
)
}
pub const fn is_moraic_nasal(&self) -> bool {
matches!(
self,
Self::Nn | Self::Nm | Self::Ng | Self::Nd | Self::Nq | Self::Npl | Self::Nr
)
}
pub const fn is_sokuon(&self) -> bool {
matches!(
self,
Self::Cl | Self::ClP | Self::ClT | Self::ClK | Self::ClS | Self::ClV | Self::ClQ
)
}
pub const fn is_consonant(&self) -> bool {
self.is_unvoiced_consonant() || self.is_voiced_consonant() || self.is_continuant_sokuon()
}
pub const fn is_unvoiced_consonant(&self) -> bool {
matches!(
self,
Self::K
| Self::Ky
| Self::Kw
| Self::S
| Self::Sh
| Self::T
| Self::Ts
| Self::Ty
| Self::Ch
| Self::P
| Self::Py
| Self::F
| Self::Fy
| Self::H
| Self::Hy
)
}
pub const fn is_voiced_consonant(&self) -> bool {
matches!(
self,
Self::G
| Self::Gy
| Self::Gw
| Self::Z
| Self::J
| Self::D
| Self::Dy
| Self::B
| Self::By
| Self::M
| Self::My
| Self::N
| Self::Ny
| Self::R
| Self::Ry
| Self::W
| Self::Y
| Self::V
| Self::ClV
)
}
pub const fn is_continuant_sokuon(&self) -> bool {
matches!(self, Self::ClS)
}
pub const fn is_silent(&self) -> bool {
matches!(
self,
Self::Cl | Self::ClP | Self::ClT | Self::ClK | Self::ClQ | Self::Pau | Self::Sp
)
}
pub const fn is_rest(&self) -> bool {
matches!(self, Self::Sp | Self::Pau)
}
pub const fn is_special(&self) -> bool {
self.is_rest() || matches!(self, Self::Unk)
}
pub fn resolve_q_final_glottal_stop(
self,
next: Option<Phoneme>,
enable_final_glottal_stop: bool,
) -> Phoneme {
if self != Phoneme::Cl || !enable_final_glottal_stop {
return self;
}
match q_environment(next) {
QEnvironment::UtteranceBoundary => Phoneme::ClQ,
QEnvironment::VoicelessBilabialStop
| QEnvironment::VoicelessAlveolarStopOrAffricate
| QEnvironment::VoicelessVelarStop
| QEnvironment::VoicelessOrUnmarkedContinuant
| QEnvironment::VoicedStopOrAffricate
| QEnvironment::Unresolved => self,
}
}
pub fn resolve_q_allophone(self, next: Option<Phoneme>, split_q_allophones: bool) -> Phoneme {
if self != Phoneme::Cl || !split_q_allophones {
return self;
}
match q_environment(next) {
QEnvironment::VoicelessBilabialStop => Phoneme::ClP,
QEnvironment::VoicelessAlveolarStopOrAffricate => Phoneme::ClT,
QEnvironment::VoicelessVelarStop => Phoneme::ClK,
QEnvironment::VoicelessOrUnmarkedContinuant => Phoneme::ClS,
QEnvironment::VoicedStopOrAffricate => Phoneme::ClV,
QEnvironment::UtteranceBoundary | QEnvironment::Unresolved => Phoneme::Cl,
}
}
pub fn resolve_n_allophone(
self,
next: Option<Phoneme>,
split_n_allophones: bool,
split_n_before_r: bool,
split_n_before_palatal_affricate: bool,
) -> Phoneme {
if self != Phoneme::Nn || !split_n_allophones {
return self;
}
match n_environment(next) {
NEnvironment::Bilabial => Phoneme::Nm,
NEnvironment::Velar => Phoneme::Ng,
NEnvironment::Alveolar => Phoneme::Nd,
NEnvironment::Liquid => {
if split_n_before_r {
Phoneme::Nr
} else {
Phoneme::Nd
}
}
NEnvironment::PalatalAffricate => {
if split_n_before_palatal_affricate {
Phoneme::Npl
} else {
Phoneme::Nd
}
}
NEnvironment::UtteranceBoundary => Phoneme::Nq,
NEnvironment::Unresolved => Phoneme::Nn,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NEnvironment {
Bilabial,
Velar,
Alveolar,
PalatalAffricate,
Liquid,
UtteranceBoundary,
Unresolved,
}
fn n_environment(next: Option<Phoneme>) -> NEnvironment {
use Phoneme::*;
match next {
None => NEnvironment::UtteranceBoundary,
Some(Pau) | Some(Sp) => NEnvironment::UtteranceBoundary,
Some(P) | Some(Py) | Some(B) | Some(By) | Some(M) | Some(My) => NEnvironment::Bilabial,
Some(K) | Some(Ky) | Some(Kw) | Some(G) | Some(Gy) | Some(Gw) => NEnvironment::Velar,
Some(T) | Some(Ty) | Some(Ts) | Some(D) | Some(Dy) | Some(N) | Some(Ny) | Some(Z) => {
NEnvironment::Alveolar
}
Some(Ch) | Some(J) => NEnvironment::PalatalAffricate,
Some(R) | Some(Ry) => NEnvironment::Liquid,
Some(A) | Some(E) | Some(I) | Some(O) | Some(U) | Some(UnvoicedA) | Some(UnvoicedE)
| Some(UnvoicedI) | Some(UnvoicedO) | Some(UnvoicedU) | Some(Y) | Some(W) | Some(S)
| Some(Sh) | Some(F) | Some(Fy) | Some(H) | Some(Hy) | Some(V) | Some(Cl) | Some(ClP)
| Some(ClT) | Some(ClK) | Some(ClS) | Some(ClV) | Some(ClQ) | Some(Nn) | Some(Nm)
| Some(Ng) | Some(Nd) | Some(Nq) | Some(Npl) | Some(Nr) | Some(Unk) => {
NEnvironment::Unresolved
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum QEnvironment {
VoicelessBilabialStop,
VoicelessAlveolarStopOrAffricate,
VoicelessVelarStop,
VoicelessOrUnmarkedContinuant,
VoicedStopOrAffricate,
UtteranceBoundary,
Unresolved,
}
fn q_environment(next: Option<Phoneme>) -> QEnvironment {
use Phoneme::*;
match next {
None => QEnvironment::UtteranceBoundary,
Some(Pau) | Some(Sp) => QEnvironment::UtteranceBoundary,
Some(P) | Some(Py) => QEnvironment::VoicelessBilabialStop,
Some(T) | Some(Ty) | Some(Ts) | Some(Ch) => QEnvironment::VoicelessAlveolarStopOrAffricate,
Some(K) | Some(Ky) | Some(Kw) => QEnvironment::VoicelessVelarStop,
Some(S) | Some(Sh) | Some(F) | Some(Fy) | Some(H) | Some(Hy) | Some(V) => {
QEnvironment::VoicelessOrUnmarkedContinuant
}
Some(B) | Some(By) | Some(D) | Some(Dy) | Some(G) | Some(Gy) | Some(Gw) | Some(Z)
| Some(J) => QEnvironment::VoicedStopOrAffricate,
Some(A) | Some(E) | Some(I) | Some(O) | Some(U) | Some(UnvoicedA) | Some(UnvoicedE)
| Some(UnvoicedI) | Some(UnvoicedO) | Some(UnvoicedU) | Some(Y) | Some(W) | Some(M)
| Some(My) | Some(N) | Some(Ny) | Some(R) | Some(Ry) | Some(Cl) | Some(ClP) | Some(ClT)
| Some(ClK) | Some(ClS) | Some(ClV) | Some(ClQ) | Some(Nn) | Some(Nm) | Some(Ng)
| Some(Nd) | Some(Nq) | Some(Npl) | Some(Nr) | Some(Unk) => QEnvironment::Unresolved,
}
}
#[derive(Debug, Clone, Copy)]
struct PhonemeList {
len: usize,
data: [Phoneme; Phoneme::ALL.len()],
}
impl PhonemeList {
const fn new(
split_n_allophones: bool,
split_n_before_r: bool,
split_n_before_palatal_affricate: bool,
split_q_allophones: bool,
enable_final_glottal_stop: bool,
) -> Self {
let mut data = [Phoneme::Unk; Phoneme::ALL.len()];
let mut len = 0;
let mut i = 0;
while i < Phoneme::ALL.len() {
let p = Phoneme::ALL[i];
let include = match p {
Phoneme::Nm | Phoneme::Ng | Phoneme::Nd | Phoneme::Nq => split_n_allophones,
Phoneme::Nr => split_n_allophones && split_n_before_r,
Phoneme::Npl => split_n_allophones && split_n_before_palatal_affricate,
Phoneme::ClP | Phoneme::ClT | Phoneme::ClK | Phoneme::ClS | Phoneme::ClV => {
split_q_allophones
}
Phoneme::ClQ => enable_final_glottal_stop,
_ => true,
};
if include {
data[len] = p;
len += 1;
}
i += 1;
}
Self { len, data }
}
}
const POSSIBLE_PHONEMES_TABLE: [PhonemeList; 32] = {
let mut table = [PhonemeList::new(false, false, false, false, false); 32];
let mut idx = 0;
while idx < 32 {
let split_n = (idx & 1) != 0;
let split_n_r = (idx & 2) != 0;
let split_n_pa = (idx & 4) != 0;
let split_q = (idx & 8) != 0;
let final_glottal = (idx & 16) != 0;
table[idx] = PhonemeList::new(split_n, split_n_r, split_n_pa, split_q, final_glottal);
idx += 1;
}
table
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phoneme_exhaustiveness() {
let all_phonemes = Phoneme::ALL;
for p in all_phonemes {
let is_voiced = p.is_voiced();
let is_unvoiced = p.is_unvoiced();
let is_silent = p.is_silent();
let is_special = p.is_special();
let is_voicing_unresolved = p.is_voicing_underspecified();
let true_count = [
is_voiced,
is_unvoiced,
is_silent,
is_special,
is_voicing_unresolved,
]
.iter()
.filter(|&&x| x)
.count();
if !matches!(p, Phoneme::Sp | Phoneme::Pau) {
assert_eq!(
true_count, 1,
"{:?} の分類が正しくありません (voiced: {}, unvoiced: {}, silent: {}, special: {})",
p, is_voiced, is_unvoiced, is_silent, is_special
);
} else {
assert_eq!(
true_count, 2,
"{:?} の分類が正しくありません (voiced: {}, unvoiced: {}, silent: {}, special: {})",
p, is_voiced, is_unvoiced, is_silent, is_special
);
}
}
}
#[test]
fn test_n_and_q_environment_cover_all_phonemes() {
assert_eq!(n_environment(None), NEnvironment::UtteranceBoundary);
assert_eq!(q_environment(None), QEnvironment::UtteranceBoundary);
for &p in Phoneme::ALL.iter() {
let _ = n_environment(Some(p));
let _ = q_environment(Some(p));
}
}
#[test]
fn test_resolve_n_allophone_core_cases() {
let n = Phoneme::Nn;
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::M), true, false, false),
Phoneme::Nm
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::P), true, false, false),
Phoneme::Nm
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::B), true, false, false),
Phoneme::Nm
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::K), true, false, false),
Phoneme::Ng
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::G), true, false, false),
Phoneme::Ng
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::T), true, false, false),
Phoneme::Nd
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Z), true, false, false),
Phoneme::Nd
);
assert_eq!(n.resolve_n_allophone(None, true, false, false), Phoneme::Nq);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Pau), true, false, false),
Phoneme::Nq
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Sp), true, false, false),
Phoneme::Nq
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::A), true, false, false),
Phoneme::Nn
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Y), true, false, false),
Phoneme::Nn
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::S), true, false, false),
Phoneme::Nn
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::H), true, false, false),
Phoneme::Nn
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::V), true, false, false),
Phoneme::Nn
);
}
#[test]
fn test_resolve_n_allophone_master_switch_off() {
let n = Phoneme::Nn;
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::P), false, true, true),
Phoneme::Nn
);
assert_eq!(n.resolve_n_allophone(None, false, true, true), Phoneme::Nn);
}
#[test]
fn test_resolve_n_allophone_low_confidence_options() {
let n = Phoneme::Nn;
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::R), true, false, false),
Phoneme::Nd
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Ch), true, false, false),
Phoneme::Nd
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::J), true, false, false),
Phoneme::Nd
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::R), true, true, false),
Phoneme::Nr
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Ry), true, true, false),
Phoneme::Nr
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Ch), true, false, true),
Phoneme::Npl
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::J), true, false, true),
Phoneme::Npl
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::Ch), true, true, false),
Phoneme::Nd
);
assert_eq!(
n.resolve_n_allophone(Some(Phoneme::R), true, false, true),
Phoneme::Nd
);
}
#[test]
fn test_resolve_n_allophone_is_noop_for_non_nn() {
assert_eq!(
Phoneme::T.resolve_n_allophone(Some(Phoneme::K), true, true, true),
Phoneme::T
);
}
#[test]
fn test_resolve_q_allophone_core_cases() {
let cl = Phoneme::Cl;
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::P), true), Phoneme::ClP);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::T), true), Phoneme::ClT);
assert_eq!(
cl.resolve_q_allophone(Some(Phoneme::Ts), true),
Phoneme::ClT
);
assert_eq!(
cl.resolve_q_allophone(Some(Phoneme::Ch), true),
Phoneme::ClT
);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::K), true), Phoneme::ClK);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::S), true), Phoneme::ClS);
assert_eq!(
cl.resolve_q_allophone(Some(Phoneme::Sh), true),
Phoneme::ClS
);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::H), true), Phoneme::ClS);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::V), true), Phoneme::ClS);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::B), true), Phoneme::ClV);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::G), true), Phoneme::ClV);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::Z), true), Phoneme::ClV);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::J), true), Phoneme::ClV);
}
#[test]
fn test_resolve_q_allophone_master_switch_off() {
assert_eq!(
Phoneme::Cl.resolve_q_allophone(Some(Phoneme::P), false),
Phoneme::Cl
);
}
#[test]
fn test_resolve_q_allophone_does_not_touch_utterance_boundary() {
let cl = Phoneme::Cl;
assert_eq!(cl.resolve_q_allophone(None, true), Phoneme::Cl);
assert_eq!(
cl.resolve_q_allophone(Some(Phoneme::Pau), true),
Phoneme::Cl
);
assert_eq!(cl.resolve_q_allophone(Some(Phoneme::Sp), true), Phoneme::Cl);
}
#[test]
fn test_resolve_q_final_glottal_stop() {
let cl = Phoneme::Cl;
assert_eq!(cl.resolve_q_final_glottal_stop(None, true), Phoneme::ClQ);
assert_eq!(
cl.resolve_q_final_glottal_stop(Some(Phoneme::Pau), true),
Phoneme::ClQ
);
assert_eq!(
cl.resolve_q_final_glottal_stop(Some(Phoneme::Sp), true),
Phoneme::ClQ
);
assert_eq!(cl.resolve_q_final_glottal_stop(None, false), Phoneme::Cl);
}
#[test]
fn test_ordinary_medial_geminates_never_produce_glottal_stop() {
let cl = Phoneme::Cl;
let ordinary_following_consonants = [
Phoneme::P,
Phoneme::T,
Phoneme::K,
Phoneme::S,
Phoneme::Sh,
Phoneme::B,
Phoneme::D,
Phoneme::G,
Phoneme::Z,
Phoneme::J,
Phoneme::V,
];
for &next in &ordinary_following_consonants {
assert_eq!(
cl.resolve_q_final_glottal_stop(Some(next), true),
Phoneme::Cl,
"{:?} の前で誤って声門閉鎖になっています",
next
);
}
}
#[test]
fn test_resolve_q_allophone_non_obstruent_environments_are_noop() {
let cl = Phoneme::Cl;
let non_obstruent_environments = [
Phoneme::A,
Phoneme::Y,
Phoneme::W,
Phoneme::M,
Phoneme::N,
Phoneme::R,
];
for &next in &non_obstruent_environments {
assert_eq!(cl.resolve_q_allophone(Some(next), true), Phoneme::Cl);
}
}
#[test]
fn test_possible_phonemes_exhaustively() {
for i in 0..32 {
let split_n = (i & 1) != 0;
let split_n_r = (i & 2) != 0;
let split_n_pa = (i & 4) != 0;
let split_q = (i & 8) != 0;
let final_glottal = (i & 16) != 0;
let phonemes =
Phoneme::possible_phonemes(split_n, split_n_r, split_n_pa, split_q, final_glottal);
assert!(phonemes.contains(&Phoneme::Nn));
assert!(phonemes.contains(&Phoneme::Cl));
assert!(phonemes.contains(&Phoneme::A));
assert!(phonemes.contains(&Phoneme::Sp));
assert!(phonemes.contains(&Phoneme::Pau));
assert!(phonemes.contains(&Phoneme::Unk));
assert_eq!(phonemes.contains(&Phoneme::Nm), split_n);
assert_eq!(phonemes.contains(&Phoneme::Ng), split_n);
assert_eq!(phonemes.contains(&Phoneme::Nd), split_n);
assert_eq!(phonemes.contains(&Phoneme::Nq), split_n);
assert_eq!(phonemes.contains(&Phoneme::Nr), split_n && split_n_r);
assert_eq!(phonemes.contains(&Phoneme::Npl), split_n && split_n_pa);
assert_eq!(phonemes.contains(&Phoneme::ClP), split_q);
assert_eq!(phonemes.contains(&Phoneme::ClT), split_q);
assert_eq!(phonemes.contains(&Phoneme::ClK), split_q);
assert_eq!(phonemes.contains(&Phoneme::ClS), split_q);
assert_eq!(phonemes.contains(&Phoneme::ClV), split_q);
assert_eq!(phonemes.contains(&Phoneme::ClQ), final_glottal);
let mut prev_position = None;
for &p in phonemes {
let current_position = Phoneme::ALL.iter().position(|&x| x == p).unwrap();
if let Some(prev) = prev_position {
assert!(
prev < current_position,
"音素の順序が壊れています: 直前={:?}, 現在={:?}, (フラグのインデックス={})",
Phoneme::ALL[prev],
p,
i
);
}
prev_position = Some(current_position);
}
}
}
}