-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cloned Playback Volume data model module into Capture module #175
Open
DragRedSim
wants to merge
13
commits into
Artemis-RGB:master
Choose a base branch
from
DragRedSim:feature/audio-capture
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+355
−220
Open
Changes from 1 commit
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
6c9637a
Cloned Playback Volume module into Capture module
DragRedSim 3a42800
Altered checked endpoint to be default communications capture device
DragRedSim 15bb0b0
Merge branch 'Artemis-RGB:master' into feature/audio-capture
DragRedSim 4b32652
Attempt to refactor into abstract class
DragRedSim 1f0145d
Bugfix: role/directions actually honoured
DragRedSim fac1795
Bugfix: actually requested audio capture for volume levels
DragRedSim 7d959b8
Bugfix: actually requested audio capture for volume levels
DragRedSim 65e02c6
Merge branch 'feature/audio-capture' of https://github.com/DragRedSim…
DragRedSim 93f774d
Reversion of some of my tweaks made in experimentation
DragRedSim 6d77290
Deduplication and cleanup relevant to the CaptureVolumeModule
DragRedSim 9740b9c
Merge branch 'Artemis-RGB:master' into feature/audio-capture
DragRedSim 93b960a
Merge branch 'Artemis-RGB:master' into feature/audio-capture
DragRedSim ab1269f
Merge branch 'Artemis-RGB:master' into feature/audio-capture
DragRedSim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
228 changes: 228 additions & 0 deletions
228
src/Collections/Artemis.Plugins.Audio/DataModelExpansion/AudioEndpointVolumeModule.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using Artemis.Core; | ||
using Artemis.Core.Modules; | ||
using Artemis.Plugins.Audio.DataModelExpansion.DataModels; | ||
using Artemis.Plugins.Audio.Services; | ||
using NAudio.CoreAudioApi; | ||
using Serilog; | ||
|
||
namespace Artemis.Plugins.Audio.DataModelExpansion | ||
{ | ||
public abstract class AudioEndpointVolumeModule : Module<AudioEndpointVolumeDataModel> | ||
{ | ||
public override List<IModuleActivationRequirement> ActivationRequirements => null; | ||
|
||
#region Constructor | ||
|
||
public AudioEndpointVolumeModule(ILogger logger, NAudioDeviceEnumerationService naudioDeviceEnumerationService, NAudio.CoreAudioApi.Role role, NAudio.CoreAudioApi.DataFlow flow) | ||
{ | ||
_logger = logger; | ||
_naudioDeviceEnumerationService = naudioDeviceEnumerationService; | ||
_role = role; | ||
_flow = flow; | ||
} | ||
|
||
#endregion | ||
|
||
#region Properties & Fields | ||
|
||
internal readonly NAudioDeviceEnumerationService _naudioDeviceEnumerationService; | ||
internal readonly ILogger _logger; | ||
internal readonly object _audioEventLock = new(); | ||
internal readonly List<DynamicChild<AudioChannelDataModel>> _channelsDataModels = new(); | ||
internal readonly NAudio.CoreAudioApi.DataFlow _flow; | ||
internal readonly NAudio.CoreAudioApi.Role _role; | ||
internal bool _audioDeviceChanged; | ||
internal float _lastMasterPeakVolumeNormalized; | ||
internal MMDevice _audioDevice; | ||
internal AudioEndpointVolume _audioEndpointVolume; | ||
|
||
#endregion | ||
|
||
#region Plugin Methods | ||
|
||
public abstract override DataModelPropertyAttribute GetDataModelDescription(); | ||
|
||
public override void Enable() | ||
{ | ||
_naudioDeviceEnumerationService.NotificationClient.DefaultDeviceChanged += NotificationClient_DefaultDeviceChanged; | ||
UpdateAudioEndpointDevice(true); | ||
|
||
// We don't need mor than ~30 updates per second. It will keep CPU usage controlled. 60 or more updates per second could rise cpu usage | ||
AddTimedUpdate(TimeSpan.FromMilliseconds(33), UpdatePeakVolume, "UpdatePeakVolume"); | ||
} | ||
|
||
public override void Disable() | ||
{ | ||
_naudioDeviceEnumerationService.NotificationClient.DefaultDeviceChanged -= NotificationClient_DefaultDeviceChanged; | ||
_audioEndpointVolume?.Dispose(); | ||
_audioEndpointVolume = null; | ||
FreeAudioEndpointDevice(); | ||
} | ||
|
||
public override void Update(double deltaTime) | ||
{ | ||
DataModel.TimeSinceLastSound += TimeSpan.FromSeconds(deltaTime); | ||
if (_audioDeviceChanged) UpdateAudioEndpointDevice(); | ||
} | ||
|
||
#endregion | ||
|
||
#region Update DataModel Methods | ||
|
||
private void UpdatePeakVolume(double deltaTime) | ||
{ | ||
if (IsEnabled == false) | ||
{ | ||
// To avoid null object exception on _enumerator use after plugin is disabled. | ||
return; | ||
} | ||
|
||
// If no one one is using this DataModel, don't update this part. | ||
if (DataModel.ActivePaths.Count < 1) | ||
{ | ||
return; | ||
} | ||
|
||
if (_audioDevice == null) | ||
{ | ||
// To avoid null object exception on device change or don't update if there are no devices at all | ||
return; | ||
} | ||
|
||
// Update Main volume Peak | ||
lock (_audioEventLock) // To avoid query an Device/EndPoint that is not the current device anymore or has more or less channels | ||
{ | ||
// Absolute master peak volume | ||
float peakVolumeNormalized = _audioDevice?.AudioMeterInformation.MasterPeakValue ?? 0f; | ||
|
||
// Don't update datamodel if not neeeded | ||
if (Math.Abs(_lastMasterPeakVolumeNormalized - peakVolumeNormalized) < 0.00001f) | ||
return; | ||
|
||
// Sound detected. Reset timespan | ||
if (Math.Abs(_lastMasterPeakVolumeNormalized - 0.0) > 0.00001f) DataModel.TimeSinceLastSound = TimeSpan.Zero; | ||
|
||
DataModel.PeakVolumeNormalized = _lastMasterPeakVolumeNormalized = peakVolumeNormalized; | ||
DataModel.PeakVolume = peakVolumeNormalized * 100f; | ||
|
||
// Master peak volume relative to master volume | ||
DataModel.PeakVolumeRelativeNormalized = peakVolumeNormalized * DataModel.VolumeNormalized; | ||
DataModel.PeakVolumeRelative = peakVolumeNormalized * 100f * DataModel.VolumeNormalized; | ||
|
||
// Update Channels Peak | ||
AudioMeterInformationChannels channelsVolumeNormalized = _audioDevice?.AudioMeterInformation.PeakValues; | ||
|
||
//One more check because AudioEndpoint device can be null any time (device for example). If this is the case, just keep the actual values and update in the next update. | ||
if (channelsVolumeNormalized == null) | ||
return; | ||
|
||
for (int i = 0; i < _channelsDataModels.Count && i < channelsVolumeNormalized.Count; i++) | ||
{ | ||
DynamicChild<AudioChannelDataModel> audioChannelDataModel = _channelsDataModels[i]; | ||
audioChannelDataModel.Value.PeakVolumeNormalized = channelsVolumeNormalized[i]; | ||
audioChannelDataModel.Value.PeakVolume = channelsVolumeNormalized[i] * 100f; | ||
} | ||
} | ||
} | ||
|
||
private void UpdateVolumeDataModel() | ||
{ | ||
DataModel.VolumeChanged.Trigger(); | ||
DataModel.VolumeNormalized = _audioEndpointVolume.MasterVolumeLevelScalar; | ||
DataModel.Volume = DataModel.VolumeNormalized * 100f; | ||
DataModel.ChannelCount = _audioEndpointVolume.Channels.Count; | ||
DataModel.DeviceState = _audioDevice.State; | ||
DataModel.Muted = _audioEndpointVolume.Mute; | ||
|
||
lock (_audioEventLock) | ||
{ | ||
for (int i = 0; i < _channelsDataModels.Count; i++) | ||
{ | ||
DynamicChild<AudioChannelDataModel> audioChannelDataModel = _channelsDataModels[i]; | ||
float volumeNormalized = _audioEndpointVolume.Channels[i].VolumeLevelScalar; | ||
audioChannelDataModel.Value.VolumeNormalized = volumeNormalized; | ||
audioChannelDataModel.Value.Volume = volumeNormalized * 100f; | ||
} | ||
} | ||
} | ||
|
||
private void PopulateChannels() | ||
{ | ||
DataModel.Channels.ClearDynamicChildren(); | ||
_logger.Verbose($"AudioEndpoint device {_audioDevice.FriendlyName} channel list cleared"); | ||
_logger.Verbose($"Preparing to populate {_audioEndpointVolume.Channels.Count} channels for device {_audioDevice.FriendlyName}"); | ||
_channelsDataModels.Clear(); | ||
for (int i = 0; i < _audioEndpointVolume.Channels.Count; i++) | ||
{ | ||
_channelsDataModels.Add( | ||
DataModel.Channels.AddDynamicChild(i.ToString(), new AudioChannelDataModel { ChannelIndex = i }, $"Channel {i}") | ||
); | ||
_logger.Verbose($"AudioEndpoint device {_audioDevice.FriendlyName} channel {i} populated"); | ||
} | ||
} | ||
|
||
#endregion | ||
|
||
#region Audio Management methods | ||
|
||
private void NotificationClient_DefaultDeviceChanged() | ||
{ | ||
_audioDeviceChanged = true; | ||
// Workarround. MMDevice won't dispose if Dispose() is called from | ||
// non parent thread and NaudioNotificationClient callbacks come from another thread. | ||
// We will use Update() mrhod to dispose MMDevice from creator thread because this (NotificationClient_DefaultDeviceChanged()) is called from another thread | ||
} | ||
|
||
private void UpdateAudioEndpointDevice(bool firstRun = false) | ||
{ | ||
lock (_audioEventLock) | ||
{ | ||
if (!firstRun) FreeAudioEndpointDevice(); | ||
|
||
if (SetAudioEndpointDevice()) | ||
{ | ||
PopulateChannels(); | ||
_audioDeviceChanged = false; | ||
UpdateVolumeDataModel(); | ||
} | ||
} | ||
} | ||
|
||
private void FreeAudioEndpointDevice() | ||
{ | ||
string disposingAudioEndpointDeviceFriendlyName = _audioDevice?.FriendlyName ?? "Unknown"; | ||
_audioDevice?.Dispose(); | ||
_audioDevice = null; | ||
_logger.Verbose($"AudioEndpoint device {disposingAudioEndpointDeviceFriendlyName} unregistered as source device to fill AudioEndpoint volume data model"); | ||
DataModel.Reset(); | ||
} | ||
|
||
private bool SetAudioEndpointDevice() | ||
{ | ||
_audioDevice = _naudioDeviceEnumerationService.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console); | ||
|
||
if (_audioDevice == null) | ||
{ | ||
_logger.Verbose("No audio device found with Console role. Audio peak volume won't be updated."); | ||
return false; | ||
} | ||
|
||
_audioEndpointVolume = _audioDevice.AudioEndpointVolume; | ||
|
||
_audioEndpointVolume.OnVolumeNotification += _audioEndpointVolume_OnVolumeNotification; | ||
DataModel.DefaultDeviceName = _audioDevice.FriendlyName; | ||
|
||
_logger.Verbose($"AudioEndpoint device {_audioDevice.FriendlyName} registered to to fill AudioEndpoint volume data model"); | ||
return true; | ||
} | ||
|
||
private void _audioEndpointVolume_OnVolumeNotification(AudioVolumeNotificationData data) | ||
{ | ||
UpdateVolumeDataModel(); | ||
} | ||
|
||
#endregion | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way at least the module will correctly capture the mic. Unfortunately, the volume doesn't seem to update unless i also have the native windows mixer open:
Artemis.UI.Windows_e0ZRENbMP9.mp4
I'm not sure if we need to listen for some event or call some update function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, missed that one (_flow/_role). That's a major issue. Fix implemented.
In regards to the volume not updating, I can confirm this issue occurs on my system. My suspicion is that nAudio is registering the device and pulling in data - but not actually opening access to the device itself, in regards to the security context. If you note the icon in the system tray for "Apps using your microphone", it will appear when the Windows recording device panel is enabled, and disappear when it is not. The same occurs if you use the Settings sound panel (the "replacement" control panel), or (for another example) if you have a Discord call open. I'll do a little more digging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, we do have to initialise a recording access in order for the volume data to be updated. This was outside my use case, as I only wanted the mute state or otherwise; but better to do it right the first time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my experience users don't like it when apps listen to the mic 24/7 so I would be careful with how you implement this. Maybe separate volume and actual sounds into separate modules or add an option to enable mic capture or something? It's not a trivial feature to add imo.