音频Balance源码总结

news2024/12/24 3:14:29

音频Balance源码总结

何为音频Balance?

顾名思义,Balance及平衡,平衡也就是涉及多方,音频左右甚至四通道,调节所有通道的音量比,使用户在空间内听到各个通道的音频大小不一,好似置身于真实环境中;

博主分析的Balance源码在

./system/media/audio_utils/include/audio_utils/Balance.h
./system/media/audio_utils/Balance.cpp

Balance原理

如下图,提供给用户一个设置进度条:

在这里插入图片描述

用户设置balance在-0.5,那就给channel left的音量为1,channel right的音量x(0~1,通过一定算法计算得到),最后我们遍历buffer中的每一个音频数据,左侧通道乘1就会保持不变,右侧乘小于1的数据,音量就会减小,这样就达成了左右channel输出的音频不同了

如上原理,很简单吧!但是我们要考虑几个问题

  • 音频数据的格式类别(双通道、2.1通道的数据格式)
  • 各个通道的音频分量如何确定?

辨别音频数据格式类别

这个也是Balance类第一步要做的事情,对应代码中,在执行Balance之间必须要先调用

void setChannelMask(audio_channel_mask_t channelMask);

插播一个知识点,在ChannelMask中,表示音频数据存储方式有以下2种:

  • AUDIO_CHANNEL_REPRESENTATION_POSITION
    位置表示法,音频数据中每个bit位表示每个位置的音频数据,如前左 前右等音响
  • AUDIO_CHANNEL_REPRESENTATION_INDEX
    序号法表示,每个bit位表示不同的通道,如0位是主通道,1位是辅助通道等
    用audio_channel_mask_get_representation(channelMask)函数可以得到mask是用哪种方法表示;

接着上setChannelMask看,主要根据channelMask不同表示方法进行不同的处理:
深入代码,看看Balance如何区分每个channel的情况:

AUDIO_CHANNEL_REPRESENTATION_INDEX处理方式
void Balance::setChannelMask(audio_channel_mask_t channelMask)
{
    //去掉haptic震动反馈通道,这个不属于balance范畴
    channelMask &= ~ AUDIO_CHANNEL_HAPTIC_ALL;
    //如果部署输出类型的mask,或者与之前的mask相同,就没必要再次设置
    if (!audio_is_output_channel(channelMask) // invalid mask
            || mChannelMask == channelMask) { // no need to do anything
        return;
    }

    mChannelMask = channelMask;
    mChannelCount = audio_channel_count_from_out_mask(channelMask);

    // save mBalance into balance for later restoring, then reset
    const float balance = mBalance;
    mBalance = 0.f;

    // reset mVolumes将vector数组重置大小为mChannelCount
    mVolumes.resize(mChannelCount);
        //填充mVolumes所有值为1.f
    std::fill(mVolumes.begin(), mVolumes.end(), 1.f);

    // reset ramping variables
    mRampBalance = 0.f;
    mRampVolumes.clear();
    //如果mChannelMask是按照序号法来表示,如index 0表示主通道、1副通道,
    //则不是按照声场位置表示法AUDIO_CHANNEL_REPRESENTATION_POSITION
    if (audio_channel_mask_get_representation(mChannelMask)
            == AUDIO_CHANNEL_REPRESENTATION_INDEX) {
        mSides.clear();       // mSides unused for channel index masks.
        setBalance(balance);  // recompute balance
        return;
    }
}

以上函数主要做了以下几件事情:

  1. 从ChannelMask中获取通道数,并根据通道数resize重置mVolumes(vector类型)大小为channelCount,因为balance就是根据通道来进行加减乘除的
  2. 如果channelMask是AUDIO_CHANNEL_REPRESENTATION_INDEX格式,也就是index方式来表示通道数据,无需用mSide位置来表示
  3. setBalance为每个声道设置对应的音量,后面展开
AUDIO_CHANNEL_REPRESENTATION_POSITION处理方式

