音视频开发之旅——音频基础概念、交叉编译原理和实践(LAME的交叉编译)(Android)

news2024/11/27 8:23:50

本文主要讲解的是音频基础概念交叉编译原理和实践(LAME的交叉编译),是基于Android平台,示例代码如下所示:

AndroidAudioDemo

音频基础概念

在进行音频开发的之前,了解声学的基础还是很有必要的。

声音的物理性质

在初中物理的时候学过,声音是由三要素组成:音调响度音色

音调

声音的高低叫做音调。物体振动得越快,发出声音的音调就越高;物体振动得越慢,发出的音调越低。频率过零率,指信号的符号变化的比率)决定了音调,频率越高,波长越短,声音更容易绕过障碍物,也就是能量衰减越小,反之得到相反的结论。

响度

声音的强弱叫做响度。我们可以一般用分贝(dB)来描述响度,分贝越大,声音响度越大,反之得到相反的结论。

音色

声音的品质叫做音色,它反映了每个物体发出的声音特有的品质。例如在同样的音调和响度下,吉他和钢琴的声音听起来是不同的,也就是音色是不同的。波的形状决定声音的音色,吉他和钢琴音色不同就是因为它们介质产生的波形不同。

业界来说,人耳能够听到频率范围大约为20Hz20kHz**,对**3kHz4kHz频率范围内的声音比较敏感,对于较低或者较高频率的声音,人耳的敏感度会减弱;在分贝较低时,听觉的频率特性会很不均匀,反之就会较为均匀。一个频率范围较宽的音乐,最佳的分贝范围为80dB~90dB超过90dB就会损害人耳,105dB是人耳的极限。

声音在不同的介质传播的速度也会不一样,在空气中的传播速度为340m/s,不过在真空是无法传播的。

有时候我们在空旷的地方或者高山大喊的时候,会听到回声(echo),产生回声的原因是声音在传播的过程中遇到障碍物后反弹回来后再次让我们听到,但是如果这两种声音传回到我们耳朵的时差小于80毫秒的话,我们就无法分辨这两种声音。

音频数字化

将声音模拟信号转换为数字信号的过程称之为音频数字化,这里需要经过三个步骤:采样量化编码

采样

首先对模拟信号进行采样,采样是指在时间轴(横轴)对信号进行数字化,根据奎斯特定理(采样定理,我们要按比声音最高音频高两倍以上的频率对声音进行采样,这个过程也称为AD转换。上面提过的人耳能够听到的频率为20Hz~20kHz,所以一般采样频率为44.1kHz,也就是说1秒会采样44100次

量化

上面提到的,具体每个采样需要怎样处理呢?这就需要量化,量化是指在幅度轴(纵轴)上对信号进行数字化,要注意的是,和上面提到的采样形成平面直角坐标系,举个例子:用16bit的二进制信号表示这个声音的一个采样,16bit等于一个short,表示范围为[-32768, 32767],也就是说有65536个可能取值,所以在幅度上分为65536层。

编码

最后一步就是要将采样的数据进行存储,也就是是需要进行编码,编码就是按照一定的格式记录采样和量化后的数据数据,例如:顺序存储压缩存储等等。常用的格式为音频的裸数据格式,也就是脉冲编码调制(Pulse Code Modulation,简称PCM)。描述一段PCM的数据需要这几个概念:采样率(sampleRate)量化格式(sampleFormat,也称为位深度)声道数(channel)比特率用于衡量音频数据单位时间内的容量大小,也就是一秒时间内的比特数目,我们以常见的CD格式和DVD-Audio格式为例子:

CD格式的采样率为44100Hz,量化格式为16bit(2byte),声道数为2,那么它的比特率为:

44100 * 16 * 2 = 1411200bps

转换可得1411200bps / 1024 = 1378.125Kibps

DVD-Audio格式的采样率为96000Hz,量化格式为24bit(3byte),声道数为6.那么它的比特率为:

96000 * 24 * 6 = 13824000bps

转换可得13824000bps / 1024 = 13500Kibps,再转换可得13500Kibps / 1024 ≈ 13.18Mibps

一般来说一首歌曲的时间大概在4分钟左右,那我们算下CD格式和DVD-Audio格式会占用多大的存储空间,如下所示:

CD格式:1411200bps * 4 * 60 = 338688000b,转换可得338688000b / 8 / 1024 / 1024 ≈ 40.37MiB

DVD-Audio格式:13824000bps * 4 * 60 = 3317760000b,转换可得3317760000b / 8 / 1024 / 1024 = 395.51MiB

由数据可得,DVD-Audio格式一秒时间内的比特数目大于CD格式,因此它的音质会更好,当然所占的储存空间也会相应得大。

压缩编码

由上面可以看到一首歌如果仅仅是已CD格式去存储的已经占用了40.37MiB,如果只是存储在存储设备上(例如:硬盘或者光盘)那还可以接受,但是如果在网络上实时在线传输的话,这样的大小实在是太大了,所以我们需要对其进行压缩编码,压缩编码里有个指标叫做压缩比,压缩比是小于1,压缩比越小(越接近0),丢失的信息就越多,反之得出相反的结论。压缩算法有两种:无损压缩有损压缩。无损压缩是指解压后的数据能够复原;有损压缩是指解压后的数据不能够复原,压缩导致的丢失得越多,还原的失真就越大。

有如下常用的压缩编码格式:

WAV编码

WAV(Waveform Audio File Format)是微软专门为Windows开发的一种编码格式,它会在PCM数据格式的前面加上44字节,分别用来描述该PCM数据的采样率、声道数、量化格式。

优点:音质非常好,有大量软件支持。

缺点:占用的存储空间较大。

适用场合:多媒体开发的中间文件、音乐和音效素材。

MP3编码

MP3(MPEG-1或者MPEG-2 Audio Layer III)是一种有损压缩的编码格式,它通过舍弃PCM数据人类听觉不重要的部分,已达到压缩成较小文件的目的,对于大多数用户来说,它的音质和不压缩的音频没有明显的下降。我们常用LAME编码MP3文件,下面会讲解到。

优点:音质在**高码率(≥128Kbit/s)**表现不错,同时压缩比也比较高;有大量硬件和软件支持,兼容性不错。

适用场合:高码率(≥128Kbit/s)的音频。并且需要比较好的兼容性。

AAC编码

AAC(Advanced Audio Coding,高级音频编码)是一种高压缩比的编码格式,由于采用多声道和使用低复杂性的描述方式,使其比几乎所有的传统编码方式在同规格的情况下更胜一筹。目前衍生出LC-AACHE-AAC v1HE-AAC v2三种主要的编码格式。LC-AAC是比较传统的AAC,主要编码中高码率(≥80Kbit/s)的音频;HE-AAC v1是高效AAC,是对AAC的扩展,它使用频段复制(SBR)提高频域的压缩效率,适用于中低码率(≤80Kbit/s);HE-AAC v2结合使用了**频段复制(SBR)参数立体声(PS)提高立体声信号的压缩效率,进一步降低了对码率的需要(接近于50%),主要编码低码率(≤48Kbit/s)**的音质。大部分编码器都设置为≤48Kbit/s自动启用PS,>48Kbit/s就关闭PS,箱单与HE-AAC v1。

优点:音质在**中低码率(<128Kbit/s)**表现优异,多用于视频中音频轨的编码。

适用场合:中低码率(<128Kbit/s)的音频,多用于视频中音频轨的编码。

Ogg编码

Ogg在各种码率下都有优秀的表现,尤其在中低码率的场景表现不错,同时它不收到软件专利的限制,完全免费。Ogg有着非常出的的算法,可以用更小的码率编码出更好的音质,举个例子:128Kbit/s的Ogg音质甚至比192Kbit甚至更高的MP3还要好。

优点:可以用更小的码率编码出更好的音质,在各种码率下都变现优异。

缺点:目前兼容性不够好,流媒体特性不支持。

适用场合:语音聊天的音频消息。

Android平台增加C和C++支持

Android提供了一种编译框架,叫做JNI(Java Native Interface),用于允许运行于JVM的Java或者Kotlin代码去调用本地代码(C、C++、汇编语言)。大概步骤为将相关的C/C++代码放在项目模块的cpp目录下,在构建项目的时候,Gradle会将这些代码和应用的代码一起打包到原生库,然后Java或者Kotlin代码就可以通过JNI去调用原生库中的函数。什么时候需要用到JNI呢?有以下几种情况:

  • 应用程序需要一些平台的特性支持,但是Java层没有提供相应的API支持,例如:OpenSL ES的使用)

  • 调用一些已经存在并且已是成熟方案的C/C++库,例如:使用LAME编码MP3文件、使用FFmpeg处理音频或者视频、使用OpenGL ES处理视频特效。

  • 应用程序对部分逻辑的运行速度有较高的要求,那么这部分就可以用C/C++实现,再通过JNI向Java层提供访问接口。

