• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2* Copyright (C) 2023 Huawei Device Co., Ltd.
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7* http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15import audio from '@ohos.multimedia.audio';
16import fs from '@ohos.file.fs';
17import router from '@ohos.router';
18import { BusinessError } from '@ohos.base';
19
20const MIN_RECORD_SECOND = 5;
21const TOTAL_SECOND = 30;
22const RANDOM_NUM = 10000;
23const INTERVAL_TIME = 1000;
24const READ_TIME_OUT = 0;
25class Options {
26  offset: number = 0;
27  length: number = 0;
28}
29
30@Entry
31@Component
32struct NormalCapturer {
33  @State fontColor: string = '#182431';
34  @State selectedFontColor: string = '#007DFF';
35  @State currentIndex: number = 0;
36  private audioCapturer?: audio.AudioCapturer;
37  private audioRenderer?: audio.AudioRenderer;
38  @State recordState: string = 'init'; // [init,started,continued,paused,stoped];
39  private audioCapturerOptions: audio.AudioCapturerOptions = {
40    streamInfo: {
41      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
42      channels: audio.AudioChannel.CHANNEL_2,
43      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
44      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
45    },
46    capturerInfo: {
47      source: audio.SourceType.SOURCE_TYPE_MIC,
48      capturerFlags: 0
49    }
50  };
51  private audioRendererOptions: audio.AudioRendererOptions = {
52    streamInfo: {
53      samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
54      channels: audio.AudioChannel.CHANNEL_2,
55      sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
56      encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
57    },
58    rendererInfo: {
59      content: audio.ContentType.CONTENT_TYPE_MUSIC,
60      usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
61      rendererFlags: 0
62    }
63  };
64  @State title: string = '';
65  @State date: string = '';
66  @State playSec: number = 0;
67  @State renderState: number = 0;
68  @State recordSec: number = 0;
69  @State showTime: string = '00:00:00';
70  private interval: number = 0;
71  private bufferSize = 0;
72  private path = ``;
73  private fd = 0;
74  @State isRecordOver: boolean = false;
75  @State start: number = 0;
76  @State capturerOffsetStart: number = 0;
77
78  @Builder TabBuilder(index: number, btnId: string) {
79    Column() {
80      Text(index === 0 ? $r('app.string.NORMAL_CAPTURER') : $r('app.string.PARALLEL_CAPTURER'))
81        .fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
82        .opacity(this.currentIndex === index ? 1 : 0.6)
83        .fontSize(16)
84        .fontWeight(this.currentIndex === index ? 500 : 400)
85        .lineHeight(22)
86        .margin({ top: 17, bottom: 7 });
87      Divider()
88        .strokeWidth(2)
89        .color('#007DFF')
90        .opacity(this.currentIndex === index ? 1 : 0);
91    }.width(78).id('btn_' + btnId);
92  }
93
94  async aboutToAppear(): Promise<void> {
95    console.log('NomalCapturer aboutToAppear');
96    await this.initResource();
97  }
98
99  async initResource(): Promise<void> {
100    try {
101      this.audioCapturer = await audio.createAudioCapturer(this.audioCapturerOptions);
102      this.bufferSize = await this.audioCapturer.getBufferSize();
103      this.recordState = 'init';
104      this.title = `${this.getDate(2)}_${Math.floor(Math.random() * RANDOM_NUM)}`;
105      this.path = `/data/storage/el2/base/haps/entry/files/normal_capturer_${this.title}.pcm`;
106      this.date = this.getDate(1);
107      await this.openFile(this.path);
108    } catch (err) {
109      let error = err as BusinessError;
110      console.log(`NormalCapturer:createAudioCapturer err=${JSON.stringify(error)}`);
111    }
112  }
113
114  async releseResource(): Promise<void> {
115    if (this.fd > 0) {
116      this.closeFile();
117      this.fd = 0;
118    }
119    if (this.interval) {
120      clearInterval(this.interval);
121    }
122    if (this.audioCapturer) {
123      console.log('NomalCapturer,audioCapturer released');
124      await this.audioCapturer.release();
125      this.audioCapturer = undefined;
126      this.recordState = 'init';
127      clearInterval(this.interval);
128    }
129    if (this.audioRenderer) {
130      console.log('NomalCapturer,audioRenderer released');
131      await this.audioRenderer.release();
132      this.audioRenderer = undefined;
133    }
134  }
135
136  async aboutToDisappear(): Promise<void> {
137    console.log('NomalCapturer,aboutToDisappear is called');
138    await this.releseResource();
139  }
140
141  async openFile(path: string): Promise<void> {
142    console.log(path);
143    try {
144      await fs.open(path, 0o100);
145      console.log('file created success');
146    } catch (err) {
147      let error = err as BusinessError;
148      console.log('file created err:' + JSON.stringify(error));
149      return;
150    }
151
152    try {
153      let file = await fs.open(path, 0o2);
154      this.fd = file.fd;
155      console.log(`file open success for read and write mode,fd=${file.fd}`);
156    } catch (err) {
157      let error = err as BusinessError;
158      console.log('file open err:' + JSON.stringify(error));
159      return;
160    }
161  }
162
163  async closeFile(): Promise<void> {
164    try {
165      await fs.close(this.fd);
166      console.log('file close success');
167    } catch (err) {
168      let error = err as BusinessError;
169      console.log('file close err:' + JSON.stringify(error));
170      return;
171    }
172  }
173
174  async capturerStart(): Promise<void> {
175    if (!this.audioCapturer) {
176      console.log(`NormalCapturer,capturerStart:audioCapturer is null`);
177      return;
178    }
179
180    try {
181      await this.audioCapturer.start();
182      // when start,init recordSec
183      this.recordSec = 0;
184      this.recordState = 'started';
185      console.log('audioCapturer start ok');
186      clearInterval(this.interval);
187      this.interval = setInterval(async () => {
188        if (this.recordSec >= TOTAL_SECOND) {
189          // over TOTAL_SECOND,need to stop auto
190          clearInterval(this.interval);
191          if (this.audioCapturer && this.audioCapturer.state === audio.AudioState.STATE_RUNNING) {
192            await this.capturerStop();
193          }
194          return;
195        }
196        this.recordSec++;
197        this.showTime = this.getTimesBySecond(this.recordSec);
198
199      }, INTERVAL_TIME);
200      setTimeout(async () => {
201        await this.readCapturer();
202      }, READ_TIME_OUT);
203    } catch (err) {
204      let error = err as BusinessError;
205      console.log(`NormalCapturer:audioCapturer start err=${JSON.stringify(error)}`);
206    }
207  }
208
209  async renderCreate(): Promise<void> {
210    try {
211      this.audioRenderer = await audio.createAudioRenderer(this.audioRendererOptions);
212      this.renderState = this.audioRenderer.state;
213      this.audioRenderer.on('stateChange', (state) => {
214        this.renderState = state;
215      });
216    } catch (err) {
217      let error = err as BusinessError;
218      console.log(`createAudioRenderer err=${JSON.stringify(error)}`);
219    }
220  }
221
222  async renderStart(): Promise<void> {
223    if (!this.audioRenderer) {
224      return;
225    }
226    let bufferSize = 0;
227    try {
228      bufferSize = await this.audioRenderer.getBufferSize();
229      await this.audioRenderer.start();
230    } catch (err) {
231      let error = err as BusinessError;
232      console.log(`start err:${JSON.stringify(error)}`);
233    }
234
235    try {
236      let stat = await fs.stat(this.path);
237      let buf = new ArrayBuffer(bufferSize);
238      console.log(`audioRenderer write start..........`);
239      let startOffset = this.start;
240      while (startOffset <= stat.size) {
241        if (this.audioRenderer.state === audio.AudioState.STATE_PAUSED) {
242          break;
243        }
244        // change tag,to stop
245        if (this.audioRenderer.state === audio.AudioState.STATE_STOPPED) {
246          break;
247        }
248        if (this.audioRenderer.state === audio.AudioState.STATE_RELEASED) {
249          return;
250        }
251        let options: Options = {
252          offset: startOffset,
253          length: bufferSize
254        };
255        console.log('renderStart,options=' + JSON.stringify(options));
256
257        await fs.read(this.fd, buf, options);
258        await this.audioRenderer.write(buf);
259        this.playSec = Math.round(startOffset / stat.size * this.recordSec);
260        startOffset = startOffset + bufferSize;
261        this.start = startOffset;
262      }
263      console.log(`audioRenderer write end..........`)
264      if (this.audioRenderer.state === audio.AudioState.STATE_RUNNING) {
265        this.start = 0;
266        await this.renderStop();
267      }
268    } catch (err) {
269      let error = err as BusinessError;
270      console.log(`write err:${JSON.stringify(error)}`);
271    }
272  }
273
274  async renderPause(): Promise<void> {
275    if (!this.audioRenderer) {
276      return;
277    }
278    try {
279      await this.audioRenderer.pause();
280    } catch (err) {
281      let error = err as BusinessError;
282      console.log(`pause err:${JSON.stringify(error)}`);
283    }
284  }
285
286  async renderStop(): Promise<void> {
287    if (!this.audioRenderer) {
288      return;
289    }
290    try {
291      await this.audioRenderer.stop();
292      this.start = 0;
293    } catch (err) {
294      let error = err as BusinessError;
295      console.log(`stop err:${JSON.stringify(error)}`);
296    }
297  }
298
299  async releaseStop(): Promise<void> {
300    if (!this.audioRenderer) {
301      return;
302    }
303    try {
304      await this.audioRenderer.release();
305    } catch (err) {
306      let error = err as BusinessError;
307      console.log(`release err:${JSON.stringify(error)}`);
308    }
309  }
310
311  async capturerContinue(): Promise<void> {
312    if (!this.audioCapturer) {
313      console.log(`NormalCapturer,capturerContinue:audioCapturer is null`);
314      return;
315    }
316
317    try {
318      await this.audioCapturer.start()
319      this.recordState = 'continued';
320      console.log('audioCapturer start ok');
321      this.interval = setInterval(async () => {
322        if (this.recordSec >= TOTAL_SECOND) {
323          // over TOTAL_SECOND,need to stop auto
324          clearInterval(this.interval);
325          if (this.audioCapturer && this.audioCapturer.state === audio.AudioState.STATE_RUNNING) {
326            await this.capturerStop();
327          }
328          return;
329        }
330        this.recordSec++;
331        this.showTime = this.getTimesBySecond(this.recordSec);
332      }, INTERVAL_TIME);
333      setTimeout(async () => {
334        await this.readCapturer();
335      }, READ_TIME_OUT);
336    } catch (err) {
337      let error = err as BusinessError;
338      console.log(`NormalCapturer:audioCapturer start err=${JSON.stringify(error)}`);
339    }
340  }
341
342  async capturerStop(): Promise<void> {
343    if (!this.audioCapturer) {
344      console.log(`NormalCapturer,capturerStop:audioCapturer is null`);
345      return;
346    }
347    if (this.recordSec < MIN_RECORD_SECOND) {
348      return;
349    }
350
351    try {
352      await this.audioCapturer.stop();
353      // when recordState is started or continued
354      this.recordState = 'stopped';
355      clearInterval(this.interval);
356    } catch (err) {
357      let error = err as BusinessError;
358      // when recordState is paused
359      this.recordState = 'stopped';
360      console.log(`NormalCapturer:audioCapturer stop err=${JSON.stringify(error)}`);
361    }
362    this.isRecordOver = true;
363    await this.renderCreate();
364  }
365
366  async capturerPause(): Promise<void> {
367    if (!this.audioCapturer) {
368      console.log(`NormalCapturer,capturerPause:audioCapturer is null`);
369      return;
370    }
371
372    try {
373      await this.audioCapturer.stop();
374      this.recordState = 'paused';
375
376      clearInterval(this.interval);
377    } catch (err) {
378      let error = err as BusinessError;
379      console.log(`NormalCapturer:audioCapturer stop err=${JSON.stringify(error)}`);
380      return;
381    }
382  }
383
384  async readCapturer(): Promise<void> {
385    console.log('NormalCapturer:readCapturer enter');
386    if (!this.audioCapturer) {
387      console.log(`NormalCapturer,readCapturer:audioCapturer is null`);
388      return;
389    }
390
391    try {
392      let startOffset = this.capturerOffsetStart;
393      while (true) {
394        if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) {
395          console.log('state is changed to be stopped');
396          break;
397        }
398        let buffer = await this.audioCapturer.read(this.bufferSize, true);
399        console.log('NormalCapturer:readCapturer read success');
400        let options: Options = {
401          offset: startOffset,
402          length: this.bufferSize
403        };
404        let writen = await fs.write(this.fd, buffer, options);
405        console.log(`NormalCapturer:readCapturer,startOffset=${startOffset},writen=${writen}`);
406        startOffset += this.bufferSize;
407        this.capturerOffsetStart = startOffset;
408      }
409    } catch (err) {
410      let error = err as BusinessError;
411      console.log(`readCapturer err=${JSON.stringify(error)}`);
412    }
413  }
414
415  formatNumber(num: number): string {
416    if (num <= 9) {
417      return '0' + num;
418    } else {
419      return '' + num;
420    }
421  }
422
423  getDate(mode: number): string {
424    let date = new Date();
425    if (mode === 1) {
426      return `${date.getFullYear()}/${this.formatNumber(date.getMonth() + 1)}/${this.formatNumber(date.getDate())}`;
427    } else {
428      return `${date.getFullYear()}${this.formatNumber(date.getMonth() + 1)}${this.formatNumber(date.getDate())}`;
429    }
430  }
431
432  getTimesBySecond(t: number): string {
433    let h = Math.floor(t / 60 / 60 % 24);
434    let m = Math.floor(t / 60 % 60);
435    let s = Math.floor(t % 60);
436    let hs = h < 10 ? '0' + h : h;
437    let ms = m < 10 ? '0' + m : m;
438    let ss = s < 10 ? '0' + s : s;
439    return `${hs}:${ms}:${ss}`;
440  }
441
442  @Builder InitRecord() {
443    Column() {
444      Image($r('app.media.ic_record')).width(56).height(56);
445    }
446    .width('100%')
447    .height(56)
448    .position({ y: 60 })
449    .id('normal_start_record_btn')
450    .onClick(() => {
451      this.capturerStart();
452    });
453  }
454
455  @Builder StartedRecord() {
456    Column() {
457      Text(this.showTime).fontSize(21).fontWeight(500).margin({ bottom: 8 })
458    }.width('100%').height(66).position({ y: 30 }).id('normal_show_time_txt');
459
460
461    Column() {
462      Image($r('app.media.ic_recording')).width(56).height(56);
463    }
464    .width('100%')
465    .height(56)
466    .position({ y: 60 })
467    .id('normal_stop_record_btn')
468    .onClick(() => {
469      this.capturerStop();
470    });
471
472    Column() {
473      Image($r('app.media.ic_record_pause')).width(24).height(24);
474      Text($r('app.string.PAUSE')).fontSize(12).fontWeight(400).id('normal_pause_record_btn').margin({ top: 2 });
475    }
476    .height(56)
477    .width(56)
478    .position({ x: '80%', y: 60 })
479    .alignItems(HorizontalAlign.Center)
480    .justifyContent(FlexAlign.Center)
481    .onClick(() => {
482      this.capturerPause();
483    });
484  }
485
486  @Builder PausedRecord() {
487    Column() {
488      Text(this.showTime).fontSize(21).fontWeight(500).margin({ bottom: 8 });
489    }.width('100%').height(66).position({ y: 30 });
490
491    Column() {
492      Image($r('app.media.ic_recording')).width(56).height(56);
493    }.width('100%').height(56).position({ y: 60 })
494    .onClick(() => {
495      this.capturerStop();
496    });
497
498    Column() {
499      Image($r('app.media.ic_record_continue')).width(24).height(24);
500      Text($r('app.string.CONTINUE')).fontSize(12).fontWeight(400).margin({ top: 2 });
501    }
502    .height(56)
503    .width(56)
504    .position({ x: '80%', y: 60 })
505    .alignItems(HorizontalAlign.Center)
506    .justifyContent(FlexAlign.Center)
507    .id('normal_continue_record_btn')
508    .onClick(() => {
509      this.capturerContinue();
510    });
511  }
512
513  @Builder FinishedRecord() {
514    Column() {
515      Image($r('app.media.ic_record')).width(56).height(56);
516    }
517    .width('100%')
518    .height(56)
519    .position({ y: 60 })
520    .opacity(0.4)
521    .id('disalbe_btn');
522  }
523
524  build() {
525    Column() {
526      Column() {
527        Navigation() {
528        }
529        .width('100%')
530        .height('100%')
531        .hideBackButton(false)
532        .titleMode(NavigationTitleMode.Mini)
533        .title($r('app.string.AUDIO_CAPTURER'))
534        .mode(NavigationMode.Stack)
535        .backgroundColor('#F1F3F5');
536      }
537      .id('normal_capturer_back_btn')
538      .width('100%')
539      .height(56)
540      .onClick(async () => {
541        await router.replaceUrl({ url: 'pages/Index' });
542      });
543
544      Column() {
545        Tabs({ barPosition: BarPosition.Start, index: 0 }) {
546          TabContent() {
547          }.tabBar(this.TabBuilder(0, 'normal_capturer'));
548
549          TabContent() {
550          }.tabBar(this.TabBuilder(1, 'parallel_capturer'));
551        }
552        .vertical(false)
553        .barMode(BarMode.Fixed)
554        .barWidth(360)
555        .barHeight(56)
556        .animationDuration(400)
557        .onChange((index: number) => {
558          this.currentIndex = index;
559          if (this.currentIndex === 1) {
560            router.replaceUrl({ url: 'pages/ParallelCapturer' });
561          }
562        })
563        .width('100%')
564        .height(56)
565      }.padding({ left: 12, right: 12 });
566
567
568      if (this.isRecordOver === true) {
569        Column() {
570          Row() {
571            Text(this.title)
572              .fontSize(16)
573              .fontWeight(500)
574              .fontColor('#182431')
575              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'));
576            if (this.renderState === audio.AudioState.STATE_RUNNING) {
577              Image($r('app.media.ic_record_playing')).width(24).height(24).id('playing_state');
578            } else {
579              Image($r('app.media.ic_record_paused')).width(24).height(24).id('paused_state');
580            }
581
582          }.width('100%').height(24).justifyContent(FlexAlign.SpaceBetween).margin({ top: 16 });
583
584          Row() {
585            Text(this.date)
586              .fontSize(16)
587              .fontWeight(400)
588              .fontColor('#182431')
589              .opacity(0.6)
590              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'));
591            Text(this.getTimesBySecond(this.recordSec) + '')
592              .fontSize(16)
593              .fontWeight(400)
594              .fontColor('#182431')
595              .opacity(0.6)
596              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'));
597          }.width('100%').height(24).justifyContent(FlexAlign.SpaceBetween).margin({ top: 4 });
598
599          Row() {
600            Progress({ value: this.playSec, total: this.recordSec, type: ProgressType.Linear })
601              .color('#007DFF')
602              .value(this.playSec)
603              .width('100%')
604              .height(4)
605          }.margin({ top: 23, bottom: 3 });
606
607          Row() {
608            Text(this.getTimesBySecond(this.playSec) + '')
609              .fontSize(12)
610              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
611              .fontColor('#182431')
612              .opacity(0.6)
613              .fontWeight(400);
614            Text(this.getTimesBySecond(this.recordSec) + '')
615              .fontSize(12)
616              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
617              .fontColor('#182431')
618              .opacity(0.6)
619              .fontWeight(400);
620          }.justifyContent(FlexAlign.SpaceBetween).width('100%');
621        }
622        .width('100%')
623        .height(126)
624        .backgroundColor(Color.White)
625        .borderRadius(24)
626        .margin({ top: 12 })
627        .padding({ left: 12, right: 12 })
628        .id('normal_player')
629        .onClick(() => {
630          if (this.renderState === audio.AudioState.STATE_PREPARED) {
631            this.renderStart();
632          }
633          if (this.renderState === audio.AudioState.STATE_RUNNING) {
634            this.renderPause();
635          }
636          if (this.renderState === audio.AudioState.STATE_PAUSED) {
637            this.renderStart();
638          }
639          if (this.renderState === audio.AudioState.STATE_STOPPED) {
640            this.renderStart();
641          }
642        });
643      }
644      Row() {
645        if (this.recordState === 'init') {
646          this.InitRecord();
647        } else if (this.recordState === 'started') {
648          this.StartedRecord();
649        } else if (this.recordState === 'paused') {
650          this.PausedRecord();
651        } else if (this.recordState === 'continued') {
652          this.StartedRecord();
653        } else if (this.recordState === 'stopped') {
654          this.FinishedRecord();
655        }
656      }
657      .width('100%')
658      .alignItems(VerticalAlign.Center)
659      .height(116)
660      .position({ y: '82%' });
661    }
662    .width('100%')
663    .height('100%')
664    .justifyContent(FlexAlign.Start)
665    .backgroundColor('#F1F3F5')
666    .padding({ left: 12, right: 12 });
667  }
668}
669