Step 3 of 8
Audio Sources
Everything that makes a sound. Namespace: OwnaudioNET.Sources
There are four kinds of source and one wrapper. They all implement IAudioSource, so learning one teaches you the rest.
| Type | Reach for it when⦠|
|---|---|
FileSource | You have a file. Streamed from disk, seekable, tempo- and pitch-shiftable. |
SampleSource | The audio is already a float[] in memory β short clips, one-shots. |
StreamingSource | You want to generate audio β a synth, a metronome, a network stream. |
InputSource | The audio is arriving live from a mic or line input. |
SourceWithEffects | Not a source β a wrapper that gives any of the above its own effect chain. |
What they all share
| Property | Type | Default | Description |
|---|---|---|---|
Id | Guid | auto | Unique identifier for this source. |
State | AudioState | Stopped | Current playback state. |
Volume | float | 1.0 | Track volume (0.0 β 20.0). Values above 1.0 amplify. |
Pan | float | 0.0 | Stereo pan (-1.0 left β¦ 0.0 center β¦ +1.0 right), equal-power. |
Loop | bool | false | Loop when reaching end of stream. |
Position | double | β | Current playback position in seconds (read-only). |
Duration | double | β | Total duration in seconds (read-only). |
IsEndOfStream | bool | β | Whether the source has reached its end. |
Tempo | float | 1.0 | Playback speed multiplier, clamped to 0.8β1.2. Goes straight to the native track β no buffer clear, safe to set from a slider. FileSource only Wired only on FileSource; SampleSource/StreamingSource/InputSource store the value but do not apply it (backward-compatibility surface). |
PitchShift | float | 0.0 | Pitch shift in semitones, clamped to -12 β¦ +12. Goes straight to the native track β no buffer clear, safe to set from a slider. FileSource only Wired only on FileSource; 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). |
OutputChannelMapping | int[]? | null | Hardware channel routing. See Channel Routing. |
source.Play();
source.Pause();
source.Stop();
source.Seek(double positionInSeconds); // returns bool
source.RouteToChannels(params int[] channels); // fluent helper
source.Dispose();Common Events
| Event | Args | Description |
|---|---|---|
StateChanged | AudioStateChangedEventArgs | Playback state changed. |
Error | AudioErrorEventArgs | An error occurred during playback. |
PositionChanged | EventArgs | Position 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
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
| Property | Type | Default | Description |
|---|---|---|---|
StartOffset | double | 0.0 | Timeline start position in seconds for master clock sync. |
IsAttachedToClock | bool | false | Whether attached to a master clock (read-only). |
SyncTolerance | double | 0.005 | Green zone threshold: drift below this needs no correction (5ms). |
SoftSyncTolerance | double | 0.025 | Yellow zone threshold: triggers gradual tempo correction (25ms). |
SoftSyncMaxTempoAdjustment | double | 0.02 | Maximum tempo drift during soft sync correction (2%). |
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.
// 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);Complete Example
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.
// 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 bufferStreamingSource
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) without reloading or restarting anything.
Constructor
var source = new StreamingSource(
render: MyGenerator, // AudioRenderCallback
config: OwnaudioNet.CreateDefaultConfig()
);The Render Callback
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
| Member | Value | Notes |
|---|---|---|
Duration | double.PositiveInfinity | A generator has no end. |
IsEndOfStream | false | Never runs out of audio. |
Position | double | Seconds since the last seek target. |
Seek(seconds) | true | Drops 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.
Complete Example β sine generator
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 ~120 ms, no restart neededStreamingSource 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.
// 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.
var source = new FileSource("vocal.wav");
var trackFx = new SourceWithEffects(source);
// Build effect chain
trackFx.AddEffect(new CompressorEffect { Threshold = 0.7f, 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);
Channel Routing
Route individual sources onto specific channels of a multi-channel output. Routing is done entirely with the per-source OutputChannelMapping: it places each source onto the channels of the mixer bus, and the bus's Config.Channels channels map straight through to the audio interface in order (bus channel 0 β physical 0, 1 β 1, β¦).
// 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.
| Scenario | How | Result |
|---|---|---|
| Mono file β stereo output | targetChannels: 2 | L and R are identical (duplication) |
| Stereo file β mono output | targetChannels: 1 | (L + R) Γ 0.5 β equal power sum |
// 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);// 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);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.