diff --git a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt index 57f4a2c..a1cb859 100644 --- a/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/AudioRecorderService.kt @@ -21,130 +21,23 @@ class AudioRecorderService : IntervalRecorderService() { override var batchesFolder: BatchesFolder = AudioBatchesFolder.viaInternalFolder(this) + private val handler = Handler(Looper.getMainLooper()) + + var amplitudes = mutableListOf() + private set var amplitudesAmount = 1000 + var selectedMicrophone: MicrophoneInfo? = null var recorder: MediaRecorder? = null private set + + // Callbacks var onSelectedMicrophoneChange: (MicrophoneInfo?) -> Unit = {} var onMicrophoneDisconnected: () -> Unit = {} var onMicrophoneReconnected: () -> Unit = {} - - var amplitudes = mutableListOf() - private set - - private val handler = Handler(Looper.getMainLooper()) - var onAmplitudeChange: ((List) -> 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() { super.startNewCycle() @@ -189,6 +82,7 @@ class AudioRecorderService : createAmplitudesTimer() } + // ==== Amplitude related ==== private fun getAmplitudeAmount(): Int = amplitudesAmount 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?) { selectedMicrophone = microphone onSelectedMicrophoneChange(microphone) @@ -263,6 +260,16 @@ class AudioRecorderService : 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( override val maxDuration: Long, override val intervalDuration: Long, diff --git a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt index 7343398..be3c1b0 100644 --- a/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/VideoRecorderService.kt @@ -3,7 +3,9 @@ package app.myzel394.alibi.services import android.annotation.SuppressLint import android.util.Range import androidx.camera.core.Camera +import androidx.camera.core.CameraInfo import androidx.camera.core.CameraSelector +import androidx.camera.core.TorchState import androidx.camera.core.impl.CameraConfig import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.video.FileOutputOptions @@ -47,6 +49,64 @@ class VideoRecorderService : 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 private fun runOnMain(callback: () -> Unit) { val mainHandler = ContextCompat.getMainExecutor(this) @@ -88,6 +148,7 @@ class VideoRecorderService : selectedCamera, videoCapture ) + cameraControl = CameraControl(camera!!) _cameraAvailableListener.complete(Unit) } @@ -108,33 +169,6 @@ class VideoRecorderService : 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` private fun stopActiveRecording() { @@ -147,33 +181,6 @@ class VideoRecorderService : .prepareRecording(this, settings.getOutputOptions(this)) .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( folderPath = batchesFolder.exportFolderForSettings(), 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 + } + } }