diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs index 432d16694..42535faa9 100644 --- a/OpenUtau.Core/KoreanPhonemizerUtil.cs +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -9,6 +9,7 @@ using Serilog; using static OpenUtau.Api.Phonemizer; using OpenUtau.Api; +using System.Text; namespace OpenUtau.Core { /// @@ -90,6 +91,76 @@ public static class KoreanPhonemizerUtil { ["ㅁ"] = 2 }; + /// + /// A dictionary of first consonants composed of {Romanization: Hangul}. + ///

{로마자:한글} 로 구성된 초성 딕셔너리 입니다. + ///
+ public static readonly Dictionary ROMAJI_KOREAN_FIRST_CONSONANTS_DICT = new Dictionary() { + {"g", "ㄱ"}, + {"n", "ㄴ"}, + {"d", "ㄷ"}, + {"r", "ㄹ"}, + {"l", "ㄹ"}, + {"m", "ㅁ"}, + {"b", "ㅂ"}, + {"s", "ㅅ"}, + {"j", "ㅈ"}, + {"ch", "ㅊ"}, + {"k", "ㅋ"}, + {"t", "ㅌ"}, + {"p", "ㅍ"}, + {"h", "ㅎ"}, + {"gg", "ㄲ"}, + {"kk", "ㄲ"}, + {"dd", "ㄸ"}, + {"tt", "ㄸ"}, + {"bb", "ㅃ"}, + {"pp", "ㅃ"}, + {"ss", "ㅆ"}, + {"jj", "ㅉ"}, + {"", "ㅇ" } + }; + + /// + /// A dictionary of middle vowels composed of {Romanization: Hangul}. + ///

{로마자:한글} 로 구성된 중성 딕셔너리 입니다. + ///
로마자의 길이 순으로 정렬 되어 있습니다. + ///
+ public static readonly Dictionary ROMAJI_KOREAN_MIDDLE_VOWELS_DICT = new Dictionary() { + {"yeo", "ㅕ"}, + {"weo", "ㅝ"}, + {"eui", "ㅢ"}, + {"ui", "ㅢ"}, + {"wa", "ㅘ"}, + {"wi", "ㅟ"}, + {"we", "ㅙ"}, + {"ya", "ㅑ"}, + {"yu", "ㅠ"}, + {"ye", "ㅖ"}, + {"yo", "ㅛ"}, + {"eu", "ㅡ"}, + {"eo", "ㅓ"}, + {"a", "ㅏ"}, + {"i", "ㅣ"}, + {"u", "ㅜ"}, + {"e", "ㅔ"}, + {"o", "ㅗ"}, + }; + + // + /// A dictionary of last consonants composed of {Romanization: Hangul}. + ///

{로마자:한글} 로 구성된 종성 딕셔너리 입니다. + ///
+ public static readonly Dictionary ROMAJI_KOREAN_LAST_CONSONANTS_DICT = new Dictionary() { + {"k", "ㄱ"}, + {"n", "ㄴ"}, + {"t", "ㄷ"}, + {"l", "ㄹ"}, + {"m", "ㅁ"}, + {"p", "ㅂ"}, + {"ng", "ㅇ"}, + {"", " " } + }; /// /// Confirms if input string is hangeul. @@ -124,6 +195,77 @@ public static bool IsHangeul(string? character) { return isHangeul; } + + /// + /// It checks if the input string is valid Korean Romanization. + ///
입력된 문자열이 유효한 표기의 한국어 로마자인지 확인합니다. + ///
+ /// + ///
(Example: 'rin') + /// + /// Bool + /// + public static bool IsKoreanRomaji(string lyric) { + if (!KoreanPhonemizerUtil.IsHangeul(lyric) && KoreanPhonemizerUtil.TryParseKoreanRomaji(lyric) != null) { + return true; + } + return false; + } + + /// + /// It checks if the input string is valid Korean Romanization, converts it to Korean if valid, and returns null if not. + ///
입력된 문자열이 유효한 표기의 한국어 로마자인지 확인하고, 유효할 경우 한국어로 변환합니다. 아닐 경우 null을 반환합니다. + ///
+ /// + ///
(Example: 'rin') + /// + /// String or null + /// (ex) 린 + /// + public static string? TryParseKoreanRomaji(string? romaji) { + + if (string.IsNullOrEmpty(romaji)) { + return null; + } + List allRomajiHangeul = new List(); + List allRomajiRomaji = new List(); + StringBuilder sb = new StringBuilder(); + foreach (var first in ROMAJI_KOREAN_FIRST_CONSONANTS_DICT.Keys) { + foreach (var middle in ROMAJI_KOREAN_MIDDLE_VOWELS_DICT.Keys) { + foreach (var last in ROMAJI_KOREAN_LAST_CONSONANTS_DICT.Keys) { + sb.Clear(); + sb.Append(first); + sb.Append(middle); + sb.Append(last); + allRomajiRomaji.Add(sb.ToString()); + sb.Clear(); + sb.Append(ROMAJI_KOREAN_FIRST_CONSONANTS_DICT[first]); + sb.Append("\t"); + sb.Append(ROMAJI_KOREAN_MIDDLE_VOWELS_DICT[middle]); + sb.Append("\t"); + sb.Append(ROMAJI_KOREAN_LAST_CONSONANTS_DICT[last]); + allRomajiHangeul.Add(sb.ToString()); + + } + } + } + + if (allRomajiRomaji.Contains(romaji)) { + string hangeul = allRomajiHangeul[allRomajiRomaji.IndexOf(romaji)]; + Hashtable separated = new Hashtable() { + [0] = hangeul.Split("\t")[0].ToString(), + [1] = hangeul.Split("\t")[1].ToString(), + [2] = hangeul.Split("\t")[2].ToString() + }; + string result = Merge(separated); + Log.Debug("Korean Romaji Parsed: " + romaji + " -> " + result); + return result; + } else { + return null; + } + + } + /// /// Separates complete hangeul string's first character in three parts - firstConsonant(초성), middleVowel(중성), lastConsonant(종성). ///
입력된 문자열의 0번째 글자를 초성, 중성, 종성으로 분리합니다. @@ -177,6 +319,49 @@ public static Hashtable Separate(string character) { return separatedHangeul; } + /// + /// It separates the input Korean Romanized string into initial consonant, medial vowel, and final consonant. + ///
If the Romanized string contains incorrect notation, it returns a list of length 3 filled with empty strings. + ///
입력된 한국어 로마자의 문자열을 로마자 표기 초성, 중성, 종성으로 분리합니다. + ///
올바르지 않은 표기법의 로마자가 들어오면 빈 문자열이 담긴 Length 3의 리스트를 반환합니다. + ///
+ /// + ///
(Example: 'nyang') + /// + /// {firstConsonant(초성), middleVowel(중성), lastConsonant(종성)} + /// (ex) {"n", "ya", "ng"} + /// + public static string[] SeparateRomaji(string character) { + try { + string[] separatedCharacter = new string[0]; + foreach (var vowel in ROMAJI_KOREAN_MIDDLE_VOWELS_DICT.Keys) { + if (character.Contains(vowel)) { + // 예시를 기준으로 변수 part는 {"n", "ng"} + var part = character.Split(vowel); + if (!(part[1] == "")) { // 글자에 초성, 중성, 종성이 전부 있는 경우 + separatedCharacter = new string[] { part[0], vowel, part[1] }; + } else if (part == new string[] {"", ""}) { // 글자에 중성만 존재하는 경우 + separatedCharacter = new string[] { "", vowel, "" }; + } else if (part.Length == 2) { // 글자에 초성, 중성만 존재 하는 경우 + separatedCharacter = new string[] { part[0], vowel, "" }; + } + break; + } + + if (separatedCharacter.Length == 0) { // 무엇도 해당하지 않을경우 빈 문자열 3개만 담음 + separatedCharacter = new string[] { "", "", ""}; + } + + return separatedCharacter; + } + } catch (Exception e) { + Log.Error(e, "SeparateRomaji Method Error!"); + return new string[] {"", "", ""}; + } + + return new string[] {"", "", ""}; + } + /// /// merges separated hangeul into complete hangeul. (Example: {[offset + 0]: "ㄱ", [offset + 1]: "ㅏ", [offset + 2]: " "} => "가"}) /// 자모로 쪼개진 한글을 합쳐진 한글로 반환합니다. @@ -202,7 +387,7 @@ public static string Merge(Hashtable separatedHangeul, int offset = 0){ int mergedCode = HANGEUL_UNICODE_START + (firstConsonantIndex * 21 + middleVowelIndex) * 28 + lastConsonantIndex; string result = Convert.ToChar(mergedCode).ToString(); - Debug.Print("Hangeul merged: " + $"{firstConsonant} + {middleVowel} + {lastConsonant} = " + result); + //Debug.Print("Hangeul merged: " + $"{firstConsonant} + {middleVowel} + {lastConsonant} = " + result); return result; } @@ -1426,4 +1611,4 @@ public FinalConsonantData(string grapheme, string phoneme) { } } -} \ No newline at end of file +} diff --git a/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs index a1552cdd8..9a5217a5c 100644 --- a/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using OpenUtau.Api; @@ -34,7 +34,8 @@ protected virtual bool additionalTest(string lyric) { // nullIfNotFound가 true이면 음소가 찾아지지 않을 때 음소가 아닌 null을 리턴한다. // nullIfNotFound가 false면 음소가 찾아지지 않을 때 그대로 음소를 반환 string phonemeToReturn; - string color = string.Empty; + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string color = attr.voiceColor ?? string.Empty; int toneShift = 0; int? alt = null; if (phoneme.Equals("")) {return phoneme;} @@ -168,44 +169,80 @@ public Result GenerateResult(String firstPhoneme, String secondPhoneme, String t }; } - /// - /// Returns Result with three input Phonemes. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Result - public Result GenerateResult(String firstPhoneme, String secondPhoneme, String thirdPhoneme, int totalDuration, int secondTotalDurationDivider=3, int thirdTotalDurationDivider=8){ - return new Result() { - phonemes = new Phoneme[] { - new Phoneme { phoneme = firstPhoneme}, - new Phoneme { phoneme = secondPhoneme, - position = totalDuration - totalDuration / secondTotalDurationDivider}, - new Phoneme { phoneme = thirdPhoneme, - position = totalDuration - totalDuration / thirdTotalDurationDivider}, - }// -음소 있이 이어줌 - }; + public class ProcessResult { + public Note[] KoreanLryicNotes { get; set; } + public Note? KoreanLryicPrevNote { get; set; } + public Note? KoreanLryicNextNote { get; set; } } - /// - /// It AUTOMATICALLY generates phonemes based on phoneme hints (each phonemes should be separated by ",". (Example: [a, a i, ya])) - /// But it can't generate phonemes automatically, so should implement ConvertPhonemes() Method in child class. - /// Also it can't generate Endsounds automatically, so should implement GenerateEndSound() Method in child class. + + // + /// The Romanized lyrics will be completely converted to Hangul. + /// 로마자 가사들을 전부 한글로 바꿔줍니다. /// - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { - Note note = notes[0]; - string lyric = note.lyric; - string phoneticHint = note.phoneticHint; + /// + /// + /// + /// ProcessResult? + public static ProcessResult? ConvertRomajiNoteToHangeul(Note[] notes, Note? prevNote, Note? nextNote) { + var note = notes[0]; + var lyric = note.lyric; - Note? prevNote = prevNeighbour; // null or Note - Note thisNote = note; - Note? nextNote = nextNeighbour; // null or Note + if (KoreanPhonemizerUtil.IsKoreanRomaji(lyric)) { // TODO + notes[0] = new Note() { + lyric = KoreanPhonemizerUtil.TryParseKoreanRomaji(lyric), + phoneticHint = note.phoneticHint, + position = note.position, + duration = note.duration, + tone = note.tone, + phonemeAttributes = note.phonemeAttributes + }; + Note? prevNoteNew = prevNote; + Note? nextNoteNew = nextNote; + if (prevNote != null) { + if (KoreanPhonemizerUtil.IsHangeul(prevNote.Value.lyric) != null) { + if (prevNote != null) { + prevNoteNew = new Note() { + lyric = KoreanPhonemizerUtil.TryParseKoreanRomaji(prevNote.Value.lyric), + phoneticHint = prevNote.Value.phoneticHint, + position = prevNote.Value.position, + duration = prevNote.Value.duration, + tone = prevNote.Value.tone, + phonemeAttributes = prevNote.Value.phonemeAttributes + }; + } + if (nextNote != null) { + nextNoteNew = new Note() { + lyric = KoreanPhonemizerUtil.TryParseKoreanRomaji(nextNote.Value.lyric), + phoneticHint = nextNote.Value.phoneticHint, + position = nextNote.Value.position, + duration = nextNote.Value.duration, + tone = nextNote.Value.tone, + phonemeAttributes = nextNote.Value.phonemeAttributes + }; - int totalDuration = notes.Sum(n => n.duration); + } + + } + } + return new ProcessResult { + KoreanLryicNotes = notes, + KoreanLryicPrevNote = prevNoteNew, + KoreanLryicNextNote = nextNoteNew, + }; + } + return null; + } + + // + /// It analyzes the phonetic hint and returns the result. + /// 발음 힌트를 분석하여 Result를 반환합니다. + /// + /// + /// + /// + /// Result? + public Result? RenderPhoneticHint(USinger singer, Note note, int totalDuration) { + var phoneticHint = note.phoneticHint; if (phoneticHint != null) { // if there are phonetic hint @@ -297,8 +334,59 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN return new Result() { phonemes = phonemes }; - } - else if (KoreanPhonemizerUtil.IsHangeul(lyric) || !KoreanPhonemizerUtil.IsHangeul(lyric) && additionalTest(lyric)) { + } + return null; + } + + /// + /// Returns Result with three input Phonemes. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Result + public Result GenerateResult(String firstPhoneme, String secondPhoneme, String thirdPhoneme, int totalDuration, int secondTotalDurationDivider=3, int thirdTotalDurationDivider=8){ + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = firstPhoneme}, + new Phoneme { phoneme = secondPhoneme, + position = totalDuration - totalDuration / secondTotalDurationDivider}, + new Phoneme { phoneme = thirdPhoneme, + position = totalDuration - totalDuration / thirdTotalDurationDivider}, + }// -음소 있이 이어줌 + }; + } + /// + /// It AUTOMATICALLY generates phonemes based on phoneme hints (each phonemes should be separated by ",". (Example: [a, a i, ya])) + /// But it can't generate phonemes automatically, so should implement ConvertPhonemes() Method in child class. + /// Also it can't generate Endsounds automatically, so should implement GenerateEndSound() Method in child class. + /// + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + Note note = notes[0]; + string lyric = note.lyric; + + Note? prevNote = prevNeighbour; // null or Note + Note thisNote = note; + Note? nextNote = nextNeighbour; // null or Note + + int totalDuration = notes.Sum(n => n.duration); + + var phoneticHint = RenderPhoneticHint(singer, note, totalDuration); + if (phoneticHint != null) { + return (Result) phoneticHint; + } + + var romaji2Korean = ConvertRomajiNoteToHangeul(notes, prevNeighbour, nextNeighbour); + if (romaji2Korean != null) { + return ConvertPhonemes(romaji2Korean.KoreanLryicNotes, prev, next, romaji2Korean.KoreanLryicPrevNote, romaji2Korean.KoreanLryicNextNote, prevNeighbours); + } + + if (KoreanPhonemizerUtil.IsHangeul(lyric) || !KoreanPhonemizerUtil.IsHangeul(lyric) && additionalTest(lyric)) { return ConvertPhonemes(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours); } else { @@ -313,4 +401,4 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN /// public abstract class BaseIniManager : KoreanPhonemizerUtil.BaseIniManager{} } -} \ No newline at end of file +} diff --git a/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs index 35263e0a6..1c5b43e68 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Melanchall.DryWetMidi.MusicTheory; using OpenUtau.Api; using OpenUtau.Core; using OpenUtau.Core.Ustx; @@ -294,12 +295,29 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN int totalDuration = notes.Sum(n => n.duration); int vcLength = 120; + + var phoneticHint = RenderPhoneticHint(singer, notes[0], totalDuration); + if (phoneticHint != null) { + return (Result) phoneticHint; + } + + var romaji2Korean = ConvertRomajiNoteToHangeul(notes, prevNeighbour, nextNeighbour); + if (romaji2Korean != null) { + notes = romaji2Korean.KoreanLryicNotes; + prevNeighbour = romaji2Korean.KoreanLryicPrevNote; + nextNeighbour = romaji2Korean.KoreanLryicNextNote; + + prevLyric = prevNeighbour?.lyric; + nextLyric = nextNeighbour?.lyric; + } + List phonemesArr = new List(); var currentLyric = notes[0].lyric; currentKoreanLyrics = SeparateHangul(currentLyric != null ? currentLyric[0] : '\0'); + if (currentLyric[0] >= '가' && currentLyric[0] <= '힣') { } else { diff --git a/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs index 2ac3ba5e9..6c1fa2afd 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs @@ -169,6 +169,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN string currentLyric = note.lyric; // 현재 가사 var attr0 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; var attr1 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; + int totalDuration = notes.Sum(n => n.duration); // 가사의 초성, 중성, 종성 분리 // P(re)Lconsonant, PLvowel, PLfinal / C(urrent)Lconsonant, CLvowel, CLfinal / N(ext)Lconsonant, NLvowel, NLfinal @@ -213,6 +214,11 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN int uCL, uPL, uNL; lCL = 0; lPL = 0; lNL = 0; + var phoneticHint = RenderPhoneticHint(singer, notes[0], totalDuration); + if (phoneticHint != null) { + return (Result) phoneticHint; + } + // 현재 노트 첫번째 글자 확인 firstCL = currentLyric[0]; @@ -583,7 +589,6 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN // 만약 받침이 있다면 if (FC != "") { - int totalDuration = notes.Sum(n => n.duration); int fcLength = totalDuration / 3; if ((TCLfinal == "k") || (TCLfinal == "p") || (TCLfinal == "t")) { fcLength = totalDuration / 2; } @@ -610,7 +615,6 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN // 뒤에 노트가 있다면 if (nextExist) { if ((nextNeighbour?.lyric)[0] == 'ㄹ') { VC = TCLplainvowel + "l"; } } if ((VC != "") && (TNLconsonant != "")) { - int totalDuration = notes.Sum(n => n.duration); int vcLength = 60; if ((TNLconsonant == "r") || (TNLconsonant == "h")) { vcLength = 30; } else if (TNLconsonant == "s") { vcLength = totalDuration/3; } @@ -634,7 +638,6 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }; } } else if (VC != "" && TNLconsonant == "") { - int totalDuration = notes.Sum(n => n.duration); int vcLength = 60; var nextAttr = nextNeighbour.Value.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; var nextUnicode = ToUnicodeElements(nextNeighbour?.lyric); @@ -886,8 +889,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }, }; } - - int totalDuration = notes.Sum(n => n.duration); + int vcLength = 60; var nextAttr = nextNeighbour.Value.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; if (singer.TryGetMappedOto(nextLyric, nextNeighbour.Value.tone + nextAttr.toneShift, nextAttr.voiceColor, out var oto)) { diff --git a/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs index ef45888ad..c3a800f3e 100644 --- a/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs @@ -131,6 +131,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN string color = string.Empty; int shift = 0; int? alt; + int totalDuration = notes.Sum(n => n.duration); string color1 = string.Empty; int shift1 = 0; @@ -150,6 +151,20 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN string currPhoneme; string[] prevIMF; + var phoneticHint = RenderPhoneticHint(singer, notes[0], totalDuration); + if (phoneticHint != null) { + return (Result) phoneticHint; + } + + var romaji2Korean = ConvertRomajiNoteToHangeul(notes, prevNeighbour, nextNeighbour); + if (romaji2Korean != null) { + notes = romaji2Korean.KoreanLryicNotes; + prevNeighbour = romaji2Korean.KoreanLryicPrevNote; + nextNeighbour = romaji2Korean.KoreanLryicNextNote; + + note = notes[0]; + } + // Check if lyric is R, - or an end breath and return appropriate Result; otherwise, move to next steps if (note.lyric == "R" || note.lyric == "R2" || note.lyric == "-" || note.lyric == "H" || note.lyric == "B" || note.lyric == "bre") {