OpenHarmony 3.2 Beta Audio——音频渲染

news2024/11/25 16:03:14

一、简介

Audio是多媒体子系统中的一个重要模块,其涉及的内容比较多,有音频的渲染、音频的采集、音频的策略管理等。本文主要针对音频渲染功能进行详细地分析,并通过源码中提供的例子,对音频渲染进行流程的梳理。

二、目录

foundation/multimedia/audio_framework

audio_framework
├── frameworks
│   ├── js                          #js 接口
│   │   └── napi
│   │       └── audio_renderer      #audio_renderer NAPI接口
│   │           ├── include
│   │           │   ├── audio_renderer_callback_napi.h
│   │           │   ├── renderer_data_request_callback_napi.h
│   │           │   ├── renderer_period_position_callback_napi.h
│   │           │   └── renderer_position_callback_napi.h
│   │           └── src
│   │               ├── audio_renderer_callback_napi.cpp
│   │               ├── audio_renderer_napi.cpp
│   │               ├── renderer_data_request_callback_napi.cpp
│   │               ├── renderer_period_position_callback_napi.cpp
│   │               └── renderer_position_callback_napi.cpp
│   └── native                      #native 接口
│       └── audiorenderer
│           ├── BUILD.gn
│           ├── include
│           │   ├── audio_renderer_private.h
│           │   └── audio_renderer_proxy_obj.h
│           ├── src
│           │   ├── audio_renderer.cpp
│           │   └── audio_renderer_proxy_obj.cpp
│           └── test
│               └── example
│                   └── audio_renderer_test.cpp
├── interfaces
│   ├── inner_api                   #native实现的接口
│   │   └── native
│   │       └── audiorenderer       #audio渲染本地实现的接口定义
│   │           └── include
│   │               └── audio_renderer.h
│   └── kits                        #js调用的接口
│       └── js
│           └── audio_renderer      #audio渲染NAPI接口的定义
│               └── include
│                   └── audio_renderer_napi.h
└── services                        #服务端
    └── audio_service
        ├── BUILD.gn
        ├── client                  #IPC调用中的proxy端
        │   ├── include
        │   │   ├── audio_manager_proxy.h
        │   │   ├── audio_service_client.h
        │   └── src
        │       ├── audio_manager_proxy.cpp
        │       ├── audio_service_client.cpp
        └── server                  #IPC调用中的server端
            ├── include
            │   └── audio_server.h
            └── src
                ├── audio_manager_stub.cpp
                └── audio_server.cpp

三、音频渲染总体流程

四、Native接口使用

在OpenAtom OpenHarmony(以下简称“OpenHarmony”)系统中,音频模块提供了功能测试代码,本文选取了其中的音频渲染例子作为切入点来进行介绍,例子采用的是对wav格式的音频文件进行渲染。wav格式的音频文件是wav头文件和音频的原始数据,不需要进行数据解码,所以音频渲染直接对原始数据进行操作,文件路径为:

foundation/multimedia/audio_framework/frameworks/native/audiorenderer/test/example/audio_renderer_test.cpp

bool TestPlayback(int argc, char *argv[]) const
{
        FILE* wavFile = fopen(path, "rb");
        //读取wav文件头信息
        size_t bytesRead = fread(&wavHeader, 1, headerSize, wavFile);

        //设置AudioRenderer参数
        AudioRendererOptions rendererOptions = {};
        rendererOptions.streamInfo.encoding = AudioEncodingType::ENCODING_PCM;
        rendererOptions.streamInfo.samplingRate = static_cast<AudioSamplingRate>(wavHeader.SamplesPerSec);
        rendererOptions.streamInfo.format = GetSampleFormat(wavHeader.bitsPerSample);
        rendererOptions.streamInfo.channels = static_cast<AudioChannel>(wavHeader.NumOfChan);
        rendererOptions.rendererInfo.contentType = contentType;
        rendererOptions.rendererInfo.streamUsage = streamUsage;
        rendererOptions.rendererInfo.rendererFlags = 0;

        //创建AudioRender实例
        unique_ptr<AudioRenderer> audioRenderer = AudioRenderer::Create(rendererOptions);

        shared_ptr<AudioRendererCallback> cb1 = make_shared<AudioRendererCallbackTestImpl>();
        //设置音频渲染回调
        ret = audioRenderer->SetRendererCallback(cb1);

        //InitRender方法主要调用了audioRenderer实例的Start方法,启动音频渲染
        if (!InitRender(audioRenderer)) {
            AUDIO_ERR_LOG("AudioRendererTest: Init render failed");
            fclose(wavFile);
            return false;
        }

        //StartRender方法主要是读取wavFile文件的数据,然后通过调用audioRenderer实例的Write方法进行播放
        if (!StartRender(audioRenderer, wavFile)) {
            AUDIO_ERR_LOG("AudioRendererTest: Start render failed");
            fclose(wavFile);
            return false;
        }

        //停止渲染
        if (!audioRenderer->Stop()) {
            AUDIO_ERR_LOG("AudioRendererTest: Stop failed");
        }

        //释放渲染
        if (!audioRenderer->Release()) {
            AUDIO_ERR_LOG("AudioRendererTest: Release failed");
        }

        //关闭wavFile
        fclose(wavFile);
        return true;
    }

