diff --git a/OpenUtau.Core/Classic/ClassicRenderer.cs b/OpenUtau.Core/Classic/ClassicRenderer.cs index aba324b4a..a4ebe403b 100644 --- a/OpenUtau.Core/Classic/ClassicRenderer.cs +++ b/OpenUtau.Core/Classic/ClassicRenderer.cs @@ -47,15 +47,15 @@ public RenderResult Layout(RenderPhrase phrase) { }; } - public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { if (phrase.wavtool == SharpWavtool.nameConvergence || phrase.wavtool == SharpWavtool.nameSimple) { - return RenderInternal(phrase, progress, cancellation, isPreRender); + return RenderInternal(phrase, progress, trackNo, cancellation, isPreRender); } else { - return RenderExternal(phrase, progress, cancellation, isPreRender); + return RenderExternal(phrase, progress, trackNo, cancellation, isPreRender); } } - public Task RenderInternal(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task RenderInternal(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { var resamplerItems = new List(); foreach (var phone in phrase.phones) { resamplerItems.Add(new ResamplerItem(phrase, phone)); @@ -78,7 +78,7 @@ public Task RenderInternal(RenderPhrase phrase, Progress progress, VoicebankFiles.Inst.CopyBackMetaFiles(item.inputFile, item.inputTemp); } } - progress.Complete(1, $"{item.resampler} \"{item.phone.phoneme}\""); + progress.Complete(1, $"Track {trackNo}: {item.resampler} \"{item.phone.phoneme}\""); }); var result = Layout(phrase); var wavtool = new SharpWavtool(true); @@ -91,13 +91,13 @@ public Task RenderInternal(RenderPhrase phrase, Progress progress, return task; } - public Task RenderExternal(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task RenderExternal(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { var resamplerItems = new List(); foreach (var phone in phrase.phones) { resamplerItems.Add(new ResamplerItem(phrase, phone)); } var task = Task.Run(() => { - string progressInfo = $"{phrase.wavtool} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; + string progressInfo = $"Track {trackNo} : {phrase.wavtool} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; progress.Complete(0, progressInfo); var wavPath = Path.Join(PathManager.Inst.CachePath, $"cat-{phrase.hash:x16}.wav"); var result = Layout(phrase); diff --git a/OpenUtau.Core/Classic/ResamplerItem.cs b/OpenUtau.Core/Classic/ResamplerItem.cs index b234d68dd..e6c597221 100644 --- a/OpenUtau.Core/Classic/ResamplerItem.cs +++ b/OpenUtau.Core/Classic/ResamplerItem.cs @@ -74,27 +74,29 @@ public ResamplerItem(RenderPhrase phrase, RenderPhone phone) { pitchCount = Math.Max(pitchCount, 0); pitches = new int[pitchCount]; - double phoneStartMs = phone.positionMs - pitchLeadingMs; - double phraseStartMs = phrase.positionMs - phrase.leadingMs; - for (int i = 0; i < phone.tempos.Length; i++) { - double startMs = Math.Max(phrase.timeAxis.TickPosToMsPos(phone.tempos[i].position), phoneStartMs); - double endMs = i + 1 < phone.tempos.Length ? phrase.timeAxis.TickPosToMsPos(phone.tempos[i + 1].position) : phone.positionMs + phone.envelope[4].X; - double durationMs = endMs - startMs; - int tempoPitchCount = (int)Math.Floor(MusicMath.TempoMsToTick(tempo, durationMs) / 5.0); - int tempoPitchSkip = (int)Math.Floor(MusicMath.TempoMsToTick(tempo, startMs - phoneStartMs) / 5.0); - tempoPitchCount = Math.Min(tempoPitchCount, pitches.Length - tempoPitchSkip); - int phrasePitchSkip = (int)Math.Floor(phrase.timeAxis.TicksBetweenMsPos(phraseStartMs, startMs) / 5.0); - double intervalPitchMs = 120 / tempo * 500 / 480 * 5; - double diffPitchMs = startMs - phraseStartMs - phrase.timeAxis.TickPosToMsPos(phrasePitchSkip * 5); - double tempoRatio = phone.tempos[i].bpm / tempo; - for (int j = 0; j < tempoPitchCount; j++) { - int index = tempoPitchSkip + j; - int scaled = phrasePitchSkip + (int)Math.Ceiling(j * tempoRatio); - scaled = Math.Clamp(scaled, 0, phrase.pitches.Length - 1); - int nextScaled = Math.Clamp(scaled + 1, 0, phrase.pitches.Length - 1); - index = Math.Clamp(index, 0, pitchCount - 1); - pitches[index] = (int)Math.Round((phrase.pitches[nextScaled]- phrase.pitches[scaled]) /intervalPitchMs * diffPitchMs + phrase.pitches[scaled] - phone.tone * 100); - } + var phrasePitchStartMs = phrase.positionMs - phrase.leadingMs; + var phrasePitchStartTick = (int)Math.Floor(phrase.timeAxis.MsPosToNonExactTickPos(phrasePitchStartMs)); + + var pitchIntervalMs = MusicMath.TempoTickToMs(tempo, 5); + var pitchSampleStartMs = phone.positionMs - pitchLeadingMs; + + for (int i=0; i= 1 && ParseFloat(parts[0], out pbsX) ? pbsX : 0; float pbsY = parts.Length >= 2 && ParseFloat(parts[1], out pbsY) ? pbsY : 0; - points.Add(new PitchPoint(pbsX, pbsY)); - // PBW, PBY - var x = points.First().X; - if (!string.IsNullOrWhiteSpace(pbw)) { - var w = pbw.Split(',').Select(s => ParseFloat(s, out var v) ? v : 0).ToList(); - var y = (pby ?? "").Split(',').Select(s => ParseFloat(s, out var v) ? v : 0).ToList(); + if(points.Count > 0) { + points[0] = new PitchPoint(pbsX, pbsY); + } else { + points.Add(new PitchPoint(pbsX, pbsY)); + } + } + if (points.Count == 0) { + return; + } + // PBW, PBY + var x = points.First().X; + var w = new List(); + var y = new List(); + if (!string.IsNullOrWhiteSpace(pbw)) { + w = pbw.Split(',').Select(s => ParseFloat(s, out var v) ? v : 0).ToList(); + } + if (!string.IsNullOrWhiteSpace(pby)) { + y = pby.Split(',').Select(s => ParseFloat(s, out var v) ? v : 0).ToList(); + } + if (w.Count != 0 || y.Count != 0) { + if (points.Count > 1 && points.Count - 1 == w.Count && y.Count == 0) { // replace w only + for (var i = 0; i < w.Count(); i++) { + x += w[i]; + points[i + 1].X = x; + } + } else if (points.Count > 1 && w.Count == 0 && points.Count - 1 == y.Count) { // replace y only + for (var i = 0; i < y.Count(); i++) { + points[i + 1].Y = y[i]; + } + } else { while (w.Count > y.Count) { y.Add(0); } + for (var i = points.Count - 1; i > 0; i--) { + points.Remove(points[i]); + } for (var i = 0; i < w.Count(); i++) { x += w[i]; points.Add(new PitchPoint(x, y[i])); } } - // PBM - if (!string.IsNullOrWhiteSpace(pbm)) { - var m = pbm.Split(new[] { ',' }); - for (var i = 0; i < m.Count() && i < points.Count; i++) { - switch (m[i]) { - case "r": - points[i].shape = PitchPointShape.o; - break; - case "s": - points[i].shape = PitchPointShape.l; - break; - case "j": - points[i].shape = PitchPointShape.i; - break; - default: - points[i].shape = PitchPointShape.io; - break; - } + } + // PBM + if (!string.IsNullOrWhiteSpace(pbm)) { + var m = pbm.Split(new[] { ',' }); + for (var i = 0; i < m.Count() && i < points.Count; i++) { + switch (m[i]) { + case "r": + points[i].shape = PitchPointShape.o; + break; + case "s": + points[i].shape = PitchPointShape.l; + break; + case "j": + points[i].shape = PitchPointShape.i; + break; + default: + points[i].shape = PitchPointShape.io; + break; } } + } + if (points.Count > 1) { this.pitch = pitch; } } diff --git a/OpenUtau.Core/Classic/WorldlineRenderer.cs b/OpenUtau.Core/Classic/WorldlineRenderer.cs index 275ca926c..b3bc19510 100644 --- a/OpenUtau.Core/Classic/WorldlineRenderer.cs +++ b/OpenUtau.Core/Classic/WorldlineRenderer.cs @@ -47,7 +47,7 @@ public RenderResult Layout(RenderPhrase phrase) { }; } - public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { var resamplerItems = new List(); foreach (var phone in phrase.phones) { resamplerItems.Add(new ResamplerItem(phrase, phone)); @@ -55,7 +55,7 @@ public Task Render(RenderPhrase phrase, Progress progress, Cancell var task = Task.Run(() => { var result = Layout(phrase); var wavPath = Path.Join(PathManager.Inst.CachePath, $"wdl-{phrase.hash:x16}.wav"); - string progressInfo = $"{this} {string.Join(" ", phrase.phones.Select(p => p.phoneme))}"; + string progressInfo = $"Track {trackNo}: {this} {string.Join(" ", phrase.phones.Select(p => p.phoneme))}"; progress.Complete(0, progressInfo); if (File.Exists(wavPath)) { try { diff --git a/OpenUtau.Core/DependencyInstaller.cs b/OpenUtau.Core/DependencyInstaller.cs index b63536dcb..87d7073d7 100644 --- a/OpenUtau.Core/DependencyInstaller.cs +++ b/OpenUtau.Core/DependencyInstaller.cs @@ -41,7 +41,7 @@ public static void Install(string archivePath) { entry.WriteToFile(Path.Combine(basePath, entry.Key)); } } - DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"dependency \"{name}\" installaion finished")); + DocManager.Inst.ExecuteCmd(new ProgressBarNotification(0, $"Installed dependency \"{name}\"")); } } } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerConfig.cs b/OpenUtau.Core/DiffSinger/DiffSingerConfig.cs index a8b5a7f29..f75ce88dd 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerConfig.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerConfig.cs @@ -35,6 +35,7 @@ public class DsConfig { public int sample_rate = 44100; public bool predict_dur = true; public bool use_expr = false; + public bool use_note_rest = false; public float frameMs(){ return 1000f * hop_size / sample_rate; } diff --git a/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs b/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs index 9de9066a5..7cbec606d 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerPitch.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; @@ -67,6 +68,11 @@ public DiffSingerSpeakerEmbedManager getSpeakerEmbedManager(){ return speakerEmbedManager; } + void SetRange(T[] list, T value, int startIndex, int endIndex){ + for(int i=startIndex;i(); - //Pitch Predictor + //Pitch Predictor + var note_rest = new List{true}; + bool prevNoteRest = true; + int phIndex = 0; + foreach(var note in phrase.notes) { + //Slur notes follow the previous note's rest status + if(note.lyric.StartsWith("+")) { + note_rest.Add(prevNoteRest); + continue; + } + //find all the phonemes in the note's time range + while(phIndex ph.end <= note.position + 1) + .TakeWhile(ph => ph.position < note.end - 1) + .ToArray(); + //If all the phonemes in a note's time range are AP, SP or consonant, + //it is a rest note + bool isRest = phs.Length == 0 + || phs.All(ph => ph.phoneme == "AP" || ph.phoneme == "SP" || !g2p.IsVowel(ph.phoneme)); + note_rest.Add(isRest); + prevNoteRest = isRest; + } + var note_midi = phrase.notes .Select(n=>(float)n.tone) .Prepend((float)phrase.notes[0].tone) .ToArray(); + //get the index of groups of consecutive rest notes + int restGroupStart = 0; + var restGroups = new List>{}; + foreach(int noteIndex in Enumerable.Range(1,note_rest.Count - 1)) { + if(!note_rest[noteIndex-1] && note_rest[noteIndex]) { + //start a new rest group + restGroupStart = noteIndex; + } + if(note_rest[noteIndex-1] && !note_rest[noteIndex]) { + //end the current rest group + restGroups.Add(new Tuple(restGroupStart,noteIndex)); + } + } + if(!note_rest[^1]) { + //end the last rest group + restGroups.Add(new Tuple(restGroupStart,note_rest.Count)); + } + //Set tone for each rest group + foreach(var restGroup in restGroups){ + if(restGroup.Item1 == 0 && restGroup.Item2 == note_rest.Count){ + //If All the notes are rest notes, don't set tone + break; + } + if(restGroup.Item1 == 0){ + //If the first note is a rest note, set the tone to the tone of the first non-rest note + SetRange(note_midi, note_midi[restGroup.Item2], 0, restGroup.Item2); + } else if(restGroup.Item2 == note_rest.Count){ + //If the last note is a rest note, set the tone to the tone of the last non-rest note + SetRange(note_midi, note_midi[restGroup.Item1-1], restGroup.Item1, note_rest.Count); + } else { + //If the first and last notes are non-rest notes, set the tone to the nearest non-rest note + SetRange(note_midi, + note_midi[restGroup.Item1-1], + restGroup.Item1, + (restGroup.Item1 + restGroup.Item2 + 1)/2 + ); + SetRange(note_midi, + note_midi[restGroup.Item2], + (restGroup.Item1 + restGroup.Item2 + 1)/2, + restGroup.Item2 + ); + } + } + //use the delta of the positions of the next note and the current note //to prevent incorrect timing when there is a small space between two notes var note_dur = phrase.notes.Zip(phrase.notes.Skip(1), @@ -138,7 +213,6 @@ public RenderPitchResult Process(RenderPhrase phrase){ .Append(0) .ToList(); note_dur[^1]=totalFrames-note_dur.Sum(); - var pitch = Enumerable.Repeat(60f, totalFrames).ToArray(); var retake = Enumerable.Repeat(true, totalFrames).ToArray(); var speedup = Preferences.Default.DiffsingerSpeedup; @@ -185,6 +259,13 @@ public RenderPitchResult Process(RenderPhrase phrase){ pitchInputs.Add(NamedOnnxValue.CreateFromTensor("spk_embed", spkEmbedTensor)); } + //Melody encoder + if(dsConfig.use_note_rest) { + pitchInputs.Add(NamedOnnxValue.CreateFromTensor("note_rest", + new DenseTensor(note_rest.ToArray(), new int[] { note_rest.Count }, false) + .Reshape(new int[] { 1, note_rest.Count }))); + } + var pitchOutputs = pitchModel.Run(pitchInputs); var pitch_out = pitchOutputs.First().AsTensor().ToArray(); var pitchEnd = phrase.timeAxis.MsPosToTickPos(startMs + (totalFrames - 1) * frameMs) - phrase.position; diff --git a/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs b/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs index cdfb4420b..792c2aafb 100644 --- a/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs +++ b/OpenUtau.Core/DiffSinger/DiffSingerRenderer.cs @@ -66,7 +66,7 @@ public RenderResult Layout(RenderPhrase phrase) { }; } - public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { var task = Task.Run(() => { lock (lockObj) { if (cancellation.IsCancellationRequested) { diff --git a/OpenUtau.Core/Enunu/EnunuRenderer.cs b/OpenUtau.Core/Enunu/EnunuRenderer.cs index ab906601f..d5545f86a 100644 --- a/OpenUtau.Core/Enunu/EnunuRenderer.cs +++ b/OpenUtau.Core/Enunu/EnunuRenderer.cs @@ -68,13 +68,13 @@ public RenderResult Layout(RenderPhrase phrase) { }; } - public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) { + public Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) { var task = Task.Run(() => { lock (lockObj) { if (cancellation.IsCancellationRequested) { return new RenderResult(); } - string progressInfo = $"{this} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; + string progressInfo = $"Track {trackNo}: {this} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; progress.Complete(0, progressInfo); var tmpPath = Path.Join(PathManager.Inst.CachePath, $"enu-{phrase.preEffectHash:x16}"); var ustPath = tmpPath + ".tmp"; diff --git a/OpenUtau.Core/Render/IRenderer.cs b/OpenUtau.Core/Render/IRenderer.cs index 1b38e4abc..fdc5f6ef3 100644 --- a/OpenUtau.Core/Render/IRenderer.cs +++ b/OpenUtau.Core/Render/IRenderer.cs @@ -42,7 +42,7 @@ public interface IRenderer { bool SupportsRenderPitch { get; } bool SupportsExpression(UExpressionDescriptor descriptor); RenderResult Layout(RenderPhrase phrase); - Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender = false); + Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender = false); RenderPitchResult LoadRenderedPitch(RenderPhrase phrase); UExpressionDescriptor[] GetSuggestedExpressions(USinger singer, URenderSettings renderSettings); } diff --git a/OpenUtau.Core/Render/RenderEngine.cs b/OpenUtau.Core/Render/RenderEngine.cs index 3f3ba17fb..13ee40419 100644 --- a/OpenUtau.Core/Render/RenderEngine.cs +++ b/OpenUtau.Core/Render/RenderEngine.cs @@ -213,7 +213,7 @@ private void RenderRequests( var phrase = tuple.Item1; var source = tuple.Item2; var request = tuple.Item3; - var task = phrase.renderer.Render(phrase, progress, cancellation, true); + var task = phrase.renderer.Render(phrase, progress, request.trackNo, cancellation, true); task.Wait(); if (cancellation.IsCancellationRequested) { break; diff --git a/OpenUtau.Core/Util/TimeAxis.cs b/OpenUtau.Core/Util/TimeAxis.cs index 3296d9604..66d53e91c 100644 --- a/OpenUtau.Core/Util/TimeAxis.cs +++ b/OpenUtau.Core/Util/TimeAxis.cs @@ -123,6 +123,12 @@ public double TickPosToMsPos(double tick) { return segment.msPos + segment.msPerTick * (tick - segment.tickPos); } + public double MsPosToNonExactTickPos(double ms) { + var segment = tempoSegments.First(seg => seg.msPos == ms || seg.msEnd > ms); // TODO: optimize + double tickPos = segment.tickPos + (ms - segment.msPos) * segment.ticksPerMs; + return tickPos; + } + public int MsPosToTickPos(double ms) { var segment = tempoSegments.First(seg => seg.msPos == ms || seg.msEnd > ms); // TODO: optimize double tickPos = segment.tickPos + (ms - segment.msPos) * segment.ticksPerMs; diff --git a/OpenUtau.Core/Vogen/VogenRenderer.cs b/OpenUtau.Core/Vogen/VogenRenderer.cs index 6b9d8a938..5b3599cec 100644 --- a/OpenUtau.Core/Vogen/VogenRenderer.cs +++ b/OpenUtau.Core/Vogen/VogenRenderer.cs @@ -54,7 +54,7 @@ public RenderResult Layout(RenderPhrase phrase) { }; } - public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender = false) { + public Task Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender = false) { var task = Task.Run(() => { lock (lockObj) { if (cancellation.IsCancellationRequested) { @@ -62,7 +62,7 @@ public Task Render(RenderPhrase phrase, Progress progress, Cancell } var result = Layout(phrase); var wavPath = Path.Join(PathManager.Inst.CachePath, $"vog-{phrase.hash:x16}.wav"); - string progressInfo = $"{this} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; + string progressInfo = $"Track {trackNo}: {this} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\""; progress.Complete(0, progressInfo); if (File.Exists(wavPath)) { try { diff --git a/OpenUtau.Plugin.Builtin/Data/en-xsampa.template.yaml b/OpenUtau.Plugin.Builtin/Data/en-xsampa.template.yaml index e229a5ceb..96df29ffd 100644 --- a/OpenUtau.Plugin.Builtin/Data/en-xsampa.template.yaml +++ b/OpenUtau.Plugin.Builtin/Data/en-xsampa.template.yaml @@ -141,6 +141,13 @@ symbols: - {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} diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index ffdc622b5..8c53a7416 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -20,7 +20,7 @@ public class EnXSampaPhonemizer : SyllableBasedPhonemizer { /// Due to the flexibility of X-SAMPA, it was easy to add the custom sounds. More suggestions for this are always welcome. /// - private readonly string[] vowels = "a,A,@,{,V,O,aU,aI,E,3,eI,I,i,oU,OI,U,u,Q,Ol,Ql,aUn,e@,eN,IN,e,o,Ar,Qr,Er,Ir,Or,Ur,ir,ur,aIr,aUr,A@,Q@,E@,I@,O@,U@,i@,u@,aI@,aU@,@r,@l,@m,@n,@N,1,e@m,e@n,y,I\\,M,U\\,Y,@\\,@`,3`,A`,Q`,E`,I`,O`,U`,i`,u`,aI`,aU`,},2,3\\,6,7,8,9,&,{~,I~,aU~,VI,VU,@U,i:,u:,O:,e@0,E~,e~,3r,ar,or,{l,Al,al,El,Il,il,ul,Ul,mm,nn,ll,NN".Split(','); + private readonly string[] vowels = "a,A,@,{,V,O,aU,aI,E,3,eI,I,i,oU,OI,U,u,Q,Ol,Ql,aUn,e@,eN,IN,e,o,Ar,Qr,Er,Ir,Or,Ur,ir,ur,aIr,aUr,A@,Q@,E@,I@,O@,U@,i@,u@,aI@,aU@,@r,@l,@m,@n,@N,1,e@m,e@n,y,I\\,M,U\\,Y,@\\,@`,3`,A`,Q`,E`,I`,O`,U`,i`,u`,aI`,aU`,},2,3\\,6,7,8,9,&,{~,I~,aU~,VI,VU,@U,ai,ei,Oi,au,ou,Ou,@u,i:,u:,O:,e@0,E~,e~,3r,ar,or,{l,Al,al,El,Il,il,ul,Ul,mm,nn,ll,NN".Split(','); private readonly string[] consonants = "b,tS,d,D,4,f,g,h,dZ,k,l,m,n,N,p,r,s,S,t,T,v,w,W,j,z,Z,t_},・,_".Split(','); private readonly string[] affricates = "tS,dZ".Split(','); private readonly string[] shortConsonants = "4".Split(","); @@ -35,7 +35,7 @@ public class EnXSampaPhonemizer : SyllableBasedPhonemizer { .ToDictionary(parts => parts[0], parts => parts[1]); // 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".Split(';') + 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]) @@ -143,12 +143,12 @@ protected override string[] GetSymbols(Note note) { } List modified = new List(); // Splits diphthongs and affricates if not present in the bank - string[] diphthongs = new[] { "aI", "eI", "OI", "aU", "oU", "VI", "VU", "@U" }; + 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} b", note.tone)) { + if (diphthongs.Contains(s) && !HasOto($"- {s}", note.tone) && !HasOto(s, note.tone) && !HasOto(ValidateAlias($"- {s}"), note.tone) && !HasOto(ValidateAlias(s), note.tone)) { modified.AddRange(new string[] { s[0].ToString(), s[1] + '^'.ToString() }); - } else if (affricates.Contains(s) && !HasOto($"i {s}", note.tone) && !HasOto($"i: {s}", note.tone)) { + } 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)) { modified.AddRange(new string[] { s[0].ToString(), s[1].ToString() }); } else { modified.Add(s); @@ -169,19 +169,19 @@ protected override List ProcessSyllable(Syllable syllable) { var rv = $"- {v}"; // Switch between phonetic systems, depending on certain aliases in the bank - if (HasOto($"i: b", syllable.tone) || !HasOto($"3 b", syllable.tone)) { + if (HasOto($"- i:", syllable.tone) || HasOto($"i:", syllable.tone) || (!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone))) { isVocaSampa = true; } - if (!HasOto($"V b", syllable.vowelTone)) { + if (!HasOto($"- V", syllable.vowelTone) && !HasOto($"V", syllable.vowelTone)) { isSimpleDelta = true; } - if (!HasOto($"I b", syllable.vowelTone)) { + if ((!HasOto($"- I", syllable.vowelTone) && !HasOto($"I", syllable.vowelTone)) || (!HasOto($"- U", syllable.vowelTone) && !HasOto($"U", syllable.vowelTone))) { isMiniDelta = true; } - if (HasOto("か", syllable.vowelTone)) { + if (HasOto("あ", syllable.vowelTone) || HasOto("- あ", syllable.vowelTone)) { isEnPlusJa = true; } @@ -189,7 +189,7 @@ protected override List ProcessSyllable(Syllable syllable) { isTrueXSampa = true; } - if (!HasOto($"3 b", syllable.tone) && !HasOto($"@` b", syllable.tone)) { + if ((!HasOto($"- 3", syllable.tone) && !HasOto($"3", syllable.tone)) || (!HasOto($"- @`", syllable.tone) && !HasOto($"@`", syllable.tone))) { isSalemList = true; } @@ -222,7 +222,7 @@ protected override List ProcessSyllable(Syllable syllable) { 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))) { + } 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 { @@ -299,7 +299,7 @@ protected override List ProcessSyllable(Syllable syllable) { lastC = i; basePhoneme = ccv; break; - } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) || !HasOto(ValidateAlias(ccv), syllable.vowelTone))) { + } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone))) { lastC = i; basePhoneme = rccv; break; @@ -432,7 +432,7 @@ protected override List ProcessEnding(Ending ending) { var vcr2 = $"{v}{cc[0]} -"; 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))) { + } 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); diff --git a/OpenUtau.Test/Classic/UstTest.cs b/OpenUtau.Test/Classic/UstTest.cs index 43342f77e..1863f3916 100644 --- a/OpenUtau.Test/Classic/UstTest.cs +++ b/OpenUtau.Test/Classic/UstTest.cs @@ -58,7 +58,12 @@ public void EqualInLyric() { Length=15 Lyric=A==B[C=D],EFG NoteNum=60 -PreUtterance="; +PreUtterance= +VBR=80,200,20,20,20,0,-50,0 +PBW=292,183 +PBS=-222;-19 +PBY=-20.7, +"; using (var stream = new MemoryStream()) { using (var writer = new StreamWriter(stream, leaveOpen: true)) { writer.Write(ust); @@ -73,6 +78,12 @@ public void EqualInLyric() { Assert.Single(part.notes); Assert.Equal("A==B[C=D],EFG", part.notes.First().lyric); Assert.Equal(60, part.notes.First().tone); + Assert.Equivalent(new UPitch {data = new List { + new PitchPoint { X = -222, Y = -19, shape = PitchPointShape.io}, + new PitchPoint { X = 70, Y = -20.7f, shape = PitchPointShape.io}, // X = -222 + 292 = 70 + new PitchPoint { X = 253, Y = 0, shape = PitchPointShape.io} // X = 70 + 183 = 253 + }, snapFirst = false } , part.notes.First().pitch); + Assert.Equivalent(new UVibrato { length = 80, period = 200, depth = 20, @in = 20, @out = 20, shift = 0, drift = -50, volLink = 0 }, part.notes.First().vibrato); } } } @@ -94,27 +105,37 @@ public void ParsePluginParseNoteTest() { var before = UNote.Create(); before.lyric = "a"; - before.duration = 10; + before.duration = 100; var first = UNote.Create(); first.lyric = "ka"; - first.duration = 20; + first.duration = 200; + first.pitch.data = new List { + new PitchPoint { X = -50, Y = 0, shape = PitchPointShape.io}, + new PitchPoint { X = 0, Y = 10, shape = PitchPointShape.io}, + new PitchPoint { X = 93.75f, Y = -12.2f, shape = PitchPointShape.io}, + new PitchPoint { X = 194.7f, Y = 0, shape = PitchPointShape.io} + }; var second = UNote.Create(); second.lyric = "r"; - second.duration = 30; + second.duration = 300; var third = UNote.Create(); third.lyric = "ta"; - third.duration = 40; + third.duration = 400; + third.pitch.data = new List { + new PitchPoint { X = -100, Y = 0, shape = PitchPointShape.io}, + new PitchPoint { X = 130, Y = 0, shape = PitchPointShape.io} + }; var last = UNote.Create(); last.lyric = "na"; - last.duration = 50; + last.duration = 500; var after = UNote.Create(); after.lyric = "ha"; - after.duration = 60; + after.duration = 600; part.notes.Add(before); part.notes.Add(first); @@ -141,12 +162,16 @@ public void ParsePluginParseNoteTest() { writer.WriteLine("[#0000]"); writer.WriteLine("Length=480"); writer.WriteLine("Lyric=A"); + writer.WriteLine("PBY=10,-10,0"); // Change only height of the third point writer.WriteLine("[#0001]"); writer.WriteLine("Length=480"); writer.WriteLine("Lyric=R"); // duration is null (change) writer.WriteLine("[#0002]"); writer.WriteLine("Lyric=zo"); + writer.WriteLine("PBS=-50"); // Reset points + writer.WriteLine("PBW=100"); + writer.WriteLine("PBY=0"); // duration is zero (delete) writer.WriteLine("[#0003]"); writer.WriteLine("Length="); @@ -164,8 +189,22 @@ public void ParsePluginParseNoteTest() { Assert.Equal(3, toAdd.Count); Assert.Equal(480, toAdd[0].duration); Assert.Equal("A", toAdd[0].lyric); - Assert.Equal(40, toAdd[1].duration); + Assert.Equivalent(new UPitch { + data = new List { + new PitchPoint { X = -50, Y = 0, shape = PitchPointShape.io}, + new PitchPoint { X = 0, Y = 10, shape = PitchPointShape.io}, + new PitchPoint { X = 93.75f, Y = -10, shape = PitchPointShape.io}, + new PitchPoint { X = 194.7f, Y = 0, shape = PitchPointShape.io} + }, snapFirst = true + }, toAdd[0].pitch); + Assert.Equal(400, toAdd[1].duration); Assert.Equal("zo", toAdd[1].lyric); + Assert.Equivalent(new UPitch { + data = new List { + new PitchPoint { X = -50, Y = 0, shape = PitchPointShape.io}, + new PitchPoint { X = 50, Y = 0, shape = PitchPointShape.io} + }, snapFirst = true + }, toAdd[1].pitch); Assert.Equal(240, toAdd[2].duration); Assert.Equal("me", toAdd[2].lyric); } finally { diff --git a/OpenUtau/ViewModels/NotesViewModel.cs b/OpenUtau/ViewModels/NotesViewModel.cs index 07f2d4605..64ee78259 100644 --- a/OpenUtau/ViewModels/NotesViewModel.cs +++ b/OpenUtau/ViewModels/NotesViewModel.cs @@ -511,17 +511,24 @@ public void DeselectNotes() { } public void ToggleSelectNote(UNote note) { + /// + /// Change the selection state of a note without affecting the selection state of the other notes. + /// Add it to selection if it isn't selected, or deselect it if it is already selected. + /// if (Part == null) { return; } if (Selection.Contains(note)) { DeselectNote(note); } else { - SelectNote(note); + SelectNote(note, false); } } public void SelectNote(UNote note) { + /// + /// Select a note and deselect all the other notes. + /// SelectNote(note, true); } public void SelectNote(UNote note, bool deselectExisting) {