开源 Objective-C IOS 应用开发(十七)CAF音频的录制

u 文章的目的为了记录使用Objective-C 进行IOS app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。

相关链接:

开源 Objective-C IOS 应用开发(一)macOS 的使用

开源 Objective-C IOS 应用开发(二)Xcode安装

开源 Objective-C IOS 应用开发(三)第一个iPhone的APP

开源 Objective-C IOS 应用开发(四)Xcode工程文件结构

开源 Objective-C IOS 应用开发(五)iOS操作(action)和输出口(Outlet)

推荐链接:

开源 Arkts 鸿蒙应用 开发(一)工程文件分析-CSDN博客

开源 Arkts 鸿蒙应用 开发(二)封装库.har制作和应用-CSDN博客

开源 Arkts 鸿蒙应用 开发(三)Arkts的介绍-CSDN博客

开源 Arkts 鸿蒙应用 开发(四)布局和常用控件-CSDN博客

开源 Arkts 鸿蒙应用 开发(五)控件组成和复杂控件-CSDN博客

开源 Arkts 鸿蒙应用 开发(六)数据持久--文件和首选项存储-CSDN博客

开源 Arkts 鸿蒙应用 开发(七)数据持久--sqlite关系数据库-CSDN博客

开源 Arkts 鸿蒙应用 开发(八)多媒体--相册和相机-CSDN博客

开源 Arkts 鸿蒙应用 开发(九)通讯--tcp客户端-CSDN博客

开源 Arkts 鸿蒙应用 开发(十)通讯--Http-CSDN博客

开源 Arkts 鸿蒙应用 开发(十一)证书和包名修改-CSDN博客

开源 Arkts 鸿蒙应用 开发(十二)传感器的使用-CSDN博客

开源 Arkts 鸿蒙应用 开发(十三)音频--MP3播放_arkts avplayer播放音频 mp3-CSDN博客

开源 Arkts 鸿蒙应用 开发(十四)线程--任务池(taskpool)-CSDN博客

开源 Arkts 鸿蒙应用 开发(十五)自定义绘图控件--仪表盘-CSDN博客

开源 Arkts 鸿蒙应用 开发(十六)自定义绘图控件--波形图-CSDN博客

开源 Arkts 鸿蒙应用 开发(十七)通讯--http多文件下载-CSDN博客

开源 Arkts 鸿蒙应用 开发(十八)通讯--Ble低功耗蓝牙服务器-CSDN博客

推荐链接:

开源 java android app 开发(一)开发环境的搭建-CSDN博客

开源 java android app 开发(二)工程文件结构-CSDN博客

开源 java android app 开发(三)GUI界面布局和常用组件-CSDN博客

开源 java android app 开发(四)GUI界面重要组件-CSDN博客

开源 java android app 开发(五)文件和数据库存储-CSDN博客

开源 java android app 开发(六)多媒体使用-CSDN博客

开源 java android app 开发(七)通讯之Tcp和Http-CSDN博客

开源 java android app 开发(八)通讯之Mqtt和Ble-CSDN博客

开源 java android app 开发(九)后台之线程和服务-CSDN博客

开源 java android app 开发(十)广播机制-CSDN博客

开源 java android app 开发(十一)调试、发布-CSDN博客

开源 java android app 开发(十二)封库.aar-CSDN博客

本章内容主要是实现CAF音频的录制。

目录:

1.手机演示

2.所有源码

3.源码分析

一、手机演示

二、所有源码

AppDelegate.h文件

复制代码
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

AppDelegate.m文件

复制代码
#import "AppDelegate.h"
#import "MainViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    // 创建窗口
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
    // 创建主视图控制器
    MainViewController *mainViewController = [[MainViewController alloc] init];
    
    // 创建导航控制器(可选,提供更好的导航体验)
    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:mainViewController];
    navController.navigationBar.prefersLargeTitles = YES;
    mainViewController.navigationItem.title = @"CAF音频录制";
    
    // 设置根视图控制器
    self.window.rootViewController = navController;
    
    // 设置窗口背景色并显示
    self.window.backgroundColor = [UIColor systemBackgroundColor];
    [self.window makeKeyAndVisible];
    
    return YES;
}

@end

AudioRecorder.h文件

复制代码
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

NS_ASSUME_NONNULL_BEGIN

@interface AudioRecorder : NSObject

