Запись звука: микрофон, PCM и WAV
Собрать ограниченную запись, обнаружить потерю кадров и сохранить WAV.
Запись требует трёх отдельных частей: источник звука, шина с захватом PCM и сохранение результата. Само включение аудиовхода не создаёт WAV-файл. Ниже — ограниченная запись со своей шины, без прямого прослушивания микрофона.
Сначала проверьте тракт без микрофона
Используйте DemoTone.cs как источник. Это позволяет отдельно проверить маршрутизацию, захват и сохранение. Только затем подключайте микрофон: иначе ошибка разрешения устройства выглядит как ошибка WAV.
Класс создаёт собственную приглушённую шину с AudioEffectCapture. Он собирает стереокадры в основном потоке, ограничивает длину и отказывается выдавать результат при потерянных кадрах. Dispose() удаляет только принадлежащую ему шину.
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;
}
}Подключение к своему интерфейсу
- Создайте
PcmBusRecordingодин раз иAudioStreamPlayerкак дочерний объект своего уровня. Назначьте игрокуBus = recording.BusNameи потоковое воспроизведениеPlaybackType = AudioServer.PlaybackType.Stream. - Для проверки назначьте плееру поток из
DemoTone.Create(). По команде пользователя вызовитеrecording.Begin(), затемplayer.Play(). - Пока запись активна, вызывайте
Poll()каждый кадр. Захват не является бесконечной очередью: если основной поток долго не читает данные, кадры теряются. Пример превращает такую потерю в ошибку вместо тихо повреждённого файла. - Для остановки сначала вызовите
player.Stop(), затемrecording.Finish(). Пустой результат —null; это не готовая запись. - Для непустого
AudioStreamWavвызовитеSaveToWav("user://recording.wav")и проверьтеError.Ok. Освободите полученный ресурс после использования. - При закрытии экрана остановите плеер до
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-сохранение/загрузка, лимит, позднее получение клипа, потеря кадров и очистка. Физический микрофон, разрешения ОС, качество прослушивания, аппаратная задержка и экспорт не проверялись.