@@ -29,10 +29,13 @@ import android.media.AudioFormat
29
29
import android.media.AudioRecord
30
30
import android.media.MediaRecorder
31
31
import android.media.audiofx.NoiseSuppressor
32
+ import android.os.Build
33
+ import androidx.annotation.RequiresApi
32
34
import kotlinx.coroutines.Dispatchers
33
35
import kotlinx.coroutines.GlobalScope
34
36
import kotlinx.coroutines.launch
35
37
import kotlinx.coroutines.withContext
38
+ import java.io.DataOutputStream
36
39
import java.io.File
37
40
import java.nio.ByteBuffer
38
41
import java.nio.ByteOrder
@@ -82,48 +85,50 @@ class WaveRecorder(private var filePath: String) {
82
85
private var isPaused = false
83
86
private lateinit var audioRecorder: AudioRecord
84
87
private var noiseSuppressor: NoiseSuppressor ? = null
85
- private var timeModulus = 1
86
-
87
88
88
89
/* *
89
90
* Starts audio recording asynchronously and writes recorded data chunks on storage.
90
91
*/
91
- @SuppressLint(" MissingPermission" )
92
92
fun startRecording () {
93
-
94
93
if (! isAudioRecorderInitialized()) {
95
- audioRecorder = AudioRecord (
96
- MediaRecorder .AudioSource .MIC ,
94
+ initializeAudioRecorder()
95
+ GlobalScope .launch(Dispatchers .IO ) {
96
+ if (waveConfig.audioEncoding == AudioFormat .ENCODING_PCM_FLOAT ) {
97
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
98
+ writeFloatAudioDataToStorage()
99
+ } else {
100
+ throw UnsupportedOperationException (" Float audio is not supported on this version of Android. You need Android Android 6.0 or above" )
101
+ }
102
+ } else
103
+ writeAudioDataToStorage()
104
+ }
105
+ }
106
+ }
107
+
108
+ @SuppressLint(" MissingPermission" )
109
+ private fun initializeAudioRecorder () {
110
+ audioRecorder = AudioRecord (
111
+ MediaRecorder .AudioSource .MIC ,
112
+ waveConfig.sampleRate,
113
+ waveConfig.channels,
114
+ waveConfig.audioEncoding,
115
+ AudioRecord .getMinBufferSize(
97
116
waveConfig.sampleRate,
98
117
waveConfig.channels,
99
- waveConfig.audioEncoding,
100
- AudioRecord .getMinBufferSize(
101
- waveConfig.sampleRate,
102
- waveConfig.channels,
103
- waveConfig.audioEncoding
104
- )
118
+ waveConfig.audioEncoding
105
119
)
106
- timeModulus = bitPerSample(waveConfig.audioEncoding) * waveConfig.sampleRate / 8
107
- if (waveConfig.channels == AudioFormat .CHANNEL_IN_STEREO )
108
- timeModulus * = 2
109
-
110
- audioSessionId = audioRecorder.audioSessionId
111
-
112
- isRecording = true
113
-
114
- audioRecorder.startRecording()
120
+ )
115
121
116
- if (noiseSuppressorActive) {
117
- noiseSuppressor = NoiseSuppressor .create(audioRecorder.audioSessionId)
118
- }
122
+ audioSessionId = audioRecorder.audioSessionId
123
+ isRecording = true
124
+ audioRecorder.startRecording()
119
125
120
- onStateChangeListener?. let {
121
- it( RecorderState . RECORDING )
122
- }
126
+ if (noiseSuppressorActive) {
127
+ noiseSuppressor = NoiseSuppressor .create(audioRecorder.audioSessionId )
128
+ }
123
129
124
- GlobalScope .launch(Dispatchers .IO ) {
125
- writeAudioDataToStorage()
126
- }
130
+ onStateChangeListener?.let {
131
+ it(RecorderState .RECORDING )
127
132
}
128
133
}
129
134
@@ -144,10 +149,18 @@ class WaveRecorder(private var filePath: String) {
144
149
145
150
withContext(Dispatchers .Main ) {
146
151
onAmplitudeListener?.let {
147
- it(calculateAmplitudeMax(data))
152
+ it(
153
+ calculateAmplitude(
154
+ data = data,
155
+ audioFormat = waveConfig.audioEncoding
156
+ )
157
+ )
148
158
}
149
159
onTimeElapsed?.let {
150
- val audioLengthInSeconds: Long = file.length() / timeModulus
160
+ val audioLengthInSeconds = calculateElapsedTime(
161
+ file,
162
+ waveConfig
163
+ )
151
164
it(audioLengthInSeconds)
152
165
}
153
166
}
@@ -160,14 +173,95 @@ class WaveRecorder(private var filePath: String) {
160
173
noiseSuppressor?.release()
161
174
}
162
175
163
- private fun calculateAmplitudeMax (data : ByteArray ): Int {
164
- val shortData = ShortArray (data.size / 2 )
165
- ByteBuffer .wrap(data).order(ByteOrder .LITTLE_ENDIAN ).asShortBuffer()
166
- .get(shortData)
176
+ @RequiresApi(Build .VERSION_CODES .M )
177
+ private suspend fun writeFloatAudioDataToStorage () {
178
+ val bufferSize = AudioRecord .getMinBufferSize(
179
+ waveConfig.sampleRate,
180
+ waveConfig.channels,
181
+ waveConfig.audioEncoding
182
+ )
183
+ val data = FloatArray (bufferSize)
184
+ val file = File (filePath)
185
+ val outputStream = DataOutputStream (file.outputStream())
186
+ while (isRecording) {
187
+ val operationStatus = audioRecorder.read(data, 0 , bufferSize, AudioRecord .READ_BLOCKING )
167
188
168
- return shortData.maxOrNull()?.toInt() ? : 0
189
+ if (AudioRecord .ERROR_INVALID_OPERATION != operationStatus) {
190
+ if (! isPaused) {
191
+ data.forEach {
192
+ val bytes =
193
+ ByteBuffer .allocate(4 ).order(ByteOrder .LITTLE_ENDIAN ).putFloat(it)
194
+ .array()
195
+ outputStream.write(bytes)
196
+ }
197
+ }
198
+
199
+ withContext(Dispatchers .Main ) {
200
+ onAmplitudeListener?.let {
201
+ it(
202
+ calculateAmplitude(data)
203
+ )
204
+ }
205
+ onTimeElapsed?.let {
206
+ val audioLengthInSeconds = calculateElapsedTime(
207
+ file,
208
+ waveConfig
209
+ )
210
+ it(audioLengthInSeconds)
211
+ }
212
+ }
213
+
214
+
215
+ }
216
+ }
217
+
218
+ outputStream.close()
219
+ noiseSuppressor?.release()
169
220
}
170
-
221
+
222
+ private fun calculateAmplitude (data : ByteArray , audioFormat : Int ): Int {
223
+ return when (audioFormat) {
224
+ AudioFormat .ENCODING_PCM_8BIT -> {
225
+ val scaleFactor = 32767.0 / 255.0
226
+ println (data.average().plus(128 ) * scaleFactor)
227
+ (data.average().plus(128 ) * scaleFactor).toInt()
228
+ }
229
+
230
+ AudioFormat .ENCODING_PCM_16BIT -> {
231
+ val shortData = ShortArray (data.size / 2 )
232
+ ByteBuffer .wrap(data).order(ByteOrder .LITTLE_ENDIAN ).asShortBuffer().get(shortData)
233
+ shortData.maxOrNull()?.toInt() ? : 0
234
+ }
235
+
236
+ AudioFormat .ENCODING_PCM_32BIT -> {
237
+ val intData = IntArray (data.size / 4 )
238
+ ByteBuffer .wrap(data).order(ByteOrder .LITTLE_ENDIAN ).asIntBuffer().get(intData)
239
+ val maxAmplitude = intData.maxOrNull() ? : 0
240
+ val scaledAmplitude = ((maxAmplitude / Int .MAX_VALUE .toFloat()) * 32768 ).toInt()
241
+ scaledAmplitude
242
+ }
243
+
244
+ else -> throw IllegalArgumentException (" Unsupported audio format for encoding $audioFormat bit" )
245
+ }
246
+ }
247
+
248
+ private fun calculateAmplitude (data : FloatArray ): Int {
249
+ val maxFloatAmplitude = data.maxOrNull() ? : 0f
250
+ return (maxFloatAmplitude * 32768 ).toInt()
251
+ }
252
+
253
+ private fun calculateElapsedTime (audioFile : File , waveConfig : WaveConfig ): Long {
254
+ val bytesPerSample = bitPerSample(waveConfig.audioEncoding) / 8
255
+ val channelNumbers = when (waveConfig.channels) {
256
+ AudioFormat .CHANNEL_IN_MONO -> 1
257
+ AudioFormat .CHANNEL_IN_STEREO -> 2
258
+ else -> throw IllegalArgumentException (" Unsupported audio channel" )
259
+ }
260
+ val totalSamplesRead = (audioFile.length() / bytesPerSample) / channelNumbers
261
+ return (totalSamplesRead / waveConfig.sampleRate)
262
+ }
263
+
264
+
171
265
/* * Changes @property filePath to @param newFilePath
172
266
* Calling this method while still recording throws an IllegalStateException
173
267
*/
0 commit comments