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 @@
+
+
+
+