reckfellENGINE

Запись звука: микрофон, PCM и WAV

Собрать ограниченную запись, обнаружить потерю кадров и сохранить WAV.

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

Запись требует трёх отдельных частей: источник звука, шина с захватом PCM и сохранение результата. Само включение аудиовхода не создаёт WAV-файл. Ниже — ограниченная запись со своей шины, без прямого прослушивания микрофона.

Сначала проверьте тракт без микрофона

Используйте DemoTone.cs как источник. Это позволяет отдельно проверить маршрутизацию, захват и сохранение. Только затем подключайте микрофон: иначе ошибка разрешения устройства выглядит как ошибка WAV.

Класс создаёт собственную приглушённую шину с AudioEffectCapture. Он собирает стереокадры в основном потоке, ограничивает длину и отказывается выдавать результат при потерянных кадрах. Dispose() удаляет только принадлежащую ему шину.

C#
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using Reckfell;

public sealed class PcmBusRecording : IDisposable
{
    private readonly AudioEffectCapture _capture = new() { BufferLength = 1 };
    private readonly List<Vector2> _frames = new();
    private readonly int _rate, _limit;
    private long _discardedAtStart;
    private bool _disposed, _failed;
    public string BusName { get; } = "Recording_" + Guid.NewGuid().ToString("N");
    public bool IsRecording { get; private set; }
    public int FrameCount => _frames.Count;

    public PcmBusRecording(float maxSeconds = 10)
    {
        if (!float.IsFinite(maxSeconds) || maxSeconds <= 0 || maxSeconds > 30)
            throw new ArgumentOutOfRangeException(nameof(maxSeconds));
        _rate = (int)AudioServer.GetMixRate(); _limit = (int)(_rate * maxSeconds);
        AudioServer.AddBus(); int bus = AudioServer.BusCount - 1;
        AudioServer.SetBusName(bus, BusName);
        AudioServer.AddBusEffect(bus, _capture);
        AudioServer.SetBusMute(bus, true); // Capture before mute; no live monitoring.
    }
    public void Begin()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        _frames.Clear(); _capture.ClearBuffer();
        _discardedAtStart = _capture.GetDiscardedFrames();
        _failed = false; IsRecording = true;
    }
    public void Poll()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
        if (!IsRecording) return;
        if (_capture.GetDiscardedFrames() != _discardedAtStart)
        {
            _failed = true; IsRecording = false;
            throw new InvalidOperationException("Audio frames were lost; discard this recording.");
        }
        int count = _capture.GetFramesAvailable();
        if (count == 0) return;
        var frames = _capture.GetBuffer(count);
        if (_capture.GetDiscardedFrames() != _discardedAtStart)
        {
            _failed = true; IsRecording = false;
            throw new InvalidOperationException("Audio frames were lost; discard this recording.");
        }
        int keep = Math.Min(frames.Length, _limit - _frames.Count);
        for (int i = 0; i < keep; i++) _frames.Add(frames[i]);
        if (_frames.Count == _limit) IsRecording = false;
    }
    public AudioStreamWav? Finish()
    {
        Poll(); IsRecording = false;
        if (_failed)
            throw new InvalidOperationException("Audio frames were lost; discard this recording.");
        if (_frames.Count == 0) return null;
        byte[] pcm = new byte[_frames.Count * 4];
        for (int i = 0; i < _frames.Count; i++)
        {
            Vector2 frame = _frames[i].IsFinite() ? _frames[i] : Vector2.Zero;
            BinaryPrimitives.WriteInt16LittleEndian(pcm.AsSpan(i * 4, 2), (short)(Math.Clamp(frame.X, -1, 1) * short.MaxValue));
            BinaryPrimitives.WriteInt16LittleEndian(pcm.AsSpan(i * 4 + 2, 2), (short)(Math.Clamp(frame.Y, -1, 1) * short.MaxValue));
        }
        return new AudioStreamWav { Format = AudioStreamWav.FormatEnum.Format16Bits, Stereo = true, MixRate = _rate, Data = pcm };
    }
    public void Dispose()
    {
        if (_disposed) return;
        IsRecording = false;
        int bus = AudioServer.GetBusIndex(BusName);
        if (bus > 0) AudioServer.RemoveBus(bus);
        _capture.Dispose(); _frames.Clear(); _disposed = true;
    }
}

