PDA

View Full Version : change frequency




mightyPhone
Jan 18, 2009, 09:47 PM
How can I change the playback frequency (pitch) of a .wav file programmatically?



neil.b
Jan 19, 2009, 04:10 AM
I figured out a way to do this using AVFoundation and AIFF files.

You need to load the sample into an NSData object and you can then use NSData methods to modify the sample rate stored in the AIFF header. The reason you need to pull the samples into NSData objects is because you can't modify the contents of the NIB/XIB files. For that reason it's a bit wasteful with memory (you've effectively got two copies of the sample loaded up - one in the XIB and the other in the NSData object).

Then initialise an instance of an AVAudioPlayer object using the initWithData method (instead of initWithURL). Then you can play the sound. The only downside is you need to re-initialise the AVAudioPlayer object every time you change the pitch so it increases latency - not massively but it's noticable. It depends on what you're trying to do with the sound as to how effective this method is for you.

I posted about this in the Apple Dev Forum. I'm still playing around with it but it works.

(rest of post copied from my Dev Forum posts)

The basic steps are;

1) Load the AIF sample into a NSData object, allocate & initialise the AVAudioPlayer with the NSData
2) Modify the sample rate via the NSData object
3) When you want to play the sound, initialise the AVAudioPlayer using the initWithData method (you have to do this or the sample rate change has no effect)


1) Load the AIF sample into a NSData object, allocate & initialise the AVAudioPlayer with the NSData

AVAudioPlayer *myAudioPlayer;
NSMutableData *soundFileData;
soundFileData = [NSMutableData dataWithContentsOfURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"sample.aif" ofType:NULL]]];
myPlayer = [[AVAudioPlayer alloc] initWithData:soundFileData error:NULL];

2) Modify the sample rate via the NSData object
I've not done any proper decoding of the AIFF chunks, just worked on the assumption that with a "normal" AIF sample, the sample rate is at offset 0x1C and, being in IEEE 80-bit extended format, is 10 bytes long. I found a handy C function to convert to/from IEEE 80-bit. So, my quick rough-and-ready sample rate modifier;

# define FloatToUnsigned(f) ((unsigned long)(((long)(f - 2147483648.0)) + 2147483647L) + 1)

void ConvertToIeeeExtended(double num, char* bytes)
{
int sign;
int expon;
double fMant, fsMant;
unsigned long hiMant, loMant;

if (num < 0) {
sign = 0x8000;
num *= -1;
} else {
sign = 0;
}

if (num == 0) {
expon = 0; hiMant = 0; loMant = 0;
}
else {
fMant = frexp(num, &expon);
if ((expon > 16384) || !(fMant < 1)) { /* Infinity or NaN */
expon = sign|0x7FFF; hiMant = 0; loMant = 0; /* infinity */
}
else { /* Finite */
expon += 16382;
if (expon < 0) { /* denormalized */
fMant = ldexp(fMant, expon);
expon = 0;
}
expon |= sign;
fMant = ldexp(fMant, 32);
fsMant = floor(fMant);
hiMant = FloatToUnsigned(fsMant);
fMant = ldexp(fMant - fsMant, 32);
fsMant = floor(fMant);
loMant = FloatToUnsigned(fsMant);
}
}

bytes[0] = expon >> 8;
bytes[1] = expon;
bytes[2] = hiMant >> 24;
bytes[3] = hiMant >> 16;
bytes[4] = hiMant >> 8;
bytes[5] = hiMant;
bytes[6] = loMant >> 24;
bytes[7] = loMant >> 16;
bytes[8] = loMant >> 8;
bytes[9] = loMant;
}

And the method to modify the sample rate;

-(void) changeSampleRate:(NSMutableData *)sampleData :(double)newRate
{
char *sampleRateByteBuffer = malloc(10);
NSRange sampleRateRange = NSMakeRange(0x1C, 10);
ConvertToIeeeExtended(newRate, sampleRateByteBuffer);


[sampleData replaceBytesInRange:sampleRateRange withBytes:sampleRateByteBuffer];

//free the temp buffers
free(sampleRateByteBuffer);

}

3) When you want to play the sound, initialise the AVAudioPlayer using the initWithData method (you have to do this or the sample rate change has no effect), then play the sound

