Обработка сигналов

Мы все видели аудиовизуализацию в той или иной форме, но как реализовать ее в нашем приложении Какао? Если вы почти не разбираетесь в обработке сигналов или аудио в цифровом формате, это руководство идеально вам подойдет. Это руководство поможет вам сориентироваться и понять платформу Apple Accelerate и способы визуализации графики на экране.

Этот проект основан непосредственно на моем предыдущем руководстве:



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

Этот учебник разбит на две части - ввод (обработка сигнала) и вывод (визуализация).

Целью этого руководства является:

а) Постарайтесь понять, как работает обработка звука.

б) Совершенствовать навыки работы с металлом

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

Так что держитесь, будьте готовы изучать новые вещи, и, надеюсь, вы сможете забрать все необходимые навыки, чтобы делать все, что захотите, включая аудиовизуализацию.

Требования к проекту

Ну что вообще такое аудиовизуализатор? В техническом смысле это когда вы используете маркеры аудиосигнала, такие как объем и частотный спектр, для создания определенных визуальных аспектов (в реальном времени).

Тип аудиовизуализатора, который мы создадим сегодня, будет состоять из кругового масштабирования (подпрыгивания) к текущей громкости звука и частотных линий, отображающих среднюю величину отдельных интервалов частот. Если вы не понимаете, о чем я только что сказал, не волнуйтесь, об этом мы поговорим позже.

Проблема может быть разбита на две части: ввод и вывод.

Ввод

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

Прямо сейчас у нас должно быть два вопроса:

  • Как мы отбираем аудиосигнал?
  • Как быстро выполнять операции с данными?

Для сэмплирования аудиосигнала мы будем использовать класс AVAudioEngine AVFoundation, который позволяет нам установить обратный вызов, в котором мы будем получать данные буфера обратно из аудиосигнала через дискретные временные интервалы. Для обработки аудиосигнала мы будем использовать фреймворк Accelerate, который используется для высокопроизводительных вычислений на базе ЦП.

Вывод

У нас есть свой вклад; что теперь? Мы должны отобразить это на экране! Технически вы можете использовать все, что захотите для визуальных эффектов (CALayer / Shape или даже обычный UIView), но это будет ужасно с точки зрения производительности. Поэтому неудивительно, что мы будем передавать входные данные по графическому конвейеру и обрабатывать отображение в наших шейдерных функциях!

Итак, на высоком уровне нам необходимо:

  1. Настройка AVAudioEngine
  2. Установите кран на главный узел микшера, чтобы получить данные буфера
  3. Обработка данных аудиобуфера на ЦП
  4. Отправьте его по графическому конвейеру на GPU

Пора начинать 😄

Настраивать

Стартовый код проекта находится на моем Github здесь.

Здесь мало чем отличается от готового продукта предыдущего урока. Я переименовал класс MetalCircleView, в котором мы работали для нашего круга, в AudioVisualizer, чтобы он больше соответствовал этому руководству. Я также добавил файл music.mp3, который будет содержать аудио, которое мы будем визуализировать. Не стесняйтесь обменивать это на что угодно.

Я выбрал песню EDM, так как она лучше демонстрирует силу того, что мы делаем. Мы будем выполнять всю обработку сигналов внутри класса SignalProcessing и все металлические детали внутри класса AudioVisualizer, используя класс ViewController в качестве промежуточного звена между ними.

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

Часть 1: Получение входных данных (обработка сигналов)

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

Раздел 1. Звуковой движок и принцип работы аудиоданных

Итак, как мы воспроизводим музыку в приложении Какао? Что ж, мы используем фреймворк AVFoundation. На самом деле у нас есть только одно требование к типу класса игрока. Нам нужно вернуть текущие аудиоданные, чтобы мы могли их обработать. AVPlayer и AVAudioPlayer кажутся хорошими; с ними легко работать, и в течение одной минуты в Stack Overflow вы можете скопировать и вставить свой путь к аудио в вашем приложении!

К сожалению, эти классы не поддерживают отводы аудиоданных. Далее у нас есть AVAudioEngine, гораздо более универсальный класс на основе узлов. Поддерживает ли он отводы аудиоданных? Ага. Мы также можем напрямую работать с CoreAudio, который также поддерживает аудиоотводы. Хотя это не рекомендуется ... уровень сложности для наших целей слишком высок.

AVAudioEngine

Группа подключенных объектов аудиоузла, используемая для генерации и обработки аудиосигналов и выполнения аудиоввода и вывода.

