Merge pull request #113 from Nitwel/voice

Add Voice Assistant 🎤
This commit is contained in:
Nitwel 2024-03-15 19:37:05 +01:00 committed by GitHub
commit 53bd68e36d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 603 additions and 15 deletions

3
assets/chat_bubble.blend Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5fa5f006a42d87f43a8b411e4e4bf64a8b4fbbaedd0d02579134a8fa59161eb
size 894176

BIN
assets/chat_bubble.blend1 Normal file

Binary file not shown.

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f215158ae0aba0113e0077040342fc1b508cfec0a3a1e022c9ce0c16973e2ee1
size 17828

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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")]

View File

@ -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))

View File

@ -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"]

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

3
default_bus_layout.tres Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fb9d247646174775b00db7902c224ac62f734b3a6467af32919d12d2a6861c38
size 555

View File

@ -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

View File

@ -1,3 +1,3 @@
extends Node
@onready var body = get_node("/root/Main/House")
@onready var body = get_node_or_null("/root/Main/House")

View File

@ -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

View File

@ -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))

View File

@ -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"
@ -14,3 +14,4 @@ func clear():
type = "HASS_WS"
url = ""
token = ""
voice_assistant = false

View File

@ -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

9
lib/utils/font_tools.gd Normal file
View File

@ -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

11
lib/utils/sample_hold.gd Normal file
View File

@ -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

View File

@ -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="."]

View File

@ -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"

View File

@ -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))

View File

@ -0,0 +1,3 @@
[gd_scene format=3 uid="uid://bpy811vonnq2u"]
[node name="Node2D" type="Node2D"]