From 4c207a352db1bce99d7f8c5002ecdf03e10ea622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=ED=98=84=20=EA=B9=80?= Date: Tue, 26 Dec 2023 11:14:28 +0900 Subject: [PATCH 1/6] Add KoreanPhonemizerUtil --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 1047 +++++++++++++++++++++++++ 1 file changed, 1047 insertions(+) create mode 100644 OpenUtau.Core/KoreanPhonemizerUtil.cs diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs new file mode 100644 index 000000000..dd646aa30 --- /dev/null +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -0,0 +1,1047 @@ +using System; +using System.Collections; +using System.Diagnostics; +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 - ㄱ/ㄷ/ㅂ/ㅅ/ㅈ. + ///

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

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

비음 테이블입니다. + ///
+ 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("ㅇ") && 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" + }; + + 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 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] + }); + } + } + } +} \ No newline at end of file From 38ce7350ff7fb468594248cdac723622973c0d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=ED=98=84=20=EA=B9=80?= Date: Wed, 27 Dec 2023 17:08:03 +0900 Subject: [PATCH 2/6] Moved BaseIniManager here for ENUNU KO phonemizer --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 880 ++++++++++++++++++++++++++ 1 file changed, 880 insertions(+) diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index dd646aa30..21ba733a4 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -1,6 +1,11 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using OpenUtau.Core.Ustx; using static OpenUtau.Api.Phonemizer; namespace OpenUtau.Core { @@ -1043,5 +1048,880 @@ public static String Variate(String? prevNeighbour, String note, String? nextNei }); } } + + 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) { + // 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 = ""; + Hashtable lyricSeparated = Variate(prevNote_, currentNote[0], nextNote_); + lyric += firstConsonants[(string)lyricSeparated[3]][0]; + lyric += vowels[(string)lyricSeparated[4]][0]; + lyric += lastConsonants[(string)lyricSeparated[5]][0]; + ResultLyrics.Add(lyric); + + prevNote = currentNote; + + noteIdx++; + } + Enumerable.Zip(groups, ResultLyrics.ToArray(), ChangeLyric).Last(); + } + + /// + /// abstract class for BaseIniManager(https://github.com/Enichan/Ini/blob/master/Ini.cs) + /// + /// Note: This class will be NOT USED when implementing child korean phonemizers. This class is only for BaseIniManager. + /// + /// + public abstract class IniParser + { + public struct IniValue { + private static bool TryParseInt(string text, out int value) { + int res; + if (Int32.TryParse(text, + System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, + out res)) { + value = res; + return true; + } + value = 0; + return false; + } + private static bool TryParseDouble(string text, out double value) { + double res; + if (Double.TryParse(text, + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out res)) { + value = res; + return true; + } + value = Double.NaN; + return false; + } + public string Value; + public IniValue(object value) { + var formattable = value as IFormattable; + if (formattable != null) { + Value = formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture); + } else { + Value = value != null ? value.ToString() : null; + } + } + public IniValue(string value) { + Value = value; + } + public bool ToBool(bool valueIfInvalid = false) { + bool res; + if (TryConvertBool(out res)) { + return res; + } + return valueIfInvalid; + } + public bool TryConvertBool(out bool result) { + if (Value == null) { + result = default(bool); + return false; + } + var boolStr = Value.Trim().ToLowerInvariant(); + if (boolStr == "true") { + result = true; + return true; + } else if (boolStr == "false") { + result = false; + return true; + } + result = default(bool); + return false; + } + public int ToInt(int valueIfInvalid = 0) { + int res; + if (TryConvertInt(out res)) { + return res; + } + return valueIfInvalid; + } + public bool TryConvertInt(out int result) { + if (Value == null) { + result = default(int); + return false; + } + if (TryParseInt(Value.Trim(), out result)) { + return true; + } + return false; + } + public double ToDouble(double valueIfInvalid = 0) { + double res; + if (TryConvertDouble(out res)) { + return res; + } + return valueIfInvalid; + } + public bool TryConvertDouble(out double result) { + if (Value == null) { + result = default(double); + return false; ; + } + if (TryParseDouble(Value.Trim(), out result)) { + return true; + } + return false; + } + public string GetString() { + return GetString(true, false); + } + public string GetString(bool preserveWhitespace) { + return GetString(true, preserveWhitespace); + } + public string GetString(bool allowOuterQuotes, bool preserveWhitespace) { + if (Value == null) { + return ""; + } + var trimmed = Value.Trim(); + if (allowOuterQuotes && trimmed.Length >= 2 && trimmed[0] == '"' && trimmed[trimmed.Length - 1] == '"') { + var inner = trimmed.Substring(1, trimmed.Length - 2); + return preserveWhitespace ? inner : inner.Trim(); + } else { + return preserveWhitespace ? Value : Value.Trim(); + } + } + public override string ToString() { + return Value; + } + public static implicit operator IniValue(byte o) { + return new IniValue(o); + } + public static implicit operator IniValue(short o) { + return new IniValue(o); + } + public static implicit operator IniValue(int o) { + return new IniValue(o); + } + public static implicit operator IniValue(sbyte o) { + return new IniValue(o); + } + public static implicit operator IniValue(ushort o) { + return new IniValue(o); + } + public static implicit operator IniValue(uint o) { + return new IniValue(o); + } + public static implicit operator IniValue(float o) { + return new IniValue(o); + } + public static implicit operator IniValue(double o) { + return new IniValue(o); + } + public static implicit operator IniValue(bool o) { + return new IniValue(o); + } + public static implicit operator IniValue(string o) { + return new IniValue(o); + } + private static readonly IniValue _default = new IniValue(); + public static IniValue Default { get { return _default; } } + } + public class IniFile : IEnumerable>, IDictionary { + private Dictionary sections; + public IEqualityComparer StringComparer; + public bool SaveEmptySections; + public IniFile() + : this(DefaultComparer) { + } + public IniFile(IEqualityComparer stringComparer) { + StringComparer = stringComparer; + sections = new Dictionary(StringComparer); + } + public void Save(string path, FileMode mode = FileMode.Create) { + using (var stream = new FileStream(path, mode, FileAccess.Write)) { + Save(stream); + } + } + public void Save(Stream stream) { + using (var writer = new StreamWriter(stream)) { + Save(writer); + } + } + public void Save(StreamWriter writer) { + foreach (var section in sections) { + if (section.Value.Count > 0 || SaveEmptySections) { + writer.WriteLine(string.Format("[{0}]", section.Key.Trim())); + foreach (var kvp in section.Value) { + writer.WriteLine(string.Format("{0}={1}", kvp.Key, kvp.Value)); + } + writer.WriteLine(""); + } + } + } + public void Load(string path, bool ordered = false) { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { + Load(stream, ordered); + } + } + public void Load(Stream stream, bool ordered = false) { + using (var reader = new StreamReader(stream)) { + Load(reader, ordered); + } + } + public void Load(StreamReader reader, bool ordered = false) { + IniSection section = null; + while (!reader.EndOfStream) { + var line = reader.ReadLine(); + if (line != null) { + var trimStart = line.TrimStart(); + if (trimStart.Length > 0) { + if (trimStart[0] == '[') { + var sectionEnd = trimStart.IndexOf(']'); + if (sectionEnd > 0) { + var sectionName = trimStart.Substring(1, sectionEnd - 1).Trim(); + section = new IniSection(StringComparer) { Ordered = ordered }; + sections[sectionName] = section; + } + } else if (section != null && trimStart[0] != ';') { + string key; + IniValue val; + if (LoadValue(line, out key, out val)) { + section[key] = val; + } + } + } + } + } + } + private bool LoadValue(string line, out string key, out IniValue val) { + var assignIndex = line.IndexOf('='); + if (assignIndex <= 0) { + key = null; + val = null; + return false; + } + key = line.Substring(0, assignIndex).Trim(); + var value = line.Substring(assignIndex + 1); + val = new IniValue(value); + return true; + } + public bool ContainsSection(string section) { + return sections.ContainsKey(section); + } + public bool TryGetSection(string section, out IniSection result) { + return sections.TryGetValue(section, out result); + } + bool IDictionary.TryGetValue(string key, out IniSection value) { + return TryGetSection(key, out value); + } + public bool Remove(string section) { + return sections.Remove(section); + } + public IniSection Add(string section, Dictionary values, bool ordered = false) { + return Add(section, new IniSection(values, StringComparer) { Ordered = ordered }); + } + public IniSection Add(string section, IniSection value) { + if (value.Comparer != StringComparer) { + value = new IniSection(value, StringComparer); + } + sections.Add(section, value); + return value; + } + public IniSection Add(string section, bool ordered = false) { + var value = new IniSection(StringComparer) { Ordered = ordered }; + sections.Add(section, value); + return value; + } + void IDictionary.Add(string key, IniSection value) { + Add(key, value); + } + bool IDictionary.ContainsKey(string key) { + return ContainsSection(key); + } + public ICollection Keys { + get { return sections.Keys; } + } + public ICollection Values { + get { return sections.Values; } + } + void ICollection>.Add(KeyValuePair item) { + ((IDictionary)sections).Add(item); + } + public void Clear() { + sections.Clear(); + } + bool ICollection>.Contains(KeyValuePair item) { + return ((IDictionary)sections).Contains(item); + } + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { + ((IDictionary)sections).CopyTo(array, arrayIndex); + } + public int Count { + get { return sections.Count; } + } + bool ICollection>.IsReadOnly { + get { return ((IDictionary)sections).IsReadOnly; } + } + bool ICollection>.Remove(KeyValuePair item) { + return ((IDictionary)sections).Remove(item); + } + public IEnumerator> GetEnumerator() { + return sections.GetEnumerator(); + } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + public IniSection this[string section] { + get { + IniSection s; + if (sections.TryGetValue(section, out s)) { + return s; + } + s = new IniSection(StringComparer); + sections[section] = s; + return s; + } + set { + var v = value; + if (v.Comparer != StringComparer) { + v = new IniSection(v, StringComparer); + } + sections[section] = v; + } + } + public string GetContents() { + using (var stream = new MemoryStream()) { + Save(stream); + stream.Flush(); + var builder = new StringBuilder(Encoding.UTF8.GetString(stream.ToArray())); + return builder.ToString(); + } + } + public static IEqualityComparer DefaultComparer = new CaseInsensitiveStringComparer(); + class CaseInsensitiveStringComparer : IEqualityComparer { + public bool Equals(string x, string y) { + return String.Compare(x, y, true) == 0; + } + public int GetHashCode(string obj) { + return obj.ToLowerInvariant().GetHashCode(); + } + +#if JS + public new bool Equals(object x, object y) { + var xs = x as string; + var ys = y as string; + if (xs == null || ys == null) { + return xs == null && ys == null; + } + return Equals(xs, ys); + } + + public int GetHashCode(object obj) { + if (obj is string) { + return GetHashCode((string)obj); + } + return obj.ToStringInvariant().ToLowerInvariant().GetHashCode(); + } +#endif + } + } + + public class IniSection : IEnumerable>, IDictionary { + private Dictionary values; + + #region Ordered + private List orderedKeys; + + public int IndexOf(string key) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call IndexOf(string) on IniSection: section was not ordered."); + } + return IndexOf(key, 0, orderedKeys.Count); + } + + public int IndexOf(string key, int index) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call IndexOf(string, int) on IniSection: section was not ordered."); + } + return IndexOf(key, index, orderedKeys.Count - index); + } + + public int IndexOf(string key, int index, int count) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call IndexOf(string, int, int) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + if (count < 0) { + throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); + } + if (index + count > orderedKeys.Count) { + throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + var end = index + count; + for (int i = index; i < end; i++) { + if (Comparer.Equals(orderedKeys[i], key)) { + return i; + } + } + return -1; + } + + public int LastIndexOf(string key) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call LastIndexOf(string) on IniSection: section was not ordered."); + } + return LastIndexOf(key, 0, orderedKeys.Count); + } + + public int LastIndexOf(string key, int index) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call LastIndexOf(string, int) on IniSection: section was not ordered."); + } + return LastIndexOf(key, index, orderedKeys.Count - index); + } + + public int LastIndexOf(string key, int index, int count) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call LastIndexOf(string, int, int) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + if (count < 0) { + throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); + } + if (index + count > orderedKeys.Count) { + throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + var end = index + count; + for (int i = end - 1; i >= index; i--) { + if (Comparer.Equals(orderedKeys[i], key)) { + return i; + } + } + return -1; + } + + public void Insert(int index, string key, IniValue value) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call Insert(int, string, IniValue) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + values.Add(key, value); + orderedKeys.Insert(index, key); + } + + public void InsertRange(int index, IEnumerable> collection) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call InsertRange(int, IEnumerable>) on IniSection: section was not ordered."); + } + if (collection == null) { + throw new ArgumentNullException("Value cannot be null." + Environment.NewLine + "Parameter name: collection"); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + foreach (var kvp in collection) { + Insert(index, kvp.Key, kvp.Value); + index++; + } + } + + public void RemoveAt(int index) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call RemoveAt(int) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + var key = orderedKeys[index]; + orderedKeys.RemoveAt(index); + values.Remove(key); + } + + public void RemoveRange(int index, int count) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call RemoveRange(int, int) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + if (count < 0) { + throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); + } + if (index + count > orderedKeys.Count) { + throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + for (int i = 0; i < count; i++) { + RemoveAt(index); + } + } + + public void Reverse() { + if (!Ordered) { + throw new InvalidOperationException("Cannot call Reverse() on IniSection: section was not ordered."); + } + orderedKeys.Reverse(); + } + + public void Reverse(int index, int count) { + if (!Ordered) { + throw new InvalidOperationException("Cannot call Reverse(int, int) on IniSection: section was not ordered."); + } + if (index < 0 || index > orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + if (count < 0) { + throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); + } + if (index + count > orderedKeys.Count) { + throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + orderedKeys.Reverse(index, count); + } + + public ICollection GetOrderedValues() { + if (!Ordered) { + throw new InvalidOperationException("Cannot call GetOrderedValues() on IniSection: section was not ordered."); + } + var list = new List(); + for (int i = 0; i < orderedKeys.Count; i++) { + list.Add(values[orderedKeys[i]]); + } + return list; + } + + public IniValue this[int index] { + get { + if (!Ordered) { + throw new InvalidOperationException("Cannot index IniSection using integer key: section was not ordered."); + } + if (index < 0 || index >= orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + return values[orderedKeys[index]]; + } + set { + if (!Ordered) { + throw new InvalidOperationException("Cannot index IniSection using integer key: section was not ordered."); + } + if (index < 0 || index >= orderedKeys.Count) { + throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); + } + var key = orderedKeys[index]; + values[key] = value; + } + } + + public bool Ordered { + get { + return orderedKeys != null; + } + set { + if (Ordered != value) { + orderedKeys = value ? new List(values.Keys) : null; + } + } + } + #endregion + + public IniSection() + : this(IniFile.DefaultComparer) { + } + + public IniSection(IEqualityComparer stringComparer) { + this.values = new Dictionary(stringComparer); + } + + public IniSection(Dictionary values) + : this(values, IniFile.DefaultComparer) { + } + + public IniSection(Dictionary values, IEqualityComparer stringComparer) { + this.values = new Dictionary(values, stringComparer); + } + + public IniSection(IniSection values) + : this(values, IniFile.DefaultComparer) { + } + + public IniSection(IniSection values, IEqualityComparer stringComparer) { + this.values = new Dictionary(values.values, stringComparer); + } + + public void Add(string key, IniValue value) { + values.Add(key, value); + if (Ordered) { + orderedKeys.Add(key); + } + } + + public bool ContainsKey(string key) { + return values.ContainsKey(key); + } + + /// + /// Returns this IniSection's collection of keys. If the IniSection is ordered, the keys will be returned in order. + /// + public ICollection Keys { + get { return Ordered ? (ICollection)orderedKeys : values.Keys; } + } + + public bool Remove(string key) { + var ret = values.Remove(key); + if (Ordered && ret) { + for (int i = 0; i < orderedKeys.Count; i++) { + if (Comparer.Equals(orderedKeys[i], key)) { + orderedKeys.RemoveAt(i); + break; + } + } + } + return ret; + } + + public bool TryGetValue(string key, out IniValue value) { + return values.TryGetValue(key, out value); + } + + /// + /// Returns the values in this IniSection. These values are always out of order. To get ordered values from an IniSection call GetOrderedValues instead. + /// + public ICollection Values { + get { + return values.Values; + } + } + + void ICollection>.Add(KeyValuePair item) { + ((IDictionary)values).Add(item); + if (Ordered) { + orderedKeys.Add(item.Key); + } + } + + public void Clear() { + values.Clear(); + if (Ordered) { + orderedKeys.Clear(); + } + } + + bool ICollection>.Contains(KeyValuePair item) { + return ((IDictionary)values).Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { + ((IDictionary)values).CopyTo(array, arrayIndex); + } + + public int Count { + get { return values.Count; } + } + + bool ICollection>.IsReadOnly { + get { return ((IDictionary)values).IsReadOnly; } + } + + bool ICollection>.Remove(KeyValuePair item) { + var ret = ((IDictionary)values).Remove(item); + if (Ordered && ret) { + for (int i = 0; i < orderedKeys.Count; i++) { + if (Comparer.Equals(orderedKeys[i], item.Key)) { + orderedKeys.RemoveAt(i); + break; + } + } + } + return ret; + } + + public IEnumerator> GetEnumerator() { + if (Ordered) { + return GetOrderedEnumerator(); + } else { + return values.GetEnumerator(); + } + } + + private IEnumerator> GetOrderedEnumerator() { + for (int i = 0; i < orderedKeys.Count; i++) { + yield return new KeyValuePair(orderedKeys[i], values[orderedKeys[i]]); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public IEqualityComparer Comparer { get { return values.Comparer; } } + + public IniValue this[string name] { + get { + IniValue val; + if (values.TryGetValue(name, out val)) { + return val; + } + return IniValue.Default; + } + set { + if (Ordered && !orderedKeys.Contains(name, Comparer)) { + orderedKeys.Add(name); + } + values[name] = value; + } + } + + public static implicit operator IniSection(Dictionary dict) { + return new IniSection(dict); + } + + public static explicit operator Dictionary(IniSection section) { + return section.values; + } + } + } + + /// + /// abstract class for Ini Management + /// To use, child phonemizer should implement this class(BaseIniManager) with its own setting values! + /// + public abstract class BaseIniManager : IniParser{ + protected USinger singer; + protected IniFile iniFile = new IniFile(); + protected string iniFileName; + + public BaseIniManager() { } + + /// + /// if no [iniFileName] in Singer Directory, it makes new [iniFileName] with settings in [IniSetUp(iniFile)]. + /// + /// + /// + public void Initialize(USinger singer, string iniFileName) { + this.singer = singer; + this.iniFileName = iniFileName; + try { + iniFile.Load($"{singer.Location}/{iniFileName}"); + IniSetUp(iniFile); // you can override IniSetUp() to use. + } + catch { + IniSetUp(iniFile); // you can override IniSetUp() to use. + } + } + + /// + /// 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(IniFile iniFile) { + } + + /// + /// 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 boolean vaule, nothing happens. But if there's no valid boolean value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, bool 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected void SetOrReadThisValue(string sectionName, string keyName, bool defaultValue) { + iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToBool(defaultValue); + iniFile.Save($"{singer.Location}/{iniFileName}"); + } + + /// + /// 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 void SetOrReadThisValue(string sectionName, string keyName, string defaultValue) { + if (!iniFile[sectionName].ContainsKey(keyName)) { + // 키가 존재하지 않으면 새로 값을 넣는다 + iniFile[sectionName][keyName] = defaultValue; + iniFile.Save($"{singer.Location}/{iniFileName}"); + } + // 키가 존재하면 그냥 스킵 + } + + /// + /// + /// 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) { + iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToInt(defaultValue); + iniFile.Save($"{singer.Location}/{iniFileName}"); + } + + /// + /// 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 double vaule, nothing happens. But if there's no valid double value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, double 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected void SetOrReadThisValue(string sectionName, string keyName, double defaultValue) { + iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToDouble(defaultValue); + iniFile.Save($"{singer.Location}/{iniFileName}"); + } + } } + } \ No newline at end of file From 572ee5cf497224b705ada031faf379e28b8afb72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=ED=98=84=20=EA=B9=80?= Date: Wed, 27 Dec 2023 21:51:02 +0900 Subject: [PATCH 3/6] =?UTF-8?q?Fixed=20=E3=84=BB=20missing=20in=20Yeoneum?= =?UTF-8?q?=20Targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index 21ba733a4..527082bef 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -287,6 +287,10 @@ private static Hashtable Variate(Hashtable firstCharSeparated, Hashtable nextCha firstLastConsonant = "ㅂ"; nextFirstConsonant = "ㅅ"; } + else if (firstLastConsonant.Equals("ㄻ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅁ"; + } else if (firstLastConsonant.Equals("ㅇ") && nextFirstConsonant.Equals("ㅇ")) { // Do nothing } @@ -1063,7 +1067,7 @@ public static Note[] ChangeLyric(Note[] group, string lyric) { return group; } - public static void RomanizeNotes(Note[][] groups, Dictionary firstConsonants, Dictionary vowels, Dictionary lastConsonants) { + public static void RomanizeNotes(Note[][] groups, Dictionary firstConsonants, Dictionary vowels, Dictionary lastConsonants, string semivowelSeparator=" ") { // for ENUNU Phonemizer int noteIdx = 0; @@ -1105,9 +1109,17 @@ public static void RomanizeNotes(Note[][] groups, Dictionary f string lyric = ""; Hashtable lyricSeparated = Variate(prevNote_, currentNote[0], nextNote_); lyric += firstConsonants[(string)lyricSeparated[3]][0]; - lyric += vowels[(string)lyricSeparated[4]][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); + + ResultLyrics.Add(lyric.Trim()); prevNote = currentNote; @@ -1116,7 +1128,7 @@ public static void RomanizeNotes(Note[][] groups, Dictionary f Enumerable.Zip(groups, ResultLyrics.ToArray(), ChangeLyric).Last(); } - /// + /// /// abstract class for BaseIniManager(https://github.com/Enichan/Ini/blob/master/Ini.cs) /// /// Note: This class will be NOT USED when implementing child korean phonemizers. This class is only for BaseIniManager. From dcea9beaf51763e1798b49f33eb903a60ec3b8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=ED=98=84=20=EA=B9=80?= Date: Thu, 28 Dec 2023 00:54:02 +0900 Subject: [PATCH 4/6] Fixed error when non-Korean lyric inserted --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index 527082bef..6d11b75dc 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -1107,6 +1107,14 @@ public static void RomanizeNotes(Note[][] groups, Dictionary f 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] != "") { From 40b91954638ba9388e891f0479b20a936e09bc5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=ED=98=84=20=EA=B9=80?= Date: Fri, 29 Dec 2023 00:22:20 +0900 Subject: [PATCH 5/6] Fix BaseIniManager implementation, Add JamoDictionary for yamldata --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 982 ++++++-------------------- 1 file changed, 227 insertions(+), 755 deletions(-) diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index 6d11b75dc..91a49d5fa 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -4,8 +4,9 @@ using System.Diagnostics; using System.IO; using System.Linq; -using System.Text; using OpenUtau.Core.Ustx; +using OpenUtau.Classic; +using Serilog; using static OpenUtau.Api.Phonemizer; namespace OpenUtau.Core { @@ -1066,7 +1067,6 @@ public static Note[] ChangeLyric(Note[] group, string lyric) { }; return group; } - public static void RomanizeNotes(Note[][] groups, Dictionary firstConsonants, Dictionary vowels, Dictionary lastConsonants, string semivowelSeparator=" ") { // for ENUNU Phonemizer @@ -1136,747 +1136,56 @@ public static void RomanizeNotes(Note[][] groups, Dictionary f Enumerable.Zip(groups, ResultLyrics.ToArray(), ChangeLyric).Last(); } - /// - /// abstract class for BaseIniManager(https://github.com/Enichan/Ini/blob/master/Ini.cs) - /// - /// Note: This class will be NOT USED when implementing child korean phonemizers. This class is only for BaseIniManager. - /// - /// - public abstract class IniParser - { - public struct IniValue { - private static bool TryParseInt(string text, out int value) { - int res; - if (Int32.TryParse(text, - System.Globalization.NumberStyles.Integer, - System.Globalization.CultureInfo.InvariantCulture, - out res)) { - value = res; - return true; - } - value = 0; - return false; - } - private static bool TryParseDouble(string text, out double value) { - double res; - if (Double.TryParse(text, - System.Globalization.NumberStyles.Float, - System.Globalization.CultureInfo.InvariantCulture, - out res)) { - value = res; - return true; - } - value = Double.NaN; - return false; - } - public string Value; - public IniValue(object value) { - var formattable = value as IFormattable; - if (formattable != null) { - Value = formattable.ToString(null, System.Globalization.CultureInfo.InvariantCulture); - } else { - Value = value != null ? value.ToString() : null; - } - } - public IniValue(string value) { - Value = value; - } - public bool ToBool(bool valueIfInvalid = false) { - bool res; - if (TryConvertBool(out res)) { - return res; - } - return valueIfInvalid; - } - public bool TryConvertBool(out bool result) { - if (Value == null) { - result = default(bool); - return false; - } - var boolStr = Value.Trim().ToLowerInvariant(); - if (boolStr == "true") { - result = true; - return true; - } else if (boolStr == "false") { - result = false; - return true; - } - result = default(bool); - return false; - } - public int ToInt(int valueIfInvalid = 0) { - int res; - if (TryConvertInt(out res)) { - return res; - } - return valueIfInvalid; - } - public bool TryConvertInt(out int result) { - if (Value == null) { - result = default(int); - return false; - } - if (TryParseInt(Value.Trim(), out result)) { - return true; - } - return false; - } - public double ToDouble(double valueIfInvalid = 0) { - double res; - if (TryConvertDouble(out res)) { - return res; - } - return valueIfInvalid; - } - public bool TryConvertDouble(out double result) { - if (Value == null) { - result = default(double); - return false; ; - } - if (TryParseDouble(Value.Trim(), out result)) { - return true; - } - return false; - } - public string GetString() { - return GetString(true, false); - } - public string GetString(bool preserveWhitespace) { - return GetString(true, preserveWhitespace); - } - public string GetString(bool allowOuterQuotes, bool preserveWhitespace) { - if (Value == null) { - return ""; - } - var trimmed = Value.Trim(); - if (allowOuterQuotes && trimmed.Length >= 2 && trimmed[0] == '"' && trimmed[trimmed.Length - 1] == '"') { - var inner = trimmed.Substring(1, trimmed.Length - 2); - return preserveWhitespace ? inner : inner.Trim(); - } else { - return preserveWhitespace ? Value : Value.Trim(); - } - } - public override string ToString() { - return Value; - } - public static implicit operator IniValue(byte o) { - return new IniValue(o); - } - public static implicit operator IniValue(short o) { - return new IniValue(o); - } - public static implicit operator IniValue(int o) { - return new IniValue(o); - } - public static implicit operator IniValue(sbyte o) { - return new IniValue(o); - } - public static implicit operator IniValue(ushort o) { - return new IniValue(o); - } - public static implicit operator IniValue(uint o) { - return new IniValue(o); - } - public static implicit operator IniValue(float o) { - return new IniValue(o); - } - public static implicit operator IniValue(double o) { - return new IniValue(o); - } - public static implicit operator IniValue(bool o) { - return new IniValue(o); - } - public static implicit operator IniValue(string o) { - return new IniValue(o); - } - private static readonly IniValue _default = new IniValue(); - public static IniValue Default { get { return _default; } } - } - public class IniFile : IEnumerable>, IDictionary { - private Dictionary sections; - public IEqualityComparer StringComparer; - public bool SaveEmptySections; - public IniFile() - : this(DefaultComparer) { - } - public IniFile(IEqualityComparer stringComparer) { - StringComparer = stringComparer; - sections = new Dictionary(StringComparer); - } - public void Save(string path, FileMode mode = FileMode.Create) { - using (var stream = new FileStream(path, mode, FileAccess.Write)) { - Save(stream); - } - } - public void Save(Stream stream) { - using (var writer = new StreamWriter(stream)) { - Save(writer); - } - } - public void Save(StreamWriter writer) { - foreach (var section in sections) { - if (section.Value.Count > 0 || SaveEmptySections) { - writer.WriteLine(string.Format("[{0}]", section.Key.Trim())); - foreach (var kvp in section.Value) { - writer.WriteLine(string.Format("{0}={1}", kvp.Key, kvp.Value)); - } - writer.WriteLine(""); - } - } - } - public void Load(string path, bool ordered = false) { - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { - Load(stream, ordered); - } - } - public void Load(Stream stream, bool ordered = false) { - using (var reader = new StreamReader(stream)) { - Load(reader, ordered); - } - } - public void Load(StreamReader reader, bool ordered = false) { - IniSection section = null; - while (!reader.EndOfStream) { - var line = reader.ReadLine(); - if (line != null) { - var trimStart = line.TrimStart(); - if (trimStart.Length > 0) { - if (trimStart[0] == '[') { - var sectionEnd = trimStart.IndexOf(']'); - if (sectionEnd > 0) { - var sectionName = trimStart.Substring(1, sectionEnd - 1).Trim(); - section = new IniSection(StringComparer) { Ordered = ordered }; - sections[sectionName] = section; - } - } else if (section != null && trimStart[0] != ';') { - string key; - IniValue val; - if (LoadValue(line, out key, out val)) { - section[key] = val; - } - } - } - } - } - } - private bool LoadValue(string line, out string key, out IniValue val) { - var assignIndex = line.IndexOf('='); - if (assignIndex <= 0) { - key = null; - val = null; - return false; - } - key = line.Substring(0, assignIndex).Trim(); - var value = line.Substring(assignIndex + 1); - val = new IniValue(value); - return true; - } - public bool ContainsSection(string section) { - return sections.ContainsKey(section); - } - public bool TryGetSection(string section, out IniSection result) { - return sections.TryGetValue(section, out result); - } - bool IDictionary.TryGetValue(string key, out IniSection value) { - return TryGetSection(key, out value); - } - public bool Remove(string section) { - return sections.Remove(section); - } - public IniSection Add(string section, Dictionary values, bool ordered = false) { - return Add(section, new IniSection(values, StringComparer) { Ordered = ordered }); - } - public IniSection Add(string section, IniSection value) { - if (value.Comparer != StringComparer) { - value = new IniSection(value, StringComparer); - } - sections.Add(section, value); - return value; - } - public IniSection Add(string section, bool ordered = false) { - var value = new IniSection(StringComparer) { Ordered = ordered }; - sections.Add(section, value); - return value; - } - void IDictionary.Add(string key, IniSection value) { - Add(key, value); - } - bool IDictionary.ContainsKey(string key) { - return ContainsSection(key); - } - public ICollection Keys { - get { return sections.Keys; } - } - public ICollection Values { - get { return sections.Values; } - } - void ICollection>.Add(KeyValuePair item) { - ((IDictionary)sections).Add(item); - } - public void Clear() { - sections.Clear(); - } - bool ICollection>.Contains(KeyValuePair item) { - return ((IDictionary)sections).Contains(item); - } - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { - ((IDictionary)sections).CopyTo(array, arrayIndex); - } - public int Count { - get { return sections.Count; } - } - bool ICollection>.IsReadOnly { - get { return ((IDictionary)sections).IsReadOnly; } - } - bool ICollection>.Remove(KeyValuePair item) { - return ((IDictionary)sections).Remove(item); - } - public IEnumerator> GetEnumerator() { - return sections.GetEnumerator(); - } - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - public IniSection this[string section] { - get { - IniSection s; - if (sections.TryGetValue(section, out s)) { - return s; - } - s = new IniSection(StringComparer); - sections[section] = s; - return s; - } - set { - var v = value; - if (v.Comparer != StringComparer) { - v = new IniSection(v, StringComparer); - } - sections[section] = v; - } - } - public string GetContents() { - using (var stream = new MemoryStream()) { - Save(stream); - stream.Flush(); - var builder = new StringBuilder(Encoding.UTF8.GetString(stream.ToArray())); - return builder.ToString(); - } - } - public static IEqualityComparer DefaultComparer = new CaseInsensitiveStringComparer(); - class CaseInsensitiveStringComparer : IEqualityComparer { - public bool Equals(string x, string y) { - return String.Compare(x, y, true) == 0; - } - public int GetHashCode(string obj) { - return obj.ToLowerInvariant().GetHashCode(); - } - -#if JS - public new bool Equals(object x, object y) { - var xs = x as string; - var ys = y as string; - if (xs == null || ys == null) { - return xs == null && ys == null; - } - return Equals(xs, ys); - } - - public int GetHashCode(object obj) { - if (obj is string) { - return GetHashCode((string)obj); - } - return obj.ToStringInvariant().ToLowerInvariant().GetHashCode(); - } -#endif - } - } - - public class IniSection : IEnumerable>, IDictionary { - private Dictionary values; - - #region Ordered - private List orderedKeys; - - public int IndexOf(string key) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call IndexOf(string) on IniSection: section was not ordered."); - } - return IndexOf(key, 0, orderedKeys.Count); - } - - public int IndexOf(string key, int index) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call IndexOf(string, int) on IniSection: section was not ordered."); - } - return IndexOf(key, index, orderedKeys.Count - index); - } - - public int IndexOf(string key, int index, int count) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call IndexOf(string, int, int) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - if (count < 0) { - throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); - } - if (index + count > orderedKeys.Count) { - throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); - } - var end = index + count; - for (int i = index; i < end; i++) { - if (Comparer.Equals(orderedKeys[i], key)) { - return i; - } - } - return -1; - } - - public int LastIndexOf(string key) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call LastIndexOf(string) on IniSection: section was not ordered."); - } - return LastIndexOf(key, 0, orderedKeys.Count); - } - - public int LastIndexOf(string key, int index) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call LastIndexOf(string, int) on IniSection: section was not ordered."); - } - return LastIndexOf(key, index, orderedKeys.Count - index); - } - - public int LastIndexOf(string key, int index, int count) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call LastIndexOf(string, int, int) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - if (count < 0) { - throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); - } - if (index + count > orderedKeys.Count) { - throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); - } - var end = index + count; - for (int i = end - 1; i >= index; i--) { - if (Comparer.Equals(orderedKeys[i], key)) { - return i; - } - } - return -1; - } - - public void Insert(int index, string key, IniValue value) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call Insert(int, string, IniValue) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - values.Add(key, value); - orderedKeys.Insert(index, key); - } - - public void InsertRange(int index, IEnumerable> collection) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call InsertRange(int, IEnumerable>) on IniSection: section was not ordered."); - } - if (collection == null) { - throw new ArgumentNullException("Value cannot be null." + Environment.NewLine + "Parameter name: collection"); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - foreach (var kvp in collection) { - Insert(index, kvp.Key, kvp.Value); - index++; - } - } - - public void RemoveAt(int index) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call RemoveAt(int) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - var key = orderedKeys[index]; - orderedKeys.RemoveAt(index); - values.Remove(key); - } - - public void RemoveRange(int index, int count) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call RemoveRange(int, int) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - if (count < 0) { - throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); - } - if (index + count > orderedKeys.Count) { - throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); - } - for (int i = 0; i < count; i++) { - RemoveAt(index); - } - } - - public void Reverse() { - if (!Ordered) { - throw new InvalidOperationException("Cannot call Reverse() on IniSection: section was not ordered."); - } - orderedKeys.Reverse(); - } - - public void Reverse(int index, int count) { - if (!Ordered) { - throw new InvalidOperationException("Cannot call Reverse(int, int) on IniSection: section was not ordered."); - } - if (index < 0 || index > orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - if (count < 0) { - throw new IndexOutOfRangeException("Count cannot be less than zero." + Environment.NewLine + "Parameter name: count"); - } - if (index + count > orderedKeys.Count) { - throw new ArgumentException("Index and count were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); - } - orderedKeys.Reverse(index, count); - } - - public ICollection GetOrderedValues() { - if (!Ordered) { - throw new InvalidOperationException("Cannot call GetOrderedValues() on IniSection: section was not ordered."); - } - var list = new List(); - for (int i = 0; i < orderedKeys.Count; i++) { - list.Add(values[orderedKeys[i]]); - } - return list; - } - - public IniValue this[int index] { - get { - if (!Ordered) { - throw new InvalidOperationException("Cannot index IniSection using integer key: section was not ordered."); - } - if (index < 0 || index >= orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - return values[orderedKeys[index]]; - } - set { - if (!Ordered) { - throw new InvalidOperationException("Cannot index IniSection using integer key: section was not ordered."); - } - if (index < 0 || index >= orderedKeys.Count) { - throw new IndexOutOfRangeException("Index must be within the bounds." + Environment.NewLine + "Parameter name: index"); - } - var key = orderedKeys[index]; - values[key] = value; - } - } - - public bool Ordered { - get { - return orderedKeys != null; - } - set { - if (Ordered != value) { - orderedKeys = value ? new List(values.Keys) : null; - } - } - } - #endregion - - public IniSection() - : this(IniFile.DefaultComparer) { - } - - public IniSection(IEqualityComparer stringComparer) { - this.values = new Dictionary(stringComparer); - } - - public IniSection(Dictionary values) - : this(values, IniFile.DefaultComparer) { - } - - public IniSection(Dictionary values, IEqualityComparer stringComparer) { - this.values = new Dictionary(values, stringComparer); - } - - public IniSection(IniSection values) - : this(values, IniFile.DefaultComparer) { - } - - public IniSection(IniSection values, IEqualityComparer stringComparer) { - this.values = new Dictionary(values.values, stringComparer); - } - - public void Add(string key, IniValue value) { - values.Add(key, value); - if (Ordered) { - orderedKeys.Add(key); - } - } - - public bool ContainsKey(string key) { - return values.ContainsKey(key); - } - - /// - /// Returns this IniSection's collection of keys. If the IniSection is ordered, the keys will be returned in order. - /// - public ICollection Keys { - get { return Ordered ? (ICollection)orderedKeys : values.Keys; } - } - - public bool Remove(string key) { - var ret = values.Remove(key); - if (Ordered && ret) { - for (int i = 0; i < orderedKeys.Count; i++) { - if (Comparer.Equals(orderedKeys[i], key)) { - orderedKeys.RemoveAt(i); - break; - } - } - } - return ret; - } - - public bool TryGetValue(string key, out IniValue value) { - return values.TryGetValue(key, out value); - } - - /// - /// Returns the values in this IniSection. These values are always out of order. To get ordered values from an IniSection call GetOrderedValues instead. - /// - public ICollection Values { - get { - return values.Values; - } - } - - void ICollection>.Add(KeyValuePair item) { - ((IDictionary)values).Add(item); - if (Ordered) { - orderedKeys.Add(item.Key); - } - } - - public void Clear() { - values.Clear(); - if (Ordered) { - orderedKeys.Clear(); - } - } - - bool ICollection>.Contains(KeyValuePair item) { - return ((IDictionary)values).Contains(item); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { - ((IDictionary)values).CopyTo(array, arrayIndex); - } - - public int Count { - get { return values.Count; } - } - - bool ICollection>.IsReadOnly { - get { return ((IDictionary)values).IsReadOnly; } - } - - bool ICollection>.Remove(KeyValuePair item) { - var ret = ((IDictionary)values).Remove(item); - if (Ordered && ret) { - for (int i = 0; i < orderedKeys.Count; i++) { - if (Comparer.Equals(orderedKeys[i], item.Key)) { - orderedKeys.RemoveAt(i); - break; - } - } - } - return ret; - } - - public IEnumerator> GetEnumerator() { - if (Ordered) { - return GetOrderedEnumerator(); - } else { - return values.GetEnumerator(); - } - } - - private IEnumerator> GetOrderedEnumerator() { - for (int i = 0; i < orderedKeys.Count; i++) { - yield return new KeyValuePair(orderedKeys[i], values[orderedKeys[i]]); - } - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { - return GetEnumerator(); - } - - public IEqualityComparer Comparer { get { return values.Comparer; } } - - public IniValue this[string name] { - get { - IniValue val; - if (values.TryGetValue(name, out val)) { - return val; - } - return IniValue.Default; - } - set { - if (Ordered && !orderedKeys.Contains(name, Comparer)) { - orderedKeys.Add(name); - } - values[name] = value; - } - } - - public static implicit operator IniSection(Dictionary dict) { - return new IniSection(dict); - } - - public static explicit operator Dictionary(IniSection section) { - return section.values; - } - } - } - /// /// abstract class for Ini Management /// To use, child phonemizer should implement this class(BaseIniManager) with its own setting values! /// - public abstract class BaseIniManager : IniParser{ + public abstract class BaseIniManager { protected USinger singer; - protected IniFile iniFile = new IniFile(); + 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 settings in [IniSetUp(iniFile)]. + /// if no [iniFileName] in Singer Directory, it makes new [iniFileName] with [iniFile]]. /// /// /// - public void Initialize(USinger singer, string iniFileName) { + /// + public void Initialize(USinger singer, string iniFileName, Hashtable defaultIniSetting) { this.singer = singer; this.iniFileName = iniFileName; + iniSetting = defaultIniSetting; + filePath = Path.Combine(singer.Location, iniFileName); try { - iniFile.Load($"{singer.Location}/{iniFileName}"); - IniSetUp(iniFile); // you can override IniSetUp() to use. + 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 { - IniSetUp(iniFile); // 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; + }; } } @@ -1886,20 +1195,72 @@ public void Initialize(USinger singer, string iniFileName) { /// (ex) /// SetOrReadThisValue("sectionName", "keyName", true); /// - protected virtual void IniSetUp(IniFile iniFile) { + 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 boolean vaule, nothing happens. But if there's no valid boolean value, overwrites current value with default value. + /// 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) { - iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToBool(defaultValue); - iniFile.Save($"{singer.Location}/{iniFileName}"); - } + /// /// + 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. @@ -1908,13 +1269,47 @@ protected void SetOrReadThisValue(string sectionName, string keyName, bool defau /// 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 void SetOrReadThisValue(string sectionName, string keyName, string defaultValue) { - if (!iniFile[sectionName].ContainsKey(keyName)) { - // 키가 존재하지 않으면 새로 값을 넣는다 - iniFile[sectionName][keyName] = defaultValue; - iniFile.Save($"{singer.Location}/{iniFileName}"); - } - // 키가 존재하면 그냥 스킵 + 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; } /// @@ -1925,23 +1320,100 @@ protected void SetOrReadThisValue(string sectionName, string keyName, string def /// 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) { - iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToInt(defaultValue); - iniFile.Save($"{singer.Location}/{iniFileName}"); - } - - /// - /// 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 double vaule, nothing happens. But if there's no valid double value, overwrites current value with default value. - /// 섹션과 키 이름을 입력받고, double 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 - /// - protected void SetOrReadThisValue(string sectionName, string keyName, double defaultValue) { - iniFile[sectionName][keyName] = iniFile[sectionName][keyName].ToDouble(defaultValue); - iniFile.Save($"{singer.Location}/{iniFileName}"); + 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 From 2d09e90b8d57bb47a2bb31fafe9dea70c69fbedd Mon Sep 17 00:00:00 2001 From: EX3 <100339835+EX3exp@users.noreply.github.com> Date: Tue, 16 Jan 2024 19:41:49 +0900 Subject: [PATCH 6/6] Fixed error phoneme when non-Korean Character came --- OpenUtau.Core/KoreanPhonemizerUtil.cs | 38 ++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index 91a49d5fa..632d195cd 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -45,7 +45,7 @@ public static class KoreanPhonemizerUtil { /// A hashtable of basicsounds - ㄱ/ㄷ/ㅂ/ㅅ/ㅈ. ///

예사소리 테이블입니다. /// - static readonly Hashtable basicSounds = new Hashtable() { + public static readonly Hashtable basicSounds = new Hashtable() { ["ㄱ"] = 0, ["ㄷ"] = 1, ["ㅂ"] = 2, @@ -59,7 +59,7 @@ public static class KoreanPhonemizerUtil { ///

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

된소리 테이블입니다. /// - static readonly Hashtable fortisSounds = new Hashtable() { + public static readonly Hashtable fortisSounds = new Hashtable() { [0] = "ㄲ", [1] = "ㄸ", [2] = "ㅃ", @@ -83,7 +83,7 @@ public static class KoreanPhonemizerUtil { /// A hashtable of nasal sounds - ㄴ/ㅇ/ㅁ. ///

비음 테이블입니다. /// - static readonly Hashtable nasalSounds = new Hashtable() { + public static readonly Hashtable nasalSounds = new Hashtable() { ["ㄴ"] = 0, ["ㅇ"] = 1, ["ㅁ"] = 2 @@ -537,6 +537,7 @@ private static Hashtable Variate(Hashtable separated) { /// private static Hashtable Variate(string firstChar, string nextChar, int returnCharIndex = 0) { // 글자 넣어도 쓸 수 있음 + Hashtable firstCharSeparated = Separate(firstChar); Hashtable nextCharSeparated = Separate(nextChar); return Variate(firstCharSeparated, nextCharSeparated, returnCharIndex); @@ -649,15 +650,28 @@ public static Hashtable Variate(Note? prevNeighbour, Note note, Note? nextNeighb [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]); + 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]); + 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; }