音声の送受信

概要

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

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

用語

音声トラック

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

音声を送信する

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

// 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 では音声入力デバイスは使用されません。 権限などのプラットフォーム固有の注意は、 プラットフォームごとの注意 をご確認ください。

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

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

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

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

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

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

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

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

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

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

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

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

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 にしてください。

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 です。

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 やプレーヤーを用意する必要はありません。 一方で、再生対象の音声が来たことを検知したい場合は、リモート音声トラックの追加 / 削除をイベントで確認します。

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

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

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' を判定して確認できます。

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

接続状態とあわせて見る

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

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

イベントの詳細については イベント をご確認ください。

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 音声の送信

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

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

Android

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

iOS / iPadOS

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

macOS

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

関連ページ