2

I have an mp3 sound I'd like to play with pan, pitch/samplerate, and volume controls set once (not changing in realtime). I am using AVAudioPlayer at the moment, which works, but the rate setting performs time stretching instead of performing the samplerate change where the lower values cause a sound to get slower and lower-pitched, and the higher ones cause a sound to get faster and higher-pitched (kind of like tape speed). E.g., setting samplerate to 88200 HZ when your sound is actually 44100 will result in it playing at 200% speed/pitch. Is something like this possible with AVAudioPlayer, or is there another way to accomplish this? Here's what I have so far:

player=[[AVAudioPlayer alloc] initWithContentsOfURL:
[NSURL fileURLWithPath: @"sound.mp3"] error: nil];
player.volume=0.4f;
player.pan=-1f;
player.enableRate=YES;
player.rate=2.0f;
[player play];

Note: I'm not referring to the pitch method where time stretching is used in combination to keep the length of the sound aproximetly the same, or any "advanced" algorithms.

NS studios
  • 149
  • 14
  • It doesn’t look like AvAudioPlayer provides that kind of functionality https://developer.apple.com/documentation/avfoundation/avaudioplayer – fdcpp Dec 31 '20 at 10:38
  • Changing playback speed is not necessarily as simple as changing sample rate. There are on a set number of sample rates supported so you will likely have to take a different approach. The most obvious way I can think of is with an Audio Unit that interpolates between sample values, either dropping or adding in value accordingly – fdcpp Dec 31 '20 at 10:41
  • @fdcpp Is there another way I can do it then? Perhaps another class? Another framework? – NS studios Dec 31 '20 at 10:45
  • If it is for non-real-time you could ingest the audio file, get the samples, apply the interpolation save a new audio file and play it back out. – fdcpp Dec 31 '20 at 10:47
  • For Frameworks: JUCE is perhaps heavy handed. OpenAL could do it, but is old fashioned. AudioUnits do seem to be the most likely contender – fdcpp Dec 31 '20 at 10:50
  • I’m curious, does changing AVSampleRateKey have any effect on pitch? I assumed it would just drop the quality but may be wrong https://developer.apple.com/documentation/avfoundation/avsampleratekey – fdcpp Dec 31 '20 at 11:00
  • @fdcpp I don't know how to set my own value for that property. What method would I use? The documentation doesn't really make it obvious. – NS studios Dec 31 '20 at 19:42
  • Apologies, misdirection on my part, AVSampleRateKey would only be set when initialising [AVAudioRecorder](https://developer.apple.com/documentation/avfoundation/avaudiorecorder/1388386-init). AVAudioPlayer will get it's settings from header of the audio data – fdcpp Dec 31 '20 at 21:29
  • In which case, you will need to go a little deeper. Again, it depends if you need real-time control or if you will set play back speed ahead of time. – fdcpp Dec 31 '20 at 21:29
  • @fdcpp I just need to set the pan, pitch (samplerate), and volume once. I don't need realtime changes to the sound. I just need to set the parameters, and play once. I'd appreciate if you can think of any way (preferably with an example) that can get me there, preferably without external libraries or any such things. I guess one could perhaps modify the file itself in memory before sending it to be played, but I don't really have experience in the audio programming field, and would have no idea how to go about it. – NS studios Dec 31 '20 at 22:44

1 Answers1

3

AVAudioEngine can give you pitch, rate, pan and volume control:

self.engine = [[AVAudioEngine alloc] init];

NSError *error;

AVAudioPlayerNode *playerNode = [[AVAudioPlayerNode alloc] init];
AVAudioMixerNode *mixer = [[AVAudioMixerNode alloc] init];
AVAudioUnitVarispeed *varispeed = [[AVAudioUnitVarispeed alloc] init];

[self.engine attachNode:playerNode];
[self.engine attachNode:varispeed];
[self.engine attachNode:mixer];

[self.engine connect:playerNode to:varispeed format:nil];
[self.engine connect:varispeed to:mixer format:nil];
[self.engine connect:mixer to:self.engine.mainMixerNode format:nil];

BOOL result = [self.engine startAndReturnError: &error];
assert(result);

AVAudioFile *audioFile = [[AVAudioFile alloc] initForReading:url error:&error];
assert(audioFile);

// rate & pitch (fused), pan and volume controls
varispeed.rate = 0.5; // half pitch & rate
mixer.pan = -1;       // left speaker
mixer.volume = 0.5;   // half volume

[playerNode scheduleFile:audioFile atTime:nil completionHandler:nil];
[playerNode play];

If you want separate rate & pitch control, replace the AVAudioUnitVarispeed node with an AVAudioUnitTimePitch node.

Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • This is definitely the best way to achieve the desired result. The only addition I can suggest is to link pitch to rate with `pitch.pitch = 1200 * log2(pitch.rate)` – fdcpp Jan 01 '21 at 13:19
  • 1
    Thank you @fdcpp I was too lazy to take that step – Rhythmic Fistman Jan 01 '21 at 14:14
  • @Rhythmic Fistman Thank you so much! That works perfectly, except changing pitch does result in time stretching, i.e., the audio doesn't slow down or speed up with the pitch changes even when left at the default rate. I would expect -1200 pitch to be roughly twice as slower as the original pitch, etc. Is that what the 1200 * log2(pitch.rate) formula is supposed to do? – NS studios Jan 01 '21 at 15:35
  • Yes! Try that one. – Rhythmic Fistman Jan 01 '21 at 15:45
  • Ah wait, you might want `AVAudioUnitVarispeed`. One sec, updating answer. – Rhythmic Fistman Jan 01 '21 at 15:49
  • @Rhythmic Fistman One more thing (sorry). In case I needed to play the mp3 sound from memory, is this something I could do with the current setup? I've looked at scheduleBuffer method, but it expects a pcm buffer. Would mp3 work with it? I have previously been able to use NSData with AvAudioPlayer... – NS studios Jan 02 '21 at 18:07
  • @NSstudios you can convert the buffers to LPCM using an `AVAudioConverter` – Rhythmic Fistman Jan 04 '21 at 15:47