Sound & live coding

DanaProcessing has its own audio engine, written from scratch in C# and running inside the sketch's own process — no SuperCollider, no external synth, no extra package. It follows Sonic Pi's way of thinking: live loops that repeat in time with each other, Sleep measured in beats, built-in synths and drums. Because sound and drawing share one clock, visuals can react to the exact moment a note is heard. Everything here is on Sketch and needs no using of its own.

Sound

On this page · 19 entries
C#
public class MySketch : Sketch
{
    float flash;

    public override void Setup()
    {
        Size(800, 500);
        Bpm = 120;
    }

    [LiveLoop]
    async Task Drums()
    {
        Sample("kick");
        await Sleep(1);
        Sample("kick");
        Sample("snare");
        await Sleep(1);
    }

    [LiveLoop]
    async Task Bass()
    {
        UseSynth(Synth.Saw);
        foreach (var note in new[] { "E2", "G2", "A2", "B2" })
        {
Play(note, amp: 0.5, release: 0.3, cutoff: 80);
await Sleep(0.5);
        }
    }

    public override void OnNote(NoteEvent e)
    {
        if (e.Sample == "kick") flash = 1;   // exactly when the kick is heard
    }

    public override void Draw()
    {
        Background(20 + flash * 60, 20, 30);
        flash *= 0.85f;
        float r = 80 + Amplitude * 600;   // grows with the volume
        Ellipse(Width / 2f, Height / 2f, r, r);
    }
}
Live coding in the IDE. While the loops play, change a note or a Sleep and press Run (Ctrl+R) or Hot Reload: the music doesn't stop. Each loop picks up its new code on its next pass, in time with the others. A loop you add joins on the next beat; a loop you delete (or whose [LiveLoop] you remove) finishes its current pass and stops. Sketch → Stop sound (Ctrl+.) silences everything and leaves the visuals running; the Run button, which reads ■ Stop while a sketch plays (Shift+F5), stops the sketch and its sound together. Loop errors show up in the Run tab of the bottom panel.
How the timing stays tight. Sleep doesn't pause the thread for "about" that long — it advances the loop's own musical clock by exactly that many beats. Loops run about 100 ms ahead of what you hear, and every sound is stamped with its exact sample position, so however late the thread pool wakes a loop up, the beat still lands on the exact sample. The audio thread itself never allocates memory, so garbage collection doesn't cause clicks.
Platforms. Sound output currently works on Windows (through the built-in winmm API, no install needed). On macOS and Linux the engine, live loops, OnNote and the analysis functions all run on the same clock — you just don't hear anything yet.

[LiveLoop]

  • [LiveLoop] attribute on async Task Name()

Marks a sketch method as a live loop, like Sonic Pi's live_loop: it starts after Setup() and repeats forever, in sync with every other loop. The method must be async Task with no parameters, and every pass must await Sleep(...) at least once — a loop that finishes a pass without sleeping is stopped (and reported) instead of freezing the audio. Each pass runs the loop's code from the current version of the sketch, which is what makes Run and Hot Reload swap code without a gap.

Sleep

  • Sleep(double beats) → Task

Waits beats beats at the current Bpm. Always await it. Inside a [LiveLoop] it's musical time — exact, never drifting, shared with every loop. Outside a loop it's a plain real-time delay.

Bpm → double · get/set

Tempo in beats per minute, shared by every loop. Default 60 (one beat = one second), like Sonic Pi. Usually set once in Setup().

UseSynth

  • UseSynth(Synth synth)

Chooses the instrument for the following Play() calls — per loop when called inside one, otherwise for the whole sketch. The default is Synth.Sine. See the Synth enumeration below.

Play

  • Play(double note, …)
  • Play(string note, …)
  • Play(int[] notes, …)

Plays a note. note is a MIDI number (60 = C4, fractions allowed for microtones), a name like "C4", "F#3", "Eb2", or an array for a chord (Play(Notes.Chord("C4", "minor"))). Inside a loop the note sounds at the loop's current musical time; outside a loop (e.g. in MousePressed()) it sounds right away. All the optional parameters are named, Sonic Pi style:

parameterdefaultmeaning
amp1Volume. Several loud voices at once are softly limited, never hard-clipped.
pan0Stereo position, −1 (left) to 1 (right).
attack · decay · sustain · release0 · 0 · 0 · 1Envelope in seconds: rises over attack, falls to sustainLevel over decay, holds for sustain, fades out over release. Total length = the sum of the four.
sustainLevel1Level held during sustain, 0–1.
cutoff0 (off)Low-pass filter, as a MIDI note like Sonic Pi: 80 ≈ 830 Hz (dark), 110 ≈ 4.7 kHz (bright).
res0Filter resonance, 0–1.
synthcurrentOverrides UseSynth for this one note.

Sample

  • Sample(string name, double amp = 1, double rate = 1, double pan = 0)
  • Sample(SampleBuffer buffer, …)

Plays a sound. The built-in drums need no files: "kick" (or "bd"), "snare" ("sn") and "hat" ("hh"). Any other name is a path to a .wav file, loaded once and cached. rate changes speed and pitch together: 2 is an octave up and twice as fast, 0.5 an octave down.

LoadSample

  • LoadSample(string path) → SampleBuffer

