extends Node #{ #var mic_capture : AudioEffectOpusChunked #var audio_stats : Dictionary[int, Array] # id->[last_number, packetsreceived, packetslost] #var packets_sent: int = 0 # #var packet_queue: Dictionary[int, Array] # sender_id -> Array of PackedByteArrays # # #func _ready(): #playstuff() #var mic_bus = AudioServer.get_bus_index("Mic") #mic_capture = AudioServer.get_bus_effect(mic_bus, 0) # #func _process(_delta: float): #if !mic_capture or Multiplayer.playerDict.is_empty(): #return #if Input.is_action_just_pressed("noise"): #play_note(69) #print("test") #var t1 = Time.get_unix_time_from_system() #for sender in packet_queue.keys(): #var VoiceChatPlayerOutputNode: AudioStreamPlayer3D = Multiplayer.playerDict.get(sender).VoiceChat #var OpusStream : AudioStreamOpusChunked = VoiceChatPlayerOutputNode.stream #while OpusStream.chunk_space_available() and packet_queue.get(sender).size() > 0: #var pkt: PackedByteArray = packet_queue.get(sender).pop_front() #print(packet_queue.get(sender).size()) #OpusStream.queue_length_frames() #OpusStream.push_opus_packet(pkt, 8, 0) #var t2 = Time.get_unix_time_from_system() #print("Time 1: ", 1000*(t2-t1), "ms") #var seq_num : PackedByteArray = PackedByteArray() #seq_num.resize(8) #while mic_capture.chunk_available(): #if multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED: #mic_capture.drop_chunk() #continue #seq_num.encode_u64(0,packets_sent+1) #var packet = mic_capture.read_opus_packet(seq_num) #mic_capture.drop_chunk() #_voice_packet_received.rpc(packet) #packets_sent += 1 #if (packets_sent % 500) == 0: #print("Packets sent: ", packets_sent, " from id ", multiplayer.get_unique_id(), " ", hash(packet)) #print("Size before: ", packet.size()) #print("Hash send: ", hash(packet)) #var t3 = Time.get_unix_time_from_system() #print("Time 2: ", 1000*(t3-t2), "ms") # #@rpc("any_peer", "call_remote" ,"unreliable_ordered", 1) #func _voice_packet_received(packet: PackedByteArray): #var sender_id = multiplayer.get_remote_sender_id() # ##Stats for printing #var sender_stats = audio_stats.get(sender_id, [0,0,0]) #if (sender_stats[0]+1 != packet.decode_u32(0)): #sender_stats[2]+=1 #sender_stats[0]=packet.decode_u64(0) #sender_stats[1]+=1 #audio_stats.set(sender_id, sender_stats) #if (sender_stats[1] % 500) == 0: #print("Packets received: ", sender_stats[1], " from id ", sender_id, " Lossrate: ", sender_stats[2], "/", sender_stats[1], "(" , str(100*(sender_stats[2]/sender_stats[1])), "%)") #print("Size after: ", packet.size()) #print("Hash received: ", hash(packet)) #print("Packet no.: ", packet.decode_u64(0)) #var VoiceChatPlayerOutputNode: AudioStreamPlayer3D = Multiplayer.playerDict.get(sender_id).VoiceChat #var _OpusStream : AudioStreamOpusChunked = VoiceChatPlayerOutputNode.stream ## OpusSteam #if not packet_queue.has(sender_id): #var x : Array[PackedByteArray] = [] #packet_queue.set(sender_id, x) #if packet_queue.get(sender_id).size() > 250: #push_error("VoiceChat buffer for user ", sender_id, " too large!") #var x : Array[PackedByteArray] = [] #packet_queue.set(sender_id, x) #packet_queue.get(sender_id).append(packet) # #var playback: AudioStreamGeneratorPlayback #var player : AudioStreamPlayer # #func playstuff(): #var generator = AudioStreamGenerator.new() #generator.mix_rate = 44100 #generator.buffer_length = 2 #player = AudioStreamPlayer.new() #player.bus = "Mic" #player.stream = generator #add_child(player) #player.play() #playback = player.get_stream_playback() # #func play_note(midi_note: int): #var freq = 440.0 * pow(2.0, (midi_note - 69) / 12.0) # MIDI ? Hz #generate_sine_wave(freq) # #func generate_sine_wave(frequency: float): #var sample_rate = 44100.0 #var increment = TAU * frequency / sample_rate #var phase = 0.0 #var num_samples = int(sample_rate * 2) # 0.5 seconds #var buffer = PackedVector2Array() #for i in num_samples: #var sample = sin(phase) #buffer.append(Vector2(sample, sample)) # stereo #phase += increment #playback.push_buffer(buffer) #} var current_sample_rate: int = 48000 var has_loopback: bool = true var local_playback: AudioStreamGeneratorPlayback = null var local_voice_buffer: PackedByteArray = PackedByteArray() var network_playback: Dictionary[int, AudioStreamGeneratorPlayback] = {} var network_voice_buffer: Dictionary[int, PackedByteArray] = {} var max_buffer_size: int = 1000 @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 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) func _process(_delta: float) -> void: if not Steamworks.is_initiallized: return check_for_voice_local() for player : int in network_voice_buffer: process_voice_data(network_voice_buffer.get(player), str(player)) func add_joined_peer(id): var remote_audio_stream_player : Node3D = get_node_or_null(^"/root/Main/Players/%s/VoiceChat_a" % id) network_playback.set(id, remote_audio_stream_player) network_voice_buffer.set(id,PackedByteArray()) 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("Voice message has data: %s / %s" % [voice_data['result'], voice_data['written']]) # Here we can pass this voice data off on the network send_voice.rpc(voice_data['buffer']) # If loopback is enable, play it back at this point if has_loopback: print("Loopback on") # Our sample rate function above without toggling get_sample_rate() process_voice_data(voice_data, "local") @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)) local_peer_buffer.append_array(voice_buffer) print_debug("[VoiceChat] AudioBuffer size: " + str(local_peer_buffer.size())) @rpc("any_peer","call_remote","reliable") func send_sample_rate(sample_rate : int): if sample_rate < 11025 || sample_rate > 48000: return var sender_id : int = multiplayer.get_remote_sender_id() if network_playback.has(sender_id): network_playback.get(sender_id).stream.mix_rate = sample_rate 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: %s", current_sample_rate) func process_voice_data(voice_data: Dictionary, voice_source: String) -> void: var decompressed_voice: Dictionary = Steam.decompressVoice(voice_data['buffer'], current_sample_rate) if decompressed_voice['result'] == Steam.VOICE_RESULT_OK and decompressed_voice['size'] > 0: print("[VoiceChat] Decompressed voice: %s" % decompressed_voice['size']) if voice_source == "local": 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 #Better explanation for 16-bit conversions network_voice_buffer.set(voice_source, decompressed_voice['uncompressed']) var _network_voice_buffer : PackedByteArray = network_voice_buffer.get(voice_source) _network_voice_buffer.resize(decompressed_voice['size']) var _network_playback : AudioStreamGenerator = network_playback.get(voice_source) for i: int in _network_playback.get_frames_available(): if _network_playback.size() == 0: break var raw_value: int = _network_voice_buffer[0] | (_network_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 _network_playback.push_frame(Vector2(amplitude, amplitude)) # Delete the used samples _network_playback.remove_at(0) _network_playback.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()