通过上(电话)扬声器播放音频

时间:2013-08-02 21:44:40

标签: ios media-player avfoundation audiotoolbox

我正试图在我的应用程序中通过iPhone上的上部扬声器播放音频,这是您在通话期间按下的那个。我知道这是可能的,因为我在App Store中播放了一个游戏(通过“点按水龙头”的“The Heist”)来模拟电话,并且确实如此。

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

我找到了一篇似乎与此有关的帖子:link。它甚至提供了一堆代码,所以我认为我在家是免费的,但现在我似乎无法让代码工作。为简单起见,我只是将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音乐播放器播放音频(在viewDidLoad上面的AVAudioSession行之后初始化,以获得它的价值):

_musicPlayer = [MPMusicPlayerController iPodMusicPlayer];

并通过MPMediaPickerController

选择该iPod音乐播放器的媒体
- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) mediaItemCollection {
    if (mediaItemCollection) {
        [_musicPlayer setQueueWithItemCollection: mediaItemCollection];
        [_musicPlayer play];
    }

    [self dismissViewControllerAnimated:YES completion:nil];
}

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

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

4 个答案:

答案 0 :(得分:27)

此刻也在努力解决这个问题。也许这会对以后的人有所帮助。你也可以使用更新的覆盖端口的方法。实际代码中的许多方法实际上都已弃用。

因此,如果您通过获取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];
}

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

答案 1 :(得分:4)

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

使用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;

请参阅apple的文档here

但真正的谜团是你在接收器路由时遇到任何麻烦的原因。这是playAndRecord类别的默认行为。 Apple的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的博客)From iPod Library to PCM Samples in Far Fewer Steps Than Were Previously Necessary - 它是From iphone media library to pcm samples in dozens of confounding and potentially lossy steps的续集 - 这应该让你了解你将面临的复杂性。自iOS6以来,可能变得更容易,但我不太确定!


1 ios6中有一个otherAudioPlaying只读BOOL属性,但就是它

答案 2 :(得分:1)

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

答案 3 :(得分:0)

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