react-native-nitro-audio-record 0.1.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/LICENSE +21 -0
- package/NitroAudioRecord.podspec +33 -0
- package/README.md +92 -0
- package/android/CMakeLists.txt +32 -0
- package/android/build.gradle +148 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/nitroaudiorecord/HybridNitroAudioRecord.kt +189 -0
- package/android/src/main/java/com/nitroaudiorecord/NitroAudioRecordPackage.kt +20 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridNitroAudioRecord.swift +108 -0
- package/lib/commonjs/index.js +9 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/nitro-audio-record.nitro.js +6 -0
- package/lib/commonjs/specs/nitro-audio-record.nitro.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/specs/nitro-audio-record.nitro.js +4 -0
- package/lib/module/specs/nitro-audio-record.nitro.js.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-audio-record.nitro.d.ts +18 -0
- package/lib/typescript/src/specs/nitro-audio-record.nitro.d.ts.map +1 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroAudioRecord+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroAudioRecord+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroAudioRecordOnLoad.cpp +56 -0
- package/nitrogen/generated/android/NitroAudioRecordOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JAudioRecordOptions.hpp +74 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridNitroAudioRecordSpec.cpp +87 -0
- package/nitrogen/generated/android/c++/JHybridNitroAudioRecordSpec.hpp +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroaudiorecord/AudioRecordOptions.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroaudiorecord/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroaudiorecord/HybridNitroAudioRecordSpec.kt +73 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroaudiorecord/NitroAudioRecordOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroAudioRecord+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroAudioRecord-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/NitroAudioRecord-Swift-Cxx-Bridge.hpp +177 -0
- package/nitrogen/generated/ios/NitroAudioRecord-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/NitroAudioRecordAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroAudioRecordAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroAudioRecordSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroAudioRecordSpecSwift.hpp +109 -0
- package/nitrogen/generated/ios/swift/AudioRecordOptions.swift +75 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNitroAudioRecordSpec.swift +58 -0
- package/nitrogen/generated/ios/swift/HybridNitroAudioRecordSpec_cxx.swift +183 -0
- package/nitrogen/generated/shared/c++/AudioRecordOptions.hpp +100 -0
- package/nitrogen/generated/shared/c++/HybridNitroAudioRecordSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridNitroAudioRecordSpec.hpp +70 -0
- package/package.json +123 -0
- package/src/index.ts +5 -0
- package/src/specs/nitro-audio-record.nitro.ts +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 iwater
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "NitroAudioRecord"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
|
|
14
|
+
s.source = { :git => "https://github.com/iwater/react-native-nitro-audio-record.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = [
|
|
17
|
+
# Implementation (Swift)
|
|
18
|
+
"ios/**/*.{swift}",
|
|
19
|
+
# Autolinking/Registration (Objective-C++)
|
|
20
|
+
"ios/**/*.{m,mm}",
|
|
21
|
+
# Implementation (C++ objects)
|
|
22
|
+
"cpp/**/*.{hpp,cpp}",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
load 'nitrogen/generated/ios/NitroAudioRecord+autolinking.rb'
|
|
26
|
+
add_nitrogen_files(s)
|
|
27
|
+
|
|
28
|
+
s.frameworks = 'AVFoundation', 'AudioToolbox'
|
|
29
|
+
|
|
30
|
+
s.dependency 'React-jsi'
|
|
31
|
+
s.dependency 'React-callinvoker'
|
|
32
|
+
install_modules_dependencies(s)
|
|
33
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# react-native-nitro-audio-record
|
|
2
|
+
|
|
3
|
+
A high-performance audio recording library for React Native powered by **Nitro Modules**.
|
|
4
|
+
|
|
5
|
+
## Relationship with `react-native-audio-record`
|
|
6
|
+
|
|
7
|
+
This library is a **modernized, high-performance successor** to the original [react-native-audio-record](https://github.com/goodatlas/react-native-audio-record).
|
|
8
|
+
|
|
9
|
+
While it provides similar functionality, it has been completely rewritten to leverage the **New React Native Architecture** and **Nitro Modules** for zero-copy JSI communication. It is designed to be faster, type-safe, and future-proof.
|
|
10
|
+
|
|
11
|
+
- **Turbo Performance**: Zero-copy data transfer using JSI `ArrayBuffer`.
|
|
12
|
+
- **Modern Architecture**: Built for the new React Native architecture (TurboModules/Fabric).
|
|
13
|
+
- **TypeScript First**: Fully type-safe API.
|
|
14
|
+
- **Real-time Streaming**: Get audio chunks as they are recorded.
|
|
15
|
+
|
|
16
|
+
## Use Cases
|
|
17
|
+
|
|
18
|
+
- **Real-time Speech Recognition**: Stream PCM buffers directly to APIs like Google Cloud Speech-to-Text, OpenAI Whisper, or Deepgram.
|
|
19
|
+
- **Audio Visualization**: Draw real-time waveforms or frequency spectrums using the zero-copy `ArrayBuffer`.
|
|
20
|
+
- **Custom Audio Processing**: Apply Noise Suppression, Gain Control, or other DSP algorithms in JavaScript or C++.
|
|
21
|
+
- **Accurate Audio Scoring (GOP)**: High-fidelity audio capture for language learning apps that require precise PCM data.
|
|
22
|
+
- **Low-Latency VOIP**: Lightweight recording for real-time communication.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
npm install react-native-nitro-audio-record react-native-nitro-modules
|
|
28
|
+
npx pod-install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { NitroAudioRecord } from 'react-native-nitro-audio-record';
|
|
35
|
+
|
|
36
|
+
// Initialize with options
|
|
37
|
+
NitroAudioRecord.setup({
|
|
38
|
+
sampleRate: 16000, // default 44100
|
|
39
|
+
channels: 1, // 1 or 2
|
|
40
|
+
bitsPerSample: 16, // 8 or 16
|
|
41
|
+
wavFile: 'test.wav' // saved in document directory
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Listen for real-time audio data (ArrayBuffer)
|
|
45
|
+
NitroAudioRecord.onData((data: ArrayBuffer) => {
|
|
46
|
+
// Direct zero-copy access to PCM data
|
|
47
|
+
console.log('Chunk size:', data.byteLength);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Start recording
|
|
51
|
+
NitroAudioRecord.start();
|
|
52
|
+
|
|
53
|
+
// Stop recording and get the file path
|
|
54
|
+
const audioFile = await NitroAudioRecord.stop();
|
|
55
|
+
console.log('Saved to:', audioFile);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Migration from `react-native-audio-record`
|
|
59
|
+
|
|
60
|
+
If you are coming from the original `react-native-audio-record`, here are the key changes:
|
|
61
|
+
|
|
62
|
+
| Field | original `react-native-audio-record` | `react-native-nitro-audio-record` |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| **Import** | `import AudioRecord from '...'` | `import { NitroAudioRecord } from '...'` |
|
|
65
|
+
| **Initialization** | `AudioRecord.init(options)` | `NitroAudioRecord.setup(options)` |
|
|
66
|
+
| **Real-time Data** | Event Listener (`.on('data', ...)`) | Callback (`.onData(...)`) |
|
|
67
|
+
| **Data Format** | Base64 String | `ArrayBuffer` (Fast JSI) |
|
|
68
|
+
| **New Arch** | Bridge (Bridge-based) | Nitro (New Arch / JSI) |
|
|
69
|
+
|
|
70
|
+
### Code Example (Before vs After)
|
|
71
|
+
|
|
72
|
+
**Before:**
|
|
73
|
+
```javascript
|
|
74
|
+
import AudioRecord from 'react-native-audio-record';
|
|
75
|
+
AudioRecord.init(options);
|
|
76
|
+
AudioRecord.on('data', base64Data => {
|
|
77
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**After:**
|
|
82
|
+
```typescript
|
|
83
|
+
import { NitroAudioRecord } from 'react-native-nitro-audio-record';
|
|
84
|
+
NitroAudioRecord.setup(options);
|
|
85
|
+
NitroAudioRecord.onData(arrayBuffer => {
|
|
86
|
+
// Use arrayBuffer directly without decoding!
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
project(NitroAudioRecord)
|
|
2
|
+
cmake_minimum_required(VERSION 3.9.0)
|
|
3
|
+
|
|
4
|
+
set (PACKAGE_NAME NitroAudioRecord)
|
|
5
|
+
set (CMAKE_VERBOSE_MAKEFILE ON)
|
|
6
|
+
set (CMAKE_CXX_STANDARD 20)
|
|
7
|
+
|
|
8
|
+
# Enable Raw Props parsing in react-native (for Nitro Views)
|
|
9
|
+
add_compile_options(-DRN_SERIALIZABLE_STATE=1)
|
|
10
|
+
|
|
11
|
+
# Define C++ library and add all sources
|
|
12
|
+
add_library(${PACKAGE_NAME} SHARED
|
|
13
|
+
src/main/cpp/cpp-adapter.cpp
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Add Nitrogen specs :)
|
|
17
|
+
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroAudioRecord+autolinking.cmake)
|
|
18
|
+
|
|
19
|
+
# Set up local includes
|
|
20
|
+
include_directories(
|
|
21
|
+
"src/main/cpp"
|
|
22
|
+
"../cpp"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
find_library(LOG_LIB log)
|
|
26
|
+
|
|
27
|
+
# Link all libraries together
|
|
28
|
+
target_link_libraries(
|
|
29
|
+
${PACKAGE_NAME}
|
|
30
|
+
${LOG_LIB}
|
|
31
|
+
android # <-- Android core
|
|
32
|
+
)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
repositories {
|
|
3
|
+
google()
|
|
4
|
+
mavenCentral()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
dependencies {
|
|
8
|
+
classpath "com.android.tools.build:gradle:8.8.0"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def reactNativeArchitectures() {
|
|
13
|
+
def value = rootProject.getProperties().get("reactNativeArchitectures")
|
|
14
|
+
return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def isNewArchitectureEnabled() {
|
|
18
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
apply plugin: "com.android.library"
|
|
22
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
23
|
+
apply from: '../nitrogen/generated/android/NitroAudioRecord+autolinking.gradle'
|
|
24
|
+
apply from: "./fix-prefab.gradle"
|
|
25
|
+
|
|
26
|
+
if (isNewArchitectureEnabled()) {
|
|
27
|
+
apply plugin: "com.facebook.react"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def getExtOrDefault(name) {
|
|
31
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroAudioRecord_" + name]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def getExtOrIntegerDefault(name) {
|
|
35
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroAudioRecord_" + name]).toInteger()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
android {
|
|
39
|
+
namespace "com.nitroaudiorecord"
|
|
40
|
+
|
|
41
|
+
ndkVersion getExtOrDefault("ndkVersion")
|
|
42
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
43
|
+
|
|
44
|
+
defaultConfig {
|
|
45
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
46
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
47
|
+
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
|
48
|
+
|
|
49
|
+
externalNativeBuild {
|
|
50
|
+
cmake {
|
|
51
|
+
cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
|
|
52
|
+
arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
|
53
|
+
abiFilters (*reactNativeArchitectures())
|
|
54
|
+
|
|
55
|
+
buildTypes {
|
|
56
|
+
debug {
|
|
57
|
+
cppFlags "-O1 -g"
|
|
58
|
+
}
|
|
59
|
+
release {
|
|
60
|
+
cppFlags "-O2"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
externalNativeBuild {
|
|
68
|
+
cmake {
|
|
69
|
+
path "CMakeLists.txt"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
packagingOptions {
|
|
74
|
+
excludes = [
|
|
75
|
+
"META-INF",
|
|
76
|
+
"META-INF/**",
|
|
77
|
+
"**/libc++_shared.so",
|
|
78
|
+
"**/libfbjni.so",
|
|
79
|
+
"**/libjsi.so",
|
|
80
|
+
"**/libfolly_json.so",
|
|
81
|
+
"**/libfolly_runtime.so",
|
|
82
|
+
"**/libglog.so",
|
|
83
|
+
"**/libhermes.so",
|
|
84
|
+
"**/libhermes-executor-debug.so",
|
|
85
|
+
"**/libhermes_executor.so",
|
|
86
|
+
"**/libreactnative.so",
|
|
87
|
+
"**/libreactnativejni.so",
|
|
88
|
+
"**/libturbomodulejsijni.so",
|
|
89
|
+
"**/libreact_nativemodule_core.so",
|
|
90
|
+
"**/libjscexecutor.so"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
buildFeatures {
|
|
95
|
+
buildConfig true
|
|
96
|
+
prefab true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
buildTypes {
|
|
100
|
+
release {
|
|
101
|
+
minifyEnabled false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lintOptions {
|
|
106
|
+
disable "GradleCompatible"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
compileOptions {
|
|
110
|
+
sourceCompatibility JavaVersion.VERSION_1_8
|
|
111
|
+
targetCompatibility JavaVersion.VERSION_1_8
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
sourceSets {
|
|
115
|
+
main {
|
|
116
|
+
if (isNewArchitectureEnabled()) {
|
|
117
|
+
java.srcDirs += [
|
|
118
|
+
// React Codegen files
|
|
119
|
+
"${project.buildDir}/generated/source/codegen/java"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
repositories {
|
|
127
|
+
mavenCentral()
|
|
128
|
+
google()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
dependencies {
|
|
133
|
+
// For < 0.71, this will be from the local maven repo
|
|
134
|
+
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
|
|
135
|
+
//noinspection GradleDynamicVersion
|
|
136
|
+
implementation "com.facebook.react:react-native:+"
|
|
137
|
+
|
|
138
|
+
// Add a dependency on NitroModules
|
|
139
|
+
implementation project(":react-native-nitro-modules")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (isNewArchitectureEnabled()) {
|
|
143
|
+
react {
|
|
144
|
+
jsRootDir = file("../src/")
|
|
145
|
+
libraryName = "NitroAudioRecord"
|
|
146
|
+
codegenJavaPackageName = "com.nitroaudiorecord"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
tasks.configureEach { task ->
|
|
2
|
+
// Make sure that we generate our prefab publication file only after having built the native library
|
|
3
|
+
// so that not a header publication file, but a full configuration publication will be generated, which
|
|
4
|
+
// will include the .so file
|
|
5
|
+
|
|
6
|
+
def prefabConfigurePattern = ~/^prefab(.+)ConfigurePackage$/
|
|
7
|
+
def matcher = task.name =~ prefabConfigurePattern
|
|
8
|
+
if (matcher.matches()) {
|
|
9
|
+
def variantName = matcher[0][1]
|
|
10
|
+
task.outputs.upToDateWhen { false }
|
|
11
|
+
task.dependsOn("externalNativeBuild${variantName}")
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEvaluate {
|
|
16
|
+
def abis = reactNativeArchitectures()
|
|
17
|
+
rootProject.allprojects.each { proj ->
|
|
18
|
+
if (proj === rootProject) return
|
|
19
|
+
|
|
20
|
+
def dependsOnThisLib = proj.configurations.findAll { it.canBeResolved }.any { config ->
|
|
21
|
+
config.dependencies.any { dep ->
|
|
22
|
+
dep.group == project.group && dep.name == project.name
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!dependsOnThisLib && proj != project) return
|
|
26
|
+
|
|
27
|
+
if (!proj.plugins.hasPlugin('com.android.application') && !proj.plugins.hasPlugin('com.android.library')) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def variants = proj.android.hasProperty('applicationVariants') ? proj.android.applicationVariants : proj.android.libraryVariants
|
|
32
|
+
// Touch the prefab_config.json files to ensure that in ExternalNativeJsonGenerator.kt we will re-trigger the prefab CLI to
|
|
33
|
+
// generate a libnameConfig.cmake file that will contain our native library (.so).
|
|
34
|
+
// See this condition: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ExternalNativeJsonGenerator.kt;l=207-219?q=createPrefabBuildSystemGlue
|
|
35
|
+
variants.all { variant ->
|
|
36
|
+
def variantName = variant.name
|
|
37
|
+
abis.each { abi ->
|
|
38
|
+
def searchDir = new File(proj.projectDir, ".cxx/${variantName}")
|
|
39
|
+
if (!searchDir.exists()) return
|
|
40
|
+
def matches = []
|
|
41
|
+
searchDir.eachDir { randomDir ->
|
|
42
|
+
def prefabFile = new File(randomDir, "${abi}/prefab_config.json")
|
|
43
|
+
if (prefabFile.exists()) matches << prefabFile
|
|
44
|
+
}
|
|
45
|
+
matches.each { prefabConfig ->
|
|
46
|
+
prefabConfig.setLastModified(System.currentTimeMillis())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package com.nitroaudiorecord
|
|
2
|
+
|
|
3
|
+
import android.media.AudioFormat
|
|
4
|
+
import android.media.AudioRecord
|
|
5
|
+
import android.media.MediaRecorder.AudioSource
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import com.margelo.nitro.NitroModules
|
|
8
|
+
import com.margelo.nitro.core.ArrayBuffer
|
|
9
|
+
import com.margelo.nitro.core.Promise
|
|
10
|
+
import com.margelo.nitro.nitroaudiorecord.AudioRecordOptions
|
|
11
|
+
import com.margelo.nitro.nitroaudiorecord.HybridNitroAudioRecordSpec
|
|
12
|
+
import java.io.File
|
|
13
|
+
import java.io.FileInputStream
|
|
14
|
+
import java.io.FileOutputStream
|
|
15
|
+
import java.nio.ByteBuffer
|
|
16
|
+
|
|
17
|
+
class HybridNitroAudioRecord : HybridNitroAudioRecordSpec() {
|
|
18
|
+
private val TAG = "NitroAudioRecord"
|
|
19
|
+
|
|
20
|
+
private var sampleRateInHz = 44100
|
|
21
|
+
private var channelConfig = AudioFormat.CHANNEL_IN_MONO
|
|
22
|
+
private var audioFormat = AudioFormat.ENCODING_PCM_16BIT
|
|
23
|
+
private var audioSource = AudioSource.VOICE_RECOGNITION
|
|
24
|
+
|
|
25
|
+
private var recorder: AudioRecord? = null
|
|
26
|
+
private var bufferSize = 0
|
|
27
|
+
private var isRecording = false
|
|
28
|
+
|
|
29
|
+
private lateinit var tmpFile: String
|
|
30
|
+
private lateinit var outFile: String
|
|
31
|
+
|
|
32
|
+
private var onDataCallback: ((data: ArrayBuffer) -> Unit)? = null
|
|
33
|
+
|
|
34
|
+
override fun setup(options: AudioRecordOptions) {
|
|
35
|
+
sampleRateInHz = options.sampleRate.toInt()
|
|
36
|
+
channelConfig = if (options.channels == 2L) AudioFormat.CHANNEL_IN_STEREO else AudioFormat.CHANNEL_IN_MONO
|
|
37
|
+
audioFormat = if (options.bitsPerSample == 8L) AudioFormat.ENCODING_PCM_8BIT else AudioFormat.ENCODING_PCM_16BIT
|
|
38
|
+
audioSource = (options.audioSource ?: AudioSource.VOICE_RECOGNITION.toDouble()).toInt()
|
|
39
|
+
|
|
40
|
+
val context = NitroModules.applicationContext
|
|
41
|
+
val documentDirectoryPath = context.filesDir.absolutePath
|
|
42
|
+
outFile = "$documentDirectoryPath/${options.wavFile ?: "audio.wav"}"
|
|
43
|
+
tmpFile = "$documentDirectoryPath/temp.pcm"
|
|
44
|
+
|
|
45
|
+
isRecording = false
|
|
46
|
+
bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
47
|
+
val recordingBufferSize = bufferSize * 3
|
|
48
|
+
recorder = AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, recordingBufferSize)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun start() {
|
|
52
|
+
if (isRecording) return
|
|
53
|
+
|
|
54
|
+
isRecording = true
|
|
55
|
+
recorder?.startRecording()
|
|
56
|
+
Log.d(TAG, "started recording")
|
|
57
|
+
|
|
58
|
+
Thread {
|
|
59
|
+
try {
|
|
60
|
+
var bytesRead: Int
|
|
61
|
+
var count = 0
|
|
62
|
+
val buffer = ByteArray(bufferSize)
|
|
63
|
+
val os = FileOutputStream(tmpFile)
|
|
64
|
+
|
|
65
|
+
while (isRecording) {
|
|
66
|
+
bytesRead = recorder?.read(buffer, 0, buffer.length) ?: -1
|
|
67
|
+
|
|
68
|
+
// skip first 2 buffers to eliminate "click sound"
|
|
69
|
+
if (bytesRead > 0 && ++count > 2) {
|
|
70
|
+
// Create ArrayBuffer and notify callback
|
|
71
|
+
onDataCallback?.let { callback ->
|
|
72
|
+
val byteBuffer = ByteBuffer.allocateDirect(bytesRead)
|
|
73
|
+
byteBuffer.put(buffer, 0, bytesRead)
|
|
74
|
+
val arrayBuffer = ArrayBuffer.wrap(byteBuffer)
|
|
75
|
+
callback(arrayBuffer)
|
|
76
|
+
}
|
|
77
|
+
os.write(buffer, 0, bytesRead)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
recorder?.stop()
|
|
82
|
+
os.close()
|
|
83
|
+
saveAsWav()
|
|
84
|
+
} catch (e: Exception) {
|
|
85
|
+
Log.e(TAG, "Error during recording", e)
|
|
86
|
+
}
|
|
87
|
+
}.start()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override fun stop(): Promise<String> {
|
|
91
|
+
isRecording = false
|
|
92
|
+
return Promise { resolve ->
|
|
93
|
+
// In a real implementation, we might need a better way to wait for the thread to finish
|
|
94
|
+
// such as using a CountDownLatch or joining the thread.
|
|
95
|
+
// For now, we'll wait a bit (simplistic).
|
|
96
|
+
Thread {
|
|
97
|
+
Thread.sleep(100)
|
|
98
|
+
resolve(outFile)
|
|
99
|
+
}.start()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun onData(callback: (data: ArrayBuffer) -> Unit) {
|
|
104
|
+
onDataCallback = callback
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun saveAsWav() {
|
|
108
|
+
try {
|
|
109
|
+
val fileIn = File(tmpFile)
|
|
110
|
+
val `in` = FileInputStream(fileIn)
|
|
111
|
+
val out = FileOutputStream(outFile)
|
|
112
|
+
val totalAudioLen = fileIn.length()
|
|
113
|
+
val totalDataLen = totalAudioLen + 36
|
|
114
|
+
|
|
115
|
+
addWavHeader(out, totalAudioLen, totalDataLen)
|
|
116
|
+
|
|
117
|
+
val data = ByteArray(bufferSize)
|
|
118
|
+
var bytesRead: Int
|
|
119
|
+
while (`in`.read(data).also { bytesRead = it } != -1) {
|
|
120
|
+
out.write(data, 0, bytesRead)
|
|
121
|
+
}
|
|
122
|
+
Log.d(TAG, "file saved at path: $outFile")
|
|
123
|
+
|
|
124
|
+
`in`.close()
|
|
125
|
+
out.close()
|
|
126
|
+
fileIn.delete()
|
|
127
|
+
} catch (e: Exception) {
|
|
128
|
+
Log.e(TAG, "Error saving as wav", e)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Throws(Exception::class)
|
|
133
|
+
private fun addWavHeader(out: FileOutputStream, totalAudioLen: Long, totalDataLen: Long) {
|
|
134
|
+
val sampleRate = sampleRateInHz.toLong()
|
|
135
|
+
val channels = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
|
|
136
|
+
val bitsPerSample = if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) 8 else 16
|
|
137
|
+
val byteRate = sampleRate * channels * bitsPerSample / 8
|
|
138
|
+
val blockAlign = channels * bitsPerSample / 8
|
|
139
|
+
|
|
140
|
+
val header = ByteArray(44)
|
|
141
|
+
|
|
142
|
+
header[0] = 'R'.toByte() // RIFF chunk
|
|
143
|
+
header[1] = 'I'.toByte()
|
|
144
|
+
header[2] = 'F'.toByte()
|
|
145
|
+
header[3] = 'F'.toByte()
|
|
146
|
+
header[4] = (totalDataLen and 0xff).toByte() // how big is the rest of this file
|
|
147
|
+
header[5] = (totalDataLen shr 8 and 0xff).toByte()
|
|
148
|
+
header[6] = (totalDataLen shr 16 and 0xff).toByte()
|
|
149
|
+
header[7] = (totalDataLen shr 24 and 0xff).toByte()
|
|
150
|
+
header[8] = 'W'.toByte() // WAVE chunk
|
|
151
|
+
header[9] = 'A'.toByte()
|
|
152
|
+
header[10] = 'V'.toByte()
|
|
153
|
+
header[11] = 'E'.toByte()
|
|
154
|
+
header[12] = 'f'.toByte() // 'fmt ' chunk
|
|
155
|
+
header[13] = 'm'.toByte()
|
|
156
|
+
header[14] = 't'.toByte()
|
|
157
|
+
header[15] = ' '.toByte()
|
|
158
|
+
header[16] = 16 // 4 bytes: size of 'fmt ' chunk
|
|
159
|
+
header[17] = 0
|
|
160
|
+
header[18] = 0
|
|
161
|
+
header[19] = 0
|
|
162
|
+
header[20] = 1 // format = 1 for PCM
|
|
163
|
+
header[21] = 0
|
|
164
|
+
header[22] = channels.toByte() // mono or stereo
|
|
165
|
+
header[23] = 0
|
|
166
|
+
header[24] = (sampleRate and 0xff).toByte() // samples per second
|
|
167
|
+
header[25] = (sampleRate shr 8 and 0xff).toByte()
|
|
168
|
+
header[26] = (sampleRate shr 16 and 0xff).toByte()
|
|
169
|
+
header[27] = (sampleRate shr 24 and 0xff).toByte()
|
|
170
|
+
header[28] = (byteRate and 0xff).toByte() // bytes per second
|
|
171
|
+
header[29] = (byteRate shr 8 and 0xff).toByte()
|
|
172
|
+
header[30] = (byteRate shr 16 and 0xff).toByte()
|
|
173
|
+
header[31] = (byteRate shr 24 and 0xff).toByte()
|
|
174
|
+
header[32] = blockAlign.toByte() // bytes in one sample, for all channels
|
|
175
|
+
header[33] = 0
|
|
176
|
+
header[34] = bitsPerSample.toByte() // bits in a sample
|
|
177
|
+
header[35] = 0
|
|
178
|
+
header[36] = 'd'.toByte() // beginning of the data chunk
|
|
179
|
+
header[37] = 'a'.toByte()
|
|
180
|
+
header[38] = 't'.toByte()
|
|
181
|
+
header[39] = 'a'.toByte()
|
|
182
|
+
header[40] = (totalAudioLen and 0xff).toByte() // how big is this data chunk
|
|
183
|
+
header[41] = (totalAudioLen shr 8 and 0xff).toByte()
|
|
184
|
+
header[42] = (totalAudioLen shr 16 and 0xff).toByte()
|
|
185
|
+
header[43] = (totalAudioLen shr 24 and 0xff).toByte()
|
|
186
|
+
|
|
187
|
+
out.write(header, 0, 44)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.nitroaudiorecord;
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.NativeModule;
|
|
4
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
5
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
6
|
+
import com.facebook.react.BaseReactPackage;
|
|
7
|
+
import com.margelo.nitro.nitroaudiorecord.NitroAudioRecordOnLoad;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
public class NitroAudioRecordPackage : BaseReactPackage() {
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
|
|
14
|
+
|
|
15
|
+
companion object {
|
|
16
|
+
init {
|
|
17
|
+
NitroAudioRecordOnLoad.initializeNative();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|