Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example: muting microphone #4

Open
zllovesuki opened this issue Aug 20, 2020 · 2 comments
Open

Example: muting microphone #4

zllovesuki opened this issue Aug 20, 2020 · 2 comments

Comments

@zllovesuki
Copy link

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

1.14

What operating system and processor architecture are you using (go env)?

Windows 10 64-bit 2004

Hello, I came across your library and I was wondering if you have an example of muting the microphone. I see that the examples are all about output devices but none are about input devices. Thank you!

@zllovesuki
Copy link
Author

zllovesuki commented Aug 21, 2020

I figured it out.

package main

import (
	"fmt"

	"github.com/go-ole/go-ole"
	"github.com/moutend/go-wca/pkg/wca"
)

func main() {
	if err := volumn(); err != nil {
		panic(err)
	}
}

func volumn() (err error) {
	if err = ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil {
		return
	}
	defer ole.CoUninitialize()

	var mmde *wca.IMMDeviceEnumerator
	if err = wca.CoCreateInstance(wca.CLSID_MMDeviceEnumerator, 0, wca.CLSCTX_ALL, wca.IID_IMMDeviceEnumerator, &mmde); err != nil {
		return
	}
	defer mmde.Release()

	var mmd *wca.IMMDevice
	if err = mmde.GetDefaultAudioEndpoint(wca.ECapture, wca.EConsole, &mmd); err != nil {
		return
	}
	defer mmd.Release()

	/*var ps *wca.IPropertyStore
	if err = mmd.OpenPropertyStore(wca.STGM_READ, &ps); err != nil {
		return
	}
	defer ps.Release()

	var pv wca.PROPVARIANT
	if err = ps.GetValue(&wca.PKEY_Device_FriendlyName, &pv); err != nil {
		return
	}
	fmt.Printf("%s\n", pv.String())*/

	var aev *wca.IAudioEndpointVolume
	if err = mmd.Activate(wca.IID_IAudioEndpointVolume, wca.CLSCTX_ALL, nil, &aev); err != nil {
		return
	}
	defer aev.Release()

	var mute bool
	if err = aev.GetMute(&mute); err != nil {
		return
	}

	if err = aev.SetMute(!mute, nil); err != nil {
		return
	}

	fmt.Printf("mute: %v\n", !mute)

	return
}

Notice that we are using ECapture instead of ERender

@emmaly
Copy link

emmaly commented Jul 25, 2024

It would be nice if an example were created and published with the /_example directory, but considering this is no longer an actual issue, please consider closing it @zllovesuki.

On a side note: in case having two example is useful for anyone coming across this, I recently implemented something like this to be used while streaming in OBS, but I needed to be specific as to which microphone was being muted. Here's how I went about it, which is definitely far more code and complication than @zllovesuki 's above.

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/go-ole/go-ole"
	"github.com/moutend/go-wca/pkg/wca"
)

type micDevice struct {
	id   string
	name string
}

type micState struct {
	muted  *bool
	volume *float32
}

func (m *micState) IsEqual(other *micState) bool {
	if m == nil && other == nil { // both are nil, which is equal
		return true
	}

	// one is nil, but not both
	if m == nil || other == nil {
		return false
	}

	// one has nil muted, but the other doesn't
	if m.muted == nil && other.muted != nil {
		return false
	}
	if m.muted != nil && other.muted == nil {
		return false
	}

	// one has nil volume, but the other doesn't
	if m.volume == nil && other.volume != nil {
		return false
	}
	if m.volume != nil && other.volume == nil {
		return false
	}

	// both are not nil, so compare the values and return the result
	return *m.muted == *other.muted && *m.volume == *other.volume
}

// IsMuted returns true if the mic is muted or the volume is 0 (nil values are not considered muted)
func (m *micState) IsMuted() bool {
	if m == nil {
		return false
	}
	return (m.muted != nil && *m.muted) || (m.volume != nil && *m.volume == 0)
}

// IsUnmuted returns true if the mic is not muted or the volume is greater than 0 (nil values are not considered unmuted)
func (m *micState) IsUnmuted() bool {
	if m == nil {
		return false
	}
	return (m.muted != nil && !*m.muted) || (m.volume != nil && *m.volume > 0)
}

// IsUndetermined returns true if the mic state is undetermined (nil values)
func (m *micState) IsUndetermined() bool {
	return m == nil || (m.muted == nil && m.volume == nil)
}

