diff --git a/CHANGELOG.md b/CHANGELOG.md index 22acfbf..9e8ff14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Paste from JSXFR. ## [1.2.0] - 2022-10-13 ### Added diff --git a/addons/gdfxr/Base58.gd b/addons/gdfxr/Base58.gd new file mode 100644 index 0000000..8021260 --- /dev/null +++ b/addons/gdfxr/Base58.gd @@ -0,0 +1,38 @@ +extends Object + +const BASE_58_ALPHABET := "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + +static func b58decode(v: String) -> StreamPeerBuffer: + # Base 58 is a number expressed in the base-58 numeral system. + # When encoding data, big-endian is used and leading zeros are encoded as leading `1`s. + + var original_length := v.length() + v = v.lstrip(BASE_58_ALPHABET[0]) + var zeros := original_length - v.length() + + var buffer := PackedByteArray() + buffer.resize(v.length()) # Won't be as long as base 58 string since the buffer is 256-based. + buffer.fill(0) + + var length := 0 + for c in v: + var carry := BASE_58_ALPHABET.find(c) + if carry == -1: + return null + var i := 0 + while carry != 0 or i < length: + var pos := buffer.size() - 1 - i + carry += 58 * buffer[pos] + buffer[pos] = carry % 256 + carry /= 256 + i += 1 + length = i + + var result := StreamPeerBuffer.new() + for _i in zeros: + result.put_8(0) + result.put_data(buffer.slice(buffer.size() - length)) + result.seek(0) + + return result diff --git a/addons/gdfxr/SFXRConfig.gd b/addons/gdfxr/SFXRConfig.gd index a02db31..6cd26d4 100644 --- a/addons/gdfxr/SFXRConfig.gd +++ b/addons/gdfxr/SFXRConfig.gd @@ -18,6 +18,8 @@ enum Category { BLIP_SELECT, } +const Base58 := preload("res://addons/gdfxr/Base58.gd") + var wave_type: int = WaveType.SQUARE_WAVE var p_env_attack := 0.0 # Attack Time @@ -463,3 +465,45 @@ func is_equal(other: RefCounted) -> bool: # SFXRConfig and sound_vol == other.sound_vol ) + + +# Load base58 string copied from jsfxr +# See https://github.com/chr15m/jsfxr/blob/a708164e6ce200008d88202e1aaf2b9171a17ec2/sfxr.js#L132-L175 +func load_from_base58(v: String) -> int: # Error + var buffer := Base58.b58decode(v) + if not buffer: + return ERR_INVALID_DATA + if buffer.get_size() != 89: + return ERR_INVALID_DATA + + var params_order = [ + "p_env_attack", + "p_env_sustain", + "p_env_punch", + "p_env_decay", + "p_base_freq", + "p_freq_limit", + "p_freq_ramp", + "p_freq_dramp", + "p_vib_strength", + "p_vib_speed", + "p_arp_mod", + "p_arp_speed", + "p_duty", + "p_duty_ramp", + "p_repeat_speed", + "p_pha_offset", + "p_pha_ramp", + "p_lpf_freq", + "p_lpf_ramp", + "p_lpf_resonance", + "p_hpf_freq", + "p_hpf_ramp", + ] + + wave_type = buffer.get_8() + + for param in params_order: + set(param, buffer.get_float()) + + return OK diff --git a/addons/gdfxr/editor/Editor.gd b/addons/gdfxr/editor/Editor.gd index a10e8bd..1c29d4b 100644 --- a/addons/gdfxr/editor/Editor.gd +++ b/addons/gdfxr/editor/Editor.gd @@ -1,10 +1,11 @@ @tool extends Container -enum ExtraOption { SAVE_AS, COPY, PASTE, RECENT } +enum ExtraOption { SAVE_AS, COPY, PASTE, PASTE_JSFXR, RECENT } const SFXRConfig := preload("../SFXRConfig.gd") const SFXRGenerator := preload("../SFXRGenerator.gd") +const Base58 := preload("../Base58.gd") const NUM_RECENTS := 4 class RecentEntry: @@ -47,6 +48,7 @@ func _ready(): popup.add_separator() popup.add_icon_item(get_theme_icon("ActionCopy", "EditorIcons"), translator.tr("Copy"), ExtraOption.COPY) popup.add_icon_item(get_theme_icon("ActionPaste", "EditorIcons"), translator.tr("Paste"), ExtraOption.PASTE) + popup.add_item(translator.tr("Paste from jsfxr"), ExtraOption.PASTE_JSFXR) popup.add_separator(translator.tr("Recently Generated")) popup.id_pressed.connect(_on_Extra_id_pressed) @@ -129,7 +131,7 @@ func _popup_message(content: String) -> void: var dialog := AcceptDialog.new() add_child(dialog) dialog.dialog_text = content - dialog.window_title = translator.tr("SFXR Editor") + dialog.title = translator.tr("SFXR Editor") dialog.popup_centered() dialog.visibility_changed.connect(dialog.queue_free) @@ -297,6 +299,7 @@ func _on_Load_pressed(): func _on_Extra_about_to_show(): var popup := extra_button.get_popup() popup.set_item_disabled(popup.get_item_index(ExtraOption.PASTE), _config_clipboard == null) + popup.set_item_disabled(popup.get_item_index(ExtraOption.PASTE_JSFXR), not DisplayServer.clipboard_has()) # Rebuild recents menu everytime :) var first_recent_index := popup.get_item_index(ExtraOption.RECENT) @@ -326,6 +329,13 @@ func _on_Extra_id_pressed(id: int) -> void: ExtraOption.PASTE: _restore_from_config(_config_clipboard) + ExtraOption.PASTE_JSFXR: + var pasted := SFXRConfig.new() + if pasted.load_from_base58(DisplayServer.clipboard_get()) == OK: + _restore_from_config(pasted) + else: + _popup_message(translator.tr("Clipboard does not contain code copied from jsfxr.")) + _: var i := id - ExtraOption.RECENT as int if i < 0 or _config_recents.size() <= i: diff --git a/addons/gdfxr/editor/translations/gdfxr.pot b/addons/gdfxr/editor/translations/gdfxr.pot index 839bb18..23766af 100644 --- a/addons/gdfxr/editor/translations/gdfxr.pot +++ b/addons/gdfxr/editor/translations/gdfxr.pot @@ -8,14 +8,14 @@ msgid "" msgstr "" "Project-Id-Version: gdfxr 1.0\n" "Report-Msgid-Bugs-To: timothyqiu32@gmail.com\n" -"POT-Creation-Date: 2022-09-20 14:01+0800\n" +"POT-Creation-Date: 2022-12-04 13:45+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.10.3\n" +"Generated-By: Babel 2.11.0\n" #: addons/gdfxr/editor/Editor.gd msgid "Save As..." @@ -29,6 +29,10 @@ msgstr "" msgid "Paste" msgstr "" +#: addons/gdfxr/editor/Editor.gd +msgid "Paste from jsfxr" +msgstr "" + #: addons/gdfxr/editor/Editor.gd msgid "Recently Generated" msgstr "" @@ -109,6 +113,10 @@ msgstr "" msgid "None" msgstr "" +#: addons/gdfxr/editor/Editor.gd +msgid "Clipboard does not contain code copied from jsfxr." +msgstr "" + #: addons/gdfxr/editor/Editor.tscn msgid "New" msgstr "" @@ -245,7 +253,3 @@ msgstr "" msgid "Waveform" msgstr "" -#: addons/gdfxr/editor/ParamSlider.tscn -msgid "Hold Ctrl to snap to 0.01 increments." -msgstr "" - diff --git a/addons/gdfxr/editor/translations/zh_CN.po b/addons/gdfxr/editor/translations/zh_CN.po index 2fbe171..9ca15d9 100644 --- a/addons/gdfxr/editor/translations/zh_CN.po +++ b/addons/gdfxr/editor/translations/zh_CN.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: gdfxr 1.0\n" "Report-Msgid-Bugs-To: timothyqiu32@gmail.com\n" -"POT-Creation-Date: 2022-09-20 14:01+0800\n" -"PO-Revision-Date: 2022-09-20 14:01+0800\n" +"POT-Creation-Date: 2022-12-04 13:45+0800\n" +"PO-Revision-Date: 2022-12-04 13:45+0800\n" "Last-Translator: Haoyu Qiu \n" "Language-Team: \n" "Language: zh_CN\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "Generated-By: Babel 2.9.1\n" -"X-Generator: Poedit 3.1\n" +"X-Generator: Poedit 3.2.1\n" #: addons/gdfxr/editor/Editor.gd msgid "Save As..." @@ -31,6 +31,10 @@ msgstr "复制" msgid "Paste" msgstr "粘贴" +#: addons/gdfxr/editor/Editor.gd +msgid "Paste from jsfxr" +msgstr "从 jsfxr 粘贴" + #: addons/gdfxr/editor/Editor.gd msgid "Recently Generated" msgstr "最近生成" @@ -117,6 +121,10 @@ msgstr "" msgid "None" msgstr "无" +#: addons/gdfxr/editor/Editor.gd +msgid "Clipboard does not contain code copied from jsfxr." +msgstr "剪贴板中没有从 jsfxr 复制的代码。" + #: addons/gdfxr/editor/Editor.tscn msgid "New" msgstr "新建" @@ -253,6 +261,5 @@ msgstr "高通变频" msgid "Waveform" msgstr "波形" -#: addons/gdfxr/editor/ParamSlider.tscn -msgid "Hold Ctrl to snap to 0.01 increments." -msgstr "按住 Ctrl 吸附到 0.01 增量。" +#~ msgid "Hold Ctrl to snap to 0.01 increments." +#~ msgstr "按住 Ctrl 吸附到 0.01 增量。" diff --git a/example/Example.gd b/example/Example.gd index c8ed340..d4073dd 100644 --- a/example/Example.gd +++ b/example/Example.gd @@ -1,7 +1,38 @@ -extends CenterContainer +extends Container -func _ready() -> void: - var audio := load("res://example/example.sfxr") as AudioStreamSample - print(audio) - audio.save_to_wav("/home/timothy/Desktop/foo.wav") - $AudioStreamPlayer.stream = audio +# These two classes are for runtime generation. +const SFXRConfig = preload("res://addons/gdfxr/SFXRConfig.gd") +const SFXRGenerator = preload("res://addons/gdfxr/SFXRGenerator.gd") + +@onready var audio_player: AudioStreamPlayer = $AudioPlayer +@onready var adhoc_audio_player: AudioStreamPlayer = $AdhocAudioPlayer + + +func _on_Play_pressed() -> void: + audio_player.play() + + +func _on_PlayFile_pressed() -> void: + adhoc_audio_player.stream = preload("res://example/example.sfxr") + adhoc_audio_player.play() + + +func _on_Generate_pressed() -> void: + var config := SFXRConfig.new() + + # Fill the fields manually + # config.p_base_freq = 0.5 + + # Load from .sfxr file + # config.load("res://example/example.sfxr") + + # Load from jsfxr base58 string + config.load_from_base58("34T6PkmKkNTf3aUynCpV3oetaq6ecj9Grh9W7tiTbccVYK8FxNKBbfBFXJCLzk8QTy4d7fbiCfY2gXDaiengXbENjdLWt5jZBtcz8QmSCXjHCSuooDCWp4SrT") + + # generate_audio_stream() might freeze a bit when generating long sounds. + # It's recommended to pre-generate the sound effects in editor. + # If you do want to generate the sound effects on the fly, you might want + # to generate and cache the sound effects at the start of your game. + var generator := SFXRGenerator.new() + adhoc_audio_player.stream = generator.generate_audio_stream(config) + adhoc_audio_player.play() diff --git a/example/Example.tscn b/example/Example.tscn index 52cf66c..1ec34cc 100644 --- a/example/Example.tscn +++ b/example/Example.tscn @@ -1,23 +1,59 @@ -[gd_scene load_steps=2 format=3 uid="uid://bv31mn2hs6wom"] +[gd_scene load_steps=3 format=3 uid="uid://bv31mn2hs6wom"] -[ext_resource type="AudioStream" uid="uid://byf7u7a25fuf4" path="res://example/example.sfxr" id="1"] +[ext_resource type="Script" path="res://example/Example.gd" id="1"] +[ext_resource type="AudioStream" uid="uid://byf7u7a25fuf4" path="res://example/example.sfxr" id="2"] -[node name="Example" type="CenterContainer"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 +[node name="Example" type="GridContainer"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 grow_horizontal = 2 grow_vertical = 2 +theme_override_constants/h_separation = 32 +theme_override_constants/v_separation = 32 +columns = 2 +script = ExtResource("1") -[node name="Button" type="Button" parent="."] +[node name="AudioPlayer" type="AudioStreamPlayer" parent="."] +stream = ExtResource("2") + +[node name="AdhocAudioPlayer" type="AudioStreamPlayer" parent="."] + +[node name="Play" type="Button" parent="."] layout_mode = 2 -offset_left = 555.0 -offset_top = 308.0 -offset_right = 596.0 -offset_bottom = 339.0 +size_flags_vertical = 4 text = "Play" -[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."] -stream = ExtResource("1") +[node name="Label" type="Label" parent="."] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +text = "A .sfxr file can be used as regular audio files like .wav, .ogg, and .mp3." +autowrap_mode = 3 -[connection signal="pressed" from="Button" to="AudioStreamPlayer" method="play"] +[node name="PlayFile" type="Button" parent="."] +layout_mode = 2 +size_flags_vertical = 4 +text = "Load .sfxr File" + +[node name="Label2" type="Label" parent="."] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +text = "A .sfxr file is a AudioStreamSample resource that can be loaded with load() or preload()." +autowrap_mode = 3 + +[node name="Generate" type="Button" parent="."] +layout_mode = 2 +size_flags_vertical = 4 +text = "Runtime Generation" + +[node name="Label3" type="Label" parent="."] +custom_minimum_size = Vector2(500, 0) +layout_mode = 2 +text = "You can generate the sound effect at runtime. However, due to performance constraints with GDScript, your game might freeze when generating long sounds." +autowrap_mode = 3 + +[connection signal="pressed" from="Play" to="." method="_on_Play_pressed"] +[connection signal="pressed" from="PlayFile" to="." method="_on_PlayFile_pressed"] +[connection signal="pressed" from="Generate" to="." method="_on_Generate_pressed"]