我们需要以下组件:

  • Android原生开发套件(NDK):这是一套可以让开发者使用C/C++的工具。

  • ndk-build脚本或者CMake:用于构建原生库。

  • LLDB:Android Studio用于调试原生代码的程序,默认情况下,它会随同Android Studio的安装而安装。

这里讲解下ndk-build脚本CMake的区别,在讲解之前,我们要了解下下面的内容:

GNU、GCC、gcc、g++

  • GNU:它是一个完全自由的操作系统,起源于GNU计划。

  • GCC:GNU Compiler Collection(GNU编译器套件)的缩写,它是一组GNU操作系统中的编译器集合,可以用于编译C、C++、Java、Go等语言。

  • gcc:GCC中的GNU C Compiler(C编译器)。

  • g++:GCC中的GNU C++ Compiler(C++编译器)。

对于.c文件和.cpp文件,gcc会分别当作c文件和cpp文件编译,而g++会统一当作cpp文件编译。

编译C/C++的四个步骤

接下来我们要了解一下使用gcc(GNU Compiler Collection,GNU编译器套件)生成可执行二进制文件的大概过程:

预处理(Preprocess)

预处理(Preprocess):预处理会处理一些编译前的准备工作,把一些#define的宏定义完成文本替换,然后将#include里的文件复制到.cpp文件,如果.h文件里还有.h文件,那么就会递归展开,要注意的是,在这一步中,代码注释会被忽略。通过g++ -E命令将.c文件预处理为.i文件,它是文本文件。

编译(Compile)

编译(Compile):编译是把代码转换成汇编代码,同时检查词法规则和语法规则,如果没有出现语法错误,那么不管逻辑是否错误都不会报错。通过g++ -S命令将.i文件转换为.s文件,它是文本文件。

汇编(Assemble)

汇编(Assemble):汇编是把汇编代码(.s文件)转换为机器码。通过g++ -c命令将.s文件转换为.o文件(目标文件),它是二进制格式。

链接(Link)

C/C++代码经过汇编后生成的.o文件(目标文件),它是二进制文件,但是它不是最终可执行的,需要和系统组件(例如:标准库、动态链接库)链接起来才能得到可执行的二进制文件(Executable File),完成这个过程的组件叫做链接器(Linker)。链接分为静态链接动态链接,生成的文件叫做静态库动态库

静态库

静态库在Linux下为.a文件,在Windows下为.lib文件。之所以称之为静态库,是因为在链接阶段会将.o文件和引用到的库一起链接打包到可执行文件中,它有如下特点:

  • 会在编译时期完成静态库对函数库的链接。

  • 程序在运行的时候与函数无关,方便移植。

  • 会浪费一定的空间和资源,因为所有目标文件和涉及到的函数库被链接合成一个可执行文件。

动态库

动态库在Linux下为.so文件,在Windows下为.dll文件。动态库在程序编译时不会被链接到目标文件,而是在程序运行时才被载入,它有如下特点:

  • 把对一些库函数的链接载入推迟到程序运行的时期。

  • 不同的应用程序如果调用相同的库,那么在内存中只需要有一份该共享库的实例,可以实现进程之间的资源共享,节省了空间。

  • 由于动态库是在程序运行的时候才载入,因此解决了静态库对程序的更新、部署和发布带来的麻烦,只需要更新动态库就可以了,即增量更新

  • 开发者可以在程序代码中控制链接载入,即显示调用

