25
25
package com.github.squti.androidwaverecorder
26
26
27
27
import android.annotation.SuppressLint
28
+ import android.content.Context
28
29
import android.media.AudioFormat
29
30
import android.media.AudioRecord
30
31
import android.media.MediaRecorder
31
32
import android.media.audiofx.NoiseSuppressor
33
+ import android.net.Uri
32
34
import android.os.Build
33
35
import androidx.annotation.RequiresApi
34
36
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -38,7 +40,9 @@ import kotlinx.coroutines.launch
38
40
import kotlinx.coroutines.withContext
39
41
import java.io.DataOutputStream
40
42
import java.io.File
43
+ import java.io.FileOutputStream
41
44
import java.io.OutputStream
45
+ import java.math.BigDecimal
42
46
import java.util.LinkedList
43
47
import java.util.concurrent.TimeUnit
44
48
import java.util.concurrent.atomic.AtomicBoolean
@@ -49,7 +53,20 @@ import java.util.concurrent.atomic.AtomicBoolean
49
53
* Kotlin Coroutine with IO dispatcher to writing input data on storage asynchronously.
50
54
* @property filePath the path of the file to be saved.
51
55
*/
52
- class WaveRecorder (private var filePath : String ) {
56
+ class WaveRecorder {
57
+ private var fileUri: Uri ? = null
58
+ private var filePath: String? = null
59
+ private lateinit var context: Context
60
+
61
+ constructor (fileUri: Uri , context: Context ) {
62
+ this .fileUri = fileUri
63
+ this .context = context
64
+ }
65
+
66
+ constructor (filePath: String ) {
67
+ this .filePath = filePath
68
+ }
69
+
53
70
/* *
54
71
* Configuration for recording audio file.
55
72
*/
@@ -103,7 +120,7 @@ class WaveRecorder(private var filePath: String) {
103
120
private var isSkipping = AtomicBoolean (false )
104
121
private lateinit var audioRecorder: AudioRecord
105
122
private var noiseSuppressor: NoiseSuppressor ? = null
106
- private var silenceDuration = 0L
123
+ private var silenceDuration = BigDecimal . ZERO
107
124
private var currentState: RecorderState = RecorderState .STOP
108
125
109
126
/* *
@@ -167,15 +184,22 @@ class WaveRecorder(private var filePath: String) {
167
184
private suspend fun writeByteAudioDataToStorage () {
168
185
val bufferSize = calculateMinBufferSize(waveConfig)
169
186
val data = ByteArray (bufferSize)
170
- val file = File (filePath)
171
- val outputStream = DataOutputStream (file.outputStream())
172
- val fileWriter = FileWriter (outputStream)
187
+ val outputStream: OutputStream = if (fileUri != null ) {
188
+ context.contentResolver.openOutputStream(fileUri ? : return ) ? : return
189
+ } else {
190
+ val file = File (filePath!! )
191
+ FileOutputStream (file)
192
+ }
193
+ val dataOutputStream = DataOutputStream (outputStream)
194
+ val fileWriter = FileWriter (dataOutputStream)
173
195
174
196
val bufferSizeToKeep =
175
197
(waveConfig.sampleRate * channelCount(waveConfig.channels) * (bitPerSample(waveConfig.audioEncoding) / 8 ) * silenceDetectionConfig.bufferDurationInMillis / 1000 ).toInt()
176
198
177
199
val lastSkippedData = LinkedList <ByteArray >()
178
200
201
+ var fileDurationInMillis = BigDecimal .ZERO
202
+
179
203
while (audioRecorder.recordingState == AudioRecord .RECORDSTATE_RECORDING ) {
180
204
val operationStatus = audioRecorder.read(data, 0 , bufferSize)
181
205
@@ -197,30 +221,41 @@ class WaveRecorder(private var filePath: String) {
197
221
198
222
if (! isSkipping.get()) {
199
223
updateState(RecorderState .RECORDING )
224
+ lastSkippedData.forEach {
225
+ fileDurationInMillis + = calculateDurationInMillis(it, waveConfig)
226
+ }
200
227
fileWriter.writeDataToStream(lastSkippedData, data)
228
+ fileDurationInMillis + = calculateDurationInMillis(data, waveConfig)
201
229
}
202
230
}
203
231
204
- updateListeners(amplitude, file )
232
+ updateListeners(amplitude, fileDurationInMillis.toLong() )
205
233
}
206
234
}
207
235
updateState(RecorderState .STOP )
208
- cleanup(outputStream )
236
+ cleanup(dataOutputStream )
209
237
}
210
238
211
239
@RequiresApi(Build .VERSION_CODES .M )
212
240
private suspend fun writeFloatAudioDataToStorage () {
213
241
val bufferSize = calculateMinBufferSize(waveConfig)
214
242
val data = FloatArray (bufferSize)
215
- val file = File (filePath)
216
- val outputStream = DataOutputStream (file.outputStream())
217
- val fileWriter = FileWriter (outputStream)
243
+ val outputStream: OutputStream = if (fileUri != null ) {
244
+ context.contentResolver.openOutputStream(fileUri ? : return ) ? : return
245
+ } else {
246
+ val file = File (filePath!! )
247
+ FileOutputStream (file)
248
+ }
249
+ val dataOutputStream = DataOutputStream (outputStream)
250
+ val fileWriter = FileWriter (dataOutputStream)
218
251
219
252
val bufferSizeToKeep =
220
253
(waveConfig.sampleRate * channelCount(waveConfig.channels) * silenceDetectionConfig.bufferDurationInMillis / 1000 ).toInt()
221
254
222
255
val lastSkippedData = LinkedList <FloatArray >()
223
256
257
+ var fileDurationInMillis = BigDecimal .ZERO
258
+
224
259
while (audioRecorder.recordingState == AudioRecord .RECORDSTATE_RECORDING ) {
225
260
val operationStatus = audioRecorder.read(data, 0 , bufferSize, AudioRecord .READ_BLOCKING )
226
261
@@ -242,10 +277,14 @@ class WaveRecorder(private var filePath: String) {
242
277
243
278
if (! isSkipping.get()) {
244
279
updateState(RecorderState .RECORDING )
280
+ lastSkippedData.forEach {
281
+ fileDurationInMillis + = calculateDurationInMillis(it, waveConfig)
282
+ }
245
283
fileWriter.writeDataToStream(lastSkippedData, data)
284
+ fileDurationInMillis + = calculateDurationInMillis(data, waveConfig)
246
285
}
247
286
}
248
- updateListeners(amplitude, file )
287
+ updateListeners(amplitude, fileDurationInMillis.toLong() )
249
288
}
250
289
}
251
290
updateState(RecorderState .STOP )
@@ -273,7 +312,7 @@ class WaveRecorder(private var filePath: String) {
273
312
) {
274
313
if (amplitude < silenceDetectionConfig.minAmplitudeThreshold) {
275
314
silenceDuration + = calculateDurationInMillis(data, waveConfig)
276
- if (silenceDuration >= silenceDetectionConfig.preSilenceDurationInMillis) {
315
+ if (silenceDuration.toLong() >= silenceDetectionConfig.preSilenceDurationInMillis) {
277
316
if (! isSkipping.get()) {
278
317
isSkipping.set(true )
279
318
updateState(RecorderState .SKIPPING_SILENCE )
@@ -284,7 +323,7 @@ class WaveRecorder(private var filePath: String) {
284
323
}
285
324
}
286
325
} else {
287
- silenceDuration = 0L
326
+ silenceDuration = BigDecimal . ZERO
288
327
isSkipping.set(false )
289
328
}
290
329
}
@@ -297,7 +336,7 @@ class WaveRecorder(private var filePath: String) {
297
336
) {
298
337
if (amplitude < silenceDetectionConfig.minAmplitudeThreshold) {
299
338
silenceDuration + = calculateDurationInMillis(data, waveConfig)
300
- if (silenceDuration >= silenceDetectionConfig.preSilenceDurationInMillis) {
339
+ if (silenceDuration.toLong() >= silenceDetectionConfig.preSilenceDurationInMillis) {
301
340
302
341
if (! isSkipping.get()) {
303
342
isSkipping.set(true )
@@ -309,25 +348,21 @@ class WaveRecorder(private var filePath: String) {
309
348
}
310
349
}
311
350
} else {
312
- silenceDuration = 0L
351
+ silenceDuration = BigDecimal . ZERO
313
352
isSkipping.set(false )
314
353
}
315
354
}
316
355
317
- private suspend fun updateListeners (amplitude : Int , file : File ) {
356
+ private suspend fun updateListeners (amplitude : Int , fileDurationInMillis : Long ) {
318
357
withContext(Dispatchers .Main ) {
319
358
onAmplitudeListener?.let {
320
359
it(amplitude)
321
360
}
322
361
onTimeElapsed?.let {
323
- val audioLength =
324
- calculateDurationInMillis(file, waveConfig)
325
- it(TimeUnit .MILLISECONDS .toSeconds(audioLength))
362
+ it(TimeUnit .MILLISECONDS .toSeconds(fileDurationInMillis))
326
363
}
327
364
onTimeElapsedInMillis?.let {
328
- val audioLength =
329
- calculateDurationInMillis(file, waveConfig)
330
- it(audioLength)
365
+ it(fileDurationInMillis)
331
366
}
332
367
}
333
368
}
@@ -360,6 +395,16 @@ class WaveRecorder(private var filePath: String) {
360
395
filePath = newFilePath
361
396
}
362
397
398
+ /* * Changes @property fileUri to @param newFilePath
399
+ * Calling this method while still recording throws an IllegalStateException
400
+ */
401
+ fun changeFilePath (newFileUri : Uri ) {
402
+ if (audioRecorder.recordingState == AudioRecord .RECORDSTATE_RECORDING )
403
+ throw IllegalStateException (" Cannot change filePath when still recording." )
404
+ else
405
+ fileUri = newFileUri
406
+ }
407
+
363
408
/* *
364
409
* Stops audio recorder and release resources then writes recorded file headers.
365
410
*/
@@ -369,22 +414,33 @@ class WaveRecorder(private var filePath: String) {
369
414
audioRecorder.release()
370
415
isPaused.set(false )
371
416
isSkipping.set(false )
372
- silenceDuration = 0L
417
+ silenceDuration = BigDecimal . ZERO
373
418
audioSessionId = - 1
374
- WaveHeaderWriter (filePath, waveConfig).writeHeader()
419
+ if (fileUri != null ) {
420
+ WaveHeaderWriter (fileUri!! , context, waveConfig).writeHeader()
421
+ } else {
422
+ WaveHeaderWriter (filePath!! , waveConfig).writeHeader()
423
+ }
424
+
375
425
}
376
426
377
427
}
378
428
379
429
private fun isAudioRecorderInitialized (): Boolean =
380
430
this ::audioRecorder.isInitialized && audioRecorder.state == AudioRecord .STATE_INITIALIZED
381
431
432
+ /* *
433
+ * Pauses audio recorder
434
+ */
382
435
fun pauseRecording () {
383
436
isPaused.set(true )
384
437
}
385
438
439
+ /* *
440
+ * Resumes audio recorder
441
+ */
386
442
fun resumeRecording () {
387
- silenceDuration = 0L
443
+ silenceDuration = BigDecimal . ZERO
388
444
isSkipping.set(false )
389
445
isPaused.set(false )
390
446
}
0 commit comments