Как это работает? Класс можно представить как граф. По умолчанию у нас есть mainMixerNode, который подключается к узлу вывода (проигрывателю аудиовыхода устройства по умолчанию). Чтобы воспроизводить музыку, нам нужно подключить узел проигрывателя к mainMixerNode и указать этому узлу воспроизводить звук. Но это еще не все!

AVAudioNode

Абстрактный класс для блока генерации, обработки или ввода-вывода звука.

Тип AVAudioNode - это то, что мы можем прикрепить к нашему звуковому движку. AVAudioNode - это родительский класс для различных типов узлов, которые вы можете присоединить в любом порядке для создания, обработки (добавления эффектов) и вывода. В нашем случае нам нужен только AVAudioPlayerNode для воспроизведения нашей музыки. Узлы имеют входные и выходные шины, шины передают данные через узлы, поэтому их можно рассматривать как точки подключения.

Давайте посмотрим, как это будет выглядеть, когда мы добавим его в наш проект. Мы собираемся войти в наш класс ViewController и сначала настроить сам движок.

Итак, как вы можете видеть, мы сначала импортировали библиотеку AVFoundation, добавили движок как свойство экземпляра класса ViewController, а затем запустили его. Теперь _ = engine.mainMixerNode кажется странной строкой, но мы знаем, что с AVAudioEngine у ​​нас есть mainMixerNode по умолчанию, который подключается к выходному узлу по умолчанию. Что может быть менее очевидным, так это то, что, поскольку это синглтон, он инициализируется только при первом доступе к нему.

Затем давайте добавим наш плеер и посмотрим, сможем ли мы включить музыку!

Если вы запустите программу, вы должны увидеть наш красивый кружок из последнего урока и услышать свой файл music.mp3.

Теперь давайте возьмем воспроизводимые аудиоданные. Это делается с помощью installTapOnBus, который является методом экземпляра AVAudioNode.

func installTap(onBus bus: AVAudioNodeBus,
               bufferSize: AVAudioFrameCount,
                   format: AVAudioFormat?,
           block tapBlock: @escaping AVAudioNodeTapBlock)

При этом на шине устанавливается аудиоразъем для записи, мониторинга и наблюдения за выходом узла.

onBus - описывает, с какой выходной шины вы хотите получать данные (поскольку узлы иногда могут иметь несколько шин). 0 - всегда беспроигрышный вариант, поскольку обычно используется только одна выходная шина. При создании соединений между узлами также существуют различные перегрузки функций, в которых вы можете указать шины, на которых хотите установить соединения.

bufferSize - описывает количество байтов, которое вы хотите вернуть из аудиоданных.

format - nil для наших целей, сам разберется

блок - это данные, переданные в обратном вызове, состоящие из AVAudioPCMBuffer и AVAudioTime (время, в котором находится дорожка)

Давайте установим кран (этот шаг очень простой) перед вызовом play на нашем узле player.

engine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: nil) { (buffer, time) in
}

Мы задействовали mainMixerNode движка - почему? Ну это не важно. Отвод работает на выходной шине, и поскольку наша музыка одинакова во всем конвейере воспроизведения (AVAudioPlayerNode- ›AVAudioMixerNode-› AVAudioOutputNode), не имеет значения, в какую часть мы подключаемся.

AVAudioPCMBuffer

Это наш буфер, но что на самом деле? Во-первых, давайте начнем с того, что это за класс, в частности:

Подкласс AVAudioBuffer для использования с аудиоформатами PCM.

Хммм, это не очень помогло. Что такое AVAudioBuffer?

Буфер аудиоданных и его формат.

Итак, мы можем понять, что класс содержит аудиоданные в формате PCM. Во-первых, что такое ПКМ? Из Википедии:

Импульсно-кодовая модуляция (PCM) - это метод, используемый для цифрового представления дискретизированных аналоговых сигналов. Это стандартная форма цифрового звука в компьютерах, компакт-дисках, цифровой телефонии и других приложениях цифрового звука.

Это просто стандартный формат, в который декодируется любой звук для работы на оборудовании. Например, формат mp3 является сжатым и с потерями. Это означает, что при кодировании из PCM в mp3 он был сжат (это не влияет на качество данных), но при кодировании также отбрасываются «менее важные» данные для экономии места (это влияет на качество данных) .

Какой бы цифровой аудиоформат вы ни подавали в фреймворк, он будет декодирован в PCM, будь то аппаратное или программное обеспечение.

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

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

