From 332651060399b9e80d8d9cd9782e355ac7cb9da4 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 28 Jun 2024 12:02:08 +0530 Subject: [PATCH 01/34] Base module setup for PhoneAudioInput plugin --- .../radar-android-phone-audio-input/README.md | 0 .../build.gradle | 21 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 9 ++++++++ .../audio/input/PhoneAudioInputManager.kt | 4 ++++ .../audio/input/PhoneAudioInputProvider.kt | 4 ++++ .../audio/input/PhoneAudioInputService.kt | 4 ++++ .../phone/audio/input/PhoneAudioInputState.kt | 4 ++++ 7 files changed, 46 insertions(+) create mode 100644 plugins/radar-android-phone-audio-input/README.md create mode 100644 plugins/radar-android-phone-audio-input/build.gradle create mode 100644 plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt diff --git a/plugins/radar-android-phone-audio-input/README.md b/plugins/radar-android-phone-audio-input/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/radar-android-phone-audio-input/build.gradle b/plugins/radar-android-phone-audio-input/build.gradle new file mode 100644 index 000000000..275b8c227 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/build.gradle @@ -0,0 +1,21 @@ +apply from: "$rootDir/gradle/android.gradle" + +android { + namespace "org.radarbase.passive.phone.audio.input" +} + +//---------------------------------------------------------------------------// +// Configuration // +//---------------------------------------------------------------------------// + +description = "" + +//---------------------------------------------------------------------------// +// Sources and classpath configurations // +//---------------------------------------------------------------------------// + +dependencies { + api project(":radar-commons-android") +} + +apply from: "$rootDir/gradle/publishing.gradle" \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml new file mode 100644 index 000000000..827d84a95 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt new file mode 100644 index 000000000..a8071c931 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -0,0 +1,4 @@ +package org.radarbase.passive.phone.audio.input + +class PhoneAudioInputManager { +} \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt new file mode 100644 index 000000000..2597dc416 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt @@ -0,0 +1,4 @@ +package org.radarbase.passive.phone.audio.input + +class PhoneAudioInputProvider { +} \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt new file mode 100644 index 000000000..0c987b78c --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt @@ -0,0 +1,4 @@ +package org.radarbase.passive.phone.audio.input + +class PhoneAudioInputService { +} \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt new file mode 100644 index 000000000..94412b261 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -0,0 +1,4 @@ +package org.radarbase.passive.phone.audio.input + +class PhoneAudioInputState { +} \ No newline at end of file From 9db6791a32f5a024291828356331e05a340bdf18 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sat, 29 Jun 2024 10:21:31 +0530 Subject: [PATCH 02/34] Added superclasses to components of plugins --- .../audio/input/PhoneAudioInputManager.kt | 17 +++++++++- .../audio/input/PhoneAudioInputProvider.kt | 33 ++++++++++++++++++- .../audio/input/PhoneAudioInputService.kt | 19 ++++++++++- .../phone/audio/input/PhoneAudioInputState.kt | 4 ++- .../src/main/res/values/strings.xml | 5 +++ 5 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index a8071c931..8056cc938 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -1,4 +1,19 @@ package org.radarbase.passive.phone.audio.input -class PhoneAudioInputManager { +import android.media.AudioRecord +import org.radarbase.android.source.AbstractSourceManager + +class PhoneAudioInputManager(service: PhoneAudioInputService): AbstractSourceManager(service) { + + private var audioRecord: AudioRecord? = null + + init { + name = service.getString(R.string.phone_audio_input_display_name) + } + + override fun start(acceptableIds: Set) { + + } + + } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt index 2597dc416..4abe1ae50 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt @@ -1,4 +1,35 @@ package org.radarbase.passive.phone.audio.input -class PhoneAudioInputProvider { +import android.Manifest +import org.radarbase.android.BuildConfig +import org.radarbase.android.RadarService +import org.radarbase.android.source.SourceProvider + +class PhoneAudioInputProvider(radarService: RadarService): SourceProvider(radarService) { + + override val description: String + get() = radarService.getString(R.string.phone_audio_input_description) + override val pluginNames: List + get() = listOf( + "phone_audio_input", + "audio_input", + ".phone.PhoneAudioInputProvider", + "org.radarbase.passive.phone.audio.input.PhoneAudioInputProvider" + + ) + override val serviceClass: Class + get() = PhoneAudioInputService::class.java + override val displayName: String + get() = radarService.getString(R.string.phone_audio_input_display_name) + override val sourceProducer: String + get() = "ANDROID" + override val sourceModel: String + get() = "PHONE" + override val version: String + get() = BuildConfig.VERSION_NAME + override val permissionsNeeded: List + get() = listOf(Manifest.permission.RECORD_AUDIO) + + override val actions: List + get() = super.actions } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt index 0c987b78c..90ffd38c7 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt @@ -1,4 +1,21 @@ package org.radarbase.passive.phone.audio.input -class PhoneAudioInputService { +import org.radarbase.android.config.SingleRadarConfiguration +import org.radarbase.android.source.SourceManager +import org.radarbase.android.source.SourceService + +class PhoneAudioInputService: SourceService() { + + override val defaultState: PhoneAudioInputState + get() = PhoneAudioInputState() + + override fun createSourceManager(): PhoneAudioInputManager = PhoneAudioInputManager(this) + + override fun configureSourceManager( + manager: SourceManager, + config: SingleRadarConfiguration + ) { + manager as PhoneAudioInputManager + + } } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt index 94412b261..d972c4726 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -1,4 +1,6 @@ package org.radarbase.passive.phone.audio.input -class PhoneAudioInputState { +import org.radarbase.android.source.BaseSourceState + +class PhoneAudioInputState: BaseSourceState() { } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml new file mode 100644 index 000000000..dcf3505c3 --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + A plugin for recording uncompressed high-quality audio, utilizing low-level classes to directly interact with hardware, with capabilities for playback and audio input device selection. + Phone Audio Input\n + \ No newline at end of file From e72059cf3609540f9d92a7180033d59771658749 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sat, 29 Jun 2024 14:51:09 +0530 Subject: [PATCH 03/34] Added configuration parameters and Recorder Initialization logic --- .../build.gradle | 2 +- .../src/main/AndroidManifest.xml | 4 +- .../audio/input/PhoneAudioInputManager.kt | 65 ++++++++++++++++++- .../audio/input/PhoneAudioInputService.kt | 21 ++++++ .../phone/audio/input/PhoneAudioInputState.kt | 12 ++++ .../src/main/res/values/strings.xml | 4 +- 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/plugins/radar-android-phone-audio-input/build.gradle b/plugins/radar-android-phone-audio-input/build.gradle index 275b8c227..0baef1d8a 100644 --- a/plugins/radar-android-phone-audio-input/build.gradle +++ b/plugins/radar-android-phone-audio-input/build.gradle @@ -8,7 +8,7 @@ android { // Configuration // //---------------------------------------------------------------------------// -description = "" +description = "Plugin for recording uncompressed high-quality audio." //---------------------------------------------------------------------------// // Sources and classpath configurations // diff --git a/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml index 827d84a95..3960dff3e 100644 --- a/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml +++ b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ - + \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 8056cc938..6d26159b1 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -1,19 +1,80 @@ package org.radarbase.passive.phone.audio.input +import android.annotation.SuppressLint import android.media.AudioRecord import org.radarbase.android.source.AbstractSourceManager +import org.radarbase.android.source.SourceStatusListener +import org.slf4j.Logger +import org.slf4j.LoggerFactory -class PhoneAudioInputManager(service: PhoneAudioInputService): AbstractSourceManager(service) { +class PhoneAudioInputManager(service: PhoneAudioInputService) : + AbstractSourceManager(service) { private var audioRecord: AudioRecord? = null + @get: Synchronized + @set: Synchronized + var sampleRates: Array = arrayOf(44100, 22050, 16000, 11025, 8000) + + var audioSource: Int + get() = state.audioSource.get() + set(value) { state.audioSource.set(value) } + var currentSampleRate: Int + get() = state.currentSampleRate.get() + set(value) { state.currentSampleRate.set(value) } + var currentChannel: Int + get() = state.currentChannel.get() + set(value) { state.currentChannel.set(value) } + var currentAudioFormat: Int + get() = state.audioFormat.get() + set(value) { state.audioFormat.set(value) } + var recorderBufferSize: Int + get() = state.recorderBufferSize.get() + set(value) { state.recorderBufferSize.set(value) } + init { name = service.getString(R.string.phone_audio_input_display_name) } override fun start(acceptableIds: Set) { - + register() + status = SourceStatusListener.Status.READY + createRecorder() } + @SuppressLint("MissingPermission") + private fun createRecorder() { + status = SourceStatusListener.Status.CONNECTING + var i = 0 + try { + do { + recorderBufferSize = AudioRecord.getMinBufferSize( + currentSampleRate, + currentChannel, + currentAudioFormat + ) + audioRecord = AudioRecord( + audioSource, + currentSampleRate, + currentChannel, + currentAudioFormat, + recorderBufferSize + ) + } while ((++i < sampleRates.size) && (audioRecord?.state != AudioRecord.STATE_INITIALIZED)) + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + status = SourceStatusListener.Status.DISCONNECTED + } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { + logger.info("Successfully initialized AudioRecord") + status = SourceStatusListener.Status.CONNECTED + } + } catch (ex: IllegalArgumentException) { + logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) + } catch (ex: Exception) { + logger.error("Exception while initializing AudioRecord. ", ex) + } + } + companion object { + private val logger: Logger = LoggerFactory.getLogger(PhoneAudioInputManager::class.java) + } } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt index 90ffd38c7..500513e95 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt @@ -1,5 +1,7 @@ package org.radarbase.passive.phone.audio.input +import android.media.AudioFormat +import android.media.MediaRecorder import org.radarbase.android.config.SingleRadarConfiguration import org.radarbase.android.source.SourceManager import org.radarbase.android.source.SourceService @@ -16,6 +18,25 @@ class PhoneAudioInputService: SourceService() { config: SingleRadarConfiguration ) { manager as PhoneAudioInputManager + manager.audioSource = config.getInt(PHONE_AUDIO_INPUT_AUDIO_SOURCE, PHONE_AUDIO_INPUT_AUDIO_SOURCE_DEFAULT) + manager.recorderBufferSize = config.getInt(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE, PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) + manager.currentAudioFormat = config.getInt(PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT, PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT_DEFAULT) + manager.currentChannel = config.getInt(PHONE_AUDIO_INPUT_CURRENT_CHANNEL, PHONE_AUDIO_INPUT_CURRENT_CHANNEL_DEFAULT) + manager.currentSampleRate = config.getInt(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE, PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) + } + + companion object { + private const val PHONE_AUDIO_INPUT_PREFIX = "phone-audio-input-" + const val PHONE_AUDIO_INPUT_AUDIO_SOURCE = PHONE_AUDIO_INPUT_PREFIX + "audio-source" + const val PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE = PHONE_AUDIO_INPUT_PREFIX + "recorder-buffer-size" + const val PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT = PHONE_AUDIO_INPUT_PREFIX + "current-audio-format" + const val PHONE_AUDIO_INPUT_CURRENT_CHANNEL = PHONE_AUDIO_INPUT_PREFIX + "current-channel" + const val PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE = PHONE_AUDIO_INPUT_PREFIX + "current-sample-rate" + const val PHONE_AUDIO_INPUT_AUDIO_SOURCE_DEFAULT = MediaRecorder.AudioSource.MIC + const val PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT = -1 + const val PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT_DEFAULT = AudioFormat.ENCODING_PCM_16BIT + const val PHONE_AUDIO_INPUT_CURRENT_CHANNEL_DEFAULT = AudioFormat.CHANNEL_IN_MONO + const val PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT = 44100 } } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt index d972c4726..ae037f3ec 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -1,6 +1,18 @@ package org.radarbase.passive.phone.audio.input +import android.media.AudioFormat +import android.media.MediaRecorder import org.radarbase.android.source.BaseSourceState +import org.radarbase.passive.phone.audio.input.PhoneAudioInputService.Companion.PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT +import org.radarbase.passive.phone.audio.input.PhoneAudioInputService.Companion.PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT +import java.util.concurrent.atomic.AtomicInteger class PhoneAudioInputState: BaseSourceState() { + + var audioSource: AtomicInteger = AtomicInteger(MediaRecorder.AudioSource.MIC) + var currentSampleRate: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) + var currentChannel: AtomicInteger = AtomicInteger(AudioFormat.CHANNEL_IN_MONO) + var audioFormat: AtomicInteger = AtomicInteger(AudioFormat.ENCODING_PCM_16BIT) + var recorderBufferSize: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) + } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml index dcf3505c3..92bcb87d4 100644 --- a/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml +++ b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - A plugin for recording uncompressed high-quality audio, utilizing low-level classes to directly interact with hardware, with capabilities for playback and audio input device selection. - Phone Audio Input\n + Plugin for recording uncompressed high-quality audio, utilizing low-level classes to directly interact with hardware, with capabilities for playback and audio input device selection. + Phone Audio Input \ No newline at end of file From 92719197f39e8feb5e93e5c651c21ba04f9900b0 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Mon, 1 Jul 2024 15:14:27 +0530 Subject: [PATCH 04/34] Renamed variables and some minor changes --- .../audio/input/PhoneAudioInputManager.kt | 43 ++++++++----------- .../audio/input/PhoneAudioInputService.kt | 10 ++--- .../phone/audio/input/PhoneAudioInputState.kt | 6 +-- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 6d26159b1..c64beb242 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -12,25 +12,21 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : private var audioRecord: AudioRecord? = null - @get: Synchronized - @set: Synchronized - var sampleRates: Array = arrayOf(44100, 22050, 16000, 11025, 8000) - var audioSource: Int get() = state.audioSource.get() set(value) { state.audioSource.set(value) } - var currentSampleRate: Int - get() = state.currentSampleRate.get() - set(value) { state.currentSampleRate.set(value) } - var currentChannel: Int - get() = state.currentChannel.get() - set(value) { state.currentChannel.set(value) } - var currentAudioFormat: Int + var sampleRate: Int + get() = state.sampleRate.get() + set(value) { state.sampleRate.set(value) } + var channel: Int + get() = state.channel.get() + set(value) { state.channel.set(value) } + var audioFormat: Int get() = state.audioFormat.get() set(value) { state.audioFormat.set(value) } - var recorderBufferSize: Int - get() = state.recorderBufferSize.get() - set(value) { state.recorderBufferSize.set(value) } + var bufferSize: Int + get() = state.bufferSize.get() + set(value) { state.bufferSize.set(value) } init { name = service.getString(R.string.phone_audio_input_display_name) @@ -47,20 +43,19 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : status = SourceStatusListener.Status.CONNECTING var i = 0 try { - do { - recorderBufferSize = AudioRecord.getMinBufferSize( - currentSampleRate, - currentChannel, - currentAudioFormat + + bufferSize = AudioRecord.getMinBufferSize( + sampleRate, + channel, + audioFormat ) audioRecord = AudioRecord( audioSource, - currentSampleRate, - currentChannel, - currentAudioFormat, - recorderBufferSize + sampleRate, + channel, + audioFormat, + bufferSize ) - } while ((++i < sampleRates.size) && (audioRecord?.state != AudioRecord.STATE_INITIALIZED)) if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { status = SourceStatusListener.Status.DISCONNECTED } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt index 500513e95..6fff521a3 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputService.kt @@ -19,10 +19,10 @@ class PhoneAudioInputService: SourceService() { ) { manager as PhoneAudioInputManager manager.audioSource = config.getInt(PHONE_AUDIO_INPUT_AUDIO_SOURCE, PHONE_AUDIO_INPUT_AUDIO_SOURCE_DEFAULT) - manager.recorderBufferSize = config.getInt(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE, PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) - manager.currentAudioFormat = config.getInt(PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT, PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT_DEFAULT) - manager.currentChannel = config.getInt(PHONE_AUDIO_INPUT_CURRENT_CHANNEL, PHONE_AUDIO_INPUT_CURRENT_CHANNEL_DEFAULT) - manager.currentSampleRate = config.getInt(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE, PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) + manager.bufferSize = config.getInt(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE, PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) + manager.audioFormat = config.getInt(PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT, PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT_DEFAULT) + manager.channel = config.getInt(PHONE_AUDIO_INPUT_CURRENT_CHANNEL, PHONE_AUDIO_INPUT_CURRENT_CHANNEL_DEFAULT) + manager.sampleRate = config.getInt(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE, PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) } companion object { @@ -37,6 +37,6 @@ class PhoneAudioInputService: SourceService() { const val PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT = -1 const val PHONE_AUDIO_INPUT_CURRENT_AUDIO_FORMAT_DEFAULT = AudioFormat.ENCODING_PCM_16BIT const val PHONE_AUDIO_INPUT_CURRENT_CHANNEL_DEFAULT = AudioFormat.CHANNEL_IN_MONO - const val PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT = 44100 + const val PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT = 16000 } } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt index ae037f3ec..d585b9acf 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -10,9 +10,9 @@ import java.util.concurrent.atomic.AtomicInteger class PhoneAudioInputState: BaseSourceState() { var audioSource: AtomicInteger = AtomicInteger(MediaRecorder.AudioSource.MIC) - var currentSampleRate: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) - var currentChannel: AtomicInteger = AtomicInteger(AudioFormat.CHANNEL_IN_MONO) + var sampleRate: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) + var channel: AtomicInteger = AtomicInteger(AudioFormat.CHANNEL_IN_MONO) var audioFormat: AtomicInteger = AtomicInteger(AudioFormat.ENCODING_PCM_16BIT) - var recorderBufferSize: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) + var bufferSize: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) } \ No newline at end of file From 4959548b16c8b3466051bc1e73b466c8c4729781 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Mon, 1 Jul 2024 16:44:26 +0530 Subject: [PATCH 05/34] Added addtional recorder initialization logic and some calculations --- .../audio/input/PhoneAudioInputManager.kt | 65 ++++++++++++------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index c64beb242..8595aea0c 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -1,6 +1,7 @@ package org.radarbase.passive.phone.audio.input import android.annotation.SuppressLint +import android.media.AudioFormat import android.media.AudioRecord import org.radarbase.android.source.AbstractSourceManager import org.radarbase.android.source.SourceStatusListener @@ -24,12 +25,29 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : var audioFormat: Int get() = state.audioFormat.get() set(value) { state.audioFormat.set(value) } + /** + * The total number of bytes needed to hold the audio data for one `framePeriod`. + */ var bufferSize: Int get() = state.bufferSize.get() set(value) { state.bufferSize.set(value) } + /** The interval(ms) in which the recorded samples are output to the file */ + private val TIMER_INTERVAL = 120 + + /** + * The framePeriod is calculated as the number of samples in `TIMER_INTERVAL` milliseconds. + * It represents how many samples correspond to the given interval. + */ + private var framePeriod: Int + private var bitsPerSample: Int + private var numChannels: Int + init { name = service.getString(R.string.phone_audio_input_display_name) + bitsPerSample = if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) 16 else 8 + numChannels = if (channel == AudioFormat.CHANNEL_IN_MONO) 1 else 2 + framePeriod = sampleRate * TIMER_INTERVAL/1000 } override fun start(acceptableIds: Set) { @@ -41,33 +59,36 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : @SuppressLint("MissingPermission") private fun createRecorder() { status = SourceStatusListener.Status.CONNECTING - var i = 0 - try { + framePeriod = sampleRate * TIMER_INTERVAL/1000 + bufferSize = framePeriod * bitsPerSample * numChannels * 2 / 8 + logger.info("Calculated buffer size: $bufferSize (bytes), and frame period: $framePeriod") - bufferSize = AudioRecord.getMinBufferSize( - sampleRate, - channel, - audioFormat + val calculatedBufferSize: Int = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat) + if (calculatedBufferSize != AudioRecord.ERROR_BAD_VALUE) { + if (bufferSize < calculatedBufferSize) { + bufferSize = calculatedBufferSize + framePeriod = bufferSize / (2 * bitsPerSample * numChannels / 8) + logger.info("Updating buffer size to: $bufferSize, and frame period to: $framePeriod") + } + try { + audioRecord = AudioRecord(audioSource, sampleRate, channel, audioFormat, bufferSize ) - audioRecord = AudioRecord( - audioSource, - sampleRate, - channel, - audioFormat, - bufferSize - ) - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + status = SourceStatusListener.Status.DISCONNECTED + } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { + logger.info("Successfully initialized AudioRecord") + status = SourceStatusListener.Status.CONNECTED + } + } catch (ex: IllegalArgumentException) { + logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) + } catch (ex: Exception) { + logger.error("Exception while initializing AudioRecord. ", ex) + } + } else { + logger.error("Error in calculating buffer size") status = SourceStatusListener.Status.DISCONNECTED - } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { - logger.info("Successfully initialized AudioRecord") - status = SourceStatusListener.Status.CONNECTED } - } catch (ex: IllegalArgumentException) { - logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) - } catch (ex: Exception) { - logger.error("Exception while initializing AudioRecord. ", ex) } - } companion object { private val logger: Logger = LoggerFactory.getLogger(PhoneAudioInputManager::class.java) From 4422fcec74885f957acb1a28c4b1382cc528ff10 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Tue, 2 Jul 2024 17:08:41 +0530 Subject: [PATCH 06/34] Audio recording and saving internally --- .../audio/input/PhoneAudioInputManager.kt | 158 ++++++++++++++++-- .../audio/input/PhoneAudioInputProvider.kt | 2 +- 2 files changed, 141 insertions(+), 19 deletions(-) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 8595aea0c..7e13493c7 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -1,17 +1,28 @@ package org.radarbase.passive.phone.audio.input -import android.annotation.SuppressLint +import android.Manifest +import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioRecord +import android.media.AudioRecord.STATE_INITIALIZED +import android.os.Environment +import android.os.Process +import androidx.core.content.ContextCompat import org.radarbase.android.source.AbstractSourceManager import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.SafeHandler import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.File +import java.io.RandomAccessFile +import java.lang.Short.reverseBytes class PhoneAudioInputManager(service: PhoneAudioInputService) : AbstractSourceManager(service) { private var audioRecord: AudioRecord? = null + private var randomAccessWriter: RandomAccessFile? = null + private var buffer: ByteArray = byteArrayOf() var audioSource: Int get() = state.audioSource.get() @@ -40,26 +51,60 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : * It represents how many samples correspond to the given interval. */ private var framePeriod: Int - private var bitsPerSample: Int - private var numChannels: Int + private var bitsPerSample: Short + private var numChannels: Short + private val audioDir: File? + private var recordingFile: File? = null + private var payloadSize: Int = 0 + + private val tempHandler = SafeHandler.getInstance("Temp", Process.THREAD_PRIORITY_BACKGROUND) init { name = service.getString(R.string.phone_audio_input_display_name) bitsPerSample = if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) 16 else 8 numChannels = if (channel == AudioFormat.CHANNEL_IN_MONO) 1 else 2 framePeriod = sampleRate * TIMER_INTERVAL/1000 + + val internalDirs = service.filesDir + status = if (internalDirs != null) { + audioDir = File(internalDirs, "org.radarbase.passive.phone.audio.input") + val dirCreated = audioDir.mkdirs() + val directoryExists = audioDir.exists() + logger.info("Dir Created: $dirCreated.Exists: $directoryExists") + clearAudioDirectory() + SourceStatusListener.Status.READY + } else { + audioDir = null + SourceStatusListener.Status.UNAVAILABLE + } } override fun start(acceptableIds: Set) { register() - status = SourceStatusListener.Status.READY createRecorder() + Thread { + startAudioRecording() + tempHandler.delay(15000, ::stopRecording) + }.start() + } + + private fun stopRecording() { + logger.warn("Stopping Recording: Saving data") + audioRecord?.stop() + randomAccessWriter?.apply { + seek(4) + writeInt(Integer.reverseBytes(36 + payloadSize)) + seek(40) // Write size to Subchunk2Size field + writeInt(Integer.reverseBytes(payloadSize)) + close() + } + } - @SuppressLint("MissingPermission") private fun createRecorder() { - status = SourceStatusListener.Status.CONNECTING - framePeriod = sampleRate * TIMER_INTERVAL/1000 + if (ContextCompat.checkSelfPermission(service, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + status = SourceStatusListener.Status.CONNECTING + framePeriod = sampleRate * TIMER_INTERVAL / 1000 bufferSize = framePeriod * bitsPerSample * numChannels * 2 / 8 logger.info("Calculated buffer size: $bufferSize (bytes), and frame period: $framePeriod") @@ -71,24 +116,101 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : logger.info("Updating buffer size to: $bufferSize, and frame period to: $framePeriod") } try { - audioRecord = AudioRecord(audioSource, sampleRate, channel, audioFormat, bufferSize - ) - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { - status = SourceStatusListener.Status.DISCONNECTED - } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { - logger.info("Successfully initialized AudioRecord") - status = SourceStatusListener.Status.CONNECTED - } - } catch (ex: IllegalArgumentException) { + audioRecord = AudioRecord( + audioSource, sampleRate, channel, audioFormat, bufferSize + ) + if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + disconnect() + } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { + logger.info("Successfully initialized AudioRecord") + status = SourceStatusListener.Status.CONNECTED + } + } catch (ex: IllegalArgumentException) { logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) } catch (ex: Exception) { logger.error("Exception while initializing AudioRecord. ", ex) } - } else { + } else { logger.error("Error in calculating buffer size") - status = SourceStatusListener.Status.DISCONNECTED + disconnect() + } + } + else { + logger.error("Permission not granted for RECORD_AUDIO, disconnecting now") + disconnect() + } + } + + private fun clearAudioDirectory() { + audioDir?.let { audioDir -> + audioDir.parentFile + ?.list{ _, name -> name.startsWith("phone_audio_input") && name.endsWith(".wav") } + ?.forEach { File(audioDir.parentFile, it).delete() } + + audioDir.walk().filter { it.name.startsWith("phone_audio_input") && it.path.endsWith(".wav") } + .forEach { it.delete() } + } + } + + private fun startAudioRecording() { + setupRecording() + if ((audioRecord?.state == STATE_INITIALIZED) && (recordingFile != null)) { + audioRecord?.startRecording() + audioRecord?.read(buffer, 0, buffer.size) + logger.info("Started Recording without saying ") + audioRecord?.setRecordPositionUpdateListener(updateListener) + audioRecord?.setPositionNotificationPeriod(framePeriod) + } else { + logger.error("Trying to start recording on uninitialized AudioRecord or filePath is null, state: ${audioRecord?.state}, $recordingFile") + disconnect() + } + } + + private fun setupRecording() { + setRecordingPath() + writeFileHeaders() + buffer = ByteArray(framePeriod * bitsPerSample / 8 * numChannels) + } + + private fun setRecordingPath() { + recordingFile = File(audioDir, "phone_audio_input"+System.currentTimeMillis()+".wav") + } + + private fun writeFileHeaders() { + randomAccessWriter = RandomAccessFile(recordingFile, "rw") + + randomAccessWriter?.apply { + setLength(0) // Set file length to 0, to prevent unexpected behavior in case the file already existed + writeBytes("RIFF") + writeInt(0) // Final file size not known yet, write 0 + writeBytes("WAVE") + writeBytes("fmt ") + writeInt(Integer.reverseBytes(16)) // Sub-chunk size, 16 for PCM + writeShort(reverseBytes(1.toShort()).toInt()) // AudioFormat, 1 for PCM + writeShort(reverseBytes(numChannels).toInt()) // Number of channels, 1 for mono, 2 for stereo + writeInt(Integer.reverseBytes(sampleRate)) // Sample rate + writeInt(Integer.reverseBytes(sampleRate * bitsPerSample * numChannels / 8)) // Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8 + writeShort(reverseBytes((numChannels * bitsPerSample / 8).toShort()).toInt()) // Block align, NumberOfChannels*BitsPerSample/8 + writeShort(reverseBytes(bitsPerSample).toInt()) // Bits per sample + writeBytes("data") + writeInt(0) // Data chunk size not known yet, write 0 + } + } + + private val updateListener = object : AudioRecord.OnRecordPositionUpdateListener { + override fun onMarkerReached(recorder: AudioRecord?) { + TODO("Not yet implemented") + } + + override fun onPeriodicNotification(recorder: AudioRecord?) { + audioRecord?.let { + val dataRead = it.read(buffer, 0, buffer.size) + randomAccessWriter?.write(buffer) + payloadSize += dataRead + logger.info("Recording Data in callback") } } + } companion object { private val logger: Logger = LoggerFactory.getLogger(PhoneAudioInputManager::class.java) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt index 4abe1ae50..4c0c7da09 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt @@ -28,7 +28,7 @@ class PhoneAudioInputProvider(radarService: RadarService): SourceProvider - get() = listOf(Manifest.permission.RECORD_AUDIO) + get() = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) override val actions: List get() = super.actions From 0023d46c6932602677deacf8eea48262d5ed4f76 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Tue, 2 Jul 2024 17:10:19 +0530 Subject: [PATCH 07/34] Remove permissions --- .../passive/phone/audio/input/PhoneAudioInputProvider.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt index 4c0c7da09..4abe1ae50 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt @@ -28,7 +28,7 @@ class PhoneAudioInputProvider(radarService: RadarService): SourceProvider - get() = listOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) + get() = listOf(Manifest.permission.RECORD_AUDIO) override val actions: List get() = super.actions From 3ee318a7ed1ce6ed79ba8da0044461a60803cd52 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 5 Jul 2024 13:19:43 +0530 Subject: [PATCH 08/34] Addition of PhoneInputAudioActivity --- .../build.gradle | 13 +++++- .../src/main/AndroidManifest.xml | 21 ++++++--- .../audio/input/PhoneAudioInputActivity.kt | 43 +++++++++++++++++++ .../audio/input/PhoneAudioInputManager.kt | 7 ++- .../audio/input/PhoneAudioInputProvider.kt | 5 ++- .../res/layout/activity_phone_audio_input.xml | 11 +++++ .../src/main/res/values/strings.xml | 1 + 7 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt create mode 100644 plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml diff --git a/plugins/radar-android-phone-audio-input/build.gradle b/plugins/radar-android-phone-audio-input/build.gradle index 0baef1d8a..bb923c688 100644 --- a/plugins/radar-android-phone-audio-input/build.gradle +++ b/plugins/radar-android-phone-audio-input/build.gradle @@ -13,9 +13,20 @@ description = "Plugin for recording uncompressed high-quality audio." //---------------------------------------------------------------------------// // Sources and classpath configurations // //---------------------------------------------------------------------------// +android { + buildFeatures { + viewBinding true + } +} dependencies { api project(":radar-commons-android") + implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "com.google.android.material:material:$material_version" + implementation "androidx.activity:activity:1.9.0" + implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" + implementation "androidx.legacy:legacy-support-v4:$legacy_support_version" } -apply from: "$rootDir/gradle/publishing.gradle" \ No newline at end of file +apply from: "$rootDir/gradle/publishing.gradle" +apply plugin: 'org.jetbrains.kotlin.android' \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml index 3960dff3e..702249cc3 100644 --- a/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml +++ b/plugins/radar-android-phone-audio-input/src/main/AndroidManifest.xml @@ -1,11 +1,18 @@ - + + - + - - + + + + + \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt new file mode 100644 index 000000000..bca00535c --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt @@ -0,0 +1,43 @@ +package org.radarbase.passive.phone.audio.input + +import android.content.ComponentName +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import org.radarbase.android.IRadarBinder +import org.radarbase.passive.phone.audio.input.databinding.ActivityPhoneAudioInputBinding + +class PhoneAudioInputActivity : AppCompatActivity() { + + private lateinit var binding: ActivityPhoneAudioInputBinding + private var recorderProvider: PhoneAudioInputProvider? = null + + private val state: PhoneAudioInputState? + get() = recorderProvider?.connection?.sourceState + + private val radarServiceConnection = object : ServiceConnection{ + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val radarService = service as IRadarBinder + recorderProvider = null + for (provider in radarService.connections) { + if (provider is PhoneAudioInputProvider) { + recorderProvider = provider + } + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + recorderProvider = null + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + binding = ActivityPhoneAudioInputBinding.inflate(layoutInflater) + setContentView(binding.root) + + } +} \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 7e13493c7..1e5cfc8bf 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -5,7 +5,6 @@ import android.content.pm.PackageManager import android.media.AudioFormat import android.media.AudioRecord import android.media.AudioRecord.STATE_INITIALIZED -import android.os.Environment import android.os.Process import androidx.core.content.ContextCompat import org.radarbase.android.source.AbstractSourceManager @@ -94,7 +93,7 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : randomAccessWriter?.apply { seek(4) writeInt(Integer.reverseBytes(36 + payloadSize)) - seek(40) // Write size to Subchunk2Size field + seek(40) writeInt(Integer.reverseBytes(payloadSize)) close() } @@ -119,9 +118,9 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : audioRecord = AudioRecord( audioSource, sampleRate, channel, audioFormat, bufferSize ) - if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { + if (audioRecord?.state != STATE_INITIALIZED) { disconnect() - } else if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) { + } else if (audioRecord?.state == STATE_INITIALIZED) { logger.info("Successfully initialized AudioRecord") status = SourceStatusListener.Status.CONNECTED } diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt index 4abe1ae50..df7eca444 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputProvider.kt @@ -1,6 +1,7 @@ package org.radarbase.passive.phone.audio.input import android.Manifest +import android.content.Intent import org.radarbase.android.BuildConfig import org.radarbase.android.RadarService import org.radarbase.android.source.SourceProvider @@ -31,5 +32,7 @@ class PhoneAudioInputProvider(radarService: RadarService): SourceProvider - get() = super.actions + get() = listOf(Action(radarService.getString(R.string.startRecordingActivity)){ + startActivity(Intent(this, PhoneAudioInputActivity::class.java)) + }) } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml b/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml new file mode 100644 index 000000000..9fe9afa0c --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml index 92bcb87d4..ded09a11c 100644 --- a/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml +++ b/plugins/radar-android-phone-audio-input/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ Plugin for recording uncompressed high-quality audio, utilizing low-level classes to directly interact with hardware, with capabilities for playback and audio input device selection. Phone Audio Input + Phone Audio Input \ No newline at end of file From 62da5b8ca32b48eb2702680d31817ea9a428c10b Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 5 Jul 2024 17:24:45 +0530 Subject: [PATCH 09/34] SafeHandler support added Interfaces defined in PhoneAudioInputState for smooth connection between Activity and Manager InputRecordInfo for status tracking of Audio Recording --- .../audio/input/PhoneAudioInputManager.kt | 168 ++++++++++-------- .../phone/audio/input/PhoneAudioInputState.kt | 19 ++ .../audio/input/utils/InputRecordInfo.kt | 9 + 3 files changed, 119 insertions(+), 77 deletions(-) create mode 100644 plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 1e5cfc8bf..01bce8ccc 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -22,6 +22,8 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : private var audioRecord: AudioRecord? = null private var randomAccessWriter: RandomAccessFile? = null private var buffer: ByteArray = byteArrayOf() + private val audioRecordingHandler = SafeHandler.getInstance("PHONE-AUDIO-INPUT", Process.THREAD_PRIORITY_BACKGROUND) + private val recordeProcessingHandler: SafeHandler = SafeHandler.getInstance("AUDIO-RECORD-PROCESSING", Process.THREAD_PRIORITY_AUDIO) var audioSource: Int get() = state.audioSource.get() @@ -56,8 +58,6 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : private var recordingFile: File? = null private var payloadSize: Int = 0 - private val tempHandler = SafeHandler.getInstance("Temp", Process.THREAD_PRIORITY_BACKGROUND) - init { name = service.getString(R.string.phone_audio_input_display_name) bitsPerSample = if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) 16 else 8 @@ -80,95 +80,87 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : override fun start(acceptableIds: Set) { register() + audioRecordingHandler.start() createRecorder() - Thread { - startAudioRecording() - tempHandler.delay(15000, ::stopRecording) - }.start() - } - - private fun stopRecording() { - logger.warn("Stopping Recording: Saving data") - audioRecord?.stop() - randomAccessWriter?.apply { - seek(4) - writeInt(Integer.reverseBytes(36 + payloadSize)) - seek(40) - writeInt(Integer.reverseBytes(payloadSize)) - close() - } - } private fun createRecorder() { - if (ContextCompat.checkSelfPermission(service, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { - status = SourceStatusListener.Status.CONNECTING - framePeriod = sampleRate * TIMER_INTERVAL / 1000 - bufferSize = framePeriod * bitsPerSample * numChannels * 2 / 8 - logger.info("Calculated buffer size: $bufferSize (bytes), and frame period: $framePeriod") - - val calculatedBufferSize: Int = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat) - if (calculatedBufferSize != AudioRecord.ERROR_BAD_VALUE) { - if (bufferSize < calculatedBufferSize) { - bufferSize = calculatedBufferSize - framePeriod = bufferSize / (2 * bitsPerSample * numChannels / 8) - logger.info("Updating buffer size to: $bufferSize, and frame period to: $framePeriod") - } - try { - audioRecord = AudioRecord( - audioSource, sampleRate, channel, audioFormat, bufferSize - ) - if (audioRecord?.state != STATE_INITIALIZED) { - disconnect() - } else if (audioRecord?.state == STATE_INITIALIZED) { - logger.info("Successfully initialized AudioRecord") - status = SourceStatusListener.Status.CONNECTED + audioRecordingHandler.execute { + if (ContextCompat.checkSelfPermission(service, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + status = SourceStatusListener.Status.CONNECTING + framePeriod = sampleRate * TIMER_INTERVAL / 1000 + bufferSize = framePeriod * bitsPerSample * numChannels * 2 / 8 + logger.info("Calculated buffer size: $bufferSize (bytes), and frame period: $framePeriod") + + val calculatedBufferSize: Int = AudioRecord.getMinBufferSize(sampleRate, channel, audioFormat) + if (calculatedBufferSize != AudioRecord.ERROR_BAD_VALUE) { + if (bufferSize < calculatedBufferSize) { + bufferSize = calculatedBufferSize + framePeriod = bufferSize / (2 * bitsPerSample * numChannels / 8) + logger.info("Updating buffer size to: $bufferSize, and frame period to: $framePeriod") + } + try { + audioRecord = AudioRecord( + audioSource, sampleRate, channel, audioFormat, bufferSize + ) + if (audioRecord?.state != STATE_INITIALIZED) { + disconnect() + } else if (audioRecord?.state == STATE_INITIALIZED) { + logger.info("Successfully initialized AudioRecord") + status = SourceStatusListener.Status.CONNECTED + } + } catch (ex: IllegalArgumentException) { + logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) + } catch (ex: Exception) { + logger.error("Exception while initializing AudioRecord. ", ex) } - } catch (ex: IllegalArgumentException) { - logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) - } catch (ex: Exception) { - logger.error("Exception while initializing AudioRecord. ", ex) + } else { + logger.error("Error in calculating buffer size") + disconnect() } } else { - logger.error("Error in calculating buffer size") + logger.error("Permission not granted for RECORD_AUDIO, disconnecting now") disconnect() } } - else { - logger.error("Permission not granted for RECORD_AUDIO, disconnecting now") - disconnect() - } } private fun clearAudioDirectory() { - audioDir?.let { audioDir -> - audioDir.parentFile - ?.list{ _, name -> name.startsWith("phone_audio_input") && name.endsWith(".wav") } - ?.forEach { File(audioDir.parentFile, it).delete() } - - audioDir.walk().filter { it.name.startsWith("phone_audio_input") && it.path.endsWith(".wav") } - .forEach { it.delete() } + audioRecordingHandler.execute { + audioDir?.let { audioDir -> + audioDir.parentFile + ?.list { _, name -> name.startsWith("phone_audio_input") && name.endsWith(".wav") } + ?.forEach { File(audioDir.parentFile, it).delete() } + + audioDir.walk() + .filter { it.name.startsWith("phone_audio_input") && it.path.endsWith(".wav") } + .forEach { it.delete() } + } } } private fun startAudioRecording() { setupRecording() - if ((audioRecord?.state == STATE_INITIALIZED) && (recordingFile != null)) { - audioRecord?.startRecording() - audioRecord?.read(buffer, 0, buffer.size) - logger.info("Started Recording without saying ") - audioRecord?.setRecordPositionUpdateListener(updateListener) - audioRecord?.setPositionNotificationPeriod(framePeriod) - } else { - logger.error("Trying to start recording on uninitialized AudioRecord or filePath is null, state: ${audioRecord?.state}, $recordingFile") - disconnect() + audioRecordingHandler.execute { + if ((audioRecord?.state == STATE_INITIALIZED) && (recordingFile != null)) { + audioRecord?.startRecording() + audioRecord?.read(buffer, 0, buffer.size) + logger.info("Started Recording without saying ") + audioRecord?.setRecordPositionUpdateListener(updateListener) + audioRecord?.setPositionNotificationPeriod(framePeriod) + } else { + logger.error("Trying to start recording on uninitialized AudioRecord or filePath is null, state: ${audioRecord?.state}, $recordingFile") + disconnect() + } } } private fun setupRecording() { - setRecordingPath() - writeFileHeaders() - buffer = ByteArray(framePeriod * bitsPerSample / 8 * numChannels) + audioRecordingHandler.execute { + setRecordingPath() + writeFileHeaders() + buffer = ByteArray(framePeriod * bitsPerSample / 8 * numChannels) + } } private fun setRecordingPath() { @@ -180,10 +172,12 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : randomAccessWriter?.apply { setLength(0) // Set file length to 0, to prevent unexpected behavior in case the file already existed + // RIFF header writeBytes("RIFF") writeInt(0) // Final file size not known yet, write 0 writeBytes("WAVE") - writeBytes("fmt ") + // fmt sub-chunk + writeBytes("fmt") writeInt(Integer.reverseBytes(16)) // Sub-chunk size, 16 for PCM writeShort(reverseBytes(1.toShort()).toInt()) // AudioFormat, 1 for PCM writeShort(reverseBytes(numChannels).toInt()) // Number of channels, 1 for mono, 2 for stereo @@ -191,26 +185,46 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : writeInt(Integer.reverseBytes(sampleRate * bitsPerSample * numChannels / 8)) // Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8 writeShort(reverseBytes((numChannels * bitsPerSample / 8).toShort()).toInt()) // Block align, NumberOfChannels*BitsPerSample/8 writeShort(reverseBytes(bitsPerSample).toInt()) // Bits per sample + // data sub-chunk writeBytes("data") - writeInt(0) // Data chunk size not known yet, write 0 + writeInt(0) // Data chunk size not known yet } } private val updateListener = object : AudioRecord.OnRecordPositionUpdateListener { override fun onMarkerReached(recorder: AudioRecord?) { - TODO("Not yet implemented") + // No Action } override fun onPeriodicNotification(recorder: AudioRecord?) { - audioRecord?.let { - val dataRead = it.read(buffer, 0, buffer.size) - randomAccessWriter?.write(buffer) - payloadSize += dataRead - logger.info("Recording Data in callback") + audioRecordingHandler.execute { + audioRecord?.let { + val dataRead = it.read(buffer, 0, buffer.size) + randomAccessWriter?.write(buffer) + payloadSize += dataRead + logger.info("Recording Data in callback") + } } } } + private fun stopRecording() { + logger.warn("Stopping Recording: Saving data") + audioRecordingHandler.execute { + audioRecord?.stop() + randomAccessWriter?.apply { + seek(4) + writeInt(Integer.reverseBytes(36 + payloadSize)) + seek(40) + writeInt(Integer.reverseBytes(payloadSize)) + close() + } + } + } + + override fun onClose() { + super.onClose() + } companion object { private val logger: Logger = LoggerFactory.getLogger(PhoneAudioInputManager::class.java) } diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt index d585b9acf..64e2da93f 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -1,5 +1,6 @@ package org.radarbase.passive.phone.audio.input +import android.media.AudioDeviceInfo import android.media.AudioFormat import android.media.MediaRecorder import org.radarbase.android.source.BaseSourceState @@ -15,4 +16,22 @@ class PhoneAudioInputState: BaseSourceState() { var audioFormat: AtomicInteger = AtomicInteger(AudioFormat.ENCODING_PCM_16BIT) var bufferSize: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT) + + interface AudioRecordManager { + fun startRecording() + fun stopRecording() + } + + interface AudioPlayerManager { + fun startPlayback() + fun stopPlayback() + fun pausePlayback() + fun resumePlayback() + } + + interface InputAudioDeviceManager { + fun getConnectedDevices(): List + fun setDefaultInputDevice(device: AudioDeviceInfo) + } + } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt new file mode 100644 index 000000000..5aefa201a --- /dev/null +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt @@ -0,0 +1,9 @@ +package org.radarbase.passive.phone.audio.input.utils + +data class InputRecordInfo( + val recorderCreated: Boolean, + val recordingPathSet: Boolean, + val fileHeadersWritten: Boolean, + val bufferCreated: Boolean, + val isRecording: Boolean +) \ No newline at end of file From 634eab9a69d429a3b6ff2a339d664da979bec523 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sun, 7 Jul 2024 12:26:18 +0530 Subject: [PATCH 10/34] Wrapping RandomAccessFile in BufferedOutputStream --- .../audio/input/PhoneAudioInputManager.kt | 99 ++++++++++++------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt index 01bce8ccc..85c9cd648 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputManager.kt @@ -10,20 +10,25 @@ import androidx.core.content.ContextCompat import org.radarbase.android.source.AbstractSourceManager import org.radarbase.android.source.SourceStatusListener import org.radarbase.android.util.SafeHandler +import org.radarbase.passive.phone.audio.input.utils.InputRecordInfo import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.BufferedOutputStream import java.io.File +import java.io.FileOutputStream import java.io.RandomAccessFile import java.lang.Short.reverseBytes class PhoneAudioInputManager(service: PhoneAudioInputService) : - AbstractSourceManager(service) { + AbstractSourceManager(service), PhoneAudioInputState.AudioRecordManager { private var audioRecord: AudioRecord? = null private var randomAccessWriter: RandomAccessFile? = null + private var bufferedOutputStream: BufferedOutputStream? = null private var buffer: ByteArray = byteArrayOf() private val audioRecordingHandler = SafeHandler.getInstance("PHONE-AUDIO-INPUT", Process.THREAD_PRIORITY_BACKGROUND) - private val recordeProcessingHandler: SafeHandler = SafeHandler.getInstance("AUDIO-RECORD-PROCESSING", Process.THREAD_PRIORITY_AUDIO) + private val recordProcessingHandler: SafeHandler = SafeHandler.getInstance("AUDIO-RECORD-PROCESSING", Process.THREAD_PRIORITY_AUDIO) + private val pluginStatusInfo: InputRecordInfo = InputRecordInfo() var audioSource: Int get() = state.audioSource.get() @@ -82,6 +87,7 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : register() audioRecordingHandler.start() createRecorder() + state.audioRecordManager = this } private fun createRecorder() { @@ -99,6 +105,7 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : framePeriod = bufferSize / (2 * bitsPerSample * numChannels / 8) logger.info("Updating buffer size to: $bufferSize, and frame period to: $framePeriod") } + buffer = ByteArray(framePeriod * bitsPerSample / 8 * numChannels) try { audioRecord = AudioRecord( audioSource, sampleRate, channel, audioFormat, bufferSize @@ -108,6 +115,7 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : } else if (audioRecord?.state == STATE_INITIALIZED) { logger.info("Successfully initialized AudioRecord") status = SourceStatusListener.Status.CONNECTED + pluginStatusInfo.recorderCreated = true } } catch (ex: IllegalArgumentException) { logger.error("Invalid parameters passed to AudioRecord constructor. ", ex) @@ -125,27 +133,43 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : } } + override fun startRecording() { + startAudioRecording() + } + + override fun stopRecording() { + stopAudioRecording() + } + private fun clearAudioDirectory() { audioRecordingHandler.execute { audioDir?.let { audioDir -> audioDir.parentFile ?.list { _, name -> name.startsWith("phone_audio_input") && name.endsWith(".wav") } - ?.forEach { File(audioDir.parentFile, it).delete() } + ?.forEach { + File(audioDir.parentFile, it).delete() + logger.debug("Deleted audio file: {}", it) + } audioDir.walk() .filter { it.name.startsWith("phone_audio_input") && it.path.endsWith(".wav") } - .forEach { it.delete() } + .forEach { + it.delete() + logger.debug("Deleted audio file: {}", it) + } } } } private fun startAudioRecording() { - setupRecording() audioRecordingHandler.execute { + setupRecording() if ((audioRecord?.state == STATE_INITIALIZED) && (recordingFile != null)) { + bufferedOutputStream = BufferedOutputStream(FileOutputStream(randomAccessWriter!!.fd)) audioRecord?.startRecording() + state.isRecording.postValue(true) audioRecord?.read(buffer, 0, buffer.size) - logger.info("Started Recording without saying ") + logger.trace("Started recording") audioRecord?.setRecordPositionUpdateListener(updateListener) audioRecord?.setPositionNotificationPeriod(framePeriod) } else { @@ -156,38 +180,37 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : } private fun setupRecording() { - audioRecordingHandler.execute { setRecordingPath() writeFileHeaders() - buffer = ByteArray(framePeriod * bitsPerSample / 8 * numChannels) - } } private fun setRecordingPath() { recordingFile = File(audioDir, "phone_audio_input"+System.currentTimeMillis()+".wav") + randomAccessWriter = RandomAccessFile(recordingFile, "rw") + pluginStatusInfo.recordingPathSet = true } private fun writeFileHeaders() { - randomAccessWriter = RandomAccessFile(recordingFile, "rw") - randomAccessWriter?.apply { - setLength(0) // Set file length to 0, to prevent unexpected behavior in case the file already existed + randomAccessWriter?.use { + it.setLength(0) // Set file length to 0, to prevent unexpected behavior in case the file already existed // RIFF header - writeBytes("RIFF") - writeInt(0) // Final file size not known yet, write 0 - writeBytes("WAVE") + it.writeBytes("RIFF") + it.writeInt(0) // Final file size not known yet, write 0 + it.writeBytes("WAVE") // fmt sub-chunk - writeBytes("fmt") - writeInt(Integer.reverseBytes(16)) // Sub-chunk size, 16 for PCM - writeShort(reverseBytes(1.toShort()).toInt()) // AudioFormat, 1 for PCM - writeShort(reverseBytes(numChannels).toInt()) // Number of channels, 1 for mono, 2 for stereo - writeInt(Integer.reverseBytes(sampleRate)) // Sample rate - writeInt(Integer.reverseBytes(sampleRate * bitsPerSample * numChannels / 8)) // Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8 - writeShort(reverseBytes((numChannels * bitsPerSample / 8).toShort()).toInt()) // Block align, NumberOfChannels*BitsPerSample/8 - writeShort(reverseBytes(bitsPerSample).toInt()) // Bits per sample + it.writeBytes("fmt") + it.writeInt(Integer.reverseBytes(16)) // Sub-chunk size, 16 for PCM + it.writeShort(reverseBytes(1.toShort()).toInt()) // AudioFormat, 1 for PCM + it.writeShort(reverseBytes(numChannels).toInt()) // Number of channels, 1 for mono, 2 for stereo + it.writeInt(Integer.reverseBytes(sampleRate)) // Sample rate + it.writeInt(Integer.reverseBytes(sampleRate * bitsPerSample * numChannels / 8)) // Byte rate, SampleRate*NumberOfChannels*BitsPerSample/8 + it.writeShort(reverseBytes((numChannels * bitsPerSample / 8).toShort()).toInt()) // Block align, NumberOfChannels*BitsPerSample/8 + it.writeShort(reverseBytes(bitsPerSample).toInt()) // Bits per sample // data sub-chunk - writeBytes("data") - writeInt(0) // Data chunk size not known yet + it.writeBytes("data") + it.writeInt(0) // Data chunk size not known yet + pluginStatusInfo.fileHeadersWritten = true } } @@ -200,30 +223,36 @@ class PhoneAudioInputManager(service: PhoneAudioInputService) : audioRecordingHandler.execute { audioRecord?.let { val dataRead = it.read(buffer, 0, buffer.size) - randomAccessWriter?.write(buffer) + bufferedOutputStream?.use { bos-> + bos.write(buffer, 0, dataRead) + } payloadSize += dataRead - logger.info("Recording Data in callback") + logger.debug("onPeriodicNotification: Recording Audio") } } } } - private fun stopRecording() { + private fun stopAudioRecording() { logger.warn("Stopping Recording: Saving data") audioRecordingHandler.execute { audioRecord?.stop() - randomAccessWriter?.apply { - seek(4) - writeInt(Integer.reverseBytes(36 + payloadSize)) - seek(40) - writeInt(Integer.reverseBytes(payloadSize)) - close() + state.isRecording.postValue(false) + bufferedOutputStream?.close() + randomAccessWriter?.use { + it.seek(4) + it.writeInt(Integer.reverseBytes(36 + payloadSize)) + it.seek(40) + it.writeInt(Integer.reverseBytes(payloadSize)) } } } override fun onClose() { - super.onClose() + audioRecordingHandler.stop{ + audioRecord?.release() + } + recordProcessingHandler.stop() } companion object { private val logger: Logger = LoggerFactory.getLogger(PhoneAudioInputManager::class.java) From 2c2d5659b2c8800fe952ecaae655baab369352ce Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sun, 7 Jul 2024 12:28:06 +0530 Subject: [PATCH 11/34] Updated UI to start and stop recording --- .../audio/input/PhoneAudioInputActivity.kt | 62 ++++++++++++++++++- .../phone/audio/input/PhoneAudioInputState.kt | 5 ++ .../audio/input/utils/InputRecordInfo.kt | 10 +-- .../res/layout/activity_phone_audio_input.xml | 21 +++++-- .../src/main/res/values/colors.xml | 5 ++ .../src/main/res/values/strings.xml | 3 + 6 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 plugins/radar-android-phone-audio-input/src/main/res/values/colors.xml diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt index bca00535c..ba7682510 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputActivity.kt @@ -1,22 +1,37 @@ package org.radarbase.passive.phone.audio.input import android.content.ComponentName +import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder +import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer import org.radarbase.android.IRadarBinder +import org.radarbase.android.RadarApplication.Companion.radarApp +import org.radarbase.android.source.SourceStatusListener +import org.radarbase.android.util.Boast import org.radarbase.passive.phone.audio.input.databinding.ActivityPhoneAudioInputBinding class PhoneAudioInputActivity : AppCompatActivity() { private lateinit var binding: ActivityPhoneAudioInputBinding private var recorderProvider: PhoneAudioInputProvider? = null - private val state: PhoneAudioInputState? get() = recorderProvider?.connection?.sourceState + private val isRecordingObserver: Observer = Observer { isRecording-> + if (isRecording) { + binding.btnStartStopRec.text = getString(R.string.stop_recording) + binding.btnStartStopRec.setBackgroundColor(getColor(R.color.color_btn_stop_record)) + } else { + binding.btnStartStopRec.text = getString(R.string.start_recording) + binding.btnStartStopRec.setBackgroundColor(getColor(R.color.color_btn_start_record)) + } + } + private val radarServiceConnection = object : ServiceConnection{ override fun onServiceConnected(name: ComponentName?, service: IBinder?) { val radarService = service as IRadarBinder @@ -26,10 +41,16 @@ class PhoneAudioInputActivity : AppCompatActivity() { recorderProvider = provider } } + if (state == null) { + Boast.makeText(this@PhoneAudioInputActivity, R.string.unable_to_record_toast, Toast.LENGTH_SHORT) + return + } + state?.isRecording?.observe(this@PhoneAudioInputActivity, isRecordingObserver) } override fun onServiceDisconnected(name: ComponentName?) { recorderProvider = null + state?.isRecording?.removeObserver(isRecordingObserver) } } @@ -40,4 +61,43 @@ class PhoneAudioInputActivity : AppCompatActivity() { setContentView(binding.root) } + + override fun onStart() { + super.onStart() + bindService(Intent(this, radarApp.radarService), radarServiceConnection, 0) + } + + override fun onResume() { + super.onResume() + binding.btnStartStopRec.setOnClickListener { + if (state != null && state?.status == SourceStatusListener.Status.CONNECTED) { + if (binding.btnStartStopRec.text == getString(R.string.start_recording)) { + val pluginState = state ?: return@setOnClickListener + pluginState.audioRecordManager?.startRecording() + } else if (binding.btnStartStopRec.text == getString(R.string.stop_recording)) { + state?.audioRecordManager?.stopRecording() + disableRecordingAndEnablePlayback() + } + } else { + Boast.makeText(this, R.string.unable_to_record_toast, Toast.LENGTH_SHORT) + } + } + } + + private fun disableRecordingAndEnablePlayback() { + + } + + override fun onPause() { + super.onPause() + } + + override fun onStop() { + super.onStop() + unbindService(radarServiceConnection) + } + + override fun onDestroy() { + super.onDestroy() + } } \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt index 64e2da93f..b582b19e8 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/PhoneAudioInputState.kt @@ -3,6 +3,8 @@ package org.radarbase.passive.phone.audio.input import android.media.AudioDeviceInfo import android.media.AudioFormat import android.media.MediaRecorder +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import org.radarbase.android.source.BaseSourceState import org.radarbase.passive.phone.audio.input.PhoneAudioInputService.Companion.PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT import org.radarbase.passive.phone.audio.input.PhoneAudioInputService.Companion.PHONE_AUDIO_INPUT_RECORDER_BUFFER_SIZE_DEFAULT @@ -10,6 +12,9 @@ import java.util.concurrent.atomic.AtomicInteger class PhoneAudioInputState: BaseSourceState() { + var audioRecordManager: AudioRecordManager? = null + val isRecording: MutableLiveData = MutableLiveData(false) + var audioSource: AtomicInteger = AtomicInteger(MediaRecorder.AudioSource.MIC) var sampleRate: AtomicInteger = AtomicInteger(PHONE_AUDIO_INPUT_CURRENT_SAMPLE_RATE_DEFAULT) var channel: AtomicInteger = AtomicInteger(AudioFormat.CHANNEL_IN_MONO) diff --git a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt index 5aefa201a..7be754200 100644 --- a/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt +++ b/plugins/radar-android-phone-audio-input/src/main/java/org/radarbase/passive/phone/audio/input/utils/InputRecordInfo.kt @@ -1,9 +1,9 @@ package org.radarbase.passive.phone.audio.input.utils data class InputRecordInfo( - val recorderCreated: Boolean, - val recordingPathSet: Boolean, - val fileHeadersWritten: Boolean, - val bufferCreated: Boolean, - val isRecording: Boolean + var recorderCreated: Boolean = false, + var recordingPathSet: Boolean = false, + var fileHeadersWritten: Boolean = false, + var bufferCreated: Boolean = false, + var isRecording: Boolean = false ) \ No newline at end of file diff --git a/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml b/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml index 9fe9afa0c..2123ab273 100644 --- a/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml +++ b/plugins/radar-android-phone-audio-input/src/main/res/layout/activity_phone_audio_input.xml @@ -1,11 +1,24 @@ - - \ No newline at end of file + +