以下这段代码也是在setChannelMask方法中的,是当channelMask为AUDIO_CHANNEL_REPRESENTATION_POSITION的处理方式

    //sideFromChannel也就是把每一个channel映射到声场的位置,0-left,1-right,2-center
    //比如以下为9x1u的channel对应左边的声场位置,其他依次类推
    static constexpr int sideFromChannel[] = {
        0, // AUDIO_CHANNEL_OUT_FRONT_LEFT            = 0x1u,
        1, // AUDIO_CHANNEL_OUT_FRONT_RIGHT           = 0x2u,
        2, // AUDIO_CHANNEL_OUT_FRONT_CENTER          = 0x4u,
        2, // AUDIO_CHANNEL_OUT_LOW_FREQUENCY         = 0x8u,   //低频分量的数据,专门传输到低音炮的外放装置中
        0, // AUDIO_CHANNEL_OUT_BACK_LEFT             = 0x10u,
        1, // AUDIO_CHANNEL_OUT_BACK_RIGHT            = 0x20u,
        0, // AUDIO_CHANNEL_OUT_FRONT_LEFT_OF_CENTER  = 0x40u,
        1, // AUDIO_CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x80u,
        2, // AUDIO_CHANNEL_OUT_BACK_CENTER           = 0x100u,
        0, // AUDIO_CHANNEL_OUT_SIDE_LEFT             = 0x200u,
        1, // AUDIO_CHANNEL_OUT_SIDE_RIGHT            = 0x400u,
        2, // AUDIO_CHANNEL_OUT_TOP_CENTER            = 0x800u,
        0, // AUDIO_CHANNEL_OUT_TOP_FRONT_LEFT        = 0x1000u,
        2, // AUDIO_CHANNEL_OUT_TOP_FRONT_CENTER      = 0x2000u,
        1, // AUDIO_CHANNEL_OUT_TOP_FRONT_RIGHT       = 0x4000u,
        0, // AUDIO_CHANNEL_OUT_TOP_BACK_LEFT         = 0x8000u,
        2, // AUDIO_CHANNEL_OUT_TOP_BACK_CENTER       = 0x10000u,
        1, // AUDIO_CHANNEL_OUT_TOP_BACK_RIGHT        = 0x20000u,
        0, // AUDIO_CHANNEL_OUT_TOP_SIDE_LEFT         = 0x40000u,
        1, // AUDIO_CHANNEL_OUT_TOP_SIDE_RIGHT        = 0x80000u,
     };

    mSides.resize(mChannelCount);
    for (unsigned i = 0, channel = channelMask; channel != 0; ++i) {
        //计算channel从低位开始第一个1的位置
        const int index = __builtin_ctz(channel);
        if (index < std::size(sideFromChannel)) {
            mSides[i] = sideFromChannel[index];
   } else {
            mSides[i] = 2; // consider center
        }
        channel &= ~(1 << index);
    }
    setBalance(balance); // recompute balance

可以看到android定义的channel的位置有很多的,如AUDIO_CHANNEL_OUT_FRONT_LEFT、AUDIO_CHANNEL_OUT_BACK_RIGHT、AUDIO_CHANNEL_OUT_TOP_SIDE_LEFT等等,但是这么多的位置的喇叭,对应到sideFromChannel里面去的取值,只有0、1、2,也就是left、right和center;相当于把上面20个位置转换到3个位置上去,多维转换到一维了;(显然这么做是不合理的,Android这么做后期肯定是可以优化的)
最后面的for循环,就是记录当前的channelMask的几个channel对应那几个位置side,mSide数组就是保存了channel的位置side,用以下一幅图表示就是:
在这里插入图片描述

确定每个通道的音频音量

接上面代码,当确定好每个channel的位置后,接下来就是为每个channel计算他的音量了,也就是setBalance

