如何在iOS上获取音频音量级别并接收音量更改通知?

60
我正在编写一个非常简单的应用程序,当按下按钮时会播放声音。由于在将设备设置为静音时该按钮没有太多意义,因此我希望在设备的音频音量为零时禁用它。(并在音量再次增加时重新启用它。)
我正在寻找一种工作(且AppStore安全)的方法来检测当前音量设置,并在音量级别更改时获得通知/回调。我不想改变音量设置。
所有这些都是在我的ViewController中实现的,在其中使用了所述按钮。我已经测试过iPhone 4运行iOS 4.0.1和4.0.2以及iPhone 3G运行4.0.1。使用iOS SDK 4.0.2构建,使用llvm 1.5。(使用gcc或llvm-gcc没有任何改进。)无论哪种方式实施都没有问题,在构建期间既没有错误也没有警告。静态分析器也很高兴。
到目前为止,以下是我尝试过的所有内容,都没有成功。
根据苹果的音频服务文档,我应该为kAudioSessionProperty_CurrentHardwareOutputVolume注册一个AudioSessionAddPropertyListener,它应该像这样工作:
// Registering for Volume Change notifications
AudioSessionInitialize(NULL, NULL, NULL, NULL);
returnvalue = AudioSessionAddPropertyListener (

kAudioSessionProperty_CurrentHardwareOutputVolume ,
      audioVolumeChangeListenerCallback,
      self
);

returnvalue0,这意味着注册回调函数成功。

不幸的是,当我按设备上的音量按钮、耳机按钮或将铃声静音开关翻转时,我的函数 audioVolumeChangeListenerCallback 从未得到回调。

当使用完全相同的代码注册 kAudioSessionProperty_AudioRouteChange(在 WWDC 视频、开发者文档和许多网站上用作类似的示例项目)时,实际上会在更改音频路由时(通过插入/拔出耳机或将设备插入底座)得到回调。

名为 Doug 的用户发布了一个标题为 iPhone volume changed event for volume already max 的帖子,在那里他声称他成功地使用了这种方式(除非音量实际上已经设置为最大)。但对我来说并没有起作用。

我尝试过的另一种方法是像这样在 NSNotificationCenter 中注册。

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

这应该通知我的方法volumeChanged有任何SystemVolume的更改,但实际上并没有这样做。
由于普遍的信念告诉我,如果一个人在使用Cocoa时努力实现某些事情,那么他可能正在做一些根本错误的事情,因此我期望在这里漏掉了某些东西。很难相信没有简单的方法获取当前音量级别,然而我还没有能够找到一个使用苹果的文档、示例代码、谷歌、苹果开发者论坛或通过观看WWDC 2010视频来找到一个。

1
请查看Stuart在下面提供的iOS7解决方案。 - amergin
10个回答

70

你有没有可能在volumeChanged:方法的签名中写错了?这个对我起作用,在我的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
}

每次按下按钮时,即使音量没有改变(因为已经达到最大/最小值),我的volumeChanged方法也会被调用。


它对我有效:我复制并粘贴了Sandy的示例代码。建议您首先获取Apple提供的示例代码(我使用'aurioTouch'),然后将Sandy的示例代码添加到其中。也许这可以帮助您找出发生了什么事情。 - tomjpsun
7
如果添加 MPVolumeView *slide = [MPVolumeView new];#import <MediaPlayer/MediaPlayer.h>,就可以正常工作。 - Gal
@Sandy:flopes和Gal的评论是正确的。你应该将它们添加到你的答案中。 - onmyway133
6
“AVSystemController_SystemVolumeDidChangeNotification”是私有的吗?如果是,你的应用可能会被拒绝。 - LightningStryk
我在 addObserver 之前添加了 [MPVolumeView new];,这样我的通知就可以在 iOS 6 及以上版本中正常工作。 - Vicky
显示剩余2条评论

57

一些答案中使用的AudioSession API已于iOS 7停用。它被AVAudioSession替换,后者公开了一个outputVolume属性,用于系统范围的输出音量。可以使用KVO观察到此属性的更改通知,正如文档中指出的那样:

 

值范围为0.0到1.0,其中0.0表示最小音量,1.0表示最大音量。

 

系统范围的输出音量只能由用户直接设置;要在应用程序中提供音量控制,请使用MPVolumeView类。

 

您可以使用键值观察来观察此属性的更改。

要使此功能正常工作,您需要确保您的应用程序音频会话处于活动状态:

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)
    }
}

在被释放之前别忘了停止观察:

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

你们可别忘了,这个 KVO 是在你第一次启动视频时调用的。 - thibaut noah
无法在 Xcode 9 beta 5 中编译。请查看我刚刚发布的 Swift 3 版本。尝试编辑上面的代码,但被拒绝了。 - Joshua C. Lerner

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;
}

这样可以获取当前音量水平。1 是最大音量,0 是没有声音。注意:不需要显示任何 UI 元素即可正常工作。还要注意当前音量水平是相对于耳机或扬声器的(也就是说,两个音量级别是不同的),这将获取设备当前使用的音量级别。这并没有回答你关于接收音量变化通知的问题。


1
优美的代码,但它并不具备未来的可扩展性。如果苹果公司更改了某些内容,你的代码找不到UISlider时,应用程序将在尝试检索值时崩溃,因为UISlider尚未初始化。 - AlBeebe
将uislider初始化为UISlider *volumeViewSlider = nil....然后在获取值之前,检查volumeViewSlider是否为nil。如果是,则不要尝试获取值或释放它! - AlBeebe
这样的黑客行为是强烈反对的! - Mostafa Berg
你还能做什么? - Gui13
将其踩一下以帮助提升 Stuart 更为更新的答案,以便于未来读者查看。 - Warpling

4

你是否使用了AudioSessionSetActive启动音频会话?


添加 AudioSessionSetActive(true); 不会改变任何东西。苹果的示例中也没有这样做。 - MacLemon
在设置属性监听器并激活音频会话后,它对我起作用了。这是有道理的,因为另一个音频会话(通常是iPod)正在进行音量更改。 - NebulaFox
在调用float volume = [[AVAudioSession sharedInstance] outputVolume];之前,我设置了AudioSessionSetActive(YES);,这样就可以正常工作了。如果没有调用AudioSessionSetActive(YES),我会继续得到相同(不正确)的音量返回。 - Jonathon Horsman

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)
}

然后您调用

var avAudioSession = AVAudioSession.sharedInstance()
try? avAudioSession.setActive(true)
startObservingVolumeChanges()

2

在 Stuart 的答案基础上,使用 AVAudioSession 来适应 Swift 3 的一些变化。我希望代码能清楚地说明每个组件的位置。

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")
 }

2

以下是Stuart优秀答案的Swift 3版本:

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)
    }
}

2

Swift 5 / iOS 13

在我的测试中,我发现与系统音量交互的最可靠方法是使用 MPVolumeView 作为每个操作的中间人。您已经需要在您的视图层次结构中某处拥有此视图,以便使系统隐藏音量更改 HUD。

设置期间,通常在 viewDidLoad() 中,创建您的 MPVolumeView (如果您不想实际使用系统提供的控件,则可以将其设置为屏幕外):

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

获取设置音量:

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

观察音量变化(包括使用硬件按钮):

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

@objc func volumeDidChange() {
    // Handle volume change
}

1
非常有用,谢谢Robin。我尝试了几种旧版本的Swift方法,这是唯一一个对我有效的方法。对于那些不知道的人,您必须导入MediaPlayer才能创建MPVolumeView。 - Steven Love
很好的答案,只想再补充一点:不需要使用“-CGFloat.greatestFiniteMagnitude”,你可以将“x”设置为“0”,然后将“MPVolumeView”的“showsRouteButton”属性设置为“false”。 - spencer

1

我认为这取决于其他实现。例如,如果您使用滑块来控制声音的音量,您可以通过UIControlEventValueChanged执行检查操作,如果得到0值,则可以将按钮隐藏或禁用。

类似于以下内容:

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

在哪里,void checkZeroVolume 可以比较实际音量,因为它会在任何音量更改后触发。


由于我不需要或者想要控制音量,因此我没有实现 MPVolumeView,也就无法获取滑块的值。 - MacLemon
1
然而,如果性能不是问题,最简单的方法是像Vanya建议的那样,拥有一个隐藏的MPVolumeView并添加一个KVO到其“value”属性。 - AlcubierreDrive
对于未来的读者,我会下投票帮助提高 Stuart 更为更新的回答。 - Warpling

0

进入设置-声音,检查“按钮更改”选项。 如果关闭该选项,则按音量按钮时系统音量不会改变。 也许这就是你没有收到通知的原因。


我们如何确认用户已启用按钮?或者我们如何强制用户这样做? - Priyal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接