r/GodotCSharp

▲ 90 r/GodotCSharp+1 crossposts

Who said C# cannot do real-time audio synthesis? I built a programmatic DAW to find out.

(Links showing the app in action here and below)

Throughout my career, I have always heard that real-time audio belongs strictly to C++ or native first languages. If you try it in a managed language, people tell you the garbage collector will stutter, the buffer will underrun, and your speakers will crackle.

I am a systems engineer by trade, but I am also an amateur music producer. Over the years, I have used Ableton, Reason, Fruity Loops, Cakewalk, and Max/MSP, but each of them either has a massive learning curve or just feels disconnected from certain forms of music. Traditional DAWs force you into a grid, which makes music sound block-like and square. I wanted to explore more organic, generative possibilities for ambient and electronic music without the massive friction of building a physical Eurorack or writing complex software in PureData.

Originally, I just wanted to build a portable, cross-platform C# library for sound processing. To dogfood it, I decided to embed it in a Godot app. Little by little, I kept building devices until I got carried away and ended up with a programmatic, node-based DAW called Sigilgraph.

Here is exactly how it works under the hood and how a 100% C# engine guarantees it will never underrun the audio buffer.

The Pull Model and Cache Locality

At its core, the synthesis engine is a graph of operators that gets pulled directly by the audio driver. The hard part is giving the driver absolute guarantees that this graph will finish processing in time.

To do that, the entire graph processing needs to live in the CPU cache. You have to avoid heap allocation and heap access completely. The hot path must never allocate objects or arrays. Because of this, the garbage collector is left completely unused 99% of the time, saving it for trivial, low-frequency allocations completely outside the audio loop.

Think about the math for a 96 kHz stereo stream. The engine needs to guarantee that there are always enough samples sitting in a 256-sample buffer at the audio driver level. This means processing at least 192,000 floats per second.

It sounds uninteresting on paper, but those 192,000 floats are traversing a graph of over 100 operators before they hit the driver. We are talking about Fourier transforms, filter s-domain operations, delay buckets, and dozens of oscillators, all offering the opportunity to have their parameters changed in real-time. There is no heavy message passing and there are no extra abstraction layers. Everything is mutated directly in real-time.

To keep things packed tightly in memory, I used structs and Span<T> pervasively, completely replacing standard heap arrays on the hot path. To maintain strict cache locality, the engine processes these spans in blocks and leverages SIMD hardware intrinsics via Vector<T> for heavy lifting like Fourier transforms and intensive add-multiply operations. Think of it this way: instead of a web of heap objects being managed and looked up, the engine uses a pull model where the audio driver pulls samples from the bottom up. The entire graph evaluates as a massive, deeply nested functional cascade up to 200 levels deep on the call stack, constructed using a fluent API builder pattern. To keep this structure stable and prevent cyclic evaluations without introducing overhead, the nodes utilize simple reentrant flags. Live session mutations work by simply appending or decoupling pre-allocated sub-graphs at the block boundaries. This architecture guarantees we never starve the audio driver buffer, avoiding expensive object reconstruction while playing.

The Lambda Trick for Parameters

When it came to connecting the UI to the bottom of the graph operators, I chose to use delegates and lambdas. This was a deliberate compromise for simplicity. I could have used ref float pointers everywhere, but the code would have become incredibly brittle and less idiomatic for C#. Lambdas give us composition and allow for the live replacement of behaviors on the fly.

The trick to making lambdas performant enough for real-time audio is to never evaluate them per sample. Instead, they are evaluated once per block.

By evaluating parameters at the block level, you reduce function calling overhead by 64x. On a standard 48 kHz sampling rate, you only run about 750 parameter evaluations per second (48,000 / 64). This gives you more than enough resolution for smooth parameter changes, and it spares us from having to run a dedicated control signal rail. We just interleave the parameter updates right at the start of the sample block processing.

Picking Your Fights and Profiling

Getting to this point required a lot of deep technical analysis. Tracing tools like JetBrains dotTrace and dotMemory were absolutely essential for this task. Monitoring memory spikes, hunting down hidden GC calls, and doing rigorous hot path analysis was paramount. If you don't profile, you are just guessing.

But the biggest lesson I learned during this project was that you have to pick your battles. You have to let some things go.

While the sample generation and parameter parsing paths are completely locked down and allocation-free, all the note rail and note-passing logic is actually completely garbage collected. Why? Because notes are incredibly small objects and they trigger very infrequently compared to the millions of samples and parameters flowing through the system. Trying to optimize the note-passing system into unmanaged memory structures would have been a massive waste of development time for zero real-world performance gain.

Why C# is More Than Capable

This project completely changed how I look at the C# memory model. It proved to me that modern .NET is an incredibly capable environment for high-performance audio because it gives you the best of both worlds.

It allows for low-level, granular control over memory and hardware instructions where you absolutely need that control, but it lets you drop back into the comfort and simplicity of a managed language for the things that don't matter as much. You don't have to sacrifice productivity to get bare-metal execution speed anymore.

I would love to hear your thoughts on this architecture, and I am happy to dive into the weeds in the comments if anyone has questions about the node evaluation loop, the SIMD operations, or how we profiled the hot paths.

Here is a demo of the project in action: Sigilgraph × VST3 (Trailer #2)
The project page: Sigilgraph Audio Workbench

r/Sigilgraph

u/Sigilgraph — 6 days ago
▲ 1.7k r/GodotCSharp+1 crossposts

SpriteStack2D: Add-on to create fake 3D from a single texture

You can find it here: https://github.com/VitSoonYoung/SpriteStack2D

In my last post I mention credit to Berlin Nights's Sprite Stacker, but I only borrow the texture as a test image, I didn't use any of his code. Sorry for the confusion as this is not my main language.

u/Novaleaf — 8 days ago
▲ 128 r/GodotCSharp+1 crossposts

How to check if RayCast2D hits a corner of tilemap?

I wanna make rope physics in my game and I want them to wrap around any corner. Right now the only issue I have so far is TileMap corner detection, I've even asked AI for help, but the code it wrote me is kinda unreliable (as always :>).

And I'm using C# but I don't I think I'll have any problems translating GDScript to C#.

So, I would appreciate any help! Thanks!

u/SeniorMatthew — 10 days ago
▲ 21 r/GodotCSharp+1 crossposts

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

u/Electronic_Party1902 — 12 days ago