Skip to content

Commit 1bcbcd7

Browse files
committed
ReWrite NAudioEngine with AudioGraph so SpectrumAnalyzer and WaveformTimeline can work with AOT
1 parent 960ca15 commit 1bcbcd7

File tree

12 files changed

+613
-625
lines changed

12 files changed

+613
-625
lines changed
2.15 MB
Binary file not shown.
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
using System.ComponentModel;
2+
using System.Runtime.InteropServices;
3+
using System.Runtime.InteropServices.Marshalling;
4+
using System.Text;
5+
using Microsoft.UI.Dispatching;
6+
using Windows.Media;
7+
using Windows.Media.Audio;
8+
using Windows.Media.Render;
9+
using Windows.Storage;
10+
11+
namespace DevWinUIGallery.Common;
12+
13+
[GeneratedComInterface]
14+
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
15+
public unsafe partial interface IMemoryBufferByteAccess
16+
{
17+
void GetBuffer(out byte* buffer, out uint capacity);
18+
}
19+
20+
public sealed partial class AudioGraphEngine : ISpectrumPlayer, IWaveformPlayer, INotifyPropertyChanged, IDisposable
21+
{
22+
private readonly DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
23+
24+
private AudioGraph graph;
25+
private AudioFileInputNode fileNode;
26+
private AudioDeviceOutputNode deviceNode;
27+
private AudioFrameOutputNode frameNode;
28+
29+
private readonly int fftDataSize = 2048;
30+
private readonly SampleAggregator sampleAggregator;
31+
32+
private bool isPlaying;
33+
private double channelLength;
34+
private double channelPosition;
35+
36+
public event PropertyChangedEventHandler PropertyChanged;
37+
private AudioSampleRingBuffer sampleBuffer;
38+
private CancellationTokenSource processingCts;
39+
40+
private TimeSpan playbackStartTime;
41+
public TimeSpan SelectionBegin { get; set; }
42+
public TimeSpan SelectionEnd { get; set; }
43+
public AudioGraphEngine()
44+
{
45+
sampleAggregator = new SampleAggregator(fftDataSize);
46+
47+
sampleBuffer = new AudioSampleRingBuffer(fftDataSize * 8);
48+
processingCts = new CancellationTokenSource();
49+
StartProcessingLoop();
50+
}
51+
private void StartProcessingLoop()
52+
{
53+
Task.Run(async () =>
54+
{
55+
float left, right;
56+
57+
while (!processingCts.IsCancellationRequested)
58+
{
59+
if (sampleBuffer.TryRead(out left) &&
60+
sampleBuffer.TryRead(out right))
61+
{
62+
sampleAggregator.Add(left, right);
63+
}
64+
else
65+
{
66+
await Task.Delay(1);
67+
}
68+
}
69+
}, processingCts.Token);
70+
}
71+
72+
public bool GetFFTData(float[] fftDataBuffer)
73+
{
74+
sampleAggregator.GetFFTResults(fftDataBuffer);
75+
return IsPlaying;
76+
}
77+
78+
public int GetFFTFrequencyIndex(int frequency)
79+
{
80+
double maxFrequency = graph != null
81+
? graph.EncodingProperties.SampleRate / 2.0
82+
: 22050;
83+
84+
return (int)((frequency / maxFrequency) * (fftDataSize / 2));
85+
}
86+
87+
public double ChannelPosition
88+
{
89+
get => channelPosition;
90+
set
91+
{
92+
if (fileNode == null)
93+
return;
94+
95+
value = Math.Max(0, Math.Min(value, ChannelLength));
96+
fileNode.Seek(TimeSpan.FromSeconds(value));
97+
channelPosition = value;
98+
NotifyPropertyChanged(nameof(ChannelPosition));
99+
}
100+
}
101+
102+
public double ChannelLength
103+
{
104+
get => channelLength;
105+
private set
106+
{
107+
channelLength = value;
108+
NotifyPropertyChanged(nameof(ChannelLength));
109+
}
110+
}
111+
112+
private float[] waveformData;
113+
114+
public float[] WaveformData
115+
{
116+
get => waveformData;
117+
private set
118+
{
119+
waveformData = value;
120+
NotifyPropertyChanged(nameof(WaveformData));
121+
}
122+
}
123+
private async Task GenerateWaveformAsync(string wavPath)
124+
{
125+
await Task.Run(() =>
126+
{
127+
byte[] data = File.ReadAllBytes(wavPath);
128+
129+
if (data.Length < 44)
130+
throw new InvalidOperationException("Invalid WAV file");
131+
132+
int channels = BitConverter.ToInt16(data, 22);
133+
int bitsPerSample = BitConverter.ToInt16(data, 34);
134+
135+
int dataChunkOffset = -1;
136+
int dataChunkSize = 0;
137+
138+
for (int i = 12; i < data.Length - 8;)
139+
{
140+
string chunkId = Encoding.ASCII.GetString(data, i, 4);
141+
int chunkSize = BitConverter.ToInt32(data, i + 4);
142+
143+
if (chunkId == "data")
144+
{
145+
dataChunkOffset = i + 8;
146+
dataChunkSize = chunkSize;
147+
break;
148+
}
149+
150+
i += 8 + chunkSize;
151+
}
152+
153+
if (dataChunkOffset < 0)
154+
throw new InvalidOperationException("WAV data chunk not found");
155+
156+
const int samplesPerBucket = 1024;
157+
158+
List<float> waveform = new();
159+
160+
float leftSumSq = 0, rightSumSq = 0;
161+
float leftPeak = 0, rightPeak = 0;
162+
int sampleCounter = 0;
163+
164+
int bytesPerSample = bitsPerSample / 8;
165+
int frameSize = bytesPerSample * channels;
166+
167+
for (int i = dataChunkOffset; i + frameSize <= dataChunkOffset + dataChunkSize; i += frameSize)
168+
{
169+
float l, r;
170+
171+
if (bitsPerSample == 16)
172+
{
173+
l = BitConverter.ToInt16(data, i) / 32768f;
174+
r = channels == 2 ? BitConverter.ToInt16(data, i + 2) / 32768f : l;
175+
}
176+
else if (bitsPerSample == 24)
177+
{
178+
int left = (data[i + 2] << 24) | (data[i + 1] << 16) | (data[i] << 8);
179+
left >>= 8;
180+
l = left / 8388608f;
181+
182+
if (channels == 2)
183+
{
184+
int right = (data[i + 5] << 24) | (data[i + 4] << 16) | (data[i + 3] << 8);
185+
right >>= 8;
186+
r = right / 8388608f;
187+
}
188+
else
189+
r = l;
190+
}
191+
else if (bitsPerSample == 32)
192+
{
193+
l = BitConverter.ToSingle(data, i);
194+
r = channels == 2 ? BitConverter.ToSingle(data, i + 4) : l;
195+
}
196+
else
197+
{
198+
throw new NotSupportedException($"Unsupported WAV bit depth: {bitsPerSample}");
199+
}
200+
201+
leftPeak = Math.Max(leftPeak, Math.Abs(l));
202+
rightPeak = Math.Max(rightPeak, Math.Abs(r));
203+
204+
leftSumSq += l * l;
205+
rightSumSq += r * r;
206+
sampleCounter++;
207+
208+
if (sampleCounter >= samplesPerBucket)
209+
{
210+
float leftRms = MathF.Sqrt(leftSumSq / sampleCounter);
211+
float rightRms = MathF.Sqrt(rightSumSq / sampleCounter);
212+
213+
waveform.Add((leftRms + leftPeak) * 0.5f);
214+
waveform.Add((rightRms + rightPeak) * 0.5f);
215+
216+
leftSumSq = rightSumSq = 0;
217+
leftPeak = rightPeak = 0;
218+
sampleCounter = 0;
219+
}
220+
}
221+
222+
float max = 0f;
223+
for (int i = 0; i < waveform.Count; i++)
224+
{
225+
float v = Math.Abs(waveform[i]);
226+
if (v > max)
227+
max = v;
228+
}
229+
230+
if (max > 0)
231+
{
232+
float gain = 1f / max;
233+
for (int i = 0; i < waveform.Count; i++)
234+
waveform[i] *= gain;
235+
}
236+
237+
dispatcherQueue.TryEnqueue(() =>
238+
{
239+
WaveformData = waveform.ToArray();
240+
});
241+
});
242+
}
243+
244+
public bool IsPlaying
245+
{
246+
get => isPlaying;
247+
private set
248+
{
249+
isPlaying = value;
250+
NotifyPropertyChanged(nameof(IsPlaying));
251+
}
252+
}
253+
254+
public async Task OpenFileAsync(StorageFile storageFile, bool generateWaveform)
255+
{
256+
DisposeGraph();
257+
258+
if (generateWaveform)
259+
{
260+
await GenerateWaveformAsync(storageFile.Path);
261+
}
262+
263+
var settings = new AudioGraphSettings(AudioRenderCategory.Media)
264+
{
265+
QuantumSizeSelectionMode = QuantumSizeSelectionMode.ClosestToDesired,
266+
DesiredSamplesPerQuantum = fftDataSize
267+
};
268+
269+
var graphResult = await AudioGraph.CreateAsync(settings);
270+
if (graphResult.Status != AudioGraphCreationStatus.Success)
271+
throw new InvalidOperationException("AudioGraph creation failed");
272+
273+
graph = graphResult.Graph;
274+
275+
var deviceResult = await graph.CreateDeviceOutputNodeAsync();
276+
deviceNode = deviceResult.DeviceOutputNode;
277+
278+
var fileResult = await graph.CreateFileInputNodeAsync(storageFile);
279+
fileNode = fileResult.FileInputNode;
280+
281+
frameNode = graph.CreateFrameOutputNode();
282+
283+
fileNode.AddOutgoingConnection(deviceNode);
284+
fileNode.AddOutgoingConnection(frameNode);
285+
286+
ChannelLength = fileNode.Duration.TotalSeconds;
287+
288+
graph.QuantumStarted += OnQuantumStarted;
289+
}
290+
291+
public void Play()
292+
{
293+
if (graph == null)
294+
return;
295+
296+
playbackStartTime = fileNode.Position;
297+
graph.Start();
298+
IsPlaying = true;
299+
}
300+
301+
302+
public void Pause()
303+
{
304+
if (graph == null)
305+
return;
306+
307+
graph.Stop();
308+
IsPlaying = false;
309+
}
310+
311+
public void Stop()
312+
{
313+
if (graph == null)
314+
return;
315+
316+
graph.Stop();
317+
fileNode.Seek(TimeSpan.Zero);
318+
ChannelPosition = 0;
319+
IsPlaying = false;
320+
}
321+
322+
private unsafe void OnQuantumStarted(AudioGraph sender, object args)
323+
{
324+
var frame = frameNode.GetFrame();
325+
using var buffer = frame.LockBuffer(AudioBufferAccessMode.Read);
326+
using var reference = buffer.CreateReference();
327+
328+
((IMemoryBufferByteAccess)reference)
329+
.GetBuffer(out byte* data, out uint capacity);
330+
331+
float* samples = (float*)data;
332+
int count = (int)(capacity / sizeof(float));
333+
334+
for (int i = 0; i < count; i++)
335+
sampleBuffer.Write(samples[i]);
336+
337+
double positionSeconds = 0;
338+
if (fileNode != null && IsPlaying)
339+
positionSeconds = fileNode.Position.TotalSeconds;
340+
341+
dispatcherQueue.TryEnqueue(() =>
342+
{
343+
channelPosition = positionSeconds;
344+
NotifyPropertyChanged(nameof(ChannelPosition));
345+
});
346+
}
347+
348+
public void Dispose()
349+
{
350+
DisposeGraph();
351+
GC.SuppressFinalize(this);
352+
}
353+
354+
private void DisposeGraph()
355+
{
356+
if (graph != null)
357+
{
358+
graph.QuantumStarted -= OnQuantumStarted;
359+
graph.Stop();
360+
graph.Dispose();
361+
graph = null;
362+
}
363+
364+
fileNode = null;
365+
deviceNode = null;
366+
frameNode = null;
367+
}
368+
private void NotifyPropertyChanged(string name)
369+
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
370+
}

0 commit comments

Comments
 (0)