Refactor BGM handling
This commit is contained in:
parent
be7874ba27
commit
57d88e876a
|
@ -236,414 +236,3 @@ func _process(delta: float) -> void:
|
|||
self.is_playing = play_pulse(bgm_timestamp)
|
||||
if not self.is_playing:
|
||||
print('BGM finished playing')
|
||||
|
||||
|
||||
# TODO: need to interleave channels for tempo and master volume!
|
||||
const MAX_NOTE_EVENTS := 2048
|
||||
class NoteEvent:
|
||||
var p_start: int # In pulse space
|
||||
var p_end: int
|
||||
var instrument: int
|
||||
var pitch: int
|
||||
var velocity: float
|
||||
var adsr_attack: int
|
||||
var adsr_decay: int
|
||||
var adsr_sustain: int
|
||||
var adsr_release: int
|
||||
|
||||
|
||||
class TrackCurve: # built-in Curve class is too restrictive for this
|
||||
var default: float
|
||||
var entries: PoolVector3Array
|
||||
var baked_integrals: PoolRealArray
|
||||
func _init(default: float = 0.0):
|
||||
self.default = default
|
||||
self.entries = PoolVector3Array()
|
||||
self.baked_integrals = PoolRealArray()
|
||||
|
||||
func add_point(pulse: int, value: float, ramp_to_next: bool) -> void:
|
||||
var l := len(self.entries)
|
||||
var entry := Vector3(float(pulse), value, float(ramp_to_next))
|
||||
if l == 0 or self.entries[-1].x < pulse:
|
||||
self.entries.append(entry)
|
||||
else: # Find the first entry bigger than pulse, and insert before
|
||||
for i in l:
|
||||
if self.entries[i].x > pulse:
|
||||
self.entries.insert(i, entry)
|
||||
break
|
||||
|
||||
var last_pulse_block_get: int = -1 # Cache previous position for sequential lookups
|
||||
func get_pulse(pulse: float) -> float:
|
||||
var l := len(self.entries)
|
||||
if l == 0 or pulse < self.entries[-1].x:
|
||||
return self.default
|
||||
if pulse > self.entries[-1].x:
|
||||
return self.entries[-1].y
|
||||
for i in l-2:
|
||||
# Find first entry beyond
|
||||
if pulse < self.entries[i+1].x:
|
||||
if self.entries[i].z > 0: # ramp_to_next
|
||||
return range_lerp(pulse, self.entries[i].x, self.entries[i+1].x, self.entries[i].y, self.entries[i+1].y)
|
||||
else:
|
||||
return self.entries[i].y
|
||||
return self.default # Should be unreachable
|
||||
|
||||
func bake_integrals():
|
||||
# Store the starting integrated value (i.e. time for the tempo curve) of each pulse value
|
||||
self.baked_integrals.clear()
|
||||
var last_pulse := 0.0
|
||||
var last_value := self.default
|
||||
var last_integral := 0.0
|
||||
var last_ramp := false
|
||||
for entry in self.entries:
|
||||
var step_pulse = entry.x - last_pulse
|
||||
var integral := last_integral
|
||||
if last_ramp:
|
||||
# Treat it as a rectangle where the height is the average of the slanted top.
|
||||
integral += step_pulse * (last_value + entry.y)/2.0
|
||||
else:
|
||||
integral += step_pulse * last_value
|
||||
self.baked_integrals.append(integral)
|
||||
last_pulse = entry.x
|
||||
last_value = entry.y
|
||||
last_integral = integral
|
||||
last_ramp = entry.z > 0
|
||||
|
||||
var last_integral_block_get: int = -1 # Cache previous position for sequential lookups
|
||||
func get_integral(pulse: float) -> float:
|
||||
# This is for tempo -> time. Need to bake it to have any hope of efficiency.
|
||||
if self.baked_integrals.empty():
|
||||
self.bake_integrals()
|
||||
# Find first entry earlier than the pulse
|
||||
for i in range(len(self.entries)-1, -1, -1):
|
||||
var entry = self.entries[i]
|
||||
if pulse > entry.x:
|
||||
var integral = self.baked_integrals[i]
|
||||
var step_pulse = pulse - entry.x
|
||||
if entry.z: # Ramp to next
|
||||
# Treat it as a rectangle where the height is the average of the slanted top.
|
||||
integral += step_pulse * (entry.y + entries[i+1].y)/2.0 # If last entry somehow has ramp-to-next (it shouldn't), this will out-of-range error
|
||||
else:
|
||||
integral += step_pulse * entry.y
|
||||
return integral
|
||||
return 0.0
|
||||
|
||||
|
||||
func render_channels(_t_start: float, _t_end: float, inst_map: Array) -> Array: # [data: PoolByteArray, target_time_length: float in seconds]
|
||||
# Since some channels contain global events (tempo and global volume for now),
|
||||
# the strategy will be to preprocess each channel in a global-state-agnostic way,
|
||||
# then once all the global tracks are known, as well as the longest unlooped length,
|
||||
# do a second pass to generate the final events
|
||||
# self.print_channel_events(inst_map)
|
||||
var instrument_adsrs = RomLoader.snes_data.bgm_instrument_adsrs # TODO: UNHARDCODE THIS
|
||||
var all_note_events = []
|
||||
|
||||
var curve_master_volume := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now
|
||||
var curve_master_tempo := TrackCurve.new(120.0) # bpm is too big, need pulses per second
|
||||
|
||||
var curve_channel_pans := []
|
||||
|
||||
for channel in self.num_tracks:
|
||||
var curve_velocity := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now
|
||||
var curve_pan := TrackCurve.new() # [-1.0, 1.0] for now
|
||||
var channel_note_events = []
|
||||
var track: Array = self.tracks[channel]
|
||||
var l := len(track)
|
||||
var p := 0 # current pulse
|
||||
|
||||
if l == 0: # Empty channel, move on
|
||||
all_note_events.append(channel_note_events)
|
||||
curve_channel_pans.append(curve_pan)
|
||||
continue
|
||||
|
||||
# var num_notes: int = 0
|
||||
var current_instrument := 0
|
||||
var current_octave := 5
|
||||
var current_transpose := 0
|
||||
# var current_velocity := 100
|
||||
var current_adsr_attack := 0
|
||||
var current_adsr_decay := 0
|
||||
var current_adsr_sustain := 0
|
||||
var current_adsr_release := 0
|
||||
|
||||
# First, check if it ends in a GOTO, then store the program counter of the destination
|
||||
var infinite_loop_target_program_counter = -1
|
||||
var infinite_loop_target_pulse = -1
|
||||
if track[-1][0] == EventType.GOTO:
|
||||
infinite_loop_target_program_counter = track[-1][1]
|
||||
|
||||
var program_counter := 0
|
||||
while true: #num_notes < MAX_NOTE_EVENTS:
|
||||
if program_counter >= l:
|
||||
break
|
||||
if program_counter == infinite_loop_target_program_counter:
|
||||
infinite_loop_target_pulse = p
|
||||
var event = track[program_counter]
|
||||
program_counter += 1
|
||||
match event[0]: # Control codes
|
||||
EventType.GOTO: # This is a preprocessed event list, so GOTO is a final infinite loop marker
|
||||
var note_event = NoteEvent.new()
|
||||
note_event.p_start = p
|
||||
note_event.p_end = infinite_loop_target_pulse # Fake final note event using p_start > p_end to encode the infinite jump back loop.
|
||||
# Note that event[1] points to an Event, not a NoteEvent, not a Pulse, so we looked it up earlier
|
||||
channel_note_events.append(note_event)
|
||||
break
|
||||
EventType.MASTER_VOLUME:
|
||||
curve_master_volume.add_point(p, event[1]/255.0, false)
|
||||
EventType.TEMPO:
|
||||
var new_tempo = music.tempo_to_seconds_per_pulse(event[1])
|
||||
curve_master_tempo.add_point(p, new_tempo, false)
|
||||
EventType.TEMPO_SLIDE:
|
||||
var old_tempo = curve_master_tempo.get_pulse(p)
|
||||
var new_tempo = music.tempo_to_seconds_per_pulse(event[2])
|
||||
var slide_duration: int = event[1] # TODO: work out how this is scaled
|
||||
curve_master_tempo.add_point(p, old_tempo, true)
|
||||
curve_master_tempo.add_point(p + slide_duration, new_tempo, false)
|
||||
EventType.NOTE:
|
||||
var note = event[1]
|
||||
var duration = event[2]
|
||||
if note >= 0: # Don't shift or play rests
|
||||
note += (12 * current_octave) + current_transpose
|
||||
var note_event = NoteEvent.new()
|
||||
note_event.p_start = p
|
||||
note_event.p_end = p + duration
|
||||
note_event.instrument = current_instrument
|
||||
note_event.pitch = note # pitch_idx #* self.channel_fine_tuning[channel]
|
||||
note_event.velocity = curve_velocity.get_pulse(p) # current_velocity
|
||||
note_event.adsr_attack = current_adsr_attack
|
||||
note_event.adsr_decay = current_adsr_decay
|
||||
note_event.adsr_sustain = current_adsr_sustain
|
||||
note_event.adsr_release = current_adsr_release
|
||||
channel_note_events.append(note_event)
|
||||
# num_notes += 1
|
||||
elif note == music.NOTE_IS_TIE:
|
||||
channel_note_events[-1].p_end += duration
|
||||
p += duration
|
||||
EventType.VOLUME:
|
||||
var new_velocity: float = event[1]/255.0
|
||||
curve_velocity.add_point(p, new_velocity, false)
|
||||
EventType.VOLUME_SLIDE: # TODO: implement slides
|
||||
var old_velocity = curve_velocity.get_pulse(p)
|
||||
var slide_duration: int = event[1]
|
||||
var new_velocity: float = event[2]/255.0
|
||||
curve_velocity.add_point(p, old_velocity, true)
|
||||
curve_velocity.add_point(p + slide_duration, new_velocity, false)
|
||||
EventType.PAN:
|
||||
var new_pan = 1.0 - event[1]/127.5
|
||||
curve_pan.add_point(p, new_pan, false)
|
||||
EventType.PAN_SLIDE: # TODO: implement slides
|
||||
var old_pan = curve_pan.get_pulse(p)
|
||||
var new_pan = 1.0 - event[2]/127.5
|
||||
var slide_duration: int = event[1] # TODO: work out how slides are scaled
|
||||
curve_pan.add_point(p, old_pan, true)
|
||||
curve_pan.add_point(p + slide_duration, new_pan, false)
|
||||
EventType.PITCH_SLIDE: # TODO: implement slides
|
||||
var slide_duration: int = event[1]
|
||||
var target_pitch: int = event[2] # Signed
|
||||
EventType.OCTAVE:
|
||||
current_octave = event[1]
|
||||
EventType.OCTAVE_UP:
|
||||
current_octave += 1
|
||||
EventType.OCTAVE_DOWN:
|
||||
current_octave -= 1
|
||||
EventType.TRANSPOSE_ABS:
|
||||
current_transpose = event[1]
|
||||
EventType.TRANSPOSE_REL:
|
||||
current_transpose += event[1]
|
||||
EventType.TUNING:
|
||||
var fine_tune: int = event[1]
|
||||
var scale: float
|
||||
if fine_tune < 0x80:
|
||||
scale = 1.0 + fine_tune/255.0
|
||||
else:
|
||||
scale = fine_tune/255.0
|
||||
self.channel_fine_tuning[channel] = scale
|
||||
EventType.PROGCHANGE:
|
||||
var event_idx = event[1]-0x20
|
||||
if event_idx >= 0:
|
||||
current_instrument = inst_map[event_idx] - 1
|
||||
if current_instrument < len(instrument_adsrs) and current_instrument > 0:
|
||||
var adsr = instrument_adsrs[current_instrument]
|
||||
current_adsr_attack = adsr[2]
|
||||
current_adsr_decay = adsr[3]
|
||||
current_adsr_sustain = adsr[0]
|
||||
current_adsr_release = adsr[1]
|
||||
EventType.ADSR_DEFAULT: # TODO - Investigate actual scaling and order
|
||||
if current_instrument < len(instrument_adsrs) and current_instrument > 0:
|
||||
var adsr = instrument_adsrs[current_instrument]
|
||||
current_adsr_attack = adsr[2]
|
||||
current_adsr_decay = adsr[3]
|
||||
current_adsr_sustain = adsr[0]
|
||||
current_adsr_release = adsr[1]
|
||||
EventType.ADSR_ATTACK:
|
||||
current_adsr_attack = event[1]
|
||||
EventType.ADSR_DECAY:
|
||||
current_adsr_decay = event[1]
|
||||
EventType.ADSR_SUSTAIN:
|
||||
current_adsr_sustain = event[1]
|
||||
EventType.ADSR_RELEASE:
|
||||
current_adsr_release = event[1]
|
||||
EventType.VIBRATO_ON:
|
||||
self.channel_vibrato_delay[channel] = event[1]
|
||||
self.channel_vibrato_rate[channel] = event[2]
|
||||
self.channel_vibrato_depth[channel] = event[3]
|
||||
self.channel_vibrato_on[channel] = 1
|
||||
EventType.VIBRATO_OFF:
|
||||
self.channel_vibrato_on[channel] = 0
|
||||
EventType.TREMOLO_ON:
|
||||
self.channel_tremolo_delay[channel] = event[1]
|
||||
self.channel_tremolo_rate[channel] = event[2]
|
||||
self.channel_tremolo_depth[channel] = event[3]
|
||||
self.channel_tremolo_on[channel] = 1
|
||||
EventType.TREMOLO_OFF:
|
||||
self.channel_tremolo_on[channel] = 0
|
||||
EventType.PAN_LFO_ON:
|
||||
self.channel_pan_lfo_depth[channel] = event[1]
|
||||
self.channel_pan_lfo_rate[channel] = event[2]
|
||||
self.channel_pan_lfo_on[channel] = 1
|
||||
EventType.PAN_LFO_OFF:
|
||||
self.channel_pan_lfo_on[channel] = 0
|
||||
EventType.NOISE_FREQ:
|
||||
self.channel_noise_freq[channel] = event[1]
|
||||
EventType.NOISE_ON:
|
||||
self.channel_noise_on[channel] = 1
|
||||
EventType.NOISE_OFF:
|
||||
self.channel_noise_on[channel] = 0
|
||||
EventType.PITCHMOD_ON:
|
||||
self.channel_pitchmod_on[channel] = 1
|
||||
EventType.PITCHMOD_OFF:
|
||||
self.channel_pitchmod_on[channel] = 0
|
||||
EventType.ECHO_ON:
|
||||
self.channel_echo_on[channel] = 1
|
||||
EventType.ECHO_OFF:
|
||||
self.channel_echo_on[channel] = 0
|
||||
EventType.ECHO_VOLUME:
|
||||
self.channel_echo_volume[channel] = event[1]
|
||||
EventType.ECHO_VOLUME_SLIDE: # TODO: implement slides
|
||||
self.channel_echo_volume[channel] = event[2]
|
||||
var slide_duration: int = event[1]
|
||||
EventType.ECHO_FEEDBACK_FIR: # TODO
|
||||
var feedback: int = event[1]
|
||||
var filterIndex: int = event[2]
|
||||
EventType.END:
|
||||
break
|
||||
_:
|
||||
break
|
||||
# End of track
|
||||
all_note_events.append(channel_note_events)
|
||||
curve_channel_pans.append(curve_pan)
|
||||
|
||||
# Integrate tempo so we can get a pulse->time mapping
|
||||
curve_master_tempo.bake_integrals()
|
||||
# Find the longest channel
|
||||
var channel_loop_p_returns = PoolIntArray()
|
||||
var channel_loop_p_lengths = PoolIntArray()
|
||||
var longest_channel_idx = 0
|
||||
var longest_channel_p_end = 0
|
||||
var highest_channel_p_return = -1
|
||||
for channel in self.num_tracks:
|
||||
if all_note_events[channel].empty():
|
||||
channel_loop_p_returns.append(-1)
|
||||
continue
|
||||
var note_event: NoteEvent = all_note_events[channel][-1]
|
||||
var p_end = note_event.p_end
|
||||
if p_end < note_event.p_start:
|
||||
# Ends on infinite loop
|
||||
channel_loop_p_returns.append(p_end)
|
||||
channel_loop_p_lengths.append(note_event.p_start - p_end)
|
||||
if p_end > highest_channel_p_return:
|
||||
highest_channel_p_return = p_end
|
||||
p_end = note_event.p_start
|
||||
else:
|
||||
channel_loop_p_returns.append(-1)
|
||||
channel_loop_p_lengths.append(0)
|
||||
|
||||
if p_end > longest_channel_p_end:
|
||||
longest_channel_p_end = p_end
|
||||
longest_channel_idx = channel
|
||||
|
||||
var target_pulse_length = longest_channel_p_end + 200
|
||||
var target_time_length = curve_master_tempo.get_integral(target_pulse_length)
|
||||
|
||||
# Second pass - encode the notes with the now-known global tempo and volume curves
|
||||
var data := PoolByteArray()
|
||||
for channel in self.num_tracks:
|
||||
var events = all_note_events[channel]
|
||||
var loop_return_note_event_idx = -1
|
||||
var loop_return_p = channel_loop_p_returns[channel]
|
||||
var curve_pan: TrackCurve = curve_channel_pans[channel]
|
||||
|
||||
var midi_events_bytes_t_start := StreamPeerBuffer.new()
|
||||
var midi_events_bytes_t_end := StreamPeerBuffer.new()
|
||||
var midi_events_bytes3 := StreamPeerBuffer.new()
|
||||
var midi_events_bytes_adsr := StreamPeerBuffer.new()
|
||||
|
||||
var num_notes: int = 0
|
||||
var event_ptr := 0
|
||||
var l_events := len(events)
|
||||
var loop_p_offset := 0
|
||||
for i in MAX_NOTE_EVENTS:
|
||||
if event_ptr >= l_events:
|
||||
break
|
||||
if (loop_return_p >= 0) and event_ptr == l_events-1:
|
||||
event_ptr = loop_return_note_event_idx
|
||||
loop_p_offset += channel_loop_p_lengths[channel]
|
||||
var event: NoteEvent = events[event_ptr]
|
||||
var p = event.p_start
|
||||
if loop_return_note_event_idx < 0 and p >= loop_return_p:
|
||||
loop_return_note_event_idx = event_ptr
|
||||
midi_events_bytes_t_start.put_32(int(curve_master_tempo.get_integral(p + loop_p_offset) * 32000))
|
||||
midi_events_bytes_t_end.put_32(int(curve_master_tempo.get_integral(event.p_end + loop_p_offset) * 32000)) # t_end
|
||||
midi_events_bytes3.put_u8(event.instrument)
|
||||
midi_events_bytes3.put_u8(event.pitch)
|
||||
midi_events_bytes3.put_u8(int(event.velocity * curve_master_volume.get_pulse(p) * 255.0)) # velocity
|
||||
midi_events_bytes3.put_u8(int((curve_pan.get_pulse(p)+1.0) * 127.5)) # pan
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_attack)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_decay)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_sustain)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_release)
|
||||
|
||||
event_ptr += 1
|
||||
num_notes += 1
|
||||
# Fill up end of notes array with dummies
|
||||
for i in range(num_notes, MAX_NOTE_EVENTS):
|
||||
midi_events_bytes_t_start.put_32(0x0FFFFFFF)
|
||||
midi_events_bytes_t_end.put_32(0x0FFFFFFF)
|
||||
midi_events_bytes3.put_32(0)
|
||||
midi_events_bytes_adsr.put_32(0)
|
||||
data += midi_events_bytes_t_start.data_array + midi_events_bytes_t_end.data_array + midi_events_bytes3.data_array + midi_events_bytes_adsr.data_array
|
||||
var smp_loop_start = -1
|
||||
var smp_loop_end = -1
|
||||
if highest_channel_p_return > 0:
|
||||
smp_loop_start = curve_master_tempo.get_integral(highest_channel_p_return + 100) * 32000
|
||||
smp_loop_end = curve_master_tempo.get_integral(longest_channel_p_end + 100) * 32000
|
||||
return [data, target_time_length, [smp_loop_start, smp_loop_end]]
|
||||
|
||||
func print_channel_events(inst_map: Array) -> void:
|
||||
for channel in self.num_tracks:
|
||||
print('================Channel %d================'%channel)
|
||||
var track: Array = self.tracks[channel]
|
||||
var l := len(track)
|
||||
var p := 0 # current pulse
|
||||
for event in track: #num_notes < MAX_NOTE_EVENTS:
|
||||
var print_str := 'p=%6d : %s '%[p, EventType.keys()[event[0]]]
|
||||
match event[0]:
|
||||
EventType.NOTE:
|
||||
var note = event[1]
|
||||
var duration = event[2]
|
||||
match note:
|
||||
music.NOTE_IS_REST:
|
||||
print('p=%6d : NOTE_REST %d pulses'%[p, duration])
|
||||
music.NOTE_IS_TIE:
|
||||
print('p=%6d : NOTE_TIE %d pulses'%[p, duration])
|
||||
_:
|
||||
print(print_str, event.slice(1, -1))
|
||||
p += duration
|
||||
EventType.PROGCHANGE:
|
||||
var event_idx = event[1]-0x20
|
||||
if event_idx >= 0:
|
||||
print(print_str, ' instrument %02d'%(inst_map[event_idx] - 1))
|
||||
else:
|
||||
print(print_str, event.slice(1, -1))
|
||||
_:
|
||||
print(print_str, event.slice(1, -1))
|
||||
|
|
|
@ -0,0 +1,450 @@
|
|||
#warning-ignore-all:shadowed_variable
|
||||
extends Node
|
||||
const music := preload('res://scripts/loaders/snes/music_ff5.gd')
|
||||
const EventType := music.EventType
|
||||
var MUSIC := music.new()
|
||||
const NUM_TRACKS := 8 # TODO
|
||||
const MAX_NOTE_EVENTS := 2048
|
||||
class NoteEvent:
|
||||
var p_start: int # In pulse space
|
||||
var p_end: int
|
||||
var instrument: int
|
||||
var pitch: int
|
||||
var velocity: float
|
||||
var adsr_attack: int
|
||||
var adsr_decay: int
|
||||
var adsr_sustain: int
|
||||
var adsr_release: int
|
||||
|
||||
|
||||
class TrackCurve: # built-in Curve class is too restrictive for this
|
||||
var default: float
|
||||
var entries: PoolVector3Array
|
||||
var baked_integrals: PoolRealArray
|
||||
func _init(default: float = 0.0):
|
||||
self.default = default
|
||||
self.entries = PoolVector3Array()
|
||||
self.baked_integrals = PoolRealArray()
|
||||
|
||||
func add_point(pulse: int, value: float, ramp_to_next: bool = false) -> void:
|
||||
var l := len(self.entries)
|
||||
var entry := Vector3(float(pulse), value, float(ramp_to_next))
|
||||
if l == 0 or self.entries[-1].x < pulse:
|
||||
self.entries.append(entry)
|
||||
else: # Find the first entry bigger than pulse, and insert before
|
||||
for i in l:
|
||||
if self.entries[i].x > pulse:
|
||||
self.entries.insert(i, entry)
|
||||
break
|
||||
|
||||
var last_pulse_block_get: int = -1 # Cache previous position for sequential lookups
|
||||
func get_pulse(pulse: float) -> float:
|
||||
var l := len(self.entries)
|
||||
if l == 0 or pulse < self.entries[-1].x:
|
||||
return self.default
|
||||
if pulse > self.entries[-1].x:
|
||||
return self.entries[-1].y
|
||||
for i in l-2:
|
||||
# Find first entry beyond
|
||||
if pulse < self.entries[i+1].x:
|
||||
if self.entries[i].z > 0: # ramp_to_next
|
||||
return range_lerp(pulse, self.entries[i].x, self.entries[i+1].x, self.entries[i].y, self.entries[i+1].y)
|
||||
else:
|
||||
return self.entries[i].y
|
||||
return self.default # Should be unreachable
|
||||
|
||||
func bake_integrals():
|
||||
# Store the starting integrated value (i.e. time for the tempo curve) of each pulse value
|
||||
self.baked_integrals.clear()
|
||||
var last_pulse := 0.0
|
||||
var last_value := self.default
|
||||
var last_integral := 0.0
|
||||
var last_ramp := false
|
||||
for entry in self.entries:
|
||||
var step_pulse = entry.x - last_pulse
|
||||
var integral := last_integral
|
||||
if last_ramp:
|
||||
# Treat it as a rectangle where the height is the average of the slanted top.
|
||||
integral += step_pulse * (last_value + entry.y)/2.0
|
||||
else:
|
||||
integral += step_pulse * last_value
|
||||
self.baked_integrals.append(integral)
|
||||
last_pulse = entry.x
|
||||
last_value = entry.y
|
||||
last_integral = integral
|
||||
last_ramp = entry.z > 0
|
||||
|
||||
var last_integral_block_get: int = -1 # Cache previous position for sequential lookups
|
||||
func get_integral(pulse: float) -> float:
|
||||
# This is for tempo -> time. Need to bake it to have any hope of efficiency.
|
||||
if self.baked_integrals.empty():
|
||||
self.bake_integrals()
|
||||
# Find first entry earlier than the pulse
|
||||
for i in range(len(self.entries)-1, -1, -1):
|
||||
var entry = self.entries[i]
|
||||
if pulse > entry.x:
|
||||
var integral = self.baked_integrals[i]
|
||||
var step_pulse = pulse - entry.x
|
||||
if entry.z: # Ramp to next
|
||||
# Treat it as a rectangle where the height is the average of the slanted top.
|
||||
integral += step_pulse * (entry.y + entries[i+1].y)/2.0 # If last entry somehow has ramp-to-next (it shouldn't), this will out-of-range error
|
||||
else:
|
||||
integral += step_pulse * entry.y
|
||||
return integral
|
||||
return 0.0
|
||||
|
||||
|
||||
static func render_channels(tracks: Array, inst_map: Array, debug_name := 'none') -> Array: # [data: PoolByteArray, target_time_length: float in seconds]
|
||||
# Since some channels contain global events (tempo and global volume for now),
|
||||
# the strategy will be to preprocess each channel in a global-state-agnostic way,
|
||||
# then once all the global tracks are known, as well as the longest unlooped length,
|
||||
# do a second pass to generate the final events
|
||||
# self.print_channel_events(inst_map)
|
||||
var instrument_adsrs = RomLoader.snes_data.bgm_instrument_adsrs # TODO: UNHARDCODE THIS
|
||||
var all_note_events = []
|
||||
|
||||
var curve_master_volume := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now
|
||||
var curve_master_tempo := TrackCurve.new(120.0) # bpm is too big, need pulses per second
|
||||
|
||||
var curve_channel_pans := []
|
||||
|
||||
for channel in NUM_TRACKS:
|
||||
var curve_velocity := TrackCurve.new(100.0/255.0) # [0.0, 1.0] for now
|
||||
var curve_pan := TrackCurve.new() # [-1.0, 1.0] for now
|
||||
# Stored and unused for now
|
||||
var curve_fine_tuning := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_vibrato_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_vibrato_delay := TrackCurve.new()
|
||||
var curve_vibrato_rate := TrackCurve.new()
|
||||
var curve_vibrato_depth := TrackCurve.new()
|
||||
var curve_tremolo_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_tremolo_delay := TrackCurve.new()
|
||||
var curve_tremolo_rate := TrackCurve.new()
|
||||
var curve_tremolo_depth := TrackCurve.new()
|
||||
var curve_pan_lfo_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_pan_lfo_rate := TrackCurve.new()
|
||||
var curve_pan_lfo_depth := TrackCurve.new()
|
||||
var curve_noise_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_noise_freq := TrackCurve.new()
|
||||
var curve_pitchmod_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_echo_on := TrackCurve.new() # [0.0, 1.0] for now
|
||||
var curve_echo_volume := TrackCurve.new()
|
||||
|
||||
var channel_note_events = []
|
||||
var track: Array = tracks[channel]
|
||||
var l := len(track)
|
||||
var p := 0 # current pulse
|
||||
|
||||
if l == 0: # Empty channel, move on
|
||||
all_note_events.append(channel_note_events)
|
||||
curve_channel_pans.append(curve_pan)
|
||||
continue
|
||||
|
||||
# var num_notes: int = 0
|
||||
var current_instrument := 0
|
||||
var current_octave := 5
|
||||
var current_transpose := 0
|
||||
# var current_velocity := 100
|
||||
var current_adsr_attack := 0
|
||||
var current_adsr_decay := 0
|
||||
var current_adsr_sustain := 0
|
||||
var current_adsr_release := 0
|
||||
|
||||
# First, check if it ends in a GOTO, then store the program counter of the destination
|
||||
var infinite_loop_target_program_counter = -1
|
||||
var infinite_loop_target_pulse = -1
|
||||
if track[-1][0] == EventType.GOTO:
|
||||
infinite_loop_target_program_counter = track[-1][1]
|
||||
|
||||
var program_counter := 0
|
||||
while true: #num_notes < MAX_NOTE_EVENTS:
|
||||
if program_counter >= l:
|
||||
break
|
||||
if program_counter == infinite_loop_target_program_counter:
|
||||
infinite_loop_target_pulse = p
|
||||
var event = track[program_counter]
|
||||
program_counter += 1
|
||||
match event[0]: # Control codes
|
||||
EventType.GOTO: # This is a preprocessed event list, so GOTO is a final infinite loop marker
|
||||
var note_event = NoteEvent.new()
|
||||
note_event.p_start = p
|
||||
note_event.p_end = infinite_loop_target_pulse # Fake final note event using p_start > p_end to encode the infinite jump back loop.
|
||||
# Note that event[1] points to an Event, not a NoteEvent, not a Pulse, so we looked it up earlier
|
||||
channel_note_events.append(note_event)
|
||||
break
|
||||
EventType.MASTER_VOLUME:
|
||||
curve_master_volume.add_point(p, event[1]/255.0, false)
|
||||
EventType.TEMPO:
|
||||
var new_tempo = music.tempo_to_seconds_per_pulse(event[1])
|
||||
curve_master_tempo.add_point(p, new_tempo, false)
|
||||
EventType.TEMPO_SLIDE:
|
||||
var old_tempo = curve_master_tempo.get_pulse(p)
|
||||
var new_tempo = music.tempo_to_seconds_per_pulse(event[2])
|
||||
var slide_duration: int = event[1] # TODO: work out how this is scaled
|
||||
curve_master_tempo.add_point(p, old_tempo, true)
|
||||
curve_master_tempo.add_point(p + slide_duration, new_tempo, false)
|
||||
EventType.NOTE:
|
||||
var note = event[1]
|
||||
var duration = event[2]
|
||||
if note >= 0: # Don't shift or play rests
|
||||
note += (12 * current_octave) + current_transpose
|
||||
var note_event = NoteEvent.new()
|
||||
note_event.p_start = p
|
||||
note_event.p_end = p + duration
|
||||
note_event.instrument = current_instrument
|
||||
note_event.pitch = note # pitch_idx #* curve_fine_tuning
|
||||
note_event.velocity = curve_velocity.get_pulse(p) # current_velocity
|
||||
note_event.adsr_attack = current_adsr_attack
|
||||
note_event.adsr_decay = current_adsr_decay
|
||||
note_event.adsr_sustain = current_adsr_sustain
|
||||
note_event.adsr_release = current_adsr_release
|
||||
channel_note_events.append(note_event)
|
||||
# num_notes += 1
|
||||
elif note == music.NOTE_IS_TIE:
|
||||
if not channel_note_events:
|
||||
print('Encountered a tie with no preceeding note! %s channel %d pulse %d (loop return is %d)' % [debug_name, channel, p, infinite_loop_target_pulse])
|
||||
else:
|
||||
if channel_note_events[-1].p_end != p:
|
||||
print('Encountered a tie with preceeding rest! %s channel %d pulse %d (loop return is %d)' % [debug_name, channel, p, infinite_loop_target_pulse])
|
||||
channel_note_events[-1].p_end += duration
|
||||
p += duration
|
||||
EventType.VOLUME:
|
||||
var new_velocity: float = event[1]/255.0
|
||||
curve_velocity.add_point(p, new_velocity, false)
|
||||
EventType.VOLUME_SLIDE: # TODO: implement slides
|
||||
var old_velocity = curve_velocity.get_pulse(p)
|
||||
var slide_duration: int = event[1]
|
||||
var new_velocity: float = event[2]/255.0
|
||||
curve_velocity.add_point(p, old_velocity, true)
|
||||
curve_velocity.add_point(p + slide_duration, new_velocity, false)
|
||||
EventType.PAN:
|
||||
var new_pan = 1.0 - event[1]/127.5
|
||||
curve_pan.add_point(p, new_pan, false)
|
||||
EventType.PAN_SLIDE: # TODO: implement slides
|
||||
var old_pan = curve_pan.get_pulse(p)
|
||||
var new_pan = 1.0 - event[2]/127.5
|
||||
var slide_duration: int = event[1] # TODO: work out how slides are scaled
|
||||
curve_pan.add_point(p, old_pan, true)
|
||||
curve_pan.add_point(p + slide_duration, new_pan, false)
|
||||
EventType.PITCH_SLIDE: # TODO: implement slides
|
||||
var slide_duration: int = event[1]
|
||||
var target_pitch: int = event[2] # Signed
|
||||
EventType.OCTAVE:
|
||||
current_octave = event[1]
|
||||
EventType.OCTAVE_UP:
|
||||
current_octave += 1
|
||||
EventType.OCTAVE_DOWN:
|
||||
current_octave -= 1
|
||||
EventType.TRANSPOSE_ABS:
|
||||
current_transpose = event[1]
|
||||
EventType.TRANSPOSE_REL:
|
||||
current_transpose += event[1]
|
||||
EventType.TUNING:
|
||||
var fine_tune: int = event[1]
|
||||
var scale: float
|
||||
if fine_tune < 0x80:
|
||||
scale = 1.0 + fine_tune/255.0
|
||||
else:
|
||||
scale = fine_tune/255.0
|
||||
curve_fine_tuning.add_point(p, scale)
|
||||
EventType.PROGCHANGE:
|
||||
var event_idx = event[1]-0x20
|
||||
if event_idx >= 0:
|
||||
current_instrument = inst_map[event_idx] - 1
|
||||
if current_instrument < len(instrument_adsrs) and current_instrument > 0:
|
||||
var adsr = instrument_adsrs[current_instrument]
|
||||
current_adsr_attack = adsr[2]
|
||||
current_adsr_decay = adsr[3]
|
||||
current_adsr_sustain = adsr[0]
|
||||
current_adsr_release = adsr[1]
|
||||
EventType.ADSR_DEFAULT: # TODO - Investigate actual scaling and order
|
||||
if current_instrument < len(instrument_adsrs) and current_instrument > 0:
|
||||
var adsr = instrument_adsrs[current_instrument]
|
||||
current_adsr_attack = adsr[2]
|
||||
current_adsr_decay = adsr[3]
|
||||
current_adsr_sustain = adsr[0]
|
||||
current_adsr_release = adsr[1]
|
||||
EventType.ADSR_ATTACK:
|
||||
current_adsr_attack = event[1]
|
||||
EventType.ADSR_DECAY:
|
||||
current_adsr_decay = event[1]
|
||||
EventType.ADSR_SUSTAIN:
|
||||
current_adsr_sustain = event[1]
|
||||
EventType.ADSR_RELEASE:
|
||||
current_adsr_release = event[1]
|
||||
EventType.VIBRATO_ON:
|
||||
curve_vibrato_delay.add_point(p, event[1])
|
||||
curve_vibrato_rate.add_point(p, event[2])
|
||||
curve_vibrato_depth.add_point(p, event[3])
|
||||
curve_vibrato_on.add_point(p, 1)
|
||||
EventType.VIBRATO_OFF:
|
||||
curve_vibrato_on.add_point(p, 0)
|
||||
EventType.TREMOLO_ON:
|
||||
curve_tremolo_delay.add_point(p, event[1])
|
||||
curve_tremolo_rate.add_point(p, event[2])
|
||||
curve_tremolo_depth.add_point(p, event[3])
|
||||
curve_tremolo_on.add_point(p, 1)
|
||||
EventType.TREMOLO_OFF:
|
||||
curve_tremolo_on.add_point(p, 0)
|
||||
EventType.PAN_LFO_ON:
|
||||
curve_pan_lfo_depth.add_point(p, event[1])
|
||||
curve_pan_lfo_rate.add_point(p, event[2])
|
||||
curve_pan_lfo_on.add_point(p, 1)
|
||||
EventType.PAN_LFO_OFF:
|
||||
curve_pan_lfo_on.add_point(p, 0)
|
||||
EventType.NOISE_FREQ:
|
||||
curve_noise_freq.add_point(p, event[1])
|
||||
EventType.NOISE_ON:
|
||||
curve_noise_on.add_point(p, 1)
|
||||
EventType.NOISE_OFF:
|
||||
curve_noise_on.add_point(p, 0)
|
||||
EventType.PITCHMOD_ON:
|
||||
curve_pitchmod_on.add_point(p, 1)
|
||||
EventType.PITCHMOD_OFF:
|
||||
curve_pitchmod_on.add_point(p, 0)
|
||||
EventType.ECHO_ON:
|
||||
curve_echo_on.add_point(p, 1)
|
||||
EventType.ECHO_OFF:
|
||||
curve_echo_on.add_point(p, 0)
|
||||
EventType.ECHO_VOLUME:
|
||||
curve_echo_volume.add_point(p, event[1])
|
||||
EventType.ECHO_VOLUME_SLIDE:
|
||||
var slide_duration: int = event[1] # TODO: work out how slides are scaled
|
||||
var old_echo_volume = curve_echo_volume.get_pulse(p)
|
||||
var new_echo_volume = event[2]
|
||||
curve_echo_volume.add_point(p, old_echo_volume, true)
|
||||
curve_echo_volume.add_point(p + slide_duration, new_echo_volume)
|
||||
EventType.ECHO_FEEDBACK_FIR: # TODO
|
||||
var feedback: int = event[1]
|
||||
var filterIndex: int = event[2]
|
||||
EventType.END:
|
||||
break
|
||||
_:
|
||||
break
|
||||
# End of track
|
||||
all_note_events.append(channel_note_events)
|
||||
curve_channel_pans.append(curve_pan)
|
||||
|
||||
# Integrate tempo so we can get a pulse->time mapping
|
||||
curve_master_tempo.bake_integrals()
|
||||
# Find the longest channel
|
||||
var channel_loop_p_returns = PoolIntArray()
|
||||
var channel_loop_p_lengths = PoolIntArray()
|
||||
var longest_channel_idx = 0
|
||||
var longest_channel_p_end = 0
|
||||
var highest_channel_p_return = -1
|
||||
for channel in NUM_TRACKS:
|
||||
if all_note_events[channel].empty():
|
||||
channel_loop_p_returns.append(-1)
|
||||
continue
|
||||
var note_event: NoteEvent = all_note_events[channel][-1]
|
||||
var p_end = note_event.p_end
|
||||
if p_end < note_event.p_start:
|
||||
# Ends on infinite loop
|
||||
channel_loop_p_returns.append(p_end)
|
||||
channel_loop_p_lengths.append(note_event.p_start - p_end)
|
||||
if p_end > highest_channel_p_return:
|
||||
highest_channel_p_return = p_end
|
||||
p_end = note_event.p_start
|
||||
else:
|
||||
channel_loop_p_returns.append(-1)
|
||||
channel_loop_p_lengths.append(0)
|
||||
|
||||
if p_end > longest_channel_p_end:
|
||||
longest_channel_p_end = p_end
|
||||
longest_channel_idx = channel
|
||||
|
||||
var target_pulse_length = longest_channel_p_end + 200
|
||||
var target_time_length = curve_master_tempo.get_integral(target_pulse_length)
|
||||
|
||||
# Second pass - encode the notes with the now-known global tempo and volume curves
|
||||
var data := PoolByteArray()
|
||||
for channel in NUM_TRACKS:
|
||||
var events = all_note_events[channel]
|
||||
var loop_return_note_event_idx = -1
|
||||
var loop_return_p = channel_loop_p_returns[channel]
|
||||
var curve_pan: TrackCurve = curve_channel_pans[channel]
|
||||
|
||||
var midi_events_bytes_t_start := StreamPeerBuffer.new()
|
||||
var midi_events_bytes_t_end := StreamPeerBuffer.new()
|
||||
var midi_events_bytes3 := StreamPeerBuffer.new()
|
||||
var midi_events_bytes_adsr := StreamPeerBuffer.new()
|
||||
|
||||
var num_notes: int = 0
|
||||
var event_ptr := 0
|
||||
var l_events := len(events)
|
||||
var loop_p_offset := 0
|
||||
for i in MAX_NOTE_EVENTS:
|
||||
if event_ptr >= l_events:
|
||||
break
|
||||
if (loop_return_p >= 0) and event_ptr == l_events-1:
|
||||
event_ptr = loop_return_note_event_idx
|
||||
loop_p_offset += channel_loop_p_lengths[channel]
|
||||
var event: NoteEvent = events[event_ptr]
|
||||
var p = event.p_start
|
||||
if loop_return_note_event_idx < 0 and p >= loop_return_p:
|
||||
loop_return_note_event_idx = event_ptr
|
||||
midi_events_bytes_t_start.put_32(int(curve_master_tempo.get_integral(p + loop_p_offset) * 32000))
|
||||
midi_events_bytes_t_end.put_32(int(curve_master_tempo.get_integral(event.p_end + loop_p_offset) * 32000)) # t_end
|
||||
midi_events_bytes3.put_u8(event.instrument)
|
||||
midi_events_bytes3.put_u8(event.pitch)
|
||||
midi_events_bytes3.put_u8(int(event.velocity * curve_master_volume.get_pulse(p) * 255.0)) # velocity
|
||||
midi_events_bytes3.put_u8(int((curve_pan.get_pulse(p)+1.0) * 127.5)) # pan
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_attack)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_decay)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_sustain)
|
||||
midi_events_bytes_adsr.put_u8(event.adsr_release)
|
||||
|
||||
event_ptr += 1
|
||||
num_notes += 1
|
||||
# Fill up end of notes array with dummies
|
||||
for i in range(num_notes, MAX_NOTE_EVENTS):
|
||||
midi_events_bytes_t_start.put_32(0x0FFFFFFF)
|
||||
midi_events_bytes_t_end.put_32(0x0FFFFFFF)
|
||||
midi_events_bytes3.put_32(0)
|
||||
midi_events_bytes_adsr.put_32(0)
|
||||
data += midi_events_bytes_t_start.data_array + midi_events_bytes_t_end.data_array + midi_events_bytes3.data_array + midi_events_bytes_adsr.data_array
|
||||
var smp_loop_start = -1
|
||||
var smp_loop_end = -1
|
||||
if highest_channel_p_return > 0:
|
||||
smp_loop_start = curve_master_tempo.get_integral(highest_channel_p_return + 100) * 32000
|
||||
smp_loop_end = curve_master_tempo.get_integral(longest_channel_p_end + 100) * 32000
|
||||
return [data, target_time_length, [smp_loop_start, smp_loop_end]]
|
||||
|
||||
|
||||
static func disassemble_channel_events(channel_events: Array, inst_map: Array) -> PoolStringArray:
|
||||
var output := PoolStringArray()
|
||||
var p := 0 # current pulse
|
||||
for event in channel_events:
|
||||
var print_str := 'p=%6d : %s '%[p, EventType.keys()[event[0]]]
|
||||
var print_str2 := str(event.slice(1, -1))
|
||||
match event[0]:
|
||||
EventType.NOTE:
|
||||
var note = event[1]
|
||||
var duration = event[2]
|
||||
match note:
|
||||
music.NOTE_IS_REST:
|
||||
output.append('p=%6d : NOTE_REST %d pulses'%[p, duration])
|
||||
music.NOTE_IS_TIE:
|
||||
output.append('p=%6d : NOTE_TIE %d pulses'%[p, duration])
|
||||
_:
|
||||
output.append(print_str + print_str2)
|
||||
p += duration
|
||||
EventType.PROGCHANGE:
|
||||
var event_idx = event[1]-0x20
|
||||
if event_idx >= 0:
|
||||
output.append(print_str + 'instrument %02d'%(inst_map[event_idx] - 1))
|
||||
else:
|
||||
output.append(print_str + print_str2)
|
||||
_:
|
||||
output.append(print_str + print_str2)
|
||||
return output
|
||||
|
||||
static func disassemble_bgm(tracks: Array, inst_map: Array) -> PoolStringArray:
|
||||
var output := PoolStringArray()
|
||||
var channel := 0
|
||||
for channel_events in tracks:
|
||||
output.append('================Channel %d================'%channel)
|
||||
channel += 1
|
||||
output.append_array(disassemble_channel_events(channel_events, inst_map))
|
||||
return output
|
|
@ -1,48 +1,26 @@
|
|||
extends Control
|
||||
|
||||
signal render_initial_ready(key) # A small chunk at the start has been rendered and is ready to play
|
||||
signal render_complete(key) # The full track has been rendered and is ready to pop-in
|
||||
|
||||
const INPUT_TEX_WIDTH := 2048
|
||||
const INPUT_FORMAT := Image.FORMAT_RGBA8 # Image.FORMAT_LA8
|
||||
const INPUT_BYTES_PER_TEXEL := 4 # 2
|
||||
const OUTPUT_BYTES_PER_TEXEL := 4
|
||||
const OUTPUT_FRAMEBUFFER_SIZE := Vector2(4096, 4096)
|
||||
const OUTPUT_WIDTH := int(OUTPUT_FRAMEBUFFER_SIZE.x)
|
||||
const OUTPUT_HEIGHT := int(OUTPUT_FRAMEBUFFER_SIZE.y)
|
||||
const QUAD_COLOR := PoolColorArray([Color.white, Color.white, Color.white, Color.white])
|
||||
var viewport: Viewport
|
||||
var render_queue: Array # of Images
|
||||
var result_queue: Array # of [String, PoolByteArray]
|
||||
var current_textures: Array # of ImageTextures - Needed to prevent GC before draw
|
||||
var waiting_for_viewport: Array
|
||||
var done_first_draw: bool
|
||||
var OUTPUT_FRAMEBUFFER_SIZE: Vector2
|
||||
var OUTPUT_WIDTH: int
|
||||
var OUTPUT_HEIGHT: int
|
||||
onready var viewport: Viewport = self.get_parent()
|
||||
onready var render_queue: Array = [] # of [desc key, remaining_samples]
|
||||
onready var cached_midis: Dictionary = {} # desc: [target_samples, ImageTexture]
|
||||
onready var cached_renders: Dictionary = {} # desc: [remaining_samples, PoolByteArray]
|
||||
onready var current_textures: Array = [] # of ImageTextures - Needed to prevent GC before draw
|
||||
onready var waiting_for_viewport: Array = []
|
||||
onready var done_first_draw: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
self.viewport = get_parent()
|
||||
self.render_queue = []
|
||||
self.result_queue = []
|
||||
self.waiting_for_viewport = []
|
||||
self.done_first_draw = false
|
||||
self.current_textures = []
|
||||
self.get_parent().size = OUTPUT_FRAMEBUFFER_SIZE
|
||||
self.material.set_shader_param('OUTPUT_FRAMEBUFFER_SIZE', OUTPUT_FRAMEBUFFER_SIZE)
|
||||
self.material.set_shader_param('INT_OUTPUT_WIDTH', OUTPUT_WIDTH)
|
||||
|
||||
func push_image(img: Image, target_samples: int = -1, desc: String = '') -> void:
|
||||
var target_rows = ceil(target_samples/float(OUTPUT_WIDTH))
|
||||
if target_samples <= 0:
|
||||
target_rows = int(img.get_size().y)
|
||||
self.render_queue.append([img, target_rows, desc])
|
||||
|
||||
func push_bytes(data: PoolByteArray, target_samples: int = -1, desc: String = '') -> void:
|
||||
var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2))))
|
||||
var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_FORMAT
|
||||
while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway
|
||||
data.append(0)
|
||||
var image := Image.new()
|
||||
image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data)
|
||||
var target_rows = ceil(target_samples/float(OUTPUT_WIDTH))
|
||||
if target_samples <= 0:
|
||||
target_rows = rows
|
||||
self.render_queue.append([image, target_rows, desc])
|
||||
self._update_viewport(4096, 4096)
|
||||
|
||||
func _process(_delta) -> void:
|
||||
update()
|
||||
|
@ -58,35 +36,77 @@ func _draw() -> void:
|
|||
# otherwise, this picks it up the following frame
|
||||
get_result()
|
||||
|
||||
if not self.render_queue:
|
||||
return
|
||||
if self.render_queue:
|
||||
self._render_in_batch()
|
||||
# self._render_one_at_a_time()
|
||||
|
||||
|
||||
func _update_viewport(width: int, height: int) -> void:
|
||||
self.OUTPUT_WIDTH = width
|
||||
self.OUTPUT_HEIGHT = height
|
||||
self.OUTPUT_FRAMEBUFFER_SIZE = Vector2(width, height)
|
||||
self.viewport.size = OUTPUT_FRAMEBUFFER_SIZE
|
||||
self.material.set_shader_param('OUTPUT_FRAMEBUFFER_SIZE', OUTPUT_FRAMEBUFFER_SIZE)
|
||||
self.material.set_shader_param('INT_OUTPUT_WIDTH', OUTPUT_WIDTH)
|
||||
|
||||
|
||||
func _render_midi(key: String, output_rows_drawn_including_this: int, rows_to_draw: int) -> void:
|
||||
var target_samples_and_tex = self.cached_midis[key]
|
||||
var target_samples: int = target_samples_and_tex[0]
|
||||
var tex: ImageTexture = target_samples_and_tex[1]
|
||||
var y_top: int = OUTPUT_HEIGHT - output_rows_drawn_including_this
|
||||
var y_bot: int = y_top + rows_to_draw
|
||||
var uv_inv_v: float = 1 - (rows_to_draw / OUTPUT_FRAMEBUFFER_SIZE.y)
|
||||
var uvs := PoolVector2Array([Vector2(0, uv_inv_v), Vector2(1, uv_inv_v), Vector2(1, 1), Vector2(0, 1)])
|
||||
var points := PoolVector2Array([Vector2(0, y_top), Vector2(OUTPUT_WIDTH, y_top), Vector2(OUTPUT_WIDTH, y_bot), Vector2(0, y_bot)])
|
||||
draw_primitive(points, QUAD_COLOR, uvs, tex)
|
||||
self.waiting_for_viewport.append([rows_to_draw, key]) # Grab the result next draw
|
||||
|
||||
|
||||
func _render_in_batch() -> void:
|
||||
# self._update_viewport(4096, 4096)
|
||||
self.waiting_for_viewport = []
|
||||
var rows_drawn := 0
|
||||
while self.render_queue:
|
||||
var draw_rows: int = self.render_queue[0][1]
|
||||
rows_drawn += draw_rows
|
||||
var target_samples: int = self.render_queue[0][1]
|
||||
var rows_to_draw := int(ceil(target_samples/float(OUTPUT_WIDTH)))
|
||||
rows_drawn += rows_to_draw
|
||||
if rows_drawn > OUTPUT_HEIGHT:
|
||||
if self.waiting_for_viewport.empty():
|
||||
print('Could not fit %s into %dx%d output framebuffer, it needs %d rows'%[self.render_queue[0][2], OUTPUT_WIDTH, OUTPUT_HEIGHT, draw_rows])
|
||||
print('Could not fit %s into %dx%d output framebuffer, it needs %d rows'%[self.render_queue[0][2], OUTPUT_WIDTH, OUTPUT_HEIGHT, rows_to_draw])
|
||||
self.render_queue.pop_front()
|
||||
break
|
||||
|
||||
# Draw the next ImageTexture
|
||||
var image_and_uv_rows_and_desc = self.render_queue.pop_front()
|
||||
var i := len(self.waiting_for_viewport)
|
||||
if len(self.current_textures) < i+1:
|
||||
self.current_textures.append(ImageTexture.new())
|
||||
var tex: ImageTexture = self.current_textures[i]
|
||||
tex.create_from_image(image_and_uv_rows_and_desc[0], 0)
|
||||
self.material.set_shader_param('midi_events_size', tex.get_size())
|
||||
var y_top: int = OUTPUT_HEIGHT - rows_drawn
|
||||
var y_bot: int = y_top + draw_rows
|
||||
var uv_inv_v: float = 1 - (draw_rows / OUTPUT_FRAMEBUFFER_SIZE.y)
|
||||
var uvs := PoolVector2Array([Vector2(0, uv_inv_v), Vector2(1, uv_inv_v), Vector2(1, 1), Vector2(0, 1)])
|
||||
var points := PoolVector2Array([Vector2(0, y_top), Vector2(OUTPUT_WIDTH, y_top), Vector2(OUTPUT_WIDTH, y_bot), Vector2(0, y_bot)])
|
||||
draw_primitive(points, QUAD_COLOR, uvs, tex)
|
||||
self.waiting_for_viewport.append([draw_rows, image_and_uv_rows_and_desc[2]]) # Grab the result next draw
|
||||
self._render_midi(self.render_queue.pop_front()[0], rows_drawn, rows_to_draw)
|
||||
|
||||
|
||||
func _render_one_at_a_time() -> void: # Non power-of-two dimensioned textures should be restricted to GLES3
|
||||
self.waiting_for_viewport = []
|
||||
var rows_drawn := 0
|
||||
if self.render_queue:
|
||||
var target_samples: int = self.render_queue[0][1]
|
||||
var rows_to_draw := int(ceil(target_samples/float(OUTPUT_WIDTH)))
|
||||
self._update_viewport(4096, rows_to_draw)
|
||||
rows_drawn += rows_to_draw
|
||||
# Draw the next ImageTexture
|
||||
self._render_midi(self.render_queue.pop_front()[0], rows_drawn, rows_to_draw)
|
||||
|
||||
|
||||
func push_image(image: Image, target_samples: int, desc: String) -> void:
|
||||
var tex := ImageTexture.new()
|
||||
tex.create_from_image(image, 0)
|
||||
self.cached_midis[desc] = [target_samples, tex]
|
||||
self.material.set_shader_param('midi_events_size', tex.get_size()) # Should all be the same size for now, revisit if we need mixed sizes.
|
||||
self.render_queue.append([desc, target_samples])
|
||||
|
||||
func push_bytes(data: PoolByteArray, target_samples: int, desc: String) -> void:
|
||||
var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2))))
|
||||
var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_FORMAT
|
||||
while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway
|
||||
data.append(0)
|
||||
var image := Image.new()
|
||||
image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data)
|
||||
self.push_image(image, target_samples, desc)
|
||||
|
||||
|
||||
func get_result() -> void:
|
||||
|
@ -97,11 +117,13 @@ func get_result() -> void:
|
|||
var retrieved_rows := 0
|
||||
for rows_and_desc in self.waiting_for_viewport:
|
||||
var entry_rows: int = rows_and_desc[0]
|
||||
var entry_desc: String = rows_and_desc[1]
|
||||
var key: String = rows_and_desc[1]
|
||||
var bytes_start := retrieved_rows * OUTPUT_WIDTH * OUTPUT_BYTES_PER_TEXEL
|
||||
var bytes_end := (retrieved_rows + entry_rows) * OUTPUT_WIDTH * OUTPUT_BYTES_PER_TEXEL
|
||||
var entry_bytes := result_bytes.subarray(bytes_start, bytes_end-1)
|
||||
self.result_queue.append([entry_desc, entry_bytes])
|
||||
self.cached_renders[key] = [0, entry_bytes]
|
||||
emit_signal('render_initial_ready', key)
|
||||
emit_signal('render_complete', key)
|
||||
retrieved_rows += entry_rows
|
||||
# result_bytes.resize(result_byte_count)
|
||||
self.waiting_for_viewport = []
|
||||
|
|
|
@ -2,6 +2,7 @@ extends Node2D
|
|||
#warning-ignore-all:return_value_discarded
|
||||
signal exit
|
||||
const MusicPlayer := preload('res://scripts/MusicPlayer.gd')
|
||||
const MusicRenderer := preload('res://scripts/MusicRenderer.gd')
|
||||
var MusicLoader := preload('res://scripts/loaders/snes/music_ff5.gd').new()
|
||||
onready var bgm_titles := Common.load_glyph_table('res://data/5/bgm_titles.txt')
|
||||
onready var audio_renderer := $'%audio_renderer'
|
||||
|
@ -13,6 +14,8 @@ var sfx_buttons = []
|
|||
var initialized_instrument_texture := false
|
||||
var queued_bgm_playback := ''
|
||||
|
||||
# TODO: Add a tempo slider, a uniform in the shader for tempo scale, and use these to demo JAOT rendering
|
||||
|
||||
const NUM_CHANNELS := 8
|
||||
var music_player = null
|
||||
var inst_sample_map := {}
|
||||
|
@ -160,8 +163,10 @@ func _ready() -> void:
|
|||
self._create_bgm_playback()
|
||||
$btn_stop.connect('pressed', self, '_stop_all')
|
||||
$btn_exit.connect('pressed', self, '_exit')
|
||||
for i in len(RomLoader.snes_data.bgm_song_pointers):
|
||||
var pointer = RomLoader.snes_data.bgm_song_pointers[i]
|
||||
audio_renderer.connect('render_initial_ready', self, '_on_render_initial_ready')
|
||||
audio_renderer.connect('render_complete', self, '_on_render_complete')
|
||||
# for i in len(RomLoader.snes_data.bgm_song_pointers):
|
||||
# var pointer = RomLoader.snes_data.bgm_song_pointers[i]
|
||||
# print('BGM 0x%02X (%02d) at 0x%06X' % [i, i, pointer])
|
||||
|
||||
|
||||
|
@ -182,11 +187,21 @@ func initialize_instrument_texture() -> void:
|
|||
print('@%dms - Initialized instrument samples texture' % get_ms())
|
||||
|
||||
|
||||
func save_bgm_disassembly(bgm_id: int, filename: String = '') -> void:
|
||||
if not filename:
|
||||
filename = 'output/BGM%02d disassembly.txt' % bgm_id
|
||||
var disassembly: PoolStringArray = MusicRenderer.disassemble_bgm(bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[bgm_id])
|
||||
var file := File.new()
|
||||
file.open(filename, File.WRITE)
|
||||
for line in disassembly:
|
||||
file.store_line(line)
|
||||
file.close()
|
||||
|
||||
|
||||
func queue_prerender_bgm(bgm_id: int) -> void:
|
||||
if not self.initialized_instrument_texture:
|
||||
self.initialize_instrument_texture()
|
||||
var mp = MusicPlayer.new(bgm_tracksets[bgm_id], self.inst_sample_map)
|
||||
var data_and_target_time_and_loops = mp.render_channels(0, 540, RomLoader.snes_data.bgm_instrument_indices[bgm_id])
|
||||
var data_and_target_time_and_loops = MusicRenderer.render_channels(bgm_tracksets[bgm_id], RomLoader.snes_data.bgm_instrument_indices[bgm_id], 'BGM%02d'%bgm_id)
|
||||
var data = data_and_target_time_and_loops[0]
|
||||
var target_time = data_and_target_time_and_loops[1]
|
||||
var target_samples = target_time * 32000
|
||||
|
@ -195,45 +210,60 @@ func queue_prerender_bgm(bgm_id: int) -> void:
|
|||
self.prerendered_bgm_start_and_end_loops[bgm_key] = data_and_target_time_and_loops[2]
|
||||
|
||||
|
||||
|
||||
func render_all_bgm(bgms_to_render: int = 70) -> void:
|
||||
$btn_render.set_disabled(true)
|
||||
self.initialize_instrument_texture()
|
||||
for bgm_id in bgms_to_render:
|
||||
self.queue_prerender_bgm(bgm_id)
|
||||
# self.save_bgm_disassembly(bgm_id)
|
||||
if bgm_id % 10 == 9 and bgm_id < bgms_to_render-1:
|
||||
print('@%dms - Processed %d/%d bgm tracks' % [get_ms(), bgm_id+1, bgms_to_render])
|
||||
print('@%dms - Processed %d bgm tracks and sent to gpu for rendering' % [get_ms(), bgms_to_render])
|
||||
|
||||
|
||||
const save_prerendered_audio := false
|
||||
var print_batch_results := ''
|
||||
func _get_prerendered_audio():
|
||||
self.print_batch_results = ''
|
||||
audio_renderer.get_result()
|
||||
var tracks_rendered := ''
|
||||
while audio_renderer.result_queue:
|
||||
var result = audio_renderer.result_queue.pop_front()
|
||||
var desc = result[0]
|
||||
var rendered_audio := AudioStreamSample.new()
|
||||
rendered_audio.data = result[1]
|
||||
rendered_audio.stereo = true
|
||||
rendered_audio.mix_rate = 32000
|
||||
rendered_audio.format = AudioStreamSample.FORMAT_16_BITS
|
||||
if prerendered_bgm_start_and_end_loops[desc][0] >= 0:
|
||||
rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[desc][0]))
|
||||
rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[desc][1]))
|
||||
rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD
|
||||
self.prerendered_bgms[desc] = rendered_audio
|
||||
if save_prerendered_audio:
|
||||
var error = rendered_audio.save_to_wav('output/rendered_%s.wav'%desc)
|
||||
print('@%dms - Saved render of %s (error code %s)' % [get_ms(), desc, globals.ERROR_CODE_STRINGS[error]])
|
||||
else:
|
||||
# print('@%dms - Rendered %s without saving' % [get_ms(), desc])
|
||||
tracks_rendered = '%s, %s'%[tracks_rendered, desc]
|
||||
if self.queued_bgm_playback == desc:
|
||||
self.audio_player.stream = rendered_audio
|
||||
self.audio_player.play()
|
||||
self.queued_bgm_playback = ''
|
||||
print('@%dms - Rendered %s without saving' % [get_ms(), tracks_rendered.right(2)])
|
||||
print('@%dms - Rendered %s without saving' % [get_ms(), self.print_batch_results.right(2)])
|
||||
|
||||
func _on_render_initial_ready(key: String):
|
||||
# Used for JAOT playback
|
||||
var remaining_samples_and_data = audio_renderer.cached_renders[key]
|
||||
|
||||
var rendered_audio := AudioStreamSample.new()
|
||||
rendered_audio.data = remaining_samples_and_data[1]
|
||||
rendered_audio.stereo = true
|
||||
rendered_audio.mix_rate = 32000
|
||||
rendered_audio.format = AudioStreamSample.FORMAT_16_BITS
|
||||
if prerendered_bgm_start_and_end_loops[key][0] >= 0:
|
||||
rendered_audio.loop_begin = int(round(prerendered_bgm_start_and_end_loops[key][0]))
|
||||
rendered_audio.loop_end = int(round(prerendered_bgm_start_and_end_loops[key][1]))
|
||||
rendered_audio.loop_mode = AudioStreamSample.LOOP_FORWARD
|
||||
self.prerendered_bgms[key] = rendered_audio
|
||||
|
||||
if self.queued_bgm_playback == key:
|
||||
print('@%dms - Rendered initial chunk of %s for immediate playback' % [get_ms(), key])
|
||||
self.audio_player.stream = rendered_audio
|
||||
self.audio_player.play()
|
||||
self.queued_bgm_playback = ''
|
||||
|
||||
func _on_render_complete(key: String):
|
||||
# Used for JAOT playback
|
||||
var remaining_samples_and_data = audio_renderer.cached_renders[key]
|
||||
audio_renderer.cached_renders.erase(key)
|
||||
|
||||
if remaining_samples_and_data[0] != 0: # Should be 0
|
||||
print_debug('render_completed signal for incomplete render! %s has %d remaining samples, should be 0'%[key, remaining_samples_and_data[0]])
|
||||
# Assume _on_render_initial_ready was already called and AudioStreamSample has already been created
|
||||
self.prerendered_bgms[key].data = remaining_samples_and_data[1]
|
||||
if save_prerendered_audio:
|
||||
var error = self.prerendered_bgms[key].save_to_wav('output/rendered_%s.wav'%key)
|
||||
print('@%dms - Saved render of %s (error code %s)' % [get_ms(), key, globals.ERROR_CODE_STRINGS[error]])
|
||||
else:
|
||||
# print('@%dms - Rendered %s without saving' % [get_ms(), key])
|
||||
self.print_batch_results = '%s, %s'%[self.print_batch_results, key]
|
||||
|
||||
|
||||
func get_shader_test_pattern() -> PoolByteArray:
|
||||
|
|
Loading…
Reference in New Issue