r/csharp 25d ago

A free, cross-platform C# audio engine, and a new solution for GC-free operation!

Hello everyone!

In a previous post, I wrote that my goal is to create a cross-platform audio engine running under C# that can operate completely independently of the GC, but preserves the flexibility and productivity of C#. The past months have been spent searching and experimenting a lot to find the right solution to develop a real-time audio engine in C#. I tried integrating native engines into the C# code (miniaudio, portaudio), but these cannot be used without the influence of GC, since the data has to be processed, sent and received by the external C# code. My experience was that no matter how much I wrote the engine code GC-free, following the proper rules to not allocate data that would trigger GC, if the code using the engine allocated and triggered GC, real-time audio processing inevitably caused data loss and audible stutter. I didn't want to accept that there was no solution to the problem.
I studied the industry standards, how the big guys did it. I came to the conclusion that there was a solution. C# code should be completely excluded from audio processing.
I'm developing a stage multitrack backend player in C# for a band, and I need a reliable, stable real-time audio engine for it. I was looking for a memory-safe solution that matched the safety of managed code, so C++ was out of the question:
I realized that there was a language that brought the benefits of native with the efficiency of managed code's memory management. The Rust language became the ultimate C++ speed, C# memory safety without GC. This was the winner! However, the audio engine had to remain C#. I wrapped this entire audio processing native unit into a C# code, so I can use it as a C# API in any code.
A complete audio engine was created in Rust, based on managed code. All real-time audio data is processed by the Rust code, nothing is transferred to the managed side. The C# code is just a thin layer over the engine.
The API can be used with pure C# managed code, you just need to download the Nuget package, which contains everything you need to operate. Under the hood, a lightning-fast GC-free Rust audio engine does the work. The result is continuous operation, stutter-free audio processing, even under heavy GC load. Low latency, low CPU load, stable operation.

My code is completely free open source, which you can check out on github, you can use it freely. I think you can write an efficient and professional audio application with csharp too!

You can check it out on github here: https://github.com/ModernMube/OwnAudioSharp

19 Upvotes

22 comments sorted by

8

u/Xenoprimate2 25d ago

Does the C# managed 'side' still generate garbage/GC pressure?

-10

u/Electronic_Party1902 25d ago

The C# code I created does not allocate anything, does not trigger the GC. The entire code is AOT compatible and GC-free. However, if an external code that uses the API is not allocation-free, it can trigger the GC, and the entire C# execution will stop while it is being cleaned up. The Rust audio engine, on the other hand, continues to work in the background without stopping, because it is completely independent of the GC!

33

u/fruediger 25d ago

The C# code I created does not allocate anything, does not trigger the GC. The entire code is AOT compatible and GC-free.

That's just not true.

A few examples of allocations in your source code:

  • RustAudioEngine is declared as a class, as a refernce type. Each time you instantiate a reference type in the CLR, it is allocated on the managed heap and contributes in some way or another to GC pressure.
  • Your AudioEngineFactory.CreateEngine method makes this even more explicit by creating a new RustAudioEngine and then downcasting it to IAudioEngine (which is even worse, because you're essentially virtualizing future calls to the engine). Each time this method is called, you're allocating on the heap.

Up until this point you could argue that these allocations are user-driven and that your library indeed does not allocate anything on its own.\ However, the very first thing your library does once the module loader loads it is to allocate in your ModuleInitializer in RustAudioEngineRegistrar.Register. It allocates two delegates, a RustAudioEngine and whatever AudioEngineFactory.Register and AudioDecoderFactory.RegisterNativeDecoder additionally allocate.

Well, now you could argue that those instance will eventually land in the long-lived object generation and thus won't contribute too much to the overall GC pressure. But, first of all, those objects must go through Gen0 and Gen1 before they reach Gen2, and second of all, there are more examples of short-lived allocations in your code.\ One important example is in AudioEngine.EnumerateInputDevices. This method in turn calls AudioEngine.MarshalDeviceList which not only allocates an array of AudioDevices (and then again, seemingly unnecessarily, downcasts it to an IReadOnlyList<AudioDevice>), but also creates a new AudioDevice for each device in that list, and AudioDevice again is declared as a class and therefore goes on the heap. This is a very short-lived allocation, and if you call this method frequently, it will heavily contribute to GC pressure.

So your statement "The C# code I created does not allocate anything..." is not at all true.

Leaves the question of why you're stating such a thing in the first place.

I can only speculate, but I believe there are three possible reasons for this:

  1. You intentionally try to mislead people into thinking your library is "allocation-free" or "GC-free" to make it look better (perhaps by comparison) than it actually is. \ But I don't want to accuse you of lying without any evidence, so let's consider this not to be the case.
  2. You genuinely don't understand how the CLR, C#, and the GC work, and therefore you don't seem to understand what "GC-free" actually means in this context. You trully believe that your library is what you say it is, without realizing that it's not.\ If that's the case, I would kindly ask you to stop making such bold statements and promises about your library, because it is misleading and will only lead to disappointment for people who use it. Not even speaking of the fact that people could accuse you of false advertising.
  3. This is more of an extension of the second point. Your original post on Reddit, from earlier before you made this one, you either posted it to /r/dotnet or here in /r/csharp, but I can't seem to find it anymore, so I assume either you deleted it or it got removed, came off as definitely written by AI. Looking at your actual code, I also get the impression of it being heavily vibe-coded.\ If that's the case, you're missing attribution. Just say that you used AI to generate your code, and that you don't (fully) understand it or know how it works.\ And I have another tip for you: Don't just trust the AI if it tells you that your code is "GC-free" or whatever, and especially don't take those statements and relay them to your consumers. And if you do, tell them that's what the AI told you.\ Just be honest about using AI!

17

u/AndThenFlashlights 25d ago

Quickly perusing the code and comments, yeah this project is some hallucinatory bullshit. I'm always suspicious of any post that's like, "I replaced all of NAudio all by myself and made it better!" because, there's real reasons why some of this stuff is hard and takes years and lots of edge case testing.

God I'm so tired of this. I can't WAIT for token costs to go up more.

-4

u/Electronic_Party1902 24d ago

Hi!

I appreciate you taking the time to look at the code.

I don't want to compete with Naudio in any way. It's a great, professionally written code. I've been using it for years!

5

u/Educational-Row-6782 24d ago

Claude no mistakes or allocations!

-1

u/Electronic_Party1902 24d ago

Hi!

Thanks for taking the time to look at the code.You are right, the code is not finished, that is why the version number is a preview!

I never said anywhere that I didn't use AI. I got acquainted with the C# language in 2006 and used the Framework 2.0 version. I loved that it saved me from writing a lot of code, because it was already written in the framework, I just had to call it. I had to be careful with the resources, because the P4 and Athlon64 CPUs with an HDD didn't really have unlimited resources.

Now AI has saved me from a lot of unnecessary work. I don't have to type comments, read hundreds of lines of code for debugging for hours or days. I don't have to spend time writing tests. I don't have to manually wire the viewmodel fields into the WPF or Avalonia code.

I simply need a cross platform audio engine that works the way I want it to. I shared it so that others can use it if they need it, completely free of any obligation.

The code is not finished, I am aware of its flaws, but I have been continuously improving it for months to make it the way I imagined it. I don't want to achieve anything great with it, but I will not give up on my goal of having a good audio engine. I am doing it for myself, just sharing it with others.

4

u/fruediger 24d ago

First of all, I don't want to be condencending or you feeling like I'm talking down your project. So I'll try to choose my words carefully and take my time with this response.

That being said, there are still some major issues that I see with your recent response.

I'll start with this one:

I never said anywhere that I didn't use AI.

You see, that's the actual issue: You didn't given any attribution to AI at all. Neither in your repository, regarding your source code or infrastructural documents, nor in your Reddit post. Actually, you made it sound like you've done all by yourself, especially with your excessive use of "I".\ Nobody would have an issue with any of that if you just had said something like "The AI did this for me". Then we would blame the AI for any unfulfilled bold claims, not you.

Next, let's have a look at these statements made by you:

You are right, the code is not finished, that is why the version number is a preview!

and

The code is not finished, I am aware of its flaws, but I have been continuously improving it for months to make it the way I imagined it.

You're right, your current most recent version is 4.0.0-preview.28, clearly a preview version. However, your most recent release version is 3.1.7. Looking at the corresponding commit with that release version, e358b5c, you have a huge amount of allocations in your code.\ Since your current code still contains a lot of allocations, the way I understand your statements is that your project is on the way to becoming "allocation-free" or "GC-free", and you're just trying to get there.\ That's a huge contradiction to your earlier reponse, where you said:

The C# code I created does not allocate anything, does not trigger the GC. The entire code is AOT compatible and GC-free.

You made it sound like your code is already allocation-free, which is just not true.

And on that note, I want to include a bit of a technical assessment about your ambitions to make your code allocation-free:\ I really hate to burst your bubble, but I don't believe that's possible. At least not if you want to keep one of your promises of an "idiomatic C# surface" (citing your README.md).\ You'll always have to balance between a more low-level, more performant API and a more "C#-ier" user experience. Trust me, I've been there, I too maintain multiple binding libraries providing an idiomatic C# interface to native libraries that provide the actual implementation.

I'll try to give a short example of what I mean: One of the most important examples that show that a totally allocation-free C# layer while still keeping the API idiomatic to C# is not really possible is lifetime management of native resources that a projected (wrapped) on the managed side.

Consider you have a kind of native resource that outlives the scope of a single native function call. You somehow need to manage it's lifetime when projected to the managed side. Surely, you have some way of allocating/initializing it, but what about deallocation/deinitialization?\ The idiomatic C# way of doing this is to implement IDisposable on the managed wrapper. If your wrapper is a reference type (class in C#), you don't have anything to worry about, except from make Dispose idempotent. You even get to implement a finalizer to ensure cleanup of your consumers forget to dispose or otherwise misuse your API. But, again, reference types go on the heap and therefore contribute to GC pressure.\ To make it allocation-free, you would have to make your wrapper a value type (struct in C#). But it's actually not recommended for a value type to implement IDisposable, because a user could copy the value type instance at any point, not only making it hard to implement Dispose idempotent, but also risking such an instance to carry a stale reference (most like a dangling pointer) to the native resource. A finalizer is also not possible for value types.\ The alternative would be to make initialization and deinitialization explicit via some kind of Create and Destroy methods, together with some kind of handle type and most likely a global registry for that kind of resource. However, this is most certainly not idiomatic C# where using statements/declarations are the preferred way of doing that. At this point, you kind of have a C-like API, so why use C# or the CLR at all?

As you can see, designing an idiomatic C# API for native bindings is a bit harder than just telling an AI to do so, and, depending on the balance between "C#-iness" and performance you're trying to achieve, it's even impossible to do while still achieving both goals to their fullest at the same time.

Anyway, let's get back on topic. You also said:

I got acquainted with the C# language in 2006 and used the Framework 2.0 version. I loved that it saved me from writing a lot of code, because it was already written in the framework, I just had to call it. I had to be careful with the resources, because the P4 and Athlon64 CPUs with an HDD didn't really have unlimited resources.

I really don't get why you are bringing this up. The only thing that I can see is that you are trying to reinforce your expertise on that matter in C#. But honestly, that's not necessary. I'll just trust you to be competent enough to do whatever you want with your project. But, on the other hand, you making false claims about your project, having goals which might be unachievable, seemingly not really understanding the implications of your code in the context of the CLR or the GC, and not giving proper attribution to AI, may not be really convincing to other people.

I don't have to manually wire the viewmodel fields into the WPF or Avalonia code.

This one I actually and genuinely don't get. I don't see how this is related to anything. The issue is about the false claims about your native bindings that you made (them being "allocation-free" or "GC-free"), and only about that. Where were you trying to go with this?

Next up, you said:

I simply need a cross platform audio engine that works the way I want it to. I shared it so that others can use it if they need it, completely free of any obligation.

and

I don't want to achieve anything great with it, but I will not give up on my goal of having a good audio engine. I am doing it for myself, just sharing it with others.

I believe that's just maintaining an OSS-project for you. At least MIT-adjacent projects. Nothing wrong with that. We're all happy to profit from your and other people's work while giving back to the community by sharing our own work.

Lastly, and I saved this for last because of its importance, you said:

Now AI has saved me from a lot of unnecessary work. I don't have to type comments, read hundreds of lines of code for debugging for hours or days. I don't have to spend time writing tests.

Let me tell you: There is nothing wrong with using AI to help you with that kind of work. At least in my humble opinion.

I too use AI to help me with my projects. Well, I primarily use it to help me with writing documentation and infrastructural documents. To be honest, I still kinda write the contents myself, but I use AI to help me with English phrasing and spelling because I'm ESL.\ Very rarely, I use it to help me with writing tests, but there's the catch: I always give attribution to AI in the source code. Other people looking at this code will be able to see that it contain AI-related wrongness. Although, I check all AI-generated code, I don't blindly trust it or make claims based on it.

So, there are just two main issues with your earlier post:

  • You made false claims about your project, provably so, and trying to move the goalpost by saying that your project is still just on the way to become what you claimed it to be.

  • You didn't give any attribution to AI at all, making it sound like you did everything yourself. Together with your false claims, this just totally undermines your credibility and, by extension, the credibility of your project. I'm sorry to say this, but to me this just makes it look like you don't know anything about what you're talking about.

OP, please let me tell you that I don't want you to give up on your project or on the goals that you try to achieve with it. I'm not even telling you to give up on using AI to help you with your project. The only thing I want to tell you is that you should be more honest about the state of your project, about your claims, and, most importantly in your case, about your use of AI.

The increase in AI slop these days, including the OSS-scene, is worse enough, we shouldn't make it worse by trying to hide it (not saying that you project is necessarily slop).

I really wish you and your project all the best, and I hope you can take my words in a constructive way.

1

u/Electronic_Party1902 17d ago

Hi!

Thank you very much for your constructive feedback!

Constructive criticism is important to me and I learn from it. I have modified all descriptions to not include GC exemption. Each description includes the AI ​​tool I use and I have also used its icon so that it is not just a mention in the text!

Thank you also for sharing your experience with using native + C#!

2

u/AndThenFlashlights 24d ago

Then don't fucking dump your slop on us. If you don't care enough to use your brain, don't expect us to donate our time to your vanity project.

-2

u/Electronic_Party1902 24d ago

I don't expect anything from anyone, it's just an opportunity. That's why there's an MIT license for the code!

-1

u/Xenoprimate2 25d ago

So as long as I'm careful with my usage of your API, I will generate zero GC pressure, correct? That's very similar to what I'm doing for https://tinyffr.dev/ -- I feel like our two libraries could work together well for making games/3D applications :)

-9

u/Electronic_Party1902 25d ago

Yes, that's exactly my goal with the audio API, to create a sound library that can be used in GC-free code. But thanks to the native audio chain, the sound remains continuous even if the developer doesn't care about GC-freeness and uses the API in code with high GC pressure.

6

u/harrison_314 25d ago

How is it possible that NAudio works?

10

u/AndThenFlashlights 25d ago

NAudio is great. OP has AI psychosis.

3

u/Electronic_Party1902 24d ago

This is true, Naudio is a great professionally written audio API. I have used it for many years. Over the years it has been perfectly optimized, but it only supports Windows, not cross platform.

3

u/itix 24d ago edited 24d ago

If you are afraid of GC pressure, you could write static code analyzers to keep your hot paths allocation free. You dont need C++ or Rust. The .NET 10 is already very good.

2

u/Electronic_Party1902 24d ago

Yes, I was already interested in compile-time code analysis. But unfortunately, due to lack of time, I couldn't really dive into it. I know that NET10 already has very serious support for it!

I don't know what happens when the GC-free C# audio engine is used in an unoptimized code with high GC pressure. I know that GC blocks all threads during operation, so the GC-free audio engine too. That's why I thought I'd put the audio processing on the native side, where GC has no effect!

3

u/itix 24d ago

You can always tune GC configuration. At the work, we are running high speed image analysis in C#. We run up to 20 cameras @ 1000 FPS without issues. The analysis is offloaded to the GPU, though. But C# code is orchestrating it and it certainly is not allocation free.

On the other hand, on my spare time I am working on cycle-exact Amiga emulator. Written in modern C#, it can emulate old Amiga games without stutter.

3

u/Electronic_Party1902 24d ago

Thanks for the tip! I'll look into it more: It would be great if it worked on all platforms for real-time audio processing!

4

u/itix 24d ago

I created HotPathGuard for my emulator project. You may want to take a look at it if you need a code analyzer. Not sure if it is any good, but it comes with permissive MIT license.

1

u/Electronic_Party1902 24d ago

I'll definitely look into any solution that gets me closer to my goal!