reckfellENGINE

Часы аудио: позиция, пауза и перемотка

Полный индикатор C# с учётом микширования, оценкой задержки и сбросом часов после перемотки.

На этой странице

Выберите источник времени

Для декоративной анимации интерфейса достаточно delta. Если движение должно следовать позиции музыки, суммирование delta создаёт отдельные часы: изменение TimeScale, пауза или перемотка расходятся с аудио. GetPlaybackPosition() относится к последнему голосу плеера, обновляется аудиомикшером и между смешиваниями меняется ступенчато.

Для одного потока со скоростью 1 используйте оценку: GetPlaybackPosition() + AudioServer.GetTimeSinceLastMix() - AudioServer.GetOutputLatency(). Первый член — позиция, второй интерполирует время между смешиваниями, третий оценивает задержку до вывода. Это не измерение момента, когда звук дошёл до уха: устройство, Bluetooth, ОС и экран добавляют свои задержки. Для ритм-игры предусмотрите пользовательскую калибровку.

Запустить готовые часы

Добавьте DemoTone.cs и MusicClockDemo.cs ниже. Прикрепите MusicClockDemo к корню Control, соберите и запустите F6. С начала запускает четырёхсекундный тестовый тон; число секунд идёт вперёд, пауза его замораживает, 1.5 s перемещает позицию. Число после 120 BPM — индекс условной доли, начинающийся с нуля; тон не является музыкальным треком с разметкой долей.

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}";
    }
}

Пример рассчитан на один незацикленный поток, PitchScale = 1 и глобальную скорость аудио 1. Он сохраняет последнее значение на паузе, ограничивает оценку длительностью и не даёт мелким колебаниям между потоками увести индикатор назад. При явной перемотке этот монотонный ограничитель сбрасывается к целевой позиции, поэтому перемотка назад работает. Не используйте вечный max(previous, now) без такого сброса.

AudioStreamPlayer.Seek() не выполняет перемотку на паузе. Метод SeekTo примера сохраняет паузу, останавливает единственный голос, запускает его с нужной позиции и возвращает паузу. Конечность числа и диапазон 0 <= seconds < length проверяются до изменения состояния. Stop сбрасывает часы; естественный конец оставляет полную длительность. При Loop потребуются отдельный счётчик кругов и явный выбор: показывать позицию внутри круга или общее время.

Что учесть в игре

УсловиеРешение
Изменён PitchScale или скорость аудиосервераПересчитайте интерполяцию под скорость; готовый пример рассчитан только на 1
Меняется устройство/драйвер выводаПосле смены обновите оценку через RefreshOutputLatency; не опрашивайте дорогую задержку каждый кадр
Несколько одновременно играющих голосовНе считайте GetPlaybackPosition часами всех голосов; выделите отдельный музыкальный плеер
Плеер поставлен на паузу извнеВсе изменения паузы/запуска/перемотки проводите через один контроллер часов
Interactive или составной музыкальный потокУточните семантику позиции его реализации; не переносите формулу слепо
Нужен звук на точной аудиогранице_Process() и сигналы кадра не являются аудиопотоком; такой планировщик нужен отдельно

Не вычисляйте попадание в ритм только по целому индексу доли из _Process: кадр может перескочить границу. Храните времена событий на шкале трека, проверяйте пересечение интервала предыдущей/новой позиции и отдельно обрабатывайте явные перемотки. Подтверждение пользовательского попадания должно учитывать настроенное временное окно, а не совпадение визуальных кадров.

Вызов Play тоже не означает немедленное появление звука на устройстве. GetTimeToNextMix() оценивает ожидание следующего смешивания; GetOutputLatency() — последующий вывод. Не прибавляйте обе оценки повторно к формуле позиции выше. Оценка от системного таймера после старта может быть полезна для короткого фрагмента, но это отдельные часы с возможным дрейфом; после паузы и перемотки их тоже нужно согласовывать.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.