@livekit/react-native 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +4 -3
  2. package/android/build.gradle +2 -1
  3. package/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt +61 -5
  4. package/android/src/main/java/com/livekit/reactnative/LivekitReactNativeModule.kt +81 -4
  5. package/android/src/main/java/com/livekit/reactnative/audio/events/Events.kt +6 -0
  6. package/android/src/main/java/com/livekit/reactnative/audio/processing/AudioFormat.kt +2 -0
  7. package/android/src/main/java/com/livekit/reactnative/audio/processing/AudioProcessingController.kt +27 -0
  8. package/android/src/main/java/com/livekit/reactnative/audio/processing/AudioProcessorInterface.kt +52 -0
  9. package/android/src/main/java/com/livekit/reactnative/audio/processing/AudioRecordSamplesDispatcher.kt +72 -0
  10. package/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkManager.kt +75 -0
  11. package/android/src/main/java/com/livekit/reactnative/audio/processing/CustomAudioProcessingFactory.kt +78 -0
  12. package/android/src/main/java/com/livekit/reactnative/audio/processing/MultibandVolumeProcessor.kt +181 -0
  13. package/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt +67 -0
  14. package/android/src/main/java/com/livekit/reactnative/audio/processing/fft/FFTAudioAnalyzer.kt +224 -0
  15. package/ios/LKAudioProcessingAdapter.h +26 -0
  16. package/ios/LKAudioProcessingAdapter.m +117 -0
  17. package/ios/LKAudioProcessingManager.h +34 -0
  18. package/ios/LKAudioProcessingManager.m +63 -0
  19. package/ios/LivekitReactNative-Bridging-Header.h +2 -0
  20. package/ios/LivekitReactNative.h +9 -4
  21. package/ios/LivekitReactNative.m +83 -5
  22. package/ios/Logging.swift +4 -0
  23. package/ios/audio/AVAudioPCMBuffer.swift +136 -0
  24. package/ios/audio/AudioProcessing.swift +163 -0
  25. package/ios/audio/AudioRendererManager.swift +72 -0
  26. package/ios/audio/FFTProcessor.swift +147 -0
  27. package/ios/audio/MultibandVolumeAudioRenderer.swift +65 -0
  28. package/ios/audio/RingBuffer.swift +51 -0
  29. package/ios/audio/VolumeAudioRenderer.swift +48 -0
  30. package/lib/commonjs/LKNativeModule.js +18 -0
  31. package/lib/commonjs/LKNativeModule.js.map +1 -0
  32. package/lib/commonjs/components/BarVisualizer.js +192 -0
  33. package/lib/commonjs/components/BarVisualizer.js.map +1 -0
  34. package/lib/commonjs/events/EventEmitter.js +45 -0
  35. package/lib/commonjs/events/EventEmitter.js.map +1 -0
  36. package/lib/commonjs/hooks/useMultibandTrackVolume.js +64 -0
  37. package/lib/commonjs/hooks/useMultibandTrackVolume.js.map +1 -0
  38. package/lib/commonjs/hooks/useTrackVolume.js +45 -0
  39. package/lib/commonjs/hooks/useTrackVolume.js.map +1 -0
  40. package/lib/commonjs/hooks.js +24 -0
  41. package/lib/commonjs/hooks.js.map +1 -1
  42. package/lib/commonjs/index.js +14 -0
  43. package/lib/commonjs/index.js.map +1 -1
  44. package/lib/module/LKNativeModule.js +12 -0
  45. package/lib/module/LKNativeModule.js.map +1 -0
  46. package/lib/module/components/BarVisualizer.js +182 -0
  47. package/lib/module/components/BarVisualizer.js.map +1 -0
  48. package/lib/module/events/EventEmitter.js +36 -0
  49. package/lib/module/events/EventEmitter.js.map +1 -0
  50. package/lib/module/hooks/useMultibandTrackVolume.js +58 -0
  51. package/lib/module/hooks/useMultibandTrackVolume.js.map +1 -0
  52. package/lib/module/hooks/useTrackVolume.js +39 -0
  53. package/lib/module/hooks/useTrackVolume.js.map +1 -0
  54. package/lib/module/hooks.js +2 -0
  55. package/lib/module/hooks.js.map +1 -1
  56. package/lib/module/index.js +3 -0
  57. package/lib/module/index.js.map +1 -1
  58. package/lib/typescript/lib/commonjs/LKNativeModule.d.ts +3 -0
  59. package/lib/typescript/lib/commonjs/components/BarVisualizer.d.ts +32 -0
  60. package/lib/typescript/lib/commonjs/events/EventEmitter.d.ts +4 -0
  61. package/lib/typescript/lib/commonjs/hooks/useMultibandTrackVolume.d.ts +8 -0
  62. package/lib/typescript/lib/commonjs/hooks/useTrackVolume.d.ts +8 -0
  63. package/lib/typescript/lib/module/LKNativeModule.d.ts +2 -0
  64. package/lib/typescript/lib/module/components/BarVisualizer.d.ts +10 -0
  65. package/lib/typescript/lib/module/events/EventEmitter.d.ts +3 -0
  66. package/lib/typescript/lib/module/hooks/useMultibandTrackVolume.d.ts +7 -0
  67. package/lib/typescript/lib/module/hooks/useTrackVolume.d.ts +7 -0
  68. package/lib/typescript/lib/module/hooks.d.ts +2 -0
  69. package/lib/typescript/lib/module/index.d.ts +1 -0
  70. package/lib/typescript/src/LKNativeModule.d.ts +2 -0
  71. package/lib/typescript/src/components/BarVisualizer.d.ts +49 -0
  72. package/lib/typescript/src/events/EventEmitter.d.ts +6 -0
  73. package/lib/typescript/src/hooks/useMultibandTrackVolume.d.ts +31 -0
  74. package/lib/typescript/src/hooks/useTrackVolume.d.ts +9 -0
  75. package/lib/typescript/src/hooks.d.ts +2 -0
  76. package/lib/typescript/src/index.d.ts +1 -0
  77. package/livekit-react-native.podspec +1 -1
  78. package/package.json +7 -6
  79. package/src/LKNativeModule.ts +19 -0
  80. package/src/components/BarVisualizer.tsx +252 -0
  81. package/src/events/EventEmitter.ts +51 -0
  82. package/src/hooks/useMultibandTrackVolume.ts +97 -0
  83. package/src/hooks/useTrackVolume.ts +62 -0
  84. package/src/hooks.ts +2 -0
  85. package/src/index.tsx +3 -0
