Sound players: start, pause and position
Complete C# examples without external audio: controls, voices, and 2D/3D listeners.
Choose a player
| Type | Purpose | What affects volume |
|---|---|---|
AudioStreamPlayer | Music and interface | Player and buses; world position is ignored |
AudioStreamPlayer2D | A source in a 2D world | Player, listener distance/direction, then buses |
AudioStreamPlayer3D | A source in a 3D world | Attenuation model, listener, direction, additional 3D settings, then buses |
An AudioStream is a resource, not an already playing voice. For a project file, assign a resource loaded with ResourceLoader.Load<AudioStream>(path) to Stream; check for null before playing. Built-in formats include WAV, Ogg Vorbis and MP3; import settings determine compression and looping. Short effects need attention to decoding latency and simultaneous voices; music needs attention to resource size and decoding cost. Do not choose a format solely by its extension.
Run the example
Add both files below to a C# project. Create a level with a Control root, attach SoundDemo.cs, build and run with F6. Restart plays a one-second tone; pause before its end, then resume. Stop stops playback without a natural-end event. No external WAV is needed.
DemoTone creates mono 16-bit PCM at 22,050 Hz with short fades at its edges. Data contains raw little-endian samples without a WAV header. LoopBegin and LoopEnd are sample indices, not seconds; for stereo they mean frames containing a channel pair. Its parameter limits and 30-second cap belong to this teaching generator.
DemoTone.cs
using System;
using System.Buffers.Binary;
using Reckfell;
public static class DemoTone
{
public static AudioStreamWav Create(float frequency = 440, double seconds = .3,
float amplitude = .15f, bool loop = false)
{
const int rate = 22050;
if (!float.IsFinite(frequency) || frequency <= 0 || frequency >= rate / 2f ||
!double.IsFinite(seconds) || seconds < .02 || seconds > 30 ||
!float.IsFinite(amplitude) || amplitude < 0 || amplitude > 1)
throw new ArgumentOutOfRangeException(nameof(frequency), "Invalid teaching-tone parameters.");
int count = (int)(seconds * rate);
byte[] data = new byte[count * 2];
for (int i = 0; i < count; i++)
{
double fade = Math.Min(1, Math.Min(i, count - 1 - i) / (rate * .005));
short value = (short)(Math.Sin(2 * Math.PI * frequency * i / rate) * amplitude * fade * short.MaxValue);
BinaryPrimitives.WriteInt16LittleEndian(data.AsSpan(i * 2, 2), value);
}
return new AudioStreamWav
{
Format = AudioStreamWav.FormatEnum.Format16Bits, MixRate = rate, Stereo = false,
Data = data, LoopMode = loop ? AudioStreamWav.LoopModeEnum.Forward : AudioStreamWav.LoopModeEnum.Disabled,
LoopBegin = 0, LoopEnd = count
};
}
}SoundDemo.cs
using System;
using Reckfell;
public partial class SoundDemo : Control
{
public AudioStreamPlayer Voice { get; private set; } = null!;
public int FinishedCount { get; private set; }
public Label Status { get; private set; } = null!;
private bool _ru;
private string Text(string ru, string en) => _ru ? ru : en;
public override void _Ready()
{
_ru = TranslationServer.GetLocale().StartsWith("ru", StringComparison.OrdinalIgnoreCase);
Voice = new AudioStreamPlayer
{
Name = "Voice", Stream = DemoTone.Create(seconds: 1), MaxPolyphony = 1,
PlaybackType = AudioServer.PlaybackType.Stream
};
AddChild(Voice);
var layout = new VBoxContainer { Position = new Vector2(24, 24) }; AddChild(layout);
Status = new Label { Text = Text("Готово", "Ready") }; layout.AddChild(Status);
AddButton(layout, Text("Запустить заново", "Restart"), Restart);
AddButton(layout, Text("Пауза", "Pause"), () => SetPaused(true));
AddButton(layout, Text("Продолжить", "Resume"), () => SetPaused(false));
AddButton(layout, Text("Остановить", "Stop"), StopSound);
Voice.Finished += () => { FinishedCount++; Status.Text = Text("Завершено", "Finished"); };
}
private static void AddButton(VBoxContainer parent, string text, Action action)
{
var button = new Button { Text = text }; parent.AddChild(button); button.Pressed += action;
}
public void Restart()
{
Voice.Stop(); Voice.Play();
Status.Text = Text("Воспроизведение", "Playing");
}
public void SetPaused(bool paused)
{
if (!Voice.IsPlaying() && !Voice.StreamPaused) return;
Voice.StreamPaused = paused;
Status.Text = paused ? Text("Пауза", "Paused") : Text("Воспроизведение", "Playing");
}
public void StopSound()
{
Voice.Stop(); Status.Text = Text("Остановлено", "Stopped");
}
}States and multiple voices
| Action / check | Result in Stream mode |
|---|---|
Play() | Creates a voice at the beginning; Play(seconds) chooses a start position |
StreamPaused = true | Pauses existing voices; IsPlaying() becomes false while HasStreamPlayback() stays true |
StreamPaused = false | Resumes paused voices |
Stop() | Stops all voices of this player without Finished |
Seek(seconds) | Restarts playback at a position only while the player is playing; does nothing when stopped or paused |
| Natural end | Emits Finished; looping does not emit an end event |
StreamPaused does not retain a pending pause when no voices exist. Set it after Play(). A guard such as if (!IsPlaying()) return before resuming is incorrect: IsPlaying is already false while paused. The example accounts for both states.
MaxPolyphony = 1 keeps one voice; a new Play replaces the oldest. With a larger limit, Play adds overlapping sound. Finished means one or more voices ended during an update: others can still be playing. It does not guarantee that the whole player is empty. Check HasStreamPlayback() after the event. Use separate players or a dedicated polyphonic stream for independently controlled sources.
GetStreamPlayback() returns the most recently created playback object. Keeping a reference to it does not replace checking the player's current state. Deleting a player ends its sound: a temporary explosion must live until its sound ends, or delegate playback to an owner that lives longer.
A 2D source
Keep DemoTone in the project. In a separate level, attach SpatialSound2D.cs to an Entity2D root; set a 512 × 288 viewport and run with F6. The white point is the listener and blue is the source. Arrow keys move the source: stronger left channel on the left, stronger right on the right, quieter with distance. The example uses the standard ui_left/right/up/down actions.
SpatialSound2D.cs
using Reckfell;
public partial class SpatialSound2D : Entity2D
{
public AudioListener2D Listener { get; private set; } = null!;
public AudioStreamPlayer2D Source { get; private set; } = null!;
public override void _Ready()
{
GetViewport().AudioListenerEnable2D = true;
Listener = new AudioListener2D { Name = "Listener", Position = new Vector2(256, 144) };
AddChild(Listener); Listener.MakeCurrent();
Source = new AudioStreamPlayer2D
{
Name = "Source", Position = new Vector2(100, 144), Stream = DemoTone.Create(loop: true),
MaxDistance = 600, Attenuation = 1, PanningStrength = 2,
PlaybackType = AudioServer.PlaybackType.Stream
};
AddChild(Source); Source.Play();
}
public override void _Process(double delta)
{
Source.Position += Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down") * 160 * (float)delta;
QueueRedraw();
}
public override void _Draw()
{
if (Source is null || Listener is null) return;
DrawCircle(Listener.Position, 8, Colors.White);
DrawCircle(Source.Position, 8, new Color(.15f, .35f, 1));
DrawLine(Listener.Position, Source.Position, new Color(.4f, .4f, .4f), 1);
}
}Without an explicit AudioListener2D, the listener is at the center of the relevant viewport's visible region. A camera changes that region's transform. An explicit listener can attach hearing to a character. In 2D, MaxDistance uses 2D world units, usually pixels; panning also depends on viewport size and scale. Do not copy meter values from 3D.
A 3D source
In another level, attach SpatialSound3D.cs to an Entity3D root. Keep DemoTone in the project. Code creates a listener at the origin, a camera and a blue source sphere; arrow keys move it on XZ. The camera displays the world while the separate AudioListener3D defines the listening point.
SpatialSound3D.cs
using Reckfell;
public partial class SpatialSound3D : Entity3D
{
public AudioListener3D Listener { get; private set; } = null!;
public AudioStreamPlayer3D Source { get; private set; } = null!;
public override void _Ready()
{
GetViewport().AudioListenerEnable3D = true;
Listener = new AudioListener3D { Name = "Listener" }; AddChild(Listener); Listener.MakeCurrent();
Source = new AudioStreamPlayer3D
{
Name = "Source", Position = new Vector3(3, 0, -3), Stream = DemoTone.Create(loop: true),
UnitSize = 1, MaxDistance = 30, PanningStrength = 1,
AttenuationFilterDb = 0, PlaybackType = AudioServer.PlaybackType.Stream
};
AddChild(Source);
var material = new StandardMaterial3D
{
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
AlbedoColor = new Color(.15f, .35f, 1)
};
Source.AddChild(new MeshInstance3D { Mesh = new SphereMesh { Radius = .2f, Height = .4f }, MaterialOverride = material });
var camera = new Camera3D { Position = new Vector3(0, 6, 10) }; AddChild(camera);
camera.LookAt(Vector3.Zero); camera.MakeCurrent();
Source.Play();
}
public override void _Process(double delta)
{
Vector2 move = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
Source.Position += new Vector3(move.X, 0, move.Y) * 4 * (float)delta;
}
}Here world units are treated as meters: UnitSize = 1, MaxDistance = 30. UnitSize affects the attenuation model; it does not resize objects. A wall alone does not muffle a source: collisions do not create automatic acoustic occlusion. Implement that separately, for example by checking an obstruction and changing a filter/volume. Areas can override audio buses through separate Area2D/Area3D settings.
Positional players start playback and calculate panning during physics updates. Do not expect final panning in the same statement sequence immediately after Play. The examples use PlaybackType.Stream so the engine mixer processes buses and effects. Check Sample behavior separately, particularly on Web; these examples do not test it.
Diagnostics
| Symptom | Check |
|---|---|
| Resume does nothing | A !IsPlaying() guard may be rejecting a paused player |
| Repeated sounds cut off | MaxPolyphony, deleting the player, Stop before Play |
| An end handler runs while sound continues | Other voices in this player, or other players |
| A positional source is silent | Same world and viewport, enabled/current listener, distance and volume |
| Several sources change their loop mode | They share one AudioStream; create a separate resource for different settings |
| Sound becomes higher and shorter | PitchScale changes pitch along with speed; it is not independent time stretching |