Собственный аудиопоток: 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.
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.