HarmonyOS 鸿蒙Next中如何实现听歌识曲功能?

HarmonyOS 鸿蒙Next中如何实现听歌识曲功能? 如何实现听歌识曲功能?

6 回复

效果

https://alliance-communityfile-drcn.dbankcdn.com/FileServer/getFile/cmtybbs/567/739/627/0030086000567739627.20251226012151.77889580985698136699540723633175:50001231000000:2800:B5F0AA43A47A09D733313B6514250E7EB6817546ECBD2F0E8DCD0358F5CD1ABC.gif

实现思路

  1. 完成界面ui和动画效果 2.点击事件后申请录音API使用权限、出现动画 3.通过鸿蒙自带的ai语音识别文字功能 4.把识别结果做参数请求后端API获取歌曲信息

完整代码

import { speechRecognizer } from '@kit.CoreSpeechKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import AudioCapturer from '../../utils/AudioCapturer';
import { permissionUtil } from '../../utils/PermissionUtil';


const TAG = 'CoreSpeechKitDemo';
let asrEngine: speechRecognizer.SpeechRecognitionEngine;



@Builder
export function MainPageBuilder() { // 细节2:按照这个格式配置
  Listen()
}

@Component
struct Listen {
  @StorageProp('bottomRectHeight') bottomRectHeight: number = 0;
  @StorageProp('topRectHeight') topRectHeight: number = 0;

  @State isRecognize: boolean = false
  @State circleScaleIn: number = 1
  @State circleScaleOut: number = 1

  @Consume navPathStack:NavPathStack

