【鸿蒙星光分享】HarmonyOS 语音朗读功能同步教程

HarmonyOS 语音朗读功能同步教程

本文档详细介绍如何将 DigitalSprouting 项目中的语音朗读(TTS)功能同步到 langdu 项目中。

功能概述

语音朗读功能使用 HarmonyOS 的 @kit.MediaKit 实现音频播放,支持:

  • 数字发音朗读 (0-10)
  • 自定义文本音频播放
  • 音量控制
  • AI朗读任意字符

目录结构

完成后的目录结构:

复制代码
langdu/
├── entry/src/main/ets/
│   ├── services/
│   │   └── AudioService.ets      # 音频服务
│   ├── entryability/
│   │   └── EntryAbility.ets      # 初始化服务
│   └── pages/
│       └── Index.ets             # 演示页面
└── entry/src/main/resources/
    └── rawfile/
        └── audio/
            └── numbers/          # 数字发音文件
                ├── number_0.mp3
                ├── number_1.mp3
                └── ...

步骤一:创建 AudioService 服务

1.1 创建 services 目录

entry/src/main/ets/ 下创建 services 文件夹。

1.2 创建 AudioService.ets 文件

创建文件:entry/src/main/ets/services/AudioService.ets

typescript 复制代码
/**
 * 语音朗读服务 - 简化版
 * 用于播放数字发音
 */
import { media } from '@kit.MediaKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 音频服务类
 */
export class AudioService {
  private static instance: AudioService | null = null;
  private context: common.UIAbilityContext | null = null;
  private isInitialized: boolean = false;

  private constructor() {}

  /**
   * 获取单例实例
   */
  static getInstance(): AudioService {
    if (AudioService.instance === null) {
      AudioService.instance = new AudioService();
    }
    return AudioService.instance;
  }

  /**
   * 初始化音频服务
   */
  async initialize(context: common.UIAbilityContext): Promise<void> {
    if (this.isInitialized) {
      console.info('[AudioService] Already initialized');
      return;
    }

    this.context = context;
    this.isInitialized = true;
    console.info('[AudioService] Initialized successfully');
  }

  /**
   * 播放数字发音(0-10)
   * @param number 要播放的数字(0-10)
   * @param volume 音量大小(0.0-1.0),默认 0.9
   */
  async playNumberPronunciation(number: number, volume: number = 0.9): Promise<void> {
    if (!this.isInitialized || this.context === null) {
      console.error('[AudioService] Service not initialized');
      return;
    }

    if (number < 0 || number > 10) {
      console.error(`[AudioService] Invalid number: ${number}. Must be between 0-10`);
      return;
    }

    try {
      // 获取音频文件描述符
      const path: string = `audio/numbers/number_${number}.mp3`;
      const fileDescriptor = await this.context.resourceManager.getRawFd(path);

      const fd: media.AVFileDescriptor = {
        fd: fileDescriptor.fd,
        offset: fileDescriptor.offset,
        length: fileDescriptor.length
      };

      // 创建播放器
      const player: media.AVPlayer = await media.createAVPlayer();

      // 设置错误监听
      player.on('error', async (error: BusinessError) => {
        console.error(`[AudioService] Player error: ${JSON.stringify(error)}`);
        try {
          await player.release();
        } catch (e) {
          console.error(`[AudioService] Release error: ${JSON.stringify(e)}`);
        }
      });

      // 等待播放
      return new Promise<void>((resolve, reject) => {
        player.on('stateChange', async (state: string) => {
          try {
            if (state === 'initialized') {
              await player.prepare();
            } else if (state === 'prepared') {
              await player.setVolume(volume);
              await player.play();
            } else if (state === 'playing') {
              console.info(`[AudioService] Playing number: ${number}`);
              resolve();
            } else if (state === 'completed') {
              await player.release();
            }
          } catch (error) {
            console.error(`[AudioService] State error: ${JSON.stringify(error)}`);
            try {
              await player.release();
            } catch (e) {
              console.error(`[AudioService] Release error: ${JSON.stringify(e)}`);
            }
            reject(error);
          }
        });

        // 设置音频源
        player.fdSrc = fd;
      });
    } catch (error) {
      console.error(`[AudioService] Failed to play: ${JSON.stringify(error)}`);
      throw error;
    }
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    this.isInitialized = false;
    console.info('[AudioService] Released');
  }
}

