服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - IOS - iOS实时录音和播放功能

iOS实时录音和播放功能

2021-03-19 15:42人生为代码而活 IOS

这篇文章主要为大家详细介绍了iOS实时录音和播放功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

需求:最近公司需要做一个楼宇对讲的功能:门口机(连接WIFI)拨号对室内机(对应的WIFI)的设备进行呼叫,室内机收到呼叫之后将对收到的数据进行UDP广播的转发,手机(连接对应的WIFI)收到视频流之后,实时的展示视频数据(手机可以接听,挂断,手机接听之后,室内机不展示视频,只是进行转发。)

简单点说就是手机客户端需要做一个类似于直播平台的软件,可以实时的展示视频,实时的播放接收到的声音数据,并且实时将手机麦克风收到的声音回传给室内机,室内机负责转发给门口机。 

这篇文章介绍iOS怎么进行实时的录音和播放收到的声音数据 

想要使用系统的框架实时播放声音和录音数据,就得知道音频队列服务,

在AudioToolbox框架中的音频队列服务,它完全可以做到音频播放和录制,

一个音频服务队列有三个部分组成:

1.三个缓冲器Buffers:没个缓冲器都是一个存储音频数据的临时仓库。
2.一个缓冲队列Buffer Queue:一个包含音频缓冲器的有序队列。
3.一个回调CallBack:一个自定义的队列回调函数。

 具体怎么运转的还是百度吧!

我的简单理解:

对于播放:系统会自动从缓冲队列中循环取出每个缓冲器中的数据进行播放,我们需要做的就是将接收到的数据循环的放到缓冲器中,剩下的就交给系统去实现了。

对于录音:  系统会自动将录的声音放入队列中的每个缓冲器中,我们需要做的就是从回调函数中将数据转化我们自己的数据就OK了。

#pragma mark--实时播放

1. 导入系统框架AudioToolbox.framework  AVFoundation.framework

2. 获取麦克风权限,在工程的Info.plist文件中加入Privacy - Microphone Usage Description 这个key 描述:App想要访问您的麦克风

3. 创建播放声音的类 EYAudio 

EYAudio.h

?
1
2
3
4
5
6
7
8
9
10
11
12
#import <Foundation/Foundation.h>
 
@interface EYAudio : NSObject
 
// 播放的数据流数据
- (void)playWithData:(NSData *)data;
// 声音播放出现问题的时候可以重置一下
- (void)resetPlay;
// 停止播放
- (void)stop;
 
@end

EYAudio.m

 #import "EYAudio.h"

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
 
#define MIN_SIZE_PER_FRAME 1920  //每个包的大小,室内机要求为960,具体看下面的配置信息
#define QUEUE_BUFFER_SIZE 3   //缓冲器个数
#define SAMPLE_RATE    16000 //采样频率
 
@interface EYAudio(){
  AudioQueueRef audioQueue;                 //音频播放队列
  AudioStreamBasicDescription _audioDescription;
  AudioQueueBufferRef audioQueueBuffers[QUEUE_BUFFER_SIZE]; //音频缓存
  BOOL audioQueueBufferUsed[QUEUE_BUFFER_SIZE];       //判断音频缓存是否在使用
  NSLock *sysnLock;
  NSMutableData *tempData;
  OSStatus osState;
}
@end
 
@implementation EYAudio
 
#pragma mark - 提前设置AVAudioSessionCategoryMultiRoute 播放和录音
+ (void)initialize
{
  NSError *error = nil;
  //只想要播放:AVAudioSessionCategoryPlayback
  //只想要录音:AVAudioSessionCategoryRecord
  //想要"播放和录音"同时进行 必须设置为:AVAudioSessionCategoryMultiRoute 而不是AVAudioSessionCategoryPlayAndRecord(设置这个不好使)
  BOOL ret = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryMultiRoute error:&error];
  if (!ret) {
    NSLog(@"设置声音环境失败");
    return;
  }
  //启用audio session
  ret = [[AVAudioSession sharedInstance] setActive:YES error:&error];
  if (!ret)
  {
    NSLog(@"启动失败");
    return;
  }
}
 
- (void)resetPlay
{
  if (audioQueue != nil) {
    AudioQueueReset(audioQueue);
  }
}
 
- (void)stop
{
  if (audioQueue != nil) {
    AudioQueueStop(audioQueue,true);
  }
 
  audioQueue = nil;
  sysnLock = nil;
}
 
