macOS coreAudio 之 AudioQueue 播放本地音频文件

news2024/11/27 5:32:46

macOS的音频模块使用还是和 iOS有细微差别的。

今天记录是的是 使用 AudioQueue 配合 AudioFile 进行播放macOS 本地音频文件

本文打仓库代码为: JBPlayLocalMusicFile.m

CoreAudio 作为Apple音频系统中音频库的集合,今天需要使用到的库为:

  • AudioQueue 位于 <AudioToolbox/AudioQueue.h>, 作为输出模块,输入音频到系统默认扬声器
  • AudioFile 位于 <AudioToolbox/AudioFile.h>, 读取本地音频文件,然后将读取的Buffer塞入 AudioQueue的音频队列中,等待系统扬声器宠幸。

读取和播放的大致流程为:在这里插入图片描述

1. 前置说明

全局设置两个值分别为

//每个缓冲区0.5秒的数据量
#define kBufferDurationInSeconds 0.5
//分配三个缓冲区
#define kNumberBuffer 3
  • kBufferDurationInSeconds 代表AudioQueue 每个缓冲区存储 0.5秒时间的数据,最后我们代码里面会通过计算转换成0.5秒时间的数据量的 内存控件
  • kNumberBuffer 代表 AudioQueue 中缓冲区的数量,我们知道至少需要两个缓冲区,其中一个采集数据的缓冲区A, 另外一个是 回调函数的缓冲区B。 当采集的数据达到0.5秒的内存大小后,A缓冲区会将所有数据转移到B缓冲区,然后A继续采集,B在回调函数中供用户使用。但是考虑数据的连贯性,如果哪个环节出问题,会导致数据A缓冲区不能及时进行数据采集,所以需要一个备用的缓冲区C来进行应急。

2. 辅助宏

播放demo只负责调通逻辑和了解学习API,并没有处理错误情况,我们将所有的错误只做了log输出,

#include <TargetConditionals.h>

//负责将 OSStatus 转成 fourcc
#if TARGET_RT_BIG_ENDIAN
#   define FourCC2Str(fourcc) (const char[]){*((char*)&fourcc), *(((char*)&fourcc)+1), *(((char*)&fourcc)+2), *(((char*)&fourcc)+3),0}
#else
#   define FourCC2Str(fourcc) (const char[]){*(((char*)&fourcc)+3), *(((char*)&fourcc)+2), *(((char*)&fourcc)+1), *(((char*)&fourcc)+0),0}
#endif

#define printErr(logStr, status) \
    if (status != noErr) {\
        NSLog(@"==== 出现错误: %@ code: %d(%s)", logStr, (int)status, FourCC2Str(status));\
    }

日志输入大概这酱紫
CoreAudioDemo[37280:4192311] ==== 出现错误: AudioFileGetProperty kAudioFilePropertyPacketSizeUpperBound code: -50()

3. 类初始化与全局变量

- (void)start; start 为入口函数
- (void)stop stop 为结束函数
在初始化方法中监听 外部的 通知,进行 stop的调用

- (instancetype)init {
    self = [super init];
    _aspds = NULL;
    _isDone = FALSE;
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(stop) name:JBStopNotification object:nil];
    return  self;
}

类声明的变量

@interface JBPlayLocalMusicFile()
{
@public
	//这些C的struct 并没有 get set 方法,不能设置为 property, 所以直接设置为成员变量
    AudioFileID _audioFile;
    AudioQueueRef _mQueue;
    AudioStreamPacketDescription *_aspds; //从文件中读取的包秒数,每次读取后将其传入 AudioQueue中,以便能正确解码和播放
}

@property (nonatomic, assign) AudioStreamBasicDescription mASBD;
@property (nonatomic, assign) BOOL isDone; //是否播放完毕
@property (nonatomic, assign) UInt32 byteSizeInBuffer; //缓冲区应有的字节数(0.5秒内)
@property (nonatomic, assign) UInt32 packetsNumInBuffer; // 缓冲区应对应的数据包数量(0.5秒内)
@property (nonatomic, assign) Float64 readOffsetOfPackets; //读取了多少 packets
@end

