Skip to content

Commit

Permalink
Merge pull request #5 from Myzel394/improve-architecture
Browse files Browse the repository at this point in the history
Improve architecture
  • Loading branch information
Myzel394 authored Aug 8, 2023
2 parents ca1e043 + 9d1966e commit 28e3a06
Show file tree
Hide file tree
Showing 21 changed files with 984 additions and 458 deletions.
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<service android:name=".services.RecorderService" android:foregroundServiceType="microphone" />
<service android:name=".services.AudioRecorderService" android:foregroundServiceType="microphone|camera" />
</application>

</manifest>
6 changes: 5 additions & 1 deletion app/src/main/java/app/myzel394/alibi/NotificationHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import android.os.Build
import androidx.annotation.RequiresApi

object NotificationHelper {
const val RECORDER_CHANNEL_ID = "recorder"
const val RECORDER_CHANNEL_NOTIFICATION_ID = 1

@RequiresApi(Build.VERSION_CODES.O)
fun createChannels(context: Context) {
val channel = NotificationChannel(
"recorder",
RECORDER_CHANNEL_ID,
context.resources.getString(R.string.notificationChannels_recorder_name),
android.app.NotificationManager.IMPORTANCE_LOW,
)
Expand All @@ -19,4 +22,5 @@ object NotificationHelper {
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}

}
104 changes: 104 additions & 0 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,14 @@ package app.myzel394.alibi.db

import android.media.MediaRecorder
import android.os.Build
import android.util.Log
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter.ISO_DATE_TIME

@Serializable
data class AppSettings(
Expand All @@ -27,6 +34,103 @@ data class AppSettings(
}
}

@Serializable
data class LastRecording(
val folderPath: String,
@Serializable(with = LocalDateTimeSerializer::class)
val recordingStart: LocalDateTime,
val maxDuration: Long,
val intervalDuration: Long,
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
}
}

@Serializable
data class AudioRecorderSettings(
val maxDuration: Long = 30 * 60 * 1000L,
Expand Down
21 changes: 20 additions & 1 deletion app/src/main/java/app/myzel394/alibi/db/AppSettingsSerializer.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package app.myzel394.alibi.db

import androidx.datastore.core.Serializer
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import java.io.InputStream
import java.io.OutputStream
import java.time.LocalDateTime

class AppSettingsSerializer: Serializer<AppSettings> {
override val defaultValue: AppSettings = AppSettings.getDefaultInstance()
Expand All @@ -30,4 +37,16 @@ class AppSettingsSerializer: Serializer<AppSettings> {
).encodeToByteArray()
)
}
}
}

class LocalDateTimeSerializer: KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}

override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/app/myzel394/alibi/enums/RecorderState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.myzel394.alibi.enums

enum class RecorderState {
IDLE,
RECORDING,
PAUSED,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package app.myzel394.alibi.services

import android.media.MediaRecorder
import android.os.Build

class AudioRecorderService: IntervalRecorderService() {
var amplitudesAmount = 1000

var recorder: MediaRecorder? = null
private set

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

private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
MediaRecorder()
}.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(filePath)
setOutputFormat(settings!!.outputFormat)
setAudioEncoder(settings!!.encoder)
setAudioEncodingBitRate(settings!!.bitRate)
setAudioSamplingRate(settings!!.samplingRate)
}
}

private fun resetRecorder() {
runCatching {
recorder?.let {
it.stop()
it.release()
}
}
}

override fun startNewCycle() {
super.startNewCycle()

val newRecorder = createRecorder().also {
it.prepare()
}

resetRecorder()

newRecorder.start()
recorder = newRecorder
}

override fun pause() {
super.pause()

resetRecorder()
}

override fun stop() {
super.stop()

resetRecorder()
}

override fun getAmplitudeAmount(): Int = amplitudesAmount

override fun getAmplitude(): Int = recorder?.maxAmplitude ?: 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package app.myzel394.alibi.services

import android.os.Handler
import android.os.Looper
import app.myzel394.alibi.enums.RecorderState
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit

abstract class ExtraRecorderInformationService: RecorderService() {
abstract fun getAmplitudeAmount(): Int
abstract fun getAmplitude(): Int

var amplitudes = mutableListOf<Int>()
private set

private val handler = Handler(Looper.getMainLooper())

var onAmplitudeChange: ((List<Int>) -> Unit)? = null

private fun updateAmplitude() {
if (state !== RecorderState.RECORDING) {
return
}

amplitudes.add(getAmplitude())
onAmplitudeChange?.invoke(amplitudes)

// Delete old amplitudes
if (amplitudes.size > getAmplitudeAmount()) {
amplitudes.drop(amplitudes.size - getAmplitudeAmount())
}

handler.postDelayed(::updateAmplitude, 100)
}

private fun createAmplitudesTimer() {
handler.postDelayed(::updateAmplitude, 100)
}

override fun start() {
createAmplitudesTimer()
}

override fun resume() {
createAmplitudesTimer()
}

}
Loading

0 comments on commit 28e3a06

Please sign in to comment.