diff --git a/OpenUtau/Controls/PianoRoll.axaml b/OpenUtau/Controls/PianoRoll.axaml index 647d22145..d8d72f942 100644 --- a/OpenUtau/Controls/PianoRoll.axaml +++ b/OpenUtau/Controls/PianoRoll.axaml @@ -609,10 +609,14 @@ - - - - + + + + + + + + View Vibrato (U) View Waveform (W) View Expressions (L) + Cursor Tool + Left click to select + Pen Tool + Left click to draw + Right click to reset + Line Tool + Left click to draw (draw straight line) + Right click to reset + Eraser Tool + Click to reset + Vertical Stretch Tool + Left click to stretch the curve vertically + Right click to reset + Horizontal Stretch Tool + Left click to stretch the curve horizontally + Right click to reset + Vertical Shift Tool + Left click to move the curve vertically + Right click to reset + Horizontal Shift Tool + Left click to move the curve horizontally + Right click to reset + Line Draw Pitch Tool (5) + Left click to draw (draw straight line) + Right click to reset + Hold Ctrl to select + Hold Alt to smoothen Draw Pitch Tool (Shift + 1) Left click to draw Right click to reset diff --git a/OpenUtau/Styles/PianoRollStyles.axaml b/OpenUtau/Styles/PianoRollStyles.axaml index 49f5b656d..1b497684a 100644 --- a/OpenUtau/Styles/PianoRollStyles.axaml +++ b/OpenUtau/Styles/PianoRollStyles.axaml @@ -122,6 +122,18 @@ + + + + diff --git a/OpenUtau/ViewModels/CurveViewModel.cs b/OpenUtau/ViewModels/CurveViewModel.cs index 9dd13082f..82da5f8c8 100644 --- a/OpenUtau/ViewModels/CurveViewModel.cs +++ b/OpenUtau/ViewModels/CurveViewModel.cs @@ -19,8 +19,12 @@ public CurveSelectionEvent(CurveSelection selection) { public enum CurveTools { CurveSelectTool, CurvePenTool, - //CurveLineTool, - CurveEraserTool + CurveLineTool, + CurveEraserTool, + CurveVerticalStretchTool, + CurveHorizontalStretchTool, + CurveVerticalShiftTool, + CurveHorizontalShiftTool } public class CurveViewModel : ViewModelBase, ICmdSubscriber { @@ -46,13 +50,16 @@ public void Select(UExpressionDescriptor descriptor, int startTick, int endTick, (int x, int y) endPoint = (endTick, curve?.Sample(endTick) ?? (int)descriptor.CustomDefaultValue); var xs = new List(); var ys = new List(); + int minTick = Math.Min(startTick, endTick); + int maxTick = Math.Max(startTick, endTick); + if (curve != null) { for (int i = 0; i < curve.xs.Count; i++) { var x = curve.xs[i]; - if (endTick < x) { + if (maxTick <= x) { break; } - if (startTick <= x) { + if (minTick < x) { xs.Add(x); ys.Add(curve.ys[i]); } @@ -105,6 +112,16 @@ public void Paste(UVoicePart part, UExpressionDescriptor descriptor) { DocManager.Inst.EndUndoGroup(); } + public bool TryGetSelection(string abbr, out CurveSelection selection) { + if (this.selection.HasValue(abbr)) { + selection = this.selection.Clone(); + return true; + } + + selection = new CurveSelection(); + return false; + } + public void OnNext(UCommand cmd, bool isUndo) { if (cmd is UNotification notif) { if (cmd is LoadPartNotification || cmd is LoadProjectNotification || cmd is SelectExpressionNotification) { diff --git a/OpenUtau/Views/NoteEditStates.cs b/OpenUtau/Views/NoteEditStates.cs index 98da786cc..b048bc95d 100644 --- a/OpenUtau/Views/NoteEditStates.cs +++ b/OpenUtau/Views/NoteEditStates.cs @@ -937,6 +937,511 @@ public override void Update(IPointer pointer, Point point) { vm.CurveViewModel.Select(descriptor, minTick, maxTick, curve); } } + class CurveVerticalShiftState : NoteEditState { + protected readonly UExpressionDescriptor descriptor; + protected CurveSelection? initialSelection; + protected string abbr = string.Empty; + + protected int[] oldXs = Array.Empty(); + protected int[] oldYs = Array.Empty(); + + protected override bool ShowValueTip => true; + protected override string? commandNameKey => "command.exp.edit"; + + public CurveVerticalShiftState( + Control control, + PianoRollViewModel vm, + IValueTip valueTip, + UExpressionDescriptor descriptor) : base(control, vm, valueTip) { + this.descriptor = descriptor; + } + + public override void Begin(IPointer pointer, Point point) { + base.Begin(pointer, point); + + var notesVm = vm.NotesViewModel; + var curveVm = vm.CurveViewModel; + + abbr = descriptor.abbr; + + if (!curveVm.TryGetSelection(abbr, out var selection)) { + initialSelection = null; + return; + } + + initialSelection = selection; + + var part = notesVm.Part; + var curve = part?.curves.FirstOrDefault(c => c.abbr == abbr); + + oldXs = curve?.xs.ToArray() ?? Array.Empty(); + oldYs = curve?.ys.ToArray() ?? Array.Empty(); + } + + public override void Update(IPointer pointer, Point point) { + if (CanEdit(out var part, out var project, out var curve)) { + BuildEditedCurve(point, out var finalXs, out var finalYs); + + if (!CurveEquals(oldXs, oldYs, finalXs, finalYs)) { + DocManager.Inst.ExecuteCmd(new MergedSetCurveCommand( + project, + part, + abbr, + oldXs, + oldYs, + finalXs.ToArray(), + finalYs.ToArray())); + } + } + } + + public override void End(IPointer pointer, Point point) { + base.End(pointer, point); + + if (initialSelection != null && initialSelection.HasValue(abbr)) { + var notesVm = vm.NotesViewModel; + var part = notesVm.Part; + + if (part != null) { + var curve = part.curves.FirstOrDefault(c => c.abbr == abbr); + if (curve != null) { + int startTick = initialSelection.StartPoint.x; + int endTick = initialSelection.EndPoint.x; + + vm.CurveViewModel.Select( + descriptor, + startTick, + endTick, + curve); + } + } + } + + initialSelection = null; + } + + protected virtual bool CanEdit(out UVoicePart part, out UProject project, out UCurve curve) { + part = null!; + project = null!; + curve = null!; + + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return false; + } + + var notesVm = vm.NotesViewModel; + if (notesVm.Part == null || notesVm.Project == null) { + return false; + } + + var targetCurve = notesVm.Part.curves.FirstOrDefault(c => c.abbr == abbr); + if (targetCurve == null) { + return false; + } + + part = notesVm.Part; + project = notesVm.Project; + curve = targetCurve; + return true; + } + + protected void BuildEditedCurve(Point point, out List newXs, out List newYs) { + newXs = oldXs.ToList(); + newYs = oldYs.ToList(); + + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return; + } + + initialSelection.GetSelectedRange(abbr, out var selectedXs, out var selectedYs); + + for (int i = 0; i < selectedYs.Count; i++) { + selectedYs[i] = TransformY(selectedXs[i], selectedYs[i], point); + } + + int minTick = Math.Min(initialSelection.StartPoint.x, initialSelection.EndPoint.x); + int maxTick = Math.Max(initialSelection.StartPoint.x, initialSelection.EndPoint.x); + + for (int i = newXs.Count - 1; i >= 0; i--) { + int x = newXs[i]; + if (minTick <= x && x <= maxTick) { + newXs.RemoveAt(i); + newYs.RemoveAt(i); + } + } + + for (int i = 0; i < selectedXs.Count; i++) { + InsertCurvePointSorted(selectedXs[i], selectedYs[i], newXs, newYs); + } + } + + protected virtual int TransformY(int x, int y, Point point) { + int deltaY = PointToCurveValue(point) - PointToCurveValue(startPoint); + valueTip.UpdateValueTip($"add:{deltaY:0}"); + return ClampY(y + deltaY); + } + + protected int PointToCurveValue(Point point) { + if (control.Bounds.Height <= 0) { + return ClampY(descriptor.CustomDefaultValue); + } + + return ClampY(Math.Round( + descriptor.min + (descriptor.max - descriptor.min) * (1 - point.Y / control.Bounds.Height))); + } + + protected int ClampY(double y) { + return (int)Math.Round(Math.Clamp(y, descriptor.min, descriptor.max)); + } + + protected static void InsertCurvePointSorted(int x, int y, List xs, List ys) { + for (int i = 0; i < xs.Count; i++) { + if (xs[i] == x) { + ys[i] = y; + return; + } + + if (x < xs[i]) { + xs.Insert(i, x); + ys.Insert(i, y); + return; + } + } + + xs.Add(x); + ys.Add(y); + } + + private static bool CurveEquals( + int[] oldXs, + int[] oldYs, + List newXs, + List newYs) { + if (oldXs.Length != newXs.Count || oldYs.Length != newYs.Count) { + return false; + } + + for (int i = 0; i < oldXs.Length; i++) { + if (oldXs[i] != newXs[i]) { + return false; + } + } + + for (int i = 0; i < oldYs.Length; i++) { + if (oldYs[i] != newYs[i]) { + return false; + } + } + + return true; + } + } + class CurveVerticalStretchState : CurveVerticalShiftState { + public CurveVerticalStretchState( + Control control, + PianoRollViewModel vm, + IValueTip valueTip, + UExpressionDescriptor descriptor) : base(control, vm, valueTip, descriptor) { + } + + protected override int TransformY(int x, int y, Point point) { + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return y; + } + + int deltaY = PointToCurveValue(point) - PointToCurveValue(startPoint); + double range = descriptor.max - descriptor.min; + if (range <= 0) { + return y; + } + + double scale = 1.0 + deltaY / range; + valueTip.UpdateValueTip($"scale:{scale:0.00}"); + + int centerY = GetSelectionCenterY(); + double stretchedY = Math.Round(centerY + (y - centerY) * scale); + + return ClampY(stretchedY); + } + + private int GetSelectionCenterY() { + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return ClampY(descriptor.CustomDefaultValue); + } + + initialSelection.GetSelectedRange(abbr, out _, out var ys); + + if (ys.Count == 0) { + return ClampY(descriptor.CustomDefaultValue); + } + + int minY = ys.Min(); + int maxY = ys.Max(); + return (int)(minY + maxY) / 2; + } + } + + class CurveHorizontalShiftState : NoteEditState { + protected readonly UExpressionDescriptor descriptor; + protected CurveSelection? initialSelection; + protected string abbr = string.Empty; + + protected int[] baseXs = Array.Empty(); + protected int[] baseYs = Array.Empty(); + + protected int lastStartTick; + protected int lastEndTick; + + protected override bool ShowValueTip => true; + protected override string? commandNameKey => "command.exp.edit"; + + public CurveHorizontalShiftState( + Control control, + PianoRollViewModel vm, + IValueTip valueTip, + UExpressionDescriptor descriptor) : base(control, vm, valueTip) { + this.descriptor = descriptor; + } + + public override void Begin(IPointer pointer, Point point) { + base.Begin(pointer, point); + + var notesVm = vm.NotesViewModel; + var curveVm = vm.CurveViewModel; + + abbr = descriptor.abbr; + + if (!curveVm.TryGetSelection(abbr, out var selection)) { + initialSelection = null; + return; + } + + initialSelection = selection; + + var part = notesVm.Part; + var curve = part?.curves.FirstOrDefault(c => c.abbr == abbr); + + baseXs = curve?.xs.ToArray() ?? Array.Empty(); + baseYs = curve?.ys.ToArray() ?? Array.Empty(); + + lastStartTick = initialSelection.StartPoint.x; + lastEndTick = initialSelection.EndPoint.x; + } + + public override void Update(IPointer pointer, Point point) { + if (CanEdit(out var project, out var part)) { + GetCurrentCurveArrays(part, out var oldXs, out var oldYs); + + BuildEditedCurve(point, out var newXs, out var newYs); + + if (!CurveEquals(oldXs, oldYs, newXs, newYs)) { + DocManager.Inst.ExecuteCmd(new MergedSetCurveCommand( + project, + part, + abbr, + oldXs, + oldYs, + newXs.ToArray(), + newYs.ToArray())); + } + } + } + + public override void End(IPointer pointer, Point point) { + base.End(pointer, point); + + if (initialSelection != null && initialSelection.HasValue(abbr)) { + var notesVm = vm.NotesViewModel; + var part = notesVm.Part; + + if (part != null) { + var curve = part.curves.FirstOrDefault(c => c.abbr == abbr); + if (curve != null) { + vm.CurveViewModel.Select( + descriptor, + lastStartTick, + lastEndTick, + curve); + } + } + } + + initialSelection = null; + } + + protected virtual bool CanEdit(out UProject project, out UVoicePart part) { + part = null!; + project = null!; + + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return false; + } + + var notesVm = vm.NotesViewModel; + if (notesVm.Project == null || notesVm.Part == null) { + return false; + } + + part = notesVm.Part; + project = notesVm.Project; + return true; + } + + protected void GetCurrentCurveArrays(UVoicePart part, out int[] xs, out int[] ys) { + var curve = part.curves.FirstOrDefault(c => c.abbr == abbr); + xs = curve?.xs.ToArray() ?? Array.Empty(); + ys = curve?.ys.ToArray() ?? Array.Empty(); + } + + protected void BuildEditedCurve(Point point, out List newXs, out List newYs) { + newXs = baseXs.ToList(); + newYs = baseYs.ToList(); + + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return; + } + + initialSelection.GetSelectedRange(abbr, out var selectedXs, out var selectedYs); + + int originalMinTick = Math.Min(initialSelection.StartPoint.x, initialSelection.EndPoint.x); + int originalMaxTick = Math.Max(initialSelection.StartPoint.x, initialSelection.EndPoint.x); + + int movedStartTick = TransformX(initialSelection.StartPoint.x, initialSelection.StartPoint.y, point); + int movedEndTick = TransformX(initialSelection.EndPoint.x, initialSelection.EndPoint.y, point); + + int movedMinTick = Math.Min(movedStartTick, movedEndTick); + int movedMaxTick = Math.Max(movedStartTick, movedEndTick); + + int removeMinTick = Math.Min(originalMinTick, movedMinTick); + int removeMaxTick = Math.Max(originalMaxTick, movedMaxTick); + + for (int i = newXs.Count - 1; i >= 0; i--) { + int x = newXs[i]; + if (removeMinTick <= x && x <= removeMaxTick) { + newXs.RemoveAt(i); + newYs.RemoveAt(i); + } + } + + for (int i = 0; i < selectedXs.Count; i++) { + InsertCurvePointSorted( + TransformX(selectedXs[i], selectedYs[i], point), + TransformY(selectedXs[i], selectedYs[i], point), + newXs, + newYs); + } + + lastStartTick = movedStartTick; + lastEndTick = movedEndTick; + } + + protected virtual int TransformX(int x, int y, Point point) { + int deltaTick = PointToTick(point) - PointToTick(startPoint); + return ClampTick(x + deltaTick); + } + + protected virtual int TransformY(int x, int y, Point point) { + return ClampY(y); + } + + protected int PointToTick(Point point) { + var notesVm = vm.NotesViewModel; + + int tick = notesVm.PointToTick(point); + if (notesVm.IsSnapOn) { + int snapUnit = notesVm.Project.resolution * 4 / notesVm.SnapDiv; + tick = (int)Math.Floor((double)tick / snapUnit) * snapUnit; + } + + return tick; + } + + protected int ClampTick(int tick) { + return Math.Max(0, tick); + } + + protected int ClampY(double y) { + return (int)Math.Round(Math.Clamp(y, descriptor.min, descriptor.max)); + } + + protected static void InsertCurvePointSorted(int x, int y, List xs, List ys) { + for (int i = 0; i < xs.Count; i++) { + if (xs[i] == x) { + ys[i] = y; + return; + } + + if (x < xs[i]) { + xs.Insert(i, x); + ys.Insert(i, y); + return; + } + } + + xs.Add(x); + ys.Add(y); + } + + protected static bool CurveEquals( + int[] oldXs, + int[] oldYs, + List newXs, + List newYs) { + if (oldXs.Length != newXs.Count || oldYs.Length != newYs.Count) { + return false; + } + + for (int i = 0; i < oldXs.Length; i++) { + if (oldXs[i] != newXs[i]) { + return false; + } + } + + for (int i = 0; i < oldYs.Length; i++) { + if (oldYs[i] != newYs[i]) { + return false; + } + } + + return true; + } + } + class CurveHorizontalStretchState : CurveHorizontalShiftState { + public CurveHorizontalStretchState( + Control control, + PianoRollViewModel vm, + IValueTip valueTip, + UExpressionDescriptor descriptor) : base(control, vm, valueTip, descriptor) { + } + + protected override int TransformX(int x, int y, Point point) { + if (initialSelection == null || !initialSelection.HasValue(abbr)) { + return x; + } + + int deltaTick = PointToTick(point) - PointToTick(startPoint); + + int startTick = initialSelection.StartPoint.x; + int endTick = initialSelection.EndPoint.x; + + int minTick = Math.Min(startTick, endTick); + int maxTick = Math.Max(startTick, endTick); + + int width = maxTick - minTick; + if (width <= 0) { + return x; + } + + double centerTick = (minTick + maxTick) / 2.0; + + double scale = 1.0 + (double)deltaTick / width; + scale = Math.Max(0.01, scale); + + int stretchedX = (int)Math.Round(centerTick + (x - centerTick) * scale); + + return ClampTick(stretchedX); + } + } class VibratoChangeStartState : NoteEditState { public readonly UNote note;