react-native-audio-concat 0.2.3 → 0.3.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/AudioConcat.podspec +2 -21
- package/README.md +3 -7
- package/android/build.gradle +2 -54
- package/android/src/main/java/com/{margelo/nitro/audioconcat/AudioConcat.kt → audioconcat/AudioConcatModule.kt} +73 -31
- package/android/src/main/java/com/audioconcat/AudioConcatPackage.kt +33 -0
- package/ios/AudioConcat.h +5 -0
- package/ios/AudioConcat.mm +104 -0
- package/lib/module/NativeAudioConcat.js +5 -0
- package/lib/module/NativeAudioConcat.js.map +1 -0
- package/lib/module/index.js +2 -28
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeAudioConcat.d.ts +12 -0
- package/lib/typescript/src/NativeAudioConcat.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -27
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +14 -18
- package/src/NativeAudioConcat.ts +12 -0
- package/src/index.tsx +4 -32
- package/android/CMakeLists.txt +0 -24
- package/android/src/main/cpp/cpp-adapter.cpp +0 -6
- package/android/src/main/java/com/margelo/nitro/audioconcat/AudioConcatPackage.kt +0 -22
- package/ios/AudioConcat.swift +0 -75
- package/lib/module/AudioConcat.nitro.js +0 -4
- package/lib/module/AudioConcat.nitro.js.map +0 -1
- package/lib/typescript/src/AudioConcat.nitro.d.ts +0 -16
- package/lib/typescript/src/AudioConcat.nitro.d.ts.map +0 -1
- package/nitro.json +0 -17
- package/nitrogen/generated/android/audioconcat+autolinking.cmake +0 -82
- package/nitrogen/generated/android/audioconcat+autolinking.gradle +0 -27
- package/nitrogen/generated/android/audioconcatOnLoad.cpp +0 -44
- package/nitrogen/generated/android/audioconcatOnLoad.hpp +0 -25
- package/nitrogen/generated/android/c++/JAudioData.hpp +0 -53
- package/nitrogen/generated/android/c++/JAudioDataOrSilence.cpp +0 -26
- package/nitrogen/generated/android/c++/JAudioDataOrSilence.hpp +0 -72
- package/nitrogen/generated/android/c++/JHybridAudioConcatSpec.cpp +0 -77
- package/nitrogen/generated/android/c++/JHybridAudioConcatSpec.hpp +0 -64
- package/nitrogen/generated/android/c++/JSilentData.hpp +0 -53
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audioconcat/AudioData.kt +0 -29
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audioconcat/AudioDataOrSilence.kt +0 -42
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audioconcat/HybridAudioConcatSpec.kt +0 -52
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audioconcat/SilentData.kt +0 -29
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/audioconcat/audioconcatOnLoad.kt +0 -35
- package/nitrogen/generated/ios/AudioConcat+autolinking.rb +0 -60
- package/nitrogen/generated/ios/AudioConcat-Swift-Cxx-Bridge.cpp +0 -48
- package/nitrogen/generated/ios/AudioConcat-Swift-Cxx-Bridge.hpp +0 -160
- package/nitrogen/generated/ios/AudioConcat-Swift-Cxx-Umbrella.hpp +0 -53
- package/nitrogen/generated/ios/AudioConcatAutolinking.mm +0 -33
- package/nitrogen/generated/ios/AudioConcatAutolinking.swift +0 -25
- package/nitrogen/generated/ios/c++/HybridAudioConcatSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridAudioConcatSpecSwift.hpp +0 -81
- package/nitrogen/generated/ios/swift/AudioData.swift +0 -35
- package/nitrogen/generated/ios/swift/AudioDataOrSilence.swift +0 -18
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -47
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +0 -47
- package/nitrogen/generated/ios/swift/HybridAudioConcatSpec.swift +0 -49
- package/nitrogen/generated/ios/swift/HybridAudioConcatSpec_cxx.swift +0 -142
- package/nitrogen/generated/ios/swift/SilentData.swift +0 -35
- package/nitrogen/generated/shared/c++/AudioData.hpp +0 -67
- package/nitrogen/generated/shared/c++/HybridAudioConcatSpec.cpp +0 -21
- package/nitrogen/generated/shared/c++/HybridAudioConcatSpec.hpp +0 -70
- package/nitrogen/generated/shared/c++/SilentData.hpp +0 -67
- package/src/AudioConcat.nitro.ts +0 -19
package/AudioConcat.podspec
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
-
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'
|
|
5
4
|
|
|
6
5
|
Pod::Spec.new do |s|
|
|
7
6
|
s.name = "AudioConcat"
|
|
@@ -14,27 +13,9 @@ Pod::Spec.new do |s|
|
|
|
14
13
|
s.platforms = { :ios => min_ios_version_supported }
|
|
15
14
|
s.source = { :git => "https://github.com/felixchen-dev/react-native-audio-concat.git", :tag => "#{s.version}" }
|
|
16
15
|
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
17
18
|
|
|
18
|
-
s.source_files = [
|
|
19
|
-
"ios/**/*.{swift}",
|
|
20
|
-
"ios/**/*.{m,mm}",
|
|
21
|
-
"cpp/**/*.{hpp,cpp}",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
s.pod_target_xcconfig = {
|
|
25
|
-
"HEADER_SEARCH_PATHS" => [
|
|
26
|
-
"${PODS_ROOT}/RCT-Folly",
|
|
27
|
-
],
|
|
28
|
-
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) FOLLY_NO_CONFIG FOLLY_CFG_NO_COROUTINES",
|
|
29
|
-
"OTHER_CPLUSPLUSFLAGS" => folly_compiler_flags,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
s.dependency 'React-jsi'
|
|
34
|
-
s.dependency 'React-callinvoker'
|
|
35
|
-
|
|
36
|
-
load 'nitrogen/generated/ios/AudioConcat+autolinking.rb'
|
|
37
|
-
add_nitrogen_files(s)
|
|
38
19
|
|
|
39
20
|
install_modules_dependencies(s)
|
|
40
21
|
end
|
package/README.md
CHANGED
|
@@ -6,17 +6,14 @@ Concatenate audio files and silence periods into a single audio file for React N
|
|
|
6
6
|
|
|
7
7
|
- ✅ Concat multiple audio files with silence periods
|
|
8
8
|
- ✅ Support for iOS and Android
|
|
9
|
-
- ✅ High performance using [Nitro Modules](https://nitro.margelo.com/)
|
|
10
9
|
- ✅ Output in M4A format
|
|
11
10
|
|
|
12
11
|
## Installation
|
|
13
12
|
|
|
14
13
|
```sh
|
|
15
|
-
npm install react-native-audio-concat
|
|
14
|
+
npm install react-native-audio-concat
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
> **Note:** `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
|
|
19
|
-
|
|
20
17
|
### iOS
|
|
21
18
|
|
|
22
19
|
```sh
|
|
@@ -35,10 +32,10 @@ import { concatAudioFiles } from 'react-native-audio-concat';
|
|
|
35
32
|
// Concatenate audio files with silence periods
|
|
36
33
|
const data = [
|
|
37
34
|
{ filePath: '/path/to/audio1.m4a' },
|
|
38
|
-
{ durationMs: 500 },
|
|
35
|
+
{ durationMs: 500 }, // 500ms silence
|
|
39
36
|
{ filePath: '/path/to/audio2.m4a' },
|
|
40
37
|
{ durationMs: 1000 }, // 1 second silence
|
|
41
|
-
{ filePath: '/path/to/audio3.m4a' }
|
|
38
|
+
{ filePath: '/path/to/audio3.m4a' },
|
|
42
39
|
];
|
|
43
40
|
|
|
44
41
|
const outputPath = '/path/to/merged.m4a';
|
|
@@ -72,7 +69,6 @@ Concatenates audio files and silence periods into a single output file.
|
|
|
72
69
|
|
|
73
70
|
Check out the [example app](example/) for a complete working example.
|
|
74
71
|
|
|
75
|
-
|
|
76
72
|
## Contributing
|
|
77
73
|
|
|
78
74
|
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
package/android/build.gradle
CHANGED
|
@@ -15,79 +15,28 @@ buildscript {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
def reactNativeArchitectures() {
|
|
19
|
-
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
20
|
-
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
21
|
-
}
|
|
22
18
|
|
|
23
19
|
apply plugin: "com.android.library"
|
|
24
20
|
apply plugin: "kotlin-android"
|
|
25
|
-
apply from: '../nitrogen/generated/android/audioconcat+autolinking.gradle'
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
// as it triggers autolinking. Only the app module should autolink dependencies.
|
|
22
|
+
apply plugin: "com.facebook.react"
|
|
29
23
|
|
|
30
24
|
def getExtOrIntegerDefault(name) {
|
|
31
25
|
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["AudioConcat_" + name]).toInteger()
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
android {
|
|
35
|
-
namespace "com.
|
|
29
|
+
namespace "com.audioconcat"
|
|
36
30
|
|
|
37
31
|
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
38
32
|
|
|
39
33
|
defaultConfig {
|
|
40
34
|
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
41
35
|
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
42
|
-
|
|
43
|
-
externalNativeBuild {
|
|
44
|
-
cmake {
|
|
45
|
-
cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
|
|
46
|
-
arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
47
|
-
abiFilters (*reactNativeArchitectures())
|
|
48
|
-
|
|
49
|
-
buildTypes {
|
|
50
|
-
debug {
|
|
51
|
-
cppFlags "-O1 -g"
|
|
52
|
-
}
|
|
53
|
-
release {
|
|
54
|
-
cppFlags "-O2"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
externalNativeBuild {
|
|
62
|
-
cmake {
|
|
63
|
-
path "CMakeLists.txt"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
packagingOptions {
|
|
68
|
-
excludes = [
|
|
69
|
-
"META-INF",
|
|
70
|
-
"META-INF/**",
|
|
71
|
-
"**/libc++_shared.so",
|
|
72
|
-
"**/libfbjni.so",
|
|
73
|
-
"**/libjsi.so",
|
|
74
|
-
"**/libfolly_json.so",
|
|
75
|
-
"**/libfolly_runtime.so",
|
|
76
|
-
"**/libglog.so",
|
|
77
|
-
"**/libhermes.so",
|
|
78
|
-
"**/libhermes-executor-debug.so",
|
|
79
|
-
"**/libhermes_executor.so",
|
|
80
|
-
"**/libreactnative.so",
|
|
81
|
-
"**/libreactnativejni.so",
|
|
82
|
-
"**/libturbomodulejsijni.so",
|
|
83
|
-
"**/libreact_nativemodule_core.so",
|
|
84
|
-
"**/libjscexecutor.so"
|
|
85
|
-
]
|
|
86
36
|
}
|
|
87
37
|
|
|
88
38
|
buildFeatures {
|
|
89
39
|
buildConfig true
|
|
90
|
-
prefab true
|
|
91
40
|
}
|
|
92
41
|
|
|
93
42
|
buildTypes {
|
|
@@ -125,5 +74,4 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
|
|
|
125
74
|
dependencies {
|
|
126
75
|
implementation "com.facebook.react:react-android"
|
|
127
76
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
128
|
-
implementation project(":react-native-nitro-modules")
|
|
129
77
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
package com.
|
|
2
|
-
|
|
3
|
-
import com.facebook.
|
|
4
|
-
import com.
|
|
1
|
+
package com.audioconcat
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
8
|
import android.media.MediaCodec
|
|
6
9
|
import android.media.MediaCodecInfo
|
|
7
10
|
import android.media.MediaExtractor
|
|
@@ -11,14 +14,21 @@ import java.io.File
|
|
|
11
14
|
import java.nio.ByteBuffer
|
|
12
15
|
import android.util.Log
|
|
13
16
|
|
|
14
|
-
@
|
|
15
|
-
class
|
|
16
|
-
|
|
17
|
+
@ReactModule(name = AudioConcatModule.NAME)
|
|
18
|
+
class AudioConcatModule(reactContext: ReactApplicationContext) :
|
|
19
|
+
NativeAudioConcatSpec(reactContext) {
|
|
20
|
+
|
|
21
|
+
private data class AudioConfig(
|
|
17
22
|
val sampleRate: Int,
|
|
18
23
|
val channelCount: Int,
|
|
19
24
|
val bitRate: Int
|
|
20
25
|
)
|
|
21
26
|
|
|
27
|
+
private sealed class AudioDataOrSilence {
|
|
28
|
+
data class AudioFile(val filePath: String) : AudioDataOrSilence()
|
|
29
|
+
data class Silence(val durationMs: Double) : AudioDataOrSilence()
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
private fun extractAudioConfig(filePath: String): AudioConfig {
|
|
23
33
|
val extractor = MediaExtractor()
|
|
24
34
|
try {
|
|
@@ -112,7 +122,7 @@ private data class AudioConfig(
|
|
|
112
122
|
audioTrackIndex = muxer.addTrack(newFormat)
|
|
113
123
|
muxer.start()
|
|
114
124
|
muxerStarted = true
|
|
115
|
-
Log.d("
|
|
125
|
+
Log.d("AudioConcat", "Encoder started, format: $newFormat")
|
|
116
126
|
}
|
|
117
127
|
MediaCodec.INFO_TRY_AGAIN_LATER -> {
|
|
118
128
|
if (!endOfStream) {
|
|
@@ -270,29 +280,54 @@ private data class AudioConfig(
|
|
|
270
280
|
}
|
|
271
281
|
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
283
|
+
private fun parseAudioData(data: ReadableArray): List<AudioDataOrSilence> {
|
|
284
|
+
val result = mutableListOf<AudioDataOrSilence>()
|
|
285
|
+
for (i in 0 until data.size()) {
|
|
286
|
+
val item = data.getMap(i)
|
|
287
|
+
if (item != null) {
|
|
288
|
+
if (item.hasKey("filePath")) {
|
|
289
|
+
val filePath = item.getString("filePath")
|
|
290
|
+
if (filePath != null) {
|
|
291
|
+
result.add(AudioDataOrSilence.AudioFile(filePath))
|
|
292
|
+
}
|
|
293
|
+
} else if (item.hasKey("durationMs")) {
|
|
294
|
+
result.add(AudioDataOrSilence.Silence(item.getDouble("durationMs")))
|
|
295
|
+
}
|
|
277
296
|
}
|
|
297
|
+
}
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
278
300
|
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
override fun getName(): String {
|
|
302
|
+
return NAME
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
override fun concatAudioFiles(data: ReadableArray, outputPath: String, promise: Promise) {
|
|
306
|
+
try {
|
|
307
|
+
if (data.size() == 0) {
|
|
308
|
+
promise.reject("EMPTY_DATA", "Data array is empty")
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
val parsedData = parseAudioData(data)
|
|
313
|
+
Log.d("AudioConcat", "Streaming merge of ${parsedData.size} items")
|
|
314
|
+
Log.d("AudioConcat", "Output: $outputPath")
|
|
281
315
|
|
|
282
316
|
// Get audio config from first audio file
|
|
283
317
|
var audioConfig: AudioConfig? = null
|
|
284
|
-
for (item in
|
|
285
|
-
if (item is AudioDataOrSilence.
|
|
286
|
-
audioConfig = extractAudioConfig(item.
|
|
318
|
+
for (item in parsedData) {
|
|
319
|
+
if (item is AudioDataOrSilence.AudioFile) {
|
|
320
|
+
audioConfig = extractAudioConfig(item.filePath)
|
|
287
321
|
break
|
|
288
322
|
}
|
|
289
323
|
}
|
|
290
324
|
|
|
291
325
|
if (audioConfig == null) {
|
|
292
|
-
|
|
326
|
+
promise.reject("NO_AUDIO_FILES", "No audio files found in data array")
|
|
327
|
+
return
|
|
293
328
|
}
|
|
294
329
|
|
|
295
|
-
Log.d("
|
|
330
|
+
Log.d("AudioConcat", "Audio config: ${audioConfig.sampleRate}Hz, ${audioConfig.channelCount}ch, ${audioConfig.bitRate}bps")
|
|
296
331
|
|
|
297
332
|
// Delete existing output file
|
|
298
333
|
val outputFile = File(outputPath)
|
|
@@ -310,19 +345,19 @@ private data class AudioConfig(
|
|
|
310
345
|
|
|
311
346
|
try {
|
|
312
347
|
// Process each item
|
|
313
|
-
for ((index, item) in
|
|
348
|
+
for ((index, item) in parsedData.withIndex()) {
|
|
314
349
|
when (item) {
|
|
315
|
-
is AudioDataOrSilence.
|
|
316
|
-
val filePath = item.
|
|
317
|
-
Log.d("
|
|
350
|
+
is AudioDataOrSilence.AudioFile -> {
|
|
351
|
+
val filePath = item.filePath
|
|
352
|
+
Log.d("AudioConcat", "Item $index: Streaming decode $filePath")
|
|
318
353
|
|
|
319
|
-
val isLastFile = (index ==
|
|
354
|
+
val isLastFile = (index == parsedData.size - 1)
|
|
320
355
|
streamDecodeAudioFile(filePath, encoder, isLastFile)
|
|
321
356
|
}
|
|
322
357
|
|
|
323
|
-
is AudioDataOrSilence.
|
|
324
|
-
val durationMs = item.
|
|
325
|
-
Log.d("
|
|
358
|
+
is AudioDataOrSilence.Silence -> {
|
|
359
|
+
val durationMs = item.durationMs
|
|
360
|
+
Log.d("AudioConcat", "Item $index: Streaming silence ${durationMs}ms")
|
|
326
361
|
|
|
327
362
|
streamEncodeSilence(
|
|
328
363
|
durationMs,
|
|
@@ -336,14 +371,21 @@ private data class AudioConfig(
|
|
|
336
371
|
|
|
337
372
|
// Finish encoding
|
|
338
373
|
encoder.finish()
|
|
339
|
-
Log.d("
|
|
374
|
+
Log.d("AudioConcat", "Successfully merged audio to $outputPath")
|
|
375
|
+
promise.resolve(outputPath)
|
|
340
376
|
|
|
341
377
|
} catch (e: Exception) {
|
|
342
|
-
Log.e("
|
|
343
|
-
|
|
378
|
+
Log.e("AudioConcat", "Error during streaming merge: ${e.message}", e)
|
|
379
|
+
promise.reject("MERGE_ERROR", e.message, e)
|
|
344
380
|
}
|
|
345
381
|
|
|
346
|
-
|
|
382
|
+
} catch (e: Exception) {
|
|
383
|
+
Log.e("AudioConcat", "Error parsing data: ${e.message}", e)
|
|
384
|
+
promise.reject("PARSE_ERROR", e.message, e)
|
|
347
385
|
}
|
|
348
386
|
}
|
|
387
|
+
|
|
388
|
+
companion object {
|
|
389
|
+
const val NAME = "AudioConcat"
|
|
390
|
+
}
|
|
349
391
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package com.audioconcat
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
import java.util.HashMap
|
|
9
|
+
|
|
10
|
+
class AudioConcatPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
12
|
+
return if (name == AudioConcatModule.NAME) {
|
|
13
|
+
AudioConcatModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
20
|
+
return ReactModuleInfoProvider {
|
|
21
|
+
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
|
|
22
|
+
moduleInfos[AudioConcatModule.NAME] = ReactModuleInfo(
|
|
23
|
+
AudioConcatModule.NAME,
|
|
24
|
+
AudioConcatModule.NAME,
|
|
25
|
+
false, // canOverrideExistingModule
|
|
26
|
+
false, // needsEagerInit
|
|
27
|
+
false, // isCxxModule
|
|
28
|
+
true // isTurboModule
|
|
29
|
+
)
|
|
30
|
+
moduleInfos
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#import "AudioConcat.h"
|
|
2
|
+
#import <AVFoundation/AVFoundation.h>
|
|
3
|
+
|
|
4
|
+
@implementation AudioConcat
|
|
5
|
+
|
|
6
|
+
- (void)concatAudioFiles:(NSArray *)data
|
|
7
|
+
outputPath:(NSString *)outputPath
|
|
8
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
10
|
+
{
|
|
11
|
+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
12
|
+
@try {
|
|
13
|
+
AVMutableComposition *composition = [AVMutableComposition composition];
|
|
14
|
+
AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio
|
|
15
|
+
preferredTrackID:kCMPersistentTrackID_Invalid];
|
|
16
|
+
|
|
17
|
+
CMTime currentTime = kCMTimeZero;
|
|
18
|
+
|
|
19
|
+
for (NSDictionary *item in data) {
|
|
20
|
+
if (item[@"filePath"]) {
|
|
21
|
+
// Add audio file
|
|
22
|
+
NSString *filePath = item[@"filePath"];
|
|
23
|
+
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
|
|
24
|
+
|
|
25
|
+
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
|
|
26
|
+
NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeAudio];
|
|
27
|
+
|
|
28
|
+
if (tracks.count == 0) {
|
|
29
|
+
reject(@"ERR_NO_AUDIO_TRACK", [NSString stringWithFormat:@"No audio track found in file: %@", filePath], nil);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
AVAssetTrack *track = tracks[0];
|
|
34
|
+
CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
|
|
35
|
+
|
|
36
|
+
NSError *error = nil;
|
|
37
|
+
[audioTrack insertTimeRange:timeRange
|
|
38
|
+
ofTrack:track
|
|
39
|
+
atTime:currentTime
|
|
40
|
+
error:&error];
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
reject(@"ERR_INSERT_TRACK", error.localizedDescription, error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
currentTime = CMTimeAdd(currentTime, asset.duration);
|
|
48
|
+
|
|
49
|
+
} else if (item[@"durationMs"]) {
|
|
50
|
+
// Add silence period
|
|
51
|
+
NSNumber *durationMs = item[@"durationMs"];
|
|
52
|
+
double durationSeconds = [durationMs doubleValue] / 1000.0;
|
|
53
|
+
CMTime silenceDuration = CMTimeMakeWithSeconds(durationSeconds, 600);
|
|
54
|
+
|
|
55
|
+
currentTime = CMTimeAdd(currentTime, silenceDuration);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Export the composition
|
|
60
|
+
NSURL *outputURL = [NSURL fileURLWithPath:outputPath];
|
|
61
|
+
|
|
62
|
+
// Remove existing file if it exists
|
|
63
|
+
[[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
|
|
64
|
+
|
|
65
|
+
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:composition
|
|
66
|
+
presetName:AVAssetExportPresetAppleM4A];
|
|
67
|
+
exportSession.outputURL = outputURL;
|
|
68
|
+
exportSession.outputFileType = AVFileTypeAppleM4A;
|
|
69
|
+
|
|
70
|
+
[exportSession exportAsynchronouslyWithCompletionHandler:^{
|
|
71
|
+
switch (exportSession.status) {
|
|
72
|
+
case AVAssetExportSessionStatusCompleted:
|
|
73
|
+
resolve(outputPath);
|
|
74
|
+
break;
|
|
75
|
+
case AVAssetExportSessionStatusFailed:
|
|
76
|
+
reject(@"ERR_EXPORT_FAILED", exportSession.error.localizedDescription, exportSession.error);
|
|
77
|
+
break;
|
|
78
|
+
case AVAssetExportSessionStatusCancelled:
|
|
79
|
+
reject(@"ERR_EXPORT_CANCELLED", @"Export was cancelled", nil);
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
reject(@"ERR_EXPORT_UNKNOWN", @"Unknown export error", nil);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}];
|
|
86
|
+
|
|
87
|
+
} @catch (NSException *exception) {
|
|
88
|
+
reject(@"ERR_EXCEPTION", exception.reason, nil);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
94
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
95
|
+
{
|
|
96
|
+
return std::make_shared<facebook::react::NativeAudioConcatSpecJSI>(params);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
+ (NSString *)moduleName
|
|
100
|
+
{
|
|
101
|
+
return @"AudioConcat";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeAudioConcat.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAWpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,aAAa,CAAC","ignoreList":[]}
|
package/lib/module/index.js
CHANGED
|
@@ -1,33 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
const AudioConcatHybridObject = NitroModules.createHybridObject('AudioConcat');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Concat audio files and silence periods into a single output file.
|
|
8
|
-
*
|
|
9
|
-
* @param data - Array of audio files and silence periods to merge.
|
|
10
|
-
* Each item can be either:
|
|
11
|
-
* - `{ filePath: string }` for an audio file
|
|
12
|
-
* - `{ durationMs: number }` for a silence period
|
|
13
|
-
* @param outputPath - Absolute path where the merged audio file will be saved (M4A format)
|
|
14
|
-
* @returns Promise that resolves with the output file path
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* ```typescript
|
|
18
|
-
* const data = [
|
|
19
|
-
* { filePath: '/path/to/audio1.m4a' },
|
|
20
|
-
* { durationMs: 500 }, // 500ms silence
|
|
21
|
-
* { filePath: '/path/to/audio2.m4a' },
|
|
22
|
-
* { durationMs: 1000 }, // 1 second silence
|
|
23
|
-
* { filePath: '/path/to/audio3.m4a' }
|
|
24
|
-
* ];
|
|
25
|
-
* const output = '/path/to/merged.m4a';
|
|
26
|
-
* const result = await concatAudioFiles(data, output);
|
|
27
|
-
* console.log('concat file:', result);
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
3
|
+
import AudioConcat from "./NativeAudioConcat.js";
|
|
30
4
|
export function concatAudioFiles(data, outputPath) {
|
|
31
|
-
return
|
|
5
|
+
return AudioConcat.concatAudioFiles(data, outputPath);
|
|
32
6
|
}
|
|
33
7
|
//# sourceMappingURL=index.js.map
|
package/lib/module/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["
|
|
1
|
+
{"version":3,"names":["AudioConcat","concatAudioFiles","data","outputPath"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,WAAW,MAAM,wBAAqB;AAI7C,OAAO,SAASC,gBAAgBA,CAC9BC,IAA0D,EAC1DC,UAAkB,EACD;EACjB,OAAOH,WAAW,CAACC,gBAAgB,CAACC,IAAI,EAAEC,UAAU,CAAC;AACvD","ignoreList":[]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TurboModule } from 'react-native';
|
|
2
|
+
export type AudioDataOrSilence = {
|
|
3
|
+
filePath: string;
|
|
4
|
+
} | {
|
|
5
|
+
durationMs: number;
|
|
6
|
+
};
|
|
7
|
+
export interface Spec extends TurboModule {
|
|
8
|
+
concatAudioFiles(data: AudioDataOrSilence[], outputPath: string): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
declare const _default: Spec;
|
|
11
|
+
export default _default;
|
|
12
|
+
//# sourceMappingURL=NativeAudioConcat.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NativeAudioConcat.d.ts","sourceRoot":"","sources":["../../../src/NativeAudioConcat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE,MAAM,MAAM,kBAAkB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,gBAAgB,CACd,IAAI,EAAE,kBAAkB,EAAE,EAC1B,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAAC;CACpB;;AAED,wBAAqE"}
|
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* - `{ filePath: string }` for an audio file
|
|
8
|
-
* - `{ durationMs: number }` for a silence period
|
|
9
|
-
* @param outputPath - Absolute path where the merged audio file will be saved (M4A format)
|
|
10
|
-
* @returns Promise that resolves with the output file path
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* const data = [
|
|
15
|
-
* { filePath: '/path/to/audio1.m4a' },
|
|
16
|
-
* { durationMs: 500 }, // 500ms silence
|
|
17
|
-
* { filePath: '/path/to/audio2.m4a' },
|
|
18
|
-
* { durationMs: 1000 }, // 1 second silence
|
|
19
|
-
* { filePath: '/path/to/audio3.m4a' }
|
|
20
|
-
* ];
|
|
21
|
-
* const output = '/path/to/merged.m4a';
|
|
22
|
-
* const result = await concatAudioFiles(data, output);
|
|
23
|
-
* console.log('concat file:', result);
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export declare function concatAudioFiles(data: AudioDataOrSilence[], outputPath: string): Promise<string>;
|
|
27
|
-
export type { AudioDataOrSilence } from './AudioConcat.nitro';
|
|
1
|
+
export type { AudioDataOrSilence } from './NativeAudioConcat';
|
|
2
|
+
export declare function concatAudioFiles(data: Array<{
|
|
3
|
+
filePath: string;
|
|
4
|
+
} | {
|
|
5
|
+
durationMs: number;
|
|
6
|
+
}>, outputPath: string): Promise<string>;
|
|
28
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAEA,YAAY,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,EAC1D,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
|