//balance参数取值范围在[-1,1],这个函数的意义在于为每个channel计算它的音量值
void Balance::setBalance(float balance)
{
    //如何这次设置的balance和上次相同
    if (mBalance == balance                         // no change
        //balance值非法,或者绝对值大于1,就认为非法
        || isnan(balance) || fabs(balance) > 1.f) { // balance out of range
        return;
    }   
    mBalance = balance;
    //通道数小于2个也没必要
    if (mChannelCount < 2) { 
        return;              
    }   
    //如果音频是双通道或者是用INDEX表示音频数据格式
    if (mChannelMask == AUDIO_CHANNEL_OUT_STEREO
            || audio_channel_mask_get_representation(mChannelMask)
                    == AUDIO_CHANNEL_REPRESENTATION_INDEX) {
        //那就只需要计算左右两个通道的音量值,mVolumes[0]保存左声道的音量,mVolumes[1]保存右声道的音量
        computeStereoBalance(balance, &mVolumes[0], &mVolumes[1]);
        return;
    }   
    //side位置表示法格式的音频,则需要计算3个位置的音量
    //计算好当前声道平衡balance对应在left、right和center的音量值
    float balanceVolumes[3]; // left, right, center
    //同上这里只计算左右left\right声道的音量值,中间的取音频本身的音量值
    computeStereoBalance(balance, &balanceVolumes[0], &balanceVolumes[1]);
    balanceVolumes[2] = 1.f; // center  TODO: consider center scaling.
    for (size_t i = 0; i < mVolumes.size(); ++i) {
        //mSides表示当前channel的声场位置,mSides[i]可能是0、1、2取值,分别代表
        //left位置、right位置和中间位置,在根据上面计算这3个位置对应的音量balanceVolumes
        //就可以得到每个side的音量增益了
        mVolumes[i] = balanceVolumes[mSides[i]];
    }
}

上面代码很简单,主要是初始化好每个channel的音量数据,将参数传入到computeStereoBalance函数进行计算得到,看看是如何计算每个声道的音量:

/*
* 当blance值在[-1,0],表示左声道最大音量1.f,右声道为1.f与balance差值
* 当blance值在[0,1],表示有声道要比作声道大,同理
*/
void Balance::computeStereoBalance(float balance, float *left, float *right) const
{   //balance大于0,说明用户是要设置声道平衡在右侧
    if (balance > 0.f) {
        //1-balance肯定是一个小于1的数字,mCurve是一个函数,可以把[0~1]范围的值映射到[0~1]
        *left = mCurve(1.f - balance);
        //右边音量取1.f,保持音频本身的音量
        *right = 1.f;
    //小于0,道理同上
    } else if (balance < 0.f) {
        *left = 1.f;
        *right = mCurve(1.f + balance);
    //等于0的话,说明平衡点在中间,左右都保持原本的音量输出
    } else {
        *left = 1.f;
        *right = 1.f;
    }
}

上面代码也很简单,可以看我写的注释,主要根据用户设置的balance进行设置:

  1. 小于0,说明平衡点在左侧,左边声道肯定取值1,保持音频本身音量输出,而右声道通过1-balance在用mCurve归一化映射函数映射到小于1的值
  2. 大于0,说明平衡点在右侧,方法同上

这里我认为mCurve(1.f-balance)这个算法不是唯一的,我们可以根据实际测试结果进行修改,比如按照上面的配置设置后,在balance小于0的情况下,right声道完全听不到声音,可以加一个基础音量等,这个改进方法就仁者见仁智者见智了

最后,把我们的计算好的音量保存到mVolumes成员中取即可,它是一个vector类型,每个的index代表这个通道/位置的音量值,一幅图总结如下:
在这里插入图片描述

附加知识点—mCurve函数如何确定的

在Balance头文件中有定义,如下:

explicit Balance(
            bool ramp = true,   //是否用于渐变音量
            std::function<float(float)> curve = [](float x) { return x * (x + 0.2f); }) //音量映射函数
        : mRamp(ramp)
        , mCurve(normalize(std::move(curve))) { }

