@siteed/expo-audio-stream 1.12.3 → 1.13.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/CHANGELOG.md +5 -4
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +866 -70
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +4 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +30 -9
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +163 -24
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +62 -0
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +10 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +158 -0
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -1
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/useAudioRecorder.js +9 -9
- package/build/useAudioRecorder.js.map +1 -1
- package/ios/AudioProcessor.swift +391 -1
- package/ios/ExpoAudioStreamModule.swift +100 -0
- package/ios/Features.swift +30 -0
- package/package.json +1 -1
- package/plugin/build/index.d.ts +0 -1
- package/plugin/build/index.js +0 -5
- package/plugin/src/index.ts +0 -6
- package/src/AudioAnalysis/AudioAnalysis.types.ts +66 -0
- package/src/AudioAnalysis/extractAudioAnalysis.ts +219 -0
- package/src/index.ts +12 -1
- package/src/useAudioRecorder.tsx +9 -9
|
@@ -9,6 +9,11 @@ import android.util.Log
|
|
|
9
9
|
import android.os.Handler
|
|
10
10
|
import android.os.Looper
|
|
11
11
|
import expo.modules.kotlin.Promise
|
|
12
|
+
import android.app.NotificationChannel
|
|
13
|
+
import android.app.NotificationManager
|
|
14
|
+
import android.os.Build.VERSION_CODES
|
|
15
|
+
import android.app.Notification
|
|
16
|
+
import androidx.core.app.NotificationCompat
|
|
12
17
|
|
|
13
18
|
class AudioRecordingService : Service() {
|
|
14
19
|
private val notificationManager by lazy {
|
|
@@ -28,17 +33,33 @@ class AudioRecordingService : Service() {
|
|
|
28
33
|
Log.d(Constants.TAG, "AudioRecordingService onStartCommand")
|
|
29
34
|
|
|
30
35
|
if (!isRunning) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
isRunning = true
|
|
37
|
+
|
|
38
|
+
// Start as foreground service if keepAwake is true, regardless of notification settings
|
|
39
|
+
val keepAwake = AudioRecorderManager.getInstance()?.getKeepAwakeStatus() ?: false
|
|
40
|
+
if (keepAwake) {
|
|
41
|
+
// Create a minimal notification channel if needed, but don't show the notification
|
|
42
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
43
|
+
val channel = NotificationChannel(
|
|
44
|
+
"recording_service",
|
|
45
|
+
"Recording Service",
|
|
46
|
+
NotificationManager.IMPORTANCE_LOW
|
|
47
|
+
)
|
|
48
|
+
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
49
|
+
notificationManager.createNotificationChannel(channel)
|
|
50
|
+
|
|
51
|
+
// Create minimal notification but don't show it
|
|
52
|
+
val notification = NotificationCompat.Builder(this, "recording_service")
|
|
53
|
+
.setContentTitle("")
|
|
54
|
+
.setContentText("")
|
|
55
|
+
.build()
|
|
56
|
+
|
|
57
|
+
startForeground(1, notification)
|
|
58
|
+
}
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
|
-
|
|
41
|
-
return
|
|
61
|
+
|
|
62
|
+
return START_STICKY
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
override fun onDestroy() {
|
|
@@ -52,45 +52,54 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
|
|
55
|
-
val fileUri = options["fileUri"] as? String
|
|
56
|
-
val pointsPerSecond = (options["pointsPerSecond"] as? Double) ?: 20.0
|
|
57
|
-
val algorithm = options["algorithm"] as? String ?: "peak"
|
|
58
|
-
val featuresMap = options["features"] as? Map<*, *>
|
|
59
|
-
val features = featuresMap?.filterKeys { it is String }
|
|
60
|
-
?.filterValues { it is Boolean }
|
|
61
|
-
?.mapKeys { it.key as String }
|
|
62
|
-
?.mapValues { it.value as Boolean }
|
|
63
|
-
?: emptyMap()
|
|
64
|
-
val skipWavHeader = (options["skipWavHeader"] as? Boolean) ?: true
|
|
65
|
-
|
|
66
|
-
if (fileUri == null) {
|
|
67
|
-
promise.reject("INVALID_ARGUMENTS", "fileUri is required", null)
|
|
68
|
-
return@AsyncFunction
|
|
69
|
-
}
|
|
70
|
-
|
|
71
55
|
try {
|
|
72
|
-
val
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
56
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
57
|
+
|
|
58
|
+
// Get decoding options
|
|
59
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
|
|
60
|
+
val decodingConfig = if (decodingOptionsMap != null) {
|
|
61
|
+
DecodingConfig(
|
|
62
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
63
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
64
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
65
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
66
|
+
)
|
|
67
|
+
} else null
|
|
68
|
+
|
|
69
|
+
val audioData = audioProcessor.loadAudioFromAnyFormat(fileUri, decodingConfig)
|
|
70
|
+
?: throw IllegalStateException("Failed to load audio file")
|
|
71
|
+
|
|
72
|
+
val pointsPerSecond = (options["pointsPerSecond"] as? Double) ?: 20.0
|
|
73
|
+
val algorithm = options["algorithm"] as? String ?: "peak"
|
|
74
|
+
val featuresMap = options["features"] as? Map<*, *>
|
|
75
|
+
val features = featuresMap?.filterKeys { it is String }
|
|
76
|
+
?.filterValues { it is Boolean }
|
|
77
|
+
?.mapKeys { it.key as String }
|
|
78
|
+
?.mapValues { it.value as Boolean }
|
|
79
|
+
?: emptyMap()
|
|
77
80
|
|
|
78
81
|
val recordingConfig = RecordingConfig(
|
|
79
82
|
sampleRate = audioData.sampleRate,
|
|
80
83
|
channels = audioData.channels,
|
|
81
|
-
encoding =
|
|
84
|
+
encoding = when (audioData.bitDepth) {
|
|
85
|
+
8 -> "pcm_8bit"
|
|
86
|
+
16 -> "pcm_16bit"
|
|
87
|
+
32 -> "pcm_32bit"
|
|
88
|
+
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
89
|
+
},
|
|
82
90
|
pointsPerSecond = pointsPerSecond,
|
|
83
91
|
algorithm = algorithm,
|
|
84
92
|
features = features
|
|
85
93
|
)
|
|
86
94
|
|
|
87
|
-
Log.d(
|
|
95
|
+
Log.d(Constants.TAG, "extractAudioAnalysis: $recordingConfig")
|
|
88
96
|
audioProcessor.resetCumulativeAmplitudeRange()
|
|
89
97
|
|
|
90
98
|
val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
|
|
91
99
|
promise.resolve(analysisData.toDictionary())
|
|
92
100
|
} catch (e: Exception) {
|
|
93
|
-
|
|
101
|
+
Log.e(Constants.TAG, "Audio processing failed: ${e.message}", e)
|
|
102
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
94
103
|
}
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -174,6 +183,136 @@ class ExpoAudioStreamModule : Module(), EventSender {
|
|
|
174
183
|
}
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
AsyncFunction("extractPreview") { options: Map<String, Any>, promise: Promise ->
|
|
187
|
+
try {
|
|
188
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
189
|
+
val numberOfPoints = (options["numberOfPoints"] as? Int) ?: 100
|
|
190
|
+
val algorithm = (options["algorithm"] as? String)?.lowercase() ?: "rms"
|
|
191
|
+
val startTime = (options["startTime"] as? Number)?.toLong()
|
|
192
|
+
val endTime = (options["endTime"] as? Number)?.toLong()
|
|
193
|
+
|
|
194
|
+
Log.d(Constants.TAG, """
|
|
195
|
+
Extracting preview with params:
|
|
196
|
+
- fileUri: $fileUri
|
|
197
|
+
- numberOfPoints: $numberOfPoints
|
|
198
|
+
- algorithm: $algorithm
|
|
199
|
+
- startTime: ${startTime ?: "none"}
|
|
200
|
+
- endTime: ${endTime ?: "none"}
|
|
201
|
+
""".trimIndent())
|
|
202
|
+
|
|
203
|
+
// Get decoding options with defaults
|
|
204
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
|
|
205
|
+
val decodingConfig = if (decodingOptionsMap != null) {
|
|
206
|
+
DecodingConfig(
|
|
207
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int ?: 22050,
|
|
208
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int ?: 1,
|
|
209
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
210
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
211
|
+
)
|
|
212
|
+
} else DecodingConfig(
|
|
213
|
+
targetSampleRate = 16000,
|
|
214
|
+
targetChannels = 1,
|
|
215
|
+
targetBitDepth = 16,
|
|
216
|
+
normalizeAudio = false
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
Log.d(Constants.TAG, """
|
|
220
|
+
Using decoding config:
|
|
221
|
+
- targetSampleRate: ${decodingConfig.targetSampleRate}
|
|
222
|
+
- targetChannels: ${decodingConfig.targetChannels}
|
|
223
|
+
- targetBitDepth: ${decodingConfig.targetBitDepth}
|
|
224
|
+
- normalizeAudio: ${decodingConfig.normalizeAudio}
|
|
225
|
+
""".trimIndent())
|
|
226
|
+
|
|
227
|
+
// Use loadAudioRange when time range is specified, otherwise fall back to loadAudioFromAnyFormat
|
|
228
|
+
val audioData = if (startTime != null && endTime != null) {
|
|
229
|
+
audioProcessor.loadAudioRange(fileUri, startTime, endTime, decodingConfig)
|
|
230
|
+
} else {
|
|
231
|
+
audioProcessor.loadAudioFromAnyFormat(fileUri, decodingConfig)
|
|
232
|
+
} ?: throw IllegalStateException("Failed to load audio file")
|
|
233
|
+
|
|
234
|
+
val previewConfig = RecordingConfig(
|
|
235
|
+
sampleRate = audioData.sampleRate,
|
|
236
|
+
channels = audioData.channels,
|
|
237
|
+
encoding = when (audioData.bitDepth) {
|
|
238
|
+
8 -> "pcm_8bit"
|
|
239
|
+
16 -> "pcm_16bit"
|
|
240
|
+
32 -> "pcm_32bit"
|
|
241
|
+
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
242
|
+
},
|
|
243
|
+
pointsPerSecond = 0.0, // Will be overridden by numberOfPoints
|
|
244
|
+
algorithm = algorithm,
|
|
245
|
+
features = emptyMap() // No features needed for preview
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
val preview = audioProcessor.generatePreview(
|
|
249
|
+
audioData = audioData,
|
|
250
|
+
numberOfPoints = numberOfPoints,
|
|
251
|
+
startTimeMs = startTime,
|
|
252
|
+
endTimeMs = endTime,
|
|
253
|
+
config = previewConfig
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
Log.d(Constants.TAG, "Preview generated successfully with ${preview.dataPoints.size} points")
|
|
257
|
+
promise.resolve(preview.toDictionary())
|
|
258
|
+
} catch (e: Exception) {
|
|
259
|
+
Log.e(Constants.TAG, "Preview generation failed: ${e.message}", e)
|
|
260
|
+
Log.e(Constants.TAG, "Stack trace: ${e.stackTraceToString()}")
|
|
261
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
|
|
266
|
+
try {
|
|
267
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
268
|
+
val startTimeMs = requireNotNull(options["startTimeMs"] as? Number)?.toLong()
|
|
269
|
+
?: throw IllegalArgumentException("startTimeMs is required")
|
|
270
|
+
val endTimeMs = requireNotNull(options["endTimeMs"] as? Number)?.toLong()
|
|
271
|
+
?: throw IllegalArgumentException("endTimeMs is required")
|
|
272
|
+
val outputFileName = options["outputFileName"] as? String
|
|
273
|
+
|
|
274
|
+
// Get decoding options
|
|
275
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
|
|
276
|
+
val decodingConfig = if (decodingOptionsMap != null) {
|
|
277
|
+
DecodingConfig(
|
|
278
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
279
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
280
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
281
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
282
|
+
)
|
|
283
|
+
} else null
|
|
284
|
+
|
|
285
|
+
Log.d(Constants.TAG, """
|
|
286
|
+
Trimming audio with params:
|
|
287
|
+
- fileUri: $fileUri
|
|
288
|
+
- startTimeMs: $startTimeMs
|
|
289
|
+
- endTimeMs: $endTimeMs
|
|
290
|
+
- outputFileName: ${outputFileName ?: "auto-generated"}
|
|
291
|
+
""".trimIndent())
|
|
292
|
+
|
|
293
|
+
val trimmedAudio = audioProcessor.trimAudio(
|
|
294
|
+
fileUri = fileUri,
|
|
295
|
+
startTimeMs = startTimeMs,
|
|
296
|
+
endTimeMs = endTimeMs,
|
|
297
|
+
config = decodingConfig,
|
|
298
|
+
outputFileName = outputFileName
|
|
299
|
+
) ?: throw IllegalStateException("Failed to trim audio")
|
|
300
|
+
|
|
301
|
+
// Create a map with the available data
|
|
302
|
+
val resultMap = mapOf<String, Any>(
|
|
303
|
+
"sampleRate" to trimmedAudio.sampleRate,
|
|
304
|
+
"channels" to trimmedAudio.channels,
|
|
305
|
+
"bitDepth" to trimmedAudio.bitDepth,
|
|
306
|
+
"dataSize" to trimmedAudio.data.size
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
promise.resolve(resultMap)
|
|
310
|
+
} catch (e: Exception) {
|
|
311
|
+
Log.e(Constants.TAG, "Failed to trim audio: ${e.message}", e)
|
|
312
|
+
promise.reject("TRIM_ERROR", e.message ?: "Unknown error", e)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
177
316
|
OnDestroy {
|
|
178
317
|
AudioRecorderManager.destroy()
|
|
179
318
|
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents the configuration for decoding audio data.
|
|
3
|
+
*/
|
|
4
|
+
export interface DecodingConfig {
|
|
5
|
+
targetSampleRate?: number;
|
|
6
|
+
targetChannels?: number;
|
|
7
|
+
targetBitDepth?: number;
|
|
8
|
+
normalizeAudio?: boolean;
|
|
9
|
+
}
|
|
1
10
|
/**
|
|
2
11
|
* Represents various audio features extracted from an audio signal.
|
|
3
12
|
*/
|
|
@@ -71,4 +80,57 @@ export interface AudioAnalysis {
|
|
|
71
80
|
speaker: number;
|
|
72
81
|
}[];
|
|
73
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Options for specifying a time range within an audio file.
|
|
85
|
+
*/
|
|
86
|
+
export interface AudioRangeOptions {
|
|
87
|
+
/** Start time in milliseconds */
|
|
88
|
+
startTime?: number;
|
|
89
|
+
/** End time in milliseconds */
|
|
90
|
+
endTime?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Options for generating a quick preview of audio waveform.
|
|
94
|
+
* This is optimized for UI rendering with a specified number of points.
|
|
95
|
+
*/
|
|
96
|
+
export interface PreviewOptions extends AudioRangeOptions {
|
|
97
|
+
/** URI of the audio file to analyze */
|
|
98
|
+
fileUri: string;
|
|
99
|
+
/**
|
|
100
|
+
* Total number of points to generate for the preview.
|
|
101
|
+
* @default 100
|
|
102
|
+
*/
|
|
103
|
+
numberOfPoints?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Algorithm used to calculate amplitude values
|
|
106
|
+
* @default "rms"
|
|
107
|
+
*/
|
|
108
|
+
algorithm?: AmplitudeAlgorithm;
|
|
109
|
+
/**
|
|
110
|
+
* Optional configuration for decoding the audio file.
|
|
111
|
+
* Defaults to:
|
|
112
|
+
* - targetSampleRate: undefined (keep original)
|
|
113
|
+
* - targetChannels: undefined (keep original)
|
|
114
|
+
* - targetBitDepth: 16
|
|
115
|
+
* - normalizeAudio: false
|
|
116
|
+
*/
|
|
117
|
+
decodingOptions?: DecodingConfig;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Represents a simplified preview of audio waveform,
|
|
121
|
+
* optimized for quick visualization.
|
|
122
|
+
*/
|
|
123
|
+
export interface AudioPreview {
|
|
124
|
+
/** Number of data points per second */
|
|
125
|
+
pointsPerSecond: number;
|
|
126
|
+
/** Duration of the audio in milliseconds */
|
|
127
|
+
durationMs: number;
|
|
128
|
+
/** Range of amplitude values in the preview */
|
|
129
|
+
amplitudeRange: {
|
|
130
|
+
min: number;
|
|
131
|
+
max: number;
|
|
132
|
+
};
|
|
133
|
+
/** Array of data points representing the waveform */
|
|
134
|
+
dataPoints: DataPoint[];
|
|
135
|
+
}
|
|
74
136
|
//# sourceMappingURL=AudioAnalysis.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioAnalysis.types.d.ts","sourceRoot":"","sources":["../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,gBAAgB,EAAE,MAAM,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,eAAe,EAAE,MAAM,CAAA;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,GAAG,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,aAAa,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,WAAW,CAAC,EAAE,MAAM,CAAA;IAEpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,KAAK,CAAA;AAE/C;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,SAAS,EAAE,CAAA;IACvB,kBAAkB,EAAE,kBAAkB,CAAA;IACtC,cAAc,EAAE;QACZ,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;KACd,CAAA;IAED,cAAc,CAAC,EAAE;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;KAClB,EAAE,CAAA;CACN"}
|
|
1
|
+
{"version":3,"file":"AudioAnalysis.types.d.ts","sourceRoot":"","sources":["../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":"AAAA,sEAAsE","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude: number // Minimum amplitude value in the audio signal.\n maxAmplitude: number // Maximum amplitude value in the audio signal.\n zcr: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n}\n\n/**\n * Options to specify which audio features to extract.\n */\nexport interface AudioFeaturesOptions {\n energy?: boolean\n mfcc?: boolean\n rms?: boolean\n zcr?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number\n activeSpeech?: boolean\n dB?: number\n silent?: boolean\n features?: AudioFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n // TODO: speaker detection\n speaker?: number\n}\n\nexport type AmplitudeAlgorithm = 'peak' | 'rms'\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n pointsPerSecond: number // How many consolidated value per second\n durationMs: number // Duration of the audio in milliseconds\n bitDepth: number // Bit depth of the audio\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeAlgorithm: AmplitudeAlgorithm // Algorithm used to calculate amplitude values.\n amplitudeRange: {\n min: number\n max: number\n }\n // TODO: speaker detection\n speakerChanges?: {\n timestamp: number // Timestamp of the speaker change in milliseconds.\n speaker: number // Speaker identifier.\n }[]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AudioAnalysis.types.js","sourceRoot":"","sources":["../../src/AudioAnalysis/AudioAnalysis.types.ts"],"names":[],"mappings":"AAAA,sEAAsE","sourcesContent":["// packages/expo-audio-stream/src/AudioAnalysis/AudioAnalysis.types.ts\n\n/**\n * Represents the configuration for decoding audio data.\n */\nexport interface DecodingConfig {\n targetSampleRate?: number\n targetChannels?: number\n targetBitDepth?: number\n normalizeAudio?: boolean\n}\n\n/**\n * Represents various audio features extracted from an audio signal.\n */\nexport interface AudioFeatures {\n energy: number // The infinite integral of the squared signal, representing the overall energy of the audio.\n mfcc: number[] // Mel-frequency cepstral coefficients, describing the short-term power spectrum of a sound.\n rms: number // Root mean square value, indicating the amplitude of the audio signal.\n minAmplitude: number // Minimum amplitude value in the audio signal.\n maxAmplitude: number // Maximum amplitude value in the audio signal.\n zcr: number // Zero-crossing rate, indicating the rate at which the signal changes sign.\n spectralCentroid: number // The center of mass of the spectrum, indicating the brightness of the sound.\n spectralFlatness: number // Measure of the flatness of the spectrum, indicating how noise-like the signal is.\n spectralRolloff: number // The frequency below which a specified percentage (usually 85%) of the total spectral energy lies.\n spectralBandwidth: number // The width of the spectrum, indicating the range of frequencies present.\n chromagram: number[] // Chromagram, representing the 12 different pitch classes of the audio.\n tempo: number // Estimated tempo of the audio signal, measured in beats per minute (BPM).\n hnr: number // Harmonics-to-noise ratio, indicating the proportion of harmonics to noise in the audio signal.\n}\n\n/**\n * Options to specify which audio features to extract.\n */\nexport interface AudioFeaturesOptions {\n energy?: boolean\n mfcc?: boolean\n rms?: boolean\n zcr?: boolean\n spectralCentroid?: boolean\n spectralFlatness?: boolean\n spectralRolloff?: boolean\n spectralBandwidth?: boolean\n chromagram?: boolean\n tempo?: boolean\n hnr?: boolean\n}\n\n/**\n * Represents a single data point in the audio analysis.\n */\nexport interface DataPoint {\n id: number\n amplitude: number\n activeSpeech?: boolean\n dB?: number\n silent?: boolean\n features?: AudioFeatures\n startTime?: number\n endTime?: number\n // start / end position in bytes\n startPosition?: number\n endPosition?: number\n // number of audio samples for this point (samples size depends on bit depth)\n samples?: number\n // TODO: speaker detection\n speaker?: number\n}\n\nexport type AmplitudeAlgorithm = 'peak' | 'rms'\n\n/**\n * Represents the complete data from the audio analysis.\n */\nexport interface AudioAnalysis {\n pointsPerSecond: number // How many consolidated value per second\n durationMs: number // Duration of the audio in milliseconds\n bitDepth: number // Bit depth of the audio\n samples: number // Size of the audio in bytes\n numberOfChannels: number // Number of audio channels\n sampleRate: number // Sample rate of the audio\n dataPoints: DataPoint[] // Array of data points from the analysis.\n amplitudeAlgorithm: AmplitudeAlgorithm // Algorithm used to calculate amplitude values.\n amplitudeRange: {\n min: number\n max: number\n }\n // TODO: speaker detection\n speakerChanges?: {\n timestamp: number // Timestamp of the speaker change in milliseconds.\n speaker: number // Speaker identifier.\n }[]\n}\n\n/**\n * Options for specifying a time range within an audio file.\n */\nexport interface AudioRangeOptions {\n /** Start time in milliseconds */\n startTime?: number\n /** End time in milliseconds */\n endTime?: number\n}\n\n/**\n * Options for generating a quick preview of audio waveform.\n * This is optimized for UI rendering with a specified number of points.\n */\nexport interface PreviewOptions extends AudioRangeOptions {\n /** URI of the audio file to analyze */\n fileUri: string\n /**\n * Total number of points to generate for the preview.\n * @default 100\n */\n numberOfPoints?: number\n /**\n * Algorithm used to calculate amplitude values\n * @default \"rms\"\n */\n algorithm?: AmplitudeAlgorithm\n /**\n * Optional configuration for decoding the audio file.\n * Defaults to:\n * - targetSampleRate: undefined (keep original)\n * - targetChannels: undefined (keep original)\n * - targetBitDepth: 16\n * - normalizeAudio: false\n */\n decodingOptions?: DecodingConfig\n}\n\n/**\n * Represents a simplified preview of audio waveform,\n * optimized for quick visualization.\n */\nexport interface AudioPreview {\n /** Number of data points per second */\n pointsPerSecond: number\n /** Duration of the audio in milliseconds */\n durationMs: number\n /** Range of amplitude values in the preview */\n amplitudeRange: {\n min: number\n max: number\n }\n /** Array of data points representing the waveform */\n dataPoints: DataPoint[]\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConsoleLike } from '../ExpoAudioStream.types';
|
|
2
|
-
import { AmplitudeAlgorithm, AudioAnalysis, AudioFeaturesOptions } from './AudioAnalysis.types';
|
|
2
|
+
import { AmplitudeAlgorithm, AudioAnalysis, AudioFeaturesOptions, AudioPreview, DecodingConfig, PreviewOptions } from './AudioAnalysis.types';
|
|
3
3
|
import { WavFileInfo } from '../utils/getWavFileInfo';
|
|
4
4
|
export interface ExtractAudioAnalysisProps {
|
|
5
5
|
fileUri?: string;
|
|
@@ -17,6 +17,15 @@ export interface ExtractAudioAnalysisProps {
|
|
|
17
17
|
features?: AudioFeaturesOptions;
|
|
18
18
|
featuresExtratorUrl?: string;
|
|
19
19
|
logger?: ConsoleLike;
|
|
20
|
+
decodingOptions?: DecodingConfig;
|
|
20
21
|
}
|
|
22
|
+
export interface ExtractAudioFromAnyFormatProps extends ExtractAudioAnalysisProps {
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
decodingOptions?: DecodingConfig;
|
|
25
|
+
startTime?: number;
|
|
26
|
+
endTime?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function extractAudioFromAnyFormat({ fileUri, arrayBuffer, mimeType, decodingOptions, startTime, endTime, ...restProps }: ExtractAudioFromAnyFormatProps): Promise<AudioAnalysis>;
|
|
21
29
|
export declare const extractAudioAnalysis: ({ fileUri, pointsPerSecond, arrayBuffer, bitDepth, skipWavHeader, durationMs, sampleRate, numberOfChannels, algorithm, features, featuresExtratorUrl, logger, }: ExtractAudioAnalysisProps) => Promise<AudioAnalysis>;
|
|
30
|
+
export declare function extractPreview({ fileUri, numberOfPoints, algorithm, startTime, endTime, decodingOptions, }: PreviewOptions): Promise<AudioPreview>;
|
|
22
31
|
//# sourceMappingURL=extractAudioAnalysis.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractAudioAnalysis.d.ts","sourceRoot":"","sources":["../../src/AudioAnalysis/extractAudioAnalysis.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGtD,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,oBAAoB,
|
|
1
|
+
{"version":3,"file":"extractAudioAnalysis.d.ts","sourceRoot":"","sources":["../../src/AudioAnalysis/extractAudioAnalysis.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGtD,OAAO,EACH,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,cAAc,EACjB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAkB,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAGrE,MAAM,WAAW,yBAAyB;IACtC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,kBAAkB,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,EAAE,oBAAoB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,eAAe,CAAC,EAAE,cAAc,CAAA;CACnC;AAED,MAAM,WAAW,8BACb,SAAQ,yBAAyB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,yBAAyB,CAAC,EAC5C,OAAO,EACP,WAAW,EACX,QAAQ,EACR,eAAe,EACf,SAAS,EACT,OAAO,EACP,GAAG,SAAS,EACf,EAAE,8BAA8B,GAAG,OAAO,CAAC,aAAa,CAAC,CAsJzD;AAED,eAAO,MAAM,oBAAoB,oKAa9B,yBAAyB,KAAG,QAAQ,aAAa,CAsGnD,CAAA;AAED,wBAAsB,cAAc,CAAC,EACjC,OAAO,EACP,cAAc,EACd,SAAiB,EACjB,SAAS,EACT,OAAO,EACP,eAAe,GAClB,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAsCxC"}
|
|
@@ -3,6 +3,128 @@ import { isWeb } from '../constants';
|
|
|
3
3
|
import { convertPCMToFloat32 } from '../utils/convertPCMToFloat32';
|
|
4
4
|
import { getWavFileInfo } from '../utils/getWavFileInfo';
|
|
5
5
|
import { InlineFeaturesExtractor } from '../workers/InlineFeaturesExtractor.web';
|
|
6
|
+
export async function extractAudioFromAnyFormat({ fileUri, arrayBuffer, mimeType, decodingOptions, startTime, endTime, ...restProps }) {
|
|
7
|
+
if (isWeb) {
|
|
8
|
+
try {
|
|
9
|
+
// Get the audio data
|
|
10
|
+
let audioBuffer;
|
|
11
|
+
if (arrayBuffer) {
|
|
12
|
+
audioBuffer = arrayBuffer;
|
|
13
|
+
}
|
|
14
|
+
else if (fileUri) {
|
|
15
|
+
const response = await fetch(fileUri);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
throw new Error(`Failed to fetch fileUri: ${response.statusText}`);
|
|
18
|
+
}
|
|
19
|
+
audioBuffer = await response.arrayBuffer();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
throw new Error('Either arrayBuffer or fileUri must be provided');
|
|
23
|
+
}
|
|
24
|
+
// Create audio context with target sample rate if specified
|
|
25
|
+
const audioContext = new (window.AudioContext ||
|
|
26
|
+
window.webkitAudioContext)({
|
|
27
|
+
sampleRate: decodingOptions?.targetSampleRate,
|
|
28
|
+
});
|
|
29
|
+
// Decode the audio data
|
|
30
|
+
const decodedAudioBuffer = await audioContext.decodeAudioData(audioBuffer);
|
|
31
|
+
// Calculate the actual duration in milliseconds
|
|
32
|
+
const fullDurationMs = decodedAudioBuffer.duration * 1000;
|
|
33
|
+
const effectiveDurationMs = endTime
|
|
34
|
+
? endTime - (startTime || 0)
|
|
35
|
+
: fullDurationMs - (startTime || 0);
|
|
36
|
+
// Create a new buffer for the selected range
|
|
37
|
+
const rangeLength = decodedAudioBuffer.length;
|
|
38
|
+
const rangeBuffer = new AudioBuffer({
|
|
39
|
+
length: rangeLength,
|
|
40
|
+
numberOfChannels: decodedAudioBuffer.numberOfChannels,
|
|
41
|
+
sampleRate: decodedAudioBuffer.sampleRate,
|
|
42
|
+
});
|
|
43
|
+
// Copy the selected range
|
|
44
|
+
for (let channel = 0; channel < decodedAudioBuffer.numberOfChannels; channel++) {
|
|
45
|
+
const channelData = decodedAudioBuffer.getChannelData(channel);
|
|
46
|
+
const rangeData = channelData.slice(0, rangeLength);
|
|
47
|
+
rangeBuffer.copyToChannel(rangeData, channel);
|
|
48
|
+
}
|
|
49
|
+
// Use the range buffer instead of the full buffer
|
|
50
|
+
let processedBuffer = rangeBuffer;
|
|
51
|
+
// Get original properties
|
|
52
|
+
const originalSampleRate = processedBuffer.sampleRate;
|
|
53
|
+
const length = processedBuffer.length;
|
|
54
|
+
// Determine target format
|
|
55
|
+
const targetChannels = decodingOptions?.targetChannels ??
|
|
56
|
+
processedBuffer.numberOfChannels;
|
|
57
|
+
const targetSampleRate = decodingOptions?.targetSampleRate ?? processedBuffer.sampleRate;
|
|
58
|
+
// Create offline context for resampling if needed
|
|
59
|
+
if (targetSampleRate !== originalSampleRate) {
|
|
60
|
+
const offlineCtx = new OfflineAudioContext(targetChannels, (length * targetSampleRate) / originalSampleRate, targetSampleRate);
|
|
61
|
+
const source = offlineCtx.createBufferSource();
|
|
62
|
+
source.buffer = processedBuffer;
|
|
63
|
+
source.connect(offlineCtx.destination);
|
|
64
|
+
source.start();
|
|
65
|
+
processedBuffer = await offlineCtx.startRendering();
|
|
66
|
+
}
|
|
67
|
+
// Convert to the desired format
|
|
68
|
+
const newLength = processedBuffer.length;
|
|
69
|
+
let wavBuffer;
|
|
70
|
+
// Create appropriate buffer based on target bit depth
|
|
71
|
+
switch (decodingOptions?.targetBitDepth) {
|
|
72
|
+
case 16:
|
|
73
|
+
wavBuffer = new Int16Array(newLength * targetChannels);
|
|
74
|
+
break;
|
|
75
|
+
case 8:
|
|
76
|
+
wavBuffer = new Int8Array(newLength * targetChannels);
|
|
77
|
+
break;
|
|
78
|
+
case 32:
|
|
79
|
+
default:
|
|
80
|
+
wavBuffer = new Float32Array(newLength * targetChannels);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
// Interleave channels and handle bit depth conversion
|
|
84
|
+
const numChannels = Math.min(processedBuffer.numberOfChannels, targetChannels);
|
|
85
|
+
for (let channel = 0; channel < numChannels; channel++) {
|
|
86
|
+
const channelData = processedBuffer.getChannelData(channel);
|
|
87
|
+
for (let i = 0; i < newLength; i++) {
|
|
88
|
+
let sample = channelData[i];
|
|
89
|
+
// Normalize if requested
|
|
90
|
+
if (decodingOptions?.normalizeAudio) {
|
|
91
|
+
sample = Math.max(-1, Math.min(1, sample));
|
|
92
|
+
}
|
|
93
|
+
// Convert sample based on target bit depth
|
|
94
|
+
if (decodingOptions?.targetBitDepth === 16) {
|
|
95
|
+
sample = sample * 32767; // Convert to 16-bit range
|
|
96
|
+
}
|
|
97
|
+
else if (decodingOptions?.targetBitDepth === 8) {
|
|
98
|
+
sample = sample * 127; // Convert to 8-bit range
|
|
99
|
+
}
|
|
100
|
+
wavBuffer[i * targetChannels + channel] = sample;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Pass the duration to extractAudioAnalysis
|
|
104
|
+
return await extractAudioAnalysis({
|
|
105
|
+
arrayBuffer: wavBuffer.buffer,
|
|
106
|
+
bitDepth: decodingOptions?.targetBitDepth ?? 32,
|
|
107
|
+
skipWavHeader: true,
|
|
108
|
+
sampleRate: targetSampleRate,
|
|
109
|
+
numberOfChannels: targetChannels,
|
|
110
|
+
durationMs: effectiveDurationMs,
|
|
111
|
+
...restProps,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Failed to process audio:', error);
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// For native platforms, pass through all options
|
|
121
|
+
return await extractAudioAnalysis({
|
|
122
|
+
fileUri,
|
|
123
|
+
decodingOptions,
|
|
124
|
+
...restProps,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
6
128
|
export const extractAudioAnalysis = async ({ fileUri, pointsPerSecond = 20, arrayBuffer, bitDepth, skipWavHeader = true, durationMs, sampleRate, numberOfChannels, algorithm = 'rms', features, featuresExtratorUrl, logger, }) => {
|
|
7
129
|
if (isWeb) {
|
|
8
130
|
if (!arrayBuffer && !fileUri) {
|
|
@@ -83,4 +205,40 @@ export const extractAudioAnalysis = async ({ fileUri, pointsPerSecond = 20, arra
|
|
|
83
205
|
return res;
|
|
84
206
|
}
|
|
85
207
|
};
|
|
208
|
+
export async function extractPreview({ fileUri, numberOfPoints, algorithm = 'rms', startTime, endTime, decodingOptions, }) {
|
|
209
|
+
if (isWeb) {
|
|
210
|
+
// For web, we can reuse the existing extractAudioFromAnyFormat with modified parameters
|
|
211
|
+
const analysis = await extractAudioFromAnyFormat({
|
|
212
|
+
fileUri,
|
|
213
|
+
algorithm,
|
|
214
|
+
decodingOptions,
|
|
215
|
+
startTime, // Pass startTime
|
|
216
|
+
endTime, // Pass endTime
|
|
217
|
+
pointsPerSecond: (numberOfPoints ?? 100) /
|
|
218
|
+
((endTime ? endTime - (startTime || 0) : 1000) / 1000), // Adjust points per second calculation
|
|
219
|
+
});
|
|
220
|
+
// Convert AudioAnalysis to AudioPreview format and adjust duration
|
|
221
|
+
return {
|
|
222
|
+
pointsPerSecond: analysis.pointsPerSecond,
|
|
223
|
+
durationMs: endTime
|
|
224
|
+
? endTime - (startTime || 0)
|
|
225
|
+
: analysis.durationMs, // Use range duration if specified
|
|
226
|
+
amplitudeRange: analysis.amplitudeRange,
|
|
227
|
+
dataPoints: analysis.dataPoints.map((point) => ({
|
|
228
|
+
id: point.id,
|
|
229
|
+
amplitude: point.amplitude,
|
|
230
|
+
startTime: point.startTime,
|
|
231
|
+
endTime: point.endTime,
|
|
232
|
+
})),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return await ExpoAudioStreamModule.extractPreview({
|
|
236
|
+
fileUri,
|
|
237
|
+
numberOfPoints,
|
|
238
|
+
algorithm,
|
|
239
|
+
startTime,
|
|
240
|
+
endTime,
|
|
241
|
+
decodingOptions,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
86
244
|
//# sourceMappingURL=extractAudioAnalysis.js.map
|