Audio fingerprinting in C#: how SoundFingerprinting ingests, stores and matches
Open source audio fingerprinting in .NET. An efficient algorithm for acoustic fingerprinting written purely in C#.
At a glance
- What is it?
- SoundFingerprinting is a C# framework for acoustic and video fingerprinting. It gives you two builder chains, an in-memory store by default, Emy for persistence, and a query-time spectral bridge for the parts of a recording that hashes cannot cover.
- Who is it for?
- SoundFingerprinting fits teams already writing C# who need to recognise audio or video they have fingerprinted themselves, and who can accept an MIT-licensed core with a separate datastore for persistence. It is the wrong choice for a project that needs music identification against someone else's catalogue, or for a commercial deployment that leans on Emy without reading its licence terms.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two builder chains carry the whole ingest and query cycle
Audio fingerprinting in this framework has two halves that mirror each other: a command builder that turns media into hashes, and a command builder that turns a sample into a match. Both are fluent chains, and both take their collaborators as arguments instead of reaching for globals.
On the ingest side, `FingerprintCommandBuilder.Instance.BuildFingerprintCommand()` opens the chain. `.From(file)` names the media, `.UsingServices(audioService)` supplies the decoder, and `.Hash()` produces the hash collection that gets paired with a `TrackInfo` and written through `modelService.Insert(track, avHashes)`. The audio decoder arrives as `IAudioService`, the store arrives as `IModelService`, and the default implementations are `SoundFingerprintingAudioService` and `InMemoryModelService`. Swapping the decoder for an FFmpeg-backed one is therefore a constructor decision, not a rewrite.
The query side mirrors that shape. `QueryCommandBuilder.Instance.BuildQueryCommand()` takes `.From(file, secondsToAnalyze, startAtSecond)`, so the caller decides how much of the sample to analyze and from which second. `.Query()` returns a result object whose `BestMatch` property carries the matched `Audio.Track`. The asymmetry that matters in practice: fingerprints come from a whole file, but a query only needs a slice of a sample, ten seconds is the value used in the documented example, and the match still resolves to the track that was inserted earlier. A query sample may equally come from a URL, a microphone or a radio tuner, and the framework leaves that sourcing decision to the surrounding application.
First run: fingerprint a file, then query ten seconds of it
There is no install command in the documentation, so there is nothing to copy here. The framework ships as the `SoundFingerprinting` NuGet package, and the documentation points at the project wiki for setup details. What the code examples do show is the first end to end use: keep one `IModelService` instance and one `IAudioService` instance for the lifetime of the process, then fingerprint a file and store the result against a `TrackInfo` that carries the identifier, title and artist.
The following method is the documented ingest routine. It builds fingerprints from a file path and hands them to the model service so they can be retrieved later.
public async Task StoreForLaterRetrieval(string file)
{
var track = new TrackInfo("GBBKS1200164", "Skyfall", "Adele");
// create fingerprints
var avHashes = await FingerprintCommandBuilder.Instance
.BuildFingerprintCommand()
.From(file)
.UsingServices(audioService)
.Hash();
// store hashes in the database for later retrieval
modelService.Insert(track, avHashes);
}The query routine then takes a sample, a length to analyze and a starting second, and returns the best matching track.
public async Task<TrackData?> GetBestMatchForSong(string file)
{
int secondsToAnalyze = 10; // number of seconds to analyze from query file
int startAtSecond = 0; // start at the begining
// query the underlying database for similar audio sub-fingerprints
var queryResult = await QueryCommandBuilder.Instance.BuildQueryCommand()
.From(file, secondsToAnalyze, startAtSecond)
.UsingServices(modelService, audioService)
.Query();
return queryResult.BestMatch?.Audio.Track;
}One more case belongs in the same tutorial. If the source was played with vinyl-style pitch control, where tempo and pitch move together, the known percentage can be passed with the query samples so the change is compensated before fingerprinting: `.From(audioSamples, sourcePlaybackSpeedPercentage: 8)`. Without that compensation the hashes of a pitch-shifted sample and of the original do not line up.
The bundled store keeps everything in RAM, and Emy is the way out
The storage story is the first decision a new user has to make, because the default is not persistent. The package bundles a plain in-memory implementation, the `InMemoryModelService` class, and the documentation describes it as the default storage. Fingerprints inserted into it live in the process. When the process exits, the catalogue is gone, and any ingest performed before a restart has to be repeated.
For a catalogue that has to survive a restart, the documentation names Emy as the preferred choice: it is an external persistent storage for fingerprints, and a community version of it is free for non-commercial use. Emy is a separate component with its own package, `SoundFingerprinting.Emy`, and it carries its own compatibility matrix against FFmpeg, so adopting it adds a second version relationship to track.
That split has a licence consequence worth stating plainly. The SoundFingerprinting core is MIT licensed, with the licence text in `license.txt` at the root of the repository. Emy's community edition is free for non-commercial use, which means a commercial deployment that persists fingerprints in Emy is outside the free tier and has to be checked against whatever terms Emy offers. The documentation does not spell out those terms, so they have to be read directly rather than inferred.
The alternative to all of this is not another library but a different architecture: keep the fingerprinting in the same process as the media, and let the surrounding application own persistence, exporting whatever `Insert` and `Query` need. That keeps the MIT core as the only dependency, at the price of writing the storage yourself.
Spectral profile bridging covers the regions hashes cannot
Broadband intros, fade-outs and stretches of atmospheric silence are the parts of a recording that hash poorly, because there is little spectral structure to key on. Since v15.0.0 the framework can capture an optional per-second spectral profile, described as SFM plus relative power, at ingest time and consume it at query time to bridge regions where the hashes do not match.
The mechanism is invisible by default. It is opted into per query by setting `QueryConfiguration.SfmMatchStrategy`, and the setter cascades through to `FingerprintConfiguration.ComputeSpectralProfile` so the profile is captured during query-time fingerprinting automatically rather than through a separate call.
var queryResult = await QueryCommandBuilder.Instance.BuildQueryCommand()
.From(file)
.WithQueryConfig(config =>
{
// covers both broadband and silent regions in one pass
config.Audio.SfmMatchStrategy = CompositeBridgingStrategy.BroadbandOrSilent;
return config;
})
.UsingServices(modelService, audioService)
.Query();The available strategies are specific rather than generic. `NoBridgingStrategy` is the default and means no change against the behaviour of v14. `BroadbandNoiseBridgingStrategy` applies when the per-second SFM is above 0.70 on both sides of the gap. `SilentRegionBridgingStrategy` applies when per-second power is below 5 percent on both sides, and it replaces the removed `TreatSilenceAsSignal` flag. `SimilarProfileBridgingStrategy` uses a tight tolerance of under 0.10 on the SFM difference, capped at `min(10s, 0.30 × queryLength)` so that speech is not merged with speech. `CompositeBridgingStrategy` is the safe union of the others.
Two guards matter for anyone reasoning about recall. Bridging is bounded by a universal cap of 70 percent cumulative bridged seconds, and the amount actually bridged is reported back through `Coverage.BridgedSeconds`. Synthetic bridges are structurally indistinguishable from real matches downstream, with no wire format change to `MatchedWith`. And a track with no stored profile short-circuits cleanly to no bridging, so behaviour for tracks ingested before v15.0.0 is identical to v14.
Seam jump policy decides whether a realtime chunk continues the same playing
Realtime recognition has a problem that offline queries do not: a stream arrives in chunks, and the same track can appear more than once inside it. Since v15.17.0 a realtime query stitches consecutive chunks that match the same track into a single result entry, and `RealtimeQueryConfiguration.SeamJumpPolicy` decides what a jump in track position means.
There are three answers. `Stitch` is the default and treats the chunk as a continuation of the same playing of the track. `SourceSkip` treats the jump as a seek in the source, which is the right reading when the listener moved. `NewOccurrenceAbove(seconds)` treats the jump as another occurrence of audio that the track repeats, and the documentation points it at long programs that embed their own advertising spots, where the same audio legitimately appears several times in one stored track.
Choosing wrongly in either direction produces a bad result: `Stitch` on a seek merges two unrelated moments into one entry, while `SourceSkip` on a repeated commercial splits a single program into several. The reasoning and the measurements behind the three policies live on a wiki page rather than in the repository itself, so the policy table is where an evaluation has to start.
Video fingerprints share the same datastore, and FFmpeg drives the version matrix
Since v8.0.0 the framework handles video as well as audio. Video fingerprints are generated from video frames in the same way audio fingerprints are generated from samples, and they are inserted into and queried from the same datastore for exact and similar matches. The documentation states that a single project can fingerprint audio, video, or both at the same time, which means one catalogue can hold both kinds of entry rather than forcing a choice between them.
Format handling is delegated rather than enumerated. The supported media formats are described on a separate wiki page that covers both files and realtime streams, so the list of containers and codecs is not something the repository README enumerates. What the README does carry is a version matrix, and it is aimed at anyone using `FFmpegAudioService`: the matrix lines up SoundFingerprinting, SoundFingerprinting.Emy and FFmpeg, and the note applies to the FFmpeg service specifically. That matrix is the upgrade constraint for this project. A minor version bump in the framework can require a matching FFmpeg build, and the presence of `SoundFingerprinting.Emy` in the same row means a datastore upgrade can be part of the same decision.
The repository layout supports that reading. `src/` holds the code, `build/` sits beside it, and the root carries `build.sh`, `build.cmd` and `build-naudio.cmd` alongside a `bump.sh` script, which is consistent with a project that publishes versioned packages on its own release train rather than through a single monolithic build.
Where this library is the wrong tool
The design has limits that follow directly from its shape, and they are worth naming before anyone writes a service around it.
First, recognition here means matching against a catalogue that you built by ingesting media yourself. The framework identifies what you inserted. It is not a music identification service that queries a shared reference database, so the coverage of the catalogue is entirely a function of what the application has processed. A second limitation follows from the default store: with `InMemoryModelService` the catalogue is process-local and volatile, and a service that needs fingerprints to survive a restart has to bring Emy or its own persistence layer, and the Emy community edition is restricted to non-commercial use.
Third, the hard cases are opt-in rather than automatic. Broadband noise and silence are only bridged when `SfmMatchStrategy` is set, the default leaves those regions unmatched, and the per-query strategies are individually tuned with fixed thresholds such as SFM above 0.70 and power below 5 percent. Pitch-shifted playback needs the caller to supply the known percentage. Fourth, the realtime path has to be told what a position jump means, and the wrong choice there corrupts results in a way that is invisible in the output.
A concrete alternative for a team that does not want to own a catalogue or a datastore at all is to use a hosted identification service and keep this library out of the picture. For a team that has already decided to fingerprint its own media in C#, the realistic alternative is the same framework with a persistent store and the bridging strategies enabled, which is a configuration change rather than a rewrite.
MIT core, develop branch, last push on 2026-10-15
The licence is MIT, with the text in `license.txt` in the repository root, which covers the fingerprinting core and its code samples. The parts that are not MIT are the ones outside the repository: Emy, whose community version is free for non-commercial use, and any FFmpeg build your deployment depends on, each governed by its own terms rather than by this project.
The project is not archived, and the last push was on 2026-10-15, which is the same day as the v15.17.0 release. The releases immediately before it were v15.16.0 on 2026-10-12 and v15.15.0 on 2026-10-09. The default branch is `develop`, so the shipped package and the tip of the branch are not the same thing, and a team tracking the repository has to decide which line it upgrades from. The project homepage is https://emysound.com, and the project also lists a chat room.
The upgrade cost is therefore a version-triangle check rather than a code change: the framework version, the `SoundFingerprinting.Emy` version and the FFmpeg version have to line up in the matrix, and the v15 line has been adding behaviour behind configuration (the bridging strategies from v15.0.0, the seam jump policy from v15.17.0) while the storage contract stayed as it was.
Editorial conclusion
SoundFingerprinting fits teams already writing C# who need to recognise audio or video they have fingerprinted themselves, and who can accept an MIT-licensed core with a separate datastore for persistence. It is the wrong choice for a project that needs music identification against someone else's catalogue, or for a commercial deployment that leans on Emy without reading its licence terms. Before adopting it, check the FFmpeg version matrix in the README against the FFmpeg build you ship, confirm that Emy is licensed for your use, and measure recall on your own material with the SFM bridging strategies switched on, since the default leaves the hard regions unmatched.
Frequently asked questions
How do I add SoundFingerprinting to a .NET project?
The framework ships as the SoundFingerprinting NuGet package, and the storage bundled with it is the in-memory InMemoryModelService class, so a first run needs no external datastore. The default audio library is SoundFingerprintingAudioService, and the project documentation points at the wiki for setup details.
Can SoundFingerprinting fingerprint video as well as audio?
Yes. Since v8.0.0 video fingerprinting is supported, with fingerprints generated from video frames and stored in the same datastore for later exact and similar matches. A single project can fingerprint audio, video or both at the same time.
Does SoundFingerprinting match a track that was played with pitch control?
You pass the known playback speed percentage with the query samples so the change is compensated before fingerprinting, for example `.From(audioSamples, sourcePlaybackSpeedPercentage: 8)`. Without that compensation the hashes of a pitch-shifted sample and of the original do not line up.
What should I use instead of the in-memory store for a large fingerprint catalogue?
Emy is the preferred choice for external persistent storage, and its community version is free for non-commercial use. Commercial use falls outside that free tier, so the Emy terms have to be read before it is put into a shipped product.
How does SoundFingerprinting handle intros and fade-outs that hashes miss?
Since v15.0.0 an optional per-second spectral profile can bridge regions where hashes do not match. You opt in per query by setting `QueryConfiguration.SfmMatchStrategy` to a built-in strategy, and tracks with no stored profile fall back to no bridging.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/addictedcs-soundfingerprinting)Was this useful?