|
| 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