Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 167 additions & 6 deletions src/platform/windows/audio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

// standard includes
#include <format>
#include <utility>

// platform includes
#include <Audioclient.h>
Expand Down Expand Up @@ -599,9 +600,10 @@ namespace platf::audio {
* @param frame_size Number of samples captured per audio frame.
* @param channels_out Channels out.
* @param continuous Whether silent audio should continue to be emitted.
* @param capture_device Endpoint device to capture from; the default render device is used when empty.
* @return 0 on success; nonzero or negative platform status on failure.
*/
int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous) {
int init(std::uint32_t sample_rate, std::uint32_t frame_size, std::uint32_t channels_out, bool continuous, device_t capture_device) {
audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
if (!audio_event) {
BOOST_LOG(error) << "Couldn't create Event handle"sv;
Expand Down Expand Up @@ -632,7 +634,8 @@ namespace platf::audio {
return -1;
}

auto device = default_device(device_enum);
select_capture_device(std::move(capture_device));

if (!device) {
return -1;
}
Expand Down Expand Up @@ -708,6 +711,20 @@ namespace platf::audio {
return 0;
}

/**
* @brief Select the endpoint used by this capture stream.
*
* @param capture_device Explicit endpoint to capture, or an empty pointer to follow the default endpoint.
*/
void select_capture_device(device_t capture_device) {
follows_default_device = !capture_device;
if (follows_default_device) {
device = default_device(device_enum);
} else {
device = std::move(capture_device);
}
}

~mic_wasapi_t() override {
if (device_enum) {
device_enum->UnregisterEndpointNotificationCallback(&endpt_notification);
Expand Down Expand Up @@ -744,8 +761,11 @@ namespace platf::audio {
(*default_endpt_changed_cb)();
}

// Reinitialize to pick up the new default device
return capture_e::reinit;
// Reinitialize to pick up the new default device, unless capture is
// pinned to an explicitly requested sink
if (follows_default_device) {
return capture_e::reinit;
}
}

status = WaitForSingleObjectEx(audio_event.get(), default_latency_ms, FALSE);
Expand Down Expand Up @@ -833,6 +853,7 @@ namespace platf::audio {
float *sample_buf_pos; ///< Current write position in `sample_buf`.
int channels; ///< Number of channels in the capture format.
bool continuous_audio; ///< Whether audio packets continue during silence.
bool follows_default_device; ///< Whether capture follows the default render device rather than an explicit sink.

HANDLE mmcss_task_handle = nullptr; ///< MMCSS task handle for the audio capture thread.
};
Expand Down Expand Up @@ -924,6 +945,34 @@ namespace platf::audio {
return std::nullopt;
}

/**
* @brief Resolve a sink name to the audio endpoint device it refers to.
*
* @param sink Sink name, virtual sink descriptor, or device identifier.
* @return Endpoint device to capture from, or an empty pointer if the sink couldn't be resolved.
*/
device_t get_sink_device(const std::string &sink) {
std::wstring device_id;
if (auto virtual_sink_info = extract_virtual_sink_info(sink)) {
device_id = virtual_sink_info->first;
} else if (auto matched = find_device_id(match_all_fields(utf_utils::from_utf8(sink)))) {
device_id = matched->second;
} else {
return nullptr;
}

device_t device;
if (FAILED(device_enum->GetDevice(device_id.c_str(), &device))) {
return nullptr;
}

if (DWORD device_state {}; FAILED(device->GetState(&device_state)) || device_state != DEVICE_STATE_ACTIVE) {
return nullptr;
}

return device;
}

/**
* @brief Create a microphone capture stream for the requested layout.
*
Expand All @@ -938,7 +987,25 @@ namespace platf::audio {
std::unique_ptr<mic_t> microphone(const std::uint8_t *mapping, int channels, std::uint32_t sample_rate, std::uint32_t frame_size, bool continuous_audio, [[maybe_unused]] bool host_audio_enabled) override {
auto mic = std::make_unique<mic_wasapi_t>();

if (mic->init(sample_rate, frame_size, channels, continuous_audio)) {
// Prefer the sink that was assigned to this capture session since it accounts
// for the priority between virtual and configured sinks.
const auto &requested_sink = assigned_sink.empty() ? config::audio.sink : assigned_sink;

// Capture the requested sink directly instead of relying on it being the default
// render device, so that capture keeps working when the default device differs
// from the sink or changes during the session.
device_t capture_device;
if (!requested_sink.empty()) {
capture_device = get_sink_device(requested_sink);
if (!capture_device) {
BOOST_LOG(error) << "Couldn't resolve audio sink ["sv << requested_sink << "] to a capture device"sv;
return nullptr;
}

BOOST_LOG(info) << "Capturing audio from sink ["sv << requested_sink << ']';
}

if (mic->init(sample_rate, frame_size, channels, continuous_audio, std::move(capture_device))) {
return nullptr;
}

Expand Down Expand Up @@ -1356,8 +1423,102 @@ namespace platf::audio {

policy_t policy; ///< Windows policy configuration interface used to switch default audio devices.
audio::device_enum_t device_enum; ///< Device enumerator used to query and watch audio endpoints.
std::string assigned_sink; ///< Virtual sink assigned while Sunshine captures host audio.
std::string assigned_sink; ///< Sink assigned while Sunshine captures host audio, captured directly by the microphone.
};

#ifdef SUNSHINE_TESTS
namespace tests {
/**
* @brief Resolve a sink through the production Windows endpoint lookup.
*
* @param sink Sink name, virtual sink descriptor, or device identifier.
* @param device_enum Device enumerator supplied by the test.
* @return `true` when the sink resolves to an active endpoint.
*/
bool sink_device_available(const std::string &sink, IMMDeviceEnumerator *device_enum) {
audio_control_t control;
device_enum->AddRef();
control.device_enum.reset(device_enum);
return static_cast<bool>(control.get_sink_device(sink));
}

/**
* @brief Exercise microphone creation with controlled assigned and configured sinks.
*
* @param assigned_sink Sink selected by the shared audio context.
* @param configured_sink Sink configured by the user.
* @param device_enum Device enumerator supplied by the test.
* @return `true` when microphone initialization succeeds.
*/
bool microphone_available(const std::string &assigned_sink, const std::string &configured_sink, IMMDeviceEnumerator *device_enum) {
audio_control_t control;
device_enum->AddRef();
control.device_enum.reset(device_enum);
control.assigned_sink = assigned_sink;

auto previous_configured_sink = std::exchange(config::audio.sink, configured_sink);
auto microphone = control.microphone(nullptr, 2, 48000, 240, false, false);
config::audio.sink = std::move(previous_configured_sink);
return static_cast<bool>(microphone);
}

/**
* @brief Select a default or explicit capture endpoint through the production selection path.
*
* @param device_enum Device enumerator supplied by the test.
* @param capture_device Explicit endpoint, or `nullptr` to select the default endpoint.
* @return `true` when capture follows the default endpoint.
*/
bool capture_follows_default_device(IMMDeviceEnumerator *device_enum, IMMDevice *capture_device) {
mic_wasapi_t microphone;
device_enum->AddRef();
microphone.device_enum.reset(device_enum);

device_t selected_device;
if (capture_device) {
capture_device->AddRef();
selected_device.reset(capture_device);
}

microphone.select_capture_device(std::move(selected_device));
return microphone.follows_default_device;
}

/**
* @brief Exercise the production default-device-change path without live audio hardware.
*
* @param follows_default_device Whether the capture follows the default render endpoint.
* @param install_callback Whether to install a default-device-change callback.
* @param render_device_changed Whether to signal a render rather than capture endpoint change.
* @param callback_count Receives the number of callback invocations.
* @return Capture result produced after processing the notification.
*/
capture_e simulate_default_device_change(bool follows_default_device, bool install_callback, bool render_device_changed, int &callback_count) {
mic_wasapi_t mic;
mic.audio_event.reset(CreateEventA(nullptr, FALSE, FALSE, nullptr));
mic.default_latency_ms = 0;
mic.sample_buf = util::buffer_t<float> {1};
mic.sample_buf_pos = std::begin(mic.sample_buf);
mic.continuous_audio = false;
mic.follows_default_device = follows_default_device;

if (install_callback) {
mic.default_endpt_changed_cb = [&callback_count] {
++callback_count;
};
}

mic.endpt_notification.OnDefaultDeviceChanged(
render_device_changed ? eRender : eCapture,
eConsole,
nullptr
);

std::vector<float> sample(1);
return mic.sample(sample);
}
} // namespace tests
#endif
} // namespace platf::audio

namespace platf {
Expand Down
Loading
Loading