From 388d080eef5367d7a0ba0b16c91126b1750374c9 Mon Sep 17 00:00:00 2001 From: Gabriel <91475996+bdshrk@users.noreply.github.com> Date: Tue, 21 May 2024 21:56:25 +0100 Subject: [PATCH] initial commit --- .clang-format | 6 + .gitignore | 403 +++++++++++++++++++++++ README.md | 38 +++ Resource.rc | Bin 0 -> 4692 bytes auto-duck-bgm.sln | 31 ++ auto-duck-bgm.vcxproj | 170 ++++++++++ auto-duck-bgm.vcxproj.filters | 47 +++ doc/screenshot.png | Bin 0 -> 5385 bytes res/icon_bypassed.ico | Bin 0 -> 1150 bytes res/icon_default.ico | Bin 0 -> 1150 bytes res/speaker-volume-control-mute.png | Bin 0 -> 496 bytes res/speaker-volume-control-up.png | Bin 0 -> 555 bytes res/speaker-volume-control.png | Bin 0 -> 545 bytes res/speaker-volume-low.png | Bin 0 -> 440 bytes res/speaker-volume-none.png | Bin 0 -> 412 bytes res/speaker-volume.png | Bin 0 -> 480 bytes resource.h | 37 +++ src/Engine.cpp | 480 ++++++++++++++++++++++++++++ src/Engine.h | 248 ++++++++++++++ src/UI.cpp | 179 +++++++++++ src/UI.h | 36 +++ 21 files changed, 1675 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Resource.rc create mode 100644 auto-duck-bgm.sln create mode 100644 auto-duck-bgm.vcxproj create mode 100644 auto-duck-bgm.vcxproj.filters create mode 100644 doc/screenshot.png create mode 100644 res/icon_bypassed.ico create mode 100644 res/icon_default.ico create mode 100644 res/speaker-volume-control-mute.png create mode 100644 res/speaker-volume-control-up.png create mode 100644 res/speaker-volume-control.png create mode 100644 res/speaker-volume-low.png create mode 100644 res/speaker-volume-none.png create mode 100644 res/speaker-volume.png create mode 100644 resource.h create mode 100644 src/Engine.cpp create mode 100644 src/Engine.h create mode 100644 src/UI.cpp create mode 100644 src/UI.h diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..27dd0e8 --- /dev/null +++ b/.clang-format @@ -0,0 +1,6 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: '4' + +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2ec18c --- /dev/null +++ b/.gitignore @@ -0,0 +1,403 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + + + +#### CUSTOM +build/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..da010dd --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Auto-Duck BGM + +**Auto-Duck BGM can reduce or mute audio from a program when any other audio plays.** + +![Screenshot](doc/screenshot.png) + +## Features + +- Very low memory and CPU usage. +- Bypass the effect, returning the volume to normal. +- Smooth fading between minimum and maximum volume. +- Runs in the taskbar notification area with settings available on right-click. + +This program is Windows only and uses the Windows Core Audio API. + +Similar to: + +- [Background Music](https://github.com/kyleneideck/BackgroundMusic), a macOS audio utility with an auto-pause feature. +- [Automatic Volume Mixer](https://github.com/Klocman/Automatic-Volume-Mixer), which can achieve a similar effect by automating Windows Volume Mixer based on rules. + +## Settings + +The settings can be configured from the `.ini` file within the same folder as the executable (the `.ini` file will generate on the first run). You may also open and reload the `.ini` file using the relevant options from the tray area icon's menu. + +Settings that can be configured: + +- The controlled executable. +- The minimum and maximum volume while the program is running. +- The volume that any other program must exceed to trigger the duck. +- The number of consecutive times the trigger volume must be exceeded to trigger the duck, and, separately, the number of consecutive times the volume must be below to trigger the unduck. +- Changing the duration of the fade between the minimum and maximum volume. +- The volume that the controlled executable is set to when the program is bypassed or quit. +- Set excluded applications that are ignored when playing audio. +- Run a custom Windows command on duck or unduck (e.g., to play or pause music). + +## Credits + +Icons from Yusuke Kamiyamane's Fugue Icons are available under a [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) - [https://p.yusukekamiyamane.com/](https://p.yusukekamiyamane.com/) diff --git a/Resource.rc b/Resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..0627cad0fd925b9958fb85612cfd802e85542ada GIT binary patch literal 4692 zcmds)eQy#$5XR?k6TicG{l`XAeE9@W5J|5s;h@o^38AG_5=sd#)+T;-_4n+t<$yay zYz&&s-R|wq?mjd7%-rnvZv`t@V8_s*O?Y}XrSFWsiOsBI1HKOJ1tqX+yRsqA znEoksm%cHz=FMnb(hm62YJD3ab6`*H3A+?ry+`km(E-w3CmXKKKd?_0I%&<^b8f9W!htCiT#JJ8pQ( z`*nA#_UHmmcZmNDIH?I72<$m$KIF^$;gLJTYpfxr>}T*`lJcs)v)HpG@9MH<(cZzU z#H(UFsD*zWvUeMPMHH?3l@jF6Y_diJ7PpyoLcQ;Hjp)@^i@NHV>U+Fg1earbZ!cLp zMy|we$(&V*Hls%?y5w^}9&&~zN9aebQ1j+DDx1#-4fC7?z872au@$nlu3%B zL{`LEXPZBx-HIz|G(xCvUn?WfKWO&Q<8`RPhX$T~?0mcBc<0-a$>v!d@2lgpbu1Rb zA!QHuwEZs2=bgQVx4Yl1=D2w5R*Z>(%135I#wN(W&<V zqa35i({iokU#rNZ^DPn)_2)f9T3WBJ?pIl0jFGG}PrEd+Mq|TyA?OtEJ9a_jzuH)< z1W}%DLQthde*ym~S)jNSxW7}MTDjb2y5!*(n{-Y(r3&^;vY;woo1I38{jb1qi(Qq$ zDkt3>Ssp6op%IobM(_I3<>k=dx3F3Y-_{ zMx`t}Kyu_}=;o!^)puyb#bsPcHh+VRSUO>5GCtv3NaS0>BVDxWfTA1N{e1L>I?4Zq zxCS$8L@(Wyl+jhl_KL)~h*M58LTxX%+MoCBvx&lk4()l4*g@+7C=cc%i`(5grV>$D zkur9^EgPrHV_>?#HX|%Gq38y?jw=6u|5!QrZh1! + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {29ab400f-903d-4e30-81d0-2836065310f4} + auto-duck-bgm + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + $(SolutionDir)build\$(Platform)\$(Configuration)\ + build\$(Platform)\$(Configuration)\ + + + $(SolutionDir)build\$(Platform)\$(Configuration)\ + build\$(Platform)\$(Configuration)\ + + + $(SolutionDir)build\$(Platform)\$(Configuration)\ + build\$(Platform)\$(Configuration)\ + + + $(SolutionDir)build\$(Platform)\$(Configuration)\ + build\$(Platform)\$(Configuration)\ + + + + Level3 + true + WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Console + true + + + + + EnableAllWarnings + true + true + true + WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + false + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Windows + true + true + false + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Console + true + + + + + EnableAllWarnings + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + false + $(ProjectDir);%(AdditionalIncludeDirectories) + + + Windows + true + true + false + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/auto-duck-bgm.vcxproj.filters b/auto-duck-bgm.vcxproj.filters new file mode 100644 index 0000000..97b6a9d --- /dev/null +++ b/auto-duck-bgm.vcxproj.filters @@ -0,0 +1,47 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + + + Header Files + + + Source Files + + + Source Files + + + + + Resource Files + + + Resource Files + + + + + + \ No newline at end of file diff --git a/doc/screenshot.png b/doc/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..ddcdce2c74e81c15962d637b6d497732976ab54e GIT binary patch literal 5385 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 PV3+=98#!L*a1G}VN<_A9 literal 0 HcmV?d00001 diff --git a/res/icon_default.ico b/res/icon_default.ico new file mode 100644 index 0000000000000000000000000000000000000000..fcadc201b0d2e57fa84de21d11634649f96426ad GIT binary patch literal 1150 zcmc(eO=uHA6vyA@qlWlFO;Mu}6Acz622l~Ag!CqnkU&Txf`Ob;d&r>%j2b-XL5tN( zTM$VO7HVm&RpX(71viu)TGT)wSD^?IL5UQ^gNMTN|F)ym;=L1o`(9?|H}iIeCDQQg z?j|%J4cdtIB0C18%Lg>`i3DSt8#B{Zf_gMy1zo@ic7fes4{(D6DoQ_T8s1hRbxRiz zcL(SJ9&iMlz`P9N0JsD^%Cl_pG4yYby$_(Y5NAyds_WvVWt=4Ve zAc!9I`$ry+j(+*nYKedMFMmJWVc=2zb&P!zY&P5d{=U8^w+e;Dce?)LX{EwV!(fNQ zF=DsdW&aDH7n}w|%Ks1y27iV^A%3sxygWb8i`6Q>nVsdz*cjL6=Gf(O-E%sf;>+H* z!A>wNKH@?qli_$g&abDZxjHq)uO=t?#l!@c$H!U6IzFH84v@Km)NV%QzszQ{UrVJD zH=9jvG#b3Tw8RVbIzOA4;kU?fjyK(Ixk~d+hY+`eYs$Zf^_~miaJZ1q=O32K!d zUaQr(h4Yhsuh%R7IdBN{g0%8~*F}*?qzkyC(P%iA%iS;x<8!;+X8g|sC}xw-;xg#@ zk1uObEEc0=GI_jMEI#OTI-E+Su7Gu$D1byhG4uY_S(r75DCoGbzxQm~R58fvH(3nI>;z`j2%$S^vI zhN2LJTO%RNATt8BXnT8JaA`6(U-;es?s?zmxx4>gsLZn4?Nwj^4D(ucfYoa4@%em< z*=&|;TssQXn8qEhlerJXf*RYgp2nA zpPfUb zRVy%ebaXBR0&8!w{6diDKzXH9D)p$x m45<=0AwWt1vV)lm&==fjA9B!^9)2AN~j82Ot`+0l~q+nq_5W-EZE!`5zJz zl1`NKJUl$ufLOxY+uJTRHML^z-o4KrKYsil$j^bPv5Gm6Z5+DO46cchkueO2SA(#f zon28*PEOCPS+lmCKY#wywr$(~U%GVZKS&&=UdL~wP6OVi;s_gRaI5> zAE@d7nKNhpA3JvZ|Gs?({;ycE639OJUteFp5~NneZFQBB%W4Nz_cg&l_HL+t#sksO z(e|G{ePW1=OlJ7^?>`v-{rivM^XIn=;^OiQWo7A2K(+NuD!m|EcQP>iHv($C31ka0 z{r~@8`r^flKi|E3#~>sm#ULOc&cMqn%)rIP&+z|0BLfo?D+ADQR*-G1tlVJC#>NMx zVW1p{--0j;3rnY!mHqspqMAcHcb@t(d-jt5d-tCI&%wdb1yZOVc%ll39Snj_1q12b z7_q?2%*+nNGC=IDtgPBKapH`Zmo7a4#dj+r7VfH9MBTh<5_UlwZ^rpyc@(3ev1GA002ovPDHLkV1o8O1=Iil literal 0 HcmV?d00001 diff --git a/res/speaker-volume-control.png b/res/speaker-volume-control.png new file mode 100644 index 0000000000000000000000000000000000000000..863f557928252e0dcabda7a97e0146e4a44b176b GIT binary patch literal 545 zcmV++0^a?JP)X|Hz)Nqf1ww@=GE6xu?!_5&XtIJoEg{l3_BG;Bk$SkxE; z^e8pyN03Bsd8FNLe+MuFqF@_>Am~b`)0fKS@*5#U3X*!=`Ruyx6!Y|^4GM)qgrTdd z+O6w)E}zfeZM9mUVGxlXd%XPr-`9A2-%}2F0pOYz3WWv%6hWHJ=C@2HlcJ}x*=(O_ zng=Y);?#Ns#9Ol7Gb-HR91Gq;ZZK3TmHG_BNb>I6zVE|z+psJfj#EdY(SmK;Q4rs8 z0K${N!GJ8#`BSo1tD&do0&f@V`hLL4HWXzEEQo<<%PTh(MH%MwegLx|8rzI&wYri> zB%->Wg6DbQD}!rtgGCmEnNAplj2-lpZstoM7=+{zT>#ZI?RhSDd1`d@$(KrHrZGAB zPE1e#5V2Tn9Fmg*pGHnx|8O2#!Pk${WAsL6XV>_|#OuFvbAJSYXOQ&XnpLqAcKlgr;P6U{A6N6m-g<*d_ot00000NkvXXu0mjf*z)>Y literal 0 HcmV?d00001 diff --git a/res/speaker-volume-low.png b/res/speaker-volume-low.png new file mode 100644 index 0000000000000000000000000000000000000000..e89c39cc7899b1d1991a868c25de4b231bccb172 GIT binary patch literal 440 zcmV;p0Z0CcP)bKyK;djQ+XlyR`+^{xYMMr1AK8Fmm=jbHsMqT~L?V$OY{g=+q$Ei$S(Zsv zRflyx3Qc$(o*II)aj_y?P{bb%CvR<5kx2rH{fu|U$56G z9NwkD!1$u;1N>PbmVGrF%vk4NF!3r4g*6O=uT(1U?RJ|uE=V5F61iMHayl2tFc>tj zW0H=Y5L^!Yw8V0)Rx6;srPF)mc>G{7kvI$q!f{klX6L+KueqD=Wyj%;Mg`lSh80MM zLhIMXVyRiJJ{d%GMU#eQ*&XDz(U8psmoz2}IA9)pz{47B=YbsiwLvbIlQ0xP5H=M>NoTX!W5;m_d=m>ejyuBCfri83b26EX!FDQ@${2HEkd7w|zKisE%Ty?U@S%#VsrV0000R?z(=Z1L4p}r#KKd|ARL3l3qAnFv W?u|L<=qTL)0000 session) { + this->session = session; +} + +AudioSession::~AudioSession() {} + +IAudioSessionControl *AudioSession::getSession() { return session; } + +IAudioSessionControl2 *AudioSession::getSession2() { + if (!session2) { + HRESULT hr = getSession()->QueryInterface( + __uuidof(IAudioSessionControl2), (void **)&session2); + if (FAILED(hr)) + throw std::runtime_error( + "Failed to get session control 2 interface"); + } + return session2; +} + +ISimpleAudioVolume *AudioSession::getSimpleAudioVolume() { + if (!simpleAudioVolume) { + HRESULT hr = getSession()->QueryInterface(__uuidof(ISimpleAudioVolume), + (void **)&simpleAudioVolume); + if (FAILED(hr)) + throw std::runtime_error( + "Failed to get simple audio volume interface"); + } + return simpleAudioVolume; +} + +std::wstring AudioSession::getExecutableName() { + if (!name) { + LPWSTR wName; + HRESULT hr = getSession2()->GetSessionIdentifier(&wName); + if (FAILED(hr)) + throw std::runtime_error( + "Failed to get session identifier/executable name"); + std::wstring wideString(wName); + LocalFree(wName); + + // extract "abc.exe" from the returned string + name = std::make_unique(); + auto endOfPathBackslash = wideString.rfind(L"\\"); + if (endOfPathBackslash != std::wstring::npos) { + std::wstring executableRegion = + wideString.substr(endOfPathBackslash + 1); + auto endOfPathPercentage = executableRegion.find(L"%"); + if (endOfPathPercentage != std::wstring::npos) { + *name = executableRegion.substr(0, endOfPathPercentage); + } + } + } + return *name; +} + +float AudioSession::getSessionVolume() { + if (!volume) { + volume = std::make_unique(); + HRESULT hr = getSimpleAudioVolume()->GetMasterVolume(volume.get()); + if (FAILED(hr)) + throw std::runtime_error("Failed to get volume"); + } + return *volume; +} + +void AudioSession::setSessionVolume(float &newVolume) { + HRESULT hr = getSimpleAudioVolume()->SetMasterVolume(newVolume, NULL); + if (FAILED(hr)) + throw std::runtime_error("Failed to set volume"); + std::cout << "Volume set to: " << newVolume << std::endl; +} + +IAudioMeterInformation *AudioSession::getAudioMeterInformation() { + if (!audioMeterInformation) { + HRESULT hr = getSession()->QueryInterface( + __uuidof(IAudioMeterInformation), (void **)&audioMeterInformation); + if (FAILED(hr)) + throw std::runtime_error("Failed to get audio meter interface"); + } + return audioMeterInformation; +} + +float AudioSession::getPeakAudioLevel() { + if (!volumePeak) { + volumePeak = std::make_unique(); + HRESULT hr = getAudioMeterInformation()->GetPeakValue(volumePeak.get()); + if (FAILED(hr)) + throw std::runtime_error("Failed to get peak audio level"); + } + return *volumePeak; +} + +float Engine::getMaxPeakAudioLevel( + const std::vector &excludedExecutables) { + float maxVolume = 0.0f; + for (auto &session : sessions) { + if (std::find(excludedExecutables.begin(), excludedExecutables.end(), + session->getExecutableName()) != + excludedExecutables.end()) { + continue; + } + + float volume = session->getPeakAudioLevel(); + if (volume > maxVolume) + maxVolume = volume; + } + + return maxVolume; +} + +bool Engine::init() { + try { + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if (FAILED(hr)) + throw std::runtime_error("Failed to initialize COM"); + + hr = deviceEnumerator.CoCreateInstance(__uuidof(MMDeviceEnumerator), + nullptr, CLSCTX_ALL); + if (FAILED(hr)) + throw std::runtime_error("Failed to create device enumerator"); + + hr = deviceEnumerator->GetDefaultAudioEndpoint( + EDataFlow::eRender, ERole::eConsole, &device); + if (FAILED(hr)) + throw std::runtime_error("Failed to get default audio endpoint"); + + hr = device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, NULL, + (void **)&sessionManager2); + if (FAILED(hr)) + throw std::runtime_error("Failed to activate session manager"); + } catch (std::exception &exception) { + handleError(exception); + return false; + } + return true; +} + +std::vector> Engine::getAudioSessions() { + if (sessionManager2 == nullptr) + throw std::runtime_error( + "Failed to get audio session (engine uninitialised)"); + + HRESULT hr; + + std::vector> sessions; + CComPtr sessionEnumerator; + + hr = sessionManager2->GetSessionEnumerator(&sessionEnumerator); + if (FAILED(hr)) + throw std::runtime_error("Failed to get session enumerator"); + + int count; + hr = sessionEnumerator->GetCount(&count); + if (FAILED(hr)) + throw std::runtime_error("Failed to get session count"); + + for (int i = 0; i < count; i++) { + CComPtr pSessionControl; + hr = sessionEnumerator->GetSession(i, &pSessionControl); + if (FAILED(hr)) + throw std::runtime_error("Failed to get session " + + std::to_string(i)); + + auto session = std::make_shared(pSessionControl); + sessions.push_back(session); + } + + return sessions; +} + +std::shared_ptr +Engine::getAudioSessionByExecutableName(const std::wstring &executableName) { + for (auto &session : sessions) { + std::wstring processName = session->getExecutableName(); + if (!processName.empty() && processName == executableName) + return session; + } + return nullptr; +} + +void Engine::requestQuit() { quitRequested = true; } + +std::wstring Engine::getAbsoluteExecutablePath() { + wchar_t buffer[MAX_PATH]; + GetModuleFileNameW(NULL, buffer, MAX_PATH); + + std::wstring exePath(buffer); + size_t lastSlashPos = exePath.find_last_of(L"\\/"); + std::wstring exeDirectory = exePath.substr(0, lastSlashPos + 1); + + return exeDirectory; +} + +std::wstring Engine::getSettingsINIPath() { + auto exeDirectory = getAbsoluteExecutablePath(); + return exeDirectory + SETTINGS_FILENAME; +} + +bool Engine::openSettingsINI() { + try { + ShellExecuteW(NULL, L"open", getSettingsINIPath().c_str(), NULL, NULL, + SW_SHOWNORMAL); + } catch (std::exception &exception) { + handleError(exception); + return false; + } + return true; +} + +bool Engine::readSettingsINI() { + try { + tryCreateDefaultSettingsINI(); + + readINIValue(L"Performance", L"fTickIdleMS", paramTickIdleMS); + readINIValue(L"Performance", L"fTickTransitionsMS", + paramTickTransitionMS); + readINIValue(L"General", L"fFadeSpeedMS", paramFadeSpeedMS); + readINIValue(L"General", L"fVolumeMinimumToTrigger", + paramVolumeMinimumToTrigger); + readINIValue(L"General", L"fVolumeMax", paramVolumeMax); + readINIValue(L"General", L"iConsecutiveMinimumsToTrigger", + paramConsecutiveMinimumsToTrigger); + readINIValue(L"General", L"iConsecutiveMinimumsToEnd", + paramConsecutiveMinimumsToEnd); + + readINIValue(L"General", L"sExcludedExecutables", + paramExcludedExecutables); + readINIValue(L"General", L"sControlledExecutable", + paramControlledExecutable); + + paramExcludedExecutables.push_back(paramControlledExecutable); + + readINIValue(L"General", L"fVolumeRestore", paramVolumeRestore); + + readINIValue(L"General", L"sCommandOnDuck", paramCommandOnDuck); + readINIValue(L"General", L"sCommandOnUnduck", paramCommandOnUnduck); + } catch (std::exception &exception) { + handleError(exception); + return false; + } + return true; +} + +void Engine::tryCreateDefaultSettingsINI() { + auto settingsPath = getSettingsINIPath(); + + std::wifstream checkFile(settingsPath); + if (!checkFile) { + std::wofstream file(settingsPath); + if (!file.is_open()) { + throw std::runtime_error("Failed to create default INI file"); + } + file << SETTINGS_DEFAULT; + file.close(); + } + checkFile.close(); +} + +std::wstring Engine::readINIValueStringW(const std::wstring §ion, + const std::wstring &key) { + auto settingsPath = getSettingsINIPath(); + wchar_t buffer[1024]; + DWORD bytesRead = GetPrivateProfileStringW( + section.c_str(), key.c_str(), NULL, buffer, 1024, settingsPath.c_str()); + + if (bytesRead == 0) { + DWORD error = GetLastError(); + if (error != ERROR_SUCCESS) + throw std::runtime_error( + "Failed to read value from INI file\n" + "If the program has just updated, " + "there may be new settings not present in your INI file.\n" + "Try deleting the INI file and opening the program again.\n" + "Key: " + + wStringToString(key)); + } + + std::wstring value(buffer); + return value; +} + +void Engine::runCommandSilent(std::wstring &command) { + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + ZeroMemory(&pi, sizeof(pi)); + + std::wstring formattedCommand = CMD_START + command; + + if (!CreateProcessW(NULL, const_cast(formattedCommand.c_str()), + NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, + &pi)) + throw std::runtime_error("Failed to create process for command"); + + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); +} + +void Engine::setBypassed(bool newBypassed) { bypassed = newBypassed; } + +bool Engine::getBypassed() const { return bypassed; } + +std::unique_ptr Engine::engine; // singleton +Engine *Engine::get() { + if (!engine) + engine = std::make_unique(); + return engine.get(); +} + +bool Engine::running() { + try { + if (!readSettingsINI()) + return hasError(); + + if (!init()) + return hasError(); + + while (!hasError()) { + + sessions = getAudioSessions(); + + float maxVolume = getMaxPeakAudioLevel(paramExcludedExecutables); + + auto sessionControls = + getAudioSessionByExecutableName(paramControlledExecutable); + + float volumeTarget = (maxVolume > paramVolumeMinimumToTrigger) + ? paramVolumeMin + : paramVolumeMax; + + float sleepNeeded = paramTickIdleMS; + + if (getBypassed()) + volumeTarget = paramVolumeRestore; + + // if found controlling program... + if (sessionControls) { + shortStatusString = + L"Found and controlling " + paramControlledExecutable; + + float volumeCurrent = sessionControls->getSessionVolume(); + bool shouldTransition = + std::abs(volumeCurrent - volumeTarget) > 0.001; + + if (shouldTransition && getBypassed()) { + sessionControls->setSessionVolume(paramVolumeRestore); + // run unduck command if bypassing and currently ducked... + if (volumeCurrent == paramVolumeMin) + runCommandSilent(paramCommandOnUnduck); + } + + if (shouldTransition && !getBypassed()) { + // increase consecutive minimums if at minimum + if (volumeTarget == paramVolumeMin) { + currentConsecutiveMinimumsToTrigger = + min(currentConsecutiveMinimumsToTrigger + 1, + paramConsecutiveMinimumsToTrigger); + } else { + currentConsecutiveMinimumsToEnd = + min(currentConsecutiveMinimumsToEnd + 1, + paramConsecutiveMinimumsToEnd); + } + + // if either minimum is at the target value + if ((currentConsecutiveMinimumsToTrigger == + paramConsecutiveMinimumsToTrigger) || + (currentConsecutiveMinimumsToEnd == + paramConsecutiveMinimumsToEnd)) { + + float directionMult = + ((volumeCurrent - volumeTarget) > 0) ? -1.0f : 1.0f; + + float step = + (paramVolumeMax - paramVolumeMin) * + (paramTickTransitionMS / paramFadeSpeedMS) * + directionMult; + + float newVolume = + min(max(volumeCurrent + step, paramVolumeMin), + paramVolumeMax); + + sessionControls->setSessionVolume(newVolume); + + sleepNeeded = paramTickTransitionMS; + + // if transitioning, set both values to max to ensure + // smooth transitioning + currentConsecutiveMinimumsToEnd = + paramConsecutiveMinimumsToEnd; + currentConsecutiveMinimumsToTrigger = + paramConsecutiveMinimumsToTrigger; + + // try duck command + if (newVolume == paramVolumeMin && directionMult < 0.0) + runCommandSilent(paramCommandOnDuck); + + // try unduck command + if (volumeCurrent == paramVolumeMin && + directionMult > 0.0) + runCommandSilent(paramCommandOnUnduck); + } + + } else { + currentConsecutiveMinimumsToEnd = 0; + currentConsecutiveMinimumsToTrigger = 0; + } + } else { + // failure to file controlled executable is not fatal. + std::cout + << "Cannot find controlled executable, will keep looking." + << std::endl; + shortStatusString = L"Controlled executable not found"; + } + + // cleanup + sessions.clear(); + + Sleep((DWORD)sleepNeeded); + + if (quitRequested) + break; + } + } catch (std::runtime_error &error) { + handleError(error); + } + + // try resetting volume to restore value + try { + sessions = getAudioSessions(); + auto s = getAudioSessionByExecutableName(paramControlledExecutable); + if (s) + s->setSessionVolume(paramVolumeRestore); + + sessions.clear(); + } catch (std::runtime_error &error) { + handleError(error); + } + + return hasError(); +} + +bool Engine::hasError() const { return !errorString.empty(); } + +std::wstring &Engine::getErrorString() { return errorString; } + +std::wstring &Engine::getShortStatusString() { return shortStatusString; } + +void Engine::handleError(const std::exception &exception) { + errorString = stringToWString(exception.what()); + if (errorString.empty()) + errorString = L"Unknown error"; + shortStatusString = L"An error has occurred"; +} + +std::string Engine::wStringToString(const std::wstring &wstr) { + std::wstring_convert, wchar_t> converter; + return converter.to_bytes(wstr); +} + +std::wstring Engine::stringToWString(const std::string &str) { + std::wstring_convert, wchar_t> converter; + return converter.from_bytes(str); +} + +Engine::Engine() {} + +Engine::~Engine() { + sessions.clear(); + // explicitly free CComPtrs before CoUninitialize() + deviceEnumerator = nullptr; + device = nullptr; + sessionManager2 = nullptr; + CoUninitialize(); +} diff --git a/src/Engine.h b/src/Engine.h new file mode 100644 index 0000000..628a5c7 --- /dev/null +++ b/src/Engine.h @@ -0,0 +1,248 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static const LPCWSTR PROG_BRAND_NAME = L"Auto-Duck BGM"; +static const std::wstring SETTINGS_FILENAME = L"settings.ini"; +static const std::wstring CMD_START = L"cmd.exe /C "; + +static const std::wstring SETTINGS_DEFAULT = LR"([Performance] +; Controls how frequently the program queries volume information when idle. +fTickIdleMS=1000.0 + +; Controls how frequently the program queries volume information when transitioning. Higher values mean a smoother transition. +fTickTransitionsMS=50.0 + + + +[General] +; Control the fade speed of the audio. +fFadeSpeedMS=1000.0 + +; Number of consecutive samples that the volume needs to be above the fVolumeMinimumToTrigger to trigger the duck. 1 will trigger the duck immediately. +iConsecutiveMinimumsToTrigger=1 + +; Number of consecutive samples that the volume needs to be below the fVolumeMinimumToTrigger to end the duck. +iConsecutiveMinimumsToEnd=3 + +; Minimum volume of programs not excluded or controlled to trigger the duck. +fVolumeMinimumToTrigger=0.0 + +; The minimum volume the controlled program will be lowered to. 0.0 is muted. +fVolumeMin=0.0 + +; The maximum volume the controlled program will be raised to. For background music, set to a lower value. +fVolumeMax=0.2 + +; The volume to restore the controlled program to when this program is closed or bypassed. +fVolumeRestore=1.0 + +; Excluded executable names that are ignored when calculating whether to trigger. Separated by a "/" character. +sExcludedExecutables=nvcontainer.exe/amdow.exe/amddvr.exe + +; The program that is targeted. +sControlledExecutable=foobar2000.exe + +; Run a Windows command when ducked or unducked. Leave empty for no commands. +sCommandOnDuck= +sCommandOnUnduck= +)"; + +// audiosession store info about a single IAudioSessionControl within a single +// tick. interfaces other than IAudioSessionControl are not requested/created +// until they are accessed. +// the result of all function calls are cached in the appropriate private +// variables. +class AudioSession { + private: + CComPtr session = nullptr; + CComPtr session2 = nullptr; + CComPtr simpleAudioVolume = nullptr; + CComPtr audioMeterInformation = nullptr; + std::unique_ptr name = nullptr; + std::unique_ptr volume = nullptr; + std::unique_ptr volumePeak = nullptr; + + public: + AudioSession(CComPtr session); + ~AudioSession(); + + IAudioSessionControl *getSession(); + IAudioSessionControl2 *getSession2(); + ISimpleAudioVolume *getSimpleAudioVolume(); + IAudioMeterInformation *getAudioMeterInformation(); + + // attempts to extract the executable name from the session identifier in + // the form of "ABC.exe" + std::wstring getExecutableName(); + + // session volume is volume level set on the mixer (Sndvol) + float getSessionVolume(); + void setSessionVolume(float &newVolume); + + // peak audio level is the max of any channel of the current audio session. + // this has NO averaging of peak levels (RMS loudness) + float getPeakAudioLevel(); +}; + +// singleton engine class accessible via Engine::get(). +// the running() function blocks until the engine is requested to quit via +// requestQuit() or an error occurs. if the engine encountered an error, use +// hasError() to check and getErrorString() to fetch the error string. +class Engine { + private: + static std::unique_ptr engine; // singleton + + std::vector> sessions; + + std::wstring errorString; + std::wstring shortStatusString = L""; + void handleError(const std::exception &exception); + + CComPtr deviceEnumerator = nullptr; + CComPtr device = nullptr; + CComPtr sessionManager2 = nullptr; + + // params set by ini + float paramFadeSpeedMS; + float paramTickIdleMS; + float paramTickTransitionMS; + float paramVolumeMinimumToTrigger; + float paramVolumeMax; + float paramVolumeMin; + float paramVolumeRestore; + int paramConsecutiveMinimumsToEnd; + int paramConsecutiveMinimumsToTrigger; + std::vector paramExcludedExecutables; + std::wstring paramControlledExecutable; + std::wstring paramCommandOnDuck; + std::wstring paramCommandOnUnduck; + + int currentConsecutiveMinimumsToTrigger = 0; + int currentConsecutiveMinimumsToEnd = 0; + + bool quitRequested = false; + bool bypassed = false; + + // string conversion functions + std::string wStringToString(const std::wstring &wstr); + std::wstring stringToWString(const std::string &str); + + // create a default ini settings file if one is not found + void tryCreateDefaultSettingsINI(); + + std::wstring getAbsoluteExecutablePath(); + std::wstring getSettingsINIPath(); + + // get the max peak audio level while ignoring any executables with names in + // the excludedExecutables vector + float + getMaxPeakAudioLevel(const std::vector &excludedExecutables); + + // initialise COM objects, etc... + bool init(); + + // returns all audio sessions at the current point as reported by the + // session manager + std::vector> getAudioSessions(); + + // find an audio session instance with the given executable name in the + // format of "abc.exe" + std::shared_ptr + getAudioSessionByExecutableName(const std::wstring &executableName); + + // run a windows command (i.e., "cmd.exe /c ...") silently in the + // background. + void runCommandSilent(std::wstring &command); + + // ### INI TEMPLATE FUNCTIONS ### + // a series of template functions to read a value from the ini and convert + // it to the appropriate type and store it in the given variable + + // read a value from the ini as a string + std::wstring readINIValueStringW(const std::wstring §ion, + const std::wstring &key); + + // generic + template + inline void readINIValue(const std::wstring §ion, + const std::wstring &key, T &value) { + value = readINIValueStringW(section, key); + } + + // int + template <> + inline void readINIValue(const std::wstring §ion, + const std::wstring &key, int &value) { + value = std::stoi(readINIValueStringW(section, key)); + } + + // float + template <> + inline void readINIValue(const std::wstring §ion, + const std::wstring &key, float &value) { + value = std::stof(readINIValueStringW(section, key)); + } + + // list of values separated by '/'s + template <> + inline void readINIValue(const std::wstring §ion, + const std::wstring &key, + std::vector &value) { + value.clear(); + auto str = readINIValueStringW(section, key); + + size_t start = 0; + size_t end = str.find(L"/"); + + while (end != std::wstring::npos) { + value.push_back(str.substr(start, end - start)); + start = end + 1; + end = str.find(L"/", start); + } + + value.push_back(str.substr(start)); + } + + public: + Engine(); + ~Engine(); + + // get or create singleton + static Engine *get(); + + // runs the engine and blocks until it quit is requested or an error occurs. + // returns whether or not the engine quit because of an error. + bool running(); + + bool hasError() const; + std::wstring &getErrorString(); + std::wstring &getShortStatusString(); + + // open the settings ini with the default windows application for opening + // .ini files (usually notepad). returns if successfully opened + bool openSettingsINI(); + + // read the settings ini and updates param variables. returns if read all + // successfully. this function can also be used to reload the ini during + // execution. + bool readSettingsINI(); + + // tell the engine to quit on the next tick. (running() will return) + void requestQuit(); + + void setBypassed(bool newBypassed); + bool getBypassed() const; +}; diff --git a/src/UI.cpp b/src/UI.cpp new file mode 100644 index 0000000..63c96a1 --- /dev/null +++ b/src/UI.cpp @@ -0,0 +1,179 @@ +#include "UI.h" + +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, + LPARAM lParam) { + switch (uMsg) { + case WM_USER + 1: + // when click on tray icon, open context menu + switch (lParam) { + case WM_LBUTTONDOWN: + createContextMenu(hwnd); + break; + case WM_RBUTTONDOWN: + createContextMenu(hwnd); + break; + } + break; + + case WM_DESTROY: + PostQuitMessage(0); + break; + + case WM_COMMAND: + // menu item selected in context menu + switch (LOWORD(wParam)) { + case ID_TRAYMENU_OPEN_SETTINGS: + Engine::get()->openSettingsINI(); + break; + + case ID_TRAYMENU_EXIT: + Shell_NotifyIcon(NIM_DELETE, &nid); + DestroyIcon(nid.hIcon); + PostQuitMessage(0); + quit(); + break; + + case ID_TRAYMENU_RELOAD_SETTINGS: + Engine::get()->readSettingsINI(); + break; + + case ID_TRAYMENU_TOGGLE: + Engine::get()->setBypassed(!Engine::get()->getBypassed()); + createTrayIcon(hwnd); + break; + } + break; + + case WM_QUERYENDSESSION: + // windows is asking if this app can exit when shutdown + return TRUE; + + case WM_ENDSESSION: + if (wParam) { // if user has shutdown/logged off of the computer + Shell_NotifyIcon(NIM_DELETE, &nid); + DestroyIcon(nid.hIcon); + PostQuitMessage(0); + quit(); + } + break; + + default: + return DefWindowProc(hwnd, uMsg, wParam, lParam); + } + return 0; +} + +void createTrayIcon(HWND hwnd) { + // delete any existing icon and create a new one + Shell_NotifyIcon(NIM_DELETE, &nid); + + nid.cbSize = sizeof(NOTIFYICONDATA); + nid.hWnd = hwnd; + nid.uID = 1; + nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; + + // choose the correct icon based on bypassed status + nid.hIcon = LoadIcon(GetModuleHandle(NULL), + MAKEINTRESOURCE((!Engine::get()->getBypassed()) + ? IDI_ICON_DEFAULT + : IDI_ICON_BYPASSED)); + nid.uCallbackMessage = WM_USER + 1; + wcscpy_s(nid.szTip, PROG_BRAND_NAME); + + // add the icon to the tray area + Shell_NotifyIcon(NIM_ADD, &nid); +} + +void createContextMenu(HWND hwnd) { + // make the window foreground otherwise clicking away from the menu will not + // close it. + SetForegroundWindow(hwnd); + + HMENU hMenu = LoadMenu(GetModuleHandle(NULL), MAKEINTRESOURCE(IDR_MENU1)); + HMENU hSubMenu = GetSubMenu(hMenu, 0); + + // set disabled/enabled text + ModifyMenuW(hSubMenu, ID_TRAYMENU_TOGGLE, MF_BYCOMMAND | MF_STRING, + ID_TRAYMENU_TOGGLE, + (Engine::get()->getBypassed()) ? L"Enable" : L"Disable"); + + // set status string + ModifyMenuW(hSubMenu, ID_TRAYMENU_STATUSTEXT, + MF_BYCOMMAND | MF_STRING | MF_DISABLED, ID_TRAYMENU_STATUSTEXT, + Engine::get()->getShortStatusString().c_str()); + + // show the menu at the appropriate point based on cursor pos + POINT pt; + GetCursorPos(&pt); + TrackPopupMenu(hSubMenu, TPM_LEFTALIGN | TPM_LEFTBUTTON, pt.x, pt.y, 0, + hwnd, NULL); +} + +void runUI() { + // window class + WNDCLASS wc = {0}; + wc.hInstance = GetModuleHandle(NULL); + wc.lpfnWndProc = WindowProc; + wc.lpszClassName = PROG_BRAND_NAME; + RegisterClass(&wc); + + // create a MESSAGE ONLY window (not visible) + // https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows + hwnd = CreateWindow(wc.lpszClassName, NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, + NULL, NULL, NULL); + + createTrayIcon(hwnd); + + // handle messages while a quit is not requested... + MSG msg{}; + while (!quitRequested) { + // cannot use GetMessage as it blocks the while loop so we cant quit + // from other threads... + if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Sleep((DWORD)(UI_POLL_RATE_MS)); + } +} + +void createErrorBox(std::wstring &errorString) { + std::wstring errorStringFormatted = L"Fatal error:\n"; + errorStringFormatted += errorString; + + MessageBoxW(NULL, errorStringFormatted.c_str(), PROG_BRAND_NAME, + MB_OK | MB_ICONERROR); + + // we should quit if an error has been encountered... + quit(); +} + +int main() { return run(); } + +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) { + return run(); +} + +int run() { + uiThread = std::thread(runUI); + + auto engine = Engine::get(); + if (engine->running()) + createErrorBox(engine->getErrorString()); + + if (uiThread.joinable()) + uiThread.join(); + + int error = (engine->hasError()) ? 1 : 0; + engine = nullptr; // to call deconstructor + + return error; +} + +void quit() { + // technically, this can be called from multiple threads, but as the values + // are only ever getting changed to true, there should be no need for mutex + quitRequested = true; + Engine::get()->requestQuit(); +} diff --git a/src/UI.h b/src/UI.h new file mode 100644 index 0000000..2208608 --- /dev/null +++ b/src/UI.h @@ -0,0 +1,36 @@ +#pragma once + +#include "resource.h" + +#include "Engine.h" + +static NOTIFYICONDATA nid; +static HWND hwnd; + +static std::thread uiThread; + +// the poll rate to check and process new messages in ms +static const float UI_POLL_RATE_MS = 50.0f; + +// use for handling when quitting is requested from across multiple threads +static bool quitRequested = false; + +int main(); // console entry point (debug) +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, _In_ int nShowCmd); // win entry +int run(); + +// window proc callback function +LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +void createTrayIcon(HWND hwnd); +void createContextMenu(HWND hwnd); +void createErrorBox(std::wstring &errorString); + +// function that handles creating ui and processing messages. +// should run on a separate thread. +void runUI(); + +// tells the ui AND ENGINE to quit asap. the entire program will terminate when +// both have honoured the request and quit. +void quit();