@property (nonatomic, readonly) BOOL isRecording;
@property (nonatomic, copy) NSString *currentFilePath;

- (BOOL)startRecording;
- (void)stopRecording;
- (void)pauseRecording;
- (void)resumeRecording;

@end

NS_ASSUME_NONNULL_END

AudioRecorder.m文件

复制代码
#import "AudioRecorder.h"

#define kNumberBuffers 3

typedef struct AQRecorderState {
    AudioStreamBasicDescription  mDataFormat;
    AudioQueueRef                mQueue;
    AudioQueueBufferRef          mBuffers[kNumberBuffers];
    AudioFileID                  mAudioFile;
    UInt32                       bufferByteSize;
    SInt64                       mCurrentPacket;
    bool                         mIsRunning;
} AQRecorderState;

static void HandleInputBuffer(void *inUserData,
                             AudioQueueRef inAQ,
                             AudioQueueBufferRef inBuffer,
                             const AudioTimeStamp *inStartTime,
                             UInt32 inNumPackets,
                             const AudioStreamPacketDescription *inPacketDesc) {
    
    AQRecorderState *pAqData = (AQRecorderState *)inUserData;
    
    if (inNumPackets == 0 && pAqData->mDataFormat.mBytesPerPacket != 0) {
        inNumPackets = inBuffer->mAudioDataByteSize / pAqData->mDataFormat.mBytesPerPacket;
    }
    
    if (AudioFileWritePackets(pAqData->mAudioFile,
                             false,
                             inBuffer->mAudioDataByteSize,
                             inPacketDesc,
                             pAqData->mCurrentPacket,
                             &inNumPackets,
                             inBuffer->mAudioData) == noErr) {
        pAqData->mCurrentPacket += inNumPackets;
    }
    
    if (pAqData->mIsRunning) {
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}

@implementation AudioRecorder {
    AQRecorderState _aqData;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupAudioFormat:&_aqData.mDataFormat];
        _aqData.mIsRunning = false;
    }
    return self;
}

- (void)dealloc {
    if (_aqData.mIsRunning) {
        [self stopRecording];
    }
}

- (void)setupAudioFormat:(AudioStreamBasicDescription *)format {
    format->mSampleRate = 44100.0;
    format->mFormatID = kAudioFormatLinearPCM;
    format->mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    format->mFramesPerPacket = 1;
    format->mChannelsPerFrame = 1;
    format->mBitsPerChannel = 16;
    format->mBytesPerPacket = 2;
    format->mBytesPerFrame = 2;
    format->mReserved = 0;
}

- (void)deriveBufferSize:(AudioQueueRef)audioQueue
              format:(AudioStreamBasicDescription *)format
            seconds:(Float64)seconds
       outBufferSize:(UInt32 *)outBufferSize {
    
    static const int maxBufferSize = 0x50000;
    int maxPacketSize = format->mBytesPerPacket;
    
    if (maxPacketSize == 0) {
        UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
        AudioQueueGetProperty(audioQueue,
                            kAudioQueueProperty_MaximumOutputPacketSize,
                            &maxPacketSize,
                            &maxVBRPacketSize);
    }
    
    Float64 numBytesForTime = format->mSampleRate * maxPacketSize * seconds;
    *outBufferSize = (UInt32)(numBytesForTime < maxBufferSize ? numBytesForTime : maxBufferSize);
}

