I have a basic beep sound that I loaded with AVAudioPlayer in my app.
It can play the beep fine if my finger isn't panning my MKMapView.
My beep is set to play every 2 seconds.
When I start panning the map view, and don't take my finger off the screen, the beep stops playing.
I recall NSUrlConnection also doesn't fire while scrolling a table view, I thought this might be the same problem but I couldn't figure out how I can add my audio player to the proper run loop.
I setup my player like this:
-(void)setupBeep
{
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/beep.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = 0;
if(error)
{
NSLog(#"Error opening sound file: %#", [error localizedDescription]);
}
}
and I am playing my sound like this:
// plays a beeping sound
-(void)beep
{
[audioPlayer play];
}
Anyone came across this problem before?
How have you scheduled your -beep method to be called?
I suspect you’ve added an NSTimer to the main run loop in the default input mode. The reason you’re not hearing anything is that while MKMapView is tracking your finger, the run loop is in not in NSDefaultRunLoopMode—it’s in UITrackingRunLoopMode.
Try scheduling in NSRunLoopCommonModes, which includes both default and tracking modes:
NSTimer *timer = [NSTimer timerWithTimeInterval:interval target:self selector:#selector(beep) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Also, be aware that NSTimer retains its target. A repeating timer will reschedule itself automatically, so you’ll need to call its -invalidate method to remove it from the loop and allow the target to be released.
EDIT: Check out Apple’s Threading Programming Guide: Run Loops for a much more detailed explanation.
Related
I'm working on an IOS game, and the background music clip is 7 seconds long, so when the scene changes from one scene to the next, I want it to stop instantly, but it continues and finishes the 7 second loop then stops.
This is the code:
[self runAction:[SKAction repeatActionForever:[SKAction playSoundFileNamed:#"sound 1.m4a" waitForCompletion:YES]]];
if (_dead == YES) {
[self removeAllActions];
}
where _dead is when the player dies, and the new scene is triggered.
How can I get the music to stop that instant, as opposed to finishing its loop?
Instead of using SKAction to play your background sound file, I would suggest using AVFoundation which allows you to simply issue a stop command.
Update
I can't think of any food tutorials that focus primarily on the sound side. Most deal with video and sound. Try google with something like 'AVFoundation tutorial'.
To use AVFoundation you can do this for your purposes...
Add the AVFoundation.framework to your project.
In your .h file add the delegate <AVAudioPlayerDelegate>
In your .m file #import <AVFoundation/AVFoundation.h>
Create a property AVAudioPlayer *_backgroundMusicPlayer;
Create this method:
- (void)playBackgroundMusic:(NSString *)filename {
NSError *error;
NSURL *backgroundMusicURL = [[NSBundle mainBundle] URLForResource:filename withExtension:nil];
_backgroundMusicPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:backgroundMusicURL error:&error];
_backgroundMusicPlayer.numberOfLoops = -1;
_backgroundMusicPlayer.volume = 0.2;
_backgroundMusicPlayer.delegate = self;
[_backgroundMusicPlayer prepareToPlay];
[_backgroundMusicPlayer play];
}
To start playing [self playBackgroundMusic:[NSString stringWithFormat:#"bgMusic.mp3"]];
To stop playing [_backgroundMusicPlayer stop];
That should do the trick. I suggest you read up on the AVAudioPlayer Class Reference so you understand its properties. For example, numberOfLoops set to -1 will loop the sound indefinitely until you call the stop method. Another issue to keep in mind is the audio file sound format. AVFoundation is somewhat picky about what sound files it will play. I always stick to mp3.
Fuzzygoat also makes an excellent point. I have not tried his approach in regards to sound so I do not know if it will solve your issue. It is similar to what you already have but with a slight change.
To start the sound:
SKAction *mySound = [SKAction repeatActionForever:[SKAction playSoundFileNamed:#"astroKitty 1.m4a" waitForCompletion:YES]];
[self runAction:mySound withKey:#"boogieDown"];
To stop the sound:
[self removeActionForKey:#"boogieDown"];
I am trying to start playing a sound from a background task via an AVAudioPlayer that is instantiated then, so here's what I've got.
For readability I cut out all user selected values.
- (void)restartHandler {
bg = 0;
bg = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bg];
}];
tim = [NSTimer timerWithTimeInterval:.1 target:self selector:#selector(upd) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tim forMode:NSRunLoopCommonModes];
}
- (void)upd {
if ([[NSDate date] timeIntervalSinceDate:startDate] >= difference) {
[self playSoundFile];
[tim invalidate];
[[UIApplication sharedApplication] endBackgroundTask:bg];
}
}
- (void)playSoundFile {
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
// new player object
_player = [[AVQueuePlayer alloc] init];
[_player insertItem:[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:#"Manamana" withExtension:#"m4a"]] afterItem:nil];
[_player setVolume:.7];
[_player play];
NSLog(#"Testing");
}
Explanation: bg, tim, startDate, difference and _player are globally declared variables. I call - (void)restartHandler from a user-fired method and inside it start a 10Hz repeating timer for - (void)upd. When a pre-set difference is reached, - (void)playSoundFile gets called and initiates and starts the player. The testing NSLog at the end of the method gets called.
Now the strange thing is if I call - (void)playSoundFile when the app is active, everything works out just fine, but if I start it from the background task it just won't play any sound.
Edit
So I tried using different threads at runtime and as it seems, if the Player is not instantiated on the Main Thread this same problem will appear.
I also tried using AVPlayer and AVAudioPlayer where the AVAudioPlayer's .playing property did return YES and still no sound was playing.
From what I've learned after writing an player App, it seems that you can not start playing audio when your App is already in background for longer than X seconds, even if you have configured everything right.
To fix it, you have to use background task wisely.
The most important thing is that you must NOT call endBackgroundTask immediately after playSoundFile. Delay it for about 5-10 seconds.
Here is how I managed doing it for iOS6 and iOS7.
1, Add audio UIBackgroundModes in plist.
2, Set audio session category:
3, Create an AVQueuePlayer in the main thread and enqueue an audio asset before the App enter background.
*For continues playing in background (like playing a playlist)*
4, Make sure the audio queue in AVQueuePlayer never become empty, otherwise your App will be suspended when it finishes playing the last asset.
5, If 5 seconds is not enough for you to get the next asset you can employ background task to delay the suspend. When the asset is ready you should call endBackgroundTask after 10 seconds.
include your playSoundFile call in the below, this should always run it in main thread
dispatch_async(dispatch_get_main_queue(), ^{
});
add this if to your - (void) playSoundFile to check if player is created, if not, then create it
if(!_player) {
_player = [[AVQueuePlayer alloc] init];
}
Seems to me as if you do not have the appropriate background mode set?
Also, looks like another user had the same issue and fixed it with an Apple Docs post. Link to Post
You may need to just do the step to set the audio session as active:
[audioSession setActive:YES error:&activationError];
Hope it helps!
If you need to start playing a sound in the background (without a trick such as keeping on playing at volume zero until you need the sound):
Set Background mode Audio in your p.list;
Set AudioSession category to AVAudioSessionCategoryPlayback
Set AudioSession to active
When your backgrounded app becomes running, start a background task before playing the sound (apparently when the remaining background time is less than 10 seconds, the sound is not played)
This now works for me.
What fixed it for me was to start playing some audio just as application quits, so I added 1sec blank audio in applicationWillResignActive in the AppDelegate
func applicationWillResignActive(_ application: UIApplication) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
self.endBackgroundUpdateTask(true)
})
let secSilence = URL(fileURLWithPath: Bundle.main.path(forResource: "1secSilence", ofType: "mp3")!)
do {
audioPlayer = try AVAudioPlayer(contentsOf: secSilence)
}
catch {
print("Error in loading sound file")
}
audioPlayer.prepareToPlay()
audioPlayer.play()
}
I am starting to get convinced that iOS is just pure magic. I looked through numerous posts on AVAudioPlayer for solutions, but none worked.
When the application starts I have a movie starting off from MPMoviePlayerController and I have AVAudioPlayer kicking off with a sound as follows:
-(void)createAndPlayMovieFromURL:(NSURL *)movieURL sourceType:(MPMovieSourceType)sourceType{
/* Create a new movie player object. */
player = [[MPMoviePlayerController alloc] initWithContentURL:movieURL];
player.controlStyle = MPMovieControlModeDefault;
if (player)
{
/* Save the movie object. */
[self setMoviePlayerController:player];
/* Specify the URL that points to the movie file. */
[player setContentURL:movieURL];
/* If you specify the movie type before playing the movie it can result
in faster load times. */
[player setMovieSourceType:sourceType];
CGRect frame = CGRectMake(0, 0, 480, 320);
/* Inset the movie frame in the parent view frame. */
[[player view] setFrame:frame];
[player view].backgroundColor = [UIColor blackColor];
/* To present a movie in your application, incorporate the view contained
in a movie player’s view property into your application’s view hierarchy.
Be sure to size the frame correctly. */
[self.view addSubview: [player view]];
}
[[self moviePlayerController] play];
}
and for sound:
- (void)setupAudioPlayBack:(NSString *) path : (NSString *) extension{
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle]
pathForResource:path
ofType:extension]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc]
initWithContentsOfURL:url
error:&error];
if (error)
{
NSLog(#"Error in audioPlayer: %#",
[error localizedDescription]);
} else {
audioPlayer.delegate = self;
[audioPlayer prepareToPlay];
}
}
And now lets talk magic. When the first movie plays and the sound, the sound is not responding to either the volume controls on the phone nor to Silent Mode On switch.
But then immediately a second movie plays with its own sound and everything works fine - there is no sound with Silent Mode On, if Silent Mode Off it responds to Volume Controls. 3rd movie - plays perfectly too.
I tried using the player.useApplicationAudioSession = NO for MPMoviePlayerController - fixed the problem with the first time sound not responding to Silent Mode or Volume Controls, but now when the second movie starts to play it freezes instantly and the sound played is cut from the original size to something around 2 seconds.
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategorySoloAmbient error:nil]; didn't help either wherever I set it.
So my question is - how to make the MPMoviePlayerController interact with AVAudioPlayer without any problems...
Ok I actually found a solution (once again, magic).
When you start your ViewController you set it up as follows:
- (void)viewDidLoad
{
[self setupAudioPlayBack:#"sound1" :#"caf"];
[self playMovieFile:[self localMovieURL:#"mov1" :#"mov"]];
audioTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(playAudio) userInfo:nil repeats:NO];
[super viewDidLoad];
}
Where the playAudio method is simply:
- (void)playAudio{
[audioPlayer play];
}
Just 0.1 millisecond timer is enough to make everything work (0.0 doesn't work) and make sure a new video doesn't start before the audio finishes or it will destroy the settings so you will have to invalidate the timer and redo the timer again.
I have a piano application. It's working fine, with a little error. If I play several keys at the same time very fast, the sounds disappears for a couple of seconds, and receive the following message in the console
AudioQueueStart posting message to kill mediaserverd
Here is the relevant code:
-(IBAction)playNoteFromKeyTouch:(id) sender{
[NSThread detachNewThreadSelector:#selector(playNote:) toTarget:self withObject:[NSString stringWithFormat:#"Piano.mf.%#",[sender currentTitle]]];
}
-(void)playNote:(NSString *) note{
NSError *err;
NSString *path = [[NSBundle mainBundle] pathForResource:note ofType:#"aiff"];
AVAudioPlayer *p = [[AVAudioPlayer alloc ] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&err];
p.delegate = self;
if (err) {
NSLog(#"%#", err);
}else{
[p prepareToPlay];
[p play];
}
}
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
[player release];
}
I have tested with Instruments and I don't have any memory leak. If somebody could have an idea to avoid this error it would be appreciated.
I suffer from a similar issue.
I spent ages trying to solve the issue, and I think my particular issue takes place when:
I'm in an AudioCategory that doesn't allow sound to play while the mute switch is on.
I start to play a sound (I actually don't do this in the app, but this is how I can reproduce reliably).
With the sound still playing, I switch to another AudioCategory that doesn't allow sound to play while the mute switch is on.
From this point onwards, I get 'posting message to kill mediaserverd' from what looks like various points in calls in the AudioSession API. The app hangs, the device hangs, and I struggle to get the device back to a normal running state.
According to this message it's the device's mute switch.
It turns out that having the iPad muted via the device's physical switch was
causing the problem with my app. As long as the button is not
switched on the problem does not occur.
Sheesh. How to programmatically override?
I "solved" the issue using SoundBankPlayer instead of AVAudioPlayer. SoundBanker info.
I'm using the MediaPlayer framework to play a pretty sizeable movie (~200 MB) as soon as my application is launched. When I attempt to play the video in my viewDidLoad, breakpoints indicated that the view was added however the video did not show up on the device.
I then set up a button to confirm that the video worked at all. Running the IBAction from the button showed no problems.
So then I was stumped. And I wondered if it had anything to do with the fact that the video was being called as soon as the application was launched. So I added a NSTimer with a delay of five seconds to the viewDidLoad call. Lo and behold, it worked fine. Can anyone shed any light on to why the video won't play unless there is an initial delay? Code below.
- (void)viewDidLoad {
NSTimer *currentTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:#selector(playMovie) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:currentTimer forMode:NSRunLoopCommonModes];
[super viewDidLoad];
}
-(IBAction)playMovie
{
NSString *filepath = [[NSBundle mainBundle] pathForResource:#"Speed" ofType:#"mov"];
NSURL *fileURL = [NSURL fileURLWithPath:filepath];
MPMoviePlayerController *moviePlayerController = [[MPMoviePlayerController alloc] initWithContentURL:fileURL];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlaybackComplete:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:moviePlayerController];
[self.view addSubview:moviePlayerController.view];
moviePlayerController.fullscreen = YES;
[moviePlayerController play];
}
Try playing the movie in viewDidAppear: instead of viewDidLoad. At the point viewDidLoad is called, the view is not yet in the view hierarchy. You could still create the MPMoviePlayerController in viewDidLoad, just don't play it until the view actually appears.
I think the size of the video has something to do here. I tried doing the same thing with a smaller video and it works fine. It seems the video is loaded first and then played ( see this stack over flow question: MPMoviePlayerController not showing controls until video is loaded). Apparently, the 200 MB video takes 4-5 seconds to preload. Try the code that was'nt working with a video of smaller size. If it works fine, then it's definitely the size problem.
Edit: I found this in MPMoviePlayerController Class Reference "If you know the source type of the movie, setting the value of this property before playback begins can improve the load times for the movie content. If you do not set the source type explicitly before playback, the movie player controller must gather this information, which might delay playback." I notice you haven't declared source type. Add to that the size of the video, and you've got yourself 4-5 seconds of load time.
Also try this before playing [moviePlayerController prepareToPlay];
I had a different experience but moving the code in viewDidAppear: helped my case as well. I have IOS code that segues from one storyboard to another and once the segue is complete - in the viewDidLoad: I was initializing a periodic timer. The code for the timer is below [I was 'calling' startTimer in viewDidLoad:]:
// Schedules a new timer, adds it to the current run loop and waits forever.
- (void) startTimer
{
_timer = [NSTimer scheduledTimerWithTimeInterval: 10.0
target:self
selector:#selector(request)
userInfo:nil
repeats:YES];
// [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
// [[NSRunLoop mainRunLoop] runUntilDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
}
What is interesting is that the when I call the startTimer in viewDidLoad: I can never observe the view on the iPhone change to the next controller - I can see the NSLog statement that shows the control came to
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
but the control does not go to the ViewController.
This behavior is independent of the timing of the periodic timer - setting low or high does not impact the problem. The observed problem goes away when I move the startTimer invocation to viewDidAppear:
This is followed by another set of challenge even when I move the timer code to viewDidAppear:. This code of having a regular timer - I have created this so that I can get the RSSI value for the Bluetooth Low Energy Connection. What I have observed is that I stop getting delegate callbacks for the Bluetooth delegate.