@siteed/expo-audio-stream 0.5.1 → 0.6.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/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +9 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +62 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +4 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +445 -0
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +12 -0
- package/android/src/main/java/net/siteed/audiostream/EventSender.kt +7 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +43 -392
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +16 -0
- package/build/ExpoAudioStream.types.d.ts +12 -1
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStreamModule.web.d.ts +2 -2
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.web.js +5 -1
- package/build/ExpoAudioStreamModule.web.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -0
- package/build/index.js.map +1 -1
- package/build/useAudioRecording.d.ts +5 -4
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +38 -27
- package/build/useAudioRecording.js.map +1 -1
- package/ios/AudioStreamManager.swift +46 -16
- package/ios/ExpoAudioStreamModule.swift +2 -2
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +14 -5
- package/src/ExpoAudioStreamModule.web.ts +8 -3
- package/src/index.ts +4 -0
- package/src/useAudioRecording.ts +48 -34
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.IOException
|
|
5
|
+
import java.io.OutputStream
|
|
6
|
+
import java.io.RandomAccessFile
|
|
7
|
+
|
|
8
|
+
class AudioFileHandler(private val filesDir: File) {
|
|
9
|
+
// Method to write WAV file header
|
|
10
|
+
fun writeWavHeader(out: OutputStream, sampleRateInHz: Int, channels: Int, bitDepth: Int) {
|
|
11
|
+
val header = ByteArray(44)
|
|
12
|
+
val byteRate = sampleRateInHz * channels * bitDepth / 8
|
|
13
|
+
val blockAlign = channels * bitDepth / 8
|
|
14
|
+
|
|
15
|
+
// RIFF/WAVE header
|
|
16
|
+
"RIFF".toByteArray().copyInto(header, 0)
|
|
17
|
+
header[4] = 0 // Size will be updated later
|
|
18
|
+
"WAVE".toByteArray().copyInto(header, 8)
|
|
19
|
+
"fmt ".toByteArray().copyInto(header, 12)
|
|
20
|
+
|
|
21
|
+
// 16 for PCM
|
|
22
|
+
header[16] = 16
|
|
23
|
+
header[20] = 1 // Audio format 1 for PCM (not compressed)
|
|
24
|
+
header[22] = channels.toByte()
|
|
25
|
+
header[24] = (sampleRateInHz and 0xff).toByte()
|
|
26
|
+
header[25] = (sampleRateInHz shr 8 and 0xff).toByte()
|
|
27
|
+
header[26] = (sampleRateInHz shr 16 and 0xff).toByte()
|
|
28
|
+
header[27] = (sampleRateInHz shr 24 and 0xff).toByte()
|
|
29
|
+
header[28] = (byteRate and 0xff).toByte()
|
|
30
|
+
header[29] = (byteRate shr 8 and 0xff).toByte()
|
|
31
|
+
header[30] = (byteRate shr 16 and 0xff).toByte()
|
|
32
|
+
header[31] = (byteRate shr 24 and 0xff).toByte()
|
|
33
|
+
header[32] = blockAlign.toByte()
|
|
34
|
+
header[34] = bitDepth.toByte()
|
|
35
|
+
"data".toByteArray().copyInto(header, 36)
|
|
36
|
+
|
|
37
|
+
out.write(header, 0, 44)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fun updateWavHeader(file: File) {
|
|
41
|
+
try {
|
|
42
|
+
RandomAccessFile(file, "rw").use { raf ->
|
|
43
|
+
val fileSize = raf.length()
|
|
44
|
+
val dataSize = fileSize - 44 // Subtract the header size
|
|
45
|
+
|
|
46
|
+
raf.seek(4) // Write correct file size, excluding the first 8 bytes of the RIFF header
|
|
47
|
+
raf.writeInt(Integer.reverseBytes((dataSize + 36).toInt()))
|
|
48
|
+
|
|
49
|
+
raf.seek(40) // Go to the data size position
|
|
50
|
+
raf.writeInt(Integer.reverseBytes(dataSize.toInt())) // Write the size of the data segment
|
|
51
|
+
}
|
|
52
|
+
} catch (e: IOException) {
|
|
53
|
+
println("Could not update WAV header: ${e.message}")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun clearAudioStorage() {
|
|
58
|
+
filesDir.listFiles()?.forEach {
|
|
59
|
+
it.delete()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import android.media.AudioFormat
|
|
4
|
+
import android.media.AudioRecord
|
|
5
|
+
import android.media.MediaRecorder
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import android.os.SystemClock
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import androidx.annotation.RequiresApi
|
|
13
|
+
import androidx.core.os.bundleOf
|
|
14
|
+
import expo.modules.kotlin.Promise
|
|
15
|
+
import java.io.ByteArrayOutputStream
|
|
16
|
+
import java.io.File
|
|
17
|
+
import java.io.FileOutputStream
|
|
18
|
+
import java.io.IOException
|
|
19
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AudioRecorderManager(
|
|
23
|
+
private val filesDir: File,
|
|
24
|
+
private val permissionUtils: PermissionUtils,
|
|
25
|
+
private val audioDataEncoder: AudioDataEncoder,
|
|
26
|
+
private val eventSender: EventSender
|
|
27
|
+
) {
|
|
28
|
+
private var audioRecord: AudioRecord? = null
|
|
29
|
+
private var sampleRateInHz = Constants.DEFAULT_SAMPLE_RATE
|
|
30
|
+
private var channelConfig = Constants.DEFAULT_CHANNEL_CONFIG
|
|
31
|
+
private var audioFormat = Constants.DEFAULT_AUDIO_FORMAT
|
|
32
|
+
private var bufferSizeInBytes =
|
|
33
|
+
AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
34
|
+
private var isRecording = AtomicBoolean(false)
|
|
35
|
+
private val isPaused = AtomicBoolean(false)
|
|
36
|
+
private var streamUuid: String? = null
|
|
37
|
+
private var audioFile: File? = null
|
|
38
|
+
private var recordingThread: Thread? = null
|
|
39
|
+
private var recordingStartTime: Long = 0
|
|
40
|
+
private var totalRecordedTime: Long = 0
|
|
41
|
+
private var totalDataSize = 0
|
|
42
|
+
private var interval = 1000L // Emit data every 1000 milliseconds (1 second)
|
|
43
|
+
private var lastEmitTime = SystemClock.elapsedRealtime()
|
|
44
|
+
private var lastPauseTime = 0L
|
|
45
|
+
private var pausedDuration = 0L
|
|
46
|
+
private var lastEmittedSize = 0L
|
|
47
|
+
private var mimeType = "audio/wav"
|
|
48
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
49
|
+
private var bitDepth = 16
|
|
50
|
+
private var channels = 1
|
|
51
|
+
private val audioRecordLock = Any()
|
|
52
|
+
private var audioFileHandler: AudioFileHandler = AudioFileHandler(filesDir)
|
|
53
|
+
|
|
54
|
+
@RequiresApi(Build.VERSION_CODES.R)
|
|
55
|
+
fun startRecording(options: Map<String, Any?>, promise: Promise) {
|
|
56
|
+
if (!permissionUtils.checkRecordingPermission()) {
|
|
57
|
+
promise.reject("PERMISSION_DENIED", "Recording permission has not been granted", null)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
62
|
+
promise.reject("ALREADY_RECORDING", "Recording is already in progress", null)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Extract and validate recording options
|
|
67
|
+
sampleRateInHz = options["sampleRate"] as? Int ?: Constants.DEFAULT_SAMPLE_RATE
|
|
68
|
+
if (sampleRateInHz !in listOf(16000, 44100, 48000)) {
|
|
69
|
+
promise.reject(
|
|
70
|
+
"INVALID_SAMPLE_RATE",
|
|
71
|
+
"Sample rate must be one of 16000, 44100, or 48000 Hz",
|
|
72
|
+
null
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
channels = options["channels"] as? Int ?: 1
|
|
78
|
+
if (channels !in 1..2) {
|
|
79
|
+
promise.reject(
|
|
80
|
+
"INVALID_CHANNELS",
|
|
81
|
+
"Channels must be either 1 (Mono) or 2 (Stereo)",
|
|
82
|
+
null
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val encodingType = options["encoding"] as? String ?: "pcm_16bit"
|
|
88
|
+
if (encodingType !in listOf("pcm_16bit", "pcm_8bit", "aac", "opus")) {
|
|
89
|
+
promise.reject(
|
|
90
|
+
"INVALID_ENCODING",
|
|
91
|
+
"Encoding must be one of the following: pcm_16bit, pcm_8bit, aac, opus",
|
|
92
|
+
null
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
var fileExtension = ".wav" // Default
|
|
99
|
+
|
|
100
|
+
audioFormat = when (encodingType) {
|
|
101
|
+
"pcm_8bit", "pcm_16bit" -> {
|
|
102
|
+
fileExtension = "wav"
|
|
103
|
+
mimeType = "audio/wav" // WAV is typically used for PCM data.
|
|
104
|
+
AudioFormat.ENCODING_PCM_16BIT
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
"opus" -> {
|
|
108
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
109
|
+
// Handle the case where Opus is not supported by the device
|
|
110
|
+
promise.reject(
|
|
111
|
+
"UNSUPPORTED_FORMAT",
|
|
112
|
+
"Opus encoding not supported on this Android version.",
|
|
113
|
+
null
|
|
114
|
+
)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
fileExtension = "opus"
|
|
118
|
+
mimeType = "audio/opus"
|
|
119
|
+
AudioFormat.ENCODING_OPUS
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
"aac_lc" -> {
|
|
123
|
+
fileExtension = "aac"
|
|
124
|
+
mimeType = "audio/aac"
|
|
125
|
+
AudioFormat.ENCODING_AAC_LC
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
else -> {
|
|
129
|
+
fileExtension = "wav"
|
|
130
|
+
mimeType = "audio/wav" // Default case or throw an error if unsupported
|
|
131
|
+
AudioFormat.ENCODING_DEFAULT
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interval = options["interval"] as? Long ?: Constants.DEFAULT_INTERVAL
|
|
136
|
+
if (interval < Constants.MIN_INTERVAL) {
|
|
137
|
+
promise.reject(
|
|
138
|
+
"INVALID_INTERVAL",
|
|
139
|
+
"Interval must be at least ${Constants.MIN_INTERVAL} ms",
|
|
140
|
+
null
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
|
|
146
|
+
|
|
147
|
+
Log.d(
|
|
148
|
+
Constants.TAG,
|
|
149
|
+
"Starting recording with the following parameters: Sample Rate: $sampleRateInHz Hz, Channels: $channels, Encoding: $encodingType, File Extension: $fileExtension, MIME Type: $mimeType, Interval: $interval ms"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Initialize the AudioRecord if it's a new recording or if it's not currently paused
|
|
153
|
+
if (audioRecord == null || !isPaused.get()) {
|
|
154
|
+
audioRecord = AudioRecord(
|
|
155
|
+
MediaRecorder.AudioSource.MIC,
|
|
156
|
+
sampleRateInHz,
|
|
157
|
+
channelConfig,
|
|
158
|
+
audioFormat,
|
|
159
|
+
bufferSizeInBytes
|
|
160
|
+
)
|
|
161
|
+
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
|
|
162
|
+
promise.reject(
|
|
163
|
+
"INITIALIZATION_FAILED",
|
|
164
|
+
"Failed to initialize the audio recorder",
|
|
165
|
+
null
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
streamUuid = java.util.UUID.randomUUID().toString()
|
|
172
|
+
audioFile = File(filesDir, "audio_${streamUuid}.${fileExtension}")
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
FileOutputStream(audioFile, true).use { fos ->
|
|
176
|
+
audioFileHandler.writeWavHeader(fos, sampleRateInHz, channels, bitDepth)
|
|
177
|
+
}
|
|
178
|
+
} catch (e: IOException) {
|
|
179
|
+
promise.reject("FILE_CREATION_FAILED", "Failed to create the audio file", e)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
audioRecord?.startRecording()
|
|
184
|
+
isPaused.set(false)
|
|
185
|
+
isRecording.set(true)
|
|
186
|
+
|
|
187
|
+
if (!isPaused.get()) {
|
|
188
|
+
recordingStartTime =
|
|
189
|
+
System.currentTimeMillis() // Only reset start time if it's not a resume
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
recordingThread = Thread { recordingProcess() }.apply { start() }
|
|
193
|
+
|
|
194
|
+
val result = bundleOf(
|
|
195
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
196
|
+
"channels" to channels,
|
|
197
|
+
"bitDepth" to bitDepth,
|
|
198
|
+
"sampleRate" to sampleRateInHz,
|
|
199
|
+
"mimeType" to mimeType
|
|
200
|
+
)
|
|
201
|
+
promise.resolve(result)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun stopRecording(promise: Promise) {
|
|
205
|
+
synchronized(audioRecordLock) {
|
|
206
|
+
|
|
207
|
+
if (!isRecording.get()) {
|
|
208
|
+
Log.e(Constants.TAG, "Recording is not active")
|
|
209
|
+
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
val audioData = ByteArray(bufferSizeInBytes)
|
|
215
|
+
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
216
|
+
Log.d(Constants.TAG, "Last Read $bytesRead bytes")
|
|
217
|
+
if (bytesRead > 0) {
|
|
218
|
+
emitAudioData(audioData, bytesRead)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Log.d(Constants.TAG, "Stopping recording state = ${audioRecord?.state}")
|
|
222
|
+
if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
|
|
223
|
+
Log.d(Constants.TAG, "Stopping AudioRecord");
|
|
224
|
+
audioRecord!!.stop()
|
|
225
|
+
}
|
|
226
|
+
} catch (e: IllegalStateException) {
|
|
227
|
+
Log.e(Constants.TAG, "Error reading from AudioRecord", e);
|
|
228
|
+
} finally {
|
|
229
|
+
audioRecord?.release()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
val fileSize = audioFile?.length() ?: 0
|
|
234
|
+
val dataFileSize = fileSize - 44 // Subtract header size
|
|
235
|
+
val byteRate = sampleRateInHz * channels * (bitDepth / 8)
|
|
236
|
+
|
|
237
|
+
// Calculate duration based on the data size and byte rate
|
|
238
|
+
val duration = if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0
|
|
239
|
+
|
|
240
|
+
// Create result bundle
|
|
241
|
+
val result = bundleOf(
|
|
242
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
243
|
+
"duration" to duration,
|
|
244
|
+
"channels" to channels,
|
|
245
|
+
"bitDepth" to bitDepth,
|
|
246
|
+
"sampleRate" to sampleRateInHz,
|
|
247
|
+
"size" to fileSize,
|
|
248
|
+
"mimeType" to mimeType
|
|
249
|
+
)
|
|
250
|
+
promise.resolve(result)
|
|
251
|
+
|
|
252
|
+
// Reset the timing variables
|
|
253
|
+
isRecording.set(false)
|
|
254
|
+
isPaused.set(false)
|
|
255
|
+
totalRecordedTime = 0
|
|
256
|
+
pausedDuration = 0
|
|
257
|
+
} catch (e: Exception) {
|
|
258
|
+
Log.d(Constants.TAG, "Failed to stop recording", e)
|
|
259
|
+
promise.reject("STOP_FAILED", "Failed to stop recording", e)
|
|
260
|
+
} finally {
|
|
261
|
+
audioRecord = null
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fun pauseRecording(promise: Promise) {
|
|
267
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
268
|
+
audioRecord?.stop()
|
|
269
|
+
lastPauseTime =
|
|
270
|
+
System.currentTimeMillis() // Record the time when the recording was paused
|
|
271
|
+
isPaused.set(true)
|
|
272
|
+
promise.resolve("Recording paused")
|
|
273
|
+
} else {
|
|
274
|
+
promise.reject(
|
|
275
|
+
"NOT_RECORDING_OR_ALREADY_PAUSED",
|
|
276
|
+
"Recording is either not active or already paused",
|
|
277
|
+
null
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fun resumeRecording(promise: Promise) {
|
|
283
|
+
if (isRecording.get() && !isPaused.get()) {
|
|
284
|
+
promise.reject("NOT_PAUSED", "Recording is not paused", null)
|
|
285
|
+
return
|
|
286
|
+
} else if (audioRecord == null) {
|
|
287
|
+
promise.reject("NOT_RECORDING", "Recording is not active", null)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Calculate the duration the recording was paused
|
|
291
|
+
pausedDuration += System.currentTimeMillis() - lastPauseTime
|
|
292
|
+
isPaused.set(false)
|
|
293
|
+
audioRecord?.startRecording()
|
|
294
|
+
promise.resolve("Recording resumed")
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
fun getStatus(): Bundle {
|
|
298
|
+
synchronized(audioRecordLock) {
|
|
299
|
+
if (!isRecording.get()) {
|
|
300
|
+
Log.d(Constants.TAG, "Not recording --- skip status with default values")
|
|
301
|
+
|
|
302
|
+
return bundleOf(
|
|
303
|
+
"isRecording" to false,
|
|
304
|
+
"isPaused" to false,
|
|
305
|
+
"mime" to mimeType,
|
|
306
|
+
"size" to 0,
|
|
307
|
+
"interval" to interval,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Ensure you update this to check if audioFile is null or not
|
|
312
|
+
val fileSize = audioFile?.length() ?: 0
|
|
313
|
+
|
|
314
|
+
val duration = when (mimeType) {
|
|
315
|
+
"audio/wav" -> {
|
|
316
|
+
// WAV files store raw audio data, so we can calculate duration like this
|
|
317
|
+
val dataFileSize =
|
|
318
|
+
fileSize - Constants.WAV_HEADER_SIZE // Assuming header is always 44 bytes
|
|
319
|
+
val byteRate = sampleRateInHz * channels * (bitDepth / 8)
|
|
320
|
+
if (byteRate > 0) dataFileSize * 1000 / byteRate else 0
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
"audio/opus", "audio/aac" -> {
|
|
324
|
+
// For compressed formats, the duration might need to be retrieved differently,
|
|
325
|
+
// perhaps from metadata or requiring a library to parse the file if not stored elsewhere.
|
|
326
|
+
getCompressedAudioDuration(audioFile)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
else -> 0
|
|
330
|
+
}
|
|
331
|
+
return bundleOf(
|
|
332
|
+
"duration" to duration,
|
|
333
|
+
"isRecording" to isRecording.get(),
|
|
334
|
+
"isPaused" to isPaused.get(),
|
|
335
|
+
"mime" to mimeType,
|
|
336
|
+
"size" to totalDataSize,
|
|
337
|
+
"interval" to interval,
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fun listAudioFiles(promise: Promise) {
|
|
343
|
+
val fileList =
|
|
344
|
+
filesDir.list()?.filter { it.endsWith(".wav") }?.map { File(filesDir, it).absolutePath }
|
|
345
|
+
?: listOf()
|
|
346
|
+
promise.resolve(fileList)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fun clearAudioStorage() {
|
|
350
|
+
audioFileHandler.clearAudioStorage()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private fun recordingProcess() {
|
|
354
|
+
Log.i(Constants.TAG, "Starting recording process...")
|
|
355
|
+
FileOutputStream(audioFile, true).use { fos ->
|
|
356
|
+
// Buffer to accumulate data
|
|
357
|
+
val accumulatedAudioData = ByteArrayOutputStream()
|
|
358
|
+
audioFileHandler.writeWavHeader(
|
|
359
|
+
accumulatedAudioData,
|
|
360
|
+
sampleRateInHz,
|
|
361
|
+
channels,
|
|
362
|
+
bitDepth
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
// Write audio data directly to the file
|
|
366
|
+
val audioData = ByteArray(bufferSizeInBytes)
|
|
367
|
+
Log.d(Constants.TAG, "Entering recording loop")
|
|
368
|
+
while (isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
369
|
+
if (isPaused.get()) {
|
|
370
|
+
// If recording is paused, skip reading from the microphone
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
val bytesRead = synchronized(audioRecordLock) {
|
|
375
|
+
// Only synchronize the read operation and the check
|
|
376
|
+
audioRecord?.let {
|
|
377
|
+
if (it.state != AudioRecord.STATE_INITIALIZED) {
|
|
378
|
+
Log.e(Constants.TAG, "AudioRecord not initialized")
|
|
379
|
+
return@let -1
|
|
380
|
+
}
|
|
381
|
+
it.read(audioData, 0, bufferSizeInBytes)
|
|
382
|
+
} ?: -1 // Handle null case
|
|
383
|
+
}
|
|
384
|
+
if (bytesRead > 0) {
|
|
385
|
+
fos.write(audioData, 0, bytesRead)
|
|
386
|
+
totalDataSize += bytesRead
|
|
387
|
+
accumulatedAudioData.write(audioData, 0, bytesRead)
|
|
388
|
+
|
|
389
|
+
// Emit audio data at defined intervals
|
|
390
|
+
if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
|
|
391
|
+
emitAudioData(
|
|
392
|
+
accumulatedAudioData.toByteArray(),
|
|
393
|
+
accumulatedAudioData.size()
|
|
394
|
+
)
|
|
395
|
+
lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
|
|
396
|
+
accumulatedAudioData.reset() // Clear the accumulator
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
Log.d(Constants.TAG, "Bytes written to file: $bytesRead")
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Update the WAV header to reflect the actual data size
|
|
404
|
+
audioFile?.let { file ->
|
|
405
|
+
audioFileHandler.updateWavHeader(file)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private fun emitAudioData(audioData: ByteArray, length: Int) {
|
|
410
|
+
val encodedBuffer = audioDataEncoder.encodeToBase64(audioData)
|
|
411
|
+
|
|
412
|
+
val fileSize = audioFile?.length() ?: 0
|
|
413
|
+
val from = lastEmittedSize
|
|
414
|
+
val deltaSize = fileSize - lastEmittedSize
|
|
415
|
+
lastEmittedSize = fileSize
|
|
416
|
+
|
|
417
|
+
// Calculate position in milliseconds
|
|
418
|
+
val positionInMs = (from * 1000) / (sampleRateInHz * channels * (bitDepth / 8))
|
|
419
|
+
|
|
420
|
+
mainHandler.post {
|
|
421
|
+
try {
|
|
422
|
+
eventSender.sendExpoEvent(
|
|
423
|
+
Constants.AUDIO_EVENT_NAME, bundleOf(
|
|
424
|
+
"fileUri" to audioFile?.toURI().toString(),
|
|
425
|
+
"lastEmittedSize" to from,
|
|
426
|
+
"encoded" to encodedBuffer,
|
|
427
|
+
"deltaSize" to length,
|
|
428
|
+
"position" to positionInMs,
|
|
429
|
+
"mimeType" to mimeType,
|
|
430
|
+
"totalSize" to fileSize,
|
|
431
|
+
"streamUuid" to streamUuid
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
} catch (e: Exception) {
|
|
435
|
+
Log.e(Constants.TAG, "Failed to send event", e)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private fun getCompressedAudioDuration(file: File?): Long {
|
|
441
|
+
// Placeholder function for fetching duration from a compressed audio file
|
|
442
|
+
// This would depend on how you store or can retrieve duration info for compressed formats
|
|
443
|
+
return 0L // Implement this based on your specific requirements
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
object Constants {
|
|
4
|
+
const val AUDIO_EVENT_NAME = "AudioData"
|
|
5
|
+
const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
|
|
6
|
+
const val DEFAULT_CHANNEL_CONFIG = 1 // Mono
|
|
7
|
+
const val DEFAULT_AUDIO_FORMAT = 16 // 16-bit PCM
|
|
8
|
+
const val DEFAULT_INTERVAL = 1000L
|
|
9
|
+
const val MIN_INTERVAL = 100L // Minimum interval in ms for emitting audio data
|
|
10
|
+
const val WAV_HEADER_SIZE = 44
|
|
11
|
+
const val TAG = "AudioRecorderModule"
|
|
12
|
+
}
|