Displays a stereo waveform and allows a user to change playback position.
Property
| Name |
|---|
| LeftLevelBrush |
| RightLevelBrush |
| ProgressBarBrush |
| ProgressBarThickness |
| CenterLineBrush |
| CenterLineThickness |
| RepeatRegionBrush |
| AllowRepeatRegions |
| TimelineTickBrush |
Methods
| Name |
|---|
| RegisterSoundPlayer |
Virtual Methods (override)
| Name |
|---|
| OnLeftLevelBrushChanged |
| OnRightLevelBrushChanged |
| OnProgressBarBrushChanged |
| OnProgressBarThicknessChanged |
| OnCenterLineBrushChanged |
| OnCenterLineThicknessChanged |
| OnRepeatRegionBrushChanged |
| OnAllowRepeatRegionsChanged |
| OnTimelineTickBrushChanged |
Example
xml
<dev:WaveformTimeline x:Name="WaveformTimelineSample" />WARNING
it is best to use NAudio library to generate data, however NAudio does not support AOT.
AudioGraphEngine
cs
soundEngine = new AudioGraphEngine();
WaveformTimelineSample.RegisterSoundPlayer(soundEngine);
await soundEngine.OpenFileAsync(@"C:\myAudio.mp3");
soundEngine.Play();WARNING
GenerateWaveformAsync only support simple Wav format.
cs
[GeneratedComInterface]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
public unsafe partial interface IMemoryBufferByteAccess
{
void GetBuffer(out byte* buffer, out uint capacity);
}
public sealed partial class AudioGraphEngine : ISpectrumPlayer, IWaveformPlayer, INotifyPropertyChanged, IDisposable
{
private readonly DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private AudioGraph graph;
private AudioFileInputNode fileNode;
private AudioDeviceOutputNode deviceNode;
private AudioFrameOutputNode frameNode;
private readonly int fftDataSize = 2048;
private readonly SampleAggregator sampleAggregator;
private bool isPlaying;
private double channelLength;
private double channelPosition;
public event PropertyChangedEventHandler PropertyChanged;
private AudioSampleRingBuffer sampleBuffer;
private CancellationTokenSource processingCts;
private TimeSpan playbackStartTime;
public TimeSpan SelectionBegin { get; set; }
public TimeSpan SelectionEnd { get; set; }
public AudioGraphEngine()
{
sampleAggregator = new SampleAggregator(fftDataSize);
sampleBuffer = new AudioSampleRingBuffer(fftDataSize * 8);
processingCts = new CancellationTokenSource();
StartProcessingLoop();
}
private void StartProcessingLoop()
{
Task.Run(async () =>
{
float left, right;
while (!processingCts.IsCancellationRequested)
{
if (sampleBuffer.TryRead(out left) &&
sampleBuffer.TryRead(out right))
{
sampleAggregator.Add(left, right);
}
else
{
await Task.Delay(1);
}
}
}, processingCts.Token);
}
public bool GetFFTData(float[] fftDataBuffer)
{
sampleAggregator.GetFFTResults(fftDataBuffer);
return IsPlaying;
}
public int GetFFTFrequencyIndex(int frequency)
{
double maxFrequency = graph != null
? graph.EncodingProperties.SampleRate / 2.0
: 22050;
return (int)((frequency / maxFrequency) * (fftDataSize / 2));
}
public double ChannelPosition
{
get => channelPosition;
set
{
if (fileNode == null)
return;
value = Math.Max(0, Math.Min(value, ChannelLength));
fileNode.Seek(TimeSpan.FromSeconds(value));
channelPosition = value;
NotifyPropertyChanged(nameof(ChannelPosition));
}
}
public double ChannelLength
{
get => channelLength;
private set
{
channelLength = value;
NotifyPropertyChanged(nameof(ChannelLength));
}
}
private float[] waveformData;
public float[] WaveformData
{
get => waveformData;
private set
{
waveformData = value;
NotifyPropertyChanged(nameof(WaveformData));
}
}
private async Task GenerateWaveformAsync(string wavPath)
{
await Task.Run(() =>
{
byte[] data = File.ReadAllBytes(wavPath);
if (data.Length < 44)
throw new InvalidOperationException("Invalid WAV file");
int channels = BitConverter.ToInt16(data, 22);
int bitsPerSample = BitConverter.ToInt16(data, 34);
int dataChunkOffset = -1;
int dataChunkSize = 0;
for (int i = 12; i < data.Length - 8;)
{
string chunkId = Encoding.ASCII.GetString(data, i, 4);
int chunkSize = BitConverter.ToInt32(data, i + 4);
if (chunkId == "data")
{
dataChunkOffset = i + 8;
dataChunkSize = chunkSize;
break;
}
i += 8 + chunkSize;
}
if (dataChunkOffset < 0)
throw new InvalidOperationException("WAV data chunk not found");
const int samplesPerBucket = 1024;
List<float> waveform = new();
float leftSumSq = 0, rightSumSq = 0;
float leftPeak = 0, rightPeak = 0;
int sampleCounter = 0;
int bytesPerSample = bitsPerSample / 8;
int frameSize = bytesPerSample * channels;
for (int i = dataChunkOffset; i + frameSize <= dataChunkOffset + dataChunkSize; i += frameSize)
{
float l, r;
if (bitsPerSample == 16)
{
l = BitConverter.ToInt16(data, i) / 32768f;
r = channels == 2 ? BitConverter.ToInt16(data, i + 2) / 32768f : l;
}
else if (bitsPerSample == 24)
{
int left = (data[i + 2] << 24) | (data[i + 1] << 16) | (data[i] << 8);
left >>= 8;
l = left / 8388608f;
if (channels == 2)
{
int right = (data[i + 5] << 24) | (data[i + 4] << 16) | (data[i + 3] << 8);
right >>= 8;
r = right / 8388608f;
}
else
r = l;
}
else if (bitsPerSample == 32)
{
l = BitConverter.ToSingle(data, i);
r = channels == 2 ? BitConverter.ToSingle(data, i + 4) : l;
}
else
{
throw new NotSupportedException($"Unsupported WAV bit depth: {bitsPerSample}");
}
leftPeak = Math.Max(leftPeak, Math.Abs(l));
rightPeak = Math.Max(rightPeak, Math.Abs(r));
leftSumSq += l * l;
rightSumSq += r * r;
sampleCounter++;
if (sampleCounter >= samplesPerBucket)
{
float leftRms = MathF.Sqrt(leftSumSq / sampleCounter);
float rightRms = MathF.Sqrt(rightSumSq / sampleCounter);
waveform.Add((leftRms + leftPeak) * 0.5f);
waveform.Add((rightRms + rightPeak) * 0.5f);
leftSumSq = rightSumSq = 0;
leftPeak = rightPeak = 0;
sampleCounter = 0;
}
}
float max = 0f;
for (int i = 0; i < waveform.Count; i++)
{
float v = Math.Abs(waveform[i]);
if (v > max)
max = v;
}
if (max > 0)
{
float gain = 1f / max;
for (int i = 0; i < waveform.Count; i++)
waveform[i] *= gain;
}
dispatcherQueue.TryEnqueue(() =>
{
WaveformData = waveform.ToArray();
});
});
}
public bool IsPlaying
{
get => isPlaying;
private set
{
isPlaying = value;
NotifyPropertyChanged(nameof(IsPlaying));
}
}
public async Task OpenFile(string path)
{
DisposeGraph();
var file = await StorageFile.GetFileFromPathAsync(path);
await GenerateWaveformAsync(path);
var settings = new AudioGraphSettings(AudioRenderCategory.Media)
{
QuantumSizeSelectionMode = QuantumSizeSelectionMode.ClosestToDesired,
DesiredSamplesPerQuantum = fftDataSize
};
var graphResult = await AudioGraph.CreateAsync(settings);
if (graphResult.Status != AudioGraphCreationStatus.Success)
throw new InvalidOperationException("AudioGraph creation failed");
graph = graphResult.Graph;
var deviceResult = await graph.CreateDeviceOutputNodeAsync();
deviceNode = deviceResult.DeviceOutputNode;
var fileResult = await graph.CreateFileInputNodeAsync(file);
fileNode = fileResult.FileInputNode;
frameNode = graph.CreateFrameOutputNode();
fileNode.AddOutgoingConnection(deviceNode);
fileNode.AddOutgoingConnection(frameNode);
ChannelLength = fileNode.Duration.TotalSeconds;
graph.QuantumStarted += OnQuantumStarted;
}
public void Play()
{
if (graph == null)
return;
playbackStartTime = fileNode.Position;
graph.Start();
IsPlaying = true;
}
public void Pause()
{
if (graph == null)
return;
graph.Stop();
IsPlaying = false;
}
public void Stop()
{
if (graph == null)
return;
graph.Stop();
fileNode.Seek(TimeSpan.Zero);
ChannelPosition = 0;
IsPlaying = false;
}
private unsafe void OnQuantumStarted(AudioGraph sender, object args)
{
var frame = frameNode.GetFrame();
using var buffer = frame.LockBuffer(AudioBufferAccessMode.Read);
using var reference = buffer.CreateReference();
((IMemoryBufferByteAccess)reference)
.GetBuffer(out byte* data, out uint capacity);
float* samples = (float*)data;
int count = (int)(capacity / sizeof(float));
for (int i = 0; i < count; i++)
sampleBuffer.Write(samples[i]);
double positionSeconds = 0;
if (fileNode != null && IsPlaying)
positionSeconds = fileNode.Position.TotalSeconds;
dispatcherQueue.TryEnqueue(() =>
{
channelPosition = positionSeconds;
NotifyPropertyChanged(nameof(ChannelPosition));
});
}
public void Dispose()
{
DisposeGraph();
GC.SuppressFinalize(this);
}
private void DisposeGraph()
{
if (graph != null)
{
graph.QuantumStarted -= OnQuantumStarted;
graph.Stop();
graph.Dispose();
graph = null;
}
fileNode = null;
deviceNode = null;
frameNode = null;
}
private void NotifyPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public sealed partial class AudioSampleRingBuffer
{
private readonly float[] buffer;
private int writeIndex;
private int readIndex;
public AudioSampleRingBuffer(int capacity)
{
buffer = new float[capacity];
}
public void Write(float value)
{
buffer[writeIndex] = value;
writeIndex = (writeIndex + 1) % buffer.Length;
}
public bool TryRead(out float value)
{
if (readIndex == writeIndex)
{
value = 0;
return false;
}
value = buffer[readIndex];
readIndex = (readIndex + 1) % buffer.Length;
return true;
}
}
public static partial class FastFourierTransform
{
/// <summary>
/// In-place radix-2 FFT (forward).
/// </summary>
public static void FFT(Complex[] data, int exponent)
{
int n = 1 << exponent;
// Bit-reversal
int j = 0;
for (int i = 0; i < n; i++)
{
if (i < j)
{
var temp = data[i];
data[i] = data[j];
data[j] = temp;
}
int m = n >> 1;
while (j >= m && m >= 2)
{
j -= m;
m >>= 1;
}
j += m;
}
// FFT stages
for (int stage = 1; stage <= exponent; stage++)
{
int step = 1 << stage;
int halfStep = step >> 1;
double angleStep = -2.0 * Math.PI / step;
Complex phaseStep = new Complex(
Math.Cos(angleStep),
Math.Sin(angleStep));
for (int k = 0; k < n; k += step)
{
Complex phase = Complex.One;
for (int i = 0; i < halfStep; i++)
{
int evenIndex = k + i;
int oddIndex = evenIndex + halfStep;
Complex even = data[evenIndex];
Complex odd = phase * data[oddIndex];
data[evenIndex] = even + odd;
data[oddIndex] = even - odd;
phase *= phaseStep;
}
}
}
}
}
public sealed partial class SampleAggregator
{
private readonly int bufferSize;
private readonly int fftExponent;
private readonly Complex[] channelData;
private int channelDataPosition;
private float volumeLeftMaxValue;
private float volumeLeftMinValue;
private float volumeRightMaxValue;
private float volumeRightMinValue;
public SampleAggregator(int bufferSize)
{
if ((bufferSize & (bufferSize - 1)) != 0)
throw new ArgumentException("FFT buffer size must be a power of two");
this.bufferSize = bufferSize;
fftExponent = (int)Math.Log(bufferSize, 2);
channelData = new Complex[bufferSize];
Clear();
}
public void Clear()
{
volumeLeftMaxValue = float.MinValue;
volumeRightMaxValue = float.MinValue;
volumeLeftMinValue = float.MaxValue;
volumeRightMinValue = float.MaxValue;
channelDataPosition = 0;
}
/// <summary>
/// Adds a stereo sample.
/// </summary>
public void Add(float leftValue, float rightValue)
{
if (channelDataPosition == 0)
{
volumeLeftMaxValue = float.MinValue;
volumeRightMaxValue = float.MinValue;
volumeLeftMinValue = float.MaxValue;
volumeRightMinValue = float.MaxValue;
}
// Convert stereo → mono (average)
float mono = (leftValue + rightValue) * 0.5f;
channelData[channelDataPosition] = new Complex(mono, 0);
channelDataPosition++;
volumeLeftMaxValue = Math.Max(volumeLeftMaxValue, leftValue);
volumeLeftMinValue = Math.Min(volumeLeftMinValue, leftValue);
volumeRightMaxValue = Math.Max(volumeRightMaxValue, rightValue);
volumeRightMinValue = Math.Min(volumeRightMinValue, rightValue);
if (channelDataPosition >= bufferSize)
channelDataPosition = 0;
}
/// <summary>
/// Copies FFT magnitude data into the provided buffer.
/// Buffer length must be >= bufferSize / 2.
/// </summary>
public void GetFFTResults(float[] fftBuffer)
{
if (fftBuffer == null)
throw new ArgumentNullException(nameof(fftBuffer));
if (fftBuffer.Length < bufferSize / 2)
throw new ArgumentException("FFT buffer is too small");
// Clone so capture thread is not disturbed
var fftData = new Complex[bufferSize];
Array.Copy(channelData, fftData, bufferSize);
FastFourierTransform.FFT(fftData, fftExponent);
for (int i = 0; i < bufferSize / 2; i++)
{
double real = fftData[i].Real;
double imag = fftData[i].Imaginary;
fftBuffer[i] = (float)Math.Sqrt(real * real + imag * imag);
}
}
public float LeftMaxVolume => volumeLeftMaxValue;
public float LeftMinVolume => volumeLeftMinValue;
public float RightMaxVolume => volumeRightMaxValue;
public float RightMinVolume => volumeRightMinValue;
}NAudioEngine
cs
soundEngine = NAudioEngine.Instance;
WaveformTimelineSample.RegisterSoundPlayer(soundEngine);
soundEngine.OpenFile(@"C:\myAudio.mp3");
soundEngine.Play();cs
public partial class NAudioEngine : INotifyPropertyChanged, ISpectrumPlayer, IWaveformPlayer, IDisposable
{
private readonly DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private static NAudioEngine instance;
private readonly DispatcherTimer positionTimer = new DispatcherTimer();
private CancellationTokenSource waveformCts;
private Task waveformTask;
private readonly int fftDataSize = (int)FFTDataSize.FFT2048;
private bool disposed;
private bool canPlay;
private bool canPause;
private bool canStop;
private bool isPlaying;
private bool inChannelTimerUpdate;
private double channelLength;
private double channelPosition;
private bool inChannelSet;
private WaveOut waveOutDevice;
private WaveStream activeStream;
private WaveChannel32 inputStream;
private SampleAggregator sampleAggregator;
private SampleAggregator waveformAggregator;
private string pendingWaveformPath;
private float[] fullLevelData;
private float[] waveformData;
private TimeSpan repeatStart;
private TimeSpan repeatStop;
private bool inRepeatSet;
private const int waveformCompressedPointCount = 2000;
private const int repeatThreshold = 200;
public static NAudioEngine Instance
{
get
{
if (instance == null)
instance = new NAudioEngine();
return instance;
}
}
private NAudioEngine()
{
positionTimer.Interval = TimeSpan.FromMilliseconds(50);
positionTimer.Tick += positionTimer_Tick;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
StopAndCloseStream();
}
disposed = true;
}
}
public bool GetFFTData(float[] fftDataBuffer)
{
sampleAggregator.GetFFTResults(fftDataBuffer);
return isPlaying;
}
public int GetFFTFrequencyIndex(int frequency)
{
double maxFrequency;
if (ActiveStream != null)
maxFrequency = ActiveStream.WaveFormat.SampleRate / 2.0d;
else
maxFrequency = 22050; // Assume a default 44.1 kHz sample rate.
return (int)((frequency / maxFrequency) * (fftDataSize / 2));
}
public TimeSpan SelectionBegin
{
get { return repeatStart; }
set
{
if (!inRepeatSet)
{
inRepeatSet = true;
TimeSpan oldValue = repeatStart;
repeatStart = value;
if (oldValue != repeatStart)
NotifyPropertyChanged("SelectionBegin");
inRepeatSet = false;
}
}
}
public TimeSpan SelectionEnd
{
get { return repeatStop; }
set
{
if (!inChannelSet)
{
inRepeatSet = true;
TimeSpan oldValue = repeatStop;
repeatStop = value;
if (oldValue != repeatStop)
NotifyPropertyChanged("SelectionEnd");
inRepeatSet = false;
}
}
}
public float[] WaveformData
{
get { return waveformData; }
protected set
{
float[] oldValue = waveformData;
waveformData = value;
if (oldValue != waveformData)
NotifyPropertyChanged("WaveformData");
}
}
public double ChannelLength
{
get { return channelLength; }
protected set
{
double oldValue = channelLength;
channelLength = value;
if (oldValue != channelLength)
NotifyPropertyChanged("ChannelLength");
}
}
public double ChannelPosition
{
get { return channelPosition; }
set
{
if (!inChannelSet)
{
inChannelSet = true; // Avoid recursion
double oldValue = channelPosition;
double position = Math.Max(0, Math.Min(value, ChannelLength));
if (!inChannelTimerUpdate && ActiveStream != null)
ActiveStream.Position = (long)((position / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
channelPosition = position;
if (oldValue != channelPosition)
NotifyPropertyChanged("ChannelPosition");
inChannelSet = false;
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private partial class WaveformGenerationParams
{
public WaveformGenerationParams(int points, string path)
{
Points = points;
Path = path;
}
public int Points { get; protected set; }
public string Path { get; protected set; }
}
private void GenerateWaveformData(string path)
{
// If a task is running, cancel it and queue the new request
if (waveformTask != null && !waveformTask.IsCompleted)
{
pendingWaveformPath = path;
waveformCts.Cancel();
return;
}
if (waveformCompressedPointCount != 0)
{
waveformCts = new CancellationTokenSource();
waveformTask = GenerateWaveformDataAsync(
new WaveformGenerationParams(waveformCompressedPointCount, path),
waveformCts.Token);
}
}
private async Task GenerateWaveformDataAsync(
WaveformGenerationParams waveformParams,
CancellationToken token)
{
try
{
await Task.Run(() =>
{
using var waveformMp3Stream = new Mp3FileReader(waveformParams.Path);
using var waveformInputStream = new WaveChannel32(waveformMp3Stream);
waveformInputStream.Sample += waveStream_Sample;
int frameLength = fftDataSize;
int frameCount = (int)((double)waveformInputStream.Length / frameLength);
int waveformLength = frameCount * 2;
byte[] readBuffer = new byte[frameLength];
waveformAggregator = new SampleAggregator(frameLength);
float maxLeftPointLevel = float.MinValue;
float maxRightPointLevel = float.MinValue;
int currentPointIndex = 0;
int readCount = 0;
float[] waveformCompressedPoints = new float[waveformParams.Points];
List<float> waveformData = new();
List<int> waveMaxPointIndexes = new();
for (int i = 1; i <= waveformParams.Points; i++)
{
waveMaxPointIndexes.Add(
(int)Math.Round(
waveformLength * ((double)i / waveformParams.Points), 0));
}
while (currentPointIndex * 2 < waveformParams.Points)
{
token.ThrowIfCancellationRequested();
waveformInputStream.ReadExactly(readBuffer);
waveformData.Add(waveformAggregator.LeftMaxVolume);
waveformData.Add(waveformAggregator.RightMaxVolume);
maxLeftPointLevel = Math.Max(maxLeftPointLevel, waveformAggregator.LeftMaxVolume);
maxRightPointLevel = Math.Max(maxRightPointLevel, waveformAggregator.RightMaxVolume);
if (readCount > waveMaxPointIndexes[currentPointIndex])
{
waveformCompressedPoints[currentPointIndex * 2] = maxLeftPointLevel;
waveformCompressedPoints[currentPointIndex * 2 + 1] = maxRightPointLevel;
maxLeftPointLevel = float.MinValue;
maxRightPointLevel = float.MinValue;
currentPointIndex++;
}
if (readCount % 3000 == 0)
{
var cloned = (float[])waveformCompressedPoints.Clone();
dispatcherQueue.TryEnqueue(() =>
{
WaveformData = cloned;
});
}
readCount++;
}
var finalClone = (float[])waveformCompressedPoints.Clone();
dispatcherQueue.TryEnqueue(() =>
{
fullLevelData = waveformData.ToArray();
WaveformData = finalClone;
});
}, token);
}
catch (OperationCanceledException)
{
// Expected on cancellation
}
finally
{
// If cancellation occurred and a new path is pending, restart
if (!string.IsNullOrEmpty(pendingWaveformPath) &&
waveformCompressedPointCount != 0)
{
string nextPath = pendingWaveformPath;
pendingWaveformPath = null;
waveformCts = new CancellationTokenSource();
waveformTask = GenerateWaveformDataAsync(
new WaveformGenerationParams(waveformCompressedPointCount, nextPath),
waveformCts.Token);
}
}
}
private void StopAndCloseStream()
{
if (waveOutDevice != null)
{
waveOutDevice.Stop();
}
if (activeStream != null)
{
inputStream.Close();
inputStream = null;
ActiveStream.Close();
ActiveStream = null;
}
if (waveOutDevice != null)
{
waveOutDevice.Dispose();
waveOutDevice = null;
}
}
public void Stop()
{
if (waveOutDevice != null)
{
waveOutDevice.Stop();
}
IsPlaying = false;
CanStop = false;
CanPlay = true;
CanPause = false;
}
public void Pause()
{
if (IsPlaying && CanPause)
{
waveOutDevice.Pause();
IsPlaying = false;
CanPlay = true;
CanPause = false;
}
}
public void Play()
{
if (CanPlay)
{
waveOutDevice.Play();
IsPlaying = true;
CanPause = true;
CanPlay = false;
CanStop = true;
}
}
public void OpenFile(string path)
{
Stop();
if (ActiveStream != null)
{
SelectionBegin = TimeSpan.Zero;
SelectionEnd = TimeSpan.Zero;
ChannelPosition = 0;
}
StopAndCloseStream();
if (System.IO.File.Exists(path))
{
try
{
waveOutDevice = new WaveOut()
{
DesiredLatency = 100
};
ActiveStream = new Mp3FileReader(path);
inputStream = new WaveChannel32(ActiveStream);
sampleAggregator = new SampleAggregator(fftDataSize);
inputStream.Sample += inputStream_Sample;
waveOutDevice.Init(inputStream);
ChannelLength = inputStream.TotalTime.TotalSeconds;
GenerateWaveformData(path);
CanPlay = true;
}
catch
{
ActiveStream = null;
CanPlay = false;
}
}
}
public WaveStream ActiveStream
{
get { return activeStream; }
protected set
{
WaveStream oldValue = activeStream;
activeStream = value;
if (oldValue != activeStream)
NotifyPropertyChanged("ActiveStream");
}
}
public bool CanPlay
{
get { return canPlay; }
protected set
{
bool oldValue = canPlay;
canPlay = value;
if (oldValue != canPlay)
NotifyPropertyChanged("CanPlay");
}
}
public bool CanPause
{
get { return canPause; }
protected set
{
bool oldValue = canPause;
canPause = value;
if (oldValue != canPause)
NotifyPropertyChanged("CanPause");
}
}
public bool CanStop
{
get { return canStop; }
protected set
{
bool oldValue = canStop;
canStop = value;
if (oldValue != canStop)
NotifyPropertyChanged("CanStop");
}
}
public bool IsPlaying
{
get { return isPlaying; }
protected set
{
bool oldValue = isPlaying;
isPlaying = value;
if (oldValue != isPlaying)
NotifyPropertyChanged("IsPlaying");
if (isPlaying)
positionTimer.Start();
else
positionTimer.Stop();
}
}
private void inputStream_Sample(object sender, SampleEventArgs e)
{
sampleAggregator.Add(e.Left, e.Right);
long repeatStartPosition = (long)((SelectionBegin.TotalSeconds / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
long repeatStopPosition = (long)((SelectionEnd.TotalSeconds / ActiveStream.TotalTime.TotalSeconds) * ActiveStream.Length);
if (((SelectionEnd - SelectionBegin) >= TimeSpan.FromMilliseconds(repeatThreshold)) && ActiveStream.Position >= repeatStopPosition)
{
sampleAggregator.Clear();
ActiveStream.Position = repeatStartPosition;
}
}
void waveStream_Sample(object sender, SampleEventArgs e)
{
waveformAggregator.Add(e.Left, e.Right);
}
void positionTimer_Tick(object sender, object e)
{
inChannelTimerUpdate = true;
ChannelPosition = ((double)ActiveStream.Position / (double)ActiveStream.Length) * ActiveStream.TotalTime.TotalSeconds;
inChannelTimerUpdate = false;
}
}
public partial class SampleAggregator
{
private float volumeLeftMaxValue;
private float volumeLeftMinValue;
private float volumeRightMaxValue;
private float volumeRightMinValue;
private Complex[] channelData;
private int bufferSize;
private int binaryExponentitation;
private int channelDataPosition;
public SampleAggregator(int bufferSize)
{
this.bufferSize = bufferSize;
binaryExponentitation = (int)Math.Log(bufferSize, 2);
channelData = new Complex[bufferSize];
}
public void Clear()
{
volumeLeftMaxValue = float.MinValue;
volumeRightMaxValue = float.MinValue;
volumeLeftMinValue = float.MaxValue;
volumeRightMinValue = float.MaxValue;
channelDataPosition = 0;
}
/// <summary>
/// Add a sample value to the aggregator.
/// </summary>
/// <param name="value">The value of the sample.</param>
public void Add(float leftValue, float rightValue)
{
if (channelDataPosition == 0)
{
volumeLeftMaxValue = float.MinValue;
volumeRightMaxValue = float.MinValue;
volumeLeftMinValue = float.MaxValue;
volumeRightMinValue = float.MaxValue;
}
// Make stored channel data stereo by averaging left and right values.
channelData[channelDataPosition].X = (leftValue + rightValue) / 2.0f;
channelData[channelDataPosition].Y = 0;
channelDataPosition++;
volumeLeftMaxValue = Math.Max(volumeLeftMaxValue, leftValue);
volumeLeftMinValue = Math.Min(volumeLeftMinValue, leftValue);
volumeRightMaxValue = Math.Max(volumeRightMaxValue, rightValue);
volumeRightMinValue = Math.Min(volumeRightMinValue, rightValue);
if (channelDataPosition >= channelData.Length)
{
channelDataPosition = 0;
}
}
/// <summary>
/// Performs an FFT calculation on the channel data upon request.
/// </summary>
/// <param name="fftBuffer">A buffer where the FFT data will be stored.</param>
public void GetFFTResults(float[] fftBuffer)
{
Complex[] channelDataClone = new Complex[bufferSize];
channelData.CopyTo(channelDataClone, 0);
FastFourierTransform.FFT(true, binaryExponentitation, channelDataClone);
for (int i = 0; i < channelDataClone.Length / 2; i++)
{
// Calculate actual intensities for the FFT results.
fftBuffer[i] = (float)Math.Sqrt(channelDataClone[i].X * channelDataClone[i].X + channelDataClone[i].Y * channelDataClone[i].Y);
}
}
public float LeftMaxVolume
{
get { return volumeLeftMaxValue; }
}
public float LeftMinVolume
{
get { return volumeLeftMinValue; }
}
public float RightMaxVolume
{
get { return volumeRightMaxValue; }
}
public float RightMinVolume
{
get { return volumeRightMinValue; }
}
}
Demo
you can run demo and see this feature.