Page 1 of 1

Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Wed Jan 29, 2025 4:16 pm
by AndyMK
Sets the Default Audio Device. Warning, this uses an Undocumented API method that may change in the future. As it stands, this is the only way to do it for now as Microsoft does not officially supply a method for this.

Code: Select all

; -----------------------------------------------------------------------------
;  Author: AndyMK
;  Date  : 02/02/2025
;  PB 6.20 Beta 3 (x64)
; -----------------------------------------------------------------------------
EnableExplicit

#CLSCTX_ALL = $17
#DEVICE_STATE_ACTIVE = $1

; For roles in SetDefaultEndpoint
#ERole_Console         = 0
#ERole_Multimedia      = 1
#ERole_Communications  = 2

; For IPropertyStore
#STGM_READ = $00000000

; -----------------------------------------------------------------------------
;  Structures for property access
; -----------------------------------------------------------------------------
Structure PROPERTYKEY
  Data1.l
  Data2.w
  Data3.w
  Data4.b[8]
  pid.l
EndStructure

Structure PROPVARIANT_CLIPDATA
  cbSize.l
  ulClipFmt.l
  *pClipData.BYTE
EndStructure

Structure PROPVARIANT_BSTRBLOB
  cbSize.l
  *pData.BYTE
EndStructure

Structure PROPVARIANT_BLOB
  cbSize.l
  *pBlobData.BYTE
EndStructure

Structure PROPVARIANT_VERSIONEDSTREAM
  guidVersion.GUID
  *pStream.IStream
EndStructure

Structure PROPVARIANT_CAC
  cElems.l
  *pElems.BYTE
EndStructure

Structure PROPVARIANT_CAUB
  cElems.l
  *pElems.BYTE
EndStructure

Structure PROPVARIANT_CAI
  cElems.l
  *pElems.WORD
EndStructure

Structure PROPVARIANT_CAUI
  cElems.l
  *pElems.WORD
EndStructure

Structure PROPVARIANT_CAL
  cElems.l
  *pElems.LONG
EndStructure

Structure PROPVARIANT_CAUL
  cElems.l
  *pElems.LONG
EndStructure

Structure PROPVARIANT_CAFLT
  cElems.l
  *pElems.FLOAT
EndStructure

Structure PROPVARIANT_CADBL
  cElems.l
  *pElems.DOUBLE
EndStructure

Structure PROPVARIANT_CACY
  cElems.l
  *pElems.QUAD
EndStructure

Structure PROPVARIANT_CADATE
  cElems.l
  *pElems.DOUBLE
EndStructure

Structure PROPVARIANT_CABSTR
  cElems.l
  *pElems.INTEGER
EndStructure

Structure PROPVARIANT_CABSTRBLOB
  cElems.l
  *pElems.PROPVARIANT_BSTRBLOB
EndStructure

Structure PROPVARIANT_CABOOL
  cElems.l
  *pElems.WORD
EndStructure

Structure PROPVARIANT_CASCODE
  cElems.l
  *pElems.LONG
EndStructure

Structure PROPVARIANT_CAPROPVARIANT
  cElems.l
  *pElems.PROPVARIANT
EndStructure

Structure PROPVARIANT_CAH
  cElems.l
  *pElems.QUAD
EndStructure

Structure PROPVARIANT_CAUH
  cElems.l
  *pElems.QUAD
EndStructure

Structure PROPVARIANT_CALPSTR
  cElems.l
  *pElems.INTEGER
EndStructure

Structure PROPVARIANT_CALPWSTR
  cElems.l
  *pElems.INTEGER
EndStructure

Structure PROPVARIANT_CAFILETIME
  cElems.l
  *pElems.FILETIME
EndStructure

Structure PROPVARIANT_CACLIPDATA
  cElems.l
  *pElems.PROPVARIANT_CLIPDATA
EndStructure

Structure PROPVARIANT_CACLSID
  cElems.l
  *pElems.CLSID
EndStructure