Если мы посмотрим в разделе «Доступ к данным буфера PCM» документации AVAudioPCMBuffer, то увидим, что у нас есть три различных варианта:

1. var floatChannelData: UnsafePointer<UnsafeMutablePointer<Float>>?

2. var int16ChannelData: UnsafePointer<UnsafeMutablePointer<Int16>>?

3. var int32ChannelData: UnsafePointer<UnsafeMutablePointer<Int32>>?

Это странный тип, который вы не часто видите, если не работаете с «низкоуровневыми» фреймворками Cocoa. У нас есть внешний неизменяемый указатель на разные каналы, и каждый канал содержит изменяемые указатели на звуковые кадры. Ну сколько у нас аудиоканалов? Это зависит от нашего аудиоформата.

Сейчас идеальное время, чтобы сделать шаг назад и распечатать полученный AVAudioFormat или AVAudioFile.

print(format)

<AVAudioFormat 0x6000021201e0: 2 ch, 44100 Hz, Float32, non-inter>

Итак, у нас есть два канала - звук был дискретизирован с частотой 44100 Гц, а амплитуда звуковых кадров представлена ​​32-битным числом с плавающей точкой. Последнее свойство указывает, что формат не чередуется. Вы можете прочитать больше об этом здесь".

Аудиоканал - это звук, исходящий из одной точки или идущий в нее. Например, вы, возможно, слышали о домашних системах объемного звука 5.1 или наушниках. Они работают, только если ваш аудиоформат содержит пять полноразмерных каналов и один низкочастотный канал (для сабвуфера).

Я отвлекся - для наших целей мы можем просто использовать один из каналов (проще всего придерживаться первого, поскольку всегда будет хотя бы один канал). Но ради интереса вы можете использовать разные каналы в своих интересах при создании более продвинутых аудиовизуализаторов. На ум приходит двухканальный аудиовизуализатор.

Частота дискретизации, размер выборки касания и частота дискретизации касания

Сначала мы установили частоту дискретизации аудиоформата. Мы сделали это случайно, когда получили формат обработки из нашего AVAudioFile. Частота дискретизации определяет (в Гц), как часто происходит захват цифрового значения из аналогового сигнала. Важно знать, с какой скоростью воспроизводить каждый аудиокадр, чтобы он звучал одинаково. Например, большинство аудио имеет частоту дискретизации 22050 или 44100 Гц; если мы удвоим частоту дискретизации при воспроизведении, звук будет вдвое быстрее, а если мы уменьшим вдвое частоту дискретизации при воспроизведении, воспроизведение будет вдвое медленнее.

Затем мы указали размер выборки для касания с помощью параметра bufferSize. К сожалению, фреймворку обычно безразлично, что мы сказали о размере выборки. Реальный размер выборки можно найти с помощью buffer.frameLength. Это неудивительно, поскольку на самом деле это указано в документации для installTap:

размер буфера

Запрошенный размер входящих буферов. Реализация может выбрать другой размер.

Теперь важный вопрос: какова частота дискретизации тапа? Или как часто мы получаем обратный вызов с размером выборки? Если частота обратного вызова и размер выборки не совпадают, мы могли бы пропустить много аудиокадров! После некоторого экспериментального тестирования выяснилось, что фреймворк запускает обратный вызов каждые 0,1 с без потери звукового кадра. Это означает, что если частота дискретизации аудиоформата составляет 44100 Гц, мы будем получать 4410 аудиокадров каждые 0,1 с. (Примечание: эти числа, кажется, изменились для меня с тех пор, как я начал писать это… почти год назад. Не пугайтесь, это не имеет значения для целей данного руководства).

Завершая первый раздел, наш ViewController должен выглядеть так:

Теперь мы готовы к следующей части: обработке сигналов!

Раздел 2: Обработка сигналов

Во-первых, давайте вернемся к требованиям к производительности. Поскольку мы хотим, чтобы это было быстро, нам нужны две вещи.

  1. Быстро посчитайте.
  2. Не делайте этого в основном потоке (UI).

Во-первых, мы уже знаем, что можем использовать фреймворк Accelerate. Во втором случае нам повезло. Из документации installTap:

tapBlock может быть вызван в потоке, отличном от основного потока.

Мы скорее всего (это может не быть детерминированным поведением, планируем соответственно для производственных целей) не будем в основном потоке, поэтому нам не нужно беспокоиться об использовании GCD или NSThreads для запуска эти расчеты.

