//Disables console window #if !_DEBUG #pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:mainCRTStartup") #endif // we need commctrl v6 for LoadIconMetric() // see https://github.com/microsoft/Windows-classic-samples/blob/main/Samples/Win7Samples/winui/shell/appshellintegration/NotificationIcon/NotificationIcon.cpp #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") #pragma comment(lib, "comctl32.lib") #include #include #include #include #include #include #include #include "Util.h" #include "AudioApi.h" #include "Settings.h" #include "resource.h" #include "AsuroTool.h" class __declspec(uuid("3bc52579-15fd-43bb-9686-6273c238535e")) TrayGUID; UINT const WMAPP_NOTIFYCALLBACK = WM_APP + 1; UINT const WMAPP_HIDEFLYOUT = WM_APP + 2; DrawData* hackyDrawData; ApplicationData* hackyAppData; bool justDocked = false; bool isHidden = false; int main() { ApplicationData applicationData{}; ImGuiCallbacks callbacks{}; callbacks.initFunc = std::bind(init, std::placeholders::_1, std::ref(applicationData)); callbacks.drawFunc = std::bind(draw, std::placeholders::_1, std::ref(applicationData)); callbacks.cleanupFunc = std::bind(cleanup, std::placeholders::_1, std::ref(applicationData)); startImgui(callbacks, "Audio Thingy", 600, 400); } LRESULT CALLBACK trayIconEventHandler(int code, WPARAM wParam, LPARAM lParam) { if (code >= 0) { CWPSTRUCT* data = (CWPSTRUCT*)lParam; if (data->message == WMAPP_NOTIFYCALLBACK) { switch (data->lParam) { case NIN_SELECT: glfwShowWindow(hackyDrawData->window); glfwRestoreWindow(hackyDrawData->window); break; } } } return CallNextHookEx(NULL, code, wParam, lParam); } void init(DrawData& drawData, ApplicationData& appData) { // sad :( hackyDrawData = &drawData; hackyAppData = &appData; // Load text font ImGuiIO& io = ImGui::GetIO(); io.Fonts->AddFontFromFileTTF("Montserrat-Regular.ttf", 18.0f); // Load icon font static const ImWchar icons_ranges[] = { 0xEA01, 0xF2DF, 0 }; ImFontConfig icons_config; icons_config.MergeMode = true; icons_config.PixelSnapH = true; io.Fonts->AddFontFromFileTTF("remixicon.ttf", 14.0f, &icons_config, icons_ranges); // Set window icon HINSTANCE instance = GetModuleHandle(NULL); LPWSTR iconId = MAKEINTRESOURCE(IDI_ICON1); HANDLE iconLarge = LoadImageW(instance, iconId, IMAGE_ICON, 64, 64, 0); HANDLE iconSmall = LoadImageW(instance, iconId, IMAGE_ICON, 32, 32, 0); SendMessage(drawData.window_handle, WM_SETICON, ICON_BIG, (LPARAM)iconLarge); SendMessage(drawData.window_handle, WM_SETICON, ICON_SMALL, (LPARAM)iconSmall); SendMessage(drawData.window_handle, WM_SETICON, ICON_SMALL2, (LPARAM)iconSmall); // Set tray icon NOTIFYICONDATA nid = { sizeof(nid) }; nid.hWnd = drawData.window_handle; nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE | NIF_SHOWTIP | NIF_GUID; nid.guidItem = __uuidof(TrayGUID); nid.uCallbackMessage = WMAPP_NOTIFYCALLBACK; HRESULT iconResult; iconResult = LoadIconMetric(instance, iconId, LIM_SMALL, &nid.hIcon); isError(iconResult, "Failed to load tray icon image: "); iconResult = LoadString(instance, IDS_STRING_TOOLTIP, nid.szTip, ARRAYSIZE(nid.szTip)); isError(iconResult, "Failed to load tray icon text: "); Shell_NotifyIcon(NIM_ADD, &nid); nid.uVersion = NOTIFYICON_VERSION_4; Shell_NotifyIcon(NIM_SETVERSION, &nid); if (!SetWindowsHookEx(WH_CALLWNDPROC, trayIconEventHandler, instance, GetCurrentThreadId())) { std::cout << "Failed to hook tray icon events: " << std::hex << GetLastError() << std::endl; } // Set window minimize behavior glfwSetWindowIconifyCallback(drawData.window, [](GLFWwindow* window, int isIconified) { if (isIconified && hackyAppData->settings.docked) { glfwHideWindow(window); } isHidden = isIconified; }); glfwSetWindowFocusCallback(drawData.window, [](GLFWwindow* window, int isFocused) { if (!isFocused && hackyAppData->settings.docked && !justDocked) { glfwIconifyWindow(window); } }); // Load settings initSettings(drawData, appData); // Set up audio device api HRESULT audioResult; audioResult = CoInitializeEx(NULL, COINIT_MULTITHREADED); isError(audioResult, "Failed to initialize COM: "); audioResult = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&appData.audioData->deviceEnumerator)); isError(audioResult, "Failed to set up audio device enumerator: "); appData.audioData->audioNotificationListener = new AudioNotificationListener(appData.audioData); audioResult = appData.audioData->deviceEnumerator->RegisterEndpointNotificationCallback(appData.audioData->audioNotificationListener); isError(audioResult, "Failed to register audio notification listener: "); reloadDeviceLists(*appData.audioData); } void draw(DrawData& drawData, ApplicationData& appData) { justDocked = false; // sad :( hackyDrawData = &drawData; hackyAppData = &appData; // Actual Drawing if (isHidden) { glfwWaitEvents(); return; } float customYCursor = 0; ImVec2 viewportSize = ImGui::GetMainViewport()->Size; // Menu Bar customYCursor += menuBar(drawData, appData).y; // Playback Devices ImGui::SetNextWindowPos(ImVec2(0, customYCursor)); ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0)); customYCursor += audioDeviceWindow(appData, appData.audioData->playbackDevices, " \xEE\xB8\x84 Playback").y; customYCursor += 5.; // Recording devices ImGui::SetNextWindowPos(ImVec2(0, customYCursor)); ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0)); customYCursor += audioDeviceWindow(appData, appData.audioData->recordingDevices, " \xEE\xBD\x8F Recording").y; // Resize viewport drawData.window_size.y = customYCursor; if (appData.settings.docked) { int monitorX, monitorY, monitorW, monitorH; glfwGetMonitorWorkarea(glfwGetPrimaryMonitor(), &monitorX, &monitorY, &monitorW, &monitorH); glfwSetWindowPos(drawData.window, monitorX + monitorW - drawData.window_size.x, monitorY + monitorH - drawData.window_size.y); } } void cleanup(DrawData& drawData, ApplicationData& appData) { // Remove tray icon NOTIFYICONDATA nid = { sizeof(nid) }; nid.uFlags = NIF_GUID; nid.guidItem = __uuidof(TrayGUID); bool test = Shell_NotifyIcon(NIM_DELETE, &nid); } ImVec2 menuBar(DrawData& drawData, ApplicationData& appData) { ImVec2 size{}; bool closeMenu = false; if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("Settings")) { bool prevDocked = appData.settings.docked; ImGui::Checkbox("Docked", &appData.settings.docked); if (appData.settings.docked != prevDocked) { closeMenu = true; updateDocked(drawData, appData); } ImGui::Checkbox("Show Disabled Devices", &appData.settings.showDisabledDevices); if (ImGui::Checkbox("Autostart", &appData.settings.autostart)) { setAutostart(appData.settings.autostart); } ImGui::EndMenu(); } if (ImGui::BeginMenu("Debug")) { if (ImGui::Button("Manual Refresh")) { reloadDeviceLists(*appData.audioData); } ImGui::EndMenu(); } if (appData.settings.docked) { ImVec2 availableSpace = ImGui::GetContentRegionAvail(); ImVec2 cursorPos = ImGui::GetCursorPos(); ImGui::SetCursorPosX(cursorPos.x + availableSpace.x - 33.); if (ImGui::SmallButton("-")) { glfwIconifyWindow(drawData.window); } if (ImGui::SmallButton("x")) { glfwSetWindowShouldClose(drawData.window, GLFW_TRUE); } } size = ImGui::GetWindowSize(); ImGui::EndMainMenuBar(); } if (closeMenu) { ImGui::SetWindowFocus(); } return size; } bool customButton(const char* id_start, const char* id_end, const char* title, bool visible) { std::string buttonId(id_start); buttonId.append(id_end); bool result = false; if (visible) { ImGui::PushID(buttonId.c_str()); result = ImGui::SmallButton(title); ImGui::PopID(); } else { ImGui::InvisibleButton(buttonId.c_str(), ImGui::CalcTextSize(title)); } return result; } ImVec2 audioDeviceWindow(ApplicationData& appData, std::vector& deviceList, const char* title) { if (ImGui::Begin(title, 0, ImGuiWindowFlags_NoResize)) { if (ImGui::BeginTable("DeviceTable", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_Borders | ImGuiTableFlags_NoSavedSettings)) { ImGui::TableSetupColumn("Devices", ImGuiTableColumnFlags_WidthStretch, 3.); ImGui::TableSetupColumn("Volume", ImGuiTableColumnFlags_WidthStretch, 1.); ImGui::TableSetupColumn("Defaults", ImGuiTableColumnFlags_WidthFixed, 55.); ImGui::TableHeadersRow(); for (auto& dev : deviceList) { std::string deviceIdUtf8 = utf8Encode(dev.id); if (dev.state == DEVICE_STATE_ACTIVE) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1., 1., 1., 1.)); } else if (!appData.settings.showDisabledDevices) { continue; } else { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(.7, .7, .7, 1.)); } // Device Name ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::Text(dev.name.c_str()); // Volume ImGui::TableNextColumn(); if (dev.state == DEVICE_STATE_ACTIVE) { // Log scale because it looks better (no actual reason for these exact numbers) float meterValue = log10f(getMeterValue(dev.meterInterface) * 9. + 1.); auto drawList = ImGui::GetWindowDrawList(); ImVec2 windowPos = ImGui::GetWindowPos(); ImVec2 cursorPos = ImGui::GetCursorScreenPos(); ImVec2 space = ImGui::GetContentRegionAvail(); float lineY = cursorPos.y + ImGui::GetTextLineHeight() / 2. + 2.; const float linePaddingX = 3.; cursorPos.x += linePaddingX; drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX), lineY), IM_COL32(120, 120, 120, 255), 2.); drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX) * meterValue, lineY), IM_COL32(200, 200, 255, 255), 3.); float volume = getVolume(dev.volumeInterface); float prevVolume = volume; ImGui::SetNextItemWidth(space.x); ImGui::PushID(dev.device); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0)); ImGui::SliderFloat("", &volume, 0., 1., "", ImGuiSliderFlags_NoRoundToFormat | ImGuiSliderFlags_AlwaysClamp); ImGui::PopStyleColor(); ImGui::PopID(); if (prevVolume != volume) { setVolume(dev.volumeInterface, volume); } } // Defaults ImGui::TableNextColumn(); if (dev.state == DEVICE_STATE_ACTIVE) { if (dev.isDefaultConsole) { drawCircle(5, IM_COL32(50, 50, 222, 255)); } if (customButton("bn_d_", deviceIdUtf8.c_str(), "\xEE\xBE\x82", !dev.isDefaultConsole)) { setDefaultAudioDevice(*appData.audioData, dev.id.c_str(), ERole::eConsole); } ImGui::SameLine(); if (dev.isDefaultCommunication) { drawCircle(5, IM_COL32(222, 50, 50, 255)); } if (customButton("bn_c_", deviceIdUtf8.c_str(), "\xEE\xBF\xA9", !dev.isDefaultCommunication)) { setDefaultAudioDevice(*appData.audioData, dev.id.c_str(), ERole::eCommunications); } } ImGui::PopStyleColor(); } ImGui::EndTable(); } } ImVec2 size = ImGui::GetWindowSize(); ImGui::End(); return size; } void drawCircle(float radius, ImU32 color) { ImGui::Dummy(ImVec2(0, 0)); ImGui::SameLine(); auto drawList = ImGui::GetWindowDrawList(); ImVec2 cursorPos = ImGui::GetCursorPos(); ImVec2 windowPos = ImGui::GetWindowPos(); drawList->AddCircleFilled(ImVec2(cursorPos.x + windowPos.x, cursorPos.y + windowPos.y + ImGui::GetTextLineHeight() / 2.), radius, color); }