- 小程序录音实时波形图
首先,做这个不需要把MP3转pcm。 结果就是,转了pcm也不知道怎么出波形。搞了我好几天。。。 但是微信现在不需要引入js-mp3库就可以转,仅仅记录一下,代码如下: var audioCtx = wx.createWebAudioContext() audioCtx.decodeAudioData(frameBuffer, audioBuffer => { let float32Array = audioBuffer.getChannelData(0) ...} AudioContext.createAnalyser(),这是浏览器的接口,微信的WebAudioContext暂时没有这个。 页面js: const that = this; var recorderManager = wx.getRecorderManager() //把frameBuffer转一下,再转成普通Array,传给voice组件 recorderManager.onFrameRecorded((res) => { const { frameBuffer } = res let uint8Array = new Uint8Array(frameBuffer) that.setData({ voiceLine: new Array(...uint8Array) }) }) const options = { duration: 40000, numberOfChannels: 1, format: 'mp3', frameSize: 0.01, //这里设置很小,就会只取一帧就触发onFrameRecorded事件 // sampleRate: 44100, encodeBitRate: 16000, } recorderManager.start(options) 页面wxml:引入组件,传递数据 组件voice.wxml: 组件voice.wxss: .canvas { position: fixed; top: 0; left: 10px; right: 60; bottom: 0; width: 300px; height: 100%; } 组件js: var voiceLine = []; var MaxValue = 0; Component({ /** * 组件的属性列表 */ properties: { voice: Array }, /** * 组件的初始数据 */ data: { }, lifetimes: { // 组件刚刚被创建时执行 attached() { wx.createSelectorQuery().in(this) .select('#canvas') .fields({ node: true, size: true, }) .exec(this.init.bind(this)) }, //删除该组件绑定的所有事件 detached() { } }, pageLifetimes: { show: function () { }, hide: function () { // 页面被隐藏 }, resize: function (size) { // 页面尺寸变化 } }, observers: { 'voice': function (voice) { if (!voice instanceof Array || voice.length === 0) { return; } // 显示单一波形,调试用 //解开注释可以查看每个波形,慢慢找规律 // this.renderVoice(voice); // return; let voiceLen = voice.length, plantValue = 0, //常见数据 serialValueNum = 0 //连续计数器 for (var i = 0; i < voiceLen; i++) { //连续9个表示该帧空了 if (serialValueNum > 9) { console.log('总长:', voiceLen, '常见数据:', plantValue, '干了:', serialValueNum) if (voiceLine.length > 60) { voiceLine.shift() } voiceLine.push(0) this.renderCanvas(); return; } if (plantValue != voice[i]) { plantValue = voice[i] serialValueNum = 0 } else { serialValueNum++; } } // 按长度显示 let middleNum = voice.length if (middleNum > MaxValue) { MaxValue = middleNum console.log('MaxValue:', MaxValue); } //长度<100抛弃数据 if (middleNum < 100) { console.log('长度<100抛弃数据:', middleNum, voice); middleNum = 100 } if (voiceLine.length > 60) { voiceLine.shift() } voiceLine.push(middleNum) this.renderCanvas(); }, }, /** * 组件的方法列表 */ methods: { init(res) { const width = res[0].width const height = res[0].height const canvas = res[0].node const ctx = canvas.getContext('2d') const dpr = wx.getSystemInfoSync().pixelRatio canvas.width = width * dpr canvas.height = height * dpr ctx.scale(dpr, dpr) this.ctx = ctx this.width = width this.height = height }, renderVoice(voiceLine) { if (typeof this.ctx === 'undefined') { return } const width = this.width const height = this.height const ctx = this.ctx ctx.clearRect(0, 0, width, height) var len = voiceLine.length, barHeight = 1, q = 0, left = 0 let plantValue = 0, //常见数据 serialValueNum = 0 //连续计数器 console.log(voiceLine); ctx.strokeStyle = 'blue' for (var i = 0; i < len; i++) { if (i == len - 1 && serialValueNum > 9) { //连续5个很罕见 console.log('总长:', len, '常见数据:', plantValue, '干了:', serialValueNum) } if (plantValue != voiceLine[i]) { plantValue = voiceLine[i] serialValueNum = 0 } else { serialValueNum++; } q = i % height left = parseInt(i / height) * 60 + 60 barHeight = (voiceLine[i] / 255) * 60 // 绘制向上的线条 ctx.beginPath(); ctx.moveTo(left, q); ctx.lineTo(left + barHeight, q); ctx.stroke(); } }, renderCanvas() { if (typeof this.ctx === 'undefined') { return } const width = this.width const height = this.height const ctx = this.ctx ctx.clearRect(0, 0, width, height) var len = voiceLine.length, barHeight = 1, t_arr = [] let now_V, min = 100, max = MaxValue - min; ctx.strokeStyle = 'blue' ctx.lineWidth = 3; for (var i = 0; i < len; i++) { now_V = voiceLine[i] == 0 ? 0 : voiceLine[i] - min if (now_V == 0) { ctx.beginPath(); ctx.moveTo(3, i * 8 - 4); ctx.lineTo(3, i * 8 + 4); ctx.stroke(); continue; } t_arr.push(now_V) barHeight = (now_V / max) * 60 // 绘制向上的线条 ctx.beginPath(); ctx.moveTo(0, i * 8); ctx.lineTo(barHeight, i * 8); ctx.stroke(); } t_arr.sort((a, b) => a - b); console.log('Min', t_arr[0], 'Max', t_arr[t_arr.length - 1]) } } }) 原理:说白了,传过来的一串数据,当有大声音时,会突然变长。 当完全静音时,会连续出现85或170这个值。如果是转Int8Array,会有-86这个值。 至于为什么,上面代码解开注释可以观察单个波形。 [图片] 最后成品的案例在:“艺匠人”小程序->新建作品。 感兴趣的童鞋搜一下玩玩吧~
2023-02-07 - 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23