23 January 2009

Set default Wave Out Audio Device - VB.Net / DRVM_MAPPER_PREFERRED_SET

You can set the default audio playback device in windows 2000, Me, and XP with the DRVM_MAPPER_PREFERRED_SET message, which is sent with waveOutMessage().

It doesn't work on vista - you get MMSYSERR_NOTSUPPORTED (8), returned.
You can do it in Vista - see http://vachanger.sourceforge.net/

Anyway, the code uses the Windows Media .Net library.

Imports MultiMedia ' http://windowsmedianet.sourceforge.net/
Imports System.Runtime.InteropServices
Imports System.ComponentModel

Public Class Form1

    Private DevicesComboBox As New ComboBox
    Private DefaultDeviceLabel As New Label
    Private WithEvents SetDefaultButton As New Button
    Private Const DRVM_MAPPER_PREFERRED_GET As Integer = &H2015
    Private Const DRVM_MAPPER_PREFERRED_SET As Integer = &H2016
    Private WAVE_MAPPER As New IntPtr(-1)

    ' This just brings together a device ID and a WaveOutCaps so 
    ' that we can store them in a combobox.
    Private Structure WaveOutDevice

        Private m_id As Integer
        Public Property Id() As Integer
            Get
                Return m_id
            End Get
            Set(ByVal value As Integer)
                m_id = value
            End Set
        End Property

        Private m_caps As WaveOutCaps
        Public Property WaveOutCaps() As WaveOutCaps
            Get
                Return m_caps
            End Get
            Set(ByVal value As WaveOutCaps)
                m_caps = value
            End Set
        End Property

        Sub New(ByVal id As Integer, ByVal caps As WaveOutCaps)
            m_id = id
            m_caps = caps
        End Sub

        Public Overrides Function ToString() As String
            Return WaveOutCaps.szPname
        End Function

    End Structure

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' I do use the IDE for this stuff normally... (in case anyone is wondering)
        Me.Controls.AddRange(New Control() {DevicesComboBox, DefaultDeviceLabel, SetDefaultButton})
        DevicesComboBox.Location = New Point(5, 5)
        DevicesComboBox.DropDownStyle = ComboBoxStyle.DropDownList
        DevicesComboBox.Width = Me.ClientSize.Width - 10
        DevicesComboBox.Anchor = AnchorStyles.Left Or AnchorStyles.Right
        DefaultDeviceLabel.Location = New Point(DevicesComboBox.Left, DevicesComboBox.Bottom + 5)
        DefaultDeviceLabel.AutoSize = True
        SetDefaultButton.Location = New Point(DefaultDeviceLabel.Left, DefaultDeviceLabel.Bottom + 5)
        SetDefaultButton.Text = "Set Default"
        SetDefaultButton.AutoSize = True
        RefreshInformation()
    End Sub

    Private Sub RefreshInformation()
        PopulateDeviceComboBox()
        DisplayDefaultWaveOutDevice()
    End Sub

    Private Sub PopulateDeviceComboBox()
        DevicesComboBox.Items.Clear()
        ' How many wave out devices are there? WaveOutGetNumDevs API call.
        Dim waveOutDeviceCount As Integer = waveOut.GetNumDevs()
        For i As Integer = 0 To waveOutDeviceCount - 1
            Dim caps As New WaveOutCaps
            ' Get a name - its in a WAVEOUTCAPS structure. 
            ' The name is truncated to 31 chars by the api call. You probably have to 
            ' dig around in the registry to get the full name.
            Dim result As Integer = waveOut.GetDevCaps(i, caps, Marshal.SizeOf(caps))
            If result <> MMSYSERR.NoError Then
                Dim err As MMSYSERR = DirectCast(result, MMSYSERR)
                Throw New Win32Exception("GetDevCaps() error, Result: " & result.ToString("x8") & ", " & err.ToString)
            End If
            DevicesComboBox.Items.Add(New WaveOutDevice(i, caps))
        Next
        DevicesComboBox.SelectedIndex = 0
    End Sub

    Private Sub DisplayDefaultWaveOutDevice()
        Dim currentDefault As Integer = GetIdOfDefaultWaveOutDevice()
        Dim device As WaveOutDevice = DirectCast(DevicesComboBox.Items(currentDefault), WaveOutDevice)
        DefaultDeviceLabel.Text = "Defualt: " & device.WaveOutCaps.szPname
    End Sub

    Private Function GetIdOfDefaultWaveOutDevice() As Integer        
        Dim id As Integer = 0
        Dim hId As IntPtr
        Dim flags As Integer = 0
        Dim hFlags As IntPtr
        Dim result As Integer
        Try
            ' It would be easier to declare a nice overload with ByRef Integers.
            hId = Marshal.AllocHGlobal(4)
            hFlags = Marshal.AllocHGlobal(4)
            ' http://msdn.microsoft.com/en-us/library/bb981557.aspx
            result = waveOut.Message(WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET, hId, hFlags)
            If result <> MMSYSERR.NoError Then
                Dim err As MMSYSERR = DirectCast(result, MMSYSERR)
                Throw New Win32Exception("waveOutMessage() error, Result: " & result.ToString("x8") & ", " & err.ToString)
            End If
            id = Marshal.ReadInt32(hId)
            flags = Marshal.ReadInt32(hFlags)
        Finally
            Marshal.FreeHGlobal(hId)
            Marshal.FreeHGlobal(hFlags)
        End Try
        ' There is only one flag, DRVM_MAPPER_PREFERRED_FLAGS_PREFERREDONLY, defined as 1
        ' "When the DRVM_MAPPER_PREFERRED_FLAGS_PREFERREDONLY flag bit is set, ... blah ...,  
        ' the waveIn and waveOut APIs use only the current preferred device and do not search 
        ' for other available devices if the preferred device is unavailable. 
        Return id
    End Function

    Private Sub SetDefaultButton_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles SetDefaultButton.Click
        If DevicesComboBox.Items.Count = 0 Then Return
        Dim selectedDevice As WaveOutDevice = DirectCast(DevicesComboBox.SelectedItem, WaveOutDevice)
        SetDefault(selectedDevice.Id)
        RefreshInformation()
    End Sub

    Private Sub SetDefault(ByVal id As Integer)
        Dim defaultId As Integer = GetIdOfDefaultWaveOutDevice()
        If defaultId = id Then Return ' no change.
        Dim result As Integer
        ' So here we say "change the Id of the device that has id id to 0", which makes it the default.
        result = waveOut.Message(WAVE_MAPPER, DRVM_MAPPER_PREFERRED_SET, New IntPtr(id), IntPtr.Zero)
        If result <> MMSYSERR.NoError Then
            Dim err As MMSYSERR = DirectCast(result, MMSYSERR)
            Throw New Win32Exception("waveOutMessage() error, Result: " & result.ToString("x8") & ", " & err.ToString)
        End If
    End Sub

End Class

15 January 2009

Enable/Disable a device programmatically with VB.Net using the setup api.

Very quickly...

Check in device manager to see if the device has "Disable" as an option when you R click it. If so then look at the properties, and find the "class guid" and "device instance id".

1) Get a handle to a device info set using SetupDiGetClassDevs - this will get all devices in a class.
2) Get device info data for each device in the class using SetupDiEnumDeviceInfo
3) Get the device instance id for each device using the device info data from (2) and SetupDiGetDeviceInstanceId.
4) Fill in a structure to say you want a property change and call SetupDiSetClassInstallParams. This sets the property in the device info set.
5) Call SetupDiCallClassInstaller to get the installer to make the changes stick.

9 January 2009

Font choice -> dumb bug

Oh wonderful. I thought I was doing something seriously wrong whilst trying to record sound. WaveInOpen takes a flag value, I wanted to specify that it should callback a function and read it as:

#define CALLBACK_FUNCTION   0x000300001

where it is actually

#define CALLBACK_FUNCTION   0x00030000l 

Big difference eh. 1 vs L. The L specifies a C Long (32 bit integer).

I ended up sending two flags then, 0x300000 and 0x1. 0x1 tells it to just query - "can the device be opened" - without actually opening it. 0x300000 isn't defined. In testing it did call my callback function about 1 time in 3, the other 2 times it just crashed without error. So I though it was because I was doing the wrong thing in the callback. Argh.