カスタムオーディオソース
PlanetKitは、基本的に通話開始時にマイクデバイスを自動的に検出して制御し、マイクでキャプチャーしたオーディオを通話に使用します。ただし、アプリケーションの要求事項によっては、この構造を変更する必要があります。ユーザーのオーディオの代わりに、外部ソースから取得したオーディオを送信するのが一例です。
このような要求を満たすために、PlanetKitは希望するオーディオフレームを直接供給できるカスタムオーディオソース(custom audio source)機能を提供します。この機能を通じて、ユーザーが希望するオーディオフレームを相手に伝えることができます。
カスタムオーディオソースを使用している間は、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またはPlanetKitConferenceのsetCustomAudioSource()を呼び出してカスタムオーディオソースを開始します。
カスタムオーディオソースが開始されると、カスタムオーディオソースから呼び出される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またはPlanetKitConferenceのclearCustomAudioSource()を呼び出してカスタムオーディオソースを停止します。
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ファイルなど実際のエコーが入力できるデバイスでない場合は該当なし)
- PlanetKitから引き渡されたスピーカーデータをカスタムオーディオシンクで変調して出力する場合
- 必要設定
- カスタムオーディオシンクの使用開始後に
startUserAcousticEchoCancellerReference()呼び出し putUserAcousticEchoCancellerReference()でスピーカーに出力したい音源を提供- カスタムオーディオシンクの使用終了後に
stopUserAcousticEchoCancellerReference()呼び出し
- カスタムオーディオシンクの使用開始後に
- 条件
Case 1:VQE制御のAEC設定が必要な場合
カスタムオーディオソースまたはカスタムオーディオシンクを利用して実際のマイクとスピーカー以外の入力または出力を使用している場合は、通話セッションクラスであるPlanetKitCallおよびPlanetKitConferenceで提供されるVQE機能の1つであるAECの使用を中止して、通話品質を向上させることができます。PlanetKitSendVoiceProcessorのsetAcousticEchoCancellerMode()を呼び出して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またはPlanetKitConferenceのsetCustomAudioSource()を呼び出し、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またはPlanetKitConferenceのclearCustomAudioSource()を呼び出し、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またはPlanetKitConferenceのsetAudioSink()を呼び出し、アプリケーションAECリファレンスの使用を開始するために通話セッションクラスのstartUserAcousticEchoCancellerReference()を呼び出します。
val customAudioSink = MyCustomAudioSink(context)
// For 1-to-1 call
fun useCustomAudioSinkForCall(call: PlanetKitCall) {
call.setAudioSink(customAudioSink)
call.startUserAcousticEchoCancellerReference()
}
カスタムオーディオシンクをスピーカーに戻しておく
カスタムオーディオシンクをスピーカーに戻しておくには、DefaultSpeakerAudioSinkオブジェクトを引数として、PlanetKitCallまたはPlanetKitConferenceのsetAudioSink()を呼び出し、アプリケーションAECリファレンスの使用を中止するために通話セッションクラスのstopUserAcousticEchoCancellerReference()を呼び出します。
// For 1-to-1 call
fun useDefaultAudioSinkForCall(call: PlanetKitCall) {
val defaultAudioSink = DefaultSpeakerAudioSink.getInstance()
call.setAudioSink(defaultAudioSink)
call.stopUserAcousticEchoCancellerReference()
}
関連API
カスタムオーディオソース機能とこれに関連するAPIは、次のとおりです。