Микширование цифрового звука во Flash
В Декабре прошлого года мы делали небольшое промо-приложение для Facebook, для крупного бренда — производителя мышек и видеокамер. (Ура, после этой фразы аудитория поредела в двое, а пара человек насторожилось). Одной из фич приложения была функция записи голоса с микрофона пользователя. Причем голос накладывался на аудиотрек.
Фото с сайта: www.thepodcasthost.com

Для решения этой задачи мы использовали потоковый сервер Red5 (после некоторых тестов перешли на Wowza). На стороне клиента, естественно, Flash. Передавать на сервер смешанный голос и аудиотрек — технически невозможно (в RTMP-поток может писать только камера и микрофон, но не звук из колонок). Голос пользователя сохранялся потоковым сервером в виде отдельного файла. После чего специально обученный серверный скрипт микшировал голос и аудиотрек, работая через FFmpeg.

Все бы прекрасно, если бы не сетевые задержки. Небольшие, крохотные задержки при старте записи, передаче данных, завершении записи приводили к тому, что музыка и слова шли несинхронно. Задержки составляли от 0.1 до 0.5 секунды, но были всегда произвольными и могли возникать не только в момент старта записи, но и в какие-то моменты посередине. Простым сдвигом/растяжением файла проблема не лечилась. Как оказалось, несинхронный текст и мелодия начинают нервировать уже при задержках в 0.2 сек.

Epic Fail. Итак, задача: Смикшировать аудиопоток с микрофона и проигрываемый файл.

Решение, как водится, под катом.

Всякий, кто решает проблему микширования аудиопотоков в реальном времени, рано или поздно набредает на вот эту неопимистичную заметку. Краткий пересказ: «Ищите дальше!».

Однако! В Flash-плеере 10.1 появилась возможность получать поток данных непосредственно с микрофона (объекта Microphone), в виде массива байт (ByteArray). Вот как это выглядит изнутри:

package { import flash.display.MovieClip; import flash.events.*; import flash.media.*; import flash.utils.Timer; import flash.utils.ByteArray; import flash.net.URLRequest; public class testClass extends MovieClip { const DELAY_LENGTH:int = 10000; var sound:Sound = new Sound(); var channel:SoundChannel = new SoundChannel(); var mic:Microphone; var timer:Timer; var soundBytes:ByteArray = new ByteArray(); var extSound:Sound; var extChannel:SoundChannel; var isMicReady, isSoundReady; public function testClass() { trace("record"); isMicReady = false; isSoundReady = false; var url:String = "1.mp3"; var urlRequest:URLRequest = new URLRequest(url); extSound = new Sound(); extSound.load(urlRequest); extSound.addEventListener(Event.COMPLETE, soundLoaded); mic = Microphone.getMicrophone(); mic.setSilenceLevel(0, DELAY_LENGTH); mic.gain = 100; mic.rate = 44; mic.addEventListener(StatusEvent.STATUS, micStatus); mic.addEventListener(SampleDataEvent.SAMPLE_DATA, micSampleDataHandler); } function micStatus(evt:StatusEvent) { if (evt.code == "Microphone.Unmuted") { isMicReady = true; tryToStart(null); } } function soundLoaded(evt) { isSoundReady = true; tryToStart(null); } function tryToStart(evt) { if (isMicReady && isSoundReady) { startAll(); } } function startAll() { extChannel = extSound.play(); timer = new Timer(DELAY_LENGTH); timer.addEventListener(TimerEvent.TIMER, timerHandler); timer.start(); } function micSampleDataHandler(event:SampleDataEvent):void { //trace(event); if (event.data != null) { while(event.data.bytesAvailable) { var sample:Number = event.data.readFloat(); //event.data.writeFloat(sample - 0.5); soundBytes.writeFloat(sample); } } } function timerHandler(event:TimerEvent):void { trace(soundBytes.length); //extSound.close(); extChannel.stop(); extSound.play(0); mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, micSampleDataHandler); timer.stop(); soundBytes.position = 0; sound.addEventListener(SampleDataEvent.SAMPLE_DATA, playbackSampleHandler); channel.addEventListener( Event.SOUND_COMPLETE, playbackComplete ); channel = sound.play(); } function playbackSampleHandler(event:SampleDataEvent):void { for (var i:int = 0; i < 8192 && soundBytes.bytesAvailable > 0; i++) { //trace(sample); var sample:Number = soundBytes.readFloat(); event.data.writeFloat(sample); event.data.writeFloat(sample); } } function playbackComplete( event:Event ):void { trace( "Playback finished."); } } } 

Добавляем к этому массиву байт заголовки, и получается обычный Wav-файл. А вот MicRecorder и маленькая, прекрасная библиотека, которая проделывает за нас всю эту работу.

Для проигрываения wav-файлов можно так же воспользоваться небольшой библиотекой AS3WavSound.

Таким образом всё микширование цифрового звука (по ссылке — чуть-чуть математики) можно производить на клиенте, передавая на сервер готовый wav-файл. (Впрочем, знатоки матана и рядов Фурье могут отправлять и чего-нибудь покомпактнее :-)).