export const audioService = AudioService.getInstance();

代码说明:

  • 使用单例模式确保全局只有一个音频服务实例
  • initialize() 方法需要传入 UIAbilityContext 用于获取资源
  • playNumberPronunciation() 是核心方法,播放数字发音
  • 使用 AVPlayer 状态机:initializedpreparedplayingcompleted

步骤二:准备音频资源

2.1 创建目录结构

entry/src/main/resources/rawfile/ 下创建音频目录:

复制代码
rawfile/
└── audio/
    └── numbers/

2.2 添加音频文件

将数字发音文件放入 audio/numbers/ 目录:

  • number_0.mp3 - 零
  • number_1.mp3 - 一
  • number_2.mp3 - 二
  • number_3.mp3 - 三
  • number_4.mp3 - 四
  • number_5.mp3 - 五
  • number_6.mp3 - 六
  • number_7.mp3 - 七
  • number_8.mp3 - 八
  • number_9.mp3 - 九
  • number_10.mp3 - 十

音频要求:

  • 格式:MP3
  • 采样率:44.1kHz 或 48kHz
  • 比特率:128-192kbps
  • 时长:1-2秒

注意:您需要自行准备这些音频文件,可以使用 TTS 软件生成或录制。


步骤三:修改 EntryAbility 初始化服务

3.1 编辑 EntryAbility.ets

修改文件:entry/src/main/ets/entryability/EntryAbility.ets

在文件顶部添加导入:

typescript 复制代码
import { audioService } from '../services/AudioService';

onCreate 方法中初始化服务:

typescript 复制代码
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onCreate');

  // 初始化音频服务
  await audioService.initialize(this.context);
}

onDestroy 方法中释放资源:

typescript 复制代码
async onDestroy(): Promise<void> {
  hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onDestroy');

  // 释放音频服务资源
  await audioService.release();
}

完整的 EntryAbility.ets 示例见附录A。


步骤四:创建演示页面

4.1 修改 Index.ets

修改文件:entry/src/main/ets/pages/Index.ets

typescript 复制代码
import { audioService } from '../services/AudioService';

@Entry
@Component
struct Index {
  @State selectedNumber: number = 0;
  @State isPlaying: boolean = false;

  build() {
    Column() {
      // 标题
      Text('语音朗读演示')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 30 })

      // 数字选择区域
      Text(`当前数字: ${this.selectedNumber}`)
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ bottom: 20 })

      // 数字选择按钮
      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
        ForEach([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], (num: number) => {
          Button(num.toString())
            .width(60)
            .height(60)
            .fontSize(20)
            .margin(5)
            .backgroundColor(this.selectedNumber === num ? '#007AFF' : '#E0E0E0')
            .fontColor(this.selectedNumber === num ? '#FFFFFF' : '#333333')
            .onClick(() => {
              this.selectedNumber = num;
            })
        })
      }
      .width('90%')
      .margin({ bottom: 30 })

      // 播放按钮
      Button(this.isPlaying ? '播放中...' : '点击听发音')
        .width('80%')
        .height(50)
        .fontSize(18)
        .backgroundColor(this.isPlaying ? '#CCCCCC' : '#4CAF50')
        .fontColor('#FFFFFF')
        .enabled(!this.isPlaying)
        .onClick(async () => {
          this.isPlaying = true;
          try {
            await audioService.playNumberPronunciation(this.selectedNumber, 0.9);
          } catch (error) {
            console.error(`播放失败: ${JSON.stringify(error)}`);
          }
          // 延迟重置状态,让音频播放完成
          setTimeout(() => {
            this.isPlaying = false;
          }, 1500);
        })

      // 使用说明
      Text('选择数字后点击按钮听发音')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
  }
}

