如何用C#实现Philips Robust Hashing算法提取32位音频指纹?
Hey there! As someone who’s built audio fingerprinting tools before, I’ll walk you through implementing PRH (from Haitsma’s A Highly Robust Audio Fingerprinting System) in C# step by step. We’ll cover the core algorithm logic, code snippets, and key tips to avoid common pitfalls.
Core PRH Algorithm Overview
First, let’s recap the high-level flow to make sure we’re aligned:
- Preprocess Audio: Convert to 8kHz mono, 16-bit PCM (PRH’s standard input format).
- Frame the Signal: Split audio into overlapping frames with specific timing.
- Spectral Analysis: Compute FFT for each frame, extract frequency band energies.
- Differential Energy Calculation: Compute energy differences across consecutive frames.
- Fingerprint Generation: Compare differential values across frequency bands to generate 32-bit binary fingerprints.
Step-by-Step Implementation
1. Setup Dependencies
We’ll use two libraries to simplify heavy lifting:
NAudio: For reading and converting audio files.MathNet.Numerics: For FFT calculations.
Install them via NuGet:
Install-Package NAudio Install-Package MathNet.Numerics
2. Audio Preprocessing
First, convert any input audio (WAV, MP3, etc.) to 8kHz mono, 16-bit PCM. Here’s a helper method:
using NAudio.Wave; using System.IO; using System.Collections.Generic; public float[] PreprocessAudio(string audioFilePath) { using var audioFileReader = new AudioFileReader(audioFilePath); // Convert to 8kHz mono, 16-bit PCM using var resampler = new MediaFoundationResampler(audioFileReader, new WaveFormat(8000, 16, 1)); resampler.ResamplerQuality = 60; // Balanced quality/speed // Read all samples as normalized float values [-1, 1] var samples = new List<float>(); var buffer = new float[4096]; int bytesRead; while ((bytesRead = resampler.Read(buffer, 0, buffer.Length)) > 0) { samples.AddRange(buffer.Take(bytesRead)); } return samples.ToArray(); }
3. Frame the Audio Signal
PRH uses:
- Frame length: 31.25ms = 250 samples (at 8kHz)
- Frame shift: 10ms = 80 samples (170 samples overlap between frames)
- Hamming window to reduce spectral leakage
using MathNet.Numerics.Signals; using System.Collections.Generic; public List<float[]> GetFramedSamples(float[] audioSamples) { const int sampleRate = 8000; const float frameDurationMs = 31.25f; const float frameShiftMs = 10f; int frameSize = (int)(sampleRate * frameDurationMs / 1000); int frameShift = (int)(sampleRate * frameShiftMs / 1000); var frames = new List<float[]>(); // Generate Hamming window var window = Window.Hamming(frameSize); for (int i = 0; i + frameSize <= audioSamples.Length; i += frameShift) { var frame = audioSamples.Skip(i).Take(frameSize).ToArray(); // Apply window to the frame for (int j = 0; j < frameSize; j++) { frame[j] *= window[j]; } frames.Add(frame); } return frames; }
4. Compute Frequency Band Energies
Calculate the FFT for each frame, then split the 0-3kHz frequency range into 33 logarithmically spaced bands (as specified in Haitsma’s paper). Compute the energy (sum of squared magnitudes) for each band.
using MathNet.Numerics.IntegralTransforms; using MathNet.Numerics; using System.Collections.Generic; public List<double[]> GetBandEnergies(List<float[]> frames) { const int sampleRate = 8000; const int numBands = 33; var bandEnergies = new List<double[]>(); foreach (var frame in frames) { // Convert frame to complex numbers for FFT var complexFrame = frame.Select(x => new Complex(x, 0)).ToArray(); // Compute forward FFT (Matlab-compatible) Fourier.Forward(complexFrame, FourierOptions.Matlab); // Get max frequency bin for 3kHz int maxFreqBin = (int)(3000 * complexFrame.Length / sampleRate); // Generate logarithmic band edges var bandEdges = GenerateLogarithmicBands(0, maxFreqBin, numBands); // Calculate energy per band var energies = new double[numBands]; for (int b = 0; b < numBands; b++) { int startBin = bandEdges[b]; int endBin = bandEdges[b + 1]; double energy = 0; for (int bin = startBin; bin < endBin; bin++) { energy += complexFrame[bin].MagnitudeSquared; } energies[b] = energy; } bandEnergies.Add(energies); } return bandEnergies; } // Helper to generate logarithmic band edges private int[] GenerateLogarithmicBands(int minBin, int maxBin, int numBands) { var edges = new int[numBands + 1]; edges[0] = minBin; edges[numBands] = maxBin; // Logarithmic spacing to match human auditory perception double logMin = Math.Log10(minBin + 1); double logMax = Math.Log10(maxBin); double step = (logMax - logMin) / numBands; for (int i = 1; i < numBands; i++) { edges[i] = (int)Math.Pow(10, logMin + step * i) - 1; } return edges; }
5. Calculate Differential Energies
PRH uses two differential values per band:
- Δ1(t, i) = E(t, i) - E(t-1, i) (current vs previous frame)
- Δ2(t, i) = E(t, i) - E(t-2, i) (current vs two frames prior)
We can only generate fingerprints once we have at least 3 frames.
using System.Collections.Generic; using System; public List<Tuple<double[], double[]>> GetDifferentialEnergies(List<double[]> bandEnergies) { var diffs = new List<Tuple<double[], double[]>>(); // Start from index 2 (need t-2, t-1, t frames) for (int t = 2; t < bandEnergies.Count; t++) { var delta1 = new double[bandEnergies[t].Length]; var delta2 = new double[bandEnergies[t].Length]; for (int i = 0; i < bandEnergies[t].Length; i++) { delta1[i] = bandEnergies[t][i] - bandEnergies[t-1][i]; delta2[i] = bandEnergies[t][i] - bandEnergies[t-2][i]; } diffs.Add(Tuple.Create(delta1, delta2)); } return diffs; }
6. Generate 32-bit Fingerprints
Compare differential values across 32 non-overlapping band pairs. For each pair (i,j), set a bit to 1 if both Δ1(i) > Δ1(j) AND Δ2(i) > Δ2(j); otherwise 0.
using System.Collections.Generic; // 32 band pairs aligned with PRH specifications private static readonly (int i, int j)[] BandPairs = new (int, int)[] { (0,1),(2,3),(4,5),(6,7),(8,9),(10,11),(12,13),(14,15), (16,17),(18,19),(20,21),(22,23),(24,25),(26,27),(28,29),(30,31), (0,2),(1,3),(4,6),(5,7),(8,10),(9,11),(12,14),(13,15), (16,18),(17,19),(20,22),(21,23),(24,26),(25,27),(28,30),(29,31) }; public List<uint> GenerateFingerprints(List<Tuple<double[], double[]>> differentialEnergies) { var fingerprints = new List<uint>(); foreach (var (delta1, delta2) in differentialEnergies) { uint fingerprint = 0; for (int bitIndex = 0; bitIndex < BandPairs.Length; bitIndex++) { var (i, j) = BandPairs[bitIndex]; bool bitSet = (delta1[i] > delta1[j]) && (delta2[i] > delta2[j]); if (bitSet) { fingerprint |= (uint)1 << bitIndex; } } fingerprints.Add(fingerprint); } return fingerprints; }
Key Guidance & Tips
- Debugging: Start with short 10-30 second audio clips and print intermediate values (frame energies, deltas) to verify consistency. Double-check that your logarithmic band edges match the paper’s specs.
- Performance: For large audio files, process frames in batches instead of loading the entire file into memory. Reuse complex arrays for FFT to reduce memory overhead.
- Robustness: Test with distorted, compressed, or noisy versions of your audio to ensure fingerprints remain consistent. PRH is designed for robustness, but proper preprocessing is critical.
- Paper Reference: Keep Haitsma’s paper handy—if you run into issues, cross-verify frame sizes, band spacing, and differential logic against the original specs.
内容的提问来源于stack exchange,提问作者yuno

