445 lines
13 KiB
C++
445 lines
13 KiB
C++
//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")
|
|
#pragma comment(lib, "rpcrt4.lib")
|
|
|
|
#include "Util.h"
|
|
#include "AudioApi.h"
|
|
#include "Settings.h"
|
|
#include "AsuroTool.h"
|
|
#include "WindowsShell.h"
|
|
|
|
#include <array>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <format>
|
|
#include <ctime>
|
|
|
|
#define MAX_FONT_PATH_LENGTH 2048
|
|
|
|
// Globals for use in callbacks
|
|
DrawData* gDrawData;
|
|
ApplicationData* gAppData;
|
|
bool justDocked = false;
|
|
bool isHidden = false;
|
|
|
|
int main()
|
|
{
|
|
std::wstring appDir;
|
|
getAppDir(appDir);
|
|
if (_wchdir(appDir.c_str()) != 0)
|
|
{
|
|
std::cout << "Failed to set working dir." << std::endl;
|
|
}
|
|
|
|
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", 700, 400);
|
|
}
|
|
|
|
void init(DrawData& drawData, ApplicationData& appData)
|
|
{
|
|
std::wstring appPath;
|
|
getAppDir(appPath);
|
|
char appPathStr[MAX_FONT_PATH_LENGTH];
|
|
HRESULT convResult = WideCharToMultiByte(CP_UTF8, NULL, &appPath[0], -1, appPathStr, MAX_FONT_PATH_LENGTH, NULL, nullptr);
|
|
isError(convResult, "Failed to convert path: ");
|
|
|
|
// sad :(
|
|
gDrawData = &drawData;
|
|
gAppData = &appData;
|
|
|
|
// Load text font
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
std::string fontPath = std::string(appPathStr);
|
|
fontPath.append("\\Montserrat-Regular.ttf");
|
|
io.Fonts->AddFontFromFileTTF(fontPath.c_str(), 18.0f);
|
|
|
|
// Load icon font
|
|
static const ImWchar icons_ranges[] = { 0xEA01, 0xF2DF, 0 };
|
|
ImFontConfig icons_config;
|
|
icons_config.MergeMode = true;
|
|
icons_config.PixelSnapH = true;
|
|
icons_config.GlyphOffset = { 0., 2. };
|
|
std::string iconFontPath = std::string(appPathStr);
|
|
iconFontPath.append("\\remixicon.ttf");
|
|
io.Fonts->AddFontFromFileTTF(iconFontPath.c_str(), 14.0f, &icons_config, icons_ranges);
|
|
|
|
|
|
initShell(drawData);
|
|
initSettings(drawData, appData);
|
|
initAudio(appData);
|
|
}
|
|
|
|
void draw(DrawData& drawData, ApplicationData& appData)
|
|
{
|
|
justDocked = false;
|
|
|
|
if (isHidden)
|
|
{
|
|
glfwWaitEvents();
|
|
return;
|
|
}
|
|
|
|
float customYCursor = 0;
|
|
ImVec2 viewportSize = ImGui::GetMainViewport()->Size;
|
|
|
|
// Menu Bar
|
|
customYCursor += menuBar(drawData, appData).y;
|
|
|
|
// Checklist
|
|
ImGui::SetNextWindowPos(ImVec2(0, customYCursor));
|
|
ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0));
|
|
customYCursor += checklistWindow(appData, std::format(" {} Checklist", ICON_CHECK_FILL).c_str()).y;
|
|
|
|
customYCursor += 5.;
|
|
|
|
// Playback Devices
|
|
ImGui::SetNextWindowPos(ImVec2(0, customYCursor));
|
|
ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0));
|
|
customYCursor += audioDeviceWindow(appData, appData.audioData->playbackDevices, std::format(" {} Playback", ICON_HEADPHONE_FILL).c_str()).y;
|
|
|
|
customYCursor += 5.;
|
|
|
|
// Recording devices
|
|
ImGui::SetNextWindowPos(ImVec2(0, customYCursor));
|
|
ImGui::SetNextWindowSize(ImVec2(viewportSize.x, 0));
|
|
customYCursor += audioDeviceWindow(appData, appData.audioData->recordingDevices, std::format(" {} Recording", ICON_MIC_FILL).c_str()).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)
|
|
{
|
|
cleanupShell(drawData);
|
|
}
|
|
|
|
ImVec2 menuBar(DrawData& drawData, ApplicationData& appData)
|
|
{
|
|
ImVec2 size{};
|
|
bool closeMenu = false;
|
|
|
|
if (ImGui::BeginMainMenuBar())
|
|
{
|
|
if (ImGui::BeginMenu("Settings"))
|
|
{
|
|
if (ImGui::Checkbox("Docked", &appData.settings.docked))
|
|
{
|
|
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 - 47.);
|
|
|
|
if (ImGui::SmallButton(ICON_SUBTRACT_FILL))
|
|
{
|
|
glfwIconifyWindow(drawData.window);
|
|
}
|
|
if (ImGui::SmallButton(ICON_CLOSE_FILL))
|
|
{
|
|
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;
|
|
}
|
|
|
|
void drawCheclistDayLines(ApplicationData& appData, ImDrawList* drawList, float lineHeight, time_t day)
|
|
{
|
|
auto& tasks = appData.settings.tasks;
|
|
size_t totalTasks = appData.settings.taskNames.size();
|
|
size_t count = std::count_if(tasks.begin(), tasks.end(), [&](std::pair<std::string, std::vector<time_t>> t) { return std::any_of(t.second.begin(), t.second.end(), [&](time_t tt) { return tt == day; }); });
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
ImVec2 cursorPos = ImGui::GetCursorScreenPos();
|
|
drawList->AddLine({ cursorPos.x, cursorPos.y }, { cursorPos.x, cursorPos.y + lineHeight }, ImColor(.4f, .9f, .3f), 3.f);
|
|
ImGui::SetCursorScreenPos({ cursorPos.x + 5.f, cursorPos.y });
|
|
}
|
|
for (int i = 0; i < max(0, totalTasks - count); i++)
|
|
{
|
|
ImVec2 cursorPos = ImGui::GetCursorScreenPos();
|
|
drawList->AddLine({ cursorPos.x, cursorPos.y }, { cursorPos.x, cursorPos.y + lineHeight }, ImColor(.1f, .3f, .05f), 3.f);
|
|
ImGui::SetCursorScreenPos({ cursorPos.x + 5.f, cursorPos.y });
|
|
}
|
|
}
|
|
|
|
time_t getDayStartOf(time_t time)
|
|
{
|
|
tm localTime;
|
|
localtime_s(&localTime, &time);
|
|
localTime.tm_hour = 0;
|
|
localTime.tm_min = 0;
|
|
localTime.tm_sec = 0;
|
|
return mktime(&localTime);
|
|
}
|
|
|
|
ImVec2 checklistWindow(ApplicationData& appData, const char* title)
|
|
{
|
|
if (ImGui::Begin(title, 0, ImGuiWindowFlags_NoResize))
|
|
{
|
|
time_t today = getDayStartOf(std::time(nullptr));
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
float lineHeight = ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2;
|
|
|
|
for (int pastDay = 3; pastDay >= 1; pastDay--)
|
|
{
|
|
time_t date = today - (60 * 60 * 24 * pastDay);
|
|
drawCheclistDayLines(appData, drawList, lineHeight, date);
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 5.f);
|
|
}
|
|
|
|
ImVec2 cursorPos = ImGui::GetCursorScreenPos();
|
|
drawList->AddLine({ cursorPos.x, cursorPos.y - 2.f }, { cursorPos.x, cursorPos.y + lineHeight + 2.f }, IM_COL32_WHITE, 1.f);
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.f);
|
|
drawCheclistDayLines(appData, drawList, lineHeight, today);
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.f);
|
|
|
|
auto todayMatcher = [&](time_t t) { return getDayStartOf(t) == today; };
|
|
|
|
for (std::string& taskName : appData.settings.taskNames)
|
|
{
|
|
if (!appData.settings.tasks.contains(taskName))
|
|
{
|
|
appData.settings.tasks.insert({ taskName, std::vector<time_t>{} });
|
|
}
|
|
|
|
std::vector<time_t>& taskDates = appData.settings.tasks[taskName];
|
|
bool taskDoneToday = std::any_of(taskDates.begin(), taskDates.end(), todayMatcher);
|
|
|
|
if (ImGui::Checkbox(taskName.c_str(), &taskDoneToday))
|
|
{
|
|
if (taskDoneToday)
|
|
{
|
|
taskDates.push_back(today);
|
|
}
|
|
else
|
|
{
|
|
std::erase_if(taskDates, todayMatcher);
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
}
|
|
}
|
|
|
|
ImVec2 size = ImGui::GetWindowSize();
|
|
ImGui::End();
|
|
return size;
|
|
}
|
|
|
|
ImVec2 audioDeviceWindow(ApplicationData& appData, std::vector<AudioDevice>& 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)
|
|
{
|
|
// Mute button
|
|
ImGui::PushID(std::string("bn_mute_").append(deviceIdUtf8).c_str());
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
|
|
|
|
bool isDeviceMuted = isMuted(dev.volumeInterface);
|
|
if (ImGui::Button(isDeviceMuted ? ICON_VOLUME_MUTE_FILL : ICON_VOLUME_UP_FILL))
|
|
{
|
|
setMuted(dev.volumeInterface, !isDeviceMuted);
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopID();
|
|
ImGui::SameLine(0, 2);
|
|
|
|
// Meter
|
|
static std::array<float, 2> meterValues{};
|
|
UINT channelCount = getMeterValues(dev.meterInterface, meterValues);
|
|
|
|
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;
|
|
|
|
// BG line
|
|
drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX), lineY), IM_COL32(120, 120, 120, 255), 2.);
|
|
|
|
// Channel lines
|
|
if (channelCount == 1)
|
|
{
|
|
drawList->AddLine(ImVec2(cursorPos.x, lineY), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX) * meterValues[0], lineY), IM_COL32(200, 200, 255, 255), 3.);
|
|
}
|
|
if (channelCount == 2)
|
|
{
|
|
drawList->AddLine(ImVec2(cursorPos.x, lineY - 2), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX) * meterValues[0], lineY - 2), IM_COL32(200, 200, 255, 255), 3.);
|
|
drawList->AddLine(ImVec2(cursorPos.x, lineY + 2), ImVec2(cursorPos.x + (space.x - 2. * linePaddingX) * meterValues[1], lineY + 2), IM_COL32(200, 200, 255, 255), 3.);
|
|
}
|
|
|
|
ImGui::SetNextItemWidth(space.x);
|
|
ImGui::PushID(&dev.id);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
|
|
if (isDeviceMuted) ImGui::PushStyleColor(ImGuiCol_SliderGrab, ImVec4(.4, .4, .4, 1.));
|
|
|
|
float volume = getVolume(dev.volumeInterface);
|
|
if (ImGui::SliderFloat("", &volume, 0., 1., "", ImGuiSliderFlags_NoRoundToFormat | ImGuiSliderFlags_AlwaysClamp))
|
|
{
|
|
setVolume(dev.volumeInterface, volume);
|
|
}
|
|
|
|
if (isDeviceMuted) ImGui::PopStyleColor();
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// 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(), ICON_MUSIC_2_FILL, !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(), ICON_PHONE_FILL, !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);
|
|
}
|