diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 2ea0f4271..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, "")); @@ -862,7 +868,6 @@ protected override List ProcessEnding(Ending ending) { } private string AliasFormat(string alias, string type, int tone, string prevV, string t = "-") { var aliasFormats = new Dictionary { - // Define alias formats for different types { "dynStart", new string[] { "" } }, { "dynMid", new string[] { "" } }, { "dynMid_vv", new string[] { "" } }, @@ -883,12 +888,10 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st { "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 = ""; @@ -900,10 +903,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } 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" @@ -911,10 +911,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"-{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); } } } @@ -922,7 +924,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st 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]; @@ -935,10 +937,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{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); } } } @@ -946,7 +950,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st 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]; @@ -960,10 +964,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{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); } } } @@ -981,9 +987,11 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } 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; @@ -1699,7 +1707,6 @@ 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; @@ -1711,112 +1718,22 @@ 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 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 + // 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); 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; - } - } - - 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; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (Regex.IsMatch(alias, $@"(? true; + + 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 8fe5cca1d..2e837ed4f 100644 --- a/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EStoJAPhonemizer.cs @@ -669,5 +669,39 @@ 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); + + 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/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 3c9f81aaa..b73b974fc 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; } @@ -492,6 +494,11 @@ protected override List ProcessSyllable(Syllable syllable) { if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { cc1 = $"{string.Join("", cc.Skip(i))}"; } + 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); } @@ -525,6 +532,11 @@ protected override List ProcessSyllable(Syllable syllable) { 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]}"; @@ -833,123 +845,23 @@ protected override string ValidateAlias(string alias) { return 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; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } + // Endings has 50 ticks gap + protected override bool NoGap => true; - // consonant timings + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); 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; - } - } - - 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; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (Regex.IsMatch(alias, $@"(?(); - 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}"); @@ -433,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, ""); @@ -465,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, ""); @@ -733,28 +748,110 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st }; - // 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; @@ -799,114 +896,50 @@ 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 double GetTransitionBasicLengthMs(string alias = "") { - //I wish these were automated instead :') - double transitionMultiplier = 1.0; // Default multiplier + protected override bool NoGap => true; + + 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 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)) { - 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/EnglishVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnglishVCCVPhonemizer.cs index d8f7952bc..49dc72d7a 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) { @@ -175,7 +180,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 +189,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 @@ -210,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; } @@ -900,5 +908,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..cd20f323b 100644 --- a/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FILtoJAPhonemizer.cs @@ -513,5 +513,39 @@ 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); + + 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/FilipinoPhonemizer.cs b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs index 96171865e..1f0b2861f 100644 --- a/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/FilipinoPhonemizer.cs @@ -809,12 +809,10 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st { "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 = ""; @@ -826,10 +824,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } 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" @@ -837,10 +832,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"-{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); } } } @@ -848,7 +845,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st 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]; @@ -861,10 +858,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{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); } } } @@ -872,7 +871,7 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st 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]; @@ -886,10 +885,12 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st $"{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); } } } @@ -907,9 +908,11 @@ private string AliasFormat(string alias, string type, int tone, string prevV, st } 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; @@ -937,123 +940,23 @@ 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); - } - - 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; + // Endings has 50 ticks gap + protected override bool NoGap => true; - 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 + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); 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; - } - } - - 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; - } - } + var symbol = kvp.Key; + var value = kvp.Value; - foreach (var c in affricate) { - if (PhonemeIsPresent(alias, c)) { - return base.GetTransitionBasicLengthMs() * affricate_def; + if (Regex.IsMatch(alias, $@"(? 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 242b03a5c..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; } @@ -456,9 +458,12 @@ protected override string ValidateAlias(string alias) { return alias; } + protected override bool NoGap => true; + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - protected override double GetTransitionBasicLengthMs(string alias = "") { - return base.GetTransitionBasicLengthMs(); + return otoLength; } } } \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs index 6cd8f0c2c..fea1e2304 100644 --- a/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ItalianSyllableBasedPhonemizer.cs @@ -226,5 +226,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 98fd877d5..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; } @@ -477,8 +480,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; } } } diff --git a/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs b/OpenUtau.Plugin.Builtin/SyllableBasedPhonemizer.cs index c576430f0..97be85b2c 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) { @@ -166,6 +168,8 @@ 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); @@ -183,11 +187,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) { @@ -203,13 +211,17 @@ 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; } } } + var phonemesArray = phonemes.ToArray(); + CustomParameters(notes, prev, next, prevNeighbour, nextNeighbour, prevNeighbours, phonemesArray); + var finalPhonemes = AssignAllAffixes(phonemesArray.ToList(), notes, prevNeighbours); return new Result() { - phonemes = AssignAllAffixes(phonemes, notes, prevNeighbours) + phonemes = finalPhonemes }; } @@ -224,11 +236,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); @@ -346,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(); @@ -359,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); } @@ -375,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) { @@ -389,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; + } } } } @@ -419,6 +441,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 /// @@ -488,7 +524,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; @@ -511,6 +546,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. Will return true if dynamically flagged using glides() or TryAddPhoneme(). + /// + protected virtual bool IsGlide(string alias) { + return runtimeGlides.Contains(alias) && enableGlides; + } + + 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. @@ -745,6 +790,45 @@ 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 + /// + 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)) { + // 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; + } + + return GetTransitionBasicLengthMsByConstant(); + } + /// /// a note length modifier, from 1 to 0.3. Used to make transition notes shorter on high tempo /// @@ -811,6 +895,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 /// @@ -838,6 +930,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 /// @@ -896,6 +1002,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(); @@ -914,7 +1021,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(); } } @@ -922,7 +1029,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(); } } @@ -932,17 +1039,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; @@ -950,7 +1061,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; @@ -967,15 +1077,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; } @@ -998,25 +1106,23 @@ 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>(); + 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 cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh)) { if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(rulePh)) captures[rulePh] = new Queue(); - captures[rulePh].Enqueue(actualPh); + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); } else { match = false; break; } @@ -1026,56 +1132,112 @@ 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) { - finalPhonemes.Add(IsGroupKeyword(toPh) && captures.ContainsKey(toPh) && captures[toPh].Count > 0 ? captures[toPh].Dequeue() : toPh); + // 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++) { + // 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(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; // Store just the base group name + } else { + cleanParts[k] = partNoParens; // Store literals + } + } + + 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; + + 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(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 cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.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++) { + string partNoParens = parts[k].Trim('(', ')'); + int cutoff = partNoParens.IndexOfAny(new[] { '!', '=', '&' }); + string potentialGroup = cutoff >= 0 ? partNoParens.Substring(0, cutoff) : partNoParens; + + if (baseGroupTo == null && IsGroupKeyword(potentialGroup)) { + baseGroupTo = potentialGroup; + cleanParts[k] = potentialGroup; + } else { + cleanParts[k] = partNoParens; + } + } + + 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; } } } @@ -1096,11 +1258,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 = ""; @@ -1108,8 +1271,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) { @@ -1142,8 +1310,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); @@ -1152,8 +1319,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); @@ -1254,33 +1425,111 @@ private List ExtractVowels(string[] symbols) { } return vowelIds; } + + 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 currentPhonemeI = phonemeSymbols.Count - i - 1; + + var nextGlobalIndex = globalStartIndex + prevPhonemeI; + var nextPAttr = attributes?.FirstOrDefault(a => a.index == nextGlobalIndex) ?? default; + + string nextAlias = phonemeSymbols[prevPhonemeI]; + string currentAlias = phonemeSymbols[currentPhonemeI]; - private Phoneme[] MakePhonemes(List phonemeSymbols, int containerLength, int position, bool isEnding) { + 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 + 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; + } + } + } - var phonemes = new Phoneme[phonemeSymbols.Count]; 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)); + phonemes[phonemeI] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex + }; + if (i == 0) { - if (!isEnding) { - transitionLengthTick = 0; + if (isEnding) { + var pAttr = attributes?.FirstOrDefault(a => a.index == globalIndex) ?? default; + 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; + int maxAllowed = containerLength / 3; + phonemes[phonemeI].position = System.Math.Min(targetTicks, maxAllowed); + } else { + // Natural mode: Use the full Preutterance + 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 stretched 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; + // Initialize empty slots properly to avoid null crashes + phonemes[phonemeI] = new Phoneme { + phoneme = null, + position = 0, + index = globalIndex + }; } } - - 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) { @@ -1292,18 +1541,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; } 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); }