Repository navigation
Expand file tree
/
Copy pathAudioCurrentOutputCommand.cs
More file actions
102 lines (89 loc) · 4.29 KB
/
Copy pathAudioCurrentOutputCommand.cs
File metadata and controls
102 lines (89 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System.Diagnostics;
using LoupixDeck.PluginSdk;
namespace LoupixDeck.Plugin.Audio;
/// <summary>
/// Opens the output-device picker and labels itself with the device currently in use, so the
/// button doubles as a read-out. The plugin refreshes it when the audio backend reports a device
/// change (see <see cref="IAudioService.SubscribeDeviceChanges"/>); the slow poll only catches an
/// event that never arrived.
/// </summary>
internal sealed class AudioCurrentOutputCommand(
IAudioService audio, AudioAliasStore aliasStore, AudioVisibilityStore visibility) : IDisplayCommand
{
/// <summary>Shown when no output endpoint is currently the default (or none exist).</summary>
private const string NoDeviceLabel = "No device";
/// <summary>
/// Enumerating endpoints does real COM/pactl work per call (see
/// <see cref="IAudioService.GetEndpoints"/>), and several buttons could carry this command
/// at once, so the resolved label is cached for one poll interval rather than re-enumerated
/// on every <see cref="GetText"/> call. A device change clears it through <see cref="InvalidateLabel"/>.
/// </summary>
private static readonly Lock CacheLock = new();
private static string? _cachedLabel;
private static long _cachedAtTimestamp;
public CommandDescriptor Descriptor { get; } = new()
{
CommandName = "Audio.CurrentOutput",
DisplayName = "Audio: Current Output",
Group = "Audio",
Icon = AudioButtonLayouts.Speaker,
ButtonLayout = AudioButtonLayouts.IconWithCaption(AudioButtonLayouts.Speaker, "Output", tall: true),
Description = "Show the active output device, and open the picker when pressed",
HiddenFromMenu = true,
// The look of the picker this button opens; the button itself only shows the device name.
ParameterTemplate = MixerTileStyle.ParameterTemplate,
Parameters = MixerTileStyle.Parameters()
};
public ButtonTargets SupportedTargets => ButtonTargets.TouchButton;
public TimeSpan UpdateInterval => LabelLifetime;
public string GetText(CommandContext ctx) => Label(ctx, audio, aliasStore);
/// <summary>How long a resolved label is reused, and how often the host polls it. A fallback
/// only: a default change refreshes the button at once.</summary>
internal static readonly TimeSpan LabelLifetime = TimeSpan.FromSeconds(30);
/// <summary>
/// The name of the current default output, as every button that shows it reads it. Shared with
/// <see cref="AudioCycleOutputCommand"/>, which shows the same thing.
/// </summary>
internal static string Label(CommandContext ctx, IAudioService audio, AudioAliasStore aliasStore)
{
string label;
lock (CacheLock)
{
long now = Stopwatch.GetTimestamp();
if (_cachedLabel != null && Stopwatch.GetElapsedTime(_cachedAtTimestamp, now) < LabelLifetime)
{
label = _cachedLabel;
}
else
{
label = ResolveLabel(audio, aliasStore);
_cachedLabel = label;
_cachedAtTimestamp = now;
}
}
// Translated on the way out, not into the cache: a language switch has to reach a label
// that is already cached. Device names come from the OS and stay as they are.
return label == NoDeviceLabel ? ctx.Host.Tr(NoDeviceLabel) : label;
}
/// <summary>Forgets the cached label, so a button refreshed right after a switch shows the new device.</summary>
internal static void InvalidateLabel()
{
lock (CacheLock) _cachedLabel = null;
}
private static string ResolveLabel(IAudioService audio, AudioAliasStore aliasStore)
{
foreach (AudioEndpointInfo ep in audio.GetEndpoints(AudioEndpointKind.Render))
{
if (ep.IsDefault) return aliasStore.Resolve(ep);
}
return NoDeviceLabel;
}
public Task Execute(CommandContext ctx)
{
AudioFolderGrid grid = FolderGridResolver.Resolve(ctx.Host);
MixerTileStyle style = MixerTileStyle.FromParameters(ctx.Parameters);
ctx.Host.OpenFolder(new AudioDevicesFolderProvider(audio, AudioEndpointKind.Render, aliasStore, visibility, grid, ctx.Host,
style));
return Task.CompletedTask;
}
}