David Patterson
David Patterson

Reputation: 21

CoreAudio in vb.net 6.0 Windows 11 : Is there a way to do a ControlChangeNotify callback?

I am writing a volume control app in vb.net 6.0, using a reference to CoreAudioApi.dll. I can:

However, despite a lot of research, I can not setup a callback for ControlChangeNotify. It appears to be simple in C, however I am writing in VB. Can any one suggest a solution?

Update 18/12/2022 @Jimi supplied some very useful definitions and I have edited the post to show the current code attempts. I can get the master default volume using masterVol = GetMasterVolumeObject() as an IAudioEndpointVolume.

Update 22/12/2022 Following a re-read of many postings it became apparent that it was important to create a persistent reference to mastervol and the class reference (MyCallBack) to the class module implementing IAudioEndpointVolumeCallback.

Update 24/12/2022 The AudioCallback class is now firing when volume/mute are changed internally and externally. Transferring the callback data to the main form is problematic. I now have a working system which involves:

  1. Declaring public events in the call back class for each type of control I want to change in the main form.
  2. Initiating a BackgroundWorker to Process the data 10mS after being called by the callback class.
  3. Calculating new audio values from the pNotifyData pointer.
  4. RaiseEvent for each slider/checkbox and label in the main form that needs updating.
  5. AddHandler in the main form to process the callback events.
  6. Create handler routines with delegates for each control type.

From my point of view this topic is solved.

My thanks to @Jimi for his contribution.

Latest Updates:

Option Explicit On
Imports System.ComponentModel
Imports System.Runtime.InteropServices

Public Class AudioCallback

    Implements IAudioEndpointVolumeCallback

    Public Event LabelReady(sender As AudioCallback, ByVal T As String)
    Public Event SliderReady(sender As AudioCallback, EP As String, ByVal T As Integer)
    Public Event CheckReady(sender As AudioCallback, EP As String, ByVal T As Boolean)

    Public Function OnNotify(pNotifyData As IntPtr) As Integer Implements IAudioEndpointVolumeCallback.OnNotify
        ' Move to global structure
        Gstructure = Marshal.PtrToStructure(pNotifyData, GetType(AUDIO_VOLUME_NOTIFICATION_DATA))

        BackGround()                                            ' Asynchronous call and delagate controls
        Return 0
    End Function

    Private Sub BackGround()

        Dim bgw = New BackgroundWorker()
        AddHandler bgw.DoWork,
            Sub()
                System.Threading.Thread.Sleep(10)
            End Sub

        AddHandler bgw.RunWorkerCompleted,
            Sub()
                Update()
            End Sub

        bgw.RunWorkerAsync()
    End Sub

    Private Sub Update()

        Dim svolL As Integer
        Dim svolR As Integer
        Dim balance As Integer

        'ChkMaster.Checked = Gstructure.bMuted
        RaiseEvent CheckReady(Me, "R", Gstructure.bMuted)

        svolL = 0.5 + 100 * Gstructure.Left
        svolR = 0.5 + 100 * Gstructure.Right
        If svolL = svolR Then
            balance = 0
        Else
            If svolR > svolL Then
                balance = 100 - svolL * 100.0 / svolR
            Else
                balance = -(100 - svolR * 100.0 / svolL)
            End If
        End If

        RaiseEvent SliderReady(Me, "RV", 0.5 + 100 * Gstructure.fMasterVolume)  ' HMaster.Value = 0.5 + 100 * Gstructure.fMasterVolume
        RaiseEvent SliderReady(Me, "RB", balance)                               ' HBalance.Value = balance

        'LGuid.Text = Gstructure.guidEventContext.ToString
        RaiseEvent LabelReady(Me, Gstructure.guidEventContext.ToString)
    End Sub

End Class


' Main form Extracts ============================================

Imports CoreAudioApi

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Show()

        ' events for handling AudioCallback events
        AddHandler MyCallBack.LabelReady, AddressOf LReady
        AddHandler MyCallBack.SliderReady, AddressOf HReady
        AddHandler MyCallBack.CheckReady, AddressOf CReady

End Sub

' Label Guid displays Callback GUID
' CheckBox ChkMaster displays Mute status
' Horizontal scrollbar HMaster displays default render volume
' Horizontal scrollbar HBalance displays default render balance
' The global variable IsSettingMaster prevents code associated
' with altering the value of the sliders from re-setting
' the volume / balance.

Private Sub LReady(sender As AudioCallback, Data As String)                                                 ' Set GUID label for Render
        UpdateLabel(LGuid, Data)
    End Sub

    Private Delegate Sub UpdateLabelDelegate(TB As Label, param As String)
    Private Sub UpdateLabel(TB As Label, param As String)
        If TB.InvokeRequired Then
            TB.Invoke(New UpdateLabelDelegate(AddressOf UpdateLabel), New Object() {TB, param})
        Else
            TB.Text = param
        End If
    End Sub


    Private Sub CReady(sender As AudioCallback, EP As String, Data As Boolean)                                  ' Set Master (Render) Mute checkbox
        If EP = "R" Then UpdateChkbox(ChkMaster, Data)
    End Sub
    
    Private Delegate Sub UpdateChkboxDelegate(TB As CheckBox, param As Boolean)
    Private Sub UpdateChkbox(TB As CheckBox, param As Boolean)
        If TB.InvokeRequired Then
            TB.Invoke(New UpdateChkboxDelegate(AddressOf UpdateChkbox), New Object() {TB, param})
        Else
            TB.Checked = param
        End If
    End Sub


    Private Sub HReady(sender As AudioCallback, EP As String, Data As Integer)                                  ' Set Master (Render) Volume / Master Balance slide
        IsSettingMaster = True
        If EP = "RV" Then UpdateHslide(HMaster, Data)
        If EP = "RB" Then UpdateHslide(HBalance, Data)
        IsSettingMaster = False
    End Sub

    Private Delegate Sub UpdateHslideDelegate(TB As HScrollBar, param As Integer)
    Private Sub UpdateHslide(TB As HScrollBar, param As Integer)
        If TB.InvokeRequired Then
            TB.Invoke(New UpdateHslideDelegate(AddressOf UpdateHslide), New Object() {TB, param})
        Else
            TB.Value = param
        End If
    End Sub


History:


' ===============================
' Public Class containing Callback:

Option Explicit On
Imports System.Runtime.InteropServices

Public Class AudioCallback

    Implements IAudioEndpointVolumeCallback
    Public Function OnNotify(pNotifyData As IntPtr) As Integer Implements IAudioEndpointVolumeCallback.OnNotify
        ' Move to global structure
        Gstructure = Marshal.PtrToStructure(pNotifyData, GetType(AUDIO_VOLUME_NOTIFICATION_DATA))

        ' Need mechanism to process new data in main form
        ' Must be asynchronous
        ' Raise event did not work
        ' Enabling Timer did not work

        HaveChange = True                                       ' Flag global variable

        Return 0
    End Function

End Class


' ===============================
' Public Class to Setup Callback:

Option Explicit On

Imports System.Runtime.InteropServices

' Implements IMMDevice, IMMDeviceEnumerator, IAudioEndpointVolume
' developed from https://exchangetuts.com/how-to-check-if-the-system-audio-is-muted-1641156904496320

Public Class CoreAudio


    ' Public definition in ModCoreaudioAlt  https://stackoverflow.com/questions/52001368/how-to-check-if-the-system-audio-is-muted/52013031#52013031
    '   Public Interface IAudioEndpointVolumeCallback
    '   Public Structure AUDIO_VOLUME_NOTIFICATION_DATA
    '   Public Interface IMMDevice
    '   Public Interface IMMDeviceEnumerator
    ' End Public definition  in ModCoreaudioAlt ==============================

    Dim CLSID_MMDeviceEnumerator As Guid = New Guid("{BCDE0395-E52F-467C-8E3D-C4579291692E}")
    Dim MMDeviceEnumeratorType As Type = Type.GetTypeFromCLSID(CLSID_MMDeviceEnumerator, True)

    Private hr As Integer

    Friend Enum EDataFlow
        eRender
        eCapture
        eAll
        EDataFlow_enum_count
    End Enum
    Friend Enum ERole
        eConsole
        eMultimedia
        eCommunications
        ERole_enum_count
    End Enum

    <Flags>
    Friend Enum CLSCTX As UInteger
        CLSCTX_INPROC_SERVER = &H1                              ' In CLSCTX_ALL
        CLSCTX_INPROC_HANDLER = &H2                             ' In CLSCTX_ALL
        CLSCTX_LOCAL_SERVER = &H4                               ' In CLSCTX_ALL
        CLSCTX_INPROC_SERVER16 = &H8
        CLSCTX_REMOTE_SERVER = &H10                             ' In CLSCTX_ALL
        CLSCTX_INPROC_HANDLER16 = &H20
        CLSCTX_RESERVED1 = &H40
        CLSCTX_RESERVED2 = &H80
        CLSCTX_RESERVED3 = &H100
        CLSCTX_RESERVED4 = &H200
        CLSCTX_NO_CODE_DOWNLOAD = &H400
        CLSCTX_RESERVED5 = &H800
        CLSCTX_NO_CUSTOM_MARSHAL = &H1000
        CLSCTX_ENABLE_CODE_DOWNLOAD = &H2000
        CLSCTX_NO_FAILURE_LOG = &H4000
        CLSCTX_DISABLE_AAA = &H8000
        CLSCTX_ENABLE_AAA = &H10000
        CLSCTX_FROM_DEFAULT_CONTEXT = &H20000
        CLSCTX_ACTIVATE_32_BIT_SERVER = &H40000
        CLSCTX_ACTIVATE_64_BIT_SERVER = &H80000
        CLSCTX_INPROC = CLSCTX_INPROC_SERVER Or CLSCTX_INPROC_HANDLER

        CLSCTX_SERVER = CLSCTX_INPROC_SERVER Or CLSCTX_LOCAL_SERVER Or CLSCTX_REMOTE_SERVER ' In CLSCTX_ALL
        CLSCTX_ALL = CLSCTX_SERVER Or CLSCTX_INPROC_HANDLER
    End Enum

    Friend Function GetMasterVolumeObject() As IAudioEndpointVolume
        ' Get the default IAudioEndpintVolume as "ppEndpoint" for eRender & eMultimedia

        Dim deviceEnumerator As IMMDeviceEnumerator = Nothing
        Dim MediaDevice As IMMDevice = Nothing
        Dim ppEndpoint As IAudioEndpointVolume = Nothing
        Dim EndPointVolID As Guid = GetType(IAudioEndpointVolume).GUID

        Try
            Dim MMDeviceEnumerator As Object = Activator.CreateInstance(MMDeviceEnumeratorType)
            deviceEnumerator = CType(MMDeviceEnumerator, IMMDeviceEnumerator)
            deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, MediaDevice)
            MediaDevice.Activate(EndPointVolID, CLSCTX.CLSCTX_ALL, IntPtr.Zero, ppEndpoint)
        Catch ex As Exception
            Form1.Showme("Error in GetMasterVolumeObject: " & ex.Message, Color.Red)
            ppEndpoint = Nothing
        Finally
            If Not IsNothing(deviceEnumerator) Then Marshal.ReleaseComObject(deviceEnumerator)
            If Not IsNothing(MediaDevice) Then Marshal.ReleaseComObject(MediaDevice)
        End Try

        Return ppEndpoint
    End Function

