diff --git a/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs new file mode 100644 index 000000000..505d9363b --- /dev/null +++ b/OpenUtau.Core/DiffSinger/DiffSingerKoreanPhonemizer.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Melanchall.DryWetMidi.Interaction; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using OpenUtau.Core.Util; +using Serilog; +using static OpenUtau.Api.Phonemizer; + +// TODO 이중모음 늘어지는 문제 해결, 이중모음 분리 + 이중모음 미분리 케이스 모두 구현 + +namespace OpenUtau.Core.DiffSinger{ + [Phonemizer("DiffSinger Korean Phonemizer", "DIFFS KO", language: "KO")] + public class DiffSingerKoreanPhonemizer : DiffSingerBasePhonemizer{ + protected Hanguel hangeul = new Hanguel(); + 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; + } + } + + protected Note variateThisNote(Note? prevNeighbour, Note[] notes, Note? nextNeighbour){ + Note noteToReturn = notes[0]; + noteToReturn.lyric = hangeul.merge(hangeul.variate(prevNeighbour, notes[0], nextNeighbour)); + return noteToReturn; + } + + 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 }; + } + + + 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){ + var wordPhonemes = new List{ + new phonemesPerNote(-1, notes[0].tone) + }; + var dsPhonemes = GetDsPhonemes(notes[0]); + var isVowel = dsPhonemes.Select(s => g2p.IsVowel(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) { + var note = nonExtensionNotes[noteIndex]; + wordPhonemes.Add(new phonemesPerNote(note.position, note.tone)); + noteIndex++; + } + wordPhonemes[^1].Phonemes.Add(dsPhonemes[i]); + } + return wordPhonemes; + } + + 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 = 500f;//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? prev = null; + String? next = phrase[1][0].lyric; + int i = 0; + foreach (var character in phrase) { + next = null; + if (i != phrase.Length - 1){ + next = phrase[i + 1][0].lyric; + } + + Debug.Print("prev: " + prev + "curr: " + character[0].lyric + "next: " + next); + String? prevTemp = character[0].lyric; + character[0].lyric = hangeul.variate(prev, character[0].lyric, next); + Debug.Print(character[0].lyric); + prev = prevTemp; + + var wordPhonemes = ProcessWord(character); + 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)); + //Stretch the duration of the rest phonemes + foreach (var pair in phAlignPoints.Zip(phAlignPoints.Skip(1), (a, b) => Tuple.Create(a, b))) { + 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)); + } + + //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; + } + } + } + +} \ No newline at end of file diff --git a/OpenUtau.Core/Util/Hangeul.cs b/OpenUtau.Core/Util/Hangeul.cs index 4e075705c..1aaa66576 100644 --- a/OpenUtau.Core/Util/Hangeul.cs +++ b/OpenUtau.Core/Util/Hangeul.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Diagnostics; using static OpenUtau.Api.Phonemizer; namespace OpenUtau.Core.Util @@ -169,6 +170,42 @@ public Hashtable separate(string character) { return separatedHangeul; } + /// + /// merges separated hangeul into complete hangeul. (Example: {[0]: "ㄱ", [1]: "ㅏ", [2]: " "} => "가"}) + /// 자모로 쪼개진 한글을 합쳐진 한글로 반환합니다. + /// + /// separated Hangeul. + /// Returns complete Hangeul Character. + public string merge(Hashtable separatedHangeul){ + + int hangeulIndex; // unicode index of hangeul - unicode index of '가' (ex) '냥' + + 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 = 'ㅇ'; + } + + + Hashtable mergedHangeul; // (ex) 냥 + + 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) 메소드를 사용해야 합니다. @@ -735,5 +772,285 @@ public Hashtable variate(Note? prevNeighbour, Note note, Note? nextNeighbour) { } + /// + /// (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 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) { + // 0번가사 없는 걸로 간주함 null냥냥 + lyrics[0] = null; + } + } + if ((lyrics[1] != null) && (lyrics[1].StartsWith('!'))) { + /// 중간노트 ! 기호로 시작함 ex) 냥[!냥]냥 + /// 음운변동 미적용 + lyrics[1] = lyrics[1].TrimStart('!'); + if (lyrics[0] != null) { + // 0번가사 없는 걸로 간주함 null[!냥]냥 + lyrics[0] = null; + } + if (lyrics[2] != null) { + // 2번가사도 없는 걸로 간주함 null[!냥]null + lyrics[2] = null; + } + } + if ((lyrics[2] != null) && (lyrics[2].StartsWith('!'))) { + /// 뒤노트 ! 기호로 시작함 ex) 냥냥[!냥] + if (lyrics[2] != null) { + // 2번가사 없는 걸로 간주함 냥냥b + lyrics[2] = null; + } + } + + 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]}); + } + } } } \ No newline at end of file diff --git a/OpenUtau.Core/BaseKoreanPhonemizer.cs b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs similarity index 99% rename from OpenUtau.Core/BaseKoreanPhonemizer.cs rename to OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs index 9392770c8..3623aeb6e 100644 --- a/OpenUtau.Core/BaseKoreanPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/BaseKoreanPhonemizer.cs @@ -5,7 +5,7 @@ using OpenUtau.Core.Ustx; using OpenUtau.Core.Util; -namespace OpenUtau.Core { +namespace OpenUtau.Plugin.Builtin { public abstract class BaseKoreanPhonemizer : Phonemizer { // Can process Phoneme variation. // Can find Alias in oto, including Voice color etc. diff --git a/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs index 7651b5ef1..e8a74c68a 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCVPhonemizer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using OpenUtau.Api; -using OpenUtau.Core; using OpenUtau.Core.Ustx; using OpenUtau.Core.Util; @@ -12,7 +11,7 @@ namespace OpenUtau.Plugin.Builtin { /// Phonemizer for 'KOR CV' /// [Phonemizer("Korean CV Phonemizer", "KO CV", "EX3", language: "KO")] - public class KoreanCVPhonemizer : Core.BaseKoreanPhonemizer { + public class KoreanCVPhonemizer : BaseKoreanPhonemizer { // 1. Load Singer and Settings private KoreanCVIniSetting koreanCVIniSetting; // Setting object