Skip to content

[Enhancement]: Volume Control - Implement the per-app route READ so the picker can show where an app is actually going #2088

Description

@laurentiu021

Problem

AudioPolicyConfigFactory.GetPersistedDefaultEndpoint is a stub. Its whole body is:

public static string? GetPersistedDefaultEndpoint(object policyConfig, uint processId)
{
    _ = policyConfig;
    _ = processId;
    return null;
}

It is documented as deliberate: the out-string marshaling on the undocumented IAudioPolicyConfig vtable is
build-variant, and the author chose not to guess at it.

#1585 fixed the consequence — the picker no longer renders that null as the default device's name, and now
shows "Choose a device" instead. That is honest but still incomplete: Volume Control can set an app's output
device and cannot show it, so after a restart the user has no way to see from SysManager which app is routed
where. The override is in force in Windows; the app just cannot read it back.

Expected behavior

Implement the read using the same feature-detect-and-degrade contract that already governs the write path:

  • Add the GetPersistedDefaultAudioEndpoint vtable slot behind a try/catch that returns null on any
    COM or marshaling failure — exactly the shape of TryCreate, which returns null when it cannot bind.
  • Keep the three-state contract [Bug]: Volume Control - Per-app output routing never shows the current device: the read path is a hardcoded stub returning null #1585 introduced. null must continue to mean "could not read", so a
    marshaling failure degrades to the existing "Choose a device" placeholder rather than to a wrong device
    name. The existing guard TheAudioRouteRead_ReportsUnknownRatherThanTheDefault already enforces that the
    service keeps the nullable return and does not collapse it.
  • Pin the endpoint-id round trip with a unit test, the way ToPolicyEndpointId already is — that helper is
    unit-tested precisely because the string format is easy to get wrong.
  • Once the read is real, also call SetOutputDeviceFromService for surviving rows, not only new ones
    (MergeInto currently takes the ApplyUpdate branch for survivors, which never touches the selection). Do
    it on tab activation or on a slow cadence, not on the 1 Hz reconcile pass: that is a COM call per app
    per second, and the device list is already deliberately re-read on a slower cadence for the same reason.

Note the ordering constraint: doing that last step before the read works would be a regression, not
progress. With the read still stubbed it would overwrite whatever the user had just picked with null on
every pass.

Rationale

The tab's own header promises "Route an app to a specific output device (headset, speakers)". Half of that
promise is kept. Reading the route back is what turns it from a fire-and-forget action into something the user
can verify, which for the target persona is the difference between trusting the tab and re-picking the device
every time she opens it.

Verification constraint (why this was split out)

This cannot be verified by reading the source. It needs a machine that runs the application, with real
audio sessions and more than one render endpoint, because the failure mode is a marshaling mismatch that only
appears at the COM boundary. ARCHITECTURE.md already records the same constraint for the routing SET path:
"can only be runtime-verified on a real desktop, with real audio hardware."

So this issue is explicitly gated on running the built exe on real hardware and confirming that a
route set through SysManager reads back as the same endpoint id after a restart. A green unit suite is not
sufficient evidence here.

Evidence

Services/AudioPolicyConfig.csGetPersistedDefaultEndpoint body re-read verbatim, plus the doc comment
explaining why it is unimplemented and what the null now produces. Services/AudioMixerService.cs
GetSessionOutputDevice returns string? and returns null on every unknown path (post-#1585).
ViewModels/AudioMixerViewModel.csMergeInto: SetOutputDeviceFromService is called only in the new-row
branch; survivors hit ApplyUpdate, which does not touch SelectedOutputDevice. ViewModels/ AudioSessionRowViewModel.csSetOutputDeviceFromService implements the three-state switch.
SysManager.Tests/ArchitectureTests.csTheAudioRouteRead_ReportsUnknownRatherThanTheDefault guards the
contract that this work must not weaken.

Affected area

Volume Control

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions