mirror of
https://github.com/Relintai/gdfxr.git
synced 2025-04-20 01:43:13 +02:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
d1e4b34cdc | |||
|
7b3194f500 | ||
|
4562f4b77c | ||
|
576708bb4a | ||
|
5b910afb64 | ||
|
9acce6be58 | ||
|
0621590150 | ||
|
b5322f430f | ||
|
2a73615082 | ||
|
3afc110a58 | ||
|
5bea4a85ed | ||
|
7c82e81fdd | ||
|
d9520c17c2 | ||
|
36c647c263 | ||
|
8c3a37a3cb | ||
|
a6a4a93485 | ||
|
f124fd9ed6 | ||
|
ab2b07f650 | ||
|
a2cf6ac404 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -4,7 +4,9 @@
|
||||
# Ignore some files when exporting to a ZIP.
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.github export-ignore
|
||||
/LICENSE export-ignore
|
||||
/CHANGELOG.md export-ignore
|
||||
/README.md export-ignore
|
||||
/README-zh_CN.md export-ignore
|
||||
/project.godot export-ignore
|
||||
|
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
ko_fi: timothyqiu
|
||||
custom:
|
||||
- "https://paypal.me/timothyqiu32"
|
||||
- "https://afdian.net/@timothyqiu"
|
37
CHANGELOG.md
Normal file
37
CHANGELOG.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
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]
|
||||
|
||||
## [1.3.0] - 2023-02-23
|
||||
### Added
|
||||
- Bit depth and sample rate import options.
|
||||
- Paste from JSXFR.
|
||||
- Add default filename for Save As dialog, with trailing number incremented.
|
||||
|
||||
## [1.2.0] - 2022-10-13
|
||||
### Added
|
||||
- Implement custom slider.
|
||||
|
||||
## [1.1.1] - 2022-07-30
|
||||
### Fixed
|
||||
- Slider min value for a bipolar parameter should be -1.
|
||||
|
||||
## [1.1.0] - 2022-03-10
|
||||
### Added
|
||||
- Extra options menu: Save As, Copy, and Paste.
|
||||
- Add a copy of the LICENSE in the plugin folder.
|
||||
- Keep several recently generated sounds in the menu.
|
||||
|
||||
## [1.0.0] - 2022-02-25
|
||||
### Added
|
||||
- Initial release.
|
||||
|
||||
[1.0.0]: https://github.com/timothyqiu/gdfxr/releases/tag/1.0
|
||||
[1.1.0]: https://github.com/timothyqiu/gdfxr/releases/tag/1.1
|
||||
[1.1.1]: https://github.com/timothyqiu/gdfxr/releases/tag/1.1.1
|
||||
[1.2.0]: https://github.com/timothyqiu/gdfxr/releases/tag/1.2
|
||||
[1.3.0]: https://github.com/timothyqiu/gdfxr/releases/tag/1.3
|
@ -3,12 +3,15 @@
|
||||
<img src="icon.png?raw=true" align="right" />
|
||||
|
||||
[](LICENSE)
|
||||
[](https://godotengine.org/asset-library/asset/1249)
|
||||
[](README.md)
|
||||
|
||||
以 Godot 插件形式移植的 [sfxr](https://www.drpetter.se/project_sfxr.html "DrPetter's homepage - sfxr"),这是一个非常流行的复古游戏音效生成器。
|
||||
|
||||
你可以在 Godot 中把 sfxr 音效文件当作普通的音频文件使用,也可以像在原始的 sfxr 中一样对音效进行编辑。
|
||||
|
||||
> 🚧 如果你想在 Godot 4 里使用这个插件,请移步 [godot-4](https://github.com/timothyqiu/gdfxr/tree/godot-4) 分支。
|
||||
|
||||
## 安装
|
||||
|
||||
这是一个普通的 Godot 插件。安装时,先下载 ZIP 包,解压后将 `addons/` 文件夹移动到你的项目文件夹中,然后在项目设置中启用本插件。
|
||||
@ -29,4 +32,10 @@
|
||||
|
||||
如果你希望使用原始的 sfxr 保存出的文件,请确保使用 `.sfxr` 扩展名保存。你也可以在原始的 sfxr 中加载并编辑 `.sfxr` 文件。
|
||||
|
||||
`.sfxr` 文件有循环(Loop)、位深度(Bit Depth)、采样率(Sample Rate)等导入选项。可以在 Godot 编辑器的导入面板中找到。
|
||||
|
||||
**注意:** 由于 GDScript 的性能限制,生成较长的音效时编辑器可能会有短暂的停滞。只有编辑器会受此影响。在游戏中使用 `.sfxr` 文件是不会在运行时生成任何东西的。
|
||||
|
||||
## 更新日志
|
||||
|
||||
见 [CHANGELOG](CHANGELOG.md)。
|
||||
|
10
README.md
10
README.md
@ -3,6 +3,7 @@
|
||||
<img src="icon.png?raw=true" align="right" />
|
||||
|
||||
[](LICENSE)
|
||||
[](https://godotengine.org/asset-library/asset/1249)
|
||||
[](README-zh_CN.md)
|
||||
|
||||
A Godot plugin that ports [sfxr](https://www.drpetter.se/project_sfxr.html "DrPetter's homepage - sfxr"),
|
||||
@ -11,6 +12,8 @@ the popular program of choice to make retro sound effects for games.
|
||||
You can use sfxr sound files like regular audio files in Godot and edit sound files like in the
|
||||
original sfxr.
|
||||
|
||||
> 🚧 Checkout the [godot-4](https://github.com/timothyqiu/gdfxr/tree/godot-4) branch if you want to use this plugin in Godot 4.
|
||||
|
||||
## Installation
|
||||
|
||||
This is a regular plugin for Godot. To install, download the ZIP archive, extract it, and move the
|
||||
@ -39,6 +42,13 @@ But they can be used directly as regular `AudioStream`s.
|
||||
If you want to reuse an existing sound from the original sfxr, make sure to save it with an
|
||||
`.sfxr` extension. You can also load & edit the `.sfxr` file with the original sfxr.
|
||||
|
||||
Options for changing Looping, Bit Depth, and Sample Rate are available as import options
|
||||
of the `.sfxr` file. You can find these options in Godot editor's Import dock.
|
||||
|
||||
**Note:** Due to performance constraints with GDScript, the editor may freeze a bit when generating
|
||||
long sounds. This only happens in-editor.
|
||||
Using `.sfxr` files in-game won't generate anything at runtime.
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG](CHANGELOG.md).
|
||||
|
38
addons/gdfxr/Base58.gd
Normal file
38
addons/gdfxr/Base58.gd
Normal file
@ -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 := PoolByteArray()
|
||||
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.subarray(buffer.size() - length, -1))
|
||||
result.seek(0)
|
||||
|
||||
return result
|
7
addons/gdfxr/LICENSE
Normal file
7
addons/gdfxr/LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 2022 Haoyu Qiu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -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
|
||||
@ -465,3 +467,45 @@ func is_equal(other: Reference) -> 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
|
||||
|
@ -5,6 +5,15 @@ const SFXRConfig := preload("SFXRConfig.gd")
|
||||
|
||||
const master_vol := 0.05
|
||||
|
||||
enum WavBits {
|
||||
WAV_BITS_8,
|
||||
WAV_BITS_16,
|
||||
}
|
||||
enum WavFreq {
|
||||
WAV_FREQ_44100,
|
||||
WAV_FREQ_22050,
|
||||
}
|
||||
|
||||
var _config: SFXRConfig
|
||||
|
||||
var rep_time: int
|
||||
@ -41,32 +50,42 @@ var fltdmp: float
|
||||
var fltphp: float
|
||||
|
||||
|
||||
func generate_audio_stream(config: SFXRConfig) -> AudioStreamSample:
|
||||
func generate_audio_stream(
|
||||
config: SFXRConfig,
|
||||
wav_bits: int = WavBits.WAV_BITS_8,
|
||||
wav_freq: int = WavFreq.WAV_FREQ_44100
|
||||
) -> AudioStreamSample:
|
||||
var stream := AudioStreamSample.new()
|
||||
stream.format = AudioStreamSample.FORMAT_8_BITS
|
||||
stream.mix_rate = 44100
|
||||
stream.format = AudioStreamSample.FORMAT_8_BITS if wav_bits == WavBits.WAV_BITS_8 else AudioStreamSample.FORMAT_16_BITS
|
||||
stream.mix_rate = 44100 if wav_freq == WavFreq.WAV_FREQ_44100 else 22050
|
||||
|
||||
_config = config
|
||||
stream.data = _generate_samples()
|
||||
stream.data = _generate_samples(wav_bits, wav_freq).data_array
|
||||
_config = null
|
||||
|
||||
return stream
|
||||
|
||||
|
||||
func generate_samples(config: SFXRConfig) -> PoolByteArray:
|
||||
func generate_samples(
|
||||
config: SFXRConfig,
|
||||
wav_bits: int = WavBits.WAV_BITS_8,
|
||||
wav_freq: int = WavFreq.WAV_FREQ_44100
|
||||
) -> PoolByteArray:
|
||||
_config = config
|
||||
var data := _generate_samples()
|
||||
var data := _generate_samples(wav_bits, wav_freq).data_array
|
||||
_config = null
|
||||
return data
|
||||
|
||||
|
||||
func _generate_samples() -> PoolByteArray:
|
||||
func _generate_samples(wav_bits: int, wav_freq: int) -> StreamPeerBuffer:
|
||||
_reset_sample(true)
|
||||
|
||||
var playing_sample := true
|
||||
var env_stage := 0
|
||||
var env_time := 0
|
||||
var output := PoolByteArray([])
|
||||
var filesample: float = 0
|
||||
var fileacc := 0
|
||||
var buffer := StreamPeerBuffer.new()
|
||||
|
||||
# SynthSample
|
||||
while playing_sample:
|
||||
@ -174,15 +193,21 @@ func _generate_samples() -> PoolByteArray:
|
||||
ssample *= 4.0 # arbitrary gain to get reasonable output volume...
|
||||
ssample = clamp(ssample, -1.0, +1.0)
|
||||
|
||||
var filesample := int((1 + ssample) / 2 * 255)
|
||||
filesample += ssample
|
||||
fileacc += 1
|
||||
|
||||
# This is a hack, AudioStreamSample wants a int8_t directly interpreted as uint8_t
|
||||
filesample += 128
|
||||
if filesample > 255:
|
||||
filesample -= 255
|
||||
|
||||
output.push_back(filesample)
|
||||
return output
|
||||
if wav_freq == WavFreq.WAV_FREQ_44100 or fileacc == 2:
|
||||
filesample /= fileacc
|
||||
fileacc = 0
|
||||
|
||||
if wav_bits == WavBits.WAV_BITS_8:
|
||||
buffer.put_8(filesample * 255)
|
||||
else:
|
||||
buffer.put_16(filesample * 32000)
|
||||
|
||||
filesample = 0
|
||||
|
||||
return buffer
|
||||
|
||||
|
||||
func _reset_sample(restart: bool) -> void:
|
||||
|
203
addons/gdfxr/editor/EditSlider.gd
Normal file
203
addons/gdfxr/editor/EditSlider.gd
Normal file
@ -0,0 +1,203 @@
|
||||
tool
|
||||
extends Control
|
||||
|
||||
signal value_changed(value)
|
||||
|
||||
export var value: float = 0.0 setget set_value
|
||||
export var min_value: float = 0.0
|
||||
export var max_value: float = 1.0
|
||||
|
||||
var _line_edit: LineEdit
|
||||
|
||||
var _stylebox_normal: StyleBox
|
||||
var _stylebox_hover: StyleBox
|
||||
var _stylebox_editing: StyleBox
|
||||
var _stylebox_value: StyleBox
|
||||
|
||||
var _line_edit_just_closed := false
|
||||
var _mouse_hovering := false
|
||||
var _is_editing := false
|
||||
|
||||
var _drag_start_position: Vector2
|
||||
var _drag_cancelled := true
|
||||
var _drag_dist := 0.0
|
||||
var _drag_start_factor: float
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
mouse_default_cursor_shape = Control.CURSOR_HSIZE
|
||||
rect_clip_content = true
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
|
||||
var style := StyleBoxEmpty.new()
|
||||
style.content_margin_left = 8
|
||||
style.content_margin_right = 8
|
||||
|
||||
_line_edit = LineEdit.new()
|
||||
_line_edit.set_as_toplevel(true)
|
||||
_line_edit.visible = false
|
||||
_line_edit.add_theme_stylebox_override("normal", style)
|
||||
_line_edit.add_theme_stylebox_override("focus", StyleBoxEmpty.new())
|
||||
|
||||
var _ret: int
|
||||
_ret = _line_edit.connect("focus_exited", self, "_on_line_edit_focus_exited")
|
||||
_ret = _line_edit.connect("text_entered", self, "_on_line_edit_text_entered")
|
||||
_ret = _line_edit.connect("visibility_changed", self, "_on_line_edit_visibility_changed")
|
||||
|
||||
add_child(_line_edit)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var font := get_theme_font("font", "LineEdit")
|
||||
var color := get_theme_color("highlighted_font_color" if _mouse_hovering else "font_color", "Editor")
|
||||
var number_string := "%.3f" % value
|
||||
var number_size := font.get_string_size(number_string)
|
||||
var pos := Vector2(
|
||||
(rect_size.x - number_size.x) / 2,
|
||||
(rect_size.y - number_size.y) / 2 + font.get_ascent()
|
||||
)
|
||||
|
||||
var stylebox := _stylebox_editing if _is_editing else _stylebox_hover if _mouse_hovering else _stylebox_normal
|
||||
|
||||
if _line_edit.visible:
|
||||
draw_style_box(stylebox, Rect2(Vector2.ZERO, rect_size))
|
||||
else:
|
||||
var value_width := rect_size.x * ((value - min_value) / (max_value - min_value))
|
||||
draw_style_box(stylebox, Rect2(value_width, 0, rect_size.x - value_width, rect_size.y))
|
||||
draw_style_box(_stylebox_value, Rect2(0, 0, value_width, rect_size.y))
|
||||
draw_string(font, pos, number_string, color)
|
||||
|
||||
|
||||
func _get_minimum_size() -> Vector2:
|
||||
var ms := _stylebox_normal.get_minimum_size()
|
||||
ms.y += get_theme_font("font", "LineEdit").get_height()
|
||||
return ms
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb and mb.button_index == BUTTON_LEFT:
|
||||
if mb.pressed:
|
||||
_drag_prepare(mb)
|
||||
else:
|
||||
_drag_done()
|
||||
if _drag_cancelled:
|
||||
_show_text_edit()
|
||||
_drag_cancelled = true
|
||||
_is_editing = mb.pressed
|
||||
update()
|
||||
|
||||
var mm := event as InputEventMouseMotion
|
||||
if mm and mm.button_mask & BUTTON_MASK_LEFT:
|
||||
_drag_motion(mm)
|
||||
_drag_cancelled = false
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
match what:
|
||||
NOTIFICATION_ENTER_TREE, NOTIFICATION_THEME_CHANGED:
|
||||
_update_stylebox()
|
||||
|
||||
NOTIFICATION_MOUSE_ENTER:
|
||||
_mouse_hovering = true
|
||||
update()
|
||||
|
||||
NOTIFICATION_MOUSE_EXIT:
|
||||
_mouse_hovering = false
|
||||
update()
|
||||
|
||||
NOTIFICATION_FOCUS_ENTER:
|
||||
if (Input.is_action_pressed("ui_focus_next") or Input.is_action_pressed("ui_focus_prev")) and not _line_edit_just_closed:
|
||||
_show_text_edit()
|
||||
_line_edit_just_closed = false
|
||||
|
||||
|
||||
func set_value(v: float) -> void:
|
||||
if is_equal_approx(v, value):
|
||||
return
|
||||
value = v
|
||||
emit_signal("value_changed", value)
|
||||
update()
|
||||
|
||||
|
||||
func _update_stylebox() -> void:
|
||||
_stylebox_normal = get_theme_stylebox("normal", "LineEdit")
|
||||
_stylebox_hover = StyleBoxFlat.new()
|
||||
_stylebox_hover.bg_color = get_theme_color("highlight_color", "Editor")
|
||||
_stylebox_editing = StyleBoxFlat.new()
|
||||
_stylebox_editing.bg_color = get_theme_color("dark_color_2", "Editor")
|
||||
_stylebox_value = StyleBoxFlat.new()
|
||||
_stylebox_value.bg_color = get_theme_color("accent_color", "Editor") * Color(1, 1, 1, 0.4)
|
||||
|
||||
|
||||
func _drag_prepare(mouse: InputEventMouse) -> void:
|
||||
_drag_dist = 0.0
|
||||
_drag_start_factor = (value - min_value) / (max_value - min_value)
|
||||
_drag_start_position = mouse.global_position
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
func _drag_done() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
if _drag_cancelled:
|
||||
Input.warp_mouse_position(_drag_start_position)
|
||||
else:
|
||||
Input.warp_mouse_position(rect_global_position + rect_size * Vector2(
|
||||
(value - min_value) / (max_value - min_value),
|
||||
0.5
|
||||
))
|
||||
|
||||
|
||||
func _drag_motion(motion: InputEventMouseMotion) -> void:
|
||||
_drag_dist += motion.relative.x
|
||||
|
||||
var factor := _drag_start_factor + _drag_dist / rect_size.x
|
||||
if factor < 0 or 1 < factor:
|
||||
factor = clamp(factor, 0, 1)
|
||||
_drag_dist = (factor - _drag_start_factor) * rect_size.x
|
||||
|
||||
var v := factor * (max_value - min_value) + min_value
|
||||
var snap := motion.command or motion.shift
|
||||
if snap and not (is_equal_approx(v, min_value) or is_equal_approx(v, max_value)):
|
||||
if motion.shift and motion.command:
|
||||
v = round(v * 1000.0) * 0.001
|
||||
elif motion.shift:
|
||||
v = round(v * 100.0) * 0.01
|
||||
else:
|
||||
v = round(v * 10.0) * 0.1
|
||||
|
||||
set_value(clamp(v, min_value, max_value))
|
||||
|
||||
update()
|
||||
|
||||
|
||||
func _show_text_edit() -> void:
|
||||
var gr := get_global_rect()
|
||||
_line_edit.text = str(value)
|
||||
_line_edit.set_position(gr.position)
|
||||
_line_edit.set_size(gr.size)
|
||||
_line_edit.show_modal()
|
||||
_line_edit.select_all()
|
||||
_line_edit.grab_focus()
|
||||
_line_edit.focus_next = find_next_valid_focus().get_path()
|
||||
_line_edit.focus_previous = find_prev_valid_focus().get_path()
|
||||
|
||||
|
||||
func _on_line_edit_focus_exited():
|
||||
if _line_edit.get_menu().visible:
|
||||
return
|
||||
if _line_edit.text.is_valid_float():
|
||||
set_value(clamp(_line_edit.text.to_float(), min_value, max_value))
|
||||
if not _line_edit_just_closed:
|
||||
_line_edit.hide()
|
||||
update()
|
||||
|
||||
|
||||
func _on_line_edit_text_entered(_text: String):
|
||||
_line_edit.hide()
|
||||
|
||||
|
||||
func _on_line_edit_visibility_changed():
|
||||
if not _line_edit.visible:
|
||||
_line_edit_just_closed = true
|
@ -1,23 +1,37 @@
|
||||
tool
|
||||
extends Container
|
||||
|
||||
enum ExtraOption { SAVE_AS, COPY, PASTE, PASTE_JSFXR, RECENT }
|
||||
enum DefaultFilename { EMPTY, GUESS_FOR_SAVE }
|
||||
|
||||
const SFXRConfig := preload("../SFXRConfig.gd")
|
||||
const SFXRGenerator := preload("../SFXRGenerator.gd")
|
||||
const Base58 := preload("../Base58.gd")
|
||||
const NUM_RECENTS := 4
|
||||
|
||||
class RecentEntry:
|
||||
var title: String
|
||||
var config := SFXRConfig.new()
|
||||
|
||||
var plugin: EditorPlugin
|
||||
|
||||
var _config := SFXRConfig.new()
|
||||
var _config_defaults := SFXRConfig.new()
|
||||
var _config_clipboard: SFXRConfig
|
||||
var _config_recents: Array
|
||||
var _recents_id := 0
|
||||
var _generator := SFXRGenerator.new()
|
||||
var _path: String
|
||||
var _modified := false
|
||||
var _param_map := {}
|
||||
var _syncing_ui := false # a hack since Range set_value emits value_changed
|
||||
var _category_names := {}
|
||||
|
||||
onready var audio_player := $AudioStreamPlayer as AudioStreamPlayer
|
||||
onready var filename_label := find_node("Filename") as Label
|
||||
onready var save_button := find_node("Save") as Button
|
||||
onready var restore_button := find_node("Restore") as Button
|
||||
onready var extra_button := find_node("Extra") as MenuButton
|
||||
onready var version_button := find_node("VersionButton")
|
||||
onready var translator := $PluginTranslator
|
||||
|
||||
@ -29,6 +43,25 @@ func _ready():
|
||||
for child in get_children():
|
||||
_hook_plugin(child)
|
||||
|
||||
var popup := extra_button.get_popup()
|
||||
popup.add_item(translator.tr("Save As..."), ExtraOption.SAVE_AS)
|
||||
popup.add_separator()
|
||||
popup.add_item(translator.tr("Copy"), ExtraOption.COPY)
|
||||
popup.add_item(translator.tr("Paste"), ExtraOption.PASTE)
|
||||
popup.add_item(translator.tr("Paste from jsfxr"), ExtraOption.PASTE_JSFXR)
|
||||
popup.add_separator(translator.tr("Recently Generated"))
|
||||
popup.connect("id_pressed", self, "_on_Extra_id_pressed")
|
||||
|
||||
_category_names = {
|
||||
SFXRConfig.Category.PICKUP_COIN: translator.tr("Pickup/Coin"),
|
||||
SFXRConfig.Category.LASER_SHOOT: translator.tr("Laser/Shoot"),
|
||||
SFXRConfig.Category.EXPLOSION: translator.tr("Explosion"),
|
||||
SFXRConfig.Category.POWERUP: translator.tr("Powerup"),
|
||||
SFXRConfig.Category.HIT_HURT: translator.tr("Hit/Hurt"),
|
||||
SFXRConfig.Category.JUMP: translator.tr("Jump"),
|
||||
SFXRConfig.Category.BLIP_SELECT: translator.tr("Blip/Select"),
|
||||
}
|
||||
|
||||
var params := find_node("Params") as Container
|
||||
for category in params.get_children():
|
||||
for control in category.get_children():
|
||||
@ -45,13 +78,18 @@ func _notification(what: int):
|
||||
|
||||
match what:
|
||||
NOTIFICATION_ENTER_TREE, NOTIFICATION_THEME_CHANGED:
|
||||
find_node("ScrollContainer").add_stylebox_override("bg", get_stylebox("bg", "Tree"))
|
||||
find_node("ScrollContainer").add_theme_stylebox_override("bg", get_theme_stylebox("bg", "Tree"))
|
||||
|
||||
if extra_button:
|
||||
var popup = extra_button.get_popup()
|
||||
popup.set_item_icon(popup.get_item_index(ExtraOption.COPY), get_theme_icon("ActionCopy", "EditorIcons"))
|
||||
popup.set_item_icon(popup.get_item_index(ExtraOption.PASTE), get_theme_icon("ActionPaste", "EditorIcons"))
|
||||
|
||||
|
||||
func edit(path: String) -> void:
|
||||
if _modified:
|
||||
_popup_confirm(
|
||||
translator.tr("There are unsaved changes.\nOpen '%s' anyway?") % path,
|
||||
str(translator.tr("There are unsaved changes.\nOpen '%s' anyway?")) % path,
|
||||
"_set_editing_file", [path]
|
||||
)
|
||||
else:
|
||||
@ -65,6 +103,20 @@ func _hook_plugin(node: Node) -> void:
|
||||
_hook_plugin(child)
|
||||
|
||||
|
||||
func _push_recent(title: String) -> void:
|
||||
var recent: RecentEntry
|
||||
if _config_recents.size() < NUM_RECENTS:
|
||||
recent = RecentEntry.new()
|
||||
else:
|
||||
recent = _config_recents.pop_back()
|
||||
|
||||
_recents_id += 1
|
||||
recent.title = "#%d %s" % [_recents_id, title]
|
||||
recent.config.copy_from(_config)
|
||||
|
||||
_config_recents.push_front(recent)
|
||||
|
||||
|
||||
func _popup_confirm(content: String, callback: String, binds := []) -> void:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
add_child(dialog)
|
||||
@ -84,23 +136,39 @@ func _popup_message(content: String) -> void:
|
||||
dialog.popup_centered()
|
||||
|
||||
|
||||
func _popup_file_dialog(mode: int, callback: String) -> void:
|
||||
func _popup_file_dialog(mode: int, callback: String, default_filename: int = DefaultFilename.EMPTY) -> void:
|
||||
var dialog := EditorFileDialog.new()
|
||||
add_child(dialog)
|
||||
dialog.access = EditorFileDialog.ACCESS_RESOURCES
|
||||
dialog.mode = mode
|
||||
|
||||
match default_filename:
|
||||
DefaultFilename.EMPTY:
|
||||
pass
|
||||
|
||||
DefaultFilename.GUESS_FOR_SAVE:
|
||||
if _path:
|
||||
dialog.current_path = _generate_serial_path(_path)
|
||||
|
||||
dialog.add_filter("*.sfxr; %s" % translator.tr("SFXR Audio"))
|
||||
dialog.connect("popup_hide", dialog, "queue_free")
|
||||
dialog.connect("file_selected", self, callback)
|
||||
dialog.popup_centered_ratio()
|
||||
|
||||
|
||||
func _reset_defaults() -> void: # SFXRConfig
|
||||
func _reset_defaults() -> void:
|
||||
_config_defaults.copy_from(_config)
|
||||
_set_modified(false)
|
||||
_sync_ui()
|
||||
|
||||
|
||||
func _restore_from_config(config: SFXRConfig) -> void:
|
||||
_config.copy_from(config)
|
||||
_sync_ui()
|
||||
_set_modified(not config.is_equal(_config_defaults))
|
||||
audio_player.stream = null
|
||||
|
||||
|
||||
func _set_editing_file(path: String) -> int: # Error
|
||||
if path.empty():
|
||||
_config.reset()
|
||||
@ -108,7 +176,7 @@ func _set_editing_file(path: String) -> int: # Error
|
||||
else:
|
||||
var err := _config.load(path)
|
||||
if err != OK:
|
||||
_popup_message(translator.tr("'%s' is not a valid SFXR file.") % path)
|
||||
_popup_message(str(translator.tr("'%s' is not a valid SFXR file.")) % path)
|
||||
return err
|
||||
audio_player.stream = load(path)
|
||||
|
||||
@ -121,7 +189,7 @@ func _set_modified(value: bool) -> void:
|
||||
_modified = value
|
||||
|
||||
var has_file := not _path.empty()
|
||||
var base = _path if has_file else translator.tr("Unsaved sound")
|
||||
var base = _path if has_file else str(translator.tr("Unsaved sound"))
|
||||
if _modified:
|
||||
base += "(*)"
|
||||
filename_label.text = base
|
||||
@ -139,6 +207,37 @@ func _sync_ui() -> void:
|
||||
_syncing_ui = false
|
||||
|
||||
|
||||
func _generate_serial_path(path: String) -> String:
|
||||
var directory := Directory.new()
|
||||
if directory.open(path.get_base_dir()) != OK:
|
||||
return path
|
||||
|
||||
if not directory.file_exists(path.get_file()):
|
||||
return path
|
||||
|
||||
var basename := path.get_basename()
|
||||
var extension := path.get_extension()
|
||||
|
||||
# Extract trailing number.
|
||||
var num_string: String
|
||||
for i in range(basename.length() - 1, -1, -1):
|
||||
var c: String = basename[i]
|
||||
if "0" <= c and c <= "9":
|
||||
num_string = c + num_string
|
||||
else:
|
||||
break
|
||||
var number := num_string.to_int() if num_string else 0
|
||||
var name_string: String = basename.substr(0, basename.length() - num_string.length())
|
||||
|
||||
while true:
|
||||
number += 1
|
||||
var attemp := "%s%d.%s" % [name_string, number, extension]
|
||||
if not directory.file_exists(attemp):
|
||||
return attemp
|
||||
|
||||
return path # Unreachable
|
||||
|
||||
|
||||
func _on_param_changed(name, value):
|
||||
if _syncing_ui:
|
||||
return
|
||||
@ -174,8 +273,10 @@ func _on_Play_pressed(force_regenerate := false):
|
||||
func _on_Randomize_pressed(category: int):
|
||||
if category == -1:
|
||||
_config.randomize()
|
||||
_push_recent(translator.tr("Randomize"))
|
||||
else:
|
||||
_config.randomize_in_category(category)
|
||||
_push_recent(_category_names.get(category, "Unknown"))
|
||||
|
||||
_set_modified(true)
|
||||
_sync_ui()
|
||||
@ -185,6 +286,7 @@ func _on_Randomize_pressed(category: int):
|
||||
func _on_Mutate_pressed():
|
||||
_config.mutate()
|
||||
|
||||
_push_recent(translator.tr("Mutate"))
|
||||
_set_modified(true)
|
||||
_sync_ui()
|
||||
_on_Play_pressed(true)
|
||||
@ -233,3 +335,53 @@ func _on_Load_pressed():
|
||||
else:
|
||||
_popup_file_dialog(EditorFileDialog.MODE_OPEN_FILE, "_set_editing_file")
|
||||
|
||||
|
||||
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 OS.has_clipboard())
|
||||
|
||||
# Rebuild recents menu everytime :)
|
||||
var first_recent_index := popup.get_item_index(ExtraOption.RECENT)
|
||||
if first_recent_index != -1:
|
||||
var count := popup.get_item_count()
|
||||
for i in count - first_recent_index:
|
||||
popup.remove_item(count - 1 - i)
|
||||
|
||||
if _config_recents.empty():
|
||||
popup.add_item(translator.tr("None"), ExtraOption.RECENT)
|
||||
popup.set_item_disabled(popup.get_item_index(ExtraOption.RECENT), true)
|
||||
else:
|
||||
for i in _config_recents.size():
|
||||
popup.add_item(_config_recents[i].title, ExtraOption.RECENT + i)
|
||||
|
||||
|
||||
func _on_Extra_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
ExtraOption.SAVE_AS:
|
||||
_popup_file_dialog(EditorFileDialog.MODE_SAVE_FILE, "_on_SaveAsDialog_confirmed", DefaultFilename.GUESS_FOR_SAVE)
|
||||
|
||||
ExtraOption.COPY:
|
||||
if not _config_clipboard:
|
||||
_config_clipboard = SFXRConfig.new()
|
||||
_config_clipboard.copy_from(_config)
|
||||
|
||||
ExtraOption.PASTE:
|
||||
_restore_from_config(_config_clipboard)
|
||||
|
||||
ExtraOption.PASTE_JSFXR:
|
||||
var pasted := SFXRConfig.new()
|
||||
if pasted.load_from_base58(OS.clipboard) == 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:
|
||||
printerr("Bad index %d (%d in total)" % [i, _config_recents.size()])
|
||||
return
|
||||
var recent: RecentEntry = _config_recents[i]
|
||||
_restore_from_config(recent.config)
|
||||
_on_Play_pressed()
|
||||
|
||||
|
@ -11,9 +11,6 @@
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource( 2 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
|
||||
@ -44,14 +41,22 @@ hint_tooltip = "Save"
|
||||
script = ExtResource( 1 )
|
||||
icon_name = "Save"
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="Toolbar"]
|
||||
[node name="Extra" type="MenuButton" parent="Toolbar"]
|
||||
margin_left = 48.0
|
||||
margin_right = 52.0
|
||||
margin_right = 60.0
|
||||
margin_bottom = 22.0
|
||||
hint_tooltip = "Extra Options"
|
||||
script = ExtResource( 1 )
|
||||
icon_name = "GuiTabMenuHl"
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="Toolbar"]
|
||||
margin_left = 64.0
|
||||
margin_right = 68.0
|
||||
margin_bottom = 22.0
|
||||
|
||||
[node name="Play" type="Button" parent="Toolbar"]
|
||||
margin_left = 56.0
|
||||
margin_right = 136.0
|
||||
margin_left = 72.0
|
||||
margin_right = 152.0
|
||||
margin_bottom = 22.0
|
||||
rect_min_size = Vector2( 80, 0 )
|
||||
size_flags_horizontal = 0
|
||||
@ -60,8 +65,8 @@ script = ExtResource( 1 )
|
||||
icon_name = "Play"
|
||||
|
||||
[node name="Restore" type="Button" parent="Toolbar"]
|
||||
margin_left = 140.0
|
||||
margin_right = 220.0
|
||||
margin_left = 156.0
|
||||
margin_right = 236.0
|
||||
margin_bottom = 22.0
|
||||
rect_min_size = Vector2( 80, 0 )
|
||||
hint_tooltip = "Restore"
|
||||
@ -71,12 +76,12 @@ script = ExtResource( 1 )
|
||||
icon_name = "Reload"
|
||||
|
||||
[node name="VSeparator2" type="VSeparator" parent="Toolbar"]
|
||||
margin_left = 224.0
|
||||
margin_right = 228.0
|
||||
margin_left = 240.0
|
||||
margin_right = 244.0
|
||||
margin_bottom = 22.0
|
||||
|
||||
[node name="Filename" type="Label" parent="Toolbar"]
|
||||
margin_left = 232.0
|
||||
margin_left = 248.0
|
||||
margin_top = 4.0
|
||||
margin_right = 1020.0
|
||||
margin_bottom = 18.0
|
||||
@ -216,6 +221,7 @@ margin_right = 211.0
|
||||
margin_bottom = 126.0
|
||||
label = "Change Amount"
|
||||
parameter = "p_arp_mod"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider6" parent="Editor/ScrollContainer/Params/Envolope" instance=ExtResource( 3 )]
|
||||
margin_top = 130.0
|
||||
@ -255,6 +261,7 @@ margin_right = 210.0
|
||||
margin_bottom = 74.0
|
||||
label = "Slide"
|
||||
parameter = "p_freq_ramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider4" parent="Editor/ScrollContainer/Params/Frequency" instance=ExtResource( 3 )]
|
||||
margin_top = 78.0
|
||||
@ -262,6 +269,7 @@ margin_right = 210.0
|
||||
margin_bottom = 100.0
|
||||
label = "Delta Slide"
|
||||
parameter = "p_freq_dramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider5" parent="Editor/ScrollContainer/Params/Frequency" instance=ExtResource( 3 )]
|
||||
margin_top = 104.0
|
||||
@ -308,6 +316,7 @@ margin_right = 220.0
|
||||
margin_bottom = 74.0
|
||||
label = "Duty Sweep"
|
||||
parameter = "p_duty_ramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider5" parent="Editor/ScrollContainer/Params/Waveform" instance=ExtResource( 3 )]
|
||||
margin_top = 78.0
|
||||
@ -315,6 +324,7 @@ margin_right = 220.0
|
||||
margin_bottom = 100.0
|
||||
label = "Phaser Offset"
|
||||
parameter = "p_pha_offset"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider3" parent="Editor/ScrollContainer/Params/Waveform" instance=ExtResource( 3 )]
|
||||
margin_top = 104.0
|
||||
@ -322,6 +332,7 @@ margin_right = 220.0
|
||||
margin_bottom = 126.0
|
||||
label = "Phaser Sweep"
|
||||
parameter = "p_pha_ramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="Filter" type="VBoxContainer" parent="Editor/ScrollContainer/Params"]
|
||||
margin_left = 653.0
|
||||
@ -340,6 +351,7 @@ margin_right = 242.0
|
||||
margin_bottom = 48.0
|
||||
label = "Low-pass Sweep"
|
||||
parameter = "p_lpf_ramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="ParamSlider5" parent="Editor/ScrollContainer/Params/Filter" instance=ExtResource( 3 )]
|
||||
margin_top = 52.0
|
||||
@ -361,12 +373,14 @@ margin_right = 242.0
|
||||
margin_bottom = 126.0
|
||||
label = "High-pass Sweep"
|
||||
parameter = "p_hpf_ramp"
|
||||
bipolar = true
|
||||
|
||||
[node name="PluginTranslator" parent="." instance=ExtResource( 5 )]
|
||||
|
||||
[connection signal="pressed" from="Toolbar/New" to="." method="_on_New_pressed"]
|
||||
[connection signal="pressed" from="Toolbar/Load" to="." method="_on_Load_pressed"]
|
||||
[connection signal="pressed" from="Toolbar/Save" to="." method="_on_Save_pressed"]
|
||||
[connection signal="about_to_show" from="Toolbar/Extra" to="." method="_on_Extra_about_to_show"]
|
||||
[connection signal="pressed" from="Toolbar/Play" to="." method="_on_Play_pressed"]
|
||||
[connection signal="pressed" from="Toolbar/Restore" to="." method="_on_Restore_pressed"]
|
||||
[connection signal="pressed" from="Editor/Generators/Button" to="." method="_on_Randomize_pressed" binds= [ 0 ]]
|
||||
|
@ -13,4 +13,4 @@ func _notification(what: int):
|
||||
match what:
|
||||
NOTIFICATION_ENTER_TREE, NOTIFICATION_THEME_CHANGED:
|
||||
if icon_name:
|
||||
icon = get_icon(icon_name, "EditorIcons")
|
||||
icon = get_theme_icon(icon_name, "EditorIcons")
|
||||
|
@ -9,13 +9,10 @@ margin_bottom = 40.0
|
||||
rect_pivot_offset = Vector2( 41, -65 )
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
margin_top = 13.0
|
||||
margin_right = 133.0
|
||||
margin_right = 128.0
|
||||
margin_bottom = 27.0
|
||||
rect_min_size = Vector2( 100, 0 )
|
||||
size_flags_horizontal = 3
|
||||
@ -23,11 +20,11 @@ text = "Waveform"
|
||||
align = 2
|
||||
|
||||
[node name="OptionButton" type="OptionButton" parent="."]
|
||||
margin_left = 137.0
|
||||
margin_left = 132.0
|
||||
margin_top = 10.0
|
||||
margin_right = 237.0
|
||||
margin_bottom = 30.0
|
||||
rect_min_size = Vector2( 100, 0 )
|
||||
rect_min_size = Vector2( 105, 0 )
|
||||
size_flags_vertical = 4
|
||||
clip_text = true
|
||||
|
||||
@ -36,7 +33,9 @@ margin_left = 241.0
|
||||
margin_top = 9.0
|
||||
margin_right = 253.0
|
||||
margin_bottom = 31.0
|
||||
focus_mode = 0
|
||||
size_flags_vertical = 4
|
||||
enabled_focus_mode = 0
|
||||
script = ExtResource( 2 )
|
||||
icon_name = "ReloadSmall"
|
||||
|
||||
|
@ -6,6 +6,7 @@ signal param_reset(name)
|
||||
|
||||
export var label: String setget set_label
|
||||
export var parameter: String
|
||||
export var bipolar := false setget set_bipolar
|
||||
|
||||
|
||||
func set_label(v: String) -> void:
|
||||
@ -13,6 +14,14 @@ func set_label(v: String) -> void:
|
||||
$Label.text = v
|
||||
|
||||
|
||||
func set_bipolar(v: bool) -> void:
|
||||
bipolar = v
|
||||
if bipolar:
|
||||
$HSlider.min_value = -1.0
|
||||
else:
|
||||
$HSlider.min_value = 0.0
|
||||
|
||||
|
||||
func set_value(v: float) -> void:
|
||||
$HSlider.value = v
|
||||
|
||||
@ -31,3 +40,4 @@ func _on_HSlider_value_changed(value: float):
|
||||
|
||||
func _on_Reset_pressed():
|
||||
emit_signal("param_reset", parameter)
|
||||
|
||||
|
@ -1,40 +1,39 @@
|
||||
[gd_scene load_steps=3 format=2]
|
||||
[gd_scene load_steps=4 format=2]
|
||||
|
||||
[ext_resource path="res://addons/gdfxr/editor/ParamSlider.gd" type="Script" id=1]
|
||||
[ext_resource path="res://addons/gdfxr/editor/EditorIconButton.gd" type="Script" id=2]
|
||||
[ext_resource path="res://addons/gdfxr/editor/EditSlider.gd" type="Script" id=3]
|
||||
|
||||
[node name="ParamSlider" type="HBoxContainer"]
|
||||
margin_right = 253.0
|
||||
margin_bottom = 40.0
|
||||
size_flags_horizontal = 3
|
||||
script = ExtResource( 1 )
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
margin_top = 13.0
|
||||
margin_right = 143.0
|
||||
margin_right = 128.0
|
||||
margin_bottom = 27.0
|
||||
rect_min_size = Vector2( 100, 0 )
|
||||
size_flags_horizontal = 3
|
||||
align = 2
|
||||
|
||||
[node name="HSlider" type="HSlider" parent="."]
|
||||
margin_left = 147.0
|
||||
margin_top = 12.0
|
||||
[node name="HSlider" type="Control" parent="."]
|
||||
margin_left = 132.0
|
||||
margin_right = 237.0
|
||||
margin_bottom = 28.0
|
||||
rect_min_size = Vector2( 90, 0 )
|
||||
size_flags_vertical = 4
|
||||
max_value = 1.0
|
||||
step = 0.0
|
||||
margin_bottom = 40.0
|
||||
rect_min_size = Vector2( 105, 0 )
|
||||
rect_clip_content = true
|
||||
focus_mode = 2
|
||||
mouse_default_cursor_shape = 10
|
||||
script = ExtResource( 3 )
|
||||
|
||||
[node name="Reset" type="ToolButton" parent="."]
|
||||
margin_left = 241.0
|
||||
margin_top = 9.0
|
||||
margin_right = 253.0
|
||||
margin_bottom = 31.0
|
||||
focus_mode = 0
|
||||
size_flags_vertical = 4
|
||||
script = ExtResource( 2 )
|
||||
icon_name = "ReloadSmall"
|
||||
|
@ -25,10 +25,10 @@ func set_plugin(v: EditorPlugin) -> void:
|
||||
_translate_node(get_parent())
|
||||
|
||||
|
||||
func tr(message: String) -> String:
|
||||
func tr(message: StringName) -> StringName:
|
||||
if _translation:
|
||||
var translated := _translation.get_message(message)
|
||||
if not translated.empty():
|
||||
if translated != "":
|
||||
return translated
|
||||
return message
|
||||
|
||||
@ -48,6 +48,9 @@ func _translate_node(node: Node):
|
||||
|
||||
if node is Label:
|
||||
node.text = tr(node.text)
|
||||
|
||||
if node is Slider:
|
||||
node.hint_tooltip = tr(node.hint_tooltip)
|
||||
|
||||
for child in node.get_children():
|
||||
_translate_node(child)
|
||||
|
@ -8,14 +8,62 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: gdfxr 1.0\n"
|
||||
"Report-Msgid-Bugs-To: timothyqiu32@gmail.com\n"
|
||||
"POT-Creation-Date: 2022-02-25 17:37+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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
"Generated-By: Babel 2.11.0\n"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Save As..."
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Paste"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Paste from jsfxr"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Recently Generated"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Pickup/Coin"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Laser/Shoot"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Explosion"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Powerup"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Hit/Hurt"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Blip/Select"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
#, python-format
|
||||
@ -24,7 +72,7 @@ msgid ""
|
||||
"Open '%s' anyway?"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.gd
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "SFXR Editor"
|
||||
msgstr ""
|
||||
|
||||
@ -41,6 +89,14 @@ msgstr ""
|
||||
msgid "Unsaved sound"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Randomize"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Mutate"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid ""
|
||||
"There are unsaved changes.\n"
|
||||
@ -53,6 +109,14 @@ msgid ""
|
||||
"Load anyway?"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
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 ""
|
||||
@ -65,50 +129,18 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Extra Options"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Play"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn addons/gdfxr/editor/Editor.tscn
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Restore"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Pickup/Coin"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Laser/Shoot"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Explosion"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Powerup"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Hit/Hurt"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Jump"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Blip/Select"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Mutate"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Randomize"
|
||||
msgstr ""
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Attack Time"
|
||||
msgstr ""
|
||||
|
@ -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-02-25 17:37+0800\n"
|
||||
"PO-Revision-Date: 2022-02-25 17:38+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 <timothyqiu32@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: zh_CN\n"
|
||||
@ -17,7 +17,55 @@ msgstr ""
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"Generated-By: Babel 2.9.1\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
"X-Generator: Poedit 3.2.1\n"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Save As..."
|
||||
msgstr "另存为..."
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Copy"
|
||||
msgstr "复制"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Paste"
|
||||
msgstr "粘贴"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Paste from jsfxr"
|
||||
msgstr "从 jsfxr 粘贴"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid "Recently Generated"
|
||||
msgstr "最近生成"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Pickup/Coin"
|
||||
msgstr "拾取/金币"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Laser/Shoot"
|
||||
msgstr "激光/射击"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Explosion"
|
||||
msgstr "爆炸"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Powerup"
|
||||
msgstr "升级"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Hit/Hurt"
|
||||
msgstr "击中/受伤"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Jump"
|
||||
msgstr "跳跃"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Blip/Select"
|
||||
msgstr "短滴/选择"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
#, python-format
|
||||
@ -45,6 +93,14 @@ msgstr "“%s”不是有效的 SFXR 文件。"
|
||||
msgid "Unsaved sound"
|
||||
msgstr "未保存音效"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Randomize"
|
||||
msgstr "随机"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Mutate"
|
||||
msgstr "演化"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
msgid ""
|
||||
"There are unsaved changes.\n"
|
||||
@ -61,6 +117,14 @@ msgstr ""
|
||||
"存在未保存的修改。\n"
|
||||
"仍然要加载吗?"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.gd
|
||||
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 "新建"
|
||||
@ -73,6 +137,10 @@ msgstr "加载"
|
||||
msgid "Save"
|
||||
msgstr "保存"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Extra Options"
|
||||
msgstr "更多选项"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Play"
|
||||
msgstr "播放"
|
||||
@ -81,42 +149,6 @@ msgstr "播放"
|
||||
msgid "Restore"
|
||||
msgstr "恢复"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Pickup/Coin"
|
||||
msgstr "拾取/金币"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Laser/Shoot"
|
||||
msgstr "激光/射击"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Explosion"
|
||||
msgstr "爆炸"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Powerup"
|
||||
msgstr "升级"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Hit/Hurt"
|
||||
msgstr "击中/受伤"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Jump"
|
||||
msgstr "跳跃"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Blip/Select"
|
||||
msgstr "短滴/选择"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Mutate"
|
||||
msgstr "演化"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Randomize"
|
||||
msgstr "随机"
|
||||
|
||||
#: addons/gdfxr/editor/Editor.tscn
|
||||
msgid "Attack Time"
|
||||
msgstr "起音时间"
|
||||
@ -228,3 +260,6 @@ msgstr "高通变频"
|
||||
#: addons/gdfxr/editor/ParamOption.tscn
|
||||
msgid "Waveform"
|
||||
msgstr "波形"
|
||||
|
||||
#~ msgid "Hold Ctrl to snap to 0.01 increments."
|
||||
#~ msgstr "按住 Ctrl 吸附到 0.01 增量。"
|
||||
|
@ -36,8 +36,20 @@ func get_preset_name(preset):
|
||||
func get_import_options(preset):
|
||||
return [
|
||||
{
|
||||
name="loop",
|
||||
default_value=false,
|
||||
"name":"loop",
|
||||
"default_value":false,
|
||||
},
|
||||
{
|
||||
"name":"bit_depth",
|
||||
"property_hint":PROPERTY_HINT_ENUM,
|
||||
"hint_string":"8 Bits,16 Bits",
|
||||
"default_value":SFXRGenerator.WavBits.WAV_BITS_8,
|
||||
},
|
||||
{
|
||||
"name":"sample_rate",
|
||||
"property_hint":PROPERTY_HINT_ENUM,
|
||||
"hint_string":"44100 Hz,22050 Hz",
|
||||
"default_value":SFXRGenerator.WavFreq.WAV_FREQ_44100,
|
||||
},
|
||||
]
|
||||
|
||||
@ -53,7 +65,9 @@ func import(source_file, save_path, options, platform_variants, gen_files):
|
||||
printerr("Failed to open %s: %d" % [source_file, err])
|
||||
return err
|
||||
|
||||
var stream := SFXRGenerator.new().generate_audio_stream(config)
|
||||
var stream := SFXRGenerator.new().generate_audio_stream(
|
||||
config, options.bit_depth, options.sample_rate
|
||||
)
|
||||
if options.loop:
|
||||
stream.loop_mode = AudioStreamSample.LOOP_FORWARD
|
||||
stream.loop_end = stream.data.size()
|
||||
|
@ -3,5 +3,5 @@
|
||||
name="gdfxr"
|
||||
description="A Godot plugin that ports sfxr, the popular program of choice to make retro sound effects for games."
|
||||
author="Haoyu Qiu"
|
||||
version="1.0"
|
||||
version="1.3"
|
||||
script="plugin.gd"
|
||||
|
38
example/Example.gd
Normal file
38
example/Example.gd
Normal file
@ -0,0 +1,38 @@
|
||||
extends Container
|
||||
|
||||
# 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()
|
@ -1,23 +1,74 @@
|
||||
[gd_scene load_steps=2 format=2]
|
||||
[gd_scene load_steps=3 format=2]
|
||||
|
||||
[ext_resource path="res://example/example.sfxr" type="AudioStream" id=1]
|
||||
[ext_resource path="res://example/Example.gd" type="Script" id=1]
|
||||
[ext_resource path="res://example/example.sfxr" type="AudioStream" id=2]
|
||||
|
||||
[node name="Example" type="CenterContainer"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
__meta__ = {
|
||||
"_edit_use_anchors_": false
|
||||
}
|
||||
[node name="Example" type="GridContainer"]
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
custom_constants/hseparation = 32
|
||||
custom_constants/vseparation = 32
|
||||
columns = 2
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="Button" type="Button" parent="."]
|
||||
margin_left = 448.0
|
||||
margin_top = 284.0
|
||||
margin_right = 576.0
|
||||
margin_bottom = 316.0
|
||||
[node name="AudioPlayer" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 2 )
|
||||
|
||||
[node name="AdhocAudioPlayer" type="AudioStreamPlayer" parent="."]
|
||||
|
||||
[node name="Play" type="Button" parent="."]
|
||||
margin_right = 141.0
|
||||
margin_bottom = 32.0
|
||||
rect_min_size = Vector2( 128, 32 )
|
||||
size_flags_vertical = 4
|
||||
text = "Play"
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
|
||||
stream = ExtResource( 1 )
|
||||
[node name="Label" type="Label" parent="."]
|
||||
margin_left = 173.0
|
||||
margin_right = 473.0
|
||||
margin_bottom = 31.0
|
||||
rect_min_size = Vector2( 300, 0 )
|
||||
text = "A .sfxr file can be used as regular audio files like .wav, .ogg, and .mp3."
|
||||
autowrap = true
|
||||
|
||||
[connection signal="pressed" from="Button" to="AudioStreamPlayer" method="play"]
|
||||
[node name="PlayFile" type="Button" parent="."]
|
||||
margin_top = 64.0
|
||||
margin_right = 141.0
|
||||
margin_bottom = 96.0
|
||||
rect_min_size = Vector2( 128, 32 )
|
||||
size_flags_vertical = 4
|
||||
text = "Load .sfxr File"
|
||||
|
||||
[node name="Label2" type="Label" parent="."]
|
||||
margin_left = 173.0
|
||||
margin_top = 64.0
|
||||
margin_right = 473.0
|
||||
margin_bottom = 95.0
|
||||
rect_min_size = Vector2( 300, 0 )
|
||||
text = "A .sfxr file is a AudioStreamSample resource that can be loaded with load() or preload()."
|
||||
autowrap = true
|
||||
|
||||
[node name="Generate" type="Button" parent="."]
|
||||
margin_top = 144.0
|
||||
margin_right = 141.0
|
||||
margin_bottom = 176.0
|
||||
rect_min_size = Vector2( 128, 32 )
|
||||
size_flags_vertical = 4
|
||||
text = "Runtime Generation"
|
||||
|
||||
[node name="Label3" type="Label" parent="."]
|
||||
margin_left = 173.0
|
||||
margin_top = 128.0
|
||||
margin_right = 473.0
|
||||
margin_bottom = 193.0
|
||||
rect_min_size = Vector2( 300, 0 )
|
||||
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 = true
|
||||
|
||||
[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"]
|
||||
|
@ -12,3 +12,5 @@ dest_files=[ "res://.import/example.sfxr-e3731bcdcd8403a2391e92667a5d1076.sample
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
bit_depth=0
|
||||
sample_rate=0
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
Loading…
Reference in New Issue
Block a user