以下是 f ( x ) = x ( x + 0.2 f ) f(x)=x(x+0.2f) f(x)=x(x+0.2f)的函数图像:
在这里插入图片描述

为什么要选取这种函数呢?为啥不默认一个 y = x y=x y=x这线性函数,估计可能是音量本身不是线性的,音量增加和测量结果分贝是一种非线性关系,更像对数函数的图像,这里 f ( x ) = x ( x + 0.2 ) 这种抛物线曲线也类似的,所以就选取这种函数了 f(x)=x(x+0.2)这种抛物线曲线也类似的,所以就选取这种函数了 f(x)=x(x+0.2)这种抛物线曲线也类似的,所以就选取这种函数了
其中,normalize函数如下:

template<typename T>
    static std::function<T(T)> normalize(std::function<T(T)> f) {
        const T f0 = f(0);
        //T(1)相当于使用构造函数,构造了一个对象T,其值为1
        const T r = T(1) / (f(1) - f0); //计算得到f(1)-f(0)差值与1的大小比

        if (f0 != T(0) ||  // must be exactly 0 at 0, since we promise g(0) == 0
        //numeric_limits是c++中表达个类型数的极值库,这里表示T类型,epsilon计算机可表达
        //的T类型最小正实数,比如T为float类型,则比较两个float是否相等,可以让二者相剪
        //差值小于等于epsilon即可;
        //进入第二个条件,也就是f0 = 0,r与1之间的差值绝对值,如果小于后者,说明函数f从入参[0,1]
        //可以正常映射到[0,1],如果大于,则f函数的映射范围则有可能大于1,或者小于1,没有无限接近于1
            fabs(r - T(1)) > std::numeric_limits<T>::epsilon() * 3) { // some fudge allowed on r.
                //r*(fx-f0)=T(1)*(fx - f0)/(f1-f0),也就是x在[0,1]范围内比例大小,乘1,最终结果肯定在
                //[0,1]之间
            return [f, f0, r](T x) { return r * (f(x) - f0); };
        }
        // no translation required.
        return f;
    }

假设normalize函数命令为g(x),它能保证我们最终的映射函数2个点:

  1. g(0)一定等于0
  2. g(x<1)的情况一定也是小于1的
    最终, g ( x ) g(x) g(x)的值在0~1范围内;
    算法如上代码:
  3. r可以理解为f函数在[0,1]两个点上的y值对比,假设 f ( x ) = x f(x)=x f(x)=x,那这个函数刚好满足 f ( 0 ) = 0 f(0)=0 f(0)=0且最终映射的值在y轴上也是在(0,1)
  4. 所以在if条件中
fabs(r - T(1)) > std::numeric_limits<T>::epsilon() * 3)

r − T ( 1 ) r-T(1) rT(1)的绝对值就是在判断这个f函数与 y = x y=x y=x线性函数相差多大,epsilon()函数理解为T类型值得最小正整数,乘3是为了放款比较范围;这里相减后的绝对值

  • 小于的话,就理解为r和T(1)理论相等,f函数无限逼近类似于 y = x y=x y=x的函数图像,
  • 大于的话,就理解r和T(1)不相等,f函数远离逼近类似于 y = x y=x y=x的函数图像,可能最终映射的y值比1大,或者比1小的太多,映射不合理
    理解如下这副图像:
    在这里插入图片描述

举例y=x线性函数可能不合理,上图中y=0.5x和y=2x和代码中epsilon()也是不相符的,只是为了能清楚解释这段代码的原理,随机选择的函数

最后这行代码r * (f(x) - f0)带入r的表达式,也就是 T ( 1 ) ∗ ( f ( x ) − f ( 0 ) ) f ( 1 ) − f ( 0 ) \frac{T(1)*(f(x)-f(0))}{f(1)-f(0)} f(1)f(0)T(1)(f(x)f(0))等比例映射值而已,最终结果肯定小于1的

音量应用到音频数据中去

音量分配到音频数据中就更简单了,遍历每个音频数据,遍历每个channel,依次乘以每个channel的音量即可

