На днях Морган из Swift London отметил, что может быть интересно использовать пользовательский интерфейс на основе моего узла Swift в качестве основы для синтезатора звука. Всегда готовый к испытаниям, я потратил немного времени на изучение Core Audio, чтобы понять, как это можно сделать, и создал небольшое демонстрационное приложение: многоканальный генератор тонов .
В прошлом я сделал нечто не слишком отличающееся от Flash . В этом проекте мне пришлось вручную создать 8192 семпла для синтеза тона, и в итоге я использовал ActionScript Workers для выполнения этой работы в фоновом режиме. После некоторых раздумий кажется, что в Core Audio подобный подход можно использовать , но для создания чистых синусоидальных волн есть гораздо более простой способ: использовать Audio Toolbox .
К счастью, я нашел эту прекрасную статью по Джину Де Лизе . Его проект SwiftSimpleGraph имеет весь стандартный код для создания синусоидальных волн, поэтому все, что мне нужно было сделать, это добавить пользовательский интерфейс.
Мой проект содержит четыре экземпляра ToneWidget , каждый из которых содержит числовые циферблаты для частоты и скорости и средство визуализации синусоидальной волны . Также имеется дополнительный синусоидальный рендер, который отображает составную частоту всех четырех каналов.
Рендерер синусоидальной волны создает растровое изображение волны на основе некоторого кода, который Джозеф Лорд подправил в моем коде распространения реакции Swift летом. Он предоставляет метод setFrequencyVelocityPairs (), который принимает массив экземпляров FrequencyVelocityPair . Когда это вызывается, метод drawSineWave () перебирает массив, создавая и суммируя вертикальные значения для каждого столбца в растровом изображении:
for i in 1 ..< Int(frame.width) { let scale = M_PI * 5 let curveX = Double(i) var curveY = Double(frame.height / 2) for pair in frequencyVelocityPairs { let frequency = Double(pair.frequency) / 127.0 let velocity = Double(pair.velocity) / 127.0 curveY += ((sin(curveX / scale * frequency * 5)) * (velocity * 10)) } [...]
Затем вместо простого построения сгенерированной позиции для каждого столбца он рисует линию между вновь рассчитанной вертикальной позицией для текущего столбца и предыдущей вертикальной позицией для предыдущего столбца:
for yy in Int(min(previousCurveY, curveY)) ... Int(max(previousCurveY, curveY)) { let pixelIndex : Int = (yy * Int(frame.width) + i); pixelArray[pixelIndex].r = UInt8(255 * colorRef[0]); pixelArray[pixelIndex].g = UInt8(255 * colorRef[1]); pixelArray[pixelIndex].b = UInt8(255 * colorRef[2]); }
Результатом является непрерывная волна:
Поскольку средство визуализации синусоидальной формы принимает массив значений, я могу использовать один и тот же компонент как для отдельных каналов (где длина массива равна единице), так и для составного из всех четырех (где длина массива равна четырем).
Я был готов использовать GCD для рисования синусоидальной волны в фоновом потоке, но и симулятор, и мой старый iPad с радостью делают это в основном потоке, сохраняя пользовательский интерфейс полностью отзывчивым.
В контроллере представления у меня есть массив, содержащий каждый виджет, и другой массив, содержащий воспроизводимые в данный момент заметки, который заполняется внутри viewDidLoad ():
override func viewDidLoad() { super.viewDidLoad() view.addSubview(sineWaveRenderer) for i in 0 ... 3 { let toneWidget = ToneWidget(index: i, frame: CGRectZero) toneWidget.addTarget(self, action: "toneWidgetChangeHandler:", forControlEvents: UIControlEvents.ValueChanged) toneWidgets.append(toneWidget) view.addSubview(toneWidget) currentNotes.append(toneWidget.getFrequencyVelocityPair()) soundGenerator.playNoteOn(UInt32(toneWidget.getFrequencyVelocityPair().frequency), velocity: UInt32(toneWidget.getFrequencyVelocityPair().velocity), channelNumber: UInt32(toneWidget.getIndex())) } }
Когда пользователь изменяет частоту или скорость в любом из этих виджетов, я использую значение в массиве currentNotes, чтобы отключить текущую воспроизводимую ноту, обновить основной рендерер синусоидальной волны и воспроизвести новую ноту:
func toneWidgetChangeHandler(toneWidget : ToneWidget) { soundGenerator.playNoteOff(UInt32(currentNotes[toneWidget.getIndex()].frequency), channelNumber: UInt32(toneWidget.getIndex())) updateSineWave() soundGenerator.playNoteOn(UInt32(toneWidget.getFrequencyVelocityPair().frequency), velocity: UInt32(toneWidget.getFrequencyVelocityPair().velocity), channelNumber: UInt32(toneWidget.getIndex())) currentNotes[toneWidget.getIndex()] = toneWidget.getFrequencyVelocityPair() }
Метод updateSineWave () просто перебирает виджеты, получает каждое значение и передает их в средство визуализации синусоидальной волны:
func updateSineWave() { var values = [FrequencyVelocityPair]() for widget in toneWidgets { values.append(widget.getFrequencyVelocityPair()) } sineWaveRenderer.setFrequencyVelocityPairs(values) }
Еще раз, большое спасибо Джину Де Лизе, который действительно сделал всю тяжелую работу, проводя исследование Core Audio и Audio Toolbox API по этому вопросу. Весь исходный код этого проекта доступен в моем репозитории GitHub здесь .