@@ -0,0 +1,181 @@
1
+ package com.livekit.reactnative.audio.processing
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactContext
5
+ import com.facebook.react.modules.core.DeviceEventManagerModule
6
+ import com.livekit.reactnative.audio.events.Events
7
+ import com.livekit.reactnative.audio.processing.fft.FFTAudioAnalyzer
8
+ import kotlinx.coroutines.CoroutineScope
9
+ import kotlinx.coroutines.Dispatchers
10
+ import kotlinx.coroutines.SupervisorJob
11
+ import kotlinx.coroutines.cancel
12
+ import kotlinx.coroutines.delay
13
+ import kotlinx.coroutines.flow.Flow
14
+ import kotlinx.coroutines.flow.conflate
15
+ import kotlinx.coroutines.flow.transform
16
+ import kotlinx.coroutines.launch
17
+ import org.webrtc.AudioTrackSink
18
+ import java.nio.ByteBuffer
19
+ import kotlin.math.pow
20
+ import kotlin.math.round
21
+ import kotlin.math.roundToInt
22
+ import kotlin.math.sqrt
23
+ import kotlin.time.Duration
24
+
25
+ class MultibandVolumeProcessor(
26
+ minFrequency: Float = 1000f,
27
+ maxFrequency: Float = 8000f,
28
+ barCount: Int,
29
+ interval: Duration,
30
+ private val reactContext: ReactContext,
31
+ ) : BaseMultibandVolumeProcessor(minFrequency, maxFrequency, barCount, interval) {
32
+
33
+ var reactTag: String? = null
34
+ override fun onMagnitudesCollected(magnitudes: FloatArray) {
35
+ val reactTag = this.reactTag ?: return
36
+ val event = Arguments.createMap().apply {
37
+ putArray("magnitudes", Arguments.fromArray(magnitudes))
38
+ putString("id", reactTag)
39
+ }
40
+ reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
41
+ .emit(Events.LK_MULTIBAND_PROCESSED.name, event)
42
+ }
43
+ }
44
+
45
+ abstract class BaseMultibandVolumeProcessor(
46
+ val minFrequency: Float = 1000f,
47
+ val maxFrequency: Float = 8000f,
48
+ val barCount: Int,
49
+ val interval: Duration,
50
+ ) : AudioTrackSink {
51
+
52
+ private val audioProcessor = FFTAudioAnalyzer()
53
+ private var coroutineScope: CoroutineScope? = null
54
+
55
+ abstract fun onMagnitudesCollected(magnitudes: FloatArray)
56
+
57
+ fun start() {
58
+ coroutineScope?.cancel()
59
+ val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
60
+ coroutineScope = scope
61
+ scope.launch {
62
+ val averages = FloatArray(barCount)
63
+ audioProcessor.fftFlow.throttleLatest(interval).collect { fft ->
64
+ val loPass: Int
65
+ val hiPass: Int
66
+ val audioFormat = audioProcessor.configuredInputFormat
67
+
68
+ if (audioFormat != null) {
69
+ loPass = (minFrequency * fft.size / (audioFormat.sampleRate / 2)).roundToInt().coerceIn(fft.indices)
70
+ hiPass = (maxFrequency * fft.size / (audioFormat.sampleRate / 2)).roundToInt().coerceIn(fft.indices)
71
+ } else {
72
+ loPass = 0
73
+ hiPass = fft.size
74
+ }
75
+
76
+ val sliced = fft.slice(loPass until hiPass)
77
+ val magnitudes = calculateAmplitudeBarsFromFFT(sliced, averages, barCount)
78
+
79
+ onMagnitudesCollected(magnitudes)
80
+ }
81
+ }
82
+ }
83
+
84
+ fun stop() {
85
+ coroutineScope?.cancel()
86
+ coroutineScope = null
87
+ }
88
+
89
+ fun release() {
90
+ stop()
91
+ audioProcessor.release()
92
+ }
93
+
94
+ override fun onData(
95
+ audioData: ByteBuffer,
96
+ bitsPerSample: Int,
97
+ sampleRate: Int,
98
+ numberOfChannels: Int,
99
+ numberOfFrames: Int,
100
+ absoluteCaptureTimestampMs: Long
101
+ ) {
102
+ val curAudioFormat = audioProcessor.configuredInputFormat
103
+ if (curAudioFormat == null ||
104
+ curAudioFormat.bitsPerSample != bitsPerSample ||
105
+ curAudioFormat.sampleRate != sampleRate ||
106
+ curAudioFormat.numberOfChannels != numberOfChannels
107
+ ) {
108
+ audioProcessor.configure(AudioFormat(bitsPerSample, sampleRate, numberOfChannels))
109
+ }
110
+
111
+ audioProcessor.queueInput(audioData)
112
+ }
113
+ }
114
+
115
+ fun <T> Flow<T>.throttleLatest(interval: Duration): Flow<T> = this
116
+ .conflate()
117
+ .transform {
118
+ emit(it)
119
+ delay(interval)
120
+ }
121
+
122
+
123
+ private const val MIN_CONST = 2f
124
+ private const val MAX_CONST = 25f
125
+
126
+ private fun calculateAmplitudeBarsFromFFT(
127
+ fft: List<Float>,
128
+ averages: FloatArray,
129
+ barCount: Int,
130
+ ): FloatArray {
131
+ val amplitudes = FloatArray(barCount)
132
+ if (fft.isEmpty()) {
133
+ return amplitudes
134
+ }
135
+
136
+ // We average out the values over 3 occurrences (plus the current one), so big jumps are smoothed out
137
+ // Iterate over the entire FFT result array.
138
+ for (barIndex in 0 until barCount) {
139
+ // Note: each FFT is a real and imaginary pair.
140
+ // Scale down by 2 and scale back up to ensure we get an even number.
141
+ val prevLimit = (round(fft.size.toFloat() / 2 * barIndex / barCount).toInt() * 2)
142
+ .coerceIn(0, fft.size - 1)
143
+ val nextLimit = (round(fft.size.toFloat() / 2 * (barIndex + 1) / barCount).toInt() * 2)
144
+ .coerceIn(0, fft.size - 1)
145
+
146
+ var accum = 0f
147
+ // Here we iterate within this single band
148
+ for (i in prevLimit until nextLimit step 2) {
149
+ // Convert real and imaginary part to get energy
150
+
151
+ val realSq = fft[i]
152
+ .toDouble()
153
+ .pow(2.0)
154
+ val imaginarySq = fft[i + 1]
155
+ .toDouble()
156
+ .pow(2.0)
157
+ val raw = sqrt(realSq + imaginarySq).toFloat()
158
+
159
+ accum += raw
160
+ }
161
+
162
+ // A window might be empty which would result in a 0 division
163
+ if ((nextLimit - prevLimit) != 0) {
164
+ accum /= (nextLimit - prevLimit)
165
+ } else {
166
+ accum = 0.0f
167
+ }
168
+
169
+ val smoothingFactor = 5
170
+ var avg = averages[barIndex]
171
+ avg += (accum - avg / smoothingFactor)
172
+ averages[barIndex] = avg
173
+
174
+ var amplitude = avg.coerceIn(MIN_CONST, MAX_CONST)
175
+ amplitude -= MIN_CONST
176
+ amplitude /= (MAX_CONST - MIN_CONST)
177
+ amplitudes[barIndex] = amplitude
178
+ }
179
+
180
+ return amplitudes
181
+ }
@@ -0,0 +1,67 @@
1
+ package com.livekit.reactnative.audio.processing
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactContext
5
+ import com.facebook.react.modules.core.DeviceEventManagerModule
6
+ import com.livekit.reactnative.audio.events.Events
7
+ import org.webrtc.AudioTrackSink
8
+ import java.nio.ByteBuffer
9
+ import kotlin.math.round
10
+ import kotlin.math.sqrt
11
+
12
+ class VolumeProcessor(private val reactContext: ReactContext) : BaseVolumeProcessor() {
13
+ var reactTag: String? = null
14
+
15
+ override fun onVolumeCalculated(volume: Double) {
16
+ val reactTag = this.reactTag ?: return
17
+ val event = Arguments.createMap().apply {
18
+ putDouble("volume", volume)
19
+ putString("id", reactTag)
20
+ }
21
+ reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
22
+ .emit(Events.LK_VOLUME_PROCESSED.name, event)
23
+ }
24
+ }
25
+
26
+ abstract class BaseVolumeProcessor : AudioTrackSink {
27
+ abstract fun onVolumeCalculated(volume: Double)
28
+
29
+ override fun onData(
30
+ audioData: ByteBuffer,
31
+ bitsPerSample: Int,
32
+ sampleRate: Int,
33
+ numberOfChannels: Int,
34
+ numberOfFrames: Int,
35
+ absoluteCaptureTimestampMs: Long
36
+ ) {
37
+ audioData.mark()
38
+ audioData.position(0)
39
+ var average = 0L
40
+ val bytesPerSample = bitsPerSample / 8
41
+
42
+ // RMS average calculation
43
+ for (i in 0 until numberOfFrames) {
44
+ val value = when (bytesPerSample) {
45
+ 1 -> audioData.get().toLong()
46
+ 2 -> audioData.getShort().toLong()
47
+ 4 -> audioData.getInt().toLong()
48
+ else -> throw IllegalArgumentException()
49
+ }
50
+
51
+ average += value * value
52
+ }
53
+
54
+ average /= numberOfFrames
55
+
56
+ val volume = round(sqrt(average.toDouble()))
57
+ val volumeNormalized = when (bytesPerSample) {
58
+ 1 -> volume / Byte.MAX_VALUE
59
+ 2 -> volume / Short.MAX_VALUE
60
+ 4 -> volume / Int.MAX_VALUE
61
+ else -> throw IllegalArgumentException()
62
+ }
63
+ audioData.reset()
64
+
65
+ onVolumeCalculated(volumeNormalized)
66
+ }
67
+ }
@@ -0,0 +1,224 @@
1
+ /*
2
+ * Copyright 2024 LiveKit, Inc.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ * Originally adapted from: https://github.com/dzolnai/ExoVisualizer
17
+ *
18
+ * MIT License
19
+ *
20
+ * Copyright (c) 2019 Dániel Zolnai
21
+ *
22
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ * of this software and associated documentation files (the "Software"), to deal
24
+ * in the Software without restriction, including without limitation the rights
25
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ * copies of the Software, and to permit persons to whom the Software is
27
+ * furnished to do so, subject to the following conditions:
28
+ *
29
+ * The above copyright notice and this permission notice shall be included in all
30
+ * copies or substantial portions of the Software.
31
+ *
32
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ * SOFTWARE.
39
+ */
40
+
41
+ package com.livekit.reactnative.audio.processing.fft;
42
+
43
+ import android.media.AudioTrack
44
+ import com.livekit.reactnative.audio.processing.AudioFormat
45
+ import com.paramsen.noise.Noise
46
+ import kotlinx.coroutines.channels.BufferOverflow
47
+ import kotlinx.coroutines.flow.Flow
48
+ import kotlinx.coroutines.flow.MutableSharedFlow
49
+ import java.nio.ByteBuffer
50
+ import java.nio.ByteOrder
51
+ import java.util.concurrent.TimeUnit
52
+ import kotlin.math.max
53
+
54
+ /**
55
+ * A Fast Fourier Transform analyzer for audio bytes.
56
+ *
57
+ * Use [queueInput] to add audio bytes, and collect on [fftFlow]
58
+ * to receive the analyzed frequencies.
59
+ */
60
+ class FFTAudioAnalyzer {
61
+
62
+ companion object {
63
+ const val SAMPLE_SIZE = 1024
64
+ private val EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder())
65
+
66
+ // Extra size next in addition to the AudioTrack buffer size
67
+ private const val BUFFER_EXTRA_SIZE = SAMPLE_SIZE * 8
68
+
69
+ // Size of short in bytes.
70
+ private const val SHORT_SIZE = 2
71
+ }
72
+
73
+ val isActive: Boolean
74
+ get() = noise != null
75
+
76
+ private var noise: Noise? = null
77
+ private lateinit var inputAudioFormat: AudioFormat
78
+ val configuredInputFormat: AudioFormat?
79
+ get() {
80
+ if (::inputAudioFormat.isInitialized) {
81
+ return inputAudioFormat
82
+ } else {
83
+ return null
84
+ }
85
+ }
86
+
87
+ private var audioTrackBufferSize = 0
88
+
89
+ private var fftBuffer: ByteBuffer = EMPTY_BUFFER
90
+ private lateinit var srcBuffer: ByteBuffer
91
+ private var srcBufferPosition = 0
92
+ private val tempShortArray = ShortArray(SAMPLE_SIZE)
93
+ private val src = FloatArray(SAMPLE_SIZE)
94
+
95
+ private val mutableFftFlow = MutableSharedFlow<FloatArray>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
96
+
97
+ /**
98
+ * A flow of frequencies for the audio bytes given through [queueInput].
99
+ */
100
+ val fftFlow: Flow<FloatArray> = mutableFftFlow
101
+
102
+ fun configure(inputAudioFormat: AudioFormat) {
103
+ this.inputAudioFormat = inputAudioFormat
104
+
105
+ noise = Noise.real(SAMPLE_SIZE)
106
+
107
+ audioTrackBufferSize = getDefaultBufferSizeInBytes(inputAudioFormat)
108
+
109
+ srcBuffer = ByteBuffer.allocate(audioTrackBufferSize + BUFFER_EXTRA_SIZE)
110
+ }
111
+
112
+ fun release() {
113
+ noise?.close()
114
+ noise = null
115
+ }
116
+
117
+ /**
118
+ * Add audio bytes to be processed.
119
+ */
120
+ fun queueInput(inputBuffer: ByteBuffer) {
121
+ if (!isActive) {
122
+ return
123
+ }
124
+ var position = inputBuffer.position()
125
+ val limit = inputBuffer.limit()
126
+ val frameCount = (limit - position) / (SHORT_SIZE * inputAudioFormat.numberOfChannels)
127
+ val singleChannelOutputSize = frameCount * SHORT_SIZE
128
+
129
+ // Setup buffer
130
+ if (fftBuffer.capacity() < singleChannelOutputSize) {
131
+ fftBuffer =
132
+ ByteBuffer.allocateDirect(singleChannelOutputSize).order(ByteOrder.nativeOrder())
133
+ } else {
134
+ fftBuffer.clear()
135
+ }
136
+
137
+ // Process inputBuffer
138
+ while (position < limit) {
139
+ var summedUp = 0
140
+ for (channelIndex in 0 until inputAudioFormat.numberOfChannels) {
141
+ val current = inputBuffer.getShort(position + 2 * channelIndex)
142
+ summedUp += current
143
+ }
144
+ // For the FFT, we use an average of all the channels and put into a single short.
145
+ fftBuffer.putShort((summedUp / inputAudioFormat.numberOfChannels).toShort())
146
+ position += inputAudioFormat.numberOfChannels * 2
147
+ }
148
+
149
+ // Reset input buffer to original position.
150
+ inputBuffer.position(position)
151
+
152
+ processFFT(this.fftBuffer)
153
+ }
154
+
155
+ private fun processFFT(buffer: ByteBuffer) {
156
+ if (noise == null) {
157
+ return
158
+ }
159
+ srcBuffer.put(buffer.array())
160
+ srcBufferPosition += buffer.array().size
161
+ // Since this is PCM 16 bit, each sample will be 2 bytes.
162
+ // So to get the sample size in the end, we need to take twice as many bytes off the buffer
163
+ val bytesToProcess = SAMPLE_SIZE * 2
164
+ while (srcBufferPosition > bytesToProcess) {
165
+ // Move to start of
166
+ srcBuffer.position(0)
167
+
168
+ srcBuffer.asShortBuffer().get(tempShortArray, 0, SAMPLE_SIZE)
169
+ tempShortArray.forEachIndexed { index, sample ->
170
+ // Normalize to value between -1.0 and 1.0
171
+ src[index] = sample.toFloat() / Short.MAX_VALUE
172
+ }
173
+
174
+ srcBuffer.position(bytesToProcess)
175
+ srcBuffer.compact()
176
+ srcBufferPosition -= bytesToProcess
177
+ srcBuffer.position(srcBufferPosition)
178
+ val dst = FloatArray(SAMPLE_SIZE + 2)
179
+ val fft = noise?.fft(src, dst)!!
180
+
181
+ mutableFftFlow.tryEmit(fft)
182
+ }
183
+ }
184
+
185
+ private fun durationUsToFrames(sampleRate: Int, durationUs: Long): Long {
186
+ return durationUs * sampleRate / TimeUnit.MICROSECONDS.convert(1, TimeUnit.SECONDS)
187
+ }
188
+
189
+ private fun getPcmFrameSize(channelCount: Int): Int {
190
+ // assumes PCM_16BIT
191
+ return channelCount * 2
192
+ }
193
+
194
+ private fun getAudioTrackChannelConfig(channelCount: Int): Int {
195
+ return when (channelCount) {
196
+ 1 -> android.media.AudioFormat.CHANNEL_OUT_MONO
197
+ 2 -> android.media.AudioFormat.CHANNEL_OUT_STEREO
198
+ // ignore other channel counts that aren't used in LiveKit
199
+ else -> android.media.AudioFormat.CHANNEL_INVALID
200
+ }
201
+ }
202
+
203
+ private fun getDefaultBufferSizeInBytes(audioFormat: AudioFormat): Int {
204
+ val outputPcmFrameSize = getPcmFrameSize(audioFormat.numberOfChannels)
205
+ val minBufferSize =
206
+ AudioTrack.getMinBufferSize(
207
+ audioFormat.sampleRate,
208
+ getAudioTrackChannelConfig(audioFormat.numberOfChannels),
209
+ android.media.AudioFormat.ENCODING_PCM_16BIT
210
+ )
211
+
212
+ check(minBufferSize != AudioTrack.ERROR_BAD_VALUE)
213
+ val multipliedBufferSize = minBufferSize * 4
214
+ val minAppBufferSize =
215
+ durationUsToFrames(audioFormat.sampleRate, 30 * 1000).toInt() * outputPcmFrameSize
216
+ val maxAppBufferSize = max(
217
+ minBufferSize.toLong(),
218
+ durationUsToFrames(audioFormat.sampleRate, 500 * 1000) * outputPcmFrameSize
219
+ ).toInt()
220
+ val bufferSizeInFrames =
221
+ multipliedBufferSize.coerceIn(minAppBufferSize, maxAppBufferSize) / outputPcmFrameSize
222
+ return bufferSizeInFrames * outputPcmFrameSize
223
+ }
224
+ }
@@ -0,0 +1,26 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <WebRTC/WebRTC.h>
3
+
4
+ @protocol LKExternalAudioProcessingDelegate
5
+
6
+ - (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(size_t)channels;
7
+
8
+ - (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) * _Nonnull)audioBuffer;
9
+
10
+ - (void)audioProcessingRelease;
11
+
12
+ @end
13
+
14
+ @interface LKAudioProcessingAdapter : NSObject <RTCAudioCustomProcessingDelegate>
15
+
16
+ - (nonnull instancetype)init;
17
+
18
+ - (void)addProcessing:(id<LKExternalAudioProcessingDelegate> _Nonnull)processor;
19
+
20
+ - (void)removeProcessing:(id<LKExternalAudioProcessingDelegate> _Nonnull)processor;
21
+
22
+ - (void)addAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
23
+
24
+ - (void)removeAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
25
+
26
+ @end
@@ -0,0 +1,117 @@
1
+ #import "LKAudioProcessingAdapter.h"
2
+ #import <WebRTC/RTCAudioRenderer.h>
3
+ #import <os/lock.h>
4
+
5
+ @implementation LKAudioProcessingAdapter {
6
+ NSMutableArray<id<RTCAudioRenderer>>* _renderers;
7
+ NSMutableArray<id<LKExternalAudioProcessingDelegate>>* _processors;
8
+ os_unfair_lock _lock;
9
+ BOOL _isAudioProcessingInitialized;
10
+ size_t _sampleRateHz;
11
+ size_t _channels;
12
+ }
13
+
14
+ - (instancetype)init {
15
+ self = [super init];
16
+ if (self) {
17
+ _isAudioProcessingInitialized = NO;
18
+ _lock = OS_UNFAIR_LOCK_INIT;
19
+ _renderers = [[NSMutableArray<id<RTCAudioRenderer>> alloc] init];
20
+ _processors = [[NSMutableArray<id<LKExternalAudioProcessingDelegate>> alloc] init];
21
+ }
22
+ return self;
23
+ }
24
+
25
+ - (void)addProcessing:(id<LKExternalAudioProcessingDelegate> _Nonnull)processor {
26
+ os_unfair_lock_lock(&_lock);
27
+ [_processors addObject:processor];
28
+ if (_isAudioProcessingInitialized) {
29
+ [processor audioProcessingInitializeWithSampleRate:_sampleRateHz channels:_channels];
30
+ }
31
+ os_unfair_lock_unlock(&_lock);
32
+ }
33
+
34
+ - (void)removeProcessing:(id<LKExternalAudioProcessingDelegate> _Nonnull)processor {
35
+ os_unfair_lock_lock(&_lock);
36
+ _processors = [[_processors
37
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject,
38
+ NSDictionary* bindings) {
39
+ return evaluatedObject != processor;
40
+ }]] mutableCopy];
41
+ os_unfair_lock_unlock(&_lock);
42
+ }
43
+
44
+ - (void)addAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer {
45
+ os_unfair_lock_lock(&_lock);
46
+ [_renderers addObject:renderer];
47
+ os_unfair_lock_unlock(&_lock);
48
+ }
49
+
50
+ - (void)removeAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer {
51
+ os_unfair_lock_lock(&_lock);
52
+ _renderers = [[_renderers
53
+ filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject,
54
+ NSDictionary* bindings) {
55
+ return evaluatedObject != renderer;
56
+ }]] mutableCopy];
57
+ os_unfair_lock_unlock(&_lock);
58
+ }
59
+
60
+ - (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(size_t)channels {
61
+ os_unfair_lock_lock(&_lock);
62
+ _isAudioProcessingInitialized = YES;
63
+ _sampleRateHz = sampleRateHz;
64
+ _channels = channels;
65
+
66
+ for (id<LKExternalAudioProcessingDelegate> processor in _processors) {
67
+ [processor audioProcessingInitializeWithSampleRate:sampleRateHz channels:channels];
68
+ }
69
+ os_unfair_lock_unlock(&_lock);
70
+ }
71
+
72
+ - (AVAudioPCMBuffer*)toPCMBuffer:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer {
73
+ AVAudioFormat* format =
74
+ [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16
75
+ sampleRate:audioBuffer.frames * 100.0
76
+ channels:(AVAudioChannelCount)audioBuffer.channels
77
+ interleaved:NO];
78
+ AVAudioPCMBuffer* pcmBuffer =
79
+ [[AVAudioPCMBuffer alloc] initWithPCMFormat:format
80
+ frameCapacity:(AVAudioFrameCount)audioBuffer.frames];
81
+ if (!pcmBuffer) {
82
+ NSLog(@"Failed to create AVAudioPCMBuffer");
83
+ return nil;
84
+ }
85
+ pcmBuffer.frameLength = (AVAudioFrameCount)audioBuffer.frames;
86
+ for (int i = 0; i < audioBuffer.channels; i++) {
87
+ float* sourceBuffer = [audioBuffer rawBufferForChannel:i];
88
+ int16_t* targetBuffer = (int16_t*)pcmBuffer.int16ChannelData[i];
89
+ for (int frame = 0; frame < audioBuffer.frames; frame++) {
90
+ targetBuffer[frame] = sourceBuffer[frame];
91
+ }
92
+ }
93
+ return pcmBuffer;
94
+ }
95
+
96
+ - (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer {
97
+ os_unfair_lock_lock(&_lock);
98
+ for (id<LKExternalAudioProcessingDelegate> processor in _processors) {
99
+ [processor audioProcessingProcess:audioBuffer];
100
+ }
101
+
102
+ for (id<RTCAudioRenderer> renderer in _renderers) {
103
+ [renderer renderPCMBuffer:[self toPCMBuffer:audioBuffer]];
104
+ }
105
+ os_unfair_lock_unlock(&_lock);
106
+ }
107
+
108
+ - (void)audioProcessingRelease {
109
+ os_unfair_lock_lock(&_lock);
110
+ for (id<LKExternalAudioProcessingDelegate> processor in _processors) {
111
+ [processor audioProcessingRelease];
112
+ }
113
+ _isAudioProcessingInitialized = NO;
114
+ os_unfair_lock_unlock(&_lock);
115
+ }
116
+
117
+ @end
@@ -0,0 +1,34 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <WebRTC/WebRTC.h>
3
+ #import "LKAudioProcessingAdapter.h"
4
+
5
+ @interface LKAudioProcessingManager : NSObject
6
+
7
+ @property(nonatomic, strong) RTCDefaultAudioProcessingModule* _Nonnull audioProcessingModule;
8
+
9
+ @property(nonatomic, strong) LKAudioProcessingAdapter* _Nonnull capturePostProcessingAdapter;
10
+
11
+ @property(nonatomic, strong) LKAudioProcessingAdapter* _Nonnull renderPreProcessingAdapter;
12
+
13
+ + (_Nonnull instancetype)sharedInstance;
14
+
15
+
16
+ - (void)addLocalAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
17
+
18
+ - (void)removeLocalAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
19
+
20
+ - (void)addRemoteAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
21
+
22
+ - (void)removeRemoteAudioRenderer:(nonnull id<RTCAudioRenderer>)renderer;
23
+
24
+ - (void)addCapturePostProcessor:(nonnull id<LKExternalAudioProcessingDelegate>)processor;
25
+
26
+ - (void)removeCapturePostProcessor:(nonnull id<LKExternalAudioProcessingDelegate>)processor;
27
+
28
+ - (void)addRenderPreProcessor:(nonnull id<LKExternalAudioProcessingDelegate>)renderer;
29
+
30
+ - (void)removeRenderPreProcessor:(nonnull id<LKExternalAudioProcessingDelegate>)renderer;
31
+
32
+ - (void)clearProcessors;
33
+
34
+ @end