首先读取wav文件,通过读取到wav文件的头信息对AudioRendererOptions相关的参数进行设置,包括编码格式、采样率、采样格式、通道数等。根据AudioRendererOptions设置的参数来创建AudioRenderer实例(实际上是AudioRendererPrivate),后续的音频渲染主要是通过AudioRenderer实例进行。创建完成后,调用AudioRenderer的Start方法,启动音频渲染。启动后,通过AudioRenderer实例的Write方法,将数据写入,音频数据会被播放。

五、调用流程

1. 创建AudioRenderer

std::unique_ptr<AudioRenderer> AudioRenderer::Create(const std::string cachePath,
    const AudioRendererOptions &rendererOptions, const AppInfo &appInfo)
{
    ContentType contentType = rendererOptions.rendererInfo.contentType;
    
    StreamUsage streamUsage = rendererOptions.rendererInfo.streamUsage;
   
    AudioStreamType audioStreamType = AudioStream::GetStreamType(contentType, streamUsage);
    auto audioRenderer = std::make_unique<AudioRendererPrivate>(audioStreamType, appInfo);
    if (!cachePath.empty()) {
        AUDIO_DEBUG_LOG("Set application cache path");
        audioRenderer->SetApplicationCachePath(cachePath);
    }

    audioRenderer->rendererInfo_.contentType = contentType;
    audioRenderer->rendererInfo_.streamUsage = streamUsage;
    audioRenderer->rendererInfo_.rendererFlags = rendererOptions.rendererInfo.rendererFlags;

    AudioRendererParams params;
    params.sampleFormat = rendererOptions.streamInfo.format;
    params.sampleRate = rendererOptions.streamInfo.samplingRate;
    params.channelCount = rendererOptions.streamInfo.channels;
    params.encodingType = rendererOptions.streamInfo.encoding;

    if (audioRenderer->SetParams(params) != SUCCESS) {
        AUDIO_ERR_LOG("SetParams failed in renderer");
        audioRenderer = nullptr;
        return nullptr;
    }

    return audioRenderer;
}

首先通过AudioStream的GetStreamType方法获取音频流的类型,根据音频流类型创建AudioRendererPrivate对象,AudioRendererPrivate是AudioRenderer的子类。紧接着对audioRenderer进行参数设置,其中包括采样格式、采样率、通道数、编码格式。设置完成后返回创建的AudioRendererPrivate实例。

2. 设置回调

int32_t AudioRendererPrivate::SetRendererCallback(const std::shared_ptr<AudioRendererCallback> &callback)
{
    RendererState state = GetStatus();
    if (state == RENDERER_NEW || state == RENDERER_RELEASED) {
        return ERR_ILLEGAL_STATE;
    }
    if (callback == nullptr) {
        return ERR_INVALID_PARAM;
    }

    // Save reference for interrupt callback
    if (audioInterruptCallback_ == nullptr) {
        return ERROR;
    }
    std::shared_ptr<AudioInterruptCallbackImpl> cbInterrupt =
        std::static_pointer_cast<AudioInterruptCallbackImpl>(audioInterruptCallback_);
    cbInterrupt->SaveCallback(callback);

    // Save and Set reference for stream callback. Order is important here.
    if (audioStreamCallback_ == nullptr) {
        audioStreamCallback_ = std::make_shared<AudioStreamCallbackRenderer>();
        if (audioStreamCallback_ == nullptr) {
            return ERROR;
        }
    }
    std::shared_ptr<AudioStreamCallbackRenderer> cbStream =
std::static_pointer_cast<AudioStreamCallbackRenderer>(audioStreamCallback_);
    cbStream->SaveCallback(callback);
    (void)audioStream_->SetStreamCallback(audioStreamCallback_);

    return SUCCESS;
}

