feat(player): add radio recording and real waveform
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m27s

This commit is contained in:
2026-05-21 21:17:51 +02:00
parent 6aa9a59d7b
commit a6a91af402
12 changed files with 1518 additions and 286 deletions
+1
View File
@@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -1,5 +1,154 @@
package es.freetimelab.pluriwave
import android.Manifest
import android.content.pm.PackageManager
import android.media.audiofx.Visualizer
import android.os.Build
import android.os.Handler
import android.os.Looper
import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
class MainActivity : AudioServiceActivity()
class MainActivity : AudioServiceActivity() {
private val visualizerChannel = "pluriwave/audio_visualizer"
private val permissionRequestCode = 4821
private var visualizer: Visualizer? = null
private var pendingSink: EventChannel.EventSink? = null
private var pendingArgs: Map<*, *>? = null
private val mainHandler = Handler(Looper.getMainLooper())
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(
flutterEngine.dartExecutor.binaryMessenger,
visualizerChannel
).setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
pendingSink = events
pendingArgs = arguments as? Map<*, *>
startVisualizerWhenAllowed()
}
override fun onCancel(arguments: Any?) {
stopVisualizer()
pendingSink = null
pendingArgs = null
}
})
}
private fun startVisualizerWhenAllowed() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
checkSelfPermission(Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.RECORD_AUDIO),
permissionRequestCode
)
return
}
startVisualizer()
}
private fun startVisualizer() {
val sink = pendingSink ?: return
val args = pendingArgs
val sessionId = (args?.get("sessionId") as? Number)?.toInt() ?: 0
val bands = ((args?.get("bands") as? Number)?.toInt() ?: 26).coerceIn(8, 96)
stopVisualizer()
try {
val captureSize = Visualizer.getCaptureSizeRange()[1]
visualizer = Visualizer(sessionId).apply {
enabled = false
setCaptureSize(captureSize)
setDataCaptureListener(
object : Visualizer.OnDataCaptureListener {
override fun onWaveFormDataCapture(
visualizer: Visualizer?,
waveform: ByteArray?,
samplingRate: Int
) {
val data = waveform ?: return
val values = downsample(data, bands)
mainHandler.post { sink.success(values) }
}
override fun onFftDataCapture(
visualizer: Visualizer?,
fft: ByteArray?,
samplingRate: Int
) = Unit
},
Visualizer.getMaxCaptureRate() / 2,
true,
false
)
enabled = true
}
} catch (error: Throwable) {
sink.error("VISUALIZER_UNAVAILABLE", error.message, null)
stopVisualizer()
}
}
private fun downsample(data: ByteArray, bands: Int): List<Double> {
if (data.isEmpty()) return emptyList()
val bucket = maxOf(1, data.size / bands)
val values = ArrayList<Double>(bands)
var index = 0
while (index < data.size && values.size < bands) {
var sum = 0.0
var count = 0
val end = minOf(index + bucket, data.size)
for (i in index until end) {
val centered = (data[i].toInt() and 0xFF) - 128
sum += kotlin.math.abs(centered) / 128.0
count++
}
values.add(if (count == 0) 0.0 else (sum / count).coerceIn(0.0, 1.0))
index = end
}
while (values.size < bands) values.add(0.0)
return values
}
private fun stopVisualizer() {
try {
visualizer?.enabled = false
visualizer?.release()
} catch (_: Throwable) {
} finally {
visualizer = null
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode != permissionRequestCode) return
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
startVisualizer()
} else {
pendingSink?.error(
"RECORD_AUDIO_DENIED",
"Permiso de audio denegado para visualizar la onda real",
null
)
}
}
override fun onDestroy() {
stopVisualizer()
super.onDestroy()
}
}