mardi 30 décembre 2014

How to increase the volume of iOS app playing generated PCM audio?

I have a lot of experience writing embedded software on a variety of platforms and in a variety of languages, but this is my first iOS app.


This app emits audio data. The data is different each time, and must be generated by the app on-the-fly. It is encoded as 500Hz modulated PCM on a 12kHz carrier, lasting less than 100mS. The volume of the data must be independent of the system volume.


All is working well, except that the volume of the data through the speaker is very low on most iPhones. Through the receiver it is even worse, to the point of being unusable. The volume on an iPad seems better.


Sadly, I don't have many physical iPhones to try this out on.


I chose to use the AudioUnits API because it is most compatible with PCM data. An array of Float32 is populated with data sampled at 32kHz. The 12kHz carrier data varies from +1.0 to -1.0 at full volume, and is scaled down for lower volumes if the user desires.


Here are some code snippets to examine. There's nothing very exotic here, but maybe someone can point out what I am doing wrong. I am omitting much of the structure and the iOS app-specific code, since I'm not having problems there. Note also that there is more error-checking done than I have shown here, in the interest of brevity:



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];
}


Manipulating the volume slider only seems to be necessary on the iPad, but it doesn't hurt on the iPhone, as far as I can tell.

Using different data types (SInt32, int16_t) for SampleType doesn't seem to make a difference.

Scaling the data greater than the range +1.0 to -1.0 only seems to result in clipping.


Would using a different API such as AudioServicesPlaySystemSound or AVAudioPlayer result in louder output? They each present challenges, and I'm loath to implement them without some indication that it would help.

(My understanding is that I'd have to create a .caf container 'file' each time data is generated so that I can pass a URL to these APIs, then delete it after it is played. I haven't seen an example of this particular scenario; maybe there's an easier way? ... but that's a different question.)


Is the typical iPhone's speaker and receiver just not capable of pumping 12kHz out at a usable volume? I find that hard to believe.


Thanks in advance for any help!




Aucun commentaire:

Enregistrer un commentaire