Out of Bedlam Swiftish

플레이그라운드로 배우는 AudioKit

AudioKit은 오디오 신디사이즈, 오디오 처리, 오디오 분석을 위한 오픈소스 플랫폼입니다.

회사업무로 gstreamer를 사용하여 오비오, 비디오 데이터를 다루는 작업을 한 적이 있는데 그 때 macOS, iOS상에서 오디오를 처리하는 데 활용할 수 있는 AudioKit이라는 것이 있다는 것을 알게 되었습니다. 하지만 해당 프로젝트는 리눅상에서 진행하는 것이라 AudioKit에 대해서 좀 더 알아볼 기회가 없었다가 이번에 기초적인 사항들을 정리해 보았습니다.

이 글에서는 몇 가지 Xcode 플레이그라운드를 활용하여 AudioKit의 몇 가지 기초적인 기능들을 알아보도록 하겠습니다.

시작하기

먼저 OutOfBedlam/Sample-AudioKit를 내려받은 후 Sample-AudioKit.xcworkspace파일을 열어서 Xcode를 실행합니다.

이 예제코드는 Xcode 8.1로 작성되었습니다.

위의 그림의 (1)과 같이 프로젝트의 Scheme은 AudioKit-iOS > iPhone 7 Plus로 합니다.

이제 첫 번째 예제를 통해서 AudioKit을 플레이그라운드에서 구동해 볼 차례입니다.

First AudioKit

Playground4iOS 그룹 하위의 First AudioKit 페이지를 열어보면 다음과 같은 코드를 볼 수 있습니다.

import AudioKit

let oscillator = AKOscillator()

AudioKit.output = oscillator
AudioKit.start()

oscillator.start()

sleep(10)

이 코드를 실행하려면 Xcode 메뉴 상단에 있는 실행 버튼이 아니라 그림의 (2)에 해당되는 소스코드 에디터 하단에 있는 실행 버튼을 눌러야합니다.

AudioKit이 컴파일되고 플레이그라운드 코드가 실해되면 10초간 뚜~하는 소리를 들을 수 있을 것입니다.

만약 플레이그라운드 실행시 에러가 발생한다면 Xcode를 재기동해 보십시오. Xcode 플레이그라운드에서 외부 프레임워크를 사용하는 기능은 아직 불안정하여 간혹 예상치 못한 오류가 발생하는 모습이 보입니다. Xcode가 계속 안정화되면 개선될 것으로 기대합니다.

오실레이터와 음향물리

사람들은 물체를 두들기거나, 현을 튕기는 것과 같은 다양한 방법을 이용하여 음악을 만들어 왔습니다. 수백년 수천년간 전통적인 악기들을 사용하여 오다가 최초로 전자 회로를 이용하여 소리를 만들어내는 전자 악기를 사용한 기록은 1874년 통신 분야에 종사한 엘리사 그레이(Ellisha Gray)가 오실레이터(진동자 Oscillator)를 발명한 일입니다. 그럼 가장 초보적인 형태의 음향 신디사이저라고 할 수 있는 오실레이터를 만들어 보겠습니다.

예제코드에서 Oscillators 페이지를 열어보겠습니다.

import AudioKit
import PlaygroundSupport
 
// 1. Create an oscillator
let oscillator = AKOscillator()
 
// 2. Start the AudioKit 'engine'
AudioKit.output = oscillator
AudioKit.start()
 
// 3. Start the oscillator
oscillator.start()
 
PlaygroundPage.current.needsIndefiniteExecution = true

위의 코드는 삐~ 소리를 끝없이 발생시킵니다. 정지 버튼을 눌러 중단시킬 수 있습니다. 앞에서 보았던 첫 번째 예제와 다른 점이 거의 없지만 좀 더 자세히 살펴보겠습니다.

  1. AudioKit 오실레이터(oscillator)를 생성합니다. AKOscillatorAKNode의 서브클래스입니다. 노드는 오디오 파이프 라인을 구성하는 핵심 블럭중의 한 가지입니다.
  2. AudioKit 엔진에 최종 출력 노드를 연결합니다. 이 예에서는 노드는 하나만 사용되었습니다. 여기서 엔진은 물리 엔진이나 게임 엔진과 유사합니다. 오디오 파이프 라인을 실행하려면 이 엔진을 구동하여 계속 돌아가도록 유지해야 합니다.
  3. 최종적으로 osciallator를 시작시켜 음파를 발생시킵니다.

