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

Access to PortAudio's platform-specific API #85

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

dholl
Copy link
Contributor

@dholl dholl commented Apr 26, 2017

I created a branch in my repo fork called "hostapis", that I'm eager to start discussing. It enables access to PortAudio's platform-specific API in 2 alternative fashions: (Note, the first syntax needs PR #84 for name= support with query_hostapis())

import sounddevice as sd
chan_name = sd.query_hostapis(name="Core Audio")['api'].get_input_channel_name(device, channel)
chan_name = sd.hostapis.coreaudio.api.get_input_channel_name(device, channel)
  • The first approach involves a more traditional interface by just extending query_hostapis() to return an additional dictionary key, 'api'.

  • The second approach introduces a new item to the global namespace, hostapis.

Of course this branch's code has a bunch of style issues..., but at first, I'm looking forward to at least start exploring these strategies or brainstorming upon some other approaches. The underlying implementation is certainly open for debate (namedtuple's galore!), but I hope that an open code-sharing / peer review will ameliorate the ugliness, once we settle on how we want the external interface to operate. :)

Example usage:

import sounddevice as sd
# sd.hostapis is a namedtuple, so it supports
# the usual variety of access mechanisms:
# sd.hostapis[ndx].name
# sd.hostapis.coreaudio.name
# getattr(sd.hostapis, 'coreaudio').name

for hostapi in sd.hostapis:
    print hostapi.name # prints "Core Audio" - may this change with future i8n efforts in PortAudio?
    print hostapi.devices # usual list of devices...
    print hostapi.default_input_device
    print hostapi.default_output_device
    print hostapi.apiname # prints "coreaudio" - this will be constant regardless of locale.
    print repr(hostapi.api)
    if hasattr(hostapi.api, 'get_input_channel_name'):
        # Both 'coreaudio' and 'asio' support api.get_input_channel_name(device, chan)
        for device in hostapi.devices:
            num_ch = sd.query_devices(device)['max_input_channels']
            for ch in range(num_ch):
                ch_name = hostapi.api.get_input_channel_name(device, ch)
                print('device {} ch {} input name {!r}'.format(device, ch, ch_name))
    if hasattr(hostapi.api, 'get_output_channel_name'):
        # Both 'coreaudio' and 'asio' support api.get_output_channel_name(device, chan)
        for device in hostapi.devices:
            num_ch = sd.query_devices(device)['max_output_channels']
            for ch in range(num_ch):
                ch_name = hostapi.api.get_output_channel_name(device, ch)
                print('device {} ch {} output name {!r}'.format(device, ch, ch_name))

    # This is another named tuple, containing the functions and
    # CoreAudioSettings class, so we might move *Settings classes
    # out of the global namespace: AsioSettings, CoreAudioSettings,
    # WasapiSettings.

Here's a list of host-API-specific features that I've implemented so far:

asio:
    .api.Settings (AsioSettings)
    .api.get_available_buffer_sizes
    .api.get_input_channel_name
    .api.get_output_channel_name
    .api.set_stream_sample_rate

coreaudio:
    .api.Settings (CoreAudioSettings)
    .api.get_input_channel_name
    .api.get_output_channel_name
    .api.get_buffer_size_range

alsa:
    .api.enable_realtime_scheduling
    .api.get_stream_input_card
    .api.get_stream_output_card
    .api.set_num_periods
    .api.set_retries_busy

wasapi:
    .api.Settings (WasapiSettings)
    .api.get_frames_per_host_buffer

I've only tested coreaudio, and I'd love for other folks to test the others if they already have a hardware setup. I'm open to handing out contributor access to this branch in my fork to expedite the work flow.

@mgeier
Copy link
Member

mgeier commented Apr 26, 2017

Thanks a lot! This looks very promising, but I think there are also quite a few problems we still have to solve ...

AFAIU, this keeps the extra_settings mechanism unchanged, right?

Do you have an idea how the whole thing could be extended in order to solve the problem described in #55?

@dholl
Copy link
Contributor Author

dholl commented Apr 26, 2017

Yes, the commits thus far do not change any current extra_settings behavior, except for making the platform-specific settings classes (AsioSettings, CoreAudioSettings, WasapiSettings) also available as:

hostapis.asio.api.Settings
hostapis.coreaudio.api.Settings
hostapis.wasapi.api.Settings

Aside: I've been wondering about calling these .ExtraSettings instead of .Settings, to mirror the name of extra_settings.

My rationale for putting .Settings (or .ExtraSettings) under .api. is because not all platform API's have equivalent settings structures available in PortAudio. But perhaps we could bring these names names up one level, such as:

hostapis.asio.ExtraSettings
hostapis.coreaudio.ExtraSettings
hostapis.wasapi.ExtraSettings

and if a platform doesn't have an equivalent extra_settings type such as alsa, then we could leave it set to None, such as:

hostapis.alsa.ExtraSettings = None

Anyhow, to address #55 and platform-specific sd.default.extra_settings, I was wondering, could we support something like:

try:
    wasapi = sd.hostapis.wasapi
except AttributeError:
    # wasapi is unavailable on this host / PortAudio installation
    wasapi = None
else:
    wasapi.default_extra_settings = wasapi.ExtraSettings(exclusive=True)

# Do similar for CoreAudio:
try:
    coreaudio = sd.hostapis.coreaudio
except AttributeError:
    # coreaudio is unavailable on this host / PortAudio installation
    coreaudio = None
else:
    coreaudio.default_extra_settings = coreaudio.ExtraSettings(change_device_parameters=True, fail_if_conversion_required=True)

?

Then an application could cover all its bases, by including try/except/else blocks for several platforms in the same code.

Does this sound promising? If so, I'm happy to tweak this PR to include it. (i.e., shall I move .api.Settings to .ExtraSettings, and create a settable .default_extra_settings property?

Assuming we need to stay compatible with the current sd.default.extra_settings in the near future (do we need to allow applications time to transition over if we're not at version 1.0 yet?), perhaps we only use sd.hostapis.*.default_extra_settings if the global sd.default.extra_settings is None?

@mgeier
Copy link
Member

mgeier commented Apr 26, 2017

We don't have to stay compatible with any of the extra_settings stuff, I still consider it experimental (although I admittedly never mentioned that), but it would be nice to produce a deprecation warning/error for some time, so that people don't have to hunt down why their code suddenly breaks.

I don't really like to have yet another place to specify default settings ... and jumping around between extra_settings and sd.hostapis looks a bit complicated.
What about throwing away the whole extra_settings shebang and make the individual contained settings global instead of per-stream?
Sure, this is a limitation in functionality compared to before, but what would we really lose?
For example, let's look a the channel_map you implemented recently:

sd.hostapis.coreaudio.output_channel_map = -1, -1, 0, -1, 1, -1
sd.play(myarray, fs)

And to address #55 (I guess this would take care of the concerns I mentioned over there):

if sd.hostapis.wasapi:
    sd.hostapis.wasapi.output_exclusive = True

That's just brainstorming BTW, I didn't really think this through!

We would have many input/output specific settings that way ... or we could try the 2-tuple thing I did with a few other settings? The idea behind this was that most of the time we don't care or we want the same settings for input and output anyway. The problem is with parameters that are iterables, like channel_map (that's why I used input_mapping and output_mapping in sd.playrec() ... but also, most of the time we don't want the same mapping for input and output, right?).

I'm not sure if we should really combine the existing sd.query_hostapis() functionality with the new hostapi-specific stuff though.
What about re-using the extra_settings name at a different place?

coreaudio = sd.extra_settings.coreaudio
if coreaudio:
    coreaudio.change_device_parameters = True, False

The current query_hostapis() could stay unchanged (and unimportant).

Another thing: how to get quickly from a device to the new settings?
What about replacing the hostapi number in the device dicts with the new host API object?

Still just brainstorming, this probably doesn't make sense at all ...

Over in #84 (comment) you mentioned:

My own preference is for coming up with something that matches 1:1 with PortAudio's host api enumeration, since using the enumeration is guaranteed to give you the expected API.

Yeah, that sounds good, but doing all those AttributeError checks you showed above seems tedious.
I think it would good to be able to do:

if something.something.alsa:
    # do alsa settings
if something.something.wasapi:
    # do wasapi settings

There could be two different things: the enumeration with the same IDs as PortAudio uses (using query_hostapis() or something new, if you prefer) and the attribute-access-thing.

@dholl
Copy link
Contributor Author

dholl commented Apr 26, 2017

Hmm, as the library stands right now, can an application open two streams simultaneously? Perhaps even with something like this contrived example with two devices whose clocks having unsynchronized clocks:

with sd.RawStream(device=0, callback=callback0, ...) as io0, sd.RawStream(device=1, callback=callback1, ...) as io1:
    # callback0 and callback1 maintain ring buffers while another
    # thread performs sample-rate conversion and whatever other
    # processing...
    ...

Otherwise, I'm actually happy with how sounddevice currently permits specifying the platform-specific parameters through the extra_settings= argument, since those platform parameters in the *Settings classes need to be translated and passed to _lib.Pa_OpenStream() anyway.

The part I don't see the need for is having any sort of specifiable defaults at all, so I'd lobby in the other direction to simplify sounddevice by dropping any semblance of supporting defaults. :)

But I admit that there are folks who'd rather set some of these parameters ahead of time, so they don't have the chore to pass around an extra struct. My personal use case for having defaults is if I'm trying something interactive with ipython. However, when I'm writing a larger application, I ideally like to keep my code as modular as possible, even up to the point of permitting folks concurrent access when feasible, but basically, having an underlying library that maintains shared-state between concurrent instantiations drives me crazy so I avoid them like the plague.

With the current defaults in sounddevice, I like that I can just ignore the defaults and specify all parameters whenever I'm creating streams and have 1 less worry concurrency conflict to sort out. (i.e., could it be a race condition if threaded instantiation has to set some global parameters before creating the streams while another instantiation wants to create a stream with different global parameters?).

So, I'm just not a fan of globals when I want deterministic, modular scalability...

Regarding AttributeError checks, yeah, I'd agree they'd get excessive too and was already scheming of a way to support a simpler if sd.hostapis.wasapi: that you illustrated.

How's this:

  • Enumerate available api's:
In [9]: for h in sd.hostapis:
   ...:     print h.apiname
   ...: 
coreaudio
  • Access one specific api:
In [12]: sd.hostapis.coreaudio
Out[12]: _HostAPI(name=u'Core Audio', devices=[0, 1], default_input_device=0, default_output_device=1, apiname='coreaudio', api=COREAUDIO(get_buffer_size_range=<function _api_coreaudio_get_buffer_size_range at 0x101abbcf8>, get_output_channel_name=<function _api_coreaudio_get_output_channel_name at 0x101abbc80>, get_input_channel_name=<function _api_coreaudio_get_input_channel_name at 0x101abbb90>, Settings=<class 'sounddevice.CoreAudioSettings'>))

In [13]: sd.hostapis.coreaudio.name
Out[11]: u'Core Audio'
  • Try to access an unavailable api that is known to sounddevice / PortAudio:
In [16]: sd.hostapis.alsa

In [17]: sd.hostapis.alsa is None
Out[17]: True

In [18]: sd.hostapis.alsa.name
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-eff2123623a0> in <module>()
----> 1 sd.hostapis.alsa.name

AttributeError: 'NoneType' object has no attribute 'name'
  • Now try accessing a name that's not known anywhere:
In [19]: sd.hostapis.bob
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-19-aacc8a7224a6> in <module>()
----> 1 sd.hostapis.bob

AttributeError: 'HostAPIs' object has no attribute 'bob'

I just pushed this to the PR, so now it does support the desire for:

if sd.hostapis.alsa:
    # do alsa settings
if sd.hostapis.wasapi:
    # do wasapi settings

because the try/catch/else checks are tedious. :)

So getting back to:

I don't really like to have yet another place to specify default settings ... and jumping around between extra_settings and sd.hostapis looks a bit complicated.

I agree. :) So, I'd rather not store the settings themselves in sd.hostapis. I could rationalize just storing the class itself (such as AsioSettings) just to give a uniform way to access those platform-specific class names, and thus be able to remove them from your global namespace.

You mentioned wanting to minimize having platform-specific stuff in the global namespace? By renaming those classes such as AsioSettings to _AsioSettings, and then only making them available via sd.hostapis.asio.ExtraSettings, now you'd only have 1 top-level name, hostapis.

Or, I'm fine with also leaving the whole set of Settings classes as they are now, and not bother with moving them around namespaces. But if you wanted to minimize global clutter, this is one alternative. I think I could live with either (sd.AsioSettings vs sd.hostapis.asio.ExtraSettings), but I'd really not want drop the extra_settings= arguments in favor of requiring that all platform settings be made global... (Could I open two different asio devices concurrently where I want to use different channel_selectors in each? If the settings are global, then I must serialize opening them...)

Regarding defaults, Yes, if we're seriously considering permitting folks to set per-platform default Settings, then I think any implementation will start to look a bit complex.

But the whole defaults behavior, is an addition on top of PortAudio. (PortAudio doesn't bother with defaults does it?)

In short, I ideologically like being able to specify extra_settings when I actually create a stream, since it basically maps to calling _lib.Pa_OpenStream(), and I'm not complicating my modularity with shared global state. :) Now whether extra_settings is in the form of these api-specific *Settings classes, or something else like dict's that won't actually get processed until _lib.Pa_OpenStream() gets called, well, that I'm happy to change up.

@dholl
Copy link
Contributor Author

dholl commented Apr 27, 2017

Hmm, I'm still brainstorming on implementation details behind creating a sd.hostapis.*.default. For some reason _InputOutputPair keeps turning into a tuple...

@mgeier
Copy link
Member

mgeier commented Apr 27, 2017

can an application open two streams simultaneously?

Could I open two different asio devices concurrently where I want to use different channel_selectors in each? If the settings are global, then I must serialize opening them...

Exactly, those are the right questions.

I think it's perfectly fine to serialize calls for creating streams if they need different platform-specific settings.
If you do it from different threads, you have to protect the access to the library anyway.
And you probably shouldn't use different threads anyway. PortAudio doesn't really support that.
It doesn't even really support multiple streams in the first place!

The main question for me is: do we preclude certain use cases if we force people to serialize?

And I don't think so ...

Otherwise, I'm actually happy with how sounddevice currently permits specifying the platform-specific parameters through the extra_settings= argument, since those platform parameters in the *Settings classes need to be translated and passed to _lib.Pa_OpenStream() anyway.

In short, I ideologically like being able to specify extra_settings when I actually create a stream, since it basically maps to calling _lib.Pa_OpenStream(),

That's true. I wouldn't say its binding, though.

and I'm not complicating my modularity with shared global state. :)

I don't know how "modular" you'll really be in practice.
If you have two independent modules that both access the same host API at the same time you are bound to get into troubles, regardless if extra_settings is global or not.

The part I don't see the need for is having any sort of specifiable defaults at all, so I'd lobby in the other direction to simplify sounddevice by dropping any semblance of supporting defaults. :)

Duly noted.

But as you say yourself, it is useful for interactive use.
And that is definitely one of the use cases, if not the main one, I had in mind when writing the module.

However, when I'm writing a larger application, I ideally like to keep my code as modular as possible

And that's good.

But I had yet another thing in mind when introducing the module-level defaults:
It's not only for interactive stuff and of course not for large applications, but for little applications in the form of little Python modules.
My idea was that people can write simple modules without having to think about implementing ways to specify custom settings. They can just use the defaults and it probably works for them as is.
However, if you want to use their module and the defaults are not working for you, you can change the settings without changing the foreign code!

import sounddevice as sd
import some_other_module

sd.default.device = 'my special sound card'

some_other_module.run()

I don't know if such a case will ever happen, but I think it is a nice idea, isn't it?

(i.e., could it be a race condition if threaded instantiation has to set some global parameters before creating the streams while another instantiation wants to create a stream with different global parameters?).

So, I'm just not a fan of globals when I want deterministic, modular scalability...

Yes, sounds reasonable.
Except that reality isn't always that nice.

The Python interpreter has global state. Most notably, the sounddevice module is global and the PortAudio library instance, too. And it has plenty of global state in it.

So let's face it, there is global state already.
Does it really matter if we add a little more?

Sure, ideally we wouldn't.
And if we can come up with a great API that doesn't add any, we should go for it!

But for me, having global state or not is just one of many aspects we should weigh against each other to get an as "good" as possible API in the end.

So, I'd rather not store the settings themselves in sd.hostapis.

Except that settings are already stored there, e.g. by alsa.enable_realtime_scheduling().

You mentioned wanting to minimize having platform-specific stuff in the global namespace?

Yes, that would be nice, but it shouldn't be the main concern.
Moving the settings classes to some sub-namespace sounds good, but probably we can remove them altogether?

Regarding defaults, Yes, if we're seriously considering permitting folks to set per-platform default Settings, then I think any implementation will start to look a bit complex.

But this isn't really complex, is it?

if sd.hostapis.wasapi:
    sd.hostapis.wasapi.exclusive = True

Creating a special object and passing it to the stream constructor seems more complex to me.

Or are you talking about the complexity of the implementation?
There indeed will be some complexity, but I think nothing we can't handle.

But the whole defaults behavior, is an addition on top of PortAudio. (PortAudio doesn't bother with defaults does it?)

Yes, it is an addition.
And I think the current sd.default... stuff was worth adding.

And we'll see if some host API specific defaults will be considered worth adding.

@dholl
Copy link
Contributor Author

dholl commented Apr 27, 2017

Or are you talking about the complexity of the implementation?

Yep, I was referring to the complexity of the implementation. :) Which, I'm also fine with, but I'm only slightly wary of long term code maintainability. (i.e., the ability for general humanity to grok the underlying plumbing.)

So, I am fine with keeping the current global default (sd.default) and thus won't lobby for its demise. However, I would hope to keep extra_settings on stream constructors for now... .oO( Please give me some time to emotionally come to terms with the prospect that stream constructor extra_settings may pass away someday with my life becomes further dependent on globals! ) ;)

If in the process of implementing hostapi-specific extra_settings that we find that the implementation gets to be a pain to support that extra extra_settings= constructor arg, then I'm happy to reconsider dropping it. But yeah, I would like a little more time to mull over getting rid of the constructor arg. (speaking as though I was the owner of this library, which I'm not... so it is not my final decision here. but requiring me to use a shared global state to instantiate individual objects would make me reconsider running for the hills to live out my days as a hermit) :)

However, if you want to use their module and the defaults are not working for you, you can change the settings without changing the foreign code!
...
I don't know if such a case will ever happen, but I think it is a nice idea, isn't it?

Yes, it is a nice idea. Nice enough to support api-specific defaults in my opinion. :) So let's keep defaults and I suspect that this particular use case would benefit from permitting hostapi-specific defaults. Ahh, instead of storing default extra_settings under sd.hostapis... We're primarily concerned with extra_settings type stuff, right? Would this usage scenario be desirable?

import sounddevice as sd
import some_other_module

sd.default.device = 'my special sound card'
# Apply greedy performance defaults on various platforms:
sd.default.extra_settings.coreaudio = sd.CoreAudioSettings(
    change_device_parameters=True, fail_if_conversion_required=True)
sd.default.extra_settings.wasapi = sd.WasapiSettings(exclusive=True)
...

some_other_module.run()

Or would we like to get rid of the *Settings classes completely (at least from a user perspective), to support something like this:

# Apply greedy performance defaults on various platforms:
sd.default.extra_settings.coreaudio.change_device_parameters=True
sd.default.extra_settings.coreaudio.fail_if_conversion_required=True
sd.default.extra_settings.wasapi.exclusive=True

Basically, could we make it an error for a user to sd.default.extra_settings and instead, require that they assign to sd.default.extra_settings.some_api_here? The extra_settings are platform dependent.

If anything for clarity, perhaps we'd search-n-replace replace extra_settings with hostapi_settings in the code, including stream constructor arguments. This would make it clear that these settings are dependent upon the hostapi.

# Apply greedy performance defaults on various platforms:
sd.default.hostapi_settings.coreaudio.change_device_parameters=True,True
sd.default.hostapi_settings.coreaudio.fail_if_conversion_required=True,True
sd.default.hostapi_settings.wasapi.exclusive=True,True

Regarding PortAudio's thread support, it doesn't like they necessarily poo-poo the idea:
https://app.assembla.com/wiki/show/portaudio/Tips_Threading
Here, I see an opportunity that sounddevice could provide some value added benefit perhaps by adding a few additional mutexes here and there. For example from their web page:

Calls to open and close streams are definitely not thread safe on any platform. This means that if you plan to open and close streams from more than one thread, you will need to provide a locking mechanism to ensure that you don’t open or close streams in different threads at the same time.

Would adding a mutex around the 2 calls to Pa_OpenStream() and Pa_CloseStream() be beneficial? (Each of those functions are called only once in sounddevice.)

The discussion on a multi-thread safe extension to a cross-platform sound library such as sounddevice is likely best left for another day, but for now, I'd hope that we don't impose too many limits on ourselves by removing extra_settings= from stream constructors.

@mgeier
Copy link
Member

mgeier commented Apr 28, 2017

I would like to clarify something. I'm not insisting on adding global state, what I'm saying is: "If the best API we can come up with adds global state, so be it."

Putting the stream initialization parameters aside for a bit, let's talk about the other host-API-specific functionality you are mentioning in your original comment.

The most pressing question for me is: how will a user access those features?
Should it be through the host-API enumeration feature (that's currently called query_hostapis()) or through a separate and new API?
If latter, should we remove query_hostapis() at some point?

I think the idea of using sd.query_hostapis(name='Core Audio') (#84) is too clumsy, I do like the idea of using attribute access instead of strings to select features from different host APIs.
However, since this is a feature that will probably not so often be used in interactive sessions, it's probably OK to use strings? But then we should define an a-priori known set of strings instead of taking the "official" strings.

If we use attribute access, having both sd.query_hostapis() and sd.hostapis.xyz.abc() also looks strange.
That's why I thought using sd.extra_settings.xyz... would be good, but actually it's not, since several features (most, actually) are for getting information rather than setting it.
For the same reason, it doesn't make sense to put this stuff into sd.default....

Are there other names we could use?
"host" instead of "hostapi"?
Something like "platform", "backend", ...?
Or some more generic term like "extra", "special", "addon", ...?

@mgeier
Copy link
Member

mgeier commented Apr 28, 2017

To get an idea what we are dealing with, I've made (hopefully complete) list of all the currently available host-API-specific settings: #4 (comment).

@mgeier
Copy link
Member

mgeier commented Apr 28, 2017

If we combine the host API enumeration and the host-API-specific features, we should consider that there are some host APIs that don't have specific settings, e.g. paSoundManager, paAL.
I don't know if those really exist, though. I've never seen them or heard of anyone having seen them ...

@dholl
Copy link
Contributor Author

dholl commented Apr 28, 2017

The most pressing question for me is: how will a user access those features?

Ideally, whatever strikes some balance between user keystrokes and library maintenance. :)

For example, 1 small change that could happen right now might be to change query_hostapis() to return namedtuple instead of dict, because the contents of those dict's always use the same keys. This could let folks access known keys via:

  • hostapi_names = [hostapi.name for hostapi in query_hostapis()]
    instead of currently as:
  • hostapi_names = [hostapi['name'] for hostapi in query_hostapis()]

This same change could be applied to have query_devices() returning named tuples as well. :)

It just depends on if you want folks to be able to use slightly shorter syntax:

  • query_devices(device, 'output').max_output_channels
    vs
  • query_devices(device, 'output')['max_output_channels']

My own preference is for the slightly shorter user-syntax, especially since the returned dict keys is always the same for each of query_hostapis() and query_devices().

So here, my own preference would be for returning a namedtuple instead of a dict, and the functionality of namedtuple is close enough to dict for our use cases:

  • namedtuple offers the equivalent access to each field (such as by .max_input_channels instead of ['max_input_channels']
  • namedtuple still permits access by string using getattr().
  • and if really needed, namedtuple still permits discovering/iterating over fields such as with ._asdict() or ._fields

So, the functionality of namedtuple is on par with dict, but shortens user-keystrokes from ['blah'] to .blah. :)

Should it be through the host-API enumeration feature (that's currently called query_hostapis()) or through a separate and new API?
If latter, should we remove query_hostapis() at some point?

Your choice. :)

I wager that identical access to the host-API-specific functions can be made through the current host-API enumeration mechanism sd.query_hostapis() or through a different namespace such as sd.hostapis. So I threw together this PR to provide a concrete, hands-on example of both to aid discussion.

As this PR stands right now, identical functionality is available from both sd.query_hostapis() and sd.hostapis. Examples:

  • Enumerating over available hostapi's:

    for hostapi in sd.query_hostapis():
        print(hostapi['max_output_channels'])
    for hostapi in sd.hostapis:
        print(hostapi.max_output_channels)
  • Calling a function from a device's hostapi using its hostapi index:

    hostapi_ndx = query_devices(device, 'output')['hostapi']
    sd.query_hostapis(hostapi_ndx)['api'].get_input_channel_name(...)
    sd.hostapis[hostapi_ndx].api.get_input_channel_name(...)
  • Calling a function with the assumption of a hard-coded hostapi:

    sd.query_hostapis(apiname='coreaudio')['api'].get_input_channel_name(...)
    sd.hostapis.coreaudio.api.get_input_channel_name(...)

I don't see the need to keep both. I'm partial to the shorter notation of sd.hostapis, but I could live with the longer sd.query_hostapis() instead.

So regarding the broad category of "host-API-specific functionality", could we (or should we not?) divide this topic into two subareas? Specifically:

  1. The extra_settings stuff that must be passed to _lib.Pa_OpenStream() during stream creation.
  2. These other PortAudio functions that can be invoked before or after stream creation, such as
    • ASIO's get_available_buffer_sizes(), get_input_channel_name(), get_output_channel_name(),
    • WASAPI's get_frames_per_host_buffer(),
    • CoreAudio's get_buffer_size_range(), get_input_channel_name(), get_output_channel_name(),
    • ALSA's etc... :)

I was originally thinking along these lines, because the first category is only used during stream instantiation and not at other times, so I just assumed to keep these categories in 2 different realms to avoid ambiguity. At the moment, this PR does happen to keep these areas separate.

  • realm 1 includes the current *Settings classes
  • realm 2 is what I drafted through sd.hostapis[ndx].api.* and sd.query_hostapis(nix)['api'].*

Are you pondering merging these 2 realms? For example, putting ASIO's channel_selectors into the same namespace/grouping/whatever with ASIO's get_input_channel_name()?

Hmm, colocating CoreAudio's channel_map and change_device_parameters with get_buffer_size_range()? Changing CoreAudio's channel_map after stream creation won't change any old stream, right? Hmm, but then again, CoreAudio does not offer any functions that would change run-time behavior... (unless I missed something when I did my first skim over PA's hostapi functions)

I'm taking a step back for a moment and asking "why the heck not?"

Hmm... ASIO does have a curious function called PaAsio_SetStreamSampleRate(), which this PR provides access via sd.hostapis.asio.set_stream_sample_rate(stream, sample_rate); could this function really change the sample rate after stream creation? Interesting, looks like ASIO can change sample rate with certain drivers: http://portaudio.com/docs/v19-doxydocs/pa__asio_8h.html#a838e2a8ed83db7cf5f2864492f3c78fc

OK... Why not? :) cringe... it feels dirty to "cross the streams" so to speak... but I'm not sure my own aversion is founded this time...

So.... what if everything was thrown into the same namespace...

Ahh, should things like ASIO's channel_selectors and CoreAudio's channel_map be settable per-device? Such as, could we go a step further, and extend query_devices() to provide access to a device's properties?

sd.devices[device_index].name
sd.devices[device_index].hostapi -> reference to sd.hostapis[...]
sd.devices[device_index].max_input_channels
sd.devices[device_index].samplerate -> defaults to default_samplerate
...
sd.devices[device_index].channel_map # this is only available on coreaudio devices
sd.devices[device_index].channel_selectors # this is only available on asio devices
...

uh.. hm. samplerate... What if we made samplerate be a settable global, and no longer a stream parameter, such that we don't even have a set of "defaults" any more? For example, remove all settings-related init arguments of _StreamBase and make 'em globals? Hm, and remove the device parameter as well? I suppose this would make usage of sounddevice sort of like filling out a form, and then using a with-block to actually open/close devices. (I'm still not sure I like this, but I'm mentally pursuing the thought to whatever ends...)

sd.device = my_favorite_interface
sd.channels = 2
sd.channel_map = [2, 3], [-1, -1, 0, 1, -1, 0, -1, ...] # because this is a coreaudio device, so this specific name is available here with the quirky annoyance that the underlying PortAudio exposes from the underlying CoreAudio mapping requiring -1's in the output list?
sd.dither = True  # This defaults to true anyway...
sd.prime_output_buffers_using_stream_callback = True # Would otherwise default to False...
with sd.StreamRaw as stream0:
    ....

?

Would sounddevice ever want to support opening multiple streams, such as:

sd.device = my_favorite_interface
...
with sd.StreamRaw as stream0:
    sd.device = my_other_favorite_interface
    with sd.StreamRaw as stream1:
        ...

Would it still be nice if all of these relevant settings could be suppled at creation time, such as:

# some_module_A.py
import sounddevice as sd

param_set0 = sd.StreamParameters()
param_set0.device = ...
...
param_set1 = sd.StreamParameters()
param_set1.device = ...
...
with sd.StreamRaw(param_set0) as stream0, sd.StreamRaw(param_set1) as stream1:
    ...

And could we support your external-module-overriding-defaults use-case via something like being able to "push" default StreamParameters onto a stack?

# some_module_B.py
import sounddevice as sd
param_set = sd.StreamParameters()
param_set.change_device_parameters = True # CoreAudio-specific
param_set.fail_if_conversion_required = True # CoreAudio-specific
param_set.exclusive = True # WASAPI-specific

sd.push_default_params(param_set)
import some_module_A
sd.pop_default_params()

# Or accomplish the same via a with-block to perform implicit pop:
with sd.push_default_params_context(param_set):
    import some_module_A
# pop is implicitly performed here.

And allow some way to layer-up the stack of defaults so that we could even allow yet another module to do the same to some_module_B?

# some_module_C.py
import sounddevice as sd
param_set = sd.StreamParameters()
param_set.samplerate = 48000
with sd.push_default_params_context(param_set):
    import some_module_B

Perhaps the above could be implemented with sd.StreamParameters() returning a dict-like thing that offers .name access to fields, but still keeping track of which fields haven't been set at all, so that later when some_module_A finally goes to open a device, something like this occurs to combine the layers together:

# somewhere in the depths of
# class _StreamBase(object)
#     __init__(self, stream_params=None):
# Pseudo-code:
final_stream_params = {}
# Layer on each stack level of default settings, so that
# settings in newer levels override those in older levels:
for stack_level in stack_of_defaults:
   final_stream_params.update(stack_level._asdict())
# Now if the caller provided a stream_params as an argument to a class derived
# from _StreamBase, then we should apply their most recent stream_params on top:
if stream_params is not None:
    final_stream_params.update(stream_params._asdict())
# Now go use the final_stream_params dict to create a stream...

This would provide a way for some_module_A to open a stream with their explicit parameter sets, and then let any unspecified settings default to however the stack-of-defaults plays out from some_module_B and some_module_C.

@tgarc
Copy link
Contributor

tgarc commented Apr 30, 2017

Oh man, I've really fallen behind D: Let me try and catch up here. I've listed some comments below to things I read that made my hairs raise :P

One kind of general comment about this commit: I think this should focus on the change of API and re-implementing the existing host api extensions. We shouldn't just glob in brand new support for all these other untested host-api features. And as a side note, we really are going to need a test suite soon. Dammit.

On to my specific comments..

... And you probably shouldn't use different threads anyway. PortAudio doesn't really support that.

You're kind of saying two different things here. There's nothing wrong with a mult-threaded application that uses portaudio. The authors are just saying "we do not provide thread safety to the API". I don't think that's a discouragement to use multiple threads, they just don't want to provide any concurrency guarantees. I think it's very discouraging (and unfounded) to say that people "just shouldn't use multiple threads". (In fact, you won't find any statement to that effect in the portaudio documentation).

Hmm, as the library stands right now, can an application open two streams simultaneously?

Yes.

It doesn't even really support multiple streams in the first place!

I don't think that's accurate. What if I have two different devices I want to play out from and I don't care if they're synchronized? I think that's a perfectly reasonable use case and one that portaudio does support. More complicated use cases of having multiple synchronized streams is also (reportedly) possible on some platforms, but I can't really speak to that.

there are some host APIs that don't have specific settings, e.g. paSoundManager, paAL.
I don't know if those really exist, though.

I found an old listing here that spells out the libraries supported. AL is apparently Silicon Graphics Audio Library which seemingly met its demise quite a while ago. And SoundManager is a pre-osx audio api for mac. So I think it's okay to drop any semblance of support for these libraries.

However, I would hope to keep extra_settings on stream constructors for now.

I second that. I think it should only be removed if we can find another non-global way to implement configuration of these parameters. I see defaults as mostly a way to appease users that want to use the play/rec/playrec convenience functions. IMHO, I don't think it's very useful for general development.

... perhaps we'd search-n-replace replace extra_settings with hostapi_settings in the code, including stream constructor arguments.

I'm in agreement with this. extra_settings is unnecessarily ambiguous. We could deprecate it to avoid breaking backwards compatibility.

@tgarc
Copy link
Contributor

tgarc commented Apr 30, 2017

I'm actually a fan of the extra_settings implementation for the most part. What's really lacking IMO is support for

(1) Host specific functions that are not necessarily tied to a particular stream like: PaAsio_GetInputChanneName, PaAsio_GetAvailableBufferSizes (and similar functions for alsa/coreaudio/etc.)

(2) Host specific functions that are stream specific like PaAsio_GetInputChannelName or PaAlsa_GetStreamInputCard. (PaAsio_SetStreamSampleRate - a stream specific setter function - seems to be an outlier but should be treated similar to the getters).

Now, I would think that ideally, from the perspective of a user, I would want settings in category (2) to be attributes or functions that are bound to the Stream class itself. But this I think would require that _StreamBase be replaced with a factory function/class that produces host specific Streams. So in the end you would have stream classes for each API. For example, you might implement GetStreamInputCard like this:

class AlsaStream(_StreamBase):
    @property
    def input_card(self):
        return _lib.PaAlsa_GetStreamInputCard

The obvious downside is that it would significantly increase i) code size, ii) complexity, and iii) namespace pollution. One way to address (iii) would be to put the host specifc stream classes in a submodule (streams) while still leaving the base stream classes as factory functions. And we could potentially address (i) and (ii) by using some templated code that enumerates the APIs and dynamically generates the stream classes.

That's kind of the 'brute force' approach.

Looking in the other direction, a very straightforward to implement approach would be to simply add the host API functions as static methods attached to the *Settings classes. That would mean usage would look something like this:

sd.AlsaSettings.set_num_periods(8)
apiconf = sd.AlsaSettings(realtime=True)
s = sd.Stream(hostapi_settings=apiconf)
input_card = sd.AlsaSettings.get_input_card(s)

Which is kind of unintuitive and certainly unpythonic but is fully functional with minimal implementation and maintenance effort.

I don't think either of these on their own are implementation worthy, but I'm just thinking out loud.

@mgeier
Copy link
Member

mgeier commented May 5, 2017

Thanks a lot @dholl and @tgarc for the brainstorming and the many great suggestions!

I'm OK with keeping extra_settings, unless we find a much better alternative that doesn't need them.

I'm OK with renaming them, but only if it's worth the change.
I think hostapi_settings is not a good alternative, since all settings, including e.g. sampling rate, are "host API settings". Those other settings are just "extra" settings, they are "host API specific", they are ...?

I would like to keep the portable features from PortAudio separate from the non-portable ones.
I'm OK with the non-portable ones being second class citizens.

I don't insist on being able to specify multiple extra_settings at the same time, and since #55 was basically withdrawn, we have less motivation to implement that.

@dholl

Here, I see an opportunity that sounddevice could provide some value added benefit perhaps by adding a few additional mutexes here and there.

I'm very reluctant to add mutexes, most likely we make it worse with that.

I think if users want to use multiple threads, they should be responsible for any necessary locking or whatever.

change query_hostapis() to return namedtuple instead of dict

I think a namedtuple isn't a good match, since it doesn't really have tuple character.
But we could manually add attribute access.
I consider this an unimportant detail for now.

I think we could get rid of query_hostapis() and replace it by a module-level object sd.hostapis (as you already suggested above).
For this object we could provide numeric subscripting, to get a host API by ID, e.g.

myapi = sd.hostapis[0]

This would automatically (if we also provide __len__()) allow iteration.

In addition to that, we could allow dict-like access:

myapi = sd.hostapis['alsa']

This would raise a KeyError if a given host API is not available.
For easier usage, we could also provide a get() method, like on dict:

alsa = sd.hostapis.get('alsa')
if alsa:
    alsa.whatever()

Therefore, this object could be used like a tuple or like a dict. Or both.
It doesn't have to be populated at initialization time, the returned objects could be created on demand.

I don't think attribute access (like sd.hostapis.alsa) is necessary here, because it's not very important for interactive usage.

I think it's important to provide a fixed set of host API strings in the documentation, to avoid confusion.

Now the question is how such a "host API" object should look like.

I don't really like the detour alsa.api.do_something() you suggested above, I think it would be better to drop one level.

What about keeping dict-like access for the properties we already had before, like e.g. alsa['max_output_channels']?
The additional host API specific functions could be provided as methods, like alsa.input_card(mystream).

So regarding the broad category of "host-API-specific functionality", could we (or should we not?) divide this topic into two subareas? Specifically:

  1. The extra_settings stuff that must be passed to _lib.Pa_OpenStream() during stream creation.
  2. These other PortAudio functions that can be invoked before or after stream creation, such as
    * ASIO's get_available_buffer_sizes(), get_input_channel_name(), get_output_channel_name(),
    * WASAPI's get_frames_per_host_buffer(),
    * CoreAudio's get_buffer_size_range(), get_input_channel_name(), get_output_channel_name(),
    * ALSA's etc... :)

That's the question!

I'm OK with having separate realms, there is just one special case: PaAlsa_EnableRealtimeScheduling. I think it would be nice to be able to set this as a default before opening a stream.

Are you pondering merging these 2 realms?

Yes I am. But probably it's not a good idea.

Ahh, should things like ASIO's channel_selectors and CoreAudio's channel_map be settable per-device? Such as, could we go a step further, and extend query_devices() to provide access to a device's properties?

I don't know. It indeed sounds cringeworthy.
I'm hesitant, but I don't want to dismiss it yet.

I suppose this would make usage of sounddevice sort of like filling out a form, and then using a with-block to actually open/close devices. (I'm still not sure I like this, but I'm mentally pursuing the thought to whatever ends...)

This would take it to the extreme, and it sounds strange.
Still, I don't see a problem in having it both ways: on the one hand individual parameters, on the other hand sd.default settings.

Your example with param_set0 and param_set1 is already possible. Just make a dict of parameters and use it like this:

with sd.Stream(**param_dict0):
    ...

Now the thing with the parameter stack really seems to get out of control, but hey, we are brainstorming! So it's good!
I'm not very enthusiastic about this idea though.

You basically can do all this already.
You can create dictionaries with settings and update them as you wish, where previous settings will be overwritten.
You can also overwrite the settings in sd.default multiple times, again overwriting the previous settings.
You indeed cannot "roll back" so some intermediate previous state, but would that really be helpful?

@tgarc

I think this should focus on the change of API and re-implementing the existing host api extensions. We shouldn't just glob in brand new support for all these other untested host-api features.

Well I already partially implemented this when introducing extra_settings. And look where this got us. Now we have to revisit all this again.
I would really like it if we could come up with a solution that takes care of this topic for good.
We don't have to actually implement every single feature (and there might even be new features in the future, who knows), but we should at least think about all features and come up with an API that at least theoretically could accommodate all of them.

And as a side note, we really are going to need a test suite soon. Dammit.

Oh yes! Any volunteers?

I don't think that's a discouragement to use multiple threads, they just don't want to provide any concurrency guarantees. I think it's very discouraging (and unfounded) to say that people "just shouldn't use multiple threads".

You are right, my bad.
Let's just also say "don't want to provide any concurrency guarantees", shall we?

It doesn't even really support multiple streams in the first place!

I don't think that's accurate. What if I have two different devices I want to play out from and I don't care if they're synchronized? I think that's a perfectly reasonable use case and one that portaudio does support. More complicated use cases of having multiple synchronized streams is also (reportedly) possible on some platforms, but I can't really speak to that.

Sorry, I was wrong to say it like that.
I just don't want to make more guarantees than PortAudio does.
And it is kinda hard to find that out, since PortAudio isn't really explicit about that.

I would like to mainly advertise the platform-independent features, because that's what's great about PortAudio.
Of course I don't want to keep people from using any feature of PortAudio, but they should know what they're doing!

(1) Host specific functions that are not necessarily tied to a particular stream like: PaAsio_GetInputChanneName, PaAsio_GetAvailableBufferSizes (and similar functions for alsa/coreaudio/etc.)

(2) Host specific functions that are stream specific like PaAsio_GetInputChannelName or PaAlsa_GetStreamInputCard. (PaAsio_SetStreamSampleRate - a stream specific setter function - seems to be an outlier but should be treated similar to the getters).

Now, I would think that ideally, from the perspective of a user, I would want settings in category (2) to be attributes or functions that are bound to the Stream class itself.

I dislike this idea quite a lot.
I'd like to keep the platform-specific stuff separated from the general use stuff.

Having host-API-specific stream classes would defeat the purpose of using a platform-independent library in the first place, wouldn't it?

If you want to re-implement everything in Python, I think it would be better to actually use the platform-native APIs, like it's done in https://github.com/bastibe/Python-Audio. But I still think using PortAudio is the better strategy.

Looking in the other direction, a very straightforward to implement approach would be to simply add the host API functions as static methods attached to the *Settings classes.

I'm surprised that you seem to strongly dislike global state, yet a class object having state would be OK ...

Which is kind of unintuitive and certainly unpythonic

Yeah, indeed.

but is fully functional with minimal implementation and maintenance effort.

Right. But it is really ugly.

Back to your points (1) and (2) above, what about the following?

asio = sd.hostapis['asio']
name = asio.input_channel_name(1, 5)
a, b, c, d = asio.available_buffer_sizes(1)
asio.change_samplerate(mystream, 48000)

# and, on another system:

idx = sd.hostapis['alsa'].input_card(mystream)

And to get rid of the *Settings classes, we could try something like this:

ca_settings = sd.hostapis['coreaudio'].extra_settings(change_device_parameters=True, fail_if_conversion_required=True)
sd.play(myarray, extra_settings=ca_settings)

This way, ca_settings could be the raw CFFI structure, no need to wrap it into a special class.

@dholl
Copy link
Contributor Author

dholl commented May 6, 2017

change query_hostapis() to return namedtuple instead of dict

I think a namedtuple isn't a good match, since it doesn't really have tuple character.

Uh, yes it does according to isinstance(). :)

zero(ttys002):...Python/sound> ipython
Python 2.7.10 (default, Feb  6 2017, 23:53:20) 
Type "copyright", "credits" or "license" for more information.

IPython 5.3.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: import sounddevice as sd

In [2]: isinstance(sd.hostapis, tuple)
Out[2]: True

But we could manually add attribute access.
I consider this an unimportant detail for now.

...

Please please please read up on namedtuple. I find that is is a very nice container for its combination of a low implementation overhead and minimal user fuss with ['...'] stuff. :)

For this object we could provide numeric subscripting, to get a host API by ID, e.g.

myapi = sd.hostapis[0]

The namedtuple-based implementation in this pull request already supports this. :)

In [3]: sd.hostapis[0]
Out[3]: _HostAPI(name=u'Core Audio', devices=[0, 1, 2], default_input_device=0, default_output_device=1, apiname='coreaudio', api=COREAUDIO(get_buffer_size_range=<function _api_coreaudio_get_buffer_size_range at 0x10e9d7320>, get_output_channel_name=<function _api_coreaudio_get_output_channel_name at 0x10e9d72a8>, get_input_channel_name=<function _api_coreaudio_get_input_channel_name at 0x10e9d71b8>, Settings=<class 'sounddevice.CoreAudioSettings'>))

In [4]: sd.hostapis[0].devices
Out[4]: [0, 1, 2]

In [5]: sd.hostapis[0].apiname
Out[5]: 'coreaudio'

In [6]: sd.hostapis[0].api.get_input_channel_name(2, 8)
Out[6]: u'Mic 9'

This would automatically (if we also provide len()) allow iteration.

Yep, the current PR supports len() as well:

In [7]: len(sd.hostapis)
Out[7]: 1

In addition to that, we could allow dict-like access:

myapi = sd.hostapis['alsa']

Yep the PR demonstrates this too -- including if someone really wants to use a strings like 'alsa' or 'coreaudio':

In [8]: sd.hostapis._asdict()['coreaudio']
Out[8]: _HostAPI(name=u'Core Audio', devices=[0, 1, 2], default_input_device=0, default_output_device=1, apiname='coreaudio', api=COREAUDIO(get_buffer_size_range=<function _api_coreaudio_get_buffer_size_range at 0x10e9d7320>, get_output_channel_name=<function _api_coreaudio_get_output_channel_name at 0x10e9d72a8>, get_input_channel_name=<function _api_coreaudio_get_input_channel_name at 0x10e9d71b8>, Settings=<class 'sounddevice.CoreAudioSettings'>))

In [9]: sd.hostapis._asdict()['coreaudio'].devices
Out[9]: [0, 1, 2]

But I'd recommend dropping the whole dict-step:

In [10]: coreaudio = sd.hostapis.coreaudio

In [11]: coreaudio.devices
Out[11]: [0, 1, 2]

This would raise a KeyError if a given host API is not available.

Note: This PR used to raise an AttributeError on unavailable hostapi's, until I implemented a small wrapper (just a subclass) to return None for unavailable hostapi's. (to reduce the plethora of try/catch blocks) Here the commit where I implemented the subclass wrapper: 45842ee

Note that this commit does not (should not?) interfere with len() or other access to / iteration over the namedtuple's fields.

So with this commit, this syntax is currently supported:

alsa = sd.hostapis.alsa
if alsa:
    alsa.whatever()

If someone doesn't like the None-behavior I added, you can still invoke the exception behavior by trying to access 1 level deeper into the hostapi's struct. In this example, I'm running on my laptop where I only have coreaudio, and no alsa, but please try this on another platform to verify you get equivalent behavior:

In [21]: alsa_api = sd.hostapis.alsa.api
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-21-61cce51548fa> in <module>()
----> 1 alsa_api = sd.hostapis.alsa.api

AttributeError: 'NoneType' object has no attribute 'api'

In [22]: coreaudio_api = sd.hostapis.coreaudio.api

In [23]: print coreaudio_api
COREAUDIO(get_buffer_size_range=<function _api_coreaudio_get_buffer_size_range at 0x10e9d7320>, get_output_channel_name=<function _api_coreaudio_get_output_channel_name at 0x10e9d72a8>, get_input_channel_name=<function _api_coreaudio_get_input_channel_name at 0x10e9d71b8>, Settings=<class 'sounddevice.CoreAudioSettings'>)

Therefore, this object could be used like a tuple or like a dict. Or both.

Yep, please read up on namedtuple. :) Despite its unintuitive name, it is already part of Python's "batteries included" standard library and appears to provide just about all our desired functionality.

It doesn't have to be populated at initialization time, the returned objects could be created on demand.

True. But is it possible for any of the returned data from query_hostapis() to ever change after _lib.Pa_Initialize() gets called? If not, then a namedtuple seems the simplest at the moment. :)

I don't think attribute access (like sd.hostapis.alsa) is necessary here, because it's not very important for interactive usage.

For interactive usage, I like that I can use tab-completion with the current PR:

In [13]: sd.hostapis.
       sd.hostapis.al              sd.hostapis.count           sd.hostapis.oss              
       sd.hostapis.alsa            sd.hostapis.directsound     sd.hostapis.soundmanager     
       sd.hostapis.asio            sd.hostapis.indevelopment   sd.hostapis.wasapi           
       sd.hostapis.audiosciencehpi sd.hostapis.index           sd.hostapis.wdmks            
       sd.hostapis.beos            sd.hostapis.jack                                         
       sd.hostapis.coreaudio       sd.hostapis.mme                                         

sd_hostapis

sd_hostapis_coreaudio

sd_hostapis_coreaudio_api

A dict-style access couldn't do tab completion, right? How's that for interactive user experience? :)

I think it's important to provide a fixed set of host API strings in the documentation, to avoid confusion.

Yep. That's easilly fixed. Which doc string should I tweak? :) Given that the binding to each _lib.Pa_... has to be manually created in sounddevice.py, some doc string can get updated at the same time.

What if I add some descriptive text to the doc string I already created on sd.hostapis? :)

In [20]: sd.hostapis?
Type:        HostAPIs
String form: HostAPIs(coreaudio=_HostAPI(name=u'Core Audio', devices=[0, 1, 2], default_input_device=0, defaul <...> audio_get_input_channel_name at 0x10e9d71b8>, Settings=<class 'sounddevice.CoreAudioSettings'>)))
Length:      1
File:        ~/Library/Python/2.7/lib/python/site-packages/sounddevice.py
Docstring:   Access to PortAudio Host API's

In [21]: 

Back to your points (1) and (2) above, what about the following?
...

So this last example can already be rewritten as:

asio = sd.hostapis.asio.api
name = asio.get_input_channel_name(1, 5)
a, b, c, d = asio.get_available_buffer_sizes(1)
asio.set_stream_sample_rate(mystream, 48000)

# and, on another system:

idx = sd.hostapis.alsa.api.get_stream_input_card(mystream)

And to get rid of the *Settings classes, we could try something like this:

And again, this is also directly supported in the current PR:

ca_settings = sd.hostapis.coreaudio.api.Settings(change_device_parameters=True, fail_if_conversion_required=True)
sd.play(myarray, extra_settings=ca_settings)

This way, ca_settings could be the raw CFFI structure, no need to wrap it into a special class.

Heh, I wish it could be a raw CFFI structure. :) But unfortunately, the raw structures to contain pointers to other raw CFFI data sush as AsioSettings._selectors and CoreAudioSettings._channel_map which mustn't be freed until after the respective *._streaminfo data had been consumed by Pa_OpenStream(). Is this correct? (I noticed the must be kept alive comments lurking around the code.)

However, I do think it is technically possible to rearrange the current code a little bit to replace the *Settings classes with (gasp) namedtuple's, and only do the conversion to CFFI structures just before calling Pa_OpenStream() in _StreamBase.

Implementation note: Instantiating a namedtuple requires that a user specify all fields, but I think this would get annoying to a library user such as if they don't want to specify everything, especially in the case of coreaudio's Settings. So I'd tweak the namedtuple to default unspecified fields to None, so that a user doesn't need to specify every setting. For an example, check out http://stackoverflow.com/questions/11351032/named-tuple-and-optional-keyword-arguments

@mgeier
Copy link
Member

mgeier commented May 7, 2017

@dholl Sorry that I was unclear, there seems to have been a misunderstanding.

change query_hostapis() to return namedtuple instead of dict

I think a namedtuple isn't a good match, since it doesn't really have tuple character.

Uh, yes it does according to isinstance(). :)

I see now where you misunderstood me, because I was too unclear: you thought that "it" means namedtuple, whereas I meant "the thing that was previously a dict", i.e. "some kind of host API object".

To clarify my position: I think a namedtuple would be possible (but not ideal) for the enumeration of hostapis but not at all suitable for storing the properties of a single hostapi.

Because the set of host API properties doesn't have tuple character.

Please please please read up on namedtuple.

No need, I know namedtuple very well, and I try to use it wherever appropriate.
Here, I think, it isn't appropriate, because it would force us to query the host APIs at initialization time, and I don't see any advantage in doing that.

I know that your current suggestion supports indexing and iteration, and that's good.

In general, supporting attribute access would also be interesting, but we definitely need a way to find out if a given host API is currently available or not.
We could simply return None if it's not available (as you did), which would violate the rule of least surprise, or we could raise an exception, which would be the normal thing, but it would be tedious to use.
Therefore I suggested to drop attribute access and keep a dict-like interface.
This would also raise an exception when used like sd.hostapis['unexisting'], but the exception could be avoided by using sd.hostapis.get('unexisting').

I agree that attribute access is easier to type in most cases, but I think that's not important here, since this will seldom be needed in interactive use cases.

Therefore, this object could be used like a tuple or like a dict. Or both.

Yep, please read up on namedtuple. :) Despite its unintuitive name, it is already part of Python's "batteries included" standard library and appears to provide just about all our desired functionality.

Sorry, I was unclear again. Saying "could be used like a tuple" was a bit too strong. I wouldn't want to provide the whole tuple API. Namely, I wouldn't implement .count() and .index(), which IMHO wouldn't make sense here and would only confuse users.

It doesn't have to be populated at initialization time, the returned objects could be created on demand.

True. But is it possible for any of the returned data from query_hostapis() to ever change after _lib.Pa_Initialize() gets called?

I'm quite sure it isn't.

If not, then a namedtuple seems the simplest at the moment. :)

It is indeed simple, but I don't like it.
I don't like to run unnecessary code at initialization, unless there is a really good reason to do so.

But if there is a good reason, I'm all for it!

I don't think attribute access (like sd.hostapis.alsa) is necessary here, because it's not very important for interactive usage.

For interactive usage, I like that I can use tab-completion with the current PR: [...]

Yes I know. I'm a fan of tab completion.
I just don't think this feature will be used that often in an interactive session.

Do you have a use case in mind where this would be the case?

BTW, your screenshots also show one of the abovementioned drawbacks of namedtuple: It lists count and index as if they were host APIs!

A dict-style access couldn't do tab completion, right?

I think I've read about ways to get that. Some Python shells can do that given some additional information. I didn't dig into that, it sounded complicated.

This way, ca_settings could be the raw CFFI structure, no need to wrap it into a special class.

Heh, I wish it could be a raw CFFI structure. :) But unfortunately, the raw structures to contain pointers to other raw CFFI data sush as AsioSettings._selectors and CoreAudioSettings._channel_map which mustn't be freed until after the respective *._streaminfo data had been consumed by Pa_OpenStream(). Is this correct? (I noticed the must be kept alive comments lurking around the code.)

Oh yes, I didn't think about that!

Indeed all structures and arrays created with ffi.new() have to be kept alive by assigning them to some Python variable.
Assigning them to another CFFI struct's member isn't enough, the memory will be deallocated if there is no Python reference.

I don't know if there is some other way around, I fear there isn't.
I guess we'll need some kind of Python object to hold the CFFI structs and arrays before we pass them to the stream constructor.

However, I do think it is technically possible to rearrange the current code a little bit to replace the *Settings classes with (gasp) namedtuple's, and only do the conversion to CFFI structures just before calling Pa_OpenStream() in _StreamBase.

You seem to be obsessed with namedtuple!
You know that namedtuple is implemented in Python?
There is literally nothing it can do that you couldn't also do with your own Python code.

@mgeier
Copy link
Member

mgeier commented May 7, 2017

About keeping arrays and structs (that are part of other structs) alive:

I've just read about a trick using weakref.WeakKeyDictionary in the CFFI docs: http://cffi.readthedocs.io/en/latest/using.html#working-with-pointers-structures-and-arrays

I didn't try it but I think it looks promising, probably we could avoid some wrapper classes with this!

@dholl
Copy link
Contributor Author

dholl commented May 7, 2017

Because the set of host API properties doesn't have tuple character.

How so?

Also to clarify, I'm currently using namedtuple for two different purposes:

  1. When returning a single hostapi dict from query_hostapis(), it could instead return a namedtuple because otherwise, the keys from each hostapi dict (such as "max_input_channels" and "max_output_channels") are always the same across hostapi's.
  2. When I collect the whole set of hostapi's from query_hostapis(), then I wrap that entire set of namedtuple's into a larger namedtuple, just for the convenience of accessing each hostapi as .apiname instead of ['apiname'].

Use case 2 is probably where we differ, correct? But what do you think about case 1?

BTW, your screenshots also show one of the abovementioned drawbacks of namedtuple: It lists count and index as if they were host APIs!

True. That looks like a side affect of python using dir() to query fields.

But available hostapis do not show up during iteration:

In [3]: [x.apiname for x in sd.hostapis]
Out[3]: ['coreaudio']

Yep, I am a fan of named tuple, and do like that it's implemented in Python, and so enjoy that it already exists so that I don't need to re-invent it when I just want something with struct-like behavior with minimal memory overhead. (but not relevant in this case for only a few instantiated objects) :)

I've just read about a trick using weakref.WeakKeyDictionary in the CFFI docs:

Nifty! That looks promising.

@mgeier
Copy link
Member

mgeier commented May 8, 2017

Because the set of host API properties doesn't have tuple character.

How so?

Because I don't see them as an ordered list like [name, devices, default_input_device, default_output_device].
I wouldn't want to get those properties by numeric indexing nor by tuple unpacking.
But technically it would of course be possible to implement it that way.

When I said "I think a namedtuple would be possible (but not ideal) for the enumeration of hostapis but not at all suitable for storing the properties of a single hostapi.", I meant use case 2 in the first part and use case 1 in the second part of the sentence.

For use case 2 I don't think it's worth (but of course possible) adding attribute access, and for use case 1 I think it's not worth (but still possible) adding numeric indexing.

While you seem to think about how simple the implementation would be, I think about how much unnecessary API dead weight this would add.

Having said that, this is only a minor detail of this whole endeavor.
The important thing is that:

  • we have a way to get a "host API object" (however that will look exactly) by host API ID
  • we have a way to get a "host API object" by using one of a pre-defined set of "selectors" (regardless whether that's 'alsa' or .alsa or both)
  • we have a way to find out if a given host API is currently available
  • we have a way to get a specific parameter from a "host API object"
  • we have a way to call specific functions on the "host API object"

The last point is what I don't really like in your current proposal, because you are forcing the IMHO unnecessary indirection via .api.
Wouldn't it be desirable to avoid that?

I don't need to re-invent it

I'm all for not re-inventing stuff, but if the namedtuple API just doesn't fit, I'm fine with creating a custom class instead.

when I just want something with struct-like behavior

If you want struct-like behavior without tuple-like behavior, you'd be better off with types.SimpleNamespace, which sadly only exists in Python 3. You could use an argparse.Namespace for compatibility, but it's a bit awkward to get this from the argparse module.

with minimal memory overhead.

You can have that with your own class, too.

@tgarc
Copy link
Contributor

tgarc commented May 8, 2017

I think hostapi_settings is not a good alternative, since all settings, including e.g. sampling rate, are "host API settings".

Sure, all settings are technically "host API settings" but I think it's useful to make the distinction that, while all other settings are supported across all platforms, "extra_settings" are only available on one particular host api. Portaudio also uses the term host api to make this distinction, so I think it's pretty clear what we're trying to say.

We don't have to actually implement every single feature (and there might even be new features in the future, who knows), but we should at least think about all features and come up with an API that at least theoretically could accommodate all of them.

Yeah I think I was a little unclear. I was trying to say more what you were saying: that we don't need to implement all these features in this commit but we should consider all the features in making decisions about this commit.

I'm surprised that you seem to strongly dislike global state, yet a class object having state would be OK...

I think you're crossing wires here. I didn't say anything about global state. Besides, I was just suggesting using static methods for non-stream-specific host api extensions that already use global state (e.g. Alsa's SetNumPeriods is global). No getting around that.

I'm very reluctant to add mutexes, most likely we make it worse with that.
I think if users want to use multiple threads, they should be responsible for any necessary locking or whatever

I strongly agree - there's no reason I can see that we should go there. As soon as we do we have to start giving out guarantees on thread safety and no..let's not do that :X

Back to your points (1) and (2) above, what about the following?

I like it. Seems natural enough, and doesn't add too much complexity. Assuming the hostapis object is not too difficult to implement.

This way, ca_settings could be the raw CFFI structure, no need to wrap it into a special class.

:oo that would be cool but how would we document the host api features then?

@mgeier
Copy link
Member

mgeier commented May 10, 2017

I think hostapi_settings is not a good alternative, since all settings, including e.g. sampling rate, are "host API settings".

Sure, all settings are technically "host API settings" but I think it's useful to make the distinction that, while all other settings are supported across all platforms, "extra_settings" are only available on one particular host api. Portaudio also uses the term host api to make this distinction, so I think it's pretty clear what we're trying to say.

Where does it use "host api" for that?

There is hostApiSpecificStreamInfo in the PaStreamParameters struct and paUseHostApiSpecificDeviceSpecification. All other names I found (and I've probably missed some) with "host api" are part of the platform-independent API.
The platform-specific headers don't seem to use a common term.

If we consequently want to use PortAudio's names, we'd have to use hostapi_specific_stream_info, which seemed a bit verbose, that's why I changed it to the arguably friendlier name extra_settings.
I'm open to changing it again, but I still think hostapi_settings is not an appropriate name.
hostapi_specific_settings would be meaningful, but I think it's a bit long, isn't it?

Assuming the hostapis object is not too difficult to implement.

Indeed, that's still to be found out ...

This way, ca_settings could be the raw CFFI structure, no need to wrap it into a special class.

:oo that would be cool but how would we document the host api features then?

Well instead of a class constructor we would use a function/method to create those CFFI structures (as I mentioned in the very end of #85 (comment)). And that's where we can document the details.
We can of course also add documentation at other places if it makes sense.

@dholl
Copy link
Contributor Author

dholl commented May 12, 2017

OK, so scrapping the namedtuple-based implementation, is the overall user-syntax desirable?

Assuming import sounddevice as sd,

  1. sd.hostapis[integer] equivalent to sd.query_hostapis(integer), and returns a dict the same as what sd.query_hostapis(integer) currently returns. (I'd still prefer that sd.query_hostapis return some sort of struct-ish object to be rid of string lookups.)
  2. sd.hostapis[string] <-- New functionality, where string is a name such as 'coreaudio', 'asio', or 'wasapi' --- taken from PortAudio's hostapi enumeration but lowercased and stripped "pa" prefix.
  3. sd.hostapis.coreaudio <-- Is it acceptable to support this interface as well as sd.hostapis['coreaudio'] ? Or drop this access mechanism completely in favor of dict-style [...] access? (dict-style is likely easier from library implementation standpoint -- could we just keep it as a dict then?)
  4. In both sd.hostapis[...] and sd.query_hostapis(...), return another dict-key containing an object (anything that implements a simple namespace) that provides access to the platform-specific functions.
    • At the moment, this PR supports sd.hostapis[...].api and sd.query_hostapis(...)['api'], but we don't like the name "api" (because really, everything is an API...), so how about call it "nonportable"? or "np"?
    • So, access could be via sd.hostapis[...]['nonportable'] and sd.query_hostapis(...)['nonportable']. More legibly, this would be written out more legibly as:
      asio = sd.hostapis['asio'] # identical to sd.query_hostapis('asio') if query_hostapis() supported lookups by string name
      asio_funcs = asio['nonportable']
      chan0_name = asio_funcs.get_input_channel_name(device, chan=0)
      asio_funcs.set_stream_sample_rate(stream, sample_rate)

@mgeier
Copy link
Member

mgeier commented May 15, 2017

but we don't like the name "api" (because really, everything is an API...), so how about call it "nonportable"? or "np"?

Why not get rid of the need for a name at all?

asio = sd.hostapis['asio']
chan0_name = asio.get_input_channel_name(device, chan=0)
asio.set_stream_sample_rate(stream, sample_rate)

See the end of #85 (comment).

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

Successfully merging this pull request may close these issues.

3 participants