  build() {
    NavDestination() {
      Column() {
        Row() {
          Image($r('app.media.ai_listen_left')).width(30).margin({ left: 10 })
          Text('听歌识曲').fontSize(16).fontWeight(600)
          Image($r('app.media.ai_listen_gd')).width(30).margin({ right: 10 })
        }.width('100%').justifyContent(FlexAlign.SpaceBetween).onClick(() => this.navPathStack.pushPathByName('AiListen',null))

        Row() {
          Text('开启桌面悬浮球,边刷视频边识曲').fontColor(Color.Gray).margin({ left: 20 })
          Toggle({ type: ToggleType.Switch, isOn: false })
            .selectedColor('#007DFF')
            .switchPointColor('#FFFFFF')
            .onChange((isOn: boolean) => {
              console.info('Component status:' + isOn)
            }).margin({ right: 20 })
        }
        .backgroundColor('#d9e8fc')
        .justifyContent(FlexAlign.SpaceBetween)
        .width('100%')
        .height(40)
        .margin({ top: 10, bottom: 20 })

        // Row() {
        //   Column() {
        //     Image($r('app.media.profile_heart_fill')).width(40).margin({ bottom: 10 }).fillColor('#fc5260')
        //     Text('哼唱识别').fontSize(14)
        //   }
        //
        //   Column() {
        //     Image($r('app.media.profile_heart_fill'))
        //       .width(40)
        //       .margin({ bottom: 10 })
        //       .fillColor('#fc5260')
        //     Text('视频识曲').fontSize(14)
        //   }
        //
        //   Column() {
        //     Image($r('app.media.profile_heart_fill')).width(40).margin({ bottom: 10 }).fillColor('#fc5260')
        //     Text('链接识曲').fontSize(14)
        //   }
        //
        //   Column() {
        //     Image($r('app.media.profile_heart_fill'))
        //       .width(40)
        //       .margin({ bottom: 10 })
        //       .fillColor('#fc5260')
        //     Text('live识别').fontSize(14)
        //   }
        //
        // }.justifyContent(FlexAlign.SpaceAround).width('90%')

        Column({ space: 40 }) {
          Stack() {
            Circle({width:200,height:200})
              .stroke('#dce9fb')
              .strokeWidth(30)
              .fill('none')
              .scale({x:this.circleScaleOut,y:this.circleScaleOut})
            Circle({ width: 200, height: 200 })
              .fill(this.isRecognize ? '#0783ff' : '#c1dbfc')
              .scale({ x: this.circleScaleIn, y:this.circleScaleIn})

            Image(this.isRecognize ? $r('app.media.ai_listen_logo_white') : $r('app.media.logo'))
              .width(150)
              .padding(15)
              .borderRadius('50%')
              .onClick(async () => {
                // 一、申请权限  麦克风
                const state = await permissionUtil.checkPermissions(['ohos.permission.MICROPHONE'], getContext())
                if (!state) return
                console.log('录音API')


                // 特效
                this.isRecognize = !this.isRecognize
                if (this.isRecognize) {
                  animateTo({ duration: 500, iterations: -1, playMode: PlayMode.Alternate }, () => {
                    this.circleScaleIn = 0.85
                  })
                  animateTo({duration:1000,iterations:-1,curve:Curve.EaseInOut},()=>{
                    this.circleScaleOut = 0.85
                  })
                } else {
                  animateTo({ duration: 0 }, () => {
                    this.circleScaleIn = 1
                    this.circleScaleOut= 1
                  })
                }

                // 二、开始的代码、取消的代码  6666666666666666
                if (this.isRecognize) {
                  console.log('识曲开始')
                  this.createByCallback();
                } else {
                  console.log('识曲取消')
                  // 1 关闭鸿蒙自带的ai语音识别文字功能
                  asrEngine.cancel(this.sessionId);
                  // 2 跳转到搜索音乐页面  根据识别后的结果 去服务器数据库里面匹配(切记并不是这么跳转而是自动超时取消、和主动取消后  记得关闭loading动画)

                }
              })
          }

          Text('点击开始识曲')
            .fontWeight(700)
            .fontSize(19)
            .fontColor('#575757')
          Text(this.aiResult)
        }.margin({ top: 40, bottom: 230 }).alignItems(HorizontalAlign.Center)

        Row() {
          Text('听歌识曲榜')
            .border({ width: { right: 2 }, color: Color.Gray })
            .width(100)
            .margin({ right: 15, left: 20 })
            .fontColor('#4487f5')
            .fontWeight(700)
          Text('第57次取消发送-菲菲公主')
            .fontColor(Color.Gray)
            .fontSize(14)
            .textOverflow({ overflow: TextOverflow.Ellipsis })
            .maxLines(1)
            .width(160)
          Image($r('app.media.ai_listen_chevron_right'))
            .fillColor(Color.Gray)
            .width(10)
            .margin({ right: 10 })
        }.backgroundColor('#f3f4fc').height(40).borderRadius(30).width('90%')

      }.padding({ top: px2vp(this.topRectHeight) })
      .linearGradient({ direction: GradientDirection.Bottom, colors: [['#d7e7ff', 0], ['#fff', 0.6]] })

    }.hideTitleBar(true)
  }



  // 极乐世界

