Skip to content

Commit bee9b39

Browse files
IsE333stakira
authored andcommitted
Add TurkishCVVCPhonemizer.
1 parent 6401baf commit bee9b39

File tree

2 files changed

+371
-0
lines changed

2 files changed

+371
-0
lines changed
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using OpenUtau.Api;
5+
using OpenUtau.Core.Ustx;
6+
7+
namespace OpenUtau.Plugin.Builtin {
8+
[Phonemizer("Turkish CVVC Phonemizer", "TR CVVC", "IsE & comorybxto", language: "TR")]
9+
public class TurkishCVVCPhonemizer : Phonemizer {
10+
static readonly string[] glottalStops = new string[] { "?", "q" };
11+
static readonly string[] vowels = new string[] { "a", "e", "ae", "eu", "i", "o", "oe", "u", "ue" };
12+
static readonly string[] sustainedConsonants = new string[] { "Y", "L", "LY", "M", "N", "NG" };
13+
static readonly string[] consonants = "9,b,c,ch,d,f,g,h,j,k,l,m,n,ng,p,r,rr,r',s,sh,t,v,w,y,z,by,dy,gy,hy,ky,ly,my,ny,py,ry,ty,Y,L,LY,M,N,NG,-,?,q".Split(',');
14+
15+
static TurkishCVVCPhonemizer() {
16+
}
17+
18+
// Store singer in field, will try reading presamp.ini later
19+
private USinger singer;
20+
public override void SetSinger(USinger singer) => this.singer = singer;
21+
22+
// make it quicker to check multiple oto occurrences at once rather than spamming if else if
23+
private bool checkOtoUntilHit(string[] input, Note note, out UOto oto) {
24+
oto = default;
25+
var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default;
26+
27+
var otos = new List<UOto>();
28+
foreach (string test in input) {
29+
if (singer.TryGetMappedOto(test + attr.alternate, note.tone + attr.toneShift, attr.voiceColor, out var otoAlt)) {
30+
otos.Add(otoAlt);
31+
} else if (singer.TryGetMappedOto(test, note.tone + attr.toneShift, attr.voiceColor, out var otoCandidacy)) {
32+
otos.Add(otoCandidacy);
33+
}
34+
}
35+
36+
string color = attr.voiceColor ?? "";
37+
if (otos.Count > 0) {
38+
if (otos.Any(oto => (oto.Color ?? string.Empty) == color)) {
39+
oto = otos.Find(oto => (oto.Color ?? string.Empty) == color);
40+
return true;
41+
} else {
42+
oto = otos.First();
43+
return true;
44+
}
45+
}
46+
return false;
47+
}
48+
49+
private string[] getNoteStart(SegmentedLyric phonemesCurrent, SegmentedLyric phonemesPrev) {
50+
string noteStart = phonemesCurrent.StartC1 + phonemesCurrent.StartC2 + phonemesCurrent.Vow;
51+
bool hasNoPrevNeighbour = (phonemesPrev.StartC1 == "") && (phonemesPrev.Vow == "");
52+
string[] result = new string[] { "- " + noteStart, noteStart, phonemesCurrent.Lyric };
53+
54+
if (hasNoPrevNeighbour) {
55+
return result;
56+
}
57+
58+
if (phonemesCurrent.StartC1 == "") {
59+
if (phonemesPrev.hasConsonantAfterVowel()) { //vc + V
60+
if (sustainedConsonants.Contains(phonemesPrev.EndC1.ToUpper())) {
61+
result[0] = phonemesPrev.EndC1.ToUpper() + " " + phonemesCurrent.Vow;
62+
}
63+
} else if (phonemesPrev.Vow != "") { //v + V
64+
if (phonemesCurrent.Has9BeforeVow) {
65+
result[0] = phonemesPrev.Vow + " 9" + phonemesCurrent.Vow;
66+
} else {
67+
result[0] = phonemesPrev.Vow + " " + phonemesCurrent.Vow;
68+
}
69+
}
70+
return result;
71+
}
72+
73+
return new string[] { noteStart, phonemesCurrent.Lyric };
74+
}
75+
76+
private string getAlternativeConsonant(string consonant, string vow) {
77+
if (vow == "e" || vow == "i" || vow == "ue" || vow == "oe") {
78+
string y = "y";
79+
if (consonant.ToUpper() == consonant) {
80+
y = "Y";
81+
}
82+
if (consonants.Contains(consonant + y)) {
83+
return consonant + y;
84+
}
85+
}
86+
return consonant;
87+
}
88+
89+
private string[] getConsonantEnding(SegmentedLyric current, bool hasNext, SegmentedLyric next) {
90+
string v_ = current.Vow + " ";
91+
92+
if (glottalStops.Contains(current.EndC1))
93+
return new string[] { v_ + current.EndC1 };
94+
95+
if (hasNext) {
96+
//if (sustainedConsonants.Contains(current.EndC1.ToUpper()))
97+
// return new string[] { v_ + current.EndC1.ToUpper() };
98+
if (current.EndC1 != "" && current.EndC1 == next.StartC1)
99+
return new string[] { v_ + getAlternativeConsonant(current.EndC1, current.Vow) };
100+
101+
if (next.StartC1 == "g" || next.StartC1 == "k") {
102+
if (current.EndC1 == "n") {
103+
current.EndC1 = "ng";
104+
} else if (current.EndC1 == "N") {
105+
current.EndC1 = "NG";
106+
}
107+
}
108+
}
109+
110+
if (!current.hasConsonantAfterVowel()) {
111+
if (!hasNext) {
112+
return new string[] { v_ + "-" };
113+
} else if (next.hasConsonantBeforeVowel()) { // V + c
114+
return new string[] { v_ + getAlternativeConsonant(next.StartC1, next.Vow) };
115+
} else {//V + v
116+
return new string[] { "" };
117+
}
118+
}
119+
120+
return new string[] {
121+
v_ + current.EndC1 + "-",
122+
v_ + current.EndC1 };
123+
}
124+
125+
private string getIfPChTK(string c) {
126+
if (c == "p" || c == "ch" || c == "t" || c == "k") {
127+
return "";
128+
}
129+
return "-";
130+
}
131+
132+
private string convertToOtoStyledLyric(string lyric) {
133+
lyric = lyric.Replace("ç", "ch");
134+
lyric = lyric.Replace("ş", "sh");
135+
lyric = lyric.Replace("ğ", "9");
136+
lyric = lyric.Replace("æ", "ae");
137+
lyric = lyric.Replace("E", "ae");
138+
lyric = lyric.Replace("ı", "eu");
139+
lyric = lyric.Replace("ö", "oe");
140+
lyric = lyric.Replace("ü", "ue");
141+
return lyric;
142+
}
143+
144+
private struct SegmentedLyric {
145+
public string Lyric;
146+
public string StartC1;
147+
public string StartC2;
148+
public string Vow;
149+
public string EndC1;
150+
public string EndC2;
151+
public bool Has9BeforeVow;
152+
public SegmentedLyric(string originalLyric, string[] phonemes, bool has9BeforeVow) {
153+
Lyric = originalLyric;
154+
StartC1 = phonemes[0];
155+
StartC2 = phonemes[1];
156+
Vow = phonemes[2];
157+
EndC1 = phonemes[3];
158+
EndC2 = phonemes[4];
159+
Has9BeforeVow = has9BeforeVow;
160+
}
161+
public SegmentedLyric(bool has9BeforeVow) {
162+
Lyric = "";
163+
StartC1 = StartC2 = Vow = EndC1 = EndC2 = "";
164+
Has9BeforeVow = has9BeforeVow;
165+
}
166+
public bool hasConsonantBeforeVowel() {
167+
return StartC1 != "";
168+
}
169+
public bool hasConsonantAfterVowel() {
170+
return EndC1 != "";
171+
}
172+
}
173+
174+
private SegmentedLyric getSegmentedPhonemes(string lyric) { //CCVCC
175+
lyric = convertToOtoStyledLyric(lyric);
176+
string[] phonemes = new string[] { "", "", "", "", "" };
177+
bool has9BeforeVow = false;
178+
int charIndex = 0;
179+
180+
for (int i = 0; i < 5; i++) {
181+
string twoCharPhoneme = "";
182+
if (charIndex + 2 <= lyric.Length) {
183+
twoCharPhoneme = lyric.Substring(charIndex, 2);
184+
}
185+
string oneCharPhoneme = lyric.Substring(charIndex, 1);
186+
187+
if (i < 2 && oneCharPhoneme == "9") {
188+
has9BeforeVow |= true;
189+
charIndex += 1;
190+
} else if (vowels.Contains(twoCharPhoneme)) {
191+
i = 2;
192+
phonemes[i] = twoCharPhoneme;
193+
charIndex += 2;
194+
} else if (vowels.Contains(oneCharPhoneme)) {
195+
i = 2;
196+
phonemes[i] = oneCharPhoneme;
197+
charIndex += 1;
198+
} else if (consonants.Contains(twoCharPhoneme) && i != 2) {
199+
phonemes[i] = twoCharPhoneme;
200+
charIndex += 2;
201+
} else if (consonants.Contains(oneCharPhoneme) && i != 2) {
202+
phonemes[i] = oneCharPhoneme;
203+
charIndex += 1;
204+
} else { // not found
205+
i -= 1;
206+
charIndex += 1;
207+
}
208+
209+
if (charIndex == lyric.Length) {
210+
break;
211+
}
212+
}
213+
return new SegmentedLyric(lyric, phonemes, has9BeforeVow);
214+
}
215+
216+
public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) {
217+
var note = notes[0];
218+
var currentLyric = note.lyric.Normalize();
219+
220+
/*
221+
if (currentLyric[0] == ',')
222+
basicMode = !basicMode;
223+
if (basicMode) {
224+
return new Result {
225+
phonemes = new Phoneme[] {
226+
new Phoneme() {
227+
phoneme = currentLyric,
228+
}
229+
},
230+
};
231+
}*/
232+
if (currentLyric[0] == '.') {
233+
return new Result {
234+
phonemes = new Phoneme[] {
235+
new Phoneme() {
236+
phoneme = currentLyric.Substring(1),
237+
}
238+
},
239+
};
240+
}
241+
242+
SegmentedLyric phonemesCurrent = getSegmentedPhonemes(currentLyric);
243+
SegmentedLyric phonemesPrev = new SegmentedLyric(false);
244+
SegmentedLyric phonemesNext = new SegmentedLyric(false);
245+
246+
if (prevNeighbour != null) {
247+
phonemesPrev = getSegmentedPhonemes(prevNeighbour.Value.lyric.Normalize());
248+
}
249+
if (nextNeighbour != null) {
250+
phonemesNext = getSegmentedPhonemes(nextNeighbour.Value.lyric.Normalize());
251+
}
252+
253+
string[] noteStartInput = getNoteStart(phonemesCurrent, phonemesPrev);
254+
string[] noteEndInput = getConsonantEnding(phonemesCurrent, nextNeighbour.HasValue, phonemesNext);//phonemesCurrent.EndC1 + "-";
255+
string noteStart = "", noteEnd = "", noteEndCC = "";
256+
257+
if (phonemesCurrent.EndC2 != "") { // + VCC
258+
noteEndInput = new string[] { phonemesCurrent.Vow + " " + phonemesCurrent.EndC1 + getIfPChTK(phonemesCurrent.EndC1) };
259+
noteEndCC = phonemesCurrent.EndC1 + phonemesCurrent.EndC2 + " -";
260+
}
261+
262+
263+
if (checkOtoUntilHit(noteStartInput, note, out var o1)) {
264+
noteStart = o1.Alias;
265+
}
266+
267+
if (checkOtoUntilHit(noteEndInput, note, out var o2)) {
268+
noteEnd = o2.Alias;
269+
} else {
270+
noteEnd = "";
271+
}
272+
273+
var input = new string[] { noteEndCC };
274+
if (checkOtoUntilHit(input, note, out var o3)) {
275+
noteEndCC = o3.Alias;
276+
} else {
277+
noteEndCC = "";
278+
}
279+
280+
if (noteStart != "" && noteEnd == "" && noteEndCC == "") {
281+
return new Result {
282+
phonemes = new Phoneme[] {
283+
new Phoneme() {
284+
phoneme = noteStart,
285+
}
286+
},
287+
};
288+
} else if (noteStart != "" && noteEnd != "") {
289+
int totalDuration = notes.Sum(n => n.duration);
290+
int lastLengthFromOto = 120;
291+
double isCVCoeff = 1;
292+
if (phonemesCurrent.hasConsonantAfterVowel())
293+
isCVCoeff = 1.8;
294+
int isEndCoeff = 2;
295+
if (nextNeighbour != null) {
296+
isEndCoeff = 1;
297+
var attr0 = nextNeighbour.Value.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; //next first
298+
if (singer.TryGetMappedOto(getNoteStart(phonemesNext, phonemesCurrent)[0], note.tone + attr0.toneShift, attr0.voiceColor, out var oto0)) {
299+
// If overlap is a negative value, vcLength is longer than Preutter
300+
if (oto0.Overlap < 0)
301+
lastLengthFromOto = timeAxis.MsPosToTickPos(oto0.Preutter - oto0.Overlap);
302+
else
303+
lastLengthFromOto = timeAxis.MsPosToTickPos(oto0.Preutter);
304+
}
305+
}
306+
// vcLength depends on the Vel of the note
307+
var attr1 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 1) ?? default; // current last (noteEnd)
308+
var vcLength = Convert.ToInt32(Math.Min(totalDuration / (2 * isEndCoeff), lastLengthFromOto * isCVCoeff * (attr1.consonantStretchRatio ?? 1)));
309+
310+
if (noteEndCC == "") {
311+
return new Result {
312+
phonemes = new Phoneme[] {
313+
new Phoneme() {
314+
phoneme = noteStart,
315+
},
316+
new Phoneme() {
317+
phoneme = noteEnd,
318+
position = totalDuration - vcLength,
319+
},
320+
},
321+
};
322+
} else {
323+
int ccLengthFromOto = 60;
324+
var attr2 = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 2) ?? default;
325+
if (nextNeighbour != null) {
326+
if (singer.TryGetMappedOto(noteEndCC, note.tone + attr2.toneShift, attr2.voiceColor, out var oto1)) {
327+
// If overlap is a negative value, vcLength is longer than Preutter
328+
if (oto1.Overlap < 0) {
329+
ccLengthFromOto = timeAxis.MsPosToTickPos(oto1.Preutter - oto1.Overlap);
330+
} else {
331+
ccLengthFromOto = timeAxis.MsPosToTickPos(oto1.Preutter);
332+
}
333+
}
334+
}
335+
vcLength = Convert.ToInt32(Math.Min(totalDuration / 3, ccLengthFromOto * (attr1.consonantStretchRatio ?? 1)));
336+
var ccLength = Convert.ToInt32(Math.Min(totalDuration / 3, lastLengthFromOto * (attr2.consonantStretchRatio ?? 1)));
337+
338+
List<PhonemeExpression> exp = new List<PhonemeExpression>();
339+
PhonemeExpression e = new PhonemeExpression() { abbr = Core.Format.Ustx.VOL, value = 70 };
340+
exp.Add(e);
341+
342+
return new Result {
343+
phonemes = new Phoneme[] {
344+
new Phoneme() {
345+
phoneme = noteStart,
346+
},
347+
new Phoneme() {
348+
phoneme = noteEnd,
349+
position = totalDuration - vcLength - ccLength,
350+
},
351+
new Phoneme() {
352+
phoneme = noteEndCC,
353+
position = totalDuration - ccLength,
354+
expressions = exp,
355+
},
356+
},
357+
};
358+
}
359+
}
360+
361+
return new Result {
362+
phonemes = new Phoneme[] {
363+
new Phoneme() {
364+
phoneme = currentLyric,
365+
}
366+
},
367+
};
368+
}
369+
}
370+
}

OpenUtau/Strings/Strings.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
<system:String x:Key="languages.pt">Portuguese</system:String>
125125
<system:String x:Key="languages.ru">Russian</system:String>
126126
<system:String x:Key="languages.th">Thai</system:String>
127+
<system:String x:Key="languages.tr">Turkish</system:String>
127128
<system:String x:Key="languages.vi">Vietnamese</system:String>
128129
<system:String x:Key="languages.zh">Chinese</system:String>
129130
<system:String x:Key="languages.zh-yue">Cantonese</system:String>

0 commit comments

Comments
 (0)