# 音声の送受信

## 概要

Sora Flutter SDK における音声の送信（音声入力デバイスや `PushAudio` による入力）と受信（再生）について説明します。

音声の送信には `MediaDevices.getUserMedia()` で音声入力デバイスを取得し、受信は `audio: true` で接続すると SDK とプラットフォームの標準経路で自動的に再生されます。
アプリケーション側で音声再生用の `Widget` やプレーヤーを別途用意する必要はありません。

## 用語

### 音声トラック

音声トラックは 1 接続に存在する音声のストリーミングデータを表します。
送信する音声トラックは Sora のルールにより 1 本です。受信する音声トラックは接続相手の数だけ存在します。
例えば 3 人で通話している場合、送信する音声トラックは 1 本、受信する音声トラックは 2 本です。

## 音声を送信する

音声を送信するには `MediaDevices.getUserMedia()` で `audio: true` を指定して `LocalMediaStream` を生成し、`SoraConnection.connect()` に渡します。

```dart
// MediaStream を生成する。
// 映像は今回関係ないため false としている。
final stream = await MediaDevices.getUserMedia(
  const GetUserMediaOptions(audio: true, video: false),
);

// 接続用の SoraConnection インスタンスを生成する。
final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: <String>['wss://sora.example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendonly,
    audio: true,
  ),
);

// 接続すると SDK が自動で音声入力デバイスのキャプチャを開始する。
await conn.connect(stream);
```