void Balance::process(float *buffer, size_t frames)
{
    ........
    if (mRamp) {
        if (mRampVolumes.size() != mVolumes.size()) {
            // If mRampVolumes is empty, we do not ramp in this process() but directly
            // apply the existing mVolumes. We save the balance and volume state here
            // and fall through to non-ramping code below. The next process() will ramp if needed.
            mRampBalance = mBalance;
            mRampVolumes = mVolumes;
        } else if (mRampBalance != mBalance) {
            if (frames > 0) {
                std::vector<float> mDeltas(mVolumes.size());
                //这里为啥要用1.f来作除法,估计是转换成float型,方便后续的计算
                const float r = 1.f / frames;
                for (size_t j = 0; j < mChannelCount; ++j) {
                        //通道j的开始音量mRampVolumes[j],最终音量mVolumes[j],乘r也就是除frames;
                        //最后mDeltas[j]就是通道j每一帧的音频增量
                    mDeltas[j] = (mVolumes[j] - mRampVolumes[j]) * r;
                }

                // ramped balance
                for (size_t i = 0; i < frames; ++i) {
                    const float findex = i;
                    for (size_t j = 0; j < mChannelCount; ++j) { // better precision: delta * i
                        //等号后面的和在乘*buffer,开始音量+音频帧数index乘增量
                        *buffer++ *= mRampVolumes[j] + mDeltas[j] * findex;
                    }
                }
          }
            mRampBalance = mBalance;
            mRampVolumes = mVolumes;
            return;
        }
    }
    for (size_t i = 0; i < frames; ++i) {
        //遍历每个通道,依次乘通道音量平衡值
        for (size_t j = 0; j < mChannelCount; ++j) {
            *buffer++ *= mVolumes[j];
        }
    }
}

上面代码很简单,主要就是ramp时渐变音量不好理解,只需要弄清ramp中的几个参数:
mRampVolumes[]:数组保存每个channel的开始音量值
mVolumes[]:数组保存每个channel最终的音量值
mDeltas[]:保存了起始音量到最终音量差值,除以音频帧数frames的值
最后*buffer++ *= mRampVolumes[j] + mDeltas[j] * findex就好理解了

扩展阅读

这里的balance把多个位置的平衡都归一化到一维上去了,那么如何改变如何扩展呢?

最常见的一个例子就是车上,四门四喇叭,设置平衡点在左前方,那么用户肯定喜欢左前方喇叭声音保持输出,其余三个喇叭适当降音;但是按照以上代码,会将后排两个喇叭都归一化到左右两个喇叭上,最终左侧喇叭声音音量一样大,右边两个喇叭一样小

思考几分钟,如何扩展呢???

扩展办法

以下是我理解的方案:

  1. 在setChannelMask的时候sideFromChannel数组的取值增加几个取值:0-front_left、1-front_right、2-back_left、3-back_right和4-center;
    这样处理后,就可以把声道channel转换为前后左右、中间几个位置了
  2. 在setBalance时,传入进来的参数就要发生变化:
setBalance(float balance)
转变为:
setBalance(float balanceX, float balanceY)
  1. compute计算每个位置音量可以按照xy坐标组成的象限来区分:
if(balanceXb、alanceX在第一象限)
    volume[front_left] = 1.f
    volume[front_right] = mCurve(1.f+balanceX)
    volume[back_left] = mCurve(1.f-volumeY)
    volume[back_right] = mCurve(1.f-volumeY)

back_right可能还需要在调整调整,这里只是阐明一个方案而已

ok,以上就是对Balance的理解!如有不正确的地方,可以在评论区指出

顺便说个事,有个盆友要找音频开发类的工作,有合适的可以推荐以下,地点成都;

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

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

相关文章

高考落幕,暑期西北行,甘肃美食等你来尝