功能说明:

  • 显示数字选择按钮 (0-10)
  • 点击数字选择当前要朗读的数字
  • 点击"听发音"按钮播放对应数字的发音

步骤五:运行测试

5.1 构建项目

在 DevEco Studio 中:

  1. 点击 Build > Build Hap(s)/APP(s) > Build Hap(s)
  2. 等待构建完成

5.2 运行到设备

  1. 连接真机或启动模拟器
  2. 点击 Run 运行项目
  3. 在应用中选择数字并点击播放按钮测试

5.3 常见问题

Q: 音频播放没有声音?

  • 检查设备音量是否开启
  • 确认音频文件路径正确
  • 查看日志确认 AudioService 初始化成功

Q: 找不到音频文件?

  • 确保音频文件放在 rawfile/audio/numbers/ 目录
  • 文件名格式为 number_X.mp3

附录A:完整的 EntryAbility.ets

typescript 复制代码
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { audioService } from '../services/AudioService';

const DOMAIN = 0x0000;
const TAG = 'EntryAbility';

export default class EntryAbility extends UIAbility {
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onCreate');

    // 初始化音频服务
    await audioService.initialize(this.context);
  }

  async onDestroy(): Promise<void> {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onDestroy');

    // 释放音频服务资源
    await audioService.release();
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, TAG, 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onBackground');
  }
}

附录B:技术要点

AVPlayer 状态机

复制代码
idle → initialized → prepared → playing → completed → released
         ↓              ↓          ↓
      prepare()      play()    release()

核心 API

API 说明
media.createAVPlayer() 创建音频播放器
player.fdSrc 设置音频文件描述符
player.prepare() 准备播放
player.play() 开始播放
player.setVolume(volume) 设置音量 (0.0-1.0)
player.release() 释放资源
resourceManager.getRawFd(path) 获取 rawfile 文件描述符

扩展功能

如需扩展功能,可参考 DigitalSprouting 项目的完整 AudioService,支持:

  • 背景音乐播放与控制
  • 音效播放器池
  • 音量持久化设置
  • 预加载优化

总结

通过以上步骤,您已成功将语音朗读功能同步到 langdu 项目。核心要点:

  1. 使用单例模式管理音频服务
  2. 在 EntryAbility 中初始化和释放服务
  3. 使用 AVPlayer 状态机播放音频
  4. 将音频文件放在 rawfile 目录

如有问题,请检查控制台日志中的 [AudioService] 标签信息。


第二部分:AI 语音合成 (TextToSpeech)

功能概述

除了播放预录制的音频文件,HarmonyOS 还提供了 AI 语音合成能力,可以将任意文本实时转换为语音。使用 @kit.CoreSpeechKit 中的 textToSpeech API 实现。

特点:

  • 支持任意中文文本朗读(最大10000字符)
  • 无需预先准备音频文件
  • 支持语速、音量调节
  • 离线模式,无需网络

两种方式对比

特性 AudioService (预录制音频) TTSService (AI语音合成)
实现方式 播放预录制的MP3文件 AI实时合成语音
依赖 @kit.MediaKit @kit.CoreSpeechKit
音频来源 rawfile目录下的音频文件 文本实时转换
内容限制 需要每个内容单独录制 支持任意文本
音色 取决于录制内容 聆小珊女声音色
文件大小 需要存储音频文件 无需额外文件
响应速度 即时播放 需要合成时间
适用场景 固定内容、特定音效 动态文本、大量内容
语速控制 不支持 支持 (0.5-2.0)
音量控制 支持 支持

选择建议

  • 选择 AudioService

    • 内容固定不变(如数字、字母)
    • 需要特定音色或音效
    • 对响应速度要求高
  • 选择 TTSService

    • 内容动态变化
    • 大量文本需要朗读
    • 不想维护音频文件

步骤一:创建 TTSService 服务

1.1 创建 TTSService.ets 文件