Structure PROPVARIANT Align #PB_Structure_AlignC
  vt.w
  wReserved1.w
  wReserved2.w
  wReserved3.w
  StructureUnion
    cVal.b
    bVal.b
    iVal.w
    uiVal.w
    lVal.l
    ulVal.l
    intVal.l
    uintVal.l
    hVal.q
    uhVal.q
    fltVal.f
    dblVal.d
    boolVal.w
    scode.l
    cyVal.q
    date.d
    filetime.FILETIME
    *puuid.CLSID
    *pclipdata.PROPVARIANT_CLIPDATA
    bstrVal.i
    bstrblobVal.PROPVARIANT_BSTRBLOB
    blob.PROPVARIANT_BLOB
    *pszVal
    *pwszVal
    *punkVal.IUnknown
    *pdispVal.IDispatch
    *pStream.IStream
    *pStorage.IStorage
    *pVersionedStream.PROPVARIANT_VERSIONEDSTREAM
    *parray.SAFEARRAY
    cac.PROPVARIANT_CAC
    caub.PROPVARIANT_CAUB
    cai.PROPVARIANT_CAI
    caui.PROPVARIANT_CAUI
    cal.PROPVARIANT_CAL
    caul.PROPVARIANT_CAUL
    cah.PROPVARIANT_CAH
    cauh.PROPVARIANT_CAUH
    caflt.PROPVARIANT_CAFLT
    cadbl.PROPVARIANT_CADBL
    cabool.PROPVARIANT_CABOOL
    cascode.PROPVARIANT_CASCODE
    cacy.PROPVARIANT_CACY
    cadate.PROPVARIANT_CADATE
    cafiletime.PROPVARIANT_CAFILETIME
    cauuid.PROPVARIANT_CACLSID
    caclipdata.PROPVARIANT_CACLIPDATA
    cabstr.PROPVARIANT_CABSTR
    cabstrblob.PROPVARIANT_CABSTRBLOB
    calpstr.PROPVARIANT_CALPSTR
    calpwstr.PROPVARIANT_CALPWSTR
    capropvar.PROPVARIANT_CAPROPVARIANT
    *pcVal.BYTE
    *pbVal.BYTE
    *piVal.WORD
    *puiVal.WORD
    *plVal.LONG
    *pulVal.LONG
    *pintVal.LONG
    *puintVal.LONG
    *pfltVal.FLOAT
    *pdblVal.DOUBLE
    *pboolVal.WORD
    *pdecVal.VARIANT_DECIMAL
    *pscode.LONG
    *pcyVal.QUAD
    *pdate.DOUBLE
    *pbstrVal.INTEGER
    *ppunkVal.INTEGER
    *ppdispVal.INTEGER
    *pparray.INTEGER
    *pvarVal.PROPVARIANT
  EndStructureUnion
EndStructure

; -----------------------------------------------------------------------------
;  COM Interfaces
; -----------------------------------------------------------------------------
Interface IMMDeviceEnumerator Extends IUnknown
  EnumAudioEndpoints(dataFlow.l, dwStateMask.l, *ppDevices)
  GetDefaultAudioEndpoint(dataFlow.l, role.l, *ppEndpoint)
  GetDevice(pwstrId, ppDevice)
  RegisterEndpointNotificationCallback(pClient)
  UnregisterEndpointNotificationCallback(pClient)
EndInterface

Interface IMMDeviceCollection Extends IUnknown
  GetCount(*pCount)
  Item(index.l, *ppDevice)
EndInterface

Interface IMMDevice Extends IUnknown
  Activate(iid.i, dwClsCtx.l, pActivationParams.i, ppInterface.i)
  OpenPropertyStore(stgmAccess.l, ppProperties.i)
  GetId(*ppstrId)
  GetState(*pdwState)
EndInterface

Interface IPropertyStore Extends IUnknown
  GetCount(*cProps.l)
  GetAt(i.l, *pKey.PROPERTYKEY)
  GetValue(*key.PROPERTYKEY, *pv.PROPVARIANT)
  SetValue(*key.PROPERTYKEY, *pv.PROPVARIANT)
  Commit()
EndInterface

; Our undocumented IPolicyConfig with placeholders
; Make sure SetDefaultEndpoint is at the correct vtable slot for your system
Interface IPolicyConfig Extends IUnknown
  Unused1()
  Unused2()
  Unused3()
  Unused4()
  Unused5()
  Unused6()
  Unused7()
  Unused8()
  Unused9()
  Unused10()
  SetDefaultEndpoint(pwstrId, role.l)
EndInterface

