Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete recordings immediately and fix dir #42

Merged
merged 7 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 13 additions & 87 deletions app/src/main/java/app/myzel394/alibi/db/AppSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package app.myzel394.alibi.db

import android.media.MediaRecorder
import android.os.Build
import android.util.Log
import app.myzel394.alibi.R
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
Expand All @@ -19,6 +18,7 @@ data class AppSettings(
val hasSeenOnboarding: Boolean = false,
val showAdvancedSettings: Boolean = false,
val theme: Theme = Theme.SYSTEM,
val lastRecording: RecordingInformation? = null,
) {
fun setShowAdvancedSettings(showAdvancedSettings: Boolean): AppSettings {
return copy(showAdvancedSettings = showAdvancedSettings)
Expand All @@ -40,6 +40,10 @@ data class AppSettings(
return copy(theme = theme)
}

fun setLastRecording(lastRecording: RecordingInformation?): AppSettings {
return copy(lastRecording = lastRecording)
}

enum class Theme {
SYSTEM,
LIGHT,
Expand All @@ -63,7 +67,7 @@ data class AppSettings(
}

@Serializable
data class LastRecording(
data class RecordingInformation(
val folderPath: String,
@Serializable(with = LocalDateTimeSerializer::class)
val recordingStart: LocalDateTime,
Expand All @@ -72,91 +76,8 @@ data class LastRecording(
val fileExtension: String,
val forceExactMaxDuration: Boolean,
) {
val fileFolder: File
get() = File(folderPath)

val filePaths: List<File>
get() =
File(folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension

name.toIntOrNull() != null
}?.toList() ?: emptyList()

val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()

private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile = File("$folderPath/${outputFile.nameWithoutExtension}-raw.${fileExtension}")
outputFile.renameTo(rawFile)

val command = "-sseof ${maxDuration / -1000} -i $rawFile -y $outputFile"

val session = FFmpegKit.execute(command)

if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)

throw Exception("Failed to strip concatenated audio")
}
}

suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recordingStart
.format(ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("$fileFolder/$fileName.${fileExtension}")

if (outputFile.exists() && !forceConcatenation) {
return outputFile
}

val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recordingStart.format(ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${intervalDuration}'" +
" -metadata max_duration='${maxDuration}'" +
" $outputFile"

val session = FFmpegKit.execute(command)

if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)

throw Exception("Failed to concatenate audios")
}

val minRequiredForPossibleInExactMaxDuration = maxDuration / intervalDuration
if (forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}

return outputFile
}
val hasRecordingsAvailable
get() = File(folderPath).listFiles()?.isNotEmpty() ?: false
}

@Serializable
Expand All @@ -172,6 +93,7 @@ data class AudioRecorderSettings(
val outputFormat: Int? = null,
val encoder: Int? = null,
val showAllMicrophones: Boolean = false,
val deleteRecordingsImmediately: Boolean = false,
) {
fun getOutputFormat(): Int {
if (outputFormat != null) {
Expand Down Expand Up @@ -303,6 +225,10 @@ data class AudioRecorderSettings(
return copy(showAllMicrophones = showAllMicrophones)
}

fun setDeleteRecordingsImmediately(deleteRecordingsImmediately: Boolean): AudioRecorderSettings {
return copy(deleteRecordingsImmediately = deleteRecordingsImmediately)
}

fun isEncoderCompatible(encoder: Int): Boolean {
if (outputFormat == null || outputFormat == MediaRecorder.OutputFormat.DEFAULT) {
return true
Expand Down
118 changes: 118 additions & 0 deletions app/src/main/java/app/myzel394/alibi/helpers/AudioRecorderExporter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package app.myzel394.alibi.helpers

import android.content.Context
import android.util.Log
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
import java.io.File
import java.time.format.DateTimeFormatter

data class AudioRecorderExporter(
val recording: RecordingInformation,
) {
val filePaths: List<File>
get() =
File(recording.folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension

name.toIntOrNull() != null
}?.toList() ?: emptyList()

val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()

private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile =
File("${recording.folderPath}/${outputFile.nameWithoutExtension}-raw.${recording.fileExtension}")
outputFile.renameTo(rawFile)

val command = "-sseof ${recording.maxDuration / -1000} -i $rawFile -y $outputFile"

val session = FFmpegKit.execute(command)

if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)

throw Exception("Failed to strip concatenated audio")
}
}

suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recording.recordingStart
.format(DateTimeFormatter.ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("${recording.folderPath}/$fileName.${recording.fileExtension}")

if (outputFile.exists() && !forceConcatenation) {
return outputFile
}

val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${recording.intervalDuration}'" +
" -metadata max_duration='${recording.maxDuration}'" +
" $outputFile"

val session = FFmpegKit.execute(command)

if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)

throw Exception("Failed to concatenate audios")
}

val minRequiredForPossibleInExactMaxDuration =
recording.maxDuration / recording.intervalDuration
if (recording.forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}

return outputFile
}

suspend fun cleanupFiles() {
filePaths.forEach {
runCatching {
it.delete()
}
}
}

companion object {
fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME)

fun clearAllRecordings(context: Context) {
getFolder(context).deleteRecursively()
}

fun hasRecordingsAvailable(context: Context) =
getFolder(context).listFiles()?.isNotEmpty() ?: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class AudioRecorderService : IntervalRecorderService() {
var onMicrophoneReconnected: () -> Unit = {}

val filePath: String
get() = "$folder/$counter.${settings!!.fileExtension}"
get() = "${outputFolder}/$counter.${settings!!.fileExtension}"

/// Tell Android to use the correct bluetooth microphone, if any selected
private fun startAudioDevice() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
package app.myzel394.alibi.services

import android.content.Context
import android.media.MediaRecorder
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioRecorderExporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.File
import java.time.LocalDateTime
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit

abstract class IntervalRecorderService: ExtraRecorderInformationService() {
abstract class IntervalRecorderService : ExtraRecorderInformationService() {
private var job = SupervisorJob()
private var scope = CoroutineScope(Dispatchers.IO + job)

protected var counter = 0
private set
protected lateinit var folder: File

var settings: Settings? = null
protected set

private lateinit var cycleTimer: ScheduledExecutorService

fun createLastRecording(): LastRecording = LastRecording(
folderPath = folder.absolutePath,
protected val outputFolder: File
get() = AudioRecorderExporter.getFolder(this)

fun getRecordingInformation(): RecordingInformation = RecordingInformation(
folderPath = outputFolder.absolutePath,
recordingStart = recordingStart,
maxDuration = settings!!.maxDuration,
fileExtension = settings!!.fileExtension,
Expand All @@ -60,18 +58,10 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
}
}

private fun getRandomFileFolder(): String {
// uuid
val folder = UUID.randomUUID().toString()

return "${externalCacheDir!!.absolutePath}/$folder"
}

override fun start() {
super.start()

folder = File(getRandomFileFolder())
folder.mkdirs()
outputFolder.mkdirs()

scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
Expand Down Expand Up @@ -104,7 +94,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration
val earliestCounter = counter - timeMultiplier

folder.listFiles()?.forEach { file ->
outputFolder.listFiles()?.forEach { file ->
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return

if (fileCounter < earliestCounter) {
Expand All @@ -123,7 +113,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val encoder: Int,
) {
val fileExtension: String
get() = when(outputFormat) {
get() = when (outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/app/myzel394/alibi/ui/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package app.myzel394.alibi.ui

import android.os.Build
import androidx.compose.ui.unit.dp
import java.io.File

val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val MAX_AMPLITUDE = 20000
val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val RECORDER_SUBFOLDER_NAME = ".recordings"

// You are not allowed to change the constants below.
// If you do so, you will be blocked on GitHub.
Expand Down
Loading