创建文件:entry/src/main/ets/services/TTSService.ets

typescript 复制代码
/**
 * AI语音合成服务 - 使用 TextToSpeech API
 * 将文本实时转换为语音播报
 */
import { textToSpeech } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * TTS服务类
 */
export class TTSService {
  private static instance: TTSService | null = null;
  private ttsEngine: textToSpeech.TextToSpeechEngine | null = null;
  private isInitialized: boolean = false;

  private constructor() {}

  /**
   * 获取单例实例
   */
  static getInstance(): TTSService {
    if (TTSService.instance === null) {
      TTSService.instance = new TTSService();
    }
    return TTSService.instance;
  }

  /**
   * 初始化TTS引擎
   */
  async initialize(): Promise<void> {
    if (this.isInitialized) {
      console.info('[TTSService] Already initialized');
      return;
    }

    try {
      // 配置引擎参数
      let extraParam: Record<string, Object> = {
        'style': 'interaction-broadcast',
        'locate': 'CN',
        'name': 'LangduTTS'
      };

      let initParamsInfo: textToSpeech.CreateEngineParams = {
        language: 'zh-CN',
        person: 0,  // 聆小珊女声音色
        online: 1,  // 离线模式
        extraParams: extraParam
      };

      // 创建TTS引擎
      this.ttsEngine = await textToSpeech.createEngine(initParamsInfo);
      this.isInitialized = true;
      console.info('[TTSService] Initialized successfully');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTSService] Failed to initialize: ${err.code} - ${err.message}`);
      throw new Error(`TTS initialization failed: ${err.message}`);
    }
  }

  /**
   * 朗读文本
   * @param text 要朗读的文本(最大10000字符)
   * @param speed 语速(0.5-2.0),默认1.0
   * @param volume 音量(0.0-1.0),默认1.0
   */
  async speak(text: string, speed: number = 1.0, volume: number = 1.0): Promise<void> {
    if (!this.isInitialized || this.ttsEngine === null) {
      console.error('[TTSService] Service not initialized');
      throw new Error('TTS service not initialized');
    }

    if (text.length === 0) {
      console.warn('[TTSService] Empty text');
      return;
    }

    if (text.length > 10000) {
      console.warn('[TTSService] Text too long, truncating to 10000 characters');
      text = text.substring(0, 10000);
    }

    try {
      // 配置播报参数
      let extraParam: Record<string, Object> = {
        'speed': speed,
        'volume': volume,
        'pitch': 1.0,
        'languageContext': 'zh-CN',
        'audioType': 'pcm'
      };

      let speakParams: textToSpeech.SpeakParams = {
        requestId: Date.now().toString(),
        extraParams: extraParam
      };

      // 设置监听器
      return new Promise<void>((resolve, reject) => {
        let speakListener: textToSpeech.SpeakListener = {
          onStart: (requestId: string, response: textToSpeech.StartResponse) => {
            console.info(`[TTSService] Started speaking, requestId: ${requestId}`);
          },
          onComplete: (requestId: string, response: textToSpeech.CompleteResponse) => {
            console.info(`[TTSService] Completed, requestId: ${requestId}`);
            resolve();
          },
          onStop: (requestId: string, response: textToSpeech.StopResponse) => {
            console.info(`[TTSService] Stopped, requestId: ${requestId}`);
            resolve();
          },
          onError: (requestId: string, errorCode: number, errorMessage: string) => {
            console.error(`[TTSService] Error: ${errorCode} - ${errorMessage}`);
            reject(new Error(`TTS error: ${errorMessage}`));
          },
          onData: (requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) => {
            // 音频数据回调,可用于自定义处理
          }
        };

        // 设置监听器并开始播报
        this.ttsEngine!.setListener(speakListener);
        this.ttsEngine!.speak(text, speakParams);
      });
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[TTSService] Failed to speak: ${err.message}`);
      throw new Error(`TTS speak failed: ${err.message}`);
    }
  }

  /**
   * 停止当前播报
   */
  stop(): void {
    if (this.ttsEngine !== null) {
      this.ttsEngine.stop();
      console.info('[TTSService] Stopped');
    }
  }

  /**
   * 判断是否正在播报
   */
  isBusy(): boolean {
    if (this.ttsEngine !== null) {
      return this.ttsEngine.isBusy();
    }
    return false;
  }

  /**
   * 释放资源
   */
  async release(): Promise<void> {
    if (this.ttsEngine !== null) {
      await this.ttsEngine.shutdown();
      this.ttsEngine = null;
    }
    this.isInitialized = false;
    console.info('[TTSService] Released');
  }
}

