mirror of
https://github.com/Myzel394/Alibi.git
synced 2025-06-19 07:15:25 +02:00
chore: Move code for better readability
This commit is contained in:
parent
c40361aced
commit
65be9a1e3e
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user