Step 3 of 8

Audio Sources

Everything that makes a sound. Namespace: OwnaudioNET.Sources

There are five kinds of source and one wrapper. They all implement IAudioSource, so learning one teaches you the rest.

TypeReach for it when…
FileSourceYou have a file. Streamed from disk, seekable, tempo- and pitch-shiftable.
SampleSourceThe audio is already a float[] in memory β€” short clips, one-shots.
GroupSourceSeveral files belong on one track β€” a DAW lane with its clips, sharing one tempo, effect chain and fader.
StreamingSourceYou want to generate audio β€” a synth, a metronome, a network stream.
InputSourceThe audio is arriving live from a mic or line input.
SourceWithEffectsNot a source β€” a wrapper that gives any of the above its own effect chain.

What they all share

PropertyTypeDefaultDescription
IdGuidautoUnique identifier for this source.
StateAudioStateStoppedCurrent playback state.
Volumefloat1.0Track volume (0.0 – 20.0). Values above 1.0 amplify.
Panfloat0.0Stereo pan (-1.0 left … 0.0 center … +1.0 right), equal-power.
LoopboolfalseLoop when reaching end of stream.
Positiondoubleβ€”Current playback position in seconds (read-only).
Durationdoubleβ€”Total duration in seconds (read-only).
IsEndOfStreamboolβ€”Whether the source has reached its end.
Tempofloat1.0Playback speed multiplier, clamped to 0.8–1.2. Goes straight to the native track β€” no buffer clear, safe to set from a slider. FileSource & GroupSource Wired only on FileSource and GroupSource; SampleSource/StreamingSource/InputSource store the value but do not apply it (backward-compatibility surface).
PitchShiftfloat0.0Pitch shift in semitones, clamped to -12 … +12. Goes straight to the native track β€” no buffer clear, safe to set from a slider. FileSource & GroupSource Wired only on FileSource and GroupSource; other sources store the value but do not apply it (backward-compatibility surface).
OutputLevels(float left, float right)β€”Real-time output levels for VU metering (0.0–1.0).
OutputChannelMappingint[]?nullHardware channel routing. See Channel Routing.
C# β€” Common methods
source.Play();
source.Pause();
source.Stop();
source.Seek(double positionInSeconds);  // returns bool
source.RouteToChannels(params int[] channels); // fluent helper
source.Dispose();

Common Events

EventArgsDescription
StateChangedAudioStateChangedEventArgsPlayback state changed.
ErrorAudioErrorEventArgsAn error occurred during playback.
PositionChangedEventArgsPosition changed significantly (throttled >50ms).

FileSource

Plays audio from a file with background decoding, circular buffer, real-time pitch/tempo, and master clock synchronization. Decoding runs entirely in the native Rust engine. Supported formats out of the box: MP3, FLAC, WAV (PCM/ADPCM), AAC, ALAC, MP4/M4A, OGG/Vorbis and AIFF β€” no external codecs required. Exotic formats can additionally be handled by FFmpeg 8+ as an optional legacy fallback when it is installed on the system.

Constructor

C#
var source = new FileSource(
    filePath: "track.mp3",
    bufferSizeInFrames: 8192,   // internal circular buffer (default: 8192)
    targetSampleRate: 48000,    // auto-resampling; 0 = use file's rate
    targetChannels: 2           // auto-channel conversion; 0 = use file's channels
);

FileSource-Specific Properties

PropertyTypeDefaultDescription
StartOffsetdouble0.0Timeline start position in seconds for master clock sync.
IsAttachedToClockboolfalseWhether attached to a master clock (read-only).
SyncTolerancedouble0.005Obsolete. No effect since the network sync got its own controller; kept so old code compiles. See drift correction.
SoftSyncTolerancedouble0.025Obsolete, no effect, kept for compatibility.
SoftSyncMaxTempoAdjustmentdouble0.02Obsolete, no effect, kept for compatibility.

Tempo & Pitch Control

ℹ️

Setting .Tempo / .PitchShift and calling SetTempoSmooth() / SetPitchSmooth() do the same thing: the value is handed to the native track, with no buffer clear and no silence gap. The smooth variants only read better at slider call sites. Under a master clock use SetTempoSynced() once the slider has settled β€” it reseeks to the current position so leftover old-tempo audio cannot leave a permanent drift.

C#
// Direct set β€” mirrored onto the native track right away
source.Tempo      = 1.1f;   // 10% faster  (range: 0.8 – 1.2)
source.PitchShift = 2.0f;   // 2 semitones up  (range: -12.0 – +12.0)

// Same thing, named for slider call sites
source.SetTempoSmooth(1.1f);
source.SetPitchSmooth(2.0f);

// Clock-synced tracks: call this once the slider settled, it reseeks away the drift
source.SetTempoSynced(1.1f);

Waveform & raw samples

Three reads that open a decoder of their own, so playback is untouched and any of them may be called while the track is running.

C#
// A waveform for a 2000 pixel wide display β€” one decoder pass, one 2000-float array
float[] peaks = source.GetPeaks(2000);

// Raw samples, whole file or a slice of it
float[] all   = source.GetFloatAudioData(TimeSpan.Zero);
float[] chunk = source.GetFloatAudioData(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(10));

// Same slice as bytes, in the file's own sample format
byte[] bytes = source.GetByteAudioData(TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(10));

GetPeaks buckets the file into as many points as the display asks for and keeps the loudest sample of each, sign and all β€” so the array is a signed envelope you can draw straight, not an absolute one. Nothing file-sized is allocated along the way. A container that will not say how long it is halves its own bucket resolution as the scan runs, which is the one case where the result comes back shorter than points; size your drawing loop off peaks.Length rather than the number you asked for.

⚠️

Do not build a waveform out of GetFloatAudioData. Reading it whole holds the entire file in memory β€” a five minute stereo song is over eighty megabytes β€” and walking it in windows opens a decoder per window. GetPeaks exists for exactly this and costs one pass.

Complete Example

C#
int sr = OwnaudioNet.Engine!.Config.SampleRate;
int ch = OwnaudioNet.Engine!.Config.Channels;

var source = new FileSource("backing.mp3",
    bufferSizeInFrames: 4096,
    targetSampleRate: sr,
    targetChannels: ch);

source.Volume      = 0.8f;
source.Pan         = -0.3f;                // slightly left
source.Tempo       = GlobalTempo / 100f;  // e.g. 100 β†’ 1.0f
source.PitchShift  = 0;
source.StartOffset = 0.0;

source.StateChanged += (_, e) =>
{
    if (e.NewState == AudioState.EndOfStream)
        Console.WriteLine("Track finished.");
};

mixer.AddSource(source);

SampleSource

Plays audio from a pre-loaded float[] array. Ideal for sound effects, short clips, or synthesized audio.

C#
// Static sample
float[] samples = LoadSamplesFromFile("click.wav");
var click = new SampleSource(samples, OwnaudioNet.CreateDefaultConfig());
mixer.AddSource(click);
click.Play();

// Dynamic β€” submit samples in real time
var synth = new SampleSource(bufferSizeInFrames: 2048, OwnaudioNet.CreateDefaultConfig());
synth.AllowDynamicUpdate = true;

// Submit new samples as they are generated
synth.SubmitSamples(newSamples); // ReadOnlySpan<float>
synth.Clear();                   // flush buffer

GroupSource

Several audio files laid out on one timeline and played as a single source β€” a DAW lane with its clips. The clips are summed in the native engine into one track, so they share one tempo and pitch stage, one effect chain, one volume, pan and route. An effect tail β€” a reverb, a delay β€” rings on across a clip edge instead of stopping with it, and a gap between two clips costs nothing.

Constructor

C#
var lane = new GroupSource(
    sampleRate: 48000,        // every clip decodes to this β€” must be the mixer's rate
    channels: 2,              // every clip decodes to this width
    memoryMaxSeconds: 30.0    // files up to this long load into memory, longer ones stream
);

Clips

C#
// Loads the file and places it at 0 s / 12.5 s on the lane's content timeline
SourceClip verse  = lane.AddClip("verse.wav", 0.0);
SourceClip chorus = lane.AddClip("chorus.wav", 12.5);

// Move it β€” heard from the next render block, playing or not
chorus.StartSeconds = 16.0;

// Take it off and release its audio
lane.RemoveClip(verse);

AddClip decodes a file up to memoryMaxSeconds into memory right there, so call it off the UI thread. Longer files are only probed and stream from disk while they play, each with a short pre-roll so they are buffered by the time the cursor reaches them. A negative start lands at zero. The file is loaded once: taking the source off the mixer and adding it back β€” the usual stop/play cycle β€” places the same audio again without decoding anything.

MemberTypeDescription
ClipsIReadOnlyList<SourceClip>The clips on the lane, in the order they were added. A snapshot β€” read it once, not every frame.
DurationdoubleEnd of the last clip in content seconds, zero while the lane is empty.
PositiondoubleContent position in seconds.
ChannelsintWidth every clip decodes to.
StartOffsetdoubleTimeline start position in seconds for master clock sync, as on FileSource.
SourceClip.StartSecondsdoubleWhere the clip sits on the lane. Setting it moves the clip.
SourceClip.Duration / EndSecondsdoubleClip length, and where it ends on the lane.
SourceClip.IsInMemoryboolDecoded into memory, otherwise streamed from disk.
SourceClip.IsOnGroupboolFalse once the clip was removed.

Behaviour

⚠️

The sample rate has to match the mixer's. The clips are decoded at the lane's rate when they are added and the native group only takes them at the session rate, so AddSource / AddSourcePrepared throw ArgumentException for a mismatch instead of attaching a lane that would play nothing. Create it with OwnaudioNet.Engine!.Config.SampleRate.

ℹ️

A GroupSource plays on the native Rust chain only. In managed mode ReadSamples returns silence.

Complete Example

C#
int sr = OwnaudioNet.Engine!.Config.SampleRate;
int ch = OwnaudioNet.Engine!.Config.Channels;

var lane = new GroupSource(sr, ch);
await Task.Run(() =>
{
    lane.AddClip("intro.wav",  0.0);
    lane.AddClip("verse.wav",  8.0);
    lane.AddClip("outro.wav", 40.0);
});

var fx = new SourceWithEffects(lane);
fx.AddEffect(new ReverbEffect());   // the tail rings on over every clip edge

fx.Volume = 0.8f;
fx.Tempo  = 1.05f;

lane.StateChanged += (_, e) =>
{
    if (e.NewState == AudioState.EndOfStream)
        Console.WriteLine("Lane finished.");
};

mixer.AddSource(fx);
fx.Play();

StreamingSource

An endless source whose audio is produced by your own callback. Where SampleSource serves a fixed buffer, StreamingSource generates audio continuously β€” ideal for synthesizers, metronomes, test tones, procedural audio, or streaming from a network buffer. A parameter change takes effect within the look-ahead window (~120 ms by default, adjustable) without reloading or restarting anything.

Constructor

C#
var source = new StreamingSource(
    render: MyGenerator,                    // AudioRenderCallback
    config: OwnaudioNet.CreateDefaultConfig()
);

The Render Callback

C#
public delegate void AudioRenderCallback(
    Span<float> buffer,    // interleaved destination, exactly frameCount Γ— Config.Channels long
    int frameCount,        // frames to produce
    long framePosition     // absolute frame index of the first requested frame
);

framePosition counts from the start of the timeline and is reset by Seek() and Stop(). Generators that must stay locked to a grid β€” a metronome, an LFO β€” should derive their phase from it rather than from an internal counter, so a seek repositions them exactly.

ℹ️

The callback runs on the source's own pump thread, never on the audio thread, so taking a lock is safe. Keep it allocation-free so the feed stays ahead of playback.

Behaviour

MemberValueNotes
Durationdouble.PositiveInfinityA generator has no end.
IsEndOfStreamfalseNever runs out of audio.
PositiondoubleSeconds since the last seek target.
Seek(seconds)trueDrops the queued look-ahead and moves the render cursor. Negatives clamp to 0.
Stop()β€”Stops and rewinds the render cursor to frame 0.

The callback is not invoked until Play() is called, so constructing a source is cheap. While paused or stopped the pump thread sleeps on a wake handle and costs no CPU.

Look-ahead and latency

The pump keeps a little rendered audio queued ahead of playback. That queue is what makes a parameter change cheap β€” nothing reloads, the new value simply reaches the output once the audio rendered with the old one has played β€” but it is also the delay between setting a value and hearing it. The default 0.12 s is invisible on anything scheduled, a metronome or a sequenced part. A generator being played β€” a synth or a VST instrument fed from a MIDI keyboard β€” hears every millisecond of it, and there it is worth turning down.

MemberValueNotes
LookAheadSecondsdoubleRead/write, any thread, while playing. The value you read back is rounded to whole frames.
DefaultLookAheadSeconds0.12What a new source starts on.
MinLookAheadSeconds0.005The floor. Under it the feed starves before it fills, which is dropouts rather than lower latency.
MaxLookAheadSeconds1.0The ceiling. Anything outside the two is clamped, not rejected.
C#
var synth = new StreamingSource(RenderSynth, config);

// Live from a keyboard: trade CPU for latency
synth.LookAheadSeconds = 0.02;          // ~20 ms

// Long backing pad, nobody plays it: let the pump idle
synth.LookAheadSeconds = 0.25;

double now = synth.LookAheadSeconds;    // what it actually settled on

A shorter window costs CPU: the pump tops the feed up in the same chunks, just more often, and it naps proportionally shorter on a full feed instead of sitting at a fixed poll. Going the other way is nearly free, which is why the default is where it is.

ℹ️

Lowering the value does not flush what is already queued β€” that would click. The shorter window is in force from the next chunk the pump renders, so you hear it once the old queue has drained, within one look-ahead of the assignment.

This is only the source's share of the delay. The device buffer (BufferSize Γ· SampleRate) is added on top, and the driver's own latency after that β€” see What latency can I expect? for the whole chain.

Complete Example β€” sine generator

C#
var config = OwnaudioNet.CreateDefaultConfig();
float frequency = 440f;   // live-adjustable from the UI thread

void RenderSine(Span<float> buffer, int frameCount, long framePosition)
{
    int ch = config.Channels;
    double step = 2.0 * Math.PI * frequency / config.SampleRate;

    for (int f = 0; f < frameCount; f++)
    {
        // phase comes from the absolute position β€” a seek stays in phase
        float sample = (float)Math.Sin((framePosition + f) * step) * 0.2f;
        for (int c = 0; c < ch; c++)
            buffer[f * ch + c] = sample;
    }
}

var tone = new StreamingSource(RenderSine, config);
tone.Volume = 0.8f;
mixer.AddSource(tone);
tone.Play();

frequency = 880f;   // takes effect within LookAheadSeconds, no restart needed
ℹ️

StreamingSource works with SourceWithEffects and Channel Routing exactly like every other source.

InputSource

Captures audio from an input device (microphone, line-in) and routes it into the mixer.

C#
// Enable input in AudioConfig first
var config = OwnaudioNet.CreateDefaultConfig();
config.EnableInput = true;
await OwnaudioNet.InitializeAsync(config);
OwnaudioNet.Start();

var mixer = new AudioMixer(OwnaudioNet.Engine!.UnderlyingEngine);
mixer.Start();

var mic = new InputSource(OwnaudioNet.Engine!, bufferSizeInFrames: 8192);
mixer.AddSource(mic);
mic.Play();

// Monitor input peak levels
var (leftLevel, rightLevel) = mic.GetInputLevels();

SourceWithEffects

Wraps any IAudioSource to add a per-track effect chain. Effects run in the order they are added.

C#
var source   = new FileSource("vocal.wav");
var trackFx  = new SourceWithEffects(source);

// Build effect chain
trackFx.AddEffect(new CompressorEffect { Threshold = -12f, Ratio = 4f });
trackFx.AddEffect(new ReverbEffect { RoomSize = 0.4f, Mix = 0.2f });
trackFx.AddEffect(new EqualizerEffect());

// Add the wrapper to the mixer β€” NOT the raw source
mixer.AddSource(trackFx);

// Manage the chain
trackFx.RemoveEffect(effect);   // returns bool
trackFx.ClearEffects();
int count           = trackFx.EffectCount;
IEffectProcessor[] fx = trackFx.GetEffects();
⚠️

If you add effects to a source that is already in the mixer, remove the raw source and add the SourceWithEffects wrapper: mixer.RemoveSource(source.Id); mixer.AddSource(trackFx); β€” and copy the patch over with it (trackFx.OutputRoute = source.OutputRoute;, same for OutputChannelMapping). The wrapper is a different source as far as the mixer is concerned, so without that the track falls back to the identity route on channels 1/2.

Channel Routing

Route individual sources onto specific channels of a multi-channel output. There are two per-source forms β€” OutputChannelMapping, below, and the destination-indexed OutputRoute that can fan one channel out to several outputs. Both place the source onto the channels of the mixer bus, whose width is AudioConfig.EffectiveOutputChannels (that is OutputChannels when you set it, otherwise Channels), and the bus channels reach the interface in order: bus 0 β†’ physical 0, 1 β†’ 1, and so on.

C#
// Open a 4-channel output β€” the 4 bus channels drive physical channels 0–3 in order
var config = new AudioConfig
{
    Channels = 4
};

// Route music to channels 0+1, metronome to channels 2+3
var music     = new FileSource("music.mp3");
var metronome = new FileSource("click.wav");

music.OutputChannelMapping     = new[] { 0, 1 };
metronome.OutputChannelMapping = new[] { 2, 3 };

// Fluent style
music.RouteToChannels(0, 1);
metronome.RouteToChannels(2, 3);

mixer.AddSource(music);
mixer.AddSource(metronome);
ℹ️

The OutputChannelMapping array length must equal the source's Config.Channels. Channels are zero-indexed.

Channel Conversion (Upmix / Downmix)

OutputChannelMapping maps source channels 1-to-1 to output channels β€” it does not change the channel count of the source itself. To split a mono source across two channels, or sum a stereo source to mono, use the targetChannels parameter on FileSource. The decoder performs the conversion before mixing, and Config.Channels reflects the converted count.

ScenarioHowResult
Mono file β†’ stereo outputtargetChannels: 2L and R are identical (duplication)
Stereo file β†’ mono outputtargetChannels: 1(L + R) Γ— 0.5 β€” equal power sum
C# β€” Stereo file used as mono (downmix)
// Stereo file loaded as mono β€” decoder sums L+R to a single channel
var source = new FileSource("stereo.wav", targetChannels: 1);
// source.Config.Channels == 1

source.OutputChannelMapping = new[] { 0 }; // route the mono signal to output channel 0
mixer.AddSource(source);
C# β€” Mono file spread across two output channels (upmix)
// Mono file loaded as stereo β€” decoder duplicates the signal to both channels
var source = new FileSource("mono.wav", targetChannels: 2);
// source.Config.Channels == 2

source.RouteToChannels(2, 3); // spread to output channels 2+3
mixer.AddSource(source);

Fan-out routing with OutputRoute

OutputChannelMapping is source-indexed: it answers "where does my channel i go?", so one source channel can only ever reach one output. OutputRoute turns the question around β€” it is destination-indexed, answering "which of my channels does bus channel dst take?" β€” and because two destinations may name the same source channel, one signal can land on several outputs at once.

C# β€” one mono click onto two separate outputs
var click = new FileSource("click.wav", targetChannels: 1);

// bus 0 ← nothing, bus 1 ← nothing, bus 2 ← src 0, bus 3 ← src 0
click.OutputRoute = new OutputRoute(new[] { -1, -1, 0, 0 });

// Fluent, with a per-destination gain β€” the drummer's cue at half level
click.RouteTo(new[] { -1, -1, 0, 0 }, new[] { 1f, 1f, 0.5f, 0.5f });

mixer.AddSource(click);
MemberTypeNotes
SourceForChannelint[]One entry per bus channel. -1 means that channel gets nothing from this source.
Gainsfloat[]?Linear gain per bus channel, or null for unity. Same length as the map.
OutputRoute.Identity(n)OutputRouteStraight i β†’ i over the first n channels.
ℹ️

Both forms coexist: set OutputRoute and it wins, leave it null and OutputChannelMapping stays in charge. Either can be changed while audio is running β€” the mixer picks it up on its next control tick, without reopening a stream, which is what makes live re-patching safe on ASIO. At most 16 channels are routable.

⚠️

A route the engine cannot render is rejected, not quietly trimmed: more than 16 bus channels, a source channel the track has not got (a 2 on a stereo source), or a non-finite gain. The track keeps the routing it had and the mixer logs which source was refused and why β€” so if a route seems to have no effect, turn the log on and read the line.

Per-source decode width

By default a source is decoded at the mixer's bus width, so an 8-channel bus meant an 8-channel decode, time-stretch and effect chain for every source β€” four times the work for a stereo song. FileSource.DecodeChannels unties the two: the source is processed at its own width and only the summation into the bus is wide.

C#
var song = new FileSource("song.flac");
song.DecodeChannels = 2;                        // stereo chain on an 8-channel bus
song.OutputRoute    = new OutputRoute(new[] { 0, 1, -1, -1, -1, -1, -1, -1 });

mixer.AddSource(song);                          // set it before adding, it is read on attach
ℹ️

Leave DecodeChannels null and nothing changes β€” the source follows the bus exactly as before. Where the signal lands is OutputRoute's job, not this one.

Picking physical input channels

Every live InputSource feeds off a single shared capture stream, so several live inputs can run at once β€” on ASIO that matters, because a driver takes one client. CaptureChannels says which physical inputs a given source takes, and its length is the source's own width.

C# β€” two live inputs on different sockets
var vocal = new InputSource(OwnaudioNet.Engine!);
vocal.CaptureChannels = new[] { 4 };          // mono, off physical input 5

var guitar = new InputSource(OwnaudioNet.Engine!);
guitar.CaptureChannels = new[] { 0, 1 };      // stereo, off inputs 1+2

mixer.AddSource(vocal);
mixer.AddSource(guitar);

// How many inputs are really there? Ask the engine, not the config.
int available = OwnaudioNet.Engine!.ActualInputChannels;
ℹ️

null keeps the old behaviour: the first N physical inputs, repeating the last one so a mono microphone still fills a stereo track. Changing it while running re-taps the source on the next control tick β€” no stream is reopened.

Because that one stream feeds everything, losing it takes every live input silent at once. The mixer reports it through StreamFaulted with Direction == AudioStreamDirection.Input, so an unplugged interface is an event and a log line rather than a room full of dead microphones.

Next

⚠️

Setting OutputChannelMapping to an array whose length does not match Config.Channels throws ArgumentException. Always set targetChannels first if you need a different channel count.