WIP: Audio editor

This commit is contained in:
Ensar Sarajčić 2023-09-27 16:23:20 +02:00
parent 15d23dbca1
commit cd62cabb52
13 changed files with 815 additions and 144 deletions

View file

@ -100,5 +100,7 @@ dependencies {
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.constraintlayout)
implementation(libs.tandroidlame)
implementation(libs.bundles.audiotool)
implementation(libs.bundles.amplituda)
implementation(libs.autofittextview)
}

View file

@ -77,6 +77,8 @@
</intent-filter>
</activity>
<activity android:name=".activities.EditRecordingActivity" />
<activity
android:name=".activities.SettingsActivity"
android:configChanges="orientation"

View file

@ -0,0 +1,362 @@
package com.simplemobiletools.voicerecorder.activities
import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.provider.DocumentsContract
import android.widget.SeekBar
import androidx.core.view.children
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.databinding.ActivityEditRecordingBinding
import com.simplemobiletools.voicerecorder.extensions.deleteRecordings
import com.simplemobiletools.voicerecorder.extensions.getAllRecordings
import com.simplemobiletools.voicerecorder.helpers.getAudioFileContentUri
import com.simplemobiletools.voicerecorder.models.Recording
import linc.com.amplituda.Amplituda
import linc.com.amplituda.callback.AmplitudaSuccessListener
import linc.com.library.AudioTool
import java.io.File
import java.util.Timer
import java.util.TimerTask
class EditRecordingActivity : SimpleActivity() {
companion object {
const val RECORDING_ID = "recording_id"
}
private var player: MediaPlayer? = null
private var progressTimer = Timer()
private lateinit var recording: Recording
private lateinit var currentRecording: Recording
private var progressStart: Float = 0f
private lateinit var binding: ActivityEditRecordingBinding
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
super.onCreate(savedInstanceState)
binding = ActivityEditRecordingBinding.inflate(layoutInflater)
setContentView(binding.root)
setupOptionsMenu()
updateMaterialActivityViews(binding.mainCoordinator, binding.recordingVisualizer, useTransparentNavigation = false, useTopSearchMenu = false)
val recordingId = intent.getIntExtra(RECORDING_ID, -1)
if (recordingId == -1) {
finish()
return
}
recording = getAllRecordings().first { it.id == recordingId }
currentRecording = recording
// AudioTool.getInstance(this)
// .withAudio(File(recording.path))
// .cutAudio("00:00:00", "00:00:00.250") {}
// .saveCurrentTo(recording.path)
// .release()
// binding.recordingVisualizer.waveProgressColor = getProperPrimaryColor()
// binding.recordingVisualizer.setSampleFrom(recording.path)
binding.recordingVisualizer.chunkColor = getProperPrimaryColor()
binding.recordingVisualizer.recreate()
binding.recordingVisualizer.editListener = {
if (binding.recordingVisualizer.startPosition >= 0f) {
binding.settingsToolbar.menu.children.forEach { it.isVisible = true }
} else {
binding.settingsToolbar.menu.children.forEach { it.isVisible = false }
}
}
updateVisualization()
// android.media.MediaCodec.createByCodecName().createInputSurface()
// binding.recordingVisualizer.update()
initMediaPlayer()
playRecording(recording.path, recording.id, recording.title, recording.duration, false)
binding.playerControlsWrapper.playPauseBtn.setOnClickListener {
togglePlayPause()
}
setupColors()
}
private fun updateVisualization() {
Amplituda(this).processAudio(currentRecording.path)
.get(AmplitudaSuccessListener {
binding.recordingVisualizer.recreate()
binding.recordingVisualizer.clearEditing()
binding.recordingVisualizer.putAmplitudes(it.amplitudesAsList())
})
}
private fun setupColors() {
val properPrimaryColor = getProperPrimaryColor()
updateTextColors(binding.mainCoordinator)
val textColor = getProperTextColor()
arrayListOf(binding.playerControlsWrapper.previousBtn, binding.playerControlsWrapper.nextBtn).forEach {
it.applyColorFilter(textColor)
}
binding.playerControlsWrapper.playPauseBtn.background.applyColorFilter(properPrimaryColor)
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
private fun setupOptionsMenu() {
binding.settingsToolbar.inflateMenu(R.menu.menu_edit)
// binding.settingsToolbar.toggleHideOnScroll(false)
// binding.settingsToolbar.setupMenu()
// binding.settingsToolbar.onSearchOpenListener = {
// if (binding.viewPager.currentItem == 0) {
// binding.viewPager.currentItem = 1
// }
// }
// binding.settingsToolbar.onSearchTextChangedListener = { text ->
// getPagerAdapter()?.searchTextChanged(text)
// }
binding.settingsToolbar.menu.children.forEach { it.isVisible = false }
binding.settingsToolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.play -> {
val start = binding.recordingVisualizer.startPosition
val end = binding.recordingVisualizer.endPosition
val startMillis = start * currentRecording.duration
val durationMillis = (end - start) * currentRecording.duration
val startMillisPart = String.format("%.3f", startMillis - startMillis.toInt()).replace("0.", "")
val durationMillisPart = String.format("%.3f", durationMillis - durationMillis.toInt()).replace("0.", "")
val startFormatted = (startMillis.toInt()).getFormattedDuration(true) + ".$startMillisPart"
val durationFormatted = (durationMillis.toInt()).getFormattedDuration(true) + ".$durationMillisPart"
AudioTool.getInstance(this)
.withAudio(File(currentRecording.path))
.cutAudio(startFormatted, durationFormatted) {
progressStart = binding.recordingVisualizer.startPosition
playRecording(it.path, null, it.name, durationMillis.toInt(), true)
}
.release()
// playRecording()
}
R.id.cut -> {
val start = binding.recordingVisualizer.startPosition
val end = binding.recordingVisualizer.endPosition
val startMillis = start * currentRecording.duration
val endMillis = end * currentRecording.duration
val realEnd = (1 - end) * currentRecording.duration
val startMillisPart = String.format("%.3f", startMillis - startMillis.toInt()).replace("0.", "")
val endMillisPart = String.format("%.3f", endMillis - endMillis.toInt()).replace("0.", "")
val realEndMillisPart = String.format("%.3f", realEnd - realEnd.toInt()).replace("0.", "")
val startFormatted = (startMillis.toInt()).getFormattedDuration(true) + ".$startMillisPart"
val endFormatted = (endMillis.toInt()).getFormattedDuration(true) + ".$endMillisPart"
val realEndFormatted = (realEnd.toInt()).getFormattedDuration(true) + ".$realEndMillisPart"
var leftPart: File? = null
var rightPart: File? = null
fun merge() {
if (leftPart != null && rightPart != null) {
ensureBackgroundThread {
AudioTool.getInstance(this)
.joinAudios(arrayOf(leftPart, rightPart), "${currentRecording.path}.edit.${currentRecording.path.getFilenameExtension()}") {
runOnUiThread {
currentRecording = Recording(-1, it.name, it.path, it.lastModified().toInt(), (startMillis + realEnd).toInt(), it.getProperSize(false).toInt())
updateVisualization()
playRecording(currentRecording.path, currentRecording.id, currentRecording.title, currentRecording.duration, true)
}
}
}
}
}
AudioTool.getInstance(this)
.withAudio(File(currentRecording.path))
.cutAudio("00:00:00", startFormatted) {
leftPart = it
merge()
}
AudioTool.getInstance(this)
.withAudio(File(currentRecording.path))
.cutAudio(endFormatted, realEndFormatted) {
rightPart = it
merge()
}
}
// R.id.save -> {
// binding.recordingVisualizer.clearEditing()
// currentRecording = recording
// playRecording(currentRecording.path, currentRecording.id, currentRecording.title, currentRecording.duration, true)
// }
R.id.clear -> {
progressStart = 0f
binding.recordingVisualizer.clearEditing()
playRecording(currentRecording.path, currentRecording.id, currentRecording.title, currentRecording.duration, true)
}
R.id.reset -> {
progressStart = 0f
binding.recordingVisualizer.clearEditing()
currentRecording = recording
playRecording(currentRecording.path, currentRecording.id, currentRecording.title, currentRecording.duration, true)
}
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
private fun initMediaPlayer() {
player = MediaPlayer().apply {
setWakeMode(this@EditRecordingActivity, PowerManager.PARTIAL_WAKE_LOCK)
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnCompletionListener {
progressTimer.cancel()
binding.playerControlsWrapper.playerProgressbar.progress = binding.playerControlsWrapper.playerProgressbar.max
binding.playerControlsWrapper.playerProgressCurrent.text = binding.playerControlsWrapper.playerProgressMax.text
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
setOnPreparedListener {
// setupProgressTimer()
// player?.start()
}
}
}
fun playRecording(path: String, id: Int?, title: String?, duration: Int?, playOnPrepared: Boolean) {
resetProgress(title, duration)
// (binding.recordingsList.adapter as RecordingsAdapter).updateCurrentRecording(recording.id)
// playOnPreparation = playOnPrepared
player!!.apply {
reset()
try {
val uri = Uri.parse(path)
when {
DocumentsContract.isDocumentUri(this@EditRecordingActivity, uri) -> {
setDataSource(this@EditRecordingActivity, uri)
}
path.isEmpty() -> {
setDataSource(this@EditRecordingActivity, getAudioFileContentUri(id?.toLong() ?: 0))
}
else -> {
setDataSource(path)
}
}
} catch (e: Exception) {
showErrorToast(e)
return
}
try {
prepareAsync()
} catch (e: Exception) {
showErrorToast(e)
return
}
}
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
binding.playerControlsWrapper.playerProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
player?.seekTo(progress * 1000)
binding.playerControlsWrapper.playerProgressCurrent.text = progress.getFormattedDuration()
resumePlayback()
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
private fun setupProgressTimer() {
progressTimer.cancel()
progressTimer = Timer()
progressTimer.scheduleAtFixedRate(getProgressUpdateTask(), 100, 100)
}
private fun getProgressUpdateTask() = object : TimerTask() {
override fun run() {
Handler(Looper.getMainLooper()).post {
if (player != null) {
binding.recordingVisualizer.updateProgress(player!!.currentPosition.toFloat() / (currentRecording.duration * 1000) + progressStart)
val progress = Math.round(player!!.currentPosition / 1000.toDouble()).toInt()
updateCurrentProgress(progress)
binding.playerControlsWrapper.playerProgressbar.progress = progress
}
}
}
}
private fun updateCurrentProgress(seconds: Int) {
binding.playerControlsWrapper.playerProgressCurrent.text = seconds.getFormattedDuration()
}
private fun resetProgress(title: String?, duration: Int?) {
updateCurrentProgress(0)
binding.playerControlsWrapper.playerProgressbar.progress = 0
binding.playerControlsWrapper.playerProgressbar.max = duration ?: 0
binding.playerControlsWrapper.playerTitle.text = title ?: ""
binding.playerControlsWrapper.playerProgressMax.text = (duration ?: 0).getFormattedDuration()
}
private fun togglePlayPause() {
if (getIsPlaying()) {
pausePlayback()
} else {
resumePlayback()
}
}
private fun pausePlayback() {
player?.pause()
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
progressTimer.cancel()
}
private fun resumePlayback() {
player?.start()
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(true))
setupProgressTimer()
}
private fun getToggleButtonIcon(isPlaying: Boolean): Drawable {
val drawable = if (isPlaying) com.simplemobiletools.commons.R.drawable.ic_pause_vector else com.simplemobiletools.commons.R.drawable.ic_play_vector
return resources.getColoredDrawableWithColor(drawable, getProperPrimaryColor().getContrastColor())
}
private fun skip(forward: Boolean) {
// val curr = player?.currentPosition ?: return
// var newProgress = if (forward) curr + FAST_FORWARD_SKIP_MS else curr - FAST_FORWARD_SKIP_MS
// if (newProgress > player!!.duration) {
// newProgress = player!!.duration
// }
//
// player!!.seekTo(newProgress)
// resumePlayback()
}
private fun getIsPlaying() = player?.isPlaying == true
override fun onResume() {
super.onResume()
}
override fun onPause() {
super.onPause()
}
}

View file

@ -1,5 +1,6 @@
package com.simplemobiletools.voicerecorder.adapters
import android.content.Intent
import android.view.*
import android.widget.PopupMenu
import android.widget.TextView
@ -11,6 +12,7 @@ import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.views.MyRecyclerView
import com.simplemobiletools.voicerecorder.BuildConfig
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.EditRecordingActivity
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.databinding.ItemRecordingBinding
import com.simplemobiletools.voicerecorder.dialogs.DeleteConfirmationDialog
@ -281,6 +283,13 @@ class RecordingsAdapter(
}
}
R.id.cab_edit -> {
Intent(activity, EditRecordingActivity::class.java).apply {
putExtra(EditRecordingActivity.RECORDING_ID, recordingId)
activity.startActivity(this)
}
}
R.id.cab_delete -> {
executeItemMenuOperation(recordingId, removeAfterCallback = false) {
askConfirmDelete()

View file

@ -84,23 +84,23 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
private fun setupViews() {
binding.playPauseBtn.setOnClickListener {
if (playedRecordingIDs.empty() || binding.playerProgressbar.max == 0) {
binding.nextBtn.callOnClick()
binding.playerControlsWrapper.playPauseBtn.setOnClickListener {
if (playedRecordingIDs.empty() || binding.playerControlsWrapper.playerProgressbar.max == 0) {
binding.playerControlsWrapper.nextBtn.callOnClick()
} else {
togglePlayPause()
}
}
binding.playerProgressCurrent.setOnClickListener {
binding.playerControlsWrapper.playerProgressCurrent.setOnClickListener {
skip(false)
}
binding.playerProgressMax.setOnClickListener {
binding.playerControlsWrapper.playerProgressMax.setOnClickListener {
skip(true)
}
binding.previousBtn.setOnClickListener {
binding.playerControlsWrapper.previousBtn.setOnClickListener {
if (playedRecordingIDs.isEmpty()) {
return@setOnClickListener
}
@ -116,14 +116,14 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
playRecording(prevRecording, true)
}
binding.playerTitle.setOnLongClickListener {
if (binding.playerTitle.value.isNotEmpty()) {
context.copyToClipboard(binding.playerTitle.value)
binding.playerControlsWrapper.playerTitle.setOnLongClickListener {
if (binding.playerControlsWrapper.playerTitle.value.isNotEmpty()) {
context.copyToClipboard(binding.playerControlsWrapper.playerTitle.value)
}
true
}
binding.nextBtn.setOnClickListener {
binding.playerControlsWrapper.nextBtn.setOnClickListener {
val adapter = getRecordingsAdapter()
if (adapter == null || adapter.recordings.isEmpty()) {
return@setOnClickListener
@ -193,9 +193,9 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
setOnCompletionListener {
progressTimer.cancel()
binding.playerProgressbar.progress = binding.playerProgressbar.max
binding.playerProgressCurrent.text = binding.playerProgressMax.text
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
binding.playerControlsWrapper.playerProgressbar.progress = binding.playerControlsWrapper.playerProgressbar.max
binding.playerControlsWrapper.playerProgressCurrent.text = binding.playerControlsWrapper.playerProgressMax.text
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
setOnPreparedListener {
@ -245,12 +245,12 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
}
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(playOnPreparation))
binding.playerProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(playOnPreparation))
binding.playerControlsWrapper.playerProgressbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser && !playedRecordingIDs.isEmpty()) {
player?.seekTo(progress * 1000)
binding.playerProgressCurrent.text = progress.getFormattedDuration()
binding.playerControlsWrapper.playerProgressCurrent.text = progress.getFormattedDuration()
resumePlayback()
}
}
@ -273,22 +273,22 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
if (player != null) {
val progress = Math.round(player!!.currentPosition / 1000.toDouble()).toInt()
updateCurrentProgress(progress)
binding.playerProgressbar.progress = progress
binding.playerControlsWrapper.playerProgressbar.progress = progress
}
}
}
}
private fun updateCurrentProgress(seconds: Int) {
binding.playerProgressCurrent.text = seconds.getFormattedDuration()
binding.playerControlsWrapper.playerProgressCurrent.text = seconds.getFormattedDuration()
}
private fun resetProgress(recording: Recording?) {
updateCurrentProgress(0)
binding.playerProgressbar.progress = 0
binding.playerProgressbar.max = recording?.duration ?: 0
binding.playerTitle.text = recording?.title ?: ""
binding.playerProgressMax.text = (recording?.duration ?: 0).getFormattedDuration()
binding.playerControlsWrapper.playerProgressbar.progress = 0
binding.playerControlsWrapper.playerProgressbar.max = recording?.duration ?: 0
binding.playerControlsWrapper.playerTitle.text = recording?.title ?: ""
binding.playerControlsWrapper.playerProgressMax.text = (recording?.duration ?: 0).getFormattedDuration()
}
fun onSearchTextChanged(text: String) {
@ -307,13 +307,13 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
private fun pausePlayback() {
player?.pause()
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
progressTimer.cancel()
}
private fun resumePlayback() {
player?.start()
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(true))
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(true))
setupProgressTimer()
}
@ -352,12 +352,12 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
context.updateTextColors(binding.playerHolder)
val textColor = context.getProperTextColor()
arrayListOf(binding.previousBtn, binding.nextBtn).forEach {
arrayListOf(binding.playerControlsWrapper.previousBtn, binding.playerControlsWrapper.nextBtn).forEach {
it.applyColorFilter(textColor)
}
binding.playPauseBtn.background.applyColorFilter(properPrimaryColor)
binding.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
binding.playerControlsWrapper.playPauseBtn.background.applyColorFilter(properPrimaryColor)
binding.playerControlsWrapper.playPauseBtn.setImageDrawable(getToggleButtonIcon(false))
}
fun finishActMode() = getRecordingsAdapter()?.finishActMode()

View file

@ -0,0 +1,188 @@
package com.simplemobiletools.voicerecorder.views
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.simplemobiletools.commons.extensions.adjustAlpha
import com.simplemobiletools.commons.extensions.getProperPrimaryColor
import com.simplemobiletools.commons.extensions.getProperTextColor
import com.simplemobiletools.commons.helpers.LOWER_ALPHA
import com.visualizer.amplitude.dp
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class AudioEditorView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val chunkPaint = Paint()
private val highlightPaint = Paint()
private val progressPaint = Paint()
private var chunks = ArrayList<Float>()
private var topBottomPadding = 6.dp()
private var startX: Float = -1f
private var endX: Float = -1f
private var currentProgress: Float = 0f
private enum class Dragging {
START,
END,
NONE
}
private var dragging = Dragging.NONE
var startPosition: Float = -1f
var endPosition: Float = -1f
var editListener: (() -> Unit)? = null
var chunkColor = Color.RED
set(value) {
chunkPaint.color = value
field = value
}
private var chunkWidth = 20.dp()
set(value) {
chunkPaint.strokeWidth = value
field = value
}
private var chunkSpace = 1.dp()
var chunkMinHeight = 3.dp() // recommended size > 10 dp
var chunkRoundedCorners = false
set(value) {
if (value) {
chunkPaint.strokeCap = Paint.Cap.ROUND
} else {
chunkPaint.strokeCap = Paint.Cap.BUTT
}
field = value
}
init {
chunkPaint.strokeWidth = chunkWidth
chunkPaint.color = chunkColor
chunkRoundedCorners = false
highlightPaint.color = context.getProperPrimaryColor().adjustAlpha(LOWER_ALPHA)
progressPaint.color = context.getProperTextColor()
progressPaint.strokeWidth = 4.dp()
}
fun recreate() {
chunks.clear()
invalidate()
}
fun clearEditing() {
startX = -1f
endX = -1f
startPosition = -1f
endPosition = -1f
editListener?.invoke()
invalidate()
}
fun putAmplitudes(amplitudes: List<Int>) {
val maxAmp = amplitudes.max()
chunkWidth = (1.0f / amplitudes.size) * (2.0f / 3)
chunkSpace = chunkWidth / 2
chunks.addAll(amplitudes.map { it.toFloat() / maxAmp })
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (chunkWidth < 1f) {
chunkWidth *= width
chunkSpace = chunkWidth / 2
}
val verticalCenter = height / 2
var x = chunkSpace
val maxHeight = height - (topBottomPadding * 2)
val verticalDrawScale = maxHeight - chunkMinHeight
if (verticalDrawScale == 0f) {
return
}
chunks.forEach {
val chunkHeight = it * verticalDrawScale + chunkMinHeight
val startY = verticalCenter - chunkHeight / 2
val stopY = verticalCenter + chunkHeight / 2
canvas.drawLine(x, startY, x, stopY, chunkPaint)
x += chunkWidth + chunkSpace
}
if (startPosition >= 0f || startX >= 0f ) {
val start: Float
val end: Float
if (startX >= 0f) {
start = startX
end = endX
} else {
start = width * startPosition
end = width * endPosition
}
canvas.drawRect(start, 0f, end, height.toFloat(), highlightPaint)
}
canvas.drawLine(width * currentProgress, 0f, width * currentProgress, height.toFloat(), progressPaint)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (abs(event.x - startPosition * width) < 50.0f) {
startX = event.x
endX = endPosition * width
dragging = Dragging.START
} else if (abs(event.x - endPosition * width) < 50.0f) {
endX = event.x
startX = startPosition * width
dragging = Dragging.END
} else {
startX = event.x
endX = event.x
dragging = Dragging.END
}
}
MotionEvent.ACTION_MOVE -> {
if (dragging == Dragging.START) {
startX = event.x
} else if (dragging == Dragging.END) {
endX = event.x
}
}
MotionEvent.ACTION_UP -> {
if (dragging == Dragging.START) {
startX = event.x
} else if (dragging == Dragging.END) {
endX = event.x
}
dragging = Dragging.NONE
startPosition = min(startX, endX) / width
endPosition = max(startX, endX) / width
startX = -1f
endX = -1f
}
}
invalidate()
editListener?.invoke()
return true
}
fun updateProgress(progress: Float) {
currentProgress = progress
invalidate()
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9.64,7.64c0.23,-0.5 0.36,-1.05 0.36,-1.64 0,-2.21 -1.79,-4 -4,-4S2,3.79 2,6s1.79,4 4,4c0.59,0 1.14,-0.13 1.64,-0.36L10,12l-2.36,2.36C7.14,14.13 6.59,14 6,14c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4c0,-0.59 -0.13,-1.14 -0.36,-1.64L12,14l7,7h3v-1L9.64,7.64zM6,8c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM6,20c-1.1,0 -2,-0.89 -2,-2s0.9,-2 2,-2 2,0.89 2,2 -0.9,2 -2,2zM12,12.5c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5 0.5,0.22 0.5,0.5 -0.22,0.5 -0.5,0.5zM19,3l-6,6 2,2 7,-7L22,3z"/>
</vector>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/settings_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/color_primary"
app:title="@string/settings"
app:titleTextAppearance="@style/AppTheme.ActionBar.TitleTextStyle" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_wrapper"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize">
<com.simplemobiletools.voicerecorder.views.AudioEditorView
android:id="@+id/recording_visualizer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/player_controls_wrapper"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginStart="@dimen/big_margin"
android:layout_marginEnd="@dimen/big_margin" />
<include
android:id="@+id/player_controls_wrapper"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recording_visualizer"
layout="@layout/layout_player_controls" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<com.simplemobiletools.voicerecorder.fragments.PlayerFragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -37,120 +36,7 @@
</com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller>
<RelativeLayout
<include
android:id="@+id/player_controls_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="HardcodedText">
<View
android:id="@+id/player_divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/divider_grey"
android:importantForAccessibility="no" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/medium_margin"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="1"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/big_text_size"
tools:text="Recording title" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_progress_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/player_controls"
android:layout_alignTop="@+id/player_progressbar"
android:layout_alignBottom="@+id/player_progressbar"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:text="00:00" />
<com.simplemobiletools.commons.views.MySeekBar
android:id="@+id/player_progressbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/player_title"
android:layout_toStartOf="@+id/player_progress_max"
android:layout_toEndOf="@+id/player_progress_current"
android:clickable="true"
android:focusable="true"
android:max="0"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_progress_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/player_progressbar"
android:layout_alignBottom="@+id/player_progressbar"
android:layout_alignParentEnd="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="@dimen/medium_margin"
android:paddingEnd="@dimen/normal_margin"
android:text="00:00" />
<RelativeLayout
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/player_progressbar"
android:orientation="horizontal">
<ImageView
android:id="@+id/previous_btn"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@+id/play_pause_btn"
android:layout_alignBottom="@+id/play_pause_btn"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/play_pause_btn"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/previous"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_previous_vector" />
<ImageView
android:id="@+id/play_pause_btn"
android:layout_width="@dimen/toggle_recording_button_size"
android:layout_height="@dimen/toggle_recording_button_size"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/player_button_margin"
android:layout_marginEnd="@dimen/player_button_margin"
android:layout_marginBottom="@dimen/big_margin"
android:background="@drawable/circle_background"
android:contentDescription="@string/playpause"
android:padding="@dimen/activity_margin"
android:src="@drawable/ic_play_vector" />
<ImageView
android:id="@+id/next_btn"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@+id/play_pause_btn"
android:layout_alignBottom="@+id/play_pause_btn"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/play_pause_btn"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/next"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_next_vector" />
</RelativeLayout>
</RelativeLayout>
layout="@layout/layout_player_controls" />
</com.simplemobiletools.voicerecorder.fragments.PlayerFragment>

View file

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_controls_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="HardcodedText"
tools:showIn="@layout/fragment_player">
<View
android:id="@+id/player_divider"
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/divider_grey"
android:importantForAccessibility="no" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/medium_margin"
android:ellipsize="end"
android:gravity="center_horizontal"
android:maxLines="1"
android:padding="@dimen/activity_margin"
android:textSize="@dimen/big_text_size"
tools:text="Recording title" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_progress_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/player_controls"
android:layout_alignTop="@+id/player_progressbar"
android:layout_alignBottom="@+id/player_progressbar"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="@dimen/normal_margin"
android:paddingEnd="@dimen/medium_margin"
android:text="00:00" />
<com.simplemobiletools.commons.views.MySeekBar
android:id="@+id/player_progressbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/player_title"
android:layout_toStartOf="@+id/player_progress_max"
android:layout_toEndOf="@+id/player_progress_current"
android:clickable="true"
android:focusable="true"
android:max="0"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin" />
<com.simplemobiletools.commons.views.MyTextView
android:id="@+id/player_progress_max"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/player_progressbar"
android:layout_alignBottom="@+id/player_progressbar"
android:layout_alignParentEnd="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="@dimen/medium_margin"
android:paddingEnd="@dimen/normal_margin"
android:text="00:00" />
<RelativeLayout
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/player_progressbar"
android:orientation="horizontal">
<ImageView
android:id="@+id/previous_btn"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@+id/play_pause_btn"
android:layout_alignBottom="@+id/play_pause_btn"
android:layout_centerVertical="true"
android:layout_toStartOf="@+id/play_pause_btn"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/previous"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_previous_vector" />
<ImageView
android:id="@+id/play_pause_btn"
android:layout_width="@dimen/toggle_recording_button_size"
android:layout_height="@dimen/toggle_recording_button_size"
android:layout_centerHorizontal="true"
android:layout_marginStart="@dimen/player_button_margin"
android:layout_marginEnd="@dimen/player_button_margin"
android:layout_marginBottom="@dimen/big_margin"
android:background="@drawable/circle_background"
android:contentDescription="@string/playpause"
android:padding="@dimen/activity_margin"
android:src="@drawable/ic_play_vector" />
<ImageView
android:id="@+id/next_btn"
android:layout_width="@dimen/normal_icon_size"
android:layout_height="@dimen/normal_icon_size"
android:layout_alignTop="@+id/play_pause_btn"
android:layout_alignBottom="@+id/play_pause_btn"
android:layout_centerVertical="true"
android:layout_toEndOf="@+id/play_pause_btn"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/next"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_next_vector" />
</RelativeLayout>
</RelativeLayout>

View file

@ -18,6 +18,12 @@
android:icon="@drawable/ic_delete_vector"
android:showAsAction="always"
android:title="@string/delete" />
<item
android:id="@+id/cab_edit"
android:icon="@drawable/ic_edit_vector"
android:showAsAction="ifRoom"
android:title="@string/edit"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cab_open_with"
android:showAsAction="never"

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/play"
android:icon="@drawable/ic_play_vector"
android:title="Play"
app:showAsAction="ifRoom" />
<item
android:id="@+id/cut"
android:icon="@drawable/ic_cut_vector"
android:title="Cut"
app:showAsAction="ifRoom" />
<!-- <item-->
<!-- android:id="@+id/save"-->
<!-- android:icon="@drawable/ic_save_vector"-->
<!-- android:title="Save"-->
<!-- app:showAsAction="ifRoom" />-->
<item
android:id="@+id/reset"
android:icon="@drawable/ic_reset_vector"
android:title="Reset"
app:showAsAction="ifRoom" />
<item
android:id="@+id/clear"
android:icon="@drawable/ic_cross_vector"
android:title="Clear"
app:showAsAction="ifRoom" />
</menu>

View file

@ -15,6 +15,11 @@ room = "2.6.0-alpha02"
simple-commons = "257a2ab069"
#AudioRecordView
audiorecordview = "1.0.4"
#AudioTool
audiotool = "1.2.1"
amplituda = "2.2.2"
waveformseekbar = "5.0.1"
mobileffmpeg = "4.4"
#TAndroidLame
tandroidlame = "1.1"
#AutofitTextView
@ -24,7 +29,7 @@ gradlePlugins-agp = "8.1.1"
#build
app-build-compileSDKVersion = "34"
app-build-targetSDK = "34"
app-build-minimumSDK = "23"
app-build-minimumSDK = "24"
app-build-javaVersion = "VERSION_17"
app-build-kotlinJVMTarget = "17"
#versioning
@ -46,6 +51,11 @@ simple-tools-commons = { module = "com.github.SimpleMobileTools:Simple-Commons",
eventbus = { module = "org.greenrobot:eventbus", version.ref = "eventbus" }
#AudioRecordView
audiorecordview = { module = "com.github.Armen101:AudioRecordView", version.ref = "audiorecordview" }
#AudioTool
audiotool = { module = "com.github.lincollincol:AudioTool", version.ref = "audiotool" }
amplituda = { module = "com.github.lincollincol:amplituda", version.ref = "amplituda" }
mobileffmpeg = { module = "com.arthenica:mobile-ffmpeg-full", version.ref = "mobileffmpeg" }
waveformseekbar = { module = "com.github.massoudss:waveformSeekBar", version.ref = "waveformseekbar" }
#TAndroidLame
tandroidlame = { module = "com.github.naman14:TAndroidLame", version.ref = "tandroidlame" }
#AutofitTextView
@ -55,6 +65,14 @@ room = [
"androidx-room-ktx",
"androidx-room-runtime",
]
audiotool = [
"audiotool",
"mobileffmpeg",
]
amplituda = [
"amplituda",
"waveformseekbar",
]
[plugins]
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }