티스토리 뷰

최근 주변을 통해 App 에서 사운드 관련 작업의 논의를 했었다. 

나는 예전에 사운드 디자인과 엔지니어링을 했었는데 Swift 로는 작업을 해본적이 없어 이번 기회에 조금 공부해보았다.

 

이전에 Max MSP 와 Sonic Pi 에서 작업해본적이 있긴 한데, 그때처럼 play(); 하면 되는게 아닌가? 하는 쉬운 생각이 들었다.

Max MSP 와 Sonic Pi

 

그래서 AVFoundation의 오디오 파트를 훑어봤는다. EQ와 input, output 채널 등을 사용할 수 있게 되어 있었으나 내가 필요한 오실레이터(OSC, Oscillator)는 Swift 에 기본제공되지 않는다. 

오실레이터를 직접 구현해야 하고, 버퍼 등을 직접 만들어야 한다. 물론 이것도 재미있겠지만 오디오 작업에는 AudioKit 이라는 라이브러리를 많이 사용하는 것을 알게 되었다.

 

아무래도 Swift 로 오디오 작업을 하는것이 흔한 것은 아닌가보다. 자료가 많지는 않은데, AudioKit 의 Cookbook 앱이 잘되어있어서 참고하면서 어렵지 않게 작업해볼 수 있었다.

 

구현한 기능

- OSC로 파형, 노이즈 출력

- 10채널 EQ 연결

 

 

간단하게 위 형태의 앱을 만들어봤는데, OSC 로 파형을 출력하고 10채널 EQ를 적용할 수 있도록 구현했다. 

 

오디오 작업을 할 때 시그널 체인(Signal Chain)은 매우 중요하다. pre와 post의 차이에 따라 의도하고자 하는 소리가 달라질 수 밖에 없고, input 과 output 을 제대로 연결하지 않으면 소리가 실종된다. 코드로 오디오를 다룰 때도 마찬가지이다. 시그널 체인에 맞게 input과 output 을 연결해주고, 최종 아웃풋을 시스템의 output 으로 연결하면 된다. AudioKit 에서는 이러한 시그널들을 Node 객체로 관리한다. 트리 자료구조의 Node 와 동일한 역할이다.

 

ProTools 나 Logic Pro 에서는 EQ, Compressor, Mixer 가 잘 되어있지만 코드에서는 직접 연결하거나 만들어줘야 한다. AudioKit 라이브러리는 AVFoundation 을 확장하여서 기본 구현되어있는 EQ와 Mixer는 물론, OSC 와 Compressor, Audio Player, Signal Visualization, 각종 이펙트 종류들(reverb, delay 등등)을 미리 구현되어 있다.

 

코드

앞서 얘기했듯이 AudioKit 의 시그널 체인은 Node 로 연결이 된다. input과 output에 사용되는 Mixer, Fader, Audio Source 등은 모두 Node 프로토콜을 채택하고 있다.

public class Mixer: Node, NamedNode {
	...
}

public class ParametricEQ: Node {
	...
}

public class Fader: Node {
	...
}

 

그리고 아래와 같이 signal 을 객체 생성과 함께 연결해주었다.

 

private let inputMixer = Mixer()

private lazy var EQChainGroup: [ParametricEQ] = {
    var eqArray = [ParametricEQ]()
    eqParameters.enumerated().forEach { (index, parameter) in
        if index == 0 {
            eqArray.append(ParametricEQ(inputMixer))
        } else {
            eqArray.append(ParametricEQ(eqArray.last!))
        }
    }
    return eqArray
}()

private var EQChainOutput: Node {
    return EQChainGroup.last!
}

private lazy var masterFader: Fader = {
    return Fader(EQChainOutput)
}()

public func start() {
    let pinkNoise = PinkNoise()
    pinkNoise.amplitude = 0.5
    self.pinkNoise = pinkNoise

    osc.frequency = AUValue(440)
    osc.amplitude = oscParameter.gain
    osc.setWaveform(Table(.sine))

    inputMixer.addInput(pinkNoise)
    inputMixer.addInput(osc)

    pinkNoise.start()
    osc.start()

    engine.output = masterFader
    try? engine.start()
}

 

 

 

시그널의 흐름을 그림으로 그려보면 이러한 형태가 된다. 나중에 오디오 파일같은 또 다른 오디오 소스가 생긴다면, Input Mixer 에 넣어주면 된다. 혹은 오디오 소스별로 EQ를 걸어줄수도 있겠다. 

 

private func didSetEQParameter() {
    eqParameters.enumerated().forEach { (index, parameter) in
        EQChainGroup[index].centerFreq = Float(parameter.frequency)
        EQChainGroup[index].q = parameter.q
        EQChainGroup[index].gain = parameter.gain
    }
}

private func didSetOSCParameter() {
    osc.frequency = oscParameter.frequency
    osc.amplitude = oscParameter.gain

    switch oscParameter.type {
    case .sine:
        osc.setWaveform(Table(.sine))
    case .sawtooth:
        osc.setWaveform(Table(.sawtooth))
    case .triangle:
        osc.setWaveform(Table(.triangle))
    case .square:
        osc.setWaveform(Table(.square))
    }
}

private func switchNoise() {
    guard let pinkNoise else { return }
    pinkNoise.isStarted ? pinkNoise.stop() : pinkNoise.start()
}

private func switchOSC() {
    osc.isStarted ? osc.stop() : osc.start()
}

 

UI 에서 EQ와 Oscillator의 파라미터를 수정할 경우, EQ와 OSC에 접근하여 각 필드값을 수정하면 즉시 반영된다.

오랜만에 재미있는걸 해서 기분이 좋다 :) 컴프레서나 딜레이 같은거 직접 만들어보는것도 재밋겠다.

 

코드: https://github.com/jisu15-kim/AudioPlayground

 

GitHub - jisu15-kim/AudioPlayground

Contribute to jisu15-kim/AudioPlayground development by creating an account on GitHub.

github.com

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함