diff --git a/real-app/real-app.vcxproj b/real-app/real-app.vcxproj index e114276..cc3665d 100644 --- a/real-app/real-app.vcxproj +++ b/real-app/real-app.vcxproj @@ -18,9 +18,15 @@ + + + + + + @@ -30,9 +36,15 @@ + + + + + + @@ -101,7 +113,7 @@ MultiThreadedDLL - NotSet + Windows libcurl_a.lib;Ws2_32.lib;crypt32.lib;Wldap32.lib;Normaliz.lib;%(AdditionalDependencies) @@ -123,7 +135,7 @@ true true - NotSet + Windows libcurl_a.lib;Ws2_32.lib;crypt32.lib;Wldap32.lib;Normaliz.lib;%(AdditionalDependencies) diff --git a/real-app/src/OStreamSink.cpp b/real-app/src/OStreamSink.cpp new file mode 100644 index 0000000..849e873 --- /dev/null +++ b/real-app/src/OStreamSink.cpp @@ -0,0 +1,23 @@ +#include "OStreamSink.h" + +using namespace miniant::Spdlog; + +OStreamSink::OStreamSink(std::shared_ptr outputStream, bool forceFlush): + m_outputStream(std::move(outputStream)), + m_forceFlush(forceFlush) {} + +std::mutex& OStreamSink::GetMutex() { + return mutex_; +} + +void OStreamSink::sink_it_(const spdlog::details::log_msg& message) { + fmt::memory_buffer formattedMessage; + sink::formatter_->format(message, formattedMessage); + m_outputStream->write(formattedMessage.data(), static_cast(formattedMessage.size())); + if (m_forceFlush) + m_outputStream->flush(); +} + +void OStreamSink::flush_() { + m_outputStream->flush(); +} diff --git a/real-app/src/OStreamSink.h b/real-app/src/OStreamSink.h new file mode 100644 index 0000000..da05edb --- /dev/null +++ b/real-app/src/OStreamSink.h @@ -0,0 +1,27 @@ +#pragma once + +// Work-around for compilation error caused by spdlog using Windows headers +#include + +#include + +#include + +namespace miniant::Spdlog { + +class OStreamSink : public spdlog::sinks::base_sink { +public: + explicit OStreamSink(std::shared_ptr outputStream, bool forceFlush=false); + + std::mutex& GetMutex(); + +protected: + void sink_it_(const spdlog::details::log_msg& message) override; + void flush_() override; + +private: + std::shared_ptr m_outputStream; + bool m_forceFlush; +}; + +} diff --git a/real-app/src/Windows/Console.cpp b/real-app/src/Windows/Console.cpp new file mode 100644 index 0000000..6ecf7b3 --- /dev/null +++ b/real-app/src/Windows/Console.cpp @@ -0,0 +1,26 @@ +#include "Console.h" + +#include + +#include + +using namespace miniant::Windows; + +Console::Console(std::function onShow): + m_onShow(std::move(onShow)) {} + +void Console::Open() { + ::AllocConsole(); + + FILE* dummy; + freopen_s(&dummy, "conout$", "w", stdout); + std::cout.clear(); + + if (m_onShow) + m_onShow(); +} + +void Console::Close() { + ::SendMessage(::GetConsoleWindow(), WM_CLOSE, 0, 0); + ::FreeConsole(); +} diff --git a/real-app/src/Windows/Console.h b/real-app/src/Windows/Console.h new file mode 100644 index 0000000..8ac9296 --- /dev/null +++ b/real-app/src/Windows/Console.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace miniant::Windows { + +class Console { +public: + Console(std::function onShow); + + void Open(); + void Close(); + +private: + std::function m_onShow; +}; + +} diff --git a/real-app/src/Windows/Exception.cpp b/real-app/src/Windows/Exception.cpp new file mode 100644 index 0000000..e58dfbf --- /dev/null +++ b/real-app/src/Windows/Exception.cpp @@ -0,0 +1,14 @@ +#include "Exception.h" + +#include + +#include + +using namespace miniant::Windows; + +std::string GetErrorMessage() noexcept { + DWORD lastError = ::GetLastError(); + return "Last error: " + std::to_string(lastError); +} + +Exception::Exception() noexcept: std::runtime_error(GetErrorMessage()) {} diff --git a/real-app/src/Windows/Exception.h b/real-app/src/Windows/Exception.h new file mode 100644 index 0000000..82d5d8e --- /dev/null +++ b/real-app/src/Windows/Exception.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace miniant::Windows { + +class Exception : std::runtime_error { +public: + Exception() noexcept; +}; + +} diff --git a/real-app/src/Windows/GlobalWindowProcedure.cpp b/real-app/src/Windows/GlobalWindowProcedure.cpp new file mode 100644 index 0000000..b32fa2a --- /dev/null +++ b/real-app/src/Windows/GlobalWindowProcedure.cpp @@ -0,0 +1,46 @@ +#include "GlobalWindowProcedure.h" + +#include "Exception.h" + +using namespace miniant::Windows; + +std::unordered_map GlobalWindowProcedure::s_windowProcedureMap; + +UINT GlobalWindowProcedure::GetFreeEventId() noexcept { + static UINT nextFreeEventId = WM_USER; + return nextFreeEventId++; +} + +WNDCLASS GlobalWindowProcedure::RegisterWindowClass(LPCTSTR lpszClassName) { + WNDCLASS wc = {}; + wc.lpszClassName = lpszClassName; + wc.lpfnWndProc = &WndProc; + if (!::GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast(&WndProc), &wc.hInstance)) + throw Exception(); + + if (::RegisterClass(&wc) == 0) + throw Exception(); + + return wc; +} + +void GlobalWindowProcedure::SetWindowProcedure(HWND hWnd, WindowProcedure procedure) { + s_windowProcedureMap[hWnd] = procedure; +} + +LRESULT CALLBACK GlobalWindowProcedure::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + WindowProcedure* windowProcedure; + try { + windowProcedure = &s_windowProcedureMap.at(hWnd); + } catch (std::out_of_range&) { + return ::DefWindowProc(hWnd, uMsg, wParam, lParam); + } + + if (*windowProcedure) { + std::optional lResult = (*windowProcedure)(hWnd, uMsg, wParam, lParam); + if (lResult) + return lResult.value(); + } + + return ::DefWindowProc(hWnd, uMsg, wParam, lParam); +} diff --git a/real-app/src/Windows/GlobalWindowProcedure.h b/real-app/src/Windows/GlobalWindowProcedure.h new file mode 100644 index 0000000..3c07ad3 --- /dev/null +++ b/real-app/src/Windows/GlobalWindowProcedure.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include +#include + +namespace miniant::Windows { + +class GlobalWindowProcedure { +public: + using WindowProcedure = std::function(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lparam)>; + + static UINT GetFreeEventId() noexcept; + + static WNDCLASS RegisterWindowClass(LPCTSTR lpszClassName); + static void SetWindowProcedure(HWND hWnd, WindowProcedure procedure); + +private: + static std::unordered_map s_windowProcedureMap; + + static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); +}; + +} diff --git a/real-app/src/Windows/MessagingWindow.cpp b/real-app/src/Windows/MessagingWindow.cpp new file mode 100644 index 0000000..c5c588f --- /dev/null +++ b/real-app/src/Windows/MessagingWindow.cpp @@ -0,0 +1,45 @@ +#include "MessagingWindow.h" + +#include "Exception.h" +#include "GlobalWindowProcedure.h" + +using namespace miniant::Windows; + +constexpr TCHAR CLASS_NAME[] = TEXT("MessagingWindow"); + +MessagingWindow::MessagingWindow() { + m_hWnd = ::CreateWindowEx( + 0, + CLASS_NAME, + NULL, + 0, + 0, 0, 0, 0, + HWND_MESSAGE, + NULL, + GlobalWindowProcedure::RegisterWindowClass(CLASS_NAME).hInstance, + NULL); + if (m_hWnd == NULL) + throw Exception(); + + GlobalWindowProcedure::SetWindowProcedure(m_hWnd, [this](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + auto eventHandler = m_eventHandlerMap.find(uMsg); + if (eventHandler != m_eventHandlerMap.end()) { + if (eventHandler->second) + return eventHandler->second(*this, wParam, lParam); + } + + return std::optional(); + }); +} + +MessagingWindow::~MessagingWindow() noexcept { + GlobalWindowProcedure::SetWindowProcedure(m_hWnd, nullptr); +} + +HWND MessagingWindow::GetHWindow() noexcept { + return m_hWnd; +} + +void MessagingWindow::SetEventHandler(UINT event, EventHandler handler) { + m_eventHandlerMap[event] = std::move(handler); +} diff --git a/real-app/src/Windows/MessagingWindow.h b/real-app/src/Windows/MessagingWindow.h new file mode 100644 index 0000000..3c8273c --- /dev/null +++ b/real-app/src/Windows/MessagingWindow.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include +#include +#include + +namespace miniant::Windows { + +class MessagingWindow { +public: + using EventHandler = std::function(MessagingWindow&, WPARAM, LPARAM)>; + + MessagingWindow(); + ~MessagingWindow() noexcept; + + HWND GetHWindow() noexcept; + + void SetEventHandler(UINT event, EventHandler handler); + +private: + HWND m_hWnd; + std::unordered_map m_eventHandlerMap; +}; + +} diff --git a/real-app/src/Windows/TrayIcon.cpp b/real-app/src/Windows/TrayIcon.cpp new file mode 100644 index 0000000..9812924 --- /dev/null +++ b/real-app/src/Windows/TrayIcon.cpp @@ -0,0 +1,56 @@ +#include "TrayIcon.h" + +#include "Exception.h" +#include "GlobalWindowProcedure.h" + +using namespace miniant::Windows; + +TrayIcon::TrayIcon(MessagingWindow& window, HICON hIcon): + m_window(window) { + m_data = {}; + m_data.cbSize = sizeof(m_data); + m_data.uVersion = NOTIFYICON_VERSION_4; + m_data.uFlags = NIF_ICON | NIF_MESSAGE; + m_data.hWnd = window.GetHWindow(); + m_data.hIcon = hIcon; + m_data.uID = 1; + m_data.uCallbackMessage = GlobalWindowProcedure::GetFreeEventId(); + + window.SetEventHandler(m_data.uCallbackMessage, [this](const MessagingWindow& window, WPARAM wParam, LPARAM lParam) { + switch (LOWORD(lParam)) { + case WM_LBUTTONUP: + if (m_lButtonUpHandler) + m_lButtonUpHandler(*this); + break; + + default: + break; + } + + return std::optional(); + }); +} + +TrayIcon::~TrayIcon() noexcept { + m_window.SetEventHandler(m_data.uCallbackMessage, nullptr); + ::Shell_NotifyIcon(NIM_DELETE, &m_data); +} + +void TrayIcon::Show() { + if (!::Shell_NotifyIcon(NIM_ADD, &m_data)) + throw Exception(); + + if (!::Shell_NotifyIcon(NIM_SETVERSION, &m_data)) { + ::Shell_NotifyIcon(NIM_DELETE, &m_data); + throw Exception(); + } +} + +void TrayIcon::Hide() { + if (!::Shell_NotifyIcon(NIM_DELETE, &m_data)) + throw Exception(); +} + +void TrayIcon::SetLButtonUpHandler(TrayEventHandler handler) noexcept { + m_lButtonUpHandler = std::move(handler); +} diff --git a/real-app/src/Windows/TrayIcon.h b/real-app/src/Windows/TrayIcon.h new file mode 100644 index 0000000..75504c9 --- /dev/null +++ b/real-app/src/Windows/TrayIcon.h @@ -0,0 +1,25 @@ +#pragma once + +#include "MessagingWindow.h" + +namespace miniant::Windows { + +class TrayIcon { +public: + using TrayEventHandler = std::function(TrayIcon&)>; + + TrayIcon(MessagingWindow& window, HICON hIcon); + ~TrayIcon() noexcept; + + void Show(); + void Hide(); + + void SetLButtonUpHandler(TrayEventHandler handler) noexcept; + +private: + NOTIFYICONDATA m_data; + MessagingWindow m_window; + TrayEventHandler m_lButtonUpHandler; +}; + +} diff --git a/real-app/src/main.cpp b/real-app/src/main.cpp index 34e74de..dff3397 100644 --- a/real-app/src/main.cpp +++ b/real-app/src/main.cpp @@ -1,35 +1,89 @@ #include "AutoUpdater.h" +#include "OStreamSink.h" + +#include "Windows/Console.h" #include "Windows/MinimumLatencyAudioClient.h" +#include "Windows/MessagingWindow.h" +#include "Windows/TrayIcon.h" + +#include "../resource.h" #include -#include + +#include +#include #include + #include +#include using namespace miniant::AutoUpdater; +using namespace miniant::Spdlog; +using namespace miniant::Windows; using namespace miniant::Windows::WasapiLatency; constexpr Version APP_VERSION(0, 1, 3); +constexpr TCHAR COMMAND_LINE_OPTION_TRAY[] = TEXT("--tray"); void WaitForAnyKey(const std::string& message) { while (_kbhit()) _getch(); - + spdlog::get("app_out")->info(message); _getch(); } -int main(int argc, char** argv) { - auto sink = std::make_shared(std::cout); +void DisplayExitMessage(int errorCode) { + if (errorCode == 0) + WaitForAnyKey("\nPress any key to disable and exit . . ."); + else + WaitForAnyKey("\nPress any key to exit . . ."); +} + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { + auto oss = std::make_shared(); + auto sink = std::make_shared(oss, true); auto app_out = std::make_shared("app_out", sink); app_out->set_pattern("%v"); spdlog::register_logger(app_out); + app_out->info("Minimum audio latency enabled on the DEFAULT playback device!\n"); + + auto console = std::make_shared([=] { + std::lock_guard lock(sink->GetMutex()); + std::cout << oss->str(); + std::cout.flush(); + oss->set_rdbuf(std::cout.rdbuf()); + }); + + std::unique_ptr window; + std::unique_ptr trayIcon; + + std::wstring commandLine(pCmdLine); + int errorCode = 0; + + if (commandLine == COMMAND_LINE_OPTION_TRAY) { + window = std::make_unique(); + HICON hIcon = ::LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); + trayIcon = std::make_unique(*window, hIcon); + trayIcon->SetLButtonUpHandler([=, &errorCode](TrayIcon& trayIcon) { + trayIcon.Hide(); + console->Open(); + + DisplayExitMessage(errorCode); + + console->Close(); + return std::optional(); + }); + trayIcon->Show(); + } else { + console->Open(); + } app_out->info("REAL - REduce Audio Latency {}, mini)(ant, 2018", APP_VERSION.ToString()); app_out->info("Project: https://github.com/miniant-git/REAL\n"); - int errorCode = MinimumLatencyAudioClient().Start(); + errorCode = MinimumLatencyAudioClient().Start(); if (errorCode == 0) app_out->info("Minimum audio latency enabled on the DEFAULT playback device!\n"); else @@ -38,10 +92,15 @@ int main(int argc, char** argv) { app_out->info("Checking for updates..."); AutoUpdater(APP_VERSION).Update(); - if (errorCode == 0) - WaitForAnyKey("\nPress any key to disable and exit . . ."); - else - WaitForAnyKey("\nPress any key to exit . . ."); + if (commandLine == COMMAND_LINE_OPTION_TRAY) { + MSG msg; + while (::GetMessage(&msg, NULL, 0, 0) > 0) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } else { + DisplayExitMessage(errorCode); + } return errorCode; }