鸿蒙开发-音频播放

4 阅读4分钟

听辨训练怎么做?HarmonyOS音频播放与交互反馈

如果你对乐理学习感兴趣,可以去鸿蒙应用市场搜一下**「耳聪记」**,下载下来体验体验。播放音程、和弦、节奏,选择正确答案,训练音乐听辨能力。体验完了再回来看这篇文章,你会更清楚音频播放和训练交互是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Web Audio API是我的看家本领之一。去年开始转战鸿蒙生态,用ArkTS开发App,发现音频播放这块差异不小。

比如:

  • 音频播放:Web里new Audio(url).play()就能播放;鸿蒙里得用media.createAudioPlayer()
  • 状态管理:Web里用useState管理播放状态;鸿蒙里用@State装饰器。
  • 事件监听:Web里audio.addEventListener('ended', handler);鸿蒙里用player.on('complete', callback)

别担心,接下来这篇文章,我会用"耳聪记"的听辨训练功能,带你看看HarmonyOS怎么播放音频并实现训练交互。


这篇文章聊什么

耳聪记的核心功能是听辨训练

  1. 播放音程、和弦、节奏音频
  2. 显示题目和选项
  3. 判断答案正误
  4. 记录训练成绩

第一步:设计训练数据结构

// Web前端同学看这里:React里我们用interface定义类型
// 鸿蒙里ArkTS也支持interface,用法基本一致

// 音程定义
const INTERVALS = [
  { id: 'unison', name: '纯一度', semitones: 0, sound: 'unison.mp3' },
  { id: 'minor2nd', name: '小二度', semitones: 1, sound: 'minor2nd.mp3' },
  { id: 'major2nd', name: '大二度', semitones: 2, sound: 'major2nd.mp3' },
  { id: 'minor3rd', name: '小三度', semitones: 3, sound: 'minor3rd.mp3' },
  { id: 'major3rd', name: '大三度', semitones: 4, sound: 'major3rd.mp3' },
  { id: 'perfect4th', name: '纯四度', semitones: 5, sound: 'perfect4th.mp3' },
  { id: 'tritone', name: '三全音', semitones: 6, sound: 'tritone.mp3' },
  { id: 'perfect5th', name: '纯五度', semitones: 7, sound: 'perfect5th.mp3' },
];

// 和弦类型
const CHORD_TYPES = [
  { id: 'major', name: '大三和弦', intervals: [0, 4, 7], sound: 'major.mp3' },
  { id: 'minor', name: '小三和弦', intervals: [0, 3, 7], sound: 'minor.mp3' },
  { id: 'dim', name: '减三和弦', intervals: [0, 3, 6], sound: 'dim.mp3' },
  { id: 'aug', name: '增三和弦', intervals: [0, 4, 8], sound: 'aug.mp3' },
  { id: 'maj7', name: '大七和弦', intervals: [0, 4, 7, 11], sound: 'maj7.mp3' },
  { id: 'min7', name: '小七和弦', intervals: [0, 3, 7, 10], sound: 'min7.mp3' },
];

// 训练题目
interface TrainingQuestion {
  id: string;
  type: string;         // 'interval' | 'chord' | 'rhythm'
  correctAnswer: string;
  options: string[];
  audioFile: string;
}

// 训练记录
interface TrainingRecord {
  date: string;
  type: string;
  total: number;
  correct: number;
  accuracy: number;
}

第二步:实现音频播放

// Web前端同学看这里:React里我们用new Audio()播放
// 鸿蒙里用media.createAudioPlayer(),需要异步创建

import { media } from '@kit.MediaKit';

let audioPlayer: media.AudioPlayer | null = null;

async function initAudioPlayer(): Promise<void> {
  audioPlayer = media.createAudioPlayer();
}

async function playAudio(fileName: string): Promise<void> {
  if (!audioPlayer) {
    await initAudioPlayer();
  }

  try {
    // 设置音频源
    audioPlayer!.src = `resources/rawfile/${fileName}`;
    audioPlayer!.play();

    // 监听播放完成
    audioPlayer!.on('complete', () => {
      console.log('播放完成');
    });
  } catch (err) {
    console.error(`播放失败: ${err}`);
  }
}

// 释放资源
function releaseAudioPlayer(): void {
  if (audioPlayer) {
    audioPlayer.release();
    audioPlayer = null;
  }
}

第三步:实现训练页面

