Генерация звука во Flash 10

Апрель 17, 2009
|

Все мы давно живем в мире цифровых технологий, которые преобладают над аналоговыми. Применительно к нашей теме, цифровой звук обладает огромными достоинствами — это возможность копирования цифрового оригинала без потерь, возможность сжатия и передачи по цифровым каналам, более простая обработка и редактирование.

Для лучшего понимания материала можно ознакомится с теорией, на мой взгляд неплохая статья. Тем, кому скучно читать теорию, смело двигайтесь дальше.

Немного истории

Перед основной темой хотел бы рассказать немного истории о том, как в FP10 появился прямой доступ к звуковому буферу и, в итоге, возможность генерации звука. Все началось с известного флешера Andre Michelle. Очень интересного и разностороннего человека, за экспериментами которого я постоянно слежу. После появления FP9, пока все изучали новое АПИ, Андре нашел хитрый путь, как в реальном времени генерировать звуковые данные. На эти эксперементы его сподвигли старые работы товарища Frank Baumgartner'a, который тоже использовал некую «технологию» для создания звуковых эффектов еще в далеком 2002 году. После нескольких, полностью работающих примеров (вот один из первых), которые Андре показал общественности, все были в недоумении и задавались вопросом — «Как это вообще возможно?». Исходные файлы пока были не доступны, но после появления первого декомпилера для AS3 «загадка» была раскрыта. Все оказалось достаточно просто, надо было включить фантазию и идти нестандартным путем. В AS3 появился очень гибкий метод — loadBytes класса Loader. Этот метод позволяет создать муви-клип из обычных бинарных данных, находящихся в байтовом массиве ByteArray. Андре создал простой некомпрессированный SWF файл, который содержал в себе звук в формате PCM (это обычный WAV файл без сжатия) пригодный для импорта в сцену как класс. И поместил его в байтовый массив. Немного изучив спецификацию формата SWF, нетрудно найти сами звуковые данные и записывать вместо них необходимую информацию. В итоге алгоритм генерации звука состоял из нескольких этапов и выглядел так:

  1. Генерация звуковых данных в байтовый массив с некоторым смещением от его начала (с учетом спецификации SWF файла).
  2. Загрузка муви-клипа из байтового массива и импорт звука как класса.
  3. Установка функции на событие onSoundComplete.
  4. Запуск воспроизведения звука через метод play().
  5. При окончании проигрывания повторяем все с первого этапа.

Краткий оригинал этой истории можно прочитать в блоге Андре. Подробнее про принцип генерации звука в FP9 читайте в блоге FlashBrighton, тоже интересно.

Технология доказала, что может существовать, быть интересной для людей и положила начало некоторым коммерческим проектам. Но случилось «несчастье», появилась операционная система «Виста», и различные билды 9-го плеера стали вести себя по разному на этой ОС. Событие onSoundComplete стало приходить иногда с задержкой, иногда нормально. Звук «рвался» у половины пользователей сервиса. Последний билд №115 ничего не изменил к лучшему. За всем этим от Андре последовало несколько писем в Адоби, а потом просьба, чтобы в следующей версии флеш-плеера был нормальный доступ к звуковому буферу. Просьбу поддержали и проголосовали за нее не только фаны, но и много других флешеров, оценивших будущие возможности. Для обсуждения проблемы была создана целая кампания по улучшению звуковых возможностей во Флеш, к которой, в последствии, подключилась и Flj. В итоге, спустя год-два, мы получили полноценный доступ к звуковому буферу и теперь можем его использовать без проблем и «хаков» (скажем большое спасибо Tinic Uro  — Flashplayer engineer из Адоб.).

Вот несколько интересных проектов и экспериментов, основанных на доступе к звуковому буферу во Флеш:

Новое звуковое АПИ

Итак, что у нас появилось нового в АПИ, связанного со звуком и актуального нашей  теме?

Новое событие класса Sound — «sampleData», вызывается при запросе аудио-данных от плеера. Как раз оно дает нам возможность генерировать звук в реальном времени. Использовать его очень просто, достаточно создать новый экземпляр класса Sound, который изначально будет «пустым», то есть не будет содержать в себе звуковых данных. Добавить функцию обработки на событие «sampleData». И запустить воспроизведение звука с помощью метода play(), который в свою очередь вернет объект SoundChannel, звуковой канал. Функция-обработчик события на входе будет получать объект SampleDataEvent и используя его свойство data можно записывать звуковые сэмплы в буфер, как в обычный байтовый массив (ByteArray) методом writeFloat(). Потом записанные нами сэмплы будут воспроизведены звуковой картой. Флеш-плеер будет посылать события, пока мы не прекратим записывать сэмплы в звуковой буфер или пока звуковой канал (SoundChannel) не будет остановлен методом stop().

Самый простой пример генерации тона:

<code class="javascript">

var sound:Sound = new&nbsp;Sound();

function soundUpdate(event:SampleDataEvent):void {

	for&nbsp;( var c:int=0;&nbsp;c<3072;&nbsp; c++ ) {

		var sample:Number = Math.sin((Number(c+event.position)/Math.PI/2.0))*0.25;

		event.data.writeFloat(sample);		// записываем значение семпла в&nbsp;левый&nbsp;канал

		event.data.writeFloat(sample);		// и&nbsp;правый&nbsp;канал

	}

}

sound.addEventListener('sampleData',soundUpdate);

var soundChannel:SoundChannel = sound.play();

    </code>

Вот более продвинутый вариант примера, в котором частота генерируемого тона зависит от положения мышки над флешкой:

<code class="javascript">

var phaseL:Number = 0;

var phaseR:Number = 0;



var incrementL:Number = 0;

var incrementR:Number = 0;



function soundUpdate(event:SampleDataEvent):void {

	for ( var c:int = 0; c<3072; c++ ) {



		// получим новое приращение исходя из&nbsp;позиции&nbsp;мышки

		var incrementNewL:Number = 0.03 + 0.2 * stage.mouseY / stage.stageHeight;

		var incrementNewR:Number = 0.03 + 0.2 * stage.mouseX / stage.stageWidth;



		// нарастим текущее приращение (фильтр требуется для более плавного изменения тона)

		incrementL += (incrementNewL &mdash; incrementL) * 0.0002;

		incrementR += (incrementNewR &mdash; incrementR) * 0.0002;



		// рассчитаем значения семплов для правого и левого звукового канала

		var sampleL:Number = Math.sin(phaseL += incrementL);

		var sampleR:Number = Math.sin(phaseR += incrementR);



		// забишем семплы в звуковой буфер

		event.data.writeFloat(sampleL);

		event.data.writeFloat(sampleR);

	}

}

var sound:Sound = new Sound();

sound.addEventListener('sampleData', soundUpdate);

var soundChannel:SoundChannel = sound.play();

</code>

Формат звука, используемый для генерации, всегда будет иметь частоту дискретизации 44100 Гц, обязательно два канала, а сэмплы всегда представлены 32-битным Float числом (числом с плавающей запятой, 4 байта). В итоге на каждый сэмпл звука должно быть записано 8 байт.

Величины, которые мы можем записать в звуковой буфер, должны быть в пределах от -1.0 до 1.0.  Большие значения будут просто обрезаны звуковой картой до пороговых. Если мы записываем нули в буфер, то логично, что мы не услышим звука :).

Количество сэмплов, которые мы можем записать в буфер, может варьироваться в пределах от 2048 до 8192. Причем если мы запишем сэмплы в количестве, меньшем, чем 2048, то сэмплы будут проиграны, а потом флеш-плеер пошлет событие SoundComplete, что будет значить, что наш звук полностью проигран и завершен, и конечно после этого звуковой канал будет остановлен.

Если мы записываем минимальное количество сэмплов, равное 2048, то мы имеем минимальную задержку с момента начала записи в буфер до момента ее воспроизведения через звуковую карту. Эта задержка будет равна примерно 46 миллисекундам (t, сек =2048.0/44100.0). Но при малом количестве сэмплов появляется высокая вероятность щелчков и «разрыва» звука, если процессор компьютера сильно нагружен расчетами или перерисовкой. Перечисленные артефакты очень хорошо заметны на слух. Адоб не рекомендует использовать малое количество сэмплов, так как это может работать на разных конфигурациях компьютеров и ОС по-разному. Если мы будем записывать 8192 сэмпла, то вероятность «разрыва» звука становится минимальной, и на разных платформах это будет работать максимально стабильно, хотя опять же не идеально. Задержка составит уже 186 миллисекунд.

На самом деле длительность задержки критична только в тех случаях, когда звук генерируется по событию от пользователя. Например, при нажатии на элементы интерфейса, или при использовании клавиатуры. В таких случаях пользователю можно предлагать на выбор величину буфера. Для остальных приложений, например, таких как плееры, существующая задержка вообще не имеет значения и нам лучше записывать в буфер максимально возможное количество сэмплов.