4. 打开本地音频文件

start函数内首先调用openAudioFile, _audioFile 为声明的 @public 的成员变量
AudioFileID _audioFile;
这里我们使用AudioFileOpenURL函数 只读权限打开 flac 类型的音频,并绑定到 _audioFile 的指针句柄中,以后我们操作 这个文件就通过这个全局成员变量进行控制。

//打开 音频 文件
- (void)openAudioFile{
    NSURL *audioURL  = [[NSBundle mainBundle] URLForResource:@"G_E_M_ 邓紫棋 - 句号" withExtension:@"flac"];
    OSStatus status =  AudioFileOpenURL((__bridge  CFURLRef)audioURL, kAudioFileReadPermission, 0, &(_audioFile));
    printErr(@"AudioFileOpenURL", status);
}

5. 获取音频文件里面的 基本流信息

在上一步中我们打开了 音频文件,并获取到了指向它的句柄,现在我们操控_audioFile, 从文件中获取 AudioStreamBasicDescription的信息。
AudioStreamBasicDescription 作为 Apple 描述音频流的基本格式,里面的值的含义就不具体说明了,调用[JBHelper printASBD:asbd];这个辅助函数能够打印处 ASBD 的具体值信息。
值得说明的是,由于我们读取的是 flac 格式,不是 PCM 这种线性的裸数据, 所以某些值是空的以0表示,因为可能是VBR(可变比特率)等,需要我们在后面的步骤进行进一步提取。

在这个方法中中我们 将取出的 AudioStreamBasicDescription 保存到全局变量 self.mASBD 中。

- (void)getASBDInFile {
    /***
     mp3 flac 文件格式, 和PCM 有点差别
2023-07-21 14:58:34.893822+0800 CoreAudioDemo[3193:5836480] planar:false bitsPerchannel:0
2023-07-21 14:58:34.894105+0800 CoreAudioDemo[3193:5836480] flags: 
	kAppleLosslessFormatFlag_16BitSourceData
	kAudioFormatFlagIsFloat
2023-07-21 14:58:34.894247+0800 CoreAudioDemo[3193:5836480] 
ASBD: 
	mSampleRate = 44100
	mFormatID = 1718378851(flac)
	mFormatFlags = 1
	mBytesPerPacket = 0
	mFramesPerPacket = 4096
	mBytesPerFrame = 0
	mChannelsPerFrame = 2
	mBitsPerChannel = 0
	mReserved = 0
     */
    AudioStreamBasicDescription asbd;
    UInt32 asbdSize = sizeof(asbd);
    OSStatus status = AudioFileGetProperty(_audioFile, kAudioFilePropertyDataFormat, &asbdSize, &asbd);
    printErr(@"AudioFileOpenURL", status);
    [JBHelper printASBD:asbd];
    self.mASBD = asbd;
}

6. 初始化 AudioQueue

在获取了文件中的 ASBD 后,我们就可以初始化 AudioQueue 了。

  • 第一个参数为 我们在上一步获取的 _mASBD 的引用
  • 第二个参数为 回调函数的名称,实际上 AudioQueue 会在合适的时候 自动对该函数进行回调,这也是 C 语言常用的回调模式,该回调我们会在 后面进行详细介绍。
  • 第三个参数为 第二参数回调函数 里面作为参数回传回来的值,这样我们可以在回调函数回调回来的时候访问我们特定的类的实例,这里桥接成 void * 传入
  • 第四第五为 特定的Runloop 模式的值,可以不指定
  • 第六参数,为预留参数,只能传 0
  • 最后一个参数 _mQueue 传入全局变量 AudioQueueRef 的地址, 只有传入上一级的地址,才能改变当前的地址,二级指针的操作。

这样我们把 作为 输出 Output 模式的 AudioQueue 的回调函数绑定好了,并且将 AudioQueue也初始化了,

    //init audio queue by output param
    OSStatus status = AudioQueueNewOutput(&_mASBD,
                                          jbAudioQueueOutputCallback,
                                          (__bridge void *)self,
                                          NULL,
                                          NULL,
                                          0,
                                          &_mQueue);
    printErr(@"AudioQueueNewOutput", status);

