【iOS】iOS语音通话回音消除(AEC)技术实现

一、前言

在语音通话、互动直播、语音转文字类应用或者游戏中,需要采集用户的麦克风音频数据,然后将音频数据发送给其它终端或者语音识别服务。如果直接使用采集的麦克风数据,就会存在回音问题。所谓回音就是在语音通话过程中,如果用户开着扬声器,那么自己讲话的声音和对方讲话的声音(即是扬声器的声音)就会混在一起,如果没有消除对方的声音,那么对方听到的就是带有回音的声音,这样的声音就会有问题。因此采集麦克风数据后,必须要消除回音,才能得到良好的用户体验。

回音消除的英文专业术语叫Acoustic Echo Cancellation,简称AEC。如何实现回音消除,技术细节实现上是一个比较复杂的数学问题。一般手机厂商都提供了底层的回音消除技术实现,app只需要调用相关api即可。但是也不是简单的调用,需要对一些基本知识有一定的了解,比如手机上提供的音频框架有哪些、每个音频框架功能是什么、哪些框架是可以实现AEC的、实现AEC的API怎么调用、有什么注意事项等等都是我们需要关注和总结的。本文主要对iOS设备上如何实现回音消除的相关知识进行梳理。

二、iOS音频框架概览

iOS系统提供了五类音频框架,从上层到下层分别是Media Player、AV Foundation、OpenAL 、 Audio Toolbox、AudioUnit,如下图。iOS 中的所有音频技术都建立在音频单元(AudioUnit)之上。AudioUnit是所有音频框架的底层实现,像Media Player、AV Foundation、OpenAL 和 Audio Toolbox都是AudioUnit的包装,它们为特定任务提供专用和简化的 API。
在这里插入图片描述
基础的音频功能,例如录制和播放、音效,使用上层音频框架即可解决。只有当您需要最高要求的控制、性能或灵活性时,或者当您需要只能通过直接使用音频单元才能获得的特定功能(例如AEC)时,才需要使用AudioUnit。

当我们需要实现以下功能之一时,直接使用低层的AudioUnit,而不是通过更高级别的 API:

  • 具有低延迟的同步音频 I/O(输入和输出),例如 VoIP(互联网协议语音)应用程序
  • 响应式播放合成声音,例如音乐类型游戏或合成乐器
  • 使用特定的音频单元功能,例如声学回声消除、混合或音调均衡
  • 一种处理链架构,可让您将音频处理模块组装到灵活的网络中。这是 iOS 中唯一提供此功能的音频 API。

当前游戏中需要使用iOS音频框架来采集麦克风数据,iOS中能提供录音功能的API有AVAudioRecorder、AudioQueue以及AudioUnit。其中AVAudioRecorder只能将麦克风音频录制到文件里,而AudioQueue虽然可以实时获取麦克风音频数据,但是不提供AEC功能。因此必须选择AudioUnit来实现游戏场景中的录制音频同时消除回音。

三、AudioUnit的分类

PerposeAudio Units
Effect(效果器)iPod Equalizer(iPod均衡器)
Mixing(混音器)3D Mixer (3D立体声)
Multichannel Mixer (多通道混音器)
I/O(输入输出)Remote I/O (远端输入输出,连接麦克风和扬声器)
Voice-Processing I/O (同上,增加了回声抑制等特性,适于网络语音处理)
Generic Output (一般输出,输出到应用程序)
Format conversion(格式转换)Format Converter (格式转换器)

(一)、效果器单元

iOS 4 提供了一个效果器,即iPod Equalizer,内置 iPod 应用程序使用的是同一个均衡器。使用此音频单元时,您必须提供自己的UI。此音频单元提供一组预设均衡曲线,例如Bass Booster、Pop 和 Spoken Word。

(二)、混音器单元

iOS 提供了两个混音器单元。一种是3D Mixer(3D混音器) ,另一种是Multichannel Mixer(多通道混音器)。3D混音器单元是构建OpenAL的基础。在大多数情况下,如果您需要3D混音器单元的功能,最好的选择是使用OpenAL,它提供了更高级的 API,非常适合游戏应用程序。多通道混音器单元为任意数量的单声道或立体声流提供混音,并带有立体声输出。您可以打开或关闭每个输入,设置其输入增益,并设置其立体声声相位置。

