Skip to content

Commit

Permalink
Merge pull request #992 from EX3exp/ko-cv-phonemizer
Browse files Browse the repository at this point in the history
Add KoreanCVPhonemizer
  • Loading branch information
stakira authored Jan 25, 2024
2 parents 7b19118 + 6d2b50d commit 7fa2fc9
Showing 1 changed file with 310 additions and 0 deletions.
310 changes: 310 additions & 0 deletions OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs
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));
}
}
}

0 comments on commit 7fa2fc9

Please sign in to comment.