export const ttsService = TTSService.getInstance();

代码说明:

  • 使用 textToSpeech.createEngine() 创建语音合成引擎
  • speak() 方法支持语速和音量参数
  • 通过 SpeakListener 回调监听播报状态
  • 支持 stop() 中断当前播报

步骤二:创建 AI 朗读演示页面

2.1 创建 TTSDemo.ets 文件

创建文件:entry/src/main/ets/pages/TTSDemo.ets

typescript 复制代码
import { ttsService } from '../services/TTSService';

@Entry
@Component
struct TTSDemo {
  @State inputText: string = '你好,欢迎使用华为鸿蒙语音合成功能。';
  @State isPlaying: boolean = false;
  @State speed: number = 1.0;
  @State volume: number = 1.0;
  @State statusText: string = '准备就绪';

  aboutToAppear(): void {
    // 初始化TTS服务
    ttsService.initialize()
      .then(() => {
        this.statusText = 'TTS引擎已就绪';
      })
      .catch((error: Error) => {
        this.statusText = `初始化失败: ${error.message}`;
      });
  }

  aboutToDisappear(): void {
    // 释放资源
    ttsService.release();
  }

  build() {
    Column() {
      // 标题
      Text('AI语音合成演示')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      // 状态显示
      Text(this.statusText)
        .fontSize(14)
        .fontColor(this.statusText.includes('失败') ? '#FF0000' : '#666666')
        .margin({ bottom: 20 })

      // 文本输入区域
      TextArea({ text: this.inputText, placeholder: '请输入要朗读的文本...' })
        .width('90%')
        .height(150)
        .fontSize(16)
        .onChange((value: string) => {
          this.inputText = value;
        })
        .margin({ bottom: 20 })

      // 语速控制
      Row() {
        Text(`语速: ${this.speed.toFixed(1)}`)
          .fontSize(14)
          .width(80)
        Slider({
          value: this.speed,
          min: 0.5,
          max: 2.0,
          step: 0.1
        })
          .width('60%')
          .onChange((value: number) => {
            this.speed = value;
          })
      }
      .width('90%')
      .margin({ bottom: 10 })

      // 音量控制
      Row() {
        Text(`音量: ${(this.volume * 100).toFixed(0)}%`)
          .fontSize(14)
          .width(80)
        Slider({
          value: this.volume,
          min: 0,
          max: 1,
          step: 0.1
        })
          .width('60%')
          .onChange((value: number) => {
            this.volume = value;
          })
      }
      .width('90%')
      .margin({ bottom: 20 })

      // 播放/停止按钮
      Row({ space: 20 }) {
        Button(this.isPlaying ? '播放中...' : '开始朗读')
          .width('40%')
          .height(50)
          .fontSize(16)
          .backgroundColor(this.isPlaying ? '#CCCCCC' : '#4CAF50')
          .enabled(!this.isPlaying)
          .onClick(async () => {
            if (this.inputText.trim().length === 0) {
              this.statusText = '请输入文本';
              return;
            }

            this.isPlaying = true;
            this.statusText = '正在朗读...';

            try {
              await ttsService.speak(this.inputText, this.speed, this.volume);
              this.statusText = '朗读完成';
            } catch (error) {
              const err = error as Error;
              this.statusText = `朗读失败: ${err.message}`;
            }

            this.isPlaying = false;
          })

        Button('停止')
          .width('40%')
          .height(50)
          .fontSize(16)
          .backgroundColor('#F44336')
          .enabled(this.isPlaying)
          .onClick(() => {
            ttsService.stop();
            this.isPlaying = false;
            this.statusText = '已停止';
          })
      }
      .margin({ bottom: 20 })

      // 预设文本按钮
      Text('预设文本:')
        .fontSize(14)
        .fontColor('#666666')
        .margin({ bottom: 10 })

      Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
        ForEach([
          '你好',
          '欢迎使用鸿蒙系统',
          '今天天气真好',
          '一二三四五六七八九十'
        ], (text: string) => {
          Button(text)
            .fontSize(12)
            .margin(5)
            .backgroundColor('#E0E0E0')
            .fontColor('#333333')
            .onClick(() => {
              this.inputText = text;
            })
        })
      }
      .width('90%')
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F5F5F5')
  }
}

