diff --git a/OpenUtau.Core/Editing/NoteBatchEdits.cs b/OpenUtau.Core/Editing/NoteBatchEdits.cs index 0bdf64bb4..cc99813e6 100644 --- a/OpenUtau.Core/Editing/NoteBatchEdits.cs +++ b/OpenUtau.Core/Editing/NoteBatchEdits.cs @@ -770,6 +770,369 @@ public void Run(UProject project, UVoicePart part, List selectedNotes, Do } } + public class BakePointsToPITD : BatchEdit { + public virtual string Name => name; + private string name; + public BakePointsToPITD() { + name = "pianoroll.menu.notes.bakpointstopitd"; + } + + public struct SavedVibrato { + public float length, period, depth, @in, @out, shift, drift, volLink; + } + + public class TransferPitchToPitdCommand : PartCommand { + Dictionary> oldPitches = new(); + Dictionary> newPitches = new(); + Dictionary oldVibratos = new(); + + UCurve pitdCurve; + List oldCurveXs; + List oldCurveYs; + List newCurveXs; + List newCurveYs; + + UCurve dynCurve; + List oldDynXs; + List oldDynYs; + List newDynXs; + List newDynYs; + + public TransferPitchToPitdCommand(UProject project, UVoicePart part, UCurve pitdCurve, UCurve dynCurve) : base(project, part) { + this.pitdCurve = pitdCurve; + this.oldCurveXs = new List(pitdCurve.xs); + this.oldCurveYs = new List(pitdCurve.ys); + + this.dynCurve = dynCurve; + if (dynCurve != null) { + this.oldDynXs = new List(dynCurve.xs); + this.oldDynYs = new List(dynCurve.ys); + } + } + public void AddNoteState(UNote note) { + oldPitches[note] = note.pitch.data.Select(p => new PitchPoint(p.X, p.Y, p.shape)).ToList(); + + newPitches[note] = new List { + new PitchPoint(-25f, 0f, PitchPointShape.io), + new PitchPoint(0f, 0f, PitchPointShape.io) + }; + + var v = note.vibrato; + oldVibratos[note] = new SavedVibrato { + length = v.length, period = v.period, depth = v.depth, + @in = v.@in, @out = v.@out, shift = v.shift, drift = v.drift, volLink = v.volLink + }; + } + public void SetNewCurveData(List pXs, List pYs, List dXs, List dYs) { + newCurveXs = pXs; + newCurveYs = pYs; + newDynXs = dXs; + newDynYs = dYs; + } + public override void Execute() { + foreach (var kvp in newPitches) { + kvp.Key.pitch.data.Clear(); + kvp.Key.pitch.data.AddRange(kvp.Value); + kvp.Key.vibrato.length = 0; + kvp.Key.vibrato.depth = 0; + kvp.Key.vibrato.volLink = 0; + } + + pitdCurve.xs.Clear(); + pitdCurve.ys.Clear(); + pitdCurve.xs.AddRange(newCurveXs); + pitdCurve.ys.AddRange(newCurveYs); + + if (dynCurve != null && newDynXs != null) { + dynCurve.xs.Clear(); + dynCurve.ys.Clear(); + dynCurve.xs.AddRange(newDynXs); + dynCurve.ys.AddRange(newDynYs); + } + } + public override void Unexecute() { + foreach (var kvp in oldPitches) { + kvp.Key.pitch.data.Clear(); + kvp.Key.pitch.data.AddRange(kvp.Value); + } + foreach (var kvp in oldVibratos) { + var target = kvp.Key.vibrato; + var src = kvp.Value; + target.length = src.length; + target.period = src.period; + target.depth = src.depth; + target.@in = src.@in; + target.@out = src.@out; + target.shift = src.shift; + target.drift = src.drift; + target.volLink = src.volLink; + } + + pitdCurve.xs.Clear(); + pitdCurve.ys.Clear(); + pitdCurve.xs.AddRange(oldCurveXs); + pitdCurve.ys.AddRange(oldCurveYs); + + if (dynCurve != null && oldDynXs != null) { + dynCurve.xs.Clear(); + dynCurve.ys.Clear(); + dynCurve.xs.AddRange(oldDynXs); + dynCurve.ys.AddRange(oldDynYs); + } + } + public override string ToString() => "pianoroll.menu.notes.bakpointstopitd"; + } + + private double GetShapeWeight(double t, PitchPointShape shape) { + switch (shape) { + case PitchPointShape.i: return 1.0 - Math.Cos(t * Math.PI / 2.0); + case PitchPointShape.o: return Math.Sin(t * Math.PI / 2.0); + case PitchPointShape.io: return 0.5 * (1.0 - Math.Cos(t * Math.PI)); + case PitchPointShape.l: default: return t; + } + } + + public void Run(UProject project, UVoicePart part, List selectedNotes, DocManager docManager) { + var notes = selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList(); + if (notes.Count == 0) return; + + var pitdCurve = part.curves.FirstOrDefault(c => c.abbr == "pitd"); + if (pitdCurve == null) { + if (project.expressions.TryGetValue("pitd", out var pitdDesc)) { + pitdCurve = new UCurve(pitdDesc); + part.curves.Add(pitdCurve); + } else { + return; + } + } + + var dynCurve = part.curves.FirstOrDefault(c => c.abbr == "dyn"); + if (dynCurve == null) { + if (project.expressions.TryGetValue("dyn", out var dynDesc)) { + dynCurve = new UCurve(dynDesc); + part.curves.Add(dynCurve); + } + } + + var command = new TransferPitchToPitdCommand(project, part, pitdCurve, dynCurve); + + List tempXs = new List(pitdCurve.xs); + List tempYs = new List(pitdCurve.ys); + List tempDynXs = dynCurve != null ? new List(dynCurve.xs) : null; + List tempDynYs = dynCurve != null ? new List(dynCurve.ys) : null; + + int sampleInterval = 1; + + foreach (var note in notes) { + command.AddNoteState(note); + + double noteStartMs = project.timeAxis.TickPosToMsPos(note.position); + double noteEndMs = project.timeAxis.TickPosToMsPos(note.position + note.duration); + double noteMs = noteEndMs - noteStartMs; + + float minMs = -40f; + float maxMs = (float)noteMs + 40f; + + if (note.pitch.data.Count > 0) { + minMs = Math.Min(minMs, note.pitch.data.Min(p => p.X)); + maxMs = Math.Max(maxMs, note.pitch.data.Max(p => p.X)); + } + + int startTick = project.timeAxis.MsPosToTickPos(noteStartMs + minMs); + int endTick = project.timeAxis.MsPosToTickPos(noteStartMs + maxMs); + + double vLengthMs = noteMs * (note.vibrato.length / 100.0); + double vStartMs = noteMs - vLengthMs; + bool hasVolLink = dynCurve != null && note.vibrato.length > 0 && note.vibrato.volLink != 0; + + for (int i = tempXs.Count - 1; i >= 0; i--) { + if (tempXs[i] >= startTick && tempXs[i] <= endTick) { + tempXs.RemoveAt(i); + tempYs.RemoveAt(i); + } + } + + if (hasVolLink) { + for (int i = tempDynXs.Count - 1; i >= 0; i--) { + if (tempDynXs[i] >= startTick && tempDynXs[i] <= endTick) { + tempDynXs.RemoveAt(i); + tempDynYs.RemoveAt(i); + } + } + } + + for (int t = startTick; t <= endTick; t += sampleInterval) { + double currentMs = project.timeAxis.TickPosToMsPos(t) - noteStartMs; + + float currentY = 0; + if (note.pitch.data.Count > 1) { + var sortedPitches = note.pitch.data.OrderBy(p => p.X).ToList(); + if (currentMs <= sortedPitches.First().X) { + currentY = sortedPitches.First().Y; + } else if (currentMs >= sortedPitches.Last().X) { + currentY = sortedPitches.Last().Y; + } else { + for (int i = 0; i < sortedPitches.Count - 1; i++) { + var p1 = sortedPitches[i]; + var p2 = sortedPitches[i + 1]; + if (currentMs >= p1.X && currentMs <= p2.X) { + double ratio = (currentMs - p1.X) / (p2.X - p1.X); + currentY = (float)(p1.Y + (p2.Y - p1.Y) * GetShapeWeight(ratio, p1.shape)); + break; + } + } + } + } + + float pitchCents = currentY * 10f; + if (currentMs < 0) { + var prevNote = part.notes.LastOrDefault(n => n.position < note.position); + if (prevNote != null) { + float diffCents = (note.tone - prevNote.tone) * 100f; + if (currentMs <= -25) { + pitchCents += diffCents; + } else { + double ratio = (currentMs - (-25)) / 25.0; + double weight = 0.5 * (1.0 - Math.Cos(ratio * Math.PI)); + pitchCents += diffCents * (float)(1.0 - weight); + } + } + } + + float vibratoCents = 0; + float dynVibrato = 0; + if (note.vibrato.length > 0 && note.vibrato.period > 0 && currentMs >= vStartMs && currentMs <= noteMs) { + double vt = currentMs - vStartMs; + double env = 1.0; + double inMs = vLengthMs * (note.vibrato.@in / 100.0); + double outMs = vLengthMs * (note.vibrato.@out / 100.0); + + if (inMs > 0 && vt < inMs) env = vt / inMs; + else if (outMs > 0 && vt > vLengthMs - outMs) env = (vLengthMs - vt) / outMs; + + double phaseOffset = 2 * Math.PI * (note.vibrato.shift / 100.0); + double pureSine = env * Math.Sin(2 * Math.PI * (vt / note.vibrato.period) + phaseOffset); + vibratoCents = (float)(note.vibrato.depth * pureSine + env * note.vibrato.depth * (note.vibrato.drift / 100.0)); + if (hasVolLink) dynVibrato = (float)(note.vibrato.volLink * pureSine * 0.6); + } + + tempXs.Add(t); + tempYs.Add((int)Math.Round(pitchCents + vibratoCents)); + if (hasVolLink) { + tempDynXs.Add(t); + tempDynYs.Add((int)Math.Round(dynCurve.Sample(t) + dynVibrato)); + } + } + } + + var sortedPitd = tempXs.Zip(tempYs, (x, y) => new { x, y }).OrderBy(item => item.x).DistinctBy(i => i.x).ToList(); + List finalDynXs = null; + List finalDynYs = null; + if (tempDynXs != null) { + var sortedDyn = tempDynXs.Zip(tempDynYs, (x, y) => new { x, y }).OrderBy(item => item.x).DistinctBy(i => i.x).ToList(); + finalDynXs = sortedDyn.Select(i => i.x).ToList(); + finalDynYs = sortedDyn.Select(i => i.y).ToList(); + } + + command.SetNewCurveData(sortedPitd.Select(i => i.x).ToList(), sortedPitd.Select(i => i.y).ToList(), finalDynXs, finalDynYs); + + docManager.StartUndoGroup("command.batch.transfer_pitch_to_pitd", true); + docManager.ExecuteCmd(command); + docManager.EndUndoGroup(); + } + } + + public class SimplifyPitchPoints : BatchEdit { + public virtual string Name => name; + private string name; + public SimplifyPitchPoints() { + name = "pianoroll.menu.notes.simplifypitchpoints"; + } + + private List SimplifyDP(List points, float epsilon) { + if (points.Count <= 2) return points; + + double maxError = 0; + int maxIndex = 0; + + float x1 = points.First().X; + float y1 = points.First().Y; + float x2 = points.Last().X; + float y2 = points.Last().Y; + + for (int i = 1; i < points.Count - 1; i++) { + float x0 = points[i].X; + float y0 = points[i].Y; + + float yInt; + if (x2 == x1) { + yInt = y1; + } else { + yInt = y1 + (x0 - x1) * (y2 - y1) / (x2 - x1); + } + + double error = Math.Abs(y0 - yInt); + if (error > maxError) { + maxError = error; + maxIndex = i; + } + } + + if (maxError > epsilon) { + var left = SimplifyDP(points.GetRange(0, maxIndex + 1), epsilon); + var right = SimplifyDP(points.GetRange(maxIndex, points.Count - maxIndex), epsilon); + + left.RemoveAt(left.Count - 1); + left.AddRange(right); + return left; + } else { + return new List { points.First(), points.Last() }; + } + } + + public void Run(UProject project, UVoicePart part, List selectedNotes, DocManager docManager) { + var notes = selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList(); + if (notes.Count == 0) return; + + float toleranceEpsilon = 2f; + + var pendingChanges = new Dictionary>(); + + foreach (var note in notes) { + if (note.pitch.data.Count > 2) { + var newPoints = SimplifyDP(note.pitch.data, toleranceEpsilon); + + if (newPoints.Count < note.pitch.data.Count) { + pendingChanges[note] = newPoints; + } + } + } + + if (pendingChanges.Count > 0) { + docManager.StartUndoGroup("command.batch.simplify_pitch", true); + + foreach (var kvp in pendingChanges) { + var note = kvp.Key; + var newPoints = kvp.Value; + + docManager.ExecuteCmd(new ResetPitchPointsCommand(part, note)); + + int index = 0; + foreach (var point in newPoints) { + var clonedPoint = new PitchPoint(point.X, point.Y, PitchPointShape.io); + docManager.ExecuteCmd(new AddPitchPointCommand(part, note, clonedPoint, index)); + index++; + } + + docManager.ExecuteCmd(new DeletePitchPointCommand(part, note, index)); + docManager.ExecuteCmd(new DeletePitchPointCommand(part, note, index)); + } + + docManager.EndUndoGroup(); + } + } + } + public class RefreshRealCurves : BatchEdit { public virtual string Name => name; diff --git a/OpenUtau/Controls/PianoRoll.axaml.cs b/OpenUtau/Controls/PianoRoll.axaml.cs index 0d4b16b32..a894f18d6 100644 --- a/OpenUtau/Controls/PianoRoll.axaml.cs +++ b/OpenUtau/Controls/PianoRoll.axaml.cs @@ -127,6 +127,8 @@ await MessageBox.ShowProcessing(RootWindow, $"{name} - ? / ?", new CommonnotePaste(), new FixOverlap(), new BakePitch(), + new BakePointsToPITD(), + new SimplifyPitchPoints(), new RandomizeTiming(), new RandomizePhonemeOffset() }.Select(edit => new MenuItemViewModel() { diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index 0a38e9f4d..68076d528 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -463,6 +463,8 @@ Warning: this option removes custom presets. Add tail "R" Auto Legato Convert PITD to pitch control points + Convert pitch control points to PITD + Simplify Pitch Points Clear vibratos Commonnote Copy Commonnote Paste