Function Introduction:

Record audio and save it in m4a format, then play the audio. Refer to the documentation for developing audio recording functionality using AVRecorder (ArkTS). For more detailed interface information, please refer to the API documentation: @ohos.multimedia.media (Media Service).

Key Knowledge Points:

  1. Familiar with using AVRecorder to record audio and save it locally.
  2. Familiar with using AVPlayer to play local audio files.
  3. Familiar with the dynamic permission application method for sensitive permissions. The sensitive permission required in this project is MICROPHONE.

Usage Environment:

  • API 9
  • DevEco Studio 4.0 Release
  • Windows 11
  • Stage Model
  • ArkTS Language

Required Permissions:

  1. ohos.permission.MICROPHONE

Effect Diagram:

Core Code:

src/main/ets/utils/Permission.ets is a utility for dynamic permission application:

import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';

async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus;

  // Obtain the access token ID of the application
  let tokenId: number;
  try {
    let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (err) {
    console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
  }

  // Verify if the application has been granted the permission
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (err) {
    console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

export async function checkPermissions(permission: Permissions): Promise<boolean> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permission);

  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    return true;
  } else {
    return false;
  }
}

src/main/ets/utils/Recorder.ets is an audio recording utility class for recording and obtaining recording data:

import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import promptAction from '@ohos.promptAction';
import audio from '@ohos.multimedia.audio';

export default class AudioRecorder {
  private audioFile = null;
  private avRecorder: media.AVRecorder | undefined = undefined;
  private avProfile: media.AVRecorderProfile = {
    audioBitrate: 48000, // Audio bitrate
    audioChannels: audio.AudioChannel.CHANNEL_1, // Number of audio channels
    audioCodec: media.CodecMimeType.AUDIO_AAC, // Audio encoding format, currently only AAC is supported
    audioSampleRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // Audio sampling rate
    fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // Container format, currently only M4A is supported
  };
  private avConfig: media.AVRecorderConfig = {
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // Audio input source, set to microphone here
    profile: this.avProfile,
    url: '', // URL of the recording file
  };

  // Register audio recorder callback functions
  setAudioRecorderCallback() {
    if (this.avRecorder != undefined) {
      // Error reporting callback function
      this.avRecorder.on('error', (err) => {
        console.error(`An error occurred in the recorder, error code: ${err.code}, error message: ${err.message}`);
      });
    }
  }

  // Start recording
  async startRecord(audioPath: string) {
    // 1. Create a recording instance
    this.avRecorder = await media.createAVRecorder();
    this.setAudioRecorderCallback();
    // Create and open the recording file
    this.audioFile = fs.openSync(audioPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    // 2. Get the recording file descriptor and assign it to the url in avConfig
    this.avConfig.url = `fd://${this.audioFile.fd}`;
    // 3. Configure recording parameters to complete preparation
    await this.avRecorder.prepare(this.avConfig);
    // 4. Start recording
    await this.avRecorder.start();
    console.info('Recording in progress...');
  }

  // Pause recording
  async pauseRecord() {
    // Only call pause when in the started state for a valid state transition
    if (this.avRecorder != undefined && this.avRecorder.state === 'started') {
      await this.avRecorder.pause();
    }
  }

  // Resume recording
  async resumeRecord() {
    // Only call resume when in the paused state for a valid state transition
    if (this.avRecorder != undefined && this.avRecorder.state === 'paused') {
      await this.avRecorder.resume();
    }
  }

  // Stop recording
  async stopRecord() {
    if (this.avRecorder != undefined) {
      // 1. Stop recording
      // Only call stop when in the started or paused state for a valid state transition
      if (this.avRecorder.state === 'started' || this.avRecorder.state === 'paused') {
        await this.avRecorder.stop();
      }
      // 2. Reset
      await this.avRecorder.reset();
      // 3. Release the recording instance
      await this.avRecorder.release();
      // 4. Close the recording file descriptor
      fs.closeSync(this.audioFile);

      promptAction.showToast({ message: "Recording successful!" });
    }
  }
}

Add the required permissions in src/main/module.json5 under the module section. For field descriptions, add them to the respective string.json files:

    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:record_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      }
    ]

The page code is as follows:

import { checkPermissions } from '../utils/Permission';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';
import AudioRecorder from '../utils/Recorder';
import promptAction from '@ohos.promptAction';
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';


// Permissions requiring dynamic application
const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];
// Obtain the application context
const context = getContext(this) as common.UIAbilityContext;
// Obtain the project's files directory
const filesDir = context.filesDir;
// Create the directory if it does not exist
fs.access(filesDir, (err, res: boolean) => {
  if (!res) {
    fs.mkdirSync(filesDir);
  }
});

// Audio file path
let audioPath = filesDir + "/audio.m4a";

@Entry
@Component
struct Index {
  @State recordBtnText: string = 'Press to Record'
  @State playBtnText: string = 'Play Audio'
  // Recorder
  private audioRecorder?: AudioRecorder;
  // Player
  private avPlayer;
  private playIng: boolean = false

  // When the page is displayed
  async onPageShow() {
    // Check if already authorized
    let promise = checkPermissions(permissions[0]);
    promise.then((result) => {
      if (result) {
        // Initialize the recorder
        if (this.audioRecorder == null) {
          this.audioRecorder = new AudioRecorder();
        }
      } else {
        this.reqPermissionsAndRecord(permissions);
      }
    });
    // Create AVPlayer instance
    this.avPlayer = await media.createAVPlayer();
    // Set up AVPlayer callback
    this.setAVPlayerCallback();
    console.info('Player initialized');
  }

  build() {
    Row() {
      RelativeContainer() {
        // Record button
        Button(this.recordBtnText)
          .id('btn1')
          .width('90%')
          .margin({ bottom: 10 })
          .alignRules({
            bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onTouch((event) => {
            switch (event.type) {
              case TouchType.Down:
                console.info('Button pressed');
                // Check permission
                let promise = checkPermissions(permissions[0]);
                promise.then((result) => {
                  if (result) {
                    // Start recording
                    this.audioRecorder?.startRecord(audioPath);
                    this.recordBtnText = 'Recording...';
                  } else {
                    // Request permission
                    this.reqPermissionsAndRecord(permissions);
                  }
                });
                break;
              case TouchType.Up:
                console.info('Button released');
                if (this.audioRecorder != null) {
                  // Stop recording
                  this.audioRecorder.stopRecord();
                }
                this.recordBtnText = 'Press to Record';
                break;
            }
          })

        // Play button
        Button(this.playBtnText)
          .id('btn2')
          .width('90%')
          .margin({ bottom: 10 })
          .alignRules({
            bottom: { anchor: 'btn1', align: VerticalAlign.Top },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onClick(() => {
            if (!this.playIng) {
              this.playBtnText = 'Playing...';
              // Play audio
              this.playAudio(audioPath);
            } else {
              // Stop playing
              this.stopPlay();
            }
          })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }

  // Play audio
  async playAudio(path: string) {
    this.playIng = true;
    let fdPath = 'fd://';
    let res = fs.accessSync(path);
    if (!res) {
      console.error(`Audio file does not exist: ${path}`);
      this.playIng = false;
      return;
    }
    console.info(`Playing audio file: ${path}`);
    // Open the resource file to get the file descriptor
    let file = await fs.open(path);
    fdPath = fdPath + file.fd;
    // Assign URL to trigger the 'initialized' state change
    this.avPlayer.url = fdPath;
  }

  // Stop playing
  stopPlay() {
    this.avPlayer.reset();
  }

  // Set up AVPlayer callback functions
  setAVPlayerCallback() {
    this.avPlayer.on('error', (err) => {
      this.playIng = false;
      this.playBtnText = 'Play Audio';
      console.error(`Player error, error code: ${err.code}, error message: ${err.message}`);
      // Call reset to release resources and transition to 'idle' state
      this.avPlayer.reset();
    });
    // State change callback function
    this.avPlayer.on('stateChange', async (state) => {
      switch (state) {
        case 'initialized':
          // Resource initialization complete, start preparing the file
          this.avPlayer.prepare();
          break;
        case 'prepared':
          // Resource preparation complete, start playing the file
          this.avPlayer.play();
          break;
        case 'completed':
          // Reset resources to transition back to 'idle' state
          this.avPlayer.reset();
          break;
        case 'idle':
          this.playIng = false;
          this.playBtnText = 'Play Audio';
          break;
      }
    });
  }

  // Request permissions and start recording
  reqPermissionsAndRecord(permissions: Array<Permissions>): void {
    let atManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser will determine the authorization status and prompt the user if needed
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // Permission granted, proceed with target operation
          console.info('Permission granted successfully');
          if (this.audioRecorder == null) {
            this.audioRecorder = new AudioRecorder();
          }
        } else {
          promptAction.showToast({ message: 'Permission denied, recording requires authorization' });
          return;
        }
      }
    }).catch((err) => {
      console.error(`requestPermissionsFromUser failed, code: ${err.code}, message: ${err.message}`);
    });
  }
}
Xiaoye