Skip to content
3 changes: 3 additions & 0 deletions OpenUtau.Core/PlaybackManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ public void PlayTestSound() {
}

public void PlayTone(double freq) {
if (!Core.Util.Preferences.Default.PlayTone) {
return;
}
toneGenerator.StartTone(freq);

// If nothing is playing, start editing mix
Expand Down
110 changes: 92 additions & 18 deletions OpenUtau/Controls/ExpressionCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class ExpressionCanvas : Control {
o => o.ShowRealCurve,
(o, v) => o.ShowRealCurve = v);

public static readonly DirectProperty<ExpressionCanvas, ExpDisMode> DisplayModeProperty =
AvaloniaProperty.RegisterDirect<ExpressionCanvas, ExpDisMode>(
nameof(DisplayMode), o => o.DisplayMode, (o, v) => o.DisplayMode = v);

public double TickWidth {
get => tickWidth;
private set => SetAndRaise(TickWidthProperty, ref tickWidth, value);
Expand All @@ -60,12 +64,18 @@ public bool ShowRealCurve {
get => showRealCurve;
set => SetAndRaise(ShowRealCurveProperty, ref showRealCurve, value);
}

public ExpDisMode DisplayMode {
get => displayMode;
set => SetAndRaise(DisplayModeProperty, ref displayMode, value);
}

private double tickWidth;
private double tickOffset;
private UVoicePart? part;
private string key = string.Empty;
private bool showRealCurve = true;
private ExpDisMode displayMode = ExpDisMode.Visible;

private HashSet<UNote> selectedNotes = new HashSet<UNote>();
private CurveSelection curveSelection = new CurveSelection();
Expand Down Expand Up @@ -99,6 +109,11 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang

public override void Render(DrawingContext context) {
base.Render(context);

// Skip rendering if hidden
if (DisplayMode == ExpDisMode.Hidden) {
return;
}
if (Part == null) {
return;
}
Expand All @@ -114,7 +129,11 @@ public override void Render(DrawingContext context) {
if (descriptor.max <= descriptor.min) {
return;
}
DrawBackgroundForHitTest(context);

if (DisplayMode != ExpDisMode.Shadow) {
DrawBackgroundForHitTest(context);
}

double leftTick = TickOffset - 480;
double rightTick = TickOffset + Bounds.Width / TickWidth + 480;
double optionHeight = descriptor.type == UExpressionType.Options
Expand All @@ -123,12 +142,14 @@ public override void Render(DrawingContext context) {
if (descriptor.type == UExpressionType.Curve) {
var curve = Part.curves.FirstOrDefault(c => c.descriptor == descriptor);
double defaultHeight = Math.Round(Bounds.Height - Bounds.Height * (descriptor.defaultValue - descriptor.min) / (descriptor.max - descriptor.min));
var lPen = ThemeManager.AccentPen1;
var lPen2 = ThemeManager.AccentPen1Thickness2;
var lPenSelected = ThemeManager.AccentPen2;
var lPen2Selected = ThemeManager.AccentPen2Thickness2;

var lPen = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentPen : ThemeManager.AccentPen1;
var lPen2 = DisplayMode == ExpDisMode.Shadow ? new Pen(ThemeManager.NeutralAccentBrush, 3) : ThemeManager.AccentPen1Thickness3;
var lPenSelected = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentPen : ThemeManager.AccentPen2;
var lPen2Selected = DisplayMode == ExpDisMode.Shadow ? new Pen(ThemeManager.NeutralAccentBrush, 3) : ThemeManager.AccentPen2Thickness3;
var lPen3 = new Pen(ThemeManager.NeutralAccentBrush, 1, new DashStyle(new double[] { 4, 4 }, 0));
var brush = ThemeManager.AccentBrush1;
var brush = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentBrush : ThemeManager.AccentBrush1;

double x3 = Math.Round(viewModel.TickToneToPoint(leftTick, 0).X);
double x4 = Math.Round(viewModel.TickToneToPoint(rightTick, 0).X);
context.DrawLine(lPen3, new Point(x3, defaultHeight), new Point(x4, defaultHeight));
Expand Down Expand Up @@ -161,6 +182,12 @@ public override void Render(DrawingContext context) {
index = -index - 1;
}
index = Math.Max(0, index) - 1;

// Create geometry elements for the custom curve fill
var fillGeometry = new PathGeometry();
var fillFigure = new PathFigure { IsClosed = true };
bool fillStarted = false;

while (index < xs.Count) {
float tick1 = index < 0 ? lTick : xs[index];
float value1 = index < 0 ? descriptor.defaultValue : ys[index];
Expand All @@ -170,6 +197,18 @@ public override void Render(DrawingContext context) {
float value2 = index == xs.Count - 1 ? descriptor.defaultValue : ys[index + 1];
double x2 = viewModel.TickToneToPoint(tick2, 0).X;
double y2 = defaultHeight - Bounds.Height * (value2 - descriptor.defaultValue) / (descriptor.max - descriptor.min);

if (!fillStarted) {
fillFigure.StartPoint = new Point(x1, defaultHeight);
fillFigure.Segments!.Add(new LineSegment { Point = new Point(x1, y1), IsStroked = false });
fillStarted = true;
}
fillFigure.Segments!.Add(new LineSegment { Point = new Point(x2, y2), IsStroked = false });

if (tick2 >= rTick || index == xs.Count - 1) {
fillFigure.Segments!.Add(new LineSegment { Point = new Point(x2, defaultHeight), IsStroked = false });
}

IPen pen;
if (curveSelection.HasValue(descriptor.abbr)) {
if (curveSelection.StartPoint.x <= tick1 && tick1 <= curveSelection.EndPoint.x
Expand All @@ -182,14 +221,19 @@ public override void Render(DrawingContext context) {
pen = value1 == descriptor.defaultValue && value2 == descriptor.defaultValue ? lPen : lPen2;
}
context.DrawLine(pen, new Point(x1, y1), new Point(x2, y2));
//using (var state = context.PushTransform(Matrix.CreateTranslation(x1, y1))) {
// context.DrawGeometry(brush, null, pointGeometry);
//}
index++;
if (tick2 >= rTick) {
break;
}
}

if (fillStarted) {
fillGeometry.Figures!.Add(fillFigure);
using (var state = context.PushOpacity(0.2)) {
context.DrawGeometry(brush, null, fillGeometry);
}
}

if (ShowRealCurve) {
int baseIndexL = curve.realXs.BinarySearch(lTick);
if (baseIndexL < 0) {
Expand All @@ -202,7 +246,6 @@ public override void Render(DrawingContext context) {
}
int offset = baseIndexL;
while (offset < baseIndexR) {
// negative values are breakpoints
int start = offset;
while (start < baseIndexR && curve.realYs[start] < 0) ++start;
int end = start;
Expand All @@ -213,23 +256,24 @@ public override void Render(DrawingContext context) {
}
var geometry = new PathGeometry();
var figure = new PathFigure {
IsClosed = false
IsClosed = true
};
for (int i = start; i < end; ++i) {
float tick = curve.realXs[i];
float value = curve.realYs[i];
double x = viewModel.TickToneToPoint(tick, 0).X;
double y = Bounds.Height * (1 - value / 1000.0);

if (i == start) {
figure.StartPoint = new Point(x, Bounds.Height);
figure.StartPoint = new Point(x, defaultHeight);
}
figure.Segments!.Add(new LineSegment {
Point = new Point(x, y),
IsStroked = i != start
});
if (i == end - 1) {
figure.Segments!.Add(new LineSegment {
Point = new Point(x, Bounds.Height),
Point = new Point(x, defaultHeight),
IsStroked = false
});
}
Expand All @@ -241,6 +285,16 @@ public override void Render(DrawingContext context) {
}
return;
}
if (descriptor.type == UExpressionType.Numerical) {
double p1 = Math.Round(viewModel.TickToneToPoint(leftTick, 0).X);
double p2 = Math.Round(viewModel.TickToneToPoint(rightTick, 0).X);
var dashedPen = new Pen(ThemeManager.NeutralAccentBrushSemi, 1, new DashStyle(new double[] { 4, 4 }, 0));
double defaultHeight = Math.Round(Bounds.Height - Bounds.Height * (descriptor.defaultValue - descriptor.min) / (descriptor.max - descriptor.min));
context.DrawLine(dashedPen, new Point(p1, defaultHeight), new Point(p2, defaultHeight));
}
var shadowHPen = new Pen(ThemeManager.NeutralAccentBrush, 3);
var shadowVPen = new Pen(ThemeManager.NeutralAccentBrush, 3);

foreach (var phoneme in Part.phonemes) {
if (phoneme.Error || phoneme.Parent == null) {
continue;
Expand All @@ -251,17 +305,36 @@ public override void Render(DrawingContext context) {
continue;
}
var note = phoneme.Parent;
var hPen = selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness2 : ThemeManager.AccentPen1Thickness2;
var vPen = selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3;
var brush = selectedNotes.Contains(note) ? ThemeManager.AccentBrush2 : ThemeManager.AccentBrush1;

var hPen = DisplayMode == ExpDisMode.Shadow ? shadowHPen : (selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3);
var vPen = DisplayMode == ExpDisMode.Shadow ? shadowVPen : (selectedNotes.Contains(note) ? ThemeManager.AccentPen2Thickness3 : ThemeManager.AccentPen1Thickness3);
var brush = DisplayMode == ExpDisMode.Shadow ? ThemeManager.NeutralAccentBrush : (selectedNotes.Contains(note) ? ThemeManager.AccentBrush2 : ThemeManager.AccentBrush1);

var (value, overriden) = phoneme.GetExpression(project, track, Key);
double x1 = Math.Round(viewModel.TickToneToPoint(phoneme.position, 0).X);
double x2 = Math.Round(viewModel.TickToneToPoint(phoneme.End, 0).X);

if (descriptor.type == UExpressionType.Numerical) {
double valueHeight = Math.Round(Bounds.Height - Bounds.Height * (value - descriptor.min) / (descriptor.max - descriptor.min));
double zeroHeight = Math.Round(Bounds.Height - Bounds.Height * (0f - descriptor.min) / (descriptor.max - descriptor.min));

double rectX = x1;
double rectY = Math.Min(zeroHeight, valueHeight);
double rectHeight = Math.Abs(zeroHeight - valueHeight);
double rectWidth = Math.Max(0, Math.Max(x1, x2) - rectX);
var fillRect = new Rect(rectX, rectY, rectWidth, rectHeight);

// Use 45% opacity if edited, 15% opacity if default
double fillOpacity = overriden ? 0.45 : 0.15;

using (var state = context.PushOpacity(fillOpacity)) {
context.DrawRectangle(brush, null, fillRect);
}

// Vertical and horizontal lines
context.DrawLine(vPen, new Point(x1 + 0.5, zeroHeight + 0.5), new Point(x1 + 0.5, valueHeight + 3));
context.DrawLine(hPen, new Point(x1 + 3, valueHeight), new Point(Math.Max(x1 + 3, x2 - 3), valueHeight));
context.DrawLine(hPen, new Point(x1 + 3, valueHeight), new Point(Math.Max(x1 + 3, x2), valueHeight));

using (var state = context.PushTransform(Matrix.CreateTranslation(x1 + 0.5, valueHeight))) {
context.DrawGeometry(overriden ? brush : ThemeManager.BackgroundBrush, vPen, pointGeometry);
}
Expand All @@ -281,7 +354,8 @@ public override void Render(DrawingContext context) {
}
}
}
if (descriptor.type == UExpressionType.Options) {

if (descriptor.type == UExpressionType.Options && DisplayMode != ExpDisMode.Shadow) {
for (int i = 0; i < descriptor.options.Length; ++i) {
string option = descriptor.options[i];
if (string.IsNullOrEmpty(option)) {
Expand Down
35 changes: 29 additions & 6 deletions OpenUtau/Controls/NotesCanvas.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,37 @@ 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);
bool hasError = note.Error;

// Check for Phoneme Errors (mimicking PhonemeCanvas behavior)
if (!hasError && Part != null && Part.phonemes != null) {
int phonemeCount = 0;
foreach (var p in Part.phonemes) {
if (p.Parent == note) {
phonemeCount++;
// If any attached phoneme has an error, the whole note is flagged
if (p.Error) {
hasError = true;
break;
}
}
}
// Edge Case: If the note is not a continuation/rest but generated 0 phonemes,
// it means the phonemizer completely failed to process the lyric.
if (!hasError && phonemeCount == 0 && !note.lyric.StartsWith("+") && !note.lyric.StartsWith("-")) {
hasError = true;
}
}
// apply the transparent/greyed-out brush if an error was found
var brush = selectedNotes.Contains(note)
? (note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2)
: (note.Error ? ThemeManager.AccentBrush1Semi : ThemeManager.AccentBrush1);
? (hasError ? ThemeManager.AccentBrush3Semi : ThemeManager.AccentBrush2)
: (hasError ? ThemeManager.NeutralAccentBrushSemi : ThemeManager.AccentBrush1);

context.DrawRectangle(brush, null, new Rect(leftTop, rightBottom), 2, 2);
if (TrackHeight < 10 || note.lyric.Length == 0) {
return;
}
// grey out the Phonemizer Transition Badges
if (ShowPhonemizerTags && TrackHeight >= 20) {
string currentOver = note.PhonemizerOverride ?? "";
bool isCurrentDefault = string.IsNullOrEmpty(currentOver) || currentOver.Equals("Default", StringComparison.OrdinalIgnoreCase);
Expand All @@ -240,13 +264,12 @@ private void RenderNoteBody(UNote note, NotesViewModel viewModel, DrawingContext
bool isTransition = !isContinuation && ((note.Prev == null && !isCurrentDefault) || (note.Prev != null && currentPh != prevPh));

if (isTransition) {
// Badge Background utilizes the same hasError flag
var badgeBrush = selectedNotes.Contains(note)
? (note.Error ? ThemeManager.AccentBrush2Semi : ThemeManager.AccentBrush2)
: (note.Error ? ThemeManager.AccentBrush1Semi : ThemeManager.AccentBrush1);
? (hasError ? ThemeManager.AccentBrush3Semi : ThemeManager.AccentBrush2)
: (hasError ? ThemeManager.NeutralAccentBrushSemi : ThemeManager.AccentBrush1);

if (isCurrentDefault) {
// Due to the limitation, we'll display a dot to inndicate
// the transition to default phonemizer instead of showing language tag
double boxWidth = 16;
double boxHeight = 16;
double dotRadius = 3;
Expand Down
Loading
Loading