reckfellENGINE

Audio recording: microphone, PCM, and WAV

Collect a bounded recording, detect discarded frames, and save WAV.

On this page

Recording needs three separate parts: an audio source, a bus with PCM capture, and result storage. Enabling audio input does not itself create a WAV file. This example records from its own bus with a duration limit and no direct microphone monitoring.

Test the path without a microphone first

Use DemoTone.cs as the source. This isolates routing, capture, and saving. Add microphone input afterward so that a device-permission failure is not mistaken for a WAV error.

The class creates its own muted bus with AudioEffectCapture. It collects stereo frames on the main thread, limits duration, and refuses a result after discarded frames. Dispose() removes only its owned bus.

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

Connect to your UI

  1. Create PcmBusRecording once and an AudioStreamPlayer as a level child. Assign Bus = recording.BusName and PlaybackType = AudioServer.PlaybackType.Stream to the player.
  2. For testing, use a stream from DemoTone.Create(). On a user command, call recording.Begin(), then player.Play().
  3. Call Poll() every frame while recording. Capture is not an infinite queue: leaving data unread can lose frames. This example reports loss rather than silently producing damaged audio.
  4. Stop with player.Stop() first, then recording.Finish(). An empty result is null, not a usable recording.
  5. For a nonempty AudioStreamWav, call SaveToWav("user://recording.wav") and check Error.Ok. Dispose the returned resource when finished.
  6. Stop the player before disposing the recorder when leaving the screen. Removing its bus must not leave a running microphone that could be rerouted to the main output.

The default cap is 10 seconds; up to 30 is allowed. Reaching the cap stops growth and preserves the collected clip even if retrieved later. Begin() clears the previous take. The result uses mixer sample rate and stereo PCM16: a frame contains two channels and occupies 4 bytes.

The cap stops frame collection, not the source. Stop AudioStreamPlayer in your code as soon as IsRecording becomes false. This class does not own the player or automatically disable physical audio input.

Connect microphone input

Enable audio/driver/enable_input in project settings, check AudioServer.InputDevice, and check OS/export permissions. Assign new AudioStreamMicrophone() to the player while retaining the same bus. Begin reading on an explicit user command and show recording state plus a stop control.

Bus mute does not prevent the preceding capture, as verified with a synthetic tone. It prevents direct output monitoring; do not unmute without a deliberate monitoring/headphone setup. All sources routed to the bus are captured, not just the latest player.

AudioEffectRecord or AudioEffectCapture

AudioEffectRecord can capture a bus signal with a simple recording workflow; its result type is AudioStreamWav, not AudioStreamSample. Add the effect to a bus before enabling recording. Restarting begins a new take. Do not unconditionally call GetRecording() without data or treat an active flag as proof of PCM.

This example uses AudioEffectCapture to control reads, memory/duration limits, and discarded frames explicitly. ClearBuffer() clears queued data but does not reset accumulated GetDiscardedFrames(), so each take records its own counter baseline.

Diagnose failures

No data: check Play(), stream, bus, polling, and input permission. File contains silence: inspect the real source and levels; a WAV alone does not prove microphone functionality. Discarded-frame refusal: reduce main-thread stalls, read more frequently, or design a dedicated streaming path. Save failure: check the error code and user:// access; do not save into exported game resources.

Checks cover a Dummy-driver synthetic tone, muted-bus capture, PCM16, WAV save/reload, duration cap, delayed retrieval, overflow refusal, and cleanup. Physical microphone input, OS permissions, listening quality, hardware latency, and exports were not tested.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.