(三)、输入/输出单元

iOS 提供了三个 I/O 单元,分别是Remote I/O、 Voice-Processing I/O以及Generic Output 。
1、Remote I/O单元是最常用的。它连接到设备的输入和输出音频硬件(例如麦克风和扬声器),让您可以低延迟访问各个输入和传出音频样本值。它提供硬件音频格式和您的应用程序音频格式之间的格式转换,通过包含的格式转换器单元实现。
2、Voice-Processing I/O单元通过添加用于 VoIP 或语音聊天应用的声学回声消除来扩展远程 I/O 单元。它还提供自动增益校正、语音处理质量调整和静音功能。
3、Generic Output单元不连接到音频硬件,而是提供一种将处理链的输出发送到您的应用程序的机制。您通常会使用通用输出单元进行离线音频处理。

(四)、格式转换器单元

iOS 4 提供了一个Format Converter unit,通常通过 I/O 单元间接使用。

AudioUnit由Type、SubType和Manufacturer ID(制造商ID)这三个部分唯一标识。如下表所求。

Name and descriptionIdentifier keysCorresponding four-char codes
Format Converter Unit (支持与线性 PCM 之间的音频格式转换)kAudioUnitType_FormatConverter
kAudioUnitSubType_AUConverter
kAudioUnitManufacturer_Apple
aufc
conv
appl
iPod Equalizer Unit(提供 iPod 均衡器的功能)kAudioUnitType_Effect
kAudioUnitSubType_AUiPodEQ
kAudioUnitManufacturer_Apple
aufx
ipeq
appl
3D Mixer Unit(支持混合多个音频流、输出声像、采样率转换等)kAudioUnitType_Mixer
kAudioUnitSubType_AU3DMixerEmbedded
kAudioUnitManufacturer_Apple
aumx
3dem
appl
Multichannel Mixer Unit(支持将多个音频流混合到一个流中)kAudioUnitType_Mixer
kAudioUnitSubType_MultiChannelMixer
kAudioUnitManufacturer_Apple
aumx
mcm
xappl
Generic Output Unit(支持与线性PCM格式相互转换;可用于启动和停止图形)kAudioUnitType_Output
kAudioUnitSubType_GenericOutput
kAudioUnitManufacturer_Apple
auou
genr
appl
Remote I/O 单元 Unit(连接到设备硬件以进行输入、输出或同步输入和输出)kAudioUnitType_Output
kAudioUnitSubType_RemoteIO
kAudioUnitManufacturer_Apple
auou
rioc
appl
Voice-Processing I/O Unit(具有I/O单元的特性,为双向通信增加了回声抑制)kAudioUnitType_Output
kAudioUnitSubType_VoiceProcessingIO
kAudioUnitManufacturer_Apple
auou
vpio
appl

从以上AudioUnit的分类可以看出,要实现回音消除必须使用Voice-Processing I/O 这个类型的AudioUnit。

四、AudioUnit内部架构

音频单元的各个部分被组织成Scope和Element,如图所示。调用函数来配置AudioUnit时,您必须指定Scope和Element以标识函数的特定目标。
在这里插入图片描述


配置或者控制AudioUnit的函数:

UInt32 busCount = 2;
 
OSStatus result = AudioUnitSetProperty (
    mixerUnit,
    kAudioUnitProperty_ElementCount,   // the property key
    kAudioUnitScope_Input,             // the scope to set the property on
    0,                                 // the element to set the property on
    &busCount,                         // the property value
    sizeof (busCount)
);

Scope是AudioUnit中的编程上下文。尽管名称Global Scope可能另有含义,但这些上下文从不嵌套。
Element是嵌套在音频单元范围内的编程上下文。当一个元素是输入或输出范围的一部分时,它类似于物理音频设备中的信号总线(bus)——因此有时也称为总线。这两个术语——Element和总线(Bus)——在音频单元编程中指的是完全相同的东西。本文档在强调信号流时使用“总线”,在强调音频单元的特定功能方面时使用“元素”,例如 I/O 单元的输入和输出元素。
您通过其零索引整数值指定一个元素(或总线)。如果设置适用于整个范围的属性或参数,请将元素值指定为0。
上图说明了音频单元的一种常见架构,其中输入和输出上的元素数量相同。然而,各种音频单元使用各种架构。例如,混频器单元可能有多个输入元件,但只有一个输出元件。
全局范围的Scope如上图底部所示,适用于整个音频单元,不与任何特定音频流相关联。它只有一个元素,即元素 0。一些属性,例如每个切片的最大帧数 ( kAudioUnitProperty_MaximumFramesPerSlice),仅适用于全局范围。