오실레이터는 무한히 지속되는 반복적이면서 주기적인 신호를 생성합니다. 이 예제 플레이그라운드의 AKOscillator는 사인파(sine wave)를 생성합니다. 이 디지털 사인파는 AudioKit에 의해 처리되어 컴퓨터의 스피커나 헤드폰을 통해서 물리적으로 동일한 사인파 진동인 소리로 출력됩니다. 이 소리는 공기중의 압력파로 사람의 귀로 전달됩니다.

오실레이터가 어떤 소리를 낼지 결정하는데에는 두 가지 인자가 있습니다. 첫번째로 사인파의 높이인 진폭(amplitude)은 얼마나 소리가 큰지를 결정하고 두번째 주파수(frequency)는 소리의 높이 (음의 고저)를 결정합니다.

플레이그라운드에서 AKOscillator를 생성한 후 다음과 같이 수정해 보겠습니다.

oscillator.frequency = 300
oscillator.amplitude = 0.5

소리를 잘 들어 보면 크기가 절반으로 줄고 더 저음의 소리가 나는 것을 알 수 있습니다. 주파수 (frequency)는 헤르츠(hertz 초당 사이클) 단위로 음의 높이를 지정합니다. 진폭(amplitude)은 0에서 1 사이의 값으로 지정합니다.

불행히도 엘리사 그레이는 간발의 차이로 그래헴 벨에게 세계 최초의 전화기 발명 특허권을 놓쳤으나 자신이 발명한 오실레이터를 통해서 최초의 전자 악기의 특허를 받게됩니다.

앞의 예제에 아래의 코드를 적용하면 바이브레이션을 추가할 수 있습니다.

oscillator.rampTime = 0.2
oscillator.frequency = 500
AKPlaygroundLoop(every: 0.5) {
  oscillator.frequency =
    oscillator.frequency == 500 ? 100 : 500
}

rampTime 속성은 오실레이터의 frequency 또는 amplitude와 같은 속성값이 부드럽게 변경되도록 만들어 줍니다. AudioKit에서 제공하는 AKPlaygroundLoop를 사용해서 주기적으로 코드를 실행할 수 있습니다. 이 예에서는 오실레이터의 주파수를 500Hz에서 100Hz로 0.5초마다 변경하도록 만들었습니다.

Sound Envelopes

악기가 음을 낼때, 진폭(소리의 크기)은 시간에 따라 달라지며 이 또한 악기별로 특색이 있습니다. 이러한 효과를 모델링한 것을 Attack-Decay-Sustain-Release(줄여서 ADSR) Envelope이라고 합니다.

이 envelope을 구성하는 요소는

  • Attack: 최대 볼륨으로 올라가는데 걸리는 시간
  • Decay: 올라간 볼륨이 sustain 수준으로 떨어지는데 걸리는 시간
  • Sustain: decay이후에 release가 시작될 때까지 유지되는 볼륨의 레벨
  • Release: 볼륨이 0으로 떨어지는데 걸리는 시간

예를 들어 해머가 현을 쳐서 소리를 내는 피아노는 아주 간결한 attack과 빠른 decay가 특징이고 현을 활로 켜서 연주하는 바이올린은 피아노보다 긴 attack과 decay와 sustain이 특징입니다.

ADSR를 적용한 예를 보겠습니다.

import AudioKit
import PlaygroundSupport
 
let oscillator = AKOscillator()

이 부분은 이전의 코드처럼 오실레이터를 생성하는 것이므로 익숙할 것입니다. 다음의 코드를 추가합니다.

let envelope = AKAmplitudeEnvelope(oscillator)
envelope.attackDuration = 0.01
envelope.decayDuration = 0.1
envelope.sustainLevel = 0.1
envelope.releaseDuration = 0.3

이 코드에서는 ADSR envelope를 정의하는 AKAmplitudeEnvelope를 생성합니다. 여기서 시간을 나타내는 duration 파라미터들을 초단위이며 level은 진폭(amplitude)으로 0에서 1사이의 값을 사용합니다.

