Merge pull request #114 from pavelpoley/task/real-mp3
Save real mp3 files
This commit is contained in:
commit
f8f3313574
|
@ -68,4 +68,5 @@ dependencies {
|
|||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'com.github.naman14:TAndroidLame:1.1'
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
tools:replace="android:label">
|
||||
|
||||
<activity
|
||||
android:name=".activities.WidgetRecordDisplayConfigureActivity"
|
||||
|
|
|
@ -20,6 +20,7 @@ const val EXTENSION_OGG = 2
|
|||
|
||||
val BITRATES = arrayListOf(32000, 64000, 96000, 128000, 160000, 192000, 256000, 320000)
|
||||
const val DEFAULT_BITRATE = 128000
|
||||
const val SAMPLE_RATE = 44100
|
||||
|
||||
const val RECORDING_RUNNING = 0
|
||||
const val RECORDING_STOPPED = 1
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package com.simplemobiletools.voicerecorder.recorder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.media.MediaRecorder
|
||||
import com.simplemobiletools.voicerecorder.extensions.config
|
||||
import com.simplemobiletools.voicerecorder.helpers.SAMPLE_RATE
|
||||
import java.io.FileDescriptor
|
||||
|
||||
class MediaRecorderWrapper(val context: Context) : Recorder {
|
||||
|
||||
private var recorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
|
||||
setOutputFormat(context.config.getOutputFormat())
|
||||
setAudioEncoder(context.config.getAudioEncoder())
|
||||
setAudioEncodingBitRate(context.config.bitrate)
|
||||
setAudioSamplingRate(SAMPLE_RATE)
|
||||
}
|
||||
|
||||
override fun setOutputFile(path: String) {
|
||||
recorder.setOutputFile(path)
|
||||
}
|
||||
|
||||
override fun setOutputFile(fileDescriptor: FileDescriptor) {
|
||||
recorder.setOutputFile(fileDescriptor)
|
||||
}
|
||||
|
||||
override fun prepare() {
|
||||
recorder.prepare()
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
recorder.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
recorder.stop()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun pause() {
|
||||
recorder.pause()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun resume() {
|
||||
recorder.resume()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
recorder.release()
|
||||
}
|
||||
|
||||
override fun getMaxAmplitude(): Int {
|
||||
return recorder.maxAmplitude
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package com.simplemobiletools.voicerecorder.recorder
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import com.naman14.androidlame.AndroidLame
|
||||
import com.naman14.androidlame.LameBuilder
|
||||
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
|
||||
import com.simplemobiletools.voicerecorder.extensions.config
|
||||
import com.simplemobiletools.voicerecorder.helpers.SAMPLE_RATE
|
||||
import java.io.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.abs
|
||||
|
||||
class Mp3Recorder(val context: Context) : Recorder {
|
||||
private var mp3buffer: ByteArray = ByteArray(0)
|
||||
private var isPaused = AtomicBoolean(false)
|
||||
private var isStopped = AtomicBoolean(false)
|
||||
private var amplitude = AtomicInteger(0)
|
||||
private var outputPath: String? = null
|
||||
private var androidLame: AndroidLame? = null
|
||||
private var fileDescriptor: FileDescriptor? = null
|
||||
private var outputStream: FileOutputStream? = null
|
||||
private val minBufferSize = AudioRecord.getMinBufferSize(
|
||||
SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT
|
||||
)
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private val audioRecord = AudioRecord(
|
||||
MediaRecorder.AudioSource.CAMCORDER,
|
||||
SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
minBufferSize * 2
|
||||
)
|
||||
|
||||
override fun setOutputFile(path: String) {
|
||||
outputPath = path
|
||||
}
|
||||
|
||||
override fun prepare() {}
|
||||
|
||||
override fun start() {
|
||||
val rawData = ShortArray(minBufferSize)
|
||||
mp3buffer = ByteArray((7200 + rawData.size * 2 * 1.25).toInt())
|
||||
|
||||
outputStream = try {
|
||||
if (fileDescriptor != null) {
|
||||
FileOutputStream(fileDescriptor)
|
||||
} else {
|
||||
FileOutputStream(File(outputPath))
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
val androidLame = LameBuilder()
|
||||
.setInSampleRate(SAMPLE_RATE)
|
||||
.setOutBitrate(context.config.bitrate / 1000)
|
||||
.setOutSampleRate(SAMPLE_RATE)
|
||||
.setOutChannels(1)
|
||||
.build()
|
||||
|
||||
ensureBackgroundThread {
|
||||
audioRecord.startRecording()
|
||||
|
||||
while (!isStopped.get()) {
|
||||
if (!isPaused.get()) {
|
||||
val count = audioRecord.read(rawData, 0, minBufferSize)
|
||||
if (count > 0) {
|
||||
val encoded = androidLame.encode(rawData, rawData, count, mp3buffer)
|
||||
if (encoded > 0) {
|
||||
try {
|
||||
updateAmplitude(rawData)
|
||||
outputStream!!.write(mp3buffer, 0, encoded)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
isPaused.set(true)
|
||||
isStopped.set(true)
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
isPaused.set(true)
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
isPaused.set(false)
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
androidLame?.flush(mp3buffer)
|
||||
outputStream?.close()
|
||||
}
|
||||
|
||||
override fun getMaxAmplitude(): Int {
|
||||
return amplitude.get()
|
||||
}
|
||||
|
||||
override fun setOutputFile(fileDescriptor: FileDescriptor) {
|
||||
this.fileDescriptor = fileDescriptor
|
||||
}
|
||||
|
||||
private fun updateAmplitude(data: ShortArray) {
|
||||
var sum = 0L
|
||||
for (i in 0 until minBufferSize step 2) {
|
||||
sum += abs(data[i].toInt())
|
||||
}
|
||||
amplitude.set((sum / (minBufferSize / 8)).toInt())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.simplemobiletools.voicerecorder.recorder
|
||||
|
||||
import java.io.FileDescriptor
|
||||
|
||||
interface Recorder {
|
||||
fun setOutputFile(path: String)
|
||||
fun setOutputFile(fileDescriptor: FileDescriptor)
|
||||
fun prepare()
|
||||
fun start()
|
||||
fun stop()
|
||||
fun pause()
|
||||
fun resume()
|
||||
fun release()
|
||||
fun getMaxAmplitude(): Int
|
||||
}
|
|
@ -6,7 +6,6 @@ import android.app.*
|
|||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaRecorder
|
||||
import android.media.MediaScannerConnection
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
@ -24,6 +23,9 @@ import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsRelati
|
|||
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
|
||||
import com.simplemobiletools.voicerecorder.helpers.*
|
||||
import com.simplemobiletools.voicerecorder.models.Events
|
||||
import com.simplemobiletools.voicerecorder.recorder.MediaRecorderWrapper
|
||||
import com.simplemobiletools.voicerecorder.recorder.Mp3Recorder
|
||||
import com.simplemobiletools.voicerecorder.recorder.Recorder
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
@ -40,7 +42,7 @@ class RecorderService : Service() {
|
|||
private var status = RECORDING_STOPPED
|
||||
private var durationTimer = Timer()
|
||||
private var amplitudeTimer = Timer()
|
||||
private var recorder: MediaRecorder? = null
|
||||
private var recorder: Recorder? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
|
@ -86,40 +88,38 @@ class RecorderService : Service() {
|
|||
currFilePath = "$baseFolder/${getCurrentFormattedDateTime()}.${config.getExtensionText()}"
|
||||
|
||||
try {
|
||||
recorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
|
||||
setOutputFormat(config.getOutputFormat())
|
||||
setAudioEncoder(config.getAudioEncoder())
|
||||
setAudioEncodingBitRate(config.bitrate)
|
||||
setAudioSamplingRate(44100)
|
||||
|
||||
if (isRPlus() && hasProperStoredFirstParentUri(currFilePath)) {
|
||||
val fileUri = createDocumentUriUsingFirstParentTreeUri(currFilePath)
|
||||
createSAFFileSdk30(currFilePath)
|
||||
val outputFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w")!!.fileDescriptor
|
||||
setOutputFile(outputFileDescriptor)
|
||||
} else if (!isRPlus() && isPathOnSD(currFilePath)) {
|
||||
var document = getDocumentFile(currFilePath.getParentPath())
|
||||
document = document?.createFile("", currFilePath.getFilenameFromPath())
|
||||
|
||||
val outputFileDescriptor = contentResolver.openFileDescriptor(document!!.uri, "w")!!.fileDescriptor
|
||||
setOutputFile(outputFileDescriptor)
|
||||
} else {
|
||||
setOutputFile(currFilePath)
|
||||
}
|
||||
|
||||
prepare()
|
||||
start()
|
||||
duration = 0
|
||||
status = RECORDING_RUNNING
|
||||
broadcastRecorderInfo()
|
||||
startForeground(RECORDER_RUNNING_NOTIF_ID, showNotification())
|
||||
|
||||
durationTimer = Timer()
|
||||
durationTimer.scheduleAtFixedRate(getDurationUpdateTask(), 1000, 1000)
|
||||
|
||||
startAmplitudeUpdates()
|
||||
recorder = if (recordMp3()) {
|
||||
Mp3Recorder(this)
|
||||
} else {
|
||||
MediaRecorderWrapper(this)
|
||||
}
|
||||
|
||||
if (isRPlus() && hasProperStoredFirstParentUri(currFilePath)) {
|
||||
val fileUri = createDocumentUriUsingFirstParentTreeUri(currFilePath)
|
||||
createSAFFileSdk30(currFilePath)
|
||||
val outputFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w")!!.fileDescriptor
|
||||
recorder?.setOutputFile(outputFileDescriptor)
|
||||
} else if (!isRPlus() && isPathOnSD(currFilePath)) {
|
||||
var document = getDocumentFile(currFilePath.getParentPath())
|
||||
document = document?.createFile("", currFilePath.getFilenameFromPath())
|
||||
|
||||
val outputFileDescriptor = contentResolver.openFileDescriptor(document!!.uri, "w")!!.fileDescriptor
|
||||
recorder?.setOutputFile(outputFileDescriptor)
|
||||
} else {
|
||||
recorder?.setOutputFile(currFilePath)
|
||||
}
|
||||
|
||||
recorder?.prepare()
|
||||
recorder?.start()
|
||||
duration = 0
|
||||
status = RECORDING_RUNNING
|
||||
broadcastRecorderInfo()
|
||||
startForeground(RECORDER_RUNNING_NOTIF_ID, showNotification())
|
||||
|
||||
durationTimer = Timer()
|
||||
durationTimer.scheduleAtFixedRate(getDurationUpdateTask(), 1000, 1000)
|
||||
|
||||
startAmplitudeUpdates()
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
stopRecording()
|
||||
|
@ -137,7 +137,7 @@ class RecorderService : Service() {
|
|||
release()
|
||||
|
||||
ensureBackgroundThread {
|
||||
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath) ) {
|
||||
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath)) {
|
||||
addFileInNewMediaStore()
|
||||
} else {
|
||||
addFileInLegacyMediaStore()
|
||||
|
@ -233,7 +233,7 @@ class RecorderService : Service() {
|
|||
override fun run() {
|
||||
if (recorder != null) {
|
||||
try {
|
||||
EventBus.getDefault().post(Events.RecordingAmplitude(recorder!!.maxAmplitude))
|
||||
EventBus.getDefault().post(Events.RecordingAmplitude(recorder!!.getMaxAmplitude()))
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
|
@ -298,4 +298,8 @@ class RecorderService : Service() {
|
|||
private fun broadcastStatus() {
|
||||
EventBus.getDefault().post(Events.RecordingStatus(status))
|
||||
}
|
||||
|
||||
private fun recordMp3(): Boolean {
|
||||
return config.extension == EXTENSION_MP3
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue