Недавно решил 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.
Вполне может случиться, что в каком-то браузере это не заработает, если будут какие-то отзывы, попробую собрать статистику по этому вопросу.