因为您正在获取带有WAV数据的缓冲区,所以您可以使用wav-decoder
库来解析它,然后将其提供给pitchfinder
库以获取音频的频率。
const Pitchfinder = require('pitchfinder')
const WavDecoder = require('wav-decoder')
const detectPitch = new Pitchfinder.YIN()
const frequency = detectPitch(WavDecoder.decode(data).channelData[0])
然而,由于您正在使用Electron,您也可以直接在Chromium中使用MediaStream Recording API。
首先,这仅适用于Electron 1.7+,因为它使用了Chromium 58,这是Chromium的第一个包含修复了导致 AudioContext
无法从MediaRecorder
解码音频数据的错误的版本。
另外,为了实现此代码,我将使用ES7 async
和await
语法,这应该在Node.js 7.6+和Electron 1.7+上正常运行。
所以假设您的Electron的index.html
如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Frequency Finder</title>
</head>
<body>
<h1>Tuner</h1>
<div><label for="devices">Device:</label> <select id="devices"></select></div>
<div>Pitch: <span id="pitch"></span></div>
<div>Frequency: <span id="frequency"></span></div>
<div><button id="record" disabled>Record</button></div>
</body>
<script>
require('./renderer.js')
</script>
</html>
现在让我们开始处理renderer
脚本。首先,让我们设置一些将要使用的变量:
const audioContext = new AudioContext()
const devicesSelect = document.querySelector('#devices')
const pitchText = document.querySelector('#pitch')
const frequencyText = document.querySelector('#frequency')
const recordButton = document.querySelector('#record')
let audioProcessor, mediaRecorder, sourceStream, recording
好的,现在进入代码的其余部分。首先,让我们用所有可用的音频输入设备填充Electron窗口中的下拉式菜单<select>
。
navigator.mediaDevices.enumerateDevices().then(devices => {
const fragment = document.createDocumentFragment()
devices.forEach(device => {
if (device.kind === 'audioinput') {
const option = document.createElement('option')
option.textContent = device.label
option.value = device.deviceId
fragment.appendChild(option)
}
})
devicesSelect.appendChild(fragment)
devicesSelect.dispatchEvent(new Event('change'))
})
在最后,你会注意到我们调用了一个在Electron窗口中设置的<select>
元素上的事件。但是,请稍等,我们从未编写过该事件处理程序!让我们在刚才编写的代码之前添加一些代码:
devicesSelect.addEventListener('change', async e => {
if (e.target.value) {
if (recording) {
stop()
}
sourceStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {
exact: e.target.value
}
}
})
recordButton.disabled = !sourceStream
}
})
让我们实际编写一个处理程序来处理录制按钮,因为此时它什么也不做:
recordButton.addEventListener('click', () => {
if (recording) {
stop()
} else {
record()
}
})
现在我们展示音频设备,让用户选择它们,并拥有一个录音按钮……但我们仍有未实现的功能 -
record()
和
stop()
。
让我们停下来做一个架构决策。
我们可以在
renderer.js
中记录音频,抓取音频数据,并分析它以获得其音高。然而,分析音高的数据是一项昂贵的操作。因此,最好能够运行该操作的能力在进程外运行。
幸运的是,Electron 1.7带来了支持具有节点上下文的Web Worker。创建一个Web Worker将允许我们在不同的进程中运行昂贵的操作,因此它不会在运行时阻止主进程(和UI)。
因此,在考虑到这一点的同时,让我们假设我们将在
audio-processor.js
中创建一个Web Worker。我们稍后会进行实现,但我们将假设它接受一个对象
{sampleRate, audioData}
的消息,其中
sampleRate
是采样率,
audioData
是我们将传递给
pitchfinder
的
Float32Array
。
让我们还假设:
- 如果录制的处理成功,工作者将返回一个对象
{frequency, key, octave}
的消息 - 例如{frequency: 440.0, key:'A',octave:4}
。
- 如果录制的处理失败,工作者将返回一个带有
null
的消息。
让我们编写我们的
record
函数:
function record () {
recording = true
recordButton.textContent = 'Stop recording'
if (!audioProcessor) {
audioProcessor = new Worker('audio-processor.js')
audioProcessor.onmessage = e => {
if (recording) {
if (e.data) {
pitchText.textContent = e.data.key + e.data.octave.toString()
frequencyText.textContent = e.data.frequency.toFixed(2) + 'Hz'
} else {
pitchText.textContent = 'Unknown'
frequencyText.textContent = ''
}
}
}
}
mediaRecorder = new MediaRecorder(sourceStream)
mediaRecorder.ondataavailable = async e => {
if (e.data.size !== 0) {
const response = await fetch(URL.createObjectURL(data))
const arrayBuffer = await response.arrayBuffer()
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
const audioData = audioBuffer.getChannelData(0)
audioProcessor.postMessage({
sampleRate: audioBuffer.sampleRate,
audioData
})
}
}
mediaRecorder.start()
}
一旦我们开始使用 MediaRecorder
录制,直到停止录制,我们才会收到 ondataavailable
处理程序的调用。这是编写 stop
函数的良好时机。
function stop () {
recording = false
mediaRecorder.stop()
recordButton.textContent = 'Record'
}
现在我们只需要在
audio-processor.js
中创建我们的工作者即可。让我们继续并创建它。
const Pitchfinder = require('pitchfinder')
const keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
function analyseAudioData ({sampleRate, audioData}) {
const detectPitch = Pitchfinder.YIN({sampleRate})
const frequency = detectPitch(audioData)
if (frequency === null) {
return null
}
const c0 = 440.0 * Math.pow(2.0, -4.75)
const halfStepsBelowMiddleC = Math.round(12.0 * Math.log2(frequency / c0))
const octave = Math.floor(halfStepsBelowMiddleC / 12.0)
const key = keys[Math.floor(halfStepsBelowMiddleC % 12)]
return {frequency, key, octave}
}
onmessage = e => {
postMessage(analyseAudioData(e.data))
}
现在,如果你把所有内容都运行起来……
它不能正常工作!为什么呢?
我们需要更新
main.js
(或者您的主要脚本名称)这样当主电子窗口被创建时,Electron会告诉我们提供Node支持在网页工作者的上下文中。否则,那个
require('pitchfinder')
并不能做到我们想要的。
这很简单,我们只需要在窗口的
webPreferences
对象中添加
nodeIntegrationInWorker: true
即可。例如:
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegrationInWorker: true
}
})
现在,如果你运行你所编写的代码,你将得到一个简单的Electron应用程序,它可以让你录制一小段音频,测试其音高,并将该音高显示在屏幕上。这对于小片段的音频效果最佳,因为音频越长,处理所需时间越长。
如果你想要更完整的例子,比如能够实时监听和返回音高而不是让用户一直点击录制和停止,可以看一下我制作的
electron-tuner app。随意查看源代码,以了解如何操作 - 我已经尽力确保其有良好的注释。
这是它的截图:
![Screenshot of electron-tuner](https://istack.dev59.com/tFYTf.webp)
希望这些内容能够对你的努力有所帮助。