reckfellENGINE

Собственный аудиопоток: PCM и синтез

Рабочий генератор C#, буфер, ресемплинг и нативный контракт.

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

AudioStream хранит описание звука, AudioStreamPlayback — состояние отдельного воспроизведения. Один ресурс может использоваться несколькими плеерами: курсор, фазу и состояние декодера для независимых голосов нельзя безусловно хранить в общем ресурсе.

Выберите уровень реализации

ЗадачаПодход
Проиграть WAV, Ogg или MP3Готовый AudioStream и AudioStreamPlayer; собственный декодер не нужен
Синтезировать звук или передавать готовые PCM-кадрыAudioStreamGenerator и его буфер воспроизведения
Добавить нативный декодер или особый поток данныхСобственный AudioStream/AudioStreamPlayback через подходящее нативное API
Обрабатывать уже смешанный звук шиныAudioEffect, а не ресурс AudioStream

Рабочий пример потокового PCM на C#

Создайте SineStream.cs в проекте C# с SDK Reckfell 1.0.0 и прикрепите к Entity запускаемого уровня. При обычном запуске пример генерирует тихий стереотон 220 Гц, пока Entity находится в дереве. Bus должен указывать на существующую шину; по умолчанию используется Master.

C#
using System;
using Reckfell;

public partial class SineStream : Entity
{
    private const int SourceRate = 48000;
    private readonly Vector2[] _block = new Vector2[256];
    private AudioStreamGenerator? _stream;
    private AudioStreamPlayer? _player;
    private AudioStreamGeneratorPlayback? _playback;
    private double _phase;

    [Export] public StringName Bus { get; set; } = "Master";
    public int UnderrunCount => _playback?.GetSkips() ?? 0;

    public override void _Ready()
    {
        _stream = new AudioStreamGenerator
        {
            MixRateMode = AudioStreamGenerator.AudioStreamGeneratorMixRate.Custom,
            MixRate = SourceRate,
            BufferLength = 0.15f,
        };
        _player = new AudioStreamPlayer
        {
            Stream = _stream,
            Bus = Bus,
            PlaybackType = AudioServer.PlaybackType.Stream,
        };
        AddChild(_player);
        _player.Play();
        _playback = _player.GetStreamPlayback() as AudioStreamGeneratorPlayback;
        FillQueue();
    }

    public override void _Process(double delta) => FillQueue();

    public static void FillBlock(Span<Vector2> frames, ref double phase)
    {
        for (int i = 0; i < frames.Length; i++)
        {
            float sample = (float)(0.1 * Math.Sin(Math.Tau * phase));
            frames[i] = new Vector2(sample, sample);
            phase += 220.0 / SourceRate;
            phase -= Math.Floor(phase);
        }
    }

    private void FillQueue()
    {
        if (_playback is null)
            return;
        int available = _playback.GetFramesAvailable();
        while (available > 0)
        {
            int count = Math.Min(available, _block.Length);
            Span<Vector2> frames = _block.AsSpan(0, count);
            FillBlock(frames, ref _phase);
            if (!_playback.PushBuffer(frames))
                return;
            available -= count;
        }
    }

    public override void _ExitTree()
    {
        _player?.Stop();
        _player?.Free();
        _player = null;
        _playback?.Dispose();
        _playback = null;
        _stream?.Dispose();
        _stream = null;
    }
}

PCM здесь — нормализованные float-значения: Vector2 содержит левый и правый канал одного кадра. Один кадр не равен одному каналу и не равен одному байту. Оба канала получают одинаковый сигнал с амплитудой 0.1; фазовый шаг рассчитан для исходных 48000 кадров в секунду.

MixRateMode явно установлен в Custom: именно тогда MixRate задаёт частоту исходного потока. Режимы Output/Input используют соответствующую частоту AudioServer. Это не включение микрофона. Частота выхода устройства может отличаться от исходной; движок выполняет ресемплинг. Не считайте 44100 Гц неизменной частотой всех систем.

Буфер, фаза и пропуски

GetFramesAvailable возвращает место для записи, а не количество накопленного звука. Заполняйте только доступную часть. PushBuffer возвращает false, если целый блок не помещается; частичного принятия этого блока нет. Пример использует одного производителя и повторно используемый массив; не пишите в тот же playback из нескольких независимых производителей.

Фаза сохраняется между блоками, поэтому на их границах не начинается новая синусоида. _Process пополняет очередь, но delta не определяет высоту тона: её определяют исходная частота кадров и шаг фазы.

BufferLength задаёт желаемую длину буфера при создании playback. Реальная ёмкость округляется внутренним кольцевым буфером; это не точная задержка «от ввода до колонки». Больше очередь — больше запас на задержки пополнения и больше возможная задержка управления. Не меняйте частоту или длину буфера у уже работающего примера; остановите его и создайте согласованный поток заново.

При нехватке PCM микшер дополняет недостающую часть нулями и увеличивает GetSkips. UnderrunCount в примере показывает этот счётчик: это число случаев нехватки, не число потерянных кадров. Он сбрасывается при старте playback. Проверяйте его рост на целевых устройствах и под нагрузкой. ClearBuffer допускается только у неактивного playback; во время работы это не способ безопасного перематывания. Seek у генератора не реализует переход по времени.

Диагностика

СимптомПроверка
ТишинаEntity находится в дереве, плеер запущен, Bus существует, шина не заглушена, устройство доступно
Треск или разрывыРост UnderrunCount, задержки главного потока, слишком маленький запас PCM
Неверный тон/скоростьИсходная частота, MixRateMode и формула фазового шага согласованы
Звук клиппируетУровень PCM и итоговая сумма голосов/эффектов на шине
Управление звуком запаздываетДлина очереди плюс задержки микшера, драйвера и устройства

Пример демонстрирует непрерывное заполнение очереди. Он не реализует плавный запуск/остановку, управление музыкальным темпом, микрофон, сетевой jitter buffer или гарантии работы при произвольной нагрузке. Для плавной остановки добавьте огибающую и дождитесь её воспроизведения вместо резкого удаления Entity.

Контракт собственного нативного декодера

В текущих заголовках C++ ресурс переопределяет instantiate_playback(), возвращая Ref<AudioStreamPlayback>. Каждый новый playback хранит собственные курсор и состояние декодирования. Для описания длительности используется double get_length() const.

Прямое смешивание имеет сигнатуру int mix(AudioFrame *p_buffer, float p_rate_scale, int p_frames): заполните выходные стереокадры и верните число действительно смешанных кадров. Это не старый void-callback и не указатель на PCM16. Для AudioStreamPlaybackResampled внутренний callback — int _mix_internal(AudioFrame *p_buffer, int p_frames), а get_stream_sampling_rate() сообщает частоту исходного потока.

В аудиопотоке не выполняйте файловое/сетевое ожидание, тяжёлое декодирование без ограничения времени, вывод журнала или выделение памяти на каждый блок. Готовьте буферы и данные заранее; передавайте их с контролируемой синхронизацией. Загрузка файла и регистрация формата — отдельная задача, описанная в собственном формате ресурсов.

Эти нативные сигнатуры сверены с исходниками; приведённый исполняемый пример использует готовый AudioStreamGenerator и не является реализацией нового нативного декодера.

Справочник: AudioStreamGenerator, AudioStreamGeneratorPlayback, AudioStreamPlayback, AudioServer.

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

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

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

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