From 3203a0d25154e7a65b4cd6a8042de7e3dac6f40d Mon Sep 17 00:00:00 2001 From: emeraldsingers Date: Sun, 12 Jul 2026 17:10:05 +0300 Subject: [PATCH 1/2] Add smooth vertical playback auto-scroll Add optional vertical note following during piano roll playback. Center the active note vertically using frame-rate-independent exponential smoothing, configurable follow margin and damping, and a per-frame movement limit. Prepare scrolling before upcoming notes and ignore isolated melodic outliers to avoid abrupt jumps. Add the toggle to the piano roll View menu and expose vertical follow settings in Preferences. --- OpenUtau.Core/Util/Preferences.cs | 5 + OpenUtau/Controls/PianoRoll.axaml | 5 + OpenUtau/Controls/PianoRoll.axaml.cs | 4 + OpenUtau/Strings/Strings.axaml | 1 + OpenUtau/ViewModels/NotesViewModel.cs | 115 +++++++++++++++++++- OpenUtau/ViewModels/PreferencesViewModel.cs | 14 +++ OpenUtau/Views/PreferencesDialog.axaml | 12 ++ 7 files changed, 155 insertions(+), 1 deletion(-) diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index f14fe185d..b7f101576 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -184,10 +184,15 @@ public class SerializablePreferences { public double PlayPosMarkerMargin = 0.9; public int LockStartTime = 0; public int PlaybackAutoScroll = 2; + public double PlaybackVerticalFollowMargin = 1.5; + // Lower damping follows more smoothly; max step caps tracks scrolled per frame. + public double PlaybackVerticalFollowDamping = 3; + public double PlaybackVerticalFollowMaxStep = 1.5; public bool ReverseLogOrder = true; public bool ShowPortrait = true; public bool ShowIcon = true; public bool ShowGhostNotes = true; + public bool ShowPlaybackVerticalFollow = false; public EditTool EditTool = new EditTool(); public bool PlayTone = true; public bool ShowVibrato = true; diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml index 647d22145..56d9a49f8 100644 --- a/OpenUtau/Controls/PianoRoll.axaml +++ b/OpenUtau/Controls/PianoRoll.axaml @@ -296,6 +296,11 @@ + + + + + diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs index 00cfaaefe..cacb7e8de 100644 --- a/OpenUtau/Controls/PianoRoll.axaml.cs +++ b/OpenUtau/Controls/PianoRoll.axaml.cs @@ -284,6 +284,10 @@ void OnMenuShowGhostNotes(object sender, RoutedEventArgs args) { MessageBus.Current.SendMessage(new PianorollRefreshEvent("Part")); } + void OnMenuPlaybackVerticalFollow(object sender, RoutedEventArgs args) { + ViewModel.NotesViewModel.ShowPlaybackVerticalFollow = + !ViewModel.NotesViewModel.ShowPlaybackVerticalFollow; + } void OnMenuUseTrackColor(object sender, RoutedEventArgs args) { Preferences.Default.UseTrackColor = !Preferences.Default.UseTrackColor; Preferences.Save(); diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index a5f175793..b7d92629c 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -515,6 +515,7 @@ Warning: this option removes custom presets. Toggle Overwrite Pitch Mode View Vibrato (U) View Waveform (W) + Center Playback Note Vertically View Expressions (L) Draw Pitch Tool (Shift + 1) Left click to draw diff --git a/OpenUtau/ViewModels/NotesViewModel.cs b/OpenUtau/ViewModels/NotesViewModel.cs index c14e9deee..461751af4 100644 --- a/OpenUtau/ViewModels/NotesViewModel.cs +++ b/OpenUtau/ViewModels/NotesViewModel.cs @@ -61,6 +61,7 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { [Reactive] public bool ShowPhoneme { get; set; } [Reactive] public bool ShowNoteParams { get; set; } [Reactive] public bool ShowExpressions { get; set; } + [Reactive] public bool ShowPlaybackVerticalFollow { get; set; } [Reactive] public bool IsSnapOn { get; set; } [Reactive] public string SnapDivText { get; set; } [Reactive] public string KeyText { get; set; } @@ -108,6 +109,10 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { private readonly object portraitLock = new object(); private int userSnapDiv = -2; private int userKey => Project.key; + private int playPosTick; + private DateTime playbackVerticalFollowLastFrame = DateTime.UtcNow; + private double? playbackVerticalFollowTargetOffset; + private static readonly int PlaybackVerticalFollowOutlierThreshold = 12; public NotesViewModel() { SnapDivs = new List(); @@ -267,6 +272,16 @@ public NotesViewModel() { Preferences.Default.ShowNoteParams = showNoteParams; Preferences.Save(); }); + ShowPlaybackVerticalFollow = Preferences.Default.ShowPlaybackVerticalFollow; + this.WhenAnyValue(x => x.ShowPlaybackVerticalFollow) + .Subscribe(showPlaybackVerticalFollow => { + Preferences.Default.ShowPlaybackVerticalFollow = showPlaybackVerticalFollow; + Preferences.Save(); + ResetPlaybackVerticalFollowAnimation(); + }); + this.WhenAnyValue(x => x.TrackOffset) + .Skip(1) + .Subscribe(_ => playbackVerticalFollowTargetOffset = null); TickWidth = ViewConstants.PianoRollTickWidthDefault; TrackHeight = ViewConstants.NoteHeightDefault; @@ -443,6 +458,7 @@ private void LoadPart(UPart part, UProject project) { } UnloadPart(); Part = part as UVoicePart; + ResetPlaybackVerticalFollowAnimation(); OnPartModified(); LoadPortrait(part, project); LoadWindowTitle(part, project); @@ -557,6 +573,7 @@ private void LoadTrackColor(UPart? part, UProject? project) { private void UnloadPart() { DeselectNotes(); Part = null; + ResetPlaybackVerticalFollowAnimation(); LoadPortrait(null, null); LoadWindowTitle(null, null); } @@ -978,11 +995,12 @@ public void ClearPhraseCache() { } private void SetPlayPos(int tick, bool waitingRendering) { + playPosTick = tick - (Part?.position ?? 0); PlayPosWaitingRendering = waitingRendering; if (waitingRendering) { return; } - tick -= Part?.position ?? 0; + tick = playPosTick; PlayPosX = TickToneToPoint(tick, 0).X; UpdateHighlight(); } @@ -1075,6 +1093,9 @@ public void OnNext(UCommand cmd, bool isUndo) { if (!setPlayPosTick.pause || Preferences.Default.LockStartTime == 1) { MaybeAutoScroll(PlayPosX); } + if (!setPlayPosTick.pause && !setPlayPosTick.waitingRendering) { + MaybeAutoScrollVertical(); + } } else if (cmd is SetRangeSelectionNotification) { UpdateHighlight(); } else if (cmd is FocusNoteNotification focusNote) { @@ -1169,6 +1190,98 @@ private void AutoScroll(double positionX) { TickOffset = Math.Clamp(TickOffset + scrollDelta, 0, HScrollBarMax); } + private void MaybeAutoScrollVertical() { + if (!ShowPlaybackVerticalFollow || Part == null || ViewportTracks <= 0) { + return; + } + var targetNote = FindPlaybackVerticalFollowNote(playPosTick); + if (targetNote == null) { + return; + } + double noteTrackPos = ViewConstants.MaxTone - 1 - (targetNote.AdjustedTone - 0.5); + double margin = Preferences.Default.PlaybackVerticalFollowMargin; + double centerTrackPos = TrackOffset + ViewportTracks * 0.5; + + if (playbackVerticalFollowTargetOffset.HasValue && + Math.Abs(playbackVerticalFollowTargetOffset.Value - TrackOffset) < 0.1) { + playbackVerticalFollowTargetOffset = null; + } + if (Math.Abs(noteTrackPos - centerTrackPos) > margin) { + playbackVerticalFollowTargetOffset = noteTrackPos - ViewportTracks * 0.5; + } + + double targetTrackOffset = Math.Clamp( + playbackVerticalFollowTargetOffset ?? TrackOffset, 0, VScrollBarMax); + double scrollDelta = GetVerticalFollowScrollValueDelta(targetTrackOffset); + if (Math.Abs(scrollDelta) >= 0.0001) { + TrackOffset = Math.Clamp(TrackOffset + scrollDelta, 0, VScrollBarMax); + } + } + + private UNote? FindPlaybackVerticalFollowNote(int tick) { + if (Part == null || Part.notes.Count == 0) { + return null; + } + UNote? current = Part.notes.FirstOrDefault( + note => note.LeftBound <= tick && tick < note.RightBound); + if (current != null) { + return IsToneOutlier(current) ? null : current; + } + UNote? next = current?.Next ?? Part.notes.FirstOrDefault(note => note.position > tick); + if (next != null && current == null && tick < GetVerticalFollowPreparationTick(next)) { + return null; + } + return next != null && IsToneOutlier(next) ? null : next; + } + + private bool IsToneOutlier(UNote note) { + if (PlaybackVerticalFollowOutlierThreshold <= 0) { + return false; + } + var window = new List(); + var previous = note.Prev; + for (int i = 0; i < 3 && previous != null; i++, previous = previous.Prev) { + window.Add(previous.tone); + } + var next = note.Next; + for (int i = 0; i < 3 && next != null; i++, next = next.Next) { + window.Add(next.tone); + } + if (window.Count == 0) { + return false; + } + window.Sort(); + double median = window[window.Count / 2]; + return Math.Abs(note.tone - median) > PlaybackVerticalFollowOutlierThreshold; + } + + private int GetVerticalFollowPreparationTick(UNote note) { + int minLead = Math.Max(1, Project.resolution / 4); + int maxLead = Math.Max(minLead, Project.resolution * 2); + int prepTick = note.position - Math.Clamp(note.duration, minLead, maxLead); + UNote? previous = note.Prev; + return previous == null ? prepTick : Math.Max(prepTick, previous.RightBound); + } + + private double GetVerticalFollowScrollValueDelta(double targetTrackOffset) { + var now = DateTime.UtcNow; + double dt = Math.Clamp((now - playbackVerticalFollowLastFrame).TotalSeconds, 0, 0.05); + playbackVerticalFollowLastFrame = now; + double delta = targetTrackOffset - TrackOffset; + if (Math.Abs(delta) < 0.01) { + return 0; + } + double alpha = 1 - Math.Exp(-Preferences.Default.PlaybackVerticalFollowDamping * dt); + double step = delta * alpha; + double maxStep = Preferences.Default.PlaybackVerticalFollowMaxStep; + return Math.Clamp(step, -maxStep, maxStep); + } + + private void ResetPlaybackVerticalFollowAnimation() { + playbackVerticalFollowLastFrame = DateTime.UtcNow; + playbackVerticalFollowTargetOffset = null; + } + private double GetScrollValueDelta(double positionX) { var pageScroll = Preferences.Default.PlaybackAutoScroll == 2; if (pageScroll) { diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index 0683b822e..f2a8c1749 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -58,6 +58,8 @@ public AudioOutputDevice? AudioOutputDevice { [Reactive] public int PreferPortAudio { get; set; } [Reactive] public int LockStartTime { get; set; } [Reactive] public int PlaybackAutoScroll { get; set; } + [Reactive] public double PlaybackVerticalFollowMargin { get; set; } + [Reactive] public double PlaybackVerticalFollowDamping { get; set; } [Reactive] public double PlayPosMarkerMargin { get; set; } // Paths @@ -141,6 +143,8 @@ public PreferencesViewModel() { UseSystemDefaultDevice = Preferences.Default.UseSystemDefaultAudioDevice; PreferPortAudio = Preferences.Default.PreferPortAudio ? 1 : 0; PlaybackAutoScroll = Preferences.Default.PlaybackAutoScroll; + PlaybackVerticalFollowMargin = Preferences.Default.PlaybackVerticalFollowMargin; + PlaybackVerticalFollowDamping = Preferences.Default.PlaybackVerticalFollowDamping; PlayPosMarkerMargin = Preferences.Default.PlayPosMarkerMargin; LockStartTime = Preferences.Default.LockStartTime; InstallToAdditionalSingersPath = Preferences.Default.InstallToAdditionalSingersPath; @@ -224,6 +228,16 @@ public PreferencesViewModel() { Preferences.Default.PlaybackAutoScroll = autoScroll; Preferences.Save(); }); + this.WhenAnyValue(vm => vm.PlaybackVerticalFollowMargin) + .Subscribe(margin => { + Preferences.Default.PlaybackVerticalFollowMargin = Math.Clamp(margin, 0.0, 10.0); + Preferences.Save(); + }); + this.WhenAnyValue(vm => vm.PlaybackVerticalFollowDamping) + .Subscribe(damping => { + Preferences.Default.PlaybackVerticalFollowDamping = Math.Clamp(damping, 1.0, 20.0); + Preferences.Save(); + }); this.WhenAnyValue(vm => vm.PlayPosMarkerMargin) .Subscribe(playPosMarkerMargin => { Preferences.Default.PlayPosMarkerMargin = playPosMarkerMargin; diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index 290323724..60da1e104 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -129,6 +129,18 @@ + + + + + + + + + + From 3b4f92212a4caffa09eb7dcd834c21db818ac9a9 Mon Sep 17 00:00:00 2001 From: emeraldsingers Date: Sun, 12 Jul 2026 17:24:58 +0300 Subject: [PATCH 2/2] Add playback note highlighting Highlight the active piano roll note during playback with configurable fade-in and fade-out speeds. Add an opt-in View menu toggle, persist the preference, and avoid repainting NotesCanvas on playback tick updates when highlighting is disabled. --- OpenUtau.Core/Util/Preferences.cs | 3 + OpenUtau/Controls/NotesCanvas.cs | 123 +++++++++++++++++++- OpenUtau/Controls/PianoRoll.axaml | 7 ++ OpenUtau/Controls/PianoRoll.axaml.cs | 4 + OpenUtau/Strings/Strings.axaml | 3 + OpenUtau/ViewModels/NotesViewModel.cs | 16 ++- OpenUtau/ViewModels/PreferencesViewModel.cs | 14 +++ OpenUtau/Views/PreferencesDialog.axaml | 12 ++ 8 files changed, 176 insertions(+), 6 deletions(-) diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index b7f101576..7d6273400 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -193,6 +193,9 @@ public class SerializablePreferences { public bool ShowIcon = true; public bool ShowGhostNotes = true; public bool ShowPlaybackVerticalFollow = false; + public bool ShowPlaybackNoteHighlight = false; + public double PlaybackHighlightFadeInPerSecond = 8.0; + public double PlaybackHighlightFadeOutPerSecond = 6.2; public EditTool EditTool = new EditTool(); public bool PlayTone = true; public bool ShowVibrato = true; diff --git a/OpenUtau/Controls/NotesCanvas.cs b/OpenUtau/Controls/NotesCanvas.cs index a0b1e46e4..979c495be 100644 --- a/OpenUtau/Controls/NotesCanvas.cs +++ b/OpenUtau/Controls/NotesCanvas.cs @@ -4,6 +4,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Threading; using OpenUtau.App.ViewModels; using OpenUtau.Core; using OpenUtau.Core.Ustx; @@ -52,6 +53,16 @@ class NotesCanvas : Control { nameof(ShowVibrato), o => o.ShowVibrato, (o, v) => o.ShowVibrato = v); + public static readonly DirectProperty ShowPlaybackNoteHighlightProperty = + AvaloniaProperty.RegisterDirect( + nameof(ShowPlaybackNoteHighlight), + o => o.ShowPlaybackNoteHighlight, + (o, v) => o.ShowPlaybackNoteHighlight = v); + public static readonly DirectProperty PlayPosTickProperty = + AvaloniaProperty.RegisterDirect( + nameof(PlayPosTick), + o => o.PlayPosTick, + (o, v) => o.PlayPosTick = v); public static readonly DirectProperty ShowPhonemizerTagsProperty = AvaloniaProperty.RegisterDirect( nameof(ShowPhonemizerTags), @@ -90,6 +101,14 @@ public bool ShowVibrato { get => showVibrato; private set => SetAndRaise(ShowVibratoProperty, ref showVibrato, value); } + public bool ShowPlaybackNoteHighlight { + get => showPlaybackNoteHighlight; + private set => SetAndRaise(ShowPlaybackNoteHighlightProperty, ref showPlaybackNoteHighlight, value); + } + public int PlayPosTick { + get => playPosTick; + private set => SetAndRaise(PlayPosTickProperty, ref playPosTick, value); + } public bool ShowPhonemizerTags { get => showPhonemizerTags; private set => SetAndRaise(ShowPhonemizerTagsProperty, ref showPhonemizerTags, value); @@ -103,6 +122,11 @@ public bool ShowPhonemizerTags { private bool showPitch = true; private bool showFinalPitch = true; private bool showVibrato = true; + private bool showPlaybackNoteHighlight; + private int playPosTick = int.MinValue; + private DateTime playbackHighlightLastFrame = DateTime.UtcNow; + private readonly Dictionary playbackHighlightLevels = new Dictionary(); + private bool playbackAnimationInvalidateQueued; private bool showPhonemizerTags = true; private PolylineGeometry polylineGeometry = new PolylineGeometry(); private Points points = new Points(); @@ -129,7 +153,10 @@ public NotesCanvas() { MessageBus.Current.Listen() .Subscribe(_ => RefreshGhostNotes()); this.WhenAnyValue(x => x.Part) - .Subscribe(_ => RefreshGhostNotes()); + .Subscribe(_ => { + ResetPlaybackHighlightAnimation(); + RefreshGhostNotes(); + }); } void RefreshGhostNotes() { @@ -146,6 +173,9 @@ void RefreshGhostNotes() { protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); + if (change.Property == PlayPosTickProperty && !showPlaybackNoteHighlight) { + return; + } InvalidateVisual(); } @@ -162,6 +192,7 @@ public override void Render(DrawingContext context) { double leftTick = TickOffset - 480; double rightTick = TickOffset + Bounds.Width / TickWidth + 480; bool hidePitch = viewModel.TickWidth <= ViewConstants.PianoRollTickWidthShowDetails * 0.5; + UpdatePlaybackHighlightAnimation(); if (showGhostNotes) { foreach (UPart otherPart in otherPartsInView) { @@ -219,9 +250,17 @@ private void RenderNoteBody(UNote note, NotesViewModel viewModel, DrawingContext Size size = viewModel.TickToneToSize(note.duration, 1); size = size.WithWidth(size.Width - 1).WithHeight(Math.Floor(size.Height - 2)); Point rightBottom = new Point(leftTop.X + size.Width, leftTop.Y + size.Height); - var brush = selectedNotes.Contains(note) + bool isSelected = selectedNotes.Contains(note); + var brush = isSelected ? (note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2) : (note.Error ? ThemeManager.AccentBrush1Semi : ThemeManager.AccentBrush1); + if (!isSelected) { + float highlight = GetPlaybackHighlightLevel(note); + if (highlight > 0.001f) { + var highlightedBrush = note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2; + brush = BlendBrush(brush, highlightedBrush, highlight); + } + } context.DrawRectangle(brush, null, new Rect(leftTop, rightBottom), 2, 2); if (TrackHeight < 10 || note.lyric.Length == 0) { return; @@ -335,6 +374,86 @@ private void RenderGhostNote(UNote note, NotesViewModel viewModel, DrawingContex context.DrawRectangle(brush, null, new Rect(leftTop, rightBottom), 2, 2); } + private void ResetPlaybackHighlightAnimation() { + playbackHighlightLevels.Clear(); + playbackHighlightLastFrame = DateTime.UtcNow; + } + + private void UpdatePlaybackHighlightAnimation() { + if (Part == null) { + ResetPlaybackHighlightAnimation(); + return; + } + var now = DateTime.UtcNow; + float dt = (float)Math.Clamp((now - playbackHighlightLastFrame).TotalSeconds, 0, 0.1); + playbackHighlightLastFrame = now; + + UNote? activeNote = null; + if (ShowPlaybackNoteHighlight && PlaybackManager.Inst.PlayingMaster) { + activeNote = Part.notes.FirstOrDefault( + note => note.LeftBound <= PlayPosTick && PlayPosTick < note.RightBound); + if (activeNote != null && !playbackHighlightLevels.ContainsKey(activeNote)) { + playbackHighlightLevels[activeNote] = 0f; + } + } + + bool animating = false; + foreach (var note in playbackHighlightLevels.Keys.ToList()) { + float current = playbackHighlightLevels[note]; + float target = note == activeNote ? 1f : 0f; + float fadeIn = (float)Math.Clamp( + Preferences.Default.PlaybackHighlightFadeInPerSecond, 0.1, 30.0); + float fadeOut = (float)Math.Clamp( + Preferences.Default.PlaybackHighlightFadeOutPerSecond, 0.1, 30.0); + float next = MoveTowards(current, target, (target > current ? fadeIn : fadeOut) * dt); + if (target == 0f && next <= 0.001f) { + playbackHighlightLevels.Remove(note); + continue; + } + playbackHighlightLevels[note] = next; + animating |= Math.Abs(next - target) > 0.001f; + } + if (animating) { + QueuePlaybackAnimationFrame(); + } + } + + private void QueuePlaybackAnimationFrame() { + if (playbackAnimationInvalidateQueued) { + return; + } + playbackAnimationInvalidateQueued = true; + Dispatcher.UIThread.Post(() => { + playbackAnimationInvalidateQueued = false; + InvalidateVisual(); + }, DispatcherPriority.Background); + } + + private float GetPlaybackHighlightLevel(UNote note) { + return playbackHighlightLevels.TryGetValue(note, out var value) ? value : 0f; + } + + private static float MoveTowards(float current, float target, float maxDelta) { + if (Math.Abs(target - current) <= maxDelta) { + return target; + } + return current + Math.Sign(target - current) * maxDelta; + } + + private static IBrush BlendBrush(IBrush from, IBrush to, float amount) { + if (from is not ISolidColorBrush fromSolid || to is not ISolidColorBrush toSolid) { + return amount >= 0.5f ? to : from; + } + amount = Math.Clamp(amount, 0f, 1f); + Color a = fromSolid.Color; + Color b = toSolid.Color; + return new SolidColorBrush(Color.FromArgb( + (byte)(a.A + (b.A - a.A) * amount), + (byte)(a.R + (b.R - a.R) * amount), + (byte)(a.G + (b.G - a.G) * amount), + (byte)(a.B + (b.B - a.B) * amount))); + } + private void RenderPitchBend(UNote note, NotesViewModel viewModel, DrawingContext context) { var pitchExp = note.pitch; var pts = pitchExp.data; diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml index 56d9a49f8..6f1a366a7 100644 --- a/OpenUtau/Controls/PianoRoll.axaml +++ b/OpenUtau/Controls/PianoRoll.axaml @@ -169,6 +169,8 @@ ShowVibrato="{Binding NotesViewModel.ShowVibrato}" ShowPitch="{Binding NotesViewModel.ShowPitch}" ShowFinalPitch="{Binding NotesViewModel.ShowFinalPitch}" + ShowPlaybackNoteHighlight="{Binding NotesViewModel.ShowPlaybackNoteHighlight}" + PlayPosTick="{Binding NotesViewModel.PlayPosTick}" ShowPhonemizerTags="{Binding ShowPhonemizerTags}" Part="{Binding NotesViewModel.Part}" PointerPressed="NotesCanvasPointerPressed" @@ -301,6 +303,11 @@ + + + + + diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs index cacb7e8de..c1d0b1a45 100644 --- a/OpenUtau/Controls/PianoRoll.axaml.cs +++ b/OpenUtau/Controls/PianoRoll.axaml.cs @@ -288,6 +288,10 @@ void OnMenuPlaybackVerticalFollow(object sender, RoutedEventArgs args) { ViewModel.NotesViewModel.ShowPlaybackVerticalFollow = !ViewModel.NotesViewModel.ShowPlaybackVerticalFollow; } + void OnMenuPlaybackNoteHighlight(object sender, RoutedEventArgs args) { + ViewModel.NotesViewModel.ShowPlaybackNoteHighlight = + !ViewModel.NotesViewModel.ShowPlaybackNoteHighlight; + } void OnMenuUseTrackColor(object sender, RoutedEventArgs args) { Preferences.Default.UseTrackColor = !Preferences.Default.UseTrackColor; Preferences.Save(); diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index b7d92629c..2e8e270a8 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -516,6 +516,9 @@ Warning: this option removes custom presets. View Vibrato (U) View Waveform (W) Center Playback Note Vertically + Highlight Notes On Playback + Note highlight fade-in speed + Note highlight fade-out speed View Expressions (L) Draw Pitch Tool (Shift + 1) Left click to draw diff --git a/OpenUtau/ViewModels/NotesViewModel.cs b/OpenUtau/ViewModels/NotesViewModel.cs index 461751af4..c0799456f 100644 --- a/OpenUtau/ViewModels/NotesViewModel.cs +++ b/OpenUtau/ViewModels/NotesViewModel.cs @@ -52,6 +52,7 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { [Reactive] public double PlayPosHighlightX { get; set; } [Reactive] public double PlayPosHighlightWidth { get; set; } [Reactive] public bool PlayPosWaitingRendering { get; set; } + [Reactive] public int PlayPosTick { get; set; } [Reactive] public bool ShowTips { get; set; } [Reactive] public bool PlayTone { get; set; } [Reactive] public bool ShowVibrato { get; set; } @@ -62,6 +63,7 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { [Reactive] public bool ShowNoteParams { get; set; } [Reactive] public bool ShowExpressions { get; set; } [Reactive] public bool ShowPlaybackVerticalFollow { get; set; } + [Reactive] public bool ShowPlaybackNoteHighlight { get; set; } [Reactive] public bool IsSnapOn { get; set; } [Reactive] public string SnapDivText { get; set; } [Reactive] public string KeyText { get; set; } @@ -109,7 +111,6 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { private readonly object portraitLock = new object(); private int userSnapDiv = -2; private int userKey => Project.key; - private int playPosTick; private DateTime playbackVerticalFollowLastFrame = DateTime.UtcNow; private double? playbackVerticalFollowTargetOffset; private static readonly int PlaybackVerticalFollowOutlierThreshold = 12; @@ -279,6 +280,13 @@ public NotesViewModel() { Preferences.Save(); ResetPlaybackVerticalFollowAnimation(); }); + ShowPlaybackNoteHighlight = Preferences.Default.ShowPlaybackNoteHighlight; + this.WhenAnyValue(x => x.ShowPlaybackNoteHighlight) + .Subscribe(showPlaybackNoteHighlight => { + Preferences.Default.ShowPlaybackNoteHighlight = showPlaybackNoteHighlight; + Preferences.Save(); + MessageBus.Current.SendMessage(new NotesRefreshEvent()); + }); this.WhenAnyValue(x => x.TrackOffset) .Skip(1) .Subscribe(_ => playbackVerticalFollowTargetOffset = null); @@ -995,12 +1003,12 @@ public void ClearPhraseCache() { } private void SetPlayPos(int tick, bool waitingRendering) { - playPosTick = tick - (Part?.position ?? 0); + PlayPosTick = tick - (Part?.position ?? 0); PlayPosWaitingRendering = waitingRendering; if (waitingRendering) { return; } - tick = playPosTick; + tick = PlayPosTick; PlayPosX = TickToneToPoint(tick, 0).X; UpdateHighlight(); } @@ -1194,7 +1202,7 @@ private void MaybeAutoScrollVertical() { if (!ShowPlaybackVerticalFollow || Part == null || ViewportTracks <= 0) { return; } - var targetNote = FindPlaybackVerticalFollowNote(playPosTick); + var targetNote = FindPlaybackVerticalFollowNote(PlayPosTick); if (targetNote == null) { return; } diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index f2a8c1749..5bfd2c797 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -60,6 +60,8 @@ public AudioOutputDevice? AudioOutputDevice { [Reactive] public int PlaybackAutoScroll { get; set; } [Reactive] public double PlaybackVerticalFollowMargin { get; set; } [Reactive] public double PlaybackVerticalFollowDamping { get; set; } + [Reactive] public double PlaybackHighlightFadeInPerSecond { get; set; } + [Reactive] public double PlaybackHighlightFadeOutPerSecond { get; set; } [Reactive] public double PlayPosMarkerMargin { get; set; } // Paths @@ -145,6 +147,8 @@ public PreferencesViewModel() { PlaybackAutoScroll = Preferences.Default.PlaybackAutoScroll; PlaybackVerticalFollowMargin = Preferences.Default.PlaybackVerticalFollowMargin; PlaybackVerticalFollowDamping = Preferences.Default.PlaybackVerticalFollowDamping; + PlaybackHighlightFadeInPerSecond = Preferences.Default.PlaybackHighlightFadeInPerSecond; + PlaybackHighlightFadeOutPerSecond = Preferences.Default.PlaybackHighlightFadeOutPerSecond; PlayPosMarkerMargin = Preferences.Default.PlayPosMarkerMargin; LockStartTime = Preferences.Default.LockStartTime; InstallToAdditionalSingersPath = Preferences.Default.InstallToAdditionalSingersPath; @@ -238,6 +242,16 @@ public PreferencesViewModel() { Preferences.Default.PlaybackVerticalFollowDamping = Math.Clamp(damping, 1.0, 20.0); Preferences.Save(); }); + this.WhenAnyValue(vm => vm.PlaybackHighlightFadeInPerSecond) + .Subscribe(value => { + Preferences.Default.PlaybackHighlightFadeInPerSecond = Math.Clamp(value, 0.1, 30.0); + Preferences.Save(); + }); + this.WhenAnyValue(vm => vm.PlaybackHighlightFadeOutPerSecond) + .Subscribe(value => { + Preferences.Default.PlaybackHighlightFadeOutPerSecond = Math.Clamp(value, 0.1, 30.0); + Preferences.Save(); + }); this.WhenAnyValue(vm => vm.PlayPosMarkerMargin) .Subscribe(playPosMarkerMargin => { Preferences.Default.PlayPosMarkerMargin = playPosMarkerMargin; diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index 60da1e104..f8c07fab1 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -141,6 +141,18 @@ + + + + + + + + + +