Player can hear themselves in online proximity chat game after 4.4 to 4.5 move

Godot Version

4.5

Question

Thanks for your help! This week, I tried to migrate my project from Godot 4.4 to 4.5, and I cannot get the microphone to work as it did previously. After the update, players always seem to hear their own voice playing when they didn’t in 4.4 and I just can’t work out why?

(By default, I am using the two_voip plugin that uses opus compression to send voice data to other players, with each player having a microphone and AudioStreamPlayer3D for proximity chat. I can also toggle on a version of this using the use_two_voip variable below, which switches to using in-built Godot nodes. The issue happens regardless of which method is used.)

I have tried:

  • disabling processing for all AudioStreamPlayers
  • setting the gain of all AudioStreamPlayers to -80dB
  • adjusting the in-game voice audio bus to 0 volume with the settings menu I have made (adjusts the volume of the dedicated ‘voice’ audio bus which other player’s voices are played on).

But the player always continues to hear their own voice, always at the same volume?

My player scene has a voice_manager scene which handles all voice audio-related nodes and functions, and looks like this:
VoiceManager : Node3D (script attached)
¬ AudioStreamMic : AudioStreamPlayer (empty stream object, AudioStreamMicrophone is setup during the VoiceManager’s ready())
¬ AudioStreamPlayer3D : AudioStreamPlayer3D (with a Generator - used if not using two-voip)
¬ AudioStreamPlayer3D_oc : AudioStreamPlayer3D (with an OpusChunked - used if using two-voip)

The played voices output to a ‘Voice’ audio bus, which routes to Master.

The recorded voice outputs to either the ‘Voice_Capture’ (with Capture effect) or ‘Voice_Capture_oc’ bus (with OpusChunked effect), depending on which method is selected. This routes to a ‘Mute’ bus, which is muted and set to -80dB and routes to Master (so there shouldn’t be any way for a player to hear their own voice).

Here is a snapshot of the important bits of the code inside the VoiceManager’s script:

In a nutshell:

  • In _ready, decides whether the Player parent is the multiplayer authority. If it is, then it sets up the microphone and disables the AudioStreamPlayer. Otherwise (it’s another peer’s Player), it sets up the AudioStreamPlayer (one of the two using the chosen method) and disables the microphone.
  • Each _process, decides whether or not to process the mic (depends on user settings, could have muted (option 1) or pressed push-to-talk (option 2)) and always processes the speakers.
  • There is a pair of disable/enable mic functions that can be called, I only use these when the player wants to perform a mic check in the settings menu while in-game.
  • process_mic checks whether the mic should be operating, then calls the appropriate lines of code to capture the player’s audio from their mic depending on the capture method. It then calls the appropriate send_voice_data rpc to send the voice data to all peers.
  • process_speaker checks whether the speaker should be playing, then calls the appropriate lines of code depending on the capture method to play the audio from the appropriate buffer.
  • Lastly, there are a few rpcs for receiving voice data and saving it to the appropriate buffer.
extends Node3D

class_name VoiceManager

# Source 1: passthecodeine - https://www.reddit.com/r/godot/comments/186yn4o/voip_in_godot_basic_overview_not_full_tutorial/
# Source 2: FinePointCGI - https://www.youtube.com/watch?v=AomgXrpiRmM&t=1511s

var voice_capture_bus_index : int		# The index of the 'Voice_Capture' audio bus
var capture_effect = AudioEffectCapture	# The index of the 'Capture' effect in 'Voice_Capture' AB.
var player_voice_recording : PackedVector2Array	# An uncompressed array of left/right mic data.
var receive_buffer : PackedFloat32Array # Received compressed voice data, only for compressed data.
var audio_buffer_limit := 512			# Buffer size limit, will toggle live.
var audio_stream_playback : AudioStreamGeneratorPlayback	# Generator to take numbers and play.
var max_amplitude := 0.0		# record the loudest sound from the player each buffer read.
var input_threshold := 0.005	# voice must be greater than this dB to be considered 'speaking'
var use_floats := true	# set true to compress data using FinePointCGI's method.
var disable_mic := false#:	# set to true to disable the player's microphone.
	#set(value):
		#print("setting disable_mic to " + str(value))
		#disable_mic = value

