diff --git a/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs new file mode 100644 index 000000000..4d9319e30 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using Melanchall.DryWetMidi.Interaction; +using OpenUtau.Api; +using OpenUtau.Classic; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Plugin.Builtin { + [Phonemizer("Thai VCCV Phonemizer", "TH VCCV", "PRINTmov", language: "TH")] + public class ThaiVCCVPhonemizer : Phonemizer { + + readonly string[] vowels = new string[] { + "a", "i", "u", "e", "o", "@", "Q", "3", "6", "1", "ia", "ua", "I", "8" + }; + + readonly string[] diphthongs = new string[] { + "r", "l", "w" + }; + + readonly string[] consonants = new string[] { + "b", "ch", "d", "f", "g", "h", "j", "k", "kh", "l", "m", "n", "p", "ph", "r", "s", "t", "th", "w", "y" + }; + + readonly string[] endingConsonants = new string[] { + "b", "ch", "d", "f", "g", "h", "j", "k", "kh", "l", "m", "n", "p", "ph", "r", "s", "t", "th", "w", "y" + }; + + private readonly Dictionary VowelMapping = new Dictionary { + {"เcือะ", "6"}, {"เcือx", "6"}, {"แcะ", "@"}, {"แcx", "@"}, {"เcอะ", "3"}, {"เcอ", "3"}, {"ไc", "I"}, {"ใc", "I"}, {"เcาะ", "Q"}, {"cอx", "Q"}, + {"cืx", "1"}, {"cึx", "1"}, {"cือ", "1"}, {"cะ", "a"}, {"cัx", "a"}, {"cาx", "a"}, {"เcา", "8"}, {"เcะ", "e"}, {"เcx", "e"}, {"cิx", "i"}, {"cีx", "i"}, + {"เcียะ", "ia"}, {"เcียx", "ia"}, {"โcะ", "o"}, {"โcx", "o"}, {"cุx", "u"}, {"cูx", "u"}, {"cัวะ", "ua"}, {"cัว", "ua"}, {"cำ", "am"}, {"เcิx", "3"}, {"เcิ", "3"} + }; + + private readonly Dictionary CMapping = new Dictionary { + {'ก', "k"}, {'ข', "kh"}, {'ค', "kh"}, {'ฆ', "kh"}, {'ฅ', "kh"}, {'ฃ', "kh"}, + {'จ', "j"}, {'ฉ', "ch"}, {'ช', "ch"}, {'ฌ', "ch"}, + {'ฎ', "d"}, {'ด', "d"}, + {'ต', "t"}, {'ฏ', "t"}, + {'ถ', "th"}, {'ฐ', "th"}, {'ฑ', "th"}, {'ธ', "th"}, {'ท', "th"}, + {'บ', "b"}, {'ป', "p"}, {'พ', "ph"}, {'ผ', "ph"}, {'ภ', "ph"}, {'ฟ', "f"}, {'ฝ', "f"}, + {'ห', "h"}, {'ฮ', "h"}, + {'ม', "m"}, {'น', "n"}, {'ณ', "n"}, {'ร', "r"}, {'ล', "l"}, {'ฤ', "r"}, + {'ส', "s"}, {'ศ', "s"}, {'ษ', "s"}, {'ซ', "s"}, + {'ง', "g"}, {'ย', "y"}, {'ญ', "y"}, {'ว', "w"}, {'ฬ', "r"} + }; + + private readonly Dictionary XMapping = new Dictionary { + {'บ', "b"}, {'ป', "b"}, {'พ', "b"}, {'ฟ', "b"}, {'ภ', "b"}, + {'ด', "d"}, {'จ', "d"}, {'ช', "d"}, {'ซ', "d"}, {'ฎ', "d"}, {'ฏ', "d"}, {'ฐ', "d"}, + {'ฑ', "d"}, {'ฒ', "d"}, {'ต', "d"}, {'ถ', "d"}, {'ท', "d"}, {'ธ', "d"}, {'ศ', "d"}, {'ษ', "d"}, {'ส', "d"}, + {'ก', "k"}, {'ข', "k"}, {'ค', "k"}, {'ฆ', "k"}, + {'ว', "w"}, + {'ย', "y"}, + {'น', "n"}, {'ญ', "n"}, {'ณ', "n"}, {'ร', "n"}, {'ล', "n"}, {'ฬ', "n"}, + {'ง', "g"}, + {'ม', "m"} + }; + + private USinger singer; + public override void SetSinger(USinger singer) => this.singer = singer; + + private bool checkOtoUntilHit(string[] input, Note note, out UOto oto) { + oto = default; + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + + foreach (string test in input) { + if (singer.TryGetMappedOto(test, note.tone + attr.toneShift, attr.voiceColor, out var otoCandidacy)) { + oto = otoCandidacy; + return true; + } + } + return false; + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + var note = notes[0]; + var currentLyric = note.lyric.Normalize(); + if (!string.IsNullOrEmpty(note.phoneticHint)) { + currentLyric = note.phoneticHint.Normalize(); + } + + var phonemes = new List(); + + List tests = new List(); + + string prevTemp = ""; + if (prevNeighbour != null) { + prevTemp = prevNeighbour.Value.lyric; + } + var prevTh = ParseInput(prevTemp); + + var noteTh = ParseInput(currentLyric); + + if (noteTh.Consonant != null && noteTh.Dipthong == null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else if (noteTh.Consonant != null && noteTh.Dipthong != null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Dipthong + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } else { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Dipthong }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + if (checkOtoUntilHit(new string[] { noteTh.Dipthong + noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + } + } + + if (noteTh.Consonant == null && noteTh.Vowel != null) { + if (prevTh.EndingConsonant != null && checkOtoUntilHit(new string[] { prevTh.EndingConsonant + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } else if (prevTh.Vowel != null && checkOtoUntilHit(new string[] { prevTh.Vowel + noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } else if (checkOtoUntilHit(new string[] { noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + } + + if (noteTh.EndingConsonant != null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Vowel + noteTh.EndingConsonant }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else if (nextNeighbour != null && noteTh.Vowel != null) { + var nextTh = ParseInput(nextNeighbour.Value.lyric); + if (checkOtoUntilHit(new string[] { noteTh.Vowel + " " + nextTh.Consonant }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } + + if (prevNeighbour == null && tests.Count >= 1) { + if (checkOtoUntilHit(new string[] { "-" + tests[0] }, note, out var tempOto)) { + tests[0] = (tempOto.Alias); + } + } + + if (nextNeighbour == null && tests.Count >= 1) { + if (noteTh.EndingConsonant == null) { + if (checkOtoUntilHit(new string[] { noteTh.Vowel + "-" }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else { + if (checkOtoUntilHit(new string[] { tests[tests.Count - 1] + "-" }, note, out var tempOto)) { + tests[tests.Count - 1] = (tempOto.Alias); + } + } + } + + if (tests.Count <= 0) { + if (checkOtoUntilHit(new string[] { currentLyric }, note, out var tempOto)) { + tests.Add(currentLyric); + } + } + + if (checkOtoUntilHit(tests.ToArray(), note, out var oto)) { + + var noteDuration = notes.Sum(n => n.duration); + + for (int i = 0; i < tests.ToArray().Length; i++) { + + int position = 0; + int vcPosition = noteDuration - 120; + + if (nextNeighbour != null && tests[i].Contains(" ")) { + var nextLyric = nextNeighbour.Value.lyric.Normalize(); + if (!string.IsNullOrEmpty(nextNeighbour.Value.phoneticHint)) { + nextLyric = nextNeighbour.Value.phoneticHint.Normalize(); + } + var nextTh = ParseInput(nextLyric); + var nextCheck = nextTh.Vowel; + if (nextTh.Consonant != null) { + nextCheck = nextTh.Consonant + nextTh.Vowel; + } + if (nextTh.Dipthong != null) { + nextCheck = nextTh.Consonant + nextTh.Dipthong + nextTh.Vowel; + } + var nextAttr = nextNeighbour.Value.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + if (singer.TryGetMappedOto(nextCheck, nextNeighbour.Value.tone + nextAttr.toneShift, nextAttr.voiceColor, out var nextOto)) { + if (oto.Overlap > 0) { + vcPosition = noteDuration - MsToTick(nextOto.Overlap) - MsToTick(nextOto.Preutter); + } + } + } + + + if (noteTh.Dipthong == null || tests.Count <= 2) { + if (i == 1) { + position = Math.Max((int)(noteDuration * 0.75), vcPosition); + } + } else { + if (i == 1) { + position = Math.Min((int)(noteDuration * 0.1), 60); + } else if (i == 2) { + position = Math.Max((int)(noteDuration * 0.75), vcPosition); + } + } + + phonemes.Add(new Phoneme { phoneme = tests[i], position = position }); + } + + } + + return new Result { + phonemes = phonemes.ToArray() + }; + } + + (string Consonant, string Dipthong, string Vowel, string EndingConsonant) ParseInput(string input) { + + input = WordToPhonemes(input); + + string consonant = null; + string dipthong = null; + string vowel = null; + string endingConsonant = null; + + if (input == null) { + return (null, null, null, null); + } + + if (input.Length >= 3) { + foreach (var dip in diphthongs) { + if (input[1].ToString().Equals(dip) || input[2].ToString().Equals(dip)) { + dipthong = dip; + } + } + } + + foreach (var con in consonants) { + if (input.StartsWith(con)) { + if (consonant == null || consonant.Length < con.Length) { + consonant = con; + } + } + if (input.EndsWith(con)) { + if (endingConsonant == null || endingConsonant.Length < con.Length) { + endingConsonant = con; + } + } + } + + foreach (var vow in vowels) { + if (input.Contains(vow)) { + if (vowel == null || vowel.Length < vow.Length) { + vowel = vow; + } + } + } + + return (consonant, dipthong, vowel, endingConsonant); + } + + public string WordToPhonemes(string input) { + input.Replace(" ", ""); + input = RemoveInvalidLetters(input); + if (!Regex.IsMatch(input, "[ก-ฮ]")) { + return input; + } + foreach (var mapping in VowelMapping) { + string pattern = "^" + mapping.Key + .Replace("c", "([ก-ฮ][ลรว]?|อ[ย]?|ห[ก-ฮ]?)") + .Replace("x", "([ก-ฮ]?)") + "$"; + + var match = Regex.Match(input, pattern); + if (match.Success) { + string c = match.Groups[1].Value; + string x = match.Groups.Count > 2 ? match.Groups[2].Value : string.Empty; + if (c.Length >= 2 && (c.StartsWith("ห") || c.StartsWith("อ"))) { + c = c.Substring(1); + } + string cConverted = ConvertC(c); + string xConverted = ConvertX(x); + if (mapping.Value == "a" && input.Contains("ั") && x == "ว") { + return cConverted + "ua"; + } + if (mapping.Value == "e" && x == "ย") { + return cConverted + "3" + xConverted; + } + return cConverted + mapping.Value + xConverted; + } + } + if (input.Length == 1) { + return ConvertC(input) + "Q"; + } else if (input.Length == 2) { + return ConvertC(input[0].ToString()) + "o" + ConvertX(input[1].ToString()); + } else if (input.Length == 3) { + if (input[1] == 'ว') { + return ConvertC(input[0].ToString()) + "ua" + ConvertX(input[2].ToString()); + } else { + return ConvertC(input.Substring(0, 2).ToString()) + "o" + ConvertX(input[1].ToString()); + } + } else if (input.Length == 4) { + if (input[21] == 'ว') { + return ConvertC(input.Substring(0, 2).ToString()) + "ua" + ConvertX(input[3].ToString()); + } + } + return input; + } + + private string ConvertC(string input) { + if (string.IsNullOrEmpty(input)) return input; + char firstChar = input[0]; + char? secondChar = input.Length > 1 ? input[1] : (char?)null; + if (CMapping.ContainsKey(firstChar)) { + string firstCharConverted = CMapping[firstChar]; + if (secondChar != null && CMapping.ContainsKey((char)secondChar)) { + return firstCharConverted + CMapping[(char)secondChar]; + } + return firstCharConverted; + } + return input; + } + + private string ConvertX(string input) { + if (string.IsNullOrEmpty(input)) return input; + char firstChar = input[0]; + if (XMapping.ContainsKey(firstChar)) { + return XMapping[firstChar]; + } + return input; + } + + private string RemoveInvalidLetters(string input) { + input = Regex.Replace(input, ".์", ""); + input = Regex.Replace(input, "[่้๊๋็]", ""); + return input; + } + + } +} diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index 87a06cf53..afa89e2ee 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -122,6 +122,7 @@ Vietnamese Chinese Cantonese + Thai Apply Cancel diff --git a/OpenUtau/Strings/Strings.de-DE.axaml b/OpenUtau/Strings/Strings.de-DE.axaml index dfadbb10d..66ec7f352 100644 --- a/OpenUtau/Strings/Strings.de-DE.axaml +++ b/OpenUtau/Strings/Strings.de-DE.axaml @@ -115,6 +115,7 @@ Vietnamesisch Chinesisch Kantonesisch + Thailändisch Anwenden Abbrechen diff --git a/OpenUtau/Strings/Strings.es-ES.axaml b/OpenUtau/Strings/Strings.es-ES.axaml index 91d99dc56..c9b0971dc 100644 --- a/OpenUtau/Strings/Strings.es-ES.axaml +++ b/OpenUtau/Strings/Strings.es-ES.axaml @@ -115,6 +115,7 @@ vietnamita chino cantonés + tailandés diff --git a/OpenUtau/Strings/Strings.es-MX.axaml b/OpenUtau/Strings/Strings.es-MX.axaml index 1a7d5d2c2..172a550a6 100644 --- a/OpenUtau/Strings/Strings.es-MX.axaml +++ b/OpenUtau/Strings/Strings.es-MX.axaml @@ -115,6 +115,7 @@ vietnamita chino cantonés + tailandés Aplicar Cancelar diff --git a/OpenUtau/Strings/Strings.fi-FI.axaml b/OpenUtau/Strings/Strings.fi-FI.axaml index ece3ff511..931e8e8f0 100644 --- a/OpenUtau/Strings/Strings.fi-FI.axaml +++ b/OpenUtau/Strings/Strings.fi-FI.axaml @@ -115,6 +115,7 @@ vietnam kiina kantoninkiina + thaikieli diff --git a/OpenUtau/Strings/Strings.fr-FR.axaml b/OpenUtau/Strings/Strings.fr-FR.axaml index 7efa1a104..7bedc4ffa 100644 --- a/OpenUtau/Strings/Strings.fr-FR.axaml +++ b/OpenUtau/Strings/Strings.fr-FR.axaml @@ -115,6 +115,7 @@ vietnamien chinois cantonais + thaïlandais Appliquer Annuler diff --git a/OpenUtau/Strings/Strings.id-ID.axaml b/OpenUtau/Strings/Strings.id-ID.axaml index 500f8ece1..8f4c7d90a 100644 --- a/OpenUtau/Strings/Strings.id-ID.axaml +++ b/OpenUtau/Strings/Strings.id-ID.axaml @@ -115,6 +115,7 @@ Vietnam Tionghoa Kanton + Thai Terapkan Batalkan diff --git a/OpenUtau/Strings/Strings.it-IT.axaml b/OpenUtau/Strings/Strings.it-IT.axaml index 09ac9ba7d..8c5e8c903 100644 --- a/OpenUtau/Strings/Strings.it-IT.axaml +++ b/OpenUtau/Strings/Strings.it-IT.axaml @@ -115,6 +115,7 @@ vietnamita cinese cantonese + thailandese Applica Cancella diff --git a/OpenUtau/Strings/Strings.ja-JP.axaml b/OpenUtau/Strings/Strings.ja-JP.axaml index e95a86d7b..dcfec1fc3 100644 --- a/OpenUtau/Strings/Strings.ja-JP.axaml +++ b/OpenUtau/Strings/Strings.ja-JP.axaml @@ -120,6 +120,7 @@ ベトナム語 中国語 広東語 + タイ語 適用 キャンセル diff --git a/OpenUtau/Strings/Strings.ko-KR.axaml b/OpenUtau/Strings/Strings.ko-KR.axaml index fb12fe84a..6ba7cfb64 100644 --- a/OpenUtau/Strings/Strings.ko-KR.axaml +++ b/OpenUtau/Strings/Strings.ko-KR.axaml @@ -115,6 +115,7 @@ 베트남어 중국어 광둥어 + 태국어 적용 취소 diff --git a/OpenUtau/Strings/Strings.nl-NL.axaml b/OpenUtau/Strings/Strings.nl-NL.axaml index 918a2a0fd..433f94d91 100644 --- a/OpenUtau/Strings/Strings.nl-NL.axaml +++ b/OpenUtau/Strings/Strings.nl-NL.axaml @@ -115,6 +115,7 @@ Vietnamees Chinees Kantonees + Thais Toepassen Annuleer diff --git a/OpenUtau/Strings/Strings.pl-PL.axaml b/OpenUtau/Strings/Strings.pl-PL.axaml index cc2e49dd4..881e33279 100644 --- a/OpenUtau/Strings/Strings.pl-PL.axaml +++ b/OpenUtau/Strings/Strings.pl-PL.axaml @@ -122,6 +122,7 @@ wietnamski chiński kantoński + tajski Zastosuj Anuluj diff --git a/OpenUtau/Strings/Strings.pt-BR.axaml b/OpenUtau/Strings/Strings.pt-BR.axaml index af493f629..8ba2d0538 100644 --- a/OpenUtau/Strings/Strings.pt-BR.axaml +++ b/OpenUtau/Strings/Strings.pt-BR.axaml @@ -115,6 +115,7 @@ vietnamita chinês cantonês + tailandês Aplicar Cancelar diff --git a/OpenUtau/Strings/Strings.ru-RU.axaml b/OpenUtau/Strings/Strings.ru-RU.axaml index 18c94b6a3..eb57472c6 100644 --- a/OpenUtau/Strings/Strings.ru-RU.axaml +++ b/OpenUtau/Strings/Strings.ru-RU.axaml @@ -115,6 +115,7 @@ вьетнамский китайский кантонский + тайский Применить Отмена diff --git a/OpenUtau/Strings/Strings.th-TH.axaml b/OpenUtau/Strings/Strings.th-TH.axaml index aabc9ecf3..e46175350 100644 --- a/OpenUtau/Strings/Strings.th-TH.axaml +++ b/OpenUtau/Strings/Strings.th-TH.axaml @@ -115,6 +115,7 @@ เวียดนาม จีน กวางตุ้ง + ไทย ใช้งาน ยกเลิก diff --git a/OpenUtau/Strings/Strings.vi-VN.axaml b/OpenUtau/Strings/Strings.vi-VN.axaml index 19e32a03e..e903299f6 100644 --- a/OpenUtau/Strings/Strings.vi-VN.axaml +++ b/OpenUtau/Strings/Strings.vi-VN.axaml @@ -115,6 +115,7 @@ Tiếng Việt Tiếng Trung Tiếng Quảng Đông + Tiếng Thái Áp dụng Huỷ bỏ diff --git a/OpenUtau/Strings/Strings.zh-CN.axaml b/OpenUtau/Strings/Strings.zh-CN.axaml index f693e840e..c90c3511e 100644 --- a/OpenUtau/Strings/Strings.zh-CN.axaml +++ b/OpenUtau/Strings/Strings.zh-CN.axaml @@ -115,6 +115,7 @@ 越南语 中文 中文(粤语) + 泰语 应用 取消 diff --git a/OpenUtau/Strings/Strings.zh-TW.axaml b/OpenUtau/Strings/Strings.zh-TW.axaml index 573b93033..a1e89f00f 100644 --- a/OpenUtau/Strings/Strings.zh-TW.axaml +++ b/OpenUtau/Strings/Strings.zh-TW.axaml @@ -115,6 +115,7 @@ 越南文 中文 粵語 + 泰語 套用 取消