本文にスキップする
Version: 6.1

カスタムオーディオソース

PlanetKitは、基本的に通話開始時にマイクデバイスを自動的に検出して制御し、マイクでキャプチャーしたオーディオを通話に使用します。ただし、アプリケーションの要求事項によっては、この構造を変更する必要があります。ユーザーのオーディオの代わりに、外部ソースから取得したオーディオを送信するのが一例です。

このような要求を満たすために、PlanetKitは希望するオーディオフレームを直接供給できるカスタムオーディオソース(custom audio source)機能を提供します。この機能を通じて、ユーザーが希望するオーディオフレームを相手に伝えることができます。

Note

カスタムオーディオソースを使用している間は、PlanetKitがマイクデバイスを制御しません。

通話タイプSDKの最低バージョン
1対1通話、グループ通話(カンファレンス)PlanetKit 6.0

活用事例

以下のようなアプリケーションの要求事項がある場合、カスタムオーディオソースを活用して実装できます。

  • 音声ファイルをオーディオソースとして使用
  • 外部オーディオソース(ライブ配信中継やWebストリーミング)連動
  • 加工されたオーディオデータ、または複数のオーディオソースをミキシングしたデータをオーディオソースとして使用
  • マイクデバイスに対する制御をPlanetKitではなくアプリケーションで直接制御

実装手順

カスタムオーディオソース機能を実装する手順は、次のとおりです。

カスタムオーディオソースクラスを実装する

PlanetKitCustomAudioSourceを拡張(extend)し、メソッドを実装します。

import com.linecorp.planetkit.audio.PlanetKitCustomAudioSource
import com.linecorp.planetkit.audio.AudioFrame
import com.linecorp.planetkit.audio.PlanetKitAudioSampleType
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Timer
import kotlin.math.PI
import kotlin.math.sin
import kotlin.concurrent.fixedRateTimer

// Example implementation of a custom audio source
class MyCustomAudioSource : PlanetKitCustomAudioSource() {

// Audio configuration settings
private val sampleType = PlanetKitAudioSampleType.SIGNED_SHORT_16
private val sampleRate = 48000 // 48 kHz

// Streaming state management
private var isStreaming = false
private var timer: Timer? = null

// Start audio streaming
fun start() {
if (!isStreaming) {
isStreaming = true

timer = fixedRateTimer(
name = "CustomAudioSourceTimer",
initialDelay = 0L,
period = 20L // 20ms cycle
) {
if (isStreaming) {
generateAndSendAudioFrames()
}
}
}
}

// Stop audio streaming
fun stop() {
isStreaming = false
timer?.cancel()
timer = null
}

// Main loop that generates and sends audio frames every 20ms
private fun generateAndSendAudioFrames() {
// Generate audio data (example: sine wave)
val audioData = generateSineWave()

// Create ByteBuffer
val buffer = ByteBuffer.allocate(audioData.size * 2).order(ByteOrder.LITTLE_ENDIAN)
for (sample in audioData) {
buffer.putShort(sample)
}

buffer.flip()

// Create and send AudioFrame
val audioFrame = createAudioFrame(buffer)
postFrameData(audioFrame)
audioFrame.release()
}

private fun generateSineWave(): ShortArray {
val samples = ShortArray(960) // 20ms at 48kHz = 960 samples
for (i in samples.indices) {
val time = i.toDouble() / sampleRate
val amplitude = (Short.MAX_VALUE * 0.3).toInt()
samples[i] = (amplitude * sin(2 * PI * 440 * time)).toInt().toShort()
}
return samples
}

private fun createAudioFrame(buffer: ByteBuffer): AudioFrame {
return object : AudioFrame {
override fun getBuffer(): ByteBuffer = buffer
override fun getSampleType(): PlanetKitAudioSampleType = sampleType
override fun getSize(): Long = buffer.remaining().toLong()
override fun getSamplingRate(): Int = sampleRate
override fun getTimestamp(): Long = System.nanoTime()
override fun release() {}
}
}
}

カスタムオーディオソースを使用して相手にオーディオを転送する

PlanetKitCustomAudioSourceオブジェクトを引数として、PlanetKitCallまたはPlanetKitConferencesetCustomAudioSource()を呼び出してカスタムオーディオソースを開始します。

カスタムオーディオソースが開始されると、カスタムオーディオソースから呼び出されるpostFrameData()メソッドを介してオーディオデータが送信されます。