// Web前端同学看这里:React里我们用useState管理状态
// 鸿蒙里用@State装饰器,状态变化自动触发UI更新

@Entry
@Component
struct TrainingPage {
  @State currentQuestion: TrainingQuestion | null = null
  @State selectedAnswer: string = ''
  @State isAnswered: boolean = false
  @State isCorrect: boolean = false
  @State score: number = 0
  @State totalQuestions: number = 0

  async aboutToAppear() {
    await this.loadNextQuestion();
  }

  async loadNextQuestion() {
    // 随机选择题型
    const types = ['interval', 'chord'];
    const type = types[Math.floor(Math.random() * types.length)];

    if (type === 'interval') {
      const interval = INTERVALS[Math.floor(Math.random() * INTERVALS.length)];
      this.currentQuestion = {
        id: `q_${Date.now()}`,
        type: 'interval',
        correctAnswer: interval.name,
        options: this.generateOptions(interval.name, INTERVALS.map(i => i.name)),
        audioFile: interval.sound
      };
    } else {
      const chord = CHORD_TYPES[Math.floor(Math.random() * CHORD_TYPES.length)];
      this.currentQuestion = {
        id: `q_${Date.now()}`,
        type: 'chord',
        correctAnswer: chord.name,
        options: this.generateOptions(chord.name, CHORD_TYPES.map(c => c.name)),
        audioFile: chord.sound
      };
    }

    this.selectedAnswer = '';
    this.isAnswered = false;
    this.isCorrect = false;
  }

  generateOptions(correct: string, allOptions: string[]): string[] {
    const options = [correct];
    while (options.length < 4) {
      const random = allOptions[Math.floor(Math.random() * allOptions.length)];
      if (!options.includes(random)) {
        options.push(random);
      }
    }
    return options.sort(() => Math.random() - 0.5);
  }

  async checkAnswer(answer: string) {
    if (this.isAnswered || !this.currentQuestion) return;

    this.selectedAnswer = answer;
    this.isAnswered = true;
    this.isCorrect = answer === this.currentQuestion.correctAnswer;
    this.totalQuestions++;

    if (this.isCorrect) {
      this.score++;
    }
  }

  build() {
    Column() {
      // 得分显示
      Row() {
        Text(`正确: ${this.score}`)
          .fontSize(14)
          .fontColor('#22c55e')
        Text(`总数: ${this.totalQuestions}`)
          .fontSize(14)
          .fontColor('#6b7280')
          .margin({ left: 16 })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ bottom: 20 })

      if (this.currentQuestion) {
        // 题目类型
        Text(this.currentQuestion.type === 'interval' ? '听辨音程' : '听辨和弦')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })

        // 播放按钮
        Button('播放音频')
          .width(120)
          .height(120)
          .backgroundColor('#3b82f6')
          .borderRadius(60)
          .fontSize(16)
          .onClick(() => {
            playAudio(this.currentQuestion!.audioFile);
          })

        // 选项
        ForEach(this.currentQuestion.options, (option: string) => {
          Button(option)
            .width('100%')
            .height(48)
            .backgroundColor(this.getOptionColor(option))
            .borderRadius(12)
            .margin({ top: 8 })
            .onClick(() => this.checkAnswer(option))
        })

        // 下一题按钮
        if (this.isAnswered) {
          Button('下一题')
            .width('100%')
            .height(48)
            .backgroundColor('#3b82f6')
            .borderRadius(12)
            .margin({ top: 20 })
            .onClick(() => this.loadNextQuestion())
        }
      }
    }
    .padding(16)
  }

  private getOptionColor(option: string): string {
    if (!this.isAnswered) return '#f3f4f6';
    if (option === this.currentQuestion?.correctAnswer) return '#22c55e';
    if (option === this.selectedAnswer && !this.isCorrect) return '#ef4444';
    return '#f3f4f6';
  }
}

第四步:常见问题

4.1 音频加载延迟

问题:首次播放有延迟。

解决:在页面加载时预初始化播放器。

4.2 播放中断

问题:快速切换题目时音频混乱。

解决:播放新音频前先调用stop()停止当前播放。


总结

这篇文章围绕"耳聪记"的听辨训练功能,讲解了:

音频播放

  • media.createAudioPlayer()的使用
  • 音频源设置和播放控制
  • 播放完成事件监听

训练交互

  • 随机题目生成
  • 选项判断和反馈
  • 得分统计

如果你对"耳聪记"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。