func wcaECaptureDeviceInfo(matcher func(micDevice) bool, callback func(micDevice, micState) *micState) error {
	// Initialize COM library
	ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED)
	defer ole.CoUninitialize()

	// Initialize Windows Core Audio
	var enumerator *wca.IMMDeviceEnumerator
	if err := wca.CoCreateInstance(wca.CLSID_MMDeviceEnumerator, 0, wca.CLSCTX_ALL, wca.IID_IMMDeviceEnumerator, &enumerator); err != nil {
		return fmt.Errorf("failed to create device enumerator: %v", err)
	}
	defer enumerator.Release()

	// Enumerate audio endpoint devices
	var deviceCollection *wca.IMMDeviceCollection
	if err := enumerator.EnumAudioEndpoints(wca.ECapture, wca.DEVICE_STATE_ACTIVE, &deviceCollection); err != nil {
		return fmt.Errorf("failed to enumerate audio endpoints: %v", err)
	}
	defer deviceCollection.Release()

	// Get device count
	var count uint32
	if err := deviceCollection.GetCount(&count); err != nil {
		return fmt.Errorf("failed to get device count: %v", err)
	}

	// Iterate over devices
	for i := uint32(0); i < count; i++ {
		var item *wca.IMMDevice
		if err := deviceCollection.Item(i, &item); err != nil {
			return fmt.Errorf("failed to get device item: %v", err)
		}
		defer item.Release()

		// Get the device ID
		var deviceId string
		if err := item.GetId(&deviceId); err != nil {
			return fmt.Errorf("failed to get device ID: %v", err)
		}

		// Open the property store
		var propertyStore *wca.IPropertyStore
		if err := item.OpenPropertyStore(wca.STGM_READ, &propertyStore); err != nil {
			return fmt.Errorf("failed to open property store: %v", err)
		}
		defer propertyStore.Release()

		// Get the friendly name of the device
		var propVariant wca.PROPVARIANT
		if err := propertyStore.GetValue(&wca.PKEY_Device_FriendlyName, &propVariant); err != nil {
			return fmt.Errorf("failed to get friendly name: %v", err)
		}
		deviceName := propVariant.String()
		propVariant.Clear()

		// Check if the device is the one we're looking for
		if matcher(micDevice{id: deviceId, name: deviceName}) {
			// log.Printf("Found Windows Core Audio device: %s\n", deviceName)

			// Get the IAudioEndpointVolume interface
			var audioEndpointVolume *wca.IAudioEndpointVolume
			if err := item.Activate(wca.IID_IAudioEndpointVolume, wca.CLSCTX_ALL, nil, &audioEndpointVolume); err != nil {
				return fmt.Errorf("failed to activate audio endpoint volume: %v", err)
			}
			defer audioEndpointVolume.Release()

			getMicState := func(audioEndpointVolume *wca.IAudioEndpointVolume) *micState {
				// Build the micState object, starting with nil values to indicate undetermined state
				state := &micState{}

				// Get the mute state
				for j := 0; j < 5; j++ {
					var muted bool
					if err := audioEndpointVolume.GetMute(&muted); err != nil {
						log.Printf("failed to get mute state: %v", err)
					} else {
						state.muted = &muted
						// log.Printf("Mute state: %v\n", muted)
						break
					}
					time.Sleep(100 * time.Millisecond)
				}

				// Get the volume level
				for j := 0; j < 5; j++ {
					var volume float32
					if err := audioEndpointVolume.GetMasterVolumeLevelScalar(&volume); err != nil {
						log.Printf("failed to get volume level: %v", err)
					} else {
						state.volume = &volume
						// log.Printf("Volume level: %v\n", volume)
						break
					}
					time.Sleep(100 * time.Millisecond)
				}

				return state
			}

			setMicState := func(audioEndpointVolume *wca.IAudioEndpointVolume, state *micState) {
				if state == nil {
					return
				}

				// Existing state
				currentState := getMicState(audioEndpointVolume)

				// Set the mute state, if necessary
				if state.muted != nil && currentState.muted != state.muted {
					log.Printf("Setting mute state to %v\n", *state.muted)
					if err := audioEndpointVolume.SetMute(*state.muted, nil); err != nil {
						log.Printf("Failed to set mute state: %v\n", err)
						// } else {
						//   log.Printf("Set mute state: %v\n", *state.muted)
					}
				}

				// Set the volume level, if necessary
				if state.volume != nil && currentState.volume != state.volume {
					log.Printf("Setting volume level to %v\n", *state.volume)
					if err := audioEndpointVolume.SetMasterVolumeLevelScalar(*state.volume, nil); err != nil {
						log.Printf("Failed to set volume level: %v\n", err)
						// } else {
						//   log.Printf("Set volume level: %v\n", *state.volume)
					}
				}
			}

			// Get the initial state
			state := getMicState(audioEndpointVolume)

			// Call the callback, if provided
			if callback != nil {
				dev := micDevice{
					id:   deviceId,
					name: deviceName,
				}

				newState := callback(dev, *state)

				// Set the new state, if provided
				if newState != nil {
					updateMicState := callback(dev, *state)
					if updateMicState != nil {
						// the callback asked for a change, so update the state
						setMicState(audioEndpointVolume, updateMicState)
					}
				}
			}

		}
	}
	return nil
}

