본문 바로가기

프로그래밍/iOS,macOS

[iOS] CoreAudio AudioUnit

Audio Unit Hosting Fundamentals (apple.com)

 

Audio Unit Hosting Fundamentals

Audio Unit Hosting Fundamentals All audio technologies in iOS are built on top of audio units, as shown in Figure 1-1. The higher-level technologies shown here—Media Player, AV Foundation, OpenAL, and Audio Toolbox—wrap audio units to provide dedicated

developer.apple.com

AudioUnit 은 2개의 Element 이루어져 있으며, 각 element의 input 측을 input scope, output측을 output scope 로 정의한다. 0번을 OutputElement, 1번을 InputElement 로 사용한다. 물리적인 장치에서 전달되는 신호의 흐름을 이야기하는 경우에 input bus, output bus 라는 용어가 사용되기도 하는데, element와 bus는 같은 요소를 나타내는 용어이다. (bus 0번이 출력, 1번이 입력이고, 버스의 입력측이 input scope, 출력측이 output scope)

Remote I/O unit의 경우 output element의 output scope는 출력장치에 연결되어 있으며, input element의 input scope 은 입력장치, input element 의 output scope가 output element의 input에 연결하는 구조이다.

 

input element output과 output element 의 input 사이에 별도의 audio unit 들을 구성할수 있으며, io의 input element 의 input을 사용하지 않고, 별도의 입력장치를 사용하는 것도 가능하다. 

 

 

 

 

AudioComponentDescription

 

Identifier keys for Audio Units

 

Using Specific Audio Units

Using Specific Audio Units Each iOS audio unit has certain things in common with all others and certain things unique to itself. Earlier chapters in this document described the common aspects, among them the need to find the audio unit at runtime, instanti

developer.apple.com

 

IO 오디오 유닛 생성

IO 오디오 컴포넌트 생성을 위한 desc 생성

AudioComponentDescription audioCompoDesc;
audioCompoDesc.componentType = kAudioUnitType_Output;
audioCompoDesc.componentSubType = kAudioUnitSubType_VoiceProcessingIO;
audioCompoDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioCompoDesc.componentFlags = 0;
audioCompoDesc.componentFlagsMask = 0;


// swift
val audioCompoDesc = AudioComponentDescription(
    componentType: kAudioUnitType_Output,
    componentSubType: kAudioUnitSubType_VoiceProcessingIO,
    componentManufacturer: kAudioUnitManufacturer_Apple,
    componentFlags: 0,
    componentFlagsMask: 0 )
    

 

 

해당 컴포넌트 검색

AudioComponent found = AudioComponentFindNext( nullptr, &audioCompoDesc );

// swift
let found = AudioComponentFindNext( nil, &audioCompoDesc )

 

 

오디오 유닛 인스턴싱

AudioUnit audioUnit;
AudioComponentInstanceNew( found, &audioUnit );

 

 

장치 입출력 여부 설정

해당 유닛의 입력, 출력 scope 중 어떤 element 를 활성화할 지 정의한다. 

input scope 의 경우 element 1번

output scope의 경우 element 0번을 활성화한다.

static const AudioUnitElement kOutputBus = 0; // 스피커
static const AudioUnitElement kInputBus = 1; // 마이크
// objC
UInt32 enable_input = 1;
UInt32 enable_output = 1;

AudioUnitSetProperty( unit, 
    kAudioOutputUnitProperty_EnalbeIO,
    kAudioUnitScope_Input,
    kInputBus,
    &enable_input, sizeof( enable_input ));


// swift
AudioUnitSetProperty( unit!,
    kAudioOutputUnitProperty_EnableIO,
    kAudioUnitScope_Output,
    kOutputBus,
    &enable_output, UInt32(MemoryLayout<UInt32>.size))

 

스트림의 정보 설정

입력, 출력 장치는 하드웨어에서 강제한 입출력 스트림 포맷을 사용한다. 하드웨어와 연결되는 부분을 제외한 input element의 output과 output element의 input 연결의 경우 스트림 포맷을 정의해 주어야 한다.

