From efb424a7593fd8db337de55caebc02cb16eeed70 Mon Sep 17 00:00:00 2001 From: VideoPlayerCode <38923130+VideoPlayerCode@users.noreply.github.com> Date: Fri, 2 Aug 2019 23:55:12 +0200 Subject: [PATCH] Volume Linker v1.0.0.1 --- .gitignore | 352 +++++ LICENSE.txt | 674 ++++++++ README.md | 325 ++++ Screenshot.png | Bin 0 -> 14283 bytes VolumeLinker.sln | 31 + VolumeLinker/.editorconfig | 22 + VolumeLinker/AudioDevice.cpp | 91 ++ VolumeLinker/AudioDevice.h | 42 + VolumeLinker/AudioDeviceManager.cpp | 341 ++++ VolumeLinker/AudioDeviceManager.h | 65 + VolumeLinker/AudioEndpointVolumeCallback.h | 117 ++ VolumeLinker/DPIAware.manifest | 50 + VolumeLinker/VolumeLinker.rc | Bin 0 -> 12860 bytes VolumeLinker/VolumeLinker.vcxproj | 231 +++ VolumeLinker/VolumeLinker.vcxproj.filters | 71 + VolumeLinker/VolumeLinker_Disabled.ico | Bin 0 -> 185879 bytes VolumeLinker/VolumeLinker_Main.ico | Bin 0 -> 176345 bytes VolumeLinker/framework.h | 71 + VolumeLinker/helpers.h | 35 + VolumeLinker/main.cpp | 1062 +++++++++++++ .../non-package libraries/WinReg/WinReg.hpp | 1384 +++++++++++++++++ VolumeLinker/packages.config | 4 + VolumeLinker/resource.h | 38 + VolumeLinker/targetver.h | 59 + 24 files changed, 5065 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Screenshot.png create mode 100644 VolumeLinker.sln create mode 100644 VolumeLinker/.editorconfig create mode 100644 VolumeLinker/AudioDevice.cpp create mode 100644 VolumeLinker/AudioDevice.h create mode 100644 VolumeLinker/AudioDeviceManager.cpp create mode 100644 VolumeLinker/AudioDeviceManager.h create mode 100644 VolumeLinker/AudioEndpointVolumeCallback.h create mode 100644 VolumeLinker/DPIAware.manifest create mode 100644 VolumeLinker/VolumeLinker.rc create mode 100644 VolumeLinker/VolumeLinker.vcxproj create mode 100644 VolumeLinker/VolumeLinker.vcxproj.filters create mode 100644 VolumeLinker/VolumeLinker_Disabled.ico create mode 100644 VolumeLinker/VolumeLinker_Main.ico create mode 100644 VolumeLinker/framework.h create mode 100644 VolumeLinker/helpers.h create mode 100644 VolumeLinker/main.cpp create mode 100644 VolumeLinker/non-package libraries/WinReg/WinReg.hpp create mode 100644 VolumeLinker/packages.config create mode 100644 VolumeLinker/resource.h create mode 100644 VolumeLinker/targetver.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8efec65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,352 @@ +## 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/master/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/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# 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/ + +# 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 +*.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 + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# 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 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/ + +# 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/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3877ae0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f28d3f0 --- /dev/null +++ b/README.md @@ -0,0 +1,325 @@ +# Volume Linker by VideoPlayerCode + +![Volume Linker Screenshot](https://raw.githubusercontent.com/VideoPlayerCode/VolumeLinker/master/Screenshot.png) + +A minimalistic and robust application for linking/synchronizing the volume +between two audio interfaces in Windows. + +Potential uses include audio signal processing and routing setups. + +For example, you may be routing all system audio to a virtual audio device +(in other words, a virtual cable), and then using an audio DSP/VST host +which fetches the audio from the virtual cable, processes it with some +VST effects to improve the sound quality or apply virtual surround effects +(such as the excellent Waves NX), and then finally outputs the result +to your system's real sound card. + +The classic problem with such a configuration is that your volume controls +in Windows won't work anymore, since they will only change the volume +of your system's "default playback device" (which is your "virtual cable" +device in such a configuration), and therefore won't change the volume +of your *hardware* output device. In other words, you'll be stuck with +a static volume amount, and changing the volume becomes very tedious. + +That's where this application comes in... It allows you to synchronize +the volume of your real hardware output device to automatically match +the state of your virtual device. So when you change volume in Windows, +and the virtual device's volume changes, your real hardware device +will also change volume automatically and seamlessly! + +This application was created to solve that specific problem, but there +may be other use cases for it as well. Have fun! + + +## Installation + +1. Unzip the application into any folder on your hard disk. + +2. Start `VolumeLinker32.exe` (32-bit) or `VolumeLinker64.exe` (64-bit). + There is no difference between the two applications, but if you have + a 64-bit version of Windows, you should be using the 64-bit version + since it's optimized for such processors. + + +## How to Use + +1. Select your source audio device in the `Master (sync from)` dropdown. + +2. Select your target audio device in the `Slave (sync to)` dropdown. + +3. Press the `Link Devices` button. The volume of the master device will + immediately be copied to the slave device. And any changes to the master + device's volume will instantly be synchronized to the slave device + while the devices remain linked. + +4. The interface also contains a volume slider and a mute control, which + automatically shows you the master device's volume, and allows you to + change the volume by using the slider. This is useful for testing the + link and making sure that the slave device's volume is also changing. + +5. The application *must* continue running to maintain the link. Feel free + to minimize the application to hide its window. The application will + sit in the system tray (notification area), and you can always open + it again by clicking on its tray icon, or by launching the .exe again. + + +## Automatic Startup at Login + +1. You must use a method such as an `Autostart` link or the Windows + `Task Scheduler` to automatically start the application. If you are + unfamiliar with how to do this, simply use your favorite web search + engine and educate yourself. + +2. The application supports two command-line switches intended to make + your automatic startup experience better. They are as follows: + + `/minimized` (or `/m` or `/minimize`): + Volume Linker will start in the "minimized to tray" state, so that + you won't have to see its window automatically appear on your desktop. + + `/link` (or `/l`): + Always attempt to link devices (if both master and slave are selected) + at startup, even if they were unlinked while the program was last + closed. This attempt is done silently (no error popup boxes). If there + are any problems, the devices will simply remain unlinked. + +3. The recommended startup command is `VolumeLinker64.exe /l /m`, which + will ensure that the application starts minimized, and that it always + attempts to link the last-selected devices at startup, regardless of + whether you may have manually unlinked them during a previous session. + + +## Settings Storage + +1. The settings are automatically saved when the application is closed, + but only if you've *manually* changed any of the settings since opening + the application (by changing the selections in the master/slave dropdown + menus, or pressing the link/unlink button). + + This behavior is simply intended to automatically protect you against + inadvertent overwriting of your settings, such as if you open the + application and discover that your audio device is temporarily missing + from your system (perhaps you've disabled it) and has therefore become + de-selected. In that case, just close the application, fix the problem, + and start the application again. Your old settings will still be active, + thanks to the save-protection. Users don't even have to think about this + protection method, since it is automatic, but it is mentioned here since + some people are sure to be curious about it. + +2. Device selections are saved by their internal hardware IDs, which is + the only correct and robust method to perfectly identify a specific + audio device (since multiple devices may have the same name). + + However, this may not always work properly with USB audio devices, since + they *possibly* change their hardware ID every time they are connected + and disconnected. Thankfully, most people use internal or always-connected + devices with permanent IDs. Check your device if you are having problems. + +3. All settings are stored per-user, in the following registry key: + + `HKEY_CURRENT_USER\Software\VolumeLinker` + + If you want a portable version of this application, and you have OCD + about cleanliness, then you may want to make a wrapper which exports + and deletes that registry key on exit, and then re-imports it again + the next time you start the application. That's your own responsibility, + however. This application will always be designed to use the registry + for settings storage, since it is the most byte-safe and reliable storage + method on Windows. + + Furthermore, the settings that are stored are very minimal, and by + most standards Volume Linker already qualifies as a portable application, + since it consists of a single re-distributable .exe file which runs on + any supported system. + + +## System Requirements and Performance + +Supported Operating Systems: + +* Windows 10. +* Windows 8.1. +* Windows 7 SP1 (Service Pack 1). + +Older operating systems are **not** supported, due to lacking the necessary +system APIs. Future operating systems after Windows 10 *should* work too. + +The application has been fully tested on all supported operating systems +above. However, on Windows 8.1 you will see Microsoft's "SmartScreen" +warning you about the lack of a developer certificate, so simply read their +silly warning and then accept it -- and no, there's no way that I'm going +to pay Microsoft money just to distribute this application with a useless +Windows 8 certificate. If you're using Windows 8, you should be familiar +with seeing annoying "SmartScreen" warnings by now! You may *also* get +false (incorrect) detections in Microsoft's Defender for old versions +of Windows; in that case, update your Defender to the latest version, +or just verify that it's clean via the online scanner at virustotal.com. + +You may also be happy to know that there's full support for high-DPI +screens, which means that the GUI will always look crisp and beautiful. + +As for performance, you can expect the application to use `0% CPU`, +and around `1-2 MB` of RAM. However, if you are doing a ton of rapid +volume slider movements via Windows or the application's GUI (which have +very smooth volume sliders and send a ton of volume change events), +you may get a temporary spike of `0.2-0.8% CPU` during the movement, +before it returns back to `0% CPU` again. Either way, as you can see, +the application is extremely light and won't impact your system at all! + +Furthermore, the application contains the *statically embedded* version +of the Microsoft Visual C++ runtime, which means that you won't have +to install any C++ runtime on your system. It - just - works! + +That's why the .exe files are quite large, but it's worth it for the +convenience of not needing to install any additional software! + + +## Notes about Fast User Switching + +When you're performing "fast user switching", the other users on your +computer actually *remain logged in*, and their applications continue +to run on your system even while you're using another account. + +Therefore, if you are using Volume Linker, and then "fast switching" to +another account, you won't be able to open Volume Linker on the *other* +account, and will see an error message about only being able to run "one +instance of Volume Linker per machine". + +This is because it would be extremely dangerous to link more than two +devices with each other. For example, one user may sync device A to +device B, and the other may sync device B to device A, which would +create an infinite loop that would do very bad things to your system. + +There are multiple ways to get around this limitation: + +* Start Volume Linker on any of your accounts, and then do a "fast user + switch" to one of your other account(s). The link will remain active, + and any volume changes will automatically be synced even while you + are using the other accounts. That should be good enough for most + people. + +* Alternatively, simply close Volume Linker before switching accounts. + +* But the best alternative would simply be to just do a regular "Log Out", + which will close all of the user's applications completely. This means + that your other users get 100% of your computer's performance, since + no other accounts remain logged in and wasting CPU. It also means that + there won't be any conflicts, since Volume Linker won't be running on + the other accounts anymore. This is the recommended method! + + +## Software License and Copyright + +This software is licensed under the GNU General Public License v3. + +Copyright (c) 2019 VideoPlayerCode. + +Website: +https://github.com/VideoPlayerCode/VolumeLinker + + +## Recommended 3rd Party Software + +The intention for creating Volume Linker was to make it easy to control +your system's *hardware* volume output via a *virtual* audio device, +when you're processing system audio through special effects. + +Therefore, this section simply lists software that I highly recommend +for people who want to do audio processing on their system: + +* **Best virtual audio cable**: "Virtual Audio Cable (VAC)" by Evgenii + Muzychenko, from https://vac.muzychenko.net/en/. + + There are many other applications for "virtual cables", but this + is the highest performing, most accurately programmed of them all. + The author is a genius of Windows driver programming, who has been + working on this software since 1998 and knows everything about + the Windows audio driver APIs, and his application allows complete + customization of the virtual cables it creates. You should be + creating cables using the "WaveRT" protocol if you are on Win10, + since that's the highest-performance, lowest-latency protocol. + + Some free or alternative drivers exist (such as LoopBeAudio and + VB-Cable), but all of them are buggy and will not work properly + for all cases. It's clear that their authors lack the technical + skill and driver knowledge that went into making VAC. + +* **Best headphone calibration plugin**: "Sonarworks Reference + for Headphones", from https://www.sonarworks.com/reference/headphones. + + All headphones and speakers color the sound, and this plugin is + used by music professionals to calibrate the frequency profile + of your headphones/speakers, to balance the sound for perfect + tonal accuracy. It is the industry standard for this purpose. + +* **Best crossfeed processing plugin**: "CanOpener Studio" by + Goodhertz, from https://goodhertz.co/canopener-studio. + + When you are listening to stereo music on headphones, your ears + will get a very unnatural and fatiguing stereo image, since each + ear gets an isolated signal, with zero ambience or feeling. This + can quickly lead to claustrophobia and fatigue. Music professionals + therefore use applications such as the excellent CanOpener Studio, + to "open up" the headphones by intelligently blending the left + and right signals, which creates a very natural and airy sound + without changing the audio integrity whatsoever. This is perfect + even for the most demanding audiophiles. + +* **Best virtual surround plugin**: "Waves NX" by Waves, + from https://www.waves.com/plugins/nx. + + It is actually possible to create virtual surround sound by + applying binaural HRTF (head-related transfer function) processing + to a signal, and calculating the filtering that your ears and head + would have done to provide "locational clues" in the real world. + + The Waves NX plugin is without a doubt the industry leader when + it comes to transparent sound and realism. They are used by + music producers to listen to their mixes while working, and + is therefore tuned for transparency and realism. It's like + listening to your audio in a really high-quality studio with + perfect acoustics. And the plugin lets you tweak the speaker + positions and room ambience amounts, for total control. It's + also *the only* "virtual surround" solution that accurately + succeeds in making it feel like sounds are *behind* you (which + is the most difficult direction to achieve correctly). + + To use this plugin with headphones, you simply have to create a + virtual audio cable with 8 channels, and configure it as + a "speaker pin" in the VAC Control Panel, and then you + use the Windows "speaker configuration" properties to choose + that your virtual cable is a "7.1 surround" system, and + then set that cable as your system's default output device. + + After that, you simply need a VST plugin host which is capable + of routing your virtual cable into the "Waves NX" plugin, and + then out to your headphones as binaural stereo with amazing + surround sound feeling. + + This will give you surround in all movies and games, and + what's even more amazing is that Waves NX only uses 0.5-1.6% + CPU, which is stunning considering what it achieves. + +* **Best audio plugin host**: "Element Lite" (free) by + Kushview, from https://kushview.net/element/. + + There is simply no other application which can do the same + thing for free. Element is incredible. It allows you to configure + an audio input and output, and then lets you hook up virtual + connectors from every individual channel, through any VST plugins + you want to use, and then out to your system's audio output. + + Be sure that you set its audio mode to "Windows Audio" (*not* + "exclusive mode"), and set your speakers/headphones as the + output, and the virtual 7.1 cable as your input. + + From there, you will now be able to insert any VST plugins + you own on your system. Furthermore, Element uses extremely + little CPU (about 0.2-0.3% for me), and only about 20MB of + RAM. So there's no doubt that this is the best plugin host + that you can use for realtime audio purposes. + + If you want to insert Waves NX, you should be using the "Waves + NX 7.1/Stereo" plugin, which converts from surround to stereo. + + Have fun! diff --git a/Screenshot.png b/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..6331e89ce3f33caebb57ad662ca4a7ae57977c12 GIT binary patch literal 14283 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