- (instancetype)init
{
  self = [super init];
  if (self) {
    sysnLock = [[NSLock alloc]init];
 
    //设置音频参数 具体的信息需要问后台
    _audioDescription.mSampleRate = SAMPLE_RATE;
    _audioDescription.mFormatID = kAudioFormatLinearPCM;
    _audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    //1单声道
    _audioDescription.mChannelsPerFrame = 1;
    //每一个packet一侦数据,每个数据包下的桢数,即每个数据包里面有多少桢
    _audioDescription.mFramesPerPacket = 1;
    //每个采样点16bit量化 语音每采样点占用位数
    _audioDescription.mBitsPerChannel = 16;
    _audioDescription.mBytesPerFrame = (_audioDescription.mBitsPerChannel / 8) * _audioDescription.mChannelsPerFrame;
    //每个数据包的bytes总数,每桢的bytes数*每个数据包的桢数
    _audioDescription.mBytesPerPacket = _audioDescription.mBytesPerFrame * _audioDescription.mFramesPerPacket;
 
    // 使用player的内部线程播放 新建输出
    AudioQueueNewOutput(&_audioDescription, AudioPlayerAQInputCallback, (__bridge void * _Nullable)(self), nil, 0, 0, &audioQueue);
 
    // 设置音量
    AudioQueueSetParameter(audioQueue, kAudioQueueParam_Volume, 1.0);
 
    // 初始化需要的缓冲区
    for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
      audioQueueBufferUsed[i] = false;
      osState = AudioQueueAllocateBuffer(audioQueue, MIN_SIZE_PER_FRAME, &audioQueueBuffers[i]);
    }
 
    osState = AudioQueueStart(audioQueue, NULL);
    if (osState != noErr) {
      NSLog(@"AudioQueueStart Error");
    }
  }
  return self;
}
 
// 播放数据
-(void)playWithData:(NSData *)data
{
  [sysnLock lock];
 
  tempData = [NSMutableData new];
  [tempData appendData: data];
  NSUInteger len = tempData.length;
  Byte *bytes = (Byte*)malloc(len);
  [tempData getBytes:bytes length: len];
 
  int i = 0;
  while (true) {
    if (!audioQueueBufferUsed[i]) {
      audioQueueBufferUsed[i] = true;
      break;
    }else {
      i++;
      if (i >= QUEUE_BUFFER_SIZE) {
        i = 0;
      }
    }
  }
 
  audioQueueBuffers[i] -> mAudioDataByteSize = (unsigned int)len;
  // 把bytes的头地址开始的len字节给mAudioData,向第i个缓冲器
  memcpy(audioQueueBuffers[i] -> mAudioData, bytes, len);
 
  // 释放对象
  free(bytes);
 
  //将第i个缓冲器放到队列中,剩下的都交给系统了
  AudioQueueEnqueueBuffer(audioQueue, audioQueueBuffers[i], 0, NULL);
 
  [sysnLock unlock];
}
 
// ************************** 回调 **********************************
// 回调回来把buffer状态设为未使用
static void AudioPlayerAQInputCallback(void* inUserData,AudioQueueRef audioQueueRef, AudioQueueBufferRef audioQueueBufferRef) {
 
  EYAudio* audio = (__bridge EYAudio*)inUserData;
 
  [audio resetBufferState:audioQueueRef and:audioQueueBufferRef];
}
 
- (void)resetBufferState:(AudioQueueRef)audioQueueRef and:(AudioQueueBufferRef)audioQueueBufferRef {
  // 防止空数据让audioqueue后续都不播放,为了安全防护一下
  if (tempData.length == 0) {
    audioQueueBufferRef->mAudioDataByteSize = 1;
    Byte* byte = audioQueueBufferRef->mAudioData;
    byte = 0;
    AudioQueueEnqueueBuffer(audioQueueRef, audioQueueBufferRef, 0, NULL);
  }
 
  for (int i = 0; i < QUEUE_BUFFER_SIZE; i++) {
    // 将这个buffer设为未使用
    if (audioQueueBufferRef == audioQueueBuffers[i]) {
      audioQueueBufferUsed[i] = false;
    }
  }
}
 
@end

 外界使用: 不断调用下面的方法将NSData传递进来

- (void)playWithData:(NSData *)data;

#pragma mark--实时录音

1. 导入系统框架AudioToolbox.framework  AVFoundation.framework

2. 创建录音的类 EYRecord

EYRecord.h

?
1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>
 
@interface ESARecord : NSObject
 
//开始录音
- (void)startRecording;
 
//停止录音
- (void)stopRecording;
 
@end

EYRecord.m

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#import "ESARecord.h"
#import <AudioToolbox/AudioToolbox.h>
 
#define QUEUE_BUFFER_SIZE 3   // 输出音频队列缓冲个数
#define kDefaultBufferDurationSeconds 0.03//调整这个值使得录音的缓冲区大小为960,实际会小于或等于960,需要处理小于960的情况
#define kDefaultSampleRate 16000  //定义采样率为16000
 
extern NSString * const ESAIntercomNotifationRecordString;
 
static BOOL isRecording = NO;
 
@interface ESARecord(){
  AudioQueueRef _audioQueue;             //输出音频播放队列
  AudioStreamBasicDescription _recordFormat;
  AudioQueueBufferRef _audioBuffers[QUEUE_BUFFER_SIZE]; //输出音频缓存
}
@property (nonatomic, assign) BOOL isRecording;
 
@end
 
@implementation ESARecord
 
