0 Comments Posted in:

I started creating NAudio back in 2002, using v1.0 of the .NET Framework and developing on the open source SharpDevelop IDE.

Of course, a huge amount has changed in the .NET world since then. However, since NAudio was heavily used in commercial applications running on Windows XP, I was always reluctant to depend on newer .NET features that would cause us to have to stop supporting legacy versions of Windows, or to maintain two different versions of NAudio.

So NAudio remained for a long time on .NET 2.0, before eventually upgrading to .NET 3.5. This means it has not yet taken advantage of Task and async / await, and a .NET Standard version has not been built, meaning it can't be used from .NET Core.

Part of the reason for this is simply that NAudio is very Windows-centric. A large part of the codebase consists of P/Invoke or COM interop wrappers around the various Windows audio APIs. So even if a .NET Standard build were to be created, much of the functionality would fail to work if you tried to use it in a .NET Core app running on Linux.

Having said that, there are a fair amount of general purpose utility classes in NAudio that would be usable cross-platform, and it seems a shame to block .NET Core apps from using it, especially if they are being run on Windows. So over my Easter holidays I started to see what it would take to make a .NET Standard version of NAudio.

New csproj format

The first step was moving to the new csproj file format. I really love this new format, and it brings a lot of benefits. You don't need to explicitly include source files - they are picked up automatically. It's got a nicer way of specifying NuGet dependencies, and it can also contain all the metadata needed to define the project as a NuGet package. I also need to support unsafe code blocks.

Here's the first part of my csproj file, specifying three target frameworks (more on that later!), and the metadata for the NuGet package;

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net35;uap10.0.10240</TargetFrameworks>
    <Version>1.9.0-preview1</Version>
    <Authors>Mark Heath &amp; Contributors</Authors>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <Description>NAudio, an audio library for .NET</Description>
    <PackageLicenseUrl>https://github.com/naudio/NAudio/blob/master/license.txt</PackageLicenseUrl>
    <PackageProjectUrl>https://github.com/naudio/NAudio</PackageProjectUrl>
    <PackageTags>C# .NET audio sound</PackageTags>
    <RepositoryUrl>https://github.com/naudio/NAudio</RepositoryUrl>
    <Copyright>© Mark Heath 2018</Copyright>
    <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    <GenerateDocumentationFile Condition=" '$(Configuration)' == 'Release' ">true</GenerateDocumentationFile>
  </PropertyGroup>

Target Frameworks

I started off by trying to target the lowest version of .NET Standard I could get away with - .NET Standard 1.6. I'd had some success with a previous proof of concept attempt for that in the past. But I soon found there was just too much to fix up, and that .NET Standard 2.0 was going to be much easier.

But I also wanted to see if I could still produce a .NET 3.5 build, so I added the net35 target framework moniker. This resulted in some code that could compile for .NET 3.5 and not for .NET Standard 2.0 and vice versa.

There were two ways of handling this. First, I could exclude whole C# files that weren't going to work on that target framework. So for example, in .NET Standard, the WinForms user interface components were never going to work, but also a number of the input and output device implementations used things like Windows Forms objects or the Windows Registry and so couldn't easily be made to compile for .NET Standard 2.0. Here's how to exclude certain files from a particular framework.

  <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
    <Compile Remove="Utils\ProgressLog*.*" />
    <Compile Remove="Gui\*.*" />
    <Compile Remove="Wave\MmeInterop\WaveWindow.cs" />
    <Compile Remove="Wave\MmeInterop\WaveCallbackInfo.cs" />
    <Compile Remove="Wave\WaveInputs\WaveIn.cs" />
    <Compile Remove="Wave\WaveOutputs\WaveOut.cs" />
    <Compile Remove="Wave\WaveOutputs\AsioOut.cs" />
    <Compile Remove="Wave\WaveOutputs\AsioAudioAvailableEventArgs.cs" />
    <Compile Remove="Wave\WaveFormats\WaveFormatCustomMarshaler.cs" />
  </ItemGroup>

I also needed to reference Windows.Forms for the .NET 3.5 target framework only, which can be done like this:

  <ItemGroup Condition=" '$(TargetFramework)' == 'net35' ">
    <Reference Include="System.Windows.Forms" />
  </ItemGroup>

There were still some individual bits of code I needed to exclude depending on what target framework I was building. For this, I needed to use the built-in #define symbols, which are NET35 and NETSTANDARD2_0.

A fair amount of marshaling code needed to switch based on the target. Here's a simple example:

public static int SizeOf<T>()
{
#if NET35
    return Marshal.SizeOf(typeof (T));
#else
    return Marshal.SizeOf<T>();
#endif
}

One of the biggest challenges at this point was making sense of the thousands of error messages I was getting. Unfortunately, Visual Studio doesn't seem to offer any way to just build for a specific target framework, so you end up with errors mixed together for different target frameworks and it can be confusing to know which is for which. Add that to the fact that ReSharper also struggles with multi-targetting, and it was quite a frustrating experience trying to chase all the compile errors out.

There is a dropdown in the top-left of the VS 2017 code editor window that lets you see the syntax highlighting depending on what target you chose, which is handy to see which code surrounded by #if blocks is valid.

UWP

Over the past few years I've maintained a rather hacky and incomplete build of NAudio for Windows 8, WinRT, Universal Windows apps, and I wanted to see if I could target that as well. The framework moniker is uap10.0, which I'm sure I had compiling successfully at Easter, but for whatever reason on my new dev machine I can't make that compile at all, and so for now I've specifically targetted uap10.0.10240 which is the first version of Windows 10.

Obviously lots of NAudio has to be excluded to target UWP, and I also had to reference the Microsoft.NETCore.UniversalWindowsPlatform NuGet package, as well as MSBuild.Sdk.Extras which I then imported as a project. To be honest, I don't really understand exactly what this does or why it was necessary, but I was really floundering at this point until I found a project by Oren Novotny, and copied the csproj from there (which has since changed to be .NET Standard 2.0 only).

Here's my UWP specific parts of the project file:

  <ItemGroup Condition=" '$(TargetFramework)' == 'uap10.0.10240' ">
    <Compile Remove="Utils\ProgressLog*.*" />
    <Compile Remove="Gui\*.*" />
    <Compile Remove="Wave\Compression\*.cs" />
    <Compile Remove="Wave\Asio\*.cs" />
    <Compile Remove="Wave\MmeInterop\WaveWindow.cs" />
    <Compile Remove="Wave\MmeInterop\WaveCallbackInfo.cs" />
    <Compile Remove="Wave\Midi\MidiInterop.cs" />
    <Compile Remove="Wave\WaveInputs\WaveIn.cs" />
    <Compile Remove="Wave\WaveInputs\WaveInEvent.cs" />
    <Compile Remove="Wave\WaveInputs\WasapiCapture.cs" />
    <Compile Remove="Wave\WaveInputs\WasapiLoopbackCapture.cs" />
    <Compile Remove="Wave\WaveOutputs\WaveOut.cs" />
    <Compile Remove="Wave\WaveOutputs\WaveOutEvent.cs" />
    <Compile Remove="Wave\WaveOutputs\DirectSoundOut.cs" />
    <Compile Remove="Wave\WaveOutputs\WasapiOut.cs" />
    <Compile Remove="Wave\WaveOutputs\MediaFoundationEncoder.cs" />
    <Compile Remove="Wave\WaveOutputs\AsioOut.cs" />
    <Compile Remove="Wave\WaveOutputs\AsioAudioAvailableEventArgs.cs" />
    <Compile Remove="Wave\WaveStreams\AudioFileReader.cs" />
    <Compile Remove="Wave\WaveStreams\Mp3FileReader.cs" />
    <Compile Remove="Wave\WaveStreams\WaveFormatConversionProvider.cs" />
    <Compile Remove="Wave\WaveStreams\WaveFormatConversionStream.cs" />
    <Compile Remove="Wave\WaveFormats\WaveFormatCustomMarshaler.cs" />
    <Compile Remove="Wave\WaveProviders\MediaFoundationResampler.cs" />
    <Compile Remove="FileFormats\Mp3\Mp3FrameDecompressor.cs" />
  </ItemGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'uap10.0' or '$(TargetFramework)' == 'uap10.0.10240'">
    <PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.0.8" />
    <PackageReference Include="MSBuild.Sdk.Extras" Version="1.5.4" PrivateAssets="All" />
  </ItemGroup>
  <Import Project="$(MSBuildSDKExtrasTargets)" Condition="Exists('$(MSBuildSDKExtrasTargets)')" />

I also had to add in a lot more code exclusions as in UWP there are lots of small missing attributes that were part of my interop signatures. For example:

    [ComImport,
#if !WINDOWS_UWP
    System.Security.SuppressUnmanagedCodeSecurity,
#endif
    Guid("59eff8b9-938c-4a26-82f2-95cb84cdc837"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]

Success?

With this in place, I was eventually able to get NAudio building, outputting a single NuGet package containing a .NET 3.5 dll, a .NET Standard DLL, and a UAP DLL.

A quick test revealed that this worked great - I could add it to regular .NET Framework apps, and use all the WinForms stuff if I wanted. I could also use it from a .NET Core console app and play audio with the WaveOutEvent class (on Windows of course)! And I could use it from a UWP application as well.

So mission accomplished perhaps? Well, I have one reservation about what I've done so far, and that's whether it's worth having the UWP target framework. With this target, it's not possible to build the NAudio solution on Windows 7 or without the UWP 10240 SDK installed. This would be a pain for contributors who want to make pull requests or experiment locally.

I could of course go back to separate solutions and a nuspec file to piece it all together, but given how experimental the UWP code still is, and the fact that newer versions of UWP now support .NET Standard 2.0 anyway, I'm wondering whether I'd be better off just targetting .NET Standard 2.0 and .NET 3.5, and producing an entirely separate assembly for the few UWP specific classes I have, and requiring that you use it on versions of Windows 10 that support .NET Standard 2.0. Let me know in the comments if you have a strong preference either way.

Try it out!

If you want to be nosey and see what I'm doing, the code is available on GitHub in the netstandard branch and I've pushed a 1.9.0.preview1 package to NuGet which you're encouraged to try and report your results.

What's Next?

My idea is that NAudio 1.9 will support both .NET Standard 2.0 and .NET 3.5 (not sure about UWP), but that it will mark the final version that I avoid making any breaking changes to the public API.

For NAudio 2.0, I'd like to modernize the interfaces for playback and recording, taking advantage of Span<T> and async/await for a much more modern coding style. I'd also split the behaviour out into more packages. The core NAudio 2.0 package would have contain .NET Standard cross-platform friendly code, but you'd then add additional NuGet packages for WaveIn/WaveOut, or WASAPI, or ASIO. There'd be another for UWP.

Of course, this assumes I manage to find some free time to work on NAudio 2.0, which realistically there won't be much of, but that's at least an idea of where I'd like to go next with the project.

Want to get up to speed with the the fundamentals principles of digital audio and how to got about writing audio applications with NAudio? Be sure to check out my Pluralsight courses, Digital Audio Fundamentals, and Audio Programming with NAudio.
Vote on HN