uniapp实现音频文件解析及画频谱柱状图支持小程序h5
家人们,我废话少说,先看效果图(= ̄ω ̄=)喵了个咪!
先说一下技术栈
- uniapp+vue3+l-echat
- css引入了一个colorUI库,我觉得这个特别好用,基本不用写样式了
- 柱状图引用的是一个l-echart 封装好的,直接用
- 封装了一个音频解析js
我们直接讲代码
1.首先是dom树结构
<template>
<view class="w100 bg-white">
<view class="cu-bar bg-white">
<view class="action" style="font-size: 30rpx;">
文件名:{{data.audioFileName}}
</view>
<view class="action">
</view>
</view>
<LEchart class="echartDom" ref="chartRef" :customStyle="{width:'100%',height:'70vh'}" @finished="init"></LEchart>
<view class="w100 padding-xs">
<button class="cu-btn bg-blue" @click="playAudio">播放</button>
<button class="cu-btn bg-blue margin-left-xs" @click="stopAudio">暂停</button>
</view>
<view class="w100 padding-xs">
<button class="cu-btn margin-right-xs margin-bottom-xs" :class="{'bg-blue':data.active==index}"
v-for="(item,index) in data.freqArray" :key="index" @click="ChangeRange(item,index)">{{item.label}}</button>
</view>
</view>
</template>
2.js相关逻辑
<script setup>
import LEchart from '@/components/l-echart/l-echart.vue'
// lime-echart是一个demo的组件,用于测试组件
// import LEchart from '@/components/lime-echart/lime-echart.vue'
import {
onMounted,
reactive,
ref
} from "vue"
// nvue 不需要引入
// #ifdef VUE3
// #ifdef MP
// 由于vue3 使用vite 不支持umd格式的包,小程序依然可以使用,但需要使用require
const echarts = require('../../static/echarts.min.js');
const {
FFT,
ComplexArray
} = require('../../static/lib/fft.js');
// #endif
// #ifndef MP
// 由于 vue3 使用vite 不支持umd格式的包,故引入npm的包
import * as echarts from 'echarts';
import {
FFT,
ComplexArray
} from "../../static/lib/fft.js"
// #endif
// #endif
// 柱状图初始化定义
let chartRef = ref(null); // 获取dom
let myChart=ref(null);
const state = reactive({
option: {},
})
state.option = {
animation:false,
legend: {
show: true,
data: []
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
formatter(){}
},
grid: {
left: '5%',
right: '10%',
top: '1%',
bottom: '1%',
containLabel: true
},
xAxis: {
name: 'Hz',
type: 'category',
data: [],
axisLabel: {
showMinLabel:true,
showMaxLabel:true,
color:'#999999'
},
axisTick: {
show: false
},
axisLine: {
show: true,
lineStyle: {
color: '#999999'
}
},
z: 10
},
yAxis: {
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: '#e0e0e0'
}
},
axisTick: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false,
lineStyle: {
type: 'solid',
color: '#e0e0e0'
}
}
},
dataZoom: [{
min: 0,
// 设置滚动条的隐藏与显示
show: false,
// 设置滚动条类型
type: "slider",
// 数据窗口范围的起始数值
startValue: 0,
// 数据窗口范围的结束数值(一页显示多少条数据)
endValue: 1024,
},
{
// 没有下面这块的话,只能拖动滚动条,
// 鼠标滚轮在区域内不能控制外部滚动条
type: "inside",
// 滚轮是否触发缩放
zoomOnMouseWheel: false,
// 鼠标滚轮触发滚动
moveOnMouseMove: true,
moveOnMouseWheel: true,
},
],
series: [{
data: [],
type: "bar",
itemStyle: {
color: '#188df0'
},
}],
color: ['#83bff6']
}
// ****处理音频相关部分****
let innerAudioContext=ref(null)
let data = reactive({
frequencyData: [], // 存储频谱数据
// audioUrl: 'https://wlds.pqyjy.com/gateway/files/Devices/512Hz.wav', // Audio URL
audioUrl: 'https://wlds.pqyjy.com/gateway//files/Devices/2024110003/20241113/2024110003_20241113123034.wav',
audioFileName:'2024110003_20241113123034.wav',//音频文件名称
audioPlaying: false, // 跟踪音频是否正在播放
sampleRate: 0, // 采样率
pcmData: [], // 音频的 PCM 数据
freqArray: [{
index: 0,
min: null,
max: null,
label: '默认'
},
{
index: 1,
min: 200,
max: 800,
label: '[200~800]'
},
{
index: 2,
min: 250,
max: 1000,
label: '[250~1K]'
},
{
index: 3,
min: 250,
max: 2000,
label: '[250~2K]'
},
{
index: 4,
min: 100,
max: 2000,
label: '[100~2K]'
},
],
active: 0,
});
// 从 URL 加载音频
function loadAudio(fileUrl) {
uni.request({
url: fileUrl,
responseType: 'arraybuffer', // 请求响应为二进制数据
success:(res) =>{
const uint8Array = new Uint8Array(res.data);
// 解析 WAV 文件头部,提取 PCM 数据和采样率等信息
const pcmInfo = parseWAVFile(uint8Array);
data.sampleRate= pcmInfo.sampleRate;
data.pcmData= pcmInfo.data;
},
fail(err) {
console.error('加载音频文件失败:', err);
}
});
}
// 解析 WAV 文件头部,提取 PCM 数据和采样率等信息
function parseWAVFile(wavData) {
const buffer = wavData.buffer;
const header = {
sampleRate: (wavData[24] | (wavData[25] << 8) | (wavData[26] << 16) | (wavData[27] << 24)), // 解析采样率
channels: wavData[22], // 声道数
bitsPerSample: wavData[34] // 每个样本的位数
};
// 提取 PCM 数据
const dataView = new DataView(buffer);
const pcmStart = 44; // PCM 数据从第 44 字节开始
const pcmData = [];
for (let i = pcmStart; i < dataView.byteLength; i += 2) {
pcmData.push(dataView.getInt16(i, true) / 32768); // 归一化 PCM 数据
}
return {
data: pcmData,
sampleRate: header.sampleRate,
channels: header.channels,
bitsPerSample: header.bitsPerSample
};
}
// 根据当前播放时间更新 FFT 分析
function updateFFT(currentTime) {
const segmentSize = 1024; // 每次处理的 PCM 数据大小
const startIndex = Math.floor(currentTime * data.sampleRate); // 根据当前时间计算 PCM 数据的起始索引
// 确保 PCM 数据足够长,可以进行 FFT 计算
if (startIndex + segmentSize > data.pcmData.length) {
return;
}
// 获取当前时间段的 PCM 数据
const segment = data.pcmData.slice(startIndex, startIndex + segmentSize);
// 对当前段数据进行 FFT 计算
performFFT(segment, data.sampleRate);
}
// 对 PCM 数据进行 FFT 分析
function performFFT(pcmSegment, sampleRate) {
const fftSize = 1024;
const complexArray = new ComplexArray(fftSize);
// 填充复数数组用于 FFT 计算
for (let i = 0; i < fftSize; i++) {
complexArray.real[i] = pcmSegment[i] || 0; // 如果没有数据,则填充 0
complexArray.imag[i] = 0;
}
// 执行 FFT 计算
complexArray.FFT();
// 处理频率数据
const frequencyData = [];
for (let i = 1; i < fftSize / 2; i++) { // 只取一半频率数据
const real = complexArray.real[i];
const imag = complexArray.imag[i];
const magnitude = Math.sqrt(real ** 2 + imag ** 2); // 计算幅值
const freq = (sampleRate / fftSize) * i; // 计算频率
frequencyData.push({ frequency: freq, magnitude });
}
data.frequencyData = frequencyData;
drawSpectrum(frequencyData); // 绘制频谱
}
// 绘制频谱图
function drawSpectrum(frequencyData){
const xData=[];
const serData=[];
// 把默认携带参数算出来
if(data.active==0){
if(frequencyData.length>0){
data.freqArray[0].min=frequencyData[0].frequency;
data.freqArray[0].max=frequencyData[frequencyData.length-1].frequency;
}
}
// 拿到范围参数
const range=data.freqArray[data.active];
frequencyData.forEach((item)=>{
if(item.frequency>=range.min&&item.frequency<=range.max){
xData.push(item.frequency);
serData.push(item.magnitude);
}
});
state.option.xAxis.data=xData;
state.option.series[0].data=serData;
state.option.dataZoom[0].min=range.min;
state.option.dataZoom[0].max=range.max;
state.option.dataZoom[0].endValue=xData.length;
myChart.setOption(state.option);
}
// 改变范围
function ChangeRange(item,index){
if(index==data.active){
return;
}else{
data.active=index;
drawSpectrum(data.frequencyData);
}
}
// 点击播放按钮时播放音频
function playAudio() {
if (!data.audioPlaying) {
innerAudioContext.play(); // 播放音频
}
}
// 点击停止按钮时停止音频播放
function stopAudio() {
if (data.audioPlaying) {
innerAudioContext.stop(); // 停止音频播放
}
}
// 组件能被调用必须是组件的节点已经被渲染到页面上
onMounted(() => {
// 组件能被调用必须是组件的节点已经被渲染到页面上
setTimeout(async () => {
if (!chartRef.value) return
myChart = await chartRef.value.init(echarts);
myChart.setOption(state.option)
}, 300);
// 音频相关处理部分
loadAudio(data.audioUrl);
// 初始化 innerAudioContext,用于播放音频
innerAudioContext = uni.createInnerAudioContext();
innerAudioContext.src = data.audioUrl;
innerAudioContext.autoplay = false; // 设置为 false,手动控制播放
// 处理音频播放事件
innerAudioContext.onPlay(() => {
console.log("音频播放开始");
data.audioPlaying = true;
});
innerAudioContext.onPause(() => {
console.log("音频播放暂停");
data.audioPlaying = false;
});
innerAudioContext.onStop(() => {
console.log("音频播放停止");
data.audioPlaying = false;
});
innerAudioContext.onEnded(() => {
console.log("音频播放结束");
data.audioPlaying = false;
});
// 当音频播放时实时更新频谱
innerAudioContext.onTimeUpdate(() => {
const currentTime = innerAudioContext.currentTime;
updateFFT(currentTime); // 根据当前时间更新 FFT
});
})
// 渲染完成
const init = () => {
console.log("渲染完成");
}
</script>
这样就大功告成啦!!! 如果帮助到你记得点赞加收藏哦!!!!!
如果需要源码可加wx:x2251774980