godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
90.61k stars 21.1k forks source link

iOS: Low audio volume when using Play and Record session category #88893

Closed LeonardoDemartino closed 7 months ago

LeonardoDemartino commented 8 months ago

Tested versions

System information

iOS 16

Issue description

If you are creating an app for iOS that needs both to record and playback audio, you'll need to set "Play and Record" as the audio session category on Project Settings->Audio->General. However, for some reason, iOS decides that (I assume that in order to avoid interference) the audio playback should only be routed to the upper speaker and with ultra low volume (as you were in a phone call or something)

Similar issues I have found over the internet: https://stackoverflow.com/questions/45155648/avaudiosessions-playandrecord-category-and-avaudiosessionmodemeasurement-are-in https://forums.developer.apple.com/forums/thread/721535 https://stackoverflow.com/questions/22676367/audio-session-using-measurement-mode-causes-low-volume-no-sound-in-ios-7-1

As a suggestion, I think that by adding here: https://github.com/godotengine/godot/blob/bb6b06c81343073f10cbbd2af515cf0dac1e6549/platform/ios/app_delegate.mm#L122

withOptions: [AVAudioSessionCategoryOptionDefaultToSpeaker] (https://developer.apple.com/documentation/avfaudio/avaudiosessioncategoryoptions/avaudiosessioncategoryoptiondefaulttospeaker)

Could fix the issue.

Also, if Godot sets by default to "AVAudioSessionModeMeasurement" (https://developer.apple.com/documentation/avfaudio/avaudiosessionmodemeasurement) another solution would be to simply: [audioSession setMode: AVAudioSessionModeDefault]

Steps to reproduce

1) Set up a Godot project with: Audio->Driver->Enable Input checked and Audio->General->Session Category->Play and Record (or use the attached MRP) 2) Make the project to play sound. 3) Run the app in an iOS device and hear how the volume is at ultra low levels. If you change the Session Category to, for example, Playback or Ambient, the sound plays at a correct volume but you can't obviously record anything from the microphone.

Minimal reproduction project (MRP)

mic_record.zip

akien-mga commented 8 months ago

CC @bruvzg

LeonardoDemartino commented 8 months ago

CC @georgwacker as well because he was the one who added Session Categories to iOS builds in the first place (thanks for adding this feature btw! 😃) https://github.com/godotengine/godot/pull/81196

bruvzg commented 8 months ago

If I understand correctly AVAudioSessionCategoryOptionDefaultToSpeaker will send audio to the main speaker instead of headset if it's connected, so it should not be the default (or only) option, but can be exposed to be user configurable.

Also, if Godot sets by default to "AVAudioSessionModeMeasurement"

I do not think mode is set, so it should be AVAudioSessionModeDefault.

LeonardoDemartino commented 8 months ago

@bruvzg Yes, from what I can see the mode is not set on Godot. Maybe it is by default on AVAudioSessionModeMeasurement somehow? Strange... And you are right about AVAudioSessionCategoryOptionDefaultToSpeaker not taking into account a future headset/airpods being connected.

I'm curious about how other apps on iOS handle this, for example WhatsApp switches to "Play And Record" / "Record" while recording a voice message? And switchs it back to "Playback" after sending it? This is interesting and I'm quite lost, because it seems that the only solution would be to have a function to be called in runtime to dinamically switch between session categories.

I would love to find a more general solution since this will over complicate how Godot mic input/playback should be handled on iOS.

By the way, on the app I'm working on we are super careful to not playing audio while the user is recording. This works fantastic on Android, but iOS is somehow trying fix an audio feedback loop that we don't actually have.

georgwacker commented 8 months ago

Looks like AVAudioSessionCategoryOptionDefaultToSpeaker only works when the category is set to PlayAndRecord.

The speaker option could be added along side the Mix with others options, but we would need to combine the options.

Currently, the variant of setCategory without the AVAudioSession.Mode is used, so the Mode should be default. If needed, we could use the other variant of that function and expose all the Mode options to the user.

Here are some infos about Responding to audio route changes.

LeonardoDemartino commented 8 months ago

So I think I have found something interesting. In one of our apps published on the App Store we both record and play sounds. It is not made with Godot, it is pure C++ and SDL.

The interesting part is that on initialization, SDL always sets the mode to AVAudioSessionCategoryOptionDefaultToSpeaker if Play And Record is selected. https://github.com/libsdl-org/SDL/blob/e03746b25f485fdf8637cbcb3126dc2c52bd32a7/src/audio/coreaudio/SDL_coreaudio.m#L422

And the app routes correctly the audio output if you plug a headset or use AirPods. And SDL don't have a routeChangeNotification observer.

I think this should be the way to go for Godot as well.

LeonardoDemartino commented 8 months ago

Another interesting thing: https://developer.apple.com/library/archive/qa/qa1754/_index.html "When using AVAudioSessionCategoryOptionDefaultToSpeaker, user gestures will be honored. For example, plugging in a headset will cause the route to change to headset mic/headphones and unplugging the headset will cause the route to change to built-in mic/speaker (as opposed to built-in mic/receiver) when this override has been set."

It says something completely contradictory to the latest official documentation of AVAudioSessionCategoryOptionDefaultToSpeaker but matches what's actually happening in the device.

This seems to be a miswording in the Apple documentation.

Setting AVAudioSessionCategoryOptionDefaultToSpeaker will fix this issue for Godot.