如何增加iOS应用播放量生成的PCM音频?

时间:2014-12-26 20:20:19

标签: ios iphone audio volume pcm

我有很多在各种平台和各种语言上编写嵌入式软件的经验,但这是我的第一个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才能使用。以下是播放几种不同频率时扬声器的响应。这些测量是在具有高端记录设备的隔音环境中进行的:

1-2-4-8-10-12kHz

注意从1kHz到12kHz表现出多少滚降,并且输出在10kHz时甚至比在12kHz时更差。此外,输出似乎是以某种方式进行后处理:

1kHz_Post_Processing

我还实现并比较了三种不同的API:AudioUnits,AudioServicesPlaySystemSound和AVAudioPlayer。它们之间的差异并不显着。

能够通过将AVAudioSession模式设置为AVAudioSessionModeMeasurement而不是AVAudioSessionModeDefault来删除后处理,但仍然没有增加音量的乐趣。我认为iPhone只能在可用音量下输出12kHz。

0 个答案:

没有答案