Недавно решил 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',
}
);
Сайт я решил выложить на бесплатном
мне записывать 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());
_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.
Вполне может случиться, что в каком-то браузере это не заработает, если будут какие-то отзывы, попробую собрать статистику по этому вопросу.
Автор: optiklab