Статьи

Синтез звука в Swift: основной звуковой генератор звука

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