From cc0ab5949f7ff2aa6f538ed193774e07e73496f4 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sat, 7 Dec 2024 23:41:44 +0200 Subject: [PATCH 01/26] Initial work on audio layers --- project.godot | 4 --- src/Autoload/ExtensionsApi.gd | 2 +- src/Autoload/Global.gd | 2 +- src/Autoload/Tools.gd | 5 +++- src/Classes/Cels/AudioCel.gd | 18 +++++++++++++ src/Classes/Layers/AudioLayer.gd | 36 ++++++++++++++++++++++++++ src/Classes/Project.gd | 4 +-- src/UI/Timeline/AnimationTimeline.gd | 2 ++ src/UI/Timeline/AnimationTimeline.tscn | 4 ++- src/UI/Timeline/LayerButton.gd | 13 +++++++++- src/UI/Timeline/LayerButton.tscn | 1 - 11 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 src/Classes/Cels/AudioCel.gd create mode 100644 src/Classes/Layers/AudioLayer.gd diff --git a/project.godot b/project.godot index aa3a195ef..0d3ae5115 100644 --- a/project.godot +++ b/project.godot @@ -27,10 +27,6 @@ config/windows_native_icon="res://assets/graphics/icons/icon.ico" config/ExtensionsAPI_Version=5 config/Pxo_Version=4 -[audio] - -driver/driver="Dummy" - [autoload] Global="*res://src/Autoload/Global.gd" diff --git a/src/Autoload/ExtensionsApi.gd b/src/Autoload/ExtensionsApi.gd index 2ef1820aa..65686e618 100644 --- a/src/Autoload/ExtensionsApi.gd +++ b/src/Autoload/ExtensionsApi.gd @@ -631,7 +631,7 @@ class ProjectAPI: ## Returns the current cel. ## Cel type can be checked using function [method get_class_name] inside the cel - ## type can be GroupCel, PixelCel, Cel3D, or BaseCel. + ## type can be GroupCel, PixelCel, Cel3D, CelTileMap, AudioCel or BaseCel. func get_current_cel() -> BaseCel: return current_project.get_current_cel() diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 295ec1457..00e17c014 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -14,7 +14,7 @@ signal cel_switched ## Emitted whenever you select a different cel. signal project_data_changed(project: Project) ## Emitted when project data is modified. signal font_loaded ## Emitted when a new font has been loaded, or an old one gets unloaded. -enum LayerTypes { PIXEL, GROUP, THREE_D, TILEMAP } +enum LayerTypes { PIXEL, GROUP, THREE_D, TILEMAP, AUDIO } enum GridTypes { CARTESIAN, ISOMETRIC, ALL } ## ## Used to tell whether a color is being taken from the current theme, ## or if it is a custom color. diff --git a/src/Autoload/Tools.gd b/src/Autoload/Tools.gd index 2b7b13455..7a330724d 100644 --- a/src/Autoload/Tools.gd +++ b/src/Autoload/Tools.gd @@ -800,7 +800,10 @@ func _cel_switched() -> void: var layer: BaseLayer = Global.current_project.layers[Global.current_project.current_layer] var layer_type := layer.get_layer_type() # Do not make any changes when its the same type of layer, or a group layer - if layer_type == _curr_layer_type or layer_type == Global.LayerTypes.GROUP: + if ( + layer_type == _curr_layer_type + or layer_type in [Global.LayerTypes.GROUP, Global.LayerTypes.AUDIO] + ): return _show_relevant_tools(layer_type) diff --git a/src/Classes/Cels/AudioCel.gd b/src/Classes/Cels/AudioCel.gd new file mode 100644 index 000000000..8f992c0fa --- /dev/null +++ b/src/Classes/Cels/AudioCel.gd @@ -0,0 +1,18 @@ +class_name AudioCel +extends BaseCel +## A class for the properties of cels in AudioLayers. +## The term "cel" comes from "celluloid" (https://en.wikipedia.org/wiki/Cel). + + +func _init(_opacity := 1.0) -> void: + opacity = _opacity + image_texture = ImageTexture.new() + + +func get_image() -> Image: + var image := Global.current_project.new_empty_image() + return image + + +func get_class_name() -> String: + return "AudioCel" diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd new file mode 100644 index 000000000..9d677e014 --- /dev/null +++ b/src/Classes/Layers/AudioLayer.gd @@ -0,0 +1,36 @@ +class_name AudioLayer +extends BaseLayer + +signal audio_changed + +var audio: AudioStream: + set(value): + audio = value + audio_changed.emit() + + +func _init(_project: Project, _name := "") -> void: + project = _project + name = _name + + +# Overridden Methods: +func serialize() -> Dictionary: + var data := {"name": name, "type": get_layer_type()} + return data + + +func deserialize(dict: Dictionary) -> void: + super.deserialize(dict) + + +func get_layer_type() -> int: + return Global.LayerTypes.AUDIO + + +func new_empty_cel() -> AudioCel: + return AudioCel.new() + + +func set_name_to_default(number: int) -> void: + name = tr("Audio track") + " %s" % number diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 3d8ab17a1..f7d92dd77 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -640,10 +640,10 @@ func find_first_drawable_cel(frame := frames[current_frame]) -> BaseCel: var result: BaseCel var cel := frame.cels[0] var i := 0 - while cel is GroupCel and i < layers.size(): + while (cel is GroupCel or cel is AudioCel) and i < layers.size(): cel = frame.cels[i] i += 1 - if not cel is GroupCel: + if cel is not GroupCel and cel is not AudioCel: result = cel return result diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 2cf58ce6c..e9232d3ec 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -855,6 +855,8 @@ func _on_add_layer_list_id_pressed(id: int) -> void: Global.LayerTypes.THREE_D: layer = Layer3D.new(project) SteamManager.set_achievement("ACH_3D_LAYER") + Global.LayerTypes.AUDIO: + layer = AudioLayer.new(project) add_layer(layer, project) diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn index 622135afa..a13e6896b 100644 --- a/src/UI/Timeline/AnimationTimeline.tscn +++ b/src/UI/Timeline/AnimationTimeline.tscn @@ -240,7 +240,7 @@ offset_left = -22.0 offset_top = -10.0 offset_bottom = 10.0 mouse_default_cursor_shape = 2 -item_count = 4 +item_count = 5 popup/item_0/text = "Add Pixel Layer" popup/item_1/text = "Add Group Layer" popup/item_1/id = 1 @@ -248,6 +248,8 @@ popup/item_2/text = "Add 3D Layer" popup/item_2/id = 2 popup/item_3/text = "Add Tilemap Layer" popup/item_3/id = 3 +popup/item_4/text = "Add Audio Layer" +popup/item_4/id = 4 [node name="TextureRect" type="TextureRect" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons/AddLayer/AddLayerList"] layout_mode = 0 diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 800fea1e7..7c2aa0896 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -15,6 +15,7 @@ var button_pressed := false: get: return main_button.button_pressed +var audio_player: AudioStreamPlayer @onready var properties: AcceptDialog = Global.control.find_child("LayerProperties") @onready var main_button := %LayerMainButton as Button @onready var expand_button := %ExpandButton as BaseButton @@ -31,7 +32,7 @@ var button_pressed := false: func _ready() -> void: main_button.layer_index = layer_index main_button.hierarchy_depth_pixel_shift = HIERARCHY_DEPTH_PIXEL_SHIFT - Global.cel_switched.connect(func(): z_index = 1 if button_pressed else 0) + Global.cel_switched.connect(_on_cel_switched) var layer := Global.current_project.layers[layer_index] layer.name_changed.connect(func(): label.text = layer.name) layer.visibility_changed.connect(update_buttons) @@ -39,6 +40,10 @@ func _ready() -> void: linked_button.visible = true elif layer is GroupLayer: expand_button.visible = true + elif layer is AudioLayer: + audio_player = AudioStreamPlayer.new() + audio_player.stream = layer.audio + add_child(audio_player) custom_minimum_size.y = Global.animation_timeline.cel_size label.text = layer.name line_edit.text = layer.name @@ -56,6 +61,12 @@ func _ready() -> void: update_buttons() +func _on_cel_switched() -> void: + z_index = 1 if button_pressed else 0 + if is_instance_valid(audio_player): + audio_player.play(Global.current_project.current_frame / Global.current_project.fps) + + func update_buttons() -> void: var layer := Global.current_project.layers[layer_index] if layer is GroupLayer: diff --git a/src/UI/Timeline/LayerButton.tscn b/src/UI/Timeline/LayerButton.tscn index 57e0881bf..02267e664 100644 --- a/src/UI/Timeline/LayerButton.tscn +++ b/src/UI/Timeline/LayerButton.tscn @@ -169,7 +169,6 @@ caret_blink_interval = 0.5 disable_3d = true item_count = 2 item_0/text = "Properties" -item_0/id = 0 item_1/text = "Clipping mask" item_1/checkable = 1 item_1/id = 1 From 1b7494a76745959e25d0ec9a194d872b1e012269 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:23:38 +0200 Subject: [PATCH 02/26] Load ogg audio files --- src/Autoload/OpenSave.gd | 16 ++++++++++++++++ src/UI/Timeline/LayerButton.gd | 1 + 2 files changed, 17 insertions(+) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 0a8eb3237..cc709a8e4 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -45,6 +45,8 @@ func handle_loading_file(file: String) -> void: return var file_name: String = file.get_file().get_basename() Global.control.find_child("ShaderEffect").change_shader(shader, file_name) + elif file_ext == "ogg": # Audio file + open_audio_file(file) else: # Image files # Attempt to load as APNG. @@ -902,6 +904,20 @@ func set_new_imported_tab(project: Project, path: String) -> void: Global.tabs.delete_tab(prev_project_pos) +func open_audio_file(path: String) -> void: + var audio_stream := AudioStreamOggVorbis.load_from_file(path) + if not is_instance_valid(audio_stream): + return + var project := Global.current_project + for layer in project.layers: + if layer is AudioLayer and not is_instance_valid(layer.audio): + layer.audio = audio_stream + return + var new_layer := AudioLayer.new(project) + new_layer.audio = audio_stream + Global.animation_timeline.add_layer(new_layer, project) + + func update_autosave() -> void: if not is_instance_valid(autosave_timer): return diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 7c2aa0896..9ffbbdcbd 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -43,6 +43,7 @@ func _ready() -> void: elif layer is AudioLayer: audio_player = AudioStreamPlayer.new() audio_player.stream = layer.audio + layer.audio_changed.connect(func(): audio_player.stream = layer.audio) add_child(audio_player) custom_minimum_size.y = Global.animation_timeline.cel_size label.text = layer.name From 2c8c1ba8fd9642c65f41556d048960d0c0e6d5f0 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 02:58:02 +0200 Subject: [PATCH 03/26] Fix playback position --- src/UI/Timeline/LayerButton.gd | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 9ffbbdcbd..0962fe354 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -65,7 +65,12 @@ func _ready() -> void: func _on_cel_switched() -> void: z_index = 1 if button_pressed else 0 if is_instance_valid(audio_player): - audio_player.play(Global.current_project.current_frame / Global.current_project.fps) + var audio_length := audio_player.stream.get_length() + var normalized_pos := Global.current_project.current_frame / Global.current_project.fps + if normalized_pos < 1: + audio_player.play(normalized_pos * audio_length) + else: + audio_player.stop() func update_buttons() -> void: From e5d95c69e25f48a1bb92c82adf8f3b8b5f6ca052 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 02:58:10 +0200 Subject: [PATCH 04/26] Support mp3 files --- src/Autoload/OpenSave.gd | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index cc709a8e4..8eea808a6 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -45,7 +45,7 @@ func handle_loading_file(file: String) -> void: return var file_name: String = file.get_file().get_basename() Global.control.find_child("ShaderEffect").change_shader(shader, file_name) - elif file_ext == "ogg": # Audio file + elif file_ext == "ogg" or file_ext == "mp3": # Audio file open_audio_file(file) else: # Image files @@ -905,7 +905,14 @@ func set_new_imported_tab(project: Project, path: String) -> void: func open_audio_file(path: String) -> void: - var audio_stream := AudioStreamOggVorbis.load_from_file(path) + var audio_stream: AudioStream + var file_ext := path.get_extension().to_lower() + if file_ext == "ogg": + audio_stream = AudioStreamOggVorbis.load_from_file(path) + elif file_ext == "mp3": + var file := FileAccess.open(path, FileAccess.READ) + audio_stream = AudioStreamMP3.new() + audio_stream.data = file.get_buffer(file.get_length()) if not is_instance_valid(audio_stream): return var project := Global.current_project From f398a6159c4d17bdd44dc54818f2867f3e87afc5 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 03:20:31 +0200 Subject: [PATCH 05/26] Play audio at the appropriate position when the animation runs, and stop when the pause button is pressed --- src/UI/Timeline/LayerButton.gd | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 0962fe354..4163eb82b 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -14,6 +14,7 @@ var button_pressed := false: main_button.button_pressed = value get: return main_button.button_pressed +var animation_running := false var audio_player: AudioStreamPlayer @onready var properties: AcceptDialog = Global.control.find_child("LayerProperties") @@ -45,6 +46,8 @@ func _ready() -> void: audio_player.stream = layer.audio layer.audio_changed.connect(func(): audio_player.stream = layer.audio) add_child(audio_player) + Global.animation_timeline.animation_started.connect(_on_animation_started) + Global.animation_timeline.animation_finished.connect(_on_animation_finished) custom_minimum_size.y = Global.animation_timeline.cel_size label.text = layer.name line_edit.text = layer.name @@ -64,13 +67,30 @@ func _ready() -> void: func _on_cel_switched() -> void: z_index = 1 if button_pressed else 0 + if not animation_running or Global.current_project.current_frame == 0: + _play_audio() + + +func _on_animation_started(_dir: bool) -> void: + animation_running = true + _play_audio() + + +func _on_animation_finished() -> void: + animation_running = false if is_instance_valid(audio_player): - var audio_length := audio_player.stream.get_length() - var normalized_pos := Global.current_project.current_frame / Global.current_project.fps - if normalized_pos < 1: - audio_player.play(normalized_pos * audio_length) - else: - audio_player.stop() + audio_player.stop() + + +func _play_audio() -> void: + if not is_instance_valid(audio_player): + return + var audio_length := audio_player.stream.get_length() + var normalized_pos := Global.current_project.current_frame / Global.current_project.fps + if normalized_pos < 1: + audio_player.play(normalized_pos * audio_length) + else: + audio_player.stop() func update_buttons() -> void: From a7efc3eb038c21ff31c2271682b1467325dbe04c Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 03:53:58 +0200 Subject: [PATCH 06/26] Change audio cel textures for the cels where audio is playing --- src/Classes/Project.gd | 6 +++++- src/UI/Timeline/CelButton.gd | 16 +++++++++++++++- src/UI/Timeline/CelButton.tscn | 1 - 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index f7d92dd77..438401031 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -8,6 +8,7 @@ signal serialized(dict: Dictionary) signal about_to_deserialize(dict: Dictionary) signal resized signal timeline_updated +signal fps_changed const INDEXED_MODE := Image.FORMAT_MAX + 1 @@ -65,7 +66,10 @@ var brushes: Array[Image] = [] var reference_images: Array[ReferenceImage] = [] var reference_index: int = -1 # The currently selected index ReferenceImage var vanishing_points := [] ## Array of Vanishing Points -var fps := 6.0 +var fps := 6.0: + set(value): + fps = value + fps_changed.emit() var user_data := "" ## User defined data, set in the project properties. var x_symmetry_point: float diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index e3689e60d..8cf48a731 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -31,6 +31,9 @@ func _ready() -> void: popup_menu.add_item("Unlink Cels") elif cel is GroupCel: transparent_checker.visible = false + elif cel is AudioCel: + _is_playing_audio() + Global.current_project.fps_changed.connect(_is_playing_audio) func _notification(what: int) -> void: @@ -66,7 +69,8 @@ func button_setup() -> void: var base_layer := Global.current_project.layers[layer] tooltip_text = tr("Frame: %s, Layer: %s") % [frame + 1, base_layer.name] - cel_texture.texture = cel.image_texture + if cel is not AudioCel: + cel_texture.texture = cel.image_texture if is_instance_valid(linked): linked.visible = cel.link_set != null if cel.link_set != null: @@ -396,3 +400,13 @@ func _sort_cel_indices_by_frame(a: Array, b: Array) -> bool: if frame_a < frame_b: return true return false + + +func _is_playing_audio() -> void: + var layer := Global.current_project.layers[layer] as AudioLayer + var audio_length := layer.audio.get_length() + var final_frame := audio_length * Global.current_project.fps + if frame < final_frame: + cel_texture.texture = preload("res://assets/graphics/icons/icon.png") + else: + cel_texture.texture = null diff --git a/src/UI/Timeline/CelButton.tscn b/src/UI/Timeline/CelButton.tscn index 5861f2f92..4bdcd781d 100644 --- a/src/UI/Timeline/CelButton.tscn +++ b/src/UI/Timeline/CelButton.tscn @@ -74,7 +74,6 @@ grow_vertical = 2 [node name="PopupMenu" type="PopupMenu" parent="."] item_count = 1 item_0/text = "Properties" -item_0/id = 0 [connection signal="pressed" from="." to="." method="_on_CelButton_pressed"] [connection signal="id_pressed" from="PopupMenu" to="." method="_on_PopupMenu_id_pressed"] From 5242b96f64e7d5e1520309475b3d973cfcbafed5 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 03:54:15 +0200 Subject: [PATCH 07/26] Fix audio not playing at the appropriate position --- src/UI/Timeline/LayerButton.gd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 4163eb82b..91ac1e78a 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -86,9 +86,11 @@ func _play_audio() -> void: if not is_instance_valid(audio_player): return var audio_length := audio_player.stream.get_length() - var normalized_pos := Global.current_project.current_frame / Global.current_project.fps - if normalized_pos < 1: - audio_player.play(normalized_pos * audio_length) + var final_frame := audio_length * Global.current_project.fps + if Global.current_project.current_frame < final_frame: + var inverse_fps := 1.0 / Global.current_project.fps + var playback_position := Global.current_project.current_frame * inverse_fps + audio_player.play(playback_position) else: audio_player.stop() From 145036f71d14780298753b0049478e4c0a8eba5e Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 03:56:13 +0200 Subject: [PATCH 08/26] Don't play audio is layer is invisible --- src/UI/Timeline/LayerButton.gd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 91ac1e78a..1429f78a9 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -85,6 +85,9 @@ func _on_animation_finished() -> void: func _play_audio() -> void: if not is_instance_valid(audio_player): return + var layer := Global.current_project.layers[layer_index] + if not layer.visible: + return var audio_length := audio_player.stream.get_length() var final_frame := audio_length * Global.current_project.fps if Global.current_project.current_frame < final_frame: From 0cef80ab6fb0b2be9ca3f8183d394a595bc5d90b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Dec 2024 04:13:37 +0200 Subject: [PATCH 09/26] Set the audio layer names to be the imported audio file names --- src/Autoload/OpenSave.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 8eea808a6..614718e24 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -920,7 +920,7 @@ func open_audio_file(path: String) -> void: if layer is AudioLayer and not is_instance_valid(layer.audio): layer.audio = audio_stream return - var new_layer := AudioLayer.new(project) + var new_layer := AudioLayer.new(project, path.get_basename().get_file()) new_layer.audio = audio_stream Global.animation_timeline.add_layer(new_layer, project) From 963eef41245a6e5d15b30f426fcca0e80b289447 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 15:44:24 +0200 Subject: [PATCH 10/26] Import audio from videos --- src/Autoload/OpenSave.gd | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 614718e24..cc58a93bc 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -187,8 +187,8 @@ func handle_loading_video(file: String) -> bool: project_size.x = temp_image.get_width() if temp_image.get_height() > project_size.y: project_size.y = temp_image.get_height() - DirAccess.remove_absolute(Export.TEMP_PATH) if images_to_import.size() == 0 or project_size == Vector2i.ZERO: + DirAccess.remove_absolute(Export.TEMP_PATH) return false # We didn't find any images, return # If we found images, create a new project out of them var new_project := Project.new([], file.get_basename().get_file(), project_size) @@ -198,6 +198,14 @@ func handle_loading_video(file: String) -> bool: Global.projects.append(new_project) Global.tabs.current_tab = Global.tabs.get_tab_count() - 1 Global.canvas.camera_zoom() + var output_audio_file := temp_path_real.path_join("audio.ogg") + # ffmpeg -y -i input_file -vn audio.ogg + var ffmpeg_execute_audio: PackedStringArray = ["-y", "-i", file, "-vn", output_audio_file] + var success_audio := OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true) + if FileAccess.file_exists(output_audio_file): + open_audio_file(output_audio_file) + temp_dir.remove("audio.ogg") + DirAccess.remove_absolute(Export.TEMP_PATH) return true From 47fb74b2687d76724efe91856783c07b4698f32f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:39:18 +0200 Subject: [PATCH 11/26] Export videos with audio Only works with mp3 for now --- src/Autoload/Export.gd | 59 +++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 5a55d831a..15e7293a6 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -427,7 +427,7 @@ func export_processed_images( if is_single_file_format(project): if is_using_ffmpeg(project.file_format): - var video_exported := export_video(export_paths) + var video_exported := export_video(export_paths, project) if not video_exported: Global.popup_error( tr("Video failed to export. Ensure that FFMPEG is installed correctly.") @@ -505,8 +505,9 @@ func export_processed_images( ## Uses FFMPEG to export a video -func export_video(export_paths: PackedStringArray) -> bool: +func export_video(export_paths: PackedStringArray, project: Project) -> bool: DirAccess.make_dir_absolute(TEMP_PATH) + var video_duration := 0 var temp_path_real := ProjectSettings.globalize_path(TEMP_PATH) var input_file_path := temp_path_real.path_join("input.txt") var input_file := FileAccess.open(input_file_path, FileAccess.WRITE) @@ -516,25 +517,65 @@ func export_video(export_paths: PackedStringArray) -> bool: processed_images[i].image.save_png(temp_file_path) input_file.store_line("file '" + temp_file_name + "'") input_file.store_line("duration %s" % processed_images[i].duration) + video_duration += processed_images[i].duration input_file.close() + + # ffmpeg -y -f concat -i input.txt output_path var ffmpeg_execute: PackedStringArray = [ "-y", "-f", "concat", "-i", input_file_path, export_paths[0] ] - var output := [] - var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, output, true) - print(output) - var temp_dir := DirAccess.open(TEMP_PATH) - for file in temp_dir.get_files(): - temp_dir.remove(file) - DirAccess.remove_absolute(TEMP_PATH) + var success := OS.execute(Global.ffmpeg_path, ffmpeg_execute, [], true) if success < 0 or success > 1: var fail_text := """Video failed to export. Make sure you have FFMPEG installed and have set the correct path in the preferences.""" Global.popup_error(tr(fail_text)) + _clear_temp_folder() return false + # ffmpeg -i input1 -i input2 ... -i inputn -filter_complex amix=inputs=n output_path + var ffmpeg_combine_audio: PackedStringArray = ["-y"] + var audio_layer_count := 0 + var max_audio_duration := 0 + for layer in project.layers: + if layer is AudioLayer and layer.audio is AudioStreamMP3: + var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3" + var temp_file_path := temp_path_real.path_join(temp_file_name) + var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE) + temp_audio_file.store_buffer(layer.audio.data) + audio_layer_count += 1 + ffmpeg_combine_audio.append("-i") + ffmpeg_combine_audio.append(temp_file_path) + if layer.audio.get_length() >= max_audio_duration: + max_audio_duration = layer.audio.get_length() + if audio_layer_count > 0: + var amix_inputs_string := "amix=inputs=%s" % audio_layer_count + var audio_file_path := temp_path_real.path_join("audio.mp3") + ffmpeg_combine_audio.append_array( + PackedStringArray(["-filter_complex", amix_inputs_string, audio_file_path]) + ) + OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true) + var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension()) + DirAccess.copy_absolute(export_paths[0], copied_video) + # ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file + var ffmpeg_final_video: PackedStringArray = [ + "-y", "-i", copied_video, "-i", audio_file_path + ] + if max_audio_duration > video_duration: + ffmpeg_final_video.append("-shortest") + ffmpeg_final_video.append_array( + ["-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0", export_paths[0]] + ) + OS.execute(Global.ffmpeg_path, ffmpeg_final_video, [], true) + _clear_temp_folder() return true +func _clear_temp_folder() -> void: + var temp_dir := DirAccess.open(TEMP_PATH) + for file in temp_dir.get_files(): + temp_dir.remove(file) + DirAccess.remove_absolute(TEMP_PATH) + + func export_animated(args: Dictionary) -> void: var project: Project = args["project"] var exporter: AImgIOBaseExporter = args["exporter"] From 98d2e124155c244fbace7e86e192149764e2a3c7 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:40:40 +0200 Subject: [PATCH 12/26] Remove support for ogg audio files as they cannot be saved At least until I find a way to save them. Wav files will be supported with Godot 4.4 --- src/Autoload/OpenSave.gd | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index cc58a93bc..d8fac67fe 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -45,7 +45,7 @@ func handle_loading_file(file: String) -> void: return var file_name: String = file.get_file().get_basename() Global.control.find_child("ShaderEffect").change_shader(shader, file_name) - elif file_ext == "ogg" or file_ext == "mp3": # Audio file + elif file_ext == "mp3": # Audio file open_audio_file(file) else: # Image files @@ -198,13 +198,13 @@ func handle_loading_video(file: String) -> bool: Global.projects.append(new_project) Global.tabs.current_tab = Global.tabs.get_tab_count() - 1 Global.canvas.camera_zoom() - var output_audio_file := temp_path_real.path_join("audio.ogg") - # ffmpeg -y -i input_file -vn audio.ogg + var output_audio_file := temp_path_real.path_join("audio.mp3") + # ffmpeg -y -i input_file -vn audio.mp3 var ffmpeg_execute_audio: PackedStringArray = ["-y", "-i", file, "-vn", output_audio_file] var success_audio := OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true) if FileAccess.file_exists(output_audio_file): open_audio_file(output_audio_file) - temp_dir.remove("audio.ogg") + temp_dir.remove("audio.mp3") DirAccess.remove_absolute(Export.TEMP_PATH) return true @@ -915,12 +915,9 @@ func set_new_imported_tab(project: Project, path: String) -> void: func open_audio_file(path: String) -> void: var audio_stream: AudioStream var file_ext := path.get_extension().to_lower() - if file_ext == "ogg": - audio_stream = AudioStreamOggVorbis.load_from_file(path) - elif file_ext == "mp3": - var file := FileAccess.open(path, FileAccess.READ) - audio_stream = AudioStreamMP3.new() - audio_stream.data = file.get_buffer(file.get_length()) + var file := FileAccess.open(path, FileAccess.READ) + audio_stream = AudioStreamMP3.new() + audio_stream.data = file.get_buffer(file.get_length()) if not is_instance_valid(audio_stream): return var project := Global.current_project From 3a7d3410d3fce96bae65c1316999bb5c163b0687 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:55:58 +0200 Subject: [PATCH 13/26] Fix adding/removing in-between frames breaking the visual indication of audio cels --- src/UI/Timeline/CelButton.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index 8cf48a731..b23f2e2f6 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -33,6 +33,7 @@ func _ready() -> void: transparent_checker.visible = false elif cel is AudioCel: _is_playing_audio() + Global.cel_switched.connect(_is_playing_audio) Global.current_project.fps_changed.connect(_is_playing_audio) From 1088a8bbe77382275aa569bc9b48441e4916de87 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:56:07 +0200 Subject: [PATCH 14/26] Minor code improvements --- src/Autoload/Export.gd | 3 +++ src/UI/Timeline/LayerButton.gd | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 15e7293a6..6619a80d2 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -547,6 +547,7 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: if layer.audio.get_length() >= max_audio_duration: max_audio_duration = layer.audio.get_length() if audio_layer_count > 0: + # If we have audio layers, merge them all into one file. var amix_inputs_string := "amix=inputs=%s" % audio_layer_count var audio_file_path := temp_path_real.path_join("audio.mp3") ffmpeg_combine_audio.append_array( @@ -554,6 +555,8 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: ) OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true) var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension()) + + # Then mix the audio file with the video. DirAccess.copy_absolute(export_paths[0], copied_video) # ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file var ffmpeg_final_video: PackedStringArray = [ diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 1429f78a9..c77e4e0b0 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -91,8 +91,8 @@ func _play_audio() -> void: var audio_length := audio_player.stream.get_length() var final_frame := audio_length * Global.current_project.fps if Global.current_project.current_frame < final_frame: - var inverse_fps := 1.0 / Global.current_project.fps - var playback_position := Global.current_project.current_frame * inverse_fps + var seconds_per_frame := 1.0 / Global.current_project.fps + var playback_position := Global.current_project.current_frame * seconds_per_frame audio_player.play(playback_position) else: audio_player.stop() From ecd479b4e2510ec939fdf5dc4ac828ed7474da47 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:54:04 +0200 Subject: [PATCH 15/26] Export audio in videos with custom delay --- src/Autoload/Export.gd | 50 ++++++++++++++++++++------------ src/Classes/Layers/AudioLayer.gd | 1 + 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 6619a80d2..3978edf0a 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -531,43 +531,55 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: Global.popup_error(tr(fail_text)) _clear_temp_folder() return false - # ffmpeg -i input1 -i input2 ... -i inputn -filter_complex amix=inputs=n output_path + # Find audio layers var ffmpeg_combine_audio: PackedStringArray = ["-y"] var audio_layer_count := 0 var max_audio_duration := 0 + var adelay_string := "" for layer in project.layers: if layer is AudioLayer and layer.audio is AudioStreamMP3: var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3" var temp_file_path := temp_path_real.path_join(temp_file_name) var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE) temp_audio_file.store_buffer(layer.audio.data) - audio_layer_count += 1 ffmpeg_combine_audio.append("-i") ffmpeg_combine_audio.append(temp_file_path) + var delay: int = layer.playback_position * 1000 + # [n]adelay=delay_in_ms:all=1[na] + adelay_string += ( + "[%s]adelay=%s:all=1[%sa];" % [audio_layer_count, delay, audio_layer_count] + ) + audio_layer_count += 1 if layer.audio.get_length() >= max_audio_duration: max_audio_duration = layer.audio.get_length() if audio_layer_count > 0: # If we have audio layers, merge them all into one file. - var amix_inputs_string := "amix=inputs=%s" % audio_layer_count + for i in audio_layer_count: + adelay_string += "[%sa]" % i + var amix_inputs_string := "amix=inputs=%s[a]" % audio_layer_count + var final_filter_string := adelay_string + amix_inputs_string var audio_file_path := temp_path_real.path_join("audio.mp3") ffmpeg_combine_audio.append_array( - PackedStringArray(["-filter_complex", amix_inputs_string, audio_file_path]) + PackedStringArray( + ["-filter_complex", final_filter_string, "-map", '"[a]"', audio_file_path] + ) ) - OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true) - var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension()) - - # Then mix the audio file with the video. - DirAccess.copy_absolute(export_paths[0], copied_video) - # ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file - var ffmpeg_final_video: PackedStringArray = [ - "-y", "-i", copied_video, "-i", audio_file_path - ] - if max_audio_duration > video_duration: - ffmpeg_final_video.append("-shortest") - ffmpeg_final_video.append_array( - ["-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0", export_paths[0]] - ) - OS.execute(Global.ffmpeg_path, ffmpeg_final_video, [], true) + # ffmpeg -i input1 -i input2 ... -i inputn -filter_complex amix=inputs=n output_path + var combined_audio_success := OS.execute(Global.ffmpeg_path, ffmpeg_combine_audio, [], true) + if combined_audio_success == 0 or combined_audio_success == 1: + var copied_video := temp_path_real.path_join("video." + export_paths[0].get_extension()) + # Then mix the audio file with the video. + DirAccess.copy_absolute(export_paths[0], copied_video) + # ffmpeg -y -i video_file -i input_audio -c:v copy -map 0:v:0 -map 1:a:0 video_file + var ffmpeg_final_video: PackedStringArray = [ + "-y", "-i", copied_video, "-i", audio_file_path + ] + if max_audio_duration > video_duration: + ffmpeg_final_video.append("-shortest") + ffmpeg_final_video.append_array( + ["-c:v", "copy", "-map", "0:v:0", "-map", "1:a:0", export_paths[0]] + ) + OS.execute(Global.ffmpeg_path, ffmpeg_final_video, [], true) _clear_temp_folder() return true diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index 9d677e014..7d872ff60 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -7,6 +7,7 @@ var audio: AudioStream: set(value): audio = value audio_changed.emit() +var playback_position := 0.0 ## Measured in seconds. func _init(_project: Project, _name := "") -> void: From 404d938565be950a0dff9602e553f7997e5aae8b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:52:57 +0200 Subject: [PATCH 16/26] Support frame delay --- assets/graphics/misc/musical_note.png | Bin 0 -> 192 bytes assets/graphics/misc/musical_note.png.import | 34 +++++++++++++++++++ src/Autoload/Export.gd | 4 +-- src/Classes/Frame.gd | 14 ++++++++ src/UI/Timeline/CelButton.gd | 10 +++--- src/UI/Timeline/FrameButton.gd | 5 +-- src/UI/Timeline/LayerButton.gd | 9 +++-- 7 files changed, 62 insertions(+), 14 deletions(-) create mode 100644 assets/graphics/misc/musical_note.png create mode 100644 assets/graphics/misc/musical_note.png.import diff --git a/assets/graphics/misc/musical_note.png b/assets/graphics/misc/musical_note.png new file mode 100644 index 0000000000000000000000000000000000000000..9e9d64499f538795faaa22b5be5dcee463bfa14d GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1IgDo+>35R2Zo zQx5VSP~dQWx#|D^)UsQPmL!y_?Ox+G+kfA5vnCT>^`5)bXTcJ2N(H pn|e+$ZA-TGF7Lc2I;H void: for cel in frame.cels: var image := Image.new() image.copy_from(cel.get_image()) - var duration := frame.duration * (1.0 / project.fps) + var duration := frame.get_duration_in_seconds(project.fps) processed_images.append( ProcessedImage.new(image, project.frames.find(frame), duration) ) @@ -298,7 +298,7 @@ func process_animation(project := Global.current_project) -> void: image.copy_from(crop) if trim_images: image = image.get_region(image.get_used_rect()) - var duration := frame.duration * (1.0 / project.fps) + var duration := frame.get_duration_in_seconds(project.fps) processed_images.append(ProcessedImage.new(image, project.frames.find(frame), duration)) diff --git a/src/Classes/Frame.gd b/src/Classes/Frame.gd index 57f9611f5..52994988c 100644 --- a/src/Classes/Frame.gd +++ b/src/Classes/Frame.gd @@ -11,3 +11,17 @@ var user_data := "" ## User defined data, set in the frame properties. func _init(_cels: Array[BaseCel] = [], _duration := 1.0) -> void: cels = _cels duration = _duration + + +func get_duration_in_seconds(fps: float) -> float: + return duration * (1.0 / fps) + + +func position_in_seconds(project: Project, start_from := 0) -> float: + var pos := 0.0 + for i in range(start_from, project.frames.size()): + var frame := project.frames[i] + if frame == self: + break + pos += frame.get_duration_in_seconds(project.fps) + return pos diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index b23f2e2f6..50e0bcd48 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -404,10 +404,10 @@ func _sort_cel_indices_by_frame(a: Array, b: Array) -> bool: func _is_playing_audio() -> void: - var layer := Global.current_project.layers[layer] as AudioLayer - var audio_length := layer.audio.get_length() - var final_frame := audio_length * Global.current_project.fps - if frame < final_frame: - cel_texture.texture = preload("res://assets/graphics/icons/icon.png") + var frame_class := Global.current_project.frames[frame] + var layer_class := Global.current_project.layers[layer] as AudioLayer + var audio_length := layer_class.audio.get_length() + if frame_class.position_in_seconds(Global.current_project) < audio_length: + cel_texture.texture = preload("res://assets/graphics/misc/musical_note.png") else: cel_texture.texture = null diff --git a/src/UI/Timeline/FrameButton.gd b/src/UI/Timeline/FrameButton.gd index 82f63866c..2602a0f40 100644 --- a/src/UI/Timeline/FrameButton.gd +++ b/src/UI/Timeline/FrameButton.gd @@ -19,8 +19,9 @@ func _ready() -> void: func _update_tooltip() -> void: - var duration := Global.current_project.frames[frame].duration - var duration_sec := duration * (1.0 / Global.current_project.fps) + var frame_class := Global.current_project.frames[frame] + var duration := frame_class.duration + var duration_sec := frame_class.get_duration_in_seconds(Global.current_project.fps) var duration_str := str(duration_sec) if "." in duration_str: # If its a decimal value duration_str = "%.2f" % duration_sec # Up to 2 decimal places diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index c77e4e0b0..008cb4483 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -89,11 +89,10 @@ func _play_audio() -> void: if not layer.visible: return var audio_length := audio_player.stream.get_length() - var final_frame := audio_length * Global.current_project.fps - if Global.current_project.current_frame < final_frame: - var seconds_per_frame := 1.0 / Global.current_project.fps - var playback_position := Global.current_project.current_frame * seconds_per_frame - audio_player.play(playback_position) + var frame := Global.current_project.frames[Global.current_project.current_frame] + var frame_pos := frame.position_in_seconds(Global.current_project) + if frame_pos < audio_length: + audio_player.play(frame_pos) else: audio_player.stop() From 16c06bf9aff64c6bf57674738d85fd8afd593574 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:25:02 +0200 Subject: [PATCH 17/26] Change the frame where the audio plays at --- src/Classes/Frame.gd | 16 +++++++++----- src/Classes/Layers/AudioLayer.gd | 6 +++++- src/UI/Timeline/CelButton.gd | 5 ++++- src/UI/Timeline/LayerButton.gd | 12 ++++++----- src/UI/Timeline/LayerProperties.gd | 18 ++++++++++++++++ src/UI/Timeline/LayerProperties.tscn | 32 ++++++++++++++++++++++------ 6 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/Classes/Frame.gd b/src/Classes/Frame.gd index 52994988c..21ef3ff9b 100644 --- a/src/Classes/Frame.gd +++ b/src/Classes/Frame.gd @@ -19,9 +19,15 @@ func get_duration_in_seconds(fps: float) -> float: func position_in_seconds(project: Project, start_from := 0) -> float: var pos := 0.0 - for i in range(start_from, project.frames.size()): - var frame := project.frames[i] - if frame == self: - break - pos += frame.get_duration_in_seconds(project.fps) + var index := project.frames.find(self) + if index > start_from: + for i in range(start_from, index): + var frame := project.frames[i] + pos += frame.get_duration_in_seconds(project.fps) + else: + if start_from >= project.frames.size(): + return -1.0 + for i in range(start_from, index, -1): + var frame := project.frames[i] + pos -= frame.get_duration_in_seconds(project.fps) return pos diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index 7d872ff60..5f25d848a 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -7,7 +7,11 @@ var audio: AudioStream: set(value): audio = value audio_changed.emit() -var playback_position := 0.0 ## Measured in seconds. +var playback_position := 0.0: ## Measured in seconds. + get(): + var frame := project.frames[playback_frame] + return frame.position_in_seconds(project) +var playback_frame := 0 func _init(_project: Project, _name := "") -> void: diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index 50e0bcd48..3da27b0d2 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -407,7 +407,10 @@ func _is_playing_audio() -> void: var frame_class := Global.current_project.frames[frame] var layer_class := Global.current_project.layers[layer] as AudioLayer var audio_length := layer_class.audio.get_length() - if frame_class.position_in_seconds(Global.current_project) < audio_length: + var frame_pos := frame_class.position_in_seconds( + Global.current_project, layer_class.playback_frame + ) + if frame_pos >= 0 and frame_pos < audio_length: cel_texture.texture = preload("res://assets/graphics/misc/musical_note.png") else: cel_texture.texture = null diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 008cb4483..910d8beb8 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -67,8 +67,10 @@ func _ready() -> void: func _on_cel_switched() -> void: z_index = 1 if button_pressed else 0 - if not animation_running or Global.current_project.current_frame == 0: - _play_audio() + var layer := Global.current_project.layers[layer_index] + if layer is AudioLayer: + if not animation_running or Global.current_project.current_frame == layer.playback_frame: + _play_audio() func _on_animation_started(_dir: bool) -> void: @@ -85,13 +87,13 @@ func _on_animation_finished() -> void: func _play_audio() -> void: if not is_instance_valid(audio_player): return - var layer := Global.current_project.layers[layer_index] + var layer := Global.current_project.layers[layer_index] as AudioLayer if not layer.visible: return var audio_length := audio_player.stream.get_length() var frame := Global.current_project.frames[Global.current_project.current_frame] - var frame_pos := frame.position_in_seconds(Global.current_project) - if frame_pos < audio_length: + var frame_pos := frame.position_in_seconds(Global.current_project, layer.playback_frame) + if frame_pos >= 0 and frame_pos < audio_length: audio_player.play(frame_pos) else: audio_player.stop() diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd index fd4dfd229..a62ca34cb 100644 --- a/src/UI/Timeline/LayerProperties.gd +++ b/src/UI/Timeline/LayerProperties.gd @@ -4,9 +4,11 @@ signal layer_property_changed var layer_indices: PackedInt32Array +@onready var grid_container: GridContainer = $GridContainer @onready var name_line_edit := $GridContainer/NameLineEdit as LineEdit @onready var opacity_slider := $GridContainer/OpacitySlider as ValueSlider @onready var blend_modes_button := $GridContainer/BlendModeOptionButton as OptionButton +@onready var play_at_frame_slider := $GridContainer/PlayAtFrameSlider as ValueSlider @onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit @onready var tileset_option_button := $GridContainer/TilesetOptionButton as OptionButton @@ -23,8 +25,15 @@ func _on_visibility_changed() -> void: opacity_slider.value = first_layer.opacity * 100.0 var blend_mode_index := blend_modes_button.get_item_index(first_layer.blend_mode) blend_modes_button.selected = blend_mode_index + if first_layer is AudioLayer: + play_at_frame_slider.value = first_layer.playback_frame + 1 + play_at_frame_slider.max_value = project.frames.size() user_data_text_edit.text = first_layer.user_data + for child in grid_container.get_children(): + if not child.is_in_group(&"AllLayers"): + child.visible = first_layer is not AudioLayer get_tree().set_group(&"TilemapLayers", "visible", first_layer is LayerTileMap) + get_tree().set_group(&"AudioLayers", "visible", first_layer is AudioLayer) tileset_option_button.clear() if first_layer is LayerTileMap: for i in project.tilesets.size(): @@ -149,3 +158,12 @@ func _on_tileset_option_button_item_selected(index: int) -> void: project.undo_redo.add_undo_method(Global.canvas.draw_layers) project.undo_redo.add_undo_method(func(): Global.cel_switched.emit()) project.undo_redo.commit_action() + + +func _on_play_at_frame_slider_value_changed(value: float) -> void: + if layer_indices.size() == 0: + return + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + if layer is AudioLayer: + layer.playback_frame = value - 1 diff --git a/src/UI/Timeline/LayerProperties.tscn b/src/UI/Timeline/LayerProperties.tscn index 74ac0b682..27dfe24fe 100644 --- a/src/UI/Timeline/LayerProperties.tscn +++ b/src/UI/Timeline/LayerProperties.tscn @@ -5,22 +5,22 @@ [node name="LayerProperties" type="AcceptDialog"] title = "Layer properties" -size = Vector2i(300, 208) +size = Vector2i(300, 235) script = ExtResource("1_54q1t") [node name="GridContainer" type="GridContainer" parent="."] offset_left = 8.0 offset_top = 8.0 offset_right = 292.0 -offset_bottom = 159.0 +offset_bottom = 186.0 columns = 2 -[node name="NameLabel" type="Label" parent="GridContainer"] +[node name="NameLabel" type="Label" parent="GridContainer" groups=["AllLayers"]] layout_mode = 2 size_flags_horizontal = 3 text = "Name:" -[node name="NameLineEdit" type="LineEdit" parent="GridContainer"] +[node name="NameLineEdit" type="LineEdit" parent="GridContainer" groups=["AllLayers"]] layout_mode = 2 size_flags_horizontal = 3 @@ -52,13 +52,32 @@ layout_mode = 2 size_flags_horizontal = 3 mouse_default_cursor_shape = 2 -[node name="UserDataLabel" type="Label" parent="GridContainer"] +[node name="PlayAtFrameLabel" type="Label" parent="GridContainer" groups=["AudioLayers"]] +layout_mode = 2 +text = "Play at frame:" + +[node name="PlayAtFrameSlider" type="TextureProgressBar" parent="GridContainer" groups=["AudioLayers"]] +layout_mode = 2 +focus_mode = 2 +mouse_default_cursor_shape = 2 +theme_type_variation = &"ValueSlider" +min_value = 1.0 +value = 1.0 +allow_greater = true +nine_patch_stretch = true +stretch_margin_left = 3 +stretch_margin_top = 3 +stretch_margin_right = 3 +stretch_margin_bottom = 3 +script = ExtResource("2_bwpwc") + +[node name="UserDataLabel" type="Label" parent="GridContainer" groups=["AllLayers"]] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 0 text = "User data:" -[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"] +[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer" groups=["AllLayers"]] layout_mode = 2 size_flags_horizontal = 3 scroll_fit_content_height = true @@ -77,5 +96,6 @@ mouse_default_cursor_shape = 2 [connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"] [connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"] [connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"] +[connection signal="value_changed" from="GridContainer/PlayAtFrameSlider" to="." method="_on_play_at_frame_slider_value_changed"] [connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"] [connection signal="item_selected" from="GridContainer/TilesetOptionButton" to="." method="_on_tileset_option_button_item_selected"] From 04f5a162c045a5ee52273ecf81b1e2a5880dc224 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:34:21 +0200 Subject: [PATCH 18/26] Fix crashes when the audio layer has no track --- src/Autoload/Export.gd | 6 +++--- src/Classes/Layers/AudioLayer.gd | 7 +++++++ src/UI/Timeline/CelButton.gd | 2 +- src/UI/Timeline/LayerButton.gd | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index 8f0e8a817..b60545e0b 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -537,7 +537,7 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: var max_audio_duration := 0 var adelay_string := "" for layer in project.layers: - if layer is AudioLayer and layer.audio is AudioStreamMP3: + if layer is AudioLayer and is_instance_valid(layer.audio) and layer.audio is AudioStreamMP3: var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3" var temp_file_path := temp_path_real.path_join(temp_file_name) var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE) @@ -550,8 +550,8 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: "[%s]adelay=%s:all=1[%sa];" % [audio_layer_count, delay, audio_layer_count] ) audio_layer_count += 1 - if layer.audio.get_length() >= max_audio_duration: - max_audio_duration = layer.audio.get_length() + if layer.get_audio_length() >= max_audio_duration: + max_audio_duration = layer.get_audio_length() if audio_layer_count > 0: # If we have audio layers, merge them all into one file. for i in audio_layer_count: diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index 5f25d848a..b1da55b0a 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -19,6 +19,13 @@ func _init(_project: Project, _name := "") -> void: name = _name +func get_audio_length() -> float: + if is_instance_valid(audio): + return audio.get_length() + else: + return -1.0 + + # Overridden Methods: func serialize() -> Dictionary: var data := {"name": name, "type": get_layer_type()} diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index 3da27b0d2..03edb4053 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -406,7 +406,7 @@ func _sort_cel_indices_by_frame(a: Array, b: Array) -> bool: func _is_playing_audio() -> void: var frame_class := Global.current_project.frames[frame] var layer_class := Global.current_project.layers[layer] as AudioLayer - var audio_length := layer_class.audio.get_length() + var audio_length := layer_class.get_audio_length() var frame_pos := frame_class.position_in_seconds( Global.current_project, layer_class.playback_frame ) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 910d8beb8..8ef90e984 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -90,7 +90,7 @@ func _play_audio() -> void: var layer := Global.current_project.layers[layer_index] as AudioLayer if not layer.visible: return - var audio_length := audio_player.stream.get_length() + var audio_length := layer.get_audio_length() var frame := Global.current_project.frames[Global.current_project.current_frame] var frame_pos := frame.position_in_seconds(Global.current_project, layer.playback_frame) if frame_pos >= 0 and frame_pos < audio_length: From a1764f332339767cdeecaed7445bd741b3bba835 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 01:42:22 +0200 Subject: [PATCH 19/26] Remove unneeded cel properties for audio cels --- src/UI/Timeline/CelProperties.gd | 6 +++--- src/UI/Timeline/CelProperties.tscn | 10 ++++++---- src/UI/Timeline/LayerProperties.gd | 4 +--- src/UI/Timeline/LayerProperties.tscn | 16 ++++++++-------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/UI/Timeline/CelProperties.gd b/src/UI/Timeline/CelProperties.gd index 0aefdd9da..f36d53a50 100644 --- a/src/UI/Timeline/CelProperties.gd +++ b/src/UI/Timeline/CelProperties.gd @@ -15,18 +15,18 @@ func _on_visibility_changed() -> void: Global.dialog_open(visible) var first_cel := Global.current_project.frames[cel_indices[0][0]].cels[cel_indices[0][1]] if visible: + var first_layer := Global.current_project.layers[cel_indices[0][1]] if cel_indices.size() == 1: - var layer := Global.current_project.layers[cel_indices[0][1]] frame_num.text = str(cel_indices[0][0] + 1) - layer_num.text = layer.name + layer_num.text = first_layer.name else: - var first_layer := Global.current_project.layers[cel_indices[0][1]] var last_layer := Global.current_project.layers[cel_indices[-1][1]] frame_num.text = "[%s...%s]" % [cel_indices[0][0] + 1, cel_indices[-1][0] + 1] layer_num.text = "[%s...%s]" % [first_layer.name, last_layer.name] opacity_slider.value = first_cel.opacity * 100.0 z_index_slider.value = first_cel.z_index user_data_text_edit.text = first_cel.user_data + get_tree().set_group(&"VisualCels", "visible", first_layer is not AudioLayer) else: cel_indices = [] diff --git a/src/UI/Timeline/CelProperties.tscn b/src/UI/Timeline/CelProperties.tscn index bb13c2042..18c2aa419 100644 --- a/src/UI/Timeline/CelProperties.tscn +++ b/src/UI/Timeline/CelProperties.tscn @@ -33,12 +33,12 @@ layout_mode = 2 text = "1" horizontal_alignment = 1 -[node name="OpacityLabel" type="Label" parent="GridContainer"] +[node name="OpacityLabel" type="Label" parent="GridContainer" groups=["VisualCels"]] layout_mode = 2 size_flags_horizontal = 3 text = "Opacity:" -[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer"] +[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualCels"]] layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 @@ -52,12 +52,12 @@ stretch_margin_right = 3 stretch_margin_bottom = 3 script = ExtResource("1_85pb7") -[node name="ZIndexLabel" type="Label" parent="GridContainer"] +[node name="ZIndexLabel" type="Label" parent="GridContainer" groups=["VisualCels"]] layout_mode = 2 size_flags_horizontal = 3 text = "Z-Index:" -[node name="ZIndexSlider" type="TextureProgressBar" parent="GridContainer"] +[node name="ZIndexSlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualCels"]] layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 @@ -76,11 +76,13 @@ script = ExtResource("1_85pb7") [node name="UserDataLabel" type="Label" parent="GridContainer"] layout_mode = 2 +size_flags_horizontal = 3 size_flags_vertical = 0 text = "User data:" [node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"] layout_mode = 2 +size_flags_horizontal = 3 scroll_fit_content_height = true [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd index a62ca34cb..9057ecc6a 100644 --- a/src/UI/Timeline/LayerProperties.gd +++ b/src/UI/Timeline/LayerProperties.gd @@ -29,9 +29,7 @@ func _on_visibility_changed() -> void: play_at_frame_slider.value = first_layer.playback_frame + 1 play_at_frame_slider.max_value = project.frames.size() user_data_text_edit.text = first_layer.user_data - for child in grid_container.get_children(): - if not child.is_in_group(&"AllLayers"): - child.visible = first_layer is not AudioLayer + get_tree().set_group(&"VisualLayers", "visible", first_layer is not AudioLayer) get_tree().set_group(&"TilemapLayers", "visible", first_layer is LayerTileMap) get_tree().set_group(&"AudioLayers", "visible", first_layer is AudioLayer) tileset_option_button.clear() diff --git a/src/UI/Timeline/LayerProperties.tscn b/src/UI/Timeline/LayerProperties.tscn index 27dfe24fe..f2b92fe32 100644 --- a/src/UI/Timeline/LayerProperties.tscn +++ b/src/UI/Timeline/LayerProperties.tscn @@ -15,21 +15,21 @@ offset_right = 292.0 offset_bottom = 186.0 columns = 2 -[node name="NameLabel" type="Label" parent="GridContainer" groups=["AllLayers"]] +[node name="NameLabel" type="Label" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 text = "Name:" -[node name="NameLineEdit" type="LineEdit" parent="GridContainer" groups=["AllLayers"]] +[node name="NameLineEdit" type="LineEdit" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 -[node name="OpacityLabel" type="Label" parent="GridContainer"] +[node name="OpacityLabel" type="Label" parent="GridContainer" groups=["VisualLayers"]] layout_mode = 2 size_flags_horizontal = 3 text = "Opacity:" -[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer"] +[node name="OpacitySlider" type="TextureProgressBar" parent="GridContainer" groups=["VisualLayers"]] layout_mode = 2 size_flags_horizontal = 3 focus_mode = 2 @@ -42,12 +42,12 @@ stretch_margin_right = 3 stretch_margin_bottom = 3 script = ExtResource("2_bwpwc") -[node name="BlendModeLabel" type="Label" parent="GridContainer"] +[node name="BlendModeLabel" type="Label" parent="GridContainer" groups=["VisualLayers"]] layout_mode = 2 size_flags_horizontal = 3 text = "Blend mode:" -[node name="BlendModeOptionButton" type="OptionButton" parent="GridContainer"] +[node name="BlendModeOptionButton" type="OptionButton" parent="GridContainer" groups=["VisualLayers"]] layout_mode = 2 size_flags_horizontal = 3 mouse_default_cursor_shape = 2 @@ -71,13 +71,13 @@ stretch_margin_right = 3 stretch_margin_bottom = 3 script = ExtResource("2_bwpwc") -[node name="UserDataLabel" type="Label" parent="GridContainer" groups=["AllLayers"]] +[node name="UserDataLabel" type="Label" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 0 text = "User data:" -[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer" groups=["AllLayers"]] +[node name="UserDataTextEdit" type="TextEdit" parent="GridContainer"] layout_mode = 2 size_flags_horizontal = 3 scroll_fit_content_height = true From 7633b0fe2a054e5288bcfdca6bb81d527369bb26 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:32:34 +0200 Subject: [PATCH 20/26] Pxo loading/saving --- src/Autoload/Export.gd | 6 +++--- src/Autoload/OpenSave.gd | 11 +++++++++-- src/Classes/Layers/AudioLayer.gd | 14 +++++++++++++- src/Classes/Project.gd | 27 +++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index b60545e0b..ca839dcd6 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -536,15 +536,15 @@ func export_video(export_paths: PackedStringArray, project: Project) -> bool: var audio_layer_count := 0 var max_audio_duration := 0 var adelay_string := "" - for layer in project.layers: - if layer is AudioLayer and is_instance_valid(layer.audio) and layer.audio is AudioStreamMP3: + for layer in project.get_all_audio_layers(): + if layer.audio is AudioStreamMP3: var temp_file_name := str(audio_layer_count + 1).pad_zeros(number_of_digits) + ".mp3" var temp_file_path := temp_path_real.path_join(temp_file_name) var temp_audio_file := FileAccess.open(temp_file_path, FileAccess.WRITE) temp_audio_file.store_buffer(layer.audio.data) ffmpeg_combine_audio.append("-i") ffmpeg_combine_audio.append(temp_file_path) - var delay: int = layer.playback_position * 1000 + var delay := floori(layer.playback_position * 1000) # [n]adelay=delay_in_ms:all=1[na] adelay_string += ( "[%s]adelay=%s:all=1[%sa];" % [audio_layer_count, delay, audio_layer_count] diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index d8fac67fe..868d25ee1 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -201,7 +201,7 @@ func handle_loading_video(file: String) -> bool: var output_audio_file := temp_path_real.path_join("audio.mp3") # ffmpeg -y -i input_file -vn audio.mp3 var ffmpeg_execute_audio: PackedStringArray = ["-y", "-i", file, "-vn", output_audio_file] - var success_audio := OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true) + OS.execute(Global.ffmpeg_path, ffmpeg_execute_audio, [], true) if FileAccess.file_exists(output_audio_file): open_audio_file(output_audio_file) temp_dir.remove("audio.mp3") @@ -448,6 +448,14 @@ func save_pxo_file( zip_packer.start_file(tileset_path.path_join(str(j))) zip_packer.write_file(tile.image.get_data()) zip_packer.close_file() + var audio_layers := project.get_all_audio_layers() + for i in audio_layers.size(): + var layer := audio_layers[i] + var audio_path := "audio/%s" % i + if layer.audio is AudioStreamMP3: + zip_packer.start_file(audio_path) + zip_packer.write_file(layer.audio.data) + zip_packer.close_file() zip_packer.close() if temp_path != path: @@ -914,7 +922,6 @@ func set_new_imported_tab(project: Project, path: String) -> void: func open_audio_file(path: String) -> void: var audio_stream: AudioStream - var file_ext := path.get_extension().to_lower() var file := FileAccess.open(path, FileAccess.READ) audio_stream = AudioStreamMP3.new() audio_stream.data = file.get_buffer(file.get_length()) diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index b1da55b0a..2bf49607d 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -26,14 +26,26 @@ func get_audio_length() -> float: return -1.0 +func get_audio_type() -> String: + if not is_instance_valid(audio): + return "" + return audio.get_class() + + # Overridden Methods: func serialize() -> Dictionary: - var data := {"name": name, "type": get_layer_type()} + var data := { + "name": name, + "type": get_layer_type(), + "playback_frame": playback_frame, + "audio_type": get_audio_type() + } return data func deserialize(dict: Dictionary) -> void: super.deserialize(dict) + playback_frame = dict.get("playback_frame", playback_frame) func get_layer_type() -> int: diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 438401031..fcec8ee2a 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -360,6 +360,7 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces tileset.deserialize(saved_tileset) tilesets.append(tileset) if dict.has("frames") and dict.has("layers"): + var audio_layers := 0 for saved_layer in dict.layers: match int(saved_layer.get("type", Global.LayerTypes.PIXEL)): Global.LayerTypes.PIXEL: @@ -370,6 +371,18 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces layers.append(Layer3D.new(self)) Global.LayerTypes.TILEMAP: layers.append(LayerTileMap.new(self, null)) + Global.LayerTypes.AUDIO: + var layer := AudioLayer.new(self) + var audio_path := "audio/%s" % audio_layers + if zip_reader.file_exists(audio_path): + var audio_data := zip_reader.read_file(audio_path) + var stream: AudioStream + if saved_layer.get("audio_type", "") == "AudioStreamMP3": + stream = AudioStreamMP3.new() + stream.data = audio_data + layer.audio = stream + layers.append(layer) + audio_layers += 1 var frame_i := 0 for frame in dict.frames: @@ -394,6 +407,8 @@ func deserialize(dict: Dictionary, zip_reader: ZIPReader = null, file: FileAcces var tileset := tilesets[tileset_index] var new_cel := CelTileMap.new(tileset, image) cels.append(new_cel) + Global.LayerTypes.AUDIO: + cels.append(AudioCel.new()) cel["pxo_version"] = pxo_version cels[cel_i].deserialize(cel) _deserialize_metadata(cels[cel_i], cel) @@ -662,6 +677,18 @@ func get_all_pixel_cels() -> Array[PixelCel]: return cels +func get_all_audio_layers(only_valid_streams := true) -> Array[AudioLayer]: + var audio_layers: Array[AudioLayer] + for layer in layers: + if layer is AudioLayer: + if only_valid_streams: + if is_instance_valid(layer.audio): + audio_layers.append(layer) + else: + audio_layers.append(layer) + return audio_layers + + ## Reads data from [param cels] and appends them to [param data], ## to be used for the undo/redo system. ## It adds data such as the images of [PixelCel]s, From 0b0ae02ce2b8b35ff507b4fd15140e6ca32c37f5 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:58:40 +0200 Subject: [PATCH 21/26] Load audio files from the audio layer properties --- src/UI/Timeline/LayerProperties.gd | 21 +++++++++++++++++++++ src/UI/Timeline/LayerProperties.tscn | 25 +++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/UI/Timeline/LayerProperties.gd b/src/UI/Timeline/LayerProperties.gd index 9057ecc6a..028213732 100644 --- a/src/UI/Timeline/LayerProperties.gd +++ b/src/UI/Timeline/LayerProperties.gd @@ -11,6 +11,11 @@ var layer_indices: PackedInt32Array @onready var play_at_frame_slider := $GridContainer/PlayAtFrameSlider as ValueSlider @onready var user_data_text_edit := $GridContainer/UserDataTextEdit as TextEdit @onready var tileset_option_button := $GridContainer/TilesetOptionButton as OptionButton +@onready var audio_file_dialog := $AudioFileDialog as FileDialog + + +func _ready() -> void: + audio_file_dialog.use_native_dialog = Global.use_native_file_dialogs func _on_visibility_changed() -> void: @@ -158,6 +163,10 @@ func _on_tileset_option_button_item_selected(index: int) -> void: project.undo_redo.commit_action() +func _on_audio_file_button_pressed() -> void: + audio_file_dialog.popup_centered() + + func _on_play_at_frame_slider_value_changed(value: float) -> void: if layer_indices.size() == 0: return @@ -165,3 +174,15 @@ func _on_play_at_frame_slider_value_changed(value: float) -> void: var layer := Global.current_project.layers[layer_index] if layer is AudioLayer: layer.playback_frame = value - 1 + + +func _on_audio_file_dialog_file_selected(path: String) -> void: + var audio_stream: AudioStream + if path.get_extension() == "mp3": + var file := FileAccess.open(path, FileAccess.READ) + audio_stream = AudioStreamMP3.new() + audio_stream.data = file.get_buffer(file.get_length()) + for layer_index in layer_indices: + var layer := Global.current_project.layers[layer_index] + if layer is AudioLayer: + layer.audio = audio_stream diff --git a/src/UI/Timeline/LayerProperties.tscn b/src/UI/Timeline/LayerProperties.tscn index f2b92fe32..2f3cb52fc 100644 --- a/src/UI/Timeline/LayerProperties.tscn +++ b/src/UI/Timeline/LayerProperties.tscn @@ -5,14 +5,15 @@ [node name="LayerProperties" type="AcceptDialog"] title = "Layer properties" -size = Vector2i(300, 235) +position = Vector2i(0, 36) +size = Vector2i(300, 270) script = ExtResource("1_54q1t") [node name="GridContainer" type="GridContainer" parent="."] offset_left = 8.0 offset_top = 8.0 offset_right = 292.0 -offset_bottom = 186.0 +offset_bottom = 221.0 columns = 2 [node name="NameLabel" type="Label" parent="GridContainer"] @@ -52,6 +53,15 @@ layout_mode = 2 size_flags_horizontal = 3 mouse_default_cursor_shape = 2 +[node name="AudioFileLabel" type="Label" parent="GridContainer" groups=["AudioLayers"]] +layout_mode = 2 +text = "Audio file:" + +[node name="AudioFileButton" type="Button" parent="GridContainer" groups=["AudioLayers"]] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Load file" + [node name="PlayAtFrameLabel" type="Label" parent="GridContainer" groups=["AudioLayers"]] layout_mode = 2 text = "Play at frame:" @@ -92,10 +102,21 @@ text = "Tileset:" layout_mode = 2 mouse_default_cursor_shape = 2 +[node name="AudioFileDialog" type="FileDialog" parent="."] +title = "Open a File" +size = Vector2i(870, 400) +always_on_top = true +ok_button_text = "Open" +file_mode = 0 +access = 2 +filters = PackedStringArray("*.mp3 ; MP3 Audio") + [connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"] [connection signal="text_changed" from="GridContainer/NameLineEdit" to="." method="_on_name_line_edit_text_changed"] [connection signal="value_changed" from="GridContainer/OpacitySlider" to="." method="_on_opacity_slider_value_changed"] [connection signal="item_selected" from="GridContainer/BlendModeOptionButton" to="." method="_on_blend_mode_option_button_item_selected"] +[connection signal="pressed" from="GridContainer/AudioFileButton" to="." method="_on_audio_file_button_pressed"] [connection signal="value_changed" from="GridContainer/PlayAtFrameSlider" to="." method="_on_play_at_frame_slider_value_changed"] [connection signal="text_changed" from="GridContainer/UserDataTextEdit" to="." method="_on_user_data_text_edit_text_changed"] [connection signal="item_selected" from="GridContainer/TilesetOptionButton" to="." method="_on_tileset_option_button_item_selected"] +[connection signal="file_selected" from="AudioFileDialog" to="." method="_on_audio_file_dialog_file_selected"] From c56b242c464c8f9a67a757663a65f3c78bb9d8e2 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 03:38:22 +0200 Subject: [PATCH 22/26] Change the audio driver to Dummy from the Preferences for performance reasons --- src/Autoload/Global.gd | 9 +++++++++ src/Preferences/PreferencesDialog.gd | 7 +++++++ src/Preferences/PreferencesDialog.tscn | 16 ++++++++++++++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 00e17c014..edbab1ab7 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -490,6 +490,11 @@ var window_transparency := false: return window_transparency = value _save_to_override_file() +var dummy_audio_driver := false: + set(value): + if value != dummy_audio_driver: + dummy_audio_driver = value + _save_to_override_file() ## Found in Preferences. The time (in minutes) after which backup is created (if enabled). var autosave_interval := 1.0: @@ -726,6 +731,7 @@ func _init() -> void: window_transparency = ProjectSettings.get_setting( "display/window/per_pixel_transparency/allowed" ) + dummy_audio_driver = ProjectSettings.get_setting("audio/driver/driver") == "Dummy" func _ready() -> void: @@ -1187,3 +1193,6 @@ func _save_to_override_file() -> void: file.store_line("[display]\n") file.store_line("window/subwindows/embed_subwindows=%s" % single_window_mode) file.store_line("window/per_pixel_transparency/allowed=%s" % window_transparency) + file.store_line("[audio]\n") + if dummy_audio_driver: + file.store_line('driver/driver="Dummy"') diff --git a/src/Preferences/PreferencesDialog.gd b/src/Preferences/PreferencesDialog.gd index 86085fb54..e3fa87af5 100644 --- a/src/Preferences/PreferencesDialog.gd +++ b/src/Preferences/PreferencesDialog.gd @@ -181,6 +181,13 @@ var preferences: Array[Preference] = [ false, true ), + Preference.new( + "dummy_audio_driver", + "Performance/PerformanceContainer/DummyAudioDriver", + "button_pressed", + false, + true + ), Preference.new("tablet_driver", "Drivers/DriversContainer/TabletDriver", "selected", 0) ] diff --git a/src/Preferences/PreferencesDialog.tscn b/src/Preferences/PreferencesDialog.tscn index 7b79c020e..bdc767232 100644 --- a/src/Preferences/PreferencesDialog.tscn +++ b/src/Preferences/PreferencesDialog.tscn @@ -1142,13 +1142,25 @@ mouse_default_cursor_shape = 2 button_pressed = true text = "On" -[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"] +[node name="WindowTransparencyLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] layout_mode = 2 tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." mouse_filter = 0 text = "Enable window transparency" -[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer"] +[node name="WindowTransparency" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] +layout_mode = 2 +tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." +mouse_default_cursor_shape = 2 +text = "On" + +[node name="DummyAudioDriverLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] +layout_mode = 2 +tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." +mouse_filter = 0 +text = "Use dummy audio driver" + +[node name="DummyAudioDriver" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] layout_mode = 2 tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." mouse_default_cursor_shape = 2 From b7d4a9bd776c09e3105810b19aa210d460fbf280 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:26:46 +0200 Subject: [PATCH 23/26] Clone audio layers, disable layer merge and FX buttons when an audio layer is selected --- src/UI/Timeline/AnimationTimeline.gd | 6 ++++++ src/UI/Timeline/AnimationTimeline.tscn | 1 + 2 files changed, 7 insertions(+) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index e9232d3ec..e1c1cd0e4 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -41,6 +41,7 @@ var global_layer_expand := true @onready var move_up_layer := %MoveUpLayer as Button @onready var move_down_layer := %MoveDownLayer as Button @onready var merge_down_layer := %MergeDownLayer as Button +@onready var layer_fx := %LayerFX as Button @onready var blend_modes_button := %BlendModes as OptionButton @onready var opacity_slider := %OpacitySlider as ValueSlider @onready var frame_scroll_container := %FrameScrollContainer as Control @@ -906,6 +907,8 @@ func _on_CloneLayer_pressed() -> void: cl_layer = LayerTileMap.new(project, src_layer.tileset) else: cl_layer = src_layer.get_script().new(project) + if src_layer is AudioLayer: + cl_layer.audio = src_layer.audio cl_layer.project = project cl_layer.index = src_layer.index var src_layer_data: Dictionary = src_layer.serialize() @@ -1193,10 +1196,13 @@ func _toggle_layer_buttons() -> void: ( project.current_layer == child_count or layer is GroupLayer + or layer is AudioLayer or project.layers[project.current_layer - 1] is GroupLayer or project.layers[project.current_layer - 1] is Layer3D + or project.layers[project.current_layer - 1] is AudioLayer ) ) + Global.disable_button(layer_fx, layer is AudioLayer) func project_changed() -> void: diff --git a/src/UI/Timeline/AnimationTimeline.tscn b/src/UI/Timeline/AnimationTimeline.tscn index a13e6896b..2d5742db8 100644 --- a/src/UI/Timeline/AnimationTimeline.tscn +++ b/src/UI/Timeline/AnimationTimeline.tscn @@ -382,6 +382,7 @@ texture = ExtResource("5") stretch_mode = 3 [node name="LayerFX" type="Button" parent="TimelineContainer/TimelineButtons/LayerTools/MarginContainer/LayerSettingsContainer/LayerButtons" groups=["UIButtons"]] +unique_name_in_owner = true custom_minimum_size = Vector2(24, 24) layout_mode = 2 size_flags_horizontal = 3 From 7f63bb3a16a9671c16f7a75805453d62e8bf6b9b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:15:10 +0200 Subject: [PATCH 24/26] Easily change the playback frame of an audio layer from the right click menu of cel buttons --- src/Classes/Layers/AudioLayer.gd | 6 +++++- src/UI/Timeline/CelButton.gd | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index 2bf49607d..0a6100bf4 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -2,6 +2,7 @@ class_name AudioLayer extends BaseLayer signal audio_changed +signal playback_frame_changed var audio: AudioStream: set(value): @@ -11,7 +12,10 @@ var playback_position := 0.0: ## Measured in seconds. get(): var frame := project.frames[playback_frame] return frame.position_in_seconds(project) -var playback_frame := 0 +var playback_frame := 0: + set(value): + playback_frame = value + playback_frame_changed.emit() func _init(_project: Project, _name := "") -> void: diff --git a/src/UI/Timeline/CelButton.gd b/src/UI/Timeline/CelButton.gd index 03edb4053..35ec7853b 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -32,9 +32,11 @@ func _ready() -> void: elif cel is GroupCel: transparent_checker.visible = false elif cel is AudioCel: + popup_menu.add_item("Play audio here") _is_playing_audio() Global.cel_switched.connect(_is_playing_audio) Global.current_project.fps_changed.connect(_is_playing_audio) + Global.current_project.layers[layer].playback_frame_changed.connect(_is_playing_audio) func _notification(what: int) -> void: @@ -134,7 +136,11 @@ func _on_PopupMenu_id_pressed(id: int) -> void: properties.cel_indices = _get_cel_indices() properties.popup_centered() MenuOptions.DELETE: - _delete_cel_content() + var layer_class := Global.current_project.layers[layer] + if layer_class is AudioLayer: + layer_class.playback_frame = frame + else: + _delete_cel_content() MenuOptions.LINK, MenuOptions.UNLINK: var project := Global.current_project From ef9a3e47a25b8ad1e24e76c44d9b2eae380df362 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:27:05 +0200 Subject: [PATCH 25/26] Update Translations.pot --- Translations/Translations.pot | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Translations/Translations.pot b/Translations/Translations.pot index 948144197..df9c4a3c4 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -1773,6 +1773,10 @@ msgstr "" msgid "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." msgstr "" +#. An option found in the preferences, under the Performance section. +msgid "Use dummy audio driver" +msgstr "" + #. Found in the Preferences, under Drivers. Specifies the renderer/video driver being used. msgid "Renderer:" msgstr "" @@ -2203,6 +2207,10 @@ msgstr "" msgid "Unlink Cels" msgstr "" +#. An option found in the right click menu of an audio cel. If selected, the audio of the audio layer will start playing from this frame. +msgid "Play audio here" +msgstr "" + msgid "Properties" msgstr "" @@ -2275,6 +2283,11 @@ msgstr "" msgid "Add Tilemap Layer" msgstr "" +#. One of the options of the create new layer button. +#: src/UI/Timeline/AnimationTimeline.tscn +msgid "Add Audio Layer" +msgstr "" + #: src/UI/Timeline/AnimationTimeline.tscn msgid "Remove current layer" msgstr "" @@ -2405,6 +2418,17 @@ msgstr "" msgid "Expand/collapse group" msgstr "" +#. Refers to the audio file of an audio layer. +msgid "Audio file:" +msgstr "" + +msgid "Load file" +msgstr "" + +#. An option in the audio layer properties, allows users to play the audio starting from a specific frame. +msgid "Play at frame:" +msgstr "" + msgid "Palette" msgstr "" From fa3642f99c2743178e8868bfd5e965e8638e8651 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:36:33 +0200 Subject: [PATCH 26/26] Some code improvements and documentation --- Translations/Translations.pot | 3 +++ src/Autoload/Global.gd | 2 +- src/Classes/Layers/AudioLayer.gd | 13 +++++++++---- src/Preferences/PreferencesDialog.tscn | 2 -- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Translations/Translations.pot b/Translations/Translations.pot index df9c4a3c4..621efde8a 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -2251,6 +2251,9 @@ msgstr "" msgid "Tilemap" msgstr "" +msgid "Audio" +msgstr "" + msgid "Layers" msgstr "" diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index edbab1ab7..420ec8672 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -1193,6 +1193,6 @@ func _save_to_override_file() -> void: file.store_line("[display]\n") file.store_line("window/subwindows/embed_subwindows=%s" % single_window_mode) file.store_line("window/per_pixel_transparency/allowed=%s" % window_transparency) - file.store_line("[audio]\n") if dummy_audio_driver: + file.store_line("[audio]\n") file.store_line('driver/driver="Dummy"') diff --git a/src/Classes/Layers/AudioLayer.gd b/src/Classes/Layers/AudioLayer.gd index 0a6100bf4..ca22fb086 100644 --- a/src/Classes/Layers/AudioLayer.gd +++ b/src/Classes/Layers/AudioLayer.gd @@ -1,18 +1,21 @@ class_name AudioLayer extends BaseLayer +## A unique type of layer which acts as an audio track for the timeline. +## Each audio layer has one audio stream, and its starting position can be +## in any point during the animation. signal audio_changed signal playback_frame_changed -var audio: AudioStream: +var audio: AudioStream: ## The audio stream of the layer. set(value): audio = value audio_changed.emit() -var playback_position := 0.0: ## Measured in seconds. +var playback_position := 0.0: ## The time in seconds where the audio stream starts playing. get(): var frame := project.frames[playback_frame] return frame.position_in_seconds(project) -var playback_frame := 0: +var playback_frame := 0: ## The frame where the audio stream starts playing. set(value): playback_frame = value playback_frame_changed.emit() @@ -23,6 +26,7 @@ func _init(_project: Project, _name := "") -> void: name = _name +## Returns the length of the audio stream. func get_audio_length() -> float: if is_instance_valid(audio): return audio.get_length() @@ -30,6 +34,7 @@ func get_audio_length() -> float: return -1.0 +## Returns the class name of the audio stream. E.g. "AudioStreamMP3". func get_audio_type() -> String: if not is_instance_valid(audio): return "" @@ -61,4 +66,4 @@ func new_empty_cel() -> AudioCel: func set_name_to_default(number: int) -> void: - name = tr("Audio track") + " %s" % number + name = tr("Audio") + " %s" % number diff --git a/src/Preferences/PreferencesDialog.tscn b/src/Preferences/PreferencesDialog.tscn index bdc767232..341e268df 100644 --- a/src/Preferences/PreferencesDialog.tscn +++ b/src/Preferences/PreferencesDialog.tscn @@ -1156,13 +1156,11 @@ text = "On" [node name="DummyAudioDriverLabel" type="Label" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] layout_mode = 2 -tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." mouse_filter = 0 text = "Use dummy audio driver" [node name="DummyAudioDriver" type="CheckBox" parent="HSplitContainer/VBoxContainer/ScrollContainer/RightSide/Performance/PerformanceContainer" groups=["DesktopOnly"]] layout_mode = 2 -tooltip_text = "If enabled, the application window can become transparent. This affects performance, so keep it off if you don't need it." mouse_default_cursor_shape = 2 text = "On"