微信小程序 web方案实现调音器功能

技术原理与关键能力/API 使用总结

一、项目技术栈总览

  • 平台: 微信小程序, Skyline 渲染引擎 + glass-easel 组件框架
  • 语言: ES6 module, 无构建/无 npm 依赖
  • 状态: app.globalData + wx storage
  • 说明: 无第三方 npm 包(packNpmRelationList 为空,无 package.json),全部依赖微信原生 API

二、核心算法原理

  1. YIN 音高检测 (pitchDetect.js)
  2. 音分/音符换算 (noteUtils.js)
  3. 滤波器 (highPass/lowPass)
  4. DFT 频谱
  5. 乐器识别
  6. 节拍器调度 (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.2minFreq=50maxFreq=2000minRms=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/sizesetTimeout 重试
后台录音耗电 页面隐藏未停 onHide/onUnload 显式 stopCapture
首帧闪烁 Skyline 首次渲染只用 data 初值 页面 data 用模块加载时计算的值初始化
切 tab 闪动 原用 navigateTo 整页跳转 改自定义 tabBar + switchTab(无转场、缓存复用)

一句话总结:AutoTuner 在零三方依赖的前提下,用 RecorderManager 取原始 PCM,自研 YIN + DFT 算法做高精度音高检测,再用 InnerAudioContext/WebAudio + Canvas 2D + 本地存储 完成「采音 → 分析 → 可视化 → 参考音 → 记录」的完整闭环,核心难点集中在真机音频兼容性Skyline 首帧渲染两处。

posted @ 2026-09-11 20:03  Hey,Coder!  阅读(26)  评论(0)    收藏  举报