diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml new file mode 100644 index 000000000..de3583f3d --- /dev/null +++ b/.github/workflows/pr-test.yml @@ -0,0 +1,26 @@ +on: + pull_request: + branches: [ master ] + +jobs: + pr-test: + runs-on: ${{ matrix.os.runs-on }} + + strategy: + matrix: + os: + - runs-on: windows-latest + arch: win-x64 + - runs-on: macos-latest + arch: osx-x64 + - runs-on: ubuntu-latest + arch: linux-x64 + + steps: + - uses: actions/checkout@v1 + + - name: restore + run: dotnet restore OpenUtau -r ${{ matrix.os.arch }} + + - name: test + run: dotnet test OpenUtau.Test diff --git a/OpenUtau.Core/Api/G2pDictionary.cs b/OpenUtau.Core/Api/G2pDictionary.cs index e0953409b..b4a5464e2 100644 --- a/OpenUtau.Core/Api/G2pDictionary.cs +++ b/OpenUtau.Core/Api/G2pDictionary.cs @@ -16,10 +16,12 @@ class TrieNode { TrieNode root; Dictionary phonemeSymbols; // (phoneme, isVowel) + HashSet glideSymbols; - G2pDictionary(TrieNode root, Dictionary phonemeSymbols) { + G2pDictionary(TrieNode root, Dictionary phonemeSymbols, HashSet glideSymbols) { this.root = root; this.phonemeSymbols = phonemeSymbols; + this.glideSymbols = glideSymbols; } public bool IsValidSymbol(string symbol) { @@ -30,6 +32,10 @@ public bool IsVowel(string symbol) { return phonemeSymbols.TryGetValue(symbol, out var isVowel) && isVowel; } + public bool IsGlide(string symbol) { + return glideSymbols.Contains(symbol); + } + public string[] Query(string grapheme) { return QueryTrie(root, grapheme, 0); } @@ -56,10 +62,12 @@ string[] QueryTrie(TrieNode node, string word, int index) { public class Builder { TrieNode root; Dictionary phonemeSymbols; // (phoneme, isVowel) + HashSet glideSymbols; internal Builder() { root = new TrieNode(); phonemeSymbols = new Dictionary(); + glideSymbols = new HashSet(); } /// @@ -67,12 +75,22 @@ internal Builder() { /// public Builder AddSymbol(string symbol, string type) { phonemeSymbols[symbol] = type == "vowel"; + if(type == "semivowel" || type == "liquid") { + glideSymbols.Add(symbol); + } return this; } public Builder AddSymbol(string symbol, bool isVowel) { phonemeSymbols[symbol] = isVowel; return this; } + public Builder AddSymbol(string symbol, bool isVowel, bool isGlide) { + phonemeSymbols[symbol] = isVowel; + if (isGlide && !isVowel) { + glideSymbols.Add(symbol); + } + return this; + } /// /// Must finish adding symbols before adding entries, otherwise symbols get ignored. @@ -123,7 +141,7 @@ public Builder Load(TextReader textReader) { } public G2pDictionary Build() { - return new G2pDictionary(root, phonemeSymbols); + return new G2pDictionary(root, phonemeSymbols, glideSymbols); } } diff --git a/OpenUtau.Core/Api/G2pFallbacks.cs b/OpenUtau.Core/Api/G2pFallbacks.cs index bd82ebfd8..92b755880 100644 --- a/OpenUtau.Core/Api/G2pFallbacks.cs +++ b/OpenUtau.Core/Api/G2pFallbacks.cs @@ -25,6 +25,15 @@ public bool IsVowel(string symbol) { return false; } + public bool IsGlide(string symbol) { + foreach (var dict in dictionaries) { + if (dict.IsValidSymbol(symbol)) { + return dict.IsGlide(symbol); + } + } + return false; + } + public string[] Query(string grapheme) { foreach (var dict in dictionaries) { var result = dict.Query(grapheme); diff --git a/OpenUtau.Core/Api/G2pPack.cs b/OpenUtau.Core/Api/G2pPack.cs index bb2c92873..1a2c92aa3 100644 --- a/OpenUtau.Core/Api/G2pPack.cs +++ b/OpenUtau.Core/Api/G2pPack.cs @@ -59,6 +59,10 @@ public bool IsVowel(string symbol) { return Dict.IsVowel(symbol); } + public bool IsGlide(string symbol) { + return Dict.IsGlide(symbol); + } + public string[] Query(string grapheme) { if (grapheme.Length == 0 || kAllPunct.IsMatch(grapheme)) { return null; diff --git a/OpenUtau.Core/Api/G2pRemapper.cs b/OpenUtau.Core/Api/G2pRemapper.cs index 653161756..506e38e4f 100644 --- a/OpenUtau.Core/Api/G2pRemapper.cs +++ b/OpenUtau.Core/Api/G2pRemapper.cs @@ -5,14 +5,17 @@ namespace OpenUtau.Api { public class G2pRemapper : IG2p { private IG2p mapped; private Dictionary phonemeSymbols; // (phoneme, isVowel) + private HashSet glideSymbols; private Dictionary replacements; public G2pRemapper(IG2p mapped, Dictionary phonemeSymbols, - Dictionary replacements) { + Dictionary replacements, + HashSet glideSymbols = null) { this.mapped = mapped; this.phonemeSymbols = phonemeSymbols; this.replacements = replacements; + this.glideSymbols = glideSymbols ?? new HashSet(); } public bool IsValidSymbol(string symbol) { @@ -23,6 +26,10 @@ public bool IsVowel(string symbol) { return phonemeSymbols.TryGetValue(symbol, out var isVowel) && isVowel; } + public bool IsGlide(string symbol) { + return glideSymbols.Contains(symbol); + } + public string[] Query(string grapheme) { var phonemes = mapped.Query(grapheme); if (phonemes == null) { diff --git a/OpenUtau.Core/Api/IG2p.cs b/OpenUtau.Core/Api/IG2p.cs index 861ead3a8..afe45d34d 100644 --- a/OpenUtau.Core/Api/IG2p.cs +++ b/OpenUtau.Core/Api/IG2p.cs @@ -3,6 +3,11 @@ public interface IG2p { bool IsValidSymbol(string symbol); bool IsVowel(string symbol); + /// + /// Returns true if the symbol is a semivowel or liquid phoneme, like y, w, l, r in English. + /// + bool IsGlide(string symbol); + /// /// Produces a list of phonemes from grapheme. /// diff --git a/OpenUtau.Core/Classic/ClassicRenderer.cs b/OpenUtau.Core/Classic/ClassicRenderer.cs index 2f1d19fda..56e2ec7e8 100644 --- a/OpenUtau.Core/Classic/ClassicRenderer.cs +++ b/OpenUtau.Core/Classic/ClassicRenderer.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using NAudio.Wave; diff --git a/OpenUtau.Core/Classic/ClassicSingerLoader.cs b/OpenUtau.Core/Classic/ClassicSingerLoader.cs index fd79a64cd..13848b5da 100644 --- a/OpenUtau.Core/Classic/ClassicSingerLoader.cs +++ b/OpenUtau.Core/Classic/ClassicSingerLoader.cs @@ -17,11 +17,7 @@ static USinger AdjustSingerType(Voicebank v) { } public static IEnumerable FindAllSingers() { List singers = new List(); - foreach (var path in new string[] { - PathManager.Inst.SingersPathOld, - PathManager.Inst.SingersPath, - PathManager.Inst.AdditionalSingersPath, - }) { + foreach (var path in PathManager.Inst.SingersPaths) { var loader = new VoicebankLoader(path); singers.AddRange(loader.SearchAll() .Select(AdjustSingerType)); diff --git a/OpenUtau.Core/Classic/ExeWavtool.cs b/OpenUtau.Core/Classic/ExeWavtool.cs index f852e3138..08f76c55e 100644 --- a/OpenUtau.Core/Classic/ExeWavtool.cs +++ b/OpenUtau.Core/Classic/ExeWavtool.cs @@ -26,6 +26,15 @@ public float[] Concatenate(List resamplerItems, string tempPath, if (cancellation.IsCancellationRequested) { return null; } + //The builtin worldline resampler can't be called from bat script, + //so we need to call it directly from C# + foreach(var item in resamplerItems){ + if(!(item.resampler is ExeResampler) && !cancellation.IsCancellationRequested && !File.Exists(item.outputFile)){ + lock (Renderers.GetCacheLock(item.outputFile)) { + item.resampler.DoResamplerReturnsFile(item, Log.Logger); + } + } + } PrepareHelper(); string batPath = Path.Combine(PathManager.Inst.CachePath, "temp.bat"); lock (tempBatLock) { @@ -93,7 +102,7 @@ void WriteSetUp(StreamWriter writer, List resamplerItems, string void WriteItem(StreamWriter writer, ResamplerItem item, int index, int total) { writer.WriteLine($"@set resamp={item.resampler.FilePath}"); - writer.WriteLine($"@set params={item.volume} {item.modulation} !{item.tempo.ToString("G999")} {Base64.Base64EncodeInt12(item.pitches)}"); + writer.WriteLine($"@set params={item.volume} {item.modulation} !{item.tempo:G999} {Base64.Base64EncodeInt12(item.pitches)}"); writer.WriteLine($"@set flag=\"{item.GetFlagsString()}\""); writer.WriteLine($"@set env={GetEnvelope(item)}"); writer.WriteLine($"@set stp={item.skipOver}"); @@ -101,7 +110,7 @@ void WriteItem(StreamWriter writer, ResamplerItem item, int index, int total) { string relOutputFile = Path.GetRelativePath(PathManager.Inst.CachePath, item.outputFile); writer.WriteLine($"@set temp=\"%cachedir%\\{relOutputFile}\""); string toneName = MusicMath.GetToneName(item.tone); - string dur = $"{item.phone.duration.ToString("G999")}@{item.phone.adjustedTempo.ToString("G999")}{(item.durCorrection >= 0 ? "+" : "")}{item.durCorrection}"; + string dur = $"{item.phone.duration:G999}@{item.phone.adjustedTempo:G999}{(item.durCorrection >= 0 ? "+" : "")}{item.durCorrection}"; string relInputTemp = Path.GetRelativePath(PathManager.Inst.CachePath, item.inputTemp); writer.WriteLine($"@echo {MakeProgressBar(index + 1, total)}"); writer.WriteLine($"@call %helper% \"%oto%\\{relInputTemp}\" {toneName} {dur} {item.preutter} {item.offset} {item.durRequired} {item.consonant} {item.cutoff} {index}"); diff --git a/OpenUtau.Core/Classic/ResamplerItem.cs b/OpenUtau.Core/Classic/ResamplerItem.cs index e6c597221..9cfba16e8 100644 --- a/OpenUtau.Core/Classic/ResamplerItem.cs +++ b/OpenUtau.Core/Classic/ResamplerItem.cs @@ -2,12 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using System.Text; using K4os.Hash.xxHash; using NAudio.Wave; using OpenUtau.Core; using OpenUtau.Core.Render; using OpenUtau.Core.Ustx; +using static NetMQ.NetMQSelector; using static OpenUtau.Api.Phonemizer; namespace OpenUtau.Classic { @@ -80,7 +82,7 @@ public ResamplerItem(RenderPhrase phrase, RenderPhone phone) { var pitchIntervalMs = MusicMath.TempoTickToMs(tempo, 5); var pitchSampleStartMs = phone.positionMs - pitchLeadingMs; - for (int i=0; i EnvelopeMsToSamples() { + int skipOverSamples = (int)(skipOver * 44100 / 1000); + var envelope = phone.envelope.ToList(); + double shift = -envelope[0].X; + for (int i = 0; i < envelope.Count; ++i) { + var point = envelope[i]; + point.X = (float)((point.X + shift) * 44100 / 1000) + skipOverSamples; + point.Y /= 100; + envelope[i] = point; + } + return envelope; + } + + public void ApplyEnvelope(float[] samples) { + var envelope = EnvelopeMsToSamples(); + int nextPoint = 0; + for (int i = 0; i < samples.Length; ++i) { + while (nextPoint < envelope.Count && i > envelope[nextPoint].X) { + nextPoint++; + } + float gain; + if (nextPoint == 0) { + gain = envelope.First().Y; + } else if (nextPoint >= envelope.Count) { + gain = envelope.Last().Y; + } else { + var p0 = envelope[nextPoint - 1]; + var p1 = envelope[nextPoint]; + if (p0.X >= p1.X) { + gain = p0.Y; + } else { + gain = p0.Y + (p1.Y - p0.Y) * (i - p0.X) / (p1.X - p0.X); + } + } + samples[i] *= gain; + } + } } } diff --git a/OpenUtau.Core/Classic/SharpWavtool.cs b/OpenUtau.Core/Classic/SharpWavtool.cs index 8cd477265..84ac074f4 100644 --- a/OpenUtau.Core/Classic/SharpWavtool.cs +++ b/OpenUtau.Core/Classic/SharpWavtool.cs @@ -56,23 +56,23 @@ public float[] Concatenate(List resamplerItems, string tempPath, segment.posMs = item.phone.positionMs - item.phone.leadingMs - (phrase.positionMs - phrase.leadingMs); segment.posSamples = (int)Math.Round(segment.posMs * 44100 / 1000); segment.skipSamples = (int)Math.Round(item.skipOver * 44100 / 1000); - segment.envelope = EnvelopeMsToSamples(item.phone.envelope, segment.skipSamples); - - if (!phaseComp) { - continue; + segment.envelope = item.EnvelopeMsToSamples(); + + if (phaseComp) { + var headWindow = GetHeadWindow(segment.samples, segment.envelope, out segment.headWindowStart); + segment.headWindowF0 = GetF0AtSample(phrase, + segment.posSamples - segment.skipSamples + segment.headWindowStart + headWindow.Length / 2); + segment.headPhase = CalcPhase(headWindow, + segment.posSamples - segment.skipSamples + segment.headWindowStart, 44100, segment.headWindowF0); + + var tailWindow = GetTailWindow(segment.samples, segment.envelope, out segment.tailWindowStart); + segment.tailWindowF0 = GetF0AtSample(phrase, + segment.posSamples - segment.skipSamples + segment.tailWindowStart + tailWindow.Length / 2); + segment.tailPhase = CalcPhase(tailWindow, + segment.posSamples - segment.skipSamples + segment.tailWindowStart, 44100, segment.tailWindowF0); } - var headWindow = GetHeadWindow(segment.samples, segment.envelope, out segment.headWindowStart); - segment.headWindowF0 = GetF0AtSample(phrase, - segment.posSamples - segment.skipSamples + segment.headWindowStart + headWindow.Length / 2); - segment.headPhase = CalcPhase(headWindow, - segment.posSamples - segment.skipSamples + segment.headWindowStart, 44100, segment.headWindowF0); - - var tailWindow = GetTailWindow(segment.samples, segment.envelope, out segment.tailWindowStart); - segment.tailWindowF0 = GetF0AtSample(phrase, - segment.posSamples - segment.skipSamples + segment.tailWindowStart + tailWindow.Length / 2); - segment.tailPhase = CalcPhase(tailWindow, - segment.posSamples - segment.skipSamples + segment.tailWindowStart, 44100, segment.tailWindowF0); + item.ApplyEnvelope(segment.samples); } if (phaseComp) { @@ -100,7 +100,6 @@ public float[] Concatenate(List resamplerItems, string tempPath, var phraseSamples = new float[0]; foreach (var segment in segments) { Array.Resize(ref phraseSamples, segment.posSamples + segment.correction + segment.samples.Length - segment.skipSamples); - ApplyEnvelope(segment.samples, segment.envelope); for (int i = Math.Max(0, -segment.skipSamples); i < segment.samples.Length - segment.skipSamples; i++) { phraseSamples[segment.posSamples + segment.correction + i] += segment.samples[segment.skipSamples + i]; } @@ -108,42 +107,6 @@ public float[] Concatenate(List resamplerItems, string tempPath, return phraseSamples; } - private static void ApplyEnvelope(float[] data, IList envelope) { - int nextPoint = 0; - for (int i = 0; i < data.Length; ++i) { - while (nextPoint < envelope.Count && i > envelope[nextPoint].X) { - nextPoint++; - } - float gain; - if (nextPoint == 0) { - gain = envelope.First().Y; - } else if (nextPoint >= envelope.Count) { - gain = envelope.Last().Y; - } else { - var p0 = envelope[nextPoint - 1]; - var p1 = envelope[nextPoint]; - if (p0.X >= p1.X) { - gain = p0.Y; - } else { - gain = p0.Y + (p1.Y - p0.Y) * (i - p0.X) / (p1.X - p0.X); - } - } - data[i] *= gain; - } - } - - private static IList EnvelopeMsToSamples(IList envelope, int skipOverSamples) { - envelope = new List(envelope); - double shift = -envelope[0].X; - for (var i = 0; i < envelope.Count; i++) { - var point = envelope[i]; - point.X = (float)((point.X + shift) * 44100 / 1000) + skipOverSamples; - point.Y /= 100; - envelope[i] = point; - } - return envelope; - } - private float[] GetHeadWindow(float[] samples, IList envelope, out int windowStart) { var windowCenter = (envelope[0] + envelope[1]) * 0.5f; windowStart = Math.Max((int)windowCenter.X - 440, 0); diff --git a/OpenUtau.Core/Classic/VoicebankLoader.cs b/OpenUtau.Core/Classic/VoicebankLoader.cs index 20eaa7c04..d518d6a67 100644 --- a/OpenUtau.Core/Classic/VoicebankLoader.cs +++ b/OpenUtau.Core/Classic/VoicebankLoader.cs @@ -6,6 +6,7 @@ using System.Text; using OpenUtau.Core; using OpenUtau.Core.Ustx; +using OpenUtau.Core.Util; using Serilog; namespace OpenUtau.Classic { @@ -45,7 +46,15 @@ public IEnumerable SearchAll() { if (!Directory.Exists(basePath)) { return result; } - result.AddRange(Directory.EnumerateFiles(basePath, kCharTxt, SearchOption.AllDirectories) + IEnumerable files; + if (Preferences.Default.LoadDeepFolderSinger) { + files = Directory.EnumerateFiles(basePath, kCharTxt, SearchOption.AllDirectories); + } else { + // TopDirectoryOnly + files = Directory.GetDirectories(basePath) + .SelectMany(path => Directory.EnumerateFiles(path, kCharTxt)); + } + result.AddRange(files .Select(filePath => { try { var voicebank = new Voicebank(); @@ -341,6 +350,15 @@ public static OtoSet ParseOtoSet(Stream stream, string filePath, Encoding encodi }; while (!reader.EndOfStream) { var line = reader.ReadLine().Trim(); + if (line.StartsWith("#Charaset:")) { + try { + var charaset = Encoding.GetEncoding(line.Replace("#Charaset:", "")); + if (encoding != charaset) { + stream.Position = 0; + return ParseOtoSet(stream, filePath, charaset); + } + } catch { } + } trace.line = line; try { Oto oto = ParseOto(line, trace); diff --git a/OpenUtau.Core/Classic/WorldlineRenderer.cs b/OpenUtau.Core/Classic/WorldlineRenderer.cs index 9f7f5623e..4b6a17862 100644 --- a/OpenUtau.Core/Classic/WorldlineRenderer.cs +++ b/OpenUtau.Core/Classic/WorldlineRenderer.cs @@ -5,11 +5,13 @@ using System.Threading; using System.Threading.Tasks; using NAudio.Wave; +using NumSharp; using OpenUtau.Core; using OpenUtau.Core.Format; using OpenUtau.Core.Render; using OpenUtau.Core.SignalChain; using OpenUtau.Core.Ustx; +using static NetMQ.NetMQSelector; namespace OpenUtau.Classic { public class WorldlineRenderer : IRenderer { @@ -28,6 +30,7 @@ public class WorldlineRenderer : IRenderer { Ustx.BREC, Ustx.TENC, Ustx.VOIC, + Ustx.DIR, }; public USingerType SingerType => USingerType.Classic; @@ -83,6 +86,7 @@ public Task Render(RenderPhrase phrase, Progress progress, int tra var voicing = SampleCurve(phrase, phrase.voicing, 1.0, frames, x => 0.01 * x); phraseSynth.SetCurves(f0, gender, tension, breathiness, voicing); result.samples = phraseSynth.Synth(); + AddDirects(phrase, resamplerItems, result); var source = new WaveSource(0, 0, 0, 1); source.SetSamples(result.samples); WaveFileWriter.CreateWaveFile16(wavPath, new ExportAdapter(source).ToMono(1, 0)); @@ -114,6 +118,30 @@ double[] SampleCurve(RenderPhrase phrase, float[] curve, double defaultValue, in return result; } + private static void AddDirects(RenderPhrase phrase, List resamplerItems, RenderResult result) { + foreach (var item in resamplerItems) { + if (!item.phone.direct) { + continue; + } + double posMs = item.phone.positionMs - item.phone.leadingMs - (phrase.positionMs - phrase.leadingMs); + int startPhraseIndex = (int)(posMs / 1000 * 44100); + using (var waveStream = Wave.OpenFile(item.phone.oto.File)) { + if (waveStream == null) { + continue; + } + float[] samples = Wave.GetSamples(waveStream!.ToSampleProvider().ToMono(1, 0)); + int offset = (int)(item.phone.oto.Offset / 1000 * 44100); + int cutoff = (int)(item.phone.oto.Cutoff / 1000 * 44100); + int length = cutoff >= 0 ? (samples.Length - offset - cutoff) : -cutoff; + samples = samples.Skip(offset).Take(length).ToArray(); + item.ApplyEnvelope(samples); + for (int i = 0; i < Math.Min(samples.Length, result.samples.Length - startPhraseIndex); ++i) { + result.samples[startPhraseIndex + i] = samples[i]; + } + } + } + } + public RenderPitchResult LoadRenderedPitch(RenderPhrase phrase) { return null; } diff --git a/OpenUtau.Core/Commands/Notifications.cs b/OpenUtau.Core/Commands/Notifications.cs index dbdd77b09..4ecea51e3 100644 --- a/OpenUtau.Core/Commands/Notifications.cs +++ b/OpenUtau.Core/Commands/Notifications.cs @@ -211,9 +211,9 @@ public PartRenderedNotification(UVoicePart part) { } public class GotoOtoNotification : UNotification { - public readonly USinger singer; - public readonly UOto oto; - public GotoOtoNotification(USinger singer, UOto oto) { + public readonly USinger? singer; + public readonly UOto? oto; + public GotoOtoNotification(USinger? singer, UOto? oto) { this.singer = singer; this.oto = oto; } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerItalianPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerItalianPhonemizer.cs new file mode 100644 index 000000000..9c44a143e --- /dev/null +++ b/OpenUtau.Core/DiffSinger/DiffSingerItalianPhonemizer.cs @@ -0,0 +1,18 @@ +using OpenUtau.Api; +using OpenUtau.Core.G2p; + +namespace OpenUtau.Core.DiffSinger { + [Phonemizer("DiffSinger Italian Phonemizer", "DIFFS IT", language: "IT")] + public class DiffSingerItalianPhonemizer : DiffSingerG2pPhonemizer { + protected override string GetDictionaryName() => "dsdict-it.yaml"; + protected override IG2p LoadBaseG2p() => new ItalianG2p(); + protected override string[] GetBaseG2pVowels() => new string[] { + "a", "a1", "e", "e1", "EE", "i", "i1", "o", "o1", "OO", "u", "u1" + }; + + protected override string[] GetBaseG2pConsonants() => new string[] { + "b", "d", "dz", "dZZ", "f", "g", "JJ", "k", "l", "LL", "m", "n", + "nf", "ng", "p", "r", "rr", "s", "SS", "t", "ts", "tSS", "v", "w", "y", "z" + }; + } +} diff --git a/OpenUtau.Core/DiffSinger/DiffSingerJyutpingPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerJyutpingPhonemizer.cs index b4a4d01c3..909f8f3df 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerJyutpingPhonemizer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerJyutpingPhonemizer.cs @@ -4,7 +4,7 @@ using System.Linq; namespace OpenUtau.Core.DiffSinger { - [Phonemizer("DiffSinger Jyutping Phonemizer", "DIFFS ZH-YUE", language: "ZH")] + [Phonemizer("DiffSinger Jyutping Phonemizer", "DIFFS ZH-YUE", language: "ZH-YUE")] public class DiffSingerJyutpingPhonemizer : DiffSingerBasePhonemizer { protected override string[] Romanize(IEnumerable lyrics) { return ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, true).Split(" "); 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/DiffSinger/DiffSingerPitch.cs b/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs index 1462a3f91..e5c8326d7 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs @@ -14,7 +14,7 @@ namespace OpenUtau.Core.DiffSinger { - public class DsPitch + public class DsPitch : IDisposable { string rootPath; DsConfig dsConfig; @@ -73,6 +73,7 @@ void SetRange(T[] list, T value, int startIndex, int endIndex){ list[i] = value; } } + public RenderPitchResult Process(RenderPhrase phrase){ var startMs = Math.Min(phrase.notes[0].positionMs, phrase.phones[0].positionMs) - headMs; var endMs = phrase.notes[^1].endMs + tailMs; @@ -280,5 +281,22 @@ public RenderPitchResult Process(RenderPhrase phrase){ }; } } + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + linguisticModel?.Dispose(); + pitchModel?.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs b/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs index 2015cdfe1..da8edf5c1 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs @@ -101,10 +101,12 @@ public Task Render(RenderPhrase phrase, Progress progress, int tra } } if (result.samples == null) { - result.samples = InvokeDiffsinger(phrase, depth, speedup); - var source = new WaveSource(0, 0, 0, 1); - source.SetSamples(result.samples); - WaveFileWriter.CreateWaveFile16(wavPath, new ExportAdapter(source).ToMono(1, 0)); + result.samples = InvokeDiffsinger(phrase, depth, speedup, cancellation); + if (result.samples != null) { + var source = new WaveSource(0, 0, 0, 1); + source.SetSamples(result.samples); + WaveFileWriter.CreateWaveFile16(wavPath, new ExportAdapter(source).ToMono(1, 0)); + } } if (result.samples != null) { Renderers.ApplyDynamics(phrase, result); @@ -120,7 +122,7 @@ public Task Render(RenderPhrase phrase, Progress progress, int tra leadingMs、positionMs、estimatedLengthMs: timeaxis layout in Ms, double */ - float[] InvokeDiffsinger(RenderPhrase phrase, int depth, int speedup) { + float[] InvokeDiffsinger(RenderPhrase phrase, int depth, int speedup, CancellationTokenSource cancellation) { var singer = phrase.singer as DiffSingerSinger; //Check if dsconfig.yaml is correct if(String.IsNullOrEmpty(singer.dsConfig.vocoder) || @@ -130,6 +132,7 @@ float[] InvokeDiffsinger(RenderPhrase phrase, int depth, int speedup) { } var vocoder = singer.getVocoder(); + var acousticModel = singer.getAcousticSession(); var frameMs = vocoder.frameMs(); var frameSec = frameMs / 1000; int headFrames = (int)Math.Round(headMs / frameMs); @@ -218,8 +221,16 @@ float[] InvokeDiffsinger(RenderPhrase phrase, int depth, int speedup) { } //Variance: Energy and Breathiness + if(singer.dsConfig.useBreathinessEmbed || singer.dsConfig.useEnergyEmbed){ - var varianceResult = singer.getVariancePredictor().Process(phrase); + var variancePredictor = singer.getVariancePredictor(); + VarianceResult varianceResult; + lock(variancePredictor){ + if(cancellation.IsCancellationRequested) { + return null; + } + varianceResult = singer.getVariancePredictor().Process(phrase); + } //TODO: let user edit variance curves if(singer.dsConfig.useEnergyEmbed){ var energyCurve = phrase.curves.FirstOrDefault(curve => curve.Item1 == ENE); @@ -246,26 +257,36 @@ float[] InvokeDiffsinger(RenderPhrase phrase, int depth, int speedup) { .Reshape(new int[] { 1, breathiness.Length }))); } } - - var acousticModel = singer.getAcousticSession(); - Onnx.VerifyInputNames(acousticModel, acousticInputs); Tensor mel; - var acousticOutputs = acousticModel.Run(acousticInputs); - mel = acousticOutputs.First().AsTensor().Clone(); - + lock(acousticModel){ + if(cancellation.IsCancellationRequested) { + return null; + } + Onnx.VerifyInputNames(acousticModel, acousticInputs); + var acousticOutputs = acousticModel.Run(acousticInputs); + mel = acousticOutputs.First().AsTensor().Clone(); + } //vocoder //waveform = session.run(['waveform'], {'mel': mel, 'f0': f0})[0] var vocoderInputs = new List(); vocoderInputs.Add(NamedOnnxValue.CreateFromTensor("mel", mel)); vocoderInputs.Add(NamedOnnxValue.CreateFromTensor("f0",f0tensor)); float[] samples; - var vocoderOutputs = vocoder.session.Run(vocoderInputs); - samples = vocoderOutputs.First().AsTensor().ToArray(); + lock(vocoder){ + if(cancellation.IsCancellationRequested) { + return null; + } + var vocoderOutputs = vocoder.session.Run(vocoderInputs); + samples = vocoderOutputs.First().AsTensor().ToArray(); + } return samples; } public RenderPitchResult LoadRenderedPitch(RenderPhrase phrase) { - return (phrase.singer as DiffSingerSinger).getPitchPredictor().Process(phrase); + var pitchPredictor = (phrase.singer as DiffSingerSinger).getPitchPredictor(); + lock(pitchPredictor){ + return pitchPredictor.Process(phrase); + } } public UExpressionDescriptor[] GetSuggestedExpressions(USinger singer, URenderSettings renderSettings) { diff --git a/OpenUtau.Core/DiffSinger/DiffSingerRhythmizerPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerRhythmizerPhonemizer.cs index 784332e8f..fc6133d38 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerRhythmizerPhonemizer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerRhythmizerPhonemizer.cs @@ -220,5 +220,9 @@ public List stretch(IList source, double ratio, double endPos) { result.RemoveAt(result.Count - 1); return result; } + + protected override string[] Romanize(IEnumerable lyrics) { + return BaseChinesePhonemizer.Romanize(lyrics); + } } } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerSinger.cs b/OpenUtau.Core/DiffSinger/DiffSingerSinger.cs index 62379ae16..b3353dd9e 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerSinger.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerSinger.cs @@ -27,6 +27,8 @@ class DiffSingerSinger : USinger { public override byte[] AvatarData => avatarData; public override string Portrait => voicebank.Portrait == null ? null : Path.Combine(Location, voicebank.Portrait); public override float PortraitOpacity => voicebank.PortraitOpacity; + public override int PortraitHeight => voicebank.PortraitHeight; + public override string Sample => voicebank.Sample == null ? null : Path.Combine(Location, voicebank.Sample); public override string DefaultPhonemizer => voicebank.DefaultPhonemizer; public override Encoding TextFileEncoding => voicebank.TextFileEncoding; public override IList Subbanks => subbanks; @@ -168,5 +170,33 @@ public DsVariance getVariancePredictor(){ } return variancePredictor; } + + public override void FreeMemory(){ + Log.Information($"Freeing memory for singer {Id}"); + if(acousticSession != null) { + lock(acousticSession) { + acousticSession?.Dispose(); + } + acousticSession = null; + } + if(vocoder != null) { + lock(vocoder) { + vocoder?.Dispose(); + } + vocoder = null; + } + if(pitchPredictor != null) { + lock(pitchPredictor) { + pitchPredictor?.Dispose(); + } + pitchPredictor = null; + } + if(variancePredictor != null){ + lock(variancePredictor) { + variancePredictor?.Dispose(); + } + variancePredictor = null; + } + } } } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerSpanishPhonemizer.cs b/OpenUtau.Core/DiffSinger/DiffSingerSpanishPhonemizer.cs new file mode 100644 index 000000000..c8ab7aae0 --- /dev/null +++ b/OpenUtau.Core/DiffSinger/DiffSingerSpanishPhonemizer.cs @@ -0,0 +1,20 @@ +using OpenUtau.Api; +using OpenUtau.Core.G2p; + +namespace OpenUtau.Core.DiffSinger +{ + [Phonemizer("DiffSinger Spanish Phonemizer", "DIFFS ES", language: "ES")] + public class DiffSingerSpanishPhonemizer : DiffSingerG2pPhonemizer + { + protected override string GetDictionaryName()=>"dsdict-es.yaml"; + protected override IG2p LoadBaseG2p() => new SpanishG2p(); + protected override string[] GetBaseG2pVowels() => new string[] { + "a", "e", "i", "o", "u" + }; + + protected override string[] GetBaseG2pConsonants() => new string[] { + "b", "B", "ch", "d", "D", "f", "g", "G", "gn", "I", "k", "l", + "ll", "m", "n", "p", "r", "rr", "s", "t", "U", "w", "x", "y", "Y", "z" + }; + } +} diff --git a/OpenUtau.Core/DiffSinger/DiffSingerVariance.cs b/OpenUtau.Core/DiffSinger/DiffSingerVariance.cs index 54015113e..647c9d742 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerVariance.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerVariance.cs @@ -17,7 +17,7 @@ public struct VarianceResult{ public float[] energy; public float[] breathiness; } - public class DsVariance{ + public class DsVariance : IDisposable{ string rootPath; DsConfig dsConfig; List phonemes; @@ -172,5 +172,22 @@ public VarianceResult Process(RenderPhrase phrase){ breathiness = breathiness_pred.ToArray() }; } + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + linguisticModel?.Dispose(); + varianceModel?.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerVocoder.cs b/OpenUtau.Core/DiffSinger/DiffSingerVocoder.cs index c1af1a436..0eccc6d4f 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerVocoder.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerVocoder.cs @@ -3,7 +3,7 @@ using Microsoft.ML.OnnxRuntime; namespace OpenUtau.Core.DiffSinger { - public class DsVocoder { + public class DsVocoder : IDisposable { public string Location; public DsVocoderConfig config; public InferenceSession session; @@ -27,6 +27,23 @@ public DsVocoder(string name) { public float frameMs() { return 1000f * config.hop_size / config.sample_rate; } + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + session?.Dispose(); + } + disposedValue = true; + } + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } [Serializable] diff --git a/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs b/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs new file mode 100644 index 000000000..d2b42bc43 --- /dev/null +++ b/OpenUtau.Core/Enunu/EnunuKoreanPhonemizer.cs @@ -0,0 +1,606 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using K4os.Hash.xxHash; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Core.Enunu { + [Phonemizer("Enunu Korean Phonemizer", "ENUNU KO", "EX3", language:"KO")] + public class EnunuKoreanPhonemizer : EnunuPhonemizer { + readonly string PhonemizerType = "ENUNU KO"; + public string semivowelSep; + private KoreanENUNUSetting koreanENUNUSetting; // Manages Settings + private bool isSeparateSemiVowels; // Nanages n y a or ny a + + /// + /// Default KO ENUNU first consonants table + /// + static readonly List firstDefaultConsonants = new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData[19]{ + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄱ", "g"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄲ", "kk"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄴ", "n"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄷ", "d"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄸ", "tt"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㄹ", "r"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅁ", "m"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅂ", "b"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅃ", "pp"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅅ", "s"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅆ", "ss"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅇ", ""), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅈ", "j"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅉ", "jj"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅊ", "ch"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅋ", "k"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅌ", "t"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅍ", "p"), + new KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData("ㅎ", "h") + }.ToList(); + + /// + /// Default KO ENUNU plain vowels table + /// + static readonly List plainDefaultVowels = new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData[7]{ + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅏ", "a"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅣ", "i"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅜ", "u"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅔ/ㅐ", "e"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅗ", "o"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅓ", "eo"), + new KoreanPhonemizerUtil.JamoDictionary.PlainVowelData("ㅡ", "eu") + }.ToList(); + + /// + /// Default KO ENUNU semivowels table + /// + static readonly List semiDefaultVowels = new KoreanPhonemizerUtil.JamoDictionary.SemivowelData[2]{ + new KoreanPhonemizerUtil.JamoDictionary.SemivowelData("y", "y"), + new KoreanPhonemizerUtil.JamoDictionary.SemivowelData("w", "w") + }.ToList(); + + /// + /// Default KO ENUNU final consonants table + /// + static readonly List finalDefaultConsonants = new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData[7]{ + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄱ", "K"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄴ", "N"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄷ", "T"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㄹ", "L"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅁ", "M"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅂ", "P"), + new KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData("ㅇ", "NG") + }.ToList(); + + /// + /// KO ENUNU phoneme table of first consonants. (key "null" is for Handling empty string) + /// + private Dictionary FirstConsonants = new Dictionary(){ + {"ㄱ", new string[2]{"g", ConsonantType.NORMAL.ToString()}}, + {"ㄲ", new string[2]{"kk", ConsonantType.FORTIS.ToString()}}, + {"ㄴ", new string[2]{"n", ConsonantType.NASAL.ToString()}}, + {"ㄷ", new string[2]{"d", ConsonantType.NORMAL.ToString()}}, + {"ㄸ", new string[2]{"tt", ConsonantType.FORTIS.ToString()}}, + {"ㄹ", new string[2]{"r", ConsonantType.LIQUID.ToString()}}, + {"ㅁ", new string[2]{"m", ConsonantType.NASAL.ToString()}}, + {"ㅂ", new string[2]{"b", ConsonantType.NORMAL.ToString()}}, + {"ㅃ", new string[2]{"pp", ConsonantType.FORTIS.ToString()}}, + {"ㅅ", new string[2]{"s", ConsonantType.NORMAL.ToString()}}, + {"ㅆ", new string[2]{"ss", ConsonantType.FRICATIVE.ToString()}}, + {"ㅇ", new string[2]{"", ConsonantType.NOCONSONANT.ToString()}}, + {"ㅈ", new string[2]{"j", ConsonantType.NORMAL.ToString()}}, + {"ㅉ", new string[2]{"jj", ConsonantType.FORTIS.ToString()}}, + {"ㅊ", new string[2]{"ch", ConsonantType.ASPIRATE.ToString()}}, + {"ㅋ", new string[2]{"k", ConsonantType.ASPIRATE.ToString()}}, + {"ㅌ", new string[2]{"t", ConsonantType.ASPIRATE.ToString()}}, + {"ㅍ", new string[2]{"p", ConsonantType.ASPIRATE.ToString()}}, + {"ㅎ", new string[2]{"h", ConsonantType.H.ToString()}}, + {" ", new string[2]{"", ConsonantType.NOCONSONANT.ToString()}}, + {"null", new string[2]{"", ConsonantType.PHONEME_IS_NULL.ToString()}} // 뒤 글자가 없을 때를 대비 + }; + + /// + /// KO ENUNU phoneme table of middle vowels (key "null" is for Handling empty string) + /// + private Dictionary MiddleVowels = 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"}}, + {" ", new string[3]{"", "", ""}}, + {"null", new string[3]{"", "", ""}} // 뒤 글자가 없을 때를 대비 + }; + /// + /// KO ENUNU phoneme table of last consonants. (key "null" is for Handling empty string) + /// + private Dictionary LastConsonants = new Dictionary(){ + //ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈㅊㅋㅌㅍㅎ + {"ㄱ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}}, + {"ㄲ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}}, + {"ㄳ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}}, + {"ㄴ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}}, + {"ㄵ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}}, + {"ㄶ", new string[3]{" N", "2", BatchimType.NASAL_END.ToString()}}, + {"ㄷ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㄹ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}}, + {"ㄺ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}}, + {"ㄻ", new string[3]{" M", "1", BatchimType.NASAL_END.ToString()}}, + {"ㄼ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}}, + {"ㄽ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}}, + {"ㄾ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}}, + {"ㄿ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅀ", new string[3]{" L", "4", BatchimType.LIQUID_END.ToString()}}, + {"ㅁ", new string[3]{" M", "1", BatchimType.NASAL_END.ToString()}}, + {"ㅂ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅄ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅅ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅆ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅇ", new string[3]{" NG", "3", BatchimType.NG_END.ToString()}}, + {"ㅈ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅊ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅋ", new string[3]{" K", "", BatchimType.NORMAL_END.ToString()}}, + {"ㅌ", new string[3]{" T", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅍ", new string[3]{" P", "1", BatchimType.NORMAL_END.ToString()}}, + {"ㅎ", new string[3]{" T", "1", BatchimType.H_END.ToString()}}, + {" ", new string[3]{"", "", BatchimType.NO_END.ToString()}}, + {"null", new string[3]{"", "", BatchimType.PHONEME_IS_NULL.ToString()}} // 뒤 글자가 없을 때를 대비 + }; + + struct TimingResult { + public string path_full_timing; + public string path_mono_timing; + } + + struct TimingResponse { + public string error; + public TimingResult result; + } + public override void SetSinger(USinger singer) { + if (singer.SingerType != USingerType.Enunu) {return;} + + this.singer = singer as EnunuSinger; + + koreanENUNUSetting = new KoreanENUNUSetting("jamo_dict.yaml"); + + koreanENUNUSetting.Initialize(singer, "ko-ENUNU.ini", new Hashtable(){ + { + "SETTING", new Hashtable(){ + {"Separate semivowels, like 'n y a'(otherwise 'ny a')", "True"} + } + } + } + ); + + isSeparateSemiVowels = koreanENUNUSetting.isSeparateSemiVowels; + semivowelSep = isSeparateSemiVowels ? " ": ""; + + // Modify Phoneme Tables + // First Consonants + FirstConsonants["ㄱ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄱ")}"; + FirstConsonants["ㄲ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄲ")}"; + FirstConsonants["ㄴ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄴ")}"; + FirstConsonants["ㄷ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄷ")}"; + FirstConsonants["ㄸ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄸ")}"; + FirstConsonants["ㄹ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㄹ")}"; + FirstConsonants["ㅁ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅁ")}"; + FirstConsonants["ㅂ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅂ")}"; + FirstConsonants["ㅃ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅃ")}"; + FirstConsonants["ㅅ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅅ")}"; + FirstConsonants["ㅆ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅆ")}"; + FirstConsonants["ㅇ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅇ")}"; + FirstConsonants["ㅈ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅈ")}"; + FirstConsonants["ㅉ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅉ")}"; + FirstConsonants["ㅊ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅊ")}"; + FirstConsonants["ㅋ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅋ")}"; + FirstConsonants["ㅌ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅌ")}"; + FirstConsonants["ㅍ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅍ")}"; + FirstConsonants["ㅎ"][0] = $"{koreanENUNUSetting.GetFirstConsonantPhoneme("ㅎ")}"; + + + // Vowels + MiddleVowels["ㅑ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + MiddleVowels["ㅒ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + MiddleVowels["ㅕ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + MiddleVowels["ㅖ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + MiddleVowels["ㅘ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅙ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅚ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅛ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + MiddleVowels["ㅝ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅞ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅟ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("w"); + MiddleVowels["ㅠ"][1] = koreanENUNUSetting.GetSemiVowelPhoneme("y"); + + MiddleVowels["ㅏ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}"; + MiddleVowels["ㅐ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅑ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}"; + MiddleVowels["ㅒ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅓ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}"; + MiddleVowels["ㅔ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅕ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}"; + MiddleVowels["ㅖ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅗ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ")}"; + MiddleVowels["ㅘ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ")}"; + MiddleVowels["ㅙ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅚ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅛ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ")}"; + MiddleVowels["ㅜ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ")}"; + MiddleVowels["ㅝ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")}"; + MiddleVowels["ㅞ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ/ㅐ")}"; + MiddleVowels["ㅟ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}"; + MiddleVowels["ㅠ"][2] = $" {koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ")}"; + MiddleVowels["ㅡ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅡ")}"; + MiddleVowels["ㅢ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}"; // ㅢ는 ㅣ로 발음 + MiddleVowels["ㅣ"][2] = $"{koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ")}"; + + // final consonants + LastConsonants["ㄱ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}"; + LastConsonants["ㄲ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}"; + LastConsonants["ㄳ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}"; + LastConsonants["ㄴ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}"; + LastConsonants["ㄵ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}"; + LastConsonants["ㄶ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ")}"; + LastConsonants["ㄷ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㄹ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}"; + LastConsonants["ㄺ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}"; + LastConsonants["ㄻ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ")}"; + LastConsonants["ㄼ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}"; + LastConsonants["ㄽ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}"; + LastConsonants["ㄾ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}"; + LastConsonants["ㄿ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}"; + LastConsonants["ㅀ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ")}"; + LastConsonants["ㅁ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ")}"; + LastConsonants["ㅂ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}"; + LastConsonants["ㅄ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}"; + LastConsonants["ㅅ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㅆ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㅇ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅇ")}"; + LastConsonants["ㅈ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㅊ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㅋ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ")}"; + LastConsonants["ㅌ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + LastConsonants["ㅍ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ")}"; + LastConsonants["ㅎ"][0] = $" {koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ")}"; + + } + private class KoreanENUNUSetting : KoreanPhonemizerUtil.BaseIniManager{ + // uses KO-ENUNU.ini, jamo_dict.yaml + public bool isSeparateSemiVowels; + public string yamlFileName; + private KoreanPhonemizerUtil.JamoDictionary jamoDict; + public KoreanENUNUSetting(string yamlFileName) { + this.yamlFileName = yamlFileName; + } + protected override void IniSetUp(Hashtable iniFile) { + // ko-ENUNU.ini + jamo_dict.yaml + SetOrReadThisValue("SETTING", "Separate semivowels, like 'n y a'(otherwise 'ny a')", false, out var resultValue); // 반자음 떼기 유무 - 기본값 false + isSeparateSemiVowels = resultValue; + + try { + jamoDict = Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(Path.Combine(singer.Location, yamlFileName))); + if (jamoDict == null) { + throw new IOException("yaml file is null"); + } + } + catch (IOException e) { + Log.Error(e, $"Failed to read {Path.Combine(singer.Location, yamlFileName)}"); + + jamoDict = new KoreanPhonemizerUtil.JamoDictionary(firstDefaultConsonants.ToArray(), plainDefaultVowels.ToArray(), semiDefaultVowels.ToArray(), finalDefaultConsonants.ToArray()); + + File.WriteAllText(Path.Combine(singer.Location, yamlFileName), Yaml.DefaultSerializer.Serialize(jamoDict)); + } + + } + + + public string GetFirstConsonantPhoneme(string Phoneme) { + KoreanPhonemizerUtil.JamoDictionary.FirstConsonantData results = jamoDict.firstConsonants.ToList().Find(c => c.grapheme == Phoneme); + string result = results.phoneme; + if (result == null) { + result = firstDefaultConsonants.Find(c => c.grapheme == Phoneme).phoneme; + } + return result.Trim(); + } + + public string GetPlainVowelPhoneme(string Phoneme) { + KoreanPhonemizerUtil.JamoDictionary.PlainVowelData results = jamoDict.plainVowels.ToList().Find(c => c.grapheme == Phoneme); + string result = results.phoneme; + if (result == null) { + result = plainDefaultVowels.Find(c => c.grapheme == Phoneme).phoneme; + } + return result.Trim(); + } + + public string GetSemiVowelPhoneme(string Phoneme) { + KoreanPhonemizerUtil.JamoDictionary.SemivowelData results = jamoDict.semivowels.ToList().Find(c => c.grapheme == Phoneme); + string result = results.phoneme; + if (result == null) { + result = semiDefaultVowels.Find(c => c.grapheme == Phoneme).phoneme; + } + return result.Trim(); + } + + public string GetFinalConsonantPhoneme(string Phoneme) { + KoreanPhonemizerUtil.JamoDictionary.FinalConsonantData results = jamoDict.finalConsonants.ToList().Find(c => c.grapheme == Phoneme); + string result = results.phoneme; + if (result == null) { + result = finalDefaultConsonants.Find(c => c.grapheme == Phoneme).phoneme; + } + return result.Trim(); + } + } + + public enum ConsonantType{ + /// 예사소리 + NORMAL, + /// 거센소리 + ASPIRATE, + /// 된소리 + FORTIS, + /// 마찰음 + FRICATIVE, + /// 비음 + NASAL, + /// 유음 + LIQUID, + /// + H, + /// 자음의 음소값 없음(ㅇ) + NOCONSONANT, + /// 음소 자체가 없음 + PHONEME_IS_NULL + } + + /// + /// Last Consonant's type. + /// + public enum BatchimType{ + /// 예사소리 받침 + NORMAL_END, + /// 비음 받침 + NASAL_END, + /// 유음 받침 + LIQUID_END, + /// ㅇ받침 + NG_END, + /// ㅎ받침 + H_END, + /// 받침이 없음 + NO_END, + /// 음소 자체가 없음 + PHONEME_IS_NULL + } + + Dictionary partResult = new Dictionary(); + + public override void SetUp(Note[][] notes) { + partResult.Clear(); + if (notes.Length == 0 || singer == null || !singer.Found) { + return; + } + double bpm = timeAxis.GetBpmAtTick(notes[0][0].position); + ulong hash = HashNoteGroups(notes, bpm); + var tmpPath = Path.Join(PathManager.Inst.CachePath, $"lab-{hash:x16}"); + var ustPath = tmpPath + ".tmp"; + var enutmpPath = tmpPath + "_enutemp"; + var scorePath = Path.Join(enutmpPath, $"score.lab"); + var timingPath = Path.Join(enutmpPath, $"timing.lab"); + var enunuNotes = NoteGroupsToEnunu(notes); + if (!File.Exists(scorePath) || !File.Exists(timingPath)) { + EnunuUtils.WriteUst(enunuNotes, bpm, singer, ustPath); + var response = EnunuClient.Inst.SendRequest(new string[] { "timing", ustPath }); + if (response.error != null) { + throw new Exception(response.error); + } + } + var noteIndexes = LabelToNoteIndex(scorePath, enunuNotes); + var timing = ParseLabel(timingPath); + timing.Zip(noteIndexes, (phoneme, noteIndex) => Tuple.Create(phoneme, noteIndex)) + .GroupBy(tuple => tuple.Item2) + .ToList() + .ForEach(g => { + if (g.Key >= 0) { + var noteGroup = notes[g.Key]; + partResult[noteGroup] = g.Select(tu => tu.Item1).ToArray(); + } + }); + } + + ulong HashNoteGroups(Note[][] notes, double bpm) { + using (var stream = new MemoryStream()) { + using (var writer = new BinaryWriter(stream)) { + writer.Write(this.PhonemizerType); + writer.Write(this.singer.Location); + writer.Write(bpm); + foreach (var ns in notes) { + foreach (var n in ns) { + writer.Write(n.lyric); + if(n.phoneticHint!= null) { + writer.Write("["+n.phoneticHint+"]"); + } + writer.Write(n.position); + writer.Write(n.duration); + writer.Write(n.tone); + } + } + return XXH64.DigestOf(stream.ToArray()); + } + } + } + + static int[] LabelToNoteIndex(string scorePath, EnunuNote[] enunuNotes) { + var result = new List(); + int lastPos = 0; + int index = 0; + var score = ParseLabel(scorePath); + foreach (var p in score) { + if (p.position != lastPos) { + index++; + lastPos = p.position; + } + result.Add(enunuNotes[index].noteIndex); + } + return result.ToArray(); + } + + static Phoneme[] ParseLabel(string path) { + var phonemes = new List(); + using (var reader = new StreamReader(path, Encoding.UTF8)) { + while (!reader.EndOfStream) { + var line = reader.ReadLine(); + var parts = line.Split(); + if (parts.Length == 3 && + long.TryParse(parts[0], out long pos) && + long.TryParse(parts[1], out long end)) { + phonemes.Add(new Phoneme { + phoneme = parts[2], + position = (int)(pos / 1000L), + }); + } + } + } + return phonemes.ToArray(); + } + + protected override EnunuNote[] NoteGroupsToEnunu(Note[][] notes) { + KoreanPhonemizerUtil.RomanizeNotes(notes, FirstConsonants, MiddleVowels, LastConsonants, semivowelSep); + var result = new List(); + int position = 0; + int index = 0; + + while (index < notes.Length) { + if (position < notes[index][0].position) { + result.Add(new EnunuNote { + lyric = "R", + length = notes[index][0].position - position, + noteNum = 60, + noteIndex = -1, + }); + position = notes[index][0].position; + } else { + var lyric = notes[index][0].lyric; + result.Add(new EnunuNote { + lyric = lyric, + length = notes[index].Sum(n => n.duration), + noteNum = notes[index][0].tone, + noteIndex = index, + }); + position += result.Last().length; + index++; + } + } + return result.ToArray(); + } + + // public void AdjustPos(Phoneme[] phonemes, Note[] prevNote){ + // //TODO + // Phoneme? prevPhone = null; + // Phoneme? nextPhone = null; + // Phoneme currPhone; + + // int length = phonemes.Last().position; + // int prevLength; + // if (prevNote == null){ + // prevLength = length; + // } + // else{ + // prevLength = MsToTick(prevNote.Sum(n => n.duration)); + // } + + // for (int i=0; i < phonemes.Length; i++) { + // currPhone = phonemes[i]; + // if (i < phonemes.Length - 1){ + // nextPhone = phonemes[i+1]; + // } + // else{ + // nextPhone = null; + // } + + // if (i == 0){ + // // TODO 받침 + 자음 오면 받침길이 + 자음길이 / 2의 위치에 자음이 오도록 하기 + // if (isPlainVowel(phonemes[i].phoneme)) { + // phonemes[i].position = 0; + // } + // else if (nextPhone != null && ! isPlainVowel(((Phoneme)nextPhone).phoneme) && ! isSemivowel(((Phoneme)nextPhone).phoneme) && isPlainVowel(((Phoneme)nextPhone).phoneme) && isSemivowel(currPhone.phoneme)) { + // phonemes[i + 1].position = length / 10; + // } + // else if (nextPhone != null && isSemivowel(((Phoneme)nextPhone).phoneme)){ + // if (i + 2 < phonemes.Length){ + // phonemes[i + 2].position = length / 10; + // } + + // } + // } + // prevPhone = currPhone; + // } + // } + + // private bool isPlainVowel(string phoneme){ + // if (phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅏ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅣ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅜ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅔ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅗ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅡ") || phoneme == koreanENUNUSetting.GetPlainVowelPhoneme("ㅓ")){ + // return true; + // } + // return false; + // } + + // private bool isBatchim(string phoneme){ + // if (phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄱ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄴ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄷ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㄹ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅁ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅂ") || phoneme == koreanENUNUSetting.GetFinalConsonantPhoneme("ㅇ")){ + // return true; + // } + // return false; + // } + + // private bool isSemivowel(string phoneme) { + // if (phoneme == koreanENUNUSetting.GetSemiVowelPhoneme("w") || phoneme == koreanENUNUSetting.GetSemiVowelPhoneme("y")){ + // return true; + // } + // return false; + // } + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevs) { + if (partResult.TryGetValue(notes, out var phonemes)) { + var phonemes_ = phonemes.Select(p => { + double posMs = p.position * 0.1; + p.position = MsToTick(posMs) - notes[0].position; + return p; + }).ToArray(); + + //AdjustPos(phonemes_, prevs); + return new Result { + phonemes = phonemes_, + }; + } + return new Result { + phonemes = new Phoneme[] { + new Phoneme { + phoneme = "error", + } + }, + }; + } + + public override void CleanUp() { + partResult.Clear(); + } + + } +} diff --git a/OpenUtau.Core/Format/Formats.cs b/OpenUtau.Core/Format/Formats.cs index 299b241ea..69ddbc990 100644 --- a/OpenUtau.Core/Format/Formats.cs +++ b/OpenUtau.Core/Format/Formats.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using OpenUtau.Classic; using OpenUtau.Core.Ustx; @@ -40,12 +41,16 @@ public static ProjectFormats DetectProjectFormat(string file) { } } - public static void LoadProject(string[] files) { + /// + /// Read project from files to a new UProject object, used by LoadProject and ImportTracks. + /// + /// Names of the files to be loaded + public static UProject? ReadProject(string[] files){ if (files.Length < 1) { - return; + return null; } ProjectFormats format = DetectProjectFormat(files[0]); - UProject project; + UProject? project = null; switch (format) { case ProjectFormats.Ustx: project = Ustx.Load(files[0]); @@ -66,37 +71,51 @@ public static void LoadProject(string[] files) { default: throw new FileFormatException("Unknown file format"); } + return project; + } + + /// + /// Load project from files. + /// + /// Names of the files to be loaded + public static void LoadProject(string[] files) { + UProject project = ReadProject(files); if (project != null) { DocManager.Inst.ExecuteCmd(new LoadProjectNotification(project)); } } - public static void ImportTracks(UProject project, string[] files) { - if (files.Length < 1) { + + /// + /// Read multiple projects for importing tracks + /// + /// Names of the files to be loaded + /// + public static UProject[] ReadProjects(string[] files){ + if (files == null || files.Length < 1) { + return new UProject[0]; + } + return files + .Select(file => ReadProject(new string[] { file })) + .Where(p => p != null) + .Cast() + .ToArray(); + } + + /// + /// Import tracks from files to the current existing editing project. + /// + /// The current existing editing project + /// loaded project objects to be imported + /// If set to true, the tempo of the imported project will be used + /// + public static void ImportTracks(UProject project, UProject[] loadedProjects, bool importTempo = true) { + if (loadedProjects == null || loadedProjects.Length < 1) { return; } int initialTracks = project.tracks.Count; int initialParts = project.parts.Count; - foreach (string file in files) { - ProjectFormats format = DetectProjectFormat(file); - UProject loaded; - switch (format) { - case ProjectFormats.Ustx: - loaded = Ustx.Load(file); - break; - case ProjectFormats.Vsq3: - case ProjectFormats.Vsq4: - loaded = VSQx.Load(file); - break; - case ProjectFormats.Ust: - loaded = Ust.Load(new[] { file }); - break; - case ProjectFormats.Midi: - loaded = MidiWriter.LoadProject(file); - break; - default: - throw new FileFormatException("Unknown file format"); - } + foreach (UProject loaded in loadedProjects) { int trackCount = project.tracks.Count; foreach (var (abbr, descriptor) in loaded.expressions) { if (!project.expressions.ContainsKey(abbr)) { @@ -111,6 +130,9 @@ public static void ImportTracks(UProject project, string[] files) { project.parts.Add(part); part.trackNo += trackCount; } + } + if (importTempo) { + var loaded = loadedProjects[0]; project.timeSignatures.Clear(); project.timeSignatures.AddRange(loaded.timeSignatures); project.tempos.Clear(); @@ -126,5 +148,24 @@ public static void ImportTracks(UProject project, string[] files) { project.ValidateFull(); DocManager.Inst.ExecuteCmd(new LoadProjectNotification(project)); } + + /// + /// Import tracks from files to the current existing editing project. + /// + /// The current existing editing project + /// Names of the files to be imported + /// If set to true, the tempo of the imported project will be used + /// + public static void ImportTracks(UProject project, string[] files, bool importTempo = true) { + if (files == null || files.Length < 1) { + return; + } + UProject[] loadedProjects = files + .Select(file => ReadProject(new string[] { file })) + .Where(p => p != null) + .Cast() + .ToArray(); + ImportTracks(project, loadedProjects, importTempo); + } } } diff --git a/OpenUtau.Core/Format/USTx.cs b/OpenUtau.Core/Format/USTx.cs index aa7e12af3..e186c612c 100644 --- a/OpenUtau.Core/Format/USTx.cs +++ b/OpenUtau.Core/Format/USTx.cs @@ -26,12 +26,13 @@ public class Ustx { public const string LPF = "lpf"; public const string MOD = "mod"; public const string ALT = "alt"; + public const string DIR = "dir"; public const string SHFT = "shft"; public const string SHFC = "shfc"; public const string TENC = "tenc"; public const string VOIC = "voic"; - public static readonly string[] required = { DYN, PITD, CLR, ENG, VEL, VOL, ATK, DEC, }; + public static readonly string[] required = { DYN, PITD, CLR, ENG, VEL, VOL, ATK, DEC }; public static void AddDefaultExpressions(UProject project) { project.RegisterExpression(new UExpressionDescriptor("dynamics (curve)", DYN, -240, 120, 0) { type = UExpressionType.Curve }); @@ -49,6 +50,7 @@ public static void AddDefaultExpressions(UProject project) { project.RegisterExpression(new UExpressionDescriptor("lowpass", LPF, 0, 100, 0, "H")); project.RegisterExpression(new UExpressionDescriptor("modulation", MOD, 0, 100, 0)); project.RegisterExpression(new UExpressionDescriptor("alternate", ALT, 0, 16, 0)); + project.RegisterExpression(new UExpressionDescriptor("direct", DIR, false, new string[] { "off", "on" })); project.RegisterExpression(new UExpressionDescriptor("tone shift", SHFT, -36, 36, 0)); project.RegisterExpression(new UExpressionDescriptor("tone shift (curve)", SHFC, -1200, 1200, 0) { type = UExpressionType.Curve }); project.RegisterExpression(new UExpressionDescriptor("tension (curve)", TENC, -100, 100, 0) { type = UExpressionType.Curve }); 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.Core/Render/RenderEngine.cs b/OpenUtau.Core/Render/RenderEngine.cs index ec6396892..8453189c0 100644 --- a/OpenUtau.Core/Render/RenderEngine.cs +++ b/OpenUtau.Core/Render/RenderEngine.cs @@ -169,6 +169,7 @@ public void PreRenderProject(ref CancellationTokenSource cancellation) { private RenderPartRequest[] PrepareRequests() { RenderPartRequest[] requests; + SingerManager.Inst.ReleaseSingersNotInUse(project); lock (project) { requests = project.parts .Where(part => part is UVoicePart) diff --git a/OpenUtau.Core/Render/RenderPhrase.cs b/OpenUtau.Core/Render/RenderPhrase.cs index 1557239be..d73066f5f 100644 --- a/OpenUtau.Core/Render/RenderPhrase.cs +++ b/OpenUtau.Core/Render/RenderPhrase.cs @@ -63,6 +63,7 @@ public class RenderPhone { public readonly float volume; public readonly float velocity; public readonly float modulation; + public readonly bool direct; public readonly Vector2[] envelope; public readonly UOto oto; @@ -109,12 +110,13 @@ internal RenderPhone(UProject project, UTrack track, UVoicePart part, UNote note flags = phoneme.GetResamplerFlags(project, track); string voiceColor = phoneme.GetVoiceColor(project, track); suffix = track.Singer.Subbanks.FirstOrDefault( - subbank => subbank.Color == voiceColor)?.Suffix; + subbank => subbank.Color == voiceColor)?.Suffix ?? string.Empty; volume = phoneme.GetExpression(project, track, Format.Ustx.VOL).Item1 * 0.01f; velocity = phoneme.GetExpression(project, track, Format.Ustx.VEL).Item1 * 0.01f; modulation = phoneme.GetExpression(project, track, Format.Ustx.MOD).Item1 * 0.01f; leadingMs = phoneme.preutter; envelope = phoneme.envelope.data.ToArray(); + direct = phoneme.GetExpression(project, track, Format.Ustx.DIR).Item1 == 1; oto = phoneme.oto; hash = Hash(); @@ -134,10 +136,11 @@ private ulong Hash() { writer.Write(flag.Item2.Value); } } - writer.Write(suffix ?? string.Empty); + writer.Write(suffix); writer.Write(volume); writer.Write(velocity); writer.Write(modulation); + writer.Write(direct); writer.Write(leadingMs); foreach (var point in envelope) { writer.Write(point.X); diff --git a/OpenUtau.Core/Render/Renderers.cs b/OpenUtau.Core/Render/Renderers.cs index bc55a2ef9..704f15454 100644 --- a/OpenUtau.Core/Render/Renderers.cs +++ b/OpenUtau.Core/Render/Renderers.cs @@ -105,13 +105,7 @@ public static IReadOnlyList GetSupportedResamplers(IWavtool? wavtool } public static IReadOnlyList GetSupportedWavtools(IResampler? resampler) { - if (resampler is WorldlineResampler) { - return ToolsManager.Inst.Wavtools - .Where(r => r is SharpWavtool) - .ToArray(); - } else { - return ToolsManager.Inst.Wavtools; - } + return ToolsManager.Inst.Wavtools; } } } diff --git a/OpenUtau.Core/Render/Worldline.cs b/OpenUtau.Core/Render/Worldline.cs index c31a9a6fd..dd72f533f 100644 --- a/OpenUtau.Core/Render/Worldline.cs +++ b/OpenUtau.Core/Render/Worldline.cs @@ -206,7 +206,7 @@ public SynthRequestWrapper(ResamplerItem item) { required_length = item.durRequired, consonant = item.consonant, cut_off = item.cutoff, - volume = item.volume, + volume = item.phone.direct ? 0 : item.volume, modulation = item.modulation, tempo = item.tempo, pitch_bend_length = item.pitches.Length, diff --git a/OpenUtau.Core/SingerManager.cs b/OpenUtau.Core/SingerManager.cs index 250088514..382b08a61 100644 --- a/OpenUtau.Core/SingerManager.cs +++ b/OpenUtau.Core/SingerManager.cs @@ -19,6 +19,8 @@ public class SingerManager : SingletonBase { private readonly ConcurrentQueue reloadQueue = new ConcurrentQueue(); private CancellationTokenSource reloadCancellation; + + private HashSet singersUsed = new HashSet(); public void Initialize() { InitializationTask = Task.Run(() => { @@ -32,7 +34,8 @@ public void SearchAllSingers() { Directory.CreateDirectory(PathManager.Inst.SingersPath); var stopWatch = Stopwatch.StartNew(); var singers = ClassicSingerLoader.FindAllSingers() - .Concat(Vogen.VogenSingerLoader.FindAllSingers()); + .Concat(Vogen.VogenSingerLoader.FindAllSingers()) + .Distinct(); Singers = singers .ToLookup(s => s.Id) .ToDictionary(g => g.Key, g => g.First()); @@ -109,5 +112,26 @@ private void Refresh() { }).Start(DocManager.Inst.MainScheduler); } } + + //Check which singers are in use and free memory for those that are not + public void ReleaseSingersNotInUse(UProject project) { + //Check which singers are in use + var singersInUse = new HashSet(); + foreach(var track in project.tracks){ + var singer = track.Singer; + if(singer != null){ + singersInUse.Add(singer); + } + } + //Release singers that are no longer in use + foreach(var singer in singersUsed){ + if(!singersInUse.Contains(singer)){ + singer.FreeMemory(); + singersUsed.Remove(singer); + } + } + //Update singers used + singersUsed.UnionWith(singersInUse); + } } } diff --git a/OpenUtau.Core/Ustx/UExpression.cs b/OpenUtau.Core/Ustx/UExpression.cs index 437806f32..d46bcf90b 100644 --- a/OpenUtau.Core/Ustx/UExpression.cs +++ b/OpenUtau.Core/Ustx/UExpression.cs @@ -65,7 +65,7 @@ public UExpressionDescriptor Clone() { }; } - public override string ToString() => name; + public override string ToString() => $"{abbr.ToUpper()}: {name}"; } public class UExpression { diff --git a/OpenUtau.Core/Ustx/USinger.cs b/OpenUtau.Core/Ustx/USinger.cs index 736875662..e13e34534 100644 --- a/OpenUtau.Core/Ustx/USinger.cs +++ b/OpenUtau.Core/Ustx/USinger.cs @@ -187,7 +187,7 @@ private static void AddToneRange(string range, SortedSet set) { [Flags] public enum USingerType { Classic = 0x1, Enunu = 0x2, Vogen = 0x4, DiffSinger=0x5 } - public class USinger : INotifyPropertyChanged { + public class USinger : INotifyPropertyChanged, IEquatable { protected static readonly List emptyOtos = new List(); public virtual string Id { get; } @@ -268,6 +268,16 @@ public virtual bool TryGetMappedOto(string phoneme, int tone, string color, out public virtual byte[] LoadPortrait() => null; public virtual byte[] LoadSample() => null; public override string ToString() => Name; + public bool Equals(USinger other) { + // Tentative: Since only the singer's Id is recorded in ustx and preferences, singers with the same Id are considered identical. + // Singer with the same directory name in different locations may be identical. + if (other != null && other.Id == this.Id) { + return true; + } else { + return false; + } + } + public override int GetHashCode() => Id.GetHashCode(); public static USinger CreateMissing(string name) { return new USinger() { @@ -280,5 +290,14 @@ public static USinger CreateMissing(string name) { private void NotifyPropertyChanged(string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + /// + /// Some types of singers store their data in memory when rendering. + /// This method is called when the singer is no longer used. + /// Note: + /// - the voicebank may be used again even after this method is called. + /// - this method may be called even when the singer has not been used + /// + public virtual void FreeMemory(){ } } } diff --git a/OpenUtau.Core/Util/OS.cs b/OpenUtau.Core/Util/OS.cs index 94d25e57a..a432ce589 100644 --- a/OpenUtau.Core/Util/OS.cs +++ b/OpenUtau.Core/Util/OS.cs @@ -13,22 +13,23 @@ public static void OpenFolder(string path) { if (Directory.Exists(path)) { Process.Start(new ProcessStartInfo { FileName = GetOpener(), - Arguments = path, + Arguments = GetWrappedPath(path), }); } } public static void GotoFile(string path) { if (File.Exists(path)) { + var wrappedPath = GetWrappedPath(path); if (IsWindows()) { Process.Start(new ProcessStartInfo { FileName = GetOpener(), - Arguments = $"/select, {path}", + Arguments = $"/select, {wrappedPath}", }); } else if (IsMacOS()) { Process.Start(new ProcessStartInfo { FileName = GetOpener(), - Arguments = $" -R {path}", + Arguments = $" -R {wrappedPath}", }); } else { OpenFolder(Path.GetDirectoryName(path)); @@ -95,5 +96,12 @@ private static string GetOpener() { } throw new IOException($"None of {string.Join(", ", linuxOpeners)} found."); } + private static string GetWrappedPath(string path) { + if (IsWindows()) { + return path; + } else { + return $"\"{path}\""; + } + } } } diff --git a/OpenUtau.Core/Util/PathManager.cs b/OpenUtau.Core/Util/PathManager.cs index fcba680f7..fac5d9cae 100644 --- a/OpenUtau.Core/Util/PathManager.cs +++ b/OpenUtau.Core/Util/PathManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -85,6 +86,19 @@ public PathManager() { public string NotePresetsFilePath => Path.Combine(DataPath, "notepresets.json"); public string BackupsPath => Path.Combine(DataPath, "Backups"); + public List SingersPaths { + get { + var list = new List { SingersPath }; + if (Directory.Exists(SingersPathOld)) { + list.Add(SingersPathOld); + } + if (Directory.Exists(AdditionalSingersPath)) { + list.Add(AdditionalSingersPath); + } + return list.Distinct().ToList(); + } + } + Regex invalid = new Regex("[\\x00-\\x1f<>:\"/\\\\|?*]|^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9]|CLOCK\\$)(\\.|$)|[\\.]$", RegexOptions.IgnoreCase); public string GetPartSavePath(string exportPath, string partName, int partNo) { diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index a6a94bedb..3b0833224 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -149,6 +149,7 @@ public class SerializablePreferences { public string SkipUpdate = string.Empty; public string AdditionalSingerPath = string.Empty; public bool InstallToAdditionalSingersPath = true; + public bool LoadDeepFolderSinger = true; public bool PreferCommaSeparator = false; public bool ResamplerLogging = false; public List RecentSingers = new List(); @@ -160,6 +161,7 @@ public class SerializablePreferences { public int PlaybackAutoScroll = 2; public bool ReverseLogOrder = true; public bool ShowPortrait = true; + public bool ShowIcon = true; public bool ShowGhostNotes = true; public bool PlayTone = true; public bool ShowVibrato = true; @@ -178,6 +180,8 @@ public class SerializablePreferences { public bool RememberMid = false; public bool RememberUst = true; public bool RememberVsqx = true; + public int ImportTempo = 0; + public string PhoneticAssistant = string.Empty; } } } diff --git a/OpenUtau.Core/Util/TimeAxis.cs b/OpenUtau.Core/Util/TimeAxis.cs index 66d53e91c..c7b528265 100644 --- a/OpenUtau.Core/Util/TimeAxis.cs +++ b/OpenUtau.Core/Util/TimeAxis.cs @@ -51,7 +51,9 @@ public void BuildSegments(UProject project) { posTick = timeSigSegments.Last().tickPos + timeSigSegments.Last().ticksPerBar * (timesig.barPosition - lastBarPos); } else { - Debug.Assert(timesig.barPosition == 0); + if(timesig.barPosition != 0) { + throw new Exception("First time signature must be at bar 0."); + } } timeSigSegments.Add(new TimeSigSegment { barPos = timesig.barPosition, @@ -76,7 +78,9 @@ public void BuildSegments(UProject project) { for (var i = 0; i < project.tempos.Count; ++i) { var tempo = project.tempos[i]; if (i == 0) { - Debug.Assert(tempo.position == 0); + if(tempo.position != 0) { + throw new Exception("First tempo must be at tick 0."); + } } var index = tempoSegments.FindIndex(seg => seg.tickPos >= tempo.position); if (index < 0) { diff --git a/OpenUtau.Core/Vogen/VogenSingerLoader.cs b/OpenUtau.Core/Vogen/VogenSingerLoader.cs index 8361c09bd..583ce8281 100644 --- a/OpenUtau.Core/Vogen/VogenSingerLoader.cs +++ b/OpenUtau.Core/Vogen/VogenSingerLoader.cs @@ -30,11 +30,7 @@ class VogenSingerLoader { public static IEnumerable FindAllSingers() { List singers = new List(); - foreach (var path in new string[] { - PathManager.Inst.SingersPathOld, - PathManager.Inst.SingersPath, - PathManager.Inst.AdditionalSingersPath, - }) { + foreach (var path in PathManager.Inst.SingersPaths) { var loader = new VogenSingerLoader(path); singers.AddRange(loader.SearchAll()); } @@ -50,7 +46,15 @@ public IEnumerable SearchAll() { if (!Directory.Exists(basePath)) { return result; } - result.AddRange(Directory.EnumerateFiles(basePath, "*.vogeon", SearchOption.AllDirectories) + IEnumerable files; + if (Preferences.Default.LoadDeepFolderSinger) { + files = Directory.EnumerateFiles(basePath, "*.vogeon", SearchOption.AllDirectories); + } else { + // TopDirectoryOnly + files = Directory.GetDirectories(basePath) + .SelectMany(path => Directory.EnumerateFiles(path, "*.vogeon")); + } + result.AddRange(files .Select(filePath => { try { return LoadSinger(filePath); diff --git a/OpenUtau.Core/Vogen/VogenYuePhonemizer.cs b/OpenUtau.Core/Vogen/VogenYuePhonemizer.cs index 867f4722c..bb2d1c954 100644 --- a/OpenUtau.Core/Vogen/VogenYuePhonemizer.cs +++ b/OpenUtau.Core/Vogen/VogenYuePhonemizer.cs @@ -3,7 +3,7 @@ using System; namespace OpenUtau.Core.Vogen { - [Phonemizer("Vogen Chinese Yue Phonemizer", "VOGEN ZH-YUE", language: "ZH")] + [Phonemizer("Vogen Chinese Yue Phonemizer", "VOGEN ZH-YUE", language: "ZH-YUE")] public class VogenYuePhonemizer : VogenBasePhonemizer { private static TrieNode? trie; private static InferenceSession? g2p; 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/CantoneseCVVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs new file mode 100644 index 000000000..4f05191c8 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenUtau.Api; +using OpenUtau.Classic; +using OpenUtau.Core.G2p; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Plugin.Builtin { + /// + /// Cantonese CVVC phonemizer. + /// It works similarly to the Chinese CVVC phonemizer, including presamp.ini requirement. + /// The big difference is that it converts hanzi to jyutping instead of pinyin. + /// + [Phonemizer("Cantonese CVVC Phonemizer", "ZH-YUE CVVC", "Lotte V", language: "ZH-YUE")] + public class CantoneseCVVCPhonemizer : Phonemizer { + private Dictionary vowels = new Dictionary(); + private Dictionary consonants = new Dictionary(); + private USinger singer; + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + var lyric = notes[0].lyric; + string consonant = consonants.TryGetValue(lyric, out consonant) ? consonant : lyric; + string prevVowel = "-"; + if (prevNeighbour != null) { + var prevLyric = prevNeighbour.Value.lyric; + if (vowels.TryGetValue(prevLyric, out var vowel)) { + prevVowel = vowel; + } + }; + var attr0 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + var attr1 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; + var attr2 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 2) ?? default; + if (lyric == "-" || lyric.ToLowerInvariant() == "r") { + if (singer.TryGetMappedOto($"{prevVowel} R", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto1)) { + return MakeSimpleResult(oto1.Alias); + } + return MakeSimpleResult($"{prevVowel} R"); + } + string currVowel = vowels.TryGetValue(lyric, out currVowel) ? currVowel : lyric; + int totalDuration = notes.Sum(n => n.duration); // totalDuration of current note + + if (singer.TryGetMappedOto($"{prevVowel} {lyric}", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto)) { + if (nextNeighbour == null && singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var oto1)) { + // automatically add ending if present + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = oto.Alias, + }, + new Phoneme() { + phoneme = oto1.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + return MakeSimpleResult(oto.Alias); + } + int vcLen = 120; + if (singer.TryGetMappedOto(lyric, notes[0].tone + attr1.toneShift, attr1.voiceColor, out var cvOto)) { + vcLen = MsToTick(cvOto.Preutter); + if (cvOto.Overlap == 0 && vcLen < 120) { + vcLen = Math.Min(120, vcLen * 2); // explosive consonant with short preutter. + } + if (cvOto.Overlap < 0) { + vcLen = MsToTick(cvOto.Preutter - cvOto.Overlap); + } + } + + if (singer.TryGetMappedOto(lyric, notes[0].tone + attr0.toneShift, attr0.voiceColor, out var cvOtoSimple)) { + lyric = cvOtoSimple.Alias; + } + + var vcPhoneme = $"{prevVowel} {consonant}"; + if (prevNeighbour != null) { + if (singer.TryGetMappedOto(vcPhoneme, prevNeighbour.Value.tone + attr0.toneShift, attr0.voiceColor, out oto)) { + vcPhoneme = oto.Alias; + } + // prevDuration calculated on basis of previous note length + int prevDuration = prevNeighbour.Value.duration; + // vcLength depends on the Vel of the current base note + vcLen = Convert.ToInt32(Math.Min(prevDuration / 1.5, Math.Max(30, vcLen * (attr1.consonantStretchRatio ?? 1)))); + } else { + if (singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out oto)) { + vcPhoneme = oto.Alias; + } + // no previous note, so length can be minimum velocity regardless of oto + vcLen = Convert.ToInt32(Math.Min(vcLen * 2, Math.Max(30, vcLen * (attr1.consonantStretchRatio ?? 1)))); + } + + if (nextNeighbour == null) { // automatically add ending if present + if (singer.TryGetMappedOto($"{prevVowel} {lyric}", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto0)) { + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var otoEnd)) { + // automatically add ending if present + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = oto0.Alias, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } else { + // use vc if present + if (prevNeighbour == null && singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out var vcOto1)) { + vcPhoneme = vcOto1.Alias; + // automatically add ending if present + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr2.toneShift, attr2.voiceColor, out var otoEnd)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = vcPhoneme, + position = -vcLen, + }, + new Phoneme() { + phoneme = cvOto?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } else if (prevNeighbour != null && singer.TryGetMappedOto(vcPhoneme, prevNeighbour.Value.tone + attr0.toneShift, attr0.voiceColor, out var vcOto2)) { + vcPhoneme = vcOto2.Alias; + // automatically add ending if present + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr2.toneShift, attr2.voiceColor, out var otoEnd)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = vcPhoneme, + position = -vcLen, + }, + new Phoneme() { + phoneme = cvOto?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } // just base note and ending + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var otoEnd1)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = cvOtoSimple?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd1.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } + } + + if (singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out oto)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = vcPhoneme, + position = -vcLen, + }, + new Phoneme() { + phoneme = cvOto?.Alias ?? lyric, + }, + }, + }; + } + return MakeSimpleResult(cvOtoSimple?.Alias ?? lyric); + } + + public override void SetSinger(USinger singer) { + if (this.singer == singer) { + return; + } + this.singer = singer; + vowels.Clear(); + consonants.Clear(); + if (this.singer == null) { + return; + } + try { + string file = Path.Combine(singer.Location, "presamp.ini"); + using (var reader = new StreamReader(file, singer.TextFileEncoding)) { + var blocks = Ini.ReadBlocks(reader, file, @"\[\w+\]"); + var vowelLines = blocks.Find(block => block.header == "[VOWEL]").lines; + foreach (var iniLine in vowelLines) { + var parts = iniLine.line.Split('='); + if (parts.Length >= 3) { + string vowelLower = parts[0]; + string vowelUpper = parts[1]; + string[] sounds = parts[2].Split(','); + foreach (var sound in sounds) { + vowels[sound] = vowelLower; + } + } + } + var consonantLines = blocks.Find(block => block.header == "[CONSONANT]").lines; + foreach (var iniLine in consonantLines) { + var parts = iniLine.line.Split('='); + if (parts.Length >= 3) { + string consonant = parts[0]; + string[] sounds = parts[1].Split(','); + foreach (var sound in sounds) { + consonants[sound] = consonant; + } + } + } + var priority = blocks.Find(block => block.header == "PRIORITY"); + var replace = blocks.Find(block => block.header == "REPLACE"); + var alias = blocks.Find(block => block.header == "ALIAS"); + } + } catch (Exception e) { + Log.Error(e, "failed to load presamp.ini"); + } + } + + /// + /// Converts hanzi notes to jyutping phonemes. + /// + /// + public override void SetUp(Note[][] groups) { + JyutpingConversion.RomanizeNotes(groups); + } + + /// + /// Converts hanzi to jyutping, based on G2P. + /// + public class JyutpingConversion { + public static Note[] ChangeLyric(Note[] group, string lyric) { + 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 string[] Romanize(IEnumerable lyrics) { + var lyricsArray = lyrics.ToArray(); + var hanziLyrics = lyricsArray + .Where(ZhG2p.CantoneseInstance.IsHanzi) + .ToList(); + var jyutpingResult = ZhG2p.CantoneseInstance.Convert(hanziLyrics, false, false).ToLower().Split(); + if (jyutpingResult == null) { + return lyricsArray; + } + var jyutpingIndex = 0; + for (int i = 0; i < lyricsArray.Length; i++) { + if (lyricsArray[i].Length == 1 && ZhG2p.CantoneseInstance.IsHanzi(lyricsArray[i])) { + lyricsArray[i] = jyutpingResult[jyutpingIndex]; + jyutpingIndex++; + } + } + return lyricsArray; + } + + public static void RomanizeNotes(Note[][] groups) { + var ResultLyrics = Romanize(groups.Select(group => group[0].lyric)); + Enumerable.Zip(groups, ResultLyrics, ChangeLyric).Last(); + } + + public void SetUp(Note[][] groups) { + RomanizeNotes(groups); + } + } + } +} diff --git a/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs b/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs new file mode 100644 index 000000000..fdb0be122 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs @@ -0,0 +1,368 @@ +using OpenUtau.Api; +using OpenUtau.Core.G2p; +using OpenUtau.Core.Ustx; +using System.Collections.Generic; +using System.Linq; + +namespace OpenUtau.Plugin.Builtin { + /// + /// Cantonese phonemizer for Syo-style banks. + /// Supports both full jyutping syllables as well as syllable fallbacks without a final consonant or falling diphthong. + /// Supports hanzi and jyutping input. + /// + [Phonemizer("Cantonese Syo-Style Phonemizer", "ZH-YUE SYO", "Lotte V", language: "ZH-YUE")] + public class CantoneseSyoPhonemizer : Phonemizer { + + /// + /// The consonant table. + /// + static readonly string consonants = "b,p,m,f,d,t,n,l,g,k,ng,h,gw,kw,w,z,c,s,j"; + + /// + /// The vowel split table. + /// + static readonly string vowels = "aap=aa p,aat=aa t,aak=aa k,aam=aa m,aan=aa n,aang=aa ng,aai=aa i,aau=aa u,ap=a p,at=a t,ak=a k,am=a m,an=a n,ang=a ng,ai=a i,au=a u,op=o p,ot=o t,ok=o k,om=o m,on=o n,ong=o ng,oi=o i,ou=o u,oet=oe t,oek=oe k,oeng=oe ng,oei=oe i,eot=eo t,eon=eo n,eoi=eo i,ep=e p,et=e t,ek=e k,em=e m,en=e n,eng=e ng,ei=e i,eu=e u,up=u p,ut=u t,uk=uu k,um=um,un=u n,ung=uu ng,ui=u i,yut=yu t,yun=yu n,ip=i p,it=i t,ik=ii k,im=i m,in=i n,ing=ii ng,iu=i u"; + + /// + /// Check for vowel substitutes. + /// + static readonly string[] substitution = new string[] { + "aap,aat,aak,aam,aan,aang,aai,aau=aa", "ap,at,ak,am,an,ang,ai,au=a", "op,ot,ok,om,on,ong,oi,ou=o", "oet,oek,oen,oeng,oei=oe", "eot,eon,eoi=eo","ep,et,ek,em,en,eng,ei,eu=e", "uk,ung=uu", "up,ut,um,un,ui=u", "yut,yun=yu","ik,ing=ii", "ip,it,im,in,iu=i" + }; + + /// + /// Check for substitutes for finals. + /// + static readonly string[] finalSub = new string[] { + "ii ng=i ng", "ii k=i k", "uu k=u k", "uu ng=u ng", "oe t=eo t", "oe i=eo i" + }; + + static HashSet cSet; + static Dictionary vDict; + static readonly Dictionary substituteLookup; + static readonly Dictionary finalSubLookup; + + static CantoneseSyoPhonemizer() { + cSet = new HashSet(consonants.Split(',')); + vDict = vowels.Split(',') + .Select(s => s.Split('=')) + .ToDictionary(a => a[0], a => a[1]); + substituteLookup = substitution.ToList() + .SelectMany(line => { + var parts = line.Split('='); + return parts[0].Split(',').Select(orig => (orig, parts[1])); + }) + .ToDictionary(t => t.Item1, t => t.Item2); + finalSubLookup = finalSub.ToList() + .SelectMany(line => { + var parts = line.Split('='); + return parts[0].Split(',').Select(orig => (orig, parts[1])); + }) + .ToDictionary(t => t.Item1, t => t.Item2); + } + + private USinger singer; + + // Simply stores the singer in a field. + public override void SetSinger(USinger singer) => this.singer = singer; + + /// + /// Converts hanzi notes to jyutping phonemes. + /// + /// + public override void SetUp(Note[][] groups) { + JyutpingConversion.RomanizeNotes(groups); + } + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + // The overall logic is: + // 1. Remove consonant: "jyut" -> "yut". + // 2. Lookup the trailing sound in vowel table: "yut" -> "yu t". + // 3. Split the total duration and returns "jyut"/"jyu" and "yu t". + var note = notes[0]; + var lyric = note.lyric; + string consonant = string.Empty; + string vowel = string.Empty; + + if (lyric.Length > 2 && cSet.Contains(lyric.Substring(0, 2))) { + // First try to find consonant "gw", "kw", "ng", and extract vowel. + consonant = lyric.Substring(0, 2); + vowel = lyric.Substring(2); + } else if (lyric.Length > 1 && cSet.Contains(lyric.Substring(0, 1)) && lyric != "ng") { + // Then try to find single character consonants, and extract vowel. + consonant = lyric.Substring(0, 1); + vowel = lyric.Substring(1); + } else { + // Otherwise the lyric is a vowel. + vowel = lyric; + } + + string phoneme0 = lyric; + + // Get color + string color = string.Empty; + int toneShift = 0; + int? alt = 0; + if (note.phonemeAttributes != null) { + var attr = note.phonemeAttributes.FirstOrDefault(attr0 => attr0.index == 0); + color = attr.voiceColor; + toneShift = attr.toneShift; + alt = attr.alternate; + } + + string fin = $"{vowel} -"; + // We will need to split the total duration for phonemes, so we compute it here. + int totalDuration = notes.Sum(n => n.duration); + // Lookup the vowel split table. For example, "yut" will match "yu t". + if (vDict.TryGetValue(vowel, out var phoneme1) && !string.IsNullOrEmpty(phoneme1)) { + // Now phoneme0="jyu" and phoneme1="yu t", + // try to give "yu t" 120 ticks, but no more than half of the total duration. + int length1 = 120; + + if (length1 > totalDuration / 2) { + length1 = totalDuration / 2; + } + var lyrics = new List { lyric }; + // find potential substitute symbol + if (substituteLookup.TryGetValue(vowel ?? string.Empty, out var sub)) { + if (!string.IsNullOrEmpty(consonant)) { + lyrics.Add($"{consonant}{sub}"); + } else { + lyrics.Add(sub); + } + } + + // Try initial and then a plain lyric + if (prevNeighbour == null || (prevNeighbour != null && (prevNeighbour.Value.lyric.EndsWith("p") || prevNeighbour.Value.lyric.EndsWith("t") || prevNeighbour.Value.lyric.EndsWith("k")))) { + var initial = $"- {lyric}"; + var initial2 = $"- {lyrics[1]}"; + var tests = new List { initial, initial2, lyric, lyrics[1] }; + if (checkOtoUntilHit(tests, note, out var otoInit)) { + phoneme0 = otoInit.Alias; + } + } else { // nothing special necessary + if (checkOtoUntilHit(lyrics, note, out var otoLyric)) { + phoneme0 = otoLyric.Alias; + } + } + + int length2 = 60; + if (length2 > totalDuration / 2) { + length2 = totalDuration / 2; + } + if (nextNeighbour == null && singer.TryGetMappedOto(fin, note.tone, out _)) { + // Vowel ending is minimum 60 ticks, maximum half of note + var finals = new List { fin }; + if (checkOtoUntilHitFinal(finals, note, out var otoFin)) { + phoneme1 = otoFin.Alias; + } + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme0, + }, + new Phoneme() { + phoneme = phoneme1, + position = totalDuration - length2, + } + }, + }; + } else { + var tails = new List { phoneme1 }; + // find potential substitute symbol + if (finalSubLookup.TryGetValue(phoneme1 ?? string.Empty, out var finSub)) { + tails.Add(finSub); + } + if (checkOtoUntilHitFinal(tails, note, out var otoTail)) { + phoneme1 = otoTail.Alias; + } else { + return MakeSimpleResult(phoneme0); + } + } + + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme0, + }, + new Phoneme() { + phoneme = phoneme1, + position = totalDuration - length1, + } + }, + }; + } + + // Check for vowel ending on open syllables. + // If a vowel ending does not exist, it will not be inserted. + if (nextNeighbour == null && string.IsNullOrEmpty(phoneme1) && !string.IsNullOrEmpty(fin)) { + // Vowel ending is minimum 60 ticks, maximum half of note + int length1 = 60; + + if (length1 > totalDuration / 2) { + length1 = totalDuration / 2; + } + // Try initial and then a plain lyric + var lyrics = new List { lyric }; + if (prevNeighbour == null || (prevNeighbour != null && (prevNeighbour.Value.lyric.EndsWith("p") || prevNeighbour.Value.lyric.EndsWith("t") || prevNeighbour.Value.lyric.EndsWith("k")))) { + var initial = $"- {lyric}"; + var tests = new List { initial, lyric }; + if (checkOtoUntilHit(tests, note, out var otoInit)) { + phoneme0 = otoInit.Alias; + } + } else { // nothing special necessary + if (checkOtoUntilHit(lyrics, note, out var otoLyric)) { + phoneme0 = otoLyric.Alias; + } else { + return MakeSimpleResult(phoneme0); + } + } + + // Map vowel ending + var tails = new List { fin }; + if (checkOtoUntilHitFinal(tails, note, out var otoTail)) { + fin = otoTail.Alias; + } else { + return MakeSimpleResult(phoneme0); + } + + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme0, + }, + new Phoneme() { + phoneme = fin, + position = totalDuration - length1, + } + }, + }; + } + + // Try initial and then a plain lyric + if (prevNeighbour == null || (prevNeighbour != null && (prevNeighbour.Value.lyric.EndsWith("p") || prevNeighbour.Value.lyric.EndsWith("t") || prevNeighbour.Value.lyric.EndsWith("k")))) { + var simpleInitial = $"- {lyric}"; + var tests = new List { simpleInitial, lyric }; + if (checkOtoUntilHit(tests, note, out var otoInit)) { + phoneme0 = otoInit.Alias; + } else { + return MakeSimpleResult(phoneme0); + } + } else { // nothing special necessary + var tests = new List { lyric }; + if (checkOtoUntilHit(tests, note, out var otoLyric)) { + phoneme0 = otoLyric.Alias; + } else { + return MakeSimpleResult(phoneme0); + } + } + // Not spliting is needed. Return as is. + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = phoneme0, + } + }, + }; + + } + + /// + /// Converts hanzi to jyutping, based on G2P. + /// + public class JyutpingConversion { + public static Note[] ChangeLyric(Note[] group, string lyric) { + 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 string[] Romanize(IEnumerable lyrics) { + var lyricsArray = lyrics.ToArray(); + var hanziLyrics = lyricsArray + .Where(ZhG2p.CantoneseInstance.IsHanzi) + .ToList(); + var jyutpingResult = ZhG2p.CantoneseInstance.Convert(hanziLyrics, false, false).ToLower().Split(); + if (jyutpingResult == null) { + return lyricsArray; + } + var jyutpingIndex = 0; + for (int i = 0; i < lyricsArray.Length; i++) { + if (lyricsArray[i].Length == 1 && ZhG2p.CantoneseInstance.IsHanzi(lyricsArray[i])) { + lyricsArray[i] = jyutpingResult[jyutpingIndex]; + jyutpingIndex++; + } + } + return lyricsArray; + } + + public static void RomanizeNotes(Note[][] groups) { + var ResultLyrics = Romanize(groups.Select(group => group[0].lyric)); + Enumerable.Zip(groups, ResultLyrics, ChangeLyric).Last(); + } + + public void SetUp(Note[][] groups) { + RomanizeNotes(groups); + } + } + + // make it quicker to check multiple oto occurrences at once rather than spamming if else if + private bool checkOtoUntilHit(List input, Note note, out UOto oto) { + oto = default; + var attr = note.phonemeAttributes?.FirstOrDefault(attrCheck => attrCheck.index == 0) ?? default; + + var otos = new List(); + foreach (string test in input) { + if (singer.TryGetMappedOto(test + attr.alternate, note.tone + attr.toneShift, attr.voiceColor, out var otoAlt)) { + otos.Add(otoAlt); + } else if (singer.TryGetMappedOto(test, note.tone + attr.toneShift, attr.voiceColor, out var otoCandidacy)) { + otos.Add(otoCandidacy); + } + } + + string color = attr.voiceColor ?? ""; + if (otos.Count > 0) { + if (otos.Any(otoCheck => (otoCheck.Color ?? string.Empty) == color)) { + oto = otos.Find(otoCheck => (otoCheck.Color ?? string.Empty) == color); + return true; + } else { + oto = otos.First(); + return true; + } + } + return false; + } + + // Check for final consonant or vowel ending + private bool checkOtoUntilHitFinal(List input, Note note, out UOto oto) { + oto = default; + var attr = note.phonemeAttributes?.FirstOrDefault(attrCheck => attrCheck.index == 1) ?? default; + + var otos = new List(); + foreach (string test in input) { + if (singer.TryGetMappedOto(test + attr.alternate, note.tone + attr.toneShift, attr.voiceColor, out var otoAlt)) { + otos.Add(otoAlt); + } else if (singer.TryGetMappedOto(test, note.tone + attr.toneShift, attr.voiceColor, out var otoCandidacy)) { + otos.Add(otoCandidacy); + } + } + + string color = attr.voiceColor ?? ""; + if (otos.Count > 0) { + if (otos.Any(otoCheck => (otoCheck.Color ?? string.Empty) == color)) { + oto = otos.Find(otoCheck => (otoCheck.Color ?? string.Empty) == color); + return true; + } else { + return false; + } + } + return false; + } + } +} diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVCPhonemizer.cs index 5bf66a02e..42c8fba29 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVCPhonemizer.cs @@ -14,7 +14,6 @@ public class ChineseCVVCPhonemizer : BaseChinesePhonemizer { private Dictionary vowels = new Dictionary(); private Dictionary consonants = new Dictionary(); private USinger singer; - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { var lyric = notes[0].lyric; string consonant = consonants.TryGetValue(lyric, out consonant) ? consonant : lyric; @@ -27,13 +26,31 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }; var attr0 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; var attr1 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; + var attr2 = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == 2) ?? default; if (lyric == "-" || lyric.ToLowerInvariant() == "r") { if (singer.TryGetMappedOto($"{prevVowel} R", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto1)) { return MakeSimpleResult(oto1.Alias); } return MakeSimpleResult($"{prevVowel} R"); } + string currVowel = vowels.TryGetValue(lyric, out currVowel) ? currVowel : lyric; + int totalDuration = notes.Sum(n => n.duration); // totalDuration of current note + if (singer.TryGetMappedOto($"{prevVowel} {lyric}", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto)) { + if (nextNeighbour == null && singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var oto1)) { + // automatically add ending if present + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = oto.Alias, + }, + new Phoneme() { + phoneme = oto1.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } return MakeSimpleResult(oto.Alias); } int vcLen = 120; @@ -46,15 +63,20 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN vcLen = MsToTick(cvOto.Preutter - cvOto.Overlap); } } + + if (singer.TryGetMappedOto(lyric, notes[0].tone + attr0.toneShift, attr0.voiceColor, out var cvOtoSimple)) { + lyric = cvOtoSimple.Alias; + } + var vcPhoneme = $"{prevVowel} {consonant}"; if (prevNeighbour != null) { if (singer.TryGetMappedOto(vcPhoneme, prevNeighbour.Value.tone + attr0.toneShift, attr0.voiceColor, out oto)) { vcPhoneme = oto.Alias; } - // totalDuration calculated on basis of previous note length - int totalDuration = prevNeighbour.Value.duration; + // prevDuration calculated on basis of previous note length + int prevDuration = prevNeighbour.Value.duration; // vcLength depends on the Vel of the current base note - vcLen = Convert.ToInt32(Math.Min(totalDuration / 1.5, Math.Max(30, vcLen * (attr1.consonantStretchRatio ?? 1)))); + vcLen = Convert.ToInt32(Math.Min(prevDuration / 1.5, Math.Max(30, vcLen * (attr1.consonantStretchRatio ?? 1)))); } else { if (singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out oto)) { vcPhoneme = oto.Alias; @@ -63,6 +85,81 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN vcLen = Convert.ToInt32(Math.Min(vcLen * 2, Math.Max(30, vcLen * (attr1.consonantStretchRatio ?? 1)))); } + if (nextNeighbour == null) { // automatically add ending if present + if (singer.TryGetMappedOto($"{prevVowel} {lyric}", notes[0].tone + attr0.toneShift, attr0.voiceColor, out var oto0)) { + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var otoEnd)) { + // automatically add ending if present + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = oto0.Alias, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } else { + // use vc if present + if (prevNeighbour == null && singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out var vcOto1)) { + vcPhoneme = vcOto1.Alias; + // automatically add ending if present + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr2.toneShift, attr2.voiceColor, out var otoEnd)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = vcPhoneme, + position = -vcLen, + }, + new Phoneme() { + phoneme = cvOto?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } else if (prevNeighbour != null && singer.TryGetMappedOto(vcPhoneme, prevNeighbour.Value.tone + attr0.toneShift, attr0.voiceColor, out var vcOto2)) { + vcPhoneme = vcOto2.Alias; + // automatically add ending if present + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr2.toneShift, attr2.voiceColor, out var otoEnd)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = vcPhoneme, + position = -vcLen, + }, + new Phoneme() { + phoneme = cvOto?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } // just base note and ending + if (singer.TryGetMappedOto($"{currVowel} R", notes[0].tone + attr1.toneShift, attr1.voiceColor, out var otoEnd1)) { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = cvOtoSimple?.Alias ?? lyric, + }, + new Phoneme() { + phoneme = otoEnd1.Alias, + position = totalDuration - (totalDuration / 6), + }, + }, + }; + } + } + } + if (singer.TryGetMappedOto(vcPhoneme, notes[0].tone + attr0.toneShift, attr0.voiceColor, out oto)) { return new Result { phonemes = new Phoneme[] { @@ -76,7 +173,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN }, }; } - return MakeSimpleResult(cvOto?.Alias ?? lyric); + return MakeSimpleResult(cvOtoSimple?.Alias ?? lyric); } public override void SetSinger(USinger singer) { diff --git a/OpenUtau.Plugin.Builtin/ChineseCVVPhonemizer.cs b/OpenUtau.Plugin.Builtin/ChineseCVVPhonemizer.cs index d8df02df2..04a05de44 100644 --- a/OpenUtau.Plugin.Builtin/ChineseCVVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ChineseCVVPhonemizer.cs @@ -84,6 +84,10 @@ public bool IsVowel(string phoneme){ return !phoneme.StartsWith("_"); } + public bool IsGlide(string phoneme){ + return false; + } + public string[] Query(string lyric){ // The overall logic is: // 1. Remove consonant: "duang" -> "uang". diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index a3b4d4b86..de3b54bcd 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -4,21 +4,24 @@ using System.Linq; using OpenUtau.Api; using OpenUtau.Core.G2p; -using OpenUtau.Core.Ustx; using Serilog; namespace OpenUtau.Plugin.Builtin { + /// + /// General English phonemizer for X-SAMPA voicebanks. + /// The difference between this phonemizer and the Teto English phonemizer is that this one was made to support all X-SAMPA-based banks. + /// However, it should be fully compatible with Kasane Teto's English voicebank regardless. + ///

+ ///

+ /// It supports Delta-style English banks, as well as other English X-SAMPA voicebank styles. + /// There is also support for extended English phonemes, which can be included in a custom dictionary and/or phonetic input. + /// Due to the flexibility of X-SAMPA, it was easy to add the custom sounds. More suggestions for this are always welcome. + ///

+ ///

+ /// Read more about X-SAMPA here. + ///
[Phonemizer("English X-SAMPA phonemizer", "EN X-SAMPA", "Lotte V", language: "EN")] public class EnXSampaPhonemizer : SyllableBasedPhonemizer { - /// - /// General English phonemizer for X-SAMPA voicebanks. - /// The difference between this phonemizer and the Teto English phonemizer is that this one was made to support all X-SAMPA-based banks. - /// However, it should be fully compatible with Kasane Teto's English voicebank regardless. - /// It supports Delta-style English banks, as well as other English X-SAMPA voicebank styles. - /// There is also support for extended English phonemes, which can be included in a custom dictionary and/or phonetic input. - /// Due to the flexibility of X-SAMPA, it was easy to add the custom sounds. More suggestions for this are always welcome. - /// - private readonly string[] vowels = "a,A,@,{,V,O,aU,aI,E,3,eI,I,i,oU,OI,U,u,Q,Ol,Ql,aUn,e@,eN,IN,e,o,Ar,Qr,Er,Ir,Or,Ur,ir,ur,aIr,aUr,A@,Q@,E@,I@,O@,U@,i@,u@,aI@,aU@,@r,@l,@m,@n,@N,1,e@m,e@n,y,I\\,M,U\\,Y,@\\,@`,3`,A`,Q`,E`,I`,O`,U`,i`,u`,aI`,aU`,},2,3\\,6,7,8,9,&,{~,I~,aU~,VI,VU,@U,ai,ei,Oi,au,ou,Ou,@u,i:,u:,O:,e@0,E~,e~,3r,ar,or,{l,Al,al,El,Il,il,ul,Ul,mm,nn,ll,NN".Split(','); private readonly string[] consonants = "b,tS,d,D,4,f,g,h,dZ,k,l,m,n,N,p,r,s,S,t,T,v,w,W,j,z,Z,t_},・,_".Split(','); private readonly string[] affricates = "tS,dZ".Split(','); @@ -160,6 +163,10 @@ protected override List ProcessSyllable(Syllable syllable) { string[] cc = syllable.cc; string v = syllable.v; + string[] CurrentWordCc = syllable.CurrentWordCc; + string[] PreviousWordCc = syllable.PreviousWordCc; + int prevWordConsonantsCount = syllable.prevWordConsonantsCount; + string basePhoneme; var phonemes = new List(); var lastC = cc.Length - 1; @@ -276,11 +283,21 @@ protected override List ProcessSyllable(Syllable syllable) { } } else { // VCV var vcv = $"{prevV} {cc[0]}{v}"; + var vcvEnd = $"{prevV}{cc[0]} {v}"; var vccv = $"{prevV} {string.Join("", cc)}{v}"; var crv = $"{cc.Last()} {v}"; - if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone))) { + // Use regular VCV if the current word starts with one consonant and the previous word ends with none + if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + // Use end VCV if current word does not start with a consonant but the previous word does end with one + } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) || HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { + basePhoneme = vcvEnd; + // Use regular VCV if end VCV does not exist + } else if (syllable.IsVCVWithOneConsonant && !HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone) && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone))) { basePhoneme = vcv; - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone))) { + // VCV with multiple consonants, only for current word onset and null previous word ending + // TODO: multi-VCV for words ending with one or more consonants? + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { basePhoneme = vccv; lastC = 0; } else { @@ -293,11 +310,11 @@ protected override List ProcessSyllable(Syllable syllable) { for (var i = firstC; i < cc.Length; i++) { var ccv = $"{string.Join("", cc.Skip(i))}{v}"; var rccv = $"- {string.Join("", cc.Skip(i))}{v}"; - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { lastC = i; basePhoneme = ccv; break; - } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone))) { + } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { lastC = i; basePhoneme = rccv; break; @@ -314,7 +331,7 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) { phonemes.Add(vr); } - } else if (HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) { + } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && !affricates.Contains(string.Join("", cc.Take(2)))) { phonemes.Add(vcc); firstC = 1; break; @@ -331,63 +348,71 @@ protected override List ProcessSyllable(Syllable syllable) { } } } + for (var i = firstC; i < lastC; i++) { // we could use some CCV, so lastC is used // we could use -CC so firstC is used var cc1 = $"{string.Join("", cc.Skip(i))}"; var ccv = string.Join("", cc.Skip(i)) + v; - var ucv = $"_{cc.Last()}{v}"; ; + var ucv = $"_{cc.Last()}{v}"; + // Use [C1C2...] when current word starts with 2 consonants or more + if (CurrentWordCc.Length >= 2) { + cc1 = $"{string.Join("", cc.Skip(i))}"; + } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } - if (!HasOto(cc1, syllable.tone)) { + // Use [C1C2] when current word has 2 consonants or more and [C1C2C3...] does not exist + if (!HasOto(cc1, syllable.tone) && CurrentWordCc.Length >= 2) { cc1 = $"{cc[i]}{cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } - if (!HasOto(cc1, syllable.tone)) { + // Use [C1 C2] when either [C1C2] does not exist, or current word has 1 consonant or less and previous word has 1 consonant or more + if (!HasOto(cc1, syllable.tone) || (prevWordConsonantsCount >= 1 && CurrentWordCc.Length <= 1)) { cc1 = $"{cc[i]} {cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + // Use CCV if it exists + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { basePhoneme = ccv; lastC = i; + // Use _CV if it exists } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone) && !cc1.Contains($"{cc[i]} {cc[i + 1]}")) { basePhoneme = ucv; } if (i + 1 < lastC) { var cc2 = $"{string.Join("", cc.Skip(i))}"; - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); + // Use [C2C3...] when current word starts with 2 consonants or more + if (CurrentWordCc.Length >= 2) { + cc2 = $"{string.Join("", cc.Skip(i))}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } - if (!HasOto(cc2, syllable.tone)) { + // Use [C2C3] when current word has 2 consonants or more and [C2C3C4...] does not exist + if (!HasOto(cc2, syllable.tone) && CurrentWordCc.Length >= 2) { cc2 = $"{cc[i + 1]}{cc[i + 2]}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } - if (!HasOto(cc2, syllable.tone)) { + // Use [C2 C3] when either [C2C3] does not exist, or current word has 1 consonant or less and previous word has 2 consonants or more + if (!HasOto(cc2, syllable.tone) || (prevWordConsonantsCount >= 2 && CurrentWordCc.Length <= 1 && !CurrentWordCc.Contains(cc2))) { cc2 = $"{cc[i + 1]} {cc[i + 2]}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + // Use CCV if it exists + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { basePhoneme = ccv; lastC = i; - } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && (HasOto(cc2, syllable.vowelTone) || HasOto(ValidateAlias(cc2), syllable.vowelTone)) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}")) { + // Use _CV if it exists + } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && (HasOto(cc2, syllable.vowelTone) || HasOto(ValidateAlias(cc2), syllable.vowelTone)) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}") && !PreviousWordCc.Contains(ucv)) { basePhoneme = ucv; } if (HasOto(cc1, syllable.tone) && HasOto(cc2, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { @@ -399,6 +424,7 @@ protected override List ProcessSyllable(Syllable syllable) { i++; } } else { + /// Add single consonant if no CC cluster // like [V C1] [C1] [C2 ..] TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); } diff --git a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs index 19bac5f7f..47bd632df 100644 --- a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs @@ -195,7 +195,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN // Insert 2nd phoneme (when next doesn't have hint) if (nextNeighbour != null && string.IsNullOrEmpty(nextNeighbour.Value.phoneticHint)) { int totalDuration = notes.Sum(n => n.duration); - if (TickToMs(totalDuration) < 100) { + if (TickToMs(totalDuration) < 100 && presamp.MustVC == false) { return MakeSimpleResult(currentLyric); } 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 diff --git a/OpenUtau.Plugin.Builtin/PhonemeBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/PhonemeBasedPhonemizer.cs index e0ed68bd8..2c33d0a99 100644 --- a/OpenUtau.Plugin.Builtin/PhonemeBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/PhonemeBasedPhonemizer.cs @@ -81,7 +81,8 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN return MakeSimpleResult(note.lyric); } // Find phone types of symbols. - var isVowel = symbols.Select(s => g2p.IsVowel(s)).ToArray(); + var isVowel = symbols.Select(g2p.IsVowel).ToArray(); + var isGlide = symbols.Select(g2p.IsGlide).ToArray(); // Arpasing aligns the first vowel at 0 and shifts leading consonants to negative positions, // so we need to find the first vowel. var phonemes = new Phoneme[symbols.Length]; @@ -94,7 +95,13 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN var nonExtensionNotes = notes.Where(n=>!IsSyllableVowelExtensionNote(n)).ToArray(); for (int i = 0; i < symbols.Length; i++) { if (isVowel[i] && alignments.Count < nonExtensionNotes.Length) { - alignments.Add(Tuple.Create(i, nonExtensionNotes[alignments.Count].position - notes[0].position, false)); + //Handle glide phonemes + //For "Consonant-Glide-Vowel" syllable, the glide phoneme is placed after the start position of the note. + if(i>=2 && isGlide[i-1] && !isVowel[i-2]){ + alignments.Add(Tuple.Create(i-1, nonExtensionNotes[alignments.Count].position - notes[0].position, false)); + } else{ + alignments.Add(Tuple.Create(i, nonExtensionNotes[alignments.Count].position - notes[0].position, false)); + } } } int position = notes[0].duration; diff --git a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs index 1826e3131..e350abf21 100644 --- a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs @@ -1,42 +1,85 @@ using System; using System.Collections.Generic; -using System.Text; using OpenUtau.Api; using OpenUtau.Core.G2p; using System.Linq; using Serilog; +using System.IO; namespace OpenUtau.Plugin.Builtin { + /// + /// Spanish syllable-based phonemizer. + /// I tried to make this phonemizer as compatible with many different methods as possible. + /// Supports both CVVC and VCV if the voicebank has it. + /// Supports seseo ("s" instead of "z" if the voicebank doesn't have the latter). + /// It also substitutes "nh" for "ny" if the voicebank doesn't have the first. + /// Ít now also uses "i" instead of "y" and "u" instead of "w" depending on what the voicebank supports. + /// Now with full VCV support, including "consonant VCV" if the voicebank has either of them (ex. "l ba", "n da" but also "m bra" etc.). + /// [Phonemizer("Spanish Syllable-Based Phonemizer", "ES SYL", "Lotte V", language: "ES")] public class SpanishSyllableBasedPhonemizer : SyllableBasedPhonemizer { - /// - /// Spanish syllable-based phonemizer. - /// I tried to make this phonemizer as compatible with many different methods as possible. - /// Supports both CVVC and VCV if the voicebank has it. - /// Supports seseo ("s" instead of "z" if the voicebank doesn't have the latter). - /// It also substitutes "nh" for "ny" if the voicebank doesn't have the first. - /// Ít now also uses "i" instead of "y" and "u" instead of "w" depending on what the voicebank supports. - /// Now with full VCV support, including "consonant VCV" if the voicebank has either of them (ex. "l ba", "n da" but also "m bra" etc.). - /// - private readonly string[] vowels = "a,e,i,o,u".Split(','); - private readonly string[] consonants = "b,ch,d,dz,f,g,h,hh,j,k,l,ll,m,n,nh,p,r,rr,s,sh,t,ts,w,y,z,zz,zh".Split(','); - private readonly Dictionary dictionaryReplacements = ("a=a;e=e;i=i;o=o;u=u;" + - "b=b;ch=ch;d=d;dz=dz;f=f;g=g;gn=nh;k=k;l=l;ll=j;m=m;n=n;p=p;r=r;rr=rr;s=s;sh=sh;t=t;ts=ts;w=w;x=h;y=y;z=z;zz=zz;zh=zh;I=i;U=u;B=b;D=d;G=g;Y=y").Split(';') + private readonly string[] consonants = "b,ch,d,dz,f,g,h,hh,j,k,l,ll,m,n,nh,p,r,rr,s,sh,t,ts,w,y,z,zz,zh,I,U".Split(','); + private readonly Dictionary dictionaryReplacements = ("a=a;e=e;i=i;o=o;u=u;" + "b=b;ch=ch;d=d;f=f;g=g;gn=nh;k=k;l=l;ll=j;m=m;n=n;p=p;r=r;rr=rr;s=s;t=t;w=w;x=h;y=y;z=z;I=I;U=U;B=b;D=d;G=g;Y=y").Split(';') .Select(entry => entry.Split('=')) .Where(parts => parts.Length == 2) .Where(parts => parts[0] != parts[1]) .ToDictionary(parts => parts[0], parts => parts[1]); private readonly string[] longConsonants = "ch,dz,h,s,sh,k,p,rr,t,ts,z".Split(','); - private readonly string[] notClusters = "dz,hh,ll,nh,sh,zz,zh".Split(','); - private readonly string[] specialClusters = "by,dy,fy,gy,hy,jy,ky,ly,my,py,ry,rry,sy,ty,vy,zy,bw,chw,dw,fw,gw,hw,jw,kw,lw,llw,mw,nw,pw,rw,rrw,sw,tw,vw,zw,bl,fl,gl,kl,pl,br,dr,fr,gr,kr,pr,tr".Split(','); + private readonly string[] initialCC = "bl,bly,blw,br,bry,brw,by,bw,dr,dry,drw,dy,dw,fl,fly,flw,fr,fry,frw,fy,fw,gl,gly,glw,gr,gry,grw,gy,gw,kl,kly,klw,kr,kry,krw,ky,kw,pl,ply,plw,pr,pry,prw,py,pw,tl,tly,tlw,tr,try,trw,ty,tw,chy,chw,hy,hw,jy,jw,ly,lw,my,mw,ny,nw,ry,rw,sy,sw,vy,vw,zy,zw,nhy,nhw,rry,rrw,ts".Split(','); + private readonly string[] notClusters = "dz,hh,ll,nh,sh,zz,zh,th,ng".Split(','); + private readonly string[] burstConsonants = "ch,dz,j,k,p,t,ts,r".Split(','); + + // For banks with a seseo accent + private readonly Dictionary seseo = "z=s".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isSeseo = false; + + // For banks with alternate semivowel aliases + private readonly Dictionary semiVowelFallback = "jya=ja;jye=je;jyo=jo;jyu=ju;chya=cha;chye=che;chyo=cho;chyu=chu;nhya=nha;nhye=nhe;nhyo=nho;nhyu=nhu;nhwa=nua;nhwe=nue;nhwi=nui;nhwo=nuo;ya=ia;ye=ie;yo=io;yu=iu;wa=ua;we=ue;wi=ui;wo=uo".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isSemiVowelFallback = false; + + // For banks with alternate alias for "ñ" + private readonly Dictionary eñeFallback = "nhya=nya;nhye=nye;nhyo=nyo;nhyu=nyu;nhwa=nwa;nhwe=nwe;nhwi=nwi;nhwo=nwo;nha=nya;nhe=nye;nyi=ni;nhi=nyi;nho=nyo;nhu=nyu;a nh=a n;e nh=e n;i nh=i n;o nh=o n;u nh=u n;n nh=n n;l nh=l n;m nh=m n".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isEñeFallback = false; protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => "cmudict_es.txt"; - protected override IG2p LoadBaseDictionary() => new SpanishG2p(); + + protected override IG2p LoadBaseDictionary() { + var g2ps = new List(); + + // Load dictionary from singer folder. + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, "es-syl.yaml"); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + g2ps.Add(new SpanishG2p()); + return new G2pFallbacks(g2ps.ToArray()); + } protected override Dictionary GetDictionaryPhonemesReplacement() => dictionaryReplacements; protected override List ProcessSyllable(Syllable syllable) { @@ -46,14 +89,28 @@ protected override List ProcessSyllable(Syllable syllable) { string basePhoneme; var phonemes = new List(); + var lastC = cc.Length - 1; var firstC = 0; - var rcv = $"- {v}"; + + var rv = $"- {v}"; var vv1 = $"{prevV} {v}"; var vv2 = $"{prevV}{v}"; + + // Switch between phonetic systems, depending on certain aliases in the bank + if (!HasOto($"z{v}", syllable.tone) && !HasOto($"- z{v}", syllable.tone)) { + isSeseo = true; + } + if (!HasOto($"{cc.Length}y{v}", syllable.vowelTone) && !HasOto($"- {cc.Length}y{v}", syllable.vowelTone)) { + isSemiVowelFallback = true; + } + if (!HasOto($"nh{v}", syllable.vowelTone) && !HasOto($"- nh{v}", syllable.vowelTone)) { + isEñeFallback = true; + } + if (syllable.IsStartingV) { - basePhoneme = rcv; - if (!HasOto(rcv, syllable.vowelTone)) { + basePhoneme = rv; + if (!HasOto(rv, syllable.vowelTone)) { basePhoneme = $"{v}"; } } else if (syllable.IsVV) { @@ -71,275 +128,151 @@ protected override List ProcessSyllable(Syllable syllable) { } } else if (syllable.IsStartingCVWithOneConsonant) { // TODO: move to config -CV or -C CV - var rc = $"- {cc[0]}{v}"; - var src = $"- s{v}"; - if (HasOto(rc, syllable.vowelTone)) { - basePhoneme = rc; - } else if (cc[0] == "z" - && !HasOto(cc[0], syllable.vowelTone) - && HasOto(src, syllable.vowelTone)) { - basePhoneme = src; + var rcv = $"- {cc[0]}{v}"; + if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone)) { + basePhoneme = rcv; } else { basePhoneme = $"{cc[0]}{v}"; } } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { // try RCCV - var rvvc = $"- {string.Join("", cc)}{v}"; - var vvc = string.Join("", cc) + v; - if (HasOto(rvvc, syllable.vowelTone)) { - basePhoneme = rvvc; - } else if (!HasOto(rvvc, syllable.vowelTone) && HasOto(ValidateAlias(rvvc), syllable.vowelTone)) { - basePhoneme = ValidateAlias(rvvc); - } else if (!HasOto(ValidateAlias(rvvc), syllable.vowelTone) && HasOto(vvc, syllable.vowelTone)) { - basePhoneme = vvc; - } else if (!HasOto(vvc, syllable.vowelTone) && HasOto(ValidateAlias(vvc), syllable.vowelTone)) { - basePhoneme = ValidateAlias(vvc); + var rccv = $"- {string.Join("", cc)}{v}"; + var ccv = string.Join("", cc) + v; + var crv = $"{cc.Last()} {v}"; + if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (initialCC.Contains(string.Join("", cc)) || initialCC.Contains(ValidateAlias(string.Join("", cc))))) { + basePhoneme = rccv; + lastC = 0; + } else if (!HasOto(rccv, syllable.vowelTone) && !HasOto(ValidateAlias(rccv), syllable.vowelTone) && (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && (initialCC.Contains(string.Join("", cc)) || initialCC.Contains(ValidateAlias(string.Join("", cc))))) { + basePhoneme = ccv; + lastC = 0; } else { - basePhoneme = $"{cc.Last()}{v}"; + basePhoneme = ValidateAlias(crv); + if (!HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = $"{cc.Last()}{v}"; + } // try RCC for (var i = cc.Length; i > 1; i--) { - if (TryAddPhoneme(phonemes, syllable.tone, $"- {string.Join("", cc.Take(i))}", ValidateAlias($"- {string.Join("", cc.Take(i))}"), string.Join("", cc.Take(i)), ValidateAlias(string.Join("", cc.Take(i))))) { - firstC = i; - break; + var rcc = $"- {string.Join("", cc.Take(i))}"; + var cc1 = string.Join("", cc.Take(i)); + if (initialCC.Contains(string.Join("", cc.Take(i))) || initialCC.Contains(ValidateAlias(string.Join("", cc.Take(i))))) { + if (TryAddPhoneme(phonemes, syllable.tone, rcc, ValidateAlias(rcc), cc1, ValidateAlias(cc1))) { + firstC = i - 1; + break; + } } } if (phonemes.Count == 0) { TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); } - // try CCV - for (var i = firstC; i < cc.Length - 1; i++) { - var ccv = string.Join("", cc.Skip(i)) + v; - if (HasOto(ccv, syllable.tone)) { - basePhoneme = ccv; - lastC = i; - if (!HasOto(ccv, syllable.tone)) { - ccv = ValidateAlias(ccv); - basePhoneme = ccv; - lastC = i; - break; - } - break; - } - } } } else { // VCV var vcv = $"{prevV} {cc[0]}{v}"; var vccv = $"{prevV} {string.Join("", cc)}{v}"; - var nyvcv = $"{prevV} ny{v}"; - var svcv = $"{prevV} s{v}"; - var syvccv = $"{prevV} sy{v}"; - var swvccv = $"{prevV} sw{v}"; - if (HasOto(vcv, syllable.vowelTone) + if ((HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && (syllable.IsVCVWithOneConsonant)) { basePhoneme = vcv; - } else if (cc[0] == "nh" - && !HasOto(cc[0], syllable.vowelTone) - && HasOto(nyvcv, syllable.vowelTone) - && syllable.IsVCVWithOneConsonant) { - basePhoneme = nyvcv; - } else if (cc[0] == "z" - && !HasOto(cc[0], syllable.vowelTone) - && HasOto(svcv, syllable.vowelTone) - && syllable.IsVCVWithOneConsonant) { - basePhoneme = svcv; - } else if (string.Join("", cc) == "zy" - && !HasOto(string.Join("", cc), syllable.vowelTone) - && HasOto(syvccv, syllable.vowelTone) - && syllable.IsVCVWithMoreThanOneConsonant) { - basePhoneme = syvccv; - } else if (string.Join("", cc) == "zw" - && !HasOto(string.Join("", cc), syllable.vowelTone) - && HasOto(swvccv, syllable.vowelTone) - && syllable.IsVCVWithMoreThanOneConsonant) { - basePhoneme = swvccv; - } else if (HasOto(vccv, syllable.vowelTone) + } else if ((HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone)) && syllable.IsVCVWithMoreThanOneConsonant - && !notClusters.Contains(string.Join("", cc))) { + && (initialCC.Contains(string.Join("", cc)) || initialCC.Contains(ValidateAlias(string.Join("", cc))))) { basePhoneme = vccv; - } else { - basePhoneme = cc.Last() + v; + lastC = 0; + } else { + var cv = cc.Last() + v; + var crv = $"{cc.Last()} {v}"; + basePhoneme = cv; + if (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = crv; + } + // try CCV if (cc.Length - firstC > 1) { - for (var i = firstC; i < cc.Length; i++) { + for (var i = firstC; i < lastC; i++) { var ccv = $"{string.Join("", cc.Skip(i))}{v}"; var ccv2 = $"{string.Join(" ", cc.Skip(i))}{v}"; var ccv3 = $"{cc[0]} {string.Join("", cc.Skip(i))}{v}"; - if (HasOto(ccv, syllable.vowelTone) && !notClusters.Contains(string.Join("", cc.Skip(i)))) { + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && (initialCC.Contains(string.Join("", cc.Skip(i))) || initialCC.Contains(ValidateAlias(string.Join("", cc.Skip(i)))))) { lastC = i; basePhoneme = ccv; - if (!HasOto(ccv, syllable.vowelTone)) { - ccv = ValidateAlias(ccv); - lastC = i; - basePhoneme = ccv; - break; - } break; - } else if (HasOto(ccv2, syllable.vowelTone)) { + } else if (HasOto(ccv2, syllable.vowelTone) || HasOto(ValidateAlias(ccv2), syllable.vowelTone)) { lastC = i; basePhoneme = ccv2; - if (!HasOto(ccv2, syllable.vowelTone)) { - ccv2 = ValidateAlias(ccv); - lastC = i; - basePhoneme = ccv2; - break; - } break; - } else if (HasOto(ccv3, syllable.vowelTone) && !notClusters.Contains(string.Join("", cc.Skip(i)))) { + } else if ((HasOto(ccv3, syllable.vowelTone) || HasOto(ValidateAlias(ccv3), syllable.vowelTone)) && (initialCC.Contains(string.Join("", cc.Skip(i))) || initialCC.Contains(ValidateAlias(string.Join("", cc.Skip(i)))))) { lastC = i; basePhoneme = ccv3; - if (!HasOto(ccv3, syllable.vowelTone)) { - ccv3 = ValidateAlias(ccv3); - lastC = i; - basePhoneme = ccv3; - break; - } break; + } else { + continue; } } } - // try vc + // try vcc or vc for (var i = lastC + 1; i >= 0; i--) { var vc1 = $"{prevV} {cc[0]}"; var vc2 = $"{prevV}{cc[0]}"; - // try vcc - var vcc = $"{prevV} {string.Join("", cc.Take(i))}"; - var vcc2 = $"{prevV}{string.Join("", cc.Take(i))}"; - if (i == 0 && HasOto(vc1, syllable.tone)) { - phonemes.Add(vc1); - if (!HasOto(vc1, syllable.tone)) { - vc1 = ValidateAlias(vc1); - phonemes.Add(vc1); - break; - } - break; - } else if (HasOto(vcc, syllable.tone) && !notClusters.Contains(string.Join("", cc.Take(i)))) { + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vcc2 = $"{prevV}{string.Join("", cc.Take(2))}"; + if ((HasOto(ValidateAlias(vcc), syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && (!notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2)))))) { phonemes.Add(vcc); - firstC = i - 1; - if (!HasOto(vcc, syllable.tone)) { - vcc = ValidateAlias(vcc); - phonemes.Add(vcc); - firstC = i - 1; - break; - } + firstC = 1; break; - } else if (HasOto(vcc2, syllable.tone) && !notClusters.Contains(string.Join("", cc.Take(i)))) { + } else if ((HasOto(vcc2, syllable.tone) || HasOto(ValidateAlias(vcc2), syllable.tone)) && (!notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2)))))) { phonemes.Add(vcc2); - firstC = i - 1; - if (!HasOto(vcc2, syllable.tone)) { - vcc2 = ValidateAlias(vcc2); - phonemes.Add(vcc2); - firstC = i - 1; - break; - } + firstC = 1; + break; + } else if ((HasOto(vc1, syllable.tone) || HasOto(ValidateAlias(vc1), syllable.tone))) { + phonemes.Add(vc1); break; - } else if (!HasOto(vc1, syllable.tone) && HasOto(vc2, syllable.tone)) { + } else if (HasOto(vc2, syllable.tone) || HasOto(ValidateAlias(vc2), syllable.tone)) { phonemes.Add(vc2); - if (!HasOto(vc2, syllable.tone)) { - vc2 = ValidateAlias(vc1); - phonemes.Add(vc2); - break; - } break; - } else if (!HasOto(vc1, syllable.tone) && !HasOto(vc2, syllable.tone)) { - continue; - } - } - if (cc.Length - firstC > 1) { - for (var i = firstC; i < cc.Length; i++) { - var spccv = $"{string.Join("", cc.Skip(i))}{v}"; - var ccv3 = $"{cc[0]} {string.Join("", cc.Skip(i))}{v}"; - var syccv = $"{cc[0]} sy{v}"; - var swccv = $"{cc[0]} sw{v}"; - if (specialClusters.Contains(string.Join("", cc.Skip(i)))) { - lastC = i; - basePhoneme = spccv; - if (!HasOto(spccv, syllable.vowelTone)) { - spccv = ValidateAlias(spccv); - lastC = i; - basePhoneme = spccv; - } - } - if (specialClusters.Contains(string.Join("", cc.Skip(i))) && HasOto(ccv3, syllable.vowelTone)) { - lastC = i; - basePhoneme = ccv3; - if (!HasOto(ccv3, syllable.vowelTone)) { - ccv3 = ValidateAlias(ccv3); - lastC = i; - basePhoneme = ccv3; - } - } - if (string.Join("", cc) == "zy" - && !HasOto(string.Join("", cc), syllable.vowelTone) - && HasOto(syccv, syllable.vowelTone)) { - lastC = i; - basePhoneme = syccv; - } - if (string.Join("", cc) == "zw" - && !HasOto(string.Join("", cc), syllable.vowelTone) - && HasOto(swccv, syllable.vowelTone)) { - lastC = i; - basePhoneme = swccv; - } } } - } } - var rccv = $"- {string.Join("", cc)}{v}"; - if (!HasOto(rccv, syllable.vowelTone) && !HasOto(ValidateAlias(rccv), syllable.vowelTone)) { - for (var i = firstC; i < lastC; i++) { - // we could use some CCV, so lastC is used - // we could use -CC so firstC is used - var cc1 = $"{cc[i]} {cc[i + 1]}"; - var ncc1 = $"{cc[i]} n"; - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + for (var i = firstC; i < lastC; i++) { + // we could use some CCV, so lastC is used + // we could use -CC so firstC is used + var cc1 = $"{cc[i]}{cc[i + 1]}"; + + // Use [C1C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // Use [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + if (i + 1 < lastC) { + var cc2 = $"{cc[i + 1]}{cc[i + 2]}"; + + // Use [C2C3] + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); } - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]}{cc[i + 1]}"; + // Use [C2 C3] + if (!HasOto(cc2, syllable.tone)) { + cc2 = $"{cc[i + 1]} {cc[i + 2]}"; } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); } - if (i + 1 < lastC) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - var ncc2 = $"{cc[i + 1]} n"; - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - if (!HasOto(cc2, syllable.tone)) { - cc2 = $"{cc[i + 1]}{cc[i + 2]}"; - } - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - if (HasOto(cc1, syllable.tone) && HasOto(cc2, syllable.tone)) { - // like [V C1] [C1 C2] [C2 C3] [C3 ..] - phonemes.Add(cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, $"{cc[i]} {cc[i + 1]}-", ValidateAlias($"{cc[i]} {cc[i + 1]}-"))) { - // like [V C1] [C1 C2-] [C3 ..] - i++; - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1))) { - if (cc[i + 2] == "nh" && !HasOto(cc[i + 2], syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, ncc2, ValidateAlias(ncc2)); ; - } - // like [V C1] [C1 C2] [C2 ..] - } else if (!HasOto(cc1, syllable.tone) && firstC == i - 1 && cc[i] != cc[0]) { - TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); - } - if (!HasOto(cc2, syllable.tone) && firstC == i - 1 && cc[i + 1] != cc[1]) { - TryAddPhoneme(phonemes, syllable.tone, cc[i + 1], ValidateAlias(cc[i + 1])); - } + if ((HasOto(cc1, syllable.tone) || HasOto(ValidateAlias(cc1), syllable.tone)) && (HasOto(cc2, syllable.tone) || HasOto(ValidateAlias(cc2), syllable.tone))) { + // like [V C1] [C1 C2] [C2 C3] [C3 ..] + phonemes.Add(cc1); + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1))) { + // like [V C1] [C1 C2] [C2 ..] } else { - // like [V C1] [C1 C2] [C2 ..] or like [V C1] [C1 -] [C3 ..] - TryAddPhoneme(phonemes, syllable.tone, cc1); - if (cc[i + 1] == "nh" && !HasOto(cc[i + 1], syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, ncc1, ValidateAlias(ncc1)); - } - if (!HasOto(cc1, syllable.tone) && firstC == i - 1 && cc[i] != cc[0]) { - TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); - } + // like[V C1][C1][C2..] + TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); } + } else { + // like [V C1] [C1 C2] [C2 ..] or like [V C1] [C1 -] [C3 ..] + TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); } } @@ -352,56 +285,138 @@ protected override List ProcessEnding(Ending ending) { string v = ending.prevV; var phonemes = new List(); - if (ending.IsEndingV) { // ending V - TryAddPhoneme(phonemes, ending.tone, $"{v} R", $"{v} -", $"{v}-"); - } else if (ending.IsEndingVCWithOneConsonant) { // ending VC - TryAddPhoneme(phonemes, ending.tone, $"{v} {cc[0]}-", ValidateAlias($"{v} {cc[0]}-"), $"{v}{cc[0]}-", ValidateAlias($"{v}{cc[0]}-"), $"{v} {cc[0]}", ValidateAlias($"{v} {cc[0]}"), $"{v}{cc[0]}", ValidateAlias($"{v}{cc[0]}"), cc[0], ValidateAlias(cc[0])); - } else if (ending.IsEndingVCWithMoreThanOneConsonant) { // ending VCC (very rare, usually only occurs in words ending with "x") - var vcc = $"{v} {string.Join("", cc)}"; - var vcc2 = $"{v}{string.Join("", cc)}"; + + var lastC = cc.Length - 1; + var firstC = 0; + + if (ending.IsEndingV) { // ending V + TryAddPhoneme(phonemes, ending.tone, $"{v} -", $"{v}-", $"{v} R"); + } else if (ending.IsEndingVCWithOneConsonant) { // ending VC + var vcr = $"{v} {cc[0]}-"; + var vcr2 = $"{v}{cc[0]}-"; var vc = $"{v} {cc[0]}"; var vc2 = $"{v}{cc[0]}"; - var cc1 = $"{cc[0]} {cc[1]}"; - var cc2 = $"{cc[0]}{cc[1]}"; - TryAddPhoneme(phonemes, ending.tone, vcc, vcc2); - if (!HasOto(vcc, ending.tone) && !HasOto(vcc2, ending.tone) && HasOto(vc, ending.tone)) { - phonemes.Add(vc); - if (!HasOto(vc, ending.tone)) { - vc = ValidateAlias(vc); + if (HasOto(vcr, ending.tone) || HasOto(ValidateAlias(vcr), ending.tone)) { + phonemes.Add(vcr); + } else if (HasOto(vcr2, ending.tone) || HasOto(ValidateAlias(vcr2), ending.tone)) { + phonemes.Add(vcr2); + } else { + if (HasOto(vc, ending.tone) || HasOto(ValidateAlias(vc), ending.tone)) { phonemes.Add(vc); - } - TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1)); - if (!HasOto(cc1, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc2, ValidateAlias(cc2)); - } - if (!HasOto(cc1, ending.tone) && !HasOto(cc2, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc[0] + cc[1], ValidateAlias(cc[0] + cc[1])); - } - } - if (!HasOto(vcc, ending.tone) && !HasOto(vcc2, ending.tone) && !HasOto(vc, ending.tone) && HasOto(vc2, ending.tone)) { - phonemes.Add(vc2); - if (!HasOto(vc2, ending.tone)) { - vc2 = ValidateAlias(vc2); + } else if (HasOto(vc2, ending.tone) || HasOto(ValidateAlias(vc2), ending.tone)) { phonemes.Add(vc2); } - TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1)); - if (!HasOto(cc1, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc2, ValidateAlias(cc2)); - } else if (!HasOto(cc1, ending.tone) && !HasOto(cc2, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc[0] + cc[1], ValidateAlias(cc[0] + cc[1])); + if (burstConsonants.Contains(cc[0]) || burstConsonants.Contains(ValidateAlias(cc[0]))) { + TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} -", $"{ValidateAlias(cc[0])} -", cc[0], ValidateAlias(cc[0])); + } else { + TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} -", $"{ValidateAlias(cc[0])} -", $"{cc[0]}-", $"{ValidateAlias(cc[0])}-", $"{cc[0]} R", $"{ValidateAlias(cc[0])} R"); } } - if (!HasOto(vcc, ending.tone) && !HasOto(vcc2, ending.tone) && !HasOto(vc, ending.tone) && !HasOto(vc2, ending.tone)) { - phonemes.Add(cc[0]); - if (!HasOto(cc[0], ending.tone)) { - cc[0] = ValidateAlias(cc[0]); - phonemes.Add(cc[0]); + } else { // ending VCC (very rare, usually only occurs in words ending with "x") + for (var i = lastC; i >= 0; i--) { + var vcc = $"{v} {string.Join("", cc.Take(2))}-"; + var vcc3 = $"{v}{string.Join("", cc.Take(2))}"; + var vcc2 = $"{v} {string.Join("", cc.Take(2))}"; + var vcc4 = $"{v}{string.Join(" ", cc.Take(2))}-"; + var vc = $"{v} {cc[0]}"; + var vc2 = $"{v}{cc[0]}"; + if ((HasOto(vcc, ending.tone) || HasOto(ValidateAlias(vcc), ending.tone)) && lastC == 1 && !notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2))))) { + phonemes.Add(vcc); + firstC = 1; + break; + } else if ((HasOto(vcc2, ending.tone) || HasOto(ValidateAlias(vcc2), ending.tone)) && !notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2))))) { + phonemes.Add(vcc2); + firstC = 1; + break; + } else if ((HasOto(vcc3, ending.tone) || HasOto(ValidateAlias(vcc3), ending.tone)) && !notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2))))) { + phonemes.Add(vcc3); + firstC = 1; + break; + } else if ((HasOto(vcc4, ending.tone) || HasOto(ValidateAlias(vcc4), ending.tone)) && lastC == 1 && !notClusters.Contains(string.Join("", cc.Take(2))) && !notClusters.Contains(ValidateAlias(string.Join("", cc.Take(2))))) { + phonemes.Add(vcc4); + firstC = 1; + break; + } else + if (HasOto(vc, ending.tone) || HasOto(ValidateAlias(vc), ending.tone)) { + phonemes.Add(vc); + break; + } else { + TryAddPhoneme(phonemes, ending.tone, vc2, ValidateAlias(vc2)); + break; } - TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1)); - if (!HasOto(cc1, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc2, ValidateAlias(cc2)); - } else if (!HasOto(cc1, ending.tone) && !HasOto(cc2, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, cc[1], ValidateAlias(cc[1])); + } + + for (var i = firstC; i < lastC; i++) { + // all CCs except the first one are /C1C2/, the last one is /C1 C2-/ + // but if there is no /C1C2/, we try /C1 C2-/, vise versa for the last one + var cc1 = $"{cc[i]} {cc[i + 1]}"; + if (i < cc.Length - 2) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone) && !notClusters.Contains($"{cc[i]}{cc[i + 1]}") && !notClusters.Contains(ValidateAlias($"{cc[i]}{cc[i + 1]}"))) { + cc1 = $"{cc[i]}{cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = $"{cc[i + 1]}{cc[i + 2]}"; + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone)) || HasOto($"{cc[i + 1]}{cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]}{cc[i + 2]}-"), ending.tone)) { + // like [C1 C2][C2 ...] + phonemes.Add(cc1); + } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone) || HasOto($"{cc[i + 1]}{cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]}{cc[i + 2]}-"), ending.tone)))) { + // like [C1 C2-][C3 ...] + phonemes.Add(cc[i]); + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}-", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), $"{cc[i + 1]}{cc[i + 2]}-", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}-"))) { + // like [C1 C2-][C3 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { + // like [C1C2][C2 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + i++; + } else { + // like [C1][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} -", ValidateAlias($"{cc[i]} -")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -")); + i++; + } + } else { + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = $"{cc[i]}{cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}-", ValidateAlias($"{cc[i]} {cc[i + 1]}-"), $"{cc[i]}{cc[i + 1]}-", ValidateAlias($"{cc[i]}{cc[i + 1]}-"))) { + // like [C1 C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + // like [C1 C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}", ValidateAlias($"{cc[i]}{cc[i + 1]}"))) { + // like [C1C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else { + // like [C1][C2 -] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} -", ValidateAlias($"{cc[i]} -")); + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } } } } @@ -409,43 +424,38 @@ protected override List ProcessEnding(Ending ending) { } protected override string ValidateAlias(string alias) { - foreach (var consonant in new[] { "j" }) { - alias = alias.Replace(consonant, "ll"); - } - foreach (var consonant in specialClusters) { - alias = alias.Replace("w", "u"); + // Validate alias depending on method + if (isSeseo) { + foreach (var syllable in seseo) { + alias = alias.Replace(syllable.Key, syllable.Value); + } } - foreach (var consonant in new[] { "z" }) { - alias = alias.Replace(consonant, "s"); + if (isSemiVowelFallback) { + foreach (var syllable in semiVowelFallback) { + alias = alias.Replace(syllable.Key, syllable.Value); + } } - foreach (var consonant in specialClusters) { - alias = alias.Replace("y", "i"); + if (isEñeFallback) { + foreach (var syllable in eñeFallback) { + alias = alias.Replace(syllable.Key, syllable.Value); + } } - foreach (var consonant in new[] { "nh" }) { - alias = alias.Replace(consonant, "ny"); + + // Other validations + if (alias.Contains("I")) { + alias = alias.Replace("I", "i"); } - foreach (var consonant in new[] { " nh" }) { - alias = alias.Replace(consonant, " n"); + if (alias.Contains("U")) { + alias = alias.Replace("U", "u"); } - foreach (var vowel in vowels) { - foreach (var consonant in new[] { " ny" }) { - return alias.Replace(vowel + consonant, vowel + " n"); - } + foreach (var cc in new[] { "ks" }) { + alias = alias.Replace("ks", "x"); } - foreach (var consonant1 in consonants) { - foreach (var consonant2 in new[] { " ny" }) { - return alias.Replace(consonant1 + consonant2, consonant1 + " n"); - } - } - foreach (var vowel in vowels) { - foreach (var consonant in new[] { " nh" }) { - return alias.Replace(vowel + consonant, vowel + " n"); - } + if (alias == "r") { + alias = alias.Replace("r", "rr"); } - foreach (var consonant1 in consonants) { - foreach (var consonant2 in new[] { " nh" }) { - return alias.Replace(consonant1 + consonant2, consonant1 + " n"); - } + if (alias == "ch") { + alias = alias.Replace("ch", "tch"); } return alias; } diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index f6a13f31a..72b94c289 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -171,7 +171,12 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } } - // Assign pitch/color suffixes + return new Result() { + phonemes = AssignAllAffixes(phonemes, notes, prevNeighbours) + }; + } + + protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { int noteIndex = 0; for (int i = 0; i < phonemes.Count; i++) { var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; @@ -183,10 +188,10 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN noteIndex++; } var noteStartPosition = notes[noteIndex].position - notes[0].position; - int tone = (prevNeighbours != null && prevNeighbours.Length > 0 && phoneme.position < noteStartPosition) ? - prevNeighbours.Last().tone : (noteIndex > 0 && phoneme.position < noteStartPosition) ? - notes[noteIndex-1].tone : notes[noteIndex].tone; - + int tone = (prevs != null && prevs.Length > 0 && phoneme.position < noteStartPosition) ? + prevs.Last().tone : (noteIndex > 0 && phoneme.position < noteStartPosition) ? + notes[noteIndex - 1].tone : notes[noteIndex].tone; + var validatedAlias = phoneme.phoneme; if (validatedAlias != null) { validatedAlias = ValidateAliasIfNeeded(validatedAlias, tone + toneShift); @@ -200,10 +205,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN phonemes[i] = phoneme; } - - return new Result() { - phonemes = phonemes.ToArray() - }; + return phonemes.ToArray(); } private Result HandleError() { diff --git a/OpenUtau/Controls/ExpSelector.axaml b/OpenUtau/Controls/ExpSelector.axaml index 2681d8cd5..f4914d030 100644 --- a/OpenUtau/Controls/ExpSelector.axaml +++ b/OpenUtau/Controls/ExpSelector.axaml @@ -64,8 +64,9 @@ IsOpen="{TemplateBinding IsDropDownOpen, Mode=TwoWay}" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MaxHeight="{TemplateBinding MaxDropDownHeight}" - PlacementTarget="Background" - PlacementAnchor="BottomLeft" + Placement="AnchorAndGravity" + PlacementAnchor="Top" + PlacementGravity="TopRight" IsLightDismissEnabled="True"> - +