I have recently published azo, a re-implementation of ASIO's underlying COM API. As mentioned in #1296, the project's goal is for it to replace asio-sys in cpal's ASIO backend. I'm creating this issue for discussion and tracking purposes.
Motivation
asio-sys is bound to the limitations of Steinberg's ASIO SDK (outlined below), which are unsolvable from the outside.
ASIO SDK Limitations
Licensing Issues
The ASIO SDK has been under a propietary license for most of its life, with GPL being added as an alternative just a few months ago. Neither of them is a great fit for cpal:
- The propietary license (under which
ASIO SDK was introduced into cpal) prohibits any form of redistribution, including even the header files, which requires developers to either
- use a build script that automatically downloads the SDK from Steinberg's website - which is questionable both legally and security wise
- manually provide the SDK themselves - which is inconvenient
- The GPL license is copyleft, which I doubt is 100% compatible with
cpal's permissive license either (IANAL, correct me if I'm wrong)
Limited Capability
Most notably, ASIO SDK is incapable of initializing more than 1 driver at a time. This makes using multiple devices in parallel impossible. The spec explicitly calls this out as a limitation of the implementation, rather than a design choice.¹ There are also some other minor limitations, like not supporting strings which exceed certain fixed buffers in length, but these have essentially become part of the standard at this point.
azo
solves both of these problems. It is permissively licensed, and has multi-driver support without limitations. As the author I am of course heavily biased towards it, but I'm willing to do my due diligence in terms of field testing. For that, my first step was to implement cpal's custom host API, and in doing so, I encountered some problems with its design that I would like to discuss here.
cpal Limitations
No channel Mapping
cpal doesn't have a mechanism for channel mapping at the moment, so requesting n channels always means 0..n. Combined with the fact that ASIO channels are always mono (by design), this effectively means that only the first input and output channel of each device can be used. The current implementation works around this by including a mixer, which combines mono channels to stereo using an intermediate buffer. However, I'm very apprehensive of going this route, as it misrepresents the device's topology. The ability to record/playback stereo streams on a device without stereo channels might cause confusion among dependents.
One might point out that other backends feature native mixers out of the box, such as WASAPI's system wide sound server, but this isn't quite comparable, because in those cases, the native mixer IS the origin/destination of the stream, as far as cpal is concerned.
No Bulk Stream Creation
Another problem I faced is that in cpals API, streams are created one at a time, but ASIO synchronizes all channels by design, including their buffer size, sample position, buffer callback, running state, etc. Therefore, it isn't even possible to use the 1 input and 1 output channel that are accessible at the same time, unless there is some serious spaghetti going on under the hood. (edit: the upcoming duplex API solves this)
Overwhelming trait requirements
The device and stream traits impose some requirements that I struggled to fulfill. For instance, devices being Clone was counter intuitive to me. I understand the notion of it being a device identifier, rather than an life handle, but this just raises the question where the actual life handle (driver instance in ASIO's case) should reside. I ended up putting it behind an Arc that all Device and Stream instances hold, but it felt like cheating the system.
The Send bound on Stream was also tricky. ASIO uses apartment threaded COM, and painfully doesn't support marshalling AT ALL, which is very hard to make thread safe. Maybe this more of a criticism of ASIO rather than of cpal, but it is also what we have to work with.
Conclusion
Maybe it is a skill issue, but I am unable to come up with a "correct" design of fitting ASIO into cpal's API to an extent that is practically useful. I don't know how well the other backends fared in this regard, would love to hear some opinions on this.
I'm actually considering to make myself familiar with the other backends before continuing with ASIO, so I can get a bigger picture of why cpal is the way it is, but I fear I may be underestimating the depth of this rabbit hole.
¹ASIO 2.3 Specification, section IV.1:
[...] However, the implementation is currently limited to one active driver. Also, it implies that a driver’s name will have max. 32 characters including the terminating '\0'.
I have recently published azo, a re-implementation of ASIO's underlying COM API. As mentioned in #1296, the project's goal is for it to replace
asio-sysincpal's ASIO backend. I'm creating this issue for discussion and tracking purposes.Motivation
asio-sysis bound to the limitations of Steinberg's ASIO SDK (outlined below), which are unsolvable from the outside.ASIO SDKLimitationsLicensing Issues
The
ASIO SDKhas been under a propietary license for most of its life, with GPL being added as an alternative just a few months ago. Neither of them is a great fit forcpal:ASIO SDKwas introduced intocpal) prohibits any form of redistribution, including even the header files, which requires developers to eithercpal's permissive license either (IANAL, correct me if I'm wrong)Limited Capability
Most notably,
ASIO SDKis incapable of initializing more than 1 driver at a time. This makes using multiple devices in parallel impossible. The spec explicitly calls this out as a limitation of the implementation, rather than a design choice.¹ There are also some other minor limitations, like not supporting strings which exceed certain fixed buffers in length, but these have essentially become part of the standard at this point.azosolves both of these problems. It is permissively licensed, and has multi-driver support without limitations. As the author I am of course heavily biased towards it, but I'm willing to do my due diligence in terms of field testing. For that, my first step was to implement
cpal's custom host API, and in doing so, I encountered some problems with its design that I would like to discuss here.cpalLimitationsNo channel Mapping
cpaldoesn't have a mechanism for channel mapping at the moment, so requestingnchannels always means0..n. Combined with the fact that ASIO channels are always mono (by design), this effectively means that only the first input and output channel of each device can be used. The current implementation works around this by including a mixer, which combines mono channels to stereo using an intermediate buffer. However, I'm very apprehensive of going this route, as it misrepresents the device's topology. The ability to record/playback stereo streams on a device without stereo channels might cause confusion among dependents.One might point out that other backends feature native mixers out of the box, such as WASAPI's system wide sound server, but this isn't quite comparable, because in those cases, the native mixer IS the origin/destination of the stream, as far as
cpalis concerned.No Bulk Stream Creation
Another problem I faced is that in
cpals API, streams are created one at a time, but ASIO synchronizes all channels by design, including their buffer size, sample position, buffer callback, running state, etc.Therefore, it isn't even possible to use the 1 input and 1 output channel that are accessible at the same time, unless there is some serious spaghetti going on under the hood.(edit: the upcoming duplex API solves this)Overwhelming trait requirements
The device and stream traits impose some requirements that I struggled to fulfill. For instance, devices being
Clonewas counter intuitive to me. I understand the notion of it being a device identifier, rather than an life handle, but this just raises the question where the actual life handle (driver instance in ASIO's case) should reside. I ended up putting it behind anArcthat allDeviceandStreaminstances hold, but it felt like cheating the system.The
Sendbound onStreamwas also tricky. ASIO uses apartment threaded COM, and painfully doesn't support marshalling AT ALL, which is very hard to make thread safe. Maybe this more of a criticism of ASIO rather than ofcpal, but it is also what we have to work with.Conclusion
Maybe it is a skill issue, but I am unable to come up with a "correct" design of fitting ASIO into
cpal's API to an extent that is practically useful. I don't know how well the other backends fared in this regard, would love to hear some opinions on this.I'm actually considering to make myself familiar with the other backends before continuing with ASIO, so I can get a bigger picture of why
cpalis the way it is, but I fear I may be underestimating the depth of this rabbit hole.¹ASIO 2.3 Specification, section IV.1: