extends Node var current_sample_rate: int = 48000 var has_loopback: bool = false var max_buffer_size: int = 1000 var voicechat_player_path : NodePath = ^"VoiceChat" var sample_rate_min : int = 11025 var sample_rate_max : int = 48000 var local_playback: AudioStreamGeneratorPlayback = null var local_voice_buffer: PackedByteArray = PackedByteArray() var network_playback: Dictionary[int, AudioStreamGeneratorPlayback] = {} var network_voice_buffer: Dictionary[int, PackedByteArray] = {} @onready var loopback_player : AudioStreamPlayer= $/root/Main/VoiceLoopback func _ready() -> void: if not Steamworks.is_initiallized: push_warning("[VoiceChat] VoiceChat disabled due to SteamAPI.") return if has_loopback: print("[VoiceChat] Loopback on") record_voice(true) loopback_player.stream.mix_rate = current_sample_rate loopback_player.play() local_playback = loopback_player.get_stream_playback() multiplayer.peer_disconnected.connect(remove_disconnected_peer) Multiplayer.player_ready.connect(add_joined_peer) func _process(_delta: float) -> void: if not Steamworks.is_initiallized: return check_for_voice_local() func add_joined_peer(id): # Fix calling this on yourself var player : Node3D = Multiplayer.playerDict.get(id) var remote_audio_stream_playback = player.get_node_or_null(voicechat_player_path).get_stream_playback() network_playback.set(id, remote_audio_stream_playback) network_voice_buffer.set(id,PackedByteArray()) current_sample_rate = Steam.getVoiceOptimalSampleRate() send_sample_rate.rpc_id(id, current_sample_rate) func remove_disconnected_peer(id): network_playback.erase(id) network_voice_buffer.erase(id) func check_for_voice_local() -> void: var available_voice: Dictionary = Steam.getAvailableVoice() if !available_voice: return # Seems there is voice data if available_voice['result'] == Steam.VOICE_RESULT_OK and available_voice['buffer'] > 0: # Valve's getVoice uses 1024 but GodotSteam's is set at 8192? # Our sizes might be way off; internal GodotSteam notes that Valve suggests 8kb # However, this is not mentioned in the header nor the SpaceWar example but -is- in Valve's docs which are usually wrong var voice_data: Dictionary = Steam.getVoice() if voice_data['result'] == Steam.VOICE_RESULT_OK and voice_data['written']: #print("[VoiceChat] Voice message has data: %s / %s" % [voice_data['result'], voice_data['written']]) # Here we can pass this voice data off on the network #TODO: Only execute if multiplayer connected get_sample_rate() send_voice.rpc(voice_data['buffer']) # If loopback is enable, play it back at this point if has_loopback: # Our sample rate function above without toggling process_voice_data(voice_data['buffer'], 1) @rpc("any_peer","call_remote","unreliable") func send_voice(voice_buffer : PackedByteArray): var sender_id : int = multiplayer.get_remote_sender_id() if not network_playback.has(sender_id): push_error("[VoiceChat] Received audio data, but there is no AudioStreamPlayer available for that user. Emitter: " + str(sender_id)) return if not network_voice_buffer.has(sender_id): push_error("[VoiceChat] Received audio data without allocated voice buffer for that user. Emitter: " + str(sender_id)) return var local_peer_buffer : PackedByteArray = network_voice_buffer.get(sender_id) if local_peer_buffer.size() >= max_buffer_size: push_error("[VoiceChat] AudioBuffer exeeding size limit. Emitter: " + str(sender_id)) process_voice_data(voice_buffer, sender_id) @rpc("any_peer","call_remote","reliable") func send_sample_rate(sample_rate : int): var sender_id : int = multiplayer.get_remote_sender_id() if sample_rate < sample_rate_min || sample_rate > sample_rate_max: push_warning("[VoiceChat] Received invalid sample rate for peer. Emitter: " + str(sender_id)) return if not network_playback.has(sender_id): push_warning("[VoiceChat] Received sample rate for invalid peer. Emitter: " + str(sender_id)) # Race condition? return if not Multiplayer.playerDict.has(sender_id): push_error("[VoiceChat] Player exists in VoiceChat variables, but not playerDict??? Emitter: " + str(sender_id)) return if not Multiplayer.playerDict.has(sender_id): return var player : Node3D = Multiplayer.playerDict.get(sender_id) if not player.has_node(voicechat_player_path): return player.get_node(voicechat_player_path).stream.mix_rate = sample_rate # Fix this (ref: process_voice_data TODO:_1 network_playback.get(sender_id).clear_buffer() network_voice_buffer.get(sender_id).clear() func get_sample_rate() -> void: current_sample_rate = Steam.getVoiceOptimalSampleRate() if loopback_player.stream.mix_rate != current_sample_rate: send_sample_rate.rpc(current_sample_rate) loopback_player.stream.mix_rate = current_sample_rate print("[VoiceChat] Changed own sample rate to: " + str(current_sample_rate)) func process_voice_data(voice_data: PackedByteArray, voice_source: int) -> void: #TODO:_1 Fix this with a mix rate dict var decompressed_voice : Dictionary if voice_source == 0: decompressed_voice = Steam.decompressVoice(voice_data, current_sample_rate) elif Multiplayer.playerDict.has(voice_source): var player : Node3D = Multiplayer.playerDict.get(voice_source) if not player.has_node(voicechat_player_path): push_error("[VoiceChat] Player %s doesn't have a VoiceChat node", voice_source) return var mix_rate = player.get_node_or_null(voicechat_player_path).stream.mix_rate assert(mix_rate >= sample_rate_min and mix_rate <= sample_rate_max) decompressed_voice = Steam.decompressVoice(voice_data, mix_rate) else: push_error("[VoiceChat] Received voice data for non existing player") return if decompressed_voice['result'] == Steam.VOICE_RESULT_OK and decompressed_voice['size'] > 0: if voice_source == 0: local_voice_buffer = decompressed_voice['uncompressed'] local_voice_buffer.resize(decompressed_voice['size']) # We now iterate through the local_voice_buffer and push the samples to the audio generator for i: int in local_playback.get_frames_available(): if local_voice_buffer.size() == 0: break # Steam's audio data is represented as 16-bit single channel PCM audio, so we need to convert it to amplitudes # Combine the low and high bits to get full 16-bit value var raw_value: int = local_voice_buffer[0] | (local_voice_buffer[1] << 8) # Make it a 16-bit signed integer raw_value = (raw_value + 32768) & 0xffff # Convert the 16-bit integer to a float on from -1 to 1 var amplitude: float = float(raw_value - 32768) / 32768.0 # push_frame() takes a Vector2. The x represents the left channel and the y represents the right channel local_playback.push_frame(Vector2(amplitude, amplitude)) # Delete the used samples local_voice_buffer.remove_at(0) local_voice_buffer.remove_at(0) else: #TODO: Better naming #TODO: Better explanation for 16-bit conversions var peer_voice_buffer : PackedByteArray = network_voice_buffer.get(voice_source) peer_voice_buffer = decompressed_voice['uncompressed'] peer_voice_buffer.resize(decompressed_voice['size']) var peer_playback : AudioStreamGeneratorPlayback = network_playback.get(voice_source) for i: int in peer_playback.get_frames_available(): if peer_voice_buffer.size() == 0: break var raw_value: int = peer_voice_buffer[0] | (peer_voice_buffer[1] << 8) # Make it a 16-bit signed integer raw_value = (raw_value + 32768) & 0xffff # Convert the 16-bit integer to a float on from -1 to 1 var amplitude: float = float(raw_value - 32768) / 32768.0 # push_frame() takes a Vector2. The x represents the left channel and the y represents the right channel peer_playback.push_frame(Vector2(amplitude, amplitude)) # Delete the used samples peer_voice_buffer.remove_at(0) peer_voice_buffer.remove_at(0) func record_voice(is_recording: bool) -> void: # If talking, suppress all other audio or voice comms from the Steam UI Steam.setInGameVoiceSpeaking(Steamworks.steam_id, is_recording) if is_recording: Steam.startVoiceRecording() else: Steam.stopVoiceRecording()