- PVSM.RU - https://www.pvsm.ru -
В прошлой статье [1] мы немного познакомились с возможностями Audio API и написали простенький визуализатор сигнала. Теперь настало время копнуть поглубже и распробовать новые фишки API. Но нам нужна цель, к которой мы будем стремиться, и в данном случае нашей целью будет как следует поиздеваться над входящим сигналом и его характеристиками. Другими словами, мы напишем маленький вокодер [2].
Так как итоговый код получился довольно-таки большим, то в статье будет рассмотрены наиболее важные и интересные с точки зрения Audio API фрагменты. Итоговый результат вы конечно же сможете посмотреть на демке [3].
Итак Audio API поддерживает три вида источника сигнала:
В демо-примере [3] реализованы все три вида источника, а также возможность переключения между ними. Мы же рассмотрим, пожалуй, самый интересный из них, а именно внешний аудио поток с микрофона.
Для того чтобы достучаться до нашего источника, нам для начала нужно получить для этого разрешение пользователя и захватить audio — поток. И что же вы думаете, нам не придется городить тонны кода для этого, а всего лишь использовать одну функцию под названием getUserMedia. Эта волшебная ф-я принимает три аргумента:
{video: true, audio: true}
Итого, с учетом различных спецификаций браузеров наша ф-я инициализации будет выглядеть вот так:
var context = null, dest = null, source = null;
var init = function () {
try {
var audioContext = w.audioContext || w.webkitAudioContext;
navigator.getMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
//Создаем контекст
context = new audioContext();
//выход по умолчанию
dest = context.destination;
var bufferLoader = new BufferLoader(context, ["effects/reverb.wav"], function (buffers) {
navigator.getMedia({ audio: true }, function (striam) {
//Создаем интерфейс для получения данных из потока
source = context.createMediaStreamSource(striam);
}, function (e) {
alert(e);
});
});
bufferLoader.load();
} catch (e) {
alert (e.message);
}
};
Рассмотрим, что же тут происходит. В начале мы создаем audioContext для нашей страницы (что это такое описано в предыдущей статье [1]), далее мы видим новую функцию BufferLoader. Она занимается тем, что при помощи XHR2 подтягивает внешние аудио-файлы и бережно складирует их в буфер. В нашем случае она нам потребуется, чтобы подтянуть один аудио эффект, который будет описан ниже. Функция эта не стандартная и нам придется её написать.
//Подгрузка файлов в буффер
var BufferLoader = function (context, urlList, callback) {
this.context = context;
this.urlList = urlList;
this.onload = callback;
this.bufferList = new Array();
this.loadCount = 0;
};
BufferLoader.prototype.load = function () {
for (var i = 0; i < this.urlList.length; ++i) {
this.loadBuffer(this.urlList[i], i);
}
};
BufferLoader.prototype.loadBuffer = function (url, index) {
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
var loader = this;
request.onload = function () {
loader.context.decodeAudioData(
request.response,
function (buffer) {
if (!buffer) {
alert('error decoding file data: ' + url);
return;
}
loader.bufferList[index] = buffer;
if (++loader.loadCount == loader.urlList.length) {
loader.onload(loader.bufferList);
}
},
function (error) {
console.error('decodeAudioData error', error);
}
);
}
request.onerror = function () {
alert('BufferLoader: XHR error');
}
request.send();
};
После подгрузки эффекта, мы захватываем аудио поток и, если пользователь нам это позволил, то нам необходимо будет связать захваченный сигнал с нашим audio — контекстом. Для этого мы воспользуемся ф-й createMediaStreamSource. Теперь наш входной сигнал в полном нашем распоряжении и, уж поверьте, мы знатно над ним поиздеваемся.
Пришло время нам написать функцию, которая будет безжалостно издеваться над входным потоком. Рассмотрим методы, которыми мы будем пользоваться:
Воспользуемся этими методами и накидаем нашу ф-ю преобразования:
var AudioModulation = function (buffers, source) {
var am = this;
//Общий усилитель
var sourceGain = context.createGain();
sourceGain.gain.value = 2;
//Свертка сигнала с дорожкой для создания эффекта реверберации
var sourceConvolver = context.createConvolver();
sourceConvolver.buffer = buffers[0];
//Добавляем компрессор частот
var sourceCompressor = context.createDynamicsCompressor();
sourceCompressor.threshold.value = -18.2;
sourceCompressor.ratio.value = 4;
//Соединяем все модули вместе
source.connect(sourceGain);
sourceGain.connect(sourceConvolver);
sourceConvolver.connect(sourceCompressor);
//Подключаем на выход
sourceCompressor.connect(dest);
}
Все значения, такие как громкость или значение, с которого начинается компрессия, можно привязать к пользовательскому интерфейсу, для изменения в реальном времени, как это сделано в демке. В результате пропускания сигнала через нашу функцию мы получим на выходе немного усиленную версию с эффектом эха (как если бы вы говорили с ведром на голове или в шлеме). Но принципиально нового звучания мы на выходе пока не получим, а это значит, что мы движемся дальше. Следующим шагом мы попробуем реализовать эффект кольцевой модуляции.
Кольцевая модуляция — это аудио эффект, который был очень популярен в «лохматые» годы и применялся для создания голосов всякого рода монстров и роботов. Суть этого эффекта в том, что у нас есть два сигнала, один называется несущим (carrier) и является синтезированным сигналом произвольной частоты, а второй это модулирующий сигнал, и эти сигналы перемножаются. В итоге мы получаем новый сигнал с искажениями и металлическими нотками. Для реализации этого чуда рассмотрим следующие методы:
Ну что же, этого нам вполне хватит, чтобы реализовать задуманное. В итоге ф-я AudioModulation преобразится в:
var AudioModulation = function (buffers, source) {
var am = this;
//Общий усилитель
var sourceGain = context.createGain();
sourceGain.gain.value = 2;
//Свертка сигнала с дорожкой для создания эффекта реверберации
var sourceConvolver = context.createConvolver();
sourceConvolver.buffer = buffers[0];
//Добавляем компрессор частот
var sourceCompressor = context.createDynamicsCompressor();
sourceCompressor.threshold.value = -18.2;
sourceCompressor.ratio.value = 4;
//Соединяем все модули вместе
source.connect(sourceGain);
sourceGain.connect(sourceConvolver);
sourceConvolver.connect(sourceCompressor);
//Кольцевая модуляция
var ringGain = this.ringModulation();
sourceCompressor.connect(ringGain);
//Подключаем на выход
ringGain.connect(dest);
}
AudioModulation.prototype.ringModulation = function () {
//Усилитель, несущего сигнала
var ringGain = context.createGain();
ringGain.gain.value = 1;
//Несущий сигнал
ringCarrier = context.createOscillator();
//Создаем синусоидальный сигнал с частой 40Гц
ringCarrier.type = ringCarrier.SINE;
ringCarrier.frequency.value = 40;
//На пол октавы вверх
ringCarrier.detune.value = 600;
//Создаем фильтр, который обрезает все что ниже 10Гц
var ngHigpass = context.createBiquadFilter();
ngHigpass.type = ngHigpass.HIGHPASS;
ngHigpass.frequency.value = 10;
//Применяем фильтр к созданному сигналу, а так же цепляем к нему усилитель
ringCarrier.connect(ngHigpass);
ngHigpass.connect(ringGain.gain);
return ringGain;
};
Ну вот, совсем другое дело, после всего это мы получим уже довольно-таки замаскированный «роботизированный сигнал», но, как говорится, хорошего много не бывает, и поэтому мы добавим ко всему этому великолепию эквалайзер для ручной настройки различных частот, а реализовывать мы его будет при помощи уже знакомой нам ф-и createBiquadFilter с типом highshelf.
Для начала создадим массив с настройками, по которым будем строить фильтры:
var filters = [{gain: 1,frequency: 40},{gain: 3,frequency: 120},....,{gain: -2,frequency: 16000}];
Параметрами в нем является уровень усиления и частота. Теперь ф-я, которая создает фильтры:
AudioModulation.prototype.setFilters = function (source) {
var fil = [{ gain: 1, frequency: 40 }, { gain: 3, frequency: 120 }, { gain: -2, frequency: 16000}],
out = null, ln = fil.length;
for (var i = 0; i < ln; i++) {
var loc = fil[i],
currFilter = null;
currFilter = context.createBiquadFilter();
currFilter.type = currFilter.HIGHSHELF;
currFilter.gain.value = loc.gain;
currFilter.Q.value = 1;
currFilter.frequency.value = loc.frequency;
if (!out) {
source.connect(currFilter);
out = currFilter;
} else {
out.connect(currFilter);
out = currFilter;
}
}
return out;
};
В результате ф-я преобразования примет вид:
var AudioModulation = function (buffers, source) {
var am = this;
//Общий усилитель
var sourceGain = context.createGain();
sourceGain.gain.value = 2;
//Свертка сигнала с дорожкой для создания эффекта реверберации
var sourceConvolver = context.createConvolver();
sourceConvolver.buffer = buffers[0];
//Добавляем компрессор частот
var sourceCompressor = context.createDynamicsCompressor();
sourceCompressor.threshold.value = -18.2;
sourceCompressor.ratio.value = 4;
//Соединяем все модули вместе
source.connect(sourceGain);
sourceGain.connect(sourceConvolver);
sourceConvolver.connect(sourceCompressor);
//Кольцевая модуляция
var ringGain = this.ringModulation();
sourceCompressor.connect(ringGain);
//Подключаем фильтры
var outFilters = this.setFilters(sourceCompressor);
//Подключаем на выход
outFilters.connect(dest);
}
//Кольцевая модуляция
AudioModulation.prototype.ringModulation = function () {
//Усилитель, несущего сигнала
var ringGain = context.createGain();
ringGain.gain.value = 1;
//Несущий сигнал
ringCarrier = context.createOscillator();
//Создаем синусоидальный сигнал с частой 40Гц
ringCarrier.type = ringCarrier.SINE;
ringCarrier.frequency.value = 40;
//На пол октавы вверх
ringCarrier.detune.value = 600;
//Создаем фильтр, который обрезает все что ниже 10Гц
var ngHigpass = context.createBiquadFilter();
ngHigpass.type = ngHigpass.HIGHPASS;
ngHigpass.frequency.value = 10;
//Применяем фильтр к созданному сигналу, а так же цепляем к нему усилитель
ringCarrier.connect(ngHigpass);
ngHigpass.connect(ringGain.gain);
return ringGain;
};
//Фильтрация
AudioModulation.prototype.setFilters = function (source) {
var fil = [{ gain: 1, frequency: 40 }, { gain: 3, frequency: 120 }, { gain: -2, frequency: 16000}],
out = null, ln = fil.length;
while (ln--) {
var loc = fil[ln],
currFilter = null;
currFilter = context.createBiquadFilter();
currFilter.type = currFilter.HIGHSHELF;
currFilter.gain.value = loc.gain;
currFilter.Q.value = 1;
currFilter.frequency.value = loc.frequency;
if (!out) {
source.connect(currFilter);
out = currFilter;
} else {
out.connect(currFilter);
out = currFilter;
}
}
return out;
};
Ну вот теперь мы имеем полноценный эквалайзер и можем усиливать или ослаблять любую частоту в сигнале. И если бы мы были «лалками», то остановились бы на достигнутом и со спокойной совестью мучили микрофон играясь с настройками, но мы же хотим большего. И вот тут мы, так сказать, добавим вишенку на торт — мы попробуем реализовать эффект под названием pitch shifter.
Суть эффекта заключается в том, что к сигналу добавляется его копия, которая отстает от основного тона на любой интервал в пределах двух октав вверх или вниз. Это очень модный эффект и его реализация чертовски сложна, так что мы сделаем, так сказать, упрощенную версию.
Для того, чтобы начать работу над этим эффектом, нам будет необходим интерфейс, который бы позволял получать данные сигнала, которые мы могли бы изменять.
Для его создания мы воспользуемся Дискретным преобразованием Фурье [10] (а если быть точнее, то его разновидностью оконным преобразованием Фурье [11]) и методом, знакомым нам по прошлой статье, createScriptProcessor [12]. Он принимает три параметра: buffer(размер кадра или окна данных, которые выбираются из сигнала в единицу времени), numberOfInputChannels (кол-во входных каналов), numberOfOutputChannels(кол-во выходных каналов). Результатом вызова этого метода и станет создание объекта интерфейса, который нам нужен. Полученный объект имеет свое событие onaudioprocess, которое отрабатывает каждый раз, когда происходит новая выборка данных из сигнала. Итого, преобразование нашего сигнала будет выглядеть следующим образом:
var currentGrainSize = 512
var currentOverLap = 0.50;
var currentShiftRatio = 0.77;
var node = context.createScriptProcessor(currentGrainSize, 1, 1);
//Создаем весовую последовательность конечной длины при помощи ф-и Окна Ханна (Одна из видов оконного преобразования Фурье)
node.grainWindow = hannWindow(currentGrainSize);
//Создаем буфер, который больше нашей выборки в два раза
node.buffer = new Float32Array(currentGrainSize* 2);
node.onaudioprocess = function (event) {
//Входные данные
var input = event.inputBuffer.getChannelData(0);
//Выходные данные
output = event.outputBuffer.getChannelData(0),
ln = input.length;
for (i = 0; i < ln; i++) {
//Применяем оконного преобразования Фурье
input[i] *= this.grainWindow[i];
//Перескакиваем на вторую половину буффера
this.buffer[i] = this.buffer[i + currentGrainSize];
//Обнуляем вторую половину
this.buffer[i + currentGrainSize] = 0.0;
}
// Расчет смещения
var grainData = new Float32Array(currentGrainSize * 2);
for (var i = 0, j = 0.0; i < currentGrainSize; i++, j += currentShiftRatio) {
var index = Math.floor(j) % currentGrainSize;
var a = input[index];
var b = input[(index + 1) % currentGrainSize];
grainData[i] += linearInterpolation(a, b, j % 1.0) * this.grainWindow[i];
}
// Перекрытие
for (i = 0; i < currentGrainSize; i += Math.round(currentGrainSize * (1 - currentOverLap))) {
for (j = 0; j <= currentGrainSize; j++) {
this.buffer[i + j] += grainData[j];
}
}
// Подаем обработанный поток на выход
for (i = 0; i < currentGrainSize; i++) {
output[i] = this.buffer[i];
}
}
Теперь, оперируя параметрами шага и перекрытия, мы можем получить эффект ускорения или замедления произношения. Для расчетов нам необходимо будет реализовать ф-и hannWindow (Ф-я расчета окна Ханна) и linearInterpolation(ф-я линейной интерполяции). Итоговый вариант нашей ф-и преобразования будет следующим:
var AudioModulation = function (buffers, source) {
var am = this, currentGrainSize = 512, currentOverLap = 0.50, currentShiftRatio = 0.77,
node = context.createScriptProcessor(currentGrainSize, 1, 1);
//Общий усилитель
var sourceGain = context.createGain();
sourceGain.gain.value = 2;
//Свертка сигнала с дорожкой для создания эффекта реверберации
var sourceConvolver = context.createConvolver();
sourceConvolver.buffer = buffers[0];
//Добавляем компрессор частот
var sourceCompressor = context.createDynamicsCompressor();
sourceCompressor.threshold.value = -18.2;
sourceCompressor.ratio.value = 4;
//Соединяем все модули вместе
source.connect(sourceGain);
sourceGain.connect(sourceConvolver);
sourceConvolver.connect(sourceCompressor);
//Кольцевая модуляция
var ringGain = this.ringModulation();
sourceCompressor.connect(ringGain);
//Подключаем фильтры
var outFilters = this.setFilters(sourceCompressor);
//Подключаем на выход
outFilters.connect(dest);
//Создаем весовую последовательность конечной длины при помощи ф-и Окна Ханна (Одна из видов оконного преобразования Фурье)
node.grainWindow = this.hannWindow(currentGrainSize);
//Создаем буфер, который больше нашей выборки в два раза
node.buffer = new Float32Array(currentGrainSize* 2);
node.onaudioprocess = function (event) {
//Входные данные
var input = event.inputBuffer.getChannelData(0);
//Выходные данные
output = event.outputBuffer.getChannelData(0),
ln = input.length;
for (i = 0; i < ln; i++) {
//Применяем оконного преобразования Фурье
input[i] *= this.grainWindow[i];
//Перескакиваем на вторую половину буффера
this.buffer[i] = this.buffer[i + currentGrainSize];
//Обнуляем вторую половину
this.buffer[i + currentGrainSize] = 0.0;
}
// Расчет смещения
var grainData = new Float32Array(currentGrainSize * 2);
for (var i = 0, j = 0.0; i < currentGrainSize; i++, j += currentShiftRatio) {
var index = Math.floor(j) % currentGrainSize;
var a = input[index];
var b = input[(index + 1) % currentGrainSize];
grainData[i] += am.linearInterpolation(a, b, j % 1.0) * this.grainWindow[i];
}
// Перекрытие
for (i = 0; i < currentGrainSize; i += Math.round(currentGrainSize * (1 - currentOverLap))) {
for (j = 0; j <= currentGrainSize; j++) {
this.buffer[i + j] += grainData[j];
}
}
// Подаем обработанный поток на выход
for (i = 0; i < currentGrainSize; i++) {
output[i] = this.buffer[i];
}
}
}
AudioModulation.prototype.hannWindow = function (length) {
var window = new Float32Array(length);
for (var i = 0; i < length; i++) {
window[i] = 0.5 * (1 - Math.cos(2 * Math.PI * i / (length - 1)));
}
return window;
};
AudioModulation.prototype.linearInterpolation = function (a, b, t) {
return a + (b - a) * t;
};
//Кольцевая модуляция
AudioModulation.prototype.ringModulation = function () {
//Усилитель, несущего сигнала
var ringGain = context.createGain();
ringGain.gain.value = 1;
//Несущий сигнал
ringCarrier = context.createOscillator();
//Создаем синусоидальный сигнал с частой 40Гц
ringCarrier.type = ringCarrier.SINE;
ringCarrier.frequency.value = 40;
//На пол октавы вверх
ringCarrier.detune.value = 600;
//Создаем фильтр, который обрезает все что ниже 10Гц
var ngHigpass = context.createBiquadFilter();
ngHigpass.type = ngHigpass.HIGHPASS;
ngHigpass.frequency.value = 10;
//Применяем фильтр к созданному сигналу, а так же цепляем к нему усилитель
ringCarrier.connect(ngHigpass);
ngHigpass.connect(ringGain.gain);
return ringGain;
};
//Фильтрация
AudioModulation.prototype.setFilters = function (source) {
var fil = [{ gain: 1, frequency: 40 }, { gain: 3, frequency: 120 }, { gain: -2, frequency: 16000}],
out = null, ln = fil.length;
while (ln--) {
var loc = fil[ln],
currFilter = null;
currFilter = context.createBiquadFilter();
currFilter.type = currFilter.HIGHSHELF;
currFilter.gain.value = loc.gain;
currFilter.Q.value = 1;
currFilter.frequency.value = loc.frequency;
if (!out) {
source.connect(currFilter);
out = currFilter;
} else {
out.connect(currFilter);
out = currFilter;
}
}
return out;
};
Ну вот теперь, с чистой совестью, мы можем насладиться проделанной работой. Конечно, можно не останавливаться на достигнутом и, например, добавить визуализатор спектра, какой-нибудь модный эффект типа Phaser [13], но это уже на ваше усмотрение. Теперь, копнув глубже Audio API, становится понятно, что благодаря тем механизмам, которые сейчас доступны разработчикам, возможно реализовать практически любые эффекты и обработку звуковых сигналов. Вы ограничены только вашим воображением.
Конечный вариант с разными источника сигнала с интерфейсом управления вы можете посмотреть вот тут:
Автор: abaddon65
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/54596
Ссылки в тексте:
[1] прошлой статье: http://habrahabr.ru/post/210422/
[2] вокодер: http://ru.wikipedia.org/wiki/%C2%EE%EA%EE%E4%E5%F0
[3] демке: http://phalcon.demosite.pro/vokoder/
[4] createGain: http://docs.webplatform.org/wiki/apis/webaudio/GainNode
[5] createConvolver: http://docs.webplatform.org/wiki/apis/webaudio/ConvolverNode
[6] createDynamicsCompressor: http://docs.webplatform.org/wiki/apis/webaudio/DynamicsCompressorNode
[7] createOscillator: http://docs.webplatform.org/wiki/apis/webaudio/OscillatorNode
[8] createBiquadFilter: http://docs.webplatform.org/wiki/apis/webaudio/BiquadFilterNode
[9] Добротность: http://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%B1%D1%80%D0%BE%D1%82%D0%BD%D0%BE%D1%81%D1%82%D1%8C
[10] Дискретным преобразованием Фурье: http://www.dsplib.ru/content/dft/dft.html
[11] оконным преобразованием Фурье: http://www.dsplib.ru/content/win/win.html
[12] createScriptProcessor: http://docs.webplatform.org/wiki/apis/webaudio/ScriptProcessorNode
[13] Phaser: http://en.wikipedia.org/wiki/Phaser_(effect)
[14] github: https://github.com/abaddonGIT/captureVoice
[15] docs.webplatform.org/wiki/apis/webaudio: http://docs.webplatform.org/wiki/apis/webaudio
[16] www.w3.org/TR/2011/WD-webaudio-20111215/: http://www.w3.org/TR/2011/WD-webaudio-20111215/
[17] www.studfiles.ru/dir/cat32/subj116/file1543/view2417.html: http://www.studfiles.ru/dir/cat32/subj116/file1543/view2417.html
[18] chimera.labs.oreilly.com/books/1234000001552/ch04.html#s04_2: http://chimera.labs.oreilly.com/books/1234000001552/ch04.html#s04_2
[19] analogiu.ru/6/6-5-2.html: http://analogiu.ru/6/6-5-2.html
[20] www.html5rocks.com/ru/tutorials/getusermedia/intro/: http://www.html5rocks.com/ru/tutorials/getusermedia/intro/
[21] html5.by/blog/audio/: http://html5.by/blog/audio/
[22] sites.google.com/site/mikescoderama/pitch-shifting: https://sites.google.com/site/mikescoderama/pitch-shifting
[23] Источник: http://habrahabr.ru/post/211905/
Нажмите здесь для печати.