Recursive AI

Identify an enrolled speaker in C#

OneSpeech is a free, open-source speech recognition library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

Match audio to enrolled IDs, or return unknown when no score meets your threshold.

Terminal
dotnet add package RecursiveAI.OneSpeech

OneAudio is a dependency of RecursiveAI.OneSpeech; no separate install is needed.

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

sample.flac and other.wav are mono 16 kHz recordings in the zip. default passes CancellationToken.None: this example does not request cancellation.

C#
using OneSpeech;
using OneSpeech.Embedders;
using OneSpeech.Speakers;
AudioBuffer Read(string f) => new(
    OneAudio.Audio.Load(f).Samples, 16000);
using var model = await CampPlusEmbedder.LoadAsync();
using var registry = new InMemorySpeakerRegistry(model, 0.95);
registry.Enroll("reader", Read("sample.flac"), default);
foreach (var file in new[] { "sample.flac", "other.wav" })
{
    var match = registry.Identify(Read(file), default);
    Console.WriteLine(match.SpeakerId ?? "unknown");
}

Unzip the example, then dotnet run.

Prints:

Text
reader
unknown

The full program

sample.flac is the LibriSpeech test-clean clip 1089-134686-0001: “stuff it into you his belly counselled him”. It is included under CC BY 4.0, credited to Vassil Panayotov, Guoguo Chen, Daniel Povey and Sanjeev Khudanpur (source). other.wav is a four-second NASA Apollo 11 excerpt with a different voice. The NASA audio is adapted from Apollo 11 historical radio audio, a US government work; see NASA’s media policy. No endorsement is implied. The repeated LibriSpeech file demonstrates exact-input matching, not generalization to a new utterance.

C#
using OneSpeech;
using OneSpeech.Embedders;
using OneSpeech.Speakers;
AudioBuffer Read(string path)
{
    var clip = OneAudio.Audio.Load(path,
        new OneAudio.LoadOptions { SampleRate = 16000, Channels = 1 });
    return new AudioBuffer(clip.Samples, 16000);
}
using var embedder = await CampPlusEmbedder.LoadAsync();
using var registry = new InMemorySpeakerRegistry(embedder, threshold: 0.95);
registry.Enroll("reader", Read("sample.flac"), default);
foreach (var file in new[] { "sample.flac", "other.wav" })
{
    var match = registry.Identify(Read(file), default);
    Console.WriteLine($"{file}: {match.SpeakerId ?? "unknown"} ({match.Score:F4})");
}

dotnet run prints:

Text
sample.flac: reader (1.0000)
other.wav: unknown (-0.0686)

Invalid audio or IDs throw instead of adding an enrollment; failed extraction leaves any previous enrollment for that ID intact.

Speaker embeddings are biometric data. Obtain informed consent, limit retention and access, and check applicable law, including Illinois BIPA and GDPR Article 9, before enrollment, identification or verification.

Variations

Forget the enrolled ID. Add this line after the identification loop:

C#
Console.WriteLine(registry.Remove("reader", default));
Text
True

The short answer

To identify a speaker from audio in C#, enroll an ID in InMemorySpeakerRegistry, extract the query with the same embedder, and call Identify. Supply an explicit acceptance threshold. A result below that threshold has a null SpeakerId, which the example prints as “unknown”.

The sample enrolls one public recording and queries that identical file, then queries a different voice. The identical-file score only confirms the example’s mechanics. It does not establish recognition accuracy on unseen utterances, microphones, rooms or populations.

How to identify a speaker?

Use a speaker embedder to map consented audio into fixed-length vectors, then compare queries against enrolled vectors. CampPlusEmbedder is the model role here. Its input must be nonempty mono PCM at the required sample rate. The registry borrows the embedder; dispose the registry before disposing the embedder.

Enrollment IDs are case-sensitive. Enrolling an existing ID replaces it atomically only after extraction succeeds. Multiple enrollment utterances can be supplied through the registry overload: it unit-normalizes each vector, averages them with equal weight and normalizes the result. It does not keep a collection of original audio files.

Without an optional score normalizer, the registry uses cosine similarity. It returns the highest-scoring ID that meets the inclusive threshold, breaking exact ties by ordinal ID. Below threshold it returns a null ID with the best score. An empty registry returns a null ID and negative infinity. See Speaker identity and scores.

Can AI identify a person by voice?

A speaker model can compare a query against known enrollments, but a match is not proof of a person’s identity. The threshold in this example, 0.95, is deliberately explicit and is not a recommended universal operating point. Choose a threshold using disjoint development trials representative of your intended use. Raw cosine is neither a probability nor a calibrated authentication decision.

Do not interpret an unknown result as proof that the person was never enrolled. Channel differences, short recordings and other conditions can affect scores. Nor does the highest score necessarily justify acceptance: an unknown person will still have a best-scoring enrollment when the registry is nonempty. The threshold is what permits rejection.

Avoid logging embeddings and remove enrollments when no longer needed. Clearing managed arrays is best effort, not a memory-erasure guarantee.

How can I identify different speakers in an audio recording?

Identification assumes that the query represents the voice you want to compare. A meeting containing several voices needs segmentation or diarization before per-speaker identification. A diarizer labels anonymous turns; those labels are not enrolled identities. Comparing a whole mixed conversation as one query does not identify each participant.

The registry lives in memory only. This example does not create a database, serialize embeddings or restore enrollments after restart. Dispose it or remove IDs when finished. If you need an application-level enrollment store, its consent, access controls and lifecycle are separate responsibilities rather than features demonstrated by this recipe.

All onespeech recipes