android 音效可视化--Visualizer

news2024/11/26 17:20:01

        Visualizer 是使应用程序能够检索当前播放音频的一部分以进行可视化。它不是录音接口,仅返回部分低质量的音频内容。但是,为了保护某些音频数据的隐私,使用 Visualizer 需要 android.permission.RECORD_AUDIO权限。传递给构造函数的音频会话 ID 指示应可视化哪些音频内容:

  • 如果会话为 0,则音频输出混合可视化
  • 如果会话不为 0,则显示来自特定会话android.media.MediaPlayer或使用此音频会话的音频android.media.AudioTrack

可以捕获两种类型的音频内容表现形式:

  • 波形数据:使用该getWaveForm(byte[])方法连续的8位(无符号)单声道样本
  • 频率数据:采用8位幅度FFTgetFft(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 = false
   mVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]
   mVisualizer?.setDataCaptureListener(
                captureListener,
                Visualizer.getMaxCaptureRate() / 2,
                true,
                true
            )
            // Enabled Visualizer and disable when we're done with the stream
            mVisualizer?.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 = false
        mVisualizer?.release()
        mVisualizer = null
    }

完整VisualizerView代码

package com.example.knowledgemanagement.visualizer

import 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.LogUtils


class VisualizerView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) :
    View(context, attrs, defStyleAttr) {
    private val TAG = "VisualizerView"
    private var mBytes: ByteArray? = null
    private var mFFTBytes: ByteArray? = null
    private val mRect = Rect()
    private var mVisualizer: Visualizer? = null
    private var mRenderers: MutableSet<Renderer> = HashSet()
    var left1 = 0
    var top1 = 0
    var right1 = 0
    var bottom1 = 0
    private var mCanvas: Canvas? = null
    private var mAudioSamplingTime: Long = 0
    private var mFftSamplingTime: Long = 0
    private val mSamplingTime = 100 //数据采样时间间隔
    private var isLink = false

    init {
        setLayerType(LAYER_TYPE_SOFTWARE, null) //禁止硬件加速
        init()
    }

    private fun init() {
        mBytes = null
        mFFTBytes = null
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        left1 = left
        top1 = top
        right1 = right
        bottom1 = bottom
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        mCanvas = canvas
        mRect[0, 0, width] = height
        mBytes?.let {
            // Render all audio renderers
            for (r in mRenderers) {
                r.audioRender(canvas, it, mRect)
            }
        }

        mFFTBytes?.let {
            // Render all FFT renderers
            for (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 = false
            mVisualizer?.captureSize = Visualizer.getCaptureSizeRange()[1]

            // Pass through Visualizer data to VisualizerView
            val 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 = bytes
                        invalidate()
                        mAudioSamplingTime = currentTimeMillis
                    }
                }

                override fun onFftDataCapture(
                    visualizer: Visualizer, bytes: ByteArray,
                    samplingRate: Int
                ) {
                    LogUtils.i(TAG, "onFftDataCapture")
                    val currentTimeMillis = System.currentTimeMillis()
                    if (currentTimeMillis - mFftSamplingTime >= mSamplingTime) {
                        mFFTBytes = bytes
                        invalidate()
                        mFftSamplingTime = currentTimeMillis
                    }
                }
            }

            mVisualizer?.setDataCaptureListener(
                captureListener,
                Visualizer.getMaxCaptureRate() / 2,
                true,
                true
            )
            // Enabled Visualizer and disable when we're done with the stream
            mVisualizer?.enabled = true

        } catch (e: RuntimeException) {
        }
    }

    fun setVisualizerEnable(flag: Boolean) {
        mVisualizer?.enabled = flag
    }

    fun release() {
        mVisualizer?.enabled = false
        mVisualizer?.release()
        mVisualizer = null
        isLink = 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 = 96
    override fun onAudioRender(canvas: Canvas, data: ByteArray, rect: Rect) {}

    override fun onFftRender(canvas: Canvas, data: ByteArray, rect: Rect) {
        val baseX = rect.width() / mSpectrumNum
        val height = rect.height()
        for (i in 0 until mSpectrumNum) {
            val magnitude = (baseX * i + baseX / 2).toFloat()
            mFFTPoints?.let {
                it[i * 4] = magnitude
                it[i * 4 + 1] = (height / 2).toFloat()
                it[i * 4 + 2] = magnitude
                it[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.visualizer

import 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.hypot


class 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 = 0
    private var vGap = 0
    private var levelStep = 0
    private var strokeWidth = 0f
    private var strokeLength = 0f
    var mDataEn = true

    init {
        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 * yr
        strokeLength = DN_SL * xr
        hGap = ((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 = value
        if (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: Int
        private 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 = 1
            var i = 2
            while (i < data.size) {
                model[j] = hypot(data[i].toDouble(), data[i + 1].toDouble()).toInt().toByte()
                i += 2
                j++
            }
        } 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 = -4
        for (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.visualizer

import android.graphics.Canvas
import android.graphics.Rect

abstract class Renderer {
    // Have these as members, so we don't have to re-create them each time
    var mPoints: FloatArray? = null
    var mFFTPoints: FloatArray? = null
    var 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2247960.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

汽车HiL测试:利用TS-GNSS模拟器掌握硬件性能的仿真艺术

一、汽车HiL测试的概念 硬件在环&#xff08;Hardware-in-the-Loop&#xff0c;简称HiL&#xff09;仿真测试&#xff0c;是模型基于设计&#xff08;Model-Based Design&#xff0c;简称MBD&#xff09;验证流程中的一个关键环节。该步骤至关重要&#xff0c;因为它整合了实际…

基于Boost库的搜索引擎

本专栏内容为&#xff1a;项目专栏 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;基于Boots的搜索引擎 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &#x1f339;&#x1f339;&#x1f339;关注我带你学习编程知识…

二叉树oj题解析

二叉树 二叉树的最近公共祖先什么是最近公共祖先&#xff1f;leetcode中求二叉树中最近公共祖先解题1.解题2. 根据二叉树创建字符串 二叉树的最近公共祖先 什么是最近公共祖先&#xff1f; 最近的公共祖先指的是这一棵树中两个节点中深度最大的且公共的祖先节点就是最近祖先节…

AI赋能电商:构建高效、智能化的新零售生态

随着人工智能&#xff08;AI&#xff09;技术的不断进步&#xff0c;其在电商领域的应用日益广泛&#xff0c;从购物推荐到供应链管理&#xff0c;再到商品定价&#xff0c;AI正在全面改变传统电商的运营模式&#xff0c;并推动行业向智能化和精细化方向发展。本文将探讨如何利…

【从零开始的LeetCode-算法】43. 网络延迟时间

有 n 个网络节点&#xff0c;标记为 1 到 n。 给你一个列表 times&#xff0c;表示信号经过 有向 边的传递时间。 times[i] (ui, vi, wi)&#xff0c;其中 ui 是源节点&#xff0c;vi 是目标节点&#xff0c; wi 是一个信号从源节点传递到目标节点的时间。 现在&#xff0c;…

【数据结构】树——链式存储二叉树的基础

写在前面 书接上文&#xff1a;【数据结构】树——顺序存储二叉树 本篇笔记主要讲解链式存储二叉树的主要思想、如何访问每个结点、结点之间的关联、如何递归查找每个结点&#xff0c;为后续更高级的树形结构打下基础。不了解树的小伙伴可以查看上文 文章目录 写在前面 一、链…

泷羽sec-linux

基础之linux 声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团…

重新定义社媒引流:AI社媒引流王如何为品牌赋能?

在社交媒体高度竞争的时代&#xff0c;引流已经不再是单纯追求流量的数字游戏&#xff0c;而是要找到“对的用户”&#xff0c;并与他们建立真实的连接。AI社媒引流王通过技术创新和智能策略&#xff0c;重新定义了社媒引流的方式&#xff0c;帮助品牌在精准触达和高效互动中脱…

centos 服务器 docker 使用代理

宿主机使用代理 在宿主机的全局配置文件中添加代理信息 vim /etc/profile export http_proxyhttp://127.0.0.1:7897 export https_proxyhttp://127.0.0.1:7897 export no_proxy"localhost,127.0.0.1,::1,172.171.0.0" docker 命令使用代理 例如我想在使用使用 do…

WebRTC音视频同步原理与实现详解(上)

第一章、RTP时间戳与NTP时间戳 1.1 RTP时间戳 时间戳&#xff0c;用来定义媒体负载数据的采样时刻&#xff0c;从单调线性递增的时钟中获取&#xff0c;时钟的精度由 RTP 负载数据的采样频率决定。 音频和视频的采样频率是不一样的&#xff0c;一般音频的采样频率有 8KHz、…

Matlab 深度学习工具箱 案例学习与测试————求二阶微分方程

clc clear% 定义输入变量 x linspace(0,2,10000);% 定义网络的层参数 inputSize 1; layers [featureInputLayer(inputSize,Normalization"none")fullyConnectedLayer(10)sigmoidLayerfullyConnectedLayer(1)sigmoidLayer]; % 创建网络 net dlnetwork(layers);% 训…

互联网直播/点播EasyDSS视频推拉流平台视频点播有哪些技术特点?

在数字化时代&#xff0c;视频点播应用已经成为我们生活中不可或缺的一部分。监控技术与视频点播的结合正悄然改变着我们获取和享受媒体内容的方式。这一变革不仅体现在技术层面的进步&#xff0c;更深刻地影响了我们。 EasyDSS视频直播点播平台是一款高性能流媒体服务软件。E…

神经网络(系统性学习二):单层神经网络(感知机)

此前篇章&#xff1a; 神经网络中常用的激活函数 神经网络&#xff08;系统性学习一&#xff09;&#xff1a;入门篇 单层神经网络&#xff08;又叫感知机&#xff09; 单层网络是最简单的全连接神经网络&#xff0c;它仅有输入层和输出层&#xff0c;没有隐藏层。即&#x…

构建 Java Web 应用程序:从 Servlet 到数据库交互(Eclipse使用JDBC连接Mysql数据库)

第 1 部分&#xff1a;环境设置 安装 Java Development Kit (JDK)&#xff1a;下载并安装 JDK。设置 IDE&#xff1a;安装并配置 IDE&#xff08;如 IntelliJ IDEA 或 Eclipse&#xff09;。安装数据库&#xff1a;下载并安装 MySQL 数据库。配置数据库&#xff1a;创建数据库…

进程间通信5:信号

引入 我们之前学习了信号量&#xff0c;信号量和信号可不是一个东西&#xff0c;不能混淆。 信号是什么以及一些基础概念 信号是一种让进程给其他进程发送异步消息的方式 信号是随时产生的&#xff0c;无法预测信号可以临时保存下来&#xff0c;之后再处理信号是异步发送的…

jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js

参考资料&#xff1a; jQuery-Word-Export导出word_jquery.wordexport.js下载-CSDN博客 近期又需要自己做个 Html2Doc 的解决方案&#xff0c;因为客户又不想要 Html2pdf 的下载了&#xff0c;当初还给我费尽心思解决Html转pdf时中文输出的问题&#xff08;html转pdf文件下载之…

docker镜像、容器、仓库介绍

docker docker介绍docker镜像命令docker容器命令docker仓库 docker介绍 官网 Docker 是一种开源的容器化平台&#xff0c;用于开发、部署和运行应用。它通过将应用程序及其依赖项打包到称为“容器”的单一包中&#xff0c;使得应用能够在任何环境下运行&#xff0c;不受底层系…

51单片机-独立按键与数码管联动

独立键盘和矩阵键盘检测原理及实现 键盘的分类&#xff1a;编码键盘和非编码键盘 键盘上闭合键的识别由专用的硬件编码器实现&#xff0c;并产生键编码号或键值的称为编码键盘&#xff0c;如&#xff1a;计算机键盘。靠软件编程识别的称为非编码键盘&#xff1b;在单片机组成…

嵌入式驱动开发详解3(pinctrl和gpio子系统)

文章目录 前言pinctrl子系统pin引脚配置pinctrl驱动详解 gpio子系统gpio属性配置gpio子系统驱动gpio子系统API函数与gpio子系统相关的of函数 pinctrl和gpio子系统的使用设备树配置驱动层部分用户层部分 前言 如果不用pinctrl和gpio子系统的话&#xff0c;我们开发驱动时需要先…

STM32C011开发(1)----开发板测试

STM32C011开发----1.开发板测试 概述硬件准备视频教学样品申请源码下载参考程序生成STM32CUBEMX串口配置LED配置堆栈设置串口重定向主循环演示 概述 STM32C011F4P6-TSSOP20 评估套件可以使用户能够无缝评估 STM32C0 系列TSSOP20 封装的微控制器功能&#xff0c;基于 ARM Corte…