Вот моя реализация JVM для настольного компьютера, она отлично работает, по крайней мере, в Linux:
Код: Выделить всё
class NativeWebRTCClientDesktopImpl : NativeWebRTCClient() {
private val audioPlayer = AudioPlayer()
override fun onAudioReceivedInternal(data: ByteBuffer) {
audioPlayer.onAudioReceivedInternal(data)
}
class AudioPlayer {
private val audioLine: SourceDataLine
private val sampleRate = SAMPLE_RATE
private val channels = CHANNELS_COUNT
private val bitsPerSample = BITS_PER_SAMPLE
init {
val format = AudioFormat(
sampleRate.toFloat(),
bitsPerSample,
channels,
true,
false
)
audioLine = AudioSystem.getSourceDataLine(format).apply {
open(format)
start()
}
}
fun onAudioReceivedInternal(data: ByteBuffer) {
val bytes = ByteArray(data.remaining())
data.get(bytes)
audioLine.write(bytes, 0, bytes.size)
}
fun cleanup() {
audioLine.drain()
audioLine.stop()
audioLine.close()
}
}
private companion object {
private const val CHANNELS_COUNT = 2
private const val SAMPLE_RATE = 48_000
private const val BITS_PER_SAMPLE = 16
}
}
Код: Выделить всё
class NativeWebRTCClientAndroidImpl(
private val audioManager: AudioManager,
) : NativeWebRTCClient() {
private val logger = Logger.withTag(LOG_TAG)
private val audioPlayer = AudioPlayer()
override fun onAudioReceivedInternal(data: ByteBuffer) {
audioPlayer.onAudioReceivedInternal(data)
}
private inner class AudioPlayer {
private val frameSize = 960 // WebRTC frame size
private val bytesPerFrame = frameSize * 2 * 2 // stereo * 16bit
private val audioTrack: AudioTrack
private val bufferSize: Int = AudioTrack.getMinBufferSize(
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT
)
private val tempDataBuffer = ByteArray(TEMP_DATA_BUFFER_SIZE)
private val dataBuffer = ByteArray(bufferSize)
private var currentBufferPosition = 0
private var speakerDevice: AudioDeviceInfo? = null
private val routingCallback = AudioTrack.OnRoutingChangedListener { forceSpeakerOutput() }
init {
logger.d { "Buffer size: $bufferSize" }
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
devices.forEach {
logger.d { "Available device: ${it.type} - ${it.productName}" }
}
speakerDevice = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
.firstOrNull { device ->
device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
}
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build()
val audioFormat = AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(CHANNEL_CONFIG)
.build()
audioTrack = AudioTrack.Builder()
.setAudioAttributes(audioAttributes)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build()
audioTrack.addOnRoutingChangedListener(routingCallback, null)
audioManager.apply {
mode = AudioManager.MODE_NORMAL
isSpeakerphoneOn = true
}
speakerDevice?.let {
if (!audioTrack.setPreferredDevice(it)) {
logger.e { "Failed to set ${it.type} as preferred device" }
}
}
audioTrack.play()
}
private fun forceSpeakerOutput() {
speakerDevice?.let { device ->
if (audioTrack.preferredDevice?.id != device.id) {
audioTrack.preferredDevice = device
}
}
audioManager.isSpeakerphoneOn = true
}
fun onAudioReceivedInternal(data: ByteBuffer) {
val dataSize = data.remaining()
data.get(tempDataBuffer, 0, dataSize)
// I've also tried to simply do audioTrack.write() with the received data without buffering
// Result is the same
// The idea here was to wait until min buffer will be filled
var offset = 0
while (offset < dataSize) {
val copyLength = minOf(bufferSize - currentBufferPosition, dataSize - offset)
tempDataBuffer.copyInto(
dataBuffer,
currentBufferPosition,
offset,
offset + copyLength
)
currentBufferPosition += copyLength
offset += copyLength
logger.d { "Copied $copyLength to buffer, ${dataSize - offset} left" }
if (currentBufferPosition == bufferSize) {
audioTrack.write(dataBuffer, 0, bufferSize, AudioTrack.WRITE_BLOCKING)
currentBufferPosition = 0
}
}
}
}
private companion object {
private const val LOG_TAG = "NativeWebRTCClient"
private const val SAMPLE_RATE = 48000
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_STEREO
private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private const val TEMP_DATA_BUFFER_SIZE = 1024 * 128
}
}
Я тоже попробовал это (так как на стороне C++ это int16_t):
Код: Выделить всё
fun onAudioReceivedInternal(data: ByteBuffer) {
data.order(ByteOrder.LITTLE_ENDIAN)
val shortBuffer = data.asShortBuffer()
val shortArray = ShortArray(shortBuffer.remaining())
shortBuffer.get(shortArray)
audioTrack.write(shortArray, 0, shortArray.size, AudioTrack.WRITE_BLOCKING)
}
Аудиоформат правильный.
Что может быть не так?
UPD: Добавлена минимальная демо: https://github.com/RankoR/android-pcm-experiments
Звучит не так плохо, как мой WebRTC-поток, но и у него есть заметные артефакты.
Подробнее здесь: https://stackoverflow.com/questions/792 ... -artifacts