Public Sub Callback()

        Try
            masterVol = GetMasterVolumeObject()

            ' MyCallBack defined in Module ModCoreAudioAlt : Public MyCallBack As New AudioCallback

            If IsNothing(MyCallBack) Then
                Form1.Showme("Failed to set MyCallBack", Color.Red)
            Else

                hr = masterVol.RegisterControlChangeNotify(MyCallBack)

                If hr <> 0 Then
                    Form1.Showme("Callback register failed", Color.Red)
                    If Not IsNothing(masterVol) Then Marshal.ReleaseComObject(masterVol)
                Else
                    Form1.Showme("Callback register OK", Color.Blue)
                    CallBackOn = True
                End If
            End If

        Catch ex As Exception
            Form1.Showme("CallBack error " & ex.Message, Color.Red)
            If Not IsNothing(masterVol) Then Marshal.ReleaseComObject(masterVol)
        End Try

    End Sub

    Public Sub Cancelcallback()
        If CallBackOn = False Then Exit Sub

        hr = masterVol.UnregisterControlChangeNotify(MyCallBack)
        If hr <> 0 Then
            MsgBox("Callback Failed to UnRegister", vbOK, "Core Audio Callback")
        Else
            If Not IsNothing(masterVol) Then Marshal.ReleaseComObject(masterVol)
            Form1.Showme("Callback Un-register OK", Color.Blue)
            CallBackOn = False
        End If

    End Sub

End Class

' ==============
' Public Module:

Imports System.Runtime.InteropServices
Imports CoreAudioApi

