Разработка
Создание живой звуковой волны в SwiftUI
В этом кратком руководстве мы расскажем вам, как использовать FFT для анализа наборов звуковых данных, чтобы визуализировать полученные частоты в виде живой функции звуковой волны в приложении SwiftUI.
Быстрое преобразование Фурье, также известное как FFT, — это алгоритм, который быстро и эффективно вычисляет громкость и частоту любого дискретизированного звука. Этот расчет точно определяет, какие музыкальные ноты (частоты) скрываются внутри звука, преобразуя длинный список измерений звука (временная область) в список интенсивности нот (частотная область).
Для достижения такого результата FFT использует комплексные числа и тригонометрические функции, которые позволяют определить, сколько каждой частоты присутствует в сигнале.
Это преобразование применяет подход «разделяй и властвуй» к дискретному преобразованию Фурье (DFT), разбивая сигнал на длинный список вычислений, анализируя и объединяя их в сумму синусоидальных волн с различными частотами, чтобы улучшить производительность и сократить время с N² до N log N.
Его скорость делает его особенно полезным при работе с большими наборами данных и с приложениями реального времени, такими как визуализаторы музыки, медицинские мониторы сердечного ритма и компрессоры изображений.
В этом кратком руководстве мы расскажем вам, как использовать FFT для анализа наборов звуковых данных, чтобы визуализировать полученные частоты в виде живой функции звуковой волны в приложении SwiftUI.
Задача будет включать:
- сбор аудиоданных в реальном времени;
- использование быстрого преобразования Фурье (FFT) для обработки этих данных;
- и отображение формы волны.
Для этой цели мы будем использовать фреймворки Apple AVFoundation, Accelerate и Swift Charts.
В нашем случае FFT преобразует необработанные аудиосигналы с микрофона в их частотные компоненты, позволяя увидеть, насколько сильна каждая частота, что помогает визуализировать изменение формы звука во времени.
По завершении этого урока у вас будет рабочее приложение SwiftUI, которое визуально отображает живую звуковую волну, как показано ниже:=
Прежде чем начать
Поскольку форма звуковой волны отображается в соответствии с аудиосигналом, который мы получаем через iPhone, нам необходимо запросить у пользователей разрешение на использование микрофона. Поэтому убедитесь, что вы уже создали новый проект Xcode и добавили свойство Privacy - Microphone Usage Description со своим значением в файл Info.plist вашего проекта, чтобы получить доступ к микрофону устройства в iOS.
Пропуск этого шага приведет к сбою приложения сразу после его запуска.
Кроме того, если вы также добавите свойство Metal Capture Enabled с значением YES, вы избавитесь от некоторых «шумов» в консоли при запуске приложения.
Добавление этого свойства позволяет записывать, что ваше приложение делает с Metal, графическим движком Apple, во время интенсивной работы, такой как обновление живых звуковых волн в интерфейсе.
Это также позволяет просматривать захват в отладчике GPU Xcode, который показывает все этапы рендеринга, текстуры и данные о производительности этих вычислений.
Этот шаг полезен, но не является строго необходимым, так как приложение будет нормально работать и не вылетать, даже если вы его пропустите.
Шаг 1: Сбор аудиоданных
Цель этого первого шага — определить класс, который будет получать доступ, отслеживать и обрабатывать аудиосигнал, поступающий с микрофона в режиме реального времени, с помощью AVAudioEngine.
Позже этот класс также будет отвечать за выполнение FFT для преобразования аудиообразцов в частотные амплитуды.
Вот как это сделать.
1.1 Определение значений конфигурации
В новом файле AudioWaveformMonitor.swift:
// 1. Importing the frameworks import SwiftUI import AVFoundation import Accelerate import Charts
1. Начнем с импорта фреймворков:
AVFoundationиспользуется для захвата и управления входным сигналом микрофонаAccelerateобеспечивает быстрые математические операции для обработки аудио в частотной областиSwiftUIиChartsотвечают за создание пользовательского интерфейса и визуальное отображение полученной волны
// 2. Declare key configuration values
enum Constants {
// a. Amount of frequency bins to keep after performing the FFT
static let sampleAmount: Int = 200
// b. Reduce the number of plotted points
static let downsampleFactor = 8
// c. Handle high spikes distortion in the chart
static let magnitudeLimit: Float = 100
}
2. Создаем перечисление Constants, которое группирует ключевые значения конфигурации.
sampleAmountопределяет, сколько частотных ячеек мы хотим сохранить после выполнения FFTdownsampleFactorпомогает уменьшить количество отображаемых точек, чтобы избежать загромождения и перегрузки графика- а
magnitudeLimitгарантирует, что необычно высокие всплески в данных не искажают масштаб графика
Если вы хотите получить более перегруженный график, просто уменьшите значение downsampleFactor по своему усмотрению.
1.2 Определение класса AudioWaveformMonitor
В том же файле:
...
// 1. AudioWaveformMonitor class
@MainActor
@Observable
final class AudioWaveformMonitor {
// 2. Shared instance of the class
static let shared = AudioWaveformMonitor()
// 3. Provide access to the microphone stream
private var audioEngine = AVAudioEngine()
// 4. Store the results
var fftMagnitudes = [Float](repeating: 0, count: Constants.sampleAmount)
// 5. Pick a subset of fftMagnitudes at regular intervals according to the downsampleFacto
var downsampledMagnitudes: [Float] {
fftMagnitudes.lazy.enumerated().compactMap { index, value in
index.isMultiple(of: Constants.downsampleFactor) ? value : nil
}
}
}
1. Определяем класс AudioWaveformMonitor, который:
- обрабатывает звук с аудиовходав реальном времени
- отвечает за преобразование его из временных выборок в частотные величины с помощью функции, выполняющей быстрое преобразование Фурье (FFT)
2. Создаем общий инстанс AudioWaveformMonitor.
3. Объявляем экземпляр AVAudioEngine, необходимый для обеспечения доступа к потоку микрофона.
4. Результаты AVAudioEngine хранятся в массиве fftMagnitudes.
5. Чтобы визуализация оставалась четкой, вычисленная переменная downsampledMagnitudes просто выбирает подмножество своих значений через равные промежутки времени в соответствии с downsampleFactor, который мы определили в константах.
1.3 Определение методов запуска и остановки
Теперь нам нужно определить методы для запуска и остановки мониторинга звука, поступающего с микрофона.
final class AudioWaveformMonitor {
...
// 1. Track if audio monitoring is running or has stopped
var isMonitoring = false
private init() {}
// The methods to be implemented
func startMonitoring() async { ... }
func stopMonitoring() { ... }
func performFFT(data: [Float]) async -> [Float] { ... }
}
1. Добавляем свойство a isMonitoring для отслеживания, работает ли мониторинг звука или он остановлен, что полезно для координации состояния пользовательского интерфейса.
Наконец, мы определяем три различных метода следующим образом:
startMonitoring()подключается к микрофону и начинаем асинхронно обрабатывать входящие данныеstopMonitoring()отключает все и сбрасывает данныеperformFFT()— это асинхронная функция, которая фактически выполняет преобразование FFT, отвечая за извлечение различных частот из необработанной волны, полученной с микрофона
Шаг 2: Запуск мониторинга, остановка мониторинга и выполнение FFT
Давайте подробнее рассмотрим определения трех функций: startMonitoring(), stopMonitoring() и performFFT().
2.1 Асинхронный запуск мониторинга звуковой волны
Прежде всего, нам нужно определить функцию startMonitoring(), которая:
- запускает мониторинг аудио
- обрабатывает звуки, захваченные микрофоном
- и преобразует их в амплитуды частот, которые будут отображаться интерфейсом
Мы сформировали код таким образом, чтобы все оставалось безопасным в рамках более строгой модели параллелизма Swift 6.
Сначала давайте определим некоторые свойства, которые понадобятся для выполнения FFT.
final class AudioWaveformMonitor {
...
// 1. The configuration parameter for the FFT
private let bufferSize = 8192
// 2. The FFT configuration
private var fftSetup: OpaquePointer?
...
}
1. Определяем bufferSize, на котором будет установлена конфигурация FFT.
2. Создаем свойство fftSetup типа OpaquePointer?, которое будет указывать на саму конфигурацию, используемую Accelerate.
Затем в методе startMonitoring():
func startMonitoring() async {
// 1. Set up the input node from the audio engine
let inputNode = audioEngine.inputNode
// 2. Set up the input format from the audio engine
let inputFormat = inputNode.inputFormat(forBus: 0)
// 3. Set the FFT configuration
fftSetup = vDSP_DFT_zop_CreateSetup(nil, UInt(self.bufferSize), .FORWARD)
}
1. Создаем свойство inputNode и устанавливаем его значение из аудиодвижка.
2. Создаем еще одно свойство inputFormat и устанавливаем его значение из аудиодвижка.
3. Устанавливаем конфигурацию FFT с помощью vDSP_DFT_zop_CreateSetup, подготовив Accelerate к вычислениям в частотной области.
func startMonitoring() async {
...
// Listen to microphone input
let audioStream = AsyncStream<[Float]> { continuation in
inputNode.installTap(onBus: 0, bufferSize: UInt32(bufferSize), format: inputFormat) { @Sendable buffer, _ in
// 1. Access the first audio channel
let channelData = buffer.floatChannelData?[0]
let frameCount = Int(buffer.frameLength)
// 2. Convert it into a Float array
let floatData = Array(UnsafeBufferPointer(start: channelData, count: frameCount))
// 3. Yield into the stream
continuation.yield(floatData)
}
}
}
Далее мы создаем AsyncStream, который прослушивает входной сигнал микрофона с помощью installTap(onBus:). Внутри tap:
- Получаем доступ к первому аудиоканалу
buffer.floatChannelData?[0] - Преобразуем его в массив
Floatс помощьюUnsafeBufferPointer yieldэтот массив в поток
Экземпляр audioStream связывает аудиовход с колбеком с более структурированным параллелизмом Swift 6. Пометив замыкание как @Sendable, мы гарантируем его потокобезопасность и соответствие самым последним, более строгим правилам параллельности.
Кроме того, поскольку AudioWaveformMonitor помечен @MainActor, все обновления свойств, включая изменения fftMagnitudes, происходят безопасно в основном потоке, сохраняя синхронизацию с интерфейсом, построенным с помощью SwiftUI.
func startMonitoring() async {
...
do {
// 1. Start the audioEngine
try audioEngine.start()
// 2. Update the property to monitor the state of the audioEngine
isMonitoring = true
} catch {
print("Error starting audio engine: \(error.localizedDescription)")
return
}
// 3. Retrieving the data from the audioStream
for await floatData in audioStream {
// 4. For each buffer, compute the FFT and store the results
self.fftMagnitudes = await self.performFFT(data: floatData)
}
}
Перед обработкой потока:
- Запускаем аудиодвижок
- И устанавливаем
isMonitoringвtrue, чтобы отразить активное состояние - Наконец, входим в цикл
for await(выполняющийся на главном акторе), чтобы получить данные типаfloat - Для каждого буфера вызываем асинхронную функцию
performFFT(_:)для вычисления FFT вне главного потока, чтобы избежать замедления интерфейса, затем ожидаем и сохраняем результаты вfftMagnitudes
2.2 Остановка мониторинга звуковой волны
Теперь давайте определим функцию stopMonitoring(), чтобы остановить мониторинг звука.
func stopMonitoring() {
// 1. Stop the audioEngine
audioEngine.stop()
// 2. Remove the tap from the microphone input
audioEngine.inputNode.removeTap(onBus: 0)
// 3. Reset the fftMagnitudes array to all zeros, to clear the visualization
fftMagnitudes = [Float](repeating: 0, count: Constants.sampleAmount)
// 4. Release the FFT setup free system memory
if let setup = fftSetup {
vDSP_DFT_DestroySetup(setup)
fftSetup = nil
}
// 5. Update the audioEngine state property
isMonitoring = false
}
Эта функция останавливает мониторинг звука следующим образом:
- Останавливает звуковой движок
- Отключает микрофон от линейного входа
- Затем сбрасывает массив
fftMagnitudesдо нулевых значений, обеспечивая полное очищение визуализации - Кроме того, она освобождает настройки FFT (
fftSetup), используемые для вычислений в частотной области, с помощьюvDSP_DFT_DestroySetup, помогая освободить память и системные ресурсы - Наконец, она устанавливает
isMonitoringвfalse, чтобы отразить, что мониторинг полностью остановлен
2.3 Асинхронное выполнение быстрого преобразования Фурье
Наиболее важной функцией является performFFT().
Она отвечает за асинхронное создание волшебства. Она принимает массив чисел с плавающей запятой (data), представляющих аудиообразцы, взятые с микрофона, и выполняет быстрое преобразование Фурье (FFT) с использованием фреймворка Apple Accelerate.
Хотя код вызывает функции, которые вычисляют дискретное преобразование Фурье (DFT), он использует высокооптимизированные подпрограммы, которые реализуют алгоритм быстрого преобразования Фурье (FFT).
По сути, FFT — это эффективный способ вычисления DFT, и фреймворк Apple Accelerate предоставляет эту быструю, оптимизированную версию. Поэтому мы часто называем его FFT, хотя технически мы вычисляем DFT.
func performFFT(data: [Float]) async -> [Float] {
// Check the configuration
guard let setup = fftSetup else {
return [Float](repeating: 0, count: Constants.sampleAmount)
}
// 1. Copy of the audio samples as float
var realIn = data
// 2. The imaginary part
var imagIn = [Float](repeating: 0, count: bufferSize)
// 3. The transformed values of the real data
var realOut = [Float](repeating: 0, count: bufferSize)
// The transformed values of the imaginary data
var imagOut = [Float](repeating: 0, count: bufferSize)
}
После того, как эта асинхронная функция выполняет быстрое преобразование Фурье (FFT) входящих аудиоданных, она получает их частотный состав и возвращает массив амплитуд, готовый для отображения формы волны.
Мы начинаем с проверки наличия настройки FFT. Если ее нет, мы возвращаем массив нулей длиной Constants.sampleAmount. Предполагая, что настройка действительна, мы инициализируем следующие массивы:
realInсодержит действительную часть входного аудиосигнала (данные)imagIn— мнимую часть, которая устанавливается равной нулю (поскольку входной сигнал имеет действительное значение)- И
realOutиimagOutбудут хранить преобразованные значения после применения FFT
func performFFT(data: [Float]) async -> [Float] {
...
// Property storing computed magnitudes
var magnitudes = [Float](repeating: 0, count: Constants.sampleAmount)
// 1. Nested loops to safely access all data
realIn.withUnsafeMutableBufferPointer { realInPtr in
imagIn.withUnsafeMutableBufferPointer { imagInPtr in
realOut.withUnsafeMutableBufferPointer { realOutPtr in
imagOut.withUnsafeMutableBufferPointer { imagOutPtr in
// 2. Execute the Discrete Fourier Transform (DFT)
vDSP_DFT_Execute(setup, realInPtr.baseAddress!, imagInPtr.baseAddress!, realOutPtr.baseAddress!, imagOutPtr.baseAddress!)
// 3. Hold the DFT output
var complex = DSPSplitComplex(realp: realOutPtr.baseAddress!, imagp: imagOutPtr.baseAddress!)
// 4. Compute and save the magnitude of each frequency component
vDSP_zvabs(&complex, 1, &magnitudes, 1, UInt(Constants.sampleAmount))
}
}
}
}
return magnitudes.map { min($0, Constants.magnitudeLimit) }
}
- Используя фреймворк
Accelerate, мы предоставляем прямые указатели (baseAddress!) на каждый массив, чтобы фреймворк мог взаимодействовать с данными, и с помощью последовательности вложенных замыканий (withUnsafeMutableBufferPointer) обеспечиваем безопасный доступ к памяти - На этом этапе функция выполняет дискретное преобразование Фурье (DFT) с помощью
vDSP_DFT_Execute, разбивая и преобразуя сигнал в его частотные компоненты и сохраняя результаты вrealOutиimagOut. - Здесь
DSPSplitComplex— это структура, которая хранит выходные данные DFT в виде отдельных действительных и мнимых частей. - Она используется
vDSP_zvabsдля вычисления амплитуды каждой частотной компоненты с помощью евклидовой нормы:
magnitude=√realOut²+imagOut²
Результат затем сохраняется в массиве Float под названием magnitudes.
func performFFT(data: [Float]) async -> [Float] {
...
// The returing values
return magnitudes.map { min($0, Constants.magnitudeLimit) }
Хотя буфер необработанного аудио содержит тысячи семплов (в нашем случае 8192), мы сохраняем только 200 (Constants.sampleAmount), чтобы обеспечить плавное и эффективное отображение графика.
Наконец, мы возвращаем результаты, которые сопоставляются, чтобы ограничить их максимальное значение до Constants.magnitudeLimit. Это ограничение гарантирует, что аномально громкие или шумные частоты не искажают визуальное представление.
Эти величины отражают силу каждой частоты в аудиосигнале, что является ключевым моментом для визуализации живых аудиоволн.
Шаг 3: Создание пользовательского интерфейса с помощью SwiftUI
И наконец, мы создаем интерфейс для отображения живой волны на основе данных FFT в реальном времени в нашем приложении. Мы будем использовать Swift Charts для визуализации величин в виде линейного графика.
struct ContentView: View {
// 1. AudioWaveformMonitor shared instance
private var monitor = AudioWaveformMonitor.shared
// Gradients for the chart
private let chartGradient = LinearGradient(
gradient: Gradient(colors: [.blue, .purple, .red]),
startPoint: .leading,
endPoint: .trailing
)
var body: some View {
VStack(spacing: 20) {
// Title
Text("Live Audio Waveform")
.font(.title2.bold())
.padding(.top, 20)
Spacer()
// 2. The button to start and stop the monitoring based on AudioWaveformMonitor.isMonitoring property
Button(action: {
if monitor.isMonitoring {
monitor.stopMonitoring()
} else {
Task { await monitor.startMonitoring() }
}
}) {
Label(monitor.isMonitoring ? "Stop" : "Start", systemImage: monitor.isMonitoring ? "stop.fill" : "waveform")
.font(.title2.bold())
.frame(maxWidth: .infinity)
.padding()
.background(monitor.isMonitoring ? Color.red : Color.blue)
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.horizontal)
}
}
.padding(.bottom, 20)
.padding()
}
}
Создаем представление, содержащее:
- Общий экземпляр
AudioWaveformMonitor - Кнопку для запуска и остановки мониторинга на основе свойства
AudioWaveformMonitor.isMonitoring, которая позволяет пользователю запускать и останавливать мониторинг аудио, переключая ее метку и цвет между «Start» и «Stop» в зависимости от состояния мониторинга.
Теперь импортируем фреймворк Charts и добавим следующее между заголовком и Spacer:
struct ContentView: View {
...
var body: some View {
VStack(spacing: 20) {
...
// 1. Chart
Chart(monitor.downsampledMagnitudes.indices, id: \\ .self) { index in
// 2. The LineMark
LineMark(
// a. frequency bins adjusted by Constants.downsampleFactor to spread points apart
x: .value("Frequency", index * Constants.downsampleFactor),
// b. the magnitude (intensity) of each frequency
y: .value("Magnitude", monitor.downsampledMagnitudes[index])
)
// 3. Smoothing the curves
.interpolationMethod(.catmullRom)
// The line style
.lineStyle(StrokeStyle(lineWidth: 3))
// The color
.foregroundStyle(chartGradient)
}
.chartYScale(domain: 0...max(monitor.fftMagnitudes.max() ?? 0, Constants.magnitudeLimit))
.chartXAxis(.hidden)
.chartYAxis(.hidden)
.frame(height: 300)
.padding()
// 3. Smoothing the curves
.animation(.easeOut, value: monitor.downsampledMagnitudes)
...
}
1. Добавляем диаграмму, которая визуализирует живую звуковую волну, отображая частоты FFT в формате линейного графика.
2. Используем LineMark:
- Ось x представляет частотные диапазоны, скорректированные
Constants.downsampleFactorдля распределения точек - Ось y показывает величину (интенсивность) каждой частоты, но ограничена
Constants.magnitudeLimit, чтобы избежать чрезмерных пиков
3. Для сглаживания кривых мы используем .interpolationMethod(.catmullRom) и неявную анимацию .animation(.easeOut, value: monitor.downsampledMagnitudes), которая обновляет график при каждом обновлении величин.
Интересный факт о том, почему мы выбрали анимацию .easeOut, заключается в том, что она не слишком тяжелая для обработки. Действительно, если вы попробуете использовать вместо нее анимацию .smooth, процессор начнет перегружаться из-за большого количества информации, отображаемой в интерфейсе, что приведет к замедлению работы процессора и периодическим задержкам анимации.
Конечный результат
Теперь мы разработали базовое приложение, которое может получать аудио в реальном времени с микрофона, анализировать его с помощью FFT и отображать амплитуды частот в виде динамической волны с помощью SwiftUI и Swift Charts. Эта настройка является отличной основой для создания более продвинутых приложений для обработки аудио.
Не стесняйтесь расширять его, добавляя такие функции, как распознавание звука, фильтрация аудио, запись или более сложные визуализации в соответствии с потребностями вашего проекта!
Да, мы только что нарисовали звуковую волну с помощью LineMark из Swift Chart. Но если вы ищете другие способы визуализации звука, существует бесконечное множество режимов визуализации, которые, конечно же, можно применить.
Мы решили не углубляться в подробности работы вычислений FFT в Swift, но если вы хотите узнать больше, ознакомьтесь с официальной документацией Apple для разработчиков.
В конце концов, если вам интересно, как это работает, и вы также хотите интегрировать обнаружение звука в буферы живого аудио, вот еще одна статья, который вы можете посмотреть.
-
Аналитика магазинов4 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Видео и подкасты для разработчиков4 недели назад
Разбор кода: iOS-приложение для управления личными финансами на Swift. Часть 1
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.47
-
Разработка4 недели назад
100 уроков о том, как я довёл своё приложение до продажи за семизначную сумму


