diff --git a/assets/chat_bubble.blend b/assets/chat_bubble.blend new file mode 100644 index 0000000..f944d04 --- /dev/null +++ b/assets/chat_bubble.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5fa5f006a42d87f43a8b411e4e4bf64a8b4fbbaedd0d02579134a8fa59161eb +size 894176 diff --git a/assets/chat_bubble.blend1 b/assets/chat_bubble.blend1 new file mode 100644 index 0000000..6ee4542 Binary files /dev/null and b/assets/chat_bubble.blend1 differ diff --git a/assets/models/chat_bubble/chat_bubble.glb b/assets/models/chat_bubble/chat_bubble.glb new file mode 100644 index 0000000..8cf0d61 --- /dev/null +++ b/assets/models/chat_bubble/chat_bubble.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f215158ae0aba0113e0077040342fc1b508cfec0a3a1e022c9ce0c16973e2ee1 +size 17828 diff --git a/assets/models/chat_bubble/chat_bubble.glb.import b/assets/models/chat_bubble/chat_bubble.glb.import new file mode 100644 index 0000000..f0166c8 --- /dev/null +++ b/assets/models/chat_bubble/chat_bubble.glb.import @@ -0,0 +1,34 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://b12raorbby1xd" +path="res://.godot/imported/chat_bubble.glb-03622c64b96f5698360bcfb8a4904483.scn" + +[deps] + +source_file="res://assets/models/chat_bubble/chat_bubble.glb" +dest_files=["res://.godot/imported/chat_bubble.glb-03622c64b96f5698360bcfb8a4904483.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/apply_root_scale=true +nodes/root_scale=1.0 +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +import_script/path="" +_subresources={} +gltf/naming_version=1 +gltf/embedded_image_handling=1 diff --git a/content/main.gd b/content/main.gd index 9feab7e..dbf478b 100644 --- a/content/main.gd +++ b/content/main.gd @@ -2,6 +2,7 @@ extends Node3D var sky = preload ("res://assets/materials/sky.material") var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material") +const VoiceAssistant = preload ("res://content/system/assist/assist.tscn") @onready var environment: WorldEnvironment = $WorldEnvironment @onready var camera: XRCamera3D = $XROrigin3D/XRCamera3D @@ -10,6 +11,7 @@ var sky_passthrough = preload ("res://assets/materials/sky_passthrough.material" @onready var house = $House @onready var menu = $Menu @onready var keyboard = $Keyboard +var voice_assistant = null func _ready(): # In case we're running on the headset, use the passthrough sky @@ -20,6 +22,8 @@ func _ready(): else: RenderingServer.set_debug_generate_wireframes(true) + update_voice_assistant() + controller_left.button_pressed.connect(func(name): _emit_action(name, true, false) ) @@ -61,6 +65,17 @@ func _ready(): remove_child(keyboard) ) +func update_voice_assistant(): + if Store.settings.is_loaded() == false: + await Store.settings.on_loaded + + if Store.settings.voice_assistant&&voice_assistant == null: + voice_assistant = VoiceAssistant.instantiate() + add_child(voice_assistant) + elif !Store.settings.voice_assistant&&voice_assistant != null: + remove_child(voice_assistant) + voice_assistant.queue_free() + func toggle_menu(): if menu.show_menu == false: add_child(menu) diff --git a/content/system/assist/assist.gd b/content/system/assist/assist.gd new file mode 100644 index 0000000..ce3143a --- /dev/null +++ b/content/system/assist/assist.gd @@ -0,0 +1,109 @@ +extends Node3D + +const sample_hold = preload ("res://lib/utils/sample_hold.gd") +const Chat = preload ("./chat.gd") + +const audio_freq = 44100 +const target_freq = 16000 +const sample_rate_ratio: float = audio_freq / target_freq * 1.5 + +var effect: AudioEffectCapture +@export var input_threshold: float = 0.05 +@onready var audio_recorder: AudioStreamPlayer = $AudioStreamRecord +@onready var audio_timer: Timer = $AudioTimer +@onready var visual_timer: Timer = $VisualTimer +@onready var audio_player_3d: AudioStreamPlayer3D = $AudioStreamPlayer3D +@onready var chat_user: Chat = $ChatUser +@onready var chat_assistant: Chat = $ChatAssistant +@onready var loader: Node3D = $Loader +@onready var camera = $"/root/Main/XROrigin3D/XRCamera3D" + +var running := false + +func _ready(): + var index = AudioServer.get_bus_index("Record") + effect = AudioServer.get_bus_effect(index, 0) + + finish() + + audio_timer.timeout.connect(func(): + HomeApi.api.assist_handler.send_data(PackedByteArray()) + ) + + HomeApi.api.assist_handler.on_wake_word.connect(func(text): + loader.visible=true + chat_user.visible=false + chat_assistant.visible=false + global_position=camera.global_position + camera.global_transform.basis.z * - 0.5 + global_position.y *= 0.7 + global_transform.basis=Basis.looking_at((camera.global_position - global_position) * - 1) + running=true + ) + + HomeApi.api.assist_handler.on_stt_message.connect(func(text): + loader.visible=false + chat_user.visible=true + chat_user.text=text + ) + HomeApi.api.assist_handler.on_tts_message.connect(func(text): + chat_assistant.visible=true + chat_assistant.text=text + ) + + HomeApi.api.assist_handler.on_tts_sound.connect(func(audio): + audio_player_3d.stream=audio + audio_player_3d.play() + visual_timer.start() + running=false + ) + + HomeApi.api.assist_handler.on_error.connect(func(): + running=false + finish() + ) + + visual_timer.timeout.connect(func(): + if audio_player_3d.playing == false: + finish() + else: + await audio_player_3d.finished + finish() + ) + +func finish(): + if running: + return + + chat_user.visible = false + chat_assistant.visible = false + loader.visible = false + +func _process(_delta): + var sterioData: PackedVector2Array = effect.get_buffer(effect.get_frames_available()) + + if sterioData.size() == 0: + return + + var monoSampled := sample_hold.sample_and_hold(sterioData, sample_rate_ratio) + + # 16 bit PCM + var data := PackedByteArray() + data.resize(monoSampled.size() * 2) + + var max_amplitude = 0.0 + + for i in range(monoSampled.size()): + + var value = monoSampled[i] + max_amplitude = max(max_amplitude, value) + + data.encode_s16(i * 2, int(value * 32767)) + + if max_amplitude > input_threshold: + if audio_timer.is_stopped(): + HomeApi.api.assist_handler.start_wakeword() + + audio_timer.start() + + if audio_timer.is_stopped() == false: + HomeApi.api.assist_handler.send_data(data) diff --git a/content/system/assist/assist.tscn b/content/system/assist/assist.tscn new file mode 100644 index 0000000..76c7c44 --- /dev/null +++ b/content/system/assist/assist.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=5 format=3 uid="uid://oydbwnek6xb4"] + +[ext_resource type="Script" path="res://content/system/assist/assist.gd" id="1_5obhy"] +[ext_resource type="PackedScene" uid="uid://cy6jklyde3pgo" path="res://content/system/assist/chat.tscn" id="2_laew1"] +[ext_resource type="PackedScene" uid="uid://b0d1582vpkr8m" path="res://content/system/assist/loader.tscn" id="3_25iy1"] + +[sub_resource type="AudioStreamMicrophone" id="AudioStreamMicrophone_6tv2x"] + +[node name="Assist" type="Node3D"] +script = ExtResource("1_5obhy") + +[node name="AudioStreamRecord" type="AudioStreamPlayer" parent="."] +stream = SubResource("AudioStreamMicrophone_6tv2x") +autoplay = true +bus = &"Record" + +[node name="AudioTimer" type="Timer" parent="."] +wait_time = 2.0 +one_shot = true + +[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="."] + +[node name="VisualTimer" type="Timer" parent="."] +wait_time = 5.0 +one_shot = true + +[node name="ChatUser" parent="." instance=ExtResource("2_laew1")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.109997, 0.025, 0) +flip = false + +[node name="ChatAssistant" parent="." instance=ExtResource("2_laew1")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0499932, -0.025, 0) +text = "Hello, World!" + +[node name="Loader" parent="." instance=ExtResource("3_25iy1")] diff --git a/content/system/assist/chat.gd b/content/system/assist/chat.gd new file mode 100644 index 0000000..2cd61a2 --- /dev/null +++ b/content/system/assist/chat.gd @@ -0,0 +1,37 @@ +@tool +extends Node3D + +const FontTools = preload ("res://lib/utils/font_tools.gd") + +@onready var label: Label3D = $Label3D +@onready var chat: Skeleton3D = $chat_bubble/Armature/Skeleton3D +@onready var model: MeshInstance3D = $chat_bubble/Armature/Skeleton3D/Cube + +@export var text := "Hello, World!": + set(value): + if !is_node_ready(): await ready + + text = value + label.text = value + update() + +@export var flip: bool = false: + set(value): + if !is_node_ready(): await ready + + flip = value + model.rotation_degrees.x = -90 if value else 90 + +const base_width = 0.8 * 0.2 + +func update(): + var text_width = FontTools.get_font_size(label).x + + var offset = (text_width - base_width) / 0.2 + + offset = max(0.0, offset) + + if flip: + offset = -offset + + chat.set_bone_pose_position(1 if flip else 0, Vector3(0, offset, 0)) \ No newline at end of file diff --git a/content/system/assist/chat.tscn b/content/system/assist/chat.tscn new file mode 100644 index 0000000..6d1136a --- /dev/null +++ b/content/system/assist/chat.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=5 format=3 uid="uid://cy6jklyde3pgo"] + +[ext_resource type="PackedScene" uid="uid://b12raorbby1xd" path="res://assets/models/chat_bubble/chat_bubble.glb" id="1_lsdcs"] +[ext_resource type="Script" path="res://content/system/assist/chat.gd" id="1_rbrak"] +[ext_resource type="Material" uid="uid://bujy3egn1oqac" path="res://assets/materials/pri-500.material" id="2_ps3pl"] +[ext_resource type="FontVariation" uid="uid://d2ofyimg5s65q" path="res://assets/fonts/ui_font_500.tres" id="4_gxfp3"] + +[node name="Chat" type="Node3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.41237e-06, 0, 0) +script = ExtResource("1_rbrak") +text = "Hello World" +flip = true + +[node name="chat_bubble" parent="." instance=ExtResource("1_lsdcs")] +transform = Transform3D(0.2, 0, 0, 0, 0.2, 0, 0, 0, 0.2, -0.0154175, 0, 0.0710473) + +[node name="Armature" parent="chat_bubble" index="0"] +transform = Transform3D(1, 0, 0, 0, 0, 1, 0, -1, 0, 0.5, 0, 0) + +[node name="Cube" parent="chat_bubble/Armature/Skeleton3D" index="0"] +transform = Transform3D(-4.37114e-08, -1, -4.37114e-08, 0, -4.37114e-08, 1, -1, 4.37114e-08, 1.91069e-15, 0, 0.35, 0) +material_override = ExtResource("2_ps3pl") + +[node name="Label3D" type="Label3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.006) +pixel_size = 0.001 +text = "Hello World" +font = ExtResource("4_gxfp3") +font_size = 20 +outline_size = 0 +horizontal_alignment = 0 + +[editable path="chat_bubble"] diff --git a/content/system/assist/loader.gd b/content/system/assist/loader.gd new file mode 100644 index 0000000..838be69 --- /dev/null +++ b/content/system/assist/loader.gd @@ -0,0 +1,42 @@ +@tool +extends Node3D + +const material: StandardMaterial3D = preload ("res://assets/materials/pri-500.material") +var time: float = 0.0 +const DOT_COUNT = 8 +const RADIUS = 0.025 + +func _ready(): + generate_meshes() + +func generate_meshes(): + for i in range(DOT_COUNT): + var mesh := MeshInstance3D.new() + mesh.mesh = CylinderMesh.new() + mesh.mesh.top_radius = 0.005 + mesh.mesh.bottom_radius = 0.005 + mesh.mesh.height = 0.005 + mesh.material_override = material.duplicate() + mesh.material_override.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + + add_child(mesh) + + mesh.position = Vector3(sin(i * PI / DOT_COUNT * 2), cos(i * PI / DOT_COUNT * 2), 0) * RADIUS + mesh.rotation_degrees = Vector3(90, 0, 0) + +func _process(delta): + if !visible: + return + + time += delta + + for i in range(get_child_count()): + var mesh := get_child(i) + + if mesh == null: + return + + mesh.material_override.albedo_color.a = saw_tooth(i / float(get_child_count()) + time) + +func saw_tooth(x: float) -> float: + return 1 - fmod(x, 1) \ No newline at end of file diff --git a/content/system/assist/loader.tscn b/content/system/assist/loader.tscn new file mode 100644 index 0000000..2cc43de --- /dev/null +++ b/content/system/assist/loader.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://b0d1582vpkr8m"] + +[ext_resource type="Script" path="res://content/system/assist/loader.gd" id="1_3bi3s"] + +[node name="Loader" type="Node3D"] +script = ExtResource("1_3bi3s") diff --git a/content/ui/components/input/text_handler.gd b/content/ui/components/input/text_handler.gd index 51bef8b..9679eac 100644 --- a/content/ui/components/input/text_handler.gd +++ b/content/ui/components/input/text_handler.gd @@ -1,5 +1,7 @@ extends RefCounted +const FontTools = preload ("res://lib/utils/font_tools.gd") + var label: Label3D var text: String = "" @@ -73,14 +75,13 @@ func _calculate_caret_position(click_pos_x: float): return gap_offsets.size() - 1 func _calculate_text_gaps(): - var font = label.get_font() var offsets = [0.0] for i in range(text.length()): var chars = text.substr(0, i + 1) # Can't use single chars because of kerning. - var size = font.get_string_size(chars, HORIZONTAL_ALIGNMENT_CENTER, -1, label.font_size) + var size = FontTools.get_font_size(label, chars) - offsets.append(size.x * label.pixel_size) + offsets.append(size.x) return offsets diff --git a/content/ui/menu/settings/settings_menu.gd b/content/ui/menu/settings/settings_menu.gd index c46d8c5..51e3afc 100644 --- a/content/ui/menu/settings/settings_menu.gd +++ b/content/ui/menu/settings/settings_menu.gd @@ -3,6 +3,7 @@ extends Node3D const credits_scene = preload ("./credits.tscn") @onready var connection_status = $Content/ConnectionStatus +@onready var main = $"/root/Main" @onready var input_url = $Content/InputURL @onready var input_token = $Content/InputToken @@ -11,6 +12,7 @@ const credits_scene = preload ("./credits.tscn") @onready var save = $Content/Save @onready var clear_save = $Content/ClearSave @onready var background = $Background +@onready var voice_assist = $Content/VoiceAssist func _ready(): background.visible = false @@ -53,6 +55,31 @@ func _ready(): House.body.update_house() ) + voice_assist.on_button_down.connect(func(): + if Store.settings.is_loaded() == false: + await Store.settings.on_loaded + + OS.request_permissions() + + voice_assist.label="mic" + + Store.settings.voice_assistant=true + main.update_voice_assistant() + Store.settings.save_local() + ) + + voice_assist.on_button_up.connect(func(): + if Store.settings.is_loaded() == false: + await Store.settings.on_loaded + + voice_assist.label="mic_off" + + Store.settings.voice_assistant=false + main.update_voice_assistant() + Store.settings.save_local() + + ) + HomeApi.on_connect.connect(func(): connection_status.text="Connected" ) @@ -60,3 +87,9 @@ func _ready(): HomeApi.on_disconnect.connect(func(): connection_status.text="Disconnected" ) + + if Store.settings.is_loaded() == false: + await Store.settings.on_loaded + + voice_assist.label = "mic_off" if Store.settings.voice_assistant == false else "mic" + voice_assist.active = Store.settings.voice_assistant diff --git a/content/ui/menu/settings/settings_menu.tscn b/content/ui/menu/settings/settings_menu.tscn index c41a242..7e45261 100644 --- a/content/ui/menu/settings/settings_menu.tscn +++ b/content/ui/menu/settings/settings_menu.tscn @@ -131,3 +131,18 @@ outline_size = 0 horizontal_alignment = 0 autowrap_mode = 3 width = 150.0 + +[node name="VoiceAssist" parent="Content" instance=ExtResource("1_faxng")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1, 0, 0.12) +label = "mic_off" +icon = true +toggleable = true + +[node name="LabelVoiceAssist" type="Label3D" parent="Content"] +transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0.01, 0, 0.12) +pixel_size = 0.001 +text = "Voice- +Assist:" +font_size = 18 +outline_size = 0 +horizontal_alignment = 0 diff --git a/default_bus_layout.tres b/default_bus_layout.tres new file mode 100644 index 0000000..7386b28 --- /dev/null +++ b/default_bus_layout.tres @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb9d247646174775b00db7902c224ac62f734b3a6467af32919d12d2a6861c38 +size 555 diff --git a/export_presets.cfg b/export_presets.cfg index 1ef519e..76601c7 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -155,7 +155,7 @@ permissions/receive_boot_completed=false permissions/receive_mms=false permissions/receive_sms=false permissions/receive_wap_push=false -permissions/record_audio=false +permissions/record_audio=true permissions/reorder_tasks=false permissions/restart_packages=false permissions/send_respond_via_message=false @@ -377,7 +377,7 @@ permissions/receive_boot_completed=false permissions/receive_mms=false permissions/receive_sms=false permissions/receive_wap_push=false -permissions/record_audio=false +permissions/record_audio=true permissions/reorder_tasks=false permissions/restart_packages=false permissions/send_respond_via_message=false diff --git a/lib/globals/house_body.gd b/lib/globals/house_body.gd index c10f30a..4b0b520 100644 --- a/lib/globals/house_body.gd +++ b/lib/globals/house_body.gd @@ -1,3 +1,3 @@ extends Node -@onready var body = get_node("/root/Main/House") \ No newline at end of file +@onready var body = get_node_or_null("/root/Main/House") \ No newline at end of file diff --git a/lib/home_apis/hass_ws/handlers/assist.gd b/lib/home_apis/hass_ws/handlers/assist.gd new file mode 100644 index 0000000..dde4eba --- /dev/null +++ b/lib/home_apis/hass_ws/handlers/assist.gd @@ -0,0 +1,132 @@ +const HASS_API = preload ("../hass.gd") + +signal on_wake_word(wake_word: String) +signal on_stt_message(message: String) +signal on_tts_message(message: String) +signal on_tts_sound(sound: AudioStreamMP3) +signal on_error() + +var api: HASS_API +var pipe_running := false +var handler_id := 0 +var wake_word = null: + set(value): + if value != wake_word&&value != null: + on_wake_word.emit(value) + wake_word = value + +var stt_message = null: + set(value): + if value != stt_message&&value != null: + on_stt_message.emit(value) + stt_message = value + +var tts_message = null: + set(value): + if value != tts_message&&value != null: + on_tts_message.emit(value) + tts_message = value + +var tts_sound = null: + set(value): + if value != tts_sound&&value != null: + on_tts_sound.emit(value) + tts_sound = value + +func _init(hass: HASS_API): + self.api = hass + +func on_connect(): + pass + +func start_wakeword(): + if pipe_running: + return + + api.send_packet({ + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "timeout": 5, + "sample_rate": 16000 + }, + "timeout": 60 + }, true) + +func send_data(data: PackedByteArray): + + # prepend the handler id to the data in 8 bits + if pipe_running: + var stream = PackedByteArray() + + stream.resize(1) + stream.encode_s8(0, handler_id) + stream.append_array(data) + + api.send_raw(stream) + +func handle_message(message: Dictionary): + if message["type"] != "event": + return + + var event = message["event"] + + if event.has("type") == false: + return + + match event["type"]: + "run-start": + pipe_running = true + handler_id = event["data"]["runner_data"]["stt_binary_handler_id"] + "wake_word-end": + if pipe_running == false: + return + + if event["data"]["wake_word_output"].has("wake_word_phrase") == false: + return + + wake_word = event["data"]["wake_word_output"]["wake_word_phrase"] + "stt-end": + if pipe_running == false: + return + + if event["data"]["stt_output"].has("text") == false: + return + + stt_message = event["data"]["stt_output"]["text"] + "intent-end": + if pipe_running == false: + return + + tts_message = event["data"]["intent_output"]["response"]["speech"]["plain"]["speech"] + "tts-end": + if pipe_running == false: + return + + if event["data"]["tts_output"].has("url") == false: + return + + var headers = PackedStringArray(["Authorization: Bearer %s" % api.token, "Content-Type: application/json"]) + var url = "%s://%s%s" % ["https" if api.url.begins_with("wss") else "http", api.url.split("//")[1],event["data"]["tts_output"]["url"]] + + Request.request(url, headers, HTTPClient.METHOD_GET) + + var response = await Request.request_completed + + if response[0] != HTTPRequest.RESULT_SUCCESS: + return + + var sound = AudioStreamMP3.new() + sound.data = response[3] + + tts_sound = sound + "error": + if event["data"]["code"] == "stt-no-text-recognized": + on_error.emit() + "run-end": + pipe_running = false + wake_word = null + handler_id = 0 + _: + pass diff --git a/lib/home_apis/hass_ws/hass.gd b/lib/home_apis/hass_ws/hass.gd index ceca25e..91907e9 100644 --- a/lib/home_apis/hass_ws/hass.gd +++ b/lib/home_apis/hass_ws/hass.gd @@ -2,6 +2,7 @@ extends Node const AuthHandler = preload ("./handlers/auth.gd") const IntegrationHandler = preload ("./handlers/integration.gd") +const AssistHandler = preload ("./handlers/assist.gd") signal on_connect() signal on_disconnect() @@ -25,6 +26,7 @@ var packet_callbacks := CallbackMap.new() var auth_handler: AuthHandler var integration_handler: IntegrationHandler +var assist_handler: AssistHandler func _init(url:=self.url, token:=self.token): self.url = url @@ -32,6 +34,7 @@ func _init(url:=self.url, token:=self.token): auth_handler = AuthHandler.new(self, url, token) integration_handler = IntegrationHandler.new(self) + assist_handler = AssistHandler.new(self) devices_template = devices_template.replace("\n", " ").replace("\t", "").replace("\r", " ") connect_ws() @@ -82,6 +85,7 @@ func handle_packet(packet: Dictionary): if LOG_MESSAGES: print("Received packet: %s" % str(packet).substr(0, 1000)) auth_handler.handle_message(packet) + assist_handler.handle_message(packet) if packet.has("id"): packet_callbacks.call_key(int(packet.id), [packet]) @@ -117,6 +121,7 @@ func start_subscriptions(): func handle_connect(): integration_handler.on_connect() + assist_handler.on_connect() connected = true on_connect.emit() @@ -176,7 +181,15 @@ func send_request_packet(packet: Dictionary, ignore_initial:=false): return await promise.settled -func send_packet(packet: Dictionary): +func send_raw(packet: PackedByteArray): + if LOG_MESSAGES: print("Sending binary: %s" % packet.hex_encode()) + socket.send(packet) + +func send_packet(packet: Dictionary, with_id:=false): + if with_id: + packet.id = id + id += 1 + if LOG_MESSAGES: print("Sending packet: %s" % encode_packet(packet)) socket.send_text(encode_packet(packet)) diff --git a/lib/stores/settings.gd b/lib/stores/settings.gd index e2fc1ee..d277210 100644 --- a/lib/stores/settings.gd +++ b/lib/stores/settings.gd @@ -1,11 +1,11 @@ extends StoreClass -const StoreClass = preload("./store.gd") - +const StoreClass = preload ("./store.gd") var type: String = "HASS_WS" var url: String = "" var token: String = "" +var voice_assistant: bool = false func _init(): _save_path = "user://settings.json" @@ -13,4 +13,5 @@ func _init(): func clear(): type = "HASS_WS" url = "" - token = "" \ No newline at end of file + token = "" + voice_assistant = false \ No newline at end of file diff --git a/lib/stores/store.gd b/lib/stores/store.gd index 2a68ba8..f2ac103 100644 --- a/lib/stores/store.gd +++ b/lib/stores/store.gd @@ -1,6 +1,6 @@ extends RefCounted -const VariantSerializer = preload("res://lib/utils/variant_serializer.gd") +const VariantSerializer = preload ("res://lib/utils/variant_serializer.gd") signal on_loaded signal on_saved @@ -18,7 +18,7 @@ func create_dict(): var data: Dictionary = {} for prop_info in get_property_list(): - if prop_info.name.begins_with("_") || prop_info.hint_string != "": + if prop_info.name.begins_with("_")||prop_info.hint_string != "": continue var prop = get(prop_info.name) @@ -32,10 +32,14 @@ func create_dict(): func use_dict(dict: Dictionary): for prop_info in get_property_list(): - if prop_info.name.begins_with("_") || prop_info.hint_string != "": + if prop_info.name.begins_with("_")||prop_info.hint_string != "": continue var prop = get(prop_info.name) + + if dict.has(prop_info.name) == false: + continue + var prop_value = dict[prop_info.name] if prop is Store: @@ -43,7 +47,7 @@ func use_dict(dict: Dictionary): else: set(prop_info.name, prop_value) -func save_local(path = _save_path): +func save_local(path=_save_path): if path == null: return false @@ -61,7 +65,7 @@ func save_local(path = _save_path): return true -func load_local(path = _save_path): +func load_local(path=_save_path): if path == null: return false diff --git a/lib/utils/font_tools.gd b/lib/utils/font_tools.gd new file mode 100644 index 0000000..a8677c6 --- /dev/null +++ b/lib/utils/font_tools.gd @@ -0,0 +1,9 @@ +static func get_font_size(label: Label3D, chars=null): + var font = label.font + + if font == null: + return Vector2(0, 0) + + var size = font.get_string_size(label.text if chars == null else chars, label.horizontal_alignment, label.width, label.font_size) * label.pixel_size + + return size \ No newline at end of file diff --git a/lib/utils/sample_hold.gd b/lib/utils/sample_hold.gd new file mode 100644 index 0000000..97f25c5 --- /dev/null +++ b/lib/utils/sample_hold.gd @@ -0,0 +1,11 @@ +static func sample_and_hold(data: PackedVector2Array, sample_rate: float) -> PackedFloat32Array: + var new_data: PackedFloat32Array = PackedFloat32Array() + new_data.resize(int(data.size() / sample_rate)) + + var counter = 0.0 + + for i in range(new_data.size()): + new_data[i] = data[int(counter)].y + counter += sample_rate + + return new_data \ No newline at end of file diff --git a/lib/utils/sample_hold.tscn b/lib/utils/sample_hold.tscn new file mode 100644 index 0000000..9cc2184 --- /dev/null +++ b/lib/utils/sample_hold.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3 uid="uid://b4l22m7bxamsc"] + +[ext_resource type="Script" path="res://test/lib/utils/sample_hold/sample_hold.gd" id="1_t0y35"] + +[node name="Node2D" type="Node2D"] +script = ExtResource("1_t0y35") + +[node name="CanvasLayer" type="CanvasLayer" parent="."] diff --git a/project.godot b/project.godot index 0ca5bc8..ef8aca1 100644 --- a/project.godot +++ b/project.godot @@ -15,6 +15,10 @@ run/main_scene="res://content/main.tscn" config/features=PackedStringArray("4.2", "Mobile") config/icon="res://assets/logo.png" +[audio] + +driver/enable_input=true + [autoload] XRToolsUserSettings="*res://addons/godot-xr-tools/user_settings/user_settings.gd" diff --git a/test/lib/utils/sample_hold/sample_hold.gd b/test/lib/utils/sample_hold/sample_hold.gd new file mode 100644 index 0000000..2e38cd9 --- /dev/null +++ b/test/lib/utils/sample_hold/sample_hold.gd @@ -0,0 +1,34 @@ +@tool +extends Node2D + +const sample_hold = preload ("res://lib/utils/sample_hold.gd") + +var data = PackedVector2Array() +var result: PackedFloat32Array + +func _ready(): + for i in range(0, 44100): + var value = sin(i * 2 * PI / 44100.0) + data.push_back(Vector2(value, value)) + + result = sample_hold.sample_and_hold(data, 44100.0 / 16000.0 * 1.5) + +func _draw(): + var size = get_viewport().get_visible_rect().size + size.x *= 10 + size.y *= 4 + var center = size / 2 + + draw_line(Vector2(0, size.y / 2), Vector2(size.x, size.y / 2), Color(1, 1, 1)) + + for i in range(0, data.size()): + var value = data[i] + var x = i * (size.x / data.size()) + + draw_line(Vector2(x, 0), Vector2(x, value.x * center.y), Color(1, 0, 0)) + + for i in range(0, result.size()): + var value = result[i] + var x = i * (size.x / result.size()) + + draw_line(Vector2(x, 0), Vector2(x, value * center.y), Color(0, 1, 0)) \ No newline at end of file diff --git a/test/lib/utils/sample_hold/sample_hold.tscn b/test/lib/utils/sample_hold/sample_hold.tscn new file mode 100644 index 0000000..cba1c37 --- /dev/null +++ b/test/lib/utils/sample_hold/sample_hold.tscn @@ -0,0 +1,3 @@ +[gd_scene format=3 uid="uid://bpy811vonnq2u"] + +[node name="Node2D" type="Node2D"]