А теперь перейдем к делу. Мы знаем, что используем первый канал, и мы знаем, что фреймворк меньше заботится о нашем запрошенном bufferSize, поэтому давайте получим эту информацию.

Теперь у нас есть массив чисел с плавающей запятой (channelData) и размер массива (фреймы). Количество кадров представлено в виде AVAudioFrameCount, который является просто псевдонимом для UInt32.

Представляем Accelerate

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

Если мы посмотрим на документацию, то увидим, что фреймворк предоставляет нам несколько библиотек.

Нас интересует vDSP:

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

Это основа обработки сигналов. Здесь мы найдем абсолютно все, что нужно для наших целей.

Раздел 2.1. Измерение громкости сигнала

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

Мы замечаем, что значения могут быть как положительными, так и отрицательными; как это так? Что ж, звук - это волна давления, и относительно некоторой базовой линии давление может быть выше или ниже. Подробнее здесь.

Давайте посмотрим на документацию по vDSP в разделе «Снижение вектора» ›Расчет среднего значения вектора. Потому что, в конце концов, мы имеем дело с вектором информации и пытаемся найти какую-то среднюю громкость нашего текущего аудиобуфера для отображения пользователю.

Итак, какой из них мы выберем?

Это немного сложно. Лучшим вариантом, который у нас есть в структуре vDSP, является вычисление квадрата среднего корня. Это имеет смысл, поскольку RMS используется для вычисления среднего значения функции, которая идет выше и ниже оси x. Также оказывается, что на практике это очень хороший и широко применяемый метод измерения громкости.

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

Давайте сделаем это и посмотрим, что у нас получится!

Теперь перейдем к нашему классу SignalProcessing.

Нам нужно импортировать Accelerate, чтобы воспользоваться преимуществами библиотеки vDSP. Теперь давайте посмотрим на функцию RMS, vDSP_rmsqv.

func vDSP_rmsqv(_ __A: UnsafePointer<Float>,
                _ __IA: vDSP_Stride,
                _ __C: UnsafeMutablePointer<Float>,
                _ __N: vDSP_Length)

A: (я знаю, очень наглядно) Указатель на наши данные, определенный как реальный входной вектор одинарной точности. Одинарная точность означает, что это число с плавающей запятой; реальный ввод, означающий, что значения реальные, а не сложные.

ИА: Скорость наших буферных данных. Шаг - это расстояние между дискретными значениями данных в памяти (в каком-то контейнере, таком как массив, вектор и т. Д.), Измеренное в каких-то единицах. В нашем случае vDSP_Stride - это единичный шаг, то есть он переместит в памяти x байтов, где x обозначает размер типа значения в контейнере. vDSP_Stride - это просто псевдоним типа для Int.

C: указатель на Float, куда мы хотели бы записать результат операции.

N: длина данных, над которыми мы хотим выполнить операцию. Это просто псевдоним типа UInt.

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

Если мы вернемся к функции processAudioData нашего ViewController, мы сможем получить эффективную громкость для текущего аудиосэмпла.

Итак, у нас есть значение для образца - что теперь? Эти значения довольно малы. Если вы помните, как работают точки в металле, оси X и Y масштабируются от -1 до 1. Сначала нам нужно установить базовую линию, или, скорее, насколько большим будет круг при нулевой громкости и каков будет максимум это может быть? Если мы подумаем об этом с точки зрения пользовательского интерфейса, мы не хотим, чтобы круг заполнял весь MetalView; должно быть место для частотных линий. Мы будем использовать минимум 0,3 и максимум 0,6.

Нам нужно нормализовать эти значения, чтобы они соответствовали диапазону от 0,3 до 0,6. Если мы используем базовую линию радиуса 0,3 для нашего круга, тогда наивный подход будет 0,3 + rmsValue; но, как мы видим, эти значения слишком малы, чтобы иметь заметную разницу, поэтому нам нужно их увеличить.

Во-первых, давайте превратим результат в децибелы (10*log10f(val)), чтобы упростить работу и понимание.

Теперь мы получим значения в диапазоне от -160 дБ (без звука) до 0 дБ (самый громкий). Чтобы настроить это значение на шкалу +0,3, а не на шкалу -160, потребуется некоторая простая арифметика.

Но теперь мы замечаем другую проблему: все наши ценности колеблются в районе 5,27–5,28. Это не кажется слишком впечатляющим. Что мы можем сделать, чтобы подчеркнуть эти небольшие изменения? Что ж, мы можем выбрать увеличение определенного диапазона, которым, по-видимому, ограничивается наша громкость.

