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

Setting any device volume , a small discrepancy in volume.SetMasterVolumeLevelScalar() and the significance of volume.GetVolumeRange() #51

Open
drp0 opened this issue Nov 23, 2021 · 1 comment

Comments

@drp0
Copy link

drp0 commented Nov 23, 2021

I adapted a program by akaufman1 at #8 to read and set any audio device volume. (Well done akaufman1)
This allows me to set microphone and other volumes on my PC (Windows 11, Python 3.9.1)
I have noticed slight discrepancies in setting the volume of the speakers to say, 22% using volume.SetMasterVolumeLevelScalar(0.22, None).
volume.GetMasterVolumeLevelScalar() Reports 22 after setting. However windows control bar shows 21.
Thanks to Joseph Argumido for pointing me in the right direction for SetMasterVolumeLevelScalar.

To date, I have been unable to fully understand the significance of the 3 values returned by volume.GetVolumeRange()
Any thoughts? If some or all of the values are in decibels, how can they be converted to more meaningful values?
Is there a GetVolumeRangeLevel equivalent?

from __future__ import print_function
from ctypes import POINTER, cast
import comtypes
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume, CLSID_MMDeviceEnumerator, IMMDeviceEnumerator, EDataFlow, ERole

# audioVol3
# D.R.Patterson, 23/11/2021
# Get and set the volume of any audio device
# Using percentages.

# For the basis of this code: akaufman1, https://github.com/AndreMiras/pycaw/issues/8
# For pointing the way with SetMasterVolumeLevelScalar: Roy Joseph Argumido, https://github.com/AndreMiras/pycaw/issues/13

class MyAudioUtilities(AudioUtilities):
    @staticmethod
    def GetDevice(id=None, default=0):
        device_enumerator = comtypes.CoCreateInstance(
            CLSID_MMDeviceEnumerator,
            IMMDeviceEnumerator,
            comtypes.CLSCTX_INPROC_SERVER)
        if id is not None:
            thisDevice = device_enumerator.GetDevice(id)
        else:
            if default == 0:
                # output
                thisDevice = device_enumerator.GetDefaultAudioEndpoint(EDataFlow.eRender.value, ERole.eMultimedia.value)
            else:
                # input
                thisDevice = device_enumerator.GetDefaultAudioEndpoint(EDataFlow.eCapture.value, ERole.eMultimedia.value)
        return thisDevice


def main():
    mixer_output = None
    tmp = None
    devicelist = MyAudioUtilities.GetAllDevices()
    i = 0
    for device in devicelist:
        print(i, device)
        i += 1
    print(i, "Default Output")
    i += 1
    print(i, "Default Input")
    i += 1
    deviceSel = i
    
    while (deviceSel >= i) or (deviceSel < 0):
        print()
        search = input("Which device 0 to " + str(i-1) + ": ")
        deviceSel = int(search)

    
    if deviceSel < i-2:
        mixer_output = devicelist[int(search)]
        print(mixer_output)
        tmp = mixer_output.id
        devices = MyAudioUtilities.GetDevice(tmp)
    else:
        if deviceSel == i-2:
            print("Default Output")
            devices = MyAudioUtilities.GetDevice(tmp, 0)   # default output
        else:
            print("Default Input")
            devices = MyAudioUtilities.GetDevice(tmp, 1)   # default input
    print()
    interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
    volume = cast(interface, POINTER(IAudioEndpointVolume))
    
    print("GetMute(): ", volume.GetMute())      # set using volume.SetMute(1, None)
    print("GetMasterVolumeLevelScalar(): %s" % int(0.5 + 100.0 * volume.GetMasterVolumeLevelScalar()))
    print("GetVolumeRange(): (%s, %s, %s)" % volume.GetVolumeRange())

    newLevel = input("Enter new level (Ctrl C to quit): ")

    intnewLevel = int(newLevel.replace('%', ''))
    if intnewLevel < 0:  intnewLevel = 0.0
    if intnewLevel > 100: intnewLevel = 100.0
    print("SetMasterVolumeLevelScalar", intnewLevel / 100.0)
    volume.SetMasterVolumeLevelScalar(intnewLevel / 100.0, None)
    print("GetMasterVolumeLevelScalar(): %s" % int(0.5 + 100.0 * volume.GetMasterVolumeLevelScalar()))

