开源 Objective-C IOS 应用开发(十八)音频的播放

文章的目的为了记录使用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>

三、源码分析

  1. AppDelegate.m

application:didFinishLaunchingWithOptions:

  • 功能:应用启动入口点

  • 具体作用:

    • 创建主窗口

    • 初始化音频播放视图控制器

    • 设置导航控制器并启用大标题

    • 配置根视图控制器

    • 显示窗口

  1. AudioPlayer.h (协议部分)

AudioPlayerDelegate 协议方法

  • audioPlayerDidFinishPlaying:successfully: - 播放完成回调

  • audioPlayerDecodeErrorDidOccur:error: - 解码错误回调

  • audioPlayerDidStartPlaying: - 开始播放回调

  • audioPlayerDidPause: - 暂停回调

  • audioPlayerDidStop: - 停止回调

  1. 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
  • 功能:处理音频数据填充到缓冲区

  • 流程:

    1. 检查播放状态

    2. 从音频文件读取数据包

    3. 将数据填入音频队列缓冲区

    4. 更新当前包位置

    5. 检测播放完成

handlePropertyListener:inAudioQueue:

objc

  • (void)handlePropertyListener:(AudioQueuePropertyID)inID inAudioQueue:(AudioQueueRef)inAQ
  • 功能:处理音频队列属性变化

  • 主要处理:播放状态变化,通知代理停止事件

setupAudioPlayer

  • 功能:初始化音频播放器状态

  • 操作:清零结构体,设置初始状态

cleanup

  • 功能:清理音频资源

  • 操作:释放音频队列,关闭音频文件

calculateDuration

  • 功能:计算音频文件总时长

  • 使用API:AudioFileGetProperty + kAudioFilePropertyEstimatedDuration

deriveBufferSize

  • 功能:计算合适的缓冲区大小

  • 算法:基于采样率、帧率和最大包大小计算

loadAudioFile:

objc

  • (BOOL)loadAudioFile:(NSString *)filePath
  • 功能:加载音频文件并初始化播放环境

  • 详细流程:

    1. 清理现有资源

    2. 检查文件存在性

    3. 打开音频文件 (AudioFileOpenURL)

    4. 获取音频格式 (AudioFileGetProperty)

    5. 创建音频队列 (AudioQueueNewOutput)

    6. 添加属性监听器

    7. 计算缓冲区和时长

    8. 分配和入队缓冲区

play

  • 功能:开始或恢复播放

  • 两种模式:

    • 从暂停恢复:直接启动队列

    • 重新开始:重置位置,重新填充缓冲区

pause

  • 功能:暂停播放

  • 操作:暂停音频队列,更新状态,通知代理

stop

  • 功能:停止播放

  • 操作:立即停止队列,重置位置,更新状态

seekToTime:

objc

  • (BOOL)seekToTime:(NSTimeInterval)time
  • 功能:跳转到指定时间位置

  • 流程:

    1. 参数验证

    2. 记录播放状态

    3. 计算目标包位置

    4. 重置音频队列

    5. 重新填充缓冲区

    6. 恢复播放状态

属性访问方法

  • setVolume: - 设置音量

  • isPlaying - 检查是否正在播放

  • isPaused - 检查是否暂停

  • duration - 获取音频时长

  • currentTime - 获取当前播放时间

  1. 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状态

技术特点分析

  1. 底层音频API:使用AudioToolbox框架,直接操作音频队列

  2. 缓冲区管理:三重缓冲机制,确保流畅播放

  3. 精确控制:支持seek、暂停、恢复等精细操作

  4. 完整生命周期:正确的资源管理和清理

  5. 错误处理:完善的错误检测和回调机制

  6. UI交互:响应式UI状态更新

相关推荐
胖虎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 小时前
本地运行开源大语言模型工具全览与对比
人工智能·语言模型·开源