Skip to content

Commit 1b78f8b

Browse files
committed
Implement URI support for Scoped Storage compatibility
1 parent ea45483 commit 1b78f8b

File tree

6 files changed

+274
-83
lines changed

6 files changed

+274
-83
lines changed

android-wave-recorder/src/main/java/com/github/squti/androidwaverecorder/Calculate.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ package com.github.squti.androidwaverecorder
2727
import android.media.AudioFormat
2828
import android.media.AudioRecord
2929
import java.io.File
30+
import java.math.BigDecimal
31+
import java.math.MathContext
3032
import java.nio.ByteBuffer
3133
import java.nio.ByteOrder
3234

@@ -69,26 +71,38 @@ internal fun calculateAmplitude(data: FloatArray): Int {
6971
return (maxFloatAmplitude * 32768).toInt()
7072
}
7173

72-
internal fun calculateDurationInMillis(data: ByteArray, waveConfig: WaveConfig): Long {
74+
internal fun calculateDurationInMillis(data: ByteArray, waveConfig: WaveConfig): BigDecimal {
7375
return when (waveConfig.audioEncoding) {
7476
AudioFormat.ENCODING_PCM_8BIT -> {
75-
(data.size / 1 / channelCount(waveConfig.channels) / waveConfig.sampleRate.toFloat() * 1000).toLong()
77+
BigDecimal(data.size).divide(
78+
BigDecimal(1 * channelCount(waveConfig.channels) * waveConfig.sampleRate),
79+
MathContext.DECIMAL64
80+
) * BigDecimal(1000)
7681
}
7782

7883
AudioFormat.ENCODING_PCM_16BIT -> {
79-
(data.size / 2 / channelCount(waveConfig.channels) / waveConfig.sampleRate.toFloat() * 1000).toLong()
84+
BigDecimal(data.size).divide(
85+
BigDecimal(2 * channelCount(waveConfig.channels) * waveConfig.sampleRate),
86+
MathContext.DECIMAL64
87+
) * BigDecimal(1000)
8088
}
8189

8290
AudioFormat.ENCODING_PCM_32BIT -> {
83-
(data.size / 4 / channelCount(waveConfig.channels) / waveConfig.sampleRate.toFloat() * 1000).toLong()
91+
BigDecimal(data.size).divide(
92+
BigDecimal(4 * channelCount(waveConfig.channels) * waveConfig.sampleRate),
93+
MathContext.DECIMAL64
94+
) * BigDecimal(1000)
8495
}
8596

8697
else -> throw IllegalArgumentException("Unsupported audio format for encoding ${waveConfig.audioEncoding}")
8798
}
8899
}
89100

90-
internal fun calculateDurationInMillis(data: FloatArray, waveConfig: WaveConfig): Long {
91-
return (data.size / channelCount(waveConfig.channels) / waveConfig.sampleRate.toFloat() * 1000).toLong()
101+
internal fun calculateDurationInMillis(data: FloatArray, waveConfig: WaveConfig): BigDecimal {
102+
return BigDecimal(data.size).divide(
103+
BigDecimal(channelCount(waveConfig.channels) * waveConfig.sampleRate),
104+
MathContext.DECIMAL64
105+
) * BigDecimal(1000)
92106
}
93107

94108
internal fun calculateDurationInMillis(audioFile: File, waveConfig: WaveConfig): Long {

android-wave-recorder/src/main/java/com/github/squti/androidwaverecorder/WaveHeaderWriter.kt

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,38 @@
2424

2525
package com.github.squti.androidwaverecorder
2626

27+
import android.content.Context
2728
import android.media.AudioFormat
29+
import android.net.Uri
2830
import java.io.File
31+
import java.io.FileInputStream
2932
import java.io.RandomAccessFile
3033

31-
internal class WaveHeaderWriter(private val filePath: String, private val waveConfig: WaveConfig) {
34+
class WaveHeaderWriter {
35+
private var fileUri: Uri? = null
36+
private var filePath: String? = null
37+
private lateinit var context: Context
38+
private var waveConfig: WaveConfig
39+
40+
constructor(fileUri: Uri, context: Context, waveConfig: WaveConfig) {
41+
this.fileUri = fileUri
42+
this.context = context
43+
this.waveConfig = waveConfig
44+
}
45+
46+
constructor(filePath: String, waveConfig: WaveConfig) {
47+
this.filePath = filePath
48+
this.waveConfig = waveConfig
49+
}
3250

3351
fun writeHeader() {
34-
val inputStream = File(filePath).inputStream()
52+
val inputStream: FileInputStream = if (fileUri != null) {
53+
val fileDescriptor =
54+
context.contentResolver.openFileDescriptor(fileUri!!, "rw")?.fileDescriptor
55+
FileInputStream(fileDescriptor)
56+
} else {
57+
File(filePath!!).inputStream()
58+
}
3559
val totalAudioLen = inputStream.channel.size() - 44
3660
val totalDataLen = totalAudioLen + 36
3761
val channels = if (waveConfig.channels == AudioFormat.CHANNEL_IN_MONO)
@@ -52,10 +76,16 @@ internal class WaveHeaderWriter(private val filePath: String, private val waveCo
5276
waveConfig.audioEncoding == AudioFormat.ENCODING_PCM_FLOAT
5377
)
5478

55-
val randomAccessFile = RandomAccessFile(File(filePath), "rw")
56-
randomAccessFile.seek(0)
57-
randomAccessFile.write(header)
58-
randomAccessFile.close()
79+
if (fileUri != null) {
80+
val outputStream = context.contentResolver.openOutputStream(fileUri!!, "rw")
81+
outputStream?.write(header)
82+
outputStream?.close()
83+
} else {
84+
val randomAccessFile = RandomAccessFile(File(filePath!!), "rw")
85+
randomAccessFile.seek(0)
86+
randomAccessFile.write(header)
87+
randomAccessFile.close()
88+
}
5989
}
6090

6191
private fun getWavFileHeaderByteArray(

android-wave-recorder/src/main/java/com/github/squti/androidwaverecorder/WaveRecorder.kt

Lines changed: 81 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
package com.github.squti.androidwaverecorder
2626

2727
import android.annotation.SuppressLint
28+
import android.content.Context
2829
import android.media.AudioFormat
2930
import android.media.AudioRecord
3031
import android.media.MediaRecorder
3132
import android.media.audiofx.NoiseSuppressor
33+
import android.net.Uri
3234
import android.os.Build
3335
import androidx.annotation.RequiresApi
3436
import kotlinx.coroutines.DelicateCoroutinesApi
@@ -38,7 +40,9 @@ import kotlinx.coroutines.launch
3840
import kotlinx.coroutines.withContext
3941
import java.io.DataOutputStream
4042
import java.io.File
43+
import java.io.FileOutputStream
4144
import java.io.OutputStream
45+
import java.math.BigDecimal
4246
import java.util.LinkedList
4347
import java.util.concurrent.TimeUnit
4448
import java.util.concurrent.atomic.AtomicBoolean
@@ -49,7 +53,20 @@ import java.util.concurrent.atomic.AtomicBoolean
4953
* Kotlin Coroutine with IO dispatcher to writing input data on storage asynchronously.
5054
* @property filePath the path of the file to be saved.
5155
*/
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+
5370
/**
5471
* Configuration for recording audio file.
5572
*/
@@ -103,7 +120,7 @@ class WaveRecorder(private var filePath: String) {
103120
private var isSkipping = AtomicBoolean(false)
104121
private lateinit var audioRecorder: AudioRecord
105122
private var noiseSuppressor: NoiseSuppressor? = null
106-
private var silenceDuration = 0L
123+
private var silenceDuration = BigDecimal.ZERO
107124
private var currentState: RecorderState = RecorderState.STOP
108125

109126
/**
@@ -167,15 +184,22 @@ class WaveRecorder(private var filePath: String) {
167184
private suspend fun writeByteAudioDataToStorage() {
168185
val bufferSize = calculateMinBufferSize(waveConfig)
169186
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)
173195

174196
val bufferSizeToKeep =
175197
(waveConfig.sampleRate * channelCount(waveConfig.channels) * (bitPerSample(waveConfig.audioEncoding) / 8) * silenceDetectionConfig.bufferDurationInMillis / 1000).toInt()
176198

177199
val lastSkippedData = LinkedList<ByteArray>()
178200

201+
var fileDurationInMillis = BigDecimal.ZERO
202+
179203
while (audioRecorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
180204
val operationStatus = audioRecorder.read(data, 0, bufferSize)
181205

@@ -197,30 +221,41 @@ class WaveRecorder(private var filePath: String) {
197221

198222
if (!isSkipping.get()) {
199223
updateState(RecorderState.RECORDING)
224+
lastSkippedData.forEach {
225+
fileDurationInMillis += calculateDurationInMillis(it, waveConfig)
226+
}
200227
fileWriter.writeDataToStream(lastSkippedData, data)
228+
fileDurationInMillis += calculateDurationInMillis(data, waveConfig)
201229
}
202230
}
203231

204-
updateListeners(amplitude, file)
232+
updateListeners(amplitude, fileDurationInMillis.toLong())
205233
}
206234
}
207235
updateState(RecorderState.STOP)
208-
cleanup(outputStream)
236+
cleanup(dataOutputStream)
209237
}
210238

211239
@RequiresApi(Build.VERSION_CODES.M)
212240
private suspend fun writeFloatAudioDataToStorage() {
213241
val bufferSize = calculateMinBufferSize(waveConfig)
214242
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)
218251

219252
val bufferSizeToKeep =
220253
(waveConfig.sampleRate * channelCount(waveConfig.channels) * silenceDetectionConfig.bufferDurationInMillis / 1000).toInt()
221254

222255
val lastSkippedData = LinkedList<FloatArray>()
223256

257+
var fileDurationInMillis = BigDecimal.ZERO
258+
224259
while (audioRecorder.recordingState == AudioRecord.RECORDSTATE_RECORDING) {
225260
val operationStatus = audioRecorder.read(data, 0, bufferSize, AudioRecord.READ_BLOCKING)
226261

@@ -242,10 +277,14 @@ class WaveRecorder(private var filePath: String) {
242277

243278
if (!isSkipping.get()) {
244279
updateState(RecorderState.RECORDING)
280+
lastSkippedData.forEach {
281+
fileDurationInMillis += calculateDurationInMillis(it, waveConfig)
282+
}
245283
fileWriter.writeDataToStream(lastSkippedData, data)
284+
fileDurationInMillis += calculateDurationInMillis(data, waveConfig)
246285
}
247286
}
248-
updateListeners(amplitude, file)
287+
updateListeners(amplitude, fileDurationInMillis.toLong())
249288
}
250289
}
251290
updateState(RecorderState.STOP)
@@ -273,7 +312,7 @@ class WaveRecorder(private var filePath: String) {
273312
) {
274313
if (amplitude < silenceDetectionConfig.minAmplitudeThreshold) {
275314
silenceDuration += calculateDurationInMillis(data, waveConfig)
276-
if (silenceDuration >= silenceDetectionConfig.preSilenceDurationInMillis) {
315+
if (silenceDuration.toLong() >= silenceDetectionConfig.preSilenceDurationInMillis) {
277316
if (!isSkipping.get()) {
278317
isSkipping.set(true)
279318
updateState(RecorderState.SKIPPING_SILENCE)
@@ -284,7 +323,7 @@ class WaveRecorder(private var filePath: String) {
284323
}
285324
}
286325
} else {
287-
silenceDuration = 0L
326+
silenceDuration = BigDecimal.ZERO
288327
isSkipping.set(false)
289328
}
290329
}
@@ -297,7 +336,7 @@ class WaveRecorder(private var filePath: String) {
297336
) {
298337
if (amplitude < silenceDetectionConfig.minAmplitudeThreshold) {
299338
silenceDuration += calculateDurationInMillis(data, waveConfig)
300-
if (silenceDuration >= silenceDetectionConfig.preSilenceDurationInMillis) {
339+
if (silenceDuration.toLong() >= silenceDetectionConfig.preSilenceDurationInMillis) {
301340

302341
if (!isSkipping.get()) {
303342
isSkipping.set(true)
@@ -309,25 +348,21 @@ class WaveRecorder(private var filePath: String) {
309348
}
310349
}
311350
} else {
312-
silenceDuration = 0L
351+
silenceDuration = BigDecimal.ZERO
313352
isSkipping.set(false)
314353
}
315354
}
316355

317-
private suspend fun updateListeners(amplitude: Int, file: File) {
356+
private suspend fun updateListeners(amplitude: Int, fileDurationInMillis: Long) {
318357
withContext(Dispatchers.Main) {
319358
onAmplitudeListener?.let {
320359
it(amplitude)
321360
}
322361
onTimeElapsed?.let {
323-
val audioLength =
324-
calculateDurationInMillis(file, waveConfig)
325-
it(TimeUnit.MILLISECONDS.toSeconds(audioLength))
362+
it(TimeUnit.MILLISECONDS.toSeconds(fileDurationInMillis))
326363
}
327364
onTimeElapsedInMillis?.let {
328-
val audioLength =
329-
calculateDurationInMillis(file, waveConfig)
330-
it(audioLength)
365+
it(fileDurationInMillis)
331366
}
332367
}
333368
}
@@ -360,6 +395,16 @@ class WaveRecorder(private var filePath: String) {
360395
filePath = newFilePath
361396
}
362397

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+
363408
/**
364409
* Stops audio recorder and release resources then writes recorded file headers.
365410
*/
@@ -369,22 +414,33 @@ class WaveRecorder(private var filePath: String) {
369414
audioRecorder.release()
370415
isPaused.set(false)
371416
isSkipping.set(false)
372-
silenceDuration = 0L
417+
silenceDuration = BigDecimal.ZERO
373418
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+
375425
}
376426

377427
}
378428

379429
private fun isAudioRecorderInitialized(): Boolean =
380430
this::audioRecorder.isInitialized && audioRecorder.state == AudioRecord.STATE_INITIALIZED
381431

432+
/**
433+
* Pauses audio recorder
434+
*/
382435
fun pauseRecording() {
383436
isPaused.set(true)
384437
}
385438

439+
/**
440+
* Resumes audio recorder
441+
*/
386442
fun resumeRecording() {
387-
silenceDuration = 0L
443+
silenceDuration = BigDecimal.ZERO
388444
isSkipping.set(false)
389445
isPaused.set(false)
390446
}

sample/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33
<uses-permission android:name="android.permission.RECORD_AUDIO" />
4-
4+
<uses-permission
5+
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
6+
android:maxSdkVersion="28" />
57
<application
68
android:allowBackup="true"
79
android:icon="@mipmap/ic_launcher"

0 commit comments

Comments
 (0)