WebAudio指纹概述
浏览器中的 WebAudio API 提供了丰富的功能,其中包括了大量生成和处理音频数据的API。WebAudio API 的音频指纹技术是一种利用音频信号的特征来唯一标识音频的技术。因为WebAudio API 提供了丰富的音频处理功能,包括合成、过滤、分析等,通过一系列功能组合,可以使生成的音频指纹具有一定的独特性。生成音频指纹的过程通常涉及到对音频数据进行数学运算或特征提取,这使得生成的指纹在一定程度上是稳定的,即使音频本身有一定的变化也不会影响其唯一性。而且Web Audio API 提供了实时处理音频数据的能力,因此可以用于实时生成和识别音频指纹。
WebAudio API主要用在AudioContext上下文中,在进行任何其他操作之前,始终需要创建一个AudioContext的实例,如图4-7所示。
在有了音频来源之后,通过节点压缩,就可以得到Buffer输出了。在实际的WebAudio API 操作过程中,通用的做法是创建单个AudioContext实例,并在所有后续处理中重复使用。每个AudioContext实例都具有一个目标属性,该属性用于表示该上下文中所有音频的目标。此外,还存在一种特殊类型的AudioContext,即OfflineAudioContext。在获取音频指纹的时候,通常是会创建OfflineAudioContext,主要原因在于它不会将音频呈现给设备硬件,而是快速生成音频并将其保存到AudioBuffer中。因此,OfflineAudioContext的目标是内存中的数据结构,而常规的AudioContext的目标是音频设备。
创建OfflineAudioContext实例时,需要传递三个参数:
- 通道数:通道数表示音频数据中的声道数量。在数字音频中,通常有单声道和立体声两种通道配置。单声道通常用于单一音频源,而立体声则包含左右两个独立的声道,用于模拟左右声源的位置和方向。通道数还可以扩展到更多声道,如环绕声,以提供更加沉浸式的音频体验。
- 样本总数:样本总数指的是音频数据中包含的采样数量。音频数据是通过对连续时间信号进行采样来获取的,每个采样点对应着一个特定时间点上的音频振幅值。样本总数决定了音频的持续时间,即音频的长度。采样率和样本总数共同决定了音频的时长,样本总数越多,音频的时长就越长。
- 采样率:采样率表示每秒钟对声音信号进行采样的次数,单位为赫兹Hz,采样率决定了数字音频的精度和质量,以及其能够表示的频率范围。常见的标准采样率包括44.1 kHz和48 kHz,它们通常用于CD音质和音频制作。更高的采样率,如96 kHz或192kHz可以提供更高的音频质量和更广的频率响应范围,但也会增加文件大小。
Oscillator振荡器是一种产生周期性波形的电子设备或软件组件。在音频领域中,振荡器通常用于生成声音信号的基础波形,例如正弦波、方波、锯齿波等。这些基础波形可以用于合成各种声音,是合成器和音频处理中的重要组件之一。振荡器通过产生连续的电压或数字信号来生成波形。在软件中,振荡器通常是由算法来模拟的,这些算法根据所需的波形形状和参数生成连续的样本值。在处理音频时需要一个来源,振荡器是一个很好的选择,因为它是通过数学方法生成样本的,可以生成具有指定频率的周期波形。
压缩节点是一种用于动态范围压缩的节点类型。它可以降低音频信号的动态范围,即减小最响亮部分与最安静部分之间的差异,从而提高音频的平均音量并减少峰值。压缩节点通常用于音频信号处理的动态范围控制,以确保音频在播放过程中的一致性和平衡。压缩节点通常作为音频处理图中的一个节点,与其他节点,如声音源、效果器等连接在一起,以对输入信号进行压缩处理。通过调整压缩节点的参数,可以控制压缩的程度和效果,从而实现对音频信号动态范围的调节和控制。
AudioBuffer是WebAudio API中表示音频数据的数据结构。它用于存储音频样本的实际数据,并提供了一组函数来访问和操作这些数据。在WebAudio API中,AudioBuffer通常作为音频源节点的输入,用于播放音频或将其传递给其他音频处理节点进行进一步处理。AudioBuffer中包含音频数据的实际样本,这些样本表示音频波形在离散时间点上的振幅值。音频数据可以来自于多种来源,例如从服务器加载、用户录制或通过WebAudio API生成。每个AudioBuffer实例都具有固定的采样率和通道数。这些属性在创建AudioBuffer时被指定,并决定了AudioBuffer中存储的音频数据的格式和结构。此外,AudioBuffer提供了一组函数来访问和操作存储的音频数据。例如,可以使用getChannelData获取特定通道的音频数据,并使用set修改音频数据的值。这些函数使得可以直接对音频数据进行编辑和处理,例如音频混合、剪辑、变速、变调等操作。AudioBuffer通常作为音频源节点的输入,用于播放音频或将其传递给其他音频处理节点进行进一步处理。通过创建AudioBufferSourceNode并将AudioBuffer作为其缓冲区传递给它,可以将AudioBuffer中存储的音频数据进行播放或传递给音频图中的其他节点。由于音频数据以二进制格式存储在AudioBuffer中,因此可以在Web Audio API中高效地进行音频处理操作,而无需频繁地将数据从JavaScript代码中复制到Web Audio API中。
在WebAudio指纹的计算过程中,一般使用OfflineAudioContext上下文,接着设置特殊的振荡器来作为音频源,在经过压缩节点操作之后,就可以使用getChannelData来获取生成的AudioBuffer了,指纹即是通过对该Buffer的计算完成的。
let audioContext = new (window.OfflineAudioContext ||window.webkitOfflineAudioContext)(1, 44100, 44100);let outputValue;if (!audioContext) {outputValue = 0;} else {let oscillator = audioContext.createOscillator();oscillator.type = "triangle";oscillator.frequency.value = 10000;let compressor = audioContext.createDynamicsCompressor();if (compressor.threshold) compressor.threshold.value = -50;if (compressor.knee) compressor.knee.value = 40;if (compressor.ratio) compressor.ratio.value = 12;if (compressor.reduction) compressor.reduction.value = -20;if (compressor.attack) compressor.attack.value = 0;if (compressor.release) compressor.release.value = 0.25;oscillator.connect(compressor);compressor.connect(audioContext.destination);oscillator.start(0);audioContext.startRendering();audioContext.oncomplete = function(event) {outputValue = 0;let renderedBuffer = event.renderedBuffer.getChannelData(0);let bufferAsString = '';for (let i = 0; i < renderedBuffer.length; i++) {bufferAsString += renderedBuffer[i].toString();}let fullBufferHash = hash(bufferAsString);console.log('Full buffer hash: ' + fullBufferHash);for (let i = 4500; i < 5000; i++) {outputValue += Math.abs(renderedBuffer[i]);}console.log('Output value: ' + outputValue);compressor.disconnect();};}
前两行代码创建了一个OfflineAudioContext实例。使用离线音频上下文允许程序处理和渲染音频数据而不实时播放。使用window.OfflineAudioContext或window.webkitOfflineAudioContext确保代码兼容不同的浏览器。使用createOscillator 创建一个振荡器,用于生成音频信号。使用createDynamicsCompressor 创建一个动态压缩器节点,该节点用于减少音频信号的动态范围。然后将振荡器连接到压缩器,压缩器再连接到音频上下文的输出
当渲染完成后,oncomplete 定义的函数被调用,getChannelData用于获取渲染后的音频数据,接着将音频数据转换为字符串,并计算其哈希值。代码最后计算了特定样本范围内的数据的绝对值之和,这个数是音频指纹常用的指纹数字。
WebAudio指纹获取
本节会使用JavaScript脚本编写一个获取WebAudio音频信息的脚本。这段代码利用JavaScript的WebAudio API来生成和处理音频数据,最终目的是生成一个音频哈希值和一个从特定样本范围内计算得到的输出值。以下是具体代码:
从4.3.2的代码中可以看出,音频指纹的修改点很多,但是最终是使用getChannelData来获取渲染后的音频数据的,因此可以选择该函数作为音频指纹修改点。对其中的音频数组进行遍历噪声,从而影响整个音频。
WebAudio相关的Chromium源码位于“src\third_party\blink\renderer\modules\webaudio”目录之中,重点要修改的是AudioBuffer相关的,因此选择其中的audio_buffer.cc文件作为指纹定制的文件。
getChannelData有两个重载,具体代码如下:
NotShared<DOMFloat32Array> AudioBuffer::getChannelData(unsigned channel_index,ExceptionState& exception_state) {if (channel_index >= channels_.size()) {exception_state.ThrowDOMException(DOMExceptionCode::kIndexSizeError,"channel index (" + String::Number(channel_index) +") exceeds number of channels (" +String::Number(channels_.size()) + ")");return NotShared<DOMFloat32Array>(nullptr);}return getChannelData(channel_index);}NotShared<DOMFloat32Array> AudioBuffer::getChannelData(unsigned channel_index) {if (channel_index >= channels_.size()) {return NotShared<DOMFloat32Array>(nullptr);}return NotShared<DOMFloat32Array>(channels_[channel_index].Get());}
从JavaScript代码获取音频指纹的时候,只传递了一个参数,因此选择第二个函数作为切入函数。该函数用于获取音频缓冲区中特定通道的数据。其中的函数签名如下:
- NotShared<DOMFloat32Array>:返回类型是 NotShared 包装的 DOMFloat32Array 对象。NotShared 是一种智能指针,表示该对象不应与其他对象共享。
- AudioBuffer:::表明该函数是 AudioBuffer 类的成员。
- getChannelData:接收一个无符号整数参数 channel_index,表示要获取的数据通道索引。
然后检查传入的通道索引 channel_index 是否超出了音频通道的数量。如果超出了有效范围,表示请求的通道不存在。否则将获取到的通道数据包装成 DOMFloat32Array对象并返回。
由此可见,可以在最后的通道数据正式返回之前,将里边的数据进行遍历,挨个进行微调之后,从而完成音频指纹定制:
//ruyiconst base::CommandLine* ruyi_command_line =base::CommandLine::ForCurrentProcess();if (ruyi_command_line->HasSwitch(blink::switches::kRuyi)) {const std::string ruyi_fp =ruyi_command_line->GetSwitchValueASCII(blink::switches::kRuyi);absl::optional<base::Value> json_reader =base::JSONReader::Read(ruyi_fp);double webaudio_data =*(json_reader->GetDict().FindDouble("webaudio"));DOMFloat32Array* channels__ = channels_[channel_index].Get();size_t channel_size = channels__->length();for (size_t i = 0; i < channel_size; i++) {channels__->Data()[i] += (0.00001 * webaudio_data);}return NotShared<DOMFloat32Array>(channels__);}//ruyi end
修改的代码从 JSON 中读取一个名为 webaudio 的双精度浮点数值,为了防止音频数据修改过大,这里对音频数据进行遍历的时候,将该值按一定比例加到指定通道的音频数据样本上。
在修改完毕之后,可以到音频检测网站audiofingerprint.openwpm.com进行测试,传递不同参数,可以得到不同的指纹信息,如图4-8所示: