К основному контенту

Запись и модификация звука в 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'и говорят сами за себя. Алгоритм прост:

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

    Комментарии

    Популярные сообщения из этого блога

    Делаем себе бесплатный VPN на Amazon EC2

    Читать этот пост в Telegraph. Другие посты в канале в Telegram. Кто только не расписывал уже пошаговые инструкции по этой теме. Однако, время идёт, ПО меняется, инструкции нуждаются в обновлении, а люди в современной России всё больше нуждаются в применении VPN. Я собираюсь описать все шаги для создания бесплатного сервера на Amazon EC2 с операционной системой Linux и необходимые команды для настройки VPN сервера на нём. Чтобы не повторяться о деталях, которые были много раз описаны на русскоязычных и англоязычных ресурсах, по ходу статьи я просто приведу целую кипу ссылок, где можно почерпнуть необходимую информацию, а где информация устарела - опишу подробнее что нужно сдеать. В итоге, сервер будет доступен для вас из любой точки планеты, с любой операционной системы, и бесплатно (с определёнными ограничениями по трафику). Шаг первый - Регистрируемся на Amazon AWS Нужно зайти на сайт https://aws.amazon.com/ru и сразу перейти к Регистрации, нажав одноимённую кнопку. При р

    В помощь программисту: инструкции по работе с Ubuntu сервером

    Программистам чаще приходится писать код и заботиться о его чистоте, правильных абстракциях в коде, корректных зависимостях и прочих сложностях профессии. При этом, настройка и обслуживание серверов, хоть и связанная область - это отдельный навык, необходимый не каждому, и помнить о котором в деталях сложно. Поэтому, я делаю ряд микро-инструкций, которыми буду пользоваться и сам, когда необходимо. Это не статьи, а пошаговые помощники, которые я буду дополнять и наполнять по мере надобности. Делаем бесплатный VPN на Amazon EC2 Создание ключей SSH Подключение к серверу через SSH Передача файла с Linux сервера наWindows машину Делаем VPN сервер на Ubuntu 20.04 используя OpenVPN и EasyRSA  Отображение GUI с Linux сервера на Windows машине

    Выбираем все плюсы из трех парадигм Entity Framework

    Между парадигмами разработки с Entity Framework (Code First, Model First, Database First) я выбрал промежуточную, потому что ни одна меня не устраивала полностью. В Code First меня радуют чистые POCO классы, но не устраивает невозможность моделирования базы. В Database First и Model First мне не нравится генерация EDMX и другого всего лишнего. Таким образом, я нашел для себя такое решение: 1. Я моделирую схему в любой удобной программе (тут любая внешняя программа моделирования, генерирующая SQL Server-совместимые скрипты генерации базы) Рис. Смоделированная схема БД. 2. Создаю базу в SQL Management Studio 3. Делаю Reverse Engineering базы в POCO классы (как в Code First) с помощью плагина Entity Framework Power Tools Рис. Установленный плагин для Reverse Engineer. Рис. Вот так делается Reverse Engineer базы данных в POCO классы. Рис. Результат генерации POCO классов на основе базы данных: папочка Models с готовым контекстом, классами объектов и маппинг-классами.