Initial commit. Implement music sync features.
This commit is contained in:
4
.editorconfig
Normal file
4
.editorconfig
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Normalize EOL for all files that Git considers text files.
|
||||||
|
* text=auto eol=lf
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
|
|
||||||
|
# Ignore folders starting with .
|
||||||
|
.*/
|
||||||
BIN
chart/test_nibelungen/audio.mp3
Normal file
BIN
chart/test_nibelungen/audio.mp3
Normal file
Binary file not shown.
19
chart/test_nibelungen/audio.mp3.import
Normal file
19
chart/test_nibelungen/audio.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="mp3"
|
||||||
|
type="AudioStreamMP3"
|
||||||
|
uid="uid://btmy8ffph5gn3"
|
||||||
|
path="res://.godot/imported/audio.mp3-a6ce572dbede3712a85c3e444692049c.mp3str"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://chart/test_nibelungen/audio.mp3"
|
||||||
|
dest_files=["res://.godot/imported/audio.mp3-a6ce572dbede3712a85c3e444692049c.mp3str"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
9
chart/test_nibelungen/tempo.tres
Normal file
9
chart/test_nibelungen/tempo.tres
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="Tempo" load_steps=2 format=3 uid="uid://b34uhnkvwfyc1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c3vwmf1f1woo3" path="res://resource_type/tempo.gd" id="1_yhdoo"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_yhdoo")
|
||||||
|
bpms = Array[float]([160.0, 222.0, 200.0, 190.0, 170.0, 150.0, 135.0])
|
||||||
|
lengths = Array[float]([51.0, 638.0, 640.0, 641.0, 642.0, 644.0])
|
||||||
|
metadata/_custom_type_script = "uid://c3vwmf1f1woo3"
|
||||||
1
image/godot_icon.svg
Normal file
1
image/godot_icon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
43
image/godot_icon.svg.import
Normal file
43
image/godot_icon.svg.import
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://m4o76cw7njx2"
|
||||||
|
path="res://.godot/imported/godot_icon.svg-701d499c081c2a6850df1a5de9e7d612.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://image/godot_icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/godot_icon.svg-701d499c081c2a6850df1a5de9e7d612.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
21
project.godot
Normal file
21
project.godot
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; It's best edited using the editor UI and not directly,
|
||||||
|
; since the parameters that go here are not all obvious.
|
||||||
|
;
|
||||||
|
; Format:
|
||||||
|
; [section] ; section goes between []
|
||||||
|
; param=value ; assign values to parameters
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="SolarBeat"
|
||||||
|
run/main_scene="uid://dwro2d0482v0p"
|
||||||
|
config/features=PackedStringArray("4.5", "GL Compatibility")
|
||||||
|
config/icon="res://image/godot_icon.svg"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="gl_compatibility"
|
||||||
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
4
resource_type/chart.gd
Normal file
4
resource_type/chart.gd
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
## Contains all the notes and events that make up a chart.
|
||||||
|
class_name Chart extends Resource
|
||||||
|
|
||||||
|
var notes: Array[Note]
|
||||||
1
resource_type/chart.gd.uid
Normal file
1
resource_type/chart.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://oih6cupm4xnu
|
||||||
10
resource_type/music.gd
Normal file
10
resource_type/music.gd
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
class_name Music extends Resource
|
||||||
|
|
||||||
|
## Music AudioStream.
|
||||||
|
@export var stream: AudioStream = null
|
||||||
|
|
||||||
|
## Music offset in seconds.
|
||||||
|
@export var offset: float = 0.0
|
||||||
|
|
||||||
|
## BPM data to sync to the music.
|
||||||
|
@export var tempo: Tempo = Tempo.new()
|
||||||
1
resource_type/music.gd.uid
Normal file
1
resource_type/music.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://rg6orh6kutai
|
||||||
13
resource_type/song.gd
Normal file
13
resource_type/song.gd
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
## Represents a level for a music audio that may contain multiple charts.
|
||||||
|
class_name Song extends Resource
|
||||||
|
|
||||||
|
var music: Music
|
||||||
|
|
||||||
|
var charts: Dictionary[StringName, Chart] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func has_music() -> bool:
|
||||||
|
return music != null and music.stream != null
|
||||||
|
|
||||||
|
func has_chart() -> bool:
|
||||||
|
return not charts.is_empty()
|
||||||
1
resource_type/song.gd.uid
Normal file
1
resource_type/song.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://jcm4r2avbciq
|
||||||
75
resource_type/tempo.gd
Normal file
75
resource_type/tempo.gd
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
## Resource for storing BPM data and loading into a SyncTrack.
|
||||||
|
class_name Tempo extends Resource
|
||||||
|
|
||||||
|
## Defines how the BPM changes throughout its length.
|
||||||
|
enum BPM_TYPE {
|
||||||
|
HOLD = 0, ## BPM stays constant through the whole length.
|
||||||
|
LINEAR = 1 ## BPM linearly interpolates to the next BPM.
|
||||||
|
}
|
||||||
|
|
||||||
|
# The data for each BPM is stored in the same index
|
||||||
|
# across the different arrays.
|
||||||
|
@export var bpms: Array[float] = []
|
||||||
|
@export var lengths: Array[float] = []
|
||||||
|
@export var types: Array[BPM_TYPE] = []
|
||||||
|
|
||||||
|
|
||||||
|
## Get the number of valid BPM changes.
|
||||||
|
func size() -> int:
|
||||||
|
return min(bpms.size(), lengths.size() + 1)
|
||||||
|
|
||||||
|
|
||||||
|
## Ensure the arrays obey the following properties:
|
||||||
|
## all arrays are the same size (every BPM has a length and type),
|
||||||
|
## last element of BPM Length Beats is -1 (since the
|
||||||
|
## corresponding BPM is assumed to last the rest of the chart).
|
||||||
|
## Extra BPM or BPM Length values are truncated.
|
||||||
|
## If BPM Types must be resized greater, assume every BPM after is
|
||||||
|
## BPM_TYPE.HOLD.
|
||||||
|
func normalize() -> void:
|
||||||
|
var target_size := size()
|
||||||
|
bpms.resize(target_size)
|
||||||
|
lengths.resize(target_size)
|
||||||
|
types.resize(target_size)
|
||||||
|
|
||||||
|
if target_size > 0:
|
||||||
|
_make_bpm_positive()
|
||||||
|
_make_lengths_positive()
|
||||||
|
lengths[-1] = -1
|
||||||
|
types[-1] = BPM_TYPE.HOLD
|
||||||
|
|
||||||
|
|
||||||
|
#----- Static Constructors -----#
|
||||||
|
|
||||||
|
static func create_from_arrays(
|
||||||
|
p_bpms: Array[float],
|
||||||
|
p_lengths: Array[float],
|
||||||
|
p_types: Array[BPM_TYPE]
|
||||||
|
) -> Tempo:
|
||||||
|
var result: Tempo = Tempo.new()
|
||||||
|
result.bpms = p_bpms
|
||||||
|
result.lengths = p_lengths
|
||||||
|
result.types = p_types
|
||||||
|
result.normalize()
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func create_from_nodes(_p_nodes: Array[Variant]) -> Tempo:
|
||||||
|
var result: Tempo = Tempo.new()
|
||||||
|
# Parse array and add data to result...
|
||||||
|
# Might not be used?
|
||||||
|
return result
|
||||||
|
#----- ------------- -----#
|
||||||
|
|
||||||
|
|
||||||
|
## Makes sure every BPM entry is positive.
|
||||||
|
## Negative BPMs are inverted.
|
||||||
|
func _make_bpm_positive() -> void:
|
||||||
|
for i: int in range(bpms.size()):
|
||||||
|
bpms[i] = abs(bpms[i])
|
||||||
|
|
||||||
|
|
||||||
|
## Makes sure every length entry is positive.
|
||||||
|
## Negative lengths are inverted.
|
||||||
|
func _make_lengths_positive() -> void:
|
||||||
|
for i: int in range(lengths.size()):
|
||||||
|
lengths[i] = abs(lengths[i])
|
||||||
1
resource_type/tempo.gd.uid
Normal file
1
resource_type/tempo.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c3vwmf1f1woo3
|
||||||
9
rhythm_game/lane/lane.gd
Normal file
9
rhythm_game/lane/lane.gd
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class_name Lane extends Node2D
|
||||||
|
|
||||||
|
|
||||||
|
func get_hit_pos() -> Vector2:
|
||||||
|
return position
|
||||||
|
|
||||||
|
|
||||||
|
func update(_beat: BeatUpdate) -> void:
|
||||||
|
pass
|
||||||
1
rhythm_game/lane/lane.gd.uid
Normal file
1
rhythm_game/lane/lane.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://u42y08gn6bi7
|
||||||
6
rhythm_game/lane/lane.tscn
Normal file
6
rhythm_game/lane/lane.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://ch06ds4gr82nl"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://u42y08gn6bi7" path="res://rhythm_game/lane/lane.gd" id="1_p48sd"]
|
||||||
|
|
||||||
|
[node name="Lane" type="Node2D"]
|
||||||
|
script = ExtResource("1_p48sd")
|
||||||
5
rhythm_game/metronome.gd
Normal file
5
rhythm_game/metronome.gd
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class_name Metronome extends AudioStreamPlayer
|
||||||
|
|
||||||
|
func on_conductor_ticked(update: BeatUpdate) -> void:
|
||||||
|
if update.new_beat():
|
||||||
|
play()
|
||||||
1
rhythm_game/metronome.gd.uid
Normal file
1
rhythm_game/metronome.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bbpyym0kgujev
|
||||||
26
rhythm_game/music_sync/beat_update.gd
Normal file
26
rhythm_game/music_sync/beat_update.gd
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
class_name BeatUpdate
|
||||||
|
|
||||||
|
## Beat this frame.
|
||||||
|
var current: float
|
||||||
|
|
||||||
|
## Beat last frame.
|
||||||
|
var previous: float
|
||||||
|
|
||||||
|
|
||||||
|
## Beat updated forward in time.
|
||||||
|
func forward() -> bool:
|
||||||
|
return current > previous
|
||||||
|
|
||||||
|
|
||||||
|
## Beat updated backward in time.
|
||||||
|
func backward() -> bool:
|
||||||
|
return current < previous
|
||||||
|
|
||||||
|
|
||||||
|
func new_beat() -> bool:
|
||||||
|
return floori(previous) != floori(current)
|
||||||
|
|
||||||
|
|
||||||
|
func _init(current_beat: float = 0.0, previous_beat: float = 0.0) -> void:
|
||||||
|
current = current_beat
|
||||||
|
previous = previous_beat
|
||||||
1
rhythm_game/music_sync/beat_update.gd.uid
Normal file
1
rhythm_game/music_sync/beat_update.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dyjoi5i3iha0h
|
||||||
142
rhythm_game/music_sync/conductor.gd
Normal file
142
rhythm_game/music_sync/conductor.gd
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
## Class for playing music and providing an interface to keep events in sync.
|
||||||
|
class_name Conductor extends Node
|
||||||
|
|
||||||
|
## If true, start immediately when ready.
|
||||||
|
@export var autostart: bool = false
|
||||||
|
|
||||||
|
## Music to play.
|
||||||
|
@export var music: Music
|
||||||
|
|
||||||
|
signal started
|
||||||
|
signal ticked(beat_update: BeatUpdate)
|
||||||
|
signal ticked_forward(new_beat: float)
|
||||||
|
#signal ticked_backward(new_beat: float)
|
||||||
|
signal finished
|
||||||
|
|
||||||
|
|
||||||
|
func start() -> void:
|
||||||
|
assert(music != null)
|
||||||
|
assert(_audio_player != null)
|
||||||
|
|
||||||
|
_stop_playback_and_initialize()
|
||||||
|
_active = true
|
||||||
|
started.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return _active
|
||||||
|
|
||||||
|
|
||||||
|
func current_time() -> float:
|
||||||
|
return(_audio_player.get_playback_position() +
|
||||||
|
AudioServer.get_time_since_last_mix() +
|
||||||
|
_music_offset - _remaining_silence)
|
||||||
|
|
||||||
|
|
||||||
|
func previous_time() -> float:
|
||||||
|
return _previous_time
|
||||||
|
|
||||||
|
|
||||||
|
func current_beat() -> float:
|
||||||
|
return _sync_track.to_beat(current_time())
|
||||||
|
|
||||||
|
|
||||||
|
func previous_beat() -> float:
|
||||||
|
return _previous_beat
|
||||||
|
|
||||||
|
|
||||||
|
func current_bpm() -> float:
|
||||||
|
return _sync_track.bpm_at_time(current_time())
|
||||||
|
|
||||||
|
|
||||||
|
func add_time(seconds: float) -> void:
|
||||||
|
if _audio_player == null:
|
||||||
|
return
|
||||||
|
_audio_player.seek(_audio_player.get_playback_position() + seconds)
|
||||||
|
_previous_time = current_time()
|
||||||
|
_previous_beat = current_beat()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ====== IMPLEMENTATION ====== #
|
||||||
|
|
||||||
|
var _active: bool = false
|
||||||
|
|
||||||
|
var _previous_time: float = -1.0
|
||||||
|
var _previous_beat: float = -1.0
|
||||||
|
|
||||||
|
var _audio_player: AudioStreamPlayer = null
|
||||||
|
var _sync_track: SyncTrack = SyncTrack.new()
|
||||||
|
|
||||||
|
var _music_offset: float
|
||||||
|
var _music_playing: bool = false
|
||||||
|
var _remaining_silence: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_audio_player = AudioStreamPlayer.new()
|
||||||
|
add_child(_audio_player)
|
||||||
|
_audio_player.finished.connect(_on_music_finished)
|
||||||
|
|
||||||
|
if autostart:
|
||||||
|
start()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_music_finished() -> void:
|
||||||
|
_music_playing = false
|
||||||
|
_active = false
|
||||||
|
finished.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not _active:
|
||||||
|
return
|
||||||
|
|
||||||
|
if _music_playing:
|
||||||
|
_beat_update()
|
||||||
|
else:
|
||||||
|
_update_silence(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_silence(elapsed_time: float) -> void:
|
||||||
|
_remaining_silence -= elapsed_time
|
||||||
|
|
||||||
|
if _remaining_silence < 0:
|
||||||
|
_play_music_from_offset(-_remaining_silence)
|
||||||
|
_remaining_silence = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _beat_update() -> void:
|
||||||
|
var _current_time = current_time()
|
||||||
|
|
||||||
|
# _audio_player.get_playback_position() used in current_time()
|
||||||
|
# is not perfect and may return a lesser value than last frame.
|
||||||
|
# This makes sure the beat never updates backward.
|
||||||
|
if _current_time <= _previous_time:
|
||||||
|
return
|
||||||
|
|
||||||
|
var _current_beat = _sync_track.to_beat(_current_time)
|
||||||
|
ticked.emit(BeatUpdate.new(_current_beat, _previous_beat))
|
||||||
|
ticked_forward.emit(_current_beat)
|
||||||
|
|
||||||
|
_previous_beat = _current_beat
|
||||||
|
_previous_time = _current_time
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_playback_and_initialize() -> void:
|
||||||
|
if _audio_player.playing:
|
||||||
|
_audio_player.stop()
|
||||||
|
|
||||||
|
_active = false
|
||||||
|
_music_playing = false
|
||||||
|
_previous_time = -1.0
|
||||||
|
_previous_beat = -1.0
|
||||||
|
_music_offset = music.offset
|
||||||
|
_remaining_silence = _music_offset
|
||||||
|
_audio_player.stream = music.stream
|
||||||
|
_sync_track.initialize(music.tempo)
|
||||||
|
|
||||||
|
|
||||||
|
func _play_music_from_offset(offset: float) -> void:
|
||||||
|
_audio_player.play(offset)
|
||||||
|
_music_playing = true
|
||||||
1
rhythm_game/music_sync/conductor.gd.uid
Normal file
1
rhythm_game/music_sync/conductor.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://s16dt0bu0jrg
|
||||||
25
rhythm_game/music_sync/event_layout.gd
Normal file
25
rhythm_game/music_sync/event_layout.gd
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
class_name EventLayout extends Node
|
||||||
|
|
||||||
|
#var _beats: Array[float]
|
||||||
|
#var _callables: Array[Callable]
|
||||||
|
#var _index: int = 0
|
||||||
|
|
||||||
|
### Schedule the callable to be called on the specified beat.
|
||||||
|
#func schedule(beat: float, callable: Callable) -> void:
|
||||||
|
#var i := schedule_beats.find_custom(func(s_beat: float): return s_beat > beat )
|
||||||
|
#if i == -1:
|
||||||
|
#i = schedule_beats.size()
|
||||||
|
#schedule_beats.insert(i, beat)
|
||||||
|
#schedule_callables.insert(i, callable)
|
||||||
|
#
|
||||||
|
### Clears all scheduled callables.
|
||||||
|
#func reset_schedule() -> void:
|
||||||
|
#schedule_beats.clear()
|
||||||
|
#schedule_callables.clear()
|
||||||
|
#schedule_index = 0
|
||||||
|
|
||||||
|
#
|
||||||
|
#while(schedule_index < schedule_beats.size()
|
||||||
|
#and get_beat() >= schedule_beats.get(schedule_index)):
|
||||||
|
#schedule_callables.get(schedule_index).call()
|
||||||
|
#schedule_index += 1
|
||||||
1
rhythm_game/music_sync/event_layout.gd.uid
Normal file
1
rhythm_game/music_sync/event_layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://102cl75cfpgw
|
||||||
167
rhythm_game/music_sync/sync_track.gd
Normal file
167
rhythm_game/music_sync/sync_track.gd
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
## Class for converting beats to time (since start of music) and vice versa.
|
||||||
|
class_name SyncTrack extends Node
|
||||||
|
|
||||||
|
|
||||||
|
func initialize(tempo: Tempo) -> void:
|
||||||
|
_set_bpm_data(tempo)
|
||||||
|
_calculate_cache()
|
||||||
|
|
||||||
|
|
||||||
|
## Get the number of valid BPM changes.
|
||||||
|
func size() -> int:
|
||||||
|
return min(_bpms.size(), _lengths.size() + 1)
|
||||||
|
|
||||||
|
|
||||||
|
## Get the BPM at the given time (since music start).
|
||||||
|
func bpm_at_time(time: float) -> float:
|
||||||
|
var i: int = _get_bpm_index_at_time(time)
|
||||||
|
if _types[i] == Tempo.BPM_TYPE.LINEAR:
|
||||||
|
var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time)
|
||||||
|
return lerp(_bpms[i], _bpms[i+1], delta)
|
||||||
|
else:
|
||||||
|
return _bpms.get(i)
|
||||||
|
|
||||||
|
|
||||||
|
## Get the beat at the given time (since music start).
|
||||||
|
func to_beat(time: float) -> float:
|
||||||
|
var i: int = _get_bpm_index_at_time(time)
|
||||||
|
|
||||||
|
# If i is not the last BPM:
|
||||||
|
if i < _bpms.size() - 1:
|
||||||
|
var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time)
|
||||||
|
return lerp(_elapsed_beats[i], _elapsed_beats[i+1], delta)
|
||||||
|
|
||||||
|
# Else i is the last BPM:
|
||||||
|
else:
|
||||||
|
var bps: float = _bpms[i] / 60.0
|
||||||
|
var remaining_seconds: float = time - _elapsed_seconds[i]
|
||||||
|
var remaining_beats: float = remaining_seconds * bps
|
||||||
|
return _elapsed_beats[i] + remaining_beats
|
||||||
|
|
||||||
|
|
||||||
|
## Get the time (since music start) at the given beat.
|
||||||
|
func to_time(beat: float) -> float:
|
||||||
|
var i: int = _get_bpm_index_at_beat(beat)
|
||||||
|
|
||||||
|
if i == -1:
|
||||||
|
return -1.0
|
||||||
|
|
||||||
|
# Else if i is not the last BPM:
|
||||||
|
elif i < _bpms.size() - 1:
|
||||||
|
var delta = inverse_lerp(_elapsed_beats[i], _elapsed_beats[i+1], beat)
|
||||||
|
return lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], delta)
|
||||||
|
|
||||||
|
# Else i is the last BPM:
|
||||||
|
else:
|
||||||
|
var bps: float = _bpms[i] / 60.0
|
||||||
|
var remaining_beats: float = beat - _elapsed_beats[i]
|
||||||
|
var remaining_seconds: float = remaining_beats / bps
|
||||||
|
return _elapsed_seconds[i] + remaining_seconds
|
||||||
|
|
||||||
|
|
||||||
|
func _init(tempo: Tempo = Tempo.new()) -> void:
|
||||||
|
initialize(tempo)
|
||||||
|
|
||||||
|
|
||||||
|
# IMPLEMENTATION
|
||||||
|
|
||||||
|
# === Note about BPM data ===
|
||||||
|
# The data for each BPM is stored in the same index
|
||||||
|
# across the different arrays.
|
||||||
|
# ===========================
|
||||||
|
|
||||||
|
## BPM array (Beats per Minute).
|
||||||
|
var _bpms: Array[float]
|
||||||
|
## The duration of each BPM in beats.
|
||||||
|
var _lengths: Array[float]
|
||||||
|
## The BpmData.BPM_TYPE of each BPM.
|
||||||
|
var _types: Array[Tempo.BPM_TYPE]
|
||||||
|
|
||||||
|
# Cache variables that make calculations easier.
|
||||||
|
## The duration of each BPM in seconds.
|
||||||
|
var _length_seconds: Array[float]
|
||||||
|
## Seconds passed before the corresponding BPM.
|
||||||
|
var _elapsed_seconds: Array[float] = [0.0]
|
||||||
|
## Beats passed before the corresponding BPM.
|
||||||
|
var _elapsed_beats: Array[float] = [0.0]
|
||||||
|
|
||||||
|
|
||||||
|
## Set the BPM data arrays.
|
||||||
|
func _set_bpm_data(tempo: Tempo) -> void:
|
||||||
|
|
||||||
|
_clear_bpm_data()
|
||||||
|
tempo.normalize()
|
||||||
|
_bpms = tempo.bpms
|
||||||
|
_lengths = tempo.lengths
|
||||||
|
_types = tempo.types
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_cache() -> void:
|
||||||
|
_clear_cache()
|
||||||
|
for i: int in range(0, _bpms.size() - 1):
|
||||||
|
var bps: float
|
||||||
|
var length_in_beats: float = _lengths[i]
|
||||||
|
if _types[i] == Tempo.BPM_TYPE.LINEAR:
|
||||||
|
# Average BPS.
|
||||||
|
bps = (_bpms[i] + _bpms[i+1]) / 120.0
|
||||||
|
else:
|
||||||
|
bps = _bpms[i] / 60.0
|
||||||
|
var length_in_seconds: float = length_in_beats / bps
|
||||||
|
|
||||||
|
_length_seconds.append(length_in_seconds)
|
||||||
|
_elapsed_seconds.append(_elapsed_seconds.back() + length_in_seconds)
|
||||||
|
_elapsed_beats.append(_elapsed_beats.back() + length_in_beats)
|
||||||
|
|
||||||
|
if _length_seconds.size() > 0:
|
||||||
|
_length_seconds[-1] = -1.0
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_bpm_data() -> void:
|
||||||
|
_bpms = []
|
||||||
|
_lengths = []
|
||||||
|
_types = []
|
||||||
|
|
||||||
|
|
||||||
|
func _clear_cache() -> void:
|
||||||
|
_length_seconds = []
|
||||||
|
_elapsed_seconds = [0.0]
|
||||||
|
_elapsed_beats = [0.0]
|
||||||
|
|
||||||
|
|
||||||
|
func _get_bpm_index_at_time(time: float) -> int:
|
||||||
|
assert(_elapsed_seconds.size() > 0)
|
||||||
|
assert(_bpms.size() > 0)
|
||||||
|
|
||||||
|
var _find_index: Callable = func (p_elapsed_seconds: float) -> bool:
|
||||||
|
return time < p_elapsed_seconds
|
||||||
|
|
||||||
|
var index: int = _elapsed_seconds.find_custom(_find_index)
|
||||||
|
if index == -1:
|
||||||
|
index = _bpms.size() - 1
|
||||||
|
else:
|
||||||
|
index -= 1
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
func _get_bpm_index_at_beat(beat: float) -> int:
|
||||||
|
var _find_index: Callable = func (p_elapsed_beats: float) -> bool:
|
||||||
|
return beat < p_elapsed_beats
|
||||||
|
|
||||||
|
if _elapsed_beats.size() == 0:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
var index: int = _elapsed_beats.find_custom(_find_index)
|
||||||
|
if index > 0:
|
||||||
|
index -= 1
|
||||||
|
elif index == -1:
|
||||||
|
index = max(0, _bpms.size()-1)
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
func _print_data() -> void:
|
||||||
|
print("=== SyncTrack ===")
|
||||||
|
print("BPMs: ", _bpms)
|
||||||
|
print("BPM Lengths (Beats): ", _lengths)
|
||||||
|
print("BPM Types: ", _types)
|
||||||
|
print("Elapsed Seconds: ", _elapsed_seconds)
|
||||||
|
print("Elapsed Beats: ", _elapsed_beats)
|
||||||
1
rhythm_game/music_sync/sync_track.gd.uid
Normal file
1
rhythm_game/music_sync/sync_track.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://wbqg55ld6py5
|
||||||
18
rhythm_game/note.gd
Normal file
18
rhythm_game/note.gd
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
class_name Note extends Object
|
||||||
|
|
||||||
|
enum TYPE {
|
||||||
|
TAP = 0,
|
||||||
|
HOLD_START = 1,
|
||||||
|
HOLD_END = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
var hit_beat: float
|
||||||
|
|
||||||
|
var type: TYPE
|
||||||
|
|
||||||
|
var lane: int
|
||||||
|
|
||||||
|
func _init(p_hit_beat: float = -99.0, p_lane: int = 0, p_type: TYPE = TYPE.TAP) -> void:
|
||||||
|
hit_beat = p_hit_beat
|
||||||
|
lane = p_lane
|
||||||
|
type = p_type
|
||||||
1
rhythm_game/note.gd.uid
Normal file
1
rhythm_game/note.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cgxrnivh6rjje
|
||||||
84
rhythm_game/note_layout.gd
Normal file
84
rhythm_game/note_layout.gd
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#TODO Split class into two.
|
||||||
|
# One should be responsible for storing arrays of data, like a NoteData class?
|
||||||
|
# Another represents a slice of the data, NoteView.
|
||||||
|
# Before that, figure out how the part of a hold note between the hold start
|
||||||
|
# and hold end will be implemented.
|
||||||
|
class_name NoteLayout extends Node
|
||||||
|
|
||||||
|
## Controls at what beat notes are visible.
|
||||||
|
@export_group("Note View Offset")
|
||||||
|
## At what beat offset from the current beat will notes will be visible.
|
||||||
|
## Example, spawn = 4.0 means notes are spawned 4 beats before
|
||||||
|
## they are meant to be hit.
|
||||||
|
@export var spawn: float = 4.0
|
||||||
|
|
||||||
|
## At what beat offset from the current beat will notes be invisible.
|
||||||
|
## Example, despawn = 4.0 means notes are despawned 4 beats after
|
||||||
|
## they are meant to be hit.
|
||||||
|
@export var despawn: float = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
## The index in notes of the first active note.
|
||||||
|
func start() -> int:
|
||||||
|
return _start
|
||||||
|
|
||||||
|
|
||||||
|
## The index in notes after the last active note.
|
||||||
|
func end() -> int:
|
||||||
|
return _end
|
||||||
|
|
||||||
|
|
||||||
|
func size() -> int:
|
||||||
|
return _beat.size()
|
||||||
|
|
||||||
|
|
||||||
|
func beat(index: int) -> float:
|
||||||
|
return _beat[index]
|
||||||
|
|
||||||
|
|
||||||
|
func lane(index: int) -> int:
|
||||||
|
return _lane[index]
|
||||||
|
|
||||||
|
|
||||||
|
func type(index: int) -> Note.TYPE:
|
||||||
|
return _type[index]
|
||||||
|
|
||||||
|
|
||||||
|
# ======= IMPLEMENTATION ======== #
|
||||||
|
|
||||||
|
# All notes in the chart, arranged by ascending beat.
|
||||||
|
var _beat: Array[float] = []
|
||||||
|
var _lane: Array[int] = []
|
||||||
|
var _type: Array[Note.TYPE] = []
|
||||||
|
|
||||||
|
var _start: int = 0
|
||||||
|
var _end: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _push_note(_note: Note) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# TODO FIX THESE
|
||||||
|
func _update_forward(new_beat: float) -> void:
|
||||||
|
var spawn_beat = new_beat + spawn
|
||||||
|
|
||||||
|
while _end < size() and _beat[_end] <= spawn_beat:
|
||||||
|
_end += 1
|
||||||
|
|
||||||
|
var despawn_beat = new_beat - despawn
|
||||||
|
|
||||||
|
while _start < size() and _beat[_start] < despawn_beat:
|
||||||
|
_start += 1
|
||||||
|
|
||||||
|
# TODO FIX THESE
|
||||||
|
#func _update_backward(new_beat: float) -> void:
|
||||||
|
#var spawn_beat = new_beat + note_spawn_offset
|
||||||
|
#
|
||||||
|
#while _end >= 0 and _beat[_end] > spawn_beat:
|
||||||
|
#_end -= 1
|
||||||
|
#
|
||||||
|
#var despawn_beat = new_beat - note_despawn_offset
|
||||||
|
#
|
||||||
|
#while _start >= 0 and _beat[_start] < despawn_beat:
|
||||||
|
#_start -= 1
|
||||||
1
rhythm_game/note_layout.gd.uid
Normal file
1
rhythm_game/note_layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dlnnbx2wvn66t
|
||||||
37
rhythm_game/rhythm_game.tscn
Normal file
37
rhythm_game/rhythm_game.tscn
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[gd_scene load_steps=10 format=3 uid="uid://dwro2d0482v0p"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dlnnbx2wvn66t" path="res://rhythm_game/note_layout.gd" id="1_cr5rn"]
|
||||||
|
[ext_resource type="Script" uid="uid://102cl75cfpgw" path="res://rhythm_game/music_sync/event_layout.gd" id="1_jnfl3"]
|
||||||
|
[ext_resource type="Script" uid="uid://s16dt0bu0jrg" path="res://rhythm_game/music_sync/conductor.gd" id="2_62aw1"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://btmy8ffph5gn3" path="res://chart/test_nibelungen/audio.mp3" id="3_txi6k"]
|
||||||
|
[ext_resource type="Resource" uid="uid://b34uhnkvwfyc1" path="res://chart/test_nibelungen/tempo.tres" id="5_10cpq"]
|
||||||
|
[ext_resource type="Script" uid="uid://rg6orh6kutai" path="res://resource_type/music.gd" id="5_nsyv8"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://be8dyt7nfpffw" path="res://sfx/sfx_cowbell.ogg" id="6_ecbku"]
|
||||||
|
[ext_resource type="Script" uid="uid://bbpyym0kgujev" path="res://rhythm_game/metronome.gd" id="7_nsyv8"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_10cpq"]
|
||||||
|
script = ExtResource("5_nsyv8")
|
||||||
|
stream = ExtResource("3_txi6k")
|
||||||
|
offset = -0.17
|
||||||
|
tempo = ExtResource("5_10cpq")
|
||||||
|
metadata/_custom_type_script = "uid://rg6orh6kutai"
|
||||||
|
|
||||||
|
[node name="RhythmGame" type="Node2D"]
|
||||||
|
script = ExtResource("1_jnfl3")
|
||||||
|
|
||||||
|
[node name="Conductor" type="Node" parent="."]
|
||||||
|
script = ExtResource("2_62aw1")
|
||||||
|
autostart = true
|
||||||
|
music = SubResource("Resource_10cpq")
|
||||||
|
metadata/_custom_type_script = "uid://s16dt0bu0jrg"
|
||||||
|
|
||||||
|
[node name="NoteLayout" type="Node" parent="."]
|
||||||
|
script = ExtResource("1_cr5rn")
|
||||||
|
metadata/_custom_type_script = "uid://dlnnbx2wvn66t"
|
||||||
|
|
||||||
|
[node name="Metronome" type="AudioStreamPlayer" parent="."]
|
||||||
|
stream = ExtResource("6_ecbku")
|
||||||
|
script = ExtResource("7_nsyv8")
|
||||||
|
metadata/_custom_type_script = "uid://bbpyym0kgujev"
|
||||||
|
|
||||||
|
[connection signal="ticked" from="Conductor" to="Metronome" method="on_conductor_ticked"]
|
||||||
BIN
sfx/sfx_cowbell.ogg
Normal file
BIN
sfx/sfx_cowbell.ogg
Normal file
Binary file not shown.
19
sfx/sfx_cowbell.ogg.import
Normal file
19
sfx/sfx_cowbell.ogg.import
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="oggvorbisstr"
|
||||||
|
type="AudioStreamOggVorbis"
|
||||||
|
uid="uid://be8dyt7nfpffw"
|
||||||
|
path="res://.godot/imported/sfx_cowbell.ogg-db8c1909cb010205f13e34959d743eb1.oggvorbisstr"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://sfx/sfx_cowbell.ogg"
|
||||||
|
dest_files=["res://.godot/imported/sfx_cowbell.ogg-db8c1909cb010205f13e34959d743eb1.oggvorbisstr"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
Reference in New Issue
Block a user