Make

Make其实是一个批量处理的工具,它是通过调用makefile文件中开发者指定的命令来进行编译链接,例如调用gcc或者其他编译器的命令。上面提的ndk-build脚本就是使用NDK基于Make来构建项目。

CMake

如果是简单的工程makefile手写起来还是比较轻松的,但是如果复杂的工程手写起来就比较麻烦了,换平台还要重新修改,所以就有上面提到的CMake。CMake是一个开源的跨平台自动化建构系统,它可以根据CMakeList.txt文件自动生成makefile文件来给上面的Make工具使用。它也是目前Android Studio编译NDK默认构建工具,当然也可以使用上面提到的ndk-build脚本,官方也是支持的。

本机编译

我们要在PC上运行一个二进制的程序(要注意的是,是以源码的方式进行编译,而不是以包管理器的方式去安装)会经过如下步骤:

  1. 得到这段程序的源代码,它可以是自己编写的源代码,也可以是从第三方开源网站上下载的源代码。

  2. 在PC上编译链接这些源代码生成可执行文件。

  3. 在终端(Terminal)下执行该可执行文件。

总结就是使用本机器的编译器和链接器,将源代码编译链接成一个可以在本机器运行的程序,这个编译过程叫做本机编译,它是正常的编译过程。

交叉编译

了解完本机编译后,交叉编译就好理解了,它就是一个平台(例如:PC)上生成另外一个平台(例如:Android、iOS、其他嵌入式设备)可执行的程序。这里的编译机器是PC,所以编译器是安装在PC上,并且运行在PC上的,而这个编译器叫做交叉工具编译链。那其实为啥需要交叉编译呢?因为运行程序的目标平台运算能力和存储能力都是有限的,尽管现在iOS和Android设备的性能越来越强劲,但是和PC还是有一定的距离,而且ARM平台下的编译工具和整个编译过程异常繁琐,所以PC是最佳选择。目前大部分的嵌入式开发平台都提供本身平台交叉编译所需要运行在PC上的交叉工具编译链。

在所有的编译器中,包括自行安装在PC上的编译器和嵌入式平台的交叉工具编译链,都包含以下这几个工具:

  • CC:编译器,作用是对C或者C++源文件编译成汇编文件。

  • AS:将汇编文件翻译成机器码,生成目标文件,汇编文件使用的是指令助记符。

  • AR:打包器,它可以从一个库增加或者删除目标代码模块。

  • LD:链接器,作用是为前面生成的目标代码分配地址空间,将多个目标文件链接成一个库或者可执行文件。

  • GDB:调试工具,它可以对正在运行的程序进行代码调试。

  • STRIP:消除最终生成的库文件或者可执行文件其中的源码。

  • NM:查看静态库文件中符号表。

  • Objdump:查看静态库或者动态库的方法签名。

在Android的NDK提供的交叉工具编译链就在开发者用到的ndk下的prebuilt/darwin-x86_64/bin路径中,它提供了上面这些工具。我的路径如下所示:

/Users/tanjiajun/Library/Android/sdk/ndk/26.1.10909125/prebuilt/darwin-x86_64/bin

LAME的交叉编译

我们了解完交叉编译后,以LAME库为例进行实践。

先介绍一下LAME库,它是目前最优秀也是最常用的MP3编码引擎。当码率达到320Kbit/s以上的时候,LAME编码出来的音频质量几乎可以和CD音质媲美,并且还能保证其文件体积非常小,因此如果要在移动端编码MP3文件,使用LAME是唯一选择。

下面来讲解下,在Android平台下如何交叉编译LAME库,并且打印LAME版本。

Android Studio准备工作

首先我们的Android Studio要下载好NDK,然后新建一个Android项目,右键模块点击“Add C++ to Module”,上面也提及过,Android Studio编译NDK的默认构建工具是CMake,所以我们会看到添加完毕后会有相关的代码和文件生成,例如生成了cpp文件夹和里面相关的cpp文件和CMakeList.txt文件。

下载LAME库并解压,复制到项目中

然后在SourceForge下载最新版本的LAME库,目前为3.100,点击下面文本即可下载:

lame-3.100.tar.gz

下载完成后,解压文件得到lame-3.100文件夹,然后找到libmp3lame文件夹,把里面的.c和.h文件全部复制到Android Studio项目生成的cpp文件夹下,我这边会新建一个lame的文件夹来存放这些文件,这样看起来目录会整洁点,我这边写了个的Python脚本用于把文件夹里的.c和.h文件复制到指定的目录,同时还会打印复制后的绝对路径。我已经把它push到示例代码中,文件名为CopySpecifiedFiles.py,代码如下所示:

import os
import shutil

oldDir = '/Users/tanjiajun/lame-3.100/libmp3lame'
newDir = '/Users/tanjiajun/StudioProjects/AndroidAudioDemo/app/src/main/cpp/lame'
cExtension = '.c'
hExtension = '.h'

if not os.path.exists(newDir):
    os.makedirs(newDir)
for file in os.listdir(oldDir):
    extension = os.path.splitext(file)[-1]
    if extension == cExtension or extension == hExtension:
        shutil.copy(os.path.join(oldDir, file), newDir)
        print(os.path.join(newDir, file))

然后再找到上面提到的lame-3.100文件夹下的include文件夹,把里面的lame.h文件同样复制到Android Studio的lame文件夹。

编译项目

接下来,我们需要修改CMakeList.txt文件,利用上面脚本打印的绝对路径,修改后代码如下所示:

cmake_minimum_required(VERSION 3.22.1)

project("audiodemo")

add_library(
        ${CMAKE_PROJECT_NAME}
        SHARED
        audiodemo.cpp
        lame/reservoir.c
        lame/mpglib_interface.c
        lame/machine.h
        lame/fft.h
        lame/set_get.c
        lame/quantize_pvt.h
        lame/psymodel.h
        lame/newmdct.c
        lame/id3tag.h
        lame/lame-analysis.h
        lame/id3tag.c
        lame/reservoir.h
        lame/lameerror.h
        lame/set_get.h
        lame/quantize.c
        lame/fft.c
        lame/l3side.h
        lame/newmdct.h
        lame/quantize.h
        lame/gain_analysis.c
        lame/encoder.c
        lame/lame.c
        lame/bitstream.c
        lame/quantize_pvt.c
        lame/presets.c
        lame/bitstream.h
        lame/encoder.h
        lame/gain_analysis.h
        lame/lame_global_flags.h
        lame/psymodel.c
        lame/lame.h
        lame/tables.c
        lame/tables.h
        lame/takehiro.c
        lame/util.c
        lame/util.h
        lame/vbrquantize.c
        lame/vbrquantize.h
        lame/VbrTag.c
        lame/VbrTag.h
        lame/version.c
        lame/version.h
)

target_link_libraries(
        ${CMAKE_PROJECT_NAME}
        android
        log
)

然后我们make project,build完后以下文件会报错,这里列出解决的办法:

util.h

在570行,如图所示:

util_error.png

解决办法:

extern float fast_log2(float x);

fft.c

在47行,如图所示:

fft_error.png

解决办法:删除该行代码。

set_get.h

在24行,如图所示:

set_get_error.png

解决办法:

#include "lame.h"

其他错误

如图所示:

lame_error.png

解决办法:修改app模块中的build.gradle.kts文件,在android函数中增加如下代码:

android {
    ...

    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cFlags("-DSTDC_HEADERS")
            }
        }
    }

    ...
}

修改后,再次make project后编译成功,CMake会帮我们自动生成libaudiodemo.so文件,过程上面也提过了,这里就不再赘述。

打印LAME库的版本

新建MainActivity,这里我使用Compose写界面,并且在AndroidManifest.xml添加相关的代码,AndroidManifest.xml的代码我就不贴了,详细可查看该demo。调用getLameVersion函数就能获取当前LAME版本,要注意的是,需要调用**System.loadLibrary(“audiodemo”)**把生成的动态库加载进来。代码如下所示:

/**
 * Created by TanJiaJun on 2023/12/13.
 */
class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ContentView()
        }
    }

    @Composable
    private fun ContentView() {
        ConstraintLayout(modifier = Modifier.fillMaxSize()) {
            val (topBox, lameVersionText) = createRefs()
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(45.dp)
                    .background(Purple80)
                    .constrainAs(topBox) {
                        start.linkTo(parent.start)
                        top.linkTo(parent.top)
                        end.linkTo(parent.end)
                    }
            ) {
                Text(
                    modifier = Modifier.align(Alignment.Center),
                    text = packageManager.getApplicationLabel(applicationInfo).toString(),
                    fontSize = 18.sp,
                    color = White
                )
            }
            Text(
                modifier = Modifier.constrainAs(lameVersionText) {
                    start.linkTo(parent.start)
                    top.linkTo(topBox.bottom)
                    end.linkTo(parent.end)
                    bottom.linkTo(parent.bottom)
                },
                text = getLameVersion(),
                fontSize = 16.sp,
                color = Black
            )
        }
    }

    /**
     * 获取当前LAME版本
     *
     * @return 当前LAME版本
     */
    private external fun getLameVersion(): String

    private companion object {
        init {
            System.loadLibrary("androidaudiodemo")
        }
    }

}

运行后,我们就可以看到界面有个居中的3.100文本,这就是目前编译的LAME版本,代表我们编译LAME库成功。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:谭嘉俊

我的简书:谭嘉俊

我的CSDN:谭嘉俊

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

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

相关文章

Redis之事务(详细解析)

请直接看原文:不能回滚的Redis事务还能用吗 - 知乎 (zhihu.com) ------------------------------------------------------------------------------------------------------------------------------ 1、Redis事务的概念: Redis 事务的本质是一组命令的集合。…

HCIA-Datacom题库(自己整理分类的)_38_IPv6多选【20道题】

1.IPV6地址包含以下哪些类型? 任播地址 组播地址 单播地址 广播地址 2.IPv6有以下哪几种地址? 任播地址 链路本地地址 组播地址 单播地址 广播地址 3.下面关于IPv6描述正确的是? IPv6的地址长度为128bits IPv6的地址长度为64bit…

android插件化开发指南,字节跳动安卓开发面试题

Android进阶延伸点 1、如何进行单元测试,如何保证App稳定 ? 参考回答: 要测试Android应用程序,通常会创建以下类型自动单元测试 本地测试:只在本地机器JVM上运行,以最小化执行时间,这种单元测…

1.Zookeeper理论基础

1.Zookeeper的基本概念 是一个分布式应用协调框架 ,java编写的。客户端 /服务端 的架构模式。CP设计(一致性,分区容错) 它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:服务注册服务、状态同步服务、集群管理、分布…

对于爬虫的学习

