@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.
- package/.eslintrc.js +5 -0
- package/.yarnrc.yml +8 -0
- package/NATIVE_EVENTS.md +270 -0
- package/README.md +289 -0
- package/android/build.gradle +92 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/expo/modules/audiostream/AudioDataEncoder.kt +178 -0
- package/android/src/main/java/expo/modules/audiostream/AudioEffectsManager.kt +107 -0
- package/android/src/main/java/expo/modules/audiostream/AudioPlaybackManager.kt +651 -0
- package/android/src/main/java/expo/modules/audiostream/AudioRecorderManager.kt +509 -0
- package/android/src/main/java/expo/modules/audiostream/Constants.kt +21 -0
- package/android/src/main/java/expo/modules/audiostream/EventSender.kt +7 -0
- package/android/src/main/java/expo/modules/audiostream/ExpoAudioStreamView.kt +7 -0
- package/android/src/main/java/expo/modules/audiostream/ExpoPlayAudioStreamModule.kt +280 -0
- package/android/src/main/java/expo/modules/audiostream/PermissionUtils.kt +16 -0
- package/android/src/main/java/expo/modules/audiostream/RecordingConfig.kt +60 -0
- package/android/src/main/java/expo/modules/audiostream/SoundConfig.kt +46 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/AudioPipeline.kt +685 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/JitterBuffer.kt +227 -0
- package/android/src/main/java/expo/modules/audiostream/pipeline/PipelineIntegration.kt +315 -0
- package/app.plugin.js +1 -0
- package/build/ExpoPlayAudioStreamModule.d.ts +3 -0
- package/build/ExpoPlayAudioStreamModule.d.ts.map +1 -0
- package/build/ExpoPlayAudioStreamModule.js +5 -0
- package/build/ExpoPlayAudioStreamModule.js.map +1 -0
- package/build/events.d.ts +36 -0
- package/build/events.d.ts.map +1 -0
- package/build/events.js +25 -0
- package/build/events.js.map +1 -0
- package/build/index.d.ts +125 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +222 -0
- package/build/index.js.map +1 -0
- package/build/pipeline/index.d.ts +81 -0
- package/build/pipeline/index.d.ts.map +1 -0
- package/build/pipeline/index.js +140 -0
- package/build/pipeline/index.js.map +1 -0
- package/build/pipeline/types.d.ts +132 -0
- package/build/pipeline/types.d.ts.map +1 -0
- package/build/pipeline/types.js +5 -0
- package/build/pipeline/types.js.map +1 -0
- package/build/types.d.ts +221 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +10 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioPipeline.swift +562 -0
- package/ios/AudioUtils.swift +356 -0
- package/ios/ExpoPlayAudioStream.podspec +27 -0
- package/ios/ExpoPlayAudioStreamModule.swift +436 -0
- package/ios/ExpoPlayAudioStreamView.swift +7 -0
- package/ios/JitterBuffer.swift +208 -0
- package/ios/Logger.swift +7 -0
- package/ios/Microphone.swift +221 -0
- package/ios/MicrophoneDataDelegate.swift +4 -0
- package/ios/PipelineIntegration.swift +214 -0
- package/ios/RecordingResult.swift +10 -0
- package/ios/RecordingSettings.swift +11 -0
- package/ios/SharedAudioEngine.swift +484 -0
- package/ios/SoundConfig.swift +45 -0
- package/ios/SoundPlayer.swift +408 -0
- package/ios/SoundPlayerDelegate.swift +7 -0
- package/package.json +49 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +28 -0
- package/plugin/src/index.ts +53 -0
- package/plugin/tsconfig.json +9 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/src/ExpoPlayAudioStreamModule.ts +5 -0
- package/src/events.ts +66 -0
- package/src/index.ts +359 -0
- package/src/pipeline/index.ts +216 -0
- package/src/pipeline/types.ts +169 -0
- package/src/types.ts +270 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.annotation.SuppressLint
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.media.AudioDeviceCallback
|
|
8
|
+
import android.media.AudioDeviceInfo
|
|
9
|
+
import android.media.AudioManager
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.os.Bundle
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import androidx.annotation.RequiresApi
|
|
16
|
+
import androidx.core.app.ActivityCompat
|
|
17
|
+
import expo.modules.interfaces.permissions.Permissions
|
|
18
|
+
import expo.modules.kotlin.Promise
|
|
19
|
+
import expo.modules.kotlin.modules.Module
|
|
20
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
21
|
+
import expo.modules.audiostream.pipeline.PipelineIntegration
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExpoPlayAudioStreamModule : Module(), EventSender {
|
|
26
|
+
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
27
|
+
private lateinit var audioPlaybackManager: AudioPlaybackManager
|
|
28
|
+
private lateinit var audioManager: AudioManager
|
|
29
|
+
private lateinit var pipelineIntegration: PipelineIntegration
|
|
30
|
+
|
|
31
|
+
// Ensure callbacks are delivered on the main thread
|
|
32
|
+
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
|
|
33
|
+
|
|
34
|
+
private val reportedGroups = mutableSetOf<String>()
|
|
35
|
+
|
|
36
|
+
/** Map every device type to a logical group key */
|
|
37
|
+
private fun groupKey(type: Int): String = when (type) {
|
|
38
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
|
39
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> "BLUETOOTH"
|
|
40
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
41
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
42
|
+
AudioDeviceInfo.TYPE_USB_HEADSET -> "WIRED"
|
|
43
|
+
else -> type.toString() // fallback, treats every other type separately
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// We care about these types – includes both SCO and A2DP but we will collapse them into one group
|
|
47
|
+
private val interestingTypes = setOf(
|
|
48
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
|
49
|
+
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
|
50
|
+
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
51
|
+
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
52
|
+
AudioDeviceInfo.TYPE_USB_HEADSET
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
private val audioCallCallback = object : AudioDeviceCallback() {
|
|
56
|
+
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
57
|
+
val descriptions = addedDevices?.map { "${it.productName} (type=${it.type})" } ?: emptyList()
|
|
58
|
+
Log.d("ExpoAudioCallback", "onAudioDevicesAdded: $descriptions")
|
|
59
|
+
|
|
60
|
+
val firstOfGroup = addedDevices?.filter { d ->
|
|
61
|
+
d.type in interestingTypes && reportedGroups.add(groupKey(d.type))
|
|
62
|
+
}
|
|
63
|
+
if (firstOfGroup?.isNotEmpty()==true) {
|
|
64
|
+
val matched = firstOfGroup.map { "${it.productName} (type=${it.type})" }
|
|
65
|
+
Log.d("ExpoAudioCallback", "AudioDeviceCallback ➜ ADDED (interesting): $matched")
|
|
66
|
+
pipelineIntegration.logAudioTrackHealth("device_added")
|
|
67
|
+
val params = Bundle()
|
|
68
|
+
params.putString("reason", "newDeviceAvailable")
|
|
69
|
+
sendExpoEvent(Constants.DEVICE_RECONNECTED_EVENT_NAME, params)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
|
74
|
+
val descriptions = removedDevices?.map { "${it.productName} (type=${it.type})" } ?: emptyList()
|
|
75
|
+
Log.d("ExpoAudioCallback", "onAudioDevicesRemoved: $descriptions")
|
|
76
|
+
|
|
77
|
+
val lastOfGroup = removedDevices?.filter { d ->
|
|
78
|
+
d.type in interestingTypes && reportedGroups.remove(groupKey(d.type))
|
|
79
|
+
}
|
|
80
|
+
if (lastOfGroup?.isNotEmpty() == true) {
|
|
81
|
+
val matched = lastOfGroup.map { "${it.productName} (type=${it.type})" }
|
|
82
|
+
Log.d("ExpoAudioCallback", "AudioDeviceCallback ➜ REMOVED (interesting): $matched")
|
|
83
|
+
pipelineIntegration.logAudioTrackHealth("device_removed")
|
|
84
|
+
audioPlaybackManager.stopPlayback(null)
|
|
85
|
+
val params = Bundle()
|
|
86
|
+
params.putString("reason", "oldDeviceUnavailable")
|
|
87
|
+
sendExpoEvent(Constants.DEVICE_RECONNECTED_EVENT_NAME, params)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@SuppressLint("MissingPermission")
|
|
93
|
+
@RequiresApi(Build.VERSION_CODES.R)
|
|
94
|
+
override fun definition() = ModuleDefinition {
|
|
95
|
+
Name("ExpoPlayAudioStream")
|
|
96
|
+
|
|
97
|
+
Events(
|
|
98
|
+
Constants.AUDIO_EVENT_NAME,
|
|
99
|
+
Constants.SOUND_CHUNK_PLAYED_EVENT_NAME,
|
|
100
|
+
Constants.SOUND_STARTED_EVENT_NAME,
|
|
101
|
+
Constants.DEVICE_RECONNECTED_EVENT_NAME,
|
|
102
|
+
PipelineIntegration.EVENT_STATE_CHANGED,
|
|
103
|
+
PipelineIntegration.EVENT_PLAYBACK_STARTED,
|
|
104
|
+
PipelineIntegration.EVENT_ERROR,
|
|
105
|
+
PipelineIntegration.EVENT_ZOMBIE_DETECTED,
|
|
106
|
+
PipelineIntegration.EVENT_UNDERRUN,
|
|
107
|
+
PipelineIntegration.EVENT_DRAINED,
|
|
108
|
+
PipelineIntegration.EVENT_AUDIO_FOCUS_LOST,
|
|
109
|
+
PipelineIntegration.EVENT_AUDIO_FOCUS_RESUMED
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Initialize managers for playback and for recording
|
|
113
|
+
initializeManager()
|
|
114
|
+
initializePlaybackManager()
|
|
115
|
+
initializePipeline()
|
|
116
|
+
|
|
117
|
+
OnCreate {
|
|
118
|
+
audioManager = appContext.reactContext?.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
119
|
+
audioManager.registerAudioDeviceCallback(audioCallCallback, mainHandler)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
OnDestroy {
|
|
123
|
+
reportedGroups.clear()
|
|
124
|
+
audioManager.unregisterAudioDeviceCallback(audioCallCallback)
|
|
125
|
+
// Module is being destroyed (app shutdown)
|
|
126
|
+
// Just clean up resources without reinitialization
|
|
127
|
+
pipelineIntegration.destroy()
|
|
128
|
+
audioPlaybackManager.runOnDispose()
|
|
129
|
+
audioRecorderManager.release()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Function("destroy") {
|
|
133
|
+
// User explicitly called destroy - clean up and reinitialize for reuse
|
|
134
|
+
pipelineIntegration.destroy()
|
|
135
|
+
audioPlaybackManager.runOnDispose()
|
|
136
|
+
audioRecorderManager.release()
|
|
137
|
+
|
|
138
|
+
// Reinitialize all managers so the module can be used again
|
|
139
|
+
initializeManager()
|
|
140
|
+
initializePlaybackManager()
|
|
141
|
+
initializePipeline()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
AsyncFunction("requestPermissionsAsync") { promise: Promise ->
|
|
145
|
+
Permissions.askForPermissionsWithPermissionsManager(
|
|
146
|
+
appContext.permissions,
|
|
147
|
+
promise,
|
|
148
|
+
Manifest.permission.RECORD_AUDIO
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
AsyncFunction("getPermissionsAsync") { promise: Promise ->
|
|
153
|
+
Permissions.getPermissionsWithPermissionsManager(
|
|
154
|
+
appContext.permissions,
|
|
155
|
+
promise,
|
|
156
|
+
Manifest.permission.RECORD_AUDIO
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
AsyncFunction("playSound") { chunk: String, turnId: String, encoding: String?, promise: Promise ->
|
|
161
|
+
val pcmEncoding = when (encoding) {
|
|
162
|
+
"pcm_f32le" -> PCMEncoding.PCM_F32LE
|
|
163
|
+
"pcm_s16le", null -> PCMEncoding.PCM_S16LE
|
|
164
|
+
else -> {
|
|
165
|
+
Log.d(Constants.TAG, "Unsupported encoding: $encoding, defaulting to PCM_S16LE")
|
|
166
|
+
PCMEncoding.PCM_S16LE
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
audioPlaybackManager.playAudio(chunk, turnId, promise, pcmEncoding)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
AsyncFunction("stopSound") { promise: Promise -> audioPlaybackManager.stopPlayback(promise) }
|
|
173
|
+
|
|
174
|
+
AsyncFunction("clearSoundQueueByTurnId") { turnId: String, promise: Promise ->
|
|
175
|
+
audioPlaybackManager.setCurrentTurnId(turnId)
|
|
176
|
+
promise.resolve(null)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
AsyncFunction("startMicrophone") { options: Map<String, Any?>, promise: Promise ->
|
|
180
|
+
audioRecorderManager.startRecording(options, promise)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
AsyncFunction("stopMicrophone") { promise: Promise ->
|
|
184
|
+
audioRecorderManager.stopRecording(promise)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Function("toggleSilence") { isSilent: Boolean ->
|
|
188
|
+
// Just toggle silence without returning any value
|
|
189
|
+
audioRecorderManager.toggleSilence(isSilent)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
AsyncFunction("setSoundConfig") { config: Map<String, Any?>, promise: Promise ->
|
|
193
|
+
val useDefault = config["useDefault"] as? Boolean ?: false
|
|
194
|
+
|
|
195
|
+
if (useDefault) {
|
|
196
|
+
// Reset to default configuration
|
|
197
|
+
Log.d(Constants.TAG, "Resetting sound configuration to default values")
|
|
198
|
+
audioPlaybackManager.resetConfigToDefault(promise)
|
|
199
|
+
} else {
|
|
200
|
+
// Extract configuration values
|
|
201
|
+
val sampleRate = (config["sampleRate"] as? Number)?.toInt() ?: 16000
|
|
202
|
+
val playbackModeString = config["playbackMode"] as? String ?: "regular"
|
|
203
|
+
|
|
204
|
+
// Convert string playback mode to enum
|
|
205
|
+
val playbackMode = when (playbackModeString) {
|
|
206
|
+
"voiceProcessing" -> PlaybackMode.VOICE_PROCESSING
|
|
207
|
+
"conversation" -> PlaybackMode.CONVERSATION
|
|
208
|
+
else -> PlaybackMode.REGULAR
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create a new SoundConfig object
|
|
212
|
+
val soundConfig = SoundConfig(sampleRate = sampleRate, playbackMode = playbackMode)
|
|
213
|
+
|
|
214
|
+
// Update the sound player configuration
|
|
215
|
+
Log.d(Constants.TAG, "Setting sound configuration - sampleRate: $sampleRate, playbackMode: $playbackModeString")
|
|
216
|
+
audioPlaybackManager.updateConfig(soundConfig, promise)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Native Audio Pipeline V3 ────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
AsyncFunction("connectPipeline") { options: Map<String, Any?>, promise: Promise ->
|
|
223
|
+
pipelineIntegration.connect(options, promise)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
AsyncFunction("pushPipelineAudio") { options: Map<String, Any?>, promise: Promise ->
|
|
227
|
+
pipelineIntegration.pushAudio(options, promise)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Function("pushPipelineAudioSync") { options: Map<String, Any?> ->
|
|
231
|
+
pipelineIntegration.pushAudioSync(options)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
AsyncFunction("disconnectPipeline") { promise: Promise ->
|
|
235
|
+
pipelineIntegration.disconnect(promise)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
AsyncFunction("invalidatePipelineTurn") { options: Map<String, Any?>, promise: Promise ->
|
|
239
|
+
pipelineIntegration.invalidateTurn(options, promise)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Function("getPipelineTelemetry") {
|
|
243
|
+
pipelineIntegration.getTelemetry()
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Function("getPipelineState") {
|
|
247
|
+
pipelineIntegration.getState()
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
}
|
|
251
|
+
private fun initializeManager() {
|
|
252
|
+
val androidContext =
|
|
253
|
+
appContext.reactContext ?: throw IllegalStateException("Android context not available")
|
|
254
|
+
val permissionUtils = PermissionUtils(androidContext)
|
|
255
|
+
val audioEncoder = AudioDataEncoder()
|
|
256
|
+
val audioEffectsManager = AudioEffectsManager()
|
|
257
|
+
audioRecorderManager =
|
|
258
|
+
AudioRecorderManager(
|
|
259
|
+
permissionUtils,
|
|
260
|
+
audioEncoder,
|
|
261
|
+
this,
|
|
262
|
+
audioEffectsManager
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fun initializePlaybackManager() {
|
|
267
|
+
audioPlaybackManager = AudioPlaybackManager(this)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private fun initializePipeline() {
|
|
271
|
+
val ctx = appContext.reactContext
|
|
272
|
+
?: throw IllegalStateException("Android context not available")
|
|
273
|
+
pipelineIntegration = PipelineIntegration(ctx, this)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
277
|
+
Log.d(Constants.TAG, "Sending event EXPO: $eventName")
|
|
278
|
+
this@ExpoPlayAudioStreamModule.sendEvent(eventName, params)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import androidx.core.content.ContextCompat
|
|
6
|
+
|
|
7
|
+
class PermissionUtils(private val context: Context) {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the recording permission has been granted.
|
|
11
|
+
* @return Boolean indicating whether the RECORD_AUDIO permission is granted.
|
|
12
|
+
*/
|
|
13
|
+
fun checkRecordingPermission(): Boolean {
|
|
14
|
+
return ContextCompat.checkSelfPermission(context, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
data class RecordingConfig(
|
|
4
|
+
val sampleRate: Int = Constants.DEFAULT_SAMPLE_RATE,
|
|
5
|
+
val channels: Int = 1,
|
|
6
|
+
val encoding: String = "pcm_16bit",
|
|
7
|
+
val interval: Long = Constants.DEFAULT_INTERVAL,
|
|
8
|
+
val pointsPerSecond: Double = 20.0
|
|
9
|
+
) {
|
|
10
|
+
/**
|
|
11
|
+
* Validates the recording configuration
|
|
12
|
+
* @return Error information if invalid, null if valid
|
|
13
|
+
*/
|
|
14
|
+
fun validate(): ValidationResult? {
|
|
15
|
+
// Check sample rate
|
|
16
|
+
if (sampleRate !in listOf(16000, 24000, 44100, 48000)) {
|
|
17
|
+
return ValidationResult(
|
|
18
|
+
"INVALID_SAMPLE_RATE",
|
|
19
|
+
"Sample rate must be one of 16000, 24000, 44100, or 48000 Hz"
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check channels
|
|
24
|
+
if (channels !in 1..2) {
|
|
25
|
+
return ValidationResult(
|
|
26
|
+
"INVALID_CHANNELS",
|
|
27
|
+
"Channels must be either 1 (Mono) or 2 (Stereo)"
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// All checks passed
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
companion object {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a RecordingConfig from options map
|
|
38
|
+
* @param options Map containing configuration options
|
|
39
|
+
* @return New RecordingConfig instance
|
|
40
|
+
*/
|
|
41
|
+
fun fromOptions(options: Map<String, Any?>): RecordingConfig {
|
|
42
|
+
return RecordingConfig(
|
|
43
|
+
sampleRate = (options["sampleRate"] as? Number)?.toInt() ?: Constants.DEFAULT_SAMPLE_RATE,
|
|
44
|
+
channels = (options["channels"] as? Number)?.toInt() ?: 1,
|
|
45
|
+
encoding = options["encoding"] as? String ?: "pcm_16bit",
|
|
46
|
+
interval = (options["interval"] as? Number)?.toLong() ?: Constants.DEFAULT_INTERVAL,
|
|
47
|
+
pointsPerSecond = (options["pointsPerSecond"] as? Number)?.toDouble() ?: 20.0
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Data class to hold validation error information
|
|
55
|
+
*/
|
|
56
|
+
data class ValidationResult(
|
|
57
|
+
val code: String,
|
|
58
|
+
val message: String
|
|
59
|
+
)
|
|
60
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package expo.modules.audiostream
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines different playback modes for audio processing
|
|
5
|
+
*/
|
|
6
|
+
enum class PlaybackMode {
|
|
7
|
+
/**
|
|
8
|
+
* Regular playback mode for standard audio playback
|
|
9
|
+
*/
|
|
10
|
+
REGULAR,
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Conversation mode optimized for speech
|
|
14
|
+
*/
|
|
15
|
+
CONVERSATION,
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Voice processing mode with enhanced voice quality
|
|
19
|
+
*/
|
|
20
|
+
VOICE_PROCESSING
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for audio playback settings
|
|
25
|
+
*/
|
|
26
|
+
data class SoundConfig(
|
|
27
|
+
/**
|
|
28
|
+
* The sample rate for audio playback in Hz
|
|
29
|
+
*/
|
|
30
|
+
val sampleRate: Int = 44100,
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The playback mode (regular, conversation, or voiceProcessing)
|
|
34
|
+
*/
|
|
35
|
+
val playbackMode: PlaybackMode = PlaybackMode.REGULAR
|
|
36
|
+
) {
|
|
37
|
+
companion object {
|
|
38
|
+
/**
|
|
39
|
+
* Default configuration with standard settings
|
|
40
|
+
*/
|
|
41
|
+
val DEFAULT = SoundConfig(
|
|
42
|
+
sampleRate = 44100,
|
|
43
|
+
playbackMode = PlaybackMode.REGULAR
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|