我有很多在各种平台和各种语言上编写嵌入式软件的经验,但这是我的第一个iOS应用程序。
此应用会发出音频数据。每次数据都不同,必须由应用程序即时生成。它在12kHz载波上编码为500Hz调制PCM,持续时间小于100mS。数据量必须独立于系统容量。
一切都运行良好,但大多数iPhone上通过扬声器的数据量非常低。通过接收器它甚至更糟,到了无法使用的程度。 iPad上的音量似乎更好。
可悲的是,我没有很多物理iPhone试试这个。
我选择使用AudioUnits API,因为它与PCM数据最兼容。一个Float32阵列填充了32kHz采样的数据。 12kHz载波数据在全音量时从+1.0到-1.0变化,并且如果用户需要,则按比例缩小以获得较低音量。
以下是一些需要检查的代码段。这里没有什么异国情调,但也许有人可以指出我做错了什么。我省略了大部分结构和iOS应用专用代码,因为我没有遇到问题。另请注意,为了简洁起见,完成的错误检查比我在此处所示的更多:
typedef Float32 SampleType;
AURenderCallbackStruct renderCallbackStruct;
AVAudioSession * session;
AudioComponentInstance audioUnit;
AudioComponentDescription audioComponentDescription;
AudioStreamBasicDescription audioStreamBasicDesc;
UISlider IBOutlet * volumeViewSlider;
static SampleType * generatedAudioData; // Array to hold
// generated audio data
...
// Description to use to find the default playback output unit:
audioComponentDescription.componentType = kAudioUnitType_Output;
audioComponentDescription.componentSubType = kAudioUnitSubType_RemoteIO;
audioComponentDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
audioComponentDescription.componentFlags = 0; // Docs say: "Set this value to zero."
audioComponentDescription.componentFlagsMask = 0; // Docs say: "Set this value to zero."
// Callback structure to use for setting up the audio unit:
renderCallbackStruct.inputProc = RenderCallback;
renderCallbackStruct.inputProcRefCon = (__bridge void *)( self );
// Set the format for the audio stream in the AudioStreamBasicDescription (ASBD):
audioStreamBasicDesc.mBitsPerChannel = BITS_PER_BYTE * BYTES_PER_SAMPLE; // (8 * sizeof ( SampleType ))
audioStreamBasicDesc.mBytesPerFrame = BYTES_PER_SAMPLE;
audioStreamBasicDesc.mBytesPerPacket = BYTES_PER_SAMPLE;
audioStreamBasicDesc.mChannelsPerFrame = MONO_CHANNELS; // 1
audioStreamBasicDesc.mFormatFlags = (kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked |
kAudioFormatFlagIsNonInterleaved);
audioStreamBasicDesc.mFormatID = kAudioFormatLinearPCM;
audioStreamBasicDesc.mFramesPerPacket = 1;
audioStreamBasicDesc.mReserved = 0;
audioStreamBasicDesc.mSampleRate = 32000;
session = [AVAudioSession sharedInstance];
[session setActive: YES error: &sessionError];
// This figures out which UISlider subview in the MPVolumeViews controls the master volume.
// Later, this is manipulated to set the volume to max temporarily so that the AudioUnit can
// play the data at the user's desired volume.
MPVolumeView * volumeView = [[MPVolumeView alloc] init];
for ( UIView *view in [volumeView subviews] )
{
if ([view.class.description isEqualToString:@"MPVolumeSlider"])
{
volumeViewSlider = ( UISlider * ) view;
break;
}
}
...
// (This is freed later:)
generatedAudioData = (SampleType *) malloc ( lengthOfAudioData * sizeof ( SampleType ));
[session setCategory: AVAudioSessionCategoryPlayAndRecord
withOptions: AVAudioSessionCategoryOptionDuckOthers |
AudioSessionOverrideAudioRoute_Speaker
error: &sessionError];
// Set the master volume to max temporarily so that the AudioUnit can
// play the data at the user's desired volume:
oldVolume = volumeViewSlider.value; // (This is set back after the data is played)
[volumeViewSlider setValue: 1.0f animated: NO];
AudioComponent defaultOutput = AudioComponentFindNext ( NULL, &audioComponentDescription );
// Create a new unit based on this to use for output
AudioComponentInstanceNew ( defaultOutput, &audioUnit );
UInt32 enableOutput = 1;
AudioUnitElement outputBus = 0;
// Enable IO for playback
AudioUnitSetProperty ( audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
outputBus,
&enableOutput,
sizeof ( enableOutput ) );
// Set the audio data stream formats:
AudioUnitSetProperty ( audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
outputBus,
&audioStreamBasicDesc,
sizeof ( AudioStreamBasicDescription ) );
// Set the function to be called each time more input data is needed by the unit:
AudioUnitSetProperty ( audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Global,
outputBus,
&renderCallbackStruct,
sizeof ( renderCallbackStruct ) );
// Because the iPhone plays audio through the receiver by default,
// it is necessary to override the output if the user prefers to
// play through the speaker:
// (if-then code removed for brevity)
[session overrideOutputAudioPort: AVAudioSessionPortOverrideSpeaker
error: &overrideError];
// The pcmEncoder fills generatedAudioData with the ... uh, well,
// the generated audio data:
[pcmEncoder createAudioData: generatedAudioData
audioDataSize: lengthOfAudioData];
AudioUnitInitialize ( audioUnit );
AudioOutputUnitStart ( audioUnit );
...
///////////////////////////////////////////////////////////////
OSStatus RenderCallback ( void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * ioData)
{
SampleType * ioDataBuffer = (SampleType *)ioData->mBuffers[0].mData;
// Check that the data has been exhausted, and if so, tear down the audio unit:
if ( dataLeftToCopy <= 0 )
{ // Tear down the audio unit on the main thread instead of this thread:
[audioController performSelectorOnMainThread: @selector ( tearDownAudioUnit )
withObject: nil
waitUntilDone: NO];
return noErr;
}
// Otherwise, copy the PCM data from generatedAudioData to ioDataBuffer and update the index
// of source data.
... (Boring code that copies data omitted)
return noErr;
}
///////////////////////////////////////////////////////////////
- (void) tearDownAudioUnit
{
if ( audioUnit )
{
AudioOutputUnitStop ( audioUnit );
AudioUnitUninitialize ( audioUnit );
AudioComponentInstanceDispose ( audioUnit );
audioUnit = nil;
}
// Change the session override back to play through the default output stream:
NSError * deactivationError = nil;
int errorInt = [session overrideOutputAudioPort: AVAudioSessionPortOverrideNone
error: &deactivationError];
// Free the audio data memory:
if ( generatedAudioData ) { free ( generatedAudioData ); generatedAudioData = nil; }
[volumeViewSlider setValue: oldVolume animated: NO];
}
在iPad上似乎只需要操作音量滑块,但就我所知,它在iPhone上并没有受到伤害。
对SampleType使用不同的数据类型(SInt32,int16_t)似乎没有什么区别
将大于+1.0到-1.0范围的数据缩放似乎只会导致剪裁。
使用不同的API(如AudioServicesPlaySystemSound或AVAudioPlayer)会导致更大的输出吗?他们每个人都提出挑战,我不愿意在没有任何迹象表明它会有所帮助的情况下实施这些挑战 (我的理解是,每次生成数据时我都必须创建一个.caf容器'文件',以便我可以将URL传递给这些API,然后在播放后删除它。我还没有看到一个例子这种特殊情况;也许有一种更简单的方法?......但这是一个不同的问题。)
典型的iPhone扬声器和接收器是否无法以可用音量输出12kHz?我觉得很难相信。
提前感谢您的帮助!
更新:2015年1月13日
最后收购了iPhone 5C才能使用。以下是播放几种不同频率时扬声器的响应。这些测量是在具有高端记录设备的隔音环境中进行的:
注意从1kHz到12kHz表现出多少滚降,并且输出在10kHz时甚至比在12kHz时更差。此外,输出似乎是以某种方式进行后处理:
我还实现并比较了三种不同的API:AudioUnits,AudioServicesPlaySystemSound和AVAudioPlayer。它们之间的差异并不显着。
我 能够通过将AVAudioSession模式设置为AVAudioSessionModeMeasurement而不是AVAudioSessionModeDefault来删除后处理,但仍然没有增加音量的乐趣。我认为iPhone只能在可用音量下输出12kHz。