How to get audio volume level, and volume changed notifications on iOS?
Asked Answered
T

10

60

I'm writing a very simple application that plays a sound when pressing a button. Since that button does not make a lot of sense when the device is set to silence I want to disable it when the device's audio volume is zero. (And subsequently reenable it when the volume is cranked up again.)

I am seeking a working (and AppStore safe) way to detect the current volume setting and get a notification/callback when the volume level changes. I do not want to alter the volume setting.

All this is implemented in my ViewController where said button is used. I've tested this with an iPhone 4 running iOS 4.0.1 and 4.0.2 as well as an iPhone 3G running 4.0.1. Built with iOS SDK 4.0.2 with llvm 1.5. (Using gcc or llvm-gcc doesn't improve anything.) There are no issues during build implementing either way, neither errors nor warnings. Static analyzer is happy as well.

Here is what I've tried so far, all without any success.

Following Apple's audio services documentation I should register an AudioSessionAddPropertyListener for kAudioSessionProperty_CurrentHardwareOutputVolume which should work like this:

// Registering for Volume Change notifications
AudioSessionInitialize(NULL, NULL, NULL, NULL);
returnvalue = AudioSessionAddPropertyListener (

kAudioSessionProperty_CurrentHardwareOutputVolume ,
      audioVolumeChangeListenerCallback,
      self
);

returnvalue is 0, which means that registering the callback worked.

Sadly, I never get a callback to my function audioVolumeChangeListenerCallback when I press the volume buttons on my device, the headset clicker or flip the ringer-silent switch.

When using the exact same code for registering for kAudioSessionProperty_AudioRouteChange (which is used as an analogous sample project in WWDC videos, Developer documentation and on numerous sites on the interwebs) I actually do get a callback when changing the audio route (by plugging in/out a headset or docking the device).

A user named Doug opened a thread titled iPhone volume changed event for volume already max where he claimed that he is sucessfully using this way (unless the volume would not actually change because it is already set to maximum). Still, it doesn't work for me.

Another way I have tried is to register at NSNotificationCenter like this.

// sharedAVSystemController 
AudioSessionInitialize(NULL, NULL, NULL, NULL);
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
                                         selector:@selector(volumeChanged:) 
                                             name:@"AVSystemController_SystemVolumeDidChangeNotification" 
                                           object:nil];

This should notify my method volumeChanged of any SystemVolume changes but it doesn't actually do so.

Since common belief tells me that if one is working too hard to achieve something with Cocoa one is doing something fundamentally wrong I'm expecting to miss something here. It's hard to believe that there is no simple way to get the current volume level, yet I haven't been able to find one using Apple's documentation, sample code, Google, Apple Developer Forums or by watching WWDC 2010 videos.

Thereunder answered 6/9, 2010 at 11:58 Comment(1)
Check out Stuart's answer below for the iOS7 solution.Donnelly
S
70

Any chance you did your signature wrong for the volumeChanged: method? This worked for me, dumped in my appdelegate:

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(volumeChanged:)
     name:@"AVSystemController_SystemVolumeDidChangeNotification"
     object:nil];
}

- (void)volumeChanged:(NSNotification *)notification
{
    float volume =
    [[[notification userInfo]
      objectForKey:@"AVSystemController_AudioVolumeNotificationParameter"]
     floatValue];

    // Do stuff with volume
}

My volumeChanged: method gets hit every time the button is pressed, even if the volume does not change as a result (because it's already at max/min).

Shoeshine answered 24/6, 2011 at 18:25 Comment(7)
It works for me: I copy & paste Sandy's sample code. Suggest you first take the sample code which Apple provided (I use 'aurioTouch'), add Sandy's sample code there. Maybe it could help you to find out what happened.Hasheem
Works fine if you add MPVolumeView *slide = [MPVolumeView new]; and also #import <MediaPlayer/MediaPlayer.h>Jimmy
@Sandy: flopes and Gal 's comments work. You should add this to your answerCarrell
is "AVSystemController_SystemVolumeDidChangeNotification" private? because if it is this can get your app rejected.Nystatin
Will apple accpet if i use this in my app?Cleisthenes
I add [MPVolumeView new]; befour addObserver and my notification working 6+.Sulphanilamide
I don't think Apple will accept it, it uses private api.Hermineherminia
Z
57

The AudioSession API used by some answers here has been deprecated as of iOS 7. It was replaced by AVAudioSession, which exposes an outputVolume property for the system wide output volume. This can be observed using KVO to receive notifications when the volume changes, as pointed out in the documentation:

A value in the range 0.0 to 1.0, with 0.0 representing the minimum volume and 1.0 representing the maximum volume.

The system wide output volume can be set directly only by the user; to provide volume control in your app, use the MPVolumeView class.

You can observe changes to the value of this property by using key-value observing.

You need to ensure your app's audio session is active for this to work:

let audioSession = AVAudioSession.sharedInstance()
do {
    try audioSession.setActive(true)
    startObservingVolumeChanges()
} catch {
    print(“Failed to activate audio session")
}

So if all you need is to query the current system volume:

let volume = audioSession.outputVolume

Or we can be notified of changes like so:

private struct Observation {
    static let VolumeKey = "outputVolume"
    static var Context = 0

}

func startObservingVolumeChanges() {
    audioSession.addObserver(self, forKeyPath: Observation.VolumeKey, options: [.Initial, .New], context: &Observation.Context)
}

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if context == &Observation.Context {
        if keyPath == Observation.VolumeKey, let volume = (change?[NSKeyValueChangeNewKey] as? NSNumber)?.floatValue {
            // `volume` contains the new system output volume...
            print("Volume: \(volume)")
        }
    } else {
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
}

Don't forget to stop observing before being deallocated:

func stopObservingVolumeChanges() {
    audioSession.removeObserver(self, forKeyPath: Observation.VolumeKey, context: &Observation.Context)
}
Zachary answered 20/3, 2015 at 16:3 Comment(2)
Don't you guys forget that this kvo is called when you launch the video for the fist time.Hertfordshire
Doesn't compile using Xcode 9 beta 5. See Swift 3 version that I just posted. Tried to edit the code above, but was rejected.Complicated
E
6
-(float) getVolumeLevel
{
    MPVolumeView *slide = [MPVolumeView new];
    UISlider *volumeViewSlider;

    for (UIView *view in [slide subviews]){
        if ([[[view class] description] isEqualToString:@"MPVolumeSlider"]) {
            volumeViewSlider = (UISlider *) view;
        }
    }

    float val = [volumeViewSlider value];
    [slide release];

    return val;
}

That should get you the current volume level. 1 is max volume, 0 is no volume. Note: no UI elements need to be displayed for this to work. Also note current volume level is relative to headphones or speakers (meaning, the two volume levels are different, and this gets you whichever the device is currently using. This doesn't answer your question regarding receiving notifications of when volume changes.

Epigenous answered 4/2, 2012 at 3:41 Comment(5)
Beautiful however this code isn't future proof. If apple changes things and your code doesn't find a UISlider, the app will crash when it tries to retrieve the value since UISlider hasn't been initialized.Curdle
initialize uislider as UISlider *volumeViewSlider = nil.... then before you get the value, check if volumeViewSlider is nil. If it is, then dont try and get the value or release it!Curdle
Such hacks are strongly discouraged !Joselynjoseph
Yeah but what else can you do??Introit
Downvoting to help bump Stuart's more up-to-date answer for future readers.Meadows
P
4

did you start the audio session with AudioSessionSetActive

Peary answered 6/9, 2010 at 13:5 Comment(3)
Adding AudioSessionSetActive (true); doesn't change anything. Apple doesn't do it in their examples either.Thereunder
After setting the property listeners and activating the audio session, it did start working for me. This makes sense as another audio session, iPod usually, is taking volume changes.Anglicism
I set AudioSessionSetActive(YES); prior to calling float volume = [[AVAudioSession sharedInstance] outputVolume]; and it worked. Without calling AudioSessionSetActive(YES) I continued getting the same (incorrect) volume returned.Hecate
W
2

Adding on to Stuart's answer using AVAudioSession to account for some changes in Swift 3. I hope the code will make it clear as to where each component goes.

override func viewWillAppear(_ animated: Bool) {
    listenVolumeButton()
}

func listenVolumeButton(){
   let audioSession = AVAudioSession.sharedInstance()
   do{
       try audioSession.setActive(true)
       let vol = audioSession.outputVolume
       print(vol.description) //gets initial volume
     }
   catch{
       print("Error info: \(error)")
   }
   audioSession.addObserver(self, forKeyPath: "outputVolume", options: 
   NSKeyValueObservingOptions.new, context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "outputVolume"{
        let volume = (change?[NSKeyValueChangeKey.newKey] as 
        NSNumber)?.floatValue
        print("volume " + volume!.description)
    }
}

 override func viewWillDisappear(_ animated: Bool) {
     audioSession.removeObserver(self, forKeyPath: "outputVolume")
 }
Wildwood answered 26/3, 2017 at 18:16 Comment(0)
C
2

Swift 3 version of Stuart's excellent answer:

let audioSession = AVAudioSession.sharedInstance()

do {
    try audioSession.setActive(true)
    startObservingVolumeChanges()
} 
catch {
    print("Failed to activate audio session")
}

let volume = audioSession.outputVolume

private struct Observation {
    static let VolumeKey = "outputVolume"
    static var Context = 0
}

func startObservingVolumeChanges() {
    audioSession.addObserver(self, forKeyPath: Observation.VolumeKey, options: [.Initial, .New], context: &Observation.Context)
}

override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
    if context == &Observation.Context {
        if keyPath == Observation.VolumeKey, let volume = (change?[NSKeyValueChangeNewKey] as? NSNumber)?.floatValue {
            // `volume` contains the new system output volume...
            print("Volume: \(volume)")
        }
    } else {
        super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
    }
}
Complicated answered 15/8, 2017 at 12:39 Comment(0)
F
2

Swift 4

func startObservingVolumeChanges() {
    avAudioSession.addObserver(self, forKeyPath: Observation.VolumeKey, options: [.initial, .new], context: &Observation.Context)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &Observation.Context {
        if keyPath == Observation.VolumeKey, let volume = (change?[NSKeyValueChangeKey.newKey] as? NSNumber)?.floatValue {
            print("\(logClassName): Volume: \(volume)")
        }
    } else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
    }
}

func stopObservingVolumeChanges() {
    avAudioSession.removeObserver(self, forKeyPath: Observation.VolumeKey, context: &Observation.Context)
}

and then you call

var avAudioSession = AVAudioSession.sharedInstance()
try? avAudioSession.setActive(true)
startObservingVolumeChanges()
Filiform answered 19/1, 2018 at 14:11 Comment(0)
I
2

Swift 5 / iOS 13

In my tests I've found that the most reliable way to interface with the system volume is using an MPVolumeView as an intermediary for every operation. You already need to have this view somewhere in your view hierarchy in order to make the system hide the volume-change HUD.

During setup, likely inside viewDidLoad(), create your MPVolumeView (offscreen if you don't want to actually use the system-provided control):

let systemVolumeView = MPVolumeView(frame: CGRect(x: -CGFloat.greatestFiniteMagnitude, y: 0, width: 0, height: 0))
myContainerView.addSubview(systemVolumeView)
self.systemVolumeSlider = systemVolumeView.subviews.first(where:{ $0 is UISlider }) as? UISlider

Get and set the volume:

var volumeLevel:Float {
    get {
        return self.systemVolumeSlider.value
    }
    set {
        self.systemVolumeSlider.value = newValue
    }
}

Observe changes to the volume (including from the hardware buttons):

self.systemVolumeSlider.addTarget(self, action: #selector(volumeDidChange), for: .valueChanged)

@objc func volumeDidChange() {
    // Handle volume change
}
Ilke answered 11/7, 2020 at 21:17 Comment(2)
Super useful; thank you Robin. I tried several other approaches with older Swift versions and this is the only one that has worked for me. And for anyone that doesn't know, you must import MediaPlayer to create the MPVolumeView.Brahmani
Great answer, just want to add another thing '-CGFloat.greatestFiniteMagnitude' is not needed, you can set 'x' to '0' and then set the 'showsRouteButton' property of 'MPVolumeView' to 'false' instead.Labonte
W
1

I think it depends on other implementation. If you for instance use the slider for controlling the volume of sound you can make a checking action by UIControlEventValueChanged and if you get a 0 value you can set the button hidden or disabled.

Something like:

[MusicsliderCtl addTarget:self action:@selector(checkZeroVolume:)forControlEvents:UIControlEventValueChanged];

where void checkZeroVolume could do the comparing of the actual volume since it is triggered after any volume change.

Weep answered 6/9, 2010 at 13:23 Comment(3)
Since I don't need or want to control the volume I don't implement a MPVolumeView which I could ask for a slider value.Thereunder
Still, if performance is not an issue, the easiest way is to do as Vanya suggests and have a hidden MPVolumeView and add a KVO to its value propertySonjasonnet
Downvoting to help bump Stuart's more up-to-date answer for future readers.Meadows
T
0

Go into settings->sounds and check 'Change with Buttons'. If it's off the system volume won't change when pressing the volume buttons. Maybe that's the reason why you didn't get notified.

Trike answered 17/4, 2014 at 6:43 Comment(1)
how can we confirm that user has the buttons enabled? or how can we force user to do so?Corticosteroid

© 2022 - 2024 — McMap. All rights reserved.