From 4af7887b6fe3ef3ee39db310353b001048830de3 Mon Sep 17 00:00:00 2001 From: 2xxbin <2xxbin1208@gmail.com> Date: Wed, 16 Oct 2024 01:22:05 +0900 Subject: [PATCH 1/5] make zh cvv plus phonemizer --- .../ChineseCVVPlusPhonemizer.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs new file mode 100644 index 000000000..219472c91 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenUtau.Api; +using OpenUtau.Core; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Plugin.Builtin { + [Serializable] + public class ChineseCVVPlusConfigYaml { + public string VowelTailPrefix = "_"; + public bool UseSingleNasalVowel = false; + public bool UseSingleMultipleVowel = false; + public string[] SupportedTailBreath = {","}; + public Dictionary CustomTailVowel = new Dictionary() { + {"ian", "ian"}, + }; + } + + + [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] + public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { + static readonly String[] CONSONANTS = {"zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "z", "c", "s", "r", "j", "q", "x", "g", "k", "h"}; + static readonly String[] SINGLE_VOWELS = {"a", "o", "e", "i", "u", "v", "er"}; + static readonly String[] MULTIPLE_VOWELS = {"ai", "ei", "ao", "ou", "ia", "iao", "ie", "iou", "ua", "uo", "uai", "uei", "ve"}; + static readonly String[] NESAL_VOWELS = {"an", "en", "ang", "eng", "ong", "ian", "iang", "ing", "iong", "uan", "uen", "uang", "ueng", "van", "vn"}; + List? CombineVowels; + List? Vowels; + static readonly Dictionary DEFAULT_TAIL_VOWELS = new Dictionary() { + {"ai", "ai"}, + {"ei", "ei"}, + {"ao", "ao"}, + {"ou", "ou"}, + {"an", "an"}, + {"en", "en"}, + {"ang", "ang"}, + {"eng", "eng"}, + {"ong", "ong"}, + {"ia", "ia"}, + {"iao", "ao"}, + {"ie", "ie"}, + {"iu", "ou"}, + {"iou", "ou"}, + {"ian", "ian"}, + {"in", "in"}, + {"iang", "ang"}, + {"ing", "ing"}, + {"iong", "ong"}, + {"ua", "ua"}, + {"uo", "uo"}, + {"uai", "ai"}, + {"ui", "ei"}, + {"uei", "ei"}, + {"uan", "an"}, + {"un", "uen"}, + {"uang", "ang"}, + {"ueng", "eng"}, + {"ve", "ve"}, + {"van", "en"}, + {"vn", "vn"} + }; + Dictionary tailVowels; + private USinger? singer; + ChineseCVVPlusConfigYaml SettingYaml; + public override void SetSinger(USinger singer) { + if(singer == null) { + return; + } + this.singer = singer; + + CombineVowels = MULTIPLE_VOWELS.Concat(NESAL_VOWELS).ToList(); + Vowels = SINGLE_VOWELS.Concat(CombineVowels).ToList(); + + var configPath = Path.Join(singer.Location, "zhcvvplus.yaml"); + + if(!File.Exists(configPath)) { + Log.Information("Cannot Find zhcvvplus.yaml, creating a new one..."); + var defaultConfig = new ChineseCVVPlusConfigYaml {}; + var configContent = Yaml.DefaultDeserializer.Serialize(defaultConfig); + File.WriteAllText(configPath, configContent); + Log.Information("New zhcvvplus.yaml created with default settings."); + } + + try { + var configContent = File.ReadAllText(configPath); + SettingYaml = Yaml.DefaultDeserializer.Deserialize(configContent); + }catch (Exception e) { + Log.Error(e, $"Failed to load zhcvvplus.yaml (configPath: '{configPath}')"); + } + + + tailVowels = new Dictionary(DEFAULT_TAIL_VOWELS); + foreach (var customTailVowel in SettingYaml.CustomTailVowel) { + tailVowels[customTailVowel.Key] = customTailVowel.Value; + } + + } + + private string getLryicVowel(string lryic) { + string prefix = lryic.Substring(0, Math.Min(2, lryic.Length)); + string suffix = lryic.Length > 2 ? lryic.Substring(2) : ""; + + 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(); + } + + 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 (tailVowels.ContainsKey(lryicVowel)) { + var tailPhoneme = $"{SettingYaml.VowelTailPrefix}{tailVowels[lryicVowel]}"; + + if ((totalDuration <= 480 && (SettingYaml.UseSingleNasalVowel && NESAL_VOWELS.Contains(lryicVowel) || + SettingYaml.UseSingleMultipleVowel && MULTIPLE_VOWELS.Contains(lryicVowel)) || + (!SettingYaml.UseSingleNasalVowel && NESAL_VOWELS.Contains(lryicVowel)) || + (!SettingYaml.UseSingleMultipleVowel && MULTIPLE_VOWELS.Contains(lryicVowel)))) { + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = phoneme }, + new Phoneme { phoneme = tailPhoneme, position = totalDuration - Math.Min(totalDuration / 3, 300) }, + } + }; + } + }; + + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme, + } + } + }; + } catch (Exception e) { + Log.Error(e, "zh cvv+ error"); + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = "ERROR", + } + } + }; + } + } + } +} \ No newline at end of file From f5a0690934c9e4871d94d1d3742155ae3ea99903 Mon Sep 17 00:00:00 2001 From: 2xxbin <2xxbin1208@gmail.com> Date: Wed, 16 Oct 2024 22:53:13 +0900 Subject: [PATCH 2/5] Chinese CVV Plus Phonemizer first completed --- .../ChineseCVVPlusPhonemizer.cs | 165 +++++++++++++----- 1 file changed, 120 insertions(+), 45 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs index 219472c91..155d7d78b 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs @@ -5,6 +5,9 @@ using OpenUtau.Api; using OpenUtau.Core; using OpenUtau.Core.Ustx; +using YamlDotNet.Core; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.EventEmitters; using Serilog; namespace OpenUtau.Plugin.Builtin { @@ -13,22 +16,23 @@ public class ChineseCVVPlusConfigYaml { public string VowelTailPrefix = "_"; public bool UseSingleNasalVowel = false; public bool UseSingleMultipleVowel = false; - public string[] SupportedTailBreath = {","}; - public Dictionary CustomTailVowel = new Dictionary() { - {"ian", "ian"}, + public bool UseRetan = false; + public string[] SupportedTailBreath = {"-"}; + 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"}; + public string[] MultipleVowelDict = {"ai", "ei", "ao", "ou", "ia", "iao", "ie", "iou", "ua", "uo", "uai", "uei", "ui", "ve"}; + + public int FastTailVowelTimingTick = 100; + public int SingleVowelsReferenceTimimgTick = 480; + public Dictionary FastTailVowelDict = new Dictionary() { + {"ia", "ia"}, + {"ie", "ie"}, + {"ua", "ua"}, + {"uo", "uo"}, + {"ve", "ve"}, }; - } - - - [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] - public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { - static readonly String[] CONSONANTS = {"zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "z", "c", "s", "r", "j", "q", "x", "g", "k", "h"}; - static readonly String[] SINGLE_VOWELS = {"a", "o", "e", "i", "u", "v", "er"}; - static readonly String[] MULTIPLE_VOWELS = {"ai", "ei", "ao", "ou", "ia", "iao", "ie", "iou", "ua", "uo", "uai", "uei", "ve"}; - static readonly String[] NESAL_VOWELS = {"an", "en", "ang", "eng", "ong", "ian", "iang", "ing", "iong", "uan", "uen", "uang", "ueng", "van", "vn"}; - List? CombineVowels; - List? Vowels; - static readonly Dictionary DEFAULT_TAIL_VOWELS = new Dictionary() { + public Dictionary SlowTailVowelDict = new Dictionary() { {"ai", "ai"}, {"ei", "ei"}, {"ao", "ao"}, @@ -38,9 +42,7 @@ public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { {"ang", "ang"}, {"eng", "eng"}, {"ong", "ong"}, - {"ia", "ia"}, {"iao", "ao"}, - {"ie", "ie"}, {"iu", "ou"}, {"iou", "ou"}, {"ian", "ian"}, @@ -48,8 +50,6 @@ public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { {"iang", "ang"}, {"ing", "ing"}, {"iong", "ong"}, - {"ua", "ua"}, - {"uo", "uo"}, {"uai", "ai"}, {"ui", "ei"}, {"uei", "ei"}, @@ -57,79 +57,154 @@ public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { {"un", "uen"}, {"uang", "ang"}, {"ueng", "eng"}, - {"ve", "ve"}, {"van", "en"}, - {"vn", "vn"} + {"vn", "vn"}, }; - Dictionary tailVowels; + } + + class FlowStyleIntegerSequences : ChainedEventEmitter { + public FlowStyleIntegerSequences(IEventEmitter nextEmitter) + : base(nextEmitter) {} + + public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter) { + eventInfo = new SequenceStartEventInfo(eventInfo.Source) { + Style = YamlDotNet.Core.Events.SequenceStyle.Flow + }; + + nextEmitter.Emit(eventInfo, emitter); + } + } + + + [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] + public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { + String[] Consonants; + String[] SingleVowels; + String[] MultipleVowels; + String[] NesalVowels; + String[] TailBreaths; + Dictionary FastTailVowels; + Dictionary SlowTailVowels; + Dictionary TailVowels; private USinger? singer; ChineseCVVPlusConfigYaml SettingYaml; public override void SetSinger(USinger singer) { + if(singer == null) { return; } - this.singer = singer; - - CombineVowels = MULTIPLE_VOWELS.Concat(NESAL_VOWELS).ToList(); - Vowels = SINGLE_VOWELS.Concat(CombineVowels).ToList(); var configPath = Path.Join(singer.Location, "zhcvvplus.yaml"); if(!File.Exists(configPath)) { Log.Information("Cannot Find zhcvvplus.yaml, creating a new one..."); - var defaultConfig = new ChineseCVVPlusConfigYaml {}; - var configContent = Yaml.DefaultDeserializer.Serialize(defaultConfig); + var serializer = new SerializerBuilder().WithEventEmitter(next => new FlowStyleIntegerSequences(next)).Build(); + var configContent = serializer.Serialize(new ChineseCVVPlusConfigYaml {}); File.WriteAllText(configPath, configContent); Log.Information("New zhcvvplus.yaml created with default settings."); } try { var configContent = File.ReadAllText(configPath); - SettingYaml = Yaml.DefaultDeserializer.Deserialize(configContent); + var deserializer = new DeserializerBuilder().Build(); + SettingYaml = deserializer.Deserialize(configContent); }catch (Exception e) { Log.Error(e, $"Failed to load zhcvvplus.yaml (configPath: '{configPath}')"); } - - tailVowels = new Dictionary(DEFAULT_TAIL_VOWELS); - foreach (var customTailVowel in SettingYaml.CustomTailVowel) { - tailVowels[customTailVowel.Key] = customTailVowel.Value; - } + Consonants = SettingYaml.ConsonantDict.OrderByDescending(c => c.Length).ToArray(); + SingleVowels = SettingYaml.SingleVowelDict; + MultipleVowels = SettingYaml.MultipleVowelDict; + NesalVowels = SettingYaml.NesalVowelDict; + FastTailVowels = SettingYaml.FastTailVowelDict; + SlowTailVowels = SettingYaml.SlowTailVowelDict; + TailVowels = FastTailVowels.Concat(SlowTailVowels).ToDictionary(g => g.Key, g => g.Value); + TailBreaths = SettingYaml.SupportedTailBreath; + this.singer = singer; } private string getLryicVowel(string lryic) { string prefix = lryic.Substring(0, Math.Min(2, lryic.Length)); string suffix = lryic.Length > 2 ? lryic.Substring(2) : ""; - foreach (var consonant in CONSONANTS) { + 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(); } + public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note) { + var color = string.Empty; + var toneShift = 0; + int? alt = null; + if (phoneme.Equals("")) { + return false; + } + + if (singer.TryGetMappedOto(phoneme + alt, note.tone + toneShift, color, out var otoAlt)) { + return true; + } else if (singer.TryGetMappedOto(phoneme, note.tone + toneShift, color, out var oto)) { + return true; + } else if (singer.TryGetMappedOto(phoneme, note.tone, color, out oto)) { + return true; + } + + 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(), + position = totalDuration - ((totalDuration / phoneticHints.Length) * (phoneticHints.Length - phoneticHint.index)), + }; + } + + return new Result { + phonemes = phonemes, + }; + } + + if(TailBreaths.Contains(phoneme) && prev != null) { + return new Result { + phonemes = new Phoneme[] { new Phoneme { phoneme = $"{getLryicVowel(prev?.lyric)} {phoneme}" } } + }; + } + + if (SettingYaml.UseRetan && prev == null && isExistPhonemeInOto(singer, $"- {phoneme}", notes[0])) { + phoneme = $"- {phoneme}"; + } + + if (TailVowels.ContainsKey(lryicVowel)) { + var tailPhoneme = $"{SettingYaml.VowelTailPrefix}{TailVowels[lryicVowel]}"; - if (tailVowels.ContainsKey(lryicVowel)) { - var tailPhoneme = $"{SettingYaml.VowelTailPrefix}{tailVowels[lryicVowel]}"; - - if ((totalDuration <= 480 && (SettingYaml.UseSingleNasalVowel && NESAL_VOWELS.Contains(lryicVowel) || - SettingYaml.UseSingleMultipleVowel && MULTIPLE_VOWELS.Contains(lryicVowel)) || - (!SettingYaml.UseSingleNasalVowel && NESAL_VOWELS.Contains(lryicVowel)) || - (!SettingYaml.UseSingleMultipleVowel && MULTIPLE_VOWELS.Contains(lryicVowel)))) { + if ((totalDuration <= SettingYaml.SingleVowelsReferenceTimimgTick && + (SettingYaml.UseSingleNasalVowel && NesalVowels.Contains(lryicVowel) || SettingYaml.UseSingleMultipleVowel && MultipleVowels.Contains(lryicVowel)) || + (!SettingYaml.UseSingleNasalVowel && NesalVowels.Contains(lryicVowel)) || + (!SettingYaml.UseSingleMultipleVowel && MultipleVowels.Contains(lryicVowel)))) { + + var tailVowelPosition = totalDuration - totalDuration / 3; + if (FastTailVowels.ContainsKey(lryicVowel)) { + tailVowelPosition = SettingYaml.FastTailVowelTimingTick; + } + return new Result() { phonemes = new Phoneme[] { new Phoneme { phoneme = phoneme }, - new Phoneme { phoneme = tailPhoneme, position = totalDuration - Math.Min(totalDuration / 3, 300) }, + new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition }, } }; } From 9c3d78beb26972ee9b8a2480a08549a8fa1ac721 Mon Sep 17 00:00:00 2001 From: 2xxbin <2xxbin1208@gmail.com> Date: Thu, 17 Oct 2024 00:47:46 +0900 Subject: [PATCH 3/5] Add Korean comments. To be translated into English later. --- .../ChineseCVVPlusPhonemizer.cs | 88 +++++++++++++++---- 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs index 155d7d78b..c7aea6c0e 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs @@ -11,20 +11,33 @@ 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 FastTailVowelDict = new Dictionary() { {"ia", "ia"}, {"ie", "ie"}, @@ -32,7 +45,9 @@ public class ChineseCVVPlusConfigYaml { {"uo", "uo"}, {"ve", "ve"}, }; + // 느린 복합운모. 느린 복합운모의 포지션은 노트의 1/3로 계산 됨 public Dictionary SlowTailVowelDict = new Dictionary() { + // {"모음의 기본형": "접모음의 접두사를 제외한 표기"} {"ai", "ai"}, {"ei", "ei"}, {"ao", "ao"}, @@ -62,6 +77,7 @@ public class ChineseCVVPlusConfigYaml { }; } + // yaml을 직렬화 할 때, 배열을 인라인 스타일로 만들기 위한 커스텀 이벤트 class FlowStyleIntegerSequences : ChainedEventEmitter { public FlowStyleIntegerSequences(IEventEmitter nextEmitter) : base(nextEmitter) {} @@ -76,17 +92,27 @@ 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 FastTailVowels; + // ChineseCVVPlusConfigYaml 의 SlowTailVowelDict Dictionary SlowTailVowels; + // FastTailVowels + SlowTailVowels Dictionary TailVowels; private USinger? singer; + // zhcvvplus.yaml를 담아두는 변수 ChineseCVVPlusConfigYaml SettingYaml; public override void SetSinger(USinger singer) { @@ -94,8 +120,10 @@ public override void SetSinger(USinger singer) { 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(); @@ -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(); @@ -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; @@ -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; @@ -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)), }; } @@ -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 라고 적어 출력 } } }; From 263e67db3bd504e9f87df24055f12d0aa0300718 Mon Sep 17 00:00:00 2001 From: EX3 Date: Thu, 17 Oct 2024 11:26:10 +0900 Subject: [PATCH 4/5] Added summary, clr & tone suffix support, Simplified code --- .../ChineseCVVPlusPhonemizer.cs | 598 ++++++++++-------- 1 file changed, 341 insertions(+), 257 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs index c7aea6c0e..3fcb3444c 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -11,280 +11,364 @@ using Serilog; namespace OpenUtau.Plugin.Builtin { - // zhcvvplus.yaml 클래스 - [Serializable] - public class ChineseCVVPlusConfigYaml { - // 접모음의 접두사 지정. 기본값 "_" - public string VowelTailPrefix = "_"; - // 일정 길이 이상의 비운모를 접모음 없이 사용할 건지에 대한 여부 - public bool UseSingleNasalVowel = false; - // 일정 길이 이상의 복합운모를 접모음 없이 사용할 건지에 대한 여부 - public bool UseSingleMultipleVowel = false; - // 연단음 여부. 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"}; - // 운모 지정. 위와 동일한 이유로 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 FastTailVowelDict = new Dictionary() { - {"ia", "ia"}, - {"ie", "ie"}, - {"ua", "ua"}, - {"uo", "uo"}, - {"ve", "ve"}, - }; - // 느린 복합운모. 느린 복합운모의 포지션은 노트의 1/3로 계산 됨 - public Dictionary SlowTailVowelDict = new Dictionary() { - // {"모음의 기본형": "접모음의 접두사를 제외한 표기"} - {"ai", "ai"}, - {"ei", "ei"}, - {"ao", "ao"}, - {"ou", "ou"}, - {"an", "an"}, - {"en", "en"}, - {"ang", "ang"}, - {"eng", "eng"}, - {"ong", "ong"}, - {"iao", "ao"}, - {"iu", "ou"}, - {"iou", "ou"}, - {"ian", "ian"}, - {"in", "in"}, - {"iang", "ang"}, - {"ing", "ing"}, - {"iong", "ong"}, - {"uai", "ai"}, - {"ui", "ei"}, - {"uei", "ei"}, - {"uan", "an"}, - {"un", "uen"}, - {"uang", "ang"}, - {"ueng", "eng"}, - {"van", "en"}, - {"vn", "vn"}, - }; - } - - // yaml을 직렬화 할 때, 배열을 인라인 스타일로 만들기 위한 커스텀 이벤트 - class FlowStyleIntegerSequences : ChainedEventEmitter { - public FlowStyleIntegerSequences(IEventEmitter nextEmitter) - : base(nextEmitter) {} - - public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter) { - eventInfo = new SequenceStartEventInfo(eventInfo.Source) { - Style = YamlDotNet.Core.Events.SequenceStyle.Flow + /// + /// zhcvvplus.yaml 클래스 + /// + [Serializable] + public class ChineseCVVPlusConfigYaml { + + /// + /// 접모음의 접두사 지정. 기본값 "_" + /// + public string VowelTailPrefix = "_"; + + /// + /// 일정 길이 이상의 비운모를 접모음 없이 사용할 건지에 대한 여부 + /// + public bool UseSingleNasalVowel = false; + + /// + /// 일정 길이 이상의 복합운모를 접모음 없이 사용할 건지에 대한 여부 + /// + public bool UseSingleMultipleVowel = false; + + /// + /// 연단음 여부. 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" }; + + /// + /// 운모 지정. 위와 동일한 이유로 yaml에 분리 + /// + public string[] SingleVowelDict = { "a", "o", "e", "i", "u", "v", "er" }; + + /// + /// 비운모 지정. 위와 동일한 이유로 yaml에 분리 + /// + public string[] NasalVowelDict = { "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 FastTailVowelDict = new Dictionary() { + {"ia", "ia"}, + {"ie", "ie"}, + {"ua", "ua"}, + {"uo", "uo"}, + {"ve", "ve"}, + }; + /// + /// 느린 복합운모. 느린 복합운모의 포지션은 노트의 1/3로 계산 됨 + ///

+ /// {"모음의 기본형": "접모음의 접두사를 제외한 표기"} + ///
+ public Dictionary SlowTailVowelDict = new Dictionary() + { + {"ai", "ai"}, + {"ei", "ei"}, + {"ao", "ao"}, + {"ou", "ou"}, + {"an", "an"}, + {"en", "en"}, + {"ang", "ang"}, + {"eng", "eng"}, + {"ong", "ong"}, + {"iao", "ao"}, + {"iu", "ou"}, + {"iou", "ou"}, + {"ian", "ian"}, + {"in", "in"}, + {"iang", "ang"}, + {"ing", "ing"}, + {"iong", "ong"}, + {"uai", "ai"}, + {"ui", "ei"}, + {"uei", "ei"}, + {"uan", "an"}, + {"un", "uen"}, + {"uang", "ang"}, + {"ueng", "eng"}, + {"van", "en"}, + {"vn", "vn"}, }; - nextEmitter.Emit(eventInfo, emitter); - } - } - - - // 포네마이저 - [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] - public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { - // ChineseCVVPlusConfigYaml 의 ConsonantDict - String[] Consonants; - // ChineseCVVPlusConfigYaml 의 SingleVowelDict - String[] SingleVowels; - // ChineseCVVPlusConfigYaml 의 MultipleVowelDict - String[] MultipleVowels; - // ChineseCVVPlusConfigYaml 의 NesalVowelDict - String[] NesalVowels; - // ChineseCVVPlusConfigYaml 의 SupportedTailBreath - String[] TailBreaths; - // ChineseCVVPlusConfigYaml 의 FastTailVowelDict - Dictionary FastTailVowels; - // ChineseCVVPlusConfigYaml 의 SlowTailVowelDict - Dictionary SlowTailVowels; - // FastTailVowels + SlowTailVowels - Dictionary 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(); - var configContent = serializer.Serialize(new ChineseCVVPlusConfigYaml {}); - File.WriteAllText(configPath, configContent); - Log.Information("New zhcvvplus.yaml created with default settings."); - } - - // zhcvvplus.yaml 읽기 - try { - var configContent = File.ReadAllText(configPath); - var deserializer = new DeserializerBuilder().Build(); - SettingYaml = deserializer.Deserialize(configContent); - }catch (Exception e) { - Log.Error(e, $"Failed to load zhcvvplus.yaml (configPath: '{configPath}')"); - } - - // yaml 안의 내용을 변수에 지정 - Consonants = SettingYaml.ConsonantDict.OrderByDescending(c => c.Length).ToArray(); // 후의 replace에 사용되기 때문에 글자수가 긴 순으로 내림차순 정렬 함. - SingleVowels = SettingYaml.SingleVowelDict; - MultipleVowels = SettingYaml.MultipleVowelDict; - NesalVowels = SettingYaml.NesalVowelDict; - FastTailVowels = SettingYaml.FastTailVowelDict; - SlowTailVowels = SettingYaml.SlowTailVowelDict; - 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(); - } + [YamlIgnore] + public Dictionary TailVowels { + get { + return FastTailVowelDict.Concat(SlowTailVowelDict).ToDictionary(g => g.Key, g => g.Value); + } + } + - // oto.ini 안에 해당 에일리어스가 있는지 확인하는 메소드. - public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note) { - var color = string.Empty; - var toneShift = 0; - int? alt = null; - if (phoneme.Equals("")) { - return false; - } - - if (singer.TryGetMappedOto(phoneme + alt, note.tone + toneShift, color, out var otoAlt)) { - return true; - } else if (singer.TryGetMappedOto(phoneme, note.tone + toneShift, color, out var oto)) { - return true; - } else if (singer.TryGetMappedOto(phoneme, note.tone, color, out oto)) { - return true; - } - - return false; + [YamlIgnore] + public string[] Consonants { + get { + return ConsonantDict.OrderByDescending(c => c.Length).ToArray(); + } + } } - // 음소 처리 - 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)), + /// + /// yaml을 직렬화 할 때, 배열을 인라인 스타일로 만들기 위한 커스텀 이벤트 + /// + class FlowStyleIntegerSequences : ChainedEventEmitter { + public FlowStyleIntegerSequences(IEventEmitter nextEmitter) + : base(nextEmitter) { } + + public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter) { + eventInfo = new SequenceStartEventInfo(eventInfo.Source) { + Style = YamlDotNet.Core.Events.SequenceStyle.Flow }; - } - return new Result { - phonemes = phonemes, - }; + nextEmitter.Emit(eventInfo, emitter); } + } + + + /// + /// 포네마이저 + /// + [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] + public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { + private USinger? singer; + /// + /// zhcvvplus.yaml를 담아두는 변수 + /// + ChineseCVVPlusConfigYaml Config; + public override void SetSinger(USinger singer) { + + if (singer == null) { + return; + } - // 만약 노트가 끝 어미숨 노트라면 - if(TailBreaths.Contains(phoneme) && prev != null) { - return new Result { - // "모음의 기본 형태 + 가사로 작성한 어미숨" 형태로 출력 - phonemes = new Phoneme[] { new Phoneme { phoneme = $"{getLryicVowel(prev?.lyric)} {phoneme}" } } - }; + // zhcvvplus.yaml 경로 지정 + var configPath = Path.Join(singer.Location, "zhcvvplus.yaml"); + + // 만약 없다면, 새로 제작해 추가 + if (!File.Exists(configPath)) { + CreateConfigChineseCVVPlus(configPath); + } + + // zhcvvplus.yaml 읽기 + try { + var configContent = File.ReadAllText(configPath); + var deserializer = new DeserializerBuilder().Build(); + Config = deserializer.Deserialize(configContent); + } catch (Exception e) { + Log.Error(e, $"Failed to load zhcvvplus.yaml (configPath: '{configPath}')"); + try { + CreateConfigChineseCVVPlus(configPath); + } catch (Exception e2) { + Log.Error(e2, "Failed to create zhcvvplus.yaml"); + } + } + + // 음원 지정 + this.singer = singer; + + if (Config == null) { + Log.Error("Failed to load zhcvvplus.yaml, using default settings."); + Config = new ChineseCVVPlusConfigYaml(); + } } - // 만약 zhcvvplus.yaml에서 연단음 여부가 True고, 앞 노트가 없으면서, oto.ini에 "- 가사" 에일리어스가 존재한다면 - if (SettingYaml.UseRetan && prev == null && isExistPhonemeInOto(singer, $"- {phoneme}", notes[0])) { - // 가사를 "- 가사"로 변경 - phoneme = $"- {phoneme}"; + // 노트의 가사를 받아 모음을 반환하는 메소드. + private string GetLyricVowel(string lyric) { + string initialPrefix = string.Empty; + + // Handle the case that first character is not an alphabet(e.g "- qian") - remove it until the first alphabet apears, otherwise GetLyricVowel will return its lyric as it is. + while (!char.IsLetter(lyric.First())) { + initialPrefix += lyric.First(); + lyric = lyric.Remove(0, 1); + if (lyric.Length == 0) { + return lyric; + } + } + + // 자음 뿐만이 아닌 모음도 제거가 되는 문제(ian -> ia 등) 을 방지하기 위해 가사의 앞 2글자 분리 + string prefix = lyric.Substring(0, Math.Min(2, lyric.Length)); + string suffix = lyric.Length > 2 ? lyric.Substring(2) : string.Empty; + + // 자음 리스트를 순서대로 선회하며 replace 됨. + foreach (var consonant in Config.Consonants) { + if (prefix.StartsWith(consonant)) { + prefix = prefix.Replace(consonant, string.Empty); + } + } + + // 모음 표기를 일반 표기로 변경 + return $"{initialPrefix}{(prefix + suffix).Replace("yu", "v").Replace("y", "i").Replace("w", "u").Trim()}"; } - // 만약 접모음이 필요한 가사라면 - 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; + // oto.ini 안에 해당 에일리어스가 있는지 확인하는 메소드. + public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note) { + + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string color = attr.voiceColor ?? string.Empty; + + var toneShift = 0; + int? alt = null; + if (phoneme.Equals(string.Empty)) { + return false; } - return new Result() { - phonemes = new Phoneme[] { - new Phoneme { phoneme = phoneme }, // 원 노트 가사 - new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition }, // 접모음 - } - }; - } - }; + if (singer.TryGetMappedOto(phoneme + alt, note.tone + toneShift, color, out var otoAlt)) { + return true; + } else if (singer.TryGetMappedOto(phoneme, note.tone + toneShift, color, out var oto)) { + return true; + } else if (singer.TryGetMappedOto(phoneme, note.tone, color, out oto)) { + return true; + } + + return false; + } - // 위 if문중 어디에도 해당하지 않는다면 - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = phoneme, // 입력한 가사로 출력 + static string GetOtoAlias(USinger singer, string phoneme, Note note) { + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string color = attr.voiceColor ?? string.Empty; + int? alt = attr.alternate; + var toneShift = attr.toneShift; + + + if (singer.TryGetMappedOto(phoneme + alt, note.tone + toneShift, color, out var otoAlt)) { + return otoAlt.Alias; + } else if (singer.TryGetMappedOto(phoneme, note.tone + toneShift, color, out var oto)) { + return oto.Alias; + } else if (singer.TryGetMappedOto(phoneme, note.tone, color, out oto)) { + return oto.Alias; } - } - }; - } catch (Exception e) { // 처리 과정중 오류가 생긴다면 - Log.Error(e, "An error occurred during the phoneme processing in zh cvv+ module."); // 로깅 + return phoneme; + } - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = "ERROR", // 가사에 ERROR 라고 적어 출력 + void CreateConfigChineseCVVPlus(string configPath) { + Log.Information("Cannot Find zhcvvplus.yaml, creating a new one..."); + var serializer = new SerializerBuilder().WithEventEmitter(next => new FlowStyleIntegerSequences(next)).Build(); + var configContent = serializer.Serialize(new ChineseCVVPlusConfigYaml { }); + File.WriteAllText(configPath, configContent); + Log.Information("New zhcvvplus.yaml created with default values."); + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + try { + + int totalDuration = notes.Sum(n => n.duration); + string phoneme = notes[0].lyric; + string? lryicVowel = GetLyricVowel(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 = GetOtoAlias(singer, phoneticHint.hint.Trim(), notes[0]) , + // 포지션은 균등하게 n등분 + position = totalDuration - ((totalDuration / phoneticHints.Length) * (phoneticHints.Length - phoneticHint.index)), + }; + } + + return new Result { + phonemes = phonemes, + }; + } + + // 만약 노트가 끝 어미숨 노트라면 + if (Config.SupportedTailBreath.Contains(phoneme) && prev != null) { + phoneme = GetOtoAlias(singer, $"{GetLyricVowel(prev?.lyric)} {phoneme}", notes[0]); + + return new Result { + // "모음의 기본 형태 + 가사로 작성한 어미숨" 형태로 출력 + phonemes = new Phoneme[] { new Phoneme { phoneme = phoneme } } + }; + } + + // 만약 zhcvvplus.yaml에서 연단음 여부가 True고, 앞 노트가 없으면서, oto.ini에 "- 가사" 에일리어스가 존재한다면 + if (Config.UseRetan && prev == null && isExistPhonemeInOto(singer, $"- {phoneme}", notes[0])) { + // 가사를 "- 가사"로 변경 + phoneme = $"- {phoneme}"; + phoneme = GetOtoAlias(singer, phoneme, notes[0]); + } + + // 만약 접모음이 필요한 가사라면 + if (Config.TailVowels.ContainsKey(lryicVowel)) { + // 접노트 가사 선언 + var tailPhoneme = $"{Config.VowelTailPrefix}{Config.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 <= Config.SingleVowelsReferenceTimimgTick && + (Config.UseSingleNasalVowel && Config.NasalVowelDict.Contains(lryicVowel) + || Config.UseSingleMultipleVowel && Config.MultipleVowelDict.Contains(lryicVowel)) || + (!Config.UseSingleNasalVowel && Config.NasalVowelDict.Contains(lryicVowel)) || + (!Config.UseSingleMultipleVowel && Config.MultipleVowelDict.Contains(lryicVowel)))) { + + // 자연스러움을 위해 접운모의 위치는 노트의 1/3로 지정 + var tailVowelPosition = totalDuration - totalDuration / 3; + + // 만약 빠른 접모음 이라면 + if (Config.FastTailVowelDict.ContainsKey(lryicVowel)) { + // zhcvvplus.yaml에서 지정한 포지션으로 변경 + tailVowelPosition = Config.FastTailVowelTimingTick; + } + phoneme = GetOtoAlias(singer, phoneme, notes[0]); + tailPhoneme = GetOtoAlias(singer, tailPhoneme, notes[0]); + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = phoneme }, // 원 노트 가사 + new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition}, // 접모음 + } + }; + } + }; + + // 위 if문중 어디에도 해당하지 않는다면 + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme, // 입력한 가사로 출력 + } + } + }; + } 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", + } + } + }; } - } - }; - } + } } - } -} \ No newline at end of file +} From 3643eb6e8e1e7950e39d1810473e4f39306bebdd Mon Sep 17 00:00:00 2001 From: 2xxbin <2xxbin1208@gmail.com> Date: Thu, 17 Oct 2024 12:07:31 +0900 Subject: [PATCH 5/5] Translate the comments to English --- .../ChineseCVVPlusPhonemizer.cs | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs index 3fcb3444c..e6e3ee21f 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPlusPhonemizer.cs @@ -12,66 +12,66 @@ namespace OpenUtau.Plugin.Builtin { /// - /// zhcvvplus.yaml 클래스 + /// zhcvvplus.yaml class /// [Serializable] public class ChineseCVVPlusConfigYaml { /// - /// 접모음의 접두사 지정. 기본값 "_" + /// Prefix of affix vowel. Default value is "_" /// public string VowelTailPrefix = "_"; /// - /// 일정 길이 이상의 비운모를 접모음 없이 사용할 건지에 대한 여부 + /// Whether to use a long Nasal Vowel without a tail vowel /// public bool UseSingleNasalVowel = false; /// - /// 일정 길이 이상의 복합운모를 접모음 없이 사용할 건지에 대한 여부 + /// Whether to use a long Multiple Vowel without a tail vowel /// public bool UseSingleMultipleVowel = false; /// - /// 연단음 여부. True로 설정시 첫번째 노트의 가사에 "- " 추가 + /// Whether to use retan. If set to True, "- " is added to the first note's lyrics. /// public bool UseRetan = false; /// - /// 어미숨 종류. 여러개 사용 가능 + /// Types of End Breath. Multiple types can be used. /// public string[] SupportedTailBreath = { "-" }; /// - /// 성모 지정. 추가 자음이 생길 상황을 대비해 커스텀 가능하게 yaml에 분리 + /// Specify the Consonant. Separated into yaml for customization in case additional consonants are needed. /// public string[] ConsonantDict = { "zh", "ch", "sh", "b", "p", "m", "f", "d", "t", "n", "l", "z", "c", "s", "r", "j", "q", "x", "g", "k", "h" }; /// - /// 운모 지정. 위와 동일한 이유로 yaml에 분리 + /// Specify the Vowel. Separated into yaml for the same reason as above. /// public string[] SingleVowelDict = { "a", "o", "e", "i", "u", "v", "er" }; /// - /// 비운모 지정. 위와 동일한 이유로 yaml에 분리 + /// Specify the Nasal Vowel. Separated into yaml for the same reason as above. /// public string[] NasalVowelDict = { "an", "en", "ang", "eng", "ong", "ian", "iang", "ing", "iong", "uan", "uen", "un", "uang", "ueng", "van", "vn" }; /// - /// 복합운모 지정. 위와 동일한 이유로 yaml에 분리 + /// Specify the Multiple Vowel. Separated into yaml for the same reason as above. /// public string[] MultipleVowelDict = { "ai", "ei", "ao", "ou", "ia", "iao", "ie", "iou", "ua", "uo", "uai", "uei", "ui", "ve" }; /// - /// 빠른 접모음의 위치 (tick 단위). + /// Position of fast tail vowel (in ticks). /// public int FastTailVowelTimingTick = 100; /// - /// UseSingleNasalVowel 혹은 UseSingleMultipleVowel 가 True 일때, 단독 사용의 판단 기준 (tick 단위) + /// The criterion for determining single usage when UseSingleNasalVowel or UseSingleMultipleVowel is set to True (in ticks). /// public int SingleVowelsReferenceTimimgTick = 480; /// - /// 빠른 복합운모. 만일 빠른 복합운모의 비운모가 필요가 없다면 이부분은 비워두고 전부 SlowTailVowelDict 로 옮겨두면 됨 + /// Fast Multiple Vowel. If a fast Nasal Vowel is not needed, leave this empty and move everything to SlowTailVowelDict. /// public Dictionary FastTailVowelDict = new Dictionary() { {"ia", "ia"}, @@ -81,9 +81,9 @@ public class ChineseCVVPlusConfigYaml { {"ve", "ve"}, }; /// - /// 느린 복합운모. 느린 복합운모의 포지션은 노트의 1/3로 계산 됨 + /// Slow Multiple Vowel. The position of the slow Multiple Vowel is calculated as 1/3 of the note. ///

