sezo-audio-engine 0.0.2
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/LICENSE +21 -0
- package/README.md +51 -0
- package/android/build.gradle +44 -0
- package/android/cpp/CMakeLists.txt +23 -0
- package/android/cpp/NativeBridge.cpp +6 -0
- package/android/settings.gradle +12 -0
- package/android/src/main/java/expo/modules/audioengine/ExpoAudioEngineModule.kt +549 -0
- package/dist/AudioEngineModule.d.ts +2 -0
- package/dist/AudioEngineModule.js +46 -0
- package/dist/AudioEngineModule.types.d.ts +108 -0
- package/dist/AudioEngineModule.types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioEngine/AudioEngineConfig.swift +13 -0
- package/ios/AudioEngine/AudioSessionManager.swift +61 -0
- package/ios/AudioEngine/AudioTrack.swift +72 -0
- package/ios/AudioEngine/NativeAudioEngine.swift +1180 -0
- package/ios/ExpoAudioEngine.podspec +26 -0
- package/ios/ExpoAudioEngineModule.swift +156 -0
- package/package.json +60 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
package expo.modules.audioengine
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
import com.sezo.audioengine.AudioEngine
|
|
5
|
+
import expo.modules.kotlin.Promise
|
|
6
|
+
import expo.modules.kotlin.modules.Module
|
|
7
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
8
|
+
import java.util.Collections
|
|
9
|
+
|
|
10
|
+
class ExpoAudioEngineModule : Module() {
|
|
11
|
+
private var audioEngine: AudioEngine? = null
|
|
12
|
+
private val loadedTrackIds = mutableSetOf<String>()
|
|
13
|
+
private val pendingExtractions = Collections.synchronizedMap(mutableMapOf<Long, PendingExtraction>())
|
|
14
|
+
private val progressLogState = Collections.synchronizedMap(mutableMapOf<Long, Float>())
|
|
15
|
+
private var lastExtractionJobId: Long? = null
|
|
16
|
+
|
|
17
|
+
private data class PendingExtraction(
|
|
18
|
+
val promise: Promise,
|
|
19
|
+
val trackId: String?,
|
|
20
|
+
val outputPath: String,
|
|
21
|
+
val format: String,
|
|
22
|
+
val bitrate: Int,
|
|
23
|
+
val operation: String
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
override fun definition() = ModuleDefinition {
|
|
27
|
+
Name("ExpoAudioEngineModule")
|
|
28
|
+
|
|
29
|
+
AsyncFunction("initialize") { config: Map<String, Any?> ->
|
|
30
|
+
Log.d(TAG, "Initialize called with config: $config")
|
|
31
|
+
|
|
32
|
+
val sampleRate = (config["sampleRate"] as? Number)?.toInt() ?: 44100
|
|
33
|
+
val maxTracks = (config["maxTracks"] as? Number)?.toInt() ?: 8
|
|
34
|
+
|
|
35
|
+
audioEngine = AudioEngine()
|
|
36
|
+
val success = audioEngine?.initialize(sampleRate, maxTracks) ?: false
|
|
37
|
+
|
|
38
|
+
if (!success) {
|
|
39
|
+
throw Exception("Failed to initialize audio engine")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Log.d(TAG, "Audio engine initialized successfully")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
AsyncFunction("release") {
|
|
46
|
+
Log.d(TAG, "Release called")
|
|
47
|
+
audioEngine?.let { engine ->
|
|
48
|
+
synchronized(pendingExtractions) {
|
|
49
|
+
pendingExtractions.keys.forEach { engine.cancelExtraction(it) }
|
|
50
|
+
pendingExtractions.clear()
|
|
51
|
+
}
|
|
52
|
+
progressLogState.clear()
|
|
53
|
+
engine.setExtractionProgressListener(null)
|
|
54
|
+
engine.setExtractionCompletionListener(null)
|
|
55
|
+
engine.release()
|
|
56
|
+
engine.destroy()
|
|
57
|
+
}
|
|
58
|
+
audioEngine = null
|
|
59
|
+
loadedTrackIds.clear()
|
|
60
|
+
Log.d(TAG, "Audio engine released")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
AsyncFunction("loadTracks") { tracks: List<Map<String, Any?>> ->
|
|
64
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
65
|
+
|
|
66
|
+
tracks.forEach { track ->
|
|
67
|
+
val id = track["id"] as? String ?: throw Exception("Missing track id")
|
|
68
|
+
val uri = track["uri"] as? String ?: throw Exception("Missing track uri")
|
|
69
|
+
val startTimeMs = (track["startTimeMs"] as? Number)?.toDouble() ?: 0.0
|
|
70
|
+
|
|
71
|
+
Log.d(TAG, "Loading track: id=$id, uri=$uri")
|
|
72
|
+
val filePath = convertUriToPath(uri)
|
|
73
|
+
|
|
74
|
+
if (!engine.loadTrack(id, filePath, startTimeMs)) {
|
|
75
|
+
throw Exception("Failed to load track: $id from $filePath")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
loadedTrackIds.add(id)
|
|
79
|
+
Log.d(TAG, "Track loaded successfully: $id")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
AsyncFunction("unloadTrack") { trackId: String ->
|
|
84
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
85
|
+
|
|
86
|
+
if (!engine.unloadTrack(trackId)) {
|
|
87
|
+
throw Exception("Failed to unload track: $trackId")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
loadedTrackIds.remove(trackId)
|
|
91
|
+
Log.d(TAG, "Track unloaded: $trackId")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
AsyncFunction("unloadAllTracks") {
|
|
95
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
96
|
+
engine.unloadAllTracks()
|
|
97
|
+
loadedTrackIds.clear()
|
|
98
|
+
Log.d(TAG, "All tracks unloaded")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Function("getLoadedTracks") {
|
|
102
|
+
loadedTrackIds.map { mapOf("id" to it) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Function("play") {
|
|
106
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
107
|
+
engine.play()
|
|
108
|
+
Log.d(TAG, "Playback started")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Function("pause") {
|
|
112
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
113
|
+
engine.pause()
|
|
114
|
+
Log.d(TAG, "Playback paused")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Function("stop") {
|
|
118
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
119
|
+
engine.stop()
|
|
120
|
+
Log.d(TAG, "Playback stopped")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Function("seek") { positionMs: Double ->
|
|
124
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
125
|
+
engine.seek(positionMs)
|
|
126
|
+
Log.d(TAG, "Seeked to position: $positionMs ms")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Function("isPlaying") {
|
|
130
|
+
audioEngine?.isPlaying() ?: false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Function("getCurrentPosition") {
|
|
134
|
+
audioEngine?.getCurrentPosition() ?: 0.0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Function("getDuration") {
|
|
138
|
+
audioEngine?.getDuration() ?: 0.0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Function("setTrackVolume") { trackId: String, volume: Double ->
|
|
142
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
143
|
+
engine.setTrackVolume(trackId, volume.toFloat())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Function("setTrackMuted") { trackId: String, muted: Boolean ->
|
|
147
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
148
|
+
engine.setTrackMuted(trackId, muted)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Function("setTrackSolo") { trackId: String, solo: Boolean ->
|
|
152
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
153
|
+
engine.setTrackSolo(trackId, solo)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
Function("setTrackPan") { trackId: String, pan: Double ->
|
|
157
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
158
|
+
engine.setTrackPan(trackId, pan.toFloat())
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
Function("setMasterVolume") { volume: Double ->
|
|
162
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
163
|
+
engine.setMasterVolume(volume.toFloat())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
Function("getMasterVolume") {
|
|
167
|
+
audioEngine?.getMasterVolume()?.toDouble() ?: 1.0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Function("setPitch") { semitones: Double ->
|
|
171
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
172
|
+
engine.setPitch(semitones.toFloat())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Function("getPitch") {
|
|
176
|
+
audioEngine?.getPitch()?.toDouble() ?: 0.0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Function("setSpeed") { rate: Double ->
|
|
180
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
181
|
+
engine.setSpeed(rate.toFloat())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
Function("getSpeed") {
|
|
185
|
+
audioEngine?.getSpeed()?.toDouble() ?: 1.0
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Function("setTempoAndPitch") { tempo: Double, pitch: Double ->
|
|
189
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
190
|
+
engine.setSpeed(tempo.toFloat())
|
|
191
|
+
engine.setPitch(pitch.toFloat())
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Function("setTrackPitch") { trackId: String, semitones: Double ->
|
|
195
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
196
|
+
engine.setTrackPitch(trackId, semitones.toFloat())
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Function("getTrackPitch") { trackId: String ->
|
|
200
|
+
audioEngine?.getTrackPitch(trackId)?.toDouble() ?: 0.0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
Function("setTrackSpeed") { trackId: String, rate: Double ->
|
|
204
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
205
|
+
engine.setTrackSpeed(trackId, rate.toFloat())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Function("getTrackSpeed") { trackId: String ->
|
|
209
|
+
audioEngine?.getTrackSpeed(trackId)?.toDouble() ?: 1.0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
AsyncFunction("startRecording") { config: Map<String, Any?>? ->
|
|
213
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
214
|
+
|
|
215
|
+
val sampleRate = (config?.get("sampleRate") as? Number)?.toInt() ?: 44100
|
|
216
|
+
val channels = (config?.get("channels") as? Number)?.toInt() ?: 1
|
|
217
|
+
val format = (config?.get("format") as? String) ?: "aac"
|
|
218
|
+
val bitrate = (config?.get("bitrate") as? Number)?.toInt() ?: 128000
|
|
219
|
+
val quality = config?.get("quality") as? String
|
|
220
|
+
|
|
221
|
+
val actualBitrate = when (quality) {
|
|
222
|
+
"low" -> 64000
|
|
223
|
+
"medium" -> 128000
|
|
224
|
+
"high" -> 192000
|
|
225
|
+
else -> bitrate
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
val outputDir = getCacheDir()
|
|
229
|
+
val fileName = "recording_${System.currentTimeMillis()}.$format"
|
|
230
|
+
val outputPath = "$outputDir/$fileName"
|
|
231
|
+
|
|
232
|
+
Log.d(TAG, "Starting recording: $outputPath (format=$format, bitrate=$actualBitrate)")
|
|
233
|
+
|
|
234
|
+
val success = engine.startRecording(
|
|
235
|
+
outputPath = outputPath,
|
|
236
|
+
sampleRate = sampleRate,
|
|
237
|
+
channels = channels,
|
|
238
|
+
format = format,
|
|
239
|
+
bitrate = actualBitrate,
|
|
240
|
+
bitsPerSample = 16
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if (!success) {
|
|
244
|
+
throw Exception("Failed to start recording")
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
Log.d(TAG, "Recording started successfully")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
AsyncFunction("stopRecording") {
|
|
251
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
252
|
+
|
|
253
|
+
Log.d(TAG, "Stopping recording")
|
|
254
|
+
val result = engine.stopRecording()
|
|
255
|
+
|
|
256
|
+
if (!result.success) {
|
|
257
|
+
throw Exception("Failed to stop recording: ${result.errorMessage}")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
Log.d(TAG, "Recording stopped: ${result.fileSize} bytes, ${result.durationSamples} samples")
|
|
261
|
+
|
|
262
|
+
sendEvent(
|
|
263
|
+
"recordingStopped",
|
|
264
|
+
mapOf(
|
|
265
|
+
"uri" to "file://${result.outputPath}",
|
|
266
|
+
"duration" to (result.durationSamples / 44.1).toInt(),
|
|
267
|
+
"startTimeMs" to result.startTimeMs,
|
|
268
|
+
"startTimeSamples" to result.startTimeSamples,
|
|
269
|
+
"sampleRate" to 44100,
|
|
270
|
+
"channels" to 1,
|
|
271
|
+
"format" to "aac",
|
|
272
|
+
"fileSize" to result.fileSize
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
mapOf(
|
|
277
|
+
"uri" to "file://${result.outputPath}",
|
|
278
|
+
"duration" to (result.durationSamples / 44.1).toInt(),
|
|
279
|
+
"startTimeMs" to result.startTimeMs,
|
|
280
|
+
"startTimeSamples" to result.startTimeSamples,
|
|
281
|
+
"sampleRate" to 44100,
|
|
282
|
+
"channels" to 1,
|
|
283
|
+
"format" to "aac",
|
|
284
|
+
"fileSize" to result.fileSize
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Function("isRecording") {
|
|
289
|
+
audioEngine?.isRecording() ?: false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Function("setRecordingVolume") { volume: Double ->
|
|
293
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
294
|
+
engine.setRecordingVolume(volume.toFloat())
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
AsyncFunction("extractTrack") { trackId: String, config: Map<String, Any?>?, promise: Promise ->
|
|
298
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
299
|
+
|
|
300
|
+
val format = (config?.get("format") as? String) ?: "wav"
|
|
301
|
+
val bitrate = (config?.get("bitrate") as? Number)?.toInt() ?: 128000
|
|
302
|
+
val bitsPerSample = (config?.get("bitsPerSample") as? Number)?.toInt() ?: 16
|
|
303
|
+
val includeEffects = (config?.get("includeEffects") as? Boolean) ?: true
|
|
304
|
+
val outputDir = (config?.get("outputDir") as? String) ?: getCacheDir()
|
|
305
|
+
|
|
306
|
+
val fileName = "track_${trackId}_${System.currentTimeMillis()}.$format"
|
|
307
|
+
val outputPath = "$outputDir/$fileName"
|
|
308
|
+
|
|
309
|
+
Log.d(TAG, "Extracting track: $trackId to $outputPath (format=$format, bitrate=$bitrate)")
|
|
310
|
+
|
|
311
|
+
val jobId = engine.startExtractTrack(
|
|
312
|
+
trackId = trackId,
|
|
313
|
+
outputPath = outputPath,
|
|
314
|
+
format = format,
|
|
315
|
+
bitrate = bitrate,
|
|
316
|
+
bitsPerSample = bitsPerSample,
|
|
317
|
+
includeEffects = includeEffects
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if (jobId <= 0L) {
|
|
321
|
+
promise.reject("EXTRACTION_FAILED", "Failed to start extraction", null)
|
|
322
|
+
return@AsyncFunction
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
pendingExtractions[jobId] = PendingExtraction(
|
|
326
|
+
promise = promise,
|
|
327
|
+
trackId = trackId,
|
|
328
|
+
outputPath = outputPath,
|
|
329
|
+
format = format,
|
|
330
|
+
bitrate = bitrate,
|
|
331
|
+
operation = "track"
|
|
332
|
+
)
|
|
333
|
+
lastExtractionJobId = jobId
|
|
334
|
+
attachExtractionListeners(engine)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
AsyncFunction("extractAllTracks") { config: Map<String, Any?>?, promise: Promise ->
|
|
338
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
339
|
+
|
|
340
|
+
val format = (config?.get("format") as? String) ?: "wav"
|
|
341
|
+
val bitrate = (config?.get("bitrate") as? Number)?.toInt() ?: 128000
|
|
342
|
+
val bitsPerSample = (config?.get("bitsPerSample") as? Number)?.toInt() ?: 16
|
|
343
|
+
val includeEffects = (config?.get("includeEffects") as? Boolean) ?: true
|
|
344
|
+
val outputDir = (config?.get("outputDir") as? String) ?: getCacheDir()
|
|
345
|
+
|
|
346
|
+
val fileName = "mixed_tracks_${System.currentTimeMillis()}.$format"
|
|
347
|
+
val outputPath = "$outputDir/$fileName"
|
|
348
|
+
|
|
349
|
+
Log.d(TAG, "Extracting all tracks mixed to $outputPath (format=$format, bitrate=$bitrate)")
|
|
350
|
+
|
|
351
|
+
val jobId = engine.startExtractAllTracks(
|
|
352
|
+
outputPath = outputPath,
|
|
353
|
+
format = format,
|
|
354
|
+
bitrate = bitrate,
|
|
355
|
+
bitsPerSample = bitsPerSample,
|
|
356
|
+
includeEffects = includeEffects
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if (jobId <= 0L) {
|
|
360
|
+
promise.reject("EXTRACTION_FAILED", "Failed to start extraction", null)
|
|
361
|
+
return@AsyncFunction
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
pendingExtractions[jobId] = PendingExtraction(
|
|
365
|
+
promise = promise,
|
|
366
|
+
trackId = null,
|
|
367
|
+
outputPath = outputPath,
|
|
368
|
+
format = format,
|
|
369
|
+
bitrate = bitrate,
|
|
370
|
+
operation = "mix"
|
|
371
|
+
)
|
|
372
|
+
lastExtractionJobId = jobId
|
|
373
|
+
attachExtractionListeners(engine)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Function("cancelExtraction") { jobId: Double? ->
|
|
377
|
+
val engine = audioEngine ?: throw Exception("Engine not initialized")
|
|
378
|
+
val resolvedJobId = jobId?.toLong() ?: lastExtractionJobId
|
|
379
|
+
if (resolvedJobId == null) {
|
|
380
|
+
false
|
|
381
|
+
} else {
|
|
382
|
+
engine.cancelExtraction(resolvedJobId)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
Function("getInputLevel") {
|
|
387
|
+
audioEngine?.getInputLevel()?.toDouble() ?: 0.0
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
Function("getOutputLevel") { 0.0 }
|
|
391
|
+
Function("getTrackLevel") { _trackId: String -> 0.0 }
|
|
392
|
+
|
|
393
|
+
AsyncFunction("enableBackgroundPlayback") { _metadata: Map<String, Any?> -> }
|
|
394
|
+
Function("updateNowPlayingInfo") { _metadata: Map<String, Any?> -> }
|
|
395
|
+
AsyncFunction("disableBackgroundPlayback") { }
|
|
396
|
+
|
|
397
|
+
Events(
|
|
398
|
+
"playbackStateChange",
|
|
399
|
+
"positionUpdate",
|
|
400
|
+
"playbackComplete",
|
|
401
|
+
"trackLoaded",
|
|
402
|
+
"trackUnloaded",
|
|
403
|
+
"recordingStarted",
|
|
404
|
+
"recordingStopped",
|
|
405
|
+
"extractionProgress",
|
|
406
|
+
"extractionComplete",
|
|
407
|
+
"error"
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private fun attachExtractionListeners(engine: AudioEngine) {
|
|
412
|
+
if (pendingExtractions.isEmpty()) {
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
engine.setExtractionProgressListener { jobId, progress ->
|
|
417
|
+
val pending = pendingExtractions[jobId] ?: return@setExtractionProgressListener
|
|
418
|
+
val clamped = progress.coerceIn(0.0f, 1.0f)
|
|
419
|
+
val lastLogged = progressLogState[jobId] ?: -1.0f
|
|
420
|
+
if (clamped >= 1.0f || clamped - lastLogged >= 0.05f) {
|
|
421
|
+
progressLogState[jobId] = clamped
|
|
422
|
+
Log.d(TAG, "Extraction progress job=$jobId op=${pending.operation} progress=$clamped")
|
|
423
|
+
}
|
|
424
|
+
sendEvent(
|
|
425
|
+
"extractionProgress",
|
|
426
|
+
mapOf(
|
|
427
|
+
"jobId" to jobId,
|
|
428
|
+
"progress" to clamped.toDouble(),
|
|
429
|
+
"trackId" to pending.trackId,
|
|
430
|
+
"outputPath" to pending.outputPath,
|
|
431
|
+
"format" to pending.format,
|
|
432
|
+
"operation" to pending.operation
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
engine.setExtractionCompletionListener { jobId, result ->
|
|
438
|
+
val pending = pendingExtractions.remove(jobId)
|
|
439
|
+
progressLogState.remove(jobId)
|
|
440
|
+
if (pending == null) {
|
|
441
|
+
return@setExtractionCompletionListener
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (!result.success) {
|
|
445
|
+
Log.e(TAG, "Extraction failed for job=$jobId: ${result.errorMessage}")
|
|
446
|
+
sendEvent(
|
|
447
|
+
"extractionComplete",
|
|
448
|
+
mapOf(
|
|
449
|
+
"success" to false,
|
|
450
|
+
"jobId" to jobId,
|
|
451
|
+
"trackId" to pending.trackId,
|
|
452
|
+
"outputPath" to pending.outputPath,
|
|
453
|
+
"format" to pending.format,
|
|
454
|
+
"bitrate" to pending.bitrate,
|
|
455
|
+
"errorMessage" to (result.errorMessage ?: "Unknown error"),
|
|
456
|
+
"operation" to pending.operation
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
val code = if (result.errorMessage == "Extraction cancelled") {
|
|
460
|
+
"EXTRACTION_CANCELLED"
|
|
461
|
+
} else {
|
|
462
|
+
"EXTRACTION_FAILED"
|
|
463
|
+
}
|
|
464
|
+
pending.promise.reject(code, result.errorMessage, null)
|
|
465
|
+
} else {
|
|
466
|
+
Log.d(TAG, "Extraction successful: ${result.fileSize} bytes, ${result.durationSamples} samples")
|
|
467
|
+
Log.d(TAG, "Extraction complete event (${pending.operation})")
|
|
468
|
+
|
|
469
|
+
sendEvent(
|
|
470
|
+
"extractionComplete",
|
|
471
|
+
mapOf(
|
|
472
|
+
"success" to true,
|
|
473
|
+
"jobId" to jobId,
|
|
474
|
+
"trackId" to pending.trackId,
|
|
475
|
+
"uri" to "file://${result.outputPath}",
|
|
476
|
+
"outputPath" to result.outputPath,
|
|
477
|
+
"duration" to (result.durationSamples / 44.1).toInt(),
|
|
478
|
+
"format" to pending.format,
|
|
479
|
+
"fileSize" to result.fileSize,
|
|
480
|
+
"bitrate" to pending.bitrate,
|
|
481
|
+
"operation" to pending.operation
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
val response = if (pending.operation == "mix") {
|
|
486
|
+
listOf(
|
|
487
|
+
mapOf(
|
|
488
|
+
"uri" to "file://${result.outputPath}",
|
|
489
|
+
"duration" to (result.durationSamples / 44.1).toInt(),
|
|
490
|
+
"format" to pending.format,
|
|
491
|
+
"fileSize" to result.fileSize,
|
|
492
|
+
"bitrate" to pending.bitrate
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
} else {
|
|
496
|
+
mapOf(
|
|
497
|
+
"trackId" to pending.trackId,
|
|
498
|
+
"uri" to "file://${result.outputPath}",
|
|
499
|
+
"duration" to (result.durationSamples / 44.1).toInt(),
|
|
500
|
+
"format" to pending.format,
|
|
501
|
+
"fileSize" to result.fileSize,
|
|
502
|
+
"bitrate" to pending.bitrate
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
pending.promise.resolve(response)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (pendingExtractions.isEmpty()) {
|
|
510
|
+
engine.setExtractionProgressListener(null)
|
|
511
|
+
engine.setExtractionCompletionListener(null)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private fun getCacheDir(): String {
|
|
517
|
+
return appContext.reactContext?.cacheDir?.absolutePath ?: "/tmp"
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private fun convertUriToPath(uri: String): String {
|
|
521
|
+
return when {
|
|
522
|
+
uri.startsWith("file://") -> {
|
|
523
|
+
// Remove file:// prefix
|
|
524
|
+
uri.substring(7)
|
|
525
|
+
}
|
|
526
|
+
uri.startsWith("/") -> {
|
|
527
|
+
// Already an absolute path
|
|
528
|
+
uri
|
|
529
|
+
}
|
|
530
|
+
uri.startsWith("content://") -> {
|
|
531
|
+
// TODO: Handle content:// URIs with ContentResolver
|
|
532
|
+
// For now, throw an error - this will be implemented when needed
|
|
533
|
+
throw Exception("content:// URIs not yet supported. Use file:// or absolute paths.")
|
|
534
|
+
}
|
|
535
|
+
uri.startsWith("asset://") -> {
|
|
536
|
+
// TODO: Handle asset:// URIs by copying to temp file
|
|
537
|
+
// For now, throw an error - this will be implemented when needed
|
|
538
|
+
throw Exception("asset:// URIs not yet supported. Use file:// or absolute paths.")
|
|
539
|
+
}
|
|
540
|
+
else -> {
|
|
541
|
+
throw Exception("Unsupported URI scheme: $uri. Use file:// or absolute paths.")
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
companion object {
|
|
547
|
+
private const val TAG = "ExpoAudioEngine"
|
|
548
|
+
}
|
|
549
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
const NativeAudioEngineModule = requireNativeModule('ExpoAudioEngineModule');
|
|
3
|
+
export const AudioEngineModule = {
|
|
4
|
+
initialize: (config) => NativeAudioEngineModule.initialize(config),
|
|
5
|
+
release: () => NativeAudioEngineModule.release(),
|
|
6
|
+
loadTracks: (tracks) => NativeAudioEngineModule.loadTracks(tracks),
|
|
7
|
+
unloadTrack: (trackId) => NativeAudioEngineModule.unloadTrack(trackId),
|
|
8
|
+
unloadAllTracks: () => NativeAudioEngineModule.unloadAllTracks(),
|
|
9
|
+
getLoadedTracks: () => NativeAudioEngineModule.getLoadedTracks(),
|
|
10
|
+
play: () => NativeAudioEngineModule.play(),
|
|
11
|
+
pause: () => NativeAudioEngineModule.pause(),
|
|
12
|
+
stop: () => NativeAudioEngineModule.stop(),
|
|
13
|
+
seek: (positionMs) => NativeAudioEngineModule.seek(positionMs),
|
|
14
|
+
isPlaying: () => NativeAudioEngineModule.isPlaying(),
|
|
15
|
+
getCurrentPosition: () => NativeAudioEngineModule.getCurrentPosition(),
|
|
16
|
+
getDuration: () => NativeAudioEngineModule.getDuration(),
|
|
17
|
+
setTrackVolume: (trackId, volume) => NativeAudioEngineModule.setTrackVolume(trackId, volume),
|
|
18
|
+
setTrackMuted: (trackId, muted) => NativeAudioEngineModule.setTrackMuted(trackId, muted),
|
|
19
|
+
setTrackSolo: (trackId, solo) => NativeAudioEngineModule.setTrackSolo(trackId, solo),
|
|
20
|
+
setTrackPan: (trackId, pan) => NativeAudioEngineModule.setTrackPan(trackId, pan),
|
|
21
|
+
setTrackPitch: (trackId, semitones) => NativeAudioEngineModule.setTrackPitch(trackId, semitones),
|
|
22
|
+
getTrackPitch: (trackId) => NativeAudioEngineModule.getTrackPitch(trackId),
|
|
23
|
+
setTrackSpeed: (trackId, rate) => NativeAudioEngineModule.setTrackSpeed(trackId, rate),
|
|
24
|
+
getTrackSpeed: (trackId) => NativeAudioEngineModule.getTrackSpeed(trackId),
|
|
25
|
+
setMasterVolume: (volume) => NativeAudioEngineModule.setMasterVolume(volume),
|
|
26
|
+
getMasterVolume: () => NativeAudioEngineModule.getMasterVolume(),
|
|
27
|
+
setPitch: (semitones) => NativeAudioEngineModule.setPitch(semitones),
|
|
28
|
+
getPitch: () => NativeAudioEngineModule.getPitch(),
|
|
29
|
+
setSpeed: (rate) => NativeAudioEngineModule.setSpeed(rate),
|
|
30
|
+
getSpeed: () => NativeAudioEngineModule.getSpeed(),
|
|
31
|
+
setTempoAndPitch: (tempo, pitch) => NativeAudioEngineModule.setTempoAndPitch(tempo, pitch),
|
|
32
|
+
startRecording: (config) => NativeAudioEngineModule.startRecording(config),
|
|
33
|
+
stopRecording: () => NativeAudioEngineModule.stopRecording(),
|
|
34
|
+
isRecording: () => NativeAudioEngineModule.isRecording(),
|
|
35
|
+
setRecordingVolume: (volume) => NativeAudioEngineModule.setRecordingVolume(volume),
|
|
36
|
+
extractTrack: (trackId, config) => NativeAudioEngineModule.extractTrack(trackId, config),
|
|
37
|
+
extractAllTracks: (config) => NativeAudioEngineModule.extractAllTracks(config),
|
|
38
|
+
cancelExtraction: (jobId) => NativeAudioEngineModule.cancelExtraction(jobId),
|
|
39
|
+
getInputLevel: () => NativeAudioEngineModule.getInputLevel(),
|
|
40
|
+
getOutputLevel: () => NativeAudioEngineModule.getOutputLevel(),
|
|
41
|
+
getTrackLevel: (trackId) => NativeAudioEngineModule.getTrackLevel(trackId),
|
|
42
|
+
enableBackgroundPlayback: (metadata) => NativeAudioEngineModule.enableBackgroundPlayback(metadata),
|
|
43
|
+
updateNowPlayingInfo: (metadata) => NativeAudioEngineModule.updateNowPlayingInfo(metadata),
|
|
44
|
+
disableBackgroundPlayback: () => NativeAudioEngineModule.disableBackgroundPlayback(),
|
|
45
|
+
addListener: (event, callback) => NativeAudioEngineModule.addListener(event, callback)
|
|
46
|
+
};
|