2016-12-15 2 views
2

В настоящее время я разрабатываю электронное приложение, которое, я надеюсь, сможет измерить шаг ввода гитары на рабочий стол.Pitch detection - Node.js

Моя первоначальная идея - один тон за раз, поэтому, пожалуйста, дайте мне знать, подходит ли FTT.

Edit: в комментарии, кажется, что FTT не велик, так что я рассматривает возможность использования гармонического спектра продуктов, например

Я не слишком много опыта работы с Node.js, но до сих пор я ve удалось разбить сломанный пакет 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] */ 
}); 

Хорошо, поэтому я получаю массив данных, который, кажется, является хорошим началом. Я знаю, что я должен как-то применять алгоритм определения высоты тона, чтобы начать анализ высоты тона

Я иду в правильном направлении? В каком формате должны находиться эти данные? Как я могу использовать эти данные для определения высоты тона?

+0

Да! Мне нравится идея btw, я надеюсь, что вы сделаете ее тюнер;) Всегда нужен гитарный тюнер с командной строкой. – theonlygusti

+1

Использование FFT - отличный способ измерить [pitch] (https://en.wikipedia.org/wiki/Pitch_ (музыка)), особенно если вы хотите получить достаточную точность для тюнера. Есть гораздо лучшие алгоритмы определения высоты тона (https://en.wikipedia.org/wiki/Pitch_detection_algorithm), например. [Гармонический спектр продукта] (http://musicweb.ucsd.edu/%7Etrsmyth/analysis/Harmonic_Product_Spectrum.html). Примечание: есть тонкое, но важное различие между * pitch * и * частотой * - вы хотите измерить музыкальную высоту, а не частоту. –

+0

@PaulR вы могли бы рассказать о различии? Я думал, что частота волны определила высоту тона. Https://en.wikipedia.org/wiki/Scientific_pitch_notation в таблице: «В таблице ниже приведены обозначения для смол на основе стандартных частот клавиш фортепиано», поэтому я получил немного запутано – Alvaro

ответ

3

Поскольку вы получаете буфер с данными 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, вы можете просто использовать MediaStream записи API в Chromium.

Прежде всего, это будет работать только с Electron 1.7+, поскольку использует Chromium 58, первую версию Chromium, чтобы включить исправление для a bug which prevented the AudioContext from decoding audio data from the MediaRecorder.

Кроме того, для целей настоящего кода я буду использовать синтаксис ES7 async и await, который должен отлично работать на Node.js 7.6+ и Electron 1.7+.

Итак, давайте предположим, ваш index.html для Electron выглядит следующим образом:

<!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> элемент в окне Electron. Но, держись, мы никогда не писали этого обработчика событий! Давайте добавим некоторый код выше код, который мы только что написали:

// 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 обеспечивает поддержку веб-работников с контекстом узла. Создание веб-рабочего позволит нам запускать дорогостоящую операцию в другом процессе, поэтому она не блокирует основной процесс (и пользовательский интерфейс) во время его работы.

Помня об этом, предположим, что мы создадим веб-работника в audio-processor.js. Мы вернемся к реализации позже, но предположим, что оно принимает сообщение с объектом, {sampleRate, audioData}, где sampleRate - это частота дискретизации, а audioData - это номер Float32Array, который мы перейдем к pitchfinder.

Давайте также предположим, что:

  • Если обработка записи удалось, работник возвращает сообщение с объектом {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) { 
     // 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. Давайте продолжим и создадим его.

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 (или как бы там ни было имя вашего основного сценария), чтобы при создании основного электронного окна электронному устройству предлагалось поддерживать поддержку узла в контексте веб-рабочего.В противном случае require('pitchfinder') не делает того, что мы хотим.

Это просто, нам просто нужно добавить nodeIntegrationInWorker: true в окно webPreferences. Например:

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

Теперь, если вы бежите, что вы собрали, вы получите простое приложение Electron, что позволяет записать небольшой фрагмент аудио, проверить его высоту, а затем отобразить этот шаг к экран.

Это будет лучше всего работать с небольшими фрагментами аудио, поскольку чем дольше звук, тем дольше он обрабатывается.

Если вы хотите получить более подробный пример, который будет более подробным, например, возможность прослушивания и возврата звука в реальном времени, вместо того, чтобы записывать запись пользователя и останавливаться все время, посмотрите на electron-tuner app, что у меня есть сделал. Не стесняйтесь просматривать источник, чтобы узнать, как все сделано. Я сделал все возможное, чтобы убедиться, что он хорошо прокомментирован.

Вот скриншот из него:

Screenshot of electron-tuner

Надеюсь, все это поможет вам в ваших усилиях.

+1

разве вы не знаете этого, человек, над которым я работаю, решил, что они хотят, чтобы их визуализатор был символом частоты, а не «как гитарный тюнер». Таким образом, я иду гораздо более доступным маршрутом FFT. Тем не менее, вы сделали замечательную работу, ответив на это, и спасибо вам большое! –

+0

Рад, что я мог хоть как-то помочь. :) –

+1

Просто сообщите вам, что я создал узел-аддон для репозитория pitchfinder (названный узловым указателем). Это намного быстрее (в моем проекте я запускаю обнаружение в режиме реального времени на нескольких каналах, поэтому мне нужно, чтобы он был быстрее). Я также реализовал алгоритм MacLeod, который лучше работает для инструментов (по крайней мере, я так думаю для гитары) –