All notable changes to RecursiveAI.OneAudio are recorded here. The format follows Keep a Changelog;
versions follow Semantic Versioning once 1.0.0 is released. Until then, every 0.x release may change the API.
[Unreleased]
[1.2.1] - 2026-10-27
A patch release for RecursiveAI.OneAudio.Devices; the core package changes only its version number. The API changes are additive.
What has been checked on real hardware is unchanged from 1.2.0 (see below); the fixes here are covered by tests over the manual backend and a stream that cannot name its device.
Fixed
- A stream whose backend cannot name its device (
IOpenDevice.Deviceisnull, as in a browser audio context) was taken for a change of the default device on every poll, so the stream was reopened about once a second and counted as a default switch.nullnow means "unknown": the package compares the stream's device with the default only when both are known. A backend that sees the default change itself still says so withNotifyDefaultChanged, and that is honoured. The meaning ofnullis documented onIOpenDevice.Device. DeviceCounters.RenderAllocatedBytescould read a few hundred bytes on a host that steps a manual device in short blocks. We could not reproduce it: the package's render path allocates nothing after warm-up (now tested with 480-frame blocks, with and without rate conversion), so the bytes most likely come from first-use work in the program's own source or the runtime after the eight callbacks the probe skips. The skipped count is now an option, below.
Added
IOpenDevice.StateText(a default interface member returningnull, so existing backends are unaffected): a short state word such asawaiting_gesture. It is exposed asAudioOutputDevice.StateTextandAudioInputDevice.StateTextand appended toDetailafter the route extras.DeviceStreamOptions.AllocationWarmupBlocks(default 8, the previous fixed value): how many callbacksRenderAllocatedBytesignores at the start of a stream.
[1.2.0] - 2026-10-27
Device playback and capture, in the new RecursiveAI.OneAudio.Devices package (same version as the core package).
What has been checked on real hardware so far: ALSA playback and capture on Linux (PipeWire); Core Audio playback on macOS, including following a change of the default output; and playback on an iPad (iPadOS 17, NativeAOT) through the speaker, with the device clock, peaks and zero underruns and allocations confirmed, and a simulated audio-session interruption suspending and resuming the stream. Not yet on real hardware: WASAPI (checked by unit tests and on Windows CI runners, which have no audio device), capture from a real microphone on macOS and iOS, and a real system interruption on iOS (a call or another app taking the audio session).
Added
IAudioSource(sample rate, channels,Render), implemented byAudioMixer: anything an output device can pull audio from.- A new package,
RecursiveAI.OneAudio.Devices, versioned in step with the core package: device playback and capture. This first part is the backend-independent layer:AudioOutputDeviceandAudioInputDevice(callbacks on the device thread, a limiter, an optional BS.1770 meter, default-device following, reopen after a lost device, silent real-time pacing while no device is open, events),DeviceCounters(frames rendered, written and played from the device's own position, played peak, underruns, losses, reopens),AudioDevices, theIAudioDeviceBackendseam, andManualBackendfor tests and offline runs. The one-call layer isAudio.Play(mixer),Audio.CreateOutputandAudio.CreateInput, as C# 14 extension members onAudio. AlsaBackend(Linux playback through ALSA, including PipeWire and PulseAudio through their ALSA plug-ins): enumeration, period and buffer negotiation, float or 16-bit, underrun recovery, device position fromsnd_pcm_delay, and capture (Audio.CreateInput, callback orRead).CoreAudioBackend(macOS and iOS): device enumeration with default and hot-plug notifications, playback and capture through audio units with rate and channel conversion, a device-clockFramesPlayed, follow-the-default and device-loss handling, microphone-permission refusal with a reason, and on iOS the audio-session category, interruptions and route changes.tools/ios-smokeruns it in the iOS simulator.WasapiBackend(Windows playback and capture through WASAPI): MMDevice enumeration, shared mode with automatic rate and channel conversion, exclusive mode with format negotiation and a stated refusal, device position fromIAudioClock, endpoint notifications for default and hot-plug changes, lost-device handling, and the "Pro Audio" scheduler class on the device thread. All COM calls are v-table function pointers, so it is trim and NativeAOT clean. Verified so far by unit tests over hand-built COM objects and by CI on Windows; not yet on real hardware.- Single-threaded hosts:
IManualDevice(public;Advance(frames), optionalWantedFrames) lets any backend whoseIsManualis true be stepped by the caller, with no thread started by the package.AudioOutputDevice.Advancenow also does the supervisor's work (reopen after a loss, default change, retry, events), soPump()is optional;AudioOutputDevice.WantedFramesreads the stream's request. DeviceStreamOptions.AllowRateConversion(default true): an output device running at another sample rate than the source gets the source converted with the polyphase resampler;AudioOutputDevice.DeviceFormatreports the device's format andManualBackend.DeviceSampleRateacts out such a device.AudioOutputDevice.IsSuspendedandIsSystemSuspended(also onAudioInputDevice): readable suspension state;Suspendis documented to stop the source's clock and, once the queue has played out, the device's.AudioOutputDevice.Muted: feeds silence without rendering the source (its clock stands still);DeviceCounters.MutedFramescounts it.DeviceCounters.DeviceUnderruns(the device API's own count, reported through the optionalIDeviceEngine.NotifyDeviceUnderruns) next toUnderruns.RenderAllocatedBytesis always kept (measured at the noise of a callback's time);DeviceStreamOptions.MeasureRenderAllocationsis now a no-op.BlockHookon both devices: a per-block callback with the frames handed over so far.ManualBackend.Sink: every output block streamed to a callback with no capacity limit.AudioOutputDevice.Loudnessreturns the core'sLoudnessSnapshot(with the maxima) instead of a type of its own;LoudnessReadingis gone.
[1.1.0] - 2026-10-26
Added
- A one-call layer on top of the existing API, nothing in it changes:
Audio(andAudioKit, the same calls over a registry of your choosing) withOpen,Load(decode to anAudioClip, optionally converting rate and channels),Transcodeto WAV, FLAC or Ogg Opus,Resample,Measure,Normalize(samples, clips or file to file, default -14 LUFS with a -1 dBTP true-peak ceiling) andCreateMixer. mixer.Play(path)andmixer.PlayStream(path, loop: true)extension methods (AudioMixerPlayExtensions), withPlayOptionsand aStreamingVoicehandle.- Options records
LoadOptions,TranscodeOptions,NormalizeOptions,PlayOptions; resultsTranscodeResultandNormalizeResult; enumAudioFileFormat. AudioClip.Samples: a read-only view of the decoded samples.tests/OneAudio.AotSize: three tiny NativeAOT apps that compare published size with the default registry, a WAV-onlyAudioKitand the low-level API.
[1.0.0] - 2026-10-25
The first stable release. From here on the public API follows Semantic Versioning.
- Decode WAV / RF64, Ogg (chained), Vorbis, Opus, FLAC (native and Ogg) and MP3 (MPEG-1/2/2.5 Layer III), streaming with exact seeking.
- Encode WAV / RF64, FLAC (levels 0 to 8) and Opus (including Ogg Opus files).
- Polyphase and linear resampling; the real-time mixer with buses, effects (filters, reverb, limiter) and per-bus loudness meters; EBU R 128 / BS.1770 loudness with true peak and normalisation.
- No changes to the API since 0.1.0-alpha.5. Symbol packages (
.snupkg) are published alongside the package.
[0.1.0-alpha.5] - 2026-10-25
Fixed
- Ogg Vorbis: a short stream that fits on a single page, such as a brief click followed by digital silence, decoded to silence or lost its first or last samples. The one granule position on that page marks where the stream ends, but the decoder also read it as the position of the stream's first sample and cut the difference from the start. It is now taken as the end trim only. Streams whose first audio page is not the last page are unchanged.
[0.1.0-alpha.4] - 2026-10-25
Added
AudioMixer.TryEnableBusMeter(bus): enables a bus meter without throwing when no meter is free (or the mixer has none);FreeBusMeterstells "no slot" from "queue full" after a false.AudioMixer.IsBusMetered(bus)reports whether a bus currently holds a meter.- Results in the README: conformance, fuzzing and benchmarks against other libraries and native tools, losses included.
[0.1.0-alpha.3] - 2026-10-24
Added
- Per-bus loudness metering in the mixer.
MixerOptions.MaxBusMeters(0 to 64, default 0) preallocates that many BS.1770 / EBU R 128 meters at construction;AudioMixer.EnableBusMeter,DisableBusMeterandResetBusMeterturn one on, off or back to zero for any bus or for the master output (AudioMixer.MasterBus) through the command queue;AudioMixer.TryGetBusLoudnessreads it from any thread without locking. The reading is aLoudnessSnapshot(momentary, short-term and integrated LUFS, true peak in dBTP, the two maxima, frames measured) published once per render pass, never torn. A bus is measured after its effects and gain; the master output is measured after the limiter. A mixer without enabled meters does no metering work, and the render path still allocates nothing with meters enabled and toggling. AudioMixer.FreeBusMetersandAudioMixer.FreeReverbSlots, the number of meter and reverb slots not yet in use.
Changed
- Behaviour change:
AudioMixer.SetBusReverbthrowsInvalidOperationException(namingMixerOptions.MaxReverbs) when the bus has no reverb and every reverb slot is taken, or the mixer has none. It used to returnfalse, the same value as for a full command queue, which was easy to miss.falsenow means only that the command queue was full (and is counted inDroppedCommands). Retuning a bus that already has a reverb never throws. The signature is unchanged.
[0.1.0-alpha.2] - 2026-10-24
Added
- Decoders: FLAC (native and Ogg FLAC, bit-exact against the reference decoder on the public test corpus, exact seeking) and MP3 (MPEG-1, 2 and 2.5 Layer III with ID3v2, Xing/LAME and VBRI handling, gapless trimming and exact seeking, within the ISO/IEC 11172-4 accuracy limits).
- Chained Ogg streams: consecutive links of the same format play as one stream, with exact seeking and length across links.
- Loudness:
LoudnessMeter(ITU-R BS.1770-5 and EBU R 128: momentary, short-term, integrated, loudness range, true peak),LoudnessAnalyzerandLoudnessNormalization; the EBU Tech 3341 and 3342 test signals are inside their tolerances. - Mixer effects: low-pass and high-pass filters on voices, buses and the master output, a reverb insert (
MixerOptions.MaxReverbs), and a lookahead limiter on the master output (MixerOptions.LimiterLookaheadMilliseconds). All state is preallocated; the render path allocates nothing. - Streaming sources report position and length; ducking can be removed again (
DuckingSettings.None,ClearDucking). - Encoders:
WavEncoder(RIFF and RF64, extensible),FlacEncoder(levels 0 to 8, seek table, MD5; output decodes bit-exact), and the Opus encoder (SILK, hybrid and CELT, byte-identical to the RFC 6716 reference encoder on the tested configurations) with the Ogg Opus file writer (RFC 7845).
[0.1.0-alpha.1] - 2026-10-24
Added
- Decoders for WAV (PCM 8/16/24/32-bit, float, extensible), the Ogg container, Vorbis and Opus (all RFC 8251 test vectors pass), behind an explicit codec registry so unused codecs trim away. Streaming reads into caller-provided spans, with seeking.
- Resampling: polyphase (two quality tiers) and linear.
- The real-time mixer: voices and buses, gain, pan, sample-accurate start and loop points, crossfades, ducking, voice caps with priority, per-voice rate conversion and pitch, multichannel layouts, and a decode-ahead pump for streaming sources. The render callback is lock-free and allocates nothing. Vectorised mixing kernels with scalar reference paths.
- NativeAOT and trim clean; fuzz targets for the parsers and decoders.