Skip to content

Commit

Permalink
Add Korean comments. To be translated into English later.
Browse files Browse the repository at this point in the history
  • Loading branch information
2xxbin authored and EX3exp committed Oct 17, 2024
1 parent f5a0690 commit 9c3d78b
Showing 1 changed file with 73 additions and 15 deletions.
88 changes: 73 additions & 15 deletions OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,43 @@
using Serilog;

namespace OpenUtau.Plugin.Builtin {
// zhcvvplus.yaml 클래스
[Serializable]
public class ChineseCVVPlusConfigYaml {
public string VowelTailPrefix = "_";
public class ChineseCVVPlusConfigYaml {
// 접모음의 접두사 지정. 기본값 "_"
public string VowelTailPrefix = "_";
// 일정 길이 이상의 비운모를 접모음 없이 사용할 건지에 대한 여부
public bool UseSingleNasalVowel = false;
// 일정 길이 이상의 복합운모를 접모음 없이 사용할 건지에 대한 여부
public bool UseSingleMultipleVowel = false;
public bool UseRetan = false;
public string[] SupportedTailBreath = {"-"};
// 연단음 여부. True로 설정시 첫번째 노트의 가사에 "- " 추가
public bool UseRetan = false;
// 어미숨 종류. 여러개 사용 가능
public string[] SupportedTailBreath = {"-"};
// 성모 지정. 추가 자음이 생길 상황을 대비해 커스텀 가능하게 yaml에 분리
public string[] ConsonantDict = {"zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "z", "c", "s", "r", "j", "q", "x", "g", "k", "h"};
public string[] SingleVowelDict = {"a", "o", "e", "i", "u", "v", "er"};
public string[] NesalVowelDict = {"an", "en", "ang", "eng", "ong", "ian", "iang", "ing", "iong", "uan", "uen", "un", "uang", "ueng", "van", "vn"};
// 운모 지정. 위와 동일한 이유로 yaml에 분리
public string[] SingleVowelDict = {"a", "o", "e", "i", "u", "v", "er"};
// 비운모 지정. 위와 동일한 이유로 yaml에 분리
public string[] NesalVowelDict = {"an", "en", "ang", "eng", "ong", "ian", "iang", "ing", "iong", "uan", "uen", "un", "uang", "ueng", "van", "vn"};
// 복합운모 지정. 위와 동일한 이유로 yaml에 분리
public string[] MultipleVowelDict = {"ai", "ei", "ao", "ou", "ia", "iao", "ie", "iou", "ua", "uo", "uai", "uei", "ui", "ve"};

// 빠른 접모음의 위치 (tick 단위).
public int FastTailVowelTimingTick = 100;
// UseSingleNasalVowel 혹은 UseSingleMultipleVowel 가 True 일때, 단독 사용의 판단 기준 (tick 단위)
public int SingleVowelsReferenceTimimgTick = 480;
// 빠른 복합운모. 만일 빠른 복합운모의 비운모가 필요가 없다면 이부분은 비워두고 전부 SlowTailVowelDict 로 옮겨두면 됨
public Dictionary<string, string> FastTailVowelDict = new Dictionary<string, string>() {
{"ia", "ia"},
{"ie", "ie"},
{"ua", "ua"},
{"uo", "uo"},
{"ve", "ve"},
};
// 느린 복합운모. 느린 복합운모의 포지션은 노트의 1/3로 계산 됨
public Dictionary<string, string> SlowTailVowelDict = new Dictionary<string, string>() {
// {"모음의 기본형": "접모음의 접두사를 제외한 표기"}
{"ai", "ai"},
{"ei", "ei"},
{"ao", "ao"},
Expand Down Expand Up @@ -62,6 +77,7 @@ public class ChineseCVVPlusConfigYaml {
};
}

// yaml을 직렬화 할 때, 배열을 인라인 스타일로 만들기 위한 커스텀 이벤트
class FlowStyleIntegerSequences : ChainedEventEmitter {
public FlowStyleIntegerSequences(IEventEmitter nextEmitter)
: base(nextEmitter) {}
Expand All @@ -76,26 +92,38 @@ public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter) {
}


// 포네마이저
[Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")]
public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer {
String[] Consonants;
// ChineseCVVPlusConfigYaml 의 ConsonantDict
String[] Consonants;
// ChineseCVVPlusConfigYaml 의 SingleVowelDict
String[] SingleVowels;
// ChineseCVVPlusConfigYaml 의 MultipleVowelDict
String[] MultipleVowels;
// ChineseCVVPlusConfigYaml 의 NesalVowelDict
String[] NesalVowels;
// ChineseCVVPlusConfigYaml 의 SupportedTailBreath
String[] TailBreaths;
// ChineseCVVPlusConfigYaml 의 FastTailVowelDict
Dictionary<String, String> FastTailVowels;
// ChineseCVVPlusConfigYaml 의 SlowTailVowelDict
Dictionary<String, String> SlowTailVowels;
// FastTailVowels + SlowTailVowels
Dictionary<String, String> TailVowels;
private USinger? singer;
// zhcvvplus.yaml를 담아두는 변수
ChineseCVVPlusConfigYaml SettingYaml;
public override void SetSinger(USinger singer) {

if(singer == null) {
return;
}

// zhcvvplus.yaml 경로 지정
var configPath = Path.Join(singer.Location, "zhcvvplus.yaml");

// 만약 없다면, 새로 제작해 추가
if(!File.Exists(configPath)) {
Log.Information("Cannot Find zhcvvplus.yaml, creating a new one...");
var serializer = new SerializerBuilder().WithEventEmitter(next => new FlowStyleIntegerSequences(next)).Build();
Expand All @@ -104,6 +132,7 @@ public override void SetSinger(USinger singer) {
Log.Information("New zhcvvplus.yaml created with default settings.");
}

// zhcvvplus.yaml 읽기
try {
var configContent = File.ReadAllText(configPath);
var deserializer = new DeserializerBuilder().Build();
Expand All @@ -112,7 +141,8 @@ public override void SetSinger(USinger singer) {
Log.Error(e, $"Failed to load zhcvvplus.yaml (configPath: '{configPath}')");
}

Consonants = SettingYaml.ConsonantDict.OrderByDescending(c => c.Length).ToArray();
// yaml 안의 내용을 변수에 지정
Consonants = SettingYaml.ConsonantDict.OrderByDescending(c => c.Length).ToArray(); // 후의 replace에 사용되기 때문에 글자수가 긴 순으로 내림차순 정렬 함.
SingleVowels = SettingYaml.SingleVowelDict;
MultipleVowels = SettingYaml.MultipleVowelDict;
NesalVowels = SettingYaml.NesalVowelDict;
Expand All @@ -121,22 +151,28 @@ public override void SetSinger(USinger singer) {
TailVowels = FastTailVowels.Concat(SlowTailVowels).ToDictionary(g => g.Key, g => g.Value);
TailBreaths = SettingYaml.SupportedTailBreath;

// 음원 지정
this.singer = singer;
}

// 노트의 가사를 받아 모음을 반환하는 메소드.
private string getLryicVowel(string lryic) {
// 자음 뿐만이 아닌 모음도 제거가 되는 문제(ian -> ia 등) 을 방지하기 위해 가사의 앞 2글자 분리
string prefix = lryic.Substring(0, Math.Min(2, lryic.Length));
string suffix = lryic.Length > 2 ? lryic.Substring(2) : "";

// 자음 리스트를 순서대로 선회하며 replace 됨.
foreach (var consonant in Consonants) {
if (prefix.StartsWith(consonant)) {
prefix = prefix.Replace(consonant, "");
}
}

// 모음 표기를 일반 표기로 변경
return (prefix + suffix).Replace("yu", "v").Replace("y", "i").Replace("w", "u").Trim();
}

// oto.ini 안에 해당 에일리어스가 있는지 확인하는 메소드.
public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note) {
var color = string.Empty;
var toneShift = 0;
Expand All @@ -156,19 +192,24 @@ public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note
return false;
}

// 음소 처리
public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) {
try {
// 필요한 변수 선언
int totalDuration = notes.Sum(n => n.duration);
var phoneme = notes[0].lyric;
var lryicVowel = getLryicVowel(notes[0].lyric);

// 만약 발음 힌트가 존재 한다면.
if(notes[0].phoneticHint != null) {
// 발음 힌트는 쉼표 기준으로 분리 됨.
var phoneticHints = notes[0].phoneticHint.Split(",");
var phonemes = new Phoneme[phoneticHints.Length];

foreach(var phoneticHint in phoneticHints.Select((hint, index) => (hint, index))) {
phonemes[phoneticHint.index] = new Phoneme {
phoneme = phoneticHint.hint.Trim(),
// 포지션은 균등하게 n등분
position = totalDuration - ((totalDuration / phoneticHints.Length) * (phoneticHints.Length - phoneticHint.index)),
};
}
Expand All @@ -178,51 +219,68 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN
};
}

// 만약 노트가 끝 어미숨 노트라면
if(TailBreaths.Contains(phoneme) && prev != null) {
return new Result {
// "모음의 기본 형태 + 가사로 작성한 어미숨" 형태로 출력
phonemes = new Phoneme[] { new Phoneme { phoneme = $"{getLryicVowel(prev?.lyric)} {phoneme}" } }
};
}

// 만약 zhcvvplus.yaml에서 연단음 여부가 True고, 앞 노트가 없으면서, oto.ini에 "- 가사" 에일리어스가 존재한다면
if (SettingYaml.UseRetan && prev == null && isExistPhonemeInOto(singer, $"- {phoneme}", notes[0])) {
// 가사를 "- 가사"로 변경
phoneme = $"- {phoneme}";
}

// 만약 접모음이 필요한 가사라면
if (TailVowels.ContainsKey(lryicVowel)) {
// 접노트 가사 선언
var tailPhoneme = $"{SettingYaml.VowelTailPrefix}{TailVowels[lryicVowel]}";

// 1. 노트의 길이가 zhcvvplus.yaml의 판단 틱보다 작거나 같은 동시에
// 1-1. 가사가 비운모면서 zhcvvplus.yaml의 비운모 단독 사용 여부가 True 거나
// 1-2. 가사가 복합 운모면서 zhcvvplus.yaml의 복합운모 단독 사용 여부가 True 일때
// 2. 혹은 zhcvvplus.yaml의 비운모 단독 사용 여부가 False 이면서 가사가 비운모일때
// 3. 혹은 zhcvvplus.yaml의 복합운모 단독 사용 여부가 False 이면서 가사가 복합운모일때
if ((totalDuration <= SettingYaml.SingleVowelsReferenceTimimgTick &&
(SettingYaml.UseSingleNasalVowel && NesalVowels.Contains(lryicVowel) || SettingYaml.UseSingleMultipleVowel && MultipleVowels.Contains(lryicVowel)) ||
(!SettingYaml.UseSingleNasalVowel && NesalVowels.Contains(lryicVowel)) ||
(!SettingYaml.UseSingleMultipleVowel && MultipleVowels.Contains(lryicVowel)))) {


// 자연스러움을 위해 접운모의 위치는 노트의 1/3로 지정
var tailVowelPosition = totalDuration - totalDuration / 3;

// 만약 빠른 접모음 이라면
if (FastTailVowels.ContainsKey(lryicVowel)) {
// zhcvvplus.yaml에서 지정한 포지션으로 변경
tailVowelPosition = SettingYaml.FastTailVowelTimingTick;
}

return new Result() {
phonemes = new Phoneme[] {
new Phoneme { phoneme = phoneme },
new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition },
new Phoneme { phoneme = phoneme }, // 원 노트 가사
new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition }, // 접모음
}
};
}
};

// 위 if문중 어디에도 해당하지 않는다면
return new Result {
phonemes = new Phoneme[] {
new Phoneme() {
phoneme = phoneme,
phoneme = phoneme, // 입력한 가사로 출력
}
}
};
} catch (Exception e) {
Log.Error(e, "zh cvv+ error");
} catch (Exception e) { // 처리 과정중 오류가 생긴다면
Log.Error(e, "An error occurred during the phoneme processing in zh cvv+ module."); // 로깅

return new Result {
phonemes = new Phoneme[] {
new Phoneme() {
phoneme = "ERROR",
phoneme = "ERROR", // 가사에 ERROR 라고 적어 출력
}
}
};
Expand Down

0 comments on commit 9c3d78b

Please sign in to comment.