lzwu2u-g*NYYjD0eejt+7Zv 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 literal 0 HcmV?d00001 diff --git a/VolumeLinker.sln b/VolumeLinker.sln new file mode 100644 index 0000000..6d99e5e --- /dev/null +++ b/VolumeLinker.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "VolumeLinker", "VolumeLinker\VolumeLinker.vcxproj", "{5D0AF82B-F01D-4C26-9466-CE4FF21E7868}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Debug|x64.ActiveCfg = Debug|x64 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Debug|x64.Build.0 = Debug|x64 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Debug|x86.ActiveCfg = Debug|Win32 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Debug|x86.Build.0 = Debug|Win32 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Release|x64.ActiveCfg = Release|x64 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Release|x64.Build.0 = Release|x64 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Release|x86.ActiveCfg = Release|Win32 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2E2250FE-A5E1-49F8-9399-FBC7525A89D3} + EndGlobalSection +EndGlobal diff --git a/VolumeLinker/.editorconfig b/VolumeLinker/.editorconfig new file mode 100644 index 0000000..f90440c --- /dev/null +++ b/VolumeLinker/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# Windows-style newlines with a newline ending in every file +[*] +end_of_line = crlf +insert_final_newline = true + +# C/C++ files +[*.{h,hpp,c,cpp}] +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +# Manifest files +[*.manifest] +charset = utf-8 +trim_trailing_whitespace = true +indent_style = tab diff --git a/VolumeLinker/AudioDevice.cpp b/VolumeLinker/AudioDevice.cpp new file mode 100644 index 0000000..729cbae --- /dev/null +++ b/VolumeLinker/AudioDevice.cpp @@ -0,0 +1,91 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#include "AudioDevice.h" +#include "helpers.h" + +AudioDevice::AudioDevice( + size_t itemOffset, + wil::com_ptr_nothrow pEndpoint) +{ + HRESULT hr; + + // Get the endpoint's ID string. + wil::unique_cotaskmem_string pwszID; + hr = pEndpoint->GetId(&pwszID); + THROW_IF_COM_FAILED(hr, "Unable to retrieve audio endpoint ID."); + + // Open device property storage. + wil::com_ptr_nothrow pProps; + hr = pEndpoint->OpenPropertyStore(STGM_READ, &pProps); + THROW_IF_COM_FAILED(hr, "Unable to open device property storage."); + + // Get the endpoint's friendy-name property. + // NOTE: Multiple endpoints can have identical names (but IDs will always differ). + wil::unique_prop_variant varName; + hr = pProps->GetValue(PKEY_Device_FriendlyName, &varName); + THROW_IF_COM_FAILED(hr, "Unable to get name of audio endpoint."); + + m_iItemOffset = itemOffset; + m_pEndpoint = std::move(pEndpoint); + m_wsId = pwszID.get(); + m_wsName = varName.pwszVal; +} + +AudioDevice::~AudioDevice() +{ +} + +wil::com_ptr_nothrow AudioDevice::activateAudioEndpointVolume() +{ + HRESULT hr; + + // Create a COM object for the device, with the "endpoint volume" interface. + wil::com_ptr_nothrow pEndptVol; + hr = m_pEndpoint->Activate(__uuidof(IAudioEndpointVolume), + CLSCTX_ALL, NULL, (void**)& pEndptVol); + THROW_IF_COM_FAILED(hr, "Unable to open device endpoint volume control."); + + return pEndptVol; +} + +const size_t AudioDevice::getItemOffset() +{ + return m_iItemOffset; +} + +const wstring& AudioDevice::getId() +{ + return m_wsId; +} + +const LPWSTR AudioDevice::getIdMS() +{ + return (LPWSTR)m_wsId.c_str(); +} + +const wstring& AudioDevice::getName() +{ + return m_wsName; +} + +const LPWSTR AudioDevice::getNameMS() +{ + return (LPWSTR)m_wsName.c_str(); +} diff --git a/VolumeLinker/AudioDevice.h b/VolumeLinker/AudioDevice.h new file mode 100644 index 0000000..a830b98 --- /dev/null +++ b/VolumeLinker/AudioDevice.h @@ -0,0 +1,42 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +#include "framework.h" + +using std::wstring; + +class AudioDevice +{ +private: + size_t m_iItemOffset; + wil::com_ptr_nothrow m_pEndpoint; + wstring m_wsId; + wstring m_wsName; +public: + AudioDevice(size_t itemOffset, wil::com_ptr_nothrow pEndpoint); + ~AudioDevice(); + wil::com_ptr_nothrow activateAudioEndpointVolume(); + const size_t getItemOffset(); + const wstring& getId(); + const LPWSTR getIdMS(); + const wstring& getName(); + const LPWSTR getNameMS(); +}; diff --git a/VolumeLinker/AudioDeviceManager.cpp b/VolumeLinker/AudioDeviceManager.cpp new file mode 100644 index 0000000..9fae5e5 --- /dev/null +++ b/VolumeLinker/AudioDeviceManager.cpp @@ -0,0 +1,341 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#include "AudioDeviceManager.h" +#include "helpers.h" + +using std::move; + +AudioDeviceManager::AudioDeviceManager( + GUID processGUID) +{ + HRESULT hr; + + // The exit code from the class will always be zero (no error) unless the callback failed. + m_exitCode = 0; + + // Save the GUID that we will identify our COM calls with. + if (processGUID == GUID_NULL) { + throw std::runtime_error("Invalid process GUID given to AudioDeviceManager."); + } + m_processGUID = processGUID; + + // At the moment we don't have any dialog handle that we're attached to. + m_hDialog = NULL; + m_iMuteCheckboxID = 0; + m_iVolumeSliderID = 0; + + // Get enumerator for audio endpoint devices. + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), + NULL, CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), + (void**)& m_pEnumerator); + THROW_IF_COM_FAILED(hr, "Unable to create audio device enumerator."); + + // Get all audio-rendering devices (except ones that are disabled/not present). + hr = m_pEnumerator->EnumAudioEndpoints( + eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_UNPLUGGED, &m_pCollection); + THROW_IF_COM_FAILED(hr, "Unable to enumerate audio devices."); + + // Count the discovered devices. + UINT count; + hr = m_pCollection->GetCount(&count); + THROW_IF_COM_FAILED(hr, "Unable to count audio devices in collection."); + m_collectionCount = static_cast(count); + if (m_collectionCount == 0) { + throw std::runtime_error("No audio devices found."); + } + + for (size_t i = 0; i < m_collectionCount; ++i) { + // Get pointer to endpoint number i. + wil::com_ptr_nothrow pEndpoint; + hr = m_pCollection->Item(static_cast(i), &pEndpoint); + THROW_IF_COM_FAILED(hr, "Unable to retrieve an audio endpoint."); + + // Save device to vector. + AudioDevice device(i, move(pEndpoint)); + m_audioDevices.push_back(move(device)); + } + + // Sort the devices by name in ascending order (case SENSITIVE). + std::sort(m_audioDevices.begin(), m_audioDevices.end(), [](AudioDevice& a, AudioDevice& b) -> bool + { + // NOTE: This requires that "_wsetlocale" has been executed to set the program's + // locale, otherwise it uses the "C" (ANSI) basic locale by default and can't + // achieve a case-insensitive comparison of international letters. + return _wcsicmp(a.getName().c_str(), b.getName().c_str()) < 0; // Case Insensitive (Microsoft implementation). + }); + + // Validate collection sizes. + if (m_collectionCount != m_audioDevices.size()) { + throw std::runtime_error("Unable to retrieve all audio device information."); + } + + // There is no link at the beginning. + m_bLinkActive = false; + m_iMasterDeviceIdx = -1; + m_iSlaveDeviceIdx = -1; + m_pMasterEndptVol = nullptr; + m_pSlaveEndptVol = nullptr; + + // Register a callback lambda which calls our class instance's volume callback. + m_endpointVolumeCallback.registerCallback( + [this](const PAUDIO_VOLUME_NOTIFICATION_DATA& pNotify) -> void { + this->_onVolumeCallback(pNotify); + }); +} + +AudioDeviceManager::~AudioDeviceManager() +{ + // Ensure that any callback-link between devices is unloaded first, before regular destruction. + this->unlinkDevices(); + + // Remove ourselves as callback from the inner AudioEndpointVolumeCallback class. + // NOTE: Probably completely pointless since that object and its reference to us + // would get destroyed in reverse creation-order and properly release anyway. + m_endpointVolumeCallback.unregisterCallback(); +} + +int AudioDeviceManager::getExitCode() noexcept +{ + return m_exitCode; +} + +const vector& AudioDeviceManager::getAudioDevices() +{ + return m_audioDevices; +} + +const AudioDevice& AudioDeviceManager::getDevice( + ptrdiff_t idx) +{ + if (idx < 0) { + throw std::runtime_error("Negative device number requested."); + } + + try { + return m_audioDevices.at(static_cast(idx)); + } + catch (const std::out_of_range&) { + throw std::runtime_error("Invalid device number requested."); + } +} + +bool AudioDeviceManager::isLinkActive() noexcept +{ + return m_bLinkActive; +} + +void AudioDeviceManager::linkDevices( + ptrdiff_t masterIdx, + ptrdiff_t slaveIdx) +{ + HRESULT hr; + + // Ensure that any existing link is broken first. + this->unlinkDevices(); + + // Don't allow circular links between the same device. + if (masterIdx == slaveIdx) { + throw std::runtime_error("Cannot link device to itself."); + } + + // Retrieve the devices (throws if the indices are invalid, negative, etc). + AudioDevice masterDevice = this->getDevice(masterIdx); + AudioDevice slaveDevice = this->getDevice(slaveIdx); + + // Connect to the "endpoint volume control" interfaces for both devices. + m_pMasterEndptVol = masterDevice.activateAudioEndpointVolume(); + m_pSlaveEndptVol = slaveDevice.activateAudioEndpointVolume(); + + // Register our callback to get volume/mute change notifications for the master device. + hr = m_pMasterEndptVol->RegisterControlChangeNotify( + (IAudioEndpointVolumeCallback*)& m_endpointVolumeCallback); + THROW_IF_COM_FAILED(hr, "Unable to register master audio endpoint volume callback."); + + // Signal the fact that the link is now active (since the callback registration succeeded). + m_bLinkActive = true; + m_iMasterDeviceIdx = masterIdx; + m_iSlaveDeviceIdx = slaveIdx; + + // Get the master device's current volume and mute-state. + BOOL bMuted; + float fMasterVolume; + hr = m_pMasterEndptVol->GetMute(&bMuted); + if (FAILED(hr)) { + this->unlinkDevices(); + throw std::runtime_error("Failed to retrieve master device's volume state. Link could not be established."); + } + hr = m_pMasterEndptVol->GetMasterVolumeLevelScalar(&fMasterVolume); + if (FAILED(hr)) { + this->unlinkDevices(); + throw std::runtime_error("Failed to retrieve master device's mute state. Link could not be established."); + } + + // Apply the same volume to the slave-device immediately. + bool success = this->_setSlaveVolume(fMasterVolume, bMuted); + if (!success) { + this->unlinkDevices(); + throw std::runtime_error("Failed to sync master volume to slave device. Link could not be established."); + } + + // Lastly, update the GUI immediately to display the master device's volume/mute state. + this->_updateDialog(fMasterVolume, bMuted); +} + +void AudioDeviceManager::unlinkDevices() noexcept +{ + // Unregister the master device's callback. + if (m_bLinkActive && m_pMasterEndptVol) { + // NOTE: Ignoring HRESULT since the only possible error is that the pointer is NULL. + m_pMasterEndptVol->UnregisterControlChangeNotify( + (IAudioEndpointVolumeCallback*)& m_endpointVolumeCallback); + } + + // Clear all device pointers (releases the old COM resources). + m_bLinkActive = false; + m_iMasterDeviceIdx = -1; + m_iSlaveDeviceIdx = -1; + m_pMasterEndptVol = nullptr; + m_pSlaveEndptVol = nullptr; +} + +ptrdiff_t AudioDeviceManager::getMasterDeviceIdx() noexcept +{ + return m_iMasterDeviceIdx; +} + +ptrdiff_t AudioDeviceManager::getSlaveDeviceIdx() noexcept +{ + return m_iSlaveDeviceIdx; +} + +void AudioDeviceManager::setDialog( + HWND hDlg, + ptrdiff_t muteCheckboxID, + ptrdiff_t volumeSliderID) +{ + m_hDialog = hDlg; + m_iMuteCheckboxID = muteCheckboxID; + m_iVolumeSliderID = volumeSliderID; +} + +void AudioDeviceManager::_updateDialog( + float fMasterVolume, + BOOL bMuted) +{ + if (m_hDialog == NULL) { + return; + } + + PostMessage(GetDlgItem(m_hDialog, static_cast(m_iMuteCheckboxID)), + BM_SETCHECK, (bMuted) ? BST_CHECKED : BST_UNCHECKED, 0); + + // Calculate slider position (while rounding halves up). + ptrdiff_t sliderVolume = static_cast((static_cast(MAX_VOL) * fMasterVolume) + 0.5); + if (sliderVolume < 0) { sliderVolume = 0; } + else if (sliderVolume > MAX_VOL) { sliderVolume = MAX_VOL; } + + PostMessage(GetDlgItem(m_hDialog, static_cast(m_iVolumeSliderID)), + TBM_SETPOS, TRUE, static_cast(static_cast(sliderVolume))); +} + +bool AudioDeviceManager::setMasterVolume( + float fVolume) noexcept +{ + // If we don't have any device, automatically return true. + if (!m_pMasterEndptVol) { + return true; + } + + // Attempt to set the volume, and only return true if successful. + // NOTE: Will also propagate to slave device, via the master device's callback. + HRESULT hr; + hr = m_pMasterEndptVol->SetMasterVolumeLevelScalar(fVolume, &m_processGUID); + if (FAILED(hr)) { return false; } + + return true; +} + +bool AudioDeviceManager::setMasterMute( + BOOL bMuted) noexcept +{ + // If we don't have any device, automatically return true. + if (!m_pMasterEndptVol) { + return true; + } + + // Attempt to set the mute-state, and only return true if successful. + // NOTE: Will also propagate to slave device, via the master device's callback. + HRESULT hr; + hr = m_pMasterEndptVol->SetMute(bMuted, &m_processGUID); + if (FAILED(hr)) { return false; } + + return true; +} + +bool AudioDeviceManager::_setSlaveVolume( + float fVolume, + BOOL bMuted) noexcept +{ + // If we don't have any device, automatically return true. + if (!m_pSlaveEndptVol) { + return true; + } + +#ifdef _DEBUG + OutputDebugStringA(std::string("SetSlave:" + std::to_string(fVolume) + " " + (bMuted ? "M" : "_") + "\n").c_str()); +#endif + + // Attempt to set the volume and mute-state, and only return true if both succeeded. + HRESULT hr; + hr = m_pSlaveEndptVol->SetMasterVolumeLevelScalar(fVolume, &m_processGUID); + if (FAILED(hr)) { return false; } + hr = m_pSlaveEndptVol->SetMute(bMuted, &m_processGUID); + if (FAILED(hr)) { return false; } + + return true; +} + +void AudioDeviceManager::_onVolumeCallback( + const PAUDIO_VOLUME_NOTIFICATION_DATA& pNotify) +{ + // Do nothing if the callback was somehow triggered while we don't have any linked master/slave devices. + if (!m_bLinkActive || !m_pMasterEndptVol || !m_pSlaveEndptVol) { + return; + } + + // Update dialog if the volume event wasn't sent by our own program... + if (pNotify->guidEventContext != m_processGUID) { + this->_updateDialog(pNotify->fMasterVolume, pNotify->bMuted); + } + + // Sync the volume to the slave device (regardless of who changed the master device's volume)... + bool success = this->_setSlaveVolume(pNotify->fMasterVolume, pNotify->bMuted); + if (!success && m_hDialog != NULL) { + // Quit the whole program (close the main dialog) if slave volume failed. + // NOTE: Posting a WM_CLOSE is the correct way to "close a window". However, it doesn't support + // signaling that there was an error. So we'll transmit that by setting our class "exit code" value. + m_exitCode = 1; + MessageBoxW(m_hDialog, L"Failed to sync master volume to slave device in callback. The program will now exit.", + L"Fatal Error", MB_OK); + PostMessage(m_hDialog, WM_CLOSE, 0, 0); + return; + } +} diff --git a/VolumeLinker/AudioDeviceManager.h b/VolumeLinker/AudioDeviceManager.h new file mode 100644 index 0000000..9c48694 --- /dev/null +++ b/VolumeLinker/AudioDeviceManager.h @@ -0,0 +1,65 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +#include "framework.h" +#include "AudioDevice.h" +#include "AudioEndpointVolumeCallback.h" + +using std::vector; + +class AudioDeviceManager +{ +private: + int m_exitCode; + GUID m_processGUID; + HWND m_hDialog; + ptrdiff_t m_iMuteCheckboxID; + ptrdiff_t m_iVolumeSliderID; + wil::com_ptr_nothrow m_pEnumerator; + wil::com_ptr_nothrow m_pCollection; + size_t m_collectionCount; + std::vector m_audioDevices; + bool m_bLinkActive; + ptrdiff_t m_iMasterDeviceIdx; + ptrdiff_t m_iSlaveDeviceIdx; + wil::com_ptr_nothrow m_pMasterEndptVol; + wil::com_ptr_nothrow m_pSlaveEndptVol; + AudioEndpointVolumeCallback m_endpointVolumeCallback; + + void _updateDialog(float fMasterVolume, BOOL bMuted); + bool _setSlaveVolume(float fVolume, BOOL bMuted) noexcept; + void _onVolumeCallback(const PAUDIO_VOLUME_NOTIFICATION_DATA& pNotify); + +public: + AudioDeviceManager(GUID processGUID); + ~AudioDeviceManager(); + int getExitCode() noexcept; + const vector& getAudioDevices(); + const AudioDevice& getDevice(ptrdiff_t idx); + bool isLinkActive() noexcept; + void linkDevices(ptrdiff_t masterIdx, ptrdiff_t slaveIdx); + void unlinkDevices() noexcept; + ptrdiff_t getMasterDeviceIdx() noexcept; + ptrdiff_t getSlaveDeviceIdx() noexcept; + void setDialog(HWND hDlg, ptrdiff_t muteCheckbox, ptrdiff_t volumeSlider); + bool setMasterVolume(float fVolume) noexcept; + bool setMasterMute(BOOL bMuted) noexcept; +}; diff --git a/VolumeLinker/AudioEndpointVolumeCallback.h b/VolumeLinker/AudioEndpointVolumeCallback.h new file mode 100644 index 0000000..4f30a0a --- /dev/null +++ b/VolumeLinker/AudioEndpointVolumeCallback.h @@ -0,0 +1,117 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +#include "framework.h" + +//----------------------------------------------------------- +// Client implementation of IAudioEndpointVolumeCallback +// interface. When a method in the IAudioEndpointVolume +// interface changes the volume level or muting state of the +// endpoint device, the change initiates a call to the +// client's IAudioEndpointVolumeCallback::OnNotify method. +//----------------------------------------------------------- +class AudioEndpointVolumeCallback : public IAudioEndpointVolumeCallback +{ + LONG m_references; // Reference counter. + std::function m_externalCallback; + +public: + AudioEndpointVolumeCallback() : + m_references(1) // Set counter to 1 reference when constructed. + { + } + + ~AudioEndpointVolumeCallback() + { + } + + // IUnknown methods -- AddRef, Release, and QueryInterface + + ULONG STDMETHODCALLTYPE AddRef() noexcept + { + return InterlockedIncrement(&m_references); + } + + ULONG STDMETHODCALLTYPE Release() noexcept + { + ULONG const refCount = InterlockedDecrement(&m_references); + if (0 == refCount) { + delete this; + } + return refCount; + } + + HRESULT STDMETHODCALLTYPE QueryInterface( + REFIID riid, + VOID** ppvInterface) noexcept + { + if (IID_IUnknown == riid) { + AddRef(); + *ppvInterface = static_cast(this); + } + else if (__uuidof(IAudioEndpointVolumeCallback) == riid) { + AddRef(); + *ppvInterface = static_cast(this); + } + else { + *ppvInterface = NULL; + return E_NOINTERFACE; + } + return S_OK; + } + + // Callback method for endpoint-volume-change notifications. + + HRESULT STDMETHODCALLTYPE OnNotify( + PAUDIO_VOLUME_NOTIFICATION_DATA pNotify) + { + if (pNotify == NULL) { + return E_INVALIDARG; + } + +#ifdef _DEBUG + OutputDebugStringA(std::string("Callback:" + std::to_string(pNotify->fMasterVolume) + " " + (pNotify->bMuted ? "M" : "_") + "\n").c_str()); +#endif + + // Execute any registered callback, but BLOCK any exceptions it throws. + try { + if (m_externalCallback) { + m_externalCallback(pNotify); // Throws if invalid (or throwing) callback. + } + } + catch (...) {} + + return S_OK; + } + + // Registering or unregistering external callback. + + void registerCallback( + std::function callback) + { + m_externalCallback = callback; + } + + void unregisterCallback() + { + m_externalCallback = nullptr; + } +}; diff --git a/VolumeLinker/DPIAware.manifest b/VolumeLinker/DPIAware.manifest new file mode 100644 index 0000000..4ef4e5b --- /dev/null +++ b/VolumeLinker/DPIAware.manifest @@ -0,0 +1,50 @@ + + + + + + True/PM + + PerMonitorV2 + + + diff --git a/VolumeLinker/VolumeLinker.rc b/VolumeLinker/VolumeLinker.rc new file mode 100644 index 0000000000000000000000000000000000000000..6ca350ceb682aa17209725aa697afec14faa7ea1 GIT binary patch literal 12860 zcmd^GZBH9V5Z=#~`X8>&7ot+YA>{3wF@{*Nje`vdRV2iF-57(6$xGGW-u8KBZhX7v zyR%Jc>!`BK`Sy0_eP?E8jz9lcb*nCNP1kd$Zs@MvExs3Sgs}sR-MTw>?E1K?x*ze0 z+@(8r1AK=VAK`k2u_3NpcZXl6_#NSn(0c9$b5`8lGWdpRL8$;ABg9Xk}_J%W^)8 zRh&UGpJ8)8_DL%{75QDk_C}zzAI!chl*Tm5OmmB8Tcn88J;H7)()TPoXz{ruw-GG& z9NbdJ>baI+Q?&DAjP^197;~0H9v>kSTFw9?^mHfgC(Jm;{iS<_u{HO_y^#6o{d6l* zRmsxABlkn#rOtq%;G&O=D0gKSF!taLj@-}kc`R5P z7;Ry6AJ-k(@iuaV@fyavSXBXrJ<;Gbv|*#h2PH_k&_~hpHNkg7 zJRv>S9{6T#pbU0JM(;%uDi8iGyAAhD4w~!GYE7aP=_7QVQ3d2SFfzKef+q@% z*jnJ4R5J$nlq&>|9to5-VA7j2I=+(?Y3L>3;-g0=bViA) zXovfQQsSLTRqbI}bW8n~-Anv^A(#!~-VC^ovGa>?RUlu(MVV{>3-LVx#x%ZaY(IOr zHpUzqHI!wDiFV9Lz6Y*+`(DPpE6J=zkn}B{s*gK$AK|$eiPwRju)2^k?U(sa*S!W# z+Ln#BWRy~SBRWj%j(es9nQqE1*oxPwMN!s-+QsU2%xzhC<_@B+DF8f1l;uLBUq|hfpS=p8M~NotH^V>o95w4Ia(LKo@a73i=`ezJIsxT zkl+%2e+A8__-#4dV?>uw=b= zwsV?*N=+&T>bx7ne>T=A@vMsks-kyBEn09JS9%g=+Pc#BTnB(vm3fSC{*%ytd^?#g z(GKkWSTf8|p0tKyS$0H%2O_~xk}n=al0JN2jxYZMIkvGEI@kxBvM0Dt89S0s=x6*{ zN21FX{DE2-ce3tiv$LnNvl(^tG(i1X41NoIby!dvR>k~-dY=}3pNpU5S>>5P_9K|Z zUdGKN&Z;fhvxkk#3YEDT`svOUn{d#BJ+OxsC}-5A@7zpL}Hs6|>t?;)o3+;8xf`8d-V6LHf% z)gzjv&WF7Uk3z3}4&RcGnL6d^T0L-CJZ~1>?43c6!@Qrlt0F?)S1<;k2q*&oXybIP68PaAoX%G#$(z5@B&K#HV*wMq4@ z5n>bNLymZ+-$iEsRccn0>j{2G(7smAmO%dwIAR0~IeA>53ubz!kP>rnN^J%CIJLI| ztgMc$0wQa7zskH3=Bx%(tOmGv%DEh4GtImMHv{BV=XhpR?+AyVWX}xnn?6Q)=iN2# zm*iAL?=w3$*_M^Y6=GUNKA6yFyQ)avNAQWPs_lVh<_$cP9YX80fOR>unH_Av66o1O z56h9othQOj&A&3*Yc0r+0?fK9;94Vual|XXmR^a;Ahw7)OG97jz%pWQZcD5;rp3_~W5@JAhf1)(@uaj^X`?PD3kBi>Ds2^3-FUW^V z4;uExN~le9H0*B_QJ)1PyYEp1rMOpcAMWz(X8oRalPASIJbpLItcE*%HlJWV`CIZ0 zmi45)fpTq6Be5`c6eDC#?1iwC%bcFIBIa-Gq1<6KlHBST7OFj8+W#<~%Q{SiapqX; z0dw|oPBz92h1r zl$}6o)Y>Fa>X)Avdcw~&5P8D_Tgp2+EKQ25;^_f)R`P9wuUN-;br<`8({MHfduq5?L$3h> zt7Gh|G8$|3%Vy4YC0|YOW!xBi_OoAOS7{eG4a;WvR3tUN`RT}~BH5L`tr|U(bg&Dr zH3D{fccG)%)`itwTTh*PUsFLEn~| z$_}X09_JfS#kY9Yab9bPHp1+HT}{4$v5%;yxF;bggz|VjI|i)t70+Mu=G%!fT(Ju( z6}DEl*AP3E#zb1IinT#uNJH|x1IjlcBY&1*&h|R&?XzD_$pD2MczQd}uR?hr{-#j02CnS}2t-jh>;XYJ^ zyVAHVBcET3zvUbmXLCml>-z}Kv-oX4zfli&>KwGr4OT;zp zE}AyOC#+e|*CUySrrnqZ+B^9+E%Pq4qrcC;*8I<4A?))`TIxgCM~LJ8R+7XlvzN({ z&~F?-V#Bfg?&C4SeXaQu&;RU#S?@+Io&H<>33r{on-ISDl2DucqOZ-*b^pD0L>D{y zQ%d%ow-(EFi_Kk_+xVSJdO!Z>g1wP7{jGXdD${QPhi}!J92Z+A`Fn!zLbLKW%YOrq G*S`SwlPx3w literal 0 HcmV?d00001 diff --git a/VolumeLinker/VolumeLinker.vcxproj b/VolumeLinker/VolumeLinker.vcxproj new file mode 100644 index 0000000..dc434d7 --- /dev/null +++ b/VolumeLinker/VolumeLinker.vcxproj @@ -0,0 +1,231 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {5D0AF82B-F01D-4C26-9466-CE4FF21E7868} + Win32Proj + VolumeLinker + 10.0 + + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + Application + true + v142 + Unicode + + + Application + false + v142 + true + Unicode + + + + + + + + + + + + + + + + + + + + + true + $(ProjectDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName)32 + $(ProjectDir)non-package libraries\;$(IncludePath) + true + + + true + $(ProjectName)64 + $(ProjectDir)non-package libraries\;$(IncludePath) + true + $(ProjectDir)$(Platform)\$(Configuration)\ + + + false + $(ProjectDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + $(ProjectName)32 + $(ProjectDir)non-package libraries\;$(IncludePath) + true + + + false + $(ProjectName)64 + $(ProjectDir)non-package libraries\;$(IncludePath) + true + $(ProjectDir)$(Platform)\$(Configuration)\ + + + + + + Level3 + Disabled + true + WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + %(AdditionalDependencies) + + + false + $(ProjectDir)DPIAware.manifest + + + + + + + Level3 + Disabled + true + _DEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + + + Windows + true + %(AdditionalDependencies) + + + false + $(ProjectDir)DPIAware.manifest + + + + + + + Level3 + MaxSpeed + true + true + true + WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + true + %(AdditionalDependencies) + + + false + $(ProjectDir)DPIAware.manifest + + + + + + + Level3 + MaxSpeed + true + true + true + NDEBUG;_WINDOWS;%(PreprocessorDefinitions) + true + MultiThreaded + + + Windows + true + true + true + %(AdditionalDependencies) + + + false + $(ProjectDir)DPIAware.manifest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/VolumeLinker/VolumeLinker.vcxproj.filters b/VolumeLinker/VolumeLinker.vcxproj.filters new file mode 100644 index 0000000..19f975c --- /dev/null +++ b/VolumeLinker/VolumeLinker.vcxproj.filters @@ -0,0 +1,71 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;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 + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Resource Files + + + + + Resource Files + + + Resource Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + + + + + + \ No newline at end of file diff --git a/VolumeLinker/VolumeLinker_Disabled.ico b/VolumeLinker/VolumeLinker_Disabled.ico new file mode 100644 index 0000000000000000000000000000000000000000..25a4e4ae450d2b9d7fdc75cf4680f5fd4a73eb89 GIT binary patch literal 185879 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 LXLy$X@ZbLjB@QKz literal 0 HcmV?d00001 diff --git a/VolumeLinker/framework.h b/VolumeLinker/framework.h new file mode 100644 index 0000000..716376b --- /dev/null +++ b/VolumeLinker/framework.h @@ -0,0 +1,71 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +// Sets the necessary values to target our application for Windows 7 SP1 and higher. +// NOTE: This file must be included BEFORE any Windows-related headers (to set their platform). +#include "targetver.h" + +// Enables Common Controls 6, to activate modern Windows GUI visual styled controls. +// Technique from: https://docs.microsoft.com/en-us/windows/win32/controls/cookbook-overview +#pragma comment(linker,"\"/manifestdependency:type='win32' \ +name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \ +processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +// Tell the linker to include additional, necessary libraries. +#pragma comment(lib, "Comctl32.lib") + +// Exclude rarely-used stuff from Windows headers. +#define WIN32_LEAN_AND_MEAN + +// Windows Headers. +#include + +// C Runtime Headers. +#include +#include +#include +#include +#include +#include // Locale core. +#include // Unicode locale settings. + +// Standard C++ Library. +#include +#include +#include +#include +#include + +// More Windows Headers. +#include // Common Controls GUI code. +#include // Helpers for controls (such as GET_X_LPARAM/GET_Y_LPARAM). +#include // Interacting with audio devices. +#include // Interacting with volume mixers for audio devices. +#include // Constants for "PKEY" device property storage. +#include // Necessary for NotifyIcon. + +// Windows Implementation Library. +// NOTE: Must be included *after* all Windows API headers! See https://github.com/Microsoft/wil/wiki/RAII-resource-wrappers +#include // https://github.com/Microsoft/wil/wiki/RAII-resource-wrappers +#include // https://github.com/microsoft/wil/wiki/WinRT-and-COM-wrappers + +// Registry Wrapper. +#include diff --git a/VolumeLinker/helpers.h b/VolumeLinker/helpers.h new file mode 100644 index 0000000..ad91a23 --- /dev/null +++ b/VolumeLinker/helpers.h @@ -0,0 +1,35 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +#define THROW_IF_COM_FAILED(hr, msg) \ + if (FAILED(hr)) \ + { throw std::runtime_error(msg); } + +// Maximum volume level on trackbar. +#define MAX_VOL 100 + +// Debugging helpers. +#define OUTPUT_DEBUG_VALUE_TOSTRING(any) \ + OutputDebugStringA((std::to_string(any) + "\r\n").c_str()); +#define OUTPUT_DEBUG_MSGBOXA(txt) \ + MessageBoxA(NULL, txt, "Message", MB_OK); +#define OUTPUT_DEBUG_MSGBOXW(txt) \ + MessageBoxW(NULL, txt, L"Message", MB_OK); diff --git a/VolumeLinker/main.cpp b/VolumeLinker/main.cpp new file mode 100644 index 0000000..6f7b719 --- /dev/null +++ b/VolumeLinker/main.cpp @@ -0,0 +1,1062 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#include "framework.h" +#include "resource.h" +#include "helpers.h" +#include "AudioDeviceManager.h" + +using std::vector; +using std::wstring; + +// Startup Option: Start the application minimized (hidden window). +static bool g_optStartMinimized = false; + +// Startup Option: Always attempt to link devices (if both master and slave are selected) +// at startup, even if they were unlinked while the program was last closed. This attempt +// is done silently (no error popup boxes). +// NOTE: This option does not activate "g_saveChanges", so if the last manually set state +// was "unlinked", the registry state will still say "unlinked" until the user manually +// toggles the master/slave/link-settings. This protects against inadvertent overwriting. +static bool g_optForceLink = false; + +// Program instance handle. +static HINSTANCE g_hInstance = NULL; + +// Dialog handle from dialog box procedure. +static HWND g_hDlg = NULL; + +// Whether we have a notification area icon. +static bool g_hasNotifyIcon = false; + +// Data for the notificiation area icon. +static NOTIFYICONDATA g_notifyIconData = {}; + +// Context-menu for notification area icon. +static HMENU g_hTrayMenu = NULL; + +// Application and tray icons. +static HICON g_iconLargeMain = NULL; // Large main icon. +static HICON g_iconSmallMain = NULL; // Small main icon. +static HICON g_iconLargeDisabled = NULL; // Large disabled icon. +static HICON g_iconSmallDisabled = NULL; // Small disabled icon. + +// Audio device manager for the whole program. +static std::unique_ptr g_deviceManager = nullptr; + +// Determines whether the user has MANUALLY changed any settings (which then needs saving). +static bool g_saveChanges = false; + +// Access rights for registry access (telling the 32-bit version to use 64-bit keys if opened on Win64). +static const REGSAM g_regDesiredAccess = KEY_READ | KEY_WRITE | KEY_WOW64_64KEY; + +// Registry key and value names. +static const auto g_regHKey = HKEY_CURRENT_USER; +static const auto g_regSoftwareKey = wstring(L"SOFTWARE\\VolumeLinker"); +static const auto g_regMasterDevice = wstring(L"MasterDevice"); +static const auto g_regSlaveDevice = wstring(L"SlaveDevice"); +static const auto g_regLinkActive = wstring(L"LinkActive"); + +#define APP_WM_ICONNOTIFY (WM_APP + 1) +#define APP_WM_BRINGTOFRONT (WM_APP + 6400) +#define APP_WINDOW_TITLE_32 L"Volume Linker (32-bit)" +#define APP_WINDOW_TITLE_64 L"Volume Linker (64-bit)" + +// Forward declarations of other functions. +void ExitCleanup() noexcept; +void Dlg_ShowAndForeground() noexcept; +ptrdiff_t Dlg_GetDropdownSelection(int dlgItem) noexcept; +void Dlg_ShowLinkState() noexcept; +void Dlg_LinkDevices(bool showErrors); +void Dlg_UnlinkDevices() noexcept; +bool Dlg_SaveSettings() noexcept; +BOOL CALLBACK AboutDlgProc(HWND, UINT, WPARAM, LPARAM); +BOOL CALLBACK QuitDlgProc(HWND, UINT, WPARAM, LPARAM); +BOOL CALLBACK MainDlgProc(HWND, UINT, WPARAM, LPARAM); + +int APIENTRY wWinMain( + _In_ HINSTANCE hInstance, + _In_opt_ HINSTANCE hPrevInstance, + _In_ LPWSTR lpCmdLine, + _In_ int nCmdShow) +{ + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + UNREFERENCED_PARAMETER(nCmdShow); + + // Refuse to open multiple instances of the program. + // NOTE: The mutex is reference-counted by the OS and perfectly auto-released AFTER our + // process is terminated. So we will NOT manually ReleaseMutex/CloseHandle. + // NOTE: A "Global" mutex means that all users can see it (ie. with "fast user switching"). + // NOTE: This "only allow one process on machine" locking is extremely important, to avoid + // multiple "volume link" callbacks all doing the same (or opposite) things and conflicting. + auto singleInstanceMutex = CreateMutex(NULL, TRUE, L"Global\\VideoPlayerCode.VolumeLinker"); + if (singleInstanceMutex == NULL || GetLastError() == ERROR_ALREADY_EXISTS) { + // Attempt to find the other instance's window (by its title). + // NOTE: This can have false positives if some other window uses the exact same title. + // NOTE: On Windows 10+ with "virtual desktop", this also finds the app on other desktops. + // NOTE: If the application is running on another user's account during "fast user switching", + // this will NOT find THEIR window. (Verified on Win7 SP1, Win8.1 and Win10.) + HWND existingWindow = FindWindow(NULL, APP_WINDOW_TITLE_64); + if (!existingWindow) { + existingWindow = FindWindow(NULL, APP_WINDOW_TITLE_32); + } + if (existingWindow) { + // We've found what we assume is our other instance's window, so we'll post (async) + // a custom message to make that window activate itself. We use a very conservative, + // custom WM_APP-based message that is unlikely to ever trigger unexpected behavior + // if we accidentally send this to the wrong window. The WM_APP messages are within + // a non-OS range by themselves, and our special one is deep within the WM_APP range. + PostMessage(existingWindow, APP_WM_BRINGTOFRONT, 0, 0); + } + else { + // The mutex is taken but we can't find the window. Most likely running as another user, + // with "fast user switching". Warn user that their computer can only run ONE instance. + MessageBoxW(NULL, L"You can only have one active instance of Volume Linker per computer.\r\nPerhaps it's still running on another user's account?", L"Fatal Error", MB_OK); + } + return 0; + } + + // Exit code. + int exitCode = 0; + + // Set the locale for string handling/comparison/sorting purposes (mainly for _wcsicmp). + // NOTE: We'll automatically use the system's locale, by passing in an empty value. + _wsetlocale(LC_ALL, L""); + + // Read command line parameters. + for (int i = 1; i < __argc; ++i) { // Skips "0" (the executable path). + if (_wcsicmp(__wargv[i], L"/m") == 0 || + _wcsicmp(__wargv[i], L"/minimized") == 0 || + _wcsicmp(__wargv[i], L"/minimize") == 0) + { + g_optStartMinimized = true; + } + else if (_wcsicmp(__wargv[i], L"/l") == 0 || + _wcsicmp(__wargv[i], L"/link") == 0) + { + g_optForceLink = true; + } + } + + // Save program instance handle to global variable. + g_hInstance = hInstance; + + // Open COM connection for current thread, in single-threaded mode. + HRESULT hr; + hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + + // Guarantees that "CoUninitialize" is called when the program ends. + // NOTE: It's okay that this is called even if CoInitializeEx failed above. + wil::unique_couninitialize_call cleanup; + + try { + // Verify that the COM connection was opened. + THROW_IF_COM_FAILED(hr, "Unable to initialize COM connection."); + + // Create a random COM event-context GUID to identify our application process. + GUID processGUID = GUID_NULL; + hr = CoCreateGuid(&processGUID); + THROW_IF_COM_FAILED(hr, "Unable to create COM process GUID."); + + // Connect to the audio COM server and retrieve list of devices. + // NOTE: Saves a global pointer to the class for use by our dialog. + g_deviceManager = std::make_unique(processGUID); + + // Initialize and register Windows GUI control classes (Common Controls 6+). + INITCOMMONCONTROLSEX icex; + icex.dwSize = sizeof(INITCOMMONCONTROLSEX); + icex.dwICC = ICC_WIN95_CLASSES; // All standard classes. + if (!InitCommonControlsEx(&icex)) { + throw std::runtime_error("Unable to initialize common controls library."); + } + + // Load all application icons. + // NOTE: Automatically loads (with scaling if necessary) appropriate icon sizes based on screen DPI. + LoadIconMetric(hInstance, MAKEINTRESOURCE(IDI_MAINICON), LIM_LARGE, &g_iconLargeMain); + LoadIconMetric(hInstance, MAKEINTRESOURCE(IDI_MAINICON), LIM_SMALL, &g_iconSmallMain); + LoadIconMetric(hInstance, MAKEINTRESOURCE(IDI_DISABLEDICON), LIM_LARGE, &g_iconLargeDisabled); + LoadIconMetric(hInstance, MAKEINTRESOURCE(IDI_DISABLEDICON), LIM_SMALL, &g_iconSmallDisabled); + + // Create our main program dialog. + // NOTE: Unlike the modal DialogBox(), which contains an internal message loop and + // doesn't return until the dialog is closed, the CreateDialog() technique actually + // returns immediately and the messages are processed via the program's main message + // loop instead. + // NOTE: Before the function returns, it executes the callback with WM_INITDIALOG, + // and provides the exact dialog HWND handle to that function, and our code in that + // event then writes the handle to g_hDlg. However, we'll assign it here too (yes, + // the exact same value), because it allows us to use inspect the return value to + // detect when the dialog failed to create (in which case WM_INITDIALOG wouldn't run). + // NOTE: Because our dialog lacks the "WS_VISIBLE" style, it won't be auto-shown + // by the CreateDialog function. That allows us to manually control initial visibility! + // More details: + // https://docs.microsoft.com/en-us/windows/win32/dlgbox/using-dialog-boxes#creating-a-modeless-dialog-box + // https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues + g_hDlg = CreateDialog(hInstance, MAKEINTRESOURCE(DLG_VOLUMELINKER), NULL, (DLGPROC)MainDlgProc); + if (g_hDlg == NULL) { + throw std::runtime_error("Unable to load application interface."); + } + + // Next, it's time to show the window, EXCEPT if the user has requested "start minimized". + if (!g_optStartMinimized) { + ShowWindow(g_hDlg, SW_SHOW); + } + + // We must now implement a standard Windows message pump loop... + // NOTE: All callbacks run within this main GUI thread, which means that + // the callbacks can use COM resources owned by the main thread (this one). + // NOTE: If anything within the callback throws and isn't caught in there, + // it will bubble up to us and be handled by our exception-catcher below, + // and the program will then exit gracefully... However, CERTAIN MESSAGES + // do not allow the stack to unwind and will NOT bubble up to us! So all + // error handling should optimally be done inside the message handler! + // NOTE: The GetMessage() loops until it sees WM_QUIT (by PostQuitMessage()). + + MSG msg; + BOOL bRet; + + while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0) + { + if (bRet == -1) + { + // A code of "-1" means that there's a serious error which prevented + // "GetMessage()" from retrieving a message. Exit the application. + // NOTE: This is a SERIOUS problem and should never happen, so we do + // not run any WM_CLOSE to cleanly autosave... we will exit quickly. + throw std::runtime_error("Critical failure in message loop."); + } + // NOTE: IsWindow() ensures that the dialog exists, and IsDialogMessage() + // actually checks if the message belongs to the dialog and if so it processes + // the message for that dialog! If they both return false, it means it's + // a "regular (NON-dialog) window" or "thread message" instead, so we use + // the regular message handlers (translate/dispatch) instead. + // NOTE: Even though this is a "typical" message loop, it doesn't matter + // that our application lacks actual (regular) windows, since the DispatchMessage + // function only calls the callbacks of *registered* windows, and we simply + // don't have *any* windows! Our dialog is handled by IsDialogMessage(). + else if (!IsWindow(g_hDlg) || !IsDialogMessage(g_hDlg, &msg)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + + // Use the returned code from the final message (WM_QUIT) as the exit code. + exitCode = static_cast(msg.wParam); + } + catch (const std::exception& ex) { + // NOTE: Exceptions are never wide (Unicode), so we must use the regular ANSI msgbox. + MessageBoxA(NULL, ex.what(), + "Fatal Error", MB_OK); + exitCode = 1; // Signal error as exit-code. + goto Exit; + } + +Exit: + // Exit-cleanup goes here... + ExitCleanup(); + + // Return the exit code to the system. + return exitCode; +} + +void ExitCleanup() noexcept +{ + // NOTE: This function can safely be called multiple times. + + // Destroy and liberate all audio devices and related COM connections. + g_deviceManager.reset(); + + // Remove the notification area icon (if one is registered). Otherwise it lingers after exit. + if (g_hasNotifyIcon) { + Shell_NotifyIcon(NIM_DELETE, &g_notifyIconData); + g_hasNotifyIcon = false; + } + + // Free the memory used by the dynamically loaded/scaled application (and notification area) icons. + // NOTE: We free (from system DLL heap?) memory allocated by LoadIconMetric. + if (g_iconLargeMain != NULL) { DestroyIcon(g_iconLargeMain); g_iconLargeMain = NULL; } + if (g_iconSmallMain != NULL) { DestroyIcon(g_iconSmallMain); g_iconSmallMain = NULL; } + if (g_iconLargeDisabled != NULL) { DestroyIcon(g_iconLargeDisabled); g_iconLargeDisabled = NULL; } + if (g_iconSmallDisabled != NULL) { DestroyIcon(g_iconSmallDisabled); g_iconSmallDisabled = NULL; } + + // Free the memory used by the context-menu. + // NOTE: This is necessary because Windows only auto-releases menus at exit if the menus are owned + // by an app-window. And our menu is a popup-menu, so I am unsure whether it's owned by the dialog. + if (g_hTrayMenu != NULL) { + DestroyMenu(g_hTrayMenu); + g_hTrayMenu = NULL; + } +} + +void Dlg_ShowAndForeground() noexcept +{ + // Make window visible again if it's invisible. + if (!IsWindowVisible(g_hDlg)) { + ShowWindow(g_hDlg, SW_SHOW); + } + + // Ensure that the window is no longer minimized. + if (IsIconic(g_hDlg)) { // "Iconic" means minimized. + ShowWindow(g_hDlg, SW_RESTORE); // "Restore the minimized window". + } + + // Attempt to bring the window to the foreground. + // NOTE: System restricts this function and only performs the action if our calling + // thread received the most recent Windows input/message event, or similar privileges. + // NOTE: If the window is on another virtual desktop (Windows 10+), this call will + // cause that exact virtual desktop to be focused when the app goes to the foreground. + SetForegroundWindow(g_hDlg); +} + +ptrdiff_t Dlg_GetDropdownSelection( + int dlgItem) noexcept +{ + // Retrieve the item selection offset from the desired list. + ptrdiff_t selectionIdx = SendDlgItemMessage(g_hDlg, dlgItem, CB_GETCURSEL, 0, 0); + + // If the checkbox lacks a selection, return -1 instead. + if (selectionIdx == CB_ERR) { + selectionIdx = -1; + } + + return selectionIdx; +} + +void Dlg_ShowLinkState() noexcept +{ + auto isLinked = g_deviceManager && g_deviceManager->isLinkActive(); + + // Apply dialog icon (the top left corner icon). + // NOTE: We don't need to check if any of the icons are NULL (not loaded), + // because in that case WM_SETICON simply removes the targeted icon. + // NOTE: By default, a dialog uses the application's embedded "main" icon + // as its taskbar icon. But as soon as we send either ICON_SMALL or ICON_BIG, + // it replaces the taskbar icon too! And we must send both, otherwise it will + // scale up the ICON_SMALL as a blurry taskbar icon! Also note that if we ever + // send a NULL (0) icon, the taskbar reverts to the app's embedded icon again. + SendMessage(g_hDlg, WM_SETICON, ICON_SMALL, (LPARAM)(isLinked ? g_iconSmallMain : g_iconSmallDisabled)); + SendMessage(g_hDlg, WM_SETICON, ICON_BIG, (LPARAM)(isLinked ? g_iconLargeMain : g_iconLargeDisabled)); + + // Also update the notification area icon. + // NOTE: According to various researchers on the internet, the notification tray + // always makes its own private copy of the icons we give it, which is nice. + if (g_hasNotifyIcon) { + g_notifyIconData.hIcon = (isLinked ? g_iconSmallMain : g_iconSmallDisabled); + Shell_NotifyIcon(NIM_MODIFY, &g_notifyIconData); + } + + // Update buttons and volume controls based on link-state. + SendDlgItemMessage(g_hDlg, IDC_BUTTON_LINK, WM_SETTEXT, 0, (LPARAM)(isLinked ? L"Unlink Devices" : L"Link Devices")); + EnableWindow(GetDlgItem(g_hDlg, IDC_SLIDER_VOLUME), isLinked ? TRUE : FALSE); + EnableWindow(GetDlgItem(g_hDlg, IDC_CHECK_MUTE), isLinked ? TRUE : FALSE); + if (!isLinked) { + // Unlinked: Uncheck the "mute" checkbox and put the volume slider all the way to the left. + SendDlgItemMessage(g_hDlg, IDC_CHECK_MUTE, BM_SETCHECK, BST_UNCHECKED, 0); + SendDlgItemMessage(g_hDlg, IDC_SLIDER_VOLUME, TBM_SETPOS, TRUE, 0); + } +} + +void Dlg_LinkDevices( + bool showErrors) +{ + // Retrieve the item selection offsets from both lists. + auto masterIdx = Dlg_GetDropdownSelection(IDC_MASTERLIST); + auto slaveIdx = Dlg_GetDropdownSelection(IDC_SLAVELIST); + + // If any of the checkboxes lack a selection, just unlink any existing connection. + if (masterIdx < 0 || slaveIdx < 0) { + Dlg_UnlinkDevices(); + return; + } + + try { + // Attempt to link the devices. Throws if there are any problems establishing the link. + if (g_deviceManager) { + g_deviceManager->linkDevices(masterIdx, slaveIdx); + } + } + catch (const std::exception& ex) { + // NOTE: Exceptions are never wide (Unicode), so we must use the regular ANSI msgbox. + if (showErrors) { + MessageBoxA(g_hDlg, ex.what(), + "Link Failed", MB_OK); + } + } + + // Show the result of the linking attempt. + Dlg_ShowLinkState(); +} + +void Dlg_UnlinkDevices() noexcept +{ + if (g_deviceManager && g_deviceManager->isLinkActive()) { + g_deviceManager->unlinkDevices(); + } + Dlg_ShowLinkState(); +} + +bool Dlg_SaveSettings() noexcept +{ + // Ensure that this function only runs when there's still a dialog. + if (g_hDlg == NULL) { + return false; + } + + // Only save settings to registry if they've been marked as "user has modified manually". + // NOTE: The purpose is to avoid ultra annoying behavior where the user may open the app + // while their soundcard device is missing somehow, and then closing the application and + // fixing their soundcard, and finally re-opening the app but then seeing that their old + // device choice has now been cleared (forgotten) since the settings had been re-saved while + // the device was missing. So, instead of being annoying like that, we will simply NEVER + // save settings UNLESS the user has MANUALLY clicked on the Link/Unlink button OR changed + // active selection in any of the dropdowns. Thanks to this safeguard, the user can relax + // and close and re-open the application and be sure that it WILL always re-open with + // their LAST USER-CONFIGURATION, rather than some annoying auto-saved "broken" state! + if (g_saveChanges) { + // Save last-used settings to registry (while ensuring no uncaught exceptions escape). + try { + // Prepare initial states for the variables we'll be saving to the registry. + auto linkActive = g_deviceManager->isLinkActive(); + wstring masterDeviceId; + wstring slaveDeviceId; + + // Retrieve the item selection offsets from both lists. + auto masterIdx = Dlg_GetDropdownSelection(IDC_MASTERLIST); + auto slaveIdx = Dlg_GetDropdownSelection(IDC_SLAVELIST); + + // Retrieve the selected devices and their hardware IDs (throws if the indices are invalid, negative, etc). + // NOTE: If any of the devices have problems, we'll save an empty ID and no "link active" state. + try { + auto masterDevice = g_deviceManager->getDevice(masterIdx); + masterDeviceId = masterDevice.getId(); + } + catch (...) { + linkActive = false; + masterDeviceId = L""; + } + try { + auto slaveDevice = g_deviceManager->getDevice(slaveIdx); + slaveDeviceId = slaveDevice.getId(); + } + catch (...) { + linkActive = false; + slaveDeviceId = L""; + } + + // Open the program's key (creates it if missing). + // NOTE: We never need elevated privileges to read/write the user's registry, + // so we'll just silently ignore any throws here (which should never happen). + winreg::RegKey key{ g_regHKey, g_regSoftwareKey, g_regDesiredAccess }; + + // Write the settings to the registry. (Can throw but should never happen.) + key.SetStringValue(g_regMasterDevice, masterDeviceId); + key.SetStringValue(g_regSlaveDevice, slaveDeviceId); + key.SetDwordValue(g_regLinkActive, linkActive ? 1 : 0); + + // Mark the fact that we've successfully saved the changes. + g_saveChanges = false; + + return true; + } + catch (...) {} + } + + return false; +} + +BOOL CALLBACK AboutDlgProc( + HWND hDlg, + UINT message, + WPARAM wParam, + LPARAM lParam) +{ + if (message == WM_COMMAND) { + auto controlId = LOWORD(wParam); // Control's identifier. + switch (controlId) + { + case IDOK: // User pressed the "OK" button in the dialog. + case IDCANCEL: // Triggered automatically by methods such as user pressing escape. + EndDialog(hDlg, 0); + return TRUE; + } + } + + return FALSE; // Let default handler take care of all other events. +} + +BOOL CALLBACK QuitDlgProc( + HWND hDlg, + UINT message, + WPARAM wParam, + LPARAM lParam) +{ + if (message == WM_COMMAND) { + auto controlId = LOWORD(wParam); // Control's identifier. + switch (controlId) + { + case IDC_BUTTON_QUIT: + case IDC_BUTTON_MINIMIZE: + case IDC_BUTTON_CANCEL: + EndDialog(hDlg, controlId); // Return ID of selected control. + return TRUE; + case IDCANCEL: // Triggered automatically by methods such as user pressing escape. + EndDialog(hDlg, IDC_BUTTON_CANCEL); // Return "cancel". + return TRUE; + } + } + + return FALSE; // Let default handler take care of all other events. +} + +BOOL CALLBACK MainDlgProc( + HWND hDlg, + UINT message, + WPARAM wParam, + LPARAM lParam) +{ + switch (message) + { + case WM_DPICHANGED: // Win 8.1+ only: The DPI has changed (either by user, or by dragging to different-DPI screen). + { + // NOTE: High-DPI is *automatically* handled thanks to "PerMonitorV2" mode + // (see "DPIAware.manifest") on Win10, and the system does this regardless of + // whether we return TRUE or FALSE here, which means that it's integrated + // into the Win10+ OS and not part of the default dialog handler! + // We don't have to do anything here. But this code block is here just + // to note that older OS versions from Windows 8.1 and onwards send this + // event, BUT that we HAVEN'T implemented any high-DPI intelligence + // WHATSOEVER for Windows 7/8/8.1! See "DPIAware.manifest" for why. + // The only reason why we're marking the application as high-DPI aware + // even on those older systems (despite lacking manual scaling code) is + // to ensure that all of our screen coordinates become non-virtualized, + // so that we can display popup menus at correct screen positions, etc, + // regardless of OS. Read the manifest for more information. + return FALSE; // Say that we didn't handle this event. Use default handler. + } + + case WM_SYSCOMMAND: // User requested a command from the Window-menu (via taskbar/window icon), titlebar buttons (ie. min/max/close), or Alt-F4. + { + if (wParam == SC_CLOSE) { // User wants to close the window. + // Alright, we know that the user has manually requested to close the window, + // but we unfortunately don't know if they've used Alt-F4 or not. There's + // a way to check if they used a "mnemonic" (Alt-F4), but that also triggers + // for the taskbar-rightclick "Close window" option, so we can't rely on it. + + // Either way, we must now show a confirmation dialog to be sure that the user + // knows what they're doing. + auto choice = DialogBox(g_hInstance, + MAKEINTRESOURCE(DLG_QUITPANEL), hDlg, (DLGPROC)QuitDlgProc); + switch (choice) + { + case IDC_BUTTON_QUIT: + return FALSE; // Use default "quit" handler (exit program). + case IDC_BUTTON_MINIMIZE: + ShowWindow(hDlg, SW_MINIMIZE); // Minimize the main dialog. + // Also fall through to "default" to abort quitting... + default: // IDC_BUTTON_CANCEL + return TRUE; // Abort the quitting. + } + } + + return FALSE; // Let Windows handle most of the syscommands. + } + + case APP_WM_ICONNOTIFY: // Messages from notification icon interaction. + { + auto eventType = LOWORD(lParam); + auto iconId = HIWORD(lParam); + + switch (eventType) + { + //case WM_LBUTTONDBLCLK: // Left double-click. (Pointless; NIN_SELECT is always sent with the first click during double-click). + case NIN_SELECT: // Left click-up. Also: "user selects icon with mouse activates it with ENTER" (the latter is no longer true in Win10, which sends NO EVENT if you hover with the mouse and then use the keyboard to "activate"). + case NIN_KEYSELECT: // User "activated" icon with ENTER/SPACEBAR (in "Win+B" keyboard mode). Sent TWICE if ENTER is used. + { + // Ensure that the window is visible, not minimized, and bring it to front. + Dlg_ShowAndForeground(); + + break; + } + + case WM_CONTEXTMENU: // User requests tray icon menu (ie. via right-click or requesting menu while icon has keyboard-focus (via Shift-F10 or dedicated "menu"-key)). + { + // Get the X and Y mouse cursor positions; or, if menu was opened via keyboard, + // it is the tray icon's position. The coordinates are real pixels (DPI aware). + auto pos = MAKEPOINTS(wParam); // Menu coordinates. + + // Load the notification tray icon's context-menu (if not loaded yet). + if (g_hTrayMenu == NULL) { + g_hTrayMenu = LoadMenu(g_hInstance, MAKEINTRESOURCE(IDR_MENU_TRAY)); + } + + // Check if the menu is loaded, and display the sub-menu containing the popup items. + // NOTE: The sub-menu does not need manual resource releasing since it's part of the bigger menu. + if (g_hTrayMenu != NULL) { + auto trayMenu = GetSubMenu(g_hTrayMenu, 0); + if (trayMenu != NULL) { + // Fetch the optimal horizontal placement of the menu (to the left or right of cursor). + // NOTE: "Nonzero if drop-down menus are right-aligned with their corresponding menu-bar item; + // or 0 if the menus are left-aligned." (MSDN). + // NOTE: We're using the non-aware (DPI unaware) version of the API, because "GetSystemMetricsForDpi" + // is only available in Windows 10+. Still, it seems like we're only asking for a RTL/LTR text direction + // flag, most likely based on the user's language (ie. Arabic computers are RTL), so DPI shouldn't matter! + auto menuHorizAlign = GetSystemMetrics(SM_MENUDROPALIGNMENT) == 0 ? TPM_LEFTALIGN : TPM_RIGHTALIGN; + + // We must first set the parent window as the "foreground" window, otherwise the popup menu won't + // close at all if you click outside the menu. This fact is documented in "TrackPopupMenuEx" (MSDN). + // NOTE: This works properly even if the dialog is in SW_HIDE and Minimized state. We DON'T need + // to make it visible. The call below is enough to make hDlg "foreground" anyway, and fixes the menu. + // NOTE: Most likely, windows are able to have the "foreground" flag regardless of WHERE they are, + // and the OS only clears their flag when you click/tab to something else. And the popup menu probably + // listens to some "parent's foreground focus lost" event to detect when to auto-close the menu. + SetForegroundWindow(hDlg); + + // Display the menu. This operation is BLOCKING and won't return until the menu is closed! + // NOTE: We use the window-message mode which means the selected item is sent to OUR message queue. + // NOTE: Despite this call being blocking, it thankfully doesn't prevent the volume callback + // from directly updating the volume-slider and mute-checkbox whenever the device volume changes! + // NOTE: We do NOT specify vertical alignment (ie. TPM_BOTTOMALIGN). By not forcing an alignment, + // the menu will be vertically auto-aligned by Windows, based on where the icon is on the screen. + TrackPopupMenuEx(trayMenu, + menuHorizAlign | TPM_RIGHTBUTTON /* allow both left and right clicks */, + pos.x, pos.y, hDlg, NULL); + } + } + + break; + } + } + + return TRUE; + } + + case WM_SIZE: // Fired AFTER the window's size/state has changed (ie. after animation has finished). + { + switch (wParam) + { + case SIZE_MINIMIZED: // Window has become minimized. + // Make window invisible to hide it from the taskbar while it's minimized. + // NOTE: The controls (ie. volume/mute) will continue to update while the dialog is hidden. + ShowWindow(hDlg, SW_HIDE); + return TRUE; + } + + break; + } + + case APP_WM_BRINGTOFRONT: // Used by other instances to tell this instance to activate itself. + { + // Ensure that the window is visible, not minimized, and bring it to front. + Dlg_ShowAndForeground(); + + return TRUE; + } + + case WM_INITDIALOG: // Executed by CreateDialog, for initializing the dialog box controls. + { + // Save program-global handle for our dialog. + g_hDlg = hDlg; + + // Register the notification area icon. + // NOTE: We must do this BEFORE we try to NIM_MODIFY, since we need to be able to set the + // correct link-status icon during our initialization further down. But we also can't NIM_ADD + // before CreateDialog, since we don't know our window handle until we've been created. + g_notifyIconData.cbSize = sizeof(g_notifyIconData); // NOTE: Since we only target Win7+, we don't need "NOTIFYICONDATA_V3_SIZE" (or older) here. + g_notifyIconData.uVersion = NOTIFYICON_VERSION_4; // Use "modern" tray icons introduced in Vista and later. Improves and adds modern event messages! + g_notifyIconData.hWnd = hDlg; // Send all "tray icon" window messages to our dialog's message handler. + g_notifyIconData.uID = 1; // We use ID-based icons since GUID icons are messy (tied to the executable path unless app is Authenticode-signed). + g_notifyIconData.uFlags = NIF_ICON | NIF_TIP | NIF_SHOWTIP | NIF_MESSAGE; // Setting desired features for the tray icon. + g_notifyIconData.uCallbackMessage = APP_WM_ICONNOTIFY; // Which window message the icon sends to the hWnd. + StringCchCopy(g_notifyIconData.szTip, ARRAYSIZE(g_notifyIconData.szTip), L"Volume Linker"); // Tooltip. Can be up to 128 chars in current structure. Safest way of writing the string (https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa). + g_notifyIconData.hIcon = g_iconSmallDisabled; // Begin in disabled state. Use the globally loaded icon as our notification icon. + if (Shell_NotifyIcon(NIM_ADD, &g_notifyIconData)) { // Add the icon to the tray. Returns TRUE on success. + Shell_NotifyIcon(NIM_SETVERSION, &g_notifyIconData); // Tell icon to behave according to the "uVersion" value. + g_hasNotifyIcon = true; + } + + // Apply the correct (32-bit or 64-bit) dialog title. +#ifdef _WIN64 + SetWindowText(hDlg, APP_WINDOW_TITLE_64); +#else + SetWindowText(hDlg, APP_WINDOW_TITLE_32); +#endif + + // Set the min/max value range of the volume slider. + SendDlgItemMessage(hDlg, IDC_SLIDER_VOLUME, TBM_SETRANGEMIN, FALSE, 0); + SendDlgItemMessage(hDlg, IDC_SLIDER_VOLUME, TBM_SETRANGEMAX, FALSE, MAX_VOL); + + // Tell the device manager to use (auto-update) our dialog and its volume-controls. + g_deviceManager->setDialog(hDlg, IDC_CHECK_MUTE, IDC_SLIDER_VOLUME); + + // Read last-used settings from registry (while ensuring no uncaught exceptions escape). + bool linkActive = false; + wstring masterDeviceId; + wstring slaveDeviceId; + try { + // Open the program's key (creates it if missing). + // NOTE: We never need elevated privileges to read/write the user's registry, + // so we'll just silently ignore any throws here (which should never happen). + winreg::RegKey key{ g_regHKey, g_regSoftwareKey, g_regDesiredAccess }; + + // Attempt to read all settings. Will throw if types mismatch or values are missing. + linkActive = (key.GetDwordValue(g_regLinkActive) == 1); + masterDeviceId = key.GetStringValue(g_regMasterDevice); + slaveDeviceId = key.GetStringValue(g_regSlaveDevice); + } + catch (...) { + // If any of the settings couldn't be read, we'll just set them all to defaults. + linkActive = false; + masterDeviceId = L""; + slaveDeviceId = L""; + } + + // Retrieve vector of all detected audio playback devices. + auto audioDevices = g_deviceManager->getAudioDevices(); + + // Populate the dropdown menus, and detect which entries (if any) should be auto-selected. + ptrdiff_t counter = 0; + ptrdiff_t masterIdx = -1; + ptrdiff_t slaveIdx = -1; + for (auto& device : audioDevices) { + SendDlgItemMessage(hDlg, IDC_MASTERLIST, CB_ADDSTRING, 0, (LPARAM)device.getNameMS()); + SendDlgItemMessage(hDlg, IDC_SLAVELIST, CB_ADDSTRING, 0, (LPARAM)device.getNameMS()); + + auto deviceId = device.getId(); + if (deviceId.compare(masterDeviceId) == 0 && masterDeviceId.length() > 0) { + masterIdx = counter; + } + if (deviceId.compare(slaveDeviceId) == 0 && slaveDeviceId.length() > 0) { + slaveIdx = counter; + } + + ++counter; + } + + // Force the dropdown menus to select their last-used devices (if invalid index = clears selection). + if (masterIdx != -1) { + SendDlgItemMessage(hDlg, IDC_MASTERLIST, CB_SETCURSEL, (WPARAM)masterIdx, 0); + } + if (slaveIdx != -1) { + SendDlgItemMessage(hDlg, IDC_SLAVELIST, CB_SETCURSEL, (WPARAM)slaveIdx, 0); + } + + // Automatically link again if both devices were found, and were linked last time (OR told to force-link). + // NOTE: We always suppress error popup boxes here, because it would be extremely annoying to have this + // program on autostart (as most people would), and to get a popup error during login. It's better that + // people just personally realize volume isn't linked (if there was a problem) and open the GUI to fix it. + if ((linkActive || g_optForceLink) && masterIdx >= 0 && slaveIdx >= 0) { + Dlg_LinkDevices(false); // No error boxes on failure. + } + else { + // Since we didn't auto-link, we should at least set the control states to the unlinked state. + Dlg_ShowLinkState(); + } + + return TRUE; + } + + case WM_QUERYENDSESSION: // Windows is shutting down and wants to know if we're ok with that. + // SUPER IMPORTANT: The normal shutdown flow is that the OS asks about WM_QUERYENDSESSION, + // and you return TRUE if you are ready to shut down. Next, the OS sends WM_ENDSESSION + // and lets you take up to 5 seconds to finish saving files before you return a value, + // and after that it kills your application instantly without calling any "other" clean + // shutdown/WM_QUIT/DestroyWindow etc. The way Microsoft's "Raymond Chen" puts it is that + // it basically "tears down the whole building" and just destroys the resource memory + // immediately without worrying about housekeeping first. In short, "WM_ENDSESSION" *is* + // your app's "exit point"! Furthermore, the docs say that you can OPTIONALLY choose + // to manually destroy your window and post quit messages during the WM_ENDSESSION + // event, "but you aren't required to do so"... But that's POINTLESS, because the app is + // always KILLED after WM_ENDSESSION and is NOT told to exit its message-loop (even if + // you manually post WM_QUIT), and is NOT given a chance to cleanly exit from its "main()" + // routine. So ALL shutdown code MUST be handled WITHIN the WM_ENDSESSION call... In fact, + // in Raymond Chen's blog post, he literally says "If you need to do cleanup, you need + // to do that BEFORE returning from WM_ENDSESSION, because you cannot rely on ANY further + // code in your program running after you've returned", and he further clarifies that + // NOT EVEN C++ OBJECT DESTRUCTORS WILL RUN. The application is literally TERMINATED. + // + // But that's actually not every ceaveat, because there's a serious, undocumented side-effect + // if you DO decide to manually DestroyWindow during your WM_ENDSESSION handler... If you + // destroy the window, the application INSTANTLY TERMINATES upon that call, and does NOT + // run ANY more lines of code after that DestroyWindow call! It took Mozilla 2.5 WEEKS + // to discover this undocumented side-effect: https://bugzilla.mozilla.org/show_bug.cgi?id=333907 + // + // Furthermore, doing PostQuitMessage is completely pointless since your application is + // terminated before the loop ever has a chance to process that message. So don't waste + // precious time posting that message either! + // + // So, the FINAL summary becomes: Treat WM_ENDSESSION as your exit point, and do NOT destroy + // windows or post WM_QUIT from there. Just cleanly save work and delete any objects whose + // destructors MUST run, and remember that you only have 5 seconds of real time to do + // all of that work and MUST return from the handler before that. And then, let Windows + // "tear down the building" (forcefully terminates your program). ;-) + // + // However, all of this is harder to implement due to the fact that OUR window is Dialog- + // based and has a DEFAULT handler. The problem is that if we return FALSE (int 0), the + // default handler runs instead. But the OS *wants* us to return 0 from WM_ENDSESSION if + // we've successfully and gracefully prepared ourselves to be killed. But doing so will + // just trigger the dialog's DEFAULT handler instead, which returns a non-0 value to the + // OS in that situation, and makes the OS think we're "not responding" during shutdown. + // + // Here are the results based on what we return: + // * QUERYEND: TRUE, ENDSESSION: FALSE (aka 0) = We enter a weird state where the OS + // believes that we've stopped responding (because the default dialog handler gets + // confused by QUERYEND: TRUE by us). + // * QUERYEND: FALSE, ENDSESSION: FALSE = Default dialog handler runs for both events, + // and NO other events are fired (no WM_CLOSE any other nice stuff, not even IDCANCEL). + // + // Therefore, the ONLY choice is to return FALSE for BOTH, so that the default dialog + // handler's "clean shutdown" executes for both events. However, we still want to run + // our own clean shutdown handling. Luckily, the window-message flow is STILL always + // "windows OS -> our callback -> the default handler if our callback said FALSE". So we + // can ALWAYS see BOTH messages (and can take some time, doing cleanup BEFORE we return + // FALSE to let the default dialog handler run, and THEN letting Windows insta-kill us). + // + // So that's our solution. We'll return FALSE in both cases, and we'll handle the + // WM_ENDSESSION message with the "shutting down" flag by doing an auto-save in there, + // as well as manually deleting any objects whose destructors MUST run. And we will + // NEVER, EVER call "DestroyWindow" or "PostQuitMessage" during the cleanup steps! ;-) + // + // ALSO NOTE: If our secondary DLG_QUITPANEL is visible, it's always parented to our + // main dialog (it's modal), so what happens is that the DLG_QUITPANEL auto-closes FIRST + // via its own default dialog handler, and THEN our main dialog runs its own final + // cleanup. In other words, DLG_QUITPANEL WON'T interfere with our final auto-save/cleanup! + + return FALSE; + + case WM_ENDSESSION: // Windows is informing us whether the session is truly ending or not. + { + // We must now handle the WM_ENDSESSION(wParam = TRUE) state to do a clean application + // shutdown without data loss. There's no guarantee how long the process will live after + // we return from handling this message (usually it's INSTANTLY TERMINATED without even + // running any object destructors), but what we do know is that the OS gives us 5 seconds + // to handle this WM_ENDSESSION message: + // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms700677(v=vs.85) + // And whatever we do, we MUST avoid calling DestroyWindow (and PostQuitMessage). + + // NOTE: This shutdown routine has been confirmed to work properly (destroy all critical + // objects, auto-save) on Win7 SP1, Win8.1 and Win10. Win8 was not tested but assumed ok. + + if (wParam == TRUE) { // Session is truly ending (shutdown hasn't been aborted). + // Save configuration to registry. + Dlg_SaveSettings(); + + // Break any active link/callback, just to be nice and clean... + Dlg_UnlinkDevices(); + + // Because no other code will run after we return (not even destructors or the + // remainder of "main()"), we must manually clean up some critical destructors. + // We do that by running the same cleanup function as "main()" would have done. + ExitCleanup(); + + // Also uninitialize the COM connection for this thread (if any still exists). + // NOTE: During NORMAL app closing, this is handled by the object named "cleanup". + CoUninitialize(); + + // NOTE: As mentioned, our application will now be TERMINATED after this returns! + } + + return FALSE; + } + + case WM_CLOSE: // We've been told to close ourselves via "standard" methods such as the X button. + { + // Save configuration to registry. + Dlg_SaveSettings(); + + // Break any active link/callback, just to be nice and clean... + Dlg_UnlinkDevices(); + + // Close and unload the window (dialog) resources. + if (g_hDlg != NULL) { + DestroyWindow(g_hDlg); // Immediately causes a WM_DESTROY to be processed (before returning). + g_hDlg = NULL; // Should have been set automatically by WM_DESTROY, but let's be extra sure! + } + + // Tell the main application message loop to quit (by posting a WM_QUIT message). + // NOTE: We'll base our exit code on the device manager's result. In almost all cases, + // it will be 0 (no error). However, if the volume-linking callback failed critically + // while syncing volumes, it will have set itself to a non-zero exit code and then told + // this dialog to WM_CLOSE. So we'll exit with the code from the device manager! + int exitCode = g_deviceManager ? g_deviceManager->getExitCode() : 0; + PostQuitMessage(exitCode); + + return TRUE; + } + + case WM_DESTROY: + { + // Mark the fact that the window has been destroyed (unloaded). + g_hDlg = NULL; + + return TRUE; + } + + case WM_COMMAND: + { + // Parse the message. These parsing rules are true for all except 1 or 2 notification types. + auto notificationCode = HIWORD(wParam); // Notification code. + auto controlId = LOWORD(wParam); // Control's identifier. + auto hControl = (HWND)lParam; // Control's window handle. + + switch (notificationCode) + { + case BN_CLICKED: // Button clicked (even if done via keyboard-spacebar). + // We must check the control, since this event triggers for "X" (close) clicks too. + switch (controlId) + { + case IDC_BUTTON_LINK: + { + // Automatically toggle between linking or un-linking. + if (!g_deviceManager->isLinkActive()) { + Dlg_LinkDevices(true); + } + else { + Dlg_UnlinkDevices(); + } + + // Mark the fact that the user has MANUALLY changed the device/link-settings. + // NOTE: We don't care whether the link succeeded or not; the mere fact that + // the user has clicked the button is enough for us to mark changes for saving. +#ifdef _DEBUG + if (!g_saveChanges) { + OutputDebugStringA("> Settings marked for saving (by link-button).\r\n"); + } +#endif + g_saveChanges = true; + + return TRUE; + } + + case IDC_CHECK_MUTE: + { + // Update the master device's mute state (will also sync to the slave device automatically). + auto nChecked = SendDlgItemMessage(hDlg, IDC_CHECK_MUTE, BM_GETCHECK, 0, 0); + BOOL bMuted = (BST_CHECKED == nChecked); + g_deviceManager->setMasterMute(bMuted); + return TRUE; + } + } + + break; + + case CBN_SELCHANGE: // Combobox selection changed (won't trigger when just opening and closing list without changing). + { + // When the user changes device selection, automatically unlink any active link. + Dlg_UnlinkDevices(); + + // Mark the fact that the user has MANUALLY changed the device/link-settings. +#ifdef _DEBUG + if (!g_saveChanges) { + OutputDebugStringA("> Settings marked for saving (by dropdown).\r\n"); + } +#endif + g_saveChanges = true; + + return TRUE; + } + } + + switch (controlId) + { + case IDOK: // User pressed "enter" in the dialog. + case IDCANCEL: // User pressed "escape" in the dialog, *or* the DEFAULT "WM_CLOSE" handler ran (which then sends this event too). + // NOTE: Since our dialog is acting as the main application window, we want to suppress + // both kinds of "enter/escape" key presses in the dialog. We already override the default + // handler for WM_CLOSE, but a direct "escape" press still sends this event, so we're + // just ensuring that absolutely NO action happens when these keys are pressed. + return TRUE; // Do nothing. + + case IDM_TRAYMENU_SHOW: + // Ensure that the window is visible, not minimized, and bring it to front. + Dlg_ShowAndForeground(); + + return TRUE; + + case IDM_TRAYMENU_ABOUT: + // Display the about-box in a modal (blocking) way. + DialogBox(g_hInstance, + MAKEINTRESOURCE(DLG_ABOUT), hDlg, (DLGPROC)AboutDlgProc); + + return TRUE; + + case IDM_TRAYMENU_QUIT: + // Since they've right-clicked the notification icon and selected "Quit", + // we'll assume that they know what they are doing, and we won't ask them + // if they want to "Quit, Minimize or Cancel". We'll send a direct WM_CLOSE! + PostMessage(hDlg, WM_CLOSE, 0, 0); + + return TRUE; + } + + break; + } + + case WM_HSCROLL: // An event has happened in a horizontal scrollbar. + { + // Only proceed if the event was sent by a scrollbar control (lParam is non-NULL), + // and react to all scrollbar movement events except "movement has ended". + // Docs: https://docs.microsoft.com/en-us/windows/win32/controls/wm-hscroll + if (lParam != NULL && LOWORD(wParam) != SB_ENDSCROLL) { + // Retrieve scrollbar position (only whole integers, from 0 to 100 (MAX_VOL)). + ptrdiff_t iVolume = SendDlgItemMessage(hDlg, IDC_SLIDER_VOLUME, TBM_GETPOS, 0, 0); + + // Ensure valid numeric range, as an extra "failsafe" protection just in case... + if (iVolume < 0) { iVolume = 0; } + else if (iVolume > MAX_VOL) { iVolume = MAX_VOL; } + + // Convert the volume to a float (range: 0.0 to 1.0) and set the device volume. + float fVolume = static_cast(iVolume) / MAX_VOL; + g_deviceManager->setMasterVolume(fVolume); + + // The standard Windows system controls for volume (keyboard keys or volume mixer) + // dynamically manage the "mute" state based on the user's volume actions, as follows: + // - When volume reaches 0, the audio device is toggled to "muted". + // - When volume is moved to any non-zero position, it is always unmuted (if muted). + // - The latter applies even if muted at volume 60 and then moved to 61 (unmutes). + // We need to replicate the same behavior here since the API won't do it automatically. + if (iVolume == 0) { + // Mute the master device (will also sync to the slave device automatically). + g_deviceManager->setMasterMute(TRUE); + SendDlgItemMessage(hDlg, IDC_CHECK_MUTE, BM_SETCHECK, BST_CHECKED, 0); + } + else { + // Optimize by only sending "unmute" if it was actually muted. We'll use our "mute" + // checkbox's state to determine this, since it's perfectly synced to the system's + // actual mute-state by the volume-callback (and when GUI-user toggles it manually). + auto nChecked = SendDlgItemMessage(hDlg, IDC_CHECK_MUTE, BM_GETCHECK, 0, 0); + BOOL bMuted = (BST_CHECKED == nChecked); + if (bMuted) { + // Unmute the master device (will also sync to the slave device automatically). + g_deviceManager->setMasterMute(FALSE); + SendDlgItemMessage(hDlg, IDC_CHECK_MUTE, BM_SETCHECK, BST_UNCHECKED, 0); + } + } + + return TRUE; + } + + break; + } + } + + // Signal that we didn't handle the event. + return FALSE; +} diff --git a/VolumeLinker/non-package libraries/WinReg/WinReg.hpp b/VolumeLinker/non-package libraries/WinReg/WinReg.hpp new file mode 100644 index 0000000..3bc1ce2 --- /dev/null +++ b/VolumeLinker/non-package libraries/WinReg/WinReg.hpp @@ -0,0 +1,1384 @@ +#ifndef INCLUDE_GIOVANNI_DICANIO_WINREG_HPP +#define INCLUDE_GIOVANNI_DICANIO_WINREG_HPP + + +//////////////////////////////////////////////////////////////////////////////// +// +// *** Modern C++ Wrappers Around Windows Registry C API *** +// +// Copyright (C) by Giovanni Dicanio +// +// First version: 2017, January 22nd +// Last update: 2019, March 26th +// +// E-mail: +// +// Registry key handles are safely and conveniently wrapped +// in the RegKey resource manager C++ class. +// +// Errors are signaled throwing exceptions of class RegException. +// +// Unicode UTF-16 strings are represented using the std::wstring class; +// ATL's CString is not used, to avoid dependencies from ATL or MFC. +// +// This is a header-only self-contained reusable module. +// +// Compiler: Visual Studio 2015 +// Code compiles clean at /W4 on both 32-bit and 64-bit builds. +// +// =========================================================================== +// +// The MIT License(MIT) +// +// Copyright(c) 2017 Giovanni Dicanio +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +//////////////////////////////////////////////////////////////////////////////// + + +#include // Windows Platform SDK +#include // _ASSERTE + +#include // std::unique_ptr +#include // std::runtime_error +#include // std::wstring +#include // std::swap, std::pair +#include // std::vector + + +namespace winreg +{ + + +//------------------------------------------------------------------------------ +// Safe, efficient and convenient C++ wrapper around HKEY registry key handles. +// +// This class is movable but not copyable. +// +// This class is designed to be very *efficient* and low-overhead, for example: +// non-throwing operations are carefully marked as noexcept, so the C++ compiler +// can emit optimized code. +// +// Moreover, this class just wraps a raw HKEY handle, without any +// shared-ownership overhead like in std::shared_ptr; you can think of this +// class kind of like a std::unique_ptr for HKEYs. +// +// The class is also swappable (defines a custom non-member swap); +// relational operators are properly overloaded as well. +//------------------------------------------------------------------------------ +class RegKey +{ +public: + + // + // Construction/Destruction + // + + // Initialize as an empty key handle + RegKey() noexcept = default; + + // Take ownership of the input key handle + explicit RegKey(HKEY hKey) noexcept; + + // Open the given registry key if it exists, else create a new key. + // Uses default KEY_READ|KEY_WRITE access. + // For finer grained control, call the Create() method overloads. + // Throw RegException on failure. + RegKey(HKEY hKeyParent, const std::wstring& subKey); + + // Open the given registry key if it exists, else create a new key. + // Allow the caller to specify the desired access to the key (e.g. KEY_READ + // for read-only access). + // For finer grained control, call the Create() method overloads. + // Throw RegException on failure. + RegKey(HKEY hKeyParent, const std::wstring& subKey, REGSAM desiredAccess); + + + // Take ownership of the input key handle. + // The input key handle wrapper is reset to an empty state. + RegKey(RegKey&& other) noexcept; + + // Move-assign from the input key handle. + // Properly check against self-move-assign (which is safe and does nothing). + RegKey& operator=(RegKey&& other) noexcept; + + // Ban copy + RegKey(const RegKey&) = delete; + RegKey& operator=(const RegKey&) = delete; + + // Safely close the wrapped key handle (if any) + ~RegKey() noexcept; + + + // + // Properties + // + + // Access the wrapped raw HKEY handle + HKEY Get() const noexcept; + + // Is the wrapped HKEY handle valid? + bool IsValid() const noexcept; + + // Same as IsValid(), but allow a short "if (regKey)" syntax + explicit operator bool() const noexcept; + + // Is the wrapped handle a predefined handle (e.g.HKEY_CURRENT_USER) ? + bool IsPredefined() const noexcept; + + + // + // Operations + // + + // Close current HKEY handle. + // If there's no valid handle, do nothing. + // This method doesn't close predefined HKEY handles (e.g. HKEY_CURRENT_USER). + void Close() noexcept; + + // Transfer ownership of current HKEY to the caller. + // Note that the caller is responsible for closing the key handle! + HKEY Detach() noexcept; + + // Take ownership of the input HKEY handle. + // Safely close any previously open handle. + // Input key handle can be nullptr. + void Attach(HKEY hKey) noexcept; + + // Non-throwing swap; + // Note: There's also a non-member swap overload + void SwapWith(RegKey& other) noexcept; + + + // + // Wrappers around Windows Registry APIs. + // See the official MSDN documentation for these APIs for detailed explanations + // of the wrapper method parameters. + // + + // Wrapper around RegCreateKeyEx, that allows you to specify desired access + void Create( + HKEY hKeyParent, + const std::wstring& subKey, + REGSAM desiredAccess = KEY_READ | KEY_WRITE + ); + + // Wrapper around RegCreateKeyEx + void Create( + HKEY hKeyParent, + const std::wstring& subKey, + REGSAM desiredAccess, + DWORD options, + SECURITY_ATTRIBUTES* securityAttributes, + DWORD* disposition + ); + + // Wrapper around RegOpenKeyEx + void Open( + HKEY hKeyParent, + const std::wstring& subKey, + REGSAM desiredAccess = KEY_READ | KEY_WRITE + ); + + + // + // Registry Value Setters + // + + void SetDwordValue(const std::wstring& valueName, DWORD data); + void SetQwordValue(const std::wstring& valueName, const ULONGLONG& data); + void SetStringValue(const std::wstring& valueName, const std::wstring& data); + void SetExpandStringValue(const std::wstring& valueName, const std::wstring& data); + void SetMultiStringValue(const std::wstring& valueName, const std::vector& data); + void SetBinaryValue(const std::wstring& valueName, const std::vector& data); + void SetBinaryValue(const std::wstring& valueName, const void* data, DWORD dataSize); + + + // + // Registry Value Getters + // + + DWORD GetDwordValue(const std::wstring& valueName); + ULONGLONG GetQwordValue(const std::wstring& valueName); + std::wstring GetStringValue(const std::wstring& valueName); + + enum class ExpandStringOption + { + DontExpand, + Expand + }; + + std::wstring GetExpandStringValue( + const std::wstring& valueName, + ExpandStringOption expandOption = ExpandStringOption::DontExpand + ); + + std::vector GetMultiStringValue(const std::wstring& valueName); + std::vector GetBinaryValue(const std::wstring& valueName); + + + // + // Query Operations + // + + void QueryInfoKey(DWORD& subKeys, DWORD &values, FILETIME& lastWriteTime); + + // Return the DWORD type ID for the input registry value + DWORD QueryValueType(const std::wstring& valueName); + + // Enumerate the subkeys of the registry key, using RegEnumKeyEx + std::vector EnumSubKeys(); + + // Enumerate the values under the registry key, using RegEnumValue. + // Returns a vector of pairs: In each pair, the wstring is the value name, + // the DWORD is the value type. + std::vector> EnumValues(); + + + // + // Misc Registry API Wrappers + // + + void DeleteValue(const std::wstring& valueName); + void DeleteKey(const std::wstring& subKey, REGSAM desiredAccess); + void DeleteTree(const std::wstring& subKey); + void FlushKey(); + void LoadKey(const std::wstring& subKey, const std::wstring& filename); + void SaveKey(const std::wstring& filename, SECURITY_ATTRIBUTES* securityAttributes); + void EnableReflectionKey(); + void DisableReflectionKey(); + bool QueryReflectionKey(); + void ConnectRegistry(const std::wstring& machineName, HKEY hKeyPredefined); + + + // Return a string representation of Windows registry types + static std::wstring RegTypeToString(DWORD regType); + + // + // Relational comparison operators are overloaded as non-members + // ==, !=, <, <=, >, >= + // + + +private: + // The wrapped registry key handle + HKEY m_hKey{ nullptr }; +}; + + +//------------------------------------------------------------------------------ +// An exception representing an error with the registry operations +//------------------------------------------------------------------------------ +class RegException + : public std::runtime_error +{ +public: + RegException(const char* message, LONG errorCode) + : std::runtime_error{ message } + , m_errorCode{ errorCode } + {} + + RegException(const std::string& message, LONG errorCode) + : std::runtime_error{ message } + , m_errorCode{ errorCode } + {} + + // Get the error code returned by Windows registry APIs + LONG ErrorCode() const noexcept + { + return m_errorCode; + } + +private: + // Error code, as returned by Windows registry APIs + LONG m_errorCode; +}; + + +//------------------------------------------------------------------------------ +// Overloads of relational comparison operators for RegKey +//------------------------------------------------------------------------------ + +inline bool operator==(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() == b.Get(); +} + +inline bool operator!=(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() != b.Get(); +} + +inline bool operator<(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() < b.Get(); +} + +inline bool operator<=(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() <= b.Get(); +} + +inline bool operator>(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() > b.Get(); +} + +inline bool operator>=(const RegKey& a, const RegKey& b) noexcept +{ + return a.Get() >= b.Get(); +} + + +//------------------------------------------------------------------------------ +// RegKey Inline Methods +//------------------------------------------------------------------------------ + +inline RegKey::RegKey(const HKEY hKey) noexcept + : m_hKey{ hKey } +{} + + +inline RegKey::RegKey(const HKEY hKeyParent, const std::wstring& subKey) +{ + Create(hKeyParent, subKey); +} + + +inline RegKey::RegKey(const HKEY hKeyParent, const std::wstring& subKey, REGSAM desiredAccess) +{ + Create(hKeyParent, subKey, desiredAccess); +} + + +inline RegKey::RegKey(RegKey&& other) noexcept + : m_hKey{ other.m_hKey } +{ + // Other doesn't own the handle anymore + other.m_hKey = nullptr; +} + + +inline RegKey& RegKey::operator=(RegKey&& other) noexcept +{ + // Prevent self-move-assign + if ((this != &other) && (m_hKey != other.m_hKey)) + { + // Close current + Close(); + + // Move from other (i.e. take ownership of other's raw handle) + m_hKey = other.m_hKey; + other.m_hKey = nullptr; + } + return *this; +} + + +inline RegKey::~RegKey() noexcept +{ + // Release the owned handle (if any) + Close(); +} + + +inline HKEY RegKey::Get() const noexcept +{ + return m_hKey; +} + + +inline void RegKey::Close() noexcept +{ + if (IsValid()) + { + // Do not call RegCloseKey on predefined keys + if (! IsPredefined()) + { + ::RegCloseKey(m_hKey); + } + + // Avoid dangling references + m_hKey = nullptr; + } +} + + +inline bool RegKey::IsValid() const noexcept +{ + return m_hKey != nullptr; +} + + +inline RegKey::operator bool() const noexcept +{ + return IsValid(); +} + + +inline bool RegKey::IsPredefined() const noexcept +{ + // Predefined keys + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724836(v=vs.85).aspx + + if ( (m_hKey == HKEY_CURRENT_USER) + || (m_hKey == HKEY_LOCAL_MACHINE) + || (m_hKey == HKEY_CLASSES_ROOT) + || (m_hKey == HKEY_CURRENT_CONFIG) + || (m_hKey == HKEY_CURRENT_USER_LOCAL_SETTINGS) + || (m_hKey == HKEY_PERFORMANCE_DATA) + || (m_hKey == HKEY_PERFORMANCE_NLSTEXT) + || (m_hKey == HKEY_PERFORMANCE_TEXT) + || (m_hKey == HKEY_USERS)) + { + return true; + } + + return false; +} + + +inline HKEY RegKey::Detach() noexcept +{ + HKEY hKey{ m_hKey }; + + // We don't own the HKEY handle anymore + m_hKey = nullptr; + + // Transfer ownership to the caller + return hKey; +} + + +inline void RegKey::Attach(const HKEY hKey) noexcept +{ + // Prevent self-attach + if (m_hKey != hKey) + { + // Close any open registry handle + Close(); + + // Take ownership of the input hKey + m_hKey = hKey; + } +} + + +inline void RegKey::SwapWith(RegKey& other) noexcept +{ + // Enable ADL (not necessary in this case, but good practice) + using std::swap; + + // Swap the raw handle members + swap(m_hKey, other.m_hKey); +} + + +inline void swap(RegKey& a, RegKey& b) noexcept +{ + a.SwapWith(b); +} + + +inline void RegKey::Create( + const HKEY hKeyParent, + const std::wstring& subKey, + const REGSAM desiredAccess +) +{ + constexpr DWORD kDefaultOptions = REG_OPTION_NON_VOLATILE; + + Create(hKeyParent, subKey, desiredAccess, kDefaultOptions, + nullptr, // no security attributes, + nullptr // no disposition + ); +} + + +inline void RegKey::Create( + const HKEY hKeyParent, + const std::wstring& subKey, + const REGSAM desiredAccess, + const DWORD options, + SECURITY_ATTRIBUTES* const securityAttributes, + DWORD* const disposition +) +{ + HKEY hKey{ nullptr }; + LONG retCode = ::RegCreateKeyEx( + hKeyParent, + subKey.c_str(), + 0, // reserved + REG_NONE, // user-defined class type parameter not supported + options, + desiredAccess, + securityAttributes, + &hKey, + disposition + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegCreateKeyEx failed.", retCode }; + } + + // Safely close any previously opened key + Close(); + + // Take ownership of the newly created key + m_hKey = hKey; +} + + +inline void RegKey::Open( + const HKEY hKeyParent, + const std::wstring& subKey, + const REGSAM desiredAccess +) +{ + HKEY hKey{ nullptr }; + LONG retCode = ::RegOpenKeyEx( + hKeyParent, + subKey.c_str(), + REG_NONE, // default options + desiredAccess, + &hKey + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegOpenKeyEx failed.", retCode }; + } + + // Safely close any previously opened key + Close(); + + // Take ownership of the newly created key + m_hKey = hKey; +} + + +inline void RegKey::SetDwordValue(const std::wstring& valueName, const DWORD data) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_DWORD, + reinterpret_cast(&data), + sizeof(data) + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write DWORD value: RegSetValueEx failed.", retCode }; + } +} + + +inline void RegKey::SetQwordValue(const std::wstring& valueName, const ULONGLONG& data) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_QWORD, + reinterpret_cast(&data), + sizeof(data) + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write QWORD value: RegSetValueEx failed.", retCode }; + } +} + + +inline void RegKey::SetStringValue(const std::wstring& valueName, const std::wstring& data) +{ + _ASSERTE(IsValid()); + + // String size including the terminating NUL, in bytes + const DWORD dataSize = static_cast((data.length() + 1) * sizeof(wchar_t)); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_SZ, + reinterpret_cast(data.c_str()), + dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write string value: RegSetValueEx failed.", retCode }; + } +} + + +inline void RegKey::SetExpandStringValue(const std::wstring& valueName, const std::wstring& data) +{ + _ASSERTE(IsValid()); + + // String size including the terminating NUL, in bytes + const DWORD dataSize = static_cast((data.length() + 1) * sizeof(wchar_t)); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_EXPAND_SZ, + reinterpret_cast(data.c_str()), + dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write expand string value: RegSetValueEx failed.", retCode }; + } +} + + +namespace details +{ + +// Helper function to build a multi-string from a vector +inline std::vector BuildMultiString(const std::vector& data) +{ + // Special case of the empty multi-string + if (data.empty()) + { + // Build a vector containing just two NULs + return std::vector(2, L'\0'); + } + + // Get the total length in wchar_ts of the multi-string + size_t totalLen = 0; + for (const auto& s : data) + { + // Add one to current string's length for the terminating NUL + totalLen += (s.length() + 1); + } + + // Add one for the last NUL terminator (making the whole structure double-NUL terminated) + totalLen++; + + // Allocate a buffer to store the multi-string + std::vector multiString; + multiString.reserve(totalLen); + + // Copy the single strings into the multi-string + for (const auto& s : data) + { + multiString.insert(multiString.end(), s.begin(), s.end()); + + // Don't forget to NUL-terminate the current string + multiString.push_back(L'\0'); + } + + // Add the last NUL-terminator + multiString.push_back(L'\0'); + + return multiString; +} + +} // namespace details + + +inline void RegKey::SetMultiStringValue( + const std::wstring& valueName, + const std::vector& data +) +{ + _ASSERTE(IsValid()); + + // First, we have to build a double-NUL-terminated multi-string from the input data + const std::vector multiString = details::BuildMultiString(data); + + // Total size, in bytes, of the whole multi-string structure + const DWORD dataSize = static_cast(multiString.size() * sizeof(wchar_t)); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_MULTI_SZ, + reinterpret_cast(&multiString[0]), + dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write multi-string value: RegSetValueEx failed.", retCode }; + } +} + + +inline void RegKey::SetBinaryValue(const std::wstring& valueName, const std::vector& data) +{ + _ASSERTE(IsValid()); + + // Total data size, in bytes + const DWORD dataSize = static_cast(data.size()); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_BINARY, + &data[0], + dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write binary data value: RegSetValueEx failed.", retCode }; + } +} + + +inline void RegKey::SetBinaryValue( + const std::wstring& valueName, + const void* const data, + const DWORD dataSize +) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegSetValueEx( + m_hKey, + valueName.c_str(), + 0, // reserved + REG_BINARY, + static_cast(data), + dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot write binary data value: RegSetValueEx failed.", retCode }; + } +} + + +inline DWORD RegKey::GetDwordValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + DWORD data{}; // to be read from the registry + DWORD dataSize = sizeof(data); // size of data, in bytes + + const DWORD flags = RRF_RT_REG_DWORD; + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + &data, + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get DWORD value: RegGetValue failed.", retCode }; + } + + return data; +} + + +inline ULONGLONG RegKey::GetQwordValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + ULONGLONG data{}; // to be read from the registry + DWORD dataSize = sizeof(data); // size of data, in bytes + + const DWORD flags = RRF_RT_REG_QWORD; + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + &data, + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get QWORD value: RegGetValue failed.", retCode }; + } + + return data; +} + + +inline std::wstring RegKey::GetStringValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + // Get the size of the result string + DWORD dataSize = 0; // size of data, in bytes + const DWORD flags = RRF_RT_REG_SZ; + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + nullptr, // output buffer not needed now + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get size of string value: RegGetValue failed.", retCode }; + } + + // Allocate a string of proper size. + // Note that dataSize is in bytes and includes the terminating NUL; + // we have to convert the size from bytes to wchar_ts for wstring::resize. + std::wstring result; + result.resize(dataSize / sizeof(wchar_t)); + + // Call RegGetValue for the second time to read the string's content + retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + &result[0], // output buffer + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get string value: RegGetValue failed.", retCode }; + } + + // Remove the NUL terminator scribbled by RegGetValue from the wstring + result.resize((dataSize / sizeof(wchar_t)) - 1); + + return result; +} + + +inline std::wstring RegKey::GetExpandStringValue( + const std::wstring& valueName, + const ExpandStringOption expandOption +) +{ + _ASSERTE(IsValid()); + + DWORD flags = RRF_RT_REG_EXPAND_SZ; + + // Adjust the flag for RegGetValue considering the expand string option specified by the caller + if (expandOption == ExpandStringOption::DontExpand) + { + flags |= RRF_NOEXPAND; + } + + // Get the size of the result string + DWORD dataSize = 0; // size of data, in bytes + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + nullptr, // output buffer not needed now + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get size of expand string value: RegGetValue failed.", retCode }; + } + + // Allocate a string of proper size. + // Note that dataSize is in bytes and includes the terminating NUL. + // We must convert from bytes to wchar_ts for wstring::resize. + std::wstring result; + result.resize(dataSize / sizeof(wchar_t)); + + // Call RegGetValue for the second time to read the string's content + retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + &result[0], // output buffer + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get expand string value: RegGetValue failed.", retCode }; + } + + // Remove the NUL terminator scribbled by RegGetValue from the wstring + result.resize((dataSize / sizeof(wchar_t)) - 1); + + return result; +} + + +inline std::vector RegKey::GetMultiStringValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + // Request the size of the multi-string, in bytes + DWORD dataSize = 0; + const DWORD flags = RRF_RT_REG_MULTI_SZ; + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + nullptr, // output buffer not needed now + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get size of multi-string value: RegGetValue failed.", retCode }; + } + + // Allocate room for the result multi-string. + // Note that dataSize is in bytes, but our vector::resize method requires size + // to be expressed in wchar_ts. + std::vector data; + data.resize(dataSize / sizeof(wchar_t)); + + // Read the multi-string from the registry into the vector object + retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // no type required + &data[0], // output buffer + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get multi-string value: RegGetValue failed.", retCode }; + } + + // Resize vector to the actual size returned by GetRegValue. + // Note that the vector is a vector of wchar_ts, instead the size returned by GetRegValue + // is in bytes, so we have to scale from bytes to wchar_t count. + data.resize(dataSize / sizeof(wchar_t)); + + // Parse the double-NUL-terminated string into a vector, + // which will be returned to the caller + std::vector result; + const wchar_t* currStringPtr = &data[0]; + while (*currStringPtr != L'\0') + { + // Current string is NUL-terminated, so get its length calling wcslen + const size_t currStringLength = wcslen(currStringPtr); + + // Add current string to the result vector + result.push_back(std::wstring{ currStringPtr, currStringLength }); + + // Move to the next string + currStringPtr += currStringLength + 1; + } + + return result; +} + + +inline std::vector RegKey::GetBinaryValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + // Get the size of the binary data + DWORD dataSize = 0; // size of data, in bytes + const DWORD flags = RRF_RT_REG_BINARY; + LONG retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + nullptr, // output buffer not needed now + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get size of binary data: RegGetValue failed.", retCode }; + } + + // Allocate a buffer of proper size to store the binary data + std::vector data(dataSize); + + // Call RegGetValue for the second time to read the data content + retCode = ::RegGetValue( + m_hKey, + nullptr, // no subkey + valueName.c_str(), + flags, + nullptr, // type not required + &data[0], // output buffer + &dataSize + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get binary data: RegGetValue failed.", retCode }; + } + + return data; +} + + +inline DWORD RegKey::QueryValueType(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + DWORD typeId{}; // will be returned by RegQueryValueEx + + LONG retCode = ::RegQueryValueEx( + m_hKey, + valueName.c_str(), + nullptr, // reserved + &typeId, + nullptr, // not interested + nullptr // not interested + ); + + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot get the value type: RegQueryValueEx failed.", retCode }; + } + + return typeId; +} + + +inline void RegKey::QueryInfoKey(DWORD& subKeys, DWORD &values, FILETIME& lastWriteTime) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegQueryInfoKey( + m_hKey, + nullptr, + nullptr, + nullptr, + &subKeys, + nullptr, + nullptr, + &values, + nullptr, + nullptr, + nullptr, + &lastWriteTime + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegQueryInfoKey failed.", retCode }; + } +} + + +inline std::vector RegKey::EnumSubKeys() +{ + _ASSERTE(IsValid()); + + // Get some useful enumeration info, like the total number of subkeys + // and the maximum length of the subkey names + DWORD subKeyCount{}; + DWORD maxSubKeyNameLen{}; + LONG retCode = ::RegQueryInfoKey( + m_hKey, + nullptr, // no user-defined class + nullptr, // no user-defined class size + nullptr, // reserved + &subKeyCount, + &maxSubKeyNameLen, + nullptr, // no subkey class length + nullptr, // no value count + nullptr, // no value name max length + nullptr, // no max value length + nullptr, // no security descriptor + nullptr // no last write time + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegQueryInfoKey failed while preparing for subkey enumeration.", retCode }; + } + + // NOTE: According to the MSDN documentation, the size returned for subkey name max length + // does *not* include the terminating NUL, so let's add +1 to take it into account + // when I allocate the buffer for reading subkey names. + maxSubKeyNameLen++; + + // Preallocate a buffer for the subkey names + auto nameBuffer = std::make_unique(maxSubKeyNameLen); + + // The result subkey names will be stored here + std::vector subkeyNames; + + // Enumerate all the subkeys + for (DWORD index = 0; index < subKeyCount; index++) + { + // Get the name of the current subkey + DWORD subKeyNameLen = maxSubKeyNameLen; + retCode = ::RegEnumKeyEx( + m_hKey, + index, + nameBuffer.get(), + &subKeyNameLen, + nullptr, // reserved + nullptr, // no class + nullptr, // no class + nullptr // no last write time + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot enumerate subkeys: RegEnumKeyEx failed.", retCode }; + } + + // On success, the ::RegEnumKeyEx API writes the length of the + // subkey name in the subKeyNameLen output parameter + // (not including the terminating NUL). + // So I can build a wstring based on that length. + subkeyNames.push_back(std::wstring{ nameBuffer.get(), subKeyNameLen }); + } + + return subkeyNames; +} + + +inline std::vector> RegKey::EnumValues() +{ + _ASSERTE(IsValid()); + + // Get useful enumeration info, like the total number of values + // and the maximum length of the value names + DWORD valueCount{}; + DWORD maxValueNameLen{}; + LONG retCode = ::RegQueryInfoKey( + m_hKey, + nullptr, // no user-defined class + nullptr, // no user-defined class size + nullptr, // reserved + nullptr, // no subkey count + nullptr, // no subkey max length + nullptr, // no subkey class length + &valueCount, + &maxValueNameLen, + nullptr, // no max value length + nullptr, // no security descriptor + nullptr // no last write time + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ + "RegQueryInfoKey failed while preparing for value enumeration.", + retCode + }; + } + + // NOTE: According to the MSDN documentation, the size returned for value name max length + // does *not* include the terminating NUL, so let's add +1 to take it into account + // when I allocate the buffer for reading value names. + maxValueNameLen++; + + // Preallocate a buffer for the value names + auto nameBuffer = std::make_unique(maxValueNameLen); + + // The value names and types will be stored here + std::vector> valueInfo; + + // Enumerate all the values + for (DWORD index = 0; index < valueCount; index++) + { + // Get the name and the type of the current value + DWORD valueNameLen = maxValueNameLen; + DWORD valueType{}; + retCode = ::RegEnumValue( + m_hKey, + index, + nameBuffer.get(), + &valueNameLen, + nullptr, // reserved + &valueType, + nullptr, // no data + nullptr // no data size + ); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "Cannot enumerate values: RegEnumValue failed.", retCode }; + } + + // On success, the RegEnumValue API writes the length of the + // value name in the valueNameLen output parameter + // (not including the terminating NUL). + // So we can build a wstring based on that. + valueInfo.push_back( + std::make_pair(std::wstring{ nameBuffer.get(), valueNameLen }, valueType) + ); + } + + return valueInfo; +} + + +inline void RegKey::DeleteValue(const std::wstring& valueName) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegDeleteValue(m_hKey, valueName.c_str()); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegDeleteValue failed.", retCode }; + } +} + + +inline void RegKey::DeleteKey(const std::wstring& subKey, const REGSAM desiredAccess) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegDeleteKeyEx(m_hKey, subKey.c_str(), desiredAccess, 0); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegDeleteKeyEx failed.", retCode }; + } +} + + +inline void RegKey::DeleteTree(const std::wstring& subKey) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegDeleteTree(m_hKey, subKey.c_str()); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegDeleteTree failed.", retCode }; + } +} + + +inline void RegKey::FlushKey() +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegFlushKey(m_hKey); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegFlushKey failed.", retCode }; + } +} + + +inline void RegKey::LoadKey(const std::wstring& subKey, const std::wstring& filename) +{ + Close(); + + LONG retCode = ::RegLoadKey(m_hKey, subKey.c_str(), filename.c_str()); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegLoadKey failed.", retCode }; + } +} + + +inline void RegKey::SaveKey( + const std::wstring& filename, + SECURITY_ATTRIBUTES* const securityAttributes +) +{ + _ASSERTE(IsValid()); + + LONG retCode = ::RegSaveKey(m_hKey, filename.c_str(), securityAttributes); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegSaveKey failed.", retCode }; + } +} + + +inline void RegKey::EnableReflectionKey() +{ + LONG retCode = ::RegEnableReflectionKey(m_hKey); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegEnableReflectionKey failed.", retCode }; + } +} + + +inline void RegKey::DisableReflectionKey() +{ + LONG retCode = ::RegDisableReflectionKey(m_hKey); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegDisableReflectionKey failed.", retCode }; + } +} + + +inline bool RegKey::QueryReflectionKey() +{ + BOOL isReflectionDisabled = FALSE; + LONG retCode = ::RegQueryReflectionKey(m_hKey, &isReflectionDisabled); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegQueryReflectionKey failed.", retCode }; + } + + return (isReflectionDisabled ? true : false); +} + + +inline void RegKey::ConnectRegistry(const std::wstring& machineName, const HKEY hKeyPredefined) +{ + // Safely close any previously opened key + Close(); + + HKEY hKeyResult{ nullptr }; + LONG retCode = ::RegConnectRegistry(machineName.c_str(), hKeyPredefined, &hKeyResult); + if (retCode != ERROR_SUCCESS) + { + throw RegException{ "RegConnectRegistry failed.", retCode }; + } + + // Take ownership of the result key + m_hKey = hKeyResult; +} + + +inline std::wstring RegKey::RegTypeToString(const DWORD regType) +{ + switch (regType) + { + case REG_SZ: return L"REG_SZ"; + case REG_EXPAND_SZ: return L"REG_EXPAND_SZ"; + case REG_MULTI_SZ: return L"REG_MULTI_SZ"; + case REG_DWORD: return L"REG_DWORD"; + case REG_QWORD: return L"REG_QWORD"; + case REG_BINARY: return L"REG_BINARY"; + + default: return L"Unknown/unsupported registry type"; + } +} + + +} // namespace winreg + + +#endif // INCLUDE_GIOVANNI_DICANIO_WINREG_HPP diff --git a/VolumeLinker/packages.config b/VolumeLinker/packages.config new file mode 100644 index 0000000..2e822b8 --- /dev/null +++ b/VolumeLinker/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/VolumeLinker/resource.h b/VolumeLinker/resource.h new file mode 100644 index 0000000..c591c16 --- /dev/null +++ b/VolumeLinker/resource.h @@ -0,0 +1,38 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by VolumeLinker.rc +// +#define IDI_MAINICON 104 +#define IDI_DISABLEDICON 105 +#define DLG_VOLUMELINKER 106 +#define DLG_QUITPANEL 107 +#define DLG_ABOUT 108 +#define IDR_MENU_TRAY 109 +#define IDC_SLIDER_VOLUME 1000 +#define IDC_CHECK_MUTE 1001 +#define IDC_STATIC_MINVOL 1002 +#define IDC_STATIC_MAXVOL 1003 +#define IDC_STATIC_MASTERLABEL 1004 +#define IDC_MASTERLIST 1005 +#define IDC_STATIC_SLAVELABEL 1006 +#define IDC_SLAVELIST 1007 +#define IDC_BUTTON_LINK 1008 +#define IDC_BUTTON_QUIT 1009 +#define IDC_BUTTON_MINIMIZE 1010 +#define IDC_BUTTON_CANCEL 1011 +#define IDC_STATIC_ABOUT 1012 +#define IDC_STATIC_ABOUT_THIRDPARTY 1013 +#define IDM_TRAYMENU_SHOW 40005 +#define IDM_TRAYMENU_ABOUT 40006 +#define IDM_TRAYMENU_QUIT 40007 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 110 +#define _APS_NEXT_COMMAND_VALUE 40008 +#define _APS_NEXT_CONTROL_VALUE 1014 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/VolumeLinker/targetver.h b/VolumeLinker/targetver.h new file mode 100644 index 0000000..207da68 --- /dev/null +++ b/VolumeLinker/targetver.h @@ -0,0 +1,59 @@ +/* + * This file is part of the Volume Linker project (https://github.com/VideoPlayerCode/VolumeLinker). + * Copyright (C) 2019 VideoPlayerCode. + * + * Volume Linker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Volume Linker is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Volume Linker. If not, see . + * + */ + +#pragma once + +// Build a backwards-compatible application so that it runs on Windows 7 SP1 or higher. +// +// NOTE: Win7 SP1 is the earliest possible version that is supported by the Win10 SDK. +// See https://developer.microsoft.com/en-US/windows/downloads/windows-10-sdk for OS list. +// And the reason why we don't use "an older SDK" to target even older operating systems is +// simply because only idiots run something older than Windows 7. ;-) Those older operating +// systems are no longer supported by Microsoft for a reason. They are old and full of exploits. +// And their older SDKs are full of unfixed bugs too, and would lower the quality of the Win7-10 +// compatibility. So no thanks! If someone wants VolumeLinker, they need Win7+. ;-) +// +// NOTE: We've also set the "Project Properties -> C/C++ -> Code Generation -> Runtime Library" +// option for the RELEASE builds to "Multi-threaded (/MT)" instead of the default "Multi- +// threaded DLL (/MD)". This embeds the Visual C++ runtime into the executable so that people +// won't have to manually install the appropriate runtime (and not even KB2999226 either), which +// means that they CAN'T get the "This program can't start because MSVCP140.dll is missing" error. +// These settings together guarantee the most painless experience for older operating systems, +// and only grows the executable by about 100 KB, which is not much! +// +// NOTE: If we ever compile any library .DLL files in the same folder as the .EXE, they NEED to +// be built with the same static /MT runtime version, but even that's a bit risky because memory +// always has to be freed by the runtime that allocated it (since each statically compiled Visual +// C++ runtime would have its own individual heap), so in most cases, the ONLY working solution +// to be able to interact with custom .DLL files is to switch to /MD for everything. +// +// NOTE: "0x0601" is the raw value for "_WIN32_WINNT_WIN7". And yes we are supposed to manually +// copy the hex value rather than using the define (since it's defined in "SDKDDKVer.h", but +// we MUST set the new target platform value BEFORE we include that file). +#include +#define _WIN32_WINNT 0x0601 + +// Including SDKDDKVer.h defines the available Windows platforms and feature levels. +// +// By default, it targets the highest available OS (such as Windows 10) if just included as-is. +// But here are Microsoft's instructions for targeting an older version (which we are doing): +// +// "If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h". +#include