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

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 😄

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

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

相关文章

Spring Cloud Gateway - 新一代微服务API网关

Spring Cloud Gateway - 新一代微服务API网关 文章目录 Spring Cloud Gateway - 新一代微服务API网关1.网关介绍2.Spring Cloud Gateway介绍3.Spring Cloud Gateway的特性4.Spring Cloud Gateway的三大核心概念5.Gateway工作流程6.Gateway核心配置7.动态路由8.Predicate自定义P…

【腾讯云 Cloud Studio 实战训练营】使用Cloud Studio制作蛋仔派对兑换码工具

目录 &#x1f373;前言&#x1f373;实验介绍&#x1f373;产品介绍&#x1f373;抓包分析&#x1f603;登录分析&#x1f603;&#x1f603;第一步&#xff0c;获取验证码&#x1f603;&#x1f603;第二步&#xff0c;保存验证码&#x1f603;&#x1f603;第三步&#xff0…

山西电力市场日前价格预测【2023-07-24】

日前价格预测 预测明日&#xff08;2023-07-24&#xff09;山西电力市场全天平均日前电价为338.25元/MWh。其中&#xff0c;最高日前电价为377.59元/MWh&#xff0c;预计出现在20: 30。最低日前电价为283.56元/MWh&#xff0c;预计出现在13: 30。 价差方向预测 1&#xff1a;实…

LiveNVR监控流媒体Onvif/RTSP功能-支持无人机、IPC等设备RTMP推流转码分发H5无插件播放也支持GB28181输出

LiveNVR支持无人机、IPC等设备RTMP推流转码分发H5无插件播放也支持GB28181输出 1、无人机推流转国标2、获取RTMP推流地址2.1、RTMP推流地址格式2.2、推流地址示例 2、设备RTMP推流3、配置拉转RTMP3.1、直播流地址格式3.2、直播流地地址示例3.3、通道配置直播流地址 4、配置级联…

精准测试之分布式调用链底层逻辑

目录 前言&#xff1a; ⼀、分布式调⽤链系统概述 分布式架构所带来的问题 分布式链路监控的作用 ⼆、调用链系统的演进 链路监控系统列表 三、调用链系统的底层实现逻辑 调用链系统的本质 调用链基本元素 事件捕捉 事件串联 事件的开始与结束 上传 四、Span 内容…

Stable Diffusion如何生成高质量的图-prompt写法介绍

文章目录 Stable Diffusion使用尝试下效果prompt的编写技巧prompt 和 negative promptPrompt格式Prompt规则细节优化Guidance Scale 总结 Stable Diffusion Stable Diffusion是一个开源的图像生成AI系统,由Anthropic公司开发。它基于 Transformer模型架构,可以通过文字描述生成…

WAIC2023:图像内容安全黑科技助力可信AI发展

目录 0 写在前面1 AI图像篡改检测2 生成式图像鉴别2.1 主干特征提取通道2.2 注意力模块2.3 纹理增强模块 3 OCR对抗攻击4 助力可信AI向善发展总结 0 写在前面 2023世界人工智能大会(WAIC)已圆满结束&#xff0c;恰逢全球大模型和生成式人工智能蓬勃兴起之时&#xff0c;今年参…

Two Days wpf 分享 分页组件

迟来的wpf分享。 目录 一、序言 二、前期准备 三、前端界面 四、后台代码部分 1、先定义些变量后面使用 2、先是按钮事件代码。 首页按钮 上一页按钮 下一页按钮 末尾按钮 画每页显示等数据 每页显示多少条 判断是否为数字的事件 分页数字的点击触发事件 跳转到…

jmeter常用的提取器(正则表达式和JSON提取器)

jmeter常用的后置处理器有两种提取数据&#xff1a; 1、JSON提取器 获取后可以将变量token引用到其他所需要的地方 &#xff08;正则表达式和JSON提取器&#xff09;:2023接口自动化测试框架必会两大神器:正则提取器和Jsonpath提取器_哔哩哔哩_bilibilihttps://www.bilibili.…

JVM运行时数据区——堆内的区域分布

