Skip to content
Merged
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
38 changes: 34 additions & 4 deletions src/ui/Controls/AudioVisualizerControl/AudioVisualizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ public Color WaveformColor
set
{
_paintWaveform = new Pen(new SolidColorBrush(value), 1);
ResetFancyColorCaches();
SetValue(WaveformColorProperty, value);
}
}
Expand All @@ -149,6 +150,7 @@ public Color WaveformSelectedColor
set
{
_paintPenSelected = new Pen(new SolidColorBrush(value), 1);
ResetFancyColorCaches();
SetValue(WaveformSelectedColorProperty, value);
}
}
Expand Down Expand Up @@ -217,7 +219,17 @@ public Color WaveformParagraphRightColor
// "center video position" mode can turn into a seek that keeps the play-head centered
// (#12864). Hosts without a video player (or that never set this) keep plain scrolling.
public Func<bool>? GetIsVideoPlaying { get; set; }
public Color WaveformFancyHighColor { get; set; } = Colors.Orange;
private Color _waveformFancyHighColor = Colors.Orange;

public Color WaveformFancyHighColor
{
get => _waveformFancyHighColor;
set
{
_waveformFancyHighColor = value;
ResetFancyColorCaches();
}
}

private Color _paragraphBackground = Color.FromArgb(140, 70, 70, 70);

