diff --git a/.git-pr-release-template b/.git-pr-release-template new file mode 100644 index 0000000..a471555 --- /dev/null +++ b/.git-pr-release-template @@ -0,0 +1,4 @@ +<%= ENV['APP_VERSION'] %> +<% pull_requests.each do |pr| -%> +<%= pr.to_checklist_item %> +<% end -%> \ No newline at end of file diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..cd0f4f7 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,37 @@ +name: create a release pull request + +on: + workflow_dispatch: + + pull_request: + types: [ closed ] + branches: + - develop + +env: + pathOfVersioning: ${{ github.workspace }}/SimpleVolumeMixer + +jobs: + create-release-pr: + runs-on: ubuntu-latest + steps: + + - name: checkout this solution + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - id: nbgv + run: | + VERSION=`nbgv get-version -p ${{ env.pathOfVersioning }} -v NuGetPackageVersion` + echo "::set-output name=version::$VERSION" + + - name: create a release pull request + uses: bakunyo/git-pr-release-action@281e1fe424fac01f3992542266805e4202a22fe0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_PR_RELEASE_BRANCH_PRODUCTION: master + GIT_PR_RELEASE_BRANCH_STAGING: develop + GIT_PR_RELEASE_LABELS: release + APP_VERSION: v${{ steps.nbgv.outputs.version }} + GIT_PR_RELEASE_TEMPLATE: .git-pr-release-template diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a82c6e4..8077a9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ env: jobs: build-artifact: + if: github.event.pull_request.merged == true runs-on: windows-2019 outputs: artifactName: ${{ steps.vars.outputs.artifactName }} @@ -79,10 +80,10 @@ jobs: - id: vars shell: bash run: | - if [[ "${{ needs.build-artifact.outputs.prereleaseVersionNoLeadingHyphen }}" = "beta" -o "${{ needs.build-artifact.outputs.prereleaseVersionNoLeadingHyphen }}" = "alpha" ]]; then - echo "::set-output name=isPrerelease::true"; + if [ "${{ needs.build-artifact.outputs.prereleaseVersionNoLeadingHyphen }}" = "beta" ] || [ "${{ needs.build-artifact.outputs.prereleaseVersionNoLeadingHyphen }}" = "alpha" ]; then + echo "::set-output name=isPrerelease::true" else - echo "::set-output name=isPrerelease::false"; + echo "::set-output name=isPrerelease::false" fi; - id: create-release @@ -99,6 +100,7 @@ jobs: run: | echo "::set-output name=uploadUrl::${{ steps.create-release.outputs.upload_url }}" + upload-release: runs-on: ubuntu-latest needs: [ create-release, build-artifact ] diff --git a/.idea/.idea.SimpleVolumeMixer/.idea/misc.xml b/.idea/.idea.SimpleVolumeMixer/.idea/misc.xml new file mode 100644 index 0000000..283b9b4 --- /dev/null +++ b/.idea/.idea.SimpleVolumeMixer/.idea/misc.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Contracts/Models/Repository/ICoreAudioRepository.cs b/SimpleVolumeMixer/Core/Contracts/Models/Repository/ICoreAudioRepository.cs index bdfa79f..c8b12f2 100644 --- a/SimpleVolumeMixer/Core/Contracts/Models/Repository/ICoreAudioRepository.cs +++ b/SimpleVolumeMixer/Core/Contracts/Models/Repository/ICoreAudioRepository.cs @@ -1,4 +1,5 @@ -using Reactive.Bindings; +using System.Collections.ObjectModel; +using Reactive.Bindings; using SimpleVolumeMixer.Core.Helper.CoreAudio; using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; @@ -6,8 +7,31 @@ namespace SimpleVolumeMixer.Core.Contracts.Models.Repository; public interface ICoreAudioRepository { - ReadOnlyReactiveCollection AudioDevices { get; } + /// + /// 現在使用可能なの一覧を取得する。 + /// 読み取り専用であり、このオブジェクトからデバイスの増減を行うことは出来ない。 + /// デバイスの増減はによりCoreAudioAPIからデバイス一覧を取り直すか、 + /// がCoreAudioAPIからの通知を受け、その結果デバイスが追加されるかに限る。 + /// + ReadOnlyObservableCollection AudioDevices { get; } + + /// + /// ロールのデバイスを取得する。 + /// インスタンスはからロールを持つ物を検索して取得できる値と同一である。 + /// IReadOnlyReactiveProperty CommunicationRoleDevice { get; } + + /// + /// ロールのデバイスを取得する。 + /// インスタンスはからロールを持つ物を検索して取得できる値と同一である。 + /// IReadOnlyReactiveProperty MultimediaRoleDevice { get; } + + /// + /// 引数のデバイスとのデバイスに対し、のロールを割り当てる。 + /// + /// + /// + /// void SetDefaultDevice(AudioDeviceAccessor accessor, DataFlowType dataFlowType, RoleType roleType); } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/DisposableComponent.cs b/SimpleVolumeMixer/Core/Helper/Component/DisposableComponent.cs deleted file mode 100644 index d9beb63..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/DisposableComponent.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reactive.Disposables; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -/// -/// Concrete class for IDisposableComponent. See Interface for detailed behavior. -/// -/// -public abstract class DisposableComponent : IDisposableComponent -{ - /// - public event EventHandler? Disposing; - - /// - public event EventHandler? Disposed; - - /// - /// An external component that delegates the entire destruction process. - /// - private readonly CompositeDisposable _disposable; - - /// - /// ctor. - /// - public DisposableComponent() - { - _disposable = new CompositeDisposable(); - } - - /// - public ICollection Disposable => _disposable; - - /// - public bool IsDisposed => _disposable.IsDisposed; - - /// - /// Called before the object is destroyed by a call to Dispose(). - /// - protected virtual void OnDisposing() - { - Disposing?.Invoke(this, EventArgs.Empty); - } - - /// - /// Called after an object has been destroyed by a call to Dispose(). - /// - protected virtual void OnDisposed() - { - Disposed?.Invoke(this, EventArgs.Empty); - } - - /// - public void Dispose() - { - if (_disposable.IsDisposed) - { - return; - } - - OnDisposing(); - _disposable.Dispose(); - OnDisposed(); - } - - /// - void IDisposable.Dispose() - { - Dispose(); - } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/IDisposableComponent.cs b/SimpleVolumeMixer/Core/Helper/Component/IDisposableComponent.cs deleted file mode 100644 index 0eca6ea..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/IDisposableComponent.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -public interface IDisposableComponent : IDisposable -{ - /// - /// This event occurs before the destruction process by IDisposable. - /// - event EventHandler? Disposing; - - /// - /// This event occurs after the destruction process by IDisposable. - /// - event EventHandler? Disposed; - - /// - /// Obtains an ICollection that holds IDisposable and allows the IDisposable associated with this object to be destroyed at once. - /// IDisposable registered with an ICollection that can be retrieved from this property will be destroyed as soon as this object's Dispose() method is called. - /// - ICollection Disposable { get; } - - /// - /// Gets whether the object has been destroyed. - /// After calling the Dispose() method, this property always returns false. - /// - bool IsDisposed { get; } - - /// - /// Calls the object destruction process. - /// After this function is called, this object cannot be used. - /// - /// - new void Dispose(); -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/IPollingMonitor.cs b/SimpleVolumeMixer/Core/Helper/Component/IPollingMonitor.cs new file mode 100644 index 0000000..d587f32 --- /dev/null +++ b/SimpleVolumeMixer/Core/Helper/Component/IPollingMonitor.cs @@ -0,0 +1,68 @@ +using System.ComponentModel; +using SimpleVolumeMixer.Core.Helper.Component.Types; + +namespace SimpleVolumeMixer.Core.Helper.Component; + +/// +/// Provides an interface to a function that monitors certain values at regular intervals. +/// +public interface IPollingMonitor : INotifyPropertyChanged +{ + /// + /// Occurs when the result of polling by this interface differs from the previous value. + /// + /// + new event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Holds the latest value obtained during polling. + /// If the value is rewritten by the polling process, the event is fired. + /// + object Value { get; } + + /// + /// Gets or sets the interval for polling. The interval can be set in milliseconds. + /// See the enum definition for detailed seconds. + /// If is set, + /// no polling is performed and the value is updated only when the method is called externally. + /// If this property is changed while the polling process is running, + /// the polling process execution interval is adjusted; if is set, the polling process is stopped. + /// + /// + PollingMonitorIntervalType IntervalType { get; set; } + + /// + /// Start polling. However, this method only starts the process and does not block the thread. + /// If this method is called when is set to or when a polling operation is already running, + /// no new polling operation will be started and nothing will happen. + /// + void Start(); + + /// + /// Stops polling, even if called when polling is not running, + /// including when is , no exception is made. + /// + void Stop(); + + /// + /// The process of obtaining the latest value of the polling process is implemented. + /// When the value is updated, the property is rewritten and the fact that the value has been updated is notified externally by . + /// + /// + /// + void Refresh(); +} + +/// +/// An interface that applies a generic to the property of the interface. +/// +/// +public interface IPollingMonitor : IPollingMonitor +{ + /// + /// It has the same functionality as the inherited source, the only difference being that the type is generic. + /// See the documentation of the inherited source for detailed functionality. + /// + /// + new T Value { get; } +} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/IPropertyMonitor.cs b/SimpleVolumeMixer/Core/Helper/Component/IPropertyMonitor.cs deleted file mode 100644 index a6ba34e..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/IPropertyMonitor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using SimpleVolumeMixer.Core.Helper.Component.Types; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -public interface IPropertyMonitor -{ - object Value { get; set; } - PropertyMonitorIntervalType IntervalType { get; set; } - void Start(); - void Stop(); - void Refresh(); -} - -public interface IPropertyMonitor : IPropertyMonitor -{ - new T Value { get; set; } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/KeyValueInstanceManager.cs b/SimpleVolumeMixer/Core/Helper/Component/KeyValueInstanceManager.cs index 8ff5747..3c126eb 100644 --- a/SimpleVolumeMixer/Core/Helper/Component/KeyValueInstanceManager.cs +++ b/SimpleVolumeMixer/Core/Helper/Component/KeyValueInstanceManager.cs @@ -1,20 +1,36 @@ using System; using System.Collections.Generic; +using DisposableComponents; namespace SimpleVolumeMixer.Core.Helper.Component; +/// +/// This component is used to ensure that the TK and TV values are always 1-1. +/// The key must implement so that the value can also be destroyed when the key is destroyed. +/// +/// Key value type +/// Type corresponding to TK public class KeyValueInstanceManager : DisposableComponent where TK : IDisposableComponent { private readonly object _gate = new(); private readonly IDictionary _instances; private readonly Func _factory; + /// + /// ctor + /// + /// Function to generate the value corresponding to a key public KeyValueInstanceManager(Func factory) { _instances = new Dictionary(); _factory = factory; } + /// + /// If the value corresponding to the key already exists, return that value; if not, create and return a new value. + /// + /// + /// public TV Obtain(TK key) { lock (_gate) @@ -33,6 +49,11 @@ public TV Obtain(TK key) } } + /// + /// Process to erase the value from this instance when the key is destroyed. + /// + /// + /// private void KeyOnDisposed(object? sender, EventArgs e) { if (sender == null) diff --git a/SimpleVolumeMixer/Core/Helper/Component/NotifyPropertyChangedBase.cs b/SimpleVolumeMixer/Core/Helper/Component/NotifyPropertyChangedBase.cs index f1dc263..65c98ec 100644 --- a/SimpleVolumeMixer/Core/Helper/Component/NotifyPropertyChangedBase.cs +++ b/SimpleVolumeMixer/Core/Helper/Component/NotifyPropertyChangedBase.cs @@ -3,10 +3,25 @@ namespace SimpleVolumeMixer.Core.Helper.Component; +/// +/// Abstract class for efficiently preparing implementations of the interface +/// +/// public class NotifyPropertyChangedBase : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler? PropertyChanged; + /// + /// Attempts to write the value of to the . + /// When the value of the is updated to the value of , + /// a property change notification by is generated. + /// If the values are the same, the aforementioned process is not performed. + /// + /// A reference to a variable that holds a value. Note that the variable may be rewritten to the value of + /// new value + /// Name of the location where this function was called. This is set automatically, so you do not need to do anything explicitly. + /// protected void SetValue(ref T holder, T newValue, [CallerMemberName] string? callerMemberName = null) { if (!Equals(holder, newValue)) @@ -16,6 +31,10 @@ protected void SetValue(ref T holder, T newValue, [CallerMemberName] string? } } + /// + /// Raises PropertyChanged event + /// + /// Name of the property where the change occurred protected void RaisePropertyChanged(string? propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); diff --git a/SimpleVolumeMixer/Core/Helper/Component/PollingMonitor.cs b/SimpleVolumeMixer/Core/Helper/Component/PollingMonitor.cs new file mode 100644 index 0000000..a92c625 --- /dev/null +++ b/SimpleVolumeMixer/Core/Helper/Component/PollingMonitor.cs @@ -0,0 +1,169 @@ +using System; +using System.Reactive.Disposables; +using System.Timers; +using Reactive.Bindings.Extensions; +using SimpleVolumeMixer.Core.Helper.Component.Types; + +namespace SimpleVolumeMixer.Core.Helper.Component; + +public static class PollingMonitor +{ + /// + /// Comparison function for int type only. + /// + public static Func IntComparer = (x, y) => x == y; + + /// + /// Comparison function for float type only. + /// + public static Func FloatComparer = (x, y) => x.Equals(y); + + /// + /// Comparison function for bool type only. + /// + public static Func BoolComparer = (x, y) => x == y; + + /// + /// Comparison processing using object.Equals; + /// if boxing occurs, as in the case of ints and floats, it is more efficient to have a dedicated comparison function. + /// + /// + /// + public static Func GetDefaultComparer() => (x, y) => Equals(x, y); +} + +public class PollingMonitor : NotifyPropertyChangedBase, IDisposable, IPollingMonitor +{ + private static readonly Action DefaultWriter = _ => { }; + private readonly Func _comparer; + + private readonly CompositeDisposable _disposable; + private readonly Func _reader; + private readonly Timer _timer; + private readonly Action _writer; + private PollingMonitorIntervalType _intervalType; + private T _value; + + /// + /// ctor + /// + /// Polling interval + /// Function making the polling target call + /// This function delegates write processing when the Value property is written from the outside. + /// Comparison process to detect that a value has been rewritten + public PollingMonitor( + PollingMonitorIntervalType intervalType, + Func reader, + Action? writer = null, + Func? comparer = null + ) + { + _disposable = new CompositeDisposable(); + _reader = reader; + _writer = writer != null ? writer : DefaultWriter; + _comparer = comparer != null ? comparer : PollingMonitor.GetDefaultComparer(); + + _value = reader(); + RaisePropertyChanged(nameof(Value)); + + _timer = new Timer().AddTo(_disposable); + _timer.Enabled = false; + _timer.Elapsed += TimerOnElapsed; + + IntervalType = intervalType; + } + + /// + /// Extend IPollingMonitor.Value to also support write processing. + /// The actual writing process is performed through the function object obtained at class initialization. + /// Also, to avoid changing the interface behavior (or rather, to avoid notification loops), + /// the event is not triggered when writing to this property. + /// + /// + public T Value + { + get => _value; + set + { + _writer(value); + _value = value; + } + } + + /// + object IPollingMonitor.Value + { +#pragma warning disable CS8603 + get => Value; +#pragma warning restore CS8603 + } + + /// + public PollingMonitorIntervalType IntervalType + { + get => _intervalType; + set + { + _intervalType = value; + + if (_intervalType == PollingMonitorIntervalType.Manual) + { + _timer.Stop(); + } + else + { + _timer.Interval = (double)_intervalType; + _timer.Start(); + } + } + } + + private void TimerOnElapsed(object sender, ElapsedEventArgs e) + { + Refresh(); + } + + /// + public void Start() + { + if (_intervalType == PollingMonitorIntervalType.Manual) + { + return; + } + + if (_timer.Enabled) + { + return; + } + + _timer.Enabled = true; + _timer.Start(); + } + + /// + public void Stop() + { + _timer.Enabled = false; + _timer.Stop(); + } + + /// + public void Refresh() + { + var newValue = _reader(); + if (!_comparer(_value, newValue)) + { + _value = newValue; + RaisePropertyChanged(nameof(Value)); + } + } + + /// + public void Dispose() + { + _timer.Elapsed -= TimerOnElapsed; + _timer.Stop(); + + _disposable.Dispose(); + } +} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/PropertyMonitor.cs b/SimpleVolumeMixer/Core/Helper/Component/PropertyMonitor.cs deleted file mode 100644 index bd2d8b4..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/PropertyMonitor.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Reactive.Disposables; -using System.Timers; -using Reactive.Bindings.Extensions; -using SimpleVolumeMixer.Core.Helper.Component.Types; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -public static class PropertyMonitor -{ - public static readonly Func IntComparer = (x, y) => x == y; - public static readonly Func FloatComparer = (x, y) => x.Equals(y); - public static readonly Func BoolComparer = (x, y) => x == y; -} - -public class PropertyMonitor : NotifyPropertyChangedBase, IDisposable, IPropertyMonitor -{ - private static readonly Func DefaultComparer = (x, y) => Equals(x, y); - private static readonly Action DefaultWriter = _ => { }; - private readonly Func _comparer; - - private readonly CompositeDisposable _disposable; - private readonly Func _reader; - private readonly Timer _timer; - private readonly Action _writer; - private PropertyMonitorIntervalType _intervalType; - private T _value; - - public PropertyMonitor( - PropertyMonitorIntervalType intervalType, - Func reader, - Action? writer = null, - Func? comparer = null - ) - { - _disposable = new CompositeDisposable(); - _intervalType = intervalType; - _reader = reader; - _writer = writer != null ? writer : DefaultWriter; - _comparer = comparer != null ? comparer : DefaultComparer; - - _timer = new Timer().AddTo(_disposable); - _timer.Enabled = true; - _timer.Elapsed += TimerOnElapsed; - TimerSetting(); - - _value = reader(); - RaisePropertyChanged(nameof(Value)); - } - - public T Value - { - get => _value; - set - { - WriteValue(value); - _value = value; - } - } - - object IPropertyMonitor.Value - { -#pragma warning disable CS8603 - get => Value; -#pragma warning restore CS8603 - set => Value = (T)value; - } - - public PropertyMonitorIntervalType IntervalType - { - get => _intervalType; - set - { - _intervalType = value; - TimerSetting(); - } - } - - private void TimerOnElapsed(object sender, ElapsedEventArgs e) - { - Refresh(); - } - - private void TimerSetting() - { - if (_intervalType == PropertyMonitorIntervalType.Manual) - { - _timer.Stop(); - } - else - { - _timer.Interval = (double)_intervalType; - _timer.Start(); - } - } - - public void Start() - { - if (_intervalType == PropertyMonitorIntervalType.Manual) - { - throw new InvalidOperationException("state is manual."); - } - - _timer.Start(); - } - - public void Stop() - { - _timer.Stop(); - } - - public void Refresh() - { - var newValue = ReadValue(); - if (!_comparer(_value, newValue)) - { - _value = newValue; - RaisePropertyChanged(nameof(Value)); - } - } - - protected virtual T ReadValue() - { - return _reader(); - } - - protected virtual void WriteValue(T value) - { - _writer(value); - } - - public void Dispose() - { - _timer.Stop(); - _timer.Elapsed -= TimerOnElapsed; - - _disposable.Dispose(); - } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/QueueProcessor.cs b/SimpleVolumeMixer/Core/Helper/Component/QueueProcessor.cs index aa5718d..bf202a1 100644 --- a/SimpleVolumeMixer/Core/Helper/Component/QueueProcessor.cs +++ b/SimpleVolumeMixer/Core/Helper/Component/QueueProcessor.cs @@ -2,57 +2,111 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; +using DisposableComponents; +using Microsoft.Extensions.Logging; using Reactive.Bindings.Extensions; namespace SimpleVolumeMixer.Core.Helper.Component; +/// +/// Provides a mechanism for managing (and related objects) in a queue and executing them sequentially in a worker thread. +/// +/// Types of objects passed as Func arguments +/// Type of object returned as the return value of Func public class QueueProcessor : DisposableComponent { - private readonly BlockingCollection> _handles; + private readonly string _name; + private readonly ILogger _logger; + private readonly BlockingCollection> _items; private readonly Task _loopTask; private readonly CancellationTokenSource _cancellation; - public QueueProcessor(int capacity = 1024) + /// + /// ctor + /// + /// + /// + /// Maximum number of queues registered + public QueueProcessor(string name, ILogger logger, int capacity = 4096) { - _handles = new BlockingCollection>(capacity).AddTo(Disposable); - _loopTask = new Task(() => DoProcess()).AddTo(Disposable); + _name = name; + _logger = logger; + _items = new BlockingCollection>(capacity).AddTo(Disposable); _cancellation = new CancellationTokenSource().AddTo(Disposable); - } - - public void StartRequest() - { + _loopTask = new Task(() => DoProcess(), _cancellation.Token).AddTo(Disposable); _loopTask.Start(); } - public void StopRequest() + /// + /// Register Func (and its auxiliary objects) in the queue. + /// When the number of registrations in the queue reaches the capacity set in the constructor, + /// the thread calling this method blocks until the number of registrations in the queue is less than the capacity. + /// + /// + public void Push(QueueProcessorItem item) { - _cancellation.Cancel(false); - } - - public void Push(QueueProcessorHandle handle) - { - _handles.Add(handle); + if (IsDisposed) + { + return; + } + + _items.Add(item); } + /// + /// Infinite loop process called by worker thread + /// private void DoProcess() { + _logger.LogInformation("[{}] start looping...", _name); + while (!_cancellation.IsCancellationRequested) { - var handle = _handles.Take(); - if (handle.CancelRequest) + try { - handle.Executed = true; - continue; - } + var item = _items.Take(_cancellation.Token); + if (item.CancelRequest) + { + item.Executed = true; + continue; + } - handle.Result = handle.Function(handle.Argument); - handle.Executed = true; + item.Result = item.Function(item.Argument); + item.Executed = true; + } + catch (OperationCanceledException ex) + { + _logger.LogError(ex, "[{}] canceled", _name); + } } + + _logger.LogInformation("[{}] finish looping...", _name); } + /// protected override void OnDisposing() { - StopRequest(); + _logger.LogInformation("[{}] disposing...", _name); + + _cancellation.Cancel(false); + + switch (_loopTask.Status) + { + case TaskStatus.Canceled: + case TaskStatus.Faulted: + case TaskStatus.RanToCompletion: + break; + default: + _loopTask.Wait(); + break; + } + base.OnDisposing(); } + + protected override void OnDisposed() + { + _logger.LogInformation("[{}] disposed...", _name); + base.OnDisposed(); + } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorHandle.cs b/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorHandle.cs deleted file mode 100644 index d5de999..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorHandle.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -public static class QueueProcessorHandle -{ - public static QueueProcessorHandle OfAction(Action action) - { - return OfAction(null, action); - } - - public static QueueProcessorHandle OfAction(string? identity, Action action) - { - return OfAction(identity, null, _ => { action(); }); - } - - public static QueueProcessorHandle OfAction(TA1 args, Action action) - { - return OfAction(null, args, action); - } - - public static QueueProcessorHandle OfAction(string? identity, TA1 args, Action action) - { - return OfFunction(identity, args, x => - { - action(x); - return null; - }); - } - - public static QueueProcessorHandle OfFunction(Func func) - { - return OfFunction(null, func); - } - - public static QueueProcessorHandle OfFunction(string? identity, Func func) - { - return OfFunction(identity, null, func); - } - - public static QueueProcessorHandle OfFunction(TA1 args, Func func) - { - return OfFunction(null, args, func); - } - - public static QueueProcessorHandle OfFunction(string? identity, TA1 args, Func func) - { - return new QueueProcessorHandle(identity, args, func); - } -} - -public class QueueProcessorHandle -{ - public QueueProcessorHandle(string? identity, TA argument, Func func) - { - Identity = identity ?? Guid.NewGuid().ToString(); - Function = func; - Argument = argument; - CancelRequest = false; - - Result = default; - } - - public string Identity { get; } - public Func Function { get; } - public TA Argument { get; } - public TR? Result { get; set; } - public bool CancelRequest { get; set; } - public bool Executed { get; set; } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorItem.cs b/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorItem.cs new file mode 100644 index 0000000..c8618f3 --- /dev/null +++ b/SimpleVolumeMixer/Core/Helper/Component/QueueProcessorItem.cs @@ -0,0 +1,70 @@ +using System; + +namespace SimpleVolumeMixer.Core.Helper.Component; + +public static class QueueProcessorItem +{ + public static QueueProcessorItem OfAction(Action action) + { + return OfAction(null, action); + } + + public static QueueProcessorItem OfAction(string? identity, Action action) + { + return OfAction(identity, null, _ => { action(); }); + } + + public static QueueProcessorItem OfAction(TA1 args, Action action) + { + return OfAction(null, args, action); + } + + public static QueueProcessorItem OfAction(string? identity, TA1 args, Action action) + { + return OfFunction(identity, args, x => + { + action(x); + return null; + }); + } + + public static QueueProcessorItem OfFunction(Func func) + { + return OfFunction(null, func); + } + + public static QueueProcessorItem OfFunction(string? identity, Func func) + { + return OfFunction(identity, null, func); + } + + public static QueueProcessorItem OfFunction(TA1 args, Func func) + { + return OfFunction(null, args, func); + } + + public static QueueProcessorItem OfFunction(string? identity, TA1 args, Func func) + { + return new QueueProcessorItem(identity, args, func); + } +} + +public class QueueProcessorItem +{ + public QueueProcessorItem(string? identity, TA argument, Func func) + { + Identity = identity ?? Guid.NewGuid().ToString(); + Function = func; + Argument = argument; + CancelRequest = false; + + Result = default; + } + + public string Identity { get; } + public Func Function { get; } + public TA Argument { get; } + public TR? Result { get; set; } + public bool CancelRequest { get; set; } + public bool Executed { get; set; } +} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/SafetyAccessorComponent.cs b/SimpleVolumeMixer/Core/Helper/Component/SafetyAccessorComponent.cs deleted file mode 100644 index 457567f..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/SafetyAccessorComponent.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace SimpleVolumeMixer.Core.Helper.Component; - -/// -/// Extends DisposableComponent to provide a mechanism whereby read/write functions can be executed only when the object is not destroyed. -/// -/// -public abstract class SafetyAccessorComponent : DisposableComponent -{ - /// - /// ctor. - /// - public SafetyAccessorComponent() : base() - { - } - - /// - /// Assists in safely reading values from the reader. - /// If this object is destroyed, the value of defaultValue is returned. - /// - /// Function to read the value - /// Default value for when the object was destroyed - /// Type of value to be read - /// Value read from reader or default value - protected virtual T SafeRead(Func reader, T defaultValue) - { - return IsDisposed - ? defaultValue - : reader(); - } - - /// - /// Assists in safely writing the value of param. - /// If this object is destroyed, no call to writer is made. - /// - /// Function for writing values - /// Value to be written - /// Type of value to be written - protected virtual void SafeWrite(Action writer, T param) - { - if (!IsDisposed) - { - writer(param); - } - } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/SynchronizedReactiveCollectionWrapper.cs b/SimpleVolumeMixer/Core/Helper/Component/SynchronizedObservableCollectionWrapper.cs similarity index 59% rename from SimpleVolumeMixer/Core/Helper/Component/SynchronizedReactiveCollectionWrapper.cs rename to SimpleVolumeMixer/Core/Helper/Component/SynchronizedObservableCollectionWrapper.cs index 260936e..f8cc055 100644 --- a/SimpleVolumeMixer/Core/Helper/Component/SynchronizedReactiveCollectionWrapper.cs +++ b/SimpleVolumeMixer/Core/Helper/Component/SynchronizedObservableCollectionWrapper.cs @@ -1,23 +1,40 @@ using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reactive.Concurrency; +using System.Threading; +using DisposableComponents; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace SimpleVolumeMixer.Core.Helper.Component; -public class SynchronizedReactiveCollectionWrapper : DisposableComponent, ICollection where T : class +/// +/// Wrapper for exclusive handling of . +/// +/// element type +public class SynchronizedObservableCollectionWrapper : DisposableComponent, ICollection where T : class { private readonly object _gate = new(); - private readonly ReactiveCollection _collection; + private readonly ObservableCollection _collection; - public SynchronizedReactiveCollectionWrapper() + public SynchronizedObservableCollectionWrapper() { _collection = new ReactiveCollection().AddTo(Disposable); - ReadOnlyCollection = _collection.ToReadOnlyReactiveCollection(UIDispatcherScheduler.Default, false); + ReadOnlyCollection = _collection + .ToReadOnlyReactiveCollection(UIDispatcherScheduler.Default, false) + .AddTo(Disposable); } - public ReadOnlyReactiveCollection ReadOnlyCollection { get; } + /// + /// This is a read-only collection, linked to the update of the wrapped . + /// Change notifications are sent to the UI thread. + /// + public ReadOnlyObservableCollection ReadOnlyCollection { get; } + /// + /// A lock object used in the exclusion process. It is made into a property for use in inheritance. + /// protected object Gate => _gate; public IEnumerator GetEnumerator() diff --git a/SimpleVolumeMixer/Core/Helper/Component/Types/PollingMonitorIntervalType.cs b/SimpleVolumeMixer/Core/Helper/Component/Types/PollingMonitorIntervalType.cs new file mode 100644 index 0000000..c7b3a58 --- /dev/null +++ b/SimpleVolumeMixer/Core/Helper/Component/Types/PollingMonitorIntervalType.cs @@ -0,0 +1,34 @@ +namespace SimpleVolumeMixer.Core.Helper.Component.Types; + +/// +/// Represents the polling interval. The unit of the number set here is milliseconds. +/// +public enum PollingMonitorIntervalType +{ + /// + /// This is the value set when you want to update manually. + /// + Manual = int.MinValue, + /// + /// Check in increments of 500 milliseconds. + /// + Low = 500, + /// + /// Check in increments of 200 milliseconds. + /// + LowMiddle = 200, + /// + /// Check in increments of 100 milliseconds. + /// + Normal = 100, + /// + /// Check in increments of 40 milliseconds. + /// Note that heavy use will overload the app! + /// + High = 40, + /// + /// Check in increments of 10 milliseconds. + /// Note that heavy use will overload the app! + /// + Immediate = 10 +} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/Component/Types/PropertyMonitorIntervalType.cs b/SimpleVolumeMixer/Core/Helper/Component/Types/PropertyMonitorIntervalType.cs deleted file mode 100644 index 3996f6f..0000000 --- a/SimpleVolumeMixer/Core/Helper/Component/Types/PropertyMonitorIntervalType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace SimpleVolumeMixer.Core.Helper.Component.Types; - -public enum PropertyMonitorIntervalType -{ - Manual = int.MinValue, - Low = 500, - LowMiddle = 200, - Normal = 100, - High = 40, - Immediate = 10 -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessor.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessor.cs index 34661b6..356dc12 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessor.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessor.cs @@ -1,57 +1,123 @@ using System; +using System.Collections.ObjectModel; +using System.Threading; using System.Threading.Tasks; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; -using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +using SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; using SimpleVolumeMixer.Core.Helper.CoreAudio.Internal; using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class AudioDeviceAccessor : SafetyAccessorComponent +/// +/// と及び周辺インターフェース・関連情報を総括するラッパーオブジェクト。 +/// +/// +/// +/// +/// +public class AudioDeviceAccessor : SafetyAccessComponent { + /// + /// デバイスが持つロールに変化が発生した際に発動するイベントハンドラ。 + /// このオブジェクトがにより生成されて間もなくか、 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// + /// + /// + /// public event EventHandler? RoleChanged { add => Role.RoleChanged += value; remove => Role.RoleChanged -= value; } + private readonly ILogger _logger; private readonly AudioEndpointVolume _endpointVolume; private readonly AudioMeterInformation _meterInformation; private readonly AudioSessionAccessorManager _accessorManager; + /// + /// ctor + /// + /// + /// public AudioDeviceAccessor(MMDevice device, ILogger logger) { Device = device.AddTo(Disposable); + _logger = logger; _endpointVolume = AudioEndpointVolume.FromDevice(Device).AddTo(Disposable); _meterInformation = AudioMeterInformation.FromDevice(Device).AddTo(Disposable); _accessorManager = new AudioSessionAccessorManager(this, logger); - Role = new DeviceRoleHolder(this); + Role = new AudioDeviceRole(this); } + /// + /// 生の + /// public MMDevice Device { get; } - public DeviceRoleHolder Role { get; } - public ReadOnlyReactiveCollection Sessions => _accessorManager.ReadOnlyCollection; + + /// + /// このデバイスが持つロールを管理する + /// + /// + public AudioDeviceRole Role { get; } + + /// + /// このデバイスに紐付けられたの一覧を取得する。 + /// セッションの一覧にアクセスするには、をあらかじめ呼び出しておく必要がある。 + /// が呼び出された後はセッションにアクセスすることができなくなる。 + /// + public ReadOnlyObservableCollection Sessions => _accessorManager.ReadOnlyCollection; + public string? DeviceId => SafeRead(() => Device.DeviceID, null); public string? FriendlyName => SafeRead(() => Device.FriendlyName, null); public string? DevicePath => SafeRead(() => Device.DevicePath, null); public DeviceStateType DeviceState => SafeRead( - () => AccessorHelper.DeviceStates[Device.DeviceState], - DeviceStateType.Unknown); + () => Device.GetStateNative(out var value) >= 0 + ? AccessorHelper.DeviceStates[value] + : DeviceStateType.Unknown, + DeviceStateType.Unknown + ); public DataFlowType DataFlow => SafeRead( () => AccessorHelper.DataFlows[Device.DataFlow], DataFlowType.Unknown); public int ChannelCount => SafeRead(_endpointVolume.GetChannelCount, 0); - public float PeakValue => SafeRead(_meterInformation.GetPeakValue, 0.0f); - public float[]? ChannelsPeakValues => SafeRead(_meterInformation.GetChannelsPeakValues, null); + + public float PeakValue => SafeRead(() => _meterInformation.GetPeakValueNative(out var value) >= 0 + ? value + : 0.0f, + 0.0f + ); + + public float[]? ChannelsPeakValues + { + get + { + if (_meterInformation.GetMeteringChannelCountNative(out var count) < 0) + { + return null; + } + + if (_meterInformation.GetChannelsPeakValuesNative(count, out var result) < 0) + { + return null; + } + + return result; + } + } + public int MeteringChannelCount => SafeRead(_meterInformation.GetMeteringChannelCount, 0); public float MasterVolumeLevel @@ -62,28 +128,41 @@ public float MasterVolumeLevel public float MasterVolumeLevelScalar { - get => SafeRead(_endpointVolume.GetMasterVolumeLevelScalar, 0.0f); + get => SafeRead( + () => _endpointVolume.GetMasterVolumeLevelScalarNative(out var value) >= 0 + ? value + : 0.0f, + 0.0f + ); set => SafeWrite(v => _endpointVolume.MasterVolumeLevelScalar = v, value); } public bool IsMuted { - get => SafeRead(_endpointVolume.GetMute, false); + get + { + TrySafeRead(_endpointVolume.GetMute, false, out var result); + return result; + } set => SafeWrite(v => _endpointVolume.IsMuted = v, value); } public Task OpenSession() { + _logger.LogInformation("open session in {}", FriendlyName); return _accessorManager.OpenSession(); } public void CloseSession() { + _logger.LogInformation("close session in {}", FriendlyName); _accessorManager.CloseSession(); } protected override void OnDisposing() { + _logger.LogInformation("disposing device... {}", FriendlyName); + CloseSession(); base.OnDisposing(); } diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessorManager.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessorManager.cs index 24dccf0..0dacffd 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessorManager.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceAccessorManager.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; @@ -11,21 +12,33 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class AudioDeviceAccessorManager : SynchronizedReactiveCollectionWrapper +/// +/// 複数のを管理するためのオブジェクト。 +/// 各の生成・破棄を担うほか、 +/// ユーザ操作由来での機能実行及び、CoreAudioAPIからの通知を受けての機能実行を行う。 +/// +public class AudioDeviceAccessorManager : SynchronizedObservableCollectionWrapper { /// - /// いずれかのデバイスが破棄される際に呼び出される + /// このオブジェクトに登録されているいずれかのが + /// イベントを発動させた時に連動して発動する。 /// + /// public event EventHandler? DeviceDisposing; /// - /// いずれかのデバイスが破棄された際に呼び出される + /// このオブジェクトに登録されているいずれかのが + /// イベントを発動させた時に連動して発動する。 /// + /// public event EventHandler? DeviceDisposed; /// - /// デバイスロールが変更された際に呼び出される + /// このオブジェクトに登録されているいずれかのが + /// イベントを発動させた時に連動して発動する。 /// + /// + /// public event EventHandler? DeviceRoleChanged; private readonly ILogger _logger; @@ -33,6 +46,10 @@ public class AudioDeviceAccessorManager : SynchronizedReactiveCollectionWrapper< private readonly MMNotificationClient _notificationClient; private readonly NotificationClientEventAdapter _clientEventAdapter; + /// + /// ctor + /// + /// public AudioDeviceAccessorManager(ILogger logger) { _logger = logger; @@ -47,7 +64,7 @@ public AudioDeviceAccessorManager(ILogger logger) } /// - /// 引数のデバイスIDとデータフローを持つデバイスが内蔵コレクションにあるかを確認する + /// 引数のデバイスIDとデータフローを持つデバイスがコレクションにあるかを確認する /// /// /// @@ -66,7 +83,7 @@ public bool Contains(string? deviceId, DataFlowType dataFlowType) } /// - /// 引数のデバイスが内蔵コレクションにあるかを確認する + /// 引数のデバイスがコレクションにあるかを確認する /// /// /// @@ -95,7 +112,7 @@ public bool Contains(MMDevice device) } /// - /// 引数のデータフローとロールが割り当てられているデバイスを取得する + /// 引数のが割り当てられているデバイスを取得する /// /// /// @@ -128,7 +145,7 @@ public bool Contains(MMDevice device) } /// - /// CoreAudioAPIから取得したMMDeviceをラッピングし、内蔵コレクションに追加する + /// CoreAudioAPIから取得したMMDeviceをラッピングし、コレクションに追加する /// /// public void Add(MMDevice device) @@ -146,7 +163,7 @@ public void Add(MMDevice device) } /// - /// デバイスの破棄処理を呼び出し、内蔵コレクションから削除する + /// デバイスの破棄処理を呼び出し、コレクションから削除する /// /// public new void Remove(AudioDeviceAccessor ax) @@ -172,7 +189,7 @@ public void Remove(string deviceId) } /// - /// 引数のMMDeviceが持つデバイスIDとDataFlowに一致するものを削除する + /// 引数のが持つデバイスIDとに一致するものを削除する /// /// public void Remove(MMDevice device) @@ -187,7 +204,7 @@ public void Remove(MMDevice device) } /// - /// 内蔵コレクションをクリアし、かつ実行時点で保持していたセッションを破棄する + /// コレクションをクリアし、かつ実行時点で保持していたデバイスをすべて破棄する /// public new void Clear() { @@ -203,9 +220,8 @@ public void Remove(MMDevice device) } /// - /// CoreAudioAPIから使用可能なデバイス一覧を取得し、内蔵コレクションに順次登録していく + /// CoreAudioAPIから使用可能なデバイス一覧を取得し、コレクションに順次登録する。 /// - /// public void CollectAudioEndpoints() { lock (Gate) @@ -213,7 +229,7 @@ public void CollectAudioEndpoints() using var devices = _deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active); if (devices == null) { - throw new ApplicationException("Failed to acquire DeviceCollection."); + throw new InvalidOperationException("通常はありえない"); } foreach (var device in devices) @@ -350,7 +366,7 @@ private void OnDeviceStateChanged(object? sender, DeviceStateChangedEventArgs e) } /// - /// 内蔵コレクションに保持しているいずれかのデバイスのロール情報が変更された際に呼び出される + /// コレクションに保持しているいずれかのデバイスのロール情報が変更された際に呼び出される /// /// /// @@ -360,7 +376,7 @@ private void OnDeviceRoleChanged(object? sender, DeviceAccessorRoleHolderChanged } /// - /// 内蔵コレクションに保持しているデバイスが破棄される際に呼び出される + /// コレクションに保持しているデバイスが破棄される際に呼び出される /// /// /// @@ -373,16 +389,16 @@ private void OnDeviceDisposing(object? sender, EventArgs e) if (Contains(ax)) { - // このクラス内からデバイスを消す際はDispose()を呼んで内蔵コレクションから削除しているが、 - // 外的要因でDispose()が呼び出された際は内蔵コレクションに残ってしまう。 - // 上記のケースに対応できるよう、破棄処理の呼び出し時にも内蔵コレクションからの削除処理を置いておく(既にコレクションから消えててもエラーにならないので) + // このクラス内からデバイスを消す際はDispose()を呼んでコレクションから削除しているが、 + // 外的要因でデバイスのDispose()が呼び出された際は破棄された状態でコレクションに残ってしまう。 + // 上記のケースに対応できるよう、破棄処理の呼び出し時にもコレクションからの削除処理を置いておく。 Remove(ax); } } } /// - /// 内蔵コレクションに保持しているデバイスが破棄された際に呼び出される + /// コレクションに保持しているデバイスが破棄された際に呼び出される /// /// /// diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/DeviceRoleHolder.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceRole.cs similarity index 51% rename from SimpleVolumeMixer/Core/Helper/CoreAudio/DeviceRoleHolder.cs rename to SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceRole.cs index cdc055d..35a11be 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/DeviceRoleHolder.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioDeviceRole.cs @@ -1,25 +1,41 @@ using System; +using CSCore.CoreAudioAPI; using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.Event; using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class DeviceRoleHolder : NotifyPropertyChangedBase +/// +/// が参照するに割り当てられたロールを保持する。 +/// からはロールを直接取得することは出来ず、各デバイスに割り当てられたロールを同時に全て知ることが難しくなっている。 +/// よって、このオブジェクトにが持つロールの情報を集約して管理し、デバイスが持つロールの情報にアクセスしやすくする。 +/// +public class AudioDeviceRole : NotifyPropertyChangedBase { - internal event EventHandler? RoleChanged; + /// + /// ロールの状態が変化した際に発動するイベントハンドラ + /// + public event EventHandler? RoleChanged; private readonly AudioDeviceAccessor _device; private bool _multimedia; private bool _communications; - internal DeviceRoleHolder(AudioDeviceAccessor device) + /// + /// ctor + /// + /// + public AudioDeviceRole(AudioDeviceAccessor device) { _device = device; _multimedia = false; _communications = false; } + /// + /// デバイスがのロールを持っているかを取得・設定する。 + /// public bool Multimedia { get => _multimedia; @@ -34,6 +50,9 @@ public bool Multimedia } } + /// + /// デバイスがのロールを持っているかを取得・設定する。 + /// public bool Communications { get => _communications; diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessor.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessor.cs index 7b429ab..7e6dda2 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessor.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessor.cs @@ -1,9 +1,9 @@ using System; using System.Diagnostics; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; using Reactive.Bindings.Extensions; -using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.Event; using SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; using SimpleVolumeMixer.Core.Helper.CoreAudio.Internal; @@ -11,14 +11,56 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class AudioSessionAccessor : SafetyAccessorComponent +/// +/// と及び周辺インターフェース・関連情報を総括するラッパーオブジェクト。 +/// +/// +/// +/// +/// +/// +public class AudioSessionAccessor : SafetyAccessComponent { + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? DisplayNameChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? IconPathChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? SimpleVolumeChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? ChannelVolumeChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? GroupingParamChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? StateChanged; + + /// + /// セッションが持つ値に変化が生じた際に発生するイベントハンドラ。 + /// CoreAudioAPIから経由で通知があった際に発動する。 + /// public event EventHandler? SessionDisconnected; private readonly ILogger _logger; @@ -28,7 +70,7 @@ public class AudioSessionAccessor : SafetyAccessorComponent private readonly SimpleAudioVolume _audioVolume; private readonly AudioSessionEventAdapter _eventAdapter; - internal AudioSessionAccessor(AudioSessionControl audioSessionControl, ILogger logger) + public AudioSessionAccessor(AudioSessionControl audioSessionControl, ILogger logger) { _logger = logger; _session = audioSessionControl.AddTo(Disposable); @@ -55,11 +97,22 @@ internal AudioSessionAccessor(AudioSessionControl audioSessionControl, ILogger l public bool IsSystemSoundSession => SafeRead(() => _sessionControl2.IsSystemSoundSession, false); public AudioSessionStateType SessionState => SafeRead( - () => AccessorHelper.SessionStates[_session.SessionState], + () => _session.GetStateNative(out var value) >= 0 + ? AccessorHelper.SessionStates[value] + : AudioSessionStateType.Unknown, AudioSessionStateType.Unknown); - public float PeakValue => SafeRead(_meterInformation.GetPeakValue, 0.0f); - public int MeteringChannelCount => SafeRead(_meterInformation.GetMeteringChannelCount, 0); + public float PeakValue => SafeRead( + () => _meterInformation.GetPeakValueNative(out var value) >= 0 + ? value + : 0.0f, + 0.0f); + + public int MeteringChannelCount => SafeRead( + () => _meterInformation.GetMeteringChannelCountNative(out var value) >= 0 + ? value + : 0, + 0); public string? DisplayName { diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessorManager.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessorManager.cs index 83d4e09..57e803f 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessorManager.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionAccessorManager.cs @@ -2,39 +2,55 @@ using System.Linq; using System.Threading.Tasks; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; +using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.Event; using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class AudioSessionAccessorManager : SynchronizedReactiveCollectionWrapper +/// +/// 複数のを管理するためのオブジェクト。 +/// 各の生成・破棄を担うほか、 +/// ユーザ操作由来での機能実行及び、CoreAudioAPIからの通知を受けての機能実行を行う。 +/// +public class AudioSessionAccessorManager : SynchronizedObservableCollectionWrapper { /// - /// いずれかのセッションが破棄される際に呼び出される + /// このオブジェクトに登録されているいずれかのが + /// イベントを発動させた時に連動して発動する。 /// + /// public event EventHandler? SessionDisposing; /// - /// いずれかのセッションが破棄された際に呼び出される + /// このオブジェクトに登録されているいずれかのが + /// イベントを発動させた時に連動して発動する。 /// + /// public event EventHandler? SessionDisposed; private readonly ILogger _logger; private readonly AudioSessionManagerAccessor _sessionManager; + /// + /// ctor + /// + /// + /// public AudioSessionAccessorManager(AudioDeviceAccessor device, ILogger logger) { _logger = logger; - _sessionManager = new AudioSessionManagerAccessor(device, logger); + _sessionManager = new AudioSessionManagerAccessor(device, logger).AddTo(Disposable); _sessionManager.SessionManagerOpened += OnSessionManagerOpened; _sessionManager.SessionManagerClosed += OnSessionManagerClosed; _sessionManager.SessionCreated += OnSessionCreated; } /// - /// 引数のPIDを持つセッションが内蔵コレクションにあるかを確認する + /// 引数のPIDを持つセッションがコレクションにあるかを確認する /// /// /// @@ -70,7 +86,7 @@ public bool Contains(int? procId) } /// - /// CoreAudioAPIから取得したAudioSessionControlをラッピングし、内蔵コレクションに追加する + /// CoreAudioAPIから取得したAudioSessionControlをラッピングし、コレクションに追加する /// /// public void Add(AudioSessionControl session) @@ -99,7 +115,7 @@ public void Add(AudioSessionControl session) } /// - /// セッションの破棄処理を呼び出し、内蔵コレクションから削除する + /// セッションの破棄処理を呼び出し、コレクションから削除する /// /// public new void Remove(AudioSessionAccessor ax) @@ -112,7 +128,7 @@ public void Add(AudioSessionControl session) } /// - /// 引数のPIDを持つセッションを全て削除する。 + /// 引数のPIDを持つセッションを全て破棄し、削除する。 /// /// public void Remove(int? procId) @@ -131,7 +147,7 @@ public void Remove(int? procId) } /// - /// 引数のAudioSessionControlを持つセッションを全て削除する。 + /// 引数のを持つセッションを破棄し、全て削除する。 /// /// public void Remove(AudioSessionControl session) @@ -147,7 +163,7 @@ public void Remove(AudioSessionControl session) } /// - /// 内蔵コレクションをクリアし、かつ実行時点で保持していたセッションを破棄する + /// コレクションをクリアし、かつ実行時点で保持していたセッションを破棄する /// public new void Clear() { @@ -164,7 +180,7 @@ public void Remove(AudioSessionControl session) } /// - /// セッションマネージャを取得し、使用可能な状態にする + /// セッションマネージャをCoreAudioAPIから取得・セットアップし、使用可能な状態にする /// /// public Task OpenSession() @@ -173,7 +189,7 @@ public Task OpenSession() } /// - /// セッションマネージャを破棄し、使用できない状態にする + /// セッションマネージャを破棄し、使用を終了する /// public void CloseSession() { @@ -210,7 +226,7 @@ private void OnSessionManagerOpened(object? sender, EventArgs e) return; } - // セッションマネージャからデバイスに紐づくセッション一覧をすべて抜き出し、内蔵コレクションに追加していく + // セッションマネージャからデバイスに紐づくセッション一覧をすべて抜き出し、コレクションに追加していく foreach (var session in enumerator) { Add(session); @@ -224,7 +240,7 @@ private void OnSessionManagerOpened(object? sender, EventArgs e) /// private void OnSessionManagerClosed(object? sender, EventArgs e) { - // 全セッションの破棄処理を起動したのち内蔵コレクションからも全削除し、セッションを使用できないようにする + // 全セッションの破棄処理を起動したのちコレクションからも全削除し、セッションを使用できないようにする Clear(); } @@ -237,7 +253,7 @@ private void OnSessionDisconnected(object? sender, AudioSessionAccessorDisconnec { if (!Contains(e.Session)) { - // 以降、切断されたセッションを使用しないよう破棄し、内蔵コレクションからも削除する + // 以降、切断されたセッションを使用しないよう破棄し、コレクションからも削除する Remove(e.Session); } } @@ -253,16 +269,18 @@ private void OnStateChanged(object? sender, AudioSessionAccessorStateChangedEven { case AudioSessionStateType.AudioSessionStateActive: case AudioSessionStateType.AudioSessionStateInactive: + // 音の出力を終了するとInactiveになるアプリもあるため(SystemSoundが筆頭)、 + // Inactiveとなっても削除しない break; case AudioSessionStateType.AudioSessionStateExpired: - // 期限切れとなったセッションは使用不可能であるため破棄し、内蔵コレクションからも削除する + // 期限切れとなったセッションは使用不可能であるため破棄し、コレクションからも削除する Remove(e.Session); break; } } /// - /// 内蔵コレクションに保持しているセッションが破棄される際に呼び出される + /// コレクションに保持しているセッションが破棄される際に呼び出される /// /// /// @@ -275,16 +293,16 @@ private void OnSessionDisposing(object? sender, EventArgs e) if (Contains(ax)) { - // このクラス内からセッションを消す際はDispose()を呼んで内蔵コレクションから削除しているが、 - // 外的要因でDispose()が呼び出された際は内蔵コレクションに残ってしまう。 - // 上記のケースに対応できるよう、破棄処理の呼び出し時にも内蔵コレクションからの削除処理を置いておく(既にコレクションから消えててもエラーにならないので) + // このクラス内からセッションを消す際はDispose()を呼んでコレクションから削除しているが、 + // 外的要因でDispose()が呼び出された際は破棄された状態でコレクションに残ってしまう。 + // 上記のケースに対応できるよう、破棄処理の呼び出し時にもコレクションからの削除処理を置いておく。 Remove(ax); } } } /// - /// 内蔵コレクションに保持しているセッションが破棄されたら呼び出される + /// コレクションに保持しているセッションが破棄されたら呼び出される /// /// /// diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionManagerAccessor.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionManagerAccessor.cs index 2b6a59c..ab1f121 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionManagerAccessor.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/AudioSessionManagerAccessor.cs @@ -2,13 +2,13 @@ using System.Threading; using System.Threading.Tasks; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; -using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class AudioSessionManagerAccessor : SafetyAccessorComponent +public class AudioSessionManagerAccessor : DisposableComponent { public event EventHandler? SessionManagerOpened; public event EventHandler? SessionManagerClosed; @@ -35,6 +35,11 @@ public void OnSessionCreated(object? sender, SessionCreatedEventArgs e) public async Task OpenSessionManager() { + // 古いインスタンスがある場合は破棄 + CloseSessionManager(); + + _logger.LogInformation("open sessionManager..."); + // AudioSessionManager2はMTAスレッドでしか取得できないので、別スレッドを起動する(WPFはSTAスレッド) var tcs = new TaskCompletionSource(); var thread = new Thread((args) => @@ -42,7 +47,10 @@ public async Task OpenSessionManager() if (args is MMDevice device) { tcs.SetResult(AudioSessionManager2.FromMMDevice(device)); + return; } + + throw new InvalidOperationException("MMDevice以外が渡されてくるのは実装上あり得ない"); }); thread.SetApartmentState(ApartmentState.MTA); thread.Start(_device.Device); @@ -51,9 +59,6 @@ public async Task OpenSessionManager() lock (_gate) { - // 古いインスタンスがある場合は破棄 - CloseSessionManager(); - // 個別で破棄するのでDisposableには入れない _sessionManager = sessionManager; _eventAdapter = new AudioSessionManagerEventAdapter(sessionManager, _logger); @@ -65,22 +70,31 @@ public async Task OpenSessionManager() public void CloseSessionManager() { + _logger.LogInformation("close sessionManager..."); + + var sessionManager = _sessionManager; + var eventAdapter = _eventAdapter; + lock (_gate) { if (_sessionManager == null || _eventAdapter == null) { - _logger.LogInformation("sessionManger is not allocated."); + _logger.LogInformation("sessionManger is not allocated"); return; } - _sessionManager.Dispose(); _sessionManager = null; _eventAdapter.SessionCreated -= OnSessionCreated; - _eventAdapter.Dispose(); _eventAdapter = null; } + // 破棄処理は排他する必要なく単独で動かしてもいいのでlockの外でやる + sessionManager?.Dispose(); + eventAdapter?.Dispose(); + + _logger.LogInformation("closed sessionManager : {}", sessionManager); + SessionManagerClosed?.Invoke(this, EventArgs.Empty); } @@ -99,6 +113,8 @@ public void CloseSessionManager() protected override void OnDisposing() { + _logger.LogInformation("disposing ..."); + CloseSessionManager(); base.OnDisposing(); } diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/CoreAudioAccessor.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/CoreAudioAccessor.cs index a6bb9dc..e6ad00a 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/CoreAudioAccessor.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/CoreAudioAccessor.cs @@ -1,27 +1,44 @@ using System; +using System.Collections.ObjectModel; +using DisposableComponents; using Microsoft.Extensions.Logging; -using Reactive.Bindings; using Reactive.Bindings.Extensions; -using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +using SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio; -public class CoreAudioAccessor : SafetyAccessorComponent +public class CoreAudioAccessor : DisposableComponent { + /// + /// CoreAudioAPIからデバイスの破棄通知があり、がそれを受けてデバイスの破棄を開始した時に呼び出される。 + /// + /// + /// public event EventHandler? DeviceDisposing { add => _deviceManager.DeviceDisposing += value; remove => _deviceManager.DeviceDisposing -= value; } + /// + /// CoreAudioAPIからデバイスの破棄通知があり、がそれを受けてデバイスの破棄を完了した時に呼び出される。 + /// + /// + /// public event EventHandler? DeviceDisposed { add => _deviceManager.DeviceDisposed += value; remove => _deviceManager.DeviceDisposed -= value; } + /// + /// CoreAudioAPIからデバイスの破棄通知があり、がそれを受けてデバイスのロールを最新に更新した際に呼び出される。 + /// + /// + /// + /// public event EventHandler? DeviceRoleChanged { add => _deviceManager.DeviceRoleChanged += value; @@ -31,6 +48,10 @@ public event EventHandler? DeviceRoleC private readonly ILogger _logger; private readonly AudioDeviceAccessorManager _deviceManager; + /// + /// ctor + /// + /// public CoreAudioAccessor(ILogger logger) { _logger = logger; @@ -38,8 +59,21 @@ public CoreAudioAccessor(ILogger logger) _deviceManager.CollectAudioEndpoints(); } - public ReadOnlyReactiveCollection AudioDevices => _deviceManager.ReadOnlyCollection; + /// + /// 現在使用可能なの一覧を取得する。 + /// 読み取り専用であり、このオブジェクトからデバイスの増減を行うことは出来ない。 + /// デバイスの増減はによりCoreAudioAPIからデバイス一覧を取り直すか、 + /// がCoreAudioAPIからの通知を受け、その結果デバイスが追加されるかに限る。 + /// + public ReadOnlyObservableCollection AudioDevices => _deviceManager.ReadOnlyCollection; + /// + /// 引数のが割り当てられているデバイスを取得する + /// + /// + /// + /// + /// public AudioDeviceAccessor? GetDefaultDevice(DataFlowType dataFlowType, RoleType roleType) { return _deviceManager.GetDefaultDevice(dataFlowType, roleType); diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioDeviceAccessorEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioDeviceAccessorEventArgs.cs index 8d57070..aa42835 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioDeviceAccessorEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioDeviceAccessorEventArgs.cs @@ -2,12 +2,22 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// to be used when an event related to occurs. +/// public class AudioDeviceAccessorEventArgs : EventArgs { + /// + /// ctor + /// + /// where the event occurred public AudioDeviceAccessorEventArgs(AudioDeviceAccessor device) { Device = device; } + /// + /// where the event occurred. + /// public AudioDeviceAccessor Device { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorChannelVolumeChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorChannelVolumeChangedEventArgs.cs index 48035a5..aea7bc1 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorChannelVolumeChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorChannelVolumeChangedEventArgs.cs @@ -1,17 +1,43 @@ -namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +using System; +namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; + +/// +/// to be used when there is a change in the value related to the channel in . +/// public class AudioSessionAccessorChannelVolumeChangedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorChannelVolumeChangedEventArgs(AudioSessionAccessor session, int channelCount, - float[] channelVolumes, int changedChannel) : - base(session) + /// + /// ctor + /// + /// where the event occurred + /// Value after change + /// Value after change + /// Value after change + public AudioSessionAccessorChannelVolumeChangedEventArgs( + AudioSessionAccessor session, + int channelCount, + float[] channelVolumes, + int changedChannel + ) : base(session) { ChannelCount = channelCount; ChannelVolumes = channelVolumes; ChangedChannel = changedChannel; } + /// + /// Value after change + /// public int ChannelCount { get; } + + /// + /// Value after change + /// public float[] ChannelVolumes { get; } + + /// + /// Value after change + /// public int ChangedChannel { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisconnectedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisconnectedEventArgs.cs index 652353a..744ec72 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisconnectedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisconnectedEventArgs.cs @@ -1,15 +1,29 @@ -using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +using System; +using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// used when disconnection notification is received from . +/// public class AudioSessionAccessorDisconnectedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorDisconnectedEventArgs(AudioSessionAccessor session, - AudioSessionDisconnectReasonType disconnectReason) : + /// + /// ctor + /// + /// where the event occurred + /// Reason for disconnection + public AudioSessionAccessorDisconnectedEventArgs( + AudioSessionAccessor session, + AudioSessionDisconnectReasonType disconnectReason + ) : base(session) { DisconnectReason = disconnectReason; } + /// + /// Reason for disconnection + /// public AudioSessionDisconnectReasonType DisconnectReason { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionIconPathChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisplayNameChangedEventArgs.cs similarity index 57% rename from SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionIconPathChangedEventArgs.cs rename to SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisplayNameChangedEventArgs.cs index 0ee58f2..c6477fe 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionIconPathChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorDisplayNameChangedEventArgs.cs @@ -1,9 +1,14 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// +/// public class AudioSessionAccessorDisplayNameChangedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorDisplayNameChangedEventArgs(AudioSessionAccessor session, string newDisplayName) : - base(session) + public AudioSessionAccessorDisplayNameChangedEventArgs( + AudioSessionAccessor session, + string newDisplayName + ) : base(session) { NewDisplayName = newDisplayName; } diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorEventArgs.cs index 188e0f5..2c9c6c2 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorEventArgs.cs @@ -2,12 +2,22 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// to be used when an event related to occurs. +/// public class AudioSessionAccessorEventArgs : EventArgs { + /// + /// ctor + /// + /// where the event occurred public AudioSessionAccessorEventArgs(AudioSessionAccessor session) { Session = session; } + /// + /// where the event occurred. + /// public AudioSessionAccessor Session { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorGroupingParamChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorGroupingParamChangedEventArgs.cs index 5c78eae..b31abb3 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorGroupingParamChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorGroupingParamChangedEventArgs.cs @@ -4,8 +4,10 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; public class AudioSessionAccessorGroupingParamChangedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorGroupingParamChangedEventArgs(AudioSessionAccessor session, Guid newGroupingParam) : - base(session) + public AudioSessionAccessorGroupingParamChangedEventArgs( + AudioSessionAccessor session, + Guid newGroupingParam + ) : base(session) { NewGroupingParam = newGroupingParam; } diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorIconPathChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorIconPathChangedEventArgs.cs new file mode 100644 index 0000000..06bd653 --- /dev/null +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorIconPathChangedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; + +/// +/// used when there is a change in the IconPath of . +/// +public class AudioSessionAccessorIconPathChangedEventArgs : AudioSessionAccessorEventArgs +{ + /// + /// ctor + /// + /// where the event occurred + /// New IconPath + public AudioSessionAccessorIconPathChangedEventArgs(AudioSessionAccessor session, string newIconPath) : + base(session) + { + NewIconPath = newIconPath; + } + + /// + /// New IconPath + /// + public string NewIconPath { get; } +} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorSimpleVolumeChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorSimpleVolumeChangedEventArgs.cs index 4087775..61aaea5 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorSimpleVolumeChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorSimpleVolumeChangedEventArgs.cs @@ -1,15 +1,35 @@ -namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +using System; +namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; + +/// +/// used when volume or mute state is changed. +/// public class AudioSessionAccessorSimpleVolumeChangedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorSimpleVolumeChangedEventArgs(AudioSessionAccessor session, float newVolume, bool isMuted) - : - base(session) + /// + /// ctor + /// + /// where the event occurred + /// New volume + /// New mute state + public AudioSessionAccessorSimpleVolumeChangedEventArgs( + AudioSessionAccessor session, + float newVolume, + bool isMuted + ) : base(session) { NewVolume = newVolume; IsMuted = isMuted; } + /// + /// New volume + /// public float NewVolume { get; } + + /// + /// New mute state + /// public bool IsMuted { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorStateChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorStateChangedEventArgs.cs index 928dbb4..fdf1a38 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorStateChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionAccessorStateChangedEventArgs.cs @@ -1,14 +1,28 @@ -using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +using System; +using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// used when the state of changes. +/// public class AudioSessionAccessorStateChangedEventArgs : AudioSessionAccessorEventArgs { - public AudioSessionAccessorStateChangedEventArgs(AudioSessionAccessor session, AudioSessionStateType stateType) : - base(session) + /// + /// ctor + /// + /// where the event occurred + /// New state + public AudioSessionAccessorStateChangedEventArgs( + AudioSessionAccessor session, + AudioSessionStateType stateType + ) : base(session) { NewState = stateType; } + /// + /// New state + /// public AudioSessionStateType NewState { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionDisplayNameChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionDisplayNameChangedEventArgs.cs deleted file mode 100644 index 3cde0b5..0000000 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionDisplayNameChangedEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; - -public class AudioSessionAccessorIconPathChangedEventArgs : AudioSessionAccessorEventArgs -{ - public AudioSessionAccessorIconPathChangedEventArgs(AudioSessionAccessor session, string newIconPath) : - base(session) - { - NewIconPath = newIconPath; - } - - public string NewIconPath { get; } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionManagerStartedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionManagerStartedEventArgs.cs deleted file mode 100644 index 98cdbe8..0000000 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/AudioSessionManagerStartedEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; - -public class AudioSessionManagerStartedEventArgs : System.EventArgs -{ - internal AudioSessionManagerStartedEventArgs(IEnumerable sessions) - { - Sessions = sessions.ToList(); - } - - public IList Sessions { get; } -} \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/DeviceAccessorRoleHolderChangedEventArgs.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/DeviceAccessorRoleHolderChangedEventArgs.cs index 4f82d64..d372bff 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/DeviceAccessorRoleHolderChangedEventArgs.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Event/DeviceAccessorRoleHolderChangedEventArgs.cs @@ -1,19 +1,44 @@ -using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +using System; +using SimpleVolumeMixer.Core.Helper.CoreAudio.Types; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Event; +/// +/// fired when the state of held by changes. +/// public class DeviceAccessorRoleHolderChangedEventArgs : AudioDeviceAccessorEventArgs { - internal DeviceAccessorRoleHolderChangedEventArgs(AudioDeviceAccessor device, RoleType role, bool oldState, - bool newState) : - base(device) + /// + /// ctor + /// + /// where the event occurred + /// Role in which the change occurred + /// old state + /// new state + public DeviceAccessorRoleHolderChangedEventArgs( + AudioDeviceAccessor device, + RoleType role, + bool oldState, + bool newState + ) : base(device) { Role = role; OldState = oldState; NewState = newState; } + /// + /// Role in which the change occurred + /// public RoleType Role { get; } + + /// + /// old state + /// public bool OldState { get; } + + /// + /// new state + /// public bool NewState { get; } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionEventAdapter.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionEventAdapter.cs index a4ea920..b235546 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionEventAdapter.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionEventAdapter.cs @@ -1,24 +1,69 @@ using System; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; +using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; +/// +/// Notification of session-related events such as volume level changes, display names, session status, etc. +/// Since we do not want to block CoreAudioAPI in the app-side processing, +/// notifications from CoreAudioAPI are stored once in and are notified to the app side asynchronously and sequentially. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/ja-jp/windows/win32/api/audiopolicy/nn-audiopolicy-iaudiosessionevents?redirectedfrom=MSDN +/// public class AudioSessionEventAdapter : DisposableComponent { + /// + /// The OnDisplayNameChanged method notifies the client that the display name for the session has changed. + /// public event EventHandler? DisplayNameChanged; + + /// + /// The OnIconPathChanged method notifies the client that the display icon for the session has changed. + /// public event EventHandler? IconPathChanged; + + /// + /// The OnSimpleVolumeChanged method notifies the client that the volume level or muting state of the audio session has changed. + /// public event EventHandler? SimpleVolumeChanged; + + /// + /// The OnChannelVolumeChanged method notifies the client that the volume level of an audio channel in the session submix has changed. + /// public event EventHandler? ChannelVolumeChanged; + + /// + /// The OnGroupingParamChanged method notifies the client that the grouping parameter for the session has changed. + /// public event EventHandler? GroupingParamChanged; + + /// + /// The OnStateChanged method notifies the client that the stream-activity state of the session has changed. + /// public event EventHandler? StateChanged; + + /// + /// The OnSessionDisconnected method notifies the client that the audio session has been disconnected. + /// public event EventHandler? SessionDisconnected; private readonly AudioSessionControl _session; private readonly ILogger _logger; private readonly QueueProcessor _processor; + /// + /// ctor + /// + /// Object to subscribe to events from CoreAudioAPI + /// Used to log event details from CoreAudioAPI public AudioSessionEventAdapter(AudioSessionControl session, ILogger logger) { _session = session; @@ -30,15 +75,14 @@ public AudioSessionEventAdapter(AudioSessionControl session, ILogger logger) _session.StateChanged += OnStateChanged; _session.SessionDisconnected += OnSessionDisconnected; _logger = logger; - _processor = new QueueProcessor(int.MaxValue); - _processor.StartRequest(); + _processor = new QueueProcessor(nameof(AudioSessionEventAdapter), logger).AddTo(Disposable); } private void OnDisplayNameChanged(object? sender, AudioSessionDisplayNameChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ ctx:{ctx}, newDisplayName:{newDisplayName} ]", + "sender:{Sender}, " + + "args:[ ctx:{Ctx}, newDisplayName:{NewDisplayName} ]", sender, e.EventContext, e.NewDisplayName @@ -50,8 +94,8 @@ private void OnDisplayNameChanged(object? sender, AudioSessionDisplayNameChanged private void OnIconPathChanged(object? sender, AudioSessionIconPathChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ ctx:{ctx}, newIconPath:{newIconPath} ]", + "sender:{Sender}, " + + "args:[ ctx:{Ctx}, newIconPath:{NewIconPath} ]", sender, e.EventContext, e.NewIconPath @@ -63,8 +107,8 @@ private void OnIconPathChanged(object? sender, AudioSessionIconPathChangedEventA private void OnSimpleVolumeChanged(object? sender, AudioSessionSimpleVolumeChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ ctx:{ctx}, newVolume:{newIconPath}, isMuted:{isMuted} ]", + "sender:{Sender}, " + + "args:[ ctx:{Ctx}, newVolume:{NewIconPath}, isMuted:{IsMuted} ]", sender, e.EventContext, e.NewVolume, @@ -77,8 +121,8 @@ private void OnSimpleVolumeChanged(object? sender, AudioSessionSimpleVolumeChang private void OnChannelVolumeChanged(object? sender, AudioSessionChannelVolumeChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ ctx:{ctx}, changedChannel:{changedChannel}, channelCount:{channelCount}, channelVolumes:{channelVolumes} ]", + "sender:{Sender}, " + + "args:[ ctx:{Ctx}, changedChannel:{ChangedChannel}, channelCount:{ChannelCount}, channelVolumes:{ChannelVolumes} ]", sender, e.EventContext, e.ChangedChannel, @@ -92,8 +136,8 @@ private void OnChannelVolumeChanged(object? sender, AudioSessionChannelVolumeCha private void OnGroupingParamChanged(object? sender, AudioSessionGroupingParamChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ ctx:{ctx}, newGroupingParam:{newGroupingParam} ]", + "sender:{Sender}, " + + "args:[ ctx:{Ctx}, newGroupingParam:{NewGroupingParam} ]", sender, e.EventContext, e.NewGroupingParam @@ -105,8 +149,8 @@ private void OnGroupingParamChanged(object? sender, AudioSessionGroupingParamCha private void OnStateChanged(object? sender, AudioSessionStateChangedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ newState:{newState} ]", + "sender:{Sender}, " + + "args:[ newState:{NewState} ]", sender, e.NewState ); @@ -117,8 +161,8 @@ private void OnStateChanged(object? sender, AudioSessionStateChangedEventArgs e) private void OnSessionDisconnected(object? sender, AudioSessionDisconnectedEventArgs e) { _logger.LogDebug( - "sender:{sender}, " + - "args:[ disconnectReason:{disconnectReason} ]", + "sender:{Sender}, " + + "args:[ disconnectReason:{DisconnectReason} ]", sender, e.DisconnectReason ); @@ -128,12 +172,12 @@ private void OnSessionDisconnected(object? sender, AudioSessionDisconnectedEvent private void Push(Action action) { - _processor.Push(QueueProcessorHandle.OfAction(action)); + _processor.Push(QueueProcessorItem.OfAction(action)); } protected override void OnDisposing() { - _logger.LogDebug("disposing..."); + _logger.LogInformation("disposing... {}", _session); _session.DisplayNameChanged -= OnDisplayNameChanged; _session.IconPathChanged -= OnIconPathChanged; @@ -143,8 +187,6 @@ protected override void OnDisposing() _session.StateChanged -= OnStateChanged; _session.SessionDisconnected -= OnSessionDisconnected; - _processor.StopRequest(); - base.OnDisposing(); } } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionManagerEventAdapter.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionManagerEventAdapter.cs index 1237c5b..5f5384d 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionManagerEventAdapter.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/AudioSessionManagerEventAdapter.cs @@ -1,20 +1,51 @@ using System; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; +using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; +/// +/// Notify of audio session creation, system ducking, and system undocking events. +/// Since we do not want to block CoreAudioAPI in the app-side processing, +/// notifications from CoreAudioAPI are stored once in and are notified to the app side asynchronously and sequentially. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// +/// https://docs.microsoft.com/ja-jp/windows/win32/api/audiopolicy/nn-audiopolicy-iaudiosessionnotification?redirectedfrom=MSDN +/// https://docs.microsoft.com/en-us/windows/win32/api/audiopolicy/nn-audiopolicy-iaudiovolumeducknotification +/// public class AudioSessionManagerEventAdapter : DisposableComponent { + /// + /// The OnSessionCreated method notifies the registered processes that the audio session has been created. + /// public event EventHandler? SessionCreated; + + /// + /// The OnVolumeDuckNotification method sends a notification about a pending system ducking event. + /// public event EventHandler? VolumeDuckNotification; + + /// + /// The OnVolumeUnduckNotification method sends a notification about a pending system unducking event. + /// public event EventHandler? VolumeUnDuckNotification; private readonly AudioSessionManager2 _sessionManager; private readonly ILogger _logger; private readonly QueueProcessor _processor; + /// + /// ctor + /// + /// Object to subscribe to events from CoreAudioAPI + /// Used to log event details from CoreAudioAPI public AudioSessionManagerEventAdapter(AudioSessionManager2 sessionManager, ILogger logger) { _sessionManager = sessionManager; @@ -22,8 +53,7 @@ public AudioSessionManagerEventAdapter(AudioSessionManager2 sessionManager, ILog _sessionManager.VolumeDuckNotification += OnVolumeDuckNotification; _sessionManager.VolumeUnDuckNotification += OnVolumeUnDuckNotification; _logger = logger; - _processor = new QueueProcessor(int.MaxValue); - _processor.StartRequest(); + _processor = new QueueProcessor(nameof(AudioSessionManagerEventAdapter), logger).AddTo(Disposable); } private void OnSessionCreated(object? sender, SessionCreatedEventArgs e) @@ -67,17 +97,16 @@ private void OnVolumeUnDuckNotification(object? sender, VolumeDuckNotificationEv private void Push(Action action) { - _processor.Push(QueueProcessorHandle.OfAction(action)); + _processor.Push(QueueProcessorItem.OfAction(action)); } protected override void OnDisposing() { - _logger.LogDebug("disposing..."); + _logger.LogInformation($"disposing... {_sessionManager}"); _sessionManager.SessionCreated -= OnSessionCreated; _sessionManager.VolumeDuckNotification -= OnVolumeDuckNotification; _sessionManager.VolumeUnDuckNotification -= OnVolumeUnDuckNotification; - _processor.StopRequest(); base.OnDisposing(); } diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/NotificationClientEventAdapter.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/NotificationClientEventAdapter.cs index df6f20e..179fd87 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/NotificationClientEventAdapter.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/EventAdapter/NotificationClientEventAdapter.cs @@ -1,22 +1,59 @@ using System; using CSCore.CoreAudioAPI; +using DisposableComponents; using Microsoft.Extensions.Logging; using SimpleVolumeMixer.Core.Helper.Component; namespace SimpleVolumeMixer.Core.Helper.CoreAudio.EventAdapter; +/// +/// Notifications when an audio endpoint device is added or removed, when the state or properties of an endpoint device change, +/// or when there is a change in the default role assigned to an endpoint device. +/// Since we do not want to block CoreAudioAPI in the app-side processing, +/// notifications from CoreAudioAPI are stored once in and are notified to the app side asynchronously and sequentially. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/ja-jp/windows/win32/api/mmdeviceapi/nn-mmdeviceapi-immnotificationclient +/// public class NotificationClientEventAdapter : DisposableComponent { + /// + /// The OnDeviceAdded method indicates that a new audio endpoint device has been added. + /// public event EventHandler? DeviceAdded; + + /// + /// The OnDeviceRemoved method indicates that an audio endpoint device has been removed. + /// public event EventHandler? DeviceRemoved; + + /// + /// The OnPropertyValueChanged method indicates that the value of a property belonging to an audio endpoint device has changed. + /// public event EventHandler? DevicePropertyChanged; + + /// + /// The OnDeviceStateChanged method indicates that the state of an audio endpoint device has changed. + /// public event EventHandler? DeviceStateChanged; + + /// + /// The OnDefaultDeviceChanged method notifies the client that the default audio endpoint device for a particular device role has changed. + /// public event EventHandler? DefaultDeviceChanged; private readonly MMNotificationClient _client; private readonly ILogger _logger; private readonly QueueProcessor _processor; + /// + /// ctor + /// + /// Object to subscribe to events from CoreAudioAPI + /// Used to log event details from CoreAudioAPI public NotificationClientEventAdapter(MMNotificationClient client, ILogger logger) { _client = client; @@ -26,17 +63,16 @@ public NotificationClientEventAdapter(MMNotificationClient client, ILogger logge _client.DeviceStateChanged += OnDeviceStateChanged; _client.DefaultDeviceChanged += OnDefaultDeviceChanged; _logger = logger; - _processor = new QueueProcessor(int.MaxValue); - _processor.StartRequest(); + _processor = new QueueProcessor(nameof(NotificationClientEventAdapter), logger, int.MaxValue); } private void OnDeviceAdded(object? sender, DeviceNotificationEventArgs e) { _logger.LogDebug( "sender:{sender}, " + - "args:[ deviceId:{deviceId} ]", + "args:[ device:{device} ]", sender, - e.DeviceId + GetDeviceName(e) ); Push(() => DeviceAdded?.Invoke(this, e)); @@ -46,9 +82,9 @@ private void OnDeviceRemoved(object? sender, DeviceNotificationEventArgs e) { _logger.LogDebug( "sender:{sender}, " + - "args:[ deviceId:{deviceId} ]", + "args:[ device:{device} ]", sender, - e.DeviceId + GetDeviceName(e) ); Push(() => DeviceRemoved?.Invoke(this, e)); @@ -58,9 +94,9 @@ private void OnDevicePropertyChanged(object? sender, DevicePropertyChangedEventA { _logger.LogDebug( "sender:{sender}, " + - "args:[ deviceId:{deviceId}, propertyKey:{key} ]", + "args:[ device:{device}, propertyKey:{key} ]", sender, - e.DeviceId, + GetDeviceName(e), e.PropertyKey ); @@ -71,9 +107,9 @@ private void OnDeviceStateChanged(object? sender, DeviceStateChangedEventArgs e) { _logger.LogDebug( "sender:{sender}, " + - "args:[ deviceId:{deviceId}, state:{state} ]", + "args:[ device:{device}, state:{state} ]", sender, - e.DeviceId, + GetDeviceName(e), e.DeviceState ); @@ -84,9 +120,9 @@ private void OnDefaultDeviceChanged(object? sender, DefaultDeviceChangedEventArg { _logger.LogDebug( "sender:{sender}, " + - "args:[ deviceId:{deviceId}, dataFlow:{dataFlow}, role:{role} ]", + "args:[ device:{device}, dataFlow:{dataFlow}, role:{role} ]", sender, - e.DeviceId, + GetDeviceName(e), e.DataFlow, e.Role ); @@ -96,12 +132,19 @@ private void OnDefaultDeviceChanged(object? sender, DefaultDeviceChangedEventArg private void Push(Action action) { - _processor.Push(QueueProcessorHandle.OfAction(action)); + _processor.Push(QueueProcessorItem.OfAction(action)); + } + + private string? GetDeviceName(DeviceNotificationEventArgs e) + { + return e.TryGetDevice(out var device) + ? device.FriendlyName + : e.DeviceId; } protected override void OnDisposing() { - _logger.LogDebug("disposing..."); + _logger.LogInformation($"disposing... {_client}"); _client.DeviceAdded -= OnDeviceAdded; _client.DeviceRemoved -= OnDeviceRemoved; diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Internal/AccessorHelper.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Internal/AccessorHelper.cs index 840a1d6..64019f3 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Internal/AccessorHelper.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Internal/AccessorHelper.cs @@ -6,8 +6,15 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Internal; -internal static class AccessorHelper +/// +/// Class that collects helper functions for handling CoreAudioAPI +/// +public static class AccessorHelper { + /// + /// [CoreAudioAPI -> Apps] + /// enum conversion table for DataFlow. + /// public static readonly IReadOnlyDictionary DataFlows = new Dictionary { @@ -16,9 +23,17 @@ internal static class AccessorHelper { DataFlow.All, DataFlowType.All } }; + /// + /// [Apps -> CoreAudioAPI] + /// enum conversion table for DataFlow. + /// public static readonly IReadOnlyDictionary DataFlowsRev = DataFlows.ToDictionary(x => x.Value, x => x.Key); + /// + /// [CoreAudioAPI -> Apps] + /// enum conversion table for DeviceState. + /// public static readonly IReadOnlyDictionary DeviceStates = new Dictionary { @@ -29,9 +44,17 @@ internal static class AccessorHelper { DeviceState.All, DeviceStateType.All } }; + /// + /// [Apps -> CoreAudioAPI] + /// enum conversion table for DeviceState. + /// public static readonly IReadOnlyDictionary DeviceStatesRev = DeviceStates.ToDictionary(x => x.Value, x => x.Key); + /// + /// [CoreAudioAPI -> Apps] + /// enum conversion table for AudioSessionState. + /// public static readonly IReadOnlyDictionary SessionStates = new Dictionary { @@ -40,9 +63,17 @@ internal static class AccessorHelper { AudioSessionState.AudioSessionStateExpired, AudioSessionStateType.AudioSessionStateExpired } }; + /// + /// [Apps -> CoreAudioAPI] + /// enum conversion table for AudioSessionState. + /// public static readonly IReadOnlyDictionary SessionStatesRev = SessionStates.ToDictionary(x => x.Value, x => x.Key); + /// + /// [CoreAudioAPI -> Apps] + /// enum conversion table for Role. + /// public static readonly IReadOnlyDictionary Roles = new Dictionary { @@ -51,9 +82,17 @@ internal static class AccessorHelper { Role.Communications, RoleType.Communications } }; + /// + /// [Apps -> CoreAudioAPI] + /// enum conversion table for Role. + /// public static readonly IReadOnlyDictionary RolesRev = Roles.ToDictionary(x => x.Value, x => x.Key); + /// + /// [CoreAudioAPI -> Apps] + /// enum conversion table for AudioSessionDisconnectReason. + /// public static readonly IReadOnlyDictionary SessionDisconnectReasons = new Dictionary @@ -84,6 +123,10 @@ public static readonly IReadOnlyDictionary + /// [Apps -> CoreAudioAPI] + /// enum conversion table for AudioSessionDisconnectReason. + /// public static readonly IReadOnlyDictionary SessionDisconnectReasonsRev = SessionDisconnectReasons.ToDictionary(x => x.Value, x => x.Key); diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionDisconnectReasonType.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionDisconnectReasonType.cs index ed6d369..3d4394c 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionDisconnectReasonType.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionDisconnectReasonType.cs @@ -2,12 +2,50 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +/// +/// The reason that the audio session was disconnected. The caller sets this parameter to one of the AudioSessionDisconnectReason enumeration values shown in the following table. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/ja-jp/windows/win32/api/audiopolicy/nf-audiopolicy-iaudiosessionevents-onsessiondisconnected?redirectedfrom=MSDN +/// public enum AudioSessionDisconnectReasonType { + /// + /// It is an unknown state that does not belong to any of the states. + /// This is a value provided for the convenience of the application; the Windows API will not be notified of this value. + /// + Unknown = int.MinValue, + + /// + /// The user removed the audio endpoint device. + /// DisconnectReasonDeviceRemoval = AudioSessionDisconnectReason.DisconnectReasonDeviceRemoval, + + /// + /// The Windows audio service has stopped. + /// DisconnectReasonServerShutdown = AudioSessionDisconnectReason.DisconnectReasonServerShutdown, + + /// + /// The stream format changed for the device that the audio session is connected to. + /// DisconnectReasonFormatChanged = AudioSessionDisconnectReason.DisconnectReasonFormatChanged, + + /// + /// The user logged off the Windows Terminal Services (WTS) session that the audio session was running in. + /// DisconnectReasonSessionLogoff = AudioSessionDisconnectReason.DisconnectReasonSessionLogoff, + + /// + /// The WTS session that the audio session was running in was disconnected. + /// DisconnectReasonSessionDisconnected = AudioSessionDisconnectReason.DisconnectReasonSessionDisconnected, + + /// + /// The (shared-mode) audio session was disconnected to make the audio endpoint device available for an exclusive-mode connection. + /// DisconnectReasonExclusiveModeOverride = AudioSessionDisconnectReason.DisconnectReasonExclusiveModeOverride, } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionStateType.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionStateType.cs index 976060d..dd9a063 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionStateType.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/AudioSessionStateType.cs @@ -2,10 +2,35 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +/// +/// The AudioSessionState enumeration defines constants that indicate the current state of an audio session. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/audiosessiontypes/ne-audiosessiontypes-audiosessionstate +/// public enum AudioSessionStateType { + /// + /// It is an unknown state that does not belong to any of the states. + /// This is a value provided for the convenience of the application; the Windows API will not be notified of this value. + /// Unknown = int.MinValue, + + /// + /// The audio session is inactive. (It contains at least one stream, but none of the streams in the session is currently running.) + /// AudioSessionStateInactive = AudioSessionState.AudioSessionStateInactive, + + /// + /// The audio session is active. (At least one of the streams in the session is running.) + /// AudioSessionStateActive = AudioSessionState.AudioSessionStateActive, + + /// + /// The audio session has expired. (It contains no streams.) + /// AudioSessionStateExpired = AudioSessionState.AudioSessionStateExpired } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DataFlowType.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DataFlowType.cs index 4337ee2..9349a66 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DataFlowType.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DataFlowType.cs @@ -2,10 +2,35 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +/// +/// The EDataFlow enumeration defines constants that indicate the direction in which audio data flows between an audio endpoint device and an application. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/mmdeviceapi/ne-mmdeviceapi-edataflow +/// public enum DataFlowType { + /// + /// It is an unknown state that does not belong to any of the states. + /// This is a value provided for the convenience of the application; the Windows API will not be notified of this value. + /// Unknown = int.MinValue, + + /// + /// Audio rendering stream. Audio data flows from the application to the audio endpoint device, which renders the stream. + /// Render = DataFlow.Render, + + /// + /// Audio capture stream. Audio data flows from the audio endpoint device that captures the stream, to the application. + /// Capture = DataFlow.Capture, + + /// + /// Audio rendering or capture stream. Audio data can flow either from the application to the audio endpoint device, or from the audio endpoint device to the application. + /// All = DataFlow.All } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DeviceStateType.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DeviceStateType.cs index c8e6175..8f2a4ce 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DeviceStateType.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/DeviceStateType.cs @@ -2,12 +2,50 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +/// +/// The DEVICE_STATE_XXX constants indicate the current state of an audio endpoint device. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/en-us/windows/win32/coreaudio/device-state-xxx-constants +/// public enum DeviceStateType { + /// + /// It is an unknown state that does not belong to any of the states. + /// This is a value provided for the convenience of the application; the Windows API will not be notified of this value. + /// Unknown = int.MinValue, + + /// + /// The audio endpoint device is active. That is, the audio adapter that connects to the endpoint device is present and enabled. In addition, + /// if the endpoint device plugs into a jack on the adapter, then the endpoint device is plugged in. + /// Active = DeviceState.Active, + + /// + /// The audio endpoint device is disabled. The user has disabled the device in the Windows multimedia control panel, + /// Mmsys.cpl. For more information, see Remarks. + /// Disabled = DeviceState.Disabled, + + /// + /// The audio endpoint device is not present because the audio adapter that connects to the endpoint device has been removed from the system, + /// or the user has disabled the adapter device in Device Manager. + /// NotPresent = DeviceState.NotPresent, + + /// + /// The audio endpoint device is unplugged. The audio adapter that contains the jack for the endpoint device is present and enabled, + /// but the endpoint device is not plugged into the jack. Only a device with jack-presence detection can be in this state. + /// For more information about jack-presence detection, see Audio Endpoint Devices. + /// UnPlugged = DeviceState.UnPlugged, + + /// + /// Includes audio endpoint devices in all states active, disabled, not present, and unplugged. + /// All = DeviceState.All } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/RoleType.cs b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/RoleType.cs index 76546b7..f51da33 100644 --- a/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/RoleType.cs +++ b/SimpleVolumeMixer/Core/Helper/CoreAudio/Types/RoleType.cs @@ -2,10 +2,35 @@ namespace SimpleVolumeMixer.Core.Helper.CoreAudio.Types; +/// +/// The ERole enumeration defines constants that indicate the role that the system has assigned to an audio endpoint device. +/// +/// +/// We use information from MSDN and CSCore functions, so please refer to their documentation as well. +/// The document text for some functions is taken from MSDN. +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/Mmdeviceapi/ne-mmdeviceapi-erole +/// public enum RoleType { + /// + /// It is an unknown state that does not belong to any of the states. + /// This is a value provided for the convenience of the application; the Windows API will not be notified of this value. + /// Unknown = int.MinValue, + + /// + /// Games, system notification sounds, and voice commands. + /// Console = Role.Console, + + /// + /// Music, movies, narration, and live music recording. + /// Multimedia = Role.Multimedia, + + /// + /// Voice communications (talking to another person). + /// Communications = Role.Communications } \ No newline at end of file diff --git a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioDevice.cs b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioDevice.cs index ede6bf0..f6db7f4 100644 --- a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioDevice.cs +++ b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioDevice.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Disposables; using System.Threading.Tasks; +using DisposableComponents; using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; @@ -10,19 +11,24 @@ namespace SimpleVolumeMixer.Core.Models.Domain.CoreAudio; +/// +/// を監視し、値の変更があったら経由で通知及び最新値の配信を行う。 +/// の存在が前提となるため、の仕組みを利用して破棄されたことを検知し、 +/// それに合わせて監視の終了やこのクラスの破棄を行う仕組みも実装する。 +/// public class AudioDevice : DisposableComponent { - private readonly PropertyMonitor _deviceId; - private readonly PropertyMonitor _friendlyName; - private readonly PropertyMonitor _devicePath; - private readonly PropertyMonitor _deviceState; - private readonly PropertyMonitor _dataFlow; - private readonly PropertyMonitor _channelCount; - private readonly PropertyMonitor _peakValue; - private readonly PropertyMonitor _meteringChannelCount; - private readonly PropertyMonitor _masterVolumeLevel; - private readonly PropertyMonitor _masterVolumeLevelScalar; - private readonly PropertyMonitor _isMuted; + private readonly PollingMonitor _deviceId; + private readonly PollingMonitor _friendlyName; + private readonly PollingMonitor _devicePath; + private readonly PollingMonitor _deviceState; + private readonly PollingMonitor _dataFlow; + private readonly PollingMonitor _channelCount; + private readonly PollingMonitor _peakValue; + private readonly PollingMonitor _meteringChannelCount; + private readonly PollingMonitor _masterVolumeLevel; + private readonly PollingMonitor _masterVolumeLevelScalar; + private readonly PollingMonitor _isMuted; internal AudioDevice(AudioDeviceAccessor ax) { @@ -32,57 +38,57 @@ internal AudioDevice(AudioDeviceAccessor ax) .ToReadOnlyReactiveCollection(x => new AudioSession(x)) .AddTo(Disposable); - _deviceId = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _deviceId = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.DeviceId ); - _friendlyName = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _friendlyName = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.FriendlyName ); - _devicePath = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _devicePath = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.DevicePath ); - _deviceState = new PropertyMonitor( - PropertyMonitorIntervalType.Normal, + _deviceState = new PollingMonitor( + PollingMonitorIntervalType.Normal, () => ax.DeviceState ); - _dataFlow = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _dataFlow = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.DataFlow ); - _channelCount = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _channelCount = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.ChannelCount, - comparer: PropertyMonitor.IntComparer + comparer: PollingMonitor.IntComparer ); - _peakValue = new PropertyMonitor( - PropertyMonitorIntervalType.High, + _peakValue = new PollingMonitor( + PollingMonitorIntervalType.High, () => ax.PeakValue, - comparer: PropertyMonitor.FloatComparer + comparer: PollingMonitor.FloatComparer ); - _meteringChannelCount = new PropertyMonitor( - PropertyMonitorIntervalType.Low, + _meteringChannelCount = new PollingMonitor( + PollingMonitorIntervalType.Low, () => ax.MeteringChannelCount ); - _masterVolumeLevel = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _masterVolumeLevel = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.MasterVolumeLevel, (x) => ax.MasterVolumeLevel = x, - PropertyMonitor.FloatComparer + PollingMonitor.FloatComparer ); - _masterVolumeLevelScalar = new PropertyMonitor( - PropertyMonitorIntervalType.Normal, + _masterVolumeLevelScalar = new PollingMonitor( + PollingMonitorIntervalType.Normal, () => ax.MasterVolumeLevelScalar, (x) => ax.MasterVolumeLevelScalar = x, - PropertyMonitor.FloatComparer + PollingMonitor.FloatComparer ); - _isMuted = new PropertyMonitor( - PropertyMonitorIntervalType.Normal, + _isMuted = new PollingMonitor( + PollingMonitorIntervalType.Normal, () => ax.IsMuted, (x) => ax.IsMuted = x, - PropertyMonitor.BoolComparer + PollingMonitor.BoolComparer ); Role = new DeviceRole(this, ax.Role).AddTo(Disposable); diff --git a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioSession.cs b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioSession.cs index 58b1721..bccb3ce 100644 --- a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioSession.cs +++ b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/AudioSession.cs @@ -6,6 +6,7 @@ using System.IO; using System.Windows.Media; using System.Windows.Media.Imaging; +using DisposableComponents; using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; @@ -16,19 +17,24 @@ namespace SimpleVolumeMixer.Core.Models.Domain.CoreAudio; +/// +/// を監視し、値の変更があったら経由で通知及び最新値の配信を行う。 +/// の存在が前提となるため、の仕組みを利用して破棄されたことを検知し、 +/// それに合わせて監視の終了やこのクラスの破棄を行う仕組みも実装する。 +/// public class AudioSession : DisposableComponent { private static readonly HashSet ImageTypeExt = new(new[] { ".png", ".jpg", ".bmp" }); private readonly AudioSessionAccessor _accessor; - private readonly PropertyMonitor _sessionState; - private readonly PropertyMonitor _peakValue; - private readonly PropertyMonitor _meteringChannelCount; - private readonly PropertyMonitor _displayName; - private readonly PropertyMonitor _iconSource; - private readonly PropertyMonitor _groupingParam; - private readonly PropertyMonitor _masterVolume; - private readonly PropertyMonitor _isMuted; + private readonly PollingMonitor _sessionState; + private readonly PollingMonitor _peakValue; + private readonly PollingMonitor _meteringChannelCount; + private readonly PollingMonitor _displayName; + private readonly PollingMonitor _iconSource; + private readonly PollingMonitor _groupingParam; + private readonly PollingMonitor _masterVolume; + private readonly PollingMonitor _isMuted; internal AudioSession(AudioSessionAccessor ax) { @@ -36,43 +42,43 @@ internal AudioSession(AudioSessionAccessor ax) IsSystemSound = _accessor.IsSystemSoundSession; - _sessionState = new PropertyMonitor( - PropertyMonitorIntervalType.Normal, + _sessionState = new PollingMonitor( + PollingMonitorIntervalType.Normal, () => ax.SessionState ); - _peakValue = new PropertyMonitor( - PropertyMonitorIntervalType.High, + _peakValue = new PollingMonitor( + PollingMonitorIntervalType.High, () => ax.PeakValue, - comparer: PropertyMonitor.FloatComparer + comparer: PollingMonitor.FloatComparer ); - _meteringChannelCount = new PropertyMonitor( - PropertyMonitorIntervalType.Low, + _meteringChannelCount = new PollingMonitor( + PollingMonitorIntervalType.Low, () => ax.MeteringChannelCount, - comparer: PropertyMonitor.IntComparer + comparer: PollingMonitor.IntComparer ); - _displayName = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _displayName = new PollingMonitor( + PollingMonitorIntervalType.Manual, ResolveDisplayName ); - _iconSource = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _iconSource = new PollingMonitor( + PollingMonitorIntervalType.Manual, ResolveIcon ); - _groupingParam = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _groupingParam = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.GroupingParam ); - _masterVolume = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _masterVolume = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.MasterVolume, (x) => ax.MasterVolume = x, - PropertyMonitor.FloatComparer + PollingMonitor.FloatComparer ); - _isMuted = new PropertyMonitor( - PropertyMonitorIntervalType.Manual, + _isMuted = new PollingMonitor( + PollingMonitorIntervalType.Manual, () => ax.IsMuted, (x) => ax.IsMuted = x, - PropertyMonitor.BoolComparer + PollingMonitor.BoolComparer ); SessionState = _sessionState.ToReactivePropertySlimAsSynchronized(x => x.Value); @@ -158,6 +164,11 @@ private void OnGroupingParamChanged(object? sender, AudioSessionAccessorGrouping _groupingParam.Refresh(); } + /// + /// CoreAudioAPIからセッションの表示名を取れないことがあるので、 + /// プロセスのタイトルバー → プロセス名 → プロセスの実行ファイル名 とフォールバックして表示名の取得を試みる + /// + /// 取得結果。フォールバックしても取得できなければnull private string? ResolveDisplayName() { if (IsSystemSound) @@ -201,6 +212,11 @@ private void OnGroupingParamChanged(object? sender, AudioSessionAccessorGrouping return null; } + /// + /// CoreAudioAPIからセッションのアイコン取得する機能があるが、実際はほとんど取得できないので + /// セッションの実行ファイルから抽出を試みる。 + /// + /// 取得結果。フォールバックしても取得できなければnull private ImageSource? ResolveIcon() { if (IsSystemSound) @@ -256,7 +272,7 @@ private ImageSource BytesToImageSource(byte[] bytes) protected override void OnDisposing() { - var monitors = new IPropertyMonitor[] + var monitors = new IPollingMonitor[] { _sessionState, _peakValue, diff --git a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/DeviceRole.cs b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/DeviceRole.cs index 3956e0d..8e92fba 100644 --- a/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/DeviceRole.cs +++ b/SimpleVolumeMixer/Core/Models/Domain/CoreAudio/DeviceRole.cs @@ -1,13 +1,17 @@ -using Reactive.Bindings; +using DisposableComponents; +using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Helper.Component; using SimpleVolumeMixer.Core.Helper.CoreAudio; namespace SimpleVolumeMixer.Core.Models.Domain.CoreAudio; +/// +/// を監視し、値の変更があったら経由で通知及び最新値の配信を行う。 +/// public class DeviceRole : DisposableComponent { - public DeviceRole(AudioDevice device, DeviceRoleHolder holder) + public DeviceRole(AudioDevice device, AudioDeviceRole holder) { Device = device; Multimedia = holder.ToReactivePropertySlimAsSynchronized(x => x.Multimedia).AddTo(Disposable); diff --git a/SimpleVolumeMixer/Core/Models/Repository/CoreAudioRepository.cs b/SimpleVolumeMixer/Core/Models/Repository/CoreAudioRepository.cs index 3d8920b..3fb0b6b 100644 --- a/SimpleVolumeMixer/Core/Models/Repository/CoreAudioRepository.cs +++ b/SimpleVolumeMixer/Core/Models/Repository/CoreAudioRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Reactive.Disposables; using Microsoft.Extensions.Logging; using Reactive.Bindings; @@ -26,6 +27,10 @@ public class CoreAudioRepository : ICoreAudioRepository private readonly IReactiveProperty _communicationRoleDevice; private readonly IReactiveProperty _multimediaRoleDevice; + /// + /// ctor + /// + /// public CoreAudioRepository(ILogger logger) { _logger = logger; @@ -46,7 +51,7 @@ public CoreAudioRepository(ILogger logger) .AddTo(_disposable); } - public ReadOnlyReactiveCollection AudioDevices => _accessor.AudioDevices; + public ReadOnlyObservableCollection AudioDevices => _accessor.AudioDevices; public IReadOnlyReactiveProperty CommunicationRoleDevice => _communicationRoleDevice; public IReadOnlyReactiveProperty MultimediaRoleDevice => _multimediaRoleDevice; diff --git a/SimpleVolumeMixer/Core/Services/CoreAudioService.cs b/SimpleVolumeMixer/Core/Services/CoreAudioService.cs index f12ccb7..ded7e73 100644 --- a/SimpleVolumeMixer/Core/Services/CoreAudioService.cs +++ b/SimpleVolumeMixer/Core/Services/CoreAudioService.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Reactive.Disposables; using System.Reactive.Linq; using Reactive.Bindings; @@ -18,23 +16,26 @@ public class CoreAudioService : IDisposable, ICoreAudioService { private readonly CompositeDisposable _disposable; private readonly ICoreAudioRepository _coreAudioRepository; - private readonly KeyValueInstanceManager _instanceManager; public CoreAudioService(ICoreAudioRepository coreAudioRepository) { _disposable = new CompositeDisposable(); - _instanceManager = new KeyValueInstanceManager(x => new AudioDevice(x)); _coreAudioRepository = coreAudioRepository; - + + // AudioDeviceAccessorに対し1-1でAudioDeviceのインスタンスを結びつけたい + var instanceManager = new KeyValueInstanceManager(x => new AudioDevice(x)); + Devices = _coreAudioRepository.AudioDevices - .ToReadOnlyReactiveCollection(x => _instanceManager.Obtain(x)) + .ToReadOnlyReactiveCollection(x => instanceManager.Obtain(x)) .AddTo(_disposable); + + // Repositoryにあわせて、各ロールのデバイスはDevicesにも登録されているものであるようにする CommunicationRoleDevice = _coreAudioRepository.CommunicationRoleDevice - .Select(x => x != null ? _instanceManager.Obtain(x) : null) + .Select(x => x != null ? instanceManager.Obtain(x) : null) .ToReadOnlyReactivePropertySlim() .AddTo(_disposable); MultimediaRoleDevice = _coreAudioRepository.MultimediaRoleDevice - .Select(x => x != null ? _instanceManager.Obtain(x) : null) + .Select(x => x != null ? instanceManager.Obtain(x) : null) .ToReadOnlyReactivePropertySlim() .AddTo(_disposable); } diff --git a/SimpleVolumeMixer/NLog.config b/SimpleVolumeMixer/NLog.config index 9e03c84..88000ef 100644 --- a/SimpleVolumeMixer/NLog.config +++ b/SimpleVolumeMixer/NLog.config @@ -2,7 +2,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + value="${longdate}\t[t${threadid}]\t[${logger}]\t[${callsite:includeNamespace=false}]\t[${level:upperCase=true}]] ${message} ${exception:format=message,stacktrace} ${event-properties:item=ir-objects}" /> @@ -12,21 +12,18 @@ archiveNumbering="Sequence" maxArchiveFiles="5" archiveEvery="Day" - layout="${Layout}"/> + layout="${Layout}" /> - ${longdate} [t${threadid}] [${logger}] [${callsite:includeNamespace=false}] - [${level:upperCase=true}] ${message} ${exception:format=message,stacktrace} - ${event-properties:item=ir-objects} - + ${longdate} [t${threadid}] [${logger}] [${callsite:includeNamespace=false}] [${level:upperCase=true}] ${message} ${exception:format=message,stacktrace} ${event-properties:item=ir-objects} - + \ No newline at end of file diff --git a/SimpleVolumeMixer/SimpleVolumeMixer.csproj b/SimpleVolumeMixer/SimpleVolumeMixer.csproj index 701fd0a..dd17004 100644 --- a/SimpleVolumeMixer/SimpleVolumeMixer.csproj +++ b/SimpleVolumeMixer/SimpleVolumeMixer.csproj @@ -16,29 +16,30 @@ all runtime; compile; build; native; contentfiles; analyzers; buildtransitive - + + all runtime; compile; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + @@ -63,7 +64,7 @@ - + @@ -73,19 +74,19 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/SimpleVolumeMixer/UI/Helpers/Components/DisposableComponentLifetimeManager.cs b/SimpleVolumeMixer/UI/Helpers/Components/DisposableComponentLifetimeManager.cs index 373e256..d9af1e1 100644 --- a/SimpleVolumeMixer/UI/Helpers/Components/DisposableComponentLifetimeManager.cs +++ b/SimpleVolumeMixer/UI/Helpers/Components/DisposableComponentLifetimeManager.cs @@ -1,4 +1,5 @@ using System; +using DisposableComponents; using SimpleVolumeMixer.Core.Helper.Component; using Unity.Lifetime; diff --git a/SimpleVolumeMixer/UI/Models/UseCase/AudioDevicesPageUseCase.cs b/SimpleVolumeMixer/UI/Models/UseCase/AudioDevicesPageUseCase.cs index 8482b3b..39e9cd3 100644 --- a/SimpleVolumeMixer/UI/Models/UseCase/AudioDevicesPageUseCase.cs +++ b/SimpleVolumeMixer/UI/Models/UseCase/AudioDevicesPageUseCase.cs @@ -1,4 +1,5 @@ -using Reactive.Bindings; +using DisposableComponents; +using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Contracts.Services; using SimpleVolumeMixer.Core.Helper.Component; diff --git a/SimpleVolumeMixer/UI/Models/UseCase/AudioSessionsPageUseCase.cs b/SimpleVolumeMixer/UI/Models/UseCase/AudioSessionsPageUseCase.cs index 8acd1c6..124fd03 100644 --- a/SimpleVolumeMixer/UI/Models/UseCase/AudioSessionsPageUseCase.cs +++ b/SimpleVolumeMixer/UI/Models/UseCase/AudioSessionsPageUseCase.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Linq; +using DisposableComponents; using Reactive.Bindings; using Reactive.Bindings.Extensions; using SimpleVolumeMixer.Core.Contracts.Services; @@ -22,6 +23,7 @@ public AudioSessionsPageUseCase(ICoreAudioService coreAudioService) // マルチメディアロールのデバイスを初期表示にしたい. SelectedDevice = coreAudioService.MultimediaRoleDevice + .ObserveOnUIDispatcher() .ToReactiveProperty() .AddTo(Disposable); @@ -29,6 +31,7 @@ public AudioSessionsPageUseCase(ICoreAudioService coreAudioService) // 前回値が出来るまで待ち合わせるので、下記の購読開始処理を実行しただけでは動作しない. SelectedDevice .Zip(SelectedDevice.Skip(1), (x, y) => new { OldValue = x, NewValue = y }) + .ObserveOnUIDispatcher() .Subscribe(x => OnSelectedDeviceChanged(x.OldValue, x.NewValue)) .AddTo(Disposable); diff --git a/SimpleVolumeMixer/UI/Styles/AudioSessionTemplate.xaml b/SimpleVolumeMixer/UI/Styles/AudioSessionTemplate.xaml index 6cf1807..26be6c4 100644 --- a/SimpleVolumeMixer/UI/Styles/AudioSessionTemplate.xaml +++ b/SimpleVolumeMixer/UI/Styles/AudioSessionTemplate.xaml @@ -6,15 +6,18 @@ xmlns:interactivity="http://prismlibrary.com/" xmlns:audio="clr-namespace:SimpleVolumeMixer.UI.ViewModels.Audio"> + + + + @@ -60,6 +64,7 @@ + + + + + + @@ -155,6 +165,7 @@ + + x?.Sessions.ToReadOnlyReactiveCollection(i => new AudioSessionViewModel(i))) + .Select(x => x?.Sessions.ToReadOnlyReactiveCollection(i => new AudioSessionViewModel(i), UIDispatcherScheduler.Default)) .ToReadOnlyReactivePropertySlim() .AddTo(Disposable); } diff --git a/SimpleVolumeMixer/UI/ViewModels/AudioSessionsPageViewModel.cs b/SimpleVolumeMixer/UI/ViewModels/AudioSessionsPageViewModel.cs index 4c875c7..5f2799b 100644 --- a/SimpleVolumeMixer/UI/ViewModels/AudioSessionsPageViewModel.cs +++ b/SimpleVolumeMixer/UI/ViewModels/AudioSessionsPageViewModel.cs @@ -2,6 +2,7 @@ using System.Reactive.Disposables; using System.Windows.Controls; using System.Windows.Input; +using DisposableComponents; using Prism.Commands; using Prism.Regions; using Reactive.Bindings; diff --git a/SimpleVolumeMixer/UI/Views/Controls/AudioSessionsSubHorizontalPage.xaml b/SimpleVolumeMixer/UI/Views/Controls/AudioSessionsSubHorizontalPage.xaml index e2be6ae..33e38e4 100644 --- a/SimpleVolumeMixer/UI/Views/Controls/AudioSessionsSubHorizontalPage.xaml +++ b/SimpleVolumeMixer/UI/Views/Controls/AudioSessionsSubHorizontalPage.xaml @@ -9,6 +9,8 @@ d:DesignHeight="300" d:DesignWidth="300"> + + + +