🎸 零基础学 ArkUI25:手把手教你开发一个乐器调音器 App

博主说: 前面我们做了录音器,今天做它的"进阶版"——乐器调音器!调音器通过麦克风采集声音,分析音频频率,告诉你当前音高是哪个音符,音准是偏高还是偏低。这个项目会涉及音频采集、频率分析(FFT)、Canvas 实时绘图等硬核知识。做完它,你就是 ArkUI 音频处理的专家了!


📱 应用场景

吉他手调弦、小提琴手校音、钢琴调律、尤克里里调音……不管什么乐器,调音都是基本功。我们要开发的调音器会实现:

  • 麦克风实时采集音频
  • 音频频率分析(检测基频)
  • 显示当前音符(C4/D4/E4/F4/G4/A4/B4)
  • 音准指示器(偏低 / 标准 / 偏高)
  • 可视化波形显示
  • 乐器模式切换(吉他/小提琴/尤克里里预设)
  • 参考音播放(可选扩展)

⚙️ 运行环境要求

项目 版本要求
操作系统 Windows 10/11、macOS 13+ 或 Ubuntu 22.04+
DevEco Studio 5.0.3.800 及以上
HarmonyOS SDK API 12(HarmonyOS 5.0.0)及以上
应用模型 Stage 模型
权限要求 ohos.permission.MICROPHONE(麦克风权限)
核心 API @ohos.multimedia.audio(音频录制)+ @ohos.multimedia.media
开发语言 ArkTS

🛠️ 实战:从零搭建乐器调音器

Step 1:理解调音器工作原理

调音器的核心是 音频频率检测

麦克风采集 → 音频数据(PCM) → FFT 频谱分析 → 检测基频
                                                      ↓
显示器 ← 音准偏差计算 ← 对比标准音高 ← 确定最近音符

标准音高参考表(A4 = 440Hz):

音符 频率 (Hz) 吉他弦
C4 261.63
D4 293.66
E4 329.63 ①弦(最细)
F4 349.23
G4 392.00 ③弦
A4 440.00 ②弦(基准音)
B4 493.88
E3 164.81 ⑥弦(最粗)
A3 220.00 ⑤弦
D3 146.83 ④弦

Step 2:项目结构

com.example.tuner/
├── entry/src/main/ets/
│   ├── entryability/EntryAbility.ts
│   └── pages/
│       └── Index.ets           ← 主页面
├── entry/src/main/module.json5 ← 配置麦克风权限
└── build-profile.json5

Step 3:完整代码

// pages/Index.ets — 乐器调音器主页面
import audio from '@ohos.multimedia.audio';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';

// 音符频率表(12 平均律,A4 = 440Hz)
const NOTE_FREQUENCIES: { name: string; freq: number; octave: number }[] = [
  { name: 'C', freq: 261.63, octave: 4 },
  { name: 'C#', freq: 277.18, octave: 4 },
  { name: 'D', freq: 293.66, octave: 4 },
  { name: 'D#', freq: 311.13, octave: 4 },
  { name: 'E', freq: 329.63, octave: 4 },
  { name: 'F', freq: 349.23, octave: 4 },
  { name: 'F#', freq: 369.99, octave: 4 },
  { name: 'G', freq: 392.00, octave: 4 },
  { name: 'G#', freq: 415.30, octave: 4 },
  { name: 'A', freq: 440.00, octave: 4 },
  { name: 'A#', freq: 466.16, octave: 4 },
  { name: 'B', freq: 493.88, octave: 4 },
];

// 吉他调音预设
const GUITAR_TUNING = [
  { string: '6弦', note: 'E3', freq: 164.81 },
  { string: '5弦', note: 'A3', freq: 220.00 },
  { string: '4弦', note: 'D3', freq: 146.83 },
  { string: '3弦', note: 'G3', freq: 392.00 },
  { string: '2弦', note: 'B3', freq: 493.88 },
  { string: '1弦', note: 'E4', freq: 329.63 },
];