AKAmplitudeEnvelope도 역시 AKOscillator와 마찬가지로 AKNode의 서브클래스입니다. 위의 코드에서 oscillator를 envelope에 생성자로 전달하여 두 노드들을 서로 연결한다는 것을 알 수 있습니다.

이제 다음의 코드를 추가합니다.

AudioKit.output = envelope
AudioKit.start()
 
oscillator.start()

이렇게 AudioKit 엔진을 시작하면, oscillator에서 ADSR envelope을 통해서 출력이 만들어 집니다.

envelope의 효과를 들어보기위해서는 노드를 시작하고 중지하는 일을 반복해야합니다. 이를 위해서 아래와 같은 코드가 마지막으로 필요합니다.

AKPlaygroundLoop(every: 0.5) {
  if (envelope.isStarted) {
    envelope.stop()
  } else {
    envelope.start()
  }
}
 
PlaygroundPage.current.needsIndefiniteExecution = true

이제 동일한 음이 반복적으로 발생하는 것을 들을 수 있는데, 이제 소리가 좀 더 피아노와 비슷해 졌습니다.

이 루프(loop)는 초당 두번씩 실행되어 한 번은 ADSR을 시작하고 한번은 중지합니다. 루프가 시작되면 최대 음량으로 0.01초에 도달하는 빠른 attack에 이어서 0.1초간의 decay로 sustain 레벨(level)에 도달하여 0.5초간 유지됩니다. 최종 release는 0.3초간 진행됩니다.

ADSR 값들을 변경하여 바이올린과 같은 다른 악기의 음색을 만들어 낼 수 있습니다.

지금까지 살펴본 음향들은 AKOsciallator를 통해 만들어내는 사인파(sine wave)들입니다. 오실레이터를 사용해서도 곡을 연주할 수도 있지만 이걸 음악이라고 하기에는 좀 부족합니다.

다음으로 좀 더 풍부한 소리를 만드는 방법에 대해서 살펴보겠습니다.

가음합성 (Additive Sound Synthesis)

모든 악기들은 특유의 음색(timbre)이 있습니다. 음색의 차이 때문에 피아노와 바이올린이 똑같은 음을 연주해도 소리가 서로 다른 것입니다. 음색의 중요한 속성은 그 악기가 만들어 내는 음향의 스펙트럼입니다. 스펙트럼은 하나의 음을 연주했을 때 만들어지는 주파수의 범위라고 할 수 있습니다. 앞서의 오실레이터 플레이그라운드 예에서는 단일 주파수만을 만들어냈기 때문에 인공적인 소리로 들렸던 것입니다.

단일 음을 생성하는 오실레이터 여러 개를 뱅크(bank)로 묶어서 출력함으로써 실제 악기에 가까운 신디사이즈를 만들수 있습니다. 이러한 방식을 additive synthesis라고 합니다.

아래의 예는 Additive Synthesis 플레이그라운드 페이지의 코드입니다.

import AudioKit
import PlaygroundSupport
 
func createAndStartOscillator(frequency: Double) -> AKOscillator {
  let oscillator = AKOscillator()
  oscillator.frequency = frequency
  oscillator.start()
  return oscillator
}

additive synthesis를 위해서는 여러 개의 오실레이터가 필요합니다. 여러 개의 오실레이터를 간편하게 만들기위해서 createAndStartOscillator함수를 사용하도록 하겠습니다.

let frequencies = (1...5).map { $0 * 261.63 }

범위 연산자와 map을 사용해서 5개의 주파수 범위를 생성합니다. 여기서 261.63은 표준 건반에서의 가운데 위치한 C(도) 음의 주파수입니다. 이 주파수를 기준으로 배수로 다른 주파수들을 만들어 화성을 구성하는 것입니다.

let oscillators = frequencies.map {
  createAndStartOscillator(frequency: $0)
}

이제 개별 주파수를 생성하는 오실레이터들을 만든 후 아래에서 그 오실레이터들을 합칩니다.

let mixer = AKMixer()
oscillators.forEach { mixer.connect($0) }

