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



Комментарии
Отправить комментарий