From 40d2372f2beb3f420d722f39f41abff1f8a4dd20 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Wed, 20 Jul 2022 07:00:32 -0400 Subject: [PATCH] import retro platformer controller and mod player --- addons/modplayer/GMP.gd | 23 + addons/modplayer/Mod.gd | 290 +++++ addons/modplayer/ModPlayer.gd | 1053 +++++++++++++++++ addons/modplayer/ModPlayer.tscn | 6 + addons/modplayer/XM.gd | 287 +++++ addons/modplayer/icon.png | Bin 0 -> 924 bytes addons/modplayer/icon.png.import | 35 + addons/modplayer/plugin.cfg | 6 + addons/platformer_controller/._LICENSE | Bin 0 -> 4096 bytes addons/platformer_controller/DemoScene.tscn | 73 ++ addons/platformer_controller/LICENSE | 21 + addons/platformer_controller/README.md | 75 ++ .../platformer_controller.gd | 213 ++++ player/Player.tscn | 26 +- project.godot | 29 + 15 files changed, 2127 insertions(+), 10 deletions(-) create mode 100644 addons/modplayer/GMP.gd create mode 100644 addons/modplayer/Mod.gd create mode 100644 addons/modplayer/ModPlayer.gd create mode 100644 addons/modplayer/ModPlayer.tscn create mode 100644 addons/modplayer/XM.gd create mode 100644 addons/modplayer/icon.png create mode 100644 addons/modplayer/icon.png.import create mode 100644 addons/modplayer/plugin.cfg create mode 100644 addons/platformer_controller/._LICENSE create mode 100644 addons/platformer_controller/DemoScene.tscn create mode 100644 addons/platformer_controller/LICENSE create mode 100644 addons/platformer_controller/README.md create mode 100644 addons/platformer_controller/platformer_controller.gd diff --git a/addons/modplayer/GMP.gd b/addons/modplayer/GMP.gd new file mode 100644 index 0000000..cc4aefc --- /dev/null +++ b/addons/modplayer/GMP.gd @@ -0,0 +1,23 @@ +""" + Godot Mod Player Plugin by arlez80 (Yui Kinomoto) +""" + +tool +extends EditorPlugin + +func _enter_tree( ): + #self.add_custom_type( "GodotModPlayer", "Spatial", preload("ModPlayer.gd"), preload("icon.png") ) + pass + +func _exit_tree( ): + #self.remove_custom_type( "GodotModPlayer" ) + pass + +func has_main_screen(): + return true + +func make_visible( visible:bool ): + pass + +func get_plugin_name( ): + return "Godot Mod Player" diff --git a/addons/modplayer/Mod.gd b/addons/modplayer/Mod.gd new file mode 100644 index 0000000..059a540 --- /dev/null +++ b/addons/modplayer/Mod.gd @@ -0,0 +1,290 @@ +""" + MOD reader by あるる(きのもと 結衣) @arlez80 + + MIT License +""" + +class_name Mod + +const period_table:PoolIntArray = PoolIntArray([ + 1712,1616,1525,1440,1357,1281,1209,1141,1077,1017, 961, 907, + 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, + 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, + 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, + 107, 101, 95, 90, 85, 80, 76, 71, 67, 64, 60, 57, +]) + +enum ModFlags { + LINEAR_FREQUENCY_TABLE = 1, +} + +enum ModWaveFormType { + SINE_WAVEFORM = 0, + SAW_WAVEFORM = 1, + SQUARE_WAVEFORM = 2, + RAMDOM_WAVEFORM = 3, + REV_SAW_WAVEFORM = 4, +} + +enum ModLoopType { + NO_LOOP = 0, + FORWARD_LOOP = 1, + PING_PONG_LOOP = 2, +} + +class ModData: + var module_name:String + var tracker_name:String + var version:int + var flags:int = 0 + + var song_length:int + var unknown_number:int + var channel_count:int + var song_positions:Array # int + var magic:String + var patterns:Array # ModPattern[][] + var instruments:Array # ModInstrument[] + + var init_tick:int = 6 + var init_bpm:int = 125 + var restart_position:int = 0 + +class ModPatternNote: + var key_number:int + var note:int + var instrument:int + var volume:int = 0 + var effect_command:int + var effect_param:int + +class ModInstrument: + var name:String + var samples:Array # ModSample[96] + var volume_envelope:ModEnvelope + var panning_envelope:ModEnvelope + var vibrato_type:int = -1 # ModWaveFormType + var vibrato_speed:int = -1 + var vibrato_depth:int = -1 + var vibrato_depth_shift:int = -1 + var volume_fadeout:int = -1 + +class ModSample: + var data:PoolByteArray + var name:String + var length:int + var loop_start:int + var loop_length:int + var volume:int + var finetune:int + var loop_type:int = ModLoopType.FORWARD_LOOP # set(ModLoopType) + var bit:int = 8 # 8 or 16 + var panning:int = -1 + var relative_note:int = 0 + +class ModEnvelope: + var points:Array # ModEnvelopePoint[12] + var point_count:int + var sustain_point:int + var loop_start_point:int + var loop_end_point:int + + var enabled:bool + var sustain_enabled:bool + var loop_enabled:bool + + func set_flag( f:int ): + self.enabled = ( f & 1 ) != 0 + self.sustain_enabled = ( f & 2 ) != 0 + self.loop_enabled = ( f & 4 ) != 0 + +class ModEnvelopePoint: + var frame:int + var value:int + +class ModParseResult: + var error:int = OK + var data:ModData = null + + func _init( ): + pass + +""" + ファイルから読み込み + + @param path ファイルパス + @return 読んだ Modファイルデータ +""" +func read_file( path:String ) -> ModParseResult: + var f:File = File.new( ) + var result: = ModParseResult.new( ) + + var err:int = f.open( path, f.READ ) + if err != OK: + result.error = err + return result + + var stream:StreamPeerBuffer = StreamPeerBuffer.new( ) + stream.set_data_array( f.get_buffer( f.get_len( ) ) ) + stream.big_endian = true + f.close( ) + + result.data = self._read( stream ) + return result + +""" + 配列から読み込み + + @param data データ + @return ModData +""" +func read_data( data:PoolByteArray ) -> ModParseResult: + var stream:StreamPeerBuffer = StreamPeerBuffer.new( ) + stream.set_data_array( data ) + stream.big_endian = true + + var result: = ModParseResult.new( ) + result.data = self._read( stream ) + return result + +""" + ストリームから読み込み + + @param stream ストリーム + @return 読んだ Modファイルデータ +""" +func _read( stream:StreamPeerBuffer ) -> ModData: + var mod: = ModData.new( ) + mod.module_name = self._read_string( stream, 20 ) + mod.tracker_name = "" + mod.instruments = self._read_instruments( stream ) + mod.song_length = stream.get_u8( ) + mod.unknown_number = stream.get_u8( ) + mod.song_positions = stream.get_partial_data( 128 )[1] + var max_song_position:int = 0 + for sp in mod.song_positions: + if max_song_position < sp: + max_song_position = sp + + mod.magic = self._read_string( stream, 4 ) + var channel_count:int = 4 + match mod.magic: + "6CHN": + channel_count = 6 + "FLT8", "8CHN", "CD81", "OKTA": + channel_count = 8 + "16CN": + channel_count = 16 + "32CN": + channel_count = 32 + _: + if mod.magic.substr( 2 ) == "CH": + channel_count = int( mod.magic ) + # print( "Unknown magic" ) + # breakpoint + pass + mod.channel_count = channel_count + + mod.patterns = self._read_patterns( stream, max_song_position + 1, channel_count ) + self._read_sample_data( stream, mod.instruments ) + + return mod + +""" + 楽器データを読み込む + + @param stream ストリーム + @return 楽器データ +""" +func _read_instruments( stream:StreamPeerBuffer ) -> Array: # of ModSample + var instruments:Array = [] + + for i in range( 31 ): + var sample: = ModSample.new( ) + sample.name = self._read_string( stream, 22 ) + sample.length = stream.get_u16( ) * 2 + var finetune:int = stream.get_u8( ) & 0x0F + if 0x08 < finetune: + finetune = finetune - 0x10 + sample.finetune = finetune * 16 + sample.volume = stream.get_u8( ) + sample.loop_start = stream.get_u16( ) * 2 + sample.loop_length = stream.get_u16( ) * 2 + if sample.loop_length < 8: + sample.loop_type = ModLoopType.NO_LOOP + else: + sample.loop_type = ModLoopType.FORWARD_LOOP + + var instrument: = ModInstrument.new( ) + instrument.name = sample.name + instrument.samples = [] + for k in range( 96 ): + instrument.samples.append( sample ) + instrument.volume_envelope = ModEnvelope.new( ) + instrument.volume_envelope.enabled = false + instrument.panning_envelope = ModEnvelope.new( ) + instrument.panning_envelope.enabled = false + + instruments.append( instrument ) + + return instruments + +""" + パターンを読み込む + + @param stream ストリームデータ + @param max_positions 最大パターン数 + @param channels 最大チャンネル数 + @return パターンデータ +""" +func _read_patterns( stream:StreamPeerBuffer, max_position:int, channels:int ) -> Array: + var patterns:Array = [] + + for position in range( 0, max_position ): + var pattern:Array = [] + for i in range( 0, 64 ): + var line:Array = [] + for ch in range( 0, channels ): + var v1:int = stream.get_u16( ) + var v2:int = stream.get_u16( ) + var mod_pattern_note: = ModPatternNote.new( ) + mod_pattern_note.instrument = ( ( v1 >> 8 ) & 0xF0 ) | ( ( v2 >> 12 ) & 0x0F ) + mod_pattern_note.key_number = v1 & 0x0FFF + mod_pattern_note.effect_command = ( v2 >> 8 ) & 0x0F + mod_pattern_note.effect_param = v2 & 0x0FF + # TODO: 遅かったら二分探索にでもしませんか + if 0 < mod_pattern_note.key_number: + mod_pattern_note.note = 23 + for k in period_table: + mod_pattern_note.note += 1 + if k <= mod_pattern_note.key_number: + break + else: + mod_pattern_note.note = 0 + line.append( mod_pattern_note ) + pattern.append( line ) + patterns.append( pattern ) + + return patterns + +""" + 楽器のデータに基づき、波形を読み込む + + @param stream ストリーム + @param instruments 波形を読み込む楽器データリスト +""" +func _read_sample_data( stream:StreamPeerBuffer, instruments:Array ) -> void: + for instrument in instruments: + var sample:Mod.ModSample = instrument.samples[0] + if 0 < sample.length: + sample.data = stream.get_partial_data( sample.length )[1] + +""" + 文字列の読み込み + + @param stream ストリーム + @param size 文字列サイズ + @return 読み込んだ文字列を返す +""" +func _read_string( stream:StreamPeerBuffer, size:int ) -> String: + return stream.get_partial_data( size )[1].get_string_from_ascii( ) diff --git a/addons/modplayer/ModPlayer.gd b/addons/modplayer/ModPlayer.gd new file mode 100644 index 0000000..55b049b --- /dev/null +++ b/addons/modplayer/ModPlayer.gd @@ -0,0 +1,1053 @@ +extends Node + +""" + 100% pure GDScript software Mod Player [Godot Mod Player] + by あるる(きのもと 結衣) @arlez80 + + MIT License +""" + +class_name ModPlayer, "icon.png" + +# ------------------------------------------------------- +# 定数 +const mod_master_bus_name:String = "arlez80_GModP_MASTER_BUS" +const mod_channel_bus_name:String = "arlez80_GModP_CHANNEL_BUS%d" +const default_mix_rate:int = 4144 +const default_linear_mix_rate:int = 8363 +const chip_speed:float = 50.0 +const volume_table:PoolRealArray = PoolRealArray([ + -144.0,-36.1,-30.1,-26.6,-24.1,-22.1,-20.6,-19.2,-18.1,-17,-16.1,-15.3,-14.5,-13.8,-13.2,-12.6,-12,-11.5,-11,-10.5,-10.1,-9.7,-9.3,-8.9,-8.5,-8.2,-7.8,-7.5,-7.2,-6.9,-6.6,-6.3,-6,-5.8,-5.5,-5.2,-5,-4.8,-4.5,-4.3,-4.1,-3.9,-3.7,-3.5,-3.3,-3.1,-2.9,-2.7,-2.5,-2.3,-2.1,-2,-1.8,-1.6,-1.5,-1.3,-1.2,-1,-0.9,-0.7,-0.6,-0.4,-0.3,-0.1,0.0 +]) + +# ----------------------------------------------------------------------------- +# Signals + +signal note_on( channel_number, note ) +signal looped + +# ----------------------------------------------------------------------------- +# Classes +class GodotModPlayerChannelAudioEffect: + var ae_panner:AudioEffectPanner = null + +class GodotModPlayerInstrument: + var source:Object # Mod.ModInstrument + var array_ass:Array # AudioStreamSample[] + +class GodotModPlayerPitch: + const center_key_freq:float = 856.0 + + var value:int # 現在値 + var dest:int # 変化目標(ポルタメント用) + var speed:int # 変化量(ポルタメント用) + + var arpeggio:Array # アルペジオリスト + var arpeggio_count:int # アルペジオカウンタ + var arpeggio_enabled:bool + + var linear_freq:bool = false + + func _init( ): + self.value = 0 + self.dest = 0 + self.speed = 0 + self.arpeggio = [0,0,0] + self.arpeggio_count = 0 + self.arpeggio_enabled = false + + func update( ): + if self.dest < self.value: + self.value -= self.speed + if self.value < self.dest: + self.value = self.dest + self.speed = 0 + elif self.value < self.dest: + self.value += self.speed + if self.dest < self.value: + self.value = self.dest + self.speed = 0 + + """ + 現在のピッチスケールを得る + + @param with 加算値 + @return 計算したピッチスケール + """ + func get_pitch_scale( with:int = 0 ) -> float: + var v:int = self.value + with + + if self.arpeggio_enabled: + v += self.arpeggio[self.arpeggio_count] + self.arpeggio_count = ( self.arpeggio_count + 1 ) % 3 + + if self.linear_freq: + return pow( 2.0, ( 4608 - v ) / 768.0 ) + + if v == 0: + return 0.0001 + + return center_key_freq / v + + """ + ファインピッチスケールを取得 + + @return 現在のファインピッチスケール + """ + func get_fine_pitch_scale( ) -> float: + return pow( 2.0, ( self.value / 128.0 ) / 12.0 ) + +class GodotModPlayerEffect: + var type:int = 0 + var phase:int = 0 + var speed:int = 0 + var depth:int = 0 + var depth_shift:int = 0 + + var value:int = 0 + + """ + 更新 + """ + func update( ) -> void: + var v:int = 0 + match self.type: + Mod.ModWaveFormType.SINE_WAVEFORM: # 正弦波 + # TODO: そのうちPoolByteArrayのtableにでもしておく + v = int( sin( self.phase * ( PI / 32.0 ) ) * 255.0 ) + Mod.ModWaveFormType.SAW_WAVEFORM: # のこぎり + v = 255 - ( ( self.phase + 0x20 ) & 0x3F ) * 8 + Mod.ModWaveFormType.SQUARE_WAVEFORM: # 矩形波 + v = 255 - ( self.phase & 0x20 ) * 16 + Mod.ModWaveFormType.RAMDOM_WAVEFORM: # 乱数 + v = self.rng.randi( 511 ) - 255 + Mod.ModWaveFormType.REV_SAW_WAVEFORM: # 逆のこぎり + v = ( ( self.phase + 0x20 ) & 0x3F ) * 8 + + self.phase += self.speed + + self.value = ( v * self.depth ) >> self.depth_shift + +class GodotModPlayerEnvelope: + const Mod = preload( "Mod.gd" ) + + var source:Mod.ModEnvelope + + var frame:int = 0 + var value:int = 0 + var init_value:int = 0 + var sustain:bool = false + var enabled:bool = false + + """ + ノートオン + + @param _source エンベロープデータ + """ + func note_on( _source:Mod.ModEnvelope ) -> void: + self.source = _source + self.sustain = true + self.frame = 0 + if self.source != null and self.source.enabled: + self.value = self.source.points[0].value + else: + self.value = self.init_value + + """ + ノートオフ + """ + func note_off( ) -> void: + self.sustain = false + + """ + 更新 + """ + func update( ) -> void: + if self.source == null: + self.value = self.init_value + return + + self.enabled = self.source.enabled + + if not self.enabled: + self.value = self.init_value + return + + var current_point:int = 0 + var prev_sum_frame:int = 0 + var sum_frame:int = 0 + var loop_start_frame:int = 0 + for i in range( 1, self.source.point_count ): + sum_frame += self.source.points[i].frame + if self.source.loop_start_point == i: + loop_start_frame = sum_frame + if self.frame < sum_frame: + var t:float = float( self.frame - prev_sum_frame ) / float( sum_frame - prev_sum_frame ) + var s:float = 1.0 - t + self.value = int( self.source.points[i-1].value * s + self.source.points[i].value * t ) + break + prev_sum_frame = sum_frame + current_point = i + + if self.source.sustain_enabled and self.sustain and self.source.sustain_point == current_point: + pass + elif self.source.loop_enabled and self.source.loop_end_point == current_point: + self.frame = loop_start_frame + elif current_point == self.source.point_count and sum_frame < self.frame: + self.frame = sum_frame + else: + self.frame += 1 + +class GodotModPlayerChannelStatus: + const head_silent_second:float = 1.0 + const gap_second:float = 44100.0 / 1024.0 / 1000.0 + + var source_inst:Object # Mod.ModInstrument + var source_sample:Object # Mod.ModSample + var asps:Array # AudioStreamPlayer[] + var asp_switcher:int + + var channel_number:int + var last_instrument:int = -1 + var last_key_number:int = -1 + var mute:bool + var pitch:GodotModPlayerPitch + var fine_pitch:GodotModPlayerPitch + var relative_pitch:float = 1.0 + + var sample_number:int + var volume:int + var panning:int + var amplified:int + var fx_count:int + var vibrato:GodotModPlayerEffect + var tremolo:GodotModPlayerEffect + + var volume_env:GodotModPlayerEnvelope + var panning_env:GodotModPlayerEnvelope + + var force_note_off:bool = false + + """ + コンストラクタ + + @param _channel_number チャンネル番号 + @param linear_freq 周波数指定が線形か? + """ + func _init( _channel_number:int, linear_freq:bool ): + self.mute = false + self.force_note_off = false + + self.channel_number = _channel_number + self.panning = 128 + + for i in range( 2 ): + var asp: = AudioStreamPlayer.new( ) + asp.bus = mod_channel_bus_name % self.channel_number + self.asps.append( asp ) + self.asp_switcher = 0 + + self.pitch = GodotModPlayerPitch.new( ) + self.fine_pitch = GodotModPlayerPitch.new( ) + self.pitch.linear_freq = linear_freq + self.fine_pitch.linear_freq = linear_freq + self.vibrato = GodotModPlayerEffect.new( ) + self.vibrato.depth_shift = 7 + self.tremolo = GodotModPlayerEffect.new( ) + self.tremolo.depth_shift = 6 + self.volume_env = GodotModPlayerEnvelope.new( ) + self.volume_env.init_value = 64 + self.panning_env = GodotModPlayerEnvelope.new( ) + self.panning_env.init_value = 32 + + """ + Tick更新 + """ + func tick_update( ) -> void: + self.volume_env.update( ) + self.panning_env.update( ) + + """ + 更新 + """ + func update( ) -> void: + if self.mute: + for asp in self.asps: + if asp.is_playing( ): + asp.stop( ) + return + + for i in range( self.asps.size( ) ): + var asp:AudioStreamPlayer = self.asps[i] + if self.asp_switcher == i and ( not self.force_note_off ): + if asp.is_playing( ): + asp.volume_db = self.get_volume_db( ) + asp.pitch_scale = self.get_pitch_scale( ) + else: + if asp.is_playing( ): + asp.volume_db -= 4.0 + if asp.volume_db < -100.0: + asp.stop( ) + + """ + volume_dbを取得 + + @return 現在のvolume_dbを返す + """ + func get_volume_db( ) -> float: + var v:float = ( clamp( self.volume + self.tremolo.value, 0.0, 64.0 ) / 64.0 ) * ( self.volume_env.value / 64.0 ) + return volume_table[int(v * 64)] + + """ + ピッチスケールを取得 + + @return 現在のピッチスケールを返す + """ + func get_pitch_scale( ) -> float: + return self.pitch.get_pitch_scale( self.vibrato.value ) * self.relative_pitch * self.fine_pitch.get_fine_pitch_scale( ) + + """ + ノートオフ + """ + func note_off( ) -> void: + self.volume_env.note_off( ) + self.panning_env.note_off( ) + + """ + ノートオン + + @param inst 楽器データ + @param sample_number サンプル番号 + @param effect_command エフェクトコマンド + @param key_number 周波数 + @param note ノート番号 + """ + func note_on( inst:GodotModPlayerInstrument, sample_number:int, effect_command:int, key_number:int, note:int ) -> void: + self.last_key_number = key_number + + if self.mute: + return + + self.force_note_off = false + self.asp_switcher = ( self.asp_switcher + 1 ) % self.asps.size( ) + + self.source_inst = inst.source + self.source_sample = self.source_inst.samples[note-1] + if self.source_sample.panning != -1: + self.panning = self.source_sample.panning + + var asp:AudioStreamPlayer = self.asps[self.asp_switcher] + asp.stop( ) + asp.stream = inst.array_ass[note-1] + self.volume = self.source_sample.volume + self.sample_number = sample_number + if effect_command == 0x03 || effect_command == 0x05: + self.pitch.dest = key_number + else: + self.pitch.value = key_number + self.fine_pitch.value = self.source_sample.finetune + self.relative_pitch = pow( 2.0, self.source_sample.relative_note / 12.0 ) + if self.source_inst.vibrato_type != -1: + self.vibrato.type = self.source_inst.vibrato_type + self.vibrato.speed = self.source_inst.vibrato_speed + self.vibrato.depth = self.source_inst.vibrato_depth + self.vibrato.depth_shift = self.source_inst.vibrato_depth_shift + + self.volume_env.note_on( self.source_inst.volume_envelope ) + self.panning_env.note_on( self.source_inst.panning_envelope ) + + asp.volume_db = self.get_volume_db( ) + var pitch_scale:float = self.get_pitch_scale( ) + asp.pitch_scale = pitch_scale + asp.play( max( 0.0, self.head_silent_second - clamp( self.gap_second - AudioServer.get_time_to_next_mix( ), 0.0, self.gap_second ) * pitch_scale ) ) + +# ----------------------------------------------------------------------------- +# Export + +# ファイル +export (String, FILE, "*.mod,*.xm") var file:String = "" setget set_file +# 再生中か? +export (bool) var playing:bool = false +# 音量 +export (float, -144.0, 0.0) var volume_db:float = -20.0 setget set_volume_db +# キーシフト(未実装) +#export (int) var key_shift:int = 0 +# ループフラグ +export (bool) var loop:bool = false +# mix_target same as AudioStreamPlayer's one +export (int, "MIX_TARGET_STEREO", "MIX_TARGET_SURROUND", "MIX_TARGET_CENTER") var mix_target:int = AudioStreamPlayer.MIX_TARGET_STEREO +# bus same as AudioStreamPlayer's one +export (String) var bus:String = "Master" +# リリースタイムの発音軽減を与えてブツ切り回避する +#export (bool) var append_release_time:bool = true +# 1秒間処理する回数 +export (int, 60, 480) var sequence_per_seconds:int = 120 +# スレッドでの動作にするか +export (bool) var no_thread_mode:bool = false + +# ----------------------------------------------------------------------------- +# 変数 + +# Mod Playerスレッド +var thread:Thread = null +var mutex:Mutex = Mutex.new() +var thread_delete:bool = false +# Modデータ +var mod_data:Mod.ModData = null setget set_mod_data +# テンポ +var tempo:float = 125.0 setget set_tempo +# 行毎秒 +var row_per_second:float = 0.02 +# tick毎行 +var tick_per_row:int = 4 +# tick毎秒 +var tick_per_second:float = 1.0 / chip_speed +# 次の行への秒数 +var next_row_remain_second:float = 0.0 +# 次のtickへの秒数 +var next_tick_remain_second:float = 0.0 +# 追加tick +var extra_tick:int = 0 +# tick処理済み回数 +var processed_tick_count:int = 0 +# 位置(秒) +var position:float = 0.0 +# 曲位置 +var song_position:int = 0 +# パターン内位置 +var pattern_position:int = 0 +# 次の行 +var pattern_position_on_next_row:int = 0 +# 楽器 +var instruments:Array = [] +# チャンネル +var channel_status:Array = [] +# Modチャンネルエフェクト +var channel_audio_effects:Array = [] +# パターンジャンプ先 +var pattern_position_jump_point:int = 0 +# パターンループカウンタ +var pattern_loop_count:int = 0 +# パターンループ先 +var pattern_loop_origin:int = 0 +# 乱数 +var rng:RandomNumberGenerator +# グローバル音量コマンドを有効にするか? +var enable_global_volume_command:bool = true +# グローバル音量 (mod/xm指定の数字) +var global_volume:int = 64 +# グローバル音量 (計算用db) +var global_volume_db:float = 0.0 + +""" + 準備 +""" +func _ready( ): + # HTML5 もしくは デバッグモード時には強制的にスレッド未使用モードに変更する + if OS.get_name( ) == "HTML5" or OS.is_debug_build( ): + self.no_thread_mode = true + + self.rng = RandomNumberGenerator.new( ) + + if AudioServer.get_bus_index( self.mod_master_bus_name ) == -1: + AudioServer.add_bus( -1 ) + var mod_master_bus_idx:int = AudioServer.get_bus_count( ) - 1 + AudioServer.set_bus_name( mod_master_bus_idx, self.mod_master_bus_name ) + AudioServer.set_bus_send( mod_master_bus_idx, self.bus ) + AudioServer.set_bus_volume_db( AudioServer.get_bus_index( self.mod_master_bus_name ), self.volume_db ) + + for i in range( 32 ): + AudioServer.add_bus( -1 ) + var mod_channel_bus_idx:int = AudioServer.get_bus_count( ) - 1 + AudioServer.set_bus_name( mod_channel_bus_idx, self.mod_channel_bus_name % i ) + AudioServer.set_bus_send( mod_channel_bus_idx, self.mod_master_bus_name ) + AudioServer.set_bus_volume_db( mod_channel_bus_idx, 0.0 ) + + var cae: = GodotModPlayerChannelAudioEffect.new( ) + cae.ae_panner = AudioEffectPanner.new( ) + AudioServer.add_bus_effect( mod_channel_bus_idx, cae.ae_panner ) + self.channel_audio_effects.append( cae ) + + if self.playing: + self.play( ) + +""" + 通知 + + @param what 通知要因 +""" +func _notification( what:int ): + # 破棄時 + if what == NOTIFICATION_PREDELETE: + self.thread_delete = true + if self.thread != null: + self.thread.wait_to_finish( ) + self.thread = null + #AudioServer.remove_bus( AudioServer.get_bus_index( self.mod_master_bus_name ) ) + #for i in range( 0, 16 ): + # AudioServer.remove_bus( AudioServer.get_bus_index( self.midi_channel_bus_name % i ) ) + +""" + Mutex Lock (for debug purpose) +""" +func _lock( callee:String ) -> void: + # print( "locked by %s" % callee ) + self.mutex.lock( ) + +""" + Mutex Unload (for debug purpose) +""" +func _unlock( callee:String ) -> void: + # print( "unlocked by %s" % callee ) + self.mutex.unlock( ) + +""" + 再生前の初期化 +""" +func _prepare_to_play( ) -> void: + self._lock( "prepare_to_play" ) + + # ファイル読み込み + if self.mod_data == null: + match self.file.get_extension( ): + "mod": + var mod_reader: = Mod.new( ) + self.mod_data = mod_reader.read_file( self.file ).data + "xm": + var xm_reader: = XM.new( ) + var m:Object = xm_reader.read_file( self.file ).data + self.mod_data = m + _: + self.mod_data = null + + if self.mod_data == null: + self._unlock( "prepare_to_play" ) + self.stop( ) + return + + if self.channel_status != null: + for t in self.channel_status: + for asp in t.asps: + self.remove_child( asp ) + + self.set_volume_db( self.volume_db ) + + self.instruments = [] + var temp_head_silent:Array = [] + var head_silent_samples:int = default_mix_rate + if self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0: + head_silent_samples = default_linear_mix_rate + for i in range( head_silent_samples ): + temp_head_silent.append( 0 ) + var head_silent:PoolByteArray = PoolByteArray( temp_head_silent ) + var loaded:Dictionary = {} + for t in self.mod_data.instruments: + var inst:GodotModPlayerInstrument = GodotModPlayerInstrument.new( ) + + inst.source = t + inst.array_ass = [] + + for sample in t.samples: + var id:int = sample.get_instance_id( ) + var ass:AudioStreamSample = null + if not( id in loaded ): + ass = AudioStreamSample.new( ) + ass.stereo = false + if self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0: + ass.mix_rate = self.default_linear_mix_rate + else: + ass.mix_rate = self.default_mix_rate + if sample.bit == 16: + ass.data = head_silent + head_silent + sample.data + ass.format = AudioStreamSample.FORMAT_16_BITS + else: + ass.data = head_silent + sample.data + ass.format = AudioStreamSample.FORMAT_8_BITS + ass.loop_begin = sample.loop_start + head_silent_samples + ass.loop_end = ass.loop_begin + sample.loop_length + if sample.bit == 16: + ass.loop_begin /= 2 + ass.loop_end /= 2 + ass.loop_mode = AudioStreamSample.LOOP_DISABLED + if sample.loop_type & Mod.ModLoopType.FORWARD_LOOP != 0: + ass.loop_mode = AudioStreamSample.LOOP_FORWARD + elif sample.loop_type & Mod.ModLoopType.PING_PONG_LOOP != 0: + ass.loop_mode = AudioStreamSample.LOOP_PING_PONG + loaded[id] = ass + loaded[id] = ass + else: + ass = loaded[id] + inst.array_ass.append( ass ) + + self.instruments.append( inst ) + for k in loaded.keys( ): + loaded.erase( k ) + + self.channel_status = [] + for i in range( self.mod_data.channel_count ): + var cs: = GodotModPlayerChannelStatus.new( i, self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0 ) + if 4 < self.mod_data.channel_count: + cs.panning = 128 + else: + cs.panning = [64,192,192,64][i] + for asp in cs.asps: + self.add_child( asp ) + self.channel_status.append( cs ) + + self._unlock( "prepare_to_play" ) + +""" + 再生 + + @param from_position 再生開始位置(現在未実装) +""" +func play( from_position:float = 0.0 ): + self._prepare_to_play( ) + if self.mod_data == null: + return + + self.playing = true + if from_position == 0.0: + self.position = 0.0 + self.song_position = 0 + self.pattern_position_jump_point = 1 + self.pattern_position = -1 + self.pattern_position_on_next_row = -1 + self.pattern_loop_count = 0 + self.pattern_loop_origin = 0 + self.tick_per_second = 1.0 / self.chip_speed + self.set_tempo( self.mod_data.init_bpm ) + self.set_tick( self.mod_data.init_tick ) + self.processed_tick_count = 10000 + self.next_tick_remain_second = 0.0 + self.next_row_remain_second = self.row_per_second + else: + self.seek( from_position ) + +""" + シーク + TODO:未実装 + + @param to_position 再生位置 +""" +func seek( to_position:float ) -> void: + self._lock( "seek" ) + self._previous_time = 0.0 + self._stop_all_notes( ) + self._unlock( "seek" ) + +""" + 停止 +""" +func stop( ) -> void: + self._lock( "stop" ) + self._stop_all_notes( ) + self.playing = false + self._unlock( "stop" ) + +""" + ファイル変更 + + @param path ファイルパス +""" +func set_file( path:String ) -> void: + file = path + self.mod_data = null + if self.playing: + self.play( ) + +""" + Modデータ変更 + + @param md Modデータ +""" +func set_mod_data( md:Mod.ModData ) -> void: + mod_data = md + +""" + 音量設定 + + @param vdb 音量 +""" +func set_volume_db( vdb:float ) -> void: + var master_bus_id:int = AudioServer.get_bus_index( self.mod_master_bus_name ) + volume_db = vdb + if master_bus_id == -1: + return + + var gvdb:float = self.global_volume_db if self.enable_global_volume_command else 0.0 + AudioServer.set_bus_volume_db( master_bus_id, volume_db + gvdb ) + +""" + 全音を止める +""" +func _stop_all_notes( ) -> void: + for t in self.channel_status: + t.note_off( ) + t.force_note_off = true + for asp in t.asps: + if asp.is_playing( ): + asp.stop( ) + +""" + テンポ設定 + + @param _tempo テンポ +""" +func set_tempo( _tempo:float ) -> void: + tempo = _tempo + self.tick_per_second = 1.0 / ( self.chip_speed * ( _tempo / 125.0 ) ) + self.next_row_remain_second -= self.row_per_second + self.row_per_second = self.tick_per_row * self.tick_per_second + self.next_row_remain_second += self.row_per_second + +""" + tickからテンポ設定 + + @param _tick Tick数 +""" +func set_tick( _tick:int ) -> void: + self.tick_per_row = _tick + self.next_row_remain_second -= self.row_per_second + self.row_per_second = self.tick_per_row * self.tick_per_second + self.next_row_remain_second += self.row_per_second + +""" + 1フレームでの処理 + + @param delta +""" +func _process( delta:float ): + if self.no_thread_mode: + self._sequence( delta ) + else: + if self.thread == null or ( not self.thread.is_alive( ) ): + self._lock( "_process" ) + if self.thread != null: + self.thread.wait_to_finish( ) + self.thread = Thread.new( ) + self.thread.start( self, "_thread_process" ) + self._unlock( "_process" ) + +""" + スレッド処理 +""" +func _thread_process( ) -> void: + var last_time:int = OS.get_ticks_usec( ) + + while not self.thread_delete: + self._lock( "_thread_process" ) + + var current_time:int = OS.get_ticks_usec( ) + var delta:float = ( current_time - last_time ) / 1000000.0 + self._sequence( delta ) + + self._unlock( "_thread_process" ) + + last_time = current_time + var msec:int = int( 1000.0 / self.sequence_per_seconds ) + OS.delay_msec( msec ) + +""" + シーケンス処理を行う + + @param delta 前回からの経過時間 sec +""" +func _sequence( delta:float ) -> void: + if delta < 0.0: + return + + if self.mod_data != null: + if self.playing: + self.position += delta + self.next_row_remain_second -= delta + self.next_tick_remain_second -= delta + if self.next_row_remain_second <= 0.0: + self.next_tick_remain_second = -INF + while self.next_tick_remain_second <= 0.0 and self.processed_tick_count < self.tick_per_row + self.extra_tick: + self._process_tick( ) + self.next_tick_remain_second += self.tick_per_second + self._process_row( ) + self.processed_tick_count += 1 + if self.next_row_remain_second <= 0.0: + self.extra_tick = 0 + self._process_move_to_next_line( ) + self.next_row_remain_second = self.row_per_second + self.next_row_remain_second + self.next_tick_remain_second = 0.0 + self.processed_tick_count = 1 + self._process_update_audio_effects( ) + +""" + 次の行に移行する +""" +func _process_move_to_next_line( ) -> void: + if 0 <= self.pattern_position_on_next_row: + self.pattern_position = self.pattern_position_on_next_row + self.pattern_position_on_next_row = -1 + self.song_position = self.pattern_position_jump_point + self.pattern_position_jump_point = self.song_position + 1 + else: + self.pattern_position += 1 + if len( self.mod_data.patterns[self.mod_data.song_positions[self.song_position]] ) <= self.pattern_position: + self.pattern_position = 0 + self.song_position = self.pattern_position_jump_point + self.pattern_position_jump_point = self.song_position + 1 + + if self.mod_data.song_length <= self.song_position: + if self.loop: + self.song_position = self.mod_data.restart_position + self.pattern_position_jump_point = self.song_position + 1 + self.emit_signal( "looped" ) + else: + self.song_position = 0 + self.stop( ) + return + +""" + 1行処理 +""" +func _process_row( ) -> void: + var pattern_line:Array = self.mod_data.patterns[self.mod_data.song_positions[self.song_position]][self.pattern_position] + for channel in self.channel_status: + self._process_row_for_channel( channel, pattern_line[channel.channel_number] ) + +""" + チャンネルごとの1行処理 + + @param channel チャンネルデータ + @param note ノートデータ +""" +func _process_row_for_channel( channel:GodotModPlayerChannelStatus, note:Mod.ModPatternNote ) -> void: + #printt( channel.channel_number, pattern_node.sample_number, pattern_node.key_number, pattern_node.effect_command ) + + var note_on:bool = self.processed_tick_count == 1 + # Note関係のエフェクトコマンド + if note.effect_command == 0x0E: + match ( note.effect_param >> 4 ): + 0x09: # Retrigger Note + note_on = ( ( self.processed_tick_count - 1 ) % ( note.effect_param & 0x0F ) ) == 0 + 0x0D: # Note Delay + note_on = ( note.effect_param & 0x0F ) == self.processed_tick_count - 1 + + # 楽器設定 + if note.instrument != 0: + channel.last_instrument = note.instrument - 1 + # 発音 + if note_on: + channel.pitch.arpeggio_count = 0 + if note.note == 96: + channel.note_off( ) + elif 0 < note.key_number: + if 0 <= channel.last_instrument and channel.last_instrument < self.instruments.size( ): + var inst:GodotModPlayerInstrument = self.instruments[channel.last_instrument] + channel.note_on( inst, note.instrument, note.effect_command, note.key_number, note.note ) + + self.emit_signal( "note_on", channel.channel_number, note ) + elif note.instrument != 0: + if 0 <= channel.last_instrument and channel.last_instrument < self.instruments.size( ) and 0 <= channel.last_key_number: + var inst:GodotModPlayerInstrument = self.instruments[channel.last_instrument] + channel.note_on( inst, note.instrument, note.effect_command, channel.last_key_number, note.note ) + + self.emit_signal( "note_on", channel.channel_number, note ) + + if self.processed_tick_count == 1: + self._process_tick_for_channel( channel, note, true ) + +""" + 1tick処理 +""" +func _process_tick( ) -> void: + if self.pattern_position < 0: + return + + var pattern_line:Array = self.mod_data.patterns[self.mod_data.song_positions[self.song_position]][self.pattern_position] + for channel in self.channel_status: + self._process_tick_for_channel( channel, pattern_line[channel.channel_number], false ) + +""" + チャンネルごとの1tick処理 + + @param channel チャンネルデータ + @param note ノートデータ + @param disable_channel_row チャンネルイベントを無視する +""" +func _process_tick_for_channel( channel:GodotModPlayerChannelStatus, note:Mod.ModPatternNote, disable_channel_row:bool ) -> void: + #print( "%08x %08x" % [ note.effect_command, note.effect_param ] ) + + channel.pitch.arpeggio_enabled = false + channel.vibrato.value = 0 + channel.tremolo.value = 0 + + channel.tick_update( ) + + if 0x10 <= note.volume and note.volume <= 0x50: + channel.volume = note.volume - 0x10 + elif 0x60 <= note.volume: + match note.volume >> 4: + 0x06: # Volume slide down + if not disable_channel_row: + channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) ) + 0x07: # Volume slide up + if not disable_channel_row: + channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) ) + 0x08: # Fine volume slide down + if not disable_channel_row: + channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) ) + 0x09: # Fine volume slide up + if not disable_channel_row: + channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) ) + 0x0A: # Set vibrato speed + channel.vibrato.speed = (channel.vibrato.speed & 0x0F) | ( ( note.volume & 0x0F ) << 4 ); + 0x0C: # Panning + var p:int = note.volume & 0x0F + p |= p << 4 + channel.panning = p + 0x0D: # Panning slide left + if not disable_channel_row: + channel.panning = int( clamp( channel.panning - ( note.panning & 0x0F ), 0, 255 ) ) + 0x0E: # Panning slide right + if not disable_channel_row: + channel.panning = int( clamp( channel.panning + ( note.panning & 0x0F ), 0, 255 ) ) + 0x0F: # Tone portamento + if 0 < note.volume & 0x0F: + var p:int = note.volume & 0x0F + p |= p << 4 + channel.pitch.speed = p + _: + printerr( "unknown volume effect command: %02x" % note.volume ) + + # エフェクトコマンド + match note.effect_command: + 0x00: # Arpeggio + channel.pitch.arpeggio[1] = int( channel.pitch.value / pow( 2.0, ( note.effect_param >> 4 ) / 12.0 ) ) - channel.pitch.value + channel.pitch.arpeggio[2] = int( channel.pitch.value / pow( 2.0, ( note.effect_param & 0x0F ) / 12.0 ) ) - channel.pitch.value + channel.pitch.arpeggio_enabled = true + 0x01: # Portament up + if not disable_channel_row: + channel.pitch.value = int( max( channel.pitch.value - note.effect_param, 0 ) ) + 0x02: # Portament down + if not disable_channel_row: + channel.pitch.value = channel.pitch.value + note.effect_param + 0x03: # Portament speed + channel.pitch.speed = note.effect_param + channel.pitch.update( ) + 0x04: # Vibrato + if 0 < note.effect_param & 0xF0: + channel.vibrato.speed = note.effect_param >> 4 + if 0 < note.effect_param & 0x0F: + channel.vibrato.depth = note.effect_param & 0x0F + channel.vibrato.update( ) + 0x05: # Portament + Volume slide + channel.pitch.update( ) + channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) ) + 0x06: # Vibrato + Volume slide + channel.vibrato.update( ) + channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) ) + 0x07: # Tremolo + if 0 < note.effect_param & 0xF0: + channel.tremolo.speed = note.effect_param >> 4 + if 0 < note.effect_param & 0x0F: + channel.tremolo.depth = note.effect_param & 0x0F + channel.tremolo.update( ) + 0x08: # Panning + channel.panning = int( clamp( note.effect_param * 2, 0, 255 ) ) + 0x09: # Sample offset + printerr( "not implemented: 9xx Sample offset" ) + 0x0A: # Volume slide + if not disable_channel_row: + channel.volume = int( clamp( channel.volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) ) + 0x0B: # Pattern jump + if disable_channel_row: + self.pattern_position_jump_point = note.effect_param + self.pattern_position_on_next_row = 0 + 0x0C: # Volume + channel.volume = int( clamp( note.effect_param, 0, 64 ) ) + 0x0D: # Pattern break + if disable_channel_row: + self.pattern_position_jump_point = self.song_position + 1 + self.pattern_position_on_next_row = ( note.effect_param >> 4 ) * 10 + ( note.effect_param & 0x0F ) + if 64 <= self.pattern_position_on_next_row: + self.pattern_position_on_next_row = 0 + 0x0E: # 拡張コマンド + match ( note.effect_param >> 4 ): + 0x01: # Fine portamento up + if 0 < note.effect_param & 0x0F: + channel.fine_pitch.speed = note.effect_param & 0x0F + channel.fine_pitch.update( ) + 0x02: # Fine portamento down + if 0 < note.effect_param & 0x0F: + channel.fine_pitch.speed = - (note.effect_param & 0x0F) + channel.fine_pitch.update( ) + 0x04: # Vibrato type + channel.vibrato.type = note.effect_param + channel.vibrato.update( ) + 0x06: # Pattern loop + if disable_channel_row: + if 0 < ( note.effect_param & 0x0F ): + if ( note.effect_param & 0x0F ) == self.pattern_loop_count: + self.pattern_loop_count = 0 + else: + self.pattern_loop_count += 1 + self.pattern_position_jump_point = self.song_position + self.pattern_position_on_next_row = self.pattern_loop_origin + else: + self.pattern_loop_origin = self.pattern_position + 0x07: # Tremoro type + channel.tremolo.type = note.effect_param + channel.tremolo.update( ) + 0x09: # Retrigger Note + pass + 0x0A: # Fine volume slide up + if not disable_channel_row: + channel.volume = int( clamp( channel.volume + ( note.volume & 0x0F ), 0, 64 ) ) + 0x0B: # Fine volume slide down + if not disable_channel_row: + channel.volume = int( clamp( channel.volume - ( note.volume & 0x0F ), 0, 64 ) ) + 0x0C: # Note cut + if self.processed_tick_count - 1 == note.effect_param & 0x0F: + channel.volume = 0 + 0x0D: # Note delay (上で処理する) + pass + 0x0E: # Pattern delay + var r:int = ( note.effect_param & 0x0F ) + self.extra_tick = r * self.tick_per_row + if disable_channel_row: + self.next_row_remain_second += self.row_per_second * r + _: + printerr( "unknown extended command: %04x" % [ note.effect_param ] ) + 0x0F: # Tick / Tempo + if disable_channel_row: + if note.effect_param < 0x20: + self.set_tick( note.effect_param ) + else: + self.set_tempo( note.effect_param ) + 0x10: # Global volume + self.global_volume = int( clamp( note.effect_param, 0, 64 ) ) + self.global_volume_db = self.volume_table[self.global_volume] + self.set_volume_db( self.volume_db ) + 0x11: # Global volume slide + self.global_volume = int( clamp( self.global_volume + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 64 ) ) + self.global_volume_db = self.volume_table[self.global_volume] + self.set_volume_db( self.volume_db ) + 0x14: # Key off + printerr( "not implemented: Kxx Key off" ) + 0x15: # Set envelope position + channel.tremolo.phase = note.effect_param + 0x19: # Pannning slide + if not disable_channel_row: + channel.panning = int( clamp( channel.panning + ( note.effect_param >> 4 ) - ( note.effect_param & 0x0F ), 0, 255 ) ) + 0x1B: # Multi retrig note + printerr( "not implemented: Rxy Multi retrig note" ) + 0x1D: # Tremor + printerr( "not implemented: Txy Tremor" ) + _: + printerr( "unknown command: %02x : %04x" % [ note.effect_command, note.effect_param ] ) + +""" + Godotのオーディオエフェクト更新 +""" +func _process_update_audio_effects( ) -> void: + for channel in self.channel_status: + channel.update( ) + var cae:GodotModPlayerChannelAudioEffect = self.channel_audio_effects[channel.channel_number] + cae.ae_panner.pan = clamp( ( ( channel.panning - 128 ) / 128.0 ) + ( ( channel.panning_env.value - 32 ) / 32.0 ), -1.0, 1.0 ) diff --git a/addons/modplayer/ModPlayer.tscn b/addons/modplayer/ModPlayer.tscn new file mode 100644 index 0000000..ec4e2a5 --- /dev/null +++ b/addons/modplayer/ModPlayer.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/modplayer/ModPlayer.gd" type="Script" id=1] + +[node name="ModPlayer" type="Node"] +script = ExtResource( 1 ) diff --git a/addons/modplayer/XM.gd b/addons/modplayer/XM.gd new file mode 100644 index 0000000..28da330 --- /dev/null +++ b/addons/modplayer/XM.gd @@ -0,0 +1,287 @@ +""" + XM reader by あるる(きのもと 結衣) @arlez80 + + This script required Mod.gd for declaring the data structures. + + MIT License +""" + +class_name XM + +enum XMNoteFlags { + NOTE = 0x01, + INSTRUMENT = 0x02, + VOLUME = 0x04, + EFFECT_COMMAND = 0x08, + EFFECT_PARAM = 0x10, + + FIRST_BYTE_AS_FLAGS = 0x80, +} + +""" + ファイルから読み込む + + @param path ファイルパス + @return xm +""" +func read_file( path:String ) -> Mod.ModParseResult: + var f:File = File.new( ) + var result: = Mod.ModParseResult.new( ) + + var err:int = f.open( path, f.READ ) + if err != OK: + result.error = err + return result + + var stream:StreamPeerBuffer = StreamPeerBuffer.new( ) + stream.set_data_array( f.get_buffer( f.get_len( ) ) ) + f.close( ) + + result.data = self._read( stream ) + if result.data == null: + result.error = ERR_PARSE_ERROR + return result + +""" + 配列から読み込む + + @param data データ + @return xm +""" +func read_data( data:PoolByteArray ) -> Mod.ModParseResult: + var stream:StreamPeerBuffer = StreamPeerBuffer.new( ) + stream.set_data_array( data ) + + var result: = Mod.ModParseResult.new( ) + result.data = self._read( stream ) + if result.data == null: + result.error = ERR_PARSE_ERROR + return result + +""" + ストリームから読み込み + + @param stream ストリーム + @return xm +""" +func _read( stream:StreamPeerBuffer ) -> Mod.ModData: + var xm: = Mod.ModData.new( ) + + if self._read_string( stream, 17 ) != "Extended Module: ": + return null + xm.module_name = self._read_string( stream, 20 ) + if stream.get_u8( ) != 0x1A: + return null + xm.tracker_name = self._read_string( stream, 20 ) + xm.version = stream.get_u16( ) + + var header_size:int = stream.get_u32( ) + xm.song_length = stream.get_u16( ) + xm.restart_position = stream.get_u16( ) + xm.channel_count = stream.get_u16( ) + + var pattern_count:int = stream.get_u16( ) + var instrument_count:int = stream.get_u16( ) + xm.flags = stream.get_u16( ) + xm.init_tick = stream.get_u16( ) + xm.init_bpm = stream.get_u16( ) + + xm.song_positions = stream.get_partial_data( header_size - 20 )[1] + + xm.patterns = self._read_patterns( stream, xm.flags, pattern_count, xm.channel_count ) + if len( xm.patterns ) == 0: + return null + + xm.instruments = self._read_instruments( stream, instrument_count ) + if len( xm.instruments ) == 0: + return null + + return xm + +""" + パターン読み込み + + @param stream ストリーム + @param xm_file_flags XMデータに関するフラグ + @param pattern_count パターン数 + @param channles 最大チャンネル数 + @return パターンデータ +""" +func _read_patterns( stream:StreamPeerBuffer, xm_file_flags:int, pattern_count:int, channels:int ) -> Array: + var patterns:Array = [] + + for i in range( pattern_count ): + var pattern:Array = [] + + if stream.get_u32( ) != 9: + return [] + if stream.get_u8( ) != 0: + return [] + var row_count:int = stream.get_u16( ) + var size:int = stream.get_u16( ) + + for k in range( row_count ): + var line:Array = [] + for ch in range( channels ): + var patr: = Mod.ModPatternNote.new( ) + var first:int = stream.get_u8( ) + if first & XMNoteFlags.FIRST_BYTE_AS_FLAGS != 0: + if first & XMNoteFlags.NOTE != 0: + patr.note = stream.get_u8( ) + else: + patr.note = first + first = 0xFF + if 0 < patr.note: + patr.note -= 1 + if first & XMNoteFlags.INSTRUMENT != 0: + patr.instrument = stream.get_u8( ) + if first & XMNoteFlags.VOLUME != 0: + patr.volume = stream.get_u8( ) + if first & XMNoteFlags.EFFECT_COMMAND != 0: + patr.effect_command = stream.get_u8( ) + if first & XMNoteFlags.EFFECT_PARAM != 0: + patr.effect_param = stream.get_u8( ) + if 0 < patr.note: + if xm_file_flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0: + patr.key_number = self._conv_linear_freq( patr.note ) + else: + patr.key_number = self._conv_amiga_freq( patr.note ) + line.append( patr ) + pattern.append( line ) + patterns.append( pattern ) + + return patterns + +""" + ノート番号からAMIGA式で周波数計算する + + @param note ノート番号 + @return 周波数 +""" +func _conv_amiga_freq( note:int ) -> int: + return int( 6848.0 / pow( 2.0, note / 12.0 ) ) + +""" + ノート番号から線形で周波数計算する + + @param note ノート番号 + @return 周波数 +""" +func _conv_linear_freq( note:int ) -> int: + return 7680 - note * 64 + +""" + 楽器データを読み込む + + @param stream ストリーム + @param instrument_count 楽器数 + @return 楽器データ +""" +func _read_instruments( stream:StreamPeerBuffer, instrument_count:int ) -> Array: + var instruments:Array = [] + + for i in range( instrument_count ): + var inst: = Mod.ModInstrument.new( ) + var size:int = stream.get_u32( ) + var remain_size:int = size + inst.name = self._read_string( stream, 22 ) + #print( "%d/%d %s" % [ i, instrument_count, inst.name] ) + if stream.get_u8( ) != 0: + return [] + + var sample_count:int = stream.get_u16( ) + var sample_numbers:Array = [] + remain_size -= 4 + 22 + 1 + 2 + if 0 < sample_count: + var sample_header_size:int = stream.get_u32( ) + sample_numbers = stream.get_partial_data( 96 )[1] + inst.volume_envelope = Mod.ModEnvelope.new( ) + for k in range( 12 ): + var ve: = Mod.ModEnvelopePoint.new( ) + ve.frame = stream.get_u16( ) + ve.value = stream.get_u16( ) + inst.volume_envelope.points.append( ve ) + inst.panning_envelope = Mod.ModEnvelope.new( ) + for k in range( 12 ): + var pe: = Mod.ModEnvelopePoint.new( ) + pe.frame = stream.get_u16( ) + pe.value = stream.get_u16( ) + inst.panning_envelope.points.append( pe ) + remain_size -= 4 + 96 + 48 + 48 + inst.volume_envelope.point_count = stream.get_u8( ) + inst.panning_envelope.point_count = stream.get_u8( ) + inst.volume_envelope.sustain_point = stream.get_u8( ) + inst.volume_envelope.loop_start_point = stream.get_u8( ) + inst.volume_envelope.loop_end_point = stream.get_u8( ) + inst.panning_envelope.sustain_point = stream.get_u8( ) + inst.panning_envelope.loop_start_point = stream.get_u8( ) + inst.panning_envelope.loop_end_point = stream.get_u8( ) + inst.volume_envelope.set_flag( stream.get_u8( ) ) + inst.panning_envelope.set_flag( stream.get_u8( ) ) + inst.vibrato_type = stream.get_u8( ) + inst.vibrato_speed = stream.get_u8( ) + inst.vibrato_depth = stream.get_u8( ) + inst.vibrato_depth_shift = stream.get_u8( ) + inst.volume_fadeout = stream.get_u16( ) + remain_size -= 16 + + if 0 < remain_size: + stream.get_partial_data( remain_size ) # reserved + + if 0 < sample_count: + var sounds:Array = [] + # Sound Header + for k in range( sample_count ): + var xms: = Mod.ModSample.new( ) + xms.length = stream.get_u32( ) + xms.loop_start = stream.get_u32( ) + xms.loop_length = stream.get_u32( ) + xms.volume = stream.get_u8( ) + xms.finetune = stream.get_8( ) + xms.loop_type = stream.get_u8( ) + if xms.loop_type & 16 != 0: + xms.bit = 16 + xms.loop_type &= 3 + xms.panning = stream.get_u8( ) + xms.relative_note = stream.get_8( ) + stream.get_u8( ) # Reserved + xms.name = self._read_string( stream, 22 ) + sounds.append( xms ) + # Sound Data + for xms in sounds: + var d:Array = [] + var p:int = 0 + + if xms.bit == 16: + for k in range( xms.length >> 1 ): + p += stream.get_16( ) + d.append( p & 0xFF ) + d.append( ( p >> 8 ) & 0xFF ) + else: + for k in range( xms.length ): + p += stream.get_8( ) + d.append( p & 0xFF ) + + xms.data = PoolByteArray( d ) + + inst.samples = [] + for k in sample_numbers: + if len( sounds ) <= k: + inst.samples.append( sounds[0] ) + else: + inst.samples.append( sounds[k] ) + + instruments.append( inst ) + + return instruments + +""" + 文字列の読み込み + + @param stream ストリーム + @param size 文字列サイズ + @return 読み込んだ文字列を返す +""" +func _read_string( stream:StreamPeerBuffer, size:int ) -> String: + return stream.get_partial_data( size )[1].get_string_from_ascii( ) + diff --git a/addons/modplayer/icon.png b/addons/modplayer/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cfffaa42abbdf0cc92635d18d1cc6891a382716a GIT binary patch literal 924 zcmZ{iPly~v6vjVY!MHK*KtPgZq+1Y;L}AHk4uLMiY?39q4x52Fc<2T}yoBk4ig>9a z5C>tPH!S8PR3Eb0Ii=^6Ipk1-42TM;1uy=k`s5sYyh#4ppS63=K6vlrJ?i^CDBd@n z%}XbiSC;{t*tp!@^4d7aV@tlCd3WV1u!PS0-F4rp!&HZ&e5cgZ$Dh+XKP|lfIPkKE z0IvWmKnMsR0VI$EQ=kNDfXyabEq1$1Qx1n5jX0aJERQlY*=(`ZWjAFyKvBo+#uN0UC)DesLV1w0khLxj$>IGRp(u26r(oD3q#?4)&h>U?| zR4_J_mfF!&bV8w*1$`Y2idY*tvD!K__n=SC@{9BQ`UBpIxtDTsoj>gG*Igx4*2{EX z7lXoRV<*;GSIsL>1Hf)YX2i*!FO2GVG4C+5(AExd6Bm)CJE!p#=$ zox|_%fNz2w@GI~(_@Kdug6xwyx!-6s!Z2K0TN6TbI-Mj*(lnJ)Zf|erc|IPG{i*Kn z@0Vq{SS;$gehB;rJO!QrkAX-2GM_B+{I*j5Cw$`%<3POn;^Wi%<9mMa;%8f%R}Tlo z>XoI7e$?J*zt{cpyU!ndw2biB3&NcLcF*nH`0dupxs$8ylhcsh;pxApKmK{;)q3~rTNiiU{>SsMvA)^9dm*|0KVFv8UH||9 literal 0 HcmV?d00001 diff --git a/addons/modplayer/icon.png.import b/addons/modplayer/icon.png.import new file mode 100644 index 0000000..11ab2fd --- /dev/null +++ b/addons/modplayer/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/modplayer/icon.png" +dest_files=[ "res://.import/icon.png-517e88a2da8173eba7cfa3edf0828c77.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=false +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=false +svg/scale=1.0 diff --git a/addons/modplayer/plugin.cfg b/addons/modplayer/plugin.cfg new file mode 100644 index 0000000..04e3e4f --- /dev/null +++ b/addons/modplayer/plugin.cfg @@ -0,0 +1,6 @@ +[plugin] +name="Godot Mod Player" +description="A plugin that playing Mod Files for Godot Engine." +author="arlez80" +version="1.4.4" +script="GMP.gd" \ No newline at end of file diff --git a/addons/platformer_controller/._LICENSE b/addons/platformer_controller/._LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7f780e4dbeadd227ef62d55abe3d1fd219178b62 GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103v^7WYh z1)_st0Z_RBnifVNA1W@DoS&| | +--|--|-- + + + +## Customization / Export variables +Here are the values that you can change in the inspector: + +### max_jump_height +The max jump height in pixels. You reach this when you hold down jump. + +### Min Jump Height +The minimum jump height (tapping jump). + +### Double Jump Height +The height of your jump in the air (i.e. double jump, triple jump etc.). + +### Jump Duration +How long it takes to get to the peak of the jump (in seconds). + +### Falling Gravity Multiplier +Multiplies the gravity by this while falling. + +### Max Jump Amount +How many times you can jump before hitting the ground. Set this to 2 for a double jump. + +### Max Acceleration +How much you accelerate when you hold left or right (in pixels/sec^2). + +### Friction +The higher this number, the more friction is on your character. + +### Can Hold Jump +If this is off, you have to press jump down every time you land. If its on you can keep it held. + +### Coyote Time +You can still jump this many seconds after falling off a ledge. + +### Jump Buffer +Pressing jump this many seconds before hitting the ground will still make you jump.\ +Note: This is only needed when can_hold_jump is off. + +### Input Variables +`input_left`\ +`input_right`\ +`input_jump`\ + Set these to the names of your actions in the Input Map diff --git a/addons/platformer_controller/platformer_controller.gd b/addons/platformer_controller/platformer_controller.gd new file mode 100644 index 0000000..1433411 --- /dev/null +++ b/addons/platformer_controller/platformer_controller.gd @@ -0,0 +1,213 @@ +extends KinematicBody2D + +class_name PlatformerController2D + +# Set these to the name of your action (in the Input Map) +export var input_left : String = "move_left" +export var input_right : String = "move_right" +export var input_jump : String = "jump" + +# The max jump height in pixels (holding jump) +export var max_jump_height = 150 setget set_max_jump_height +# The minimum jump height (tapping jump) +export var min_jump_height = 40 setget set_min_jump_height +# The height of your jump in the air +export var double_jump_height = 100 setget set_double_jump_height +# How long it takes to get to the peak of the jump in seconds +export var jump_duration = 0.3 setget set_jump_duration +# Multiplies the gravity by this while falling +export var falling_gravity_multiplier = 1.5 +# Set to 2 for double jump +export var max_jump_amount = 1 +export var max_acceleration = 4000 +export var friction = 8 +export var can_hold_jump : bool = false +# You can still jump this many seconds after falling off a ledge +export var coyote_time : float = 0.1 +# Only neccessary when can_hold_jump is off +# Pressing jump this many seconds before hitting the ground will still make you jump +export var jump_buffer : float = 0.1 + + +# not used +var max_speed = 100 +var acceleration_time = 10 + + +# These will be calcualted automatically +var default_gravity : float +var jump_velocity : float +var double_jump_velocity : float +# Multiplies the gravity by this when we release jump +var release_gravity_multiplier : float + + +var jumps_left : int +var holding_jump := false + +var vel = Vector2() +var acc = Vector2() + +onready var coyote_timer = Timer.new() +onready var jump_buffer_timer = Timer.new() + + +func _init(): + default_gravity = calculate_gravity(max_jump_height, jump_duration) + jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration) + double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity) + release_gravity_multiplier = calculate_release_gravity_multiplier( + jump_velocity, min_jump_height, default_gravity) + + +func _ready(): + add_child(coyote_timer) + coyote_timer.wait_time = coyote_time + coyote_timer.one_shot = true + + add_child(jump_buffer_timer) + jump_buffer_timer.wait_time = jump_buffer + jump_buffer_timer.one_shot = true + + +func _physics_process(delta): + acc.x = 0 + + if is_on_floor(): + coyote_timer.start() + if not coyote_timer.is_stopped(): + jumps_left = max_jump_amount + + if Input.is_action_pressed(input_left): + acc.x = -max_acceleration + if Input.is_action_pressed(input_right): + acc.x = max_acceleration + + + # Check for ground jumps when we can hold jump + if can_hold_jump: + if Input.is_action_pressed(input_jump): + # Dont use double jump when holding down + if is_on_floor(): + jump() + + # Check for ground jumps when we cannot hold jump + if not can_hold_jump: + if not jump_buffer_timer.is_stopped() and is_on_floor(): + jump() + + # Check for jumps in the air + if Input.is_action_just_pressed(input_jump): + holding_jump = true + jump_buffer_timer.start() + + # Only jump in the air when press the button down, code above already jumps when we are grounded + if not is_on_floor(): + jump() + + + if Input.is_action_just_released(input_jump): + holding_jump = false + + + var gravity = default_gravity + + if vel.y > 0: # If we are falling + gravity *= falling_gravity_multiplier + + if not holding_jump and vel.y < 0: # if we released jump and are still rising + if not jumps_left < max_jump_amount - 1: # Always jump to max height when we are using a double jump + gravity *= release_gravity_multiplier # multiply the gravity so we have a lower jump + + acc.y = -gravity + vel.x *= 1 / (1 + (delta * friction)) + + vel += acc * delta + vel = move_and_slide(vel, Vector2.UP) + + + +func calculate_gravity(p_max_jump_height, p_jump_duration): + # Calculates the desired gravity by looking at our jump height and jump duration + # Formula is from this video https://www.youtube.com/watch?v=hG9SzQxaCm8 + return (-2 *p_max_jump_height) / pow(p_jump_duration, 2) + + +func calculate_jump_velocity(p_max_jump_height, p_jump_duration): + # Calculates the desired jump velocity by lookihg at our jump height and jump duration + return (2 * p_max_jump_height) / (p_jump_duration) + + +func calculate_jump_velocity2(p_max_jump_height, p_gravity): + # Calculates jump velocity from jump height and gravity + # formula from + # https://sciencing.com/acceleration-velocity-distance-7779124.html#:~:text=in%20every%20step.-,Starting%20from%3A,-v%5E2%3Du + return sqrt(-2 * p_gravity * p_max_jump_height) + + +func calculate_release_gravity_multiplier(p_jump_velocity, p_min_jump_height, p_gravity): + # Calculates the gravity when the key is released based on the minimum jump height and jump velocity + # Formula is from this website https://sciencing.com/acceleration-velocity-distance-7779124.html + var release_gravity = 0 - pow(p_jump_velocity, 2) / (2 * p_min_jump_height) + return release_gravity / p_gravity + + +func calculate_friction(time_to_max): + # Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3 + # this friction will hit 90% of max speed after the accel time + return 1 - (2.30259 / time_to_max) + + +func calculate_speed(p_max_speed, p_friction): + # Formula from https://www.reddit.com/r/gamedev/comments/bdbery/comment/ekxw9g4/?utm_source=share&utm_medium=web2x&context=3 + return (p_max_speed / p_friction) - p_max_speed + + +func jump(): + if jumps_left == max_jump_amount and coyote_timer.is_stopped(): + # Your first jump must be used when on the ground + # If you fall off the ground and then jump you will be using you second jump + jumps_left -= 1 + + if jumps_left > 0: + if jumps_left < max_jump_amount: # If we are double jumping + vel.y = -double_jump_velocity + else: + vel.y = -jump_velocity + jumps_left -= 1 + + + coyote_timer.stop() + + +func set_max_jump_height(value): + max_jump_height = value + + default_gravity = calculate_gravity(max_jump_height, jump_duration) + jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration) + double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity) + release_gravity_multiplier = calculate_release_gravity_multiplier( + jump_velocity, min_jump_height, default_gravity) + + +func set_jump_duration(value): + jump_duration = value + + default_gravity = calculate_gravity(max_jump_height, jump_duration) + jump_velocity = calculate_jump_velocity(max_jump_height, jump_duration) + double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity) + release_gravity_multiplier = calculate_release_gravity_multiplier( + jump_velocity, min_jump_height, default_gravity) + + +func set_min_jump_height(value): + min_jump_height = value + release_gravity_multiplier = calculate_release_gravity_multiplier( + jump_velocity, min_jump_height, default_gravity) + + +func set_double_jump_height(value): + double_jump_height = value + double_jump_velocity = calculate_jump_velocity2(double_jump_height, default_gravity) + + diff --git a/player/Player.tscn b/player/Player.tscn index 985f5dc..163732c 100644 --- a/player/Player.tscn +++ b/player/Player.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=12 format=2] [ext_resource path="res://assets/images/Clean-Retro-Lines-Player-Plain.png" type="Texture" id=1] -[ext_resource path="res://player/Player.gd" type="Script" id=2] +[ext_resource path="res://addons/platformer_controller/platformer_controller.gd" type="Script" id=2] [sub_resource type="AtlasTexture" id=5] atlas = ExtResource( 1 ) @@ -11,10 +11,6 @@ region = Rect2( 0, 16, 16, 16 ) atlas = ExtResource( 1 ) region = Rect2( 16, 16, 16, 16 ) -[sub_resource type="AtlasTexture" id=7] -atlas = ExtResource( 1 ) -region = Rect2( 16, 32, 16, 16 ) - [sub_resource type="AtlasTexture" id=1] atlas = ExtResource( 1 ) region = Rect2( 0, 0, 16, 16 ) @@ -31,6 +27,10 @@ region = Rect2( 32, 0, 16, 16 ) atlas = ExtResource( 1 ) region = Rect2( 48, 0, 16, 16 ) +[sub_resource type="AtlasTexture" id=7] +atlas = ExtResource( 1 ) +region = Rect2( 16, 32, 16, 16 ) + [sub_resource type="SpriteFrames" id=8] animations = [ { "frames": [ SubResource( 5 ), SubResource( 6 ) ], @@ -38,15 +38,15 @@ animations = [ { "name": "idle", "speed": 2.0 }, { -"frames": [ SubResource( 7 ) ], -"loop": true, -"name": "jump", -"speed": 5.0 -}, { "frames": [ SubResource( 1 ), SubResource( 2 ), SubResource( 3 ), SubResource( 4 ) ], "loop": true, "name": "walk", "speed": 5.0 +}, { +"frames": [ SubResource( 7 ) ], +"loop": true, +"name": "jump", +"speed": 5.0 } ] [sub_resource type="RectangleShape2D" id=9] @@ -54,10 +54,16 @@ extents = Vector2( 4, 7 ) [node name="Player" type="KinematicBody2D"] script = ExtResource( 2 ) +max_jump_height = 50 +min_jump_height = 25 +double_jump_height = 65 +max_jump_amount = 2 +max_acceleration = 1500 [node name="AnimatedSprite" type="AnimatedSprite" parent="."] frames = SubResource( 8 ) animation = "idle" +frame = 1 playing = true [node name="CollisionShape2D" type="CollisionShape2D" parent="."] diff --git a/project.godot b/project.godot index b198fd0..44df75b 100644 --- a/project.godot +++ b/project.godot @@ -8,6 +8,34 @@ config_version=4 +_global_script_classes=[ { +"base": "Reference", +"class": "Mod", +"language": "GDScript", +"path": "res://addons/modplayer/Mod.gd" +}, { +"base": "Node", +"class": "ModPlayer", +"language": "GDScript", +"path": "res://addons/modplayer/ModPlayer.gd" +}, { +"base": "KinematicBody2D", +"class": "PlatformerController2D", +"language": "GDScript", +"path": "res://addons/platformer_controller/platformer_controller.gd" +}, { +"base": "Reference", +"class": "XM", +"language": "GDScript", +"path": "res://addons/modplayer/XM.gd" +} ] +_global_script_class_icons={ +"Mod": "", +"ModPlayer": "res://addons/modplayer/icon.png", +"PlatformerController2D": "", +"XM": "" +} + [application] config/name="Retro Platformer" @@ -15,6 +43,7 @@ run/main_scene="res://levels/Level01.tscn" boot_splash/image="res://assets/images/WithinLogo.png" boot_splash/fullsize=false boot_splash/use_filter=false +boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 1 ) config/icon="res://icon.png" [autoload]