Reputation: 23510
EDIT following Ben's answer
I'm trying to make something that should be very easy for someone that is used with signal processing, but that gives me headaches. I'm just trying to generate a wave sound that would play an arbitrary number of seconds, could be less, or more than a second (0.1s, 0.88s, 1.2s, ...).
To generate the wave sound, I'm using that well known method :
+ (NSData*) WAVSoundForFrequency:(float)frequency duration:(float)seconds sampleRate:(unsigned int)sampleRate gain:(float)gain
{
int frames = seconds * sampleRate;
float* rawSound = (float*)malloc(frames*sizeof(float));
if (rawSound == NULL) return nil;
for (int i = 0; i < frames; i++)
rawSound[i] = gain * sinf(i*2*M_PI*frequency/sampleRate);
// converting to raw sound and returning the whole thing
}
That is called basicaly with :
AVAudioPlayer* player = [self.audioPlayerManager buildSoundFrequency:200 duration:0.18 sampleRate:44100 gain:1.0];
player.volume = 1.0;
player.numberOfLoops = -1;
[player play];
The problem is that with those params, the wave seems no to be complete at the end so it generate clicks that can be heard on each loop. But no click if I use 0.5 seconds or 1.0 seconds for duration and 200 hertz (using the adjustedDuration of course). Still for test purpose, if I use 400 hertz or 440 hertz instead of 200, I have now clicks with 0.5s.
Note that the loop is only here for testing and find if there are clicks, or not. At the end, the sound should only play at the desired duration.
I had guessed that that was because of the duration that is not a round multiple of the wave cycle, so I have adjusted the call like this to adjust the wanted duration to the nearest duration that would be a multiple of one cycle at the wanted frequency :
float wantedDuration = 0.18;
float hertz = 200;
int wantedSampleRate = 44100;
// Adjusting wanted duration so the duration contains an entiere number of waves
float oneWaveDurationInSeconds = 1.0/hertz;
int nbWavesNeeded = roundf(wantedDuration/oneWaveDurationInSeconds);
float adjustedDuration = nbWavesNeeded * oneWaveDurationInSeconds;
// Adjusting sample rate so one wave takes an entiere number of samples
float oneSampleDuration = 1.0/wantedSampleRate;
int adjustedSamplerate = wantedSampleRate;
while (YES) {
oneSampleDuration = 1.0/adjustedSamplerate;
if (roundf(oneWaveDurationInSeconds/oneSampleDuration) == oneWaveDurationInSeconds/oneSampleDuration) break;
adjustedSamplerate++;
NSLog(@"%d", adjustedSamplerate);
}
// Debug
float nbSamplesForOneWave = oneWaveDurationInSeconds / (1.0/adjustedSamplerate);
NSLog(@"nbSamplesForOneWave : %f", nbSamplesForOneWave);
// Execute
MyAudioPlayer* player = [self.manager preloadSoundFrequency:hertz duration:adjustedDuration sampleRate:adjustedSamplerate gain:1.0
identifier:@"ii" category:@"Radar"];
player.volume = 1.0;
player.numberOfLoops = -1;
[player play];
But there is still a click.
I've been told that the problem could be the sample rate. But I really don't understand why. As far as I've understood, the sample rate is the number of samples defined in one second. So for me it's not dependent on the duration nor the frequency.
And... Why shouldn't I have a sound of 0.18s with a 44100 sample quality...
But anyway... I have imagined that if I sample 44100 points in one second, asking for a duration of 0.18 should lead to a 44100*0.18 samples. This is the number represented by int frames
. So I have tried to replace
rawSound[i] = gain * sinf(i*2*M_PI*frequency/sampleRate);
with
rawSound[i] = gain * sinf(i*2*M_PI*frequency/frames);
That doesn't work, that just make the sound far more acute. And I still don't understand why. I though it would be a less quality sound, as tehre are just less samples.
Could someone help me to generate that (possibly loopable) wave sound for any wanted delay, on the desired quality and frequency ?
I'm sure that sounds ( :-) ) easy, but I don't see the way to follow to achieve this goal.
I tried to put a NSLog to see the used values (log without Paul's ramps) :
if (i<20 || i > frames-20) NSLog(@"%f", rawSound[i]);
For 440Hz, 44100 sample rate, 1.0 duration (no adjustment) : No click
2011-10-31 01:02:34.110 testAudio[9602:207] 0.000000
2011-10-31 01:02:34.112 testAudio[9602:207] 0.062648
2011-10-31 01:02:34.113 testAudio[9602:207] 0.125051
2011-10-31 01:02:34.114 testAudio[9602:207] 0.186961
2011-10-31 01:02:34.115 testAudio[9602:207] 0.248138
2011-10-31 01:02:34.116 testAudio[9602:207] 0.308339
2011-10-31 01:02:34.116 testAudio[9602:207] 0.367330
2011-10-31 01:02:34.117 testAudio[9602:207] 0.424877
2011-10-31 01:02:34.117 testAudio[9602:207] 0.480755
2011-10-31 01:02:34.118 testAudio[9602:207] 0.534744
2011-10-31 01:02:34.119 testAudio[9602:207] 0.586632
2011-10-31 01:02:34.121 testAudio[9602:207] 0.636216
2011-10-31 01:02:34.121 testAudio[9602:207] 0.683300
2011-10-31 01:02:34.122 testAudio[9602:207] 0.727699
2011-10-31 01:02:34.123 testAudio[9602:207] 0.769240
2011-10-31 01:02:34.123 testAudio[9602:207] 0.807759
2011-10-31 01:02:34.124 testAudio[9602:207] 0.843104
2011-10-31 01:02:34.125 testAudio[9602:207] 0.875137
2011-10-31 01:02:34.126 testAudio[9602:207] 0.903732
2011-10-31 01:02:34.127 testAudio[9602:207] 0.928777
2011-10-31 01:02:34.130 testAudio[9602:207] -0.928790
2011-10-31 01:02:34.130 testAudio[9602:207] -0.903724
2011-10-31 01:02:34.131 testAudio[9602:207] -0.875102
2011-10-31 01:02:34.132 testAudio[9602:207] -0.843167
2011-10-31 01:02:34.132 testAudio[9602:207] -0.807795
2011-10-31 01:02:34.133 testAudio[9602:207] -0.769245
2011-10-31 01:02:34.134 testAudio[9602:207] -0.727667
2011-10-31 01:02:34.135 testAudio[9602:207] -0.683225
2011-10-31 01:02:34.135 testAudio[9602:207] -0.636283
2011-10-31 01:02:34.136 testAudio[9602:207] -0.586658
2011-10-31 01:02:34.137 testAudio[9602:207] -0.534724
2011-10-31 01:02:34.138 testAudio[9602:207] -0.480687
2011-10-31 01:02:34.138 testAudio[9602:207] -0.424978
2011-10-31 01:02:34.139 testAudio[9602:207] -0.367383
2011-10-31 01:02:34.140 testAudio[9602:207] -0.308342
2011-10-31 01:02:34.140 testAudio[9602:207] -0.248087
2011-10-31 01:02:34.141 testAudio[9602:207] -0.186856
2011-10-31 01:02:34.142 testAudio[9602:207] -0.125132
2011-10-31 01:02:34.142 testAudio[9602:207] -0.062676
For 440Hz, 44100 sample rate, 0.5 duration (no adjustment) : No click
2011-10-31 01:04:51.043 testAudio[9714:207] 0.000000
2011-10-31 01:04:51.045 testAudio[9714:207] 0.062648
2011-10-31 01:04:51.047 testAudio[9714:207] 0.125051
2011-10-31 01:04:51.049 testAudio[9714:207] 0.186961
2011-10-31 01:04:51.049 testAudio[9714:207] 0.248138
2011-10-31 01:04:51.050 testAudio[9714:207] 0.308339
2011-10-31 01:04:51.051 testAudio[9714:207] 0.367330
2011-10-31 01:04:51.052 testAudio[9714:207] 0.424877
2011-10-31 01:04:51.053 testAudio[9714:207] 0.480755
2011-10-31 01:04:51.054 testAudio[9714:207] 0.534744
2011-10-31 01:04:51.055 testAudio[9714:207] 0.586632
2011-10-31 01:04:51.055 testAudio[9714:207] 0.636216
2011-10-31 01:04:51.056 testAudio[9714:207] 0.683300
2011-10-31 01:04:51.057 testAudio[9714:207] 0.727699
2011-10-31 01:04:51.059 testAudio[9714:207] 0.769240
2011-10-31 01:04:51.060 testAudio[9714:207] 0.807759
2011-10-31 01:04:51.060 testAudio[9714:207] 0.843104
2011-10-31 01:04:51.061 testAudio[9714:207] 0.875137
2011-10-31 01:04:51.062 testAudio[9714:207] 0.903732
2011-10-31 01:04:51.062 testAudio[9714:207] 0.928777
2011-10-31 01:04:51.064 testAudio[9714:207] -0.928795
2011-10-31 01:04:51.065 testAudio[9714:207] -0.903730
2011-10-31 01:04:51.065 testAudio[9714:207] -0.875109
2011-10-31 01:04:51.066 testAudio[9714:207] -0.843109
2011-10-31 01:04:51.067 testAudio[9714:207] -0.807731
2011-10-31 01:04:51.067 testAudio[9714:207] -0.769253
2011-10-31 01:04:51.068 testAudio[9714:207] -0.727676
2011-10-31 01:04:51.069 testAudio[9714:207] -0.683324
2011-10-31 01:04:51.070 testAudio[9714:207] -0.636199
2011-10-31 01:04:51.070 testAudio[9714:207] -0.586669
2011-10-31 01:04:51.071 testAudio[9714:207] -0.534736
2011-10-31 01:04:51.072 testAudio[9714:207] -0.480806
2011-10-31 01:04:51.072 testAudio[9714:207] -0.424880
2011-10-31 01:04:51.073 testAudio[9714:207] -0.367282
2011-10-31 01:04:51.074 testAudio[9714:207] -0.308355
2011-10-31 01:04:51.074 testAudio[9714:207] -0.248100
2011-10-31 01:04:51.075 testAudio[9714:207] -0.186989
2011-10-31 01:04:51.076 testAudio[9714:207] -0.125025
2011-10-31 01:04:51.077 testAudio[9714:207] -0.062689
For 440Hz, 44100 sample rate, 0.25 duration (no adjustment) : Hard clicks
2011-10-31 01:05:25.245 testAudio[9759:207] 0.000000
2011-10-31 01:05:25.247 testAudio[9759:207] 0.062648
2011-10-31 01:05:25.249 testAudio[9759:207] 0.125051
2011-10-31 01:05:25.250 testAudio[9759:207] 0.186961
2011-10-31 01:05:25.251 testAudio[9759:207] 0.248138
2011-10-31 01:05:25.252 testAudio[9759:207] 0.308339
2011-10-31 01:05:25.252 testAudio[9759:207] 0.367330
2011-10-31 01:05:25.253 testAudio[9759:207] 0.424877
2011-10-31 01:05:25.254 testAudio[9759:207] 0.480755
2011-10-31 01:05:25.254 testAudio[9759:207] 0.534744
2011-10-31 01:05:25.255 testAudio[9759:207] 0.586632
2011-10-31 01:05:25.256 testAudio[9759:207] 0.636216
2011-10-31 01:05:25.257 testAudio[9759:207] 0.683300
2011-10-31 01:05:25.257 testAudio[9759:207] 0.727699
2011-10-31 01:05:25.258 testAudio[9759:207] 0.769240
2011-10-31 01:05:25.259 testAudio[9759:207] 0.807759
2011-10-31 01:05:25.260 testAudio[9759:207] 0.843104
2011-10-31 01:05:25.261 testAudio[9759:207] 0.875137
2011-10-31 01:05:25.261 testAudio[9759:207] 0.903732
2011-10-31 01:05:25.262 testAudio[9759:207] 0.928777
2011-10-31 01:05:25.263 testAudio[9759:207] -0.928781
2011-10-31 01:05:25.264 testAudio[9759:207] -0.903727
2011-10-31 01:05:25.264 testAudio[9759:207] -0.875135
2011-10-31 01:05:25.265 testAudio[9759:207] -0.843105
2011-10-31 01:05:25.266 testAudio[9759:207] -0.807763
2011-10-31 01:05:25.267 testAudio[9759:207] -0.769249
2011-10-31 01:05:25.267 testAudio[9759:207] -0.727692
2011-10-31 01:05:25.268 testAudio[9759:207] -0.683296
2011-10-31 01:05:25.269 testAudio[9759:207] -0.636217
2011-10-31 01:05:25.269 testAudio[9759:207] -0.586638
2011-10-31 01:05:25.270 testAudio[9759:207] -0.534756
2011-10-31 01:05:25.271 testAudio[9759:207] -0.480746
2011-10-31 01:05:25.271 testAudio[9759:207] -0.424873
2011-10-31 01:05:25.272 testAudio[9759:207] -0.367332
2011-10-31 01:05:25.273 testAudio[9759:207] -0.308348
2011-10-31 01:05:25.273 testAudio[9759:207] -0.248152
2011-10-31 01:05:25.274 testAudio[9759:207] -0.186952
2011-10-31 01:05:25.275 testAudio[9759:207] -0.125047
2011-10-31 01:05:25.276 testAudio[9759:207] -0.062652
EDIT
I've wroten the generated sound sample (440Hz, 444100 sample rate, 0.1 second) into a file, and opened it with a sound editor. Cut and paste the sound many times to make a longer sound : it plays with no click. The same sound sample played through the AVAudioPlayer generate clicks at the end of each sample. So the problem seems to be in the AVAudioPlayer, for a reason I do not understand, because only some specific values generate those clicks.
EDIT
I've used the wav generated file and made it play with an AVAudioPlayer with a loop : clicks
I've used the same file and made it play with a loop with OpenAL using a custom library : no more clicks. The problem is that OpenAL is really a nightmare to understand and would lead to a compleet rewrite of my sound part, just for that poor sound.
The problem is apparently the use of AVAudioPlayer. If you had a solution to make it work, it will save me days.
Upvotes: 4
Views: 1107
Reputation: 212979
In the general case any synthesised sound which you want to play needs to have an onset and offset ramp applied (aka attack and decay) otherwise you get transients at the beginning and end of the sound, which may be audible as clicks.
A simple linear ramp over a period of a few ms is usually sufficient to eliminate this, although a smoother shape such as exponential or raised cosine is generally preferred.
An added bonus is that you do not need to ensure that your waveform begins and ends at zero, since the onset and offset function takes care of this.
const int kAttack = (int)(0.005f * sampleRate); // 5 ms attack period (samples)
const int kDecay = (int)(0.010f * sampleRate); // 10 ms decay period (samples)
for (int i = 0; i < frames; i++)
{
float a = gain * sinf((float)i * 2.0f * M_PI * frequency / sampleRate);
if (i < kAttack) // if in attack (onset) period
{
a *= (float)i / kAttack; // apply linear onset ramp
}
else if (i > frames - kDecay) // if in decay (offset) period
{
a *= 1.0f - (float)(i - (frames - kDecay)) / kDecay; // apply linear offset ramp
}
rawSound[i] = a;
}
Upvotes: 0
Reputation: 93750
After seeing your edits and sample data I am reasonably convinced you are avoiding the pitfalls I described in my other answer with the specific values you've chosen.
Let me suggest an alternative: AVAudioPlayer
takes interleaved stereo samples (because numberOfChannels
is 2) and when you present an even number of samples you hear two tones (one very slightly out of phase with the other) at twice the intended frequency. When you present an odd number (as in your last example) there is one sample missing for one channel which leads to a pop.
This is a wild guess because I'm not an iOS developer and I can't understand why numberOfChannels
is read-only instead of read-write.
Upvotes: 0
Reputation: 93750
Your chosen frequency of 200Hz is not an integral number of samples at 44.1kHz. If there are 44100 samples/sec / 200 cycles/sec you get 220.5 samples/cycle. So any time nbWavesNeeded
is not even (to cancel out the half sample) your adjustedDuration
when translated into frames
has a small roundoff error which produces the pop.
(After your edit to 440Hz the problem is worse because 44100/440 has a higher greatest common factor)
As far as I've understood the principle, the wave frequency is how many up-and-down waves there are in a second. the duration is... the duration, and the sampleRate is how many cuts there are in one second. So if I cut a wave in 1, 10, 50 or 1000 parts, it's always the same wave, just less precise.
That is basically correct. So at hertz = 440
there are "440 up and down waves in a second" and with sampleRate = 44100
your second is divided into 44100 slices. How many slices does one "up and down wave" take? 1/440th of a second, or 1/440th of your 44100 slices, or 44100 / 440
which is 100.2272727272...
So if frames == 100.22727272..
then the exact end of an "up down wave" would correspond to the exact end of your rawSound
. But frames
is an integer, so you stop at frames = 100
so you have cut your wave short. When the sound player loops back to 0 it really wants to loop to 0.2272727...
but of course it can't. You hear that as a pop.
Upvotes: 4
Reputation: 70703
The way to generate a pure continuous tone on iOS is to not use AVAudioPlayer, and depend on it to properly concatenate audio fragments, but to use the Audio Queue API or the RemoteIO Audio Unit, and control the continuity of the audio going into the callback buffers yourself.
Upvotes: 1