Audio fingerprinting library and CLI — identify songs from short audio clips.
waveiden builds a compact acoustic fingerprint database from full-length audio files, then matches query clips against it in milliseconds. The approach is the same one powering services like Shazam:
- Load audio and compute a short-time Fourier transform (spectrogram)
- Detect spectral peaks and pair them into hashed landmarks
- Store
(hash, time_offset, song_name)triples in a database - At query time, look up hashes and vote on the song with the most time-consistent hits
A 30-second clip is enough for a reliable match against a database of thousands of songs. The library works against noisy or pitch-shifted recordings because the fingerprints are based on relative peak relationships rather than raw waveform content.
include/waveiden/
types.hpp Common value types (Peak, Fingerprint, MatchResult, AudioBuffer)
dsp.hpp DSP namespace: FFT, Hann window, spectrogram, peak detection
audio_reader.hpp IAudioReader interface + WavReader (libsndfile)
database.hpp IDatabase interface + SQLiteDatabase implementation
fingerprint_engine.hpp High-level engine that wires the pipeline together
waveiden.hpp Umbrella include
grpc/
proto/waveiden.proto Service definition
src/waveiden_service.* gRPC service implementation (optional, see below)
examples/cli.cpp Command-line tool built on top of the library
The library is structured around two abstract interfaces:
IDatabase— definesconnect,disconnect,indexSong,match,listSongs,removeSong, andclear. Swap in any storage backend by subclassing it.IAudioReader— definesread(path)andreadFromMemory(data, size). Swap in any audio format or source.
The FingerprintEngine class depends only on these interfaces, so it is fully testable and decoupled from I/O.
| Dependency | Required | Notes |
|---|---|---|
| C++17 compiler | yes | GCC 12+ or Clang 14+ |
| CMake ≥ 3.20 | yes | |
| libsndfile | yes | WAV / FLAC / OGG decoding |
| SQLite 3 | bundled | Fetched as amalgamation via CMake FetchContent — no system package needed |
| gRPC C++ | optional | Only needed with WAVEIDEN_ENABLE_GRPC=ON |
Install libsndfile on Fedora/RHEL:
sudo dnf install libsndfile-develOn Debian/Ubuntu:
sudo apt install libsndfile1-dev# Build library + CLI
make build
# Or with explicit CMake
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
# With gRPC extension (requires gRPC C++ installed on the system)
make grpcThe build produces:
build/libwaveiden.a— static librarybuild/waveiden_cli— command-line tool
# Installs headers to /usr/local/include/waveiden/
# and libwaveiden.a to /usr/local/lib/
sudo make install
# Or to a custom prefix
cmake --install build --prefix ~/.localAfter installation, link against the library:
find_package(waveiden REQUIRED)
target_link_libraries(my_app PRIVATE waveiden::waveiden)Or with plain compiler flags:
g++ -std=c++17 main.cpp -lwaveiden -lsndfile -o appwaveiden_cli <mode> [args...]
index <db_path> <song.wav> [song2.wav ...] Index one or more songs
match <db_path> <query.wav> Match a clip against the database
list <db_path> List all indexed songs
remove <db_path> <name> Remove a song by name
clear <db_path> Wipe the entire database
# Index a music library
./build/waveiden_cli index library.db song1.wav song2.wav song3.wav
# Identify a recording or clip
./build/waveiden_cli match library.db unknown_clip.wav
# MATCH: song2.wav confidence=8400 offset=42.3s
# See what is in the database
./build/waveiden_cli list library.db
# Remove a single entry
./build/waveiden_cli remove library.db song1.wav
# Wipe everything
./build/waveiden_cli clear library.dbmake index DB=library.db FILES="song1.wav song2.wav"
make match DB=library.db QUERY=clip.wav
make list DB=library.db
make remove DB=library.db NAME=song1.wav
make clear DB=library.db
make test # smoke test against sample WAVs#include <waveiden/waveiden.hpp>
int main() {
waveiden::SQLiteDatabase db;
db.connect("library.db");
waveiden::FingerprintEngine engine;
// Index songs
engine.indexFile("song1.wav", db);
engine.indexFile("song2.wav", db);
// Match a clip
waveiden::MatchResult result = engine.matchFile("clip.wav", db);
if (result.found) {
std::cout << result.songName << "\n";
std::cout << result.confidence << " matching fingerprints\n";
std::cout << result.timeOffset << "s into the song\n";
}
}class MyDatabase : public waveiden::IDatabase {
public:
void connect(const std::string& uri) override { /* ... */ }
void disconnect() override { /* ... */ }
void indexSong(const std::string& name,
const std::vector<waveiden::Fingerprint>& prints) override { /* ... */ }
waveiden::MatchResult match(const std::vector<waveiden::Fingerprint>& query) const override { /* ... */ }
std::vector<std::string> listSongs() const override { /* ... */ }
void removeSong(const std::string& name) override { /* ... */ }
void clear() override { /* ... */ }
bool isEmpty() const override { /* ... */ }
};class MyReader : public waveiden::IAudioReader {
public:
waveiden::AudioBuffer read(const std::string& path) const override { /* ... */ }
waveiden::AudioBuffer readFromMemory(const uint8_t* data, size_t size) const override { /* ... */ }
};
waveiden::FingerprintEngine engine(
waveiden::FingerprintConfig{},
std::make_unique<MyReader>());When built with -DWAVEIDEN_ENABLE_GRPC=ON, the waveiden_grpc static library exposes a gRPC service defined in grpc/proto/waveiden.proto. Two example binaries are also built: waveiden_grpc_server and waveiden_grpc_client.
service WaveidenService {
rpc IndexSong (IndexSongRequest) returns (IndexSongResponse);
rpc MatchSong (MatchSongRequest) returns (MatchSongResponse);
rpc ListSongs (ListSongsRequest) returns (ListSongsResponse);
rpc RemoveSong (RemoveSongRequest) returns (RemoveSongResponse);
rpc ClearDatabase (ClearDatabaseRequest) returns (ClearDatabaseResponse);
}# Build with gRPC enabled
make grpc
# Terminal 1 — start the server
make server DB=library.db
# or directly:
./build/waveiden_grpc_server --listen 0.0.0.0:50051 --db library.db
# Terminal 2 — use the client
./build/waveiden_grpc_client index "Rap God" song.wav "Lose Yourself" song2.wav
./build/waveiden_grpc_client match clip.wav
./build/waveiden_grpc_client list
./build/waveiden_grpc_client remove "Rap God"
./build/waveiden_grpc_client clear
# Point at a remote server
./build/waveiden_grpc_client --server 192.168.1.10:50051 match clip.wavMakefile shortcuts for the client:
make rpc-index FILES="MySong song.wav MySong2 song2.wav"
make rpc-match QUERY=clip.wav
make rpc-list
make rpc-remove NAME=MySong
make rpc-clear
# Override the server address
make rpc-match SERVER_ADDR=192.168.1.10:50051 QUERY=clip.wav#include <waveiden/waveiden.hpp>
#include <waveiden_service.hpp>
int main() {
waveiden::SQLiteDatabase db;
db.connect("library.db");
// Blocks until the server is shut down
waveiden::grpc_ext::runServer("0.0.0.0:50051", db);
}Link against waveiden_grpc (which pulls in waveiden, grpc++, and protobuf transitively):
target_link_libraries(my_server PRIVATE waveiden_grpc)#include <waveiden.grpc.pb.h>
#include <grpcpp/grpcpp.h>
auto channel = grpc::CreateChannel("localhost:50051",
grpc::InsecureChannelCredentials());
auto stub = waveiden::WaveidenService::NewStub(channel);
// Match a clip
waveiden::MatchSongRequest req;
req.set_audio_data(/* raw WAV bytes */);
waveiden::MatchSongResponse resp;
grpc::ClientContext ctx;
stub->MatchSong(&ctx, req, &resp);
if (resp.found())
std::cout << resp.song_name() << " @ " << resp.time_offset() << "s\n";Tests run against two full songs and two query clips on a single CPU core.
xychart-beta horizontal
title "Fingerprints indexed per song"
x-axis ["full_song1.wav (4m 29s)", "full_song2.wav (10m 6s)"]
y-axis "Fingerprints" 0 --> 110000
bar [99011, 96885]
xychart-beta
title "Indexing time (seconds)"
x-axis ["full_song1 (4m 29s)", "full_song2 (10m 6s)"]
y-axis "Wall time (s)" 0 --> 10
bar [3.4, 7.9]
Confidence = number of time-coherent hash collisions between the query and the matched song. A full song queried against itself is the theoretical maximum.
xychart-beta horizontal
title "Match confidence score"
x-axis ["full_song1 vs itself", "full_song2 vs itself", "half.wav (5m03s clip)", "middle5.wav (5s clip)"]
y-axis "Confidence" 0 --> 100000
bar [99011, 96885, 23336, 81]
xychart-beta horizontal
title "Match latency (seconds)"
x-axis ["full_song1 vs itself", "full_song2 vs itself", "half.wav (5m03s clip)", "middle5.wav (5s clip)"]
y-axis "Wall time (s)" 0 --> 17
bar [6.2, 16.1, 8.7, 0.2]
middle5.wavis a 5-second clip taken from the middle offull_song2.wav(offset ≈ 30s). Its low confidence (81) relative to its correct match illustrates that shorter clips produce fewer fingerprints and therefore lower absolute scores — but the match is still unambiguous since no other song comes close.
FingerprintConfig controls the DSP pipeline:
waveiden::FingerprintConfig cfg;
cfg.frameSize = 2048; // FFT window size in samples
cfg.hopSize = 512; // Hop between frames in samples
cfg.peakPct = 98.5; // Magnitude percentile threshold for peaks (higher = fewer, stronger peaks)
cfg.maxPeaks = 10000; // Cap on peaks kept per song
cfg.maxPairsPerPeak = 10; // Pairs formed per anchor peak
cfg.maxDt = 100; // Maximum time-frame distance between paired peaks
waveiden::FingerprintEngine engine(cfg);Raise peakPct or lower maxPairsPerPeak to reduce database size. Lower peakPct or raise maxPairsPerPeak to improve match recall on noisy clips.