AKMixer는 AudioKit의 노드 중의 한 가지로 하나 이상의 노드들의 출력을 받아서 하나의 출력으로 만들어 줍니다.

let envelope = AKAmplitudeEnvelope(mixer)
envelope.attackDuration = 0.01
envelope.decayDuration = 0.1
envelope.sustainLevel = 0.1
envelope.releaseDuration = 0.3
 
AudioKit.output = envelope
AudioKit.start()
 
AKPlaygroundLoop(every: 0.5) {
  if (envelope.isStarted) {
    envelope.stop()
  } else {
    envelope.start()
  }
}

위의 코드는 이전의 예에서 봐왔던 것봐 비슷하므로 이해하기 어렵지 않습니다. 여기서는 SDSR에 mixer의 출력을 연결하고 주기적으로 시작(start)과 중지(stop)를 반복합니다.

additive synthesis가 어떻게 동작하는지 보려면 주파수들을 다양하게 조합해보는 것이 좋은데 그렇게 하기위해서 플레이그라운드의 라이브-뷰(live-view)를 이용하면 편리합니다.

class PlaygroundView: AKPlaygroundView {
 
  override func setup() {
    addTitle("Harmonics")
 
    oscillators.forEach {
      oscillator in
      let harmonicSlider = AKPropertySlider(
        property: "\(oscillator.frequency) Hz",
        value: oscillator.amplitude
      ) { amplitude in
        oscillator.amplitude = amplitude
      }
      addSubview(harmonicSlider)
    }
 
  }
}
 
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = PlaygroundView()

AudioKit에는 아래에서 설명하는 것처럼 인터렉티브한 플레이그라운드를 만들 수 있는 다수의 클래스들이 지원합니다.

위의 코드에서 PlaygroundViewAKPlaygroundView를 상속하여 서브 뷰들(subviews)을 수직으로 배치해 줍니다. setup메서드 내에서 각 오실레이터마다 AKPropertySlider를 만들며 각 슬라이더는 개별 오실레이터의 주파수(frequency)와 진폭(amplitude)을 조정할수 있도록 콜백을 처리합니다.

위의 코드의 결과를 보기위해서는 Xcode상에서 플레이그라운드 라이브-뷰가 보여지도록 해야합니다. Xcode의 오른쪽 최상단의 두개의 원이 연결된 아이콘(아래 그림에서 1번으로 표시)을 클릭해서 어시스턴트 뷰(assistant view)를 표시하면 됩니다. 각 슬라이드의 진폭을 조정하여 악기의 음색을 변경할 수 있습니다.

실행시 컴파일 오류도 없는데 아래 화면처럼 보여지지 않는다면 Xcode를 재기동해봐야합니다. Xcode의 플레이그라운드 라이브뷰는 아직 완벽하지 않은 듯 가끔 기대하는 대로 동작하지 않는 경우가 있습니다.

폴리포니 (Polyphony)

지금까지의 플레이그라운드는 한 번에 하나의 음만 사용하였습니다. 많은 악기들이 보통 동시에 여러 개의 음을 연주할 수 있습니다. 이러한 악기를 폴리포닉(polyphonic)이라고 하는데 여기에 대비해서 하나의 음만 낼 수 있는 것을 모노포닉(monophonic)이라고 합니다.

폴리포닉(polyphonic) 음를 만들려면 복수의 오실레이터를 만들어서 각각 다른 음을 연주하게 해서 믹서노드(mixer node)로 연결하면 됩니다. 이런 복잡한 과정을 간단하게 하려면 AudioKit의 오실레이터 뱅크(oscillator bank)를 사용하면 됩니다.

아래의 예는 예제 플레이그라운드의 Polyphony 페이지를 열어서 참고하시기 바랍니다.

import PlaygroundSupport
import AudioKit
 
let bank = AKOscillatorBank()
AudioKit.output = bank
AudioKit.start()

오실레이터 뱅크를 생성하여 AudioKit의 출력으로 설정합니다. AKOscillatorBank의 소스 코드를 따라 올라가 보면 이 클래스도 AKNode를 상속하였다는 것을 알 수 있으며 동시에 AKPolyphonic프로토콜을 따르도록 되어 있다는 것을 알 수 있습니다.