IO Unit的架构图如下:

在这里插入图片描述


一个IO单元恰好包含两个元素,虽然这两个元素是一个音频单元的一部分,但您的应用程序将它们主要视为独立的实体。例如属性kAudioOutputUnitProperty_EnableIO,您可以根据应用的需要来独立启用或禁用每个元素。
I/O 单元的元素 1 直接连接到设备上的音频输入硬件,在图中用麦克风表示。此硬件连接(在元素 1 的输入范围内)对您来说是不透明的。您对从输入硬件输入的音频数据的第一次访问是在元素 1 的输出范围内。
同样,I/O 单元的元素 0 直接连接设备上的音频输出硬件,如上图中的扬声器所示。您可以将音频传送到元素 0 的输入范围,但其输出范围是不透明的。

使用音频单元时,您经常会听到 I/O 单元的两个元素不是通过编号而是通过名称来描述的:
输入元素为元素1(助记符:“Input”单词中的字母“I”的外观类似于数字1)
输出元素为元素0(助记符:单词“Output”的字母“O”的外观类似于数字0)

五、AudioUnit工作流程

音频单元通常在称为音频处理图的封闭对象的上下文中工作,如图所示。在此示例中,您的应用程序通过一个或多个回调函数将音频发送到图中的第一个音频单元,并对每个音频单元进行单独控制。I/O 单元的输出或任何音频处理图中的最后一个音频单元直接连接到输出硬件。

在这里插入图片描述
(其中EQ即Equalizer Unit(均衡器单元)缩写。)

下图是一个经典的多通道混音器单元(Multichannel Mixer Unit)和Remote IO Unit组件成的音频处理图,用于混合播放两种合成声音。先将声音输送到调音台的两个输入总线。混音器输出进入 I/O 单元的输出元件,然后将声音输出到硬件。

在这里插入图片描述

六、使用AudioUnit实现回音消除

AudioUnit使用大致分为五个步骤:获取AudioUnit实例、设置音频参数(例如采样率、声道数等)、设置采集或输出回调、初始化AudioUnit、启动AudioUnit。
下面以Voice-Processing I/O Unit为例来说明如何实现录音时消除回音。

6.1、获取AudioUnit实例

有的情况下我们可以直接使用某一个AudioUnit即可完成我们的功能,有时候需要多个AudioUnit协同处理音频。
iOS 有一个 API 用于直接处理音频单元,另一个用于操作音频处理图用于多个AudioUnit协作处理音频。
要直接使用音频单元(配置和控制它们),请使用Audio Unit Component Services Reference中描述的功能。
要创建和配置音频处理图(音频单元的处理链),请使用Audio Unit Processing Graph Services Reference中描述的函数。

6.1.1 创建AudioComponentDescription用来标识AudioUnit
//AudioUnit描述
AudioComponentDescription ioUnitDescription;
 
//AudioUnit的主类型
ioUnitDescription.componentType = kAudioUnitType_Output;

//AudioUnit的子类型,支持回音消除
ioUnitDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO;

//AudioUnit制造商,目前只支持苹果
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;

//以下两个字段固定是0
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;

以上设置可以唯一标识一个AudioUnit,下面开始获取AU实例。

6.1.2 创建AudioComponent获取AudioUnit实例
//查找AudioComponent
//第一个参数传递NULL,告诉此函数使用系统定义的顺序查找匹配的第一个系统音频单元
AudioComponent foundIoUnitReference = AudioComponentFindNext (NULL,&ioUnitDescription);

//需要实例化的AudioUnit
AudioUnit ioUnitInstance;

//实例化AudioUnit
AudioComponentInstanceNew(foundIoUnitReference,&ioUnitInstance);

AudioComponent 即音频组件,代表一种类型的AudioUnit。它用来实例化一个AudioUnit,一个AudioComponent可以用来实例化多个AudioUnit,类似于面向对象编程中类与对象的关系。

6.1.3 也可以使用音频处理图(AUGraph)获取AudioUnit实例