Expand Down Expand Up @@ -2597,6 +2609,26 @@ private void DrawClassicSelectionOverlay(DrawingContext context, ref RenderConte
// Pooled buffer for DrawClassicSelectionOverlay's visible selected regions.
private readonly List<(double Left, double Right)> _selectionOverlayIntervals = new(16);

/// <summary>
/// Drops every fancy-style cache that has a waveform color baked into it. The pen/gradient/glow
/// caches - and the pooled per-color-key batches, which keep a pen of their own - are keyed on
/// the quantized amplitude bucket, not on the color, so a color change leaves them holding pens
/// painted in the old color. Missing the batches here is what made a new waveform/selected/fancy
/// high color only show up after a restart (#13897).
/// </summary>
private void ResetFancyColorCaches()
{
_fancyWaveformPenCache.Clear();
_fancyWaveformGlowPenCache.Clear();
_fancyWaveformGradientCache.Clear();
_fancyBatches.Clear();
_fancyBatchKeysInUse.Clear();

// The color properties are not AffectsRender, so ask for the repaint that shows the new
// color instead of waiting for whatever moves the waveform next.
InvalidateVisual();
}

private Pen GetCachedFancyWaveformPen(int colorKey, Color color)
{
if (!_fancyWaveformPenCache.TryGetValue(colorKey, out var pen))
Expand Down Expand Up @@ -4229,9 +4261,7 @@ internal WaveformDisplayMode GetDisplayMode()

internal void ResetCache()
{
_fancyWaveformPenCache.Clear();
_fancyWaveformGlowPenCache.Clear();
_fancyWaveformGradientCache.Clear();
ResetFancyColorCaches();
_timeLineTextCache.Clear();
_paragraphFormattedTextCache.Clear();
_paragraphTextCache.Clear();
Expand Down
170 changes: 170 additions & 0 deletions tests/UI/Controls/AudioVisualizerColorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using Avalonia;
using Avalonia.Headless.XUnit;
using Avalonia.Media;
using Nikse.SubtitleEdit.Controls.AudioVisualizerControl;
using Nikse.SubtitleEdit.Features.Main;
using Nikse.SubtitleEdit.Logic.Media;
using System;
using System.Collections.Generic;
using System.Reflection;
using Xunit;

namespace UITests.Controls;

/// <summary>
/// Waveform color changes must take effect without restarting SE (#13897).
///
/// The fancy draw style batches columns by a quantized amplitude bucket and caches a pen per
/// bucket - both in the pen/gradient caches and in the pooled batch dictionary. None of those keys
/// carry the color, so every one of them has to be dropped when a waveform color changes,
/// otherwise the rebuilt geometry is stroked with pens still painted in the old color and only a
/// new control (i.e. a restart) shows the new one.
/// </summary>
public class AudioVisualizerColorTests
{
private const int SampleRate = 126; // Se.Settings.Waveform.WaveformMinimumSampleRate default
private const double WidthPx = 800;
private const double HeightPx = 200;

private static readonly Color OldColor = Color.FromRgb(0, 70, 0);
private static readonly Color NewColor = Color.FromRgb(255, 0, 255);

/// <summary>Mostly quiet peaks (drawn in the base waveform color) with an occasional loud one
/// so the highest peak - and with it the low/medium/high color thresholds - is well above the
/// quiet columns.</summary>
private static WavePeakData2 MakePeaks(int seconds)
{
var peaks = new WavePeak2[SampleRate * seconds];
for (var i = 0; i < peaks.Length; i++)
{
var v = i % 500 == 0 ? (short)8000 : (short)500;
peaks[i] = new WavePeak2(v, (short)-v);
}

return new WavePeakData2(SampleRate, peaks);
}

private static AudioVisualizer MakeMeasuredFancyVisualizer()
{
var av = new AudioVisualizer
{
WaveformDrawStyle = WaveformDrawStyle.Fancy,
WavePeaks = MakePeaks(60),
WaveformColor = OldColor,
WaveformSelectedColor = Color.FromArgb(150, 0, 120, 255),
WaveformFancyHighColor = Colors.Orange,
};
av.Measure(new Size(WidthPx, HeightPx));
av.Arrange(new Rect(0, 0, WidthPx, HeightPx));
av.SetPosition(0, new List<SubtitleLineViewModel>(), 0, 0, new List<SubtitleLineViewModel>());
return av;
}

private static void RenderFrame(AudioVisualizer av)
{
var drawingGroup = new DrawingGroup();
using var context = drawingGroup.Open();
av.Render(context);
}

/// <summary>Every color the cached waveform draw ops would actually paint with.</summary>
private static List<Color> CachedPenColors(AudioVisualizer av)
{
var field = typeof(AudioVisualizer).GetField("_waveformCacheDraws", BindingFlags.NonPublic | BindingFlags.Instance)!;
var list = (System.Collections.IList)field.GetValue(av)!;
var colors = new List<Color>();
foreach (var draw in list)
{
var (pen, _) = (ValueTuple<IPen, Geometry>)draw!;
switch (pen.Brush)
{
case IGradientBrush gradient:
foreach (var stop in gradient.GradientStops)
{
colors.Add(stop.Color);
}

break;
case ISolidColorBrush solid:
colors.Add(solid.Color);
break;
}
}

return colors;
}

[AvaloniaFact]
public void FancyWaveform_UsesNewWaveformColor_AfterSettingsApply_Issue13897()
{
var av = MakeMeasuredFancyVisualizer();
RenderFrame(av);
Assert.Contains(OldColor, CachedPenColors(av));

// What Settings -> OK does: push the new colors, then reset the caches.
av.WaveformColor = NewColor;
av.ResetCache();
RenderFrame(av);

var colors = CachedPenColors(av);
Assert.DoesNotContain(OldColor, colors);
Assert.Contains(NewColor, colors);
}

[AvaloniaFact]
public void FancyWaveform_UsesNewWaveformColor_WithoutResetCache_Issue13897()
{
// The setter alone must be enough - no caller should have to remember ResetCache().
var av = MakeMeasuredFancyVisualizer();
RenderFrame(av);
Assert.Contains(OldColor, CachedPenColors(av));

av.WaveformColor = NewColor;
RenderFrame(av);

var colors = CachedPenColors(av);
Assert.DoesNotContain(OldColor, colors);
Assert.Contains(NewColor, colors);
}

[AvaloniaFact]
public void FancyWaveform_UsesNewSelectedColor_Issue13897()
{
var av = MakeMeasuredFancyVisualizer();
var selected = new SubtitleLineViewModel
{
Text = "text",
StartTime = TimeSpan.Zero,
EndTime = TimeSpan.FromSeconds(5),
};
var lines = new List<SubtitleLineViewModel> { selected };
av.SetPosition(0, lines, 0, 0, lines);

var oldSelectedColor = Color.FromRgb(0, 120, 255);
av.WaveformSelectedColor = oldSelectedColor;
RenderFrame(av);
Assert.Contains(oldSelectedColor, CachedPenColors(av));

av.WaveformSelectedColor = NewColor;
RenderFrame(av);

var colors = CachedPenColors(av);
Assert.DoesNotContain(oldSelectedColor, colors);
Assert.Contains(NewColor, colors);
}

[AvaloniaFact]
public void FancyWaveform_UsesNewFancyHighColor_Issue13897()
{
var av = MakeMeasuredFancyVisualizer();
RenderFrame(av);
Assert.Contains(Colors.Orange, CachedPenColors(av));

av.WaveformFancyHighColor = NewColor;
RenderFrame(av);

var colors = CachedPenColors(av);
Assert.DoesNotContain(Colors.Orange, colors);
Assert.Contains(NewColor, colors);
}
}