高考结束&#xff0c;暑期来临&#xff0c;西北之旅成为许多人的热门选择。而来到甘肃&#xff0c;除了领略壮丽的自然风光和深厚的历史文化&#xff0c;甘肃特产和传统面点以其独特的风味和传统的制作工艺也为游客们带来了一场地道的甘肃美食体验。 平凉的美食&#x…

成立近30年,它如何找到政企采购突破点?

回看中国采购行业的发展&#xff0c;大致可以被分为四个阶段&#xff1a;上世纪90年代的传统采购时代、本世纪初的ERP采购时代、近10年的SRM采购时代以及2018年以来开启的数字化采购时代。近年来&#xff0c;大数据、人工智能和物联网的高速发展&#xff0c;为采购信息化提供底…

读书笔记-Java并发编程的艺术-第3章(Java内存模型)-第6节(final域的内存语义)

文章目录 3.6 final域的内存语义3.6.1 final 域的重排序规则3.6.2 写final 域的重排序规则3.6.3 读final 域的重排序规则3.6.4 final 域为引用类型3.6.5 为什么 final 引用不能从构造函数内“逸出”3.6.6 final 语义在处理器中的实现3.6.7 JSR-133 为什么要增强final 的语义 3.…

[知识点篇]《计算机组成原理》之计算机系统概述

1.1 计算机发展历程 世界上第一台电子数字计算机 1946年&#xff0c;ENIAC(Electronic Numerical Integrator And Computer)在美国宾夕法尼亚大学研制成功。性能低&#xff0c;耗费巨大&#xff0c;但却是科学史上的一次划时代的创新&#xff0c;奠定了电子计算机的基础&#x…

大语言模型(LLM)LangChain介绍

LangChain是一个利用大语言模型的能力开发各种下游应用的开源框架&#xff0c;它的核心理念是为各种大语言模型应用实现通用的接口&#xff0c;简化大语言模型应用的开发难度&#xff0c;主要的模块示意图为&#xff1a; Index&#xff1a;提供了各类文档导入、文本拆分、文本向…

Java 生成随机数的方法例子

前言 在实际开发中产生随机数的例子也是很普遍的,所以在程序中设计产生随机数操作很重要&#xff0c;这篇文章主要给大家介绍了关于Java随机数的几种获得方法&#xff0c;具有一定的参考价值。 一、Random 类 Random 类是从 JDK 1.0开始&#xff0c;它产生的随机数是伪随机数…

UML建模笔记

5个视图 设计。类&#xff0c;接口&#xff0c;对象如何协作。实现。组件&#xff0c;运行程序&#xff0c;文档关系。用例。用户功能期望。进程。并发与同步相关进程&#xff0c;线程。部署。部署到计算机。 建模目的 和客户共创追踪需求变更协同开发进度控制持续迭代测试生…

【SGX系列教程】(四)Intel-SGX 官方示例分析(SampleCode)——LocalAttestation

文章目录 一.LocalAttestation原理介绍1.1本地认证原理1.2 本地认证基本流程1.3 本地认证核心原理 二.源码分析2.1 README2.1.1 编译流程2.1.2 执行流程&#xff08;双进程执行 or 单进程执行&#xff0c;在后面执行部分有展示效果&#xff09;2.1.3 如何获取已签名的Enclave的…

青岛网站建设一般多少钱

青岛网站建设的价格一般会根据网站的规模、功能、设计风格等因素来定&#xff0c;价格会存在着一定的差异。一般来说&#xff0c;一个简单的网站建设可能在数千元到一万元之间&#xff0c;而一个复杂的大型网站建设可能会需要数万元到数十万元不等。所以在选择网站建设服务时&a…

DAY17-力扣刷题

1.相同的树 100. 相同的树 - 力扣&#xff08;LeetCode&#xff09; 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 class Solution {public…

守护你的每一步:揭秘电子厂劳保鞋的秘密

