android 音效可视化--Visualizer
Visualizer 是使应用程序能够检索当前播放音频的一部分以进行可视化。它不是录音接口,仅返回部分低质量的音频内容。但是,为了保护某些音频数据的隐私,使用 Visualizer 需要 android.permission.RECORD_AUDIO权限。传递给构造函数的音频会话 ID 指示应可视化哪些音频内容:
- 如果会话为 0,则音频输出混合可视化
- 如果会话不为 0,则显示来自特定会话
android.media.MediaPlayer
或使用此音频会话的音频android.media.AudioTrack
可以捕获两种类型的音频内容表现形式:
- 波形数据:使用该
getWaveForm(byte[])
方法连续的8位(无符号)单声道样本 - 频率数据:采用8位幅度FFT
getFft(byte[])
方法
捕获的长度可以通过分别调用getCaptureSize()
和setCaptureSize(int)
方法来检索或指定。捕获大小必须是返回范围内的 2 的幂getCaptureSizeRange()
。
1. 权限请求
需要在Manifest里面添加
<uses-permission android:name="android.permission.RECORD_AUDIO" />
RECORD_AUDIOAndroid 6.0后需要动态请求权限
val audioPermission =ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)if (audioPermission != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this,permissions,PERMISSION_REQUEST_CODE /* your request code */)}
2. 初始化Visualizer
首先初始化MediaPlayer,初始MediaPlayer是为了拿到audioSessionId
MusicPlayHelper.init(application, object : IMusicPlayListener {override fun loadMusicFinish(boolean: Boolean, position: Int) {MusicPlayHelper.play()}})
初始化Visualizer,musicId就是上面的audioSessionId,如果传0是全局改变,但是有可能会报错。
if (mVisualizer == null) {mVisualizer = Visualizer(musicId)}mVisualizer?.enabled = falsemVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]mVisualizer?.setDataCaptureListener(captureListener,Visualizer.getMaxCaptureRate() / 2,true,true)// Enabled Visualizer and disable when we're done with the streammVisualizer?.enabled = true
setDataCaptureListener
为可视化对象设置采样监听数据的回调,setDataCaptureListener的参数作用如下:
listener:回调对象
rate:采样的频率,其范围是0~Visualizer.getMaxCaptureRate(),此处设置为最大值一半。
waveform:是否获取波形信息
fft:是否获取快速傅里叶变换后的数据
OnDataCaptureListener
中的两个回调方法分别为:
onWaveFormDataCapture:波形数据回调
onFftDataCapture:傅里叶数据回调,即频率数据回调
3.设置Visualizer是否接收数据
enable为true正常接收,为false关闭
fun setVisualizerEnable(flag: Boolean) {mVisualizer?.enabled = flag
}
4. 释放Visualizer
使用完需要调用release方法释放
fun release() {mVisualizer?.enabled = falsemVisualizer?.release()mVisualizer = null}
完整VisualizerView代码
package com.example.knowledgemanagement.visualizerimport android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.media.audiofx.Visualizer
import android.media.audiofx.Visualizer.OnDataCaptureListener
import android.util.AttributeSet
import android.view.View
import com.xing.commonlibrary.log.LogUtilsclass VisualizerView @JvmOverloads constructor(context: Context?,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) :View(context, attrs, defStyleAttr) {private val TAG = "VisualizerView"private var mBytes: ByteArray? = nullprivate var mFFTBytes: ByteArray? = nullprivate val mRect = Rect()private var mVisualizer: Visualizer? = nullprivate var mRenderers: MutableSet<Renderer> = HashSet()var left1 = 0var top1 = 0var right1 = 0var bottom1 = 0private var mCanvas: Canvas? = nullprivate var mAudioSamplingTime: Long = 0private var mFftSamplingTime: Long = 0private val mSamplingTime = 100 //数据采样时间间隔private var isLink = falseinit {setLayerType(LAYER_TYPE_SOFTWARE, null) //禁止硬件加速init()}private fun init() {mBytes = nullmFFTBytes = null}override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {super.onLayout(changed, left, top, right, bottom)left1 = lefttop1 = topright1 = rightbottom1 = bottom}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)mCanvas = canvasmRect[0, 0, width] = heightmBytes?.let {// Render all audio renderersfor (r in mRenderers) {r.audioRender(canvas, it, mRect)}}mFFTBytes?.let {// Render all FFT renderersfor (r in mRenderers) {r.fftRender(canvas, it, mRect)}}}fun link(musicId: Int) {try {//使用前先释放if (mVisualizer != null) {release()}if (mVisualizer == null && !isLink) {mVisualizer = Visualizer(musicId)isLink = true}LogUtils.e(TAG, "nsc =" + mVisualizer?.enabled)mVisualizer?.enabled = falsemVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]// Pass through Visualizer data to VisualizerViewval captureListener: OnDataCaptureListener = object : OnDataCaptureListener {override fun onWaveFormDataCapture(visualizer: Visualizer, bytes: ByteArray,samplingRate: Int) {val currentTimeMillis = System.currentTimeMillis()LogUtils.i(TAG, "onWaveFormDataCapture")if (currentTimeMillis - mAudioSamplingTime >= mSamplingTime) {mBytes = bytesinvalidate()mAudioSamplingTime = currentTimeMillis}}override fun onFftDataCapture(visualizer: Visualizer, bytes: ByteArray,samplingRate: Int) {LogUtils.i(TAG, "onFftDataCapture")val currentTimeMillis = System.currentTimeMillis()if (currentTimeMillis - mFftSamplingTime >= mSamplingTime) {mFFTBytes = bytesinvalidate()mFftSamplingTime = currentTimeMillis}}}mVisualizer?.setDataCaptureListener(captureListener,Visualizer.getMaxCaptureRate() / 2,true,true)// Enabled Visualizer and disable when we're done with the streammVisualizer?.enabled = true} catch (e: RuntimeException) {}}fun setVisualizerEnable(flag: Boolean) {mVisualizer?.enabled = flag}fun release() {mVisualizer?.enabled = falsemVisualizer?.release()mVisualizer = nullisLink = false}fun addRenderer(renderer: Renderer?) {if (renderer != null) {mRenderers.add(renderer)}}fun clearRenderers() {mRenderers.clear()}
}
5. 实现简单的柱状显示
实现很简单,就是将拿到的数据通过canvas.drawLines绘制出来,
class ColumnarRenderer( private val mPaint: Paint) : Renderer() {private val mSpectrumNum = 96override fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect) {}override fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect) {val baseX = rect.width() / mSpectrumNumval height = rect.height()for (i in 0 until mSpectrumNum) {val magnitude = (baseX * i + baseX / 2).toFloat()mFFTPoints?.let {it[i * 4] = magnitudeit[i * 4 + 1] = (height / 2).toFloat()it[i * 4 + 2] = magnitudeit[i * 4 + 3] = (height / 2 - data[i] * 4).toFloat()}}mFFTPoints?.let { canvas.drawLines(it, mPaint) }}
}
然后调用visualizerView添加到Renderer即可
visualizerView.addRenderer(columnarRenderer);
6.实现能量块跳动
代码里面有详细备注
package com.example.knowledgemanagement.visualizerimport android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import java.util.Random
import kotlin.math.abs
import kotlin.math.hypotclass EnergyBlockRenderer(private val mPaint: Paint) : Renderer() {companion object {private const val TAG = "EnergyBlockRenderer"private const val MAX_LEVEL = 30 //音量柱·音频块 - 最大个数private const val CYLINDER_NUM = 26 //音量柱 - 最大个数private const val DN_W = 470 //view宽度与单个音频块占比 - 正常480 需微调private const val DN_H = 300 //view高度与单个音频块占比private const val DN_SL = 10 //单个音频块宽度private const val DN_SW = 2 //单个音频块高度}private var mData = ByteArray(CYLINDER_NUM) //音量柱 数组private var hGap = 0private var vGap = 0private var levelStep = 0private var strokeWidth = 0fprivate var strokeLength = 0fvar mDataEn = trueinit {levelStep = 230 / MAX_LEVEL}fun onLayout(left: Int, top: Int, right: Int, bottom: Int) {val w: Float = (right - left).toFloat()val h: Float = (bottom - top).toFloat()val xr: Float = w / DN_W.toFloat()val yr: Float = h / DN_H.toFloat()strokeWidth = DN_SW * yrstrokeLength = DN_SL * xrhGap = ((w - strokeLength * CYLINDER_NUM) / (CYLINDER_NUM + 1)).toInt()vGap = (h / (MAX_LEVEL + 2)).toInt() //频谱块高度mPaint.strokeWidth = strokeWidth //设置频谱块宽度}//绘制频谱块和倒影private fun drawCylinder(canvas: Canvas, x: Float, value: Byte, rect: Rect) {var value = valueif (value.toInt() == 0) {value = 1} //最少有一个频谱块for (i in 0 until value) { //每个能量柱绘制value个能量块val y = (rect.height() / 2 - i * vGap / 2 - vGap).toFloat() //计算y轴坐标val y1 = (rect.height() / 2 + i * vGap / 2 + vGap).toFloat()//绘制频谱块mPaint.color = color //画笔颜色canvas.drawLine(x, y, x + strokeLength, y, mPaint) //绘制频谱块//绘制音量柱倒影if (i <= 6 && value > 0) {mPaint.color = Color.WHITE //画笔颜色mPaint.alpha = 100 - 100 / 6 * i //倒影颜色canvas.drawLine(x, y1, x + strokeLength, y1, mPaint) //绘制频谱块}}}private val color: Intprivate get() {val ranColor = intArrayOf(Color.RED, Color.YELLOW, Color.MAGENTA, Color.BLUE, Color.GREEN, Color.GRAY,Color.CYAN, Color.LTGRAY, Color.TRANSPARENT)val random = Random()val value = random.nextInt(ranColor.size - 1)return ranColor[value]}override fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect) {}override fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect) {val model = ByteArray(data.size / 2 + 1)if (mDataEn) {model[0] = abs(data[1].toInt()).toByte()var j = 1var i = 2while (i < data.size) {model[j] = hypot(data[i].toDouble(), data[i + 1].toDouble()).toInt().toByte()i += 2j++}} else {for (i in 0 until CYLINDER_NUM) {model[i] = 0}}for (i in 0 until CYLINDER_NUM) {val a = (abs(model[CYLINDER_NUM - i].toInt()) / levelStep).toByte()val b = mData[i]if (a > b) {mData[i] = a} else {if (b > 0) {mData[i]--}}}var j = -4for (i in 0 until CYLINDER_NUM / 2 - 4) {drawCylinder(canvas, strokeWidth / 2 + hGap + i * (hGap + strokeLength), mData[i], rect)}for (i in CYLINDER_NUM downTo CYLINDER_NUM / 2 - 4) {j++drawCylinder(canvas, strokeWidth / 2 + hGap + (CYLINDER_NUM / 2 + j - 1) * (hGap + strokeLength), mData[i - 1], rect)}}}
Render代码
package com.example.knowledgemanagement.visualizerimport android.graphics.Canvas
import android.graphics.Rectabstract class Renderer {// Have these as members, so we don't have to re-create them each timevar mPoints: FloatArray? = nullvar mFFTPoints: FloatArray? = nullvar isPlaying = true// As the display of raw/FFT audio will usually look different, subclasses// will typically only implement one of the below methods/*** Implement this method to audioRender the audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/abstract fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect)/*** Implement this method to audioRender the FFT audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/abstract fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect)// These methods should actually be called for rendering/*** Render the audio data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/fun audioRender(canvas: Canvas, data: ByteArray, rect: Rect) {if (mPoints == null || mPoints!!.size < data.size * 4) {mPoints = FloatArray(data.size * 4)}onAudioRender(canvas, data, rect)}/*** Render the FFT data onto the canvas** @param canvas - Canvas to draw on* @param data - Data to audioRender* @param rect - Rect to audioRender into*/fun fftRender(canvas: Canvas, data: ByteArray, rect: Rect) {if (mFFTPoints == null || mFFTPoints!!.size < data.size * 4) {mFFTPoints = FloatArray(data.size * 4)}onFftRender(canvas, data, rect)}
}
7. 错误原因跟解决办法
下面错误是因为没有获取音乐的SessionId传入,传了0导致的问题。
The Visualizer initCheck failed -3 error typically occurs due to missing
permissions, invalid audio session IDs, hardware limitations, or timing issues.
By addressing these potential causes, you should be able to resolve the issue and
successfully initialize the Visualizer in your Android application.
想看更详细的介绍可以看谷歌文档:Visualizer | Android Developers
代码下载:https://download.csdn.net/download/u011324501/90038203
相关文章:

android 音效可视化--Visualizer
Visualizer 是使应用程序能够检索当前播放音频的一部分以进行可视化。它不是录音接口,仅返回部分低质量的音频内容。但是,为了保护某些音频数据的隐私,使用 Visualizer 需要 android.permission.RECORD_AUDIO权限。传递给构造函数的音频会话 …...
Python人工智能项目报告
一、实践概述 1、实践计划和目的 在现代社会,计算机技术已成为支撑社会发展的核心力量,渗透到生活的各个领域,应关注人类福祉,确保自己的工作成果能够造福社会,同时维护安全、健康的自然环境,设计出具有包…...
DockerFile 构建基础镜像
1.准备东西 DockerFile 文件 以及安装docker环境 文件内容如下: # 使用Alpine Linux作为基础镜像 FROM --platformlinux/amd64 nginx:1.27.2-alpine # 维护者信息 LABEL maintainer"xu_yhao163.com" ENV LANG en_US.UTF-8 ENV LANGUAGE en_US:en ENV …...

卷积神经网络学习记录
目录 神经网络基础定义: 基本组成部分 工作流程 卷积层(卷积定义)【CONV】: 卷积层(Convolutional Layer) 特征提取:卷积层的主要作用是通过卷积核(或滤波器)运算提…...

5种常见的k8s云原生数据管理方案详解
Kubernetes(K8s)是云原生架构的核心组件,提供高效的容器编排和管理功能。在数据存储方面,K8s通过PersistentVolumes(PV)和PersistentVolumeClaims(PVC)机制实现数据持久化࿰…...

[C++]了解内置类型升级
内置类型升级 1.调用模板T时,为什么可以使用T()类型的匿名对象来传参2.内置类型被升级成为类后的使用事项 1.调用模板T时,为什么可以使用T()类型的匿名对象来传参 当我们在定义或声明一个函数时,如果想使用模板T类型的默认构造(例…...

docker镜像源配置、换源、dockerhub国内镜像最新可用加速源(仓库)
一、临时拉取方式 在docker pull后先拼接镜像源域名,后面拼接拉取的镜像名 $ docker pull dockerpull.org/continuumio/miniconda3 二、永久配置方式 vim修改/etc/docker/daemon.json,并重启docker服务。 # 创建目录 sudo mkdir -p /etc/docker# 写…...

什么是 WPF 中的依赖属性?有什么作用?
依赖属性(Dependency Property)是 WPF 的一个核心概念,它为传统的 .NET 属性提供了增强功能,支持绑定、样式、动画和默认值等功能。通过依赖属性,WPF 提供了一种灵活的数据驱动的方式来处理 UI 属性。 1. 什么是依赖属…...
241125学习日志——[CSDIY] [ByteDance] 后端训练营 [16]
CSDIY:这是一个非科班学生的努力之路,从今天开始这个系列会长期更新,(最好做到日更),我会慢慢把自己目前对CS的努力逐一上传,帮助那些和我一样有着梦想的玩家取得胜利!!&…...
如何优化 PHP 性能?
以下是一些常见的优化 PHP 性能的方法: 启用缓存,例如使用 OPcache 来加速 PHP 脚本的执行。合理使用数据库索引,优化数据库查询语句。避免不必要的计算和重复操作,尽量复用数据和结果。减少文件包含的数量和复杂度。优化代码逻辑…...

【Linux服务器】内存问题排查
概述 项目制作过程中经常出现内存问题,在该处对排查思路进行汇总,也对常见问题进行总结,以期待下一次遇到相似问题时可以快速排查,然后解决问题 排查流程总结 首先检查内存的整体情况 使用工具htop和seme快速得知系统内存使用的…...

ModuleNotFoundError: No module named ‘simple_knn‘
【报错】复现 GaussianEditor 时引用 3D Gaussian Splatting 调用simple_knn 时遇到 ModuleNotFoundError: No module named ‘simple_knn‘ 报错: 【原因】 之前安装时直接进行配置pip install simple-knn 【解决办法】 查看 requirements.txt,才发现需…...

【论文分享】采用现场测量、卫星影像和机器学习方法研究空气温度与城市发展强度之间的关系
鉴于城市热问题的严重性,城市化与空气温度之间的关系已成为全球关注的关键问题。本次我们给大家带来一篇SCI论文的全文翻译。该论文提取了常见城市规划指标,这些指标通过卫星影像来确定城市发展的强度。该论文确定的关系可以帮助在城市化和植被平衡的决策…...

Linux -初识 与基础指令1
博客主页:【夜泉_ly】 本文专栏:【Linux】 欢迎点赞👍收藏⭐关注❤️ 文章目录 📚 前言🖥️ 初识🔐 登录 root用户👥 两种用户➕ 添加用户🧑💻 登录 普通用户⚙️ 常见…...

页的初步认识
关于准备 我们在之前的学习中,已经学习了相当一部分有关段的知识,CPU提供了段的机制来给我们的内存进行保护,但实际上我们在x86下的段base是0,实际上并没有偏移 两种分页模式 我们有两种分页模式,29912分页和101012…...

[C++]:IO流
1. IO 流 1.1 流的概念 在C中,存在一种被称为“流”的概念,它描述的是信息流动的过程,具体来说就是信息从外部输入设备(比如常见的键盘)传输到计算机内部(像内存区域),以及信息从内…...

Excel如何批量导入图片
这篇文章将介绍在Excel中如何根据某列数据,批量的导入与之匹配的图片。 准备工作 如图,我们准备了一张员工信息表以及几张员工的照片 可以看到,照片名称是每个人的名字,与Excel表中的B列(姓名)对应 的卢易…...

TCP socket api详解
文章目录 netstat -nltpaccept简单客户端工具 telnet 指定服务连接connect异常处理version 1 单进程版version 2 多进程版version 3 -- 多线程版本version 4 ---- 线程池版本 应用-简单的翻译系统服务器细节write 返回值 客户端守护进程化前台和后台进程的原理Linux的进程间关系…...
《C++搭建神经网络基石:开启智能编程新征程》
在人工智能的璀璨星空中,神经网络无疑是最为耀眼的星座之一。而 C以其卓越的性能和高效的执行效率,成为构建神经网络模型的有力武器。今天,就让我们一同探索如何使用 C构建一个基础的神经网络模型,踏上智能编程的奇妙旅程。 一、…...
if (条件) { return true; } return false; 简写为 return 条件 详解
在 Java 中,将以下代码: if (条件) {return true; } return false;简写为: return 条件;原理 在 Java 中,条件 是一个布尔表达式,它直接返回 true 或 false。所以,if-else 结构中的逻辑判断和返回值的逻…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

排序算法总结(C++)
目录 一、稳定性二、排序算法选择、冒泡、插入排序归并排序随机快速排序堆排序基数排序计数排序 三、总结 一、稳定性 排序算法的稳定性是指:同样大小的样本 **(同样大小的数据)**在排序之后不会改变原始的相对次序。 稳定性对基础类型对象…...