naudio / NAudio

Audio and MIDI library for .NET
MIT License
5.5k stars 1.1k forks source link

Split NAudio into multiple packages #591

Open jwosty opened 4 years ago

jwosty commented 4 years ago

After reading #184 I think it is time to take the .Net Standard spirit to NAudio, and split it into multiple packages that each target different platforms. The first step would be to identify the completely platform-independent interfaces and classes (for example IWavePlayer and WdlResamplingSampleProvider) and refactor them out into a new package, perhaps NAudio.Core, keeping the same namespaces for backward-compatibility. The next step would be to move the various platform/api-specific classes and interfaces into their own separate packages each; for example putting all the Asio stuff into a new package called NAudio.Asio.

This is a huge step forward for opening up the possibility for new platform implementations. For example, a motivated person could create macOS implementations of the NAudio API, and call it NAudio.AppleCoreAudio in the future. Here's a quick-n-dirty outline of the possible new package structure:

(I could also imagine breaking things down further, like splitting playback and recording into different packages, though this may be overkill)

Finally, we would also have to make sure to create a new NAudio package that includes no code but simply includes all the other packages directly, again for backward compatibility, as not to break anything that directly reference the "NAudio" package. Probably also mark it as deprecated.

I'm willing to take this on, as it is mostly dumb refactoring; although if someone beats me too this I won't be too upset :)

markheath commented 4 years ago

This is something I've given quite a lot of thought to, and if there were to be a NAudio version 2 I'd definitely break it up into smaller chunks.

My ideas are similar with an NAudio.Core package that contains all the interfaces, plus all pure cross-platform .NET standard code (e.g. things like WaveFileReader and pure C# codec or effect implementations).

Then there would be a series of interop packages for the various APIs - e.g. NAudio.Asio.Interop, NAudio.Mme.Interop (waveIn, waveOut), NAudio.Wasapi.Interop, etc. These would ideally not depend on NAudio.Core, so if someone wanted to use those interop wrappers in another project but not use the NAudio abstractions they would be free to do so (this would be particularly useful for ASIO which isn't a great fit for the existing NAudio interfaces). For some of these packages we could even switch to using other open source packages such as SharpDX's mediafoundation which has some advantages over NAudio's interop (although it's sadly not under development any more, so might no longer be an option)

Finally, various packages containing implementations of NAudio interfaces and base classes that depend on both NAudio.Core and one (or more) of the interop packages. So for example, WasapiOut could be in an NAudio.WindowsCoreAudio package, and MediaFoundationReader in a NAudio.MediaFoundation package. I haven't quite decided what the best way to slice this up, but something similar to your suggested breakdown would make sense. And as you point out would also make it obvious how to produce an extension for other operating systems (although it is possible today thanks to the NetStandard target)

One of the trickiest parts of splitting everything out in this way is the fact that I use WaveFormat extensively in NAudio, which is a struct based pretty closely on WAVEFORMATEX. The difficulty here is that only some of the audio APIs are based on WAVEFORMATEX (waveIn, waveOut, acm ... etc). WASAPI, ASIO etc have their own way of defining wave formats. I've not quite decided what the best solution to this is, but probably, the "interop" packages should always use a struct matching what the underlying API uses, and the API specific implementation packages would know how to convert from those structs into an NAudio specific WaveFormat class, which may for simplicity remain very close to what the existing one is.

As I said, I was thinking of saving this for NAudio 2, as I am very reluctant to introduce any kind of breaking change. Another technical challenge is simply how to have one repository that builds multiple NuGet packages that depend on each other. I'd want to be able to easily debug into all the code (so use project references for debugging), but in the release build make everything a NuGet package with the dependencies wired up. I'm sure this challenge has been solved by someone, but I've not found a good example that I want to copy yet.

Another goal of NAudio 2 is to enable me to rework some of the interfaces. Particularly changing to a Span<T> based WaveProvider, and cleaning up a few other interfaces as well.

In short, yes I do see the future of NAudio as being a collection of smaller packages, but would like to get the design right before pulling the trigger on it.

jwosty commented 4 years ago

The act of dividing up the classes that currently exist among some new packages can be done in a completely backwards-compatible way, as long as you don’t change any of the actual API (the API changes that you mentioned could be a separate concern and feature PR). I’ve started experimenting with this in #593.

As I said, I was thinking of saving this for NAudio 2, as I am very reluctant to introduce any kind of breaking change. Another technical challenge is simply how to have one repository that builds multiple NuGet packages that depend on each other. I'd want to be able to easily debug into all the code (so use project references for debugging), but in the release build make everything a NuGet package with the dependencies wired up. I'm sure this challenge has been solved by someone, but I've not found a good example that I want to copy yet.

nuget pack actually already handles this. If you have multiple sdk-style projects that reference each other, the pack command (also dotnet pack) will make nuget packages out of each of them and then generate the appropriate PackageReference command, no nuspec necessary.* If you want an example, my Interstellar library does exactly this (some of the constituent packages include Interstellar.Core, Interstellar.macOS.WebKit, and Interstellar.Wpf.Chromium, to name a few). dotnet pack does all the heavy lifting (which ultimately is a wrapper around nuget pack), and Interstellar's FAKE build script just does a few post-build things to the resulting package for some final prep.

*There’s just one problem with this process. Automatic packing will always generate a >= constraint on the other project libraries which is usually not what you want — usually you want a == constraint. See NuGet/Home/issues/5540. I’ve worked around this exact problem in my own library here by hacking the .nupkg after it’s built until nuget gets fixed: jwosty/Interstellar/issues/3 (fix is at 16533d33d). It's ugly and brittle, but as long as you're careful, it works. I'm sure you could write a test around this bit to assert that the behavior is correct. Feel free to copy-paste or derive something from my hack; that's all open source.

jwosty commented 4 years ago

also, How can I help with a design for this and/or NAudio 2? Should we draft up a technical document (a la RFCs) specifying the design, which can be used for collaboration? An issue thread alone could be a little hard to keep track of. I have some relevant knowledge of the macOS audio APIs that could inform new designs. If we take some breaking changes for a 2.0 version I think it should be in service of cross platform concerns, because I believe that can be a strong foundation for NAudio 2.0.

markheath commented 4 years ago

Wow I didn't know about the nuget pack feature. I'd been wondering for ages on how to do this.

And yes, it would be good to discuss the breakdown. Maybe you can give me a bit more detail on how you see the existing bits of NAudio being broken into sub-packages (i.e. not thinking NAudio 2 just yet). I think there will be a few interesting choices as to exactly what gets bundled together and they will be tricky to name well. If we can produce a v1.11 in a completely backwards compatible way that would be great.

jwosty commented 4 years ago

As a user of this library, a core package + a package for each API implementation would be useful: that way you could include only the platforms you plan on using. Beyond that I'm not sure as I'm not as familiar with some of the other aspects of the library (for example I see there is MIDI stuff). Perhaps something like this?

This leaves the option for adding more of these packages down the line, like an implementation for macos and/or linux (I would consider a macos package much higher priority)

I played around with breaking it down in PR #593. It also keeps an overall NAudio package that simply references all the appropriate ones so any projects that reference NAudio currently don't have to know the difference.

It's also probably a good idea to veer on the side of too granular of packages for now, because you can always keep splitting down the line.

hansmbakker commented 2 years ago

I'm just getting started with NAudio, but it seems like a very useful and nice library!

I see work on this issue is being done, but is there any status overview in a PR of what is done and what still needs to be done? Or a roadmap?

It is good that the packages are being restructured but I feel the package tree and dependency tree still have some room for improvement:

markheath commented 2 years ago

It would be nice to tidy up the package structure a bit further, but probably in the short term things will stay where they are. The WPF thing is because the window callback mode for WaveOut uses WinForms.

Swimburger commented 2 years ago

Does the WaveFormatConversionStream API depend on any Windows APIs? If not, it would be great if the conversion APIs are available cross-platform, which I'd assume would go under Core?

I'm trying to convert a byte array from PCM S16LE to MuLaw on MacOS, but I want this application to work everywhere.

markheath commented 2 years ago

Yes, WaveFormatConversionStream is a wrapper around the Windows ACM APIs. If you just need mu-law and a-law conversion, there are fully managed conversion algorithms for those in NAudio that you can use instead.