I implemented OpenAL code to my iphone game. When I starts the game, it runs for 1 sec and stalls for 2 sec then resumes (hiccup effect). I believe its delayed due to the sound files loading. What is the solution? Can anyone recommend any book, site or sources code (not the iphone reference, please)? Is there a loading process and where should I initialize the loading process? Would that help?
Below, I have included the related components of the OpenAL code that I have implemented. The sound file will be played and invoked by a "if" statement in the gameloop. The OpenALController class is for the sound sources and buffers creation and the InitOpenAL method is invoked in OpenALController. MyView is a customized subclass of UIView and connected to the main view (I didn't use the default view).
// MyView.m
// A customized UIView as the main view.
#import "OpenALSoundController.h"
- (void)startPlaying{
...
[self initializeValuables];
...
[self initializeTimer];
}
- (void)initializeTimer {
if (theTimer == nil) {
theTimer = [CADisplayLink displayLinkWithTarget:self selector:#selector)gameLoop)];
theTimer.frameInterval = 2;
[theTimer addToRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];
}
}
- (void)gameLoop {
...
If something = true
// Play the sound
[[OpenALSoundController sharedSoundController] playSound1];
...
}
...
#end
// OpenALSoundController.h
#interface OpenALSoundController : NSObject {
...}
...
+ (OpenALSoundController*) sharedSoundController
...
#end
// OpenALSoundController.m
// Singleton accessor
{
static OpenALSoundController* shared_sound_controller;
#synchronized(self)
{
if(nil == shared_sound_controller)
{
shared_sound_controller = [[OpenALSoundController alloc] init];
}
return shared_sound_controller;
}
return shared_sound_controller;
}
- (void) initOpenAL{
...
file_url = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:#"fire" ofType:#"wav"]];
firePcmData = MyGetOpenALAudioDataAll((CFURLRef)file_url, &data_size, &al_format,&sample_rate);
alBufferData(fireOutputBuffer, al_format, firePcmData, data_size, sample_rate);
[file_url release];
...
alSourcei(outputSourceFire, AL_BUFFER, fireOutputBuffer);
...
}
You might be interested in Finch, an OpenAL sound engine for iOS. It’s very well suited to games. It’s usually better to reuse some already existing code than develop and maintain your own.
First its better to use mp3, as wav files are huge and loading from disk takes time. Mp3 files are smaller on disk, loaded into memory and decompressed there for playing. Try experimenting by reducing mp3 bitrate/encoding quality too.
Also you need to preload sounds to avoid hiccups, otherwise you will have a delay the first time a sound is played.
Related
I know that OpenAL is fast library but it doesn't support any compressed audio format and it's not so easy to use...
AVAudioPlayer is not so fast, but supports wide range file formats, as well as compressed formats like mp3.
Also there is an SKAction class which can play a sound, as well as SystemSoundID...
I have few questions:
What would be a preferred way/player/technique to play :
sound effects(multiple at the time)?
sound effects which can be sometimes repeated after small time delay
background music that loops
Also, is it smart move to use uncompressed audio for sound effects? I suppose this is ok, because those files have small size anyway?
I'm personally using ObjectAL. It's good because it utilizes OpenAL and AVAudioPlayer but abstracts a lot of the complicated parts away from you. My game has background music, tons of sounds playing simultaneously, and loopable sounds that increase in volume, pitch etc based on a sprites speed. ObjectAL can do all of that.
ObjectAL can be used for playing simple sounds and music loops using it's OALSimpleAudio class. Or you can get deeper into it and do more complex things.
I've created a simple wrapper around ObjectAL specifically for my game so it's further abstracted away from me.
From what I've read, uncompressed audio is better. You just need to make sure you preload the sounds so that your game isnt trying to pull the file each time it's playing a sound.
This very simple class has serviced multiple projects for me flawlessly with lots of sounds running at the same time. I find it a lot simpler than fussing with OpenAL. It has everything you asked for, pre-setup sounds, multiple concurrent plays, after delay, background loops.
Doesnt matter if you use compressed files or not because you set it up first.
SKAudio.h
#import <Foundation/Foundation.h>
#import AVFoundation;
#interface SKAudio : NSObject
+(AVAudioPlayer*)setupRepeatingSound:(NSString*)file volume:(float)volume;
+(AVAudioPlayer*)setupSound:(NSString*)file volume:(float)volume;
+(void)playSound:(AVAudioPlayer*)player;
+(void)playSound:(AVAudioPlayer*)player afterDelay:(float)delaySeconds;
+(void)pauseSound:(AVAudioPlayer*)player;
#end
SKAudio.m
#import "SKAudio.h"
#implementation SKAudio
#pragma mark -
#pragma mark setup sound
// get a repeating sound
+(AVAudioPlayer*)setupRepeatingSound:(NSString*)file volume:(float)volume {
AVAudioPlayer *s = [self setupSound:file volume:volume];
s.numberOfLoops = -1;
return s;
}
// setup a sound
+(AVAudioPlayer*)setupSound:(NSString*)file volume:(float)volume{
NSError *error;
NSURL *url = [[NSBundle mainBundle] URLForResource:file withExtension:nil];
AVAudioPlayer *s = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
s.numberOfLoops = 0;
s.volume = volume;
[s prepareToPlay];
return s;
}
#pragma mark sound controls
// play a sound now through GCD
+(void)playSound:(AVAudioPlayer*)player {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[player play];
});
}
// play a sound later through GCD
+(void)playSound:(AVAudioPlayer*)player afterDelay:(float)delaySeconds {
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delaySeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[player play];
});
}
// pause a currently running sound (mostly for background music)
+(void)pauseSound:(AVAudioPlayer*)player {
[player pause];
}
#end
To use in your game:
Set up a class variable and pre-load it with your sound:
static AVAudioPlayer *whooshHit;
static AVAudioPlayer *bgMusic;
+(void)preloadShared {
// cache all the sounds in this class
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whooshHit = [SKAudio setupSound:#"whoosh-hit-chime-1.mp3" volume:1.0];
// setup background sound with a lower volume
bgMusic = [SKAudio setupRepeatingSound:#"background.mp3" volume:0.3];
});
}
...
// whoosh with delay
[SKAudio playSound:whooshHit afterDelay:1.0];
...
// whoosh and shrink SKAction
SKAction *whooshAndShrink = [SKAction group:#[
[SKAction runBlock:^{ [SKAudio playStarSound:whooshHit afterDelay:1.0]; }],
[SKAction scaleTo:0 duration:1.0]]];
...
[SKAudio playSound:bgMusic];
...
[SKAudio pauseSound:bgMusic];
Here's a port of #patrick's solution to Swift 3.
import AVFoundation
// MARK -
// MARK setup sound
// get a repeating sound
func setupRepeatingSound(file: String, volume: Float) -> AVAudioPlayer? {
let sound: AVAudioPlayer? = setupSound(file: file, volume: volume)
sound?.numberOfLoops = -1
return sound
}
// setup a sound
func setupSound(file: String, volume: Float) -> AVAudioPlayer? {
var sound: AVAudioPlayer?
if let path = Bundle.main.path(forResource: file, ofType:nil) {
let url = NSURL(fileURLWithPath: path)
do {
sound = try AVAudioPlayer(contentsOf: url as URL)
} catch {
// couldn't load file :(
}
}
sound?.numberOfLoops = 0
sound?.volume = volume
sound?.prepareToPlay()
return sound
}
// MARK sound controls
// play a sound now through GCD
func playSound(_ sound: AVAudioPlayer?) {
if sound != nil {
DispatchQueue.global(qos: .default).async {
sound!.play()
}
}
}
// play a sound later through GCD
func playSound(_ sound: AVAudioPlayer?, afterDelay: Float) {
if sound != nil {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(afterDelay)) {
sound!.play()
}
}
}
// pause a currently running sound (mostly for background music)
func pauseSound(_ sound: AVAudioPlayer?) {
sound?.pause()
}
I've been working on this project for a while, but I've encountered a problem that I can't figure out.
First, I have a checkbox button that saves to NSDefaultUser as BOOL value. It simply saves the value YES when it's pressed once and it saves NO if pressed again and so on... This checkbox button works fine like a normal custom checkbox button would.
I would like to make an option to mute all of my sounds in my app by using this checkbox button.
I'm playing my sounds by calling method such as:
- (void)startMusic1
{
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/music1.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
AVAudioPlayer *audioPlayer;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = -1;
if (audioPlayer == nil)
NSLog(#"Error: %#", [error description]);
else
[audioPlayer play];
}
So for every sound I want to play in my app, I could repeat something like:
- (IBAction)playButton:(id)sender
{
NSUserDefaults *default = [NSUserDefaults standardUserDefaults];
if(![default boolForKey:#"isMuted"])
{
[self startMusic1];
}
}
But, this gets repetitive especially because I have to manage a lot of different sounds (and different buttons that play sounds) for my project, and it seems irrelevant to repeat these steps.
I tried making a new class with a subclass of AVAudioPlayer and messed around with -(BOOL)play method for a few days, but I couldn't manage to get the results I wanted. I researched and found posts like Disable in App sounds but this still wouldn't do it.
I'm pretty new to programming overall, so it'll be great if someone could enlighten me a little.
I tackle this (rightly or wrongly, but it works for me) by using a global model singleton. This is a class that effectively maintains state across the entire application.
The way to do that is with a class that has a shared static property like this:
//// Interface
#import <Foundation/Foundation.h>
#interface AudioModel
#property (nonatomic) BOOL playAudio;
+(id)sharedInstance;
#end
//// Implementation
#import "AudioModel.h"
#implementation AudioModel
// property for toggling audio on or off
#synthesize playAudio = _playAudio;
// singleton model variable
static AudioModel* audio = nil;
-(BOOL)playAudio {
return _playAudio;
}
-(void)setPlayAudio:(BOOL)playAudio {
_playAudio = playAudio;
}
// static
+(AudioModel*)sharedInstance {
if(audio == nil)
{
audio = [[AudioModel alloc]init];
}
return audio;
}
-(AudioModel*)init {
self = [super init];
if(self){
// set up default sounds on
// this may read from your stored value
_playAudio = YES;
}
}
#end
Then when you want to read or write to this globally available singleton model, you set a variable in your controller like this:
AudioModel *volumeControl = [AudioModel sharedInstance];
if(volumeControl.playAudio){
// method to play audio passing audio file name...
}
You could also have a reference to your AVAudioPlayer instance in this class, init it on creation of the shared instance and pass files to it to play when required.
I am trying to create a audio meter level while I am recording the user voice using avaudiorecorder. Can someone help me in that regard?
Actually, the code is pretty straightforward since AVAudioPlayer and AVAudioRecorder have built in methods to get you on your way. My approach was this:
make a repeating call to -updateMeters and the averagePowerForChannel: & peakPowerForChannel: methods and call a delegate method to notify the controlling object
Example:
NSOperationQueue *queue=[[NSOperationQueue alloc] init];
NSInvocationOperation *operation=[[NSInvocationOperation alloc] initWithTarget:self selector:#selector(updateMeter) object:nil];
[queue addOperation: operation];
and
-(void)updateMeter
{
do {
//don't forget:
[recorder updateMeters];
self.averagePower = [recorder averagePowerForChannel:0];
self.peakPower = [recorder peakPowerForChannel:0];
// we don't want to surprise a ViewController with a method call
// not in the main thread
[self.delegate performSelectorOnMainThread: #selector(meterLevelsDidUpdate:) withObject:self waitUntilDone:NO];
[NSThread sleepForTimeInterval:.05]; // 20 FPS
}while(someCondition);
}
If your View Controller implements the meterLevelsDidUpdate: method, you can use this method to update your Level Meter.
create a UIView subclass with a subview that changes its height according to the average or peak value(s). Adjust to taste.
Easy, you can use NSTimer for that:
- (void)startAudioMetering {
self.meterTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(updateAudioMeter)userInfo:nil repeats:YES];
}
- (void)stopAudioMetering {
[self.meterTimer invalidate];
}
- (void)updateAudioMeter { //called by timer
// audioRecorder being your instance of AVAudioRecorder
[self.audioRecorder updateMeters];
self.dBLevel = [self.audioRecorder averagePowerForChannel:0];
}
WARNING: While creating your AVAudioRecorder instance, you have to call meteringEnabled AFTER you call prepareToRecord or record, otherwise it won't updateMeters:
[self.audioRecorder prepareToRecord];
self.audioRecorder.meteringEnabled = YES;
Swift code based on Tom's answer:
NSOperationQueue().addOperationWithBlock({[weak self] in
repeat {
self?.audioRecorder.updateMeters()
self?.averagePower = self?.audioRecorder.averagePowerForChannel(0)
self?.peakPower = self?.audioRecorder.peakPowerForChannel(0)
self?.performSelectorOnMainThread(#selector(DictaphoneViewController.updateMeter), withObject: self, waitUntilDone: false)
NSThread.sleepForTimeInterval(0.05)//20 FPS
}
while (someCondition)
})
Do the meter UI stuff inside func updateMeter(){//UI stuff here}
Its pretty simple,
The values you get in the buffer are positive and negative (this is how the waves work) so if you do the average of that values it will give you a near 0 value.
So what you have to do is just put all values positive (with the Math.abs() function) and then do the avarage, it will return you the sound level.
Hope this helps ;)
You can also use ReactiveCocoa and make use of interval:onScheduler::
Returns a signal that sends the current date/time every interval on scheduler.
Using a single audio channel:
#weakify(self)
RACDisposable *metersDisposable = [[RACSignal // make sure you dispose it eventually
interval:0.1
onScheduler:[RACScheduler scheduler]]
subscribeNext:^(NSDate *) {
#strongify(self)
[recorder updateMeters];
auto averagePower = [recorder averagePowerForChannel:0];
auto peakPower = [recorder peakPowerForChannel:0];
// Inform the delegate or make other use of averagePower and peakPower
}];
I found the answer in following link.
AVMeter for AVPlayer
Though it requires lot of customizations but I feel I will be able to do it.
I have a drum app that I've created basing it off of AVAudioPlayer instead of the common system sounds in order to have a bit of control.
The problem is that whenever two or more sounds are played at once, it lags and stops alls sounds, so your drum beat gets choppy and randomly stopped.
For example: boom boom chhhhh boom boom ch--- boom bo---- chhhhh
If you can tell at all from that onomatopoeia.
Here's the sound code, I have an array of preloaded AVAudioPlayer so that it loads faster.
- (void)triggerSound:(NSInteger)soundNumber {
NSInteger deltaNum = soundNumber*numberOfBuffers;
AVAudioPlayer *lowBuffer = [bufferBox objectAtIndex:deltaNum];
Boolean soundFired = FALSE;
// Find an unused buffer if possible, otherwise play sound from first buffer.
for (int i=0; i<numberOfBuffers; i++) {
NSLog(#"Buffer loop: %d", i);
NSLog(#"Buffer to load: %d", deltaNum);
AVAudioPlayer *tempBuffer = [bufferBox objectAtIndex:deltaNum+i];
if (!tempBuffer.playing) {
tempBuffer.currentTime = 0;
[tempBuffer play];
soundFired = TRUE;
break;
} else if (lowBuffer.currentTime>tempBuffer.currentTime) {
lowBuffer = tempBuffer;
}
}
if (!soundFired) {
lowBuffer.currentTime = 0;
[lowBuffer play];
soundFired = TRUE;
}
}
That method is called in the IBAction button press. ex: [drumObject triggerSound:3].
All help appreciated!
I've had much better experiences with the OpenAL API for some games. It is very different and more low level (you have to deal with sources and buffers) but it worked much better for me than AVAudioPlayer.
You could try using an iOS game engine for your audio needs.
For example: http://gamua.com/sparrow/
You don't actually have to use the graphics part of this engine. Just import and use the audio part of the framework (which provides a simple API on top of OpenAL).
I'm using OpenAL sound framework on the iPhone, and I'm setting different volumes on individual sounds. I'm running into a problem where I'm hearing an initial popping/clicking noise when switching from one sound to the next.
It's really noticeable when I have one sound that's got a high volume (1.0) and a second
sound that has a low one (0.2). When I hit the loud sound, and then
hit the soft sound, I hear the pop/click. But when I go from the soft
sound to the loud, I don't notice anything. So the pop/click really
happens when switching from loud to soft sounds.
Here's the init sound method:
- (id) initWithSoundFile:(NSString *)file doesLoop:(BOOL)loops
{
self = [super init];
if (self != nil)
{
if(![self loadSoundFile:file doesLoop:loops])
{
debug(#"Failed to load the sound file: %#...", file);
[self release];
return nil;
}
self.sourceFileName = file;
//temporary sound queue
self.temporarySounds = [NSMutableArray array];
//default volume/pitch
self.volume = 1.0;
self.pitch = 1.0;
}
return self;
}
and here's the play function:
- (BOOL) play
{
if([self isPlaying]) //see if the base source is busy...
{
//if so, create a new source
NSUInteger tmpSourceID;
alGenSources(1, &tmpSourceID);
//attach the buffer to the source
alSourcei(tmpSourceID, AL_BUFFER, bufferID);
alSourcePlay(tmpSourceID);
//add the sound id to the play queue so we can dispose of it later
[temporarySounds addObject: [NSNumber numberWithUnsignedInteger:tmpSourceID]];
//a "callback" for when the sound is done playing +0.1 secs
[self performSelector:#selector(deleteTemporarySource)
withObject:nil
afterDelay:(duration * pitch) + 0.1];
return ((error = alGetError()) != AL_NO_ERROR);
}
//if the base source isn't busy, just use that one...
alSourcePlay(sourceID);
return ((error = alGetError()) != AL_NO_ERROR);
}
and here's the function where i set the volume for each sound immediately after playing (ive tried setting it before playing too):
- (void) setVolume:(ALfloat)newVolume
{
volume = MAX(MIN(newVolume, 1.0f), 0.0f); //cap to 0-1
alSourcef(sourceID, AL_GAIN, volume);
//now set the volume for any temporary sounds...
for(NSNumber *tmpSourceID in temporarySounds)
{
//tmpSourceID is the source ID for the temporary sound
alSourcef([tmpSourceID unsignedIntegerValue], AL_GAIN, volume);
}
}
Any help is greatly appreciated as I've tried everything I can think of. I would be so grateful.
All I had to do was use calloc instead of malloc to allocate memory for the OpenAL buffer.
Or you could also zero set the memory with memset.
The wierd popping noise went off. It was due to junk memory in my case. That's why it was random too. Hope this helps.
This problem is caused by not calling alSourceStop.
The documentation doesn't really state this, but alSourceStop must be called on a sound source before it can be reused even if the sound had already finished and the AL_SOURCE_STATE parameter of the source is not AL_PLAYING.
I've randomly got to this unaswered question and, finding that the problem was not solved, I'll try to give my answer, even if a long time has passed.
I don't know OpenAL, but it sounds like this is a purely audio problem. It is normal to hear short clicks when you change suddenly the level of the audio, especially from a high value to a low value. For example, if you map directly the volume of the audio to a slider, which value is updated every few ms, you can easily hear clicks and pops when sliding fast the control. What audio software developers do is smoothing the parameter changes with a low pass filter.
In your case, I would suggest you to stop the clip after fading it out, and start a new clip by fading it in. The fade time can be as short as 2 ms: it's not audible, and the sound will play just finely.
I wonder if (some versions of) OpenAL can automatically deal with this issue.