mirror of
https://github.com/SimpleMobileTools/Simple-Voice-Recorder.git
synced 2025-04-01 12:10:17 +02:00
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.documentfile:documentfile:1.0.1'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'com.github.naman14:TAndroidLame:1.1'
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
tools:replace="android:label">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.WidgetRecordDisplayConfigureActivity"
|
android:name=".activities.WidgetRecordDisplayConfigureActivity"
|
||||||
|
@ -20,6 +20,7 @@ const val EXTENSION_OGG = 2
|
|||||||
|
|
||||||
val BITRATES = arrayListOf(32000, 64000, 96000, 128000, 160000, 192000, 256000, 320000)
|
val BITRATES = arrayListOf(32000, 64000, 96000, 128000, 160000, 192000, 256000, 320000)
|
||||||
const val DEFAULT_BITRATE = 128000
|
const val DEFAULT_BITRATE = 128000
|
||||||
|
const val SAMPLE_RATE = 44100
|
||||||
|
|
||||||
const val RECORDING_RUNNING = 0
|
const val RECORDING_RUNNING = 0
|
||||||
const val RECORDING_STOPPED = 1
|
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.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.MediaRecorder
|
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
@ -24,6 +23,9 @@ import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsRelati
|
|||||||
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
|
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
|
||||||
import com.simplemobiletools.voicerecorder.helpers.*
|
import com.simplemobiletools.voicerecorder.helpers.*
|
||||||
import com.simplemobiletools.voicerecorder.models.Events
|
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 org.greenrobot.eventbus.EventBus
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@ -40,7 +42,7 @@ class RecorderService : Service() {
|
|||||||
private var status = RECORDING_STOPPED
|
private var status = RECORDING_STOPPED
|
||||||
private var durationTimer = Timer()
|
private var durationTimer = Timer()
|
||||||
private var amplitudeTimer = Timer()
|
private var amplitudeTimer = Timer()
|
||||||
private var recorder: MediaRecorder? = null
|
private var recorder: Recorder? = null
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
@ -86,40 +88,38 @@ class RecorderService : Service() {
|
|||||||
currFilePath = "$baseFolder/${getCurrentFormattedDateTime()}.${config.getExtensionText()}"
|
currFilePath = "$baseFolder/${getCurrentFormattedDateTime()}.${config.getExtensionText()}"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
recorder = MediaRecorder().apply {
|
recorder = if (recordMp3()) {
|
||||||
setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
|
Mp3Recorder(this)
|
||||||
setOutputFormat(config.getOutputFormat())
|
} else {
|
||||||
setAudioEncoder(config.getAudioEncoder())
|
MediaRecorderWrapper(this)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (e: Exception) {
|
||||||
showErrorToast(e)
|
showErrorToast(e)
|
||||||
stopRecording()
|
stopRecording()
|
||||||
@ -137,7 +137,7 @@ class RecorderService : Service() {
|
|||||||
release()
|
release()
|
||||||
|
|
||||||
ensureBackgroundThread {
|
ensureBackgroundThread {
|
||||||
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath) ) {
|
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath)) {
|
||||||
addFileInNewMediaStore()
|
addFileInNewMediaStore()
|
||||||
} else {
|
} else {
|
||||||
addFileInLegacyMediaStore()
|
addFileInLegacyMediaStore()
|
||||||
@ -233,7 +233,7 @@ class RecorderService : Service() {
|
|||||||
override fun run() {
|
override fun run() {
|
||||||
if (recorder != null) {
|
if (recorder != null) {
|
||||||
try {
|
try {
|
||||||
EventBus.getDefault().post(Events.RecordingAmplitude(recorder!!.maxAmplitude))
|
EventBus.getDefault().post(Events.RecordingAmplitude(recorder!!.getMaxAmplitude()))
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -298,4 +298,8 @@ class RecorderService : Service() {
|
|||||||
private fun broadcastStatus() {
|
private fun broadcastStatus() {
|
||||||
EventBus.getDefault().post(Events.RecordingStatus(status))
|
EventBus.getDefault().post(Events.RecordingStatus(status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun recordMp3(): Boolean {
|
||||||
|
return config.extension == EXTENSION_MP3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user