ABStemPlayer/AudioCore/Impl/WasapiOutputDevice.cs
Alexander Shabarshov 950e5f1dcc Refactor playback speed & mixer management; UI tweaks
Refactor playback speed and mixer settings to be managed via PlaybackSession, removing SetSpeed/SetStem* methods. Update Play command to apply current mixer and speed. Optimize RenderLoop with reusable audio block list and conditional output start. Align playback control icons in XAML. Update tests for new speed handling and add speed decrease coverage. Remove unused IStemPlaybackEngine methods.
2026-07-05 15:07:42 +01:00

120 lines
3.6 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using NAudio.CoreAudioApi;
using NAudio.Dmo;
using NAudio.Wave;
namespace AudioCore.Impl;
public sealed class WasapiOutputDevice : IAudioOutputDevice, IDisposable
{
private readonly IWavePlayer _out;
private readonly BufferedWaveProvider _buffer;
private readonly WaveFormat _mixFormat = null!;
private readonly ByteBufferPool _pool;
private readonly bool _isFloat;
public int SampleRate { get; }
public int Channels { get; }
/// <summary>
/// For tests only, exposes the underlying buffer to allow direct writes.
/// </summary>
public BufferedWaveProvider Buffer => _buffer;
/// <summary>
/// For tests only
/// </summary>
public WasapiOutputDevice(ByteBufferPool pool, IWavePlayer output, int sampleRate = 44100, int channels = 2)
{
_pool = pool;
_mixFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels);
_isFloat = true;
SampleRate = sampleRate;
Channels = channels;
_buffer = new BufferedWaveProvider(_mixFormat)
{
DiscardOnBufferOverflow = true
};
_out = output;
_out.Init(_buffer);
}
[ExcludeFromCodeCoverage]
public WasapiOutputDevice(ByteBufferPool pool, int channels = 2)
{
Channels = channels;
_pool = pool;
// Get default audio device
var device = new MMDeviceEnumerator()
.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
// Use the system mix format (required for shared mode)
_mixFormat = device.AudioClient.MixFormat;
SampleRate = _mixFormat.SampleRate;
_isFloat = _mixFormat.Encoding == WaveFormatEncoding.Extensible &&
((WaveFormatExtensible)_mixFormat).SubFormat == AudioMediaSubtypes.MEDIASUBTYPE_IEEE_FLOAT;
// Create buffer in mix format
_buffer = new BufferedWaveProvider(_mixFormat)
{
BufferLength = _mixFormat.AverageBytesPerSecond / 2, // 0.5 seconds
DiscardOnBufferOverflow = false
};
// Event-driven WASAPI mode (true = event mode)
_out = new WasapiOut(device, AudioClientShareMode.Shared, false, 10);
_out.Init(_buffer);
}
public void Start() => _out.Play();
public void Stop() => _out.Stop();
public void Write(ReadOnlySpan<float> samples)
{
// Convert float -> PCM16 or float passthrough depending on mix format
byte[] bytes;
if (_isFloat)
{
// Device supports float32 directly
bytes = MemoryMarshal.AsBytes(samples).ToArray();
Send(bytes);
}
else
{
// Convert float -> PCM16
int count = samples.Length;
using var buffer = _pool.Rent(count * 2);
int bi = 0;
for (int i = 0; i < count; i++)
{
short pcm = (short)(Math.Clamp(samples[i], -1f, 1f) * short.MaxValue);
buffer.Samples[bi++] = (byte)(pcm & 0xFF);
buffer.Samples[bi++] = (byte)((pcm >> 8) & 0xFF);
}
Send(buffer.Samples);
}
}
private void Send(byte[] bytes)
{
// Wait until buffer has enough free space
while (_buffer.BufferedBytes + bytes.Length > _buffer.BufferLength)
{
// Sleep a tiny amount to let WASAPI consume data
Thread.Sleep(2);
}
_buffer.AddSamples(bytes, 0, bytes.Length);
}
public void Dispose()
{
_out.Dispose();
}
}