diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs new file mode 100644 index 000000000..632d195cd --- /dev/null +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -0,0 +1,1433 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using OpenUtau.Core.Ustx; +using OpenUtau.Classic; +using Serilog; +using static OpenUtau.Api.Phonemizer; + +namespace OpenUtau.Core { + /// + /// static class that performs Korean Phoneme Variation, Jamo separation, Jamo merging, etc. + /// + public static class KoreanPhonemizerUtil { + /// + /// First hangeul consonants, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 초성들입니다. + ///
+ const string FIRST_CONSONANTS = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"; + /// + /// Middle hangeul vowels, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 중성들입니다. + ///
+ const string MIDDLE_VOWELS = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"; + + /// + /// Last hangeul consonants, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 종성들입니다. + ///
+ const string LAST_CONSONANTS = " ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"; // The first blank(" ") is needed because Hangeul may not have lastConsonant. + + /// + /// unicode index of 가 + /// + const ushort HANGEUL_UNICODE_START = 0xAC00; + + /// + /// unicode index of 힣 + /// + const ushort HANGEUL_UNICODE_END = 0xD79F; + + /// + /// A hashtable of basicsounds - ㄱ/ㄷ/ㅂ/ㅅ/ㅈ. + ///

예사소리 테이블입니다. + ///
+ public static readonly Hashtable basicSounds = new Hashtable() { + ["ㄱ"] = 0, + ["ㄷ"] = 1, + ["ㅂ"] = 2, + ["ㅈ"] = 3, + ["ㅅ"] = 4 + }; + + /// + /// A hashtable of aspirate sounds - ㅋ/ㅌ/ㅍ/ㅊ/(ㅌ). + ///
[4] is "ㅌ", it will be used when conducting phoneme variation - 격음화(거센소리되기). + ///

거센소리 테이블입니다. + ///
[4]의 중복값 "ㅌ"은 오타가 아니며 격음화(거센소리되기) 수행 시에 활용됩니다. + ///
+ public static readonly Hashtable aspirateSounds = new Hashtable() { + [0] = "ㅋ", + [1] = "ㅌ", + [2] = "ㅍ", + [3] = "ㅊ", + [4] = "ㅌ" + }; + + /// + /// A hashtable of fortis sounds - ㄲ/ㄸ/ㅃ/ㅆ/ㅉ. + ///

된소리 테이블입니다. + ///
+ public static readonly Hashtable fortisSounds = new Hashtable() { + [0] = "ㄲ", + [1] = "ㄸ", + [2] = "ㅃ", + [3] = "ㅉ", + [4] = "ㅆ" + }; + + /// + /// A hashtable of nasal sounds - ㄴ/ㅇ/ㅁ. + ///

비음 테이블입니다. + ///
+ public static readonly Hashtable nasalSounds = new Hashtable() { + ["ㄴ"] = 0, + ["ㅇ"] = 1, + ["ㅁ"] = 2 + }; + + + /// + /// Confirms if input string is hangeul. + ///

입력 문자열이 한글인지 확인합니다. + ///
+ /// A string of Hangeul character. + ///
(Example: "가", "!가", "가.") + /// Returns true when input string is Hangeul, otherwise false. + public static bool IsHangeul(string? character) { + + ushort unicodeIndex; + bool isHangeul; + if ((character != null) && character.StartsWith('!')) { + // Automatically deletes ! from start. + // Prevents error when user uses ! as a phonetic symbol. + unicodeIndex = Convert.ToUInt16(character.TrimStart('!')[0]); + isHangeul = !(unicodeIndex < HANGEUL_UNICODE_START || unicodeIndex > HANGEUL_UNICODE_END); + } + else if (character != null) { + try { + unicodeIndex = Convert.ToUInt16(character[0]); + isHangeul = !(unicodeIndex < HANGEUL_UNICODE_START || unicodeIndex > HANGEUL_UNICODE_END); + } + catch { + isHangeul = false; + } + + } + else { + isHangeul = false; + } + + return isHangeul; + } + /// + /// Separates complete hangeul string's first character in three parts - firstConsonant(초성), middleVowel(중성), lastConsonant(종성). + ///
입력된 문자열의 0번째 글자를 초성, 중성, 종성으로 분리합니다. + ///
+ /// A string of complete Hangeul character. + ///
(Example: '냥') + /// + /// {firstConsonant(초성), middleVowel(중성), lastConsonant(종성)} + /// (ex) {"ㄴ", "ㅑ", "ㅇ"} + /// + public static Hashtable Separate(string character) { + + int hangeulIndex; // unicode index of hangeul - unicode index of '가' (ex) '냥' + + int firstConsonantIndex; // (ex) 2 + int middleVowelIndex; // (ex) 2 + int lastConsonantIndex; // (ex) 21 + + string firstConsonant; // (ex) "ㄴ" + string middleVowel; // (ex) "ㅑ" + string lastConsonant; // (ex) "ㅇ" + + Hashtable separatedHangeul; // (ex) {[0]: "ㄴ", [1]: "ㅑ", [2]: "ㅇ"} + + + hangeulIndex = Convert.ToUInt16(character[0]) - HANGEUL_UNICODE_START; + + // seperates lastConsonant + lastConsonantIndex = hangeulIndex % 28; + hangeulIndex = (hangeulIndex - lastConsonantIndex) / 28; + + // seperates middleVowel + middleVowelIndex = hangeulIndex % 21; + hangeulIndex = (hangeulIndex - middleVowelIndex) / 21; + + // there's only firstConsonant now + firstConsonantIndex = hangeulIndex; + + // separates character + firstConsonant = FIRST_CONSONANTS[firstConsonantIndex].ToString(); + middleVowel = MIDDLE_VOWELS[middleVowelIndex].ToString(); + lastConsonant = LAST_CONSONANTS[lastConsonantIndex].ToString(); + + separatedHangeul = new Hashtable() { + [0] = firstConsonant, + [1] = middleVowel, + [2] = lastConsonant + }; + + + return separatedHangeul; + } + + /// + /// merges separated hangeul into complete hangeul. (Example: {[0]: "ㄱ", [1]: "ㅏ", [2]: " "} => "가"}) + /// 자모로 쪼개진 한글을 합쳐진 한글로 반환합니다. + /// + /// separated Hangeul. + /// Returns complete Hangeul Character. + public static string Merge(Hashtable separatedHangeul){ + + int firstConsonantIndex; // (ex) 2 + int middleVowelIndex; // (ex) 2 + int lastConsonantIndex; // (ex) 21 + + char firstConsonant = ((string)separatedHangeul[0])[0]; // (ex) "ㄴ" + char middleVowel = ((string)separatedHangeul[1])[0]; // (ex) "ㅑ" + char lastConsonant = ((string)separatedHangeul[2])[0]; // (ex) "ㅇ" + + if (firstConsonant == ' ') {firstConsonant = 'ㅇ';} + + firstConsonantIndex = FIRST_CONSONANTS.IndexOf(firstConsonant); // 초성 인덱스 + middleVowelIndex = MIDDLE_VOWELS.IndexOf(middleVowel); // 중성 인덱스 + lastConsonantIndex = LAST_CONSONANTS.IndexOf(lastConsonant); // 종성 인덱스 + + int mergedCode = HANGEUL_UNICODE_START + (firstConsonantIndex * 21 + middleVowelIndex) * 28 + lastConsonantIndex; + + string result = Convert.ToChar(mergedCode).ToString(); + Debug.Print("Hangeul merged: " + $"{firstConsonant} + {middleVowel} + {lastConsonant} = " + result); + return result; + } + + /// + /// Conducts phoneme variation with two characters input.
※ This method is for only when there are more than one characters, so when there is single character only, Please use Variate(string character). + ///

두 글자를 입력받아 음운변동을 진행합니다.
※ 두 글자 이상이 아닌 단일 글자에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(string character) 메소드를 사용해야 합니다. + ///
+ /// Separated table of first target. + ///
첫 번째 글자를 분리한 해시테이블 + ///

(Example: {[0]="ㅁ", [1]="ㅜ", [2]="ㄴ"} - 문) + /// + /// Separated table of second target. + ///
두 번째 글자를 분리한 해시테이블 + ///

(Example: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + /// + /// 0: returns result of first target character only. + ///
1: returns result of second target character only.
else: returns result of both target characters.
+ ///
0: 첫 번째 타겟 글자의 음운변동 결과만 반환합니다. + ///
1: 두 번째 타겟 글자의 음운변동 결과만 반환합니다.
나머지 값: 두 타겟 글자의 음운변동 결과를 모두 반환합니다.
+ ///
(Example(0): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
(Example(1): {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
(Example(-1): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + /// + /// Example: when returnCharIndex = 0: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
Example: when returnCharIndex = 1: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
Example: when returnCharIndex = -1: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + ///
+ private static Hashtable Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex = -1) { + + string firstLastConsonant = (string)firstCharSeparated[2]; // 문래 에서 ㄴ, 맑다 에서 ㄺ + string nextFirstConsonant = (string)nextCharSeparated[0]; // 문래 에서 ㄹ, 맑다 에서 ㄷ + + // 1. 연음 적용 + ㅎ탈락 + if ((!firstLastConsonant.Equals(" ")) && nextFirstConsonant.Equals("ㅎ")) { + if (basicSounds.Contains(firstLastConsonant)) { + // 착하다 = 차카다 + nextFirstConsonant = (string)aspirateSounds[basicSounds[firstLastConsonant]]; + firstLastConsonant = " "; + } else { + // 뻔한 = 뻔안 (아래에서 연음 적용되서 뻐난 됨) + nextFirstConsonant = "ㅇ"; + } + } + + if (nextFirstConsonant.Equals("ㅇ") && (! firstLastConsonant.Equals(" "))) { + // ㄳ ㄵ ㄶ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅄ 일 경우에도 분기해서 연음 적용 + if (firstLastConsonant.Equals("ㄳ")) { + firstLastConsonant = "ㄱ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄵ")) { + firstLastConsonant = "ㄴ"; + nextFirstConsonant = "ㅈ"; + } + else if (firstLastConsonant.Equals("ㄶ")) { + firstLastConsonant = "ㄴ"; + nextFirstConsonant = "ㅎ"; + } + else if (firstLastConsonant.Equals("ㄺ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㄱ"; + } + else if (firstLastConsonant.Equals("ㄼ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅂ"; + } + else if (firstLastConsonant.Equals("ㄽ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄾ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅌ"; + } + else if (firstLastConsonant.Equals("ㄿ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅍ"; + } + else if (firstLastConsonant.Equals("ㅀ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅎ"; + } + else if (firstLastConsonant.Equals("ㅄ")) { + firstLastConsonant = "ㅂ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄻ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅁ"; + } + else if (firstLastConsonant.Equals("ㅇ") && nextFirstConsonant.Equals("ㅇ")) { + // Do nothing + } + else { + // 겹받침 아닐 때 연음 + nextFirstConsonant = firstLastConsonant; + firstLastConsonant = " "; + } + } + + + // 1. 유기음화 및 ㅎ탈락 1 + if (firstLastConsonant.Equals("ㅎ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㅎ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 낳다 = 나타 + firstLastConsonant = " "; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + else if (firstLastConsonant.Equals("ㅎ") && (!nextFirstConsonant.Equals("ㅅ")) && nextFirstConsonant.Equals("ㅇ")) { + // ㅎ으로 끝나고 다음 소리가 없으면 / ex) 낳아 = 나아 + firstLastConsonant = " "; + } + else if (firstLastConsonant.Equals("ㄶ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㄶ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 많다 = 만타 + firstLastConsonant = "ㄴ"; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + else if (firstLastConsonant.Equals("ㅀ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㅀ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 끓다 = 끌타 + firstLastConsonant = "ㄹ"; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + + + + + // 2-1. 된소리되기 1 + if ((firstLastConsonant.Equals("ㄳ") || firstLastConsonant.Equals("ㄵ") || firstLastConsonant.Equals("ㄽ") || firstLastConsonant.Equals("ㄾ") || firstLastConsonant.Equals("ㅄ") || firstLastConsonant.Equals("ㄼ") || firstLastConsonant.Equals("ㄺ") || firstLastConsonant.Equals("ㄿ")) && basicSounds.Contains(nextFirstConsonant)) { + // [ㄻ, (ㄶ, ㅀ)<= 유기음화에 따라 예외] 제외한 겹받침으로 끝나고 다음 소리가 예사소리이면 + nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + } + + // 3. 첫 번째 글자의 자음군단순화 및 평파열음화(음절의 끝소리 규칙) + if (firstLastConsonant.Equals("ㄽ") || firstLastConsonant.Equals("ㄾ") || firstLastConsonant.Equals("ㄼ")) { + firstLastConsonant = "ㄹ"; + } else if (firstLastConsonant.Equals("ㄵ") || firstLastConsonant.Equals("ㅅ") || firstLastConsonant.Equals("ㅆ") || firstLastConsonant.Equals("ㅈ") || firstLastConsonant.Equals("ㅉ") || firstLastConsonant.Equals("ㅊ") || firstLastConsonant.Equals("ㅌ")) { + firstLastConsonant = "ㄷ"; + } else if (firstLastConsonant.Equals("ㅃ") || firstLastConsonant.Equals("ㅍ") || firstLastConsonant.Equals("ㄿ") || firstLastConsonant.Equals("ㅄ")) { + firstLastConsonant = "ㅂ"; + } else if (firstLastConsonant.Equals("ㄲ") || firstLastConsonant.Equals("ㅋ") || firstLastConsonant.Equals("ㄺ") || firstLastConsonant.Equals("ㄳ")) { + firstLastConsonant = "ㄱ"; + } else if (firstLastConsonant.Equals("ㄻ")) { + firstLastConsonant = "ㅁ"; + } + + + + // 2-1. 된소리되기 2 + if (basicSounds.Contains(firstLastConsonant) && basicSounds.Contains(nextFirstConsonant)) { + // 예사소리로 끝나고 다음 소리가 예사소리이면 / ex) 닭장 = 닥짱 + nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + } + // else if ((firstLastConsonant.Equals("ㄹ")) && (basicSounds.Contains(nextFirstConsonant))){ + // // ㄹ로 끝나고 다음 소리가 예사소리이면 / ex) 솔직 = 솔찍 + // // 본래 관형형 어미 (으)ㄹ과 일부 한자어에서만 일어나는 변동이나, 워낙 사용되는 빈도가 많아서 기본으로 적용되게 해 두 + // // 려 했으나 좀 아닌 것 같아서 보류하기로 함 + // nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + // } + + // 1. 유기음화 2 + if (basicSounds.Contains(firstLastConsonant) && nextFirstConsonant.Equals("ㅎ")) { + // ㄱㄷㅂㅈ(+ㅅ)로 끝나고 다음 소리가 ㅎ이면 / ex) 축하 = 추카, 옷하고 = 오타고 + // ㅅ은 미리 평파열음화가 진행된 것으로 보고 ㄷ으로 간주한다 + nextFirstConsonant = (string)aspirateSounds[basicSounds[firstLastConsonant]]; + firstLastConsonant = " "; + } + else if (nextFirstConsonant.Equals("ㅎ")) { + nextFirstConsonant = "ㅇ"; + } + + if ((!firstLastConsonant.Equals("")) && nextFirstConsonant.Equals("ㅇ") && (!firstLastConsonant.Equals("ㅇ"))) { + // 연음 2 + nextFirstConsonant = firstLastConsonant; + firstLastConsonant = " "; + } + + + // 4. 비음화 + if (firstLastConsonant.Equals("ㄱ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 막론 = 망론 >> 망논 + firstLastConsonant = "ㅇ"; + } else if (firstLastConsonant.Equals("ㄷ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 슬롯머신 = 슬론머신 + firstLastConsonant = "ㄴ"; + } else if (firstLastConsonant.Equals("ㅂ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 밥먹자 = 밤먹자 >> 밤먹짜 + firstLastConsonant = "ㅁ"; + } + + // 4'. 유음화 + if (firstLastConsonant.Equals("ㄴ") && nextFirstConsonant.Equals("ㄹ")) { + // ex) 만리 = 말리 + firstLastConsonant = "ㄹ"; + } else if (firstLastConsonant.Equals("ㄹ") && nextFirstConsonant.Equals("ㄴ")) { + // ex) 칼날 = 칼랄 + nextFirstConsonant = "ㄹ"; + } + + // 4''. ㄹ비음화 + if (nextFirstConsonant.Equals("ㄹ") && nasalSounds.Contains(nextFirstConsonant)) { + // ex) 담력 = 담녁 + firstLastConsonant = "ㄴ"; + } + + + // 4'''. 자음동화 + if (firstLastConsonant.Equals("ㄴ") && nextFirstConsonant.Equals("ㄱ")) { + // ex) ~라는 감정 = ~라능 감정 + firstLastConsonant = "ㅇ"; + } + + // return results + if (returnCharIndex == 0) { + // return result of first target character + return new Hashtable() { + [0] = firstCharSeparated[0], + [1] = firstCharSeparated[1], + [2] = firstLastConsonant + }; + } else if (returnCharIndex == 1) { + // return result of second target character + return new Hashtable() { + [0] = nextFirstConsonant, + [1] = nextCharSeparated[1], + [2] = nextCharSeparated[2] + }; + } else { + // 두 글자 다 반환 + return new Hashtable() { + [0] = firstCharSeparated[0], + [1] = firstCharSeparated[1], + [2] = firstLastConsonant, + [3] = nextFirstConsonant, + [4] = nextCharSeparated[1], + [5] = nextCharSeparated[2] + }; + } + } + + /// + /// Conducts phoneme variation with one character input.
※ This method is only for when there are single character, so when there are more than one character, Please use Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1). + ///

단일 글자를 입력받아 음운변동을 진행합니다.
※ 단일 글자가 아닌 두 글자 이상에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1) 메소드를 사용해야 합니다. + ///
+ /// String of single target. + ///
음운변동시킬 단일 글자. + /// + /// (Example(삵): {[0]="ㅅ", [1]="ㅏ", [2]="ㄱ"} - 삭) + /// + public static Hashtable Variate(string character) { + /// 맨 끝 노트에서 음운변동 적용하는 함수 + /// 자음군 단순화와 평파열음화 + Hashtable separated = Separate(character); + + if (separated[2].Equals("ㄽ") || separated[2].Equals("ㄾ") || separated[2].Equals("ㄼ") || separated[2].Equals("ㅀ")) { + separated[2] = "ㄹ"; + } + else if (separated[2].Equals("ㄵ") || separated[2].Equals("ㅅ") || separated[2].Equals("ㅆ") || separated[2].Equals("ㅈ") || separated[2].Equals("ㅉ") || separated[2].Equals("ㅊ")) { + separated[2] = "ㄷ"; + } + else if (separated[2].Equals("ㅃ") || separated[2].Equals("ㅍ") || separated[2].Equals("ㄿ") || separated[2].Equals("ㅄ")) { + separated[2] = "ㅂ"; + } + else if (separated[2].Equals("ㄲ") || separated[2].Equals("ㅋ") || separated[2].Equals("ㄺ") || separated[2].Equals("ㄳ")) { + separated[2] = "ㄱ"; + } + else if (separated[2].Equals("ㄻ")) { + separated[2] = "ㅁ"; + } + else if (separated[2].Equals("ㄶ")) { + separated[2] = "ㄴ"; + } + + + return separated; + + } + /// + /// Conducts phoneme variation with one character input.
※ This method is only for when there are single character, so when there are more than one character, Please use Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1). + ///

단일 글자의 분리된 값을 입력받아 음운변동을 진행합니다.
※ 단일 글자가 아닌 두 글자 이상에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1) 메소드를 사용해야 합니다. + ///
+ /// Separated table of target. + ///
글자를 분리한 해시테이블 + /// + /// (Example({[0]="ㅅ", [1]="ㅏ", [2]="ㄺ"}): {[0]="ㅅ", [1]="ㅏ", [2]="ㄱ"} - 삭) + /// + private static Hashtable Variate(Hashtable separated) { + /// 맨 끝 노트에서 음운변동 적용하는 함수 + + if (separated[2].Equals("ㄽ") || separated[2].Equals("ㄾ") || separated[2].Equals("ㄼ") || separated[2].Equals("ㅀ")) { + separated[2] = "ㄹ"; + } + else if (separated[2].Equals("ㄵ") || separated[2].Equals("ㅅ") || separated[2].Equals("ㅆ") || separated[2].Equals("ㅈ") || separated[2].Equals("ㅉ") || separated[2].Equals("ㅊ")) { + separated[2] = "ㄷ"; + } + else if (separated[2].Equals("ㅃ") || separated[2].Equals("ㅍ") || separated[2].Equals("ㄿ") || separated[2].Equals("ㅄ")) { + separated[2] = "ㅂ"; + } + else if (separated[2].Equals("ㄲ") || separated[2].Equals("ㅋ") || separated[2].Equals("ㄺ") || separated[2].Equals("ㄳ")) { + separated[2] = "ㄱ"; + } + else if (separated[2].Equals("ㄻ")) { + separated[2] = "ㅁ"; + } + else if (separated[2].Equals("ㄶ")) { + separated[2] = "ㄴ"; + } + + return separated; + } + + /// + /// Conducts phoneme variation with two characters input.
※ This method is for only when there are more than one characters, so when there is single character only, Please use Variate(string character). + ///

두 글자를 입력받아 음운변동을 진행합니다.
※ 두 글자 이상이 아닌 단일 글자에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(string character) 메소드를 사용해야 합니다. + ///
+ /// String of first target. + ///
첫 번째 글자. + ///

(Example: 문) + /// + /// String of second target. + ///
두 번째 글자. + ///

(Example: 래) + /// + /// 0: returns result of first target character only. + ///
1: returns result of second target character only.
else: returns result of both target characters.
+ ///
0: 첫 번째 타겟 글자의 음운변동 결과만 반환합니다. + ///
1: 두 번째 타겟 글자의 음운변동 결과만 반환합니다.
나머지 값: 두 타겟 글자의 음운변동 결과를 모두 반환합니다.
+ ///
(Example(0): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
(Example(1): {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
(Example(-1): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + /// + /// Example: when returnCharIndex = 0: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
Example: when returnCharIndex = 1: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
Example: when returnCharIndex = -1: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + ///
+ private static Hashtable Variate(string firstChar, string nextChar, int returnCharIndex = 0) { + // 글자 넣어도 쓸 수 있음 + + Hashtable firstCharSeparated = Separate(firstChar); + Hashtable nextCharSeparated = Separate(nextChar); + return Variate(firstCharSeparated, nextCharSeparated, returnCharIndex); + } + + /// + /// Conducts phoneme variation automatically with prevNeighbour, note, nextNeighbour. + ///

prevNeighbour, note, nextNeighbour를 입력받아 자동으로 음운 변동을 진행합니다. + ///
+ /// Note of prev note, if exists(otherwise null). + ///
이전 노트 혹은 null. + ///

(Example: Note with lyric '춘') + /// + /// Note of current note. + ///
현재 노트. + ///

(Example: Note with lyric '향') + /// + /// Note of next note, if exists(otherwise null). + ///
다음 노트 혹은 null. + ///

(Example: null) + /// + /// Returns phoneme variation result of prevNote, currentNote, nextNote. + ///
이전 노트, 현재 노트, 다음 노트의 음운변동 결과를 반환합니다. + ///
Example: 춘 [향] null: {[0]="ㅊ", [1]="ㅜ", [2]=" ", [3]="ㄴ", [4]="ㅑ", [5]="ㅇ", [6]="null", [7]="null", [8]="null"} [추 냥 null] + ///
+ public static Hashtable Variate(Note? prevNeighbour, Note note, Note? nextNeighbour) { + // prevNeighbour와 note와 nextNeighbour의 음원변동된 가사를 반환 + // prevNeighbour : VV 정렬에 사용 + // nextNeighbour : VC 정렬에 사용 + // 뒤의 노트가 없으면 리턴되는 값의 6~8번 인덱스가 null로 채워진다. + + /// whereYeonEum : 발음기호 .을 사용하기 위한 변수 + /// .을 사용하면 앞에서 단어가 끝났다고 간주하고, 끝소리에 음운변동을 적용한 후 연음합니다. + /// ex) 무 릎 위 [무르퓌] 무 릎. 위[무르뷔] + /// + /// -1 : 해당사항 없음 + /// 0 : 이전 노트를 연음하지 않음 + /// 1 : 현재 노트를 연음하지 않음 + int whereYeonEum = -1; + + string?[] lyrics = new string?[] { prevNeighbour?.lyric, note.lyric, nextNeighbour?.lyric }; + + if (!IsHangeul(lyrics[0])) { + // 앞노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[0] != null) {lyrics[0] = null;} + } else if (!IsHangeul(lyrics[2])) { + // 뒤노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[2] != null) {lyrics[2] = null;} + } + if ((lyrics[0] != null) && lyrics[0].StartsWith('!')) { + /// 앞노트 ! 기호로 시작함 ex) [!냥]냥냥 + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null냥냥 + } + if ((lyrics[1] != null) && lyrics[1].StartsWith('!')) { + /// 중간노트 ! 기호로 시작함 ex) 냥[!냥]냥 + /// 음운변동 미적용 + lyrics[1] = lyrics[1].TrimStart('!'); + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null[!냥]냥 + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사도 없는 걸로 간주함 null[!냥]null + } + if ((lyrics[2] != null) && lyrics[2].StartsWith('!')) { + /// 뒤노트 ! 기호로 시작함 ex) 냥냥[!냥] + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사 없는 걸로 간주함 냥냥b + } + + if ((lyrics[0] != null) && lyrics[0].EndsWith('.')) { + /// 앞노트 . 기호로 끝남 ex) [냥.]냥냥 + lyrics[0] = lyrics[0].TrimEnd('.'); + whereYeonEum = 0; + } + if ((lyrics[1] != null) && lyrics[1].EndsWith('.')) { + /// 중간노트 . 기호로 끝남 ex) 냥[냥.]냥 + /// 음운변동 없이 연음만 적용 + lyrics[1] = lyrics[1].TrimEnd('.'); + whereYeonEum = 1; + } + if ((lyrics[2] != null) && lyrics[2].EndsWith('.')) { + /// 뒤노트 . 기호로 끝남 ex) 냥냥[냥.] + /// 중간노트의 발음에 관여하지 않으므로 간단히 . 만 지워주면 된다 + lyrics[2] = lyrics[2].TrimEnd('.'); + } + + // 음운변동 적용 -- + if ((lyrics[0] == null) && (lyrics[2] != null)) { + /// 앞이 없고 뒤가 있음 + /// null[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + Hashtable thisNoteSeparated = Variate(Variate(lyrics[1]), Separate(lyrics[2]), -1); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else { + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + + if (IsHangeul(lyrics[2])) { + Hashtable thisNoteSeparated = Variate(lyrics[1], lyrics[2], -1); // 현글자 뒤글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + } + else { + Hashtable thisNoteSeparated = Variate(lyrics[1]); + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); + result.Add(7, "null"); + result.Add(8, "null"); + } + + + return result; + } + } + else if ((lyrics[0] != null) && (lyrics[2] == null)) { + /// 앞이 있고 뒤는 없음 + /// 냥[냥]null + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1)); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1)); // 첫 글자와 현 글자 / 앞글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1)); // 첫 글자와 현 글자 / 뒷글자 없으니까 글자 혼자 있는걸로 음운변동 한 번 더 시키기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + } + else if ((lyrics[0] != null) && (lyrics[2] != null)) { + /// 앞도 있고 뒤도 있음 + /// 냥[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 / 무 [릎.] 위 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1), Separate(lyrics[2]), -1);// 현글자와 다음 글자 / 현 글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 / 릎. [위] 놓 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1), Separate(lyrics[2]), -1); // 현 글자와 뒤 글자 / 앞글자 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1), Separate(lyrics[2]), -1); + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + } + else { + /// 앞이 없고 뒤도 없음 + /// null[냥]null + + Hashtable result = new Hashtable() { + // 첫 글자 >> 비어 있음 + [0] = "null", + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1]); // 현 글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + + result.Add(6, "null"); // 뒤 글자 비어있음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + } + + /// + /// (for diffsinger phonemizer) + /// Conducts phoneme variation automatically with prevNeighbour, note, nextNeighbour. + ///

prevNeighbour, note, nextNeighbour를 입력받아 자동으로 음운 변동을 진행합니다. + ///
+ /// lyric String of prev note, if exists(otherwise null). + ///
이전 가사 혹은 null. + ///

(Example: lyric String with lyric '춘') + /// + /// lyric String of current note. + ///
현재 가사. + ///

(Example: Note with lyric '향') + /// + /// lyric String of next note, if exists(otherwise null). + ///
다음 가사 혹은 null. + ///

(Example: null) + /// + /// Returns phoneme variation result of prevNote, currentNote, nextNote. + ///
이전 노트, 현재 노트, 다음 노트의 음운변동 결과를 반환합니다. + ///
Example: 춘 [향] null: {[0]="ㅊ", [1]="ㅜ", [2]=" ", [3]="ㄴ", [4]="ㅑ", [5]="ㅇ", [6]="null", [7]="null", [8]="null"} [추 냥 null] + ///
+ public static String Variate(String? prevNeighbour, String note, String? nextNeighbour) { + // prevNeighbour와 note와 nextNeighbour의 음원변동된 가사를 반환 + // prevNeighbour : VV 정렬에 사용 + // nextNeighbour : VC 정렬에 사용 + // 뒤의 노트가 없으면 리턴되는 값의 6~8번 인덱스가 null로 채워진다. + + /// whereYeonEum : 발음기호 .을 사용하기 위한 변수 + /// .을 사용하면 앞에서 단어가 끝났다고 간주하고, 끝소리에 음운변동을 적용한 후 연음합니다. + /// ex) 무 릎 위 [무르퓌] 무 릎. 위[무르뷔] + /// + /// -1 : 해당사항 없음 + /// 0 : 이전 노트를 연음하지 않음 + /// 1 : 현재 노트를 연음하지 않음 + int whereYeonEum = -1; + + string?[] lyrics = new string?[] { prevNeighbour, note, nextNeighbour}; + + if (!IsHangeul(lyrics[0])) { + // 앞노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[0] != null) {lyrics[0] = null;} + } else if (!IsHangeul(lyrics[2])) { + // 뒤노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[2] != null) {lyrics[2] = null;} + } + if ((lyrics[0] != null) && lyrics[0].StartsWith('!')) { + /// 앞노트 ! 기호로 시작함 ex) [!냥]냥냥 + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null냥냥 + } + if ((lyrics[1] != null) && lyrics[1].StartsWith('!')) { + /// 중간노트 ! 기호로 시작함 ex) 냥[!냥]냥 + /// 음운변동 미적용 + lyrics[1] = lyrics[1].TrimStart('!'); + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null[!냥]냥 + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사도 없는 걸로 간주함 null[!냥]null + } + if ((lyrics[2] != null) && lyrics[2].StartsWith('!')) { + /// 뒤노트 ! 기호로 시작함 ex) 냥냥[!냥] + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사 없는 걸로 간주함 냥냥b + } + + if ((lyrics[0] != null) && lyrics[0].EndsWith('.')) { + /// 앞노트 . 기호로 끝남 ex) [냥.]냥냥 + lyrics[0] = lyrics[0].TrimEnd('.'); + whereYeonEum = 0; + } + if ((lyrics[1] != null) && lyrics[1].EndsWith('.')) { + /// 중간노트 . 기호로 끝남 ex) 냥[냥.]냥 + /// 음운변동 없이 연음만 적용 + lyrics[1] = lyrics[1].TrimEnd('.'); + whereYeonEum = 1; + } + if ((lyrics[2] != null) && lyrics[2].EndsWith('.')) { + /// 뒤노트 . 기호로 끝남 ex) 냥냥[냥.] + /// 중간노트의 발음에 관여하지 않으므로 간단히 . 만 지워주면 된다 + lyrics[2] = lyrics[2].TrimEnd('.'); + } + + // 음운변동 적용 -- + if ((lyrics[0] == null) && (lyrics[2] != null)) { + /// 앞이 없고 뒤가 있음 + /// null[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + Hashtable thisNoteSeparated = Variate(Variate(lyrics[1]), Separate(lyrics[2]), -1); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1], lyrics[2], -1); // 현글자 뒤글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 없음 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else if ((lyrics[0] != null) && (lyrics[2] == null)) { + /// 앞이 있고 뒤는 없음 + /// 냥[냥]null + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1)); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1)); // 첫 글자와 현 글자 / 앞글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1)); // 첫 글자와 현 글자 / 뒷글자 없으니까 글자 혼자 있는걸로 음운변동 한 번 더 시키기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else if ((lyrics[0] != null) && (lyrics[2] != null)) { + /// 앞도 있고 뒤도 있음 + /// 냥[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 / 무 [릎.] 위 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1), Separate(lyrics[2]), -1);// 현글자와 다음 글자 / 현 글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 / 릎. [위] 놓 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1), Separate(lyrics[2]), -1); // 현 글자와 뒤 글자 / 앞글자 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1), Separate(lyrics[2]), -1); + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else { + /// 앞이 없고 뒤도 없음 + /// null[냥]null + Hashtable result = new Hashtable() { + // 첫 글자 >> 비어 있음 + [0] = "null", + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1]); // 현 글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + + result.Add(6, "null"); // 뒤 글자 비어있음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5] + }); + } + } + + public static Note[] ChangeLyric(Note[] group, string lyric) { + // for ENUNU Phonemizer + var oldNote = group[0]; + group[0] = new Note { + lyric = lyric, + phoneticHint = oldNote.phoneticHint, + tone = oldNote.tone, + position = oldNote.position, + duration = oldNote.duration, + phonemeAttributes = oldNote.phonemeAttributes, + }; + return group; + } + public static void RomanizeNotes(Note[][] groups, Dictionary firstConsonants, Dictionary vowels, Dictionary lastConsonants, string semivowelSeparator=" ") { + // for ENUNU Phonemizer + + int noteIdx = 0; + Note[] currentNote; + Note[]? prevNote = null; + Note[]? nextNote; + + Note? prevNote_; + Note? nextNote_; + + + List ResultLyrics = new List(); + foreach (Note[] group in groups){ + currentNote = groups[noteIdx]; + if (groups.Length > noteIdx + 1 && IsHangeul(groups[noteIdx + 1][0].lyric)) { + nextNote = groups[noteIdx + 1]; + } + else { + nextNote = null; + } + + if (prevNote != null) { + prevNote_ = prevNote[0]; + if (prevNote[0].position + prevNote.Sum(note => note.duration) != currentNote[0].position) { + prevNote_ = null; + } + } + else {prevNote_ = null;} + + if (nextNote != null) { + nextNote_ = nextNote[0]; + + if (nextNote[0].position != currentNote[0].position + currentNote.Sum(note => note.duration)) { + nextNote_ = null; + } + } + else{nextNote_ = null;} + + string lyric = ""; + + if (! IsHangeul(currentNote[0].lyric)){ + ResultLyrics.Add(currentNote[0].lyric); + prevNote = currentNote; + noteIdx++; + continue; + } + + Hashtable lyricSeparated = Variate(prevNote_, currentNote[0], nextNote_); + lyric += firstConsonants[(string)lyricSeparated[3]][0]; + if (vowels[(string)lyricSeparated[4]][1] != "") { + // this vowel contains semivowel + lyric += semivowelSeparator + vowels[(string)lyricSeparated[4]][1] + vowels[(string)lyricSeparated[4]][2]; + } + else{ + lyric += " " + vowels[(string)lyricSeparated[4]][2]; + } + + lyric += lastConsonants[(string)lyricSeparated[5]][0]; + + ResultLyrics.Add(lyric.Trim()); + + prevNote = currentNote; + + noteIdx++; + } + Enumerable.Zip(groups, ResultLyrics.ToArray(), ChangeLyric).Last(); + } + + /// + /// abstract class for Ini Management + /// To use, child phonemizer should implement this class(BaseIniManager) with its own setting values! + /// + public abstract class BaseIniManager { + protected USinger singer; + protected Hashtable iniSetting = new Hashtable(); + protected string iniFileName; + protected string filePath; + protected List blocks; + + public BaseIniManager() { } + + /// + /// if no [iniFileName] in Singer Directory, it makes new [iniFileName] with [iniFile]]. + /// + /// + /// + /// + public void Initialize(USinger singer, string iniFileName, Hashtable defaultIniSetting) { + this.singer = singer; + this.iniFileName = iniFileName; + iniSetting = defaultIniSetting; + filePath = Path.Combine(singer.Location, iniFileName); + try { + using (StreamReader reader = new StreamReader(filePath, singer.TextFileEncoding)){ + List blocks = Ini.ReadBlocks(reader, filePath, @"\[\w+\]"); + if (blocks.Count == 0) { + throw new IOException($"[{iniFileName}] is empty."); + } + this.blocks = blocks; + IniSetUp(iniSetting); // you can override IniSetUp() to use. + }; + } + catch (IOException e) { + Log.Error(e, $"failed to read {iniFileName}, Making new {iniFileName}..."); + using (StreamWriter writer = new StreamWriter(filePath)){ + iniSetting = defaultIniSetting; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e_){ + Log.Error(e_, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + }; + using (StreamReader reader = new StreamReader(filePath)){ + List blocks = Ini.ReadBlocks(reader, filePath, @"\[\w+\]"); + this.blocks = blocks; + }; + } + } + + /// + /// you can override this method with your own values. + /// !! when implement this method, you have to use [SetOrReadThisValue(string sectionName, string keyName, bool/string/int/double value)] when setting or reading values. + /// (ex) + /// SetOrReadThisValue("sectionName", "keyName", true); + /// + protected virtual void IniSetUp(Hashtable iniSetting) { + } + + /// + /// for file writing, converts iniSetting to string. + /// + /// + protected string ConvertSettingsToString(){ + string result = ""; + foreach (DictionaryEntry section in iniSetting) { + result += $"[{section.Key}]\n"; + foreach (DictionaryEntry key in (Hashtable)iniSetting[section.Key]){ + result += $"{key.Key}={key.Value}\n"; + } + } + return result; + } + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid bool vaule, nothing happens. But if there's no valid bool value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, bool 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// /// + protected void SetOrReadThisValue(string sectionName, string keyName, bool defaultValue, out bool resultValue) { + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + + resultValue = result.ToLower() == "true" ? true : false; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString()); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue.ToString(); + } + resultValue = defaultValue; + } + } + else{ + using (StreamWriter writer = new StreamWriter(filePath)) { + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString().ToLower()); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + }; + } + } + + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid string vaule, nothing happens. But if there's no valid string value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, string 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected string SetOrReadThisValue(string sectionName, string keyName, string defaultValue) { + string resultValue; + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + resultValue = result; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue; + } + resultValue = defaultValue; + } + } + else{ + StreamWriter writer = new StreamWriter(filePath); + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + } + return resultValue; + } + + /// + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid int vaule, nothing happens. But if there's no valid int value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, int 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected void SetOrReadThisValue(string sectionName, string keyName, int defaultValue, out int resultValue) { + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null && int.TryParse(result, out var resultInt)) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + resultValue = resultInt; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString()); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue.ToString(); + } + resultValue = defaultValue; + } + } + else{ + StreamWriter writer = new StreamWriter(filePath); + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + } + } + } + /// + /// Data class used to deserialize yaml dictionary. + /// (for user-defined Korean jamo dictionary) + /// + public class JamoDictionary{ + public FirstConsonantData[] firstConsonants; + public PlainVowelData[] plainVowels; + public SemivowelData[] semivowels; + public FinalConsonantData[] finalConsonants; + public JamoDictionary() { } + public JamoDictionary(FirstConsonantData[] firstConsonants, PlainVowelData[] plainVowels, SemivowelData[] semivowels, FinalConsonantData[] finalConsonants){ + this.firstConsonants = firstConsonants; + this.plainVowels = plainVowels; + this.semivowels = semivowels; + this.finalConsonants = finalConsonants; + } + public struct FirstConsonantData { + public string grapheme; // ㄱ + public string phoneme; // g + public FirstConsonantData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + + public struct PlainVowelData { + public string grapheme; // ㅏ + public string phoneme; // a + + public PlainVowelData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + public struct SemivowelData { + public string grapheme; // w + public string phoneme; // w + + public SemivowelData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + + public struct FinalConsonantData { + public string grapheme; // ㄱ + public string phoneme; // K + public FinalConsonantData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + } + } + +} \ No newline at end of file