- PVSM.RU - https://www.pvsm.ru -

Запись и модификация звука в браузере

Недавно решил for fun сделать сайт, на котором будет происходить запись и модификация звука. А ещё хотелось какой-нибудь соответствующей анимации. Как работать со звуком на С++ или C# я знаю, опыт есть, однако ни разу не делал этого в браузере.
Немного погуглив, выяснилось что не так уж и много возможностей записать звук. Самая широко распространенная — использование Flash. У меня нет опыта во Flash, к тому же весь UI и функционал я хотел сделать на JavaScript + HTML, поэтому нужно было как-то обойтись без Flash или с минимальным его участием. В итоге, я нашел jQuery плагин jRecorder [1] для записи звука, который внутри себя в итоге использует Flash, а точнее ActionScript код. Но так как работа со звуком была обёрнута в JavaScript, то такой вариант мне подошел.
Запись и модификация звука в браузере [2]

Моей задумкой было сделать так, чтобы человек говорил что-нибудь в микрофон, этот звук записывался, и потом воспроизводился уже немного искаженным. Для забавы, хотелось добавить туда ещё какую-нибудь простейшую анимацию. Но, я программист, а не дизайнер, поэтому рисовать Flash или HTML5 ролик совсем не моё. Решил выкрутится более простым спосбом — страничку сайта нарисовал сам, а вот в качестве анимации решил использовать gif. Нагуглил забавного Хомячка, который что-то жуёт, и пришла в голову мысль — пускай он молчит (слушая, как человек что-то говорит в микрофон), а потом «произносит» это. То есть, вырисовалась такая задачка:
— Запись звука
— Искажение звука
— Воспроизведение звука и включение анимации

Ну что ж, работа закипела. Сначала для тестов написал нехитрый JS-код, который переключает gif картинку на статическую картинку Хомячка:

function setPictureHamsterStop()
{
 document.getElementById("switch").src = "2.png";
}

function setPictureHamsterSpeech()
{
 document.getElementById("switch").src = "3.gif";
}

Далее было необходимо встроить код jRecorder в мою страницу, а именно, чтобы во время воспроизведения звука показывался Gif, а во время записи Png. jRecorder встраивает окно Flash в страницу и делает её невидимой.
В свою страничку надо вставить небольшой блок CSS сверху, а в основном body разместить скрипт инициализации с настройками:

$.jRecorder(     
 { 
 host : 'ваш_урл_куда_сохранять_записанный_файл_wav'  ,
 callback_started_recording:     function(){callback_started(); },
 callback_stopped_recording:     function(){callback_stopped(); },
 callback_activityLevel:          function(level){callback_activityLevel(level); },
 callback_activityTime:     function(time){callback_activityTime(time); },
 callback_finished_sending:     function(time){ callback_finished_sending() },
 swf_path : 'jRecorder.swf',
 }
);

Сайт я решил выложить на бесплатном хостинге [3], для чего использовал свой Google Drive account. Как его использовать под хостинг [3] на Хабре уже писали [4]. Там куча ограничений, одно из них не позволяет
мне записывать php-скриптом файл на Google Drive извне. Поэтому сайт может быть только статическим. Но мне это не мешало, так как вся работа происходит «на клиенте».
Далее, я скопировал весь код JS из jReader и первым делом убрал из него обработчики callback-ов, которые мне не нужны. Основными для меня событиями были callback_started, callback_stopped, callback_finished_sending. Callback'и говорят сами за себя. Алгоритм прост:
— После начала записи приходит callback_started, а мы ставим картинку в статику (Хомячок молчит и слушает)
— после остановки записи попадаем в callback_stopped и делаем SendFile
— OnSendFinished показываем gif-анимацию, так как звук начинает воспроизводиться (это уже согласно логике самого jRecorder

Но тут проблема: когда начинать или останавливать запись? Мне не хотелось делать это простой кнопкой, пусть хомяк произносит слова только тогда, когда в микрофон действительно что-то говорили, а не шел простой шум или тишина.
Для этого я решил анализировать уровень звука с микрофона, на счастье, jRecorder бросает callback_activityLevel, в котором передается уровень звука — level. Мне нужно было только придумать алгоритм. И я решил делать так:
— Методом подбора установил оптимальный уровень звука, который можно считать шумом (кстати, позже, покопавшись в ActionScript исходниках jRecorder оказалось, что в нем есть подобное значение и оно равно моему).
— Опять же методом подбора установил пороговую длину записи шума. То есть, завел простой счетчик, который каждый раз увеличивается на 1, если пришел шум. Если этот счетчик больше порогового значения — то останавливаем запись (незачем нам записывать и воспроизводить шум).
— Каждый раз при входе в обработчик callback_activityLevel проверяем является ли данный уровень шумом: если да, то увеличиваем счетчик шумов на 1, а если нет — обнуляем этот счетчик (начнем считать заново).
— Дополнительно устанавливаем Boolean флажок, который ставится в true если за всю запись хотя бы раз был превышен порог шума. Это для того, чтобы не гонять «пустые» записи по сети — бережем траффик.

В итоге, если человек ничего не говорит долгое время и в микрофон не попадает никаких дополнительных шумов, то мы не воспроизводим ничего. В случае раговора (ну или шумов, что тоже бывает ) пишем 30 секунд речи,
либо если человек перестает говорить раньше, наш счетчик порога шума сам остановит запись. После остановки происходит воспроизведение звука:

var SILENCE_LEVEL = 5;
var PEAK_LEVEL = 10;
var MAX_SILENCE_TICKS = 50;
var MICROPHONE_AMPLIFY_LEVEL = 10;
var silenceCounter = 0;
var wasLevelPeak = 0; 
var isRecording = 0;

function callback_started(){
 // Устанавливаем картинку Хомячка статичной - он слушает и молчит.
 setPictureHamsterStop();
 silenceCounter = 0;
 totalTime = 0;
 wasLevelPeak = 0;
 isRecording = 1;  
}

function callback_stopped(){
 silenceCounter = 0;
 isRecording = 0;

 if (wasLevelPeak) {
  // Если было что-то кроме шума, отправляем файл со звуком на сервер.
  // В моей реализации мне это нужно было только чтобы воспроизвести звук.
  wasLevelPeak = 0;
  $.jRecorder.sendData();  
 }
 else {
  $.jRecorder.record(30);
 }
}

function callback_finished_sending(){
 // Показываем GIF картинку, в которой Хомячок начинает говорить.
 var timer = setTimeout('setPictureHamsterSpeech();', 2000);
 var timer = setTimeout('$.jRecorder.record(5);', totalTime * 1000);  
}

function callback_activityLevel(level){
  // Проверяем уровень звука.
  if (level > PEAK_LEVEL && isRecording)
  {
 wasLevelPeak = 1; // Да, есть что-то...
 silenceCounter = 0;
  }
  
  // Считаем "условное" количество сэмплов с шумами.
  if(level < SILENCE_LEVEL && isRecording)
  {
 silenceCounter = silenceCounter + 1;
  }   

  // Если мы насчитали достаточное количество шумов - то останавливаем запись
  // (просто чтобы обнулить её, позже она начнется снова).
  if (silenceCounter == MAX_SILENCE_TICKS && isRecording)
  {
  silenceCounter = 0;
  $.jRecorder.stop();
  }
}

С Java-Script частью записи-воспроизведения разобрались. Теперь встала следующая задача — модификация звука. jRecorder поставляется с исходными кодами на Action Script, но его я не знаю, да и никогда толком с Flash не работал.
Но код ActionScript оказался очень нативно понятным, и я быстро разобрался с логикой записи-воспроизведения звука. Мне нужно было дописать код модификации звука, скомпилировать его в *.swf файл, и подложить вместо существующего jRecorder.swf. Поставил Trial версию Flash, открыл проект AudioRecorderCS4.fla, погуглил код модификации звука, и на моё счастье прямо на официальном сайте Adobe [5] нашел примеры работы со звуком.

Во время записи с микрофона идут пачки сырых байт — сэмплов. В jRecorder написан обработчик звука, который срабатывая по SampleDataEvent добавлял новую пачку байт к общей «куче», чтобы
в итоге получился большой массив байт — записанного звука:

private function onSampleData(event:SampleDataEvent):void
{
 _recordingEvent.time = getTimer() - _difference;
 
 dispatchEvent( _recordingEvent );
 
 // Вот тут добавляется новая пачка байт
 while(event.data.bytesAvailable > 0)
  _buffer.writeFloat(event.data.readFloat());
}

Чтобы сделать звук смешнее, нужно лишь пропустить немного байт, то есть при воспроизведении звук проиграется просто быстрее:

private function onSampleData(event:SampleDataEvent):void
{
 _recordingEvent.time = getTimer() - _difference;
 
 dispatchEvent( _recordingEvent );
 
 /* Ускоряем звук */
 event.data.position = 0;
 while(event.data.bytesAvailable > 0)
 {
  _buffer.writeFloat(event.data.readFloat());
  _buffer.writeFloat(event.data.readFloat());
  if (event.data.bytesAvailable > 0) 
  { 
   event.data.position += 2; // Ну подумаешь, пропустили чуть-чуть
  } 
 }
}

Готово. Ctrl+Enter, компиляция, подмена jRecorder.swf, и получаем рабочий прототип. Немного криворукой графики: сам нарисовал ракету в космосе, «подогнал» gif картинки по размеру, чтобы хомячок «сидел» в ракете
(с помощью редактора Online Image Editor [6])и выложил СИЕ на Google Drive hosting. Открываем сайт, Flash спрашивает разрешение на доступ к микрофону:
Запись и модификация звука в браузере [7]
Если пользователь соглашается, то начинаются циклы записи-воспроизведения. В итоге, получилась несколько забавная поделка и плюс к опыту работы со звуком. Вот результат: Space Hamster [8].
Вполне может случиться, что в каком-то браузере это не заработает, если будут какие-то отзывы, попробую собрать статистику по этому вопросу.
Запись и модификация звука в браузере [9]

Автор: optiklab

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/29114

Ссылки в тексте:

[1] jQuery плагин jRecorder: http://www.sajithmr.me/jrecorder-jquery

[2] Image: http://4.bp.blogspot.com/-MCiQ2MwITmQ/UTzIF_KkamI/AAAAAAAABbs/N5HTLZw5Mfo/s1600/soundrec.jpg

[3] хостинге: https://www.reg.ru/?rlink=reflink-717

[4] писали: http://habrahabr.ru/post/160833/

[5] официальном сайте Adobe: http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS1C0E1BE2-5322-4558-B7F9-73326C8F48EE.html

[6] Online Image Editor: http://www.online-image-editor.com/

[7] Image: http://2.bp.blogspot.com/-Yx0kxR-Qd9c/UTzQwK80-NI/AAAAAAAABcA/Jf5AdB2BmrE/s1600/hamster1.jpg

[8] Space Hamster: http://gdriv.es/spacehamster

[9] Image: http://2.bp.blogspot.com/-ak00kUj2ZY4/UTzHp4PiQcI/AAAAAAAABbc/N_uwzMrrcYs/s1600/hamster2.jpg

[10] Источник: http://habrahabr.ru/post/172299/