본문으로 건너뛰기
Version: 6.0

커스텀 오디오 소스

기본적으로 PlanetKit은 통화 시작 시 마이크 장치를 자동으로 감지하여 제어하며, 마이크에서 캡처한 오디오를 통화에 사용합니다. 하지만 애플리케이션의 요구사항에 따라 이 구조를 변경해야 할 필요가 있을 수 있습니다. 사용자의 오디오 대신 외부 소스에서 가져온 오디오를 전송하는 것이 한 가지 예입니다.

이러한 요구를 충족할 수 있도록 PlanetKit은 원하는 오디오 프레임을 직접 공급할 수 있는 커스텀 오디오 소스(custom audio source) 기능을 제공합니다. 이 기능을 통해 사용자가 원하는 오디오 프레임을 상대방에게 전달할 수 있습니다.

Note

커스텀 오디오 소스를 사용하는 동안은 PlanetKit이 마이크 장치를 제어하지 않습니다.

지원 통화 유형최소 SDK 버전
1대1 통화, 그룹 통화(컨퍼런스)PlanetKit 6.0

활용 사례

아래와 같은 애플리케이션 요구사항이 있을 때 커스텀 오디오 소스를 활용하여 구현할 수 있습니다.

  • 음성 파일을 오디오 소스로 사용
  • 외부 오디오 소스(라이브 방송 중계나 웹 스트리밍) 연동
  • 가공된 오디오 데이터, 또는 여러 오디오 소스를 믹싱한 데이터를 오디오 소스로 사용
  • 마이크 장치에 대한 제어를 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 설정이 필요한 경우

커스텀 오디오 소스 또는 커스텀 오디오 싱크를 이용해 실제 마이크와 스피커가 아닌 다른 입력 또는 출력을 사용한다면, 통화 세션 클래스인 PlanetKitCallPlanetKitConference에서 제공하는 VQE 기능 중 하나인 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 레퍼런스 설정이 필요한 경우

통화 세션 클래스인 PlanetKitCallPlanetKitConference에서 제공하는 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는 다음과 같습니다.