1.堆内的区域分布 堆是运行时数据区最大的一块区域&#xff0c;主要用来存放对象&#xff0c;堆是所有线程公用的&#xff0c;在JVM启动时就被创建&#xff0c;堆的空间是可以调整的&#xff0c;是GC(垃圾回收)的重点区域。 堆的内存空间分区&#xff1a;新生代老年代 新生代…

Rust vs Go:常用语法对比(三)

题图来自When to use Rust and when to use Go[1] 41. Reverse a string 反转字符串 package mainimport "fmt"func Reverse(s string) string { runes : []rune(s) for i, j : 0, len(runes)-1; i < j; i, j i1, j-1 { runes[i], runes[j] runes[j], runes[i]…

【SQL应知应会】表分区(五)• MySQL版

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分区表 • MySQL版 前言一、分区表1.非分区表2.分区…

【论文】基于GANs的图像文字擦除 ——2010.EraseNet: End-to-End Text Removal in the Wild(已开源)

pytorch官方代码&#xff1a;https://github.com/lcy0604/EraseNet 论文&#xff1a;2010.EraseNet: End-to-End Text Removal in the Wild 网盘提取码&#xff1a;0719 一、图片文字去除效果 图10 SCUT-EnsText 真实数据集的去除 第一列原图带文字、第二列为去除后的标签&a…

RocketMQ分布式事务 -> 最终一致性实现

文章目录 前言事务消息场景代码示例订单服务事务日志表TransactionMQProducerOrderTransactionListener业务实现类调用总结 积分服务积分记录表消费者启动消费者监听器增加积分幂等性消费消费异常 前言 分布式事务的问题常在业务与面试中被提及, 近日摸鱼看到这篇文章, 阐述的…

认识主被动无人机遥感数据、预处理无人机遥感数据、定量估算农林植被关键性状、期刊论文插图精细制作与Appdesigner应用开发

目录 第一章、认识主被动无人机遥感数据 第二章、预处理无人机遥感数据 第三章、定量估算农林植被关键性状 第四章、期刊论文插图精细制作与Appdesigner应用开发 更多推荐 遥感技术作为一种空间大数据手段&#xff0c;能够从多时、多维、多地等角度&#xff0c;获取大量的…

Spring 能解决所有循环依赖吗?

以下内容基于 Spring6.0.4。 看了上篇文章的小伙伴&#xff0c;对于 Spring 解决循环依赖的思路应该有一个大致了解了&#xff0c;今天我们再来看一看&#xff0c;按照上篇文章介绍的思路&#xff0c;有哪些循环依赖 Spring 处理不了。 严格来说&#xff0c;其实也不是解决不了…

PoseiSwap 即将开启 POSE 单币质押,治理体系将全面运行

PoseiSwap 是目前行业首个将支持 RWA 资产交易的 DEX&#xff0c;其构建在 Nautilus Chain 上&#xff0c;并通过模块化的形式单独构建了 zk-Rollup 应用层&#xff0c;具备并行化运行、隐私特性&#xff0c;并从 Cosmos、Celestia、Eclipse 等 Layer0 设施中获得高度可组合性、…

MySQL 中NULL和空值的区别

MySQL 中NULL和空值的区别&#xff1f; 简介NULL也就是在字段中存储NULL值&#xff0c;空值也就是字段中存储空字符(’’)。区别 1、空值不占空间&#xff0c;NULL值占空间。当字段不为NULL时&#xff0c;也可以插入空值。 2、当使用 IS NOT NULL 或者 IS NULL 时&#xff0…

JDK、JRE、JVM三者之间的关系

总结 JDK包含JRE&#xff0c;JRE包含JVM。 JDK (Java Development Kit)----Java开发工具包&#xff0c;用于Java程序的开发。 JRE (Java Runtime Environment)----Java运行时环境&#xff0c;只能运行.class文件&#xff0c;不能编译。 JVM (Java Virtual Machine)----Java虚拟…

21matlab数据分析牛顿插值(matlab程序)

1.简述 一、牛顿插值法原理 1.牛顿插值多项式   定义牛顿插值多项式为&#xff1a; N n ( x ) a 0 a 1 ( x − x 0 ) a 2 ( x − x 0 ) ( x − x 1 ) ⋯ a n ( x − x 0 ) ( x − x 1 ) ⋯ ( x − x n − 1 ) N_n\left(x\right)a_0a_1\left(x-x_0\right)a_2\left(x-x_0\…