diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index 8f34c47..79d8f9e 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -19,6 +19,7 @@ export { ScanProvider } from './src/main/ets/providers/ScanProvider'; export { CameraProvider } from './src/main/ets/providers/CameraProvider'; export { LocationProvider } from './src/main/ets/providers/LocationProvider'; export { AudioProvider } from './src/main/ets/providers/AudioProvider'; +export { AmrRecorder } from './src/main/ets/providers/AmrRecorder'; export { NavProvider } from './src/main/ets/providers/NavProvider'; // 微信 SDK 封装(EntryAbility 处理回调 want 需用) diff --git a/feature_capabilities/src/main/ets/providers/AmrRecorder.ets b/feature_capabilities/src/main/ets/providers/AmrRecorder.ets new file mode 100644 index 0000000..24c2496 --- /dev/null +++ b/feature_capabilities/src/main/ets/providers/AmrRecorder.ets @@ -0,0 +1,104 @@ +/** + * AVRecorder 录音封装(设计 §6):AMR-NB(CFT_AMR + AUDIO_AMR_NB / 8kHz / 单声道 / 12.2kbps), + * 对齐 Android RAW_AMR + AMR_NB。文件落 cacheDir/.amr,key=.amr。无 UI。 + */ +import { media } from '@kit.MediaKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { common } from '@kit.AbilityKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { util } from '@kit.ArkTS'; +import { Logger } from 'common'; + +export class AmrRecorder { + private readonly log: Logger = Logger.tag('AmrRecorder'); + private recorder: media.AVRecorder | undefined = undefined; + private fd: number = -1; + private filePath: string = ''; + private fileKey: string = ''; + + isActive(): boolean { + return this.recorder !== undefined; + } + + key(): string { + return this.fileKey; + } + + async start(context: common.Context): Promise { + if (this.recorder !== undefined) { + return false; + } + const uuid: string = util.generateRandomUUID(true); + this.fileKey = `${uuid}.amr`; + this.filePath = `${context.cacheDir}/${this.fileKey}`; + try { + const file: fileIo.File = fileIo.openSync(this.filePath, + fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + this.fd = file.fd; + const recorder: media.AVRecorder = await media.createAVRecorder(); + this.recorder = recorder; + recorder.on('error', (e: BusinessError) => this.log.w(`recorder error: ${e.code} ${e.message}`)); + const profile: media.AVRecorderProfile = { + audioBitrate: 12200, + audioChannels: 1, + audioCodec: media.CodecMimeType.AUDIO_AMR_NB, + audioSampleRate: 8000, + fileFormat: media.ContainerFormatType.CFT_AMR, + }; + const config: media.AVRecorderConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, + profile: profile, + url: `fd://${this.fd}`, + }; + await recorder.prepare(config); + await recorder.start(); + return true; + } catch (e) { + this.log.w(`start failed: ${(e as BusinessError).message}`); + await this.cleanup(true); + return false; + } + } + + async stop(): Promise { + const path: string = this.filePath; + await this.cleanup(false); + return path; + } + + async cancel(): Promise { + await this.cleanup(true); + } + + private async cleanup(deleteFile: boolean): Promise { + const recorder: media.AVRecorder | undefined = this.recorder; + this.recorder = undefined; + if (recorder !== undefined) { + try { + if (recorder.state === 'started' || recorder.state === 'paused') { + await recorder.stop(); + } + await recorder.release(); + } catch (e) { + this.log.w(`cleanup recorder failed: ${(e as BusinessError).message}`); + } + } + if (this.fd >= 0) { + try { + fileIo.closeSync(this.fd); + } catch (e) { + this.log.w(`close fd failed: ${(e as BusinessError).message}`); + } + this.fd = -1; + } + if (deleteFile && this.filePath !== '') { + try { + fileIo.unlinkSync(this.filePath); + } catch (e) { + this.log.w(`unlink failed: ${(e as BusinessError).message}`); + } + this.filePath = ''; + this.fileKey = ''; + } + } +}