diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c6ad72f..0d6db3a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -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)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ab9eecb..b2eac55 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -77,6 +77,8 @@
+
+
= 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).apply {
+ try {
+ val uri = Uri.parse(currentRecording.path)
+
+ fun handleAmplitudes(amplitudaResult: AmplitudaResult<*>) {
+ binding.recordingVisualizer.recreate()
+ binding.recordingVisualizer.clearEditing()
+ binding.recordingVisualizer.putAmplitudes(amplitudaResult.amplitudesAsList())
+ }
+
+ when {
+ DocumentsContract.isDocumentUri(this@EditRecordingActivity, uri) -> {
+ processAudio(contentResolver.openInputStream(uri)).get(AmplitudaSuccessListener {
+ handleAmplitudes(it)
+ })
+ }
+
+ currentRecording.path.isEmpty() -> {
+ processAudio(contentResolver.openInputStream(getAudioFileContentUri(currentRecording.id.toLong()))).get(AmplitudaSuccessListener {
+ handleAmplitudes(it)
+ })
+ }
+
+ else -> {
+ processAudio(currentRecording.path).get(AmplitudaSuccessListener {
+ handleAmplitudes(it)
+ })
+ }
+ }
+ } catch (e: Exception) {
+ showErrorToast(e)
+ return
+ }
+ }
+ }
+
+ 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"
+ modifyAudioFile(currentRecording)
+ .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 {
+ val tempFile = File.createTempFile("${currentRecording.title}.edit.", ".${currentRecording.title.getFilenameExtension()}", cacheDir)
+ AudioTool.getInstance(this)
+ .joinAudios(arrayOf(leftPart, rightPart), tempFile.path) {
+ 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)
+ }
+ }
+ }
+ }
+ }
+
+ modifyAudioFile(currentRecording)
+ .cutAudio("00:00:00", startFormatted) {
+ leftPart = it
+ merge()
+ }
+ modifyAudioFile(currentRecording)
+ .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
+ updateVisualization()
+ 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()
+ }
+
+ private fun modifyAudioFile(recording: Recording): AudioTool {
+ return AudioTool.getInstance(this)
+ .withAudio(copyToTempFile(recording))
+ }
+
+ private fun copyToTempFile(recording: Recording): File {
+ try {
+ val uri = Uri.parse(recording.path)
+
+ when {
+ DocumentsContract.isDocumentUri(this@EditRecordingActivity, uri) -> {
+ val tempFile = File.createTempFile(recording.title, ".${recording.title.getFilenameExtension()}", cacheDir)
+ contentResolver.openInputStream(uri)?.copyTo(tempFile.outputStream())
+ return tempFile
+ }
+
+ recording.path.isEmpty() -> {
+ val tempFile = File.createTempFile(recording.title, ".${recording.title.getFilenameExtension()}", cacheDir)
+ contentResolver.openInputStream(getAudioFileContentUri(recording.id.toLong()))?.copyTo(tempFile.outputStream())
+ return tempFile
+ }
+
+ else -> {
+ return File(recording.path)
+ }
+ }
+ } catch (e: Exception) {
+ showErrorToast(e)
+ return File(recording.path)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
index 03dd35f..e790b38 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
@@ -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()
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
index 60116d8..48793ca 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
@@ -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()
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/views/AudioEditorView.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/views/AudioEditorView.kt
new file mode 100644
index 0000000..bd6b371
--- /dev/null
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/views/AudioEditorView.kt
@@ -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()
+ 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) {
+ 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()
+ }
+}
diff --git a/app/src/main/res/drawable/ic_cut_vector.xml b/app/src/main/res/drawable/ic_cut_vector.xml
new file mode 100644
index 0000000..c849af4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cut_vector.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_edit_recording.xml b/app/src/main/res/layout/activity_edit_recording.xml
new file mode 100644
index 0000000..7c715bd
--- /dev/null
+++ b/app/src/main/res/layout/activity_edit_recording.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml
index 976a3c9..d112c85 100644
--- a/app/src/main/res/layout/fragment_player.xml
+++ b/app/src/main/res/layout/fragment_player.xml
@@ -1,7 +1,6 @@
@@ -37,120 +36,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ layout="@layout/layout_player_controls" />
diff --git a/app/src/main/res/layout/layout_player_controls.xml b/app/src/main/res/layout/layout_player_controls.xml
new file mode 100644
index 0000000..ce2d955
--- /dev/null
+++ b/app/src/main/res/layout/layout_player_controls.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/cab_recordings.xml b/app/src/main/res/menu/cab_recordings.xml
index 412bc37..1b61323 100644
--- a/app/src/main/res/menu/cab_recordings.xml
+++ b/app/src/main/res/menu/cab_recordings.xml
@@ -18,6 +18,12 @@
android:icon="@drawable/ic_delete_vector"
android:showAsAction="always"
android:title="@string/delete" />
+
-
+
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e78e902..03b0685 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }