文章的目的为了记录使用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博客
本章内容主要是完整的 AudioToolbox框架的iOS音频播放器。
目录:
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 "AudioPlayerViewController.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 创建窗口
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// 创建音频播放视图控制器
AudioPlayerViewController *playerViewController = [[AudioPlayerViewController alloc] init];
// 创建导航控制器
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:playerViewController];
navController.navigationBar.prefersLargeTitles = YES;
// 设置根视图控制器
self.window.rootViewController = navController;
// 设置窗口背景色并显示
self.window.backgroundColor = [UIColor systemBackgroundColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
AudioPlayer.h文件
#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>
NS_ASSUME_NONNULL_BEGIN
@class AudioPlayer;
@protocol AudioPlayerDelegate <NSObject>
@optional
- (void)audioPlayerDidFinishPlaying:(AudioPlayer *)player successfully:(BOOL)flag;
- (void)audioPlayerDecodeErrorDidOccur:(AudioPlayer *)player error:(NSError *)error;
- (void)audioPlayerDidStartPlaying:(AudioPlayer *)player;
- (void)audioPlayerDidPause:(AudioPlayer *)player;
- (void)audioPlayerDidStop:(AudioPlayer *)player;
@end
@interface AudioPlayer : NSObject
@property (nonatomic, weak) id<AudioPlayerDelegate> delegate;
@property (nonatomic, readonly) BOOL isPlaying;
@property (nonatomic, readonly) BOOL isPaused;
@property (nonatomic, readonly) NSTimeInterval duration;
@property (nonatomic, readonly) NSTimeInterval currentTime;
@property (nonatomic, assign) float volume;
- (BOOL)loadAudioFile:(NSString *)filePath;
- (BOOL)play;
- (void)pause;
- (void)stop;
- (BOOL)seekToTime:(NSTimeInterval)time;
@end
NS_ASSUME_NONNULL_END
AudioPlayer.m文件
#import "AudioPlayer.h"
#define kNumberBuffers 3
typedef struct AQPlayerState {
AudioStreamBasicDescription mDataFormat;
AudioQueueRef mQueue;
AudioQueueBufferRef mBuffers[kNumberBuffers];
AudioFileID mAudioFile;
UInt32 bufferByteSize;
SInt64 mCurrentPacket;
UInt32 mNumPacketsToRead;
bool mIsRunning;
bool mIsPaused;
NSTimeInterval mDuration;
} AQPlayerState;
@implementation AudioPlayer {
AQPlayerState _aqData;
}
#pragma mark - Core Audio Callbacks
static void HandleOutputBuffer(void *inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
AudioPlayer *player = (__bridge AudioPlayer *)inUserData;
[player handleOutputBuffer:inBuffer inAudioQueue:inAQ];
}
static void AudioQueuePropertyListener(void *inUserData,
AudioQueueRef inAQ,
AudioQueuePropertyID inID) {
AudioPlayer *player = (__bridge AudioPlayer *)inUserData;
[player handlePropertyListener:inID inAudioQueue:inAQ];
}
#pragma mark - Instance Methods
- (void)handleOutputBuffer:(AudioQueueBufferRef)inBuffer inAudioQueue:(AudioQueueRef)inAQ {
if (!_aqData.mIsRunning) {
return;
}
UInt32 numBytesRead = 0;
UInt32 numPackets = _aqData.mNumPacketsToRead;
OSStatus status = AudioFileReadPackets(_aqData.mAudioFile,
false,
&numBytesRead,
NULL,
_aqData.mCurrentPacket,
&numPackets,
inBuffer->mAudioData);
if (status != noErr) {
NSLog(@"AudioFileReadPackets failed: %d", (int)status);
return;
}
if (numPackets > 0) {
inBuffer->mAudioDataByteSize = numBytesRead;
status = AudioQueueEnqueueBuffer(inAQ,
inBuffer,
0,
NULL);
if (status != noErr) {
NSLog(@"AudioQueueEnqueueBuffer failed: %d", (int)status);
return;
}
_aqData.mCurrentPacket += numPackets;
} else {
// 播放完成
AudioQueueStop(inAQ, false);
_aqData.mIsRunning = false;
// 通知委托
if ([self.delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying:successfully:)]) {
[self.delegate audioPlayerDidFinishPlaying:self successfully:YES];
}
}
}
- (void)handlePropertyListener:(AudioQueuePropertyID)inID inAudioQueue:(AudioQueueRef)inAQ {
if (inID == kAudioQueueProperty_IsRunning) {
UInt32 isRunning;
UInt32 size = sizeof(isRunning);
AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &isRunning, &size);
if (!isRunning) {
_aqData.mIsRunning = false;
// 如果不是暂停状态,通知停止
if (!_aqData.mIsPaused) {
if ([self.delegate respondsToSelector:@selector(audioPlayerDidStop:)]) {
[self.delegate audioPlayerDidStop:self];
}
}
}
}
}
- (instancetype)init {
self = [super init];
if (self) {
[self setupAudioPlayer];
}
return self;
}
- (void)dealloc {
[self cleanup];
}
- (void)setupAudioPlayer {
memset(&_aqData, 0, sizeof(_aqData));
_aqData.mIsRunning = false;
_aqData.mIsPaused = false;
_volume = 1.0f;
}
- (void)cleanup {
if (_aqData.mQueue) {
AudioQueueDispose(_aqData.mQueue, true);
_aqData.mQueue = NULL;
}
if (_aqData.mAudioFile) {
AudioFileClose(_aqData.mAudioFile);
_aqData.mAudioFile = NULL;
}
}
- (void)calculateDuration {
if (_aqData.mAudioFile) {
UInt32 dataSize = sizeof(_aqData.mDuration);
OSStatus status = AudioFileGetProperty(_aqData.mAudioFile,
kAudioFilePropertyEstimatedDuration,
&dataSize,
&_aqData.mDuration);
if (status != noErr) {
_aqData.mDuration = 0.0;
}
} else {
_aqData.mDuration = 0.0;
}
}
- (void)deriveBufferSize {
static const int maxBufferSize = 0x10000;
UInt32 maxPacketSize;
UInt32 propertySize = sizeof(maxPacketSize);
OSStatus status = AudioFileGetProperty(_aqData.mAudioFile,
kAudioFilePropertyPacketSizeUpperBound,
&propertySize,
&maxPacketSize);
if (status != noErr) {
maxPacketSize = 0x1000;
}
if (_aqData.mDataFormat.mFramesPerPacket) {
Float64 numPacketsForTime = _aqData.mDataFormat.mSampleRate / _aqData.mDataFormat.mFramesPerPacket * 0.5;
_aqData.bufferByteSize = numPacketsForTime * maxPacketSize;
} else {
_aqData.bufferByteSize = maxBufferSize;
}
if (_aqData.bufferByteSize > maxBufferSize) {
_aqData.bufferByteSize = maxBufferSize;
}
_aqData.mNumPacketsToRead = _aqData.bufferByteSize / maxPacketSize;
}
- (BOOL)loadAudioFile:(NSString *)filePath {
[self cleanup];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSLog(@"Audio file does not exist: %@", filePath);
return NO;
}
CFURLRef fileURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault,
(CFStringRef)filePath,
kCFURLPOSIXPathStyle,
false);
// 打开音频文件
OSStatus status = AudioFileOpenURL(fileURL,
kAudioFileReadPermission,
0,
&_aqData.mAudioFile);
CFRelease(fileURL);
if (status != noErr) {
NSLog(@"AudioFileOpenURL failed: %d", (int)status);
return NO;
}
// 获取音频数据格式
UInt32 dataFormatSize = sizeof(_aqData.mDataFormat);
status = AudioFileGetProperty(_aqData.mAudioFile,
kAudioFilePropertyDataFormat,
&dataFormatSize,
&_aqData.mDataFormat);
if (status != noErr) {
NSLog(@"AudioFileGetProperty failed: %d", (int)status);
AudioFileClose(_aqData.mAudioFile);
_aqData.mAudioFile = NULL;
return NO;
}
// 创建音频队列
status = AudioQueueNewOutput(&_aqData.mDataFormat,
HandleOutputBuffer,
(__bridge void *)self,
NULL,
NULL,
0,
&_aqData.mQueue);
if (status != noErr) {
NSLog(@"AudioQueueNewOutput failed: %d", (int)status);
AudioFileClose(_aqData.mAudioFile);
_aqData.mAudioFile = NULL;
return NO;
}
// 添加属性监听器
AudioQueueAddPropertyListener(_aqData.mQueue,
kAudioQueueProperty_IsRunning,
AudioQueuePropertyListener,
(__bridge void *)self);
// 计算缓冲区大小
[self deriveBufferSize];
// 计算音频时长
[self calculateDuration];
// 分配和入队缓冲区
for (int i = 0; i < kNumberBuffers; ++i) {
status = AudioQueueAllocateBuffer(_aqData.mQueue,
_aqData.bufferByteSize,
&_aqData.mBuffers[i]);
if (status != noErr) {
NSLog(@"AudioQueueAllocateBuffer failed: %d", (int)status);
[self cleanup];
return NO;
}
[self handleOutputBuffer:_aqData.mBuffers[i] inAudioQueue:_aqData.mQueue];
}
// 设置音量
[self setVolume:_volume];
return YES;
}
- (BOOL)play {
if (!_aqData.mQueue) {
return NO;
}
if (_aqData.mIsPaused) {
// 从暂停状态恢复
OSStatus status = AudioQueueStart(_aqData.mQueue, NULL);
if (status != noErr) {
NSLog(@"AudioQueueStart failed: %d", (int)status);
return NO;
}
_aqData.mIsRunning = true;
_aqData.mIsPaused = false;
if ([self.delegate respondsToSelector:@selector(audioPlayerDidStartPlaying:)]) {
[self.delegate audioPlayerDidStartPlaying:self];
}
} else if (!_aqData.mIsRunning) {
// 重新开始播放
_aqData.mCurrentPacket = 0;
for (int i = 0; i < kNumberBuffers; ++i) {
[self handleOutputBuffer:_aqData.mBuffers[i] inAudioQueue:_aqData.mQueue];
}
OSStatus status = AudioQueueStart(_aqData.mQueue, NULL);
if (status != noErr) {
NSLog(@"AudioQueueStart failed: %d", (int)status);
return NO;
}
_aqData.mIsRunning = true;
_aqData.mIsPaused = false;
if ([self.delegate respondsToSelector:@selector(audioPlayerDidStartPlaying:)]) {
[self.delegate audioPlayerDidStartPlaying:self];
}
}
return YES;
}
- (void)pause {
if (_aqData.mIsRunning && !_aqData.mIsPaused) {
AudioQueuePause(_aqData.mQueue);
_aqData.mIsPaused = true;
if ([self.delegate respondsToSelector:@selector(audioPlayerDidPause:)]) {
[self.delegate audioPlayerDidPause:self];
}
}
}
- (void)stop {
if (_aqData.mIsRunning) {
AudioQueueStop(_aqData.mQueue, true);
_aqData.mIsRunning = false;
_aqData.mIsPaused = false;
_aqData.mCurrentPacket = 0;
if ([self.delegate respondsToSelector:@selector(audioPlayerDidStop:)]) {
[self.delegate audioPlayerDidStop:self];
}
}
}
- (BOOL)seekToTime:(NSTimeInterval)time {
if (!_aqData.mAudioFile || time < 0 || time > _aqData.mDuration) {
return NO;
}
BOOL wasPlaying = _aqData.mIsRunning && !_aqData.mIsPaused;
if (wasPlaying) {
[self pause];
}
// 计算目标位置的包号
SInt64 targetPacket = (SInt64)(time * _aqData.mDataFormat.mSampleRate / _aqData.mDataFormat.mFramesPerPacket);
_aqData.mCurrentPacket = targetPacket;
// 清空队列
AudioQueueReset(_aqData.mQueue);
// 重新填充缓冲区
for (int i = 0; i < kNumberBuffers; ++i) {
[self handleOutputBuffer:_aqData.mBuffers[i] inAudioQueue:_aqData.mQueue];
}
if (wasPlaying) {
return [self play];
}
return YES;
}
- (void)setVolume:(float)volume {
_volume = volume;
if (_aqData.mQueue) {
AudioQueueSetParameter(_aqData.mQueue, kAudioQueueParam_Volume, volume);
}
}
- (BOOL)isPlaying {
return _aqData.mIsRunning && !_aqData.mIsPaused;
}
- (BOOL)isPaused {
return _aqData.mIsPaused;
}
- (NSTimeInterval)duration {
return _aqData.mDuration;
}
- (NSTimeInterval)currentTime {
if (!_aqData.mQueue) {
return 0.0;
}
AudioTimeStamp timeStamp;
OSStatus status = AudioQueueGetCurrentTime(_aqData.mQueue, NULL, &timeStamp, NULL);
if (status == noErr) {
return timeStamp.mSampleTime / _aqData.mDataFormat.mSampleRate;
}
return 0.0;
}
@end
AudioPlayerViewController.h文件
//
// AudioPlayerViewController.h
// audioPlay
//
// Created by Mixic2025 on 2025/11/19.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AudioPlayerViewController : UIViewController
@end
NS_ASSUME_NONNULL_END
AudioPlayerViewController.m文件
#import "AudioPlayerViewController.h"
#import "AudioPlayer.h"
#import <MobileCoreServices/MobileCoreServices.h>
@interface AudioPlayerViewController () <AudioPlayerDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate>
@property (nonatomic, strong) AudioPlayer *audioPlayer;
@property (nonatomic, strong) UIButton *loadButton;
@property (nonatomic, strong) UIButton *browseButton;
@property (nonatomic, strong) UIButton *playButton;
@property (nonatomic, strong) UIButton *pauseButton;
@property (nonatomic, strong) UIButton *stopButton;
@property (nonatomic, strong) UIButton *deleteButton;
@property (nonatomic, strong) UISlider *progressSlider;
@property (nonatomic, strong) UISlider *volumeSlider;
@property (nonatomic, strong) UILabel *statusLabel;
@property (nonatomic, strong) UILabel *timeLabel;
@property (nonatomic, strong) UILabel *fileLabel;
@property (nonatomic, strong) NSTimer *progressTimer;
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
@end
@implementation AudioPlayerViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
[self setupAudioPlayer];
[self autoLoadLatestRecording];
}
- (void)setupUI {
self.view.backgroundColor = [UIColor systemBackgroundColor];
self.title = @"CAF音频播放器";
// 文件标签
self.fileLabel = [[UILabel alloc] init];
self.fileLabel.text = @"未加载音频文件";
self.fileLabel.textAlignment = NSTextAlignmentCenter;
self.fileLabel.font = [UIFont systemFontOfSize:16];
self.fileLabel.textColor = [UIColor secondaryLabelColor];
self.fileLabel.numberOfLines = 0;
self.fileLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.fileLabel];
// 状态标签
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.timeLabel = [[UILabel alloc] init];
self.timeLabel.text = @"00:00 / 00:00";
self.timeLabel.textAlignment = NSTextAlignmentCenter;
self.timeLabel.font = [UIFont monospacedDigitSystemFontOfSize:14 weight:UIFontWeightRegular];
self.timeLabel.textColor = [UIColor tertiaryLabelColor];
self.timeLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.timeLabel];
// 进度条
self.progressSlider = [[UISlider alloc] init];
self.progressSlider.minimumValue = 0.0;
self.progressSlider.maximumValue = 1.0;
self.progressSlider.value = 0.0;
[self.progressSlider addTarget:self action:@selector(progressSliderChanged:) forControlEvents:UIControlEventValueChanged];
[self.progressSlider addTarget:self action:@selector(progressSliderTouchDown) forControlEvents:UIControlEventTouchDown];
[self.progressSlider addTarget:self action:@selector(progressSliderTouchUp) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
self.progressSlider.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.progressSlider];
// 音量滑块
UILabel *volumeLabel = [[UILabel alloc] init];
volumeLabel.text = @"音量";
volumeLabel.font = [UIFont systemFontOfSize:14];
volumeLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:volumeLabel];
self.volumeSlider = [[UISlider alloc] init];
self.volumeSlider.minimumValue = 0.0;
self.volumeSlider.maximumValue = 1.0;
self.volumeSlider.value = 1.0;
[self.volumeSlider addTarget:self action:@selector(volumeSliderChanged:) forControlEvents:UIControlEventValueChanged];
self.volumeSlider.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.volumeSlider];
// 浏览文件按钮
self.browseButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.browseButton setTitle:@"浏览文件" forState:UIControlStateNormal];
[self.browseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.browseButton.backgroundColor = [UIColor systemPurpleColor];
self.browseButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.browseButton.layer.cornerRadius = 8;
self.browseButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.browseButton addTarget:self action:@selector(browseButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.browseButton];
// 加载按钮
self.loadButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.loadButton setTitle:@"加载Documents文件" forState:UIControlStateNormal];
[self.loadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.loadButton.backgroundColor = [UIColor systemBlueColor];
self.loadButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.loadButton.layer.cornerRadius = 8;
self.loadButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.loadButton addTarget:self action:@selector(loadButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.loadButton];
// 播放按钮
self.playButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.playButton setTitle:@"播放" forState:UIControlStateNormal];
[self.playButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.playButton.backgroundColor = [UIColor systemGreenColor];
self.playButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.playButton.layer.cornerRadius = 6;
self.playButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.playButton addTarget:self action:@selector(playButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.playButton];
// 暂停按钮
self.pauseButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.pauseButton setTitle:@"暂停" forState:UIControlStateNormal];
[self.pauseButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.pauseButton.backgroundColor = [UIColor systemOrangeColor];
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 systemRedColor];
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];
// 删除按钮
self.deleteButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.deleteButton setTitle:@"删除文件" forState:UIControlStateNormal];
[self.deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
self.deleteButton.backgroundColor = [UIColor systemGrayColor];
self.deleteButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightMedium];
self.deleteButton.layer.cornerRadius = 6;
self.deleteButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.deleteButton addTarget:self action:@selector(deleteButtonTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.deleteButton];
// 设置约束
[NSLayoutConstraint activateConstraints:@[
// 文件标签
[self.fileLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:20],
[self.fileLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
[self.fileLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
// 状态标签
[self.statusLabel.topAnchor constraintEqualToAnchor:self.fileLabel.bottomAnchor constant:20],
[self.statusLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
[self.statusLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
// 进度条
[self.progressSlider.topAnchor constraintEqualToAnchor:self.statusLabel.bottomAnchor constant:30],
[self.progressSlider.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
[self.progressSlider.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
// 时间标签
[self.timeLabel.topAnchor constraintEqualToAnchor:self.progressSlider.bottomAnchor constant:8],
[self.timeLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
[self.timeLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
// 音量标签
[volumeLabel.topAnchor constraintEqualToAnchor:self.timeLabel.bottomAnchor constant:30],
[volumeLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:20],
// 音量滑块
[self.volumeSlider.centerYAnchor constraintEqualToAnchor:volumeLabel.centerYAnchor],
[self.volumeSlider.leadingAnchor constraintEqualToAnchor:volumeLabel.trailingAnchor constant:10],
[self.volumeSlider.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-20],
// 浏览按钮
[self.browseButton.topAnchor constraintEqualToAnchor:volumeLabel.bottomAnchor constant:40],
[self.browseButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.browseButton.widthAnchor constraintEqualToConstant:200],
[self.browseButton.heightAnchor constraintEqualToConstant:44],
// 加载按钮
[self.loadButton.topAnchor constraintEqualToAnchor:self.browseButton.bottomAnchor constant:20],
[self.loadButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.loadButton.widthAnchor constraintEqualToConstant:200],
[self.loadButton.heightAnchor constraintEqualToConstant:44],
// 播放按钮
[self.playButton.topAnchor constraintEqualToAnchor:self.loadButton.bottomAnchor constant:20],
[self.playButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:40],
[self.playButton.widthAnchor constraintEqualToConstant:80],
[self.playButton.heightAnchor constraintEqualToConstant:40],
// 暂停按钮
[self.pauseButton.topAnchor constraintEqualToAnchor:self.playButton.topAnchor],
[self.pauseButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.pauseButton.widthAnchor constraintEqualToConstant:80],
[self.pauseButton.heightAnchor constraintEqualToConstant:40],
// 停止按钮
[self.stopButton.topAnchor constraintEqualToAnchor:self.playButton.topAnchor],
[self.stopButton.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-40],
[self.stopButton.widthAnchor constraintEqualToConstant:80],
[self.stopButton.heightAnchor constraintEqualToConstant:40],
// 删除按钮
[self.deleteButton.topAnchor constraintEqualToAnchor:self.stopButton.bottomAnchor constant:20],
[self.deleteButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor],
[self.deleteButton.widthAnchor constraintEqualToConstant:120],
[self.deleteButton.heightAnchor constraintEqualToConstant:40]
]];
[self updateButtonStates];
}
- (void)setupAudioPlayer {
self.audioPlayer = [[AudioPlayer alloc] init];
self.audioPlayer.delegate = self;
}
- (void)autoLoadLatestRecording {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths firstObject];
NSString *recordingPath = [documentsDirectory stringByAppendingPathComponent:@"recording.caf"];
if ([[NSFileManager defaultManager] fileExistsAtPath:recordingPath]) {
[self loadAudioFile:recordingPath];
self.statusLabel.text = @"已自动加载最新录音";
}
}
#pragma mark - Progress Timer
- (void)startProgressTimer {
[self stopProgressTimer];
self.progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:@selector(updateProgress)
userInfo:nil
repeats:YES];
}
- (void)stopProgressTimer {
if (self.progressTimer) {
[self.progressTimer invalidate];
self.progressTimer = nil;
}
}
- (void)updateProgress {
if (self.audioPlayer.isPlaying) {
NSTimeInterval currentTime = self.audioPlayer.currentTime;
NSTimeInterval duration = self.audioPlayer.duration;
if (duration > 0) {
self.progressSlider.value = currentTime / duration;
self.timeLabel.text = [NSString stringWithFormat:@"%@ / %@",
[self timeStringFromTimeInterval:currentTime],
[self timeStringFromTimeInterval:duration]];
}
}
}
- (NSString *)timeStringFromTimeInterval:(NSTimeInterval)timeInterval {
NSInteger minutes = (NSInteger)(timeInterval / 60);
NSInteger seconds = (NSInteger)timeInterval % 60;
return [NSString stringWithFormat:@"%02ld:%02ld", (long)minutes, (long)seconds];
}
#pragma mark - Button Actions
- (void)browseButtonTapped {
UIDocumentPickerViewController *documentPicker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[(NSString *)kUTTypeAudio] inMode:UIDocumentPickerModeOpen];
documentPicker.delegate = self;
documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentViewController:documentPicker animated:YES completion:nil];
}
- (void)loadButtonTapped {
[self showFileList];
}
- (void)showFileList {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths firstObject];
NSError *error;
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:&error];
if (error) {
[self showAlertWithTitle:@"错误" message:@"无法读取Documents目录"];
return;
}
// 过滤出音频文件
NSArray *audioFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.caf' OR self ENDSWITH '.mp3' OR self ENDSWITH '.wav' OR self ENDSWITH '.m4a'"]];
if (audioFiles.count == 0) {
[self showAlertWithTitle:@"提示" message:@"Documents目录中没有找到音频文件"];
return;
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"选择音频文件"
message:@"请选择要播放的音频文件"
preferredStyle:UIAlertControllerStyleActionSheet];
for (NSString *fileName in audioFiles) {
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
NSNumber *fileSize = attributes[NSFileSize];
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:fileSize.longLongValue countStyle:NSByteCountFormatterCountStyleFile];
NSString *title = [NSString stringWithFormat:@"%@ (%@)", fileName, sizeString];
[alert addAction:[UIAlertAction actionWithTitle:title
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self loadAudioFile:filePath];
}]];
}
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
// 为iPad设置弹出位置
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
alert.popoverPresentationController.sourceView = self.loadButton;
alert.popoverPresentationController.sourceRect = self.loadButton.bounds;
}
[self presentViewController:alert animated:YES completion:nil];
}
- (void)loadAudioFile:(NSString *)filePath {
if ([self.audioPlayer loadAudioFile:filePath]) {
self.fileLabel.text = [NSString stringWithFormat:@"已加载: %@", [filePath lastPathComponent]];
self.statusLabel.text = @"音频文件加载成功";
// 显示文件信息
NSError *error;
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
if (!error) {
NSNumber *fileSize = attributes[NSFileSize];
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:fileSize.longLongValue countStyle:NSByteCountFormatterCountStyleFile];
self.fileLabel.text = [NSString stringWithFormat:@"%@ (%@)", [filePath lastPathComponent], sizeString];
}
[self updateButtonStates];
// 重置界面
self.progressSlider.value = 0.0;
self.timeLabel.text = @"00:00 / 00:00";
} else {
self.statusLabel.text = @"音频文件加载失败";
[self showAlertWithTitle:@"错误" message:@"无法加载音频文件,可能文件格式不支持或已损坏"];
}
}
- (void)playButtonTapped {
if ([self.audioPlayer play]) {
self.statusLabel.text = @"播放中";
[self startProgressTimer];
[self updateButtonStates];
}
}
- (void)pauseButtonTapped {
[self.audioPlayer pause];
[self stopProgressTimer];
[self updateButtonStates];
}
- (void)stopButtonTapped {
[self.audioPlayer stop];
[self stopProgressTimer];
self.progressSlider.value = 0.0;
self.timeLabel.text = @"00:00 / 00:00";
[self updateButtonStates];
}
- (void)deleteButtonTapped {
[self showDeleteOptions];
}
- (void)showDeleteOptions {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths firstObject];
NSError *error;
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:&error];
NSArray *audioFiles = [files filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.caf'"]];
if (audioFiles.count == 0) {
[self showAlertWithTitle:@"提示" message:@"没有可删除的音频文件"];
return;
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"删除文件"
message:@"选择要删除的文件"
preferredStyle:UIAlertControllerStyleActionSheet];
for (NSString *fileName in audioFiles) {
[alert addAction:[UIAlertAction actionWithTitle:fileName
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction * _Nonnull action) {
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName];
[self deleteFileAtPath:filePath];
}]];
}
[alert addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
alert.popoverPresentationController.sourceView = self.view;
alert.popoverPresentationController.sourceRect = CGRectMake(self.view.bounds.size.width/2, self.view.bounds.size.height/2, 1, 1);
}
[self presentViewController:alert animated:YES completion:nil];
}
- (void)deleteFileAtPath:(NSString *)filePath {
NSError *error;
if ([[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]) {
[self showAlertWithTitle:@"成功" message:@"文件已删除"];
// 如果删除的是当前正在播放的文件,停止播放
if ([self.audioPlayer isPlaying] || [self.audioPlayer isPaused]) {
[self.audioPlayer stop];
[self updateButtonStates];
}
// 更新界面
self.fileLabel.text = @"未加载音频文件";
self.statusLabel.text = @"文件已删除";
self.progressSlider.value = 0.0;
self.timeLabel.text = @"00:00 / 00:00";
} else {
[self showAlertWithTitle:@"错误" message:[NSString stringWithFormat:@"删除失败: %@", error.localizedDescription]];
}
}
- (void)progressSliderChanged:(UISlider *)slider {
if (self.audioPlayer.duration > 0) {
NSTimeInterval targetTime = slider.value * self.audioPlayer.duration;
self.timeLabel.text = [NSString stringWithFormat:@"%@ / %@",
[self timeStringFromTimeInterval:targetTime],
[self timeStringFromTimeInterval:self.audioPlayer.duration]];
}
}
- (void)progressSliderTouchDown {
[self stopProgressTimer];
}
- (void)progressSliderTouchUp {
if (self.audioPlayer.duration > 0) {
NSTimeInterval targetTime = self.progressSlider.value * self.audioPlayer.duration;
[self.audioPlayer seekToTime:targetTime];
[self startProgressTimer];
}
}
- (void)volumeSliderChanged:(UISlider *)slider {
self.audioPlayer.volume = slider.value;
}
- (void)updateButtonStates {
BOOL hasFile = (self.audioPlayer != nil);
BOOL isPlaying = self.audioPlayer.isPlaying;
BOOL isPaused = self.audioPlayer.isPaused;
self.playButton.enabled = hasFile && !isPlaying;
self.pauseButton.enabled = hasFile && isPlaying;
self.stopButton.enabled = hasFile && (isPlaying || isPaused);
self.playButton.alpha = self.playButton.enabled ? 1.0 : 0.5;
self.pauseButton.alpha = self.pauseButton.enabled ? 1.0 : 0.5;
self.stopButton.alpha = self.stopButton.enabled ? 1.0 : 0.5;
self.progressSlider.enabled = hasFile;
self.deleteButton.enabled = hasFile;
self.deleteButton.alpha = self.deleteButton.enabled ? 1.0 : 0.5;
}
#pragma mark - UIDocumentPickerDelegate
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
if (urls.count > 0) {
NSURL *selectedURL = urls.firstObject;
// 确保我们有访问权限
[selectedURL startAccessingSecurityScopedResource];
// 获取文件路径
NSString *filePath = selectedURL.path;
[self loadAudioFile:filePath];
[selectedURL stopAccessingSecurityScopedResource];
}
}
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
self.statusLabel.text = @"文件选择已取消";
}
#pragma mark - AudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AudioPlayer *)player successfully:(BOOL)flag {
self.statusLabel.text = @"播放完成";
[self stopProgressTimer];
self.progressSlider.value = 1.0;
[self updateButtonStates];
}
- (void)audioPlayerDidStartPlaying:(AudioPlayer *)player {
self.statusLabel.text = @"播放中";
[self startProgressTimer];
[self updateButtonStates];
}
- (void)audioPlayerDidPause:(AudioPlayer *)player {
self.statusLabel.text = @"已暂停";
[self updateButtonStates];
}
- (void)audioPlayerDidStop:(AudioPlayer *)player {
self.statusLabel.text = @"已停止";
[self updateButtonStates];
}
- (void)audioPlayerDecodeErrorDidOccur:(AudioPlayer *)player error:(NSError *)error {
self.statusLabel.text = [NSString stringWithFormat:@"解码错误: %@", error.localizedDescription];
[self stopProgressTimer];
[self updateButtonStates];
}
#pragma mark - Helper Methods
- (void)showAlertWithTitle:(NSString *)title message:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
- (void)dealloc {
[self stopProgressTimer];
}
@end
info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSMicrophoneUsageDescription</key>
<string>此应用需要访问麦克风以进行音频录制和播放</string>
</dict>
</plist>
三、源码分析
- AppDelegate.m
application:didFinishLaunchingWithOptions:
-
功能:应用启动入口点
-
具体作用:
-
创建主窗口
-
初始化音频播放视图控制器
-
设置导航控制器并启用大标题
-
配置根视图控制器
-
显示窗口
-
- AudioPlayer.h (协议部分)
AudioPlayerDelegate 协议方法
-
audioPlayerDidFinishPlaying:successfully:- 播放完成回调 -
audioPlayerDecodeErrorDidOccur:error:- 解码错误回调 -
audioPlayerDidStartPlaying:- 开始播放回调 -
audioPlayerDidPause:- 暂停回调 -
audioPlayerDidStop:- 停止回调
- AudioPlayer.m
核心数据结构
objc
typedef struct AQPlayerState {
AudioStreamBasicDescription mDataFormat; // 音频格式描述
AudioQueueRef mQueue; // 音频队列引用
AudioQueueBufferRef mBuffers[kNumberBuffers]; // 音频缓冲区
AudioFileID mAudioFile; // 音频文件ID
UInt32 bufferByteSize; // 缓冲区大小
SInt64 mCurrentPacket; // 当前包位置
UInt32 mNumPacketsToRead; // 每次读取包数
bool mIsRunning; // 是否运行中
bool mIsPaused; // 是否暂停
NSTimeInterval mDuration; // 音频时长
} AQPlayerState;
静态回调函数
HandleOutputBuffer
-
功能:音频队列输出缓冲区回调
-
触发时机:当音频队列需要填充数据时
-
调用链:系统音频队列 → 此函数 →
handleOutputBuffer:inAudioQueue:
AudioQueuePropertyListener
-
功能:音频队列属性监听回调
-
监听属性:
kAudioQueueProperty_IsRunning -
调用链:系统音频队列 → 此函数 →
handlePropertyListener:inAudioQueue:
实例方法
handleOutputBuffer:inAudioQueue:
objc
- (void)handleOutputBuffer:(AudioQueueBufferRef)inBuffer inAudioQueue:(AudioQueueRef)inAQ
-
功能:处理音频数据填充到缓冲区
-
流程:
-
检查播放状态
-
从音频文件读取数据包
-
将数据填入音频队列缓冲区
-
更新当前包位置
-
检测播放完成
-
handlePropertyListener:inAudioQueue:
objc
- (void)handlePropertyListener:(AudioQueuePropertyID)inID inAudioQueue:(AudioQueueRef)inAQ
-
功能:处理音频队列属性变化
-
主要处理:播放状态变化,通知代理停止事件
setupAudioPlayer
-
功能:初始化音频播放器状态
-
操作:清零结构体,设置初始状态
cleanup
-
功能:清理音频资源
-
操作:释放音频队列,关闭音频文件
calculateDuration
-
功能:计算音频文件总时长
-
使用API:
AudioFileGetProperty+kAudioFilePropertyEstimatedDuration
deriveBufferSize
-
功能:计算合适的缓冲区大小
-
算法:基于采样率、帧率和最大包大小计算
loadAudioFile:
objc
- (BOOL)loadAudioFile:(NSString *)filePath
-
功能:加载音频文件并初始化播放环境
-
详细流程:
-
清理现有资源
-
检查文件存在性
-
打开音频文件 (
AudioFileOpenURL) -
获取音频格式 (
AudioFileGetProperty) -
创建音频队列 (
AudioQueueNewOutput) -
添加属性监听器
-
计算缓冲区和时长
-
分配和入队缓冲区
-
play
-
功能:开始或恢复播放
-
两种模式:
-
从暂停恢复:直接启动队列
-
重新开始:重置位置,重新填充缓冲区
-
pause
-
功能:暂停播放
-
操作:暂停音频队列,更新状态,通知代理
stop
-
功能:停止播放
-
操作:立即停止队列,重置位置,更新状态
seekToTime:
objc
- (BOOL)seekToTime:(NSTimeInterval)time
-
功能:跳转到指定时间位置
-
流程:
-
参数验证
-
记录播放状态
-
计算目标包位置
-
重置音频队列
-
重新填充缓冲区
-
恢复播放状态
-
属性访问方法
-
setVolume:- 设置音量 -
isPlaying- 检查是否正在播放 -
isPaused- 检查是否暂停 -
duration- 获取音频时长 -
currentTime- 获取当前播放时间
- AudioPlayerViewController.m
生命周期方法
viewDidLoad
-
功能:视图控制器初始化
-
操作:设置UI、初始化播放器、自动加载录音
setupUI
-
功能:创建和布局所有UI控件
-
创建控件:按钮、滑块、标签等
-
使用Auto Layout进行布局
进度管理方法
startProgressTimer
- 功能:启动进度更新定时器(0.1秒间隔)
stopProgressTimer
- 功能:停止进度定时器
updateProgress
- 功能:更新进度条和时间显示
按钮动作方法
browseButtonTapped - 打开文件选择器
loadButtonTapped - 显示Documents文件列表
playButtonTapped - 开始播放
pauseButtonTapped - 暂停播放
stopButtonTapped - 停止播放
deleteButtonTapped - 删除文件
文件管理方法
showFileList
-
功能:显示Documents目录中的音频文件列表
-
过滤格式:.caf, .mp3, .wav, .m4a
loadAudioFile:
-
功能:加载选中的音频文件
-
操作:调用播放器加载,更新UI状态
showDeleteOptions & deleteFileAtPath:
- 功能:文件删除功能
代理方法实现
UIDocumentPickerDelegate
-
documentPicker:didPickDocumentsAtURLs:- 处理选中的文件 -
documentPickerWasCancelled:- 处理选择取消
AudioPlayerDelegate
- 实现所有协议方法,更新UI状态
技术特点分析
-
底层音频API:使用AudioToolbox框架,直接操作音频队列
-
缓冲区管理:三重缓冲机制,确保流畅播放
-
精确控制:支持seek、暂停、恢复等精细操作
-
完整生命周期:正确的资源管理和清理
-
错误处理:完善的错误检测和回调机制
-
UI交互:响应式UI状态更新