Запись и модификация звука в Web
Недавно решил for fun сделать сайтик, на котором будет происходить запись и модификация звука. Как делать это в Windows я знаю, опыт есть, однако ни разу не делал этого в веб. Немного погуглив, выяснилось что не так уж и много возможностей записать звук. Самая широко распространенная - использование Flash. У меня опыт во Flash нет совсем, к тому же весь UI и функционал я хотел сделать на JavaScript + HTML. В итоге, я нашел jQuery плагин jRecorder для записи звука, который внутри себя в итоге использует код на ActionScript'е.
Моей задумкой было сделать так, чтобы человек говорил что-нибудь в микрофон, этот звук записывался, и потом воспроизводился уже немного искаженным. Для забавы, хотелось добавить туда ещё какую-нибудь простейшую анимацию. Но, я программист, а не дизайнер, поэтому рисовать 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'и говорят сами за себя. Алгоритм прост:
Но тут проблема: когда начинать или останавливать запись? Мне не хотелось делать это простой кнопкой, пусть хомяк произносит слова только тогда, когда в микрофон действительно что-то говорили, а не шел простой шум или тишина. Для этого я решил анализировать уровень звука с микрофона, на счастье, jRecorder бросает callback_activityLevel, в котором передается уровень звука - level. Мне нужно было только придумать алгоритм. И я решил делать так:
В итоге, если человек ничего не говорит долгое время, и в микрофон не попадает никаких дополнительных шумов, то мы не воспроизводим ничего. В случае раговора (ну или шумов, что тоже бывает =)) пишем 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, погуглил код модификации звука, и на моё счастье прямо на официальном сайте ActionScript нашел примеры работы со звуком.
Во время записи с микрофона идут пачки сырых байт - сэмплов. В 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)и выложил СИЕ на Google Drive hosting. Открываем сайт, Flash спрашивает разрешение на доступ к микрофону:
Если пользователь соглашается, то начинаются циклы записи-воспроизведения. В итоге, получилась несколько забавная поделка и плюс к опыту работы со звуком. Вот результат: Space Hamster. Вполне может случиться, что в каком-то браузере это не заработает, если будут какие-то отзывы, попробую собрать статистику по этому вопросу.
Комментарии
Отправить комментарий