7. 将文件中的 Encoder Magic Cookie 拷贝到 音频队列中

magic cookieCoreAudio表示附加到音频流中的元数据(metadata), 能够为正常的解码文件和流提供必要的信息。

某些音频格式的文件,在播放过程中必须要有 Magic Cookie 的数据才能正常的解码播放。
经本地测试 mp3文件读取不出Magic Cookie , 而flac格式的文件能够正常读取出来。
所以我们这里 会尝试读取一次,读取不到就 返回,如果能够读取到就直接传入 AudioQueue 中去。

具体过程详看代码和注释

// MP3获取不到 magic cookie
- (void)setFileMetaDataToAudioQueue{
    //先获取长度
    UInt32 cookieDataSize = 0;
    UInt32 isWriteAble = 0;
    //注意这里是AudioFileGetPropertyInfo, 获取长度和是否可以写
    OSStatus status = AudioFileGetPropertyInfo(_audioFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, &isWriteAble);
    
    //有些没有 magic cookie ,所以不管
    if (status != noErr) {
        NSLog(@"magic cookie 不存在,忽略掉");
        return;
    }
    
    if (cookieDataSize <= 0) {
        NSLog(@"AudioFileGetPropertyInfo kAudioFilePropertyMagicCookieData get zero size data");
        return;
    }
    
    //根据长度获取对应的magic data 的内容
    Byte *cookieData = malloc(cookieDataSize *sizeof(Byte));
    //这里是AudioFileGetProperty
    status = AudioFileGetProperty(_audioFile, kAudioFilePropertyMagicCookieData, &cookieDataSize, cookieData);
    printErr(@"AudioFileGetProperty kAudioFilePropertyMagicCookieData", status);
    
    //将获取的MagicCookie 设置到 AudioQueue 中
    status = AudioQueueSetProperty(_mQueue, kAudioQueueProperty_MagicCookie, cookieData, cookieDataSize);
    printErr(@"AudioQueueSetProperty kAudioQueueProperty_MagicCookie", status);
    
    // malloc 后必须 free
    free(cookieData);
}

8. 计算 每次读取 需要的大小和包的数量

我们现在开始计算 每个缓冲区的大小和包的数量
这里我们在代码中 为 两个全局变量赋值

@property (nonatomic, assign) UInt32 byteSizeInBuffer; //缓冲区应有的字节数(0.5秒内)
@property (nonatomic, assign) UInt32 packetsNumInBuffer; // 缓冲区应对应的数据包数量(0.5秒内)

了解了这个函数是为获取上面两个值后,就可以带着目的的去看里面的函数了。
主要是考虑到 _mASBD.mBytesPerPacket 是否为 0 的两种 case,所以代码比较复杂,需要进行两种判断。

