EarTrumpet is a Windows Presentation Foundation (WPF) app consisting of a notification area icon with a context menu, flyout and a volume mixer window. Changing the default device is available on the context menu. Hotkeys can be assigned in settings to invoke the flyout, volume mixer or open settings. The flyout and volume mixer have effectively the same controls, in both cases there is a hidden popup on each app session (type space or right click) that reveal sliders for each item in the app group (e.g. multiple Firefox sessions running).
EarTrumpet uses the Windows Multimedia Device API to replace the system volume experience, enabling richer control of apps and devices through a modern UI with independent app controls.
The flyout is created at startup and held ready for display. The volume mixer and settings windows are single-instance and created on demand.
AppInformationFactory
produces IAppInfo
object from a process id (PID). Data (display name, icon location, background color, etc.) is uniquely extracted from desktop and modern apps.
Lingering processes--processes that are dead but not yet cleaned up by the system--could show up in EarTrumpet.
- Start Legacy Edge, navigate to Youtube Page 1.
- Add a tab and navigate to Youtube Page 2.
- Close Page 1 tab.
Observe: Zombie process is still in EarTrumpet.
Manages an automatically updating observable collection of devices with a default device.
Represents an audio device and its associated apps.
Represents an app with an open audio session.
Settings are stored in a key/value store that is backed by Windows Storage or the registry, if the app is packaged or not respectively. StorageFactory.GetSettings()
is used to retrieve and persist settings.
We have observed a high rate of failures (via telemetry) from the Windows.Storage.ApplicationData.Current.LocalSettings.Values
API.
Accesses the user-configurable Windows settings that represent the user's personalization, accessibility, and globalization settings.
ProcessWatcherService
uses a background thread to wait (via WaitForMultipleObjects
) on a list of process handles--processes EarTrumpet has audio sessions for--and waits to be signalled. This thread only waits for 5 seconds at a time, then dispatches notifications for any terminated processes.
The Windows audio implementation of IAudioDeviceManager
and related interfaces.
Windows has the facility to set the persisted playback device on a per-application basis. This setting is persisted however it is not reliably applied at startup. EarTrumpet mitigates this for the user by manually setting the persisted playback device.
To provide good performance, audio metering is sampled on a background thread, and then dispatched on the foreground thread as a batch.
We use Bugsnag as our error reporting service. Secure (TLS) connections are made to notify.bugsnag.com at notification time.
Contains a small internal buffer of log messages that are only shown at the users' request.
Encapsulates the Bugsnag connection and manages the metadata that is sent at notification time.
Transforms an IAudioDeviceManager
into a string for debug purposes at the users' request.
Contains metadata dictionaries populated during crash notification or at the users' request.
Contains support for loading add-on assemblies.
Add-ons must implement a single IAddonLifecycle
in an external add-on assembly.
AddonManager
uses an AddonResolver
to load all applicable add-ons into an AddonHost
(AddonManager.Host
) where they can be accessed directly.
AppDomain.CurrentDomain.AssemblyResolve
locates dependent assemblies residing in add-on directories.
Add-ons are placed in the Versions\[EarTrumpet version]
folder. An add-on can support multiple versions of EarTrumpet in one package.
If an compatible EarTrumpet version can't be found, the add-on won't be loaded. See AddonResolver
for more details.
Hosts required static global data.
Used for cross add-on communication. An add-on could register at ApplicationLifecycleEvent.Startup
and another could retrieve the service at ApplicationLifecycleEvent.Startup2
.
Contains Platform Invoke (P/Invoke) declarations and interfaces.
Contains P/Invoke wrappers.
Contains core audio COM interfaces and declarations, generated from mmdevapi.idl
. This is done by exporting to a Type Library (TLB), importing into Visual Studio, and then copying the generated artifacts.
Shell_NotifyIcon
is called directly because System.Windows.Forms.NotifyIcon
does not support the newer NOTIFICATIONDATAW
structure containing the guidItem
member. This allows the Windows Shell to migrate notification icon settings when the app install location changes.
The Shell_NotifyIcon
API, as of Windows 10 1903, doesn't emit scroll events. To capture scroll events, EarTrumpet uses the following scheme:
- Windows Shell generates
WM_MOUSEMOVE
message in response to cursor movement. - EarTrumpet calls
RegisterRawInputDevices
to requestWM_INPUT
(global raw mouse input) messages. WM_INPUT
messages are processed until the cursor leaves the tray icon bounds (as defined byShell_NotifyIconGetRect
).
Contains WPF XAML resources used to configure the UI to resemble Windows themed UI.
EarTrumpet supports all the Windows theme configurations:
- High contrast
- Light or dark theme (system or app)
- Accent color (applied only to dark theme)
- Transparency (applied to all sans high contrast)
Historical insufficient solutions:
- Split Styles and DataTemplates
- Replacing a
ResourceDictionary
of Brushes
Theme:Brush
applies colors at runtime using a special declaration in XAML.
Theme brush values are specified on elements in XAML:
<TextBlock Theme:Brush.Foreground="SystemAccent" />
The simplest value is a static color: Red
or #FFaabbcc
or SystemAccentDark1
. These colors will apply to all theme configurations.
Values can be set for Light, Dark and HighContrast configurations:
Light=LightChromeWhite, Dark=DarkChromeWhite, HighContrast=Highlight
The same value can also be written as:
Theme={Theme}ChromeWhite, HighContrast=Highlight
{Theme}
is a variable that is replaced with Light
or Dark
as applicable.
The color channel alpha may be optionally modified using a value between 0.0-1.0
:
Color/<TransparencySetting:On|Off>
Color/<TransparencySetting:On>/<TransparencySetting:Off>
Example:
Light=LightChromeWhite/0.8
Light=LightChromeWhite/0.8/1
This allows colors to be opaque when transparency isn't being used.
Brush values reference colors located by:
System.Windows.Media.ColorConverter.ConvertFromString("*ColorName*")
(.e.g#aa000000
)EarTrumpet.Interop.ImmersiveSystemColors.Lookup("*ColorName*")
API (e.g.SystemAccent1
)System.Windows.Media.Colors.*ColorName*
(e.g.Red
)System.Windows.Media.SystemColors.*ColorName*
(e.g.HotTrack
, for high contrast)- A reference like
<Theme:Ref Key="*ColorName*">
inApp.xaml
.
Complex brush values can be built up using nested <Theme:Ref.Rules>
and then referenced like a static color.
The Rules
collection is traversed, evaluating each Rule
using the following scheme:
- Evaluate
On
condition
- If True: Use
Value
or evaluateRules
- If False: Continue to next
Rule
Once a Rules
collection is entered, it must terminate with a Value
or continue to a deeper Rules
collection, not go back to the parent looking for a match.
If no rule is found at the end, this is an programming error. Ensure that the last rule is On=Any
(the default) to prevent this.
Set at the top level window and may be App
or System
. This specifies the theme configuration source (from SystemSettings
). Taskbar and flyout UI use the System
theme, while other top-level windows use the App
theme.
This is used to specify different colors between the flyout and other experiences irrespective of theme configuration.
Adding scope requires that the same tokens (Light, Dark, HighContrast) be specified equally for both scopes (even if they are the same).
Example:
Flyout:Theme=Red, Flyout:HighContrast=HotTrack, :Theme=Blue, :HighContrast=HotTrack
In this instance the flyout will use the red color, while other surfaces will use blue. High contrast will always be HotTrack
.
Contains OS-specific dependency properties (only IsWindows11
at this time) used for theming across multiple OS versions.
Because Brush uses Reflection, properties are identified by string name only. This is to avoid having to special case certain elements when a property exists with the same name on many common types of elements.
Adding a new brush should be accomplished by copying a block in Brush.cs
.
The view-model layer virtualizes moving sessions across devices. This virtualization is necessary for the case where the user moves a session that has an Inactive
state. In this case, Windows will not move the session to the new device until the next time the stream becomes Active
, resulting in incorrect placement in the UI.
EarTrumpet only targets x86. Some P/Invokes will need to be updated if targeting x64 or other platforms.
See Compiling for more information.
Updates AssemblyInfo.cs
(binary file version info) and the package AppxManifest.xml
are kept in sync. Version.txt
is the source of truth for the version.
EarTrumpet can be debugged via the EarTrumpet
project, or via the EarTrumpet.Package
project, which is slower but enables debugging the packaged app.
VSDebug
mode enables the following features over Debug
mode:
TemporaryAppItemViewModel
is marked with a red background color.- Avoid the app identity check which causes a handled startup exception.
- Taskbar top/left/right/bottom (consider RTL!)
- Taskbar auto-hide
- Per-monitor DPI (Settings > change the scale for only one display on a multi-display system)
- Theme pivots: light/dark, Use accent color, Use transparency
- High contrast
- UIAutomation / accessibility
- Keyboard, Touch, Mouse input
- Move taskbar to non-primary display (different DPI)
- Move an audio session that is not currently playing sound
- Remote desktop
- Device add/remove