Initial Commit

This commit is contained in:
Holly Stubbs 2025-04-24 09:11:42 +01:00
commit 3a9fa29ca2
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
22 changed files with 527 additions and 0 deletions

1
icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>

After

Width:  |  Height:  |  Size: 994 B

37
icon.svg.import Normal file
View file

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

30
node_3d.gd Normal file
View file

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

1
node_3d.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bx4xg8k3m1wos

56
node_3d.tscn Normal file
View file

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

16
project.godot Normal file
View file

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

122
src/Encoder.gd Normal file
View file

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

1
src/Encoder.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://3qtsqt2dj0je

50
src/Martin1.gd Normal file
View file

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

1
src/Martin1.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://cprn0ne0v2x2u

41
src/RobotBW8.gd Normal file
View file

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

1
src/RobotBW8.gd.uid Normal file
View file

@ -0,0 +1 @@
uid://bqk3kugv2vy3f

BIN
tests/checker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

34
tests/checker.png.import Normal file
View file

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

BIN
tests/spiral.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

34
tests/spiral.png.import Normal file
View file

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

BIN
tests/testtest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

34
tests/testtest.png.import Normal file
View file

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

BIN
tests/testtest2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

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

BIN
tests/testtest3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

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