(해당 포맷을 지정하지 않으면 콜백에서 렌더시에 데이터가 전달되지 않음.)

 

연결될 element의 스트림포맷을 위한 구조체 설정

// objC
struct AudioStreamBasicDescription
{
    Float64             mSampleRate;
    AudioFormatID       mFormatID;
    AudioFormatFlags    mFormatFlags;
    UInt32              mBytesPerPacket;
    UInt32              mFramesPerPacket;
    UInt32              mBytesPerFrame;
    UInt32              mChannelsPerFrame;
    UInt32              mBitsPerChannel;
    UInt32              mReserved;
}


// swift
let foramt = AudioStreamBasicDescription(
    mSampleRate : Double(8000),
    mFormatID : kAudioFormatLinearPCM,
    mFormatFlags : kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
    mBytesPerPacket : UInt32( channel * 2 ),   // 16bit
    mFramesPerPacket : 1,
    mBytesPerFrame : UInt32( channel * 2),
    mChannelsPerFrame : UInt32( channel ),
    mBitsPerChannel : UInt32( 8 * 2),
    mReserved: UInt32(0))

위처럼 2바이트(16비트) 의 고정된 크기의 패킷인 경우 해당 크기만 지정해 주면 되는데, 가변 포맷의 경우 추가 정보가 필요하다. 가변 포맷은 AudioStreamPacketDescription 구조체가 필요하며, 가변적인 값인 mBytePerPacket, mBytesPerFrame 등의 값은 0으로 설정한다.

 

 

 

스트림 포맷 적용

아래와 같이 해당하는 scope와 bus에 스트림 포맷을 지정해 준다.

// objC
AudioUnitSetPrpoperty( audioUnit, 
    kAudioUnitProperty_SteramForamat, 
    kAudioUnitScope_Output, 
    kInputBus,
    &format, sizeof( format ) );
   
   