当需要使用多个AudioUnit协同工作时需要使用AuGraph,只使用一个AudioUnit建议使用上面的方式。

// Declare and instantiate an audio processing graph
AUGraph processingGraph;
NewAUGraph (&processingGraph);
 
// Add an audio unit node to the graph, then instantiate the audio unit
AUNode ioNode;
AUGraphAddNode (
    processingGraph,
    &ioUnitDescription,
    &ioNode
);
AUGraphOpen (processingGraph); // indirectly performs audio unit instantiation
 
// Obtain a reference to the newly-instantiated I/O unit
AudioUnit ioUnit;
AUGraphNodeInfo (
    processingGraph,
    ioNode,
    NULL,
    &ioUnit
);

6.2 设置AudioUnit基本参数

接下来需要设置AudioUnit的基本参数,比如采样率、声道数、采样深度等。

    AudioStreamBasicDescription mAudioFormat;
    mAudioFormat.mSampleRate = 16000;//按照需要设置采样率,越大声音越精细,人声16000足够
    mAudioFormat.mFormatID = kAudioFormatLinearPCM;
    mAudioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    mAudioFormat.mReserved = 0;
    mAudioFormat.mChannelsPerFrame = 1;//声道数
    mAudioFormat.mBitsPerChannel = 16;//采样深度
    mAudioFormat.mFramesPerPacket = 1;//每个包有多少帧
    //每帧有多少字节
    mAudioFormat.mBytesPerFrame = (mAudioFormat.mBitsPerChannel / 8) * mAudioFormat.mChannelsPerFrame; // 每帧的bytes数2
    //每个包有多少字节
    mAudioFormat.mBytesPerPacket =  mAudioFormat.mFramesPerPacket*mAudioFormat.mBytesPerFrame;//每个包的字节数2
    
    UInt32 size = sizeof(mAudioFormat);
    
    //CheckError是检查错误码的函数,如果有异常,打印后面的字符串
    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Output,
                                    1,
                                    &mAudioFormat,
                                    size),
               "kAudioUnitProperty_StreamFormat of bus 1 failed");
    
    
    CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Input,
                                    0,
                                    &mAudioFormat,
                                    size),
               "kAudioUnitProperty_StreamFormat of bus 0 failed");

6.3 设置AudioUnit的回调

6.3.1 定义一个音频输入回调
//定义音频输入回调
OSStatus AudioInputCallback(void *inRefCon,
                            AudioUnitRenderActionFlags *ioActionFlags,
                            const AudioTimeStamp *inTimeStamp,
                            UInt32 inBusNumber,
                            UInt32 inNumberFrames,
                            AudioBufferList *__nullable ioData) {
                            
    AudioUnitRecorder *recorder = (__bridge AudioUnitRecorder *)inRefCon;
    
    AudioBuffer buffer;//创建音频缓冲
  
    UInt32 size = inNumberFrames * recorder->mAudioFormat.mBytesPerFrame;
    buffer.mDataByteSize = size; //定义缓冲大小
    buffer.mNumberChannels = 1; //声道数
    buffer.mData = malloc(size); //申请内存
    
    AudioBufferList bufferList;//创建缓冲数组
    bufferList.mNumberBuffers = 1;//只需要一个音频缓冲
    bufferList.mBuffers[0] = buffer;//给数组元素赋值
    
    OSStatus status = noErr;
    
    //调用AudioUnitRender函数获取麦克风数据,存入上面创建的AudioBufferList中。
    status = AudioUnitRender(recorder->remoteIOUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, &bufferList);
 
    //有异常则中断
    if (status != noErr) {
        printf("AudioUnitRender %d \n", (int)status);
        return status;
    }
    //这里将麦克风音频放入一个缓存区,提供给阿里云语音识别SDK
    if(recorder.isStarted){
        NSData *frame = [recorder _bufferPCMFrame:&buffer];//缓存PCM音频
        if(frame){
            //缓存满了就把数据交给阿里云SDK
            [recorder _handleVoiceFrame:frame];
        }
    }else{
        NSLog(@"WARN: audio, - recorder is stopped, ignoring the callback data %d bytes",(int)buffer.mDataByteSize);
    }
    //释放内存
    free(buffer.mData);
    return status;
}
6.3.2 给AudioUnit设置输入回调

采集麦克风数据需要设置输入回调:kAudioOutputUnitProperty_SetInputCallback
在回调里获取麦克风音频。当前需要采集麦克风音频,因此使用的是这个回调。

    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = AudioInputCallback;//AudioInputCallback是上面定义的回调函数
    callbackStruct.inputProcRefCon = (__bridge void *)(self);
    OSStatus status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Output, 0, &callbackStruct, sizeof(callbackStruct));
    //CheckError是检查错误码的函数,如果有异常,打印后面的字符串
    CheckError(status, "SetInputCallback error");

如果是播放一个音频文件或URL,需要设置渲染回调:kAudioUnitProperty_SetRenderCallback,在回调里提供音频流。

6.4 初始化AudioUnit

 CheckError(AudioUnitInitialize(remoteIOUnit),"AudioUnitInitialize error");

6.5 启动AudioUnit

CheckError(AudioOutputUnitStart(remoteIOUnit),"AudioOutputUnitStart error");

启动这个AudioUnit之后,就可以在回调里获取消除了设备回音的麦克风数据。

七、关于AudioSession(音频会话)

在使用AudioUnit之前,先要正确设置AudioSession,否则AudioUnit无法正常工作。

7.1 AudioSession工作机制

iOS是通过AudioSession来管理应用内、应用之间、设备级别的音频行为。如下图所示。

在这里插入图片描述

在使用系统音频功能之前,需要告诉系统打算如何在您的应用程序中使用音频。AudioSession充当您的应用程序和操作系统之间的中介,进而是底层音频硬件。您可以使用它向操作系统传达应用音频的性质,而无需详细说明特定行为或所需的与音频硬件的交互。将这些细节的管理委托给音频会话可确保对用户的音频体验进行最佳管理。

7.2 AudioSession使用步骤

通过AVAudioSession实例来与应用的AudioSession进行交互。
使用步骤如下:

  1. 配置AudioSessionCategory(音频会话类别)和Mode(模式)以向系统传达您打算如何在您的应用程序中使用音频
  2. 激活您应用的音频会话以将您的类别和模式配置生效
  3. 订阅并响应重要的音频会话通知,例如音频中断和Route更改(外设插拔)
  4. 进行高级音频设备配置,例如设置采样率、I/O 缓冲时间和声道数
    例如,当前场景需要使用播放音频时同时进行录音,因此按照如下方式设置AudioSession。
//获取AudioSession实例
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
     
//设置音频行为是播放和录音同时进行,默认只允许播放。
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
    
//缓冲时间0.02秒,这个决定了AudionUnit音频回调时的inNumberFrames的大小。
[audioSession setPreferredIOBufferDuration:0.02 error:nil];

//激活音频会话
[audioSession setActive:YES error:nil];
    
//监听耳机变化
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:)   name:AVAudioSessionRouteChangeNotification object:audioSession];

录音同时播放时需要使用这个Category:AVAudioSessionCategoryPlayAndRecord,它默认会从听筒输出音频,因此需要添加打开扬声器的option:AVAudioSessionCategoryOptionDefaultToSpeaker,并添加允许中途插入耳机的option:AVAudioSessionCategoryOptionAllowBluetooth

关于AudioSessionCategory的种类及行为特点,可以参考:

AudioSessionCategory的种类及行为特点

八、一些经验及注意事项

  • 录音同时播放音频场景,AudioSession的Category需要设置成:AVAudioSessionCategoryPlayAndRecord。
  • AVAudioSessionCategoryPlayAndRecord,音频默认会从听筒播放,强制开启扬声器需要再添加option:AVAudioSessionCategoryOptionDefaultToSpeaker。
  • AVAudioSessionCategoryOptionDefaultToSpeaker,如果添加了这个开启扬声器的option,戴上蓝牙耳机声音仍然从扬声器输出(有线耳机不影响),需要再添加option:AVAudioSessionCategoryOptionAllowBluetooth。
  • 录音同时播放音频场景下开启AEC,需要先播放再录音,不然声音会从听筒输出,并且AEC不生效。不开AEC时声音正常。
  • 摘下耳机,如果录音和播放没有停止,音频会变成听筒输出,无法再开启扬声器。建议在监听到耳机摘下后,先停止录音,隔一段时间比如2s再打开录音。如果有更好的解决方式欢迎留言。
  • AVAudioSessionCategoryPlayback支持后台播放和锁屏播放音频,另外还需要在Capabilities->Background Modes中勾选“Audio,AirPlay,and Picture in Picture”。
  • 录音同上,如果没有勾选这个,app切到后台会自动停止录音。

