На днях Морган из 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 здесь .
