






# 音声の操作

## 用語

### RTCAudioTrack (音声トラック)

AudioTrack は 1 接続に存在する音声のストリーミングデータを表現するオブジェクトです。
配信する音声トラックは Sora のルールにより 1 本です。受信する音声トラックは接続数分あります。

例えば 3 人で通話している場合、配信する音声トラックは 1 本、受信する音声トラックは 2 本存在します。

### RTCAudioTrackSink

RTCAudioTrackSink は送信または受信した音声トラックごとに、音声データのコールバックを受け取るプロトコルです。

音声トラックから音声データを取得するには、この RTCAudioTrackSink を利用します。

## 音声をミュートする

音声をミュートするには [メディアストリーム](devguide.html#4d7dac) ([MediaStream](_static/api/docs/Protocols/MediaStream.html)) の `audioEnabled` プロパティに `false` をセットします。 
ただし送信音声についてミュートした場合、マイクは停止しませんので注意してください。

```swift
// 指定の ConnectionID を持つ受信 Stream の音声のミュート状態を切り替える
private func handleMuteReceiveStream(connectionId: String) {
    // 受信したストリームのリストを取得する
    // Sora 接続成功時に取得する MediaChannel を mediaChannel に保持しているものとする
    let downstreams = mediaChannel?.receiverStreams ?? []
    // downstreams が空の場合は何もしない
    guard downstreams.count > 0 else {
        return
    }
    for stream in downstreams {
        // 指定のコネクションIDを持つ Stream の audioEnabled を切り替える
        if stream.streamId == connectionId {
            stream.audioEnabled = !stream.audioEnabled
        }
    }
}
```

## 受信した音声の音量を変更する

[ロール](devguide.html#750f41) が `Role.recvonly` または `Role.sendrecv` のとき、 [メディアストリーム](devguide.html#4d7dac) ([MediaStream](_static/api/docs/Protocols/MediaStream.html)) の `remoteAudioVolume` プロパティで受信ストリームの音量が変更できます。
このプロパティは 0 から 10 の値をとります。
0 をセットすると音声は出力されません。

送信ストリームで送信する音声の音量は変更できません。

```swift
let config = Configuration(url: url,
                            channelId: channelId,
                            role: .sendrecv)

// 送信または受信のストリームが追加されたときのイベントハンドラ
config.mediaChannelHandlers.onAddStream = { [weak self] stream in
    // クロージャーの実行時、 self が存在する場合のみ処理を続けます。
    guard let self = self else {
        return
    }
    // 受信ストリームのときのみ音量変更を行う
    if stream.streamId != config.publisherStreamId {
        stream.remoteAudioVolume = 3.0
    }
}
```


## 受信した音声データを取得する

音声トラックごとに音声データを取得するには `RTCAudioTrackSink` プロトコルを利用します。
このプロトコルを実装したクラスを [MediaStream](_static/api/docs/Protocols/MediaStream.html) の `addAudioTrackSink(_:)` で音声トラックに関連付けることで音声データを受信できるようになり、
`removeAudioTrackSink(_:)` を呼ぶか、自分もしくはリモートの配信クライアントがチャネルを離脱することで音声トラックとの関連付けが解除されます。

以下に音声データ受信の実装例を示します。

```swift
// このサンプルコードは RTCAudioTrackSink を用いて
// 音声データを WAV ファイルに保存するというシナリオ
// になっています。
//
// WAV 保存処理など RTCAudioTrackSink の使い方に
// 直接影響しない処理は実装を省略してあります。

import Sora
import WebRTC

/// RTCAudioTrackSink の実装例です。
final class RTCAudioTrackSinkExample: NSObject, RTCAudioTrackSink {
  private let fileHandle: FileHandle
  /// MARK: RTCAudioTrackSink protocol の実装

  func onData(
    _ audioData: Data,
    bitsPerSample: Int,
    sampleRate: Int,
    numberOfChannels: Int,
    numberOfFrames: Int
  ) {
    // ここには onData コールバックで取得した音声データ処理を実装してください。
    // 重い処理を行う場合は、別スレッドに処理を移譲してください。
    writeWAV(audioData)
  }

  func preferredNumberOfChannels() -> Int { -1 }

  /// MARK: この class 特有の処理

  init(fileURL: URL) throws {
    FileManager.default.createFile(atPath: fileURL.path, contents: nil)
    self.fileHandle = try FileHandle(forWritingTo: fileURL)
    super.init()
  }

  /// WAV ファイルへの書き込み処理
  func writeWAV(audioData: Data) {}

  /// リソースを解放する処理
  func close() {
    try? fileHandle.close()
  }

}

final class Receiver {
  private var connectionTask: ConnectionTask?
  private var mediaChannel: MediaChannel?

  // RTCAudioTrackSink は SDK 側で保持しないため、アプリ側で保持してください。
  // MediaStream は 1 つの RTCAudioTrack を持ち、RTCAudioTrack には複数の RTCAudioTrackSink を
  // 関連付けることができるため、RTCAudioTrackSink はリストで保持します。
  private var sinksByStreamId: [String: (stream: MediaStream, sinks: [RTCAudioTrackSink])] = [:]

  func start() {
    var config = Configuration(
      url: URL(string: "wss://example.com/signaling")!,
      channelId: "my-channel",
      role: .recvonly
    )

    // 自分もしくはリモートの配信クライアントがチャネルに参加した際の処理
    config.mediaChannelHandlers.onAddStream = { [weak self] stream in
      guard let self else { return }
      let url = FileManager.default.temporaryDirectory
        .appendingPathComponent("\(stream.streamId).wav")
      guard let sink = try? RTCAudioTrackSinkExample(fileURL: url) else { return }

      // MediaStream の保持する RTCAudioTrack に RTCAudioTrackSink を関連付けます
      stream.addAudioTrackSink(sink)

      // アプリ側で RTCAudioTrackSink を保持します。
      // ここで保持した RTCAudioTrackSink はリソース解放処理を行う際に利用されます。
      let entry = sinksByStreamId[stream.streamId]
      let sinks = (entry?.sinks ?? []) + [sink]
      sinksByStreamId[stream.streamId] = (stream: stream, sinks: sinks)
    }

    // リモートの配信クライアントがチャネルから切断した際の処理
    config.mediaChannelHandlers.onRemoveStream = { [weak self] stream in
      guard let self else { return }
      // onRemoveStream で RTCAudioTrack が解放されると RTCAudioTrackSink も自動で解除されるため、
      // removeAudioTrackSink を呼ぶ必要はありません。
      if let entry = sinksByStreamId[stream.streamId] {
        for sink in entry.sinks {
          // RTCAudioTrackSink のリソース解放はアプリ側の責務です。
          (sink as? RTCAudioTrackSinkExample)?.close()
        }
        sinksByStreamId[stream.streamId] = nil
      }
    }

    // 切断されたときの処理
    config.mediaChannelHandlers.onDisconnect = { [weak self] _ in
      guard let self else { return }
      for (_, entry) in sinksByStreamId {
        for sink in entry.sinks {
          // RTCAudioTrackSink のリソース解放はアプリ側の責務です。
          (sink as? RTCAudioTrackSinkExample)?.close()
        }
      }
      sinksByStreamId.removeAll()
    }

    connectionTask = Sora.shared.connect(configuration: config) { [weak self] mediaChannel, error in
      if let error { print("connect failed: \(error)") }
      self?.mediaChannel = mediaChannel
    }
  }

  // MediaStream は MediaChannel.streams などから取得できます。
  func stopReceivingAudio(stream: MediaStream) {
    if let entry = sinksByStreamId[stream.streamId] {
      for sink in entry.sinks {
        // 明示的に RTCAudioTrackSink との関連付けを解除したい
        // 場合は removeAudioTrackSink を利用します。
        stream.removeAudioTrackSink(sink)
        // RTCAudioTrackSink のリソース解放
        (sink as? RTCAudioTrackSinkExample)?.close()
      }
      sinksByStreamId[stream.streamId] = nil
    }
  }

  func stop() {
    // 解放処理は onDisconnect に寄せる
    connectionTask?.cancel()
    mediaChannel?.disconnect(error: nil)
    mediaChannel = nil
  }
}
```

### 音声データについて

コールバックに入ってくる音声データは非圧縮の 16 bit PCM (Pulse Code Modulation) 形式です。

### onData コールバックについて

RTCAudioTrackSink の onData コールバックには音声データ以外の情報も含まれています。

```swift
/// 音声データ受信コールバック。
///
/// - Parameters:
///   - audioData: PCM 形式音声データ。
///   - bitsPerSample: 1 サンプルあたりのビット数。
///                    libwebrtc では PCM 形式の音声データは 16 bit 固定のため、常に 16 が渡されます。
///   - sampleRate: サンプルレート (単位: Hz)。
///   - numberOfChannels: 音声データのチャンネル数。
///                       モノラルなら 1、ステレオなら 2 が渡されます。
///   - numberOfFrames: audioData に含まれるフレーム数。
func onData(_ audioData: Data,
               bitsPerSample: Int,
               sampleRate: Int,
               numberOfChannels: Int,
               numberOfFrames: Int)
```

> **注意**
>
> onData コールバック実装の注意点
> onData は 10 ms ごとに libwebrtc の音声処理スレッド上で呼び出されるコールバックであるため、 onData 内部で時間のかかる処理を行う場合、libwebrtc の音声処理がブロックされてしまいます。そのため onData 内部で時間のかかる処理を行わず、別スレッドで処理を行うようにしてください。

### RTCAudioTrackSink と RTCAudioTrack の関連付けについて

RTCAudioTrackSink と RTCAudioTrack の関連付けは 1:1 を前提に実装されています。1 つの RTCAudioTrackSink を複数の RTCAudioTrack に同時に関連付ける 1:N の利用は想定していません。
一方で、1 本の RTCAudioTrack に対して複数の RTCAudioTrackSink を関連付けることは可能であり、同一の RTCAudioTrack から各 RTCAudioTrackSink はそれぞれ独立して音声データを受け取ります。

### RTCAudioTrackSink と RTCAudioTrack の関連付け解除について

通常、自分もしくはリモートの配信クライアントがチャネルを離脱した際に、RTCAudioTrack と RTCAudioTrackSink の関連付けは自動で解除されます。

明示的に関連付けを解除したい場合は、 [MediaStream](_static/api/docs/Protocols/MediaStream.html) の `removeAudioTrackSink(_:)` を呼び出してください。

## 任意の音声を送信する

WebRTC ライブラリの制約により、 Sora iOS SDK ではマイク入力以外の音声を送信できません。ご了承ください。


## AVAudioSession のプロパティを変更する

> **重要**
>
> iOS 14.0 で、音声モードをデフォルト以外からデフォルトに戻したときに音声が出力されない事象を確認しています。その場合は端末を iOS 14.1 以降にアップデートしてください。

Sora iOS SDK では、AVAudioSession のプロパティを変更する簡易的な API として、 [Sora.setAudioMode メソッド](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/Sora#/s:4SoraAAC12setAudioMode_7optionss6ResultOyyts5Error_pGAA0cD0O_So29AVAudioSessionCategoryOptionsVtF) を用意しています。

この API は `AVAudioSession` の次のプロパティを変更します

- 音声モード (`AVAudioSession.Mode`)
- 音声カテゴリ (`AVAudioSession.Category`)
- 音声カテゴリオプション (`AVAudioSession.CategoryOptions`)
- 音声経路 (`AVAudioSession.PortOverride`)

ピア接続の設定や端末の状況によっては設定が反映されない場合があるので注意してください。

[Sora.setAudioMode メソッド](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/Sora#/s:4SoraAAC12setAudioMode_7optionss6ResultOyyts5Error_pGAA0cD0O_So29AVAudioSessionCategoryOptionsVtF) の型と引数は以下の通りです。

- mode: [AudioMode](_static/api/docs/Enums/AudioMode.html)- [AudioMode](_static/api/docs/Enums/AudioMode.html) は Sora iOS SDK で利用可能な 3 種類の音声モードです。- default- default の音声モードを利用します。音声カテゴリ、音声出力先の指定ができます。
    - videoChat- videoChat の音声モードを利用します。音声カテゴリは `playAndRecord`、音声出力先はスピーカーを使用します
    - voiceChat- voiceChat の音声モードを利用します。音声カテゴリは `playAndRecord`、音声出力先の指定ができます。
- options: `AVAudioSession.CategoryOptions`- 指定は任意です。デフォルトで `allowBluetooth` 、 `allowBluetoothA2DP` 、 `allowAirPlay` が設定されています。
  - 指定した場合はデフォルトの音声カテゴリオプションは設定されず、指定した音声カテゴリオプションのみが設定されます。

### AudioMode の指定方法および AVAudioSession のプロパティ設定内容

[AudioMode](_static/api/docs/Enums/AudioMode.html) はモード毎にそれぞれ異なる引数を取ります。
指定する引数の内容と、指定された引数によって設定される AVAudioSession のプロパティの値は以下のとおりです。

- `default(category: AVAudioSession.Category, output: AudioOutput)`- 音声モードに `default` を設定します
  - 音声カテゴリに引数で指定された category を設定します
  - output には [AudioOutput](_static/api/docs/Enums/AudioOutput.html) の `default` または `speaker` を指定します - output に `speaker` が指定されていた時、音声カテゴリオプションに `defaultToSpeaker` が追加で設定されます
- `videoChat`- 音声モードに `videoChat` が設定されます
  - 音声カテゴリに `playAndRecord` が設定されます
- `voiceChat(output: AudioOutput)`- 音声モードに `voiceChat` を設定します
  - 音声カテゴリに `playAndRecord` を設定します
  - output には [AudioOutput](_static/api/docs/Enums/AudioOutput.html) の `default` または `speaker` を指定します - output に `speaker` が指定されていた時、音声カテゴリオプションに `defaultToSpeaker` が追加で設定されます
    - output に `speaker` が指定されていた時、音声経路に `speaker` が設定されます

### 設定例

以下に [Sora.setAudioMode メソッド](https://sora-ios-sdk.shiguredo.jp/_static/api/docs/Classes/Sora#/s:4SoraAAC12setAudioMode_7optionss6ResultOyyts5Error_pGAA0cD0O_So29AVAudioSessionCategoryOptionsVtF) を利用した AVAudioSession のプロパティ変更の例を示します。

```swift
import Sora
// 接続時のコールバック onConnect で音声モードを変更する
config.mediaChannelHandlers.onConnect = { [weak self] _ in

    // setAudioMode を利用して、AVAudioSession のプロパティを変更する
    // AudioSession は以下のように設定される
    //   - Category : playAndRecord
    //   - Mode : default
    //   - CategoryOptions : defaultToSpeaker, allowBluetooth, allowBluetoothA2DP, allowAirPlay
    //     - output に speaker を設定することで defaultToSpeaker が設定される
    //     - options は未指定のときと同様の設定。ここでは設定方法の例として記載している。
    let result = Sora.shared.setAudioMode(.default(category: .playAndRecord, output: .speaker), options: [.allowBluetooth, .allowBluetoothA2DP, .allowAirPlay])

    // 必要に応じてエラーハンドリングを行う
    switch result {
    case .success():
       // 成功
    case .failure(let error):
       // 失敗
    }
}
```

さらに細かい設定を行う場合は `AVAudioSession` の継承クラスである libwebrtc の `RTCAudioSession` を使用して設定が可能です。
設定を行った結果、libwebrtc の動作に影響が出て、正常に動作しなくなる可能性があるため、libwebrtc の挙動を理解した上で設定をしてください。

### 音声モードを変更するタイミング

音声モードの変更は **接続完了後** に行ってください。
Sora iOS SDK は接続完了時に接続設定に従って音声カテゴリを変更するので、接続完了前にセットした音声モードは接続完了時に上書きされます。

## その他の操作

音声の出力は `AVAudioSession` により管理されているため、その他の操作は `AVAudioSession` を利用してください。

## マイクのパーミッションについて

マイクのパーミッションは配信時にのみ要求されます。
マイクを使う場合は、 [必ず Info.plist にマイクの用途を記述してください。](setup.html#153cee)
