diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..c22827c 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d0ebe6b..fa91b97 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,6 +49,8 @@ dependencies { implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) + implementation(libs.timber) + implementation(libs.kotlinx.coroutines.android) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -56,4 +58,4 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/adriankuta/visualizer/MainActivity.kt b/app/src/main/java/dev/adriankuta/visualizer/MainActivity.kt index 54b6f8c..9eaaa8f 100644 --- a/app/src/main/java/dev/adriankuta/visualizer/MainActivity.kt +++ b/app/src/main/java/dev/adriankuta/visualizer/MainActivity.kt @@ -42,6 +42,7 @@ import dev.adriankuta.visualizer.components.MetricsSection import dev.adriankuta.visualizer.components.PermissionSection import dev.adriankuta.visualizer.components.WaveformView import dev.adriankuta.visualizer.ui.theme.VisualizerTheme +import timber.log.Timber /** * Entry point activity that hosts the Compose UI for the audio visualizer demo. @@ -51,6 +52,10 @@ import dev.adriankuta.visualizer.ui.theme.VisualizerTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + // Initialize Timber for logging + Timber.plant(Timber.DebugTree()) + enableEdgeToEdge() setContent { VisualizerTheme { @@ -103,8 +108,8 @@ private fun VisualizerScreen(modifier: Modifier = Modifier) { val observer = LifecycleEventObserver { _, event -> if (!permissionGranted) return@LifecycleEventObserver when (event) { - Lifecycle.Event.ON_START -> runCatching { controller.start() } - Lifecycle.Event.ON_STOP -> runCatching { controller.stop() } + Lifecycle.Event.ON_CREATE -> runCatching { controller.start() } + Lifecycle.Event.ON_DESTROY -> runCatching { controller.stop() } else -> {} } } @@ -179,4 +184,4 @@ fun VisualizerPreview() { VisualizerTheme { VisualizerScreen() } -} \ No newline at end of file +} diff --git a/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt b/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt index 061063a..13e4136 100644 --- a/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt +++ b/app/src/main/java/dev/adriankuta/visualizer/VisualizerController.kt @@ -1,6 +1,8 @@ package dev.adriankuta.visualizer import android.media.audiofx.Visualizer +import kotlinx.coroutines.* +import timber.log.Timber /** * Simple wrapper over Android Visualizer to capture global output mix (audio session 0). @@ -11,6 +13,10 @@ class VisualizerController( private val onFft: (ByteArray) -> Unit, ) { private var visualizer: Visualizer? = null + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private var loggingJob: Job? = null + private var latestWaveform: ByteArray? = null + private var latestFft: ByteArray? = null /** * Starts capturing audio data via [Visualizer]. @@ -23,6 +29,7 @@ class VisualizerController( fun start() { if (visualizer != null) return try { + Timber.d("Starting audio visualizer listening (audioSessionId: $audioSessionId)") val v = Visualizer(audioSessionId) // Use the maximum supported capture size for better resolution val captureSize = Visualizer.getCaptureSizeRange().let { it[1] } @@ -34,7 +41,10 @@ class VisualizerController( waveform: ByteArray?, samplingRate: Int ) { - if (waveform != null) onWaveform(waveform) + if (waveform != null) { + latestWaveform = waveform.clone() + onWaveform(waveform) + } } override fun onFftDataCapture( @@ -42,13 +52,22 @@ class VisualizerController( fft: ByteArray?, samplingRate: Int ) { - if (fft != null) onFft(fft) + if (fft != null) { + latestFft = fft.clone() + onFft(fft) + } } }, rate, true, true) v.enabled = true visualizer = v + + // Start coroutine-based logging for raw data samples every 1 second + startDataLogging() + + Timber.i("Audio visualizer listening started successfully") } catch (e: Throwable) { // If Visualizer cannot be initialized (e.g. permission or device restriction), ensure cleanup + Timber.e(e, "Failed to start audio visualizer listening") stop() throw e } @@ -60,8 +79,45 @@ class VisualizerController( */ @Synchronized fun stop() { + Timber.d("Stopping audio visualizer listening") + + // Stop the logging coroutine + loggingJob?.cancel() + loggingJob = null + visualizer?.enabled = false visualizer?.release() visualizer = null + + // Clear latest data + latestWaveform = null + latestFft = null + + Timber.i("Audio visualizer listening stopped") + } + + /** + * Starts a coroutine that logs raw data samples every 1 second. + */ + private fun startDataLogging() { + loggingJob?.cancel() // Cancel any existing logging job + loggingJob = coroutineScope.launch { + while (isActive) { + delay(1000) // Wait for 1 second + + val waveform = latestWaveform + val fft = latestFft + + if (waveform != null && fft != null) { + // Log sample of raw data (first few bytes to avoid too much log spam) + val waveformSample = waveform.take(8).joinToString(", ") { it.toString() } + val fftSample = fft.take(8).joinToString(", ") { it.toString() } + + Timber.d("Raw data sample - Waveform {size: ${waveform.size}, sample: [$waveformSample...]}, FFT {size: {${fft.size}}, sample: [$fftSample...]}") + } else { + Timber.d("Raw data sample - No data available yet") + } + } + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6520302..1918c55 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.9.3" activityCompose = "1.10.1" composeBom = "2025.08.01" +timber = "5.0.1" +coroutines = "1.8.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -24,9 +26,10 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -