7

I have an app taps the microphone and also play sounds depending on mic input(don't have to be simultaneously tho) This code below works. But one problem is the output plays on the small top speaker and not the bottom real loud speakers. I could solve this problem strangely by putting the 3 lines below just before the player starts, Then I can hear the sound on speakers. But then the microphone stops listening! Even after the player stops playing. Basically mic does not like when it is

.defaultToSpeaker

Any idea?

Here also documented what I am trying to do is correct:

https://developer.apple.com/documentation/avfoundation/avaudiosession/categoryoptions/1616462-defaulttospeaker

UPDATE: I minimized the problem. No Player just mic. Code below, mic does not "work" when it is ".defaultToSpeaker". After some debugging I realized that defaultToSpeaker switches the mic from "bottom" to "front". And

 try preferredPort.setPreferredDataSource(source)

Cant seem to change it to bottom again. (I can provide code for this) And when category is defaultToSpeaker apperantly the tap buffer framelength is 4800 and not 4410. This difference seem causes trouble in my code because I need exactly 44100. So mic is actually working, but later in code it fails to do its job due to different SR. Below code can explain more.

 func tapMicrophone() {
    try? AVAudioSession.sharedInstance().setActive(false)
    try? AVAudioSession.sharedInstance().setCategory(.playAndRecord,  options: [.defaultToSpeaker])
    //setBottomMic()
    try? AVAudioSession.sharedInstance().setActive(true)

    //tracker.start()
    let input = engine.inputNode
    let inputFormat = input.outputFormat(forBus: 0)
    let sampleRate = Double(11025)
    let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: 1, interleaved: true)!
    let converter = AVAudioConverter(from: inputFormat, to: outputFormat)!
    let inputBufferSize = 44100 //  100ms of 44.1K = 4410 samples.
    let sampleRateRatio = 44100 / sampleRate

    input.installTap(onBus: 0, bufferSize: AVAudioFrameCount(inputBufferSize), format: inputFormat) {
        buffer, time in
        var error: NSError? = nil
        let capacity = Int(Double(buffer.frameCapacity) / sampleRateRatio)
        let bufferPCM16 = AVAudioPCMBuffer(pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(capacity))!
        converter.convert(to: bufferPCM16, error: &error) { inNumPackets, outStatus in
            outStatus.pointee = AVAudioConverterInputStatus.haveData
            return buffer
        }
    }

    engine.prepare()
    try! engine.start()

}

In this case I seem to have 2 options. Either solve problem on mic level, if possible make this code work with ".defaultToSpeaker". Or don't use category .playandrecord But switch between .playback and .record when mic is not needed. This didn't seem to be easy too, since it requires a lot of starting/stopping of all audio, which is necessary for activate and deactive AVAudioSession. But if this is the way to go I can provide more code.

Spring
  • 11,333
  • 29
  • 116
  • 185
  • @matt I just printed the preferred input and I see "selectedDataSource = Bottom" This was not a concious decision though. I just need to be able to listen to environmental sounds. Does this effect my problem? – Spring May 01 '19 at 21:10
  • andonly position available here is back or front. I dont see bottom? – Spring May 01 '19 at 21:33
  • I actually do not need the microphone while the sound is playing. But I am having a lot of difficulty changing back and forth AVAudioSession category, because I have to deactivate and activate the session again when I change the route. Which is slow and does not work half of the time. So i was trying find a setup that I set once in beginning and use forever. If you have a good suggestion of enabling disabling microphone smoothly during playback. That also works for me – Spring May 01 '19 at 21:39
  • @matt I just set it to use the front mic (it was bottom) I started the app and mic still does not work when I set to ".defaultToSpeaker" – Spring May 01 '19 at 21:51
  • @matt because I see that "(AVAudioSession.sharedInstance().preferredInput)" is nil. So I couldnt set it properly.. – Spring May 01 '19 at 22:00
  • Can you try avaudiosession APIs in this order 1) .setCategory with category:PlaynRecord, mode=voicechat, options:defaulttospeaker 2) .overrideOutputAudioPort with port=speaker 3) .setActive with true . Then call your input.installTap and let me know if it works – manishg May 03 '19 at 03:10
  • @manishg thx. I tried your suggestion. Mic don't work at all. Same problem. – Spring May 03 '19 at 10:20
  • I'm a bit rusty, are you sure you need to deactivate/activate the audio session when you change categories? – Rhythmic Fistman May 03 '19 at 14:45
  • @Rhythmic Fistman When I dont use activate/deactive. It seems to switch faster. But has no effect on the problem – Spring May 03 '19 at 15:13
  • Try this https://gist.github.com/manishganvir/bd4d5a8ebb0eef0fed9612a2c9e74745 to change the data source to bottom mic – manishg May 03 '19 at 17:28
  • @manishg Tnx. I tried. I confirm that mic has changed to bottom. But odd enough longBuffer.frameLength is still always 48000! thus same problem as I described in post. Do you have an explanation for 48000? – Spring May 03 '19 at 21:00
  • So it your original problem gone(mic not working when speaker is selected) ? It’s only the sample rate issue now is it? – manishg May 03 '19 at 21:05
  • @manishg this was the original problem. I either cant listen on bottom speakers Or when I can mic dont work properly. – Spring May 03 '19 at 21:08
  • So what’s the behavior now? Still same issue? – manishg May 03 '19 at 21:11
  • behaviour is, when it is ".defaultToSpeaker" my method runInference() receives frame length of 48000. So inference never runs. – Spring May 03 '19 at 21:13
  • I see difference comes from "buffer.frameCapacity" in the tap. This changes depends on category – Spring May 03 '19 at 21:26

1 Answers1

9

Seems like I found a solution. Which is actually very simple. When AVAudioSession category is .defaultToSpeaker (Or overrideOutputAudioPort) apparently the tap input buffer framelength changes to 4800 from 4410.

This strangely happens regardless of which microphone is used. So using

AVAudioSession.sharedInstance().setInputDataSource(datasource);

Does not help.

This difference seems to cause problems later in my code. So mic was actually working, but later in code it was failing to do its job due to a different framelength.

Solution/workaround was I basically hardcoded the framelength in the tap. Since I use a converter I don't expect this to be a problem. This means I can set ".defaultToSpeaker" And mic is still working as expected.

capacity = 4410 (DUH!)

Maybe there are different/better ways to approach this problem. So feel free to add your answer if so.

Spring
  • 11,333
  • 29
  • 116
  • 185