Compare commits

..

No commits in common. "main" and "with-banner" have entirely different histories.

32 changed files with 21 additions and 1546 deletions

View File

@ -1,9 +1,12 @@
# CW Generator # CW Generator
A simple application for generating self-made morse / CW audio files. All it features is a button as A simple application for generating self-made morse / CW audio files. All it features is a button as
a morse key and allows you to export this as a WAV. Optionally, a MIDI keyboard can be used as a a morse key and allows you to export this as a WAV. Optionally, a MIDI keyboard can be used as a
morse key (since godot 4.4 even on web). morse key.
## Known Limitations / TODO ## Known Limitations / TODO
MIDI support only works locally for now.
* webmidi might be an option: https://gist.github.com/srejv/b7198e25587e2d8e0a66860781b56852
Currently we can only export wav, as Godot itself doesn't include any encoders. Having the ability Currently we can only export wav, as Godot itself doesn't include any encoders. Having the ability
to export as mp3, ogg or opus would be nice, but external libraries are kind of a hassle. opus would to export as mp3, ogg or opus would be nice, but external libraries are kind of a hassle. opus would
be the nicest format, but doesn't seem to work with older Iphones. be the nicest format, but doesn't seem to work with older Iphones.

View File

@ -1,80 +0,0 @@
#
# © 2024-present https://github.com/cengiz-pz
#
@tool
class_name Share
extends Node
const PLUGIN_SINGLETON_NAME: String = "SharePlugin"
const MIME_TYPE_TEXT: String = "text/plain"
const MIME_TYPE_IMAGE: String = "image/*"
@onready var _temp_image_path: String = OS.get_user_data_dir() + "/tmp_share_img_path.png"
var _plugin_singleton: Object
func _ready() -> void:
if OS.get_name() == "Android":
_update_plugin()
func _notification(a_what: int) -> void:
if a_what == NOTIFICATION_APPLICATION_RESUMED:
_update_plugin()
func _update_plugin() -> void:
if _plugin_singleton == null:
if Engine.has_singleton(PLUGIN_SINGLETON_NAME):
_plugin_singleton = Engine.get_singleton(PLUGIN_SINGLETON_NAME)
else:
printerr("%s singleton not found!" % PLUGIN_SINGLETON_NAME)
func share_text(a_title: String, a_subject: String, a_content: String) -> void:
if _plugin_singleton != null:
_plugin_singleton.share(
SharedData.new()
.set_title(a_title)
.set_subject(a_subject)
.set_content(a_content)
.set_mime_type(MIME_TYPE_TEXT)
.get_raw_data()
)
else:
printerr("%s plugin not initialized" % PLUGIN_SINGLETON_NAME)
func share_image(a_path: String, a_title: String, a_subject: String, a_content: String) -> void:
share_file(a_path, MIME_TYPE_IMAGE, a_title, a_subject, a_content)
func share_texture(a_texture: Texture2D, a_title: String, a_subject: String, a_content: String) -> void:
var __image: Image = a_texture.get_image()
__image.save_png(_temp_image_path)
share_file(_temp_image_path, MIME_TYPE_IMAGE, a_title, a_subject, a_content)
func share_viewport(a_viewport: Viewport, a_title: String, a_subject: String, a_content: String, a_flip_y: bool = false) -> void:
var __image: Image = a_viewport.get_texture().get_image()
if a_flip_y:
__image.flip_y()
__image.save_png(_temp_image_path)
share_file(_temp_image_path, MIME_TYPE_IMAGE, a_title, a_subject, a_content)
func share_file(a_path: String, a_mime_type: String, a_title: String, a_subject: String, a_content: String) -> void:
if _plugin_singleton != null:
_plugin_singleton.share(
SharedData.new()
.set_title(a_title)
.set_subject(a_subject)
.set_content(a_content)
.set_mime_type(a_mime_type)
.set_file_path(a_path)
.get_raw_data()
)
else:
printerr("%s plugin not initialized" % PLUGIN_SINGLETON_NAME)

View File

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

View File

@ -1,65 +0,0 @@
#
# © 2024-present https://github.com/cengiz-pz
#
@tool
extends EditorPlugin
const PLUGIN_NODE_TYPE_NAME = "Share"
const PLUGIN_PARENT_NODE_TYPE = "Node"
const PLUGIN_NAME: String = "SharePlugin"
const PLUGIN_VERSION: String = "3.0"
const PLUGIN_PACKAGE: String = "org.godotengine.plugin.android.share"
const PLUGIN_DEPENDENCIES: Array = [ "androidx.appcompat:appcompat:1.7.0" ]
const PROVIDER_TAG = """
<provider android:name="%s.ShareFileProvider"
android:exported="false"
android:authorities="%s.sharefileprovider"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths"/>
</provider>
"""
var export_plugin: AndroidExportPlugin
func _enter_tree() -> void:
add_custom_type(PLUGIN_NODE_TYPE_NAME, PLUGIN_PARENT_NODE_TYPE, preload("Share.gd"), preload("icon.png"))
export_plugin = AndroidExportPlugin.new()
add_export_plugin(export_plugin)
func _exit_tree() -> void:
remove_custom_type(PLUGIN_NODE_TYPE_NAME)
remove_export_plugin(export_plugin)
export_plugin = null
class AndroidExportPlugin extends EditorExportPlugin:
var _plugin_name = PLUGIN_NAME
func _supports_platform(platform: EditorExportPlatform) -> bool:
if platform is EditorExportPlatformAndroid:
return true
return false
func _get_android_libraries(platform: EditorExportPlatform, debug: bool) -> PackedStringArray:
if debug:
return PackedStringArray(["%s/bin/debug/%s-%s-debug.aar" % [_plugin_name, _plugin_name, PLUGIN_VERSION]])
else:
return PackedStringArray(["%s/bin/release/%s-%s-release.aar" % [_plugin_name, _plugin_name, PLUGIN_VERSION]])
func _get_name() -> String:
return _plugin_name
func _get_android_dependencies(platform: EditorExportPlatform, debug: bool) -> PackedStringArray:
return PackedStringArray(PLUGIN_DEPENDENCIES)
func _get_android_manifest_application_element_contents(platform: EditorExportPlatform, debug: bool) -> String:
return PROVIDER_TAG % [PLUGIN_PACKAGE, get_option("package/unique_name")]

View File

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

BIN
addons/SharePlugin/icon.png (Stored with Git LFS)

Binary file not shown.

View File

@ -1,34 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cm4eejjdmpyec"
path="res://.godot/imported/icon.png-cbd45f0ce843eb69e82e7474c8559974.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/SharePlugin/icon.png"
dest_files=["res://.godot/imported/icon.png-cbd45f0ce843eb69e82e7474c8559974.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

View File

@ -1,47 +0,0 @@
#
# © 2024-present https://github.com/cengiz-pz
#
class_name SharedData
extends RefCounted
const DATA_KEY_TITLE = "title"
const DATA_KEY_SUBJECT = "subject"
const DATA_KEY_CONTENT = "content"
const DATA_KEY_FILE_PATH = "file_path"
const DATA_KEY_MIME_TYPE = "mime_type"
var _data: Dictionary
func _init() -> void:
_data = {}
func set_title(a_title: String) -> SharedData:
_data[DATA_KEY_TITLE] = a_title
return self
func set_subject(a_subject: String) -> SharedData:
_data[DATA_KEY_SUBJECT] = a_subject
return self
func set_content(a_content: String) -> SharedData:
_data[DATA_KEY_CONTENT] = a_content
return self
func set_file_path(a_file_path: String) -> SharedData:
_data[DATA_KEY_FILE_PATH] = a_file_path
return self
func set_mime_type(a_mime_type: String) -> SharedData:
_data[DATA_KEY_MIME_TYPE] = a_mime_type
return self
func get_raw_data() -> Dictionary:
return _data

View File

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

View File

@ -1,10 +0,0 @@
;
; © 2024-present https://github.com/cengiz-pz
;
[plugin]
name="Share"
description="Allow sharing of text or images with other apps"
author="Cengiz"
version="3.0"
script="ShareExportPlugin.gd"

View File

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

View File

@ -1,9 +0,0 @@
extends Node
var first_connect_done := false
func get_external_freq_param():
match OS.get_name():
"Web":
return JavaScriptBridge.eval("new URL(window.location.href).searchParams.get('freq')")
return null

View File

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

View File

@ -10,10 +10,8 @@ export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="export/android/cwgenerator.apk" export_path="export/android/cwgenerator.apk"
patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
seed=0
encrypt_pck=false encrypt_pck=false
encrypt_directory=false encrypt_directory=false
script_export_mode=2 script_export_mode=2
@ -47,10 +45,8 @@ package/show_as_launcher_app=false
launcher_icons/main_192x192="" launcher_icons/main_192x192=""
launcher_icons/adaptive_foreground_432x432="" launcher_icons/adaptive_foreground_432x432=""
launcher_icons/adaptive_background_432x432="" launcher_icons/adaptive_background_432x432=""
launcher_icons/adaptive_monochrome_432x432=""
graphics/opengl_debug=false graphics/opengl_debug=false
xr_features/xr_mode=0 xr_features/xr_mode=0
gesture/swipe_to_dismiss=false
screen/immersive_mode=false screen/immersive_mode=false
screen/support_small=true screen/support_small=true
screen/support_normal=true screen/support_normal=true
@ -66,7 +62,6 @@ permissions/access_checkin_properties=false
permissions/access_coarse_location=false permissions/access_coarse_location=false
permissions/access_fine_location=false permissions/access_fine_location=false
permissions/access_location_extra_commands=false permissions/access_location_extra_commands=false
permissions/access_media_location=false
permissions/access_mock_location=false permissions/access_mock_location=false
permissions/access_network_state=false permissions/access_network_state=false
permissions/access_surface_flinger=false permissions/access_surface_flinger=false
@ -129,7 +124,7 @@ permissions/install_location_provider=false
permissions/install_packages=false permissions/install_packages=false
permissions/install_shortcut=false permissions/install_shortcut=false
permissions/internal_system_window=false permissions/internal_system_window=false
permissions/internet=true permissions/internet=false
permissions/kill_background_processes=false permissions/kill_background_processes=false
permissions/location_hardware=false permissions/location_hardware=false
permissions/manage_accounts=false permissions/manage_accounts=false
@ -154,10 +149,6 @@ permissions/read_frame_buffer=false
permissions/read_history_bookmarks=false permissions/read_history_bookmarks=false
permissions/read_input_state=false permissions/read_input_state=false
permissions/read_logs=false permissions/read_logs=false
permissions/read_media_audio=false
permissions/read_media_images=false
permissions/read_media_video=false
permissions/read_media_visual_user_selected=false
permissions/read_phone_state=false permissions/read_phone_state=false
permissions/read_profile=false permissions/read_profile=false
permissions/read_sms=false permissions/read_sms=false
@ -227,10 +218,8 @@ export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="export/web/index.html" export_path="export/web/index.html"
patches=PackedStringArray()
encryption_include_filters="" encryption_include_filters=""
encryption_exclude_filters="" encryption_exclude_filters=""
seed=0
encrypt_pck=false encrypt_pck=false
encrypt_directory=false encrypt_directory=false
script_export_mode=2 script_export_mode=2
@ -258,367 +247,3 @@ progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180="" progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512="" progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color(0, 0, 0, 1) progressive_web_app/background_color=Color(0, 0, 0, 1)
[preset.2]
name="Linux"
platform="Linux"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="export/lin/cwgenerator.x86_64"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.2.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
export DISPLAY=:0
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
[preset.3]
name="macOS"
platform="macOS"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="export/mac/cwgenerator.zip"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.3.options]
export/distribution_type=1
binary_format/architecture="universal"
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
application/icon=""
application/icon_interpolation=4
application/bundle_identifier="de.sebageek.cwgenerator"
application/signature=""
application/app_category="Games"
application/short_version=""
application/version=""
application/copyright=""
application/copyright_localized={}
application/min_macos_version_x86_64="10.12"
application/min_macos_version_arm64="11.00"
application/export_angle=0
display/high_res=true
application/additional_plist_content=""
xcode/platform_build="14C18"
xcode/sdk_version="13.1"
xcode/sdk_build="22C55"
xcode/sdk_name="macosx13.1"
xcode/xcode_version="1420"
xcode/xcode_build="14C18"
codesign/codesign=1
codesign/installer_identity=""
codesign/apple_team_id=""
codesign/identity=""
codesign/entitlements/custom_file=""
codesign/entitlements/allow_jit_code_execution=false
codesign/entitlements/allow_unsigned_executable_memory=false
codesign/entitlements/allow_dyld_environment_variables=false
codesign/entitlements/disable_library_validation=false
codesign/entitlements/audio_input=false
codesign/entitlements/camera=false
codesign/entitlements/location=false
codesign/entitlements/address_book=false
codesign/entitlements/calendars=false
codesign/entitlements/photos_library=false
codesign/entitlements/apple_events=false
codesign/entitlements/debugging=false
codesign/entitlements/app_sandbox/enabled=false
codesign/entitlements/app_sandbox/network_server=false
codesign/entitlements/app_sandbox/network_client=false
codesign/entitlements/app_sandbox/device_usb=false
codesign/entitlements/app_sandbox/device_bluetooth=false
codesign/entitlements/app_sandbox/files_downloads=0
codesign/entitlements/app_sandbox/files_pictures=0
codesign/entitlements/app_sandbox/files_music=0
codesign/entitlements/app_sandbox/files_movies=0
codesign/entitlements/app_sandbox/files_user_selected=0
codesign/entitlements/app_sandbox/helper_executables=[]
codesign/entitlements/additional=""
codesign/custom_options=PackedStringArray()
notarization/notarization=0
privacy/microphone_usage_description=""
privacy/microphone_usage_description_localized={}
privacy/camera_usage_description=""
privacy/camera_usage_description_localized={}
privacy/location_usage_description=""
privacy/location_usage_description_localized={}
privacy/address_book_usage_description=""
privacy/address_book_usage_description_localized={}
privacy/calendar_usage_description=""
privacy/calendar_usage_description_localized={}
privacy/photos_library_usage_description=""
privacy/photos_library_usage_description_localized={}
privacy/desktop_folder_usage_description=""
privacy/desktop_folder_usage_description_localized={}
privacy/documents_folder_usage_description=""
privacy/documents_folder_usage_description_localized={}
privacy/downloads_folder_usage_description=""
privacy/downloads_folder_usage_description_localized={}
privacy/network_volumes_usage_description=""
privacy/network_volumes_usage_description_localized={}
privacy/removable_volumes_usage_description=""
privacy/removable_volumes_usage_description_localized={}
privacy/tracking_enabled=false
privacy/tracking_domains=PackedStringArray()
privacy/collected_data/name/collected=false
privacy/collected_data/name/linked_to_user=false
privacy/collected_data/name/used_for_tracking=false
privacy/collected_data/name/collection_purposes=0
privacy/collected_data/email_address/collected=false
privacy/collected_data/email_address/linked_to_user=false
privacy/collected_data/email_address/used_for_tracking=false
privacy/collected_data/email_address/collection_purposes=0
privacy/collected_data/phone_number/collected=false
privacy/collected_data/phone_number/linked_to_user=false
privacy/collected_data/phone_number/used_for_tracking=false
privacy/collected_data/phone_number/collection_purposes=0
privacy/collected_data/physical_address/collected=false
privacy/collected_data/physical_address/linked_to_user=false
privacy/collected_data/physical_address/used_for_tracking=false
privacy/collected_data/physical_address/collection_purposes=0
privacy/collected_data/other_contact_info/collected=false
privacy/collected_data/other_contact_info/linked_to_user=false
privacy/collected_data/other_contact_info/used_for_tracking=false
privacy/collected_data/other_contact_info/collection_purposes=0
privacy/collected_data/health/collected=false
privacy/collected_data/health/linked_to_user=false
privacy/collected_data/health/used_for_tracking=false
privacy/collected_data/health/collection_purposes=0
privacy/collected_data/fitness/collected=false
privacy/collected_data/fitness/linked_to_user=false
privacy/collected_data/fitness/used_for_tracking=false
privacy/collected_data/fitness/collection_purposes=0
privacy/collected_data/payment_info/collected=false
privacy/collected_data/payment_info/linked_to_user=false
privacy/collected_data/payment_info/used_for_tracking=false
privacy/collected_data/payment_info/collection_purposes=0
privacy/collected_data/credit_info/collected=false
privacy/collected_data/credit_info/linked_to_user=false
privacy/collected_data/credit_info/used_for_tracking=false
privacy/collected_data/credit_info/collection_purposes=0
privacy/collected_data/other_financial_info/collected=false
privacy/collected_data/other_financial_info/linked_to_user=false
privacy/collected_data/other_financial_info/used_for_tracking=false
privacy/collected_data/other_financial_info/collection_purposes=0
privacy/collected_data/precise_location/collected=false
privacy/collected_data/precise_location/linked_to_user=false
privacy/collected_data/precise_location/used_for_tracking=false
privacy/collected_data/precise_location/collection_purposes=0
privacy/collected_data/coarse_location/collected=false
privacy/collected_data/coarse_location/linked_to_user=false
privacy/collected_data/coarse_location/used_for_tracking=false
privacy/collected_data/coarse_location/collection_purposes=0
privacy/collected_data/sensitive_info/collected=false
privacy/collected_data/sensitive_info/linked_to_user=false
privacy/collected_data/sensitive_info/used_for_tracking=false
privacy/collected_data/sensitive_info/collection_purposes=0
privacy/collected_data/contacts/collected=false
privacy/collected_data/contacts/linked_to_user=false
privacy/collected_data/contacts/used_for_tracking=false
privacy/collected_data/contacts/collection_purposes=0
privacy/collected_data/emails_or_text_messages/collected=false
privacy/collected_data/emails_or_text_messages/linked_to_user=false
privacy/collected_data/emails_or_text_messages/used_for_tracking=false
privacy/collected_data/emails_or_text_messages/collection_purposes=0
privacy/collected_data/photos_or_videos/collected=false
privacy/collected_data/photos_or_videos/linked_to_user=false
privacy/collected_data/photos_or_videos/used_for_tracking=false
privacy/collected_data/photos_or_videos/collection_purposes=0
privacy/collected_data/audio_data/collected=false
privacy/collected_data/audio_data/linked_to_user=false
privacy/collected_data/audio_data/used_for_tracking=false
privacy/collected_data/audio_data/collection_purposes=0
privacy/collected_data/gameplay_content/collected=false
privacy/collected_data/gameplay_content/linked_to_user=false
privacy/collected_data/gameplay_content/used_for_tracking=false
privacy/collected_data/gameplay_content/collection_purposes=0
privacy/collected_data/customer_support/collected=false
privacy/collected_data/customer_support/linked_to_user=false
privacy/collected_data/customer_support/used_for_tracking=false
privacy/collected_data/customer_support/collection_purposes=0
privacy/collected_data/other_user_content/collected=false
privacy/collected_data/other_user_content/linked_to_user=false
privacy/collected_data/other_user_content/used_for_tracking=false
privacy/collected_data/other_user_content/collection_purposes=0
privacy/collected_data/browsing_history/collected=false
privacy/collected_data/browsing_history/linked_to_user=false
privacy/collected_data/browsing_history/used_for_tracking=false
privacy/collected_data/browsing_history/collection_purposes=0
privacy/collected_data/search_hhistory/collected=false
privacy/collected_data/search_hhistory/linked_to_user=false
privacy/collected_data/search_hhistory/used_for_tracking=false
privacy/collected_data/search_hhistory/collection_purposes=0
privacy/collected_data/user_id/collected=false
privacy/collected_data/user_id/linked_to_user=false
privacy/collected_data/user_id/used_for_tracking=false
privacy/collected_data/user_id/collection_purposes=0
privacy/collected_data/device_id/collected=false
privacy/collected_data/device_id/linked_to_user=false
privacy/collected_data/device_id/used_for_tracking=false
privacy/collected_data/device_id/collection_purposes=0
privacy/collected_data/purchase_history/collected=false
privacy/collected_data/purchase_history/linked_to_user=false
privacy/collected_data/purchase_history/used_for_tracking=false
privacy/collected_data/purchase_history/collection_purposes=0
privacy/collected_data/product_interaction/collected=false
privacy/collected_data/product_interaction/linked_to_user=false
privacy/collected_data/product_interaction/used_for_tracking=false
privacy/collected_data/product_interaction/collection_purposes=0
privacy/collected_data/advertising_data/collected=false
privacy/collected_data/advertising_data/linked_to_user=false
privacy/collected_data/advertising_data/used_for_tracking=false
privacy/collected_data/advertising_data/collection_purposes=0
privacy/collected_data/other_usage_data/collected=false
privacy/collected_data/other_usage_data/linked_to_user=false
privacy/collected_data/other_usage_data/used_for_tracking=false
privacy/collected_data/other_usage_data/collection_purposes=0
privacy/collected_data/crash_data/collected=false
privacy/collected_data/crash_data/linked_to_user=false
privacy/collected_data/crash_data/used_for_tracking=false
privacy/collected_data/crash_data/collection_purposes=0
privacy/collected_data/performance_data/collected=false
privacy/collected_data/performance_data/linked_to_user=false
privacy/collected_data/performance_data/used_for_tracking=false
privacy/collected_data/performance_data/collection_purposes=0
privacy/collected_data/other_diagnostic_data/collected=false
privacy/collected_data/other_diagnostic_data/linked_to_user=false
privacy/collected_data/other_diagnostic_data/used_for_tracking=false
privacy/collected_data/other_diagnostic_data/collection_purposes=0
privacy/collected_data/environment_scanning/collected=false
privacy/collected_data/environment_scanning/linked_to_user=false
privacy/collected_data/environment_scanning/used_for_tracking=false
privacy/collected_data/environment_scanning/collection_purposes=0
privacy/collected_data/hands/collected=false
privacy/collected_data/hands/linked_to_user=false
privacy/collected_data/hands/used_for_tracking=false
privacy/collected_data/hands/collection_purposes=0
privacy/collected_data/head/collected=false
privacy/collected_data/head/linked_to_user=false
privacy/collected_data/head/used_for_tracking=false
privacy/collected_data/head/collection_purposes=0
privacy/collected_data/other_data_types/collected=false
privacy/collected_data/other_data_types/linked_to_user=false
privacy/collected_data/other_data_types/used_for_tracking=false
privacy/collected_data/other_data_types/collection_purposes=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
open \"{temp_dir}/{exe_name}.app\" --args {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
kill $(pgrep -x -f \"{temp_dir}/{exe_name}.app/Contents/MacOS/{exe_name} {cmd_args}\")
rm -rf \"{temp_dir}\""
application/min_macos_version="10.12"
[preset.4]
name="Windows Desktop"
platform="Windows Desktop"
runnable=true
advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="export/win/cwgenerator.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
[preset.4.options]
custom_template/debug=""
custom_template/release=""
debug/export_console_wrapper=1
binary_format/embed_pck=false
texture_format/s3tc_bptc=true
texture_format/etc2_astc=false
binary_format/architecture="x86_64"
codesign/enable=false
codesign/timestamp=true
codesign/timestamp_server_url=""
codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
application/icon=""
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
application/product_version=""
application/company_name=""
application/product_name=""
application/file_description=""
application/copyright=""
application/trademarks=""
application/export_angle=0
application/export_d3d12=0
application/d3d12_agility_sdk_multiarch=true
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
ssh_remote_deploy/port="22"
ssh_remote_deploy/extra_args_ssh=""
ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
$settings = New-ScheduledTaskSettingsSet
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
Start-ScheduledTask -TaskName godot_remote_debug
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
Remove-Item -Recurse -Force '{temp_dir}'"

View File

@ -2,7 +2,7 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bkoamufjn5wa1" uid="uid://dvaugiwdmfmge"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false

View File

@ -11,16 +11,14 @@ config_version=5
[application] [application]
config/name="cw generator" config/name="cw generator"
config/version="0.1.0"
run/main_scene="res://scenes/main.tscn" run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.4", "Mobile") config/features=PackedStringArray("4.3", "Mobile")
run/max_fps=120 run/max_fps=120
config/icon="res://icon.svg" config/icon="res://icon.svg"
[autoload] [autoload]
MorseState="*res://autoloads/morse_state.gd" MorseState="*res://autoloads/morse_state.gd"
Utils="*res://autoloads/utils.gd"
[display] [display]

View File

@ -1,4 +0,0 @@
[gd_resource type="Theme" format=3 uid="uid://xxoc27tvaiut"]
[resource]
default_font_size = 30

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://xqic6oa5d7oc"] [gd_scene load_steps=2 format=3 uid="uid://xqic6oa5d7oc"]
[ext_resource type="Script" uid="uid://j1oei8suq5sj" path="res://scenes/morse_banner.gd" id="1_475pl"] [ext_resource type="Script" path="res://scenes/morse_banner.gd" id="1_475pl"]
[node name="MorseBanner" type="Control"] [node name="MorseBanner" type="Control"]
custom_minimum_size = Vector2(200, 100) custom_minimum_size = Vector2(200, 100)

View File

@ -1,21 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://ug3u6jf36dst"]
[ext_resource type="Script" uid="uid://b1k6j1jti114u" path="res://scenes/multi_morse_banner.gd" id="1_a1ve8"]
[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_a1ve8"]
mix_rate = 22050.0
[node name="MorseBanner" type="Control"]
custom_minimum_size = Vector2(200, 100)
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_a1ve8")
[node name="Player" type="AudioStreamPlayer" parent="."]
stream = SubResource("AudioStreamGenerator_a1ve8")
volume_db = -100.0
stream_paused = true

View File

@ -1,153 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://dnxcrx04kl3xy"]
[ext_resource type="Theme" uid="uid://xxoc27tvaiut" path="res://scenes/GUITheme.tres" id="1_2wc0w"]
[ext_resource type="Script" uid="uid://di8r70441xdms" path="res://scenes/multiplayer_connect.gd" id="1_uyd8l"]
[node name="MultiplayerConnect" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_2wc0w")
script = ExtResource("1_uyd8l")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ConnectView" type="Control" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ConnectView"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ConnectView/VBoxContainer"]
layout_mode = 2
[node name="FrequencyCreator" type="TextEdit" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "430.200"
[node name="Label" type="Label" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "MHz"
[node name="CreateButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(0, 100)
layout_mode = 2
size_flags_stretch_ratio = 2.41
text = "Create Frequency"
[node name="BackButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 10
text = "Back"
[node name="RefreshButton" type="Button" parent="VBoxContainer/ConnectView/VBoxContainer"]
layout_mode = 2
text = "Refresh"
[node name="FreqList" type="ItemList" parent="VBoxContainer/ConnectView/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="MorseView" type="Control" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/MorseView"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Freq:"
[node name="FreqLabel" type="Label" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="LeaveButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 10
text = "Leave Frequency"
[node name="PlayerContainer" type="VBoxContainer" parent="VBoxContainer/MorseView/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="MorseButton" type="Button" parent="VBoxContainer/MorseView/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
size_flags_stretch_ratio = 2.0
text = "MORSE"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Status:"
[node name="StatusLabel" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Disconnected"
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer2"]
layout_mode = 2
mouse_filter = 0
text = "Last Error:"
[node name="ErrorLabel" type="Label" parent="VBoxContainer/HBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
mouse_filter = 0
text = "<None>"
autowrap_mode = 2
[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/CreateButton" to="." method="_on_create_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/HBoxContainer/BackButton" to="." method="_on_back_button_pressed"]
[connection signal="pressed" from="VBoxContainer/ConnectView/VBoxContainer/RefreshButton" to="." method="_on_refresh_button_pressed"]
[connection signal="item_clicked" from="VBoxContainer/ConnectView/VBoxContainer/FreqList" to="." method="_on_freq_list_join"]
[connection signal="pressed" from="VBoxContainer/MorseView/VBoxContainer/HBoxContainer/LeaveButton" to="." method="_on_leave_button_pressed"]
[connection signal="button_down" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_down"]
[connection signal="button_up" from="VBoxContainer/MorseView/VBoxContainer/MorseButton" to="." method="_on_morse_button_button_up"]
[connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/Label" to="." method="_on_error_label_gui_input"]
[connection signal="gui_input" from="VBoxContainer/HBoxContainer/HBoxContainer2/ErrorLabel" to="." method="_on_error_label_gui_input"]

View File

@ -6,7 +6,6 @@ var phase = 0.0
var morse_state := false var morse_state := false
var playback: AudioStreamPlayback = null var playback: AudioStreamPlayback = null
var vol_on := -30 var vol_on := -30
var multiplayer_enabled: bool = true # FIXME: maybe make this a tag?
func _process(_delta): func _process(_delta):
fill_buffer() fill_buffer()
@ -20,30 +19,14 @@ func fill_buffer():
phase = fmod(phase + increment, 1.0) phase = fmod(phase + increment, 1.0)
func _ready(): func _ready():
match OS.get_name():
"Android":
%WavButton.text = "Share Wav"
"Web":
%WavButton.text = "Download Wav"
$Player.stream.mix_rate = sample_hz $Player.stream.mix_rate = sample_hz
$Player.volume_db = -100 $Player.volume_db = -100
$Player.play() $Player.play()
playback = $Player.get_stream_playback() playback = $Player.get_stream_playback()
fill_buffer() fill_buffer()
if OS.get_name() != "Web": OS.open_midi_inputs()
OS.open_midi_inputs() print(OS.get_connected_midi_inputs())
print(OS.get_connected_midi_inputs())
else:
%MidiButton.visible = true
if multiplayer_enabled:
%MultiplayerButton.visible = true
if Utils.get_external_freq_param():
print("Direct connect to external freq: ", Utils.get_external_freq_param())
_on_multiplayer_button_pressed()
func set_morse_state(state: bool): func set_morse_state(state: bool):
MorseState.set_state(state) MorseState.set_state(state)
@ -116,7 +99,7 @@ func _on_wav_button_pressed() -> void:
# save wav # save wav
var wav := AudioStreamWAV.new() var wav := AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_8_BITS wav.format = AudioStreamWAV.FORMAT_8_BITS
wav.mix_rate = int(sample_hz) wav.mix_rate = sample_hz
wav.stereo = false wav.stereo = false
wav.data = data wav.data = data
@ -124,23 +107,8 @@ func _on_wav_button_pressed() -> void:
var proposed_fname := "morse-" + Time.get_datetime_string_from_system(true) + ".wav" var proposed_fname := "morse-" + Time.get_datetime_string_from_system(true) + ".wav"
match OS.get_name(): match OS.get_name():
"Linux", "macOS", "Windows": "Linux", "macOS", "Windows":
print("Save on ", OS.get_name()) wav.save_to_wav("/tmp/foo.wav")
print("GLobalized path: " + ProjectSettings.globalize_path("user://morse.wav"))
# create file dialog
var fd := FileDialog.new()
fd.title = "Save Wav file"
fd.use_native_dialog = true
fd.add_filter("*.wav", "WAV files")
fd.current_file = proposed_fname
var save_method := func _save(path: String):
print("Save as ", path)
wav.save_to_wav(path)
fd.file_selected.connect(save_method)
add_child(fd)
fd.popup_centered()
"Android": "Android":
print("Sharing file on android") print("Sharing file on android")
var tmp_file_path := OS.get_user_data_dir().path_join(proposed_fname) var tmp_file_path := OS.get_user_data_dir().path_join(proposed_fname)
@ -161,12 +129,3 @@ func _on_wav_button_pressed() -> void:
func _on_reset_button_pressed() -> void: func _on_reset_button_pressed() -> void:
MorseState.reset() MorseState.reset()
func _on_multiplayer_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/MultiplayerConnect.tscn")
func _on_midi_button_pressed() -> void:
OS.open_midi_inputs()
print(OS.get_connected_midi_inputs())

View File

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

View File

@ -1,9 +1,8 @@
[gd_scene load_steps=6 format=3 uid="uid://ctak1goemnnc5"] [gd_scene load_steps=5 format=3 uid="uid://ctak1goemnnc5"]
[ext_resource type="Script" uid="uid://dmeokosn7gr27" path="res://scenes/main.gd" id="1_8bx00"] [ext_resource type="Script" path="res://scenes/main.gd" id="1_8bx00"]
[ext_resource type="Theme" uid="uid://xxoc27tvaiut" path="res://scenes/GUITheme.tres" id="1_jyhfs"]
[ext_resource type="PackedScene" uid="uid://xqic6oa5d7oc" path="res://scenes/MorseBanner.tscn" id="2_v02md"] [ext_resource type="PackedScene" uid="uid://xqic6oa5d7oc" path="res://scenes/MorseBanner.tscn" id="2_v02md"]
[ext_resource type="Script" uid="uid://bjt60u6r1hqf7" path="res://addons/SharePlugin/Share.gd" id="3_sugp2"] [ext_resource type="Script" path="res://addons/SharePlugin/Share.gd" id="3_ci1yg"]
[sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_kvn5v"] [sub_resource type="AudioStreamGenerator" id="AudioStreamGenerator_kvn5v"]
mix_rate = 11025.0 mix_rate = 11025.0
@ -16,7 +15,6 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
theme = ExtResource("1_jyhfs")
script = ExtResource("1_8bx00") script = ExtResource("1_8bx00")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
@ -55,48 +53,24 @@ size_flags_stretch_ratio = 2.0
text = "MORSE" text = "MORSE"
[node name="WavButton" type="Button" parent="VBoxContainer"] [node name="WavButton" type="Button" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
text = "Save Wav" text = "Write Wav"
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] [node name="ResetButton" type="Button" parent="VBoxContainer"]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="MidiButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_stretch_ratio = 0.25
text = "Open
Midi"
[node name="ResetButton" type="Button" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
size_flags_stretch_ratio = 0.5 size_flags_stretch_ratio = 0.5
text = "Reset" text = "Reset"
[node name="MultiplayerButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.5
text = "Connect to Frequency"
[node name="Player" type="AudioStreamPlayer" parent="."] [node name="Player" type="AudioStreamPlayer" parent="."]
stream = SubResource("AudioStreamGenerator_kvn5v") stream = SubResource("AudioStreamGenerator_kvn5v")
volume_db = -80.0 volume_db = -80.0
[node name="Share" type="Node" parent="."] [node name="Share" type="Node" parent="."]
script = ExtResource("3_sugp2") script = ExtResource("3_ci1yg")
[connection signal="button_down" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_down"] [connection signal="button_down" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_down"]
[connection signal="button_up" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_up"] [connection signal="button_up" from="VBoxContainer/MorseButton" to="." method="_on_morse_button_up"]
[connection signal="pressed" from="VBoxContainer/WavButton" to="." method="_on_wav_button_pressed"] [connection signal="pressed" from="VBoxContainer/WavButton" to="." method="_on_wav_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/MidiButton" to="." method="_on_midi_button_pressed"] [connection signal="pressed" from="VBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/ResetButton" to="." method="_on_reset_button_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/MultiplayerButton" to="." method="_on_multiplayer_button_pressed"]

View File

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

View File

@ -1,111 +0,0 @@
@tool
class_name MultiMorseBanner
extends Control
static var sample_hz := 22050.0
static var vol_on := -30
var phase := 0.0
var num: int
var tone_hz: int
var color_on := Color(0, 128, 0)
var color_off := Color(0, 0, 0)
var last_delta := 0.0
@export_range(0.1, 120.0) var display_sec := 5.0
@export var stretch_display := false
var playback;
func _ready() -> void:
$Player.stream.mix_rate = sample_hz
$Player.volume_db = -100
$Player.play()
playback = $Player.get_stream_playback()
fill_buffer()
var morse_step_perc := 0.45
const MMB_SCENE := preload("res://scenes/MultiMorseBanner.tscn")
class LocalMorseState:
var states: Array[int] = []
var curr_state := false
var last_change: int = 0
var start_time := 0
func reset() -> void:
last_change = Time.get_ticks_msec()
start_time = last_change
states = []
func set_state(state: bool) -> void:
if state == curr_state:
return
curr_state = state
var now := Time.get_ticks_msec()
states.push_back(now - last_change)
last_change = now
var morse_state := LocalMorseState.new()
static func new_banner(num: int, color: Color, tone: int):
var mmb = MMB_SCENE.instantiate()
mmb.num = num
mmb.color_on = color
mmb.tone_hz = tone
return mmb
func _draw_morse_rect(x: float, width: float, state: bool):
var rect := Rect2(max(x, 0.0), morse_step_perc * size.y, width, (0.5 - morse_step_perc) * size.y)
draw_rect(rect, color_on if state else color_off, true, -1.0, true)
func _draw():
# black background
draw_rect(Rect2(0.0, 0.0, size.x, size.y), Color.BLACK)
# in editor we only want a black rectangle
if Engine.is_editor_hint():
return
var morse_on := morse_state.curr_state
var first_time := Time.get_ticks_msec() - morse_state.last_change
var curr_x := float(size.x)
var px_per_s := 0.0
if not stretch_display:
px_per_s = size.x / display_sec
else:
px_per_s = size.x / (Time.get_ticks_msec() - morse_state.start_time) * 1000.0
for n in [-1] + range(morse_state.states.size() - 1, -1, -1):
var duration := first_time if n == -1 else morse_state.states[n]
var rect_width: float = min(duration / 1000.0 * px_per_s, curr_x)
curr_x -= rect_width
if morse_on:
# at the moment we only draw the morse rects
_draw_morse_rect(curr_x, rect_width, morse_on)
morse_on = not morse_on
if curr_x <= 0.0:
break
func _process(_delta):
last_delta += _delta
queue_redraw()
fill_buffer()
func set_morse_state(state: bool) -> void:
morse_state.set_state(state)
# morse_state = state
if state:
$Player.volume_db = vol_on
else:
$Player.volume_db = -100
func fill_buffer():
var increment = tone_hz / sample_hz
var frames_available = playback.get_frames_available()
for i in range(frames_available):
playback.push_frame(Vector2.ONE * sin(phase * TAU))
phase = fmod(phase + increment, 1.0)

View File

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

View File

@ -1,252 +0,0 @@
extends Control
var server := "seba-geek.de"
var port := "3784"
var ws_url := "wss://seba-geek.de/godot/cw-generator-ws/"
var IS_DEBUG = false
var ws := WebSocketPeer.new()
var ws_last_status = -1
var autoconnect_to_freq: String
var available_freqs = []
var mmb_self: MultiMorseBanner
var mmb_others: Dictionary[String, MultiMorseBanner]
class PlayerData:
var num: int
var color: Color
var tone: int
func _init(num, color, tone):
self.num = num
self.color = color
self.tone = tone
var player_data := [
PlayerData.new(0, Color( 0, 255, 0), 880),
PlayerData.new(1, Color(255, 0, 0), 1318),
PlayerData.new(2, Color( 0, 0, 255), 587),
PlayerData.new(3, Color(255, 0, 255), 440),
PlayerData.new(3, Color( 0, 255, 255), 783),
PlayerData.new(3, Color(255, 255, 0), 1567),
]
func _ready() -> void:
# FIXME: connection handling / reconnect
# FIXME: status / error messages
# FIXME: randomize default join frquency
# FIXME: automatic refresh
_connect_ws()
if not Utils.first_connect_done:
var cmdline_freq = Utils.get_external_freq_param()
if cmdline_freq:
autoconnect_to_freq = cmdline_freq
func _connect_ws():
if not IS_DEBUG:
print("Connecting to ", ws_url)
ws.connect_to_url(ws_url)
else:
print("ws://%s:%s" % [server, port])
ws.connect_to_url("ws://%s:%s" % [server, port])
func _process(_delta: float) -> void:
ws.poll()
var state := ws.get_ready_state()
while state == WebSocketPeer.STATE_OPEN and ws.get_available_packet_count():
_handle_packet()
if ws_last_status != state:
match state:
WebSocketPeer.STATE_CONNECTING:
%StatusLabel.text = "Connecting...!"
WebSocketPeer.STATE_OPEN:
%StatusLabel.text = "Connected!"
WebSocketPeer.STATE_CLOSING:
%StatusLabel.text = "Disconnecting..."
WebSocketPeer.STATE_CLOSED:
%StatusLabel.text = "Disconnected :("
# Trigger reconnect
_trigger_reconnect(1)
ws_last_status = state
func _trigger_reconnect(delay: int):
await get_tree().create_timer(delay).timeout
_connect_ws()
func _handle_packet() -> void:
var data := ws.get_packet().get_string_from_utf8()
var parsed: Dictionary = JSON.parse_string(data)
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("type"):
print("Error: Could not parse data: ", data)
return
print("Recvd data: ", parsed)
match parsed["type"]:
"hello":
# fetch frequency list on first join
_on_refresh_button_pressed()
if autoconnect_to_freq:
send_data({"cmd": "create", "freq": autoconnect_to_freq, "join-if-present": true})
"join":
_join_freq(parsed["freq"], parsed["self_id"], parsed["other_players"])
"freq-list":
%FreqList.clear()
for freq in parsed["freqs"]:
var text = "%s MHz (%d present)" % [freq["freq"], freq["players"]]
print("Adding ", text)
var idx: int = %FreqList.add_item(text)
%FreqList.set_item_metadata(idx, freq)
"morse-state":
var from_player: String = parsed["from_player"]
if from_player not in mmb_others:
print("Error: Got morse state from unknown player ", from_player)
return
mmb_others[from_player].set_morse_state(parsed["state"])
"player-joined":
var new_player: String = parsed["player"]
if new_player in mmb_others:
print("Error: got player join message, but player ", new_player, " is already present")
return
# find free player id
var player_n := 1
while true:
var found := false
for other in mmb_others.values():
if other.num == player_n:
print("New player, num ", player_n, " already taken")
found = true
break
if not found:
break
player_n += 1
make_player(player_n, new_player)
"player-left":
var player: String = parsed["player"]
if player not in mmb_others:
print("Error: player not part of freq ", player)
return
remove_player(parsed["player"])
"leave":
_leave_freq()
"error":
%ErrorLabel.text = parsed["message"]
_:
print("Unhandled message: ", parsed["type"])
func _join_freq(freq: String, player_id: String, other_players: Array):
for child in %PlayerContainer.get_children():
%PlayerContainer.remove_child(child)
mmb_self = make_player(0, player_id)
for n in range(other_players.size()):
make_player(n + 1, other_players[n])
%FreqLabel.text = "%s MHz" % freq
%ConnectView.hide()
%MorseView.show()
func _leave_freq():
%MorseView.hide()
if mmb_self:
mmb_self.set_morse_state(false)
mmb_self = null
for mmb: MultiMorseBanner in mmb_others.values():
mmb.set_morse_state(false)
mmb_others.clear()
for child in %PlayerContainer.get_children():
%PlayerContainer.remove_child(child)
%ConnectView.show()
func _on_refresh_button_pressed() -> void:
var refresh_cmd := {"cmd": "list", "type": "cw-generator"}
var data := JSON.stringify(refresh_cmd) + "\n"
ws.send_text(data)
func _on_create_button_pressed() -> void:
var freq: String = "%.3f" % float(%FrequencyCreator.text)
var refresh_cmd := {"cmd": "create", "type": "cw-generator", "freq": freq}
var data := JSON.stringify(refresh_cmd) + "\n"
ws.send_text(data)
func _on_freq_list_join(index: int, at_position: Vector2, mouse_button_index: int) -> void:
var meta = %FreqList.get_item_metadata(index)
var freq: String = meta["freq"]
print("Yop ", index, " metadata ", freq)
var join_cmd := {"cmd": "join", "freq": freq, "type": "cw-generator"}
ws.send_text(JSON.stringify(join_cmd) + "\n")
func make_player(no: int, player_id: String):
var pd: PlayerData
if no < player_data.size():
pd = player_data[no]
else:
pd = PlayerData.new(no, Color(randi_range(0, 255), randi_range(0, 255), randi_range(0, 255)), randi_range(440, 440 * 3))
var mmb = MultiMorseBanner.new_banner(pd.num, pd.color, pd.tone)
%PlayerContainer.add_child(mmb)
mmb_others[player_id] = mmb
return mmb
func remove_player(player_id: String):
mmb_others[player_id].set_morse_state(false)
%PlayerContainer.remove_child(mmb_others[player_id])
mmb_others.erase(player_id)
func set_morse_state(state: bool):
mmb_self.set_morse_state(state)
send_data({"cmd": "morse-state", "state": state})
func send_data(data: Dictionary):
var text := JSON.stringify(data) + "\n"
ws.send_text(text)
func _on_morse_button_button_down() -> void:
if not mmb_self:
return
set_morse_state(true)
func _on_morse_button_button_up() -> void:
if not mmb_self:
return
set_morse_state(false)
func _input(input_event):
if input_event is InputEventMIDI:
_process_midi_event(input_event)
func _process_midi_event(midi_event):
if not mmb_self:
return
if midi_event.channel in [0, 9]:
if midi_event.message == MIDI_MESSAGE_NOTE_ON:
set_morse_state(true)
elif midi_event.message == MIDI_MESSAGE_NOTE_OFF:
set_morse_state(false)
func _on_leave_button_pressed() -> void:
send_data({"cmd": "leave"})
func _on_error_label_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
%ErrorLabel.text = "<cleared>"
func _on_back_button_pressed() -> void:
get_tree().change_scene_to_file("res://scenes/main.tscn")

View File

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

View File

@ -1,205 +0,0 @@
import asyncio
import json
import logging
import re
from websockets.asyncio.connection import broadcast
from websockets.asyncio.server import serve
__VERSION__ = "0.0.1"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
LOG = logging.getLogger()
class Client:
freqs = {}
freq_re = re.compile(r"^\d+\.\d{3}$")
def __init__(self, websocket):
self.websocket = websocket
self.curr_freq = None
async def handle(self):
LOG.info(">>> New client %s connected", self.client)
exc = None
try:
await self._handle_client()
except Exception as e:
exc = e
finally:
# FIXME: basically handle disconnect / leave from room
LOG.info("<<< Client %s id %s disconnected: %s", self.client, self.id, exc)
if self.curr_freq:
await self._leave_room()
@property
def client(self):
ip, port, *_ = self.websocket.remote_address
if ':' in ip:
ip = f"[{ip}]"
return f"{ip}:{port}"
@property
def id(self):
return str(self.websocket.id)
async def _handle_client(self):
await self._send(type="hello", name="LobbySrv 3000", version=__VERSION__)
async for data in self.websocket:
try:
data = json.loads(data)
except json.JSONDecodeError:
self._send_error("Could not decode message, invalid json")
LOG.error("client %s sent broken data %s", self.client, repr(data))
continue
if not isinstance(data, dict) or "cmd" not in data:
await self._send_error("Invalid format in json")
LOG.error("client %s sent broken data (no cmd key in data) %s", self.client, repr(data))
continue
LOG.info("client %s wrote: %s", self.client, data)
match data["cmd"]:
case "quit":
break
case "create":
await self._create_room(data)
case "join":
await self._join_room(data)
case "leave":
await self._leave_room()
case "list":
freqs = [{"freq": freq, "players": len(players)}
for freq, players in self.freqs.items()]
await self._send(type="freq-list", freqs=freqs)
case "disconnect":
pass
case "morse-state":
await self._handle_morse_state(data)
case _:
await self._send_error("Unknown command")
async def _create_room(self, data):
if self.curr_freq:
await self._send_error(f"Already on frequency {self.curr_freq}")
return
if "freq" not in data:
await self._send_error("No frequency in create message")
return
freq = data["freq"]
if not self.freq_re.match(freq):
await self._send_error("Invalid frequency")
return
if freq in self.freqs:
if data.get("join-if-present"):
await self._join_room({"freq": freq})
else:
await self._send_error("Frequency already in use")
return
self.curr_freq = freq
self.freqs[freq] = [self]
await self._send(type="join", freq=self.curr_freq, self_id=self.id, other_players=[])
async def _join_room(self, data):
if self.curr_freq:
await self._send_error(f"Already on frequency {self.curr_freq}")
return
if "freq" not in data:
await self._send_error("No frequency in join message")
return
freq = data["freq"]
if freq not in self.freqs:
await self._send_error(f"Frequency {freq} not available")
return
self.curr_freq = freq
self.freqs[freq].append(self)
# FIXME: do we need locking here?
LOG.debug("FREQ %s %s %s", self.curr_freq, freq, self.freqs)
await self._send(type="join", freq=self.curr_freq, self_id=self.id,
other_players=[c.id for c in self._others(freq)])
await self._send_to_group(self._others(freq), type="player-joined", player=self.id)
async def _handle_morse_state(self, data):
if not self.curr_freq:
await self._send_error("No frequency selected")
return
if "state" not in data or not isinstance(data["state"], bool):
await self._send_error("No state key with type bool in data")
return
await self._send_to_group(self._others(self.curr_freq),
type="morse-state", state=data["state"], from_player=self.id)
async def _leave_room(self):
if self.curr_freq:
await self._send_to_group(self._others(self.curr_freq),
type="player-left", player=self.id)
try:
self.freqs[self.curr_freq].remove(self)
except ValueError:
LOG.warning("Player %s was not in freq %s", self.id, self.curr_freq)
if not self.freqs[self.curr_freq]:
del self.freqs[self.curr_freq]
self.curr_freq = None
else:
LOG.warning("Client %s is not on a frequency, sending a 'leave' nontheless", self.client)
try:
await self._send(type="leave")
except Exception:
pass
def _others(self, freq):
return [c for c in self.freqs[freq] if c.id != self.id]
async def _send(self, ignore_exceptions=False, **kwargs):
data = json.dumps(kwargs).encode()
LOG.debug("--> sending out to %s: %s", self.client, data)
try:
await self.websocket.send(json.dumps(kwargs).encode() + b"\n")
except Exception as e:
LOG.error("Error sending data to %s: %s", self.client, e)
if not ignore_exceptions:
raise
async def _send_to_group(self, group, **kwargs):
LOG.info("broadcast() to %s clients: %s", len(group), kwargs)
broadcast([c.websocket for c in group], json.dumps(kwargs).encode() + b"\n")
async def _send_error(self, msg: str):
await self._send(type="error", message=msg)
async def new_client(websocket):
try:
client = Client(websocket)
await client.handle()
finally:
pass
# async for message in websocket:
# await websocket.send(message)
async def main():
HOST, PORT = "0.0.0.0", 3784
async with serve(new_client, HOST, PORT) as server:
await server.serve_forever()
if __name__ == "__main__":
LOG.info("Starting server")
asyncio.run(main())

View File

@ -1,81 +0,0 @@
import json
import socketserver
__VERSION__ = "0.0.1"
# https://websockets.readthedocs.io/en/stable/reference/asyncio/server.html
class LobbyMgr:
__instance = None
def __new__(cls):
if cls.__instance is None:
cls.__instance = LobbyMgr()
return cls.__instance
class LobbyHandler(socketserver.StreamRequestHandler):
def handle(self):
print(f" >>> New client {self.client} connected")
exc = None
try:
self._handle_client()
except Exception as e:
exc = e
finally:
print(f" <<< Client {self.client} disconnected: {exc}")
@property
def client(self):
return f"{':'.join(map(str, self.client_address))}"
def _handle_client(self):
self._send(type="hello", name="LobbySrv 3000", version=__VERSION__)
while True:
data = self.rfile.readline(10000).rstrip()
print(f" <-- client {self.client} sent {repr(data)}")
if not data.strip():
continue
try:
data = json.loads(data)
except json.JSONDecodeError:
self._send_error("Could not decode message, invalid json")
continue
if not isinstance(data, dict) or "cmd" not in data:
self._send_error("Invalid format in json")
continue
print(f"{self.client_address[0]} wrote:", data)
match data["cmd"]:
case "quit":
break
case _:
self._send_error("Unknown command")
def _send(self, **kwargs):
data = json.dumps(kwargs).encode()
print(f" --> sending out to {self.client}: {data}")
self.wfile.write(json.dumps(kwargs).encode() + b"\n")
def _send_error(self, msg: str):
self._send(type="error", message=msg)
class LobbyServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
def main():
HOST, PORT = "localhost", 3784
with LobbyServer((HOST, PORT), LobbyHandler) as server:
server.serve_forever()
if __name__ == "__main__":
main()