iOS实时录音和播放功能

时间:2022-04-01 14:04:50

需求:最近公司需要做一个楼宇对讲的功能:门口机(连接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

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