• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 开发音频通话功能
2<!--Kit: Audio Kit-->
3<!--Subsystem: Multimedia-->
4<!--Owner: @songshenke-->
5<!--Designer: @caixuejiang; @hao-liangfei; @zhanganxiang-->
6<!--Tester: @Filger-->
7<!--Adviser: @zengyawen-->
8
9在音频通话场景下,音频输出(播放对端声音)和音频输入(录制本端声音)会同时进行,应用可以通过使用AudioRenderer来实现音频输出,通过使用AudioCapturer来实现音频输入,同时使用AudioRenderer和AudioCapturer即可实现音频通话功能。
10
11在音频通话开始和结束时,应用可以自行检查当前的[音频场景模式](audio-call-overview.md#音频场景模式)和[铃声模式](audio-call-overview.md#铃声模式),以便采取合适的音频管理及提示策略。
12
13以下代码示范了同时使用AudioRenderer和AudioCapturer实现音频通话功能的基本过程,其中未包含音频通话数据的传输过程,实际开发中,需要将网络传输来的对端通话数据解码播放,此处仅以读取音频文件的数据代替;同时需要将本端录制的通话数据编码打包,通过网络发送给对端,此处仅以将数据写入音频文件代替。
14
15## 使用AudioRenderer播放对端的通话声音
16
17  该过程与[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)过程相似,关键区别在于audioRendererInfo参数和音频数据来源。audioRendererInfo参数中,音频流使用类型usage需设置为VoIP通话:STREAM_USAGE_VOICE_COMMUNICATION。
18
19```ts
20import { audio } from '@kit.AudioKit';
21import { BusinessError } from '@kit.BasicServicesKit';
22import { fileIo as fs } from '@kit.CoreFileKit';
23import { common } from '@kit.AbilityKit';
24
25// 与使用AudioRenderer开发音频播放功能过程相似,关键区别在于audioRendererInfo参数和音频数据来源。
26const TAG = 'VoIPDemoForAudioRenderer';
27
28class Options {
29  offset?: number;
30  length?: number;
31}
32
33let bufferSize: number = 0;
34let audioRenderer: audio.AudioRenderer | undefined = undefined;
35let audioStreamInfo: audio.AudioStreamInfo = {
36  samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。
37  channels: audio.AudioChannel.CHANNEL_2, // 通道。
38  sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。
39  encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。
40};
41let audioRendererInfo: audio.AudioRendererInfo = {
42  // 需使用通话场景相应的参数。
43  usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, // 音频流使用类型:VoIP通话。
44  rendererFlags: 0 // 音频渲染器标志:默认为0即可。
45};
46let audioRendererOptions: audio.AudioRendererOptions = {
47  streamInfo: audioStreamInfo,
48  rendererInfo: audioRendererInfo
49};
50let file: fs.File;
51let writeDataCallback: audio.AudioRendererWriteDataCallback;
52
53async function initArguments(context: common.UIAbilityContext) {
54  let path = context.cacheDir;
55  // 确保该沙箱路径下存在该资源。
56  let filePath = path + '/StarWars10s-2C-48000-4SW.pcm';
57  file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
58  writeDataCallback = (buffer: ArrayBuffer) => {
59    let options: Options = {
60      offset: bufferSize,
61      length: buffer.byteLength
62    };
63
64    try {
65      let bufferLength = fs.readSync(file.fd, buffer, options);
66      bufferSize += buffer.byteLength;
67      // 如果当前回调传入的数据不足一帧,空白区域需要使用静音数据填充,否则会导致播放出现杂音。
68      if (bufferLength < buffer.byteLength) {
69        let view = new DataView(buffer);
70        for (let i = bufferLength; i < buffer.byteLength; i++) {
71          // 空白区域填充静音数据。当使用音频采样格式为SAMPLE_FORMAT_U8时0x7F为静音数据,使用其他采样格式时0为静音数据。
72          view.setUint8(i, 0);
73        }
74      }
75      // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。
76      // 如果开发者不希望播放某段buffer,返回audio.AudioDataCallbackResult.INVALID即可。
77      return audio.AudioDataCallbackResult.VALID;
78    } catch (error) {
79      console.error('Error reading file:', error);
80      // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。
81      return audio.AudioDataCallbackResult.INVALID;
82    }
83  };
84}
85
86// 初始化,创建实例,设置监听事件。
87async function init() {
88  audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例。
89    if (!err) {
90      console.info(`${TAG}: creating AudioRenderer success`);
91      audioRenderer = renderer;
92      if (audioRenderer !== undefined) {
93        audioRenderer.on('writeData', writeDataCallback);
94      }
95    } else {
96      console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
97    }
98  });
99}
100
101// 开始一次音频渲染。
102async function start() {
103  if (audioRenderer !== undefined) {
104    let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
105    if (stateGroup.indexOf(audioRenderer.state.valueOf()) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染。
106      console.error(TAG + 'start failed');
107      return;
108    }
109    // 启动渲染。
110    audioRenderer.start((err: BusinessError) => {
111      if (err) {
112        console.error('Renderer start failed.');
113      } else {
114        console.info('Renderer start success.');
115      }
116    });
117  }
118}
119
120// 暂停渲染。
121async function pause() {
122  if (audioRenderer !== undefined) {
123    // 只有渲染器状态为running的时候才能暂停。
124    if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING) {
125      console.info('Renderer is not running');
126      return;
127    }
128    // 暂停渲染。
129    audioRenderer.pause((err: BusinessError) => {
130      if (err) {
131        console.error('Renderer pause failed.');
132      } else {
133        console.info('Renderer pause success.');
134      }
135    });
136  }
137}
138
139// 停止渲染。
140async function stop() {
141  if (audioRenderer !== undefined) {
142    // 只有渲染器状态为running或paused的时候才可以停止。
143    if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING && audioRenderer.state.valueOf() !== audio.AudioState.STATE_PAUSED) {
144      console.info('Renderer is not running or paused.');
145      return;
146    }
147    // 停止渲染。
148    audioRenderer.stop((err: BusinessError) => {
149      if (err) {
150        console.error('Renderer stop failed.');
151      } else {
152        fs.close(file);
153        console.info('Renderer stop success.');
154      }
155    });
156  }
157}
158
159// 销毁实例,释放资源。
160async function release() {
161  if (audioRenderer !== undefined) {
162    // 渲染器状态不是released状态,才能release。
163    if (audioRenderer.state.valueOf() === audio.AudioState.STATE_RELEASED) {
164      console.info('Renderer already released');
165      return;
166    }
167    // 释放资源。
168    audioRenderer.release((err: BusinessError) => {
169      if (err) {
170        console.error('Renderer release failed.');
171      } else {
172        console.info('Renderer release success.');
173      }
174    });
175  }
176}
177
178@Entry
179@Component
180struct Index {
181  build() {
182    Scroll() {
183      Column() {
184        Row() {
185          Column() {
186            Text('初始化').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
187          }
188          .backgroundColor(Color.White)
189          .borderRadius(30)
190          .width('45%')
191          .height('25%')
192          .margin({ right: 12, bottom: 12 })
193          .onClick(async () => {
194            let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
195            initArguments(context);
196            init();
197          });
198
199          Column() {
200            Text('开始播放').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
201          }
202          .backgroundColor(Color.White)
203          .borderRadius(30)
204          .width('45%')
205          .height('25%')
206          .margin({ bottom: 12 })
207          .onClick(async () => {
208            start();
209          });
210        }
211
212        Row() {
213          Column() {
214            Text('暂停播放').fontSize(16).margin({ top: 12 });
215          }
216          .id('audio_effect_manager_card')
217          .backgroundColor(Color.White)
218          .borderRadius(30)
219          .width('45%')
220          .height('25%')
221          .margin({ right: 12, bottom: 12 })
222          .onClick(async () => {
223            pause();
224          });
225
226          Column() {
227            Text('停止播放').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
228          }
229          .backgroundColor(Color.White)
230          .borderRadius(30)
231          .width('45%')
232          .height('25%')
233          .margin({ bottom: 12 })
234          .onClick(async () => {
235            stop();
236          });
237        }
238
239        Row() {
240          Column() {
241            Text('释放资源').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
242          }
243          .id('audio_volume_card')
244          .backgroundColor(Color.White)
245          .borderRadius(30)
246          .width('45%')
247          .height('25%')
248          .margin({ right: 12, bottom: 12 })
249          .onClick(async () => {
250            release();
251          });
252        }
253        .padding(12)
254      }
255      .height('100%')
256      .width('100%')
257      .backgroundColor('#F1F3F5');
258    }
259  }
260}
261```
262
263## 使用AudioCapturer录制本端的通话声音
264
265  该过程与[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。audioCapturerInfo参数中音源类型source需设置为语音通话:SOURCE_TYPE_VOICE_COMMUNICATION。
266
267  所有录制均需要申请麦克风权限:ohos.permission.MICROPHONE,申请方式请参考[向用户申请授权](../../security/AccessToken/request-user-authorization.md)。
268
269```ts
270import { audio } from '@kit.AudioKit';
271import { BusinessError } from '@kit.BasicServicesKit';
272import { fileIo as fs } from '@kit.CoreFileKit';
273import { common } from '@kit.AbilityKit';
274
275// 与使用AudioCapturer开发音频录制功能过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。
276const TAG = 'VoIPDemoForAudioCapturer';
277
278class Options {
279  offset?: number;
280  length?: number;
281}
282
283let bufferSize: number = 0;
284let audioCapturer: audio.AudioCapturer | undefined = undefined;
285let audioStreamInfo: audio.AudioStreamInfo = {
286  samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。
287  channels: audio.AudioChannel.CHANNEL_2, // 通道。
288  sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。
289  encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。
290};
291let audioCapturerInfo: audio.AudioCapturerInfo = {
292  // 需使用通话场景相应的参数。
293  source: audio.SourceType.SOURCE_TYPE_VOICE_COMMUNICATION, // 音源类型:语音通话。
294  capturerFlags: 0 // 音频采集器标志:默认为0即可。
295};
296let audioCapturerOptions: audio.AudioCapturerOptions = {
297  streamInfo: audioStreamInfo,
298  capturerInfo: audioCapturerInfo
299};
300let file: fs.File;
301let readDataCallback: Callback<ArrayBuffer>;
302
303async function initArguments(context: common.UIAbilityContext) {
304  let path = context.cacheDir;
305  // 确保该沙箱路径下存在该资源。
306  let filePath = path + '/StarWars10s-2C-48000-4SW.pcm';
307  file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
308  readDataCallback = (buffer: ArrayBuffer) => {
309    let options: Options = {
310      offset: bufferSize,
311      length: buffer.byteLength
312    }
313    fs.writeSync(file.fd, buffer, options);
314    bufferSize += buffer.byteLength;
315  };
316}
317
318// 初始化,创建实例,设置监听事件。
319async function init() {
320  audio.createAudioCapturer(audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例。
321    if (err) {
322      console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`);
323      return;
324    }
325    console.info(`${TAG}: create AudioCapturer success`);
326    audioCapturer = capturer;
327    if (audioCapturer !== undefined) {
328      audioCapturer.on('readData', readDataCallback);
329    }
330  });
331}
332
333// 开始一次音频采集。
334async function start() {
335  if (audioCapturer !== undefined) {
336    let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
337    if (stateGroup.indexOf(audioCapturer.state.valueOf()) === -1) { // 当且仅当状态为STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一时才能启动采集。
338      console.error(`${TAG}: start failed`);
339      return;
340    }
341
342    // 启动采集。
343    audioCapturer.start((err: BusinessError) => {
344      if (err) {
345        console.error('Capturer start failed.');
346      } else {
347        console.info('Capturer start success.');
348      }
349    });
350  }
351}
352
353// 停止采集。
354async function stop() {
355  if (audioCapturer !== undefined) {
356    // 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止。
357    if (audioCapturer.state.valueOf() !== audio.AudioState.STATE_RUNNING && audioCapturer.state.valueOf() !== audio.AudioState.STATE_PAUSED) {
358      console.info('Capturer is not running or paused');
359      return;
360    }
361
362    // 停止采集。
363    audioCapturer.stop((err: BusinessError) => {
364      if (err) {
365        console.error('Capturer stop failed.');
366      } else {
367        fs.close(file);
368        console.info('Capturer stop success.');
369      }
370    });
371  }
372}
373
374// 销毁实例,释放资源。
375async function release() {
376  if (audioCapturer !== undefined) {
377    // 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release。
378    if (audioCapturer.state.valueOf() === audio.AudioState.STATE_RELEASED || audioCapturer.state.valueOf() === audio.AudioState.STATE_NEW) {
379      console.info('Capturer already released');
380      return;
381    }
382
383    // 释放资源。
384    audioCapturer.release((err: BusinessError) => {
385      if (err) {
386        console.error('Capturer release failed.');
387      } else {
388        console.info('Capturer release success.');
389      }
390    });
391  }
392}
393
394@Entry
395@Component
396struct Index {
397  build() {
398    Scroll() {
399      Column() {
400        Row() {
401          Column() {
402            Text('初始化').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
403          }
404          .backgroundColor(Color.White)
405          .borderRadius(30)
406          .width('45%')
407          .height('25%')
408          .margin({ right: 12, bottom: 12 })
409          .onClick(async () => {
410            let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
411            initArguments(context);
412            init();
413          });
414
415          Column() {
416            Text('开始录制').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
417          }
418          .backgroundColor(Color.White)
419          .borderRadius(30)
420          .width('45%')
421          .height('25%')
422          .margin({ bottom: 12 })
423          .onClick(async () => {
424            start();
425          });
426        }
427
428        Row() {
429          Column() {
430            Text('停止录制').fontSize(16).margin({ top: 12 });
431          }
432          .id('audio_effect_manager_card')
433          .backgroundColor(Color.White)
434          .borderRadius(30)
435          .width('45%')
436          .height('25%')
437          .margin({ right: 12, bottom: 12 })
438          .onClick(async () => {
439            stop();
440          });
441
442          Column() {
443            Text('释放资源').fontColor(Color.Black).fontSize(16).margin({ top: 12 });
444          }
445          .backgroundColor(Color.White)
446          .borderRadius(30)
447          .width('45%')
448          .height('25%')
449          .margin({ bottom: 12 })
450          .onClick(async () => {
451            release();
452          });
453        }
454        .padding(12)
455      }
456      .height('100%')
457      .width('100%')
458      .backgroundColor('#F1F3F5');
459    }
460  }
461}
462```
463