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)