func observeMicState(name string, callback func([]micDevice, []micState)) chan bool {
	if name == "" {
		return nil
	}

	log.Printf("Observing microphone state: %s\n", name)

	done := make(chan bool)

	deviceMatcherByName := func(name string) func(micDevice) bool {
		return func(device micDevice) bool {
			return device.name == name
		}
	}

	captureMicState := func(matchedDevices *[]micDevice, capturedStates *[]micState) func(micDevice, micState) *micState {
		return func(device micDevice, state micState) *micState {
			*matchedDevices = append(*matchedDevices, device)
			*capturedStates = append(*capturedStates, state)

			// no changes requested, so return nil
			return nil
		}
	}

	captureDeviceInfoOnce := func(name string) ([]micDevice, []micState, error) {
		states := make([]micState, 0)
		devices := make([]micDevice, 0)
		err := wcaECaptureDeviceInfo(deviceMatcherByName(name), captureMicState(&devices, &states))
		if err != nil {
			log.Printf("Failed to get state: %v\n", err)
		}
		return devices, states, err
	}

	go func(name string, callback func([]micDevice, []micState)) {
		devices, states, err := captureDeviceInfoOnce(name)
		if err != nil {
			log.Printf("Failed to get initial state: %v\n", err)
		}
		if len(devices) == 0 || len(states) == 0 {
			log.Printf("No devices found for name: %s\n", name)
		} else {
			callback(devices, states)
		}

		for {
			select {
			case <-done:
				return
			case <-time.After(1 * time.Second):
				devicesNow, statesNow, err := captureDeviceInfoOnce(name)
				if err != nil {
					log.Printf("Failed to get state: %v\n", err)
				}
				if len(devicesNow) != len(statesNow) {
					log.Printf("Mismatched devices and states: %d != %d\n", len(devicesNow), len(statesNow))
					continue
				}

				// turn the devices & states into maps for easier comparison
				historyDevices := make(map[string]micDevice)
				for i := 0; i < len(devices); i++ {
					historyDevices[devices[i].id] = devices[i]
				}
				historyStates := make(map[string]micState)
				for i := 0; i < len(states); i++ {
					historyStates[devices[i].id] = states[i]
				}

				// turn the devicesNow & statesNow into maps for easier comparison
				devicesNowMap := make(map[string]micDevice)
				for i := 0; i < len(devicesNow); i++ {
					devicesNowMap[devicesNow[i].id] = devicesNow[i]
				}
				statesNowMap := make(map[string]micState)
				for i := 0; i < len(statesNow); i++ {
					statesNowMap[devicesNow[i].id] = statesNow[i]
				}

				// find the devices and states that have changed
				changedDevices := make([]micDevice, 0)
				changedStates := make([]micState, 0)

				// check if the state has changed
				for i := 0; i < len(devicesNow); i++ {
					historyState, ok := historyStates[devicesNow[i].id]
					if (ok && !statesNow[i].IsEqual(&historyState)) || (!ok) {
						// state has changed or is new
						log.Printf("State changed for %s\n", devicesNow[i].name)
						historyStates[devicesNow[i].id] = statesNow[i]
						changedDevices = append(changedDevices, devicesNow[i])
						changedStates = append(changedStates, statesNow[i])
					}
				}

				// check if a device has been removed
				for id, device := range historyDevices {
					if _, ok := devicesNowMap[id]; !ok {
						// device has been removed
						log.Printf("Device removed: %s\n", device.name)
						delete(historyDevices, id)
						delete(historyStates, id)
						changedDevices = append(changedDevices, device)
						changedStates = append(changedStates, micState{})
					}
				}

				// check if a device has been added/removed
				if len(changedDevices) > 0 {
					// update the history from the map
					devices = make([]micDevice, 0)
					states = make([]micState, 0)
					for id, state := range historyStates {
						devices = append(devices, historyDevices[id])
						states = append(states, state)
					}

					// call the callback with the changed devices and states
					callback(changedDevices, changedStates)
				}
			}
		}
	}(name, callback)

	return done
}

func toggleMicMute(name string) {
	if name == "" {
		return
	}

	// log.Printf("Toggling microphone mute: %s\n", name)

	// the device matcher logic, which simply uses the device's display
	// name as shown in the Windows UI, but there's no reason you couldn't
	// use completely different matching logic
	deviceMatcherByName := func(name string) func(micDevice) bool {
		return func(device micDevice) bool {
			return device.name == name
		}
	}

	// this performs the actual mic state toggle, which is used in 
	toggleMicMute := func(device micDevice, state micState) *micState {
		log.Printf("Toggling mute for: %s\n", device.name)
		if state.muted != nil {
			// log.Printf("Current mute state: %v\n", *state.muted)
			wantState := !(*state.muted)
			// log.Printf("Wanted mute state: %v\n", wantState)
			return &micState{muted: &wantState}
		}
		return nil
	}

	// how the `wcaECaptureDeviceInfo` function is used to locate the mic,
	// including the matcher function and the callback that performs the toggle
	_ = wcaECaptureDeviceInfo(deviceMatcherByName(name), toggleMicMute)
}

And here's how it is used:

// provide the name as seen in the Windows UI
// (see comments in the function above for explanation)
toggleMicMute("Microphone (Yeti GX)")

diegosz added a commit to diegosz/go-wca that referenced this issue Sep 1, 2024
diegosz added a commit to diegosz/go-wca that referenced this issue Sep 1, 2024
diegosz added a commit to diegosz/go-wca that referenced this issue Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants