diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 2ea0f4271..b57f25662 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -31,12 +31,20 @@ public ArpasingPlusPhonemizer() { "eu", "oe", "yw", "yx", "wx", "ox", "ex", "ea", "ia", "oa", "ua", "ean", "eam", "eang" }; this.consonants = "b,ch,d,dh,dr,dx,f,g,hh,jh,k,l,m,n,ng,p,q,r,s,sh,t,th,tr,v,w,y,z".Split(','); + this.diphthongTails = new Dictionary() { + { "ay", "y" }, + { "ey", "y" }, + { "oy", "y" }, + { "aw", "w" }, + { "ow", "w" }, + { "er", "r" }, + { "iy", "y" }, + }; } protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => ""; - List consExceptions = new List(); // For banks with missing vowels private readonly Dictionary missingVphonemes = "ax=ah,aa=ah,ae=ah,iy=ih,uh=uw,ix=ih,ux=uh,oh=ao,eu=uh,oe=ax,uy=uw,yw=uw,yx=iy,wx=uw,ea=eh,ia=iy,oa=ao,ua=uw,R=-,N=n,mm=m,ll=l".Split(',') @@ -44,7 +52,6 @@ public ArpasingPlusPhonemizer() { .Where(parts => parts.Length == 2) .Where(parts => parts[0] != parts[1]) .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingVPhonemes = false; // For banks with missing custom consonants private readonly Dictionary missingCphonemes = "nx=n,tx=t,dx=d,zh=sh,z=s,ng=n,cl=q,vf=q,dd=d,lx=l".Split(',') @@ -52,8 +59,6 @@ public ArpasingPlusPhonemizer() { .Where(parts => parts.Length == 2) .Where(parts => parts[0] != parts[1]) .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingCPhonemes = false; - private bool isYamlFallbacks = false; private bool vc_FallBack = false; private bool phoneticHint = false; @@ -72,29 +77,6 @@ public ArpasingPlusPhonemizer() { //{"er","ah"}, }; - private readonly Dictionary vvDiphthongExceptions = - new Dictionary() { - {"aw","ah"}, - {"ow","ao"}, - {"uw","uh"}, - {"ay","ah"}, - {"ey","eh"}, - {"oy","ao"}, - }; - - private readonly Dictionary vvExceptions = - new Dictionary() { - {"aw","w"}, - {"ow","w"}, - {"uw","w"}, - {"ay","y"}, - {"ey","y"}, - {"oy","y"}, - {"iy","y"}, - {"er","r"}, - }; - - private readonly string[] ccvException = { "ng", "dh" }; private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; protected override string[] GetSymbols(Note note) { @@ -103,39 +85,13 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } - List modified = new List(original); - List finalPhonemes = ApplyReplacements(modified, false); List finalProcessedPhonemes = new List(); // SPLITS UP DR AND TR string[] tr = new[] { "tr" }; string[] dr = new[] { "dr" }; - string[] wh = new[] { "wh" }; - string[] av_c = new[] { "al", "am", "an", "ang", "ar" }; - string[] ev_c = new[] { "el", "em", "en", "eng", "err" }; - string[] iv_c = new[] { "il", "im", "in", "ing", "ir" }; - string[] ov_c = new[] { "ol", "om", "on", "ong", "or" }; - string[] uv_c = new[] { "ul", "um", "un", "ung", "ur" }; - var consonatsV1 = new List { "l", "m", "n", "r" }; - var consonatsV2 = new List { "mm", "nn", "ng" }; - // SPLITS UP 2 SYMBOL VOWELS AND 1 SYMBOL CONSONANT - List vowel3S = new List(); - foreach (string V1 in vowels) { - foreach (string C1 in consonatsV1) { - vowel3S.Add($"{V1}{C1}"); - } - } - // SPLITS UP 2 SYMBOL VOWELS AND 2 SYMBOL CONSONANT - List vowel4S = new List(); - foreach (string V1 in vowels) { - foreach (string C1 in consonatsV2) { - vowel3S.Add($"{V1}{C1}"); - } - } - IEnumerable phonemes; - phonemes = finalPhonemes; - foreach (string s in phonemes) { + foreach (string s in original) { switch (s) { case var str when dr.Contains(str) && !HasOto($"{str} {vowels}", note.tone) && !HasOto($"ay {str}", note.tone): finalProcessedPhonemes.AddRange(new string[] { "d", s[1].ToString() }); @@ -143,30 +99,6 @@ protected override string[] GetSymbols(Note note) { case var str when tr.Contains(str) && !HasOto($"{str} {vowels}", note.tone) && !HasOto($"ay {str}", note.tone): finalProcessedPhonemes.AddRange(new string[] { "t", s[1].ToString() }); break; - case var str when wh.Contains(str) && !HasOto($"{str} {vowels}", note.tone) && !HasOto($"ay {str}", note.tone): - finalProcessedPhonemes.AddRange(new string[] { "hh", s[1].ToString() }); - break; - case var str when av_c.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "aa", s[1].ToString() }); - break; - case var str when ev_c.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "eh", s[1].ToString() }); - break; - case var str when iv_c.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "iy", s[1].ToString() }); - break; - case var str when ov_c.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "ao", s[1].ToString() }); - break; - case var str when uv_c.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "uw", s[1].ToString() }); - break; - case var str when vowel3S.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s[2].ToString() }); - break; - case var str when vowel4S.Contains(str) && !HasOto($"b {str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s.Substring(2, 2) }); - break; default: finalProcessedPhonemes.Add(s); break; @@ -175,28 +107,8 @@ protected override string[] GetSymbols(Note note) { return finalProcessedPhonemes.ToArray(); } - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - // LOAD DICTIONARY FROM FOLDER - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - // LOAD DICTIONARY FROM SINGER FOLDER - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - g2ps.Add(new ArpabetPlusG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new ArpabetPlusG2p() }; } public override void SetSinger(USinger singer) { @@ -212,18 +124,367 @@ public override void SetSinger(USinger singer) { } } - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; + // AUTO ALTS + private List unotes = new(); + private UTrack utrack; + private UProject uproject; + + public override void SetUp(Note[][] notes, UProject project, UTrack track) { + base.SetUp(notes, project, track); + utrack = track; + uproject = project; + int trackNo = project.tracks.IndexOf(track); + if (trackNo < 0 && track != null) { + trackNo = track.TrackNo; + } + var part = project.parts.OfType() + .FirstOrDefault(p => p.trackNo == trackNo) + ?? project.parts.OfType().FirstOrDefault(); + + unotes = part?.notes.OrderBy(n => n.position).ToList() ?? new List(); + } + + private (UNote un, UNote unNext) UNoteAt(int absPos) { + if (unotes.Count == 0) return (null, null); + var un = unotes.LastOrDefault(n => n.position <= absPos) ?? unotes[0]; + int idx = unotes.IndexOf(un); + return (un, idx + 1 < unotes.Count ? unotes[idx + 1] : null); + } + + private string GetPureAlias(string rawAlias, USinger singer) { + if (string.IsNullOrWhiteSpace(rawAlias) || singer == null) + return rawAlias ?? ""; + + string cleanAlias = rawAlias; + + if (singer.Subbanks != null) { + var suffixes = singer.Subbanks.Select(s => s.Suffix) + .Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderByDescending(s => s.Length); + foreach (var suffix in suffixes) { + if (cleanAlias.EndsWith(suffix)) { + cleanAlias = cleanAlias.Substring(0, cleanAlias.Length - suffix.Length); + break; + } + } + + var prefixes = singer.Subbanks.Select(s => s.Prefix) + .Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderByDescending(s => s.Length); + foreach (var prefix in prefixes) { + if (cleanAlias.StartsWith(prefix)) { + cleanAlias = cleanAlias.Substring(prefix.Length); + break; + } + } + } + cleanAlias = Regex.Replace(cleanAlias, @"\s*\d+$", ""); + + return cleanAlias.Trim(); + } + + private bool TryGetMappedOtoAnyFormat(USinger singer, string baseAlias, int alt, int tone, string color, out UOto testOto) { + testOto = null; + var formats = alt == 0 + ? new[] { baseAlias } + : new[] { $"{baseAlias}{alt}" }; + + foreach (var format in formats) { + if (singer.TryGetMappedOto(format, tone, color, out testOto)) { + return true; + } + } + return false; + } + + private string FormatForChunking(string cleanAlias) { + if (string.IsNullOrEmpty(cleanAlias)) return ""; + string pure = cleanAlias.ToLower(); + pure = pure.Replace("-", "").Trim(); + pure = pure.Replace(" ", "_"); + while (pure.Contains("__")) pure = pure.Replace("__", "_"); + return pure; + } + + private string MergeChunks(string left, string right) { + if (string.IsNullOrEmpty(left)) return right; + if (string.IsNullOrEmpty(right)) return left; + + var lParts = left.Split('_'); + var rParts = right.Split('_'); + + if (lParts.Length > 0 && rParts.Length > 0 && lParts.Last() == rParts.First()) { + return left + "_" + string.Join("_", rParts.Skip(1)); + } + + return left + "_" + right; + } + + private class AltCandidate { + public int Alt; + public UOto Oto; + public string WavNorm; + public string PaddedWav; + public bool IsPhonetic; + } + + private List GetCandidates(string cleanAlias, USinger singer, int tone, string color) { + var list = new List(); + string baseAlias = cleanAlias.ToLower(); + + for (int alt = 0; alt < 25; alt++) { + if (TryGetMappedOtoAnyFormat(singer, baseAlias, alt, tone, color, out var oto)) { + string wav = oto.File; + if (string.IsNullOrEmpty(wav)) continue; + + string norm = Path.GetFileNameWithoutExtension(wav).ToLower() + .Replace("-", "").Trim().Replace(" ", "_"); + while (norm.Contains("__")) norm = norm.Replace("__", "_"); + + list.Add(new AltCandidate { + Alt = alt, + Oto = oto, + WavNorm = norm, + PaddedWav = $"_{norm}_", + IsPhonetic = norm.Any(char.IsLetter) + }); + } + } + + if (list.Count == 0 && TryGetMappedOtoAnyFormat(singer, baseAlias, 0, tone, color, out var defaultOto)) { + list.Add(new AltCandidate { Alt = 0, Oto = defaultOto, WavNorm = "", PaddedWav = "", IsPhonetic = false }); + } + + // Fallback: If no candidate was found at all, provide a blank/safe placeholder candidate + if (list.Count == 0) { + list.Add(new AltCandidate { Alt = 0, Oto = null, WavNorm = "", PaddedWav = "", IsPhonetic = false }); + } + + return list; + } + + private int ScoreTransition(AltCandidate curr, AltCandidate prev, string currChunk, string prevChunk) { + if (curr == null) return 0; + int score = 0; + + // Single emission match (does the WAV filename contain this phoneme chunk?) + if (!string.IsNullOrEmpty(curr.PaddedWav) && !string.IsNullOrEmpty(currChunk)) { + if (curr.PaddedWav.Contains($"_{currChunk}_")) score += 30; + } + + // Direct same-WAV connection (Baton continuity across transitions) + if (prev != null && !string.IsNullOrEmpty(prev.Oto?.File) && !string.IsNullOrEmpty(curr.Oto?.File)) { + if (string.Equals(curr.Oto.File, prev.Oto.File, StringComparison.OrdinalIgnoreCase)) { + score += curr.IsPhonetic ? 150 : 250; // Priority connection bonus + } + } + + // Multi-phoneme chunk overlaps in recording filename + if (!string.IsNullOrEmpty(prevChunk) && !string.IsNullOrEmpty(currChunk)) { + string backwardOverlap = MergeChunks(prevChunk, currChunk); + if (!string.IsNullOrEmpty(backwardOverlap)) { + if (curr.PaddedWav != null && curr.PaddedWav.Contains($"_{backwardOverlap}_")) score += 180; + if (prev?.PaddedWav != null && prev.PaddedWav.Contains($"_{backwardOverlap}_")) score += 180; + } + } + + // Cross-chunk presence + if (!string.IsNullOrEmpty(prevChunk) && curr.PaddedWav != null && curr.PaddedWav.Contains($"_{prevChunk}_")) score += 40; + if (!string.IsNullOrEmpty(currChunk) && prev?.PaddedWav != null && prev.PaddedWav.Contains($"_{currChunk}_")) score += 40; + + return score; + } + + private UOto runningOto = null; + + protected override void SyncAttributes(Note[] notes, List phonemeSymbols, int startIndex, List attrList) { + if (singer == null || !singer.Loaded || phonemeSymbols.Count == 0 || notes == null || notes.Length == 0) return; + + int tone = notes[0].tone; + int n = phonemeSymbols.Count; + + var (curUN, nextUN) = UNoteAt(notes[0].position); + int curIdx = curUN != null ? unotes.IndexOf(curUN) : -1; + var prevUN = curIdx > 0 ? unotes[curIdx - 1] : null; + + int noteStartPos = notes[0].position; + int noteEndPos = notes.Last().position + notes.Last().duration; + + bool isPhraseStart = prevUN == null || noteStartPos > (prevUN.position + prevUN.duration + 10); + bool isPhraseEnd = nextUN == null || nextUN.position > (noteEndPos + 10); + + if (isPhraseStart) { + runningOto = null; + } + + string[] cleanAliases = new string[n]; + string[] chunks = new string[n]; + List[] candidatesPerPhoneme = new List[n]; + + for (int i = 0; i < n; i++) { + int globalIdx = startIndex + i; + var attr = attrList.FirstOrDefault(a => a.index == globalIdx); + + cleanAliases[i] = GetPureAlias(phonemeSymbols[i], singer); + chunks[i] = FormatForChunking(cleanAliases[i]); + + string color = attr.voiceColor ?? ""; + int shiftTone = tone + (attr.toneShift ?? 0); + + if (attr.alternate.HasValue) { + // manual selection + if (TryGetMappedOtoAnyFormat(singer, cleanAliases[i].ToLower(), attr.alternate.Value, shiftTone, color, out var manualOto)) { + string norm = Path.GetFileNameWithoutExtension(manualOto.File ?? "").ToLower() + .Replace("-", "").Trim().Replace(" ", "_"); + while (norm.Contains("__")) norm = norm.Replace("__", "_"); + + candidatesPerPhoneme[i] = new List { + new AltCandidate { + Alt = attr.alternate.Value, + Oto = manualOto, + WavNorm = norm, + PaddedWav = $"_{norm}_", + IsPhonetic = norm.Any(char.IsLetter) + } + }; + } else { + candidatesPerPhoneme[i] = GetCandidates(cleanAliases[i], singer, shiftTone, color); + } + } else { + candidatesPerPhoneme[i] = GetCandidates(cleanAliases[i], singer, shiftTone, color); + } + } + + AltCandidate initialPrev = null; + string initialPrevChunk = ""; + if (!isPhraseStart && runningOto != null) { + string norm = Path.GetFileNameWithoutExtension(runningOto.File ?? "").ToLower() + .Replace("-", "").Trim().Replace(" ", "_"); + while (norm.Contains("__")) norm = norm.Replace("__", "_"); + + initialPrev = new AltCandidate { + Alt = 0, + Oto = runningOto, + WavNorm = norm, + PaddedWav = $"_{norm}_", + IsPhonetic = norm.Any(char.IsLetter) + }; + initialPrevChunk = FormatForChunking(GetPureAlias(runningOto.Alias, singer)); + } + + List nextNoteCandidates = null; + string nextNoteChunk = ""; + if (!isPhraseEnd && nextUN != null) { + var nextSymbols = base.GetSymbols(new Note { lyric = nextUN.lyric, tone = nextUN.tone }); + if (nextSymbols != null && nextSymbols.Length > 0) { + string nextFirstClean = GetPureAlias(nextSymbols[0], singer); + nextNoteChunk = FormatForChunking(nextFirstClean); + nextNoteCandidates = GetCandidates(nextFirstClean, singer, nextUN.tone, ""); + } + } + + int[][] dp = new int[n][]; + int[][] parent = new int[n][]; + + for (int i = 0; i < n; i++) { + dp[i] = new int[candidatesPerPhoneme[i].Count]; + parent[i] = new int[candidatesPerPhoneme[i].Count]; + } + + // Score with initialPrev + for (int c = 0; c < candidatesPerPhoneme[0].Count; c++) { + var curr = candidatesPerPhoneme[0][c]; + dp[0][c] = ScoreTransition(curr, initialPrev, chunks[0], initialPrevChunk); + parent[0][c] = -1; + } + + // n-1: Propagate pairwise path scores + for (int i = 1; i < n; i++) { + string prevChunk = chunks[i - 1]; + string currChunk = chunks[i]; + + for (int currIdx = 0; currIdx < candidatesPerPhoneme[i].Count; currIdx++) { + var curr = candidatesPerPhoneme[i][currIdx]; + int maxScore = int.MinValue; + int bestParent = 0; + + for (int prevIdx = 0; prevIdx < candidatesPerPhoneme[i - 1].Count; prevIdx++) { + var prev = candidatesPerPhoneme[i - 1][prevIdx]; + int transScore = ScoreTransition(curr, prev, currChunk, prevChunk); + int total = dp[i - 1][prevIdx] + transScore; + + if (total > maxScore) { + maxScore = total; + bestParent = prevIdx; + } + } + + dp[i][currIdx] = maxScore; + parent[i][currIdx] = bestParent; + } + } + + if (nextNoteCandidates != null && nextNoteCandidates.Count > 0) { + for (int c = 0; c < candidatesPerPhoneme[n - 1].Count; c++) { + var curr = candidatesPerPhoneme[n - 1][c]; + int bestNextBonus = 0; + foreach (var nextCand in nextNoteCandidates) { + int forwardScore = ScoreTransition(nextCand, curr, nextNoteChunk, chunks[n - 1]); + if (forwardScore > bestNextBonus) { + bestNextBonus = forwardScore; + } + } + dp[n - 1][c] += bestNextBonus; + } + } + + // Backtrack optimal path + int bestEndIdx = 0; + int highestFinalScore = int.MinValue; + for (int c = 0; c < candidatesPerPhoneme[n - 1].Count; c++) { + if (dp[n - 1][c] > highestFinalScore) { + highestFinalScore = dp[n - 1][c]; + bestEndIdx = c; + } + } + + int[] optimalAltIndices = new int[n]; + int currTrackIdx = bestEndIdx; + for (int i = n - 1; i >= 0; i--) { + optimalAltIndices[i] = currTrackIdx; + currTrackIdx = parent[i][currTrackIdx]; + } + + // Apply selected alternates and update continuity + for (int i = 0; i < n; i++) { + int globalIdx = startIndex + i; + int existingIdx = attrList.FindIndex(a => a.index == globalIdx); + var attr = existingIdx >= 0 ? attrList[existingIdx] : new PhonemeAttributes { index = globalIdx }; + + var candidates = candidatesPerPhoneme[i]; + int pickIdx = optimalAltIndices[i]; + if (pickIdx < 0 || pickIdx >= candidates.Count) { + pickIdx = 0; + } + + if (candidates.Count > 0) { + var chosenCandidate = candidates[pickIdx]; + if (!attr.alternate.HasValue && chosenCandidate.Alt > 0) { + attr.alternate = chosenCandidate.Alt; + } + + if (chosenCandidate.Oto != null) { + runningOto = chosenCandidate.Oto; + } + } + + if (existingIdx >= 0) attrList[existingIdx] = attr; + else attrList.Add(attr); } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; + + if (isPhraseEnd) { + runningOto = null; } - return phoneme; } + protected override List ProcessSyllable(Syllable syllable) { syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); @@ -239,32 +500,6 @@ protected override List ProcessSyllable(Syllable syllable) { string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - bool isAtomicCluster = cc.Length == 2 && ccvException.Contains(cc[0]); - - // Check for missing vowel phonemes - foreach (var entry in missingVphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isMissingVPhonemes = true; - break; - } - } - - // Check for missing consonant phonemes - foreach (var entry in missingCphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isMissingCPhonemes = true; - break; - } - } - - // Check for missing YAML fallback phonemes - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - // For VC Fallback phonemes foreach (var entry in vcFallBacks) { if (!HasOto($"{entry.Key} {cc}", syllable.tone) || (!HasOto($"ao {cc}", syllable.tone))) { @@ -279,52 +514,114 @@ protected override List ProcessSyllable(Syllable syllable) { // [V V] or [V C][C V]/[V] else if (syllable.IsVV) { if (!CanMakeAliasExtension(syllable)) { - basePhoneme = $"{prevV} {v}"; - if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && vvExceptions.ContainsKey(prevV) && prevV != v) { - // VV IS NOT PRESENT, CHECKS VVEXCEPTIONS LOGIC - //var vc = $"{prevV}{vvExceptions[prevV]}"; - var vc = AliasFormat($"{vvExceptions[prevV]}", "vcEx", syllable.vowelTone, prevV); - phonemes.Add(vc); - basePhoneme = ValidateAlias(AliasFormat($"{vvExceptions[prevV]} {v}", "dynMid", syllable.vowelTone, "")); + + string vvSpace = $"{prevV} {v}"; + string vvNoSpace = $"{prevV}{v}"; + string validVvSpace = ValidateAlias(vvSpace, syllable.vowelTone); + string validVvNoSpace = ValidateAlias(vvNoSpace, syllable.vowelTone); + + // VV with space + if (HasOto(vvSpace, syllable.vowelTone)) { + basePhoneme = vvSpace; + } else if (HasOto(validVvSpace, syllable.vowelTone)) { + basePhoneme = validVvSpace; + } + // VV without space + else if (HasOto(vvNoSpace, syllable.vowelTone)) { + basePhoneme = vvNoSpace; + } else if (HasOto(validVvNoSpace, syllable.vowelTone)) { + basePhoneme = validVvNoSpace; + } + + // Diphthong Fallbacks & Splitting + else if (diphthongSplits.ContainsKey(prevV) || diphthongTails.ContainsKey(prevV)) { + string cv = ""; + if (diphthongSplits.ContainsKey(prevV)) { + var splitOverride = diphthongSplits[prevV]; + var vc = AliasFormat(splitOverride[0].Replace("{v}", v), "vcEx", syllable.tone, prevV); + cv = AliasFormat(splitOverride[1].Replace("{v}", v), "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc, syllable.tone)); + } + else { // Default YAML diphthong logic + var tail = diphthongTails[prevV]; + var vcSpace = AliasFormat($"{prevV} {tail}", "vcEx", syllable.tone, prevV); + var vcNoSpace = AliasFormat($"{prevV}{tail}", "vcEx", syllable.tone, prevV); + cv = AliasFormat($"{tail} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, vcSpace, ValidateAlias(vcSpace, syllable.tone), vcNoSpace, ValidateAlias(vcNoSpace, syllable.tone)); + } + + string validCv = ValidateAlias(cv, syllable.vowelTone); + string validV = ValidateAlias(v, syllable.vowelTone); + if (HasOto(cv, syllable.vowelTone)) { + basePhoneme = cv; + } else if (HasOto(validCv, syllable.vowelTone)) { + basePhoneme = validCv; + } else if (HasOto(v, syllable.vowelTone)) { + basePhoneme = v; + } else if (HasOto(validV, syllable.vowelTone)) { + basePhoneme = validV; + } else { + basePhoneme = ValidateAlias(AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone); + phonemes.Add(ValidateAlias(AliasFormat($"{prevV} -", "dynMid", syllable.tone, ""), syllable.tone)); + } } else { - { - if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV} {v}"; - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { - basePhoneme = v; - } else { - basePhoneme = AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""); - phonemes.Add(AliasFormat($"{prevV} -", "dynMid", syllable.vowelTone, "")); - } + string validV = ValidateAlias(v, syllable.vowelTone); + if (HasOto(v, syllable.vowelTone)) { + basePhoneme = v; + } else if (HasOto(validV, syllable.vowelTone)) { + basePhoneme = validV; + } else { + basePhoneme = ValidateAlias(AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone); + phonemes.Add(ValidateAlias(AliasFormat($"{prevV} -", "dynMid", syllable.tone, ""), syllable.tone)); } } - // EXTEND AS [V] - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone) || missingVphonemes.ContainsKey(prevV)) { - basePhoneme = v; - } else if (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone) && vvDiphthongExceptions.ContainsKey(prevV)) { - basePhoneme = $"{vvDiphthongExceptions[prevV]} {vvDiphthongExceptions[prevV]}"; - } else { - // PREVIOUS ALIAS WILL EXTEND as [V V] + } + else { basePhoneme = null; } - // [- CV/C V] or [- C][CV/C V] } else if (syllable.IsStartingCVWithOneConsonant) { var rcv = $"- {cc[0]} {v}"; var rcv1 = $"- {cc[0]}{v}"; var crv = $"{cc[0]} {v}"; /// - CV - if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) || HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { + if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv, syllable.vowelTone), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) || HasOto(ValidateAlias(rcv1, syllable.vowelTone), syllable.vowelTone))) { basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); /// CV - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv, syllable.vowelTone), syllable.vowelTone)) { basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + bool foundStart = false; + for (int len = cc[0].Length; len > 0; len--) { + string c = cc[0].Substring(0, len); // shr -> sh -> s + string targetStart = AliasFormat(c, "cc_start", syllable.vowelTone, ""); + string validStart = ValidateAlias(targetStart, syllable.vowelTone); + + if (HasOto(targetStart, syllable.vowelTone) || HasOto(validStart, syllable.vowelTone)) { + TryAddPhoneme(phonemes, syllable.tone, targetStart, validStart); + foundStart = true; + break; + } + } + if (!foundStart) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), syllable.vowelTone)); + } } else { basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + bool foundStart = false; + for (int len = cc[0].Length; len > 0; len--) { + string c = cc[0].Substring(0, len); // shr -> sh -> s + string targetStart = AliasFormat(c, "cc_start", syllable.vowelTone, ""); + string validStart = ValidateAlias(targetStart, syllable.vowelTone); + + if (HasOto(targetStart, syllable.vowelTone) || HasOto(validStart, syllable.vowelTone)) { + TryAddPhoneme(phonemes, syllable.tone, targetStart, validStart); + foundStart = true; + break; + } + } + if (!foundStart) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), syllable.vowelTone)); + } } // [CCV/CC V] or [C C] + [CV/C V] } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { @@ -336,15 +633,15 @@ protected override List ProcessSyllable(Syllable syllable) { var ccv = $"{string.Join("", cc)} {v}"; var ccv1 = $"{string.Join("", cc)}{v}"; /// - CCV - if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !isAtomicCluster) { + if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv, syllable.vowelTone), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1, syllable.vowelTone), syllable.vowelTone)) { basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); lastC = 0; } else { /// CCV and CV - if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone))) { + if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv, syllable.vowelTone), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1, syllable.vowelTone), syllable.vowelTone))) { basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); lastC = 0; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv, syllable.vowelTone), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1, syllable.vowelTone), syllable.vowelTone)) { basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); } else { basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); @@ -352,93 +649,115 @@ protected override List ProcessSyllable(Syllable syllable) { // TRY RCC [- CC] if (!phoneticHint) { for (var i = cc.Length; i > 1; i--) { - if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, "")))) { + if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""), syllable.vowelTone))) { firstC = i - 1; break; } } } // [- C] - // todo: deincremental search for starting consonant clusters [str] → [st] → [s] if (phonemes.Count == 0) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), syllable.vowelTone)); } } - } else { + } else { // VCV + var vcv = $"{prevV} {cc[0]}{v}"; + var vcv2 = $"{prevV}{cc[0]}{v}"; + var vcvEnd = $"{prevV}{cc[0]} {v}"; + var vccv = $"{prevV} {string.Join("", cc)}{v}"; + var vccv2 = $"{prevV} {string.Join("", cc)}"; + var vccv3 = $"{prevV}{string.Join("", cc)}"; var crv = $"{cc.Last()} {v}"; var cv = $"{cc.Last()}{v}"; - /// CV - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + bool sameSubbank = AreTonesFromTheSameSubbank(syllable.tone, syllable.vowelTone); + // Use regular VCV if the current word starts with one consonant and the previous word ends with none + if (sameSubbank && syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + } else if (sameSubbank && syllable.IsVCVWithOneConsonant && (HasOto(vcv2, syllable.vowelTone) && HasOto(ValidateAlias(vcv2), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv2; + // Use end VCV if current word does not start with a consonant but the previous word does end with one + } else if (sameSubbank && syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) && HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { + basePhoneme = vcvEnd; + // Use regular VCV if end VCV does not exist + } else if (sameSubbank && syllable.IsVCVWithOneConsonant && (!HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone)) && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone))) { + basePhoneme = vcv; + // VCV with multiple consonants, only for current word onset and null previous word ending + } else if (sameSubbank && syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) && HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { + basePhoneme = vccv; + lastC = 0; + } else if (sameSubbank && syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv3, syllable.vowelTone) && HasOto(ValidateAlias(vccv3), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{prevV} {string.Join("", cc)}{v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // try [CC V] or [CCV] - for (var i = firstC; i < cc.Length - 1; i++) { - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - /// CCV - if (CurrentWordCc.Length >= 2 && !isAtomicCluster) { - if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - /// C-Last - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + /// CV + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv, syllable.vowelTone), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv, syllable.vowelTone), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // try [CC V] or [CCV] + for (var i = firstC; i < cc.Length - 1; i++) { + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + /// CCV + if (CurrentWordCc.Length >= 2) { + if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + /// C-Last + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } } } - } - // try [V C], [V CC], [VC C], [V -][- C] - for (var i = lastC + 1; i >= 0; i--) { - var vr = $"{prevV} -"; - var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; - var vc = $"{prevV} {cc[0]}"; - // Boolean Triggers - bool CCV = false; - if (!phoneticHint && CurrentWordCc.Length >= 2 && !isAtomicCluster) { - if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { - CCV = true; + // try [V C], [V CC], [VC C], [V -][- C] + for (var i = lastC + 1; i >= 0; i--) { + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vc = $"{prevV} {cc[0]}"; + var vr = AliasFormat($"{v} -", "ending", syllable.tone, ""); + // Boolean Triggers + bool CCV = false; + if (!phoneticHint && CurrentWordCc.Length >= 2) { + if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { + CCV = true; + } } - } - bool lastVC = false; - for (int len = cc[0].Length; len > 0; len--) { - string c = cc[0].Substring(0, len); // shr → sh → s - string vcTry = $"{prevV} {c}"; + bool lastVC = false; + for (int len = cc[0].Length; len > 0; len--) { + string c = cc[0].Substring(0, len); // shr → sh → s + string vcTry = $"{prevV} {c}"; - bool hasVC = - HasOto(vc, syllable.tone) || - HasOto(ValidateAlias(vc), syllable.tone); + bool hasVC = + HasOto(vc, syllable.tone) || + HasOto(ValidateAlias(vc, syllable.tone), syllable.tone); - if (!hasVC && (HasOto(vcTry, syllable.tone) || HasOto(ValidateAlias(vcTry), syllable.tone))) { - phonemes.Add(vcTry); - lastVC = true; + if (!hasVC && (HasOto(vcTry, syllable.tone) || HasOto(ValidateAlias(vcTry, syllable.tone), syllable.tone))) { + TryAddPhoneme(phonemes, syllable.tone, vcTry, ValidateAlias(vcTry, syllable.tone)); + lastVC = true; + break; + } + } + if (lastVC) { break; } - } - if (lastVC) { - break; - } - if (!lastVC && i == 0 && (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) && !HasOto(vc, syllable.tone)) { - phonemes.Add(vr); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - break; - } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { - phonemes.Add(vcc); - firstC = 1; - break; - } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { - phonemes.Add(vc); - break; - } else { - continue; + if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc, syllable.tone), syllable.tone)) && CCV) { + TryAddPhoneme(phonemes, syllable.tone, vcc, ValidateAlias(vcc, syllable.tone)); + firstC = 1; + break; + } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc, syllable.tone), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc, syllable.tone)); + break; + } else { + continue; + } } } } @@ -453,24 +772,24 @@ protected override List ProcessSyllable(Syllable syllable) { string c = cc[i + 1].Substring(0, len); // shr → sh → s string ccTry = $"{cc[i]} {c}"; - if (HasOto(ccTry, syllable.tone) && !(HasOto(cc1, syllable.tone) || HasOto(ValidateAlias(cc1), syllable.tone))) { + if (HasOto(ccTry, syllable.tone) && !(HasOto(cc1, syllable.tone) || HasOto(ValidateAlias(cc1, syllable.tone), syllable.tone))) { cc1 = ccTry; break; } } if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // [C1 C2] if (!HasOto(cc1, syllable.tone)) { cc1 = $"{cc[i]} {cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1, syllable.tone), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { var c1 = cc[i]; var c2 = cc[i + 1]; bool c1IsException = consExceptions.Contains(c1); @@ -478,8 +797,8 @@ protected override List ProcessSyllable(Syllable syllable) { // Scenario 1: Both are NOT exceptions if (!c1IsException && !c2IsException) { - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, ValidateAlias(AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""))); + //cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, ValidateAlias(AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""), syllable.vowelTone)); } // Scenario 2: C1 is an exception, C2 is NOT else if (c1IsException && !c2IsException) { @@ -495,14 +814,15 @@ protected override List ProcessSyllable(Syllable syllable) { } } if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // CCV if (CurrentWordCc.Length >= 2) { - if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !isAtomicCluster)) { + bool canGlide = true; + if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv, syllable.vowelTone), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1, syllable.vowelTone), syllable.vowelTone))) { basePhoneme = (AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, "")); lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv, syllable.vowelTone), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv, syllable.vowelTone), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); } // [C1 C2C3] @@ -510,6 +830,12 @@ protected override List ProcessSyllable(Syllable syllable) { cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; lastC = i; } + if (canGlide) { + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(cc1); + } + } // CV } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); @@ -521,17 +847,17 @@ protected override List ProcessSyllable(Syllable syllable) { if (i + 1 < lastC) { if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // [C1 C2] if (!HasOto(cc1, syllable.tone)) { cc1 = $"{cc[i]} {cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1, syllable.tone), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { var c1 = cc[i]; var c2 = cc[i + 1]; bool c1IsException = consExceptions.Contains(c1); @@ -540,8 +866,8 @@ protected override List ProcessSyllable(Syllable syllable) { // Scenario 1: Both are NOT exceptions if (!c1IsException && !c2IsException) { // [C1 -] [- C2] - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, ValidateAlias(AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""))); + //cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, ValidateAlias(AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""), syllable.vowelTone)); } // Scenario 2: C1 is an exception, C2 is NOT else if (c1IsException && !c2IsException) { @@ -557,20 +883,27 @@ protected override List ProcessSyllable(Syllable syllable) { } } if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } // CCV if (CurrentWordCc.Length >= 2) { - if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !isAtomicCluster)) { + bool canGlide = true; + if (!phoneticHint && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv, syllable.vowelTone), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1, syllable.vowelTone), syllable.vowelTone))) { basePhoneme = (AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, "")); lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv, syllable.vowelTone), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv, syllable.vowelTone), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); } // [C1 C2C3] if (!phoneticHint && (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone))) { cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; } + if (canGlide) { + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(cc1); + } + } // CV } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); @@ -583,7 +916,7 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { // like [V C1] [C1 C2] [C2 C3] [C3 ..] phonemes.Add(cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1))) { + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1, syllable.tone))) { // like [V C1] [C1 C2] [C2 ..] if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { i++; @@ -591,9 +924,9 @@ protected override List ProcessSyllable(Syllable syllable) { } else { // singular cc if (PreviousWordCc.Contains(cc1) == CurrentWordCc.Contains(cc1)) { - cc1 = ValidateAlias(cc1); + cc1 = ValidateAlias(cc1, syllable.tone); } else { - TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); + TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i], syllable.tone)); } } } else { @@ -618,8 +951,8 @@ protected override List ProcessEnding(Ending ending) { if (ending.IsEndingV) { var vR = $"{prevV} {t}"; var vR2 = $"{prevV}{t}"; - if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { - phonemes.Add(AliasFormat($"{prevV}", "ending", ending.tone, "", t)); + if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR, ending.tone), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{prevV}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{prevV}", "ending", ending.tone, "", t), ending.tone)); } } else if (ending.IsEndingVCWithOneConsonant) { var vc = $"{prevV} {cc[0]}"; @@ -628,28 +961,28 @@ protected override List ProcessEnding(Ending ending) { var vcr3 = $"{prevV} {cc[0]} {t}"; var vcr4 = $"{prevV}{cc[0]}{t}"; if (!RomajiException.Contains(cc[0])) { - if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || (HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone))) { - phonemes.Add(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t)); - } else if (HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - phonemes.Add(vcr3); - } else if (HasOto(vcr4, ending.tone) && HasOto(ValidateAlias(vcr4), ending.tone)) { - phonemes.Add(vcr4); - } else if (HasOto(vc, ending.tone) && HasOto(ValidateAlias(vc), ending.tone)) { - phonemes.Add(vc); + if (HasOto(vcr, ending.tone) || HasOto(ValidateAlias(vcr, ending.tone), ending.tone) || (HasOto(vcr2, ending.tone) || HasOto(ValidateAlias(vcr2, ending.tone), ending.tone))) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ValidateAlias(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ending.tone)); + } else if (HasOto(vcr3, ending.tone) || HasOto(ValidateAlias(vcr3, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vcr3, ValidateAlias(vcr3, ending.tone)); + } else if (HasOto(vcr4, ending.tone) || HasOto(ValidateAlias(vcr4, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vcr4, ValidateAlias(vcr4, ending.tone)); + } else if (HasOto(vc, ending.tone) || HasOto(ValidateAlias(vc, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc, ValidateAlias(vc, ending.tone)); if (vc.Contains(cc[0])) { - phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{cc[0]}", "ending", ending.tone, "", t), ending.tone)); } } else { for (int len = cc[0].Length; len > 0; len--) { string c = cc[0].Substring(0, len); // shr → sh → s string vcTry = $"{prevV} {c}"; - if ( HasOto(vcTry, ending.tone) || HasOto(ValidateAlias(vcTry), ending.tone)) { - phonemes.Add(vcTry); + if ( HasOto(vcTry, ending.tone) || HasOto(ValidateAlias(vcTry, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vcTry, ValidateAlias(vcTry, ending.tone)); break; } } if (vc.Contains(cc[0])) { - phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{cc[0]}", "ending", ending.tone, "", t), ending.tone)); } } } @@ -665,48 +998,48 @@ protected override List ProcessEnding(Ending ending) { var vc = $"{v} {cc[0]}"; if (!RomajiException.Contains(cc[0])) { if (i == 0) { - if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { - phonemes.Add(AliasFormat($"{v}", "ending", ending.tone, "", t)); + if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr, ending.tone), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2, ending.tone), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1, ending.tone), ending.tone) && !HasOto(vc, ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{v}", "ending", ending.tone, "", t), ending.tone)); } break; - } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc); + } else if (HasOto(vcc, ending.tone) || HasOto(ValidateAlias(vcc, ending.tone), ending.tone) && lastC == 1) { + TryAddPhoneme(phonemes, ending.tone, vcc, ValidateAlias(vcc, ending.tone)); firstC = 1; break; - } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc2); + } else if (HasOto(vcc2, ending.tone) || HasOto(ValidateAlias(vcc2, ending.tone), ending.tone) && lastC == 1) { + TryAddPhoneme(phonemes, ending.tone, vcc2, ValidateAlias(vcc2, ending.tone)); firstC = 1; break; - } else if (!phoneticHint && (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0]))) { - phonemes.Add(vcc3); + } else if (!phoneticHint && (HasOto(vcc3, ending.tone) || HasOto(ValidateAlias(vcc3, ending.tone), ending.tone))) { + TryAddPhoneme(phonemes, ending.tone, vcc3, ValidateAlias(vcc3, ending.tone)); if (vcc3.EndsWith(cc.Last()) && lastC == 1) { if (consonants.Contains(cc.Last())) { - phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t), ending.tone)); } } firstC = 1; break; - } else if (!phoneticHint && (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0]))) { - phonemes.Add(vcc4); + } else if (!phoneticHint && (HasOto(vcc4, ending.tone) || HasOto(ValidateAlias(vcc4, ending.tone), ending.tone))) { + TryAddPhoneme(phonemes, ending.tone, vcc4, ValidateAlias(vcc4, ending.tone)); if (vcc4.EndsWith(cc.Last()) && lastC == 1) { if (consonants.Contains(cc.Last())) { - phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t), ending.tone)); } } firstC = 1; break; - } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) - || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) - || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) - || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { - phonemes.Add(vc); + } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc, ending.tone), ending.tone) + || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2, ending.tone), ending.tone) + || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3, ending.tone), ending.tone) + || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc, ValidateAlias(vc, ending.tone)); break; } else { for (int len = cc[0].Length; len > 0; len--) { string c = cc[0].Substring(0, len); // shr → sh → s string vcTry = $"{prevV} {c}"; - if (HasOto(vcTry, ending.tone) || HasOto(ValidateAlias(vcTry), ending.tone)) { - phonemes.Add(vcTry); + if (HasOto(vcTry, ending.tone) || HasOto(ValidateAlias(vcTry, ending.tone), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vcTry, ValidateAlias(vcTry, ending.tone)); break; } } @@ -715,145 +1048,98 @@ protected override List ProcessEnding(Ending ending) { } } for (var i = firstC; i < lastC; i++) { - var cc1 = $"{cc[i]} {cc[i + 1]}"; - if (i < cc.Length - 2) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + int remainingCount = cc.Length - i; + bool matchedEndingCluster = false; + + // ([ccc-], [c cc-], [cc c-], [cc-], [c c-]) + if (!phoneticHint && remainingCount >= 2) { + for (int clusterLength = Math.Min(3, remainingCount); clusterLength >= 2; clusterLength--) { + if (i + clusterLength == cc.Length) { + var cluster = cc.Skip(i).Take(clusterLength).ToArray(); + var patterns = new List { + string.Join("", cluster) // "st", "str" + }; + + if (clusterLength == 3) { + patterns.Add($"{cluster[0]} {cluster[1]}{cluster[2]}"); // "s tr" + patterns.Add($"{cluster[0]}{cluster[1]} {cluster[2]}"); // "st r" + patterns.Add($"{cluster[0]} {cluster[1]} {cluster[2]}"); // "s t r" + } else if (clusterLength == 2) { + patterns.Add($"{cluster[0]} {cluster[1]}"); // "s t" + } - for (int len = cc[i + 2].Length; len > 0; len--) { - string c = cc[i + 2].Substring(0, len); // shr → sh → s - string ccTry = $"{cc[i + 1]} {c}"; + string[] hyphenVariations = { $"{t}", $" {t}" }; // "-", " -" - if (HasOto(ccTry, ending.tone) && !(HasOto(cc1, ending.tone) || HasOto(ValidateAlias(cc1), ending.tone))) { - cc1 = ccTry; - break; + foreach (var consPattern in patterns) { + foreach (var hyphen in hyphenVariations) { + string candidate = $"{consPattern}{hyphen}"; + if (TryAddPhoneme(phonemes, ending.tone, candidate, ValidateAlias(candidate, ending.tone))) { + matchedEndingCluster = true; + i += clusterLength - 1; + break; + } + } + if (matchedEndingCluster) break; + } + if (matchedEndingCluster) break; } } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc2, ending.tone)) { - cc2 = ValidateAlias(cc2); - } + } - if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { - // [C1 -] [- C2] - cc2 = AliasFormat($"{cc[i + 2]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, ValidateAlias(AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, ""))); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } + if (matchedEndingCluster) { + continue; + } - if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { - // like [C1 C2][C2 ...] - phonemes.Add(cc1); - } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { - // like [C1 C2-][C3 ...] - phonemes.Add(cc[i]); - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { - // like [C1 C2-][C3 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { - // like [C1C2][C2 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "")); - i++; - } else { - // like [C1][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); - i++; + // [c1 c2] + var cc1 = $"{cc[i]} {cc[i + 1]}"; + for (int len = cc[i + 1].Length; len > 0; len--) { + string c = cc[i + 1].Substring(0, len); + string ccTry = $"{cc[i]} {c}"; + if (HasOto(ccTry, ending.tone) && !(HasOto(cc1, ending.tone) || HasOto(ValidateAlias(cc1, ending.tone), ending.tone))) { + cc1 = ccTry; + break; } - // CC that ends with 3 clusters - for (int clusterLength = 3; clusterLength >= 2; clusterLength--) { - if (i + clusterLength > cc.Length) { - continue; - } - var cluster = new string[clusterLength]; - for (int k = 0; k < clusterLength; k++) { - cluster[k] = cc[i + k].ToString(); - } - // Generate all possible spacing patterns for the consonants. - var consonantPatterns = new List(); - consonantPatterns.Add(string.Join("", cluster)); - - // 3 CC. - if (!phoneticHint && clusterLength == 3) { - consonantPatterns.Add($"{cluster[0]} {cluster[1]}{cluster[2]}"); - consonantPatterns.Add($"{cluster[0]}{cluster[1]} {cluster[2]}"); - consonantPatterns.Add($"{cluster[0]} {cluster[1]} {cluster[2]}"); - } - // 2 CC. - else if (clusterLength == 2) { - consonantPatterns.Add($"{cluster[0]} {cluster[1]}"); - consonantPatterns.Add($"{cluster[0]}{cluster[1]}"); - } + } - foreach (var consPattern in consonantPatterns) { - string[] hyphenPatterns = { $"{t}", $" {t}" }; - foreach (var hyphenPattern in hyphenPatterns) { - string endingcc = $"{consPattern}{hyphenPattern}"; + bool hasCc1 = HasOto(cc1, ending.tone) || HasOto(ValidateAlias(cc1, ending.tone), ending.tone) || + HasOto($"{cc[i]} {cc[i + 1]}", ending.tone) || HasOto(ValidateAlias($"{cc[i]} {cc[i + 1]}", ending.tone), ending.tone); - if (TryAddPhoneme(phonemes, ending.tone, endingcc, ValidateAlias(endingcc))) { - i += clusterLength - 1; - } - } - } + if (i < cc.Length - 2) { + if (hasCc1) { + TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1, ending.tone), $"{cc[i]} {cc[i + 1]}", ValidateAlias($"{cc[i]} {cc[i + 1]}", ending.tone)); + } else { + // No [c c] available -> c1 fallback + TryAddPhoneme(phonemes, ending.tone, + ValidateAlias(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), ending.tone), + AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), + $"{cc[i]} {t}", + cc[i]); } } else { - for (int len = cc[i + 1].Length; len > 0; len--) { - string c = cc[i + 1].Substring(0, len); // shr → sh → s - string ccTry = $"{cc[i]} {c}"; - - if (HasOto(ccTry, ending.tone) && !(HasOto(cc1, ending.tone) || HasOto(ValidateAlias(cc1), ending.tone))) { - cc1 = ccTry; - break; - } - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 -] [- C2] - if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - cc1 = AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, ValidateAlias(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""))); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - // CC that ends with 2 clusters - if (!phoneticHint && (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}")))) { - // like [C1 C2-] - i++; - } else if (!phoneticHint && (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]} {t}", ValidateAlias($"{cc[i]} {cc[i + 1]} {t}")))) { - // like [C1 C2 -] - i++; - } else if (!phoneticHint && (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}{t}", ValidateAlias($"{cc[i]}{cc[i + 1]}{t}")))) { - // like [C1C2-] - i++; - } else if (!phoneticHint && (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]} {t}", ValidateAlias($"{cc[i]}{cc[i + 1]} {t}")))) { - // like [C1C2 -] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - // like [C1 C2][C2 -] - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 2]}", "cc_endB", ending.tone, "")); - i++; + // Final consonant pair + if (hasCc1) { + TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1, ending.tone), $"{cc[i]} {cc[i + 1]}", ValidateAlias($"{cc[i]} {cc[i + 1]}", ending.tone)); + + // Resolve final tail closure for c2 + TryAddPhoneme(phonemes, ending.tone, + ValidateAlias(AliasFormat($"{cc[i + 1]}", "ending", ending.tone, "", t), ending.tone), + AliasFormat($"{cc[i + 1]}", "ending", ending.tone, "", t), + $"{cc[i + 1]} {t}", + ValidateAlias($"{cc[i + 1]} {t}", ending.tone), + cc[i + 1]); + } else { + TryAddPhoneme(phonemes, ending.tone, + ValidateAlias(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), ending.tone), + AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), + $"{cc[i]} {t}", + cc[i]); + + TryAddPhoneme(phonemes, ending.tone, + ValidateAlias(AliasFormat($"{cc[i + 1]}", "ending", ending.tone, "", t), ending.tone), + AliasFormat($"{cc[i + 1]}", "ending", ending.tone, "", t), + $"{cc[i + 1]} {t}", + ValidateAlias($"{cc[i + 1]} {t}", ending.tone), + cc[i + 1]); } } } @@ -862,7 +1148,6 @@ protected override List ProcessEnding(Ending ending) { } private string AliasFormat(string alias, string type, int tone, string prevV, string t = "-") { var aliasFormats = new Dictionary { - // Define alias formats for different types { "dynStart", new string[] { "" } }, { "dynMid", new string[] { "" } }, { "dynMid_vv", new string[] { "" } }, @@ -883,12 +1168,10 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, }; - // Check if the given type exists in the aliasFormats dictionary if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { return alias; } - // Handle dynamic variations when type contains "dynamic" if (type.Contains("dynStart")) { string consonant = ""; string vowel = ""; @@ -900,10 +1183,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } else { consonant = alias; } - - // Handle the alias with space and without space var dynamicVariations = new List { - // Variations with space, dash, and underscore $"- {consonant}{vowel}", // "- CV" $"- {consonant} {vowel}", // "- C V" $"-{consonant} {vowel}", // "-C V" @@ -911,10 +1191,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"-{consonant}_{vowel}", // "-C_V" $"- {consonant}_{vowel}", // "- C_V" }; - // Check each dynamically generated format + foreach (var variation in dynamicVariations) { - if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { + if (HasOto(variation, tone)) { return variation; + } else if (HasOto(ValidateAlias(variation, tone), tone)) { + return ValidateAlias(variation, tone); } } } @@ -922,7 +1204,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st if (type.Contains("dynMid")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[0]; @@ -935,10 +1217,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{consonant} {vowel}", // "C V" $"{consonant}_{vowel}", // "C_V" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1, tone), tone)) { + return ValidateAlias(variation1, tone); } } } @@ -946,7 +1230,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st if (type.Contains("dynEnd")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[1]; @@ -960,10 +1244,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{vowel}{consonant}-", // "VC-" $"{vowel} {consonant} -", // "V C -" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1, tone), tone)) { + return ValidateAlias(variation1, tone); } } } @@ -981,842 +1267,350 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } else { aliasFormat = $"{format}{alias}"; } - // Check if the formatted alias exists - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + + if (HasOto(aliasFormat, tone)) { return aliasFormat; + } else if (HasOto(ValidateAlias(aliasFormat, tone), tone)) { + return ValidateAlias(aliasFormat, tone); } } return alias; } - protected override string ValidateAlias(string alias) { - - //CV FALLBACKS - if (alias == "ng ao") { - return alias.Replace("ao", "ow"); - } else if (alias == "ch ao") { - return alias.Replace("ch ao", "sh ow"); - } else if (alias == "dh ao") { - return alias.Replace("ao", "ow"); - } else if (alias == "dh oy") { - return alias.Replace("oy", "ow"); - } else if (alias == "jh ao") { - return alias.Replace("ao", "oy"); - } else if (alias == "ao -") { - return alias.Replace("ao -", "aa -"); - } else if (alias == "v ao") { - return alias.Replace("v", "b"); - } else if (alias == "z ao") { - return alias.Replace("z", "s"); - } else if (alias == "ng eh") { - return alias.Replace("ng", "n"); - } else if (alias == "z eh") { - return alias.Replace("z", "s"); - } else if (alias == "jh er") { - return alias.Replace("jh", "z"); - } else if (alias == "ng er") { - return alias.Replace("ng", "n"); - } else if (alias == "r er") { - return alias.Replace("r er", "er"); - } else if (alias == "th er") { - return alias.Replace("th er", "th r"); - } else if (alias == "jh ey") { - return alias.Replace("ey", "ae"); - } else if (alias == "ng ey") { - return alias.Replace("ng", "n"); - } else if (alias == "th ey") { - return alias.Replace("ey", "ae"); - } else if (alias == "zh ey") { - return alias.Replace("zh ey", "jh ae"); - } else if (alias == "ch ow") { - return alias.Replace("ch", "sh"); - } else if (alias == "jh ow") { - return alias.Replace("ow", "oy"); - } else if (alias == "v ow") { - return alias.Replace("v", "b"); - } else if (alias == "th ow") { - return alias.Replace("th", "s"); - } else if (alias == "z ow") { - return alias.Replace("z", "s"); - } else if (alias == "ch oy") { - return alias.Replace("ch oy", "sh ow"); - } else if (alias == "th oy") { - return alias.Replace("th oy", "s ao"); - } else if (alias == "v oy") { - return alias.Replace("v", "b"); - } else if (alias == "w oy") { - return alias.Replace("oy", "ao"); - } else if (alias == "z oy") { - return alias.Replace("oy", "aa"); - } else if (alias == "ch uh") { - return alias.Replace("ch", "sh"); - } else if (alias == "dh uh") { - return alias.Replace("dh uh", "d uw"); - } else if (alias == "jh uh") { - return alias.Replace("jh", "sh"); - } else if (alias == "ng uh") { - return alias.Replace("ng uh", "n uw"); - } else if (alias == "th uh") { - return alias.Replace("th uh", "f uw"); - } else if (alias == "v uh") { - return alias.Replace("v", "b"); - } else if (alias == "z uh") { - return alias.Replace("z", "s"); - } else if (alias == "ch uw") { - return alias.Replace("ch", "sh"); - } else if (alias == "dh uw") { - return alias.Replace("dh", "d"); - } else if (alias == "g uw") { - return alias.Replace("g", "k"); - } else if (alias == "jh uw") { - return alias.Replace("jh", "sh"); - } else if (alias == "ng uw") { - return alias.Replace("ng", "n"); - } else if (alias == "th uw") { - return alias.Replace("th uw", "f uw"); - } else if (alias == "v uw") { - return alias.Replace("v", "b"); - } else if (alias == "z uw") { - return alias.Replace("z", "s"); - } else if (alias == "zh aa") { - return alias.Replace("zh", "sh"); - } else if (alias == "zh ao") { - return alias.Replace("zh", "sh"); - } else if (alias == "zh ae") { - return alias.Replace("zh ae", "sh ah"); - } else if (alias == "ng oy") { - return alias.Replace("oy", "ow"); - } else if (alias == "sh ao") { - return alias.Replace("ao", "ow"); - } else if (alias == "z uh") { - return alias.Replace("z uh", "s uw"); - } else if (alias == "r uh") { - return alias.Replace("uh", "uw"); - } else if (alias == "sh oy") { - return alias.Replace("oy", "ow"); - } + protected override string ValidateAlias(string alias, int tone = 0) { + if (HasOto(alias, tone)) return alias; - // VALIDATE ALIAS DEPENDING ON METHOD - if (isYamlFallbacks) { - foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; } + alias = baseResolved; } - if (isMissingVPhonemes) { + // Apply Vowel-Only global fallbacks + string vAlias = alias; + if (missingVphonemes != null) { foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isMissingCPhonemes) { - foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - - var CVMappings = new Dictionary { - { "ao", new[] { "ow" } }, - { "oy", new[] { "ow" } }, - { "aw", new[] { "ah" } }, - { "ay", new[] { "ah" } }, - { "eh", new[] { "ae" } }, - { "ey", new[] { "eh" } }, - { "ow", new[] { "ao" } }, - { "uh", new[] { "uw" } }, - }; - foreach (var kvp in CVMappings) { - var v1 = kvp.Key; - var vfallbacks = kvp.Value; - foreach (var vfallback in vfallbacks) { - foreach (var c1 in consonants) { - alias = alias.Replace(c1 + " " + v1, c1 + " " + vfallback); - } + vAlias = vAlias.Replace(fb.Key, fb.Value); } } + if (vAlias != alias && HasOto(vAlias, tone)) return vAlias; - //VV (diphthongs) some - var vvReplacements = new Dictionary> { - { "ay ay", new List { "y ah" } }, - { "ey ey", new List { "iy ey" } }, - { "oy oy", new List { "y ow" } }, - { "er er", new List { "er" } }, - { "aw aw", new List { "w ae" } }, - { "ow ow", new List { "w ao" } }, - { "uw uw", new List { "w uw" } } - }; - - // Apply VV replacements - foreach (var (originalValue, replacementOptions) in vvReplacements) { - foreach (var replacementOption in replacementOptions) { - alias = alias.Replace(originalValue, replacementOption); - } - } - //VC (diphthongs) - //VC (aw specific) - bool vcSpecific = true; - if (vcSpecific) { - if (alias == "aw ch") { - return alias.Replace("ch", "t"); - } - if (alias == "aw jh") { - return alias.Replace("jh", "d"); - } - if (alias == "aw ng") { - return alias.Replace("aw ng", "uh ng"); - } - if (alias == "aw q") { - return alias.Replace("q", "t"); - } - if (alias == "aw zh") { - return alias.Replace("zh", "d"); - } - if (alias == "aw w") { - return alias.Replace("aw", "ah"); - } - - //VC (ay specific) - if (alias == "ay ng") { - return alias.Replace("ay", "ih"); - } - if (alias == "ay q") { - return alias.Replace("q", "t"); - } - if (alias == "ay zh") { - return alias.Replace("zh", "jh"); - } - //VC (ey specific) - if (alias == "ey ng") { - return alias.Replace("ey", "ih"); - } - if (alias == "ey q") { - return alias.Replace("q", "t"); - } - if (alias == "ey zh") { - return alias.Replace("zh", "jh"); - } - //VC (ow specific) - if (alias == "ow ch") { - return alias.Replace("ch", "t"); - } - if (alias == "ow jh") { - return alias.Replace("jh", "d"); - } - if (alias == "ow ng") { - return alias.Replace("ow", "uh"); - } - if (alias == "ow q") { - return alias.Replace("q", "t"); - } - if (alias == "ow zh") { - return alias.Replace("zh", "z"); - } - //VC (oy specific) - if (alias == "- oy") { - return alias.Replace("oy", "ow"); - } - if (alias == "oy f") { - return alias.Replace("oy", "ih"); - } - if (alias == "oy ng") { - return alias.Replace("iy", "ih"); - } - if (alias == "oy q") { - return alias.Replace("oy q", "iy t"); - } - if (alias == "oy zh") { - return alias.Replace("oy zh", "iy jh"); - } - - //VC (aa) - //VC (aa specific) - if (alias == "aa b") { - return alias.Replace("aa b", "aa d"); - } - if (alias == "aa dx") { - return alias.Replace("aa dx", "aa d"); - } - if (alias == "aa q") { - return alias.Replace("aa q", "aa t"); - } - if (alias == "aa y") { - return alias.Replace("aa y", "ah iy"); - } - if (alias == "aa zh") { - return alias.Replace("aa zh", "aa z"); - } - - //VC (ae specific) - if (alias == "ae b") { - return alias.Replace("ae b", "ah d"); - } - if (alias == "ae dx") { - return alias.Replace("ae dx", "ah d"); - } - if (alias == "ae q") { - return alias.Replace("ae q", "ah t"); - } - if (alias == "ae y") { - return alias.Replace("ae y", "ah iy"); - } - if (alias == "ae zh") { - return alias.Replace("ae zh", "ah z"); - } - - //VC (ah specific) - if (alias == "ah b") { - return alias.Replace("ah b", "ah d"); - } - if (alias == "ah dx") { - return alias.Replace("ah dx", "ah d"); - } - if (alias == "ah q") { - return alias.Replace("ah q", "ah t"); - } - if (alias == "ah y") { - return alias.Replace("ah y", "ah iy"); - } - if (alias == "ah zh") { - return alias.Replace("ah zh", "ah z"); - } - - //VC (ao) - //VC (ao specific) - if (alias == "ao b") { - return alias.Replace("ao b", "ah d"); - } - if (alias == "ao dx") { - return alias.Replace("ao dx", "ah d"); - } - if (alias == "ao q") { - return alias.Replace("ao q", "ao t"); - } - if (alias == "ao y") { - return alias.Replace("ao y", "ow y"); - } - if (alias == "ao zh") { - return alias.Replace("ao zh", "ah z"); - } - - //VC (ax) - //VC (ax specific) - if (alias == "ax b") { - return alias.Replace("ax b", "ah d"); - } - if (alias == "ax dx") { - return alias.Replace("ax dx", "ah d"); - } - if (alias == "ax q") { - return alias.Replace("ax q", "ah t"); - } - if (alias == "ax y") { - return alias.Replace("ax y", "ah iy"); - } - if (alias == "ax zh") { - return alias.Replace("ax zh", "ah z"); - } - - //VC (eh) - //VC (eh specific) - if (alias == "eh b") { - return alias.Replace("eh b", "eh d"); - } - if (alias == "eh ch") { - return alias.Replace("eh ch", "eh t"); - } - if (alias == "eh dh") { - return alias.Replace("eh dh", "eh d"); - } - if (alias == "eh dx") { - return alias.Replace("eh dx", "eh d"); - } - if (alias == "eh ng") { - return alias.Replace("eh ng", "eh n"); - } - if (alias == "eh q") { - return alias.Replace("eh q", "eh t"); - } - if (alias == "eh y") { - return alias.Replace("eh y", "ey"); - } - if (alias == "eh zh") { - return alias.Replace("eh zh", "eh s"); - } - - //VC (er specific) - if (alias == "er ch") { - return alias.Replace("er ch", "er t"); - } - if (alias == "er dx") { - return alias.Replace("er dx", "er d"); - } - if (alias == "er jh") { - return alias.Replace("er jh", "er d"); - } - if (alias == "er ng") { - return alias.Replace("er ng", "er n"); - } - if (alias == "er q") { - return alias.Replace("er q", "er t"); - } - if (alias == "er r") { - return alias.Replace("er r", "er"); - } - if (alias == "er sh") { - return alias.Replace("er sh", "er s"); - } - if (alias == "er zh") { - return alias.Replace("er zh", "er z"); - } - - //VC (ih specific) - if (alias == "ih b") { - return alias.Replace("ih b", "ih d"); - } - if (alias == "ih dx") { - return alias.Replace("ih dx", "ih d"); - } - if (alias == "ih hh") { - return alias.Replace("ih hh", "iy hh"); - } - if (alias == "ih q") { - return alias.Replace("ih q", "ih t"); - } - if (alias == "ih w") { - return alias.Replace("ih w", "iy w"); - } - if (alias == "ih y") { - return alias.Replace("ih y", "iy y"); - } - if (alias == "ih zh") { - return alias.Replace("ih zh", "ih z"); - } - - //VC (iy specific) - if (alias == "iy dx") { - return alias.Replace("iy dx", "iy d"); - } - if (alias == "iy f") { - return alias.Replace("iy f", "iy hh"); - } - if (alias == "iy n") { - return alias.Replace("iy n", "iy m"); - } - if (alias == "iy ng") { - return alias.Replace("iy ng", "ih ng"); - } - if (alias == "iy q") { - return alias.Replace("iy q", "iy t"); - } - if (alias == "iy tr") { - return alias.Replace("iy tr", "iy t"); - } - if (alias == "iy zh") { - return alias.Replace("iy zh", "iy z"); - } - - //VC (uh) - //VC (uh specific) - if (alias == "uh ch") { - return alias.Replace("uh ch", "uh t"); - } - if (alias == "uh dx") { - return alias.Replace("uh dx", "uh d"); - } - if (alias == "uh jh") { - return alias.Replace("uh jh", "uw d"); - } - if (alias == "uh q") { - return alias.Replace("uh q", "uh t"); - } - if (alias == "uh zh") { - return alias.Replace("uh zh", "uw z"); - } - - //VC (uw specific) - if (alias == "uw ch") { - return alias.Replace("uw ch", "uw t"); - } - if (alias == "uw dx") { - return alias.Replace("uw dx", "uw d"); - } - if (alias == "uw jh") { - return alias.Replace("uw jh", "uw d"); - } - if (alias == "uw ng") { - return alias.Replace("uw ng", "uw n"); - } - if (alias == "uw q") { - return alias.Replace("uw q", "uw t"); - } - if (alias == "uw zh") { - return alias.Replace("uw zh", "uw sh"); + // Apply Consonant-Only global fallbacks + string cAlias = alias; + if (missingCphonemes != null) { + foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { + cAlias = cAlias.Replace(fb.Key, fb.Value); } } + if (cAlias != alias && HasOto(cAlias, tone)) return cAlias; - bool ccSpecific = true; - if (ccSpecific) { - - //CC (ch specific) - if (alias == "ch r") { - return alias.Replace("ch r", "ch er"); - } - if (alias == "ch w") { - return alias.Replace("ch w", "ch ah"); - } - if (alias == "ch y") { - return alias.Replace("ch y", "ch iy"); - } - if (alias == "ch -") { - return alias.Replace("ch", "jh"); - } - - //CC (f specific) - if (alias == "f z") { - return alias.Replace("z", "s"); - } - if (alias == "f zh") { - return alias.Replace("zh", "s"); - } - if (alias == "f -") { - return alias.Replace("f", "th"); - } - - //CC (hh specific) - if (alias == "hh y") { - return alias.Replace("hh", "f"); - } - - //CC (jh specific) - if (alias == "jh r") { - return alias.Replace("jh r", "jh ah"); - } - if (alias == "jh w") { - return alias.Replace("jh w", "jh ah"); - } - if (alias == "jh y") { - return alias.Replace("y", "iy"); - } - - //CC (l specific) - if (alias == "l ch") { - return alias.Replace("ch", "t"); - } - if (alias == "l b") { - return alias.Replace("b", "d"); - } - if (alias == "l ng") { - return alias.Replace("ng", "n"); - } - if (alias == "l zh") { - return alias.Replace("zh", "z"); - } + // contextual array fallbacks + string contextualAlias = ApplyContextualFallbacks(alias, tone); + if (contextualAlias != alias) return contextualAlias; - //CC (n specific) - if (alias == "n ng") { - return alias.Replace("ng", "n"); - } - if (alias == "n n") { - return alias.Replace("n n", "n"); - } - if (alias == "n m") { - return alias.Replace("n m", "n"); - } - if (alias == "n v") { - return alias.Replace("n v", "n m"); - } - if (alias == "n zh") { - return alias.Replace("zh", "z"); - } - - //CC (ng) - foreach (var c1 in new[] { "ng" }) { - foreach (var c2 in consonants) { - alias = alias.Replace(c1 + " " + c2, "n" + " " + c2); - } - } - - //CC (ng specific) - if (alias == "ng ch") { - return alias.Replace("ch", "t"); - } - if (alias == "ng ng") { - return alias.Replace("ng", "n"); - } - if (alias == "ng zh") { - return alias.Replace("zh", "z"); - } - - //CC (th specific) - if (alias == "th y") { - return alias.Replace("th y", "th ih"); - } - if (alias == "th zh") { - return alias.Replace("zh", "s"); - } - - //CC (v specific) - if (alias == "v dh") { - return alias.Replace("dh", "d"); - } - if (alias == "v th") { - return alias.Replace("v th", "th"); - } - // CC (w C) - foreach (var c2 in consonants) { - if (!(alias.Contains($"aw {c2}") || alias.Contains($"ew {c2}") || alias.Contains($"iw {c2}") || alias.Contains($"ow {c2}") || alias.Contains($"uw {c2}"))) { - alias = alias.Replace($"w {c2}", $"uw {c2}"); - } - } - // CC (C w) - foreach (var c2 in consonants) { - if (!(alias.Contains($"aw {c2}") || alias.Contains($"ew {c2}") || alias.Contains($"iw {c2}") || alias.Contains($"ow {c2}") || alias.Contains($"uw {c2}"))) { - alias = alias.Replace($"{c2} w", $"{c2} uw"); - } - } - if (alias == "w -") { - return alias.Replace("w", "uw"); - } + return alias; + } - //CC (y C) - foreach (var c2 in consonants) { - if (!(alias.Contains($"ay {c2}") || alias.Contains($"ey {c2}") || alias.Contains($"iy {c2}") || alias.Contains($"oy {c2}"))) { - alias = alias.Replace($"y {c2}", $"iy {c2}"); - } + // VV FALLBACKS, START and END + private readonly Dictionary vvVowel1Fallbacks = new Dictionary { + { "aa", new[] { "ah", "ay", "aw", "ae", "ao" } }, + { "ae", new[] { "eh", "aw", "ay", "ah", "aa" } }, + { "ah", new[] { "aa", "aw", "ay", "ae", "ao" } }, + { "ao", new[] { "aa", "ow", "ah", "ay", "ae" } }, + { "ax", new[] { "ah", "uh", "aa" } }, + { "eh", new[] { "ey", "ax" } }, + { "er", new[] { "r", "ah", "ax" } }, + { "ih", new[] { "iy", "eh" } }, + { "iy", new[] { "ih" } }, + { "uh", new[] { "uw" } }, + { "uw", new[] { "uh" } }, + { "aw", new[] { "ae", "aa", "ah", "ay" } }, + { "ay", new[] { "ah", "ae", "aw", "aa" } }, + { "ey", new[] { "eh", "ae" } }, + { "oy", new[] { "ow", "ao" } }, + { "ow", new[] { "oy", "ao" } } + }; + + private readonly Dictionary vvVowel2Fallbacks = new Dictionary { + { "aa", new[] { "ah", "ay", "aw", "ae", "ao" } }, + { "ae", new[] { "eh", "aw", "ay", "ah", "aa" } }, + { "ah", new[] { "aa", "aw", "ay", "ae", "ao" } }, + { "ao", new[] { "aa", "ow", "ah", "ay", "ae" } }, + { "ax", new[] { "ah", "uh", "aa" } }, + { "eh", new[] { "ey", "ax" } }, + { "er", new[] { "r", "ah", "ax" } }, + { "ih", new[] { "iy", "eh" } }, + { "uh", new[] { "uw" } }, + }; + + // CV FALLBACKS + private readonly Dictionary cvConsonantFallbacks = new Dictionary { + { "b", new[] { "p", "d", "v" } }, + { "ch", new[] { "sh", "jh"} }, + { "d", new[] { "p", "d", "v" } }, + { "dh", new[] { "d", "v"} }, + { "dx", new[] { "d" } }, + { "f", new[] { "hh", "p", "th" } }, + { "g", new[] { "k" } }, + { "hh", new[] { "f" } }, + { "jh", new[] { "ch" } }, + { "k", new[] { "g" } }, + { "l", new[] { "r" } }, + { "m", new[] { "n" } }, + { "n", new[] { "m" } }, + { "ng", new[] { "n" } }, + { "p", new[] { "b", "d" } }, + { "q", new[] { "-" } }, + { "r", new[] { "er", "w", "l" } }, + { "s", new[] { "z", "f" } }, + { "sh", new[] { "s", "zh" } }, + { "t", new[] { "d", "k" } }, + { "th", new[] { "s", "th" } }, + { "v", new[] { "b", "f", "zh" } }, + { "w", new[] { "uw", "uh" } }, + { "y", new[] { "iy" } }, + { "z", new[] { "s" } }, + { "zh", new[] { "sh", "jh", "ch"} }, + }; + + private readonly Dictionary cvVowelFallbacks = new Dictionary { + { "aa", new[] { "ah", "ay", "aw", "ae", "ao" } }, + { "ae", new[] { "eh", "aw", "ay", "ah", "aa" } }, + { "ah", new[] { "aa", "aw", "ay", "ae", "ao" } }, + { "ao", new[] { "aa", "ow", "ah", "ay", "ae" } }, + { "ax", new[] { "ah", "uh", "aa" } }, + { "eh", new[] { "ey", "ax" } }, + { "ih", new[] { "iy", "eh" } }, + { "iy", new[] { "ih" } }, + { "uh", new[] { "uw" } }, + { "uw", new[] { "uh" } }, + { "aw", new[] { "ae", "aa", "ah", "ay" } }, + { "ay", new[] { "ah", "ae", "aw", "aa" } }, + { "ey", new[] { "eh", "ae" } }, + { "oy", new[] { "ow", "ao" } }, + { "ow", new[] { "oy", "ao" } } + }; + + // VC FALLBACKS + private readonly Dictionary vcVowelFallbacks = new Dictionary { + { "aa", new[] { "ah", "ae", "ao" } }, + { "ae", new[] { "eh", "ah", "aa" } }, + { "ah", new[] { "aa", "ae", "ao" } }, + { "ao", new[] { "aa", "ow", "ah", "ae" } }, + { "ax", new[] { "ah", "aa", "uh" } }, + { "eh", new[] { "ah", "ey" } }, + { "er", new[] { "r", "ah", "ax" } }, + { "ih", new[] { "iy", "eh" } }, + { "iy", new[] { "ih" } }, + { "uh", new[] { "uw" } }, + { "uw", new[] { "uh" } }, + { "aw", new[] { "uw", "uh" } }, + { "ay", new[] { "iy", "ih", "y" } }, + { "ey", new[] { "iy", "ih", "y" } }, + { "oy", new[] { "iy", "ih", "y" } }, + { "ow", new[] { "uw", "uh" } } + }; + + private readonly Dictionary vcConsonantFallbacks = new Dictionary { + { "b", new[] { "p", "d", "v" } }, + { "ch", new[] { "t", "k", "q", "p", "g" } }, + { "d", new[] { "p", "b", "g" } }, + { "dh", new[] { "d", "v"} }, + { "dx", new[] { "d", "t", "r" } }, + { "f", new[] { "s", "p", "th" } }, + { "g", new[] { "k", "p", "b" } }, + { "hh", new[] { "f", "th" } }, + { "jh", new[] { "d", "b", "g" } }, + { "k", new[] { "t", "d", "g" } }, + { "l", new[] { "r" } }, + { "m", new[] { "n" } }, + { "n", new[] { "m" } }, + { "ng", new[] { "n", "m" } }, + { "p", new[] { "b", "d", "g" } }, + { "q", new[] { "t", "-" } }, + { "r", new[] { "l", "w" } }, + { "s", new[] { "z", "f" } }, + { "sh", new[] { "s", "zh" } }, + { "t", new[] { "d", "k" } }, + { "th", new[] { "s", "th" } }, + { "v", new[] { "f", "b", "zh" } }, + { "w", new[] { "uw", "uh" } }, + { "y", new[] { "iy" } }, + { "z", new[] { "s" } }, + { "zh", new[] { "sh", "jh", "ch"} }, + }; + + // CC FALLBACKS + private readonly Dictionary ccConsonant1Fallbacks = new Dictionary { + { "b", new[] { "p", "d", "v" } }, + { "d", new[] { "p", "b", "g" } }, + { "dh", new[] { "d", "v"} }, + { "dx", new[] { "d", "t", "r" } }, + { "f", new[] { "s", "p", "th" } }, + { "g", new[] { "k", "p", "b" } }, + { "hh", new[] { "f", "th" } }, + { "k", new[] { "g", "d", "t" } }, + { "l", new[] { "r" } }, + { "m", new[] { "n" } }, + { "n", new[] { "m" } }, + { "ng", new[] { "n", "m" } }, + { "p", new[] { "b", "d", "g" } }, + { "q", new[] { "t", "-" } }, + { "r", new[] { "w", "l" } }, + { "s", new[] { "z", "f" } }, + { "sh", new[] { "s", "zh" } }, + { "t", new[] { "d", "k" } }, + { "th", new[] { "s", "th" } }, + { "v", new[] { "f", "b", "zh" } }, + { "w", new[] { "uw", "uh" } }, + { "y", new[] { "iy", "ih" } }, + { "z", new[] { "s" } }, + { "zh", new[] { "sh", "jh", "ch"} }, + }; + + private readonly Dictionary ccConsonant2Fallbacks = new Dictionary { + { "b", new[] { "p", "d", "v" } }, + { "ch", new[] { "sh", "jh"} }, + { "d", new[] { "p", "d", "v" } }, + { "dh", new[] { "d", "v"} }, + { "dx", new[] { "d" } }, + { "f", new[] { "hh", "p", "th" } }, + { "g", new[] { "k" } }, + { "hh", new[] { "f" } }, + { "jh", new[] { "ch" } }, + { "k", new[] { "g" } }, + { "l", new[] { "r" } }, + { "m", new[] { "n" } }, + { "n", new[] { "m" } }, + { "ng", new[] { "n" } }, + { "p", new[] { "b", "d" } }, + { "q", new[] { "-" } }, + { "r", new[] { "w", "l" } }, + { "s", new[] { "z", "f" } }, + { "sh", new[] { "s", "zh" } }, + { "t", new[] { "d", "k" } }, + { "th", new[] { "s", "th" } }, + { "v", new[] { "b", "f", "zh" } }, + { "w", new[] { "uw", "uh" } }, + { "y", new[] { "iy" } }, + { "z", new[] { "s" } }, + { "zh", new[] { "jh", "ch"} }, + }; + + private string ApplyContextualFallbacks(string alias, int tone) { + string p1 = null; + string p2 = null; + bool hasSpace = alias.Contains(' '); + + if (hasSpace) { + var parts = alias.Split(' '); + if (parts.Length == 2) { + p1 = parts[0]; + p2 = parts[1]; } - //CC (C y) - foreach (var c2 in consonants) { - if (!(alias.Contains($"ay {c2}") || alias.Contains($"ey {c2}") || alias.Contains($"iy {c2}") || alias.Contains($"oy {c2}"))) { - alias = alias.Replace($"{c2} y", $"{c2} y"); + } else { + var allPhonemes = vowels.Concat(consonants).Concat(new[] { "-", "R" }).OrderByDescending(p => p.Length); + foreach (var ph1 in allPhonemes) { + if (alias.StartsWith(ph1)) { + string remainder = alias.Substring(ph1.Length); + if (vowels.Contains(remainder) || consonants.Contains(remainder) || remainder == "-" || remainder == "R") { + p1 = ph1; + p2 = remainder; + break; + } } } - if (alias == "y -") { - return alias.Replace("y", "iy"); - } - } - //VC's - foreach (var v1 in vcFallBacks) { - foreach (var c1 in consonants) { - if (vc_FallBack && isMissingVPhonemes) { - alias = alias.Replace(v1.Key + " " + c1, v1.Value + " " + c1); - } - } - } + if (p1 == null || p2 == null) return alias; - // glottal - foreach (var v1 in vowels) { - if (!alias.Contains("cl " + v1) || !alias.Contains("q " + v1)) { - alias = alias.Replace("q " + v1, "- " + v1); - } - } - foreach (var c2 in consonants) { - if (!alias.Contains(c2 + " cl") || !alias.Contains(c2 + " q")) { - alias = alias.Replace(c2 + " q", $"{c2} -"); - } - } - foreach (var c2 in consonants) { - if (!alias.Contains("cl " + c2) || !alias.Contains("q " + c2)) { - alias = alias.Replace("q " + c2, "- " + c2); - } + int GetPhType(string ph) { + if (tails.Contains(ph)) return 0; // Rest + if (vowels.Contains(ph)) return 1; // Vowel + if (consonants.Contains(ph)) return 2; // Consonant + return -1; // Unknown } - // C -'s - foreach (var c1 in new[] { "d", "dh", "g", "p", "jh", "b", "s", "ch", "t", "r", "n", "l", "ng", "sh", "zh", "th", "z", "f", "k", "s", "hh" }) { - foreach (var s in new[] { "-" }) { - var str = c1 + " " + s; - if (alias.Contains(str) && !alias.Contains($"{c1} -")) { - switch (c1) { - case "b" when c1 == "b": - alias = alias.Replace(str, "d" + " " + s); - break; - case "d" when c1 == "d" || c1 == "dh" || c1 == "g" || c1 == "p": - alias = alias.Replace(str, "b" + " " + s); - break; - case "ch" when c1 == "ch": - alias = alias.Replace(str, "jh" + " " + s); - break; - case "jh" when c1 == "jh": - alias = alias.Replace(str, "ch" + " " + s); - break; - case "s" when c1 == "s": - alias = alias.Replace(str, "f" + " " + s); - break; - case "ch" when c1 == "ch": - alias = alias.Replace(str, "jh" + " " + s); - break; - case "t" when c1 == "t": - alias = alias.Replace(str, "k" + " " + s); - break; - case "r" when c1 == "r": - alias = alias.Replace(str, "er" + " " + s); - break; - case "n" when c1 == "n": - alias = alias.Replace(str, "m" + " " + s); - break; - case "ng" when c1 == "ng" || c1 == "m": - alias = alias.Replace(str, "n" + " " + s); - break; - case "sh" when c1 == "sh" || c1 == "zh" || c1 == "th" || c1 == "z" || c1 == "f": - alias = alias.Replace(str, "s" + " " + s); - break; - case "k" when c1 == "k": - alias = alias.Replace(str, "t" + " " + s); - break; - case "s" when c1 == "s": - alias = alias.Replace(str, "z" + " " + s); - break; - case "hh" when c1 == "hh": - alias = alias.Replace(str, str); - break; - } - } - } + int type1 = GetPhType(p1); + int type2 = GetPhType(p2); + var dict1 = new Dictionary(); + var dict2 = new Dictionary(); + + if (type1 == 2 && type2 == 1) { // CV + dict1 = cvConsonantFallbacks; dict2 = cvVowelFallbacks; + } + else if (type1 == 1 && type2 == 2) { // VC + dict1 = vcVowelFallbacks; dict2 = vcConsonantFallbacks; + } + else if (type1 == 2 && type2 == 2) { // CC + dict1 = ccConsonant1Fallbacks; dict2 = ccConsonant2Fallbacks; + } + else if (type1 == 1 && type2 == 1) { // VV + dict1 = vvVowel1Fallbacks; dict2 = vvVowel2Fallbacks; } - // CC's - foreach (var c1 in new[] { "f", "z", "k", "p", "d", "dh", "g", "b", "m", "r" }) { - foreach (var c2 in consonants) { - var str = c1 + " " + c2; - if (alias.Contains(str)) { - if (ccSpecific) { - switch (c1) { - case "z" when c1 == "z": - alias = alias.Replace(str, "s" + " " + c2); - break; - case "dh" when c1 == "dh" || c1 == "g" || c1 == "b": - alias = alias.Replace(str, "d" + " " + c2); - break; - case "m" when c1 == "m": - alias = alias.Replace(str, "n" + " " + c2); - break; - case "r" when c1 == "r": - alias = alias.Replace(str, "er" + " " + c2); - break; - } - } - } - } + else if (type1 == 0 && type2 == 1) { // Starting Vowel (- V) + dict2 = cvVowelFallbacks; // Fallback the vowel normally } - return base.ValidateAlias(alias); - } - - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; - - // Exact token match - if (alias == phoneme) - return true; - - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; + else if (type1 == 1 && type2 == 0) { // Ending Vowel (V -) + dict1 = vcVowelFallbacks; // Fallback the vowel normally } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; + else if (type1 == 0 && type2 == 2) { // Starting Consonant (- C) + dict2 = cvConsonantFallbacks; } - return false; + else if (type1 == 2 && type2 == 0) { // Ending Consonant (C -) + dict1 = vcConsonantFallbacks; + } + return FindValidCombination(p1, p2, dict1, dict2, tone, hasSpace) ?? alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } + private string FindValidCombination(string part1, string part2, Dictionary dict1, Dictionary dict2, int tone, bool hasSpace) { + var p1Options = new List { part1 }; + if (dict1.TryGetValue(part1, out var fallbacks1)) { + p1Options.AddRange(fallbacks1); } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } + var p2Options = new List { part2 }; + if (dict2.TryGetValue(part2, out var fallbacks2)) { + p2Options.AddRange(fallbacks2); } - // consonant timings - - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); - foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } + foreach (var opt1 in p1Options.Skip(1)) { + string tryAlias = hasSpace ? $"{opt1} {part2}" : $"{opt1}{part2}"; + if (HasOto(tryAlias, tone)) return tryAlias; } - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } + foreach (var opt2 in p2Options.Skip(1)) { + string tryAlias = hasSpace ? $"{part1} {opt2}" : $"{part1}{opt2}"; + if (HasOto(tryAlias, tone)) return tryAlias; } - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; + foreach (var opt1 in p1Options.Skip(1)) { + foreach (var opt2 in p2Options.Skip(1)) { + string tryAlias = hasSpace ? $"{opt1} {opt2}" : $"{opt1}{opt2}"; + if (HasOto(tryAlias, tone)) return tryAlias; } } - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } + return null; + } - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } + // Endings has 50 ticks gap + protected override bool NoGap => true; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } + var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); + foreach (var kvp in sortedOverrides) { + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (Regex.IsMatch(alias, $@"(? ProcessEnding(Ending ending) { // change rh V -> r V // since rh is a VC only alias, r is used as their natural approximant to make CV connections, if it happens - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } foreach (var vowel in vowels) { alias = alias.Replace("rh" + " " + vowel, "r" + " " + vowel); } diff --git a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs index 911aac120..bc5ed0b2c 100644 --- a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs @@ -460,5 +460,39 @@ private string ToHiragana(string romaji) { hiragana = hiragana.Replace("ゔ", "ヴ"); return hiragana; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var parts = alias.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + bool isVcv = false; + + if (parts.Length == 2) { + var startingVowels = new[] { "a", "i", "u", "e", "o", "n", "N", "-" }; + var endingVowels = vowels; + + // First part must be a vowel (or a rest) + if (startingVowels.Contains(parts[0])) { + string cv = parts[1]; + + // Second part must end in a vowel (Romaji CV) OR be Japanese (Hiragana/Katakana) + bool isRomajiVcv = endingVowels.Contains(cv.Last().ToString()); + bool isJapaneseVcv = cv.Any(c => c > 0xFF); + + if (isRomajiVcv || isJapaneseVcv) { + isVcv = true; + } + } + } + + if (isVcv) { + return GetTransitionBasicLengthMsByConstant() * 1.0; + } + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs index 8fe5cca1d..2e837ed4f 100644 --- a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs @@ -669,5 +669,39 @@ private string ToHiragana(string romaji) { hiragana = hiragana.Replace("ゔ", "ヴ"); return hiragana; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var parts = alias.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + bool isVcv = false; + + if (parts.Length == 2) { + var startingVowels = new[] { "a", "i", "u", "e", "o", "n", "N", "-" }; + var endingVowels = vowels; + + // First part must be a vowel (or a rest) + if (startingVowels.Contains(parts[0])) { + string cv = parts[1]; + + // Second part must end in a vowel (Romaji CV) OR be Japanese (Hiragana/Katakana) + bool isRomajiVcv = endingVowels.Contains(cv.Last().ToString()); + bool isJapaneseVcv = cv.Any(c => c > 0xFF); + + if (isRomajiVcv || isJapaneseVcv) { + isVcv = true; + } + } + } + + if (isVcv) { + return GetTransitionBasicLengthMsByConstant() * 1.0; + } + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 3c9f81aaa..3b912bca8 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -41,8 +41,6 @@ public EnXSampaPhonemizer() { .Where(parts => parts[0] != parts[1]) .ToDictionary(parts => parts[0], parts => parts[1]); } - - private bool isYamlFallbacks = false; protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => ""; @@ -173,38 +171,21 @@ public EnXSampaPhonemizer() { {"@u","u"}, {"3", "r"} }; - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Load dictionary from plugin folder. - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - - // Load dictionary from singer folder. - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(new ArpabetG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new ArpabetG2p() }; } - protected override string[] GetSymbols(Note note) { string[] original = base.GetSymbols(note); if (original == null) { return null; } List finalProcessedPhonemes = new List(); + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } // Splits diphthongs and affricates if not present in the bank string[] diphthongs = new[] { "aI", "eI", "OI", "aU", "oU", "VI", "VU", "@U", "ai", "ei", "Oi", "au", "ou", "Ou", "@u", }; @@ -221,19 +202,6 @@ protected override string[] GetSymbols(Note note) { } return finalProcessedPhonemes.ToArray(); } - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } - protected override List ProcessSyllable(Syllable syllable) { syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); @@ -253,12 +221,6 @@ protected override List ProcessSyllable(Syllable syllable) { var rv = $"- {v}"; // Switch between phonetic systems, depending on certain aliases in the bank - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } if (HasOto($"- i:", syllable.tone) || HasOto($"i:", syllable.tone) || (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone))) { isVocaSampa = true; } @@ -307,38 +269,49 @@ protected override List ProcessSyllable(Syllable syllable) { } } else if (syllable.IsVV) { if (!CanMakeAliasExtension(syllable)) { - var vv = $"{prevV} {v}"; - basePhoneme = vv; - if (!HasOto(vv, syllable.vowelTone) && !HasOto(ValidateAlias(vv), syllable.vowelTone) && (vvExceptions.ContainsKey(prevV) && prevV != v || Delta5vvExceptions.ContainsKey(prevV) && prevV != v)) { - // VV splits to [V C][CV] or [V][V] - var delta5vc = $"{Delta5vvExceptions[prevV]}"; - bool CV = false; - if ((!HasOto(delta5vc, syllable.vowelTone) && !HasOto(ValidateAlias(delta5vc), syllable.vowelTone))) { - delta5vc = $"{prevV} {vvExceptions[prevV]}"; - CV = true; + if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV} {v}"; + } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + } + + // Diphthong Fallbacks + else if (diphthongSplits.ContainsKey(prevV) || diphthongTails.ContainsKey(prevV)) { + string cv = ""; + if (diphthongSplits.ContainsKey(prevV)) { + var splitOverride = diphthongSplits[prevV]; + var vc = splitOverride[0].Replace("{v}", v); + cv = splitOverride[1].Replace("{v}", v); + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + } + else { // Default YAML diphthong logic + var tail = diphthongTails[prevV]; + var vcSpace = $"{prevV} {tail}"; + var vcNoSpace = $"{prevV}{tail}"; + cv = $"{tail}{v}"; + TryAddPhoneme(phonemes, syllable.tone, vcSpace, ValidateAlias(vcSpace), vcNoSpace, ValidateAlias(vcNoSpace)); } - phonemes.Add(delta5vc); - // if delta5 vc is not available, turn v to cv - var cv = $"{vvExceptions[prevV]}{v}"; - basePhoneme = v; - if (CV && (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone))) { + + if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { basePhoneme = cv; + } else if (!HasOto(cv, syllable.vowelTone) || !HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = $"{diphthongTails[prevV]} {v}"; + } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = ValidateAlias($"- {v}"); + phonemes.Add($"{prevV} -"); } } else { - // VV to V - if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV} {v}"; - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { basePhoneme = v; + } else { + basePhoneme = ValidateAlias($"- {v}"); + phonemes.Add($"{prevV} -"); } } - // EXTEND AS [V] - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone)) { - basePhoneme = v; - } else { - // PREVIOUS ALIAS WILL EXTEND as [V V] + } + else { basePhoneme = null; } } else if (syllable.IsStartingCVWithOneConsonant) { @@ -447,7 +420,6 @@ protected override List ProcessSyllable(Syllable syllable) { } } } - FoundMatch:; // try vcc for (var i = lastC + 1; i >= 0; i--) { var vr = $"{prevV} -"; @@ -492,6 +464,12 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { cc1 = $"{string.Join("", cc.Skip(i))}"; } + if (CurrentWordCc.Length >= 2) { + if (liquid.Contains(cc.Last()) || semivowel.Contains(cc.Last()) + || liquid.Contains(ValidateAlias(cc.Last())) || semivowel.Contains(ValidateAlias(cc.Last()))) { + glides(cc1); + } + } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } @@ -525,6 +503,12 @@ protected override List ProcessSyllable(Syllable syllable) { if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } + if (CurrentWordCc.Length >= 2) { + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(cc1); + } + } // Use [C2C3] when current word has 2 consonants or more and [C2C3C4...] does not exist if (!HasOto(cc2, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc2)) { cc2 = $"{cc[i + 1]}{cc[i + 2]}"; @@ -739,8 +723,16 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { // Validate alias depending on method + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } + if (isVocaSampa) { foreach (var syllable in vocaSampa) { alias = alias.Replace(syllable.Key, syllable.Value); @@ -801,12 +793,6 @@ protected override string ValidateAlias(string alias) { } } - if (isYamlFallbacks) { - foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - // Split diphthongs adjuster if (alias.Contains("U^")) { alias = alias.Replace("U^", "U"); @@ -833,123 +819,23 @@ protected override string ValidateAlias(string alias) { return alias; } - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; - - // Exact token match - if (alias == phoneme) - return true; + // Endings has 50 ticks gap + protected override bool NoGap => true; - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - // consonant timings + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } - } - - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } - } - - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } - - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } - - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (Regex.IsMatch(alias, $@"(?(); - - } - private string[] diphthongs = Array.Empty(); - private static string[] c_cR = { "n" }; - - protected override bool IsGroupKeyword(string rulePhoneme) { - string baseGroup = rulePhoneme.Split(new[] { '!', '+' })[0]; - return base.IsGroupKeyword(rulePhoneme) || new[] { "affricate", "fricative", "aspirate", "semivowel", "liquid", "nasal", "stop", "tap", "diphthong" }.Contains(baseGroup); - } - protected override bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { - if (base.IsGroupMatch(rulePhoneme, actualPhoneme)) return true; - string baseGroup = rulePhoneme.Split(new[] { '!', '+' })[0]; + this.diphthongTails = new Dictionary() { + { "ay", "ay-" }, + { "ey", "ey-" }, + { "oy", "oy-" }, + { "aw", "aw-" }, + { "ow", "ow-" } + }; - if (rulePhoneme.Contains("!")) { - string[] exceptions = rulePhoneme.Split('!')[1].Split(','); - if (exceptions.Contains(actualPhoneme)) return false; - } - if (rulePhoneme.Contains("+")) { - string[] inclusions = rulePhoneme.Split('+')[1].Split(','); - if (!inclusions.Contains(actualPhoneme)) return false; - } - switch (baseGroup) { - case "affricate": return affricate.Contains(actualPhoneme); - case "fricative": return fricative.Contains(actualPhoneme); - case "aspirate": return aspirate.Contains(actualPhoneme); - case "semivowel": return semivowel.Contains(actualPhoneme); - case "liquid": return liquid.Contains(actualPhoneme); - case "nasal": return nasal.Contains(actualPhoneme); - case "stop": return stop.Contains(actualPhoneme); - case "tap": return tap.Contains(actualPhoneme); - case "diphthong": return diphthongs.Contains(actualPhoneme); - default: return false; - } } + private static string[] c_cR = Array.Empty(); protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; @@ -94,12 +69,10 @@ protected override bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { .ToDictionary(parts => parts[0], parts => parts[1]); private bool isTimitPhonemes = false; - private Dictionary DiphthongExceptions = new Dictionary() { + private Dictionary diphthongTails = new Dictionary() { { "ay", "ay-" }, { "ey", "ey-" }, { "oy", "oy-" }, { "aw", "aw-" }, { "ow", "ow-" } }; - private bool isYamlFallbacks = false; - private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; protected override string[] GetSymbols(Note note) { @@ -107,8 +80,6 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } - List modified = new List(original); - List finalPhonemes = ApplyReplacements(modified, false); List finalProcessedPhonemes = new List(); // SPLITS UP DR AND TR @@ -139,10 +110,8 @@ protected override string[] GetSymbols(Note note) { vowel3S.Add($"{V1}{C1}"); } } - IEnumerable phonemes; - phonemes = finalPhonemes; - foreach (string s in phonemes) { + foreach (string s in original) { switch (s) { case var str when dr.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): finalProcessedPhonemes.AddRange(new string[] { "jh", s[1].ToString() }); @@ -185,28 +154,8 @@ protected override string[] GetSymbols(Note note) { return finalProcessedPhonemes.ToArray(); } - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - // LOAD DICTIONARY FROM FOLDER - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - // LOAD DICTIONARY FROM SINGER FOLDER - if (singer != null || singer.Found || singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - g2ps.Add(new ArpabetPlusG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new ArpabetPlusG2p() }; } public override void SetSinger(USinger singer) { @@ -214,60 +163,49 @@ public override void SetSinger(USinger singer) { if (this.singer != null && this.singer.Loaded) { - string file = Path.Combine(this.singer.Location, YamlFileName); - if (!File.Exists(file)) { - file = Path.Combine(PluginDir, YamlFileName); - } + string globalFile = Path.Combine(PluginDir, YamlFileName); + string singerFile = Path.Combine(this.singer.Location, YamlFileName); + + var filesToParse = new List(); + if (File.Exists(globalFile)) filesToParse.Add(globalFile); + if (File.Exists(singerFile) && globalFile != singerFile) filesToParse.Add(singerFile); - if (File.Exists(file)) { + c_cR = Array.Empty(); + + foreach (var file in filesToParse) { try { - var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); - - if (data?.diphthongs != null && data.diphthongs.Any()) { - DiphthongExceptions.Clear(); - foreach (var df in data.diphthongs) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - DiphthongExceptions[df.from] = df.to; - } - } - } - - this.diphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - DiphthongExceptions.Clear(); - foreach (var d in this.diphthongs) { - DiphthongExceptions[d] = d + "-"; - } - - if (data?.diphthongs != null && data.diphthongs.Any()) { - foreach (var df in data.diphthongs) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - DiphthongExceptions[df.from] = df.to; + var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); + + if (data?.symbols != null) { + + string[] targetTypes = { "nasal", "liquid", "semivowel", "fricative", "aspirate" }; + var newCcR = data.symbols + .Where(s => targetTypes.Contains(s.type?.ToLower())) + .Select(s => s.symbol) + .ToArray(); + + c_cR = c_cR.Concat(newCcR).Distinct().ToArray(); + + var yamlDiphthongs = data.symbols + .Where(s => s.type?.ToLower() == "diphthong") + .Select(s => s.symbol) + .Distinct() + .ToArray(); + + foreach (var d in yamlDiphthongs) { + if (!diphthongSplits.ContainsKey(d)) { + diphthongTails[d] = d + "-"; } } } } catch (Exception ex) { - Log.Error($"Failed to parse custom diphthongs from {YamlFileName}: {ex.Message}"); + Log.Error($"Failed to parse symbols from {file}: {ex.Message}"); } } } } - public class ArpabetYAMLData: YAMLData { - public Fallbacks[] diphthongs { get; set; } = Array.Empty(); - } - - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } protected override List ProcessSyllable(Syllable syllable) { syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); @@ -307,14 +245,6 @@ protected override List ProcessSyllable(Syllable syllable) { } } - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - - // STARTING V if (syllable.IsStartingV) { // TRIES - V THEN -V AND SO ON @@ -323,28 +253,53 @@ protected override List ProcessSyllable(Syllable syllable) { // [V V] or [V C][- C/C][V]/[V] else if (syllable.IsVV) { if (!CanMakeAliasExtension(syllable)) { - basePhoneme = $"{prevV} {v}"; - if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && DiphthongExceptions.ContainsKey(prevV)) { - // VV IS NOT PRESENT, CHECKS DiphthongExceptions LOGIC - var vc = $"{prevV} {DiphthongExceptions[prevV]}"; - if (!HasOto(vc, syllable.vowelTone) && !HasOto(ValidateAlias(vc), syllable.vowelTone)) { - vc = AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", syllable.vowelTone, ""); - } - TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); - basePhoneme = AliasFormat(v, "vv", syllable.vowelTone, ""); - } else { - { - if (!HasOto($"{prevV} {v}", syllable.vowelTone) || !HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = AliasFormat(v, "vv", syllable.vowelTone, ""); + if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV} {v}"; + } + else if (diphthongSplits.ContainsKey(prevV) || diphthongTails.ContainsKey(prevV)) { + string cv = ""; + + if (diphthongSplits.ContainsKey(prevV)) { + var splitOverride = diphthongSplits[prevV]; + var vc = AliasFormat(splitOverride[0].Replace("{v}", v), "vcEx", syllable.tone, prevV); + cv = AliasFormat(splitOverride[1].Replace("{v}", v), "vv", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + } + else { + var tail = diphthongTails[prevV]; // gets e.g., "ay-" + var vcSpace = $"{prevV} {tail}"; + var vcNoSpace = $"{prevV}{tail}"; + var vcMix = AliasFormat(tail, "diph_mix", syllable.vowelTone, ""); + + if (HasOto(vcSpace, syllable.vowelTone) || HasOto(ValidateAlias(vcSpace), syllable.vowelTone)) { + TryAddPhoneme(phonemes, syllable.tone, vcSpace, ValidateAlias(vcSpace)); + } else if (HasOto(vcNoSpace, syllable.vowelTone) || HasOto(ValidateAlias(vcNoSpace), syllable.vowelTone)) { + TryAddPhoneme(phonemes, syllable.tone, vcNoSpace, ValidateAlias(vcNoSpace)); } else { - basePhoneme = $"{prevV} {v}"; + TryAddPhoneme(phonemes, syllable.tone, vcMix, ValidateAlias(vcMix)); } + cv = AliasFormat(v, "vv", syllable.vowelTone, ""); + } + + if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = cv; + } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = AliasFormat(v, "vv", syllable.vowelTone, ""); + } + } + else { + if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = AliasFormat(v, "vv", syllable.vowelTone, ""); } } - } else { + } + else { basePhoneme = null; } - } else if (syllable.IsStartingCVWithOneConsonant) { /// [- C/-C/C] basePhoneme = AliasFormat(v, "cv", syllable.vowelTone, ""); @@ -383,13 +338,13 @@ protected override List ProcessSyllable(Syllable syllable) { TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc", syllable.tone, "")); break; /// use vowel ending - } else if (DiphthongExceptions.ContainsKey(prevV) && ((HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone) || (HasOto(vr1, syllable.tone) || HasOto(ValidateAlias(vr1), syllable.tone)) && !HasOto(vc, syllable.tone)))) { - TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", syllable.vowelTone, "")); + } else if (diphthongTails.ContainsKey(prevV) && ((HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone) || (HasOto(vr1, syllable.tone) || HasOto(ValidateAlias(vr1), syllable.tone)) && !HasOto(vc, syllable.tone)))) { + TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", syllable.vowelTone, "")); TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc", syllable.tone, "")); break; /// use consonants for diphthongs if the vb doesn't have vowel endings - } else if (DiphthongExceptions.ContainsKey(prevV) && (!(HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone) || (HasOto(vr1, syllable.tone) || HasOto(ValidateAlias(vr1), syllable.tone)) && !HasOto(vc, syllable.tone)))) { - TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", syllable.vowelTone, "")); + } else if (diphthongTails.ContainsKey(prevV) && (!(HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone) || (HasOto(vr1, syllable.tone) || HasOto(ValidateAlias(vr1), syllable.tone)) && !HasOto(vc, syllable.tone)))) { + TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", syllable.vowelTone, "")); TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc", syllable.tone, "")); break; @@ -433,6 +388,10 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""), syllable.vowelTone)) { cc1 = AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""); } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(cc1); + } // CV } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = AliasFormat(v, "cv", syllable.vowelTone, ""); @@ -465,6 +424,10 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""), syllable.vowelTone)) { cc1 = AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""); } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(cc1); + } // CV } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = AliasFormat(v, "cv", syllable.vowelTone, ""); @@ -508,8 +471,8 @@ protected override List ProcessEnding(Ending ending) { if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || (HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone))) { TryAddPhoneme(phonemes, ending.tone, AliasFormat(v, "ending", ending.tone, "", t)); /// split diphthong vowels - } else if (DiphthongExceptions.ContainsKey(prevV) && !(HasOto(vR, ending.tone) && HasOto(ValidateAlias(vR), ending.tone) && (HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)))) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{DiphthongExceptions[prevV]}", "cv", ending.tone, "", t)); + } else if (diphthongTails.ContainsKey(prevV) && !(HasOto(vR, ending.tone) && HasOto(ValidateAlias(vR), ending.tone) && (HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)))) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{diphthongTails[prevV]}", "cv", ending.tone, "", t)); } } else if (ending.IsEndingVCWithOneConsonant) { var vc = $"{v} {cc[0]}"; @@ -523,17 +486,17 @@ protected override List ProcessEnding(Ending ending) { } else if (!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone) && (HasOto(vcr2, ending.tone) || HasOto(ValidateAlias(vcr2), ending.tone))) { TryAddPhoneme(phonemes, ending.tone, vcr2); // double the consonants if has [C -]/[C-] - } else if (DiphthongExceptions.ContainsKey(prevV) && (c_cR.Contains(cc.Last())) && ((HasOto(AliasFormat(v, "ending_mix", ending.tone, ""), ending.tone) && (HasOto($"{c_cR[0]} {t}", ending.tone) || (HasOto($"{c_cR[0]}{t}", ending.tone)))))) { + } else if (diphthongTails.ContainsKey(prevV) && (c_cR.Contains(cc.Last())) && ((HasOto(AliasFormat(v, "ending_mix", ending.tone, ""), ending.tone) && (HasOto($"{c_cR[0]} {t}", ending.tone) || (HasOto($"{c_cR[0]}{t}", ending.tone)))))) { // ex: [ow][ow-][z][z -] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", ending.tone, "", t)); TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "cc1_mix", ending.tone, "", t)); TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "cc_mix", ending.tone, "", t)); - } else if (DiphthongExceptions.ContainsKey(prevV) && ((HasOto(AliasFormat(v, "ending_mix", ending.tone, ""), ending.tone)) && !HasOto(vc, ending.tone))) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", ending.tone, "", t)); + } else if (diphthongTails.ContainsKey(prevV) && ((HasOto(AliasFormat(v, "ending_mix", ending.tone, ""), ending.tone)) && !HasOto(vc, ending.tone))) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", ending.tone, "", t)); TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "cc_mix", ending.tone, "", t)); /// use consonants for diphthongs if the vb doesn't have vowel endings - } else if (DiphthongExceptions.ContainsKey(prevV) && (!(HasOto(AliasFormat(v, "ending_mix", ending.tone, "", t), ending.tone) && !HasOto(vc, ending.tone)))) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", ending.tone, "", t)); + } else if (diphthongTails.ContainsKey(prevV) && (!(HasOto(AliasFormat(v, "ending_mix", ending.tone, "", t), ending.tone) && !HasOto(vc, ending.tone)))) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", ending.tone, "", t)); if (c_cR.Contains(cc.Last())) { if (HasOto(AliasFormat($"{c_cR[0]}", "cc_mix", ending.tone, ""), ending.tone)) { TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "cc1_mix", ending.tone, "", t)); @@ -616,12 +579,12 @@ protected override List ProcessEnding(Ending ending) { } firstC = 1; break; - } else if (DiphthongExceptions.ContainsKey(prevV) && (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone)) || (HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone)) && !HasOto(vc, ending.tone)) { + } else if (diphthongTails.ContainsKey(prevV) && (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone)) || (HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone)) && !HasOto(vc, ending.tone)) { TryAddPhoneme(phonemes, ending.tone, vr1, vr); break; /// use consonants for diphthongs if the vb doesn't have vowel endings - } else if (DiphthongExceptions.ContainsKey(prevV) && (!(HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || (HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone)) && !HasOto(vc, ending.tone)))) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{DiphthongExceptions[prevV]}", "diph_mix", ending.tone, "", t)); + } else if (diphthongTails.ContainsKey(prevV) && (!(HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || (HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone)) && !HasOto(vc, ending.tone)))) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{diphthongTails[prevV]}", "diph_mix", ending.tone, "", t)); break; } else { TryAddPhoneme(phonemes, ending.tone, vc); @@ -733,36 +696,127 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st }; - // Check if the given type exists in the aliasFormats dictionary - if (!aliasFormats.ContainsKey(type)) { + if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { return alias; } - // Get the array of possible alias formats for the specified type + + if (type.Contains("dynStart")) { + string consonant = ""; + string vowel = ""; + // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[0]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations = new List { + $"- {consonant}{vowel}", // "- CV" + $"- {consonant} {vowel}", // "- C V" + $"-{consonant} {vowel}", // "-C V" + $"-{consonant}{vowel}", // "-CV" + $"-{consonant}_{vowel}", // "-C_V" + $"- {consonant}_{vowel}", // "- C_V" + }; + + foreach (var variation in dynamicVariations) { + if (HasOto(variation, tone)) { + return variation; + } else if (HasOto(ValidateAlias(variation), tone)) { + return ValidateAlias(variation); + } + } + } + + if (type.Contains("dynMid")) { + string consonant = ""; + string vowel = ""; + + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[0]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{consonant}{vowel}", // "CV" + $"{consonant} {vowel}", // "C V" + $"{consonant}_{vowel}", // "C_V" + }; + + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone)) { + return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); + } + } + } + + if (type.Contains("dynEnd")) { + string consonant = ""; + string vowel = ""; + + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[1]; + vowel = parts[0]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{vowel}{consonant} -", // "VC -" + $"{vowel} {consonant}-", // "V C-" + $"{vowel}{consonant}-", // "VC-" + $"{vowel} {consonant} -", // "V C -" + }; + + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone)) { + return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); + } + } + } + + // Get the array of possible alias formats for the specified type if not dynamic var formatsToTry = aliasFormats[type]; int counter = 0; foreach (var format in formatsToTry) { string aliasFormat; if (type.Contains("mix") && counter < 4) { - // Alternate between alias + format and format + alias for the first 4 iterations - aliasFormat = (counter % 2 == 0) ? alias + format : format + alias; + aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; counter++; - } else if (type.Contains("end")) { - aliasFormat = alias + format; + } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { + aliasFormat = $"{alias}{format}"; } else { - aliasFormat = format + alias; + aliasFormat = $"{format}{alias}"; } - // Check if the formatted alias exists using HasOto and ValidateAlias - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { - alias = aliasFormat; - return alias; + + if (HasOto(aliasFormat, tone)) { + return aliasFormat; + } else if (HasOto(ValidateAlias(aliasFormat), tone)) { + return ValidateAlias(aliasFormat); } } return alias; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { // VALIDATE ALIAS DEPENDING ON METHOD + if (HasOto(alias, tone)) return alias; + + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } if (isTimitPhonemes) { foreach (var fb in timitphonemes.OrderByDescending(f => f.Key.Length)) { alias = alias.Replace(fb.Key, fb.Value); @@ -778,12 +832,6 @@ protected override string ValidateAlias(string alias) { alias = alias.Replace(fb.Key, fb.Value); } } - if (isYamlFallbacks) { - foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - return alias; } @@ -799,114 +847,50 @@ bool PhonemeIsPresent(string alias, string phoneme) { return alias.EndsWith(phoneme); } - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier + protected override bool NoGap => true; + + protected override double GetTransitionMultiplier(string alias) { + double baseMultiplier = base.GetTransitionMultiplier(alias); + if (baseMultiplier != 1.0) { + return baseMultiplier; + } var fricative_def = 2.3; var aspirate_def = 1.3; var semivowel_def = 1.2; var liquid_def = 1.5; var nasal_def = 1.5; - var stop_def = 1.8; + var stop_def = 1.4; var tap_def = 0.5; var affricate_def = 1.5; - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - - - // consonant timings - - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); - foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } + if (PhonemeIsPresent(alias, c)) return fricative_def; } - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } + if (PhonemeIsPresent(alias, c)) return aspirate_def; } - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } + if (PhonemeIsPresent(alias, c)) return semivowel_def; } - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } + if (PhonemeIsPresent(alias, c)) return liquid_def; } - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } + if (PhonemeIsPresent(alias, c)) return nasal_def; } - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } + if (PhonemeIsPresent(alias, c)) return stop_def; } - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } + if (PhonemeIsPresent(alias, c)) return tap_def; } - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; - } + if (PhonemeIsPresent(alias, c)) return affricate_def; } - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return 1.0; } } } diff --git a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs index 4356eaae5..db160ddd9 100644 --- a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs @@ -38,8 +38,6 @@ public EnglishVCCVPhonemizer() { .Where(parts => parts[0] != parts[1]) .ToDictionary(parts => parts[0], parts => parts[1]); } - - private bool isYamlFallbacks = false; private bool useConvel = true; private readonly Dictionary vcExceptions = @@ -120,35 +118,13 @@ public EnglishVCCVPhonemizer() { private readonly string[] ccNoParsing = { "sk", "sm", "sn", "sp", "st", "hy" }; private readonly string[] stopCs = { "b", "d", "g", "k", "p", "t" }; private readonly string[] ucvCs = { "r", "l", "w", "y", "f"}; - private readonly string[] starlightccs = { "rl", "ll", "nn", "mm", "rf", "mf", "lf" }; + private readonly string[] starlightccs = { "rl", "ll", "nn", "mm" }; protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => ""; - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Load dictionary from plugin folder. - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - - // Load dictionary from singer folder. - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(new ArpabetG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new ArpabetG2p() }; } protected override string[] GetSymbols(Note note) { @@ -159,6 +135,11 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } List finalProcessedPhonemes = new List(); string[] tr_dr = new[] { "tr", "dr"}; foreach (string s in original) { @@ -178,7 +159,7 @@ protected override string[] GetSymbols(Note note) { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer == null) return; + if (this.singer == null || !this.singer.Loaded) return; string file = null; if (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) { @@ -213,19 +194,6 @@ private class VCCVYAMLData { public bool? useconvel { get; set; } } - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } - // this lets us get the unotes and utrack for convel private List unotes = new(); private UTrack utrack; @@ -238,30 +206,11 @@ public override void SetUp(Note[][] notes, UProject project, UTrack track) { .FirstOrDefault(p => p.trackNo == trackNo); unotes = part?.notes.OrderBy(n => n.position).ToList() ?? new List(); } - - // Automatic convel - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevs) { - var result = base.Process(notes, prev, next, prevNeighbour, nextNeighbour, prevs); - // Added so tests work or if the user disabled convel in the YAML - if (unotes.Count == 0 || !useConvel) return result; - - float CalcConvel(UNote note) { - float baseConvel = 100 * ((float)timeAxis.GetBpmAtTick(note.position) / 120); - float finalConvel; - var trackVel = utrack?.TrackExpressions?.FirstOrDefault(e => e.abbr == "vel"); - float velMin = trackVel?.min ?? 0f; - float velMax = trackVel?.max ?? 200f; - - if (note.duration >= 480) - finalConvel = baseConvel + (50 - 100 * ((float)note.duration / 960)); - else - finalConvel = baseConvel + (100 - (100 * ((float)note.duration / 480))); - - return Math.Clamp(finalConvel, velMin, velMax); - } - - #region regex + private (Regex pattern, string type)[] patterns; + + private void InitPatterns() { + if (patterns != null) return; string Alt(IEnumerable symbols) => $"({string.Join("|", symbols.Select(Regex.Escape).OrderByDescending(s => s.Length))})"; @@ -269,7 +218,7 @@ string Alt(IEnumerable symbols) => string C = Alt(consonants); string C2 = Alt(ucvCs); - var patterns = new (Regex pattern, string type)[] { + patterns = new (Regex pattern, string type)[] { (new Regex($@"^-{V}$"), "-V"), (new Regex($@"^_{V}$"), "_V"), (new Regex($@"^{V}-$"), "V-"), @@ -289,15 +238,42 @@ string Alt(IEnumerable symbols) => (new Regex($@"^{C}{V}$"), "CV"), (new Regex($@"^{V}$"), "V"), }; + } - string Classify(string alias) { - if (starlightccs.Contains(alias)) return "codaCC"; - foreach (var (pattern, type) in patterns) - if (pattern.IsMatch(alias)) return type; - return "Unknown"; - } + private string Classify(string alias) { + if (starlightccs.Contains(alias)) return "codaCC"; + InitPatterns(); + foreach (var (pattern, type) in patterns) + if (pattern.IsMatch(alias)) return type; + return "Unknown"; + } - #endregion + float CalcConvel(UNote note) { + float baseConvel = 100 * ((float)timeAxis.GetBpmAtTick(note.position) / 120); + float finalConvel; + var trackVel = utrack?.TrackExpressions?.FirstOrDefault(e => e.abbr == "vel"); + float velMin = trackVel?.min ?? 0f; + float velMax = trackVel?.max ?? 200f; + + if (note.duration >= 480) + finalConvel = baseConvel + (50 - 100 * ((float)note.duration / 960)); + else + finalConvel = baseConvel + (100 - (100 * ((float)note.duration / 480))); + + return Math.Clamp(finalConvel, velMin, velMax); + } + + private (UNote un, UNote unNext) UNoteAt(int absPos) { + if (unotes.Count == 0) return (null, null); + var un = unotes.LastOrDefault(n => n.position <= absPos) ?? unotes[0]; + int idx = unotes.IndexOf(un); + return (un, idx + 1 < unotes.Count ? unotes[idx + 1] : null); + } + + // Automatic convel + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevs) { + var result = base.Process(notes, prev, next, prevNeighbour, nextNeighbour, prevs); + if (unotes.Count == 0 || !useConvel || result.phonemes == null) return result; Note GetNoteForPhoneme(Phoneme phoneme, Note[] currentNotes) { int absPos = currentNotes[0].position + phoneme.position; @@ -305,12 +281,6 @@ Note GetNoteForPhoneme(Phoneme phoneme, Note[] currentNotes) { n => n.position <= absPos && absPos < n.position + n.duration, currentNotes[0]); } - - (UNote un, UNote unNext) UNoteAt(int absPos) { - var un = unotes.LastOrDefault(n => n.position <= absPos) ?? unotes[0]; - int idx = unotes.IndexOf(un); - return (un, idx + 1 < unotes.Count ? unotes[idx + 1] : null); - } var (curUN, nextUN) = UNoteAt(notes[0].position); int curIdx = unotes.IndexOf(curUN); @@ -323,52 +293,64 @@ Note GetNoteForPhoneme(Phoneme phoneme, Note[] currentNotes) { var phoneme = result.phonemes[i]; if (phoneme.phoneme == null) continue; - int absPos = notes[0].position + phoneme.position; - var (phonemeUN, phonemeUNNext) = UNoteAt(absPos); + var (phonemeUN, _) = UNoteAt(absPos); float noteVel = CalcConvel(phonemeUN); - - if (i > 0 && result.phonemes[i - 1].phoneme != null) { - var exprs = result.phonemes[i - 1].expressions; - if (exprs != null) { - var velExpr = exprs.FirstOrDefault(e => e.abbr == "vel"); - if (velExpr.abbr == "vel") - prevVel = velExpr.value; - } - } if (i < result.phonemes.Length - 1 && result.phonemes[i + 1].phoneme != null) { var nextPhoneme = result.phonemes[i + 1]; - bool foundNextVel = false; int nextAbsPos = notes[0].position + nextPhoneme.position; var (nextPhonemeUN, _) = UNoteAt(nextAbsPos); - nextVel = CalcConvel(nextPhonemeUN); + if (nextPhonemeUN != null) { + nextVel = CalcConvel(nextPhonemeUN); + } } - - float vel; - switch (Classify(phoneme.phoneme)) { - case "V C": case "VC": case "VC-": - case "VCC": case "VCC-": case "codaCC": case "C C": - case "VC C": - if (GetNoteForPhoneme(phoneme, notes).lyric == "+" || - GetNoteForPhoneme(phoneme, notes).lyric == "+~") { + + // Check for manual user override + bool isManualOverride = false; + float vel = noteVel; + + if (phonemeUN?.phonemeExpressions != null && phonemeUN.phonemeExpressions.Count > 0) { + var userExp = phonemeUN.phonemeExpressions.FirstOrDefault(e => + (e.abbr == "vel" || e.descriptor?.abbr == "vel") && (e.index ?? 0) == i); + if (userExp != null) { + vel = userExp.value; + isManualOverride = true; + } + } + + // Automatic ConVel assignment + if (!isManualOverride) { + string type = Classify(phoneme.phoneme); + switch (type) { + case "V C": case "VC": case "VC-": + case "VCC": case "VCC-": case "codaCC": case "C C": + case "VC C": case "V-": case "CC-": + var n = GetNoteForPhoneme(phoneme, notes); + if (n.lyric == "+" || n.lyric == "+~" || n.lyric.StartsWith("+")) { + vel = noteVel; + break; + } + vel = prevVel ?? noteVel; + break; + + case "onsetCC": case "-CC": + vel = nextVel ?? noteVel; + break; + + default: vel = noteVel; break; - } - vel = prevVel ?? noteVel; - break; - case "onsetCC": case "-CC": - vel = nextVel ?? noteVel; - break; - default: - vel = noteVel; - break; + } } phoneme.expressions = new List { new PhonemeExpression { abbr = "vel", value = vel } }; result.phonemes[i] = phoneme; + + // Transitions inherit this phoneme's velocity as their preceding anchor + prevVel = vel; } return result; @@ -388,12 +370,6 @@ protected override List ProcessSyllable(Syllable syllable) { int prevWordConsonantsCount = syllable.prevWordConsonantsCount; int lastCPrevWord = syllable.prevWordConsonantsCount; - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } string basePhoneme = null; var phonemes = new List(); // --------------------------- STARTING V ------------------------------- // @@ -473,6 +449,10 @@ protected override List ProcessSyllable(Syllable syllable) { basePhoneme = ccv; } } + if (liquid.Contains(cc[2]) || semivowel.Contains(cc[2]) + || liquid.Contains(ValidateAlias(cc[2])) || semivowel.Contains(ValidateAlias(cc[2]))) { + glides(ccv); + } } // if there still is no match, add [-CC] + [CC] etc. @@ -487,6 +467,10 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(currentCc, syllable.tone)) { phonemes.Add(currentCc); } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(currentCc); + } } } } @@ -613,6 +597,11 @@ protected override List ProcessSyllable(Syllable syllable) { } phonemes.Add(parsingVCC); phonemes.Add(parsingCC); + + if (liquid.Contains(cc[1]) || semivowel.Contains(cc[1]) + || liquid.Contains(ValidateAlias(cc[1])) || semivowel.Contains(ValidateAlias(cc[1]))) { + glides(parsingCC); + } } else { // bonehead [On-] + [n h] + [he] parsingCC = $"{cc[0]} {cc[1]}"; @@ -709,6 +698,11 @@ protected override List ProcessSyllable(Syllable syllable) { phonemes.Add(vc); startingC = 0; lastCforLoop -= 2; + + if (liquid.Contains(cc[2]) || semivowel.Contains(cc[2]) + || liquid.Contains(ValidateAlias(cc[2])) || semivowel.Contains(ValidateAlias(cc[2]))) { + glides(ccNoParse); + } } else { ccNoParse = $"{cc[cc.Length - 2]}{cc[cc.Length - 1]}"; var ccSP = $"{cc[0]}{cc[1]}"; @@ -721,6 +715,10 @@ protected override List ProcessSyllable(Syllable syllable) { break; } } + if (liquid.Contains(cc[1]) || semivowel.Contains(cc[1]) + || liquid.Contains(ValidateAlias(cc[1])) || semivowel.Contains(ValidateAlias(cc[1]))) { + glides(ccNoParse); + } } if (dontParse) { @@ -852,6 +850,11 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto($"{cc[i]} {cc[i + 1]}", syllable.vowelTone)) { parsingCC = $"{cc[i]} {cc[i + 1]}"; } + + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1]) + || liquid.Contains(ValidateAlias(cc[i + 1])) || semivowel.Contains(ValidateAlias(cc[i + 1]))) { + glides(parsingCC); + } } //if (i + 1 != lastCforLoop - 1) { @@ -1047,14 +1050,16 @@ private string CheckVCExceptions(string vc) { } return vc; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { //foreach (var consonant in new[] { "h" }) { // alias = alias.Replace(consonant, "hh"); //} - if (isYamlFallbacks) { - foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(syllable.Key, syllable.Value); + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; } + alias = baseResolved; } foreach (var consonant in new[] { "6r" }) { alias = alias.Replace(consonant, "3"); @@ -1062,5 +1067,59 @@ protected override string ValidateAlias(string alias) { return alias; } + + protected override PhonemeAttributes GetDynamicPhonemeAttributes(string alias, int index, PhonemeAttributes currentAttr, Note[] notes) { + if (unotes.Count == 0 || !useConvel) return currentAttr; + + // If this phoneme itself was manually edited via the envelope/property editor, use it directly + if (currentAttr.consonantStretchRatio.HasValue && Math.Abs(currentAttr.consonantStretchRatio.Value - 1.0) > 0.0001) { + return currentAttr; + } + + string type = Classify(alias); + + int targetPos = notes[0].position; + if (notes.Length > 1) { + bool isTransition = (type == "VC" || type == "V C" || type == "VC-" || type == "VCC" + || type == "VCC-" || type == "codaCC" || type == "C C" || type == "VC C" || type == "V-" || type == "CC-"); + + int noteIdx = Math.Clamp(index / 2, 0, notes.Length - 1); + if (isTransition && noteIdx > 0) { + noteIdx--; + } + targetPos = notes[noteIdx].position; + } + + var (targetUN, _) = UNoteAt(targetPos); + float vel = targetUN != null ? CalcConvel(targetUN) : 100f; + + if (targetUN?.phonemeExpressions != null && targetUN.phonemeExpressions.Count > 0) { + var userExp = targetUN.phonemeExpressions.FirstOrDefault(e => + (e.abbr == "vel" || e.descriptor?.abbr == "vel") && e.index == currentAttr.index); + if (userExp != null) { + vel = userExp.value; + } + } + + // Assign stretch ratio only to this specific phoneme + currentAttr.consonantStretchRatio = Math.Pow(2.0, (100.0 - vel) / 100.0); + return currentAttr; + } + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); + foreach (var kvp in sortedOverrides) { + var symbol = kvp.Key; + var value = kvp.Value; + + if (Regex.IsMatch(alias, $@"(? vowels; private string[] vowels = "a i u e o ay ey oy uy ow aw ew".Split(); @@ -58,152 +58,6 @@ protected override IG2p LoadBaseDictionary() { return new G2pFallbacks(g2ps.ToArray()); } - private Dictionary StartingConsonant => startingConsonant; - private static readonly Dictionary startingConsonant = new Dictionary { - { "", "" }, - { "b", "b" }, - { "by", "by" }, - { "ch", "ch" }, - { "d", "d" }, - { "dh", "d" }, - { "f", "f" }, - { "g", "g" }, - { "gy", "gy" }, - { "h", "h" }, - { "hy", "hy" }, - { "j", "j" }, - { "k", "k" }, - { "ky", "ky" }, - { "l", "r" }, - { "ly", "ry" }, - { "m", "m" }, - { "my", "my" }, - { "n", "n" }, - { "ny", "ny" }, - { "ng", "n" }, - { "p", "p" }, - { "py", "py" }, - { "r", "r" }, - { "ry", "ry" }, - { "s", "s" }, - { "sh", "sh" }, - { "t", "t" }, - { "q", "-" }, - { "ts", "ts" }, - { "th", "s" }, - { "v", "v" }, - { "w", "w" }, - { "y", "y" }, - { "z", "z" }, - { "zh", "sh" }, - }; - - private Dictionary SoloConsonant => soloConsonant; - private static readonly Dictionary soloConsonant = new Dictionary { - { "b", "ぶ" }, - { "by", "び" }, - { "ch", "ちゅ" }, - { "d", "ど" }, - { "dh", "ず" }, - { "f", "ふ" }, - { "g", "ぐ" }, - { "gy", "ぎ" }, - { "h", "ほ" }, - { "hy", "ひ" }, - { "j", "じゅ" }, - { "k", "く" }, - { "ky", "き" }, - { "l", "る" }, - { "ly", "り" }, - { "m", "む" }, - { "my", "み" }, - { "n", "ん" }, - { "ny", "に" }, - { "ng", "ん" }, - { "p", "ぷ" }, - { "py", "ぴ" }, - { "r", "る" }, - { "ry", "り" }, - { "s", "す" }, - { "sh", "しゅ" }, - { "t", "と" }, - { "ts", "つ" }, - { "th", "す" }, - { "v", "ヴ" }, - { "w", "う" }, - { "y", "い" }, - { "z", "ず" }, - { "zh", "しゅ" }, - }; - - private string[] SpecialClusters = "ky gy ts ny hy by py my ry ly".Split(); - - private Dictionary AltCv => altCv; - private static readonly Dictionary altCv = new Dictionary { - {"si", "suli" }, - {"zi", "zuli" }, - {"ti", "teli" }, - {"tu", "tolu" }, - {"di", "deli" }, - {"du", "dolu" }, - {"hu", "holu" }, - {"yi", "i" }, - {"wu", "u" }, - {"wo", "ulo" }, - {"rra", "wa" }, - {"rri", "wi" }, - {"rru", "ru" }, - {"rre", "we" }, - {"rro", "ulo" }, - }; - - private Dictionary ConditionalAlt => conditionalAlt; - private static readonly Dictionary conditionalAlt = new Dictionary { - {"ulo", "wo"}, - {"va", "fa"}, - {"vi", "fi"}, - {"vu", "fu"}, - {"ヴ", "ふ"}, - {"ve", "fe"}, - {"vo", "fo"}, - }; - - private Dictionary ExtraCv => extraCv; - private static readonly Dictionary extraCv = new Dictionary { - {"kye", new [] { "ki", "e" } }, - {"gye", new [] { "gi", "e" } }, - {"suli", new [] { "se", "i" } }, - {"she", new [] { "si", "e" } }, - {"zuli", new [] { "ze", "i" } }, - {"je", new [] { "ji", "e" } }, - {"teli", new [] { "te", "i" } }, - {"tolu", new [] { "to", "u" } }, - {"che", new [] { "chi", "e" } }, - {"tsa", new [] { "tsu", "a" } }, - {"tsi", new [] { "tsu", "i" } }, - {"tse", new [] { "tsu", "e" } }, - {"tso", new [] { "tsu", "o" } }, - {"deli", new [] { "de", "i" } }, - {"dolu", new [] { "do", "u" } }, - {"nye", new [] { "ni", "e" } }, - {"hye", new [] { "hi", "e" } }, - {"holu", new [] { "ho", "u" } }, - {"fa", new [] { "fu", "a" } }, - {"fi", new [] { "fu", "i" } }, - {"fe", new [] { "fu", "e" } }, - {"fo", new [] { "fu", "o" } }, - {"bye", new [] { "bi", "e" } }, - {"pye", new [] { "pi", "e" } }, - {"mye", new [] { "mi", "e" } }, - {"ye", new [] { "i", "e" } }, - {"rye", new [] { "ri", "e" } }, - {"wi", new [] { "u", "i" } }, - {"we", new [] { "u", "e" } }, - {"ulo", new [] { "u", "o" } }, - }; - - private string[] affricates = "ts ch j".Split(); - protected override string[] GetSymbols(Note note) { string[] original = base.GetSymbols(note); if (note.lyric == "ng") { @@ -274,244 +128,5 @@ protected override string[] GetSymbols(Note note) { } return modified.ToArray(); } - - protected override List ProcessSyllable(Syllable syllable) { - // Skip processing if this note extends the prevous syllable - if (CanMakeAliasExtension(syllable)) { - return new List { null }; - } - - var prevV = syllable.prevV; - var cc = syllable.cc; - var v = syllable.v; - var phonemes = new List(); - var usingVC = false; - - if (prevV.Length == 0) { - prevV = "-"; - } - - // Check CCs for special clusters - var adjustedCC = new List(); - for (var i = 0; i < cc.Length; i++) { - if (i == cc.Length - 1) { - adjustedCC.Add(cc[i]); - } else { - if (cc[i] == cc[i + 1]) { - adjustedCC.Add(cc[i]); - i++; - continue; - } - var diphone = $"{cc[i]}{cc[i + 1]}"; - if (SpecialClusters.Contains(diphone)) { - adjustedCC.Add(diphone); - i++; - } else { - adjustedCC.Add(cc[i]); - } - } - } - cc = adjustedCC.ToArray(); - - // Separate CCs and main CV - var finalCons = ""; - if (cc.Length > 0) { - finalCons = cc[cc.Length - 1]; - - var start = 0; - (var hasVc, var vcPhonemes) = HasVc(prevV, cc[0], syllable.tone, cc.Length); - usingVC = hasVc; - phonemes.AddRange(vcPhonemes); - - if (usingVC) { - start = 1; - } - - for (var i = start; i < cc.Length - 1; i++) { - var cons = SoloConsonant[cc[i]]; - if (!usingVC) { - cons = TryVcv(prevV, cons, syllable.tone); - } else { - usingVC = false; - } - if (HasOto(cons, syllable.tone)) { - phonemes.Add(cons); - } else if (ConditionalAlt.ContainsKey(cons)) { - cons = ConditionalAlt[cons]; - phonemes.Add(TryVcv(prevV, cons, syllable.tone)); - } - prevV = WanaKana.ToRomaji(cons).Last().ToString(); - } - } - - // Convert to hiragana - var cv = $"{StartingConsonant[finalCons]}{v}"; - cv = AltCv.ContainsKey(cv) ? AltCv[cv] : cv; - var hiragana = ToHiragana(cv); - if (!usingVC) { - hiragana = TryVcv(prevV, hiragana, syllable.vowelTone); - } else { - hiragana = FixCv(hiragana, syllable.vowelTone); - } - - // Check for nonstandard CV - var split = false; - if (HasOto(hiragana, syllable.vowelTone)) { - phonemes.Add(hiragana); - } else if (ConditionalAlt.ContainsKey(cv)) { - cv = ConditionalAlt[cv]; - hiragana = TryVcv(prevV, ToHiragana(cv), syllable.vowelTone); - if (HasOto(hiragana, syllable.vowelTone)) { - phonemes.Add(hiragana); - } else { - split = true; - } - } else { - split = true; - } - - // Handle nonstandard CV - if (split && ExtraCv.ContainsKey(cv)) { - var splitCv = ExtraCv[cv]; - for (var i = 0; i < splitCv.Length; i++) { - if (splitCv[i] != prevV) { - var converted = ToHiragana(splitCv[i]); - phonemes.Add(TryVcv(prevV, converted, syllable.vowelTone)); - prevV = splitCv[i].Last().ToString(); - } - } - } - - return phonemes; - } - - protected override List ProcessEnding(Ending ending) { - var prevV = ending.prevV; - var cc = ending.cc; - var phonemes = new List(); - - // Check CCs for special clusters - var adjustedCC = new List(); - for (var i = 0; i < cc.Length; i++) { - if (i == cc.Length - 1) { - adjustedCC.Add(cc[i]); - } else { - if (cc[i] == cc[i + 1]) { - adjustedCC.Add(cc[i]); - i++; - continue; - } - var diphone = $"{cc[i]}{cc[i + 1]}"; - if (SpecialClusters.Contains(diphone)) { - adjustedCC.Add(diphone); - i++; - } else { - adjustedCC.Add(cc[i]); - } - } - } - cc = adjustedCC.ToArray(); - - var usingVC = false; - // Convert to hiragana - for (var i = 0; i < cc.Length; i++) { - var symbol = cc[i]; - - if (i == 0) { - (var hasVc, var vcPhonemes) = HasVc(prevV, symbol, ending.tone, cc.Length + 1); - usingVC = hasVc; - phonemes.AddRange(vcPhonemes); - if (usingVC) { - continue; - } - } - - var solo = SoloConsonant[symbol]; - if (!usingVC) { - solo = TryVcv(prevV, solo, ending.tone); - } else { - usingVC = false; - solo = FixCv(solo, ending.tone); - } - - if (HasOto(solo, ending.tone)) { - phonemes.Add(solo); - } else if (ConditionalAlt.ContainsKey(solo)) { - solo = ConditionalAlt[solo]; - if (!usingVC) { - solo = TryVcv(prevV, solo, ending.tone); - } else { - solo = FixCv(solo, ending.tone); - } - phonemes.Add(solo); - } - - if (solo.Contains("ん")) { - if (ending.IsEndingVCWithOneConsonant) { - TryAddPhoneme(phonemes, ending.tone, $"n R", $"n -", $"n-"); - } else if (ending.IsEndingVCWithMoreThanOneConsonant && cc.Last() == "n" || cc.Last() == "ng") { - TryAddPhoneme(phonemes, ending.tone, $"n R", $"n -", $"n-"); - } - } - - prevV = WanaKana.ToRomaji(solo).Last().ToString(); - } - - if (ending.IsEndingV) { - TryAddPhoneme(phonemes, ending.tone, $"{prevV} R", $"{prevV} -", $"{prevV}-"); - } - - return phonemes; - } - - private (bool, string[]) HasVc(string vowel, string cons, int tone, int cc) { - if (vowel == "" || vowel == "-") { - return (false, new string[0]); - } - - var phonemes = new List(); - if (cons == "r") { - cons = "w"; - } else if (cons == "l") { - cons = "r"; - } else if (cons == "ly") { - cons = "ry"; - } else { - cons = StartingConsonant[cons]; - } - - var vc = $"{vowel} {cons}"; - var altVc = $"{vowel} {cons[0]}"; - - if (HasOto(vc, tone)) { - phonemes.Add(vc); - } else if (HasOto(altVc, tone)) { - phonemes.Add(altVc); - } else { - return (false, new string[0]); - } - - if (affricates.Contains(cons) && cc > 1) { - phonemes.Add(FixCv(SoloConsonant[cons], tone)); - } - - return (phonemes.Count > 0, phonemes.ToArray()); - } - - private string TryVcv(string vowel, string cv, int tone) { - var vcv = $"{vowel} {cv}"; - return HasOto(vcv, tone) ? vcv : FixCv(cv, tone); - } - - private string FixCv(string cv, int tone) { - var alt = $"- {cv}"; - return HasOto(cv, tone) ? cv : HasOto(alt, tone) ? alt : cv; - } - - private string ToHiragana(string romaji) { - var hiragana = WanaKana.ToHiragana(romaji); - hiragana = hiragana.Replace("ゔ", "ヴ"); - return hiragana; - } } } diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 96171865e..80a161699 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -15,7 +15,7 @@ namespace OpenUtau.Plugin.Builtin { [Phonemizer("Filipino Phonemizer", "FIL VCV & CVVC", "Cadlaxa", language: "FIL")] - public class FilipinoPhonemizer : SyllableBasedPhonemizer { + public class FilipinoPhonemizer : ArpasingPlusPhonemizer { protected override string YamlFileName => "filipino.yaml"; protected override byte[] YamlTemplate => Data.Resources.filipino_template; public FilipinoPhonemizer() { @@ -23,64 +23,20 @@ public FilipinoPhonemizer() { "a", "e", "i", "o", "u", "ay", "ey", "oy", "uy", "aw", "ew", "ow", "iw" }; this.consonants = Array.Empty(); + this.diphthongTails = new Dictionary() { + { "ay", "y" }, + { "ey", "y" }, + { "oy", "y" }, + { "uy", "y" }, + { "aw", "w" }, + { "ew", "w" }, + { "ow", "w" }, + { "iw", "w" }, + }; } - protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => ""; - - List consExceptions = new List(); - - string[] diphthongs = new[] { "ay", "ey", "oy", "uy", "aw", "ew", "ow", "iw" }; - - // For banks with missing vowels - private readonly Dictionary missingVphonemes = "ax=a".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingVPhonemes = false; - private bool isYamlFallbacks = false; - - - // For banks with missing custom consonants - private readonly Dictionary missingCphonemes = "N=n".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingCPhonemes = false; - private bool cPV_FallBack = false; - - private readonly Dictionary vvDiphthongExceptions = - new Dictionary() { - {"aw","a"}, - {"ow","o"}, - {"iw","i"}, - {"ay","a"}, - {"ey","e"}, - {"oy","o"}, - {"uy","u"}, - {"ew","e"}, - }; - - private readonly Dictionary vvExceptions = - new Dictionary() { - {"aw","w"}, - {"ow","w"}, - {"iw","w"}, - {"ay","y"}, - {"ey","y"}, - {"oy","y"}, - {"uy","y"}, - {"ew","w"}, - }; - - private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; - private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; - private static readonly string[] FinalConsonants = { "w", "y", "r", "l", "m", "n", "ng" }; - - protected override string[] GetSymbols(Note note) { string[] original = base.GetSymbols(note); if (!string.IsNullOrEmpty(note.phoneticHint)) { @@ -140,13 +96,9 @@ protected override string[] GetSymbols(Note note) { original = fallbackSplit.ToArray(); } - List modified = new List(original); - List finalPhonemes = new List(); - finalPhonemes = new List(modified); List finalProcessedPhonemes = new List(); - IEnumerable phonemes; - phonemes = finalPhonemes; - foreach (string s in phonemes) { + + foreach (string s in original) { switch (s) { default: finalProcessedPhonemes.Add(s); @@ -155,905 +107,23 @@ protected override string[] GetSymbols(Note note) { } return finalProcessedPhonemes.ToArray(); } + protected override IG2p[] GetBaseG2ps() => Array.Empty(); - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - // LOAD DICTIONARY FROM FOLDER - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - // LOAD DICTIONARY FROM SINGER FOLDER - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - //g2ps.Add(new ArpabetPlusG2p()); - return new G2pFallbacks(g2ps.ToArray()); - } - public override void SetSinger(USinger singer) { - base.SetSinger(singer); - - if (this.singer != null && this.singer.Loaded) { - - consExceptions.Clear(); - if (stop != null) consExceptions.AddRange(stop); - if (tap != null) consExceptions.AddRange(tap); - - consExceptions = consExceptions.Distinct().ToList(); - } - } - - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } - protected override List ProcessSyllable(Syllable syllable) { - syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; - var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); - var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; - string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string v = ReplacePhoneme(syllable.v, syllable.vowelTone); - List vowels = new List { v }; - string basePhoneme; - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - - // Check for missing vowel phonemes - foreach (var entry in missingVphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isMissingVPhonemes = true; - break; - } - } - - // Check for missing consonant phonemes - foreach (var entry in missingCphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isMissingCPhonemes = true; - break; - } - } - - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - - // STARTING V - if (syllable.IsStartingV) { - basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); - } - // [V V] or [V C][C V]/[V] - else if (syllable.IsVV) { - if (!CanMakeAliasExtension(syllable)) { - basePhoneme = $"{prevV} {v}"; - if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && vvExceptions.ContainsKey(prevV) && prevV != v) { - // VV IS NOT PRESENT, CHECKS VVEXCEPTIONS LOGIC - //var vc = $"{prevV}{vvExceptions[prevV]}"; - var vc = AliasFormat($"{vvExceptions[prevV]}", "vcEx", syllable.vowelTone, prevV); - TryAddPhoneme(phonemes, syllable.vowelTone, vc); - basePhoneme = AliasFormat($"{vvExceptions[prevV]} {v}", "dynMid", syllable.vowelTone, ""); - } else { - { - if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV} {v}"; - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { - basePhoneme = v; - } else { - basePhoneme = AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{prevV} -", "dynMid", syllable.vowelTone, "")); - } - } - } - // EXTEND AS [V] - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone) || missingVphonemes.ContainsKey(prevV)) { - basePhoneme = v; - } else if (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone) && vvDiphthongExceptions.ContainsKey(prevV)) { - basePhoneme = $"{vvDiphthongExceptions[prevV]} {vvDiphthongExceptions[prevV]}"; - } else { - // PREVIOUS ALIAS WILL EXTEND as [V V] - basePhoneme = null; - } - - // [- CV/C V] or [- C][CV/C V] - } else if (syllable.IsStartingCVWithOneConsonant) { - var rcv = $"- {cc[0]} {v}"; - var rcv1 = $"- {cc[0]}{v}"; - var crv = $"{cc[0]} {v}"; - /// - CV - if (HasOto(rcv, syllable.vowelTone) && HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) && HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); - /// CV - } else if (HasOto(crv, syllable.vowelTone) && HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } else { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } - // [CCV/CC V] or [C C] + [CV/C V] - } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { - // TRY [- CCV]/[- CC V] or [- CC][CCV]/[CC V] or [- C][C C][C V]/[CV] - var rccv = $"- {string.Join("", cc)} {v}"; - var rccv1 = $"- {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - var crv1 = $"{cc.Last()}{v}"; - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - /// - CCV - if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); - lastC = 0; - } else { - /// CCV and CV - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // TRY RCC [- CC] - for (var i = cc.Length; i > 1; i--) { - if (!ccvException.Contains(cc[0])) { - if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""))) { - firstC = i - 1; - } - } - break; - } - // [- C] - if (phonemes.Count == 0) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } - // try [CC V] or [CCV] - var cv = $"{cc.Last()}{v}"; - for (var i = firstC; i < cc.Length - 1; i++) { - /// CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - /// C-Last - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - } - } else { // VCV - var vcv = $"{prevV} {cc[0]}{v}"; - var vcv2 = $"{prevV}{cc[0]}{v}"; - var vcvEnd = $"{prevV}{cc[0]} {v}"; - var vccv = $"{prevV} {string.Join("", cc)}{v}"; - var vccv2 = $"{prevV} {string.Join("", cc)}"; - var vccv3 = $"{prevV}{string.Join("", cc)}"; - var crv = $"{cc.Last()} {v}"; - // Use regular VCV if the current word starts with one consonant and the previous word ends with none - if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv; - } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv2, syllable.vowelTone) && HasOto(ValidateAlias(vcv2), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv2; - // Use end VCV if current word does not start with a consonant but the previous word does end with one - } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) && HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { - basePhoneme = vcvEnd; - // Use regular VCV if end VCV does not exist - } else if (syllable.IsVCVWithOneConsonant && (!HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone)) && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone))) { - basePhoneme = vcv; - // VCV with multiple consonants, only for current word onset and null previous word ending - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) && HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { - basePhoneme = vccv; - lastC = 0; - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv3, syllable.vowelTone) && HasOto(ValidateAlias(vccv3), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{prevV} {string.Join("", cc)}{v}", "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else { - var cv = cc.Last() + v; - basePhoneme = cv; - if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { - basePhoneme = crv; - } - // try [CC V] or [CCV] - for (var i = firstC; i < cc.Length - 1; i++) { - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - /// CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - /// C-Last - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - // try [V C], [V CC], [VC C], [V -][- C] - for (var i = lastC + 1; i >= 0; i--) { - var vr = $"{prevV} -"; - //var vc_c = $"{prevV}{string.Join(" ", cc.Take(2))}-"; - //var vc_c2 = $"{prevV}{string.Join(" ", cc.Take(2))}_"; - var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; - var vc = $"{prevV} {cc[0]}"; - // Boolean Triggers - bool CCV = false; - if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { - if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { - CCV = true; - } - } - - if (i == 0 && (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) && !HasOto(vc, syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, vr, ValidateAlias(vr)); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - break; - } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { - TryAddPhoneme(phonemes, syllable.tone, vcc, ValidateAlias(vcc)); - firstC = 1; - break; - /*} else if (HasOto(vc_c, syllable.tone) && HasOto(ValidateAlias(vc_c), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} -", "dynMid", syllable.vowelTone, "")); - firstC = 1; - break; - } else if (HasOto(vc_c2, syllable.tone) && HasOto(ValidateAlias(vc_c2), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} _", "dynMid", syllable.vowelTone, "")); - firstC = 1; - break;*/ - } else if (cPV_FallBack && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { - TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); - break; - } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); - break; - } else { - continue; - } - } - } - } - - for (var i = firstC; i < lastC; i++) { - var ccv = $"{string.Join("", cc.Skip(i + 1))} {v}"; - var ccv1 = $"{string.Join("", cc.Skip(i + 1))}{v}"; - var cc1 = $"{string.Join(" ", cc.Skip(i))}"; - var lcv = $"{cc.Last()} {v}"; - var cv = $"{cc.Last()}{v}"; - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - var c1 = cc[i]; - var c2 = cc[i + 1]; - bool c1IsException = consExceptions.Contains(c1); - bool c2IsException = consExceptions.Contains(c2); - - // Scenario 1: Both are NOT exceptions - if (!c1IsException && !c2IsException) { - // [C1 -] [- C2] - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); - } - // Scenario 2: C1 is an exception, C2 is NOT - else if (c1IsException && !c2IsException) { - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - } - // Scenario 3: C1 is NOT an exception, C2 is - else if (!c1IsException && c2IsException) { - cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); - } - // Scenario 4: Both are exceptions - else if (c1IsException && c2IsException) { - cc1 = ""; - } - } - // CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // [C1 C2C3] - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - } - // CV - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - - if (i + 1 < lastC) { - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - var c1 = cc[i]; - var c2 = cc[i + 1]; - bool c1IsException = consExceptions.Contains(c1); - bool c2IsException = consExceptions.Contains(c2); - - // Scenario 1: Both are NOT exceptions - if (!c1IsException && !c2IsException) { - // [C1 -] [- C2] - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); - } - // Scenario 2: C1 is an exception, C2 is NOT - else if (c1IsException && !c2IsException) { - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - } - // Scenario 3: C1 is NOT an exception, C2 is - else if (!c1IsException && c2IsException) { - cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); - } - // Scenario 4: Both are exceptions - else if (c1IsException && c2IsException) { - cc1 = ""; - } - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // [C1 C2C3] - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - } - // CV - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - - if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { - // like [V C1] [C1 C2] [C2 C3] [C3 ..] - TryAddPhoneme(phonemes, syllable.vowelTone, cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { - // like [V C1] [C1 C2] [C2 ..] - if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { - i++; - } - } else { - // like [V C1] [C1] [C2 ..] - TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); - } - } else { - TryAddPhoneme(phonemes, syllable.tone, cc1); - } - } - - phonemes.Add(basePhoneme); - return phonemes; - } - - protected override List ProcessEnding(Ending ending) { - string prevV = ReplacePhoneme(ending.prevV, ending.tone); - string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); - string v = ReplacePhoneme(ending.prevV, ending.tone); - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; - - if (ending.IsEndingV) { - var vR = $"{v} {t}"; - var vR2 = $"{v}{t}"; - if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{v}", "ending", ending.tone, "", t))); - } - } else if (ending.IsEndingVCWithOneConsonant) { - var vc = $"{v} {cc[0]}"; - var vcr = $"{v} {cc[0]}{t}"; - var vcr2 = $"{v}{cc[0]} {t}"; - var vcr3 = $"{v}{cc[0]}{t}"; - if (!RomajiException.Contains(cc[0])) { - if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ValidateAlias(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t))); - } else if (!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone) || !HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || !HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, vc); - if (vc.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); - } - } else { - TryAddPhoneme(phonemes, ending.tone, vc); - if (vc.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); - } - } - } - } else { - for (var i = lastC; i >= 0; i--) { - var vr = $"{v} {t}"; - var vr1 = $"{v} R"; - var vr2 = $"{v}{t}"; - var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; - var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} {t}"; - var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; - var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; - var vc = $"{v} {cc[0]}"; - if (!RomajiException.Contains(cc[0])) { - if (i == 0) { - if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t)); - } - break; - } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc); - firstC = 1; - break; - } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc2); - firstC = 1; - break; - } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc3); - if (vcc3.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); - } - } - firstC = 1; - break; - } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc4); - if (vcc4.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); - } - } - firstC = 1; - break; - } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) - || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) - || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) - || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, vc); - break; - } else { - TryAddPhoneme(phonemes, ending.tone, vc); - break; - } - } - } - for (var i = firstC; i < lastC; i++) { - var cc1 = $"{cc[i]} {cc[i + 1]}"; - if (i < cc.Length - 2) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc2, ending.tone)) { - cc2 = ValidateAlias(cc2); - } - - if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { - // [C1 -] [- C2] - cc2 = AliasFormat($"{cc[i + 2]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { - // like [C1 C2][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc1); - } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { - // like [C1 C2-][C3 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i]); - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { - // like [C1 C2-][C3 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "", t)); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); - i++; - } else { - // like [C1][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); - i++; - } - } else { - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 -] [- C2] - if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - cc1 = AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "", t)); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { - // like [C1 C2-] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]} {t}", ValidateAlias($"{cc[i]} {cc[i + 1]} {t}"))) { - // like [C1 C2 -] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}{t}", ValidateAlias($"{cc[i]}{cc[i + 1]}{t}"))) { - // like [C1C2-] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]} {t}", ValidateAlias($"{cc[i]}{cc[i + 1]} {t}"))) { - // like [C1C2 -] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - // like [C1 C2][C2 -] - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 2]}", "cc_endB", ending.tone, "", t)); - i++; - } - } - } - } - return phonemes; - } - private string AliasFormat(string alias, string type, int tone, string prevV, string t = "-") { - var aliasFormats = new Dictionary { - // Define alias formats for different types - { "dynStart", new string[] { "" } }, - { "dynMid", new string[] { "" } }, - { "dynMid_vv", new string[] { "" } }, - { "dynEnd", new string[] { "" } }, - { "startingV", new string[] { "-", "- ", "_", "" } }, - { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, - { "vvExtend", new string[] { "", "_", "-", "- " } }, - { "cv", new string[] { "-", "", "- ", "_" } }, - { "cvStart", new string[] { "-", "- ", "_" } }, - { "ending", new string[] { $" {t}", $"{t}"} }, - { "ending_mix", new string[] { $"{t}", $" {t}", "--" } }, - { "cc", new string[] { "", "-", "- ", "_" } }, - { "cc_start", new string[] { "- ", "-", "_" } }, - { "cc_end", new string[] { $" {t}", $"{t}", "" } }, - { "cc_inB", new string[] { "_", "-", "- " } }, - { "cc_endB", new string[] { "_", $"{t}", $" {t}" } }, - { "cc_mix", new string[] { $" {t}", " R", $"{t}", "", "_", $"{t} ", $"{t}" } }, - { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, - }; - - // Check if the given type exists in the aliasFormats dictionary - if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { - return alias; - } - - // Handle dynamic variations when type contains "dynamic" - if (type.Contains("dynStart")) { - string consonant = ""; - string vowel = ""; - // If the alias contains a space, split it into consonant and vowel - if (alias.Contains(" ")) { - var parts = alias.Split(' '); - consonant = parts[0]; - vowel = parts[1]; - } else { - consonant = alias; - } - - // Handle the alias with space and without space - var dynamicVariations = new List { - // Variations with space, dash, and underscore - $"- {consonant}{vowel}", // "- CV" - $"- {consonant} {vowel}", // "- C V" - $"-{consonant} {vowel}", // "-C V" - $"-{consonant}{vowel}", // "-CV" - $"-{consonant}_{vowel}", // "-C_V" - $"- {consonant}_{vowel}", // "- C_V" - }; - // Check each dynamically generated format - foreach (var variation in dynamicVariations) { - if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { - return variation; - } - } - } - - if (type.Contains("dynMid")) { - string consonant = ""; - string vowel = ""; - // If the alias contains a space, split it into consonant and vowel - if (alias.Contains(" ")) { - var parts = alias.Split(' '); - consonant = parts[0]; - vowel = parts[1]; - } else { - consonant = alias; - } - var dynamicVariations1 = new List { - $"{consonant}{vowel}", // "CV" - $"{consonant} {vowel}", // "C V" - $"{consonant}_{vowel}", // "C_V" - }; - // Check each dynamically generated format - foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { - return variation1; - } - } - } - - if (type.Contains("dynEnd")) { - string consonant = ""; - string vowel = ""; - // If the alias contains a space, split it into consonant and vowel - if (alias.Contains(" ")) { - var parts = alias.Split(' '); - consonant = parts[1]; - vowel = parts[0]; - } else { - consonant = alias; - } - var dynamicVariations1 = new List { - $"{vowel}{consonant} -", // "VC -" - $"{vowel} {consonant}-", // "V C-" - $"{vowel}{consonant}-", // "VC-" - $"{vowel} {consonant} -", // "V C -" - }; - // Check each dynamically generated format - foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { - return variation1; - } - } - } - - // Get the array of possible alias formats for the specified type if not dynamic - var formatsToTry = aliasFormats[type]; - int counter = 0; - foreach (var format in formatsToTry) { - string aliasFormat; - if (type.Contains("mix") && counter < 4) { - aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; - counter++; - } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { - aliasFormat = $"{alias}{format}"; - } else { - aliasFormat = $"{format}{alias}"; - } - // Check if the formatted alias exists - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { - return aliasFormat; - } - } - return alias; - } - - protected override string ValidateAlias(string alias) { + // Endings has 50 ticks gap + protected override bool NoGap => true; + protected override string ValidateAlias(string alias, int tone = 0) { // VALIDATE ALIAS DEPENDING ON METHOD - if (isMissingVPhonemes) { - foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isMissingCPhonemes) { - foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isYamlFallbacks) { - foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - - return base.ValidateAlias(alias); - } - - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; + if (HasOto(alias, tone)) return alias; - // Exact token match - if (alias == phoneme) - return true; - - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; } + alias = baseResolved; } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - // consonant timings - - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); - foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } - } - - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } - } - - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } - - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } - - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } - - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; - } - } - - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return alias; } } -} +} \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs index 973907a1f..efc544dba 100644 --- a/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs @@ -544,7 +544,14 @@ protected override List ProcessEnding(Ending ending) { } //TODO: add "oi" exception - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } //fraloids conversion if (usesFraloids) { @@ -593,18 +600,12 @@ private string CheckCoeEnding(string cv, int tone) { return "no Coe Ending"; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs() * 1.25; + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + return otoLength; } private string CheckAliasFormatting(string alias, string type, int tone, string prevV) { diff --git a/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs index 15a36996e..9d03ca2f2 100644 --- a/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs @@ -253,19 +253,13 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } + // Endings has 50 ticks gap + protected override bool NoGap => true; - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs() * 1.25; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } protected override string[] GetSymbols(Note note) { diff --git a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs index 242b03a5c..4a34faf67 100644 --- a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs @@ -33,34 +33,9 @@ public GermanVCCVPhonemizer() { protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => "cmudict_de.txt"; - private bool isYamlFallbacks = false; - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Load dictionary from plugin folder. - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - - // Load dictionary from singer folder. - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - - // Load base g2p. - g2ps.Add(new GermanG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new GermanG2p() }; } protected override string[] GetSymbols(Note note) { @@ -69,6 +44,12 @@ protected override string[] GetSymbols(Note note) { return null; } List finalProcessedPhonemes = new List(); + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } string[] diphthongs = new[] { "aU", "OY", "aI" }; foreach (string s in original) { @@ -81,19 +62,6 @@ protected override string[] GetSymbols(Note note) { return finalProcessedPhonemes.ToArray(); } - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } - protected override List ProcessSyllable(Syllable syllable) { syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); @@ -109,13 +77,6 @@ protected override List ProcessSyllable(Syllable syllable) { string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - if (syllable.IsStartingV) { basePhoneme = $"- {v}"; ; } else if (syllable.IsVV) { @@ -406,7 +367,14 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } foreach (var VV in new[] { "a 6", "a6" }) { alias = alias.Replace(VV, "a a"); } @@ -448,17 +416,14 @@ protected override string ValidateAlias(string alias) { alias = alias.Replace("Y^", "Y"); } - if (isYamlFallbacks) { - foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - return alias; } + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - protected override double GetTransitionBasicLengthMs(string alias = "") { - return base.GetTransitionBasicLengthMs(); + return otoLength; } } } \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs index 6cd8f0c2c..0b66c4ce1 100644 --- a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs @@ -218,7 +218,14 @@ protected override List ProcessEnding(Ending ending) return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } if (isFallBack) { foreach (var fb in fallBacks) { alias = alias.Replace(fb.Key,fb.Value); @@ -226,5 +233,14 @@ protected override string ValidateAlias(string alias) { } return alias; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs index ed6d96598..bb292b8c5 100644 --- a/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs @@ -66,5 +66,14 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs index 46f70d4ad..8118933ba 100644 --- a/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs @@ -95,7 +95,14 @@ protected override List ProcessEnding(Ending ending) { } // russian specific replacements - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } foreach (var consonant in new[] { "'", "~" }) { alias = alias.Replace(consonant + "y", consonant + "i"); } @@ -105,18 +112,13 @@ protected override string ValidateAlias(string alias) { return aliasesFallback.ContainsKey(alias) ? aliasesFallback[alias] : alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } diff --git a/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs index 515bbed72..ce01c1a7e 100644 --- a/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs @@ -110,7 +110,14 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } foreach (var consonant in new[] { "'", "ch", "j" }) { foreach (var vowel in new[] { "ax", "ex" }) { alias = alias.Replace(consonant + vowel, consonant + "x"); @@ -124,18 +131,13 @@ protected override string ValidateAlias(string alias) { return alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs index ea5244d7e..1087bdeb4 100644 --- a/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs @@ -221,7 +221,14 @@ protected override List ProcessEnding(Ending ending) return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } foreach (var consonant in new[] { "B" }) { alias = alias.Replace("B", "b"); } @@ -248,5 +255,14 @@ protected override string ValidateAlias(string alias) { } return alias; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs index e350abf21..ddb47c76b 100644 --- a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs @@ -423,7 +423,14 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } // Validate alias depending on method if (isSeseo) { foreach (var syllable in seseo) { @@ -460,18 +467,13 @@ protected override string ValidateAlias(string alias) { return alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 2.0; - } - } - foreach (var c in new[] { "r" }) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs index 98fd877d5..281e88a7c 100644 --- a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs @@ -33,34 +33,11 @@ public SpanishVCCVPhonemizer() { .ToDictionary(parts => parts[0], parts => parts[1]); } - private bool isYamlFallbacks = false; protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => "cmudict_es.txt"; - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Load dictionary from plugin folder. - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - - // Load dictionary from singer folder. - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(new SpanishG2p()); - return new G2pFallbacks(g2ps.ToArray()); + protected override IG2p[] GetBaseG2ps() { + return new IG2p[] { new SpanishG2p() }; } protected override string[] GetSymbols(Note note) { @@ -68,6 +45,13 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } + List finalProcessedPhonemes = new List(); foreach (string s in original) { switch (s) { @@ -78,20 +62,6 @@ protected override string[] GetSymbols(Note note) { } return finalProcessedPhonemes.ToArray(); } - - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - return phoneme; - } - protected override List ProcessSyllable(Syllable syllable) { syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); @@ -107,13 +77,6 @@ protected override List ProcessSyllable(Syllable syllable) { string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - if (syllable.IsStartingV) { var rcv = $"- {v}"; var rcv2 = $"-{v}"; @@ -437,18 +400,20 @@ protected override List ProcessEnding(Ending ending) { } return phonemes; } - protected override string ValidateAlias(string alias) { + protected override string ValidateAlias(string alias, int tone = 0) { + string baseResolved = base.ValidateAlias(alias, tone); + if (!string.IsNullOrEmpty(baseResolved) && baseResolved != alias) { + if (HasOto(baseResolved, tone)) { + return baseResolved; + } + alias = baseResolved; + } //foreach (var consonant in new[] { "w" }) { // alias = alias.Replace("w", "u"); //} //foreach (var consonant in new[] { "y" }) { // alias = alias.Replace("y", "i"); // } - if (isYamlFallbacks) { - foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } var rules = new Dictionary { { "I", "y" }, { "U", "w" }, @@ -477,8 +442,12 @@ protected override string ValidateAlias(string alias) { return base.ValidateAlias(alias); } - protected override double GetTransitionBasicLengthMs(string alias = "") { - return base.GetTransitionBasicLengthMs(); + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 95ca6df74..8344f06d1 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -87,6 +87,17 @@ protected struct Syllable { /// public bool canAliasBeExtended; + // Lookahead & Context Properties + public string nextV; + public string[] nextCc; + public string prevBasePhoneme; + public string nextBasePhoneme; + + public string NextVowel => nextV ?? string.Empty; + public string[] NextCC => nextCc ?? Array.Empty(); + public string PrevBasePhoneme => prevBasePhoneme ?? string.Empty; + public string NextBasePhoneme => nextBasePhoneme ?? string.Empty; + // helpers public bool IsStartingV => prevV == "" && cc.Length == 0; public bool IsVV => prevV != "" && cc.Length == 0; @@ -159,14 +170,36 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN if (hasDictionary && isDictionaryLoading) { return MakeSimpleResult(""); } + + runtimeGlides.Clear(); - var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); + // Lookahead to next ending if available + Ending? nextEnding = nextNeighbour.HasValue ? MakeEnding(new[] { nextNeighbour.Value }) : null; + var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours), nextEnding); if (syllables == null) { return HandleError(); } - var phonemes = new List(); - foreach (var syllable in syllables) { + string[] predictedBases = new string[syllables.Length]; + for (int i = 0; i < syllables.Length; i++) { + var mod = ApplyBoundaryReplacements(syllables[i]); + if (tails.Contains(mod.v)) { + predictedBases[i] = mod.v; + } else { + var tempPhonemes = ProcessSyllable(mod); + predictedBases[i] = tempPhonemes?.LastOrDefault() ?? mod.v; + } + } + + var allPhonemeSymbols = new List(); + var syllablePhonemeBuckets = new List<(List symbols, int duration, int position, bool isEnding, int tone, string vowel)>(); + string runningPrevBasePhoneme = string.Empty; + + for (int i = 0; i < syllables.Length; i++) { + var syllable = syllables[i]; + syllable.prevBasePhoneme = runningPrevBasePhoneme; + syllable.nextBasePhoneme = (i + 1 < syllables.Length) ? predictedBases[i + 1] : string.Empty; + var modifiedSyllable = ApplyBoundaryReplacements(syllable); if (tails.Contains(modifiedSyllable.v)) { @@ -181,60 +214,143 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }; var endingPhonemes = ProcessEnding(ending); - - if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false)); + if (endingPhonemes != null && endingPhonemes.Count > 0) { + syllablePhonemeBuckets.Add((endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, "")); + allPhonemeSymbols.AddRange(endingPhonemes); } + runningPrevBasePhoneme = modifiedSyllable.v; continue; } - phonemes.AddRange(MakePhonemes(ProcessSyllable(modifiedSyllable), modifiedSyllable.duration, modifiedSyllable.position, false)); + + var syllablePhonemes = ProcessSyllable(modifiedSyllable); + if (syllablePhonemes != null && syllablePhonemes.Count > 0) { + syllablePhonemeBuckets.Add((syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, modifiedSyllable.v)); + allPhonemeSymbols.AddRange(syllablePhonemes); + runningPrevBasePhoneme = syllablePhonemes.LastOrDefault() ?? ""; + } } if (!nextNeighbour.HasValue) { var tryEnding = MakeEnding(notes); if (tryEnding.HasValue) { var ending = tryEnding.Value; - - if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { - ending.tail = nextNeighbour.Value.lyric; - } - var modifiedEnding = ApplyBoundaryReplacements(ending); var endingPhonemes = ProcessEnding(modifiedEnding); - if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedEnding.duration, modifiedEnding.position, true)); + if (endingPhonemes != null && endingPhonemes.Count > 0) { + syllablePhonemeBuckets.Add((endingPhonemes, modifiedEnding.duration, modifiedEnding.position, true, ending.tone, "")); + allPhonemeSymbols.AddRange(endingPhonemes); } } } + var workingAttributes = mainNote.phonemeAttributes != null + ? mainNote.phonemeAttributes.ToList() + : new List(); + + SyncAttributes(notes, allPhonemeSymbols, 0, workingAttributes); + + var phonemes = new List(); + int globalPhonemeIndex = 0; + + foreach (var bucket in syllablePhonemeBuckets) { + var madePhonemes = MakePhonemes(bucket.symbols, bucket.duration, bucket.position, bucket.isEnding, bucket.tone, workingAttributes.ToArray(), globalPhonemeIndex).ToList(); + int currentSyllablePhonemeCount = bucket.symbols.Count; + + if (!bucket.isEnding && madePhonemes.Count > 0) { + var basePhoneme = madePhonemes.Last(); + string baseAlias = basePhoneme.phoneme ?? ""; + + // Check exact alias match first, then fall back to the underlying vowel symbol + (string sustain, double offset) sustainData = default; + bool hasSustain = vowelSustains.TryGetValue(baseAlias, out sustainData) + || (!string.IsNullOrEmpty(bucket.vowel) && vowelSustains.TryGetValue(bucket.vowel, out sustainData)); + + if (hasSustain) { + string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, bucket.tone); + if (HasOto(mappedSustain, bucket.tone) || HasOto(sustainData.sustain, bucket.tone)) { + int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); + madePhonemes.Add(new Phoneme { + phoneme = sustainData.sustain, + position = basePhoneme.position + offsetTicks, + index = globalPhonemeIndex + currentSyllablePhonemeCount + }); + currentSyllablePhonemeCount++; + } + } + } + phonemes.AddRange(madePhonemes); + globalPhonemeIndex += currentSyllablePhonemeCount; + } + + var phonemesArray = phonemes.ToArray(); + var finalPhonemes = AssignAllAffixes(phonemesArray.ToList(), notes, prevNeighbours, workingAttributes); return new Result() { - phonemes = AssignAllAffixes(phonemes, notes, prevNeighbours) + phonemes = finalPhonemes }; } - protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { + protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs, List dynamicAttributes = null) { int noteIndex = 0; for (int i = 0; i < phonemes.Count; i++) { - var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; - string alt = (attr.alternate ?? GetParentAlternate())?.ToString() ?? string.Empty; + var attr = dynamicAttributes?.FirstOrDefault(a => a.index == i) + ?? notes[0].phonemeAttributes?.FirstOrDefault(a => a.index == i) + ?? default; + + var phoneme = phonemes[i]; + + int? altValue = attr.alternate ?? GetParentAlternate(); + string alt = altValue?.ToString(); + + if (string.IsNullOrEmpty(alt) && phoneme.expressions != null) { + var altExpr = phoneme.expressions.FirstOrDefault(e => e.abbr == "alt"); + if (altExpr.abbr == "alt" && altExpr.value > 0) { + altValue = (int)altExpr.value; + alt = altValue.ToString(); + } + } + alt ??= string.Empty; + string color = attr.voiceColor ?? GetParentVoiceColor(); int toneShift = attr.toneShift ?? GetParentToneShift(); - var phoneme = phonemes[i]; + while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { noteIndex++; } - var noteStartPosition = notes[noteIndex].position - notes[0].position; - int tone = (prevs != null && prevs.Length > 0 && phoneme.position < noteStartPosition) ? - prevs.Last().tone : (noteIndex > 0 && phoneme.position < noteStartPosition) ? - notes[noteIndex - 1].tone : notes[noteIndex].tone; + var noteStartPosition = notes[noteIndex].position - notes[0].position; + int tone; + if (phoneme.position < noteStartPosition) { + tone = (noteIndex > 0) ? notes[noteIndex - 1].tone : + (prevs != null && prevs.Length > 0) ? prevs.Last().tone : + notes[noteIndex].tone; + } else { + tone = notes[noteIndex].tone; + } + var validatedAlias = phoneme.phoneme; if (validatedAlias != null) { validatedAlias = ValidateAliasIfNeeded(validatedAlias, tone + toneShift); - validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + string mapped = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + if (!string.IsNullOrEmpty(alt) && alt != "0" && mapped == validatedAlias) { + if (singer.TryGetMappedOto($"{validatedAlias}{alt}", tone + toneShift, color, out var altOto)) { + mapped = altOto.Alias; + } + } + + phoneme.phoneme = mapped; - phoneme.phoneme = validatedAlias; + // Write alternate into expressions so the UI slider updates + if (altValue.HasValue && altValue.Value > 0) { + var exprList = phoneme.expressions != null + ? new List(phoneme.expressions) + : new List(); + + exprList.RemoveAll(e => e.abbr == "alt"); + exprList.Add(new PhonemeExpression { abbr = "alt", value = altValue.Value }); + phoneme.expressions = exprList; + } } else { phoneme.phoneme = null; phoneme.position = 0; @@ -255,9 +371,49 @@ private Result HandleError() { }; } + protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = + new YamlDotNet.Serialization.DeserializerBuilder() + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + private static readonly System.Collections.Concurrent.ConcurrentDictionary YamlCache = new(); + + private static string ReadVersionFast(string filePath) { + try { + using var reader = new StreamReader(filePath, Encoding.UTF8); + string line; + while ((line = reader.ReadLine()) != null) { + var trimmed = line.Trim(); + if (trimmed.StartsWith("version:", StringComparison.OrdinalIgnoreCase)) { + var parts = trimmed.Split(new[] { ':' }, 2); + if (parts.Length > 1) { + return parts[1].Trim().Trim('"', '\''); + } + } + } + } catch { + // Fall back if file reading fails + } + return string.Empty; + } + + private static YAMLData LoadYamlCached(string filePath) { + var lastWrite = File.GetLastWriteTimeUtc(filePath); + if (YamlCache.TryGetValue(filePath, out var cached) && cached.lastModified == lastWrite) { + return cached.data; + } + + using var reader = new StreamReader(filePath, Encoding.UTF8); + var parsed = TolerantDeserializer.Deserialize(reader); + YamlCache[filePath] = (lastWrite, parsed); + return parsed; + } + public override void SetSinger(USinger singer) { if (this.singer != singer) { this.singer = singer; + dictionaries.Clear(); if (this.singer == null || !this.singer.Loaded) { return; @@ -284,118 +440,256 @@ public override void SetSinger(USinger singer) { return; } - string file = null; - if (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) { - file = Path.Combine(singer.Location, YamlFileName); - } else if (!string.IsNullOrEmpty(PluginDir)) { - file = Path.Combine(PluginDir, YamlFileName); - } + // file paths + string globalFile = Path.Combine(PluginDir, YamlFileName); + string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) + ? Path.Combine(singer.Location, YamlFileName) + : null; + + // Local helper function to update and backup YAML files safely + void UpdateYamlIfNeeded(string filePath, bool isGlobal) { + if (string.IsNullOrEmpty(filePath)) return; - if (!string.IsNullOrEmpty(file)) { bool shouldWriteTemplate = false; bool shouldBackupOldFile = false; + string currentVersion = "unknown"; - if (File.Exists(file)) { + if (File.Exists(filePath)) { if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { try { - var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); - string currentVersion = checkData?.version?.Trim() ?? ""; + currentVersion = ReadVersionFast(filePath); - if (string.IsNullOrEmpty(currentVersion) || currentVersion != YamlVersion) { + // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion + if (string.IsNullOrEmpty(currentVersion)) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } else if (Version.TryParse(currentVersion, out Version currV) && + Version.TryParse(YamlVersion, out Version targetV)) { + if (currV < targetV) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { + // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") shouldWriteTemplate = true; shouldBackupOldFile = true; } } catch (Exception ex) { - Log.Error(ex, $"Failed to read version from '{file}'. Backing up and resetting to template..."); - shouldWriteTemplate = true; - shouldBackupOldFile = true; + Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); + return; } } - } else if (YamlTemplate != null) { + } else if (isGlobal && YamlTemplate != null) { shouldWriteTemplate = true; } - if (shouldBackupOldFile && File.Exists(file)) { + if (shouldBackupOldFile && File.Exists(filePath)) { try { - string backupFile = Path.Combine(Path.GetDirectoryName(file), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup{Path.GetExtension(YamlFileName)}"); + // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml + string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; + string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); + if (File.Exists(backupFile)) File.Delete(backupFile); - File.Move(file, backupFile); + File.Move(filePath, backupFile); Log.Information($"Old {YamlFileName} backed up to {backupFile}"); } catch (Exception e) { - Log.Error(e, $"Failed to back up {YamlFileName}"); + Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); + return; } } if (shouldWriteTemplate) { try { - File.WriteAllBytes(file, YamlTemplate); - Log.Information($"'{file}' created or updated to version {YamlVersion ?? "default"}"); + File.WriteAllBytes(filePath, YamlTemplate); + Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); } catch (Exception e) { - Log.Error(e, $"Failed to write template to {file}"); + Log.Error(e, $"Failed to write template to {filePath}"); } } + } - if (File.Exists(file)) { - try { - var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); - - if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); - if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); + UpdateYamlIfNeeded(globalFile, true); + UpdateYamlIfNeeded(singerFile, false); + + // add to parsing list (Global first, Singer second) + var filesToParse = new List(); + if (File.Exists(globalFile)) filesToParse.Add(globalFile); + if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); + + // backups of hardcoded defaults exist + if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); + if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); + if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); + if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); + if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); + + // reset live arrays/lists back to defaults before stacking + vowels = backupVowels; + consonants = backupConsonants; + tails = "-".Split(','); + + fricative = Array.Empty(); + aspirate = Array.Empty(); + semivowel = Array.Empty(); + liquid = Array.Empty(); + nasal = Array.Empty(); + stop = Array.Empty(); + tap = Array.Empty(); + affricate = Array.Empty(); + + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; + + diphthongTails.Clear(); + foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; + + diphthongSplits.Clear(); + foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; + + mergingReplacements.Clear(); + splittingReplacements.Clear(); + yamlFallbacks.Clear(); + PhonemeOverrides.Clear(); + if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); + vowelSustains.Clear(); + foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; + + // parse the files sequentially (Singer configs seamlessly overwrite global configs) + foreach (var file in filesToParse) { + try { + var data = LoadYamlCached(file); + + if (data.symbols != null && data.symbols.Length > 0) { + var symbolLookup = data.symbols + .Where(s => !string.IsNullOrEmpty(s.symbol) && !string.IsNullOrEmpty(s.type)) + .ToLookup(s => s.type, s => s.symbol); - var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); - vowels = backupVowels.Concat(yamlVowels).Distinct().ToArray(); + var yamlVowels = symbolLookup["vowel"].Concat(symbolLookup["diphthong"]).ToArray(); + vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); - tails = (tails ?? Array.Empty()).Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); - - fricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - aspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - semivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - liquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - nasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - stop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - tap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - affricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - - var yamlConsonants = fricative.Concat(aspirate).Concat(semivowel).Concat(liquid).Concat(nasal).Concat(stop).Concat(tap).Concat(affricate).ToArray(); - consonants = backupConsonants.Concat(yamlConsonants).Distinct().ToArray(); - - PhonemeOverrides = data.timings?.ToDictionary(t => t.symbol, t => t.value) ?? new Dictionary(); - if (backupDictionaryReplacements == null) { - backupDictionaryReplacements = new Dictionary(dictionaryReplacements); + var yamlTails = symbolLookup["tail"].ToArray(); + tails = yamlTails.Concat(tails).Distinct().ToArray(); + + var yFricative = symbolLookup["fricative"].ToArray(); + fricative = yFricative.Concat(fricative).Distinct().ToArray(); + + var yAspirate = symbolLookup["aspirate"].ToArray(); + aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); + + var ySemivowel = symbolLookup["semivowel"].ToArray(); + semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); + + var yLiquid = symbolLookup["liquid"].ToArray(); + liquid = yLiquid.Concat(liquid).Distinct().ToArray(); + + var yNasal = symbolLookup["nasal"].ToArray(); + nasal = yNasal.Concat(nasal).Distinct().ToArray(); + + var yStop = symbolLookup["stop"].ToArray(); + stop = yStop.Concat(stop).Distinct().ToArray(); + + var yTap = symbolLookup["tap"].ToArray(); + tap = yTap.Concat(tap).Distinct().ToArray(); + + var yAffricate = symbolLookup["affricate"].ToArray(); + affricate = yAffricate.Concat(affricate).Distinct().ToArray(); + + var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) + .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); + consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); + + // DIPHTHONG AUTO-TAIL DETECTION + var yamlDiphthongs = symbolLookup["diphthong"].Distinct().ToArray(); + var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); + + foreach (var d in yamlDiphthongs) { + if (!diphthongSplits.ContainsKey(d)) { + foreach (var tail in dynamicTails) { + if (d.EndsWith(tail) && d != tail) { + diphthongTails[d] = tail; + break; + } + } + } } - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) { - dictionaryReplacements[kvp.Key] = kvp.Value; + } + + if (data?.isglides != null) enableGlides = data.isglides.Value; + + // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) + if (data?.timings != null) { + foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; + } + + if (data?.replacements != null) { + var localMerge = new List(); + var localSplit = new List(); + string GetFromKey(object fromObj) { + if (fromObj is string s) return s; + if (fromObj is System.Collections.IEnumerable e) { + return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); + } + return ""; } - mergingReplacements.Clear(); - splittingReplacements.Clear(); - - if (data?.replacements != null && data.replacements.Any()) { - foreach (var replacement in data.replacements) { - string ruleScope = string.IsNullOrEmpty(replacement.where) ? "inside" : replacement.where.ToLowerInvariant(); - if (replacement.from is IEnumerable fromList) { - string[] fromArray = fromList.Select(item => item.ToString()).ToArray(); - if (replacement.to is string toString) mergingReplacements.Add(new Replacement { from = fromArray, to = toString, where = ruleScope }); - else if (replacement.to is IEnumerable toList) splittingReplacements.Add(new Replacement { from = fromArray, to = toList.Select(item => item.ToString()).ToArray(), where = ruleScope }); - } else if (replacement.from is string fromString) { - if (replacement.to is string toString) dictionaryReplacements[fromString] = toString; - else if (replacement.to is IEnumerable toList) splittingReplacements.Add(new Replacement { from = fromString, to = toList.Select(item => item.ToString()).ToArray(), where = ruleScope }); - } + foreach (var rawReplacement in data.replacements) { + string fromKey = GetFromKey(rawReplacement.from); + mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + + if (rawReplacement.from is string fromStr) { + dictionaryReplacements.Remove(fromStr); + } + + List fromList = rawReplacement.FromList; + List toList = rawReplacement.ToList; + object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); + object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); + + var cleanReplacement = new Replacement { + from = parsedFrom, + to = parsedTo, + where = rawReplacement.where + }; + + if (parsedFrom is string) { + localSplit.Add(cleanReplacement); + } else { + localMerge.Add(cleanReplacement); } } + mergingReplacements.InsertRange(0, localMerge); + splittingReplacements.InsertRange(0, localSplit); + } - if (data?.fallbacks != null) { - yamlFallbacks.Clear(); - foreach (var df in data.fallbacks) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - yamlFallbacks[df.from] = df.to; - } + if (data?.fallbacks != null) { + var localFallbacks = new List(); + foreach (var df in data.fallbacks) { + if (df.FromList.Count > 0 && df.ToList.Count > 0) { + localFallbacks.Add(df); + } + } + yamlFallbacks.InsertRange(0, localFallbacks); + } + + if (data?.diphthongs != null) { + foreach (var d in data.diphthongs) { + if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { + diphthongTails[d.from] = d.to; + } + } + } + + if (data?.vowelsustains != null) { + foreach (var v in data.vowelsustains) { + if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { + vowelSustains[v.symbol] = (v.sustain, v.offset); } } - } catch (Exception ex) { - Log.Error($"Failed to parse {YamlFileName}: {ex.Message}"); } + + } catch (Exception ex) { + Log.Error($"Failed to parse {file}: {ex.Message}"); } } @@ -419,6 +713,20 @@ public override void SetSinger(USinger singer) { private readonly string[] wordSeparators = new[] { " ", "_" }; private readonly string[] wordSeparator = new[] { " " }; + /// + /// A tracker to identify which phonemes were marked as glides dynamically. + /// + protected HashSet runtimeGlides = new HashSet(); + + /// + /// Flag a specific generated string as a glide during your ProcessSyllable / ProcessEnding loops. + /// + protected void glides(string alias) { + runtimeGlides.Add(alias); + } + + protected bool enableGlides = true; + /// /// Returns list of vowels /// @@ -462,6 +770,45 @@ protected virtual void Init() { } /// protected virtual string GetDictionaryName() { return null; } + /// + /// Greedy tokenization: identifies longest matching consonants/vowels first (e.g., "kwh", "sh", "dx") + /// and counts multi-character consonants as 1 single element, falling back to 1-character tokens. + /// + protected virtual List TokenizePhonemes(string raw) { + var tokens = new List(); + if (string.IsNullOrEmpty(raw)) return tokens; + + var knownVowels = GetVowels() ?? Array.Empty(); + var knownConsonants = (consonants != null && consonants.Length > 0) ? consonants : (GetConsonants() ?? Array.Empty()); + + var allKnown = knownVowels + .Concat(knownConsonants) + .Concat(tails ?? Array.Empty()) + .Where(s => !string.IsNullOrEmpty(s)) + .Distinct() + .OrderByDescending(s => s.Length) + .ToArray(); + + int i = 0; + while (i < raw.Length) { + bool matched = false; + foreach (var symbol in allKnown) { + if (raw.IndexOf(symbol, i, StringComparison.Ordinal) == i) { + tokens.Add(symbol); + i += symbol.Length; + matched = true; + break; + } + } + if (!matched) { + // Fallback to single character + tokens.Add(raw[i].ToString()); + i++; + } + } + return tokens; + } + /// /// extracts array of phoneme symbols from note. Override for procedural dictionary or something /// reads from dictionary if provided @@ -470,9 +817,18 @@ protected virtual void Init() { } /// protected virtual string[] GetSymbols(Note note) { string[] getSymbolsRaw(string lyrics) { - if (lyrics == null) { + if (string.IsNullOrEmpty(lyrics)) { return new string[0]; - } else return lyrics.Split(" "); + } + if (lyrics.Contains(" ")) { + var parts = lyrics.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var resultList = new List(); + foreach (var part in parts) { + resultList.AddRange(TokenizePhonemes(part)); + } + return resultList.ToArray(); + } + return TokenizePhonemes(lyrics).ToArray(); } if (tails.Contains(note.lyric)) { @@ -488,7 +844,6 @@ string[] getSymbolsRaw(string lyrics) { foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { var subResult = dictionary.Query(subword); if (subResult == null) { - Log.Warning($"Subword '{subword}' from word '{note.lyric}' can't be found in the dictionary"); subResult = HandleWordNotFound(note); if (subResult == null) { return null; @@ -511,6 +866,16 @@ string[] getSymbolsRaw(string lyrics) { } } + /// + /// Defines whether a consonant (like a liquid or semi-vowel etc) should be placed ON the note (anchor) + /// instead of pushing backward. Will return true if dynamically flagged using glides() or TryAddPhoneme(). + /// + protected virtual bool IsGlide(string alias) { + return runtimeGlides.Contains(alias) && enableGlides; + } + + protected virtual bool NoGap => true; + /// /// Instead of changing symbols in cmudict itself for each reclist, /// you may leave it be and provide symbol replacements with this method. @@ -521,7 +886,11 @@ protected virtual Dictionary GetDictionaryPhonemesReplacement() } private string[] backupVowels = null; private string[] backupConsonants = null; + private Dictionary backupDiphthongTails = null; + private Dictionary backupDiphthongSplits = null; private Dictionary backupDictionaryReplacements = null; + protected Dictionary vowelSustains = new Dictionary(); + private Dictionary backupVowelSustains = null; /// /// separates symbols to syllables, without an ending. @@ -529,7 +898,7 @@ protected virtual Dictionary GetDictionaryPhonemesReplacement() /// /// /// - protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { + protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding, Ending? nextEnding = null) { (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); if (symbols == null || vowelIds == null || notes == null) { return null; @@ -542,13 +911,12 @@ protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding var syllables = new Syllable[vowelIds.Length]; - // Making the first syllable + // Syllable 0 initialization if (prevEnding.HasValue) { var prevEndingValue = prevEnding.Value; var beginningCc = prevEndingValue.cc.ToList(); beginningCc.AddRange(symbols.Take(firstVowelId)); - // If we had a prev neighbour ending, let's take info from it syllables[0] = new Syllable() { prevV = prevEndingValue.prevV, cc = beginningCc.ToArray(), @@ -562,7 +930,6 @@ protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding prevWordConsonantsCount = prevEndingValue.cc.Count() }; } else { - // there is only empty space before us syllables[0] = new Syllable() { prevV = "", cc = symbols.Take(firstVowelId).ToArray(), @@ -576,7 +943,7 @@ protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding }; } - // normal syllables after the first one + // Subsequent syllables var noteI = 1; var ccs = new List(); var position = 0; @@ -596,13 +963,27 @@ protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding position = position, vowelTone = notes[noteI].tone, vowelAttr = notes[noteI].phonemeAttributes, - canAliasBeExtended = true // for all not-first notes is allowed + canAliasBeExtended = true }; ccs = new List(); noteI++; } } + // Assign NextVowel (nextV) and NextCC (nextCc) + for (int i = 0; i < syllables.Length; i++) { + if (i < syllables.Length - 1) { + syllables[i].nextV = syllables[i + 1].v; + syllables[i].nextCc = syllables[i + 1].cc ?? Array.Empty(); + } else if (nextEnding.HasValue) { + syllables[i].nextV = nextEnding.Value.prevV; + syllables[i].nextCc = nextEnding.Value.cc ?? Array.Empty(); + } else { + syllables[i].nextV = string.Empty; + syllables[i].nextCc = Array.Empty(); + } + } + return syllables; } @@ -722,13 +1103,72 @@ protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { return phonemesString.Split(' '); } + protected virtual string ReplacePhoneme(string phoneme, int tone) { + if (string.IsNullOrEmpty(phoneme)) return ""; + if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { + return replaced; + } + return phoneme; + } + /// - /// use to validate alias + /// Validates formatted aliases. + /// If the alias is missing in OTO, it applies character/phoneme substring replacements from YAML fallbacks. /// - /// - /// - protected virtual string ValidateAlias(string alias) { - return alias; + protected virtual string ValidateAlias(string alias, int tone = 0) { + if (string.IsNullOrEmpty(alias)) return alias; + if (HasOto(alias, tone)) return alias; + + var singleRules = yamlFallbacks + .Where(r => r.FromList.Count == 1) + .OrderByDescending(r => r.FromList[0].Length) + .ToList(); + + // Exact direct substitution check + // Try replacing ONLY the exact missing token first (e.g. "x uw" -> "sh uw") + foreach (var rule in singleRules) { + string fromStr = rule.FromList[0].Trim('(', ')'); + if (alias.Contains(fromStr)) { + foreach (var target in rule.ToList) { + string candidate = alias.Replace(fromStr, target); + if (HasOto(candidate, tone)) { + return candidate; + } + } + } + } + + // Multi-rule cascaded fallback (only if Stage 1 failed completely) + string cascadedAlias = alias; + bool changed = false; + + foreach (var rule in singleRules) { + string fromStr = rule.FromList[0].Trim('(', ')'); + if (cascadedAlias.Contains(fromStr)) { + foreach (var target in rule.ToList) { + string candidate = cascadedAlias.Replace(fromStr, target); + if (HasOto(candidate, tone)) { + return candidate; + } + } + if (rule.ToList.Count > 0) { + cascadedAlias = cascadedAlias.Replace(fromStr, rule.ToList[0]); + changed = true; + } + } + } + + if (changed && HasOto(cascadedAlias, tone)) { + return cascadedAlias; + } + + var legacyFallbacks = GetAliasesFallback(); + if (legacyFallbacks != null && legacyFallbacks.TryGetValue(alias, out var legacyTarget)) { + if (HasOto(legacyTarget, tone)) return legacyTarget; + return legacyTarget; + } + + return changed ? cascadedAlias : alias; } /// @@ -745,6 +1185,50 @@ protected double GetTransitionBasicLengthMsByConstant() { return TransitionBasicLengthMs * GetTempoNoteLengthFactor(); } + protected virtual double GetTransitionMultiplier(string alias) { + if (alias != null && PhonemeOverrides != null && PhonemeOverrides.TryGetValue(alias, out double overrideRatio)) { + return overrideRatio; + } + return 1.0; + } + + /// + /// Uses Preutterance length + /// + protected virtual double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + return GetTransitionBasicLengthMs(alias); + } + + /// + /// OTO HELPER: Calculates transition length based on the mapped Oto's Preutterance. + /// + protected double GetTransitionBasicLengthMsByOto(string alias, int tone = 0, PhonemeAttributes attr = default) { + if (string.IsNullOrEmpty(alias)) return GetTransitionBasicLengthMsByConstant(); + + string color = attr.voiceColor ?? string.Empty; + string alt = attr.alternate?.ToString() ?? string.Empty; + int toneShift = attr.toneShift ?? 0; + + var validatedAlias = ValidateAliasIfNeeded(alias, tone + toneShift); + var mappedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + // Direct OTO lookup fallback for non-subbank numeric alternates + if (!string.IsNullOrEmpty(alt) && alt != "0" && mappedAlias == validatedAlias) { + if (singer.TryGetMappedOto($"{validatedAlias}{alt}", tone + toneShift, color, out var altOto)) { + mappedAlias = altOto.Alias; + } + } + + if (singer.TryGetMappedOto(mappedAlias, tone + toneShift, out var oto)) { + if (oto.Overlap < 0) { + return oto.Preutter - oto.Overlap; + } + return oto.Preutter; + } + + return GetTransitionBasicLengthMsByConstant(); + } + /// /// a note length modifier, from 1 to 0.3. Used to make transition notes shorter on high tempo /// @@ -753,22 +1237,68 @@ protected double GetTempoNoteLengthFactor() { return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; } + protected virtual IG2p[] GetBaseG2ps() { + return Array.Empty(); + } + protected virtual IG2p LoadBaseDictionary() { - var dictionaryName = GetDictionaryName(); - var filename = Path.Combine(DictionariesPath, dictionaryName); - var dictionaryText = File.ReadAllText(filename); - var builder = G2pDictionary.NewBuilder(); - var vowels = GetVowels(); - foreach (var vowel in vowels) { - builder.AddSymbol(vowel, true); + var g2ps = new List(); + + // Native YAML Dictionary Logic + if (!string.IsNullOrEmpty(YamlFileName)) { + string path = Path.Combine(PluginDir, YamlFileName); + + // Write template if missing + if (!File.Exists(path) && YamlTemplate != null) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + + // Load dictionary from Singer Folder (Highest Priority) + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + + // Load dictionary from Plugin Folder (Fallback Priority) + if (File.Exists(path)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {path}"); + } + } + } + // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) + else { + var dictionaryName = GetDictionaryName(); + if (!string.IsNullOrEmpty(dictionaryName)) { + var filename = Path.Combine(DictionariesPath, dictionaryName); + if (File.Exists(filename)) { + var dictionaryText = File.ReadAllText(filename); + var builder = G2pDictionary.NewBuilder(); + foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); + foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); + builder.AddEntry("a", new string[] { "a" }); + ParseDictionary(dictionaryText, builder); + g2ps.Add(builder.Build()); + } + } } - var consonants = GetConsonants(); - foreach (var consonant in consonants) { - builder.AddSymbol(consonant, false); + + // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) + var childG2ps = GetBaseG2ps(); + if (childG2ps != null && childG2ps.Any()) { + g2ps.AddRange(childG2ps); } - builder.AddEntry("a", new string[] { "a" }); - ParseDictionary(dictionaryText, builder); - return builder.Build(); + + return new G2pFallbacks(g2ps.ToArray()); } /// @@ -799,6 +1329,30 @@ protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Buil #region helpers + /// + /// Child phonemizers can override this hook to dynamically populate attributes (alts, vel, etc.) + /// before timing and layout calculations occur. + /// + protected virtual void SyncAttributes(Note[] notes, List phonemeSymbols, int startIndex, List attrList) { + for (int i = 0; i < phonemeSymbols.Count; i++) { + int globalIdx = startIndex + i; + int existingIdx = attrList.FindIndex(a => a.index == globalIdx); + var attr = existingIdx >= 0 ? attrList[existingIdx] : new PhonemeAttributes { index = globalIdx }; + + attr = GetDynamicPhonemeAttributes(phonemeSymbols[i], globalIdx, attr, notes); + + if (existingIdx >= 0) attrList[existingIdx] = attr; + else attrList.Add(attr); + } + } + + /// + /// Hook for child phonemizers to compute dynamic attributes natively per phoneme alias. + /// + protected virtual PhonemeAttributes GetDynamicPhonemeAttributes(string alias, int index, PhonemeAttributes currentAttr, Note[] notes) { + return currentAttr; + } + /// /// May be used if you have different logic for short and long notes /// @@ -838,6 +1392,20 @@ protected bool TryAddPhoneme(List sourcePhonemes, int tone, params strin return false; } + /// + /// Appends a phoneme and optionally marks it as a glide simultaneously. + /// + protected bool TryAddPhoneme(List sourcePhonemes, int tone, bool isGlide, params string[] targetPhonemes) { + foreach (var phoneme in targetPhonemes) { + if (HasOto(phoneme, tone)) { + sourcePhonemes.Add(phoneme); + if (isGlide) glides(phoneme); + return true; + } + } + return false; + } + /// /// if true, you can put phoneme as null so the previous alias will be extended /// @@ -891,19 +1459,26 @@ protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { protected Dictionary dictionaryReplacements = new Dictionary(); protected Dictionary PhonemeOverrides = new Dictionary(); - protected Dictionary yamlFallbacks = new Dictionary(); + protected List yamlFallbacks = new List(); protected List consExceptions = new List(); + protected Dictionary diphthongTails = new Dictionary(); + protected Dictionary diphthongSplits = new Dictionary(); + public class YAMLData { public string version { get; set; } + public bool? isglides { get; set; } public SymbolData[] symbols { get; set; } = Array.Empty(); public Replacement[] replacements { get; set; } = Array.Empty(); - public Fallbacks[] fallbacks { get; set; } = Array.Empty(); + public Replacement[] fallbacks { get; set; } = Array.Empty(); public Timings[] timings { get; set; } = Array.Empty(); + public DiphthongData[] diphthongs { get; set; } = Array.Empty(); + public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } - public struct Fallbacks { public string from { get; set; } public string to { get; set; } } public struct Timings { public string symbol { get; set; } public double value { get; set; } } + public struct DiphthongData { public string from { get; set; } public string to { get; set; } } + public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } } public class Replacement { @@ -914,7 +1489,7 @@ public class Replacement { public List FromList { get { if (from is string s) return new List { s }; - if (from is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); return new List(); } } @@ -922,7 +1497,7 @@ public List FromList { public List ToList { get { if (to is string s) return new List { s }; - if (to is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); return new List(); } } @@ -932,17 +1507,21 @@ public List ToList { protected List splittingReplacements = new List(); protected virtual bool IsGroupKeyword(string rulePhoneme) { - string baseGroup = rulePhoneme.Split(new[] { '!', '=', '+' })[0]; + // Trim parentheses so "(vowel)" evaluates identically to "vowel" + string cleanRule = rulePhoneme.Trim('(', ')'); + string baseGroup = cleanRule.Split(new[] { '!', '=', '&' })[0]; return new[] { "vowel", "vowels", "consonant", "consonants", "affricate", "fricative", "aspirate", "semivowel", "liquid", "nasal", "stop", "tap" }.Contains(baseGroup); } protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { - string baseGroup = rulePhoneme.Split(new[] { '!', '=', '+' })[0]; - if (rulePhoneme.Contains("+")) { - string added = rulePhoneme.Substring(rulePhoneme.IndexOf('+') + 1).Split(new[] { '!', '=' })[0]; - // If it matches another group name, or a literal letter, it passes + string cleanRule = rulePhoneme.Trim('(', ')'); + string baseGroup = cleanRule.Split(new[] { '!', '=', '&' })[0]; + + // Replaced '+' with '&' for group addition + if (cleanRule.Contains("&")) { + string added = cleanRule.Substring(cleanRule.IndexOf('&') + 1).Split(new[] { '!', '=' })[0]; foreach (string inc in added.Split(',')) { if (IsGroupKeyword(inc) ? IsGroupMatch(inc, actualPhoneme) : inc == actualPhoneme) { return true; @@ -950,11 +1529,10 @@ protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { } } - // BASE GROUP: If it wasn't an addition, it must belong to the base group. bool inBaseGroup = false; switch (baseGroup) { case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; - case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; + case "consonant": case "consonants": inBaseGroup = (consonants.Length > 0 ? consonants : GetConsonants()).Contains(actualPhoneme); break; case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; @@ -967,15 +1545,13 @@ protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { if (!inBaseGroup) return false; - // EXCLUSIONS (!): Reject if it's in the excluded list. - if (rulePhoneme.Contains("!")) { - string excluded = rulePhoneme.Substring(rulePhoneme.IndexOf('!') + 1).Split(new[] { '=', '+' })[0]; + if (cleanRule.Contains("!")) { + string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; if (excluded.Split(',').Contains(actualPhoneme)) return false; } - // RESTRICTIONS (=): Reject if an equals list exists, and the phoneme isn't in it. - if (rulePhoneme.Contains("=")) { - string restricted = rulePhoneme.Substring(rulePhoneme.IndexOf('=') + 1).Split(new[] { '!', '+' })[0]; + if (cleanRule.Contains("=")) { + string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; if (!restricted.Split(',').Contains(actualPhoneme)) return false; } @@ -988,35 +1564,40 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo List finalPhonemes = new List(); int idx = 0; + // Sort validRules by the length of the matching array descending. + // This guarantees multi-phoneme matches evaluate BEFORE 1:1 matches. var validRules = mergingReplacements.Concat(splittingReplacements) - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")) + .OrderByDescending(r => r.FromList.Count) + .ThenByDescending(r => r.FromList.Sum(s => s.Length)) // Prioritize longer strings + .ToList(); var validSplits = splittingReplacements - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")) + .OrderByDescending(r => r.FromList.Sum(s => s.Length)) // Sort fallback splits too + .ToList(); while (idx < inputPhonemes.Count) { bool replaced = false; foreach (var rule in validRules) { - string[] fromArray = null; - if (rule.from is IList fromList) { - fromArray = fromList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.from is string[] strArr) { - fromArray = strArr; - } - - if (fromArray != null && fromArray.Length > 0 && idx + fromArray.Length <= inputPhonemes.Count) { + List fromArray = rule.FromList; + + if (fromArray != null && fromArray.Count > 0 && idx + fromArray.Count <= inputPhonemes.Count) { bool match = true; - var captures = new Dictionary>(); + var captures = new Dictionary>(); - for (int j = 0; j < fromArray.Length; j++) { + for (int j = 0; j < fromArray.Count; j++) { string rulePh = fromArray[j]; string actualPh = inputPhonemes[idx + j]; - if (IsGroupKeyword(rulePh)) { + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh)) { if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(rulePh)) captures[rulePh] = new Queue(); - captures[rulePh].Enqueue(actualPh); + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); } else { match = false; break; } @@ -1026,56 +1607,112 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo } if (match) { - string[] toArray = null; - if (rule.to is IList toList) { - toArray = toList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.to is string[] strArr) { - toArray = strArr; - } else if (rule.to is string toStr) { - toArray = new string[] { toStr }; - } + List toArray = rule.ToList; - if (toArray != null) { + if (toArray != null && toArray.Count > 0) { + var captureIndices = new Dictionary(); + foreach (string toPh in toArray) { - finalPhonemes.Add(IsGroupKeyword(toPh) && captures.ContainsKey(toPh) && captures[toPh].Count > 0 ? captures[toPh].Dequeue() : toPh); + // Split by + for concatenation + string[] parts = toPh.Split('+'); + string[] cleanParts = new string[parts.Length]; + string baseGroupTo = null; + + for (int k = 0; k < parts.Length; k++) { + // Strip parenthesis to find the base group cleanly + string partNoParens = parts[k].Trim('(', ')'); + int cutoff = partNoParens.IndexOfAny(new[] { '!', '=', '&' }); + string potentialGroup = cutoff >= 0 ? partNoParens.Substring(0, cutoff) : partNoParens; + + if (baseGroupTo == null && IsGroupKeyword(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; // Store just the base group name + } else { + cleanParts[k] = partNoParens; // Store literals + } + } + + if (baseGroupTo != null && captures.ContainsKey(baseGroupTo) && captures[baseGroupTo].Count > 0) { + if (!captureIndices.ContainsKey(baseGroupTo)) captureIndices[baseGroupTo] = 0; + int cIdx = captureIndices[baseGroupTo]; + if (cIdx >= captures[baseGroupTo].Count) cIdx = captures[baseGroupTo].Count - 1; + + string capturedPhoneme = captures[baseGroupTo][cIdx]; + + string reconstructed = ""; + for (int k = 0; k < cleanParts.Length; k++) { + if (cleanParts[k] == baseGroupTo) { + reconstructed += capturedPhoneme; + } else { + reconstructed += cleanParts[k]; + } + } + finalPhonemes.Add(reconstructed); + captureIndices[baseGroupTo]++; + } else { + finalPhonemes.Add(string.Join("", cleanParts)); + } } } - idx += fromArray.Length; + idx += fromArray.Count; replaced = true; break; } } } + // Fallback for single-phoneme splitting rules if (!replaced && validSplits.Any()) { string currentPhoneme = inputPhonemes[idx]; bool singleReplaced = false; foreach (var rule in validSplits) { - if (rule.from is IList || rule.from is string[]) continue; + List fromArray = rule.FromList; + if (fromArray == null || fromArray.Count != 1) continue; - string rulePh = rule.from?.ToString(); - if (rulePh == null) continue; + string rulePh = fromArray[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - if (IsGroupKeyword(rulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { + if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { - string[] toArray = null; - if (rule.to is IList toList) { - toArray = toList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.to is string[] strArr) { - toArray = strArr; - } + List toArray = rule.ToList; - if (toArray != null) { + if (toArray != null && toArray.Count > 0) { foreach(string toPh in toArray) { - finalPhonemes.Add(toPh == rulePh ? currentPhoneme : toPh); + string[] parts = toPh.Split('+'); + string[] cleanParts = new string[parts.Length]; + string baseGroupTo = null; + + for (int k = 0; k < parts.Length; k++) { + string partNoParens = parts[k].Trim('(', ')'); + int cutoff = partNoParens.IndexOfAny(new[] { '!', '=', '&' }); + string potentialGroup = cutoff >= 0 ? partNoParens.Substring(0, cutoff) : partNoParens; + + if (baseGroupTo == null && IsGroupKeyword(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; + } else { + cleanParts[k] = partNoParens; + } + } + + if (baseGroupTo != null) { + string reconstructed = ""; + for (int k = 0; k < cleanParts.Length; k++) { + if (cleanParts[k] == baseGroupTo) { + reconstructed += currentPhoneme; + } else { + reconstructed += cleanParts[k]; + } + } + finalPhonemes.Add(reconstructed); + } else { + finalPhonemes.Add(string.Join("", cleanParts)); + } } singleReplaced = true; break; - } else if (rule.to is string toStr) { - finalPhonemes.Add(toStr == rulePh ? currentPhoneme : toStr); - singleReplaced = true; - break; } } } @@ -1096,11 +1733,12 @@ private Syllable ApplyBoundaryReplacements(Syllable syllable) { bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); bool hasV = !string.IsNullOrEmpty(syllable.v); - if (hasPrevV) currentPhonemes.Add(syllable.prevV); + currentPhonemes.Add(hasPrevV ? syllable.prevV : "null"); + if (syllable.cc != null) currentPhonemes.AddRange(syllable.cc); if (hasV) currentPhonemes.Add(syllable.v); - bool isBoundary = hasPrevV && syllable.position == 0; + bool isBoundary = (hasPrevV && syllable.position == 0) || !hasPrevV; List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); string newPrevV = ""; @@ -1108,8 +1746,13 @@ private Syllable ApplyBoundaryReplacements(Syllable syllable) { List newCc = new List(); if (finalPhonemes.Count > 0) { - if (hasPrevV) { - newPrevV = finalPhonemes[0]; + string firstPh = finalPhonemes[0]; + + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; finalPhonemes.RemoveAt(0); } if (hasV && finalPhonemes.Count > 0) { @@ -1141,26 +1784,49 @@ private Ending ApplyBoundaryReplacements(Ending ending) { if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); - - if (hasPrevV) currentPhonemes.Add(ending.prevV); + currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); + if (ending.cc != null) currentPhonemes.AddRange(ending.cc); + + bool hasTail = ending.HasTail; + currentPhonemes.Add(hasTail ? ending.tail : "null"); List finalPhonemes = ApplyReplacements(currentPhonemes, true); string newPrevV = ""; + string newTail = ""; List newCc = new List(); if (finalPhonemes.Count > 0) { - if (hasPrevV) { - newPrevV = finalPhonemes[0]; - finalPhonemes.RemoveAt(0); + // The first item is always the previous vowel (or empty if null) + string firstPh = finalPhonemes[0]; + if (firstPh == "null") { + newPrevV = ""; + } else { + newPrevV = firstPh; } - newCc.AddRange(finalPhonemes); + finalPhonemes.RemoveAt(0); } + if (finalPhonemes.Count > 0) { + // The last item is always the tail (or empty if null) + string lastPh = finalPhonemes.Last(); + if (lastPh == "null") { + newTail = ""; + } else { + newTail = lastPh; + } + finalPhonemes.RemoveAt(finalPhonemes.Count - 1); + } + + newCc.AddRange(finalPhonemes); + ending.prevV = newPrevV; ending.cc = newCc.ToArray(); + ending.tail = newTail; + return ending; } @@ -1198,7 +1864,7 @@ private void ReadDictionary(string dictionaryName) { foreach (var vowel in GetVowels()) { phonemeSymbols[vowel] = true; } - foreach (var consonant in GetConsonants()) { + foreach (var consonant in (consonants.Length > 0 ? consonants : GetConsonants())) { phonemeSymbols[consonant] = false; } @@ -1207,8 +1873,6 @@ private void ReadDictionary(string dictionaryName) { foreach (var kvp in childDict) { safeDict[kvp.Key] = kvp.Value; - safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' - safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' } dictionaries[GetType()] = new G2pRemapper( @@ -1254,56 +1918,144 @@ private List ExtractVowels(string[] symbols) { } return vowelIds; } + + private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int position, bool isEnding, int tone = 0, PhonemeAttributes[] attributes = null, int globalStartIndex = 0) { + var phonemes = new Phoneme[phonemeSymbols.Count]; + + int[] trueLengths = new int[phonemeSymbols.Count]; + for (int i = 1; i < phonemeSymbols.Count; i++) { + var prevPhonemeI = phonemeSymbols.Count - i; + var currentPhonemeI = phonemeSymbols.Count - i - 1; + + var nextGlobalIndex = globalStartIndex + prevPhonemeI; + var nextPAttr = attributes?.FirstOrDefault(a => a.index == nextGlobalIndex) ?? default; + + string nextAlias = phonemeSymbols[prevPhonemeI]; + string currentAlias = phonemeSymbols[currentPhonemeI]; - private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int position, bool isEnding) { + double baseLengthMs; + double stretch = nextPAttr.consonantStretchRatio ?? 1.0; + + // Check if the alias has a YAML or Categorical multiplier + double overrideRatio = currentAlias != null ? GetTransitionMultiplier(currentAlias) : 1.0; + + if (overrideRatio != 1.0) { + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + baseLengthMs = GetTransitionBasicLengthMs(nextAlias, tone, nextPAttr); + } + + trueLengths[i] = MsToTick(baseLengthMs * stretch); + } + + // IsGlide + int anchorI = 0; + if (!isEnding) { + for (int i = 1; i < phonemeSymbols.Count; i++) { + var phonemeI = phonemeSymbols.Count - i - 1; + if (phonemeSymbols[phonemeI] != null && IsGlide(phonemeSymbols[phonemeI])) { + anchorI = i; + } else { + break; + } + } + } - var phonemes = new Phoneme[phonemeSymbols.Count]; for (var i = 0; i < phonemeSymbols.Count; i++) { var phonemeI = phonemeSymbols.Count - i - 1; - + var globalIndex = globalStartIndex + phonemeI; var validatedAlias = phonemeSymbols[phonemeI]; + var pAttr = attributes?.FirstOrDefault(a => a.index == globalIndex) ?? default; + if (validatedAlias != null) { - phonemes[phonemeI].phoneme = validatedAlias; - var transitionLengthTick = MsToTick(GetTransitionBasicLengthMs(phonemes[phonemeI].phoneme)); + var exprList = new List(); + if (pAttr.consonantStretchRatio.HasValue) { + float vel = (float)(100.0 - 100.0 * Math.Log2(pAttr.consonantStretchRatio.Value)); + exprList.Add(new PhonemeExpression { abbr = "vel", value = vel }); + } + if (pAttr.alternate.HasValue && pAttr.alternate.Value > 0) { + exprList.Add(new PhonemeExpression { abbr = "alt", value = pAttr.alternate.Value }); + } + + phonemes[phonemeI] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex, + expressions = exprList.Count > 0 ? exprList : null + }; + if (i == 0) { - if (!isEnding) { - transitionLengthTick = 0; + if (isEnding) { + double baseLengthMs; + double stretch = pAttr.consonantStretchRatio ?? 1.0; + + double overrideRatio = phonemes[phonemeI].phoneme != null ? GetTransitionMultiplier(phonemes[phonemeI].phoneme) : 1.0; + + if (overrideRatio != 1.0) { + // YAML Override active: Use the multiplier and bypass NoGap entirely + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); + } else { + // Default behavior + baseLengthMs = GetTransitionBasicLengthMsByOto(phonemes[phonemeI].phoneme, tone, pAttr); + + if (NoGap) { + // Snapped mode: Use a visible 50-tick anchor capped at 1/3 of the note + int targetTicks = 50; + int maxAllowed = containerLength / 3; + phonemes[phonemeI].position = System.Math.Min(targetTicks, maxAllowed); + } else { + // Natural mode: Use the full Preutterance + phonemes[phonemeI].position = MsToTick(baseLengthMs); + } + } } else { - transitionLengthTick *= 2; + int sum = 0; + for (int k = 1; k <= anchorI; k++) { + sum += trueLengths[k]; + } + phonemes[phonemeI].position = -sum; } + } else { + // VC transitions keep their full stretched length + phonemes[phonemeI].position = trueLengths[i]; } - // yet it's actually a length; will became position in ScalePhonemes - phonemes[phonemeI].position = transitionLengthTick; } else { - phonemes[phonemeI].phoneme = null; - phonemes[phonemeI].position = 0; + // Initialize empty slots properly to avoid null crashes + phonemes[phonemeI] = new Phoneme { + phoneme = null, + position = 0, + index = globalIndex + }; } } - - return ScalePhonemes(phonemes, position, isEnding ? phonemeSymbols.Count : phonemeSymbols.Count - 1, containerLength); + + return ScalePhonemes(phonemes, position, isEnding ? phonemeSymbols.Count - 1 : phonemeSymbols.Count - 1, containerLength); } private string ValidateAliasIfNeeded(string alias, int tone) { - if (HasOto(alias, tone)) { - return alias; - } - return ValidateAlias(alias); + return ValidateAlias(alias, tone); } private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { var offset = 0; - // reserved length for prev vowel, double length of a transition; - var containerSafeLengthTick = MsToTick(GetTransitionBasicLengthMsByConstant() * 2); var lengthModifier = 1.0; + if (containerLengthTick > 0) { var allTransitionsLengthTick = phonemes.Sum(n => n.position); - if (allTransitionsLengthTick + containerSafeLengthTick > containerLengthTick) { - lengthModifier = (double)containerLengthTick / (allTransitionsLengthTick + containerSafeLengthTick); + + // Instead of a fixed "Constant * 2", use a proportional limit. + // This allows transitions to occupy up to 80% of the note. + var maxAllowedConsonantTick = (int)(containerLengthTick * 0.8); + + if (allTransitionsLengthTick > maxAllowedConsonantTick) { + lengthModifier = (double)maxAllowedConsonantTick / allTransitionsLengthTick; } } for (var i = phonemes.Length - 1; i >= 0; i--) { - var finalLengthTick = (int)(phonemes[i].position * lengthModifier) / 5 * 5; + if (phonemes[i].phoneme == null) continue; + var finalLengthTick = (int)(phonemes[i].position * lengthModifier); phonemes[i].position = startPosition - finalLengthTick - offset; offset += finalLengthTick; } @@ -1313,4 +2065,4 @@ private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phone #endregion } -} +} \ No newline at end of file diff --git a/OpenUtau.Test/Plugins/DeVccvTest.cs b/OpenUtau.Test/Plugins/DeVccvTest.cs index 5b3f5d35d..9656dac85 100644 --- a/OpenUtau.Test/Plugins/DeVccvTest.cs +++ b/OpenUtau.Test/Plugins/DeVccvTest.cs @@ -22,7 +22,7 @@ protected override Phonemizer CreatePhonemizer() { [InlineData("de_vccv", new string[] { "Mond", "+", "+", "+", "Licht", "+" }, new string[] { "G3", "D3", "G3", "G3", "D3", "G3" }, - new string[] { "- moG3", "onG3", "nt -G3", "t lG3", "lID3", "ICG3", "Ct -G3" })] + new string[] { "- moG3", "onG3", "nt -G3", "t lG3", "lID3", "ICD3", "Ct -G3" })] public void PhonemizeTest(string singerName, string[] lyrics, string[] tones, string[] aliases) { RunPhonemizeTest(singerName, lyrics, RepeatString(lyrics.Length, ""), tones, RepeatString(lyrics.Length, ""), aliases); } diff --git a/OpenUtau.Test/Plugins/EnArpaPlusTest.cs b/OpenUtau.Test/Plugins/EnArpaPlusTest.cs index fdffd9bc7..f9f158633 100644 --- a/OpenUtau.Test/Plugins/EnArpaPlusTest.cs +++ b/OpenUtau.Test/Plugins/EnArpaPlusTest.cs @@ -15,12 +15,12 @@ protected override Phonemizer CreatePhonemizer() { new string[] { "good", "morning", }, new string[] { "A#3", "A#3" }, new string[] { "", "" }, - new string[] { "- g_C3", "g uh_C3", "uh d_C3", "d m_C3", "m ao_C3", "ao r_C3", "r n_C3", "n ih_C3", "ih ng_C3", "ng -_C3" })] + new string[] { "- g_C3", "g uh_C3", "uh d_C3", "d m_C3", "m ao1_C3", "ao r6_C3", "r n_C3", "n ih_C3", "ih ng10_C3", "ng -5_C3" })] [InlineData("en_arpa-plus", new string[] { "good", "morning" }, new string[] { "C3", "C3" }, new string[] { "", "" }, - new string[] { "- g_C3", "g uh_C3", "uh d_C3", "d m_C3", "m ao_C3", "ao r_C3", "r n_C3", "n ih_C3", "ih ng_C3", "ng -_C3" })] + new string[] { "- g_C3", "g uh_C3", "uh d_C3", "d m_C3", "m ao1_C3", "ao r6_C3", "r n_C3", "n ih_C3", "ih ng10_C3", "ng -5_C3" })] public void PhonemizeTest(string singerName, string[] lyrics, string[] tones, string[] colors, string[] aliases) { RunPhonemizeTest(singerName, lyrics, RepeatString(lyrics.Length, ""), tones, colors, aliases); } @@ -56,32 +56,32 @@ public void SyllableTest(string lyric, string hint, string[] aliases) { RunPhonemizeTest("en_arpa-plus", new NoteParams[] { new NoteParams { lyric = lyric, hint = hint, tone = "C3", phonemes = SamePhonemeParams(4, 0, 0, "") } }, aliases); } [Theory] - [InlineData("read", "", new string[] { "- r_C3", "r eh_C3", "eh d_C3", "d -_C3" })] - [InlineData("read", "r iy d", new string[] { "- r_C3", "r iy_C3", "iy d_C3", "d -_C3" })] + [InlineData("read", "", new string[] { "- r_C3", "r eh2_C3", "eh d_C3", "d -7_C3" })] + [InlineData("read", "r iy d", new string[] { "- r3_C3", "r iy7_C3", "iy d_C3", "d -_C3" })] - [InlineData("asdfjkl", "r iy d", new string[] { "- r_C3", "r iy_C3", "iy d_C3", "d -_C3" })] - [InlineData("", "r iy d", new string[] { "- r_C3", "r iy_C3", "iy d_C3", "d -_C3" })] + [InlineData("asdfjkl", "r iy d", new string[] { "- r3_C3", "r iy7_C3", "iy d_C3", "d -_C3" })] + [InlineData("", "r iy d", new string[] { "- r3_C3", "r iy7_C3", "iy d_C3", "d -_C3" })] public void SyllableExternalEndingTest(string lyric, string hint, string[] aliases) { RunPhonemizeTest("en_arpa-plus", new NoteParams[] { new NoteParams { lyric = lyric, hint = hint, tone = "C3", phonemes = SamePhonemeParams(4, 0, 0, "") } }, aliases); } [Theory] - [InlineData("more", "m aor", new string[] { "- m_C3", "m ao_C3", "ao r_C3", "r -_C3" })] + [InlineData("more", "m ao r", new string[] { "- m_C3", "m ao1_C3", "ao r6_C3", "r -5_C3" })] [InlineData("'a", "q ax hh", new string[] { "- q_C3", "q ax_C3", "ax hh_C3", "hh -_C3" })] public void SyllableCCVTest(string lyric, string hint, string[] aliases) { RunPhonemizeTest("en_arpa-plus", new NoteParams[] { new NoteParams { lyric = lyric, hint = hint, tone = "C3", phonemes = SamePhonemeParams(4, 0, 0, "") } }, aliases); } [Theory] - [InlineData("trusting", "", new string[] { "- tr_C3", "tr ah_C3", "ah st_C3", "st ih_C3", "ih ng_C3", "ng -_C3" })] - [InlineData("drive", "", new string[] { "- dr_C3", "dr ay_C3", "ay v_C3", "v -_C3" })] + [InlineData("trusting", "", new string[] { "- tr_C3", "tr ah2_C3", "ah st_C3", "st ih_C3", "ih ng8_C3", "ng -4_C3" })] + [InlineData("drive", "", new string[] { "- dr3_C3", "dr ay2_C3", "ay v1_C3", "v -1_C3" })] public void SyllableFallbackTest(string lyric, string hint, string[] aliases) { RunPhonemizeTest("en_arpa-plus", new NoteParams[] { new NoteParams { lyric = lyric, hint = hint, tone = "C3", phonemes = SamePhonemeParams(4, 0, 0, "") } }, aliases); } [Theory] - [InlineData("kroidroi", "", new string[] { "- kr_C3", "kr oy_C3", "iy dr_C3", "dr oy_C3", "oy -_C3" })] - [InlineData("whhat", "", new string[] { "- hh_C3", "hh uw_C3", "w ah_C3", "ah t_C3", "t -_C3" })] + [InlineData("kroidroi", "", new string[] { "- kr3_C3", "kr oy_C3", "iy dr_C3", "dr oy_C3", "oy -_C3" })] + [InlineData("whhat", "", new string[] { "- hh_C3", "f w_C3", "w ah1_C3", "ah t1_C3", "t -4_C3" })] public void HintTest(string lyric, string hint, string[] aliases) { RunPhonemizeTest("en_arpa-plus", new NoteParams[] { new NoteParams { lyric = lyric, hint = hint, tone = "C3", phonemes = SamePhonemeParams(4, 0, 0, "")} }, aliases);