  @State createCount: number = 0;
  @State result: boolean = false;
  @State voiceInfo: string = "";
  @State sessionId: string = "123456";
  @State sessionId2: string = "1234567";
  private mAudioCapturer = new AudioCapturer();
  // 创建引擎,通过callback形式返回
  private createByCallback() {
    // 设置创建引擎参数
    let extraParam: Record<string, Object> = {"locate": "CN", "recognizerMode": "short"};
    let initParamsInfo: speechRecognizer.CreateEngineParams = {
      language: 'zh-CN',
      online: 1,
      extraParams: extraParam
    };


    // 调用createEngine方法
    speechRecognizer.createEngine(initParamsInfo, (err: BusinessError, speechRecognitionEngine:
      speechRecognizer.SpeechRecognitionEngine) => {
      if (!err) {
        hilog.info(0x0000, TAG, 'Succeeded in creating engine.');
        // 接收创建引擎的实例
        asrEngine = speechRecognitionEngine;
        this.setListener();
      } else {
        // 无法创建引擎时返回错误码1002200001,原因:语种不支持、模式不支持、初始化超时、资源不存在等导致创建引擎失败
        // 无法创建引擎时返回错误码1002200006,原因:引擎正在忙碌中,一般多个应用同时调用语音识别引擎时触发
        // 无法创建引擎时返回错误码1002200008,原因:引擎已被销毁
        hilog.error(0x0000, TAG, `Failed to create engine. Code: ${err.code}, message: ${err.message}.`);
      }
    });
  }
  // 查询语种信息,以callback形式返回
  private queryLanguagesCallback() {
    // 设置查询相关参数
    let languageQuery: speechRecognizer.LanguageQuery = {
      sessionId: this.sessionId
    };
    // 调用listLanguages方法
    asrEngine.listLanguages(languageQuery, (err: BusinessError, languages: Array<string>) => {
      if (!err) {
        // 接收目前支持的语种信息
        hilog.info(0x0000, TAG, `Succeeded in listing languages, result: ${JSON.stringify(languages)}`);
      } else {
        hilog.error(0x0000, TAG, `Failed to create engine. Code: ${err.code}, message: ${err.message}.`);
      }
    });
  };
  // 开始识别
  private startListeningForWriteAudio() {
    // 设置开始识别的相关参数
    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: { audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16 } //audioInfo参数配置请参考AudioInfo
    }
    // 调用开始识别方法
    asrEngine.startListening(recognizerParams);
  };
  private startListeningForRecording() {
    let audioParam: speechRecognizer.AudioInfo = { audioType: 'pcm', sampleRate: 16000, soundChannel: 1, sampleBit: 16 }
    let extraParam: Record<string, Object> = {
      "recognitionMode": 0,
      "vadBegin": 2000,
      "vadEnd": 3000,
      "maxAudioDuration": 20000
    }
    let recognizerParams: speechRecognizer.StartParams = {
      sessionId: this.sessionId,
      audioInfo: audioParam,
      extraParams: extraParam
    }
    hilog.info(0x0000, TAG, 'startListening start');
    asrEngine.startListening(recognizerParams);
  };
  // 写音频流
  private async writeAudio() {
    this.startListeningForWriteAudio();
    let ctx = this.getUIContext().getHostContext() as Context
    let filenames: string[] = fileIo.listFileSync(ctx.filesDir);
    if (filenames.length <= 0) {
      hilog.error(0x0000, TAG, `No file exists in the target directory.`);
      return;
    }
    let filePath: string = `${ctx.filesDir}/${filenames[0]}`;
    let file = fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE);
    try {
      let buf: ArrayBuffer = new ArrayBuffer(1280);
      let offset: number = 0;
      while (1280 == fileIo.readSync(file.fd, buf, {
        offset: offset
      })) {
        let uint8Array: Uint8Array = new Uint8Array(buf);
        asrEngine.writeAudio(this.sessionId, uint8Array);
        await this.countDownLatch(1);
        offset = offset + 1280;
      }
    } catch (err) {
      hilog.error(0x0000, TAG, `Failed to read from file. Code: ${err.code}, message: ${err.message}.`);
    } finally {
      if (null != file) {
        fileIo.closeSync(file);
      }
    }
  }
  // 麦克风语音转文本
  private async startRecording() {
    this.startListeningForRecording();
    // 录音获取音频
    let data: ArrayBuffer;
    hilog.info(0x0000, TAG, 'create capture success');
    this.mAudioCapturer.init((dataBuffer: ArrayBuffer) => {
      hilog.info(0x0000, TAG, 'start write');
      hilog.info(0x0000, TAG, 'ArrayBuffer ' + JSON.stringify(dataBuffer));
      data = dataBuffer
      let uint8Array: Uint8Array = new Uint8Array(data);
      hilog.info(0x0000, TAG, 'ArrayBuffer uint8Array ' + JSON.stringify(uint8Array));
      // 写入音频流
      asrEngine.writeAudio(this.sessionId2, uint8Array);
    });
  };
  // 计时
  public async countDownLatch(count: number) {
    while (count > 0) {
      await this.sleep(40);
      count--;
    }
  }
  // 睡眠
  private sleep(ms: number):Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  // 设置回调
  @State aiResult:string = ''
  private setListener() {
    // 创建回调对象
    let setListener: speechRecognizer.RecognitionListener = {
      // 开始识别成功回调
      onStart(sessionId: string, eventMessage: string) {
        hilog.info(0x0000, TAG, `onStart, sessionId: ${sessionId} eventMessage: ${eventMessage}`);
      },
      // 事件回调
      onEvent(sessionId: string, eventCode: number, eventMessage: string) {
        hilog.info(0x0000, TAG, `onEvent, sessionId: ${sessionId} eventCode: ${eventCode} eventMessage: ${eventMessage}`);
      },
      // 识别结果回调,包括中间结果和最终结果
      onResult: (sessionId: string, result: speechRecognizer.SpeechRecognitionResult) => {
        hilog.info(0x0000, TAG, `hello onResult, sessionId: ${sessionId} sessionId: ${JSON.stringify(result)}`);
        hilog.info(0x0000, TAG, `hello 结果, sessionId: ${sessionId} sessionId: ${result.result}`);
        this.aiResult = result.result
      },
      // 识别完成回调
      onComplete: (sessionId: string, eventMessage: string) => {
        hilog.info(0x0000, TAG, `onComplete, sessionId: ${sessionId} eventMessage: ${eventMessage}`);
        this.isRecognize = false
        if (this.aiResult) {
          this.navPathStack.pushPathByName('SearchMusic', this.aiResult)
        }
      },
      // 错误回调,错误码通过本方法返回
      // 返回错误码1002200002,开始识别失败,重复启动startListening方法时触发
      // 更多错误码请参考错误码参考
      onError(sessionId: string, errorCode: number, errorMessage: string) {
        hilog.error(0x0000, TAG, `onError, sessionId: ${sessionId} errorCode: ${errorCode} errorMessage: ${errorMessage}`);
      },
    }
    // 设置回调
    asrEngine.setListener(setListener);
    this.startRecording();
  };
  // 极乐世界 end
}

