[MP1-4838] Volume control no longer functions properly when changing audio device (1 Viewer)

Rick164

MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    Will see if I can squeeze some time in between other projects to work on this :)

    The one that @Stéphane Lenclud proposed I'm not familiar with but does look to fit the bill, with the other library (AudioSwitcher) it already allowed for events (detect removal) and pretty sure CScore can do the same.
    Especially the enumeration part is very easy and allows for various filters (default / disconnected etc..) here:

    https://github.com/RickDB/MP1-AudioSwitcher/blob/master/MP1-AudioSwitcher/Class1.cs#L138
    https://github.com/RickDB/MP1-AudioSwitcher/blob/master/MP1-AudioSwitcher/Class1.cs#L288
     

    Virtual

    Portal Member
    January 6, 2017
    22
    12
    46
    Home Country
    Italy Italy
    if it could be useful ... I have noticed that if I play an audio MP3 with MP, pause / stop it, change the default audio device (within windows 10, not via plugin) and after that I replay an audio MP3 (or remove the pause) ... the old audio device is used to play it ...
    If I do the same thing but with a movie file ... when I replay the movie (or remove the pause) ... the NEW audio device is used ... so I think that with a movie file MediaPortal check / recheck the default audio device but with audio MP3 / volume control it doesn't check it again and continue to use the first audio device attached at startup.
     

    Rick164

    MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    So far going pretty well and decided to redo parts of VolumeHandler like @mm1352000 suggested :)

    https://gist.github.com/RickDB/f2e2b7f723ef2c974250334b0415f799

    The CoreAudioController still needs a few new events there but gonna test it some more today, for default device it will do a compare soon and if different will just handle the re-init for us in the background.

    Code:
    [2017-01-08 16:37:42,196] [Error  ] [MPMain   ] [ERROR] - Volume handler - got default audio device DENON-AVRHD-4 (NVIDIA High Definition Audio)
    [2017-01-08 16:38:24,848] [Error  ] [MPMain   ] [ERROR] - Volume handler - setting volume to 0
    [2017-01-08 16:38:24,848] [Error  ] [MPMain   ] [ERROR] - Volume handler - setting volume to 10

    // Update

    Just looked at the Mixer class and seems better suited there (less code needed)

    // Update 2

    Removed AudioEndPoint entirely which results in this which works well during testing, subscribes to 2 events:

    - Audio device changed (i.e. our initial bug report)
    - Volume changed

    Upside is that it's aware of any device / volume changes even outside of Mediaportal.

    Code:
    #region Copyright (C) 2005-2011 Team MediaPortal
    
    // Copyright (C) 2005-2011 Team MediaPortal
    // https://www.team-mediaportal.com
    //
    // MediaPortal is free software: you can redistribute it and/or modify
    // it under the terms of the GNU General Public License as published by
    // the Free Software Foundation, either version 2 of the License, or
    // (at your option) any later version.
    //
    // MediaPortal is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    // GNU General Public License for more details.
    //
    // You should have received a copy of the GNU General Public License
    // along with MediaPortal. If not, see <http://www.gnu.org/licenses/>.
    
    #endregion
    
    using System;
    using System.Runtime.InteropServices;
    using AudioSwitcher.AudioApi;
    using AudioSwitcher.AudioApi.CoreAudio;
    using AudioSwitcher.AudioApi.Observables;
    using MediaPortal.ExtensionMethods;
    using MediaPortal.GUI.Library;
    
    namespace MediaPortal.Mixer
    {
      public sealed class Mixer : IDisposable
      {
        #region Events
    
        public event MixerEventHandler LineChanged;
        public event MixerEventHandler ControlChanged;
     
        #endregion Events
    
        #region Methods
    
        public void Close()
        {
          lock (this)
          {
            if (_handle == IntPtr.Zero)
            {
              return;
            }
    
            MixerNativeMethods.mixerClose(_handle);
    
            _handle = IntPtr.Zero;
          }
        }
    
        public void Dispose()
        {
          if (_mixerControlDetailsVolume != null)
          {
            _mixerControlDetailsVolume.SafeDispose();
          }
    
          if (_mixerControlDetailsMute != null)
          {
            _mixerControlDetailsMute.SafeDispose();
          }
    
          if (_audioDefaultDevice != null)
          {
            _audioDefaultDevice.SafeDispose();
          }
    
          if (_mixerEventListener != null)
          {
            _mixerEventListener.LineChanged -= new MixerEventHandler(OnLineChanged);
            _mixerEventListener.ControlChanged -= new MixerEventHandler(OnControlChanged);
          }
    
          Close();
    
          if (_mixerEventListener != null)
          {
            _mixerEventListener.DestroyHandle();
            _mixerEventListener = null;
          }
        }
    
        public void Open()
        {
          Open(0, false);
        }
    
        public void Open(int mixerIndex, bool isDigital)
        {
          lock (this)
          {
            _waveVolume = isDigital;
            if (isDigital)
            {
              _componentType = MixerComponentType.SourceWave;
            }
            else
            {
              _componentType = MixerComponentType.DestinationSpeakers;
            }
            // not enough to change this..
    
            // Use Endpoint Volume API for Vista/Win7 if master volume is selected and always for Win8 to handle muting of master volume
            if ((OSInfo.OSInfo.VistaOrLater() && _componentType == MixerComponentType.DestinationSpeakers) ||
                OSInfo.OSInfo.Win8OrLater())
            {
              try
              {
                _audioController = new CoreAudioController();
                _audioDefaultDevice = _audioController.GetDefaultDevice(DeviceType.Playback, Role.Multimedia);
                if (_audioDefaultDevice != null)
                {
                  Log.Error($"Mixer audio device: {_audioDefaultDevice.FullName}");
                  Log.Error($"Mixer audio device volume: {_audioDefaultDevice.Volume}");
    
                  // AudioEndpointVolume_OnVolumeNotification
                  _audioDefaultDevice.VolumeChanged.Subscribe(x =>
                  {
                    OnVolumeChange();
                  });
                  _audioController.AudioDeviceChanged.Subscribe(x =>
                  {
                    OnDeviceChange();
                  });
    
    
                  _isMuted = _audioDefaultDevice.IsMuted;
                  _volume = (int) _audioDefaultDevice.Volume;
                  Log.Error($"Mixer audio device volume rounded: {_volume}");
                }
              }
              catch (Exception)
              {
                _isMuted = false;
                _volume = 100;
              }
            }
    
            // Use Windows Multimedia mixer functions for XP and for Vista and later if wave volume is selected
            if (_componentType == MixerComponentType.SourceWave || !OSInfo.OSInfo.VistaOrLater())
            {
              if (_mixerEventListener == null)
              {
                _mixerEventListener = new MixerEventListener();
                _mixerEventListener.Start();
              }
              _mixerEventListener.LineChanged += new MixerEventHandler(OnLineChanged);
              _mixerEventListener.ControlChanged += new MixerEventHandler(OnControlChanged);
    
              MixerNativeMethods.MixerControl mc = new MixerNativeMethods.MixerControl();
    
              mc.Size = 0;
              mc.ControlId = 0;
              mc.ControlType = MixerControlType.Volume;
              mc.fdwControl = 0;
              mc.MultipleItems = 0;
              mc.ShortName = string.Empty;
              mc.Name = string.Empty;
              mc.Minimum = 0;
              mc.Maximum = 0;
              mc.Reserved = 0;
    
              IntPtr handle = IntPtr.Zero;
    
              if (
                MixerNativeMethods.mixerOpen(ref handle, mixerIndex, _mixerEventListener.Handle, 0,
                                             MixerFlags.CallbackWindow) !=
                MixerError.None)
              {
                throw new InvalidOperationException();
              }
    
              _handle = handle;
    
              _mixerControlDetailsVolume = GetControl(_componentType, MixerControlType.Volume);
              _mixerControlDetailsMute = GetControl(_componentType, MixerControlType.Mute);
    
              _isMuted = (int) GetValue(_componentType, MixerControlType.Mute) == 1;
              _volume = (int) GetValue(_componentType, MixerControlType.Volume);
            }
          }
        }
    
        private MixerNativeMethods.MixerControlDetails GetControl(MixerComponentType componentType,
                                                                  MixerControlType controlType)
        {
          try
          {
            MixerNativeMethods.MixerLine mixerLine = new MixerNativeMethods.MixerLine(componentType);
    
            if (MixerNativeMethods.mixerGetLineInfoA(_handle, ref mixerLine, MixerLineFlags.ComponentType) !=
                MixerError.None)
            {
              throw new InvalidOperationException("Mixer.GetControl.1");
            }
    
            using (
              MixerNativeMethods.MixerLineControls mixerLineControls =
                new MixerNativeMethods.MixerLineControls(mixerLine.LineId, controlType))
            {
              if (MixerNativeMethods.mixerGetLineControlsA(_handle, mixerLineControls, MixerLineControlFlags.OneByType) !=
                  MixerError.None)
              {
                throw new InvalidOperationException("Mixer.GetControl.2");
              }
    
              MixerNativeMethods.MixerControl mixerControl =
                (MixerNativeMethods.MixerControl)
                  Marshal.PtrToStructure(mixerLineControls.Data, typeof (MixerNativeMethods.MixerControl));
    
              return new MixerNativeMethods.MixerControlDetails(mixerControl.ControlId);
            }
          }
          catch (Exception)
          {
            // Catch exception when audio device is disconnected
          }
          return null;
        }
    
        private object GetValue(MixerComponentType componentType, MixerControlType controlType)
        {
          try
          {
            MixerNativeMethods.MixerLine mixerLine = new MixerNativeMethods.MixerLine(componentType);
    
            if (MixerNativeMethods.mixerGetLineInfoA(_handle, ref mixerLine, MixerLineFlags.ComponentType) !=
                MixerError.None)
            {
              throw new InvalidOperationException("Mixer.OpenControl.1");
            }
    
            using (
              MixerNativeMethods.MixerLineControls mixerLineControls =
                new MixerNativeMethods.MixerLineControls(mixerLine.LineId, controlType))
            {
              MixerNativeMethods.mixerGetLineControlsA(_handle, mixerLineControls, MixerLineControlFlags.OneByType);
              MixerNativeMethods.MixerControl mixerControl =
                (MixerNativeMethods.MixerControl)
                  Marshal.PtrToStructure(mixerLineControls.Data, typeof(MixerNativeMethods.MixerControl));
    
              using (
                MixerNativeMethods.MixerControlDetails mixerControlDetails =
                  new MixerNativeMethods.MixerControlDetails(mixerControl.ControlId))
              {
                MixerNativeMethods.mixerGetControlDetailsA(_handle, mixerControlDetails, 0);
    
                return Marshal.ReadInt32(mixerControlDetails.Data);
              }
            }
          }
          catch (Exception)
          {
            // Catch exception when audio device is disconnected
          }
          // Set Volume to 30000 when audio recover
          return 30000;
        }
    
        private void SetValue(MixerNativeMethods.MixerControlDetails control, bool value)
        {
          if (control == null)
          {
            return;
          }
    
          Marshal.WriteInt32(control.Data, value ? 1 : 0);
          MixerNativeMethods.mixerSetControlDetails(_handle, control, 0);
        }
    
        private void SetValue(MixerNativeMethods.MixerControlDetails control, int value)
        {
          if (control == null)
          {
            return;
          }
    
          Marshal.WriteInt32(control.Data, value);
          MixerNativeMethods.mixerSetControlDetails(_handle, control, 0);
        }
    
        private void OnLineChanged(object sender, MixerEventArgs e)
        {
          if (LineChanged != null)
          {
            LineChanged(sender, e);
          }
        }
    
        private void OnControlChanged(object sender, MixerEventArgs e)
        {
          bool wasMuted = _isMuted;
          int lastVolume = _volume;
          _isMuted = (int)GetValue(_componentType, MixerControlType.Mute) == 1;
          _volume = (int)GetValue(_componentType, MixerControlType.Volume);
    
          if (ControlChanged != null && (wasMuted != _isMuted || lastVolume != _volume))
          {
            ControlChanged(sender, e);
          }
        }
    
        void OnVolumeChange()
        {
          bool wasMuted = _isMuted;
          int lastVolume = _volume;
          _isMuted = _audioDefaultDevice.IsMuted;
          if (_waveVolume && OSInfo.OSInfo.Win8OrLater())
          {
            _isMutedVolume = (int) GetValue(_componentType, MixerControlType.Mute) == 1;
          }
          _volume = (int)Math.Round(_audioDefaultDevice.Volume * VolumeMaximum);
    
          if (ControlChanged != null && (wasMuted != _isMuted || lastVolume != _volume))
          {
            ControlChanged(null, null);
            if (_waveVolume && OSInfo.OSInfo.Win8OrLater() && (_isMutedVolume != IsMuted))
            {
              SetValue(_mixerControlDetailsMute, _isMuted);
            }
          }
        }
    
        void OnDeviceChange()
        {
          _audioDefaultDevice = _audioController.GetDefaultDevice(DeviceType.Playback, Role.Multimedia);
        }
    
        #endregion Methods
    
        #region Properties
    
        public bool IsMuted
        {
          get { lock (this) return _isMuted; }
          set
          {
            lock (this)
            {
              if (OSInfo.OSInfo.VistaOrLater() && (_componentType == MixerComponentType.DestinationSpeakers))
              {
                if (_audioDefaultDevice != null)
                {
                  _audioDefaultDevice.Mute(value);
                }
              }
              else
              {
                //SetValue(_mixerControlDetailsMute, _isMuted = value);
                SetValue(_mixerControlDetailsMute, value);
                if (_waveVolume && OSInfo.OSInfo.Win8OrLater())
                {
                  if (_audioDefaultDevice != null)
                  {
                    _audioDefaultDevice.Mute(value);
                  }
                }
              }
            }
          }
        }
    
    
        public int Volume
        {
          get { lock (this) return _volume; }
          set
          {
            lock (this)
            {
              if (OSInfo.OSInfo.VistaOrLater() && (_componentType == MixerComponentType.DestinationSpeakers))
              {
                if (_audioDefaultDevice != null)
                {
                  _audioDefaultDevice.Volume = (float) ((float) (value)/(float) (this.VolumeMaximum));
                }
              }
              else
              {
                //SetValue(_mixerControlDetailsVolume, _volume = Math.Max(this.VolumeMinimum, Math.Min(this.VolumeMaximum, value)));
                SetValue(_mixerControlDetailsVolume, Math.Max(this.VolumeMinimum, Math.Min(this.VolumeMaximum, value)));
                if (_waveVolume && OSInfo.OSInfo.Win8OrLater())
                {
                  if (_audioDefaultDevice != null)
                  {
                    _audioDefaultDevice.Volume = (float) ((float) (value)/(float) (this.VolumeMaximum));
                  }
                }
              }
            }
          }
        }
    
        public int VolumeMaximum
        {
          get { return 65535; }
        }
    
        public int VolumeMinimum
        {
          get { return 0; }
        }
    
        #endregion Properties
    
        #region Fields
    
        private MixerComponentType _componentType = MixerComponentType.DestinationSpeakers;
        private IntPtr _handle;
        private bool _isMuted;
        private bool _isMutedVolume;
        private static MixerEventListener _mixerEventListener;
        private int _volume;
        private MixerNativeMethods.MixerControlDetails _mixerControlDetailsVolume;
        private MixerNativeMethods.MixerControlDetails _mixerControlDetailsMute;
        private CoreAudioController _audioController;
        private CoreAudioDevice _audioDefaultDevice;
        private bool _waveVolume;
    
        #endregion Fields
      }
    }
     
    Last edited:

    Rick164

    MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    First draft finished and what works:

    - Re-attaches itself to device if it changes be it internally or externally
    - Re-created VolumeHandler so OSD remains working
    - Volume control on current default device

    The current volume control is 0-65535 however CoreAudio wants it in 0-100, updated the VolumeStyles but even small changes raise it to 100 so think we have some 65535 reference hard coded which is causing it to round to 100 but can't find it yet.
    Any ideas on where I need to change this?

    Branch is located here which writes CoreAudio messages to error log for now (easier to find):

    https://github.com/RickDB/MediaPortal-1/commits/DEV-AudioImprovements

    In some plugins we also have the hard coded volume values which go above so not sure if we should just convert 0-100 to the current standard (0-65535) to make sure nothing breaks.

    Fixed that (math is hard :p ), @Virtual this means we already solved the bug with this however needs more testing and would like to expand on it some more :)

    Other scenario that I would like to handle is:

    - User is playing audio / video and in config has setup non-default directsound device.
    - Volume control will change on default direct sound device with new approach.

    So what we need to know when something is Playing which Audio Device is in use, this can be name / ID but has to come directly out of Mediaportal.
    Then on Volume change see if different than default and change attached device for that which is easy.

    Using this branch for changes:

    https://github.com/RickDB/MediaPortal-1/commits/DEV-AudioImprovements
     
    Last edited:

    Rick164

    MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    Pushed some more changes and is pretty complete and no bugs (so far) with both internal or external volume control, added some step translation as well for external volume control.

    Find an alternative way to enumerate audio devices at startup that doesn't require DirectShow (ie. replace -->this<--)

    Changed to better method here::)

    https://github.com/RickDB/MediaPortal-1/commit/9fe02668ffea798a62b5a94ac0cb602e36abda43

    When an audio device is removed, detect whether that device was being used as the audio renderer for the playing media. Only stop playback if the device is being used. It might be possible to detect this better with a completely different method such as with DirectShow graph events (eg. this one).

    Easy to do with the current library and its DeviceChanged event I think:

    https://github.com/RickDB/MediaPort...ovements/mediaportal/Core/Mixer/Mixer.cs#L130

    any idea where we check for device removal at the moment?

    Another question:

    - Which device do we tie up to volume OSD control?

    Currently I use the default audio renderer (Windows default playback) as in Mediaportal configuration there's no way of selecting which one you want to use for that, I would assume the one used in Video area however they might have a different one for Audio.
    So we could somehow read that during Player.IsPlaying() (can't find how though) or keep as-is.

    First time around Mediaportal code this deep so please let me know if it's alright :)
    Made it so that we can swap it out for the Audio library @Stéphane Lenclud suggested but first wanted to work with something I know well enough.
     
    Last edited:

    Sebastiii

    Development Group
  • Team MediaPortal
  • November 12, 2007
    16,583
    10,403
    France
    Home Country
    France France
    If i'm not wrong the remove/add detection device is on MediaPortal.cs code :)

    Yeah around line 2203 :p

    C#:
    // special chanding for audio renderer
            if (deviceInterface.dbcc_classguid == KSCATEGORY_RENDER || deviceInterface.dbcc_classguid == RDP_REMOTE_AUDIO || deviceInterface.dbcc_classguid == KSCATEGORY_AUDIO)
            {
              switch (msg.WParam.ToInt32())
              {
                case DBT_DEVICEREMOVECOMPLETE:
                  Log.Info("Main: Audio Renderer {0} removed", deviceName);
                  try
                  {
                    GUIGraphicsContext.DeviceAudioConnected--;
                    if (_stopOnLostAudioRenderer)
                    {
                      Log.Debug("Main: Stop playback");
                      g_Player.Stop();
                      while (GUIGraphicsContext.IsPlaying)
                      {
                        Thread.Sleep(100);
                      }
                    }
                  }
                  catch (Exception exception)
                  {
                    Log.Warn("Main: Exception on removal Audio Renderer {0} exception: {1} ",deviceName, exception.Message);
                  }
                  break;
    
                case DBT_DEVICEARRIVAL:
                  Log.Info("Main: Audio Renderer {0} connected", deviceName);
                  try
                  {
                    GUIGraphicsContext.DeviceAudioConnected++;
                    if (_stopOnLostAudioRenderer)
                    {
                      Log.Debug("Main: Stop playback");
                      g_Player.Stop();
                      while (GUIGraphicsContext.IsPlaying)
                      {
                        Thread.Sleep(100);
                      }
                    }
                    // Asynchronously pre-initialize the music engine if we're using the BassMusicPlayer
                    if (BassMusicPlayer.IsDefaultMusicPlayer)
                    {
                      BassMusicPlayer.FreeBass();
                      BassMusicPlayer.CreatePlayerAsync();
                    }
                  }
                  catch (Exception exception)
                  {
                    Log.Warn("Main: Exception on arrival Audio Renderer {0} exception: {1} ", deviceName, exception.Message);
                  }
                  break;
              }
            }
          }
     

    Rick164

    MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    Thanks, removed a bunch more legacy code and probably need to eventually add back WAV device support but that seems only needed for older OS (below Vista).
    Althought with both master and wav it works here and they do list it will work universally, needs some testing under Windows XP / 7 for sure.

    So no more marchals and line detections which need a few hops per volume event just plain Win32 wrapper courtesy of AudioSwitcher :)
    Now just need to find a way of reading out the audio renderer during playback which is gonna be harder.
     
    Last edited:

    Sebastiii

    Development Group
  • Team MediaPortal
  • November 12, 2007
    16,583
    10,403
    France
    Home Country
    France France
    Great Great then :)
    That a productive day :p
     

    Rick164

    MP Donator
  • Premium Supporter
  • January 7, 2006
    1,335
    1,005
    Home Country
    Netherlands Netherlands
    Yeah, volume control is now also silky smooth as it's just one hop from UI event and asynchronous :D
    Pushed everything to branch and with any luck finish up somewhere next week depending on workload.
     

    Users who are viewing this thread

    Top Bottom