WWDC Quick Look 💓 By SwiftGGTeam
Enhance your spatial computing app with RealityKit audio

Enhance your spatial computing app with RealityKit audio

元の動画を見る

Highlight

このセッションは宇宙船ゲームを例に、RealityKit オーディオ API の全機能を段階的に示します。宇宙船のサウンドデザインは多層構造で、各エンジンに蒸気の尾、排気管、タービンの 3 層の音があり、ファイル再生とリアルタイム生成の 2 方式で実装されています。


主要内容

空間コンピューティングアプリで最もよくある問題は、3D オブジェクトは立体的に見えるのに音が平坦なことです。宇宙船が頭上を飛んでも音量が距離に応じて変化せず、方向感もない—体験はすぐに破綻します。RealityKit のオーディオ API は根本からこれを解決します。Entity.playAudio で再生するすべての音はデフォルトで空間化され、6 自由度レンダリング、物理ベースの距離減衰、実環境をリアルタイムでシミュレートしたリバーブが付きます。

このセッションは宇宙船ゲームでオーディオツールチェーン全体を示します。宇宙船には 2 つのエンジンがあり、各エンジンに 3 層の音があります。蒸気の尾はループファイル再生、排気管の音量はスロットルに追従、タービンは AU オーディオユニットでリアルタイム生成。衝突音は AudioFileGroupResource で材質ペアリングに応じて選択され、ゲインは衝突速度で決まります。没入環境に入ると ReverbComponent がリバーブプリセットを切り替えます。BGM は 4 チャンネルレイアウトと AmbientAudioComponent でレンダリング。最後に AudioMixGroupsComponent でユーザーが各カテゴリの音量比率を調整できます。

最もシンプルな 2 行の再生から、指向性制御、リアルタイムオーディオ生成、材質駆動の衝突音、リバーブプリセット、環境音楽、ミックスグループまで—空間コンピューティングオーディオのほぼすべてのシナリオをカバーします。


詳細

空間オーディオファイル再生

最も基本的な使い方は 2 行です(03:11):

func playVaporTrailAudio(from engine: Entity) async throws {
    let resource = try await AudioFileResource(named: "VaporTrail")
    engine.playAudio(resource)
}
  • AudioFileResource(named:) で Bundle からオーディオファイルを読み込む
  • engine.playAudio(resource) でエンジンエンティティ上で再生し、自動的に空間レンダリング

再生即空間化—追加設定不要。2 つの詳細に注意:オーディオファイルはモノラルにすべき(空間化前にモノラルにダウンミックスされるため、事前にモノラルファイルにするとダウンミックスのアーティファクトを避けられる)。短いファイルにループとランダム開始位置を組み合わせると、同じ音源でも異なって聞こえる(04:02):

let resource = try await AudioFileResource(
    named: "VaporTrail",
    configuration: AudioFileResource.Configuration(
       shouldLoop: true,
       shouldRandomizeStartTime: true
    )
)
let controller: AudioPlaybackController = engine.playAudio(resource)
controller.gain = -.infinity
controller.fade(to: .zero, duration: 1)
  • shouldLoop: true で短いファイルをシームレスにループ
  • shouldRandomizeStartTime: true で 2 つのエンジンがファイルの異なる位置から開始
  • controller.gain = -.infinity で無音から開始し、ループ時のポップを回避
  • controller.fade(to: .zero, duration: 1) で 1 秒かけて標称音量にフェードイン

指向性(Directivity)と独立オーディオソース

エンジン音に方向感を持たせる—正面では明るく、背面ではこもる—には SpatialAudioComponentdirectivity プロパティを設定します(04:02):

let audioSource = Entity()
audioSource.orientation = .init(angle: .pi, axis: [0, 1, 0])
audioSource.components.set(
    SpatialAudioComponent(directivity: .beam(focus: 0.25))
)
engine.addChild(audioSource)
let controller = audioSource.playAudio(resource)
  • 独立した Entity をオーディオソースとして作成し、エンジンの子ノードに—位置はエンジンに追従、方向は独立制御
  • .beam(focus: 0.25) でビーム指向性を設定。focus は 0(全方向)から 1(狭いビーム)。0.25 は軽い方向感
  • audioSource を回転させ、エンジン噴口方向から音が出るようにする

スロットル駆動ゲイン

排気管の音量はスロットルに線形追従(07:10):

func updateAudio(for exhaust: Entity, throttle: Float) {
    let gain = decibels(amplitude: throttle)
    exhaust.components[SpatialAudioComponent.self]?.gain = Audio.Decibel(gain)
}

func decibels(amplitude: Float) -> Float { 20 * log10(amplitude) }
  • 線形スロットル値(0~1)を対数デシベルに変換し、人間の聴覚に合わせる
  • SpatialAudioComponentgain を直接変更—Component プロトコルに準拠するため、カスタム System で毎フレーム更新可能

リアルタイムオーディオ生成(AudioGeneratorController)

タービン音はカスタム AU オーディオユニットでリアルタイム生成(08:17):

var turbineController: AudioGeneratorController?

func playTurbineAudio(from engine: Entity) {
    let audioUnit = try await AudioUnitTurbine.instantiate()
    let configuration = AudioGeneratorConfiguration(layoutTag: kAudioChannelLayoutTag_Mono)
    let format = AVAudioFormat(
        standardFormatWithSampleRate: Double(AudioGeneratorConfiguration.sampleRate),
        channelLayout: .init(layoutTag: configuration.layoutTag)!
    )
    try audioUnit.outputBusses[0].setFormat(format)
    try audioUnit.allocateRenderResources()
    let renderBlock = audioUnit.internalRenderBlock
    turbineController = try engine.playAudio(configuration: configuration) {
        isSilence, timestamp, frameCount, outputData in
        var renderFlags = AudioUnitRenderActionFlags()
        return renderBlock(&renderFlags, timestamp, frameCount, 0, outputData, nil, nil)
    }
}
  • AudioUnitTurbine はカスタム AU。内部で複数オシレーターがスロットル値に応じて音を生成
  • AudioGeneratorConfiguration でモノラル出力を設定
  • engine.playAudio(configuration:callback:) のコールバックでオーディオバッファに直接書き込み
  • 返された AudioGeneratorController は保持必須。さもないとオーディオが停止

距離減衰のカスタマイズ

宇宙船が近づいたときだけ客室音楽が聞こえるようにするには、距離減衰を強める(11:28):

func configureDistanceAttenuation(for spaceshipHifi: Entity) {
    spaceshipHifi.components.set(
        SpatialAudioComponent(
            gain: -18,
            distanceAttenuation: .rolloff(factor: 4)
        )
    )
}
  • .rolloff(factor: 1) は自然減衰のデフォルト。factor 4 は自然の 4 倍速で減衰
  • gain: -18 でさらに音量を下げ、極近距離でのみ聞こえるようにする

材質駆動の衝突音

衝突音は 2 物体の材質ペアリングで選択(15:04):

func handleCollisionBegan(_ collision: CollisionEvents.Began) {
    guard
        let audioMaterials = audioMaterials(for: collision),
        let resource: AudioFileGroupResource = collisionAudio[audioMaterials]
    else {
        return
    }
    let controller = collision.entityA.playAudio(resource)
    controller.gain = relativeLoudness(for: collision)
}
  • AudioFileGroupResource は複数バリアントを含み、再生ごとにランダム選択で反復感を回避
  • relativeLoudness(for:) で衝突速度からゲインを計算
  • 材質はカスタム AudioMaterialComponent でエンティティに付与(rock、plastic、metal など)
  • 実世界の衝突は ARKit シーン再構築メッシュの面分類を材質列挙にマッピング

リバーブプリセット

仮想環境に入るときにリバーブを設定(17:18):

func prepareStudioEnvironment() async throws {
    let studio = try await Entity(named: "Studio", in: studioBundle)
    studio.components.set(
       ReverbComponent(reverb: .preset(.veryLargeRoom))
    )
    rootEntity.addChild(studio)
}
  • エンティティ階層に ReverbComponent を置けば有効。同時に 1 つのリバーブのみアクティブ
  • プログレッシブ没入では、リアル音響とプリセットリバーブが没入度に応じて混合
  • フル没入では空間音源が完全にプリセットリバーブを使用

環境音楽とミックスグループ

4 チャンネルレイアウトの音楽は AmbientAudioComponent でレンダリング(20:05):

let resource = try await AudioFileResource(
    named: "JoyRideMusic",
    configuration: .init(
        loadingStrategy: .stream,
        shouldLoop: true
    )
)
entity.components.set(AmbientAudioComponent())
entity.playAudio(resource)
  • loadingStrategy: .stream で大ファイルをストリーミングし、メモリ使用量を削減
  • AmbientAudioComponent は 3 自由度レンダリング:頭部と音源の回転に応答、移動には応答しない
  • 4 チャンネルファイルはチャンネルレイアウトをファイルに書き込む必要があり、RealityKit が各チャンネルを正しい角度からレンダリングできる

ミックスグループでユーザーが各カテゴリを独立制御(21:57):

let resource = try await AudioFileResource(
    named: "JoyRideMusic",
    configuration: .init(
        loadingStrategy: .stream,
        shouldLoop: true,
        mixGroupName: "Music"
    )
)

var audioMixerEntity = Entity()

func updateMixGroup(named mixGroupName: String, to level: Audio.Decibel) {
    var mixGroup = AudioMixGroup(name: mixGroupName)
    mixGroup.gain = level
    let component = AudioMixGroupsComponent(mixGroups: [mixGroup])
    audioMixerEntity.components.set(component)
}
  • mixGroupName: "Music" でリソースを Music グループに分類
  • AudioMixGroupsComponent は Component プロトコルに準拠。UI 操作やカスタム System から更新可能

重要ポイント

  1. 各音源に独立した Entity 子ノードを作成:視覚エンティティの位置と向きは音の伝播に適さない場合がある。オーディオソースを子ノードとしてマウントすれば、音の方向と位置を独立制御できる。例:エンジン中心ではなく噴口からエンジン音を出す。始め方:方向制御が必要な音源用に Entity() を新規作成し、orientationSpatialAudioComponent を設定してから視覚エンティティに addChild

  2. AudioFileGroupResource で衝突音の機械感を排除:同じ衝突音セットに複数の微妙なバリアントを用意し、再生ごとにランダム選択。ゲインは衝突速度の対数デシベルで駆動—遅い衝突は静か、速い衝突は力強い。始め方:各材質ペアリング用に 3~5 バリアントファイルを作成し、AudioFileGroupResource を構築。CollisionEvents.Began コールバックで速度からゲインを計算。

  3. 大ファイルは .stream、短ファイルは shouldLoop + shouldRandomizeStartTime:10 秒のループファイルは 5 分の完全オーディオより大幅にメモリ節約。異なる開始位置で同相叠加を回避。始め方:継続的背景音は短いループファイルにし、shouldLoop: true + shouldRandomizeStartTime: true。大きな BGM は loadingStrategy: .stream

  4. AudioMixGroupsComponent で音量制御をユーザーに委ねる:効果音、音楽、音声を異なる Mix Group に分け、ユーザーが好みに応じて独立調整。始め方AudioFileResource.ConfigurationmixGroupName を設定し、専用の audioMixerEntityAudioMixGroupsComponent を保持。UI スライダーで各グループの gain を更新。


関連セッション

コメント

GitHub Issues · utterances