9

我正在尝试让我的应用程序中的音频通过 iPhone 上的上扬声器播放,这是您在通话期间按在耳朵上的那个。我知道这是可能的,因为我玩过 App Store 上的一款游戏(“tap tap tap”中的“The Heist”),它模拟了电话通话并做到了这一点。

我在网上做了很多研究,但我很难找到任何甚至讨论过这种可能性的人。绝大多数帖子似乎是关于免提扬声器与插入式耳机(如thisthisthis),而不是上部“电话”扬声器与免提扬声器。(部分问题可能是它没有一个好名字:“电话扬声器”通常是指设备底部的免提扬声器等,因此很难进行有针对性的搜索)。我研究过苹果的Audio Session Category Route Overrides,但那些似乎(如果我错了,请纠正我)只处理底部的免提扬声器,而不是手机顶部的扬声器。

我发现了一篇似乎与此有关的帖子:链接。它甚至提供了一堆代码,所以我以为我在家自由,但现在我似乎无法让代码工作。为简单起见,我只是将DisableSpeakerPhone方法(如果我理解正确,应该是将音频重新路由到上扬声器的方法)复制到我的方法viewDidLoad中,看看它是否可行,但第一条“断言”行失败,音频继续发挥底部。(我还按照评论中的建议导入了 AudioToolbox 框架,所以这不是问题。)

这是我正在使用的主要代码块(这是我复制到我viewDidLoad的测试中的内容),尽管我链接到的文章中还有一些方法:

void DisableSpeakerPhone () {
    UInt32 dataSize = sizeof(CFStringRef);
    CFStringRef currentRoute = NULL;
    OSStatus result = noErr;

    AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &dataSize, &currentRoute);

    // Set the category to use the speakers and microphone.
    UInt32 sessionCategory = kAudioSessionCategory_PlayAndRecord;
    result = AudioSessionSetProperty (
                                      kAudioSessionProperty_AudioCategory,
                                      sizeof (sessionCategory),
                                      &sessionCategory
                                      );
    assert(result == kAudioSessionNoError);

    Float64 sampleRate = 44100.0;
    dataSize = sizeof(sampleRate);
    result = AudioSessionSetProperty (
                                      kAudioSessionProperty_PreferredHardwareSampleRate,
                                      dataSize,
                                      &sampleRate
                                      );
    assert(result == kAudioSessionNoError);

    // Default to speakerphone if a headset isn't plugged in.
    // Overriding the output audio route

    UInt32 audioRouteOverride = kAudioSessionOverrideAudioRoute_None; 
    dataSize = sizeof(audioRouteOverride);
    AudioSessionSetProperty(
                            kAudioSessionProperty_OverrideAudioRoute,
                            dataSize,
                            &audioRouteOverride);

    assert(result == kAudioSessionNoError);

    AudioSessionSetActive(YES);
} 

所以我的问题是:任何人都可以A)帮助我弄清楚为什么该代码不起作用,或者B)提供更好的建议,以便能够按下按钮并将音频路由到上部扬声器?

PS 我越来越熟悉 iOS 编程,但这是我第一次涉足 AudioSessions 等领域,因此非常感谢详细信息和代码示例!谢谢您的帮助!

更新:

根据“他是”(下)的建议,我删除了上面引用的代码并将其替换为:

[[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayAndRecord error:nil];
[[AVAudioSession sharedInstance] setActive: YES error:nil];

开始时viewDidLoad。但是,它仍然无法正常工作(我的意思是音频仍然来自手机底部的扬声器,而不是顶部的接收器)。显然,默认行为应该是AVAudioSessionCategoryPlayAndRecord自己从接收器发送音频,所以还是有问题。

更具体地说,我正在使用此代码执行的操作是通过 iPod 音乐播放器播放音频(在上面的 AVAudioSession 行之后立即初始化viewDidLoad,这是值得的):

_musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

并且该 iPod 音乐播放器的媒体是通过以下方式选择的MPMediaPickerController

- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) mediaItemCollection {
    if (mediaItemCollection) {
        [_musicPlayer setQueueWithItemCollection: mediaItemCollection];
        [_musicPlayer play];
    }

    [self dismissViewControllerAnimated:YES completion:nil];
}

这一切对我来说似乎相当简单,我没有任何错误或警告,而且我知道媒体选择器和音乐播放器工作正常,因为正确的歌曲开始播放,只是来自错误的扬声器。是否有“使用此 AudioSession 播放媒体”方法之类的?或者有没有办法检查当前处于活动状态的音频会话类别,以确认没有任何东西可以将其切换回来或其他什么?有没有办法强调告诉代码使用接收器,而不是依赖默认值来这样做?我觉得我在一码线上,我只需要越过最后一点......

编辑:我只是想到了一个理论,其中关于 iPod 音乐播放器不想从接收器中播放。我的推理:可以设置一首歌曲通过官方 iPod 应用程序开始播放,然后通过我正在开发的应用程序无缝调整(暂停、跳过等)。从一个应用程序到下一个应用程序的连续播放让我觉得也许 iPod 音乐播放器有自己的音频路由设置,或者它不会停止检查新应用程序中的设置?有谁知道他们在说什么认为它可能是这样的吗?

4

4 回答 4

28

也为此苦苦挣扎了一段时间。也许这会在以后对某人有所帮助。您还可以使用更新的覆盖端口的方法。您的示例代码中的许多方法实际上已被弃用。

因此,如果您通过获取您的 AudioSession sharedInstance,

NSError *error = nil;
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[session setActive: YES error:nil];

会话类别必须是 AVAudioSessionCategoryPlayAndRecord 您可以通过检查此值来获取当前输出。

AVAudioSessionPortDescription *routePort = session.currentRoute.outputs.firstObject;
NSString *portType = routePort.portType;

现在根据您要将其发送到的端口,只需使用切换输出

if ([portType isEqualToString:@"Receiver"]) {
       [session  overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
} else {
       [session  overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error];
}

这应该是一种将输出切换到扬声器电话和接收器的快速方法。

于 2014-06-25T20:45:26.577 回答
5

您必须首先初始化您的音频会话。

使用 C API

  AudioSessionInitialize (NULL, NULL, NULL, NULL);

在 iOS6 中,您可以改用 AVAudioSession 方法(您需要导入 AVFoundation 框架才能使用AVAudioSession):

使用 AVAudioSession 初始化

 self.audioSession = [AVAudioSession sharedInstance];

使用 AVAudioSession 设置 audioSession 类别

 [self.audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
                                       error:nil];

为了进一步研究,如果您想要更好的搜索词,以下是扬声器常量的全名:

const CFStringRef kAudioSessionOutputRoute_BuiltInReceiver;
const CFStringRef kAudioSessionOutputRoute_BuiltInSpeaker;

在这里查看苹果的文档

但真正的谜团是为什么您在路由到接收器时遇到任何问题。这是 playAndRecord 类别的默认行为。苹果的文档kAudioSessionOverrideAudioRoute_None

“为 kAudioSessionCategory_PlayAndRecord 类别指定输出音频应发送到接收器。这是该类别的默认输出音频路由。

更新

在您更新的问题中,您表明您正在使用该MPMusicPlayerController课程。此类调用全局音乐播放器(与音乐应用程序中使用的播放器相同)。此音乐播放器与您的应用程序是分开的,因此不会与您的应用程序的 audioSession 共享相同的音频会话。您在应用程序的 audioSession 上设置的任何属性都将被 MPMusicPlayerController 忽略。

如果您想控制应用程序的音频行为,您需要使用应用程序内部的音频框架。这将是AVAudioRecorder/AVAudioPlayer或核心音频(音频队列、音频单元或 OpenAL)。无论您使用哪种方法,都可以通过AVAudioSession属性或 Core Audio API 控制音频会话。Core Audio 为您提供了更细粒度的控制,但随着 iOS 的每个新版本,更多的内容被移植到 AVFoundation,所以从它开始吧。

还要记住,音频会话为您提供了一种描述应用程序音频相对于整个 iOS 环境的预期行为的方法,但它不会让您完全控制。Apple 会注意确保用户对其设备音频行为的期望在不同应用之间以及当一个应用需要中断另一个应用的音频流时保持一致。

更新 2

在您的编辑中,您提到了音频会话检查其他应用程序的音频会话设置的可能性。这不会发生1。这个想法是,每个应用程序都使用其自包含的音频会话为其自己的音频行为设置偏好。当多个应用程序竞争不可共享的资源(例如内部麦克风或其中一个扬声器)时,操作系统会在相互冲突的音频要求之间进行仲裁,并且通常会决定支持最有可能满足用户期望的行为。设备作为一个整体。

MPMusicPlayerController 类有点不寻常,因为它使您能够让一个应用程序在一定程度上控制另一个应用程序。在这种情况下,您的应用程序不会播放音频,而是向音乐播放器发送请求以代表您播放音频。您的控制受到 MPMusicPlayerController API 范围的限制。为了获得更多控制,您的应用必须提供自己的音频播放实现。

在您的评论中,您想知道:

有没有办法从 MPMusicPlayerController 中提取 MPMediaItem,然后通过特定于应用程序的音频会话或类似的方式播放它们?

这是一个新问题的(大)主题。这是一个很好的开始阅读(来自 Chris Adamson 的博客)从 iPod 库到 PCM 样本的步骤比以前需要的要少得多——它是从 iphone 媒体库到 pcm 样本的几十个令人困惑和可能有损的步骤的续集——应该给你对你将面临的复杂性有一种感觉。自 iOS6 以来,这可能会变得更容易,但我不太确定!


1 ios6中有一个otherAudioPlaying只读的BOOL属性,仅此而已

于 2013-08-02T23:54:40.453 回答
1

斯威夫特 3.0 代码

func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {       
let routePort: AVAudioSessionPortDescription? = obsession. current Route. outputs. first
let portType: String? = routePort?.portType
if (portType == "Receiver") {
    try? audioSession.overrideOutputAudioPort(.speaker)
   }
   else {
        try? audioSession.overrideOutputAudioPort(.none)
   }
于 2017-04-19T10:22:35.010 回答
0

迅捷5.0

func activateProximitySensor(isOn: Bool) {
    let device = UIDevice.current
    device.isProximityMonitoringEnabled = isOn
    if isOn {
        NotificationCenter.default.addObserver(self, selector: #selector(proximityStateDidChange), name: UIDevice.proximityStateDidChangeNotification, object: device)
        let session = AVAudioSession.sharedInstance()
        do{
            try session.setCategory(.playAndRecord)
            try session.setActive(true)
            try session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
        } catch {
            print ("\(#file) - \(#function) error: \(error.localizedDescription)")
        }
    } else {
        NotificationCenter.default.removeObserver(self, name: UIDevice.proximityStateDidChangeNotification, object: device)
    }
}

@objc func proximityStateDidChange(notification: NSNotification) {
    if let device = notification.object as? UIDevice {
        print(device)
        let session = AVAudioSession.sharedInstance()
        do{
            let routePort: AVAudioSessionPortDescription? = session.currentRoute.outputs.first
            let portType = routePort?.portType
            if let type = portType, type.rawValue == "Receiver" {
                try session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
            } else {
                try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none)
            }
        } catch {
            print ("\(#file) - \(#function) error: \(error.localizedDescription)")
        }
    }
}
于 2019-09-03T07:08:25.400 回答