【发布时间】:2020-09-28 04:21:57
【问题描述】:
我正在尝试显示音频文件的波形。我希望在 ffmpeg 处理文件时逐步绘制波形,而不是在完成后立即绘制。虽然我已经达到了这个效果,但它真的很慢;像痛苦的慢。它开始时非常快,但速度会下降到需要几分钟才能抽取样本。
我觉得必须有一种方法可以更有效地做到这一点,因为我使用了一个程序来做到这一点,我只是不知道怎么做。另一个程序可以接收超过 10 小时的音频,并逐渐显示波形而不会降低速度。我已将 ffmpeg 设置为以 500 个样本/秒的速度处理文件,但其他程序以 1000 个/秒的速度采样,它的运行速度仍然比我写的要快。另一个程序的波形显示只需要大约 120MB 的 RAM 和 10 小时的文件,而我的需要 1.5GB 的 10 分钟文件。
我相当肯定缓慢是由所有 UI 更新引起的,而 RAM 使用来自所有正在创建的矩形对象。当我禁用绘制波形时,异步流完成得非常快; 10 小时的文件不到 1 分钟。
这是我能想到的实现我想要的唯一方法。我欢迎任何帮助以改进我所写的内容或任何关于以不同方式完成它的建议。
附带说明,这不是我想要展示的全部内容。我最终会想要添加一个背景网格来帮助判断时间,并添加可拖动的线注释来标记波形中的特定位置。
MainWindow.xaml
<ItemsControl x:Name="AudioDisplayItemsControl"
DockPanel.Dock="Top"
Height="100"
ItemsSource="{Binding Samples}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type poco:Sample}">
<Rectangle Width="{Binding Width}"
Height="{Binding Height}"
Fill="ForestGreen"/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="Black"
Width="500"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
MainWindow.xaml.cs
private string _audioFilePath;
public string AudioFilePath
{
get => _audioFilePath;
set
{
if (_audioFilePath != value)
{
_audioFilePath = value;
NotifyPropertyChanged();
}
}
}
private ObservableCollection<IShape> _samples;
public ObservableCollection<IShape> Samples
{
get => _samples;
set
{
if (_samples != value)
{
_samples = value;
NotifyPropertyChanged();
}
}
}
//Eventhandler that starts this whole process
private async void GetGetWaveform_Click(object sender, RoutedEventArgs e)
{
((Button)sender).IsEnabled = false;
await GetWaveformClickAsync();
((Button)sender).IsEnabled = true;
}
private async Task GetWaveformClickAsync()
{
Samples.Clear();
double left = 0;
double width = .01;
double top = 0;
double height = 0;
await foreach (var sample in FFmpeg.GetAudioWaveform(AudioFilePath).ConfigureAwait(false))
{
// Map {-32,768, 32,767} (pcm_16le) to {-50, 50} (height of sample display)
// I don't this this mapping is correct, but that's not important right now
height = ((sample + 32768) * 100 / 65535) - 50;
// "0" pcm values are not drawn in order to save on UI updates,
// but draw position is still advanced
if (height==0)
{
left += width;
continue;
}
// Positive pcm values stretch "height" above the canvas center line
if (height > 0)
top = 50 - height;
// Negative pcm values stretch "height" below the centerline
else
{
top = 50;
height = -height;
}
Samples.Add(new Sample
{
Height = height,
Width = width,
Top = top,
Left = left,
ZIndex = 1
});
left += width;
}
}
用于定义样本的类
public interface IShape
{
double Top { get; set; }
double Left { get; set; }
}
public abstract class Shape : IShape
{
public double Top { get; set; }
public double Left { get; set; }
public int ZIndex { get; set; }
}
public class Sample : Shape
{
public double Width { get; set; }
public double Height { get; set; }
}
FFMpeg.cs
public static class FFmpeg
{
public static async IAsyncEnumerable<short> GetAudioWaveform(string filename)
{
var args = GetFFmpegArgs(FFmpegTasks.GetWaveform, filename);
await foreach (var sample in RunFFmpegAsyncStream(args))
{
yield return sample;
}
}
/// <summary>
/// Streams raw results of running ffmpeg.exe with given arguments string
/// </summary>
/// <param name="args">CLI argument string used for ffmpeg.exe</param>
private static async IAsyncEnumerable<short> RunFFmpegAsyncStream(string args)
{
using (var process = new Process())
{
process.StartInfo.FileName = @"External\ffmpeg\bin\x64\ffmpeg.exe";
process.StartInfo.Arguments = args;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.BeginErrorReadLine();
var buffer = new byte[2];
while (true)
{
// Asynchronously read a pcm16_le value from ffmpeg.exe output
var r = await process.StandardOutput.BaseStream.ReadAsync(buffer, 0, 2);
if (r == 0)
break;
yield return BitConverter.ToInt16(buffer);
}
}
}
FFmpegTasks 只是一个枚举。
GetFFmpegArgs 使用 FFmpegTasks 上的 switch 参数为 ffmpeg.exe 返回适当的 CLI 参数。
我尝试使用以下类而不是标准的 ObservableCollection,因为我希望减少 UI 更新会加快速度,但实际上它会使绘制波形变慢。
RangeObservableCollection.cs
public class RangeObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
public void AddRange(IEnumerable<T> list)
{
if (list == null)
throw new ArgumentNullException("list");
_suppressNotification = true;
foreach (T item in list)
{
Add(item);
}
_suppressNotification = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
【问题讨论】:
-
看起来您绘制的数据过多。您应该进行实时绘图,仅绘制新数据以填充视口而不是完整集,例如,仅绘制一组最后 1000 个数据值。删除旧项目,然后添加新项目。将源集合实现为队列(或堆栈或环形缓冲区)。如果这仍然没有帮助,您需要为
Canvas面板实现虚拟化。 -
解决方案是只渲染可见数据,丢弃超出视口范围的数据。
-
你会在那里得到很多矩形。即使每个本身都很小,它加起来也很快。如果你把它做成一个列表框,那么你可以使用 UI 虚拟化。假设这是一个可行的选择。如果你需要一切,那么你需要一种不同的技术。从未尝试过比较,但我听说 writeablebitmap 可用于非常动态的线条,如声波。您在图片中写线或其他内容,但重建即使是相当大的图片也非常快。在我们的地图中 1155 x 805 的 Hypsometric 计算和 writeablebitmap 花了 20 毫秒 iirc。
-
@BionicCode 不幸的是,我希望能够一次显示整个波形,可能值 10 小时;我认为队列/堆栈的想法不会有帮助。我想只在缩小显示以显示 while 波形时显示更多间歇性数据点,并在开始放大时添加更多。我仍然需要将它们全部存储起来,2500 万条短路仍然是记忆猪。我将研究这种虚拟化;这是我不熟悉的东西。感谢您的领导。
-
@Andy Ill 查看可写位图,谢谢。