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
71 changes: 34 additions & 37 deletions yip-app/App.xaml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,51 +11,48 @@
#include <winrt/Microsoft.UI.Composition.SystemBackdrops.h>

namespace winrt {
using namespace winrt::Microsoft::UI::Xaml;
using namespace winrt::Microsoft::UI::Xaml::Controls;
using namespace winrt::Microsoft::UI::Composition::SystemBackdrops;
}
using namespace winrt::Microsoft::UI::Xaml;
using namespace winrt::Microsoft::UI::Xaml::Controls;
using namespace winrt::Microsoft::UI::Composition::SystemBackdrops;
} // namespace winrt

namespace winrt::yip::implementation
namespace winrt::yip::implementation {
App::App()
{
App::App()
{
InitializeComponent();
InitializeComponent();

UnhandledException([](IInspectable const&, winrt::Microsoft::UI::Xaml::UnhandledExceptionEventArgs const& e) {
UnhandledException(
[](IInspectable const&, winrt::Microsoft::UI::Xaml::UnhandledExceptionEventArgs const& e) {
const auto msg = e.Message();
::OutputDebugStringW(L"Yip unhandled: ");
::OutputDebugStringW(msg.c_str());
::OutputDebugStringW(L"\n");
});
}
}

void App::OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& /*args*/)
{
m_window = winrt::make<winrt::yip::implementation::MainWindow>();
m_window.Title(L"Yip");

// Mica backdrop — Window owns the controller lifetime since 1.4.
if (winrt::Microsoft::UI::Composition::SystemBackdrops::MicaController::IsSupported())
{
m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::MicaBackdrop{});
}
else
{
// Older hardware → acrylic fallback.
m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::DesktopAcrylicBackdrop{});
}

m_window.Activate();

// Floating indicator pill — always-on-top tool window. Owns its own
// poll loop against the audio-core FFI; survives without MainWindow
// being focused.
//
// Per current design the pill is *invisible* in Idle + Armed states,
// so we do NOT call Activate() here — visibility is driven entirely
// by AppWindow.Show()/Hide() in TransitionTo. Window construction is
// enough to wire composition + the dispatcher queue.
m_indicator = winrt::make<winrt::yip::implementation::IndicatorWindow>();
void App::OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& /*args*/)
{
m_window = winrt::make<winrt::yip::implementation::MainWindow>();
m_window.Title(L"Yip");

// Mica backdrop — Window owns the controller lifetime since 1.4.
if (winrt::Microsoft::UI::Composition::SystemBackdrops::MicaController::IsSupported()) {
m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::MicaBackdrop{});
} else {
// Older hardware → acrylic fallback.
m_window.SystemBackdrop(winrt::Microsoft::UI::Xaml::Media::DesktopAcrylicBackdrop{});
}

m_window.Activate();

// Floating indicator pill — always-on-top tool window. Owns its own
// poll loop against the audio-core FFI; survives without MainWindow
// being focused.
//
// Per current design the pill is *invisible* in Idle + Armed states,
// so we do NOT call Activate() here — visibility is driven entirely
// by AppWindow.Show()/Hide() in TransitionTo. Window construction is
// enough to wire composition + the dispatcher queue.
m_indicator = winrt::make<winrt::yip::implementation::IndicatorWindow>();
}
} // namespace winrt::yip::implementation
20 changes: 9 additions & 11 deletions yip-app/App.xaml.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@

#include "App.xaml.g.h"

namespace winrt::yip::implementation
{
struct App : AppT<App>
{
App();
namespace winrt::yip::implementation {
struct App : AppT<App> {
App();

void OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& args);
void OnLaunched(winrt::Microsoft::UI::Xaml::LaunchActivatedEventArgs const& args);

private:
winrt::Microsoft::UI::Xaml::Window m_window{ nullptr };
winrt::Microsoft::UI::Xaml::Window m_indicator{ nullptr };
};
}
private:
winrt::Microsoft::UI::Xaml::Window m_window{nullptr};
winrt::Microsoft::UI::Xaml::Window m_indicator{nullptr};
};
} // namespace winrt::yip::implementation
146 changes: 79 additions & 67 deletions yip-app/AudioCoreInterop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,91 +5,103 @@
#include <mmdeviceapi.h>
#include <wrl/implements.h>

using Microsoft::WRL::ClassicCom;
using Microsoft::WRL::ComPtr;
using Microsoft::WRL::RuntimeClass;
using Microsoft::WRL::RuntimeClassFlags;
using Microsoft::WRL::ClassicCom;

