Разработка
Создание живой звуковой волны в 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 для разработчиков.
В конце концов, если вам интересно, как это работает, и вы также хотите интегрировать обнаружение звука в буферы живого аудио, вот еще одна статья, который вы можете посмотреть.
-
Кроссплатформенная разработка3 недели назад
Новый плагин KMP для IntelliJ IDEA и Android Studio
-
Дизайн и прототипирование4 недели назад
UI-дизайн с ChatGPT 4o
-
Аналитика промо-кампаний4 недели назад
Сравнение конверсий IAP и веб платежей в iOS-приложении
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.20