gpt4 book ai didi

javascript - 音高检测 - Node.js

转载 作者:搜寻专家 更新时间:2023-10-31 22:40:40 28 4
gpt4 key购买 nike

我目前正在开发一个电子应用程序,我希望它能够在桌面上测量吉他输入的音高。

我最初的想法是一次一个音调,所以请告诉我 FTT 是否合适。

编辑:根据评论,FTT 似乎不是很好,所以我正在考虑使用谐波产品频谱例如

我对 node.js 没有太多经验,但到目前为止我已经成功地 fork 了损坏的 microphone打包并稍微调整一下,以便能够获取 wav格式数据来自 sox .

这是生成进程并获取数据的实际代码(简化后,它实际上有一个 startCapture 方法来生成记录进程):

const spawn = require('child_process').spawn;
const PassThrough = require('stream').PassThrough;

const audio = new PassThrough;
const info = new PassThrough;

const recordingProcess = spawn('sox', ['-d', '-t', 'wav', '-p'])
recordingProcess.stdout.pipe(audio);
recordingProcess.stderr.pipe(info);

在另一个 js 文件中,我监听数据事件:
mic.startCapture({format: 'wav'});
mic.audioStream.on('data', function(data) {
/* data is Uint8Array[8192] */
});

好的,所以我得到了一系列数据,这似乎是一个好的开始。
我知道我应该以某种方式应用音高检测算法来开始音高分析

我是否朝着正确的方向前进?这个数据应该是什么格式?
如何使用此数据进行音高检测?

最佳答案

由于您获得的是带有 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 的第一个版本包含 a bug which prevented the AudioContext from decoding audio data from the MediaRecorder 的修复程序.

此外,出于此代码的目的,我将使用 ES7 asyncawait语法,它应该在 Node.js 7.6+ 和 Electron 1.7+ 上运行得很好。

所以让我们假设您的 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

好的,现在进入其余的代码。首先,让我们填充 <select>在带有所有可用音频输入设备的 Electron 窗口中下拉。

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)

// Run the event listener on the `<select>` element after the input devices
// have been populated. This way the record button won't remain disabled at
// start.
devicesSelect.dispatchEvent(new Event('change'))
})

最后你会注意到,我们调用了一个我们在 <select> 上设置的事件。电子窗口中的元素。但是,等等,我们从来没有写过那个事件处理程序!让我们添加一些代码 以上 我们刚刚写的代码:

// Runs whenever a different audio input device is selected by the user.
devicesSelect.addEventListener('change', async e => {
if (e.target.value) {
if (recording) {
stop()
}

// Retrieve the MediaStream for the selected audio input device.
sourceStream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: {
exact: e.target.value
}
}
})

// Enable the record button if we have obtained a MediaStream.
recordButton.disabled = !sourceStream
}
})

让我们实际上也为记录按钮编写一个处理程序,因为此时它什么都不做:

// Runs when the user clicks the record button.
recordButton.addEventListener('click', () => {
if (recording) {
stop()
} else {
record()
}
})

现在我们显示音频设备,让用户选择它们,并有一个录制按钮......但我们仍然有未实现的功能 - record()stop() .

让我们在这里停下来做出架构决策。

我们可以记录音频,抓取音频数据,并对其进行分析以获取其音高,所有这些都在 renderer.js 中。 .然而,分析音高数据是一项昂贵的操作。因此,最好能够在进程外运行该操作。

幸运的是,Electron 1.7 为具有 Node 上下文的 Web Worker 提供了支持。创建 Web Worker 将允许我们在不同的进程中运行昂贵的操作,因此它在运行时不会阻塞主进程(和 UI)。

因此,请记住这一点,让我们假设我们将在 audio-processor.js 中创建一个 Web Worker。 .我们稍后会讨论实现,但我们假设它接受一个带有对象的消息, {sampleRate, audioData} ,其中 sampleRate是采样率, audioDataFloat32Array我们将传递给 pitchfinder .

我们还假设:
  • 如果录音处理成功,worker 会返回一个带有对象 {frequency, key, octave} 的消息。 - 一个例子是 {frequency: 440.0, key: 'A', octave: 4} .
  • 如果录音处理失败,worker 会返回一条消息 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) {
    // Load the blob.
    const response = await fetch(URL.createObjectURL(data))
    const arrayBuffer = await response.arrayBuffer()
    // Decode the audio.
    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer)
    const audioData = audioBuffer.getChannelData(0)
    // Send the audio data to the audio processing worker.
    audioProcessor.postMessage({
    sampleRate: audioBuffer.sampleRate,
    audioData
    })
    }
    }

    mediaRecorder.start()
    }

    一旦我们开始录制 MediaRecorder ,我们不会得到我们的 ondataavailable处理程序调用直到记录停止。这是写我们的 stop的好时机功能。

    function stop () {
    recording = false
    mediaRecorder.stop()
    recordButton.textContent = 'Record'
    }

    现在剩下的就是在 audio-processor.js 中创建我们的 worker .让我们继续创建它。

    const Pitchfinder = require('pitchfinder')

    // Conversion to pitch from frequency based on technique used at
    // https://www.johndcook.com/music_hertz_bark.html

    // Lookup array for note names.
    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
    }

    // Convert the frequency to a musical pitch.

    // c = 440.0(2^-4.75)
    const c0 = 440.0 * Math.pow(2.0, -4.75)
    // h = round(12log2(f / c))
    const halfStepsBelowMiddleC = Math.round(12.0 * Math.log2(frequency / c0))
    // o = floor(h / 12)
    const octave = Math.floor(halfStepsBelowMiddleC / 12.0)
    const key = keys[Math.floor(halfStepsBelowMiddleC % 12)]

    return {frequency, key, octave}
    }

    // Analyse data sent to the worker.
    onmessage = e => {
    postMessage(analyseAudioData(e.data))
    }

    现在,如果你一起运行这一切... 它不会工作! 为什么?

    我们需要更新 main.js (或者不管你的主脚本的名字是什么),这样当主 Electron 窗口被创建时,Electron 被告知在 web worker 的上下文中提供 Node 支持。否则,该 require('pitchfinder')不做我们想让它做的事情。

    这个很简单,我们只需要添加 nodeIntegrationInWorker: true在窗口的 webPreferences目的。例如:

    mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
    nodeIntegrationInWorker: true
    }
    })

    现在,如果你运行你已经组合在一起的东西,你会得到一个简单的 Electron 应用程序,它可以让你录制一小段音频,测试它的音高,然后将该音高显示在屏幕上。

    这对小片段音频最有效,因为音频越长,处理所需的时间就越长。

    如果您想要一个更完整、更深入的示例,例如能够实时收听和返回音高,而不是让用户点击记录并一直停止,请查看 electron-tuner app我做的。随意查看源代码以了解事情是如何完成的 - 我已尽力确保它得到了很好的评论。

    这是它的屏幕截图:

    Screenshot of electron-tuner

    希望所有这些都有助于您的努力。

    关于javascript - 音高检测 - Node.js,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41174545/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com