- (void)calculateSizeOfTime{
    
    //获取kBufferDurationInSeconds时间的包的数量
    UInt32 totalNumerOfPackets = 0;
    if (_mASBD.mFramesPerPacket > 0) {
        //每次时间间隔内需要收集的样本数量
        Float64 totalNumberOfSamples =  _mASBD.mSampleRate * kBufferDurationInSeconds;
        UInt32 totalNumberOfFrames = ceil(totalNumberOfSamples); //将数据向上取整
        totalNumerOfPackets = totalNumberOfFrames / _mASBD.mFramesPerPacket;
    } else {
        // 如果mFramesPerPacket==0,则编解码器在给定时间内没有可预测的数据包大小。
        // 在这种情况下,我们将假设在给定持续时间内最多有 1 个数据包来调整缓冲区大小
        totalNumerOfPackets = 1;
    }
    
    UInt32 packetSizeUpperBound = 0;
    UInt32 packetSizeUpperBoundSize = sizeof(packetSizeUpperBound);
    // 获取 计算出来的 理论上的 最大 package 大小。非读取文件
    OSStatus status = AudioFileGetProperty(_audioFile,
                                           kAudioFilePropertyPacketSizeUpperBound,
                                           &packetSizeUpperBoundSize,
                                           &packetSizeUpperBound);
    printErr(@"AudioFileGetProperty kAudioFilePropertyPacketSizeUpperBound", status);
    
    if (_mASBD.mBytesPerPacket > 0) {
        //设置具体值
        self.byteSizeInBuffer = self.mASBD.mBytesPerPacket * totalNumerOfPackets;
    } else {
        // 获取理论上最大值
        self.byteSizeInBuffer = packetSizeUpperBound * totalNumerOfPackets;
    }
    
    //定义一个最大值,以避免 RAM 消耗过大
    //并定义一个最小值,以确保我们有一个可以在播放时没有问题的缓冲区。太小了会频繁连续从文件读取 IO 消耗比较大
    const int maxBufferSize = 0x100000; // 128KB
    const int minBufferSize = 0x4000;  // 16KB
    //调整成一个中间的适合的值
    if(self.byteSizeInBuffer > maxBufferSize) {
        self.byteSizeInBuffer = maxBufferSize;
    } else if (self.byteSizeInBuffer < minBufferSize) {
        self.byteSizeInBuffer = minBufferSize;
    }
    
    //调整后重新计算大小, 这样可能多分配内存,但至少不会内存越界
    self.packetsNumInBuffer = self.byteSizeInBuffer / packetSizeUpperBound;
}

9. 获取额外的 AudioStreamPacketDescription 信息

由于我们是非 PCM 的数据,所以获取了上面的数据后,还不能知己开始数据采集,还需要 获取 除了 AudioStreamBasicDescription 这种音频流信息外,还需要获取 Packet 的基本信息。
_aspdsAudioStreamPacketDescription 的数组, 包含了 _packetsNumInBuffer 个元素

具体见代码和注释

/**
 如果音频基本流描述没有告诉任何有关每个数据包的字节数或每个数据包的帧的信息,
 那么我们就会遇到 VBR 编码或通道大小不等的 CBR 的情况。
 在任何这些情况下,我们都必须为额外的数据包描述分配缓冲区,
 这些描述将在处理音频文件并将其数据包读入缓冲区时填充。
 */
- (void)allocPacketArray {
    BOOL isNeedASPD = _mASBD.mBytesPerPacket == 0 || _mASBD.mFramesPerPacket == 0;
    if(isNeedASPD) {
    	//calloc能够将 开辟的内存自动 设为0
        _aspds = (AudioStreamPacketDescription *)calloc(sizeof(AudioStreamPacketDescription), _packetsNumInBuffer);
    } else {
        _aspds = NULL;
    }
}

10. 开辟AudioQueue 队列

在我们前面完成一些必要的数据计算和AudioQueue的配置,现在可以进行AudioQueue的内存申请和配置了。

我们这里使用了kNumberBuffer3个缓冲区,并将其buffers 保存到这个数组里面,注意buffers 不使用引用计数,所以在函数返回后并不会析构,所以这个使用的局部变量。

  • 这里我们使用了AudioQueueAllocateBuffer 来开辟了 self.byteSizeInBuffer 这么多Byte的内存,并关联到_mQueue中,并对内存里面的值进行合适的赋值。最终将开辟的内存的首地址保存到 buffers[i] 变量中
  • jbAudioQueueOutputCallback 这里开辟内存后手动调用了一下这个回调函数,是为了在回调中读取一次 0.5 秒的文件数据,来进行填充我们刚刚开辟出来的内存。
  • self.isDone 代表着,上一步回调中就把文件的数据读完了,或者出错了,直接break,然后再 上一级方法中 调用stop 函数。

三次循环,代表着开辟出的三个缓冲队列都按我们的要求,填充完毕。也意味着我们现在的内存中包含了 0.5 * 3 = 1.5秒的数据。是时候在下一步调用 Audio start 方法,正式开始播放了.