@Entry
@Component
struct InstrumentTuner {
  // ======== 状态变量 ========
  @State detectedFreq: number = 0;        // 检测到的频率
  @State targetNote: string = '--';       // 最近音符名
  @State targetFreq: number = 440.00;     // 目标频率
  @State centOffset: number = 0;          // 音分偏差 (-50 ~ +50)
  @State isListening: boolean = false;    // 是否正在监听
  @State permissionGranted: boolean = false;
  @State instrumentMode: string = '吉他'; // 乐器模式
  @State activeString: number = -1;       // 高亮的弦

  // 吉他弦状态(用于显示每根弦的调音状态)
  @State strings: { label: string; note: string; tuned: boolean }[] = [
    { label: '6弦', note: 'E', tuned: false },
    { label: '5弦', note: 'A', tuned: false },
    { label: '4弦', note: 'D', tuned: false },
    { label: '3弦', note: 'G', tuned: false },
    { label: '2弦', note: 'B', tuned: false },
    { label: '1弦', note: 'E', tuned: false },
  ];

  private audioRecorder!: audio.AudioRecorder;
  private timerId: number = -1;

  // ======== 生命周期 ========
  aboutToAppear() {
    this.requestPermission();
  }

  // ======== 权限申请 ========
  async requestPermission() {
    const atManager = abilityAccessCtrl.createAtManager();
    try {
      const grantStatus = await atManager.requestPermissionsFromUser(
        getContext(this), ['ohos.permission.MICROPHONE']
      );
      this.permissionGranted = grantStatus[0] === 0; // 0 = granted
    } catch (err) {
      console.error('权限申请失败:', JSON.stringify(err));
      this.permissionGranted = false;
    }
  }

  // ======== 开始/停止监听 ========
  startListening() {
    if (!this.permissionGranted) {
      AlertDialog.show({ message: '请先授予麦克风权限' });
      return;
    }
    this.isListening = true;
    // 启动音频录制(实际项目中用 AudioCapturer 获取 PCM 数据)
    // 这里用 setInterval 模拟频率检测
    this.simulateFrequencyDetection();
  }

  stopListening() {
    this.isListening = false;
    if (this.timerId > -1) {
      clearInterval(this.timerId);
      this.timerId = -1;
    }
  }

  // ======== 模拟频率检测(真机用 FFT) ========
  simulateFrequencyDetection() {
    this.timerId = setInterval(() => {
      // 模拟检测到的频率(在目标频率附近随机波动)
      const target = this.targetFreq;
      const random = (Math.random() - 0.5) * 20;
      this.detectedFreq = target + random;
      this.calculatePitch();
    }, 200);
  }

  // ======== 音高计算 ========
  calculatePitch() {
    // 找到最近的音符
    let minDiff = Infinity;
    let nearestNote = NOTE_FREQUENCIES[0];

    for (const note of NOTE_FREQUENCIES) {
      const diff = Math.abs(this.detectedFreq - note.freq);
      if (diff < minDiff) {
        minDiff = diff;
        nearestNote = note;
      }
    }

    this.targetNote = nearestNote.name + nearestNote.octave;
    this.targetFreq = nearestNote.freq;

    // 计算音分偏差(1 个半音 = 100 音分)
    // cent = 1200 * log2(f / f0)
    const cent = 1200 * Math.log2(this.detectedFreq / nearestNote.freq);
    this.centOffset = Math.round(cent);

    // 判断每根弦是否已调准(偏差 < 5 音分)
    this.strings.forEach((s, i) => {
      const target = GUITAR_TUNING[i]?.freq || 440;
      s.tuned = Math.abs(this.detectedFreq - target) < 3;
    });
  }

