Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions OpenUtau.Core/Util/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,18 @@ 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 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;
Expand Down
123 changes: 121 additions & 2 deletions OpenUtau/Controls/NotesCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -52,6 +53,16 @@ class NotesCanvas : Control {
nameof(ShowVibrato),
o => o.ShowVibrato,
(o, v) => o.ShowVibrato = v);
public static readonly DirectProperty<NotesCanvas, bool> ShowPlaybackNoteHighlightProperty =
AvaloniaProperty.RegisterDirect<NotesCanvas, bool>(
nameof(ShowPlaybackNoteHighlight),
o => o.ShowPlaybackNoteHighlight,
(o, v) => o.ShowPlaybackNoteHighlight = v);
public static readonly DirectProperty<NotesCanvas, int> PlayPosTickProperty =
AvaloniaProperty.RegisterDirect<NotesCanvas, int>(
nameof(PlayPosTick),
o => o.PlayPosTick,
(o, v) => o.PlayPosTick = v);
public static readonly DirectProperty<NotesCanvas, bool> ShowPhonemizerTagsProperty =
AvaloniaProperty.RegisterDirect<NotesCanvas, bool>(
nameof(ShowPhonemizerTags),
Expand Down Expand Up @@ -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);
Expand All @@ -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<UNote, float> playbackHighlightLevels = new Dictionary<UNote, float>();
private bool playbackAnimationInvalidateQueued;
private bool showPhonemizerTags = true;
private PolylineGeometry polylineGeometry = new PolylineGeometry();
private Points points = new Points();
Expand All @@ -129,7 +153,10 @@ public NotesCanvas() {
MessageBus.Current.Listen<PartRefreshEvent>()
.Subscribe(_ => RefreshGhostNotes());
this.WhenAnyValue(x => x.Part)
.Subscribe(_ => RefreshGhostNotes());
.Subscribe(_ => {
ResetPlaybackHighlightAnimation();
RefreshGhostNotes();
});
}

void RefreshGhostNotes() {
Expand All @@ -146,6 +173,9 @@ void RefreshGhostNotes() {

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) {
base.OnPropertyChanged(change);
if (change.Property == PlayPosTickProperty && !showPlaybackNoteHighlight) {
return;
}
InvalidateVisual();
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions OpenUtau/Controls/PianoRoll.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -296,6 +298,16 @@
<CheckBox IsChecked="{Binding ShowGhostNotes}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource pianoroll.menu.view.playbackverticalfollow}" Click="OnMenuPlaybackVerticalFollow">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding NotesViewModel.ShowPlaybackVerticalFollow}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource pianoroll.menu.view.playbacknotehighlight}" Click="OnMenuPlaybackNoteHighlight">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding NotesViewModel.ShowPlaybackNoteHighlight}" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="{DynamicResource prefs.appearance.trackcolor}" Click="OnMenuUseTrackColor">
<MenuItem.Icon>
<CheckBox IsChecked="{Binding UseTrackColor}" />
Expand Down
8 changes: 8 additions & 0 deletions OpenUtau/Controls/PianoRoll.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ 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 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();
Expand Down
4 changes: 4 additions & 0 deletions OpenUtau/Strings/Strings.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ Warning: this option removes custom presets.</system:String>
<system:String x:Key="pianoroll.toggle.overwritepitch">Toggle Overwrite Pitch Mode</system:String>
<system:String x:Key="pianoroll.toggle.vibrato">View Vibrato (U)</system:String>
<system:String x:Key="pianoroll.toggle.waveform">View Waveform (W)</system:String>
<system:String x:Key="pianoroll.menu.view.playbackverticalfollow">Center Playback Note Vertically</system:String>
<system:String x:Key="pianoroll.menu.view.playbacknotehighlight">Highlight Notes On Playback</system:String>
<system:String x:Key="prefs.playback.notehighlight.fadein">Note highlight fade-in speed</system:String>
<system:String x:Key="prefs.playback.notehighlight.fadeout">Note highlight fade-out speed</system:String>
<system:String x:Key="pianoroll.toggle.expressions">View Expressions (L)</system:String>
<system:String x:Key="pianoroll.tool.drawpitch">Draw Pitch Tool (Shift + 1)
Left click to draw
Expand Down
Loading
Loading