Module ModCoreAudioAlt

    ' https://stackoverflow.com/questions/74833398/coreaudio-in-vb-net-6-0-windows-11-Is-there-a-way-to-do-a-controlchangenotify

    Public CallBackOn As Boolean = False                    ' Callback is on Flag

    Public masterVol As IAudioEndpointVolume = Nothing
    Public MyCallBack As New AudioCallback

    Public Gstructure As New AUDIO_VOLUME_NOTIFICATION_DATA ' Callback data
    Public HaveChange As Boolean = False                    ' Callback has fired

    <ComImport>
    <Guid("657804FA-D6AD-4496-8A60-352752AF4F89")>
    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IAudioEndpointVolumeCallback
        <PreserveSig()>
        Function OnNotify(pNotifyData As IntPtr) As Integer

    End Interface


    <StructLayout(LayoutKind.Sequential)>
    Public Structure AUDIO_VOLUME_NOTIFICATION_DATA
        Public guidEventContext As Guid
        Public bMuted As Boolean
        Public fMasterVolume As Single
        Public nChannels As UInteger
        Public Left As Single                                   ' .net will not allow pre-dimensioned array (aVolumes(1) as single)
        Public Right As Single
    End Structure


    ' https://gist.github.com/sverrirs/d099b34b7f72bb4fb386
    <ComImport>
    <Guid("5CDF2C82-841E-4546-9722-0CF74078229A")>
    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IAudioEndpointVolume
        Function RegisterControlChangeNotify(<MarshalAs(UnmanagedType.Interface)> pNotify As IAudioEndpointVolumeCallback) As Integer
        Function UnregisterControlChangeNotify(<MarshalAs(UnmanagedType.Interface)> pNotify As IAudioEndpointVolumeCallback) As Integer

        Function GetChannelCount(ByRef channelCount As Integer) As HRESULT
        Function SetMasterVolumeLevel() As HRESULT
        Function SetMasterVolumeLevelScalar(level As Single, eventContext As Guid) As HRESULT
        Function GetMasterVolumeLevel(<Out> ByRef level As Single) As HRESULT
        Function GetMasterVolumeLevelScalar(<Out> ByRef level As Single) As HRESULT
        Function SetChannelVolumeLevel(channelNumber As Integer, level As Single, eventContext As Guid) As HRESULT
        Function SetChannelVolumeLevelScalar(channelNumber As Integer, level As Single, eventContext As Guid) As HRESULT
        Function GetChannelVolumeLevel(channelNumber As Integer, <Out> ByRef level As Single) As HRESULT
        Function GetChannelVolumeLevelScalar(channelNumber As Integer, <Out> ByRef level As Single) As HRESULT
        Function SetMute(<MarshalAs(UnmanagedType.Bool)> isMuted As Boolean, eventContext As Guid) As HRESULT
        Function GetMute(<Out> ByRef isMuted As Boolean) As HRESULT
        Function GetVolumeStepInfo(<Out> ByRef pnStep As Integer, ByRef pnStepCount As Integer) As HRESULT
        Function VolumeStepUp(eventContext As Guid) As HRESULT
        Function VolumeStepDown(eventContext As Guid) As HRESULT
        Function QueryHardwareSupport(<Out> ByRef hardwareSupportMask As Integer) As HRESULT
        Function GetVolumeRange(<Out> ByRef volumeMin As Single, <Out> ByRef volumeMax As Single, <Out> ByRef volumeStep As Single) As HRESULT
    End Interface

    <ComImport>
    <Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")>
    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IMMDeviceEnumerator
        Function EnumAudioEndpoints(ByVal dataFlow As EDataFlow, ByVal dwStateMask As Integer, <Out> ByRef ppDevices As IMMDeviceCollection) As HRESULT
        ' for 0x80070490 : Element not found
        <PreserveSig>
        Function GetDefaultAudioEndpoint(ByVal dataFlow As EDataFlow, ByVal role As ERole, <Out> ByRef ppEndpoint As IMMDevice) As HRESULT
        Function GetDevice(ByVal pwstrId As String, <Out> ByRef ppDevice As IMMDevice) As HRESULT
        Function NotImpl1() As Integer

    End Interface

    <ComImport>
    <Guid("D666063F-1587-4E43-81F1-B948E807363F")>
    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
    Public Interface IMMDevice
        Function Activate(ByRef iid As Guid, ByVal dwClsCtx As CLSCTX, ByVal pActivationParams As IntPtr, <Out> ByRef ppInterface As IAudioEndpointVolume) As HRESULT
        Function OpenPropertyStore(ByVal stgmAccess As Integer, <Out> ByRef ppProperties As IPropertyStore) As HRESULT
        Function GetId(<Out> ByRef ppstrId As IntPtr) As HRESULT
        Function GetState(<Out> ByRef pdwState As Integer) As HRESULT
    End Interface

End Module

' ==========
' Main Form:

Public Class Form1

' Contains:
' HMaster horizonal scroll 0-105 for Volume
' HBalance horizontal scroll -100 to 105 for Balance
' ChkMaster CheckBox for Mute
' LGuid label for Callback GUID

' Uses timer1 with 1 second interval to poll for Callback flag

    Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick

        If HaveChange = False Then Exit Sub

        HaveChange = False

        ChkMaster.Checked = Gstructure.bMuted

        Dim svolL As Integer = 100 * Gstructure.Left
        Dim svolR As Integer = 100 * Gstructure.Right
        Dim balance As Integer
        If svolL = svolR Then
            balance = 0
        Else
            If svolR > svolL Then
                balance = 100 - svolL * 100.0 / svolR
            Else
                balance = -(100 - svolR * 100.0 / svolL)
            End If
        End If

        HMaster.Value = 100 * Gstructure.fMasterVolume
        HBalance.Value = balance

        LGuid.Text = (Gstructure.guidEventContext.ToString)
    End Sub


   Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
        If CallBackOn Then ClassCoreAudio.Cancelcallback()
    End Sub

end class

Updated Version

Upvotes: 0

Views: 581

Answers (0)

Related Questions