CVE-2024-27850
Overview
Background
- Audio fingerprinting
- Deriving a stable device/OS identifier from subtle differences in Web Audio processing output.
- Noise injection (Private Browsing)
- WebKit perturbs audio readback with random noise so the output cannot be used as a stable fingerprint.
- Averaging attack
- Repeating a computation and averaging the noised results to cancel zero-mean noise and recover the true value; looping a sample is one way to repeat cheaply.
- Normally-distributed vs uniform noise
- Normal (Gaussian) noise resists simple min/max averaging far better than uniform noise, raising the cost of the attack.
Root Cause Analysis
This hardens Web Audio anti-fingerprinting noise injection in Private Browsing against an averaging attack. To defeat audio fingerprinting, WebKit injects noise into audio readback (e.g. via OfflineAudioContext) in Private Browsing. The pre-patch scheme used a fixed, uniformly-distributed noise level (0.001) gated by a single boolean flag (m_needsAdditionalNoise); an attacker could negate it by looping one small audio sample many times within a single audio buffer and averaging the repeated readbacks, converging on the true (un-noised) value and recovering a stable fingerprint.
The fix implements several mitigations. It replaces the boolean m_needsAdditionalNoise with an m_noiseInjectionMultiplier (the standard deviation of the injected noise) so noise magnitude can vary. It switches from uniform to NORMALLY-distributed noise, which does not converge under simple min/max averaging and requires far more iterations to average out. It caches previously-generated random values and reapplies the same noise to values encountered repeatedly, so recomputing the same value many times in one buffer yields no new information. And it lets node types known to expose hardware/OS differences inject more noise than the 0.1% baseline: DynamicsCompressorNode and OscillatorNode raise the level, and crucially AudioBufferSourceNode amplifies noise with loop count — for a source looping more than 200 times it boosts noise by a factor of 0.005 per loop, so looping a tiny sample back-to-back in a large buffer (the exact averaging technique) produces a massive amount of noise instead of a recoverable average. New plumbing (AudioNode::noiseInjectionMultiplier subclassing hook, AudioNodeOutput::forEachInputNode) propagates the multiplier through the processing graph.
The restored invariant is that averaging/looping cannot cancel the injected noise, so audio fingerprinting stays defeated in Private Browsing.
Attack Path
- Build a looping audio graph In Private Browsing, use OfflineAudioContext with an AudioBufferSourceNode that loops a tiny sample many times in a large buffer.
- Read back repeatedly Read the rendered audio buffer, obtaining many noised copies of the same underlying value.
- Average out the noise Average the repeated readbacks; with uniform fixed noise the mean converges on the true value.
- Recover the fingerprint Reconstruct the un-noised audio characteristics to fingerprint the device/OS despite the anti-fingerprinting noise.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp |
modified | |
ifSource/WebCore/Modules/webaudio/AudioWorkletNode.cpp |
modified | |
forTools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html |
modified |
Files Changed
Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.hSource/WebCore/Modules/webaudio/AudioBuffer.cppSource/WebCore/Modules/webaudio/AudioBuffer.hSource/WebCore/Modules/webaudio/AudioBufferSourceNode.cppSource/WebCore/Modules/webaudio/AudioBufferSourceNode.hSource/WebCore/Modules/webaudio/AudioNode.hSource/WebCore/Modules/webaudio/AudioNodeOutput.cppSource/WebCore/Modules/webaudio/AudioNodeOutput.hSource/WebCore/Modules/webaudio/AudioWorkletNode.cppSource/WebCore/Modules/webaudio/BaseAudioContext.hSource/WebCore/Modules/webaudio/DynamicsCompressorNode.hSource/WebCore/Modules/webaudio/OfflineAudioContext.cppSource/WebCore/Modules/webaudio/OfflineAudioContext.hSource/WebCore/Modules/webaudio/OscillatorNode.hSource/WebCore/platform/audio/AudioUtilities.cppSource/WebCore/platform/audio/AudioUtilities.hTools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mmTools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
Audit Directions
- Same module: noise coverageAudit AudioBuffer/AudioNode readback paths (worklets, OfflineAudioContext render, copyTo) to confirm every readback applies the multiplier-scaled noise and reuses cached randoms for repeated values.
- Other averageable anti-fingerprinting noiseReview canvas/WebGL/other fingerprinting mitigations that add fixed, independent noise per read, which a repeat-and-average attack could similarly cancel.
Patch
diff --git a/Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h b/Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h
index bb5b60dcf985..9ef36a321e21 100644
--- a/Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h
+++ b/Source/WebCore/Modules/webaudio/AudioBasicProcessorNode.h
@@ -60,6 +60,8 @@ protected:
AudioProcessor* processor() { return m_processor.get(); }
const AudioProcessor* processor() const { return m_processor.get(); }
+ float noiseInjectionMultiplier() const override { return 0.01; }
+
std::unique_ptr<AudioProcessor> m_processor;
};
diff --git a/Source/WebCore/Modules/webaudio/AudioBuffer.cpp b/Source/WebCore/Modules/webaudio/AudioBuffer.cpp
index 15f207c841d7..d4d518898f8f 100644
--- a/Source/WebCore/Modules/webaudio/AudioBuffer.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioBuffer.cpp
@@ -155,7 +155,7 @@ void AudioBuffer::releaseMemory()
Locker locker { m_channelsLock };
m_channels = { };
m_channelWrappers = { };
- m_needsAdditionalNoise = false;
+ m_noiseInjectionMultiplier = 0;
}
ExceptionOr<JSC::JSValue> AudioBuffer::getChannelData(JSDOMGlobalObject& globalObject, unsigned channelIndex)
@@ -264,7 +264,7 @@ ExceptionOr<void> AudioBuffer::copyToChannel(Ref<Float32Array>&& source, unsigne
ASSERT(dst);
memmove(dst + bufferOffset, src, count * sizeof(*dst));
- m_needsAdditionalNoise = false;
+ m_noiseInjectionMultiplier = 0;
return { };
}
@@ -273,7 +273,7 @@ void AudioBuffer::zero()
for (auto& channel : m_channels)
channel->zeroFill();
- m_needsAdditionalNoise = false;
+ m_noiseInjectionMultiplier = 0;
}
size_t AudioBuffer::memoryCost() const
@@ -315,7 +315,7 @@ bool AudioBuffer::copyTo(AudioBuffer& other) const
for (unsigned channelIndex = 0; channelIndex < numberOfChannels(); ++channelIndex)
memcpy(other.rawChannelData(channelIndex), m_channels[channelIndex]->data(), length() * sizeof(float));
- other.m_needsAdditionalNoise = m_needsAdditionalNoise;
+ other.m_noiseInjectionMultiplier = m_noiseInjectionMultiplier;
return true;
}
@@ -335,13 +335,13 @@ WebCoreOpaqueRoot root(AudioBuffer* buffer)
void AudioBuffer::applyNoiseIfNeeded()
{
- if (!m_needsAdditionalNoise)
+ if (!m_noiseInjectionMultiplier)
return;
for (auto& channel : m_channels)
- AudioUtilities::applyNoise(channel->data(), channel->length(), 0.001);
+ AudioUtilities::applyNoise(channel->data(), channel->length(), m_noiseInjectionMultiplier);
- m_needsAdditionalNoise = false;
+ m_noiseInjectionMultiplier = 0;
}
} // namespace WebCore
diff --git a/Source/WebCore/Modules/webaudio/AudioBuffer.h b/Source/WebCore/Modules/webaudio/AudioBuffer.h
index cbc38ca758f4..e0a22df95b96 100644
--- a/Source/WebCore/Modules/webaudio/AudioBuffer.h
+++ b/Source/WebCore/Modules/webaudio/AudioBuffer.h
@@ -86,7 +86,8 @@ public:
bool topologyMatches(const AudioBuffer&) const;
- void setNeedsAdditionalNoise() { m_needsAdditionalNoise = true; }
+ void increaseNoiseInjectionMultiplier(float amount = 0.001) { m_noiseInjectionMultiplier += amount; }
+ float noiseInjectionMultiplier() const { return m_noiseInjectionMultiplier; }
private:
AudioBuffer(unsigned numberOfChannels, size_t length, float sampleRate, LegacyPreventDetaching = LegacyPreventDetaching::No);
@@ -109,7 +110,7 @@ private:
FixedVector<JSValueInWrappedObject> m_channelWrappers;
bool m_isDetachable { true };
mutable Lock m_channelsLock;
- bool m_needsAdditionalNoise { false };
+ float m_noiseInjectionMultiplier { 0 };
};
WebCoreOpaqueRoot root(AudioBuffer*);
diff --git a/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp b/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp
index 42f2779e6658..22ddbfedac07 100644
--- a/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp
@@ -590,6 +590,22 @@ bool AudioBufferSourceNode::propagatesSilence() const
return !m_buffer;
}
+float AudioBufferSourceNode::noiseInjectionMultiplier() const
+{
+ Locker locker { m_processLock };
+
+ if (!m_buffer)
+ return 0;
+
+ auto multiplier = m_buffer->noiseInjectionMultiplier();
+ if (m_isLooping && m_loopStart < m_loopEnd) {
+ static constexpr auto noiseMultiplierPerLoop = 0.005;
+ auto loopCount = m_buffer->duration() / (m_loopEnd - m_loopStart);
+ multiplier *= std::max(1.0, noiseMultiplierPerLoop * loopCount);
+ }
+ return multiplier;
+}
+
} // namespace WebCore
#endif // ENABLE(WEB_AUDIO)
diff --git a/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h b/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h
index 40a1e195db30..b5140957d863 100644
--- a/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h
+++ b/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h
@@ -86,6 +86,8 @@ private:
double tailTime() const final { return 0; }
double latencyTime() const final { return 0; }
+ float noiseInjectionMultiplier() const final;
+
ExceptionOr<void> startPlaying(double when, double grainOffset, std::optional<double> grainDuration);
void adjustGrainParameters() WTF_REQUIRES_LOCK(m_processLock);
diff --git a/Source/WebCore/Modules/webaudio/AudioNode.h b/Source/WebCore/Modules/webaudio/AudioNode.h
index 13e60aac9884..b01ee0ebb9ac 100644
--- a/Source/WebCore/Modules/webaudio/AudioNode.h
+++ b/Source/WebCore/Modules/webaudio/AudioNode.h
@@ -198,6 +198,7 @@ public:
void setIsTailProcessing(bool isTailProcessing) { m_isTailProcessing = isTailProcessing; }
NoiseInjectionPolicy noiseInjectionPolicy() const;
+ virtual float noiseInjectionMultiplier() const { return 0; }
protected:
// Inputs and outputs must be created before the AudioNode is initialized.
diff --git a/Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp b/Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp
index e3a9ba248b48..cde2785c6046 100644
--- a/Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioNodeOutput.cpp
@@ -175,6 +175,14 @@ void AudioNodeOutput::removeInput(AudioNodeInput* input)
m_inputs.remove(input);
}
+void AudioNodeOutput::forEachInputNode(Function<void(AudioNode&)>&& callback) const
+{
+ for (auto& node : m_inputs.values()) {
+ if (node)
+ callback(*node);
+ }
+}
+
void AudioNodeOutput::disconnectAllInputs()
{
ASSERT(context().isGraphOwner());
diff --git a/Source/WebCore/Modules/webaudio/AudioNodeOutput.h b/Source/WebCore/Modules/webaudio/AudioNodeOutput.h
index ccf95eda1831..a37a939dab70 100644
--- a/Source/WebCore/Modules/webaudio/AudioNodeOutput.h
+++ b/Source/WebCore/Modules/webaudio/AudioNodeOutput.h
@@ -68,6 +68,7 @@ public:
// Must be called with the context's graph lock.
void disconnectAll();
+ void forEachInputNode(Function<void(AudioNode&)>&&) const;
void setNumberOfChannels(unsigned);
unsigned numberOfChannels() const { return m_numberOfChannels; }
diff --git a/Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp b/Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp
index bace34d9262f..28329737ac0f 100644
--- a/Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp
+++ b/Source/WebCore/Modules/webaudio/AudioWorkletNode.cpp
@@ -221,7 +221,7 @@ void AudioWorkletNode::process(size_t framesToProcess)
if (auto& input = m_inputs[inputIndex]) {
for (unsigned channelIndex = 0; channelIndex < input->numberOfChannels(); ++channelIndex) {
auto* channel = input->channel(channelIndex);
- AudioUtilities::applyNoise(channel->mutableData(), channel->length(), 0.001);
+ AudioUtilities::applyNoise(channel->mutableData(), channel->length(), 0.01);
}
}
}
diff --git a/Source/WebCore/Modules/webaudio/BaseAudioContext.h b/Source/WebCore/Modules/webaudio/BaseAudioContext.h
index fdd2c6b783ca..086994e4ac1b 100644
--- a/Source/WebCore/Modules/webaudio/BaseAudioContext.h
+++ b/Source/WebCore/Modules/webaudio/BaseAudioContext.h
@@ -251,6 +251,10 @@ protected:
void clear();
+protected:
+ // Only accessed when the graph lock is held.
+ const Vector<AudioConnectionRefPtr<AudioNode>>& referencedSourceNodes() const { return m_referencedSourceNodes; }
+
private:
void scheduleNodeDeletion();
void workletIsReady();
diff --git a/Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h b/Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h
index b3e020dbd732..f45d3e327ce5 100644
--- a/Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h
+++ b/Source/WebCore/Modules/webaudio/DynamicsCompressorNode.h
@@ -69,6 +69,8 @@ private:
double latencyTime() const final;
bool requiresTailProcessing() const final;
+ float noiseInjectionMultiplier() const final { return 0.01; }
+
std::unique_ptr<DynamicsCompressor> m_dynamicsCompressor;
Ref<AudioParam> m_threshold;
Ref<AudioParam> m_knee;
diff --git a/Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp b/Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp
index 41dc85995c2f..6e9671838d8b 100644
--- a/Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp
+++ b/Source/WebCore/Modules/webaudio/OfflineAudioContext.cpp
@@ -51,7 +51,7 @@ OfflineAudioContext::OfflineAudioContext(Document& document, const OfflineAudioC
if (!renderTarget())
document.addConsoleMessage(MessageSource::JS, MessageLevel::Warning, makeString("Failed to construct internal AudioBuffer with "_s, options.numberOfChannels, " channel(s), a sample rate of "_s, options.sampleRate, " and a length of "_s, options.length, '.'));
else if (noiseInjectionPolicy() == NoiseInjectionPolicy::Minimal)
- renderTarget()->setNeedsAdditionalNoise();
+ renderTarget()->increaseNoiseInjectionMultiplier();
}
ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutionContext& context, const OfflineAudioContextOptions& options)
@@ -76,6 +76,43 @@ ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutio
return create(context, { numberOfChannels, length, sampleRate });
}
+void OfflineAudioContext::lazyInitialize()
+{
+ BaseAudioContext::lazyInitialize();
+
+ increaseNoiseMultiplierIfNeeded();
+}
+
+void OfflineAudioContext::increaseNoiseMultiplierIfNeeded()
+{
+ if (noiseInjectionPolicy() == NoiseInjectionPolicy::None)
+ return;
+
+ Locker locker { graphLock() };
+
+ RefPtr target = renderTarget();
+ if (!target)
+ return;
+
+ Vector<AudioConnectionRefPtr<AudioNode>, 1> remainingNodes;
+ for (auto& node : referencedSourceNodes())
+ remainingNodes.append(node.copyRef());
+
+ while (!remainingNodes.isEmpty()) {
+ auto node = remainingNodes.takeLast();
+ target->increaseNoiseInjectionMultiplier(node->noiseInjectionMultiplier());
+ for (unsigned i = 0; i < node->numberOfOutputs(); ++i) {
+ auto* output = node->output(i);
+ if (!output)
+ continue;
+
+ output->forEachInputNode([&](auto& inputNode) {
+ remainingNodes.append(&inputNode);
+ });
+ }
+ }
+}
+
void OfflineAudioContext::uninitialize()
{
if (!isInitialized())
diff --git a/Source/WebCore/Modules/webaudio/OfflineAudioContext.h b/Source/WebCore/Modules/webaudio/OfflineAudioContext.h
index 3dab3554f09d..24cad04a9c1d 100644
--- a/Source/WebCore/Modules/webaudio/OfflineAudioContext.h
+++ b/Source/WebCore/Modules/webaudio/OfflineAudioContext.h
@@ -56,6 +56,9 @@ public:
private:
OfflineAudioContext(Document&, const OfflineAudioContextOptions&);
+ void lazyInitialize() final;
+ void increaseNoiseMultiplierIfNeeded();
+
AudioBuffer* renderTarget() const { return destination().renderTarget(); }
// ActiveDOMObject
diff --git a/Source/WebCore/Modules/webaudio/OscillatorNode.h b/Source/WebCore/Modules/webaudio/OscillatorNode.h
index 7d455719acd2..06ddd6c7f083 100644
--- a/Source/WebCore/Modules/webaudio/OscillatorNode.h
+++ b/Source/WebCore/Modules/webaudio/OscillatorNode.h
@@ -65,6 +65,8 @@ private:
bool propagatesSilence() const final;
+ float noiseInjectionMultiplier() const final { return 0.01; }
+
// One of the waveform types defined in the enum.
OscillatorType m_type; // Only used on the main thread.
diff --git a/Source/WebCore/platform/audio/AudioUtilities.cpp b/Source/WebCore/platform/audio/AudioUtilities.cpp
index 73ac6ad455ef..29f8b9688893 100644
--- a/Source/WebCore/platform/audio/AudioUtilities.cpp
+++ b/Source/WebCore/platform/audio/AudioUtilities.cpp
@@ -27,6 +27,7 @@
#if ENABLE(WEB_AUDIO)
#include "AudioUtilities.h"
+#include <random>
#include <wtf/CryptographicallyRandomNumber.h>
#include <wtf/MathExtras.h>
#include <wtf/WeakRandom.h>
@@ -77,11 +78,30 @@ size_t timeToSampleFrame(double time, double sampleRate, SampleFrameRounding rou
return static_cast<size_t>(frame);
}
-void applyNoise(float* values, size_t numberOfElementsToProcess, float magnitude)
+void applyNoise(float* values, size_t numberOfElementsToProcess, float standardDeviation)
{
- WeakRandom generator;
+ std::random_device device;
+ std::mt19937 generator(device());
+ std::normal_distribution<float> distribution(1, standardDeviation);
+
+ HashMap<float, float> noiseMultipliers;
+ auto computeNoiseMultiplier = [&](float rawValue) {
+ if (!noiseMultipliers.isValidKey(rawValue))
+ return distribution(generator);
+
+ auto result = noiseMultipliers.ensure(rawValue, [&] {
+ return distribution(generator);
+ }).iterator->value;
+
+ static constexpr auto maxNoiseMultiplierMapSize = 250000;
+ if (noiseMultipliers.size() >= maxNoiseMultiplierMapSize)
+ noiseMultipliers.remove(noiseMultipliers.random());
+
+ return result;
+ };
+
for (size_t i = 0; i < numberOfElementsToProcess; ++i)
- values[i] *= 1 + magnitude * (2 * generator.get() - 1);
+ values[i] *= computeNoiseMultiplier(values[i]);
}
} // AudioUtilites
diff --git a/Source/WebCore/platform/audio/AudioUtilities.h b/Source/WebCore/platform/audio/AudioUtilities.h
index 21c98865b8dc..fbf935e3e010 100644
--- a/Source/WebCore/platform/audio/AudioUtilities.h
+++ b/Source/WebCore/platform/audio/AudioUtilities.h
@@ -64,7 +64,7 @@ inline float decibelsToLinear(float decibels)
return powf(10, 0.05f * decibels);
}
-void applyNoise(float* values, size_t numberOfElementsToProcess, float magnitude);
+void applyNoise(float* values, size_t numberOfElementsToProcess, float standardDeviation);
} // AudioUtilites
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
index a72c56140bbb..e322eb865738 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
@@ -877,6 +877,7 @@ TEST(AdvancedPrivacyProtections, AddNoiseToWebAudioAPIs)
checkFingerprintForNoise(@"testOscillatorCompressor");
checkFingerprintForNoise(@"testOscillatorCompressorWorklet");
checkFingerprintForNoise(@"testOscillatorCompressorAnalyzer");
+ checkFingerprintForNoise(@"testLoopingOscillatorCompressorBiquadFilter");
}
// FIXME when rdar://115137641 is resolved.
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
index 11201a33ae54..108373919a07 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
@@ -32,8 +32,10 @@ button {
<script>
function combineSamples(samples) {
let result = 0;
- for (const value of samples)
- result += value;
+ for (const value of samples) {
+ if (isFinite(value))
+ result += value;
+ }
return result;
}
@@ -145,6 +147,109 @@ async function testOscillatorCompressorAnalyzer() {
return combineSamples(bins);
}
+
+async function testLoopingOscillatorCompressorBiquadFilter() {
+ const sampleRate = 44100;
+ const cloneCount = 40000;
+ const targetSampleIndex = 3395;
+ const stabilizationPrecision = 6.2;
+
... (file diff truncated)
Regression Test / PoC
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
index a72c56140bbb..e322eb865738 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/AdvancedPrivacyProtections.mm
@@ -877,6 +877,7 @@ TEST(AdvancedPrivacyProtections, AddNoiseToWebAudioAPIs)
checkFingerprintForNoise(@"testOscillatorCompressor");
checkFingerprintForNoise(@"testOscillatorCompressorWorklet");
checkFingerprintForNoise(@"testOscillatorCompressorAnalyzer");
+ checkFingerprintForNoise(@"testLoopingOscillatorCompressorBiquadFilter");
}
// FIXME when rdar://115137641 is resolved.
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html b/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
index 11201a33ae54..108373919a07 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/audio-fingerprinting.html
@@ -32,8 +32,10 @@ button {
<script>
function combineSamples(samples) {
let result = 0;
- for (const value of samples)
- result += value;
+ for (const value of samples) {
+ if (isFinite(value))
+ result += value;
+ }
return result;
}
@@ -145,6 +147,109 @@ async function testOscillatorCompressorAnalyzer() {
return combineSamples(bins);
}
+
+async function testLoopingOscillatorCompressorBiquadFilter() {
+ const sampleRate = 44100;
+ const cloneCount = 40000;
+ const targetSampleIndex = 3395;
+ const stabilizationPrecision = 6.2;
+
+ async function getBaseAudioFingerprint() {
+ const baseSignal = await (async function() {
+ const context = new OfflineAudioContext(1, targetSampleIndex + 1, 44100);
+
+ const oscillator = context.createOscillator();
+ oscillator.type = "square";
+ oscillator.frequency.value = 1000;
+
+ const compressor = context.createDynamicsCompressor();
+ compressor.threshold.value = -70;
+ compressor.knee.value = 40;
+ compressor.ratio.value = 12;
+ compressor.attack.value = 0;
+ compressor.release.value = 0.25;
+
+ const filter = context.createBiquadFilter();
+ filter.type = "allpass";
+ filter.frequency.value = 5.239622852977861;
+ filter.Q.value = 0.1;
+
+ oscillator.connect(compressor);
+ compressor.connect(filter);
+ filter.connect(context.destination);
+ oscillator.start(0);
+
+ return await context.startRendering();
+ })();
+
+ const context = new OfflineAudioContext(1, baseSignal.length - 1 + cloneCount, sampleRate);
+ const sourceNode = context.createBufferSource();
+ sourceNode.buffer = baseSignal;
+ sourceNode.loop = true;
+ sourceNode.loopStart = (baseSignal.length - 1) / sampleRate;
+ sourceNode.loopEnd = baseSignal.length / sampleRate;
+ sourceNode.connect(context.destination);
+ sourceNode.start();
+
+ const clonedSignal = await context.startRendering();
+ const fingerprint = extractFingerprint(baseSignal, clonedSignal.getChannelData(0).subarray(baseSignal.length - 1));
+ return Math.abs(fingerprint);
+ }
+
+ function extractFingerprint(baseSignal, clonedSample) {
+ let fingerprint;
+ let needsDenoising = false;
+
+ for (let i = 0; i < clonedSample.length; i += Math.floor(clonedSample.length / 10)) {
+ if (clonedSample[i] === 0) {
+ // Intentionally ignored.
+ continue;
+ }
+
+ if (fingerprint === undefined) {
+ fingerprint = clonedSample[i];
+ continue;
+ }
+
+ if (fingerprint !== clonedSample[i]) {
+ needsDenoising = true;
+ break;
+ }
+ }
+
+ if (fingerprint === undefined)
+ fingerprint = baseSignal.getChannelData(0)[baseSignal.length - 1];
+ else if (needsDenoising)
+ fingerprint = getMean(clonedSample);
+
+ return fingerprint;
+ }
+
+ function getMean(signal) {
+ let total = 0;
+ let finiteValueCount = 0;
+ for (let i = 0; i < signal.length; i++) {
+ const value = signal[i];
+ if (!isFinite(value))
+ continue;
+ total += value;
+ finiteValueCount++;
+ }
+ return finiteValueCount ? (total / finiteValueCount) : 0;
+ }
+
+ function stabilize(value, precision) {
+ if (value === 0)
+ return value;
+ const power = Math.floor(Math.log10(Math.abs(value)));
+ const precisionPower = power - Math.floor(precision) + 1;
+ const precisionBase = 10 ** -precisionPower * ((precision * 10) % 10 || 1);
+ return Math.round(value * precisionBase) / precisionBase;
+ }
+
+ const rawFingerprint = await getBaseAudioFingerprint();
+ return stabilize(rawFingerprint, stabilizationPrecision);
+}
</script>
</body>
</html>