ForgetSou | Blog

❤ 武统台湾 刻不容缓 ❤

0%

一 简述

一种控制器,用于在浮动的可调整大小的窗口中响应用户启动的画中画视频播放。

API_AVAILABLE(ios(9.0), macos(10.15), tvos(14.0)) API_UNAVAILABLE(watchos)
@interface AVPictureInPictureController : NSObject

注意

画中画(PiP)是Apple希望始终在用户控制下的一项用户功能。仅在响应用户的明确请求时才调用PiP。如果某个应用以非用户直接指导的方式调用PiP,则App Store审核小组将拒绝它。

要在iOS中使用画中画,要在Xcode中执行以下步骤:

  1. 打开后台模式开启 Audio, AirPlay, and Picture in Picture

image-20201111164716519

  1. 为音频会话配置合适的类别,如AVAudioSessionCategoryPlaybackAVAudioSessionCategoryPlayAndRecord

重要

不支持子类化和重写其方法,这会导致未定义的行为。

二 官方属性方法

阅读全文 »

1.单声道录音

//  FSUnitRecorder.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^kAudioUnitRecorderOnputBlock)(AudioBufferList *bufferList);

@interface FSUnitRecorder : NSObject

@property (assign, nonatomic) double sampleRate;
@property (assign, nonatomic, readonly) BOOL isRecording;
@property (copy, nonatomic) kAudioUnitRecorderOnputBlock bufferListBlock;

- (void)start;
- (void)stop;

@end

NS_ASSUME_NONNULL_END
//  FSUnitRecorder.m
#import "FSUnitRecorder.h"

@interface FSUnitRecorder ()
{
AudioUnit audioUnit;
BOOL audioComponentInitialized;
}

@property (nonatomic,assign) AudioStreamBasicDescription inputStreamDesc;

@end

@implementation FSUnitRecorder

- (instancetype)init {
self = [super init];
if (self) {
self = [super init];
if (self) {
[self defaultSetting];
}
return self;
}
return self;
}

- (void)defaultSetting {
// 优先16000,如果设备不支持使用其它采样率
NSArray *sampleRates = @[@16000, @11025, @22050, @44100];
for (NSNumber *sampleRate in sampleRates) {
OSStatus status = [self prepareRecord:sampleRate.doubleValue];
if (status == noErr) {
self.sampleRate = [sampleRate doubleValue];
break;
}
}
}

- (OSStatus)prepareRecord:(double)sampleRate {
OSStatus status = noErr;

NSError *error;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionAllowBluetooth error:&error];
[[AVAudioSession sharedInstance] setActive:YES error:&error];
// This doesn't seem to really indicate a problem (iPhone 6s Plus)
#ifdef IGNORE
NSInteger inputChannels = session.inputNumberOfChannels;
if (!inputChannels) {
NSLog(@"ERROR: NO AUDIO INPUT DEVICE");
return -1;
}
#endif

if (!audioComponentInitialized) {
audioComponentInitialized = YES;
// Describe the RemoteIO unit
AudioComponentDescription audioComponentDescription;
audioComponentDescription.componentType = kAudioUnitType_Output;
audioComponentDescription.componentSubType = kAudioUnitSubType_RemoteIO;
audioComponentDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
audioComponentDescription.componentFlags = 0;
audioComponentDescription.componentFlagsMask = 0;

// Get the RemoteIO unit
AudioComponent remoteIOComponent = AudioComponentFindNext(NULL,&audioComponentDescription);
status = AudioComponentInstanceNew(remoteIOComponent,&(self->audioUnit));
if (CheckError(status, "Couldn't get RemoteIO unit instance")) {
return status;
}
}

UInt32 oneFlag = 1;
AudioUnitElement bus0 = 0;
AudioUnitElement bus1 = 1;

if ((NO)) {
// Configure the RemoteIO unit for playback
status = AudioUnitSetProperty (self->audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
bus0,
&oneFlag,
sizeof(oneFlag));
if (CheckError(status, "Couldn't enable RemoteIO output")) {
return status;
}
}

// Configure the RemoteIO unit for input
status = AudioUnitSetProperty(self->audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
bus1,
&oneFlag,
sizeof(oneFlag));
if (CheckError(status, "Couldn't enable RemoteIO input")) {
return status;
}

AudioStreamBasicDescription asbd;
memset(&asbd, 0, sizeof(asbd));
asbd.mSampleRate = sampleRate; // 采样率
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
asbd.mBytesPerPacket = 2;
asbd.mFramesPerPacket = 1;
asbd.mBytesPerFrame = 2;
asbd.mChannelsPerFrame = 2;
asbd.mBitsPerChannel = 16;

// Set format for output (bus 0) on the RemoteIO's input scope
status = AudioUnitSetProperty(self->audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
bus0,
&asbd,
sizeof(asbd));
if (CheckError(status, "Couldn't set the ASBD for RemoteIO on input scope/bus 0")) {
return status;
}

// Set format for mic input (bus 1) on RemoteIO's output scope
status = AudioUnitSetProperty(self->audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
bus1,
&asbd,
sizeof(asbd));
if (CheckError(status, "Couldn't set the ASBD for RemoteIO on output scope/bus 1")) {
return status;
}

