Compare commits

...

9 Commits

Author SHA1 Message Date
d1e4b34cdc Fix everything for Pandemonium. 2023-08-12 10:02:42 +02:00
Haoyu Qiu
7b3194f500 1.3 Release 2023-02-23 21:20:46 +08:00
Haoyu Qiu
4562f4b77c Add default filename for Save As dialog
Trailing number incremented.

Co-Authored-By: Russell Matney <russell.matney@gmail.com>
2023-02-16 09:53:49 +08:00
Haoyu Qiu
576708bb4a Add import options for bit depth and sample rate 2023-01-04 17:49:34 +08:00
Haoyu Qiu
5b910afb64 Implement paste from jsfxr 2022-12-04 14:01:44 +08:00
Haoyu Qiu
9acce6be58 Add Godot 4 information in README 2022-10-16 11:48:06 +08:00
Haoyu Qiu
0621590150 1.2 Release 2022-10-13 11:23:15 +08:00
Haoyu Qiu
b5322f430f Add custom slider
This is a slider inspired by those used in Blender.

The control is a draggable slider displaying the current value as its
background. Clicking the control brings up a LineEdit for manual value
input.

When dragging the slider, hold CTRL to round to 0.1 increments or hold
SHIFT to round to 0.01 increments. Holding both CTRL and SHIFT rounds to
0.001 increments.
2022-09-25 11:19:17 +08:00
Haoyu Qiu
2a73615082 Slider improvements
- Sliders now display current value when hovered.
- While dragging sliders, hold CTRL to snap the value to 0.01
  increments.
2022-09-20 14:07:12 +08:00
23 changed files with 582 additions and 71 deletions

View File

@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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.
@ -23,3 +33,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[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

View File

@ -10,6 +10,8 @@
你可以在 Godot 中把 sfxr 音效文件当作普通的音频文件使用,也可以像在原始的 sfxr 中一样对音效进行编辑。
> 🚧 如果你想在 Godot 4 里使用这个插件,请移步 [godot-4](https://github.com/timothyqiu/gdfxr/tree/godot-4) 分支。
## 安装
这是一个普通的 Godot 插件。安装时,先下载 ZIP 包,解压后将 `addons/` 文件夹移动到你的项目文件夹中,然后在项目设置中启用本插件。
@ -30,6 +32,8 @@
如果你希望使用原始的 sfxr 保存出的文件,请确保使用 `.sfxr` 扩展名保存。你也可以在原始的 sfxr 中加载并编辑 `.sfxr` 文件。
`.sfxr` 文件有循环Loop、位深度Bit Depth、采样率Sample Rate等导入选项。可以在 Godot 编辑器的导入面板中找到。
**注意:** 由于 GDScript 的性能限制,生成较长的音效时编辑器可能会有短暂的停滞。只有编辑器会受此影响。在游戏中使用 `.sfxr` 文件是不会在运行时生成任何东西的。
## 更新日志

View File

@ -12,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
@ -40,6 +42,9 @@ 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.

38
addons/gdfxr/Base58.gd Normal file
View 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

View File

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

View File

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

View 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

View File

@ -1,10 +1,12 @@
tool
extends Container
enum ExtraOption { SAVE_AS, COPY, PASTE, RECENT }
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:
@ -46,6 +48,7 @@ func _ready():
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")
@ -75,18 +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_icon("ActionCopy", "EditorIcons"))
popup.set_item_icon(popup.get_item_index(ExtraOption.PASTE), get_icon("ActionPaste", "EditorIcons"))
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:
@ -133,11 +136,20 @@ 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)
@ -164,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)
@ -177,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
@ -195,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
@ -296,6 +339,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 OS.has_clipboard())
# Rebuild recents menu everytime :)
var first_recent_index := popup.get_item_index(ExtraOption.RECENT)
@ -315,7 +359,7 @@ func _on_Extra_about_to_show():
func _on_Extra_id_pressed(id: int) -> void:
match id:
ExtraOption.SAVE_AS:
_popup_file_dialog(EditorFileDialog.MODE_SAVE_FILE, "_on_SaveAsDialog_confirmed")
_popup_file_dialog(EditorFileDialog.MODE_SAVE_FILE, "_on_SaveAsDialog_confirmed", DefaultFilename.GUESS_FOR_SAVE)
ExtraOption.COPY:
if not _config_clipboard:
@ -325,6 +369,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(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:

View File

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

View File

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

View File

@ -40,3 +40,4 @@ func _on_HSlider_value_changed(value: float):
func _on_Reset_pressed():
emit_signal("param_reset", parameter)

View File

@ -1,7 +1,8 @@
[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
@ -11,27 +12,28 @@ script = ExtResource( 1 )
[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"

View File

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

View File

@ -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-03-10 17:02+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..."
@ -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 ""
@ -68,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 ""
@ -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 ""
@ -129,7 +137,7 @@ msgstr ""
msgid "Play"
msgstr ""
#: addons/gdfxr/editor/Editor.tscn addons/gdfxr/editor/Editor.tscn
#: addons/gdfxr/editor/Editor.tscn
msgid "Restore"
msgstr ""

View File

@ -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-03-10 17:02+0800\n"
"PO-Revision-Date: 2022-03-10 17:02+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,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.0.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 "新建"
@ -252,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 增量。"

View File

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

View File

@ -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.1.1"
version="1.3"
script="plugin.gd"

38
example/Example.gd Normal file
View 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()

View File

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

View File

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