- (BOOL)startRecording {
    if (_aqData.mIsRunning) {
        return NO;
    }
    
    // 创建录音文件路径
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths firstObject];
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"recording.caf"];
    _currentFilePath = [filePath copy];
    
    CFURLRef fileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
                                                    (CFStringRef)filePath,
                                                    kCFURLPOSIXPathStyle,
                                                    false);
    
    // 创建音频队列
    OSStatus status = AudioQueueNewInput(&_aqData.mDataFormat,
                                        HandleInputBuffer,
                                        &_aqData,
                                        NULL,
                                        kCFRunLoopCommonModes,
                                        0,
                                        &_aqData.mQueue);
    
    if (status != noErr) {
        NSLog(@"Error creating audio queue: %d", (int)status);
        return NO;
    }
    
    // 创建音频文件
    status = AudioFileCreateWithURL(fileURL,
                                   kAudioFileCAFType,
                                   &_aqData.mDataFormat,
                                   kAudioFileFlags_EraseFile,
                                   &_aqData.mAudioFile);
    
    CFRelease(fileURL);
    
    if (status != noErr) {
        NSLog(@"Error creating audio file: %d", (int)status);
        AudioQueueDispose(_aqData.mQueue, true);
        return NO;
    }
    
    // 设置缓冲区大小
    [self deriveBufferSize:_aqData.mQueue
                    format:&_aqData.mDataFormat
                   seconds:0.5
              outBufferSize:&_aqData.bufferByteSize];
    
    // 创建缓冲区
    for (int i = 0; i < kNumberBuffers; ++i) {
        AudioQueueAllocateBuffer(_aqData.mQueue,
                                _aqData.bufferByteSize,
                                &_aqData.mBuffers[i]);
        
        AudioQueueEnqueueBuffer(_aqData.mQueue,
                               _aqData.mBuffers[i],
                               0,
                               NULL);
    }
    
    _aqData.mCurrentPacket = 0;
    _aqData.mIsRunning = true;
    
    // 开始录音
    status = AudioQueueStart(_aqData.mQueue, NULL);
    if (status != noErr) {
        NSLog(@"Error starting audio queue: %d", (int)status);
        return NO;
    }
    
    return YES;
}

- (void)stopRecording {
    if (!_aqData.mIsRunning) {
        return;
    }
    
    _aqData.mIsRunning = false;
    
    AudioQueueStop(_aqData.mQueue, true);
    AudioFileClose(_aqData.mAudioFile);
    AudioQueueDispose(_aqData.mQueue, true);
}

- (void)pauseRecording {
    if (_aqData.mIsRunning) {
        AudioQueuePause(_aqData.mQueue);
    }
}

- (void)resumeRecording {
    if (_aqData.mIsRunning) {
        AudioQueueStart(_aqData.mQueue, NULL);
    }
}

- (BOOL)isRecording {
    return _aqData.mIsRunning;
}

@end

MainViewController.h文件

复制代码
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MainViewController : UIViewController

@end

NS_ASSUME_NONNULL_END

MainViewController.m文件

复制代码
#import "MainViewController.h"
#import "AudioRecorder.h"

@interface MainViewController ()

@property (nonatomic, strong) AudioRecorder *audioRecorder;
@property (nonatomic, strong) UIButton *recordButton;
@property (nonatomic, strong) UIButton *stopButton;
@property (nonatomic, strong) UIButton *pauseButton;
@property (nonatomic, strong) UILabel *statusLabel;
@property (nonatomic, strong) UILabel *filePathLabel;

@end

@implementation MainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
    [self setupAudioRecorder];
}

- (void)setupUI {
    self.view.backgroundColor = [UIColor systemBackgroundColor];
    
    // 状态标签
    self.statusLabel = [[UILabel alloc] init];
    self.statusLabel.text = @"准备就绪";
    self.statusLabel.textAlignment = NSTextAlignmentCenter;
    self.statusLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightMedium];
    self.statusLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.statusLabel];
    
    // 文件路径标签
    self.filePathLabel = [[UILabel alloc] init];
    self.filePathLabel.text = @"文件将保存到 Documents 目录";
    self.filePathLabel.textAlignment = NSTextAlignmentCenter;
    self.filePathLabel.font = [UIFont systemFontOfSize:14];
    self.filePathLabel.textColor = [UIColor secondaryLabelColor];
    self.filePathLabel.numberOfLines = 0;
    self.filePathLabel.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.filePathLabel];
    
    // 录音按钮
    self.recordButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.recordButton setTitle:@"开始录音" forState:UIControlStateNormal];
    [self.recordButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    self.recordButton.backgroundColor = [UIColor systemRedColor];
    self.recordButton.titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold];
    self.recordButton.layer.cornerRadius = 8;
    self.recordButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.recordButton addTarget:self action:@selector(recordButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.recordButton];
    
    // 暂停按钮
    self.pauseButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.pauseButton setTitle:@"暂停" forState:UIControlStateNormal];
    [self.pauseButton setTitle:@"继续" forState:UIControlStateSelected];
    self.pauseButton.backgroundColor = [UIColor systemOrangeColor];
    [self.pauseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    self.pauseButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
    self.pauseButton.layer.cornerRadius = 6;
    self.pauseButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.pauseButton addTarget:self action:@selector(pauseButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.pauseButton];
    
    // 停止按钮
    self.stopButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.stopButton setTitle:@"停止录音" forState:UIControlStateNormal];
    [self.stopButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    self.stopButton.backgroundColor = [UIColor systemBlueColor];
    self.stopButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
    self.stopButton.layer.cornerRadius = 6;
    self.stopButton.translatesAutoresizingMaskIntoConstraints = NO;
    [self.stopButton addTarget:self action:@selector(stopButtonTapped) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.stopButton];
    
    // 设置约束
    [NSLayoutConstraint activateConstraints:@[
        // 状态标签约束
        [self.statusLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:40],
        [self.statusLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
        [self.statusLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
        
        // 文件路径标签约束
        [self.filePathLabel.topAnchor constraintEqualToAnchor:self.statusLabel.bottomAnchor constant:20],
        [self.filePathLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
        [self.filePathLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
        
        // 录音按钮约束
        [self.recordButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [self.recordButton.topAnchor constraintEqualToAnchor:self.filePathLabel.bottomAnchor constant:60],
        [self.recordButton.widthAnchor constraintEqualToConstant:200],
        [self.recordButton.heightAnchor constraintEqualToConstant:50],
        
        // 暂停按钮约束
        [self.pauseButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [self.pauseButton.topAnchor constraintEqualToAnchor:self.recordButton.bottomAnchor constant:20],
        [self.pauseButton.widthAnchor constraintEqualToConstant:120],
        [self.pauseButton.heightAnchor constraintEqualToConstant:40],
        
        // 停止按钮约束
        [self.stopButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
        [self.stopButton.topAnchor constraintEqualToAnchor:self.pauseButton.bottomAnchor constant:20],
        [self.stopButton.widthAnchor constraintEqualToConstant:120],
        [self.stopButton.heightAnchor constraintEqualToConstant:40]
    ]];
    
    [self updateButtonStates];
}

- (void)setupAudioRecorder {
    self.audioRecorder = [[AudioRecorder alloc] init];
}

- (void)recordButtonTapped {
    if ([self.audioRecorder startRecording]) {
        self.statusLabel.text = @"录音中...";
        self.statusLabel.textColor = [UIColor systemRedColor];
    } else {
        self.statusLabel.text = @"开始录音失败";
        self.statusLabel.textColor = [UIColor systemRedColor];
    }
    [self updateButtonStates];
}

- (void)stopButtonTapped {
    [self.audioRecorder stopRecording];
    self.statusLabel.text = @"录音已停止";
    self.statusLabel.textColor = [UIColor systemBlueColor];
    
    if (self.audioRecorder.currentFilePath) {
        self.filePathLabel.text = [NSString stringWithFormat:@"文件已保存至:\n%@", self.audioRecorder.currentFilePath];
    }
    
    [self updateButtonStates];
}

- (void)pauseButtonTapped {
    if (self.pauseButton.selected) {
        [self.audioRecorder resumeRecording];
        self.statusLabel.text = @"录音中...";
        self.pauseButton.selected = NO;
    } else {
        [self.audioRecorder pauseRecording];
        self.statusLabel.text = @"已暂停";
        self.pauseButton.selected = YES;
    }
    [self updateButtonStates];
}

- (void)updateButtonStates {
    BOOL isRecording = self.audioRecorder.isRecording;
    
    self.recordButton.enabled = !isRecording;
    self.recordButton.alpha = isRecording ? 0.5 : 1.0;
    
    self.stopButton.enabled = isRecording;
    self.stopButton.alpha = isRecording ? 1.0 : 0.5;
    
    self.pauseButton.enabled = isRecording;
    self.pauseButton.alpha = isRecording ? 1.0 : 0.5;
}

@end

info.plist中,添加以下权限,再删除原来的窗口配置

复制代码
<key>NSMicrophoneUsageDescription</key>
<string>此应用需要访问麦克风以进行音频录制</string>

三、源码分析

  1. AudioRecorder 类分析

1.1 数据结构定义

objc

typedef struct AQRecorderState {

AudioStreamBasicDescription mDataFormat; // 音频格式描述

AudioQueueRef mQueue; // 音频队列引用

AudioQueueBufferRef mBuffers[kNumberBuffers]; // 音频缓冲区数组

AudioFileID mAudioFile; // 音频文件ID

UInt32 bufferByteSize; // 缓冲区字节大小

SInt64 mCurrentPacket; // 当前包编号

bool mIsRunning; // 录制状态标志

} AQRecorderState;

作用:封装音频录制所需的所有状态信息,作为录音器的核心数据结构。

1.2 核心回调函数

objc

static void HandleInputBuffer(void *inUserData,

AudioQueueRef inAQ,

AudioQueueBufferRef inBuffer,

const AudioTimeStamp *inStartTime,

UInt32 inNumPackets,

const AudioStreamPacketDescription *inPacketDesc)

参数分析:

  • inUserData: 用户自定义数据指针,指向AQRecorderState结构体

  • inAQ: 触发回调的音频队列

  • inBuffer: 包含录音数据的缓冲区

  • inStartTime: 缓冲区时间戳

  • inNumPackets: 包数量

  • inPacketDesc: 包描述数组

函数执行流程:

  1. 获取状态数据:将void指针转换为AQRecorderState结构体指针

  2. 计算包数量:如果包数为0且每包字节数固定,则计算实际包数

  3. 写入文件:使用AudioFileWritePackets将缓冲区数据写入音频文件

  4. 更新包计数:递增当前包编号

  5. 重新入队:如果仍在录制,将缓冲区重新加入队列循环使用

1.3 音频格式设置函数

objc

  • (void)setupAudioFormat:(AudioStreamBasicDescription *)format

参数配置详解:

  • mSampleRate = 44100.0: 采样率44.1kHz(CD音质)

  • mFormatID = kAudioFormatLinearPCM: 使用线性PCM格式

  • mFormatFlags: 格式标志

    • kAudioFormatFlagIsSignedInteger: 有符号整数采样

    • kAudioFormatFlagIsPacked: 数据包紧凑排列

  • mFramesPerPacket = 1: 每包1帧

  • mChannelsPerFrame = 1: 单声道

  • mBitsPerChannel = 16: 16位采样深度

  • mBytesPerPacket = 2: 每包2字节(16位 × 1通道)

  • mBytesPerFrame = 2: 每帧2字节

1.4 缓冲区大小计算函数

objc

  • (void)deriveBufferSize:(AudioQueueRef)audioQueue

format:(AudioStreamBasicDescription *)format

seconds:(Float64)seconds

outBufferSize:(UInt32 *)outBufferSize

计算逻辑:

  1. 设置最大限制:maxBufferSize = 0x50000(约320KB)

  2. 获取包大小:如果格式中未指定,从音频队列属性获取

  3. 计算理论大小:采样率 × 最大包大小 × 时间

  4. 取较小值:理论大小与最大限制的较小值作为最终缓冲区大小

1.5 开始录制函数

objc

  • (BOOL)startRecording

执行步骤:

步骤1:文件路径创建

objc

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths firstObject];

NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"recording.caf"];

  • 获取Documents目录路径

  • 构建完整的文件路径

步骤2:音频队列创建

objc

AudioQueueNewInput(&_aqData.mDataFormat,

HandleInputBuffer,

&_aqData,

NULL,

kCFRunLoopCommonModes,

0,

&_aqData.mQueue);

  • 参数1:音频格式描述

  • 参数2:回调函数指针

  • 参数3:传递给回调的用户数据

  • 参数4:运行循环(NULL表示使用音频队列内部线程)

  • 参数5:运行循环模式

  • 参数6:保留参数(必须为0)

  • 参数7:输出的音频队列引用

步骤3:音频文件创建

objc

AudioFileCreateWithURL(fileURL,

kAudioFileCAFType,

&_aqData.mDataFormat,

kAudioFileFlags_EraseFile,

&_aqData.mAudioFile);

  • 参数2:kAudioFileCAFType指定CAF文件格式

  • 参数4:kAudioFileFlags_EraseFile表示如果文件存在则覆盖

步骤4:缓冲区分配和入队

objc

for (int i = 0; i < kNumberBuffers; ++i) {

AudioQueueAllocateBuffer(_aqData.mQueue,

_aqData.bufferByteSize,

&_aqData.mBuffers[i]);

AudioQueueEnqueueBuffer(_aqData.mQueue,

_aqData.mBuffers[i],

0,

NULL);

}

  • 创建3个缓冲区(kNumberBuffers = 3)

  • 每个缓冲区大小为之前计算的结果

  • 立即将缓冲区加入队列开始接收数据

步骤5:启动录制

objc

AudioQueueStart(_aqData.mQueue, NULL);

  • 启动音频队列,开始录制流程

1.6 停止录制函数

objc

  • (void)stopRecording

清理流程:

  1. 设置运行标志为false

  2. 停止音频队列:AudioQueueStop(_aqData.mQueue, true)

    • 第二个参数true表示同步停止,立即停止
  3. 关闭音频文件:AudioFileClose(_aqData.mAudioFile)

  4. 释放音频队列:AudioQueueDispose(_aqData.mQueue, true)

    • 释放队列及其所有资源
  5. MainViewController 类分析

2.1 界面设置函数

objc

  • (void)setupUI

UI组件创建:

状态标签 (statusLabel)

  • 显示当前录制状态

  • 使用系统字体,中等字重

文件路径标签 (filePathLabel)

  • 显示录音文件保存路径

  • 支持多行显示

录音按钮 (recordButton)

  • 红色背景,开始录音功能

  • 圆角设计,点击触发recordButtonTapped

暂停按钮 (pauseButton)

  • 橙色背景,暂停/继续功能

  • 使用selected状态切换显示文字

停止按钮 (stopButton)

  • 蓝色背景,停止录音功能

2.2 按钮状态管理

objc

  • (void)updateButtonStates

状态逻辑:

  • 录制中:开始按钮禁用,停止和暂停按钮启用

  • 未录制:开始按钮启用,停止和暂停按钮禁用

  • 通过alpha值变化提供视觉反馈

2.3 按钮事件处理

开始录音

objc

  • (void)recordButtonTapped
  • 调用audioRecorderstartRecording方法

  • 更新状态标签显示"录音中..."

  • 刷新按钮状态

停止录音

objc

  • (void)stopButtonTapped
  • 调用stopRecording停止录制

  • 显示文件保存路径

  • 重置界面状态

暂停/继续

objc

  • (void)pauseButtonTapped
  • 根据当前状态调用pauseRecordingresumeRecording

  • 切换按钮的selected状态

  • 更新状态标签显示

  1. AppDelegate 配置分析

objc

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

窗口创建流程:

  1. 创建窗口:[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]

  2. 创建视图控制器:MainViewController实例

  3. 包装导航控制器:提供导航栏和大标题支持

  4. 设置根控制器:window.rootViewController = navController

  5. 显示窗口:[window makeKeyAndVisible]

  6. 关键技术点分析

4.1 音频队列工作流程

  1. 初始化:设置格式、创建队列、分配缓冲区

  2. 回调循环:缓冲区填满 → 回调处理 → 重新入队

  3. 数据流:麦克风 → 音频队列 → 回调函数 → 文件写入

  4. 资源清理:停止队列、关闭文件、释放资源

4.2 内存管理

  • 缓冲区重用:3个缓冲区循环使用,避免频繁分配

  • 及时释放:在dealloc和stopRecording中正确释放资源

  • ARC兼容:使用Objective-C的自动引用计数

4.3 错误处理

  • 每个Core Audio函数调用后检查返回值

  • 在关键步骤提供错误日志输出

  • 通过BOOL返回值向调用方报告成功状态

相关推荐
胖虎14 小时前
iOS 如何全局修改项目字体
ios·hook·ios字体·字体适配·ios字体适配
DolphinScheduler社区4 小时前
结项报告完整版 | 为 Apache DolphinScheduler 添加 gRPC 插件
大数据·开源·apache·海豚调度·大数据工作流调度
NocoBase4 小时前
NocoBase 本周更新汇总:新增图表配置的 Al 员工
低代码·开源·资讯
songgeb4 小时前
iOS App进入后台时会发生什么
ios
笑尘pyrotechnic5 小时前
运行,暂停,检查:探索如何使用LLDB进行有效调试
ios·objective-c·lldb
metaRTC6 小时前
webRTC IPC客户端React Native版编程指南
react native·react.js·ios·webrtc·p2p·ipc
TTGGGFF7 小时前
开源项目分享 : Gitee热榜项目 2025-11-19 日榜
gitee·开源
k***12178 小时前
开源模型应用落地-工具使用篇-Spring AI-Function Call(八)
人工智能·spring·开源
oranglay8 小时前
本地运行开源大语言模型工具全览与对比
人工智能·语言模型·开源
ajassi20009 小时前
开源 Objective-C IOS 应用开发(十八)音频的播放
ios·开源·objective-c