diff --git a/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs new file mode 100644 index 000000000..8949ffdfd --- /dev/null +++ b/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Core.DiffSinger{ + [Phonemizer("DiffSinger Korean Phonemizer", "DIFFS KO", language: "KO", author: "EX3")] + public class DiffSingerKoreanPhonemizer : DiffSingerBasePhonemizer{ + USinger singer; + DsConfig dsConfig; + string rootPath; + float frameMs; + InferenceSession linguisticModel; + InferenceSession durationModel; + IG2p g2p; + List phonemes; + DiffSingerSpeakerEmbedManager speakerEmbedManager; + + string defaultPause = "SP"; + + public override void SetSinger(USinger singer) { + this.singer = singer; + if (File.Exists(Path.Join(singer.Location, "dsdur", "dsconfig.yaml"))) { + rootPath = Path.Combine(singer.Location, "dsdur"); + } else { + rootPath = singer.Location; + } + //Load Config + var configPath = Path.Join(rootPath, "dsconfig.yaml"); + try { + var configTxt = File.ReadAllText(configPath); + dsConfig = Yaml.DefaultDeserializer.Deserialize(configTxt); + } catch(Exception e) { + Log.Error(e, $"failed to load dsconfig from {configPath}"); + return; + } + this.frameMs = dsConfig.frameMs(); + //Load g2p + g2p = LoadG2p(rootPath); + //Load phonemes list + string phonemesPath = Path.Combine(rootPath, dsConfig.phonemes); + phonemes = File.ReadLines(phonemesPath,singer.TextFileEncoding).ToList(); + //Load models + var linguisticModelPath = Path.Join(rootPath, dsConfig.linguistic); + try { + linguisticModel = new InferenceSession(linguisticModelPath); + } catch (Exception e) { + Log.Error(e, $"failed to load linguistic model from {linguisticModelPath}"); + return; + } + var durationModelPath = Path.Join(rootPath, dsConfig.dur); + try { + durationModel = new InferenceSession(durationModelPath); + } catch (Exception e) { + Log.Error(e, $"failed to load duration model from {durationModelPath}"); + return; + } + } + + string[] GetSymbols(Note note) { + //priority: + //1. phonetic hint + //2. query from g2p dictionary + //3. treat lyric as phonetic hint, including single phoneme + //4. default pause + if (!string.IsNullOrEmpty(note.phoneticHint)) { + // Split space-separated symbols into an array. + return note.phoneticHint.Split() + .Where(s => g2p.IsValidSymbol(s)) // skip the invalid symbols. + .ToArray(); + } + // User has not provided hint, query g2p dictionary. + var g2presult = g2p.Query(note.lyric) + ?? g2p.Query(note.lyric.ToLowerInvariant()); + if(g2presult != null) { + return g2presult; + } + //not founded in g2p dictionary, treat lyric as phonetic hint + var lyricSplited = note.lyric.Split() + .Where(s => g2p.IsValidSymbol(s)) // skip the invalid symbols. + .ToArray(); + if (lyricSplited.Length > 0) { + return lyricSplited; + } + return new string[] { defaultPause }; + } + + // public List stretch(IList source, double ratio, double endPos, bool isVowelWithSemiPhoneme) { + // // 이중모음(y, w) 뒤의 모음일 경우, 이 함수를 호출해서 모음의 startPos를 자신의 8분의 1 길이만큼 추가한다. (타이밍을 뒤로 민다) + // //source:音素时长序列,单位ms + // //ratio:缩放比例 + // //endPos:目标终点时刻,单位ms + // //输出:缩放后的音素位置,单位ms + // if (isVowelWithSemiPhoneme){ + // double startPos = endPos - source.Sum() * ratio; + // startPos /= 2; + // var result = CumulativeSum(source.Select(x => x * ratio).Prepend(0), startPos).ToList(); + // result.RemoveAt(result.Count - 1); + // return result; + // } + // else{ + // return stretch(source, ratio, endPos); + // } + // } + string GetSpeakerAtIndex(Note note, int index){ + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == index) ?? default; + var speaker = singer.Subbanks + .Where(subbank => subbank.Color == attr.voiceColor && subbank.toneSet.Contains(note.tone)) + .FirstOrDefault(); + if(speaker is null) { + return ""; + } + return speaker.Suffix; + } + + dsPhoneme[] GetDsPhonemes(Note note){ + return GetSymbols(note) + .Select((symbol, index) => new dsPhoneme(symbol, GetSpeakerAtIndex(note, index))) + .ToArray(); + } + + List ProcessWord(Note[] notes, bool isLastNote){ + var wordPhonemes = new List{ + new phonemesPerNote(-1, notes[0].tone) + }; + var dsPhonemes = GetDsPhonemes(notes[0]); + var isVowel = dsPhonemes.Select(s => isPlainVowel(s.Symbol)).ToArray(); + var symbols = dsPhonemes.Select(s => s.Symbol).ToArray(); + var isThisSemiVowel = dsPhonemes.Select(s => isSemiVowel(s.Symbol)).ToArray(); + + + var nonExtensionNotes = notes.Where(n=>!IsSyllableVowelExtensionNote(n)).ToArray(); + //distribute phonemes to notes + var noteIndex = 0; + for (int i = 0; i < dsPhonemes.Length; i++) { + if (isVowel[i] && noteIndex < nonExtensionNotes.Length && i == dsPhonemes.Length - 1) { + // 받침 없는 노트 + var note = nonExtensionNotes[noteIndex]; + wordPhonemes.Add(new phonemesPerNote(note.position, note.tone)); + noteIndex++; + } + else if (isVowel[i] && noteIndex < nonExtensionNotes.Length && i == dsPhonemes.Length - 2) { + // 받침 있는 노트 + var note = nonExtensionNotes[noteIndex]; + wordPhonemes.Add(new phonemesPerNote(note.position, note.tone)); + } + else if (isThisSemiVowel[i] && noteIndex < nonExtensionNotes.Length){ + // 반모음이 너무 짧으면 부자연스러우니 24분의 1만큼 늘려줌 + var note = nonExtensionNotes[noteIndex]; + wordPhonemes.Add(new phonemesPerNote(note.position - note.duration / 24, note.tone)); + } + + + wordPhonemes[^1].Phonemes.Add(dsPhonemes[i]); + } + return wordPhonemes; + } + + int makePos(int duration, int divider, int targetPos){ + return duration - Math.Min(duration / divider, targetPos); + } + + int framesBetweenTickPos(double tickPos1, double tickPos2) { + return (int)(timeAxis.TickPosToMsPos(tickPos2)/frameMs) + - (int)(timeAxis.TickPosToMsPos(tickPos1)/frameMs); + } + + + + protected override void ProcessPart(Note[][] phrase) { + + float padding = 1000f; //Padding time for consonants at the beginning of a sentence, ms + + float frameMs = dsConfig.frameMs(); + var startMs = timeAxis.TickPosToMsPos(phrase[0][0].position) - padding; + var lastNote = phrase[^1][^1]; + var endTick = lastNote.position+lastNote.duration; + //[(Tick position of note, [phonemes])] + //The first item of this list is for the consonants before the first note. + var phrasePhonemes = new List{ + new phonemesPerNote(-1,phrase[0][0].tone, new List{new dsPhoneme("SP", GetSpeakerAtIndex(phrase[0][0], 0))}) + }; + var notePhIndex = new List { 1 }; + String? next; + String? prev = null; + try{ + next = phrase[1][0].lyric; + } + catch{ + next = null; + } + int i = 0; + bool isLastNote = false; + foreach (var note in phrase) { + next = null; + if (i != phrase.Length - 1){ + next = phrase[i + 1][0].lyric; + } + + String? prevTemp = note[0].lyric; + + // Phoneme variation + if (KoreanPhonemizerUtil.IsHangeul(prevTemp)){ + // Debug.Print("prev: " + prev + "curr: " + character[0].lyric + "next: " + next); + note[0].lyric = KoreanPhonemizerUtil.Variate(prev, prevTemp, next); + // Debug.Print(character[0].lyric); + } + + prev = prevTemp; + + + if (i == phrase.Length - 1){ + isLastNote = true; + } + else{ + isLastNote = false; + } + + // Pass isLastNote to handle Last Consonant(Batchim)'s length. + var wordPhonemes = ProcessWord(note, isLastNote); + + phrasePhonemes[^1].Phonemes.AddRange(wordPhonemes[0].Phonemes); + phrasePhonemes.AddRange(wordPhonemes.Skip(1)); + notePhIndex.Add(notePhIndex[^1]+wordPhonemes.SelectMany(n=>n.Phonemes).Count()); + + i += 1; + } + + + + + phrasePhonemes.Add(new phonemesPerNote(endTick,lastNote.tone)); + phrasePhonemes[0].Position = timeAxis.MsPosToTickPos( + timeAxis.TickPosToMsPos(phrasePhonemes[1].Position)-padding + ); + //Linguistic Encoder + var tokens = phrasePhonemes + .SelectMany(n => n.Phonemes) + .Select(p => (Int64)phonemes.IndexOf(p.Symbol)) + .ToArray(); + var word_div = phrasePhonemes.Take(phrasePhonemes.Count-1) + .Select(n => (Int64)n.Phonemes.Count) + .ToArray(); + //Pairwise(phrasePhonemes) + var word_dur = phrasePhonemes + .Zip(phrasePhonemes.Skip(1), (a, b) => (long)framesBetweenTickPos(a.Position, b.Position)) + .ToArray(); + //Call Diffsinger Linguistic Encoder model + var linguisticInputs = new List(); + linguisticInputs.Add(NamedOnnxValue.CreateFromTensor("tokens", + new DenseTensor(tokens, new int[] { tokens.Length }, false) + .Reshape(new int[] { 1, tokens.Length }))); + linguisticInputs.Add(NamedOnnxValue.CreateFromTensor("word_div", + new DenseTensor(word_div, new int[] { word_div.Length }, false) + .Reshape(new int[] { 1, word_div.Length }))); + linguisticInputs.Add(NamedOnnxValue.CreateFromTensor("word_dur", + new DenseTensor(word_dur, new int[] { word_dur.Length }, false) + .Reshape(new int[] { 1, word_dur.Length }))); + var linguisticOutputs = linguisticModel.Run(linguisticInputs); + Tensor encoder_out = linguisticOutputs + .Where(o => o.Name == "encoder_out") + .First() + .AsTensor(); + Tensor x_masks = linguisticOutputs + .Where(o => o.Name == "x_masks") + .First() + .AsTensor(); + //Duration Predictor + var ph_midi = phrasePhonemes + .SelectMany(n=>Enumerable.Repeat((Int64)n.Tone, n.Phonemes.Count)) + .ToArray(); + //Call Diffsinger Duration Predictor model + var durationInputs = new List(); + durationInputs.Add(NamedOnnxValue.CreateFromTensor("encoder_out", encoder_out)); + durationInputs.Add(NamedOnnxValue.CreateFromTensor("x_masks", x_masks)); + durationInputs.Add(NamedOnnxValue.CreateFromTensor("ph_midi", + new DenseTensor(ph_midi, new int[] { ph_midi.Length }, false) + .Reshape(new int[] { 1, ph_midi.Length }))); + //Speaker + if(dsConfig.speakers != null){ + var speakerEmbedManager = getSpeakerEmbedManager(); + var speakersByPhone = phrasePhonemes + .SelectMany(n => n.Phonemes) + .Select(p => p.Speaker) + .ToArray(); + var spkEmbedTensor = speakerEmbedManager.PhraseSpeakerEmbedByPhone(speakersByPhone); + durationInputs.Add(NamedOnnxValue.CreateFromTensor("spk_embed", spkEmbedTensor)); + } + var durationOutputs = durationModel.Run(durationInputs); + List durationFrames = durationOutputs.First().AsTensor().Select(x=>(double)x).ToList(); + + //Alignment + //(the index of the phoneme to be aligned, the Ms position of the phoneme) + var phAlignPoints = new List>(); + phAlignPoints = CumulativeSum(phrasePhonemes.Select(n => n.Phonemes.Count).ToList(), 0) + .Zip(phrasePhonemes.Skip(1), // + (a, b) => new Tuple(a, timeAxis.TickPosToMsPos(b.Position))) + .ToList(); + var positions = new List(); + List alignGroup = durationFrames.GetRange(1, phAlignPoints[0].Item1 - 1); + + var phs = phrasePhonemes.SelectMany(n => n.Phonemes).ToList(); + //The starting consonant's duration keeps unchanged + positions.AddRange(stretch(alignGroup, frameMs, phAlignPoints[0].Item2)); + + + + int j = 0; + double prevRatio = 0; + //Stretch the duration of the rest phonemes + var prevAlignPoint = phAlignPoints[0]; + var zipped = phAlignPoints.Zip(phAlignPoints.Skip(1), (a, b) => Tuple.Create(a, b)); + foreach (var pair in zipped) { + var currAlignPoint = pair.Item1; + var nextAlignPoint = pair.Item2; + alignGroup = durationFrames.GetRange(currAlignPoint.Item1, nextAlignPoint.Item1 - currAlignPoint.Item1); + double ratio = (nextAlignPoint.Item2 - currAlignPoint.Item2) / alignGroup.Sum(); + + positions.AddRange(stretch(alignGroup, ratio, nextAlignPoint.Item2)); + + prevAlignPoint = phAlignPoints[j]; + prevRatio = ratio; + j += 1; + } + + //Convert the position sequence to tick and fill into the result list + int index = 1; + foreach (int groupIndex in Enumerable.Range(0, phrase.Length)) { + Note[] group = phrase[groupIndex]; + var noteResult = new List>(); + if (group[0].lyric.StartsWith("+")) { + continue; + } + double notePos = timeAxis.TickPosToMsPos(group[0].position);//start position of the note, ms + for (int phIndex = notePhIndex[groupIndex]; phIndex < notePhIndex[groupIndex + 1]; ++phIndex) { + if (!String.IsNullOrEmpty(phs[phIndex].Symbol)) { + noteResult.Add(Tuple.Create(phs[phIndex].Symbol, timeAxis.TicksBetweenMsPos( + notePos, positions[phIndex - 1]))); + } + } + partResult[group[0].position] = noteResult; + } + } + + private bool isPlainVowel(string symbol){ + if (isSemiVowel(symbol)){ + return false; + } + else if (isBatchim(symbol)){ + return false; + } + else{ + return g2p.IsVowel(symbol); + } + } + + private bool isSemiVowel(string symbol){ + if (symbol.Equals("w") || symbol.Equals("y")){ + return true; + } + else{ + return false; + } + } + + private bool isBatchim(string symbol){ + if (symbol.Equals("K") || symbol.Equals("N") || symbol.Equals("T") || symbol.Equals("L") || symbol.Equals("M") || symbol.Equals("P")|| symbol.Equals("NG")){ + return true; + } + else{ + return false; + } + } + } +} \ No newline at end of file diff --git a/OpenUtau.Core/KoreanPhonemizerUtil.cs b/OpenUtau.Core/KoreanPhonemizerUtil.cs new file mode 100644 index 000000000..632d195cd --- /dev/null +++ b/OpenUtau.Core/KoreanPhonemizerUtil.cs @@ -0,0 +1,1433 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using OpenUtau.Core.Ustx; +using OpenUtau.Classic; +using Serilog; +using static OpenUtau.Api.Phonemizer; + +namespace OpenUtau.Core { + /// + /// static class that performs Korean Phoneme Variation, Jamo separation, Jamo merging, etc. + /// + public static class KoreanPhonemizerUtil { + /// + /// First hangeul consonants, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 초성들입니다. + ///
+ const string FIRST_CONSONANTS = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"; + /// + /// Middle hangeul vowels, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 중성들입니다. + ///
+ const string MIDDLE_VOWELS = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"; + + /// + /// Last hangeul consonants, ordered in unicode sequence. + ///

유니코드 순서대로 정렬된 한국어 종성들입니다. + ///
+ const string LAST_CONSONANTS = " ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ"; // The first blank(" ") is needed because Hangeul may not have lastConsonant. + + /// + /// unicode index of 가 + /// + const ushort HANGEUL_UNICODE_START = 0xAC00; + + /// + /// unicode index of 힣 + /// + const ushort HANGEUL_UNICODE_END = 0xD79F; + + /// + /// A hashtable of basicsounds - ㄱ/ㄷ/ㅂ/ㅅ/ㅈ. + ///

예사소리 테이블입니다. + ///
+ public static readonly Hashtable basicSounds = new Hashtable() { + ["ㄱ"] = 0, + ["ㄷ"] = 1, + ["ㅂ"] = 2, + ["ㅈ"] = 3, + ["ㅅ"] = 4 + }; + + /// + /// A hashtable of aspirate sounds - ㅋ/ㅌ/ㅍ/ㅊ/(ㅌ). + ///
[4] is "ㅌ", it will be used when conducting phoneme variation - 격음화(거센소리되기). + ///

거센소리 테이블입니다. + ///
[4]의 중복값 "ㅌ"은 오타가 아니며 격음화(거센소리되기) 수행 시에 활용됩니다. + ///
+ public static readonly Hashtable aspirateSounds = new Hashtable() { + [0] = "ㅋ", + [1] = "ㅌ", + [2] = "ㅍ", + [3] = "ㅊ", + [4] = "ㅌ" + }; + + /// + /// A hashtable of fortis sounds - ㄲ/ㄸ/ㅃ/ㅆ/ㅉ. + ///

된소리 테이블입니다. + ///
+ public static readonly Hashtable fortisSounds = new Hashtable() { + [0] = "ㄲ", + [1] = "ㄸ", + [2] = "ㅃ", + [3] = "ㅉ", + [4] = "ㅆ" + }; + + /// + /// A hashtable of nasal sounds - ㄴ/ㅇ/ㅁ. + ///

비음 테이블입니다. + ///
+ public static readonly Hashtable nasalSounds = new Hashtable() { + ["ㄴ"] = 0, + ["ㅇ"] = 1, + ["ㅁ"] = 2 + }; + + + /// + /// Confirms if input string is hangeul. + ///

입력 문자열이 한글인지 확인합니다. + ///
+ /// A string of Hangeul character. + ///
(Example: "가", "!가", "가.") + /// Returns true when input string is Hangeul, otherwise false. + public static bool IsHangeul(string? character) { + + ushort unicodeIndex; + bool isHangeul; + if ((character != null) && character.StartsWith('!')) { + // Automatically deletes ! from start. + // Prevents error when user uses ! as a phonetic symbol. + unicodeIndex = Convert.ToUInt16(character.TrimStart('!')[0]); + isHangeul = !(unicodeIndex < HANGEUL_UNICODE_START || unicodeIndex > HANGEUL_UNICODE_END); + } + else if (character != null) { + try { + unicodeIndex = Convert.ToUInt16(character[0]); + isHangeul = !(unicodeIndex < HANGEUL_UNICODE_START || unicodeIndex > HANGEUL_UNICODE_END); + } + catch { + isHangeul = false; + } + + } + else { + isHangeul = false; + } + + return isHangeul; + } + /// + /// Separates complete hangeul string's first character in three parts - firstConsonant(초성), middleVowel(중성), lastConsonant(종성). + ///
입력된 문자열의 0번째 글자를 초성, 중성, 종성으로 분리합니다. + ///
+ /// A string of complete Hangeul character. + ///
(Example: '냥') + /// + /// {firstConsonant(초성), middleVowel(중성), lastConsonant(종성)} + /// (ex) {"ㄴ", "ㅑ", "ㅇ"} + /// + public static Hashtable Separate(string character) { + + int hangeulIndex; // unicode index of hangeul - unicode index of '가' (ex) '냥' + + int firstConsonantIndex; // (ex) 2 + int middleVowelIndex; // (ex) 2 + int lastConsonantIndex; // (ex) 21 + + string firstConsonant; // (ex) "ㄴ" + string middleVowel; // (ex) "ㅑ" + string lastConsonant; // (ex) "ㅇ" + + Hashtable separatedHangeul; // (ex) {[0]: "ㄴ", [1]: "ㅑ", [2]: "ㅇ"} + + + hangeulIndex = Convert.ToUInt16(character[0]) - HANGEUL_UNICODE_START; + + // seperates lastConsonant + lastConsonantIndex = hangeulIndex % 28; + hangeulIndex = (hangeulIndex - lastConsonantIndex) / 28; + + // seperates middleVowel + middleVowelIndex = hangeulIndex % 21; + hangeulIndex = (hangeulIndex - middleVowelIndex) / 21; + + // there's only firstConsonant now + firstConsonantIndex = hangeulIndex; + + // separates character + firstConsonant = FIRST_CONSONANTS[firstConsonantIndex].ToString(); + middleVowel = MIDDLE_VOWELS[middleVowelIndex].ToString(); + lastConsonant = LAST_CONSONANTS[lastConsonantIndex].ToString(); + + separatedHangeul = new Hashtable() { + [0] = firstConsonant, + [1] = middleVowel, + [2] = lastConsonant + }; + + + return separatedHangeul; + } + + /// + /// merges separated hangeul into complete hangeul. (Example: {[0]: "ㄱ", [1]: "ㅏ", [2]: " "} => "가"}) + /// 자모로 쪼개진 한글을 합쳐진 한글로 반환합니다. + /// + /// separated Hangeul. + /// Returns complete Hangeul Character. + public static string Merge(Hashtable separatedHangeul){ + + int firstConsonantIndex; // (ex) 2 + int middleVowelIndex; // (ex) 2 + int lastConsonantIndex; // (ex) 21 + + char firstConsonant = ((string)separatedHangeul[0])[0]; // (ex) "ㄴ" + char middleVowel = ((string)separatedHangeul[1])[0]; // (ex) "ㅑ" + char lastConsonant = ((string)separatedHangeul[2])[0]; // (ex) "ㅇ" + + if (firstConsonant == ' ') {firstConsonant = 'ㅇ';} + + firstConsonantIndex = FIRST_CONSONANTS.IndexOf(firstConsonant); // 초성 인덱스 + middleVowelIndex = MIDDLE_VOWELS.IndexOf(middleVowel); // 중성 인덱스 + lastConsonantIndex = LAST_CONSONANTS.IndexOf(lastConsonant); // 종성 인덱스 + + int mergedCode = HANGEUL_UNICODE_START + (firstConsonantIndex * 21 + middleVowelIndex) * 28 + lastConsonantIndex; + + string result = Convert.ToChar(mergedCode).ToString(); + Debug.Print("Hangeul merged: " + $"{firstConsonant} + {middleVowel} + {lastConsonant} = " + result); + return result; + } + + /// + /// Conducts phoneme variation with two characters input.
※ This method is for only when there are more than one characters, so when there is single character only, Please use Variate(string character). + ///

두 글자를 입력받아 음운변동을 진행합니다.
※ 두 글자 이상이 아닌 단일 글자에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(string character) 메소드를 사용해야 합니다. + ///
+ /// Separated table of first target. + ///
첫 번째 글자를 분리한 해시테이블 + ///

(Example: {[0]="ㅁ", [1]="ㅜ", [2]="ㄴ"} - 문) + /// + /// Separated table of second target. + ///
두 번째 글자를 분리한 해시테이블 + ///

(Example: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + /// + /// 0: returns result of first target character only. + ///
1: returns result of second target character only.
else: returns result of both target characters.
+ ///
0: 첫 번째 타겟 글자의 음운변동 결과만 반환합니다. + ///
1: 두 번째 타겟 글자의 음운변동 결과만 반환합니다.
나머지 값: 두 타겟 글자의 음운변동 결과를 모두 반환합니다.
+ ///
(Example(0): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
(Example(1): {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
(Example(-1): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + /// + /// Example: when returnCharIndex = 0: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
Example: when returnCharIndex = 1: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
Example: when returnCharIndex = -1: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + ///
+ private static Hashtable Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex = -1) { + + string firstLastConsonant = (string)firstCharSeparated[2]; // 문래 에서 ㄴ, 맑다 에서 ㄺ + string nextFirstConsonant = (string)nextCharSeparated[0]; // 문래 에서 ㄹ, 맑다 에서 ㄷ + + // 1. 연음 적용 + ㅎ탈락 + if ((!firstLastConsonant.Equals(" ")) && nextFirstConsonant.Equals("ㅎ")) { + if (basicSounds.Contains(firstLastConsonant)) { + // 착하다 = 차카다 + nextFirstConsonant = (string)aspirateSounds[basicSounds[firstLastConsonant]]; + firstLastConsonant = " "; + } else { + // 뻔한 = 뻔안 (아래에서 연음 적용되서 뻐난 됨) + nextFirstConsonant = "ㅇ"; + } + } + + if (nextFirstConsonant.Equals("ㅇ") && (! firstLastConsonant.Equals(" "))) { + // ㄳ ㄵ ㄶ ㄺ ㄻ ㄼ ㄽ ㄾ ㄿ ㅀ ㅄ 일 경우에도 분기해서 연음 적용 + if (firstLastConsonant.Equals("ㄳ")) { + firstLastConsonant = "ㄱ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄵ")) { + firstLastConsonant = "ㄴ"; + nextFirstConsonant = "ㅈ"; + } + else if (firstLastConsonant.Equals("ㄶ")) { + firstLastConsonant = "ㄴ"; + nextFirstConsonant = "ㅎ"; + } + else if (firstLastConsonant.Equals("ㄺ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㄱ"; + } + else if (firstLastConsonant.Equals("ㄼ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅂ"; + } + else if (firstLastConsonant.Equals("ㄽ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄾ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅌ"; + } + else if (firstLastConsonant.Equals("ㄿ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅍ"; + } + else if (firstLastConsonant.Equals("ㅀ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅎ"; + } + else if (firstLastConsonant.Equals("ㅄ")) { + firstLastConsonant = "ㅂ"; + nextFirstConsonant = "ㅅ"; + } + else if (firstLastConsonant.Equals("ㄻ")) { + firstLastConsonant = "ㄹ"; + nextFirstConsonant = "ㅁ"; + } + else if (firstLastConsonant.Equals("ㅇ") && nextFirstConsonant.Equals("ㅇ")) { + // Do nothing + } + else { + // 겹받침 아닐 때 연음 + nextFirstConsonant = firstLastConsonant; + firstLastConsonant = " "; + } + } + + + // 1. 유기음화 및 ㅎ탈락 1 + if (firstLastConsonant.Equals("ㅎ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㅎ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 낳다 = 나타 + firstLastConsonant = " "; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + else if (firstLastConsonant.Equals("ㅎ") && (!nextFirstConsonant.Equals("ㅅ")) && nextFirstConsonant.Equals("ㅇ")) { + // ㅎ으로 끝나고 다음 소리가 없으면 / ex) 낳아 = 나아 + firstLastConsonant = " "; + } + else if (firstLastConsonant.Equals("ㄶ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㄶ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 많다 = 만타 + firstLastConsonant = "ㄴ"; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + else if (firstLastConsonant.Equals("ㅀ") && (! nextFirstConsonant.Equals("ㅅ")) && basicSounds.Contains(nextFirstConsonant)) { + // ㅀ으로 끝나고 다음 소리가 ㄱㄷㅂㅈ이면 / ex) 끓다 = 끌타 + firstLastConsonant = "ㄹ"; + nextFirstConsonant = (string)aspirateSounds[basicSounds[nextFirstConsonant]]; + } + + + + + // 2-1. 된소리되기 1 + if ((firstLastConsonant.Equals("ㄳ") || firstLastConsonant.Equals("ㄵ") || firstLastConsonant.Equals("ㄽ") || firstLastConsonant.Equals("ㄾ") || firstLastConsonant.Equals("ㅄ") || firstLastConsonant.Equals("ㄼ") || firstLastConsonant.Equals("ㄺ") || firstLastConsonant.Equals("ㄿ")) && basicSounds.Contains(nextFirstConsonant)) { + // [ㄻ, (ㄶ, ㅀ)<= 유기음화에 따라 예외] 제외한 겹받침으로 끝나고 다음 소리가 예사소리이면 + nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + } + + // 3. 첫 번째 글자의 자음군단순화 및 평파열음화(음절의 끝소리 규칙) + if (firstLastConsonant.Equals("ㄽ") || firstLastConsonant.Equals("ㄾ") || firstLastConsonant.Equals("ㄼ")) { + firstLastConsonant = "ㄹ"; + } else if (firstLastConsonant.Equals("ㄵ") || firstLastConsonant.Equals("ㅅ") || firstLastConsonant.Equals("ㅆ") || firstLastConsonant.Equals("ㅈ") || firstLastConsonant.Equals("ㅉ") || firstLastConsonant.Equals("ㅊ") || firstLastConsonant.Equals("ㅌ")) { + firstLastConsonant = "ㄷ"; + } else if (firstLastConsonant.Equals("ㅃ") || firstLastConsonant.Equals("ㅍ") || firstLastConsonant.Equals("ㄿ") || firstLastConsonant.Equals("ㅄ")) { + firstLastConsonant = "ㅂ"; + } else if (firstLastConsonant.Equals("ㄲ") || firstLastConsonant.Equals("ㅋ") || firstLastConsonant.Equals("ㄺ") || firstLastConsonant.Equals("ㄳ")) { + firstLastConsonant = "ㄱ"; + } else if (firstLastConsonant.Equals("ㄻ")) { + firstLastConsonant = "ㅁ"; + } + + + + // 2-1. 된소리되기 2 + if (basicSounds.Contains(firstLastConsonant) && basicSounds.Contains(nextFirstConsonant)) { + // 예사소리로 끝나고 다음 소리가 예사소리이면 / ex) 닭장 = 닥짱 + nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + } + // else if ((firstLastConsonant.Equals("ㄹ")) && (basicSounds.Contains(nextFirstConsonant))){ + // // ㄹ로 끝나고 다음 소리가 예사소리이면 / ex) 솔직 = 솔찍 + // // 본래 관형형 어미 (으)ㄹ과 일부 한자어에서만 일어나는 변동이나, 워낙 사용되는 빈도가 많아서 기본으로 적용되게 해 두 + // // 려 했으나 좀 아닌 것 같아서 보류하기로 함 + // nextFirstConsonant = (string)fortisSounds[basicSounds[nextFirstConsonant]]; + // } + + // 1. 유기음화 2 + if (basicSounds.Contains(firstLastConsonant) && nextFirstConsonant.Equals("ㅎ")) { + // ㄱㄷㅂㅈ(+ㅅ)로 끝나고 다음 소리가 ㅎ이면 / ex) 축하 = 추카, 옷하고 = 오타고 + // ㅅ은 미리 평파열음화가 진행된 것으로 보고 ㄷ으로 간주한다 + nextFirstConsonant = (string)aspirateSounds[basicSounds[firstLastConsonant]]; + firstLastConsonant = " "; + } + else if (nextFirstConsonant.Equals("ㅎ")) { + nextFirstConsonant = "ㅇ"; + } + + if ((!firstLastConsonant.Equals("")) && nextFirstConsonant.Equals("ㅇ") && (!firstLastConsonant.Equals("ㅇ"))) { + // 연음 2 + nextFirstConsonant = firstLastConsonant; + firstLastConsonant = " "; + } + + + // 4. 비음화 + if (firstLastConsonant.Equals("ㄱ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 막론 = 망론 >> 망논 + firstLastConsonant = "ㅇ"; + } else if (firstLastConsonant.Equals("ㄷ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 슬롯머신 = 슬론머신 + firstLastConsonant = "ㄴ"; + } else if (firstLastConsonant.Equals("ㅂ") && (!nextFirstConsonant.Equals("ㅇ")) && (nasalSounds.Contains(nextFirstConsonant) || nextFirstConsonant.Equals("ㄹ"))) { + // ex) 밥먹자 = 밤먹자 >> 밤먹짜 + firstLastConsonant = "ㅁ"; + } + + // 4'. 유음화 + if (firstLastConsonant.Equals("ㄴ") && nextFirstConsonant.Equals("ㄹ")) { + // ex) 만리 = 말리 + firstLastConsonant = "ㄹ"; + } else if (firstLastConsonant.Equals("ㄹ") && nextFirstConsonant.Equals("ㄴ")) { + // ex) 칼날 = 칼랄 + nextFirstConsonant = "ㄹ"; + } + + // 4''. ㄹ비음화 + if (nextFirstConsonant.Equals("ㄹ") && nasalSounds.Contains(nextFirstConsonant)) { + // ex) 담력 = 담녁 + firstLastConsonant = "ㄴ"; + } + + + // 4'''. 자음동화 + if (firstLastConsonant.Equals("ㄴ") && nextFirstConsonant.Equals("ㄱ")) { + // ex) ~라는 감정 = ~라능 감정 + firstLastConsonant = "ㅇ"; + } + + // return results + if (returnCharIndex == 0) { + // return result of first target character + return new Hashtable() { + [0] = firstCharSeparated[0], + [1] = firstCharSeparated[1], + [2] = firstLastConsonant + }; + } else if (returnCharIndex == 1) { + // return result of second target character + return new Hashtable() { + [0] = nextFirstConsonant, + [1] = nextCharSeparated[1], + [2] = nextCharSeparated[2] + }; + } else { + // 두 글자 다 반환 + return new Hashtable() { + [0] = firstCharSeparated[0], + [1] = firstCharSeparated[1], + [2] = firstLastConsonant, + [3] = nextFirstConsonant, + [4] = nextCharSeparated[1], + [5] = nextCharSeparated[2] + }; + } + } + + /// + /// Conducts phoneme variation with one character input.
※ This method is only for when there are single character, so when there are more than one character, Please use Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1). + ///

단일 글자를 입력받아 음운변동을 진행합니다.
※ 단일 글자가 아닌 두 글자 이상에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1) 메소드를 사용해야 합니다. + ///
+ /// String of single target. + ///
음운변동시킬 단일 글자. + /// + /// (Example(삵): {[0]="ㅅ", [1]="ㅏ", [2]="ㄱ"} - 삭) + /// + public static Hashtable Variate(string character) { + /// 맨 끝 노트에서 음운변동 적용하는 함수 + /// 자음군 단순화와 평파열음화 + Hashtable separated = Separate(character); + + if (separated[2].Equals("ㄽ") || separated[2].Equals("ㄾ") || separated[2].Equals("ㄼ") || separated[2].Equals("ㅀ")) { + separated[2] = "ㄹ"; + } + else if (separated[2].Equals("ㄵ") || separated[2].Equals("ㅅ") || separated[2].Equals("ㅆ") || separated[2].Equals("ㅈ") || separated[2].Equals("ㅉ") || separated[2].Equals("ㅊ")) { + separated[2] = "ㄷ"; + } + else if (separated[2].Equals("ㅃ") || separated[2].Equals("ㅍ") || separated[2].Equals("ㄿ") || separated[2].Equals("ㅄ")) { + separated[2] = "ㅂ"; + } + else if (separated[2].Equals("ㄲ") || separated[2].Equals("ㅋ") || separated[2].Equals("ㄺ") || separated[2].Equals("ㄳ")) { + separated[2] = "ㄱ"; + } + else if (separated[2].Equals("ㄻ")) { + separated[2] = "ㅁ"; + } + else if (separated[2].Equals("ㄶ")) { + separated[2] = "ㄴ"; + } + + + return separated; + + } + /// + /// Conducts phoneme variation with one character input.
※ This method is only for when there are single character, so when there are more than one character, Please use Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1). + ///

단일 글자의 분리된 값을 입력받아 음운변동을 진행합니다.
※ 단일 글자가 아닌 두 글자 이상에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(Hashtable firstCharSeparated, Hashtable nextCharSeparated, int returnCharIndex=-1) 메소드를 사용해야 합니다. + ///
+ /// Separated table of target. + ///
글자를 분리한 해시테이블 + /// + /// (Example({[0]="ㅅ", [1]="ㅏ", [2]="ㄺ"}): {[0]="ㅅ", [1]="ㅏ", [2]="ㄱ"} - 삭) + /// + private static Hashtable Variate(Hashtable separated) { + /// 맨 끝 노트에서 음운변동 적용하는 함수 + + if (separated[2].Equals("ㄽ") || separated[2].Equals("ㄾ") || separated[2].Equals("ㄼ") || separated[2].Equals("ㅀ")) { + separated[2] = "ㄹ"; + } + else if (separated[2].Equals("ㄵ") || separated[2].Equals("ㅅ") || separated[2].Equals("ㅆ") || separated[2].Equals("ㅈ") || separated[2].Equals("ㅉ") || separated[2].Equals("ㅊ")) { + separated[2] = "ㄷ"; + } + else if (separated[2].Equals("ㅃ") || separated[2].Equals("ㅍ") || separated[2].Equals("ㄿ") || separated[2].Equals("ㅄ")) { + separated[2] = "ㅂ"; + } + else if (separated[2].Equals("ㄲ") || separated[2].Equals("ㅋ") || separated[2].Equals("ㄺ") || separated[2].Equals("ㄳ")) { + separated[2] = "ㄱ"; + } + else if (separated[2].Equals("ㄻ")) { + separated[2] = "ㅁ"; + } + else if (separated[2].Equals("ㄶ")) { + separated[2] = "ㄴ"; + } + + return separated; + } + + /// + /// Conducts phoneme variation with two characters input.
※ This method is for only when there are more than one characters, so when there is single character only, Please use Variate(string character). + ///

두 글자를 입력받아 음운변동을 진행합니다.
※ 두 글자 이상이 아닌 단일 글자에서 음운변동을 적용할 경우, 이 메소드가 아닌 Variate(string character) 메소드를 사용해야 합니다. + ///
+ /// String of first target. + ///
첫 번째 글자. + ///

(Example: 문) + /// + /// String of second target. + ///
두 번째 글자. + ///

(Example: 래) + /// + /// 0: returns result of first target character only. + ///
1: returns result of second target character only.
else: returns result of both target characters.
+ ///
0: 첫 번째 타겟 글자의 음운변동 결과만 반환합니다. + ///
1: 두 번째 타겟 글자의 음운변동 결과만 반환합니다.
나머지 값: 두 타겟 글자의 음운변동 결과를 모두 반환합니다.
+ ///
(Example(0): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
(Example(1): {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
(Example(-1): {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + /// + /// Example: when returnCharIndex = 0: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ"} - 물) + ///
Example: when returnCharIndex = 1: {[0]="ㄹ", [1]="ㅐ", [2]=" "} - 래) + ///
Example: when returnCharIndex = -1: {[0]="ㅁ", [1]="ㅜ", [2]="ㄹ", [3]="ㄹ", [4]="ㅐ", [5]=" "} - 물래) + ///
+ private static Hashtable Variate(string firstChar, string nextChar, int returnCharIndex = 0) { + // 글자 넣어도 쓸 수 있음 + + Hashtable firstCharSeparated = Separate(firstChar); + Hashtable nextCharSeparated = Separate(nextChar); + return Variate(firstCharSeparated, nextCharSeparated, returnCharIndex); + } + + /// + /// Conducts phoneme variation automatically with prevNeighbour, note, nextNeighbour. + ///

prevNeighbour, note, nextNeighbour를 입력받아 자동으로 음운 변동을 진행합니다. + ///
+ /// Note of prev note, if exists(otherwise null). + ///
이전 노트 혹은 null. + ///

(Example: Note with lyric '춘') + /// + /// Note of current note. + ///
현재 노트. + ///

(Example: Note with lyric '향') + /// + /// Note of next note, if exists(otherwise null). + ///
다음 노트 혹은 null. + ///

(Example: null) + /// + /// Returns phoneme variation result of prevNote, currentNote, nextNote. + ///
이전 노트, 현재 노트, 다음 노트의 음운변동 결과를 반환합니다. + ///
Example: 춘 [향] null: {[0]="ㅊ", [1]="ㅜ", [2]=" ", [3]="ㄴ", [4]="ㅑ", [5]="ㅇ", [6]="null", [7]="null", [8]="null"} [추 냥 null] + ///
+ public static Hashtable Variate(Note? prevNeighbour, Note note, Note? nextNeighbour) { + // prevNeighbour와 note와 nextNeighbour의 음원변동된 가사를 반환 + // prevNeighbour : VV 정렬에 사용 + // nextNeighbour : VC 정렬에 사용 + // 뒤의 노트가 없으면 리턴되는 값의 6~8번 인덱스가 null로 채워진다. + + /// whereYeonEum : 발음기호 .을 사용하기 위한 변수 + /// .을 사용하면 앞에서 단어가 끝났다고 간주하고, 끝소리에 음운변동을 적용한 후 연음합니다. + /// ex) 무 릎 위 [무르퓌] 무 릎. 위[무르뷔] + /// + /// -1 : 해당사항 없음 + /// 0 : 이전 노트를 연음하지 않음 + /// 1 : 현재 노트를 연음하지 않음 + int whereYeonEum = -1; + + string?[] lyrics = new string?[] { prevNeighbour?.lyric, note.lyric, nextNeighbour?.lyric }; + + if (!IsHangeul(lyrics[0])) { + // 앞노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[0] != null) {lyrics[0] = null;} + } else if (!IsHangeul(lyrics[2])) { + // 뒤노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[2] != null) {lyrics[2] = null;} + } + if ((lyrics[0] != null) && lyrics[0].StartsWith('!')) { + /// 앞노트 ! 기호로 시작함 ex) [!냥]냥냥 + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null냥냥 + } + if ((lyrics[1] != null) && lyrics[1].StartsWith('!')) { + /// 중간노트 ! 기호로 시작함 ex) 냥[!냥]냥 + /// 음운변동 미적용 + lyrics[1] = lyrics[1].TrimStart('!'); + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null[!냥]냥 + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사도 없는 걸로 간주함 null[!냥]null + } + if ((lyrics[2] != null) && lyrics[2].StartsWith('!')) { + /// 뒤노트 ! 기호로 시작함 ex) 냥냥[!냥] + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사 없는 걸로 간주함 냥냥b + } + + if ((lyrics[0] != null) && lyrics[0].EndsWith('.')) { + /// 앞노트 . 기호로 끝남 ex) [냥.]냥냥 + lyrics[0] = lyrics[0].TrimEnd('.'); + whereYeonEum = 0; + } + if ((lyrics[1] != null) && lyrics[1].EndsWith('.')) { + /// 중간노트 . 기호로 끝남 ex) 냥[냥.]냥 + /// 음운변동 없이 연음만 적용 + lyrics[1] = lyrics[1].TrimEnd('.'); + whereYeonEum = 1; + } + if ((lyrics[2] != null) && lyrics[2].EndsWith('.')) { + /// 뒤노트 . 기호로 끝남 ex) 냥냥[냥.] + /// 중간노트의 발음에 관여하지 않으므로 간단히 . 만 지워주면 된다 + lyrics[2] = lyrics[2].TrimEnd('.'); + } + + // 음운변동 적용 -- + if ((lyrics[0] == null) && (lyrics[2] != null)) { + /// 앞이 없고 뒤가 있음 + /// null[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + Hashtable thisNoteSeparated = Variate(Variate(lyrics[1]), Separate(lyrics[2]), -1); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else { + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + + if (IsHangeul(lyrics[2])) { + Hashtable thisNoteSeparated = Variate(lyrics[1], lyrics[2], -1); // 현글자 뒤글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + } + else { + Hashtable thisNoteSeparated = Variate(lyrics[1]); + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); + result.Add(7, "null"); + result.Add(8, "null"); + } + + + return result; + } + } + else if ((lyrics[0] != null) && (lyrics[2] == null)) { + /// 앞이 있고 뒤는 없음 + /// 냥[냥]null + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1)); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1)); // 첫 글자와 현 글자 / 앞글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1)); // 첫 글자와 현 글자 / 뒷글자 없으니까 글자 혼자 있는걸로 음운변동 한 번 더 시키기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + } + else if ((lyrics[0] != null) && (lyrics[2] != null)) { + /// 앞도 있고 뒤도 있음 + /// 냥[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 / 무 [릎.] 위 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1), Separate(lyrics[2]), -1);// 현글자와 다음 글자 / 현 글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 / 릎. [위] 놓 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1), Separate(lyrics[2]), -1); // 현 글자와 뒤 글자 / 앞글자 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1), Separate(lyrics[2]), -1); + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return result; + } + } + else { + /// 앞이 없고 뒤도 없음 + /// null[냥]null + + Hashtable result = new Hashtable() { + // 첫 글자 >> 비어 있음 + [0] = "null", + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1]); // 현 글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + + result.Add(6, "null"); // 뒤 글자 비어있음 + result.Add(7, "null"); + result.Add(8, "null"); + + return result; + } + } + + /// + /// (for diffsinger phonemizer) + /// Conducts phoneme variation automatically with prevNeighbour, note, nextNeighbour. + ///

prevNeighbour, note, nextNeighbour를 입력받아 자동으로 음운 변동을 진행합니다. + ///
+ /// lyric String of prev note, if exists(otherwise null). + ///
이전 가사 혹은 null. + ///

(Example: lyric String with lyric '춘') + /// + /// lyric String of current note. + ///
현재 가사. + ///

(Example: Note with lyric '향') + /// + /// lyric String of next note, if exists(otherwise null). + ///
다음 가사 혹은 null. + ///

(Example: null) + /// + /// Returns phoneme variation result of prevNote, currentNote, nextNote. + ///
이전 노트, 현재 노트, 다음 노트의 음운변동 결과를 반환합니다. + ///
Example: 춘 [향] null: {[0]="ㅊ", [1]="ㅜ", [2]=" ", [3]="ㄴ", [4]="ㅑ", [5]="ㅇ", [6]="null", [7]="null", [8]="null"} [추 냥 null] + ///
+ public static String Variate(String? prevNeighbour, String note, String? nextNeighbour) { + // prevNeighbour와 note와 nextNeighbour의 음원변동된 가사를 반환 + // prevNeighbour : VV 정렬에 사용 + // nextNeighbour : VC 정렬에 사용 + // 뒤의 노트가 없으면 리턴되는 값의 6~8번 인덱스가 null로 채워진다. + + /// whereYeonEum : 발음기호 .을 사용하기 위한 변수 + /// .을 사용하면 앞에서 단어가 끝났다고 간주하고, 끝소리에 음운변동을 적용한 후 연음합니다. + /// ex) 무 릎 위 [무르퓌] 무 릎. 위[무르뷔] + /// + /// -1 : 해당사항 없음 + /// 0 : 이전 노트를 연음하지 않음 + /// 1 : 현재 노트를 연음하지 않음 + int whereYeonEum = -1; + + string?[] lyrics = new string?[] { prevNeighbour, note, nextNeighbour}; + + if (!IsHangeul(lyrics[0])) { + // 앞노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[0] != null) {lyrics[0] = null;} + } else if (!IsHangeul(lyrics[2])) { + // 뒤노트 한국어 아니거나 null일 경우 null처리 + if (lyrics[2] != null) {lyrics[2] = null;} + } + if ((lyrics[0] != null) && lyrics[0].StartsWith('!')) { + /// 앞노트 ! 기호로 시작함 ex) [!냥]냥냥 + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null냥냥 + } + if ((lyrics[1] != null) && lyrics[1].StartsWith('!')) { + /// 중간노트 ! 기호로 시작함 ex) 냥[!냥]냥 + /// 음운변동 미적용 + lyrics[1] = lyrics[1].TrimStart('!'); + if (lyrics[0] != null) {lyrics[0] = null;} // 0번가사 없는 걸로 간주함 null[!냥]냥 + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사도 없는 걸로 간주함 null[!냥]null + } + if ((lyrics[2] != null) && lyrics[2].StartsWith('!')) { + /// 뒤노트 ! 기호로 시작함 ex) 냥냥[!냥] + if (lyrics[2] != null) {lyrics[2] = null;} // 2번가사 없는 걸로 간주함 냥냥b + } + + if ((lyrics[0] != null) && lyrics[0].EndsWith('.')) { + /// 앞노트 . 기호로 끝남 ex) [냥.]냥냥 + lyrics[0] = lyrics[0].TrimEnd('.'); + whereYeonEum = 0; + } + if ((lyrics[1] != null) && lyrics[1].EndsWith('.')) { + /// 중간노트 . 기호로 끝남 ex) 냥[냥.]냥 + /// 음운변동 없이 연음만 적용 + lyrics[1] = lyrics[1].TrimEnd('.'); + whereYeonEum = 1; + } + if ((lyrics[2] != null) && lyrics[2].EndsWith('.')) { + /// 뒤노트 . 기호로 끝남 ex) 냥냥[냥.] + /// 중간노트의 발음에 관여하지 않으므로 간단히 . 만 지워주면 된다 + lyrics[2] = lyrics[2].TrimEnd('.'); + } + + // 음운변동 적용 -- + if ((lyrics[0] == null) && (lyrics[2] != null)) { + /// 앞이 없고 뒤가 있음 + /// null[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + Hashtable thisNoteSeparated = Variate(Variate(lyrics[1]), Separate(lyrics[2]), -1); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = new Hashtable() { + [0] = "null", // 앞 글자 없음 + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1], lyrics[2], -1); // 현글자 뒤글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 없음 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else if ((lyrics[0] != null) && (lyrics[2] == null)) { + /// 앞이 있고 뒤는 없음 + /// 냥[냥]null + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1)); // 현 글자 / 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1)); // 첫 글자와 현 글자 / 앞글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1)); // 첫 글자와 현 글자 / 뒷글자 없으니까 글자 혼자 있는걸로 음운변동 한 번 더 시키기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, "null"); // 뒤 글자 없음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else if ((lyrics[0] != null) && (lyrics[2] != null)) { + /// 앞도 있고 뒤도 있음 + /// 냥[냥]냥 + if (whereYeonEum == 1) { + // 현재 노트에서 단어가 끝났다고 가정 / 무 [릎.] 위 + Hashtable result = Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Separate(lyrics[0]), Variate(lyrics[1]), 1), Separate(lyrics[2]), -1);// 현글자와 다음 글자 / 현 글자를 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else if (whereYeonEum == 0) { + // 앞 노트에서 단어가 끝났다고 가정 / 릎. [위] 놓 + Hashtable result = Variate(Variate(lyrics[0]), Separate(lyrics[1]), 0); // 첫 글자 + Hashtable thisNoteSeparated = Variate(Variate(Variate(lyrics[0]), Separate(lyrics[1]), 1), Separate(lyrics[2]), -1); // 현 글자와 뒤 글자 / 앞글자 끝글자처럼 음운변동시켜서 음원변동 한 번 더 하기 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + else { + Hashtable result = Variate(lyrics[0], lyrics[1], 0); + Hashtable thisNoteSeparated = Variate(Variate(lyrics[0], lyrics[1], 1), Separate(lyrics[2]), -1); + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + result.Add(6, thisNoteSeparated[3]); // 뒤 글자 + result.Add(7, thisNoteSeparated[4]); + result.Add(8, thisNoteSeparated[5]); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5]}); + } + } + else { + /// 앞이 없고 뒤도 없음 + /// null[냥]null + Hashtable result = new Hashtable() { + // 첫 글자 >> 비어 있음 + [0] = "null", + [1] = "null", + [2] = "null" + }; + + Hashtable thisNoteSeparated = Variate(lyrics[1]); // 현 글자 + + result.Add(3, thisNoteSeparated[0]); // 현 글자 + result.Add(4, thisNoteSeparated[1]); + result.Add(5, thisNoteSeparated[2]); + + + result.Add(6, "null"); // 뒤 글자 비어있음 + result.Add(7, "null"); + result.Add(8, "null"); + + return Merge(new Hashtable{ + [0] = (string)result[3], + [1] = (string)result[4], + [2] = (string)result[5] + }); + } + } + + public static Note[] ChangeLyric(Note[] group, string lyric) { + // for ENUNU Phonemizer + var oldNote = group[0]; + group[0] = new Note { + lyric = lyric, + phoneticHint = oldNote.phoneticHint, + tone = oldNote.tone, + position = oldNote.position, + duration = oldNote.duration, + phonemeAttributes = oldNote.phonemeAttributes, + }; + return group; + } + public static void RomanizeNotes(Note[][] groups, Dictionary firstConsonants, Dictionary vowels, Dictionary lastConsonants, string semivowelSeparator=" ") { + // for ENUNU Phonemizer + + int noteIdx = 0; + Note[] currentNote; + Note[]? prevNote = null; + Note[]? nextNote; + + Note? prevNote_; + Note? nextNote_; + + + List ResultLyrics = new List(); + foreach (Note[] group in groups){ + currentNote = groups[noteIdx]; + if (groups.Length > noteIdx + 1 && IsHangeul(groups[noteIdx + 1][0].lyric)) { + nextNote = groups[noteIdx + 1]; + } + else { + nextNote = null; + } + + if (prevNote != null) { + prevNote_ = prevNote[0]; + if (prevNote[0].position + prevNote.Sum(note => note.duration) != currentNote[0].position) { + prevNote_ = null; + } + } + else {prevNote_ = null;} + + if (nextNote != null) { + nextNote_ = nextNote[0]; + + if (nextNote[0].position != currentNote[0].position + currentNote.Sum(note => note.duration)) { + nextNote_ = null; + } + } + else{nextNote_ = null;} + + string lyric = ""; + + if (! IsHangeul(currentNote[0].lyric)){ + ResultLyrics.Add(currentNote[0].lyric); + prevNote = currentNote; + noteIdx++; + continue; + } + + Hashtable lyricSeparated = Variate(prevNote_, currentNote[0], nextNote_); + lyric += firstConsonants[(string)lyricSeparated[3]][0]; + if (vowels[(string)lyricSeparated[4]][1] != "") { + // this vowel contains semivowel + lyric += semivowelSeparator + vowels[(string)lyricSeparated[4]][1] + vowels[(string)lyricSeparated[4]][2]; + } + else{ + lyric += " " + vowels[(string)lyricSeparated[4]][2]; + } + + lyric += lastConsonants[(string)lyricSeparated[5]][0]; + + ResultLyrics.Add(lyric.Trim()); + + prevNote = currentNote; + + noteIdx++; + } + Enumerable.Zip(groups, ResultLyrics.ToArray(), ChangeLyric).Last(); + } + + /// + /// abstract class for Ini Management + /// To use, child phonemizer should implement this class(BaseIniManager) with its own setting values! + /// + public abstract class BaseIniManager { + protected USinger singer; + protected Hashtable iniSetting = new Hashtable(); + protected string iniFileName; + protected string filePath; + protected List blocks; + + public BaseIniManager() { } + + /// + /// if no [iniFileName] in Singer Directory, it makes new [iniFileName] with [iniFile]]. + /// + /// + /// + /// + public void Initialize(USinger singer, string iniFileName, Hashtable defaultIniSetting) { + this.singer = singer; + this.iniFileName = iniFileName; + iniSetting = defaultIniSetting; + filePath = Path.Combine(singer.Location, iniFileName); + try { + using (StreamReader reader = new StreamReader(filePath, singer.TextFileEncoding)){ + List blocks = Ini.ReadBlocks(reader, filePath, @"\[\w+\]"); + if (blocks.Count == 0) { + throw new IOException($"[{iniFileName}] is empty."); + } + this.blocks = blocks; + IniSetUp(iniSetting); // you can override IniSetUp() to use. + }; + } + catch (IOException e) { + Log.Error(e, $"failed to read {iniFileName}, Making new {iniFileName}..."); + using (StreamWriter writer = new StreamWriter(filePath)){ + iniSetting = defaultIniSetting; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e_){ + Log.Error(e_, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + }; + using (StreamReader reader = new StreamReader(filePath)){ + List blocks = Ini.ReadBlocks(reader, filePath, @"\[\w+\]"); + this.blocks = blocks; + }; + } + } + + /// + /// you can override this method with your own values. + /// !! when implement this method, you have to use [SetOrReadThisValue(string sectionName, string keyName, bool/string/int/double value)] when setting or reading values. + /// (ex) + /// SetOrReadThisValue("sectionName", "keyName", true); + /// + protected virtual void IniSetUp(Hashtable iniSetting) { + } + + /// + /// for file writing, converts iniSetting to string. + /// + /// + protected string ConvertSettingsToString(){ + string result = ""; + foreach (DictionaryEntry section in iniSetting) { + result += $"[{section.Key}]\n"; + foreach (DictionaryEntry key in (Hashtable)iniSetting[section.Key]){ + result += $"{key.Key}={key.Value}\n"; + } + } + return result; + } + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid bool vaule, nothing happens. But if there's no valid bool value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, bool 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// /// + protected void SetOrReadThisValue(string sectionName, string keyName, bool defaultValue, out bool resultValue) { + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + + resultValue = result.ToLower() == "true" ? true : false; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString()); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue.ToString(); + } + resultValue = defaultValue; + } + } + else{ + using (StreamWriter writer = new StreamWriter(filePath)) { + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString().ToLower()); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + }; + } + } + + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid string vaule, nothing happens. But if there's no valid string value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, string 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected string SetOrReadThisValue(string sectionName, string keyName, string defaultValue) { + string resultValue; + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + resultValue = result; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue; + } + resultValue = defaultValue; + } + } + else{ + StreamWriter writer = new StreamWriter(filePath); + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + } + return resultValue; + } + + /// + /// + /// section's name in .ini config file. + /// key's name in .ini config file's [sectionName] section. + /// default value to overwrite if there's no valid value in config file. + /// inputs section name & key name & default value. If there's valid int vaule, nothing happens. But if there's no valid int value, overwrites current value with default value. + /// 섹션과 키 이름을 입력받고, int 값이 존재하면 넘어가고 존재하지 않으면 defaultValue 값으로 덮어씌운다 + /// + protected void SetOrReadThisValue(string sectionName, string keyName, int defaultValue, out int resultValue) { + List iniLines = blocks.Find(block => block.header == $"[{sectionName}]").lines; + if (! iniSetting.ContainsKey(sectionName)){ + iniSetting.Add(sectionName, new Hashtable()); + } + if (iniLines != null) { + string result = iniLines.Find(l => l.line.Trim().Split("=")[0] == keyName).line.Trim().Split("=")[1]; + if (result != null && int.TryParse(result, out var resultInt)) { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, result); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = result; + } + resultValue = resultInt; + } + else { + try{ + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue.ToString()); + } + catch (ArgumentException){ + ((Hashtable)iniSetting[sectionName])[keyName] = defaultValue.ToString(); + } + resultValue = defaultValue; + } + } + else{ + StreamWriter writer = new StreamWriter(filePath); + ((Hashtable)iniSetting[sectionName]).Add(keyName, defaultValue); + resultValue = defaultValue; + try{ + writer.Write(ConvertSettingsToString()); + writer.Close(); + } + catch (IOException e){ + Log.Error(e, $"[{iniFileName}] Failed to Write new {iniFileName}."); + } + Log.Information($"[{iniFileName}] failed to parse setting '{keyName}', modified {defaultValue} as default value."); + } + } + } + /// + /// Data class used to deserialize yaml dictionary. + /// (for user-defined Korean jamo dictionary) + /// + public class JamoDictionary{ + public FirstConsonantData[] firstConsonants; + public PlainVowelData[] plainVowels; + public SemivowelData[] semivowels; + public FinalConsonantData[] finalConsonants; + public JamoDictionary() { } + public JamoDictionary(FirstConsonantData[] firstConsonants, PlainVowelData[] plainVowels, SemivowelData[] semivowels, FinalConsonantData[] finalConsonants){ + this.firstConsonants = firstConsonants; + this.plainVowels = plainVowels; + this.semivowels = semivowels; + this.finalConsonants = finalConsonants; + } + public struct FirstConsonantData { + public string grapheme; // ㄱ + public string phoneme; // g + public FirstConsonantData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + + public struct PlainVowelData { + public string grapheme; // ㅏ + public string phoneme; // a + + public PlainVowelData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + public struct SemivowelData { + public string grapheme; // w + public string phoneme; // w + + public SemivowelData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + + public struct FinalConsonantData { + public string grapheme; // ㄱ + public string phoneme; // K + public FinalConsonantData(string grapheme, string phoneme) { + this.grapheme = grapheme; + this.phoneme = phoneme; + } + } + } + } + +} \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs new file mode 100644 index 000000000..b1a92eabe --- /dev/null +++ b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenUtau.Api; +using OpenUtau.Core; +using OpenUtau.Core.Ustx; + + +namespace OpenUtau.Plugin.Builtin { + /// + /// Base Phonemizer for Korean Phonemizers. + /// 1. Can process Phoneme variation(음운 변동), through Hangeul.Variate(). + /// 2. Can find Alias in oto, including Voice color etc, through FindInOto(). + /// 3. Can manage .ini configuring, through implementing IniParser at child class. (Usage is in KoreanCVPhonemizer.cs) + /// 4. Can generate phonemes according to Phoneme hints. + /// + public abstract class BaseKoreanPhonemizer : Phonemizer { + + protected USinger singer; + + protected int vcLengthShort = 90; + + protected static readonly string[] PLAIN_VOWELS = new string[]{"ㅏ", "ㅣ", "ㅜ", "ㅔ", "ㅗ", "ㅡ", "ㅓ", "ㅢ"}; + protected static readonly string[] SOFT_BATCHIMS = new string[]{"ㄴ", "ㄹ", "ㅇ"}; + protected static readonly string[] HARD_BATCHIMS = new string[]{"ㄱ", "ㄷ", "ㅂ", "ㅁ"}; + public override void SetSinger(USinger singer) => this.singer = singer; + public static string? FindInOto(USinger singer, string phoneme, Note note, bool nullIfNotFound = false) { + // 음소와 노트를 입력받고, 다음계 및 보이스컬러 에일리어스를 적용한다. + // nullIfNotFound가 true이면 음소가 찾아지지 않을 때 음소가 아닌 null을 리턴한다. + // nullIfNotFound가 false면 음소가 찾아지지 않을 때 그대로 음소를 반환 + string phonemeToReturn; + string color = string.Empty; + int toneShift = 0; + int? alt = null; + if (phoneme.Equals("")) {return phoneme;} + + if (singer.TryGetMappedOto(phoneme + alt, note.tone + toneShift, color, out var otoAlt)) { + phonemeToReturn = otoAlt.Alias; + } + else if (singer.TryGetMappedOto(phoneme, note.tone + toneShift, color, out var oto)) { + phonemeToReturn = oto.Alias; + } + else if (singer.TryGetMappedOto(phoneme, note.tone, color, out oto)) { + phonemeToReturn = oto.Alias; + } + else if (nullIfNotFound) { + phonemeToReturn = null; + } + else { + phonemeToReturn = phoneme; + } + + return phonemeToReturn; + } + + /// + /// All child Korean Phonemizer have to do is implementing this (1). + /// This Function manages phoneme conversion at Notes that are not in last position. + /// + /// + /// + /// + /// + /// + /// + /// Same as BasePhonemizer.Process(), but just manages Notes that are not in last position. + public virtual Result ConvertPhonemes(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + // below return is Dummy + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = $""}, + } + }; + } + + /// + /// All child Korean Phonemizer have to do is implementing this (2). + /// This Function manages phoneme conversion at Note in last position. + /// + /// + /// + /// + /// + /// + /// + /// Same as BasePhonemizer.Process(), but just manages Note that in last position. + public virtual Result GenerateEndSound(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + // below return is Dummy + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = $""}, + } + }; + } + + /// + /// Returns Result with two input Phonemes. + /// + /// + /// + /// + /// + public Result GenerateResult(String firstPhoneme, String secondPhoneme, int totalDuration, int secondPhonemePosition, int totalDurationDivider=3){ + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = firstPhoneme }, + new Phoneme { phoneme = secondPhoneme, + position = totalDuration - Math.Min(totalDuration / totalDurationDivider, secondPhonemePosition)}, + } + }; + } + + /// + /// Returns Result with two input Phonemes. + /// + /// + /// + /// + /// + public Result GenerateResult(String firstPhoneme, String secondPhoneme, int totalDuration, int totalDurationDivider=3){ + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = firstPhoneme }, + new Phoneme { phoneme = secondPhoneme, + position = totalDuration - totalDuration / totalDurationDivider}, + } + }; + } + + /// + /// Returns Result with one input Phonemes. + /// + public Result GenerateResult(String firstPhoneme){ + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = firstPhoneme }, + } + }; + } + + /// + /// Returns Result with three input Phonemes. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Result + public Result GenerateResult(String firstPhoneme, String secondPhoneme, String thirdPhoneme, int totalDuration, int secondPhonemePosition, int secondTotalDurationDivider=3, int thirdTotalDurationDivider=8){ + return new Result() { + phonemes = new Phoneme[] { + new Phoneme { phoneme = firstPhoneme}, + new Phoneme { phoneme = secondPhoneme, + position = totalDuration - Math.Min(totalDuration / secondTotalDurationDivider, secondPhonemePosition)}, + new Phoneme { phoneme = thirdPhoneme, + position = totalDuration - totalDuration / thirdTotalDurationDivider}, + }// -음소 있이 이어줌 + }; + } + + /// + /// 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; + string phoneticHint = note.phoneticHint; + + Note? prevNote = prevNeighbour; // null or Note + Note thisNote = note; + Note? nextNote = nextNeighbour; // null or Note + + int totalDuration = notes.Sum(n => n.duration); + + if (phoneticHint != null) { + // if there are phonetic hint + // 발음 힌트가 있음 + // 냥[nya2, ang] + string[] phoneticHints = phoneticHint.Split(','); // phonemes are seperated by ','. + int phoneticHintsLength = phoneticHints.Length; + + Phoneme[] phonemes = new Phoneme[phoneticHintsLength]; + + Dictionary VVdictionary = new Dictionary() { }; + + string[] VVsource = new string[] { "a", "i", "u", "e", "o", "eo", "eu" }; + + for (int i = 0; i < 7; i++) { + // VV 딕셔너리를 채운다 + // 나중에 발음기호에 ["a a"]를 입력하고 만일 음원에게 "a a"가 없을 경우, 자동으로 VVDictionary에서 "a a"에 해당하는 값인 "a"를 호출해 사용 + // (반대도 똑같이 적용) + + // VVDictionary 예시: {"a a", "a"} ... + for (int j = 6; j >= 0; j--) { + VVdictionary[$"{VVsource[i]} {VVsource[j]}"] = $"{VVsource[j]}"; // CV/CVC >> CBNN 호환용 + VVdictionary[$"{VVsource[j]}"] = $"{VVsource[i]} {VVsource[j]}"; // CBNN >> CV/CVC 호환용 + } + } + + for (int i = 0; i < phoneticHintsLength; i++) { + string? alias = FindInOto(singer, phoneticHints[i].Trim(), note, true); // alias if exists, otherwise null + + if (alias != null) { + // 발음기호에 입력된 phoneme이 음원에 존재함 + + if (i == 0) { + // first syllable + phonemes[i] = new Phoneme { phoneme = alias }; + } + else if ((i == phoneticHintsLength - 1) && ((phoneticHints[i].Trim().EndsWith('-')) || phoneticHints[i].Trim().EndsWith('R'))) { + // 마지막 음소이고 끝음소(ex: a -, a R)일 경우, VCLengthShort에 맞춰 음소를 배치 + phonemes[i] = new Phoneme { + phoneme = alias, + position = totalDuration - Math.Min(vcLengthShort, totalDuration / 8) + // 8등분한 길이로 끝에 숨소리 음소 배치, n등분했을 때의 음소 길이가 이보다 작다면 n등분했을 때의 길이로 간다 + }; + } + else if (phoneticHintsLength == 2) { + // 입력되는 발음힌트가 2개일 경우, 2등분되어 음소가 배치된다. + // 이 경우 부자연스러우므로 3등분해서 음소 배치하게 조정 + phonemes[i] = new Phoneme { + phoneme = alias, + position = totalDuration - totalDuration / 3 + // 3등분해서 음소가 배치됨 + }; + } + else { + phonemes[i] = new Phoneme { + phoneme = alias, + position = totalDuration - ((totalDuration / phoneticHintsLength) * (phoneticHintsLength - i)) + // 균등하게 n등분해서 음소가 배치됨 + }; + } + } else if (VVdictionary.ContainsKey(phoneticHints[i].Trim())) { + // 입력 실패한 음소가 VV 혹은 V일 때 + if (phoneticHintsLength == 2) { + // 입력되는 발음힌트가 2개일 경우, 2등분되어 음소가 배치된다. + // 이 경우 부자연스러우므로 3등분해서 음소 배치하게 조정 + phonemes[i] = new Phoneme { + phoneme = FindInOto(singer, VVdictionary[phoneticHints[i].Trim()], note), + position = totalDuration - totalDuration / 3 + // 3등분해서 음소가 배치됨 + }; + } + else { + phonemes[i] = new Phoneme { + phoneme = FindInOto(singer, VVdictionary[phoneticHints[i].Trim()], note), + position = totalDuration - ((totalDuration / phoneticHintsLength) * (phoneticHintsLength - i)) + // 균등하게 n등분해서 음소가 배치됨 + }; + } + } else { + // 그냥 음원에 음소가 없음 + phonemes[i] = new Phoneme { + phoneme = phoneticHints[i].Trim(), + position = totalDuration - ((totalDuration / phoneticHintsLength) * (phoneticHintsLength - i)) + // 균등하게 n등분해서 음소가 배치됨 + }; + } + } + + return new Result() { + phonemes = phonemes + }; + } + else if (KoreanPhonemizerUtil.IsHangeul(lyric)) { + return ConvertPhonemes(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours); + } + else { + return GenerateEndSound(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours); + } + } + + /// + /// abstract class for Ini Management + /// To use, child phonemizer should implement this class(BaseIniManager) with its own setting values! + /// + public abstract class BaseIniManager : KoreanPhonemizerUtil.BaseIniManager{} + } +} \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/KoreanCBNNPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCBNNPhonemizer.cs index 35839357a..0ee400ff3 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCBNNPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCBNNPhonemizer.cs @@ -1,854 +1,280 @@ 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 { - /// This phonemizer is based on 'KOR CVC Phonemizer'(by NANA). /// - [Phonemizer("Korean CBNN Phonemizer", "KO CBNN", "EX3", language:"KO")] + /// Phonemizer for 'KOR CBNN' /// + [Phonemizer("Korean CBNN Phonemizer", "KO CBNN", "EX3", language: "KO")] - public class KoreanCBNNPhonemizer : Phonemizer { + public class KoreanCBNNPhonemizer : BaseKoreanPhonemizer { - // ↓ Plainvowels of [ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ]. // - static readonly string[] naPlainVowels = new string[] { "a", "e", "a", "e", "eo", "e", "eo", "e", "o", "a", "e", "e", "o", "u", "eo", "e", "i", "u", "eu", "i", "i" }; - static readonly string[] naConsonants = new string[] { - "ㄱ:g","ㄲ:gg","ㄴ:n","ㄷ:d","ㄸ:dd","ㄹ:r","ㅁ:m","ㅂ:b","ㅃ:bb","ㅅ:s","ㅆ:ss","ㅇ:","ㅈ:j","ㅉ:jj","ㅊ:ch","ㅋ:k","ㅌ:t","ㅍ:p","ㅎ:h" - }; + public override void SetSinger(USinger singer) { + if (this.singer == singer) {return;} + this.singer = singer; + if (this.singer == null) {return;} - // ↓ ㅢ is e (* There's no "eui" in Kor CBNN *).// - static readonly string[] naVowels = new string[] { - "ㅏ:a","ㅐ:e","ㅑ:ya","ㅒ:ye","ㅓ:eo","ㅔ:e","ㅕ:yeo","ㅖ:ye","ㅗ:o","ㅘ:wa","ㅙ:we","ㅚ:we","ㅛ:yo","ㅜ:u","ㅝ:weo","ㅞ:we","ㅟ:wi","ㅠ:yu","ㅡ:eu","ㅢ:e","ㅣ:i" - }; - - // ↓ ["Grapheme : Phoneme"] of batchims. - static readonly string[] naFinals = new string[] { - ":","ㄱ:k","ㄲ:k","ㄳ:k","ㄴ:n","ㄵ:n","ㄶ:n","ㄷ:t","ㄹ:l","ㄺ:l","ㄻ:m","ㄼ:l","ㄽ:l","ㄾ:l","ㄿ:p","ㅀ:l","ㅁ:m","ㅂ:p","ㅄ:p","ㅅ:t","ㅆ:t","ㅇ:ng","ㅈ:t","ㅊ:t","ㅋ:k","ㅌ:t","ㅍ:p:1","ㅎ:t:2" - }; - private const int hangeulStartIndex = 0xAC00; // unicode of '가' - private const int hangeulEndIndex = 0xD7A3; // unicode of '힣' - - // ====================================================================================== - - - // ↓ Plain vowels of Korean. - static readonly string[] plainVowels = new string[] { "eu", "eo", "a", "i", "u", "e", "o" }; + if (this.singer.SingerType != USingerType.Classic){return;} + } - // ↓ Vowels of romanized CVs. - static readonly string[] vowels = new string[] { - "eu=geu,neu,deu,reu,meu,beu,seu,eu,jeu,cheu,keu,teu,peu,heu,ggeu,ddeu,bbeu,sseu,jjeu", - "eo=geo,neo,deo,reo,meo,beo,seo,eo,jeo,cheo,keo,teo,peo,heo,ggeo,ddeo,bbeo,sseo,jjeo,gyeo,nyeo,dyeo,ryeo,myeo,byeo,syeo,yeo,jyeo,chyeo,kyeo,tyeo,pyeo,hyeo,ggyeo,ddyeo,bbyeo,ssyeo,jjyeo,gweo,nweo,dweo,rweo,mweo,bweo,sweo,weo,jweo,chweo,kweo,tweo,pweo,hweo,ggweo,ddweo,bbweo,ssweo,jjweo", - "a=ga,na,da,ra,ma,ba,sa,a,ja,cha,ka,ta,pa,ha,gga,dda,bba,ssa,jja,gya,nya,dya,rya,mya,bya,sya,ya,jya,chya,kya,tya,pya,hya,ggya,ddya,bbya,ssya,jjya,gwa,nwa,dwa,rwa,mwa,bwa,swa,wa,jwa,chwa,kwa,twa,pwa,hwa,ggwa,ddwa,bbwa,sswa,jjwa", - "e=ge,ne,de,re,me,be,se,e,je,che,ke,te,pe,he,gge,dde,bbe,sse,jje,gye,nye,dye,rye,mye,bye,sye,ye,jye,chye,kye,tye,pye,hye,ggye,ddye,bbye,ssye,jjye,gwe,nwe,dwe,rwe,mwe,bwe,swe,we,jwe,chwe,kwe,twe,pwe,hwe,ggwe,ddwe,bbwe,sswe,jjwe", - "i=gi,ni,di,ri,mi,bi,si,i,ji,chi,ki,ti,pi,hi,ggi,ddi,bbi,ssi,jji,gwi,nwi,dwi,rwi,mwi,bwi,swi,wi,jwi,chwi,kwi,twi,pwi,hwi,ggwi,ddwi,bbwi,sswi,jjwi", - "o=go,no,do,ro,mo,bo,so,o,jo,cho,ko,to,po,ho,ggo,ddo,bbo,sso,jjo,gyo,nyo,dyo,ryo,myo,byo,syo,yo,jyo,chyo,kyo,tyo,pyo,hyo,ggyo,ddyo,bbyo,ssyo,jjyo", - "u=gu,nu,du,ru,mu,bu,su,u,ju,chu,ku,tu,pu,hu,ggu,ddu,bbu,ssu,jju,gyu,nyu,dyu,ryu,myu,byu,syu,yu,jyu,chyu,kyu,tyu,pyu,hyu,ggyu,ddyu,bbyu,ssyu,jjyu", - "ng=ang,ing,ung,eng,ong,eung,eong", - "n=an,in,un,en,on,eun,eon", - "m=am,im,um,em,om,eum,eom", - "l=al,il,ul,el,ol,eul,eol", - "p=ap,ip,up,ep,op,eup,eop", - "t=at,it,ut,et,ot,eut,eot", - "k=ak,ik,uk,ek,ok,euk,eok" - }; - // ↓ consonants of romanized CVs. - static readonly string[] consonants = new string[] { - "ggy=ggya,ggyu,ggye,ggyo,ggyeo", - "ggw=ggwa,ggwi,ggwe,ggweo", - "gg=gg,gga,ggi,ggu,gge,ggo,ggeu,ggeo", - "ddy=ddya,ddyu,ddye,ddyo,ddyeo", - "ddw=ddwa,ddwi,ddwe,ddweo", - "dd=dd,dda,ddi,ddu,dde,ddo,ddeu,ddeo", - "bby=bbya,bbyu,bbye,bbyo,bbyeo", - "bbw=bbwa,bbwi,bbwe,bbweo", - "bb=bb,bba,bbi,bbu,bbe,bbo,bbeu,bbeo", - "ssy=ssya,ssyu,ssye,ssyo,ssyeo", - "ssw=sswa,sswi,sswe,ssweo", - "ss=ss,ssa,ssi,ssu,sse,sso,sseu,sseo", - "gy=gya,gyu,gye,gyo,gyeo", - "gw=gwa,gwi,gwe,gweo", - "g=g,ga,gi,gu,ge,go,geu,geo", - "ny=nya,nyu,nye,nyo,nyeo", - "nw=nwa,nwi,nwe,nweo", - "n=n,na,ni,nu,ne,no,neu,neo", - "dy=dya,dyu,dye,dyo,dyeo", - "dw=dwa,dwi,dwe,dweo", - "d=d,da,di,du,de,do,deu,deo", - "ry=rya,ryu,rye,ryo,ryeo", - "rw=rwa,rwi,rwe,rweo", - "r=r,ra,ri,ru,re,ro,reu,reo", - "my=mya,myu,mye,myo,myeo", - "mw=mwa,mwi,mwe,mweo", - "m=m,ma,mi,mu,me,mo,meu,meo", - "by=bya,byu,bye,byo,byeo", - "bw=bwa,bwi,bwe,bweo", - "b=b,ba,bi,bu,be,bo,beu,beo", - "sy=sya,syu,sye,syo,syeo", - "sw=swa,swi,swe,sweo", - "s=s,sa,si,su,se,so,seu,seo", - "jy=jya,jyu,jye,jyo,jyeo", - "jw=jwa,jwi,jwe,jweo", - "j=j,ja,ji,ju,je,jo,jeu,jeo", - "chy=chya,chyu,chye,chyo,chyeo,chwa", - "chw=chwi,chwe,chweo", - "ch=ch,cha,chi,chu,che,cho,cheu,cheo", - "ky=kya,kyu,kye,kyo,kyeo", - "kw=kwa,kwi,kwe,kweo", - "k=k,ka,ki,ku,ke,ko,keu,keo", - "ty=tya,tyu,tye,tyo,tyeo", - "tw=twa,twi,twe,tweo", - "t=t,ta,ti,tu,te,to,teu,teo", - "py=pya,pyu,pye,pyo,pyeo", - "pw=pwa,pwi,pwe,pweo", - "p=p,pa,pi,pu,pe,po,peu,peo", - "hy=hya,hyu,hye,hyo,hyeo", - "hw=hwa,hwi,hwe,hweo", - "h=h,ha,hi,hu,he,ho,heu,heo" + static readonly Dictionary FIRST_CONSONANTS = new Dictionary(){ + {"ㄱ", "g"}, + {"ㄲ", "gg"}, + {"ㄴ", "n"}, + {"ㄷ", "d"}, + {"ㄸ", "dd"}, + {"ㄹ", "r"}, + {"ㅁ", "m"}, + {"ㅂ", "b"}, + {"ㅃ", "bb"}, + {"ㅅ", "s"}, + {"ㅆ", "ss"}, + {"ㅇ", ""}, + {"ㅈ", "j"}, + {"ㅉ", "jj"}, + {"ㅊ", "ch"}, + {"ㅋ", "k"}, + {"ㅌ", "t"}, + {"ㅍ", "p"}, + {"ㅎ", "h"}, + {"null", ""} // 뒤 글자가 없을 때를 대비 }; - - static readonly Dictionary vowelLookup; - static readonly Dictionary consonantLookup; - - string getConsonant(string str) { - str = str.Replace('a', ' '); - str = str.Replace('i', ' '); - str = str.Replace('u', ' '); - str = str.Replace('e', ' '); - str = str.Replace('o', ' '); - str = str.Trim(); - - return str; - } - - bool isAlphaCon(string consStr) { - String str = consStr.Replace('w', ' '); - str = consStr.Replace('y', ' '); - str = str.Trim(); - if (str == "gg") { return true; } - else if (str == "dd") { return true; } - else if (str == "bb") { return true; } - else if (str == "ss") { return true; } - else if (str == "g") { return true; } - else if (str == "n") { return true; } - else if (str == "d") { return true; } - else if (str == "r") { return true; } - else if (str == "m") { return true; } - else if (str == "b") { return true; } - else if (str == "s") { return true; } - else if (str == "j") { return true; } - else if (str == "ch") { return true; } - else if (str == "k") { return true; } - else if (str == "t") { return true; } - else if (str == "p") { return true; } - else if (str == "h") { return true; }else { return false; } - } - - static KoreanCBNNPhonemizer() { - vowelLookup = vowels.ToList() - .SelectMany(line => { - var parts = line.Split('='); - return parts[1].Split(',').Select(cv => (cv, parts[0])); - }) - .ToDictionary(t => t.Item1, t => t.Item2); - consonantLookup = consonants.ToList() - .SelectMany(line => { - var parts = line.Split('='); - return parts[1].Split(',').Select(cv => (cv, parts[0])); - }) - .ToDictionary(t => t.Item1, t => t.Item2); - } - - - // ====================================================================================== - - - private USinger singer; - public override void SetSinger(USinger singer) => this.singer = singer; - - // make it quicker to check multiple oto occurrences at once rather than spamming if else if - private bool checkOtoUntilHit(string[] input, Note note, out UOto oto){ - oto = default; - - var attr0 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - var attr1 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; - - foreach (string test in input){ - if (singer.TryGetMappedOto(test, note.tone + attr0.toneShift, attr0.voiceColor, out oto)){ - return true; - } - } - - return false; - } + static readonly Dictionary MIDDLE_VOWELS = new Dictionary(){ + {"ㅏ", 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]{"i", "", "i"}}, // ㅢ는 ㅣ로 발음 + {"ㅣ", new string[3]{"i", "", "i"}}, + {"null", new string[3]{"", "", ""}} // 뒤 글자가 없을 때를 대비 + }; + static readonly Dictionary LAST_CONSONANTS = new Dictionary(){ + //ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ + {"ㄱ", 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 ConvertForCBNN(Note[] notes, string[] prevLyric, string[] thisLyric, string[] nextLyric, Note? nextNeighbour) { + string thisMidVowelHead; + string thisMidVowelTail; - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { - var note = notes[0]; - var currentUnicode = ToUnicodeElements(note.lyric); // ← unicode of current lyric - string currentLyric = note.lyric; // ← string of current lyric - var attr0 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - var attr1 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; - //-----------------------------------------------------------------------// - ////// *** ↓↓↓ Seperates Lyrics in: // - ///// - first consonant letter(초성, "consonant" in below), // - ///// - middle vowel letter(중성, "vowel" in below), // - ///// - last consonant letter(종성, "final" in below) ↓↓↓ *** //. - + int totalDuration = notes.Sum(n => n.duration); + Note note = notes[0]; - //// ↓↓ 1 ** Variables for 'Current Notes' ** -- - // ↓ index of "consonant", "vowel", "final". - int CLconsonant = 0; - int CLvowel = 0; - int CLfinal = 0; + string soundBeforeEndSound = thisLyric[2] == " " ? thisLyric[1] : thisLyric[2]; + string thisMidVowelForEnd; - // ↓ Use for Temp - string[] TCLtemp; + thisMidVowelForEnd = MIDDLE_VOWELS.ContainsKey(soundBeforeEndSound) ? MIDDLE_VOWELS[soundBeforeEndSound][2] : LAST_CONSONANTS[soundBeforeEndSound][0]; + string endSound = $"{thisMidVowelForEnd} -"; - // ↓ use these for applying phonological rules - string TCLconsonant = ""; - string TCLvowel = ""; - string TCLfinal = ""; - string TCLplainvowel = ""; //← Simplifies vowels + bool isItNeedsFrontCV; + bool isRelaxedVC; + bool isItNeedsVC; + bool isItNeedsVV; + bool isItNeedsVSv; // V + Semivowel, example) a y, a w + bool isItNeedsEndSound; - int TCLsemivowel = 0; // semi vowel is 'y', 'w'. [0 means "there's no semi vowel], [1 means "there is 'y'"], [2 means "there is 'w'"]] + isItNeedsVV = prevLyric[2] == " " && thisLyric[0] == "ㅇ" && PLAIN_VOWELS.Contains(thisLyric[1]); - // ↓ use these for generating phonemes in phonemizers - string TCLconsonantCBNN = ""; - string TCLvowelCBNN = ""; - - //// ↓↓ 2 ** Variables for 'Next Notes' ** -- - // ↓ index of "consonant", "vowel", "final". - int NLconsonant = 0; - int NLvowel = 0; - int NLfinal = 0; - - // ↓ Use for Temp - string[] TNLtemp; - - // ↓ use these for applying phonological rules - string TNLconsonant = ""; - string TNLvowel = ""; - string TNLfinal = ""; - string TNLplainvowel = ""; - - // ↓ use these for generating phonemes in phonemizers - string TNLconsonantCBNN = ""; - //string TNLvowelCBNN = ""; - - int TNLsemivowel = 0; // semi vowel is 'y', 'w'. [0 means "there's no semi vowel], [1 means "there is 'y'"], [2 means "there is 'w'"]] - - //// ↓↓ 3 ** Variables for 'Previous Notes' ** -- - // ↓ index of "consonant", "vowel", "final". - int PLconsonant = 0; - int PLvowel = 0; - int PLfinal = 0; - - // ↓ Use for Temp - string[] TPLtemp; - - // ↓ use these for applying phonological rules - string TPLconsonant = ""; - string TPLvowel = ""; - string TPLfinal = ""; - string TPLplainvowel = ""; - string TPLplainfinal = ""; - - // ↓ use these for generating phonemes in phonemizers - //string TPLconsonantCBNN = ""; - //string TPLvowelCBNN = ""; - - //int TPLsemivowel = 0; // semi vowel is 'y', 'w'. [0 means "there's no semi vowel], [1 means "there is 'y'"], [2 means "there is 'w'"]] - - - //// ↓↓ 4 ** Variables for checking notes ** -- - bool currentHangeul = false; - bool prevHangeul = false; - bool nextHangeul = false; - - bool prevExist = false; - bool nextExist = false; - - char firstCL, firstPL, firstNL; - int uCL, uPL, uNL; - bool prevIsBreath = false; - - - // check first lyric - firstCL = currentLyric[0]; + 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] == "ㄹ")); + isItNeedsEndSound = (nextLyric[0] == "null" || nextLyric[1] == "null") && nextNeighbour == null; + if (thisLyric.All(part => part == null)) { + return GenerateResult(FindInOto(note.lyric, note)); + } + else { + thisMidVowelHead = $"{MIDDLE_VOWELS[thisLyric[1]][1]}"; + thisMidVowelTail = $"{MIDDLE_VOWELS[thisLyric[1]][2]}"; + } - uCL = (int)firstCL; - if ((uCL >= hangeulStartIndex) && (uCL <= hangeulEndIndex)) { - currentHangeul = true; - CLconsonant = (uCL - hangeulStartIndex) / (21 * 28); - CLvowel = (uCL - hangeulStartIndex) % (21 * 28) / 28; - CLfinal = (uCL - hangeulStartIndex) % 28; - - - TCLtemp = naVowels[CLvowel].Split(":"); - TCLvowel = TCLtemp[1]; - TCLplainvowel = naPlainVowels[CLvowel]; - - if (TCLvowel.StartsWith('y')) {TCLsemivowel = 1;} - else if (TCLvowel.StartsWith('w')) {TCLsemivowel = 2;} - - TCLtemp = naConsonants[CLconsonant].Split(":"); - TCLconsonant = TCLtemp[1]; - - TCLtemp = naFinals[CLfinal].Split(":"); - TCLfinal = TCLtemp[1]; - - - // TCLconsonant : 현노트 초성 TCLvowel : 현노트 중성 TCLfinal : 현노트 종성 - + string CV = $"{FIRST_CONSONANTS[thisLyric[0]]}{thisMidVowelHead}{thisMidVowelTail}{LAST_CONSONANTS[thisLyric[2]][1]}"; + if (FindInOto(CV, note, true) == null) { + CV = CV.Substring(0, CV.Length - 1); } - - // 이전 노트 존재 여부 확인 + 이전 노트 첫번째 글자 확인 - if (prevNeighbour != null) { - firstPL = (prevNeighbour?.lyric)[0]; // 가사 받아오기 - prevExist = true; // 이전 노트 존재한다 반짝 - - uPL = (int)firstPL; // 가사를 int로 변환 - - if ((uPL >= hangeulStartIndex) && (uPL <= hangeulEndIndex)) { - prevHangeul = true; - - PLconsonant = (uPL - hangeulStartIndex) / (21 * 28); - PLvowel = (uPL - hangeulStartIndex) % (21 * 28) / 28; - PLfinal = (uPL - hangeulStartIndex) % 28; - - - TPLtemp = naConsonants[PLconsonant].Split(":"); - TPLconsonant = TPLtemp[1]; - - TPLtemp = naVowels[PLvowel].Split(":"); - TPLvowel = TPLtemp[1]; - TPLplainvowel = naPlainVowels[PLvowel]; - - //if (TPLvowel.StartsWith('y')) {TPLsemivowel = 1;} - //else if (TPLvowel.StartsWith('w')) {TPLsemivowel = 2;} - - TPLtemp = naFinals[PLfinal].Split(":"); - TPLfinal = TPLtemp[1]; - TPLplainfinal = TPLfinal; + string frontCV; + string batchim; + string VC = $"{thisMidVowelTail} {FIRST_CONSONANTS[nextLyric[0]]}"; + string VV = $"{MIDDLE_VOWELS[prevLyric[1]][2]} {thisMidVowelTail}"; + string VSv = $"{thisMidVowelTail} {MIDDLE_VOWELS[nextLyric[1]][1]}"; + isItNeedsVSv = thisLyric[2] == " " && nextLyric[0] == "ㅇ" && !PLAIN_VOWELS.Contains(nextLyric[1]) && FindInOto(VSv, note, true) != null; + isItNeedsVC = thisLyric[2] == " " && nextLyric[0] != "ㅇ" && nextLyric[0] != "null" && FindInOto(VC, note, true) != null; + + frontCV = $"- {CV}"; + if (FindInOto(frontCV, note, true) == null) { + frontCV = $"-{CV}"; + if (FindInOto(frontCV, note, true) == null) { + frontCV = CV; } } - // 다음 노트 존재 여부 확인 + 다음 노트 첫번째 글자 확인 - if (nextNeighbour != null) { - firstNL = (nextNeighbour?.lyric)[0]; - nextExist = true; - uNL = (int)firstNL; - - if ((uNL >= hangeulStartIndex) && (uNL <= hangeulEndIndex)) { - nextHangeul = true; - - NLconsonant = (uNL - hangeulStartIndex) / (21 * 28); - NLvowel = (uNL - hangeulStartIndex) % (21 * 28) / 28; - NLfinal = (uNL - hangeulStartIndex) % 28; - - - TNLtemp = naConsonants[NLconsonant].Split(":"); - TNLconsonant = TNLtemp[1]; - - TNLtemp = naVowels[NLvowel].Split(":"); - TNLvowel = TNLtemp[1]; - TNLplainvowel = naPlainVowels[NLvowel]; - - if (TNLvowel.StartsWith('y')) {TNLsemivowel = 1;} - else if (TNLvowel.StartsWith('w')) {TNLsemivowel = 2;} - + if (isItNeedsVV) {CV = VV;} + - TNLtemp = naFinals[NLfinal].Split(":"); - TNLfinal = TNLtemp[1]; + if (thisLyric[2] == " " && isItNeedsVC) { // no batchim, needs VC + if (isItNeedsFrontCV){ + return GenerateResult(FindInOto(frontCV, note), FindInOto(VC, note), totalDuration, 120, 3); } + return GenerateResult(FindInOto(CV, note), FindInOto(VC, note), totalDuration, 120, 3); } - if (currentHangeul) { - // 음운규칙 적용 - if (currentHangeul) { - - // 1. 연음법칙 - string tempTCLconsonant = ""; - string tempTCLfinal = ""; - bool yeoneum = false; - bool yeoneum2 = false; - - if (prevExist && prevHangeul && (CLconsonant == 11) && (TPLfinal != "")) { - int temp = PLfinal; - if (temp == 1) { TCLtemp = naConsonants[0].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 2) { TCLtemp = naConsonants[1].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 3) { TCLtemp = naConsonants[10].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 4) { TCLtemp = naConsonants[2].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 5) { TCLtemp = naConsonants[12].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 6) { TCLtemp = naConsonants[18].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 7) { TCLtemp = naConsonants[3].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 8) { TCLtemp = naConsonants[5].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 9) { TCLtemp = naConsonants[0].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 10) { TCLtemp = naConsonants[6].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 11) { TCLtemp = naConsonants[7].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 12) { TCLtemp = naConsonants[9].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 13) { TCLtemp = naConsonants[16].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 14) { TCLtemp = naConsonants[17].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 15) { TCLtemp = naConsonants[18].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 16) { TCLtemp = naConsonants[6].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 17) { TCLtemp = naConsonants[7].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 18) { TCLtemp = naConsonants[9].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 19) { TCLtemp = naConsonants[9].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 20) { TCLtemp = naConsonants[10].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 21) { tempTCLconsonant = ""; yeoneum = true; } - else if (temp == 22) { TCLtemp = naConsonants[12].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 23) { TCLtemp = naConsonants[14].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 24) { TCLtemp = naConsonants[15].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 25) { TCLtemp = naConsonants[16].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 26) { TCLtemp = naConsonants[17].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - else if (temp == 27) { TCLtemp = naConsonants[18].Split(":"); tempTCLconsonant = TCLtemp[1]; yeoneum = true; } - } - - if (nextExist && nextHangeul && (TCLfinal != "") && (TNLconsonant == "")) { - int temp = CLfinal; - - if (temp == 1) { TCLtemp = naConsonants[0].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 2) { TCLtemp = naConsonants[1].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 3) { TCLfinal = "k"; yeoneum2 = true; } - else if (temp == 4) { TCLtemp = naConsonants[2].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 5) { TCLfinal = "n"; yeoneum2 = true; } - else if (temp == 6) { TCLfinal = "n"; yeoneum2 = true; } - else if (temp == 7) { TCLtemp = naConsonants[3].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 8) { TCLtemp = naConsonants[5].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 9) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 10) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 11) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 12) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 13) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 14) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 15) { TCLfinal = "l"; yeoneum2 = true; } - else if (temp == 16) { TCLtemp = naConsonants[6].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 17) { TCLtemp = naConsonants[7].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 18) { TCLfinal = "p"; yeoneum2 = true; } - else if (temp == 19) { TCLtemp = naConsonants[9].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 20) { TCLtemp = naConsonants[10].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - //else if (temp == 21) { TCLtemp = naConsonants[11].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - else if (temp == 22) { TCLtemp = naConsonants[12].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } else if (temp == 23) { TCLtemp = naConsonants[14].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } else if (temp == 24) { TCLtemp = naConsonants[15].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } else if (temp == 25) { TCLtemp = naConsonants[16].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } else if (temp == 26) { TCLtemp = naConsonants[17].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } else if (temp == 27) { TCLtemp = naConsonants[18].Split(":"); tempTCLfinal = TCLtemp[1]; TCLfinal = ""; yeoneum2 = true; } - - } - if (yeoneum) { TCLconsonant = tempTCLconsonant; } - if (yeoneum2) { TNLconsonant = tempTCLfinal; } - - - // 2. 격음화/유기음화/거센소리되기 - if (prevExist && prevHangeul && (TPLfinal != "")) { - if (((PLfinal == 27) && (CLconsonant == 0)) || ((PLfinal == 6) && (CLconsonant == 0)) || ((PLfinal == 15) && (CLconsonant == 0))) { TCLconsonant = "k"; } else if (((PLfinal == 27) && (CLconsonant == 3)) || ((PLfinal == 6) && (CLconsonant == 3)) || ((PLfinal == 15) && (CLconsonant == 3))) { TCLconsonant = "t"; } else if (((PLfinal == 27) && (CLconsonant == 12)) || ((PLfinal == 6) && (CLconsonant == 12)) || ((PLfinal == 15) && (CLconsonant == 12))) { TCLconsonant = "ch"; } else if (((PLfinal == 27) && (CLconsonant == 9)) || ((PLfinal == 6) && (CLconsonant == 9)) || ((PLfinal == 15) && (CLconsonant == 9))) { TCLconsonant = "ss"; } - - if ((PLfinal == 1) && (CLconsonant == 18)) { TCLconsonant = "k"; } else if ((PLfinal == 7) && (CLconsonant == 18)) { TCLconsonant = "t"; } else if ((PLfinal == 17) && (CLconsonant == 18)) { TCLconsonant = "p"; } else if ((PLfinal == 22) && (CLconsonant == 18)) { TCLconsonant = "ch"; } - } - if (nextExist && nextHangeul && (TCLfinal != "")) { - if ((NLconsonant == 0) && (CLfinal == 27)) { TCLfinal = ""; TNLconsonant = "k"; } else if ((NLconsonant == 0) && (CLfinal == 6)) { TCLfinal = "n"; TNLconsonant = "k"; } else if ((NLconsonant == 0) && (CLfinal == 15)) { TCLfinal = "l"; TNLconsonant = "k"; } else if ((NLconsonant == 3) && (CLfinal == 27)) { TCLfinal = ""; TNLconsonant = "t"; } else if ((NLconsonant == 3) && (CLfinal == 6)) { TCLfinal = "n"; TNLconsonant = "t"; } else if ((NLconsonant == 3) && (CLfinal == 15)) { TCLfinal = "l"; TNLconsonant = "t"; } else if ((NLconsonant == 12) && (CLfinal == 27)) { TCLfinal = ""; TNLconsonant = "ch"; } else if ((NLconsonant == 12) && (CLfinal == 6)) { TCLfinal = "n"; TNLconsonant = "ch"; } else if ((NLconsonant == 12) && (CLfinal == 15)) { TCLfinal = "l"; TNLconsonant = "ch"; } else if ((NLconsonant == 9) && (CLfinal == 27)) { TCLfinal = ""; TNLconsonant = "ss"; } else if ((NLconsonant == 9) && (CLfinal == 6)) { TCLfinal = "n"; TNLconsonant = "ss"; } else if ((NLconsonant == 9) && (CLfinal == 15)) { TCLfinal = "l"; TNLconsonant = "ss"; } - - if ((NLconsonant == 2) && (CLfinal == 27)) { TCLfinal = "n"; } - - if ((NLconsonant == 18) && (CLfinal == 1)) { TCLfinal = ""; TNLconsonant = "k"; } else if ((NLconsonant == 18) && (CLfinal == 7)) { TCLfinal = ""; TNLconsonant = "t"; } else if ((NLconsonant == 18) && (CLfinal == 17)) { TCLfinal = ""; TNLconsonant = "p"; } else if ((NLconsonant == 18) && (CLfinal == 22)) { TCLfinal = ""; TNLconsonant = "ch"; } - } - - - // 3. 음절의 끝소리 규칙 예외 - if (nextExist && nextHangeul) { - /* - // ㄼ + 자음이 있을 때 => ㄼ : p - if ((CLfinal == 11) && (TCLconsonant != "")) { TCLfinal = "p"; } - */ - // ㄺ + ㄱ => ㄺ : ㄹ - if ((CLfinal == 9) && (NLconsonant == 0)) { TCLfinal = "l"; } - } - - - // 4. 경음화/된소리되기 - if (prevExist && prevHangeul && TPLfinal != "") { - // ㄱㄷㅂ + ㄱㄷㅂㅅㅈ = ㄲㄸㅃㅆㅉ - if (((TPLfinal == "k") && (CLconsonant == 0)) || ((TPLfinal == "t") && (CLconsonant == 0)) || ((TPLfinal == "p") && (CLconsonant == 0))) { TCLconsonant = "gg"; } else if (((TPLfinal == "k") && (CLconsonant == 3)) || ((TPLfinal == "t") && (CLconsonant == 3)) || ((TPLfinal == "p") && (CLconsonant == 3))) { TCLconsonant = "dd"; } else if (((TPLfinal == "k") && (CLconsonant == 7)) || ((TPLfinal == "t") && (CLconsonant == 7)) || ((TPLfinal == "p") && (CLconsonant == 7))) { TCLconsonant = "bb"; } else if (((TPLfinal == "k") && (CLconsonant == 9)) || ((TPLfinal == "t") && (CLconsonant == 9)) || ((TPLfinal == "p") && (CLconsonant == 9))) { TCLconsonant = "ss"; } else if (((TPLfinal == "k") && (CLconsonant == 12)) || ((TPLfinal == "t") && (CLconsonant == 12)) || ((TPLfinal == "p") && (CLconsonant == 12))) { TCLconsonant = "jj"; } - - - // 용언 어간 받침 ㄴㅁ + ㄱㄷㅅㅈ = ㄲㄸㅆㅉ - if(((TPLfinal=="n")&&(CLconsonant==0))|| ((TPLfinal == "m") && (CLconsonant == 0))) { TCLconsonant = "gg"; } - else if (((TPLfinal == "n") && (CLconsonant == 3)) || ((TPLfinal == "m") && (CLconsonant == 3))) { TCLconsonant = "dd"; } - else if (((TPLfinal == "n") && (CLconsonant == 9)) || ((TPLfinal == "m") && (CLconsonant == 9))) { TCLconsonant = "ss"; } - else if (((TPLfinal == "n") && (CLconsonant == 12)) || ((TPLfinal == "m") && (CLconsonant == 12))) { TCLconsonant = "jj"; } - - // 관형사형 어미ㄹ / 한자어 ㄹ + ㄷㅅㅈ = ㄸㅆㅉ - if ((PLfinal == 8) && (CLconsonant == 3)) { TCLconsonant = "dd"; } else if ((PLfinal == 8) && (CLconsonant == 9)) { TCLconsonant = "ss"; } else if ((PLfinal == 8) && (CLconsonant == 12)) { TCLconsonant = "jj"; } - - // 어간 받침 ㄼㄾ + ㄱㄷㅅㅈ = ㄲㄸㅆㅉ - if (((PLfinal == 11) && (CLconsonant == 0)) || ((PLfinal == 13) && (CLconsonant == 0))) { TCLconsonant = "gg"; } else if (((PLfinal == 11) && (CLconsonant == 3)) || ((PLfinal == 13) && (CLconsonant == 3))) { TCLconsonant = "dd"; } else if (((PLfinal == 11) && (CLconsonant == 9)) || ((PLfinal == 13) && (CLconsonant == 9))) { TCLconsonant = "ss"; } else if (((PLfinal == 11) && (CLconsonant == 12)) || ((PLfinal == 13) && (CLconsonant == 12))) { TCLconsonant = "jj"; } - } - - - // 5. 구개음화 - if (prevExist && prevHangeul && (TPLfinal != "")) { - if ((PLfinal == 7) && (CLconsonant == 11) && (CLvowel == 20)) { TCLconsonant = "j"; } else if ((PLfinal == 25) && (CLconsonant == 11) && (CLvowel == 20)) { TCLconsonant = "ch"; } else if ((PLfinal == 13) && (CLconsonant == 11) && (CLvowel == 20)) { TCLconsonant = "ch"; } else if ((PLfinal == 7) && (CLconsonant == 18) && (CLvowel == 20)) { TCLconsonant = "ch"; } - } - if (nextExist && nextHangeul && (TCLfinal != "")) { - if ((CLfinal == 7) && (NLconsonant == 11) && (NLvowel == 20)) { TCLfinal = ""; } else if ((CLfinal == 25) && (NLconsonant == 11) && (NLvowel == 20)) { TCLfinal = ""; } else if ((CLfinal == 13) && (NLconsonant == 11) && (NLvowel == 20)) { TCLfinal = ""; } else if ((CLfinal == 7) && (NLconsonant == 18) && (NLvowel == 20)) { TCLfinal = ""; } - - } - - - // 6. 비음화 - /** - if (prevExist && prevHangeul && (TPLfinal != "")) { - // 한자어 받침 ㅁㅇ + ㄹ = ㄴ - if (((TPLfinal == "m") && (CLconsonant == 5)) || ((TPLfinal == "ng") && (CLconsonant == 5))) { TCLconsonant = "n"; } - - // 한자어 받침 ㄱㄷㅂ + ㄹ = ㅇㄴㅁ + ㄴ(1) - if (((TPLfinal == "k") && (CLconsonant == 5)) || ((TPLfinal == "t") && (CLconsonant == 5)) || ((TPLfinal == "p") && (CLconsonant == 5))) { TCLconsonant = "n"; } - } - **/ - if (nextExist && nextHangeul && (TCLfinal != "")) { - //받침 ㄱㄷㅂ + ㄴㅁ = ㅇㄴㅁ - if (((TCLfinal == "k") && (TNLconsonant == "n")) || ((TCLfinal == "k") && (TNLconsonant == "m"))) { TCLfinal = "ng"; } else if (((TCLfinal == "t") && (TNLconsonant == "n")) || ((TCLfinal == "t") && (TNLconsonant == "m"))) { TCLfinal = "n"; } else if (((TCLfinal == "p") && (TNLconsonant == "n")) || ((TCLfinal == "p") && (TNLconsonant == "m"))) { TCLfinal = "m"; } - - // 한자어 받침 ㄱㄷㅂ + ㄹ = ㅇㄴㅁ + ㄴ(2) - if ((TCLfinal == "k") && (NLconsonant == 5)) { TCLfinal = "ng"; } else if ((TCLfinal == "t") && (NLconsonant == 5)) { TCLfinal = "n"; } else if ((TCLfinal == "p") && (NLconsonant == 5)) { TCLfinal = "m"; } - } - - - // 7. 유음화 - /** - if (prevExist && prevHangeul && (TPLfinal != "")) { - if (((PLfinal == 8) && (TCLconsonant == "n")) || ((PLfinal == 13) && (TCLconsonant == "n")) || ((PLfinal == 15) && (TCLconsonant == "n"))) { TCLconsonant = "r"; } - } - if (nextExist && nextHangeul && (TCLfinal != "")) { - if ((TCLfinal == "n") && (TNLconsonant == "r")) { TCLfinal = "l"; } - } - **/ - - - - // 8. 받침 + ㄹ = ㄹㄹ - - - - // consonant에 변경 사항이 있을 때 - //if (prevExist && prevHangeul) { - - - // 비음화 - // (1) ㄱ(ㄲㅋㄳㄺ) - // ㄷ(ㅅ,ㅆ,ㅈ,ㅊ,ㅌ,ㅎ) - // ㅂ(ㅍ,ㄼ,ㄿ,ㅄ) - - - //} - // final에 변경 사항이 있을 때 - - + if (thisLyric[2] == " " && isItNeedsVSv) { // no batchim, needs VSv + if (isItNeedsFrontCV){ + return GenerateResult(FindInOto(frontCV, note), FindInOto(VSv, note), totalDuration, 120, 3); } + return GenerateResult(FindInOto(CV, note), FindInOto(VSv, note), totalDuration, 120, 3); + } - bool isLastBatchim = false; - - // vowels do not have suffixed phonemes in CBNN, so use suffixed '- h'~ phonemes instead. - if (!prevExist && TCLconsonant == "" && TCLfinal != "" && TCLvowel != "") { - TCLconsonant = "h"; + if (thisLyric[2] == " ") { // no batchim, doesn't need VC + if (isItNeedsFrontCV){ + return isItNeedsEndSound ? + GenerateResult(FindInOto(frontCV, note), FindInOto(endSound, note), totalDuration, 8) + : GenerateResult(FindInOto(frontCV, note)); } - - // to make FC's length to 1 if FC comes final (=no next note) - if (!nextHangeul && TCLfinal != "" &&TCLvowel != "") { - isLastBatchim = true; - } - - // To use semivowels in VC (ex: [- ga][a gy][gya], ** so not [- ga][a g][gya] **) - if (TCLsemivowel == 1 && TPLplainvowel != "i" && TPLplainvowel != "eu") {TCLconsonantCBNN = TCLconsonant + 'y';} - else if (TCLsemivowel == 2 && TPLplainvowel != "u" && TPLplainvowel != "o" && TPLplainvowel != "eu") {TCLconsonantCBNN = TCLconsonant + 'w';} - else {TCLconsonantCBNN = TCLconsonant;} - - if (TNLsemivowel == 1 && TCLplainvowel != "i" && TCLplainvowel != "eu") {TNLconsonantCBNN = TNLconsonant + 'y';} - else if (TNLsemivowel == 2 && TCLplainvowel != "u" && TCLplainvowel != "o" && TCLplainvowel != "eu") {TNLconsonantCBNN = TNLconsonant + 'w';} - else {TNLconsonantCBNN = TNLconsonant;} - - - - //To set suffix of CV, according to next-coming batchim. - if (TCLfinal == "") { - TCLvowelCBNN = TCLvowel;} - else if (TCLfinal == "m" && TCLconsonantCBNN != "" || TCLfinal == "m" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '1';} - else if (TCLfinal == "n" && TCLconsonantCBNN != "" || TCLfinal == "n" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '2';} - else if (TCLfinal == "ng" && TCLconsonantCBNN != "" || TCLfinal == "ng" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '3';} - else if (TCLfinal == "l" && TCLconsonantCBNN != "" || TCLfinal == "l" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '4';} - else if (TCLfinal == "k" && TCLconsonantCBNN != "" || TCLfinal == "k" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel;} - else if (TCLfinal == "t" && TCLconsonantCBNN != "" || TCLfinal == "t" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '3';} - else if (TCLfinal == "p" && TCLconsonantCBNN != "" || TCLfinal == "p" && TCLconsonantCBNN == "" && TCLsemivowel != 0) { - TCLvowelCBNN = TCLvowel + '1';} - else {TCLvowelCBNN = TCLvowel;} - - - string CV = (TCLconsonant + TCLvowelCBNN); - string VC = ""; - bool comesSemivowelWithoutVC = false; - - - if (TCLsemivowel != 0 && TCLconsonant == ""){ - comesSemivowelWithoutVC = true; + return isItNeedsEndSound ? + GenerateResult(FindInOto(CV, note), FindInOto(endSound, note), totalDuration, 8) + : GenerateResult(FindInOto(CV, note)); + } + + 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), FindInOto(endSound, note), totalDuration, 120, 2, 3); } - if (nextExist && (TCLfinal == "")) { VC = TCLplainvowel + " " + TNLconsonantCBNN; } - - //for Vowel VCV - if (prevExist && TPLfinal == "" && TCLconsonantCBNN == "" && !comesSemivowelWithoutVC) {CV = TPLplainvowel + " " + TCLvowel;} - - - string FC = ""; - if (TCLfinal != "") { FC = TCLplainvowel + TCLfinal; } - - - // for [- XX] phonemes - if (!prevExist || prevIsBreath || TPLfinal != "" && TCLconsonant != "r" && TCLconsonant != "n" && TCLconsonant != "" ) { CV = $"- {CV}"; } - - - // 만약 받침이 있다면 - if (FC != "") { - int totalDuration = notes.Sum(n => n.duration); - int fcLength = totalDuration / 3; - - if (isLastBatchim) { - fcLength = 1; - } - else if ((TCLfinal == "k") || (TCLfinal == "p") || (TCLfinal == "t")) { - fcLength = totalDuration / 2;} - else if ((TCLfinal == "l") || (TCLfinal == "ng") || (TCLfinal == "m")) { - fcLength = totalDuration / 5;} - else if ((TCLfinal == "n")) { - fcLength = totalDuration / 3; - } - - if (singer.TryGetMappedOto(CV, note.tone + attr0.toneShift, attr0.voiceColor, out var oto1) && singer.TryGetMappedOto(FC, note.tone + attr0.toneShift, attr0.voiceColor, out var oto2)) { - CV = oto1.Alias; - FC = oto2.Alias; - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = CV, - }, - new Phoneme() { - phoneme = FC, - position = totalDuration - fcLength, - } - }, - }; - } - - - + return isRelaxedVC ? + GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), totalDuration, 120, 8) + : GenerateResult(FindInOto(CV, note), FindInOto(batchim, note), FindInOto(endSound, note), totalDuration, 120, 2, 3); + } + 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); + } + + } - - // 만약 받침이 없다면 - if (TCLfinal == "") { - // 뒤에 노트가 있다면 - if ((TNLconsonantCBNN != "")) { - int totalDuration = notes.Sum(n => n.duration); - int vcLength = 60; - if ((TNLconsonant == "r") || (TNLconsonant == "g") || (TNLconsonant == "d") || (TNLconsonant == "n")) { vcLength = 33; } - else if (TNLconsonant == "h") { - vcLength = 15; - } - else if ((TNLconsonant == "ch") || (TNLconsonant == "gg")) { vcLength = totalDuration / 2; } - else if ((TNLconsonant == "k") || (TNLconsonant == "t") || (TNLconsonant == "p") || (TNLconsonant == "dd") || (TNLconsonant == "bb") || (TNLconsonant == "ss") || (TNLconsonant == "jj")) { vcLength = totalDuration / 3; } - vcLength = Math.Min(totalDuration / 2, vcLength); - - if (singer.TryGetMappedOto(CV, note.tone + attr0.toneShift, attr0.voiceColor, out var oto1) && singer.TryGetMappedOto(VC, note.tone + attr0.toneShift, attr0.voiceColor, out var oto2)) { - CV = oto1.Alias; - VC = oto2.Alias; - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = CV, - }, - new Phoneme() { - phoneme = VC, - position = totalDuration - vcLength, - } - }, - }; - } - - } - } + private string? FindInOto(String phoneme, Note note, bool nullIfNotFound=false){ + return BaseKoreanPhonemizer.FindInOto(singer, phoneme, note, nullIfNotFound); + } - // 그 외(받침 없는 마지막 노트) - if (singer.TryGetMappedOto(CV, note.tone + attr0.toneShift, attr0.voiceColor, out var oto)){ - CV = oto.Alias; - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = CV, - } - }, - }; - } - } + private string HandleEmptyFirstConsonant(string lyric) { + return lyric == " " ? "ㅇ" : lyric; + } - if (prevHangeul) { - string endBreath = "-"; + public override Result ConvertPhonemes(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + Note note = notes[0]; - if (prevExist && TPLfinal == "" && endBreath.Contains(currentLyric)) { - endBreath = $"{TPLplainvowel} -"; - prevIsBreath = true; // to prevent this→→ case→→, for example... "[사, -, 사 (=notes)]" should be "[- sa, a -, - sa(=phonemes)]", but it becomes [sa, a -, 사(=phonemes)] in phonemizer, so '사' note becomes *no sound. - } - else if (prevExist && TPLfinal != "" && endBreath.Contains(currentLyric)) { - endBreath = $"{TPLplainfinal} -"; - prevIsBreath = true; // to prevent this→→ case→→, for example... "[사, -, 사 (=notes)]" should be "[- sa, a -, - sa(=phonemes)]", but it becomes [sa, a -, 사(=phonemes)] in phonemizer, so '사' note becomes *no sound. - } + 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 (singer.TryGetMappedOto(endBreath, note.tone + attr0.toneShift, attr0.voiceColor, out var oto)){ - endBreath = oto.Alias; - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = endBreath, - } - }, - }; - } + if (thisLyric[0] == "null") { + return GenerateResult(FindInOto(notes[0].lyric, notes[0])); } + + return ConvertForCBNN(notes, prevLyric, thisLyric, nextLyric, nextNeighbour); + } + - - - // ====================================================================================== -/** + public override Result GenerateEndSound(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + Note note = notes[0]; if (prevNeighbour == null) { - // Use "- V" or "- CV" if present in voicebank - var initial = $"- {currentLyric}"; - string[] tests = new string[] {initial, currentLyric}; - // try [- XX] before trying plain lyric - if (checkOtoUntilHit(tests, note, out var oto)){ - currentLyric = oto.Alias; - } - } else if ("-".Contains(currentLyric)) { - var prevUnicode = ToUnicodeElements(prevNeighbour?.lyric); - prevIsBreath = true; - // end breath note - if (vowelLookup.TryGetValue(prevUnicode.LastOrDefault() ?? string.Empty, out var vow)) { - var vowel = ""; - var prevLyric = string.Join("", prevUnicode);; - vowel = vow; - - var endBreath = $"{vow} -"; - if (prevLyric.EndsWith("eo")) { - endBreath = $"eo -"; - } else if (prevLyric.EndsWith("eu")) { - endBreath = $"eu -"; - } - - // try end breath - string[] tests = new string[] {endBreath, currentLyric}; - if (checkOtoUntilHit(tests, note, out var oto)){ - currentLyric = oto.Alias; - } - } - } else { - string[] tests = new string[] {currentLyric}; - if (checkOtoUntilHit(tests, note, out var oto)){ - currentLyric = oto.Alias; - } + return GenerateResult(FindInOto(note.lyric, note)); } -**/ - if (nextNeighbour != null) { // 다음에 노트가 있으면 - var nextUnicode = ToUnicodeElements(nextNeighbour?.lyric); - var nextLyric = string.Join("", nextUnicode); - - // Check if next note is a vowel and does not require VC - if (plainVowels.Contains(nextUnicode.FirstOrDefault() ?? string.Empty)) { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = currentLyric, - } - }, - }; - } - // Insert VC before next neighbor - // Get vowel from current note - var vowel = ""; - - if (vowelLookup.TryGetValue(currentUnicode.LastOrDefault() ?? string.Empty, out var vow)) { - vowel = vow; - - if (currentLyric.Contains("e")) { - vowel = "e" + vowel; - vowel = vowel.Replace("ee", "e"); - } - } - - // Get consonant from next note - var consonant = ""; - if (consonantLookup.TryGetValue(nextUnicode.FirstOrDefault() ?? string.Empty, out var con)) { - consonant = getConsonant(nextNeighbour?.lyric); //로마자만 가능 - if (!(isAlphaCon(consonant))) { consonant = con; } - } + Note prevNeighbour_ = (Note)prevNeighbour; + Hashtable lyrics = KoreanPhonemizerUtil.Separate(prevNeighbour_.lyric); - if (consonant == "") { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = currentLyric, - } - }, - }; - } + string[] prevLyric = new string[]{ // "ㄴ", "ㅑ", "ㅇ" + HandleEmptyFirstConsonant((string)lyrics[0]), + (string)lyrics[1], + (string)lyrics[2] + }; - var vcPhoneme = $"{vowel} {consonant}"; - var vcPhonemes = new string[] {vcPhoneme, ""}; - if (checkOtoUntilHit(vcPhonemes, note, out var oto1)) { - vcPhoneme = oto1.Alias; - } else { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = currentLyric, - } - }, - }; - } + string soundBeforeEndSound = prevLyric[2] == " " ? prevLyric[1] : prevLyric[2]; + string endSound = note.lyric; + string prevMidVowel; - 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)) { - vcLength = MsToTick(oto.Preutter); + 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)); } - vcLength = Math.Min(totalDuration / 2, vcLength); - - - - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = currentLyric, - }, - new Phoneme() { - phoneme = vcPhoneme, - position = totalDuration - vcLength, - } - }, - }; + return GenerateResult(FindInOto($"{prevMidVowel}{endSound}", note, true)); } - - // No next neighbor - return new Result { - phonemes = new Phoneme[] { - new Phoneme { - phoneme = currentLyric, - } - }, - }; + return GenerateResult(FindInOto($"{prevMidVowel} {endSound}", note)); } } -} +} \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs new file mode 100644 index 000000000..2e77ff344 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs @@ -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 FIRST_CONSONANTS = new Dictionary(){ + {"ㄱ", "g"}, + {"ㄲ", "gg"}, + {"ㄴ", "n"}, + {"ㄷ", "d"}, + {"ㄸ", "dd"}, + {"ㄹ", "r"}, + {"ㅁ", "m"}, + {"ㅂ", "b"}, + {"ㅃ", "bb"}, + {"ㅅ", "s"}, + {"ㅆ", "ss"}, + {"ㅇ", ""}, + {"ㅈ", "j"}, + {"ㅉ", "jj"}, + {"ㅊ", "ch"}, + {"ㅋ", "k"}, + {"ㅌ", "t"}, + {"ㅍ", "p"}, + {"ㅎ", "h"}, + {"null", ""} // 뒤 글자가 없을 때를 대비 + }; + + static readonly Dictionary MIDDLE_VOWELS = new Dictionary(){ + {"ㅏ", 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 LAST_CONSONANTS = new Dictionary(){ + //ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ + {"ㄱ", 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)); + } + } +} \ No newline at end of file