UnityからiOS(Swift)のAudio機能を使う

UnityからiOS(Swift)のAudio機能を使う

UnityからiOS(Swift)のAudio機能を使う方法を試してみた。ちなみにUnityはまったく使ったことがない素人である…。

今回はシンプルにマイクから入力したものをスピーカー(イヤホンなど)にそのまま出力するというもの。

UnityからSwiftのコードを使う方法

ネイティブコードプラグインという方法でSwiftのコードを使えるようだ。

  • SwiftでFramework化して利用する
  • Assets/Plugins/iOS以下にSwiftファイルを配置する

https://docs.unity3d.com/ja/2023.2/Manual/PluginsForIOS.html

https://qiita.com/fuziki/items/955c2b35514bcfc37969

一番簡単そうなSwiftファイルを直接配置して利用する方法を試してみる。Assets/Plugins/iOS以下に置くとiOSプラットフォームのみ有効となるらしい。

SwiftでAudioを操作する処理を書く

簡単にAudio入力と出力させる処理をAudioManagerというクラスにまとめて書くことにした。ついでに音量としてデシベル(dB)を取得する処理も合わせて書く。

流れは以下のような形になる。

  • AudioUnitを初期化
  • マイク入力と出力を有効化
  • Audioフォーマットの設定
  • 入力・出力のコールバックを設定
  • コールバックから入力・出力・音量を取得する処理

AudioUnitを初期化

まずは、AudioComponentDescriptionを設定する。

Audioをスタート・ストップのような操作する場合、汎用出力ユニットとして、kAudioUnitType_Outputを設定する。SubTypeには、kAudioUnitSubType_RemoteIOを設定する。これは入力と出力を使用する場合に、バス0と1でI/O処理できるようになるとのこと。あとは、ベンダー識別子として、kAudioUnitManufacturer_Appleを指定。ここらへんはお決まりの設定になるようだ。

続いて、AudioComponentFindNextを使用して、定義したオーディオコンポーネントを取得し、AudioUnitのインスタンスを作成する。

// オーディオコンポーネントの設定
var desc = AudioComponentDescription(
  componentType: kAudioUnitType_Output,
  componentSubType: kAudioUnitSubType_RemoteIO,
  componentManufacturer: kAudioUnitManufacturer_Apple,
  componentFlags: 0,
  componentFlagsMask: 0
)

guard let component = AudioComponentFindNext(nil, &desc) else {
  NSLog("AudioComponent not found")
  return
}

var tempAudioUnit: AudioUnit?
if AudioComponentInstanceNew(component, &tempAudioUnit) != noErr {
  NSLog("AudioUnit instance creation failed")
  return
}
audioUnit = tempAudioUnit

マイク入力と出力を有効化

AudioUnitSetPropertyで入力(マイク)と出力(スピーカー)有効化する。

バス0が出力、バス1が入力となる。1を渡すことで、有効化されるとのこと。

var enable: UInt32 = 1

// 入力 (マイク) を有効化
AudioUnitSetProperty(
  audioUnit,
  kAudioOutputUnitProperty_EnableIO,
  kAudioUnitScope_Input,
  1,  // バス1 = マイク入力
  &enable,
  UInt32(MemoryLayout.size(ofValue: enable)))

// 出力 (スピーカー) を有効化
AudioUnitSetProperty(
  audioUnit,
  kAudioOutputUnitProperty_EnableIO,
  kAudioUnitScope_Output,
  0,  // バス0 = スピーカー出力
  &enable,
  UInt32(MemoryLayout.size(ofValue: enable)))

Audioフォーマットの設定

次は、入力と出力で使用するAudioフォーマットを設定する必要がある。

今回は、後々デシベルを取得する際にFloatの方が扱いやすかったので、PCM・44.1kHz・32bit・2ch (ステレオ)・Floatで設定してみた。ちなみにiPhoneのマイク入力はモノラルだと思うので、あまり意味ないが、そのまま入力を出力させる場合に楽なのでフォーマットを合わせた。

ステレオの場合、mBytesPerPacket、mBytesPerFrameを2倍にし、mChannelsPerFrameを2にする。

var streamFormat = AudioStreamBasicDescription(
  mSampleRate: 44100.0,
  mFormatID: kAudioFormatLinearPCM,
  mFormatFlags: kAudioFormatFlagIsFloat,
  mBytesPerPacket: UInt32(MemoryLayout<Float32>.size * 2),
  mFramesPerPacket: 1,
  mBytesPerFrame: UInt32(MemoryLayout<Float32>.size * 2),
  mChannelsPerFrame: 2,
  mBitsPerChannel: UInt32(MemoryLayout<Float32>.size * 8),
  mReserved: 0
)

// 出力フォーマット設定
AudioUnitSetProperty(
  audioUnit,
  kAudioUnitProperty_StreamFormat,
  kAudioUnitScope_Input,
  0,
  &streamFormat,
  UInt32(MemoryLayout.size(ofValue: streamFormat)))

// 入力フォーマット設定
AudioUnitSetProperty(
  audioUnit,
  kAudioUnitProperty_StreamFormat,
  kAudioUnitScope_Output,
  1,
  &streamFormat,
  UInt32(MemoryLayout.size(ofValue: streamFormat)))

入力・出力のコールバックを設定

本来は入力コールバックを設定し、入力コールバックで入力データをバッファに貯めてから、出力コールバックで出力バッファに書き込むという流れになると思う。

今回は出力コールバックから入力データを取得して、そのまま出力バッファに書き込む方法で楽をする。なので、出力コールバックのみ設定する。inputProcにコールバックで使用するメソッドを指定する。

var outCallback = AURenderCallbackStruct(
  inputProc: renderCallback,
  inputProcRefCon: UnsafeMutableRawPointer(
    Unmanaged.passUnretained(self).toOpaque())
)

AudioUnitSetProperty(
  audioUnit,
  kAudioUnitProperty_SetRenderCallback,
  kAudioUnitScope_Input,
  0,  // 出力のバス0 (スピーカー)
  &outCallback,
  UInt32(MemoryLayout.size(ofValue: outCallback)))

コールバックから入力・出力する処理

まずは、マイク入力のデータを取得するためのAudioバッファを用意する。

  • Sample チャンネル毎の音データ(44.1kHzなら44.1kサンプル)
  • Frame 各チャンネル1サンプルをまとめたもの
  • Packet フレームをまとめたもの

inNumberFramesにフレーム数が渡ってくるので、これにFloat(4バイト)xステレオなので2、8バイトを掛けたものがバッファサイズとなる。

ちなみにこれは出力用のコールバックなので、出力する際のフレーム数などが渡ってくるが、入力と出力のAudioフォーマットを合わせているので、一応これでも大丈夫っぽい。

本当は入力用のコールバックを別途用意するなりして、実際入力されたフレーム数からバッファを確保するのが良さそう。

実際の入力データは、AudioUnitRenderに確保したAudioバッファとバス1を指定して取得する。

あとは取得できた入力データを、ioData(出力用のバッファ)にコピーすることで出力完了。

private let renderCallback: AURenderCallback = {
(
  inRefCon: UnsafeMutableRawPointer,
  ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
  inTimeStamp: UnsafePointer<AudioTimeStamp>,
  inBusNumber: UInt32,
  inNumberFrames: UInt32,
  ioData: UnsafeMutablePointer<AudioBufferList>?
) -> OSStatus in


let audioUnitManager = Unmanaged<AudioManager>.fromOpaque(inRefCon)
  .takeUnretainedValue()
guard let audioUnit = audioUnitManager.audioUnit else { return noErr }

// AudioBuffer
let bufferSize = Int(inNumberFrames) * MemoryLayout<Float32>.size * 2
let audioBuffer = UnsafeMutabtlePointer<Float32>.allocate(capacity: bufferSize / MemoryLayout<Float32>.size)
var bufferList = AudioBufferList(
  mNumberBuffers: 1,
  mBuffers: AudioBuffer(
    mNumberChannels: 2,
    mDataByteSize: UInt32(bufferSize),
    mData: UnsafeMutableRawPointer(audioBuffer)
  )
)

// マイク入力からデータを取得
let status = AudioUnitRender(
  audioUnit,
  ioActionFlags,
  inTimeStamp,
  1,  // 入力のバス1 (マイク)
  inNumberFrames,
  &bufferList)
//NSLog("size: %d", Int(bufferList.mBuffers.mDataByteSize))

if status != noErr {
  NSLog("AudioUnitRender failed:", status)
  return status
}

// 出力バッファにコピー
memcpy(
  ioData.pointee.mBuffers.mData, bufferList.mBuffers.mData,
  Int(bufferList.mBuffers.mDataByteSize))

audioBuffer.deallocate()

}

コールバックから音量を取得する処理

ついでに音量を取得する処理も書く。音量はデシベルという単位で取得する。よく騒音として工事現場などでも何dBとかで表示されていると思う。