//if you don't stop the sound first, some corruption occurs - needs more investigation
[myPlayer stop];
//change the sample rate
[self changeSampleRate:soundFileData :22050]; //just using 22050 as an example
//(re)init the player object
[myPlayer initWithData:soundFileData error:NULL];
//if repeatedly playing the same sound, reset the playback pointer
if ([myPlayer currentTime] != 0) [myPlayer setCurrentTime:0];
[myPlayer play];

mightyPhone
Jan 19, 2009, 05:20 AM
Oh whoa, thank you so much neil.b.

I was thinking of something on the line like what you have using AVAudioPlayer but wasn't sure if the key is in change the sample rate. This code looks very helpful. :) Thanks.

Too bad that the iPhone doesn't have a audio systhesizer build in.

neil.b
Jan 19, 2009, 06:35 AM
Oh whoa, thank you so much neil.b.

I was thinking of something on the line like what you have using AVAudioPlayer but wasn't sure if the key is in change the sample rate. This code looks very helpful. :) Thanks.

Too bad that the iPhone doesn't have a audio systhesizer build in.

Hope it helps. :)

I have a little Xcode project with it working if you get stuck though there's nothing remotely useful apart from proving it works.

I think (hope) the next iterration of the AVFoundation Framework will be much improved. It only appear recently and is a lot better than the SystemSound stuff.

You can also use OpenAL supposedly and that can play samples at different pitches. I say supposedly only because I've yet to get it working (I've been trying out all sorts of different audio methods).

mightyPhone
Jan 19, 2009, 07:45 AM
Yea, I hope the AVFoundation will improve as well, add Sound synthesizer will be a blessing. I do like the AVFoundation tho, it makes play a simple sound hell lot easier without the need to muddle with CA. :)

Do you think OpenAL can achieve a more real time approach as far a pitch change? I briefly read through the headers but havn't seen anything obvious, time to take a deeper look again. The whole problem seem to lie in the data buffering. Since data have to be rebuffered, there will always be a delay.

neil.b
Jan 19, 2009, 08:11 AM
I'm not sure. I think it's strengths are in the simple(ish) API rather than out-and-out performance but I've yet to test it out.

The lowest latency method is using RemoteIO but it's shockingly under-documented by Apple. It gives you access to audio IO at a much lower level but what it gives in speed, it takes away in complexity and obscurity.

There's a good thread by a guy over on the Developer Forums. It's in the Media/Audio section, search for "RemoteIO". According to the guy that wrote the post he's achieving a very low latency, like a couple of milliseconds, compared to Audio Queue Services which has a latency of 0.5 seconds (or so).

I got his example stuff half-working but because the example he gave was only a skeleton, I got stuck at the point in my code where the callback function has to fill the buffers. I made a mental note to go back to it at some point and try to crowbar stuff from Audio Queue example code into the callbacks for the RemoteIO stuff to see if I can make it work.

Give me a shout if you can't find the thread on the Dev Forums.

mightyPhone
Jan 19, 2009, 05:07 PM
.

mightyPhone
Jan 19, 2009, 05:11 PM
HA! Found it. For those who are also interested in this topic, here is another link:
http://michael.tyson.id.au/2008/11/04/using-remoteio-audio-unit/

neil.b
Jan 20, 2009, 03:06 AM
Yep, that's the one. I didn't realise he'd posted it on his own website too.

Let me know how you get on. Like I said, I got it up and running (as far as no errors and the play callback was getting hit) but couldn't see where/how to create my own playback buffers and service them in the callback. At that point I got the idea of modifying the AIFF files (as above) so I lost interest in RemoteIO for a while :)

neil.b
Jan 20, 2009, 03:09 AM
Oh, meant to add: the RemoteIO method, as far as I can tell, won't solve your frequency-changing problem.

I managed to playback sounds at different pitches using AudioQueue but you have to stop, change frequency and refill the buffers every time you play a sound. Which, as you can imagine, was horribly laggy... :)

mightyPhone
Jan 20, 2009, 04:49 AM
<was horribly laggy>
Yeah, I found that's the case as well. :( There's GOT to be a better way to deal with this.

Just a thought, in the good ol days, the Moog systhesizers, all it takes is a attenuator to modulate the pitch, wonder if, somehow, there is a way to manipulate the iPhone audio hardware to do that?

neil.b
Jan 20, 2009, 05:56 AM
<was horribly laggy>
Yeah, I found that's the case as well. :( There's GOT to be a better way to deal with this.

