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" }