@export var player : Player
@export var mic : AudioStreamPlayer
@export var speaker : AudioStreamPlayer3D	# must be local to scene!

# two voip variables. 
var use_two_voip := true	# Remember to switch the export node if toggling this!
var opuschunked : AudioEffectOpusChunked
var audiostreamopuschunked : AudioStreamOpusChunked
var opuspacketsbuffer = []

var debug := true

func _ready() -> void:
	# setup voice
	if multiplayer.multiplayer_peer == null:
		print("Warning: failed to setup player mic in voice_manager: no multiplayer_peer.")
		return
	if (player.is_multiplayer_authority()):	# only initialise a mic for the authority player.
		if debug:
			print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) + 
					" - setting up microphone for player " + str(player.owner_id))
		mic.stream = AudioStreamMicrophone.new()	# make new, not a shared resource.
		#audio_stream_mic.play()
		if not use_two_voip:
			mic.bus = "Voice_Capture"
			voice_capture_bus_index = AudioServer.get_bus_index(mic.bus)
			capture_effect = AudioServer.get_bus_effect(voice_capture_bus_index, 0) # capture effect index
		else:
			mic.bus = "Voice_Capture_oc"
			voice_capture_bus_index = AudioServer.get_bus_index(mic.bus)
			opuschunked = AudioServer.get_bus_effect(voice_capture_bus_index, 0) # capture effect index
			if debug:
				print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) 
						+ " - setup opuschunked: " + str(is_instance_valid(opuschunked)))
	
	else:	# not the authority player, another peer's player. Initialise the speaker/stream_player. 
		if debug:
			print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) + 
					" - setting up speaker and stream for player " + str(player.owner_id))
		if not use_two_voip:
			speaker.play()	# start playing the player3d
			audio_stream_playback = speaker.get_stream_playback()
		else:	# using two_voip
			speaker.play()
			audiostreamopuschunked = speaker.stream
			if debug:
				print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) + 
						" - setup  audiostreamopuschunked: " + 
						str(is_instance_valid(audiostreamopuschunked)))


func _process(_delta: float) -> void:
	# handle player's voice
	#print("playing: " + str(audio_stream_mic.playing))
	if multiplayer.multiplayer_peer == null:
		return
	
	# if voice activity or (push to talk and talk pressed): proces mic.
	if KeyPersistence.audio_option == 1 or \
			(KeyPersistence.audio_option == 2 and player.push_to_talk_pressed):
		process_mic()
	
	process_speaker()


func disable_mic_function() -> void:
	disable_mic = true
	if multiplayer.multiplayer_peer == null:
		print("Warning: failed to disable player mic in voice_manager: no multiplayer_peer.")
		return
	if player.is_multiplayer_authority():
		mic.call_deferred("stop")
		if debug:
			print("Voice_Manager - mic disabled")


func enable_mic_function() -> void:
	disable_mic = false
	if multiplayer.multiplayer_peer == null:
		print("Voice_Manager - Warning: failed to enable player mic in voice_manager: " + 
				"no multiplayer_peer.")
		return
	if player.is_multiplayer_authority():
		mic.call_deferred("play")
		if debug:
			print("Voice_Manager - mic enabled")


func process_mic() -> void:
	# All code to read the player's microphone and transmit it is handled here.
	
	# current peer's player, should play the mic to record their voice.
	if multiplayer.multiplayer_peer == null:
		print("Warning: failed to process player mic in voice_manager: no multiplayer_peer.")
		return
	if is_instance_valid(mic):
		if player.is_multiplayer_authority() and (not mic.playing) and (not disable_mic):
			mic.call_deferred("play") # seems to not work if auto played or not call_deferred?
			if debug:
				print(str(multiplayer.get_unique_id()) + ": recording player " + player.name + "'s mic.")
			return
		# other peer's player, can disable the mic.
		elif ((not player.is_multiplayer_authority()) or disable_mic) and \
				mic.playing:
			mic.call_deferred("stop")
			return
	if not player.is_multiplayer_authority():	# do nothing is this is another peer's player.
		return
	
	# past this point, the microphone should be playing and the player is the authority:
	
	if use_two_voip:
		var prepend = PackedByteArray()
		if is_instance_valid(opuschunked):
			while opuschunked.chunk_available():
				var opusdata : PackedByteArray = opuschunked.read_opus_packet(prepend)
				opuschunked.drop_chunk()
				send_voice_data_pba.rpc(opusdata, player.owner_id)
	
	else:
		# if there are frames avaliable, get the full audio buffer.
		audio_buffer_limit = capture_effect.get_frames_available()
		if audio_buffer_limit > 0 and capture_effect.can_get_buffer(audio_buffer_limit):
			player_voice_recording = capture_effect.get_buffer(audio_buffer_limit)
			
			if use_floats:	# compress the data into a PackedFloat32Array.
				var player_voice_recording_c = PackedFloat32Array()
				player_voice_recording_c.resize(player_voice_recording.size())
				max_amplitude = 0.0
				
				# average the left/right mic values and save as a float.
				for i in range(player_voice_recording.size()):
					var value = (player_voice_recording[i].x + player_voice_recording[i].y) / 2
					max_amplitude = max(max_amplitude, value)
					player_voice_recording_c[i] = value
				
				# if the player spoke, send the data.
				if max_amplitude > input_threshold:
					send_voice_data_c.rpc(player_voice_recording_c, player.owner_id)
			
			else:	# leave the data as a PackedVector2Array and send
				send_voice_data.rpc(player_voice_recording, player.owner_id)
		
		capture_effect.clear_buffer()


func process_speaker() -> void:
	# All code to play any received (compressed) audio data is handled here. 
	
	# decide whether to continue with function and set correct speaker setting.
	if multiplayer.multiplayer_peer == null:
		print("Voice_Manager - Warning: failed to process player speaker in voice_manager: " +
				"no multiplayer_peer.")
		return
	# another player's peer, should play their recorded voice.
	if (not player.is_multiplayer_authority()) and (not speaker.playing):
		speaker.call_deferred("play")
		if debug:
			print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) + 
					": playing player " + player.name + "'s voice.")
		return
	# current peer's player, should not replay the player's own voice back to them.
	elif player.is_multiplayer_authority() and speaker.playing:	
		speaker.call_deferred("stop")
		return
	if player.is_multiplayer_authority():	# do nothing as this is the current peer's player.
		return
	
	# past this point, should be a non-authority player with their voice player playing.
	if player.owner_id in Lobby.players:
		speaker.volume_linear = Lobby.players[player.owner_id]["volume"]
	
	if use_two_voip:
		while audiostreamopuschunked.chunk_space_available() and opuspacketsbuffer.size() > 0:
			
			var packet = opuspacketsbuffer.pop_front()
			#print(str(multiplayer.get_unique_id()) + " - got packet " + 
					#str(packet) + " of type " + type_string(typeof(packet)))
			if packet != null:
				audiostreamopuschunked.push_opus_packet(packet, 0, 0)
				
			else:
				print("Voice_Manager - id: " + str(multiplayer.get_unique_id()) + 
						" - failed to pop packet " + str(packet) + 
						" for player " + str(player.owner_id))
				break;
			
			# discard uneeded packets - buffer seems to build up in size otherwise?
			while opuspacketsbuffer.size() > 1:
				opuspacketsbuffer.pop_back()
			
			# Get the magnitude for the multiplayer_player_list_item volume bar:
			var last_chunk = audiostreamopuschunked.read_last_chunk().get(0)	# Vector2
			if player.owner_id in Lobby.players:
				Lobby.players[player.owner_id]['current_voice'] = \
						(last_chunk[0] + (last_chunk[1]) / 2) * 10.0
		
			
	elif use_floats:	# if compressing the data, play what is in the buffer. 
		if receive_buffer.size() > 0:
			if is_instance_valid(audio_stream_playback):
				for i in range(min(audio_stream_playback.get_frames_available(), 
						receive_buffer.size())):
					# rebuild the Vector2 array for voice from the float array.
					audio_stream_playback.push_frame(Vector2(receive_buffer[0], receive_buffer[0]))
					#print(recieve_buffer.get(0))
					Lobby.players[player.owner_id]['current_voice'] = receive_buffer.get(0) * \
							10.0 #* Lobby.players[player.owner_id]['volume']
					receive_buffer.remove_at(0)


@rpc("any_peer", "call_remote", "unreliable_ordered", 1)	# call on all peers except self.
#@rpc("any_peer", "call_local", "reliable", 1)	# for debugging
func send_voice_data(data : PackedVector2Array, player_id : int):
# receive the uncompressed data and play it straight away. 
	if player.owner_id == player_id:
		if is_instance_valid(audio_stream_playback):
			for i in range(0, data.size()):
				audio_stream_playback.push_frame(data[i])
				Lobby.players[player.owner_id]['current_voice'] = data[i] * 10.0


@rpc("any_peer", "call_remote", "unreliable_ordered", 1)	# call on all peers except self.
# add the data to an audio buffer, which is played in _process()
#@rpc("any_peer", "call_local", "reliable", 1)	# for debugging
func send_voice_data_c(data : PackedFloat32Array, player_id : int):
	# receive the compressed data and send it to the buffer to be played in process_voice().
	if player.owner_id == player_id:
		receive_buffer.append_array(data)


@rpc("any_peer", "call_remote", "unreliable_ordered", 1)	# call on all peers except self.
# add the data to an opus packets buffer, which is played in _process()
#@rpc("any_peer", "call_local", "reliable", 1)	# for debugging
func send_voice_data_pba(data : PackedByteArray, player_id : int):
	# receive the compressed data and send it to the buffer to be played in process_voice().
	if player.owner_id == player_id:
		opuspacketsbuffer.append(data)
		

I don’t know exactly how the plugin works but I’ll try to help.

Does the player instance instantiate an audio emitter with the mic input? The audio must come from somwhere.

Is the audio received from the other clients sent through the same channel? Maybe the voice inputs are getting blended together into one. Can you separate the channels to debug them?

Hi Lasbelin,

Yes the voice_manager’s _ready() function sets this up, and it’s a new microphone for each player so that I don’t have the shared resource problem:

mic.stream = AudioStreamMicrophone.new()
mic.bus = "Voice_Capture_oc"
voice_capture_bus_index = AudioServer.get_bus_index(mic.bus)

There is a pause menu (not shown in the code above) where the player can get the microphone device and set the audio server’s input device using a menu:

var speaker_list : Array = AudioServer.get_output_device_list()
#...
AudioServer.set_input_device(audio_input_option_button.get_item_text(index))

The strange thing is, even if a player starts a lobby and no other players join, they can still hear themselves speaking. Even if they mute their mic and mute all audio channels, same again? It’s like the audio from the microphone is getting played into the speakers before even being processed by the audio bus.

This happens whether you use the plugin or not, and it didn’t happen in 4.4?

So I have fixed the game which is the main thing, but I’m unsure what the issue was in the end.

I didn’t mention this in the thread as I thought this was unrelated - but in the player’s pause menu there is a drop-down to select an audio device, and a ‘test microphone’ button that replays the player’s own audio to them with a delay when toggled.

Both of these nodes had an AudioStreamPlayer with a microphone - the latter for obvious reasons and the former because I was unable to setup the audio input device without a microphone playing somewhere (the settings menu within the pause menu is also instantiated in the main menu scene, so it’s not always part of a player and hence I couldn’t use the player’s mic for this). The pause menu uses the enable/disable_microphone_functions from above to stop and resume the player’s mic whenever these functions need to run.

For some reason, one of these microphones would get enabled when the player enters the tree, which was not happening previously in 4.4. I am still unsure why this happens.

To fix this, I first tried clearing the microphones and when they are required, performing an

audio_input_stream_player.stream = AudioStreamMicrophone.new()

and then erasing it when its task was done. Although this stopped the player from hearing themselves, it also stopped the audio from the player all together - as in the player’s voice was now not detected at all and no one could hear each other, despite the player’s AudioStreamPlayer microphone being setup and playing.

I tried a few things to fix this to no avail, but I noticed that toggling the ‘voice test’ button from the pause menu fixed the issue (this would turns off the player’s mic and turns on the pause menu’s mic, then on the second press turns off the pause menu’s mic and turns on the player mic again). In the end, I had to toggle the player’s microphone on and off 2 seconds after spawning in, which fixed the issue. I still have no idea why?