val customAudioSource = MyCustomAudioSource()

// For 1-to-1 call
fun useCustomAudioSourceForCall(call: PlanetKitCall) {
call.setCustomAudioSource(customAudioSource)
customAudioSource.start()
}

// For group call
fun useCustomAudioSourceForConference(conference: PlanetKitConference) {
conference.setCustomAudioSource(customAudioSource)
customAudioSource.start()
}

カスタムオーディオソースをマイクに戻しておく

カスタムオーディオソースをマイクに戻すには、PlanetKitCallまたはPlanetKitConferenceclearCustomAudioSource()を呼び出してカスタムオーディオソースを停止します。

val customAudioSource = MyCustomAudioSource()

// For 1-to-1 call
fun useDefaultAudioSourceForCall(call: PlanetKitCall) {
call.clearCustomAudioSource()
customAudioSource.stop()
}

// For group call
fun useDefaultAudioSourceForConference(conference: PlanetKitConference) {
conference.clearCustomAudioSource()
customAudioSource.stop()
}

主な考慮事項

カスタムオーディオソース機能を使用する際に考慮すべき事項は次のとおりです。

オーディオフォーマット

  • サンプルレート:48kHzまたは16kHz
  • チャンネル:モノ(1チャンネル)
  • サンプルタイプ:16-bit PCM

パフォーマンスの最適化

  • パフォーマンスのためにメモリの割り当てと解除を最小限に抑える必要があります。
  • オーディオ処理は必ずバックグラウンドスレッドで行ってください。

カスタムオーディオデバイス使用時のAEC設定

AEC(acoustic echo cancellation)は、スピーカーから出力された音がマイクに再入力されて発生するエコーを除去する技術です。PlanetKitが提供するAEC関連の機能には、VQE制御のAECアプリケーションAECリファレンスがあります。一般的には、AECに関連して別途の設定は必要ありませんが、カスタムオーディオデバイスを利用する場合は、状況に応じてAEC関連の設定を直接実行する必要があります。

AEC関連設定が必要な条件

アプリケーションの実装が以下の条件に該当する場合は、AEC関連の設定を直接実行する必要があります。

  • Case 1:VQE制御のAEC設定が必要な場合
    • 条件
      • 以下の条件のいずれかに該当する場合
        • カスタムオーディオソースからの実際のマイクデバイス以外の入力(例:WAVファイル、HTTP URLなど)を使用する場合
        • カスタムオーディオシンクからの実際のスピーカーデバイス以外の入力(例:ファイルレコーディング)を使用する場合
    • 必要設定
      • カスタムオーディオデバイスの使用開始後にsetAcousticEchoCancellerMode(PlanetKitVoiceProcessor.AcousticEchoCancellerMode.DISABLED)呼び出し
      • カスタムオーディオデバイスの使用終了後にsetAcousticEchoCancellerMode(PlanetKitVoiceProcessor.AcousticEchoCancellerMode.INTENSITY_RECOMMENDED)呼び出し
  • Case 2:アプリケーションAECリファレンス設定が必要な場合
    • 条件
      • PlanetKitから引き渡されたスピーカーデータをカスタムオーディオシンクで変調して出力する場合
        • ただし、マイク入力が実際のマイクデバイスの場合にのみ該当(入力がWAVファイルなど実際のエコーが入力できるデバイスでない場合は該当なし)
    • 必要設定
      • カスタムオーディオシンクの使用開始後にstartUserAcousticEchoCancellerReference()呼び出し
      • putUserAcousticEchoCancellerReference()でスピーカーに出力したい音源を提供
      • カスタムオーディオシンクの使用終了後にstopUserAcousticEchoCancellerReference()呼び出し

Case 1:VQE制御のAEC設定が必要な場合

カスタムオーディオソースまたはカスタムオーディオシンクを利用して実際のマイクとスピーカー以外の入力または出力を使用している場合は、通話セッションクラスであるPlanetKitCallおよびPlanetKitConferenceで提供されるVQE機能の1つであるAECの使用を中止して、通話品質を向上させることができます。PlanetKitSendVoiceProcessorsetAcousticEchoCancellerMode()を呼び出してAECモードを設定できます。

カスタムオーディオソースクラスを実装する

カスタムオーディオソースを利用してオーディオファイルを入力として使用するためのクラスを実装します。

  • 通話に接続された後、オーディオデータを送信するために定期的にpostFrameData()を呼び出します。
import com.linecorp.planetkit.audio.PlanetKitCustomAudioSource
import com.linecorp.planetkit.audio.AudioFrame
import com.linecorp.planetkit.audio.PlanetKitAudioSampleType
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.Timer
import kotlin.math.PI
import kotlin.math.sin
import kotlin.concurrent.fixedRateTimer

// Example implementation of a custom audio source
class MyCustomAudioSource : PlanetKitCustomAudioSource() {

// Audio configuration settings
private val sampleType = PlanetKitAudioSampleType.SIGNED_SHORT_16
private val sampleRate = 48000 // 48 kHz

// Streaming state management
private var isStreaming = false
private var timer: Timer? = null

// Start audio streaming
fun start() {
if (!isStreaming) {
isStreaming = true

timer = fixedRateTimer(
name = "CustomAudioSourceTimer",
initialDelay = 0L,
period = 20L // 20ms cycle
) {
if (isStreaming) {
generateAndSendAudioFrames()
}
}
}
}

// Stop audio streaming
fun stop() {
isStreaming = false
timer?.cancel()
timer = null
}

// Main loop that generates and sends audio frames every 20ms
private fun generateAndSendAudioFrames() {
// Generate audio data (example: sine wave)
val audioData = generateSineWave()

// Create ByteBuffer
val buffer = ByteBuffer.allocate(audioData.size * 2).order(ByteOrder.LITTLE_ENDIAN)
for (sample in audioData) {
buffer.putShort(sample)
}

buffer.flip()

// Create and send AudioFrame
val audioFrame = createAudioFrame(buffer)
postFrameData(audioFrame)
audioFrame.release()
}

private fun generateSineWave(): ShortArray {
val samples = ShortArray(960) // 20ms at 48kHz = 960 samples
for (i in samples.indices) {
val time = i.toDouble() / sampleRate
val amplitude = (Short.MAX_VALUE * 0.3).toInt()
samples[i] = (amplitude * sin(2 * PI * 440 * time)).toInt().toShort()
}
return samples
}

private fun createAudioFrame(buffer: ByteBuffer): AudioFrame {
return object : AudioFrame {
override fun getBuffer(): ByteBuffer = buffer
override fun getSampleType(): PlanetKitAudioSampleType = sampleType
override fun getSize(): Long = buffer.remaining().toLong()
override fun getSamplingRate(): Int = sampleRate
override fun getTimestamp(): Long = System.nanoTime()
override fun release() {}
}
}
}

カスタムオーディオソースを使用する

カスタムオーディオソースを使用するには、実装したカスタムオーディオソースのオブジェクトを引数としてPlanetKitCallまたはPlanetKitConferencesetCustomAudioSource()を呼び出し、PlanetKitVoiceProcessor.AcousticEchoCancellerMode.DISABLEDを引数としてPlanetKitSendVoiceProcessorクラスのsetAcousticEchoCancellerMode()を呼び出してAEC を無効に設定します。

val customAudioSource = MyCustomAudioSource()

// For 1-to-1 call
fun useCustomAudioSourceForCall(call: PlanetKitCall) {
val customAudioSource = MyCustomAudioSource()
call.setCustomAudioSource(customAudioSource)
customAudioSource.start()

// Disable AEC
call.sendVoiceProcessor.setAcousticEchoCancellerMode(
PlanetKitVoiceProcessor.AcousticEchoCancellerMode.DISABLED
)
}

カスタムオーディオソースをマイクに戻しておく

カスタムオーディオソースをマイクに戻しておくには、PlanetKitCallまたはPlanetKitConferenceclearCustomAudioSource()を呼び出し、PlanetKitVoiceProcessor.AcousticEchoCancellerMode.INTENSITY_RECOMMENDEDを引数としてPlanetKitSendVoiceProcessorクラスのsetAcousticEchoCancellerMode()を呼び出してAECを有効に設定します。

val customAudioSource = MyCustomAudioSource()

// For 1-to-1 call
fun useDefaultAudioSourceForCall(call: PlanetKitCall) {
call.clearCustomAudioSource()
customAudioSource.stop()

// Enable AEC by default
call.sendVoiceProcessor.setAcousticEchoCancellerMode(
PlanetKitVoiceProcessor.AcousticEchoCancellerMode.INTENSITY_RECOMMENDED
)
}

