TETRIS3000/source/midi/MidiPlayer.gd
2019-01-26 18:01:10 +01:00

401 lines
11 KiB
GDScript

extends Node
const max_track = 16
const max_channel = 16
const max_note_number = 128
const max_program_number = 128
const drum_track_channel = 0x09
const drum_track_bank = 128
const ADSR = preload( "ADSR.tscn" )
const SMF = preload( "SMF.gd" )
const SoundFont = preload( "SoundFont.gd" )
const Bank = preload( "Bank.gd" )
export var max_polyphony = 64
export var file = ""
export var playing = false
export var channel_mute = [false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false]
export var play_speed = 1.0
export var volume_db = -30
export var key_shift = 0
export var loop = false
export var loop_start = 0
export var soundfont = ""
export var mix_target = AudioStreamPlayer.MIX_TARGET_STEREO
export var bus = "Master"
var smf_data = null
var tempo = 120 setget set_tempo
var seconds_to_timebase = 2.3
var position = 0
var last_position = 0
var track_status = null
var channel_status = []
var channel_volume_db = 20
var bank = null
var audio_stream_players = []
var _used_program_numbers = []
signal changed_tempo( tempo )
signal appeared_lyric( lyric )
signal appeared_marker( marker )
signal appeared_cue_point( cue_point )
signal looped
func _ready( ):
self._prepare_to_play( )
if self.playing:
self.play( )
"""
初期化
"""
func _prepare_to_play( ):
# ファイル読み込み
if self.smf_data == null:
var smf_reader = SMF.new( )
self.smf_data = smf_reader.read_file( self.file )
self._init_track( )
self._analyse_smf( )
self._init_channel( )
# 楽器
if self.bank == null:
self.bank = Bank.new( )
if self.soundfont != "":
var sf_reader = SoundFont.new( )
var sf2 = sf_reader.read_file( self.soundfont )
self.bank.read_soundfont( sf2, self._used_program_numbers )
# 発音機
if self.audio_stream_players.size( ) == 0:
for i in range( self.max_polyphony ):
var audio_stream_player = ADSR.instance( )
audio_stream_player.mix_target = self.mix_target
audio_stream_player.bus = self.bus
self.add_child( audio_stream_player )
self.audio_stream_players.append( audio_stream_player )
else:
for audio_stream_player in self.audio_stream_players:
audio_stream_player.mix_target = self.mix_target
audio_stream_player.bus = self.bus
"""
トラック初期化
"""
class TrackSorter:
static func sort(a, b):
if a.time < b.time:
return true
elif b.time < a.time:
return false
else:
if a.event.type == SMF.MIDIEventType.note_off:
return true
elif b.event.type == SMF.MIDIEventType.note_off:
return false
func _init_track( ):
self.track_status = {
"events": [],
"event_pointer": 0,
}
for track in self.smf_data.tracks:
self.track_status.events += track.events
if 1 < self.smf_data.tracks.size( ):
self.track_status.events.sort_custom( TrackSorter, "sort" )
self.last_position = self.track_status.events[len(self.track_status.events)-1].time
"""
SMF解析
"""
func _analyse_smf( ):
var channels = []
for i in range( max_channel ):
channels.append({
#"note_on": {},
#"program_number": 0,
"number": i,
"bank": 0,
})
self._used_program_numbers = []
for event_chunk in self.track_status.events:
var channel_number = event_chunk.channel_number
var channel = channels[channel_number]
var event = event_chunk.event
match event.type:
#SMF.MIDIEventType.note_off:
# channel.note_on.erase( event.note )
#SMF.MIDIEventType.note_on:
# channel.note_on[event.note] = true
SMF.MIDIEventType.program_change:
var program_number = event.number | ( channel.bank << 7 )
# channel.program_number = program_number
if not( event.number in self._used_program_numbers ):
self._used_program_numbers.append( event.number )
if not( program_number in self._used_program_numbers ):
self._used_program_numbers.append( program_number )
SMF.MIDIEventType.control_change:
match event.number:
SMF.control_number_bank_select_msb:
if channel.number == drum_track_channel:
channel.bank = drum_track_bank
else:
channel.bank = ( channel.bank & 0x7F ) | ( event.value << 7 )
SMF.control_number_bank_select_lsb:
if channel.number == drum_track_channel:
channel.bank = drum_track_bank
else:
channel.bank = ( channel.bank & 0x3F80 ) | ( event.value & 0x7F )
SMF.control_number_tkool_loop_point:
self.loop_start = event_chunk.time
"""
チャンネル初期化
"""
func _init_channel( ):
self.channel_status = []
for i in range( max_channel ):
var drum_track = ( i == drum_track_channel )
var bank = 0
if drum_track:
bank = self.drum_track_bank
self.channel_status.append({
"number": i,
"note_on": {},
"bank": bank,
"program": 0,
"volume": 1.0,
"expression": 1.0,
"pitch_bend": 0.0,
"drum_track": drum_track,
"pan": 0.5,
})
"""
再生
@param from_position
"""
func play( from_position = 0 ):
self.playing = true
self.seek( from_position )
"""
シーク
"""
func seek( to_position ):
self._stop_all_notes( )
self.position = to_position
var pointer = 0
var length = len(self.track_status.events)
while pointer < length:
var event_chunk = self.track_status.events[pointer]
if self.position <= event_chunk.time:
break
pointer += 1
self.track_status.event_pointer = pointer
"""
停止
"""
func stop( ):
self._stop_all_notes( )
self.playing = false
"""
テンポ設定
"""
func set_tempo( bpm ):
tempo = bpm
self.seconds_to_timebase = tempo / 60.0
self.emit_signal( "changed_tempo", bpm )
"""
全音を止める
"""
func _stop_all_notes( ):
for audio_stream_player in self.audio_stream_players:
audio_stream_player.stop( )
for channel in self.channel_status:
channel.note_on = {}
"""
毎フレーム処理
"""
func _process( delta ):
if self.smf_data == null:
return
if not self.playing:
return
self.position += self.smf_data.timebase * delta * self.seconds_to_timebase * self.play_speed
self._process_track( )
"""
トラック処理
"""
func _process_track( ):
var track = self.track_status
if track.events == null:
return
var length = len(track.events)
if length <= track.event_pointer:
if self.loop:
self.seek( self.loop_start )
self.emit_signal( "looped" )
else:
self.playing = false
return
for channel in self.channel_status:
for key_number in channel.note_on.keys( ):
var note_on = channel.note_on[key_number]
if not note_on.playing:
channel.note_on.erase( key_number )
while track.event_pointer < length:
var event_chunk = track.events[track.event_pointer]
if self.position < event_chunk.time:
break
track.event_pointer += 1
var channel = self.channel_status[event_chunk.channel_number]
var event = event_chunk.event
match event.type:
SMF.MIDIEventType.note_off:
self._process_track_event_note_off( channel, event )
SMF.MIDIEventType.note_on:
self._process_track_event_note_on( channel, event )
SMF.MIDIEventType.program_change:
channel.program = event.number
SMF.MIDIEventType.control_change:
self._process_track_event_control_change( channel, event )
SMF.MIDIEventType.pitch_bend:
channel.pitch_bend = event.value / 8192.0 - 1.0
self._update_pitch_bend_note( channel )
SMF.MIDIEventType.system_event:
self._process_track_system_event( channel, event )
_:
# 無視
pass
func _process_track_event_note_off( channel, event ):
var key_number = event.note + self.key_shift
if channel.note_on.has( key_number ):
var note_player = channel.note_on[key_number]
if note_player != null:
note_player.start_release( )
channel.note_on.erase( key_number )
func _process_track_event_note_on( channel, event ):
if not self.channel_mute[channel.number]:
var key_number = event.note + self.key_shift
var note_volume = channel.volume * channel.expression * ( event.velocity / 127.0 )
var volume_db = note_volume * self.channel_volume_db - self.channel_volume_db + self.volume_db
var preset = self.bank.get_preset( channel.program, channel.bank )
var instrument = preset.instruments[key_number]
if instrument != null:
if channel.note_on.has( key_number ):
channel.note_on[key_number].start_release( )
var note_player = self._get_idle_player( channel.program )
if note_player != null:
note_player.velocity = event.velocity
note_player.maximum_volume_db = volume_db
note_player.pitch_bend = channel.pitch_bend
note_player.set_instrument( instrument )
note_player.play( )
if not channel.drum_track:
channel.note_on[key_number] = note_player
func _process_track_event_control_change( channel, event ):
match event.number:
SMF.control_number_volume:
channel.volume = event.value / 127.0
self._update_volume_note( channel )
SMF.control_number_expression:
channel.expression = event.value / 127.0
self._update_volume_note( channel )
SMF.control_number_pan:
channel.pan = event.value / 127.0
SMF.control_number_bank_select_msb:
if channel.drum_track:
channel.bank = self.drum_track_bank
else:
channel.bank = ( channel.bank & 0x7F ) | ( event.value << 7 )
SMF.control_number_bank_select_lsb:
if channel.drum_track:
channel.bank = self.drum_track_bank
else:
channel.bank = ( channel.bank & 0x3F80 ) | ( event.value & 0x7F )
_:
# 無視
pass
func _process_track_system_event( channel, event ):
match event.args.type:
SMF.MIDISystemEventType.set_tempo:
self.tempo = 60000000.0 / event.args.bpm
SMF.MIDISystemEventType.lyric:
self.emit_signal( "appeared_lyric", event.args.text )
SMF.MIDISystemEventType.marker:
self.emit_signal( "appeared_marker", event.args.text )
SMF.MIDISystemEventType.cue_point:
self.emit_signal( "appeared_cue_point", event.args.text )
_:
# 無視
pass
func _get_idle_player( program ):
var stopped_audio_stream_player = null
var minimum_volume = 100.0
var oldest_audio_stream_player = null
var oldest = 0.0
for audio_stream_player in self.audio_stream_players:
if not audio_stream_player.playing:
return audio_stream_player
if audio_stream_player.releasing and audio_stream_player.current_volume < minimum_volume:
stopped_audio_stream_player = audio_stream_player
minimum_volume = audio_stream_player.current_volume
if oldest < audio_stream_player.using_timer:
oldest_audio_stream_player = audio_stream_player
oldest = audio_stream_player.using_timer
if stopped_audio_stream_player != null:
return stopped_audio_stream_player
return oldest_audio_stream_player
func _update_volume_note( channel ):
for note in channel.note_on.values( ):
var note_volume = channel.volume * channel.expression * ( note.velocity / 127.0 )
var volume_db = note_volume * self.channel_volume_db - self.channel_volume_db + self.volume_db
note.maximum_volume_db = volume_db
func _update_pitch_bend_note( channel ):
for note in channel.note_on.values( ):
note.set_pitch_bend( channel.pitch_bend )
"""
現在発音中の音色数を返す
"""
func get_now_playing_polyphony( ):
var polyphony = 0
for audio_stream_player in self.audio_stream_players:
if audio_stream_player.playing:
polyphony += 1
return polyphony