Любой аппарат под управлением Windows Phone имеет встроенный динамик и разъем для наушников, и было бы странно, если бы их использование ограничивалось только телефонными звонками. К счастью, приложения Windows Phone могут использовать аудиофункции для воспроизведения музыки и других звуков. Как я демонстрировал в ряде предыдущих статей, приложение Windows Phone может проигрывать MP3- или WMA-файлы, хранящиеся в музыкальной библиотеке пользователя, или воспроизводить файлы, скачиваемые из Интернета.
Приложение Windows Phone также может динамически генерировать звуковые аудиосигналы, и этот метод называется потоковой передачей аудио (audio streaming). Эта процедура создает чрезвычайно большой объем данных: для звука качества CD нужно генерировать 16-битные выборки с частотой 44 100 раз в секунду как для левого, так и для правого каналов, или 176 400 байтов в секунду!
Но потоковое воспроизведение аудио — метод с большими возможностями. Если вы скомбинируете его с мультисенсорным вводом, то сможете превратить свой смартфон в электронный музыкальный инструмент. А что может быть забавнее этого?
Что такое терменвокс
Один из самых первых электронных музыкальных инструментов — терменвокс (theremin) — был создан русским изобретателем Львом Терменом (Léon Theremin) в 20-х годах прошлого века. Играющий на терменвоксе на самом деле не касался этого инструмента. Вместо этого музыкант водил руками вдоль двух антенн, которые позволяли раздельно управлять громкостью и высотой звука. В результате получался пробирающий до дрожи вибрирующий звук, похожий на завывания привидения и плавно переходящий от одной ноты к другой, — он должен быть знаком вам по таким фильмам, как «Завороженный» («Spellbound») и «День, когда Земля остановилась» («The Day the Earth Stood Still»), его также использовали некоторые рок-группы, и еще его можно услышать в четвертом сезоне сериала «Теория Большого Взрыва» («The Big Bang Theory»), серия 12. (В противоположность распространенному мнению, терменвокс никогда не использовался в музыкальной теме «Звездный путь» [«Star Trek»].)
Можно ли превратить Windows Phone в ручной терменвокс? Именно это я и решил выяснить.
Классический терменвокс генерирует звук по методу гетеродинирования (смешивания), при котором комбинирование двух высокочастотных волновых сигналов (waveforms) создает разностный тон (difference tone) в диапазоне звуковых частот (audio range). Но этот метод непрактичен, когда волновые сигналы генерируются программным обеспечением на компьютере. Гораздо эффективнее генерировать волновые сигналы прямо в диапазоне звуковых частот.
Немного повозившись с идеей использования ориентации смартфона для управления звуком, а потом попытавшись программировать захват и интерпретацию движений рук камерой смартфона а-ля Kinect, я остановился на куда более прозаичном подходе: точка касания пальца на экране смартфона является точкой в двухмерном пространстве координат, что позволяет программе использовать одну ось для частоты, а другую — для амплитуды.
Чтобы правильно сделать такую штуку, нужно немного понимать, как мы воспринимаем музыкальные звуки.
Пиксели, звуки и амплитуды
Благодаря новаторской работе Эрнста Вебера (Ernst Weber) и Густава Фехнера (Gustav Fechner) в 19-м столетии нам известно, что чувствительность человеческого уха имеет логарифмическую, а не линейную зависимость. Линейные приращения громкости входного испытательного сигнала не воспринимаются как равные. Вместо этого мы воспринимаем равными изменения, пропорциональные громкости, часто выражаемые как относительное увеличение или уменьшение. (Этот феномен вызван особенностями наших органов чувств. Например, мы инстинктивно ощущаем, что разница между одним и двумя долларами намного больше, чем между 100 и 101 долларами.)
Люди чувствительны к диапазону звуковых частот от 20 до 20 000 Гц (примерно), но восприятие частот тоже нелинейно. Во многих культурах высота музыкальных звуков структурируется октавами, каждая из которых удваивает частоту. Когда вы поете «Somewhere Over the Rainbow», два слога первого слова представляют собой октаву независимо от того, какой высоты ноту вы берете первой: от 100 до 200 Гц или от 1000 до 2000 Гц. Таким образом, диапазон человеческого слуха состоит приблизительно из 10 октав.
Октава называется так потому, что в западной музыке она охватывает восемь буквенных нот в гамме, где последняя нота октавы выше первой: A, B, C, D, E, F, G, A (минорная гамма) или C, D, E, F, G, A, B, C (мажорная гамма).
Из-за того как формируются ноты, они не кажутся на слух равно отстоящими друг от друга. Гамма, в которой все ноты равно удалены, требует еще пяти нот, что в итоге дает 12 (если не считать первую ноту дважды): C, C#, D, D#, E, F, F#, G, G#, A, A# и B. Каждый из этих интервалов известен как полутон, и они располагаются с равными промежутками (как это делается при распространенной настройке с равной темперацией); каждая нота имеет частоту, равную корню двенадцатой степени от двух (примерно 1,059) частоты ноты, расположенной под ней.
В свою очередь полутон можно разделить на 100 центов (это сотые доли в музыке). В октаве 1200 центов. Мультипликативный промежуток (multiplicative step) между центами равен корню 1200-й степени от двух, или 1,000578. Разумеется, чувствительность человеческого слуха к изменению частоты варьируется в широких пределах, но в целом считается равной примерно пяти центам.
Этот обзор физики и математики музыки необходим, потому что программе терменвокса требуется преобразовывать положение пальца на пикселях экрана в частоту. Это преобразование нужно выполнять так, чтобы каждая октава соответствовала равному количеству пикселей. Если вы решим, что у терменвокса должен быть диапазон в четыре октавы, соответствующий 800 пикселям длины экрана Windows Phone в ландшафтной ориентации, то каждой октаве будут соответствовать 200 пикселей, или шесть центов на пиксель, что отлично укладывается в пределы чувствительности человеческого уха.
Амплитуда волнового сигнала определяет, как вы воспринимаем громкость, и эта зависимость тоже логарифмическая. Децибел определяется как значение 10, умноженное на натуральный логарифм (по основанию 10) отношения двух уровней мощности (power levels). Поскольку мощность волнового сигнала является квадратом амплитуды, разница между двумя амплитудами в децибелах равна: {Для верстки: это формула}
Для звука качества CD используются 16-битные выборки, что дает диапазон между максимальной и минимальной амплитудами равный 65 536. Возьмите натуральный логарифм значения 65 536, умножьте его на 20 и вы получите диапазон в 96 децибел.
Один децибел составляет приблизительно 12% увеличения амплитуды. Человеческое ухо гораздо менее чувствительно к изменениям амплитуды по сравнению с изменениями в частоте. Чтобы люди заметили изменение громкости требуется ее повышение или понижение на несколько децибел, так что это можно легко втиснуть в размер экрана Windows Phone, равный 460 пикселям.
Реализация
В пакете кода для этой статьи содержится единственное решение Visual Studio под названием MusicalInstruments. Проект Petzold.MusicSynthesis — это DLL, которая главным образом включает файлы, обсуждавшиеся в прошлой статье. Проект приложения Theremin состоит из одной ландшафтной страницы.
Какой тип волновых сигналов должен генерировать терменвокс? В теории, это синусоидальная волна, но на практике она несколько искаженная, и, если вы попробуете поискать информацию по этому вопросу в Интернете, вы не обнаружите особого консенсуса. В своей версии я использую правильные синусоидальные волны — мне кажется это вполне резонным.
Как показано на рис. 1, в файле MainPage.xaml.cs определено несколько константных значений и вычисляется две целочисленных переменных, управляющих тем, как пиксели экрана сопоставляются с нотами.
Рис. 1. Вычисление амплитуды и частоты для терменвокса
public partial class MainPage : PhoneApplicationPage
{
static readonly Pitch MIN_PITCH = new Pitch(Note.C, 3);
static readonly Pitch MAX_PITCH = new Pitch(Note.C, 7);
static readonly double MIN_FREQ = MIN_PITCH.Frequency;
static readonly double MAX_FREQ = MAX_PITCH.Frequency;
static readonly double MIN_FREQ_LOG2 = Math.Log(MIN_FREQ) / Math.Log(2);
static readonly double MAX_FREQ_LOG2 = Math.Log(MAX_FREQ) / Math.Log(2);
...
double xStart; // The X coordinate corresponding to MIN_PITCH
int xDelta; // The number of pixels per semitone
void OnLoaded(object sender, EventArgs args)
{
int count = MAX_PITCH.MidiNumber - MIN_PITCH.MidiNumber;
xDelta = (int)((ContentPanel.ActualWidth - 4) / count);
xStart = (int)((ContentPanel.ActualWidth - count * xDelta) / 2);
...
}
...
double CalculateAmplitude(double y)
{
return Math.Min(1, Math.Pow(10, -4 * (1 - y / ContentPanel.ActualHeight)));
}
double CalculateFrequency(double x)
{
return Math.Pow(2, MIN_FREQ_LOG2 + (x - xStart) / xDelta / 12);
}
...
}
Диапазон от C (ноты до) ниже средней C (частота около 130,8 Гц) до C на три октавы выше средней C составляет примерно 2093 Гц. Два метода вычисляют частоту и относительную амплитуду (в диапазоне от 0 до 1) на основе координат точки касания, полученных через событие Touch.FrameReported.
Если вы используете эти значения просто для управления генератором синусоидальных (гармонических) колебаний (sine wave oscillator), он и близко не будет звучать как терменвокс. Когда вы двигаете пальцем по экрану, программа не получает событие для каждого отдельного пикселя на этом пути. Вместо плавного скользящего изменения частоты вы услышите очень дискретные скачки. Чтобы решить эту проблему, я создал специальный класс, показанный на рис. 2. Этот класс наследует свойство Frequency, но определяет еще три свойства: Amplitude, DestinationAmplitude и DestinationFrequency. Скользящее изменение обеспечивает сам генератор гармонических колебаний, используя мультипликативные коэффициенты. Программа на самом деле не в состоянии предвидеть, насколько быстро будет двигаться палец, но в большинстве случаев такой вариант вроде бы работает нормально.
Рис. 2. Класс ThereminOscillator
public class ThereminOscillator : Oscillator
{
readonly double ampStep;
readonly double freqStep;
public const double MIN_AMPLITUDE = 0.0001;
public ThereminOscillator(int sampleRate)
: base(sampleRate)
{
ampStep = 1 + 0.12 * 1000 / sampleRate; // ~1 db per msec
freqStep = 1 + 0.005 * 1000 / sampleRate; // ~10 cents per msec
}
public double Amplitude { set; get; }
public double DestinationAmplitude { get; set; }
public double DestinationFrequency { set; get; }
public override short GetNextSample(double angle)
{
this.Frequency *= this.Frequency < this.DestinationFrequency ?
freqStep : 1 / freqStep;
this.Amplitude *= this.Amplitude < this.DestinationAmplitude ?
ampStep : 1 / ampStep;
this.Amplitude = Math.Max(MIN_AMPLITUDE, Math.Min(1, this.Amplitude));
return (short)(short.MaxValue * this.Amplitude * Math.Sin(angle));
}
}
На рис. 3 показан обработчик события Touch.FrameReported в классе MainPage. Когда палец впервые касается экрана, в Amplitude записывается минимальное значение, так чтобы громкость звука нарастала. Когда палец отводится от экрана, звук постепенно затухает.
Рис. 3. Обработчик Touch.FrameReported в приложении Theremin
void OnTouchFrameReported(object sender, TouchFrameEventArgs args)
{
TouchPointCollection touchPoints = args.GetTouchPoints(ContentPanel);
foreach (TouchPoint touchPoint in touchPoints)
{
Point pt = touchPoint.Position;
int id = touchPoint.TouchDevice.Id;
switch (touchPoint.Action)
{
case TouchAction.Down:
oscillator.Amplitude = ThereminOscillator.MIN_AMPLITUDE;
oscillator.DestinationAmplitude = CalculateAmplitude(pt.Y);
oscillator.Frequency = CalculateFrequency(pt.X);
oscillator.DestinationFrequency = oscillator.Frequency;
HighlightLines(pt.X, true);
touchID = id;
break;
case TouchAction.Move:
if (id == touchID)
{
oscillator.DestinationFrequency = CalculateFrequency(pt.X);
oscillator.DestinationAmplitude = CalculateAmplitude(pt.Y);
HighlightLines(pt.X, true);
}
break;
case TouchAction.Up:
if (id == touchID)
{
oscillator.DestinationAmplitude = 0;
touchID = Int32.MinValue;
// Remove highlighting
HighlightLines(0, false);
}
break;
}
}
}
Как видно из кода, программа Theremin просто генерирует один тон и игнорирует попытки касания более чем одним пальцем.
Хотя частота терменвокса непрерывно меняется, экран, тем не менее, отображает линии, обозначающие дискретные ноты. Эти линии окрашиваются красным для ноты C (до) и синим для F (фа) (такие цвета используются для струн арфы), белым для знаков бекара (naturals) и серым для знаков альтерации (accidentals) (диезов). Поиграв некоторое время с этой программой, я решил, что нужна какая-то визуальная обратная связь, которая подсказывала бы, на какой ноте находится палец в данный момент, поэтому я сделал так, чтобы линии расширялись в зависимости от их расстояния от точки касания. На рис. 4 показан экран, когда палец находится между C и C#, но ближе к C.
Рис. 4. Экран Theremin
Латентность и искажение
Одна из крупных проблем в синтезе музыки программным способом — это латентность, или задержка между пользовательским вводом и последующим изменением звука. Это во многом неизбежно: потоковая передача аудио в Silverlight требует, чтобы приложение наследовало от MediaStreamSource и переопределяло метод GetSampleAsync, который предоставляет аудиоданные по запросу через объект Memory¬Stream. На внутреннем уровне эти аудиоданные хранятся в буфере. Наличие этого буфера помогает добиться того, чтобы звук воспроизводился без прерываний, но, разумеется, воспроизведение содержимого буфера всегда влечет за собой необходимость его заполнения.
К счастью, в MediaStreamSource определено свойство AudioBufferLength, которое указывает размер буфера в миллисекундах длительности звучания. (Это свойство защищенное, и его можно установить только из производного от MediaStreamSource класса перед открытием медиа-файла.) Значение по умолчанию равно 1000 (или одной секунде), но вы можете уменьшить его до 15. Более низкое значение увеличивает частоту передачи данных между ОС и классом, производным от MediaStreamSource, и может привести к прерывистому звучанию. Однако я обнаружил, что минимальное значение 15 работает вполне удовлетворительно.
Другая потенциальная проблема — простая неспособность создания больших объемов данных. Вашей программе нужно генерировать десятки или сотни тысяч байтов в секунду, и, если она не справится с этим, звук может стать прерывистым, и вы услышите множество щелчков.
Есть пара способов устранить эту проблему: вы можете сделать свой конвейер генерации аудиоданных более эффективным (об этом я вскоре расскажу) или уменьшить частоту дискретизации. Я убедился, что частота дискретизации уровня CD в 44 100 слишком велика для моих программ, и уменьшил ее до 22 050. Не исключено, что может понадобиться дальнейшее снижение до 11 025. Возьмите за правило всегда проверять свои аудиопрограммы на нескольких разных устройствах Windows Phone. В коммерческом продукте вы скорее всего предпочтете дать пользователю выбор из нескольких частот дискретизации.
Несколько генераторов
Компонент Mixer библиотеки синтезатора осуществляет смешивание данных с нескольких входов в композитный вывод для левого и правого каналов. Это довольно прямолинейная работа, но при этом учитывайте, что каждый вход — это волновой сигнал с 16-битной амплитудой, а вывод — тоже волновой сигнал с такой же амплитудой, поэтому входные сигналы нужно ослаблять в зависимости от того, сколько их. Например, если у компонента Mixer десять входов, каждый входной сигнал требуется ослаблять на одну десятую от его исходного значения.
Это влечет за собой серьезные последствия: входы Mixer нельзя добавлять или удалять, пока проигрывается музыка без увеличения или уменьшения громкости остающихся входов. Если вам нужна программа, потенциально способная проигрывать 25 разных звуков одновременно, потребуется 25 постоянных входов микшера.
Именно такая ситуация возникает в приложении Harp, которое входит в решение Musical¬Instruments. Я вообразил музыкальный инструмент со струнами, которые я мог бы перебирать кончиком пальца, но которые также позволили бы наигрывать рулады, как на арфе.
Как видно на рис. 5, визуально он очень похож на терменвокс, но у него всего две октавы вместо четырех. Струны для альтерации (диезов) располагаются вверху, а для бекаров — внизу, что в какой-то мере имитирует арфу с перекрестными струнами. Вы можете исполнять пентатонное глиссандо (вверху), хроматическое глиссандо (в середине) или диатоническое глиссандо (внизу).
Рис. 5. Программа Harp
Я использовал 25 экземпляров класса SawtoothOscillator, который генерирует простые пилообразные волновые сигналы, сильно напоминающие звуки струн. Также требовалось создать рудиментарный генератор огибающей (envelope generator). В реальности музыкальные звуки не начинаются и не прекращаются мгновенно. Звук какое-то время нарастает, а потом затухает сам по себе (послушайте, например, пианино или арфу) или после того, как музыкант останавливает его. Этими изменениями управляет генератор огибающей. Мне не требовалось ничего изощренного вроде полноценного ADSR (attack, decay, sustain, release), поэтому я создал более простой класс AttackDecayEnvelope. (В реальности тембр звука, управляемый своими гармониками [harmonic components], тоже меняется в течение длительности звучания одного тона, поэтому тембр тоже должен управляться генератором огибающей.)
Для визуальной обратной связи я решил, что струны должны вибрировать. Каждая струна на самом деле является квадратичной кривой Безье (quadratic Bezier segment) с центральной управляющей точкой, коллинеарной с двумя конечными точками. Применяя к управляющей точке повторяющуюся анимацию PointAnimation, я мог бы имитировать вибрацию струн.
На практике это обернулось катастрофой. Вибрации выглядели замечательно, но звук деградировал до состояния тихого ужаса. Тогда я решил переключиться на нечто, не столь ресурсоемкое: я использовал DispatcherTimer и смещал точки вручную с намного меньшей частотой, чем при реальной анимации.
Поиграв какое-то время с программой Harp, я расстроился из-за того, что для перебора струн требовался жест смахивания, поэтому добавил кое-какой код для извлечения звуков простым постукиванием. В этот момент мне, вероятно, следовало бы сменить название программы с Harp на HammeredDulcimer (цимбалы), но махнул на это рукой.
Как избежать вычислений с плавающей точкой
На устройстве Windows Phone, которым я пользовался большую часть времени при разработке, программа Harp работала нормально. На другом устройстве Windows Phone звучание получалось с сильным треском; это свидетельствует о том, что буферы не успевали заполняться достаточно быстро. Снижение частоты дискретизации вдвое снимало эту проблему. Треск прекращался при частоте дискретизации 11 025 Гц, но я не был готов пожертвовать качеством звука.
Вместо этого я стал внимательнее изучать конвейер, который предоставлял эти тысячи выборок в секунду. Все классы — Mixer, Mixer¬Input, Sawtooth¬Oscillator и AttackDecayEnvelope — имели одну общую особенность: они так или иначе использовали арифметику с плавающей точкой при вычислениях этих выборок. Нельзя ли ускорить этот конвейер переключение на целочисленные вычисления?
Я переписал свой класс AttackDecayEnvelope под использование целочисленной арифметики и сделал то же самое с SawtoothOscillator, который показан на рис. 6. Эти изменения значительно улучшили производительность.
Рис. 6. Целочисленная версия SawtoothOscillator
public class SawtoothOscillator : IMonoSampleProvider
{
int sampleRate;
uint angle;
uint angleIncrement;
public SawtoothOscillator(int sampleRate)
{
this.sampleRate = sampleRate;
}
public double Frequency
{
set
{
angleIncrement = (uint)(UInt32.MaxValue * value / sampleRate);
}
get
{
return (double)angleIncrement * sampleRate / UInt32.MaxValue;
}
}
public short GetNextSample()
{
angle += angleIncrement;
return (short)((angle >> 16) + short.MinValue);
}
}
{Для верстки: не потеряйте знак «пи»}
В генераторах, использующих арифметику с плавающей точкой, переменные angle и angle¬Increment имеют тип double, где angle варьируется от 0 до 2π, а angleIncrement вычисляется так:
{Для верстки: это формула}
2π • частота
Частота дискретизации
Для каждой выборки angle увеличивается на angleIncrement.
Я не исключил полностью вычисления с плавающей точкой из SawtoothOscillator. Открытое свойство Frequency по-прежнему определено как double, но оно используется только при задании частоты генератора. Переменные angle и angleIncrement являются 32-разрядными целыми без знака. Полные 32-разрядные значения используются, когда angleIncrement увеличивает значение angle, но в качестве значения для вычисления волнового сигнала задействуются лишь верхние 16 бит.
Даже с такими изменениями программа все равно не слишком хорошо работала на тех устройствах, которые я теперь рассматриваю как «медленные смартфоны» по сравнению с моим «быстрым смартфоном». Движение пальцем по всему экрану по-прежнему вызывает потрескивание.
Но то, что верно в отношении любых акустических музыкальных инструментов, справедливо и для электронных: вы должны изучить инструмент и знать не только его возможности, но и его ограничения.