Case 2:アプリケーションAECリファレンス設定が必要な場合

通話セッションクラスであるPlanetKitCallおよびPlanetKitConferenceで提供されるstartUserAcousticEchoCancellerReference()stopUserAcousticEchoCancellerReference()を利用して、AECリファレンスデータを使用するかどうかを設定できます。AECリファレンスデータを有効にした後、通話セッションクラスのputUserAcousticEchoCancellerReference()を利用して変更されたオーディオデータを提供します。

カスタムオーディオシンククラスを実装する

カスタムオーディオシンクを利用してオーディオ出力データを変更するためのクラスを実装します。

  • 通話に接続された後、受信されたオーディオデータを取得するために定期的にgetFrameData()を呼び出します。
  • オーディオデータを加工し、これを引数としてputUserAcousticEchoCancellerReference()を呼び出します。
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION
import android.media.AudioFormat
import android.media.AudioManager
import android.media.AudioTrack
import com.linecorp.planetkit.audio.AudioSink
import com.linecorp.planetkit.audio.PlanetKitAudioSampleType
import java.util.Timer
import kotlin.concurrent.fixedRateTimer

class MyCustomAudioSink(private val context: Context): AudioSink() {
companion object {
private const val SAMPLING_RATE = 16000
private const val PULL_AUDIO_INTERVAL_MS = 20
private val SAMPLE_TYPE = PlanetKitAudioSampleType.SIGNED_SHORT_16
private const val SAMPLE_COUNT_PER_PULL =
(PULL_AUDIO_INTERVAL_MS * SAMPLING_RATE) / 1000
}

private var player: AudioTrack? = null
private var timer: Timer? = null
private var isPlaying = false

override fun onPrepare() {
val bufferSize = AudioTrack.getMinBufferSize(
SAMPLING_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
player = AudioTrack(
AudioAttributes.Builder()
.setLegacyStreamType(USAGE_VOICE_COMMUNICATION)
.build(),
AudioFormat.Builder()
.setSampleRate(SAMPLING_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
audioManager.generateAudioSessionId()
)
}

override fun onStart() {
if (isPlaying) return
isPlaying = true
player?.play()

timer = fixedRateTimer(
name = "AudioSinkTimer",
initialDelay = 0L,
period = PULL_AUDIO_INTERVAL_MS.toLong()
) {
writeAudioFrame()
}
}

override fun onStop() {
isPlaying = false
timer?.cancel()
timer = null
player?.pause()
player?.flush()
}

private fun writeAudioFrame() {
if (!isPlaying) return

val frame = getFrameData(SAMPLING_RATE, SAMPLE_TYPE, SAMPLE_COUNT_PER_PULL)
player?.write(frame.getBuffer(), frame.getSize().toInt(), AudioTrack.WRITE_BLOCKING)
putUserAcousticEchoCancellerReference(frame)
frame.release()
}
}

カスタムオーディオシンクを使用する

カスタムオーディオシンクを使用するには、実装したカスタムオーディオシンクのオブジェクトを引数として、PlanetKitCallまたはPlanetKitConferencesetAudioSink()を呼び出し、アプリケーションAECリファレンスの使用を開始するために通話セッションクラスのstartUserAcousticEchoCancellerReference()を呼び出します。

val customAudioSink = MyCustomAudioSink(context)

// For 1-to-1 call
fun useCustomAudioSinkForCall(call: PlanetKitCall) {
call.setAudioSink(customAudioSink)
call.startUserAcousticEchoCancellerReference()
}

カスタムオーディオシンクをスピーカーに戻しておく

カスタムオーディオシンクをスピーカーに戻しておくには、DefaultSpeakerAudioSinkオブジェクトを引数として、PlanetKitCallまたはPlanetKitConferencesetAudioSink()を呼び出し、アプリケーションAECリファレンスの使用を中止するために通話セッションクラスのstopUserAcousticEchoCancellerReference()を呼び出します。

// For 1-to-1 call
fun useDefaultAudioSinkForCall(call: PlanetKitCall) {
val defaultAudioSink = DefaultSpeakerAudioSink.getInstance()
call.setAudioSink(defaultAudioSink)

call.stopUserAcousticEchoCancellerReference()
}

関連API

カスタムオーディオソース機能とこれに関連するAPIは、次のとおりです。