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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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" From 019a8db21c26c6ab5958f7b9922d1fc51787b23b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:13:32 +0200 Subject: [PATCH 27/30] Stop audio from playing when looping, and the audio does not play at the first frame --- src/UI/Timeline/AnimationTimeline.gd | 11 +++++++++++ src/UI/Timeline/LayerButton.gd | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index e1c1cd0e4..685f7e1f9 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -1,7 +1,14 @@ extends Panel +## Emitted when the animation starts playing. signal animation_started(forward: bool) +## Emitted when the animation reaches the final frame and is not looping, +## or if the animation is manually paused. +## Note: This signal is not emitted if the animation is looping. signal animation_finished +## Emitted when the animation loops, meaning when it reaches the final frame +## and the animation keeps playing. +signal animation_looped enum LoopType { NO, CYCLE, PINGPONG } @@ -688,9 +695,11 @@ func _on_AnimationTimer_timeout() -> void: animation_timer.wait_time = ( project.frames[project.current_frame].duration * (1 / fps) ) + animation_looped.emit() animation_timer.start() LoopType.PINGPONG: animation_forward = false + animation_looped.emit() _on_AnimationTimer_timeout() else: @@ -713,9 +722,11 @@ func _on_AnimationTimer_timeout() -> void: animation_timer.wait_time = ( project.frames[project.current_frame].duration * (1 / fps) ) + animation_looped.emit() animation_timer.start() LoopType.PINGPONG: animation_forward = true + animation_looped.emit() _on_AnimationTimer_timeout() frame_scroll_container.ensure_control_visible( Global.frame_hbox.get_child(project.current_frame) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index 8ef90e984..a82cceb51 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -47,6 +47,7 @@ func _ready() -> void: 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_looped.connect(_on_animation_looped) Global.animation_timeline.animation_finished.connect(_on_animation_finished) custom_minimum_size.y = Global.animation_timeline.cel_size label.text = layer.name @@ -78,6 +79,14 @@ func _on_animation_started(_dir: bool) -> void: _play_audio() +func _on_animation_looped() -> void: + var layer := Global.current_project.layers[layer_index] + if layer is AudioLayer: + if layer.playback_frame != 0: + if is_instance_valid(audio_player): + audio_player.stop() + + func _on_animation_finished() -> void: animation_running = false if is_instance_valid(audio_player): From 97495a19da4927e16ace07be58665973f2b91ce7 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:16:22 +0200 Subject: [PATCH 28/30] Update audio cel buttons when changing the audio of the layer --- 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 35ec7853b..b6af614f6 100644 --- a/src/UI/Timeline/CelButton.gd +++ b/src/UI/Timeline/CelButton.gd @@ -36,6 +36,7 @@ func _ready() -> void: _is_playing_audio() Global.cel_switched.connect(_is_playing_audio) Global.current_project.fps_changed.connect(_is_playing_audio) + Global.current_project.layers[layer].audio_changed.connect(_is_playing_audio) Global.current_project.layers[layer].playback_frame_changed.connect(_is_playing_audio) From 214ba1b17d8dca0acfc947332064320406868f7f Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:21:49 +0200 Subject: [PATCH 29/30] Mute audio layer when hiding it mid-play --- src/UI/Timeline/LayerButton.gd | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index a82cceb51..cc1740e3f 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -70,7 +70,10 @@ func _on_cel_switched() -> void: z_index = 1 if button_pressed else 0 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: + if not layer.is_visible_in_hierarchy(): + if is_instance_valid(audio_player): + audio_player.stop() + elif not animation_running or Global.current_project.current_frame == layer.playback_frame: _play_audio() @@ -82,7 +85,7 @@ func _on_animation_started(_dir: bool) -> void: func _on_animation_looped() -> void: var layer := Global.current_project.layers[layer_index] if layer is AudioLayer: - if layer.playback_frame != 0: + if layer.playback_frame != 0 or not layer.is_visible_in_hierarchy(): if is_instance_valid(audio_player): audio_player.stop() @@ -97,7 +100,7 @@ func _play_audio() -> void: if not is_instance_valid(audio_player): return var layer := Global.current_project.layers[layer_index] as AudioLayer - if not layer.visible: + if not layer.is_visible_in_hierarchy(): return var audio_length := layer.get_audio_length() var frame := Global.current_project.frames[Global.current_project.current_frame] From 710f6d41e670a21b1bcde078142bbff01848c126 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:29:35 +0200 Subject: [PATCH 30/30] Only plays the portion of the sound that corresponds to the specific frame so maybe we should do that as well When the animation is not running. If it is running, play the sound properly. --- src/UI/Timeline/LayerButton.gd | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index cc1740e3f..ecd09324a 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -70,16 +70,21 @@ func _on_cel_switched() -> void: z_index = 1 if button_pressed else 0 var layer := Global.current_project.layers[layer_index] if layer is AudioLayer: + if not is_instance_valid(audio_player): + return if not layer.is_visible_in_hierarchy(): - if is_instance_valid(audio_player): - audio_player.stop() - elif not animation_running or Global.current_project.current_frame == layer.playback_frame: - _play_audio() + audio_player.stop() + return + if animation_running: + if Global.current_project.current_frame == layer.playback_frame: + _play_audio(false) + else: + _play_audio(true) func _on_animation_started(_dir: bool) -> void: animation_running = true - _play_audio() + _play_audio(false) func _on_animation_looped() -> void: @@ -96,17 +101,21 @@ func _on_animation_finished() -> void: audio_player.stop() -func _play_audio() -> void: +func _play_audio(single_frame: bool) -> void: if not is_instance_valid(audio_player): return - var layer := Global.current_project.layers[layer_index] as AudioLayer + var project := Global.current_project + var layer := project.layers[layer_index] as AudioLayer if not layer.is_visible_in_hierarchy(): return 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) + var frame := project.frames[project.current_frame] + var frame_pos := frame.position_in_seconds(project, layer.playback_frame) if frame_pos >= 0 and frame_pos < audio_length: audio_player.play(frame_pos) + if single_frame: + var timer := get_tree().create_timer(frame.get_duration_in_seconds(project.fps)) + timer.timeout.connect(func(): audio_player.stop()) else: audio_player.stop()