2.2 注册页面路由

entry/src/main/resources/base/profile/main_pages.json 中添加页面:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/TTSDemo"
  ]
}

步骤三:运行测试

3.1 构建并运行

  1. 在 DevEco Studio 中构建项目
  2. 运行到真机或模拟器
  3. 在应用中可以测试两个页面:
    • Index - 预录制音频播放
    • TTSDemo - AI语音合成

3.2 测试 AI 语音合成

  1. 打开 TTSDemo 页面
  2. 等待 TTS 引擎初始化完成
  3. 输入或选择预设文本
  4. 调整语速和音量
  5. 点击"开始朗读"按钮

附录C:TextToSpeech API 说明

引擎参数

参数 说明 取值
language 语种 'zh-CN' (仅支持中文)
person 音色 0 (聆小珊女声)
online 模式 1 (离线模式)

播报参数

参数 说明 取值范围
speed 语速 0.5 - 2.0
volume 音量 0.0 - 1.0
pitch 音调 0.5 - 2.0

回调接口

回调 说明
onStart 开始播报
onComplete 播报完成
onStop 播报停止
onError 发生错误
onData 音频数据

常见错误码

错误码 说明
1002200001 引擎初始化失败
1002200002 引擎繁忙
1002200003 文本为空
1002200004 文本过长

总结

本教程介绍了两种 HarmonyOS 语音朗读实现方式:

  1. 预录制音频播放 (AudioService)

    • 使用 @kit.MediaKit 的 AVPlayer
    • 适合固定内容、高响应要求
  2. AI 语音合成 (TTSService)

    • 使用 @kit.CoreSpeechKit 的 TextToSpeech
    • 适合动态内容、无需维护音频文件

根据实际需求选择合适的方案,也可以两者结合使用。

效果图

项目源代码

https://gitcode.com/daleishen/yuyinlangdu/

班级链接

https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass\&ha_sourceId=89000248

相关推荐
IT考试认证3 小时前
华为AI认证 H13-323 HCIP-AI Solution Architect 题库
人工智能·华为·题库·hcip-ai·h13-323
爱笑的眼睛114 小时前
ArkTS接口与泛型在HarmonyOS应用开发中的深度应用
华为·harmonyos
不凡的凡6 小时前
flutter 管理工具fvm
flutter·harmonyos
柒儿吖6 小时前
Electron for HarmonyOS_PC Swifty 密码管理器适配开源鸿蒙PC开发实践
javascript·electron·harmonyos
一只栖枝6 小时前
HarmonyOS 开发高级认证是什么?含金量高吗?
华为·华为认证·harmonyos·鸿蒙·考证
柒儿吖8 小时前
Electron for 鸿蒙PC - 菜单栏完整开发指南:从原生菜单到自定义菜单的实现
javascript·electron·harmonyos
A懿轩A8 小时前
【2025最新】最新HarmonyOS 6 DevEco Studio下载安装 详细步骤(带图展示)
华为·harmonyos
大雷神8 小时前
HarmonyOS文字书写功能实现指南
华为·harmonyos
进击的阿三姐8 小时前
鸿蒙个人开发者账号如何真机调试
华为·harmonyos