@siteed/expo-audio-studio 2.18.2 → 2.18.5
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/CHANGELOG.md +23 -1
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +19 -3
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +142 -91
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +1 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +1 -1
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +6 -1
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractAudioData.js +10 -1
- package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +5 -1
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
- package/build/cjs/trimAudio.js +3 -1
- package/build/cjs/trimAudio.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +9 -38
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/utils/cleanNativeOptions.js +22 -0
- package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js +6 -1
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/esm/AudioAnalysis/extractAudioData.js +10 -1
- package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -1
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js +5 -1
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -1
- package/build/esm/trimAudio.js +3 -1
- package/build/esm/trimAudio.js.map +1 -1
- package/build/esm/useAudioRecorder.js +8 -4
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/utils/cleanNativeOptions.js +19 -0
- package/build/esm/utils/cleanNativeOptions.js.map +1 -0
- package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -1
- package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -1
- package/build/types/trimAudio.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/utils/cleanNativeOptions.d.ts +15 -0
- package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
- package/ios/AudioStreamManager.swift +76 -18
- package/ios/ExpoAudioStreamModule.swift +17 -19
- package/package.json +6 -7
- package/src/AudioAnalysis/extractAudioAnalysis.ts +12 -1
- package/src/AudioAnalysis/extractAudioData.ts +12 -1
- package/src/AudioAnalysis/extractMelSpectrogram.ts +11 -1
- package/src/trimAudio.ts +5 -1
- package/src/useAudioRecorder.tsx +8 -7
- package/src/utils/cleanNativeOptions.ts +18 -0
|
@@ -177,14 +177,14 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
177
177
|
/// - `bitrate`: The compression bitrate in bps (default is 128000).
|
|
178
178
|
/// - promise: A promise to resolve with the recording settings or reject with an error.
|
|
179
179
|
AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
|
|
180
|
-
Logger.debug("ExpoAudioStreamModule", "startRecording called
|
|
180
|
+
Logger.debug("ExpoAudioStreamModule", "startRecording called")
|
|
181
181
|
self.checkMicrophonePermission { granted in
|
|
182
182
|
guard granted else {
|
|
183
183
|
Logger.warn("ExpoAudioStreamModule", "startRecording: Permission denied.")
|
|
184
184
|
promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
|
|
185
185
|
return
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
// Check if output.compressed is enabled and format is Opus
|
|
189
189
|
var modifiedOptions = options
|
|
190
190
|
if let output = options["output"] as? [String: Any],
|
|
@@ -192,25 +192,24 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
192
192
|
let enabled = compressed["enabled"] as? Bool, enabled,
|
|
193
193
|
let format = compressed["format"] as? String,
|
|
194
194
|
format.lowercased() == "opus" {
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
// Create mutable copies
|
|
197
197
|
var modifiedOutput = output
|
|
198
198
|
var modifiedCompressed = compressed
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
// Change format to AAC and log warning
|
|
201
201
|
modifiedCompressed["format"] = "aac"
|
|
202
202
|
modifiedOutput["compressed"] = modifiedCompressed
|
|
203
203
|
modifiedOptions["output"] = modifiedOutput
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
Logger.warn("ExpoAudioStreamModule", "startRecording: Opus format is not supported on iOS. Falling back to AAC format.")
|
|
206
206
|
}
|
|
207
|
-
|
|
207
|
+
|
|
208
208
|
// Create settings with validation using the potentially modified options
|
|
209
209
|
let settingsResult = RecordingSettings.fromDictionary(modifiedOptions)
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
switch settingsResult {
|
|
212
212
|
case .success(let settings):
|
|
213
|
-
Logger.debug("ExpoAudioStreamModule", "startRecording: Settings parsed successfully. ShowNotification=\(settings.showNotification)")
|
|
214
213
|
// Initialize notification if enabled
|
|
215
214
|
if settings.showNotification {
|
|
216
215
|
Task {
|
|
@@ -220,8 +219,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
220
219
|
}
|
|
221
220
|
}
|
|
222
221
|
}
|
|
223
|
-
|
|
224
|
-
Logger.debug("ExpoAudioStreamModule", "startRecording: Calling streamManager.startRecording")
|
|
222
|
+
|
|
225
223
|
if let result = self.streamManager.startRecording(settings: settings) {
|
|
226
224
|
var resultDict: [String: Any] = [
|
|
227
225
|
"fileUri": result.fileUri,
|
|
@@ -230,7 +228,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
230
228
|
"sampleRate": result.sampleRate,
|
|
231
229
|
"mimeType": result.mimeType,
|
|
232
230
|
]
|
|
233
|
-
|
|
231
|
+
|
|
234
232
|
// Add compression info if available
|
|
235
233
|
if let compression = result.compression {
|
|
236
234
|
resultDict["compression"] = [
|
|
@@ -240,16 +238,16 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
240
238
|
"format": compression.format
|
|
241
239
|
]
|
|
242
240
|
}
|
|
243
|
-
|
|
244
|
-
Logger.info("ExpoAudioStreamModule", "
|
|
241
|
+
|
|
242
|
+
Logger.info("ExpoAudioStreamModule", "Recording started successfully")
|
|
245
243
|
promise.resolve(resultDict)
|
|
246
244
|
} else {
|
|
247
|
-
Logger.error("ExpoAudioStreamModule", "
|
|
245
|
+
Logger.error("ExpoAudioStreamModule", "Failed to start recording")
|
|
248
246
|
promise.reject("ERROR", "Failed to start recording.")
|
|
249
247
|
}
|
|
250
|
-
|
|
248
|
+
|
|
251
249
|
case .failure(let error):
|
|
252
|
-
Logger.error("ExpoAudioStreamModule", "
|
|
250
|
+
Logger.error("ExpoAudioStreamModule", "Invalid settings - \(error.localizedDescription)")
|
|
253
251
|
promise.reject("INVALID_SETTINGS", error.localizedDescription)
|
|
254
252
|
}
|
|
255
253
|
}
|
|
@@ -918,9 +916,9 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
918
916
|
private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
|
|
919
917
|
switch AVAudioSession.sharedInstance().recordPermission {
|
|
920
918
|
case .granted:
|
|
921
|
-
completion(true)
|
|
919
|
+
DispatchQueue.main.async { completion(true) }
|
|
922
920
|
case .denied:
|
|
923
|
-
completion(false)
|
|
921
|
+
DispatchQueue.main.async { completion(false) }
|
|
924
922
|
case .undetermined:
|
|
925
923
|
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
926
924
|
DispatchQueue.main.async {
|
|
@@ -928,7 +926,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate, AudioDev
|
|
|
928
926
|
}
|
|
929
927
|
}
|
|
930
928
|
@unknown default:
|
|
931
|
-
completion(false)
|
|
929
|
+
DispatchQueue.main.async { completion(false) }
|
|
932
930
|
}
|
|
933
931
|
}
|
|
934
932
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-studio",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.5",
|
|
4
4
|
"description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -96,13 +96,12 @@
|
|
|
96
96
|
"open:android": "open -a \"Android Studio\" ../../apps/playground/android",
|
|
97
97
|
"size": "bundle-size && size-limit",
|
|
98
98
|
"release": "./publish.sh",
|
|
99
|
-
"agent:validate": "cd ../../apps/playground && yarn agent:validate",
|
|
100
99
|
"agent:test:unit": "yarn test:android:unit",
|
|
101
100
|
"agent:test:integration": "yarn test:android:instrumented",
|
|
102
101
|
"agent:compilation:check": "yarn typecheck && yarn build"
|
|
103
102
|
},
|
|
104
103
|
"devDependencies": {
|
|
105
|
-
"@expo/config-plugins": "~
|
|
104
|
+
"@expo/config-plugins": "~54.0.0",
|
|
106
105
|
"@expo/npm-proofread": "^1.0.1",
|
|
107
106
|
"@siteed/publisher": "^0.4.18",
|
|
108
107
|
"@size-limit/preset-big-lib": "^11.1.4",
|
|
@@ -119,13 +118,13 @@
|
|
|
119
118
|
"eslint-plugin-prettier": "^5.1.3",
|
|
120
119
|
"eslint-plugin-promise": "^6.1.1",
|
|
121
120
|
"eslint-plugin-react": "^7.34.1",
|
|
122
|
-
"expo": "^
|
|
121
|
+
"expo": "^54.0.0",
|
|
123
122
|
"expo-module-scripts": "^4.1.7",
|
|
124
|
-
"expo-modules-core": "
|
|
123
|
+
"expo-modules-core": "~3.0.0",
|
|
125
124
|
"jest": "^29.7.0",
|
|
126
125
|
"prettier": "^3.2.5",
|
|
127
|
-
"react": "19.
|
|
128
|
-
"react-native": "0.
|
|
126
|
+
"react": "19.1.0",
|
|
127
|
+
"react-native": "0.81.5",
|
|
129
128
|
"rimraf": "^6.0.1",
|
|
130
129
|
"size-limit": "^11.1.4",
|
|
131
130
|
"ts-node": "^10.9.2",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
DecodingConfig,
|
|
16
16
|
} from './AudioAnalysis.types'
|
|
17
17
|
import { processAudioBuffer } from '../utils/audioProcessing'
|
|
18
|
+
import { cleanNativeOptions } from '../utils/cleanNativeOptions'
|
|
18
19
|
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32'
|
|
19
20
|
import crc32 from '../utils/crc32'
|
|
20
21
|
import { getWavFileInfo, WavFileInfo } from '../utils/getWavFileInfo'
|
|
@@ -209,7 +210,17 @@ export async function extractAudioAnalysis(
|
|
|
209
210
|
throw error
|
|
210
211
|
}
|
|
211
212
|
} else {
|
|
212
|
-
|
|
213
|
+
// Strip non-serializable fields — logger and arrayBuffer cause
|
|
214
|
+
// "Cannot convert '[object Object]' to a Kotlin type" on Android.
|
|
215
|
+
const {
|
|
216
|
+
logger: _logger,
|
|
217
|
+
arrayBuffer: _arrayBuffer,
|
|
218
|
+
...nativeOptions
|
|
219
|
+
} = props
|
|
220
|
+
// Clean undefined values to avoid Android Kotlin bridge crash
|
|
221
|
+
return await ExpoAudioStreamModule.extractAudioAnalysis(
|
|
222
|
+
cleanNativeOptions(nativeOptions)
|
|
223
|
+
)
|
|
213
224
|
}
|
|
214
225
|
}
|
|
215
226
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { ExtractAudioDataOptions } from '../ExpoAudioStream.types'
|
|
2
2
|
import ExpoAudioStreamModule from '../ExpoAudioStreamModule'
|
|
3
|
+
import { isWeb } from '../constants'
|
|
4
|
+
import { cleanNativeOptions } from '../utils/cleanNativeOptions'
|
|
3
5
|
|
|
4
6
|
export const extractAudioData = async (props: ExtractAudioDataOptions) => {
|
|
5
|
-
|
|
7
|
+
if (isWeb) {
|
|
8
|
+
// Web implementation handles logger natively in ExpoAudioStreamModule.ts
|
|
9
|
+
return await ExpoAudioStreamModule.extractAudioData(props)
|
|
10
|
+
}
|
|
11
|
+
// Native: only pass serializable fields — logger causes crash on Android
|
|
12
|
+
const { logger: _logger, ...nativeOptions } = props
|
|
13
|
+
// Clean undefined values to avoid Android Kotlin bridge crash
|
|
14
|
+
return await ExpoAudioStreamModule.extractAudioData(
|
|
15
|
+
cleanNativeOptions(nativeOptions)
|
|
16
|
+
)
|
|
6
17
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
processAudioBuffer,
|
|
14
14
|
ProcessedAudioData,
|
|
15
15
|
} from '../utils/audioProcessing'
|
|
16
|
+
import { cleanNativeOptions } from '../utils/cleanNativeOptions'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Extracts a mel spectrogram from audio data
|
|
@@ -100,7 +101,16 @@ export async function extractMelSpectrogram(
|
|
|
100
101
|
await audioContext.close()
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
|
-
|
|
104
|
+
// Strip logger/arrayBuffer (non-serializable) then clean undefined values
|
|
105
|
+
// to avoid Android "Cannot convert '[object Object]' to Kotlin type" crash
|
|
106
|
+
const {
|
|
107
|
+
logger: _logger,
|
|
108
|
+
arrayBuffer: _arrayBuffer,
|
|
109
|
+
...nativeOptions
|
|
110
|
+
} = options
|
|
111
|
+
return ExpoAudioStreamModule.extractMelSpectrogram(
|
|
112
|
+
cleanNativeOptions(nativeOptions)
|
|
113
|
+
)
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
/**
|
package/src/trimAudio.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
TrimProgressEvent,
|
|
7
7
|
} from './ExpoAudioStream.types'
|
|
8
8
|
import ExpoAudioStreamModule from './ExpoAudioStreamModule'
|
|
9
|
+
import { cleanNativeOptions } from './utils/cleanNativeOptions'
|
|
9
10
|
|
|
10
11
|
// Create a single emitter instance
|
|
11
12
|
const emitter = new LegacyEventEmitter(ExpoAudioStreamModule)
|
|
@@ -63,7 +64,10 @@ export async function trimAudio(
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
try {
|
|
66
|
-
|
|
67
|
+
// Clean non-serializable/undefined values to avoid Android Kotlin bridge crash
|
|
68
|
+
const result = await ExpoAudioStreamModule.trimAudio(
|
|
69
|
+
cleanNativeOptions(options)
|
|
70
|
+
)
|
|
67
71
|
return result
|
|
68
72
|
} finally {
|
|
69
73
|
if (subscription) {
|
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
StartRecordingResult,
|
|
15
15
|
} from './ExpoAudioStream.types'
|
|
16
16
|
import ExpoAudioStreamModule from './ExpoAudioStreamModule'
|
|
17
|
+
import { validateRecordingConfig } from './constants/platformLimitations'
|
|
17
18
|
import {
|
|
18
19
|
addAudioAnalysisListener,
|
|
19
20
|
addAudioEventListener,
|
|
20
21
|
AudioEventPayload,
|
|
21
22
|
addRecordingInterruptionListener,
|
|
22
23
|
} from './events'
|
|
24
|
+
import { cleanNativeOptions } from './utils/cleanNativeOptions'
|
|
23
25
|
|
|
24
26
|
export interface UseAudioRecorderProps {
|
|
25
27
|
logger?: ConsoleLike
|
|
@@ -473,11 +475,6 @@ export function useAudioRecorder({
|
|
|
473
475
|
|
|
474
476
|
const startRecording = useCallback(
|
|
475
477
|
async (recordingOptions: RecordingConfig) => {
|
|
476
|
-
// Import validation function
|
|
477
|
-
const { validateRecordingConfig } = await import(
|
|
478
|
-
'./constants/platformLimitations'
|
|
479
|
-
)
|
|
480
|
-
|
|
481
478
|
// Validate the encoding configuration
|
|
482
479
|
const validationResult = validateRecordingConfig({
|
|
483
480
|
encoding: recordingOptions.encoding,
|
|
@@ -519,8 +516,10 @@ export function useAudioRecorder({
|
|
|
519
516
|
logger?.warn(`onAudioStream is not a function`, onAudioStream)
|
|
520
517
|
onAudioStreamRef.current = null
|
|
521
518
|
}
|
|
519
|
+
// Strip undefined values and functions that can't cross the native bridge
|
|
520
|
+
const cleanOptions = cleanNativeOptions(options)
|
|
522
521
|
const startResult: StartRecordingResult =
|
|
523
|
-
await ExpoAudioStream.startRecording(
|
|
522
|
+
await ExpoAudioStream.startRecording(cleanOptions)
|
|
524
523
|
dispatch({ type: 'START' })
|
|
525
524
|
|
|
526
525
|
startResultRef.current = startResult
|
|
@@ -573,8 +572,10 @@ export function useAudioRecorder({
|
|
|
573
572
|
onAudioStreamRef.current = null
|
|
574
573
|
}
|
|
575
574
|
|
|
575
|
+
// Strip undefined values and functions that can't cross the native bridge
|
|
576
|
+
const cleanOptions = cleanNativeOptions(options)
|
|
576
577
|
// Call the native prepareRecording method
|
|
577
|
-
await ExpoAudioStream.prepareRecording(
|
|
578
|
+
await ExpoAudioStream.prepareRecording(cleanOptions)
|
|
578
579
|
logger?.debug(`recording prepared successfully`)
|
|
579
580
|
},
|
|
580
581
|
[]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips non-serializable values (functions, ArrayBuffer, undefined) from
|
|
3
|
+
* option objects before passing them to Expo native modules.
|
|
4
|
+
*
|
|
5
|
+
* Android's Kotlin bridge crashes with "Cannot convert '[object Object]' to a
|
|
6
|
+
* Kotlin type" when it receives non-plain values such as `logger`, `ArrayBuffer`,
|
|
7
|
+
* or `undefined` fields. The JSON round-trip removes all of these safely.
|
|
8
|
+
*
|
|
9
|
+
* Only use this for small config objects (never for large audio buffers).
|
|
10
|
+
*
|
|
11
|
+
* NOTE: structuredClone() is intentionally NOT used here — it preserves
|
|
12
|
+
* undefined values and non-JSON types, which is exactly what we need to strip.
|
|
13
|
+
*/
|
|
14
|
+
export function cleanNativeOptions<T>(options: T): T {
|
|
15
|
+
// NOSONAR: JSON round-trip is deliberate — it strips undefined, functions,
|
|
16
|
+
// and non-serializable values that structuredClone would preserve.
|
|
17
|
+
return JSON.parse(JSON.stringify(options)) // NOSONAR
|
|
18
|
+
}
|