diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index 132412639..f92f5bce4 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -166,6 +166,7 @@ public class SerializablePreferences { public int DiffSingerStepsPitch = 10; public bool DiffSingerTensorCache = true; public bool DiffSingerLangCodeHide = false; + public bool DiffSingerShowRenderPhraseBoundaries = false; public bool SkipRenderingMutedTracks = false; public string Language = string.Empty; public string? SortingOrder = null; diff --git a/OpenUtau/Controls/NotesCanvas.cs b/OpenUtau/Controls/NotesCanvas.cs index a0b1e46e4..a4e78c40e 100644 --- a/OpenUtau/Controls/NotesCanvas.cs +++ b/OpenUtau/Controls/NotesCanvas.cs @@ -4,8 +4,10 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.Immutable; using OpenUtau.App.ViewModels; using OpenUtau.Core; +using OpenUtau.Core.Render; using OpenUtau.Core.Ustx; using OpenUtau.Core.Util; using ReactiveUI; @@ -189,6 +191,7 @@ public override void Render(DrawingContext context) { } RenderNoteBody(note, viewModel, context); } + RenderDiffSingerPhraseBoundaries(leftTick, rightTick, viewModel, context); if (ShowFinalPitch && !hidePitch) { RenderFinalPitch(leftTick, rightTick, viewModel, context); } @@ -335,6 +338,101 @@ private void RenderGhostNote(UNote note, NotesViewModel viewModel, DrawingContex context.DrawRectangle(brush, null, new Rect(leftTop, rightBottom), 2, 2); } + private static readonly IDashStyle PhraseBoundaryDashStyle = new ImmutableDashStyle(new double[] { 4, 2, 1, 2 }, 0); + private static readonly IBrush PhraseOverlapBrush = new ImmutableSolidColorBrush(Color.FromRgb(0xFF, 0x8C, 0x00)); + + private void RenderDiffSingerPhraseBoundaries(double viewLeftTick, double viewRightTick, NotesViewModel viewModel, DrawingContext context) { + if (!Preferences.Default.DiffSingerShowRenderPhraseBoundaries) { + return; + } + if (!TryGetDiffSingerRenderer(viewModel, out var renderer)) { + return; + } + var accent = ThemeManager.AccentBrush3; + var boundaryPen = new Pen(accent, 1) { DashStyle = PhraseBoundaryDashStyle }; + var railPen = new Pen(accent, 2); + var overlapRailPen = new Pen(PhraseOverlapBrush, 2); + RenderPhrase[] phrases; + lock (Part!) { + phrases = Part!.renderPhrases.ToArray(); + } + var visible = new List<(double startTick, double endTick)>(phrases.Length); + foreach (var phrase in phrases) { + var (startTick, endTick) = GetRenderedPhraseTickBounds(phrase, renderer); + if (startTick >= viewRightTick || endTick <= viewLeftTick) { + continue; + } + visible.Add((startTick, endTick)); + } + foreach (var (startTick, endTick) in visible) { + DrawPhraseBoundaryLine(context, boundaryPen, viewModel.TickToneToPoint(startTick, 0).X); + DrawPhraseBoundaryLine(context, boundaryPen, viewModel.TickToneToPoint(endTick, 0).X); + } + var events = new List<(double tick, int delta)>(visible.Count * 2); + foreach (var (startTick, endTick) in visible) { + events.Add((startTick, +1)); + events.Add((endTick, -1)); + } + events.Sort((a, b) => a.tick.CompareTo(b.tick)); + int coverage = 0; + double? segStart = null; + int i = 0; + while (i < events.Count) { + double tick = events[i].tick; + if (segStart.HasValue && coverage > 0 && tick > segStart.Value) { + double startX = Math.Clamp(viewModel.TickToneToPoint(segStart.Value, 0).X, 0, Bounds.Width); + double endX = Math.Clamp(viewModel.TickToneToPoint(tick, 0).X, 0, Bounds.Width); + if (endX > startX) { + var pen = coverage >= 2 ? overlapRailPen : railPen; + context.DrawLine(pen, new Point(startX, 3.5), new Point(endX, 3.5)); + } + } + while (i < events.Count && events[i].tick == tick) { + coverage += events[i].delta; + i++; + } + segStart = tick; + } + } + + private void DrawPhraseBoundaryLine(DrawingContext context, IPen pen, double x) { + if (Bounds.Width < 1 || x < 0 || x > Bounds.Width) { + return; + } + double crispX = Math.Clamp(Math.Round(x) + 0.5, 0.5, Bounds.Width - 0.5); + context.DrawLine(pen, new Point(crispX, 0), new Point(crispX, Bounds.Height)); + } + + private bool TryGetDiffSingerRenderer(NotesViewModel viewModel, out IRenderer? renderer) { + renderer = null; + if (Part == null || viewModel.Project == null || Part.trackNo < 0 || Part.trackNo >= viewModel.Project.tracks.Count) { + return false; + } + var settings = viewModel.Project.tracks[Part.trackNo].RendererSettings; + renderer = settings?.Renderer; + return string.Equals(renderer?.ToString(), Renderers.DIFFSINGER, StringComparison.OrdinalIgnoreCase) + || string.Equals(settings?.renderer, Renderers.DIFFSINGER, StringComparison.OrdinalIgnoreCase); + } + + private (double startTick, double endTick) GetRenderedPhraseTickBounds(RenderPhrase phrase, IRenderer? renderer) { + if (Part == null) { + return (0, 0); + } + try { + var layout = renderer?.Layout(phrase); + if (layout != null) { + double startMs = layout.positionMs - layout.leadingMs; + double endMs = startMs + layout.estimatedLengthMs; + return ( + phrase.timeAxis.MsPosToTickPos(startMs) - Part.position, + phrase.timeAxis.MsPosToTickPos(endMs) - Part.position); + } + } catch { + // Rendering invalid singers should not break piano roll painting. + } + return (phrase.position - phrase.leading - Part.position, phrase.end - Part.position); + } + private void RenderPitchBend(UNote note, NotesViewModel viewModel, DrawingContext context) { var pitchExp = note.pitch; var pts = pitchExp.data; diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index 4aa620c1e..98f028ce0 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -587,6 +587,7 @@ Warning: this option removes custom presets. DiffSinger Tensor Cache Preferences DiffSinger + Show render phrase boundaries Editing General Note: please restart OpenUtau after changing this item. diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index 9db829b46..b91007d6a 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -121,6 +121,7 @@ public int SafeMaxThreadCount { [Reactive] public double DiffSingerDepth { get; set; } [Reactive] public bool DiffSingerTensorCache { get; set; } [Reactive] public bool DiffSingerLangCodeHide { get; set; } + [Reactive] public bool DiffSingerShowRenderPhraseBoundaries { get; set; } // Advanced [Reactive] public bool RememberMid { get; set; } @@ -175,6 +176,7 @@ public PreferencesViewModel() { DiffSingerStepsPitch = Preferences.Default.DiffSingerStepsPitch; DiffSingerTensorCache = Preferences.Default.DiffSingerTensorCache; DiffSingerLangCodeHide = Preferences.Default.DiffSingerLangCodeHide; + DiffSingerShowRenderPhraseBoundaries = Preferences.Default.DiffSingerShowRenderPhraseBoundaries; SkipRenderingMutedTracks = Preferences.Default.SkipRenderingMutedTracks; ThemeName = Preferences.Default.ThemeName; PenPlusDefault = Preferences.Default.PenPlusDefault; @@ -398,6 +400,12 @@ public PreferencesViewModel() { Preferences.Default.DiffSingerLangCodeHide = useCache; Preferences.Save(); }); + this.WhenAnyValue(vm => vm.DiffSingerShowRenderPhraseBoundaries) + .Subscribe(showBoundaries => { + Preferences.Default.DiffSingerShowRenderPhraseBoundaries = showBoundaries; + Preferences.Save(); + MessageBus.Current.SendMessage(new NotesRefreshEvent()); + }); this.WhenAnyValue(vm => vm.SkipRenderingMutedTracks) .Subscribe(skipRenderingMutedTracks => { Preferences.Default.SkipRenderingMutedTracks = skipRenderingMutedTracks; diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index e116bbe34..8087507d7 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -329,6 +329,10 @@ + + + +