本地爬取 package MyApi.a08regexdemo;import java.util.regex.Matcher; import java.util.regex.Pattern;public class RegexDemo03 {public static void main(String[] args) {//要求:找出里面所有javaxxString str"Java自从95年问世以来,经历了…

爬虫逆向网站案例

一、相关网页 东方财富人气排行榜 二、查找url 三、寻找curl并复制 四、打开Convert curl commands to code (curlconverter.com) 五、修改并执行代码 import requestscookies {st_si: 73974981954644,st_pvi: 39724919122964,st_sp: 2024-03-05%2018%3A27%3A22,st_inirUrl:…

基于java springboot+redis网上水果超市商城设计和实现以及文档

基于java springbootredis网上水果超市商城设计和实现以及文档 博主介绍:多年java开发经验,专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留…

算法Day04_203.移除链表元素

推荐阅读 算法day01_ 27. 移除元素、977.有序数组的平方 算法day02_209.长度最小的子数组 算法day03_ 59.螺旋矩阵II 目录 推荐阅读203.移除链表元素题目思路解法暴力解法虚拟头结点解法 203.移除链表元素 题目 给你一个链表的头节点 head 和一个整数 val ,请你删…

深度学习预测分析API:金融领域的Game Changer

🚀 引言 在这个AI遍地开花的时代,谁能成为金融领域的真正Game Changer?那必然是是深度学习预测分析API。如大脑般高效运转的系统不仅颠覆了传统操作,更是以无与伦比的速度和精度赋予了金融数据以全新的生命。 💼 广泛…

flutter小程序开发,Android高级工程师必备知识

AWTK 主要特色: 1、跨平台 AWTK 是跨平台的,这有两个方面的意思: AWTK 本身是跨平台的。目前支持的平台有 ZLG AWorks、Windows、Linux、MacOS、嵌入式 Linux、Android、Web 和嵌入式裸系统,可以轻松的移植到各种 RTOS 上。AWT…

缓存一致性:数据库操作与Redis事务回滚

缓存一致性:数据库操作与Redis事务回滚 今日开发需求是“保证数据库操作与Redis缓存操作的一致,在CRUD出错时,保证回滚”。 问题: 1、数据库开启事务,即可在操作失误时,回滚之前的已经做过的操作&#xff…

厚膜功率电阻基板选择

根据电阻器性能规格和应用的全部细节,专业制造商将选择最合适的厚膜功率电阻器基板技术,以适当的成本提供可靠的解决方案。 正确的电阻基板材料选择可以最大限度地提高散热效果并减小元件尺寸。这必须与材料成本和制造过程中的潜在复杂性进行权衡。 基板…

制作Python游戏全过程(汇总2)

目录 前言: 1.基于pygame库创建游戏模块: 1.1引入模块 1.1.1 这是对代码的解释: 1.1.1.1pygame 是一个用于创建游戏的 Python 库。 1.1.1.2plane_sprites 是一个自定义模块,从day6文件夹(也可以是其他的文件夹)中导入,它可能包含了游戏中使用的所有…

Android 消息恢复 - 如何在 Android 上检索已删除的短信

最新调查显示,手机每天发送和接收的短信数以亿计,尤其是Android智能手机。但与此同时,Android消息丢失也每天都在发生。因此,如何恢复Android手机上已删除的短信对于那些在设备中保存了一些重要信息的人来说似乎非常重要。 在这里…

【组合递归】【StringBuilder】Leetcode 17. 电话号码的字母组合

【组合递归】【StringBuilde】Leetcode 17. 电话号码的字母组合 StringBulider常用方法!!!!!!!!!!!!!!17. 电…

第六篇:人工智能与机器学习技术VS数据迁移(Data Migration)--- 我为什么要翻译介绍美国人工智能科技巨头IAB公司?

(source: 图片来自麻省理工官网) IAB平台,使命和功能 IAB成立于1996年,总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司,互动广告局(IAB- the Interactive Advertising Bureau)…

Unreal Engine5记录 01安装

1.下载Epic Games 启动器 官网下载地址 传送门https://www.unrealengine.com/zh-CN 点击下载启动程序,完成之后选择指定的路径安装即可。 2.登录或注册一个账户 多种登录方式,选择一种登录即可。 3.找到虚幻引擎下载列表 选择想要的版本进行下载&…

LeetCode——二叉树(Java)

二叉树 简介[简单] 144. 二叉树的前序遍历、94. 二叉树的中序遍历、145. 二叉树的后序遍历二叉树层序遍历[中等] 102. 二叉树的层序遍历[中等] 107. 二叉树的层序遍历 II[中等] 199. 二叉树的右视图[简单] 637. 二叉树的层平均值[中等] 429. N 叉树的层序遍历[中等] 515. 在每个…

微信小程序开发系列(十七)·事件传参·mark-自定义数据

目录 步骤一:按钮的创建 步骤二:按钮属性配置 步骤三:添加点击事件 步骤四:参数传递 步骤五:打印数据 步骤六:获取数据 步骤七:父进程验证 总结:data-*自定义数据和mark-自定…

Doris实战——金融壹账通指标中台的应用实践

目录 前言 一、业务痛点 二、早期架构挑战 三、架构升级 四、一体化指标数据平台 4.1 构建指标体系 4.2 构建指标平台功能 五、Doris指标应用实践 六、未来规划 原文大佬的这篇指标中台的应用实践有借鉴意义,这里摘抄下来用作学习和知识沉淀。 前言 在搭建…