11 марта 2013 в 18:28

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

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


Моей задумкой было сделать так, чтобы человек говорил что-нибудь в микрофон, этот звук записывался, и потом воспроизводился уже немного искаженным. Для забавы, хотелось добавить туда ещё какую-нибудь простейшую анимацию. Но, я программист, а не дизайнер, поэтому рисовать 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',
 }
);

Сайт я решил выложить на бесплатном хостинге, для чего использовал свой Google Drive account. Как его использовать под хостинг на Хабре уже писали. Там куча ограничений, одно из них не позволяет
мне записывать 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 нашел примеры работы со звуком.

Во время записи с микрофона идут пачки сырых байт — сэмплов. В 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());

  if (event.data.bytesAvailable > 0) // опять же, проверяем, что в потоке ещё что-то есть
  {
    _buffer.writeFloat(event.data.readFloat());
  }

  if (event.data.bytesAvailable > 0) 
  { 
   event.data.position += 2; // Ну подумаешь, пропустили чуть-чуть
  } 
 }
}


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

Если пользователь соглашается, то начинаются циклы записи-воспроизведения. В итоге, получилась несколько забавная поделка и плюс к опыту работы со звуком. Вот результат: Space Hamster.
Вполне может случиться, что в каком-то браузере это не заработает, если будут какие-то отзывы, попробую собрать статистику по этому вопросу.
+15
9222
90
optiklab 4,0 G+

Комментарии (12)

+1
monushka #
В хроме что-то не воспроизводит речь — хомяк нямкает, а звука нет. :(
0
optiklab #
Периодически есть лаги в разных браузерах (не только Chrome): javascript отрабатывает как надо и хомяк начинает «говорить», но со стороны Flash что-то работает не так и звук не воспроизводится. Refresh страницы должен помочь.
0
lestatbbk #
C jRecorder что-то не все гладко.

Exceptions 1.

RangeError: Error #1125: Индекс NaN выходит за границы диапазона 0.
at org.as3wavsound::WavSoundChannel/buffer()
at WavSoundPlayer/onSamplesCallback()

Exceptions 2.

Error #2044: Необработанный ioError:. text=Error #2032: Ошибка потока. URL: googledrive.com/host/0B4Q3U97fHTqIVzhpb09XRTJ2enM/acceptfile.php?filename=hello.wav
at Main/finalize_recording()
at Main/jSendFileToServer()
at Function/http://adobe.com/AS3/2006/builtin::apply()
at flash.external::ExternalInterface$/_callIn()
at Function/()
at flash.external::ExternalInterface$/_evalJS()
at flash.external::ExternalInterface$/call()
at Main/jStopRecording()
at Function/http://adobe.com/AS3/2006/builtin::apply()
at flash.external::ExternalInterface$/_callIn()
at Function/()
at flash.external::ExternalInterface$/_evalJS()
at flash.external::ExternalInterface$/call()
at Main/updateMeter()

Exceptions 3.

Error: Error #2030: Обнаружен конец файла.
at flash.utils::ByteArray/readFloat()
at org.bytearray.micrecorder::MicRecorder/onSampleData()

0
optiklab #
К сожалению, я не специалист по Flash. Но судя по ошибке, могу предположить что функция ByteArray/readFloat() возвращает ровно 1 байт. Однако, код модификации звука, который я взял на сайте Adobe делает 2 чтения после проверки доступности байт в потоке:
while(event.data.bytesAvailable > 0)
 {
  _buffer.writeFloat(event.data.readFloat());
  _buffer.writeFloat(event.data.readFloat());
  ...

Таким образом, при нечетном количестве байт в потоке, возможен BufferOverflow. Я на всякий случай добавил доп. проверку.
0
DAiMor #
На чистом JS есть возможность записать звук примеры есть на html5rocks
используя getUserMedia с WebAudioAPI, соответственно пока есть ограничение на кроссбраузерность и кроссплатформенность
запись можно делать с помощью скрипта recorder.js

Но по какой то непонятной мне причине так и не удалось заставить это работать на версиях Chrome от последней стабильной и вплоть до 27.0.1430.0 dev-m под Windows

запись видео кое как получилось (в.т.ч. на андроиде) но звука нет, не работает ни один пример который был дан для отображения работы с микрофоном
0
feligz #
Есть одна хитрость, почему то гугл запрещает аудио до сих пор в getUserMedia. Нужно включить «Вход Web Audio» — Включает входящий аудиоканал на базе метода getUserMedia() и API Web Audio, в разделе about:flags, тогда звук будет записываться.
0
DAiMor #
Linux Chrome 25.0.1364.152 включил WebAudioAPI, звук все равно не идет
завтра проверю на работе под Windows, но уверен все эти флаги уже выискал и включил
0
feligz #
Под Linux не идет )) тут я не понял почему, а под windows вполне.
0
DAiMor #
Все таки под Windows7 тоже что-то не идет звук никак.
0
silvansky #
FF 19, OSX 10.8, звук не слышно.
0
optiklab #
Я проверял в FF 19.0.2. Работает. Но спасибо за инфо! К сожалению, можно сделать вывод, что стабильности подобных «шатких» конструкций добиться сложно!
0
Nikitascr #
Познавательно

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.