chore: Move code for better readability

This commit is contained in:
Myzel394 2023-12-01 00:29:41 +01:00
parent c40361aced
commit 65be9a1e3e
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
2 changed files with 198 additions and 168 deletions

View File

@ -21,130 +21,23 @@ class AudioRecorderService :
IntervalRecorderService<AudioRecorderService.Settings, RecordingInformation>() { IntervalRecorderService<AudioRecorderService.Settings, RecordingInformation>() {
override var batchesFolder: BatchesFolder = AudioBatchesFolder.viaInternalFolder(this) override var batchesFolder: BatchesFolder = AudioBatchesFolder.viaInternalFolder(this)
private val handler = Handler(Looper.getMainLooper())
var amplitudes = mutableListOf<Int>()
private set
var amplitudesAmount = 1000 var amplitudesAmount = 1000
var selectedMicrophone: MicrophoneInfo? = null var selectedMicrophone: MicrophoneInfo? = null
var recorder: MediaRecorder? = null var recorder: MediaRecorder? = null
private set private set
// Callbacks
var onSelectedMicrophoneChange: (MicrophoneInfo?) -> Unit = {} var onSelectedMicrophoneChange: (MicrophoneInfo?) -> Unit = {}
var onMicrophoneDisconnected: () -> Unit = {} var onMicrophoneDisconnected: () -> Unit = {}
var onMicrophoneReconnected: () -> Unit = {} var onMicrophoneReconnected: () -> Unit = {}
var amplitudes = mutableListOf<Int>()
private set
private val handler = Handler(Looper.getMainLooper())
var onAmplitudeChange: ((List<Int>) -> Unit)? = null 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()) {
// Should be more efficient than dropping the elements, getting a new list
// clearing old list and adding new elements to it
repeat(amplitudes.size - getAmplitudeAmount()) {
amplitudes.removeAt(0)
}
}
handler.postDelayed(::updateAmplitude, 100)
}
private fun createAmplitudesTimer() {
handler.postDelayed(::updateAmplitude, 100)
}
/// Tell Android to use the correct bluetooth microphone, if any selected
private fun startAudioDevice() {
if (selectedMicrophone == null) {
return
}
val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManger.setCommunicationDevice(selectedMicrophone!!.deviceInfo)
} else {
audioManger.startBluetoothSco()
}
}
private fun clearAudioDevice() {
val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManger.clearCommunicationDevice()
} else {
audioManger.stopBluetoothSco()
}
}
private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
MediaRecorder()
}.apply {
// Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
// and Redmi Buds 3 Pro:
// - MIC: Uses the bottom microphone of the phone (17)
// - CAMCORDER: Uses the top microphone of the phone (2)
// - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17)
// - DEFAULT: Uses the bottom microphone of the phone (17)
setAudioSource(MediaRecorder.AudioSource.MIC)
when (batchesFolder.type) {
BatchesFolder.BatchType.INTERNAL -> {
setOutputFile(
batchesFolder.asInternalGetOutputPath(counter, settings.fileExtension)
)
}
BatchesFolder.BatchType.CUSTOM -> {
setOutputFile(
batchesFolder.asCustomGetFileDescriptor(counter, settings.fileExtension)
)
}
}
setOutputFormat(settings.outputFormat)
setAudioEncoder(settings.encoder)
setAudioEncodingBitRate(settings.bitRate)
setAudioSamplingRate(settings.samplingRate)
setOnErrorListener(OnErrorListener { _, _, _ ->
onError()
})
}
}
private fun resetRecorder() {
runCatching {
recorder?.let {
it.stop()
it.release()
}
clearAudioDevice()
batchesFolder.cleanup()
}
}
override fun getRecordingInformation() = RecordingInformation(
folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart,
maxDuration = settings.maxDuration,
fileExtension = settings.fileExtension,
intervalDuration = settings.intervalDuration,
type = RecordingInformation.Type.AUDIO,
)
override fun startNewCycle() { override fun startNewCycle() {
super.startNewCycle() super.startNewCycle()
@ -189,6 +82,7 @@ class AudioRecorderService :
createAmplitudesTimer() createAmplitudesTimer()
} }
// ==== Amplitude related ====
private fun getAmplitudeAmount(): Int = amplitudesAmount private fun getAmplitudeAmount(): Int = amplitudesAmount
private fun getAmplitude(): Int { private fun getAmplitude(): Int {
@ -201,6 +95,109 @@ class AudioRecorderService :
} }
} }
private fun updateAmplitude() {
if (state !== RecorderState.RECORDING) {
return
}
amplitudes.add(getAmplitude())
onAmplitudeChange?.invoke(amplitudes)
// Delete old amplitudes
if (amplitudes.size > getAmplitudeAmount()) {
// Should be more efficient than dropping the elements, getting a new list
// clearing old list and adding new elements to it
repeat(amplitudes.size - getAmplitudeAmount()) {
amplitudes.removeAt(0)
}
}
handler.postDelayed(::updateAmplitude, 100)
}
private fun createAmplitudesTimer() {
handler.postDelayed(::updateAmplitude, 100)
}
// ==== Audio device related ====
/// Tell Android to use the correct bluetooth microphone, if any selected
private fun startAudioDevice() {
if (selectedMicrophone == null) {
return
}
val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManger.setCommunicationDevice(selectedMicrophone!!.deviceInfo)
} else {
audioManger.startBluetoothSco()
}
}
private fun clearAudioDevice() {
val audioManger = getSystemService(AUDIO_SERVICE)!! as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
audioManger.clearCommunicationDevice()
} else {
audioManger.stopBluetoothSco()
}
}
// ==== Actual recording related ====
private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
MediaRecorder()
}.apply {
// Audio Source is kinda strange, here are my experimental findings using a Pixel 7 Pro
// and Redmi Buds 3 Pro:
// - MIC: Uses the bottom microphone of the phone (17)
// - CAMCORDER: Uses the top microphone of the phone (2)
// - VOICE_COMMUNICATION: Uses the bottom microphone of the phone (17)
// - DEFAULT: Uses the bottom microphone of the phone (17)
setAudioSource(MediaRecorder.AudioSource.MIC)
when (batchesFolder.type) {
BatchesFolder.BatchType.INTERNAL -> {
setOutputFile(
batchesFolder.asInternalGetOutputPath(counter, settings.fileExtension)
)
}
BatchesFolder.BatchType.CUSTOM -> {
setOutputFile(
batchesFolder.asCustomGetFileDescriptor(counter, settings.fileExtension)
)
}
}
setOutputFormat(settings.outputFormat)
setAudioEncoder(settings.encoder)
setAudioEncodingBitRate(settings.bitRate)
setAudioSamplingRate(settings.samplingRate)
setOnErrorListener(OnErrorListener { _, _, _ ->
onError()
})
}
}
// ==== Microphone related ====
private fun resetRecorder() {
runCatching {
recorder?.let {
it.stop()
it.release()
}
clearAudioDevice()
batchesFolder.cleanup()
}
}
fun changeMicrophone(microphone: MicrophoneInfo?) { fun changeMicrophone(microphone: MicrophoneInfo?) {
selectedMicrophone = microphone selectedMicrophone = microphone
onSelectedMicrophoneChange(microphone) onSelectedMicrophoneChange(microphone)
@ -263,6 +260,16 @@ class AudioRecorderService :
audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
} }
// ==== Settings ====
override fun getRecordingInformation() = RecordingInformation(
folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart,
maxDuration = settings.maxDuration,
fileExtension = settings.fileExtension,
intervalDuration = settings.intervalDuration,
type = RecordingInformation.Type.AUDIO,
)
data class Settings( data class Settings(
override val maxDuration: Long, override val maxDuration: Long,
override val intervalDuration: Long, override val intervalDuration: Long,

View File

@ -3,7 +3,9 @@ package app.myzel394.alibi.services
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Range import android.util.Range
import androidx.camera.core.Camera import androidx.camera.core.Camera
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.TorchState
import androidx.camera.core.impl.CameraConfig import androidx.camera.core.impl.CameraConfig
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FileOutputOptions import androidx.camera.video.FileOutputOptions
@ -47,6 +49,64 @@ class VideoRecorderService :
private var selectedCamera: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA private var selectedCamera: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
var cameraControl: CameraControl? = null
private set
override fun start() {
super.start()
scope.launch {
openCamera()
}
}
override suspend fun stop() {
super.stop()
stopActiveRecording()
withTimeoutOrNull(CAMERA_CLOSE_TIMEOUT) {
// Camera can only be closed after the recording has been finalized
_cameraClosedListener.await()
}
closeCamera()
}
override fun pause() {
super.pause()
stopActiveRecording()
}
@SuppressLint("MissingPermission")
override fun startNewCycle() {
super.startNewCycle()
fun action() {
activeRecording?.stop()
val newRecording = prepareVideoRecording()
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
if (event is VideoRecordEvent.Finalize) {
_cameraClosedListener.complete(Unit)
}
}
}
if (_cameraAvailableListener.isCompleted) {
action()
} else {
// Race condition of `startNewCycle` being called before `invpkeOnCompletion`
// has been called can be ignored, as the camera usually opens within 5 seconds
// and the interval can't be set shorter than 10 seconds.
_cameraAvailableListener.invokeOnCompletion {
action()
}
}
}
// Runs a function in the main thread // Runs a function in the main thread
private fun runOnMain(callback: () -> Unit) { private fun runOnMain(callback: () -> Unit) {
val mainHandler = ContextCompat.getMainExecutor(this) val mainHandler = ContextCompat.getMainExecutor(this)
@ -88,6 +148,7 @@ class VideoRecorderService :
selectedCamera, selectedCamera,
videoCapture videoCapture
) )
cameraControl = CameraControl(camera!!)
_cameraAvailableListener.complete(Unit) _cameraAvailableListener.complete(Unit)
} }
@ -108,33 +169,6 @@ class VideoRecorderService :
camera = null camera = null
} }
override fun start() {
super.start()
scope.launch {
openCamera()
}
}
override suspend fun stop() {
super.stop()
stopActiveRecording()
withTimeoutOrNull(CAMERA_CLOSE_TIMEOUT) {
// Camera can only be closed after the recording has been finalized
_cameraClosedListener.await()
}
closeCamera()
}
override fun pause() {
super.pause()
stopActiveRecording()
}
// `resume` override not needed as `startNewCycle` is called by `IntervalRecorderService` // `resume` override not needed as `startNewCycle` is called by `IntervalRecorderService`
private fun stopActiveRecording() { private fun stopActiveRecording() {
@ -147,33 +181,6 @@ class VideoRecorderService :
.prepareRecording(this, settings.getOutputOptions(this)) .prepareRecording(this, settings.getOutputOptions(this))
.withAudioEnabled() .withAudioEnabled()
@SuppressLint("MissingPermission")
override fun startNewCycle() {
super.startNewCycle()
fun action() {
activeRecording?.stop()
val newRecording = prepareVideoRecording()
activeRecording = newRecording.start(ContextCompat.getMainExecutor(this)) { event ->
if (event is VideoRecordEvent.Finalize) {
_cameraClosedListener.complete(Unit)
}
}
}
if (_cameraAvailableListener.isCompleted) {
action()
} else {
// Race condition of `startNewCycle` being called before `invpkeOnCompletion`
// has been called can be ignored, as the camera usually opens within 5 seconds
// and the interval can't be set shorter than 10 seconds.
_cameraAvailableListener.invokeOnCompletion {
action()
}
}
}
override fun getRecordingInformation(): RecordingInformation = RecordingInformation( override fun getRecordingInformation(): RecordingInformation = RecordingInformation(
folderPath = batchesFolder.exportFolderForSettings(), folderPath = batchesFolder.exportFolderForSettings(),
recordingStart = recordingStart, recordingStart = recordingStart,
@ -222,4 +229,20 @@ class VideoRecorderService :
) )
} }
} }
class CameraControl(
private val camera: Camera,
) {
fun enableTorch() {
camera.cameraControl.enableTorch(true)
}
fun disableTorch() {
camera.cameraControl.enableTorch(false)
}
fun isTorchEnabled(): Boolean {
return camera.cameraInfo.torchState.value == TorchState.ON
}
}
} }