// swift   
AudioUnitSetPrpoperty( audioUnit!, 
    kAudioUnitProperty_SteramForamat, 
    kAudioUnitScope_Input, 
    kOutputBus, 
    &format, UInt32( MemoryLayout<AudioStreamBasicDescription>.size );

 

 

연결

위 IO 유닛의 경우는 input element 의 output과 ouput element의 input이 연결되어 있다.

아래는 이미 연결되어 있으므로 처리가 필요없는 부분이나 IO유닛 외에는 별도의 연결 처리가 필요하므로 참고.


// remote io의 경우 2개의 element를 가지고 있으므로, input bus를 output bus에 연결
AudioUnitConnection unitConnection;
unitConnection.sourceAudioUnit = audioUnit; // source
unitConnection.sourceOutputNumber = 1; // remote io의 input element
unitConnection.destInputNumber = 0; // remote io의 output element

// remote io의 output element의 input scope에 연결
AudioUnitSetProperty( audioUnit,
                      kAudioUnitProperty_MakeConnection,
                      kAudioUnitScope_Input,  
                      0, // output element
                      &unitConnection,
                      sizeof( unitConnection ) );

 

필터류 유닛들은 보통 element가 한개 이므로, 입력, 출력에 대해 연결을 처리

 

 

 

프로퍼티 값 가져오기

Voice IO Auto Gain Control

// objC
UInt32 enabled = 0
UInt32 size = sizeof(enabled)
AudioUnitGetProperty( audioUnit,
    kAUVoiceIOProperty_VoiceProcessingEnableAGC,
    kAudioUnitScope_Global,
    kInputBus,
    &enabled, 
    &size);


// swfit
var enabled:UInt32 = 0
var size:UInt32 = UInt32(MemoryLayout<UInt32>.size)
AudioUnitGetProperty( audioUnit,
    kAUVoiceIOProperty_VoiceProcessingEnableAGC,
    kAudioUnitScope_Global,
    kInputBus,
    &enabled, 
    &size);

 

 

프로퍼티 목록

https://developer.apple.com/documentation/audiounit/audio_unit_properties

 

Audio Unit Properties | Apple Developer Documentation

Properties for audio units that perform offline processing—that is, processing in a nonplayback, nonrealtime mode.

developer.apple.com

 

파라미터

믹서나 각종 필터류 값 설정을 위한 파라미터

 

https://developer.apple.com/documentation/audiounit/audio_unit_parameters

 

Audio Unit Parameters | Apple Developer Documentation

An audio unit parameter is a key, defined by the audio unit it applies to, whose corresponding value specifies the setting for an adjustable attribute such as volume, pitch, or filter cutoff frequency. Parameters are typically varied by the user during aud

developer.apple.com

low pass filter의 파라미터 설정 예

AudioUnitSetParameter(effectUnit,   // AudioUnit
    kLowPassParam_CutoffFrequency,  // AudioUnitParameterID
    kAudioUnitScope_Global,         // AudioUnitScope
    0,                              // AudioUnitElement
    100,                            // AudioUnitParameterValue
    0);                             // UInt32

 


콜백

연결된 오디오 유닛들은 input을 받기위해 이전 오디오 unit의 콜백을 호출하게 된다.

 

콜백 구조

콜백 설정을 위한 구조체
// objC
struct AURenderCallbackStruct {
    AURenderCallback __nullable inputProc;
    void* __nullable inputProcRefCon;
}

// swift
public struct AURenderCallbackStruct {
    public var inputProc: AURenderCallback?
    public var inputProcRefCon: UnsafeMutableRawPointer?
}




// objC 
OSStatus onCallback(
    void* in_ref_con,
    AudioUnitRenderActionFlags* ioActionFlags,
    const AudioTimeStamp* time_stamp,
    UInt32 bus_number, 
    UInt32 num_frames,
    AudioBufferList* io_data) {
}

// Swift
func onCallback(
    inRefCon: UnsafeMutablePointer<Void>,
    ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
    inTimeStamp: UnsafePointer<AudioTimeStamp>,
    inBufNumber: UInt32,
    inNumberFrames: UInt32,
    ioData: UnsafeMutablePointer<AudioBufferList>) -> OSStatus {
    
 }

 

입력 콜백

입력 콜백은 read, write 를 위한 콜백이며, 해당 콜백 호출되면 AudioUnitRender() 를 통해 렌더링을 수행해야 한다.
입력 레더링이 완료되었다는 콜백이 아닌 준비가 되었으니 렌더링을 수행하라는 콜백이므로 주의.

global scope를 사용

// objC
AURenderCallbackStruct inputCallback;
inputCallback.inputProc = onInputCallback
inputCallback.inputProcRefCon = this;

AudioUnitSetProperty( audioUnit,
    kAudioOutputUnitProperty_SetInputCallback,
    kAudioUnitScope_Global,
    kInputBus,
    &callback, sizeof( inputCallback));

// 콜백함수
OSStatus onInputCallback( 
    void* in_ref_con,
    AudioUnitRenderActionFlags* ioActionFlags,
    const AudioTimeStamp* inTimeStamp,
    UInt32 inBusNumber,
    UInt32 inNumFrames,
    AudioBufferList* ioData) { }


    
// swift
// 콜백 구조체 생성
var inputCallback = AURenderCallbackStruct( 
    inputProc: onInputCallback, 
    inputProcRefCon: UnsafeMutableRawPointer( Unmanaged.passUnretained(self).toOpaque()))

// 콜백 정보를 프로퍼티에 설정
AudioUnitSetProperty( audioUnit,
    kAudioOutputUnitProperty_SetInputCallback,
    kAudioUnitScope_Global,
    1,
    &inputCallback,
    UInt32( MemoryLayout<AURenderCallbackStruct>.size ))
    

// 실제 콜백 함수
static let onInputCallback:AURenderCallback = { (
    inRefCon,
    ioActionFlags,
    inTimeStamp,
    inBusNumber,
    frameCount,
    ioData) -> OSStatus in 
    
    
    let object = unsafeBitCast(inRefCon, to: MyClass.self )
    if let abl = UnsafeMutableAudioBufferListPointer(ioData) {
        for buffer in abl {
        
        }
    }
    .
    .
    .
    
}

 

 

 

 

렌더 콜백

렌더 콜백은 요청에 의해 데이터를 전달하기 위한 콜백

// objC
// 구조체설정
AURenderCallbackStruct outputCallback;
outputCallback.inputProc = onOutputCallback
outputCallback.inputProcRefCon = this;

// 콜백함수
OSStatus onOutputCallback( 
    void* in_ref_con,
    AudioUnitRenderActionFlags* ioActionFlags,
    const AudioTimeStamp* inTimeStamp,
    UInt32 inBusNumber,
    UInt32 inNumFrames,
    AudioBufferList* ioData) { }

// 설정
AudioUnitSetProperty( unit,
    kAudioUnitProperty_SetRenderCallback,
    kAudioUnitScope_Input,
    kOutputBus,
    &callback, sizeof( outputCallback));




// swift
var outputCallback = 
AURenderCallbackStruct( 
    inputProc: onOutputCallback, 
    inputProcRefCon: UnsafeMutableRawPointer( Unmanaged.passUnretained(self).toOpaque()))

AudioUnitSetProperty( audioUnit,
    kAudioUnitProperty_SetRenderCallback,
    kAudioUnitScope_Global,
    kOutputBus,
    &outputCallback,
    UInt32( MemoryLayout<AURenderCallbackStruct>.size ))
    
// 콜백
let onOutputCallback : AURenderCallback = { (
    inRefCon,       // UnsafeMutableRawPointer
    ioActionFlags,  // UnsafeMutablePointer<AudioUnitRenderActionFlags>
    inTimeStamp,    // UnsafePointer<AudioTimeStamp>
    inBusNumber,    // UInt32
    frameCount,     // UInt32
    ioData          // Optional<UnsafeMutablePointer<AudioBufferList>>
    ) -> OSStatus in 
    
}

 

버퍼설정

AU 버퍼를 사용하지 않는 경우 제외
직접 버퍼를 할당하는 경우에는 제외 시켜 준다.

// objC
Uint32 flag = 0;
AudioUnitSetProperty( unit,
    kAudioUnitProperty_ShouldAllocateBuffer,
    kAudioUnitScope_Output,
    kInputBus,
    &flag, sizeof(flag));
    
    
// swift
AudioUnitSetProperty( unit,
    AudioUnitPropertyID( kAudioUnitProperty_ShouldAllocateBuffer),
    AudioUnitScope( kAudioUnitScope_Output),
    kInputBuf,
    &flag,
    UInt32(MemoryLayout<UInt32>.size))

 

 

콜백 함수 구현

AudioUnit 의 입력(입력 scope, element 1) 에서, AudioUnitRender 를 통해 데이터를 가져와서 tempBuffer에 저장.
출력( 출력 scope, element 0 ) 에서 해당 tempBuffer의 데이터를 가져오는 콜백이다.
아래 내용은 해당 구조를 이해하기 위해 쓰레드나 큐잉 등 데이터 버퍼에 대한 처리가 제외 되어 있다.
보통 버퍼를 관리하는 별도 클래스를 작성해 사용해야 한다.

// objC
AudioBuffer tempBuffer;


OSStatus OnInputCallback(
    void* in_ref_con,
    AudioUnitRenderActionFlags* flags,
    const AudioTimeStamp* time_stamp,
    UInt32 bus_number,
    UInt32 num_frames,
    AudioBufferList* io_data ) {
    
    
    MyRefefernceObject* object = static_cast<MyReferenceObject*>(in_ref_con);
    
    AudioBuffer buffer;
    buffer.mNumberChannels = 1;
    buffer.mDataByteSize = num_frames * 2;
    buffer.mData = malloc( num_frames * 2 );
    
    AudioBufferList bufferList;
    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0] = buffer;
    
    OSStatus status = AudioUnitRender( audioUnit, flags, time_stamp, bus_number, num_frames, &bufferList );
    
    AudioBuffer sourceBuffer = bufferList.mBuffers[0];
    if (tempBuffer.mDataByteSize != sourceBuffer.mDataByteSize ) {
        free( tempBuffer.mData );
        tempBuffer.mDataByteSize = sourceBuffer.mDataByteSize;
        tempBuffer.mData = malloc( sourceBuffer.mDataByteSize );
    }
    memcpy( tempBuffer.mData, bufferList.mBuffers[0].mData, bufferList.mBuffers[0].mDataByteSize);
    
    free( bufferList.mBuffers[0].mData );
}


