HarmonyOS 鸿蒙Next音频录制并保存至用户文件目录, 无法播放

发布于 1周前 作者 sinazl 最后一次编辑是 5天前 来自 鸿蒙OS

HarmonyOS 鸿蒙Next音频录制并保存至用户文件目录, 无法播放
startRecord() 5秒后stopRecord()停止录音, saveAudio()保存至用户文件,
录制的音频文件保存在cacheDir是可以播放的, 但保存到用户文件的音频无法播放
帮忙看下哪里除了问题

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
import fileIo from '@ohos.file.fs';
import { picker } from '@kit.CoreFileKit';
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';

const TAG = 'AudioDemo'

@Entry
@Component
struct AudioDemo {
  private audioCapturer: audio.AudioCapturer | undefined
  private renderModel: audio.AudioRenderer | undefined = undefined;
  private isRecording: boolean = false
  private timeOutId = -1
  private audioStreamInfo: audio.AudioStreamInfo = {
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, // 采样率
    channels: audio.AudioChannel.CHANNEL_2, // 通道
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
  }
  private audioCapturerInfo: audio.AudioCapturerInfo = {
    source: audio.SourceType.SOURCE_TYPE_MIC, // 音源类型
    capturerFlags: 0 // 音频采集器标志
  }
  private filePath = '';
  private fileName = ''
  private inputFile: fileIo.File | undefined
  private waveArrays: number[] = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4]
  @State yMax: number = 40

  aboutToDisappear(): void {
    if (this.timeOutId != -1) {
      clearTimeout(this.timeOutId)
    }
    this.stopRecord()
  }

  build() {
    Column() {
      Button('startRecord(最长5秒)').onClick((event: ClickEvent) => {
        this.checkAudioPermission()
      })
      Button('stopRecord').onClick((event: ClickEvent) => {
        this.stopRecord()
      })
      Button('startPlay').onClick((event: ClickEvent) => {
        this.playAudio()
      })
      Button('stopPlay').onClick((event: ClickEvent) => {
        this.stopPlay()
      })
    }
    .height('100%')
    .width('100%')
  }

  checkAudioPermission(){
    let grantStatus: abilityAccessCtrl.GrantStatus = this.checkAccessToken('ohos.permission.MICROPHONE')
    if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      // Logger.debug('kevin check location 已授权')
      this.startRecord()
    } else {
      let context = getContext(this) as common.UIAbilityContext;
      let atManager = abilityAccessCtrl.createAtManager();
      atManager.requestPermissionsFromUser(context, ['ohos.permission.MICROPHONE']).then((data) => {
        let grantStatus: Array<number> = data.authResults;
        let result: boolean = true
        for (let i = 0; i < grantStatus.length; i++) {
          result = result && grantStatus[i] === 0
        }
        if (result) {
          this.startRecord()
        } else {
          promptAction.showToast({
            message: 'no audio permission',
            alignment: Alignment.Center
          })
        }
      })
    }
  }

  checkAccessToken(permission: Permissions): abilityAccessCtrl.GrantStatus {
    let AtManager = abilityAccessCtrl.createAtManager()
    let bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;
    let promise: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED
    try {
      let bundledata = bundleManager.getBundleInfoForSelfSync(bundleFlags);
      let tokenID = bundledata.appInfo.accessTokenId
      promise = AtManager.verifyAccessTokenSync(tokenID, permission);
    } catch (err) {
    }
    return promise
  }

  startRecord() {
    let audioCapturerOptions: audio.AudioCapturerOptions = {
      streamInfo: this.audioStreamInfo,
      capturerInfo: this.audioCapturerInfo
    }
    let bufferSize = 0
    let context = getContext() as common.UIAbilityContext;
    let path = context.cacheDir;
    this.fileName = 'audio_' +new Date().getTime()+'.wav'
    this.filePath = path + '/' + this.fileName;
    let lastTime = new Date().getTime()
    this.inputFile = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
    let readDataCallback = (buffer: ArrayBuffer) => {
      console.log('kevin', 'record --' + buffer.byteLength + ',bufferSize=' + bufferSize)
      if (new Date().getTime() - lastTime > 300) {
        this.pcm2Db(buffer)
        lastTime = new Date().getTime()
      }

      fileIo.writeSync(this.inputFile?.fd, buffer, {
        offset: bufferSize,
        length: buffer.byteLength
      });
      bufferSize += buffer.byteLength;
    }

    audio.createAudioCapturer(audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例
      if (err) {
        console.error(TAG, `Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`);
        return;
      }
      console.info(TAG, `create AudioCapturer success`);
      this.audioCapturer = capturer;
      if (this.audioCapturer !== undefined) {
        (this.audioCapturer as audio.AudioCapturer).on('readData', readDataCallback);
        let stateGroup =
          [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
        if (stateGroup.indexOf((this.audioCapturer as audio.AudioCapturer).state.valueOf()) === -1) {
          // 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集
          console.error(`${TAG}: start failed`);
          return;
        }
        // 启动采集
        (this.audioCapturer as audio.AudioCapturer).start((err: BusinessError) => {
          if (err) {
            console.error(TAG, 'Capturer start failed.');
          } else {
            console.info(TAG, 'Capturer start success.');
            this.isRecording = true
            this.timeOutId = setTimeout(() => {
              this.stopRecord()
            }, 5000)
          }
        });
      }
    });
  }

  pcm2Db(buffer: ArrayBuffer) {
    // 获取一个 Int32Array 类型的视图,该视图将数组缓冲区解释为带符号 32 位整数数组
    const intArray = new Int32Array(buffer);
    let sum: number = 0;
    for (let i = 0; i < intArray.length; i++) {
      let sample: number = intArray[i] / 32768;
      sum += sample * sample;
    }
    let rms = Math.sqrt(sum / buffer.byteLength);
    let db = 20 * Math.log10(rms);
    // console.log('kevin cwq db is ' + db)
    if (db > 0) {
      this.yMax = Math.min(60, Math.max(2, Math.floor(db)))
    }
  }

  stopRecord() {
    if (this.audioCapturer !== undefined) {
      // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
      if ((this.audioCapturer as audio.AudioCapturer).state.valueOf() !== audio.AudioState.STATE_RUNNING &&
        (this.audioCapturer as audio.AudioCapturer).state.valueOf() !== audio.AudioState.STATE_PAUSED) {
        console.info(TAG, 'Capturer is not running or paused');
        return;
      }

      //停止采集
      (this.audioCapturer as audio.AudioCapturer).stop((err: BusinessError) => {
        if (err) {
          console.error(TAG, 'Capturer stop failed.' + JSON.stringify(err));
        } else {
          console.info(TAG, 'Capturer stop success.');
          if (this.inputFile) {
            fileIo.closeSync(this.inputFile)
          }
          this.release()
          this.saveAudio()
        }
      });
    }
  }

  //保存到手机
  saveAudio() {
    let audioSaveOptions = new picker.AudioSaveOptions();
    // 保存文件名(可选)
    audioSaveOptions.newFileNames = [this.fileName];
    let uri: string = '';
    // 请确保 getContext(this) 返回结果为 UIAbilityContext
    let context = getContext(this) as common.Context;
    const audioViewPicker = new picker.AudioViewPicker(context);
    //用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的uri。
    audioViewPicker.save(audioSaveOptions).then((audioSelectResult: Array<string>) => {
      uri = audioSelectResult[0];
      console.info('kevin audioViewPicker.save to file succeed and uri is:' + uri);
      //这里需要注意接口权限参数是fileIo.OpenMode.READ_WRITE。
      fileIo.open(uri, fileIo.OpenMode.READ_WRITE).then((file) => {
        fileIo.open(this.filePath, fileIo.OpenMode.READ_ONLY).then((srcFile) => {
          let size = fileIo.statSync(srcFile.fd).size
          console.log('kevin', 'srcFile size=' + size)
          fileIo.copyFile(srcFile.fd, file.fd).then(() => {
            console.info('kevin copyFile succeed and size is:' + fileIo.statSync(file.fd).size);
            fileIo.closeSync(file);
            fileIo.closeSync(srcFile);
          }).catch((err: BusinessError) => {
            console.error(`kevin [copyFile] failed, code is ${err.code}, message is ${err.message}`);
          })
        }).catch((err: BusinessError) => {
          console.error(`kevin open [srcFile] failed, code is ${err.code}, message is ${err.message}`);
        })
      }).catch((err: BusinessError) => {
        console.error(`kevin open [file] failed, code is ${err.code}, message is ${err.message}`);
      })


    }).catch((err: BusinessError) => {
      console.error(`kevin save failed, code is ${err.code}, message is ${err.message}`);
    })
  }

  // 销毁实例,释放资源
  release() {
    if (this.audioCapturer !== undefined) {
      // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
      if ((this.audioCapturer as audio.AudioCapturer).state.valueOf() === audio.AudioState.STATE_RELEASED ||
        (this.audioCapturer as audio.AudioCapturer).state.valueOf() === audio.AudioState.STATE_NEW) {
        console.info(TAG, 'Capturer already released');
        return;
      }

      //释放资源
      (this.audioCapturer as audio.AudioCapturer).release((err: BusinessError) => {
        if (err) {
          console.error(TAG, 'Capturer release failed.');
        } else {
          console.info(TAG, 'Capturer release success.');
        }
      });
    }
  }

  playAudio() {
    let bufferSize: number = 0;
    let audioStreamInfo: audio.AudioStreamInfo = {
      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
      channels: audio.AudioChannel.CHANNEL_2, // 通道
      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
    }
    let audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型
      rendererFlags: 0 // 音频渲染器标志
    }
    let audioRendererOptions: audio.AudioRendererOptions = {
      streamInfo: audioStreamInfo,
      rendererInfo: audioRendererInfo
    }
    //确保该路径下存在该资源
    let file: fileIo.File = fileIo.openSync(this.filePath, fileIo.OpenMode.READ_ONLY);
    let size = fileIo.statSync(file.fd).size
    let writeDataCallback = (buffer: ArrayBuffer) => {
      console.log('kevin', 'play audio writeDataCallback--' + buffer.byteLength + ',bufferSize=' + bufferSize)
      if (bufferSize < size) {
        fileIo.readSync(file.fd, buffer, {
          offset: bufferSize,
          length: buffer.byteLength
        });
        bufferSize += buffer.byteLength;
      } else {
        this.stopPlay()
      }
    }
    audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例
      if (!err) {
        console.info(`${TAG}: creating AudioRenderer success`);
        this.renderModel = renderer;
        if (this.renderModel !== undefined) {
          (this.renderModel as audio.AudioRenderer).on('writeData', writeDataCallback);
          let stateGroup =
            [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
          if (stateGroup.indexOf((this.renderModel as audio.AudioRenderer).state.valueOf()) ===
            -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
            console.error(TAG + 'start failed');
            return;
          }
          // 启动渲染
          (this.renderModel as audio.AudioRenderer).start((err: BusinessError) => {
            if (err) {
              console.error('kevin Renderer start failed.');
            } else {
              console.info('kevin Renderer start success.');
            }
          });
        }
      } else {
        console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
      }
    });
  }

  // 暂停渲染
  pauseAudio() {
    if (this.renderModel !== undefined) {
      // 只有渲染器状态为running的时候才能暂停
      if ((this.renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING) {
        console.info('kevin Renderer is not running');
        return;
      }
      // 暂停渲染
      (this.renderModel as audio.AudioRenderer).pause((err: BusinessError) => {
        if (err) {
          console.error('kevin Renderer pause failed.');
        } else {
          console.info('kevin Renderer pause success.');
        }
      });
    }
  }

  // 停止渲染
  stopPlay() {
    if (this.renderModel !== undefined) {
      // 只有渲染器状态为running或paused的时候才可以停止
      if ((this.renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING &&
        (this.renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_PAUSED) {
        console.info('kevin Renderer is not running or paused.');
        return;
      }
      // 停止渲染
      (this.renderModel as audio.AudioRenderer).stop((err: BusinessError) => {
        if (err) {
          console.error('kevin Renderer stop failed.');
        } else {
          // fileIo.close(file);//todo close
          console.info('kevin Renderer stop success.');
          this.releasePlay()
        }
      });
    }
  }

  // 销毁实例,释放资源
  releasePlay() {
    if (this.renderModel !== undefined) {
      // 渲染器状态不是released状态,才能release
      if (this.renderModel.state.valueOf() === audio.AudioState.STATE_RELEASED) {
        console.info('kevin Renderer already released');
        return;
      }
      // 释放资源
      (this.renderModel as audio.AudioRenderer).release((err: BusinessError) => {
        if (err) {
          console.error('kevin Renderer release failed.');
        } else {
          console.info('kevin Renderer release success.');
        }
      });
    }
  }
}
2 回复
保存的wav 提示不能播放,文件已损坏 AudioCapturer是音频采集器,用于录制PCM(Pulse Code Modulation)音频数据, 得到的pcm 格式不支持预览,可以将文件导出并使用 pcm 转换 wav 或 mp3 工具,以此验证录制是否有问题。

针对HarmonyOS 鸿蒙Next音频录制并保存至用户文件目录后无法播放的问题,这通常是由于音频文件的格式或编码问题导致的。

在HarmonyOS中,使用AudioCapturer录制的音频文件默认是PCM格式,这种格式的原始数据流通常无法被普通音频播放器直接识别或播放。因此,即使文件被成功保存到用户文件目录,也可能因为格式不兼容而无法播放。

要解决这个问题,你可以尝试将PCM格式的音频文件转换为WAV或MP3等更通用的音频格式。这通常需要使用专门的音频转换工具或库来实现。

此外,还需要确保在保存音频文件时,文件的扩展名与其实际格式相匹配。例如,如果文件是PCM格式,但保存时使用了“.mp3”扩展名,这可能会导致播放器在尝试播放时出错。

如果问题依旧没法解决请联系官网客服,官网地址是:https://www.itying.com/category-93-b0.html

回到顶部