参考文献:
https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/AudioUnitHostingGuide_iOS/Introduction/Introduction.html

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

九龙证券|这一刻,资本市场进入全新时代!

2023年4月10日,第一批10家主板注册制企业上市鸣锣敲钟,奏响了触及本钱商场灵魂深处革新的序曲。 动能切换中的我国对于高效资源配置的渴望,与革新进行时的本钱商场对于全面注册制的探究,一起凝集成一股连绵有力之暖流,…

2023年最强手机远程控制横测:ToDesk、向日葵、Airdroid三款APP免Root版本

前言 随着远程办公和远程协作的日益普及,跨设备、系统互通的远程控制软件已经成为职场人士不可或缺的工具之一。在国内,向日葵和ToDesk是最著名的远程控制软件;而在国外,则有微软远程桌面、AirDroid、TeamViewer、AnyDesk、Parse…

【华为机试真题详解JAVA实现】—Sudoku

目录 一、题目描述 二、解题代码 一、题目描述 问题描述:数独(Sudoku)是一款大众喜爱的数字逻辑游戏。玩家需要根据9X9盘面上的已知数字,推算出所有剩余空格的数字,并且满足每一行、每一列、每一个3X3粗线宫内的数字均含1-9,并且不重复。 例如: 输入 输出

Faster-RCNN代码解读2:快速上手使用

