Сайт Юрия Макарова

   

 

Делаем флэш анализатор спектра

 

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


 

В основе интересующего нас процесса, основанного на языке программирования ActionScript 3, лежит метод computeSpectrum(), выполняемый в классе SoundMixer. Звуковые данные считываются блоками строго определённой длины, и затем записываются в массив для дальнейшей обработки. Они представляют из себя 32-разрядные комплексные числа с плавающей запятой от -1 до 1, соответствующие фазе звукового сигнала, например -0,1268046721816063, 0,4420428418670781, и т.д. Чем выше амплитуда сигнала, тем больше абсолютное значение числа. Метод computeSpectrum() имеет следующий вид:

SoundMixer.computeSpectrum(outputArray:ByteArray, FFTMode:Boolean = true, stretchFactor:int = 0);

outputArray - параметр, описывающий объект ByteArray, т.е. наш массив, в котором хранятся звуковые данные.
FFTMode - логическое значение, показывающее, подвергается ли звук преобразованию Фурье (true), или считываются необработанные данные звуковой волны (false). В первом случае мы получаем спектр частот, во втором - комплексную амплитуду звукового сигнала.
stretchFactor - задаёт частоту сэмплирования: 0 соответствует частоте 44,1 кГц, 1 - частоте 22,05 кГц, 2 - частоте 11,025 кГц и т.д.

Этот метод использует встроенную во Flash функцию Fast Fourier Transform (FFT), или Быстрое Преобразование Фурье, которая раскладывает спектр звуковых частот на составляющие. В её основе лежит формула Кули-Тьюки (Cooly-Tukey) Nlog2N, где N - количество измерений за промежуток времени. Эта формула, впервые опубликованная в 1965 году, базируется на степени двойки. Её преимущество заключается в том, что логарифмические вычисления на отрезке в 1024 отсчёта, или 210, производятся в 17 раз быстрее, чем аналогичные операции последовательного сложения и умножения. Отсюда слово Fast в названии функции. Поэтому за основу взяли блоки длиной в 1024 отсчёта, в английской терминологии будем называть их bins.

Но для вычисления амплитуд спектра берётся только первая половина блока данных, так называемая действительная часть Real. Вторая половина, или мнимая часть Imaginary, является зеркальным отражением первой, и содержит данные выше частоты Найквиста, т.е. половины частоты дискретизации. Т.к. мнимая часть не несёт никакой дополнительной информации о звуке, её можно не использовать. Таким образом, у нас остаются блоки с действительными данными длиной в 512 bins, в которых отрезок 0-255 содержит информацию о звуке левого канала, а 256-511, -  о звуке правого канала (измерения всегда начинаются с нуля). Каждый из этих бинов содержит данные о спектральной плотности в определённой полосе частот, являющейся частью общего спектра. Ширина полосы каждого из 256 bins рассчитывается по формуле 2/N*(F/2), где N - длина блока измеряемых данных (1024), F/2 - половина частоты дискретизации (44100/2=22050), и составляет 2/1024*22050=43,06640625 Гц. Это ключевой момент, на котором строится весь спектральный анализ во флэш анимации. Теперь нетрудно подсчитать ширину полосы всего измеряемого спектра: 43,06640625*256=11025 Гц, т.е. частоты выше 11 кГц во Flash плеере отображаться не будут. Иными словами, мы имеем звуковой спектр, линейно разбитый на 256 частей с шагом в 43 Гц и условной спектральной плотностью полос: 43, 86, 129, 172, 215, ... 10982, 11025 Гц. Этого вполне достаточно для графического отображения звука. В левой части шкалы спектра расположены низкие частоты, в правой высокие.
 

Принцип работы Быстрого преобразования Фурье

 

Нам нужно создать внешний класс со спрайтом анализатора спектра. Назовём его SpectrumAnalyzer, зададим файлу расширение as и будем писать в любом текстовом редакторе, в моём случае это Adobe Animate CC, - он позволяет сразу же видеть готовый результат. Разместим файл SpectrumAnalyzer.as в одной папке с основным fla документом плеера, к которому и будем его подключать. Во время компиляции оба файла объединяются в один с расширением swf (Small Web Format), который представляет из себя полноценный флэш плеер с функцией анализатора спектра. Сам плеер мы здесь рассматривать не будем, а лишь немного коснёмся параметров подключения внешнего класса. Вначале импортируем необходимые классы и объявим внешний класс SpectrumAnalyzer, название которого должно совпадать с именем файла без расширения и с соблюдением регистра. В классе прописываем необходимые переменные, их значения можно менять в зависимости от предпочтений. Неизменной остаётся лишь длина блока сэмплов CHANNEL_LENGTH, которая всегда равна 256. Обратите внимание на константы left и right, которые представлены в виде векторов. В них мы будем хранить данные левого и правого каналов, - вектор работает во много раз быстрее массива. Графические объекты в виде вертикальных полос, которые будут показывать амплитуду сигнала разных частот, принято называть bars. Цвет этих баров пока задавать не будем, дальше станет понятно почему.

