微信小程序 web方案实现调音器功能
技术原理与关键能力/API 使用总结
一、项目技术栈总览
- 平台: 微信小程序, Skyline 渲染引擎 + glass-easel 组件框架
- 语言: ES6 module, 无构建/无 npm 依赖
- 状态: app.globalData + wx storage
- 说明: 无第三方 npm 包(packNpmRelationList 为空,无 package.json),全部依赖微信原生 API
二、核心算法原理
- YIN 音高检测 (pitchDetect.js)
- 音分/音符换算 (noteUtils.js)
- 滤波器 (highPass/lowPass)
- DFT 频谱
- 乐器识别
- 节拍器调度 (lookahead scheduler)
三、关键"三方"能力(微信原生 API)使用方法
- RecorderManager (PCM 采集)
- InnerAudioContext + FileSystemManager (参考音 WAV)
- WebAudioContext (回退/节拍器点击音)
- Canvas 2D (SelectorQuery fields node)
- Storage Sync
- setInnerAudioOption
- getWindowInfo / pixelRatio
- 自定义 tabBar + switchTab
- 自定义导航栏
AutoTuner 技术原理与关键能力使用总结
说明:本项目没有引入任何第三方 npm 库(
packNpmRelationList为空、无package.json/node_modules),全部依赖微信小程序原生 API + 自研算法。因此下文「关键三方能力」指平台提供的原生 API / 运行时能力。
一、技术栈总览
| 维度 | 选型 | 说明 |
|---|---|---|
| 平台 | 微信小程序 | appid: wxcc484689ab84d1e7 |
| 渲染引擎 | Skyline ("renderer": "skyline") |
新渲染架构,默认 display:block / content-box |
| 组件框架 | glass-easel ("componentFramework") |
新一代组件框架 |
| 模块系统 | ES Module (import/export) |
由开发者工具编译,无构建工具 |
| 状态管理 | app.globalData + wx.setStorageSync |
无 Redux/Vuex 等三方状态库 |
| 依赖 | 无 npm 三方包 | 纯本地算法实现 |
关键全局配置 app.json:
{
"renderer": "skyline",
"componentFramework": "glass-easel",
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true,
"defaultContentBox": true,
"tagNameStyleIsolation": "legacy",
"disableABTest": true
}
},
"lazyCodeLoading": "requiredComponents",
"tabBar": { "custom": true, "list": [/* 4 个 tab */] }
}
二、核心算法原理(自研,非三方库)
1. YIN 音高检测 — utils/pitchDetect.js
不用简单过零率,而是完整实现 de Cheveigné & Kawahara (2002) 的 YIN 流程:
输入 Float32 帧(2048 样本, 44.1kHz)
→ ① RMS 能量门限(过滤静音/底噪)
→ ② 差分函数 d(τ) = Σ (x[i] - x[i+τ])² // 抗倍频歧义
→ ③ 累积均值归一化差分 d'(τ) = d(τ)·τ / Σd // 与幅度无关
→ ④ 首个低于阈值的 τ,再向下找局部最小
→ ⑤ 抛物线插值得到亚样本精度
→ freq = sampleRate / τ'
// 抛物线插值(亚样本精度,精度 ~±0.1 cent)
const denom = 2 * (2 * s1 - s2 - s0)
betterTau = tauEstimate + (s2 - s0) / denom
关键参数:threshold=0.2、minFreq=50、maxFreq=2000、minRms=0.003。
2. 音分 / 音符换算 — utils/noteUtils.js
十二平均律,全部基于 A4 = 440Hz:
centsBetween(target, actual) = 1200 * log2(actual / target)
clampCents(c) = clamp(c, -50, 50)
isInTune(c, tol = 5) = |c| <= tol // ±5 cent 视为调准
frequencyToNote(f) // 半音数 → 音符名 + 八度 + 偏差音分
3. 数字滤波器 — 一阶 IIR
highPass(buffer, 0.98) // ≈144Hz,保留 E2(82)/A2(110)/D3(147) 低音弦
lowPass(buffer, 0.5) // ≈7kHz,保留谐波、滤高频噪声
4. 频谱 — 朴素 Cooley-Tukey DFT
computeSpectrumMagnitudes(buffer, fftSize = 512)
// X[k] = Σ x[n]·e^(-j2πkn/N),仅取前 N/2,仅用于可视化
5. 乐器自动识别 — utils/instrumentRecognizer.js
收集最近 30 帧音高 → 取中位数(抗噪声/离群值)→ 按频段粗分类 → 在对应分类预设中按 log2 距离匹配最近琴弦。
6. 节拍器调度 — utils/metronome.js
经典 Web Audio lookahead scheduler,避免 setInterval 抖动:
_lookahead = 25ms // 每 25ms 唤醒一次
while (nextTickTime < now + 200) { // 提前排 200ms 内的拍
setTimeout(() => playClick(isAccent), nextTickTime - now)
nextTickTime += 60000 / bpm
}
_scheduleTimer = setTimeout(() => this._schedule(), this._lookahead)
三、关键平台 API("三方能力")使用方法
1. 麦克风 PCM 采集 — wx.getRecorderManager()
utils/audioEngine.js 的核心,注意 frameSize 单位是 KB(坑点):
this.recorderManager = wx.getRecorderManager()
this.recorderManager.start({
format: 'PCM',
sampleRate: 44100,
numberOfChannels: 1,
frameSize: frameSamples * 2 / 1024, // 样本数 → KB
encodeBitRate: 128000,
duration: 600000
})
// 帧回调:Int16 → Float32,并按「固定样本窗口」重新切分
this.recorderManager.onFrameRecorded(res => {
const view = new DataView(res.frameBuffer)
const len = res.frameBuffer.byteLength >> 1
for (let i = 0; i < len; i++) buf[bufLen++] = view.getInt16(i * 2, true) / 32768
while (bufLen >= frameSamples) { /* 回调一个窗口 */ }
})
this.recorderManager.onStop(...) // 达时长上限/中断 → 自动续录
this.recorderManager.onError(...) // 权限/采集错误回调
工程要点:官方帧大小机型不一,引擎内部用「累积缓冲 + 固定窗口切分」,保证分析窗口与 UI 刷新率稳定;onStop 里自动重启实现长时间连续监听。
2. 参考音播放 — InnerAudioContext + 合成 WAV
真机兼容性最好(覆盖 iOS 静音键、部分安卓 WebAudio 无声):
// ① 手写 44 字节 WAV 头,把 Float32 采样本编码为 16bit 单声道 WAV
const wav = encodeWav(samples, 44100)
wx.getFileSystemManager().writeFile({
filePath: `${wx.env.USER_DATA_PATH}/tone_xxx.wav`,
data: wav,
success: () => { /* 缓存 + 播放 */ }
})
// ② InnerAudioContext 播放
const audio = wx.createInnerAudioContext()
audio.obeyMuteSwitch = false // iOS 静音键下也发声
audio.volume = volume
audio.src = filePath
audio.play()
// ③ 全局音频策略(只需设置一次)
wx.setInnerAudioOption({ obeyMuteSwitch: false, mixWithOther: true })
低频增强:freq < 200Hz 时叠加 2/3/4 次谐波并提升增益,解决手机小扬声器低音听不清。
3. WebAudio 回退 — wx.createWebAudioContext()
WAV 写入失败或需连续发声时用振荡器:
const ctx = wx.createWebAudioContext()
ctx.resume().catch(() => {}) // 真机首次播放必须 resume
const osc = ctx.createOscillator()
osc.type = 'sine'
osc.frequency.value = freq
const gain = ctx.createGain()
gain.gain.setValueAtTime(0.0001, ctx.currentTime) // exponentialRamp 不能到 0
gain.gain.exponentialRampToValueAtTime(vol, ctx.currentTime + 0.03)
osc.connect(gain); gain.connect(ctx.destination)
osc.start(); osc.stop(ctx.currentTime + duration + 0.05)
4. Canvas 2D 绘制 — createSelectorQuery().fields({ node: true, size: true })
tuner-dial / tuner-strobe / tuner-spectrum 三个组件共用此模式:
this.createSelectorQuery().in(this)
.select('#dial-canvas')
.fields({ node: true, size: true })
.exec(res => {
if (!res?.[0]?.node) { setTimeout(() => this.draw(), 50); return } // 异步渲染兜底
const canvas = res[0].node
const ctx = canvas.getContext('2d')
const dpr = (wx.getWindowInfo() || wx.getSystemInfoSync()).pixelRatio || 2
canvas.width = res[0].width * dpr
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr) // HiDPI 适配
// ... 绘制刻度弧 / 指针 / LED / 频谱柱
})
- 表盘:径向渐变圆 + 弧形刻度(
-50~+50 cent → 角度)+ 渐变指针。 - 频闪:32 段 LED 跑马灯,
setTimeout自驱动,速度随|cents|变化。 - 频谱:仅显示 0~2000Hz,柱高按最大值归一化,峰值/目标频率高亮。
5. 本地持久化 — wx.setStorageSync / getStorageSync
utils/storage.js 统一封装,历史记录上限 200 条:
wx.setStorageSync('autoTuner:history', list)
wx.getStorageSync('autoTuner:history') || []
6. 自定义 tabBar — custom-tab-bar/ + wx.switchTab
顶部 "tabBar": { "custom": true } + 根目录 custom-tab-bar/index.*:
// 页面侧同步选中态(Skyline 下 getTabBar 为异步回调,需兼容两种返回)
_syncTabBar() {
const instance = this.getTabBar(tabBar => {
if (tabBar?.setSelected) tabBar.setSelected(2, theme)
})
if (instance?.setSelected) instance.setSelected(2, theme)
}
Skyline 下 tabBar 根节点需 position: absolute; pointer-events: auto 才能常驻并响应点击。
7. 其它平台 API
| API | 用途 |
|---|---|
wx.showModal / wx.showToast |
权限引导、操作反馈 |
wx.openSetting |
权限被拒后跳转设置 |
wx.getWindowInfo() (回退 getSystemInfoSync) |
取 pixelRatio 做 HiDPI |
wx.setInnerAudioOption |
静音键策略、混音 |
wx.getFileSystemManager |
WAV 写文件 / 淘汰删除 |
四、数据流全景
麦克风
→ RecorderManager(frameBuffer PCM, Int16)
→ AudioEngine 累积成固定窗口(2048)
→ highPass(0.98) + lowPass(0.5)
→ ┌─ YIN detectPitch → 频率
└─ DFT computeSpectrum → 频谱(仅频谱模式)
→ centsBetween(目标, 实测) + clampCents
→ UI(data) → Canvas 表盘/频闪/频谱
→ 会话结束 → appendHistory 写入本地存储
五、工程踩坑与解决(对应实现)
| 问题 | 原因 | 解决 |
|---|---|---|
| 录音帧大小不稳 | frameSize 单位 KB、机型差异 |
引擎累积缓冲 + 固定样本窗切分 |
| 长监听中断 | 达 duration 触发 onStop |
onStop 内自动续录 |
| iOS 静音键无声 | 系统静音策略 | obeyMuteSwitch:false + setInnerAudioOption |
| 部分安卓 WebAudio 无声 | 上下文未激活 | 首次强制 resume,失败回退 WAV |
| 低音参考音听不清 | 小扬声器低频衰减 | 叠加 2/3/4 次谐波 + 增益 |
WXML 不支持 toFixed |
模板表达式受限 | 数据在 JS 侧全部预计算 |
| Canvas 未就绪 | 节点异步渲染 | exec 校验 node/size,setTimeout 重试 |
| 后台录音耗电 | 页面隐藏未停 | onHide/onUnload 显式 stopCapture |
| 首帧闪烁 | Skyline 首次渲染只用 data 初值 |
页面 data 用模块加载时计算的值初始化 |
| 切 tab 闪动 | 原用 navigateTo 整页跳转 |
改自定义 tabBar + switchTab(无转场、缓存复用) |
一句话总结:AutoTuner 在零三方依赖的前提下,用 RecorderManager 取原始 PCM,自研 YIN + DFT 算法做高精度音高检测,再用 InnerAudioContext/WebAudio + Canvas 2D + 本地存储 完成「采音 → 分析 → 可视化 → 参考音 → 记录」的完整闭环,核心难点集中在真机音频兼容性与 Skyline 首帧渲染两处。
联系我:ivesbao@163.com

浙公网安备 33010602011771号