diff --git a/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs b/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs
new file mode 100644
index 000000000..d2b42bc43
--- /dev/null
+++ b/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs
@@ -0,0 +1,606 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using K4os.Hash.xxHash;
+using OpenUtau.Api;
+using OpenUtau.Core.Ustx;
+using Serilog;
+
+namespace OpenUtau.Core.Enunu {
+ [Phonemizer("Enunu Korean Phonemizer", "ENUNU KO", "EX3", language:"KO")]
+ public class EnunuKoreanPhonemizer : EnunuPhonemizer {
+ readonly string PhonemizerType = "ENUNU KO";
+ public string semivowelSep;
+ private KoreanENUNUSetting koreanENUNUSetting; // Manages Settings
+ private bool isSeparateSemiVowels; // Nanages n y a or ny a
+
+ ///
+ /// Default KO ENUNU first consonants table
+ ///
+ static readonly List firstDefaultConsonants = new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData[19]{
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄱ", "g"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄲ", "kk"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄴ", "n"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄷ", "d"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄸ", "tt"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄹ", "r"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅁ", "m"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅂ", "b"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅃ", "pp"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅅ", "s"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅆ", "ss"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅇ", ""),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅈ", "j"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅉ", "jj"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅊ", "ch"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅋ", "k"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅌ", "t"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅍ", "p"),
+ new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅎ", "h")
+ }.ToList();
+
+ ///
+ /// Default KO ENUNU plain vowels table
+ ///
+ static readonly List plainDefaultVowels = new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData[7]{
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅏ", "a"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅣ", "i"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅜ", "u"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅔ/ㅐ", "e"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅗ", "o"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅓ", "eo"),
+ new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅡ", "eu")
+ }.ToList();
+
+ ///
+ /// Default KO ENUNU semivowels table
+ ///
+ static readonly List semiDefaultVowels = new KoreanPhonemizerUtil.JamoDictionary.SemivowelData[2]{
+ new KoreanPhonemizerUtil.JamoDictionary.SemivowelData("y", "y"),
+ new KoreanPhonemizerUtil.JamoDictionary.SemivowelData("w", "w")
+ }.ToList();
+
+ ///
+ /// Default KO ENUNU final consonants table
+ ///
+ static readonly List finalDefaultConsonants = new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData[7]{
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄱ", "K"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄴ", "N"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄷ", "T"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄹ", "L"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅁ", "M"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅂ", "P"),
+ new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅇ", "NG")
+ }.ToList();
+
+ ///
+ /// KO ENUNU phoneme table of first consonants. (key "null" is for Handling empty string)
+ ///
+ private Dictionary FirstConsonants = new Dictionary(){
+ {"ㄱ", new string[2]{"g", ConsonantType.NORMAL.ToString()}},
+ {"ㄲ", new string[2]{"kk", ConsonantType.FORTIS.ToString()}},
+ {"ㄴ", new string[2]{"n", ConsonantType.NASAL.ToString()}},
+ {"ㄷ", new string[2]{"d", ConsonantType.NORMAL.ToString()}},
+ {"ㄸ", new string[2]{"tt", ConsonantType.FORTIS.ToString()}},
+ {"ㄹ", new string[2]{"r", ConsonantType.LIQUID.ToString()}},
+ {"ㅁ", new string[2]{"m", ConsonantType.NASAL.ToString()}},
+ {"ㅂ", new string[2]{"b", ConsonantType.NORMAL.ToString()}},
+ {"ㅃ", new string[2]{"pp", ConsonantType.FORTIS.ToString()}},
+ {"ㅅ", new string[2]{"s", ConsonantType.NORMAL.ToString()}},
+ {"ㅆ", new string[2]{"ss", ConsonantType.FRICATIVE.ToString()}},
+ {"ㅇ", new string[2]{"", ConsonantType.NOCONSONANT.ToString()}},
+ {"ㅈ", new string[2]{"j", ConsonantType.NORMAL.ToString()}},
+ {"ㅉ", new string[2]{"jj", ConsonantType.FORTIS.ToString()}},
+ {"ㅊ", new string[2]{"ch", ConsonantType.ASPIRATE.ToString()}},
+ {"ㅋ", new string[2]{"k", ConsonantType.ASPIRATE.ToString()}},
+ {"ㅌ", new string[2]{"t", ConsonantType.ASPIRATE.ToString()}},
+ {"ㅍ", new string[2]{"p", ConsonantType.ASPIRATE.ToString()}},
+ {"ㅎ", new string[2]{"h", ConsonantType.H.ToString()}},
+ {" ", new string[2]{"", ConsonantType.NOCONSONANT.ToString()}},
+ {"null", new string[2]{"", ConsonantType.PHONEME_IS_NULL.ToString()}} // 뒤 글자가 없을 때를 대비
+ };
+
+ ///
+ /// KO ENUNU phoneme table of middle vowels (key "null" is for Handling empty string)
+ ///
+ private Dictionary MiddleVowels = new Dictionary(){
+ {"ㅏ", new string[3]{"a", "", "a"}},
+ {"ㅐ", new string[3]{"e", "", "e"}},
+ {"ㅑ", new string[3]{"ya", "y", " a"}},
+ {"ㅒ", new string[3]{"ye", "y", " e"}},
+ {"ㅓ", new string[3]{"eo", "", "eo"}},
+ {"ㅔ", new string[3]{"e", "", "e"}},
+ {"ㅕ", new string[3]{"yeo", "y", " eo"}},
+ {"ㅖ", new string[3]{"ye", "y", " e"}},
+ {"ㅗ", new string[3]{"o", "", "o"}},
+ {"ㅘ", new string[3]{"wa", "w", " a"}},
+ {"ㅙ", new string[3]{"we", "w", " e"}},
+ {"ㅚ", new string[3]{"we", "w", " e"}},
+ {"ㅛ", new string[3]{"yo", "y", " o"}},
+ {"ㅜ", new string[3]{"u", "", "u"}},
+ {"ㅝ", new string[3]{"weo", "w", " eo"}},
+ {"ㅞ", new string[3]{"we", "w", " e"}},
+ {"ㅟ", new string[3]{"wi", "w", " i"}},
+ {"ㅠ", new string[3]{"yu", "y", " u"}},
+ {"ㅡ", new string[3]{"eu", "", "eu"}},
+ {"ㅢ", new string[3]{"i", "", "i"}}, // ㅢ는 ㅣ로 발음
+ {"ㅣ", new string[3]{"i", "", "i"}},
+ {" ", new string[3]{"", "", ""}},
+ {"null", new string[3]{"", "", ""}} // 뒤 글자가 없을 때를 대비
+ };
+ ///
+ /// KO ENUNU phoneme table of last consonants. (key "null" is for Handling empty string)
+ ///
+ private Dictionary LastConsonants = new Dictionary(){
+ //ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ
+ {"ㄱ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}},
+ {"ㄲ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}},
+ {"ㄳ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}},
+ {"ㄴ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}},
+ {"ㄵ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}},
+ {"ㄶ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}},
+ {"ㄷ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㄹ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}},
+ {"ㄺ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}},
+ {"ㄻ", new string[3]{" M", "1", BatchimType.NASAL_END.ToString()}},
+ {"ㄼ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}},
+ {"ㄽ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}},
+ {"ㄾ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}},
+ {"ㄿ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅀ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}},
+ {"ㅁ", new string[3]{" M", "1", BatchimType.NASAL_END.ToString()}},
+ {"ㅂ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅄ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅅ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅆ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅇ", new string[3]{" NG", "3", BatchimType.NG_END.ToString()}},
+ {"ㅈ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅊ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅋ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}},
+ {"ㅌ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅍ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}},
+ {"ㅎ", new string[3]{" T", "1", BatchimType.H_END.ToString()}},
+ {" ", new string[3]{"", "", BatchimType.NO_END.ToString()}},
+ {"null", new string[3]{"", "", BatchimType.PHONEME_IS_NULL.ToString()}} // 뒤 글자가 없을 때를 대비
+ };
+
+ struct TimingResult {
+ public string path_full_timing;
+ public string path_mono_timing;
+ }
+
+ struct TimingResponse {
+ public string error;
+ public TimingResult result;
+ }
+ public override void SetSinger(USinger singer) {
+ if (singer.SingerType != USingerType.Enunu) {return;}
+
+ this.singer = singer as EnunuSinger;
+
+ koreanENUNUSetting = new KoreanENUNUSetting("jamo_dict.yaml");
+
+ koreanENUNUSetting.Initialize(singer, "ko-ENUNU.ini", new Hashtable(){
+ {
+ "SETTING", new Hashtable(){
+ {"Separate semivowels, like 'n y a'(otherwise 'ny a')", "True"}
+ }
+ }
+ }
+ );
+
+ isSeparateSemiVowels = koreanENUNUSetting.isSeparateSemiVowels;
+ semivowelSep = isSeparateSemiVowels ? " ": "";
+
+ // Modify Phoneme Tables
+ // First Consonants
+ FirstConsonants["ㄱ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄱ")}";
+ FirstConsonants["ㄲ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄲ")}";
+ FirstConsonants["ㄴ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄴ")}";
+ FirstConsonants["ㄷ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄷ")}";
+ FirstConsonants["ㄸ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄸ")}";
+ FirstConsonants["ㄹ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄹ")}";
+ FirstConsonants["ㅁ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅁ")}";
+ FirstConsonants["ㅂ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅂ")}";
+ FirstConsonants["ㅃ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅃ")}";
+ FirstConsonants["ㅅ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅅ")}";
+ FirstConsonants["ㅆ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅆ")}";
+ FirstConsonants["ㅇ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅇ")}";
+ FirstConsonants["ㅈ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅈ")}";
+ FirstConsonants["ㅉ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅉ")}";
+ FirstConsonants["ㅊ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅊ")}";
+ FirstConsonants["ㅋ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅋ")}";
+ FirstConsonants["ㅌ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅌ")}";
+ FirstConsonants["ㅍ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅍ")}";
+ FirstConsonants["ㅎ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅎ")}";
+
+
+ // Vowels
+ MiddleVowels["ㅑ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+ MiddleVowels["ㅒ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+ MiddleVowels["ㅕ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+ MiddleVowels["ㅖ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+ MiddleVowels["ㅘ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅙ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅚ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅛ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+ MiddleVowels["ㅝ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅞ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅟ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w");
+ MiddleVowels["ㅠ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y");
+
+ MiddleVowels["ㅏ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}";
+ MiddleVowels["ㅐ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅑ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}";
+ MiddleVowels["ㅒ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅓ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}";
+ MiddleVowels["ㅔ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅕ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}";
+ MiddleVowels["ㅖ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅗ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ")}";
+ MiddleVowels["ㅘ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}";
+ MiddleVowels["ㅙ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅚ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅛ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ")}";
+ MiddleVowels["ㅜ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ")}";
+ MiddleVowels["ㅝ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}";
+ MiddleVowels["ㅞ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}";
+ MiddleVowels["ㅟ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}";
+ MiddleVowels["ㅠ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ")}";
+ MiddleVowels["ㅡ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅡ")}";
+ MiddleVowels["ㅢ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}"; // ㅢ는 ㅣ로 발음
+ MiddleVowels["ㅣ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}";
+
+ // final consonants
+ LastConsonants["ㄱ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}";
+ LastConsonants["ㄲ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}";
+ LastConsonants["ㄳ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}";
+ LastConsonants["ㄴ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}";
+ LastConsonants["ㄵ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}";
+ LastConsonants["ㄶ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}";
+ LastConsonants["ㄷ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㄹ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}";
+ LastConsonants["ㄺ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}";
+ LastConsonants["ㄻ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ")}";
+ LastConsonants["ㄼ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}";
+ LastConsonants["ㄽ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}";
+ LastConsonants["ㄾ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}";
+ LastConsonants["ㄿ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}";
+ LastConsonants["ㅀ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}";
+ LastConsonants["ㅁ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ")}";
+ LastConsonants["ㅂ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}";
+ LastConsonants["ㅄ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}";
+ LastConsonants["ㅅ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㅆ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㅇ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅇ")}";
+ LastConsonants["ㅈ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㅊ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㅋ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}";
+ LastConsonants["ㅌ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+ LastConsonants["ㅍ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}";
+ LastConsonants["ㅎ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}";
+
+ }
+ private class KoreanENUNUSetting : KoreanPhonemizerUtil.BaseIniManager{
+ // uses KO-ENUNU.ini, jamo_dict.yaml
+ public bool isSeparateSemiVowels;
+ public string yamlFileName;
+ private KoreanPhonemizerUtil.JamoDictionary jamoDict;
+ public KoreanENUNUSetting(string yamlFileName) {
+ this.yamlFileName = yamlFileName;
+ }
+ protected override void IniSetUp(Hashtable iniFile) {
+ // ko-ENUNU.ini + jamo_dict.yaml
+ SetOrReadThisValue("SETTING", "Separate semivowels, like 'n y a'(otherwise 'ny a')", false, out var resultValue); // 반자음 떼기 유무 - 기본값 false
+ isSeparateSemiVowels = resultValue;
+
+ try {
+ jamoDict = Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(Path.Combine(singer.Location, yamlFileName)));
+ if (jamoDict == null) {
+ throw new IOException("yaml file is null");
+ }
+ }
+ catch (IOException e) {
+ Log.Error(e, $"Failed to read {Path.Combine(singer.Location, yamlFileName)}");
+
+ jamoDict = new KoreanPhonemizerUtil.JamoDictionary(firstDefaultConsonants.ToArray(), plainDefaultVowels.ToArray(), semiDefaultVowels.ToArray(), finalDefaultConsonants.ToArray());
+
+ File.WriteAllText(Path.Combine(singer.Location, yamlFileName), Yaml.DefaultSerializer.Serialize(jamoDict));
+ }
+
+ }
+
+
+ public string GetFirstConsonantPhoneme(string Phoneme) {
+ KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData results = jamoDict.firstConsonants.ToList().Find(c => c.grapheme == Phoneme);
+ string result = results.phoneme;
+ if (result == null) {
+ result = firstDefaultConsonants.Find(c => c.grapheme == Phoneme).phoneme;
+ }
+ return result.Trim();
+ }
+
+ public string GetPlainVowelPhoneme(string Phoneme) {
+ KoreanPhonemizerUtil.JamoDictionary.PlainVowelData results = jamoDict.plainVowels.ToList().Find(c => c.grapheme == Phoneme);
+ string result = results.phoneme;
+ if (result == null) {
+ result = plainDefaultVowels.Find(c => c.grapheme == Phoneme).phoneme;
+ }
+ return result.Trim();
+ }
+
+ public string GetSemiVowelPhoneme(string Phoneme) {
+ KoreanPhonemizerUtil.JamoDictionary.SemivowelData results = jamoDict.semivowels.ToList().Find(c => c.grapheme == Phoneme);
+ string result = results.phoneme;
+ if (result == null) {
+ result = semiDefaultVowels.Find(c => c.grapheme == Phoneme).phoneme;
+ }
+ return result.Trim();
+ }
+
+ public string GetFinalConsonantPhoneme(string Phoneme) {
+ KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData results = jamoDict.finalConsonants.ToList().Find(c => c.grapheme == Phoneme);
+ string result = results.phoneme;
+ if (result == null) {
+ result = finalDefaultConsonants.Find(c => c.grapheme == Phoneme).phoneme;
+ }
+ return result.Trim();
+ }
+ }
+
+ public enum ConsonantType{
+ /// 예사소리
+ NORMAL,
+ /// 거센소리
+ ASPIRATE,
+ /// 된소리
+ FORTIS,
+ /// 마찰음
+ FRICATIVE,
+ /// 비음
+ NASAL,
+ /// 유음
+ LIQUID,
+ /// ㅎ
+ H,
+ /// 자음의 음소값 없음(ㅇ)
+ NOCONSONANT,
+ /// 음소 자체가 없음
+ PHONEME_IS_NULL
+ }
+
+ ///
+ /// Last Consonant's type.
+ ///
+ public enum BatchimType{
+ /// 예사소리 받침
+ NORMAL_END,
+ /// 비음 받침
+ NASAL_END,
+ /// 유음 받침
+ LIQUID_END,
+ /// ㅇ받침
+ NG_END,
+ /// ㅎ받침
+ H_END,
+ /// 받침이 없음
+ NO_END,
+ /// 음소 자체가 없음
+ PHONEME_IS_NULL
+ }
+
+ Dictionary partResult = new Dictionary();
+
+ public override void SetUp(Note[][] notes) {
+ partResult.Clear();
+ if (notes.Length == 0 || singer == null || !singer.Found) {
+ return;
+ }
+ double bpm = timeAxis.GetBpmAtTick(notes[0][0].position);
+ ulong hash = HashNoteGroups(notes, bpm);
+ var tmpPath = Path.Join(PathManager.Inst.CachePath, $"lab-{hash:x16}");
+ var ustPath = tmpPath + ".tmp";
+ var enutmpPath = tmpPath + "_enutemp";
+ var scorePath = Path.Join(enutmpPath, $"score.lab");
+ var timingPath = Path.Join(enutmpPath, $"timing.lab");
+ var enunuNotes = NoteGroupsToEnunu(notes);
+ if (!File.Exists(scorePath) || !File.Exists(timingPath)) {
+ EnunuUtils.WriteUst(enunuNotes, bpm, singer, ustPath);
+ var response = EnunuClient.Inst.SendRequest(new string[] { "timing", ustPath });
+ if (response.error != null) {
+ throw new Exception(response.error);
+ }
+ }
+ var noteIndexes = LabelToNoteIndex(scorePath, enunuNotes);
+ var timing = ParseLabel(timingPath);
+ timing.Zip(noteIndexes, (phoneme, noteIndex) => Tuple.Create(phoneme, noteIndex))
+ .GroupBy(tuple => tuple.Item2)
+ .ToList()
+ .ForEach(g => {
+ if (g.Key >= 0) {
+ var noteGroup = notes[g.Key];
+ partResult[noteGroup] = g.Select(tu => tu.Item1).ToArray();
+ }
+ });
+ }
+
+ ulong HashNoteGroups(Note[][] notes, double bpm) {
+ using (var stream = new MemoryStream()) {
+ using (var writer = new BinaryWriter(stream)) {
+ writer.Write(this.PhonemizerType);
+ writer.Write(this.singer.Location);
+ writer.Write(bpm);
+ foreach (var ns in notes) {
+ foreach (var n in ns) {
+ writer.Write(n.lyric);
+ if(n.phoneticHint!= null) {
+ writer.Write("["+n.phoneticHint+"]");
+ }
+ writer.Write(n.position);
+ writer.Write(n.duration);
+ writer.Write(n.tone);
+ }
+ }
+ return XXH64.DigestOf(stream.ToArray());
+ }
+ }
+ }
+
+ static int[] LabelToNoteIndex(string scorePath, EnunuNote[] enunuNotes) {
+ var result = new List();
+ int lastPos = 0;
+ int index = 0;
+ var score = ParseLabel(scorePath);
+ foreach (var p in score) {
+ if (p.position != lastPos) {
+ index++;
+ lastPos = p.position;
+ }
+ result.Add(enunuNotes[index].noteIndex);
+ }
+ return result.ToArray();
+ }
+
+ static Phoneme[] ParseLabel(string path) {
+ var phonemes = new List();
+ using (var reader = new StreamReader(path, Encoding.UTF8)) {
+ while (!reader.EndOfStream) {
+ var line = reader.ReadLine();
+ var parts = line.Split();
+ if (parts.Length == 3 &&
+ long.TryParse(parts[0], out long pos) &&
+ long.TryParse(parts[1], out long end)) {
+ phonemes.Add(new Phoneme {
+ phoneme = parts[2],
+ position = (int)(pos / 1000L),
+ });
+ }
+ }
+ }
+ return phonemes.ToArray();
+ }
+
+ protected override EnunuNote[] NoteGroupsToEnunu(Note[][] notes) {
+ KoreanPhonemizerUtil.RomanizeNotes(notes, FirstConsonants, MiddleVowels, LastConsonants, semivowelSep);
+ var result = new List();
+ int position = 0;
+ int index = 0;
+
+ while (index < notes.Length) {
+ if (position < notes[index][0].position) {
+ result.Add(new EnunuNote {
+ lyric = "R",
+ length = notes[index][0].position - position,
+ noteNum = 60,
+ noteIndex = -1,
+ });
+ position = notes[index][0].position;
+ } else {
+ var lyric = notes[index][0].lyric;
+ result.Add(new EnunuNote {
+ lyric = lyric,
+ length = notes[index].Sum(n => n.duration),
+ noteNum = notes[index][0].tone,
+ noteIndex = index,
+ });
+ position += result.Last().length;
+ index++;
+ }
+ }
+ return result.ToArray();
+ }
+
+ // public void AdjustPos(Phoneme[] phonemes, Note[] prevNote){
+ // //TODO
+ // Phoneme? prevPhone = null;
+ // Phoneme? nextPhone = null;
+ // Phoneme currPhone;
+
+ // int length = phonemes.Last().position;
+ // int prevLength;
+ // if (prevNote == null){
+ // prevLength = length;
+ // }
+ // else{
+ // prevLength = MsToTick(prevNote.Sum(n => n.duration));
+ // }
+
+ // for (int i=0; i < phonemes.Length; i++) {
+ // currPhone = phonemes[i];
+ // if (i < phonemes.Length - 1){
+ // nextPhone = phonemes[i+1];
+ // }
+ // else{
+ // nextPhone = null;
+ // }
+
+ // if (i == 0){
+ // // TODO 받침 + 자음 오면 받침길이 + 자음길이 / 2의 위치에 자음이 오도록 하기
+ // if (isPlainVowel(phonemes[i].phoneme)) {
+ // phonemes[i].position = 0;
+ // }
+ // else if (nextPhone != null && ! isPlainVowel(((Phoneme)nextPhone).phoneme) && ! isSemivowel(((Phoneme)nextPhone).phoneme) && isPlainVowel(((Phoneme)nextPhone).phoneme) && isSemivowel(currPhone.phoneme)) {
+ // phonemes[i + 1].position = length / 10;
+ // }
+ // else if (nextPhone != null && isSemivowel(((Phoneme)nextPhone).phoneme)){
+ // if (i + 2 < phonemes.Length){
+ // phonemes[i + 2].position = length / 10;
+ // }
+
+ // }
+ // }
+ // prevPhone = currPhone;
+ // }
+ // }
+
+ // private bool isPlainVowel(string phoneme){
+ // if (phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅡ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")){
+ // return true;
+ // }
+ // return false;
+ // }
+
+ // private bool isBatchim(string phoneme){
+ // if (phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅇ")){
+ // return true;
+ // }
+ // return false;
+ // }
+
+ // private bool isSemivowel(string phoneme) {
+ // if (phoneme == koreanENUNUSetting.GetSemiVowelPhoneme("w") || phoneme == koreanENUNUSetting.GetSemiVowelPhoneme("y")){
+ // return true;
+ // }
+ // return false;
+ // }
+ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevs) {
+ if (partResult.TryGetValue(notes, out var phonemes)) {
+ var phonemes_ = phonemes.Select(p => {
+ double posMs = p.position * 0.1;
+ p.position = MsToTick(posMs) - notes[0].position;
+ return p;
+ }).ToArray();
+
+ //AdjustPos(phonemes_, prevs);
+ return new Result {
+ phonemes = phonemes_,
+ };
+ }
+ return new Result {
+ phonemes = new Phoneme[] {
+ new Phoneme {
+ phoneme = "error",
+ }
+ },
+ };
+ }
+
+ public override void CleanUp() {
+ partResult.Clear();
+ }
+
+ }
+}