Skip to content

Commit 6e55582

Browse files
committed
Implement Float encoding
1 parent 18a436f commit 6e55582

File tree

4 files changed

+146
-48
lines changed

4 files changed

+146
-48
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ internal fun bitPerSample(audioEncoding: Int) = when (audioEncoding) {
4242
AudioFormat.ENCODING_PCM_8BIT -> 8
4343
AudioFormat.ENCODING_PCM_16BIT -> 16
4444
AudioFormat.ENCODING_PCM_32BIT -> 32
45+
AudioFormat.ENCODING_PCM_FLOAT -> 32
4546
else -> 16
4647
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ internal class WaveHeaderWriter(private val filePath: String, private val waveCo
4848
sampleRate,
4949
channels,
5050
byteRate,
51-
bitPerSample(waveConfig.audioEncoding)
51+
bitPerSample(waveConfig.audioEncoding),
52+
waveConfig.audioEncoding == AudioFormat.ENCODING_PCM_FLOAT
5253
)
5354

5455
val randomAccessFile = RandomAccessFile(File(filePath), "rw")
@@ -59,7 +60,7 @@ internal class WaveHeaderWriter(private val filePath: String, private val waveCo
5960

6061
private fun getWavFileHeaderByteArray(
6162
totalAudioLen: Long, totalDataLen: Long, longSampleRate: Long,
62-
channels: Int, byteRate: Long, bitsPerSample: Int
63+
channels: Int, byteRate: Long, bitsPerSample: Int, isFloat: Boolean
6364
): ByteArray {
6465
val header = ByteArray(44)
6566
header[0] = 'R'.code.toByte()
@@ -82,7 +83,7 @@ internal class WaveHeaderWriter(private val filePath: String, private val waveCo
8283
header[17] = 0
8384
header[18] = 0
8485
header[19] = 0
85-
header[20] = 1
86+
header[20] = if (isFloat) 3 else 1
8687
header[21] = 0
8788
header[22] = channels.toByte()
8889
header[23] = 0
@@ -94,7 +95,7 @@ internal class WaveHeaderWriter(private val filePath: String, private val waveCo
9495
header[29] = (byteRate shr 8 and 0xff).toByte()
9596
header[30] = (byteRate shr 16 and 0xff).toByte()
9697
header[31] = (byteRate shr 24 and 0xff).toByte()
97-
header[32] = (channels * (bitsPerSample / 8)).toByte()
98+
header[32] = (channels * bitsPerSample / 8).toByte()
9899
header[33] = 0
99100
header[34] = bitsPerSample.toByte()
100101
header[35] = 0

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

Lines changed: 132 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ import android.media.AudioFormat
2929
import android.media.AudioRecord
3030
import android.media.MediaRecorder
3131
import android.media.audiofx.NoiseSuppressor
32+
import android.os.Build
33+
import androidx.annotation.RequiresApi
3234
import kotlinx.coroutines.Dispatchers
3335
import kotlinx.coroutines.GlobalScope
3436
import kotlinx.coroutines.launch
3537
import kotlinx.coroutines.withContext
38+
import java.io.DataOutputStream
3639
import java.io.File
3740
import java.nio.ByteBuffer
3841
import java.nio.ByteOrder
@@ -82,48 +85,50 @@ class WaveRecorder(private var filePath: String) {
8285
private var isPaused = false
8386
private lateinit var audioRecorder: AudioRecord
8487
private var noiseSuppressor: NoiseSuppressor? = null
85-
private var timeModulus = 1
86-
8788

8889
/**
8990
* Starts audio recording asynchronously and writes recorded data chunks on storage.
9091
*/
91-
@SuppressLint("MissingPermission")
9292
fun startRecording() {
93-
9493
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(
97116
waveConfig.sampleRate,
98117
waveConfig.channels,
99-
waveConfig.audioEncoding,
100-
AudioRecord.getMinBufferSize(
101-
waveConfig.sampleRate,
102-
waveConfig.channels,
103-
waveConfig.audioEncoding
104-
)
118+
waveConfig.audioEncoding
105119
)
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+
)
115121

116-
if (noiseSuppressorActive) {
117-
noiseSuppressor = NoiseSuppressor.create(audioRecorder.audioSessionId)
118-
}
122+
audioSessionId = audioRecorder.audioSessionId
123+
isRecording = true
124+
audioRecorder.startRecording()
119125

120-
onStateChangeListener?.let {
121-
it(RecorderState.RECORDING)
122-
}
126+
if (noiseSuppressorActive) {
127+
noiseSuppressor = NoiseSuppressor.create(audioRecorder.audioSessionId)
128+
}
123129

124-
GlobalScope.launch(Dispatchers.IO) {
125-
writeAudioDataToStorage()
126-
}
130+
onStateChangeListener?.let {
131+
it(RecorderState.RECORDING)
127132
}
128133
}
129134

@@ -144,10 +149,18 @@ class WaveRecorder(private var filePath: String) {
144149

145150
withContext(Dispatchers.Main) {
146151
onAmplitudeListener?.let {
147-
it(calculateAmplitudeMax(data))
152+
it(
153+
calculateAmplitude(
154+
data = data,
155+
audioFormat = waveConfig.audioEncoding
156+
)
157+
)
148158
}
149159
onTimeElapsed?.let {
150-
val audioLengthInSeconds: Long = file.length() / timeModulus
160+
val audioLengthInSeconds = calculateElapsedTime(
161+
file,
162+
waveConfig
163+
)
151164
it(audioLengthInSeconds)
152165
}
153166
}
@@ -160,14 +173,95 @@ class WaveRecorder(private var filePath: String) {
160173
noiseSuppressor?.release()
161174
}
162175

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)
167188

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()
169220
}
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+
171265
/** Changes @property filePath to @param newFilePath
172266
* Calling this method while still recording throws an IllegalStateException
173267
*/

sample/src/main/java/com/github/squti/androidwaverecordersample/MainActivity.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,17 @@ package com.github.squti.androidwaverecordersample
2626

2727
import android.Manifest
2828
import android.content.pm.PackageManager
29+
import android.media.AudioFormat
2930
import android.os.Bundle
3031
import android.util.Log
3132
import android.view.View
3233
import android.widget.Toast
3334
import androidx.appcompat.app.AppCompatActivity
3435
import androidx.core.app.ActivityCompat
3536
import androidx.core.content.ContextCompat
36-
import androidx.lifecycle.lifecycleScope
3737
import com.github.squti.androidwaverecorder.RecorderState
3838
import com.github.squti.androidwaverecorder.WaveRecorder
3939
import com.github.squti.androidwaverecordersample.databinding.ActivityMainBinding
40-
import kotlinx.coroutines.Dispatchers
41-
import kotlinx.coroutines.GlobalScope
42-
import kotlinx.coroutines.launch
4340
import java.util.*
4441
import java.util.concurrent.TimeUnit
4542

@@ -57,9 +54,13 @@ class MainActivity : AppCompatActivity() {
5754
binding = ActivityMainBinding.inflate(layoutInflater)
5855
setContentView(binding.root)
5956

60-
filePath = externalCacheDir?.absolutePath + "/audioFile.wav"
57+
filePath = filesDir.absolutePath + "/audioFile.wav"
6158

6259
waveRecorder = WaveRecorder(filePath)
60+
waveRecorder.waveConfig.sampleRate = 44100
61+
waveRecorder.waveConfig.channels = AudioFormat.CHANNEL_IN_STEREO
62+
waveRecorder.waveConfig.audioEncoding = AudioFormat.ENCODING_PCM_FLOAT
63+
6364

6465
waveRecorder.onStateChangeListener = {
6566
when (it) {
@@ -107,7 +108,7 @@ class MainActivity : AppCompatActivity() {
107108
binding.amplitudeTextView.text = "Amplitude : 0"
108109
binding.amplitudeTextView.visibility = View.VISIBLE
109110
waveRecorder.onAmplitudeListener = {
110-
binding.amplitudeTextView.text = "Amplitude : $it"
111+
binding.amplitudeTextView.text = "Amplitude : $it"
111112
}
112113

113114
} else {
@@ -159,6 +160,7 @@ class MainActivity : AppCompatActivity() {
159160
requestCode: Int,
160161
permissions: Array<String>, grantResults: IntArray
161162
) {
163+
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
162164
when (requestCode) {
163165
PERMISSIONS_REQUEST_RECORD_AUDIO -> {
164166
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {

0 commit comments

Comments
 (0)