- /// {"모음의 기본형": "접모음의 접두사를 제외한 표기"} + /// {"Basic form of the vowel": "Representation excluding the prefix of the tail vowel"} ///
public Dictionary SlowTailVowelDict = new Dictionary() { @@ -133,7 +133,7 @@ public string[] Consonants { } /// - /// yaml을 직렬화 할 때, 배열을 인라인 스타일로 만들기 위한 커스텀 이벤트 + /// Custom event to make arrays in inline style when serializing yaml /// class FlowStyleIntegerSequences : ChainedEventEmitter { public FlowStyleIntegerSequences(IEventEmitter nextEmitter) @@ -150,13 +150,13 @@ public override void Emit(SequenceStartEventInfo eventInfo, IEmitter emitter) { /// - /// 포네마이저 + /// Phonemizer /// [Phonemizer("Chinese CVV Plus Phonemizer", "ZH CVV+", "2xxbin", language: "ZH")] public class ChineseCVVPlusPhonemizer : BaseChinesePhonemizer { private USinger? singer; /// - /// zhcvvplus.yaml를 담아두는 변수 + /// Variable containing zhcvvplus.yaml /// ChineseCVVPlusConfigYaml Config; public override void SetSinger(USinger singer) { @@ -165,15 +165,15 @@ public override void SetSinger(USinger singer) { return; } - // zhcvvplus.yaml 경로 지정 + // Specify the path of zhcvvplus.yaml var configPath = Path.Join(singer.Location, "zhcvvplus.yaml"); - // 만약 없다면, 새로 제작해 추가 + // If it doesn't exist, create and add it if (!File.Exists(configPath)) { CreateConfigChineseCVVPlus(configPath); } - // zhcvvplus.yaml 읽기 + // Read zhcvvplus.yaml try { var configContent = File.ReadAllText(configPath); var deserializer = new DeserializerBuilder().Build(); @@ -187,7 +187,7 @@ public override void SetSinger(USinger singer) { } } - // 음원 지정 + // Specify the singer this.singer = singer; if (Config == null) { @@ -196,7 +196,7 @@ public override void SetSinger(USinger singer) { } } - // 노트의 가사를 받아 모음을 반환하는 메소드. + // Method that takes the lyrics of a note and returns the vowel. private string GetLyricVowel(string lyric) { string initialPrefix = string.Empty; @@ -209,22 +209,22 @@ private string GetLyricVowel(string lyric) { } } - // 자음 뿐만이 아닌 모음도 제거가 되는 문제(ian -> ia 등) 을 방지하기 위해 가사의 앞 2글자 분리 + // Split the first two characters of the lyrics to prevent the issue of removing vowels, not just consonants (e.g., ian -> ia) string prefix = lyric.Substring(0, Math.Min(2, lyric.Length)); string suffix = lyric.Length > 2 ? lyric.Substring(2) : string.Empty; - // 자음 리스트를 순서대로 선회하며 replace 됨. + // Iterate through the consonant list in order and replace them. foreach (var consonant in Config.Consonants) { if (prefix.StartsWith(consonant)) { prefix = prefix.Replace(consonant, string.Empty); } } - // 모음 표기를 일반 표기로 변경 + // Convert vowel notation to the standard form return $"{initialPrefix}{(prefix + suffix).Replace("yu", "v").Replace("y", "i").Replace("w", "u").Trim()}"; } - // oto.ini 안에 해당 에일리어스가 있는지 확인하는 메소드. + // Method to check if the alias exists in oto.ini. public static bool isExistPhonemeInOto(USinger singer, string phoneme, Note note) { var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; @@ -279,16 +279,16 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN string phoneme = notes[0].lyric; string? lryicVowel = GetLyricVowel(notes[0].lyric); - // 만약 발음 힌트가 존재 한다면. + // If a phonetic hint exists. if (notes[0].phoneticHint != null) { - // 발음 힌트는 쉼표 기준으로 분리 됨. + // Phonetic hints are separated by commas. 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 = GetOtoAlias(singer, phoneticHint.hint.Trim(), notes[0]) , - // 포지션은 균등하게 n등분 + // The position is evenly divided into n parts. position = totalDuration - ((totalDuration / phoneticHints.Length) * (phoneticHints.Length - phoneticHint.index)), }; } @@ -298,68 +298,68 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }; } - // 만약 노트가 끝 어미숨 노트라면 + // If the note is an End Breath note if (Config.SupportedTailBreath.Contains(phoneme) && prev != null) { phoneme = GetOtoAlias(singer, $"{GetLyricVowel(prev?.lyric)} {phoneme}", notes[0]); return new Result { - // "모음의 기본 형태 + 가사로 작성한 어미숨" 형태로 출력 + // Output in the form "Basic vowel shape + End Breath written with lyrics" phonemes = new Phoneme[] { new Phoneme { phoneme = phoneme } } }; } - // 만약 zhcvvplus.yaml에서 연단음 여부가 True고, 앞 노트가 없으면서, oto.ini에 "- 가사" 에일리어스가 존재한다면 + // If retan is set to True in zhcvvplus.yaml, there is no previous note, and the "- lyrics" alias exists in oto.ini if (Config.UseRetan && prev == null && isExistPhonemeInOto(singer, $"- {phoneme}", notes[0])) { // 가사를 "- 가사"로 변경 phoneme = $"- {phoneme}"; phoneme = GetOtoAlias(singer, phoneme, notes[0]); } - // 만약 접모음이 필요한 가사라면 + // If the lyrics require a tail vowel if (Config.TailVowels.ContainsKey(lryicVowel)) { - // 접노트 가사 선언 + // Declare the lyrics for the connecting note var tailPhoneme = $"{Config.VowelTailPrefix}{Config.TailVowels[lryicVowel]}"; - // 1. 노트의 길이가 zhcvvplus.yaml의 판단 틱보다 작거나 같은 동시에 - // 1-1. 가사가 비운모면서 zhcvvplus.yaml의 비운모 단독 사용 여부가 True 거나 - // 1-2. 가사가 복합 운모면서 zhcvvplus.yaml의 복합운모 단독 사용 여부가 True 일때 - // 2. 혹은 zhcvvplus.yaml의 비운모 단독 사용 여부가 False 이면서 가사가 비운모일때 - // 3. 혹은 zhcvvplus.yaml의 복합운모 단독 사용 여부가 False 이면서 가사가 복합운모일때 + // 1. When the length of the note is less than or equal to the judgment tick in zhcvvplus.yaml + // 1-1. The lyrics are a Nasal Vowel, and the single use of Nasal Vowel in zhcvvplus.yaml is set to True, or + // 1-2. The lyrics are a Multiple Vowel, and the single use of Multiple Vowel in zhcvvplus.yaml is set to True + // 2. Or when the single use of Nasal Vowel in zhcvvplus.yaml is set to False, and the lyrics are a Nasal Vowel + // 3. Or when the single use of Multiple Vowel in zhcvvplus.yaml is set to False, and the lyrics are a Multiple Vowel if ((totalDuration <= Config.SingleVowelsReferenceTimimgTick && (Config.UseSingleNasalVowel && Config.NasalVowelDict.Contains(lryicVowel) || Config.UseSingleMultipleVowel && Config.MultipleVowelDict.Contains(lryicVowel)) || (!Config.UseSingleNasalVowel && Config.NasalVowelDict.Contains(lryicVowel)) || (!Config.UseSingleMultipleVowel && Config.MultipleVowelDict.Contains(lryicVowel)))) { - // 자연스러움을 위해 접운모의 위치는 노트의 1/3로 지정 + // To ensure naturalness, the position of the tail vowel is set to 1/3 of the note. var tailVowelPosition = totalDuration - totalDuration / 3; - // 만약 빠른 접모음 이라면 + // If it is a fast tail vowel, if (Config.FastTailVowelDict.ContainsKey(lryicVowel)) { - // zhcvvplus.yaml에서 지정한 포지션으로 변경 + // Change to the position specified in zhcvvplus.yaml. tailVowelPosition = Config.FastTailVowelTimingTick; } phoneme = GetOtoAlias(singer, phoneme, notes[0]); tailPhoneme = GetOtoAlias(singer, tailPhoneme, notes[0]); return new Result() { phonemes = new Phoneme[] { - new Phoneme { phoneme = phoneme }, // 원 노트 가사 - new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition}, // 접모음 + new Phoneme { phoneme = phoneme }, // Original note lyrics + new Phoneme { phoneme = tailPhoneme, position = tailVowelPosition}, // Tail vowel } }; } }; - // 위 if문중 어디에도 해당하지 않는다면 + // If it does not match any of the above if statements, return new Result { phonemes = new Phoneme[] { new Phoneme() { - phoneme = phoneme, // 입력한 가사로 출력 + phoneme = phoneme, // Output the entered lyrics. } } }; } catch (Exception e) { - Log.Error(e, "An error occurred during the phoneme processing in zh cvv+ module."); // 로깅 + Log.Error(e, "An error occurred during the phoneme processing in zh cvv+ module."); // Logging return new Result { phonemes = new Phoneme[] {