From 9c07e906faca1cfaeaf1f824fa29fe33a41a041b Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 4 Oct 2025 15:59:18 +0800 Subject: [PATCH 01/24] Instance-based dictionary --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 2d79fa9ca..cda27915b 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -233,7 +233,7 @@ public override void SetSinger(USinger singer) { protected bool isDictionaryLoading => dictionaries[GetType()] == null; protected double TransitionBasicLengthMs => 100; - private static Dictionary dictionaries = new Dictionary(); + private Dictionary dictionaries = new Dictionary(); private const string FORCED_ALIAS_SYMBOL = "?"; private string error = ""; private readonly string[] wordSeparators = new[] { " ", "_" }; From ff17204711b29e5ff801833a1585adddaf413de9 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Thu, 19 Mar 2026 11:43:58 +0800 Subject: [PATCH 02/24] SBP Update --- .../SyllableBasedPhonemizer.cs | 144 +++++++++++++++--- 1 file changed, 120 insertions(+), 24 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index cda27915b..1fa367070 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -160,14 +160,19 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } var phonemes = new List(); + int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI + foreach (var syllable in syllables) { - phonemes.AddRange(MakePhonemes(ProcessSyllable(syllable), syllable.duration, syllable.position, false)); + var syllablePhonemes = ProcessSyllable(syllable); + phonemes.AddRange(MakePhonemes(syllablePhonemes, syllable.duration, syllable.position, false, syllable.tone, notes[0].phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += syllablePhonemes.Count; } if (!nextNeighbour.HasValue) { var tryEnding = MakeEnding(notes); if (tryEnding.HasValue) { var ending = tryEnding.Value; - phonemes.AddRange(MakePhonemes(ProcessEnding(ending), ending.duration, ending.position, true)); + var endingPhonemes = ProcessEnding(ending); + phonemes.AddRange(MakePhonemes(endingPhonemes, ending.duration, ending.position, true, ending.tone, notes[0].phonemeAttributes, globalPhonemeIndex)); } } @@ -187,11 +192,17 @@ protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] note while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { noteIndex++; } - var noteStartPosition = notes[noteIndex].position - notes[0].position; - 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 noteStartPosition = notes[noteIndex].position - notes[0].position; + int tone; + if (phoneme.position < noteStartPosition) { + tone = (noteIndex > 0) ? notes[noteIndex - 1].tone : + (prevs != null && prevs.Length > 0) ? prevs.Last().tone : + notes[noteIndex].tone; + } else { + tone = notes[noteIndex].tone; + } + var validatedAlias = phoneme.phoneme; if (validatedAlias != null) { validatedAlias = ValidateAliasIfNeeded(validatedAlias, tone + toneShift); @@ -304,7 +315,7 @@ string[] getSymbolsRaw(string lyrics) { foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { var subResult = dictionary.Query(subword); if (subResult == null) { - Log.Warning($"Subword '{subword}' from word '{note.lyric}' can't be found in the dictionary"); + //Log.Warning($"Subword '{subword}' from word '{note.lyric}' can't be found in the dictionary"); subResult = HandleWordNotFound(note); if (subResult == null) { return null; @@ -318,6 +329,16 @@ string[] getSymbolsRaw(string lyrics) { } } + /// + /// Defines whether a consonant (like a liquid or semi-vowel etc) should be placed ON the note (anchor) + /// instead of pushing backward. + /// + protected virtual bool IsGlide(string alias) { + return false; + } + + protected virtual bool NoGap => true; + /// /// Instead of changing symbols in cmudict itself for each reclist, /// you may leave it be and provide symbol replacements with this method. @@ -546,6 +567,33 @@ protected double GetTransitionBasicLengthMsByConstant() { return TransitionBasicLengthMs * GetTempoNoteLengthFactor(); } + /// + /// Uses Preutterance length + /// + protected virtual double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + return GetTransitionBasicLengthMs(alias); + } + + /// + /// OTO HELPER: Calculates transition length based on the mapped Oto's Preutterance. + /// + protected double GetTransitionBasicLengthMsByOto(string alias, int tone = 0, PhonemeAttributes attr = default) { + if (string.IsNullOrEmpty(alias)) return GetTransitionBasicLengthMsByConstant(); + + string color = attr.voiceColor ?? string.Empty; + string alt = attr.alternate?.ToString() ?? string.Empty; + int toneShift = attr.toneShift; + + var validatedAlias = ValidateAliasIfNeeded(alias, tone + toneShift); + var mappedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + if (singer.TryGetMappedOto(mappedAlias, tone + toneShift, out var oto)) { + return oto.Preutter; + } + + return GetTransitionBasicLengthMsByConstant(); + } + /// /// a note length modifier, from 1 to 0.3. Used to make transition notes shorter on high tempo /// @@ -752,33 +800,76 @@ private List ExtractVowels(string[] symbols) { } return vowelIds; } - - private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int position, bool isEnding) { - + + private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int position, bool isEnding, int tone = 0, PhonemeAttributes[] attributes = null, int globalStartIndex = 0) { var phonemes = new Phoneme[phonemeSymbols.Count]; + + int[] trueLengths = new int[phonemeSymbols.Count]; + for (int i = 1; i < phonemeSymbols.Count; i++) { + var prevPhonemeI = phonemeSymbols.Count - i; + var nextGlobalIndex = globalStartIndex + prevPhonemeI; + var nextPAttr = attributes?.FirstOrDefault(a => a.index == nextGlobalIndex) ?? default; + double nextStretch = nextPAttr.consonantStretchRatio ?? 1.0; + + string nextAlias = phonemeSymbols[prevPhonemeI]; + double baseLengthMs = GetTransitionBasicLengthMs(nextAlias, tone, nextPAttr); + trueLengths[i] = MsToTick(baseLengthMs * nextStretch); + } + + // IsGlide + int anchorI = 0; + if (!isEnding) { + for (int i = 1; i < phonemeSymbols.Count; i++) { + var phonemeI = phonemeSymbols.Count - i - 1; + if (phonemeSymbols[phonemeI] != null && IsGlide(phonemeSymbols[phonemeI])) { + anchorI = i; + } else { + break; + } + } + } + for (var i = 0; i < phonemeSymbols.Count; i++) { var phonemeI = phonemeSymbols.Count - i - 1; - + var globalIndex = globalStartIndex + phonemeI; var validatedAlias = phonemeSymbols[phonemeI]; + if (validatedAlias != null) { phonemes[phonemeI].phoneme = validatedAlias; - var transitionLengthTick = MsToTick(GetTransitionBasicLengthMs(phonemes[phonemeI].phoneme)); + if (i == 0) { - if (!isEnding) { - transitionLengthTick = 0; + if (isEnding) { + var pAttr = attributes?.FirstOrDefault(a => a.index == globalIndex) ?? default; + double baseLengthMs = GetTransitionBasicLengthMs(phonemes[phonemeI].phoneme, tone, pAttr); + + if (NoGap) { + // Snapped mode: Use a visible 50-tick anchor capped at 1/3 of the note + int targetTicks = 50; + int maxAllowed = containerLength / 3; + phonemes[phonemeI].position = System.Math.Min(targetTicks, maxAllowed); + } else { + // Natural mode: Use the full Preutterance (Right Blank space) + // Useful when the endings has a sound like those VC-'s in VCCV + phonemes[phonemeI].position = MsToTick(baseLengthMs); + } } else { - transitionLengthTick *= 2; + int sum = 0; + for (int k = 1; k <= anchorI; k++) { + sum += trueLengths[k]; + } + phonemes[phonemeI].position = -sum; } + } else { + // VC transitions keep their full length. + phonemes[phonemeI].position = trueLengths[i]; } - // yet it's actually a length; will became position in ScalePhonemes - phonemes[phonemeI].position = transitionLengthTick; } else { phonemes[phonemeI].phoneme = null; phonemes[phonemeI].position = 0; } } - - return ScalePhonemes(phonemes, position, isEnding ? phonemeSymbols.Count : phonemeSymbols.Count - 1, containerLength); + + return ScalePhonemes(phonemes, position, isEnding ? phonemeSymbols.Count - 1 : phonemeSymbols.Count - 1, containerLength); } private string ValidateAliasIfNeeded(string alias, int tone) { @@ -790,18 +881,23 @@ private string ValidateAliasIfNeeded(string alias, int tone) { private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { var offset = 0; - // reserved length for prev vowel, double length of a transition; - var containerSafeLengthTick = MsToTick(GetTransitionBasicLengthMsByConstant() * 2); var lengthModifier = 1.0; + if (containerLengthTick > 0) { var allTransitionsLengthTick = phonemes.Sum(n => n.position); - if (allTransitionsLengthTick + containerSafeLengthTick > containerLengthTick) { - lengthModifier = (double)containerLengthTick / (allTransitionsLengthTick + containerSafeLengthTick); + + // Instead of a fixed "Constant * 2", use a proportional limit. + // This allows transitions to occupy up to 80% of the note. + var maxAllowedConsonantTick = (int)(containerLengthTick * 0.8); + + if (allTransitionsLengthTick > maxAllowedConsonantTick) { + lengthModifier = (double)maxAllowedConsonantTick / allTransitionsLengthTick; } } for (var i = phonemes.Length - 1; i >= 0; i--) { - var finalLengthTick = (int)(phonemes[i].position * lengthModifier) / 5 * 5; + if (phonemes[i].phoneme == null) continue; + var finalLengthTick = (int)(phonemes[i].position * lengthModifier); phonemes[i].position = startPosition - finalLengthTick - offset; offset += finalLengthTick; } From ed9e3683ede570798b0a18d799044254a7a89f76 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Thu, 19 Mar 2026 12:41:18 +0800 Subject: [PATCH 03/24] implement GetTransitionBasicLengthMsByOto per child phonemizer --- .../ArpasingPlusPhonemizer.cs | 126 +++-------------- OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs | 8 ++ OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs | 9 ++ OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 126 +++-------------- .../EnglishVCCVPhonemizer.cs | 8 ++ OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs | 8 ++ OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 127 +++--------------- .../FrenchCVVCPhonemizer.cs | 18 +-- .../FrenchVCCVPhonemizer.cs | 18 +-- .../GermanVCCVPhonemizer.cs | 13 +- .../ItalianSyllableBasedPhonemizer.cs | 9 ++ .../PolishCVCPhonemizer.cs | 9 ++ .../RussianCVCPhonemizer.cs | 19 +-- .../RussianVCCVPhonemizer.cs | 19 +-- .../SpanishMakkusanPhonemizer.cs | 9 ++ .../SpanishSyllableBasedPhonemizer.cs | 19 +-- .../SpanishVCCVPhonemizer.cs | 19 +-- 17 files changed, 160 insertions(+), 404 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 4d487e9ad..8ed6b91b3 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -2034,123 +2034,33 @@ protected override string ValidateAlias(string alias) { return base.ValidateAlias(alias); } - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; + // Endings has 50 ticks gap + protected override bool NoGap => true; - // Exact token match - if (alias == phoneme) - return true; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - // consonant timings + var tokens = alias.Split(' ') + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList(); var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } - } - - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } - - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } - - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } - - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (symbol.Contains(" ")) { + if (alias.Replace("-", "").Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; + } + } + else if (tokens.Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; } } - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs index 911aac120..36973d76a 100644 --- a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs @@ -460,5 +460,13 @@ private string ToHiragana(string romaji) { hiragana = hiragana.Replace("ゔ", "ヴ"); return hiragana; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs index 8fe5cca1d..b6929062b 100644 --- a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs @@ -669,5 +669,14 @@ private string ToHiragana(string romaji) { hiragana = hiragana.Replace("ゔ", "ヴ"); return hiragana; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 79e6c3f41..eebe9ca8e 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -1199,123 +1199,33 @@ protected override string ValidateAlias(string alias) { return alias; } - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; + // Endings has 50 ticks gap + protected override bool NoGap => true; - // Exact token match - if (alias == phoneme) - return true; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - // consonant timings + var tokens = alias.Split(' ') + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList(); var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } - } - - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } - } - - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } - - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } - - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } - - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (symbol.Contains(" ")) { + if (alias.Replace("-", "").Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; + } + } + else if (tokens.Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; } } - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs index 46ff7d527..bbebc5f61 100644 --- a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs @@ -1024,5 +1024,13 @@ protected override string ValidateAlias(string alias) { return alias; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs index 02e2e62b1..8927f4395 100644 --- a/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs @@ -513,5 +513,13 @@ private string ToHiragana(string romaji) { hiragana = hiragana.Replace("ゔ", "ヴ"); return hiragana; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 08bd83d05..c09ed0fa5 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -1237,128 +1237,37 @@ protected override string ValidateAlias(string alias) { alias = alias.Replace(fb.Key, fb.Value); } } - return alias; return base.ValidateAlias(alias); } - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) - return false; - - // Exact token match - if (alias == phoneme) - return true; - - return alias.EndsWith(phoneme); - } - - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } - - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier - - var fricative_def = 2.3; - var aspirate_def = 1.3; - var semivowel_def = 1.2; - var liquid_def = 1.5; - var nasal_def = 1.5; - var stop_def = 1.8; - var tap_def = 0.5; - var affricate_def = 1.5; - - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } + // Endings has 50 ticks gap + protected override bool NoGap => true; - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - // consonant timings + var tokens = alias.Split(' ') + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList(); var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } - } - - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } - } - - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } - } - - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } - } - - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } - } - - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } - } - - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (symbol.Contains(" ")) { + if (alias.Replace("-", "").Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; + } + } + else if (tokens.Contains(symbol)) { + return GetTransitionBasicLengthMsByConstant() * value; } } - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs index 973907a1f..135e76349 100644 --- a/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FrenchCVVCPhonemizer.cs @@ -593,18 +593,12 @@ private string CheckCoeEnding(string cv, int tone) { return "no Coe Ending"; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs() * 1.25; + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + return otoLength; } private string CheckAliasFormatting(string alias, string type, int tone, string prevV) { diff --git a/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs index 15a36996e..9d03ca2f2 100644 --- a/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FrenchVCCVPhonemizer.cs @@ -253,19 +253,13 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } + // Endings has 50 ticks gap + protected override bool NoGap => true; - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs() * 1.25; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } protected override string[] GetSymbols(Note note) { diff --git a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs index 7a05e817f..53ea9ef32 100644 --- a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs @@ -666,13 +666,12 @@ protected override string ValidateAlias(string alias) { return alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in longConsonants) { - if (alias.Contains(c)) { - return base.GetTransitionBasicLengthMs() * 2.0; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs index 62e57407d..1617a3f03 100644 --- a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs @@ -197,5 +197,14 @@ protected override string ValidateAlias(string alias) { } return alias; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs index ed6d96598..bb292b8c5 100644 --- a/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/PolishCVCPhonemizer.cs @@ -66,5 +66,14 @@ protected override List ProcessEnding(Ending ending) { return phonemes; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs index 46f70d4ad..1a62dfaf8 100644 --- a/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/RussianCVCPhonemizer.cs @@ -105,18 +105,13 @@ protected override string ValidateAlias(string alias) { return aliasesFallback.ContainsKey(alias) ? aliasesFallback[alias] : alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } diff --git a/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs index 515bbed72..505246560 100644 --- a/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/RussianVCCVPhonemizer.cs @@ -124,18 +124,13 @@ protected override string ValidateAlias(string alias) { return alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 1.5; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs index ea5244d7e..9ff7e6357 100644 --- a/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishMakkusanPhonemizer.cs @@ -248,5 +248,14 @@ protected override string ValidateAlias(string alias) { } return alias; } + + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; + } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs index e350abf21..76ac43fc0 100644 --- a/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishSyllableBasedPhonemizer.cs @@ -460,18 +460,13 @@ protected override string ValidateAlias(string alias) { return alias; } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in longConsonants) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 2.0; - } - } - foreach (var c in new[] { "r" }) { - if (alias.EndsWith(c)) { - return base.GetTransitionBasicLengthMs() * 0.75; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } diff --git a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs index eafd6eeb9..124c9fc46 100644 --- a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs @@ -681,18 +681,13 @@ protected override string ValidateAlias(string alias) { return base.ValidateAlias(alias); } - protected override double GetTransitionBasicLengthMs(string alias = "") { - foreach (var c in shortConsonants) { - if (alias.Contains(c) && !alias.Contains("rr") && !alias.StartsWith(c) && !alias.Contains("ar") && !alias.Contains("er") && !alias.Contains("ir") && !alias.Contains("or") && !alias.Contains("ur")) { - return base.GetTransitionBasicLengthMs() * 0.50; - } - } - foreach (var c in longConsonants) { - if (alias.Contains(c) && !alias.StartsWith(c) && !alias.StartsWith("-" + c)) { - return base.GetTransitionBasicLengthMs() * 2.0; - } - } - return base.GetTransitionBasicLengthMs(); + // Endings has 50 ticks gap + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } From 38cd838f6d2af904c89cdf6764cb8efa2f17a4f6 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Thu, 19 Mar 2026 12:55:46 +0800 Subject: [PATCH 04/24] Fix DeVCCV test file to reflect correct pitch suffix --- OpenUtau.Test/Plugins/DeVccvTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenUtau.Test/Plugins/DeVccvTest.cs b/OpenUtau.Test/Plugins/DeVccvTest.cs index 44789c6cb..6e7adf00e 100644 --- a/OpenUtau.Test/Plugins/DeVccvTest.cs +++ b/OpenUtau.Test/Plugins/DeVccvTest.cs @@ -23,7 +23,7 @@ protected override Phonemizer CreatePhonemizer() { [InlineData("de_vccv", new string[] { "Mond", "+", "+", "+", "Licht", "+" }, new string[] { "G3", "D3", "G3", "G3", "D3", "G3" }, - new string[] { "- moG3", "onG3", "nt -G3", "t lG3", "lID3", "ICG3", "Ct -G3" })] + new string[] { "- moG3", "onG3", "nt -G3", "t lG3", "lID3", "ICD3", "Ct -G3" })] public void PhonemizeTest(string singerName, string[] lyrics, string[] tones, string[] aliases) { RunPhonemizeTest(singerName, lyrics, RepeatString(lyrics.Length, ""), tones, RepeatString(lyrics.Length, ""), aliases); } From 962e7855771fb61992995b7e60c3e39330adb2ad Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 21 Mar 2026 11:36:53 +0800 Subject: [PATCH 05/24] Fix phoneme overrides method --- OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs | 12 +----------- OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 12 +----------- OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 12 +----------- 3 files changed, 3 insertions(+), 33 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 8ed6b91b3..13460ad92 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -2040,22 +2040,12 @@ protected override string ValidateAlias(string alias) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - var tokens = alias.Split(' ') - .Select(t => t.Trim()) - .Where(t => !string.IsNullOrEmpty(t)) - .ToList(); - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { var symbol = kvp.Key; var value = kvp.Value; - if (symbol.Contains(" ")) { - if (alias.Replace("-", "").Contains(symbol)) { - return GetTransitionBasicLengthMsByConstant() * value; - } - } - else if (tokens.Contains(symbol)) { + if (alias.Contains(symbol)) { return GetTransitionBasicLengthMsByConstant() * value; } } diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index eebe9ca8e..ee595e428 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -1205,22 +1205,12 @@ protected override string ValidateAlias(string alias) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - var tokens = alias.Split(' ') - .Select(t => t.Trim()) - .Where(t => !string.IsNullOrEmpty(t)) - .ToList(); - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { var symbol = kvp.Key; var value = kvp.Value; - if (symbol.Contains(" ")) { - if (alias.Replace("-", "").Contains(symbol)) { - return GetTransitionBasicLengthMsByConstant() * value; - } - } - else if (tokens.Contains(symbol)) { + if (alias.Contains(symbol)) { return GetTransitionBasicLengthMsByConstant() * value; } } diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index c09ed0fa5..474edbf42 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -1247,22 +1247,12 @@ protected override string ValidateAlias(string alias) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - var tokens = alias.Split(' ') - .Select(t => t.Trim()) - .Where(t => !string.IsNullOrEmpty(t)) - .ToList(); - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); foreach (var kvp in sortedOverrides) { var symbol = kvp.Key; var value = kvp.Value; - if (symbol.Contains(" ")) { - if (alias.Replace("-", "").Contains(symbol)) { - return GetTransitionBasicLengthMsByConstant() * value; - } - } - else if (tokens.Contains(symbol)) { + if (alias.Contains(symbol)) { return GetTransitionBasicLengthMsByConstant() * value; } } From ae5d8a226cb0d97bd4d6669324400073daff25bb Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 29 Mar 2026 10:41:24 +0800 Subject: [PATCH 06/24] fix EN C+V ending timings --- .../EnglishCpVPhonemizer.cs | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index d24cb72a1..8e48f8334 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -1115,17 +1115,8 @@ bool PhonemeIsPresent(string alias, string phoneme) { return alias.EndsWith(phoneme); } - private bool PhonemeHasEndingSuffix(string alias, string phoneme) { - var escapedPhoneme = Regex.Escape(phoneme); - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b\s*-") || - Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b-")) { - return true; - } - if (Regex.IsMatch(alias, $@"\b{escapedPhoneme}\b R")) { - return true; - } - return false; - } + + protected override bool NoGap => true; protected override double GetTransitionBasicLengthMs(string alias = "") { //I wish these were automated instead :') @@ -1162,17 +1153,6 @@ protected override double GetTransitionBasicLengthMs(string alias = "") { } } - foreach (var c in allConsonants) { - if (PhonemeHasEndingSuffix(alias, c)) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } foreach (var c in fricative) { if (PhonemeIsPresent(alias, c)) { From 27b10cce5edc8666530a0ef83ba3b58faf054f76 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 29 Mar 2026 13:24:00 +0800 Subject: [PATCH 07/24] Utilize ValidateAlias on AliasFormat --- .../ArpasingPlusPhonemizer.cs | 34 +++--- .../EnglishCpVPhonemizer.cs | 106 ++++++++++++++++-- OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 33 +++--- 3 files changed, 130 insertions(+), 43 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 13460ad92..fad5389a7 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -1196,7 +1196,6 @@ protected override List ProcessEnding(Ending ending) { } private string AliasFormat(string alias, string type, int tone, string prevV) { var aliasFormats = new Dictionary { - // Define alias formats for different types { "dynStart", new string[] { "" } }, { "dynMid", new string[] { "" } }, { "dynMid_vv", new string[] { "" } }, @@ -1217,12 +1216,10 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, }; - // Check if the given type exists in the aliasFormats dictionary if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { return alias; } - // Handle dynamic variations when type contains "dynamic" if (type.Contains("dynStart")) { string consonant = ""; string vowel = ""; @@ -1234,10 +1231,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { } else { consonant = alias; } - - // Handle the alias with space and without space var dynamicVariations = new List { - // Variations with space, dash, and underscore $"- {consonant}{vowel}", // "- CV" $"- {consonant} {vowel}", // "- C V" $"-{consonant} {vowel}", // "-C V" @@ -1245,10 +1239,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"-{consonant}_{vowel}", // "-C_V" $"- {consonant}_{vowel}", // "- C_V" }; - // Check each dynamically generated format + foreach (var variation in dynamicVariations) { - if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { + if (HasOto(variation, tone)) { return variation; + } else if (HasOto(ValidateAlias(variation), tone)) { + return ValidateAlias(variation); } } } @@ -1256,7 +1252,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { if (type.Contains("dynMid")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[0]; @@ -1269,10 +1265,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"{consonant} {vowel}", // "C V" $"{consonant}_{vowel}", // "C_V" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); } } } @@ -1280,7 +1278,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { if (type.Contains("dynEnd")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[1]; @@ -1294,10 +1292,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"{vowel}{consonant}-", // "VC-" $"{vowel} {consonant} -", // "V C -" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); } } } @@ -1315,9 +1315,11 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { } else { aliasFormat = $"{format}{alias}"; } - // Check if the formatted alias exists - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + + if (HasOto(aliasFormat, tone)) { return aliasFormat; + } else if (HasOto(ValidateAlias(aliasFormat), tone)) { + return ValidateAlias(aliasFormat); } } return alias; diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index 8e48f8334..17f8d27c9 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -1055,28 +1055,110 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { }; - // Check if the given type exists in the aliasFormats dictionary - if (!aliasFormats.ContainsKey(type)) { + if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { return alias; } - // Get the array of possible alias formats for the specified type + + if (type.Contains("dynStart")) { + string consonant = ""; + string vowel = ""; + // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[0]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations = new List { + $"- {consonant}{vowel}", // "- CV" + $"- {consonant} {vowel}", // "- C V" + $"-{consonant} {vowel}", // "-C V" + $"-{consonant}{vowel}", // "-CV" + $"-{consonant}_{vowel}", // "-C_V" + $"- {consonant}_{vowel}", // "- C_V" + }; + + foreach (var variation in dynamicVariations) { + if (HasOto(variation, tone)) { + return variation; + } else if (HasOto(ValidateAlias(variation), tone)) { + return ValidateAlias(variation); + } + } + } + + if (type.Contains("dynMid")) { + string consonant = ""; + string vowel = ""; + + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[0]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{consonant}{vowel}", // "CV" + $"{consonant} {vowel}", // "C V" + $"{consonant}_{vowel}", // "C_V" + }; + + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone)) { + return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); + } + } + } + + if (type.Contains("dynEnd")) { + string consonant = ""; + string vowel = ""; + + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[1]; + vowel = parts[0]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{vowel}{consonant} -", // "VC -" + $"{vowel} {consonant}-", // "V C-" + $"{vowel}{consonant}-", // "VC-" + $"{vowel} {consonant} -", // "V C -" + }; + + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone)) { + return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); + } + } + } + + // Get the array of possible alias formats for the specified type if not dynamic var formatsToTry = aliasFormats[type]; int counter = 0; foreach (var format in formatsToTry) { string aliasFormat; if (type.Contains("mix") && counter < 4) { - // Alternate between alias + format and format + alias for the first 4 iterations - aliasFormat = (counter % 2 == 0) ? alias + format : format + alias; + aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; counter++; - } else if (type.Contains("end")) { - aliasFormat = alias + format; + } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { + aliasFormat = $"{alias}{format}"; } else { - aliasFormat = format + alias; + aliasFormat = $"{format}{alias}"; } - // Check if the formatted alias exists using HasOto and ValidateAlias - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { - alias = aliasFormat; - return alias; + + if (HasOto(aliasFormat, tone)) { + return aliasFormat; + } else if (HasOto(ValidateAlias(aliasFormat), tone)) { + return ValidateAlias(aliasFormat); } } return alias; diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 474edbf42..a9e51543a 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -1118,12 +1118,10 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, }; - // Check if the given type exists in the aliasFormats dictionary if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { return alias; } - // Handle dynamic variations when type contains "dynamic" if (type.Contains("dynStart")) { string consonant = ""; string vowel = ""; @@ -1135,10 +1133,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { } else { consonant = alias; } - - // Handle the alias with space and without space var dynamicVariations = new List { - // Variations with space, dash, and underscore $"- {consonant}{vowel}", // "- CV" $"- {consonant} {vowel}", // "- C V" $"-{consonant} {vowel}", // "-C V" @@ -1146,10 +1141,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"-{consonant}_{vowel}", // "-C_V" $"- {consonant}_{vowel}", // "- C_V" }; - // Check each dynamically generated format + foreach (var variation in dynamicVariations) { - if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { + if (HasOto(variation, tone)) { return variation; + } else if (HasOto(ValidateAlias(variation), tone)) { + return ValidateAlias(variation); } } } @@ -1157,7 +1154,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { if (type.Contains("dynMid")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[0]; @@ -1170,10 +1167,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"{consonant} {vowel}", // "C V" $"{consonant}_{vowel}", // "C_V" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); } } } @@ -1181,7 +1180,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { if (type.Contains("dynEnd")) { string consonant = ""; string vowel = ""; - // If the alias contains a space, split it into consonant and vowel + if (alias.Contains(" ")) { var parts = alias.Split(' '); consonant = parts[1]; @@ -1195,10 +1194,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { $"{vowel}{consonant}-", // "VC-" $"{vowel} {consonant} -", // "V C -" }; - // Check each dynamically generated format + foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + if (HasOto(variation1, tone)) { return variation1; + } else if (HasOto(ValidateAlias(variation1), tone)) { + return ValidateAlias(variation1); } } } @@ -1216,9 +1217,11 @@ private string AliasFormat(string alias, string type, int tone, string prevV) { } else { aliasFormat = $"{format}{alias}"; } - // Check if the formatted alias exists - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + + if (HasOto(aliasFormat, tone)) { return aliasFormat; + } else if (HasOto(ValidateAlias(aliasFormat), tone)) { + return ValidateAlias(aliasFormat); } } return alias; From 9ea15af0c0a8ecaed2ffa2c9617f73e22fb438d0 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Tue, 31 Mar 2026 21:06:26 +0800 Subject: [PATCH 08/24] Fix override timings --- OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs | 13 +++++++++++-- OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 2 +- OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index fad5389a7..bf50934ff 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -2035,10 +2035,19 @@ protected override string ValidateAlias(string alias) { } return base.ValidateAlias(alias); } + bool PhonemeIsPresent(string alias, string phoneme) { + if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) + return false; + + // Exact token match + if (alias == phoneme) + return true; + + return alias.EndsWith(phoneme); + } // Endings has 50 ticks gap protected override bool NoGap => true; - protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); @@ -2047,7 +2056,7 @@ protected override double GetTransitionBasicLengthMs(string alias, int tone, Pho var symbol = kvp.Key; var value = kvp.Value; - if (alias.Contains(symbol)) { + if (Regex.IsMatch(alias, $@"(? Date: Thu, 2 Apr 2026 07:53:09 +0800 Subject: [PATCH 09/24] Fix empty array bug --- .../ArpasingPlusPhonemizer.cs | 16 ++++++++-------- OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 16 ++++++++-------- .../EnglishCpVPhonemizer.cs | 16 ++++++++-------- OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs | 18 +++++++++--------- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index bf50934ff..8d6c9f07d 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -28,14 +28,14 @@ public class ArpasingPlusPhonemizer : SyllableBasedPhonemizer { "eu", "oe", "yw", "yx", "wx", "ox", "ex", "ea", "ia", "oa", "ua", "ean", "eam", "eang" }; private string[] consonants = "b,ch,d,dh,dr,dx,f,g,hh,jh,k,l,m,n,ng,p,q,r,s,sh,t,th,tr,v,w,y,z".Split(','); - private static string[] affricate = "".Split(','); - private static string[] fricative = "".Split(','); - private static string[] aspirate = "".Split(','); - private static string[] semivowel = "".Split(','); - private static string[] liquid = "".Split(','); - private static string[] nasal = "".Split(','); - private static string[] stop = "".Split(','); - private static string[] tap = "".Split(','); + private static string[] affricate = Array.Empty(); + private static string[] fricative = Array.Empty(); + private static string[] aspirate = Array.Empty(); + private static string[] semivowel = Array.Empty(); + private static string[] liquid = Array.Empty(); + private static string[] nasal = Array.Empty(); + private static string[] stop = Array.Empty(); + private static string[] tap = Array.Empty(); private Dictionary PhonemeOverrides = new Dictionary(); protected override string[] GetVowels() => vowels; diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 194b02c37..a1b30e72f 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -30,14 +30,14 @@ namespace OpenUtau.Plugin.Builtin { public class EnXSampaPhonemizer : SyllableBasedPhonemizer { private 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,ol,ul,Ul,oUl,@5,u5,O5,A5,E5,I5,i5,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 static string[] affricate = "".Split(','); - private static string[] fricative = "".Split(','); - private static string[] aspirate = "".Split(','); - private static string[] semivowel = "".Split(','); - private static string[] liquid = "".Split(','); - private static string[] nasal = "".Split(','); - private static string[] stop = "".Split(','); - private static string[] tap = "".Split(','); + private static string[] affricate = Array.Empty(); + private static string[] fricative = Array.Empty(); + private static string[] aspirate = Array.Empty(); + private static string[] semivowel = Array.Empty(); + private static string[] liquid = Array.Empty(); + private static string[] nasal = Array.Empty(); + private static string[] stop = Array.Empty(); + private static string[] tap = Array.Empty(); private Dictionary PhonemeOverrides = new Dictionary(); private Dictionary dictionaryReplacements = ("aa=A;ae={;ah=V;ao=O;aw=aU;ax=@;ay=aI;" + diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index 17f8d27c9..c2209e405 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -30,14 +30,14 @@ public class EnglishCpVPhonemizer : SyllableBasedPhonemizer { private static string[] diphthongs = { "ay", "ey", "oy", "aw", "ow" }; private static string[] c_cR = { "n" }; private static string[] consonants = "b,ch,d,dh,dr,dx,f,g,hh,jh,k,l,m,n,ng,p,q,r,s,sh,t,th,tr,v,w,y,z".Split(','); - private static string[] affricate = "".Split(','); - private static string[] fricative = "".Split(','); - private static string[] aspirate = "".Split(','); - private static string[] semivowel = "".Split(','); - private static string[] liquid = "".Split(','); - private static string[] nasal = "".Split(','); - private static string[] stop = "".Split(','); - private static string[] tap = "".Split(','); + private static string[] affricate = Array.Empty(); + private static string[] fricative = Array.Empty(); + private static string[] aspirate = Array.Empty(); + private static string[] semivowel = Array.Empty(); + private static string[] liquid = Array.Empty(); + private static string[] nasal = Array.Empty(); + private static string[] stop = Array.Empty(); + private static string[] tap = Array.Empty(); protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; protected override string GetDictionaryName() => ""; diff --git a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index d0beea622..04b3e2d0b 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -18,15 +18,15 @@ public class FilipinoPhonemizer : SyllableBasedPhonemizer { private string[] vowels = { "a", "e", "i", "o", "u", "ay", "ey", "oy", "uy", "aw", "ew", "ow", "iw" }; - private string[] consonants = "".Split(','); - private static string[] affricate = "".Split(','); - private static string[] fricative = "".Split(','); - private static string[] aspirate = "".Split(','); - private static string[] semivowel = "".Split(','); - private static string[] liquid = "".Split(','); - private static string[] nasal = "".Split(','); - private static string[] stop = "".Split(','); - private static string[] tap = "".Split(','); + private string[] consonants = Array.Empty(); + private static string[] affricate = Array.Empty(); + private static string[] fricative = Array.Empty(); + private static string[] aspirate = Array.Empty(); + private static string[] semivowel = Array.Empty(); + private static string[] liquid = Array.Empty(); + private static string[] nasal = Array.Empty(); + private static string[] stop = Array.Empty(); + private static string[] tap = Array.Empty(); private Dictionary PhonemeOverrides = new Dictionary(); protected override string[] GetVowels() => vowels; protected override string[] GetConsonants() => consonants; From 25e4d302adaf2b67e37fe8c9cbda9f737009db72 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Thu, 9 Apr 2026 18:32:49 +0800 Subject: [PATCH 10/24] LANG2JA phonemizers: fix timings for VCV banks --- OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs | 26 ++++++++++++++++++++ OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs | 25 +++++++++++++++++++ OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs | 26 ++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs index 36973d76a..bc5ed0b2c 100644 --- a/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ENtoJAPhonemizer.cs @@ -466,6 +466,32 @@ private string ToHiragana(string romaji) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var parts = alias.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + bool isVcv = false; + + if (parts.Length == 2) { + var startingVowels = new[] { "a", "i", "u", "e", "o", "n", "N", "-" }; + var endingVowels = vowels; + + // First part must be a vowel (or a rest) + if (startingVowels.Contains(parts[0])) { + string cv = parts[1]; + + // Second part must end in a vowel (Romaji CV) OR be Japanese (Hiragana/Katakana) + bool isRomajiVcv = endingVowels.Contains(cv.Last().ToString()); + bool isJapaneseVcv = cv.Any(c => c > 0xFF); + + if (isRomajiVcv || isJapaneseVcv) { + isVcv = true; + } + } + } + + if (isVcv) { + return GetTransitionBasicLengthMsByConstant() * 1.0; + } + return otoLength; } } diff --git a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs index b6929062b..2e837ed4f 100644 --- a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs @@ -676,6 +676,31 @@ private string ToHiragana(string romaji) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + var parts = alias.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + bool isVcv = false; + + if (parts.Length == 2) { + var startingVowels = new[] { "a", "i", "u", "e", "o", "n", "N", "-" }; + var endingVowels = vowels; + + // First part must be a vowel (or a rest) + if (startingVowels.Contains(parts[0])) { + string cv = parts[1]; + + // Second part must end in a vowel (Romaji CV) OR be Japanese (Hiragana/Katakana) + bool isRomajiVcv = endingVowels.Contains(cv.Last().ToString()); + bool isJapaneseVcv = cv.Any(c => c > 0xFF); + + if (isRomajiVcv || isJapaneseVcv) { + isVcv = true; + } + } + } + + if (isVcv) { + return GetTransitionBasicLengthMsByConstant() * 1.0; + } + return otoLength; } } diff --git a/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs index 8927f4395..cd20f323b 100644 --- a/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs @@ -519,6 +519,32 @@ private string ToHiragana(string romaji) { protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var parts = alias.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + bool isVcv = false; + + if (parts.Length == 2) { + var startingVowels = new[] { "a", "i", "u", "e", "o", "n", "N", "-" }; + var endingVowels = vowels; + + // First part must be a vowel (or a rest) + if (startingVowels.Contains(parts[0])) { + string cv = parts[1]; + + // Second part must end in a vowel (Romaji CV) OR be Japanese (Hiragana/Katakana) + bool isRomajiVcv = endingVowels.Contains(cv.Last().ToString()); + bool isJapaneseVcv = cv.Any(c => c > 0xFF); + + if (isRomajiVcv || isJapaneseVcv) { + isVcv = true; + } + } + } + + if (isVcv) { + return GetTransitionBasicLengthMsByConstant() * 1.0; + } + return otoLength; } } From 1e7fb1eefbe4ff820f60ffa4c955e16a288405ba Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 2 May 2026 13:44:06 +0800 Subject: [PATCH 11/24] Fix conflicts with SBP --- .../SyllableBasedPhonemizer.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 46d6a6b3f..3d77168da 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -167,7 +167,7 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN var phonemes = new List(); int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI - + foreach (var syllable in syllables) { var modifiedSyllable = ApplyBoundaryReplacements(syllable); @@ -185,11 +185,15 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN var endingPhonemes = ProcessEnding(ending); if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false)); + phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += endingPhonemes.Count; } continue; } - phonemes.AddRange(MakePhonemes(ProcessSyllable(modifiedSyllable), modifiedSyllable.duration, modifiedSyllable.position, false)); + + var syllablePhonemes = ProcessSyllable(modifiedSyllable); + phonemes.AddRange(MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += syllablePhonemes.Count; } if (!nextNeighbour.HasValue) { @@ -205,7 +209,8 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN var endingPhonemes = ProcessEnding(modifiedEnding); if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedEnding.duration, modifiedEnding.position, true)); + phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedEnding.duration, modifiedEnding.position, true, ending.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += endingPhonemes.Count; } } } @@ -1334,7 +1339,10 @@ private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, var validatedAlias = phonemeSymbols[phonemeI]; if (validatedAlias != null) { - phonemes[phonemeI].phoneme = validatedAlias; + phonemes[phonemeI] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex + }; if (i == 0) { if (isEnding) { @@ -1347,8 +1355,7 @@ private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int maxAllowed = containerLength / 3; phonemes[phonemeI].position = System.Math.Min(targetTicks, maxAllowed); } else { - // Natural mode: Use the full Preutterance (Right Blank space) - // Useful when the endings has a sound like those VC-'s in VCCV + // Natural mode: Use the full Preutterance phonemes[phonemeI].position = MsToTick(baseLengthMs); } } else { @@ -1359,12 +1366,16 @@ private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, phonemes[phonemeI].position = -sum; } } else { - // VC transitions keep their full length. + // VC transitions keep their full stretched length phonemes[phonemeI].position = trueLengths[i]; } } else { - phonemes[phonemeI].phoneme = null; - phonemes[phonemeI].position = 0; + // Initialize empty slots properly to avoid null crashes + phonemes[phonemeI] = new Phoneme { + phoneme = null, + position = 0, + index = globalIndex + }; } } From d9de4d8c2ac37ad580c542bd71de351a10f5d818 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 3 May 2026 08:18:16 +0800 Subject: [PATCH 12/24] Fixes in EN VCCV and ES VCCV --- .../EnglishVCCVPhonemizer.cs | 31 ++++++++++++------- .../SpanishVCCVPhonemizer.cs | 8 +++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs index 7c22a5717..bff1dda6b 100644 --- a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs @@ -175,7 +175,7 @@ protected override string[] GetSymbols(Note note) { public override void SetSinger(USinger singer) { base.SetSinger(singer); - if (this.singer == null) return; + if (this.singer == null || !this.singer.Loaded) return; string file = null; if (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) { @@ -184,24 +184,31 @@ public override void SetSinger(USinger singer) { file = Path.Combine(PluginDir, YamlFileName); } - if (string.IsNullOrEmpty(file) || !File.Exists(file)) return; + string yamlContent = null; + if (!string.IsNullOrEmpty(file) && File.Exists(file)) { + yamlContent = File.ReadAllText(file); + } else if (YamlTemplate != null) { + yamlContent = System.Text.Encoding.UTF8.GetString(YamlTemplate); + } - try { - var data = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(file)); - if (data?.vcvowels != null) { - vcVowels.Clear(); - foreach (var kvp in data.vcvowels) { - if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(kvp.Value)) { - vcVowels[kvp.Key] = kvp.Value; + if (!string.IsNullOrEmpty(yamlContent)) { + try { + var data = Core.Yaml.DefaultDeserializer.Deserialize(yamlContent); + if (data?.vcvowels != null) { + vcVowels.Clear(); + foreach (var kvp in data.vcvowels) { + if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(kvp.Value)) { + vcVowels[kvp.Key] = kvp.Value; + } } } + } catch (Exception ex) { + Log.Error($"Failed to load vcvowels from {YamlFileName}: {ex.Message}"); } - } catch (Exception ex) { - Log.Error($"Failed to load vcvowels from {YamlFileName}: {ex.Message}"); } } - private class VcVowelYAMLData { + private class VcVowelYAMLData: YAMLData { public Dictionary vcvowels { get; set; } = new Dictionary(); } // prioritize yaml replacements over dictionary replacements diff --git a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs index 98fd877d5..0d11450ae 100644 --- a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs @@ -477,8 +477,12 @@ protected override string ValidateAlias(string alias) { return base.ValidateAlias(alias); } - protected override double GetTransitionBasicLengthMs(string alias = "") { - return base.GetTransitionBasicLengthMs(); + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + return otoLength; } } } From 92e35e700acc53e748252af815d6b2ba412ae6d0 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 3 May 2026 08:44:13 +0800 Subject: [PATCH 13/24] Fix GetSymbols not updated from the pr conflict --- OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 10 ++++++---- OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs | 9 +++++---- OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs | 10 ++++++---- OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs | 11 +++++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 08f8521be..69e3b570f 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -205,6 +205,12 @@ protected override string[] GetSymbols(Note note) { return null; } List finalProcessedPhonemes = new List(); + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } // Splits diphthongs and affricates if not present in the bank string[] diphthongs = new[] { "aI", "eI", "OI", "aU", "oU", "VI", "VU", "@U", "ai", "ei", "Oi", "au", "ou", "Ou", "@u", }; @@ -227,10 +233,6 @@ private string ReplacePhoneme(string phoneme, int tone) { if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { return phoneme; } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } return phoneme; } diff --git a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs index bff1dda6b..155ba2e86 100644 --- a/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs @@ -156,6 +156,11 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } List finalProcessedPhonemes = new List(); string[] tr_dr = new[] { "tr", "dr"}; foreach (string s in original) { @@ -217,10 +222,6 @@ private string ReplacePhoneme(string phoneme, int tone) { if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { return phoneme; } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } return phoneme; } diff --git a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs index b8f72642e..03f324ac5 100644 --- a/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GermanVCCVPhonemizer.cs @@ -69,6 +69,12 @@ protected override string[] GetSymbols(Note note) { return null; } List finalProcessedPhonemes = new List(); + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } string[] diphthongs = new[] { "aU", "OY", "aI" }; foreach (string s in original) { @@ -87,10 +93,6 @@ private string ReplacePhoneme(string phoneme, int tone) { if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { return phoneme; } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } return phoneme; } diff --git a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs index 0d11450ae..fbe678c57 100644 --- a/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SpanishVCCVPhonemizer.cs @@ -68,6 +68,13 @@ protected override string[] GetSymbols(Note note) { if (original == null) { return null; } + + for (int i = 0; i < original.Length; i++) { + if (dictionaryReplacements.TryGetValue(original[i], out string replaced)) { + original[i] = replaced; + } + } + List finalProcessedPhonemes = new List(); foreach (string s in original) { switch (s) { @@ -85,10 +92,6 @@ private string ReplacePhoneme(string phoneme, int tone) { if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { return phoneme; } - // Otherwise, try to apply the dictionary replacement. - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } return phoneme; } From fce1b368378dbf183d65ca6402157af6cf480ac9 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 10 May 2026 16:40:14 +0800 Subject: [PATCH 14/24] Implement CustomParameters --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 3d77168da..d919d3569 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -215,8 +215,10 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } } + var finalPhonemes = AssignAllAffixes(phonemes, notes, prevNeighbours); + CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, finalPhonemes); return new Result() { - phonemes = AssignAllAffixes(phonemes, notes, prevNeighbours) + phonemes = finalPhonemes }; } @@ -861,6 +863,14 @@ protected bool IsShort(Ending ending) { return TickToMs(ending.duration) < GetTransitionBasicLengthMs() * 2; } + /// + /// Native API for child phonemizers to automatically apply expressions (vel, alt, clr, etc.) + /// This is called internally after all phonemes are generated and aligned, right before returning to the engine. + /// + protected virtual void CustomParameters(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours, Phoneme[] phonemes) { + // Base implementation does nothing. Child classes override this to implement custom logic. + } + /// /// Checks if mapped and validated alias exists in oto /// From 50ebbfdbc3b2e9748d1283adcf2c29f02a290701 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 10 May 2026 16:51:47 +0800 Subject: [PATCH 15/24] Don't include AssignAllAffixes in the CustomParameters --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index d919d3569..c17f33d11 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -215,8 +215,8 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } } + CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, phonemes.ToArray()); var finalPhonemes = AssignAllAffixes(phonemes, notes, prevNeighbours); - CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, finalPhonemes); return new Result() { phonemes = finalPhonemes }; From 68bd993b6be737cb343ac0619c1288457f0da692 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sun, 10 May 2026 17:49:58 +0800 Subject: [PATCH 16/24] small fixes --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index c17f33d11..eb4193b4d 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -215,8 +215,9 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } } - CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, phonemes.ToArray()); - var finalPhonemes = AssignAllAffixes(phonemes, notes, prevNeighbours); + var phonemesArray = phonemes.ToArray(); + CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, phonemesArray); + var finalPhonemes = AssignAllAffixes(phonemesArray.ToList(), notes, prevNeighbours); return new Result() { phonemes = finalPhonemes }; From ae0bbeef0bc9ad37b21ae228fadec1de2bd25419 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Fri, 15 May 2026 08:13:20 +0800 Subject: [PATCH 17/24] fix double ending consonant for EN C+V --- OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index 772eed607..a4a81e088 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -34,7 +34,7 @@ public EnglishCpVPhonemizer() { } private string[] diphthongs = Array.Empty(); - private static string[] c_cR = { "n" }; + private static string[] c_cR = Array.Empty(); protected override bool IsGroupKeyword(string rulePhoneme) { string baseGroup = rulePhoneme.Split(new[] { '!', '+' })[0]; @@ -245,6 +245,15 @@ public override void SetSinger(USinger singer) { } } } + + if (data?.symbols != null) { + string[] targetTypes = { "nasal", "liquid", "semivowel", "fricative", "aspirate" }; + c_cR = data.symbols + .Where(s => targetTypes.Contains(s.type?.ToLower())) + .Select(s => s.symbol) + .Distinct() + .ToArray(); + } } catch (Exception ex) { Log.Error($"Failed to parse custom diphthongs from {YamlFileName}: {ex.Message}"); From 3935493e842afdecd4d7ed05fb8b42cce79c074c Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Thu, 21 May 2026 17:39:01 +0800 Subject: [PATCH 18/24] Proper isGlide phoneme struct --- .../ArpasingPlusPhonemizer.cs | 6 +++ OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 6 +++ .../EnglishCpVPhonemizer.cs | 6 +++ .../SyllableBasedPhonemizer.cs | 37 +++++++++++++++++-- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 78f3ae071..c44c049f9 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -510,6 +510,9 @@ protected override List ProcessSyllable(Syllable syllable) { cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; lastC = i; } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } // CV } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); @@ -571,6 +574,9 @@ protected override List ProcessSyllable(Syllable syllable) { if (!phoneticHint && (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone))) { cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } // CV } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { basePhoneme = (AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, "")); diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 69e3b570f..e4df38d92 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -494,6 +494,9 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { cc1 = $"{string.Join("", cc.Skip(i))}"; } + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1) && liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } @@ -524,6 +527,9 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc2)) { cc2 = $"{string.Join("", cc.Skip(i))}"; } + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1) && liquid.Contains(cc[i + 2]) || semivowel.Contains(cc[i + 2])) { + glides(cc1); + } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index a4a81e088..1a2651d9a 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -442,6 +442,9 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""), syllable.vowelTone)) { cc1 = AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""); } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } // CV } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = AliasFormat(v, "cv", syllable.vowelTone, ""); @@ -474,6 +477,9 @@ protected override List ProcessSyllable(Syllable syllable) { if (HasOto(AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""), syllable.vowelTone)) { cc1 = AliasFormat($"{string.Join("", cc.Skip(i + 1))}", "cc", syllable.tone, ""); } + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } // CV } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = AliasFormat(v, "cv", syllable.vowelTone, ""); diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index eb4193b4d..96018d4ec 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -159,6 +159,8 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN if (hasDictionary && isDictionaryLoading) { return MakeSimpleResult(""); } + + runtimeGlides.Clear(); var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); if (syllables == null) { @@ -362,6 +364,7 @@ public override void SetSinger(USinger singer) { vowels = backupVowels.Concat(yamlVowels).Distinct().ToArray(); tails = (tails ?? Array.Empty()).Concat(data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol) ?? Array.Empty()).Distinct().ToArray(); + enableGlides = data?.isglides ?? true; fricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); aspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); @@ -435,6 +438,20 @@ public override void SetSinger(USinger singer) { private readonly string[] wordSeparators = new[] { " ", "_" }; private readonly string[] wordSeparator = new[] { " " }; + /// + /// A tracker to identify which phonemes were marked as glides dynamically. + /// + protected HashSet runtimeGlides = new HashSet(); + + /// + /// Flag a specific generated string as a glide during your ProcessSyllable / ProcessEnding loops. + /// + protected void glides(string alias) { + runtimeGlides.Add(alias); + } + + protected bool enableGlides = true; + /// /// Returns list of vowels /// @@ -504,7 +521,6 @@ string[] getSymbolsRaw(string lyrics) { foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { var subResult = dictionary.Query(subword); if (subResult == null) { - //Log.Warning($"Subword '{subword}' from word '{note.lyric}' can't be found in the dictionary"); subResult = HandleWordNotFound(note); if (subResult == null) { return null; @@ -529,10 +545,10 @@ string[] getSymbolsRaw(string lyrics) { /// /// Defines whether a consonant (like a liquid or semi-vowel etc) should be placed ON the note (anchor) - /// instead of pushing backward. + /// instead of pushing backward. Will return true if dynamically flagged using glides() or TryAddPhoneme(). /// protected virtual bool IsGlide(string alias) { - return false; + return runtimeGlides.Contains(alias) && enableGlides; } protected virtual bool NoGap => true; @@ -899,6 +915,20 @@ protected bool TryAddPhoneme(List sourcePhonemes, int tone, params strin return false; } + /// + /// Appends a phoneme and optionally marks it as a glide simultaneously. + /// + protected bool TryAddPhoneme(List sourcePhonemes, int tone, bool isGlide, params string[] targetPhonemes) { + foreach (var phoneme in targetPhonemes) { + if (HasOto(phoneme, tone)) { + sourcePhonemes.Add(phoneme); + if (isGlide) glides(phoneme); + return true; + } + } + return false; + } + /// /// if true, you can put phoneme as null so the previous alias will be extended /// @@ -957,6 +987,7 @@ protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { public class YAMLData { public string version { get; set; } + public bool? isglides { get; set; } public SymbolData[] symbols { get; set; } = Array.Empty(); public Replacement[] replacements { get; set; } = Array.Empty(); public Fallbacks[] fallbacks { get; set; } = Array.Empty(); From b85f96507c9bba4ca7323bceffc8dfeebf08c20f Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Fri, 29 May 2026 08:14:16 +0800 Subject: [PATCH 19/24] Add "null" boundary + phoneme group fixes --- .../SyllableBasedPhonemizer.cs | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 96018d4ec..0e0736c70 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -394,7 +394,7 @@ public override void SetSinger(USinger singer) { foreach (var replacement in data.replacements) { string ruleScope = string.IsNullOrEmpty(replacement.where) ? "inside" : replacement.where.ToLowerInvariant(); if (replacement.from is IEnumerable fromList) { - string[] fromArray = fromList.Select(item => item.ToString()).ToArray(); + string[] fromArray = fromList.Select(item => item.ToString() ?? "null").ToArray(); if (replacement.to is string toString) mergingReplacements.Add(new Replacement { from = fromArray, to = toString, where = ruleScope }); else if (replacement.to is IEnumerable toList) splittingReplacements.Add(new Replacement { from = fromArray, to = toList.Select(item => item.ToString()).ToArray(), where = ruleScope }); } else if (replacement.from is string fromString) { @@ -1006,7 +1006,7 @@ public class Replacement { public List FromList { get { if (from is string s) return new List { s }; - if (from is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); return new List(); } } @@ -1014,7 +1014,7 @@ public List FromList { public List ToList { get { if (to is string s) return new List { s }; - if (to is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); return new List(); } } @@ -1099,7 +1099,7 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo if (fromArray != null && fromArray.Length > 0 && idx + fromArray.Length <= inputPhonemes.Count) { bool match = true; - var captures = new Dictionary>(); + var captures = new Dictionary>(); for (int j = 0; j < fromArray.Length; j++) { string rulePh = fromArray[j]; @@ -1107,8 +1107,8 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo if (IsGroupKeyword(rulePh)) { if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(rulePh)) captures[rulePh] = new Queue(); - captures[rulePh].Enqueue(actualPh); + if (!captures.ContainsKey(rulePh)) captures[rulePh] = new List(); + captures[rulePh].Add(actualPh); } else { match = false; break; } @@ -1128,8 +1128,19 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo } if (toArray != null) { + var captureIndices = new Dictionary(); + foreach (string toPh in toArray) { - finalPhonemes.Add(IsGroupKeyword(toPh) && captures.ContainsKey(toPh) && captures[toPh].Count > 0 ? captures[toPh].Dequeue() : toPh); + if (IsGroupKeyword(toPh) && captures.ContainsKey(toPh) && captures[toPh].Count > 0) { + if (!captureIndices.ContainsKey(toPh)) captureIndices[toPh] = 0; + int cIdx = captureIndices[toPh]; + if (cIdx >= captures[toPh].Count) cIdx = captures[toPh].Count - 1; + + finalPhonemes.Add(captures[toPh][cIdx]); + captureIndices[toPh]++; + } else { + finalPhonemes.Add(toPh); + } } } @@ -1188,11 +1199,12 @@ private Syllable ApplyBoundaryReplacements(Syllable syllable) { bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); bool hasV = !string.IsNullOrEmpty(syllable.v); - if (hasPrevV) currentPhonemes.Add(syllable.prevV); + currentPhonemes.Add(hasPrevV ? syllable.prevV : "null"); + if (syllable.cc != null) currentPhonemes.AddRange(syllable.cc); if (hasV) currentPhonemes.Add(syllable.v); - bool isBoundary = hasPrevV && syllable.position == 0; + bool isBoundary = (hasPrevV && syllable.position == 0) || !hasPrevV; List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); string newPrevV = ""; @@ -1200,8 +1212,13 @@ private Syllable ApplyBoundaryReplacements(Syllable syllable) { List newCc = new List(); if (finalPhonemes.Count > 0) { - if (hasPrevV) { - newPrevV = finalPhonemes[0]; + string firstPh = finalPhonemes[0]; + + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; finalPhonemes.RemoveAt(0); } if (hasV && finalPhonemes.Count > 0) { @@ -1234,8 +1251,7 @@ private Ending ApplyBoundaryReplacements(Ending ending) { List currentPhonemes = new List(); bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); - - if (hasPrevV) currentPhonemes.Add(ending.prevV); + currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); if (ending.cc != null) currentPhonemes.AddRange(ending.cc); List finalPhonemes = ApplyReplacements(currentPhonemes, true); @@ -1244,8 +1260,12 @@ private Ending ApplyBoundaryReplacements(Ending ending) { List newCc = new List(); if (finalPhonemes.Count > 0) { - if (hasPrevV) { - newPrevV = finalPhonemes[0]; + string firstPh = finalPhonemes[0]; + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; finalPhonemes.RemoveAt(0); } newCc.AddRange(finalPhonemes); From e827c95bc38d2c0e0d9b2d4db8648d79ab2c0891 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 30 May 2026 17:38:07 +0800 Subject: [PATCH 20/24] Account for the negative overlap --- OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 0e0736c70..574fa3973 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -808,6 +808,11 @@ protected double GetTransitionBasicLengthMsByOto(string alias, int tone = 0, Pho var mappedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); if (singer.TryGetMappedOto(mappedAlias, tone + toneShift, out var oto)) { + // If overlap is negative, add that absolute duration to the preutterance + // to ensure the entire consonant timing is preserved. + if (oto.Overlap < 0) { + return oto.Preutter - oto.Overlap; + } return oto.Preutter; } From cf49163bd066e648cf916b3d00927c443f3c6a4e Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Sat, 30 May 2026 22:48:01 +0800 Subject: [PATCH 21/24] fix glides in en-xsampa --- OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index e4df38d92..b73b974fc 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -494,8 +494,10 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { cc1 = $"{string.Join("", cc.Skip(i))}"; } - if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1) && liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { - glides(cc1); + if (CurrentWordCc.Length >= 2) { + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); @@ -527,12 +529,14 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc2)) { cc2 = $"{string.Join("", cc.Skip(i))}"; } - if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1) && liquid.Contains(cc[i + 2]) || semivowel.Contains(cc[i + 2])) { - glides(cc1); - } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } + if (CurrentWordCc.Length >= 2) { + if (liquid.Contains(cc[i + 1]) || semivowel.Contains(cc[i + 1])) { + glides(cc1); + } + } // Use [C2C3] when current word has 2 consonants or more and [C2C3C4...] does not exist if (!HasOto(cc2, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc2)) { cc2 = $"{cc[i + 1]}{cc[i + 2]}"; From 032ae6481c96972d5ede1048a7fb177fee119a2b Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Mon, 1 Jun 2026 12:10:32 +0800 Subject: [PATCH 22/24] Concatenate phoneme symbols with replacements --- .../SyllableBasedPhonemizer.cs | 120 +++++++++++------- 1 file changed, 75 insertions(+), 45 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 574fa3973..7d2baebf1 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -1095,25 +1095,22 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo bool replaced = false; foreach (var rule in validRules) { - string[] fromArray = null; - if (rule.from is IList fromList) { - fromArray = fromList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.from is string[] strArr) { - fromArray = strArr; - } - - if (fromArray != null && fromArray.Length > 0 && idx + fromArray.Length <= inputPhonemes.Count) { + List fromArray = rule.FromList; + + if (fromArray != null && fromArray.Count > 0 && idx + fromArray.Count <= inputPhonemes.Count) { bool match = true; var captures = new Dictionary>(); - for (int j = 0; j < fromArray.Length; j++) { + for (int j = 0; j < fromArray.Count; j++) { string rulePh = fromArray[j]; string actualPh = inputPhonemes[idx + j]; - if (IsGroupKeyword(rulePh)) { + string baseRulePh = rulePh.Split(new[] { '!', '=', '+' })[0]; + + if (IsGroupKeyword(baseRulePh)) { if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(rulePh)) captures[rulePh] = new List(); - captures[rulePh].Add(actualPh); + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); } else { match = false; break; } @@ -1123,67 +1120,100 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo } if (match) { - string[] toArray = null; - if (rule.to is IList toList) { - toArray = toList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.to is string[] strArr) { - toArray = strArr; - } else if (rule.to is string toStr) { - toArray = new string[] { toStr }; - } + List toArray = rule.ToList; - if (toArray != null) { + if (toArray != null && toArray.Count > 0) { var captureIndices = new Dictionary(); foreach (string toPh in toArray) { - if (IsGroupKeyword(toPh) && captures.ContainsKey(toPh) && captures[toPh].Count > 0) { - if (!captureIndices.ContainsKey(toPh)) captureIndices[toPh] = 0; - int cIdx = captureIndices[toPh]; - if (cIdx >= captures[toPh].Count) cIdx = captures[toPh].Count - 1; + string[] parts = toPh.Split('+'); + string[] cleanParts = new string[parts.Length]; + string baseGroupTo = null; + + for (int k = 0; k < parts.Length; k++) { + int cutoff = parts[k].IndexOfAny(new[] { '!', '=' }); + cleanParts[k] = cutoff >= 0 ? parts[k].Substring(0, cutoff) : parts[k]; + + if (baseGroupTo == null && IsGroupKeyword(cleanParts[k])) { + baseGroupTo = cleanParts[k]; + } + } + + if (baseGroupTo != null && captures.ContainsKey(baseGroupTo) && captures[baseGroupTo].Count > 0) { + if (!captureIndices.ContainsKey(baseGroupTo)) captureIndices[baseGroupTo] = 0; + int cIdx = captureIndices[baseGroupTo]; + if (cIdx >= captures[baseGroupTo].Count) cIdx = captures[baseGroupTo].Count - 1; - finalPhonemes.Add(captures[toPh][cIdx]); - captureIndices[toPh]++; + string capturedPhoneme = captures[baseGroupTo][cIdx]; + string reconstructed = ""; + for (int k = 0; k < cleanParts.Length; k++) { + if (cleanParts[k] == baseGroupTo) { + reconstructed += capturedPhoneme; + } else { + reconstructed += cleanParts[k]; + } + } + finalPhonemes.Add(reconstructed); + captureIndices[baseGroupTo]++; } else { - finalPhonemes.Add(toPh); + finalPhonemes.Add(string.Join("", cleanParts)); } } } - idx += fromArray.Length; + idx += fromArray.Count; replaced = true; break; } } } + // Fallback for single-phoneme splitting rules if (!replaced && validSplits.Any()) { string currentPhoneme = inputPhonemes[idx]; bool singleReplaced = false; foreach (var rule in validSplits) { - if (rule.from is IList || rule.from is string[]) continue; + List fromArray = rule.FromList; + if (fromArray == null || fromArray.Count != 1) continue; - string rulePh = rule.from?.ToString(); - if (rulePh == null) continue; + string rulePh = fromArray[0]; + string baseRulePh = rulePh.Split(new[] { '!', '=', '+' })[0]; - if (IsGroupKeyword(rulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { + if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { - string[] toArray = null; - if (rule.to is IList toList) { - toArray = toList.Cast().Select(x => x?.ToString()).ToArray(); - } else if (rule.to is string[] strArr) { - toArray = strArr; - } + List toArray = rule.ToList; - if (toArray != null) { + if (toArray != null && toArray.Count > 0) { foreach(string toPh in toArray) { - finalPhonemes.Add(toPh == rulePh ? currentPhoneme : toPh); + string[] parts = toPh.Split('+'); + string[] cleanParts = new string[parts.Length]; + string baseGroupTo = null; + + for (int k = 0; k < parts.Length; k++) { + int cutoff = parts[k].IndexOfAny(new[] { '!', '=' }); + cleanParts[k] = cutoff >= 0 ? parts[k].Substring(0, cutoff) : parts[k]; + + if (baseGroupTo == null && IsGroupKeyword(cleanParts[k])) { + baseGroupTo = cleanParts[k]; + } + } + + if (baseGroupTo != null) { + string reconstructed = ""; + for (int k = 0; k < cleanParts.Length; k++) { + if (cleanParts[k] == baseGroupTo) { + reconstructed += currentPhoneme; + } else { + reconstructed += cleanParts[k]; + } + } + finalPhonemes.Add(reconstructed); + } else { + finalPhonemes.Add(string.Join("", cleanParts)); + } } singleReplaced = true; break; - } else if (rule.to is string toStr) { - finalPhonemes.Add(toStr == rulePh ? currentPhoneme : toStr); - singleReplaced = true; - break; } } } From 7f2e23deaf4b93e44cec8f09a72882c326cf12b0 Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Mon, 1 Jun 2026 12:33:32 +0800 Subject: [PATCH 23/24] replace + with & and added parenthesis for phoneme groupings --- .../SyllableBasedPhonemizer.cs | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 7d2baebf1..4c9c6781e 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -1029,17 +1029,21 @@ public List ToList { protected List splittingReplacements = new List(); protected virtual bool IsGroupKeyword(string rulePhoneme) { - string baseGroup = rulePhoneme.Split(new[] { '!', '=', '+' })[0]; + // Trim parentheses so "(vowel)" evaluates identically to "vowel" + string cleanRule = rulePhoneme.Trim('(', ')'); + string baseGroup = cleanRule.Split(new[] { '!', '=', '&' })[0]; return new[] { "vowel", "vowels", "consonant", "consonants", "affricate", "fricative", "aspirate", "semivowel", "liquid", "nasal", "stop", "tap" }.Contains(baseGroup); } protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { - string baseGroup = rulePhoneme.Split(new[] { '!', '=', '+' })[0]; - if (rulePhoneme.Contains("+")) { - string added = rulePhoneme.Substring(rulePhoneme.IndexOf('+') + 1).Split(new[] { '!', '=' })[0]; - // If it matches another group name, or a literal letter, it passes + string cleanRule = rulePhoneme.Trim('(', ')'); + string baseGroup = cleanRule.Split(new[] { '!', '=', '&' })[0]; + + // Replaced '+' with '&' for group addition + if (cleanRule.Contains("&")) { + string added = cleanRule.Substring(cleanRule.IndexOf('&') + 1).Split(new[] { '!', '=' })[0]; foreach (string inc in added.Split(',')) { if (IsGroupKeyword(inc) ? IsGroupMatch(inc, actualPhoneme) : inc == actualPhoneme) { return true; @@ -1047,7 +1051,6 @@ protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { } } - // BASE GROUP: If it wasn't an addition, it must belong to the base group. bool inBaseGroup = false; switch (baseGroup) { case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; @@ -1064,15 +1067,13 @@ protected virtual bool IsGroupMatch(string rulePhoneme, string actualPhoneme) { if (!inBaseGroup) return false; - // EXCLUSIONS (!): Reject if it's in the excluded list. - if (rulePhoneme.Contains("!")) { - string excluded = rulePhoneme.Substring(rulePhoneme.IndexOf('!') + 1).Split(new[] { '=', '+' })[0]; + if (cleanRule.Contains("!")) { + string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; if (excluded.Split(',').Contains(actualPhoneme)) return false; } - // RESTRICTIONS (=): Reject if an equals list exists, and the phoneme isn't in it. - if (rulePhoneme.Contains("=")) { - string restricted = rulePhoneme.Substring(rulePhoneme.IndexOf('=') + 1).Split(new[] { '!', '+' })[0]; + if (cleanRule.Contains("=")) { + string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; if (!restricted.Split(',').Contains(actualPhoneme)) return false; } @@ -1105,7 +1106,8 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo string rulePh = fromArray[j]; string actualPh = inputPhonemes[idx + j]; - string baseRulePh = rulePh.Split(new[] { '!', '=', '+' })[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; if (IsGroupKeyword(baseRulePh)) { if (IsGroupMatch(rulePh, actualPh)) { @@ -1126,16 +1128,22 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo var captureIndices = new Dictionary(); foreach (string toPh in toArray) { + // Split by + for concatenation string[] parts = toPh.Split('+'); string[] cleanParts = new string[parts.Length]; string baseGroupTo = null; for (int k = 0; k < parts.Length; k++) { - int cutoff = parts[k].IndexOfAny(new[] { '!', '=' }); - cleanParts[k] = cutoff >= 0 ? parts[k].Substring(0, cutoff) : parts[k]; + // Strip parenthesis to find the base group cleanly + string partNoParens = parts[k].Trim('(', ')'); + int cutoff = partNoParens.IndexOfAny(new[] { '!', '=', '&' }); + string potentialGroup = cutoff >= 0 ? partNoParens.Substring(0, cutoff) : partNoParens; - if (baseGroupTo == null && IsGroupKeyword(cleanParts[k])) { - baseGroupTo = cleanParts[k]; + if (baseGroupTo == null && IsGroupKeyword(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; // Store just the base group name + } else { + cleanParts[k] = partNoParens; // Store literals } } @@ -1145,6 +1153,7 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo if (cIdx >= captures[baseGroupTo].Count) cIdx = captures[baseGroupTo].Count - 1; string capturedPhoneme = captures[baseGroupTo][cIdx]; + string reconstructed = ""; for (int k = 0; k < cleanParts.Length; k++) { if (cleanParts[k] == baseGroupTo) { @@ -1177,7 +1186,8 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo if (fromArray == null || fromArray.Count != 1) continue; string rulePh = fromArray[0]; - string baseRulePh = rulePh.Split(new[] { '!', '=', '+' })[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { @@ -1190,11 +1200,15 @@ protected virtual List ApplyReplacements(List inputPhonemes, boo string baseGroupTo = null; for (int k = 0; k < parts.Length; k++) { - int cutoff = parts[k].IndexOfAny(new[] { '!', '=' }); - cleanParts[k] = cutoff >= 0 ? parts[k].Substring(0, cutoff) : parts[k]; + string partNoParens = parts[k].Trim('(', ')'); + int cutoff = partNoParens.IndexOfAny(new[] { '!', '=', '&' }); + string potentialGroup = cutoff >= 0 ? partNoParens.Substring(0, cutoff) : partNoParens; - if (baseGroupTo == null && IsGroupKeyword(cleanParts[k])) { - baseGroupTo = cleanParts[k]; + if (baseGroupTo == null && IsGroupKeyword(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; + } else { + cleanParts[k] = partNoParens; } } From b27c98d93fdc9c9f9ffa53f5e4d6c40878fe69da Mon Sep 17 00:00:00 2001 From: cadlaxa Date: Wed, 3 Jun 2026 10:02:10 +0800 Subject: [PATCH 24/24] Fix yaml timings --- .../EnglishCpVPhonemizer.cs | 74 ++++--------------- .../SyllableBasedPhonemizer.cs | 51 +++++++++++-- 2 files changed, 60 insertions(+), 65 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs index 1a2651d9a..fb22ad729 100644 --- a/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnglishCpVPhonemizer.cs @@ -899,91 +899,47 @@ bool PhonemeIsPresent(string alias, string phoneme) { protected override bool NoGap => true; - protected override double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier + protected override double GetTransitionMultiplier(string alias) { + double baseMultiplier = base.GetTransitionMultiplier(alias); + if (baseMultiplier != 1.0) { + return baseMultiplier; + } var fricative_def = 2.3; var aspirate_def = 1.3; var semivowel_def = 1.2; var liquid_def = 1.5; var nasal_def = 1.5; - var stop_def = 1.8; + var stop_def = 1.4; var tap_def = 0.5; var affricate_def = 1.5; - var allConsonants = fricative.Concat(aspirate) - .Concat(semivowel) - .Concat(liquid) - .Concat(nasal) - .Concat(stop) - .Concat(tap) - .Concat(affricate) - .Distinct(); // Ensure no duplicates - - - - // consonant timings - - var sortedOverrides = PhonemeOverrides.OrderByDescending(kv => kv.Key.Length); - foreach (var kvp in sortedOverrides) { - var overridePhoneme = kvp.Key; - var overrideValue = kvp.Value; - if (PhonemeIsPresent(alias, overridePhoneme)) { - return base.GetTransitionBasicLengthMs() * overrideValue; - } - } - - foreach (var c in fricative) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * fricative_def; - } + if (PhonemeIsPresent(alias, c)) return fricative_def; } - foreach (var c in aspirate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * aspirate_def; - } + if (PhonemeIsPresent(alias, c)) return aspirate_def; } - foreach (var c in semivowel) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * semivowel_def; - } + if (PhonemeIsPresent(alias, c)) return semivowel_def; } - foreach (var c in liquid) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * liquid_def; - } + if (PhonemeIsPresent(alias, c)) return liquid_def; } - foreach (var c in nasal) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * nasal_def; - } + if (PhonemeIsPresent(alias, c)) return nasal_def; } - foreach (var c in stop) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * stop_def; - } + if (PhonemeIsPresent(alias, c)) return stop_def; } - foreach (var c in tap) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * tap_def; - } + if (PhonemeIsPresent(alias, c)) return tap_def; } - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; - } + if (PhonemeIsPresent(alias, c)) return affricate_def; } - return base.GetTransitionBasicLengthMs() * transitionMultiplier; + return 1.0; } } } diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index 4c9c6781e..97be85b2c 100644 --- a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs @@ -378,7 +378,7 @@ public override void SetSinger(USinger singer) { var yamlConsonants = fricative.Concat(aspirate).Concat(semivowel).Concat(liquid).Concat(nasal).Concat(stop).Concat(tap).Concat(affricate).ToArray(); consonants = backupConsonants.Concat(yamlConsonants).Distinct().ToArray(); - PhonemeOverrides = data.timings?.ToDictionary(t => t.symbol, t => t.value) ?? new Dictionary(); + PhonemeOverrides = data.timings?.GroupBy(t => t.symbol).ToDictionary(g => g.Key, g => g.First().value) ?? new Dictionary(); if (backupDictionaryReplacements == null) { backupDictionaryReplacements = new Dictionary(dictionaryReplacements); } @@ -408,7 +408,10 @@ public override void SetSinger(USinger singer) { yamlFallbacks.Clear(); foreach (var df in data.fallbacks) { if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - yamlFallbacks[df.from] = df.to; + // Prevent duplicates: only use the first instance found + if (!yamlFallbacks.ContainsKey(df.from)) { + yamlFallbacks[df.from] = df.to; + } } } } @@ -787,6 +790,13 @@ protected double GetTransitionBasicLengthMsByConstant() { return TransitionBasicLengthMs * GetTempoNoteLengthFactor(); } + protected virtual double GetTransitionMultiplier(string alias) { + if (alias != null && PhonemeOverrides != null && PhonemeOverrides.TryGetValue(alias, out double overrideRatio)) { + return overrideRatio; + } + return 1.0; + } + /// /// Uses Preutterance length /// @@ -1422,13 +1432,30 @@ private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int[] trueLengths = new int[phonemeSymbols.Count]; for (int i = 1; i < phonemeSymbols.Count; i++) { var prevPhonemeI = phonemeSymbols.Count - i; + var currentPhonemeI = phonemeSymbols.Count - i - 1; + var nextGlobalIndex = globalStartIndex + prevPhonemeI; var nextPAttr = attributes?.FirstOrDefault(a => a.index == nextGlobalIndex) ?? default; - double nextStretch = nextPAttr.consonantStretchRatio ?? 1.0; string nextAlias = phonemeSymbols[prevPhonemeI]; - double baseLengthMs = GetTransitionBasicLengthMs(nextAlias, tone, nextPAttr); - trueLengths[i] = MsToTick(baseLengthMs * nextStretch); + string currentAlias = phonemeSymbols[currentPhonemeI]; + + double baseLengthMs; + double stretch = nextPAttr.consonantStretchRatio ?? 1.0; + + // Check if the alias has a YAML or Categorical multiplier + double overrideRatio = currentAlias != null ? GetTransitionMultiplier(currentAlias) : 1.0; + + if (overrideRatio != 1.0) { + // If there's a custom multiplier, use the Constant length to prevent giant envelopes + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + // Default behavior: use OTO preutterance + baseLengthMs = GetTransitionBasicLengthMsByOto(nextAlias, tone, nextPAttr); + } + + trueLengths[i] = MsToTick(baseLengthMs * stretch); } // IsGlide @@ -1458,8 +1485,20 @@ private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, if (i == 0) { if (isEnding) { var pAttr = attributes?.FirstOrDefault(a => a.index == globalIndex) ?? default; - double baseLengthMs = GetTransitionBasicLengthMs(phonemes[phonemeI].phoneme, tone, pAttr); + double baseLengthMs; + double stretch = pAttr.consonantStretchRatio ?? 1.0; + double overrideRatio = phonemes[phonemeI].phoneme != null ? GetTransitionMultiplier(phonemes[phonemeI].phoneme) : 1.0; + + if (overrideRatio != 1.0) { + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + baseLengthMs = GetTransitionBasicLengthMsByOto(phonemes[phonemeI].phoneme, tone, pAttr); + } + + phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch); + if (NoGap) { // Snapped mode: Use a visible 50-tick anchor capped at 1/3 of the note int targetTicks = 50;