결과적으로 오실레이터 뱅크도 AudioKit의 다른 노드들과 비슷하게 mixer, envelope, filter와 같은 효과들을 적용할 수 있습니다.

이 오실레이터를 시험해보려면 복수의 음을 한번에 연주할 수 있는 방법이 필요한데 다음의 코드를 사용해서 플레이그라운드 라이브-뷰에 건반을 표시하도록 하겠습니다.

class PlaygroundView: AKPlaygroundView {
  override func setup() {
    let keyboard = AKKeyboardView(width: 440, height: 100)
    addSubview(keyboard)
  }
}
 
PlaygroundPage.current.liveView = PlaygroundView()
PlaygroundPage.current.needsIndefiniteExecution = true

플레이그라운드가 컴파일되면 다음과 같은 건반을 볼 수 있습니다.

AKKeyboardView는 AudioKit의 기능을 시험하는데 사용할 수 있는 여러가지 유틸리티 중의 한 가지입니다. 건반을 클릭해 보면 아직 소리가 나지 않습니다.

이제 건반이 소리를 낼 수 있도록 다음과 같이 PlaygroundViewsetUp메서드를 수정합니다.

let keyboard = AKKeyboardView(width: 440, height: 100)
keyboard.delegate = self
addSubview(keyboard)

이 코드는 건반-뷰에 대한 델리게이트를 PlaygroundView클래스 자신으로 설정합니다. 델리게이트를 통해서 건반이 클릭되었을 때 반응할 수 있도록 만들 수 있습니다.

클래스의 정의를 다음과 같이 수정합니다.

class PlaygroundView: AKPlaygroundView, AKKeyboardDelegate

이렇게 AKKeyboardDelegate를 따르도록 하고 아래의 메서드들을 프로토콜에 따라 정의합니다.

func noteOn(note: MIDINoteNumber) {
  bank.play(noteNumber: note, velocity: 80)
}
 
func noteOff(note: MIDINoteNumber) {
  bank.stop(noteNumber: note)
}

이제 건반을 클릭할 때마다 델리게이트의 noteOn 메서드가 호출됩니다. 이 메서드의 구현은 직관적으로 오실레이터 뱅크의 play를 호출하며 noteOff메서드에서는 stop메서드를 호출합니다.

이제 건반을 클릭하거나 마우스가 미끄러지도록 해 보면 훌륭하게 연주되는 것을 들을 수 있습니다. 이 오실레이터는 이미 ADSR 기능을 포함하고 있다는 것을 소리로도 확인할 수 있을 것입니다.

키보드에서 제공되는 음의 정보에는 주파수(frequency)가 정의되어 있지 않다는 것을 알아챘을 것입니다. 주파수 대신 MIDINoteNumber 타입을 사용하고 있습니다. 이 타입의 정의를 따라가 보면 다음과 같습니다.

public typealias MIDINoteNumber = Int

MIDI는 Musical Instrument Digital Interface의 머릿글자로 악기간의 통신에 폭 넓게 이용되는 형식입니다. 음표의 숫자는 표준 키보드의 음에 대응됩니다. play 메서드의 두 번째 파라미터는 속도입니다. 마찬가지로 표준 MIDI 속성으로 얼마나 세게 해당 음을 쳤는가를 나타냅니다. 낮은 값일 수록 사용자가 부드럽게 건반을 눌렀다는 뜻이며 좀 더 조용한 소리를 만듭니다.

마지막 단계는 키보드를 폴리포닉(polyphonic) 모드로 설정하는 것입니다. setup메서드 마지막에 다음과 같은 코드를 추가합니다.

keyboard.polyphonicMode = true

이제 아래 그림과 같이 (C장조) 복수의 음을 동시에 연주할 수 있게 되었습니다.

샘플링

지금까지 살펴본 사운드 신디사이즈 기술은 오실레이터, 필터, 믹서와 같은 기본 골격으로 부터 실제에 가까운 소리를 만들어내는 시도에 관한 것이었습니다. 1970년대 컴퓨터 프로세스 파워와 저장소의 증가에 따라 사운드 샘플링이라는 완전히 다른 접근이 가능해 졌습니다. 사운드 샘플링은 소리에 대한 디지털 복사본을 만드는 것을 말합니다.

