PyOpenAL icon indicating copy to clipboard operation
PyOpenAL copied to clipboard

Audio Device Selection

Open djs45 opened this issue 2 years ago • 1 comments

Hello.

I'm working on audio 3D renderer with Python and PyOpenAL.

I have problems :

  • I can't list all audio device from my windos PC
  • So I can't select specific audio device for rendering audio
  • Will 3D audio spatialisation work with not 7.1 card (example : 10 channels pro audio board) ?

the 7.1 integrated soundboard work when selected by default on windows but I have to select another specific audio device and use PRO Audio USB Board

Many thanks for your answer and help :)

djs45 avatar Dec 21 '23 14:12 djs45

I had a similar issue with the audio device selection not functioning correctly. It does seem that PyOpenAL's current implementation doesn't support device listing properly.

Normally you'd use alc.alcIsExtensionPresent(None, b'ALC_ENUMERATION_EXT') and alc.alcGetString(None, alc.ALC_ALL_DEVICES_SPECIFIER) to get the list of playback devices. However, ALC_ALL_DEVICES_SPECIFIER is undefined in PyOpenAL. The correct constant is 0x1013, which you can pass directly.

In theory, you should be able to do:

if alc.alcIsExtensionPresent(None, b'ALC_ENUMERATION_EXT'):
        devices = alc.alcGetString(None, 0x1013)
        print(devices)

But OpenAL's documentation indicates that the strings returned for ALC_ALL_DEVICES_SPECIFIER are null character delimited with the end being marked by a double null character. Since alc.alcGetString() was bound to the CDLL with the return type ctypes.c_char_p, I'm guessing everything after the first null character gets dropped.

Here's the hack I came up with for reading these weird string lists:

import ctypes

from openal import alc

alc.ALC_ALL_DEVICES_SPECIFIER = 0x1013

def alcGetStringList(*args):
    alc.alcGetString.restype = ctypes.POINTER(ctypes.c_char)
    str_p = alc.alcGetString(*args)
    alc.alcGetString.restype = ctypes.c_char_p
    items = []
    item = b''
    for char in str_p:
        if char == b'\0':
            if not len(item):
                break
            items.append(item.decode('utf-8', errors='replace'))
            item = b''
        else:
            item += char
    return items

def list_audio_devices():
    device_list = []
    if alc.alcIsExtensionPresent(None, b'ALC_ENUMERATION_EXT'):
        return alcGetStringList(None, alc.ALC_ALL_DEVICES_SPECIFIER)
    else:
        print('WARNING: OpenAL device enumeration extension is not available.')
    return device_list
        
print(list_audio_devices())

Which gives me:

['OpenAL Soft on Headphones (3- High Definition Audio Device)', 'OpenAL Soft on Headphones (Oculus Virtual Audio Device)', 'OpenAL Soft on Digital Audio (S/PDIF) (3- High Definition Audio Device)']

THIS SOLUTION IS NOT THREAD SAFE! It temporarily overrides the default return type for alc.alcGetString() so that it can get what it needs. I don't work with DLL's much, so I'm not sure how to copy the DLL function pointer so I can have a separate function alias for the different return type.

I may look into making a PR to address the issue at some point, but looking into what the correct implementation would be would take a bit of time since the expected use case for alcGetString() based on the OpenAL documentation depends on some pointer shenanigans, which isn't very pythonic.

DaFluffyPotato avatar Mar 23 '25 19:03 DaFluffyPotato