参数传入的回调主要涉及到两个方面:一方面是AudioInterruptCallbackImpl中设置了我们传入的渲染回调,另一方面是AudioStreamCallbackRenderer中也设置了渲染回调。

3. 启动渲染

bool AudioRendererPrivate::Start(StateChangeCmdType cmdType) const
{
    AUDIO_INFO_LOG("AudioRenderer::Start");
    RendererState state = GetStatus();

    AudioInterrupt audioInterrupt;
    switch (mode_) {
        case InterruptMode::SHARE_MODE:
            audioInterrupt = sharedInterrupt_;
            break;
        case InterruptMode::INDEPENDENT_MODE:
            audioInterrupt = audioInterrupt_;
            break;
        default:
            break;
    }
    AUDIO_INFO_LOG("AudioRenderer::Start::interruptMode: %{public}d, streamType: %{public}d, sessionID: %{public}d",
        mode_, audioInterrupt.streamType, audioInterrupt.sessionID);

    if (audioInterrupt.streamType == STREAM_DEFAULT || audioInterrupt.sessionID == INVALID_SESSION_ID) {
        return false;
    }

    int32_t ret = AudioPolicyManager::GetInstance().ActivateAudioInterrupt(audioInterrupt);
    if (ret != 0) {
        AUDIO_ERR_LOG("AudioRendererPrivate::ActivateAudioInterrupt Failed");
        return false;
    }

    return audioStream_->StartAudioStream(cmdType);
}

AudioPolicyManager::GetInstance().ActivateAudioInterrupt这个操作主要是根据AudioInterrupt来进行音频中断的激活,这里涉及了音频策略相关的内容,后续会专门出关于音频策略的文章进行分析。这个方法的核心是通过调用AudioStream的StartAudioStream方法来启动音频流。

bool AudioStream::StartAudioStream(StateChangeCmdType cmdType)
{
    int32_t ret = StartStream(cmdType);

    resetTime_ = true;
    int32_t retCode = clock_gettime(CLOCK_MONOTONIC, &baseTimestamp_);

    if (renderMode_ == RENDER_MODE_CALLBACK) {
        isReadyToWrite_ = true;
        writeThread_ = std::make_unique<std::thread>(&AudioStream::WriteCbTheadLoop, this);
    } else if (captureMode_ == CAPTURE_MODE_CALLBACK) {
        isReadyToRead_ = true;
        readThread_ = std::make_unique<std::thread>(&AudioStream::ReadCbThreadLoop, this);
    }

    isFirstRead_ = true;
    isFirstWrite_ = true;
    state_ = RUNNING;
    AUDIO_INFO_LOG("StartAudioStream SUCCESS");

    if (audioStreamTracker_) {
        AUDIO_DEBUG_LOG("AudioStream:Calling Update tracker for Running");
        audioStreamTracker_->UpdateTracker(sessionId_, state_, rendererInfo_, capturerInfo_);
    }
    return true;
}

AudioStream的StartAudioStream主要的工作是调用StartStream方法,StartStream方法是AudioServiceClient类中的方法。AudioServiceClient类是AudioStream的父类。接下来看一下AudioServiceClient的StartStream方法。

int32_t AudioServiceClient::StartStream(StateChangeCmdType cmdType)
{
    int error;
    lock_guard<mutex> lockdata(dataMutex);
    pa_operation *operation = nullptr;

    pa_threaded_mainloop_lock(mainLoop);

    pa_stream_state_t state = pa_stream_get_state(paStream);

    streamCmdStatus = 0;
    stateChangeCmdType_ = cmdType;
    operation = pa_stream_cork(paStream, 0, PAStreamStartSuccessCb, (void *)this);

    while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) {
        pa_threaded_mainloop_wait(mainLoop);
    }
    pa_operation_unref(operation);
    pa_threaded_mainloop_unlock(mainLoop);

    if (!streamCmdStatus) {
        AUDIO_ERR_LOG("Stream Start Failed");
        ResetPAAudioClient();
        return AUDIO_CLIENT_START_STREAM_ERR;
    } else {
        AUDIO_INFO_LOG("Stream Started Successfully");
        return AUDIO_CLIENT_SUCCESS;
    }
}

StartStream方法中主要是调用了pulseaudio库的pa_stream_cork方法进行流启动,后续就调用到了pulseaudio库中了。pulseaudio库我们暂且不分析。

4. 写入数据

int32_t AudioRendererPrivate::Write(uint8_t *buffer, size_t bufferSize)
{
    return audioStream_->Write(buffer, bufferSize);
}

通过调用AudioStream的Write方式实现功能,接下来看一下AudioStream的Write方法。

size_t AudioStream::Write(uint8_t *buffer, size_t buffer_size)
{
    int32_t writeError;
    StreamBuffer stream;
    stream.buffer = buffer;
    stream.bufferLen = buffer_size;
    isWriteInProgress_ = true;

    if (isFirstWrite_) {
        if (RenderPrebuf(stream.bufferLen)) {
            return ERR_WRITE_FAILED;
        }
        isFirstWrite_ = false;
    }

    size_t bytesWritten = WriteStream(stream, writeError);
    isWriteInProgress_ = false;
    if (writeError != 0) {
        AUDIO_ERR_LOG("WriteStream fail,writeError:%{public}d", writeError);
        return ERR_WRITE_FAILED;
    }
    return bytesWritten;
}

Write方法中分成两个阶段,首次写数据,先调用RenderPrebuf方法,将preBuf_的数据写入后再调用WriteStream进行音频数据的写入。

size_t AudioServiceClient::WriteStream(const StreamBuffer &stream, int32_t &pError)
{
   
    size_t cachedLen = WriteToAudioCache(stream);
    if (!acache.isFull) {
        pError = error;
        return cachedLen;
    }

    pa_threaded_mainloop_lock(mainLoop);


    const uint8_t *buffer = acache.buffer.get();
    size_t length = acache.totalCacheSize;

    error = PaWriteStream(buffer, length);
    acache.readIndex += acache.totalCacheSize;
    acache.isFull = false;

    if (!error && (length >= 0) && !acache.isFull) {
        uint8_t *cacheBuffer = acache.buffer.get();
        uint32_t offset = acache.readIndex;
        uint32_t size = (acache.writeIndex - acache.readIndex);
        if (size > 0) {
            if (memcpy_s(cacheBuffer, acache.totalCacheSize, cacheBuffer + offset, size)) {
                AUDIO_ERR_LOG("Update cache failed");
                pa_threaded_mainloop_unlock(mainLoop);
                pError = AUDIO_CLIENT_WRITE_STREAM_ERR;
                return cachedLen;
            }
            AUDIO_INFO_LOG("rearranging the audio cache");
        }
        acache.readIndex = 0;
        acache.writeIndex = 0;

        if (cachedLen < stream.bufferLen) {
            StreamBuffer str;
            str.buffer = stream.buffer + cachedLen;
            str.bufferLen = stream.bufferLen - cachedLen;
            AUDIO_DEBUG_LOG("writing pending data to audio cache: %{public}d", str.bufferLen);
            cachedLen += WriteToAudioCache(str);
        }
    }

    pa_threaded_mainloop_unlock(mainLoop);
    pError = error;
    return cachedLen;
}

WriteStream方法不是直接调用pulseaudio库的写入方法,而是通过WriteToAudioCache方法将数据写入缓存中,如果缓存没有写满则直接返回,不会进入下面的流程,只有当缓存写满后,才会调用下面的PaWriteStream方法。该方法涉及对pulseaudio库写入操作的调用,所以缓存的目的是避免对pulseaudio库频繁地做IO操作,提高了效率。

六、总结

本文主要对OpenHarmony 3.2 Beta多媒体子系统的音频渲染模块进行介绍,首先梳理了Audio Render的整体流程,然后对几个核心的方法进行代码的分析。整体的流程主要通过pulseaudio库启动流,然后通过pulseaudio库的pa_stream_write方法进行数据的写入,最后播放出音频数据。

音频渲染主要分为以下几个层次:

(1)AudioRenderer的创建,实际创建的是它的子类AudioRendererPrivate实例。

(2)通过AudioRendererPrivate设置渲染的回调。

(3)启动渲染,这一部分代码最终会调用到pulseaudio库中,相当于启动了pulseaudio的流。(4)通过pulseaudio库的pa_stream_write方法将数据写入设备,进行播放。