Faster-RCNN代码解读2:快速上手使用 前言 ​ 因为最近打算尝试一下Faster-RCNN的复现,不要多想,我还没有厉害到可以一个人复现所有代码。所以,是参考别人的代码,进行自己的解读。 ​ 代码来自于B站的UP主(…

汽车电子相关术语介绍

一、相关术语介绍 1、汽车OTA 全称“Over-The-Air technology ”,即空中下载技术,通过移动通信的接口实现对软件进行远程管理,传统的做法到4S店通过整车OBD对相应的ECU进行软件升级。OTA技术最早2000年出现在日本,目前通过OTA方式…

FusionCharts Suite XT v3.20.0 Crack

FusionCharts Suite XT v3.20.0 改进了仪表的径向条形图和调整大小功能。2023 年 4 月 11 日 - 9:37新版本特征 添加了一个新方法“_changeXAxisCoordinates”,它允许用户将 x 轴更改为在图例或数据交互时自动居中对齐。更新了 Angular 集成以支持 Angular 版本 14 …

【微信小程序-原生开发】添加自定义图标(以使用阿里图标库为例)

方式一 &#xff1a; 下载svg导入 优点&#xff1a;操作方便&#xff0c;支持多彩图标缺点&#xff1a;会增加源代码大小 下载 svg 格式的图标图片&#xff0c;放入源码中使用 小程序项目中的路径为 assets\icon\美食.svg 使用时-代码范例 <image class"imgIcon"…

前端开发工具-Visual Studio Code-插件下载-迁移到新电脑

背景 前端使用的开发工具一般是Visual Studio Code&#xff0c;很多辅助功能&#xff0c;比如字体高亮、单词拼写检查、预览图片等需要安装插件。但是插件在原来的电脑&#xff0c;不想下载或者自己是新人&#xff0c;想迁移同事的插件&#xff0c;或者新电脑没有外网。 以下…

图解HTTP阅读笔记:第4章 返回结果的HTTP状态码

《图解HTTP》第四章读书笔记 图解HTTP第4章&#xff1a;返回结果的HTTP状态码4.1 状态码告知从服务器端返回的请求结果4.2 2XX成功4.2.1 200 OK4.2.2 204 No Content4.2.3 206 Parital Content4.3 3XX重定向4.3.1 301 Moved Permanently4.3.2 302 Found4.3.3 303 See Other4.3.…

OK-3399-C ADB烧录

ADB烧写 一、OK3399用户资料工具目录附带了ADB工具的资料包路径&#xff1a; 二、将其解压在C:\User目录 三、将设备通过type-c线download口与电脑相连接&#xff0c;打开命令行&#xff0c;进入解压的目录&#xff0c;查看adb是否安装成功&#xff1a; 四、安装成功后&#x…

spring-boot怎么扫描不在启动类所在包路径下的bean

前言&#xff1a; 项目中有多个模块&#xff0c;其中有些模块的包路径不在启动类的子路径下&#xff0c;此时我们怎么处理才能加载到这些类&#xff1b; 1 使用SpringBootApplication 中的scanBasePackages 属性; SpringBootApplication(scanBasePackages {"com.xxx.xx…

在proteus中仿真arduino实现矩阵键盘程序

矩阵键盘是可以解决我们端口缺乏的问题&#xff0c;当然&#xff0c;如果我们使用芯片来实现矩阵键盘的输入端口缺乏的问题将更加划算了&#xff0c;本文暂时不使用芯片来解决问题&#xff0c;而使用纯朴的8根线来实现矩阵键盘&#xff0c;目的是使初学者掌握原理。想了解使用芯…

# 切削加工形貌的相关论文阅读【1】-球头铣刀铣削球面的表面形貌建模与仿真研究

切削加工形貌论文【1】-球头铣刀铣削球面的表面形貌建模与仿真研究1. 论文【2】-球头铣刀加工表面形貌建模与仿真1.1 切削加工形貌仿真-考虑的切削参数1.2 其他试验条件1.3 主要研究目的1.4 试验与分析结果1.5 面粗糙度的评价指标2. 论文【1】-球头铣刀加工球面&#xff08;曲面…

Vue3.0中的响应式原理

回顾Vue2的响应式原理 实现原理&#xff1a; - 对象类型&#xff1a;通过 Object.defineProperty()对属性的读取、修改进行拦截&#xff08;数据劫持&#xff09;。 - 数组类型&#xff1a;通过重写更新数组的一系列方法来实现拦截。&#xff08;对数组的变更方法进行了包裹&…

nacos源码服务注册

nacos服务注册序言1.源码环境搭建1.1idea运行源码1.2 登录nacos2.服务注册分析2.1 客户端2.1.1容器启动监听2.1.2注册前初始化2.1.3注册服务2.2 服务端2.2.1注册2.2.2重试机制3.注意事项序言 本文章是分析的是nacos版本2.2 这次版本是一次重大升级优化&#xff0c;由原来&#…

浅析DNS Rebinding

0x01 攻击简介 DNS Rebinding也叫做DNS重绑定攻击或者DNS重定向攻击。在这种攻击中&#xff0c;恶意网页会导致访问者运行客户端脚本&#xff0c;攻击网络上其他地方的计算机。 在介绍DNS Rebinding攻击机制之前我们先了解一下Web同源策略&#xff0c; Web同源策略 同源策略…

微前端--qiankun原理概述

demo放最后了。。。 一、微前端 一》微前端概述 微前端概念是从微服务概念扩展而来的&#xff0c;摒弃大型单体方式&#xff0c;将前端整体分解为小而简单的块&#xff0c;这些块可以独立开发、测试和部署&#xff0c;同时仍然聚合为一个产品出现在客户面前。可以理解微前端是…

【从零开始学Skynet】基础篇(八):简易留言板

这一篇我们要把网络编程和数据库操作结合起来&#xff0c;实现一个简单的留言板功能。 1、功能需求 如下图所示&#xff0c;客户端发送“set XXX”命令时&#xff0c;程序会把留 言“XXX”存入数据库&#xff0c;发送“get”命令时&#xff0c;程序会把整个留言板返回给客户端。…

HarmonyOS/OpenHarmony应用开发-Stage模型ArkTS语言Ability基类

Ability模块提供对Ability生命周期、上下文环境等调用管理的能力&#xff0c;包括Ability创建、销毁、转储客户端信息等。 说明: 模块首批接口从API version 9 开始支持。模块接口仅可在Stage模型下使用。 导入模块: import Ability from ohos.app.ability.Ability; 接口说明…

如何利用python机器学习解决空间模拟与时间预测问题及经典案例分析

目录 专题一 机器学习原理与概述 专题二 Python编译工具组合安装教程 专题三 掌握Python语法及常见科学计算方法 专题四 机器学习数据清洗 专题五 机器学习与深度学习方法 专题六 机器学习空间模拟实践操作 专题七 机器学习时间预测实践操作 更多 了解机器学习的发展历…