package {
import fl.motion.easing.Quadratic;
import flash.display.Graphics;
import flash.display.Sprite;
import flash.events.Event;
import flash.media.Sound;
import flash.media.SoundChannel;
import flash.media.SoundMixer;
import flash.net.URLRequest;
import flash.utils.ByteArray;

public class SpectrumAnalyzer extends Sprite { // Объявляем внешний класс SpectrumAnalyzer

private var BAR_MAX_HEIGHT : int = 80; // Максимальная высота каждого бара
private var BAR_WIDTH : int = 8; // Ширина каждого бара
private var BAR_COLOR : int; // Будущий цвет баров
private var BAR_SPACING : int = 1; // Расстояние в пикселях между соседними барами
private var NUM_SEGMENTS : int = 32; // Количество баров
private var CHANNEL_LENGTH : int = 256; // Количество сэмплов в блоке данных, это значение всегда равно 256
private const left:Vector.<Number> = new Vector.<Number> (CHANNEL_LENGTH, true); // Вектор данных левого канала
private const right:Vector.<Number> = new Vector.<Number> (CHANNEL_LENGTH, true); // Вектор данных правого канала
private var barsHolder : Sprite; // Спрайт нашего анализатора
private var bins:Array = []; // Массив сэмплов
private var bars:Array = []; // Массив для создания баров
private const bytes : ByteArray = new ByteArray(); // Входящий массив байтов для FFT

Создадим функцию анализатора спектра, её название также должно совпадать с именем класса. В скобках указан параметр, отвечающий за цвет баров, который пока не определён.

public function SpectrumAnalyzer(BAR_COLOR) { // Объявляем функцию анализатора спектра с параметром цвета
barsHolder = new Sprite(); // Создаём спрайт баров
addChild(barsHolder); // Добавляем спрайт баров на сцену
bars = new Array(); // Создаём массив баров
var bar : Bar; // Объявляем переменную, соответствующую внутреннему классу Bar
for (var i : int = 0; i < NUM_SEGMENTS; i++) { // Проходим в цикле каждый бар
bar = new Bar(BAR_MAX_HEIGHT, BAR_WIDTH, BAR_COLOR); // Создаём экземпляры баров с параметрами высоты, ширины и цвета
bar.x = i * (BAR_WIDTH + BAR_SPACING); // Расположение каждого бара по горизонтали
barsHolder.addChild(bar); // Добавляем экземпляры баров на сцену
bars.push(bar); // Заполняем массив значениями параметров всех баров
}
addEventListener(Event.ENTER_FRAME, onEnterFrame); // Вызываем слушатель, который смотрит на аудио спектр
}

Запускаем метод computeSpectrum(), который создаёт снимок звуковой волны длиной в 512 bins, данные сохраняются в массиве bytes. Здесь происходит вся логика рисования, перерисовка один раз за кадр.

private function onEnterFrame(event : Event) : void {
SoundMixer.computeSpectrum(bytes, true, 0);


Теперь у нас есть возможность получить FFT данные, с которыми можно работать. Но не всё так просто, как кажется на первый взгляд. Обычно значения соседних частот усредняют для каждого бара, чтобы не было провалов в спектре. Под словом «усредняют» имеется в виду следующее: значения нескольких соседних bins складывают и сумму делят на коэффициент, равный их количеству. Таким образом получают одно стандартное значение амплитуды от 0 до 1 в некоторой точке спектра, эквивалентное сразу нескольким значениям соседних bins, и присваивают его одному бару. Допустим, у нас есть 32 бара, тогда на каждый из них будет приходиться по 8 соседних bins (256/32=8) общей шириной полосы в 344 Гц каждый (8*43=344). Первый бар будет работать в полосе частот от 0 до 344 Гц, второй, - от 345 до 688 Гц, третий, - от 689 до 1032 Гц, и т. д. с шагом в 344 Гц. Это называется линейным методом чтения данных, поскольку в основе самой функции FFT заложен алгоритм линейного преобразования. Его основной недостаток заключается в том, что в барах, отображающих низкие частоты, теряются детали. Например, на первый бар приходится три с половиной октавы клавиатуры фортепиано, на второй, - одна октава (обертоны мы здесь не учитываем). Говоря проще, основная область частот, где чувствительность слуха максимальна, как бы искусственно втиснута в несколько баров слева, и лишь относительно небольшая её часть, где амплитуда сигнала постепенно падает с ростом частоты, непропорционально распределена на бо́льшее количество баров. Алгоритм обработки здесь не учитывает особенностей восприятия звука человеком.

Нам нужен логарифмический метод чтения данных, чтобы отображать спектр так, как слышит его наше ухо. Если взглянуть на некоторые известные анализаторы спектра, такие как Voxengo SPAN в виде VST-плагина, то мы увидим, что в средней точке шкалы находится частота 700 Гц, и это не случайно. А мы уже знаем, что минимальный шаг измерения во флэш анимации составляет 43 Гц. Теперь мы можем рассчитать bin-индекс центрального бара: 700/43=16 (дробная часть здесь не имеет значения). То есть 16-й bin из 256 соответствует частоте примерно 700 Гц, и бар с этой частотой должен располагаться посередине. При линейном методе чтения это был бы 128-й bin (256/2=128) с частотой 5512 Гц (128*43,06640625=5512).

 

VST анализатор спектра Voxengo SPAN

Вверху линейное чтение данных, внизу логарифмическое

 

 

Напишем функцию, которая даст нам логарифмическое соответствие бинов каждому конкретному бару и назовём её binFromBar. Здесь также соблюдаем обязательное условие: количество баров должно быть кратно степени двойки: 2, 4, 8, 16, 32, 64, 128, 256.

private function binFromBar(bar:int):int { // Объявляем функцию binFromBar()
var xx:Number = bar; // Порядковый номер бара
var yy = Math.exp((Math.log(16) * (2 / NUM_SEGMENTS)) * xx); // bin-индекс текущего бара
trace(Math.round(yy)); // Проверяем текущее значение bin-индекса
return Math.round(yy) as int; // Возврат значения функции
}

Эта функция увеличивает разрешение на низких частотах и уменьшает его на высоких в соответствии с логарифмической шкалой. Чтение данных происходит выборочно: низкочастотные bins считываются чаще, как бы визуально раздвигая спектр в этой области, а высокочастотные реже, как бы сжимая его. Функция binFromBar() возвращает основание натурального логарифма из произведения следующих параметров: Math.log(16) - натуральный логарифм bin-индекса центрального бара, NUM_SEGMENTS  - количество баров, xx - порядковый номер бара.  Если взглянуть на окно Output, то мы увидим ряд: 1, 1, 1, 2, 2, 2, 3, 3, 4, 5, 6, и т.д. То есть бары с 1-го по 3-й соответствуют значению только 1-го bin с частотой 43 Гц, бары с 4-го по 6-й, - только 2-го bin с частотой 86 Гц, бары 7-8, - только 3-го bin с частотой 129 Гц. Иными словами, обращение к одним и тем же bins происходит настолько часто, что их значения повторяются сразу в нескольких соседних барах. Это результат высокого разрешения на НЧ, который приводит к тому, что бары низких частот двигаются блоками по 3, 3, 2, 1, 1, 1, и т.д., что не очень красиво. Если увеличить количество баров до 64 или 128, блоки будут ещё больше. Нам нужно их как-то «расцепить». Для этого будем комбинировать линейное чтение данных, скажем, до центрального бара, и логарифмическое уже до конца видимого участка спектра. Модифицированная функция будет выглядеть так:

private function binFromBar(bar:int):int { // Объявляем функцию binFromBar()
var xx:Number = bar; // Текущий порядковый номер бара
var yy:Number; // Объявляем переменную bin-индекса текущего бара
if(xx<(NUM_SEGMENTS/2)) {yy=xx} // Если номер текущего бара меньше половины их количества, то bin-индекс равен номеру бара
else{yy = Math.exp((Math.log(16) * (2/NUM_SEGMENTS)) * xx)}; // В противном случае идёт логарифмическая выборка
trace(Math.round(yy)); // Проверяем текущее значение bin-индекса
return Math.round(yy) as int; // Возврат значения функции
}

Смотрим на Output, - числа не повторяются: 0, 1, 2, 3, 4, 5, 6, 7, и т.д. Первый вариант функции лучше подойдёт для количества баров 8 и меньше, т.к. в этом случае блоки пропадут даже при полностью логарифмической выборке данных. Вариант с комбинированным чтением идеально подходит для 32 баров, т.к. бин-индекс центрального бара с частотой 700 Гц равен 16, это как раз половина от 32. Теперь спектр визуально приближен к тому, что мы слышим.

 

Убираем блоки на низкой частоте

 

Но я забежал немного вперёд. Вначале нам нужно прочитать данные в соответствии с логарифмической функцией binFromBar() и получить амплитуды всех баров. Поскольку данные колеблются в интервале от -1 до 1 в соответствии с фазой звукового сигнала, функция Math.abs() возвращает только положительные значения, и на их основе уже можно рисовать графику.

// Считываем данные левого канала в вектор left
var i:int = 0;
while (i < CHANNEL_LENGTH) {
left[i] = bytes.readFloat();
i++;
}
// Считываем данные правого канала в вектор right
i = 0;
while (i < CHANNEL_LENGTH) {
right[i] = bytes.readFloat();
i++;
}
// Получаем амплитуды всех баров
for (i = 0; i < NUM_SEGMENTS; i++) {
// Усредняем значения величин левого и правого каналов для общей спектрограммы в каждом индексе segmentTotals
binIndex = binFromBar(i);
valuesAverage = (Math.abs(left[binIndex])+Math.abs(right[binIndex])) / 2;
// Выводим новую высоту усредненных значений
newBarHeight = valuesAverage * BAR_MAX_HEIGHT;
if(valuesAverage > 1) valuesAverage = 1;
// Обновляем один из наших баров
bar = bars[i] as Bar;
bar.update(valuesAverage);
}

Амплитуды у нас есть, но это ещё не всё. Давайте попробуем воспроизвести файл с фонограммой белого шума, который, как известно, характеризуется равномерным распределением амплитуд всех частот звукового диапазона, т.е. его спектр имеет плоский вид. На спектрограмме мы увидим случайные флуктуации (отклонения от средней величины) амплитуд всех баров, но в целом они будут стремиться к одному и тому же значению. Это хорошо, но не совсем. Хорошо потому, что все наши расчёты правильные. Но если теперь воспроизвести файл с музыкой, то как бы хорошо она ни звучала, в спектре всегда будут преобладать низкие частоты. Это результат того, что энергия низких частот звука гораздо больше, чем высоких. Чтобы визуально выровнять спектр, мы будем использовать так называемый взвешивающий фильтр. Суть его состоит в том, чтобы плавно увеличивать поправку на коэффициент усиления bin, скажем, от 0,4 на самой низкой частоте до 1 на самой высокой. Нам нужно прибавлять к исходному значению 0,4 некую величину, пропорциональную номеру bin от 0 до 255, чтобы в конце получилась единица. Разницу амплитуд на краях (1-0,4=0,6) делим на количество bins: 0,6/256=0,00234375, и эту величину умножаем на текущий номер bin, чтобы получить плавное увеличение коэффициента усиления от 0,4 до 1 на отрезке 0-255. Добиваясь желаемого характера спектра, разницу амплитуд можно сделать любой, она легко пересчитывается. Напишем эту функцию и назовём её applyWeight(). Её можно применить непосредственно при чтении данных спектра.

function applyWeight(binValue:Number):Number { // Объявляем функцию взвешивающего фильтра
var k: Number = 1.5; // Дополнительный общий коэффициент усиления, в коде вместо разделительной запятой всегда ставится точка
return (0.4 + 0.00234375 * i) * k; // Возврат значения функции, где i - это номер текущего bin
}

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

function applyWeight(binValue:Number):Number { // Объявляем функцию взвешивающего фильтра
var k: Number = 1.5; // Дополнительный общий коэффициент усиления
var min_amp = 0.4; // Минимальный коэффициент усиления низких частот
var step_amp = (1 - min_amp) / 256; // Пошаговый коэффициент усиления для каждого bin
return (min_amp + step_amp * i) * k; // Возврат значения функции, где i - это номер текущего bin
}

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

// Считываем данные левого канала в вектор left и применяем к ним коэффициент взвешивающего фильтра
var i:int = 0; while (i < CHANNEL_LENGTH) {
bins[i] = applyWeight(bins[i]);
left[i] = Math.abs(bytes.readFloat() * bins[i]);
i++;
}

// Считываем данные правого канала в вектор right и применяем к ним коэффициент взвешивающего фильтра
i = 0; while (i < CHANNEL_LENGTH) {
bins[i] = applyWeight(bins[i]);
right[i] = Math.abs(bytes.readFloat() * bins[i]);
i++;
}

В силу низкого разрешения функции binFromBar() на высоких частотах измерения не всегда доходят до 11 кГц (дальше я покажу, как обойти это ограничение). Но энергия звука в этой области спектра настолько мала, что это не имеет никакого значения. Амплитуда сигнала здесь примерно в 35 раз меньше, чем на самых низких частотах, что соответствует уровню -31 дБ. В случае с 32 барами binIndex даст нам следующий трэйсинг частот:

 

Номер бара:      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
binIndex:      0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 19 23 27 32 38 45 54 64 76 91 108 128 152 181 215
Частота бара, Гц:      43 86 129 172 215 258 301 345 388 431 474 517 560 603 646 689 732 861 1034 1206 1421 1680 1981 2369 2799 3316 3962 4694 5556 6589 7838 9302
 
 
 
 

До середины шкалы чтение данных спектра идёт линейно, достаточно взглянуть на графу binIndex. Но после середины считываются лишь отдельные участки в соответствии с логарифмической функцией binFromBar(). Теоретически какая то часть информации будет потеряна за счёт провалов в измерениях. Это выражается в том, что соседние бары могут вести себя как бы независимо друг от друга, не будет общего характера поведения спектра. Чем больше количество баров, тем меньше провалов в измерениях, но тогда появляются артефакты в виде блоков на низких частотах.

 

Диапазон баров:      1-17 17-18 18-19 19-20 20-21 21-22 22-23 23-24 24-25 25-26 26-27 27-28 28-29 29-30 30-31 31-32
Пропущено bins:      0 2 3 3 4 5 6 8 9 11 14 16 19 23 28 33
Провал полосы, Гц:      0 86 129 129 172 215 258 344 388 474 603 689 818 991 1206 1421

 

Из верхней таблицы видно, сколько именно bins пропущено в интервалах между измерениями, и какой ширине полосы частот эти провалы соответствуют. Эти данные нам скоро пригодятся. Но реальный спектр звука музыкальных инструментов всегда широкополосный, амплитуды всех частот в их диапазонах меняются синхронно в соответствии с динамикой исполнения. Поэтому даже на основе выборочных частот можно иметь визуальное представление о каждом из них. Хуже, когда отдельные звуки с узкой полосой попадают в «мертвые зоны», тогда часть амплитуды их сигнала обрезается. К счастью, основная доля таких зон приходится на область обертонов, где энергия звука падает с ростом частоты. Следующая таблица наглядно показывает ширину полосы частот некоторых музыкальных инструментов.

 

Музыкальный инструмент

Частоты, Гц

Обертоны, кГц

Скрипка 196-2100 10
Контрабас 41-260 8
Акустическая гитара 82-1175 12
Электрогитара 82-1570 5
Труба 160-1175 15
Туба 29-440 1,8
Рояль 27-4200 13
Малая флейта 587-4200 10
Гобой 247-1400 12
Кларнет 147-1570 4
Литавры 73-130 4
Бас гитара 41-250 8
Альт 130-1050 8
Большой барабан (бочка) 80-400 4
Малый барабан 120-5000 8
Тарелки 200-8000 10
Томы 80-700 5
Вокал 80-1400 10

 

Есть ещё один неприятный эффект, который называется спектральной утечкой. Аналоговый звуковой сигнал бесконечен во времени, а мы ограничиваем его фреймом в 256 измерений и недостающие промежутки слева и справа заполняем нулями. Такое представление бесконечного сигнала конечным отрезком времени и есть функция Фурье. Если обрезать синусоиду в начале и в конце нашего блока измеряемых данных, то такие резкие перепады приведут к появлению гармоник, кратных частоте синусоиды. Для наглядности возьмём спектр одиночной синусоиды с частотой 700 Гц. Вместо ожидаемой амплитуды одного бара мы получим спектр в виде наложения на основную частоту её затухающих гармонических составляющих, которые искажают общую картину. Свести к минимуму провалы в измерениях и отфильтровать гармоники можно при помощи универсального для обоих случаев метода усреднения соседних bins. Но поскольку звуковые данные считываются по-разному в разных частях шкалы, усреднять мы их тоже будем по-разному.

На линейном участке спектра считываются все bins подряд, поэтому провалов в измерениях нет и в усреднении вроде бы нет необходимости. Но если усреднить по два соседних bins с индексами i и i+1, то мы получим эффект заметной фильтрации гармоник. Любопытно, что если к усреднению добавить ещё и третий bin с индексом i+2, то гармоники появятся снова, и кроме этого станет хуже детализация отображаемых частот. Здесь надо быть очень внимательным.

 

Возникновение гармоник

Фильтрация гармоник

 

На логарифмическом участке провалы в измерениях постепенно увеличиваются. Разделим его на две части, в каждой из которых будем усреднять значения измерений на основе растущего количества пропущенных bins. Здесь важно соблюдать условие, чтобы индексы усредняемых значений, соответствующих одному бару,  по возможности не повторялись больше ни в каких других. Все усредняемые значения левого и правого каналов с индексами i-N, i, i+N складываются и делятся на общий коэффициент, равный количеству binIndex.

// Получаем амплитуды всех баров
for (i = 0; i < NUM_SEGMENTS; i++) {
// Усредняем значения величин, находящихся в каждом индексе segmentTotals
binIndex = binFromBar(i);
var aa; var bb; var cc; var dd; var ee; var ff; var gg; var hh; var ii;
// На линейном участке спектра сводим к минимуму наложение гармоник
if(binIndex < 17) {
cc = left[binIndex] + right[binIndex];

dd = left[binIndex+1] + right[binIndex+1];
valuesAverage = (cc+dd) / 4;
}
// В 1-й части логарифмического участка увеличиваем количество измерений
if(binIndex > 16 && binIndex < 64) {
aa = left[binIndex-2] + right[binIndex-2];
bb = left[binIndex-1] + right[binIndex-1];
cc = left[binIndex] + right[binIndex];
dd = left[binIndex+1] + right[binIndex+1];
ee = left[binIndex+2] + right[binIndex+2];
valuesAverage = (aa+bb+cc+dd+ee) / 10;
}
// Во 2-й части логарифмического участка добавляем измерений ещё больше
if(binIndex > 63) {
aa = left[binIndex-4] + right[binIndex-4];
bb = left[binIndex-3] + right[binIndex-3];
cc = left[binIndex-2] + right[binIndex-2];
dd = left[binIndex-1] + right[binIndex-1];
ee = left[binIndex] + right[binIndex];
ff = left[binIndex+1] + right[binIndex+1];
gg = left[binIndex+2] + right[binIndex+2];
hh = left[binIndex+3] + right[binIndex+3];
ii = left[binIndex+4] + right[binIndex+4];
valuesAverage = (aa+bb+cc+dd+ee+ff+gg+hh+ii) / 18;
}
valuesAverage = Quadratic.easeOut(valuesAverage, 0, 1, 1); // Функция инерции обратного хода баров
// Выводим новую высоту усредненных значений
newBarHeight = valuesAverage * BAR_MAX_HEIGHT;
if(valuesAverage > 1) valuesAverage = 1;
// Обновляем один из наших баров
bar = bars[i] as Bar;
bar.update(valuesAverage);
}

Когда во второй части логарифмического участка мы добавляем количество измерений, то тем самым ещё и увеличиваем точность в области максимальной частоты 11 кГц, о которой я упоминал выше. Провалы в измерениях теперь сведены к минимуму, и этого уже достаточно для плавного отображения спектра с хорошей детализацией. Класс SpectrumAnalyzer практически готов, осталось добавить пару деталей. Напишем внутренний класс Bar, в котором рисуется вся графика. Здесь же зададим нашим барам инерцию движения, чтобы при резких скачках амплитуды они двигались плавно, не раздражая зрение.

import flash.display.Sprite;
import flash.display.Graphics;
import fl.motion.Color;
import flash.geom.Matrix;
import flash.display.GradientType;

class Bar extends Sprite {

private var maxHeight : Number;
private var barWidth : Number;
private var currentValue : Number;
private var color_all : Number;
private var bar_number : Number; // Номер бара

public function Bar(maxHeight : Number, barWidth : Number, color_all : Number, bar_number : Number) {
this.maxHeight = maxHeight;
this.barWidth = barWidth;
this.color_all = color_all;
this.bar_number = bar_number; // Передаём сюда номер текущего бара
currentValue = 0;
update(currentValue);
}

public function update(value : Number) : void {
var g : Graphics = graphics;
g.clear();

// Задаём инерцию прямого и обратного хода бара
if(value >= currentValue) {currentValue += (value - currentValue) / 1.8; // Инерция прямого хода
} else {currentValue += (0 - currentValue) / 16; // Инерция обратного хода
if(currentValue < 0) currentValue = 0; }

// Рисуем бар на основе текущей установки
var newHeight : Number = Math.round(currentValue * maxHeight);
g.beginFill(color_all);
g.drawRect(0, maxHeight - newHeight, barWidth, newHeight); //
x, y, ширина, высота
g.endFill();

// Рисуем пики
g.beginFill(color_all);
g.drawRect(0, maxHeight - newHeight, barWidth, 1);
g.endFill();
} }


Имеет смысл поэкспериментировать с коэффициентом инерции баров. Чем больше знаменатель в строке currentValue += (0 - currentValue) / 16, тем больше инерция движения баров. Здесь даны значения для 40 fps, но они могут быть и другими, как вам больше нравится. В любом случае, этот параметр будет зависеть от частоты кадров. Вот и всё. Осталось подключить наш класс к Flash плееру. Для этого на основной сцене документа fla пишем:

var colly = 0xFFFFFF; // Цвет наших баров
import SpectrumAnalyzer; // Импортируем внешний класс
var myAnalyzer: SpectrumAnalyzer = new SpectrumAnalyzer(colly); // Создаём экземпляр анализатора с параметром цвета
addChild(myAnalyzer); // Добавляем его на сцену
myAnalyzer.x=10; // Положение спрайта в пикселях по горизонтали, счёт от крайней левой точки плеера
myAnalyzer.y=10; // Положение спрайта в пикселях по вертикали, счёт от крайней верхней точки плеера

Необходимо помнить, что общая высота спрайта определяется максимальной высотой бара, а ширина, - шириной одного бара, умноженной на их количество, плюс суммарное расстояние между барами. При настройках, указанных выше, высота равняется 80, а ширина 8*32+31=287 пикселям соответственно. Если спрайт вылезает за границы плеера, нужно либо уменьшить соответствующие размеры баров, либо подвигать спрайт по сцене, либо увеличить размеры самого плеера. Я специально не стал задавать цвет баров в классе, а вывел установку в основной fla документ. Теперь можно менять цвет анализатора спектра на любой по желанию, например кнопкой в цикле: белый - голубой - оранжевый - зелёный.

var counter_color = 1; // Исходное состояние счётчика цветов
import SpectrumAnalyzer; // Импортируем внешний класс
var myAnalyzer_1 = new SpectrumAnalyzer(0xFFFFFF); // Экземпляр с белым цветом
var myAnalyzer_2 = new SpectrumAnalyzer(0x9AFFFE); // Экземпляр с голубым цветом
var myAnalyzer_3 = new SpectrumAnalyzer(0xFFA500); // Экземпляр с оранжевым цветом
var myAnalyzer_4 = new SpectrumAnalyzer(0x00FF00); // Экземпляр с зелёным цветом
addChild(myAnalyzer_1); // Добавляем на сцену экземпляр с белым цветом
myAnalyzer_1.x = myAnalyzer_2.x = myAnalyzer_3.x = myAnalyzer_4.x = 10; // Координаты спрайтов по горизонтали
myAnalyzer_1.y = myAnalyzer_2.y = myAnalyzer_3.y = myAnalyzer_4.y = 10; // Координаты спрайтов по вертикали

// Вешаем на событие нажатия кнопки color_btn функцию изменения цвета color()
color_btn.addEventListener(MouseEvent.MOUSE_DOWN, color);

function color(e: Event): void { // Объявляем функцию изменения цвета
counter_color++; // Каждое нажатие кнопки увеличивает значение счётчика на 1
if(counter_color > 4) {counter_color = 1} // Ограничиваем отсчёт 4-мя значениями по количеству цветов
if(counter_color == 1) { // Если состояние счётчика равно 1
removeChild(myAnalyzer_4); // Удаляем со сцены предыдущий экземпляр цвета
addChild(myAnalyzer_1); // Добавляем на сцену текущий экземпляр цвета
}
if(counter_color == 2) { // Здесь и далее всё аналогично
removeChild(myAnalyzer_1);
addChild(myAnalyzer_2);
}
if(counter_color == 3) {
removeChild(myAnalyzer_2);
addChild(myAnalyzer_3);
}
if(counter_color == 4) {
removeChild(myAnalyzer_3);
addChild(myAnalyzer_4);
} }

А теперь попробуем немного усложнить задачу. Вместо обычного цвета сделаем градиентную заливку баров, - она меньше утомляет зрение и смотрится гораздо эффектнее. Для этого при создании внутреннего класса Bar импортируем класс flash.display.GradientType. За основу возьмём любой базовый цвет. Он может быть как в шестнадцатеричной, так и в десятичной системе исчисления. Вот несколько примеров.

 

Цвет

Шестнадцатеричный вид

Десятичный вид

Белый 0xFFFFFF 16777215
Голубой 0x9AFFFE 10158078
Оранжевый 0xFFA500 16753920
Зелёный 0x00FF00 652800

 

Для градиентной заливки нужны значения максимальной и минимальной яркости одного цвета. Например, значения 0xFFFFFF и 0x222222 соответствуют 100% и 13% яркости белого цвета. Но как быть, если мы хотим менять цвета при помощи одной кнопки, и одновременно должны меняться оба значения для любого цвета? Оказывается, для получения минимальной яркости цвета в градиенте достаточно просто уменьшить на определённую величину параметр, задающий цвет. На эту мысль меня натолкнули значения цветов в десятичной системе исчисления. Немного поэкспериментировав, я пришёл к выводу, что если разделить параметр color_all на 3, то получится меньшее приемлемое значение яркости любого текущего цвета. Может быть, это не совсем научный подход, и не очень понятно, в чём здесь дело, но он работает. Причём это справедливо как для шестнадцатеричной, так и для десятичной формы цвета. Теперь, когда задача определена, можно писать код.

// Задаём текущую высоту бара
var newHeight : Number = Math.round(currentValue * maxHeight);
// Задаём интерполяцию начального и конечного цвета бара
var newColor : uint = Color.interpolateColor(color_all / 3, color_all, currentValue);
var matrix : Matrix = new Matrix();
// Создаём градиентный бокс
matrix.createGradientBox(barWidth, newHeight, 90 * Math.PI / 180, 0, maxHeight - newHeight);
// Создаём массивы со значениями цвета и прозрачности
var colors:Array = [newColor, color_all / 3];
var alphas:Array = [1, 1];
var ratios:Array = [0, 0xFF];
// Рисуем бар на основе всех установок
g.beginGradientFill(GradientType.LINEAR, colors, alphas, ratios, matrix);
g.drawRect(0, maxHeight - newHeight, barWidth, newHeight);
// x, y, ширина, высота
g.endFill(); // Завершаем рисование

 

Градиентная заливка баров

 

Если вместо градиентной заливки нужна обычная однотонная, то меняем параметр color_all / 3 на  color_all / 1, либо изначально создаём один экземпляр анализатора myAnalyzer с белым цветом, и меняем цвет при помощи класса ColorTransform(). Это будет выглядеть следующим образом.

if(counter_color == 1) { // Если состояние счётчика равно 1
var myColorTransform_1 = new ColorTransform(); myColorTransform_1.color = 0xFFFFFF; // Создаём белый цвет
myAnalyzer.transform.colorTransform = myColorTransform_1; // Меняем цвет анализатора на белый
}
if(counter_color == 2) { // Если состояние счётчика равно 2
var myColorTransform_2 = new ColorTransform(); myColorTransform_2.color = 0x9AFFFE; // Создаём голубой цвет
myAnalyzer.transform.colorTransform = myColorTransform_2; // Меняем цвет анализатора на голубой
}
if(counter_color == 3) { // Если состояние счётчика равно 3
var myColorTransform_3 = new ColorTransform(); myColorTransform_3.color = 0xFFA500; // Создаём оранжевый цвет
myAnalyzer.transform.colorTransform = myColorTransform_3; // Меняем цвет анализатора на оранжевый
}
if(counter_color == 4) { // Если состояние счётчика равно 4
var myColorTransform_4 = new ColorTransform(); myColorTransform_4.color = 0x00FF00; // Создаём зелёный цвет
myAnalyzer.transform.colorTransform = myColorTransform_4; // Меняем цвет анализатора на зелёный
}

Но для градиентной заливки метод ColorTransform не подходит, т.к. он принудительно задаёт только один цвет, игнорируя полутона. В этом случае придётся создавать несколько экземпляров анализатора.

Шкала, отображающая частоты, делается на основе программного текстового блока TextField(). Но об этом чуть позже. А пока давайте попробуем сделать обычный измеритель уровня сигнала. Возьмём наш класс SpectrumAnalyzer, слегка модифицируем его и назовём LevelMeter. В отличии от анализатора спектра, здесь нам нужны всего два бара, отображающих уровни сигналов левого и правого каналов. Метод computeSpectrum() будет выглядеть так:

SoundMixer.computeSpectrum(bytes, false, 0);

Параметр FFT ставим в false, т.е. считываем не спектр частот, а комплексную амплитуду сигнала, и все наши 256 измерений теоретически должны быть одинаковыми. Но вот что мы увидим в окне Output, если при чтении данных, например левого канала, напишем строчку trace(Math.abs(left[i])):

0.221588134765625
0.11798095703125
0.07818603515625
0.05633544921875
0.0184326171875

Здесь взяты наугад значения пяти соседних bins из 256, и все они разные. Видимо, так происходит потому, что данные хоть и не несут информацию о спектре частот, но измерены с разницей во времени. Альтернативным и более простым способом чтения амплитуд каналов является метод soundChannel.leftPeak и soundChannel.rightPeak. Но применить его во внешнем классе мне не удалось, т.к. данные считываются через SoundMixer, а смена треков и соответствующих им soundChannel происходит в самом плеере, а не во внешнем классе. Здесь моих знаний AS3 не хватило, но выход всё равно нашёлся. Нам нужно усреднить все 256 значений computeSpectrum, поэтому вначале мы их суммируем.

// Считываем данные левого канала
var i:int = 0;
var sum_l = 0; // Обнуляем переменную, хранящую сумму 256 отсчётов левого канала
while (i < CHANNEL_LENGTH) { // Проходим в цикле все значения
left[i] = bytes.readFloat(); // Считываем в вектор каждое из них
sum_l += Math.abs(left[i]); // Суммируем все значения левого канала
i++;
}
// Аналогично делаем то же самое для правого канала
i = 0;
var sum_r = 0;
while (i < CHANNEL_LENGTH) {
right[i] = bytes.readFloat();
sum_r += Math.abs(right[i]);
i++;
}var sum_ll = sum_l / 256; // Усредняем данные левого канала
var sum_rr = sum_r / 256; // Усредняем данные правого канала

В отличии от анализатора спектра, индикаторы здесь показывают раздельные уровни сигналов левого и правого каналов, поэтому усреднять значения векторов left и right нельзя.

// Создаём логарифмическую шкалу отсчётов
var rms_l = Math.floor((6/Math.LN2)*Math.log(Math.sqrt(sum_ll))*100)/100;
var rms_r = Math.floor((6/Math.LN2)*Math.log(Math.sqrt(sum_rr))*100)/100;
// Растягиваем шкалу, добиваясь чувствительности на низких амплитудах
var rms_xl = Math.exp(Math.log(1.07)*2*rms_l); // Здесь можно поиграться с цифрами
var rms_xr = Math.exp(Math.log(1.07)*2*rms_r);

// Получаем амплитуды баров
for (var ii = 0; ii < NUM_SEGMENTS; ii++) {
if(ii == 0) {valuesAverage = rms_xl}; // Амплитуда бара левого канала
if(ii == 1) {valuesAverage = rms_xr}; // Амплитуда бара правого канала
valuesAverage = Quadratic.easeOut(valuesAverage, 0, 1, 1);
// Выводим новую высоту усредненных значений
newBarHeight = valuesAverage * BAR_MAX_HEIGHT;
if(valuesAverage > 1) valuesAverage = 1;
// Обновляем один из наших баров
bar = bars[ii] as Bar;
bar.update(valuesAverage);
} }
;

// Рисуем бар измерителя уровня и поворачиваем его горизонтально
g.drawRect(0, 0, newHeight, barWidth);
// x, y, ширина, высота
g.endFill(); // Завершаем рисование

Настроить шкалу чувствительности на низких амплитудах можно при помощи тестовых сигналов. На этом сайте в режиме онлайн можно сгенерировать файлы любой частоты и амплитуды, правда только в формате wav. Их надо будет потом дополнительно конвертировать в mp3. Теперь можно рисовать бары. Обозначим каждый из них латинскими буквами L (Left Channel, или левый канал) и R (Right Channel, или правый канал), как это принято в дизайне электроники. Для этого создадим текстовые блоки, которые удобно писать именно во внешнем классе, т.к. в дальнейшем весь спрайт можно будет перемещать в любом направлении, менять его цвет, делать невидимым, и т.д. Этим мы сэкономим массу времени.

// Делаем программные текстовые блоки
var field_l = new TextField(); // Текстовый блок левого канала
var field_r = new TextField(); // Текстовый блок правого канала
field_l.text = "L"; // Вписываем букву L
field_r.text = "R"; // Вписываем букву R
field_l.selectable = false; // Делаем текст невыделяемым
field_r.selectable = false;
flash.text.AntiAliasType.ADVANCED; // Прогрессивный метод сглаживания
field_l.sharpness = 400; // Четкость контуров текста от -400 до 400
field_r.sharpness = 400;
var format = new TextFormat(); // Форматирование текста
format.font = "Arial"; // Тип шрифта
format.size = 7; // Размер шрифта (высота в пикселях)
format.bold = true; // Выбираем жирный шрифт
field_l.setTextFormat(format); // Применяем форматирование
field_r.setTextFormat(format);
addChild(field_l); // Добавляем программный текст на сцену
addChild(field_r);
field_l.textColor = BAR_COLOR; // Цвет текста
field_r.textColor = BAR_COLOR;
field_l.x = -11; // Координаты текста относительно бара
field_l.y = -3;
field_r.x = -11;
field_r.y = -3 + (BAR_WIDTH + BAR_SPACING);

Чтобы придать нашему измерителю уровня сигнала натуральный вид, стилизуем его под матричный индикатор. Будем рисовать каждый сегмент отдельно и задавать ему переменный коэффициент прозрачности, в зависимости от амплитуды сигнала на соответствующем участке шкалы. Зададим сегментам начальную прозрачность, равную 20 процентам, чтобы они были слегка видны. Это придаст анимации полное сходство с настоящим электролюминесцентным индикатором. Допустим, ширина каждого сегмента равна двум пикселям, расстояние между соседними сегментами один пиксель, и при общей ширине индикатора в 53 пикселя получится 18 сегментов с шагом в три пикселя. Максимальная амплитуда измеряемого сигнала во флэш равна единице, разобьём её на количество сегментов (1/18=0.055), и будем ступенчато суммировать это значение, начиная со второго сегмента, задавая пороговое значение амплитуды каждому из них. Если количество сегментов другое, пороговые значения необходимо пересчитать.

var vv_1; if(currentValue > 0.01) {vv_1 = 1} else {vv_1 = 0.2} // Переменная прозрачность 1-го сегмента, пороговая амплитуда минимальна
g.beginFill(color_all, vv_1); g.drawRect(0, 0, 2, barWidth); // Рисуем 1-й сегмент в начале шкалы (x, y, ширина, высота)
var vv_2; if(currentValue > 0.055) {vv_2 = 1} else {vv_2 = 0.2} // Переменная прозрачность 2-го сегмента, амплитуда > 0.055
g.beginFill(color_all, vv_2); g.drawRect(3, 0, 2, barWidth); // Рисуем 2-й сегмент со сдвигом вправо на 3 пикселя относительно 1-го
var vv_3; if(currentValue > 0.11) {vv_3 = 1} else {vv_3 = 0.2} // Переменная прозрачность 3-го сегмента, амплитуда > 0.11
g.beginFill(color_all, vv_3); g.drawRect(6, 0, 2, barWidth); // Рисуем 3-й сегмент со сдвигом вправо на 3 пикселя относительно 2-го

При превышении порогового значения амплитуды сигнала прозрачность сегмента ступенчато меняется с 20 до 100 процентов, и он «зажигается» как настоящий. Инерция падения амплитуды здесь также имеет место, поэтому при мгновенном исчезновении сигнала сегменты будут постепенно «гаснуть» один за другим. Минимальная прозрачность и цвет сегментов зависят от цвета фона плеера.

 

Пример флэш-плеера с матричным индикатором

 

 

Аналогичным образом поступим, если хотим сделать матричный анализатор спектра. Только сегменты расположим уже не горизонтально, а вертикально. Здесь весьма эффектно будет смотреться функция Peak Holder, которая на короткий момент задерживает пики сигнала, давая наглядное представление об общем характере спектра. Чтобы её реализовать, продублируем отрисовку основного бара, но не с переменной высотой, а с фиксированной, равной одному пикселю, и будем перемещать его по вертикали в такт со звуком с гораздо большей инерцией обратного хода. Поскольку эффект инерции основан на методе Quadratic.easeOut(), скорость движения от максимальной плавно снижается до нуля, поэтому пики всегда будут подниматься быстро, а опускаться медленно. Коэффициент инерции сделаем зависимым от амплитуды сигнала: чем она выше, тем дольше зависают пики, а чем ниже, тем быстрее они падают вниз.

var hold = currentValue * 500; // Задаём переменный коэффициент инерции пиков, множитель подбирается экспериментально
if(value >= currentValue_2) {
currentValue_2 = value;
} else {
currentValue_2 += (0 - currentValue_2) / hold; // Применяем инерцию пиков
if(currentValue_2 < 0)
currentValue_2 = 0;
}
// Рисуем Peak Holder
var newHeight_2 : Number = Math.round(currentValue_2 * maxHeight); // Переменная, задающая положение пика по вертикали
// Чтобы пики не сливались с сегментами и не нарушали общую геометрию, при сближении делаем их невидимыми
var pa; if((currentValue_2 - currentValue) < 0.02) {pa = 0} else {pa = 1} // Переменная pa (peak alpha) задаёт прозрачность пиков
if(currentValue < 0.02) {pa = 1}; // Делаем пики видимыми при минимальной амплитуде сигнала или его отсутствии
g.beginFill(color_all, pa); // Цвет и прозрачность пиков
g.drawRect(0, maxHeight - newHeight_2, barWidth, 1); // Рисуем пики (x, y, ширина, высота)
g.endFill(); // Завершаем рисование

Поскольку в матричном анализаторе отсутствует градиентная заливка, при смене цвета лучше использовать один экземпляр анализатора и метод ColorTransform(), как это было показано выше.

 

Матричный анализатор спектра с функцией Peak Holder

 

 

Можно сделать красивый измеритель уровня стереосигнала, в котором сочетается градиентная заливка и функция Peak Holder. Для этого нужно задать интерполяцию начального и конечного цвета.

// Задаём интерполяцию начального и конечного цвета бара, в данном случае это зелёный и жёлтый цвет
var newColor : uint = Color.interpolateColor(0x00FF00, 0xFFFF00, currentValue);

 

Вариант плеера с градиентным индикатором и функцией Peak Holder

 

Классы SrectrumAnalyzer с градиентной заливкой и MatrixAnalyzer с функцией Peak Holder импортируются одинаково, для этого достаточно указать в скобках параметр цвета. Для наглядности посмотрим, как это делается.

import SpectrumAnalyzer; // Импортируем внешний класс с градиентной заливкой
var myAnalyzer = new SpectrumAnalyzer(0xFFFFFF); // Создаём экземпляр с параметром цвета
addChild(myAnalyzer); // Добавляем его на сцену, если координаты не указаны, он появится в левом верхнем углу

import MatrixAnalyzer; // Импортируем внешний класс матричного анализатора с функцией Peak Holder
var myAnalyzer_1 = new MatrixAnalyzer(0xFFFFFF,); // Создаём экземпляр с параметром цвета
addChild(myAnalyzer_1); // Добавляем его на сцену

Внешние классы, которые мы с вами создали, получились достаточно универсальными для работы со звуком. На базе LevelMeter также можно сделать стрелочный VU Meter. Основное отличие здесь заключается в том, что вместо амплитуды бара будет меняться угол поворота стрелки индикатора. В первую очередь нужно определить крайние углы её отклонения от исходного вертикального положения. У меня получилось -45° и 10°, что ограничивает общий угол поворота стрелки в 55°. Именно это значение нужно присвоить параметру maxHeight вместо высоты бара, т.е. maxHeight = 55.

var newHeight : Number = Math.round(currentValue * maxHeight); // Зависимость угла поворота стрелки от амплитуды сигнала
g.beginFill(0x07FBFF);  // Цвет стрелки
g.drawRect(0, 0, 1, 71); // Рисуем стрелку (x, y, ширина, высота)
g.endFill(); // Завершаем рисование
this.rotation = (-45 + newHeight) + 180; // Задаём стрелке динамическое отклонение от исходного угла -45° и поворачиваем её на 180°

Поскольку начальные координаты стрелки, вокруг которых она должна вращаться, равны нулю, а отрисовка идёт вниз на 71 пиксель, то она будет отображаться перевёрнутой вниз. Для традиционного её расположения прибавляем ко всем нашим расчётам 180°. Также следует помнить, что плавность движения стрелок должна быть гораздо больше, чем в предыдущих классах. Ведь настоящие стрелочные индикаторы представляют из себя механические инерционные системы, в которых индукционная катушка с закреплённой на ней стрелкой вращается в магнитном поле вокруг своей оси. Чтобы смоделировать этот сложный процесс, зададим стрелке инерцию прямого и обратного хода. Увеличение знаменателя здесь увеличивает инерцию, и наоборот.

if(value >= currentValue) {
currentValue += (value - currentValue) / 2.3; // Инерция прямого хода стрелки
} else {
currentValue += (0 - currentValue) / 22; // Инерция обратного хода стрелки
if(currentValue < 0)
currentValue = 0;
}

Графику индикатора можно подгружать во внешний класс при помощи лоадера. Но на мой взгляд это не очень удобно, т.к. она должна всегда располагаться в одной папке с конечным swf файлом, и кроме того, работа с макетом плеера будет проходить «вслепую». Я импортировал изображение индикатора в основной fla документ. Таким образом, во внешнем классе у нас остаются только нарисованные стрелки. Чтобы хвосты стрелок не вылезали за пределы рабочей области индикаторов, сделаем их невидимыми. Для этого на основной сцене плеера нарисуем маску, сквозь которую будут видны только те части стрелок, которые мы хотим видеть. Высота маски должна соответствовать внутренней высоте рамки, обрамляющей индикатор. Её ширину можно сделать общей для двух каналов, т.к. в классе у нас две стрелки. При импорте класса, который мы немного модифицируем, в скобках нужно указать необходимые параметры, - это удобно для подгона стрелок под любую картинку индикатора.

import VUMeter; // Импортируем внешний класс
// Создаём экземпляр стрелок (цвет, начальный угол, общий угол, ширина, высота, расстояние между стрелками)
var pins = new VUMeter(0xFFFFFF, -45, 55, 1, 71, 128);
addChild(pins); // Добавляем стрелки на сцену
var pins_mask = new Sprite; // Создаём спрайт маски
pins_mask.graphics.beginFill(0xFFFFFF, 0); // Цвет и прозрачность маски
pins_mask.graphics.drawRect(275, 27, 250, 60); // Рисуем маску и подгоняем её ко всему индикатору (x, y, ширина, высота)
pins_mask.graphics.endFill(); // Завершаем рисование
addChild(pins_mask); // Добавляем маску на сцену
pins.mask = pins_mask; // Маскируем хвосты стрелок

 

VU Meter без маски и с маской

Определение цвета в Jasc Animation Shop

Графика для Magic Eye

 

Для точного определения цвета элементов плеера, например стрелок индикатора, я использую бесплатную программу для создания GIF-анимации Jasc Animation Shop. Запускаем программу, открываем в ней графический файл, из меню Animation выбираем команду Replace Color. Дальше инструментом Пипетка кликаем в том месте, цвет которого хотим определить. Затем этой же Пипеткой кликаем в изменившем свой цвет окошке возле Old Color. В появившемся новом окне Color в поле HTML Code мы увидим значение искомого цвета в шестнадцатеричной форме.

Напоследок попробуем сделать старинный индикатор в стиле Magic Eye, которые когда-то применялись для настройки ламповых радиоприёмников и регулировки уровня записи в магнитофонах. Для этого не нужно создавать внешний класс, весь код будет находиться внутри функции onEnterFrame() основного fla документа плеера. Графику берём из архива в конце статьи, разбираем её в Фотошопе на элементы и сохраняем в виде отдельных файлов в формате .png нужного размера для обеспечения прозрачности фона. Далее импортируем эти картинки в плеер и располагаем их в виде слоёв один над другим по центру, т.е. с одинаковыми координатами. В самом низу будет серый круг, над ним левый зелёный полукруг, выше него правый зелёный полукруг, и на самом верху центральный чёрный элемент с нитью накала и бликом на стекле. Для идеальной центровки изображений все они должны иметь одинаковую ширину и высоту с чётным количеством пикселей (изначальный размер картинок 100x100). Левый и правый полукруги преобразуем в два мувиклипа с присвоением им имён (Instance name) соответственно green_left_amp и green_right_amp. Зададим начальный угол одному из них -45°, а другому 45°. Тем самым они перекроют друг друга, образуя зелёный круг с вырезом наверху, угол которого будет динамически меняться в такт со звуком. Для чтения амплитуд каналов используем уже знакомые нам методы soundChannel.leftPeak и soundChannel.rightPeak. Подобные ламповые индикаторы, как правило, отображали амплитуду монофонического сигнала, поэтому переменной peak_lr присваиваем суммарное значение амплитуд обоих каналов. Поскольку максимальное значение амплитуды сигнала во флэш равно 1, значение переменной peak_lr умножаем на 45, по числу градусов максимального угла отклонения.

function onEnterFrame(event: Event): void {
var peak_l = soundChannel.leftPeak; // Амплитуда левого канала от 0 до 1
var peak_r = soundChannel.rightPeak; // Амплитуда правого канала от 0 до 1
var peak_lr = (peak_l + peak_r) / 2; // Общая амплитуда каналов
green_left_amp.rotation = -45 + (peak_lr * 45); // Динамическое вращение левого полукруга вправо от угла -45°
green_right_amp.rotation = 45 - (peak_lr * 45); // Динамическое вращение правого полукруга влево от угла 45°
}

 

Плеер с ламповым индикатором Magic Eye

 

Плавность движения и чувствительность созданных нами индикаторов получились как у ностальгической аппаратуры 80-х, 90-х годов. Примеры можно посмотреть здесь. Надеюсь, что урок вам понравился. Удачи!

P.S. Не успел я толком освоить ActionScript 3, а разработчики уже заявили о сворачивании флэш технологии. К 2020 году Adobe Flash Player перестанет обновляться и браузеры не будут его поддерживать. Все перейдут на более прогрессивную открытую платформу HTML5, которая лучше подходит для мобильных устройств. Трудно сказать, как это скажется на дизайне. Очевидно одно, - придётся снова переучиваться. Ведь для пользователя важен конечный результат, а как он будет достигнут, - ему это совсем не интересно.

Скачать: meters.rar (43 Кб)

В архиве:
SpectrumAnalyzer.as
LevelMeter.as
LevelMeterGradient.as
MatrixAnalyzer.as
VUMeter.as
Magic Eye Template.psd
Readme.txt

Последнее обновление 30 марта 2018 г.