对OpenHarmony 3.2 Beta多媒体系列开发感兴趣的读者,也可以阅读我之前写过几篇文章:

OpenHarmony 3.2 Beta多媒体系列——视频录制

OpenHarmony 3.2 Beta源码分析之MediaLibrary

OpenHarmony 3.2 Beta多媒体系列——音视频播放框架

OpenHarmony 3.2 Beta多媒体系列——音视频播放gstreamer》。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383583.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无线WiFi安全渗透与攻防(一)之无线安全环境搭建

无线安全环境搭建 1.802.11标准 &#xff08;1&#xff09;.概念 802.11标准是1997年IEEE最初制定的一个WLAN标准&#xff0c;工作在2.4GHz开放频段&#xff0c;支持1Mbit/s和2Mbit/s的数据传输速率&#xff0c;定义了物理层和MAC层规范&#xff0c;允许无线局域网及无线设备…

Crack:LightningChart .NE​​T 10.4.1中的新功能

数据游标 Lightningchart .NET UWP 展示应用 在以前的版本中&#xff0c;LightningChart .NET 提供了不同的工具来实现数据跟踪功能&#xff0c;但都需要额外的用户编码。 现在可以使用 DataCursor 浏览 ViewXY 系列。系列数据值由这个新类/对象显示在鼠标位置或鼠标位置附近。…

【matplotlib】可视化解决方案——如何解决matplotlib中文乱码问题

问题概述 Matplotlib 默认不支持中文字体&#xff0c;这是因为 matplotlib 只支持 ASCII 字符&#xff0c;但是国人使用 matplotlib 肯定需要中文标注。如下图所示&#xff0c;当不对 Matplotlib 进行设置&#xff0c;而直接使用中文时&#xff0c;绘制的图像会出现中文乱码。…

为什么我选择收费的AdsPower指纹浏览器?

在决定开始用指纹浏览器之前&#xff0c;东哥我们团队找了市面上很多产品去测试。最后&#xff0c;还是决定用AdsPower。每个人的使用感受都不一样&#xff0c;我就说几个东哥和我们团队用得顺手的点&#xff0c;大家在选择指纹浏览器的时候也可以做一个参考。 一、指纹环境强大…

3月5日,加入线上对话,点燃科技行业女性影响力!

对话升级&#xff0c;点燃科技行业女性影响力&#xff01; &#x1f44b; 2022 年&#xff0c;Jina AI 联合 14 家合作伙伴&#xff0c;首次举办了「Impact Tech, She Can」线上对话&#xff0c;11 位嘉宾与 200 多位参会者分享了如何在科技行业内打造自身影响力。 &#x1f38…

html基础(h、p、br、hr、文本加粗倾斜下划线删除线、资源路径、音频、视频、超链接)

1标题<h><h1>1级标题</h1><h2>2级标题</h2><h3>3级标题</h3><h4>4级标题</h4><h5>5级标题</h5><h6>6级标题</h6>2段落<p>和换行<br><p>段落标签</p><p> fgghikg…

大数据技术——面向对象编程基础

类类的定义字段定义:用val或var关键字进行定义方法定义:使用new关键字创建一个类的实例类成员可见性Scala类中所有成员的默认可见性为公有&#xff0c;任何作用域内都能直接访问公有成员除了默认的公有可见性&#xff0c;Scala也提供private和protected其中&#xff0c;private…

真涨脸,我用 Python 为朋友自动化整理表格

今天&#xff0c;在工作的时候&#xff0c;我的美女同事问我有没有办法自动生成一个这样的表格&#xff1a; 第一列是院校科目&#xff0c;第二列是年份&#xff0c;第三列是数量。 这张表格是基于这一文件夹填充的&#xff0c;之前要一个文件夹一个文件夹打开然后手动填写年份…

测牛学堂:软件测试python之unittest框架总结(3)

python之unittest添加整个测试类执行 如果我们的测试用例比较多的话&#xff0c;测试套件对象通过addTest一个一个添加比较麻烦&#xff0c;可以通过添加一个类的方法&#xff0c; 去执行这个类里面的所有测试方法 套件对象.addTest(unittest.makeSuite(测试类名)) import un…

“终于我从字节离职了...“一个年薪50W的测试工程师的自白...

我递上了我的辞职信&#xff0c;不是因为公司给的不多&#xff0c;也不是因为公司待我不好&#xff0c;但是我觉得&#xff0c;我每天看中我憔悴的面容&#xff0c;每天晚上拖着疲惫的身体躺在床上&#xff0c;我都不知道人生的意义&#xff0c;是赚钱吗&#xff1f;是为了更好…

