@edkimmel/expo-audio-stream 0.2.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 (75) hide show
  1. package/.eslintrc.js +5 -0
  2. package/.yarnrc.yml +8 -0
  3. package/NATIVE_EVENTS.md +270 -0
  4. package/README.md +289 -0
  5. package/android/build.gradle +92 -0
  6. package/android/src/main/AndroidManifest.xml +4 -0
  7. package/android/src/main/java/expo/modules/audiostream/AudioDataEncoder.kt +178 -0
  8. package/android/src/main/java/expo/modules/audiostream/AudioEffectsManager.kt +107 -0
  9. package/android/src/main/java/expo/modules/audiostream/AudioPlaybackManager.kt +651 -0
  10. package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +509 -0
  11. package/android/src/main/java/expo/modules/audiostream/Constants.kt +21 -0
  12. package/android/src/main/java/expo/modules/audiostream/EventSender.kt +7 -0
  13. package/android/src/main/java/expo/modules/audiostream/ExpoAudioStreamView.kt +7 -0
  14. package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +280 -0
  15. package/android/src/main/java/expo/modules/audiostream/PermissionUtils.kt +16 -0
  16. package/android/src/main/java/expo/modules/audiostream/RecordingConfig.kt +60 -0
  17. package/android/src/main/java/expo/modules/audiostream/SoundConfig.kt +46 -0
  18. package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +685 -0
  19. package/android/src/main/java/expo/modules/audiostream/pipeline/JitterBuffer.kt +227 -0
  20. package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +315 -0
  21. package/app.plugin.js +1 -0
  22. package/build/ExpoPlayAudioStreamModule.d.ts +3 -0
  23. package/build/ExpoPlayAudioStreamModule.d.ts.map +1 -0
  24. package/build/ExpoPlayAudioStreamModule.js +5 -0
  25. package/build/ExpoPlayAudioStreamModule.js.map +1 -0
  26. package/build/events.d.ts +36 -0
  27. package/build/events.d.ts.map +1 -0
  28. package/build/events.js +25 -0
  29. package/build/events.js.map +1 -0
  30. package/build/index.d.ts +125 -0
  31. package/build/index.d.ts.map +1 -0
  32. package/build/index.js +222 -0
  33. package/build/index.js.map +1 -0
  34. package/build/pipeline/index.d.ts +81 -0
  35. package/build/pipeline/index.d.ts.map +1 -0
  36. package/build/pipeline/index.js +140 -0
  37. package/build/pipeline/index.js.map +1 -0
  38. package/build/pipeline/types.d.ts +132 -0
  39. package/build/pipeline/types.d.ts.map +1 -0
  40. package/build/pipeline/types.js +5 -0
  41. package/build/pipeline/types.js.map +1 -0
  42. package/build/types.d.ts +221 -0
  43. package/build/types.d.ts.map +1 -0
  44. package/build/types.js +10 -0
  45. package/build/types.js.map +1 -0
  46. package/expo-module.config.json +9 -0
  47. package/ios/AudioPipeline.swift +562 -0
  48. package/ios/AudioUtils.swift +356 -0
  49. package/ios/ExpoPlayAudioStream.podspec +27 -0
  50. package/ios/ExpoPlayAudioStreamModule.swift +436 -0
  51. package/ios/ExpoPlayAudioStreamView.swift +7 -0
  52. package/ios/JitterBuffer.swift +208 -0
  53. package/ios/Logger.swift +7 -0
  54. package/ios/Microphone.swift +221 -0
  55. package/ios/MicrophoneDataDelegate.swift +4 -0
  56. package/ios/PipelineIntegration.swift +214 -0
  57. package/ios/RecordingResult.swift +10 -0
  58. package/ios/RecordingSettings.swift +11 -0
  59. package/ios/SharedAudioEngine.swift +484 -0
  60. package/ios/SoundConfig.swift +45 -0
  61. package/ios/SoundPlayer.swift +408 -0
  62. package/ios/SoundPlayerDelegate.swift +7 -0
  63. package/package.json +49 -0
  64. package/plugin/build/index.d.ts +5 -0
  65. package/plugin/build/index.js +28 -0
  66. package/plugin/src/index.ts +53 -0
  67. package/plugin/tsconfig.json +9 -0
  68. package/plugin/tsconfig.tsbuildinfo +1 -0
  69. package/src/ExpoPlayAudioStreamModule.ts +5 -0
  70. package/src/events.ts +66 -0
  71. package/src/index.ts +359 -0
  72. package/src/pipeline/index.ts +216 -0
  73. package/src/pipeline/types.ts +169 -0
  74. package/src/types.ts +270 -0
  75. package/tsconfig.json +9 -0
