coreAudio recording
.h
#import <UIKit/UIKit.h>
#import <AudioToolbox/AudioToolbox.h>
#import <CoreAudioKit/CoreAudioKit.h>
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>
//오디오 큐 서비스를 사용하는 클래스 정의
enum {
kNumberBuffers = 3, //오디오 버퍼큐는 3개
kNumberPackages = 10* 1000,
};
struct RecorderStat
{
AudioStreamBasicDescription mDataFormat; //오디오 스트림의 광범위한 특성정의
AudioQueueRef mQueue; // 재생오디오큐
AudioQueueBufferRef mBuffers[kNumberBuffers]; //오디오 큐 버퍼 포인트의 리스트
AudioFileID mAudioFile; //오디오 파일 객체
UInt32 bufferByteSize; //오디오 큐 버퍼 하나의 사이즈
SInt64 mCurrentPacket; //다음 재생할 패킷의 위치
AudioStreamPacketDescription *mPacketsToRead; // vbr 오디오 인경우 필요한 pecket 정보
bool mIsRunning; // 현재 재생 오디오 큐가 재생 중인지
};
@interface ViewController : UIViewController <AVAudioPlayerDelegate>
@end
.m
//
#import "ViewController.h"
#define VErr(err, msg) do {\
if(nil != err) {\
NSLog(@"[ERR]:%@--%@", (msg), [err localizedDescription]);\
return ;\
}\
} while(0)
#define VStatus(err, msg) do {\
if(noErr != err) {\
NSLog(@"[ERR:%d]:%@", err, (msg));\
return ;\
}\
} while(0)
#define VStatusBOOL(err, msg) do {\
if(noErr != err) {\
NSLog(@"[ERR:%d]:%@", err, (msg));\
return NO;\
}\
} while(0)
//콜백 함수
static void impAudioQueueInputCallback(
void * inUserData, //AudioQueueNewInput 으로부터 전달된 사용자 데이터
AudioQueueRef inAQ, //콜백을 부른 오디오 큐
AudioQueueBufferRef inBuffer, //채워야 할 버퍼
const AudioTimeStamp * inStartTime,
UInt32 inNumberPacketDescription, //실제 읽은 packet 개수 (output)
const AudioStreamPacketDescription *inPacketDescs) //aspd array의 포인터, cbr인 경우 null
{
NSLog(@"impAudioQueueInputCallback 진입");
struct RecorderStat *recorderStat = (struct RecorderStat *) inUserData;
//레코딩이 재생 중이 아니라면
NSLog(@"recoderStat.Ruing : %@" , recorderStat->mIsRunning ? @"true" : @"false");
if (!recorderStat->mIsRunning) {
OSStatus stts = AudioQueueStop(recorderStat->mQueue, true);
VStatus(stts, @"AudioQueueStop error!");
AudioFileClose(recorderStat -> mAudioFile);
return;
}
//for CBR
if (0 == inNumberPacketDescription && recorderStat-> mDataFormat.mBytesPerPacket != 0) {
inNumberPacketDescription = recorderStat -> bufferByteSize/recorderStat -> mDataFormat.mBytesPerPacket;
}
//패킷을 파일에 기록
OSStatus stt = AudioFileWritePackets(
recorderStat->mAudioFile, //데이터를 기록할 AudioFile
FALSE, //cache를 할지 말지
recorderStat->bufferByteSize, // 데이터의 길이 (input)
inPacketDescs, //aspd array의 포인터, cbr인 경우 null
recorderStat->mCurrentPacket, //기록할 패킷의 위치
&inNumberPacketDescription, //실제 읽은 packet 개수 (output)
inBuffer->mAudioData); //채울 오디오 버퍼?
VStatus(stt, @"AudioFileWritePackets Error!!");
//패킷의 인덱스 증가
recorderStat->mCurrentPacket += inNumberPacketDescription; //현재 읽고 있는 packet의 위치
//버퍼가 준비가 되었다면 AudioQueueEnqueueBuffer함수를 이용해 해당 버퍼를 오디오 큐에 밀어넣어 준다.
stt = AudioQueueEnqueueBuffer(
recorderStat->mQueue, //버퍼를 넣을 오디오 큐
inBuffer, //넣을 버퍼
0, //aspd 의 갯수, cbr == 0
NULL);
VStatus(stt, @"AudioQueueEnqueueBuffer Error!!");
}
@interface ViewController (){
struct RecorderStat recorderStat_;
}
@property (strong, nonatomic) IBOutlet UIButton *prepareBtn; //녹음을 위한 준비 버튼
@property (strong, nonatomic) IBOutlet UIButton *recordBtn; //녹음하기 버튼
@property (strong,nonatomic) NSString *filePath;
@property(strong,nonatomic) AVAudioPlayer *player;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
//오디오 레코더 셋팅 (준비)
-(BOOL) prepareAudioRecorder{
NSLog(@"prepareAudioRecorder 진입");
//파일 경로 지정
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
//데이트 포멧으로 파일명 지정
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
[dateFormatter setDateFormat:@"yyyy-MM-dd_HH-mm-ss"];
_filePath = [NSString stringWithFormat:@"%@/%@.wav" , docDir, [dateFormatter stringFromDate:[NSDate date]]];
NSLog(@"startRecord _filePath : %@ ", _filePath);
if (recorderStat_.mCurrentPacket) {
NSLog(@"prepareAudioRecorder - mCurrentPacket 두번째 이상 녹음입니다. - 현재 패킷 초기화!!");
recorderStat_.mCurrentPacket = 0;
}
OSStatus stts = noErr;
// step 1: 레코딩 포멧 셋팅
recorderStat_.mDataFormat.mFormatID = kAudioFormatLinearPCM;
recorderStat_.mDataFormat.mSampleRate = 44100.0;
recorderStat_.mDataFormat.mChannelsPerFrame = 2;
recorderStat_.mDataFormat.mBitsPerChannel = 16;
recorderStat_.mDataFormat.mFramesPerPacket = 1;
recorderStat_.mDataFormat.mBytesPerFrame = recorderStat_.mDataFormat.mChannelsPerFrame *
recorderStat_.mDataFormat.mBitsPerChannel / 8;
recorderStat_.mDataFormat.mBytesPerPacket = recorderStat_.mDataFormat.mBytesPerFrame *
recorderStat_.mDataFormat.mFramesPerPacket;
recorderStat_.mDataFormat.mFormatFlags = kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagIsSignedInteger |
kAudioFormatFlagIsBigEndian;
//mBytesPerFrame : 4 , mBytesPerPacket : 4
NSLog(@"prepareAudioRecorder - mBytesPerFrame : %d , mBytesPerPacket : %d" ,
recorderStat_.mDataFormat.mBytesPerFrame, recorderStat_.mDataFormat.mBytesPerPacket);
/*
코어오디오 에서의 AudioQueue는 오디오 하드웨어의 일부와 통하는 간단한 인터페이스이다.
하드웨어의 일부라 함은 보통 스피커나 마이크를 말한다.
AudioQueue는 버퍼 큐와 연결되어 있고 버퍼는 실제 오디오 데이터를 가지고 있는 메모리 블럭이다.
재생 큐는 데이터로 채워진 버퍼를 받아서 스피커 하드웨어에 전달 한 뒤 빈 버퍼를 콜백 함수에 전달한다.
콜백 함수에서 파일로부터 오디오 데이터를 읽어 버퍼에 담아준 뒤 오디오 큐에 전달한다.
*/
//step 2: audio input queue 생성
stts = AudioQueueNewInput(
&recorderStat_.mDataFormat,
impAudioQueueInputCallback, //콜백 함수
&recorderStat_, //사용자정의 구조체
CFRunLoopGetCurrent(), //inCallbackRunLoop
kCFRunLoopCommonModes, //inCallbackRunLoopMode
0,
&recorderStat_.mQueue); //input AudioQueue
VStatusBOOL(stts, @"AudioQueueNewInput");
// step 3: 상세한 포멧 정보
UInt32 dataFormatSize = sizeof(recorderStat_.mDataFormat);
stts = AudioQueueGetProperty(
recorderStat_.mQueue,
kAudioQueueProperty_StreamDescription,
&recorderStat_.mDataFormat,
&dataFormatSize);
//prepareAudioRecorder - dataFormatSize : 40
NSLog(@"prepareAudioRecorder - dataFormatSize : %d" , dataFormatSize);
VStatusBOOL(stts, @"AudioQueueGetProperty-AudioQueueGetProperty");
// step 4: 오디오 파일 생성
NSURL *tempURL = [NSURL URLWithString:_filePath];
CFURLRef url = (__bridge CFURLRef)(tempURL);
stts = AudioFileCreateWithURL(
url,
kAudioFileAIFFType,
&recorderStat_.mDataFormat,
kAudioFileFlags_EraseFile,
&recorderStat_.mAudioFile);
VStatusBOOL(stts, @"AudioFileOpenURL");
NSLog(@"open file %@ success!", url);
// step 5: 버퍼와 버퍼 큐 준비
/*
코어오디오에서 오디오 데이터를 오디오 큐에 전달하기 위해서는 AudioQueueBuffer 라는 객체를 사용한다.
이 버퍼를 생성하기 전에 재생할 오디오 데이터를 분석해서 어떤 사이즈의 버퍼를 사용할 지, 한 번에 몇개의 packet을 읽을지 결정해야 한다.
다음은 사용할 버퍼의 사이즈를 bufferByteSize에 저장한다.
*/
//prepareAudioRecorder - kNumberPackages : 10000 , mBytesPerPacket : 4
NSLog(@"prepareAudioRecorder - kNumberPackages : %d , mBytesPerPacket : %d" , kNumberPackages , recorderStat_.mDataFormat.mBytesPerPacket);
recorderStat_.bufferByteSize = kNumberPackages * recorderStat_.mDataFormat.mBytesPerPacket;
//prepareAudioRecorder - bufferByteSize : 40000
NSLog(@"prepareAudioRecorder - bufferByteSize : %d " , recorderStat_.bufferByteSize);
/*
오디오 AudioQueueAllocateBuffer 함수를 이용해 큐 버퍼를 생성하고 세팅해 보자.
kNumberBuffers은 Step 0에서 3개로 세팅하였다.
사실 버퍼 갯수는 더 많아도 상관없지만 보편적으로 3개 정도로 맞춘다.
(스트리밍의 경우 네크워크의 상태에 따라서 데이터를 많이 받아 놓아야 할 수도 있기 때문에 더 많은 것이 일반적이지만
로컬 파일 재생의 경우는 3개로도 충분하다.)
*/
for (int i = 0; i < kNumberBuffers; i++) {
NSLog(@"prepareAudioRecorder - for문 안 (%d) ", i);
AudioQueueAllocateBuffer(
recorderStat_.mQueue, //버퍼를 할당할 오디오 큐
recorderStat_.bufferByteSize, //버퍼의 사이즈
&recorderStat_.mBuffers[0]); //새로생긴 버퍼의 포인터
/*
AudioQueueAllocateBuffer 함수를 이용해서 Buffer를 생성한 뒤 MyAQOutputCallback 함수를 불러주었다.
이렇게 하는 이유는 AudioQueue 가 실제로 start 하기 전에 버퍼에 데이터를 미리 채워서 enqueue 해 놓으려는 의도이다.
*/
/*
버퍼가 준비가 되었다면 AudioQueueEnqueueBuffer함수를 이용해 해당 버퍼를 오디오 큐에 밀어넣어 준다.
*/
AudioQueueEnqueueBuffer(
recorderStat_.mQueue, //버퍼를 넣을 오디오 큐
recorderStat_.mBuffers[i], //넣을 버퍼
0, //ASPD 의 갯수 , cbr == 0
NULL); //ASPD array의 포인터
}
return YES;
}
// 오디오 Queue와 오디오 File 해제!
-(BOOL) disposeAudioRecorder {
NSLog(@"disposeAudioRecorder 진입");
if (recorderStat_.mQueue) {
NSLog(@"disposeAudioRecorder - mQueue 해제");
//AudioQueueStop(recorderStat_.mQueue, true);
AudioQueueDispose(recorderStat_.mQueue, false);
recorderStat_.mQueue = NULL;
}
if (recorderStat_.mAudioFile) {
NSLog(@"disposeAudioRecorder - mAudioFile 해제");
AudioFileClose(recorderStat_.mAudioFile);
recorderStat_.mAudioFile = NULL;
}
return YES;
}
- (IBAction)onPrepare:(id)sender {
NSLog(@"onPrepare 진입 - 오디오 레코더 셋팅 or 오디오 Queue와 오디오 File 해제 할것임.");
static BOOL once = NO;
//once 가 No 가 아닐경우(처음에는 NO)
if (!once) {
//오디오 레코더 셋팅 (준비)
if (![self prepareAudioRecorder]) {
NSLog(@"prepareAudioRecorder 오류 발생!");
return;
}
[_prepareBtn setTitle:@"Dispose" forState:UIControlStateNormal];
once = YES;
}else{
// 오디오 Queue와 오디오 File 해제!
if (![self disposeAudioRecorder]) {
NSLog(@"disposeAudioRecorder 오류 발생");
return;
}
[_prepareBtn setTitle:@"Prepare" forState:UIControlStateNormal];
once = NO;
}
}
- (IBAction)onPlay:(id)sender {
NSLog(@"onPlay 버튼 클릭 ");
[self setAudioSession:2];
[self preparePlayer:[NSURL URLWithString:_filePath]];
BOOL rst = [_player play];
NSLog(@"play with %d" , rst);
}
-(BOOL) preparePlayer :(NSURL *)filePath{
NSLog(@"preparePlayer 메소드 진입=");
NSError *error = nil;
_player = [[AVAudioPlayer alloc]initWithContentsOfURL:filePath error:&error];
if (error != nil) {
NSLog(@"create player[%@] error:%@", filePath, error.localizedDescription);
return NO;
}
_player.delegate = self;
_player.numberOfLoops = 100;
BOOL rst = [_player prepareToPlay];
NSLog(@"preparePlayer - 플레이어 %d. 가 준비되었습니다.", rst);
NSLog(@"preparePlayer - 파일 길이는 %g 입니다.", _player.duration);
NSLog(@"preparePlayer - 채널의 수는 %d. 입니다.", _player.numberOfChannels);
return YES;
}
-(void) stopRecord{
NSLog(@"stopRecord 메소드 진입");
// category = AVAudioSessionCategorySoloAmbient;
[self setAudioSession:2];
recorderStat_.mIsRunning = false;
}
-(void) startRecord{
NSLog(@"startRecord 메소드 진입 - filePath 경로 지정");
//category = AVAudioSessionCategoryRecord;
[self setAudioSession:1];
OSStatus stts = AudioQueueStart(
recorderStat_.mQueue,
NULL);
VStatus(stts, @"AudioQueueStart error");
recorderStat_.mIsRunning = true;
}
//녹음
- (IBAction)onRecord:(id)sender {
NSLog(@"onRecord 버튼 클릭- ");
static BOOL once = NO;
if (!once) {
[self startRecord];
[_recordBtn setTitle:@"STOP" forState:UIControlStateNormal];
once = YES;
} else {
[self stopRecord];
[_recordBtn setTitle:@"Recording" forState:UIControlStateNormal];
once = NO;
}
}
//세션 셋팅
-(void)setAudioSession: (int) mode{
NSLog(@"setAudioSession 메소드 진입 - mode : %d " , mode);
NSError *error;
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:&error];
if (error != nil) {
NSLog(@"audioSession setActive error %@" , error.localizedDescription);
return;
}
error = nil;
NSString *category;
if (mode == 1) {
category = AVAudioSessionCategoryRecord;
} else {
category = AVAudioSessionCategorySoloAmbient;
}
[[AVAudioSession sharedInstance]setCategory:category error:&error];
if (error != nil) {
NSLog(@"AudioSession setCategory(AVAudioSessionCategoryPlayAndRecord) error:%@", error.localizedDescription);
return;
} else {
NSLog(@"set category : %@ " , category);
}
}
#pragma mark AVAudioPlayerDelegate
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
NSLog(@"audioPlayerDidFinishPlaying 진입");
NSLog(@"audioPlayerDidFinishPlaying rst: %d", flag);
[_player stop];
}
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error {
NSLog(@"audioPlayerDecodeErrorDidOccur 진입");
NSLog(@"audioPlayerDecodeErrorDidOccur error:%@", error.localizedDescription);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
로그
로직순서
준비버튼 -> 레코드 버튼 -> 정지버튼 -> 디스포즈 버튼
<열었을때>
2018-12-18 16:14:27.726930+0900 CoreAudioPlay[36392:2262705] viewDidLoad 진입
<준비버튼 클릭!>
2018-12-18 16:28:11.635205+0900 CoreAudioPlay[36450:2265047] viewDidLoad 진입 - filePath 경로 지정
2018-12-18 16:28:15.347805+0900 CoreAudioPlay[36450:2265047] onPrepare 진입 - 오디오 레코더 셋팅 or 오디오 Queue와 오디오 File 해제
2018-12-18 16:28:15.347981+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder 진입
2018-12-18 16:28:15.348042+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - mBytesPerFrame : 4 , mBytesPerPacket : 4
2018-12-18 16:28:15.365912+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - dataFormatSize : 40
2018-12-18 16:28:15.368517+0900 CoreAudioPlay[36450:2265047] open file /var/mobile/Containers/Data/Application/B94E926A-0256-410F-B60C-C33C68ADF602/Documents/void3.wav success!
2018-12-18 16:28:15.368568+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - kNumberPackages : 10000 , mBytesPerPacket : 4
2018-12-18 16:28:15.368595+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - bufferByteSize : 40000
2018-12-18 16:28:15.368653+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - for문 안 (0)
2018-12-18 16:28:15.369033+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - for문 안 (1)
2018-12-18 16:28:15.369194+0900 CoreAudioPlay[36450:2265047] prepareAudioRecorder - for문 안 (2)
<레코드 버튼 클릭!>
2018-12-18 16:14:36.135516+0900 CoreAudioPlay[36392:2262705] onRecord 버튼 클릭-
2018-12-18 16:14:36.135618+0900 CoreAudioPlay[36392:2262705] startRecord 메소드 진입
2018-12-18 16:14:36.135654+0900 CoreAudioPlay[36392:2262705] setAudioSession 메소드 진입 - mode : 1
2018-12-18 16:14:36.321801+0900 CoreAudioPlay[36392:2262705] set category : AVAudioSessionCategoryRecord
2018-12-18 16:14:36.344158+0900 CoreAudioPlay[36392:2262705] [avas] AVAudioSessionPortImpl.mm:56:ValidateRequiredFields: Unknown selected data source for Port 스피커 (type: Speaker)
2018-12-18 16:14:36.574860+0900 CoreAudioPlay[36392:2262705] impAudioQueueInputCallback 진입
2018-12-18 16:14:36.574929+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : true
2018-12-18 16:14:41.684033+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : true
2018-12-18 16:14:41.916060+0900 CoreAudioPlay[36392:2262705] impAudioQueueInputCallback 진입
2018-12-18 16:14:41.916213+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : true
2018-12-18 16:14:46.188503+0900 CoreAudioPlay[36392:2262705] impAudioQueueInputCallback 진입
2018-12-18 16:14:46.188669+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : true
2018-12-18 16:14:46.397709+0900 CoreAudioPlay[36392:2262705] impAudioQueueInputCallback 진입
2018-12-18 16:14:46.397899+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : true
<onRecord 버튼 한번더 클릭 (stop)>
2018-12-18 16:14:46.603850+0900 CoreAudioPlay[36392:2262705] onRecord 버튼 클릭-
2018-12-18 16:14:46.604029+0900 CoreAudioPlay[36392:2262705] stopRecord 메소드 진입
2018-12-18 16:14:46.604109+0900 CoreAudioPlay[36392:2262705] setAudioSession 메소드 진입 - mode : 2
2018-12-18 16:14:47.142569+0900 CoreAudioPlay[36392:2262705] set category : AVAudioSessionCategorySoloAmbient
2018-12-18 16:14:47.150925+0900 CoreAudioPlay[36392:2262705] impAudioQueueInputCallback 진입
2018-12-18 16:14:47.151186+0900 CoreAudioPlay[36392:2262705] recoderStat.Ruing : false
2018-12-18 16:14:47.196723+0900 CoreAudioPlay[36392:2262705] [avas] AVAudioSessionPortImpl.mm:56:ValidateRequiredFields: Unknown selected data source for Port 스피커 (type: Speaker)
<play button 클릭>
2018-12-18 16:14:49.653140+0900 CoreAudioPlay[36392:2262705] onPlay 버튼 클릭
2018-12-18 16:14:49.653260+0900 CoreAudioPlay[36392:2262705] setAudioSession 메소드 진입 - mode : 2
2018-12-18 16:14:49.653852+0900 CoreAudioPlay[36392:2262705] set category : AVAudioSessionCategorySoloAmbient
2018-12-18 16:14:49.654001+0900 CoreAudioPlay[36392:2262705] preparePlayer 메소드 진입=
2018-12-18 16:14:49.664863+0900 CoreAudioPlay[36392:2262705] preparePlayer - 플레이어 1. 가 준비되었습니다.
2018-12-18 16:14:49.664973+0900 CoreAudioPlay[36392:2262705] preparePlayer - 파일 길이는 10.0404 입니다.
2018-12-18 16:14:49.665012+0900 CoreAudioPlay[36392:2262705] preparePlayer - 채널의 수는 2. 입니다.
2018-12-18 16:14:49.673648+0900 CoreAudioPlay[36392:2262705] play with 1
녹음하기 버튼 클릭 -> 준비 버튼 자동으로 눌림 ..... 녹음 .....
-> 정지 버튼 클릭 -> 디스 포즈 버튼 자동으로 눌림
용어
참조
https://github.com/cz-it/play_and_record_with_coreaudio
http://mm.sookmyung.ac.kr/~bigrain/class/2010/
http://mm.sookmyung.ac.kr/~bigrain/class/2010/sound2/AudioQueue_Recording02.key.pdf
https://developer.apple.com/library/archive/documentation/MusicAudio/Conceptual/CoreAudioOverview/CoreAudioEssentials/CoreAudioEssentials.html#//apple_ref/doc/uid/TP40003577-CH10-SW1
'ios 뽀개기 > objective-c' 카테고리의 다른 글
audiosessionQueue play - 코어오디오 재생하기 (0) | 2018.12.19 |
---|---|
ios audiosessionqueue 파일 재생 시간 구하는 두가지 방법 (0) | 2018.12.19 |
AVFoundation을 이용한 녹음 재생하기 (0) | 2018.12.17 |
이미지 자르기 + 제스쳐 + transparentview 예제 (0) | 2018.12.14 |
objective c block에 대한 이해 (0) | 2018.12.10 |
댓글