From 8094fcce14e9c74bc30519cb33843e69ada47c53 Mon Sep 17 00:00:00 2001 From: Kona Date: Sat, 6 Jun 2026 09:04:17 -0700 Subject: [PATCH 01/13] Set up project with new branch and added class --- GenericPhonemizer/Class1.cs | 5 +++++ GenericPhonemizer/GenericPhonemizer.csproj | 9 +++++++++ OpenUtau.sln | 10 ++++++++-- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 GenericPhonemizer/Class1.cs create mode 100644 GenericPhonemizer/GenericPhonemizer.csproj diff --git a/GenericPhonemizer/Class1.cs b/GenericPhonemizer/Class1.cs new file mode 100644 index 000000000..a30297244 --- /dev/null +++ b/GenericPhonemizer/Class1.cs @@ -0,0 +1,5 @@ +namespace GenericPhonemizer { + public class Class1 { + + } +} diff --git a/GenericPhonemizer/GenericPhonemizer.csproj b/GenericPhonemizer/GenericPhonemizer.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/GenericPhonemizer/GenericPhonemizer.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/OpenUtau.sln b/OpenUtau.sln index f55699485..cb514a3e8 100644 --- a/OpenUtau.sln +++ b/OpenUtau.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32228.430 +# Visual Studio Version 18 +VisualStudioVersion = 18.6.11819.183 stable MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8CC070AB-64DC-4C02-89E7-D8E5EC91A492}" ProjectSection(SolutionItems) = preProject @@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau", "OpenUtau\OpenUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau.Plugin.Builtin", "OpenUtau.Plugin.Builtin\OpenUtau.Plugin.Builtin.csproj", "{D24740E1-CC43-4226-A573-59C013FE50A2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericPhonemizer", "GenericPhonemizer\GenericPhonemizer.csproj", "{FE6630A1-BDD5-4C65-97BD-D329C583D370}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -48,6 +50,10 @@ Global {D24740E1-CC43-4226-A573-59C013FE50A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.Build.0 = Release|Any CPU + {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 419e20ed66c9d1d270e7cd11b4b49a5d2f2bd2dd Mon Sep 17 00:00:00 2001 From: Kona Date: Sat, 6 Jun 2026 11:12:34 -0700 Subject: [PATCH 02/13] Got it functioning with the version of OU that compiles here, however it doesn't function in normal OU. Working on figuring out whi. --- GenericPhonemizer/Class1.cs | 5 ----- GenericPhonemizer/GenericPhonemizer.csproj | 9 --------- OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs | 2 +- OpenUtau.sln | 12 ++++++------ 4 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 GenericPhonemizer/Class1.cs delete mode 100644 GenericPhonemizer/GenericPhonemizer.csproj diff --git a/GenericPhonemizer/Class1.cs b/GenericPhonemizer/Class1.cs deleted file mode 100644 index a30297244..000000000 --- a/GenericPhonemizer/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace GenericPhonemizer { - public class Class1 { - - } -} diff --git a/GenericPhonemizer/GenericPhonemizer.csproj b/GenericPhonemizer/GenericPhonemizer.csproj deleted file mode 100644 index fa71b7ae6..000000000 --- a/GenericPhonemizer/GenericPhonemizer.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs index 3d975ed51..70d2198eb 100644 --- a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs +++ b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace OpenUtau.Plugin.Builtin.Data { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/OpenUtau.sln b/OpenUtau.sln index cb514a3e8..f61f987f6 100644 --- a/OpenUtau.sln +++ b/OpenUtau.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.6.11819.183 stable +VisualStudioVersion = 18.6.11819.183 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8CC070AB-64DC-4C02-89E7-D8E5EC91A492}" ProjectSection(SolutionItems) = preProject @@ -26,7 +26,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau", "OpenUtau\OpenUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau.Plugin.Builtin", "OpenUtau.Plugin.Builtin\OpenUtau.Plugin.Builtin.csproj", "{D24740E1-CC43-4226-A573-59C013FE50A2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericPhonemizer", "GenericPhonemizer\GenericPhonemizer.csproj", "{FE6630A1-BDD5-4C65-97BD-D329C583D370}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericSyllableBasedPhonemizer", "..\OU-Generic-Syllable-Based-Phonemizer\GenericSyllableBasedPhonemizer\GenericSyllableBasedPhonemizer.csproj", "{F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,10 +50,10 @@ Global {D24740E1-CC43-4226-A573-59C013FE50A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.Build.0 = Release|Any CPU - {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE6630A1-BDD5-4C65-97BD-D329C583D370}.Release|Any CPU.Build.0 = Release|Any CPU + {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 059f278a424e6119f060d205ac8e5dc95e48777a Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:51:11 -0700 Subject: [PATCH 03/13] Formatted files correctly --- .../Data/Resources.Designer.cs | 73 + EnXSampaPhonemizerPlus/Data/Resources.resx | 124 ++ .../Data/en-xsampa.template.yaml | 177 ++ EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs | 955 +++++++++ .../EnXSampaPhonemizerPlus.csproj | 28 + .../SyllableBasedPhonemizer.cs | 1780 +++++++++++++++++ .../Data/Resources.Designer.cs | 73 + .../Data/Resources.resx | 124 ++ .../Data/generic.template.yaml | 150 ++ .../GenericSyllableBasedPhonemizer.csproj | 28 + .../GenericSyllablePhonemizer.cs | 1050 ++++++++++ .../SyllableBasedPhonemizer.cs | 1780 +++++++++++++++++ .../Data/Resources.Designer.cs | 73 + JapaneseCVVXPhonemizer/Data/Resources.resx | 124 ++ JapaneseCVVXPhonemizer/Data/template.yaml | 703 +++++++ JapaneseCVVXPhonemizer/JA_CVVX.cs | 1085 ++++++++++ JapaneseCVVXPhonemizer/JA_CVVX.csproj | 29 + .../SyllableBasedPhonemizer.cs | 1780 +++++++++++++++++ OpenUtau.sln | 22 +- 19 files changed, 10153 insertions(+), 5 deletions(-) create mode 100644 EnXSampaPhonemizerPlus/Data/Resources.Designer.cs create mode 100644 EnXSampaPhonemizerPlus/Data/Resources.resx create mode 100644 EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml create mode 100644 EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs create mode 100644 EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj create mode 100644 EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs create mode 100644 GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs create mode 100644 GenericSyllableBasedPhonemizer/Data/Resources.resx create mode 100644 GenericSyllableBasedPhonemizer/Data/generic.template.yaml create mode 100644 GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj create mode 100644 GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs create mode 100644 GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs create mode 100644 JapaneseCVVXPhonemizer/Data/Resources.Designer.cs create mode 100644 JapaneseCVVXPhonemizer/Data/Resources.resx create mode 100644 JapaneseCVVXPhonemizer/Data/template.yaml create mode 100644 JapaneseCVVXPhonemizer/JA_CVVX.cs create mode 100644 JapaneseCVVXPhonemizer/JA_CVVX.csproj create mode 100644 JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs diff --git a/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs b/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs new file mode 100644 index 000000000..e61c361b9 --- /dev/null +++ b/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace EnXSampaPhonemizerPlus.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EnXSampaPhonemizerPlus.Data.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] en_xsampa_template { + get { + object obj = ResourceManager.GetObject("en-xsampa.template", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/EnXSampaPhonemizerPlus/Data/Resources.resx b/EnXSampaPhonemizerPlus/Data/Resources.resx new file mode 100644 index 000000000..f5e319aaf --- /dev/null +++ b/EnXSampaPhonemizerPlus/Data/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + en-xsampa.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml b/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml new file mode 100644 index 000000000..fbb2f4a8b --- /dev/null +++ b/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml @@ -0,0 +1,177 @@ +%YAML 1.2 +--- +symbols: +#Delta-style (X-SAMPA) symbols + - {symbol: a, type: vowel} + - {symbol: e, type: vowel} + - {symbol: i, type: vowel} + - {symbol: o, type: vowel} + - {symbol: u, type: vowel} + - {symbol: A, type: vowel} + - {symbol: E, type: vowel} + - {symbol: I, type: vowel} + - {symbol: O, type: vowel} + - {symbol: U, type: vowel} + - {symbol: '{', type: vowel} + - {symbol: V, type: vowel} + - {symbol: 3, type: vowel} + - {symbol: aI, type: vowel} + - {symbol: eI, type: vowel} + - {symbol: OI, type: vowel} + - {symbol: aU, type: vowel} + - {symbol: oU, type: vowel} + - {symbol: tS, type: affricate} + - {symbol: D, type: fricative} + - {symbol: h, type: aspirate} + - {symbol: dZ, type: affricate} + - {symbol: N, type: nasal} + - {symbol: S, type: fricative} + - {symbol: T, type: fricative} + - {symbol: j, type: semivowel} + - {symbol: Z, type: fricative} +#Arpasing + - {symbol: aa, type: vowel} + - {symbol: ae, type: vowel} + - {symbol: ah, type: vowel} + - {symbol: ao, type: vowel} + - {symbol: aw, type: vowel} + - {symbol: ax, type: vowel} + - {symbol: ay, type: vowel} + - {symbol: b, type: stop} + - {symbol: ch, type: affricate} + - {symbol: d, type: stop} + - {symbol: dh, type: fricative} + - {symbol: dr, type: fricative} + - {symbol: dx, type: tap} + - {symbol: eh, type: vowel} + - {symbol: er, type: vowel} + - {symbol: ey, type: vowel} + - {symbol: f, type: fricative} + - {symbol: g, type: stop} + - {symbol: hh, type: aspirate} + - {symbol: ih, type: vowel} + - {symbol: iy, type: vowel} + - {symbol: jh, type: affricate} + - {symbol: k, type: stop} + - {symbol: l, type: liquid} + - {symbol: m, type: nasal} + - {symbol: n, type: nasal} + - {symbol: ng, type: nasal} + - {symbol: ow, type: vowel} + - {symbol: oy, type: vowel} + - {symbol: p, type: stop} + - {symbol: q, type: stop} + - {symbol: r, type: liquid} + - {symbol: s, type: fricative} + - {symbol: sh, type: fricative} + - {symbol: t, type: stop} + - {symbol: th, type: fricative} + - {symbol: tr, type: fricative} + - {symbol: uh, type: vowel} + - {symbol: uw, type: vowel} + - {symbol: v, type: fricative} + - {symbol: w, type: semivowel} + - {symbol: y, type: semivowel} + - {symbol: z, type: fricative} + - {symbol: zh, type: fricative} + #bonus symbols + - {symbol: e@, type: vowel} + - {symbol: e@n, type: vowel} + - {symbol: e@m, type: vowel} + - {symbol: eN, type: vowel} + - {symbol: IN, type: vowel} + - {symbol: Ar, type: vowel} + - {symbol: Qr, type: vowel} + - {symbol: Er, type: vowel} + - {symbol: Ir, type: vowel} + - {symbol: Or, type: vowel} + - {symbol: Ur, type: vowel} + - {symbol: ir, type: vowel} + - {symbol: ur, type: vowel} + - {symbol: aIr, type: vowel} + - {symbol: aUr, type: vowel} + - {symbol: A@, type: vowel} + - {symbol: Q@, type: vowel} + - {symbol: E@, type: vowel} + - {symbol: I@, type: vowel} + - {symbol: O@, type: vowel} + - {symbol: U@, type: vowel} + - {symbol: i@, type: vowel} + - {symbol: u@, type: vowel} + - {symbol: aI@, type: vowel} + - {symbol: aU@, type: vowel} + - {symbol: Q, type: vowel} + - {symbol: 1, type: vowel} + - {symbol: Ol, type: vowel} + - {symbol: aUn, type: vowel} + - {symbol: '@r', type: vowel} + - {symbol: '@l', type: vowel} + - {symbol: '@m', type: vowel} + - {symbol: '@n', type: vowel} + - {symbol: '@N', type: vowel} + - {symbol: '@', type: vowel} + - {symbol: 'I\', type: vowel} + - {symbol: M, type: vowel} + - {symbol: 'U\', type: vowel} + - {symbol: Y, type: vowel} + - {symbol: '@\', type: vowel} + - {symbol: '@`', type: vowel} + - {symbol: 3`, type: vowel} + - {symbol: A`, type: vowel} + - {symbol: Q`, type: vowel} + - {symbol: E`, type: vowel} + - {symbol: I`, type: vowel} + - {symbol: O`, type: vowel} + - {symbol: U`, type: vowel} + - {symbol: i`, type: vowel} + - {symbol: u`, type: vowel} + - {symbol: aI`, type: vowel} + - {symbol: aU`, type: vowel} + - {symbol: '}', type: vowel} + - {symbol: 2, type: vowel} + - {symbol: '3\', type: vowel} + - {symbol: 6, type: vowel} + - {symbol: 7, type: vowel} + - {symbol: 8, type: vowel} + - {symbol: 9, type: vowel} + - {symbol: '&', type: vowel} + - {symbol: '{~', type: vowel} + - {symbol: I~, type: vowel} + - {symbol: aU~, type: vowel} + - {symbol: VI, type: vowel} + - {symbol: VU, type: vowel} + - {symbol: '@U', type: vowel} + - {symbol: 'i:', type: vowel} + - {symbol: 'u:', type: vowel} + - {symbol: 'O:', type: vowel} + - {symbol: e@0, type: vowel} + - {symbol: ai, type: vowel} + - {symbol: ei, type: vowel} + - {symbol: Oi, type: vowel} + - {symbol: au, type: vowel} + - {symbol: ou, type: vowel} + - {symbol: Ou, type: vowel} + - {symbol: '@u', type: vowel} + - {symbol: 4, type: tap} + - {symbol: W, type: fricative} + - {symbol: 't_}', type: stop} + - {symbol: E~, type: vowel} + - {symbol: e~, type: vowel} + - {symbol: 3r, type: vowel} + - {symbol: ar, type: vowel} + - {symbol: or, type: vowel} + - {symbol: '{l', type: vowel} + - {symbol: Al, type: vowel} + - {symbol: al, type: vowel} + - {symbol: El, type: vowel} + - {symbol: Il, type: vowel} + - {symbol: ul, type: vowel} + - {symbol: Ul, type: vowel} + - {symbol: mm, type: vowel} + - {symbol: nn, type: vowel} + - {symbol: ll, type: vowel} + - {symbol: NN, type: vowel} + +entries: + - grapheme: openutau + phonemes: [ow, p, ah, n, uw, t, aw] diff --git a/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs b/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs new file mode 100644 index 000000000..1fad19809 --- /dev/null +++ b/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs @@ -0,0 +1,955 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Design; +using System.IO; +using System.Linq; +using Classic; +using OpenUtau.Api; +using OpenUtau.Classic; +using OpenUtau.Core.G2p; +using OpenUtau.Core.Ustx; +using Serilog; +using YamlDotNet.Core.Tokens; +using System.Text.RegularExpressions; + +namespace OpenUtau.Plugin.Builtin { + /// + /// General English phonemizer for X-SAMPA voicebanks. + /// The difference between this phonemizer and the Teto English phonemizer is that this one was made to support all X-SAMPA-based banks. + /// However, it should be fully compatible with Kasane Teto's English voicebank regardless. + ///

+ ///

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

+ ///

+ /// Read more about X-SAMPA here. + ///
+ [Phonemizer("English X-SAMPA + phonemizer", "EN X-SAMPA +", "HeadHunter4621", language: "EN")] + public class EnXSampaPhonemizer : SyllableBasedPhonemizer { + protected override string YamlFileName => "en-xsampa.yaml"; + protected override byte[] YamlTemplate => EnXSampaPhonemizerPlus.Data.Resources.en_xsampa_template; + public EnXSampaPhonemizer() { + this.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(','); + this.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(','); + this.dictionaryReplacements = ("aa=A;ae={;ah=V;ao=O;aw=aU;ax=@;ay=aI;" + + "b=b;ch=tS;d=d;dh=D;" + "dx=4;eh=E;el=@l;em=@m;en=@n;eng=@N;er=3;ey=eI;f=f;g=g;hh=h;ih=I;iy=i;jh=dZ;k=k;l=l;m=m;n=n;ng=N;ow=oU;oy=OI;" + + "p=p;q=・;r=r;s=s;sh=S;t=t;th=T;" + "uh=U;uw=u;v=v;w=w;" + "y=j;z=z;zh=Z").Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + } + + private bool isYamlFallbacks = false; + protected override string[] GetVowels() => vowels; + protected override string[] GetConsonants() => consonants; + protected override string GetDictionaryName() => ""; + + // For banks aliased with VOCALOID-style phonemes + private readonly Dictionary vocaSampa = "A=Q;E=e;i=i:;u=u:;O=O:;3=@r;oU=@U;Ar=Q@;Qr=Q@;Er=e@;er=e@;Ir=I@;ir=I@;i:r=I@;Or=O@;O:r=O@;Ur=U@;ur=U@;u:r=U@".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isVocaSampa = false; + + // For banks with slightly fewer vowels + private readonly Dictionary simpleDelta = "E=e;V=@;o=O".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isSimpleDelta = false; + + // For banks with slightly fewer vowels + private readonly Dictionary CanadianRaising = "VI=aI;VU=aU".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isMissingCanadianRaising = false; + + // For banks with only minimal vowels + private readonly Dictionary miniDelta = "I=i;U=u".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isMiniDelta = false; + + // For Japanese-English combined banks. + // NOTE: Rather rudimentary by default; a custom dictionary is recommended. + private readonly Dictionary enPlusJa = "j=y;dZ=j;r=r\\;tS=ch".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isEnPlusJa = false; + + // For banks encoded in "true"/more accurate X-SAMPA. + private readonly Dictionary trueXSampa = "r=r\\;3=@`".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isTrueXSampa = false; + + // For banks using Salem's reclist. + private readonly Dictionary salemList = "3=@r;Ar=ar;aIr=ar;aUr=ar;Or=or;aIl=al".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isSalemList = false; + + // Velar nasal fallback + private readonly Dictionary velarNasalFallback = "N g=n g;N k=n k".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isVelarNasalFallback = false; + + // For Kasane Teto's missing sample + private readonly Dictionary tetoException = "V=@".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isTetoException = false; + + // For dark L vowels + private readonly Dictionary darkLVowel = "@l=@5,ul=u5,Ol=O5,Al=A5,El=E5,Il=I5,il=i5,".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isDarkLVowel = false; + + private readonly Dictionary vvExceptions = + new Dictionary() { + {"aI","j"}, + {"eI","j"}, + {"OI","j"}, + {"aU","w"}, + {"oU","w"}, + {"VI","j"}, + {"VU","w"}, + {"@U","w"}, + {"ai","j"}, + {"Oi","j"}, + {"au","w"}, + {"ou","w"}, + {"Ou","w"}, + {"@u","w"}, + {"3", "r"} + }; + private readonly Dictionary Delta5vvExceptions = + new Dictionary() { + {"aI","I"}, + {"eI","I"}, + {"OI","I"}, + {"aU","U"}, + {"oU","U"}, + {"VI","I"}, + {"VU","U"}, + {"@U","U"}, + {"ai","i"}, + {"Oi","i"}, + {"au","u"}, + {"ou","u"}, + {"Ou","u"}, + {"@u","u"}, + {"3", "r"} + }; + protected override IG2p LoadBaseDictionary() { + var g2ps = new List(); + + // Load dictionary from plugin folder. + string path = Path.Combine(PluginDir, YamlFileName); + if (!File.Exists(path)) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + + // Load dictionary from singer folder. + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + g2ps.Add(new ArpabetPlusG2p()); + return new G2pFallbacks(g2ps.ToArray()); + } + + protected override string[] GetSymbols(Note note) { + string[] original = base.GetSymbols(note); + if (original == null) { + return null; + } + List finalProcessedPhonemes = new List(); + + // 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", }; + string[] affricates = new[] { "dZ", "tS" }; + + foreach (string s in original) { + if (diphthongs.Contains(s) && !HasOto($"- {s}", note.tone) && !HasOto(s, note.tone) && !HasOto(ValidateAlias($"- {s}"), note.tone) && !HasOto(ValidateAlias(s), note.tone)) { + finalProcessedPhonemes.AddRange(new string[] { s[0].ToString(), s[1] + '^'.ToString() }); + } else if (affricates.Contains(s) && !HasOto($"{s}A", note.tone) && !HasOto($"{s} A", note.tone) && !HasOto($"{s}Q", note.tone) && !HasOto($"{s} Q", note.tone)) { + finalProcessedPhonemes.AddRange(new string[] { s[0].ToString(), s[1].ToString() }); + } else { + finalProcessedPhonemes.Add(s); + } + } + return finalProcessedPhonemes.ToArray(); + } + // prioritize yaml replacements over dictionary replacements + private string ReplacePhoneme(string phoneme, int tone) { + // If the original phoneme has an OTO, use it directly. + 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; + } + + protected override List ProcessSyllable(Syllable syllable) { + syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; + var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); + var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; + string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string v = ReplacePhoneme(syllable.v, syllable.vowelTone); + List vowels = new List { v }; + string basePhoneme; + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + int prevWordConsonantsCount = syllable.prevWordConsonantsCount; + + + var rv = $"- {v}"; + + // Switch between phonetic systems, depending on certain aliases in the bank + foreach (var entry in yamlFallbacks) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { + isYamlFallbacks = true; + break; + } + } + if (HasOto($"- i:", syllable.tone) || HasOto($"i:", syllable.tone) || (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone))) { + isVocaSampa = true; + } + + if (!HasOto($"- VI", syllable.tone) || HasOto($"VI", syllable.tone) || (!HasOto($"- VU", syllable.tone) && !HasOto($"VU", syllable.tone))) { + isMissingCanadianRaising = true; + } + + if (!HasOto($"- V", syllable.vowelTone) && !HasOto($"V", syllable.vowelTone)) { + isSimpleDelta = true; + } + + if (!HasOto($"- bV", syllable.vowelTone) && !HasOto($"bV", syllable.vowelTone)) { + isTetoException = true; + } + + if ((!HasOto($"- I", syllable.vowelTone) && !HasOto($"I", syllable.vowelTone)) || (!HasOto($"- U", syllable.vowelTone) && !HasOto($"U", syllable.vowelTone))) { + isMiniDelta = true; + } + + if (HasOto("あ", syllable.vowelTone) || HasOto("- あ", syllable.vowelTone)) { + isEnPlusJa = true; + } + + if (HasOto($"{prevV} r\\", syllable.tone)) { + isTrueXSampa = true; + } + + if (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone) && !HasOto($"- @`", syllable.tone) && !HasOto($"@`", syllable.tone)) { + isSalemList = true; + } + + if ((!HasOto($"N g", syllable.tone) || !HasOto($"N g-", syllable.tone)) && (!HasOto($"N k", syllable.tone) || !HasOto($"N k-", syllable.tone))) { + isVelarNasalFallback = true; + } + + if (HasOto("@5", syllable.vowelTone) || HasOto("u5", syllable.vowelTone) || HasOto("O5", syllable.vowelTone) || HasOto("A5", syllable.vowelTone) || HasOto("E5", syllable.vowelTone) || HasOto("I5", syllable.vowelTone) || HasOto("i5", syllable.vowelTone) || HasOto("- @5", syllable.vowelTone) || HasOto("- u5", syllable.vowelTone) || HasOto("- O5", syllable.vowelTone) || HasOto("- A5", syllable.vowelTone) || HasOto("- E5", syllable.vowelTone) || HasOto("- I5", syllable.vowelTone) || HasOto("- i5", syllable.vowelTone)) { + isDarkLVowel = true; + } + + if (syllable.IsStartingV) { + if (HasOto(rv, syllable.vowelTone) || HasOto(ValidateAlias(rv), syllable.vowelTone)) { + basePhoneme = rv; + } else { + basePhoneme = v; + } + } else if (syllable.IsVV) { + if (!CanMakeAliasExtension(syllable)) { + var vv = $"{prevV} {v}"; + basePhoneme = vv; + if (!HasOto(vv, syllable.vowelTone) && !HasOto(ValidateAlias(vv), syllable.vowelTone) && (vvExceptions.ContainsKey(prevV) && prevV != v || Delta5vvExceptions.ContainsKey(prevV) && prevV != v)) { + // VV splits to [V C][CV] or [V][V] + var delta5vc = $"{Delta5vvExceptions[prevV]}"; + bool CV = false; + if ((!HasOto(delta5vc, syllable.vowelTone) && !HasOto(ValidateAlias(delta5vc), syllable.vowelTone))) { + delta5vc = $"{prevV} {vvExceptions[prevV]}"; + CV = true; + } + phonemes.Add(delta5vc); + // if delta5 vc is not available, turn v to cv + var cv = $"{vvExceptions[prevV]}{v}"; + basePhoneme = v; + if (CV && (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone))) { + basePhoneme = cv; + } + } else { + // VV to V + if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV} {v}"; + } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } + } + // EXTEND AS [V] + } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone)) { + basePhoneme = v; + } else { + // PREVIOUS ALIAS WILL EXTEND as [V V] + basePhoneme = null; + } + } else if (syllable.IsStartingCVWithOneConsonant) { + // TODO: move to config -CV or -C CV + var rcv = $"- {cc[0]}{v}"; + var crv = $"{cc[0]} {v}"; + var cv = $"{cc[0]}{v}"; + if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone)) { + basePhoneme = rcv; + } else if ((!HasOto(rcv, syllable.vowelTone) && !HasOto(ValidateAlias(rcv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { + basePhoneme = crv; + TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); + } else { + basePhoneme = cv; + TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); + } + } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { + // try RCCV + var rccv = $"- {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + var ucv = $"_{cc.Last()}{v}"; + if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) { + basePhoneme = rccv; + lastC = 0; + } else { + if (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) { + basePhoneme = ucv; + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = crv; + } else { + basePhoneme = $"{cc.Last()}{v}"; + } + // try RCC + for (var i = cc.Length; i > 1; i--) { + if (TryAddPhoneme(phonemes, syllable.tone, $"- {string.Join("", cc.Take(i))}", ValidateAlias($"- {string.Join("", cc.Take(i))}"))) { + firstC = i - 1; + break; + } + } + if (phonemes.Count == 0) { + TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); + } + // try CCV + for (var i = firstC; i < cc.Length - 1; i++) { + var ccv = string.Join("", cc.Skip(i)) + v; + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + basePhoneme = ccv; + lastC = i; + break; + } else { + if (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) { + basePhoneme = ucv; + break; + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = crv; + break; + } else { + basePhoneme = $"{cc.Last()}{v}"; + break; + } + } + } + } + } else { // VCV + var vcv = $"{prevV} {cc[0]}{v}"; + var vcvEnd = $"{prevV}{cc[0]} {v}"; + var vccv = $"{prevV} {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + // Use regular VCV if the current word starts with one consonant and the previous word ends with none + if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + // Use end VCV if current word does not start with a consonant but the previous word does end with one + } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) || HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { + basePhoneme = vcvEnd; + // Use regular VCV if end VCV does not exist + } else if (syllable.IsVCVWithOneConsonant && !HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone) && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone))) { + basePhoneme = vcv; + // VCV with multiple consonants, only for current word onset and null previous word ending + // TODO: multi-VCV for words ending with one or more consonants? + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { + basePhoneme = vccv; + lastC = 0; + } else { + var cv = cc.Last() + v; + basePhoneme = cv; + if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { + basePhoneme = crv; + } + // try CCV + if ((cc.Length - firstC > 1) && CurrentWordCc.Length >= 2) { + for (var i = firstC; i < cc.Length; i++) { + var ccv = $"{string.Join("", cc.Skip(0))}{v}"; + var rccv = $"- {string.Join("", cc.Skip(0))}{v}"; + var ucv = $"_{cc.Last()}{v}"; + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + lastC = 0; + basePhoneme = ccv; + break; + } else if (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone) && (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone))) { + lastC = 0; + basePhoneme = rccv; + break; + } else if ((!HasOto(rccv, syllable.vowelTone) || !HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone))) { + basePhoneme = ucv; + break; + } + } + } + FoundMatch:; + // try vcc + for (var i = lastC + 1; i >= 0; i--) { + var vr = $"{prevV} -"; + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vcc2 = $"{prevV}{string.Join(" ", cc.Take(2))}"; + var vc = $"{prevV} {cc[0]}"; + if (i == 0) { + if (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) { + phonemes.Add(vr); + } + } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && !affricate.Contains(string.Join("", cc.Take(2)))) { + phonemes.Add(vcc); + firstC = 1; + break; + } else if (HasOto(vcc2, syllable.tone) || HasOto(ValidateAlias(vcc2), syllable.tone)) { + phonemes.Add(vcc2); + firstC = 1; + break; + } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { + phonemes.Add(vc); + break; + } else { + continue; + } + } + } + } + + for (var i = firstC; i < lastC; i++) { + // we could use some CCV, so lastC is used + // we could use -CC so firstC is used + var cc1 = $"{cc[i]} {cc[i + 1]}"; + var ccv = string.Join("", cc.Skip(i + 1)) + v; + var rccv = $"- {string.Join("", cc.Skip(i + 1)) + v}"; + var ucv = $"_{cc.Last()}{v}"; + var crv = $"{cc.Last()} {v}"; + var cv = $"{cc.Last()}{v}"; + // Use [C1C2...] when current word starts with 2 consonants or more + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { + cc1 = $"{string.Join("", cc.Skip(i))}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // Use [C1C2] when current word has 2 consonants or more and [C1C2C3...] does not exist + if (!HasOto(cc1, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc1)) { + cc1 = $"{cc[i]}{cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // Use [C1 C2] when either [C1C2] does not exist, or current word has 1 consonant or less and previous word has 1 consonant or more + if ((!HasOto(cc1, syllable.tone)) || PreviousWordCc.Contains(cc1)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // Use UCV if it exists + if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && !cc1.Contains($"{cc[i]} {cc[i + 1]}")) { + basePhoneme = ucv; + } + if (i + 1 < lastC) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); + } + // Use [C2C3...] when current word starts with 2 consonants or more + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc2)) { + cc2 = $"{string.Join("", cc.Skip(i))}"; + } + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); + } + // 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]}"; + } + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); + } + // Use [C2 C3] when either [C2C3] does not exist, or current word has 1 consonant or less and previous word has 2 consonants or more + if ((!HasOto(cc2, syllable.tone)) || PreviousWordCc.Contains(cc2)) { + cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + } + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); + } + //Use CCV if it exists + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { + lastC = i; + basePhoneme = ccv; + // Use RCCV if it exists + } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { + lastC = i; + basePhoneme = rccv; + // Use _CV if it exists + } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && HasOto(cc2, syllable.vowelTone) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}") && CurrentWordCc.Length >= 2) { + basePhoneme = ucv; + // Use spaced CV if it exists + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = crv; + // Use normal CV + } else { + basePhoneme = cv; + } + if (HasOto(cc1, syllable.tone) && HasOto(cc2, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { + // like [V C1] [C1 C2] [C2 C3] [C3 ..] + phonemes.Add(cc1); + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { + // like [V C1] [C1 C2] [C2 ..] + if (cc1.Contains($"{string.Join("", cc.Skip(i))}")) { + i++; + } + } else { + // singular cc + if ((PreviousWordCc.Contains(cc1) == CurrentWordCc.Contains(cc1)) && !affricate.Contains(cc1)) { + cc1 = ValidateAlias(cc1); + } else { + TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); + } + } + } else { + // like [V C1] [C1 C2] [C2 ..] or like [V C1] [C1 -] [C3 ..] + TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); + } + } + + phonemes.Add(basePhoneme); + return phonemes; + } + + protected override List ProcessEnding(Ending ending) { + string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); + string v = ReplacePhoneme(ending.prevV, ending.tone); + string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; + var phonemes = new List(); + var vr = $"{v} {t}"; + + var lastC = cc.Length - 1; + var firstC = 0; + + if (ending.IsEndingV) { + TryAddPhoneme(phonemes, ending.tone, vr, ValidateAlias(vr)); + } else if (ending.IsEndingVCWithOneConsonant) { + var vc = $"{v} {cc[0]}"; + var vcr = $"{v} {cc[0]}{t}"; + var vcr2 = $"{v}{cc[0]} {t}"; + if (HasOto(vcr, ending.tone) || HasOto(ValidateAlias(vcr), ending.tone)) { + phonemes.Add(vcr); + } else if ((!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone)) && (HasOto(vcr2, ending.tone) || HasOto(ValidateAlias(vcr2), ending.tone))) { + phonemes.Add(vcr2); + } else { + phonemes.Add(vc); + if (affricate.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} {t}", cc[0]); + } else { + TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} {t}", ValidateAlias($"{cc[0]} {t}")); + } + } + } else { + for (var i = lastC; i >= 0; i--) { + var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; + var vcc2 = $"{v}{string.Join(" ", cc.Take(2))}{t}"; + var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; + var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; + var vc = $"{v} {cc[0]}"; + if (i == 0) { + if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone)) { + phonemes.Add(vr); + } + } else if ((HasOto(vcc, ending.tone) || HasOto(ValidateAlias(vcc), ending.tone)) && lastC == 1) { + phonemes.Add(vcc); + firstC = 1; + break; + } else if ((HasOto(vcc2, ending.tone) || HasOto(ValidateAlias(vcc2), ending.tone)) && lastC == 1) { + phonemes.Add(vcc2); + firstC = 1; + break; + } else if (HasOto(vcc3, ending.tone) || HasOto(ValidateAlias(vcc3), ending.tone)) { + phonemes.Add(vcc3); + if (vcc3.EndsWith(cc.Last()) && lastC == 1) { + if (affricate.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}"), cc.Last(), ValidateAlias(cc.Last())); + } else { + TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}")); + } + } + firstC = 1; + break; + } else if (HasOto(vcc4, ending.tone) || HasOto(ValidateAlias(vcc4), ending.tone)) { + phonemes.Add(vcc4); + if (vcc4.EndsWith(cc.Last()) && lastC == 1) { + if (affricate.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}"), cc.Last(), ValidateAlias(cc.Last())); + } else { + TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}")); + } + } + firstC = 1; + break; + } else { + phonemes.Add(vc); + break; + } + } + + + for (var i = firstC; i < lastC; i++) { + // all CCs except the first one are /C1C2/, the last one is /C1 C2-/ + // but if there is no /C1C2/, we try /C1 C2-/, vise versa for the last one + var cc1 = $"{cc[i]} {cc[i + 1]}"; + if (i < cc.Length - 2) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = $"{cc[i]}{cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = $"{cc[i + 1]}{cc[i + 2]}"; + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{cc[i + 2]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{cc[i + 2]}{t}"))) { + // like [C1 C2-][C3 ...] + i++; + } else if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { + // like [C1 C2][C2 ...] + phonemes.Add(cc1); + } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { + // like [C1 C2-][C3 ...] + phonemes.Add(cc[i]); + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { + // like [C1 C2-][C3 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { + // like [C1C2][C2 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + i++; + } else { + // like [C1][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); + i++; + } + } else { + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = $"{cc[i]}{cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { + // like [C1 C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + // like [C1 C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}", ValidateAlias($"{cc[i]}{cc[i + 1]}"))) { + // like [C1C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else { + // like [C1][C2 -] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } + } + } + } + return phonemes; + } + + protected override string ValidateAlias(string alias) { + // Validate alias depending on method + if (isVocaSampa) { + foreach (var syllable in vocaSampa) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isSimpleDelta) { + foreach (var syllable in simpleDelta) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isMiniDelta) { + foreach (var syllable in miniDelta) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isEnPlusJa) { + foreach (var syllable in enPlusJa) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isTrueXSampa) { + foreach (var syllable in trueXSampa) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isSalemList) { + foreach (var syllable in salemList) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isVelarNasalFallback) { + foreach (var syllable in velarNasalFallback) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isTetoException) { + foreach (var syllable in tetoException) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isMissingCanadianRaising) { + foreach (var syllable in CanadianRaising) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isDarkLVowel) { + foreach (var syllable in darkLVowel) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + if (isYamlFallbacks) { + foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + + // Split diphthongs adjuster + if (alias.Contains("U^")) { + alias = alias.Replace("U^", "U"); + } + if (alias.Contains("I^")) { + alias = alias.Replace("I^", "I"); + } + if (alias.Contains("u^")) { + alias = alias.Replace("u^", "u"); + } + if (alias.Contains("i^")) { + alias = alias.Replace("i^", "i"); + } + + // Other validations + if (!alias.Contains("@r") && !alias.Contains("3r")) { + foreach (var consonant1 in new[] { "r ", "r\\ ", }) { + foreach (var consonant2 in consonants) { + alias = alias.Replace(consonant1 + consonant2, "3 " + consonant2); + } + } + } + + 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; + } + } + + // 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; + } + } + + 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; + } + } + + return base.GetTransitionBasicLengthMs() * transitionMultiplier; + } + } +} diff --git a/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj b/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj new file mode 100644 index 000000000..50805590d --- /dev/null +++ b/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs b/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs new file mode 100644 index 000000000..a2852dcfe --- /dev/null +++ b/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs @@ -0,0 +1,1780 @@ +using System; +using System.Collections.Generic; +using System.Text; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using System.Linq; +using System.IO; +using Serilog; +using System.Threading.Tasks; +using static OpenUtau.Api.Phonemizer; +using System.Collections; + +namespace OpenUtau.Plugin.Builtin { + /// + /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; + /// + /// - Supports dictionary; + /// - Automatically align phonemes to notes; + /// - Supports syllable extension; + /// - Automatically calculates transition phonemes length, with constants by default, + /// but there is a pre-created function to use Oto value; + /// - The transition length is scaled based on Tempo and note length. + /// + /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". + /// + /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], + /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". + /// + /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". + /// + /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, + /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). + /// + public abstract class SyllableBasedPhonemizer : Phonemizer { + + /// + /// Syllable is [V] [C..] [V] + /// + protected struct Syllable { + /// + /// vowel from previous syllable for VC + /// + public string prevV; + /// + /// CCs, may be empty + /// + public string[] cc; + /// + /// "base" note. May not actually be vowel, if only consonants way provided + /// + public string v; + /// + /// Start position for vowel. All VC CC goes before this position + /// + public int position; + /// + /// previous note duration, i.e. this is container for VC and CC notes + /// + public int duration; + /// + /// Tone for VC and CC + /// + public int tone; + /// + /// Other phoneme attributes for VC and CC + /// + public PhonemeAttributes[] attr; + /// + /// tone for base "vowel" phoneme + /// + public int vowelTone; + /// + /// Other phoneme attributes for base "vowel" phoneme + /// + public PhonemeAttributes[] vowelAttr; + + /// + /// 0 if no consonants are taken from previous word; + /// 1 means first one is taken from previous word, etc. + /// + public int prevWordConsonantsCount; + + /// + /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. + /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. + /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible + /// + public bool canAliasBeExtended; + + // helpers + public bool IsStartingV => prevV == "" && cc.Length == 0; + public bool IsVV => prevV != "" && cc.Length == 0; + + public bool IsStartingCV => prevV == "" && cc.Length > 0; + public bool IsVCV => prevV != "" && cc.Length > 0; + + public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; + public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; + + public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; + public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; + + public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); + public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; + } + } + + protected struct Ending { + /// + /// vowel from the last syllable to make VC + /// + public string prevV; + /// + /// actuall CC at the ending + /// + public string[] cc; + /// + /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) + /// + public string tail; + public bool HasTail => !string.IsNullOrEmpty(tail); + /// + /// last note position + duration, all phonemes must be less than this + /// + public int position; + /// + /// last syllable length, max container for all VC CC C- + /// + public int duration; + /// + /// the tone from last syllable, for all ending phonemes + /// + public int tone; + /// + /// Other phoneme attributes from last syllable + /// + public PhonemeAttributes[] attr; + + // helpers + public bool IsEndingV => cc.Length == 0; + public bool IsEndingVC => cc.Length > 0; + public bool IsEndingVCWithOneConsonant => cc.Length == 1; + public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; + } + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + error = ""; + var mainNote = notes[0]; + if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return MakeForcedAliasResult(mainNote); + } + if (hasDictionary && isDictionaryLoading) { + return MakeSimpleResult(""); + } + + runtimeGlides.Clear(); + + var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); + if (syllables == null) { + return HandleError(); + } + + var phonemes = new List(); + int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI + + foreach (var syllable in syllables) { + var modifiedSyllable = ApplyBoundaryReplacements(syllable); + + if (tails.Contains(modifiedSyllable.v)) { + var ending = new Ending { + prevV = modifiedSyllable.prevV, + cc = modifiedSyllable.cc, + tail = modifiedSyllable.v, + position = modifiedSyllable.position, + duration = modifiedSyllable.duration, + tone = modifiedSyllable.tone, + attr = modifiedSyllable.attr + }; + + var endingPhonemes = ProcessEnding(ending); + + if (endingPhonemes != null) { + phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += endingPhonemes.Count; + } + continue; + } + + var syllablePhonemes = ProcessSyllable(modifiedSyllable); + var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); + int currentSyllablePhonemeCount = syllablePhonemes.Count; + + var basePhoneme = madePhonemes.LastOrDefault(); + string baseAlias = basePhoneme.phoneme ?? ""; + if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || + vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { + + string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); + if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { + int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); + madePhonemes.Add(new Phoneme { + phoneme = sustainData.sustain, + position = basePhoneme.position + offsetTicks, + index = globalPhonemeIndex + currentSyllablePhonemeCount + }); + currentSyllablePhonemeCount++; + } + } + phonemes.AddRange(madePhonemes); + globalPhonemeIndex += currentSyllablePhonemeCount; + } + + if (!nextNeighbour.HasValue) { + var tryEnding = MakeEnding(notes); + if (tryEnding.HasValue) { + var ending = tryEnding.Value; + + if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { + ending.tail = nextNeighbour.Value.lyric; + } + + var modifiedEnding = ApplyBoundaryReplacements(ending); + var endingPhonemes = ProcessEnding(modifiedEnding); + + if (endingPhonemes != null) { + 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 = finalPhonemes + }; + } + + protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { + int noteIndex = 0; + for (int i = 0; i < phonemes.Count; i++) { + var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var phoneme = phonemes[i]; + while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { + noteIndex++; + } + + 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); + validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + phoneme.phoneme = validatedAlias; + } else { + phoneme.phoneme = null; + phoneme.position = 0; + } + + phonemes[i] = phoneme; + } + return phonemes.ToArray(); + } + + private Result HandleError() { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = error + } + } + }; + } + + protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = + new YamlDotNet.Serialization.DeserializerBuilder() + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + public override void SetSinger(USinger singer) { + if (this.singer != singer) { + this.singer = singer; + dictionaries.Clear(); + + if (this.singer == null || !this.singer.Loaded) { + return; + } + + if (string.IsNullOrEmpty(YamlFileName)) { + if (backupVowels != null) this.vowels = backupVowels; + else this.vowels = GetVowels(); + + if (backupConsonants != null) this.consonants = backupConsonants; + else this.consonants = GetConsonants(); + + if (backupDictionaryReplacements != null) { + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) { + dictionaryReplacements[kvp.Key] = kvp.Value; + } + } + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + return; + } + + // file paths + string globalFile = Path.Combine(PluginDir, YamlFileName); + string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) + ? Path.Combine(singer.Location, YamlFileName) + : null; + + // Local helper function to update and backup YAML files safely + void UpdateYamlIfNeeded(string filePath, bool isGlobal) { + if (string.IsNullOrEmpty(filePath)) return; + + bool shouldWriteTemplate = false; + bool shouldBackupOldFile = false; + string currentVersion = "unknown"; + + if (File.Exists(filePath)) { + if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { + try { + var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); + currentVersion = checkData?.version?.Trim() ?? ""; + + // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion + if (string.IsNullOrEmpty(currentVersion)) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } else if (Version.TryParse(currentVersion, out Version currV) && + Version.TryParse(YamlVersion, out Version targetV)) { + if (currV < targetV) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { + // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } catch (Exception ex) { + Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); + return; + } + } + } else if (isGlobal && YamlTemplate != null) { + shouldWriteTemplate = true; + } + + if (shouldBackupOldFile && File.Exists(filePath)) { + try { + // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml + string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; + string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); + + if (File.Exists(backupFile)) File.Delete(backupFile); + File.Move(filePath, backupFile); + Log.Information($"Old {YamlFileName} backed up to {backupFile}"); + } catch (Exception e) { + Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); + return; + } + } + + if (shouldWriteTemplate) { + try { + File.WriteAllBytes(filePath, YamlTemplate); + Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); + } catch (Exception e) { + Log.Error(e, $"Failed to write template to {filePath}"); + } + } + } + + UpdateYamlIfNeeded(globalFile, true); + UpdateYamlIfNeeded(singerFile, false); + + // add to parsing list (Global first, Singer second) + var filesToParse = new List(); + if (File.Exists(globalFile)) filesToParse.Add(globalFile); + if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); + + // backups of hardcoded defaults exist + if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); + if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); + if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); + if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); + if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); + + // reset live arrays/lists back to defaults before stacking + vowels = backupVowels; + consonants = backupConsonants; + tails = "-".Split(','); + + fricative = Array.Empty(); + aspirate = Array.Empty(); + semivowel = Array.Empty(); + liquid = Array.Empty(); + nasal = Array.Empty(); + stop = Array.Empty(); + tap = Array.Empty(); + affricate = Array.Empty(); + + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; + + diphthongTails.Clear(); + foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; + + diphthongSplits.Clear(); + foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; + + mergingReplacements.Clear(); + splittingReplacements.Clear(); + yamlFallbacks.Clear(); + PhonemeOverrides.Clear(); + if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); + vowelSustains.Clear(); + foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; + + // parse the files sequentially (Singer configs seamlessly overwrite global configs) + foreach (var file in filesToParse) { + try { + var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); + + var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); + vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); + + var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tails = yamlTails.Concat(tails).Distinct().ToArray(); + + if (data?.isglides != null) enableGlides = data.isglides.Value; + + var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); + fricative = yFricative.Concat(fricative).Distinct().ToArray(); + var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); + var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); + semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); + var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); + liquid = yLiquid.Concat(liquid).Distinct().ToArray(); + var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); + nasal = yNasal.Concat(nasal).Distinct().ToArray(); + var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); + stop = yStop.Concat(stop).Distinct().ToArray(); + var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tap = yTap.Concat(tap).Distinct().ToArray(); + var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + affricate = yAffricate.Concat(affricate).Distinct().ToArray(); + + var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) + .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); + consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); + + // DIPHTHONG AUTO-TAIL DETECTION + var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); + var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); + + foreach (var d in yamlDiphthongs) { + if (!diphthongSplits.ContainsKey(d)) { + foreach (var tail in dynamicTails) { + if (d.EndsWith(tail) && d != tail) { + diphthongTails[d] = tail; + break; + } + } + } + } + + // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) + if (data?.timings != null) { + foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; + } + + if (data?.replacements != null) { + var localMerge = new List(); + var localSplit = new List(); + string GetFromKey(object fromObj) { + if (fromObj is string s) return s; + if (fromObj is System.Collections.IEnumerable e) { + return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); + } + return ""; + } + + foreach (var rawReplacement in data.replacements) { + string fromKey = GetFromKey(rawReplacement.from); + mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + + if (rawReplacement.from is string fromStr) { + dictionaryReplacements.Remove(fromStr); + dictionaryReplacements.Remove(fromStr.ToLower()); + dictionaryReplacements.Remove(fromStr.ToUpper()); + } + + List fromList = rawReplacement.FromList; + List toList = rawReplacement.ToList; + object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); + object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); + + var cleanReplacement = new Replacement { + from = parsedFrom, + to = parsedTo, + where = rawReplacement.where + }; + + if (parsedFrom is string fromString) { + if (parsedTo is string toString) { + // Dictionary handles simple 1-to-1 replacements + dictionaryReplacements[fromString] = toString; + } else { + // 1-to-Many goes to Split + localSplit.Add(cleanReplacement); + } + } else { + // Many-to-Any goes to Merge + localMerge.Add(cleanReplacement); + } + } + mergingReplacements.InsertRange(0, localMerge); + splittingReplacements.InsertRange(0, localSplit); + } + + if (data?.fallbacks != null) { + foreach (var df in data.fallbacks) { + if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { + yamlFallbacks[df.from] = df.to; + } + } + } + + if (data?.diphthongs != null) { + foreach (var d in data.diphthongs) { + if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { + diphthongTails[d.from] = d.to; + } + } + } + + if (data?.vowelsustains != null) { + foreach (var v in data.vowelsustains) { + if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { + vowelSustains[v.symbol] = (v.sustain, v.offset); + } + } + } + + } catch (Exception ex) { + Log.Error($"Failed to parse {file}: {ex.Message}"); + } + } + + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + } + } + + protected USinger singer; + protected bool hasDictionary => dictionaries.ContainsKey(GetType()); + protected IG2p dictionary => dictionaries[GetType()]; + protected bool isDictionaryLoading => dictionaries[GetType()] == null; + protected double TransitionBasicLengthMs => 100; + + private Dictionary dictionaries = new Dictionary(); + private const string FORCED_ALIAS_SYMBOL = "?"; + private string error = ""; + 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 + /// + /// + protected abstract string[] GetVowels(); + + /// + /// Returns list of consonants. Only needed if there is a dictionary + /// + /// + protected virtual string[] GetConsonants() { + throw new NotImplementedException(); + } + + /// + /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc + /// + /// List of phonemes + protected abstract List ProcessSyllable(Syllable syllable); + + /// + /// phoneme symbols for ending, like, V-, or VC-, or VC+C + /// + protected abstract List ProcessEnding(Ending ending); + + /// + /// simple alias to alias fallback + /// + /// + protected virtual Dictionary GetAliasesFallback() { return null; } + + /// + /// Use to some custom init, if needed + /// + protected virtual void Init() { } + + /// + /// Dictionary name. Must be stored in Dictionaries folder. + /// If missing or can't be read, phonetic input is used + /// + /// + protected virtual string GetDictionaryName() { return null; } + + /// + /// extracts array of phoneme symbols from note. Override for procedural dictionary or something + /// reads from dictionary if provided + /// + /// + /// + protected virtual string[] GetSymbols(Note note) { + string[] getSymbolsRaw(string lyrics) { + if (lyrics == null) { + return new string[0]; + } else return lyrics.Split(" "); + } + + if (tails.Contains(note.lyric)) { + return new string[] { note.lyric }; + } + + if (hasDictionary) { + if (!string.IsNullOrEmpty(note.phoneticHint)) { + return getSymbolsRaw(note.phoneticHint); + } + + var result = new List(); + foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { + var subResult = dictionary.Query(subword); + if (subResult == null) { + subResult = HandleWordNotFound(note); + if (subResult == null) { + return null; + } + } else { + for (int i = 0; i < subResult.Length; i++) { + string phoneme = subResult[i]; + if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { + subResult[i] = replaced; + } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { + subResult[i] = replacedExact; + } + } + } + result.AddRange(subResult); + } + return result.ToArray(); + } else { + return getSymbolsRaw(note.lyric); + } + } + + /// + /// 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. + /// + /// + protected virtual Dictionary GetDictionaryPhonemesReplacement() { + return dictionaryReplacements ?? new Dictionary(); + } + private string[] backupVowels = null; + private string[] backupConsonants = null; + private Dictionary backupDiphthongTails = null; + private Dictionary backupDiphthongSplits = null; + private Dictionary backupDictionaryReplacements = null; + protected Dictionary vowelSustains = new Dictionary(); + private Dictionary backupVowelSustains = null; + + /// + /// separates symbols to syllables, without an ending. + /// + /// + /// + /// + protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + var firstVowelId = vowelIds[0]; + if (notes.Length < vowelIds.Length) { + error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; + return null; + } + + var syllables = new Syllable[vowelIds.Length]; + + // Making the first syllable + if (prevEnding.HasValue) { + var prevEndingValue = prevEnding.Value; + var beginningCc = prevEndingValue.cc.ToList(); + beginningCc.AddRange(symbols.Take(firstVowelId)); + + // If we had a prev neighbour ending, let's take info from it + syllables[0] = new Syllable() { + prevV = prevEndingValue.prevV, + cc = beginningCc.ToArray(), + v = symbols[firstVowelId], + tone = prevEndingValue.tone, + attr = prevEndingValue.attr, + duration = prevEndingValue.duration, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes, + prevWordConsonantsCount = prevEndingValue.cc.Count() + }; + } else { + // there is only empty space before us + syllables[0] = new Syllable() { + prevV = "", + cc = symbols.Take(firstVowelId).ToArray(), + v = symbols[firstVowelId], + tone = notes[0].tone, + attr = notes[0].phonemeAttributes, + duration = -1, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes + }; + } + + // normal syllables after the first one + var noteI = 1; + var ccs = new List(); + var position = 0; + var lastSymbolI = firstVowelId + 1; + for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { + if (!vowelIds.Contains(lastSymbolI)) { + ccs.Add(symbols[lastSymbolI]); + } else { + position += notes[noteI - 1].duration; + syllables[noteI] = new Syllable() { + prevV = syllables[noteI - 1].v, + cc = ccs.ToArray(), + v = symbols[lastSymbolI], + tone = notes[noteI - 1].tone, + attr = notes[noteI - 1].phonemeAttributes, + duration = notes[noteI - 1].duration, + position = position, + vowelTone = notes[noteI].tone, + vowelAttr = notes[noteI].phonemeAttributes, + canAliasBeExtended = true // for all not-first notes is allowed + }; + ccs = new List(); + noteI++; + } + } + + return syllables; + } + + /// + /// extracts word ending + /// + /// + /// + protected Ending? MakeEnding(Note[] inputNotes) { + if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return null; + } + + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + + return new Ending() { + prevV = symbols[vowelIds.Last()], + cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), + tone = notes.Last().tone, + attr = notes.Last().phonemeAttributes, + duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), + position = notes.Sum(n => n.duration) + }; + } + + /// + /// extracts and validates symbols and vowels + /// + /// + /// + private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { + var mainNote = notes[0]; + var symbols = GetSymbols(mainNote); + if (symbols == null) { + return (null, null, null); + } + if (symbols.Length == 0) { + symbols = new string[] { "" }; + } + + symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); + symbols = ApplyExtensions(symbols, notes); + List vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + vowelIds.Add(symbols.Length - 1); + } + if (notes.Length < vowelIds.Count) { + notes = HandleNotEnoughNotes(notes, vowelIds); + } + return (symbols, vowelIds.ToArray(), notes); + } + + /// + /// When there are more syllables than notes, recombines notes to match syllables count + /// + /// + /// + /// + protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { + var newNotes = new List(); + newNotes.AddRange(notes.SkipLast(1)); + var lastNote = notes.Last(); + var position = lastNote.position; + var notesToSplit = vowelIds.Count - newNotes.Count; + var duration = lastNote.duration / notesToSplit / 15 * 15; + for (var i = 0; i < notesToSplit; i++) { + var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); + newNotes.Add(new Note() { + position = position, + duration = durationFinal, + tone = lastNote.tone, + phonemeAttributes = lastNote.phonemeAttributes + }); + position += durationFinal; + } + + return newNotes.ToArray(); + } + + /// + /// Override this method, if you want to implement some machine converting from a word to phonemes + /// + /// + /// + protected virtual string[] HandleWordNotFound(Note note) { + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); + if(HasOto(mpdlyric, note.tone)){ + error = mpdlyric; + }else{ + error = "word not found"; + } + return null; + } + + /// + /// Does this note extend the previous syllable? + /// + /// + /// + protected bool IsSyllableVowelExtensionNote(Note note) { + return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); + } + + /// + /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic + /// + /// + /// + protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { + return phonemesString.Split(' '); + } + + /// + /// use to validate alias + /// + /// + /// + protected virtual string ValidateAlias(string alias) { + return alias; + } + + /// + /// Defines basic transition length before scaling it according to tempo and note length + /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation + /// + /// Mapped alias + /// + protected virtual double GetTransitionBasicLengthMs(string alias = "") { + return GetTransitionBasicLengthMsByConstant(); + } + + 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 + /// + /// + protected double GetTempoNoteLengthFactor() { + return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; + } + + protected virtual IG2p[] GetBaseG2ps() { + return Array.Empty(); + } + + protected virtual IG2p LoadBaseDictionary() { + var g2ps = new List(); + + // Native YAML Dictionary Logic + if (!string.IsNullOrEmpty(YamlFileName)) { + string path = Path.Combine(PluginDir, YamlFileName); + + // Write template if missing + if (!File.Exists(path) && YamlTemplate != null) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + + // Load dictionary from Singer Folder (Highest Priority) + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + + // Load dictionary from Plugin Folder (Fallback Priority) + if (File.Exists(path)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {path}"); + } + } + } + // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) + else { + var dictionaryName = GetDictionaryName(); + if (!string.IsNullOrEmpty(dictionaryName)) { + var filename = Path.Combine(DictionariesPath, dictionaryName); + if (File.Exists(filename)) { + var dictionaryText = File.ReadAllText(filename); + var builder = G2pDictionary.NewBuilder(); + foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); + foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); + builder.AddEntry("a", new string[] { "a" }); + ParseDictionary(dictionaryText, builder); + g2ps.Add(builder.Build()); + } + } + } + + // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) + var childG2ps = GetBaseG2ps(); + if (childG2ps != null && childG2ps.Any()) { + g2ps.AddRange(childG2ps); + } + + return new G2pFallbacks(g2ps.ToArray()); + } + + /// + /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, + /// and replaces phonemes with replacement table + /// Is Running Async! + /// + /// + /// + protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { + var replacements = GetDictionaryPhonemesReplacement(); + foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { + if (line.StartsWith(";;;")) { + continue; + } + var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); + if (parts.Length != 2) { + continue; + } + string key = parts[0].ToLowerInvariant(); + var values = GetDictionaryWordPhonemes(parts[1]).Select( + n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); + lock (builder) { + builder.AddEntry(key, values); + }; + }; + } + + #region helpers + + /// + /// May be used if you have different logic for short and long notes + /// + /// + /// + protected bool IsShort(Syllable syllable) { + return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; + } + 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 + /// + /// + /// + /// + protected bool HasOto(string alias, int tone) { + return singer.TryGetMappedOto(alias, tone, out _); + } + + /// + /// Can be used for different variants, like exhales [v R], [v -] etc + /// + /// phonemes container to add to + /// to map alias + /// target phoneme variants + /// returns true if added any + protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { + foreach (var phoneme in targetPhonemes) { + if (HasOto(phoneme, tone)) { + sourcePhonemes.Add(phoneme); + return true; + } + } + 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 + /// + /// + /// + protected bool CanMakeAliasExtension(Syllable syllable) { + return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; + } + + /// + /// if current syllable is VV and previous one is from the same pitch, + /// you may wan't to just extend the previous alias. Put the phoneme as null fot that + /// + /// + /// + /// + protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { + if (singer.Subbanks.Count == 1) { + return true; + } + if (tone1 == tone2) { + return true; + } + var toneSets = singer.Subbanks.Select(n => n.toneSet); + foreach (var toneSet in toneSets) { + if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { + return true; + } + if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { + return false; + } + } + return true; + } + + protected virtual string YamlFileName => null; + protected virtual byte[] YamlTemplate => null; + protected virtual string YamlVersion => null; + + protected string[] vowels = Array.Empty(); + protected string[] consonants = Array.Empty(); + protected string[] tails = "-,R".Split(','); + protected string[] affricate = Array.Empty(); + protected string[] fricative = Array.Empty(); + protected string[] aspirate = Array.Empty(); + protected string[] semivowel = Array.Empty(); + protected string[] liquid = Array.Empty(); + protected string[] nasal = Array.Empty(); + protected string[] stop = Array.Empty(); + protected string[] tap = Array.Empty(); + + protected Dictionary dictionaryReplacements = new Dictionary(); + protected Dictionary PhonemeOverrides = new Dictionary(); + protected Dictionary yamlFallbacks = new Dictionary(); + protected List consExceptions = new List(); + + protected Dictionary diphthongTails = new Dictionary(); + protected Dictionary diphthongSplits = new Dictionary(); + + 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(); + public Timings[] timings { get; set; } = Array.Empty(); + public DiphthongData[] diphthongs { get; set; } = Array.Empty(); + public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); + + public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } + public struct Fallbacks { public string from { get; set; } public string to { get; set; } } + public struct Timings { public string symbol { get; set; } public double value { get; set; } } + public struct DiphthongData { public string from { get; set; } public string to { get; set; } } + public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } + } + + public class Replacement { + public object from { get; set; } + public object to { get; set; } + public string where { get; set; } = "inside"; + + public List FromList { + get { + if (from is string s) return new List { s }; + if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + + public List ToList { + get { + if (to is string s) return new List { s }; + if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + } + + protected List mergingReplacements = new List(); + protected List splittingReplacements = new List(); + + protected virtual bool IsGroupKeyword(string rulePhoneme) { + // 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 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; + } + } + } + + bool inBaseGroup = false; + switch (baseGroup) { + case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; + case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; + case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; + case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; + case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; + case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; + case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; + case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; + case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; + case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; + } + + if (!inBaseGroup) return false; + + if (cleanRule.Contains("!")) { + string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; + if (excluded.Split(',').Contains(actualPhoneme)) return false; + } + + if (cleanRule.Contains("=")) { + string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; + if (!restricted.Split(',').Contains(actualPhoneme)) return false; + } + + return true; + } + + protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; + + List finalPhonemes = new List(); + int idx = 0; + + var validRules = mergingReplacements.Concat(splittingReplacements) + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + var validSplits = splittingReplacements + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + while (idx < inputPhonemes.Count) { + bool replaced = false; + + foreach (var rule in validRules) { + 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.Count; j++) { + string rulePh = fromArray[j]; + string actualPh = inputPhonemes[idx + j]; + + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh)) { + if (IsGroupMatch(rulePh, actualPh)) { + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); + } else { + match = false; break; + } + } else if (rulePh != actualPh) { + match = false; break; + } + } + + if (match) { + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + 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++) { + // 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.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) { + List fromArray = rule.FromList; + if (fromArray == null || fromArray.Count != 1) continue; + + string rulePh = fromArray[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { + + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + foreach(string toPh in toArray) { + 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; + } + } + } + if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } else if (!replaced) { + finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } + } + return finalPhonemes; + } + + private Syllable ApplyBoundaryReplacements(Syllable syllable) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); + bool hasV = !string.IsNullOrEmpty(syllable.v); + + 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) || !hasPrevV; + List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); + + string newPrevV = ""; + string newV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + if (hasV && finalPhonemes.Count > 0) { + var vowelsList = GetVowels(); + int vIndex = finalPhonemes.Count - 1; + + for (int i = finalPhonemes.Count - 1; i >= 0; i--) { + if (vowelsList.Contains(finalPhonemes[i])) { + vIndex = i; + break; + } + } + newV = finalPhonemes[vIndex]; + for (int i = 0; i < vIndex; i++) { + newCc.Add(finalPhonemes[i]); + } + } else { + newCc.AddRange(finalPhonemes); + } + } + + syllable.prevV = newPrevV; + syllable.cc = newCc.ToArray(); + syllable.v = newV; + return syllable; + } + + private Ending ApplyBoundaryReplacements(Ending ending) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); + currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); + if (ending.cc != null) currentPhonemes.AddRange(ending.cc); + + List finalPhonemes = ApplyReplacements(currentPhonemes, true); + + string newPrevV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + newCc.AddRange(finalPhonemes); + } + + ending.prevV = newPrevV; + ending.cc = newCc.ToArray(); + return ending; + } + + #endregion + + #region private + + private Result MakeForcedAliasResult(Note note) { + return MakeSimpleResult(note.lyric.Substring(1)); + } + + protected void ReadDictionaryAndInit() { + var dictionaryName = GetDictionaryName(); + if (dictionaryName == null) { + return; + } + dictionaries[GetType()] = null; + if (Testing) { + ReadDictionary(dictionaryName); + Init(); + return; + } + OnAsyncInitStarted(); + Task.Run(() => { + ReadDictionary(dictionaryName); + Init(); + OnAsyncInitFinished(); + }); + } + + private void ReadDictionary(string dictionaryName) { + try { + var phonemeSymbols = new Dictionary(); + + foreach (var vowel in GetVowels()) { + phonemeSymbols[vowel] = true; + } + foreach (var consonant in GetConsonants()) { + phonemeSymbols[consonant] = false; + } + + var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); + var safeDict = new Dictionary(); + + foreach (var kvp in childDict) { + safeDict[kvp.Key] = kvp.Value; + safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' + safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' + } + + dictionaries[GetType()] = new G2pRemapper( + LoadBaseDictionary(), + phonemeSymbols, + safeDict); + + } catch (Exception ex) { + Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); + } + } + + private string[] ApplyExtensions(string[] symbols, Note[] notes) { + var newSymbols = new List(); + var vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + // no syllables or all consonants, the last phoneme will be interpreted as vowel + vowelIds.Add(symbols.Length - 1); + } + var lastVowelI = 0; + newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); + for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { + if (!IsSyllableVowelExtensionNote(notes[i])) { + var prevVowel = vowelIds[lastVowelI]; + lastVowelI++; + var vowel = vowelIds[lastVowelI]; + newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); + } else { + newSymbols.Add(symbols[vowelIds[lastVowelI]]); + } + } + newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); + return newSymbols.ToArray(); + } + + private List ExtractVowels(string[] symbols) { + var vowelIds = new List(); + var vowels = GetVowels(); + for (var i = 0; i < symbols.Length; i++) { + if (vowels.Contains(symbols[i])) { + vowelIds.Add(i); + } + } + 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]; + + 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) { + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + baseLengthMs = GetTransitionBasicLengthMs(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; + } + } + } + + 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] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex + }; + + if (i == 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) { + // YAML Override active: Use the multiplier and bypass NoGap entirely + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); + } else { + // Default behavior + baseLengthMs = GetTransitionBasicLengthMsByOto(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 + phonemes[phonemeI].position = MsToTick(baseLengthMs); + } + } + } else { + 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]; + } + } else { + // 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 - 1 : phonemeSymbols.Count - 1, containerLength); + } + + private string ValidateAliasIfNeeded(string alias, int tone) { + if (HasOto(alias, tone)) { + return alias; + } + return ValidateAlias(alias); + } + + private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { + var offset = 0; + var lengthModifier = 1.0; + + if (containerLengthTick > 0) { + var allTransitionsLengthTick = phonemes.Sum(n => n.position); + + // 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--) { + if (phonemes[i].phoneme == null) continue; + var finalLengthTick = (int)(phonemes[i].position * lengthModifier); + phonemes[i].position = startPosition - finalLengthTick - offset; + offset += finalLengthTick; + } + + return phonemes.Where(n => n.phoneme != null).ToArray(); + } + + #endregion + } +} diff --git a/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs b/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs new file mode 100644 index 000000000..92bc4780b --- /dev/null +++ b/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GenericSyllableBasedPhonemizer.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GenericSyllableBasedPhonemizer.Data.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] generic_template { + get { + object obj = ResourceManager.GetObject("generic_template", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/GenericSyllableBasedPhonemizer/Data/Resources.resx b/GenericSyllableBasedPhonemizer/Data/Resources.resx new file mode 100644 index 000000000..cbc43d29b --- /dev/null +++ b/GenericSyllableBasedPhonemizer/Data/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + generic.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/GenericSyllableBasedPhonemizer/Data/generic.template.yaml b/GenericSyllableBasedPhonemizer/Data/generic.template.yaml new file mode 100644 index 000000000..ab5842086 --- /dev/null +++ b/GenericSyllableBasedPhonemizer/Data/generic.template.yaml @@ -0,0 +1,150 @@ +%YAML 1.2 +--- +symbols: +#Delta-style (X-SAMPA) symbols + - {symbol: a, type: vowel} + - {symbol: e, type: vowel} + - {symbol: i, type: vowel} + - {symbol: o, type: vowel} + - {symbol: u, type: vowel} + - {symbol: A, type: vowel} + - {symbol: E, type: vowel} + - {symbol: I, type: vowel} + - {symbol: O, type: vowel} + - {symbol: U, type: vowel} + - {symbol: '{', type: vowel} + - {symbol: V, type: vowel} + - {symbol: 3, type: vowel} + - {symbol: aI, type: vowel} + - {symbol: eI, type: vowel} + - {symbol: OI, type: vowel} + - {symbol: aU, type: vowel} + - {symbol: oU, type: vowel} + - {symbol: tS, type: affricate} + - {symbol: D, type: fricative} + - {symbol: h, type: aspirate} + - {symbol: dZ, type: affricate} + - {symbol: N, type: nasal} + - {symbol: S, type: fricative} + - {symbol: T, type: fricative} + - {symbol: j, type: semivowel} + - {symbol: Z, type: fricative} +#Arpasing + - {symbol: aa, type: vowel} + - {symbol: ae, type: vowel} + - {symbol: ah, type: vowel} + - {symbol: ao, type: vowel} + - {symbol: aw, type: vowel} + - {symbol: ay, type: vowel} + - {symbol: b, type: stop} + - {symbol: ch, type: affricate} + - {symbol: d, type: stop} + - {symbol: dh, type: fricative} + - {symbol: eh, type: vowel} + - {symbol: er, type: vowel} + - {symbol: ey, type: vowel} + - {symbol: f, type: fricative} + - {symbol: g, type: stop} + - {symbol: hh, type: aspirate} + - {symbol: ih, type: vowel} + - {symbol: iy, type: vowel} + - {symbol: jh, type: affricate} + - {symbol: k, type: stop} + - {symbol: l, type: liquid} + - {symbol: m, type: nasal} + - {symbol: n, type: nasal} + - {symbol: ng, type: nasal} + - {symbol: ow, type: vowel} + - {symbol: oy, type: vowel} + - {symbol: p, type: stop} + - {symbol: r, type: liquid} + - {symbol: s, type: fricative} + - {symbol: sh, type: fricative} + - {symbol: t, type: stop} + - {symbol: th, type: fricative} + - {symbol: uh, type: vowel} + - {symbol: uw, type: vowel} + - {symbol: v, type: fricative} + - {symbol: w, type: semivowel} + - {symbol: y, type: semivowel} + - {symbol: z, type: fricative} + - {symbol: zh, type: fricative} + #bonus symbols + - {symbol: e@, type: vowel} + - {symbol: e@n, type: vowel} + - {symbol: e@m, type: vowel} + - {symbol: eN, type: vowel} + - {symbol: IN, type: vowel} + - {symbol: Ar, type: vowel} + - {symbol: Qr, type: vowel} + - {symbol: Er, type: vowel} + - {symbol: Ir, type: vowel} + - {symbol: Or, type: vowel} + - {symbol: Ur, type: vowel} + - {symbol: ir, type: vowel} + - {symbol: ur, type: vowel} + - {symbol: aIr, type: vowel} + - {symbol: aUr, type: vowel} + - {symbol: A@, type: vowel} + - {symbol: Q@, type: vowel} + - {symbol: E@, type: vowel} + - {symbol: I@, type: vowel} + - {symbol: O@, type: vowel} + - {symbol: U@, type: vowel} + - {symbol: i@, type: vowel} + - {symbol: u@, type: vowel} + - {symbol: aI@, type: vowel} + - {symbol: aU@, type: vowel} + - {symbol: Q, type: vowel} + - {symbol: 1, type: vowel} + - {symbol: Ol, type: vowel} + - {symbol: aUn, type: vowel} + - {symbol: '@r', type: vowel} + - {symbol: '@l', type: vowel} + - {symbol: '@m', type: vowel} + - {symbol: '@n', type: vowel} + - {symbol: '@N', type: vowel} + - {symbol: '@', type: vowel} + - {symbol: y, type: vowel} + - {symbol: I\, type: vowel} + - {symbol: M, type: vowel} + - {symbol: U\, type: vowel} + - {symbol: Y, type: vowel} + - {symbol: '@\', type: vowel} + - {symbol: '@`', type: vowel} + - {symbol: 3`, type: vowel} + - {symbol: A`, type: vowel} + - {symbol: Q`, type: vowel} + - {symbol: E`, type: vowel} + - {symbol: I`, type: vowel} + - {symbol: O`, type: vowel} + - {symbol: U`, type: vowel} + - {symbol: i`, type: vowel} + - {symbol: u`, type: vowel} + - {symbol: aI`, type: vowel} + - {symbol: aU`, type: vowel} + - {symbol: '}', type: vowel} + - {symbol: 2, type: vowel} + - {symbol: 3\, type: vowel} + - {symbol: 6, type: vowel} + - {symbol: 7, type: vowel} + - {symbol: 8, type: vowel} + - {symbol: 9, type: vowel} + - {symbol: '&', type: vowel} + - {symbol: '{~', type: vowel} + - {symbol: I~, type: vowel} + - {symbol: aU~, type: vowel} + - {symbol: VI, type: vowel} + - {symbol: VU, type: vowel} + - {symbol: '@U', type: vowel} + - {symbol: 'i:', type: vowel} + - {symbol: 'u:', type: vowel} + - {symbol: 'O:', type: vowel} + - {symbol: e@0, type: vowel} + - {symbol: 4, type: tap} + - {symbol: W, type: fricative} + - {symbol: 't_}', type: stop} + +entries: + - grapheme: openutau + phonemes: [ow, p, ah, n, uw, t, aw] diff --git a/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj b/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj new file mode 100644 index 000000000..5d92a0bf9 --- /dev/null +++ b/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs b/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs new file mode 100644 index 000000000..b662aebdb --- /dev/null +++ b/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs @@ -0,0 +1,1050 @@ +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using OpenUtau.Plugin.Builtin; +using Serilog; +using System.Text.RegularExpressions; + +namespace GenericSyllableBasedPhonemizer { + [Phonemizer("Generic Syllable-Based Phonemizer", "GEN SYL", "HeadHunter4621")] + // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. + // The default template contains X-SAMPA phonemes but can (and should) be modified to fit your voicebank. + public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { + protected override string YamlFileName => "generic-phonemizer.yaml"; + protected override byte[] YamlTemplate => Data.Resources.generic_template; + + public GenericSyllablePhonemizer() { + + this.vowels = Array.Empty(); + this.consonants = Array.Empty(); + } + + protected override string[] GetVowels() => vowels; + protected override string[] GetConsonants() => consonants; + protected override string GetDictionaryName() => ""; + + List consExceptions = new List(); + + string[] diphthongs = Array.Empty(); + + // For banks with missing vowels + private readonly Dictionary missingVphonemes = "ax=a".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingVPhonemes = false; + private bool isYamlFallbacks = false; + + + // For banks with missing custom consonants + private readonly Dictionary missingCphonemes = "N=n".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingCPhonemes = false; + private bool cPV_FallBack = false; + + private readonly Dictionary vvDiphthongExceptions = + new Dictionary() { + {"aw","a"}, + {"ow","o"}, + {"iw","i"}, + {"ay","a"}, + {"ey","e"}, + {"oy","o"}, + {"uy","u"}, + {"ew","e"}, + }; + + private readonly Dictionary vvExceptions = + new Dictionary() { + {"aw","w"}, + {"ow","w"}, + {"iw","w"}, + {"ay","y"}, + {"ey","y"}, + {"oy","y"}, + {"uy","y"}, + {"ew","w"}, + }; + + private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; + private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; + private static readonly string[] FinalConsonants = { "w", "y", "r", "l", "m", "n", "ng" }; + + + protected override string[] GetSymbols(Note note) { + string[] original = base.GetSymbols(note); + return note.lyric.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + + //if (original == null || original.Length == 0) { + // string lyric = note.lyric.ToLowerInvariant(); + // List fallbackSplit = new List(); + // string[] vowels = GetVowels(); + // string[] consonants = GetConsonants(); + + // // Handle apostrophes at the start or end + // bool hasLeadingApostrophe = lyric.StartsWith("'"); + // bool hasTrailingApostrophe = lyric.EndsWith("'"); + + // if (hasLeadingApostrophe) { + // lyric = lyric.Substring(1); + // } + // if (hasTrailingApostrophe && lyric.Length > 0) { + // lyric = lyric.Substring(0, lyric.Length - 1); + // } + + // int ii = 0; + // while (ii < lyric.Length) { + // string match = null; + // foreach (var cons in consonants.OrderByDescending(c => c.Length)) { + // if (lyric.Substring(ii).StartsWith(cons)) { + // match = cons; + // break; + // } + // } + // if (match == null) { + // foreach (var vow in vowels.OrderByDescending(v => v.Length)) { + // if (lyric.Substring(ii).StartsWith(vow)) { + // match = vow; + // break; + // } + // } + // } + // if (match != null) { + // fallbackSplit.Add(match); + // ii += match.Length; + // } else { + // fallbackSplit.Add(lyric[ii].ToString()); + // ii++; + // } + // } + // // Add "q" at the beginning or end if needed + // if (hasLeadingApostrophe) { + // fallbackSplit.Insert(0, "q"); + // } + // if (hasTrailingApostrophe) { + // fallbackSplit.Add("q"); + + // } + // original = fallbackSplit.ToArray(); + //} + + //List modified = new List(original); + //List finalPhonemes = new List(); + //finalPhonemes = new List(modified); + //List finalProcessedPhonemes = new List(); + //IEnumerable phonemes; + //phonemes = finalPhonemes; + //foreach (string s in phonemes) { + // switch (s) { + // default: + // finalProcessedPhonemes.Add(s); + // break; + // } + //} + //return finalProcessedPhonemes.ToArray(); + } + + protected override IG2p LoadBaseDictionary() { + var g2ps = new List(); + // LOAD DICTIONARY FROM FOLDER + string path = Path.Combine(PluginDir, YamlFileName); + if (!File.Exists(path)) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + // LOAD DICTIONARY FROM SINGER FOLDER + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + //g2ps.Add(new ArpabetPlusG2p()); + return new G2pFallbacks(g2ps.ToArray()); + } + public override void SetSinger(USinger singer) { + base.SetSinger(singer); + + if (this.singer != null && this.singer.Loaded) { + + consExceptions.Clear(); + if (stop != null) consExceptions.AddRange(stop); + if (tap != null) consExceptions.AddRange(tap); + + consExceptions = consExceptions.Distinct().ToList(); + } + } + + // prioritize yaml replacements over dictionary replacements + private string ReplacePhoneme(string phoneme, int tone) { + // If the original phoneme has an OTO, use it directly. + 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; + } + protected override List ProcessSyllable(Syllable syllable) { + syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; + var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); + var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; + string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string v = ReplacePhoneme(syllable.v, syllable.vowelTone); + List vowels = new List { v }; + string basePhoneme; + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + int prevWordConsonantsCount = syllable.prevWordConsonantsCount; + + // Check for missing vowel phonemes + foreach (var entry in missingVphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { + isMissingVPhonemes = true; + break; + } + } + + // Check for missing consonant phonemes + foreach (var entry in missingCphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isMissingCPhonemes = true; + break; + } + } + + foreach (var entry in yamlFallbacks) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isYamlFallbacks = true; + break; + } + } + + // STARTING V + if (syllable.IsStartingV) { + basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); + } + // [V V] or [V C][C V]/[V] + else if (syllable.IsVV) { + if (!CanMakeAliasExtension(syllable)) { + basePhoneme = $"{prevV} {v}"; + if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && vvExceptions.ContainsKey(prevV) && prevV != v) { + // VV IS NOT PRESENT, CHECKS VVEXCEPTIONS LOGIC + //var vc = $"{prevV}{vvExceptions[prevV]}"; + var vc = AliasFormat($"{vvExceptions[prevV]}", "vcEx", syllable.vowelTone, prevV); + TryAddPhoneme(phonemes, syllable.vowelTone, vc); + basePhoneme = AliasFormat($"{vvExceptions[prevV]} {v}", "dynMid", syllable.vowelTone, ""); + } else { + { + if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV} {v}"; + } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{prevV} -", "dynMid", syllable.vowelTone, "")); + } + } + } + // EXTEND AS [V] + } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone) || missingVphonemes.ContainsKey(prevV)) { + basePhoneme = v; + } else if (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone) && vvDiphthongExceptions.ContainsKey(prevV)) { + basePhoneme = $"{vvDiphthongExceptions[prevV]} {vvDiphthongExceptions[prevV]}"; + } else { + // PREVIOUS ALIAS WILL EXTEND as [V V] + basePhoneme = null; + } + + // [- CV/C V] or [- C][CV/C V] + } else if (syllable.IsStartingCVWithOneConsonant) { + var rcv = $"- {cc[0]} {v}"; + var rcv1 = $"- {cc[0]}{v}"; + var crv = $"{cc[0]} {v}"; + /// - CV + if (HasOto(rcv, syllable.vowelTone) && HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) && HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); + /// CV + } else if (HasOto(crv, syllable.vowelTone) && HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } else { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } + // [CCV/CC V] or [C C] + [CV/C V] + } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { + // TRY [- CCV]/[- CC V] or [- CC][CCV]/[CC V] or [- C][C C][C V]/[CV] + var rccv = $"- {string.Join("", cc)} {v}"; + var rccv1 = $"- {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + var crv1 = $"{cc.Last()}{v}"; + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + /// - CCV + if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); + lastC = 0; + } else { + /// CCV and CV + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // TRY RCC [- CC] + for (var i = cc.Length; i > 1; i--) { + if (!ccvException.Contains(cc[0])) { + if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""))) { + firstC = i - 1; + } + } + break; + } + // [- C] + if (phonemes.Count == 0) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } + // try [CC V] or [CCV] + var cv = $"{cc.Last()}{v}"; + for (var i = firstC; i < cc.Length - 1; i++) { + /// CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + /// C-Last + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + } + } else { // VCV + var vcv = $"{prevV} {cc[0]}{v}"; + var vcv2 = $"{prevV}{cc[0]}{v}"; + var vcvEnd = $"{prevV}{cc[0]} {v}"; + var vccv = $"{prevV} {string.Join("", cc)}{v}"; + var vccv2 = $"{prevV} {string.Join("", cc)}"; + var vccv3 = $"{prevV}{string.Join("", cc)}"; + var crv = $"{cc.Last()} {v}"; + // Use regular VCV if the current word starts with one consonant and the previous word ends with none + if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv2, syllable.vowelTone) && HasOto(ValidateAlias(vcv2), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv2; + // Use end VCV if current word does not start with a consonant but the previous word does end with one + } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) && HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { + basePhoneme = vcvEnd; + // Use regular VCV if end VCV does not exist + } else if (syllable.IsVCVWithOneConsonant && (!HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone)) && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone))) { + basePhoneme = vcv; + // VCV with multiple consonants, only for current word onset and null previous word ending + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) && HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { + basePhoneme = vccv; + lastC = 0; + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv3, syllable.vowelTone) && HasOto(ValidateAlias(vccv3), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{prevV} {string.Join("", cc)}{v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else { + var cv = cc.Last() + v; + basePhoneme = cv; + if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { + basePhoneme = crv; + } + // try [CC V] or [CCV] + for (var i = firstC; i < cc.Length - 1; i++) { + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + /// CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + /// C-Last + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + // try [V C], [V CC], [VC C], [V -][- C] + for (var i = lastC + 1; i >= 0; i--) { + var vr = $"{prevV} -"; + //var vc_c = $"{prevV}{string.Join(" ", cc.Take(2))}-"; + //var vc_c2 = $"{prevV}{string.Join(" ", cc.Take(2))}_"; + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vc = $"{prevV} {cc[0]}"; + // Boolean Triggers + bool CCV = false; + if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { + if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { + CCV = true; + } + } + + if (i == 0 && (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) && !HasOto(vc, syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, vr, ValidateAlias(vr)); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + break; + } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { + TryAddPhoneme(phonemes, syllable.tone, vcc, ValidateAlias(vcc)); + firstC = 1; + break; + /*} else if (HasOto(vc_c, syllable.tone) && HasOto(ValidateAlias(vc_c), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} -", "dynMid", syllable.vowelTone, "")); + firstC = 1; + break; + } else if (HasOto(vc_c2, syllable.tone) && HasOto(ValidateAlias(vc_c2), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} _", "dynMid", syllable.vowelTone, "")); + firstC = 1; + break;*/ + } else if (cPV_FallBack && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + break; + } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + break; + } else { + continue; + } + } + } + } + + for (var i = firstC; i < lastC; i++) { + var ccv = $"{string.Join("", cc.Skip(i + 1))} {v}"; + var ccv1 = $"{string.Join("", cc.Skip(i + 1))}{v}"; + var cc1 = $"{string.Join(" ", cc.Skip(i))}"; + var lcv = $"{cc.Last()} {v}"; + var cv = $"{cc.Last()}{v}"; + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CC FALLBACKS + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + var c1 = cc[i]; + var c2 = cc[i + 1]; + bool c1IsException = consExceptions.Contains(c1); + bool c2IsException = consExceptions.Contains(c2); + + // Scenario 1: Both are NOT exceptions + if (!c1IsException && !c2IsException) { + // [C1 -] [- C2] + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); + } + // Scenario 2: C1 is an exception, C2 is NOT + else if (c1IsException && !c2IsException) { + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + } + // Scenario 3: C1 is NOT an exception, C2 is + else if (!c1IsException && c2IsException) { + cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); + } + // Scenario 4: Both are exceptions + else if (c1IsException && c2IsException) { + cc1 = ""; + } + } + // CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // [C1 C2C3] + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + } + // CV + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + + if (i + 1 < lastC) { + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CC FALLBACKS + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + var c1 = cc[i]; + var c2 = cc[i + 1]; + bool c1IsException = consExceptions.Contains(c1); + bool c2IsException = consExceptions.Contains(c2); + + // Scenario 1: Both are NOT exceptions + if (!c1IsException && !c2IsException) { + // [C1 -] [- C2] + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); + } + // Scenario 2: C1 is an exception, C2 is NOT + else if (c1IsException && !c2IsException) { + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + } + // Scenario 3: C1 is NOT an exception, C2 is + else if (!c1IsException && c2IsException) { + cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); + } + // Scenario 4: Both are exceptions + else if (c1IsException && c2IsException) { + cc1 = ""; + } + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // [C1 C2C3] + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + } + // CV + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + + if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { + // like [V C1] [C1 C2] [C2 C3] [C3 ..] + TryAddPhoneme(phonemes, syllable.vowelTone, cc1); + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { + // like [V C1] [C1 C2] [C2 ..] + if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { + i++; + } + } else { + // like [V C1] [C1] [C2 ..] + TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); + } + } else { + TryAddPhoneme(phonemes, syllable.tone, cc1); + } + } + + phonemes.Add(basePhoneme); + return phonemes; + } + + protected override List ProcessEnding(Ending ending) { + string prevV = ReplacePhoneme(ending.prevV, ending.tone); + string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); + string v = ReplacePhoneme(ending.prevV, ending.tone); + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; + + if (ending.IsEndingV) { + var vR = $"{v} {t}"; + var vR2 = $"{v}{t}"; + if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{v}", "ending", ending.tone, "", t))); + } + } else if (ending.IsEndingVCWithOneConsonant) { + var vc = $"{v} {cc[0]}"; + var vcr = $"{v} {cc[0]}{t}"; + var vcr2 = $"{v}{cc[0]} {t}"; + var vcr3 = $"{v}{cc[0]}{t}"; + if (!RomajiException.Contains(cc[0])) { + if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ValidateAlias(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t))); + } else if (!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone) || !HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || !HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc); + if (vc.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + } + } else { + TryAddPhoneme(phonemes, ending.tone, vc); + if (vc.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + } + } + } + } else { + for (var i = lastC; i >= 0; i--) { + var vr = $"{v} {t}"; + var vr1 = $"{v} R"; + var vr2 = $"{v}{t}"; + var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; + var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} {t}"; + var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; + var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; + var vc = $"{v} {cc[0]}"; + if (!RomajiException.Contains(cc[0])) { + if (i == 0) { + if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t)); + } + break; + } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc); + firstC = 1; + break; + } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc2); + firstC = 1; + break; + } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc3); + if (vcc3.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + } + } + firstC = 1; + break; + } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc4); + if (vcc4.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + } + } + firstC = 1; + break; + } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) + || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) + || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) + || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc); + break; + } else { + TryAddPhoneme(phonemes, ending.tone, vc); + break; + } + } + } + for (var i = firstC; i < lastC; i++) { + var cc1 = $"{cc[i]} {cc[i + 1]}"; + if (i < cc.Length - 2) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + + if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { + // [C1 -] [- C2] + cc2 = AliasFormat($"{cc[i + 2]}", "cc_inB", ending.tone, ""); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { + // like [C1 C2][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc1); + } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { + // like [C1 C2-][C3 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i]); + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { + // like [C1 C2-][C3 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + i++; + } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + // [C1 -] [- C2] + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); + i++; + } else { + // like [C1][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); + i++; + } + } else { + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 -] [- C2] + if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + cc1 = AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, ""); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "", t)); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { + // like [C1 C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]} {t}", ValidateAlias($"{cc[i]} {cc[i + 1]} {t}"))) { + // like [C1 C2 -] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}{t}", ValidateAlias($"{cc[i]}{cc[i + 1]}{t}"))) { + // like [C1C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]} {t}", ValidateAlias($"{cc[i]}{cc[i + 1]} {t}"))) { + // like [C1C2 -] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + // like [C1 C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + // [C1 -] [- C2] + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 2]}", "cc_endB", ending.tone, "", t)); + i++; + } + } + } + } + return phonemes; + } + 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[] { "" } }, + { "dynEnd", new string[] { "" } }, + { "startingV", new string[] { "-", "- ", "_", "" } }, + { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, + { "vvExtend", new string[] { "", "_", "-", "- " } }, + { "cv", new string[] { "-", "", "- ", "_" } }, + { "cvStart", new string[] { "-", "- ", "_" } }, + { "ending", new string[] { $" {t}", $"{t}"} }, + { "ending_mix", new string[] { $"{t}", $" {t}", "--" } }, + { "cc", new string[] { "", "-", "- ", "_" } }, + { "cc_start", new string[] { "- ", "-", "_" } }, + { "cc_end", new string[] { $" {t}", $"{t}", "" } }, + { "cc_inB", new string[] { "_", "-", "- " } }, + { "cc_endB", new string[] { "_", $"{t}", $" {t}" } }, + { "cc_mix", new string[] { $" {t}", " R", $"{t}", "", "_", $"{t} ", $"{t}" } }, + { "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 = ""; + // 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; + } + + // 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" + $"-{consonant}{vowel}", // "-CV" + $"-{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)) { + return variation; + } + } + } + + 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]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{consonant}{vowel}", // "CV" + $"{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)) { + return variation1; + } + } + } + + 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]; + vowel = parts[0]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{vowel}{consonant} -", // "VC -" + $"{vowel} {consonant}-", // "V C-" + $"{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)) { + return 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) { + aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; + counter++; + } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { + aliasFormat = $"{alias}{format}"; + } else { + aliasFormat = $"{format}{alias}"; + } + // Check if the formatted alias exists + if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + return aliasFormat; + } + } + return alias; + } + + protected override string ValidateAlias(string alias) { + + // VALIDATE ALIAS DEPENDING ON METHOD + if (isMissingVPhonemes) { + foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isMissingCPhonemes) { + foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isYamlFallbacks) { + foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + + 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; + } + } + + foreach (var v in vowels) { + if (alias.EndsWith("-")) { + return base.GetTransitionBasicLengthMs() * 0.5; + } + } + + // 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; + } + } + + 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; + } + } + + return base.GetTransitionBasicLengthMs() * transitionMultiplier; + } + } +} diff --git a/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs b/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs new file mode 100644 index 000000000..a2852dcfe --- /dev/null +++ b/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs @@ -0,0 +1,1780 @@ +using System; +using System.Collections.Generic; +using System.Text; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using System.Linq; +using System.IO; +using Serilog; +using System.Threading.Tasks; +using static OpenUtau.Api.Phonemizer; +using System.Collections; + +namespace OpenUtau.Plugin.Builtin { + /// + /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; + /// + /// - Supports dictionary; + /// - Automatically align phonemes to notes; + /// - Supports syllable extension; + /// - Automatically calculates transition phonemes length, with constants by default, + /// but there is a pre-created function to use Oto value; + /// - The transition length is scaled based on Tempo and note length. + /// + /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". + /// + /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], + /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". + /// + /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". + /// + /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, + /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). + /// + public abstract class SyllableBasedPhonemizer : Phonemizer { + + /// + /// Syllable is [V] [C..] [V] + /// + protected struct Syllable { + /// + /// vowel from previous syllable for VC + /// + public string prevV; + /// + /// CCs, may be empty + /// + public string[] cc; + /// + /// "base" note. May not actually be vowel, if only consonants way provided + /// + public string v; + /// + /// Start position for vowel. All VC CC goes before this position + /// + public int position; + /// + /// previous note duration, i.e. this is container for VC and CC notes + /// + public int duration; + /// + /// Tone for VC and CC + /// + public int tone; + /// + /// Other phoneme attributes for VC and CC + /// + public PhonemeAttributes[] attr; + /// + /// tone for base "vowel" phoneme + /// + public int vowelTone; + /// + /// Other phoneme attributes for base "vowel" phoneme + /// + public PhonemeAttributes[] vowelAttr; + + /// + /// 0 if no consonants are taken from previous word; + /// 1 means first one is taken from previous word, etc. + /// + public int prevWordConsonantsCount; + + /// + /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. + /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. + /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible + /// + public bool canAliasBeExtended; + + // helpers + public bool IsStartingV => prevV == "" && cc.Length == 0; + public bool IsVV => prevV != "" && cc.Length == 0; + + public bool IsStartingCV => prevV == "" && cc.Length > 0; + public bool IsVCV => prevV != "" && cc.Length > 0; + + public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; + public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; + + public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; + public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; + + public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); + public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; + } + } + + protected struct Ending { + /// + /// vowel from the last syllable to make VC + /// + public string prevV; + /// + /// actuall CC at the ending + /// + public string[] cc; + /// + /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) + /// + public string tail; + public bool HasTail => !string.IsNullOrEmpty(tail); + /// + /// last note position + duration, all phonemes must be less than this + /// + public int position; + /// + /// last syllable length, max container for all VC CC C- + /// + public int duration; + /// + /// the tone from last syllable, for all ending phonemes + /// + public int tone; + /// + /// Other phoneme attributes from last syllable + /// + public PhonemeAttributes[] attr; + + // helpers + public bool IsEndingV => cc.Length == 0; + public bool IsEndingVC => cc.Length > 0; + public bool IsEndingVCWithOneConsonant => cc.Length == 1; + public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; + } + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + error = ""; + var mainNote = notes[0]; + if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return MakeForcedAliasResult(mainNote); + } + if (hasDictionary && isDictionaryLoading) { + return MakeSimpleResult(""); + } + + runtimeGlides.Clear(); + + var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); + if (syllables == null) { + return HandleError(); + } + + var phonemes = new List(); + int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI + + foreach (var syllable in syllables) { + var modifiedSyllable = ApplyBoundaryReplacements(syllable); + + if (tails.Contains(modifiedSyllable.v)) { + var ending = new Ending { + prevV = modifiedSyllable.prevV, + cc = modifiedSyllable.cc, + tail = modifiedSyllable.v, + position = modifiedSyllable.position, + duration = modifiedSyllable.duration, + tone = modifiedSyllable.tone, + attr = modifiedSyllable.attr + }; + + var endingPhonemes = ProcessEnding(ending); + + if (endingPhonemes != null) { + phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += endingPhonemes.Count; + } + continue; + } + + var syllablePhonemes = ProcessSyllable(modifiedSyllable); + var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); + int currentSyllablePhonemeCount = syllablePhonemes.Count; + + var basePhoneme = madePhonemes.LastOrDefault(); + string baseAlias = basePhoneme.phoneme ?? ""; + if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || + vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { + + string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); + if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { + int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); + madePhonemes.Add(new Phoneme { + phoneme = sustainData.sustain, + position = basePhoneme.position + offsetTicks, + index = globalPhonemeIndex + currentSyllablePhonemeCount + }); + currentSyllablePhonemeCount++; + } + } + phonemes.AddRange(madePhonemes); + globalPhonemeIndex += currentSyllablePhonemeCount; + } + + if (!nextNeighbour.HasValue) { + var tryEnding = MakeEnding(notes); + if (tryEnding.HasValue) { + var ending = tryEnding.Value; + + if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { + ending.tail = nextNeighbour.Value.lyric; + } + + var modifiedEnding = ApplyBoundaryReplacements(ending); + var endingPhonemes = ProcessEnding(modifiedEnding); + + if (endingPhonemes != null) { + 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 = finalPhonemes + }; + } + + protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { + int noteIndex = 0; + for (int i = 0; i < phonemes.Count; i++) { + var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var phoneme = phonemes[i]; + while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { + noteIndex++; + } + + 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); + validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + phoneme.phoneme = validatedAlias; + } else { + phoneme.phoneme = null; + phoneme.position = 0; + } + + phonemes[i] = phoneme; + } + return phonemes.ToArray(); + } + + private Result HandleError() { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = error + } + } + }; + } + + protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = + new YamlDotNet.Serialization.DeserializerBuilder() + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + public override void SetSinger(USinger singer) { + if (this.singer != singer) { + this.singer = singer; + dictionaries.Clear(); + + if (this.singer == null || !this.singer.Loaded) { + return; + } + + if (string.IsNullOrEmpty(YamlFileName)) { + if (backupVowels != null) this.vowels = backupVowels; + else this.vowels = GetVowels(); + + if (backupConsonants != null) this.consonants = backupConsonants; + else this.consonants = GetConsonants(); + + if (backupDictionaryReplacements != null) { + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) { + dictionaryReplacements[kvp.Key] = kvp.Value; + } + } + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + return; + } + + // file paths + string globalFile = Path.Combine(PluginDir, YamlFileName); + string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) + ? Path.Combine(singer.Location, YamlFileName) + : null; + + // Local helper function to update and backup YAML files safely + void UpdateYamlIfNeeded(string filePath, bool isGlobal) { + if (string.IsNullOrEmpty(filePath)) return; + + bool shouldWriteTemplate = false; + bool shouldBackupOldFile = false; + string currentVersion = "unknown"; + + if (File.Exists(filePath)) { + if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { + try { + var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); + currentVersion = checkData?.version?.Trim() ?? ""; + + // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion + if (string.IsNullOrEmpty(currentVersion)) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } else if (Version.TryParse(currentVersion, out Version currV) && + Version.TryParse(YamlVersion, out Version targetV)) { + if (currV < targetV) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { + // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } catch (Exception ex) { + Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); + return; + } + } + } else if (isGlobal && YamlTemplate != null) { + shouldWriteTemplate = true; + } + + if (shouldBackupOldFile && File.Exists(filePath)) { + try { + // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml + string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; + string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); + + if (File.Exists(backupFile)) File.Delete(backupFile); + File.Move(filePath, backupFile); + Log.Information($"Old {YamlFileName} backed up to {backupFile}"); + } catch (Exception e) { + Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); + return; + } + } + + if (shouldWriteTemplate) { + try { + File.WriteAllBytes(filePath, YamlTemplate); + Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); + } catch (Exception e) { + Log.Error(e, $"Failed to write template to {filePath}"); + } + } + } + + UpdateYamlIfNeeded(globalFile, true); + UpdateYamlIfNeeded(singerFile, false); + + // add to parsing list (Global first, Singer second) + var filesToParse = new List(); + if (File.Exists(globalFile)) filesToParse.Add(globalFile); + if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); + + // backups of hardcoded defaults exist + if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); + if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); + if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); + if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); + if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); + + // reset live arrays/lists back to defaults before stacking + vowels = backupVowels; + consonants = backupConsonants; + tails = "-".Split(','); + + fricative = Array.Empty(); + aspirate = Array.Empty(); + semivowel = Array.Empty(); + liquid = Array.Empty(); + nasal = Array.Empty(); + stop = Array.Empty(); + tap = Array.Empty(); + affricate = Array.Empty(); + + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; + + diphthongTails.Clear(); + foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; + + diphthongSplits.Clear(); + foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; + + mergingReplacements.Clear(); + splittingReplacements.Clear(); + yamlFallbacks.Clear(); + PhonemeOverrides.Clear(); + if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); + vowelSustains.Clear(); + foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; + + // parse the files sequentially (Singer configs seamlessly overwrite global configs) + foreach (var file in filesToParse) { + try { + var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); + + var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); + vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); + + var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tails = yamlTails.Concat(tails).Distinct().ToArray(); + + if (data?.isglides != null) enableGlides = data.isglides.Value; + + var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); + fricative = yFricative.Concat(fricative).Distinct().ToArray(); + var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); + var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); + semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); + var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); + liquid = yLiquid.Concat(liquid).Distinct().ToArray(); + var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); + nasal = yNasal.Concat(nasal).Distinct().ToArray(); + var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); + stop = yStop.Concat(stop).Distinct().ToArray(); + var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tap = yTap.Concat(tap).Distinct().ToArray(); + var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + affricate = yAffricate.Concat(affricate).Distinct().ToArray(); + + var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) + .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); + consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); + + // DIPHTHONG AUTO-TAIL DETECTION + var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); + var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); + + foreach (var d in yamlDiphthongs) { + if (!diphthongSplits.ContainsKey(d)) { + foreach (var tail in dynamicTails) { + if (d.EndsWith(tail) && d != tail) { + diphthongTails[d] = tail; + break; + } + } + } + } + + // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) + if (data?.timings != null) { + foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; + } + + if (data?.replacements != null) { + var localMerge = new List(); + var localSplit = new List(); + string GetFromKey(object fromObj) { + if (fromObj is string s) return s; + if (fromObj is System.Collections.IEnumerable e) { + return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); + } + return ""; + } + + foreach (var rawReplacement in data.replacements) { + string fromKey = GetFromKey(rawReplacement.from); + mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + + if (rawReplacement.from is string fromStr) { + dictionaryReplacements.Remove(fromStr); + dictionaryReplacements.Remove(fromStr.ToLower()); + dictionaryReplacements.Remove(fromStr.ToUpper()); + } + + List fromList = rawReplacement.FromList; + List toList = rawReplacement.ToList; + object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); + object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); + + var cleanReplacement = new Replacement { + from = parsedFrom, + to = parsedTo, + where = rawReplacement.where + }; + + if (parsedFrom is string fromString) { + if (parsedTo is string toString) { + // Dictionary handles simple 1-to-1 replacements + dictionaryReplacements[fromString] = toString; + } else { + // 1-to-Many goes to Split + localSplit.Add(cleanReplacement); + } + } else { + // Many-to-Any goes to Merge + localMerge.Add(cleanReplacement); + } + } + mergingReplacements.InsertRange(0, localMerge); + splittingReplacements.InsertRange(0, localSplit); + } + + if (data?.fallbacks != null) { + foreach (var df in data.fallbacks) { + if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { + yamlFallbacks[df.from] = df.to; + } + } + } + + if (data?.diphthongs != null) { + foreach (var d in data.diphthongs) { + if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { + diphthongTails[d.from] = d.to; + } + } + } + + if (data?.vowelsustains != null) { + foreach (var v in data.vowelsustains) { + if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { + vowelSustains[v.symbol] = (v.sustain, v.offset); + } + } + } + + } catch (Exception ex) { + Log.Error($"Failed to parse {file}: {ex.Message}"); + } + } + + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + } + } + + protected USinger singer; + protected bool hasDictionary => dictionaries.ContainsKey(GetType()); + protected IG2p dictionary => dictionaries[GetType()]; + protected bool isDictionaryLoading => dictionaries[GetType()] == null; + protected double TransitionBasicLengthMs => 100; + + private Dictionary dictionaries = new Dictionary(); + private const string FORCED_ALIAS_SYMBOL = "?"; + private string error = ""; + 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 + /// + /// + protected abstract string[] GetVowels(); + + /// + /// Returns list of consonants. Only needed if there is a dictionary + /// + /// + protected virtual string[] GetConsonants() { + throw new NotImplementedException(); + } + + /// + /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc + /// + /// List of phonemes + protected abstract List ProcessSyllable(Syllable syllable); + + /// + /// phoneme symbols for ending, like, V-, or VC-, or VC+C + /// + protected abstract List ProcessEnding(Ending ending); + + /// + /// simple alias to alias fallback + /// + /// + protected virtual Dictionary GetAliasesFallback() { return null; } + + /// + /// Use to some custom init, if needed + /// + protected virtual void Init() { } + + /// + /// Dictionary name. Must be stored in Dictionaries folder. + /// If missing or can't be read, phonetic input is used + /// + /// + protected virtual string GetDictionaryName() { return null; } + + /// + /// extracts array of phoneme symbols from note. Override for procedural dictionary or something + /// reads from dictionary if provided + /// + /// + /// + protected virtual string[] GetSymbols(Note note) { + string[] getSymbolsRaw(string lyrics) { + if (lyrics == null) { + return new string[0]; + } else return lyrics.Split(" "); + } + + if (tails.Contains(note.lyric)) { + return new string[] { note.lyric }; + } + + if (hasDictionary) { + if (!string.IsNullOrEmpty(note.phoneticHint)) { + return getSymbolsRaw(note.phoneticHint); + } + + var result = new List(); + foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { + var subResult = dictionary.Query(subword); + if (subResult == null) { + subResult = HandleWordNotFound(note); + if (subResult == null) { + return null; + } + } else { + for (int i = 0; i < subResult.Length; i++) { + string phoneme = subResult[i]; + if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { + subResult[i] = replaced; + } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { + subResult[i] = replacedExact; + } + } + } + result.AddRange(subResult); + } + return result.ToArray(); + } else { + return getSymbolsRaw(note.lyric); + } + } + + /// + /// 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. + /// + /// + protected virtual Dictionary GetDictionaryPhonemesReplacement() { + return dictionaryReplacements ?? new Dictionary(); + } + private string[] backupVowels = null; + private string[] backupConsonants = null; + private Dictionary backupDiphthongTails = null; + private Dictionary backupDiphthongSplits = null; + private Dictionary backupDictionaryReplacements = null; + protected Dictionary vowelSustains = new Dictionary(); + private Dictionary backupVowelSustains = null; + + /// + /// separates symbols to syllables, without an ending. + /// + /// + /// + /// + protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + var firstVowelId = vowelIds[0]; + if (notes.Length < vowelIds.Length) { + error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; + return null; + } + + var syllables = new Syllable[vowelIds.Length]; + + // Making the first syllable + if (prevEnding.HasValue) { + var prevEndingValue = prevEnding.Value; + var beginningCc = prevEndingValue.cc.ToList(); + beginningCc.AddRange(symbols.Take(firstVowelId)); + + // If we had a prev neighbour ending, let's take info from it + syllables[0] = new Syllable() { + prevV = prevEndingValue.prevV, + cc = beginningCc.ToArray(), + v = symbols[firstVowelId], + tone = prevEndingValue.tone, + attr = prevEndingValue.attr, + duration = prevEndingValue.duration, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes, + prevWordConsonantsCount = prevEndingValue.cc.Count() + }; + } else { + // there is only empty space before us + syllables[0] = new Syllable() { + prevV = "", + cc = symbols.Take(firstVowelId).ToArray(), + v = symbols[firstVowelId], + tone = notes[0].tone, + attr = notes[0].phonemeAttributes, + duration = -1, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes + }; + } + + // normal syllables after the first one + var noteI = 1; + var ccs = new List(); + var position = 0; + var lastSymbolI = firstVowelId + 1; + for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { + if (!vowelIds.Contains(lastSymbolI)) { + ccs.Add(symbols[lastSymbolI]); + } else { + position += notes[noteI - 1].duration; + syllables[noteI] = new Syllable() { + prevV = syllables[noteI - 1].v, + cc = ccs.ToArray(), + v = symbols[lastSymbolI], + tone = notes[noteI - 1].tone, + attr = notes[noteI - 1].phonemeAttributes, + duration = notes[noteI - 1].duration, + position = position, + vowelTone = notes[noteI].tone, + vowelAttr = notes[noteI].phonemeAttributes, + canAliasBeExtended = true // for all not-first notes is allowed + }; + ccs = new List(); + noteI++; + } + } + + return syllables; + } + + /// + /// extracts word ending + /// + /// + /// + protected Ending? MakeEnding(Note[] inputNotes) { + if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return null; + } + + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + + return new Ending() { + prevV = symbols[vowelIds.Last()], + cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), + tone = notes.Last().tone, + attr = notes.Last().phonemeAttributes, + duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), + position = notes.Sum(n => n.duration) + }; + } + + /// + /// extracts and validates symbols and vowels + /// + /// + /// + private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { + var mainNote = notes[0]; + var symbols = GetSymbols(mainNote); + if (symbols == null) { + return (null, null, null); + } + if (symbols.Length == 0) { + symbols = new string[] { "" }; + } + + symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); + symbols = ApplyExtensions(symbols, notes); + List vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + vowelIds.Add(symbols.Length - 1); + } + if (notes.Length < vowelIds.Count) { + notes = HandleNotEnoughNotes(notes, vowelIds); + } + return (symbols, vowelIds.ToArray(), notes); + } + + /// + /// When there are more syllables than notes, recombines notes to match syllables count + /// + /// + /// + /// + protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { + var newNotes = new List(); + newNotes.AddRange(notes.SkipLast(1)); + var lastNote = notes.Last(); + var position = lastNote.position; + var notesToSplit = vowelIds.Count - newNotes.Count; + var duration = lastNote.duration / notesToSplit / 15 * 15; + for (var i = 0; i < notesToSplit; i++) { + var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); + newNotes.Add(new Note() { + position = position, + duration = durationFinal, + tone = lastNote.tone, + phonemeAttributes = lastNote.phonemeAttributes + }); + position += durationFinal; + } + + return newNotes.ToArray(); + } + + /// + /// Override this method, if you want to implement some machine converting from a word to phonemes + /// + /// + /// + protected virtual string[] HandleWordNotFound(Note note) { + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); + if(HasOto(mpdlyric, note.tone)){ + error = mpdlyric; + }else{ + error = "word not found"; + } + return null; + } + + /// + /// Does this note extend the previous syllable? + /// + /// + /// + protected bool IsSyllableVowelExtensionNote(Note note) { + return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); + } + + /// + /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic + /// + /// + /// + protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { + return phonemesString.Split(' '); + } + + /// + /// use to validate alias + /// + /// + /// + protected virtual string ValidateAlias(string alias) { + return alias; + } + + /// + /// Defines basic transition length before scaling it according to tempo and note length + /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation + /// + /// Mapped alias + /// + protected virtual double GetTransitionBasicLengthMs(string alias = "") { + return GetTransitionBasicLengthMsByConstant(); + } + + 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 + /// + /// + protected double GetTempoNoteLengthFactor() { + return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; + } + + protected virtual IG2p[] GetBaseG2ps() { + return Array.Empty(); + } + + protected virtual IG2p LoadBaseDictionary() { + var g2ps = new List(); + + // Native YAML Dictionary Logic + if (!string.IsNullOrEmpty(YamlFileName)) { + string path = Path.Combine(PluginDir, YamlFileName); + + // Write template if missing + if (!File.Exists(path) && YamlTemplate != null) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + + // Load dictionary from Singer Folder (Highest Priority) + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + + // Load dictionary from Plugin Folder (Fallback Priority) + if (File.Exists(path)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {path}"); + } + } + } + // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) + else { + var dictionaryName = GetDictionaryName(); + if (!string.IsNullOrEmpty(dictionaryName)) { + var filename = Path.Combine(DictionariesPath, dictionaryName); + if (File.Exists(filename)) { + var dictionaryText = File.ReadAllText(filename); + var builder = G2pDictionary.NewBuilder(); + foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); + foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); + builder.AddEntry("a", new string[] { "a" }); + ParseDictionary(dictionaryText, builder); + g2ps.Add(builder.Build()); + } + } + } + + // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) + var childG2ps = GetBaseG2ps(); + if (childG2ps != null && childG2ps.Any()) { + g2ps.AddRange(childG2ps); + } + + return new G2pFallbacks(g2ps.ToArray()); + } + + /// + /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, + /// and replaces phonemes with replacement table + /// Is Running Async! + /// + /// + /// + protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { + var replacements = GetDictionaryPhonemesReplacement(); + foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { + if (line.StartsWith(";;;")) { + continue; + } + var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); + if (parts.Length != 2) { + continue; + } + string key = parts[0].ToLowerInvariant(); + var values = GetDictionaryWordPhonemes(parts[1]).Select( + n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); + lock (builder) { + builder.AddEntry(key, values); + }; + }; + } + + #region helpers + + /// + /// May be used if you have different logic for short and long notes + /// + /// + /// + protected bool IsShort(Syllable syllable) { + return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; + } + 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 + /// + /// + /// + /// + protected bool HasOto(string alias, int tone) { + return singer.TryGetMappedOto(alias, tone, out _); + } + + /// + /// Can be used for different variants, like exhales [v R], [v -] etc + /// + /// phonemes container to add to + /// to map alias + /// target phoneme variants + /// returns true if added any + protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { + foreach (var phoneme in targetPhonemes) { + if (HasOto(phoneme, tone)) { + sourcePhonemes.Add(phoneme); + return true; + } + } + 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 + /// + /// + /// + protected bool CanMakeAliasExtension(Syllable syllable) { + return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; + } + + /// + /// if current syllable is VV and previous one is from the same pitch, + /// you may wan't to just extend the previous alias. Put the phoneme as null fot that + /// + /// + /// + /// + protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { + if (singer.Subbanks.Count == 1) { + return true; + } + if (tone1 == tone2) { + return true; + } + var toneSets = singer.Subbanks.Select(n => n.toneSet); + foreach (var toneSet in toneSets) { + if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { + return true; + } + if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { + return false; + } + } + return true; + } + + protected virtual string YamlFileName => null; + protected virtual byte[] YamlTemplate => null; + protected virtual string YamlVersion => null; + + protected string[] vowels = Array.Empty(); + protected string[] consonants = Array.Empty(); + protected string[] tails = "-,R".Split(','); + protected string[] affricate = Array.Empty(); + protected string[] fricative = Array.Empty(); + protected string[] aspirate = Array.Empty(); + protected string[] semivowel = Array.Empty(); + protected string[] liquid = Array.Empty(); + protected string[] nasal = Array.Empty(); + protected string[] stop = Array.Empty(); + protected string[] tap = Array.Empty(); + + protected Dictionary dictionaryReplacements = new Dictionary(); + protected Dictionary PhonemeOverrides = new Dictionary(); + protected Dictionary yamlFallbacks = new Dictionary(); + protected List consExceptions = new List(); + + protected Dictionary diphthongTails = new Dictionary(); + protected Dictionary diphthongSplits = new Dictionary(); + + 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(); + public Timings[] timings { get; set; } = Array.Empty(); + public DiphthongData[] diphthongs { get; set; } = Array.Empty(); + public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); + + public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } + public struct Fallbacks { public string from { get; set; } public string to { get; set; } } + public struct Timings { public string symbol { get; set; } public double value { get; set; } } + public struct DiphthongData { public string from { get; set; } public string to { get; set; } } + public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } + } + + public class Replacement { + public object from { get; set; } + public object to { get; set; } + public string where { get; set; } = "inside"; + + public List FromList { + get { + if (from is string s) return new List { s }; + if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + + public List ToList { + get { + if (to is string s) return new List { s }; + if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + } + + protected List mergingReplacements = new List(); + protected List splittingReplacements = new List(); + + protected virtual bool IsGroupKeyword(string rulePhoneme) { + // 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 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; + } + } + } + + bool inBaseGroup = false; + switch (baseGroup) { + case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; + case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; + case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; + case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; + case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; + case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; + case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; + case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; + case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; + case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; + } + + if (!inBaseGroup) return false; + + if (cleanRule.Contains("!")) { + string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; + if (excluded.Split(',').Contains(actualPhoneme)) return false; + } + + if (cleanRule.Contains("=")) { + string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; + if (!restricted.Split(',').Contains(actualPhoneme)) return false; + } + + return true; + } + + protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; + + List finalPhonemes = new List(); + int idx = 0; + + var validRules = mergingReplacements.Concat(splittingReplacements) + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + var validSplits = splittingReplacements + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + while (idx < inputPhonemes.Count) { + bool replaced = false; + + foreach (var rule in validRules) { + 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.Count; j++) { + string rulePh = fromArray[j]; + string actualPh = inputPhonemes[idx + j]; + + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh)) { + if (IsGroupMatch(rulePh, actualPh)) { + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); + } else { + match = false; break; + } + } else if (rulePh != actualPh) { + match = false; break; + } + } + + if (match) { + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + 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++) { + // 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.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) { + List fromArray = rule.FromList; + if (fromArray == null || fromArray.Count != 1) continue; + + string rulePh = fromArray[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { + + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + foreach(string toPh in toArray) { + 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; + } + } + } + if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } else if (!replaced) { + finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } + } + return finalPhonemes; + } + + private Syllable ApplyBoundaryReplacements(Syllable syllable) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); + bool hasV = !string.IsNullOrEmpty(syllable.v); + + 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) || !hasPrevV; + List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); + + string newPrevV = ""; + string newV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + if (hasV && finalPhonemes.Count > 0) { + var vowelsList = GetVowels(); + int vIndex = finalPhonemes.Count - 1; + + for (int i = finalPhonemes.Count - 1; i >= 0; i--) { + if (vowelsList.Contains(finalPhonemes[i])) { + vIndex = i; + break; + } + } + newV = finalPhonemes[vIndex]; + for (int i = 0; i < vIndex; i++) { + newCc.Add(finalPhonemes[i]); + } + } else { + newCc.AddRange(finalPhonemes); + } + } + + syllable.prevV = newPrevV; + syllable.cc = newCc.ToArray(); + syllable.v = newV; + return syllable; + } + + private Ending ApplyBoundaryReplacements(Ending ending) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); + currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); + if (ending.cc != null) currentPhonemes.AddRange(ending.cc); + + List finalPhonemes = ApplyReplacements(currentPhonemes, true); + + string newPrevV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + newCc.AddRange(finalPhonemes); + } + + ending.prevV = newPrevV; + ending.cc = newCc.ToArray(); + return ending; + } + + #endregion + + #region private + + private Result MakeForcedAliasResult(Note note) { + return MakeSimpleResult(note.lyric.Substring(1)); + } + + protected void ReadDictionaryAndInit() { + var dictionaryName = GetDictionaryName(); + if (dictionaryName == null) { + return; + } + dictionaries[GetType()] = null; + if (Testing) { + ReadDictionary(dictionaryName); + Init(); + return; + } + OnAsyncInitStarted(); + Task.Run(() => { + ReadDictionary(dictionaryName); + Init(); + OnAsyncInitFinished(); + }); + } + + private void ReadDictionary(string dictionaryName) { + try { + var phonemeSymbols = new Dictionary(); + + foreach (var vowel in GetVowels()) { + phonemeSymbols[vowel] = true; + } + foreach (var consonant in GetConsonants()) { + phonemeSymbols[consonant] = false; + } + + var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); + var safeDict = new Dictionary(); + + foreach (var kvp in childDict) { + safeDict[kvp.Key] = kvp.Value; + safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' + safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' + } + + dictionaries[GetType()] = new G2pRemapper( + LoadBaseDictionary(), + phonemeSymbols, + safeDict); + + } catch (Exception ex) { + Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); + } + } + + private string[] ApplyExtensions(string[] symbols, Note[] notes) { + var newSymbols = new List(); + var vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + // no syllables or all consonants, the last phoneme will be interpreted as vowel + vowelIds.Add(symbols.Length - 1); + } + var lastVowelI = 0; + newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); + for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { + if (!IsSyllableVowelExtensionNote(notes[i])) { + var prevVowel = vowelIds[lastVowelI]; + lastVowelI++; + var vowel = vowelIds[lastVowelI]; + newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); + } else { + newSymbols.Add(symbols[vowelIds[lastVowelI]]); + } + } + newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); + return newSymbols.ToArray(); + } + + private List ExtractVowels(string[] symbols) { + var vowelIds = new List(); + var vowels = GetVowels(); + for (var i = 0; i < symbols.Length; i++) { + if (vowels.Contains(symbols[i])) { + vowelIds.Add(i); + } + } + 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]; + + 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) { + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + baseLengthMs = GetTransitionBasicLengthMs(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; + } + } + } + + 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] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex + }; + + if (i == 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) { + // YAML Override active: Use the multiplier and bypass NoGap entirely + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); + } else { + // Default behavior + baseLengthMs = GetTransitionBasicLengthMsByOto(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 + phonemes[phonemeI].position = MsToTick(baseLengthMs); + } + } + } else { + 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]; + } + } else { + // 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 - 1 : phonemeSymbols.Count - 1, containerLength); + } + + private string ValidateAliasIfNeeded(string alias, int tone) { + if (HasOto(alias, tone)) { + return alias; + } + return ValidateAlias(alias); + } + + private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { + var offset = 0; + var lengthModifier = 1.0; + + if (containerLengthTick > 0) { + var allTransitionsLengthTick = phonemes.Sum(n => n.position); + + // 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--) { + if (phonemes[i].phoneme == null) continue; + var finalLengthTick = (int)(phonemes[i].position * lengthModifier); + phonemes[i].position = startPosition - finalLengthTick - offset; + offset += finalLengthTick; + } + + return phonemes.Where(n => n.phoneme != null).ToArray(); + } + + #endregion + } +} diff --git a/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs b/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs new file mode 100644 index 000000000..a77bf3342 --- /dev/null +++ b/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace JA_SBP.Data { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JA_SBP.Data.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] template { + get { + object obj = ResourceManager.GetObject("template", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/JapaneseCVVXPhonemizer/Data/Resources.resx b/JapaneseCVVXPhonemizer/Data/Resources.resx new file mode 100644 index 000000000..4ec68391a --- /dev/null +++ b/JapaneseCVVXPhonemizer/Data/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/Data/template.yaml b/JapaneseCVVXPhonemizer/Data/template.yaml new file mode 100644 index 000000000..a9f0cdaa4 --- /dev/null +++ b/JapaneseCVVXPhonemizer/Data/template.yaml @@ -0,0 +1,703 @@ +%YAML 1.2 +--- +version: 1.2 +symbols: +# Romaji symbols + - {symbol: a, type: vowel} + - {symbol: e, type: vowel} + - {symbol: i, type: vowel} + - {symbol: o, type: vowel} + - {symbol: u, type: vowel} + - {symbol: N, type: vowel} + - {symbol: ts, type: fricative} + - {symbol: j, type: fricative} + - {symbol: dj, type: stop} +# Arpabet + extras + - {symbol: aa, type: vowel} + - {symbol: ae, type: vowel} + - {symbol: ah, type: vowel} + - {symbol: ao, type: vowel} + - {symbol: aw, type: vowel} + - {symbol: ax, type: vowel} + - {symbol: ay, type: vowel} + - {symbol: b, type: stop} + - {symbol: ch, type: affricate} + - {symbol: d, type: stop} + - {symbol: dh, type: fricative} + - {symbol: dr, type: fricative} + - {symbol: dx, type: tap} + - {symbol: eh, type: vowel} + - {symbol: er, type: vowel} + - {symbol: ey, type: vowel} + - {symbol: f, type: fricative} + - {symbol: ff, type: fricative} + - {symbol: g, type: stop} + - {symbol: hh, type: aspirate} + - {symbol: ih, type: vowel} + - {symbol: iy, type: vowel} + - {symbol: jh, type: affricate} + - {symbol: k, type: stop} + - {symbol: l, type: liquid} + - {symbol: m, type: nasal} + - {symbol: n, type: nasal} + - {symbol: ng, type: nasal} + - {symbol: ow, type: vowel} + - {symbol: oy, type: vowel} + - {symbol: p, type: stop} + - {symbol: q, type: stop} + - {symbol: Fr, type: stop} + - {symbol: r, type: liquid} + - {symbol: wr, type: liquid} + - {symbol: rr, type: stop} + - {symbol: s, type: fricative} + - {symbol: sh, type: fricative} + - {symbol: t, type: stop} + - {symbol: th, type: fricative} + - {symbol: tr, type: fricative} + - {symbol: uh, type: vowel} + - {symbol: uw, type: vowel} + - {symbol: v, type: fricative} + - {symbol: w, type: semivowel} + - {symbol: y, type: semivowel} + - {symbol: z, type: fricative} + - {symbol: zh, type: fricative} + - {symbol: Ex, type: tail} + - {symbol: In, type: tail} + - {symbol: Cm, type: tail} + - {symbol: Fr, type: tail} + +# for Arpabet to Romaji conversion +replacements: + - {from: aa, to: [a]} + - {from: ae, to: [e]} + - {from: [ae, n], to: [e, A, n]} + - {from: [ae, m], to: [e, A, m]} + - {from: [ae, ng], to: [e, A, n]} + - {from: ao, to: [o]} + - {from: aw, to: [a, w]} + - {from: ah, to: [a]} + - {from: ax, to: [a]} + - {from: [ax, n], to: [e, n]} + - {from: [ax, m], to: [e, m]} + - {from: [ax, ng], to: [e, n]} + - {from: [ax, l], to: [o, r]} + - {from: ay, to: [a, y]} + - {from: dh, to: [d]} + - {from: dr, to: [j, r]} + - {from: dx, to: [r]} + - {from: eh, to: [e]} + - {from: er, to: [o]} + - {from: ey, to: [e, y]} + - {from: hh, to: [h]} + - {from: ih, to: [e]} + - {from: iy, to: [i]} + - {from: jh, to: [dj]} + - {from: l, to: [l]} + - {from: ng, to: [ng]} + - {from: ow, to: [o, w]} + - {from: oy, to: [o, y]} + - {from: q, to: ['-']} + - {from: r, to: [r]} + - {from: th, to: [s]} + - {from: [th, r], to: [ts, r]} + - {from: tr, to: [ch, r]} + - {from: ts, to: [ts]} + - {from: uh, to: [u]} + - {from: uw, to: [u]} + - {from: v, to: [v]} + - {from: zh, to: [sh]} + # Vowel sustains + - {from: aL, to: [a, a]} + - {from: eL, to: [e, e]} + - {from: iL, to: [i, i]} + - {from: oL, to: [o, o]} + - {from: uL, to: [u, u]} + # Romaji geminations + - {from: [b, b], to: B} + - {from: [c, ch], to: CH} + - {from: [d, dy], to: DY} + - {from: [d, d], to: D} + - {from: [d, dj], to: DJ} + - {from: [g, g], to: G} + - {from: [g, gy], to: GY} + - {from: [h, h], to: H} + - {from: [h, hy], to: HY} + - {from: [j, j], to: J} + - {from: [k, k], to: K} + - {from: [k, ky], to: KY} + - {from: [l, l], to: L} + - {from: [m ,m], to: M} + - {from: [m, my], to: MY} + - {from: [n, n], to: NX} + - {from: [n, ny], to: NY} + - {from: [n, ng], to: NGX} + - {from: [p, p], to: P} + - {from: [p, py], to: PY} + - {from: [r, ry], to: RY} + - {from: [s, s], to: S} + - {from: [s, sh], to: SH} + - {from: [t, t], to: T} + - {from: [t, ty], to: TY} + - {from: [t, ts], to: TS} + - {from: [v, v], to: V} + - {from: [w, w], to: W} + - {from: [y, y], to: Y} + - {from: [z, z], to: Z} + +fallbacks: + # Only 1:1 mappings also no brackets cuz it will error out + # example entries: + - {from: A, to: a} +# Geminations + - {from: B, to: b} + - {from: CH, to: ch} + - {from: DH, to: d} + - {from: DY, to: dy} + - {from: DJ, to: dj} + - {from: F, to: f} + - {from: G, to: g} + - {from: GY, to: gy} + - {from: H, to: h} + - {from: HY, to: hy} + - {from: J, to: j} + - {from: K, to: k} + - {from: KY, to: ky} + - {from: L, to: l} + - {from: M, to: m} + - {from: MY, to: my} + - {from: NX, to: n} + - {from: NY, to: ny} + - {from: NGX, to: ng} + - {from: P, to: p} + - {from: PY, to: py} + - {from: RX, to: r} + - {from: RY, to: ry} + - {from: S, to: s} + - {from: SH, to: sh} + - {from: T, to: t} + - {from: TY, to: ty} + - {from: TS, to: ts} + - {from: V, to: v} + - {from: W, to: w} + - {from: Y, to: y} + - {from: Z, to: Z} + +timings: +# phoneme duration multipliers (decimal values in seconds) + - {symbol: ff, value: 1.3} + - {symbol: dj, value: 1.5} + - {symbol: o l, value: 1.2} + - {symbol: i l, value: 1.4} + - {symbol: u l, value: 1.2} + - {symbol: '- k', value: 0.8} + - {symbol: '- t', value: 0.8} + - {symbol: '- g', value: 0.8} + - {symbol: '- p', value: 0.8} +# Gemination length + - {symbol: B, value: 2.2} + - {symbol: CH, value: 2.2} + - {symbol: DY, value: 2.2} + - {symbol: D, value: 2.2} + - {symbol: DJ, value: 2.2} + - {symbol: F, value: 2.8} + - {symbol: G, value: 2.2} + - {symbol: GY, value: 2.2} + - {symbol: H, value: 2.8} + - {symbol: HY, value: 2.8} + - {symbol: J, value: 2.2} + - {symbol: K, value: 2.2} + - {symbol: KY, value: 2.2} + - {symbol: L, value: 2.2} + - {symbol: M, value: 2.2} + - {symbol: MY, value: 2.2} + - {symbol: NX, value: 2.2} + - {symbol: NY, value: 2.2} + - {symbol: NGX, value: 2.2} + - {symbol: P, value: 2.2} + - {symbol: PY, value: 2.2} + - {symbol: RX, value: 2.2} + - {symbol: RY, value: 2.2} + - {symbol: S, value: 2.8} + - {symbol: SH, value: 2.8} + - {symbol: T, value: 2.2} + - {symbol: TY, value: 2.2} + - {symbol: TS, value: 2.8} + - {symbol: V, value: 2.2} + - {symbol: W, value: 2.2} + - {symbol: Y, value: 2.2} + - {symbol: Z, value: 2.8} + +consonant_groups: +# same mechanics in presamp, the CONSONANTS + - {group: ch, members: [chi, che, cha, chu, cho, ち, ちぇ, ちゃ, ちゅ, ちょ]} + - {group: gy, members: [gi, gye, gya, gyu, gyo, ぎ, ぎぇ, ぎゃ, ぎゅ, ぎょ]} + - {group: ts, members: [tsu, tsa, tsi, tse, tso, つ, つぁ, つぃ, つぇ, つぉ]} + - {group: ty, members: [ti, tye, tya, tyu, tyo, てぃ, てぇ, てゃ, てゅ, てょ]} + - {group: py, members: [pi, pye, pya, pyu, pyo, ぴ, ぴぇ, ぴゃ, ぴゅ, ぴょ]} + - {group: ry, members: [ri, rye, rya, ryu, ryo, り, りぇ, りゃ, りゅ, りょ]} + - {group: ny, members: [ni, nie, nya, nyu, nyo, に, にぇ, にゃ, にゅ, にょ]} + - {group: r, members: [ra, ru, re, ro, ら, る, れ, ろ]} + - {group: hy, members: [hi, hye, hya, hyu, hyo, ひ, ひぇ, ひゃ, ひゅ, ひょ]} + - {group: dy, members: [di, dye, dya, dyu, dyo, でぃ, でぇ, でゃ, でゅ, でょ]} + - {group: by, members: [bi, bye, bya, byu, byo, び, びぇ, びゃ, びゅ, びょ]} + - {group: b, members: [ba, bu, be, bo, ば, ぶ, べ, ぼ]} + - {group: d, members: [da, de, do, du, だ, で, ど, どぅ]} + - {group: g, members: [ga, gu, ge, go, が, ぐ, げ, ご]} + - {group: f, members: [fu, fa, fi, fe, fo, ふ, ふぁ, ふぃ, ふぇ, ふぉ]} + - {group: h, members: [ha, he, ho, は, へ, ほ]} + - {group: k, members: [ka, ku, ke, ko, か, く, け, こ]} + - {group: j, members: [ji, je, ja, ju, jo, じ, じぇ, じゃ, じゅ, じょ]} + - {group: dj, members: [dji, dje, dja, dju, djo, ぢ, ぢぇ, ぢゃ, ぢゅ, ぢょ]} + - {group: m, members: [ma, mu, me, mo, ま, む, め, も]} + - {group: n, members: [na, nu, ne, no, な, ぬ, ね, の]} + - {group: p, members: [pa, pu, pe, po, ぱ, ぷ, ぺ, ぽ]} + - {group: s, members: [sa, su, si, se, so, さ, す, すぃ, せ, そ]} + - {group: sh, members: [shi, she, sha, shu, sho, し, しぇ, しゃ, しゅ, しょ]} + - {group: t, members: [ta, te, to, tu, た, て, ど, とぅ]} + - {group: w, members: [wi, wu, we, wo, wa, wo, うぃ, うぅ, うぇ, うぉ, わ, を]} + - {group: v, members: [vu, va, vi, vu, ve, vo, ヴ, ヴぁ, ヴぃ, ヴぅ, ヴぇ, ヴぉ]} + - {group: y, members: [yi, ye, ya, yu, yo, いぃ, いぇ, や, ゆ, よ]} + - {group: ky, members: [ki, kie, kya, kyu, kyo, き, きぇ, きゃ, きゅ, きょ]} + - {group: z, members: [za, zu, zi, ze, zo, ざ, ず, ずぃ, ぜ, ぞ]} + - {group: my, members: [mi, mie, mya, myu, myo, み, みぇ, みゃ, みゅ, みょ]} + # gemination groups + - {group: CH, members: [cchi, cche, ccha, cchu, ccho, っち, っちぇ, っちゃ, っちゅ, っちょ]} + - {group: GY, members: [ggi, ggye, ggya, ggyu, ggyo, っぎ, っぎぇ, っぎゃ, っぎゅ, っぎょ]} + - {group: TS, members: [ttsu, ttsa, ttsi, ttse, ttso, っつ, っつぁ, っつぃ, っつぇ, っつぉ]} + - {group: TY, members: [tti, ttye, ttya, ttyu, ttyo, ってぃ, ってぇ, ってゃ, ってゅ, ってょ]} + - {group: PY, members: [ppi, ppye, ppya, ppyu, ppyo, っぴ, っぴぇ, っぴゃ, っぴゅ, っぴょ]} + - {group: RY, members: [rri, rrye, rrya, rryu, rryo, っり, っりぇ, っりゃ, っりゅ, っりょ]} + - {group: NY, members: [nni, nnie, nnya, nnyu, nnyo, っに, っにぇ, っにゃ, っにゅ, っにょ]} + - {group: RX, members: [っら, っる, っれ, っろ]} + - {group: HY, members: [hhi, hhye, hhya, hhyu, hhyo, っひ, っひぇ, っひゃ, っひゅ, っひょ]} + - {group: DY, members: [ddi, ddye, ddya, ddyu, ddyo, っでぃ, っでぇ, っでゃ, っでゅ, っでょ]} + - {group: BY, members: [bbi, bbye, bbya, bbyu, bbyo, っび, っびぇ, っびゃ, っびゅ, っびょ]} + - {group: B, members: [bba, bbu, bbe, bbo, っば, っぶ, っべ, っぼ]} + - {group: D, members: [dda, dde, ddo, ddu, っだ, っで, っど, っどぅ]} + - {group: G, members: [gga, ggu, gge, ggo, っが, っぐ, っげ, っご]} + - {group: F, members: [っふ, っふぁ, っふぃ, っふぇ, っふぉ]} + - {group: H, members: [hha, hhe, hho, っは, っへ, っほ]} + - {group: K, members: [kka, kku, kke, kko, っか, っく, っけ, っこ]} + - {group: J, members: [jji, jje, jja, jju, jjo, っじ, っじぇ, じゃ, っじゅ, っじょ]} + - {group: DJ, members: [ddji, ddje, ddja, ddju, ddjo, っぢ, っぢぇ, っぢゃ, っぢゅ, っぢょ]} + - {group: M, members: [mma, mmu, mme, mmo, っま, っむ, っめ, っも]} + - {group: N, members: [nna, nnu, nne, nno, っな, っぬ, っね, っの]} + - {group: P, members: [ppa, ppu, ppe, ppo, っぱ, っぷ, っぺ, っぽ]} + - {group: S, members: [ssa, ssu, ssi, sse, sso, っさ, っす, っすぃ, っせ, っそ]} + - {group: SH, members: [sshi, sshe, ssha, sshu, ssho, っし, っしぇ, っしゃ, っしゅ, っしょ]} + - {group: T, members: [tta, tte, tto, ttu, った, って, っと, っとぅ]} + - {group: W, members: [wwi, wwu, wwe, wwo, wwa, っうぃ, っうぅ, っうぇ, っうぉ, っわ]} + - {group: V, members: [vvu, vva, vvi, vve, vvo, っヴ, っヴぁ, っヴぃ, っヴぇ, っヴぉ]} + - {group: Y, members: [yyi, yye, yya, yyu, yyo, っいぃ, っいぇ, っや, っゆ, っよ]} + - {group: KY, members: [kki, kkie, kkya, kkyu, kkyo, っき, っきぇ, きゃ, っきゅ, っきょ]} + - {group: Z, members: [zza, zzu, zzi, zze, zzo, っざ, っず, っずぃ, っぜ, っぞ]} + - {group: MY, members: [mmi, mmie, mmya, mmyu, mmyo, っみ, っみぇ, っみゃ, っみゅ, っみょ]} + +wanakana: +# CV / CCV Convertions + - {roma: [A], kana: あ} + - {roma: [a], kana: あ} + - {roma: [i], kana: い} + - {roma: [u], kana: う} + - {roma: [e], kana: え} + - {roma: [o], kana: お} + - {roma: [k, a], kana: か} + - {roma: [k, i], kana: き} + - {roma: [k, u], kana: く} + - {roma: [k, e], kana: け} + - {roma: [k, o], kana: こ} + - {roma: [g, a], kana: が} + - {roma: [g, i], kana: ぎ} + - {roma: [g, u], kana: ぐ} + - {roma: [g, e], kana: げ} + - {roma: [g, o], kana: ご} + - {roma: [s, a], kana: さ} + - {roma: [sh, i], kana: し} + - {roma: [s, u], kana: す} + - {roma: [s, e], kana: せ} + - {roma: [s, o], kana: そ} + - {roma: [z, a], kana: ざ} + - {roma: [j, i], kana: じ} + - {roma: [z, u], kana: ず} + - {roma: [z, e], kana: ぜ} + - {roma: [z, o], kana: ぞ} + - {roma: [t, a], kana: た} + - {roma: [ch, i], kana: ち} + - {roma: [ts, u], kana: つ} + - {roma: [t, e], kana: て} + - {roma: [t, o], kana: と} + - {roma: [d, a], kana: だ} + - {roma: [dj, i], kana: ぢ} + - {roma: [z, u], kana: づ} + - {roma: [d, e], kana: で} + - {roma: [d, o], kana: ど} + - {roma: [n, a], kana: な} + - {roma: [n, i], kana: に} + - {roma: [n, u], kana: ぬ} + - {roma: [n, e], kana: ね} + - {roma: [n, o], kana: の} + - {roma: [h, a], kana: は} + - {roma: [h, i], kana: ひ} + - {roma: [f, u], kana: ふ} + - {roma: [h, e], kana: へ} + - {roma: [h, o], kana: ほ} + - {roma: [b, a], kana: ば} + - {roma: [b, i], kana: び} + - {roma: [b, u], kana: ぶ} + - {roma: [b, e], kana: べ} + - {roma: [b, o], kana: ぼ} + - {roma: [p, a], kana: ぱ} + - {roma: [p, i], kana: ぴ} + - {roma: [p, u], kana: ぷ} + - {roma: [p, e], kana: ぺ} + - {roma: [p, o], kana: ぽ} + - {roma: [m, a], kana: ま} + - {roma: [m, i], kana: み} + - {roma: [m, u], kana: む} + - {roma: [m, e], kana: め} + - {roma: [m, o], kana: も} + - {roma: [y, a], kana: や} + - {roma: [y, u], kana: ゆ} + - {roma: [y, o], kana: よ} + - {roma: [r, a], kana: ら} + - {roma: [r, i], kana: り} + - {roma: [r, u], kana: る} + - {roma: [r, e], kana: れ} + - {roma: [r, o], kana: ろ} + - {roma: [w, a], kana: わ} + - {roma: [ky, a], kana: きゃ} + - {roma: [ky, u], kana: きゅ} + - {roma: [ky, o], kana: きょ} + - {roma: [gy, a], kana: ぎゃ} + - {roma: [gy, u], kana: ぎゅ} + - {roma: [gy, o], kana: ぎょ} + - {roma: [sh, a], kana: しゃ} + - {roma: [sh, u], kana: しゅ} + - {roma: [sh, o], kana: しょ} + - {roma: [j, a], kana: じゃ} + - {roma: [j, u], kana: じゅ} + - {roma: [j, o], kana: じょ} + - {roma: [ch, a], kana: ちゃ} + - {roma: [ch, u], kana: ちゅ} + - {roma: [ch, o], kana: ちょ} + - {roma: [dj, a], kana: ぢゃ} + - {roma: [dj, u], kana: ぢゅ} + - {roma: [dj, o], kana: ぢょ} + - {roma: [ny, a], kana: にゃ} + - {roma: [ny, u], kana: にゅ} + - {roma: [ny, o], kana: にょ} + - {roma: [hy, a], kana: ひゃ} + - {roma: [hy, u], kana: ひゅ} + - {roma: [hy, o], kana: ひょ} + - {roma: [by, a], kana: びゃ} + - {roma: [by, u], kana: びゅ} + - {roma: [by, o], kana: びょ} + - {roma: [py, a], kana: ぴゃ} + - {roma: [py, u], kana: ぴゅ} + - {roma: [py, o], kana: ぴょ} + - {roma: [my, a], kana: みゃ} + - {roma: [my, u], kana: みゅ} + - {roma: [my, o], kana: みょ} + - {roma: [ry, a], kana: りゃ} + - {roma: [ry, u], kana: りゅ} + - {roma: [ry, o], kana: りょ} + - {roma: [y, e], kana: いぇ} + - {roma: [ky, e], kana: きぇ} + - {roma: [gy, e], kana: ぎぇ} + - {roma: [sh, e], kana: しぇ} + - {roma: [j, e], kana: じぇ} + - {roma: [ch, e], kana: ちぇ} + - {roma: [dj, e], kana: ぢぇ} + - {roma: [ny, e], kana: にぇ} + - {roma: [hy, e], kana: ひぇ} + - {roma: [by, e], kana: びぇ} + - {roma: [py, e], kana: ぴぇ} + - {roma: [my, e], kana: みぇ} + - {roma: [ry, e], kana: りぇ} + - {roma: [w, i], kana: うぃ} + - {roma: [w, e], kana: うぇ} + - {roma: [w, o], kana: うぉ} + - {roma: [w, o], kana: を} + - {roma: [kw, a], kana: くぁ} + - {roma: [kw, i], kana: くぃ} + - {roma: [kw, e], kana: くぇ} + - {roma: [kw, o], kana: くぉ} + - {roma: [gw, a], kana: ぐぁ} + - {roma: [gw, i], kana: ぐぃ} + - {roma: [gw, e], kana: ぐぇ} + - {roma: [gw, o], kana: ぐぉ} + - {roma: [ts, a], kana: つぁ} + - {roma: [ts, i], kana: つぃ} + - {roma: [ts, e], kana: つぇ} + - {roma: [ts, o], kana: つぉ} + - {roma: [f, a], kana: ふぁ} + - {roma: [f, i], kana: ふぃ} + - {roma: [f, e], kana: ふぇ} + - {roma: [f, o], kana: ふぉ} + - {roma: [v, u], kana: ヴ} + - {roma: [v, a], kana: ヴぁ} + - {roma: [v, i], kana: ヴぃ} + - {roma: [v, e], kana: ヴぇ} + - {roma: [v, o], kana: ヴぉ} + - {roma: [v, u], kana: ゔ} + - {roma: [v, a], kana: ゔぁ} + - {roma: [v, i], kana: ゔぃ} + - {roma: [v, e], kana: ゔぇ} + - {roma: [v, o], kana: ゔぉ} + - {roma: [v, u], kana: ふ} + - {roma: [v, a], kana: ふぁ} + - {roma: [v, i], kana: ふぃ} + - {roma: [v, e], kana: ふぇ} + - {roma: [v, o], kana: ふぉ} + - {roma: [s, i], kana: すぃ} + - {roma: [z, i], kana: ずぃ} + - {roma: [t, i], kana: てぃ} + - {roma: [ty, u], kana: てゅ} + - {roma: [t, u], kana: とぅ} + - {roma: [d, i], kana: でぃ} + - {roma: [dy, u], kana: でゅ} + - {roma: [d, u], kana: どぅ} + - {roma: [ng, a], kana: か゜} + - {roma: [ng, i], kana: き゜} + - {roma: [ng, u], kana: く゜} + - {roma: [ng, e], kana: け゜} + - {roma: [ng, o], kana: こ゜} + - {roma: [ng, y, e], kana: き゜ぇ} + - {roma: [ng, y, a], kana: き゜ゃ} + - {roma: [ng, y, u], kana: き゜ゅ} + - {roma: [ng, y, o], kana: き゜ょ} + - {roma: [wr, a], kana: ラ} + - {roma: [wr, i], kana: リ} + - {roma: [wr, u], kana: ル} + - {roma: [wr, e], kana: レ} + - {roma: [wr, o], kana: ロ} + - {roma: [q, a], kana: '- あ'} + - {roma: [q, i], kana: '- い'} + - {roma: [q, u], kana: '- う'} + - {roma: [q, e], kana: '- え'} + - {roma: [q, o], kana: '- お'} + - {roma: [NG], kana: ン} + - {roma: [M], kana: ム} + - {roma: [N], kana: ん} + +# Geminations + - {roma: [K, a], kana: っか} + - {roma: [K, i], kana: っき} + - {roma: [K, u], kana: っく} + - {roma: [K, e], kana: っけ} + - {roma: [K, o], kana: っこ} + - {roma: [G, a], kana: っが} + - {roma: [G, i], kana: っぎ} + - {roma: [G, u], kana: っぐ} + - {roma: [G, e], kana: っげ} + - {roma: [G, o], kana: っご} + - {roma: [S, a], kana: っさ} + - {roma: [SH, i], kana: っし} + - {roma: [S, u], kana: っす} + - {roma: [S, e], kana: っせ} + - {roma: [S, o], kana: っそ} + - {roma: [Z, a], kana: っざ} + - {roma: [J, i], kana: っじ} + - {roma: [Z, u], kana: っず} + - {roma: [Z, e], kana: っぜ} + - {roma: [Z, o], kana: っぞ} + - {roma: [T, a], kana: った} + - {roma: [CH, i], kana: っち} + - {roma: [TS, u], kana: っつ} + - {roma: [T, e], kana: って} + - {roma: [T, o], kana: っと} + - {roma: [D, a], kana: っだ} + - {roma: [DJ, i], kana: っぢ} + - {roma: [Z, u], kana: っづ} + - {roma: [D, e], kana: っで} + - {roma: [D, o], kana: っど} + - {roma: [NX, a], kana: っな} + - {roma: [NX, i], kana: っに} + - {roma: [NX, u], kana: っぬ} + - {roma: [NX, e], kana: っね} + - {roma: [NX, o], kana: っの} + - {roma: [H, a], kana: っは} + - {roma: [H, i], kana: っひ} + - {roma: [F, u], kana: っふ} + - {roma: [H, e], kana: っへ} + - {roma: [H, o], kana: っほ} + - {roma: [B, a], kana: っば} + - {roma: [B, i], kana: っび} + - {roma: [B, u], kana: っぶ} + - {roma: [B, e], kana: っべ} + - {roma: [B, o], kana: っぼ} + - {roma: [P, a], kana: っぱ} + - {roma: [P, i], kana: っぴ} + - {roma: [P, u], kana: っぷ} + - {roma: [P, e], kana: っぺ} + - {roma: [P, o], kana: っぽ} + - {roma: [M, a], kana: っま} + - {roma: [M, i], kana: っみ} + - {roma: [M, u], kana: っむ} + - {roma: [M, e], kana: っめ} + - {roma: [M, o], kana: っも} + - {roma: [Y, a], kana: っや} + - {roma: [Y, u], kana: っゆ} + - {roma: [Y, o], kana: っよ} + - {roma: [RX, a], kana: っら} + - {roma: [RX, i], kana: っり} + - {roma: [RX, u], kana: っる} + - {roma: [RX, e], kana: っれ} + - {roma: [RX, o], kana: っろ} + - {roma: [W, a], kana: っわ} + - {roma: [KY, a], kana: っきゃ} + - {roma: [KY, u], kana: っきゅ} + - {roma: [KY, o], kana: っきょ} + - {roma: [GY, a], kana: っぎゃ} + - {roma: [GY, u], kana: っぎゅ} + - {roma: [GY, o], kana: っぎょ} + - {roma: [SH, a], kana: っしゃ} + - {roma: [SH, u], kana: っしゅ} + - {roma: [SH, o], kana: っしょ} + - {roma: [J, a], kana: っじゃ} + - {roma: [J, u], kana: っじゅ} + - {roma: [J, o], kana: っじょ} + - {roma: [CH, a], kana: っちゃ} + - {roma: [CH, u], kana: っちゅ} + - {roma: [CH, o], kana: っちょ} + - {roma: [DJ, a], kana: っぢゃ} + - {roma: [DJ, u], kana: っぢゅ} + - {roma: [DJ, o], kana: っぢょ} + - {roma: [NY, a], kana: っにゃ} + - {roma: [NY, u], kana: っにゅ} + - {roma: [NY, o], kana: っにょ} + - {roma: [HY, a], kana: っひゃ} + - {roma: [HY, u], kana: っひゅ} + - {roma: [HY, o], kana: っひょ} + - {roma: [BY, a], kana: っびゃ} + - {roma: [BY, u], kana: っびゅ} + - {roma: [BY, o], kana: っびょ} + - {roma: [PY, a], kana: っぴゃ} + - {roma: [PY, u], kana: っぴゅ} + - {roma: [PY, o], kana: っぴょ} + - {roma: [MY, a], kana: っみゃ} + - {roma: [MY, u], kana: っみゅ} + - {roma: [MY, o], kana: っみょ} + - {roma: [RXY, a], kana: っりゃ} + - {roma: [RXY, u], kana: っりゅ} + - {roma: [RXY, o], kana: っりょ} + - {roma: [Y, e], kana: っいぇ} + - {roma: [KY, e], kana: っきぇ} + - {roma: [GY, e], kana: っぎぇ} + - {roma: [SH, e], kana: っしぇ} + - {roma: [J, e], kana: っじぇ} + - {roma: [CH, e], kana: っちぇ} + - {roma: [DJ, e], kana: っぢぇ} + - {roma: [NY, e], kana: っにぇ} + - {roma: [HY, e], kana: っひぇ} + - {roma: [BY, e], kana: っびぇ} + - {roma: [PY, e], kana: っぴぇ} + - {roma: [MY, e], kana: っみぇ} + - {roma: [RXY, e], kana: っりぇ} + - {roma: [W, i], kana: っうぃ} + - {roma: [W, e], kana: っうぇ} + - {roma: [W, o], kana: っうぉ} + - {roma: [W, o], kana: っを} + - {roma: [KW, a], kana: っくぁ} + - {roma: [KW, i], kana: っくぃ} + - {roma: [KW, e], kana: っくぇ} + - {roma: [KW, o], kana: っくぉ} + - {roma: [GW, a], kana: っぐぁ} + - {roma: [GW, i], kana: っぐぃ} + - {roma: [GW, e], kana: っぐぇ} + - {roma: [GW, o], kana: っぐぉ} + - {roma: [TS, a], kana: っつぁ} + - {roma: [TS, i], kana: っつぃ} + - {roma: [TS, e], kana: っつぇ} + - {roma: [TS, o], kana: っつぉ} + - {roma: [F, a], kana: っふぁ} + - {roma: [F, i], kana: っふぃ} + - {roma: [F, e], kana: っふぇ} + - {roma: [F, o], kana: っふぉ} + - {roma: [V, u], kana: っヴ} + - {roma: [V, a], kana: っヴぁ} + - {roma: [V, i], kana: っヴぃ} + - {roma: [V, e], kana: っヴぇ} + - {roma: [V, o], kana: っヴぉ} + - {roma: [S, i], kana: っすぃ} + - {roma: [Z, i], kana: っずぃ} + - {roma: [T, i], kana: ってぃ} + - {roma: [TY, u], kana: ってゅ} + - {roma: [T, u], kana: っとぅ} + - {roma: [D, i], kana: っでぃ} + - {roma: [DY, u], kana: っでゅ} + - {roma: [D, u], kana: っどぅ} + - {roma: [NGX, a], kana: っか゜} + - {roma: [NGX, i], kana: っき゜} + - {roma: [NGX, u], kana: っく゜} + - {roma: [NGX, e], kana: っけ゜} + - {roma: [NGX, o], kana: っこ゜} + - {roma: [NGX, Y, e], kana: っき゜ぇ} + - {roma: [NGX, Y, a], kana: っき゜ゃ} + - {roma: [NGX, Y, u], kana: っき゜ゅ} + - {roma: [NGX, Y, o], kana: っき゜ょ} + - {roma: [WR, a], kana: ッラ} + - {roma: [WR, i], kana: ッリ} + - {roma: [WR, u], kana: ッル} + - {roma: [WR, e], kana: ッレ} + - {roma: [WR, o], kana: ッロ} + +entries: + - grapheme: b + phonemes: [b] + - grapheme: cl + phonemes: [q] + - grapheme: d + phonemes: [d] + - grapheme: dh + phonemes: [dh] + - grapheme: dr + phonemes: [dr] + - grapheme: f + phonemes: [f] + - grapheme: g + phonemes: [g] + - grapheme: h + phonemes: [h] + - grapheme: hh + phonemes: [hh] + - grapheme: j + phonemes: [j] + - grapheme: jh + phonemes: [jh] + - grapheme: l + phonemes: [l] + - grapheme: m + phonemes: [M] + - grapheme: n + phonemes: [N] + - grapheme: ng + phonemes: [NG] + - grapheme: p + phonemes: [p] + - grapheme: q + phonemes: [q] + - grapheme: s + phonemes: [s] + - grapheme: sh + phonemes: [sh] + - grapheme: t + phonemes: [t] + - grapheme: th + phonemes: [th] + - grapheme: tr + phonemes: [tr] + - grapheme: vf + phonemes: [vf] + - grapheme: Fr + phonemes: [Fr] + - grapheme: v + phonemes: [v] + - grapheme: w + phonemes: [w] + - grapheme: y + phonemes: [y] + - grapheme: z + phonemes: [z] + - grapheme: zh + phonemes: [zh] \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/JA_CVVX.cs b/JapaneseCVVXPhonemizer/JA_CVVX.cs new file mode 100644 index 000000000..c8bfec36a --- /dev/null +++ b/JapaneseCVVXPhonemizer/JA_CVVX.cs @@ -0,0 +1,1085 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Classic; +using OpenUtau.Api; +using OpenUtau.Classic; +using OpenUtau.Core.G2p; +using OpenUtau.Core.Ustx; +using Serilog; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using System.Text.RegularExpressions; +using JA_SBP.Data; +using WanaKanaNet; +using System.Text; + +namespace OpenUtau.Plugin.Builtin { + [Phonemizer("Japanese CVVX Phonemizer", "JA CVVX", "Cadlaxa", language: "JA")] + public class JA_CVVX : SyllableBasedPhonemizer { + + // Let the parent handle YAML Template logic securely + protected override string YamlFileName => "ja-cvvx.yaml"; + protected override string YamlVersion => "1.2"; + protected override byte[] YamlTemplate => Resources.template; + + public Dictionary> WanaKanaDictionary = new Dictionary>(); + public Dictionary> KanaToPhonemeDict = new Dictionary>(); + private Dictionary hiraganaToConsonantAliasMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary> defConsonants = new Dictionary>(); + + protected override string[] GetVowels() => "a i u e o".Split(); + protected override string[] GetConsonants() => "b by ch d dh f g gy h hy j k ky l ly m my n ny ng p py r ry s sh t ts th v w y z zh".Split(); + + // For banks with missing vowels + private readonly Dictionary missingVphonemes = "ax=ah".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2 && parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingVPhonemes = false; + + // For banks with missing custom consonants + private readonly Dictionary missingCphonemes = "".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2 && parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingCPhonemes = false; + + // TIMIT symbols + private readonly Dictionary timitphonemes = "axh=ax,bcl=b,dcl=d,eng=ng,gcl=g,hv=hh,kcl=k,pcl=p,tcl=t".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2 && parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isTimitPhonemes = false; + + private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; + private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; + + private List>> sortedHiraToRomaKeys; + private string[] sortedConsonants; + private string[] sortedVowels; + + public override void SetSinger(USinger singer) { + base.SetSinger(singer); + if (this.singer == null || !this.singer.Loaded) return; + + string file = Path.Combine(singer.Location, YamlFileName); + if (!File.Exists(file)) { + file = Path.Combine(PluginDir, YamlFileName); + } + + if (File.Exists(file)) { + try { + // Extract specifically the custom structures utilizing the parent's tolerant deserializer + var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); + + WanaKanaDictionary.Clear(); + KanaToPhonemeDict.Clear(); + + if (data.wanakana != null) { + foreach (var entry in data.wanakana) { + string key = string.Join("", entry.FromList); + string value = string.Join(" ", entry.ToList); + List phonemeList = entry.FromList; + + if (!WanaKanaDictionary.ContainsKey(key)) { + WanaKanaDictionary.Add(key, new List()); + } + if (!KanaToPhonemeDict.ContainsKey(value)) { + KanaToPhonemeDict.Add(value, phonemeList); + } + WanaKanaDictionary[key].Add(value); + } + } + + defConsonants.Clear(); + hiraganaToConsonantAliasMap.Clear(); + + if (data.consonantGroups != null) { + foreach (var entry in data.consonantGroups) { + string alias = entry.FromList.FirstOrDefault(); + if (string.IsNullOrEmpty(alias)) continue; + + if (!defConsonants.ContainsKey(alias)) { + defConsonants[alias] = new List(); + } + + foreach (string member in entry.ToList) { + defConsonants[alias].Add(member); + if (!Regex.IsMatch(member, "^[a-zA-Z]+$")) { + hiraganaToConsonantAliasMap[member] = alias; + } + } + } + UpdateHiraganaToConsonantMap(); + } + + foreach (var kvp in yamlFallbacks) { + missingVphonemes[kvp.Key] = kvp.Value; + } + + } catch (Exception ex) { + Log.Error(ex, $"Failed to parse custom JA_CVVX fields from {file}"); + } + } + } + + public class CustomYAMLData: YAMLData { + public WanaKanaData[] wanakana { get; set; } = Array.Empty(); + public ConsonantGroups[] consonantGroups { get; set; } = Array.Empty(); + } + + public class ConsonantGroups { + public object group { get; set; } + public object members { get; set; } + + public List FromList { + get { + if (group is string s) return new List { s }; + if (group is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + return new List(); + } + } + + public List ToList { + get { + if (members is string s) return new List { s }; + if (members is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + return new List(); + } + } + } + + public class WanaKanaData { + public object roma { get; set; } + public object kana { get; set; } + + public List FromList { + get { + if (roma is string s) return new List { s }; + if (roma is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + return new List(); + } + } + + public List ToList { + get { + if (kana is string s) return new List { s }; + if (kana is IEnumerable list) return list.Select(x => x.ToString()).ToList(); + return new List(); + } + } + } + + // Parent handles dict replacements, rely on its inheritance + private string ReplacePhoneme(string phoneme, int tone) { + if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { + return replaced; + } + if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { + return phoneme; + } + return phoneme; + } + + protected override string[] GetSymbols(Note note) { + string[] original = base.GetSymbols(note); + if (tails.Contains(note.lyric)) { + return new string[] { note.lyric }; + } + + sortedHiraToRomaKeys = KanaToPhonemeDict.OrderByDescending(k => k.Key.Length).ToList(); + sortedConsonants = consonants.OrderByDescending(c => c.Length).ToArray(); + sortedVowels = vowels.OrderByDescending(v => v.Length).ToArray(); + + if (original == null || original.Length == 0 || string.IsNullOrEmpty(original[0])) { + string lyric = note.lyric.Trim().ToLowerInvariant(); + string romaji = ""; + + if (Regex.IsMatch(lyric, @"[\p{IsHiragana}\p{IsKatakana}]+")) { + int ja = 0; + while (ja < lyric.Length) { + string match = null; + + foreach (var kv in sortedHiraToRomaKeys) { + if (ja + kv.Key.Length <= lyric.Length && lyric.Substring(ja, kv.Key.Length) == kv.Key) { + match = kv.Key; + romaji += string.Join("", kv.Value); + ja += kv.Key.Length; + break; + } + } + + if (match == null) { + romaji += WanaKana.ToRomaji(lyric[ja].ToString()); + ja++; + } + } + } else { + romaji = lyric; + } + + List split = new List(); + int ii = 0; + while (ii < romaji.Length) { + string match = null; + + foreach (var cons in sortedConsonants) { + if (romaji.Substring(ii).StartsWith(cons)) { + match = cons; + split.Add(cons); + ii += cons.Length; + break; + } + } + + if (match != null) continue; + + foreach (var vow in sortedVowels) { + if (romaji.Substring(ii).StartsWith(vow)) { + match = vow; + split.Add(vow); + ii += vow.Length; + break; + } + } + + if (match == null) { + split.Add(romaji[ii].ToString()); + ii++; + } + } + original = split.ToArray(); + } + + // Note: The previous massive Merging & Splitting Replacement loop has been removed + // since the parent class intrinsically applies all YAML regex replacements to the array + // returned by GetSymbols() via `ApplyReplacements()`. + + List finalProcessedPhonemes = new List(); + string[] tr = new[] { "tr" }; + string[] dr = new[] { "dr" }; + string[] wh = new[] { "wh" }; + string[] av_c = new[] { "al", "am", "an", "ang", "ar" }; + string[] ev_c = new[] { "el", "em", "en", "eng", "err" }; + string[] iv_c = new[] { "il", "im", "in", "ing", "ir" }; + string[] ov_c = new[] { "ol", "om", "on", "ong", "or" }; + string[] uv_c = new[] { "ul", "um", "un", "ung", "ur" }; + var consonatsV1 = new List { "l", "m", "n", "r" }; + var consonatsV2 = new List { "mm", "nn", "ng" }; + + List vowel3S = new List(); + foreach (string V1 in vowels) { + foreach (string C1 in consonatsV1) { vowel3S.Add($"{V1}{C1}"); } + } + + List vowel4S = new List(); + foreach (string V1 in vowels) { + foreach (string C1 in consonatsV2) { vowel4S.Add($"{V1}{C1}"); } + } + + IEnumerable phonemes = original; + foreach (string s in phonemes) { + switch (s) { + case var str when dr.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "jh", s[1].ToString() }); + break; + case var str when tr.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "ch", s[1].ToString() }); + break; + case var str when wh.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "hh", s[1].ToString() }); + break; + case var str when av_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "aa", s[1].ToString() }); + break; + case var str when ev_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "eh", s[1].ToString() }); + break; + case var str when iv_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "iy", s[1].ToString() }); + break; + case var str when ov_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "ao", s[1].ToString() }); + break; + case var str when uv_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { "uw", s[1].ToString() }); + break; + case var str when vowel3S.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s[2].ToString() }); + break; + case var str when vowel4S.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): + finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s.Substring(2, 2) }); + break; + default: + finalProcessedPhonemes.Add(s); + break; + } + } + return finalProcessedPhonemes.Where(p => p != null).ToArray(); + } + + private string ToHiragana(string alias, int tone) { + var convertedHiragana = ""; + int i = 0; + + while (i < alias.Length) { + bool foundMatch = false; + + var potentialRomajiKeys = WanaKanaDictionary + .Keys + .Where(key => alias.Length >= i + key.Length && + alias.Substring(i, key.Length).Equals(key, StringComparison.Ordinal)) + .OrderByDescending(key => key.Length) + .ToList(); + + foreach (var romajiKey in potentialRomajiKeys) { + var kanaValues = WanaKanaDictionary[romajiKey]; + string selectedKana = null; + + foreach (var kana in kanaValues) { + var validatedKana = kana; + if (HasOto(validatedKana, tone) || HasOto(ValidateAlias(validatedKana), tone)) { + selectedKana = validatedKana; + break; + } + } + + if (selectedKana == null) { + selectedKana = (kanaValues[0]); + } + + convertedHiragana += selectedKana; + i += romajiKey.Length; + foundMatch = true; + break; + } + + if (!foundMatch) { + convertedHiragana += alias[i]; + i++; + } + } + return convertedHiragana; + } + + private void UpdateHiraganaToConsonantMap() { + hiraganaToConsonantAliasMap.Clear(); + + foreach (var entry in defConsonants) { + string preferredConsonant = entry.Key; + List members = entry.Value; + + foreach (string member in members) { + if (member != preferredConsonant) { + if (!string.IsNullOrEmpty(member) && !hiraganaToConsonantAliasMap.ContainsKey(member)) { + hiraganaToConsonantAliasMap[member] = preferredConsonant; + } + } + } + } + } + + private string GetVC(string hiraganaSyllable, string baseConsonant) { + if (string.IsNullOrWhiteSpace(hiraganaSyllable)) return baseConsonant; + if (hiraganaToConsonantAliasMap.TryGetValue(hiraganaSyllable.Trim(), out string alias)) { + return alias; + } + return baseConsonant; + } + + protected override List ProcessSyllable(Syllable syllable) { + var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); + var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; + string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + List vowelsList = new List { ReplacePhoneme(syllable.v, syllable.vowelTone) }; + string v = ReplacePhoneme(syllable.v, syllable.vowelTone); + string basePhoneme; + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string[] CurrentWordCc = syllable.CurrentWordCc.Select(ReplacePhoneme).ToArray(); + string[] PreviousWordCc = syllable.PreviousWordCc.Select(ReplacePhoneme).ToArray(); + int prevWordConsonantsCount = syllable.prevWordConsonantsCount; + + foreach (var entry in missingVphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { + isMissingVPhonemes = true; + break; + } + } + + foreach (var entry in missingCphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isMissingCPhonemes = true; + break; + } + } + + foreach (var entry in timitphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isTimitPhonemes = true; + break; + } + } + + var hv = $"- {ToHiragana(v, syllable.vowelTone)}"; + + if (syllable.IsStartingV) { + if (HasOto(hv, syllable.vowelTone) || HasOto(ValidateAlias(hv), syllable.vowelTone)) { + basePhoneme = AliasFormat(ToHiragana(v, syllable.vowelTone), "startingV", syllable.vowelTone, ""); + } else if (HasOto(ToHiragana(v, syllable.vowelTone), syllable.vowelTone) || HasOto(ValidateAlias(ToHiragana(v, syllable.vowelTone)), syllable.vowelTone)) { + basePhoneme = AliasFormat(ToHiragana(v, syllable.vowelTone), "startingV", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); + } + } + else if (syllable.IsVV) { + if (!CanMakeAliasExtension(syllable)) { + basePhoneme = $"{prevV} {ToHiragana(v, syllable.vowelTone)}"; + int tone = syllable.vowelTone; + string hiraganaV = ToHiragana(v, tone); + string[] candidates = { $"{prevV} {hiraganaV}", $"{prevV} {v}" }; + string[] candidates2 = { AliasFormat(hiraganaV, "vv_start", tone, ""), AliasFormat(v, "vv_start", tone, "") }; + bool foundMatch = false; + for (int i = 0; i < candidates.Length; i++) { + string c1 = candidates[i]; + string c2 = candidates2[i]; + + if (HasOto(c1, tone) || HasOto(ValidateAlias(c1), tone)) { + basePhoneme = c1; + foundMatch = true; + break; + } + else if (HasOto(c2, tone) || HasOto(ValidateAlias(c2), tone)) { + basePhoneme = c2; + foundMatch = true; + break; + } + } + if (!foundMatch) { + if (HasOto(ToHiragana($"{prevV}{v}", syllable.vowelTone), syllable.vowelTone) || HasOto(ValidateAlias(ToHiragana($"{prevV}{v}", syllable.vowelTone)), syllable.vowelTone)) { + basePhoneme = ToHiragana($"{prevV}{v}", syllable.vowelTone); + } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + } else { + basePhoneme = AliasFormat($"{v}", "vv_start", syllable.vowelTone, ""); + } + } + } else if (HasOto($"{ToHiragana(v, syllable.vowelTone)}", syllable.vowelTone) && HasOto(ValidateAlias($"{ToHiragana(v, syllable.vowelTone)}"), syllable.vowelTone)) { + basePhoneme = ToHiragana(v, syllable.vowelTone); + } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = null; + } + } else if (syllable.IsStartingCVWithOneConsonant) { + var rcv = $"- {cc[0]} {v}"; + var rcv1 = $"- {cc[0]}{v}"; + var crv = $"{cc[0]} {v}"; + var hcv = ToHiragana($"{cc[0]}{v}", syllable.tone); + + if (HasOto($"- {hcv}", syllable.vowelTone) || HasOto(ValidateAlias($"- {hcv}"), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynStart", syllable.vowelTone, ""); + } else if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) || HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); + } else if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + } else { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + } + } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { + var rccv = $"- {string.Join("", cc)} {v}"; + var rccv1 = $"- {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + var crv1 = $"{cc.Last()}{v}"; + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + var hccv = ToHiragana($"{string.Join("", cc)}{v}", syllable.tone); + var hcv = ToHiragana($"{cc.Last()}{v}", syllable.tone); + + if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat(hccv, "dynStart", syllable.vowelTone, ""); + lastC = 0; + } else if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); + lastC = 0; + } else { + if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + + for (var i = cc.Length; i > 1; i--) { + if (!ccvException.Contains(cc[0])) { + if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, "")))) { + firstC = i - 1; + } + } + break; + } + + if (phonemes.Count == 0) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); + } + } + + for (var i = firstC; i < cc.Length - 1; i++) { + var cv = $"{cc.Last()} {v}"; + if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[i])) { + if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + } else { + var vcv = $"{prevV} {cc[0]}{v}"; + var vccv = $"{prevV} {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + var hccv = ToHiragana($"{string.Join("", cc)}{v}", syllable.tone); + var hcv = ToHiragana($"{cc.Last()}{v}", syllable.tone); + var hvcv = $"{prevV} {ToHiragana($"{cc[0]}{v}", syllable.tone)}"; + var hvccv = $"{prevV} {ToHiragana($"{string.Join("", cc)}{v}", syllable.tone)}"; + + if (syllable.IsVCVWithOneConsonant && (HasOto(hvcv, syllable.vowelTone) || HasOto(ValidateAlias(hvcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = hvcv; + } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(hvccv, syllable.vowelTone) || HasOto(ValidateAlias(hvccv), syllable.vowelTone) && prevWordConsonantsCount == 0)) { + basePhoneme = hvccv; + lastC = 0; + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone) && prevWordConsonantsCount == 0)) { + basePhoneme = vccv; + lastC = 0; + } else { + var cv = $"{cc.Last()}{v}"; + + if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + + for (var i = firstC; i < cc.Length - 1; i++) { + if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[i])) { + if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { + basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + + if (basePhoneme.Contains("-")) { + for (int clusterLength = 3; clusterLength >= 2; clusterLength--) { + if (clusterLength > cc.Length) continue; + + var cluster = new string[clusterLength]; + Array.Copy(cc, 0, cluster, 0, clusterLength); + + var consonantPatterns = new List(); + + if (clusterLength >= 3) { + consonantPatterns.Add($"{cluster[0]} {cluster[1]}{cluster[2]}"); + consonantPatterns.Add($"{cluster[0]}{cluster[1]} {cluster[2]}"); + consonantPatterns.Add($"{cluster[0]} {cluster[1]} {cluster[2]}"); + } else if (clusterLength == 2) { + consonantPatterns.Add($"{cluster[0]} {cluster[1]}"); + consonantPatterns.Add($"{cluster[0]}{cluster[1]}"); + } + + foreach (var consPattern in consonantPatterns) { + string[] endPatterns = { "-", $" -" }; + foreach (var end in endPatterns) { + string endingcc = $"{consPattern}{end}"; + + if (HasOto(endingcc, syllable.tone)) { + basePhoneme = endingcc; + lastC = 0; + goto FoundMatch; + } + } + } + } + } + + FoundMatch:; + + for (var i = lastC + 1; i >= 0; i--) { + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vc = $"{prevV} {cc[0]}"; + bool CCV = false; + + if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { + if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { + CCV = true; + } + } + + string preferredC = GetVC(ToHiragana($"{string.Join("", cc)}{v}", syllable.tone), cc[0]); + var hvc = $"{prevV} {preferredC}"; + + if (i == 0 && !HasOto(vc, syllable.tone)) { + break; + } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { + phonemes.Add(vcc); + firstC = 1; + break; + } else if (HasOto(hvc, syllable.tone) || HasOto(ValidateAlias(hvc), syllable.tone)) { + phonemes.Add(hvc); + break; + } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { + phonemes.Add(vc); + break; + } + } + } + } + + for (var i = firstC; i < lastC; i++) { + var cc1 = $"{string.Join(" ", cc.Skip(i))}"; + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + + if (!HasOto(cc1, syllable.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + cc1 = AliasFormat($"{cc[i]}", "cc_endB", syllable.vowelTone, ""); + } + + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]}{string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]}{string.Join("", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}"; + lastC = i; + } + + if (i + 1 < lastC) { + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + if (!HasOto(cc1, syllable.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + cc1 = AliasFormat($"{cc[i]}", "cc_endB", syllable.vowelTone, ""); + } + if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); + + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]}{string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]}{string.Join("", cc.Skip(i + 1))}"; + lastC = i; + } + if (HasOto($"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}"; + lastC = i; + } + + if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { + phonemes.Add(cc1); + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1))) { + if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { + i++; + } + } else { + if (PreviousWordCc.Contains(cc1) == CurrentWordCc.Contains(cc1)) { + cc1 = ValidateAlias(cc1); + } else { + TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); + } + } + } else { + TryAddPhoneme(phonemes, syllable.tone, cc1); + } + } + + phonemes.Add(basePhoneme); + return phonemes; + } + + protected override List ProcessEnding(Ending ending) { + string prevV = ReplacePhoneme(ending.prevV, ending.tone); + string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); + string v = ReplacePhoneme(ending.prevV, ending.tone); + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + + if (tails.Contains(ending.prevV)) { + return new List(); + } + + if (ending.IsEndingV) { + var vR = $"{prevV} -"; + var vR1 = $"{prevV} R"; + var vR2 = $"{prevV}-"; + if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR1, ending.tone) || HasOto(ValidateAlias(vR1), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { + phonemes.Add(AliasFormat($"{prevV}", "ending", ending.tone, "")); + } + } else if (ending.IsEndingVCWithOneConsonant) { + var vc = $"{prevV} {cc[0]}"; + var vcr = $"{prevV} {cc[0]}-"; + var vcr2 = $"{prevV}{cc[0]} -"; + var vcr3 = $"{prevV} {cc[0]} -"; + var vcr4 = $"{prevV}{cc[0]}-"; + if (!RomajiException.Contains(cc[0])) { + if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || (HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone))) { + phonemes.Add(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "")); + } else if (HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { + phonemes.Add(vcr3); + } else if (HasOto(vcr4, ending.tone) && HasOto(ValidateAlias(vcr4), ending.tone)) { + phonemes.Add(vcr4); + } else if (HasOto(vc, ending.tone) && HasOto(ValidateAlias(vc), ending.tone)) { + phonemes.Add(vc); + if (vc.Contains(cc[0])) { + phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "")); + } + } else { + phonemes.Add(vc); + if (vc.Contains(cc[0])) { + phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "")); + } + } + } + } else { + for (var i = lastC; i >= 0; i--) { + var vr = $"{v} -"; + var vr1 = $"{v} R"; + var vr2 = $"{v}-"; + var vcc = $"{v} {string.Join("", cc.Take(2))}-"; + var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} -"; + var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; + var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; + var vc = $"{v} {cc[0]}"; + + if (!RomajiException.Contains(cc[0])) { + if (i == 0) { + if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { + phonemes.Add(AliasFormat($"{v}", "ending", ending.tone, "")); + } + break; + } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + phonemes.Add(vcc); + firstC = 1; + break; + } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + phonemes.Add(vcc2); + firstC = 1; + break; + } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { + phonemes.Add(vcc3); + if (vcc3.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "")); + } + } + firstC = 1; + break; + } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { + phonemes.Add(vcc4); + if (vcc4.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "")); + } + } + firstC = 1; + break; + } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) + || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) + || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) + || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { + phonemes.Add(vc); + break; + } else { + phonemes.Add(vc); + break; + } + } + } + for (var i = firstC; i < lastC; i++) { + var cc1 = $"{cc[i]} {cc[i + 1]}"; + if (i < cc.Length - 2) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); + if (!HasOto(cc2, ending.tone)) cc2 = ValidateAlias(cc2); + + if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { + cc2 = AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, ""); + } + if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); + + if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone))) { + phonemes.Add(cc1); + } else if ((HasOto(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone)))) { + phonemes.Add(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "")); + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}-", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"))) { + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + i++; + } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "")); + i++; + } else { + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} -", ValidateAlias($"{cc[i]} -")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 2], ValidateAlias(cc[i + 2]), $"{cc[i + 2]} -", ValidateAlias($"{cc[i + 2]} -")); + i++; + } + } else { + if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); + if (!HasOto(cc1, ending.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; + if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); + + if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + cc1 = AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""); + } + if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); + + if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } + } + } + } + return phonemes; + } + + private string AliasFormat(string alias, string type, int tone, string prevV) { + var aliasFormats = new Dictionary { + { "dynStart", new string[] { "" } }, + { "dynMid", new string[] { "" } }, + { "dynMid_vv", new string[] { "" } }, + { "dynEnd", new string[] { "" } }, + { "startingV", new string[] { "-", "- ", "_", "" } }, + { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, + { "vv", new string[] { "", "_", "-", "- " } }, + { "vv_start", new string[] { "* ", "_", "", "" } }, + { "cv", new string[] { "-", "", "- ", "_" } }, + { "cvStart", new string[] { "-", "- ", "_" } }, + { "ending", new string[] { " R", "-", " -" } }, + { "ending_mix", new string[] { "-", " -", "R", " R", "_", "--" } }, + { "cc", new string[] { "", "-", "- ", "_" } }, + { "cc_start", new string[] { "- ", "-", "_" } }, + { "cc_end", new string[] { " -", "-", "" } }, + { "cc_inB", new string[] { "_", "-", "- " } }, + { "cc_endB", new string[] { "_", "-", " -" } }, + { "cc_mix", new string[] { " -", " R", "-", "", "_", "- ", "-" } }, + { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, + }; + + if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { + return alias; + } + + if (type.Contains("dynStart")) { + string consonant = ""; + string vowel = ""; + if (alias.Contains(" ")) { + var parts = alias.Split(' '); + consonant = parts[0]; + vowel = parts[1]; + } else { + consonant = alias; + } + + var dynamicVariations = new List { + $"- {consonant}{vowel}", + $"- {consonant} {vowel}", + $"-{consonant} {vowel}", + $"-{consonant}{vowel}", + $"-{consonant}_{vowel}", + $"- {consonant}_{vowel}", + }; + foreach (var variation in dynamicVariations) { + if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { + return 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}", + $"{consonant} {vowel}", + $"{consonant}_{vowel}", + }; + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + return 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} -", + $"{vowel} {consonant}-", + $"{vowel}{consonant}-", + $"{vowel} {consonant} -", + }; + foreach (var variation1 in dynamicVariations1) { + if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { + return variation1; + } + } + } + + var formatsToTry = aliasFormats[type]; + int counter = 0; + foreach (var format in formatsToTry) { + string aliasFormat; + if (type.Contains("mix") && counter < 4) { + aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; + counter++; + } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { + aliasFormat = $"{alias}{format}"; + } else { + aliasFormat = $"{format}{alias}"; + } + + if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + return aliasFormat; + } + } + return alias; + } + + protected override string ValidateAlias(string alias) { + if (isMissingVPhonemes) { + foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isMissingCPhonemes) { + foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isTimitPhonemes) { + foreach (var fb in timitphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + return alias; + } + + bool PhonemeIsPresent(string alias, string phoneme) { + if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) return false; + if (alias == phoneme) return true; + return alias.EndsWith(phoneme); + } + + protected override bool NoGap => true; + + protected override double GetTransitionMultiplier(string alias) { + return 1.0; + } + + protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { + double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); + + var tokens = alias.Split(' ') + .Select(t => t.Replace("-", "").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)) { + return GetTransitionBasicLengthMsByConstant() * value; + } + } + + return otoLength; + } + } +} \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/JA_CVVX.csproj b/JapaneseCVVXPhonemizer/JA_CVVX.csproj new file mode 100644 index 000000000..74eb43b38 --- /dev/null +++ b/JapaneseCVVXPhonemizer/JA_CVVX.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + JA_SBP + enable + enable + + + + + + + + + Resources.resx + True + True + + + + + + Resources.Designer.cs + ResXFileCodeGenerator + + + + diff --git a/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs b/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs new file mode 100644 index 000000000..a2852dcfe --- /dev/null +++ b/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs @@ -0,0 +1,1780 @@ +using System; +using System.Collections.Generic; +using System.Text; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using System.Linq; +using System.IO; +using Serilog; +using System.Threading.Tasks; +using static OpenUtau.Api.Phonemizer; +using System.Collections; + +namespace OpenUtau.Plugin.Builtin { + /// + /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; + /// + /// - Supports dictionary; + /// - Automatically align phonemes to notes; + /// - Supports syllable extension; + /// - Automatically calculates transition phonemes length, with constants by default, + /// but there is a pre-created function to use Oto value; + /// - The transition length is scaled based on Tempo and note length. + /// + /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". + /// + /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], + /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". + /// + /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". + /// + /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, + /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). + /// + public abstract class SyllableBasedPhonemizer : Phonemizer { + + /// + /// Syllable is [V] [C..] [V] + /// + protected struct Syllable { + /// + /// vowel from previous syllable for VC + /// + public string prevV; + /// + /// CCs, may be empty + /// + public string[] cc; + /// + /// "base" note. May not actually be vowel, if only consonants way provided + /// + public string v; + /// + /// Start position for vowel. All VC CC goes before this position + /// + public int position; + /// + /// previous note duration, i.e. this is container for VC and CC notes + /// + public int duration; + /// + /// Tone for VC and CC + /// + public int tone; + /// + /// Other phoneme attributes for VC and CC + /// + public PhonemeAttributes[] attr; + /// + /// tone for base "vowel" phoneme + /// + public int vowelTone; + /// + /// Other phoneme attributes for base "vowel" phoneme + /// + public PhonemeAttributes[] vowelAttr; + + /// + /// 0 if no consonants are taken from previous word; + /// 1 means first one is taken from previous word, etc. + /// + public int prevWordConsonantsCount; + + /// + /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. + /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. + /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible + /// + public bool canAliasBeExtended; + + // helpers + public bool IsStartingV => prevV == "" && cc.Length == 0; + public bool IsVV => prevV != "" && cc.Length == 0; + + public bool IsStartingCV => prevV == "" && cc.Length > 0; + public bool IsVCV => prevV != "" && cc.Length > 0; + + public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; + public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; + + public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; + public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; + + public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); + public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; + } + } + + protected struct Ending { + /// + /// vowel from the last syllable to make VC + /// + public string prevV; + /// + /// actuall CC at the ending + /// + public string[] cc; + /// + /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) + /// + public string tail; + public bool HasTail => !string.IsNullOrEmpty(tail); + /// + /// last note position + duration, all phonemes must be less than this + /// + public int position; + /// + /// last syllable length, max container for all VC CC C- + /// + public int duration; + /// + /// the tone from last syllable, for all ending phonemes + /// + public int tone; + /// + /// Other phoneme attributes from last syllable + /// + public PhonemeAttributes[] attr; + + // helpers + public bool IsEndingV => cc.Length == 0; + public bool IsEndingVC => cc.Length > 0; + public bool IsEndingVCWithOneConsonant => cc.Length == 1; + public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; + + public override string ToString() { + return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; + } + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + error = ""; + var mainNote = notes[0]; + if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return MakeForcedAliasResult(mainNote); + } + if (hasDictionary && isDictionaryLoading) { + return MakeSimpleResult(""); + } + + runtimeGlides.Clear(); + + var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); + if (syllables == null) { + return HandleError(); + } + + var phonemes = new List(); + int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI + + foreach (var syllable in syllables) { + var modifiedSyllable = ApplyBoundaryReplacements(syllable); + + if (tails.Contains(modifiedSyllable.v)) { + var ending = new Ending { + prevV = modifiedSyllable.prevV, + cc = modifiedSyllable.cc, + tail = modifiedSyllable.v, + position = modifiedSyllable.position, + duration = modifiedSyllable.duration, + tone = modifiedSyllable.tone, + attr = modifiedSyllable.attr + }; + + var endingPhonemes = ProcessEnding(ending); + + if (endingPhonemes != null) { + phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); + globalPhonemeIndex += endingPhonemes.Count; + } + continue; + } + + var syllablePhonemes = ProcessSyllable(modifiedSyllable); + var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); + int currentSyllablePhonemeCount = syllablePhonemes.Count; + + var basePhoneme = madePhonemes.LastOrDefault(); + string baseAlias = basePhoneme.phoneme ?? ""; + if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || + vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { + + string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); + if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { + int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); + madePhonemes.Add(new Phoneme { + phoneme = sustainData.sustain, + position = basePhoneme.position + offsetTicks, + index = globalPhonemeIndex + currentSyllablePhonemeCount + }); + currentSyllablePhonemeCount++; + } + } + phonemes.AddRange(madePhonemes); + globalPhonemeIndex += currentSyllablePhonemeCount; + } + + if (!nextNeighbour.HasValue) { + var tryEnding = MakeEnding(notes); + if (tryEnding.HasValue) { + var ending = tryEnding.Value; + + if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { + ending.tail = nextNeighbour.Value.lyric; + } + + var modifiedEnding = ApplyBoundaryReplacements(ending); + var endingPhonemes = ProcessEnding(modifiedEnding); + + if (endingPhonemes != null) { + 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 = finalPhonemes + }; + } + + protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { + int noteIndex = 0; + for (int i = 0; i < phonemes.Count; i++) { + var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var phoneme = phonemes[i]; + while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { + noteIndex++; + } + + 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); + validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); + + phoneme.phoneme = validatedAlias; + } else { + phoneme.phoneme = null; + phoneme.position = 0; + } + + phonemes[i] = phoneme; + } + return phonemes.ToArray(); + } + + private Result HandleError() { + return new Result { + phonemes = new Phoneme[] { + new Phoneme() { + phoneme = error + } + } + }; + } + + protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = + new YamlDotNet.Serialization.DeserializerBuilder() + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + + public override void SetSinger(USinger singer) { + if (this.singer != singer) { + this.singer = singer; + dictionaries.Clear(); + + if (this.singer == null || !this.singer.Loaded) { + return; + } + + if (string.IsNullOrEmpty(YamlFileName)) { + if (backupVowels != null) this.vowels = backupVowels; + else this.vowels = GetVowels(); + + if (backupConsonants != null) this.consonants = backupConsonants; + else this.consonants = GetConsonants(); + + if (backupDictionaryReplacements != null) { + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) { + dictionaryReplacements[kvp.Key] = kvp.Value; + } + } + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + return; + } + + // file paths + string globalFile = Path.Combine(PluginDir, YamlFileName); + string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) + ? Path.Combine(singer.Location, YamlFileName) + : null; + + // Local helper function to update and backup YAML files safely + void UpdateYamlIfNeeded(string filePath, bool isGlobal) { + if (string.IsNullOrEmpty(filePath)) return; + + bool shouldWriteTemplate = false; + bool shouldBackupOldFile = false; + string currentVersion = "unknown"; + + if (File.Exists(filePath)) { + if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { + try { + var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); + currentVersion = checkData?.version?.Trim() ?? ""; + + // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion + if (string.IsNullOrEmpty(currentVersion)) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } else if (Version.TryParse(currentVersion, out Version currV) && + Version.TryParse(YamlVersion, out Version targetV)) { + if (currV < targetV) { + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { + // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") + shouldWriteTemplate = true; + shouldBackupOldFile = true; + } + } catch (Exception ex) { + Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); + return; + } + } + } else if (isGlobal && YamlTemplate != null) { + shouldWriteTemplate = true; + } + + if (shouldBackupOldFile && File.Exists(filePath)) { + try { + // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml + string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; + string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); + + if (File.Exists(backupFile)) File.Delete(backupFile); + File.Move(filePath, backupFile); + Log.Information($"Old {YamlFileName} backed up to {backupFile}"); + } catch (Exception e) { + Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); + return; + } + } + + if (shouldWriteTemplate) { + try { + File.WriteAllBytes(filePath, YamlTemplate); + Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); + } catch (Exception e) { + Log.Error(e, $"Failed to write template to {filePath}"); + } + } + } + + UpdateYamlIfNeeded(globalFile, true); + UpdateYamlIfNeeded(singerFile, false); + + // add to parsing list (Global first, Singer second) + var filesToParse = new List(); + if (File.Exists(globalFile)) filesToParse.Add(globalFile); + if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); + + // backups of hardcoded defaults exist + if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); + if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); + if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); + if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); + if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); + + // reset live arrays/lists back to defaults before stacking + vowels = backupVowels; + consonants = backupConsonants; + tails = "-".Split(','); + + fricative = Array.Empty(); + aspirate = Array.Empty(); + semivowel = Array.Empty(); + liquid = Array.Empty(); + nasal = Array.Empty(); + stop = Array.Empty(); + tap = Array.Empty(); + affricate = Array.Empty(); + + dictionaryReplacements.Clear(); + foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; + + diphthongTails.Clear(); + foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; + + diphthongSplits.Clear(); + foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; + + mergingReplacements.Clear(); + splittingReplacements.Clear(); + yamlFallbacks.Clear(); + PhonemeOverrides.Clear(); + if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); + vowelSustains.Clear(); + foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; + + // parse the files sequentially (Singer configs seamlessly overwrite global configs) + foreach (var file in filesToParse) { + try { + var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); + + var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); + vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); + + var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tails = yamlTails.Concat(tails).Distinct().ToArray(); + + if (data?.isglides != null) enableGlides = data.isglides.Value; + + var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); + fricative = yFricative.Concat(fricative).Distinct().ToArray(); + var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); + var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); + semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); + var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); + liquid = yLiquid.Concat(liquid).Distinct().ToArray(); + var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); + nasal = yNasal.Concat(nasal).Distinct().ToArray(); + var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); + stop = yStop.Concat(stop).Distinct().ToArray(); + var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); + tap = yTap.Concat(tap).Distinct().ToArray(); + var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); + affricate = yAffricate.Concat(affricate).Distinct().ToArray(); + + var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) + .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); + consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); + + // DIPHTHONG AUTO-TAIL DETECTION + var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); + var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); + + foreach (var d in yamlDiphthongs) { + if (!diphthongSplits.ContainsKey(d)) { + foreach (var tail in dynamicTails) { + if (d.EndsWith(tail) && d != tail) { + diphthongTails[d] = tail; + break; + } + } + } + } + + // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) + if (data?.timings != null) { + foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; + } + + if (data?.replacements != null) { + var localMerge = new List(); + var localSplit = new List(); + string GetFromKey(object fromObj) { + if (fromObj is string s) return s; + if (fromObj is System.Collections.IEnumerable e) { + return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); + } + return ""; + } + + foreach (var rawReplacement in data.replacements) { + string fromKey = GetFromKey(rawReplacement.from); + mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); + + if (rawReplacement.from is string fromStr) { + dictionaryReplacements.Remove(fromStr); + dictionaryReplacements.Remove(fromStr.ToLower()); + dictionaryReplacements.Remove(fromStr.ToUpper()); + } + + List fromList = rawReplacement.FromList; + List toList = rawReplacement.ToList; + object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); + object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); + + var cleanReplacement = new Replacement { + from = parsedFrom, + to = parsedTo, + where = rawReplacement.where + }; + + if (parsedFrom is string fromString) { + if (parsedTo is string toString) { + // Dictionary handles simple 1-to-1 replacements + dictionaryReplacements[fromString] = toString; + } else { + // 1-to-Many goes to Split + localSplit.Add(cleanReplacement); + } + } else { + // Many-to-Any goes to Merge + localMerge.Add(cleanReplacement); + } + } + mergingReplacements.InsertRange(0, localMerge); + splittingReplacements.InsertRange(0, localSplit); + } + + if (data?.fallbacks != null) { + foreach (var df in data.fallbacks) { + if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { + yamlFallbacks[df.from] = df.to; + } + } + } + + if (data?.diphthongs != null) { + foreach (var d in data.diphthongs) { + if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { + diphthongTails[d.from] = d.to; + } + } + } + + if (data?.vowelsustains != null) { + foreach (var v in data.vowelsustains) { + if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { + vowelSustains[v.symbol] = (v.sustain, v.offset); + } + } + } + + } catch (Exception ex) { + Log.Error($"Failed to parse {file}: {ex.Message}"); + } + } + + if (!hasDictionary) { + ReadDictionaryAndInit(); + } else { + Init(); + } + } + } + + protected USinger singer; + protected bool hasDictionary => dictionaries.ContainsKey(GetType()); + protected IG2p dictionary => dictionaries[GetType()]; + protected bool isDictionaryLoading => dictionaries[GetType()] == null; + protected double TransitionBasicLengthMs => 100; + + private Dictionary dictionaries = new Dictionary(); + private const string FORCED_ALIAS_SYMBOL = "?"; + private string error = ""; + 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 + /// + /// + protected abstract string[] GetVowels(); + + /// + /// Returns list of consonants. Only needed if there is a dictionary + /// + /// + protected virtual string[] GetConsonants() { + throw new NotImplementedException(); + } + + /// + /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc + /// + /// List of phonemes + protected abstract List ProcessSyllable(Syllable syllable); + + /// + /// phoneme symbols for ending, like, V-, or VC-, or VC+C + /// + protected abstract List ProcessEnding(Ending ending); + + /// + /// simple alias to alias fallback + /// + /// + protected virtual Dictionary GetAliasesFallback() { return null; } + + /// + /// Use to some custom init, if needed + /// + protected virtual void Init() { } + + /// + /// Dictionary name. Must be stored in Dictionaries folder. + /// If missing or can't be read, phonetic input is used + /// + /// + protected virtual string GetDictionaryName() { return null; } + + /// + /// extracts array of phoneme symbols from note. Override for procedural dictionary or something + /// reads from dictionary if provided + /// + /// + /// + protected virtual string[] GetSymbols(Note note) { + string[] getSymbolsRaw(string lyrics) { + if (lyrics == null) { + return new string[0]; + } else return lyrics.Split(" "); + } + + if (tails.Contains(note.lyric)) { + return new string[] { note.lyric }; + } + + if (hasDictionary) { + if (!string.IsNullOrEmpty(note.phoneticHint)) { + return getSymbolsRaw(note.phoneticHint); + } + + var result = new List(); + foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { + var subResult = dictionary.Query(subword); + if (subResult == null) { + subResult = HandleWordNotFound(note); + if (subResult == null) { + return null; + } + } else { + for (int i = 0; i < subResult.Length; i++) { + string phoneme = subResult[i]; + if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { + subResult[i] = replaced; + } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { + subResult[i] = replacedExact; + } + } + } + result.AddRange(subResult); + } + return result.ToArray(); + } else { + return getSymbolsRaw(note.lyric); + } + } + + /// + /// 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. + /// + /// + protected virtual Dictionary GetDictionaryPhonemesReplacement() { + return dictionaryReplacements ?? new Dictionary(); + } + private string[] backupVowels = null; + private string[] backupConsonants = null; + private Dictionary backupDiphthongTails = null; + private Dictionary backupDiphthongSplits = null; + private Dictionary backupDictionaryReplacements = null; + protected Dictionary vowelSustains = new Dictionary(); + private Dictionary backupVowelSustains = null; + + /// + /// separates symbols to syllables, without an ending. + /// + /// + /// + /// + protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + var firstVowelId = vowelIds[0]; + if (notes.Length < vowelIds.Length) { + error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; + return null; + } + + var syllables = new Syllable[vowelIds.Length]; + + // Making the first syllable + if (prevEnding.HasValue) { + var prevEndingValue = prevEnding.Value; + var beginningCc = prevEndingValue.cc.ToList(); + beginningCc.AddRange(symbols.Take(firstVowelId)); + + // If we had a prev neighbour ending, let's take info from it + syllables[0] = new Syllable() { + prevV = prevEndingValue.prevV, + cc = beginningCc.ToArray(), + v = symbols[firstVowelId], + tone = prevEndingValue.tone, + attr = prevEndingValue.attr, + duration = prevEndingValue.duration, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes, + prevWordConsonantsCount = prevEndingValue.cc.Count() + }; + } else { + // there is only empty space before us + syllables[0] = new Syllable() { + prevV = "", + cc = symbols.Take(firstVowelId).ToArray(), + v = symbols[firstVowelId], + tone = notes[0].tone, + attr = notes[0].phonemeAttributes, + duration = -1, + position = 0, + vowelTone = notes[0].tone, + vowelAttr = notes[0].phonemeAttributes + }; + } + + // normal syllables after the first one + var noteI = 1; + var ccs = new List(); + var position = 0; + var lastSymbolI = firstVowelId + 1; + for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { + if (!vowelIds.Contains(lastSymbolI)) { + ccs.Add(symbols[lastSymbolI]); + } else { + position += notes[noteI - 1].duration; + syllables[noteI] = new Syllable() { + prevV = syllables[noteI - 1].v, + cc = ccs.ToArray(), + v = symbols[lastSymbolI], + tone = notes[noteI - 1].tone, + attr = notes[noteI - 1].phonemeAttributes, + duration = notes[noteI - 1].duration, + position = position, + vowelTone = notes[noteI].tone, + vowelAttr = notes[noteI].phonemeAttributes, + canAliasBeExtended = true // for all not-first notes is allowed + }; + ccs = new List(); + noteI++; + } + } + + return syllables; + } + + /// + /// extracts word ending + /// + /// + /// + protected Ending? MakeEnding(Note[] inputNotes) { + if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { + return null; + } + + (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); + if (symbols == null || vowelIds == null || notes == null) { + return null; + } + + return new Ending() { + prevV = symbols[vowelIds.Last()], + cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), + tone = notes.Last().tone, + attr = notes.Last().phonemeAttributes, + duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), + position = notes.Sum(n => n.duration) + }; + } + + /// + /// extracts and validates symbols and vowels + /// + /// + /// + private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { + var mainNote = notes[0]; + var symbols = GetSymbols(mainNote); + if (symbols == null) { + return (null, null, null); + } + if (symbols.Length == 0) { + symbols = new string[] { "" }; + } + + symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); + symbols = ApplyExtensions(symbols, notes); + List vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + vowelIds.Add(symbols.Length - 1); + } + if (notes.Length < vowelIds.Count) { + notes = HandleNotEnoughNotes(notes, vowelIds); + } + return (symbols, vowelIds.ToArray(), notes); + } + + /// + /// When there are more syllables than notes, recombines notes to match syllables count + /// + /// + /// + /// + protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { + var newNotes = new List(); + newNotes.AddRange(notes.SkipLast(1)); + var lastNote = notes.Last(); + var position = lastNote.position; + var notesToSplit = vowelIds.Count - newNotes.Count; + var duration = lastNote.duration / notesToSplit / 15 * 15; + for (var i = 0; i < notesToSplit; i++) { + var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); + newNotes.Add(new Note() { + position = position, + duration = durationFinal, + tone = lastNote.tone, + phonemeAttributes = lastNote.phonemeAttributes + }); + position += durationFinal; + } + + return newNotes.ToArray(); + } + + /// + /// Override this method, if you want to implement some machine converting from a word to phonemes + /// + /// + /// + protected virtual string[] HandleWordNotFound(Note note) { + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + string alt = attr.alternate?.ToString() ?? string.Empty; + string color = attr.voiceColor; + int toneShift = attr.toneShift; + var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); + if(HasOto(mpdlyric, note.tone)){ + error = mpdlyric; + }else{ + error = "word not found"; + } + return null; + } + + /// + /// Does this note extend the previous syllable? + /// + /// + /// + protected bool IsSyllableVowelExtensionNote(Note note) { + return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); + } + + /// + /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic + /// + /// + /// + protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { + return phonemesString.Split(' '); + } + + /// + /// use to validate alias + /// + /// + /// + protected virtual string ValidateAlias(string alias) { + return alias; + } + + /// + /// Defines basic transition length before scaling it according to tempo and note length + /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation + /// + /// Mapped alias + /// + protected virtual double GetTransitionBasicLengthMs(string alias = "") { + return GetTransitionBasicLengthMsByConstant(); + } + + 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 + /// + /// + protected double GetTempoNoteLengthFactor() { + return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; + } + + protected virtual IG2p[] GetBaseG2ps() { + return Array.Empty(); + } + + protected virtual IG2p LoadBaseDictionary() { + var g2ps = new List(); + + // Native YAML Dictionary Logic + if (!string.IsNullOrEmpty(YamlFileName)) { + string path = Path.Combine(PluginDir, YamlFileName); + + // Write template if missing + if (!File.Exists(path) && YamlTemplate != null) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + + // Load dictionary from Singer Folder (Highest Priority) + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + + // Load dictionary from Plugin Folder (Fallback Priority) + if (File.Exists(path)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {path}"); + } + } + } + // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) + else { + var dictionaryName = GetDictionaryName(); + if (!string.IsNullOrEmpty(dictionaryName)) { + var filename = Path.Combine(DictionariesPath, dictionaryName); + if (File.Exists(filename)) { + var dictionaryText = File.ReadAllText(filename); + var builder = G2pDictionary.NewBuilder(); + foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); + foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); + builder.AddEntry("a", new string[] { "a" }); + ParseDictionary(dictionaryText, builder); + g2ps.Add(builder.Build()); + } + } + } + + // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) + var childG2ps = GetBaseG2ps(); + if (childG2ps != null && childG2ps.Any()) { + g2ps.AddRange(childG2ps); + } + + return new G2pFallbacks(g2ps.ToArray()); + } + + /// + /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, + /// and replaces phonemes with replacement table + /// Is Running Async! + /// + /// + /// + protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { + var replacements = GetDictionaryPhonemesReplacement(); + foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { + if (line.StartsWith(";;;")) { + continue; + } + var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); + if (parts.Length != 2) { + continue; + } + string key = parts[0].ToLowerInvariant(); + var values = GetDictionaryWordPhonemes(parts[1]).Select( + n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); + lock (builder) { + builder.AddEntry(key, values); + }; + }; + } + + #region helpers + + /// + /// May be used if you have different logic for short and long notes + /// + /// + /// + protected bool IsShort(Syllable syllable) { + return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; + } + 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 + /// + /// + /// + /// + protected bool HasOto(string alias, int tone) { + return singer.TryGetMappedOto(alias, tone, out _); + } + + /// + /// Can be used for different variants, like exhales [v R], [v -] etc + /// + /// phonemes container to add to + /// to map alias + /// target phoneme variants + /// returns true if added any + protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { + foreach (var phoneme in targetPhonemes) { + if (HasOto(phoneme, tone)) { + sourcePhonemes.Add(phoneme); + return true; + } + } + 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 + /// + /// + /// + protected bool CanMakeAliasExtension(Syllable syllable) { + return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; + } + + /// + /// if current syllable is VV and previous one is from the same pitch, + /// you may wan't to just extend the previous alias. Put the phoneme as null fot that + /// + /// + /// + /// + protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { + if (singer.Subbanks.Count == 1) { + return true; + } + if (tone1 == tone2) { + return true; + } + var toneSets = singer.Subbanks.Select(n => n.toneSet); + foreach (var toneSet in toneSets) { + if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { + return true; + } + if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { + return false; + } + } + return true; + } + + protected virtual string YamlFileName => null; + protected virtual byte[] YamlTemplate => null; + protected virtual string YamlVersion => null; + + protected string[] vowels = Array.Empty(); + protected string[] consonants = Array.Empty(); + protected string[] tails = "-,R".Split(','); + protected string[] affricate = Array.Empty(); + protected string[] fricative = Array.Empty(); + protected string[] aspirate = Array.Empty(); + protected string[] semivowel = Array.Empty(); + protected string[] liquid = Array.Empty(); + protected string[] nasal = Array.Empty(); + protected string[] stop = Array.Empty(); + protected string[] tap = Array.Empty(); + + protected Dictionary dictionaryReplacements = new Dictionary(); + protected Dictionary PhonemeOverrides = new Dictionary(); + protected Dictionary yamlFallbacks = new Dictionary(); + protected List consExceptions = new List(); + + protected Dictionary diphthongTails = new Dictionary(); + protected Dictionary diphthongSplits = new Dictionary(); + + 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(); + public Timings[] timings { get; set; } = Array.Empty(); + public DiphthongData[] diphthongs { get; set; } = Array.Empty(); + public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); + + public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } + public struct Fallbacks { public string from { get; set; } public string to { get; set; } } + public struct Timings { public string symbol { get; set; } public double value { get; set; } } + public struct DiphthongData { public string from { get; set; } public string to { get; set; } } + public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } + } + + public class Replacement { + public object from { get; set; } + public object to { get; set; } + public string where { get; set; } = "inside"; + + public List FromList { + get { + if (from is string s) return new List { s }; + if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + + public List ToList { + get { + if (to is string s) return new List { s }; + if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); + return new List(); + } + } + } + + protected List mergingReplacements = new List(); + protected List splittingReplacements = new List(); + + protected virtual bool IsGroupKeyword(string rulePhoneme) { + // 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 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; + } + } + } + + bool inBaseGroup = false; + switch (baseGroup) { + case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; + case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; + case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; + case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; + case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; + case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; + case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; + case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; + case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; + case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; + } + + if (!inBaseGroup) return false; + + if (cleanRule.Contains("!")) { + string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; + if (excluded.Split(',').Contains(actualPhoneme)) return false; + } + + if (cleanRule.Contains("=")) { + string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; + if (!restricted.Split(',').Contains(actualPhoneme)) return false; + } + + return true; + } + + protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; + + List finalPhonemes = new List(); + int idx = 0; + + var validRules = mergingReplacements.Concat(splittingReplacements) + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + var validSplits = splittingReplacements + .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); + + while (idx < inputPhonemes.Count) { + bool replaced = false; + + foreach (var rule in validRules) { + 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.Count; j++) { + string rulePh = fromArray[j]; + string actualPh = inputPhonemes[idx + j]; + + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh)) { + if (IsGroupMatch(rulePh, actualPh)) { + if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); + captures[baseRulePh].Add(actualPh); + } else { + match = false; break; + } + } else if (rulePh != actualPh) { + match = false; break; + } + } + + if (match) { + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + 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++) { + // 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.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) { + List fromArray = rule.FromList; + if (fromArray == null || fromArray.Count != 1) continue; + + string rulePh = fromArray[0]; + string cleanRulePh = rulePh.Trim('(', ')'); + string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; + + if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { + + List toArray = rule.ToList; + + if (toArray != null && toArray.Count > 0) { + foreach(string toPh in toArray) { + 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; + } + } + } + if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } else if (!replaced) { + finalPhonemes.Add(inputPhonemes[idx]); + idx++; + } + } + return finalPhonemes; + } + + private Syllable ApplyBoundaryReplacements(Syllable syllable) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); + bool hasV = !string.IsNullOrEmpty(syllable.v); + + 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) || !hasPrevV; + List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); + + string newPrevV = ""; + string newV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + if (hasV && finalPhonemes.Count > 0) { + var vowelsList = GetVowels(); + int vIndex = finalPhonemes.Count - 1; + + for (int i = finalPhonemes.Count - 1; i >= 0; i--) { + if (vowelsList.Contains(finalPhonemes[i])) { + vIndex = i; + break; + } + } + newV = finalPhonemes[vIndex]; + for (int i = 0; i < vIndex; i++) { + newCc.Add(finalPhonemes[i]); + } + } else { + newCc.AddRange(finalPhonemes); + } + } + + syllable.prevV = newPrevV; + syllable.cc = newCc.ToArray(); + syllable.v = newV; + return syllable; + } + + private Ending ApplyBoundaryReplacements(Ending ending) { + if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; + + List currentPhonemes = new List(); + bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); + currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); + if (ending.cc != null) currentPhonemes.AddRange(ending.cc); + + List finalPhonemes = ApplyReplacements(currentPhonemes, true); + + string newPrevV = ""; + List newCc = new List(); + + if (finalPhonemes.Count > 0) { + string firstPh = finalPhonemes[0]; + if (firstPh == "null") { + newPrevV = ""; + finalPhonemes.RemoveAt(0); + } else { + newPrevV = firstPh; + finalPhonemes.RemoveAt(0); + } + newCc.AddRange(finalPhonemes); + } + + ending.prevV = newPrevV; + ending.cc = newCc.ToArray(); + return ending; + } + + #endregion + + #region private + + private Result MakeForcedAliasResult(Note note) { + return MakeSimpleResult(note.lyric.Substring(1)); + } + + protected void ReadDictionaryAndInit() { + var dictionaryName = GetDictionaryName(); + if (dictionaryName == null) { + return; + } + dictionaries[GetType()] = null; + if (Testing) { + ReadDictionary(dictionaryName); + Init(); + return; + } + OnAsyncInitStarted(); + Task.Run(() => { + ReadDictionary(dictionaryName); + Init(); + OnAsyncInitFinished(); + }); + } + + private void ReadDictionary(string dictionaryName) { + try { + var phonemeSymbols = new Dictionary(); + + foreach (var vowel in GetVowels()) { + phonemeSymbols[vowel] = true; + } + foreach (var consonant in GetConsonants()) { + phonemeSymbols[consonant] = false; + } + + var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); + var safeDict = new Dictionary(); + + foreach (var kvp in childDict) { + safeDict[kvp.Key] = kvp.Value; + safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' + safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' + } + + dictionaries[GetType()] = new G2pRemapper( + LoadBaseDictionary(), + phonemeSymbols, + safeDict); + + } catch (Exception ex) { + Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); + } + } + + private string[] ApplyExtensions(string[] symbols, Note[] notes) { + var newSymbols = new List(); + var vowelIds = ExtractVowels(symbols); + if (vowelIds.Count == 0) { + // no syllables or all consonants, the last phoneme will be interpreted as vowel + vowelIds.Add(symbols.Length - 1); + } + var lastVowelI = 0; + newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); + for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { + if (!IsSyllableVowelExtensionNote(notes[i])) { + var prevVowel = vowelIds[lastVowelI]; + lastVowelI++; + var vowel = vowelIds[lastVowelI]; + newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); + } else { + newSymbols.Add(symbols[vowelIds[lastVowelI]]); + } + } + newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); + return newSymbols.ToArray(); + } + + private List ExtractVowels(string[] symbols) { + var vowelIds = new List(); + var vowels = GetVowels(); + for (var i = 0; i < symbols.Length; i++) { + if (vowels.Contains(symbols[i])) { + vowelIds.Add(i); + } + } + 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]; + + 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) { + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + stretch *= overrideRatio; + } else { + baseLengthMs = GetTransitionBasicLengthMs(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; + } + } + } + + 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] = new Phoneme { + phoneme = validatedAlias, + index = globalIndex + }; + + if (i == 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) { + // YAML Override active: Use the multiplier and bypass NoGap entirely + baseLengthMs = GetTransitionBasicLengthMsByConstant(); + phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); + } else { + // Default behavior + baseLengthMs = GetTransitionBasicLengthMsByOto(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 + phonemes[phonemeI].position = MsToTick(baseLengthMs); + } + } + } else { + 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]; + } + } else { + // 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 - 1 : phonemeSymbols.Count - 1, containerLength); + } + + private string ValidateAliasIfNeeded(string alias, int tone) { + if (HasOto(alias, tone)) { + return alias; + } + return ValidateAlias(alias); + } + + private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { + var offset = 0; + var lengthModifier = 1.0; + + if (containerLengthTick > 0) { + var allTransitionsLengthTick = phonemes.Sum(n => n.position); + + // 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--) { + if (phonemes[i].phoneme == null) continue; + var finalLengthTick = (int)(phonemes[i].position * lengthModifier); + phonemes[i].position = startPosition - finalLengthTick - offset; + offset += finalLengthTick; + } + + return phonemes.Where(n => n.phoneme != null).ToArray(); + } + + #endregion + } +} diff --git a/OpenUtau.sln b/OpenUtau.sln index f61f987f6..19be4efae 100644 --- a/OpenUtau.sln +++ b/OpenUtau.sln @@ -26,7 +26,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau", "OpenUtau\OpenUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau.Plugin.Builtin", "OpenUtau.Plugin.Builtin\OpenUtau.Plugin.Builtin.csproj", "{D24740E1-CC43-4226-A573-59C013FE50A2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericSyllableBasedPhonemizer", "..\OU-Generic-Syllable-Based-Phonemizer\GenericSyllableBasedPhonemizer\GenericSyllableBasedPhonemizer.csproj", "{F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnXSampaPhonemizerPlus", "EnXSampaPhonemizerPlus\EnXSampaPhonemizerPlus.csproj", "{9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JA_CVVX", "JapaneseCVVXPhonemizer\JA_CVVX.csproj", "{83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericSyllableBasedPhonemizer", "GenericSyllableBasedPhonemizer\GenericSyllableBasedPhonemizer.csproj", "{61E62C45-6CE9-0275-ED91-C8F059C7A40C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,10 +54,18 @@ Global {D24740E1-CC43-4226-A573-59C013FE50A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.Build.0 = Release|Any CPU - {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F8A67035-AA14-CD7F-DF7C-1ED574ABCA7B}.Release|Any CPU.Build.0 = Release|Any CPU + {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Release|Any CPU.Build.0 = Release|Any CPU + {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Release|Any CPU.Build.0 = Release|Any CPU + {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fe45e64e6805baa4dd48851c198f1952f894fd1c Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:16:45 -0700 Subject: [PATCH 04/13] Transferred phonemizer code from previous (external) project. Also imported template and updated Resources.resx to match. Still need to make the phonemizer itself function correctly. --- .../Data/Resources.Designer.cs | 84 +- OpenUtau.Plugin.Builtin/Data/Resources.resx | 3 + .../Data/generic-syllabic.template.yaml | 50 + .../GenericSyllabicPhonemizer.cs | 1053 +++++++++++++++++ 4 files changed, 1130 insertions(+), 60 deletions(-) create mode 100644 OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml create mode 100644 OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs diff --git a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs index 3d975ed51..b9e034884 100644 --- a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs +++ b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,46 +11,32 @@ namespace OpenUtau.Plugin.Builtin.Data { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenUtau.Plugin.Builtin.Data.Resources", typeof(Resources).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("OpenUtau.Plugin.Builtin.Data.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -60,9 +45,6 @@ internal Resources() { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] arpasing_template { get { object obj = ResourceManager.GetObject("arpasing_template", resourceCulture); @@ -70,9 +52,6 @@ internal static byte[] arpasing_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] de_vccv_template { get { object obj = ResourceManager.GetObject("de_vccv_template", resourceCulture); @@ -80,39 +59,27 @@ internal static byte[] de_vccv_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] en_cPv_template { + internal static byte[] envccv_template { get { - object obj = ResourceManager.GetObject("en_cPv_template", resourceCulture); + object obj = ResourceManager.GetObject("envccv_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] en_xsampa_template { + internal static byte[] en_cPv_template { get { - object obj = ResourceManager.GetObject("en_xsampa_template", resourceCulture); + object obj = ResourceManager.GetObject("en_cPv_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] envccv_template { + internal static byte[] en_xsampa_template { get { - object obj = ResourceManager.GetObject("envccv_template", resourceCulture); + object obj = ResourceManager.GetObject("en_xsampa_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] filipino_template { get { object obj = ResourceManager.GetObject("filipino_template", resourceCulture); @@ -120,9 +87,6 @@ internal static byte[] filipino_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] german_template { get { object obj = ResourceManager.GetObject("german_template", resourceCulture); @@ -130,9 +94,6 @@ internal static byte[] german_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] kocv_template { get { object obj = ResourceManager.GetObject("kocv_template", resourceCulture); @@ -140,14 +101,17 @@ internal static byte[] kocv_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] njokis_template { get { object obj = ResourceManager.GetObject("njokis_template", resourceCulture); return ((byte[])(obj)); } } + + internal static string generic_syllabic_template { + get { + return ResourceManager.GetString("generic_syllabic_template", resourceCulture); + } + } } } diff --git a/OpenUtau.Plugin.Builtin/Data/Resources.resx b/OpenUtau.Plugin.Builtin/Data/Resources.resx index 48a0cbc95..46c6031d5 100644 --- a/OpenUtau.Plugin.Builtin/Data/Resources.resx +++ b/OpenUtau.Plugin.Builtin/Data/Resources.resx @@ -145,4 +145,7 @@ njokis.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + generic-syllablic.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml new file mode 100644 index 000000000..1fb0f9b32 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml @@ -0,0 +1,50 @@ +%YAML 1.2 +--- +symbols: + - {symbol: aa, type: vowel} + - {symbol: ae, type: vowel} + - {symbol: ah, type: vowel} + - {symbol: ao, type: vowel} + - {symbol: aw, type: vowel} + - {symbol: ax, type: vowel} + - {symbol: ay, type: vowel} + - {symbol: b, type: stop} + - {symbol: ch, type: affricate} + - {symbol: d, type: stop} + - {symbol: dh, type: fricative} + - {symbol: dr, type: fricative} + - {symbol: dx, type: tap} + - {symbol: eh, type: vowel} + - {symbol: er, type: vowel} + - {symbol: ey, type: vowel} + - {symbol: f, type: fricative} + - {symbol: g, type: stop} + - {symbol: hh, type: aspirate} + - {symbol: ih, type: vowel} + - {symbol: iy, type: vowel} + - {symbol: jh, type: affricate} + - {symbol: k, type: stop} + - {symbol: l, type: liquid} + - {symbol: m, type: nasal} + - {symbol: n, type: nasal} + - {symbol: ng, type: nasal} + - {symbol: ow, type: vowel} + - {symbol: oy, type: vowel} + - {symbol: p, type: stop} + - {symbol: q, type: stop} + - {symbol: r, type: liquid} + - {symbol: s, type: fricative} + - {symbol: sh, type: fricative} + - {symbol: t, type: stop} + - {symbol: th, type: fricative} + - {symbol: tr, type: fricative} + - {symbol: uh, type: vowel} + - {symbol: uw, type: vowel} + - {symbol: v, type: fricative} + - {symbol: w, type: semivowel} + - {symbol: y, type: semivowel} + - {symbol: z, type: fricative} + - {symbol: zh, type: fricative} +entries: + - grapheme: openutau + phonemes: [ow, p, eh, n, w, uw, t, ah, w, uw] diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs new file mode 100644 index 000000000..8e49c707d --- /dev/null +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -0,0 +1,1053 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Classic; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; +using OpenUtau.Plugin.Builtin; + +namespace GenericSyllabicPhonemizer { + [Phonemizer("Generic Syllable-Based Phonemizer", "GEN SYL", "HeadHunter4621")] + // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. + // The default template contains EN ARPAbet phonemes, but is meant to be modified to fit your voicebank (ex. adding romaji/X-SAMPA) + public class GenericSyllabicPhonemizer : SyllableBasedPhonemizer { + protected override string YamlFileName => "generic-syllabic-phonemizer.yaml"; + protected override byte[] YamlTemplate => Data.Resources.generic_syllabic_template; + + public GenericSyllabicPhonemizer() { + + this.vowels = Array.Empty(); + this.consonants = Array.Empty(); + } + + protected override string[] GetVowels() => vowels; + protected override string[] GetConsonants() => consonants; + protected override string GetDictionaryName() => ""; + + List consExceptions = new List(); + + string[] diphthongs = Array.Empty(); + + // For banks with missing vowels + private readonly Dictionary missingVphonemes = "ax=a".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingVPhonemes = false; + private bool isYamlFallbacks = false; + + + // For banks with missing custom consonants + private readonly Dictionary missingCphonemes = "N=n".Split(',') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + private bool isMissingCPhonemes = false; + private bool cPV_FallBack = false; + + private readonly Dictionary vvDiphthongExceptions = + new Dictionary() { + {"aw","a"}, + {"ow","o"}, + {"iw","i"}, + {"ay","a"}, + {"ey","e"}, + {"oy","o"}, + {"uy","u"}, + {"ew","e"}, + }; + + private readonly Dictionary vvExceptions = + new Dictionary() { + {"aw","w"}, + {"ow","w"}, + {"iw","w"}, + {"ay","y"}, + {"ey","y"}, + {"oy","y"}, + {"uy","y"}, + {"ew","w"}, + }; + + private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; + private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; + private static readonly string[] FinalConsonants = { "w", "y", "r", "l", "m", "n", "ng" }; + + + protected override string[] GetSymbols(Note note) { + string[] original = base.GetSymbols(note); + return note.lyric.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + + //if (original == null || original.Length == 0) { + // string lyric = note.lyric.ToLowerInvariant(); + // List fallbackSplit = new List(); + // string[] vowels = GetVowels(); + // string[] consonants = GetConsonants(); + + // // Handle apostrophes at the start or end + // bool hasLeadingApostrophe = lyric.StartsWith("'"); + // bool hasTrailingApostrophe = lyric.EndsWith("'"); + + // if (hasLeadingApostrophe) { + // lyric = lyric.Substring(1); + // } + // if (hasTrailingApostrophe && lyric.Length > 0) { + // lyric = lyric.Substring(0, lyric.Length - 1); + // } + + // int ii = 0; + // while (ii < lyric.Length) { + // string match = null; + // foreach (var cons in consonants.OrderByDescending(c => c.Length)) { + // if (lyric.Substring(ii).StartsWith(cons)) { + // match = cons; + // break; + // } + // } + // if (match == null) { + // foreach (var vow in vowels.OrderByDescending(v => v.Length)) { + // if (lyric.Substring(ii).StartsWith(vow)) { + // match = vow; + // break; + // } + // } + // } + // if (match != null) { + // fallbackSplit.Add(match); + // ii += match.Length; + // } else { + // fallbackSplit.Add(lyric[ii].ToString()); + // ii++; + // } + // } + // // Add "q" at the beginning or end if needed + // if (hasLeadingApostrophe) { + // fallbackSplit.Insert(0, "q"); + // } + // if (hasTrailingApostrophe) { + // fallbackSplit.Add("q"); + + // } + // original = fallbackSplit.ToArray(); + //} + + //List modified = new List(original); + //List finalPhonemes = new List(); + //finalPhonemes = new List(modified); + //List finalProcessedPhonemes = new List(); + //IEnumerable phonemes; + //phonemes = finalPhonemes; + //foreach (string s in phonemes) { + // switch (s) { + // default: + // finalProcessedPhonemes.Add(s); + // break; + // } + //} + //return finalProcessedPhonemes.ToArray(); + } + + protected override IG2p LoadBaseDictionary() { + var g2ps = new List(); + // LOAD DICTIONARY FROM FOLDER + string path = Path.Combine(PluginDir, YamlFileName); + if (!File.Exists(path)) { + Directory.CreateDirectory(PluginDir); + File.WriteAllBytes(path, YamlTemplate); + } + // LOAD DICTIONARY FROM SINGER FOLDER + if (singer != null && singer.Found && singer.Loaded) { + string file = Path.Combine(singer.Location, YamlFileName); + if (File.Exists(file)) { + try { + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); + } catch (Exception e) { + Log.Error(e, $"Failed to load {file}"); + } + } + } + g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); + //g2ps.Add(new ArpabetPlusG2p()); + return new G2pFallbacks(g2ps.ToArray()); + } + public override void SetSinger(USinger singer) { + base.SetSinger(singer); + + if (this.singer != null && this.singer.Loaded) { + + consExceptions.Clear(); + if (stop != null) consExceptions.AddRange(stop); + if (tap != null) consExceptions.AddRange(tap); + + consExceptions = consExceptions.Distinct().ToList(); + } + } + + // prioritize yaml replacements over dictionary replacements + private string ReplacePhoneme(string phoneme, int tone) { + // If the original phoneme has an OTO, use it directly. + 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; + } + protected override List ProcessSyllable(Syllable syllable) { + syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; + var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); + var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; + string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string v = ReplacePhoneme(syllable.v, syllable.vowelTone); + List vowels = new List { v }; + string basePhoneme; + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); + int prevWordConsonantsCount = syllable.prevWordConsonantsCount; + + // Check for missing vowel phonemes + foreach (var entry in missingVphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { + isMissingVPhonemes = true; + break; + } + } + + // Check for missing consonant phonemes + foreach (var entry in missingCphonemes) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isMissingCPhonemes = true; + break; + } + } + + foreach (var entry in yamlFallbacks) { + if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { + isYamlFallbacks = true; + break; + } + } + + // STARTING V + if (syllable.IsStartingV) { + basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); + } + // [V V] or [V C][C V]/[V] + else if (syllable.IsVV) { + if (!CanMakeAliasExtension(syllable)) { + basePhoneme = $"{prevV} {v}"; + if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && vvExceptions.ContainsKey(prevV) && prevV != v) { + // VV IS NOT PRESENT, CHECKS VVEXCEPTIONS LOGIC + //var vc = $"{prevV}{vvExceptions[prevV]}"; + var vc = AliasFormat($"{vvExceptions[prevV]}", "vcEx", syllable.vowelTone, prevV); + TryAddPhoneme(phonemes, syllable.vowelTone, vc); + basePhoneme = AliasFormat($"{vvExceptions[prevV]} {v}", "dynMid", syllable.vowelTone, ""); + } else { + { + if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV} {v}"; + } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { + basePhoneme = $"{prevV}{v}"; + } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { + basePhoneme = v; + } else { + basePhoneme = AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{prevV} -", "dynMid", syllable.vowelTone, "")); + } + } + } + // EXTEND AS [V] + } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone) || missingVphonemes.ContainsKey(prevV)) { + basePhoneme = v; + } else if (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone) && vvDiphthongExceptions.ContainsKey(prevV)) { + basePhoneme = $"{vvDiphthongExceptions[prevV]} {vvDiphthongExceptions[prevV]}"; + } else { + // PREVIOUS ALIAS WILL EXTEND as [V V] + basePhoneme = null; + } + + // [- CV/C V] or [- C][CV/C V] + } else if (syllable.IsStartingCVWithOneConsonant) { + var rcv = $"- {cc[0]} {v}"; + var rcv1 = $"- {cc[0]}{v}"; + var crv = $"{cc[0]} {v}"; + /// - CV + if (HasOto(rcv, syllable.vowelTone) && HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) && HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); + /// CV + } else if (HasOto(crv, syllable.vowelTone) && HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } else { + basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } + // [CCV/CC V] or [C C] + [CV/C V] + } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { + // TRY [- CCV]/[- CC V] or [- CC][CCV]/[CC V] or [- C][C C][C V]/[CV] + var rccv = $"- {string.Join("", cc)} {v}"; + var rccv1 = $"- {string.Join("", cc)}{v}"; + var crv = $"{cc.Last()} {v}"; + var crv1 = $"{cc.Last()}{v}"; + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + /// - CCV + if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); + lastC = 0; + } else { + /// CCV and CV + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // TRY RCC [- CC] + for (var i = cc.Length; i > 1; i--) { + if (!ccvException.Contains(cc[0])) { + if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""))) { + firstC = i - 1; + } + } + break; + } + // [- C] + if (phonemes.Count == 0) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + } + // try [CC V] or [CCV] + var cv = $"{cc.Last()}{v}"; + for (var i = firstC; i < cc.Length - 1; i++) { + /// CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + /// C-Last + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + } + } else { // VCV + var vcv = $"{prevV} {cc[0]}{v}"; + var vcv2 = $"{prevV}{cc[0]}{v}"; + var vcvEnd = $"{prevV}{cc[0]} {v}"; + var vccv = $"{prevV} {string.Join("", cc)}{v}"; + var vccv2 = $"{prevV} {string.Join("", cc)}"; + var vccv3 = $"{prevV}{string.Join("", cc)}"; + var crv = $"{cc.Last()} {v}"; + // Use regular VCV if the current word starts with one consonant and the previous word ends with none + if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv; + } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv2, syllable.vowelTone) && HasOto(ValidateAlias(vcv2), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { + basePhoneme = vcv2; + // Use end VCV if current word does not start with a consonant but the previous word does end with one + } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) && HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { + basePhoneme = vcvEnd; + // Use regular VCV if end VCV does not exist + } else if (syllable.IsVCVWithOneConsonant && (!HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone)) && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone))) { + basePhoneme = vcv; + // VCV with multiple consonants, only for current word onset and null previous word ending + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) && HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { + basePhoneme = vccv; + lastC = 0; + } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv3, syllable.vowelTone) && HasOto(ValidateAlias(vccv3), syllable.vowelTone))) { + basePhoneme = AliasFormat($"{prevV} {string.Join("", cc)}{v}", "dynMid", syllable.vowelTone, ""); + lastC = 0; + } else { + var cv = cc.Last() + v; + basePhoneme = cv; + if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { + basePhoneme = crv; + } + // try [CC V] or [CCV] + for (var i = firstC; i < cc.Length - 1; i++) { + var ccv = $"{string.Join("", cc)} {v}"; + var ccv1 = $"{string.Join("", cc)}{v}"; + /// CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + break; + } + /// C-Last + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } else { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + } + } + // try [V C], [V CC], [VC C], [V -][- C] + for (var i = lastC + 1; i >= 0; i--) { + var vr = $"{prevV} -"; + //var vc_c = $"{prevV}{string.Join(" ", cc.Take(2))}-"; + //var vc_c2 = $"{prevV}{string.Join(" ", cc.Take(2))}_"; + var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; + var vc = $"{prevV} {cc[0]}"; + // Boolean Triggers + bool CCV = false; + if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { + if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { + CCV = true; + } + } + + if (i == 0 && (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) && !HasOto(vc, syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, vr, ValidateAlias(vr)); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); + break; + } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { + TryAddPhoneme(phonemes, syllable.tone, vcc, ValidateAlias(vcc)); + firstC = 1; + break; + /*} else if (HasOto(vc_c, syllable.tone) && HasOto(ValidateAlias(vc_c), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} -", "dynMid", syllable.vowelTone, "")); + firstC = 1; + break; + } else if (HasOto(vc_c2, syllable.tone) && HasOto(ValidateAlias(vc_c2), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} _", "dynMid", syllable.vowelTone, "")); + firstC = 1; + break;*/ + } else if (cPV_FallBack && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + break; + } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { + TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); + break; + } else { + continue; + } + } + } + } + + for (var i = firstC; i < lastC; i++) { + var ccv = $"{string.Join("", cc.Skip(i + 1))} {v}"; + var ccv1 = $"{string.Join("", cc.Skip(i + 1))}{v}"; + var cc1 = $"{string.Join(" ", cc.Skip(i))}"; + var lcv = $"{cc.Last()} {v}"; + var cv = $"{cc.Last()}{v}"; + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CC FALLBACKS + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + var c1 = cc[i]; + var c2 = cc[i + 1]; + bool c1IsException = consExceptions.Contains(c1); + bool c2IsException = consExceptions.Contains(c2); + + // Scenario 1: Both are NOT exceptions + if (!c1IsException && !c2IsException) { + // [C1 -] [- C2] + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); + } + // Scenario 2: C1 is an exception, C2 is NOT + else if (c1IsException && !c2IsException) { + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + } + // Scenario 3: C1 is NOT an exception, C2 is + else if (!c1IsException && c2IsException) { + cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); + } + // Scenario 4: Both are exceptions + else if (c1IsException && c2IsException) { + cc1 = ""; + } + } + // CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // [C1 C2C3] + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + } + // CV + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + + if (i + 1 < lastC) { + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CC FALLBACKS + if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { + var c1 = cc[i]; + var c2 = cc[i + 1]; + bool c1IsException = consExceptions.Contains(c1); + bool c2IsException = consExceptions.Contains(c2); + + // Scenario 1: Both are NOT exceptions + if (!c1IsException && !c2IsException) { + // [C1 -] [- C2] + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); + } + // Scenario 2: C1 is an exception, C2 is NOT + else if (c1IsException && !c2IsException) { + cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); + } + // Scenario 3: C1 is NOT an exception, C2 is + else if (!c1IsException && c2IsException) { + cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); + } + // Scenario 4: Both are exceptions + else if (c1IsException && c2IsException) { + cc1 = ""; + } + } + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + // CCV + if (CurrentWordCc.Length >= 2) { + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); + lastC = i; + } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + } + // [C1 C2C3] + if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { + cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; + } + // CV + } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { + basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); + // [C1 C2] + if (!HasOto(cc1, syllable.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + // C+V + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { + cPV_FallBack = true; + basePhoneme = v; + cc1 = ValidateAlias(cc1); + } + + if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { + // like [V C1] [C1 C2] [C2 C3] [C3 ..] + TryAddPhoneme(phonemes, syllable.vowelTone, cc1); + } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { + // like [V C1] [C1 C2] [C2 ..] + if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { + i++; + } + } else { + // like [V C1] [C1] [C2 ..] + TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); + } + } else { + TryAddPhoneme(phonemes, syllable.tone, cc1); + } + } + + phonemes.Add(basePhoneme); + return phonemes; + } + + protected override List ProcessEnding(Ending ending) { + string prevV = ReplacePhoneme(ending.prevV, ending.tone); + string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); + string v = ReplacePhoneme(ending.prevV, ending.tone); + var phonemes = new List(); + var lastC = cc.Length - 1; + var firstC = 0; + string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; + + if (ending.IsEndingV) { + var vR = $"{v} {t}"; + var vR2 = $"{v}{t}"; + if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{v}", "ending", ending.tone, "", t))); + } + } else if (ending.IsEndingVCWithOneConsonant) { + var vc = $"{v} {cc[0]}"; + var vcr = $"{v} {cc[0]}{t}"; + var vcr2 = $"{v}{cc[0]} {t}"; + var vcr3 = $"{v}{cc[0]}{t}"; + if (!RomajiException.Contains(cc[0])) { + if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ValidateAlias(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t))); + } else if (!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone) || !HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || !HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc); + if (vc.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + } + } else { + TryAddPhoneme(phonemes, ending.tone, vc); + if (vc.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); + } + } + } + } else { + for (var i = lastC; i >= 0; i--) { + var vr = $"{v} {t}"; + var vr1 = $"{v} R"; + var vr2 = $"{v}{t}"; + var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; + var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} {t}"; + var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; + var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; + var vc = $"{v} {cc[0]}"; + if (!RomajiException.Contains(cc[0])) { + if (i == 0) { + if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t)); + } + break; + } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc); + firstC = 1; + break; + } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc2); + firstC = 1; + break; + } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc3); + if (vcc3.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + } + } + firstC = 1; + break; + } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { + TryAddPhoneme(phonemes, ending.tone, vcc4); + if (vcc4.EndsWith(cc.Last()) && lastC == 1) { + if (consonants.Contains(cc.Last())) { + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); + } + } + firstC = 1; + break; + } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) + || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) + || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) + || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { + TryAddPhoneme(phonemes, ending.tone, vc); + break; + } else { + TryAddPhoneme(phonemes, ending.tone, vc); + break; + } + } + } + for (var i = firstC; i < lastC; i++) { + var cc1 = $"{cc[i]} {cc[i + 1]}"; + if (i < cc.Length - 2) { + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc2, ending.tone)) { + cc2 = ValidateAlias(cc2); + } + + if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { + // [C1 -] [- C2] + cc2 = AliasFormat($"{cc[i + 2]}", "cc_inB", ending.tone, ""); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { + // like [C1 C2][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc1); + } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { + // like [C1 C2-][C3 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i]); + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { + // like [C1 C2-][C3 ...] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + i++; + } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + // [C1 -] [- C2] + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "", t)); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); + i++; + } else { + // like [C1][C2 ...] + TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); + TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); + i++; + } + } else { + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = $"{cc[i]} {cc[i + 1]}"; + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + // [C1 -] [- C2] + if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + cc1 = AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, ""); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "", t)); + } + if (!HasOto(cc1, ending.tone)) { + cc1 = ValidateAlias(cc1); + } + if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { + // like [C1 C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]} {t}", ValidateAlias($"{cc[i]} {cc[i + 1]} {t}"))) { + // like [C1 C2 -] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}{t}", ValidateAlias($"{cc[i]}{cc[i + 1]}{t}"))) { + // like [C1C2-] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]} {t}", ValidateAlias($"{cc[i]}{cc[i + 1]} {t}"))) { + // like [C1C2 -] + i++; + } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { + // like [C1 C2][C2 -] + TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); + i++; + } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { + // [C1 -] [- C2] + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); + TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 2]}", "cc_endB", ending.tone, "", t)); + i++; + } + } + } + } + return phonemes; + } + 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[] { "" } }, + { "dynEnd", new string[] { "" } }, + { "startingV", new string[] { "-", "- ", "_", "" } }, + { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, + { "vvExtend", new string[] { "", "_", "-", "- " } }, + { "cv", new string[] { "-", "", "- ", "_" } }, + { "cvStart", new string[] { "-", "- ", "_" } }, + { "ending", new string[] { $" {t}", $"{t}"} }, + { "ending_mix", new string[] { $"{t}", $" {t}", "--" } }, + { "cc", new string[] { "", "-", "- ", "_" } }, + { "cc_start", new string[] { "- ", "-", "_" } }, + { "cc_end", new string[] { $" {t}", $"{t}", "" } }, + { "cc_inB", new string[] { "_", "-", "- " } }, + { "cc_endB", new string[] { "_", $"{t}", $" {t}" } }, + { "cc_mix", new string[] { $" {t}", " R", $"{t}", "", "_", $"{t} ", $"{t}" } }, + { "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 = ""; + // 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; + } + + // 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" + $"-{consonant}{vowel}", // "-CV" + $"-{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)) { + return variation; + } + } + } + + 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]; + vowel = parts[1]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{consonant}{vowel}", // "CV" + $"{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)) { + return variation1; + } + } + } + + 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]; + vowel = parts[0]; + } else { + consonant = alias; + } + var dynamicVariations1 = new List { + $"{vowel}{consonant} -", // "VC -" + $"{vowel} {consonant}-", // "V C-" + $"{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)) { + return 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) { + aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; + counter++; + } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { + aliasFormat = $"{alias}{format}"; + } else { + aliasFormat = $"{format}{alias}"; + } + // Check if the formatted alias exists + if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { + return aliasFormat; + } + } + return alias; + } + + protected override string ValidateAlias(string alias) { + + // VALIDATE ALIAS DEPENDING ON METHOD + if (isMissingVPhonemes) { + foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isMissingCPhonemes) { + foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + if (isYamlFallbacks) { + foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { + alias = alias.Replace(fb.Key, fb.Value); + } + } + + 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; + } + } + + foreach (var v in vowels) { + if (alias.EndsWith("-")) { + return base.GetTransitionBasicLengthMs() * 0.5; + } + } + + // 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; + } + } + + 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; + } + } + + return base.GetTransitionBasicLengthMs() * transitionMultiplier; + } + } +} From ad22d61aa855071efd5ac8d7d8d21739a6e2ecd3 Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:46:23 -0700 Subject: [PATCH 05/13] Removed old project files from other external phonemizer development --- .../GenericSyllabicPhonemizer.cs | 21 ++++++++----------- OpenUtau.sln | 12 ----------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs index 8e49c707d..b662aebdb 100644 --- a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -1,21 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using Classic; -using OpenUtau.Api; +using OpenUtau.Api; using OpenUtau.Core.Ustx; using OpenUtau.Plugin.Builtin; +using Serilog; +using System.Text.RegularExpressions; -namespace GenericSyllabicPhonemizer { +namespace GenericSyllableBasedPhonemizer { [Phonemizer("Generic Syllable-Based Phonemizer", "GEN SYL", "HeadHunter4621")] // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. - // The default template contains EN ARPAbet phonemes, but is meant to be modified to fit your voicebank (ex. adding romaji/X-SAMPA) - public class GenericSyllabicPhonemizer : SyllableBasedPhonemizer { - protected override string YamlFileName => "generic-syllabic-phonemizer.yaml"; - protected override byte[] YamlTemplate => Data.Resources.generic_syllabic_template; + // The default template contains X-SAMPA phonemes but can (and should) be modified to fit your voicebank. + public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { + protected override string YamlFileName => "generic-phonemizer.yaml"; + protected override byte[] YamlTemplate => Data.Resources.generic_template; - public GenericSyllabicPhonemizer() { + public GenericSyllablePhonemizer() { this.vowels = Array.Empty(); this.consonants = Array.Empty(); diff --git a/OpenUtau.sln b/OpenUtau.sln index 19be4efae..8ceffd6c4 100644 --- a/OpenUtau.sln +++ b/OpenUtau.sln @@ -26,10 +26,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau", "OpenUtau\OpenUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau.Plugin.Builtin", "OpenUtau.Plugin.Builtin\OpenUtau.Plugin.Builtin.csproj", "{D24740E1-CC43-4226-A573-59C013FE50A2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnXSampaPhonemizerPlus", "EnXSampaPhonemizerPlus\EnXSampaPhonemizerPlus.csproj", "{9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JA_CVVX", "JapaneseCVVXPhonemizer\JA_CVVX.csproj", "{83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericSyllableBasedPhonemizer", "GenericSyllableBasedPhonemizer\GenericSyllableBasedPhonemizer.csproj", "{61E62C45-6CE9-0275-ED91-C8F059C7A40C}" EndProject Global @@ -54,14 +50,6 @@ Global {D24740E1-CC43-4226-A573-59C013FE50A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.Build.0 = Release|Any CPU - {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A4789AA-8E15-4A22-9C97-D3DDAFA1D690}.Release|Any CPU.Build.0 = Release|Any CPU - {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83A802BC-EF4F-9A58-B58F-5B930BFAFC5A}.Release|Any CPU.Build.0 = Release|Any CPU {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.Build.0 = Debug|Any CPU {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Release|Any CPU.ActiveCfg = Release|Any CPU From 13f50c24ab9170eb8d5d07831af4de3846d6dfd8 Mon Sep 17 00:00:00 2001 From: Kona <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:47:17 -0700 Subject: [PATCH 06/13] Delete EnXSampaPhonemizerPlus directory --- .../Data/Resources.Designer.cs | 73 - EnXSampaPhonemizerPlus/Data/Resources.resx | 124 -- .../Data/en-xsampa.template.yaml | 177 -- EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs | 955 --------- .../EnXSampaPhonemizerPlus.csproj | 28 - .../SyllableBasedPhonemizer.cs | 1780 ----------------- 6 files changed, 3137 deletions(-) delete mode 100644 EnXSampaPhonemizerPlus/Data/Resources.Designer.cs delete mode 100644 EnXSampaPhonemizerPlus/Data/Resources.resx delete mode 100644 EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml delete mode 100644 EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs delete mode 100644 EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj delete mode 100644 EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs diff --git a/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs b/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs deleted file mode 100644 index e61c361b9..000000000 --- a/EnXSampaPhonemizerPlus/Data/Resources.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace EnXSampaPhonemizerPlus.Data { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EnXSampaPhonemizerPlus.Data.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] en_xsampa_template { - get { - object obj = ResourceManager.GetObject("en-xsampa.template", resourceCulture); - return ((byte[])(obj)); - } - } - } -} diff --git a/EnXSampaPhonemizerPlus/Data/Resources.resx b/EnXSampaPhonemizerPlus/Data/Resources.resx deleted file mode 100644 index f5e319aaf..000000000 --- a/EnXSampaPhonemizerPlus/Data/Resources.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - en-xsampa.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml b/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml deleted file mode 100644 index fbb2f4a8b..000000000 --- a/EnXSampaPhonemizerPlus/Data/en-xsampa.template.yaml +++ /dev/null @@ -1,177 +0,0 @@ -%YAML 1.2 ---- -symbols: -#Delta-style (X-SAMPA) symbols - - {symbol: a, type: vowel} - - {symbol: e, type: vowel} - - {symbol: i, type: vowel} - - {symbol: o, type: vowel} - - {symbol: u, type: vowel} - - {symbol: A, type: vowel} - - {symbol: E, type: vowel} - - {symbol: I, type: vowel} - - {symbol: O, type: vowel} - - {symbol: U, type: vowel} - - {symbol: '{', type: vowel} - - {symbol: V, type: vowel} - - {symbol: 3, type: vowel} - - {symbol: aI, type: vowel} - - {symbol: eI, type: vowel} - - {symbol: OI, type: vowel} - - {symbol: aU, type: vowel} - - {symbol: oU, type: vowel} - - {symbol: tS, type: affricate} - - {symbol: D, type: fricative} - - {symbol: h, type: aspirate} - - {symbol: dZ, type: affricate} - - {symbol: N, type: nasal} - - {symbol: S, type: fricative} - - {symbol: T, type: fricative} - - {symbol: j, type: semivowel} - - {symbol: Z, type: fricative} -#Arpasing - - {symbol: aa, type: vowel} - - {symbol: ae, type: vowel} - - {symbol: ah, type: vowel} - - {symbol: ao, type: vowel} - - {symbol: aw, type: vowel} - - {symbol: ax, type: vowel} - - {symbol: ay, type: vowel} - - {symbol: b, type: stop} - - {symbol: ch, type: affricate} - - {symbol: d, type: stop} - - {symbol: dh, type: fricative} - - {symbol: dr, type: fricative} - - {symbol: dx, type: tap} - - {symbol: eh, type: vowel} - - {symbol: er, type: vowel} - - {symbol: ey, type: vowel} - - {symbol: f, type: fricative} - - {symbol: g, type: stop} - - {symbol: hh, type: aspirate} - - {symbol: ih, type: vowel} - - {symbol: iy, type: vowel} - - {symbol: jh, type: affricate} - - {symbol: k, type: stop} - - {symbol: l, type: liquid} - - {symbol: m, type: nasal} - - {symbol: n, type: nasal} - - {symbol: ng, type: nasal} - - {symbol: ow, type: vowel} - - {symbol: oy, type: vowel} - - {symbol: p, type: stop} - - {symbol: q, type: stop} - - {symbol: r, type: liquid} - - {symbol: s, type: fricative} - - {symbol: sh, type: fricative} - - {symbol: t, type: stop} - - {symbol: th, type: fricative} - - {symbol: tr, type: fricative} - - {symbol: uh, type: vowel} - - {symbol: uw, type: vowel} - - {symbol: v, type: fricative} - - {symbol: w, type: semivowel} - - {symbol: y, type: semivowel} - - {symbol: z, type: fricative} - - {symbol: zh, type: fricative} - #bonus symbols - - {symbol: e@, type: vowel} - - {symbol: e@n, type: vowel} - - {symbol: e@m, type: vowel} - - {symbol: eN, type: vowel} - - {symbol: IN, type: vowel} - - {symbol: Ar, type: vowel} - - {symbol: Qr, type: vowel} - - {symbol: Er, type: vowel} - - {symbol: Ir, type: vowel} - - {symbol: Or, type: vowel} - - {symbol: Ur, type: vowel} - - {symbol: ir, type: vowel} - - {symbol: ur, type: vowel} - - {symbol: aIr, type: vowel} - - {symbol: aUr, type: vowel} - - {symbol: A@, type: vowel} - - {symbol: Q@, type: vowel} - - {symbol: E@, type: vowel} - - {symbol: I@, type: vowel} - - {symbol: O@, type: vowel} - - {symbol: U@, type: vowel} - - {symbol: i@, type: vowel} - - {symbol: u@, type: vowel} - - {symbol: aI@, type: vowel} - - {symbol: aU@, type: vowel} - - {symbol: Q, type: vowel} - - {symbol: 1, type: vowel} - - {symbol: Ol, type: vowel} - - {symbol: aUn, type: vowel} - - {symbol: '@r', type: vowel} - - {symbol: '@l', type: vowel} - - {symbol: '@m', type: vowel} - - {symbol: '@n', type: vowel} - - {symbol: '@N', type: vowel} - - {symbol: '@', type: vowel} - - {symbol: 'I\', type: vowel} - - {symbol: M, type: vowel} - - {symbol: 'U\', type: vowel} - - {symbol: Y, type: vowel} - - {symbol: '@\', type: vowel} - - {symbol: '@`', type: vowel} - - {symbol: 3`, type: vowel} - - {symbol: A`, type: vowel} - - {symbol: Q`, type: vowel} - - {symbol: E`, type: vowel} - - {symbol: I`, type: vowel} - - {symbol: O`, type: vowel} - - {symbol: U`, type: vowel} - - {symbol: i`, type: vowel} - - {symbol: u`, type: vowel} - - {symbol: aI`, type: vowel} - - {symbol: aU`, type: vowel} - - {symbol: '}', type: vowel} - - {symbol: 2, type: vowel} - - {symbol: '3\', type: vowel} - - {symbol: 6, type: vowel} - - {symbol: 7, type: vowel} - - {symbol: 8, type: vowel} - - {symbol: 9, type: vowel} - - {symbol: '&', type: vowel} - - {symbol: '{~', type: vowel} - - {symbol: I~, type: vowel} - - {symbol: aU~, type: vowel} - - {symbol: VI, type: vowel} - - {symbol: VU, type: vowel} - - {symbol: '@U', type: vowel} - - {symbol: 'i:', type: vowel} - - {symbol: 'u:', type: vowel} - - {symbol: 'O:', type: vowel} - - {symbol: e@0, type: vowel} - - {symbol: ai, type: vowel} - - {symbol: ei, type: vowel} - - {symbol: Oi, type: vowel} - - {symbol: au, type: vowel} - - {symbol: ou, type: vowel} - - {symbol: Ou, type: vowel} - - {symbol: '@u', type: vowel} - - {symbol: 4, type: tap} - - {symbol: W, type: fricative} - - {symbol: 't_}', type: stop} - - {symbol: E~, type: vowel} - - {symbol: e~, type: vowel} - - {symbol: 3r, type: vowel} - - {symbol: ar, type: vowel} - - {symbol: or, type: vowel} - - {symbol: '{l', type: vowel} - - {symbol: Al, type: vowel} - - {symbol: al, type: vowel} - - {symbol: El, type: vowel} - - {symbol: Il, type: vowel} - - {symbol: ul, type: vowel} - - {symbol: Ul, type: vowel} - - {symbol: mm, type: vowel} - - {symbol: nn, type: vowel} - - {symbol: ll, type: vowel} - - {symbol: NN, type: vowel} - -entries: - - grapheme: openutau - phonemes: [ow, p, ah, n, uw, t, aw] diff --git a/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs b/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs deleted file mode 100644 index 1fad19809..000000000 --- a/EnXSampaPhonemizerPlus/EnXSampaPhonemizer.cs +++ /dev/null @@ -1,955 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.IO; -using System.Linq; -using Classic; -using OpenUtau.Api; -using OpenUtau.Classic; -using OpenUtau.Core.G2p; -using OpenUtau.Core.Ustx; -using Serilog; -using YamlDotNet.Core.Tokens; -using System.Text.RegularExpressions; - -namespace OpenUtau.Plugin.Builtin { - /// - /// General English phonemizer for X-SAMPA voicebanks. - /// The difference between this phonemizer and the Teto English phonemizer is that this one was made to support all X-SAMPA-based banks. - /// However, it should be fully compatible with Kasane Teto's English voicebank regardless. - ///

- ///

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

- ///

- /// Read more about X-SAMPA here. - ///
- [Phonemizer("English X-SAMPA + phonemizer", "EN X-SAMPA +", "HeadHunter4621", language: "EN")] - public class EnXSampaPhonemizer : SyllableBasedPhonemizer { - protected override string YamlFileName => "en-xsampa.yaml"; - protected override byte[] YamlTemplate => EnXSampaPhonemizerPlus.Data.Resources.en_xsampa_template; - public EnXSampaPhonemizer() { - this.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(','); - this.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(','); - this.dictionaryReplacements = ("aa=A;ae={;ah=V;ao=O;aw=aU;ax=@;ay=aI;" + - "b=b;ch=tS;d=d;dh=D;" + "dx=4;eh=E;el=@l;em=@m;en=@n;eng=@N;er=3;ey=eI;f=f;g=g;hh=h;ih=I;iy=i;jh=dZ;k=k;l=l;m=m;n=n;ng=N;ow=oU;oy=OI;" + - "p=p;q=・;r=r;s=s;sh=S;t=t;th=T;" + "uh=U;uw=u;v=v;w=w;" + "y=j;z=z;zh=Z").Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - } - - private bool isYamlFallbacks = false; - protected override string[] GetVowels() => vowels; - protected override string[] GetConsonants() => consonants; - protected override string GetDictionaryName() => ""; - - // For banks aliased with VOCALOID-style phonemes - private readonly Dictionary vocaSampa = "A=Q;E=e;i=i:;u=u:;O=O:;3=@r;oU=@U;Ar=Q@;Qr=Q@;Er=e@;er=e@;Ir=I@;ir=I@;i:r=I@;Or=O@;O:r=O@;Ur=U@;ur=U@;u:r=U@".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isVocaSampa = false; - - // For banks with slightly fewer vowels - private readonly Dictionary simpleDelta = "E=e;V=@;o=O".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isSimpleDelta = false; - - // For banks with slightly fewer vowels - private readonly Dictionary CanadianRaising = "VI=aI;VU=aU".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isMissingCanadianRaising = false; - - // For banks with only minimal vowels - private readonly Dictionary miniDelta = "I=i;U=u".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isMiniDelta = false; - - // For Japanese-English combined banks. - // NOTE: Rather rudimentary by default; a custom dictionary is recommended. - private readonly Dictionary enPlusJa = "j=y;dZ=j;r=r\\;tS=ch".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isEnPlusJa = false; - - // For banks encoded in "true"/more accurate X-SAMPA. - private readonly Dictionary trueXSampa = "r=r\\;3=@`".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isTrueXSampa = false; - - // For banks using Salem's reclist. - private readonly Dictionary salemList = "3=@r;Ar=ar;aIr=ar;aUr=ar;Or=or;aIl=al".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isSalemList = false; - - // Velar nasal fallback - private readonly Dictionary velarNasalFallback = "N g=n g;N k=n k".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isVelarNasalFallback = false; - - // For Kasane Teto's missing sample - private readonly Dictionary tetoException = "V=@".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isTetoException = false; - - // For dark L vowels - private readonly Dictionary darkLVowel = "@l=@5,ul=u5,Ol=O5,Al=A5,El=E5,Il=I5,il=i5,".Split(';') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - - private bool isDarkLVowel = false; - - private readonly Dictionary vvExceptions = - new Dictionary() { - {"aI","j"}, - {"eI","j"}, - {"OI","j"}, - {"aU","w"}, - {"oU","w"}, - {"VI","j"}, - {"VU","w"}, - {"@U","w"}, - {"ai","j"}, - {"Oi","j"}, - {"au","w"}, - {"ou","w"}, - {"Ou","w"}, - {"@u","w"}, - {"3", "r"} - }; - private readonly Dictionary Delta5vvExceptions = - new Dictionary() { - {"aI","I"}, - {"eI","I"}, - {"OI","I"}, - {"aU","U"}, - {"oU","U"}, - {"VI","I"}, - {"VU","U"}, - {"@U","U"}, - {"ai","i"}, - {"Oi","i"}, - {"au","u"}, - {"ou","u"}, - {"Ou","u"}, - {"@u","u"}, - {"3", "r"} - }; - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Load dictionary from plugin folder. - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - - // Load dictionary from singer folder. - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(new ArpabetPlusG2p()); - return new G2pFallbacks(g2ps.ToArray()); - } - - protected override string[] GetSymbols(Note note) { - string[] original = base.GetSymbols(note); - if (original == null) { - return null; - } - List finalProcessedPhonemes = new List(); - - // 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", }; - string[] affricates = new[] { "dZ", "tS" }; - - foreach (string s in original) { - if (diphthongs.Contains(s) && !HasOto($"- {s}", note.tone) && !HasOto(s, note.tone) && !HasOto(ValidateAlias($"- {s}"), note.tone) && !HasOto(ValidateAlias(s), note.tone)) { - finalProcessedPhonemes.AddRange(new string[] { s[0].ToString(), s[1] + '^'.ToString() }); - } else if (affricates.Contains(s) && !HasOto($"{s}A", note.tone) && !HasOto($"{s} A", note.tone) && !HasOto($"{s}Q", note.tone) && !HasOto($"{s} Q", note.tone)) { - finalProcessedPhonemes.AddRange(new string[] { s[0].ToString(), s[1].ToString() }); - } else { - finalProcessedPhonemes.Add(s); - } - } - return finalProcessedPhonemes.ToArray(); - } - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - 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; - } - - protected override List ProcessSyllable(Syllable syllable) { - syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; - var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); - var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; - string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string v = ReplacePhoneme(syllable.v, syllable.vowelTone); - List vowels = new List { v }; - string basePhoneme; - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - - - var rv = $"- {v}"; - - // Switch between phonetic systems, depending on certain aliases in the bank - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - if (HasOto($"- i:", syllable.tone) || HasOto($"i:", syllable.tone) || (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone))) { - isVocaSampa = true; - } - - if (!HasOto($"- VI", syllable.tone) || HasOto($"VI", syllable.tone) || (!HasOto($"- VU", syllable.tone) && !HasOto($"VU", syllable.tone))) { - isMissingCanadianRaising = true; - } - - if (!HasOto($"- V", syllable.vowelTone) && !HasOto($"V", syllable.vowelTone)) { - isSimpleDelta = true; - } - - if (!HasOto($"- bV", syllable.vowelTone) && !HasOto($"bV", syllable.vowelTone)) { - isTetoException = true; - } - - if ((!HasOto($"- I", syllable.vowelTone) && !HasOto($"I", syllable.vowelTone)) || (!HasOto($"- U", syllable.vowelTone) && !HasOto($"U", syllable.vowelTone))) { - isMiniDelta = true; - } - - if (HasOto("あ", syllable.vowelTone) || HasOto("- あ", syllable.vowelTone)) { - isEnPlusJa = true; - } - - if (HasOto($"{prevV} r\\", syllable.tone)) { - isTrueXSampa = true; - } - - if (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone) && !HasOto($"- @`", syllable.tone) && !HasOto($"@`", syllable.tone)) { - isSalemList = true; - } - - if ((!HasOto($"N g", syllable.tone) || !HasOto($"N g-", syllable.tone)) && (!HasOto($"N k", syllable.tone) || !HasOto($"N k-", syllable.tone))) { - isVelarNasalFallback = true; - } - - if (HasOto("@5", syllable.vowelTone) || HasOto("u5", syllable.vowelTone) || HasOto("O5", syllable.vowelTone) || HasOto("A5", syllable.vowelTone) || HasOto("E5", syllable.vowelTone) || HasOto("I5", syllable.vowelTone) || HasOto("i5", syllable.vowelTone) || HasOto("- @5", syllable.vowelTone) || HasOto("- u5", syllable.vowelTone) || HasOto("- O5", syllable.vowelTone) || HasOto("- A5", syllable.vowelTone) || HasOto("- E5", syllable.vowelTone) || HasOto("- I5", syllable.vowelTone) || HasOto("- i5", syllable.vowelTone)) { - isDarkLVowel = true; - } - - if (syllable.IsStartingV) { - if (HasOto(rv, syllable.vowelTone) || HasOto(ValidateAlias(rv), syllable.vowelTone)) { - basePhoneme = rv; - } else { - basePhoneme = v; - } - } else if (syllable.IsVV) { - if (!CanMakeAliasExtension(syllable)) { - var vv = $"{prevV} {v}"; - basePhoneme = vv; - if (!HasOto(vv, syllable.vowelTone) && !HasOto(ValidateAlias(vv), syllable.vowelTone) && (vvExceptions.ContainsKey(prevV) && prevV != v || Delta5vvExceptions.ContainsKey(prevV) && prevV != v)) { - // VV splits to [V C][CV] or [V][V] - var delta5vc = $"{Delta5vvExceptions[prevV]}"; - bool CV = false; - if ((!HasOto(delta5vc, syllable.vowelTone) && !HasOto(ValidateAlias(delta5vc), syllable.vowelTone))) { - delta5vc = $"{prevV} {vvExceptions[prevV]}"; - CV = true; - } - phonemes.Add(delta5vc); - // if delta5 vc is not available, turn v to cv - var cv = $"{vvExceptions[prevV]}{v}"; - basePhoneme = v; - if (CV && (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone))) { - basePhoneme = cv; - } - } else { - // VV to V - if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV} {v}"; - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { - basePhoneme = v; - } - } - // EXTEND AS [V] - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone)) { - basePhoneme = v; - } else { - // PREVIOUS ALIAS WILL EXTEND as [V V] - basePhoneme = null; - } - } else if (syllable.IsStartingCVWithOneConsonant) { - // TODO: move to config -CV or -C CV - var rcv = $"- {cc[0]}{v}"; - var crv = $"{cc[0]} {v}"; - var cv = $"{cc[0]}{v}"; - if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone)) { - basePhoneme = rcv; - } else if ((!HasOto(rcv, syllable.vowelTone) && !HasOto(ValidateAlias(rcv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { - basePhoneme = crv; - TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); - } else { - basePhoneme = cv; - TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); - } - } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { - // try RCCV - var rccv = $"- {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - var ucv = $"_{cc.Last()}{v}"; - if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) { - basePhoneme = rccv; - lastC = 0; - } else { - if (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) { - basePhoneme = ucv; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = crv; - } else { - basePhoneme = $"{cc.Last()}{v}"; - } - // try RCC - for (var i = cc.Length; i > 1; i--) { - if (TryAddPhoneme(phonemes, syllable.tone, $"- {string.Join("", cc.Take(i))}", ValidateAlias($"- {string.Join("", cc.Take(i))}"))) { - firstC = i - 1; - break; - } - } - if (phonemes.Count == 0) { - TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias($"- {cc[0]}")); - } - // try CCV - for (var i = firstC; i < cc.Length - 1; i++) { - var ccv = string.Join("", cc.Skip(i)) + v; - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { - basePhoneme = ccv; - lastC = i; - break; - } else { - if (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) { - basePhoneme = ucv; - break; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = crv; - break; - } else { - basePhoneme = $"{cc.Last()}{v}"; - break; - } - } - } - } - } else { // VCV - var vcv = $"{prevV} {cc[0]}{v}"; - var vcvEnd = $"{prevV}{cc[0]} {v}"; - var vccv = $"{prevV} {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - // Use regular VCV if the current word starts with one consonant and the previous word ends with none - if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv; - // Use end VCV if current word does not start with a consonant but the previous word does end with one - } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) || HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { - basePhoneme = vcvEnd; - // Use regular VCV if end VCV does not exist - } else if (syllable.IsVCVWithOneConsonant && !HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone) && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone))) { - basePhoneme = vcv; - // VCV with multiple consonants, only for current word onset and null previous word ending - // TODO: multi-VCV for words ending with one or more consonants? - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { - basePhoneme = vccv; - lastC = 0; - } else { - var cv = cc.Last() + v; - basePhoneme = cv; - if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { - basePhoneme = crv; - } - // try CCV - if ((cc.Length - firstC > 1) && CurrentWordCc.Length >= 2) { - for (var i = firstC; i < cc.Length; i++) { - var ccv = $"{string.Join("", cc.Skip(0))}{v}"; - var rccv = $"- {string.Join("", cc.Skip(0))}{v}"; - var ucv = $"_{cc.Last()}{v}"; - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { - lastC = 0; - basePhoneme = ccv; - break; - } else if (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone) && (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone))) { - lastC = 0; - basePhoneme = rccv; - break; - } else if ((!HasOto(rccv, syllable.vowelTone) || !HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone))) { - basePhoneme = ucv; - break; - } - } - } - FoundMatch:; - // try vcc - for (var i = lastC + 1; i >= 0; i--) { - var vr = $"{prevV} -"; - var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; - var vcc2 = $"{prevV}{string.Join(" ", cc.Take(2))}"; - var vc = $"{prevV} {cc[0]}"; - if (i == 0) { - if (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) { - phonemes.Add(vr); - } - } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && !affricate.Contains(string.Join("", cc.Take(2)))) { - phonemes.Add(vcc); - firstC = 1; - break; - } else if (HasOto(vcc2, syllable.tone) || HasOto(ValidateAlias(vcc2), syllable.tone)) { - phonemes.Add(vcc2); - firstC = 1; - break; - } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { - phonemes.Add(vc); - break; - } else { - continue; - } - } - } - } - - for (var i = firstC; i < lastC; i++) { - // we could use some CCV, so lastC is used - // we could use -CC so firstC is used - var cc1 = $"{cc[i]} {cc[i + 1]}"; - var ccv = string.Join("", cc.Skip(i + 1)) + v; - var rccv = $"- {string.Join("", cc.Skip(i + 1)) + v}"; - var ucv = $"_{cc.Last()}{v}"; - var crv = $"{cc.Last()} {v}"; - var cv = $"{cc.Last()}{v}"; - // Use [C1C2...] when current word starts with 2 consonants or more - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { - cc1 = $"{string.Join("", cc.Skip(i))}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // Use [C1C2] when current word has 2 consonants or more and [C1C2C3...] does not exist - if (!HasOto(cc1, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc1)) { - cc1 = $"{cc[i]}{cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // Use [C1 C2] when either [C1C2] does not exist, or current word has 1 consonant or less and previous word has 1 consonant or more - if ((!HasOto(cc1, syllable.tone)) || PreviousWordCc.Contains(cc1)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // Use UCV if it exists - if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && !cc1.Contains($"{cc[i]} {cc[i + 1]}")) { - basePhoneme = ucv; - } - if (i + 1 < lastC) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - // Use [C2C3...] when current word starts with 2 consonants or more - if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc2)) { - cc2 = $"{string.Join("", cc.Skip(i))}"; - } - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - // 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]}"; - } - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - // Use [C2 C3] when either [C2C3] does not exist, or current word has 1 consonant or less and previous word has 2 consonants or more - if ((!HasOto(cc2, syllable.tone)) || PreviousWordCc.Contains(cc2)) { - cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - } - if (!HasOto(cc2, syllable.tone)) { - cc2 = ValidateAlias(cc2); - } - //Use CCV if it exists - if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { - lastC = i; - basePhoneme = ccv; - // Use RCCV if it exists - } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { - lastC = i; - basePhoneme = rccv; - // Use _CV if it exists - } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && HasOto(cc2, syllable.vowelTone) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}") && CurrentWordCc.Length >= 2) { - basePhoneme = ucv; - // Use spaced CV if it exists - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = crv; - // Use normal CV - } else { - basePhoneme = cv; - } - if (HasOto(cc1, syllable.tone) && HasOto(cc2, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { - // like [V C1] [C1 C2] [C2 C3] [C3 ..] - phonemes.Add(cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { - // like [V C1] [C1 C2] [C2 ..] - if (cc1.Contains($"{string.Join("", cc.Skip(i))}")) { - i++; - } - } else { - // singular cc - if ((PreviousWordCc.Contains(cc1) == CurrentWordCc.Contains(cc1)) && !affricate.Contains(cc1)) { - cc1 = ValidateAlias(cc1); - } else { - TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); - } - } - } else { - // like [V C1] [C1 C2] [C2 ..] or like [V C1] [C1 -] [C3 ..] - TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); - } - } - - phonemes.Add(basePhoneme); - return phonemes; - } - - protected override List ProcessEnding(Ending ending) { - string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); - string v = ReplacePhoneme(ending.prevV, ending.tone); - string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; - var phonemes = new List(); - var vr = $"{v} {t}"; - - var lastC = cc.Length - 1; - var firstC = 0; - - if (ending.IsEndingV) { - TryAddPhoneme(phonemes, ending.tone, vr, ValidateAlias(vr)); - } else if (ending.IsEndingVCWithOneConsonant) { - var vc = $"{v} {cc[0]}"; - var vcr = $"{v} {cc[0]}{t}"; - var vcr2 = $"{v}{cc[0]} {t}"; - if (HasOto(vcr, ending.tone) || HasOto(ValidateAlias(vcr), ending.tone)) { - phonemes.Add(vcr); - } else if ((!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone)) && (HasOto(vcr2, ending.tone) || HasOto(ValidateAlias(vcr2), ending.tone))) { - phonemes.Add(vcr2); - } else { - phonemes.Add(vc); - if (affricate.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} {t}", cc[0]); - } else { - TryAddPhoneme(phonemes, ending.tone, $"{cc[0]} {t}", ValidateAlias($"{cc[0]} {t}")); - } - } - } else { - for (var i = lastC; i >= 0; i--) { - var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; - var vcc2 = $"{v}{string.Join(" ", cc.Take(2))}{t}"; - var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; - var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; - var vc = $"{v} {cc[0]}"; - if (i == 0) { - if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone)) { - phonemes.Add(vr); - } - } else if ((HasOto(vcc, ending.tone) || HasOto(ValidateAlias(vcc), ending.tone)) && lastC == 1) { - phonemes.Add(vcc); - firstC = 1; - break; - } else if ((HasOto(vcc2, ending.tone) || HasOto(ValidateAlias(vcc2), ending.tone)) && lastC == 1) { - phonemes.Add(vcc2); - firstC = 1; - break; - } else if (HasOto(vcc3, ending.tone) || HasOto(ValidateAlias(vcc3), ending.tone)) { - phonemes.Add(vcc3); - if (vcc3.EndsWith(cc.Last()) && lastC == 1) { - if (affricate.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}"), cc.Last(), ValidateAlias(cc.Last())); - } else { - TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}")); - } - } - firstC = 1; - break; - } else if (HasOto(vcc4, ending.tone) || HasOto(ValidateAlias(vcc4), ending.tone)) { - phonemes.Add(vcc4); - if (vcc4.EndsWith(cc.Last()) && lastC == 1) { - if (affricate.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}"), cc.Last(), ValidateAlias(cc.Last())); - } else { - TryAddPhoneme(phonemes, ending.tone, $"{cc.Last()} {t}", ValidateAlias($"{cc.Last()} {t}")); - } - } - firstC = 1; - break; - } else { - phonemes.Add(vc); - break; - } - } - - - for (var i = firstC; i < lastC; i++) { - // all CCs except the first one are /C1C2/, the last one is /C1 C2-/ - // but if there is no /C1C2/, we try /C1 C2-/, vise versa for the last one - var cc1 = $"{cc[i]} {cc[i + 1]}"; - if (i < cc.Length - 2) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = $"{cc[i]}{cc[i + 1]}"; - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc2, ending.tone)) { - cc2 = ValidateAlias(cc2); - } - if (!HasOto(cc2, ending.tone)) { - cc2 = $"{cc[i + 1]}{cc[i + 2]}"; - } - if (!HasOto(cc2, ending.tone)) { - cc2 = ValidateAlias(cc2); - } - if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{cc[i + 2]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{cc[i + 2]}{t}"))) { - // like [C1 C2-][C3 ...] - i++; - } else if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { - // like [C1 C2][C2 ...] - phonemes.Add(cc1); - } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { - // like [C1 C2-][C3 ...] - phonemes.Add(cc[i]); - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { - // like [C1 C2-][C3 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { - // like [C1C2][C2 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - i++; - } else { - // like [C1][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); - i++; - } - } else { - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = $"{cc[i]}{cc[i + 1]}"; - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { - // like [C1 C2-] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - // like [C1 C2][C2 -] - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}", ValidateAlias($"{cc[i]}{cc[i + 1]}"))) { - // like [C1C2][C2 -] - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } else { - // like [C1][C2 -] - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } - } - } - } - return phonemes; - } - - protected override string ValidateAlias(string alias) { - // Validate alias depending on method - if (isVocaSampa) { - foreach (var syllable in vocaSampa) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isSimpleDelta) { - foreach (var syllable in simpleDelta) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isMiniDelta) { - foreach (var syllable in miniDelta) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isEnPlusJa) { - foreach (var syllable in enPlusJa) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isTrueXSampa) { - foreach (var syllable in trueXSampa) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isSalemList) { - foreach (var syllable in salemList) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isVelarNasalFallback) { - foreach (var syllable in velarNasalFallback) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isTetoException) { - foreach (var syllable in tetoException) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isMissingCanadianRaising) { - foreach (var syllable in CanadianRaising) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isDarkLVowel) { - foreach (var syllable in darkLVowel) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - if (isYamlFallbacks) { - foreach (var syllable in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(syllable.Key, syllable.Value); - } - } - - // Split diphthongs adjuster - if (alias.Contains("U^")) { - alias = alias.Replace("U^", "U"); - } - if (alias.Contains("I^")) { - alias = alias.Replace("I^", "I"); - } - if (alias.Contains("u^")) { - alias = alias.Replace("u^", "u"); - } - if (alias.Contains("i^")) { - alias = alias.Replace("i^", "i"); - } - - // Other validations - if (!alias.Contains("@r") && !alias.Contains("3r")) { - foreach (var consonant1 in new[] { "r ", "r\\ ", }) { - foreach (var consonant2 in consonants) { - alias = alias.Replace(consonant1 + consonant2, "3 " + consonant2); - } - } - } - - 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; - } - } - - // 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; - } - } - - 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; - } - } - - return base.GetTransitionBasicLengthMs() * transitionMultiplier; - } - } -} diff --git a/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj b/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj deleted file mode 100644 index 50805590d..000000000 --- a/EnXSampaPhonemizerPlus/EnXSampaPhonemizerPlus.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - diff --git a/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs b/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs deleted file mode 100644 index a2852dcfe..000000000 --- a/EnXSampaPhonemizerPlus/SyllableBasedPhonemizer.cs +++ /dev/null @@ -1,1780 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using OpenUtau.Api; -using OpenUtau.Core.Ustx; -using System.Linq; -using System.IO; -using Serilog; -using System.Threading.Tasks; -using static OpenUtau.Api.Phonemizer; -using System.Collections; - -namespace OpenUtau.Plugin.Builtin { - /// - /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; - /// - /// - Supports dictionary; - /// - Automatically align phonemes to notes; - /// - Supports syllable extension; - /// - Automatically calculates transition phonemes length, with constants by default, - /// but there is a pre-created function to use Oto value; - /// - The transition length is scaled based on Tempo and note length. - /// - /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". - /// - /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], - /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". - /// - /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". - /// - /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, - /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). - /// - public abstract class SyllableBasedPhonemizer : Phonemizer { - - /// - /// Syllable is [V] [C..] [V] - /// - protected struct Syllable { - /// - /// vowel from previous syllable for VC - /// - public string prevV; - /// - /// CCs, may be empty - /// - public string[] cc; - /// - /// "base" note. May not actually be vowel, if only consonants way provided - /// - public string v; - /// - /// Start position for vowel. All VC CC goes before this position - /// - public int position; - /// - /// previous note duration, i.e. this is container for VC and CC notes - /// - public int duration; - /// - /// Tone for VC and CC - /// - public int tone; - /// - /// Other phoneme attributes for VC and CC - /// - public PhonemeAttributes[] attr; - /// - /// tone for base "vowel" phoneme - /// - public int vowelTone; - /// - /// Other phoneme attributes for base "vowel" phoneme - /// - public PhonemeAttributes[] vowelAttr; - - /// - /// 0 if no consonants are taken from previous word; - /// 1 means first one is taken from previous word, etc. - /// - public int prevWordConsonantsCount; - - /// - /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. - /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. - /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible - /// - public bool canAliasBeExtended; - - // helpers - public bool IsStartingV => prevV == "" && cc.Length == 0; - public bool IsVV => prevV != "" && cc.Length == 0; - - public bool IsStartingCV => prevV == "" && cc.Length > 0; - public bool IsVCV => prevV != "" && cc.Length > 0; - - public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; - public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; - - public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; - public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; - - public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); - public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; - } - } - - protected struct Ending { - /// - /// vowel from the last syllable to make VC - /// - public string prevV; - /// - /// actuall CC at the ending - /// - public string[] cc; - /// - /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) - /// - public string tail; - public bool HasTail => !string.IsNullOrEmpty(tail); - /// - /// last note position + duration, all phonemes must be less than this - /// - public int position; - /// - /// last syllable length, max container for all VC CC C- - /// - public int duration; - /// - /// the tone from last syllable, for all ending phonemes - /// - public int tone; - /// - /// Other phoneme attributes from last syllable - /// - public PhonemeAttributes[] attr; - - // helpers - public bool IsEndingV => cc.Length == 0; - public bool IsEndingVC => cc.Length > 0; - public bool IsEndingVCWithOneConsonant => cc.Length == 1; - public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; - } - } - - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { - error = ""; - var mainNote = notes[0]; - if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return MakeForcedAliasResult(mainNote); - } - if (hasDictionary && isDictionaryLoading) { - return MakeSimpleResult(""); - } - - runtimeGlides.Clear(); - - var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); - if (syllables == null) { - return HandleError(); - } - - var phonemes = new List(); - int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI - - foreach (var syllable in syllables) { - var modifiedSyllable = ApplyBoundaryReplacements(syllable); - - if (tails.Contains(modifiedSyllable.v)) { - var ending = new Ending { - prevV = modifiedSyllable.prevV, - cc = modifiedSyllable.cc, - tail = modifiedSyllable.v, - position = modifiedSyllable.position, - duration = modifiedSyllable.duration, - tone = modifiedSyllable.tone, - attr = modifiedSyllable.attr - }; - - var endingPhonemes = ProcessEnding(ending); - - if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); - globalPhonemeIndex += endingPhonemes.Count; - } - continue; - } - - var syllablePhonemes = ProcessSyllable(modifiedSyllable); - var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); - int currentSyllablePhonemeCount = syllablePhonemes.Count; - - var basePhoneme = madePhonemes.LastOrDefault(); - string baseAlias = basePhoneme.phoneme ?? ""; - if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || - vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { - - string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); - if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { - int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); - madePhonemes.Add(new Phoneme { - phoneme = sustainData.sustain, - position = basePhoneme.position + offsetTicks, - index = globalPhonemeIndex + currentSyllablePhonemeCount - }); - currentSyllablePhonemeCount++; - } - } - phonemes.AddRange(madePhonemes); - globalPhonemeIndex += currentSyllablePhonemeCount; - } - - if (!nextNeighbour.HasValue) { - var tryEnding = MakeEnding(notes); - if (tryEnding.HasValue) { - var ending = tryEnding.Value; - - if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { - ending.tail = nextNeighbour.Value.lyric; - } - - var modifiedEnding = ApplyBoundaryReplacements(ending); - var endingPhonemes = ProcessEnding(modifiedEnding); - - if (endingPhonemes != null) { - 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 = finalPhonemes - }; - } - - protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { - int noteIndex = 0; - for (int i = 0; i < phonemes.Count; i++) { - var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var phoneme = phonemes[i]; - while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { - noteIndex++; - } - - 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); - validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); - - phoneme.phoneme = validatedAlias; - } else { - phoneme.phoneme = null; - phoneme.position = 0; - } - - phonemes[i] = phoneme; - } - return phonemes.ToArray(); - } - - private Result HandleError() { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = error - } - } - }; - } - - protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = - new YamlDotNet.Serialization.DeserializerBuilder() - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); - - public override void SetSinger(USinger singer) { - if (this.singer != singer) { - this.singer = singer; - dictionaries.Clear(); - - if (this.singer == null || !this.singer.Loaded) { - return; - } - - if (string.IsNullOrEmpty(YamlFileName)) { - if (backupVowels != null) this.vowels = backupVowels; - else this.vowels = GetVowels(); - - if (backupConsonants != null) this.consonants = backupConsonants; - else this.consonants = GetConsonants(); - - if (backupDictionaryReplacements != null) { - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) { - dictionaryReplacements[kvp.Key] = kvp.Value; - } - } - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - return; - } - - // file paths - string globalFile = Path.Combine(PluginDir, YamlFileName); - string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) - ? Path.Combine(singer.Location, YamlFileName) - : null; - - // Local helper function to update and backup YAML files safely - void UpdateYamlIfNeeded(string filePath, bool isGlobal) { - if (string.IsNullOrEmpty(filePath)) return; - - bool shouldWriteTemplate = false; - bool shouldBackupOldFile = false; - string currentVersion = "unknown"; - - if (File.Exists(filePath)) { - if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { - try { - var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); - currentVersion = checkData?.version?.Trim() ?? ""; - - // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion - if (string.IsNullOrEmpty(currentVersion)) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } else if (Version.TryParse(currentVersion, out Version currV) && - Version.TryParse(YamlVersion, out Version targetV)) { - if (currV < targetV) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { - // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } catch (Exception ex) { - Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); - return; - } - } - } else if (isGlobal && YamlTemplate != null) { - shouldWriteTemplate = true; - } - - if (shouldBackupOldFile && File.Exists(filePath)) { - try { - // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml - string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; - string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); - - if (File.Exists(backupFile)) File.Delete(backupFile); - File.Move(filePath, backupFile); - Log.Information($"Old {YamlFileName} backed up to {backupFile}"); - } catch (Exception e) { - Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); - return; - } - } - - if (shouldWriteTemplate) { - try { - File.WriteAllBytes(filePath, YamlTemplate); - Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); - } catch (Exception e) { - Log.Error(e, $"Failed to write template to {filePath}"); - } - } - } - - UpdateYamlIfNeeded(globalFile, true); - UpdateYamlIfNeeded(singerFile, false); - - // add to parsing list (Global first, Singer second) - var filesToParse = new List(); - if (File.Exists(globalFile)) filesToParse.Add(globalFile); - if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); - - // backups of hardcoded defaults exist - if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); - if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); - if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); - if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); - if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); - - // reset live arrays/lists back to defaults before stacking - vowels = backupVowels; - consonants = backupConsonants; - tails = "-".Split(','); - - fricative = Array.Empty(); - aspirate = Array.Empty(); - semivowel = Array.Empty(); - liquid = Array.Empty(); - nasal = Array.Empty(); - stop = Array.Empty(); - tap = Array.Empty(); - affricate = Array.Empty(); - - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; - - diphthongTails.Clear(); - foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; - - diphthongSplits.Clear(); - foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; - - mergingReplacements.Clear(); - splittingReplacements.Clear(); - yamlFallbacks.Clear(); - PhonemeOverrides.Clear(); - if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); - vowelSustains.Clear(); - foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; - - // parse the files sequentially (Singer configs seamlessly overwrite global configs) - foreach (var file in filesToParse) { - try { - var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); - - var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); - vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); - - var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tails = yamlTails.Concat(tails).Distinct().ToArray(); - - if (data?.isglides != null) enableGlides = data.isglides.Value; - - var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); - fricative = yFricative.Concat(fricative).Distinct().ToArray(); - var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); - var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); - semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); - var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); - liquid = yLiquid.Concat(liquid).Distinct().ToArray(); - var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); - nasal = yNasal.Concat(nasal).Distinct().ToArray(); - var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); - stop = yStop.Concat(stop).Distinct().ToArray(); - var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tap = yTap.Concat(tap).Distinct().ToArray(); - var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - affricate = yAffricate.Concat(affricate).Distinct().ToArray(); - - var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) - .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); - consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); - - // DIPHTHONG AUTO-TAIL DETECTION - var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); - - foreach (var d in yamlDiphthongs) { - if (!diphthongSplits.ContainsKey(d)) { - foreach (var tail in dynamicTails) { - if (d.EndsWith(tail) && d != tail) { - diphthongTails[d] = tail; - break; - } - } - } - } - - // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) - if (data?.timings != null) { - foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; - } - - if (data?.replacements != null) { - var localMerge = new List(); - var localSplit = new List(); - string GetFromKey(object fromObj) { - if (fromObj is string s) return s; - if (fromObj is System.Collections.IEnumerable e) { - return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); - } - return ""; - } - - foreach (var rawReplacement in data.replacements) { - string fromKey = GetFromKey(rawReplacement.from); - mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - - if (rawReplacement.from is string fromStr) { - dictionaryReplacements.Remove(fromStr); - dictionaryReplacements.Remove(fromStr.ToLower()); - dictionaryReplacements.Remove(fromStr.ToUpper()); - } - - List fromList = rawReplacement.FromList; - List toList = rawReplacement.ToList; - object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); - object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); - - var cleanReplacement = new Replacement { - from = parsedFrom, - to = parsedTo, - where = rawReplacement.where - }; - - if (parsedFrom is string fromString) { - if (parsedTo is string toString) { - // Dictionary handles simple 1-to-1 replacements - dictionaryReplacements[fromString] = toString; - } else { - // 1-to-Many goes to Split - localSplit.Add(cleanReplacement); - } - } else { - // Many-to-Any goes to Merge - localMerge.Add(cleanReplacement); - } - } - mergingReplacements.InsertRange(0, localMerge); - splittingReplacements.InsertRange(0, localSplit); - } - - if (data?.fallbacks != null) { - foreach (var df in data.fallbacks) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - yamlFallbacks[df.from] = df.to; - } - } - } - - if (data?.diphthongs != null) { - foreach (var d in data.diphthongs) { - if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { - diphthongTails[d.from] = d.to; - } - } - } - - if (data?.vowelsustains != null) { - foreach (var v in data.vowelsustains) { - if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { - vowelSustains[v.symbol] = (v.sustain, v.offset); - } - } - } - - } catch (Exception ex) { - Log.Error($"Failed to parse {file}: {ex.Message}"); - } - } - - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - } - } - - protected USinger singer; - protected bool hasDictionary => dictionaries.ContainsKey(GetType()); - protected IG2p dictionary => dictionaries[GetType()]; - protected bool isDictionaryLoading => dictionaries[GetType()] == null; - protected double TransitionBasicLengthMs => 100; - - private Dictionary dictionaries = new Dictionary(); - private const string FORCED_ALIAS_SYMBOL = "?"; - private string error = ""; - 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 - /// - /// - protected abstract string[] GetVowels(); - - /// - /// Returns list of consonants. Only needed if there is a dictionary - /// - /// - protected virtual string[] GetConsonants() { - throw new NotImplementedException(); - } - - /// - /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc - /// - /// List of phonemes - protected abstract List ProcessSyllable(Syllable syllable); - - /// - /// phoneme symbols for ending, like, V-, or VC-, or VC+C - /// - protected abstract List ProcessEnding(Ending ending); - - /// - /// simple alias to alias fallback - /// - /// - protected virtual Dictionary GetAliasesFallback() { return null; } - - /// - /// Use to some custom init, if needed - /// - protected virtual void Init() { } - - /// - /// Dictionary name. Must be stored in Dictionaries folder. - /// If missing or can't be read, phonetic input is used - /// - /// - protected virtual string GetDictionaryName() { return null; } - - /// - /// extracts array of phoneme symbols from note. Override for procedural dictionary or something - /// reads from dictionary if provided - /// - /// - /// - protected virtual string[] GetSymbols(Note note) { - string[] getSymbolsRaw(string lyrics) { - if (lyrics == null) { - return new string[0]; - } else return lyrics.Split(" "); - } - - if (tails.Contains(note.lyric)) { - return new string[] { note.lyric }; - } - - if (hasDictionary) { - if (!string.IsNullOrEmpty(note.phoneticHint)) { - return getSymbolsRaw(note.phoneticHint); - } - - var result = new List(); - foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { - var subResult = dictionary.Query(subword); - if (subResult == null) { - subResult = HandleWordNotFound(note); - if (subResult == null) { - return null; - } - } else { - for (int i = 0; i < subResult.Length; i++) { - string phoneme = subResult[i]; - if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { - subResult[i] = replaced; - } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { - subResult[i] = replacedExact; - } - } - } - result.AddRange(subResult); - } - return result.ToArray(); - } else { - return getSymbolsRaw(note.lyric); - } - } - - /// - /// 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. - /// - /// - protected virtual Dictionary GetDictionaryPhonemesReplacement() { - return dictionaryReplacements ?? new Dictionary(); - } - private string[] backupVowels = null; - private string[] backupConsonants = null; - private Dictionary backupDiphthongTails = null; - private Dictionary backupDiphthongSplits = null; - private Dictionary backupDictionaryReplacements = null; - protected Dictionary vowelSustains = new Dictionary(); - private Dictionary backupVowelSustains = null; - - /// - /// separates symbols to syllables, without an ending. - /// - /// - /// - /// - protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - var firstVowelId = vowelIds[0]; - if (notes.Length < vowelIds.Length) { - error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; - return null; - } - - var syllables = new Syllable[vowelIds.Length]; - - // Making the first syllable - if (prevEnding.HasValue) { - var prevEndingValue = prevEnding.Value; - var beginningCc = prevEndingValue.cc.ToList(); - beginningCc.AddRange(symbols.Take(firstVowelId)); - - // If we had a prev neighbour ending, let's take info from it - syllables[0] = new Syllable() { - prevV = prevEndingValue.prevV, - cc = beginningCc.ToArray(), - v = symbols[firstVowelId], - tone = prevEndingValue.tone, - attr = prevEndingValue.attr, - duration = prevEndingValue.duration, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes, - prevWordConsonantsCount = prevEndingValue.cc.Count() - }; - } else { - // there is only empty space before us - syllables[0] = new Syllable() { - prevV = "", - cc = symbols.Take(firstVowelId).ToArray(), - v = symbols[firstVowelId], - tone = notes[0].tone, - attr = notes[0].phonemeAttributes, - duration = -1, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes - }; - } - - // normal syllables after the first one - var noteI = 1; - var ccs = new List(); - var position = 0; - var lastSymbolI = firstVowelId + 1; - for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { - if (!vowelIds.Contains(lastSymbolI)) { - ccs.Add(symbols[lastSymbolI]); - } else { - position += notes[noteI - 1].duration; - syllables[noteI] = new Syllable() { - prevV = syllables[noteI - 1].v, - cc = ccs.ToArray(), - v = symbols[lastSymbolI], - tone = notes[noteI - 1].tone, - attr = notes[noteI - 1].phonemeAttributes, - duration = notes[noteI - 1].duration, - position = position, - vowelTone = notes[noteI].tone, - vowelAttr = notes[noteI].phonemeAttributes, - canAliasBeExtended = true // for all not-first notes is allowed - }; - ccs = new List(); - noteI++; - } - } - - return syllables; - } - - /// - /// extracts word ending - /// - /// - /// - protected Ending? MakeEnding(Note[] inputNotes) { - if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return null; - } - - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - - return new Ending() { - prevV = symbols[vowelIds.Last()], - cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), - tone = notes.Last().tone, - attr = notes.Last().phonemeAttributes, - duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), - position = notes.Sum(n => n.duration) - }; - } - - /// - /// extracts and validates symbols and vowels - /// - /// - /// - private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { - var mainNote = notes[0]; - var symbols = GetSymbols(mainNote); - if (symbols == null) { - return (null, null, null); - } - if (symbols.Length == 0) { - symbols = new string[] { "" }; - } - - symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); - symbols = ApplyExtensions(symbols, notes); - List vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - vowelIds.Add(symbols.Length - 1); - } - if (notes.Length < vowelIds.Count) { - notes = HandleNotEnoughNotes(notes, vowelIds); - } - return (symbols, vowelIds.ToArray(), notes); - } - - /// - /// When there are more syllables than notes, recombines notes to match syllables count - /// - /// - /// - /// - protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { - var newNotes = new List(); - newNotes.AddRange(notes.SkipLast(1)); - var lastNote = notes.Last(); - var position = lastNote.position; - var notesToSplit = vowelIds.Count - newNotes.Count; - var duration = lastNote.duration / notesToSplit / 15 * 15; - for (var i = 0; i < notesToSplit; i++) { - var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); - newNotes.Add(new Note() { - position = position, - duration = durationFinal, - tone = lastNote.tone, - phonemeAttributes = lastNote.phonemeAttributes - }); - position += durationFinal; - } - - return newNotes.ToArray(); - } - - /// - /// Override this method, if you want to implement some machine converting from a word to phonemes - /// - /// - /// - protected virtual string[] HandleWordNotFound(Note note) { - var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); - if(HasOto(mpdlyric, note.tone)){ - error = mpdlyric; - }else{ - error = "word not found"; - } - return null; - } - - /// - /// Does this note extend the previous syllable? - /// - /// - /// - protected bool IsSyllableVowelExtensionNote(Note note) { - return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); - } - - /// - /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic - /// - /// - /// - protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { - return phonemesString.Split(' '); - } - - /// - /// use to validate alias - /// - /// - /// - protected virtual string ValidateAlias(string alias) { - return alias; - } - - /// - /// Defines basic transition length before scaling it according to tempo and note length - /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation - /// - /// Mapped alias - /// - protected virtual double GetTransitionBasicLengthMs(string alias = "") { - return GetTransitionBasicLengthMsByConstant(); - } - - 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 - /// - /// - protected double GetTempoNoteLengthFactor() { - return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; - } - - protected virtual IG2p[] GetBaseG2ps() { - return Array.Empty(); - } - - protected virtual IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Native YAML Dictionary Logic - if (!string.IsNullOrEmpty(YamlFileName)) { - string path = Path.Combine(PluginDir, YamlFileName); - - // Write template if missing - if (!File.Exists(path) && YamlTemplate != null) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - - // Load dictionary from Singer Folder (Highest Priority) - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - - // Load dictionary from Plugin Folder (Fallback Priority) - if (File.Exists(path)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {path}"); - } - } - } - // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) - else { - var dictionaryName = GetDictionaryName(); - if (!string.IsNullOrEmpty(dictionaryName)) { - var filename = Path.Combine(DictionariesPath, dictionaryName); - if (File.Exists(filename)) { - var dictionaryText = File.ReadAllText(filename); - var builder = G2pDictionary.NewBuilder(); - foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); - foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); - builder.AddEntry("a", new string[] { "a" }); - ParseDictionary(dictionaryText, builder); - g2ps.Add(builder.Build()); - } - } - } - - // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) - var childG2ps = GetBaseG2ps(); - if (childG2ps != null && childG2ps.Any()) { - g2ps.AddRange(childG2ps); - } - - return new G2pFallbacks(g2ps.ToArray()); - } - - /// - /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, - /// and replaces phonemes with replacement table - /// Is Running Async! - /// - /// - /// - protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { - var replacements = GetDictionaryPhonemesReplacement(); - foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { - if (line.StartsWith(";;;")) { - continue; - } - var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); - if (parts.Length != 2) { - continue; - } - string key = parts[0].ToLowerInvariant(); - var values = GetDictionaryWordPhonemes(parts[1]).Select( - n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); - lock (builder) { - builder.AddEntry(key, values); - }; - }; - } - - #region helpers - - /// - /// May be used if you have different logic for short and long notes - /// - /// - /// - protected bool IsShort(Syllable syllable) { - return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; - } - 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 - /// - /// - /// - /// - protected bool HasOto(string alias, int tone) { - return singer.TryGetMappedOto(alias, tone, out _); - } - - /// - /// Can be used for different variants, like exhales [v R], [v -] etc - /// - /// phonemes container to add to - /// to map alias - /// target phoneme variants - /// returns true if added any - protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { - foreach (var phoneme in targetPhonemes) { - if (HasOto(phoneme, tone)) { - sourcePhonemes.Add(phoneme); - return true; - } - } - 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 - /// - /// - /// - protected bool CanMakeAliasExtension(Syllable syllable) { - return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; - } - - /// - /// if current syllable is VV and previous one is from the same pitch, - /// you may wan't to just extend the previous alias. Put the phoneme as null fot that - /// - /// - /// - /// - protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { - if (singer.Subbanks.Count == 1) { - return true; - } - if (tone1 == tone2) { - return true; - } - var toneSets = singer.Subbanks.Select(n => n.toneSet); - foreach (var toneSet in toneSets) { - if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { - return true; - } - if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { - return false; - } - } - return true; - } - - protected virtual string YamlFileName => null; - protected virtual byte[] YamlTemplate => null; - protected virtual string YamlVersion => null; - - protected string[] vowels = Array.Empty(); - protected string[] consonants = Array.Empty(); - protected string[] tails = "-,R".Split(','); - protected string[] affricate = Array.Empty(); - protected string[] fricative = Array.Empty(); - protected string[] aspirate = Array.Empty(); - protected string[] semivowel = Array.Empty(); - protected string[] liquid = Array.Empty(); - protected string[] nasal = Array.Empty(); - protected string[] stop = Array.Empty(); - protected string[] tap = Array.Empty(); - - protected Dictionary dictionaryReplacements = new Dictionary(); - protected Dictionary PhonemeOverrides = new Dictionary(); - protected Dictionary yamlFallbacks = new Dictionary(); - protected List consExceptions = new List(); - - protected Dictionary diphthongTails = new Dictionary(); - protected Dictionary diphthongSplits = new Dictionary(); - - 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(); - public Timings[] timings { get; set; } = Array.Empty(); - public DiphthongData[] diphthongs { get; set; } = Array.Empty(); - public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); - - public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } - public struct Fallbacks { public string from { get; set; } public string to { get; set; } } - public struct Timings { public string symbol { get; set; } public double value { get; set; } } - public struct DiphthongData { public string from { get; set; } public string to { get; set; } } - public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } - } - - public class Replacement { - public object from { get; set; } - public object to { get; set; } - public string where { get; set; } = "inside"; - - public List FromList { - get { - if (from is string s) return new List { s }; - if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - - public List ToList { - get { - if (to is string s) return new List { s }; - if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - } - - protected List mergingReplacements = new List(); - protected List splittingReplacements = new List(); - - protected virtual bool IsGroupKeyword(string rulePhoneme) { - // 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 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; - } - } - } - - bool inBaseGroup = false; - switch (baseGroup) { - case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; - case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; - case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; - case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; - case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; - case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; - case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; - case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; - case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; - case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; - } - - if (!inBaseGroup) return false; - - if (cleanRule.Contains("!")) { - string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; - if (excluded.Split(',').Contains(actualPhoneme)) return false; - } - - if (cleanRule.Contains("=")) { - string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; - if (!restricted.Split(',').Contains(actualPhoneme)) return false; - } - - return true; - } - - protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; - - List finalPhonemes = new List(); - int idx = 0; - - var validRules = mergingReplacements.Concat(splittingReplacements) - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - var validSplits = splittingReplacements - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - while (idx < inputPhonemes.Count) { - bool replaced = false; - - foreach (var rule in validRules) { - 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.Count; j++) { - string rulePh = fromArray[j]; - string actualPh = inputPhonemes[idx + j]; - - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh)) { - if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); - captures[baseRulePh].Add(actualPh); - } else { - match = false; break; - } - } else if (rulePh != actualPh) { - match = false; break; - } - } - - if (match) { - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - 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++) { - // 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.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) { - List fromArray = rule.FromList; - if (fromArray == null || fromArray.Count != 1) continue; - - string rulePh = fromArray[0]; - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { - - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - foreach(string toPh in toArray) { - 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; - } - } - } - if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } else if (!replaced) { - finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } - } - return finalPhonemes; - } - - private Syllable ApplyBoundaryReplacements(Syllable syllable) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); - bool hasV = !string.IsNullOrEmpty(syllable.v); - - 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) || !hasPrevV; - List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); - - string newPrevV = ""; - string newV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - if (hasV && finalPhonemes.Count > 0) { - var vowelsList = GetVowels(); - int vIndex = finalPhonemes.Count - 1; - - for (int i = finalPhonemes.Count - 1; i >= 0; i--) { - if (vowelsList.Contains(finalPhonemes[i])) { - vIndex = i; - break; - } - } - newV = finalPhonemes[vIndex]; - for (int i = 0; i < vIndex; i++) { - newCc.Add(finalPhonemes[i]); - } - } else { - newCc.AddRange(finalPhonemes); - } - } - - syllable.prevV = newPrevV; - syllable.cc = newCc.ToArray(); - syllable.v = newV; - return syllable; - } - - private Ending ApplyBoundaryReplacements(Ending ending) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); - currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); - if (ending.cc != null) currentPhonemes.AddRange(ending.cc); - - List finalPhonemes = ApplyReplacements(currentPhonemes, true); - - string newPrevV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - newCc.AddRange(finalPhonemes); - } - - ending.prevV = newPrevV; - ending.cc = newCc.ToArray(); - return ending; - } - - #endregion - - #region private - - private Result MakeForcedAliasResult(Note note) { - return MakeSimpleResult(note.lyric.Substring(1)); - } - - protected void ReadDictionaryAndInit() { - var dictionaryName = GetDictionaryName(); - if (dictionaryName == null) { - return; - } - dictionaries[GetType()] = null; - if (Testing) { - ReadDictionary(dictionaryName); - Init(); - return; - } - OnAsyncInitStarted(); - Task.Run(() => { - ReadDictionary(dictionaryName); - Init(); - OnAsyncInitFinished(); - }); - } - - private void ReadDictionary(string dictionaryName) { - try { - var phonemeSymbols = new Dictionary(); - - foreach (var vowel in GetVowels()) { - phonemeSymbols[vowel] = true; - } - foreach (var consonant in GetConsonants()) { - phonemeSymbols[consonant] = false; - } - - var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); - var safeDict = new Dictionary(); - - foreach (var kvp in childDict) { - safeDict[kvp.Key] = kvp.Value; - safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' - safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' - } - - dictionaries[GetType()] = new G2pRemapper( - LoadBaseDictionary(), - phonemeSymbols, - safeDict); - - } catch (Exception ex) { - Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); - } - } - - private string[] ApplyExtensions(string[] symbols, Note[] notes) { - var newSymbols = new List(); - var vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - // no syllables or all consonants, the last phoneme will be interpreted as vowel - vowelIds.Add(symbols.Length - 1); - } - var lastVowelI = 0; - newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); - for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { - if (!IsSyllableVowelExtensionNote(notes[i])) { - var prevVowel = vowelIds[lastVowelI]; - lastVowelI++; - var vowel = vowelIds[lastVowelI]; - newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); - } else { - newSymbols.Add(symbols[vowelIds[lastVowelI]]); - } - } - newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); - return newSymbols.ToArray(); - } - - private List ExtractVowels(string[] symbols) { - var vowelIds = new List(); - var vowels = GetVowels(); - for (var i = 0; i < symbols.Length; i++) { - if (vowels.Contains(symbols[i])) { - vowelIds.Add(i); - } - } - 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]; - - 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) { - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - stretch *= overrideRatio; - } else { - baseLengthMs = GetTransitionBasicLengthMs(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; - } - } - } - - 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] = new Phoneme { - phoneme = validatedAlias, - index = globalIndex - }; - - if (i == 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) { - // YAML Override active: Use the multiplier and bypass NoGap entirely - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); - } else { - // Default behavior - baseLengthMs = GetTransitionBasicLengthMsByOto(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 - phonemes[phonemeI].position = MsToTick(baseLengthMs); - } - } - } else { - 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]; - } - } else { - // 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 - 1 : phonemeSymbols.Count - 1, containerLength); - } - - private string ValidateAliasIfNeeded(string alias, int tone) { - if (HasOto(alias, tone)) { - return alias; - } - return ValidateAlias(alias); - } - - private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { - var offset = 0; - var lengthModifier = 1.0; - - if (containerLengthTick > 0) { - var allTransitionsLengthTick = phonemes.Sum(n => n.position); - - // 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--) { - if (phonemes[i].phoneme == null) continue; - var finalLengthTick = (int)(phonemes[i].position * lengthModifier); - phonemes[i].position = startPosition - finalLengthTick - offset; - offset += finalLengthTick; - } - - return phonemes.Where(n => n.phoneme != null).ToArray(); - } - - #endregion - } -} From 037519e681b6371043e6ae2e5479818db930bc81 Mon Sep 17 00:00:00 2001 From: Kona <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 08:47:26 -0700 Subject: [PATCH 07/13] Delete JapaneseCVVXPhonemizer directory --- .../Data/Resources.Designer.cs | 73 - JapaneseCVVXPhonemizer/Data/Resources.resx | 124 -- JapaneseCVVXPhonemizer/Data/template.yaml | 703 ------- JapaneseCVVXPhonemizer/JA_CVVX.cs | 1085 ---------- JapaneseCVVXPhonemizer/JA_CVVX.csproj | 29 - .../SyllableBasedPhonemizer.cs | 1780 ----------------- 6 files changed, 3794 deletions(-) delete mode 100644 JapaneseCVVXPhonemizer/Data/Resources.Designer.cs delete mode 100644 JapaneseCVVXPhonemizer/Data/Resources.resx delete mode 100644 JapaneseCVVXPhonemizer/Data/template.yaml delete mode 100644 JapaneseCVVXPhonemizer/JA_CVVX.cs delete mode 100644 JapaneseCVVXPhonemizer/JA_CVVX.csproj delete mode 100644 JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs diff --git a/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs b/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs deleted file mode 100644 index a77bf3342..000000000 --- a/JapaneseCVVXPhonemizer/Data/Resources.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace JA_SBP.Data { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("JA_SBP.Data.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] template { - get { - object obj = ResourceManager.GetObject("template", resourceCulture); - return ((byte[])(obj)); - } - } - } -} diff --git a/JapaneseCVVXPhonemizer/Data/Resources.resx b/JapaneseCVVXPhonemizer/Data/Resources.resx deleted file mode 100644 index 4ec68391a..000000000 --- a/JapaneseCVVXPhonemizer/Data/Resources.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/Data/template.yaml b/JapaneseCVVXPhonemizer/Data/template.yaml deleted file mode 100644 index a9f0cdaa4..000000000 --- a/JapaneseCVVXPhonemizer/Data/template.yaml +++ /dev/null @@ -1,703 +0,0 @@ -%YAML 1.2 ---- -version: 1.2 -symbols: -# Romaji symbols - - {symbol: a, type: vowel} - - {symbol: e, type: vowel} - - {symbol: i, type: vowel} - - {symbol: o, type: vowel} - - {symbol: u, type: vowel} - - {symbol: N, type: vowel} - - {symbol: ts, type: fricative} - - {symbol: j, type: fricative} - - {symbol: dj, type: stop} -# Arpabet + extras - - {symbol: aa, type: vowel} - - {symbol: ae, type: vowel} - - {symbol: ah, type: vowel} - - {symbol: ao, type: vowel} - - {symbol: aw, type: vowel} - - {symbol: ax, type: vowel} - - {symbol: ay, type: vowel} - - {symbol: b, type: stop} - - {symbol: ch, type: affricate} - - {symbol: d, type: stop} - - {symbol: dh, type: fricative} - - {symbol: dr, type: fricative} - - {symbol: dx, type: tap} - - {symbol: eh, type: vowel} - - {symbol: er, type: vowel} - - {symbol: ey, type: vowel} - - {symbol: f, type: fricative} - - {symbol: ff, type: fricative} - - {symbol: g, type: stop} - - {symbol: hh, type: aspirate} - - {symbol: ih, type: vowel} - - {symbol: iy, type: vowel} - - {symbol: jh, type: affricate} - - {symbol: k, type: stop} - - {symbol: l, type: liquid} - - {symbol: m, type: nasal} - - {symbol: n, type: nasal} - - {symbol: ng, type: nasal} - - {symbol: ow, type: vowel} - - {symbol: oy, type: vowel} - - {symbol: p, type: stop} - - {symbol: q, type: stop} - - {symbol: Fr, type: stop} - - {symbol: r, type: liquid} - - {symbol: wr, type: liquid} - - {symbol: rr, type: stop} - - {symbol: s, type: fricative} - - {symbol: sh, type: fricative} - - {symbol: t, type: stop} - - {symbol: th, type: fricative} - - {symbol: tr, type: fricative} - - {symbol: uh, type: vowel} - - {symbol: uw, type: vowel} - - {symbol: v, type: fricative} - - {symbol: w, type: semivowel} - - {symbol: y, type: semivowel} - - {symbol: z, type: fricative} - - {symbol: zh, type: fricative} - - {symbol: Ex, type: tail} - - {symbol: In, type: tail} - - {symbol: Cm, type: tail} - - {symbol: Fr, type: tail} - -# for Arpabet to Romaji conversion -replacements: - - {from: aa, to: [a]} - - {from: ae, to: [e]} - - {from: [ae, n], to: [e, A, n]} - - {from: [ae, m], to: [e, A, m]} - - {from: [ae, ng], to: [e, A, n]} - - {from: ao, to: [o]} - - {from: aw, to: [a, w]} - - {from: ah, to: [a]} - - {from: ax, to: [a]} - - {from: [ax, n], to: [e, n]} - - {from: [ax, m], to: [e, m]} - - {from: [ax, ng], to: [e, n]} - - {from: [ax, l], to: [o, r]} - - {from: ay, to: [a, y]} - - {from: dh, to: [d]} - - {from: dr, to: [j, r]} - - {from: dx, to: [r]} - - {from: eh, to: [e]} - - {from: er, to: [o]} - - {from: ey, to: [e, y]} - - {from: hh, to: [h]} - - {from: ih, to: [e]} - - {from: iy, to: [i]} - - {from: jh, to: [dj]} - - {from: l, to: [l]} - - {from: ng, to: [ng]} - - {from: ow, to: [o, w]} - - {from: oy, to: [o, y]} - - {from: q, to: ['-']} - - {from: r, to: [r]} - - {from: th, to: [s]} - - {from: [th, r], to: [ts, r]} - - {from: tr, to: [ch, r]} - - {from: ts, to: [ts]} - - {from: uh, to: [u]} - - {from: uw, to: [u]} - - {from: v, to: [v]} - - {from: zh, to: [sh]} - # Vowel sustains - - {from: aL, to: [a, a]} - - {from: eL, to: [e, e]} - - {from: iL, to: [i, i]} - - {from: oL, to: [o, o]} - - {from: uL, to: [u, u]} - # Romaji geminations - - {from: [b, b], to: B} - - {from: [c, ch], to: CH} - - {from: [d, dy], to: DY} - - {from: [d, d], to: D} - - {from: [d, dj], to: DJ} - - {from: [g, g], to: G} - - {from: [g, gy], to: GY} - - {from: [h, h], to: H} - - {from: [h, hy], to: HY} - - {from: [j, j], to: J} - - {from: [k, k], to: K} - - {from: [k, ky], to: KY} - - {from: [l, l], to: L} - - {from: [m ,m], to: M} - - {from: [m, my], to: MY} - - {from: [n, n], to: NX} - - {from: [n, ny], to: NY} - - {from: [n, ng], to: NGX} - - {from: [p, p], to: P} - - {from: [p, py], to: PY} - - {from: [r, ry], to: RY} - - {from: [s, s], to: S} - - {from: [s, sh], to: SH} - - {from: [t, t], to: T} - - {from: [t, ty], to: TY} - - {from: [t, ts], to: TS} - - {from: [v, v], to: V} - - {from: [w, w], to: W} - - {from: [y, y], to: Y} - - {from: [z, z], to: Z} - -fallbacks: - # Only 1:1 mappings also no brackets cuz it will error out - # example entries: - - {from: A, to: a} -# Geminations - - {from: B, to: b} - - {from: CH, to: ch} - - {from: DH, to: d} - - {from: DY, to: dy} - - {from: DJ, to: dj} - - {from: F, to: f} - - {from: G, to: g} - - {from: GY, to: gy} - - {from: H, to: h} - - {from: HY, to: hy} - - {from: J, to: j} - - {from: K, to: k} - - {from: KY, to: ky} - - {from: L, to: l} - - {from: M, to: m} - - {from: MY, to: my} - - {from: NX, to: n} - - {from: NY, to: ny} - - {from: NGX, to: ng} - - {from: P, to: p} - - {from: PY, to: py} - - {from: RX, to: r} - - {from: RY, to: ry} - - {from: S, to: s} - - {from: SH, to: sh} - - {from: T, to: t} - - {from: TY, to: ty} - - {from: TS, to: ts} - - {from: V, to: v} - - {from: W, to: w} - - {from: Y, to: y} - - {from: Z, to: Z} - -timings: -# phoneme duration multipliers (decimal values in seconds) - - {symbol: ff, value: 1.3} - - {symbol: dj, value: 1.5} - - {symbol: o l, value: 1.2} - - {symbol: i l, value: 1.4} - - {symbol: u l, value: 1.2} - - {symbol: '- k', value: 0.8} - - {symbol: '- t', value: 0.8} - - {symbol: '- g', value: 0.8} - - {symbol: '- p', value: 0.8} -# Gemination length - - {symbol: B, value: 2.2} - - {symbol: CH, value: 2.2} - - {symbol: DY, value: 2.2} - - {symbol: D, value: 2.2} - - {symbol: DJ, value: 2.2} - - {symbol: F, value: 2.8} - - {symbol: G, value: 2.2} - - {symbol: GY, value: 2.2} - - {symbol: H, value: 2.8} - - {symbol: HY, value: 2.8} - - {symbol: J, value: 2.2} - - {symbol: K, value: 2.2} - - {symbol: KY, value: 2.2} - - {symbol: L, value: 2.2} - - {symbol: M, value: 2.2} - - {symbol: MY, value: 2.2} - - {symbol: NX, value: 2.2} - - {symbol: NY, value: 2.2} - - {symbol: NGX, value: 2.2} - - {symbol: P, value: 2.2} - - {symbol: PY, value: 2.2} - - {symbol: RX, value: 2.2} - - {symbol: RY, value: 2.2} - - {symbol: S, value: 2.8} - - {symbol: SH, value: 2.8} - - {symbol: T, value: 2.2} - - {symbol: TY, value: 2.2} - - {symbol: TS, value: 2.8} - - {symbol: V, value: 2.2} - - {symbol: W, value: 2.2} - - {symbol: Y, value: 2.2} - - {symbol: Z, value: 2.8} - -consonant_groups: -# same mechanics in presamp, the CONSONANTS - - {group: ch, members: [chi, che, cha, chu, cho, ち, ちぇ, ちゃ, ちゅ, ちょ]} - - {group: gy, members: [gi, gye, gya, gyu, gyo, ぎ, ぎぇ, ぎゃ, ぎゅ, ぎょ]} - - {group: ts, members: [tsu, tsa, tsi, tse, tso, つ, つぁ, つぃ, つぇ, つぉ]} - - {group: ty, members: [ti, tye, tya, tyu, tyo, てぃ, てぇ, てゃ, てゅ, てょ]} - - {group: py, members: [pi, pye, pya, pyu, pyo, ぴ, ぴぇ, ぴゃ, ぴゅ, ぴょ]} - - {group: ry, members: [ri, rye, rya, ryu, ryo, り, りぇ, りゃ, りゅ, りょ]} - - {group: ny, members: [ni, nie, nya, nyu, nyo, に, にぇ, にゃ, にゅ, にょ]} - - {group: r, members: [ra, ru, re, ro, ら, る, れ, ろ]} - - {group: hy, members: [hi, hye, hya, hyu, hyo, ひ, ひぇ, ひゃ, ひゅ, ひょ]} - - {group: dy, members: [di, dye, dya, dyu, dyo, でぃ, でぇ, でゃ, でゅ, でょ]} - - {group: by, members: [bi, bye, bya, byu, byo, び, びぇ, びゃ, びゅ, びょ]} - - {group: b, members: [ba, bu, be, bo, ば, ぶ, べ, ぼ]} - - {group: d, members: [da, de, do, du, だ, で, ど, どぅ]} - - {group: g, members: [ga, gu, ge, go, が, ぐ, げ, ご]} - - {group: f, members: [fu, fa, fi, fe, fo, ふ, ふぁ, ふぃ, ふぇ, ふぉ]} - - {group: h, members: [ha, he, ho, は, へ, ほ]} - - {group: k, members: [ka, ku, ke, ko, か, く, け, こ]} - - {group: j, members: [ji, je, ja, ju, jo, じ, じぇ, じゃ, じゅ, じょ]} - - {group: dj, members: [dji, dje, dja, dju, djo, ぢ, ぢぇ, ぢゃ, ぢゅ, ぢょ]} - - {group: m, members: [ma, mu, me, mo, ま, む, め, も]} - - {group: n, members: [na, nu, ne, no, な, ぬ, ね, の]} - - {group: p, members: [pa, pu, pe, po, ぱ, ぷ, ぺ, ぽ]} - - {group: s, members: [sa, su, si, se, so, さ, す, すぃ, せ, そ]} - - {group: sh, members: [shi, she, sha, shu, sho, し, しぇ, しゃ, しゅ, しょ]} - - {group: t, members: [ta, te, to, tu, た, て, ど, とぅ]} - - {group: w, members: [wi, wu, we, wo, wa, wo, うぃ, うぅ, うぇ, うぉ, わ, を]} - - {group: v, members: [vu, va, vi, vu, ve, vo, ヴ, ヴぁ, ヴぃ, ヴぅ, ヴぇ, ヴぉ]} - - {group: y, members: [yi, ye, ya, yu, yo, いぃ, いぇ, や, ゆ, よ]} - - {group: ky, members: [ki, kie, kya, kyu, kyo, き, きぇ, きゃ, きゅ, きょ]} - - {group: z, members: [za, zu, zi, ze, zo, ざ, ず, ずぃ, ぜ, ぞ]} - - {group: my, members: [mi, mie, mya, myu, myo, み, みぇ, みゃ, みゅ, みょ]} - # gemination groups - - {group: CH, members: [cchi, cche, ccha, cchu, ccho, っち, っちぇ, っちゃ, っちゅ, っちょ]} - - {group: GY, members: [ggi, ggye, ggya, ggyu, ggyo, っぎ, っぎぇ, っぎゃ, っぎゅ, っぎょ]} - - {group: TS, members: [ttsu, ttsa, ttsi, ttse, ttso, っつ, っつぁ, っつぃ, っつぇ, っつぉ]} - - {group: TY, members: [tti, ttye, ttya, ttyu, ttyo, ってぃ, ってぇ, ってゃ, ってゅ, ってょ]} - - {group: PY, members: [ppi, ppye, ppya, ppyu, ppyo, っぴ, っぴぇ, っぴゃ, っぴゅ, っぴょ]} - - {group: RY, members: [rri, rrye, rrya, rryu, rryo, っり, っりぇ, っりゃ, っりゅ, っりょ]} - - {group: NY, members: [nni, nnie, nnya, nnyu, nnyo, っに, っにぇ, っにゃ, っにゅ, っにょ]} - - {group: RX, members: [っら, っる, っれ, っろ]} - - {group: HY, members: [hhi, hhye, hhya, hhyu, hhyo, っひ, っひぇ, っひゃ, っひゅ, っひょ]} - - {group: DY, members: [ddi, ddye, ddya, ddyu, ddyo, っでぃ, っでぇ, っでゃ, っでゅ, っでょ]} - - {group: BY, members: [bbi, bbye, bbya, bbyu, bbyo, っび, っびぇ, っびゃ, っびゅ, っびょ]} - - {group: B, members: [bba, bbu, bbe, bbo, っば, っぶ, っべ, っぼ]} - - {group: D, members: [dda, dde, ddo, ddu, っだ, っで, っど, っどぅ]} - - {group: G, members: [gga, ggu, gge, ggo, っが, っぐ, っげ, っご]} - - {group: F, members: [っふ, っふぁ, っふぃ, っふぇ, っふぉ]} - - {group: H, members: [hha, hhe, hho, っは, っへ, っほ]} - - {group: K, members: [kka, kku, kke, kko, っか, っく, っけ, っこ]} - - {group: J, members: [jji, jje, jja, jju, jjo, っじ, っじぇ, じゃ, っじゅ, っじょ]} - - {group: DJ, members: [ddji, ddje, ddja, ddju, ddjo, っぢ, っぢぇ, っぢゃ, っぢゅ, っぢょ]} - - {group: M, members: [mma, mmu, mme, mmo, っま, っむ, っめ, っも]} - - {group: N, members: [nna, nnu, nne, nno, っな, っぬ, っね, っの]} - - {group: P, members: [ppa, ppu, ppe, ppo, っぱ, っぷ, っぺ, っぽ]} - - {group: S, members: [ssa, ssu, ssi, sse, sso, っさ, っす, っすぃ, っせ, っそ]} - - {group: SH, members: [sshi, sshe, ssha, sshu, ssho, っし, っしぇ, っしゃ, っしゅ, っしょ]} - - {group: T, members: [tta, tte, tto, ttu, った, って, っと, っとぅ]} - - {group: W, members: [wwi, wwu, wwe, wwo, wwa, っうぃ, っうぅ, っうぇ, っうぉ, っわ]} - - {group: V, members: [vvu, vva, vvi, vve, vvo, っヴ, っヴぁ, っヴぃ, っヴぇ, っヴぉ]} - - {group: Y, members: [yyi, yye, yya, yyu, yyo, っいぃ, っいぇ, っや, っゆ, っよ]} - - {group: KY, members: [kki, kkie, kkya, kkyu, kkyo, っき, っきぇ, きゃ, っきゅ, っきょ]} - - {group: Z, members: [zza, zzu, zzi, zze, zzo, っざ, っず, っずぃ, っぜ, っぞ]} - - {group: MY, members: [mmi, mmie, mmya, mmyu, mmyo, っみ, っみぇ, っみゃ, っみゅ, っみょ]} - -wanakana: -# CV / CCV Convertions - - {roma: [A], kana: あ} - - {roma: [a], kana: あ} - - {roma: [i], kana: い} - - {roma: [u], kana: う} - - {roma: [e], kana: え} - - {roma: [o], kana: お} - - {roma: [k, a], kana: か} - - {roma: [k, i], kana: き} - - {roma: [k, u], kana: く} - - {roma: [k, e], kana: け} - - {roma: [k, o], kana: こ} - - {roma: [g, a], kana: が} - - {roma: [g, i], kana: ぎ} - - {roma: [g, u], kana: ぐ} - - {roma: [g, e], kana: げ} - - {roma: [g, o], kana: ご} - - {roma: [s, a], kana: さ} - - {roma: [sh, i], kana: し} - - {roma: [s, u], kana: す} - - {roma: [s, e], kana: せ} - - {roma: [s, o], kana: そ} - - {roma: [z, a], kana: ざ} - - {roma: [j, i], kana: じ} - - {roma: [z, u], kana: ず} - - {roma: [z, e], kana: ぜ} - - {roma: [z, o], kana: ぞ} - - {roma: [t, a], kana: た} - - {roma: [ch, i], kana: ち} - - {roma: [ts, u], kana: つ} - - {roma: [t, e], kana: て} - - {roma: [t, o], kana: と} - - {roma: [d, a], kana: だ} - - {roma: [dj, i], kana: ぢ} - - {roma: [z, u], kana: づ} - - {roma: [d, e], kana: で} - - {roma: [d, o], kana: ど} - - {roma: [n, a], kana: な} - - {roma: [n, i], kana: に} - - {roma: [n, u], kana: ぬ} - - {roma: [n, e], kana: ね} - - {roma: [n, o], kana: の} - - {roma: [h, a], kana: は} - - {roma: [h, i], kana: ひ} - - {roma: [f, u], kana: ふ} - - {roma: [h, e], kana: へ} - - {roma: [h, o], kana: ほ} - - {roma: [b, a], kana: ば} - - {roma: [b, i], kana: び} - - {roma: [b, u], kana: ぶ} - - {roma: [b, e], kana: べ} - - {roma: [b, o], kana: ぼ} - - {roma: [p, a], kana: ぱ} - - {roma: [p, i], kana: ぴ} - - {roma: [p, u], kana: ぷ} - - {roma: [p, e], kana: ぺ} - - {roma: [p, o], kana: ぽ} - - {roma: [m, a], kana: ま} - - {roma: [m, i], kana: み} - - {roma: [m, u], kana: む} - - {roma: [m, e], kana: め} - - {roma: [m, o], kana: も} - - {roma: [y, a], kana: や} - - {roma: [y, u], kana: ゆ} - - {roma: [y, o], kana: よ} - - {roma: [r, a], kana: ら} - - {roma: [r, i], kana: り} - - {roma: [r, u], kana: る} - - {roma: [r, e], kana: れ} - - {roma: [r, o], kana: ろ} - - {roma: [w, a], kana: わ} - - {roma: [ky, a], kana: きゃ} - - {roma: [ky, u], kana: きゅ} - - {roma: [ky, o], kana: きょ} - - {roma: [gy, a], kana: ぎゃ} - - {roma: [gy, u], kana: ぎゅ} - - {roma: [gy, o], kana: ぎょ} - - {roma: [sh, a], kana: しゃ} - - {roma: [sh, u], kana: しゅ} - - {roma: [sh, o], kana: しょ} - - {roma: [j, a], kana: じゃ} - - {roma: [j, u], kana: じゅ} - - {roma: [j, o], kana: じょ} - - {roma: [ch, a], kana: ちゃ} - - {roma: [ch, u], kana: ちゅ} - - {roma: [ch, o], kana: ちょ} - - {roma: [dj, a], kana: ぢゃ} - - {roma: [dj, u], kana: ぢゅ} - - {roma: [dj, o], kana: ぢょ} - - {roma: [ny, a], kana: にゃ} - - {roma: [ny, u], kana: にゅ} - - {roma: [ny, o], kana: にょ} - - {roma: [hy, a], kana: ひゃ} - - {roma: [hy, u], kana: ひゅ} - - {roma: [hy, o], kana: ひょ} - - {roma: [by, a], kana: びゃ} - - {roma: [by, u], kana: びゅ} - - {roma: [by, o], kana: びょ} - - {roma: [py, a], kana: ぴゃ} - - {roma: [py, u], kana: ぴゅ} - - {roma: [py, o], kana: ぴょ} - - {roma: [my, a], kana: みゃ} - - {roma: [my, u], kana: みゅ} - - {roma: [my, o], kana: みょ} - - {roma: [ry, a], kana: りゃ} - - {roma: [ry, u], kana: りゅ} - - {roma: [ry, o], kana: りょ} - - {roma: [y, e], kana: いぇ} - - {roma: [ky, e], kana: きぇ} - - {roma: [gy, e], kana: ぎぇ} - - {roma: [sh, e], kana: しぇ} - - {roma: [j, e], kana: じぇ} - - {roma: [ch, e], kana: ちぇ} - - {roma: [dj, e], kana: ぢぇ} - - {roma: [ny, e], kana: にぇ} - - {roma: [hy, e], kana: ひぇ} - - {roma: [by, e], kana: びぇ} - - {roma: [py, e], kana: ぴぇ} - - {roma: [my, e], kana: みぇ} - - {roma: [ry, e], kana: りぇ} - - {roma: [w, i], kana: うぃ} - - {roma: [w, e], kana: うぇ} - - {roma: [w, o], kana: うぉ} - - {roma: [w, o], kana: を} - - {roma: [kw, a], kana: くぁ} - - {roma: [kw, i], kana: くぃ} - - {roma: [kw, e], kana: くぇ} - - {roma: [kw, o], kana: くぉ} - - {roma: [gw, a], kana: ぐぁ} - - {roma: [gw, i], kana: ぐぃ} - - {roma: [gw, e], kana: ぐぇ} - - {roma: [gw, o], kana: ぐぉ} - - {roma: [ts, a], kana: つぁ} - - {roma: [ts, i], kana: つぃ} - - {roma: [ts, e], kana: つぇ} - - {roma: [ts, o], kana: つぉ} - - {roma: [f, a], kana: ふぁ} - - {roma: [f, i], kana: ふぃ} - - {roma: [f, e], kana: ふぇ} - - {roma: [f, o], kana: ふぉ} - - {roma: [v, u], kana: ヴ} - - {roma: [v, a], kana: ヴぁ} - - {roma: [v, i], kana: ヴぃ} - - {roma: [v, e], kana: ヴぇ} - - {roma: [v, o], kana: ヴぉ} - - {roma: [v, u], kana: ゔ} - - {roma: [v, a], kana: ゔぁ} - - {roma: [v, i], kana: ゔぃ} - - {roma: [v, e], kana: ゔぇ} - - {roma: [v, o], kana: ゔぉ} - - {roma: [v, u], kana: ふ} - - {roma: [v, a], kana: ふぁ} - - {roma: [v, i], kana: ふぃ} - - {roma: [v, e], kana: ふぇ} - - {roma: [v, o], kana: ふぉ} - - {roma: [s, i], kana: すぃ} - - {roma: [z, i], kana: ずぃ} - - {roma: [t, i], kana: てぃ} - - {roma: [ty, u], kana: てゅ} - - {roma: [t, u], kana: とぅ} - - {roma: [d, i], kana: でぃ} - - {roma: [dy, u], kana: でゅ} - - {roma: [d, u], kana: どぅ} - - {roma: [ng, a], kana: か゜} - - {roma: [ng, i], kana: き゜} - - {roma: [ng, u], kana: く゜} - - {roma: [ng, e], kana: け゜} - - {roma: [ng, o], kana: こ゜} - - {roma: [ng, y, e], kana: き゜ぇ} - - {roma: [ng, y, a], kana: き゜ゃ} - - {roma: [ng, y, u], kana: き゜ゅ} - - {roma: [ng, y, o], kana: き゜ょ} - - {roma: [wr, a], kana: ラ} - - {roma: [wr, i], kana: リ} - - {roma: [wr, u], kana: ル} - - {roma: [wr, e], kana: レ} - - {roma: [wr, o], kana: ロ} - - {roma: [q, a], kana: '- あ'} - - {roma: [q, i], kana: '- い'} - - {roma: [q, u], kana: '- う'} - - {roma: [q, e], kana: '- え'} - - {roma: [q, o], kana: '- お'} - - {roma: [NG], kana: ン} - - {roma: [M], kana: ム} - - {roma: [N], kana: ん} - -# Geminations - - {roma: [K, a], kana: っか} - - {roma: [K, i], kana: っき} - - {roma: [K, u], kana: っく} - - {roma: [K, e], kana: っけ} - - {roma: [K, o], kana: っこ} - - {roma: [G, a], kana: っが} - - {roma: [G, i], kana: っぎ} - - {roma: [G, u], kana: っぐ} - - {roma: [G, e], kana: っげ} - - {roma: [G, o], kana: っご} - - {roma: [S, a], kana: っさ} - - {roma: [SH, i], kana: っし} - - {roma: [S, u], kana: っす} - - {roma: [S, e], kana: っせ} - - {roma: [S, o], kana: っそ} - - {roma: [Z, a], kana: っざ} - - {roma: [J, i], kana: っじ} - - {roma: [Z, u], kana: っず} - - {roma: [Z, e], kana: っぜ} - - {roma: [Z, o], kana: っぞ} - - {roma: [T, a], kana: った} - - {roma: [CH, i], kana: っち} - - {roma: [TS, u], kana: っつ} - - {roma: [T, e], kana: って} - - {roma: [T, o], kana: っと} - - {roma: [D, a], kana: っだ} - - {roma: [DJ, i], kana: っぢ} - - {roma: [Z, u], kana: っづ} - - {roma: [D, e], kana: っで} - - {roma: [D, o], kana: っど} - - {roma: [NX, a], kana: っな} - - {roma: [NX, i], kana: っに} - - {roma: [NX, u], kana: っぬ} - - {roma: [NX, e], kana: っね} - - {roma: [NX, o], kana: っの} - - {roma: [H, a], kana: っは} - - {roma: [H, i], kana: っひ} - - {roma: [F, u], kana: っふ} - - {roma: [H, e], kana: っへ} - - {roma: [H, o], kana: っほ} - - {roma: [B, a], kana: っば} - - {roma: [B, i], kana: っび} - - {roma: [B, u], kana: っぶ} - - {roma: [B, e], kana: っべ} - - {roma: [B, o], kana: っぼ} - - {roma: [P, a], kana: っぱ} - - {roma: [P, i], kana: っぴ} - - {roma: [P, u], kana: っぷ} - - {roma: [P, e], kana: っぺ} - - {roma: [P, o], kana: っぽ} - - {roma: [M, a], kana: っま} - - {roma: [M, i], kana: っみ} - - {roma: [M, u], kana: っむ} - - {roma: [M, e], kana: っめ} - - {roma: [M, o], kana: っも} - - {roma: [Y, a], kana: っや} - - {roma: [Y, u], kana: っゆ} - - {roma: [Y, o], kana: っよ} - - {roma: [RX, a], kana: っら} - - {roma: [RX, i], kana: っり} - - {roma: [RX, u], kana: っる} - - {roma: [RX, e], kana: っれ} - - {roma: [RX, o], kana: っろ} - - {roma: [W, a], kana: っわ} - - {roma: [KY, a], kana: っきゃ} - - {roma: [KY, u], kana: っきゅ} - - {roma: [KY, o], kana: っきょ} - - {roma: [GY, a], kana: っぎゃ} - - {roma: [GY, u], kana: っぎゅ} - - {roma: [GY, o], kana: っぎょ} - - {roma: [SH, a], kana: っしゃ} - - {roma: [SH, u], kana: っしゅ} - - {roma: [SH, o], kana: っしょ} - - {roma: [J, a], kana: っじゃ} - - {roma: [J, u], kana: っじゅ} - - {roma: [J, o], kana: っじょ} - - {roma: [CH, a], kana: っちゃ} - - {roma: [CH, u], kana: っちゅ} - - {roma: [CH, o], kana: っちょ} - - {roma: [DJ, a], kana: っぢゃ} - - {roma: [DJ, u], kana: っぢゅ} - - {roma: [DJ, o], kana: っぢょ} - - {roma: [NY, a], kana: っにゃ} - - {roma: [NY, u], kana: っにゅ} - - {roma: [NY, o], kana: っにょ} - - {roma: [HY, a], kana: っひゃ} - - {roma: [HY, u], kana: っひゅ} - - {roma: [HY, o], kana: っひょ} - - {roma: [BY, a], kana: っびゃ} - - {roma: [BY, u], kana: っびゅ} - - {roma: [BY, o], kana: っびょ} - - {roma: [PY, a], kana: っぴゃ} - - {roma: [PY, u], kana: っぴゅ} - - {roma: [PY, o], kana: っぴょ} - - {roma: [MY, a], kana: っみゃ} - - {roma: [MY, u], kana: っみゅ} - - {roma: [MY, o], kana: っみょ} - - {roma: [RXY, a], kana: っりゃ} - - {roma: [RXY, u], kana: っりゅ} - - {roma: [RXY, o], kana: っりょ} - - {roma: [Y, e], kana: っいぇ} - - {roma: [KY, e], kana: っきぇ} - - {roma: [GY, e], kana: っぎぇ} - - {roma: [SH, e], kana: っしぇ} - - {roma: [J, e], kana: っじぇ} - - {roma: [CH, e], kana: っちぇ} - - {roma: [DJ, e], kana: っぢぇ} - - {roma: [NY, e], kana: っにぇ} - - {roma: [HY, e], kana: っひぇ} - - {roma: [BY, e], kana: っびぇ} - - {roma: [PY, e], kana: っぴぇ} - - {roma: [MY, e], kana: っみぇ} - - {roma: [RXY, e], kana: っりぇ} - - {roma: [W, i], kana: っうぃ} - - {roma: [W, e], kana: っうぇ} - - {roma: [W, o], kana: っうぉ} - - {roma: [W, o], kana: っを} - - {roma: [KW, a], kana: っくぁ} - - {roma: [KW, i], kana: っくぃ} - - {roma: [KW, e], kana: っくぇ} - - {roma: [KW, o], kana: っくぉ} - - {roma: [GW, a], kana: っぐぁ} - - {roma: [GW, i], kana: っぐぃ} - - {roma: [GW, e], kana: っぐぇ} - - {roma: [GW, o], kana: っぐぉ} - - {roma: [TS, a], kana: っつぁ} - - {roma: [TS, i], kana: っつぃ} - - {roma: [TS, e], kana: っつぇ} - - {roma: [TS, o], kana: っつぉ} - - {roma: [F, a], kana: っふぁ} - - {roma: [F, i], kana: っふぃ} - - {roma: [F, e], kana: っふぇ} - - {roma: [F, o], kana: っふぉ} - - {roma: [V, u], kana: っヴ} - - {roma: [V, a], kana: っヴぁ} - - {roma: [V, i], kana: っヴぃ} - - {roma: [V, e], kana: っヴぇ} - - {roma: [V, o], kana: っヴぉ} - - {roma: [S, i], kana: っすぃ} - - {roma: [Z, i], kana: っずぃ} - - {roma: [T, i], kana: ってぃ} - - {roma: [TY, u], kana: ってゅ} - - {roma: [T, u], kana: っとぅ} - - {roma: [D, i], kana: っでぃ} - - {roma: [DY, u], kana: っでゅ} - - {roma: [D, u], kana: っどぅ} - - {roma: [NGX, a], kana: っか゜} - - {roma: [NGX, i], kana: っき゜} - - {roma: [NGX, u], kana: っく゜} - - {roma: [NGX, e], kana: っけ゜} - - {roma: [NGX, o], kana: っこ゜} - - {roma: [NGX, Y, e], kana: っき゜ぇ} - - {roma: [NGX, Y, a], kana: っき゜ゃ} - - {roma: [NGX, Y, u], kana: っき゜ゅ} - - {roma: [NGX, Y, o], kana: っき゜ょ} - - {roma: [WR, a], kana: ッラ} - - {roma: [WR, i], kana: ッリ} - - {roma: [WR, u], kana: ッル} - - {roma: [WR, e], kana: ッレ} - - {roma: [WR, o], kana: ッロ} - -entries: - - grapheme: b - phonemes: [b] - - grapheme: cl - phonemes: [q] - - grapheme: d - phonemes: [d] - - grapheme: dh - phonemes: [dh] - - grapheme: dr - phonemes: [dr] - - grapheme: f - phonemes: [f] - - grapheme: g - phonemes: [g] - - grapheme: h - phonemes: [h] - - grapheme: hh - phonemes: [hh] - - grapheme: j - phonemes: [j] - - grapheme: jh - phonemes: [jh] - - grapheme: l - phonemes: [l] - - grapheme: m - phonemes: [M] - - grapheme: n - phonemes: [N] - - grapheme: ng - phonemes: [NG] - - grapheme: p - phonemes: [p] - - grapheme: q - phonemes: [q] - - grapheme: s - phonemes: [s] - - grapheme: sh - phonemes: [sh] - - grapheme: t - phonemes: [t] - - grapheme: th - phonemes: [th] - - grapheme: tr - phonemes: [tr] - - grapheme: vf - phonemes: [vf] - - grapheme: Fr - phonemes: [Fr] - - grapheme: v - phonemes: [v] - - grapheme: w - phonemes: [w] - - grapheme: y - phonemes: [y] - - grapheme: z - phonemes: [z] - - grapheme: zh - phonemes: [zh] \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/JA_CVVX.cs b/JapaneseCVVXPhonemizer/JA_CVVX.cs deleted file mode 100644 index c8bfec36a..000000000 --- a/JapaneseCVVXPhonemizer/JA_CVVX.cs +++ /dev/null @@ -1,1085 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Classic; -using OpenUtau.Api; -using OpenUtau.Classic; -using OpenUtau.Core.G2p; -using OpenUtau.Core.Ustx; -using Serilog; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; -using System.Text.RegularExpressions; -using JA_SBP.Data; -using WanaKanaNet; -using System.Text; - -namespace OpenUtau.Plugin.Builtin { - [Phonemizer("Japanese CVVX Phonemizer", "JA CVVX", "Cadlaxa", language: "JA")] - public class JA_CVVX : SyllableBasedPhonemizer { - - // Let the parent handle YAML Template logic securely - protected override string YamlFileName => "ja-cvvx.yaml"; - protected override string YamlVersion => "1.2"; - protected override byte[] YamlTemplate => Resources.template; - - public Dictionary> WanaKanaDictionary = new Dictionary>(); - public Dictionary> KanaToPhonemeDict = new Dictionary>(); - private Dictionary hiraganaToConsonantAliasMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - public Dictionary> defConsonants = new Dictionary>(); - - protected override string[] GetVowels() => "a i u e o".Split(); - protected override string[] GetConsonants() => "b by ch d dh f g gy h hy j k ky l ly m my n ny ng p py r ry s sh t ts th v w y z zh".Split(); - - // For banks with missing vowels - private readonly Dictionary missingVphonemes = "ax=ah".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2 && parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingVPhonemes = false; - - // For banks with missing custom consonants - private readonly Dictionary missingCphonemes = "".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2 && parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingCPhonemes = false; - - // TIMIT symbols - private readonly Dictionary timitphonemes = "axh=ax,bcl=b,dcl=d,eng=ng,gcl=g,hv=hh,kcl=k,pcl=p,tcl=t".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2 && parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isTimitPhonemes = false; - - private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; - private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; - - private List>> sortedHiraToRomaKeys; - private string[] sortedConsonants; - private string[] sortedVowels; - - public override void SetSinger(USinger singer) { - base.SetSinger(singer); - if (this.singer == null || !this.singer.Loaded) return; - - string file = Path.Combine(singer.Location, YamlFileName); - if (!File.Exists(file)) { - file = Path.Combine(PluginDir, YamlFileName); - } - - if (File.Exists(file)) { - try { - // Extract specifically the custom structures utilizing the parent's tolerant deserializer - var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); - - WanaKanaDictionary.Clear(); - KanaToPhonemeDict.Clear(); - - if (data.wanakana != null) { - foreach (var entry in data.wanakana) { - string key = string.Join("", entry.FromList); - string value = string.Join(" ", entry.ToList); - List phonemeList = entry.FromList; - - if (!WanaKanaDictionary.ContainsKey(key)) { - WanaKanaDictionary.Add(key, new List()); - } - if (!KanaToPhonemeDict.ContainsKey(value)) { - KanaToPhonemeDict.Add(value, phonemeList); - } - WanaKanaDictionary[key].Add(value); - } - } - - defConsonants.Clear(); - hiraganaToConsonantAliasMap.Clear(); - - if (data.consonantGroups != null) { - foreach (var entry in data.consonantGroups) { - string alias = entry.FromList.FirstOrDefault(); - if (string.IsNullOrEmpty(alias)) continue; - - if (!defConsonants.ContainsKey(alias)) { - defConsonants[alias] = new List(); - } - - foreach (string member in entry.ToList) { - defConsonants[alias].Add(member); - if (!Regex.IsMatch(member, "^[a-zA-Z]+$")) { - hiraganaToConsonantAliasMap[member] = alias; - } - } - } - UpdateHiraganaToConsonantMap(); - } - - foreach (var kvp in yamlFallbacks) { - missingVphonemes[kvp.Key] = kvp.Value; - } - - } catch (Exception ex) { - Log.Error(ex, $"Failed to parse custom JA_CVVX fields from {file}"); - } - } - } - - public class CustomYAMLData: YAMLData { - public WanaKanaData[] wanakana { get; set; } = Array.Empty(); - public ConsonantGroups[] consonantGroups { get; set; } = Array.Empty(); - } - - public class ConsonantGroups { - public object group { get; set; } - public object members { get; set; } - - public List FromList { - get { - if (group is string s) return new List { s }; - if (group is IEnumerable list) return list.Select(x => x.ToString()).ToList(); - return new List(); - } - } - - public List ToList { - get { - if (members is string s) return new List { s }; - if (members is IEnumerable list) return list.Select(x => x.ToString()).ToList(); - return new List(); - } - } - } - - public class WanaKanaData { - public object roma { get; set; } - public object kana { get; set; } - - public List FromList { - get { - if (roma is string s) return new List { s }; - if (roma is IEnumerable list) return list.Select(x => x.ToString()).ToList(); - return new List(); - } - } - - public List ToList { - get { - if (kana is string s) return new List { s }; - if (kana is IEnumerable list) return list.Select(x => x.ToString()).ToList(); - return new List(); - } - } - } - - // Parent handles dict replacements, rely on its inheritance - private string ReplacePhoneme(string phoneme, int tone) { - if (dictionaryReplacements.TryGetValue(phoneme, out var replaced)) { - return replaced; - } - if (HasOto(phoneme, tone) || HasOto(ValidateAlias(phoneme), tone)) { - return phoneme; - } - return phoneme; - } - - protected override string[] GetSymbols(Note note) { - string[] original = base.GetSymbols(note); - if (tails.Contains(note.lyric)) { - return new string[] { note.lyric }; - } - - sortedHiraToRomaKeys = KanaToPhonemeDict.OrderByDescending(k => k.Key.Length).ToList(); - sortedConsonants = consonants.OrderByDescending(c => c.Length).ToArray(); - sortedVowels = vowels.OrderByDescending(v => v.Length).ToArray(); - - if (original == null || original.Length == 0 || string.IsNullOrEmpty(original[0])) { - string lyric = note.lyric.Trim().ToLowerInvariant(); - string romaji = ""; - - if (Regex.IsMatch(lyric, @"[\p{IsHiragana}\p{IsKatakana}]+")) { - int ja = 0; - while (ja < lyric.Length) { - string match = null; - - foreach (var kv in sortedHiraToRomaKeys) { - if (ja + kv.Key.Length <= lyric.Length && lyric.Substring(ja, kv.Key.Length) == kv.Key) { - match = kv.Key; - romaji += string.Join("", kv.Value); - ja += kv.Key.Length; - break; - } - } - - if (match == null) { - romaji += WanaKana.ToRomaji(lyric[ja].ToString()); - ja++; - } - } - } else { - romaji = lyric; - } - - List split = new List(); - int ii = 0; - while (ii < romaji.Length) { - string match = null; - - foreach (var cons in sortedConsonants) { - if (romaji.Substring(ii).StartsWith(cons)) { - match = cons; - split.Add(cons); - ii += cons.Length; - break; - } - } - - if (match != null) continue; - - foreach (var vow in sortedVowels) { - if (romaji.Substring(ii).StartsWith(vow)) { - match = vow; - split.Add(vow); - ii += vow.Length; - break; - } - } - - if (match == null) { - split.Add(romaji[ii].ToString()); - ii++; - } - } - original = split.ToArray(); - } - - // Note: The previous massive Merging & Splitting Replacement loop has been removed - // since the parent class intrinsically applies all YAML regex replacements to the array - // returned by GetSymbols() via `ApplyReplacements()`. - - List finalProcessedPhonemes = new List(); - string[] tr = new[] { "tr" }; - string[] dr = new[] { "dr" }; - string[] wh = new[] { "wh" }; - string[] av_c = new[] { "al", "am", "an", "ang", "ar" }; - string[] ev_c = new[] { "el", "em", "en", "eng", "err" }; - string[] iv_c = new[] { "il", "im", "in", "ing", "ir" }; - string[] ov_c = new[] { "ol", "om", "on", "ong", "or" }; - string[] uv_c = new[] { "ul", "um", "un", "ung", "ur" }; - var consonatsV1 = new List { "l", "m", "n", "r" }; - var consonatsV2 = new List { "mm", "nn", "ng" }; - - List vowel3S = new List(); - foreach (string V1 in vowels) { - foreach (string C1 in consonatsV1) { vowel3S.Add($"{V1}{C1}"); } - } - - List vowel4S = new List(); - foreach (string V1 in vowels) { - foreach (string C1 in consonatsV2) { vowel4S.Add($"{V1}{C1}"); } - } - - IEnumerable phonemes = original; - foreach (string s in phonemes) { - switch (s) { - case var str when dr.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "jh", s[1].ToString() }); - break; - case var str when tr.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "ch", s[1].ToString() }); - break; - case var str when wh.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "hh", s[1].ToString() }); - break; - case var str when av_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "aa", s[1].ToString() }); - break; - case var str when ev_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "eh", s[1].ToString() }); - break; - case var str when iv_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "iy", s[1].ToString() }); - break; - case var str when ov_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "ao", s[1].ToString() }); - break; - case var str when uv_c.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { "uw", s[1].ToString() }); - break; - case var str when vowel3S.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s[2].ToString() }); - break; - case var str when vowel4S.Contains(str) && !HasOto($"{str}", note.tone) && !HasOto(ValidateAlias(str), note.tone): - finalProcessedPhonemes.AddRange(new string[] { s.Substring(0, 2), s.Substring(2, 2) }); - break; - default: - finalProcessedPhonemes.Add(s); - break; - } - } - return finalProcessedPhonemes.Where(p => p != null).ToArray(); - } - - private string ToHiragana(string alias, int tone) { - var convertedHiragana = ""; - int i = 0; - - while (i < alias.Length) { - bool foundMatch = false; - - var potentialRomajiKeys = WanaKanaDictionary - .Keys - .Where(key => alias.Length >= i + key.Length && - alias.Substring(i, key.Length).Equals(key, StringComparison.Ordinal)) - .OrderByDescending(key => key.Length) - .ToList(); - - foreach (var romajiKey in potentialRomajiKeys) { - var kanaValues = WanaKanaDictionary[romajiKey]; - string selectedKana = null; - - foreach (var kana in kanaValues) { - var validatedKana = kana; - if (HasOto(validatedKana, tone) || HasOto(ValidateAlias(validatedKana), tone)) { - selectedKana = validatedKana; - break; - } - } - - if (selectedKana == null) { - selectedKana = (kanaValues[0]); - } - - convertedHiragana += selectedKana; - i += romajiKey.Length; - foundMatch = true; - break; - } - - if (!foundMatch) { - convertedHiragana += alias[i]; - i++; - } - } - return convertedHiragana; - } - - private void UpdateHiraganaToConsonantMap() { - hiraganaToConsonantAliasMap.Clear(); - - foreach (var entry in defConsonants) { - string preferredConsonant = entry.Key; - List members = entry.Value; - - foreach (string member in members) { - if (member != preferredConsonant) { - if (!string.IsNullOrEmpty(member) && !hiraganaToConsonantAliasMap.ContainsKey(member)) { - hiraganaToConsonantAliasMap[member] = preferredConsonant; - } - } - } - } - } - - private string GetVC(string hiraganaSyllable, string baseConsonant) { - if (string.IsNullOrWhiteSpace(hiraganaSyllable)) return baseConsonant; - if (hiraganaToConsonantAliasMap.TryGetValue(hiraganaSyllable.Trim(), out string alias)) { - return alias; - } - return baseConsonant; - } - - protected override List ProcessSyllable(Syllable syllable) { - var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); - var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; - string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - List vowelsList = new List { ReplacePhoneme(syllable.v, syllable.vowelTone) }; - string v = ReplacePhoneme(syllable.v, syllable.vowelTone); - string basePhoneme; - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string[] CurrentWordCc = syllable.CurrentWordCc.Select(ReplacePhoneme).ToArray(); - string[] PreviousWordCc = syllable.PreviousWordCc.Select(ReplacePhoneme).ToArray(); - int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - - foreach (var entry in missingVphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isMissingVPhonemes = true; - break; - } - } - - foreach (var entry in missingCphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isMissingCPhonemes = true; - break; - } - } - - foreach (var entry in timitphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isTimitPhonemes = true; - break; - } - } - - var hv = $"- {ToHiragana(v, syllable.vowelTone)}"; - - if (syllable.IsStartingV) { - if (HasOto(hv, syllable.vowelTone) || HasOto(ValidateAlias(hv), syllable.vowelTone)) { - basePhoneme = AliasFormat(ToHiragana(v, syllable.vowelTone), "startingV", syllable.vowelTone, ""); - } else if (HasOto(ToHiragana(v, syllable.vowelTone), syllable.vowelTone) || HasOto(ValidateAlias(ToHiragana(v, syllable.vowelTone)), syllable.vowelTone)) { - basePhoneme = AliasFormat(ToHiragana(v, syllable.vowelTone), "startingV", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); - } - } - else if (syllable.IsVV) { - if (!CanMakeAliasExtension(syllable)) { - basePhoneme = $"{prevV} {ToHiragana(v, syllable.vowelTone)}"; - int tone = syllable.vowelTone; - string hiraganaV = ToHiragana(v, tone); - string[] candidates = { $"{prevV} {hiraganaV}", $"{prevV} {v}" }; - string[] candidates2 = { AliasFormat(hiraganaV, "vv_start", tone, ""), AliasFormat(v, "vv_start", tone, "") }; - bool foundMatch = false; - for (int i = 0; i < candidates.Length; i++) { - string c1 = candidates[i]; - string c2 = candidates2[i]; - - if (HasOto(c1, tone) || HasOto(ValidateAlias(c1), tone)) { - basePhoneme = c1; - foundMatch = true; - break; - } - else if (HasOto(c2, tone) || HasOto(ValidateAlias(c2), tone)) { - basePhoneme = c2; - foundMatch = true; - break; - } - } - if (!foundMatch) { - if (HasOto(ToHiragana($"{prevV}{v}", syllable.vowelTone), syllable.vowelTone) || HasOto(ValidateAlias(ToHiragana($"{prevV}{v}", syllable.vowelTone)), syllable.vowelTone)) { - basePhoneme = ToHiragana($"{prevV}{v}", syllable.vowelTone); - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else { - basePhoneme = AliasFormat($"{v}", "vv_start", syllable.vowelTone, ""); - } - } - } else if (HasOto($"{ToHiragana(v, syllable.vowelTone)}", syllable.vowelTone) && HasOto(ValidateAlias($"{ToHiragana(v, syllable.vowelTone)}"), syllable.vowelTone)) { - basePhoneme = ToHiragana(v, syllable.vowelTone); - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone)) { - basePhoneme = v; - } else { - basePhoneme = null; - } - } else if (syllable.IsStartingCVWithOneConsonant) { - var rcv = $"- {cc[0]} {v}"; - var rcv1 = $"- {cc[0]}{v}"; - var crv = $"{cc[0]} {v}"; - var hcv = ToHiragana($"{cc[0]}{v}", syllable.tone); - - if (HasOto($"- {hcv}", syllable.vowelTone) || HasOto(ValidateAlias($"- {hcv}"), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynStart", syllable.vowelTone, ""); - } else if (HasOto(rcv, syllable.vowelTone) || HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) || HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); - } else if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, $"- {cc[0]}", ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); - } else { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); - } - } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { - var rccv = $"- {string.Join("", cc)} {v}"; - var rccv1 = $"- {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - var crv1 = $"{cc.Last()}{v}"; - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - var hccv = ToHiragana($"{string.Join("", cc)}{v}", syllable.tone); - var hcv = ToHiragana($"{cc.Last()}{v}", syllable.tone); - - if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat(hccv, "dynStart", syllable.vowelTone, ""); - lastC = 0; - } else if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); - lastC = 0; - } else { - if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - - for (var i = cc.Length; i > 1; i--) { - if (!ccvException.Contains(cc[0])) { - if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, "")))) { - firstC = i - 1; - } - } - break; - } - - if (phonemes.Count == 0) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""), ValidateAlias(AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, ""))); - } - } - - for (var i = firstC; i < cc.Length - 1; i++) { - var cv = $"{cc.Last()} {v}"; - if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[i])) { - if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - } else { - var vcv = $"{prevV} {cc[0]}{v}"; - var vccv = $"{prevV} {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - var hccv = ToHiragana($"{string.Join("", cc)}{v}", syllable.tone); - var hcv = ToHiragana($"{cc.Last()}{v}", syllable.tone); - var hvcv = $"{prevV} {ToHiragana($"{cc[0]}{v}", syllable.tone)}"; - var hvccv = $"{prevV} {ToHiragana($"{string.Join("", cc)}{v}", syllable.tone)}"; - - if (syllable.IsVCVWithOneConsonant && (HasOto(hvcv, syllable.vowelTone) || HasOto(ValidateAlias(hvcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = hvcv; - } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) || HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv; - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(hvccv, syllable.vowelTone) || HasOto(ValidateAlias(hvccv), syllable.vowelTone) && prevWordConsonantsCount == 0)) { - basePhoneme = hvccv; - lastC = 0; - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) || HasOto(ValidateAlias(vccv), syllable.vowelTone) && prevWordConsonantsCount == 0)) { - basePhoneme = vccv; - lastC = 0; - } else { - var cv = $"{cc.Last()}{v}"; - - if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - - for (var i = firstC; i < cc.Length - 1; i++) { - if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[i])) { - if (HasOto(hccv, syllable.vowelTone) || HasOto(ValidateAlias(hccv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hccv, "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } else if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(hcv, syllable.vowelTone) || HasOto(ValidateAlias(hcv), syllable.vowelTone)) { - basePhoneme = AliasFormat(hcv, "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - - if (basePhoneme.Contains("-")) { - for (int clusterLength = 3; clusterLength >= 2; clusterLength--) { - if (clusterLength > cc.Length) continue; - - var cluster = new string[clusterLength]; - Array.Copy(cc, 0, cluster, 0, clusterLength); - - var consonantPatterns = new List(); - - if (clusterLength >= 3) { - consonantPatterns.Add($"{cluster[0]} {cluster[1]}{cluster[2]}"); - consonantPatterns.Add($"{cluster[0]}{cluster[1]} {cluster[2]}"); - consonantPatterns.Add($"{cluster[0]} {cluster[1]} {cluster[2]}"); - } else if (clusterLength == 2) { - consonantPatterns.Add($"{cluster[0]} {cluster[1]}"); - consonantPatterns.Add($"{cluster[0]}{cluster[1]}"); - } - - foreach (var consPattern in consonantPatterns) { - string[] endPatterns = { "-", $" -" }; - foreach (var end in endPatterns) { - string endingcc = $"{consPattern}{end}"; - - if (HasOto(endingcc, syllable.tone)) { - basePhoneme = endingcc; - lastC = 0; - goto FoundMatch; - } - } - } - } - } - - FoundMatch:; - - for (var i = lastC + 1; i >= 0; i--) { - var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; - var vc = $"{prevV} {cc[0]}"; - bool CCV = false; - - if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { - if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { - CCV = true; - } - } - - string preferredC = GetVC(ToHiragana($"{string.Join("", cc)}{v}", syllable.tone), cc[0]); - var hvc = $"{prevV} {preferredC}"; - - if (i == 0 && !HasOto(vc, syllable.tone)) { - break; - } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { - phonemes.Add(vcc); - firstC = 1; - break; - } else if (HasOto(hvc, syllable.tone) || HasOto(ValidateAlias(hvc), syllable.tone)) { - phonemes.Add(hvc); - break; - } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { - phonemes.Add(vc); - break; - } - } - } - } - - for (var i = firstC; i < lastC; i++) { - var cc1 = $"{string.Join(" ", cc.Skip(i))}"; - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - - if (!HasOto(cc1, syllable.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - cc1 = AliasFormat($"{cc[i]}", "cc_endB", syllable.vowelTone, ""); - } - - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]}{string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]}{string.Join("", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}"; - lastC = i; - } - - if (i + 1 < lastC) { - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - if (!HasOto(cc1, syllable.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - cc1 = AliasFormat($"{cc[i]}", "cc_endB", syllable.vowelTone, ""); - } - if (!HasOto(cc1, syllable.tone)) cc1 = ValidateAlias(cc1); - - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join(" ", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]}{string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]}{string.Join("", cc.Skip(i + 1))}"; - lastC = i; - } - if (HasOto($"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]}{string.Join(" ", cc.Skip(i + 1))}"; - lastC = i; - } - - if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { - phonemes.Add(cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1, ValidateAlias(cc1))) { - if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { - i++; - } - } else { - if (PreviousWordCc.Contains(cc1) == CurrentWordCc.Contains(cc1)) { - cc1 = ValidateAlias(cc1); - } else { - TryAddPhoneme(phonemes, syllable.tone, cc1, cc[i], ValidateAlias(cc[i])); - } - } - } else { - TryAddPhoneme(phonemes, syllable.tone, cc1); - } - } - - phonemes.Add(basePhoneme); - return phonemes; - } - - protected override List ProcessEnding(Ending ending) { - string prevV = ReplacePhoneme(ending.prevV, ending.tone); - string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); - string v = ReplacePhoneme(ending.prevV, ending.tone); - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - - if (tails.Contains(ending.prevV)) { - return new List(); - } - - if (ending.IsEndingV) { - var vR = $"{prevV} -"; - var vR1 = $"{prevV} R"; - var vR2 = $"{prevV}-"; - if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR1, ending.tone) || HasOto(ValidateAlias(vR1), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { - phonemes.Add(AliasFormat($"{prevV}", "ending", ending.tone, "")); - } - } else if (ending.IsEndingVCWithOneConsonant) { - var vc = $"{prevV} {cc[0]}"; - var vcr = $"{prevV} {cc[0]}-"; - var vcr2 = $"{prevV}{cc[0]} -"; - var vcr3 = $"{prevV} {cc[0]} -"; - var vcr4 = $"{prevV}{cc[0]}-"; - if (!RomajiException.Contains(cc[0])) { - if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || (HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone))) { - phonemes.Add(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "")); - } else if (HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - phonemes.Add(vcr3); - } else if (HasOto(vcr4, ending.tone) && HasOto(ValidateAlias(vcr4), ending.tone)) { - phonemes.Add(vcr4); - } else if (HasOto(vc, ending.tone) && HasOto(ValidateAlias(vc), ending.tone)) { - phonemes.Add(vc); - if (vc.Contains(cc[0])) { - phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "")); - } - } else { - phonemes.Add(vc); - if (vc.Contains(cc[0])) { - phonemes.Add(AliasFormat($"{cc[0]}", "ending", ending.tone, "")); - } - } - } - } else { - for (var i = lastC; i >= 0; i--) { - var vr = $"{v} -"; - var vr1 = $"{v} R"; - var vr2 = $"{v}-"; - var vcc = $"{v} {string.Join("", cc.Take(2))}-"; - var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} -"; - var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; - var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; - var vc = $"{v} {cc[0]}"; - - if (!RomajiException.Contains(cc[0])) { - if (i == 0) { - if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { - phonemes.Add(AliasFormat($"{v}", "ending", ending.tone, "")); - } - break; - } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc); - firstC = 1; - break; - } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc2); - firstC = 1; - break; - } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc3); - if (vcc3.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "")); - } - } - firstC = 1; - break; - } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { - phonemes.Add(vcc4); - if (vcc4.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - phonemes.Add(AliasFormat($"{cc.Last()}", "ending", ending.tone, "")); - } - } - firstC = 1; - break; - } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) - || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) - || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) - || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { - phonemes.Add(vc); - break; - } else { - phonemes.Add(vc); - break; - } - } - } - for (var i = firstC; i < lastC; i++) { - var cc1 = $"{cc[i]} {cc[i + 1]}"; - if (i < cc.Length - 2) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); - if (!HasOto(cc2, ending.tone)) cc2 = ValidateAlias(cc2); - - if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { - cc2 = AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, ""); - } - if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); - - if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone))) { - phonemes.Add(cc1); - } else if ((HasOto(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""), ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}-", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"), ending.tone)))) { - phonemes.Add(AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "")); - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}-", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}-"))) { - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]}{cc[i + 2]}", ValidateAlias($"{cc[i + 1]}{cc[i + 2]}"))) { - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "")); - i++; - } else { - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} -", ValidateAlias($"{cc[i]} -")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 2], ValidateAlias(cc[i + 2]), $"{cc[i + 2]} -", ValidateAlias($"{cc[i + 2]} -")); - i++; - } - } else { - if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); - if (!HasOto(cc1, ending.tone)) cc1 = $"{cc[i]} {cc[i + 1]}"; - if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); - - if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - cc1 = AliasFormat($"{cc[i]}", "cc_endB", ending.tone, ""); - } - if (!HasOto(cc1, ending.tone)) cc1 = ValidateAlias(cc1); - - if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} -", ValidateAlias($"{cc[i + 1]} -"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } - } - } - } - return phonemes; - } - - private string AliasFormat(string alias, string type, int tone, string prevV) { - var aliasFormats = new Dictionary { - { "dynStart", new string[] { "" } }, - { "dynMid", new string[] { "" } }, - { "dynMid_vv", new string[] { "" } }, - { "dynEnd", new string[] { "" } }, - { "startingV", new string[] { "-", "- ", "_", "" } }, - { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, - { "vv", new string[] { "", "_", "-", "- " } }, - { "vv_start", new string[] { "* ", "_", "", "" } }, - { "cv", new string[] { "-", "", "- ", "_" } }, - { "cvStart", new string[] { "-", "- ", "_" } }, - { "ending", new string[] { " R", "-", " -" } }, - { "ending_mix", new string[] { "-", " -", "R", " R", "_", "--" } }, - { "cc", new string[] { "", "-", "- ", "_" } }, - { "cc_start", new string[] { "- ", "-", "_" } }, - { "cc_end", new string[] { " -", "-", "" } }, - { "cc_inB", new string[] { "_", "-", "- " } }, - { "cc_endB", new string[] { "_", "-", " -" } }, - { "cc_mix", new string[] { " -", " R", "-", "", "_", "- ", "-" } }, - { "cc1_mix", new string[] { "", " -", "-", " R", "_", "- ", "-" } }, - }; - - if (!aliasFormats.ContainsKey(type) && !type.Contains("dynamic")) { - return alias; - } - - if (type.Contains("dynStart")) { - string consonant = ""; - string vowel = ""; - if (alias.Contains(" ")) { - var parts = alias.Split(' '); - consonant = parts[0]; - vowel = parts[1]; - } else { - consonant = alias; - } - - var dynamicVariations = new List { - $"- {consonant}{vowel}", - $"- {consonant} {vowel}", - $"-{consonant} {vowel}", - $"-{consonant}{vowel}", - $"-{consonant}_{vowel}", - $"- {consonant}_{vowel}", - }; - foreach (var variation in dynamicVariations) { - if (HasOto(variation, tone) || HasOto(ValidateAlias(variation), tone)) { - return 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}", - $"{consonant} {vowel}", - $"{consonant}_{vowel}", - }; - foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { - return 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} -", - $"{vowel} {consonant}-", - $"{vowel}{consonant}-", - $"{vowel} {consonant} -", - }; - foreach (var variation1 in dynamicVariations1) { - if (HasOto(variation1, tone) || HasOto(ValidateAlias(variation1), tone)) { - return variation1; - } - } - } - - var formatsToTry = aliasFormats[type]; - int counter = 0; - foreach (var format in formatsToTry) { - string aliasFormat; - if (type.Contains("mix") && counter < 4) { - aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; - counter++; - } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { - aliasFormat = $"{alias}{format}"; - } else { - aliasFormat = $"{format}{alias}"; - } - - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { - return aliasFormat; - } - } - return alias; - } - - protected override string ValidateAlias(string alias) { - if (isMissingVPhonemes) { - foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isMissingCPhonemes) { - foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isTimitPhonemes) { - foreach (var fb in timitphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - return alias; - } - - bool PhonemeIsPresent(string alias, string phoneme) { - if (string.IsNullOrEmpty(alias) || string.IsNullOrEmpty(phoneme)) return false; - if (alias == phoneme) return true; - return alias.EndsWith(phoneme); - } - - protected override bool NoGap => true; - - protected override double GetTransitionMultiplier(string alias) { - return 1.0; - } - - protected override double GetTransitionBasicLengthMs(string alias, int tone, PhonemeAttributes attr) { - double otoLength = GetTransitionBasicLengthMsByOto(alias, tone, attr); - - var tokens = alias.Split(' ') - .Select(t => t.Replace("-", "").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)) { - return GetTransitionBasicLengthMsByConstant() * value; - } - } - - return otoLength; - } - } -} \ No newline at end of file diff --git a/JapaneseCVVXPhonemizer/JA_CVVX.csproj b/JapaneseCVVXPhonemizer/JA_CVVX.csproj deleted file mode 100644 index 74eb43b38..000000000 --- a/JapaneseCVVXPhonemizer/JA_CVVX.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - JA_SBP - enable - enable - - - - - - - - - Resources.resx - True - True - - - - - - Resources.Designer.cs - ResXFileCodeGenerator - - - - diff --git a/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs b/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs deleted file mode 100644 index a2852dcfe..000000000 --- a/JapaneseCVVXPhonemizer/SyllableBasedPhonemizer.cs +++ /dev/null @@ -1,1780 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using OpenUtau.Api; -using OpenUtau.Core.Ustx; -using System.Linq; -using System.IO; -using Serilog; -using System.Threading.Tasks; -using static OpenUtau.Api.Phonemizer; -using System.Collections; - -namespace OpenUtau.Plugin.Builtin { - /// - /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; - /// - /// - Supports dictionary; - /// - Automatically align phonemes to notes; - /// - Supports syllable extension; - /// - Automatically calculates transition phonemes length, with constants by default, - /// but there is a pre-created function to use Oto value; - /// - The transition length is scaled based on Tempo and note length. - /// - /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". - /// - /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], - /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". - /// - /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". - /// - /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, - /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). - /// - public abstract class SyllableBasedPhonemizer : Phonemizer { - - /// - /// Syllable is [V] [C..] [V] - /// - protected struct Syllable { - /// - /// vowel from previous syllable for VC - /// - public string prevV; - /// - /// CCs, may be empty - /// - public string[] cc; - /// - /// "base" note. May not actually be vowel, if only consonants way provided - /// - public string v; - /// - /// Start position for vowel. All VC CC goes before this position - /// - public int position; - /// - /// previous note duration, i.e. this is container for VC and CC notes - /// - public int duration; - /// - /// Tone for VC and CC - /// - public int tone; - /// - /// Other phoneme attributes for VC and CC - /// - public PhonemeAttributes[] attr; - /// - /// tone for base "vowel" phoneme - /// - public int vowelTone; - /// - /// Other phoneme attributes for base "vowel" phoneme - /// - public PhonemeAttributes[] vowelAttr; - - /// - /// 0 if no consonants are taken from previous word; - /// 1 means first one is taken from previous word, etc. - /// - public int prevWordConsonantsCount; - - /// - /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. - /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. - /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible - /// - public bool canAliasBeExtended; - - // helpers - public bool IsStartingV => prevV == "" && cc.Length == 0; - public bool IsVV => prevV != "" && cc.Length == 0; - - public bool IsStartingCV => prevV == "" && cc.Length > 0; - public bool IsVCV => prevV != "" && cc.Length > 0; - - public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; - public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; - - public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; - public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; - - public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); - public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; - } - } - - protected struct Ending { - /// - /// vowel from the last syllable to make VC - /// - public string prevV; - /// - /// actuall CC at the ending - /// - public string[] cc; - /// - /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) - /// - public string tail; - public bool HasTail => !string.IsNullOrEmpty(tail); - /// - /// last note position + duration, all phonemes must be less than this - /// - public int position; - /// - /// last syllable length, max container for all VC CC C- - /// - public int duration; - /// - /// the tone from last syllable, for all ending phonemes - /// - public int tone; - /// - /// Other phoneme attributes from last syllable - /// - public PhonemeAttributes[] attr; - - // helpers - public bool IsEndingV => cc.Length == 0; - public bool IsEndingVC => cc.Length > 0; - public bool IsEndingVCWithOneConsonant => cc.Length == 1; - public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; - } - } - - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { - error = ""; - var mainNote = notes[0]; - if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return MakeForcedAliasResult(mainNote); - } - if (hasDictionary && isDictionaryLoading) { - return MakeSimpleResult(""); - } - - runtimeGlides.Clear(); - - var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); - if (syllables == null) { - return HandleError(); - } - - var phonemes = new List(); - int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI - - foreach (var syllable in syllables) { - var modifiedSyllable = ApplyBoundaryReplacements(syllable); - - if (tails.Contains(modifiedSyllable.v)) { - var ending = new Ending { - prevV = modifiedSyllable.prevV, - cc = modifiedSyllable.cc, - tail = modifiedSyllable.v, - position = modifiedSyllable.position, - duration = modifiedSyllable.duration, - tone = modifiedSyllable.tone, - attr = modifiedSyllable.attr - }; - - var endingPhonemes = ProcessEnding(ending); - - if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); - globalPhonemeIndex += endingPhonemes.Count; - } - continue; - } - - var syllablePhonemes = ProcessSyllable(modifiedSyllable); - var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); - int currentSyllablePhonemeCount = syllablePhonemes.Count; - - var basePhoneme = madePhonemes.LastOrDefault(); - string baseAlias = basePhoneme.phoneme ?? ""; - if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || - vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { - - string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); - if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { - int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); - madePhonemes.Add(new Phoneme { - phoneme = sustainData.sustain, - position = basePhoneme.position + offsetTicks, - index = globalPhonemeIndex + currentSyllablePhonemeCount - }); - currentSyllablePhonemeCount++; - } - } - phonemes.AddRange(madePhonemes); - globalPhonemeIndex += currentSyllablePhonemeCount; - } - - if (!nextNeighbour.HasValue) { - var tryEnding = MakeEnding(notes); - if (tryEnding.HasValue) { - var ending = tryEnding.Value; - - if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { - ending.tail = nextNeighbour.Value.lyric; - } - - var modifiedEnding = ApplyBoundaryReplacements(ending); - var endingPhonemes = ProcessEnding(modifiedEnding); - - if (endingPhonemes != null) { - 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 = finalPhonemes - }; - } - - protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { - int noteIndex = 0; - for (int i = 0; i < phonemes.Count; i++) { - var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var phoneme = phonemes[i]; - while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { - noteIndex++; - } - - 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); - validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); - - phoneme.phoneme = validatedAlias; - } else { - phoneme.phoneme = null; - phoneme.position = 0; - } - - phonemes[i] = phoneme; - } - return phonemes.ToArray(); - } - - private Result HandleError() { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = error - } - } - }; - } - - protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = - new YamlDotNet.Serialization.DeserializerBuilder() - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); - - public override void SetSinger(USinger singer) { - if (this.singer != singer) { - this.singer = singer; - dictionaries.Clear(); - - if (this.singer == null || !this.singer.Loaded) { - return; - } - - if (string.IsNullOrEmpty(YamlFileName)) { - if (backupVowels != null) this.vowels = backupVowels; - else this.vowels = GetVowels(); - - if (backupConsonants != null) this.consonants = backupConsonants; - else this.consonants = GetConsonants(); - - if (backupDictionaryReplacements != null) { - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) { - dictionaryReplacements[kvp.Key] = kvp.Value; - } - } - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - return; - } - - // file paths - string globalFile = Path.Combine(PluginDir, YamlFileName); - string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) - ? Path.Combine(singer.Location, YamlFileName) - : null; - - // Local helper function to update and backup YAML files safely - void UpdateYamlIfNeeded(string filePath, bool isGlobal) { - if (string.IsNullOrEmpty(filePath)) return; - - bool shouldWriteTemplate = false; - bool shouldBackupOldFile = false; - string currentVersion = "unknown"; - - if (File.Exists(filePath)) { - if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { - try { - var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); - currentVersion = checkData?.version?.Trim() ?? ""; - - // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion - if (string.IsNullOrEmpty(currentVersion)) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } else if (Version.TryParse(currentVersion, out Version currV) && - Version.TryParse(YamlVersion, out Version targetV)) { - if (currV < targetV) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { - // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } catch (Exception ex) { - Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); - return; - } - } - } else if (isGlobal && YamlTemplate != null) { - shouldWriteTemplate = true; - } - - if (shouldBackupOldFile && File.Exists(filePath)) { - try { - // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml - string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; - string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); - - if (File.Exists(backupFile)) File.Delete(backupFile); - File.Move(filePath, backupFile); - Log.Information($"Old {YamlFileName} backed up to {backupFile}"); - } catch (Exception e) { - Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); - return; - } - } - - if (shouldWriteTemplate) { - try { - File.WriteAllBytes(filePath, YamlTemplate); - Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); - } catch (Exception e) { - Log.Error(e, $"Failed to write template to {filePath}"); - } - } - } - - UpdateYamlIfNeeded(globalFile, true); - UpdateYamlIfNeeded(singerFile, false); - - // add to parsing list (Global first, Singer second) - var filesToParse = new List(); - if (File.Exists(globalFile)) filesToParse.Add(globalFile); - if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); - - // backups of hardcoded defaults exist - if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); - if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); - if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); - if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); - if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); - - // reset live arrays/lists back to defaults before stacking - vowels = backupVowels; - consonants = backupConsonants; - tails = "-".Split(','); - - fricative = Array.Empty(); - aspirate = Array.Empty(); - semivowel = Array.Empty(); - liquid = Array.Empty(); - nasal = Array.Empty(); - stop = Array.Empty(); - tap = Array.Empty(); - affricate = Array.Empty(); - - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; - - diphthongTails.Clear(); - foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; - - diphthongSplits.Clear(); - foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; - - mergingReplacements.Clear(); - splittingReplacements.Clear(); - yamlFallbacks.Clear(); - PhonemeOverrides.Clear(); - if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); - vowelSustains.Clear(); - foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; - - // parse the files sequentially (Singer configs seamlessly overwrite global configs) - foreach (var file in filesToParse) { - try { - var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); - - var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); - vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); - - var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tails = yamlTails.Concat(tails).Distinct().ToArray(); - - if (data?.isglides != null) enableGlides = data.isglides.Value; - - var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); - fricative = yFricative.Concat(fricative).Distinct().ToArray(); - var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); - var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); - semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); - var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); - liquid = yLiquid.Concat(liquid).Distinct().ToArray(); - var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); - nasal = yNasal.Concat(nasal).Distinct().ToArray(); - var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); - stop = yStop.Concat(stop).Distinct().ToArray(); - var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tap = yTap.Concat(tap).Distinct().ToArray(); - var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - affricate = yAffricate.Concat(affricate).Distinct().ToArray(); - - var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) - .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); - consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); - - // DIPHTHONG AUTO-TAIL DETECTION - var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); - - foreach (var d in yamlDiphthongs) { - if (!diphthongSplits.ContainsKey(d)) { - foreach (var tail in dynamicTails) { - if (d.EndsWith(tail) && d != tail) { - diphthongTails[d] = tail; - break; - } - } - } - } - - // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) - if (data?.timings != null) { - foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; - } - - if (data?.replacements != null) { - var localMerge = new List(); - var localSplit = new List(); - string GetFromKey(object fromObj) { - if (fromObj is string s) return s; - if (fromObj is System.Collections.IEnumerable e) { - return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); - } - return ""; - } - - foreach (var rawReplacement in data.replacements) { - string fromKey = GetFromKey(rawReplacement.from); - mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - - if (rawReplacement.from is string fromStr) { - dictionaryReplacements.Remove(fromStr); - dictionaryReplacements.Remove(fromStr.ToLower()); - dictionaryReplacements.Remove(fromStr.ToUpper()); - } - - List fromList = rawReplacement.FromList; - List toList = rawReplacement.ToList; - object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); - object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); - - var cleanReplacement = new Replacement { - from = parsedFrom, - to = parsedTo, - where = rawReplacement.where - }; - - if (parsedFrom is string fromString) { - if (parsedTo is string toString) { - // Dictionary handles simple 1-to-1 replacements - dictionaryReplacements[fromString] = toString; - } else { - // 1-to-Many goes to Split - localSplit.Add(cleanReplacement); - } - } else { - // Many-to-Any goes to Merge - localMerge.Add(cleanReplacement); - } - } - mergingReplacements.InsertRange(0, localMerge); - splittingReplacements.InsertRange(0, localSplit); - } - - if (data?.fallbacks != null) { - foreach (var df in data.fallbacks) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - yamlFallbacks[df.from] = df.to; - } - } - } - - if (data?.diphthongs != null) { - foreach (var d in data.diphthongs) { - if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { - diphthongTails[d.from] = d.to; - } - } - } - - if (data?.vowelsustains != null) { - foreach (var v in data.vowelsustains) { - if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { - vowelSustains[v.symbol] = (v.sustain, v.offset); - } - } - } - - } catch (Exception ex) { - Log.Error($"Failed to parse {file}: {ex.Message}"); - } - } - - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - } - } - - protected USinger singer; - protected bool hasDictionary => dictionaries.ContainsKey(GetType()); - protected IG2p dictionary => dictionaries[GetType()]; - protected bool isDictionaryLoading => dictionaries[GetType()] == null; - protected double TransitionBasicLengthMs => 100; - - private Dictionary dictionaries = new Dictionary(); - private const string FORCED_ALIAS_SYMBOL = "?"; - private string error = ""; - 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 - /// - /// - protected abstract string[] GetVowels(); - - /// - /// Returns list of consonants. Only needed if there is a dictionary - /// - /// - protected virtual string[] GetConsonants() { - throw new NotImplementedException(); - } - - /// - /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc - /// - /// List of phonemes - protected abstract List ProcessSyllable(Syllable syllable); - - /// - /// phoneme symbols for ending, like, V-, or VC-, or VC+C - /// - protected abstract List ProcessEnding(Ending ending); - - /// - /// simple alias to alias fallback - /// - /// - protected virtual Dictionary GetAliasesFallback() { return null; } - - /// - /// Use to some custom init, if needed - /// - protected virtual void Init() { } - - /// - /// Dictionary name. Must be stored in Dictionaries folder. - /// If missing or can't be read, phonetic input is used - /// - /// - protected virtual string GetDictionaryName() { return null; } - - /// - /// extracts array of phoneme symbols from note. Override for procedural dictionary or something - /// reads from dictionary if provided - /// - /// - /// - protected virtual string[] GetSymbols(Note note) { - string[] getSymbolsRaw(string lyrics) { - if (lyrics == null) { - return new string[0]; - } else return lyrics.Split(" "); - } - - if (tails.Contains(note.lyric)) { - return new string[] { note.lyric }; - } - - if (hasDictionary) { - if (!string.IsNullOrEmpty(note.phoneticHint)) { - return getSymbolsRaw(note.phoneticHint); - } - - var result = new List(); - foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { - var subResult = dictionary.Query(subword); - if (subResult == null) { - subResult = HandleWordNotFound(note); - if (subResult == null) { - return null; - } - } else { - for (int i = 0; i < subResult.Length; i++) { - string phoneme = subResult[i]; - if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { - subResult[i] = replaced; - } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { - subResult[i] = replacedExact; - } - } - } - result.AddRange(subResult); - } - return result.ToArray(); - } else { - return getSymbolsRaw(note.lyric); - } - } - - /// - /// 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. - /// - /// - protected virtual Dictionary GetDictionaryPhonemesReplacement() { - return dictionaryReplacements ?? new Dictionary(); - } - private string[] backupVowels = null; - private string[] backupConsonants = null; - private Dictionary backupDiphthongTails = null; - private Dictionary backupDiphthongSplits = null; - private Dictionary backupDictionaryReplacements = null; - protected Dictionary vowelSustains = new Dictionary(); - private Dictionary backupVowelSustains = null; - - /// - /// separates symbols to syllables, without an ending. - /// - /// - /// - /// - protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - var firstVowelId = vowelIds[0]; - if (notes.Length < vowelIds.Length) { - error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; - return null; - } - - var syllables = new Syllable[vowelIds.Length]; - - // Making the first syllable - if (prevEnding.HasValue) { - var prevEndingValue = prevEnding.Value; - var beginningCc = prevEndingValue.cc.ToList(); - beginningCc.AddRange(symbols.Take(firstVowelId)); - - // If we had a prev neighbour ending, let's take info from it - syllables[0] = new Syllable() { - prevV = prevEndingValue.prevV, - cc = beginningCc.ToArray(), - v = symbols[firstVowelId], - tone = prevEndingValue.tone, - attr = prevEndingValue.attr, - duration = prevEndingValue.duration, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes, - prevWordConsonantsCount = prevEndingValue.cc.Count() - }; - } else { - // there is only empty space before us - syllables[0] = new Syllable() { - prevV = "", - cc = symbols.Take(firstVowelId).ToArray(), - v = symbols[firstVowelId], - tone = notes[0].tone, - attr = notes[0].phonemeAttributes, - duration = -1, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes - }; - } - - // normal syllables after the first one - var noteI = 1; - var ccs = new List(); - var position = 0; - var lastSymbolI = firstVowelId + 1; - for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { - if (!vowelIds.Contains(lastSymbolI)) { - ccs.Add(symbols[lastSymbolI]); - } else { - position += notes[noteI - 1].duration; - syllables[noteI] = new Syllable() { - prevV = syllables[noteI - 1].v, - cc = ccs.ToArray(), - v = symbols[lastSymbolI], - tone = notes[noteI - 1].tone, - attr = notes[noteI - 1].phonemeAttributes, - duration = notes[noteI - 1].duration, - position = position, - vowelTone = notes[noteI].tone, - vowelAttr = notes[noteI].phonemeAttributes, - canAliasBeExtended = true // for all not-first notes is allowed - }; - ccs = new List(); - noteI++; - } - } - - return syllables; - } - - /// - /// extracts word ending - /// - /// - /// - protected Ending? MakeEnding(Note[] inputNotes) { - if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return null; - } - - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - - return new Ending() { - prevV = symbols[vowelIds.Last()], - cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), - tone = notes.Last().tone, - attr = notes.Last().phonemeAttributes, - duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), - position = notes.Sum(n => n.duration) - }; - } - - /// - /// extracts and validates symbols and vowels - /// - /// - /// - private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { - var mainNote = notes[0]; - var symbols = GetSymbols(mainNote); - if (symbols == null) { - return (null, null, null); - } - if (symbols.Length == 0) { - symbols = new string[] { "" }; - } - - symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); - symbols = ApplyExtensions(symbols, notes); - List vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - vowelIds.Add(symbols.Length - 1); - } - if (notes.Length < vowelIds.Count) { - notes = HandleNotEnoughNotes(notes, vowelIds); - } - return (symbols, vowelIds.ToArray(), notes); - } - - /// - /// When there are more syllables than notes, recombines notes to match syllables count - /// - /// - /// - /// - protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { - var newNotes = new List(); - newNotes.AddRange(notes.SkipLast(1)); - var lastNote = notes.Last(); - var position = lastNote.position; - var notesToSplit = vowelIds.Count - newNotes.Count; - var duration = lastNote.duration / notesToSplit / 15 * 15; - for (var i = 0; i < notesToSplit; i++) { - var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); - newNotes.Add(new Note() { - position = position, - duration = durationFinal, - tone = lastNote.tone, - phonemeAttributes = lastNote.phonemeAttributes - }); - position += durationFinal; - } - - return newNotes.ToArray(); - } - - /// - /// Override this method, if you want to implement some machine converting from a word to phonemes - /// - /// - /// - protected virtual string[] HandleWordNotFound(Note note) { - var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); - if(HasOto(mpdlyric, note.tone)){ - error = mpdlyric; - }else{ - error = "word not found"; - } - return null; - } - - /// - /// Does this note extend the previous syllable? - /// - /// - /// - protected bool IsSyllableVowelExtensionNote(Note note) { - return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); - } - - /// - /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic - /// - /// - /// - protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { - return phonemesString.Split(' '); - } - - /// - /// use to validate alias - /// - /// - /// - protected virtual string ValidateAlias(string alias) { - return alias; - } - - /// - /// Defines basic transition length before scaling it according to tempo and note length - /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation - /// - /// Mapped alias - /// - protected virtual double GetTransitionBasicLengthMs(string alias = "") { - return GetTransitionBasicLengthMsByConstant(); - } - - 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 - /// - /// - protected double GetTempoNoteLengthFactor() { - return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; - } - - protected virtual IG2p[] GetBaseG2ps() { - return Array.Empty(); - } - - protected virtual IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Native YAML Dictionary Logic - if (!string.IsNullOrEmpty(YamlFileName)) { - string path = Path.Combine(PluginDir, YamlFileName); - - // Write template if missing - if (!File.Exists(path) && YamlTemplate != null) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - - // Load dictionary from Singer Folder (Highest Priority) - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - - // Load dictionary from Plugin Folder (Fallback Priority) - if (File.Exists(path)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {path}"); - } - } - } - // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) - else { - var dictionaryName = GetDictionaryName(); - if (!string.IsNullOrEmpty(dictionaryName)) { - var filename = Path.Combine(DictionariesPath, dictionaryName); - if (File.Exists(filename)) { - var dictionaryText = File.ReadAllText(filename); - var builder = G2pDictionary.NewBuilder(); - foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); - foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); - builder.AddEntry("a", new string[] { "a" }); - ParseDictionary(dictionaryText, builder); - g2ps.Add(builder.Build()); - } - } - } - - // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) - var childG2ps = GetBaseG2ps(); - if (childG2ps != null && childG2ps.Any()) { - g2ps.AddRange(childG2ps); - } - - return new G2pFallbacks(g2ps.ToArray()); - } - - /// - /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, - /// and replaces phonemes with replacement table - /// Is Running Async! - /// - /// - /// - protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { - var replacements = GetDictionaryPhonemesReplacement(); - foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { - if (line.StartsWith(";;;")) { - continue; - } - var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); - if (parts.Length != 2) { - continue; - } - string key = parts[0].ToLowerInvariant(); - var values = GetDictionaryWordPhonemes(parts[1]).Select( - n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); - lock (builder) { - builder.AddEntry(key, values); - }; - }; - } - - #region helpers - - /// - /// May be used if you have different logic for short and long notes - /// - /// - /// - protected bool IsShort(Syllable syllable) { - return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; - } - 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 - /// - /// - /// - /// - protected bool HasOto(string alias, int tone) { - return singer.TryGetMappedOto(alias, tone, out _); - } - - /// - /// Can be used for different variants, like exhales [v R], [v -] etc - /// - /// phonemes container to add to - /// to map alias - /// target phoneme variants - /// returns true if added any - protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { - foreach (var phoneme in targetPhonemes) { - if (HasOto(phoneme, tone)) { - sourcePhonemes.Add(phoneme); - return true; - } - } - 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 - /// - /// - /// - protected bool CanMakeAliasExtension(Syllable syllable) { - return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; - } - - /// - /// if current syllable is VV and previous one is from the same pitch, - /// you may wan't to just extend the previous alias. Put the phoneme as null fot that - /// - /// - /// - /// - protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { - if (singer.Subbanks.Count == 1) { - return true; - } - if (tone1 == tone2) { - return true; - } - var toneSets = singer.Subbanks.Select(n => n.toneSet); - foreach (var toneSet in toneSets) { - if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { - return true; - } - if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { - return false; - } - } - return true; - } - - protected virtual string YamlFileName => null; - protected virtual byte[] YamlTemplate => null; - protected virtual string YamlVersion => null; - - protected string[] vowels = Array.Empty(); - protected string[] consonants = Array.Empty(); - protected string[] tails = "-,R".Split(','); - protected string[] affricate = Array.Empty(); - protected string[] fricative = Array.Empty(); - protected string[] aspirate = Array.Empty(); - protected string[] semivowel = Array.Empty(); - protected string[] liquid = Array.Empty(); - protected string[] nasal = Array.Empty(); - protected string[] stop = Array.Empty(); - protected string[] tap = Array.Empty(); - - protected Dictionary dictionaryReplacements = new Dictionary(); - protected Dictionary PhonemeOverrides = new Dictionary(); - protected Dictionary yamlFallbacks = new Dictionary(); - protected List consExceptions = new List(); - - protected Dictionary diphthongTails = new Dictionary(); - protected Dictionary diphthongSplits = new Dictionary(); - - 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(); - public Timings[] timings { get; set; } = Array.Empty(); - public DiphthongData[] diphthongs { get; set; } = Array.Empty(); - public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); - - public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } - public struct Fallbacks { public string from { get; set; } public string to { get; set; } } - public struct Timings { public string symbol { get; set; } public double value { get; set; } } - public struct DiphthongData { public string from { get; set; } public string to { get; set; } } - public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } - } - - public class Replacement { - public object from { get; set; } - public object to { get; set; } - public string where { get; set; } = "inside"; - - public List FromList { - get { - if (from is string s) return new List { s }; - if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - - public List ToList { - get { - if (to is string s) return new List { s }; - if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - } - - protected List mergingReplacements = new List(); - protected List splittingReplacements = new List(); - - protected virtual bool IsGroupKeyword(string rulePhoneme) { - // 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 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; - } - } - } - - bool inBaseGroup = false; - switch (baseGroup) { - case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; - case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; - case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; - case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; - case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; - case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; - case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; - case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; - case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; - case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; - } - - if (!inBaseGroup) return false; - - if (cleanRule.Contains("!")) { - string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; - if (excluded.Split(',').Contains(actualPhoneme)) return false; - } - - if (cleanRule.Contains("=")) { - string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; - if (!restricted.Split(',').Contains(actualPhoneme)) return false; - } - - return true; - } - - protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; - - List finalPhonemes = new List(); - int idx = 0; - - var validRules = mergingReplacements.Concat(splittingReplacements) - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - var validSplits = splittingReplacements - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - while (idx < inputPhonemes.Count) { - bool replaced = false; - - foreach (var rule in validRules) { - 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.Count; j++) { - string rulePh = fromArray[j]; - string actualPh = inputPhonemes[idx + j]; - - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh)) { - if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); - captures[baseRulePh].Add(actualPh); - } else { - match = false; break; - } - } else if (rulePh != actualPh) { - match = false; break; - } - } - - if (match) { - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - 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++) { - // 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.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) { - List fromArray = rule.FromList; - if (fromArray == null || fromArray.Count != 1) continue; - - string rulePh = fromArray[0]; - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { - - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - foreach(string toPh in toArray) { - 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; - } - } - } - if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } else if (!replaced) { - finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } - } - return finalPhonemes; - } - - private Syllable ApplyBoundaryReplacements(Syllable syllable) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); - bool hasV = !string.IsNullOrEmpty(syllable.v); - - 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) || !hasPrevV; - List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); - - string newPrevV = ""; - string newV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - if (hasV && finalPhonemes.Count > 0) { - var vowelsList = GetVowels(); - int vIndex = finalPhonemes.Count - 1; - - for (int i = finalPhonemes.Count - 1; i >= 0; i--) { - if (vowelsList.Contains(finalPhonemes[i])) { - vIndex = i; - break; - } - } - newV = finalPhonemes[vIndex]; - for (int i = 0; i < vIndex; i++) { - newCc.Add(finalPhonemes[i]); - } - } else { - newCc.AddRange(finalPhonemes); - } - } - - syllable.prevV = newPrevV; - syllable.cc = newCc.ToArray(); - syllable.v = newV; - return syllable; - } - - private Ending ApplyBoundaryReplacements(Ending ending) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); - currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); - if (ending.cc != null) currentPhonemes.AddRange(ending.cc); - - List finalPhonemes = ApplyReplacements(currentPhonemes, true); - - string newPrevV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - newCc.AddRange(finalPhonemes); - } - - ending.prevV = newPrevV; - ending.cc = newCc.ToArray(); - return ending; - } - - #endregion - - #region private - - private Result MakeForcedAliasResult(Note note) { - return MakeSimpleResult(note.lyric.Substring(1)); - } - - protected void ReadDictionaryAndInit() { - var dictionaryName = GetDictionaryName(); - if (dictionaryName == null) { - return; - } - dictionaries[GetType()] = null; - if (Testing) { - ReadDictionary(dictionaryName); - Init(); - return; - } - OnAsyncInitStarted(); - Task.Run(() => { - ReadDictionary(dictionaryName); - Init(); - OnAsyncInitFinished(); - }); - } - - private void ReadDictionary(string dictionaryName) { - try { - var phonemeSymbols = new Dictionary(); - - foreach (var vowel in GetVowels()) { - phonemeSymbols[vowel] = true; - } - foreach (var consonant in GetConsonants()) { - phonemeSymbols[consonant] = false; - } - - var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); - var safeDict = new Dictionary(); - - foreach (var kvp in childDict) { - safeDict[kvp.Key] = kvp.Value; - safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' - safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' - } - - dictionaries[GetType()] = new G2pRemapper( - LoadBaseDictionary(), - phonemeSymbols, - safeDict); - - } catch (Exception ex) { - Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); - } - } - - private string[] ApplyExtensions(string[] symbols, Note[] notes) { - var newSymbols = new List(); - var vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - // no syllables or all consonants, the last phoneme will be interpreted as vowel - vowelIds.Add(symbols.Length - 1); - } - var lastVowelI = 0; - newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); - for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { - if (!IsSyllableVowelExtensionNote(notes[i])) { - var prevVowel = vowelIds[lastVowelI]; - lastVowelI++; - var vowel = vowelIds[lastVowelI]; - newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); - } else { - newSymbols.Add(symbols[vowelIds[lastVowelI]]); - } - } - newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); - return newSymbols.ToArray(); - } - - private List ExtractVowels(string[] symbols) { - var vowelIds = new List(); - var vowels = GetVowels(); - for (var i = 0; i < symbols.Length; i++) { - if (vowels.Contains(symbols[i])) { - vowelIds.Add(i); - } - } - 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]; - - 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) { - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - stretch *= overrideRatio; - } else { - baseLengthMs = GetTransitionBasicLengthMs(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; - } - } - } - - 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] = new Phoneme { - phoneme = validatedAlias, - index = globalIndex - }; - - if (i == 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) { - // YAML Override active: Use the multiplier and bypass NoGap entirely - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); - } else { - // Default behavior - baseLengthMs = GetTransitionBasicLengthMsByOto(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 - phonemes[phonemeI].position = MsToTick(baseLengthMs); - } - } - } else { - 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]; - } - } else { - // 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 - 1 : phonemeSymbols.Count - 1, containerLength); - } - - private string ValidateAliasIfNeeded(string alias, int tone) { - if (HasOto(alias, tone)) { - return alias; - } - return ValidateAlias(alias); - } - - private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { - var offset = 0; - var lengthModifier = 1.0; - - if (containerLengthTick > 0) { - var allTransitionsLengthTick = phonemes.Sum(n => n.position); - - // 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--) { - if (phonemes[i].phoneme == null) continue; - var finalLengthTick = (int)(phonemes[i].position * lengthModifier); - phonemes[i].position = startPosition - finalLengthTick - offset; - offset += finalLengthTick; - } - - return phonemes.Where(n => n.phoneme != null).ToArray(); - } - - #endregion - } -} From 2efb4aae1ad1dd90c1d5c749309b189df2736e55 Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:37:26 -0700 Subject: [PATCH 08/13] It functions now! I need to do much more testing before I can say that it's finished. --- .../Data/Resources.Designer.cs | 85 ++++++------------- OpenUtau.Plugin.Builtin/Data/Resources.resx | 4 +- .../Data/generic-syllabic.template.yaml | 64 +++++++++----- .../GenericSyllabicPhonemizer.cs | 15 ++-- 4 files changed, 81 insertions(+), 87 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs index 70d2198eb..f7e8b44f3 100644 --- a/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs +++ b/OpenUtau.Plugin.Builtin/Data/Resources.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -12,46 +11,32 @@ namespace OpenUtau.Plugin.Builtin.Data { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenUtau.Plugin.Builtin.Data.Resources", typeof(Resources).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("OpenUtau.Plugin.Builtin.Data.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -60,9 +45,6 @@ internal Resources() { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] arpasing_template { get { object obj = ResourceManager.GetObject("arpasing_template", resourceCulture); @@ -70,9 +52,6 @@ internal static byte[] arpasing_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] de_vccv_template { get { object obj = ResourceManager.GetObject("de_vccv_template", resourceCulture); @@ -80,39 +59,27 @@ internal static byte[] de_vccv_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] en_cPv_template { + internal static byte[] envccv_template { get { - object obj = ResourceManager.GetObject("en_cPv_template", resourceCulture); + object obj = ResourceManager.GetObject("envccv_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] en_xsampa_template { + internal static byte[] en_cPv_template { get { - object obj = ResourceManager.GetObject("en_xsampa_template", resourceCulture); + object obj = ResourceManager.GetObject("en_cPv_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] envccv_template { + internal static byte[] en_xsampa_template { get { - object obj = ResourceManager.GetObject("envccv_template", resourceCulture); + object obj = ResourceManager.GetObject("en_xsampa_template", resourceCulture); return ((byte[])(obj)); } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] filipino_template { get { object obj = ResourceManager.GetObject("filipino_template", resourceCulture); @@ -120,9 +87,6 @@ internal static byte[] filipino_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] german_template { get { object obj = ResourceManager.GetObject("german_template", resourceCulture); @@ -130,9 +94,6 @@ internal static byte[] german_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] kocv_template { get { object obj = ResourceManager.GetObject("kocv_template", resourceCulture); @@ -140,14 +101,18 @@ internal static byte[] kocv_template { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// internal static byte[] njokis_template { get { object obj = ResourceManager.GetObject("njokis_template", resourceCulture); return ((byte[])(obj)); } } + + internal static byte[] generic_syllabic_template { + get { + object obj = ResourceManager.GetObject("generic_syllabic_template", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/OpenUtau.Plugin.Builtin/Data/Resources.resx b/OpenUtau.Plugin.Builtin/Data/Resources.resx index 46c6031d5..c850f17bf 100644 --- a/OpenUtau.Plugin.Builtin/Data/Resources.resx +++ b/OpenUtau.Plugin.Builtin/Data/Resources.resx @@ -145,7 +145,7 @@ njokis.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - generic-syllablic.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + generic-syllabic.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml index 1fb0f9b32..13ce8d372 100644 --- a/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml +++ b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml @@ -1,35 +1,49 @@ %YAML 1.2 --- + # This dictionary is meant to be modified (such as adding and removing phonemes) to fit the voicebank that is using it. + # The existing set of phonemes are just examples. + symbols: - - {symbol: aa, type: vowel} - - {symbol: ae, type: vowel} - - {symbol: ah, type: vowel} - - {symbol: ao, type: vowel} - - {symbol: aw, type: vowel} - - {symbol: ax, type: vowel} - - {symbol: ay, type: vowel} + # ARPAbet Vowels + - { symbol: aa, type: vowel } + - { symbol: ae, type: vowel } + - { symbol: ah, type: vowel } + - { symbol: ao, type: vowel } + - { symbol: aw, type: vowel } + - { symbol: ax, type: vowel } + - { symbol: ay, type: vowel } + - { symbol: eh, type: vowel } + - { symbol: er, type: vowel } + - { symbol: ey, type: vowel } + - { symbol: ow, type: vowel } + - { symbol: oy, type: vowel } + - { symbol: uh, type: vowel } + - { symbol: uw, type: vowel } + + # Romaji Vowels + - { symbol: a, type: vowel } + - { symbol: i, type: vowel } + - { symbol: u, type: vowel } + - { symbol: e, type: vowel } + - { symbol: o, type: vowel } + - {symbol: N, type: vowel } + + # ARPAbet Consonants - {symbol: b, type: stop} - {symbol: ch, type: affricate} - {symbol: d, type: stop} - {symbol: dh, type: fricative} - - {symbol: dr, type: fricative} - {symbol: dx, type: tap} - - {symbol: eh, type: vowel} - - {symbol: er, type: vowel} - - {symbol: ey, type: vowel} - {symbol: f, type: fricative} - {symbol: g, type: stop} + - {symbol: h, type: aspirate} - {symbol: hh, type: aspirate} - - {symbol: ih, type: vowel} - - {symbol: iy, type: vowel} - {symbol: jh, type: affricate} - {symbol: k, type: stop} - {symbol: l, type: liquid} - {symbol: m, type: nasal} - {symbol: n, type: nasal} - {symbol: ng, type: nasal} - - {symbol: ow, type: vowel} - - {symbol: oy, type: vowel} - {symbol: p, type: stop} - {symbol: q, type: stop} - {symbol: r, type: liquid} @@ -37,14 +51,26 @@ symbols: - {symbol: sh, type: fricative} - {symbol: t, type: stop} - {symbol: th, type: fricative} - - {symbol: tr, type: fricative} - - {symbol: uh, type: vowel} - - {symbol: uw, type: vowel} - {symbol: v, type: fricative} - {symbol: w, type: semivowel} - {symbol: y, type: semivowel} - {symbol: z, type: fricative} - {symbol: zh, type: fricative} + + # Romaji Consonants + - { symbol: by, type: stop } + - { symbol: dy, type: stop } + - { symbol: gy, type: stop } + - { symbol: ky, type: stop } + - { symbol: my, type: nasal } + - { symbol: ny, type: nasal } + - { symbol: py, type: stop } + - { symbol: ry, type: tap } + - { symbol: ty, type: stop } + - { symbol: j, type: fricative } + - { symbol: ts, type: affricate } + - { symbol: cl, type: stop } + entries: - grapheme: openutau - phonemes: [ow, p, eh, n, w, uw, t, ah, w, uw] + phonemes: [ow, p, ax, n, q, uw, t, aw] diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs index b662aebdb..ff30ce767 100644 --- a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -1,16 +1,19 @@ -using OpenUtau.Api; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenUtau.Api; using OpenUtau.Core.Ustx; -using OpenUtau.Plugin.Builtin; using Serilog; using System.Text.RegularExpressions; -namespace GenericSyllableBasedPhonemizer { - [Phonemizer("Generic Syllable-Based Phonemizer", "GEN SYL", "HeadHunter4621")] +namespace OpenUtau.Plugin.Builtin { + [Phonemizer("Generic Syllabic Phonemizer", "GEN SYLLABIC", "HeadHunter4621")] // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. - // The default template contains X-SAMPA phonemes but can (and should) be modified to fit your voicebank. + // The default template contains ARPAbet and romaji phonemes but can (and should) be modified to fit your voicebank. public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { protected override string YamlFileName => "generic-phonemizer.yaml"; - protected override byte[] YamlTemplate => Data.Resources.generic_template; + protected override byte[] YamlTemplate => Data.Resources.generic_syllabic_template; public GenericSyllablePhonemizer() { From 4185e978ca4433513a256106824d124e79173be9 Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:35:52 -0700 Subject: [PATCH 09/13] Default config was missing some phonemes, fixed now. --- OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml | 2 ++ OpenUtau.sln | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml index 13ce8d372..570425a5b 100644 --- a/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml +++ b/OpenUtau.Plugin.Builtin/Data/generic-syllabic.template.yaml @@ -15,6 +15,8 @@ symbols: - { symbol: eh, type: vowel } - { symbol: er, type: vowel } - { symbol: ey, type: vowel } + - { symbol: ih, type: vowel } + - { symbol: iy, type: vowel } - { symbol: ow, type: vowel } - { symbol: oy, type: vowel } - { symbol: uh, type: vowel } diff --git a/OpenUtau.sln b/OpenUtau.sln index 8ceffd6c4..9458f78db 100644 --- a/OpenUtau.sln +++ b/OpenUtau.sln @@ -26,8 +26,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau", "OpenUtau\OpenUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenUtau.Plugin.Builtin", "OpenUtau.Plugin.Builtin\OpenUtau.Plugin.Builtin.csproj", "{D24740E1-CC43-4226-A573-59C013FE50A2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenericSyllableBasedPhonemizer", "GenericSyllableBasedPhonemizer\GenericSyllableBasedPhonemizer.csproj", "{61E62C45-6CE9-0275-ED91-C8F059C7A40C}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,10 +48,6 @@ Global {D24740E1-CC43-4226-A573-59C013FE50A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D24740E1-CC43-4226-A573-59C013FE50A2}.Release|Any CPU.Build.0 = Release|Any CPU - {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {61E62C45-6CE9-0275-ED91-C8F059C7A40C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From c12eef768881787dc3f896251384362fad4cc86b Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:39:28 -0700 Subject: [PATCH 10/13] updated comment in phonemizer code --- OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs index ff30ce767..86cb0ac28 100644 --- a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -9,8 +9,8 @@ namespace OpenUtau.Plugin.Builtin { [Phonemizer("Generic Syllabic Phonemizer", "GEN SYLLABIC", "HeadHunter4621")] - // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. - // The default template contains ARPAbet and romaji phonemes but can (and should) be modified to fit your voicebank. + // Contributed by HeadHunter4621, base code is the FIL VCV & CVVC phonemizer. + // The default template contains ARPAbet and romaji phonemes but can be easily modified to fit each voicebank. public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { protected override string YamlFileName => "generic-phonemizer.yaml"; protected override byte[] YamlTemplate => Data.Resources.generic_syllabic_template; From ba730aee9b794700d3c30301e2bb3b9b20c1f5fe Mon Sep 17 00:00:00 2001 From: HeadHunter4621 <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:41:21 -0700 Subject: [PATCH 11/13] Renamed the phonemizer --- OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs index 86cb0ac28..3313752f3 100644 --- a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -8,7 +8,7 @@ using System.Text.RegularExpressions; namespace OpenUtau.Plugin.Builtin { - [Phonemizer("Generic Syllabic Phonemizer", "GEN SYLLABIC", "HeadHunter4621")] + [Phonemizer("Generic Syllabic Phonemizer", "GEN SYL", "HeadHunter4621")] // Contributed by HeadHunter4621, base code is the FIL VCV & CVVC phonemizer. // The default template contains ARPAbet and romaji phonemes but can be easily modified to fit each voicebank. public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { From 0e4f613898d82a0796abacde8923670783c57eb7 Mon Sep 17 00:00:00 2001 From: Kona <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:45:15 -0700 Subject: [PATCH 12/13] Delete GenericSyllableBasedPhonemizer directory --- .../Data/Resources.Designer.cs | 73 - .../Data/Resources.resx | 124 -- .../Data/generic.template.yaml | 150 -- .../GenericSyllableBasedPhonemizer.csproj | 28 - .../GenericSyllablePhonemizer.cs | 1050 ---------- .../SyllableBasedPhonemizer.cs | 1780 ----------------- 6 files changed, 3205 deletions(-) delete mode 100644 GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs delete mode 100644 GenericSyllableBasedPhonemizer/Data/Resources.resx delete mode 100644 GenericSyllableBasedPhonemizer/Data/generic.template.yaml delete mode 100644 GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj delete mode 100644 GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs delete mode 100644 GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs diff --git a/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs b/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs deleted file mode 100644 index 92bc4780b..000000000 --- a/GenericSyllableBasedPhonemizer/Data/Resources.Designer.cs +++ /dev/null @@ -1,73 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace GenericSyllableBasedPhonemizer.Data { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GenericSyllableBasedPhonemizer.Data.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] generic_template { - get { - object obj = ResourceManager.GetObject("generic_template", resourceCulture); - return ((byte[])(obj)); - } - } - } -} diff --git a/GenericSyllableBasedPhonemizer/Data/Resources.resx b/GenericSyllableBasedPhonemizer/Data/Resources.resx deleted file mode 100644 index cbc43d29b..000000000 --- a/GenericSyllableBasedPhonemizer/Data/Resources.resx +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - generic.template.yaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/GenericSyllableBasedPhonemizer/Data/generic.template.yaml b/GenericSyllableBasedPhonemizer/Data/generic.template.yaml deleted file mode 100644 index ab5842086..000000000 --- a/GenericSyllableBasedPhonemizer/Data/generic.template.yaml +++ /dev/null @@ -1,150 +0,0 @@ -%YAML 1.2 ---- -symbols: -#Delta-style (X-SAMPA) symbols - - {symbol: a, type: vowel} - - {symbol: e, type: vowel} - - {symbol: i, type: vowel} - - {symbol: o, type: vowel} - - {symbol: u, type: vowel} - - {symbol: A, type: vowel} - - {symbol: E, type: vowel} - - {symbol: I, type: vowel} - - {symbol: O, type: vowel} - - {symbol: U, type: vowel} - - {symbol: '{', type: vowel} - - {symbol: V, type: vowel} - - {symbol: 3, type: vowel} - - {symbol: aI, type: vowel} - - {symbol: eI, type: vowel} - - {symbol: OI, type: vowel} - - {symbol: aU, type: vowel} - - {symbol: oU, type: vowel} - - {symbol: tS, type: affricate} - - {symbol: D, type: fricative} - - {symbol: h, type: aspirate} - - {symbol: dZ, type: affricate} - - {symbol: N, type: nasal} - - {symbol: S, type: fricative} - - {symbol: T, type: fricative} - - {symbol: j, type: semivowel} - - {symbol: Z, type: fricative} -#Arpasing - - {symbol: aa, type: vowel} - - {symbol: ae, type: vowel} - - {symbol: ah, type: vowel} - - {symbol: ao, type: vowel} - - {symbol: aw, type: vowel} - - {symbol: ay, type: vowel} - - {symbol: b, type: stop} - - {symbol: ch, type: affricate} - - {symbol: d, type: stop} - - {symbol: dh, type: fricative} - - {symbol: eh, type: vowel} - - {symbol: er, type: vowel} - - {symbol: ey, type: vowel} - - {symbol: f, type: fricative} - - {symbol: g, type: stop} - - {symbol: hh, type: aspirate} - - {symbol: ih, type: vowel} - - {symbol: iy, type: vowel} - - {symbol: jh, type: affricate} - - {symbol: k, type: stop} - - {symbol: l, type: liquid} - - {symbol: m, type: nasal} - - {symbol: n, type: nasal} - - {symbol: ng, type: nasal} - - {symbol: ow, type: vowel} - - {symbol: oy, type: vowel} - - {symbol: p, type: stop} - - {symbol: r, type: liquid} - - {symbol: s, type: fricative} - - {symbol: sh, type: fricative} - - {symbol: t, type: stop} - - {symbol: th, type: fricative} - - {symbol: uh, type: vowel} - - {symbol: uw, type: vowel} - - {symbol: v, type: fricative} - - {symbol: w, type: semivowel} - - {symbol: y, type: semivowel} - - {symbol: z, type: fricative} - - {symbol: zh, type: fricative} - #bonus symbols - - {symbol: e@, type: vowel} - - {symbol: e@n, type: vowel} - - {symbol: e@m, type: vowel} - - {symbol: eN, type: vowel} - - {symbol: IN, type: vowel} - - {symbol: Ar, type: vowel} - - {symbol: Qr, type: vowel} - - {symbol: Er, type: vowel} - - {symbol: Ir, type: vowel} - - {symbol: Or, type: vowel} - - {symbol: Ur, type: vowel} - - {symbol: ir, type: vowel} - - {symbol: ur, type: vowel} - - {symbol: aIr, type: vowel} - - {symbol: aUr, type: vowel} - - {symbol: A@, type: vowel} - - {symbol: Q@, type: vowel} - - {symbol: E@, type: vowel} - - {symbol: I@, type: vowel} - - {symbol: O@, type: vowel} - - {symbol: U@, type: vowel} - - {symbol: i@, type: vowel} - - {symbol: u@, type: vowel} - - {symbol: aI@, type: vowel} - - {symbol: aU@, type: vowel} - - {symbol: Q, type: vowel} - - {symbol: 1, type: vowel} - - {symbol: Ol, type: vowel} - - {symbol: aUn, type: vowel} - - {symbol: '@r', type: vowel} - - {symbol: '@l', type: vowel} - - {symbol: '@m', type: vowel} - - {symbol: '@n', type: vowel} - - {symbol: '@N', type: vowel} - - {symbol: '@', type: vowel} - - {symbol: y, type: vowel} - - {symbol: I\, type: vowel} - - {symbol: M, type: vowel} - - {symbol: U\, type: vowel} - - {symbol: Y, type: vowel} - - {symbol: '@\', type: vowel} - - {symbol: '@`', type: vowel} - - {symbol: 3`, type: vowel} - - {symbol: A`, type: vowel} - - {symbol: Q`, type: vowel} - - {symbol: E`, type: vowel} - - {symbol: I`, type: vowel} - - {symbol: O`, type: vowel} - - {symbol: U`, type: vowel} - - {symbol: i`, type: vowel} - - {symbol: u`, type: vowel} - - {symbol: aI`, type: vowel} - - {symbol: aU`, type: vowel} - - {symbol: '}', type: vowel} - - {symbol: 2, type: vowel} - - {symbol: 3\, type: vowel} - - {symbol: 6, type: vowel} - - {symbol: 7, type: vowel} - - {symbol: 8, type: vowel} - - {symbol: 9, type: vowel} - - {symbol: '&', type: vowel} - - {symbol: '{~', type: vowel} - - {symbol: I~, type: vowel} - - {symbol: aU~, type: vowel} - - {symbol: VI, type: vowel} - - {symbol: VU, type: vowel} - - {symbol: '@U', type: vowel} - - {symbol: 'i:', type: vowel} - - {symbol: 'u:', type: vowel} - - {symbol: 'O:', type: vowel} - - {symbol: e@0, type: vowel} - - {symbol: 4, type: tap} - - {symbol: W, type: fricative} - - {symbol: 't_}', type: stop} - -entries: - - grapheme: openutau - phonemes: [ow, p, ah, n, uw, t, aw] diff --git a/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj b/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj deleted file mode 100644 index 5d92a0bf9..000000000 --- a/GenericSyllableBasedPhonemizer/GenericSyllableBasedPhonemizer.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - diff --git a/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs b/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs deleted file mode 100644 index b662aebdb..000000000 --- a/GenericSyllableBasedPhonemizer/GenericSyllablePhonemizer.cs +++ /dev/null @@ -1,1050 +0,0 @@ -using OpenUtau.Api; -using OpenUtau.Core.Ustx; -using OpenUtau.Plugin.Builtin; -using Serilog; -using System.Text.RegularExpressions; - -namespace GenericSyllableBasedPhonemizer { - [Phonemizer("Generic Syllable-Based Phonemizer", "GEN SYL", "HeadHunter4621")] - // Nearly all of this code is from Cadlaxa's FIL VCV & CVVC phonemizer. - // The default template contains X-SAMPA phonemes but can (and should) be modified to fit your voicebank. - public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { - protected override string YamlFileName => "generic-phonemizer.yaml"; - protected override byte[] YamlTemplate => Data.Resources.generic_template; - - public GenericSyllablePhonemizer() { - - this.vowels = Array.Empty(); - this.consonants = Array.Empty(); - } - - protected override string[] GetVowels() => vowels; - protected override string[] GetConsonants() => consonants; - protected override string GetDictionaryName() => ""; - - List consExceptions = new List(); - - string[] diphthongs = Array.Empty(); - - // For banks with missing vowels - private readonly Dictionary missingVphonemes = "ax=a".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingVPhonemes = false; - private bool isYamlFallbacks = false; - - - // For banks with missing custom consonants - private readonly Dictionary missingCphonemes = "N=n".Split(',') - .Select(entry => entry.Split('=')) - .Where(parts => parts.Length == 2) - .Where(parts => parts[0] != parts[1]) - .ToDictionary(parts => parts[0], parts => parts[1]); - private bool isMissingCPhonemes = false; - private bool cPV_FallBack = false; - - private readonly Dictionary vvDiphthongExceptions = - new Dictionary() { - {"aw","a"}, - {"ow","o"}, - {"iw","i"}, - {"ay","a"}, - {"ey","e"}, - {"oy","o"}, - {"uy","u"}, - {"ew","e"}, - }; - - private readonly Dictionary vvExceptions = - new Dictionary() { - {"aw","w"}, - {"ow","w"}, - {"iw","w"}, - {"ay","y"}, - {"ey","y"}, - {"oy","y"}, - {"uy","y"}, - {"ew","w"}, - }; - - private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; - private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; - private static readonly string[] FinalConsonants = { "w", "y", "r", "l", "m", "n", "ng" }; - - - protected override string[] GetSymbols(Note note) { - string[] original = base.GetSymbols(note); - return note.lyric.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); - - //if (original == null || original.Length == 0) { - // string lyric = note.lyric.ToLowerInvariant(); - // List fallbackSplit = new List(); - // string[] vowels = GetVowels(); - // string[] consonants = GetConsonants(); - - // // Handle apostrophes at the start or end - // bool hasLeadingApostrophe = lyric.StartsWith("'"); - // bool hasTrailingApostrophe = lyric.EndsWith("'"); - - // if (hasLeadingApostrophe) { - // lyric = lyric.Substring(1); - // } - // if (hasTrailingApostrophe && lyric.Length > 0) { - // lyric = lyric.Substring(0, lyric.Length - 1); - // } - - // int ii = 0; - // while (ii < lyric.Length) { - // string match = null; - // foreach (var cons in consonants.OrderByDescending(c => c.Length)) { - // if (lyric.Substring(ii).StartsWith(cons)) { - // match = cons; - // break; - // } - // } - // if (match == null) { - // foreach (var vow in vowels.OrderByDescending(v => v.Length)) { - // if (lyric.Substring(ii).StartsWith(vow)) { - // match = vow; - // break; - // } - // } - // } - // if (match != null) { - // fallbackSplit.Add(match); - // ii += match.Length; - // } else { - // fallbackSplit.Add(lyric[ii].ToString()); - // ii++; - // } - // } - // // Add "q" at the beginning or end if needed - // if (hasLeadingApostrophe) { - // fallbackSplit.Insert(0, "q"); - // } - // if (hasTrailingApostrophe) { - // fallbackSplit.Add("q"); - - // } - // original = fallbackSplit.ToArray(); - //} - - //List modified = new List(original); - //List finalPhonemes = new List(); - //finalPhonemes = new List(modified); - //List finalProcessedPhonemes = new List(); - //IEnumerable phonemes; - //phonemes = finalPhonemes; - //foreach (string s in phonemes) { - // switch (s) { - // default: - // finalProcessedPhonemes.Add(s); - // break; - // } - //} - //return finalProcessedPhonemes.ToArray(); - } - - protected override IG2p LoadBaseDictionary() { - var g2ps = new List(); - // LOAD DICTIONARY FROM FOLDER - string path = Path.Combine(PluginDir, YamlFileName); - if (!File.Exists(path)) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - // LOAD DICTIONARY FROM SINGER FOLDER - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - //g2ps.Add(new ArpabetPlusG2p()); - return new G2pFallbacks(g2ps.ToArray()); - } - public override void SetSinger(USinger singer) { - base.SetSinger(singer); - - if (this.singer != null && this.singer.Loaded) { - - consExceptions.Clear(); - if (stop != null) consExceptions.AddRange(stop); - if (tap != null) consExceptions.AddRange(tap); - - consExceptions = consExceptions.Distinct().ToList(); - } - } - - // prioritize yaml replacements over dictionary replacements - private string ReplacePhoneme(string phoneme, int tone) { - // If the original phoneme has an OTO, use it directly. - 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; - } - protected override List ProcessSyllable(Syllable syllable) { - syllable.prevV = tails.Contains(syllable.prevV) ? "" : syllable.prevV; - var replacedPrevV = ReplacePhoneme(syllable.prevV, syllable.tone); - var prevV = string.IsNullOrEmpty(replacedPrevV) ? "" : replacedPrevV; - string[] cc = syllable.cc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string v = ReplacePhoneme(syllable.v, syllable.vowelTone); - List vowels = new List { v }; - string basePhoneme; - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string[] CurrentWordCc = syllable.CurrentWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - string[] PreviousWordCc = syllable.PreviousWordCc.Select(c => ReplacePhoneme(c, syllable.tone)).ToArray(); - int prevWordConsonantsCount = syllable.prevWordConsonantsCount; - - // Check for missing vowel phonemes - foreach (var entry in missingVphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Key, syllable.tone)) { - isMissingVPhonemes = true; - break; - } - } - - // Check for missing consonant phonemes - foreach (var entry in missingCphonemes) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isMissingCPhonemes = true; - break; - } - } - - foreach (var entry in yamlFallbacks) { - if (!HasOto(entry.Key, syllable.tone) && !HasOto(entry.Value, syllable.tone)) { - isYamlFallbacks = true; - break; - } - } - - // STARTING V - if (syllable.IsStartingV) { - basePhoneme = AliasFormat(v, "startingV", syllable.vowelTone, ""); - } - // [V V] or [V C][C V]/[V] - else if (syllable.IsVV) { - if (!CanMakeAliasExtension(syllable)) { - basePhoneme = $"{prevV} {v}"; - if (!HasOto(basePhoneme, syllable.vowelTone) && !HasOto(ValidateAlias(basePhoneme), syllable.vowelTone) && vvExceptions.ContainsKey(prevV) && prevV != v) { - // VV IS NOT PRESENT, CHECKS VVEXCEPTIONS LOGIC - //var vc = $"{prevV}{vvExceptions[prevV]}"; - var vc = AliasFormat($"{vvExceptions[prevV]}", "vcEx", syllable.vowelTone, prevV); - TryAddPhoneme(phonemes, syllable.vowelTone, vc); - basePhoneme = AliasFormat($"{vvExceptions[prevV]} {v}", "dynMid", syllable.vowelTone, ""); - } else { - { - if (HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV} {v}"; - } else if (HasOto($"{prevV}{v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV}{v}"), syllable.vowelTone)) { - basePhoneme = $"{prevV}{v}"; - } else if (HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) { - basePhoneme = v; - } else { - basePhoneme = AliasFormat($"- {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.vowelTone, AliasFormat($"{prevV} -", "dynMid", syllable.vowelTone, "")); - } - } - } - // EXTEND AS [V] - } else if (HasOto($"{v}", syllable.vowelTone) && HasOto(ValidateAlias($"{v}"), syllable.vowelTone) || missingVphonemes.ContainsKey(prevV)) { - basePhoneme = v; - } else if (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone) && vvDiphthongExceptions.ContainsKey(prevV)) { - basePhoneme = $"{vvDiphthongExceptions[prevV]} {vvDiphthongExceptions[prevV]}"; - } else { - // PREVIOUS ALIAS WILL EXTEND as [V V] - basePhoneme = null; - } - - // [- CV/C V] or [- C][CV/C V] - } else if (syllable.IsStartingCVWithOneConsonant) { - var rcv = $"- {cc[0]} {v}"; - var rcv1 = $"- {cc[0]}{v}"; - var crv = $"{cc[0]} {v}"; - /// - CV - if (HasOto(rcv, syllable.vowelTone) && HasOto(ValidateAlias(rcv), syllable.vowelTone) || (HasOto(rcv1, syllable.vowelTone) && HasOto(ValidateAlias(rcv1), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynStart", syllable.vowelTone, ""); - /// CV - } else if (HasOto(crv, syllable.vowelTone) && HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } else { - basePhoneme = AliasFormat($"{cc[0]} {v}", "dynMid", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } - // [CCV/CC V] or [C C] + [CV/C V] - } else if (syllable.IsStartingCVWithMoreThanOneConsonant) { - // TRY [- CCV]/[- CC V] or [- CC][CCV]/[CC V] or [- C][C C][C V]/[CV] - var rccv = $"- {string.Join("", cc)} {v}"; - var rccv1 = $"- {string.Join("", cc)}{v}"; - var crv = $"{cc.Last()} {v}"; - var crv1 = $"{cc.Last()}{v}"; - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - /// - CCV - if (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone) || HasOto(rccv1, syllable.vowelTone) || HasOto(ValidateAlias(rccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynStart", syllable.vowelTone, ""); - lastC = 0; - } else { - /// CCV and CV - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(crv1, syllable.vowelTone) || HasOto(ValidateAlias(crv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // TRY RCC [- CC] - for (var i = cc.Length; i > 1; i--) { - if (!ccvException.Contains(cc[0])) { - if (TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{string.Join("", cc.Take(i))}", "cc_start", syllable.vowelTone, ""))) { - firstC = i - 1; - } - } - break; - } - // [- C] - if (phonemes.Count == 0) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - } - // try [CC V] or [CCV] - var cv = $"{cc.Last()}{v}"; - for (var i = firstC; i < cc.Length - 1; i++) { - /// CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - /// C-Last - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - } - } else { // VCV - var vcv = $"{prevV} {cc[0]}{v}"; - var vcv2 = $"{prevV}{cc[0]}{v}"; - var vcvEnd = $"{prevV}{cc[0]} {v}"; - var vccv = $"{prevV} {string.Join("", cc)}{v}"; - var vccv2 = $"{prevV} {string.Join("", cc)}"; - var vccv3 = $"{prevV}{string.Join("", cc)}"; - var crv = $"{cc.Last()} {v}"; - // Use regular VCV if the current word starts with one consonant and the previous word ends with none - if (syllable.IsVCVWithOneConsonant && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv; - } else if (syllable.IsVCVWithOneConsonant && (HasOto(vcv2, syllable.vowelTone) && HasOto(ValidateAlias(vcv2), syllable.vowelTone)) && prevWordConsonantsCount == 0 && CurrentWordCc.Length == 1) { - basePhoneme = vcv2; - // Use end VCV if current word does not start with a consonant but the previous word does end with one - } else if (syllable.IsVCVWithOneConsonant && prevWordConsonantsCount == 1 && CurrentWordCc.Length == 0 && (HasOto(vcvEnd, syllable.vowelTone) && HasOto(ValidateAlias(vcvEnd), syllable.vowelTone))) { - basePhoneme = vcvEnd; - // Use regular VCV if end VCV does not exist - } else if (syllable.IsVCVWithOneConsonant && (!HasOto(vcvEnd, syllable.vowelTone) && !HasOto(ValidateAlias(vcvEnd), syllable.vowelTone)) && (HasOto(vcv, syllable.vowelTone) && HasOto(ValidateAlias(vcv), syllable.vowelTone))) { - basePhoneme = vcv; - // VCV with multiple consonants, only for current word onset and null previous word ending - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv, syllable.vowelTone) && HasOto(ValidateAlias(vccv), syllable.vowelTone)) && prevWordConsonantsCount == 0) { - basePhoneme = vccv; - lastC = 0; - } else if (syllable.IsVCVWithMoreThanOneConsonant && (HasOto(vccv3, syllable.vowelTone) && HasOto(ValidateAlias(vccv3), syllable.vowelTone))) { - basePhoneme = AliasFormat($"{prevV} {string.Join("", cc)}{v}", "dynMid", syllable.vowelTone, ""); - lastC = 0; - } else { - var cv = cc.Last() + v; - basePhoneme = cv; - if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { - basePhoneme = crv; - } - // try [CC V] or [CCV] - for (var i = firstC; i < cc.Length - 1; i++) { - var ccv = $"{string.Join("", cc)} {v}"; - var ccv1 = $"{string.Join("", cc)}{v}"; - /// CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - break; - } - /// C-Last - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone) || HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } else { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - } - } - // try [V C], [V CC], [VC C], [V -][- C] - for (var i = lastC + 1; i >= 0; i--) { - var vr = $"{prevV} -"; - //var vc_c = $"{prevV}{string.Join(" ", cc.Take(2))}-"; - //var vc_c2 = $"{prevV}{string.Join(" ", cc.Take(2))}_"; - var vcc = $"{prevV} {string.Join("", cc.Take(2))}"; - var vc = $"{prevV} {cc[0]}"; - // Boolean Triggers - bool CCV = false; - if (CurrentWordCc.Length >= 2 && !ccvException.Contains(cc[0])) { - if (HasOto(AliasFormat($"{string.Join("", cc)} {v}", "dynMid", syllable.vowelTone, ""), syllable.vowelTone)) { - CCV = true; - } - } - - if (i == 0 && (HasOto(vr, syllable.tone) || HasOto(ValidateAlias(vr), syllable.tone)) && !HasOto(vc, syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, vr, ValidateAlias(vr)); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{cc[0]}", "cc_start", syllable.vowelTone, "")); - break; - } else if ((HasOto(vcc, syllable.tone) || HasOto(ValidateAlias(vcc), syllable.tone)) && CCV) { - TryAddPhoneme(phonemes, syllable.tone, vcc, ValidateAlias(vcc)); - firstC = 1; - break; - /*} else if (HasOto(vc_c, syllable.tone) && HasOto(ValidateAlias(vc_c), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} -", "dynMid", syllable.vowelTone, "")); - firstC = 1; - break; - } else if (HasOto(vc_c2, syllable.tone) && HasOto(ValidateAlias(vc_c2), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{prevV}{string.Join(" ", cc.Take(2))} _", "dynMid", syllable.vowelTone, "")); - firstC = 1; - break;*/ - } else if (cPV_FallBack && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { - TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); - break; - } else if (HasOto(vc, syllable.tone) || HasOto(ValidateAlias(vc), syllable.tone)) { - TryAddPhoneme(phonemes, syllable.tone, vc, ValidateAlias(vc)); - break; - } else { - continue; - } - } - } - } - - for (var i = firstC; i < lastC; i++) { - var ccv = $"{string.Join("", cc.Skip(i + 1))} {v}"; - var ccv1 = $"{string.Join("", cc.Skip(i + 1))}{v}"; - var cc1 = $"{string.Join(" ", cc.Skip(i))}"; - var lcv = $"{cc.Last()} {v}"; - var cv = $"{cc.Last()}{v}"; - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - var c1 = cc[i]; - var c2 = cc[i + 1]; - bool c1IsException = consExceptions.Contains(c1); - bool c2IsException = consExceptions.Contains(c2); - - // Scenario 1: Both are NOT exceptions - if (!c1IsException && !c2IsException) { - // [C1 -] [- C2] - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); - } - // Scenario 2: C1 is an exception, C2 is NOT - else if (c1IsException && !c2IsException) { - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - } - // Scenario 3: C1 is NOT an exception, C2 is - else if (!c1IsException && c2IsException) { - cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); - } - // Scenario 4: Both are exceptions - else if (c1IsException && c2IsException) { - cc1 = ""; - } - } - // CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // [C1 C2C3] - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - } - // CV - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - - if (i + 1 < lastC) { - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CC FALLBACKS - if (!HasOto(cc1, syllable.tone) || (!HasOto(ValidateAlias(cc1), syllable.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", syllable.tone))) { - var c1 = cc[i]; - var c2 = cc[i + 1]; - bool c1IsException = consExceptions.Contains(c1); - bool c2IsException = consExceptions.Contains(c2); - - // Scenario 1: Both are NOT exceptions - if (!c1IsException && !c2IsException) { - // [C1 -] [- C2] - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - TryAddPhoneme(phonemes, syllable.tone, AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, "")); - } - // Scenario 2: C1 is an exception, C2 is NOT - else if (c1IsException && !c2IsException) { - cc1 = AliasFormat($"{c2}", "cc_inB", syllable.vowelTone, ""); - } - // Scenario 3: C1 is NOT an exception, C2 is - else if (!c1IsException && c2IsException) { - cc1 = AliasFormat($"{c1}", "cc_endB", syllable.vowelTone, ""); - } - // Scenario 4: Both are exceptions - else if (c1IsException && c2IsException) { - cc1 = ""; - } - } - if (!HasOto(cc1, syllable.tone)) { - cc1 = ValidateAlias(cc1); - } - // CCV - if (CurrentWordCc.Length >= 2) { - if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone) || HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { - basePhoneme = AliasFormat($"{string.Join("", cc.Skip(i + 1))} {v}", "dynMid", syllable.vowelTone, ""); - lastC = i; - } else if (HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone) || HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - } - // [C1 C2C3] - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; - } - // CV - } else if (CurrentWordCc.Length == 1 && PreviousWordCc.Length == 1) { - basePhoneme = AliasFormat($"{cc.Last()} {v}", "dynMid", syllable.vowelTone, ""); - // [C1 C2] - if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { - cPV_FallBack = true; - basePhoneme = v; - cc1 = ValidateAlias(cc1); - } - - if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { - // like [V C1] [C1 C2] [C2 C3] [C3 ..] - TryAddPhoneme(phonemes, syllable.vowelTone, cc1); - } else if (TryAddPhoneme(phonemes, syllable.tone, cc1)) { - // like [V C1] [C1 C2] [C2 ..] - if (cc1.Contains($"{string.Join(" ", cc.Skip(i + 1))}")) { - i++; - } - } else { - // like [V C1] [C1] [C2 ..] - TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); - } - } else { - TryAddPhoneme(phonemes, syllable.tone, cc1); - } - } - - phonemes.Add(basePhoneme); - return phonemes; - } - - protected override List ProcessEnding(Ending ending) { - string prevV = ReplacePhoneme(ending.prevV, ending.tone); - string[] cc = ending.cc.Select(c => ReplacePhoneme(c, ending.tone)).ToArray(); - string v = ReplacePhoneme(ending.prevV, ending.tone); - var phonemes = new List(); - var lastC = cc.Length - 1; - var firstC = 0; - string t = ending.HasTail ? ReplacePhoneme(ending.tail, ending.tone) : "-"; - - if (ending.IsEndingV) { - var vR = $"{v} {t}"; - var vR2 = $"{v}{t}"; - if (HasOto(vR, ending.tone) || HasOto(ValidateAlias(vR), ending.tone) || HasOto(vR2, ending.tone) || HasOto(ValidateAlias(vR2), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t), ValidateAlias(AliasFormat($"{v}", "ending", ending.tone, "", t))); - } - } else if (ending.IsEndingVCWithOneConsonant) { - var vc = $"{v} {cc[0]}"; - var vcr = $"{v} {cc[0]}{t}"; - var vcr2 = $"{v}{cc[0]} {t}"; - var vcr3 = $"{v}{cc[0]}{t}"; - if (!RomajiException.Contains(cc[0])) { - if (HasOto(vcr, ending.tone) && HasOto(ValidateAlias(vcr), ending.tone) || HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t), ValidateAlias(AliasFormat($"{v} {cc[0]}", "dynEnd", ending.tone, "", t))); - } else if (!HasOto(vcr, ending.tone) && !HasOto(ValidateAlias(vcr), ending.tone) || !HasOto(vcr2, ending.tone) && HasOto(ValidateAlias(vcr2), ending.tone) || !HasOto(vcr3, ending.tone) && HasOto(ValidateAlias(vcr3), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, vc); - if (vc.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); - } - } else { - TryAddPhoneme(phonemes, ending.tone, vc); - if (vc.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[0]}", "ending", ending.tone, "", t)); - } - } - } - } else { - for (var i = lastC; i >= 0; i--) { - var vr = $"{v} {t}"; - var vr1 = $"{v} R"; - var vr2 = $"{v}{t}"; - var vcc = $"{v} {string.Join("", cc.Take(2))}{t}"; - var vcc2 = $"{v}{string.Join(" ", cc.Take(2))} {t}"; - var vcc3 = $"{v}{string.Join(" ", cc.Take(2))}"; - var vcc4 = $"{v} {string.Join("", cc.Take(2))}"; - var vc = $"{v} {cc[0]}"; - if (!RomajiException.Contains(cc[0])) { - if (i == 0) { - if (HasOto(vr, ending.tone) || HasOto(ValidateAlias(vr), ending.tone) || HasOto(vr2, ending.tone) || HasOto(ValidateAlias(vr2), ending.tone) || HasOto(vr1, ending.tone) || HasOto(ValidateAlias(vr1), ending.tone) && !HasOto(vc, ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{v}", "ending", ending.tone, "", t)); - } - break; - } else if (HasOto(vcc, ending.tone) && HasOto(ValidateAlias(vcc), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc); - firstC = 1; - break; - } else if (HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) && lastC == 1 && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc2); - firstC = 1; - break; - } else if (HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc3); - if (vcc3.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); - } - } - firstC = 1; - break; - } else if (HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone) && !ccvException.Contains(cc[0])) { - TryAddPhoneme(phonemes, ending.tone, vcc4); - if (vcc4.EndsWith(cc.Last()) && lastC == 1) { - if (consonants.Contains(cc.Last())) { - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc.Last()}", "ending", ending.tone, "", t)); - } - } - firstC = 1; - break; - } else if (!!HasOto(vcc, ending.tone) && !HasOto(ValidateAlias(vcc), ending.tone) - || !HasOto(vcc2, ending.tone) && HasOto(ValidateAlias(vcc2), ending.tone) - || !HasOto(vcc3, ending.tone) && HasOto(ValidateAlias(vcc3), ending.tone) - || !HasOto(vcc4, ending.tone) && HasOto(ValidateAlias(vcc4), ending.tone)) { - TryAddPhoneme(phonemes, ending.tone, vc); - break; - } else { - TryAddPhoneme(phonemes, ending.tone, vc); - break; - } - } - } - for (var i = firstC; i < lastC; i++) { - var cc1 = $"{cc[i]} {cc[i + 1]}"; - if (i < cc.Length - 2) { - var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc2, ending.tone)) { - cc2 = ValidateAlias(cc2); - } - - if (!HasOto(cc2, ending.tone) && !HasOto($"{cc[i + 1]} {cc[i + 2]}", ending.tone)) { - // [C1 -] [- C2] - cc2 = AliasFormat($"{cc[i + 2]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (HasOto(cc1, ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone))) { - // like [C1 C2][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc1); - } else if ((HasOto(cc[i], ending.tone) || HasOto(ValidateAlias(cc[i]), ending.tone) && (HasOto(cc2, ending.tone) || HasOto($"{cc[i + 1]} {cc[i + 2]}{t}", ending.tone) || HasOto(ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"), ending.tone)))) { - // like [C1 C2-][C3 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i]); - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {cc[i + 2]}{t}", ValidateAlias($"{cc[i + 1]} {cc[i + 2]}{t}"))) { - // like [C1 C2-][C3 ...] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "", t)); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_endB", ending.tone, "", t)); - i++; - } else { - // like [C1][C2 ...] - TryAddPhoneme(phonemes, ending.tone, cc[i], ValidateAlias(cc[i]), $"{cc[i]} {t}", ValidateAlias($"{cc[i]} {t}")); - TryAddPhoneme(phonemes, ending.tone, cc[i + 1], ValidateAlias(cc[i + 1]), $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}")); - i++; - } - } else { - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - // [C1 -] [- C2] - if (!HasOto(cc1, ending.tone) || !HasOto(ValidateAlias(cc1), ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - cc1 = AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, ""); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i]}", "cc_endB", ending.tone, "", t)); - } - if (!HasOto(cc1, ending.tone)) { - cc1 = ValidateAlias(cc1); - } - if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]}{t}", ValidateAlias($"{cc[i]} {cc[i + 1]}{t}"))) { - // like [C1 C2-] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]} {cc[i + 1]} {t}", ValidateAlias($"{cc[i]} {cc[i + 1]} {t}"))) { - // like [C1 C2 -] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]}{t}", ValidateAlias($"{cc[i]}{cc[i + 1]}{t}"))) { - // like [C1C2-] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, $"{cc[i]}{cc[i + 1]} {t}", ValidateAlias($"{cc[i]}{cc[i + 1]} {t}"))) { - // like [C1C2 -] - i++; - } else if (TryAddPhoneme(phonemes, ending.tone, cc1, ValidateAlias(cc1))) { - // like [C1 C2][C2 -] - TryAddPhoneme(phonemes, ending.tone, $"{cc[i + 1]} {t}", ValidateAlias($"{cc[i + 1]} {t}"), cc[i + 1], ValidateAlias(cc[i + 1])); - i++; - } else if (!HasOto(cc1, ending.tone) && !HasOto($"{cc[i]} {cc[i + 1]}", ending.tone)) { - // [C1 -] [- C2] - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 1]}", "cc_inB", ending.tone, "")); - TryAddPhoneme(phonemes, ending.tone, AliasFormat($"{cc[i + 2]}", "cc_endB", ending.tone, "", t)); - i++; - } - } - } - } - return phonemes; - } - 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[] { "" } }, - { "dynEnd", new string[] { "" } }, - { "startingV", new string[] { "-", "- ", "_", "" } }, - { "vcEx", new string[] { $"{prevV} ", $"{prevV}" } }, - { "vvExtend", new string[] { "", "_", "-", "- " } }, - { "cv", new string[] { "-", "", "- ", "_" } }, - { "cvStart", new string[] { "-", "- ", "_" } }, - { "ending", new string[] { $" {t}", $"{t}"} }, - { "ending_mix", new string[] { $"{t}", $" {t}", "--" } }, - { "cc", new string[] { "", "-", "- ", "_" } }, - { "cc_start", new string[] { "- ", "-", "_" } }, - { "cc_end", new string[] { $" {t}", $"{t}", "" } }, - { "cc_inB", new string[] { "_", "-", "- " } }, - { "cc_endB", new string[] { "_", $"{t}", $" {t}" } }, - { "cc_mix", new string[] { $" {t}", " R", $"{t}", "", "_", $"{t} ", $"{t}" } }, - { "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 = ""; - // 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; - } - - // 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" - $"-{consonant}{vowel}", // "-CV" - $"-{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)) { - return variation; - } - } - } - - 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]; - vowel = parts[1]; - } else { - consonant = alias; - } - var dynamicVariations1 = new List { - $"{consonant}{vowel}", // "CV" - $"{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)) { - return variation1; - } - } - } - - 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]; - vowel = parts[0]; - } else { - consonant = alias; - } - var dynamicVariations1 = new List { - $"{vowel}{consonant} -", // "VC -" - $"{vowel} {consonant}-", // "V C-" - $"{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)) { - return 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) { - aliasFormat = (counter % 2 == 0) ? $"{alias}{format}" : $"{format}{alias}"; - counter++; - } else if (type.Contains("end") || type.Contains("End") && !(type.Contains("dynEnd"))) { - aliasFormat = $"{alias}{format}"; - } else { - aliasFormat = $"{format}{alias}"; - } - // Check if the formatted alias exists - if (HasOto(aliasFormat, tone) || HasOto(ValidateAlias(aliasFormat), tone)) { - return aliasFormat; - } - } - return alias; - } - - protected override string ValidateAlias(string alias) { - - // VALIDATE ALIAS DEPENDING ON METHOD - if (isMissingVPhonemes) { - foreach (var fb in missingVphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isMissingCPhonemes) { - foreach (var fb in missingCphonemes.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - if (isYamlFallbacks) { - foreach (var fb in yamlFallbacks.OrderByDescending(f => f.Key.Length)) { - alias = alias.Replace(fb.Key, fb.Value); - } - } - - 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; - } - } - - foreach (var v in vowels) { - if (alias.EndsWith("-")) { - return base.GetTransitionBasicLengthMs() * 0.5; - } - } - - // 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; - } - } - - 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; - } - } - - return base.GetTransitionBasicLengthMs() * transitionMultiplier; - } - } -} diff --git a/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs b/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs deleted file mode 100644 index a2852dcfe..000000000 --- a/GenericSyllableBasedPhonemizer/SyllableBasedPhonemizer.cs +++ /dev/null @@ -1,1780 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using OpenUtau.Api; -using OpenUtau.Core.Ustx; -using System.Linq; -using System.IO; -using Serilog; -using System.Threading.Tasks; -using static OpenUtau.Api.Phonemizer; -using System.Collections; - -namespace OpenUtau.Plugin.Builtin { - /// - /// Use this class as a base for easier phonemizer configuration. Works for vb styles like VCV, VCCV, CVC etc; - /// - /// - Supports dictionary; - /// - Automatically align phonemes to notes; - /// - Supports syllable extension; - /// - Automatically calculates transition phonemes length, with constants by default, - /// but there is a pre-created function to use Oto value; - /// - The transition length is scaled based on Tempo and note length. - /// - /// Note that here "Vowel" means "stretchable phoneme" and "Consonant" means "non-stretchable phoneme". - /// - /// So if a diphthong is represented with several phonemes, like English "byke" -> [b a y k], - /// then [a] as a stretchable phoneme would be a "Vowel", and [y] would be a "Consonant". - /// - /// Some reclists have consonants that also may behave as vowels, like long "M" and "N". They are "Vowels". - /// - /// If your oto hase same symbols for them, like "n" for stretchable "n" from a long note and "n" from CV, - /// then you can use a vitrual symbol [N], and then replace it with [n] in ValidateAlias(). - /// - public abstract class SyllableBasedPhonemizer : Phonemizer { - - /// - /// Syllable is [V] [C..] [V] - /// - protected struct Syllable { - /// - /// vowel from previous syllable for VC - /// - public string prevV; - /// - /// CCs, may be empty - /// - public string[] cc; - /// - /// "base" note. May not actually be vowel, if only consonants way provided - /// - public string v; - /// - /// Start position for vowel. All VC CC goes before this position - /// - public int position; - /// - /// previous note duration, i.e. this is container for VC and CC notes - /// - public int duration; - /// - /// Tone for VC and CC - /// - public int tone; - /// - /// Other phoneme attributes for VC and CC - /// - public PhonemeAttributes[] attr; - /// - /// tone for base "vowel" phoneme - /// - public int vowelTone; - /// - /// Other phoneme attributes for base "vowel" phoneme - /// - public PhonemeAttributes[] vowelAttr; - - /// - /// 0 if no consonants are taken from previous word; - /// 1 means first one is taken from previous word, etc. - /// - public int prevWordConsonantsCount; - - /// - /// If true, you may use alias extension instead of VV, by putting the phoneme as null if vowels match. - /// If you do this when canAliasBeExtended == false, the note will produce no phoneme and there will be a break. - /// Use CanMakeAliasExtension() to pass all checks if alias extension is possible - /// - public bool canAliasBeExtended; - - // helpers - public bool IsStartingV => prevV == "" && cc.Length == 0; - public bool IsVV => prevV != "" && cc.Length == 0; - - public bool IsStartingCV => prevV == "" && cc.Length > 0; - public bool IsVCV => prevV != "" && cc.Length > 0; - - public bool IsStartingCVWithOneConsonant => prevV == "" && cc.Length == 1; - public bool IsVCVWithOneConsonant => prevV != "" && cc.Length == 1; - - public bool IsStartingCVWithMoreThanOneConsonant => prevV == "" && cc.Length > 1; - public bool IsVCVWithMoreThanOneConsonant => prevV != "" && cc.Length > 1; - - public string[] PreviousWordCc => cc.Take(prevWordConsonantsCount).ToArray(); - public string[] CurrentWordCc => cc.Skip(prevWordConsonantsCount).ToArray(); - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")} {v}"; - } - } - - protected struct Ending { - /// - /// vowel from the last syllable to make VC - /// - public string prevV; - /// - /// actuall CC at the ending - /// - public string[] cc; - /// - /// The exact lyric/symbol of the tail (e.g., "R", "br", "-", etc.) - /// - public string tail; - public bool HasTail => !string.IsNullOrEmpty(tail); - /// - /// last note position + duration, all phonemes must be less than this - /// - public int position; - /// - /// last syllable length, max container for all VC CC C- - /// - public int duration; - /// - /// the tone from last syllable, for all ending phonemes - /// - public int tone; - /// - /// Other phoneme attributes from last syllable - /// - public PhonemeAttributes[] attr; - - // helpers - public bool IsEndingV => cc.Length == 0; - public bool IsEndingVC => cc.Length > 0; - public bool IsEndingVCWithOneConsonant => cc.Length == 1; - public bool IsEndingVCWithMoreThanOneConsonant => cc.Length > 1; - - public override string ToString() { - return $"({prevV}) {(cc != null ? string.Join(" ", cc) : "")}"; - } - } - - public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { - error = ""; - var mainNote = notes[0]; - if (mainNote.lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return MakeForcedAliasResult(mainNote); - } - if (hasDictionary && isDictionaryLoading) { - return MakeSimpleResult(""); - } - - runtimeGlides.Clear(); - - var syllables = MakeSyllables(notes, MakeEnding(prevNeighbours)); - if (syllables == null) { - return HandleError(); - } - - var phonemes = new List(); - int globalPhonemeIndex = 0; // Track the exact index for OpenUtau's UI - - foreach (var syllable in syllables) { - var modifiedSyllable = ApplyBoundaryReplacements(syllable); - - if (tails.Contains(modifiedSyllable.v)) { - var ending = new Ending { - prevV = modifiedSyllable.prevV, - cc = modifiedSyllable.cc, - tail = modifiedSyllable.v, - position = modifiedSyllable.position, - duration = modifiedSyllable.duration, - tone = modifiedSyllable.tone, - attr = modifiedSyllable.attr - }; - - var endingPhonemes = ProcessEnding(ending); - - if (endingPhonemes != null) { - phonemes.AddRange(MakePhonemes(endingPhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex)); - globalPhonemeIndex += endingPhonemes.Count; - } - continue; - } - - var syllablePhonemes = ProcessSyllable(modifiedSyllable); - var madePhonemes = MakePhonemes(syllablePhonemes, modifiedSyllable.duration, modifiedSyllable.position, false, modifiedSyllable.tone, mainNote.phonemeAttributes, globalPhonemeIndex).ToList(); - int currentSyllablePhonemeCount = syllablePhonemes.Count; - - var basePhoneme = madePhonemes.LastOrDefault(); - string baseAlias = basePhoneme.phoneme ?? ""; - if (vowelSustains.TryGetValue(baseAlias, out var sustainData) || - vowelSustains.TryGetValue(modifiedSyllable.v, out sustainData)) { - - string mappedSustain = ValidateAliasIfNeeded(sustainData.sustain, modifiedSyllable.tone); - if (HasOto(mappedSustain, modifiedSyllable.tone) || HasOto(sustainData.sustain, modifiedSyllable.tone)) { - int offsetTicks = MsToTick(GetTransitionBasicLengthMsByConstant() * sustainData.offset); - madePhonemes.Add(new Phoneme { - phoneme = sustainData.sustain, - position = basePhoneme.position + offsetTicks, - index = globalPhonemeIndex + currentSyllablePhonemeCount - }); - currentSyllablePhonemeCount++; - } - } - phonemes.AddRange(madePhonemes); - globalPhonemeIndex += currentSyllablePhonemeCount; - } - - if (!nextNeighbour.HasValue) { - var tryEnding = MakeEnding(notes); - if (tryEnding.HasValue) { - var ending = tryEnding.Value; - - if (nextNeighbour.HasValue && tails.Contains(nextNeighbour.Value.lyric)) { - ending.tail = nextNeighbour.Value.lyric; - } - - var modifiedEnding = ApplyBoundaryReplacements(ending); - var endingPhonemes = ProcessEnding(modifiedEnding); - - if (endingPhonemes != null) { - 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 = finalPhonemes - }; - } - - protected virtual Phoneme[] AssignAllAffixes(List phonemes, Note[] notes, Note[] prevs) { - int noteIndex = 0; - for (int i = 0; i < phonemes.Count; i++) { - var attr = notes[0].phonemeAttributes?.FirstOrDefault(attr => attr.index == i) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var phoneme = phonemes[i]; - while (noteIndex < notes.Length - 1 && notes[noteIndex].position - notes[0].position < phoneme.position) { - noteIndex++; - } - - 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); - validatedAlias = MapPhoneme(validatedAlias, tone + toneShift, color, alt, singer); - - phoneme.phoneme = validatedAlias; - } else { - phoneme.phoneme = null; - phoneme.position = 0; - } - - phonemes[i] = phoneme; - } - return phonemes.ToArray(); - } - - private Result HandleError() { - return new Result { - phonemes = new Phoneme[] { - new Phoneme() { - phoneme = error - } - } - }; - } - - protected static readonly YamlDotNet.Serialization.IDeserializer TolerantDeserializer = - new YamlDotNet.Serialization.DeserializerBuilder() - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.UnderscoredNamingConvention.Instance) - .IgnoreUnmatchedProperties() - .Build(); - - public override void SetSinger(USinger singer) { - if (this.singer != singer) { - this.singer = singer; - dictionaries.Clear(); - - if (this.singer == null || !this.singer.Loaded) { - return; - } - - if (string.IsNullOrEmpty(YamlFileName)) { - if (backupVowels != null) this.vowels = backupVowels; - else this.vowels = GetVowels(); - - if (backupConsonants != null) this.consonants = backupConsonants; - else this.consonants = GetConsonants(); - - if (backupDictionaryReplacements != null) { - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) { - dictionaryReplacements[kvp.Key] = kvp.Value; - } - } - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - return; - } - - // file paths - string globalFile = Path.Combine(PluginDir, YamlFileName); - string singerFile = (singer != null && singer.Found && singer.Loaded && !string.IsNullOrEmpty(singer.Location)) - ? Path.Combine(singer.Location, YamlFileName) - : null; - - // Local helper function to update and backup YAML files safely - void UpdateYamlIfNeeded(string filePath, bool isGlobal) { - if (string.IsNullOrEmpty(filePath)) return; - - bool shouldWriteTemplate = false; - bool shouldBackupOldFile = false; - string currentVersion = "unknown"; - - if (File.Exists(filePath)) { - if (YamlTemplate != null && !string.IsNullOrEmpty(YamlVersion)) { - try { - var checkData = Core.Yaml.DefaultDeserializer.Deserialize(File.ReadAllText(filePath)); - currentVersion = checkData?.version?.Trim() ?? ""; - - // Update if missing, or if the parsed decimal is strictly lower than the target YamlVersion - if (string.IsNullOrEmpty(currentVersion)) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } else if (Version.TryParse(currentVersion, out Version currV) && - Version.TryParse(YamlVersion, out Version targetV)) { - if (currV < targetV) { - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } else if (currentVersion != YamlVersion && !double.TryParse(currentVersion, out _)) { - // Fallback string check if version formats aren't purely numeric (e.g., "1.3b") - shouldWriteTemplate = true; - shouldBackupOldFile = true; - } - } catch (Exception ex) { - Log.Error(ex, $"Syntax error detected in '{filePath}'. Skipping template update to protect data."); - return; - } - } - } else if (isGlobal && YamlTemplate != null) { - shouldWriteTemplate = true; - } - - if (shouldBackupOldFile && File.Exists(filePath)) { - try { - // Include the version in the backup file name, e.g., arpa_backup(1.2).yaml - string safeVersion = string.IsNullOrEmpty(currentVersion) ? "unknown" : currentVersion; - string backupFile = Path.Combine(Path.GetDirectoryName(filePath), $"{Path.GetFileNameWithoutExtension(YamlFileName)}_backup({safeVersion}){Path.GetExtension(YamlFileName)}"); - - if (File.Exists(backupFile)) File.Delete(backupFile); - File.Move(filePath, backupFile); - Log.Information($"Old {YamlFileName} backed up to {backupFile}"); - } catch (Exception e) { - Log.Error(e, $"Failed to back up {filePath}. Aborting overwrite."); - return; - } - } - - if (shouldWriteTemplate) { - try { - File.WriteAllBytes(filePath, YamlTemplate); - Log.Information($"'{filePath}' created or updated to version {YamlVersion ?? "default"}"); - } catch (Exception e) { - Log.Error(e, $"Failed to write template to {filePath}"); - } - } - } - - UpdateYamlIfNeeded(globalFile, true); - UpdateYamlIfNeeded(singerFile, false); - - // add to parsing list (Global first, Singer second) - var filesToParse = new List(); - if (File.Exists(globalFile)) filesToParse.Add(globalFile); - if (!string.IsNullOrEmpty(singerFile) && File.Exists(singerFile)) filesToParse.Add(singerFile); - - // backups of hardcoded defaults exist - if (backupVowels == null) backupVowels = GetVowels() ?? Array.Empty(); - if (backupConsonants == null) backupConsonants = GetConsonants() ?? Array.Empty(); - if (backupDictionaryReplacements == null) backupDictionaryReplacements = new Dictionary(dictionaryReplacements); - if (backupDiphthongTails == null) backupDiphthongTails = new Dictionary(diphthongTails); - if (backupDiphthongSplits == null) backupDiphthongSplits = new Dictionary(diphthongSplits); - - // reset live arrays/lists back to defaults before stacking - vowels = backupVowels; - consonants = backupConsonants; - tails = "-".Split(','); - - fricative = Array.Empty(); - aspirate = Array.Empty(); - semivowel = Array.Empty(); - liquid = Array.Empty(); - nasal = Array.Empty(); - stop = Array.Empty(); - tap = Array.Empty(); - affricate = Array.Empty(); - - dictionaryReplacements.Clear(); - foreach (var kvp in backupDictionaryReplacements) dictionaryReplacements[kvp.Key] = kvp.Value; - - diphthongTails.Clear(); - foreach (var kvp in backupDiphthongTails) diphthongTails[kvp.Key] = kvp.Value; - - diphthongSplits.Clear(); - foreach (var kvp in backupDiphthongSplits) diphthongSplits[kvp.Key] = kvp.Value; - - mergingReplacements.Clear(); - splittingReplacements.Clear(); - yamlFallbacks.Clear(); - PhonemeOverrides.Clear(); - if (backupVowelSustains == null) backupVowelSustains = new Dictionary(vowelSustains); - vowelSustains.Clear(); - foreach (var kvp in backupVowelSustains) vowelSustains[kvp.Key] = kvp.Value; - - // parse the files sequentially (Singer configs seamlessly overwrite global configs) - foreach (var file in filesToParse) { - try { - var data = TolerantDeserializer.Deserialize(File.ReadAllText(file)); - - var yamlVowels = data.symbols?.Where(s => s.type == "vowel" || s.type == "diphthong").Select(s => s.symbol).ToArray() ?? Array.Empty(); - vowels = yamlVowels.Concat(vowels).Distinct().ToArray(); - - var yamlTails = data.symbols?.Where(s => s.type == "tail").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tails = yamlTails.Concat(tails).Distinct().ToArray(); - - if (data?.isglides != null) enableGlides = data.isglides.Value; - - var yFricative = data.symbols?.Where(s => s.type == "fricative").Select(s => s.symbol).ToArray() ?? Array.Empty(); - fricative = yFricative.Concat(fricative).Distinct().ToArray(); - var yAspirate = data.symbols?.Where(s => s.type == "aspirate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - aspirate = yAspirate.Concat(aspirate).Distinct().ToArray(); - var ySemivowel = data.symbols?.Where(s => s.type == "semivowel").Select(s => s.symbol).ToArray() ?? Array.Empty(); - semivowel = ySemivowel.Concat(semivowel).Distinct().ToArray(); - var yLiquid = data.symbols?.Where(s => s.type == "liquid").Select(s => s.symbol).ToArray() ?? Array.Empty(); - liquid = yLiquid.Concat(liquid).Distinct().ToArray(); - var yNasal = data.symbols?.Where(s => s.type == "nasal").Select(s => s.symbol).ToArray() ?? Array.Empty(); - nasal = yNasal.Concat(nasal).Distinct().ToArray(); - var yStop = data.symbols?.Where(s => s.type == "stop").Select(s => s.symbol).ToArray() ?? Array.Empty(); - stop = yStop.Concat(stop).Distinct().ToArray(); - var yTap = data.symbols?.Where(s => s.type == "tap").Select(s => s.symbol).ToArray() ?? Array.Empty(); - tap = yTap.Concat(tap).Distinct().ToArray(); - var yAffricate = data.symbols?.Where(s => s.type == "affricate").Select(s => s.symbol).ToArray() ?? Array.Empty(); - affricate = yAffricate.Concat(affricate).Distinct().ToArray(); - - var yamlConsonants = yFricative.Concat(yAspirate).Concat(ySemivowel).Concat(yLiquid) - .Concat(yNasal).Concat(yStop).Concat(yTap).Concat(yAffricate).ToArray(); - consonants = yamlConsonants.Concat(consonants).Distinct().ToArray(); - - // DIPHTHONG AUTO-TAIL DETECTION - var yamlDiphthongs = data.symbols?.Where(s => s.type == "diphthong").Select(s => s.symbol).Distinct().ToArray() ?? Array.Empty(); - var dynamicTails = consonants.OrderByDescending(c => c.Length).ToArray(); - - foreach (var d in yamlDiphthongs) { - if (!diphthongSplits.ContainsKey(d)) { - foreach (var tail in dynamicTails) { - if (d.EndsWith(tail) && d != tail) { - diphthongTails[d] = tail; - break; - } - } - } - } - - // OVERRIDES & DICTIONARIES (Singer keys overwrite global keys) - if (data?.timings != null) { - foreach (var t in data.timings) PhonemeOverrides[t.symbol] = t.value; - } - - if (data?.replacements != null) { - var localMerge = new List(); - var localSplit = new List(); - string GetFromKey(object fromObj) { - if (fromObj is string s) return s; - if (fromObj is System.Collections.IEnumerable e) { - return string.Join(",", e.Cast().Select(x => x?.ToString() ?? "")); - } - return ""; - } - - foreach (var rawReplacement in data.replacements) { - string fromKey = GetFromKey(rawReplacement.from); - mergingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - splittingReplacements.RemoveAll(r => GetFromKey(r.from) == fromKey); - - if (rawReplacement.from is string fromStr) { - dictionaryReplacements.Remove(fromStr); - dictionaryReplacements.Remove(fromStr.ToLower()); - dictionaryReplacements.Remove(fromStr.ToUpper()); - } - - List fromList = rawReplacement.FromList; - List toList = rawReplacement.ToList; - object parsedFrom = fromList.Count == 1 ? fromList[0] : fromList.ToArray(); - object parsedTo = toList.Count == 1 ? toList[0] : toList.ToArray(); - - var cleanReplacement = new Replacement { - from = parsedFrom, - to = parsedTo, - where = rawReplacement.where - }; - - if (parsedFrom is string fromString) { - if (parsedTo is string toString) { - // Dictionary handles simple 1-to-1 replacements - dictionaryReplacements[fromString] = toString; - } else { - // 1-to-Many goes to Split - localSplit.Add(cleanReplacement); - } - } else { - // Many-to-Any goes to Merge - localMerge.Add(cleanReplacement); - } - } - mergingReplacements.InsertRange(0, localMerge); - splittingReplacements.InsertRange(0, localSplit); - } - - if (data?.fallbacks != null) { - foreach (var df in data.fallbacks) { - if (!string.IsNullOrEmpty(df.from) && !string.IsNullOrEmpty(df.to)) { - yamlFallbacks[df.from] = df.to; - } - } - } - - if (data?.diphthongs != null) { - foreach (var d in data.diphthongs) { - if (!string.IsNullOrEmpty(d.from) && !string.IsNullOrEmpty(d.to)) { - diphthongTails[d.from] = d.to; - } - } - } - - if (data?.vowelsustains != null) { - foreach (var v in data.vowelsustains) { - if (!string.IsNullOrEmpty(v.symbol) && !string.IsNullOrEmpty(v.sustain)) { - vowelSustains[v.symbol] = (v.sustain, v.offset); - } - } - } - - } catch (Exception ex) { - Log.Error($"Failed to parse {file}: {ex.Message}"); - } - } - - if (!hasDictionary) { - ReadDictionaryAndInit(); - } else { - Init(); - } - } - } - - protected USinger singer; - protected bool hasDictionary => dictionaries.ContainsKey(GetType()); - protected IG2p dictionary => dictionaries[GetType()]; - protected bool isDictionaryLoading => dictionaries[GetType()] == null; - protected double TransitionBasicLengthMs => 100; - - private Dictionary dictionaries = new Dictionary(); - private const string FORCED_ALIAS_SYMBOL = "?"; - private string error = ""; - 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 - /// - /// - protected abstract string[] GetVowels(); - - /// - /// Returns list of consonants. Only needed if there is a dictionary - /// - /// - protected virtual string[] GetConsonants() { - throw new NotImplementedException(); - } - - /// - /// returns phoneme symbols, like, VCV, or VC + CV, or -CV, etc - /// - /// List of phonemes - protected abstract List ProcessSyllable(Syllable syllable); - - /// - /// phoneme symbols for ending, like, V-, or VC-, or VC+C - /// - protected abstract List ProcessEnding(Ending ending); - - /// - /// simple alias to alias fallback - /// - /// - protected virtual Dictionary GetAliasesFallback() { return null; } - - /// - /// Use to some custom init, if needed - /// - protected virtual void Init() { } - - /// - /// Dictionary name. Must be stored in Dictionaries folder. - /// If missing or can't be read, phonetic input is used - /// - /// - protected virtual string GetDictionaryName() { return null; } - - /// - /// extracts array of phoneme symbols from note. Override for procedural dictionary or something - /// reads from dictionary if provided - /// - /// - /// - protected virtual string[] GetSymbols(Note note) { - string[] getSymbolsRaw(string lyrics) { - if (lyrics == null) { - return new string[0]; - } else return lyrics.Split(" "); - } - - if (tails.Contains(note.lyric)) { - return new string[] { note.lyric }; - } - - if (hasDictionary) { - if (!string.IsNullOrEmpty(note.phoneticHint)) { - return getSymbolsRaw(note.phoneticHint); - } - - var result = new List(); - foreach (var subword in note.lyric.Trim().ToLowerInvariant().Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries)) { - var subResult = dictionary.Query(subword); - if (subResult == null) { - subResult = HandleWordNotFound(note); - if (subResult == null) { - return null; - } - } else { - for (int i = 0; i < subResult.Length; i++) { - string phoneme = subResult[i]; - if (dictionaryReplacements.TryGetValue(phoneme, out string replaced)) { - subResult[i] = replaced; - } else if (dictionaryReplacements.TryGetValue(subResult[i], out string replacedExact)) { - subResult[i] = replacedExact; - } - } - } - result.AddRange(subResult); - } - return result.ToArray(); - } else { - return getSymbolsRaw(note.lyric); - } - } - - /// - /// 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. - /// - /// - protected virtual Dictionary GetDictionaryPhonemesReplacement() { - return dictionaryReplacements ?? new Dictionary(); - } - private string[] backupVowels = null; - private string[] backupConsonants = null; - private Dictionary backupDiphthongTails = null; - private Dictionary backupDiphthongSplits = null; - private Dictionary backupDictionaryReplacements = null; - protected Dictionary vowelSustains = new Dictionary(); - private Dictionary backupVowelSustains = null; - - /// - /// separates symbols to syllables, without an ending. - /// - /// - /// - /// - protected virtual Syllable[] MakeSyllables(Note[] inputNotes, Ending? prevEnding) { - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - var firstVowelId = vowelIds[0]; - if (notes.Length < vowelIds.Length) { - error = $"Not enough extension notes, {vowelIds.Length - notes.Length} more expected"; - return null; - } - - var syllables = new Syllable[vowelIds.Length]; - - // Making the first syllable - if (prevEnding.HasValue) { - var prevEndingValue = prevEnding.Value; - var beginningCc = prevEndingValue.cc.ToList(); - beginningCc.AddRange(symbols.Take(firstVowelId)); - - // If we had a prev neighbour ending, let's take info from it - syllables[0] = new Syllable() { - prevV = prevEndingValue.prevV, - cc = beginningCc.ToArray(), - v = symbols[firstVowelId], - tone = prevEndingValue.tone, - attr = prevEndingValue.attr, - duration = prevEndingValue.duration, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes, - prevWordConsonantsCount = prevEndingValue.cc.Count() - }; - } else { - // there is only empty space before us - syllables[0] = new Syllable() { - prevV = "", - cc = symbols.Take(firstVowelId).ToArray(), - v = symbols[firstVowelId], - tone = notes[0].tone, - attr = notes[0].phonemeAttributes, - duration = -1, - position = 0, - vowelTone = notes[0].tone, - vowelAttr = notes[0].phonemeAttributes - }; - } - - // normal syllables after the first one - var noteI = 1; - var ccs = new List(); - var position = 0; - var lastSymbolI = firstVowelId + 1; - for (; lastSymbolI < symbols.Length & noteI < notes.Length; lastSymbolI++) { - if (!vowelIds.Contains(lastSymbolI)) { - ccs.Add(symbols[lastSymbolI]); - } else { - position += notes[noteI - 1].duration; - syllables[noteI] = new Syllable() { - prevV = syllables[noteI - 1].v, - cc = ccs.ToArray(), - v = symbols[lastSymbolI], - tone = notes[noteI - 1].tone, - attr = notes[noteI - 1].phonemeAttributes, - duration = notes[noteI - 1].duration, - position = position, - vowelTone = notes[noteI].tone, - vowelAttr = notes[noteI].phonemeAttributes, - canAliasBeExtended = true // for all not-first notes is allowed - }; - ccs = new List(); - noteI++; - } - } - - return syllables; - } - - /// - /// extracts word ending - /// - /// - /// - protected Ending? MakeEnding(Note[] inputNotes) { - if (inputNotes == null || inputNotes.Length == 0 || inputNotes[0].lyric.StartsWith(FORCED_ALIAS_SYMBOL)) { - return null; - } - - (var symbols, var vowelIds, var notes) = GetSymbolsAndVowels(inputNotes); - if (symbols == null || vowelIds == null || notes == null) { - return null; - } - - return new Ending() { - prevV = symbols[vowelIds.Last()], - cc = symbols.Skip(vowelIds.Last() + 1).ToArray(), - tone = notes.Last().tone, - attr = notes.Last().phonemeAttributes, - duration = notes.Skip(vowelIds.Length - 1).Sum(n => n.duration), - position = notes.Sum(n => n.duration) - }; - } - - /// - /// extracts and validates symbols and vowels - /// - /// - /// - private (string[], int[], Note[]) GetSymbolsAndVowels(Note[] notes) { - var mainNote = notes[0]; - var symbols = GetSymbols(mainNote); - if (symbols == null) { - return (null, null, null); - } - if (symbols.Length == 0) { - symbols = new string[] { "" }; - } - - symbols = ApplyReplacements(symbols.ToList(), false).ToArray(); - symbols = ApplyExtensions(symbols, notes); - List vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - vowelIds.Add(symbols.Length - 1); - } - if (notes.Length < vowelIds.Count) { - notes = HandleNotEnoughNotes(notes, vowelIds); - } - return (symbols, vowelIds.ToArray(), notes); - } - - /// - /// When there are more syllables than notes, recombines notes to match syllables count - /// - /// - /// - /// - protected virtual Note[] HandleNotEnoughNotes(Note[] notes, List vowelIds) { - var newNotes = new List(); - newNotes.AddRange(notes.SkipLast(1)); - var lastNote = notes.Last(); - var position = lastNote.position; - var notesToSplit = vowelIds.Count - newNotes.Count; - var duration = lastNote.duration / notesToSplit / 15 * 15; - for (var i = 0; i < notesToSplit; i++) { - var durationFinal = i != notesToSplit - 1 ? duration : lastNote.duration - duration * (notesToSplit - 1); - newNotes.Add(new Note() { - position = position, - duration = durationFinal, - tone = lastNote.tone, - phonemeAttributes = lastNote.phonemeAttributes - }); - position += durationFinal; - } - - return newNotes.ToArray(); - } - - /// - /// Override this method, if you want to implement some machine converting from a word to phonemes - /// - /// - /// - protected virtual string[] HandleWordNotFound(Note note) { - var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - string alt = attr.alternate?.ToString() ?? string.Empty; - string color = attr.voiceColor; - int toneShift = attr.toneShift; - var mpdlyric = MapPhoneme(note.lyric, note.tone + toneShift, color, alt, singer); - if(HasOto(mpdlyric, note.tone)){ - error = mpdlyric; - }else{ - error = "word not found"; - } - return null; - } - - /// - /// Does this note extend the previous syllable? - /// - /// - /// - protected bool IsSyllableVowelExtensionNote(Note note) { - return note.lyric.StartsWith("+~") || note.lyric.StartsWith("+*"); - } - - /// - /// Used to extract phonemes from CMU Dict word. Override if you need some extra logic - /// - /// - /// - protected virtual string[] GetDictionaryWordPhonemes(string phonemesString) { - return phonemesString.Split(' '); - } - - /// - /// use to validate alias - /// - /// - /// - protected virtual string ValidateAlias(string alias) { - return alias; - } - - /// - /// Defines basic transition length before scaling it according to tempo and note length - /// Use GetTransitionBasicLengthMsByConstant, GetTransitionBasicLengthMsByOto or your own implementation - /// - /// Mapped alias - /// - protected virtual double GetTransitionBasicLengthMs(string alias = "") { - return GetTransitionBasicLengthMsByConstant(); - } - - 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 - /// - /// - protected double GetTempoNoteLengthFactor() { - return (300 - Math.Clamp(bpm, 90, 300)) / (300 - 90) / 3 + 0.33; - } - - protected virtual IG2p[] GetBaseG2ps() { - return Array.Empty(); - } - - protected virtual IG2p LoadBaseDictionary() { - var g2ps = new List(); - - // Native YAML Dictionary Logic - if (!string.IsNullOrEmpty(YamlFileName)) { - string path = Path.Combine(PluginDir, YamlFileName); - - // Write template if missing - if (!File.Exists(path) && YamlTemplate != null) { - Directory.CreateDirectory(PluginDir); - File.WriteAllBytes(path, YamlTemplate); - } - - // Load dictionary from Singer Folder (Highest Priority) - if (singer != null && singer.Found && singer.Loaded) { - string file = Path.Combine(singer.Location, YamlFileName); - if (File.Exists(file)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(file)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {file}"); - } - } - } - - // Load dictionary from Plugin Folder (Fallback Priority) - if (File.Exists(path)) { - try { - g2ps.Add(G2pDictionary.NewBuilder().Load(File.ReadAllText(path)).Build()); - } catch (Exception e) { - Log.Error(e, $"Failed to load {path}"); - } - } - } - // Legacy Text Dictionary Logic (if child uses GetDictionaryName instead of YAML) - else { - var dictionaryName = GetDictionaryName(); - if (!string.IsNullOrEmpty(dictionaryName)) { - var filename = Path.Combine(DictionariesPath, dictionaryName); - if (File.Exists(filename)) { - var dictionaryText = File.ReadAllText(filename); - var builder = G2pDictionary.NewBuilder(); - foreach (var vowel in GetVowels()) builder.AddSymbol(vowel, true); - foreach (var consonant in GetConsonants()) builder.AddSymbol(consonant, false); - builder.AddEntry("a", new string[] { "a" }); - ParseDictionary(dictionaryText, builder); - g2ps.Add(builder.Build()); - } - } - } - - // Append the Child-Specific G2P Models (e.g., ArpabetPlusG2p) - var childG2ps = GetBaseG2ps(); - if (childG2ps != null && childG2ps.Any()) { - g2ps.AddRange(childG2ps); - } - - return new G2pFallbacks(g2ps.ToArray()); - } - - /// - /// Parses CMU dictionary, when phonemes are separated by spaces, and word vs phonemes are separated with two spaces, - /// and replaces phonemes with replacement table - /// Is Running Async! - /// - /// - /// - protected virtual void ParseDictionary(string dictionaryText, G2pDictionary.Builder builder) { - var replacements = GetDictionaryPhonemesReplacement(); - foreach (var line in dictionaryText.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)) { - if (line.StartsWith(";;;")) { - continue; - } - var parts = line.Trim().Split(wordSeparator, StringSplitOptions.None); - if (parts.Length != 2) { - continue; - } - string key = parts[0].ToLowerInvariant(); - var values = GetDictionaryWordPhonemes(parts[1]).Select( - n => replacements != null && replacements.ContainsKey(n) ? replacements[n] : n); - lock (builder) { - builder.AddEntry(key, values); - }; - }; - } - - #region helpers - - /// - /// May be used if you have different logic for short and long notes - /// - /// - /// - protected bool IsShort(Syllable syllable) { - return syllable.duration != -1 && TickToMs(syllable.duration) < GetTransitionBasicLengthMs() * 2; - } - 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 - /// - /// - /// - /// - protected bool HasOto(string alias, int tone) { - return singer.TryGetMappedOto(alias, tone, out _); - } - - /// - /// Can be used for different variants, like exhales [v R], [v -] etc - /// - /// phonemes container to add to - /// to map alias - /// target phoneme variants - /// returns true if added any - protected bool TryAddPhoneme(List sourcePhonemes, int tone, params string[] targetPhonemes) { - foreach (var phoneme in targetPhonemes) { - if (HasOto(phoneme, tone)) { - sourcePhonemes.Add(phoneme); - return true; - } - } - 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 - /// - /// - /// - protected bool CanMakeAliasExtension(Syllable syllable) { - return syllable.canAliasBeExtended && syllable.prevV == syllable.v && syllable.cc.Length == 0; - } - - /// - /// if current syllable is VV and previous one is from the same pitch, - /// you may wan't to just extend the previous alias. Put the phoneme as null fot that - /// - /// - /// - /// - protected bool AreTonesFromTheSameSubbank(int tone1, int tone2) { - if (singer.Subbanks.Count == 1) { - return true; - } - if (tone1 == tone2) { - return true; - } - var toneSets = singer.Subbanks.Select(n => n.toneSet); - foreach (var toneSet in toneSets) { - if (toneSet.Contains(tone1) && toneSet.Contains(tone2)) { - return true; - } - if (toneSet.Contains(tone1) != toneSet.Contains(tone2)) { - return false; - } - } - return true; - } - - protected virtual string YamlFileName => null; - protected virtual byte[] YamlTemplate => null; - protected virtual string YamlVersion => null; - - protected string[] vowels = Array.Empty(); - protected string[] consonants = Array.Empty(); - protected string[] tails = "-,R".Split(','); - protected string[] affricate = Array.Empty(); - protected string[] fricative = Array.Empty(); - protected string[] aspirate = Array.Empty(); - protected string[] semivowel = Array.Empty(); - protected string[] liquid = Array.Empty(); - protected string[] nasal = Array.Empty(); - protected string[] stop = Array.Empty(); - protected string[] tap = Array.Empty(); - - protected Dictionary dictionaryReplacements = new Dictionary(); - protected Dictionary PhonemeOverrides = new Dictionary(); - protected Dictionary yamlFallbacks = new Dictionary(); - protected List consExceptions = new List(); - - protected Dictionary diphthongTails = new Dictionary(); - protected Dictionary diphthongSplits = new Dictionary(); - - 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(); - public Timings[] timings { get; set; } = Array.Empty(); - public DiphthongData[] diphthongs { get; set; } = Array.Empty(); - public VowelSustainData[] vowelsustains { get; set; } = Array.Empty(); - - public struct SymbolData { public string symbol { get; set; } public string type { get; set; } } - public struct Fallbacks { public string from { get; set; } public string to { get; set; } } - public struct Timings { public string symbol { get; set; } public double value { get; set; } } - public struct DiphthongData { public string from { get; set; } public string to { get; set; } } - public struct VowelSustainData { public string symbol { get; set; } public string sustain { get; set; } public double offset { get; set; } } - } - - public class Replacement { - public object from { get; set; } - public object to { get; set; } - public string where { get; set; } = "inside"; - - public List FromList { - get { - if (from is string s) return new List { s }; - if (from is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - - public List ToList { - get { - if (to is string s) return new List { s }; - if (to is IEnumerable list) return list.Select(x => x.ToString() ?? "null").ToList(); - return new List(); - } - } - } - - protected List mergingReplacements = new List(); - protected List splittingReplacements = new List(); - - protected virtual bool IsGroupKeyword(string rulePhoneme) { - // 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 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; - } - } - } - - bool inBaseGroup = false; - switch (baseGroup) { - case "vowel": case "vowels": inBaseGroup = GetVowels().Contains(actualPhoneme); break; - case "consonant": case "consonants": inBaseGroup = GetConsonants().Contains(actualPhoneme); break; - case "affricate": inBaseGroup = affricate.Contains(actualPhoneme); break; - case "fricative": inBaseGroup = fricative.Contains(actualPhoneme); break; - case "aspirate": inBaseGroup = aspirate.Contains(actualPhoneme); break; - case "semivowel": inBaseGroup = semivowel.Contains(actualPhoneme); break; - case "liquid": inBaseGroup = liquid.Contains(actualPhoneme); break; - case "nasal": inBaseGroup = nasal.Contains(actualPhoneme); break; - case "stop": inBaseGroup = stop.Contains(actualPhoneme); break; - case "tap": inBaseGroup = tap.Contains(actualPhoneme); break; - } - - if (!inBaseGroup) return false; - - if (cleanRule.Contains("!")) { - string excluded = cleanRule.Substring(cleanRule.IndexOf('!') + 1).Split(new[] { '=', '&' })[0]; - if (excluded.Split(',').Contains(actualPhoneme)) return false; - } - - if (cleanRule.Contains("=")) { - string restricted = cleanRule.Substring(cleanRule.IndexOf('=') + 1).Split(new[] { '!', '&' })[0]; - if (!restricted.Split(',').Contains(actualPhoneme)) return false; - } - - return true; - } - - protected virtual List ApplyReplacements(List inputPhonemes, bool isBoundary) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return inputPhonemes; - - List finalPhonemes = new List(); - int idx = 0; - - var validRules = mergingReplacements.Concat(splittingReplacements) - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - var validSplits = splittingReplacements - .Where(r => r.where == "all" || (!isBoundary && r.where == "inside") || (isBoundary && r.where == "boundary")).ToList(); - - while (idx < inputPhonemes.Count) { - bool replaced = false; - - foreach (var rule in validRules) { - 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.Count; j++) { - string rulePh = fromArray[j]; - string actualPh = inputPhonemes[idx + j]; - - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh)) { - if (IsGroupMatch(rulePh, actualPh)) { - if (!captures.ContainsKey(baseRulePh)) captures[baseRulePh] = new List(); - captures[baseRulePh].Add(actualPh); - } else { - match = false; break; - } - } else if (rulePh != actualPh) { - match = false; break; - } - } - - if (match) { - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - 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++) { - // 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.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) { - List fromArray = rule.FromList; - if (fromArray == null || fromArray.Count != 1) continue; - - string rulePh = fromArray[0]; - string cleanRulePh = rulePh.Trim('(', ')'); - string baseRulePh = cleanRulePh.Split(new[] { '!', '=', '&' })[0]; - - if (IsGroupKeyword(baseRulePh) ? IsGroupMatch(rulePh, currentPhoneme) : rulePh == currentPhoneme) { - - List toArray = rule.ToList; - - if (toArray != null && toArray.Count > 0) { - foreach(string toPh in toArray) { - 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; - } - } - } - if (!singleReplaced) finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } else if (!replaced) { - finalPhonemes.Add(inputPhonemes[idx]); - idx++; - } - } - return finalPhonemes; - } - - private Syllable ApplyBoundaryReplacements(Syllable syllable) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return syllable; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(syllable.prevV); - bool hasV = !string.IsNullOrEmpty(syllable.v); - - 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) || !hasPrevV; - List finalPhonemes = ApplyReplacements(currentPhonemes, isBoundary); - - string newPrevV = ""; - string newV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - if (hasV && finalPhonemes.Count > 0) { - var vowelsList = GetVowels(); - int vIndex = finalPhonemes.Count - 1; - - for (int i = finalPhonemes.Count - 1; i >= 0; i--) { - if (vowelsList.Contains(finalPhonemes[i])) { - vIndex = i; - break; - } - } - newV = finalPhonemes[vIndex]; - for (int i = 0; i < vIndex; i++) { - newCc.Add(finalPhonemes[i]); - } - } else { - newCc.AddRange(finalPhonemes); - } - } - - syllable.prevV = newPrevV; - syllable.cc = newCc.ToArray(); - syllable.v = newV; - return syllable; - } - - private Ending ApplyBoundaryReplacements(Ending ending) { - if (!mergingReplacements.Any() && !splittingReplacements.Any()) return ending; - - List currentPhonemes = new List(); - bool hasPrevV = !string.IsNullOrEmpty(ending.prevV); - currentPhonemes.Add(hasPrevV ? ending.prevV : "null"); - if (ending.cc != null) currentPhonemes.AddRange(ending.cc); - - List finalPhonemes = ApplyReplacements(currentPhonemes, true); - - string newPrevV = ""; - List newCc = new List(); - - if (finalPhonemes.Count > 0) { - string firstPh = finalPhonemes[0]; - if (firstPh == "null") { - newPrevV = ""; - finalPhonemes.RemoveAt(0); - } else { - newPrevV = firstPh; - finalPhonemes.RemoveAt(0); - } - newCc.AddRange(finalPhonemes); - } - - ending.prevV = newPrevV; - ending.cc = newCc.ToArray(); - return ending; - } - - #endregion - - #region private - - private Result MakeForcedAliasResult(Note note) { - return MakeSimpleResult(note.lyric.Substring(1)); - } - - protected void ReadDictionaryAndInit() { - var dictionaryName = GetDictionaryName(); - if (dictionaryName == null) { - return; - } - dictionaries[GetType()] = null; - if (Testing) { - ReadDictionary(dictionaryName); - Init(); - return; - } - OnAsyncInitStarted(); - Task.Run(() => { - ReadDictionary(dictionaryName); - Init(); - OnAsyncInitFinished(); - }); - } - - private void ReadDictionary(string dictionaryName) { - try { - var phonemeSymbols = new Dictionary(); - - foreach (var vowel in GetVowels()) { - phonemeSymbols[vowel] = true; - } - foreach (var consonant in GetConsonants()) { - phonemeSymbols[consonant] = false; - } - - var childDict = GetDictionaryPhonemesReplacement() ?? new Dictionary(); - var safeDict = new Dictionary(); - - foreach (var kvp in childDict) { - safeDict[kvp.Key] = kvp.Value; - safeDict[kvp.Key.ToUpperInvariant()] = kvp.Value; // Safely catches 'AA' - safeDict[kvp.Key.ToLowerInvariant()] = kvp.Value; // Safely catches 'aa' - } - - dictionaries[GetType()] = new G2pRemapper( - LoadBaseDictionary(), - phonemeSymbols, - safeDict); - - } catch (Exception ex) { - Log.Error(ex, $"Failed to read dictionary {dictionaryName}"); - } - } - - private string[] ApplyExtensions(string[] symbols, Note[] notes) { - var newSymbols = new List(); - var vowelIds = ExtractVowels(symbols); - if (vowelIds.Count == 0) { - // no syllables or all consonants, the last phoneme will be interpreted as vowel - vowelIds.Add(symbols.Length - 1); - } - var lastVowelI = 0; - newSymbols.AddRange(symbols.Take(vowelIds[lastVowelI] + 1)); - for (var i = 1; i < notes.Length && lastVowelI + 1 < vowelIds.Count; i++) { - if (!IsSyllableVowelExtensionNote(notes[i])) { - var prevVowel = vowelIds[lastVowelI]; - lastVowelI++; - var vowel = vowelIds[lastVowelI]; - newSymbols.AddRange(symbols.Skip(prevVowel + 1).Take(vowel - prevVowel)); - } else { - newSymbols.Add(symbols[vowelIds[lastVowelI]]); - } - } - newSymbols.AddRange(symbols.Skip(vowelIds[lastVowelI] + 1)); - return newSymbols.ToArray(); - } - - private List ExtractVowels(string[] symbols) { - var vowelIds = new List(); - var vowels = GetVowels(); - for (var i = 0; i < symbols.Length; i++) { - if (vowels.Contains(symbols[i])) { - vowelIds.Add(i); - } - } - 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]; - - 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) { - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - stretch *= overrideRatio; - } else { - baseLengthMs = GetTransitionBasicLengthMs(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; - } - } - } - - 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] = new Phoneme { - phoneme = validatedAlias, - index = globalIndex - }; - - if (i == 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) { - // YAML Override active: Use the multiplier and bypass NoGap entirely - baseLengthMs = GetTransitionBasicLengthMsByConstant(); - phonemes[phonemeI].position = MsToTick(baseLengthMs * stretch * overrideRatio); - } else { - // Default behavior - baseLengthMs = GetTransitionBasicLengthMsByOto(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 - phonemes[phonemeI].position = MsToTick(baseLengthMs); - } - } - } else { - 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]; - } - } else { - // 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 - 1 : phonemeSymbols.Count - 1, containerLength); - } - - private string ValidateAliasIfNeeded(string alias, int tone) { - if (HasOto(alias, tone)) { - return alias; - } - return ValidateAlias(alias); - } - - private Phoneme[] ScalePhonemes(Phoneme[] phonemes, int startPosition, int phonemesCount, int containerLengthTick = -1) { - var offset = 0; - var lengthModifier = 1.0; - - if (containerLengthTick > 0) { - var allTransitionsLengthTick = phonemes.Sum(n => n.position); - - // 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--) { - if (phonemes[i].phoneme == null) continue; - var finalLengthTick = (int)(phonemes[i].position * lengthModifier); - phonemes[i].position = startPosition - finalLengthTick - offset; - offset += finalLengthTick; - } - - return phonemes.Where(n => n.phoneme != null).ToArray(); - } - - #endregion - } -} From 0f4bc456bae795be6f2a1e638e8efb245f381391 Mon Sep 17 00:00:00 2001 From: Kona <81715373+HeadHunter4621@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:54:50 -0700 Subject: [PATCH 13/13] Rename class (Copilot suggestion durring PR) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs index 3313752f3..f059ff9fb 100644 --- a/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/GenericSyllabicPhonemizer.cs @@ -11,11 +11,11 @@ namespace OpenUtau.Plugin.Builtin { [Phonemizer("Generic Syllabic Phonemizer", "GEN SYL", "HeadHunter4621")] // Contributed by HeadHunter4621, base code is the FIL VCV & CVVC phonemizer. // The default template contains ARPAbet and romaji phonemes but can be easily modified to fit each voicebank. - public class GenericSyllablePhonemizer : SyllableBasedPhonemizer { + public class GenericSyllabicPhonemizer : SyllableBasedPhonemizer { protected override string YamlFileName => "generic-phonemizer.yaml"; protected override byte[] YamlTemplate => Data.Resources.generic_syllabic_template; - public GenericSyllablePhonemizer() { + public GenericSyllabicPhonemizer() { this.vowels = Array.Empty(); this.consonants = Array.Empty();