if __name__ == "__main__":
    try:
        while True:
            main()
            print()
    except KeyboardInterrupt:
        print()

@evandrocoan
Copy link

I was also having trouble with the GetMasterVolumeLevel function. But after playing it, doing some tests, and searching, I figured out I can just use GetMasterVolumeLevelScalar to get percentage values i.e., from 0 to 1 (just multiply them by 100) instead of the DB level from GetMasterVolumeLevel . I just do not understand much of how DB values work, and I can work with percentages (which I understand better):

# python -m pip install pycaw pytest
import pytest
from pytest import approx

def range_convertion(OldValue, OldMin, OldMax, NewMin, NewMax):
    # https://stackoverflow.com/questions/929103/convert-a-number-range-to-another-range-maintaining-ratio
    OldRange = OldMax - OldMin
    if OldRange == 0:
        NewValue = NewMin
    else:
        NewRange = (NewMax - NewMin)
        NewValue = (((OldValue - OldMin) * NewRange) / OldRange) + NewMin
    return NewValue

def get_system_volume_percent(decibel):
    minimum, maximum, step = volume.GetVolumeRange()
    percent = range_convertion(decibel, minimum, maximum, 0, 100)
    return percent

def get_system_volume_decibel(percent):
    minimum, maximum, step = volume.GetVolumeRange()
    decibel = range_convertion(percent, 0, 100, minimum, maximum)
    return decibel

def test_percent_decibel():
    minimum, maximum, step = volume.GetVolumeRange()
    assert get_system_volume_percent(minimum) == 0
    assert get_system_volume_percent((maximum + minimum) / 2) == 50
    assert get_system_volume_percent(0) == 100

    assert get_system_volume_decibel(get_system_volume_percent(10)) == approx(get_system_volume_percent(get_system_volume_decibel(10)))
    assert get_system_volume_decibel(get_system_volume_percent(23)) == approx(get_system_volume_percent(get_system_volume_decibel(23)))

    assert get_system_volume_decibel(0) == minimum
    assert get_system_volume_decibel(50) == (maximum + minimum) / 2
    assert get_system_volume_decibel(100) == maximum

pytest.main([sys.argv[0], '-vvv',])

jumpSize = -5
# defaultSystemVolumeDecibel = volume.GetMasterVolumeLevelScalar()
defaultSystemVolumeDecibel = volume.GetMasterVolumeLevel()
defaultSystemVolumePercent = get_system_volume_percent(defaultSystemVolumeDecibel)
newSystemVolumePercent = min(defaultSystemVolumePercent + jumpSize, 100)
newSystemVolumeDecibel = get_system_volume_decibel(newSystemVolumePercent)

print(f'jumpSize {jumpSize}.')
print(f'defaultSystemVolumeDecibel {defaultSystemVolumeDecibel:.2f}, defaultSystemVolumePercent {defaultSystemVolumePercent:.2f}.')
print(f'newSystemVolumeDecibel     {newSystemVolumeDecibel:.2f},     newSystemVolumePercent     {newSystemVolumePercent:.2f}.')
# volume.SetMasterVolumeLevel(newSystemVolumeDecibel, None)

DB values are not linear, they are logarithmic, meaning they do not increase the sound proportionally as the percentages got from GetMasterVolumeLevelScalar : https://en.wikipedia.org/wiki/Logarithm (the values got from GetVolumeRange are the minimum DB, maximum DB, and the step the system accepts, these values should be used by someone who knows how to work with DB values).
image

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