샘플링이란 상대적으로 단순한 개념으로 디지털 사진의 원리와 동일합니다. 자연의 소리는 부드러운 파형으로 나타나는데 샘플링 과정은 단순히 음파의 진폭(amplitude)을 주기적인 간격으로 기록하는 것입니다.

캡쳐한 소리가 얼마나 원래의 소리에 가까운지를 결정하는 데에는 두 가지 중요한 요소가 있습니다.

  • Bit depth: 샘플러(sampler)가 재현할 수 있는 진폭의 단위 레벨의 수로 표현
  • Sample rate: 얼마나 자주 진폭 측정이 이루어졌는가, 헤르츠(hertz)로 측정

플레이그라운드의 Samples 페이지에서 이 속성들에 대해서 살펴보겠습니다.

import PlaygroundSupport
import AudioKit
 
let file = try AKAudioFile(readFileName: "climax-disco-part2.wav", baseDir: .resources)
let player = try AKAudioPlayer(file: file)
player.looping = true

위의 코드는 예제 소리 파일을 로드해서 플레이어가 반복적으로 재생할 수 있도록 설정합니다.

최종적으로 다음의 코드를 추가합니다.

AudioKit.output = player
AudioKit.start()
 
player.play()
 
PlaygroundPage.current.needsIndefiniteExecution = true

이렇게 하면 오디오 플레이어를 AudioKit엔진에 연결하고 재생을 시작합니다. 이런 단순한 샘플링 루프를 사용하면 오실레이터로는 구성하기 힘든 다양한 소리들을 만들어 낼 수 있습니다.

대중적으로 사용하는 MP3 사운드는 보통 더 놓은 bit depthsample rate를 사용합니다.

let bitcrusher = AKBitCrusher(player)
bitcrusher.bitDepth = 16
bitcrusher.sampleRate = 40000

그리고 AudioKit의 출력을 수정합니다.

AudioKit.output = bitcrusher

이 플레이그라운드의 출력은 동일한 샘플을 사용했지만 이전과 상당히 달라진 금속성 소리가 납니다.

AKBitCrusher는 AudioKit의 효과들 (effect) 중의 한 가지로 bit depthsample rate을 줄여주는 효과를 냅니다.

마지막으로 여러 개의 노드들을 함께 엮어서 스테레오 딜레이 효과 (streo delay effect)을 만들어 보겠습니다. 먼저 bit crusher를 생성하는 코드 라인을 지우고 대신 다음과 같이 입력합니다.

let delay = AKDelay(player)
delay.time = 0.1
delay.dryWetMix = 1

이 코드는 입력으로 사용되는 샘플 루프에 대해 0.1초의 지연 효과를 만들어 냅니다. wet/dry 믹스 값은 지연된 오디오와 지연되지 않은 오디오를 함께 섞을 수 있도록 합니다. 이 경우에는 해당 노드가 지연된 오디오만 출력하도록 지정하기위해서 1을 설정했습니다.

let leftPan = AKPanner(player, pan: -1)
let rightPan = AKPanner(delay, pan: 1)

AKPanner 노드는 왼쪽, 오른쪽 또는 그 중간 어디쯤으로 오디오를 옮기는 역할을 합니다. 위의 코드에서는 지연된 오디오는 왼쪽으로 보내고 지연되지 않은 오디오는 오른쪽으로 보냅니다.

최종 단계는 이 두 가지 오디오를 다시 믹스하는 과정입니다. 믹서를 생성하여 AudioKit의 출력으로 설정합니다.

let mix = AKMixer(leftPan, rightPan)
AudioKit.output = mix

이제 예제 코드는 동일한 샘플을 재생하지만 왼쪽과 오른쪽 스피커 사이에 약간의 지연이 만들어 집니다.

마무리

이 번 AudioKit 튜토리얼에서는 다룬 내용은 AudioKit으로 할 수 있는 일들의 극히 일부분에 지나지 않습니다. moog 필터, pitch (음높이) shifter, 그래픽 이퀄라이저를 살펴보기 바랍니다.

AudioKit을 활용하여 약간의 창조성만 더 한다면 자신만의 사운드, 전자악기, 게임 효과음을 만들 수 있습니다.