Files
etazhi/godot-mod-player-g4-master/addons/modplayer/ModPlayer.gd
T
2026-11-01 16:00:46 +03:00

1066 lines
35 KiB
GDScript

##
## 100% pure GDScript software Mod Player [Godot Mod Player]
## by あるる(きのもと 結衣) @arlez80
##
## MIT License
##
@warning_ignore_start("untyped_declaration")
@warning_ignore_start("unsafe_method_access")
@warning_ignore_start("unused_variable")
@warning_ignore_start("unsafe_property_access")
@warning_ignore_start("shadowed_variable")
@warning_ignore_start("return_value_discarded")
@warning_ignore_start("unused_parameter")
@icon("icon.png")
class_name ModPlayer
extends Node
# -------------------------------------------------------
# 定数
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:Array[float] = [
-144.0,-36.1,-30.1,-26.6,-24.1,-22.1,-20.6,
-19.2,-18.1,-17.0,-16.1,-15.3,-14.5,-13.8,-13.2,
-12.6,-12.0,-11.5,-11.0,-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.0,-5.8,-5.5,-5.2,-5.0,-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.0,-1.8,-1.6,-1.5,-1.3,-1.2,-1.0,-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:Mod.ModInstrument
var array_asw:Array[AudioStreamWAV]
class GodotModPlayerPitch:
const center_key_freq:float = 856.0
## 現在値
var value:int
## 変化目標(ポルタメント用)
var dest:int
## 変化量(ポルタメント用)
var speed:int
## 最後の変化量(ポルタメント用)
var last_speed:int
## ポルタメント量
var porta_value:int
## アルペジオリスト
var arpeggio:Array[int]
## アルペジオカウンタ
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.last_speed = 0
self.porta_value = 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 vibrato ビブラート値
## @return 計算したピッチスケール
func get_pitch_scale( vibrato:int = 0 ) -> float:
var v:int = self.value
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 ) - ( self.porta_value / 255.0 ) + ( vibrato / 255.0 / 12.0 * 5.0 ) )
v += vibrato + self.porta_value
if v == 0:
return 0.0
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: そのうちPackedByteArrayの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:
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 Vector2i( 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 = 1024.0 / 44100.0
var source_inst:Mod.ModInstrument
var source_sample: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 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 last_porta_up:int = 0
var last_porta_down:int = 0
var last_volume_slide:int = 0
var last_panning_slide:int = 0
var last_multi_retrig_note:int = 0
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 = 6 if linear_freq else 7 # XXX: 多分 Linear Frequency の時に大きめに設定してある気がした。
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 ( 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( )
var pitch:float = self.get_pitch_scale( )
if pitch < 0.01:
asp.stream_paused = true
else:
if asp.stream_paused:
asp.stream_paused = false
asp.pitch_scale = pitch
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 = ( clampf( 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( )
## ボリュームスライドを実行
## @param value 値
func execute_volume_slide( value:int ) -> void:
if value == 0:
value = self.last_volume_slide
else:
self.last_volume_slide = value
self.volume = clampi( self.volume + ( value >> 4 ) - ( value & 0x0F ), 0, 64 )
## ノートオフ
func note_off( ) -> void:
self.volume_env.note_off( )
self.panning_env.note_off( )
## 再トリガー
func retrig( ) -> void:
self.asp_switcher = ( self.asp_switcher + 1 ) % self.asps.size( )
var asp:AudioStreamPlayer = self.asps[self.asp_switcher]
var pitch_scale:float = self.get_pitch_scale( )
asp.pitch_scale = pitch_scale
asp.play( maxf( 0.0, self.head_silent_second - clampf( self.gap_second - AudioServer.get_time_to_next_mix( ), 0.0, self.gap_second ) * pitch_scale ) )
## ノートオン
## @param inst 楽器データ
## @param effect_command エフェクトコマンド
## @param key_number 周波数
## @param note ノート番号
## @param sample_offset サンプルオフセット
## @param reset_inst 楽器データ再設定
func note_on( inst:GodotModPlayerInstrument, effect_command:int, key_number:int, note:int, sample_offset:int, reset_inst:bool ) -> void:
self.last_key_number = key_number
if self.mute:
return
var tone_porta:bool = effect_command == 0x03 || effect_command == 0x05
self.force_note_off = false
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
if not tone_porta:
self.asp_switcher = ( self.asp_switcher + 1 ) % self.asps.size( )
var asp:AudioStreamPlayer = self.asps[self.asp_switcher]
var asw:AudioStreamWAV = inst.array_asw[note-1]
if tone_porta:
self.pitch.value += self.pitch.porta_value
self.pitch.dest = key_number
else:
asp.stop( )
asp.stream = asw
self.pitch.value = key_number
self.pitch.porta_value = 0
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 = 7 - self.source_inst.vibrato_depth_shift
if reset_inst:
self.volume = self.source_sample.volume
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( )
if 0.0 < pitch_scale:
asp.pitch_scale = pitch_scale
if not tone_porta:
asp.play( maxf( 0.0, self.head_silent_second + ( float(sample_offset) / float(asw.mix_rate) ) - clampf( self.gap_second - AudioServer.get_time_to_next_mix( ), 0.0, self.gap_second ) * pitch_scale ) )
if pitch_scale < 0.01:
asp.stream_paused = true
# -----------------------------------------------------------------------------
# Export
## ファイル
@export_file ("*.mod", "*.xm") var file:String = "" : set = set_file
## 再生中か?
@export var playing:bool = false
## 音量
@export_range(-144.0, 0.0) var volume_db:float = -20.0 : set = set_volume_db
## ループフラグ
@export var loop:bool = false
## mix_target same as AudioStreamPlayer's one
@export var mix_target:AudioStreamPlayer.MixTarget = AudioStreamPlayer.MIX_TARGET_STEREO
## bus same as AudioStreamPlayer's one
@export var bus:StringName = &"Master"
# -----------------------------------------------------------------------------
# 変数
## Modデータ
var mod_data:Mod.ModData = null : set = set_mod_data
## テンポ
var tempo:float = 125.0 : set = 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[GodotModPlayerInstrument] = []
## チャンネル
var channel_status:Array[GodotModPlayerChannelStatus] = []
## Modチャンネルエフェクト
var channel_audio_effects:Array[GodotModPlayerChannelAudioEffect] = []
## パターンジャンプ先
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
## グローバル音量スライド最終パラメータ
var global_volume_slide_last_param:int = 0x00
## グローバル音量 (計算用db)
var global_volume_db:float = 0.0
## 準備
func _ready( ):
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 )
else:
for i in range( 32 ):
var mod_channel_bus_idx:int = 0
for k in ( AudioServer.get_bus_count( ) ):
if AudioServer.get_bus_name( k ) == self.mod_channel_bus_name % i:
mod_channel_bus_idx = k
break
var cae: = GodotModPlayerChannelAudioEffect.new( )
for k in ( AudioServer.get_bus_effect_count( mod_channel_bus_idx ) ):
var ae: = AudioServer.get_bus_effect( mod_channel_bus_idx, k )
if ae is AudioEffectPanner:
cae.ae_panner = ae
self.channel_audio_effects.append( cae )
if self.playing:
self.play( )
## 通知
## @param what 通知要因
func _notification( what:int ):
# 破棄時
if what == NOTIFICATION_PREDELETE:
pass
# 削除せずに使いまわす
#AudioServer.remove_bus( AudioServer.get_bus_index( self.mod_master_bus_name ) )
#for i in range( 16 ):
# AudioServer.remove_bus( AudioServer.get_bus_index( self.midi_channel_bus_name % i ) )
## 再生前の初期化
func _prepare_to_play( ) -> void:
# ファイル読み込み
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.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[int] = []
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 ( head_silent_samples ):
temp_head_silent.append( 0 )
var head_silent:PackedByteArray = PackedByteArray( temp_head_silent )
var loaded:Dictionary = {}
for t in self.mod_data.instruments:
var inst:GodotModPlayerInstrument = GodotModPlayerInstrument.new( )
inst.source = t
inst.array_asw = []
for sample in t.samples:
var id:int = sample.get_instance_id( )
var ass:AudioStreamWAV = null
if not( id in loaded ):
ass = AudioStreamWAV.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 = AudioStreamWAV.FORMAT_16_BITS
ass.loop_begin = sample.loop_start + head_silent_samples * 2
else:
ass.data = head_silent + sample.data
ass.format = AudioStreamWAV.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 = AudioStreamWAV.LOOP_DISABLED
if sample.loop_type & Mod.ModLoopType.FORWARD_LOOP != 0:
ass.loop_mode = AudioStreamWAV.LOOP_FORWARD
elif sample.loop_type & Mod.ModLoopType.PING_PONG_LOOP != 0:
ass.loop_mode = AudioStreamWAV.LOOP_PINGPONG
loaded[id] = ass
else:
ass = loaded[id]
inst.array_asw.append( ass )
self.instruments.append( inst )
for k in loaded.keys( ):
loaded.erase( k )
self.channel_status = []
for i in ( 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 )
## 再生
## @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 )
## シーク
## @param to_position 再生位置
func seek( to_position:float ) -> void:
printerr( "Godot Mod Player: seekは未実装 seek not implemented yet" )
self._stop_all_notes( )
## 停止
func stop( ) -> void:
self._stop_all_notes( )
self.playing = false
## ファイル変更
## @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.mod_data == null:
return
if not self.playing:
return
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.looped.emit( )
else:
self.song_position = 0
self.stop( )
return
## 1行処理
func _process_row( ) -> void:
var pattern_line:Array[Mod.ModPatternNote] = 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
var sample_offset:int = 0
# 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
elif note.effect_command == 0x09:
sample_offset = note.effect_param * 256
# 楽器設定
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 or note.instrument != 0:
var key_number: = note.key_number if 0 < note.key_number else channel.last_key_number
if 0 <= channel.last_instrument and channel.last_instrument < self.instruments.size( ) and 0 < key_number:
var inst:GodotModPlayerInstrument = self.instruments[channel.last_instrument]
channel.note_on( inst, note.effect_command, key_number, note.note, sample_offset, note.instrument != 0 )
self.note_on.emit( 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[Mod.ModPatternNote] = 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 = clampi( channel.volume - ( note.volume & 0x0F ), 0, 64 )
0x07: # Volume slide up
if not disable_channel_row:
channel.volume = clampi( channel.volume + ( note.volume & 0x0F ), 0, 64 )
0x08: # Fine volume slide down
if not disable_channel_row:
channel.volume = clampi( channel.volume - ( note.volume & 0x0F ), 0, 64 )
0x09: # Fine volume slide up
if not disable_channel_row:
channel.volume = clampi( channel.volume + ( note.volume & 0x0F ), 0, 64 )
0x0A: # Set vibrato speed
channel.vibrato.speed = (channel.vibrato.speed & 0x0F) | ( ( note.volume & 0x0F ) << 4 );
0x0B: # Vibrato Depth
channel.vibrato.depth = note.volume & 0x0F
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 = clampi( channel.panning - ( note.volume & 0x0F ), 0, 255 )
0x0E: # Panning slide right
if not disable_channel_row:
channel.panning = clampi( channel.panning + ( note.volume & 0x0F ), 0, 255 )
0x0F: # Tone portamento
if 0 < note.volume & 0x0F:
var p:int = note.volume & 0x0F
p |= p << 4
channel.pitch.speed = self._fix_tone_portament( p )
_:
printerr( "unknown volume effect command: %02x" % note.volume )
# エフェクトコマンド
match note.effect_command:
0x00: # Arpeggio
if note.effect_param != 0:
if channel.pitch.linear_freq:
const linear_freq_factor:int = -64 # -( 768 / 12 )
channel.pitch.arpeggio[1] = ( note.effect_param >> 4 ) * linear_freq_factor
channel.pitch.arpeggio[2] = ( note.effect_param & 0x0F ) * linear_freq_factor
else:
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
else:
channel.pitch.arpeggio_enabled = false
0x01: # Portament up
var value:int = note.effect_param
if value == 0:
value = channel.last_porta_up
else:
channel.last_porta_up = value
if not disable_channel_row:
channel.pitch.porta_value -= value
0x02: # Portament down
var value:int = note.effect_param
if value == 0:
value = channel.last_porta_down
else:
channel.last_porta_down = value
if not disable_channel_row:
channel.pitch.porta_value += value
0x03: # Portament speed
if note.effect_param != 0:
channel.pitch.speed = self._fix_tone_portament( note.effect_param )
channel.pitch.last_speed = channel.pitch.speed
else:
channel.pitch.speed = channel.pitch.last_speed
channel.pitch.update( )
0x04: # Vibrato
if ( note.effect_param & 0xF0 ) != 0:
channel.vibrato.speed = note.effect_param >> 4
if ( note.effect_param & 0x0F ) != 0:
channel.vibrato.depth = note.effect_param & 0x0F
channel.vibrato.update( )
0x05: # Portament + Volume slide
channel.pitch.update( )
if not disable_channel_row:
channel.execute_volume_slide( note.effect_param )
0x06: # Vibrato + Volume slide
channel.vibrato.update( )
if not disable_channel_row:
channel.execute_volume_slide( note.effect_param )
0x07: # Tremolo
if note.effect_param & 0xF0 != 0:
channel.tremolo.speed = note.effect_param >> 4
if note.effect_param & 0x0F != 0:
channel.tremolo.depth = note.effect_param & 0x0F
channel.tremolo.update( )
0x08: # Panning
channel.panning = clampi( note.effect_param * 2, 0, 255 )
0x09: # Sample offset
# 発音時に処理
pass
0x0A: # Volume slide
if not disable_channel_row:
channel.execute_volume_slide( note.effect_param )
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 = clampi( 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 note.effect_param & 0x0F != 0:
channel.fine_pitch.speed = note.effect_param & 0x0F
channel.fine_pitch.update( )
0x02: # Fine portamento down
if note.effect_param & 0x0F != 0:
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 = clampi( channel.volume + ( note.volume & 0x0F ), 0, 64 )
0x0B: # Fine volume slide down
if not disable_channel_row:
channel.volume = clampi( 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 = clampi( 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
if note.effect_param != 0x00:
self.global_volume_slide_last_param = note.effect_param
self.global_volume = clampi( self.global_volume + ( self.global_volume_slide_last_param >> 4 ) - ( self.global_volume_slide_last_param & 0x0F ), 0, 64 )
self.global_volume_db = self.volume_table[self.global_volume]
self.set_volume_db( self.volume_db )
0x14: # Key unchecked
printerr( "not implemented: Kxx Key unchecked" )
0x15: # Set envelope position
channel.tremolo.phase = note.effect_param
0x19: # Pannning slide
if not disable_channel_row:
var value:int = note.effect_param
if value == 0:
value = channel.last_panning_slide
else:
channel.last_panning_slide = value
channel.panning = clampi( channel.panning + ( value >> 4 ) - ( value & 0x0F ), 0, 255 )
0x1B: # Multi retrig note
var value:int = note.effect_param
if value == 0:
value = channel.last_multi_retrig_note
else:
channel.last_multi_retrig_note = value
if ( ( value & 0xF0 ) >> 4 ) == 0 or ( ( self.processed_tick_count - 1 ) % ( ( value & 0xF0 ) >> 4 ) ) == 0:
channel.retrig( )
if disable_channel_row:
match value & 0x0F:
0:
pass
1:
self.global_volume = clampi( self.global_volume - 1, 0, 64 )
2:
self.global_volume = clampi( self.global_volume - 2, 0, 64 )
3:
self.global_volume = clampi( self.global_volume - 4, 0, 64 )
4:
self.global_volume = clampi( self.global_volume - 8, 0, 64 )
5:
self.global_volume = clampi( self.global_volume - 16, 0, 64 )
6:
self.global_volume = clampi( floori( self.global_volume * 2.0 / 3.0 ), 0, 64 )
7:
self.global_volume = clampi( floori( self.global_volume / 2.0 ), 0, 64 )
8:
printerr( "error: can't use 8 volume command in multi retrig note (0x1B)" )
9:
self.global_volume = clampi( self.global_volume + 1, 0, 64 )
10:
self.global_volume = clampi( self.global_volume + 2, 0, 64 )
11:
self.global_volume = clampi( self.global_volume + 4, 0, 64 )
12:
self.global_volume = clampi( self.global_volume + 8, 0, 64 )
13:
self.global_volume = clampi( self.global_volume + 16, 0, 64 )
14:
self.global_volume = clampi( floori( self.global_volume * 3.0 / 2.0 ), 0, 64 )
15:
self.global_volume = clampi( floori( self.global_volume * 2.0 ), 0, 64 )
0x1D: # Tremor
printerr( "not implemented: Txy Tremor" )
0x21: # Extra Fine Porta Up
printerr( "not implemented: Xxx Extra Fine Porta Up" )
_:
printerr( "unknown command: %02x : %04x" % [ note.effect_command, note.effect_param ] )
## ポルタメント補正
## @param v 元値
## @return 補正後値
func _fix_tone_portament( v:int ) -> int:
# Linear Frequency
if self.mod_data.flags & Mod.ModFlags.LINEAR_FREQUENCY_TABLE != 0:
# XXX: この4倍には根拠なし。耳コピでこのぐらいだと思った。
return v * 4
# Amiga Frequency
return v
## 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 = clampf( ( ( channel.panning - 128 ) / 128.0 ) + ( ( channel.panning_env.value - 32 ) / 32.0 ), -1.0, 1.0 )