Добавьте оператор печати и посмотрите результаты!

Следующая проблема, которую необходимо решить, - это частота дискретизации. Мы получаем обратный вызов каждые 0,1 секунды. Измеритель громкости A10fps выглядит не очень впечатляюще, особенно если учесть тот факт, что мы просто десять раз рендерили круг разных размеров. Он будет выглядеть неровным и совсем не гладким. Итак, что мы можем сделать, чтобы восполнить пробелы? Хороший вопрос. Мы можем сделать некоторую интерполяцию между точками, чтобы сгладить это! В этом случае линейная интерполяция более чем хороша.

Я увеличу масштаб и просто вставлю код. По сути, все, что нам нужно сделать, это сохранить prevRMSValue в классе контроллера и вызвать функцию интерполяции в нашем классе signalProcessing, используя наши предыдущие и текущие среднеквадратичные значения.

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

Раздел 2.2. Измерение частоты сигнала

Во-первых, нам нужно понять, что означает частота и как мы можем получить величины дискретных элементов разрешения по частоте, чтобы представить размер наших линий. Какова величина частотного интервала? Это количество энергии около этой частоты. Мы скоро рассмотрим эту концепцию. Сначала мы рассмотрим звуковой сигнал, затем как работает преобразование Фурье и, наконец, как мы можем реализовать быстрое преобразование Фурье (БПФ), что буквально означает быстрый способ алгоритмически вычислить преобразования Фурье.

Звук распространяется волной. Увеличение амплитуды волны увеличивает громкость звука за счет увеличения энергии волны, поэтому использование амплитуд нашего аудиосэмпла для вычисления громкости работает. На видео ниже хорошо показано, что такое частота, которую мы слышим (звук), и как она соотносится с амплитудой.

Теперь, как нам получить энергии каждой частоты?

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

Теперь давайте рассмотрим эти значения!

Возвращаясь к структуре vDSP и глядя на векторные и матричные преобразования Фурье, мы видим вариант БПФ! Здесь у нас много вариантов.

Непосредственные подразделы функций на странице БПФ сгруппированы следующим образом:

  • 1D or 2D?
  • На месте или не на месте?
  • Реальный или сложный?

Глядя на наш аудиосэмпл, должно быть ясно, что мы работаем в одном измерении, то есть нас заботит только значение на оси Y (амплитуда). На месте или вне места относится к самому алгоритму. Третий вопрос, реальный или сложный, более интересен. Когда мы переходим из временной области в частотную, мы получаем значения на мнимой и действительной осях, то есть комплексное число. Что ж, нас волнует энергия или величина этого числа. Если мы выберем реальный, мы получим значения, представляющие уровни энергии. Если мы выберем мнимый, нам нужно будет вычислить величину сложных результатов самостоятельно (простой вызов функции). Вопрос о том, следует ли игнорировать сложную часть, - это вопрос точности. В нашем случае это не имеет значения, но мы продолжим с комплексными числами.

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

Наконец-то мы сузились до желаемой функции:

func vDSP_fft_zipt(_ __Setup: FFTSetup,
                   _ __C: UnsafePointer<DSPSplitComplex>,
                   _ __IC: vDSP_Stride,
                   _ __Buffer: UnsafePointer<DSPSplitComplex>,
                   _ __Log2N: vDSP_Length,
                   _ __Direction: FFTDirection)

Но ждать! В документации говорится:

По возможности используйте вместо них подпрограммы DFT. (Например, вместо вызова vDSP_fft_zip с настройкой, созданной с помощью vDSP_create_fftsetup, вызовите vDSP_DFT_Execute(_:_:_:_:_:) с настройкой, созданной с помощью vDSP_DFT_zop_CreateSetup(_:_:_:).)

Раньше я на них не обращал внимания, но функции выполнения БПФ намного проще в использовании, поэтому мы продолжим и будем использовать vDSP_DFT_Execute(_:_:_:_:_:) вместо более подробных функций.

func vDSP_DFT_Execute(_ __Setup: OpaquePointer,
                      _ __Ir: UnsafePointer<Float>,
                      _ __Ii: UnsafePointer<Float>,    
                      _ __Or: UnsafeMutablePointer<Float>,
                      _ __Oi: UnsafeMutablePointer<Float>)

Эти исходные данные потребуют некоторых пояснений.

Во-первых, у нас есть OpaquePointer, он же установочный объект для функции, vDSP_DFT_zop_CreateSetup(_:_:_:)

