IT评测·应用市场-qidao123.com技术社区
标题:
【鸿蒙实战开发】HarmonyOS-AudioVivid本领详解
[打印本页]
作者:
北冰洋以北
时间:
2024-12-11 03:18
标题:
【鸿蒙实战开发】HarmonyOS-AudioVivid本领详解
前言
Audio Vivid(菁彩三维声)是全球首个基于AI技能的音频编解码标准,由天下超高清视频产业联盟(UWA联盟)与数字音视频编解码技能标准工作组(AVS)团结订定,共同发布。包含音频PCM数据以及元数据的音频格式,相比传统立体声音源,Audio Vivid包含音频内容源的元数据信息,能够还原物理和感知天下中的真实听感,打造极致的沉浸式听觉体验。
HarmonyOS打造全链路高清空间音频系统,包含Audio Vivid编解码、空间音频渲染算法等关键本领,并在各类终端产物逐步构建全场景空间音频特性,从传统立体声升级到三维声,得到更好的音质、更沉浸的空间感。
HarmonyOS支持播放Audio Vivid格式音源,并在耳机实现双耳空间音频渲染重放,在手机、平板、PC等支持外放空间音频渲染重放。系统的空间音频渲染本领无感接入,不必要做特定适配。
搭配HarmonyOS高清空间音频渲染本领,将音乐中的人声和各种乐器声作为独立的声音对象,重新定义各种声音对象的位置、移动轨迹和声音大小、远近等要素,实现声音在听众四周及上方全面萦绕,实现更佳的空间音频沉浸式体验,得到影院、音乐厅等的临场感与艺术体验。佩戴支持头动跟踪的耳机收听空间音频,还能实现动态头动跟踪,让声音围绕听众重新定位,还原逼真的临场感。
以下主要介绍利用HarmonyOS进行Audio Vivid格式音源的端到端播放的流程。
Audio Vivid端到端播放包罗调用系统编解码本领进行解封装、解码,以及调用系统播放本领进行渲染播放两个部门。
AudioVivid解封装
获取到Audio Vivid封装的mp4文件后,先调用解封装相干接口,选中音频轨,读取每一帧Audio Vivid,送入解码器中。详细的API请参考音频解封装API参考。
在Cmake脚本中链接到动态库
target_link_libraries(sample PUBLIC
libnative_media_codecbase.so libnative_media_core.so
libnative_media_acodec.so libnative_media_avdemuxer.so libnative_media_avsource.so
)
复制代码
添加头文件
//解封装头文件
#include "multimedia/player_framework/native_avdemuxer.h"
// 解封装解码传递信息结构体
struct AudioSampleInfo {
std::string audioCodecMime = "";
int32_t audioSampleForamt = 0;
int32_t audioSampleRate = 0;
int32_t audioChannelCount = 0;
int64_t audioChannelLayout = 0;
uint8_t audioCodecConfig[100] = {0};
size_t audioCodecSize = 0;
};
AudioSampleInfo info;
复制代码
开发步调
创建解封装实例。
// ts code获取fd和size
let inputFile = fs.openSync(filepath,fs.OpenMode.READ_ONLY);
if(inputFile){
let inputFileState = fs.stateSync(inputFile.fd);
let inputFileSize = inputFIleState.size;
}
复制代码
//C++ code
OH_AVSource *source = OH_AVSource_CreateWithFD(inputFd,0,inputFileSize);
OH_AVDemuxer *demuxer = OH_AVDemuxer_CreateWithSource(source);
auto sourceFormat = std::shared_ptr<OH_AVFormat>(OH_AVSource_GetSourceFormat(source_), OH_AVFormat_Destroy);
int32_t trackCount = 0;
OH_AVFormat_GetIntValue(sourceFormat.get(), OH_MD_KEY_TRACK_COUNT, &trackCount);
复制代码
选中音频轨。
int32_t trackCount = 0;
OH_AVFormat_GetIntValue(sourceFormat.get(), OH_MD_KEY_TRACK_COUNT, &trackCount);
for (int32_t index = 0; index < trackCount; index++) {
int trackType = -1;
auto trackFormat =
std::shared_ptr<OH_AVFormat>(OH_AVSource_GetTrackFormat(source_, index), OH_AVFormat_Destroy);
// 获取轨道类型
OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_TRACK_TYPE, &trackType);
// 判断当前轨道为音频轨
if (trackType == MEDIA_TYPE_AUD) {
// 选中音频轨
OH_AVDemuxer_SelectTrackByID(demuxer, index);
// 获取位深
OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUDIO_SAMPLE_FORMAT, &info.audioSampleForamt);
// 获取声道数
OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUD_CHANNEL_COUNT, &info.audioChannelCount);
// 获取声道布局
OH_AVFormat_GetLongValue(trackFormat.get(), OH_MD_KEY_CHANNEL_LAYOUT, &info.audioChannelLayout);
// 获取采样率
OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUD_SAMPLE_RATE, &info.audioSampleRate);
// 获取额外配置信息
uint8_t *addr = nullptr;
OH_AVFormat_GetBuffer(trackFormat.get(), OH_MD_KEY_CODEC_CONFIG, &addr, &info.audioCodecSize);
memcpy((void *)info.audioCodecConfig, (void *)addr, info.audioCodecSize);
// 获取解码器类型
char *audioCodecMime;
OH_AVFormat_GetStringValue(trackFormat.get(), OH_MD_KEY_CODEC_MIME, const_cast<char const **>(&audioCodecMime));
info.audioCodecMime = audioCodecMime;
int32_t trackId = index;
break;
}
}
复制代码
读取每一帧数据。
OH_AVBuffer *buffer;
int32_t ret = OH_AVDemuxer_ReadSampleBuffer(demuxer, trackId, buffer);
复制代码
释放解封装实例。
int32_t Release()
{
if (demuxer != nullptr) {
OH_AVDemuxer_Destroy(demuxer);
demuxer = nullptr;
}
if (source != nullptr) {
OH_AVSource_Destroy(source);
source = nullptr;
}
return AVCODEC_SAMPLE_ERR_OK;
}
复制代码
AudioVivid解码
获取解封装后的数据,送入解码器中,利用解码器获取PCM和Metadata元数据。详细的API请参考音频解码API参考。
在Cmake脚本中链接到动态库
target_link_libraries(sample PUBLIC
libnative_media_codecbase.so libnative_media_core.so
libnative_media_acodec.so libnative_media_avdemuxer.so libnative_media_avsource.so
)
复制代码
添加头文件
//解封装头文件
#include "multimedia/player_framework/native_avdemuxer.h"
// 解封装解码传递信息结构体
struct AudioSampleInfo {
std::string audioCodecMime = "";
int32_t audioSampleForamt = 0;
int32_t audioSampleRate = 0;
int32_t audioChannelCount = 0;
int64_t audioChannelLayout = 0;
uint8_t audioCodecConfig[100] = {0};
size_t audioCodecSize = 0;
};
AudioSampleInfo info;
复制代码
定义相干实例
定义CodecBufferInfo
解码码流的属性定义,为后面传给播放的码流数据封装。
struct CodecBufferInfo {
uint32_t bufferIndex = 0;
uintptr_t *buffer = nullptr;
uint8_t *bufferAddr = nullptr;
OH_AVCodecBufferAttr attr = {0, 0, 0, AVCODEC_BUFFER_FLAGS_NONE};
CodecBufferInfo(uint8_t *addr) : bufferAddr(addr){};
CodecBufferInfo(uint8_t *addr, int32_t bufferSize)
: bufferAddr(addr), attr({0, bufferSize, 0, AVCODEC_BUFFER_FLAGS_NONE}){};
CodecBufferInfo(uint32_t argBufferIndex, OH_AVMemory *argBuffer, OH_AVCodecBufferAttr argAttr)
: bufferIndex(argBufferIndex), buffer(reinterpret_cast<uintptr_t *>(argBuffer)), attr(argAttr){};
CodecBufferInfo(uint32_t argBufferIndex, OH_AVMemory *argBuffer)
: bufferIndex(argBufferIndex), buffer(reinterpret_cast<uintptr_t *>(argBuffer)){};
CodecBufferInfo(uint32_t argBufferIndex, OH_AVBuffer *argBuffer)
: bufferIndex(argBufferIndex), buffer(reinterpret_cast<uintptr_t *>(argBuffer)) {
OH_AVBuffer_GetBufferAttr(argBuffer, &attr);
};
};
复制代码
定义解码工作队列
class CodecUserData {
public:
SampleInfo *sampleInfo = nullptr;
// 输入帧数
uint32_t inputFrameCount_ = 0;
// 输入队列锁,防止多线程同时操作输入队列
std::mutex inputMutex_;
// 输入线程的条件变量,当输入队列为空时用于阻塞输入线程
std::condition_variable inputCond_;
// 输入buffer队列,存放编解码器传给用户用来写入输入数据的buffer
std::queue<CodecBufferInfo> inputBufferInfoQueue_;
// 输出帧数
uint32_t outputFrameCount_ = 0;
// 输出队列锁,防止多线程同时操作输出队列
std::mutex outputMutex_;
// 输出线程的条件变量,当输出队列为空时用于阻塞输出线程
std::condition_variable outputCond_;
std::mutex renderMutex_;
std::condition_variable renderCond_;
// 输出buffer队列,存放编解码器传给用户用来存放输出数据的buffer
std::queue<CodecBufferInfo> outputBufferInfoQueue_;
std::shared_ptr<AudioDecoder> audioCodec_;
std::queue<unsigned char> renderQueue_;
void ClearQueue() {
{
std::unique_lock<std::mutex> lock(inputMutex_);
auto emptyQueue = std::queue<CodecBufferInfo>();
inputBufferInfoQueue_.swap(emptyQueue);
}
{
std::unique_lock<std::mutex> lock(outputMutex_);
auto emptyQueue = std::queue<CodecBufferInfo>();
outputBufferInfoQueue_.swap(emptyQueue);
}
}
};
复制代码
定义回调函数
class SampleCallback {
public:
// 报错回调函数,当编解码器内部报错时调用,返回给用户相应错误码
static void OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData);
// 参数修改回调函数,当编解码器参数被修改时调用,返回给用户被修改后的format参数
static void OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData);
// 输入回调函数,当编解码器需要输入时调用,返回给用户用来写入输入数据的buffer及其对应的index
static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData);
// 输出回调函数,当编解码器生成新的输出数据时调用,返回给用户用来存放输出数据的buffer及其对应的index
static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData);
};
void SampleCallback::OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData) {
(void)codec;
(void)errorCode;
(void)userData;
}
void SampleCallback::OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData) {
}
void SampleCallback::OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
if (userData == nullptr) {
return;
}
(void)codec;
CodecUserData *codecUserData = static_cast<CodecUserData *>(userData);
std::unique_lock<std::mutex> lock(codecUserData->inputMutex_);
// 将输入buffer存放到输入队列中
codecUserData->inputBufferInfoQueue_.emplace(index, buffer);
// 通知输入线程开始运行
codecUserData->inputCond_.notify_all();
}
void SampleCallback::OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
if (userData == nullptr) {
return;
}
(void)codec;
CodecUserData *codecUserData = static_cast<CodecUserData *>(userData);
std::unique_lock<std::mutex> lock(codecUserData->outputMutex_);
// 将输出buffer存放到输出队列中
codecUserData->outputBufferInfoQueue_.emplace(index, buffer);
// 通知输出线程开始运行
codecUserData->outputCond_.notify_all();
}
复制代码
开发步调
创建解码实例。
// 创建解码器
OH_AVCodec * decoder = OH_AudioCodec_CreateByMime(info.audioCodecMime,false);
// 参数配置
OH_AVFormat *format = OH_AVFormat_Create();
OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, SAMPLE_S16LE); //或者S24LE
OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate);
OH_AVFormat_SetIntValue(format, OH_MD_KEY_AAC_IS_ADTS, 1);
OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, 96422);//码率,当前作为参考,解封装也可以获取到
OH_AVFormat_SetBuffer(format, OH_MD_KEY_CODEC_CONFIG, sampleInfo.audioCodecConfig, sampleInfo.audioCodecSize);
bool res = OH_AVFormat_SetLongValue(format, OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout);
ret = OH_AudioCodec_Configure(decoder, format);
OH_AVFormat_Destroy(format);
format = nullptr;
// 设置回调,用于输入输出buffer准备完毕后由系统回调出来
int32_t ret = OH_AudioCodec_RegisterCallback(decoder,
{SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange,
SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer},codecUserData);
// 准备回调和参数设置完毕后通知系统解码器准备好了,下一步准备启动。
ret = OH_AudioCodec_Prepare(decoder)
复制代码
音频写入解码器。
int32_t PushInputData(CodecBufferInfo &info)
{
int32_t ret = OH_AVBuffer_SetBufferAttr(reinterpret_cast<OH_AVBuffer *>(info.buffer), &info.attr);
ret = OH_AudioCodec_PushInputBuffer(decoder, info.bufferIndex);
return 0;
}
复制代码
释放利用过的输出码流。
CodecUserData*audioDecContext_ = new CodecUserData;
void AudioDecInputThread()
{
while (true) {
if(!isStarted_){
return;
}
std::unique_lock<std::mutex> lock(audioDecContext_->inputMutex_);
// 阻塞输入线程,直接程序运行结束,或者输入队列不为空
bool condRet = audioDecContext_->inputCond_.wait_for(
lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->inputBufferInfoQueue_.empty(); });
if(!isStarted_ || audioDecContext_->inputBufferInfoQueue_.empty()){
return;
}
// 获取输入buffer
CodecBufferInfo bufferInfo = audioDecContext_->inputBufferInfoQueue_.front();
audioDecContext_->inputBufferInfoQueue_.pop();
audioDecContext_->inputFrameCount_++;
lock.unlock();
// 从解封装器中读取一帧数据写入输入buffer
demuxer_->ReadSample(demuxer_->GetAudioTrackId(), reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
int32_t ret = audioDecoder_->PushInputData(bufferInfo);
if(ret != 0){
return;
}
if(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS){
return;
}
}
// StartRelease();
}
复制代码
音频写入线程。
CodecUserData*audioDecContext_ = new CodecUserData;
void AudioDecInputThread()
{
while (true) {
if(!isStarted_){
return;
}
std::unique_lock<std::mutex> lock(audioDecContext_->inputMutex_);
// 阻塞输入线程,直接程序运行结束,或者输入队列不为空
bool condRet = audioDecContext_->inputCond_.wait_for(
lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->inputBufferInfoQueue_.empty(); });
if(!isStarted_ || audioDecContext_->inputBufferInfoQueue_.empty()){
return;
}
// 获取输入buffer
CodecBufferInfo bufferInfo = audioDecContext_->inputBufferInfoQueue_.front();
audioDecContext_->inputBufferInfoQueue_.pop();
audioDecContext_->inputFrameCount_++;
lock.unlock();
// 从解封装器中读取一帧数据写入输入buffer
demuxer_->ReadSample(demuxer_->GetAudioTrackId(), reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer), bufferInfo.attr);
int32_t ret = audioDecoder_->PushInputData(bufferInfo);
if(ret != 0){
return;
}
if(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS){
return;
}
}
// StartRelease();
}
复制代码
音频解码输出线程。
void AudioDecOutputThread()
{
while (true) {
if(!isStarted_){
return;
}
std::unique_lock<std::mutex> lock(audioDecContext_->outputMutex_);
// 阻塞输出线程,直接程序运行结束,或者输出队列不为空
bool condRet = audioDecContext_->outputCond_.wait_for(
lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->outputBufferInfoQueue_.empty(); });
if(!isStarted_ || audioDecContext_->outputBufferInfoQueue_.empty()){
return;
}
// 获取输出buffer
CodecBufferInfo bufferInfo = audioDecContext_->outputBufferInfoQueue_.front();
audioDecContext_->outputBufferInfoQueue_.pop();
if(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS){
return;
}
audioDecContext_->outputFrameCount_++;
// 获取解码后的pcm数据
uint8_t *source = OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer));
OH_AVFormat * format = OH_AVBuffer_GetParameter(data);
uint8_t * metadata;
size_t size;
// 获取元数据
OH_AVFormat_GetBuffer(format, OH_MD_KEY_AUDIO_VIVID_METADATA, &metadata, &size);
#ifdef DEBUG_DECODE
if (audioOutputFile_.is_open()) {
audioOutputFile_.write((const char*)OH_AVBuffer_GetAddr(reinterpret_cast<OH_AVBuffer *>(bufferInfo.buffer)), bufferInfo.attr.size);
}
#endif
lock.unlock();
int32_t ret = audioDecoder_->FreeOutputData(bufferInfo.bufferIndex);
if(ret != 0){
return;
}
}
}
复制代码
启动解码。
int ret = OH_AudioCodec_Start(decoder);
复制代码
停止和释放实例。
OH_AudioCodec_Stop(decoder);
OH_AudioCodec_Destroy(decoder);
decoder = nullptr;
复制代码
AudioVivid播放
在获取到解码后的Audio Vivid的PCM数据和元数据之后,可以调用OHAudio的相干播放接口,进行Audio Vivid格式音源的渲染播放。详细的API说明请参考OHAudio API参考。
在Cmake脚本中链接到动态库
target_link_libraries(sample PUBLIC libohaudio.so)
复制代码
添加头文件
#include <ohaudio/native_audiorenderer.h>
#include <ohaudio/native_audiostreambuilder.h>
复制代码
开发步调
开发者可以通过以下几个步调来实现一个简单的播放功能。
创建构造器。
OHAudio提供OH_AudioStreamBuilder接口,遵照构造器筹划模式,用于构建音频流。在Audio Vivid播放场景下,必要选择OH_AudioStream_Type为AUDIOSTREAM_TYPE_RENDERER,创建一个渲染播放范例的音频构造器。
OH_AudioStreamBuilder* builder;
OH_AudioStreamBuilder_Create(&builder, AUDIOSTREAM_TYPE_RENDERER);
复制代码
配置音频流参数。
创建音频播放构造器后,可以设置音频流所必要的参数,可以参考以下案例。
Audio Vivid音源搭配系统空间音频渲染算法,播放效果和体验最佳。系统会根据输出音频流的工作场景(OH_AudioStream_Usage),选择利用对应的空间音频渲染效果,当前支持的工作场景包罗音乐、电影和有声读物。
// 设置音频采样率为48000Hz
OH_AudioStreamBuilder_SetSamplingRate(builder, 48000);
// 设置音频声道为10 (假定输入Audio Vivid音源是5.1.2声床 + 2对象格式)
OH_AudioStreamBuilder_SetChannelCount(builder, 10);
// 设置音频声道布局为5.1.2 (声道布局只考虑声床,若想使用默认声道布局,可以传入 CH_LAYOUT_UNKNOWN 参数)
OH_AudioStreamBuilder_SetChannelLayout(builder, CH_LAYOUT_5POINT1POINT2);
// 设置音频采样格式
OH_AudioStreamBuilder_SetSampleFormat(builder, AUDIOSTREAM_SAMPLE_S16LE);
// 设置音频流的编码类型为Audio Vivid编码类型
OH_AudioStreamBuilder_SetEncodingType(builder, AUDIOSTREAM_ENCODING_TYPE_AUDIOVIVID);
// 设置输出音频流的工作场景,根据实际工作场景选择音乐、电影、有声读物等类型
OH_AudioStreamBuilder_SetRendererInfo(builder, AUDIOSTREAM_USAGE_MUSIC);
复制代码
设置音频回调函数。
OHAudio利用回调模式进行音频流数据的写入,以及各种音频事件的上报,应用可以按需选择必要监听的音频事件。
为了克制不可预期的举动,在设置音频回调函数时,请确认OH_AudioRenderer_Callbacks的每一个回调都被自定义的回调方法或空指针初始化。
对于Audio Vivid播放场景,必要另外利用OH_AudioRenderer_WriteDataWithMetadataCallback进行PCM和元数据的写入。
// 自定义音频流事件函数
int32_t MyOnStreamEvent(
OH_AudioRenderer* renderer,
void* userData,
OH_AudioStream_Event event)
{
// 根据event表示的音频流事件信息,更新播放器状态和界面
return 0;
}
// 自定义音频中断事件函数
int32_t MyOnInterruptEvent(
OH_AudioRenderer* renderer,
void* userData,
OH_AudioInterrupt_ForceType type,
OH_AudioInterrupt_Hint hint)
{
// 根据type和hint表示的音频中断信息,更新播放器状态和界面
return 0;
}
// 自定义异常回调函数
int32_t MyOnError(
OH_AudioRenderer* renderer,
void* userData,
OH_AudioStream_Result error)
{
// 根据error表示的音频异常信息,做出相应的处理
return 0;
}
// 配置回调函数
OH_AudioRenderer_Callbacks callbacks;
// Audio Vivid播放时,该回调可以置空,使用元数据回调方式进行数据写入
callbacks.OH_AudioRenderer_OnWriteData = nullptr;
// 对音频流事件进行监听,如果不需要,可以使用 nullptr 赋值
callbacks.OH_AudioRenderer_OnStreamEvent = MyOnStreamEvent;
// 对音频中断事件进行监听,如果不需要,可以使用 nullptr 赋值
callbacks.OH_AudioRenderer_OnInterruptEvent = MyOnInterruptEvent;
// 对音频异常事件进行监听,如果不需要,可以使用 nullptr 赋值
callbacks.OH_AudioRenderer_OnError = MyOnError;
//设置输出音频流的回调
OH_AudioStreamBuilder_SetRendererCallback(builder, callbacks, nullptr);
// 自定义同时写入PCM数据和元数据函数
int32_t MyOnWriteDataWithMetadata(
OH_AudioRenderer* renderer,
void* userData,
void* audioData,
int32_t audioDataSize,
void* metadata,
int32_t metadataSize)
{
// 将待播放的PCM数据和元数据,分别按audioDataSize和metadataSize写入buffer
return 0;
}
// 配置回调函数
OH_AudioRenderer_WriteDataWithMetadataCallback metadataCallback = MyOnWriteDataWithMetadata;
// 设置同时写入PCM数据和元数据的回调
OH_AudioStreamBuilder_SetWriteDataWithMetadataCallback(builder, metadataCallback, nullptr);
复制代码
利用配置好的构造器,构造播放音频流。
OH_AudioRenderer* audioRenderer;
OH_AudioStreamBuilder_GenerateRenderer(builder, &audioRenderer);
复制代码
利用音频流。
可以利用以下接口,实现对音频流的控制,完成开始播放、停息播放、停止播放、清除缓存等基本操作。
在不再利用该条音频流时,可以释放播放实例,以便更好地管理内存。
释放构造器。
当构造器不再利用时,必要释放相干资源。
OH_AudioStreamBuilder_Destroy(builder);
复制代码
写在最后
有很多小伙伴不知道学习哪些鸿蒙开发技能?不知道必要重点把握哪些鸿蒙应用开发知识点?而且学习时频仍踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有须要的。
盼望这一份鸿蒙学习文档能够给大家带来帮助,有必要的小伙伴自行领取,
限时开源,先到先得~无套路领取!!
假如你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感爱好以及转行职员,可以直接领取这份资料
请点击→纯血版全套鸿蒙HarmonyOS学习文档
鸿蒙(HarmonyOS NEXT)5.0最新学习路线
路线图适合人群:
IT开发职员
:想要拓展职业边界
零底子小白
:鸿蒙爱好者,盼望从0到1学习,增长一项技能。
技能提拔/进阶跳槽
:发展瓶颈期,提拔职场竞争力,快速把握鸿蒙技能
获取以上完备版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习文档
《鸿蒙 (HarmonyOS)开发入门讲授视频》
《鸿蒙生态应用开发V3.0白皮书》
《鸿蒙 (OpenHarmony)开发底子到实战手册》
OpenHarmony北向、南向开发情况搭建
《鸿蒙开发底子》
●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
《鸿蒙开发进阶》
●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●关照与窗口管理
●多媒体技能
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来筹划
●鸿蒙系统移植和裁剪定制
……
《鸿蒙进阶实战》
●ArkTS实践
●UIAbility应用
●网络案例
……
获取以上完备鸿蒙HarmonyOS学习文档,请点击→纯血版全套鸿蒙HarmonyOS学习文档
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/)
Powered by Discuz! X3.4