commit 3a9fa29ca2671d72144f92749a72b38021def001 Author: Holly Date: Thu Apr 24 09:11:42 2025 +0100 Initial Commit diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..3329385 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ditahryq1qbup" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/node_3d.gd b/node_3d.gd new file mode 100644 index 0000000..a478002 --- /dev/null +++ b/node_3d.gd @@ -0,0 +1,30 @@ +extends Node3D + +func encode_sstv(): + #var encoder = Martin1.new() + var encoder = RobotBW8.new() + var cameraImage = get_viewport().get_texture().get_image() + #var cameraImage = load("res://checker.png").get_image() + #var cameraImage = load("res://spiral.png").get_image() + #var cameraImage = load("res://testtest.png").get_image() + #var cameraImage = load("res://testtest2.png").get_image() + #var cameraImage = load("res://testtest3.png").get_image() + var audioBuffer = encoder.EncodeSSTV(cameraImage) + + # Spew that audio yo. + $AudioStreamPlayer.stream.mix_rate = SSTVEncoder.SAMPLE_RATE + $AudioStreamPlayer.stream.buffer_length = 120 + $AudioStreamPlayer.play() + var player = $AudioStreamPlayer.get_stream_playback() + for i in range(0, audioBuffer.size()): + player.push_frame(Vector2(audioBuffer[i], audioBuffer[i])) + +# a lil hacky delay so sdfgi can settle lol. +var startTimer = 0 +var first = true +func _process(delta): + if startTimer > 10 and first: + first = false + encode_sstv() + + startTimer += 1 diff --git a/node_3d.gd.uid b/node_3d.gd.uid new file mode 100644 index 0000000..6e7e83b --- /dev/null +++ b/node_3d.gd.uid @@ -0,0 +1 @@ +uid://bx4xg8k3m1wos diff --git a/node_3d.tscn b/node_3d.tscn new file mode 100644 index 0000000..8aabbae --- /dev/null +++ b/node_3d.tscn @@ -0,0 +1,56 @@ +[gd_scene load_steps=8 format=3 uid="uid://dt230shblncgx"] + +[ext_resource type="Script" uid="uid://bx4xg8k3m1wos" path="res://node_3d.gd" id="1_a202f"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_a202f"] + +[sub_resource type="Sky" id="Sky_noarx"] +sky_material = SubResource("ProceduralSkyMaterial_a202f") + +[sub_resource type="Environment" id="Environment_a0tk4"] +background_mode = 2 +sky = SubResource("Sky_noarx") +sdfgi_enabled = true + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_r3fl7"] +albedo_color = Color(1, 0, 0, 1) +emission_enabled = true +emission = Color(1, 0, 0, 1) +emission_energy_multiplier = 3.02 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_jka67"] +albedo_color = Color(0, 1, 0, 1) +emission_enabled = true +emission = Color(0, 1, 0, 1) +emission_energy_multiplier = 3.02 + +[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_a202f"] + +[node name="Node3D" type="Node3D"] +script = ExtResource("1_a202f") + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("Environment_a0tk4") + +[node name="Camera3D" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.997971, 0) + +[node name="CSGBox3D" type="CSGBox3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.23108, -0.464242, -4.94434) +use_collision = true +size = Vector3(9.60767, 1, 10.8887) + +[node name="CSGBox3D2" type="CSGBox3D" parent="."] +transform = Transform3D(0.966061, 0, 0.258313, 0, 1, 0, -0.258313, 0, 0.966061, -0.617795, 0.535758, -2.20278) +material = SubResource("StandardMaterial3D_r3fl7") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(-0.886604, 0.302007, -0.350322, 0.226305, 0.943798, 0.240896, 0.403386, 0.1343, -0.905121, 0, 1.9072, 0) +shadow_enabled = true + +[node name="CSGBox3D3" type="CSGBox3D" parent="."] +transform = Transform3D(0.869943, 0, -0.493152, 0, 1, 0, 0.493152, 0, 0.869943, 1.11562, 0.535758, -2.43883) +material = SubResource("StandardMaterial3D_jka67") + +[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] +stream = SubResource("AudioStreamGenerator_a202f") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..241f5fa --- /dev/null +++ b/project.godot @@ -0,0 +1,16 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="SSTV" +run/main_scene="uid://dt230shblncgx" +config/features=PackedStringArray("4.4", "Forward Plus") +config/icon="res://icon.svg" diff --git a/src/Encoder.gd b/src/Encoder.gd new file mode 100644 index 0000000..93aebab --- /dev/null +++ b/src/Encoder.gd @@ -0,0 +1,122 @@ +extends Node +class_name SSTVEncoder + +const TWO_PI = PI * 2 +const SAMPLE_RATE = 48000.0 +const VIS_BIT_FREQ = { + 'ONE': 1100.0, + 'ZERO': 1300.0, +} +const PREFIX_PULSE_LENGTH = 0.1 # 100 ms +const HEADER_PULSE_LENGTH = 0.3 # 300 ms +const HEADER_BREAK_LENGTH = 0.01 # 10 ms +const VIS_BIT_LENGTH = 0.03 # 30 ms +const SYNC_PULSE_FREQ = 1200.0 + +var outputFloats = PackedFloat32Array() + +func EncodePrefix(): + outputFloats.append_array(GenerateTone(1900, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(1500, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(1900, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(1500, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(2300, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(1500, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(2300, PREFIX_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(1500, PREFIX_PULSE_LENGTH)) + +func EncodeHeader(vis: Array): + outputFloats.append_array(GenerateTone(1900, HEADER_PULSE_LENGTH)) + outputFloats.append_array(GenerateTone(SYNC_PULSE_FREQ, HEADER_BREAK_LENGTH)) + outputFloats.append_array(GenerateTone(1900, HEADER_PULSE_LENGTH)) + + # VIS Code + outputFloats.append_array(GenerateTone(SYNC_PULSE_FREQ, VIS_BIT_LENGTH)) + + var parity = 0 + vis.reverse() + for bit in vis: + var bit_freq = VIS_BIT_FREQ["ONE"] if bit else VIS_BIT_FREQ["ZERO"] + if bit: + parity += 1 + outputFloats.append_array(GenerateTone(bit_freq, VIS_BIT_LENGTH)) + + var parity_freq = VIS_BIT_FREQ["ZERO"] if parity % 2 == 0 else VIS_BIT_FREQ["ONE"] + outputFloats.append_array(GenerateTone(parity_freq, VIS_BIT_LENGTH)) + + outputFloats.append_array(GenerateTone(SYNC_PULSE_FREQ, VIS_BIT_LENGTH)) + +func GenerateTone(frequency: float, duration: float) -> PackedFloat32Array: + var samples = PackedFloat32Array() + var total_samples = int(duration * SAMPLE_RATE) + + for i in range(total_samples): + var time = float(i) / SAMPLE_RATE + var sample = sin(TWO_PI * frequency * time) + samples.append(sample) + + return samples + +func GenerateToneFromCurve(frequencies: Array, duration: float) -> PackedFloat32Array: + var samples = PackedFloat32Array() + var totalSamples = int(duration * SAMPLE_RATE) + var num_points = frequencies.size() + + var phase = 0.0 + var timeStep = 1.0 / SAMPLE_RATE + + for i in range(totalSamples): + var time = float(i) * timeStep + var normalized_time = time / duration + var index_f = normalized_time * (num_points - 1) + var index = int(index_f) + var frac = index_f - index + var freq: float + + if index >= num_points - 1: + freq = frequencies[num_points - 1] + else: + freq = lerp(frequencies[index], frequencies[index + 1], frac) + + phase += TWO_PI * freq * timeStep + + var sample = sin(phase) + samples.append(sample) + + return samples + +#func save_wav(path: String, samples: PackedFloat32Array) -> void: + #var file = FileAccess.open(path, FileAccess.WRITE) + #var byte_data = PackedByteArray() +# + #var num_samples = samples.size() + #var data_chunk_size = num_samples * 2 + #var file_size = 44 + data_chunk_size - 8 +# + ## WAV Header + #byte_data.append_array("RIFF".to_ascii_buffer()) + #byte_data.append_array(to_little_endian(file_size, 4)) + #byte_data.append_array("WAVEfmt ".to_ascii_buffer()) + #byte_data.append_array(to_little_endian(16, 4)) # PCM header size + #byte_data.append_array(to_little_endian(1, 2)) # PCM format + #byte_data.append_array(to_little_endian(1, 2)) + #byte_data.append_array(to_little_endian(SAMPLE_RATE, 4)) + #byte_data.append_array(to_little_endian(SAMPLE_RATE * 1 * 16 / 8, 4)) # Byte rate + #byte_data.append_array(to_little_endian(1 * 16 / 8, 2)) # Block align + #byte_data.append_array(to_little_endian(16, 2)) + #byte_data.append_array("data".to_ascii_buffer()) + #byte_data.append_array(to_little_endian(data_chunk_size, 4)) +# + ## Sample data + #for sample in samples: + #var int_sample = int(clamp(sample * 32767, -32767, 32767)) + #byte_data.append_array(to_little_endian(int_sample, 2)) +# + #file.store_buffer(byte_data) + #file.close() +# +#func to_little_endian(value: int, byte_count: int) -> PackedByteArray: + #var ba = PackedByteArray() + #for i in range(byte_count): + #ba.append((value >> (8 * i)) & 0xFF) + #return ba diff --git a/src/Encoder.gd.uid b/src/Encoder.gd.uid new file mode 100644 index 0000000..044a906 --- /dev/null +++ b/src/Encoder.gd.uid @@ -0,0 +1 @@ +uid://3qtsqt2dj0je diff --git a/src/Martin1.gd b/src/Martin1.gd new file mode 100644 index 0000000..75962d2 --- /dev/null +++ b/src/Martin1.gd @@ -0,0 +1,50 @@ +extends SSTVEncoder +class_name Martin1 + +const BLANKING_PULSE_FREQ = 1500.0 +const COLOR_FREQ_MULT = 3.1372549 + +func get_rgb_value_as_freq(image: Image, scan_line: int, vert_pos: int) -> Array: + #var index = scan_line * (vertResolution * 4) + vert_pos * 4 + var color = image.get_pixel(vert_pos, scan_line) + var red = float(color.r8) * COLOR_FREQ_MULT + 1500.0 + var green = float(color.g8) * COLOR_FREQ_MULT + 1500.0 + var blue = float(color.b8) * COLOR_FREQ_MULT + 1500.0 + return [red, green, blue] + +var numScanLines = 256.0 +var vertResolution = 320.0 +var blankingInterval = 0.000572 +var scanLineLength = 0.146432 +var syncPulseLength = 0.004862 +var preparedImage = [] + +func PrepareImage(image: Image): + image.resize(vertResolution, numScanLines) + image.convert(Image.FORMAT_RGB8) + + for scanLine in range(0, numScanLines): + var red = [] + var green = [] + var blue = [] + for vertPos in range(0, vertResolution): + var freqs = get_rgb_value_as_freq(image, scanLine, vertPos) + red.push_back(freqs[0]); + green.push_back(freqs[1]); + blue.push_back(freqs[2]); + preparedImage.push_back([green, blue, red]); + +func EncodeSSTV(image: Image): + PrepareImage(image) + + EncodePrefix() + EncodeHeader([false, true, false, true, true, false, false]) + + for scanLine in range(0, numScanLines): + outputFloats.append_array(GenerateTone(SYNC_PULSE_FREQ, syncPulseLength)) + outputFloats.append_array(GenerateTone(BLANKING_PULSE_FREQ, blankingInterval)) + for dataLine in range(0, 3): + outputFloats.append_array(GenerateToneFromCurve(preparedImage[scanLine][dataLine], scanLineLength)) + outputFloats.append_array(GenerateTone(BLANKING_PULSE_FREQ, blankingInterval)) + + return outputFloats diff --git a/src/Martin1.gd.uid b/src/Martin1.gd.uid new file mode 100644 index 0000000..b62f903 --- /dev/null +++ b/src/Martin1.gd.uid @@ -0,0 +1 @@ +uid://cprn0ne0v2x2u diff --git a/src/RobotBW8.gd b/src/RobotBW8.gd new file mode 100644 index 0000000..ee85bf1 --- /dev/null +++ b/src/RobotBW8.gd @@ -0,0 +1,41 @@ +extends SSTVEncoder +class_name RobotBW8 + +var preparedImage: Array + +const BLACK = 1500.0 +const WHITE = 2300.0 +const COLOR_FREQ_MULT = 3.1372549 +const BLANKING_PULSE_FREQ = 1500.0 + +const SYNC_PULSE = 1200.0 + +var scanLineLength = 0.056 +var syncPulseLength = 0.01 +var blankingInterval = 0.000572 +var numScanLines = 120 + +func PrepareImage(image: Image): + image.resize(160, numScanLines) + image.convert(Image.FORMAT_RGB8) + + for h in range(image.get_height()): + var line = PackedFloat32Array() + for w in range(image.get_width()): + var color = image.get_pixel(w, h) + line.push_back(round((color.r8 + color.g8 + color.b8) / 3) * COLOR_FREQ_MULT + BLACK) + preparedImage.push_back(line) + +func EncodeSSTV(image: Image): + PrepareImage(image) + + EncodePrefix() + EncodeHeader([false, false, false, false, false, true, false]) + + for scanLine in range(0, numScanLines): + outputFloats.append_array(GenerateTone(SYNC_PULSE_FREQ, syncPulseLength)) + outputFloats.append_array(GenerateTone(BLANKING_PULSE_FREQ, blankingInterval)) + outputFloats.append_array(GenerateToneFromCurve(preparedImage[scanLine], scanLineLength)) + outputFloats.append_array(GenerateTone(BLANKING_PULSE_FREQ, blankingInterval)) + + return outputFloats diff --git a/src/RobotBW8.gd.uid b/src/RobotBW8.gd.uid new file mode 100644 index 0000000..bb9dead --- /dev/null +++ b/src/RobotBW8.gd.uid @@ -0,0 +1 @@ +uid://bqk3kugv2vy3f diff --git a/tests/checker.png b/tests/checker.png new file mode 100644 index 0000000..0a55936 Binary files /dev/null and b/tests/checker.png differ diff --git a/tests/checker.png.import b/tests/checker.png.import new file mode 100644 index 0000000..99d5ccb --- /dev/null +++ b/tests/checker.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doprjuugw7ga7" +path="res://.godot/imported/checker.png-6bb199bedbd039461e4248c1d0b9691d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://checker.png" +dest_files=["res://.godot/imported/checker.png-6bb199bedbd039461e4248c1d0b9691d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tests/spiral.png b/tests/spiral.png new file mode 100644 index 0000000..1fd6301 Binary files /dev/null and b/tests/spiral.png differ diff --git a/tests/spiral.png.import b/tests/spiral.png.import new file mode 100644 index 0000000..5c95021 --- /dev/null +++ b/tests/spiral.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7ayqic8e3l60" +path="res://.godot/imported/spiral.png-4b65a5988dfd3c48a98523c1603a55da.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://spiral.png" +dest_files=["res://.godot/imported/spiral.png-4b65a5988dfd3c48a98523c1603a55da.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tests/testtest.png b/tests/testtest.png new file mode 100644 index 0000000..784ca15 Binary files /dev/null and b/tests/testtest.png differ diff --git a/tests/testtest.png.import b/tests/testtest.png.import new file mode 100644 index 0000000..ea2646d --- /dev/null +++ b/tests/testtest.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b44c511u6oile" +path="res://.godot/imported/testtest.png-3138b9f8c3bb6f85feeb295d99c6a7a2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://testtest.png" +dest_files=["res://.godot/imported/testtest.png-3138b9f8c3bb6f85feeb295d99c6a7a2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tests/testtest2.png b/tests/testtest2.png new file mode 100644 index 0000000..9f571b2 Binary files /dev/null and b/tests/testtest2.png differ diff --git a/tests/testtest2.png.import b/tests/testtest2.png.import new file mode 100644 index 0000000..867e061 --- /dev/null +++ b/tests/testtest2.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://t5rtcbk8u7yo" +path="res://.godot/imported/testtest2.png-939c8d08bcfd841ad5a1a682e0766d88.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://testtest2.png" +dest_files=["res://.godot/imported/testtest2.png-939c8d08bcfd841ad5a1a682e0766d88.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tests/testtest3.png b/tests/testtest3.png new file mode 100644 index 0000000..18cc8be Binary files /dev/null and b/tests/testtest3.png differ diff --git a/tests/testtest3.png.import b/tests/testtest3.png.import new file mode 100644 index 0000000..ab96a3c --- /dev/null +++ b/tests/testtest3.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqyo3c1lqmrf5" +path="res://.godot/imported/testtest3.png-6018875a4a62a4fd97780d49f559d924.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://testtest3.png" +dest_files=["res://.godot/imported/testtest3.png-6018875a4a62a4fd97780d49f559d924.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1