音声送信では `SoraConnectionConfig.audio` を `true` にするか、未指定にします。未指定時は Sora のデフォルト (`true`) が適用されます。
ロールは `SoraRole.sendonly` または `SoraRole.sendrecv` です。 `recvonly` では音声入力デバイスは使用されません。
権限などのプラットフォーム固有の注意は、 [プラットフォームごとの注意](audio.html#プラットフォームごとの注意) をご確認ください。

### 音声入力デバイスを選択する

`GetUserMediaOptions.audioDeviceId` に `MediaDevices.enumerateAudioInputDevices()` で取得したデバイス ID を指定することで、
使用する音声入力デバイスを選択できます。省略時はプラットフォーム標準の入力が使用されます。

```dart
// 利用可能な音声入力デバイスの一覧を取得する
final audioInputDevices = await MediaDevices.enumerateAudioInputDevices();

// 特定の音声入力デバイスを選択する
final stream = await MediaDevices.getUserMedia(
  GetUserMediaOptions(
    audio: true,
    video: false,
    audioDeviceId: audioInputDevices.first.deviceId,
  ),
);
```

> **注釈**
>
> `Android` では `AudioDeviceInfo.id` は接続状態に依存して変わることがあります。ID を保持しての長期利用は推奨しません。

### 送信音声のソフトミュート

接続中に `setAudioEnabled(false)` を呼ぶことにより送信音声トラックは無効となり、
入力デバイスからの音声は配信されなくなります(ソフトミュート)。
このとき実際にはパケットを送っており、無音のフレーム（デジタルサイレンスパケット）を送り続ける状態となります。

解除するには `setAudioEnabled(true)` を呼びます。

```dart
// 送信音声を無効化する
conn.setAudioEnabled(false);

// 送信音声を再開する
conn.setAudioEnabled(true);

// 現在の状態を確認する
print(conn.isAudioEnabled);
```

> **注釈**
>
> ソフトミュートでは音声入力デバイスは無効化されないため、端末のプライバシーインジケーターは消灯しません。

### 音声デバイスを使わない場合

実音声デバイスを使わずに接続する場合は、 `SoraConnectionConfig.useAudioDevice` に `false` を指定します。
この設定では、macOS / Windows / Linux で SDK が実音声デバイスを初期化せず、音声データをプッシュするためのデバイスを使用します。

```dart
final conn = await Sora.createConnection(
  SoraConnectionConfig(
    signalingUrls: <String>['wss://sora.example.com/signaling'],
    channelId: 'example-channel',
    role: SoraRole.sendonly,
    audio: true,
    useAudioDevice: false,
  ),
);
```

接続前に `MediaDevices` の API を呼び出す場合は、 `Sora.createConnection()` より前に設定値を明示してください。
`MediaDevices.setUseAudioDevice(false)` を使用します。
`useAudioDevice` は共有 `PeerConnectionFactory` の音声デバイス設定であり、個別の track の設定ではありません。

共有の `PeerConnectionFactory` が生成された後に、異なる `useAudioDevice` を指定すると `StateError` が送出されます。
同じプロセス内で複数の接続を作る場合は、音声デバイス設定を統一してください。

## PushAudioDevice を利用する

実音声デバイスを使わずにアプリケーション側の PCM を送受信する場合は、 `PushAudioDevice` を利用します。

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

`macOS` 、 `Windows` 、 `Linux` では、`useAudioDevice: false` を指定すると `PushAudioDevice` が選択されます。
アプリケーション側で生成した 10 ms 分の `Int16List` を `PushAudio.pushPcm()` に渡すことで、音声入力デバイスを使わずに音声を送信できます。

`Android` では `useAudioDevice` が無視され、常に実音声デバイスが使われます。
`iOS` では `useAudioDevice: false` による `PushAudioDevice` の経路を利用できません。

`MediaDevices.setUseAudioDevice(false)` は、`MediaDevices.createMediaStream()` 、`MediaDevices.createAudioTrack()` 、`Sora.createConnection()` など共有 `PeerConnectionFactory` を生成する API より前に呼び出してください。
接続設定にも `useAudioDevice: false` を指定し、同じプロセス内の接続で音声デバイス設定を統一します。

次の例は、48 kHz・モノラルの 440 Hz 正弦波を 10 ms ごとに送信します。
`PushAudio.pushPcm()` に渡す PCM は、サンプルをチャンネル間でインターリーブした `Int16List` にしてください。

```dart
import 'dart:async';
import 'dart:math' as math;
import 'dart:typed_data';

import 'package:sora_sdk/sora_sdk.dart';

Future<void> sendPcm() async {
  const sampleRate = 48000;
  const channels = 1;
  const samplesPer10ms = sampleRate ~/ 100;

  // 共有 PeerConnectionFactory の生成前に PushAudioDevice を選択する。
  MediaDevices.setUseAudioDevice(false);

  final stream = MediaDevices.createMediaStream();
  final audioTrack = await MediaDevices.createAudioTrack();
  stream.addTrack(audioTrack);

  final conn = await Sora.createConnection(
    SoraConnectionConfig(
      signalingUrls: <String>['wss://sora.example.com/signaling'],
      channelId: 'example-channel',
      role: SoraRole.sendonly,
      audio: true,
      video: false,
      useAudioDevice: false,
    ),
  );

  Timer? timer;
  try {
    await conn.connect(stream);

    var phase = 0.0;
    final phaseStep = 2 * math.pi * 440 / sampleRate;
    final pcm = Int16List(samplesPer10ms * channels);
    timer = Timer.periodic(const Duration(milliseconds: 10), (_) {
      for (var i = 0; i < samplesPer10ms; i++) {
        pcm[i] = (math.sin(phase) * 12000).round();
        phase += phaseStep;
      }
      PushAudio.pushPcm(pcm, sampleRate, channels);
    });

    // 実際のアプリケーションでは、音声入力のコールバックなどから PCM を供給する。
    await Future<void>.delayed(const Duration(minutes: 1));
  } finally {
    // タイマーを先に止め、PushAudio.dispose() 後に pushPcm() が呼ばれないようにする。
    timer?.cancel();
    // conn.dispose() は接続を切断してから後始末を行う。
    await conn.dispose();
    await audioTrack.dispose();
    await stream.dispose();
    PushAudio.dispose();
  }
}
```

`PushAudio.pushPcm()` の引数は、次の条件を満たす必要があります。

- `data` は 10 ms 分の符号付き 16 bit PCM ( `Int16List` ) にする
- 1 チャンネルあたりのサンプル数は `sampleRate * 10 / 1000` にする
- 複数チャンネルの場合はチャンネル間でインターリーブする
- アプリケーション側の音声ソースから 10 ms 周期で呼び出す

`sampleRate` と `channels` は、送信する PCM の形式に合わせて指定してください。
SDK の API ドキュメントでは通常の値として 48000 Hz、モノラルを示しています。
`Timer.periodic` は処理の流れを示す例であり、実際のアプリケーションでは音声入力のコールバックなど、音声ソースが提供するタイミングを利用してください。

### PushAudio で受信 PCM を取得する

`PushAudio.pullPcm()` は、`PushAudioDevice` が受信した音声を PCM として取り出す API です。
物理出力デバイスを使わずに受信音声のデコードを進めるため、アプリケーション側で 10 ms ごとに呼び出します。
`durationMs` の既定値は 10 ms です。

```dart
const sampleRate = 48000;
const channels = 1;

// 受信音声を取得する処理を 10 ms ごとに呼び出す。
final receivedPcm = PushAudio.pullPcm(sampleRate, channels);
if (receivedPcm.isEmpty) {
  // playout が開始される前は空のリストになる。
  return;
}

// receivedPcm をアプリケーション側の再生、保存、解析などの処理へ渡す。
```

`pullPcm()` の `sampleRate` 、`channels` 、`durationMs` は正の値を指定してください。
指定した形式の PCM がまだ取得できない場合は空のリストが返ります。
`pullPcm()` は音声送信の設定を行わないため、送信する場合は別途 `createAudioTrack()` で生成したトラックを `LocalMediaStream` に追加してください。

## 音声を再生する

音声を再生するには、接続設定で `audio: true` を指定するか、 `audio` を未指定にします。未指定時は Sora のデフォルト (`true`) が適用されます。
受信した音声の再生自体は SDK とプラットフォームの標準経路で自動的に行われるため、アプリケーション側で音声再生用の `Widget` やプレーヤーを用意する必要はありません。
一方で、再生対象の音声が来たことを検知したい場合は、リモート音声トラックの追加 / 削除をイベントで確認します。

### 接続して受信音声を再生する

受信音声を再生する例です。

```dart
final config = SoraConnectionConfig(
  signalingUrls: <String>['wss://sora.example.com/signaling'],
  channelId: 'example-channel',
  role: SoraRole.recvonly,
  audio: true,
  video: false,
);

final conn = await Sora.createConnection(config);
await conn.connect();
```

`recvonly` で音声受信のみを行う場合は、マイク利用の権限や設定等は不要です。

### リモート音声トラックの追加 / 削除を検知する

ここでのトラック検知は、アプリケーション側で音声を再生するための処理ではありません。
再生対象の音声が存在するか、どの接続相手の音声か、といった状態を把握するための確認手段です。

リモート音声トラックの追加 / 削除は `conn.events` で `RemoteMediaStreamTrack` の `kind == 'audio'` を判定して確認できます。

```dart
conn.events.listen((event) {
  switch (event) {
    case SoraTrackEvent(:final track):
      if (track.kind == 'audio') {
        print('audio track added: ${track.trackId} ${track.connectionId}');
      }
    case SoraRemoveTrackEvent(:final track):
      if (track.kind == 'audio') {
        print('audio track removed: ${track.trackId}');
      }
    default:
      break;
  }
});
```

`trackId` は複数のリモートトラックを扱うときの識別に使えます。
接続済みでも音声トラックが来ていなければ、再生対象の音声はありません。

#### 接続状態とあわせて見る

実際のアプリケーションでは、接続状態の変化とリモート音声トラックの追加をあわせて監視すると、受信を開始できたかを判断しやすくなります。

```dart
conn.events.listen((event) {
  switch (event) {
    case SoraConnectionStateChangedEvent(:final state):
      print('state: $state');
    case SoraTrackEvent(:final track):
      if (track.kind == 'audio') {
        print('audio track added: ${track.trackId} ${track.connectionId}');
      }
    case SoraRemoveTrackEvent(:final track):
      if (track.kind == 'audio') {
        print('audio track removed: ${track.trackId}');
      }
    default:
      break;
  }
});
```

`SoraConnectedState` が通知されても、すぐにリモート音声トラックが追加されるとは限りません。
接続状態とトラックイベントを分けて確認することで、接続は完了したが再生対象の音声はまだ来ていない状態を区別できます。

イベントの詳細については [イベント](event.html) をご確認ください。

### Sora 接続設定の `audio` と音声の関係

`audio: true` は Sora 接続で音声を扱う設定です。ロールによって意味が異なります。

| 構成 | 意味 |
| --- | --- |
| `recvonly` + `audio: true` | 受信音声を再生する |
| `recvonly` + `audio: false` | 音声を受信しない |
| `sendonly` + `audio: true` | 音声を送信する |
| `sendonly` + `audio: false` | 音声を送信しない |
| `sendrecv` + `audio: true` | 音声を送受信する |
| `sendrecv` + `audio: false` | 音声を送信せず、受信する |

`audio` を未指定にした場合も Sora のデフォルト (`true`) が適用されます。 `sendrecv` は `audio: false` でも受信音声を再生します。マイク利用等の音声入力とは切り分けて考える必要があります。

## 音声の送受信についてできないこと

Sora Flutter SDK の公開 API では、次のような音声制御はできません。

- 音声のステレオ送受信 ( Sora の実験的機能である Opus の詳細パラメータで `stereo` を指定する場合を除く)
- 再生先スピーカーの選択
- イヤホン / スピーカーの切り替え制御
- リモート音声ごとの再生音量変更
- 受信音声だけを個別にミュート
- 端末のマイクのプライバシーインジケーターを消灯させるミュート
- `iOS` / `Android` で `PushAudioDevice` を使った任意の PCM 音声の送信

> **注釈**
>
> [シグナリング設定](signaling.html) の Opus の詳細パラメータで `stereo` / `sprop_stereo` を指定できます。このパラメータ指定は Sora の実験的機能のため、利用する場合は事前に Sora のサポートへ連絡してください。

受信音声は SDK とプラットフォームの標準経路で再生されます。出力のルーティングは OS と `libwebrtc` の挙動に従います。


## プラットフォームごとの注意

### Android

- 音声出力のルーティングは Android / `libwebrtc` の標準挙動に従います
- Bluetooth / 受話スピーカー / スピーカーの切り替えは通話用音声ルートの制約を受けるため、出力先を常に厳密指定できるとは限りません
- マイクを利用する場合は `AndroidManifest.xml` に `RECORD_AUDIO` 権限の宣言と、ランタイム権限の取得が必要です
- 詳細は [Android での利用方法](android.html) をご確認ください

### iOS / iPadOS

- 音声の再生先は `AVAudioSession` の音声ルートに従います
- AirPlay や Bluetooth などの再生先をユーザーに選ばせる場合は、アプリケーション側で `AVRoutePickerView` を利用してください
- `AVAudioSession` の category が `.playAndRecord` に設定されるため、受信音量が小さくなる場合があります。これは `iOS` の仕様です。必要に応じて接続後に `AVAudioSession` の category を変更してください
- マイクを利用する場合は `Info.plist` に `NSMicrophoneUsageDescription` の設定と、ランタイム権限の取得が必要です
- 詳細は [iOS / iPadOS での利用方法](ios_ipados.html) をご確認ください

### macOS

- 音声は OS / `CoreAudio` の既定音声出力へ再生されます
- SDK から再生先を明示選択する public API はありません
- マイクを利用する場合は `Info.plist` に `NSMicrophoneUsageDescription` を設定し、Entitlements に `com.apple.security.device.audio-input` を追加する必要があります
- 詳細は [macOS での利用方法](macos.html) をご確認ください

## 関連ページ

- [実装ガイド](getting_started.html)
- [シグナリング設定](signaling.html)
- [メディアデバイス](media_device.html)
- [イベント](event.html)