Подключение к своему интерфейсу

  1. Создайте PcmBusRecording один раз и AudioStreamPlayer как дочерний объект своего уровня. Назначьте игроку Bus = recording.BusName и потоковое воспроизведение PlaybackType = AudioServer.PlaybackType.Stream.
  2. Для проверки назначьте плееру поток из DemoTone.Create(). По команде пользователя вызовите recording.Begin(), затем player.Play().
  3. Пока запись активна, вызывайте Poll() каждый кадр. Захват не является бесконечной очередью: если основной поток долго не читает данные, кадры теряются. Пример превращает такую потерю в ошибку вместо тихо повреждённого файла.
  4. Для остановки сначала вызовите player.Stop(), затем recording.Finish(). Пустой результат — null; это не готовая запись.
  5. Для непустого AudioStreamWav вызовите SaveToWav("user://recording.wav") и проверьте Error.Ok. Освободите полученный ресурс после использования.
  6. При закрытии экрана остановите плеер до Dispose() записывающего объекта. Удаление шины не должно оставлять работающий микрофон, который может перенаправиться в основной выход.

Лимит по умолчанию — 10 секунд, разрешено до 30. При достижении лимита рост прекращается; уже собранный клип сохраняется, даже если его забрать позже. Begin() начинает новую запись и очищает предыдущую. Частота берётся у микшера, формат результата — стерео PCM16; один кадр содержит два канала и занимает 4 байта.

Лимит останавливает только сбор кадров, не источник. Как только IsRecording стало false, остановите AudioStreamPlayer своим кодом. Этот класс не владеет плеером и не выключает физический аудиовход автоматически.

Подключение микрофона

Включите audio/driver/enable_input в настройках проекта, проверьте выбранное устройство AudioServer.InputDevice и разрешение ОС/экспорта. Назначьте плееру new AudioStreamMicrophone(), сохранив ту же шину. Запускайте чтение по явной команде пользователя; показывайте состояние записи и кнопку остановки.

Приглушение шины не отключает захват до него — это проверено синтетическим тоном. Оно предотвращает прямое прослушивание на выходе; не снимайте mute без продуманной схемы мониторинга и наушников. В одну шину попадут все направленные туда источники, а не только последний плеер.

AudioEffectRecord или AudioEffectCapture

AudioEffectRecord подходит для простого захвата готового сигнала шины; результат имеет тип AudioStreamWav, не AudioStreamSample. Эффект должен быть добавлен на шину до включения записи. Повторный старт начинает новую запись. Не делайте безусловный GetRecording() при отсутствии данных и не считайте флаг активности доказательством наличия PCM.

Пример выше использует AudioEffectCapture, чтобы явно контролировать чтение, лимит памяти/длительности и потерю кадров. ClearBuffer() очищает очередь, но не обнуляет накопленный счётчик GetDiscardedFrames(); поэтому сохраняется исходное значение счётчика для каждой новой записи.

Диагностика

Нет данных — проверьте Play(), поток, шину, опрос и разрешение входа. Есть файл, но тишина — проверьте реальный источник и его уровни; сам WAV не доказывает работу микрофона. Запись отказала из-за потерь — сократите блокировки основного потока, читайте чаще или спроектируйте отдельный потоковый тракт. Файл не сохранился — проверьте код ошибки и доступность user://, не пишите результат в ресурсы экспортированной игры.

Проверены синтетический тон через Dummy-драйвер, запись с приглушённой шины, PCM16, WAV-сохранение/загрузка, лимит, позднее получение клипа, потеря кадров и очистка. Физический микрофон, разрешения ОС, качество прослушивания, аппаратная задержка и экспорт не проверялись.

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

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

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

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