reckfellENGINE

Audio timing: position, pause and seeking

A complete C# time display accounting for mixing, estimated latency and seek resets.

On this page

Choose the clock

For decorative interface animation, delta is sufficient. When motion must follow music position, summing delta creates a separate clock: TimeScale changes, pauses and seeks can diverge from audio. GetPlaybackPosition() belongs to the player's most recent voice, is updated by the audio mixer and advances in steps between mixes.

For one stream at speed 1, estimate GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - AudioServer.GetOutputLatency(). The first term is position, the second interpolates between mixes and the third estimates output delay. This does not measure when sound reaches the ear: the device, Bluetooth, OS and display add delays. Offer user calibration for a rhythm game.

Run a complete clock

Add DemoTone.cs and MusicClockDemo.cs below. Attach MusicClockDemo to a Control root, build and run with F6. Restart starts a four-second test tone; seconds advance, pause freezes them and 1.5 s seeks. The number after 120 BPM is a hypothetical beat index starting at zero; the tone is not a musically annotated track.

MusicClockDemo.cs
C#
using System;
using Reckfell;

public partial class MusicClockDemo : Control
{
    public AudioStreamPlayer Voice { get; private set; } = null!;
    public Label Status { get; private set; } = null!;
    public int FinishedCount { get; private set; }
    private double _last;
    private double _outputLatency;

    public override void _Ready()
    {
        bool ru = TranslationServer.GetLocale().StartsWith("ru", StringComparison.OrdinalIgnoreCase);
        Voice = new AudioStreamPlayer { Stream = DemoTone.Create(seconds: 4), MaxPolyphony = 1, PitchScale = 1, PlaybackType = AudioServer.PlaybackType.Stream };
        AddChild(Voice); RefreshOutputLatency();
        var layout = new VBoxContainer { Position = new Vector2(24, 24) }; AddChild(layout);
        Status = new Label(); layout.AddChild(Status);
        AddButton(layout, ru ? "С начала" : "Restart", Restart);
        AddButton(layout, ru ? "Пауза / продолжить" : "Pause / resume", () => SetPaused(!Voice.StreamPaused));
        AddButton(layout, "1.5 s", () => SeekTo(1.5));
        AddButton(layout, ru ? "Остановить" : "Stop", StopClock);
        Voice.Finished += () => { FinishedCount++; _last = Voice.Stream.GetLength(); };
    }

    private static void AddButton(VBoxContainer parent, string text, Action action)
    {
        var button = new Button { Text = text }; parent.AddChild(button); button.Pressed += action;
    }

    public void RefreshOutputLatency() => _outputLatency = Math.Max(0, AudioServer.GetOutputLatency());

    public double EstimatedSeconds()
    {
        if (!Voice.IsPlaying() || Voice.StreamPaused) return _last;
        double raw = Voice.GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - _outputLatency;
        _last = Math.Clamp(Math.Max(_last, raw), 0, Voice.Stream.GetLength());
        return _last;
    }

    public void Restart() { Voice.Stop(); _last = 0; Voice.Play(); }
    public void StopClock() { Voice.Stop(); _last = 0; }

    public void SetPaused(bool paused)
    {
        if (!Voice.IsPlaying() && !Voice.StreamPaused) return;
        _last = EstimatedSeconds(); Voice.StreamPaused = paused;
    }

    public bool SeekTo(double seconds)
    {
        if ((!Voice.IsPlaying() && !Voice.StreamPaused) || !double.IsFinite(seconds) || seconds < 0 || seconds >= Voice.Stream.GetLength()) return false;
        bool paused = Voice.StreamPaused;
        Voice.Stop(); Voice.Play((float)seconds); Voice.StreamPaused = paused; _last = seconds;
        return true;
    }

    public override void _Process(double delta)
    {
        double seconds = EstimatedSeconds();
        int beat = (int)Math.Floor(seconds * 120 / 60);
        Status.Text = $"{seconds:0.000} s   |   120 BPM: {beat}";
    }
}

The example assumes one nonlooping stream, PitchScale = 1 and global audio speed 1. It retains the last value while paused, clamps to duration and prevents small cross-thread timing fluctuations from moving the display backward. An explicit seek resets this monotonic guard to the target so backward seeking works. Do not apply a permanent max(previous, now) without that reset.

AudioStreamPlayer.Seek() does not seek while paused. The example's SeekTo retains the pause, stops the single voice, starts it at the requested position and restores the pause. It checks finiteness and 0 <= seconds < length before changing state. Stop resets the clock; natural completion retains full duration. Looping needs a separate loop counter and a deliberate choice between within-loop position and total elapsed time.

Account for game conditions

ConditionAction
Changed PitchScale or global audio speedAdjust interpolation for that speed; the ready example assumes 1
Changed output device/driverRefresh the estimate through RefreshOutputLatency after the change; do not query expensive latency every frame
Several simultaneous voicesGetPlaybackPosition is not a clock for all voices; dedicate a player to music
Player paused externallyRoute all pause/start/seek changes through one clock controller
Interactive or composite music streamCheck that implementation's position semantics; do not transplant the formula blindly
Sound must start on an exact audio boundary_Process() and frame signals are not the audio thread; a separate scheduler is needed

Do not judge rhythm hits solely by the integer beat index in _Process: a frame can skip a boundary. Store event times on the track timeline, detect crossings between previous/current positions and handle explicit seeks separately. Judge input within a configured timing window, not by matching visual frames.

Calling Play does not mean sound reaches the device immediately. GetTimeToNextMix() estimates the wait to the next mix; GetOutputLatency() estimates subsequent output. Do not add both again to the position formula above. A system-timer estimate after starting can help with a short segment, but it is a separate clock that can drift; pauses and seeks require resynchronizing it too.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.