  // ======== 选择目标弦 ========
  selectString(index: number) {
    this.activeString = index;
    const tuning = GUITAR_TUNING[index];
    if (tuning) {
      this.targetFreq = tuning.freq;
      this.targetNote = tuning.note;
    }
  }

  // ======== 获取音准指示器颜色 ========
  getTunerColor(): string {
    const abs = Math.abs(this.centOffset);
    if (abs < 5) return '#34C759';   // 绿色 = 准
    if (abs < 20) return '#FF9500';  // 橙色 = 微偏
    return '#FF3B30';                 // 红色 = 偏很多
  }

  // ======== 获取音准指示器位置 ========
  getIndicatorOffset(): number {
    // -50 ~ +50 音分映射到 0% ~ 100%
    const clamped = Math.max(-50, Math.min(50, this.centOffset));
    return ((clamped + 50) / 100) * 100;
  }

  // ======== UI 构建 ========
  build() {
    Column() {
      // ---- 顶部标题 ----
      Text('🎸 乐器调音器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .padding(16)

      // ---- 乐器模式切换 ----
      Row() {
        ForEach(['吉他', '小提琴', '尤克里里'], (mode: string) => {
          Button(mode)
            .backgroundColor(this.instrumentMode === mode ? '#007AFF' : '#F0F0F0')
            .fontColor(this.instrumentMode === mode ? '#fff' : '#333')
            .borderRadius(16)
            .height(32)
            .fontSize(14)
            .onClick(() => { this.instrumentMode = mode; })
        })
      }
      .width('92%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .margin({ bottom: 16 })

      // ---- 音符显示区 ----
      Column() {
        Text(this.targetNote)
          .fontSize(64)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.detectedFreq > 0 ? '#333' : '#ccc')

        Text(this.detectedFreq > 0 ? `${this.detectedFreq.toFixed(1)} Hz` : '等待输入...')
          .fontSize(16)
          .fontColor('#888')
      }
      .padding(20)

      // ---- 音准指示器 ----
      Column() {
        Text(`音准偏差: ${this.centOffset > 0 ? '+' : ''}${this.centOffset} 音分`)
          .fontSize(14)
          .fontColor(this.getTunerColor())
          .fontWeight(FontWeight.Bold)

        // 指示器滑块
        Row() {
          // 0% 位置 = 偏低
          Text('偏低').fontSize(12).fontColor('#FF3B30')
          Column() {
            // 刻度线
            Row() {
              ForEach([0, 1, 2, 3, 4], () => {
                Divider().vertical(true).height(12).color('#ddd')
              })
            }
            .width('100%')
            .justifyContent(FlexAlign.SpaceBetween)

            // 指针
            Divider()
              .vertical(true)
              .height(24)
              .color(this.getTunerColor())
              .width(4)
              .borderRadius(2)
              .position({
                x: `${this.getIndicatorOffset()}%`,
                y: 0
              })
          }
          .width('80%')
          .height(30)

          Text('偏高').fontSize(12).fontColor('#FF3B30')
        }
        .width('90%')
        .alignItems(VerticalAlign.Center)
      }
      .padding(16)
      .width('92%')
      .backgroundColor('#F8F9FA')
      .borderRadius(12)
      .margin({ bottom: 16 })

      // ---- 吉他弦状态 ----
      if (this.instrumentMode === '吉他') {
        Text('🎸 吉他弦调音状态').fontSize(16).fontWeight(FontWeight.Bold)
          .margin({ bottom: 8 })

        Row() {
          ForEach(this.strings, (s: any, index: number) => {
            Column() {
              Text(s.label).fontSize(12).fontColor('#888')
              Text(s.note).fontSize(20).fontWeight(FontWeight.Bold)
              Circle()
                .width(12).height(12)
                .fill(s.tuned ? '#34C759' : '#ddd')
                .margin({ top: 4 })
            }
            .padding(8)
            .backgroundColor(this.activeString === index ? '#E8F0FE' : '#F8F8F8')
            .borderRadius(8)
            .onClick(() => { this.selectString(index as number); })
          })
        }
        .width('96%')
        .justifyContent(FlexAlign.SpaceEvenly)
        .margin({ bottom: 16 })
      }

      // ---- 控制按钮 ----
      Button(this.isListening ? '⏹ 停止调音' : '🎤 开始调音')
        .width('80%')
        .height(48)
        .backgroundColor(this.isListening ? '#FF3B30' : '#007AFF')
        .fontColor('#fff')
        .borderRadius(24)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          if (this.isListening) {
            this.stopListening();
          } else {
            this.startListening();
          }
        })

      // ---- 提示信息 ----
      Text('对着麦克风弹奏单音,调音器会自动检测音高')
        .fontSize(12)
        .fontColor('#999')
        .margin({ top: 12 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }
}

📚 核心知识点深度解析

1. 音高检测原理

调音器的核心算法基于 FFT(快速傅里叶变换)

时域信号(PCM 采样) → FFT → 频域信号(频谱)
                                      ↓
找到能量最大的频率分量 → 即基频 (Fundamental Frequency)
                                      ↓
基频 → 查频率表 → 最近音符名 + 音分偏差

音分计算公式:

cent = 1200 × log₂(检测频率 / 标准频率)

cent > 0 → 偏高
cent < 0 → 偏低
|cent| < 5 → 已调准 ✅

2. 12 平均律

现代音乐使用 12 平均律,每个半音的频率比为 2^(1/12):

f(n) = 440 × 2^(n/12)
其中 n 是距离 A4 的半音数

3. 吉他弦标准调音

音符 频率 对应场景
1弦 (最细) E4 329.63 Hz 高音
2弦 B3 246.94 Hz
3弦 G3 196.00 Hz
4弦 D3 146.83 Hz
5弦 A3 220.00 Hz
6弦 (最粗) E3 82.41 Hz 低音

⚠️ 避坑指南

原因 正确做法
频率检测不准确 麦克风采样率不足 使用 AudioCapturer 设置 44100Hz
检测到泛音而非基频 FFT 峰值可能是 2倍/3倍频 用 YIN 算法或 HPS 算法找基频
权限申请崩溃 没捕获异常 try/catch 包裹 requestPermissionsFromUser
调音延迟大 缓冲区太大 设置 bufferSize 为 1024 或 2048
背景噪音干扰 环境音被一起检测 加高通滤波器(>80Hz)过滤低频噪音
指示器跳动 相邻帧检测结果差异大 滑动平均平滑(取最近 5 次均值)

🔥 最佳实践

  1. 滑动平均平滑:取最近 3-5 次检测结果的均值,避免指针抖动
  2. 置信度判定:当频谱峰值不够明显时显示"信号弱",不显示不可靠的频率
  3. 音分偏差颜色编码:绿色=准(0-5)、橙色=微偏(5-20)、红色=大偏(>20)
  4. 乐器预设切换:不同乐器的音域不同,切换时自动调整目标频率范围
  5. 参考音播放:增加"播放参考音"功能,让用户靠耳朵调音
  6. 噪声门限:低于 -40dB 的音频信号不处理,节省算力

🚀 扩展挑战

  1. 真实 FFT 实现:使用 @ohos.multimedia.audio.AudioCapturer 获取 PCM 数据 + 自行实现 FFT
  2. 波形可视化:用 Canvas 绘制实时音频波形
  3. 多弦同时检测:同时检测多根弦的音(适合扫弦调音)
  4. 调音历史记录:记录每次调音的偏差曲线
  5. 节拍器:集成节拍器功能,一键切换调音/节拍模式
  6. 半音阶模式:显示当前音阶所有半音,适合管乐器调音

运行结果完整截图:
在这里插入图片描述

官方文档: HarmonyOS 应用开发文档

  • 开发者社区: 华为开发者论坛
  • 欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net/
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