dBは基準値からの相対的な数値なので、0dBを基準として大きい、小さいを表したものらしい。要するに0dBが一番うるさい音であり、それからマイナス方向に-10、-20と小さい音として表現される。

しかし、工事現場などでは80dBなど0より大きい数値が使われていると思うが、これは0dBを人間が聞こえる最小値として基準にしているからだ。

最初にこの0を最大音量とした数値(マイナスにいくほど小さくなる)を取得してみる。

マイク入力から取得したバッファからチャンネル毎のRMSを取得する。RMSとは、root mean square(二乗平均平方根)の略で、ようは音の平均値を取得する。この平均値が高ければ音量が大きいということになる。

今回は入力のAudioフォーマットもステレオになっているため、左右の大きさを計算しているが、実際はモノラル入力なので左右とも同じ値となるはず。

デシベルへの変換は、20 * log10(rms)となるが、かなり数学的な話になるので私も完全には理解ができない。音の強さなど範囲が広いものを、ある基準との比を対数(log)で表した単位とのこと。RMSや音圧の比を表現する際は20を使い、スピーカーなど出力する際の電力費の場合は、10として計算するとのこと。

var leftRMS: Float32 = 0.0
var rightRMS: Float32 = 0.0
let data = bufferList.mBuffers.mData!.assumingMemoryBound(to: Float32.self)
for frame in 0..<Int(inNumberFrames) {
  // 左右のチャンネルを個別に処理
  let leftValue = data[frame * 2]
  let rightValue = data[frame * 2 + 1]
  leftRMS += leftValue * leftValue
  rightRMS += rightValue * rightValue
}
leftRMS = sqrt(leftRMS / Float32(inNumberFrames))
rightRMS = sqrt(rightRMS / Float32(inNumberFrames))

// RMSをデシベルに変換
let leftdB = 20 * log10(leftRMS)
let rightdB = 20 * log10(rightRMS)

これでとりあえず、デシベルの取得はできた。ただ、0が最大音量なのは分かるが、最小音量が不明なため、音の大きさを表示するには、少し使いづらい。

色々iPhoneのマイク入力を試して見ると、-80〜-90dBくらいが最小音量になりそうだと分かった。なのでこの最小値を基準として、0〜140dBとなるように変換するメソッドを作成する。

static func dbfsToSPL(_ dbfs: Float32) -> Float32 {
  let silentDbfs: Float32 = -80.0 // 無音判定しきい値(dBFS)
  let loudestSPL: Float32 = 140.0 // 最大音
  let silentSPL: Float32 = 0.0 // 無音判定しきい値=0dB
  let clamped = max(min(dbfs, 0), silentDbfs) // dBFS範囲を制限
  return ((clamped + abs(silentDbfs)) / abs(silentDbfs)) * (loudestSPL - silentSPL) + silentSPL
}

UnityからSwiftのメソッドを呼び出す

ようやくSwift側の実装が終わったので、Unityから呼び出すための実装を追加する。

@_cdecl属性を使用することで、C言語から呼び出せる関数として定義する。こうすることで、Unity(C#)からも呼び出せるようになる。

@_cdecl("audioStart")
public func audioStart() {
  AudioManager.shared.startAudioUnit()
}

@_cdecl("audioStop")
public func audioStop() {
  AudioManager.shared.stopAudioUnit()
}

あとはUnityから下記のような形でこの関数を呼び出すだけ。

public class ButtonScript : MonoBehaviour
{
    [DllImport("__Internal", EntryPoint = "audioStart")]
    static extern void iOSAudioStart();
    [DllImport("__Internal", EntryPoint = "audioStop")]
    static extern void iOSAudioStop();

    void Update()
    {

    }

    public void OnStart()
    {
        Debug.Log("Button START Clicked");

#if !UNITY_EDITOR && UNITY_IOS
        iOSAudioStart();
#endif
    }

    public void OnStop()
    {
        Debug.Log("Button STOP Clicked");

#if !UNITY_EDITOR && UNITY_IOS
        iOSAudioStop();
#endif
    }
}

前にFFmpegを使っていたときにオーディオやビデオのエンコード・デコード周りを触っていたが、だいぶ忘れていたので、いい復習になった気がする。

ちなみにiPhoneのシミューレーターでマイク入力が出来たり、違う環境だと出来なかったり謎の現象が起きてだいぶハマった。色々試したが解決策は分からず、実機で動かすのが一番確実かな…。

https://github.com/pontago/unity-audio-example

参考にさせていただいたサイト

https://qiita.com/kojiro_ueda/items/85160e1b354f1281309c

https://qiita.com/mao_/items/07466e169d08cbeff221

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です