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}