Just a thought, in the good ol days, the Moog systhesizers, all it takes is a attenuator to modulate the pitch, wonder if, somehow, there is a way to manipulate the iPhone audio hardware to do that?

I'm sure there is if you could get down low enough.

Apple SDK is not going to expose that to you though. :)

By all accounts the DSP is pretty powerful - look at stuff like Noise.io Pro and see what can be achieved. I've no idea how they achieved it though :D

mightyPhone
Jan 21, 2009, 05:36 AM
I looked into Audio Queue Service, seems like there is a good posibility. :)

ezone
Dec 30, 2009, 02:22 AM
Any ideas on how to change the sampleRate for a CAF? I'm using AVAudioRecorder to record a sound sample from the iPhone mic, and I wanted to change the pitch of the recorded sample and play it back. Here are the settings I'm using for AVAudioRecorder:


[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryRecord error: &error];
NSMutableDictionary* recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue :[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];
[recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 1] forKey:AVNumberOfChannelsKey];
[recordSetting setValue :[NSNumber numberWithInt:16] forKey:AVLinearPCMBitDepthKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsBigEndianKey];
[recordSetting setValue :[NSNumber numberWithBool:NO] forKey:AVLinearPCMIsFloatKey];

// Create a new dated file
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
NSString *caldate = [now description];
recorderFilePath = [[NSString stringWithFormat:@"%@/%@.caf", DOCUMENTS_FOLDER, caldate] retain];
NSURL *url = [NSURL fileURLWithPath:recorderFilePath];
recorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:&error];

firewood
Dec 30, 2009, 10:01 PM
Do you want to change just the pitch (duration stays constant)? Or the pitch and speed of playback together?

ezone
Dec 31, 2009, 12:54 AM
Do you want to change just the pitch (duration stays constant)? Or the pitch and speed of playback together?

Changing the pitch and keeping the same duration would be ideal, but I would be happy with just a sample rate change.

macQM
Mar 9, 2011, 04:36 PM
I'm using an aiff recorded file from a url that was stored in NSMutableData. The following is the only way I could get it to play with the change rate code. I tell the player to play in another method. This one sets it up with the sampleData. However, the audio sounds normal. I changed malloc(10) to malloc(5) and malloc(1), but that did not do anything. Does anyone know what needs to be altered to change the pitch? Is it something in the C+ code? I'm not very well-versed in buffers and/or advanced audio. Thanks for any pointers/help.

-(void) changeSampleRate:(NSMutableData*)sampleData :(double)newRate
{
char *sampleRateByteBuffer = malloc(1);
NSRange sampleRateRange = NSMakeRange(0x1C, 10);
ConvertToIeeeExtended(newRate, sampleRateByteBuffer);
[sampleData replaceBytesInRange:sampleRateRange withBytes:sampleRateByteBuffer];

AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithData:sampleData error:NULL];
[audioPlayer prepareToPlay] ;

//free the temp buffers
free(sampleRateByteBuffer);

currentPlayer = audioPlayer;

Should I have different recordSettings?
NSMutableDictionary* recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM] forKey:AVFormatIDKey];[recordSetting setValue:[NSNumber numberWithFloat:44100.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];
[recordSetting setValue:[NSNumber numberWithInt: 2] forKey:AVNumberOfChannelsKey];

nickculbertson
Mar 9, 2011, 09:32 PM
Google "finch openal". It doesn't use AVAudio but it will show you how to change pitch using OpenAL (which is better anyway).
Nick

macQM
Mar 9, 2011, 11:14 PM
I guess I will try Finch. I had already downloaded but was hoping I could change pitch using AVFoundation, because my app is already done with that framework. I had already downloaded Finch but it looks really complicated.

Back to the drawing board - trying Finch. Unless anyone knows of a way to do it with AVFoundation. Is it not possible to do this with a file recorded with AVAudioRecorder and then played back with AVAudioPlayer? I noticed the MacOSX code has player.rate = 0.5 but of course, iPhone only has the change volume code (player.volume = x).

I've researched this for a few days now and found nothing except this forum thread that remotely has a chance of using AVFoundation to change pitch. If it is possible, please give me a hint of what to change in the code. Much thanks!

firewood
Mar 9, 2011, 11:58 PM
There exists no hint. You need to use something other than AVFoundation.