- (instancetype)init
{
  self = [super init];
  if (self) {
    //重置下
    memset(&_recordFormat, 0, sizeof(_recordFormat));
    _recordFormat.mSampleRate = kDefaultSampleRate;
    _recordFormat.mChannelsPerFrame = 1;
    _recordFormat.mFormatID = kAudioFormatLinearPCM;
 
    _recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
    _recordFormat.mBitsPerChannel = 16;
    _recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
    _recordFormat.mFramesPerPacket = 1;
 
    //初始化音频输入队列
    AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);
 
    //计算估算的缓存区大小
    int frames = (int)ceil(kDefaultBufferDurationSeconds * _recordFormat.mSampleRate);
    int bufferByteSize = frames * _recordFormat.mBytesPerFrame;
 
    NSLog(@"缓存区大小%d",bufferByteSize);
 
    //创建缓冲器
    for (int i = 0; i < QUEUE_BUFFER_SIZE; i++){
      AudioQueueAllocateBuffer(_audioQueue, bufferByteSize, &_audioBuffers[i]);
      AudioQueueEnqueueBuffer(_audioQueue, _audioBuffers[i], 0, NULL);
    }
  }
  return self;
}
 
-(void)startRecording
{
  // 开始录音
  AudioQueueStart(_audioQueue, NULL);
  isRecording = YES;
}
 
void inputBufferHandler(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime,UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc)
{
  if (inNumPackets > 0) {
    ESARecord *recorder = (__bridge ESARecord*)inUserData;
    [recorder processAudioBuffer:inBuffer withQueue:inAQ];
  }
  
  if (isRecording) {
    AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
  }
}
 
- (void)processAudioBuffer:(AudioQueueBufferRef )audioQueueBufferRef withQueue:(AudioQueueRef )audioQueueRef
{
  NSMutableData * dataM = [NSMutableData dataWithBytes:audioQueueBufferRef->mAudioData length:audioQueueBufferRef->mAudioDataByteSize];
  
  if (dataM.length < 960) { //处理长度小于960的情况,此处是补00
    Byte byte[] = {0x00};
    NSData * zeroData = [[NSData alloc] initWithBytes:byte length:1];
    for (NSUInteger i = dataM.length; i < 960; i++) {
      [dataM appendData:zeroData];
    }
  }
 
  // NSLog(@"实时录音的数据--%@", dataM);
  //此处是发通知将dataM 传递出去
  [[NSNotificationCenter defaultCenter] postNotificationName:@"EYRecordNotifacation" object:@{@"data" : dataM}];
}
 
-(void)stopRecording
{
  if (isRecording)
  {
    isRecording = NO;
    
    //停止录音队列和移除缓冲区,以及关闭session,这里无需考虑成功与否
    AudioQueueStop(_audioQueue, true);
    //移除缓冲区,true代表立即结束录制,false代表将缓冲区处理完再结束
    AudioQueueDispose(_audioQueue, true);
  }
  NSLog(@"停止录音");
}
 
@end

如果不好使尝试将 EYRecord.m ----> EYRecord.mm

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

延伸 · 阅读

精彩推荐
  • IOSIOS 屏幕适配方案实现缩放window的示例代码

    IOS 屏幕适配方案实现缩放window的示例代码

    这篇文章主要介绍了IOS 屏幕适配方案实现缩放window的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要...

    xiari5772021-06-01
  • IOSiOS 雷达效果实例详解

    iOS 雷达效果实例详解

    这篇文章主要介绍了iOS 雷达效果实例详解的相关资料,需要的朋友可以参考下...

    SimpleWorld11022021-01-28
  • IOSiOS布局渲染之UIView方法的调用时机详解

    iOS布局渲染之UIView方法的调用时机详解

    在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题,下面这篇文章主要给大家介绍了关于iOS布局渲染之UIView方法调用时机的相关资料...

    windtersharp7642021-05-04
  • IOS关于iOS自适应cell行高的那些事儿

    关于iOS自适应cell行高的那些事儿

    这篇文章主要给大家介绍了关于iOS自适应cell行高的那些事儿,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的...

    daisy6092021-05-17
  • IOSIOS开发之字典转字符串的实例详解

    IOS开发之字典转字符串的实例详解

    这篇文章主要介绍了IOS开发之字典转字符串的实例详解的相关资料,希望通过本文能帮助到大家,让大家掌握这样的方法,需要的朋友可以参考下...

    苦练内功5832021-04-01
  • IOS解析iOS开发中的FirstResponder第一响应对象

    解析iOS开发中的FirstResponder第一响应对象

    这篇文章主要介绍了解析iOS开发中的FirstResponder第一响应对象,包括View的FirstResponder的释放问题,需要的朋友可以参考下...

    一片枫叶4662020-12-25
  • IOSiOS通过逆向理解Block的内存模型

    iOS通过逆向理解Block的内存模型

    自从对 iOS 的逆向初窥门径后,我也经常通过它来分析一些比较大的应用,参考一下这些应用中某些功能的实现。这个探索的过程乐趣多多,不仅能满足自...

    Swiftyper12832021-03-03
  • IOSiOS中tableview 两级cell的展开与收回的示例代码

    iOS中tableview 两级cell的展开与收回的示例代码

    本篇文章主要介绍了iOS中tableview 两级cell的展开与收回的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    J_Kang3862021-04-22