Loads a .wav file into memory (PCM 8/16/24/32-bit or float, mono or stereo, any sample rate), cached by path. Call it in Setup() so the first Sample() doesn't have to read the disk.

Amplitude

  • Amplitude → float

How loud the sketch's sound is right now, 0–1, smoothed (rises instantly, falls gently) so it's ready to drive a size or a brightness. 0 while nothing has played.

Spectrum

  • Spectrum(float[] bands)

Fills bands with the current spectrum: bands.Length bands, log-spaced from 30 Hz (index 0) to 16 kHz, each 0–1 (0 = −60 dB, 1 = full scale). Allocate the array once as a field and reuse it every frame.

Waveform

  • Waveform(float[] dest)

Fills dest with the latest output samples (−1 to 1, mono, oldest first) — up to 4096 of them. Handy for an oscilloscope line.

OnNotevirtual

  • OnNote(NoteEvent e) → void

Called for every note and sample at the moment it reaches the speakers — not when the code that played it ran — on the drawing thread, right before Draw(). Override it to make visuals hit exactly on the beat. Events that pile up while drawing is paused (for instance while you type in the IDE's editor) are dropped instead of arriving in a burst.

NoteEvent

  • NoteEvent record struct

Time (engine seconds), Note (MIDI number; 0 for samples), Amp, Synth, Sample ("kick", "snare", a file name… or null for notes) and Loop (the name of the [LiveLoop] method that played it, or null).

StopAudio

  • StopAudio()

Stops every live loop and silences every sound right away — the same as Sketch → Stop sound in the IDE.

Notes.Parse · Notes.ToFrequency

  • Notes.Parse(string name) → int
  • Notes.ToFrequency(double midi) → double

Note names to MIDI numbers and MIDI numbers to Hz. Same convention as Sonic Pi: "C4" = 60, "A4" = 69 = 440 Hz. Sharps are # or s, flats b; the octave defaults to 4. A static class rather than Sketch methods because Sketch already has a Scale() — the transform one.

Notes.Chord

  • Notes.Chord(root, string kind = "major") → int[]

The notes of a chord built on root (a name or a MIDI number): Notes.Chord("C4", "minor") → [60, 63, 67]. Kinds: major, minor, dim, aug, 7, maj7, m7, sus2, sus4, power.

Notes.Scale

  • Notes.Scale(root, string kind = "major", int octaves = 1) → int[]

The notes of a scale, ending on the root an octave (or more) up, like Sonic Pi: Notes.Scale("E2", "minor_pentatonic") → [40, 43, 45, 47, 50, 52]. Kinds: major, minor, harmonic_minor, dorian, phrygian, lydian, mixolydian, major_pentatonic, minor_pentatonic, blues, chromatic.

SampleBuffer

A sound in memory, ready for Sample(). SampleBuffer.LoadWav(path) reads a file; SampleBuffer.FromMono(float[] data, int sampleRate) wraps a signal you generated in code. Read-only properties: Left, Right, Frames, SampleRate, Channels, Duration (seconds), Name. Files at a different sample rate are resampled on playback, so they always play at the right pitch.

AudioEngine.Sharedadvanced

  • AudioEngine.Shared → AudioEngine

The one engine every sketch shares (namespace DanaProcessing.Audio). It outlives individual sketches, which is why pressing Run doesn't cut the music, and only opens the sound device once something actually plays. Sketches rarely need it directly; the tunables are ScheduleAhead (seconds loops run ahead of what you hear, default 0.1), MasterVolume (0–1, default 0.7) and OutputDescription. Hosts call AudioEngine.ActivateSketch(sketch) after Setup() and after a hot-reload swap (the Avalonia host already does), can show AudioEngine.DiagnosticReported messages, and stop everything with AudioEngine.StopEverything().

Synth
SinePure tone — the default, like Sonic Pi's :beep.
SawBand-limited sawtooth: bright; great for bass and leads with cutoff.
SquareBand-limited square: hollow, retro.
TriangleSoft, flute-like.
PluckPlucked string (Karplus–Strong).
NoiseWhite noise — with a low cutoff, wind or waves.
Kick · Snare · HatSynthesized drums, also reachable as Sample("kick"), "snare", "hat". They ignore the note and the envelope.

Runnable example

A complete sketch using this section. Test in Dana opens it in the IDE, or paste it into a new tab and press Run.

C#
// Sound: live loops keep the beat, Play() makes notes, Amplitude drives visuals.
public class Beat : Sketch
{
    public override void Setup()
    {
        Size(500, 300);
        Bpm = 110;
    }

    [LiveLoop]
    async Task Drums()
    {
        Play(36, synth: Synth.Kick);
        await Sleep(1);
        Play(60, synth: Synth.Hat, amp: 0.4);
        await Sleep(1);
    }

    [LiveLoop]
    async Task Melody()
    {
        Play(new[] { 60, 63, 67, 70 }[(int)Random(4)], synth: Synth.Pluck, release: 0.4);
        await Sleep(0.5);
    }

    public override void Draw()
    {
        Background(20, 20, 30);
        float level = Amplitude;
        Fill(255, 120, 160); NoStroke();
        Circle(Width / 2, Height / 2, 40 + level * 400);
    }

    public override void MousePressed() => Play(72, synth: Synth.Saw, release: 0.3);
}