@@ -0,0 +1,178 @@
1
+ package expo.modules.audiostream
2
+
3
+ import android.media.AudioFormat
4
+ import android.media.AudioRecord
5
+ import android.media.MediaRecorder
6
+ import android.os.Build
7
+ import android.util.Base64
8
+ import android.util.Log
9
+ import java.nio.ByteBuffer
10
+ import java.nio.ByteOrder
11
+ import kotlin.math.log10
12
+ import kotlin.math.sqrt
13
+
14
+ /**
15
+ * Data class to hold audio format configuration
16
+ */
17
+ data class AudioFormatConfig(
18
+ val audioFormat: Int,
19
+ val fileExtension: String,
20
+ val mimeType: String,
21
+ val error: String? = null
22
+ )
23
+
24
+ class AudioDataEncoder {
25
+ public fun encodeToBase64(rawData: ByteArray): String {
26
+ return Base64.encodeToString(rawData, Base64.NO_WRAP)
27
+ }
28
+
29
+ /**
30
+ * Calculates the power level (dBFS) of the audio data.
31
+ * Assumes PCM 16-bit encoding.
32
+ *
33
+ * @param audioData The byte array containing audio data.
34
+ * @param bytesRead The number of bytes read into the audioData buffer.
35
+ * @return The power level in dBFS (typically -160.0 to 0.0). Returns -160.0 for silence.
36
+ */
37
+ public fun calculatePowerLevel(audioData: ByteArray, bytesRead: Int): Float {
38
+ if (bytesRead <= 0 || audioData.isEmpty()) {
39
+ return -160.0f // Represent silence or no data
40
+ }
41
+
42
+ // Assuming PCM 16-bit, so 2 bytes per sample
43
+ val shorts = ShortArray(bytesRead / 2)
44
+ // Ensure we only process the valid portion of the audioData buffer
45
+ val byteBuffer = ByteBuffer.wrap(audioData, 0, bytesRead).order(ByteOrder.LITTLE_ENDIAN)
46
+ byteBuffer.asShortBuffer().get(shorts)
47
+
48
+ if (shorts.isEmpty()) {
49
+ return -160.0f // Represent silence
50
+ }
51
+
52
+ var sumOfSquares: Double = 0.0
53
+ for (sample in shorts) {
54
+ val normalizedSample = sample / 32767.0 // Normalize sample to -1.0 to 1.0
55
+ sumOfSquares += normalizedSample * normalizedSample
56
+ }
57
+
58
+ val rms = sqrt(sumOfSquares / shorts.size)
59
+
60
+ // Handle RMS of 0 (silence) to avoid log10(0)
61
+ if (rms < 1e-9) { // Use a small epsilon to check for effective silence
62
+ return -160.0f
63
+ }
64
+
65
+ // Convert RMS to dBFS (dB relative to full scale)
66
+ val dbfs = 20.0 * log10(rms)
67
+
68
+ // Clamp the value to a minimum of -160 dBFS, maximum of 0 dBFS
69
+ return dbfs.toFloat().coerceIn(-160.0f, 0.0f)
70
+ }
71
+
72
+ /**
73
+ * Gets audio format configuration based on the encoding string
74
+ *
75
+ * @param encoding The encoding string (e.g., "pcm_16bit", "opus", etc.)
76
+ * @return AudioFormatConfig containing audioFormat, fileExtension, and mimeType
77
+ */
78
+ fun getAudioFormatConfig(encoding: String): AudioFormatConfig {
79
+ return when (encoding) {
80
+ "pcm_8bit" -> AudioFormatConfig(
81
+ audioFormat = AudioFormat.ENCODING_PCM_8BIT,
82
+ fileExtension = "wav",
83
+ mimeType = "audio/wav"
84
+ )
85
+ "pcm_16bit" -> AudioFormatConfig(
86
+ audioFormat = AudioFormat.ENCODING_PCM_16BIT,
87
+ fileExtension = "wav",
88
+ mimeType = "audio/wav"
89
+ )
90
+ "pcm_32bit" -> AudioFormatConfig(
91
+ audioFormat = AudioFormat.ENCODING_PCM_FLOAT,
92
+ fileExtension = "wav",
93
+ mimeType = "audio/wav"
94
+ )
95
+ "opus" -> {
96
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
97
+ return AudioFormatConfig(
98
+ audioFormat = AudioFormat.ENCODING_DEFAULT,
99
+ fileExtension = "wav",
100
+ mimeType = "audio/wav",
101
+ error = "Opus encoding not supported on this Android version."
102
+ )
103
+ }
104
+ AudioFormatConfig(
105
+ audioFormat = AudioFormat.ENCODING_OPUS,
106
+ fileExtension = "opus",
107
+ mimeType = "audio/opus"
108
+ )
109
+ }
110
+ "aac_lc" -> AudioFormatConfig(
111
+ audioFormat = AudioFormat.ENCODING_AAC_LC,
112
+ fileExtension = "aac",
113
+ mimeType = "audio/aac"
114
+ )
115
+ else -> {
116
+ Log.d(Constants.TAG, "Unknown encoding: $encoding, defaulting to PCM_16BIT")
117
+ AudioFormatConfig(
118
+ audioFormat = AudioFormat.ENCODING_DEFAULT,
119
+ fileExtension = "wav",
120
+ mimeType = "audio/wav"
121
+ )
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Checks if a specific audio format configuration is supported by the device
128
+ *
129
+ * @param sampleRate The sample rate in Hz
130
+ * @param channels The number of channels (1 for mono, 2 for stereo)
131
+ * @param format The audio format constant from AudioFormat
132
+ * @param permissionUtils The PermissionUtils instance to check recording permissions
133
+ * @return True if the format is supported, false otherwise
134
+ * @throws SecurityException if recording permission is not granted
135
+ */
136
+ fun isAudioFormatSupported(
137
+ sampleRate: Int,
138
+ channels: Int,
139
+ format: Int,
140
+ permissionUtils: PermissionUtils
141
+ ): Boolean {
142
+ if (!permissionUtils.checkRecordingPermission()) {
143
+ throw SecurityException("Recording permission has not been granted")
144
+ }
145
+
146
+ val channelConfig = if (channels == 1) AudioFormat.CHANNEL_IN_MONO else AudioFormat.CHANNEL_IN_STEREO
147
+ val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, format)
148
+
149
+ if (bufferSize <= 0) {
150
+ return false
151
+ }
152
+
153
+ // Always use VOICE_COMMUNICATION for better echo cancellation
154
+ val audioSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION
155
+
156
+ val audioRecord = AudioRecord(
157
+ audioSource, // Using VOICE_COMMUNICATION source
158
+ sampleRate,
159
+ channelConfig,
160
+ format,
161
+ bufferSize
162
+ )
163
+
164
+ val isSupported = audioRecord.state == AudioRecord.STATE_INITIALIZED
165
+ if (isSupported) {
166
+ val testBuffer = ByteArray(bufferSize)
167
+ audioRecord.startRecording()
168
+ val testRead = audioRecord.read(testBuffer, 0, bufferSize)
169
+ audioRecord.stop()
170
+ if (testRead < 0) {
171
+ return false
172
+ }
173
+ }
174
+
175
+ audioRecord.release()
176
+ return isSupported
177
+ }
178
+ }
@@ -0,0 +1,107 @@
1
+ package expo.modules.audiostream
2
+
3
+ import android.media.AudioRecord
4
+ import android.media.audiofx.AcousticEchoCanceler
5
+ import android.media.audiofx.AutomaticGainControl
6
+ import android.media.audiofx.NoiseSuppressor
7
+ import android.util.Log
8
+
9
+ /**
10
+ * Manages audio effects for voice recording, including:
11
+ * - Acoustic Echo Cancellation (AEC)
12
+ * - Noise Suppression (NS)
13
+ * - Automatic Gain Control (AGC)
14
+ */
15
+ class AudioEffectsManager {
16
+ // Audio effects
17
+ private var acousticEchoCanceler: AcousticEchoCanceler? = null
18
+ private var noiseSuppressor: NoiseSuppressor? = null
19
+ private var automaticGainControl: AutomaticGainControl? = null
20
+
21
+ /**
22
+ * Sets up audio effects for the provided AudioRecord instance
23
+ * @param audioRecord The AudioRecord instance to apply effects to
24
+ */
25
+ fun setupAudioEffects(audioRecord: AudioRecord) {
26
+ val audioSessionId = audioRecord.audioSessionId
27
+
28
+ // Release any existing effects first
29
+ releaseAudioEffects()
30
+
31
+ try {
32
+ // Log availability of audio effects
33
+ Log.d(Constants.TAG, "AEC available: ${AcousticEchoCanceler.isAvailable()}")
34
+ Log.d(Constants.TAG, "NS available: ${NoiseSuppressor.isAvailable()}")
35
+ Log.d(Constants.TAG, "AGC available: ${AutomaticGainControl.isAvailable()}")
36
+
37
+ // Apply echo cancellation if available
38
+ if (AcousticEchoCanceler.isAvailable()) {
39
+ acousticEchoCanceler = AcousticEchoCanceler.create(audioSessionId)
40
+ acousticEchoCanceler?.enabled = true
41
+ Log.d(Constants.TAG, "Acoustic Echo Canceler enabled: ${acousticEchoCanceler?.enabled}")
42
+ }
43
+
44
+ // Apply noise suppression
45
+ enableNoiseSuppression(audioSessionId)
46
+
47
+ // Apply automatic gain control
48
+ enableAutomaticGainControl(audioSessionId)
49
+
50
+ } catch (e: Exception) {
51
+ Log.e(Constants.TAG, "Error setting up audio effects", e)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Enables Noise Suppression if available for the given audio session.
57
+ * @param audioSessionId The audio session ID to apply the effect to.
58
+ */
59
+ private fun enableNoiseSuppression(audioSessionId: Int) {
60
+ // Apply noise suppression if available
61
+ if (NoiseSuppressor.isAvailable()) {
62
+ noiseSuppressor = NoiseSuppressor.create(audioSessionId)
63
+ noiseSuppressor?.enabled = true
64
+ Log.d(Constants.TAG, "Noise Suppressor enabled: ${noiseSuppressor?.enabled}")
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Enables Automatic Gain Control if available for the given audio session.
70
+ * @param audioSessionId The audio session ID to apply the effect to.
71
+ */
72
+ private fun enableAutomaticGainControl(audioSessionId: Int) {
73
+ // Apply automatic gain control if available
74
+ if (AutomaticGainControl.isAvailable()) {
75
+ automaticGainControl = AutomaticGainControl.create(audioSessionId)
76
+ automaticGainControl?.enabled = true
77
+ Log.d(Constants.TAG, "Automatic Gain Control enabled: ${automaticGainControl?.enabled}")
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Releases all audio effects
83
+ */
84
+ fun releaseAudioEffects() {
85
+ try {
86
+ acousticEchoCanceler?.let {
87
+ if (it.enabled) it.enabled = false
88
+ it.release()
89
+ acousticEchoCanceler = null
90
+ }
91
+
92
+ noiseSuppressor?.let {
93
+ if (it.enabled) it.enabled = false
94
+ it.release()
95
+ noiseSuppressor = null
96
+ }
97
+
98
+ automaticGainControl?.let {
99
+ if (it.enabled) it.enabled = false
100
+ it.release()
101
+ automaticGainControl = null
102
+ }
103
+ } catch (e: Exception) {
104
+ Log.e(Constants.TAG, "Error releasing audio effects", e)
105
+ }
106
+ }
107
+ }