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
363 changes: 363 additions & 0 deletions OpenUtau.Core/Editing/NoteBatchEdits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,369 @@ public void Run(UProject project, UVoicePart part, List<UNote> 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<UNote, List<PitchPoint>> oldPitches = new();
Dictionary<UNote, List<PitchPoint>> newPitches = new();
Dictionary<UNote, SavedVibrato> oldVibratos = new();

UCurve pitdCurve;
List<int> oldCurveXs;
List<int> oldCurveYs;
List<int> newCurveXs;
List<int> newCurveYs;

UCurve dynCurve;
List<int> oldDynXs;
List<int> oldDynYs;
List<int> newDynXs;
List<int> newDynYs;

public TransferPitchToPitdCommand(UProject project, UVoicePart part, UCurve pitdCurve, UCurve dynCurve) : base(project, part) {
this.pitdCurve = pitdCurve;
this.oldCurveXs = new List<int>(pitdCurve.xs);
this.oldCurveYs = new List<int>(pitdCurve.ys);

this.dynCurve = dynCurve;
if (dynCurve != null) {
this.oldDynXs = new List<int>(dynCurve.xs);
this.oldDynYs = new List<int>(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<PitchPoint> {
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<int> pXs, List<int> pYs, List<int> dXs, List<int> 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<UNote> 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<int> tempXs = new List<int>(pitdCurve.xs);
List<int> tempYs = new List<int>(pitdCurve.ys);
List<int> tempDynXs = dynCurve != null ? new List<int>(dynCurve.xs) : null;
List<int> tempDynYs = dynCurve != null ? new List<int>(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<int> finalDynXs = null;
List<int> 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<PitchPoint> SimplifyDP(List<PitchPoint> 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<PitchPoint> { points.First(), points.Last() };
}
}

public void Run(UProject project, UVoicePart part, List<UNote> selectedNotes, DocManager docManager) {
var notes = selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList();
if (notes.Count == 0) return;

float toleranceEpsilon = 2f;

var pendingChanges = new Dictionary<UNote, List<PitchPoint>>();

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;

Expand Down
2 changes: 2 additions & 0 deletions OpenUtau/Controls/PianoRoll.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions OpenUtau/Strings/Strings.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,8 @@ Warning: this option removes custom presets.</system:String>
<system:String x:Key="pianoroll.menu.notes.addtailrest">Add tail "R"</system:String>
<system:String x:Key="pianoroll.menu.notes.autolegato">Auto Legato</system:String>
<system:String x:Key="pianoroll.menu.notes.bakepitch">Convert PITD to pitch control points</system:String>
<system:String x:Key="pianoroll.menu.notes.bakpointstopitd">Convert pitch control points to PITD</system:String>
<system:String x:Key="pianoroll.menu.notes.simplifypitchpoints">Simplify Pitch Points</system:String>
<system:String x:Key="pianoroll.menu.notes.clear.vibratos">Clear vibratos</system:String>
<system:String x:Key="pianoroll.menu.notes.commonnotecopy">Commonnote Copy</system:String>
<system:String x:Key="pianoroll.menu.notes.commonnotepaste">Commonnote Paste</system:String>
Expand Down
Loading