在电子厂的繁忙车间里&#xff0c;工友们忙碌的身影中&#xff0c;你是否注意到那一双双看似普通的劳保鞋&#xff1f;它们不仅承载着工人们辛勤的汗水&#xff0c;更是守护他们每一步安全的重要装备。今天&#xff0c;就让我们一起揭秘电子厂劳保鞋的秘密&#xff0c;看看它们…

一站式企业服务平台能够帮助企业解决哪些问题?

近年来一站式企业服务平台备受区域政府及园区管理者的青睐&#xff0c;充当着区域政府或园区的千里眼和顺风耳&#xff0c;可以用来捕捉与区域经济发展相关的信息&#xff0c;也可以用来倾听企业的诉求&#xff0c;更是成为了区域深抓企业服务的多面手。 同时&#xff0c;一站式…

【漏洞复现】学分制系统GetTimeTableData SQL注入

0x01 产品简介 学分制系统由上海鹏达计算机系统开发有限公司研发&#xff0c;是基于对职业教育特点和需求的深入理解&#xff0c;结合教育部相关文件精神&#xff0c;并广泛吸纳专家、学者意见而开发的一款综合性管理系统。系统采用模块化的设计方法&#xff0c;方便学校根据自…

Java对应C++ STL的用法

sort&#xff1a; 1&#xff1a;java.util.Arrays中的静态方法Arrays.sort()方法&#xff0c;针对基本数据类型和引用对象类型的数组元素排序 2&#xff1a;java.util.Collections中的静态方法的Collections.sort()方法&#xff0c;针对集合框架中的动态数组&#xff0c;链表&…

大数据开发如何管理项目

在面试的时候总是 会问起项目&#xff0c;那在大数据开发的实际工作中&#xff0c;如何做好一个项目呢&#xff1f; 目录 1. 需求分析与项目规划1.1 需求收集与梳理1.2 可行性分析1.3 项目章程与计划 2. 数据准备与处理2.1 数据源接入2.2 数据仓库建设2.3 数据质量管理 3. 系统…

2024年4家HTTP代理服务商网站最新测评

一、芝麻HTTP芝麻HTTP作为代理服务领域的佼佼者&#xff0c;其HTTP代理服务同样表现出色。凭借海量IP资源和高效稳定的性能&#xff0c;芝麻HTTP为用户提供了卓越的代理服务体验。 特点与优势 ① 海量IP资源&#xff1a;拥有庞大的代理IP池&#xff0c;确保用户能够随时获取到…

安装OpenHarmony编译库和工具集

一、搭建开发环境 1.1、Ubuntu搭建&#xff0c;参考 VMware完美安装Ubuntu20.04-CSDN博客文章浏览阅读286次&#xff0c;点赞5次&#xff0c;收藏3次。详细介绍了VMware下安装Ubuntu20.04https://blog.csdn.net/longyuzi/article/details/139935769 1.2、拉取OpenHarmony源码…

数据结构速成--查找

由于是速成专题&#xff0c;因此内容不会十分全面&#xff0c;只会涵盖考试重点&#xff0c;各学校课程要求不同 &#xff0c;大家可以按照考纲复习&#xff0c;不全面的内容&#xff0c;可以看一下小编主页数据结构初阶的内容&#xff0c;找到对应专题详细学习一下。 目录 …

核方法总结(四)——高斯过程回归学习笔记

一、定义 基于核方法的线性回归模型和传统线性回归一样&#xff0c;可以用未知数据进行预测&#xff0c;但不能确定 预测的可信度。在参考书第二章中可知&#xff0c;基于贝叶斯方法可以实现对未知数据依概率预测&#xff0c;进而可得到预测的可信度。这一方法中&#xff0c;通…

C++中的三大池:线程池,内存池,数据库连接池

C中有三大池&#xff0c;即我们常说的&#xff1a;线程池&#xff0c;内存池&#xff0c;数据库连接池。 一.线程池 多线程同时访问共享资源造成数据混乱的原因就是因为CPU的上下文切换导致&#xff0c;线程池就是为了解决此问题而生。 多线程常用的有&#xff1a;std::threa…