- (void)allocAudioQueue {
    AudioQueueBufferRef buffers[kNumberBuffer];
    
    OSStatus status = noErr;
    for(int i = 0 ; i< kNumberBuffer; i++) {
        status = AudioQueueAllocateBuffer(_mQueue,
                                          self.byteSizeInBuffer,
                                          &buffers[i]);
        printErr(@"AudioQueueAllocateBuffer", status);
        
        //手动调用回调,用音频文件中的音频数据填充缓冲区。
        //后续调用AudioQueueStart后,会自动触发回调进行调用
        jbAudioQueueOutputCallback((__bridge  void *)self, _mQueue, buffers[i]);
        if (self.isDone) {
            //回调函数中设置为true后,代表剩余时间小于1.5秒
            break;
        }
    }
}

11. 正式播放

前面所有的都配置齐全后就可以直接启动 音频队列了。这时候就可以在默认扬声器里面听到我们的音乐声音了。
启动后 音频队列会先消耗上一步,我们手动塞入队列缓冲区的样本。
然后会在特定的时间回调调用 jbAudioQueueOutputCallback 这个函数。

status = AudioQueueStart(_mQueue, NULL);
printErr(@"AudioQueueStart", status);

12. 回调函数

我们前面所有的都讲了,现在开始理一理回调函数里面究竟做了啥。
首先定义这个函数使用了C的Static 静态函数。

  • inUserData形参为我们在AudioQueueNewOutput里面传入的参数,这个原封不动的回传了回来
  • inAQ对音频队列的引用, 我们不关心是3个队里中的哪一个,系统会自动调度
  • inBuffer我们将文件中的音频数据读取后,需要将数据写入这个内存区域,然后系统会自动解码播放。

这里的主要流程为:

  • 获取JBPlayLocalMusicFile 实例对象
  • AudioFileReadPacketData从文件中读取音频数据,并会填充_aspds这个音频格式。最后numberBytes,numberPackets这两个本地变量的值会被改变成实际本地从文件里面读取的长度和包数量
  • 如果前面无误的话,需要将上一步获取的数据,通过调用AudioQueueEnqueueBuffer 填充到 音频队列的缓冲区中,音频队列会按照以前设置的音频流格式,和本地包的特定的_aspds数据进行综合解码和播放。
static void jbAudioQueueOutputCallback(void * inUserData,
                                       AudioQueueRef inAQ, //对音频队列的引用
                                       AudioQueueBufferRef inBuffer //需要填充的缓冲区播放数据的引用
) {
    
    JBPlayLocalMusicFile *playClass = (__bridge JBPlayLocalMusicFile *)inUserData;
    if (playClass->_isDone) {
        return;
    }
    
    // 存在局部变量中后,read数据的时候会 自动更新读取到的值
    UInt32 numberBytes = playClass.byteSizeInBuffer;
    UInt32 numberPackets = playClass.packetsNumInBuffer;
    
    //读取音频包内容,并在最后一个字段中将读取到的数据填充到 inBuffer 中去
    OSStatus status = AudioFileReadPacketData(playClass->_audioFile,
                                              false,
                                              &numberBytes,
                                              playClass->_aspds,
                                              playClass.readOffsetOfPackets,
                                              &numberPackets,
                                              inBuffer->mAudioData);
    printErr(@"AudioFileReadPacketData", status);
    if (numberBytes <= 0 || numberPackets <= 0) {
        NSLog(@"数据读取完毕");
        playClass->_isDone = true;
        dispatch_async(dispatch_get_main_queue(), ^{
            [playClass stop];
        });
        return;
    }
    
    inBuffer->mAudioDataByteSize = numberBytes;
    AudioQueueEnqueueBuffer(inAQ,
                            inBuffer,
                            (playClass->_aspds ? numberPackets : 0),
                            playClass->_aspds);
    //消费完后,更新下次需要读取的文件的位置
    playClass.readOffsetOfPackets += numberPackets;
}

13. 停止音频

意外停止和正常播完停止都需要调用这个函数。

  • AudioQueueStop停止队列
  • AudioQueueDispose 处理音频队列,这里会处置其中的资源和缓冲区等,包括里面会自动调用AudioQueueAllocateBuffer对应的析构函数AudioQueueFreeBuffer
  • _aspds 如果有,就释放
  • AudioFileClose最后关闭文件。