更多关于HarmonyOS 鸿蒙Next中如何实现听歌识曲功能?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


那轻音乐可以怎么处理呀,

楼上讲得很详细啊

🤝🤝,

在HarmonyOS Next中实现听歌识曲功能,需使用音频采集、特征提取和云端匹配技术。通过@ohos.multimedia.audio模块进行音频录制,获取PCM数据。利用音频指纹算法(如频谱分析)提取特征值,生成唯一指纹。通过HTTP/HTTPS请求将指纹发送至音乐识别服务API(如第三方音乐平台接口)进行匹配,返回歌曲信息。需申请网络权限和音频录制权限。

在HarmonyOS Next中实现听歌识曲功能,核心在于音频采集、特征提取和云端/本地匹配。以下是关键步骤和API使用思路:

  1. 音频采集:使用AudioCapturer API实时录制麦克风输入的音频流。需配置采样率(建议16kHz或44.1kHz)、位深和声道数,并持续获取PCM数据。

  2. 特征提取:对采集的音频进行预处理(如降噪、归一化),然后通过算法(如频谱分析、梅尔频率倒谱系数MFCC)生成音频指纹。可借助AudioProcessor或本地AI模型(如NNRt)加速计算。

  3. 匹配服务

    • 云端匹配:将指纹通过网络模块(如Http)发送至音乐识别服务商API(如AcoustID),解析返回的歌曲信息。
    • 本地匹配:若应用内置数据库,可使用RDBObjectBox存储指纹特征,通过相似度算法(如哈希对比)快速检索。
  4. 性能优化:建议采用流式处理,分段采集音频(如10秒片段)并提取特征,以降低内存占用。可结合Worker线程避免阻塞主线程。

  5. 权限与隐私:需在module.json5中声明ohos.permission.MICROPHONE权限,并动态申请。音频数据需本地处理或加密传输,保障用户隐私。

示例代码片段(音频采集):

import audio from '@ohos.multimedia.audio';
// 创建AudioCapturer实例,配置参数并启动采集
let audioCapturer = await audio.createAudioCapturer(audio.AudioCapturerOptions);
audioCapturer.start();
// 循环读取音频数据缓冲区
let buffer = await audioCapturer.read(bufferSize, isBlocking);

注意:具体识别算法需自行实现或集成第三方SDK(需适配HarmonyOS)。建议参考华为音频开发指南和AI能力文档进行深度优化。

回到顶部