Также в АПИ появился новый полезный метод extract(target:ByteArray=null, length:Number=null, startPosition:Number = -1) для объекта Sound(). Он позволяет извлекать звуковые данные из звукового объекта (например, сжатого MP3 звука) и манипулировать ими, как угодно. На входе метода мы должны задать байтовый массив (объект ByteArray), и звуковые данные будут извлечены в массив, начиная с текущей позиции массива. Они всегда будут в формате 44100 Гц, стерео. Формат сэмпла — 32-битное число с плавающей запятой, оно будет преобразовано в тип Number при извлечении сэмпла из байтового массива методом readFloat(). Извлечение данных может занять порой достаточно большое время — 200—500 миллисекунд, и сами данные могут занять приличный объем памяти. Если учесть, что одна секунда несжатого звука занимает примерно 350 килобайт, то пятиминутный звук займет примерно 100 мегабайт. Все это надо учитывать при написании программы.

Как мы это можем применить? К примеру, использовать простой ресемплинг, чтобы выводить звуки в другой тональности.

<code class="javascript">

var sample:ByteArray = new ByteArray();

var sampleCount:Number = 119056;

var sampleLoop:Number =&nbsp;89840;

var samplePosition:Number =&nbsp;0;

var sampleIncrement:Number =&nbsp;0;

var sampleVolume:Number =&nbsp;0;

function soundUpdate(event:SampleDataEvent):void {

	for&nbsp;(var i:int=0;&nbsp;i<3072;&nbsp; i++) {

		samplePosition += sampleIncrement;

		if&nbsp;(samplePosition>=sampleCount) {

			samplePosition=sampleLoop;

			sampleVolume-=0.3;

			if&nbsp;(sampleVolume<=0) {

				sampleVolume=0;

			}

		}

		sample.position=Math.round(uint(samplePosition)<<3);

		event.data.writeFloat( sampleVolume*sample.readFloat() );

	}

}

var sound:Sound = new SoundPiano();

sound.extract(sample,sampleCount,0);

sound = new&nbsp;Sound();

sound.addEventListener('sampleData',soundUpdate);

sound.play();

   </code>

Вот рабочий пример:

В начале, мы извлекаем звуковые данные из звуковых объектов. Потом, как в предыдущем примере, создаем объект Sound() и присоединяем функцию-обработчик события «sampleData». Так же мы создадим переменную, которая будет обозначать текущую позицию в байтовом массиве, содержащем звуковые данные. И еще одну переменную, которая будет шагом приращения позиции. Если шаг равен единице, то мы услышим оригинал звука, если шаг больше единицы, то звук будет воспроизведен в более высокой тональности, а если меньше единицы, то в более низкой. Рассчитать шаг, имея частоту ноты или ее миди-код, очень просто — вот формулы:

<code class="javascript">



// для MIDI-кода ноты

var step:Number = Math.pow(2,(midi_code-69)/12)



// для частоты ноты

var step:Number = frequency/440.0

    </code>

Нам осталось извлечь звуковые данные из массива и записать их в звуковой буфер. Все очень просто, как вы сами видите. В итоге мы можем использовать короткие кусочки звуков для разных инструментов, которые мы слышим в нашей жизни, чтобы создать полноценную музыку. Причем файл будет маленького объема.

В 90-е годы, когда памяти было мало, процессоры были медленные, а объемы жестких дисков тоже оставляли желать лучшего, использовали именно этот метод, чтобы добавить музыку в приложения и игры. Такая музыка называлась трекерной, и существовало несколько популярных форматов музыкальных файлов — MOD, S3M, XM и т.п.  8bitboy player как раз предназначен для проигрывания MOD-файлов.

Также, кроме ресемплинга, мы можем обрабатывать звук перед выводом, например, через программный эквалайзер. Или использовать различные алгоритмы для наложения эффектов, таких как реверберация, фленжер и т.п.

Чип тьюнс

У меня остался еще один пример, из которого и родилась тема статьи. Это эмулятор старого музыкального процессора Yamaha YM2149, который использовался параллельно с компьютерами типа ZX Spectrum в далеких 90-х годах. В итоге, на основе эмулятора, получился простой плеер.

Этот плеер я написал для моего друга, который профессионально занимается музыкой на PC, но иногда у него есть желание понастольгировать и послушать свои старые музыкальные треки, с которых начиналась его творческая деятельность. Все его треки вы можете послушать здесь, а также подборку треков других авторов.

Плеер пока находится в постоянной доработке, поэтому исходные файлы размещу позже. Хотя, для ознакомления вы можете посмотреть классы эмулятора, это может быть интересно. Код сильно не критикуйте, плеер создавался на чистом энтузиазме долгими бессонными ночами :) .

Автор записи: Михаил Востриков

  • Defaultsp

    Каким образом рассчитывалось значение частоты в примере с мышью?

Twitter Facebook Flickr