Playing a Pure Tone (Sine Wave) in IOS


If you just want the code:

ToneGenerator.m – The finished class for this post

HandShake – The complete project

Most programming languages have some variant of: Beep(frequency, duration);

Objective C does not. Part of this is the nature of iOS devices: an interruption can happen at any time meaning there’s no way to guarantee that the call will have enough time to complete. To produce a tone on demand, the programmer must fill the audio buffer with the tone data and the device will play the data when it can.

Audio Player Setup

I got most of the setup code from An explanation is also provided there so there’s no need to restate it here.

Rendering the Audio Data

Forgetting my high school physics completely, I expected a constant frequency to need constant audio data. I expected a tone of 440 Hz to look something like, “[440, 440, 440, 440, 440]“.

A pure sound is a pure wave. Generating a wave is easy using the sine function. I started with code from, but had to update it for portability (see “Where I Screwed Up #2″).

OSStatus RenderTone(
                    void *inRefCon,
                    AudioUnitRenderActionFlags 	*ioActionFlags,
                    const AudioTimeStamp *inTimeStamp,
                    UInt32 inBusNumber,
                    UInt32 inNumberFrames,
                    AudioBufferList *ioData)

	// Fixed amplitude is good enough for our purposes
	const double amplitude = 1;
	// Get the tone parameters out of the class
        ToneGenerator *toneGenerator = (__bridge ToneGenerator*)inRefCon;
        double theta = toneGenerator->_theta;
        double frequency = toneGenerator->_frequency;
	double theta_increment = 2.0 * M_PI * frequency / SAMPLE_RATE;
	// This is a mono tone generator so we only need the first buffer
	const int channel = 0;
	Float32 *buffer = (Float32 *)ioData->mBuffers[channel].mData;
	// Generate the samples
	for (UInt32 frame = 0; frame < inNumberFrames; frame++)
		buffer[frame] = sin(theta) * amplitude;
		theta += theta_increment;
		if (theta > 2.0 * M_PI)
			theta -= 2.0 * M_PI;
	// Store the theta back in the object
	toneGenerator->_theta = theta;
	return noErr;

Originally I didn’t save theta as a class variable. The result was that the wave never completed a cycle and instead of a beep it sounded like a click.

Playing the Tone

The class now has enough in it to start playing a tone.

   [self.audioSession setActive:true error:nil];
   self.frequency = frequency;
   // Create the audio unit as shown above
   [self createToneUnit];
   // Start playback

To keep track of time, I cheated and just slept the thread using [NSThread sleepUntilDate:date].

To stop playing all that is necessary is just tearing down the toneUnit.

   self.frequency = 0;
   self.theta = 0;
   self.toneUnit = nil;

For completeness, the stop handler and interruption handlers should do the same thing.

Where I Screwed Up #1

When I settled on my naive encoding, I figured I’d just assign a tone per bucket based on the ASCII value of the character to send. Since I wanted them all to be inaudible I’d make sure all sent tones were above 19 kHz.
frequency = 19000 + (43 * (int)charToSend);

I didn’t realize until later that the lowest ASCII value I was sending was the ‘.’ character with a corresponding frequency of 20,978 kHz. As far as I can tell, that’s 978 Hz above the rated max of my iPhone speakers. It actually still works for values under ’5′ (ASCII 53) but I shouldn’t expect that.

It works for the proof of concept though. Production uses would require a better encoding scheme— for this and other reasons.

Where I Screwed Up #2

A lot of the code samples I used were before automatic reference counting. In the beginning the code crashed after 1/43 of a second (one audio frame at 44100 Hz).

Through some trial and error I managed to not get through one entire tone, but all future tones wouldn’t play correctly— my cats really hated these problems.

Automatic reference counting turned out to be the culprit. I wasn’t correctly casting the ToneGenerator in the RenderTone function so theta wasn’t properly saved and new tone units would not use fresh variables. The solution turned out to be the __bridge cast in the RenderTone function.


I found a number of helpful blog posts:

Posted in objective-c.