- (void)stop {
    
    OSStatus status = AudioQueueStop(_mQueue, true);
    printErr(@"AudioQueueStop", status);
    status = AudioQueueDispose(_mQueue, true);
    printErr(@"AAudioQueueDispose", status);
    if(_aspds) {
        free(_aspds);
    }
    status = AudioFileClose(_audioFile);
    printErr(@"AudioFileClose", status);
    self.isDone = true;
    NSLog(@"播放结束");
}

本文地址: https://blog.csdn.net/goldWave01/article/details/131834259 😄

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

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

相关文章

力扣热门100题之三数之和【中等】

题目描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请 你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组…

ChatGPT 最佳实践指南

GPT Best Practices GPT 最佳实践指南 This guide shares strategies and tactics for getting better results from GPTs. The methods described here can sometimes be deployed in combination for greater effect. We encourage experimentation to find the methods that…

Java类的封装

封装将类的某些信息隐藏在类内部&#xff0c;不允许外部程序直接访问&#xff0c;只能通过该类提供的方法来实现对隐藏信息的操作和访问。 例如&#xff1a;一台计算机内部极其复杂&#xff0c;有主板、CPU、硬盘和内存&#xff0c; 而一般用户不需要了解它的内部细节&#xff…

涤生大数据教学集群的首次运维现场复现

事故背景交代 涤生大数据花费重金购得几台较高配置的阿里云服务器机器&#xff0c;构建了一整套以cdh为核心的大数据课程教学、学员实操练习环境、但是&#xff0c;就是这个但是&#xff0c;以为集群规模目前相对较小。不会有什么幺蛾子发生&#xff0c;于是运维中最核心的监控…

[Linux] CentOS7 中 pip3 install 可能出现的 ssl 问题

由于解决问题之后, 才写的博客, 所以没有图片记录. 尽量描述清楚一些 今天写代码的时候, 突然发现 文件里用了#define定义宏之后, coc.nvim的coc-clangd补全就用不了 :checkhealth了一下, 发现nvim忘记支持python3了 尝试pip3 install neovim的时候, 发现会警告然后安装失败.…

设计模式结构型——代理模式

目录 代理模式的用途 代理模式的实现 静态代理 JDK动态代理 CGLIB动态代理 代理模式的特点 与其他模式比较 代理模式&#xff08;Proxy Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许通过创建一个代理对象来间接访问原始对象。代理模式的核心思想是将对目…

20230721在WIN10下安装openssl并解密AES-128加密的ts视频切片

20230721在WIN10下安装openssl并解密AES-128加密的ts视频切片 2023/7/21 22:58 1、前言&#xff1a; AES-128加密的ts视频切片【第一个】&#xff0c;打开有时间限制的&#xff01; https://app1ce7glfm1187.h5.xiaoeknow.com/v2/course/alive/l_64af6130e4b03e4b54da1681?typ…

小鹏G6吹响汽车智能化普惠号角

监制 | 何玺 排版 | 叶媛 小鹏G6大卖。目前其订单的交车周期已经长达12周。这款主打智能化的“未来之车”&#xff0c;已经正式吹响了汽车智能化普及的号角。 01 订单排满&#xff0c;小鹏G6成“爆款” 7月11日&#xff0c;小鹏汽车董事长何小鹏在社交媒体上发了一张照片&am…

Flink笔记

Flink笔记 2.Flink学习笔记2.1流式处理对比2.2 Flink核心概念2.2.1并行度2.2.2算子链2.2.3任务槽 2.3 DataStream2.3.2 读取数据源-源算子&#xff08;Source&#xff09;2.3.3 转换算子&#xff08;Transformation&#xff09; 2.Flink学习笔记 2.1流式处理对比 学习Spark S…

Django设置权限管理

目录 整体思路 1.使用django自带的后台功能添加组和用户 启动django服务后&#xff0c;在Django终端添加一个账号 在网页上输入网址&#xff0c;跳转到登录页面 Groups 新增组&#xff0c;设置组的名字&#xff0c;对应的权限Save即可 Users 将用户绑定组或单独设置权限 2.用…

arm-day2

汇编实现三个灯循环点亮 .text .global _start _start: /**********LED1点灯**************/ RCC_TNIT:ldr r0,0x50000a28ldr r1,[r0]orr r1,r1,#(0x1 << 4)orr r1,r1,#(0x1 << 5)str r1,[r0]LED_TNIT:ldr r0,0x50006000ldr r1,[r0]and r1,r1,#(~(0x3 << 20…

珠海市黄杨山之旅游

西湾村 早上6点半出门&#xff0c;买点五人份的早餐 A点 第一个点&#xff0c;冲 C点 D岛 到d点休息 B点 高度&#xff1a;229米 到这里有人吐了&#xff0c;建议早餐不要吃超过三个包子&#xff08;他吃了四个包子&#xff0c;1个鸡蛋&#xff0c;1个火腿&#xff09; 记…

linux高并发web服务器开发(web服务器)18_函数解析http请求, 正则表达式,sscanf使用,http中数据特殊字符编码解码

pdf详情版 01 学习目标 编写函数解析http请求 ○ GET /hello.html HTTP/1.1\r\n ○ 将上述字符串分为三部分解析出来编写函数根据文件后缀&#xff0c;返回对应的文件类型sscanf - 读取格式化的字符串中的数据 ○ 使用正则表达式拆分 ○ [^ ]的用法通过浏览器请求目录数据 ○…

Window环境RabbitMq搭建部署

Erlang下载安装及配置环境变量 下载erlang&#xff0c;原因在于RabbitMQ服务端代码是使用并发式语言Erlang编写的 Erlang下载 Erlang官网下载&#xff1a; http://www.erlang.org/downloads Erlang国内镜像下载&#xff08;推荐&#xff09;&#xff1a; http://erlang.org/d…

mysql主从复制(主-从-从)

文章目录 一、前期环境准备二、主库配置1.设置server-id值并开启binlog参数2.建立同步账户并给上权限3.查看主库状态4.锁表设置只读5.备份数据库数据 三、从库配置1.设置server-id值并开启binlog参数2.还原从主库备份数据3.设定从主库同步4.启动从库同步开关 四.测试1.在主库上…

力扣 -- 121. 买卖股票的最佳时机

题目&#xff1a; 题目链接&#xff1a;121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 解题步骤&#xff1a; 下面是用动态规划的思想解决这道题的过程&#xff0c;相信各位小伙伴都能看懂并且掌握这道经典的动规题目滴。说明&#xff1a;以下这种方法并…

【动态规划part02】| 62.不同路径、63.不同路径||

目录 &#x1f388;LeetCode 62.不同路径 &#x1f388;LeetCode 63. 不同路径 II &#x1f388;LeetCode 62.不同路径 链接&#xff1a;62.不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向…

jQuery基本介绍和 DOM 对象互相转换

文章目录 jQuery基本介绍和 DOM 对象互相转换基本介绍jQuery 的原理示意图JQuery 基本开发步骤说明:jQuery简单示例 jQuery 对象和 DOM 对象什么是 jQuery 对象DOM 对象转成 jQuery 对象应用实例 jQuery 对象转成 DOM 对象代码演示 jQuery基本介绍和 DOM 对象互相转换 基本介绍…

设计模式篇---工厂方法(可通过lambda实现)

文章目录 概念结构实例通过lambda实现总结 概念 工厂方法模式&#xff1a;定义一个用于创建对象的接口&#xff0c;但是让子类决定将哪个类实例化。工厂方法模式让一个类的实例化延迟到其子类。 这个模式还是比较好理解&#xff0c;且使用场景比较频繁。简单工厂是只有一个工厂…

使用 Vue 创建一个简单的 Loading 动画

使用 Vue 创建一个简单的 Loading 动画 1. 开始之前 确保 正确安装了 Vue 3知道如何启动一个新的 Vue 项目&#xff08;或在项目中使用Vue&#xff09;了解 Vue 3 的 Composition API&#xff08;本文将使用&#xff09; 2. 设计组件 该组件应该包含三个部分 控制逻辑旋转…