; -----------------------------------------------------------------------------
;  Procedure: GetFriendlyName(device.IMMDevice)
;    - Opens property store + retrieves PKEY_Device_FriendlyName
; -----------------------------------------------------------------------------
Procedure.s GetFriendlyName(device.IMMDevice)
  Protected props.IPropertyStore
  Protected propVariant.PROPVARIANT
  Protected hr.l
  Protected result.s = ""

  hr = device\OpenPropertyStore(#STGM_READ, @props)
  If hr = #S_OK And props

    propVariant\vt = 0
    hr = props\GetValue(?PKEY_Device_FriendlyName, @propVariant)
    If hr = #S_OK
      If propVariant\vt = 31 ; VT_LPWSTR (0x1F)
        Protected *wideName = propVariant\pwszVal
        If *wideName
          result = PeekS(*wideName, -1, #PB_Unicode)
        EndIf
      EndIf
    EndIf

    props\Release()
  EndIf

  ProcedureReturn result
EndProcedure

; -----------------------------------------------------------------------------
;  Procedure: ListRenderDevices
;    - Enumerates active render endpoints
;    - Prints each device ID + friendly name
; -----------------------------------------------------------------------------
Procedure ListRenderDevices()
  Protected hr.l, deviceEnumerator.IMMDeviceEnumerator, deviceCollection.IMMDeviceCollection
  Protected count.l, i.l
  Protected device.IMMDevice
  Protected deviceId
  Protected name.s

  CoInitialize_(#Null)

  hr = CoCreateInstance_(?CLSID_MMDeviceEnumerator, #Null, #CLSCTX_ALL, ?IID_IMMDeviceEnumerator, @deviceEnumerator)
  If hr <> #S_OK
    Debug "CoCreateInstance(IMMDeviceEnumerator) failed hr=" + Hex(hr)
    ProcedureReturn
  EndIf

  ; eRender=0, #DEVICE_STATE_ACTIVE=1 => get active render endpoints
  hr = deviceEnumerator\EnumAudioEndpoints(0, #DEVICE_STATE_ACTIVE, @deviceCollection)
  If hr <> #S_OK
    Debug "EnumAudioEndpoints() failed hr=" + Hex(hr)
    ProcedureReturn
  EndIf

  deviceCollection\GetCount(@count)
  Debug "Active Render Devices found: " + Str(count)

  For i=0 To count-1
    deviceCollection\Item(i, @device)
    If device
      ; Retrieve device ID
      device\GetId(@deviceId)
      ; Retrieve friendly name
      name = GetFriendlyName(device)

      Debug "Index=" + Str(i)
      Debug "  ID = " + PeekS(deviceId, -1, #PB_Unicode)
      Debug "  FriendlyName = " + name

      device\Release()
    EndIf
  Next

  deviceCollection\Release()
  deviceEnumerator\Release()

  CoUninitialize_()
EndProcedure

; -----------------------------------------------------------------------------
;  Procedure: SetDefaultRenderDeviceByIndex(index.l, role.l)
;    - Sets the device at 'index' as default for role (eConsole, eMultimedia, etc.)
; -----------------------------------------------------------------------------
Procedure SetDefaultRenderDeviceByIndex(index.l, role.l)
  Protected hr.l
  Protected deviceEnumerator.IMMDeviceEnumerator
  Protected deviceCollection.IMMDeviceCollection
  Protected count.l
  Protected device.IMMDevice
  Protected deviceId
  Protected name.s
  
  Protected policyConfig.IPolicyConfig

  CoInitialize_(#Null)

  hr = CoCreateInstance_(?CLSID_MMDeviceEnumerator, #Null, #CLSCTX_ALL, ?IID_IMMDeviceEnumerator, @deviceEnumerator)
  If hr <> #S_OK
    Debug "CoCreateInstance(IMMDeviceEnumerator) failed hr=" + Hex(hr)
    Goto Cleanup
  EndIf

  hr = deviceEnumerator\EnumAudioEndpoints(0, #DEVICE_STATE_ACTIVE, @deviceCollection) ; eRender=0
  If hr <> #S_OK
    Debug "EnumAudioEndpoints() failed hr=" + Hex(hr)
    Goto Cleanup
  EndIf

  deviceCollection\GetCount(@count)
  If index < 0 Or index >= count
    Debug "Invalid device index: " + Str(index)
    Goto Cleanup
  EndIf

  hr = deviceCollection\Item(index, @device)
  If hr <> #S_OK Or device=#Null
    Debug "Item(" + Str(index) + ") failed, hr=" + Hex(hr)
    Goto Cleanup
  EndIf

  hr = device\GetId(@deviceId)
  If hr <> #S_OK
    Debug "GetId() failed hr=" + Hex(hr)
    Goto Cleanup
  EndIf
  
  name = GetFriendlyName(device)
  Debug "Device ID to set as default: " + name

  ; Create IPolicyConfig
  hr = CoCreateInstance_(?CLSID_CPolicyConfigClient, #Null, #CLSCTX_ALL, ?IID_IPolicyConfig, @policyConfig)
  If hr <> #S_OK
    Debug "CoCreateInstance(CPolicyConfigClient) failed hr=" + Hex(hr)
    Goto Cleanup
  EndIf

  hr = policyConfig\SetDefaultEndpoint(deviceId, role)
  If hr = #S_OK
    Debug "Successfully set default device for role "+Str(role)
  Else
    Debug "SetDefaultEndpoint failed, hr="+Hex(hr)
  EndIf

  ; Cleanup
  Cleanup:
    If policyConfig
      policyConfig\Release()
    EndIf
    If device
      device\Release()
    EndIf
    If deviceCollection
      deviceCollection\Release()
    EndIf
    If deviceEnumerator
      deviceEnumerator\Release()
    EndIf

    CoUninitialize_()
EndProcedure

; -----------------------------------------------------------------------------
;  DataSection
; -----------------------------------------------------------------------------
DataSection
  ; CLSID_MMDeviceEnumerator {BCDE0395-E52F-467C-8E3D-C4579291692E}
  CLSID_MMDeviceEnumerator:
  Data.l $BCDE0395
  Data.w $E52F,$467C
  Data.b $8E,$3D,$C4,$57,$92,$91,$69,$2E

  IID_IMMDeviceEnumerator:
  Data.l $A95664D2
  Data.w $9614,$4F35
  Data.b $A7,$46,$DE,$8D,$B6,$36,$17,$E6

  ; class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient;
  CLSID_CPolicyConfigClient:
  Data.l $870AF99C
  Data.w $171D,$4F9E
  Data.b $AF,$0D,$E6,$3D,$F4,$0C,$2B,$C9

  ; interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig;
  IID_IPolicyConfig:
  Data.l $F8679F50
  Data.w $850A,$41CF
  Data.b $9C,$72,$43,$0F,$29,$02,$90,$C8

  ; PKEY_Device_FriendlyName = {A45C254E-DF1C-4EFD-8020-67D146A850E0}, PID=14
  PKEY_Device_FriendlyName:
  Data.l $A45C254E
  Data.w $DF1C,$4EFD
  Data.b $80,$20,$67,$D1,$46,$A8,$50,$E0
  Data.l 14
EndDataSection

; -----------------------------------------------------------------------------
;  Demo
; -----------------------------------------------------------------------------
Procedure Main()
  Debug "=== Listing Devices with Friendly Names ==="
  ListRenderDevices()
  
  Debug "=== Set device with index 1 as default (Console) ==="
  SetDefaultRenderDeviceByIndex(1, #ERole_Console)
  
  ; You can also do:
  ; SetDefaultRenderDeviceByIndex(1, #ERole_Multimedia)
  ; SetDefaultRenderDeviceByIndex(1, #ERole_Communications)
EndProcedure

Main()
End
Fixed PROPVARIANT Structure.

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Wed Jan 29, 2025 8:16 pm
by Quin
Oh this is absolutely amazing, I wanted to write a utility to do this a year or so ago with global hotkeys, but I am nowhere near smart enough to wrap COM in PB like this. You amaze me, sir. Hats off to you! :)

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Wed Jan 29, 2025 9:38 pm
by Kwai chang caine
Can be usefull
Thanks for sharing 8)

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Thu Jan 30, 2025 12:09 am
by idle
good work thanks for sharing

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Sat Feb 01, 2025 7:54 pm
by User_Russian
In x64, if you enable purifier, appears message about stack corruption in procedure GetFriendlyName().
Probably the structure PROPVARIANT is incorrect.

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Sat Feb 01, 2025 10:52 pm
by ChrisR
Great work, thanks AndyMK :)

About the stack corruption in GetFriendlyName(),
It seems to be solved using the PROPVARIANT structure as defined by nco2k and changing the line "Protected *wideName = propVariant\pwszVal" in GetFriendlyName() procedure, see PKEY_Device_FriendlyName.

Re: Windows 7/8/8.1/10/11 Set Default Audio Device

Posted: Sun Feb 02, 2025 11:25 am
by AndyMK
Thanks User_Russian/ChrisR. Updated.