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;