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

这篇具有很好参考价值的文章主要介绍了macOS coreAudio 之 AudioQueue 播放本地音频文件。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

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

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

读取和播放的大致流程为:macOS coreAudio 之 AudioQueue 播放本地音频文件,macos,AudioQueue,CoreAudio

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 😄文章来源地址https://www.toymoban.com/news/detail-607045.html

到了这里,关于macOS coreAudio 之 AudioQueue 播放本地音频文件的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MacOS本地安装Hadoop3

    金翅大鹏盖世英,展翅金鹏盖世雄。 穿云燕子锡今鸽,踏雪无痕花云平。 ---------------- 本文密钥:338 ----------------- 本文描述了在macbook pro的macos上安装hadoop3的过程,也可以作为在任何类linux平台上安装hadoop3借鉴。 1、准备工作 确保已安装jdk,本地已安装jdk1.8.0_221。 export JAV

    2024年02月15日
    浏览(46)
  • 【AIGC】MacOS 本地部署 Stable Diffusion

    其实要在本地部署 stable-diffusion 不难,只要有“魔法”一切都水到渠成,如下图: 如果没有魔法就建议到 gitee 中找一下是否有对应的镜像仓库,我在 gitee 中找到的是: https://gitee.com/ai-liam/AUTOMATIC1111-stable-diffusion-webui.git 接着就是通过 brew 进行必要软件的安装了…这个 homebr

    2024年02月21日
    浏览(47)
  • 公网远程访问macOS本地web服务器

    # 公网访问macOS本地web服务器【内网穿透】 以macOS自带的Apache为例,在本地启用apache服务器,并通过cpolar内网穿透将其暴露至公网,实现在公网环境下访问本地web服务。 1. 启动Apache服务器 在macOS上打开终端,执行命令启动Apache服务器,提示 Passwod ,输入密码 浏览器访问localh

    2024年02月08日
    浏览(39)
  • MACOS Ventura 本地安装HDFS 3.1.4

    1、终端,输入第一条命令一直回车即可,然后将生成的将公钥内容写入到 ~/.ssh/authorized_keys 中 2、系统登录-共享-远程登录,打开。如下: 3、官网下载安装包并且解压: Apache Hadoop  4、修改 core-site.xml 5、修改 hdfs-site.xml 6、修改 yarn-site.xml 7、修改 mapred-site.xml 8、配置环境变

    2024年02月07日
    浏览(39)
  • macOS下ToDesk提示“正在重新连接本地服务...”

    之前在自己的Mac上安装了ToDesk,用来远程家里的虚拟机。安装完成之后第一次打开是可以用的,后面再打开发现提示“正在重新连接本地服务…”,重启ToDesk和重启电脑N多次也不管用,重装ToDesk管用,但是远程一次之后又不行了,官网也没有找到解决办法。一次偶尔发现,找

    2024年02月08日
    浏览(61)
  • DaVinci Resolve Studio 18.6.5 (macOS, Windows) - 剪辑、调色、特效和音频后期制作

    DaVinci Resolve Studio 18.6.5 (macOS, Windows) - 剪辑、调色、特效和音频后期制作 Blackmagic Design DaVinci Resolve Studio 请访问原文链接:DaVinci Resolve Studio 18.6.5 (macOS, Windows) - 剪辑、调色、特效和音频后期制作,查看最新版。原创作品,转载请保留出处。 作者主页:sysin.org 免费! 专业的剪

    2024年02月21日
    浏览(38)
  • DaVinci Resolve Studio 18.6.2 (macOS, Windows) - 剪辑、调色、特效和音频后期制作

    DaVinci Resolve Studio 18.6.2 (macOS, Windows) - 剪辑、调色、特效和音频后期制作 Blackmagic Design DaVinci Resolve Studio 请访问原文链接:https://sysin.org/blog/davinci-resolve-18/,查看最新版。原创作品,转载请保留出处。 作者主页:sysin.org 免费! 专业的剪辑、调色、特效和音频后期制作 DaVinci

    2024年02月01日
    浏览(31)
  • 如何在 macOS 中删除 Time Machine 本地快照

    看到这个可用82GB(458.3MB可清除) 顿时感觉清爽,之前的还是可用82GB(65GB可清除),安装个xcode都安装不上,费解半天,怎么都解决不了这个问题,就是买磁盘情理软件也解决不了。 第一步:打开终端 第二步:输入 这个命令是查找出来备份的哪些,按照日期来记录。 第三

    2024年01月23日
    浏览(20)
  • Python实现本地视频/音频播放器

    在Python中,有几个库可以用于视频播放,但是没有一个库是完美的,因为它们可能依赖于外部软件或有一些限制。 先看介绍用Python实现本地视频播放器,再介绍用Python实现本地音乐播放器。 Python 实现本地视频播放器 与HTML5+JavaScript实现本地视频播放器相比,使用Python实现比

    2024年04月26日
    浏览(37)
  • 如何在 macOS 使用 AList + RCLONE 把网盘挂载到本地

    之前接触最多的可能还是百度网盘,它的免费容量比较大,我们在下载和分享一些文件的时候经常会用到,但是如果文件比较大,加上限速,使用体验并不算太好。其实除了下载文件,我很少使用百度网盘存储文件,影视资源之类的内容又经常被和谐,所以就觉得网盘似乎也

    2024年01月19日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包