OSStatus onOutputCallback( 
    AudioUnitRenderActionFlags* flags,
    const AudioTimeStamp* time_stamp,
    UInt32 bus_number,
    UInt32 num_frames,
    AudioBufferList* io_data ) {

    for( int i=0; i< io_data->mNumberBuffers; i++ ) {
        AudioBuffer buffer = ioData->mBuffers[i];
        UInt32 size = min( buffer.mDataByteSize, tempBuffer.mDataByteSize);
        memecpy( buffer.mData, tempBuffer.mData, size );
        buffer.mDataByteSize = size;
        
    }
    
    return noErr;
}





// swift
let inputCallback : AURenderCallback = { (
        inRefCon,
        ioActionFlags,
        inTimeStamp,
        inBusNumber,
        inNumberFrame,
        ioData ) -> OSStatus in
    
    let audioObject = unsafeBitCast( inRefCon, to: MyObject.self )
    var err: OSStatus = noErr
    
    var audioBuffer = AudioBuffer( 
                          mNumberChannels: UInt32(2),
                          mDataByteSize: inNumberFrame * 2,
                          mData: nil )
                          
    var bufferList = AudioBufferList(
                          mNumberBuffers: 1,
                          mBuffers: audioBuffer)
                          
    if let au = audioObject.audioUnit {
        err = AudioUnitRender( unit, 
                               ioActionFlags, 
                               inTimeStamp, 
                               inBusNumber, 
                               inNumberFrame, 
                               &bufferList)
    }
    
    
    let inputDataPtr = UnsafeMutableAudioBufferListPointer( &bufferList )
    let mBuffers : AudioBuffer = inputDataPtr[0]
    let bufferPointer = UnsafeMutableRawPointer( mBuffers.mData )
}

 

 