3.2滑动窗口

滑动窗口*** 题目链接 视频讲解 属于单调队列的模板题 如果每次移动窗口&#xff0c;然后在窗口中循环遍历查找最大值&#xff0c;时间复杂度太高 解决思路&#xff1a; 维护一个单调队列&#xff0c;其中head永远指的是当前窗口中最大的值&#xff0c;从head到tail元素递减。…

【Unity VR开发】结合VRTK4.0:创建圆盘

语录&#xff1a; 茶若相似&#xff0c;味不必如一。但凡茗茶&#xff0c;一泡苦涩&#xff0c;二泡甘香&#xff0c;三泡浓沉&#xff0c;四泡清洌&#xff0c;五泡清淡&#xff0c;此后&#xff0c;再好的茶也索然无味。诚似人生五种&#xff0c;年少青涩&#xff0c;青春芳…

【并发基础】Java中线程的创建和运行以及相关源码分析

目录 一、线程的创建和运行 1.1 创建和运行线程的三种方法 1.2 三者之间的继承关系 二、Thread类和Runnable接口的区别 2.1 Runnable接口可以实现线程之间资源共享&#xff0c;而Thread类不能 2.2 实现Runnable接口相对于继承Thread类的优点 三、实现 Runnable 接口和实现 Call…

【python学习笔记】:Excel 数据的封装函数

对比其它编程语言&#xff0c;我们都知道Python最大的优势是代码简单&#xff0c;有丰富的第三方开源库供开发者使用。伴随着近几年数据分析的热度&#xff0c;Python也成为最受欢迎的编程语言之一。而对于数据的读取和存储&#xff0c;对于普通人来讲&#xff0c;除了数据库之…

程序员推荐的良心网站合集!(第二期)

今天来给大家推荐几个程序员必看的国外良心网站合集第二期合集。 Semantic Schoolar 由微软联合创始人Paul Allen开发的免费学术搜索引擎&#xff0c;不仅可以通过时间线快速定位想要的文献&#xff0c;还有强大的筛选功能可以精准的找到自己想要的文献&#xff0c;想要什么搜…

服务器部署

文章目录目录前言1、前端服务器选型1.1、Nginx1.1.1、Nginx介绍1.1.2、正向代理&反向代理1、正向代理2、反向代理1.1.3、优点1、支持高并发2、内存消耗少3、成本低廉4、配置文件非常简单5、支持Rewrite重写6、内置的健康检查功能7、节省带宽8、稳定性高9、支持热部署1.2、N…

[oeasy]python0098_个人计算机浪潮_IBM5100_微软成立_苹果II_VisCalc

个人计算机浪潮 回忆上次内容 个人电脑(PC) 在爱好者之间疯传 人人都有一台计算机 从attair-8800到apple-1个人电脑 离普通人 更近了 如果 人人都有 自己的电脑 谁还去 用终端连接大型机 呢&#xff1f; IBM真的被干掉了吗&#xff1f;&#x1f914; 时代背景 计算机 逐渐…

JVM 全面了解

JVM包含两个子系统和两个组件&#xff0c;两个子系统为Class loader(类装载器)、Execution engine(执行引擎)&#xff1b;两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 方法区&#xff1a;存储已被虚拟机加载的类元数据信息(元空间) 堆&#xf…

数据分析-深度学习 NLP Day3句法分析

第六章句法分析在本章中&#xff0c;你将学到与句法分析相关的一些算法和技术 。 很多技术手段可以用来实 现句法分析&#xff0c;包括基于规则的和基于统计的&#xff0c;在本章中读者将会了解其基本原理和使用方法 。本章要点主要如下&#xff1a;句法分析及其难点句法分析相…

高分子PEG,Biotin-PEG-amine,Biotin-PEG-NH2,生物素-聚乙二醇-氨基

Biotin-PEG-amine&#xff0c; Biotin-PEG-NH2 | 生物素-聚乙二醇-氨基 | CAS&#xff1a;N/A | 纯度&#xff1a;95%一、试剂信息&#xff1a;CAS号&#xff1a;N/A外观&#xff1a;固体/粉末分子量&#xff1a;1K、2K、5K、3.4K、10K、20K溶解性&#xff1a;溶于有机溶剂&…