惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
罗磊的独立博客
M
MIT News - Artificial intelligence
G
Google Developers Blog
V
V2EX
D
Docker
博客园_首页
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
WordPress大学
WordPress大学
T
Tailwind CSS Blog
博客园 - 司徒正美
J
Java Code Geeks
L
LangChain Blog
博客园 - 三生石上(FineUI控件)
B
Blog RSS Feed
博客园 - 【当耐特】
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - Franky

博客园 - Hey,Coder!

systemctl slice配置docker最大占用cpu 内存 pipenv环境变量配置 .net8验证码 数据库差异对比工具 docker部署prometheus grafana pgexporter Alertmanager ubuntu24 换源 ssh配置 ubuntu24 Harbor部署 ubuntu24 kvm部署 cockpit web管理界面 .net api代理 Ubuntu 24.04 DMAR IOMMU 报错修复 uniapp iOS App 上架 vs文件软链接 Docker 网络别名 .net8通用中间件处理公共返回值结构 .net环境下OpenTelemetry Body 读取注意事项 jaeger界面显示异常 .net opentelemetry collector jaeger c# opentelemetry 自定义export c# opentelemetry 自定义export c# 生成对象的初始化代码 Elasticsearch ILM 与 Data Stream 概念及实例说明文档 portainer httphelper封装 Ocelot + Consul + SignalR 粘性会话 正交软件架构 docker postgresql17 主从复制 rabbitmq总结与c#示例 docker rabbitmq quartz dashboard docker consul registrator 服务自动注册consul 微服务动态扩容
微信小程序 web方案实现调音器功能
Hey,Coder! · 2026-09-11 · via 博客园 - Hey,Coder!

技术原理与关键能力/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 首帧渲染两处。