// Set the recording callback
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = inputCallBackFun;
callbackStruct.inputProcRefCon = (__bridge void *) self;
status = AudioUnitSetProperty(self->audioUnit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
bus1,
&callbackStruct,
sizeof (callbackStruct));
if (CheckError(status, "Couldn't set RemoteIO's render callback on bus 0")) {
return status;
}

if ((NO)) {
// Set the playback callback
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = playbackCallback;
callbackStruct.inputProcRefCon = (__bridge void *) self;
status = AudioUnitSetProperty(self->audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
bus0,
&callbackStruct,
sizeof (callbackStruct));
if (CheckError(status, "Couldn't set RemoteIO's render callback on bus 0")) {
return status;
}
}

// Initialize the RemoteIO unit
status = AudioUnitInitialize(self->audioUnit);
if (CheckError(status, "Couldn't initialize the RemoteIO unit")) {
return status;
}

return status;
}

- (void)start {
[self deleteAudioFile];
CheckError(AudioOutputUnitStart(audioUnit), "AudioOutputUnitStop failed");
_isRecording = YES;
}

- (void)stop {
CheckError(AudioOutputUnitStop(audioUnit),
"AudioOutputUnitStop failed");
_isRecording = NO;
}

- (void)deleteAudioFile {
NSString *pcmPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"record.mp3"];
if ([[NSFileManager defaultManager] fileExistsAtPath:pcmPath]) {
[[NSFileManager defaultManager] removeItemAtPath:pcmPath error:nil];
}
}

- (void)dealloc {
CheckError(AudioComponentInstanceDispose(audioUnit),
"AudioComponentInstanceDispose failed");
NSLog(@"UnitRecorder销毁");
}

static OSStatus CheckError(OSStatus error, const char *operation) {
if (error == noErr) {
return error;
}
char errorString[20] = "";
// See if it appears to be a 4-char-code
*(UInt32 *)(errorString + 1) = CFSwapInt32HostToBig(error);
if (isprint(errorString[1]) && isprint(errorString[2]) &&
isprint(errorString[3]) && isprint(errorString[4])) {
errorString[0] = errorString[5] = '\'';
errorString[6] = '\0';
} else {
// No, format it as an integer
sprintf(errorString, "%d", (int)error);
}
fprintf(stderr, "Error: %s (%s)\n", operation, errorString);
return error;
}

static OSStatus playbackCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
OSStatus status = noErr;

// Notes: ioData contains buffers (may be more than one!)
// Fill them up as much as you can. Remember to set the size value in each buffer to match how
// much data is in the buffer.
FSUnitRecorder *recorder = (__bridge FSUnitRecorder *) inRefCon;

UInt32 bus1 = 1;
status = AudioUnitRender(recorder->audioUnit,
ioActionFlags,
inTimeStamp,
bus1,
inNumberFrames,
ioData);
CheckError(status, "Couldn't render from RemoteIO unit");
return status;
}

static OSStatus inputCallBackFun(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData)
{

FSUnitRecorder *recorder = (__bridge FSUnitRecorder *)(inRefCon);

AudioBufferList bufferList;
bufferList.mNumberBuffers = 1;
bufferList.mBuffers[0].mData = NULL;
bufferList.mBuffers[0].mDataByteSize = 0;

AudioUnitRender(recorder->audioUnit,
ioActionFlags,
inTimeStamp,
1,
inNumberFrames,
&bufferList);
if (recorder.bufferListBlock) {
recorder.bufferListBlock(&bufferList);
}

return noErr;
}

@end

使用

- (FSUnitRecorder *)recorder {
if (!_recorder) {
_recorder = [[FSUnitRecorder alloc] init];
}
return _recorder;
}

@weakify(self);
self.recorder.bufferListBlock = ^(AudioBufferList * _Nonnull bufferList) {
@strongify(self);
AudioBuffer buffer = bufferList->mBuffers[0];
NSData *data = [NSData dataWithBytes:buffer.mData length:buffer.mDataByteSize];
// 处理数据
[self processSampleData:data];
};

2.双声道录音

说明:
公司新业务要接入蓝牙耳机,支持蓝牙耳机,左右耳机分别进行语音识别等功能。该业务牵扯到实时双通道录音,分别提取左右buffer,类似的业务需求市场上也是有的,比如AirPods,百度的一款蓝牙耳机(小度APP“流浪地球模式”,具体可以买一个个试用下)。
废话不多说了,直接看代码就行。

ZDUnitRecorder.h

#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void (^kAudioUnitRecorderOnputBlock)(AudioBufferList *bufferList);

@interface ZDUnitRecorder : NSObject

@property (assign, nonatomic) double sampleRate;
@property (assign, nonatomic, readonly) BOOL isRecording;
@property (copy, nonatomic) kAudioUnitRecorderOnputBlock bufferListBlock;

- (void)start;
- (void)stop;

@end

NS_ASSUME_NONNULL_END

ZDUnitRecorder.m

阅读全文 »