초기화

result = AudioUnitInitialize( audioUnit );

 

시작

AudioOutputUnitStart( audioUnit );

 

중지

AudioOutputUnitStop( audioUnit );

 

해제

AudioUnitUninitialize( audioUnit );
AudioComponentInstanceDispose( audioUnit );

 

AVAudioEngine

 

let engine:AVAudioEngine = AVAudioEngine()
let inputNode:AVAudioNode = engine.inputNode
let outputNode = engine.outputNode
let inputFormat:AVAudioFormat = inputNode.outputFormat( forBus: 0 )

inputNode.installTap( 
        onBus: 0, 
        bufferSize: AVAudioFrameCount( 1024 ), 
        format: inputFormat,
        tapBlock: { (buffer:AVAudioPCMBuffer!, time: AVAudioTime! ) -> Void in
            let length = Int( buffer.frameLength)
            
        }
    )

// connect to mixer
let mixerNode = engine.mainMixerNode
engine.connect( input, to: mixerNode, format: inputFormat )


// start
engine.start()

// stop
engine.stop()

 

 

'프로그래밍 > iOS,macOS' 카테고리의 다른 글

UITableView , UICollectionView  (1) 2020.04.14
[iOS] 키보드를 따라 올라오는 뷰  (0) 2020.04.08
[iOS] Google SignIn  (0) 2020.03.30
xcode 11. ios13 미만, storyboard 없이 시작  (0) 2020.03.29
XCFramework 만들기  (0) 2020.03.27
iOS 프레임워크 파이썬 스크립트  (0) 2019.07.23
[iOS] GPUImage  (0) 2019.07.06
xcodebuild  (0) 2019.06.06
CocoaPods 라이브러리 배포  (0) 2019.05.31
[Metal] Compute Function 샘플분석  (0) 2019.05.27