#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 := 4096 const CURVE_EPSILON := 0.0000001 class NoteEvent: var p_event_start: int # In pulse space var p_note_start: int # For tied notes, this will be earlier than p_event_start and is used for envelope calculations var p_end: int var instrument: int var pitch: int = 0 var pitch_slide: Vector3 var velocity: Vector3 var pan: Vector3 var adsr_attack_rate: int var adsr_decay_rate: int var adsr_decay_total_periods: int var adsr_sustain_rate: 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[i] = entry # Replace existing, this makes sense for the VOLUME -> VOLUME_SLIDE pattern elif self.entries[i].x > pulse: self.entries.insert(i, entry) break func add_slide(pulse_start: int, duration: int, value: float, zero_is_256: bool = false) -> void: if duration == 0: if zero_is_256: duration = 256 else: self.add_point(pulse_start, value) return self.add_point(pulse_start, self.get_pulse(pulse_start), true) self.add_point(pulse_start + duration, value) 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[0].x: return self.default if pulse >= self.entries[-1].x: return self.entries[-1].y for i in l-1: # 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 get_pulse_and_slide(pulse: float) -> Vector3: # [value, end_value, p_end_value] var value = self.default var l := len(self.entries) if l == 0 or pulse < self.entries[0].x: return Vector3(value, value, pulse) if pulse >= self.entries[-1].x: value = self.entries[-1].y return Vector3(value, value, pulse) for i in l-1: # Find first entry beyond if pulse < self.entries[i+1].x: if self.entries[i].z > 0: # ramp_to_next value = range_lerp(pulse, self.entries[i].x, self.entries[i+1].x, self.entries[i].y, self.entries[i+1].y) return Vector3(value, self.entries[i+1].y, self.entries[i+1].x) else: value = self.entries[i].y return Vector3(value, value, pulse) return Vector3(value, value, pulse) # 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 get_gcd(a: int, b: int) -> int: # Greatest Common Divisor of two numbers # Euclidian reduction var c while b: c = b b = a % b a = c return a static func get_lcm(a: int, b: int) -> int: # Least Common Multiple of two numbers return (a * b) / get_gcd(a, b) static func get_lcm_n(values: Array) -> int: # Least Common Multiple of an array of numbers var lcm: int = values.pop_back() for value in values: lcm = get_lcm(lcm, value) return lcm const LOOP_OVERSHOOT := 768 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 all_adsr_decay_total_periods: PoolIntArray = SoundLoader.all_adsr_decay_total_periods var sample_default_adsrs = RomLoader.snes_data.sfx_adsrs + RomLoader.snes_data.bgm_instrument_adsrs # TODO: UNHARDCODE THIS var all_note_events = [] var curve_master_volume := TrackCurve.new(1.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(1.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_pitch_slide := 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 := 255 var current_adsr_attack_rate := 0 var current_adsr_decay_rate := 0 var current_adsr_decay_total_periods := 0 var current_adsr_sustain_rate := 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 var last_note_pretransform_pitch := -2 var last_untied_note_p_start := 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_event_start = p note_event.p_end = infinite_loop_target_pulse # Fake final note event using p_event_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 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_slide(p, slide_duration, new_tempo) EventType.NOTE: var note = event[1] var duration = event[2] var note_event = NoteEvent.new() var curve_p_end: float = p + duration - CURVE_EPSILON note_event.p_event_start = p note_event.p_note_start = p note_event.p_end = p + duration note_event.instrument = current_instrument note_event.pan = curve_pan.get_pulse_and_slide(p) note_event.adsr_attack_rate = current_adsr_attack_rate note_event.adsr_decay_rate = current_adsr_decay_rate note_event.adsr_decay_total_periods = current_adsr_decay_total_periods note_event.adsr_sustain_rate = current_adsr_sustain_rate if note >= 0: # Don't shift or play rests last_note_pretransform_pitch = note # Ties reuse this last_untied_note_p_start = p curve_pitch_slide.add_point(p, 0) # Reset pitch slide note += (12 * current_octave) + current_transpose note_event.pitch = note # pitch_idx #* curve_fine_tuning note_event.velocity = curve_velocity.get_pulse_and_slide(p) # current_velocity elif note == music.NOTE_IS_TIE: if last_note_pretransform_pitch >= 0: note = last_note_pretransform_pitch + (12 * current_octave) + current_transpose note_event.p_note_start = last_untied_note_p_start note_event.pitch = note # pitch_idx #* curve_fine_tuning note_event.pitch_slide = curve_pitch_slide.get_pulse_and_slide(p) note_event.velocity = curve_velocity.get_pulse_and_slide(p) #else: #curve_pitch_slide.add_point(p, 0) # Reset pitch slide on rest channel_note_events.append(note_event) p += duration EventType.VOLUME: var new_velocity: float = event[1]/255.0 curve_velocity.add_point(p, new_velocity) EventType.VOLUME_SLIDE: var slide_duration: int = event[1] var new_velocity: float = event[2]/255.0 curve_velocity.add_slide(p, slide_duration, new_velocity) EventType.PAN: var new_pan = 1.0 - event[1]/127.5 curve_pan.add_point(p, new_pan) EventType.PAN_SLIDE: var new_pan = 1.0 - event[2]/127.5 var slide_duration: int = event[1] curve_pan.add_slide(p, slide_duration, new_pan) EventType.PITCH_SLIDE: var slide_duration: int = event[1] var target_pitch: int = event[2] + curve_pitch_slide.get_pulse(p) # Signed and relative curve_pitch_slide.add_slide(p, slide_duration, target_pitch) 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: curve_pitch_slide.add_point(p, 0) # Reset pitch slide current_instrument = event[1] if current_instrument >= 0x20: current_instrument = inst_map[current_instrument-0x20] - 1 + SoundLoader.SFX_NUM if current_instrument < len(sample_default_adsrs) and current_instrument > 0: var adsr = sample_default_adsrs[current_instrument] current_adsr_attack_rate = adsr.attack_rate current_adsr_decay_rate = adsr.decay_rate current_adsr_sustain_rate = adsr.sustain_rate current_adsr_decay_total_periods = all_adsr_decay_total_periods[adsr.sustain_level] EventType.ADSR_DEFAULT: # TODO - Investigate actual scaling and order if current_instrument < len(sample_default_adsrs) and current_instrument > 0: var adsr = sample_default_adsrs[current_instrument] current_adsr_attack_rate = adsr.attack_rate current_adsr_decay_rate = adsr.decay_rate current_adsr_sustain_rate = adsr.sustain_rate current_adsr_decay_total_periods = all_adsr_decay_total_periods[adsr.sustain_level] EventType.ADSR_ATTACK_RATE: pass #current_adsr_attack_rate = event[1] EventType.ADSR_DECAY_RATE: pass #current_adsr_decay_rate = event[1] EventType.ADSR_SUSTAIN_LEVEL: pass #current_adsr_decay_total_periods = all_adsr_decay_total_periods[event[1]] EventType.ADSR_SUSTAIN_RATE: pass #current_adsr_sustain_rate = 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] var new_echo_volume = event[2] curve_echo_volume.add_slide(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 if len(channel_note_events) > (MAX_NOTE_EVENTS-2): print('%s channel %d has too many note events! %d is more than %d' % [_debug_name, channel, len(channel_note_events), MAX_NOTE_EVENTS-2]) 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 channel_p_ends := 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) channel_loop_p_lengths.append(0) channel_p_ends.append(0) continue var note_event: NoteEvent = all_note_events[channel][-1] var p_end = note_event.p_end if p_end < note_event.p_event_start: # Ends on infinite loop channel_loop_p_returns.append(p_end) channel_loop_p_lengths.append(note_event.p_event_start - p_end) if p_end > highest_channel_p_return: highest_channel_p_return = p_end p_end = note_event.p_event_start else: channel_loop_p_returns.append(-1) channel_loop_p_lengths.append(0) channel_p_ends.append(p_end) 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 + LOOP_OVERSHOOT var target_time_length = curve_master_tempo.get_integral(target_pulse_length) # # DEBUG: calculate LCM of the loop lengths to determine required length for a true loop point # var unique_loop_lengths := [] # # TODO: find common loop pulses # for loop_length in channel_loop_p_lengths: # if loop_length > 0 and not (loop_length in unique_loop_lengths): # unique_loop_lengths.append(loop_length) # if unique_loop_lengths: # var loop_length_lcm = get_lcm_n(unique_loop_lengths) # var p_overall_loop_start = highest_channel_p_return #+ LOOP_OVERSHOOT # var p_overall_loop_end = p_overall_loop_start + loop_length_lcm # print('%s has lcm loop length %d pulses from loop lengths %s, from %d to %d.' % [_debug_name, loop_length_lcm, channel_loop_p_lengths, p_overall_loop_start, p_overall_loop_end]) # else: # print('%s does not loop' % _debug_name) # 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_bytes_t_start_event := StreamPeerBuffer.new() var midi_bytes_t_start_note := StreamPeerBuffer.new() var midi_bytes2 := StreamPeerBuffer.new() var midi_bytes_adsr := StreamPeerBuffer.new() var midi_bytes4 := StreamPeerBuffer.new() var midi_bytes_t_end_volume := StreamPeerBuffer.new() var midi_bytes_t_end_pan := StreamPeerBuffer.new() var midi_bytes_t_end_pitch := 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_event_start if loop_return_note_event_idx < 0 and p >= loop_return_p: loop_return_note_event_idx = event_ptr midi_bytes_t_start_event.put_32(int(curve_master_tempo.get_integral(p + loop_p_offset) * 32000)) midi_bytes_t_start_note.put_32(int(curve_master_tempo.get_integral(event.p_note_start + loop_p_offset) * 32000)) midi_bytes_t_end_volume.put_32(int(curve_master_tempo.get_integral(event.velocity.z + loop_p_offset) * 32000)) midi_bytes_t_end_pan.put_32(int(curve_master_tempo.get_integral(event.pan.z + loop_p_offset) * 32000)) midi_bytes_t_end_pitch.put_32(int(curve_master_tempo.get_integral(event.pitch_slide.z + loop_p_offset) * 32000)) # midi_bytes_t_end.put_32(int(curve_master_tempo.get_integral(event.p_end + loop_p_offset) * 32000)) # t_end midi_bytes2.put_u8(event.instrument) midi_bytes2.put_u8(event.pitch) var vel_u8 := event.velocity * curve_master_volume.get_pulse(p) * 255.0 midi_bytes2.put_u8(int(vel_u8.x)) midi_bytes2.put_u8(int(vel_u8.y)) midi_bytes_adsr.put_u8(event.adsr_attack_rate) midi_bytes_adsr.put_u8(event.adsr_decay_rate) midi_bytes_adsr.put_u8(event.adsr_decay_total_periods) midi_bytes_adsr.put_u8(event.adsr_sustain_rate) var pan_u8 := (event.pan+Vector3.ONE) * 127.5 midi_bytes4.put_u8(int(pan_u8.x)) midi_bytes4.put_u8(int(pan_u8.y)) var pitchslide_s8 := event.pitch_slide * 2 midi_bytes4.put_u8(int(pitchslide_s8.x) + 128) midi_bytes4.put_u8(int(pitchslide_s8.y) + 128) event_ptr += 1 num_notes += 1 # Fill up end of notes array with dummies var last_note_end: int = 0 if events: last_note_end = int(curve_master_tempo.get_integral(events[-1].p_end + loop_p_offset) * 32000) for i in range(num_notes, MAX_NOTE_EVENTS): midi_bytes_t_start_event.put_32(last_note_end) midi_bytes_t_start_note.put_32(last_note_end) midi_bytes_t_end_volume.put_32(0x0FFFFFFF) midi_bytes_t_end_pan.put_32(0x0FFFFFFF) midi_bytes_t_end_pitch.put_32(0x0FFFFFFF) midi_bytes2.put_32(0) midi_bytes_adsr.put_32(0) midi_bytes4.put_32(0) data += midi_bytes_t_start_event.data_array + midi_bytes_t_start_note.data_array + midi_bytes2.data_array + midi_bytes_adsr.data_array + midi_bytes4.data_array + midi_bytes_t_end_volume.data_array + midi_bytes_t_end_pan.data_array + midi_bytes_t_end_pitch.data_array var t_loop_endpoints := Vector2(-1, -1) if highest_channel_p_return >= 0: t_loop_endpoints = Vector2(curve_master_tempo.get_integral(highest_channel_p_return + 100), curve_master_tempo.get_integral(longest_channel_p_end + 100)) return [data, target_time_length, t_loop_endpoints] 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] if event_idx >= 0x20: output.append(print_str + '($%02x) = instrument %02d'%[event_idx, inst_map[event_idx-0x20] - 1]) else: output.append(print_str + 'sfx %d'%event_idx) _: 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