diff --git a/scripts/MusicPlayer.gd b/scripts/MusicPlayer.gd index c6359a0..cd32a56 100644 --- a/scripts/MusicPlayer.gd +++ b/scripts/MusicPlayer.gd @@ -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)) diff --git a/scripts/MusicRenderer.gd b/scripts/MusicRenderer.gd new file mode 100644 index 0000000..8b262ac --- /dev/null +++ b/scripts/MusicRenderer.gd @@ -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 diff --git a/test/audio_renderer.gd b/test/audio_renderer.gd index 20127bb..7a566dd 100644 --- a/test/audio_renderer.gd +++ b/test/audio_renderer.gd @@ -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 = [] diff --git a/test/audio_system.gd b/test/audio_system.gd index 4008726..eb7311b 100644 --- a/test/audio_system.gd +++ b/test/audio_system.gd @@ -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: