On Wed, Jul 31, 2019 at 9:40 AM Klaim - Joël Lamotte via SG13 <sg13@lists.isocpp.org> wrote:
Hi all,
I would like to suggest some directions to explore for some aspects of
the proposals related to this group, when we need to represent
devices.

Feel free to point me papers and authors that already explored these directions.
The following are just opinions of mine, developed while trying to
design a standard library for input devices. Keep in mind that I'm not
an expert in anything. (work on the input library is paused for now,
not sure when I can resume it).

This is mainly about devices acquisition and manipulation, so it's
related the Audio library but also potentially any paper which tries
to expose human-machine-interface devices (and maybe beyond?).

I'm also suggesting below a potential solution for the API problem in
the Audio library (how to allow using either WASAPI, ASIO or MME on
Windows?).

My points:

1. Device information is not acquisition: separate them.

+1
There can be a high cost to acquisition, delay it.

2. Device information is plain data (aka value-semantic types).

+1  VOP (Value-oriented programming)

3. Device information should always be acquired through callbacks and
helpers built around them.


Not always.  For some devices, this would require polling, as the device doesn't have a push model.
Also think of a device connected over a network - do you want a constant data stream, or just get data when needed.

I guess pull-model devices could use a callback model where you set up the callbacks and then call device.check_for_changes() or something like that.

 
More details:

When acquiring the existence, state and properties of devices, the
information is always a snapshot acquired at a specific point in time
and is always obsolete once acquired.
Assuming this, several things could be done to ease management of
devices through the lifetime of the software.

The first and second point: information should be acquired through
different types than the ones representing the device, that is for
example, the style of the current audio API looks like this:

    auto device = acquire_some_device();
    if(device.name() == "that device")
    {
       do_some_work(device.property_a(), device.property_b()); // a
might not be in sync with b or name.
       device.start(callback);
    }

In this example, the device object might change state between and
during each line. Therefore each accessor to each information might
not be in sync, that is if they are relative to each other, it is
impossible to acquire consistent synchronized state information
corresponding to a specific point in time.

I would recommend instead providing the information completely
separately and as a whole-snapshot-of-data manner:

    auto device_info = get_some_device_info(); // all information are
captured once, no device have been acquired.
    // data is already obsolete though, but that's a separate problem
that can be dealt with now.
    if(device_info.name() == "that device")
    {
        do_some_work(device_info.a(), device_info.b()); // The data
cannot have changed, even if the real state of the device changed in
between.
       some_device device(device_info.id(), callback); // here
construction = acquisition+start
       // ...
     }

This would allow:
  - working on snapshot of data, for example for presenting
information about the system without acquiring any device;
  - clarifying to the user the difference between acquiring
information about the devices and acquiring access/control of that
device, with 2 separate interfaces;
  - help the user maintain consistent information about devices by
always providing the data as coherent complete snapshots.

As a concrete example, currently the audio proposal defines functions
like get_audio_input_device_list() which return audio_device_list.
This is basically a sequence of audio_device, that you have to start()
to actually acquire/begin-to-control. The same audio_device type is
used to acquire data from the system which are fetched on demand by
accessors or updated by some callback in the implementation. Data
acquired this way might not be coherent if acquired through successive
calls to several property accessors.

The third point is about replacing any accessor function to device
information by functions storing a callback.
If all the data acquisition on devices were taking callbacks, code like this:

    auto value = device.some_property();
    work(value);

would be replaced by:

    device.on_state_changed([=](device_info info){
work(info.some_property()); }); // called at least once immediately

or more realistically:

    device.on_state_changed([=](device_info info){
        const auto property_value = info.some_property();
        app.executor([=]{ work(property_value); });// make sure to
work in the appropriate execution context
     });


In this case, the callback should be called at least once with the
current information, even if the information/state didn't change. This
is important to keep the code depending on this information always up
to date even if the data is not changing.

In some cases one might just acquire the information once and ignore
following states.
Using awaitables and coroutines through a helper function would
provide that easily:

    auto device_info = co_await device_state_snapshot(device); // this
have to be a complete information snapshot

where `device_state_snapshot` returns an awaitable, registers a
callback and unregisters it as soon as it have been called once, then
set the value to be returned.

I wouldn't encourage this but it's better provided this way as it
simplifies device interfaces, can be built over such a simple
interface and imply that it is less expensive to just set a callback
to react to the state instead of polling the information.
Also, some other patterns could be implemented using such helper
functions: capture info for n seconds and stop capturing, capture info
only if it's consistent across several calls, etc.

Combined, these API direction suggestions would also help providing a
solution for the problem of selecting audio APIs (ASIO, MMX,
DirectAudio etc. on Windows).
Have the whole device status information provided as one snapshot of
data that can be used to acquire devices. Audio APIs would then just
be one layer of information.
When the user want to acquire a device, the constructor or a separate
acquisition function would take a combination, maybe optional, of the
wanted device+api combination as one information represented by an
identifier.

For example:

    // First display a list of possible "API : device-name" list that
can be selected.
    auto all_api_info = capture_audio_api_info(); // no device acquisition
    for(auto& api_info : all_api_info)
    {
        for(auto& device_info: api_info.devices()) // each info is
relative to one API
        {  // still no device acquired
             ui_audio_settings.present(fmt("{0} : {1}",
api_info.name(), device_info.name());
        }
    }

    // Once the user select a device, aquire it and start working using it
    ui_audio_settings.on_device_selection([](auto device_info){ // the
info is relative to one API
       const auto selected_device_id = device_info.id();
       app.some_executor.post([=]{ // where it's safe to do work,
whatever the executor syntax is...
           auto maybe_device = acquire_audio_device(); // returns an
optional<audio_device>);
            // or audio_device device(device_info.id());
            app.set_audio_device(device);
        });
    });

End of suggestions. :)
What do you think?

A. Joël Lamotte
--
SG13 mailing list
SG13@lists.isocpp.org
http://lists.isocpp.org/mailman/listinfo.cgi/sg13


--
Be seeing you,
Tony