-
Notifications
You must be signed in to change notification settings - Fork 311
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #992 from EX3exp/ko-cv-phonemizer
Add KoreanCVPhonemizer
- Loading branch information
Showing
1 changed file
with
310 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,310 @@ | ||
using System; | ||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using OpenUtau.Api; | ||
using OpenUtau.Core.Ustx; | ||
using OpenUtau.Core; | ||
|
||
namespace OpenUtau.Plugin.Builtin { | ||
/// Phonemizer for 'KOR CV' /// | ||
[Phonemizer("Korean CV Phonemizer", "KO CV", "EX3", language: "KO")] | ||
|
||
public class KoreanCVPhonemizer : BaseKoreanPhonemizer { | ||
|
||
// 1. Load Singer and Settings | ||
private KoreanCVIniSetting koreanCVIniSetting; // Manages Setting | ||
|
||
public bool isUsingShi, isUsing_aX, isUsing_i, isRentan; | ||
|
||
public override void SetSinger(USinger singer) { | ||
if (this.singer == singer) {return;} | ||
this.singer = singer; | ||
if (this.singer == null) {return;} | ||
|
||
if (this.singer.SingerType != USingerType.Classic){return;} | ||
|
||
koreanCVIniSetting = new KoreanCVIniSetting(); | ||
koreanCVIniSetting.Initialize(singer, "ko-CV.ini", new Hashtable(){ | ||
{"CV", new Hashtable(){ | ||
{"Use rentan", false}, | ||
{"Use 'shi' for '시'(otherwise 'si')", false}, | ||
{"Use 'i' for '의'(otherwise 'eui')", false}, | ||
}}, | ||
{"BATCHIM", new Hashtable(){ | ||
{"Use 'aX' instead of 'a X'", false} | ||
}} | ||
}); | ||
|
||
isUsingShi = koreanCVIniSetting.isUsingShi; | ||
isUsing_aX = koreanCVIniSetting.isUsing_aX; | ||
isUsing_i = koreanCVIniSetting.isUsing_i; | ||
isRentan = koreanCVIniSetting.isRentan; | ||
} | ||
|
||
private class KoreanCVIniSetting : BaseIniManager{ | ||
public bool isRentan; | ||
public bool isUsingShi; | ||
public bool isUsing_aX; | ||
public bool isUsing_i; | ||
|
||
protected override void IniSetUp(Hashtable iniSetting) { | ||
// ko-CV.ini | ||
SetOrReadThisValue("CV", "Use rentan", false, out var resultValue); // 연단음 사용 유무 - 기본값 false | ||
isRentan = resultValue; | ||
|
||
SetOrReadThisValue("CV", "Use 'shi' for '시'(otherwise 'si')", false, out resultValue); // 시를 [shi]로 표기할 지 유무 - 기본값 false | ||
isUsingShi = resultValue; | ||
|
||
SetOrReadThisValue("CV", "Use 'i' for '의'(otherwise 'eui')", false, out resultValue); // 의를 [i]로 표기할 지 유무 - 기본값 false | ||
isUsing_i = resultValue; | ||
|
||
SetOrReadThisValue("BATCHIM", "Use 'aX' instead of 'a X'", false, out resultValue); // 받침 표기를 a n 처럼 할 지 an 처럼 할지 유무 - 기본값 false(=a n 사용) | ||
isUsing_aX = resultValue; | ||
} | ||
} | ||
|
||
static readonly Dictionary<string, string> FIRST_CONSONANTS = new Dictionary<string, string>(){ | ||
{"ㄱ", "g"}, | ||
{"ㄲ", "gg"}, | ||
{"ㄴ", "n"}, | ||
{"ㄷ", "d"}, | ||
{"ㄸ", "dd"}, | ||
{"ㄹ", "r"}, | ||
{"ㅁ", "m"}, | ||
{"ㅂ", "b"}, | ||
{"ㅃ", "bb"}, | ||
{"ㅅ", "s"}, | ||
{"ㅆ", "ss"}, | ||
{"ㅇ", ""}, | ||
{"ㅈ", "j"}, | ||
{"ㅉ", "jj"}, | ||
{"ㅊ", "ch"}, | ||
{"ㅋ", "k"}, | ||
{"ㅌ", "t"}, | ||
{"ㅍ", "p"}, | ||
{"ㅎ", "h"}, | ||
{"null", ""} // 뒤 글자가 없을 때를 대비 | ||
}; | ||
|
||
static readonly Dictionary<string, string[]> MIDDLE_VOWELS = new Dictionary<string, string[]>(){ | ||
{"ㅏ", new string[3]{"a", "", "a"}}, | ||
{"ㅐ", new string[3]{"e", "", "e"}}, | ||
{"ㅑ", new string[3]{"ya", "y", "a"}}, | ||
{"ㅒ", new string[3]{"ye", "y", "e"}}, | ||
{"ㅓ", new string[3]{"eo", "", "eo"}}, | ||
{"ㅔ", new string[3]{"e", "", "e"}}, | ||
{"ㅕ", new string[3]{"yeo", "y", "eo"}}, | ||
{"ㅖ", new string[3]{"ye", "y", "e"}}, | ||
{"ㅗ", new string[3]{"o", "", "o"}}, | ||
{"ㅘ", new string[3]{"wa", "w", "a"}}, | ||
{"ㅙ", new string[3]{"we", "w", "e"}}, | ||
{"ㅚ", new string[3]{"we", "w", "e"}}, | ||
{"ㅛ", new string[3]{"yo", "y", "o"}}, | ||
{"ㅜ", new string[3]{"u", "", "u"}}, | ||
{"ㅝ", new string[3]{"weo", "w", "eo"}}, | ||
{"ㅞ", new string[3]{"we", "w", "e"}}, | ||
{"ㅟ", new string[3]{"wi", "w", "i"}}, | ||
{"ㅠ", new string[3]{"yu", "y", "u"}}, | ||
{"ㅡ", new string[3]{"eu", "", "eu"}}, | ||
{"ㅢ", new string[3]{"eui", "eu", "i"}}, // ㅢ는 ㅣ로 발음 | ||
{"ㅣ", new string[3]{"i", "", "i"}}, | ||
{"null", new string[3]{"", "", ""}} // 뒤 글자가 없을 때를 대비 | ||
}; | ||
static readonly Dictionary<string, string[]> LAST_CONSONANTS = new Dictionary<string, string[]>(){ | ||
//ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ | ||
{"ㄱ", new string[]{"k", ""}}, | ||
{"ㄲ", new string[]{"k", ""}}, | ||
{"ㄳ", new string[]{"k", ""}}, | ||
{"ㄴ", new string[]{"n", "2"}}, | ||
{"ㄵ", new string[]{"n", "2"}}, | ||
{"ㄶ", new string[]{"n", "2"}}, | ||
{"ㄷ", new string[]{"t", "1"}}, | ||
{"ㄹ", new string[]{"l", "4"}}, | ||
{"ㄺ", new string[]{"k", ""}}, | ||
{"ㄻ", new string[]{"m", "1"}}, | ||
{"ㄼ", new string[]{"l", "4"}}, | ||
{"ㄽ", new string[]{"l", "4"}}, | ||
{"ㄾ", new string[]{"l", "4"}}, | ||
{"ㄿ", new string[]{"p", "1"}}, | ||
{"ㅀ", new string[]{"l", "4"}}, | ||
{"ㅁ", new string[]{"m", "1"}}, | ||
{"ㅂ", new string[]{"p", "1"}}, | ||
{"ㅄ", new string[]{"p", "1"}}, | ||
{"ㅅ", new string[]{"t", "1"}}, | ||
{"ㅆ", new string[]{"t", "1"}}, | ||
{"ㅇ", new string[]{"ng", "3"}}, | ||
{"ㅈ", new string[]{"t", "1"}}, | ||
{"ㅊ", new string[]{"t", "1"}}, | ||
{"ㅋ", new string[]{"k", ""}}, | ||
{"ㅌ", new string[]{"t", "1"}}, | ||
{"ㅍ", new string[]{"p", "1"}}, | ||
{"ㅎ", new string[]{"t", "1"}}, | ||
{" ", new string[]{""}}, // no batchim | ||
{"null", new string[]{"", ""}} // 뒤 글자가 없을 때를 대비 | ||
}; | ||
|
||
private Result ConvertForCV(Note[] notes, string[] prevLyric, string[] thisLyric, string[] nextLyric) { | ||
string thisMidVowelHead; | ||
string thisMidVowelTail; | ||
|
||
int totalDuration = notes.Sum(n => n.duration); | ||
Note note = notes[0]; | ||
bool isItNeedsFrontCV; | ||
bool isRelaxedVC; | ||
isItNeedsFrontCV = prevLyric[0] == "null" || prevLyric[1] == "null" || (prevLyric[2] != "null" && HARD_BATCHIMS.Contains(prevLyric[2]) && prevLyric[2] != "ㅁ"); | ||
isRelaxedVC = nextLyric[0] == "null" || nextLyric[1] == "null" || ((thisLyric[2] == nextLyric[0]) && (KoreanPhonemizerUtil.nasalSounds.ContainsKey(thisLyric[2]) || thisLyric[2] == "ㄹ")); | ||
|
||
if (thisLyric.All(part => part == null)) { | ||
return GenerateResult(FindInOto(note.lyric, note)); | ||
} | ||
else if (thisLyric[1] == "ㅢ") { | ||
if (isUsing_i) { | ||
thisMidVowelHead = $"{MIDDLE_VOWELS["ㅣ"][1]}"; | ||
thisMidVowelTail = $"{MIDDLE_VOWELS["ㅣ"][2]}"; | ||
} | ||
else { | ||
thisMidVowelHead = $"{MIDDLE_VOWELS["ㅢ"][1]}"; | ||
thisMidVowelTail = $"{MIDDLE_VOWELS["ㅢ"][2]}"; | ||
} | ||
} | ||
else { | ||
thisMidVowelHead = $"{MIDDLE_VOWELS[thisLyric[1]][1]}"; | ||
thisMidVowelTail = $"{MIDDLE_VOWELS[thisLyric[1]][2]}"; | ||
} | ||
|
||
string CV = $"{FIRST_CONSONANTS[thisLyric[0]]}{thisMidVowelHead}{thisMidVowelTail}"; | ||
string frontCV; | ||
string batchim; | ||
|
||
if (isRentan) { | ||
frontCV = $"- {CV}"; | ||
if (FindInOto(frontCV, note, true) == null) { | ||
frontCV = $"-{CV}"; | ||
if (FindInOto(frontCV, note, true) == null) { | ||
frontCV = CV; | ||
} | ||
} | ||
} | ||
else { | ||
frontCV = CV; | ||
} | ||
|
||
if (thisLyric[2] == " ") { // no batchim | ||
if (isItNeedsFrontCV){ | ||
return GenerateResult(FindInOto(frontCV, note)); | ||
} | ||
return GenerateResult(FindInOto(CV, note)); | ||
} | ||
|
||
if (isUsing_aX) { | ||
batchim = $"{thisMidVowelTail}{LAST_CONSONANTS[thisLyric[2]][0]}"; | ||
} | ||
else { | ||
batchim = $"{thisMidVowelTail} {LAST_CONSONANTS[thisLyric[2]][0]}"; | ||
} | ||
|
||
if (thisLyric[2] == "ㅁ" || ! HARD_BATCHIMS.Contains(thisLyric[2])) { // batchim ㅁ + ㄴ ㄹ ㅇ | ||
if (isItNeedsFrontCV){ | ||
return isRelaxedVC ? | ||
GenerateResult(FindInOto(frontCV, note), FindInOto(batchim, note), totalDuration, 120, 8) | ||
: GenerateResult(FindInOto(frontCV, note), FindInOto(batchim, note), "", totalDuration, 120, 3, 5); | ||
} | ||
return isRelaxedVC ? | ||
GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), totalDuration, 120, 8) | ||
: GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), "", totalDuration, 120, 3, 5); | ||
} | ||
else { | ||
if (isItNeedsFrontCV){ | ||
return isRelaxedVC ? | ||
GenerateResult(FindInOto(frontCV, note), FindInOto(batchim, note), totalDuration, 120, 8) | ||
: GenerateResult(FindInOto(frontCV, note), FindInOto(batchim, note), totalDuration, 120, 5); | ||
} | ||
return isRelaxedVC ? | ||
GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), totalDuration, 120, 8) | ||
: GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), totalDuration, 120, 5); | ||
} | ||
|
||
} | ||
|
||
private string? FindInOto(String phoneme, Note note, bool nullIfNotFound=false){ | ||
return BaseKoreanPhonemizer.FindInOto(singer, phoneme, note, nullIfNotFound); | ||
} | ||
|
||
|
||
private string HandleEmptyFirstConsonant(string lyric) { | ||
return lyric == " " ? "ㅇ" : lyric; | ||
} | ||
|
||
public override Result ConvertPhonemes(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { | ||
Note note = notes[0]; | ||
|
||
Hashtable lyrics = KoreanPhonemizerUtil.Variate(prevNeighbour, note, nextNeighbour); | ||
string[] prevLyric = new string[]{ // "ㄴ", "ㅑ", "ㅇ" | ||
HandleEmptyFirstConsonant((string)lyrics[0]), | ||
(string)lyrics[1], | ||
(string)lyrics[2] | ||
}; | ||
string[] thisLyric = new string[]{ // "ㄴ", "ㅑ", "ㅇ" | ||
HandleEmptyFirstConsonant((string)lyrics[3]), | ||
(string)lyrics[4], | ||
(string)lyrics[5] | ||
}; | ||
string[] nextLyric = new string[]{ // "ㄴ", "ㅑ", "ㅇ" | ||
HandleEmptyFirstConsonant((string)lyrics[6]), | ||
(string)lyrics[7], | ||
(string)lyrics[8] | ||
}; | ||
|
||
if (thisLyric[0] == "null") { | ||
return GenerateResult(FindInOto(notes[0].lyric, notes[0])); | ||
} | ||
|
||
return ConvertForCV(notes, prevLyric, thisLyric, nextLyric); | ||
|
||
} | ||
|
||
|
||
public override Result GenerateEndSound(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { | ||
Note note = notes[0]; | ||
if (prevNeighbour == null) { | ||
return GenerateResult(FindInOto(note.lyric, note)); | ||
} | ||
|
||
Note prevNeighbour_ = (Note)prevNeighbour; | ||
Hashtable lyrics = KoreanPhonemizerUtil.Separate(prevNeighbour_.lyric); | ||
|
||
string[] prevLyric = new string[]{ // "ㄴ", "ㅑ", "ㅇ" | ||
HandleEmptyFirstConsonant((string)lyrics[0]), | ||
(string)lyrics[1], | ||
(string)lyrics[2] | ||
}; | ||
|
||
string soundBeforeEndSound = prevLyric[2] == " " ? prevLyric[1] : prevLyric[2]; | ||
string endSound = note.lyric; | ||
string prevMidVowel; | ||
|
||
|
||
|
||
if (prevLyric[1] == "ㅢ") { | ||
if (isUsing_i) { | ||
prevMidVowel = $"{MIDDLE_VOWELS["ㅣ"][0]}"; | ||
} | ||
else { | ||
prevMidVowel = $"{MIDDLE_VOWELS["ㅢ"][0]}"; | ||
} | ||
} | ||
else{ | ||
prevMidVowel = MIDDLE_VOWELS.ContainsKey(soundBeforeEndSound) ? MIDDLE_VOWELS[soundBeforeEndSound][2] : LAST_CONSONANTS[soundBeforeEndSound][0]; | ||
} | ||
|
||
if (FindInOto($"{prevMidVowel} {endSound}", note, true) == null) { | ||
if (FindInOto($"{prevMidVowel}{endSound}", note, true) == null) { | ||
return GenerateResult(FindInOto($"{endSound}", note)); | ||
} | ||
return GenerateResult(FindInOto($"{prevMidVowel}{endSound}", note, true)); | ||
} | ||
return GenerateResult(FindInOto($"{prevMidVowel} {endSound}", note)); | ||
} | ||
} | ||
} |