• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2* Copyright (C) 2023-2024 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*/
15
16import Logger from '../../../ohosTest/ets/utils/Logger'
17import common from '@ohos.app.ability.common'
18import fs from '@ohos.file.fs'
19import router from '@ohos.router'
20import audio from '@ohos.multimedia.audio'
21import { BusinessError } from '@ohos.base'
22
23const CLOSE_MODE = 0
24const OPEN_MODE = 1
25const TRACKING_MODE = 2
26
27@Entry
28@Component
29struct SpatialAudio {
30  private audioRenderers: audio.AudioRenderer[] = []
31  private audioRendererOptions: audio.AudioRendererOptions[] = [
32    {
33      streamInfo: {
34        samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
35        channels: audio.AudioChannel.CHANNEL_2,
36        sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
37        encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
38        channelLayout: audio.AudioChannelLayout.CH_LAYOUT_STEREO
39      },
40
41      rendererInfo: {
42        usage: audio.StreamUsage.STREAM_USAGE_MOVIE,
43        rendererFlags: 0
44      }
45    },
46    {
47      streamInfo: {
48        samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
49        channels: audio.AudioChannel.CHANNEL_6,
50        sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
51        encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
52        channelLayout: audio.AudioChannelLayout.CH_LAYOUT_5POINT1_BACK
53      },
54      rendererInfo: {
55        usage: audio.StreamUsage.STREAM_USAGE_RINGTONE,
56        rendererFlags: 0
57      }
58    }
59  ]
60
61  private appContext?: common.Context
62  private audioSources = ['/2p0.pcm', '/5p1.pcm']
63  private audioSpatializationManager?: audio.AudioSpatializationManager
64  private audioRoutingManager?: audio.AudioRoutingManager
65  @State supportState: number = 1
66  @State spatializationEnabled: boolean = false
67  @State trackingEnabled: boolean = false
68  @State musicState1: boolean = false
69  @State musicState2: boolean = false
70
71  @Builder Press2PlayDemo1() {
72    Image($r("app.media.ic_pause_spa"))
73      .height(36)
74      .width(36)
75      .margin({right: 16})
76      .id("2P0_play_btn")
77      .onClick(async ()=>{
78        this.musicState1 = !this.musicState1
79        await this.playAudio(0)
80      })
81  }
82
83  @Builder Press2PauseDemo1() {
84    Image($r("app.media.ic_play_spa"))
85      .height(36)
86      .width(36)
87      .margin({right: 16})
88      .id("2P0_pause_btn")
89      .onClick(async ()=>{
90        this.musicState1 = !this.musicState1
91        await this.pauseAudio(0)
92      })
93  }
94
95  @Builder Press2PlayDemo2() {
96    Image($r("app.media.ic_pause_spa"))
97      .height(36)
98      .width(36)
99      .margin({right: 16})
100      .id("5P1_play_btn")
101      .onClick(async ()=>{
102        this.musicState2 = !this.musicState2
103        await this.playAudio(1)
104      })
105  }
106
107  @Builder Press2PauseDemo2() {
108    Image($r("app.media.ic_play_spa"))
109      .height(36)
110      .width(36)
111      .margin({right: 16})
112      .id("5P1_pause_btn")
113      .onClick(async ()=>{
114        this.musicState2 = !this.musicState2
115        await this.pauseAudio(1)
116      })
117
118  }
119
120  @Builder CloseModeOn() {
121    Image($r("app.media.ic_audio_close_on"))
122      .height(48)
123      .width(48)
124      .margin({left: 8})
125      .id("close_mode_on")
126  }
127
128  @Builder CloseModeOff() {
129    Image($r("app.media.ic_audio_close_normal"))
130      .height(48)
131      .width(48)
132      .margin({left: 8})
133      .id("close_mode_off")
134      .onClick(async ()=>{
135        if (this.supportState !== CLOSE_MODE && this.audioSpatializationManager) {
136          try {
137            this.audioSpatializationManager.setSpatializationEnabled(false, () => {
138              this.spatializationEnabled = false
139            })
140            this.audioSpatializationManager.setHeadTrackingEnabled(false, () => {
141              this.trackingEnabled = false
142            })
143          } catch (err) {
144            Logger.error(`Set Spatialization or Head Tracking disabled failed, ${JSON.stringify(err)}`)
145            return
146          }
147        }
148      })
149  }
150
151  @Builder OpenModeDisabled() {
152    Image($r("app.media.ic_audio_open_disable"))
153      .height(48)
154      .width(48)
155  }
156
157  @Builder OpenModeOn() {
158    Image($r("app.media.ic_audio_open_on"))
159      .height(48)
160      .width(48)
161      .id("open_mode_on")
162  }
163
164  @Builder OpenModeOff() {
165    Image($r("app.media.ic_audio_open_normal"))
166      .height(48)
167      .width(48)
168      .id("open_mode_off")
169      .onClick(async ()=>{
170        if (this.audioSpatializationManager) {
171          try {
172            this.audioSpatializationManager.setSpatializationEnabled(true, () => {
173              this.spatializationEnabled = true
174            })
175            this.audioSpatializationManager.setHeadTrackingEnabled(false, () => {
176              this.trackingEnabled = false
177            })
178          } catch (err) {
179            Logger.error(`Set open mode failed, ${JSON.stringify(err)}`)
180            return
181          }
182        }
183
184      })
185  }
186
187  @Builder TrackingModeOn() {
188    Image($r("app.media.ic_audio_track_on"))
189      .height(48)
190      .width(48)
191      .margin({right: 8})
192      .id("tracking_mode_on")
193  }
194
195  @Builder TrackingModeOff() {
196    Image($r("app.media.ic_audio_track_normal"))
197      .height(48)
198      .width(48)
199      .margin({right: 8})
200      .id("tracking_mode_off")
201      .onClick(async ()=>{
202        if (this.audioSpatializationManager) {
203          try {
204            this.audioSpatializationManager.setSpatializationEnabled(true, () => {
205              this.spatializationEnabled = true
206            })
207            this.audioSpatializationManager.setHeadTrackingEnabled(true, () => {
208              this.trackingEnabled = true
209            })
210          } catch (err) {
211            Logger.error(`Set HeadTracking enabled failed, ${JSON.stringify(err)}`)
212            return
213          }
214        }
215      })
216  }
217  @Builder TrackingModeDisabled() {
218    Image($r("app.media.ic_audio_track_disable"))
219      .height(48)
220      .width(48)
221      .margin({right: 8})
222      .fillColor("#182431")
223  }
224  @Builder UIForClose() {
225    this.CloseModeOn()
226    this.OpenModeDisabled()
227    this.TrackingModeDisabled()
228  }
229
230  @Builder UIForOpen() {
231    if (this.spatializationEnabled === true) {
232      this.CloseModeOff()
233      this.OpenModeOn()
234    } else {
235      this.CloseModeOn()
236      this.OpenModeOff()
237    }
238    this.TrackingModeDisabled()
239  }
240
241  @Builder UIForTracking() {
242    if (this.spatializationEnabled === true && this.trackingEnabled === true) {
243      this.CloseModeOff()
244      this.OpenModeOff()
245      this.TrackingModeOn()
246    } else if (this.spatializationEnabled === true && this.trackingEnabled === false) {
247      this.CloseModeOff()
248      this.OpenModeOn()
249      this.TrackingModeOff()
250    } else {
251      this.CloseModeOn()
252      this.OpenModeOff()
253      this.TrackingModeOff()
254    }
255  }
256
257  @Builder OpenTextEnable() {
258    Text($r("app.string.OPEN_TEXT"))
259      .height("100%")
260      .width(64)
261      .fontSize(12)
262      .fontWeight(500)
263      .margin({right:68})
264      .fontColor("#182431")
265      .textAlign(TextAlign.Center)
266  }
267
268  @Builder OpenTextDisable() {
269    Text($r("app.string.OPEN_TEXT"))
270      .height("100%")
271      .width(64)
272      .fontSize(12)
273      .fontWeight(500)
274      .margin({right:68})
275      .fontColor(Color.Grey)
276      .textAlign(TextAlign.Center)
277  }
278
279  @Builder TrackingTextEnable() {
280    Text($r("app.string.TRACKING_TEXT"))
281      .height(16)
282      .width(48)
283      .fontSize(12)
284      .fontWeight(500)
285      .fontColor("#182431")
286      .textAlign(TextAlign.Center)
287  }
288
289  @Builder TrackingTextDisable() {
290    Text($r("app.string.TRACKING_TEXT"))
291      .height(16)
292      .width(48)
293      .fontSize(12)
294      .fontWeight(500)
295      .fontColor(Color.Grey)
296      .textAlign(TextAlign.Center)
297  }
298
299  @Builder DIYTitle() {
300    Row(){
301      Text($r('app.string.SPATIAL_AUDIO'))
302        .fontWeight(700)
303        .fontSize(20)
304    }
305    .height("100%")
306    .justifyContent(FlexAlign.Center)
307
308  }
309
310  updateSupportStateUI(): void {
311    this.supportState = this.supportStateQuery()
312    if (this.audioSpatializationManager) {
313      try {
314        this.spatializationEnabled = this.audioSpatializationManager.isSpatializationEnabled()
315        this.trackingEnabled = this.audioSpatializationManager.isHeadTrackingEnabled()
316      } catch (err) {
317        Logger.error(`update Support State UI failed ,Error: ${JSON.stringify(err)}`)
318        return
319      }
320    }
321  }
322
323  supportStateQuery(): number {
324    let audioDeviceDescriptors: audio.AudioDeviceDescriptors = this.audioRenderers[0].getCurrentOutputDevicesSync()
325    audioDeviceDescriptors.forEach(audioDeviceDescriptor => {
326      Logger.info("Device Role:" + audioDeviceDescriptor.deviceRole + ", device Type:" +
327      audioDeviceDescriptor.deviceType + ", Macaddress:" + audioDeviceDescriptor.address)
328    })
329
330    let isSpaSupported: boolean = false
331    let isSpaSupportedForDevice: boolean = false
332    let isTraSupported: boolean =  false
333    let isTraSupportedForDevice: boolean = false
334    if (this.audioSpatializationManager) {
335      try {
336        isSpaSupported =  this.audioSpatializationManager.isSpatializationSupported()
337        isSpaSupportedForDevice =
338          this.audioSpatializationManager.isSpatializationSupportedForDevice(audioDeviceDescriptors[0])
339        isTraSupported =  this.audioSpatializationManager.isHeadTrackingSupported()
340        isTraSupportedForDevice =
341          this.audioSpatializationManager.isHeadTrackingSupportedForDevice(audioDeviceDescriptors[0])
342      } catch (err) {
343        Logger.error(`supportStateQuery ,Error: ${JSON.stringify(err)}`)
344      }
345
346    } else {
347      Logger.info("Get manager failed.")
348    }
349    let isSpatializationSupportedBoth: boolean = isSpaSupported && isSpaSupportedForDevice
350    let isHeadTrackingSupportedBoth: boolean = isTraSupported && isTraSupportedForDevice
351    if (isSpatializationSupportedBoth && isHeadTrackingSupportedBoth) {
352      return TRACKING_MODE
353    } else if (isSpatializationSupportedBoth) {
354      return OPEN_MODE
355    } else {
356      return CLOSE_MODE
357    }
358  }
359
360  async init(): Promise<void> {
361    if (this.appContext) {
362      return
363    }
364    this.appContext = getContext(this)
365
366    for (let index = 0; index < 2; index++) {
367      try {
368        let renderer = await audio.createAudioRenderer(this.audioRendererOptions[index])
369        Logger.info("Create renderer success")
370        this.audioRenderers.push(renderer)
371      } catch (err) {
372        Logger.error(`audioRenderer_${index} create ,Error: ${JSON.stringify(err)}`)
373        return
374      }
375    }
376
377    let audioManager = audio.getAudioManager()
378    try {
379      this.audioSpatializationManager = audioManager.getSpatializationManager()
380    } catch (err) {
381      Logger.error(`Get Spatialization Manager failed, Error: ${JSON.stringify(err)}`)
382      return
383    }
384
385    this.audioRoutingManager = audioManager.getRoutingManager()
386    this.updateSupportStateUI()
387    if (this.audioRoutingManager) {
388      this.audioRoutingManager.on("deviceChange", audio.DeviceFlag.OUTPUT_DEVICES_FLAG, () => {
389        Logger.info("Output device changed")
390        this.updateSupportStateUI()
391      })
392    }
393  }
394
395  async over(): Promise<void> {
396    this.appContext = undefined
397    this.audioRenderers.forEach(async audioRenderer => {
398      await audioRenderer.stop()
399      await audioRenderer.release()
400    })
401    this.audioRenderers = []
402
403    if (this.audioRoutingManager) {
404      this.audioRoutingManager.off("deviceChange")
405    }
406  }
407
408  async playAudio(index: number): Promise<void> {
409    if (this.audioRenderers[index] === null) {
410      return
411    }
412
413    if (this.audioRenderers[index].state === audio.AudioState.STATE_PAUSED) {
414      await this.audioRenderers[index].start()
415    } else {
416      let bufferSize: number = 0
417      try {
418        bufferSize = await this.audioRenderers[index].getBufferSize()
419        await this.audioRenderers[index].start()
420      } catch (err) {
421        let error = err as BusinessError
422        Logger.error(`AudioRenderer start : Error: ${JSON.stringify(error)}`)
423        return
424      }
425
426      let buf = new ArrayBuffer(bufferSize)
427      let filePath:string = ""
428      if (this.appContext) {
429        filePath = this.appContext.filesDir + this.audioSources[index]
430      }
431      let stat = await fs.stat(filePath)
432      let len = stat.size %  bufferSize == 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1)
433      let file = await fs.open(filePath, 0o0)
434      while (true) {
435        if (!this.audioRenderers[index]) {
436          break
437        }
438        if (this.audioRenderers[index].state === audio.AudioState.STATE_RELEASED) {
439          break
440        }
441        Logger.info("start write")
442        for (let i = 0; i < len; i++) {
443          class options {
444            offset: number = 0
445            length: number = 0
446          }
447          let readOptions: options = {
448            offset: i * bufferSize,
449            length: bufferSize
450          }
451          await fs.read(file.fd, buf, readOptions)
452          await this.audioRenderers[index].write(buf)
453        }
454      }
455    }
456  }
457
458  async pauseAudio(index: number): Promise<void> {
459    try {
460      if (this.audioRenderers[index]) {
461        await this.audioRenderers[index].pause()
462      }
463    } catch (err) {
464      Logger.error(`Pause Error: ${JSON.stringify(err)}`)
465      return
466    }
467  }
468
469  async aboutToAppear(): Promise<void> {
470    await this.init()
471  }
472
473  aboutToDisappear(): void {
474    this.over()
475  }
476
477  onPageShow(): void {
478    if (this.audioSpatializationManager === undefined){
479      return
480    }
481    this.updateSupportStateUI()
482    Logger.info("Page show")
483  }
484
485  onPageHide(): void {
486    Logger.info("Page Hide")
487    if (this.audioRenderers[0] && this.audioRenderers[0].state === audio.AudioState.STATE_RUNNING) {
488      this.audioRenderers[0].pause()
489      this.musicState1 = false
490    }
491    if (this.audioRenderers[1] && this.audioRenderers[1].state === audio.AudioState.STATE_RUNNING) {
492      this.audioRenderers[1].pause()
493      this.musicState2 = false
494    }
495  }
496
497  build() {
498    Column() {
499      Column() {
500        Row() {
501          Navigation() {
502          }
503          .hideBackButton(false)
504          .titleMode(NavigationTitleMode.Mini)
505          .title(this.DIYTitle())
506          .height("100%")
507          .mode(NavigationMode.Stack)
508          .backgroundColor('#F1F3F5')
509        }
510        .height(56)
511        .width('100%')
512        .id('spatial_audio_back_btn')
513        .onClick(async () => {
514          await router.replaceUrl({ url: 'pages/Index' })
515        })
516
517        Row() {
518          Row() {
519            Text($r("app.string.2P0_MUSIC"))
520              .fontSize(20)
521              .fontWeight(500)
522              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
523              .margin({top: 21, bottom: 21, left: 16.5})
524            if (!this.musicState1) {
525              this.Press2PlayDemo1()
526            } else {
527              this.Press2PauseDemo1()
528            }
529          }
530          .height("100%")
531          .width(336)
532          .backgroundColor('#FFFFFF')
533          .justifyContent(FlexAlign.SpaceBetween)
534          .borderRadius(24)
535        }
536        .justifyContent(FlexAlign.SpaceAround)
537        .height(68)
538        .width('100%')
539        .margin({ top: 8 })
540
541        Row() {
542          Row() {
543            Text($r("app.string.5P1_MUSIC"))
544              .fontSize(20)
545              .fontWeight(500)
546              .fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
547              .margin({top: 21, bottom: 21, left: 16.5})
548
549            if (!this.musicState2) {
550              this.Press2PlayDemo2()
551            } else {
552              this.Press2PauseDemo2()
553            }
554          }
555          .height("100%")
556          .width(336)
557          .backgroundColor('#FFFFFF')
558          .justifyContent(FlexAlign.SpaceBetween)
559          .borderRadius(24)
560        }
561        .justifyContent(FlexAlign.SpaceAround)
562        .height(68)
563        .width('100%')
564        .margin({ top: 12 })
565      }
566      .height(212)
567      .width('100%')
568
569      Column() {
570        Row() {
571          if (this.supportState === 2) {
572            this.UIForTracking()
573          } else if (this.supportState === 1) {
574            this.UIForOpen()
575          } else {
576            this.UIForClose()
577          }
578        }
579        .height(64)
580        .width(336)
581        .borderRadius(100)
582        .backgroundColor('#FFFFFF')
583        .justifyContent(FlexAlign.SpaceBetween)
584
585        Row() {
586          Text($r("app.string.CLOSE_TEXT"))
587            .height("100%")
588            .width(64)
589            .fontSize(12)
590            .fontWeight(500)
591            .margin({right:60})
592            .fontColor(Color.Black)
593            .textAlign(TextAlign.Center)
594
595          if (this.supportState === 0) {
596            this.OpenTextDisable()
597            this.TrackingTextDisable()
598          } else if (this.supportState === 1) {
599            this.OpenTextEnable()
600            this.TrackingTextDisable()
601          } else {
602            this.OpenTextEnable()
603            this.TrackingTextEnable()
604          }
605        }
606        .height(16)
607        .width(304)
608        .margin({left: 24, right: 32, top: 8})
609      }
610      .height(112)
611      .width('100%')
612    }
613    .height('100%')
614    .width('100%')
615    .backgroundColor('#f1f3f5')
616    .justifyContent(FlexAlign.SpaceBetween)
617  }
618}