namespace yip::interop
namespace yip::interop {
std::vector<Device> ListDevices()
{
std::vector<Device> ListDevices()
{
std::vector<Device> out;
DeviceInfo* arr = nullptr;
size_t len = 0;
const auto status = rec_list_devices(&arr, &len);
if (status != REC_STATUS_OK || !arr) {
return out;
}
out.reserve(len);
for (size_t i = 0; i < len; ++i) {
Device d;
d.id = arr[i].id ? arr[i].id : "";
d.name = arr[i].name ? arr[i].name : "";
d.isCapture = arr[i].kind != 0;
d.isDefault = arr[i].is_default != 0;
out.push_back(std::move(d));
}
rec_free_devices(arr, len);
std::vector<Device> out;
DeviceInfo* arr = nullptr;
size_t len = 0;
const auto status = rec_list_devices(&arr, &len);
if (status != REC_STATUS_OK || !arr) {
return out;
}
out.reserve(len);
for (size_t i = 0; i < len; ++i) {
Device d;
d.id = arr[i].id ? arr[i].id : "";
d.name = arr[i].name ? arr[i].name : "";
d.isCapture = arr[i].kind != 0;
d.isDefault = arr[i].is_default != 0;
out.push_back(std::move(d));
}
rec_free_devices(arr, len);
return out;
}

// ---- DeviceWatcher implementation ----
// ---- DeviceWatcher implementation ----

namespace {
namespace {

class NotificationClient
: public RuntimeClass<RuntimeClassFlags<ClassicCom>, IMMNotificationClient>
{
public:
explicit NotificationClient(std::function<void()> cb) : m_cb(std::move(cb)) {}
class NotificationClient : public RuntimeClass<RuntimeClassFlags<ClassicCom>, IMMNotificationClient> {
public:
explicit NotificationClient(std::function<void()> cb) : m_cb(std::move(cb)) {}

IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR, DWORD) override { fire(); return S_OK; }
IFACEMETHODIMP OnDeviceAdded(LPCWSTR) override { fire(); return S_OK; }
IFACEMETHODIMP OnDeviceRemoved(LPCWSTR) override { fire(); return S_OK; }
IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow, ERole, LPCWSTR) override { fire(); return S_OK; }
IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; }
IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR, DWORD) override
{
fire();
return S_OK;
}
IFACEMETHODIMP OnDeviceAdded(LPCWSTR) override
{
fire();
return S_OK;
}
IFACEMETHODIMP OnDeviceRemoved(LPCWSTR) override
{
fire();
return S_OK;
}
IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow, ERole, LPCWSTR) override
{
fire();
return S_OK;
}
IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; }

private:
void fire() {
// Debounce: only forward if a callback isn't already in flight.
if (!m_inflight.exchange(true)) {
if (m_cb) m_cb();
m_inflight.store(false);
}
private:
void fire()
{
// Debounce: only forward if a callback isn't already in flight.
if (!m_inflight.exchange(true)) {
if (m_cb) m_cb();
m_inflight.store(false);
}
}

std::function<void()> m_cb;
std::atomic<bool> m_inflight{ false };
};
std::function<void()> m_cb;
std::atomic<bool> m_inflight{false};
};

} // namespace
} // namespace

struct DeviceWatcher::Impl
{
ComPtr<IMMDeviceEnumerator> enumerator;
ComPtr<NotificationClient> client;
};
struct DeviceWatcher::Impl {
ComPtr<IMMDeviceEnumerator> enumerator;
ComPtr<NotificationClient> client;
};

DeviceWatcher::DeviceWatcher(std::function<void()> onChanged)
: m_impl(std::make_unique<Impl>())
{
HRESULT hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (hr != S_OK && hr != S_FALSE && hr != RPC_E_CHANGED_MODE) {
return;
}
hr = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
IID_PPV_ARGS(m_impl->enumerator.GetAddressOf()));
if (FAILED(hr)) return;
m_impl->client = Microsoft::WRL::Make<NotificationClient>(std::move(onChanged));
if (!m_impl->client) return;
m_impl->enumerator->RegisterEndpointNotificationCallback(m_impl->client.Get());
DeviceWatcher::DeviceWatcher(std::function<void()> onChanged) : m_impl(std::make_unique<Impl>())
{
HRESULT hr = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (hr != S_OK && hr != S_FALSE && hr != RPC_E_CHANGED_MODE) {
return;
}
hr = ::CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL,
IID_PPV_ARGS(m_impl->enumerator.GetAddressOf()));
if (FAILED(hr)) return;
m_impl->client = Microsoft::WRL::Make<NotificationClient>(std::move(onChanged));
if (!m_impl->client) return;
m_impl->enumerator->RegisterEndpointNotificationCallback(m_impl->client.Get());
}

DeviceWatcher::~DeviceWatcher()
{
if (m_impl && m_impl->enumerator && m_impl->client) {
m_impl->enumerator->UnregisterEndpointNotificationCallback(m_impl->client.Get());
}
DeviceWatcher::~DeviceWatcher()
{
if (m_impl && m_impl->enumerator && m_impl->client) {
m_impl->enumerator->UnregisterEndpointNotificationCallback(m_impl->client.Get());
}
}
} // namespace yip::interop
51 changes: 24 additions & 27 deletions yip-app/AudioCoreInterop.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,30 @@

#include "audio_core.h"

namespace yip::interop
{
struct Device
{
std::string id; // utf-8
std::string name; // utf-8
bool isCapture{ true };
bool isDefault{ false };
};
namespace yip::interop {
struct Device {
std::string id; // utf-8
std::string name; // utf-8
bool isCapture{true};
bool isDefault{false};
};

// Enumerate endpoints. On error returns an empty vector and silently
// swallows — callers can use `rec_last_error()` to read the message.
std::vector<Device> ListDevices();
// Enumerate endpoints. On error returns an empty vector and silently
// swallows — callers can use `rec_last_error()` to read the message.
std::vector<Device> ListDevices();

// RAII watcher: registers an `IMMNotificationClient` against the system
// device enumerator. `onChanged` is invoked from a WASAPI worker thread
// — callers must marshal to the UI dispatcher themselves.
class DeviceWatcher
{
public:
explicit DeviceWatcher(std::function<void()> onChanged);
~DeviceWatcher();
DeviceWatcher(const DeviceWatcher&) = delete;
DeviceWatcher& operator=(const DeviceWatcher&) = delete;
// RAII watcher: registers an `IMMNotificationClient` against the system
// device enumerator. `onChanged` is invoked from a WASAPI worker thread
// — callers must marshal to the UI dispatcher themselves.
class DeviceWatcher {
public:
explicit DeviceWatcher(std::function<void()> onChanged);
~DeviceWatcher();
DeviceWatcher(const DeviceWatcher&) = delete;
DeviceWatcher& operator=(const DeviceWatcher&) = delete;

private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
}
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
} // namespace yip::interop
Loading
Loading