Создает структуру данных для использования с vDSP_DFT_Execute(_:_:_:_:_:) или vDSP_DCT_Execute(_:_:_:) для выполнения комплексного дискретного преобразования Фурье, прямого или обратного.

func vDSP_DFT_zop_CreateSetup(_ __Previous: vDSP_DFT_Setup?,
                              _ __Length: vDSP_Length,
                              _ __Direction: vDSP_DFT_Direction
                              ) -> vDSP_DFT_Setup?

Для предыдущего у нас его нет, поэтому он будет нулем.

vDSP_length - это псевдоним типа unsigned long и представляет количество элементов, которые мы будем преобразовывать. Мы можем использовать значения 2¹² (4096), но это слишком много результирующих интервалов для рисования линий, так что это просто не будет хорошо выглядеть (будет слишком тесно). Использование 1024 (2¹⁰) элементов для преобразования дает гораздо более разумный результат. Теперь, как мы видели ранее по размеру выборки, полученной для нашего конкретного mp3, это только четвертая часть данных буфера! Если вам нужна более высокая точность (для нас это не совсем важно), вы можете добавить больше значений.

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

В нашем классе контроллера представления мы будем импортировать ускорение и инициализировать объект fftSetup, хранящийся как переменная класса:

import Accelerate
....
//fft setup object for 1024 values going forward (time-> frequency)
let fftSetup = vDSP_DFT_zop_CreateSetup(nil, 1024, vDSP_DFT_Direction.FORWARD)

Затем БПФ принимает два входных указателя (неизменяемые) и два выходных указателя (изменяемые) для векторов с плавающей запятой, представляющих действительную и мнимую части чисел. Итак, все, что нам нужно сделать, это создать его, а затем запустить функцию! Это будет сделано внутри класса SignalProcessing (в новой функции) и будет вызываться из класса ViewController.

Хорошо, давай остановимся на секунду. Ранее я сказал, что БПФ будет выдавать величины в элементах разрешения по частоте, но сколько там элементов разрешения по частоте, мы ничего не указали. Количество элементов разрешения по частоте фактически линейно с количеством точек данных (n / 2) согласно теореме выборки Найквиста-Шеннона.

Итак, какие частоты удерживаются в этих ячейках? Как это работает: если мы используем 1024 точки данных, время выборки для этого составляет ~ 0,025 с или 1/40 ~ 40 Гц, что означает, что самая низкая частота, которую мы можем обнаружить, составляет 1 * 40 Гц = 40 Гц (первый интервал), а самая высокая частота мы можем определить это (n / 2) * 40 Гц = 512 * 40 Гц = 20,48 кГц (последний интервал).

Далее нам нужно получить величину. Помните, что величины будут обозначать энергию в интервале частот. Есть ли способ вычислять комплексные величины в рамках vDSP? Конечно, есть. У нас есть два варианта: вычислить sqrt (a² + b²) или вычислить (a² + b²), чтобы сэкономить время вычислений. Но для нашего метода нормализации нам понадобится первый метод.

Функция, которую мы будем использовать, называется vDSP_zvabs, и ее можно найти в Абсолютных и Отрицательных функциях инфраструктуры vDSP. (Другой вариант называется vDSP_zvmags). С этого момента я не буду опускать пояснения к параметрам функций Accelerate.

Глядя на значения, мы видим широкий диапазон. И так же, как мы делали с величиной сигнала раньше, нам нужно нормализовать его. На этот раз мы масштабируем не только одно значение; мы масштабируем 1024 значения. Поэтому мы хотим быть уверены, что можем сделать это оптимально, и именно здесь векторно-скалярные операции вступают в игру из инфраструктуры DSP. Как выбрать коэффициент масштабирования? На это нет простого ответа.

Распространенный онлайн-подход - разделить на количество образцов. После того, как мы попробуем изменить значения таким образом, чтобы мы могли их использовать, наш коэффициент масштабирования будет 25,0 / 512. Примечание. Мы не пытаемся убедиться, что он не выходит за рамки, мы просто стараемся, чтобы он хорошо выглядел (это, конечно, мне не по вкусу).

Посмотрим, сможете ли вы выполнить эту часть самостоятельно, используя vDSP_vsmul.

Теперь, когда мы вернули наши результаты, мы закончили с частью 1 :)

Если вы действительно читали мою длинную статью, сделайте перерыв! Ты заслуживаешь это!

Полный код первой части находится на моем Github здесь:

Не забывайте о второй части!



Если у вас есть вопросы или предложения, оставьте их в комментариях ниже.